-complete mockup
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 7m13s
Build Docker Image / Restart Docker Compose (push) Successful in 1s

This commit is contained in:
2025-12-01 14:16:19 +07:00
parent 9136619628
commit 374e25f7ee
2 changed files with 182 additions and 91 deletions

View File

@@ -1,96 +1,122 @@
<div class="fixed bottom-5 right-5 z-50 flex flex-col items-end space-y-4 font-sans">
<!-- หน้าต่างแชท -->
<div *ngIf="isOpen"
class="w-80 h-96 bg-white rounded-xl shadow-2xl flex flex-col overflow-hidden border border-gray-200 animate-fade-in-up">
<!-- Container หลัก: Fixed มุมขวาล่าง -->
<div class="fixed bottom-5 right-5 z-50 flex flex-col items-end space-y-4 font-sans">
<!-- Header -->
<div class="bg-red-800 p-3 flex justify-between items-center text-white shadow-md">
<div class="flex items-center space-x-2">
<div class="relative">
<div class="w-8 h-8 bg-red-700 rounded-full flex items-center justify-center text-xs font-bold border border-red-600">
<!-- หน้าต่างแชท -->
<!-- เราใช้ [style.width] และ [style.height] เพื่อควบคุมขนาดจากการยืดขยาย -->
<div *ngIf="isOpen"
[style.width.px]="isMaximized ? 600 : chatWidth"
[style.height.px]="isMaximized ? 800 : chatHeight"
class="bg-white rounded-xl shadow-2xl flex flex-col overflow-hidden border border-gray-200 animate-fade-in-up relative transition-all duration-100 ease-out"
[class.max-w-[90vw]]="isMaximized"
[class.max-h-[80vh]]="isMaximized">
<!-- ============================== -->
<!-- 1. ส่วนจุดดึงขยาย (Resize Handle) -->
<!-- ============================== -->
<!-- แสดงเฉพาะตอนไม่ได้ Maximize -->
<div *ngIf="!isMaximized"
(mousedown)="startResizing($event)"
class="absolute top-0 left-0 w-6 h-6 z-50 cursor-nw-resize flex items-start justify-start p-1 opacity-0 hover:opacity-100 transition-opacity group">
<!-- Visual Indicator (มุมสามเหลี่ยมเล็กๆ) -->
<div class="w-2 h-2 border-t-2 border-l-2 border-red-400 rounded-tl-sm"></div>
</div>
<!-- Header -->
<div class="bg-red-800 p-3 flex justify-between items-center text-white shadow-md shrink-0 cursor-default select-none">
<div class="flex items-center space-x-2">
<div class="relative">
<div class="w-8 h-8 bg-red-700 rounded-full flex items-center justify-center text-xs font-bold border border-red-600">
AI
</div>
<div class="absolute bottom-0 right-0 w-2.5 h-2.5 bg-green-400 border-2 border-red-800 rounded-full"></div>
</div>
<div class="flex flex-col leading-tight">
<span class="font-bold text-sm">ผู้ช่วยวิเคราะห์ข้อมูล</span>
<span class="text-xs text-red-100">ตอบกลับทันที</span>
</div>
</div>
<!-- ปุ่มควบคุม (Control Buttons) -->
<div class="flex items-center space-x-1">
<!-- 2. ปุ่ม Maximize / Minimize -->
<button (click)="toggleMaximize()" class="hover:bg-red-700 p-1 rounded transition text-red-100 hover:text-white" title="ขยาย/ย่อ">
<!-- Icon: Maximize -->
<svg *ngIf="!isMaximized" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg>
<!-- Icon: Minimize -->
<svg *ngIf="isMaximized" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9V4.5M9 9H4.5M9 9l-6-6M15 9V4.5M15 9h4.5M15 9l6-6M9 15v4.5M9 15H4.5M9 15l-6 6M15 15v4.5M15 15h4.5M15 15l6 6" />
</svg>
</button>
<!-- ปุ่ม Close -->
<button (click)="toggleChat()" class="hover:bg-red-700 p-1 rounded transition text-red-100 hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
</div>
<!-- Chat Body -->
<div class="flex-1 p-4 overflow-y-auto bg-slate-50 space-y-3" #scrollContainer>
<div *ngFor="let msg of messages"
class="flex w-full"
[ngClass]="{'justify-end': msg.isUser, 'justify-start': !msg.isUser}">
<div *ngIf="!msg.isUser" class="w-6 h-6 bg-red-100 rounded-full shrink-0 mr-2 flex items-center justify-center text-xs text-red-800 font-bold self-end mb-1">
AI
</div>
<div class="absolute bottom-0 right-0 w-2.5 h-2.5 bg-green-400 border-2 border-red-800 rounded-full"></div>
</div>
<div class="flex flex-col leading-tight">
<span class="font-bold text-sm">ผู้ช่วยวิเคราะห์ข้อมูล</span>
<span class="text-xs text-red-100">ตอบกลับภายใน 1 นาที</span>
<div [ngClass]="{
'bg-red-800 text-white rounded-tl-2xl rounded-tr-2xl rounded-bl-2xl': msg.isUser,
'bg-white text-gray-800 border border-gray-200 rounded-tl-2xl rounded-tr-2xl rounded-br-2xl': !msg.isUser
}"
class="max-w-[85%] px-4 py-2 text-sm shadow-sm wrap-break-words relative group">
{{ msg.text }}
</div>
</div>
</div>
<button (click)="toggleChat()" class="hover:bg-red-700 p-1 rounded transition text-red-100 hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
<!-- Chat Body -->
<div class="flex-1 p-4 overflow-y-auto bg-slate-50 space-y-3" #scrollContainer>
<div *ngFor="let msg of messages"
class="flex w-full"
[ngClass]="{'justify-end': msg.isUser, 'justify-start': !msg.isUser}">
<!-- Avatar ฝั่งซ้าย (Support) -->
<div *ngIf="!msg.isUser" class="w-6 h-6 bg-red-100 rounded-full shrink-0 mr-2 flex items-center justify-center text-xs text-red-800 font-bold self-end mb-1">
AI
</div>
<div [ngClass]="{
'bg-red-800 text-white rounded-tl-2xl rounded-tr-2xl rounded-bl-2xl': msg.isUser,
'bg-white text-gray-800 border border-gray-200 rounded-tl-2xl rounded-tr-2xl rounded-br-2xl': !msg.isUser
}"
class="max-w-[75%] px-4 py-2 text-sm shadow-sm wrap-break-words relative group">
{{ msg.text }}
<span class="text-[10px] absolute bottom-0 -mb-5 opacity-0 group-hover:opacity-100 transition-opacity text-gray-400 whitespace-nowrap"
[ngClass]="{'right-0': msg.isUser, 'left-0': !msg.isUser}">
<!-- 10:42 AM -->
</span>
</div>
<!-- Footer / Input -->
<div class="p-3 bg-white border-t border-gray-200 flex items-center space-x-2 shrink-0">
<button class="text-gray-400 hover:text-red-800 transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
</svg>
</button>
<input type="text"
[(ngModel)]="newMessage"
(keyup.enter)="sendMessage()"
placeholder="พิมพ์ข้อความ..."
class="flex-1 bg-gray-100 rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-red-500 focus:bg-white transition text-gray-700 placeholder-gray-400">
<button (click)="sendMessage()"
[disabled]="!newMessage.trim()"
class="text-red-950 hover:text-red-800 p-2 transition disabled:opacity-50 disabled:cursor-not-allowed">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
</div>
<!-- Footer / Input -->
<div class="p-3 bg-white border-t border-gray-200 flex items-center space-x-2">
<button class="text-gray-400 hover:text-red-800 transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
</svg>
</button>
<input type="text"
[(ngModel)]="newMessage"
(keyup.enter)="sendMessage()"
placeholder="พิมพ์ข้อความ..."
class="flex-1 bg-gray-100 rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-red-500 focus:bg-white transition text-gray-700 placeholder-gray-400">
<button (click)="sendMessage()"
[disabled]="!newMessage.trim()"
class="text-red-950 hover:text-red-800 p-2 transition disabled:opacity-50 disabled:cursor-not-allowed">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
<!-- Launcher Button -->
<button (click)="toggleChat()"
class="group w-14 h-14 bg-red-800 hover:bg-red-700 text-white rounded-full shadow-lg shadow-red-800/30 flex items-center justify-center transition-all transform hover:scale-110 focus:outline-none ring-4 ring-red-50 hover:ring-red-100 active:scale-95">
<svg *ngIf="isOpen" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<svg *ngIf="!isOpen" xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 transition-transform group-hover:-rotate-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
</svg>
</button>
</div>
<!-- Launcher Button -->
<button (click)="toggleChat()"
class="group w-14 h-14 bg-red-800 hover:bg-red-700 text-white rounded-full shadow-lg shadow-red-800/30 flex items-center justify-center transition-all transform hover:scale-110 focus:outline-none ring-4 ring-red-50 hover:ring-red-100 active:scale-95">
<!-- Notification Badge -->
<!-- <span *ngIf="!isOpen" class="absolute top-0 right-0 -mt-1 -mr-1 flex h-4 w-4">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-4 w-4 bg-red-500 text-[10px] items-center justify-center text-white font-bold">1</span>
</span> -->
<!-- Icon X -->
<svg *ngIf="isOpen" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 transition-transform rotate-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<!-- Icon Chat -->
<svg *ngIf="!isOpen" xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 transition-transform group-hover:-rotate-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
</svg>
</button>
</div>
<!-- Overlay กันกดส่วนอื่นตอนกำลังลาก (Optional) -->
<div *ngIf="isResizing" class="fixed inset-0 z-[60] cursor-nw-resize"></div>

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormGroup, FormControl, Validators } from '@angular/forms';
import { Component, HostListener } from '@angular/core';
@Component({
selector: 'app-chat-widget-component',
@@ -7,30 +8,94 @@ import { Component } from '@angular/core';
styleUrl: './chat-widget-component.css',
})
export class ChatWidgetComponent {
isOpen = false;
isOpen = true;
newMessage = '';
chatForm!: FormGroup;
// State สำหรับจัดการขนาด
isMaximized = false;
isResizing = false;
// ขนาดเริ่มต้น (px)
chatWidth = 320;
chatHeight = 384;
// ตัวแปรสำหรับคำนวณการลาก
private startX = 0;
private startY = 0;
private startWidth = 0;
private startHeight = 0;
constructor() {
this.setupFormControl(); // เรียกใช้ตอนเริ่ม Component
}
setupFormControl() {
this.chatForm = new FormGroup({
message: new FormControl('', [Validators.required])
});
}
messages = [
{ text: 'สวัสดีครับ มีอะไรให้ทีมงานช่วยเหลือไหมครับ? 👋', isUser: false },
{ text: 'สวัสดีครับ AI พร้อมช่วยเหลือครับ 👋', isUser: false },
{ text: 'ลองกดปุ่มขยายที่หัวมุม หรือลากมุมซ้ายบนของกล่องเพื่อปรับขนาดได้เลยครับ', isUser: false },
];
toggleChat() {
this.isOpen = !this.isOpen;
this.isMaximized = false; // Reset ขนาดเมื่อปิด
}
toggleMaximize() {
this.isMaximized = !this.isMaximized;
}
sendMessage() {
if (this.newMessage.trim()) {
// 1. ใส่ข้อความเราลงไป
this.messages.push({ text: this.newMessage, isUser: true });
this.newMessage = '';
// 2. จำลองบอทตอบกลับ (Auto Reply Simulation)
setTimeout(() => {
this.messages.push({
text: 'ขอบคุณที่ติดต่อมาครับ ขณะนี้เจ้าหน้าที่กำลังติดลูกค้าท่านอื่น จะรีบตอบกลับให้เร็วที่สุดครับ',
text: 'รับทราบครับ ระบบกำลังประมวลผล...',
isUser: false
});
}, 1000);
}
}
// --- Logic สำหรับการ Resize (ลากขยาย) ---
startResizing(event: MouseEvent) {
event.preventDefault(); // ป้องกันการเลือก Text
this.isResizing = true;
// บันทึกตำแหน่งเมาส์และขนาดปัจจุบัน
this.startX = event.clientX;
this.startY = event.clientY;
this.startWidth = this.chatWidth;
this.startHeight = this.chatHeight;
}
// ใช้ @HostListener เพื่อดักจับ MouseMove ทั่วทั้ง Window
@HostListener('window:mousemove', ['$event'])
onMouseMove(event: MouseEvent) {
if (!this.isResizing) return;
// คำนวณความต่าง (Delta)
// หมายเหตุ: ลากไปทางซ้าย (ค่า X น้อยลง) ต้องทำให้กว้างขึ้น -> ใช้ startX - clientX
// ลากไปข้างบน (ค่า Y น้อยลง) ต้องทำให้สูงขึ้น -> ใช้ startY - clientY
const deltaX = this.startX - event.clientX;
const deltaY = this.startY - event.clientY;
// กำหนดขนาดใหม่ (จำกัดขนาดต่ำสุดไม่ให้เล็กเกินไป)
this.chatWidth = Math.max(300, this.startWidth + deltaX);
this.chatHeight = Math.max(350, this.startHeight + deltaY);
}
@HostListener('window:mouseup')
stopResizing() {
this.isResizing = false;
}
}