From 2b815e156c5554b852ba4cdb4fc05ed3b0491ac1 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Mon, 3 Aug 2026 06:04:25 +0700 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20rebuild=20chatbot=20UI=20to?= =?UTF-8?q?=20match=20backend=20=E2=80=94=20wider=20panel,=20guild=20conte?= =?UTF-8?q?xt,=20Indonesian?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Chatbot bubble: 220px → 320px wide, 440px tall; proper header with drag handle; quick-prompt row when chat is closed - ChatPanel: show ALL history (not last 8), Indonesian placeholder/empty state/error copy (backend speaks Indonesian), timestamps (id-ID), clear-history button, typing indicator bubbles, Enter-to-send - Send active guildId as context so backend answers reference the real server (serverInsights path in chatbot.service) - GuildId sync from layout → ChatbotProvider via ChatbotGuildSync - chatbotApi.send(message, guildId) → POST /api/chat {message, context} matching BE zod schema (guildId optional) --- .../frontend/src/app/(dashboard)/layout.tsx | 12 ++- .../src/components/chatbot/chat-panel.tsx | 101 ++++++++++++------ .../components/chatbot/chatbot-container.tsx | 45 ++++---- .../components/chatbot/chatbot-context.tsx | 73 +++++++------ services/frontend/src/lib/api/chatbot.ts | 7 +- 5 files changed, 152 insertions(+), 86 deletions(-) diff --git a/services/frontend/src/app/(dashboard)/layout.tsx b/services/frontend/src/app/(dashboard)/layout.tsx index c4a2a64..0bd260e 100644 --- a/services/frontend/src/app/(dashboard)/layout.tsx +++ b/services/frontend/src/app/(dashboard)/layout.tsx @@ -14,10 +14,19 @@ import { MiniPlayer } from "@/components/media/mini-player"; import { MediaPlayerProvider } from "@/lib/hooks/use-media-player"; import { useWebSocket, WsProvider } from "@/lib/ws/context"; +function ChatbotGuildSync({ guildId }: { guildId: string }) { + const { setGuildId } = useChatbot(); + + useEffect(() => { + setGuildId(guildId); + }, [guildId, setGuildId]); + + return null; +} + function ChatbotExpressionSync() { const ws = useWebSocket(); const { setExpression } = useChatbot(); - useEffect(() => { const unsub1 = ws.on("message_created", (data: any) => { if (data.ai_status === "flagged" || data.ai_status === "warn") { @@ -58,6 +67,7 @@ export default function DashboardLayout({ +
diff --git a/services/frontend/src/components/chatbot/chat-panel.tsx b/services/frontend/src/components/chatbot/chat-panel.tsx index cb9adea..6006d87 100644 --- a/services/frontend/src/components/chatbot/chat-panel.tsx +++ b/services/frontend/src/components/chatbot/chat-panel.tsx @@ -1,6 +1,6 @@ "use client"; -import { Send } from "lucide-react"; +import { Eraser, Send } from "lucide-react"; import { useEffect, useRef } from "react"; import { useChatbot } from "./chatbot-context"; @@ -8,8 +8,17 @@ interface ChatPanelProps { inputRef?: React.RefObject; } +function formatTime(ts: string): string { + const d = new Date(ts); + if (Number.isNaN(d.getTime())) return ""; + return d.toLocaleTimeString("id-ID", { + hour: "2-digit", + minute: "2-digit", + }); +} + export function ChatPanel({ inputRef: externalInputRef }: ChatPanelProps) { - const { messages, sendMessage, isTyping } = useChatbot(); + const { messages, sendMessage, clearMessages, isTyping } = useChatbot(); const listRef = useRef(null); const internalInputRef = useRef(null); const inputRef = externalInputRef ?? internalInputRef; @@ -20,12 +29,12 @@ export function ChatPanel({ inputRef: externalInputRef }: ChatPanelProps) { if (listRef.current) { listRef.current.scrollTop = listRef.current.scrollHeight; } - }, [messages]); + }, [messages, isTyping]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const input = inputRef.current; - if (!input || !input.value.trim()) return; + if (!input || !input.value.trim() || isTyping) return; sendMessage(input.value); input.value = ""; }; @@ -33,44 +42,54 @@ export function ChatPanel({ inputRef: externalInputRef }: ChatPanelProps) { return (
{/* Chat messages */} -
- {messages.length === 0 && ( -
-

- Ask chatbot anything +

+ {messages.length === 0 ? ( +
+

+ Halo! 👋 Aku tau soal server ini — pesan, flag, dan aktivitas. +

+

+ Coba tanya: "Gimana suasana server hari ini?"

- )} - {messages.slice(-8).map((msg, i) => ( -
- ( +
- {msg.content} - -
- ))} +
+ {msg.content} +
+ + {formatTime(msg.timestamp)} + +
+ )) + )} {isTyping && (
-
- +
+ @@ -82,20 +101,32 @@ export function ChatPanel({ inputRef: externalInputRef }: ChatPanelProps) { {/* Input bar */}
+ {messages.length > 0 && ( + + )} diff --git a/services/frontend/src/components/chatbot/chatbot-container.tsx b/services/frontend/src/components/chatbot/chatbot-container.tsx index 874afba..19e1c68 100644 --- a/services/frontend/src/components/chatbot/chatbot-container.tsx +++ b/services/frontend/src/components/chatbot/chatbot-container.tsx @@ -1,6 +1,6 @@ "use client"; -import { Bot, MessageCircle, Minimize2 } from "lucide-react"; +import { Bot, MessageCircle, Minimize2, PanelLeft } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { ChatPanel } from "./chat-panel"; import { ChatbotCanvas } from "./chatbot-canvas"; @@ -34,7 +34,6 @@ export function ChatbotContainer() { // Focus input when chat opens useEffect(() => { if (chatOpen) { - // Small delay for the animation const id = setTimeout(() => inputRef.current?.focus(), 150); return () => clearTimeout(id); } @@ -52,9 +51,9 @@ export function ChatbotContainer() { {/* Main chatbot bubble */}
{minimized ? (
@@ -109,11 +106,23 @@ export function ChatbotContainer() { {/* Chat panel (expandable) */}
+ + {/* Quick prompt row when chat is closed */} + {!chatOpen && ( + + )} )}
diff --git a/services/frontend/src/components/chatbot/chatbot-context.tsx b/services/frontend/src/components/chatbot/chatbot-context.tsx index 4b76151..04d4224 100644 --- a/services/frontend/src/components/chatbot/chatbot-context.tsx +++ b/services/frontend/src/components/chatbot/chatbot-context.tsx @@ -51,6 +51,10 @@ interface ChatbotContextValue { sendMessage: (content: string) => Promise; clearMessages: () => Promise; isTyping: boolean; + + /** Active guild context sent to the backend so answers reference the server */ + guildId: string; + setGuildId: (g: string) => void; } const ChatbotContext = createContext(null); @@ -61,6 +65,7 @@ export function ChatbotProvider({ children }: { children: ReactNode }) { const [chatOpen, setChatOpen] = useState(false); const [messages, setMessages] = useState([]); const [isTyping, setIsTyping] = useState(false); + const [guildId, setGuildId] = useState(""); const historyFetched = useRef(false); // Derived legacy state @@ -104,39 +109,45 @@ export function ChatbotProvider({ children }: { children: ReactNode }) { }); }, []); - const sendMessage = useCallback(async (content: string) => { - if (!content.trim()) return; + const sendMessage = useCallback( + async (content: string) => { + if (!content.trim()) return; - const userMsg: ChatbotMessage = { - role: "user", - content: content.trim(), - timestamp: new Date().toISOString(), - }; - setMessages((prev) => [...prev, userMsg]); - setExpression("listening"); - setIsTyping(true); - - try { - const res = await chatbotApi.send(content.trim()); - const botMsg: ChatbotMessage = { - role: "assistant", - content: res.response, - timestamp: res.timestamp ?? new Date().toISOString(), - }; - setMessages((prev) => [...prev, botMsg]); - setExpression("happy"); - } catch { - const errorMsg: ChatbotMessage = { - role: "assistant", - content: "Sorry, I couldn't process that request. Please try again.", + const userMsg: ChatbotMessage = { + role: "user", + content: content.trim(), timestamp: new Date().toISOString(), }; - setMessages((prev) => [...prev, errorMsg]); - setExpression("sad"); - } finally { - setIsTyping(false); - } - }, []); + setMessages((prev) => [...prev, userMsg]); + setExpression("listening"); + setIsTyping(true); + + try { + // Send active guild as context so the backend can answer with + // real server insights (serverInsights path in chatbot.service). + const res = await chatbotApi.send(content.trim(), guildId); + const botMsg: ChatbotMessage = { + role: "assistant", + content: res.response, + timestamp: res.timestamp ?? new Date().toISOString(), + }; + setMessages((prev) => [...prev, botMsg]); + setExpression("happy"); + } catch { + const errorMsg: ChatbotMessage = { + role: "assistant", + content: + "Maaf, aku lagi gagal nyambung ke server. Coba tanya lagi ya 🙏", + timestamp: new Date().toISOString(), + }; + setMessages((prev) => [...prev, errorMsg]); + setExpression("sad"); + } finally { + setIsTyping(false); + } + }, + [guildId], + ); const clearMessages = useCallback(async () => { try { @@ -163,6 +174,8 @@ export function ChatbotProvider({ children }: { children: ReactNode }) { sendMessage, clearMessages, isTyping, + guildId, + setGuildId, }} > {children} diff --git a/services/frontend/src/lib/api/chatbot.ts b/services/frontend/src/lib/api/chatbot.ts index 83ef8bd..63cc3a1 100644 --- a/services/frontend/src/lib/api/chatbot.ts +++ b/services/frontend/src/lib/api/chatbot.ts @@ -2,8 +2,11 @@ import type { ChatbotHistoryRow, ChatbotResponse } from "@/lib/types"; import { api } from "./client"; export const chatbotApi = { - send: (message: string) => - api.post("/api/chat", { message }), + send: (message: string, guildId?: string) => + api.post("/api/chat", { + message, + context: guildId ? { guildId } : undefined, + }), getHistory: () => api.get<{ history: ChatbotHistoryRow[]; total: number }>(