feat(frontend): rebuild chatbot UI to match backend — wider panel, guild context, Indonesian

- 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)
This commit is contained in:
asepharyana
2026-08-03 06:04:25 +07:00
parent 03d59f0738
commit 2b815e156c
5 changed files with 152 additions and 86 deletions
@@ -51,6 +51,10 @@ interface ChatbotContextValue {
sendMessage: (content: string) => Promise<void>;
clearMessages: () => Promise<void>;
isTyping: boolean;
/** Active guild context sent to the backend so answers reference the server */
guildId: string;
setGuildId: (g: string) => void;
}
const ChatbotContext = createContext<ChatbotContextValue | null>(null);
@@ -61,6 +65,7 @@ export function ChatbotProvider({ children }: { children: ReactNode }) {
const [chatOpen, setChatOpen] = useState(false);
const [messages, setMessages] = useState<ChatbotMessage[]>([]);
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}