From 7904d7f38dad34c06ea9087fca07acd02b4f145a Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Wed, 3 Jun 2026 15:01:34 +0700 Subject: [PATCH] ``` --- services/backend/src/http/app.ts | 2 + .../mascot-chat/mascot-chat.controller.ts | 89 +++++++ .../modules/mascot-chat/mascot-chat.routes.ts | 17 ++ .../mascot-chat/mascot-chat.service.ts | 228 ++++++++++++++++++ services/frontend/src/App.tsx | 20 +- .../src/shared/hooks/useMascotChat.ts | 148 +++--------- .../frontend/src/widgets/DashboardLayout.tsx | 12 +- services/frontend/src/widgets/Sidebar.tsx | 45 +++- .../src/widgets/mascot/MascotChatbot.tsx | 34 +-- 9 files changed, 422 insertions(+), 173 deletions(-) create mode 100644 services/backend/src/modules/mascot-chat/mascot-chat.controller.ts create mode 100644 services/backend/src/modules/mascot-chat/mascot-chat.routes.ts create mode 100644 services/backend/src/modules/mascot-chat/mascot-chat.service.ts diff --git a/services/backend/src/http/app.ts b/services/backend/src/http/app.ts index dfb2a17..bff9c24 100644 --- a/services/backend/src/http/app.ts +++ b/services/backend/src/http/app.ts @@ -10,6 +10,7 @@ import { createAnalyticsRouter } from "../modules/analytics/analytics.routes.js" import { createAuthRouter } from "../modules/auth/auth.routes.js"; import { createConfigRouter } from "../modules/config/config.routes.js"; import { createHealthRouter } from "../modules/health/health.routes.js"; +import { createMascotChatRouter } from "../modules/mascot-chat/mascot-chat.routes.js"; import { createMediaRouter } from "../modules/media/media.routes.js"; import { createMessagesRouter } from "../modules/messages/messages.routes.js"; import { createRecordingsRouter } from "../modules/recordings/recordings.routes.js"; @@ -66,6 +67,7 @@ export function createHttpApp(): Express { app.use("/api", createMessagesRouter()); app.use("/api", createAnalysisRouter()); app.use("/api", createAnalyticsRouter()); + app.use("/api", createMascotChatRouter()); app.use("/api", createMediaRouter()); app.use("/api", createVoiceRouter()); app.use("/api", createRecordingsRouter()); diff --git a/services/backend/src/modules/mascot-chat/mascot-chat.controller.ts b/services/backend/src/modules/mascot-chat/mascot-chat.controller.ts new file mode 100644 index 0000000..a343196 --- /dev/null +++ b/services/backend/src/modules/mascot-chat/mascot-chat.controller.ts @@ -0,0 +1,89 @@ +import type { Request, Response } from "express"; +import { createChildLogger } from "@bete/shared/logger"; +import { mascotChatService } from "./mascot-chat.service.js"; + +const logger = createChildLogger("mascot-chat.controller"); + +export async function handleMascotChat(req: Request, res: Response) { + try { + const { message, context } = req.body; + + if (!message || typeof message !== "string") { + return res.status(400).json({ + error: "INVALID_INPUT", + message: "Message is required and must be a string", + }); + } + + // Get user ID from auth middleware (if available) + const userId = (req as any).userId || "anonymous"; + + logger.debug( + { userId, messageLength: message.length, context }, + "Received mascot chat message" + ); + + // Process message & generate response + const response = await mascotChatService.processMessage(message, context, userId); + + // Save conversation to database + await mascotChatService.saveConversation({ + userId, + userMessage: message, + mascotResponse: response, + context, + timestamp: new Date(), + }); + + logger.info({ userId }, "Mascot chat processed successfully"); + + res.status(200).json({ + response, + timestamp: new Date().toISOString(), + }); + } catch (error) { + logger.error({ error }, "Error processing mascot chat"); + res.status(500).json({ + error: "INTERNAL_SERVER_ERROR", + message: "Failed to process mascot chat", + }); + } +} + +export async function getMascotChatHistory(req: Request, res: Response) { + try { + const userId = (req as any).userId || "anonymous"; + const limit = Math.min(parseInt(req.query.limit as string) || 50, 100); + + const history = await mascotChatService.getChatHistory(userId, limit); + + res.status(200).json({ + history, + total: history.length, + }); + } catch (error) { + logger.error({ error }, "Error fetching chat history"); + res.status(500).json({ + error: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch chat history", + }); + } +} + +export async function clearMascotChatHistory(req: Request, res: Response) { + try { + const userId = (req as any).userId || "anonymous"; + + await mascotChatService.clearChatHistory(userId); + + res.status(200).json({ + message: "Chat history cleared successfully", + }); + } catch (error) { + logger.error({ error }, "Error clearing chat history"); + res.status(500).json({ + error: "INTERNAL_SERVER_ERROR", + message: "Failed to clear chat history", + }); + } +} diff --git a/services/backend/src/modules/mascot-chat/mascot-chat.routes.ts b/services/backend/src/modules/mascot-chat/mascot-chat.routes.ts new file mode 100644 index 0000000..b6a50f6 --- /dev/null +++ b/services/backend/src/modules/mascot-chat/mascot-chat.routes.ts @@ -0,0 +1,17 @@ +import express, { type Router } from "express"; +import { asyncHandler } from "../../shared/middlewares/index.js"; +import { + clearMascotChatHistory, + getMascotChatHistory, + handleMascotChat, +} from "./mascot-chat.controller.js"; + +export function createMascotChatRouter(): Router { + const router = express.Router(); + + router.post("/mascot/chat", asyncHandler(handleMascotChat)); + router.get("/mascot/chat/history", asyncHandler(getMascotChatHistory)); + router.delete("/mascot/chat/history", asyncHandler(clearMascotChatHistory)); + + return router; +} diff --git a/services/backend/src/modules/mascot-chat/mascot-chat.service.ts b/services/backend/src/modules/mascot-chat/mascot-chat.service.ts new file mode 100644 index 0000000..f1ab476 --- /dev/null +++ b/services/backend/src/modules/mascot-chat/mascot-chat.service.ts @@ -0,0 +1,228 @@ +import { createChildLogger } from "@bete/shared/logger"; +import { getPool } from "../../shared/database/index.js"; + +const logger = createChildLogger("mascot-chat.service"); + +export interface MascotChatContext { + messageCount?: number; + activeParticipants?: number; + lastActivity?: string; + topicsDiscussed?: string[]; + guildId?: string; + channelId?: string; +} + +export interface SaveConversationInput { + userId: string; + userMessage: string; + mascotResponse: string; + context?: MascotChatContext; + timestamp: Date; +} + +export interface MascotChatHistoryRow { + id: string; + user_id: string; + user_message: string; + mascot_response: string; + context: MascotChatContext | null; + created_at: string; +} + +class MascotChatService { + private initialized = false; + + async processMessage( + message: string, + context: MascotChatContext | undefined, + userId: string, + ): Promise { + await this.ensureSchema(); + + const recentContext = await this.getRecentConversationContext(userId); + const serverInsights = await this.getServerInsights(context); + return this.generateResponse(message, context, serverInsights, recentContext); + } + + async saveConversation(input: SaveConversationInput): Promise { + await this.ensureSchema(); + const pool = getPool(); + + await pool.query( + ` + INSERT INTO mascot_chat_messages + (user_id, user_message, mascot_response, context, created_at) + VALUES ($1, $2, $3, $4::jsonb, $5) + `, + [ + input.userId, + input.userMessage, + input.mascotResponse, + JSON.stringify(input.context ?? {}), + input.timestamp.toISOString(), + ], + ); + } + + async getChatHistory( + userId: string, + limit: number, + ): Promise { + await this.ensureSchema(); + const pool = getPool(); + + const { rows } = await pool.query( + ` + SELECT id, user_id, user_message, mascot_response, context, created_at + FROM mascot_chat_messages + WHERE user_id = $1 + ORDER BY created_at DESC + LIMIT $2 + `, + [userId, limit], + ); + + return rows.reverse(); + } + + async clearChatHistory(userId: string): Promise { + await this.ensureSchema(); + const pool = getPool(); + await pool.query(`DELETE FROM mascot_chat_messages WHERE user_id = $1`, [ + userId, + ]); + } + + private async ensureSchema(): Promise { + if (this.initialized) return; + + const pool = getPool(); + await pool.query(` + CREATE TABLE IF NOT EXISTS mascot_chat_messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id TEXT NOT NULL, + user_message TEXT NOT NULL, + mascot_response TEXT NOT NULL, + context JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + `); + await pool.query(` + CREATE INDEX IF NOT EXISTS idx_mascot_chat_messages_user_created + ON mascot_chat_messages (user_id, created_at DESC) + `); + + this.initialized = true; + logger.info("Mascot chat schema ready"); + } + + private async getRecentConversationContext(userId: string): Promise { + const history = await this.getChatHistory(userId, 3); + return history.flatMap((row) => [ + `User: ${row.user_message}`, + `Mascot: ${row.mascot_response}`, + ]); + } + + private async getServerInsights(context?: MascotChatContext) { + const pool = getPool(); + const guildId = context?.guildId; + const channelId = context?.channelId; + + try { + const params: string[] = []; + const clauses: string[] = []; + if (guildId) { + params.push(guildId); + clauses.push(`guild_id = $${params.length}`); + } + if (channelId) { + params.push(channelId); + clauses.push(`channel_id = $${params.length}`); + } + const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : ""; + + const { rows } = await pool.query<{ + total_messages: number; + active_users: number; + flagged: number; + warned: number; + }>( + ` + SELECT + COUNT(*)::int AS total_messages, + COUNT(DISTINCT user_id)::int AS active_users, + COUNT(*) FILTER (WHERE ai_status = 'flagged')::int AS flagged, + COUNT(*) FILTER (WHERE ai_status = 'warn')::int AS warned + FROM messages + ${where} + `, + params, + ); + + return rows[0] ?? { + total_messages: 0, + active_users: 0, + flagged: 0, + warned: 0, + }; + } catch (error) { + logger.warn({ error }, "Failed to load mascot server insights"); + return { + total_messages: context?.messageCount ?? 0, + active_users: context?.activeParticipants ?? 0, + flagged: 0, + warned: 0, + }; + } + } + + private generateResponse( + input: string, + context: MascotChatContext | undefined, + insights: { + total_messages: number; + active_users: number; + flagged: number; + warned: number; + }, + recentContext: string[], + ): string { + const lower = input.toLowerCase(); + const messageCount = insights.total_messages || context?.messageCount || 0; + const activeUsers = insights.active_users || context?.activeParticipants || 0; + + if (lower.includes("ringkasan") || lower.includes("summary")) { + return `Aku rangkum ya ✨ Ada ${messageCount} pesan dari ${activeUsers} user aktif. Moderasi menemukan ${insights.flagged} flagged dan ${insights.warned} warning. Kesimpulannya: obrolan sedang ${messageCount > 50 ? "ramai" : "cukup tenang"}, dan aku sarankan fokus ke pesan yang punya status warn/flagged dulu.`; + } + + if (lower.includes("berapa") || lower.includes("jumlah")) { + if (lower.includes("pesan")) { + return `Ada ${messageCount} pesan yang tercatat di konteks ini 📊`; + } + if (lower.includes("orang") || lower.includes("user")) { + return `Ada ${activeUsers} user aktif yang ikut dalam obrolan ini 👥`; + } + } + + if (lower.includes("flag") || lower.includes("bahaya") || lower.includes("moderasi")) { + return `Status moderasi: ${insights.flagged} pesan flagged dan ${insights.warned} pesan warning. Kalau mau aman, mulai review dari daftar flagged karena itu prioritas tertinggi 🚨`; + } + + if (lower.includes("saran") || lower.includes("apa yang harus")) { + return `Saran mascot: 1) review pesan flagged, 2) cek user paling aktif di Analytics, 3) pantau channel dengan traffic tinggi, 4) kalau obrolan mulai panas, lakukan follow-up manual sebelum eskalasi 🔎`; + } + + if (lower.includes("halo") || lower.includes("hai") || lower.includes("hi")) { + return `Halo! Aku siap bantu baca situasi chat. Kamu bisa tanya "ringkasan obrolan", "berapa pesan", atau "ada yang perlu dimoderasi?" 😊`; + } + + const previousHint = recentContext.length + ? ` Aku juga mengingat konteks chat mascot sebelumnya (${Math.ceil(recentContext.length / 2)} percakapan terakhir).` + : ""; + + return `Menurutku, pertanyaan "${input}" berkaitan dengan kondisi obrolan saat ini. Data cepat: ${messageCount} pesan, ${activeUsers} user aktif, ${insights.flagged} flagged.${previousHint} Coba tanya lebih spesifik seperti "ringkasan", "moderasi", atau "saran" ya.`; + } +} + +export const mascotChatService = new MascotChatService(); diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx index 84bdccc..687c4b5 100644 --- a/services/frontend/src/App.tsx +++ b/services/frontend/src/App.tsx @@ -17,13 +17,11 @@ import { } from "./shared/api/client"; import { useAudioPlayback } from "./shared/hooks/useAudioPlayback"; import { useAudioTransmit } from "./shared/hooks/useAudioTransmit"; -import { useMascotChat } from "./shared/hooks/useMascotChat"; import { useUIState } from "./shared/hooks/useUIState"; import { Skeleton } from "./shared/ui"; import { MobileTabBar } from "./shared/ui/MobileTabBar"; import { useDashboardSocket } from "./shared/ws/socket"; import { DashboardLayout } from "./widgets/DashboardLayout"; -import { MascotChatbot } from "./widgets/mascot/MascotChatbot"; const AnalyticsPanel = lazy(() => import("./features/analytics").then((module) => ({ @@ -62,15 +60,6 @@ export default function App() { !!localStorage.getItem("admin-password"), ); const [monitorGuildId, setMonitorGuildId] = useState(""); - const [isMascotChatOpen, setIsMascotChatOpen] = useState(false); - - // Mascot chat hook with message context - const mascotChat = useMascotChat({ - messageCount: messages.messages.length, - activeParticipants: new Set(messages.messages.map((m) => m.user_id)).size, - lastActivity: messages.messages.length > 0 ? "Active" : "Idle", - topicsDiscussed: ["Analytics", "Conversation", "Insights"], - }); const audio = useAudioPlayback(); const activeTab = uiState.activeTab || "live"; @@ -174,6 +163,8 @@ export default function App() { voiceStatus={voice.voiceStatus} onTabChange={(tab) => patchUIState({ activeTab: tab })} recentMessages={messages.messages} + guildId={monitorGuildId} + channelId={uiState.selectedTextChannel || uiState.selectedVoiceChannel || undefined} > {activeTab === "live" ? ( !isAuthenticated ? ( @@ -246,13 +237,6 @@ export default function App() { onTabChange={(tab) => patchUIState({ activeTab: tab })} /> - ); } diff --git a/services/frontend/src/shared/hooks/useMascotChat.ts b/services/frontend/src/shared/hooks/useMascotChat.ts index c9aca61..faf67b8 100644 --- a/services/frontend/src/shared/hooks/useMascotChat.ts +++ b/services/frontend/src/shared/hooks/useMascotChat.ts @@ -1,15 +1,12 @@ import { useCallback, useState } from "react"; -/** - * useMascotChat — Hook untuk handle mascot chatbot responses - * Dapat di-extend dengan Discord Gateway atau API backend - */ - -interface ChatContext { +export interface ChatContext { messageCount: number; activeParticipants: number; lastActivity: string; topicsDiscussed: string[]; + guildId?: string; + channelId?: string; } export function useMascotChat(context?: ChatContext) { @@ -17,14 +14,25 @@ export function useMascotChat(context?: ChatContext) { const handleSendMessage = useCallback( async (message: string): Promise => { - // Simulate API call delay - await new Promise((resolve) => setTimeout(resolve, 500)); + try { + const response = await fetch("/api/mascot/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ message, context }), + }); - // For now, generate response based on keywords - // Later dapat di-replace dengan actual AI backend atau Discord integration - return generateIntelligentResponse(message, context); + if (!response.ok) { + throw new Error(`Mascot backend responded with ${response.status}`); + } + + const data = (await response.json()) as { response?: string }; + return data.response || fallbackResponse(message, context); + } catch (error) { + console.warn("Mascot backend unavailable, using fallback", error); + return fallbackResponse(message, context); + } }, - [context] + [context], ); return { @@ -34,120 +42,20 @@ export function useMascotChat(context?: ChatContext) { }; } -/** - * Intelligent response generator - * Can be extended to call backend API, Discord Gateway, atau AI service - */ -function generateIntelligentResponse( - input: string, - context?: ChatContext -): string { +function fallbackResponse(input: string, context?: ChatContext): string { const lower = input.toLowerCase(); - // Analytics-related questions - if ( - lower.includes("berapa") || - lower.includes("jumlah") || - lower.includes("total") - ) { - if (lower.includes("pesan")) { - return `📊 Ada ${context?.messageCount || 0} pesan dalam conversation. Cukup aktif ya! Mau tahu siapa yang paling banyak chat?`; - } - if (lower.includes("orang") || lower.includes("partisipan")) { - return `👥 Ada ${context?.activeParticipants || 0} orang yang aktif chat. Mereka bekerja sama dengan baik!`; - } + if (lower.includes("ringkasan") || lower.includes("summary")) { + return `Aku rangkum cepat ya ✨ Ada ${context?.messageCount || 0} pesan dari ${context?.activeParticipants || 0} user aktif. Backend belum bisa dihubungi, jadi ini ringkasan lokal sementara.`; } - // Insights-related questions - if ( - lower.includes("insight") || - lower.includes("ringkasan") || - lower.includes("summary") - ) { - return `📈 Dari yang aku lihat: -• Activity Level: ${context?.lastActivity || "Tinggi"} -• Top Topics: ${context?.topicsDiscussed?.join(", ") || "General discussion"} -• Engagement: Very Good! 🎯`; + if (lower.includes("berapa") && lower.includes("pesan")) { + return `Ada ${context?.messageCount || 0} pesan di konteks dashboard saat ini 📊`; } - // Recommendations - if (lower.includes("saran") || lower.includes("rekomendasi")) { - return `💡 Rekomendasi aku: -1. Tingkatkan engagement dengan more interactive discussions -2. Dokumentasikan insights untuk future reference -3. Libatkan semua partisipan dalam decision making -4. Monitor trends untuk continuous improvement - -Bagus banget perkembangannya! 🚀`; + if (lower.includes("berapa") && (lower.includes("orang") || lower.includes("user"))) { + return `Ada ${context?.activeParticipants || 0} user aktif yang terdeteksi 👥`; } - // Help/Info - if ( - lower.includes("bantuan") || - lower.includes("apa aja") || - lower.includes("bisa") - ) { - return `🤖 Aku bisa membantu dengan: -• Analytics & Insights -• Conversation Summaries -• Participant Analysis -• Trend Detection -• Recommendations -• General Q&A - -Tanya aja yang pengen kamu tahu! 😊`; - } - - // Greeting - if ( - lower.includes("halo") || - lower.includes("hi") || - lower.includes("hey") || - lower.includes("pagi") - ) { - return `Halo! 👋 Apa kabar? Ada yang bisa aku bantu tentang conversation ini?`; - } - - // Default intelligent response - return `Interessant! "${input}" - itu observation yang valid. Dari analytics, ini berhubungan dengan conversation patterns yang kami track. Ada follow-up question? 🎯`; + return `Aku belum bisa menghubungi backend, tapi dari konteks lokal ada ${context?.messageCount || 0} pesan dan ${context?.activeParticipants || 0} user aktif. Coba tanya "ringkasan obrolan" atau "berapa pesan" ya.`; } - -/** - * Backend integration hook - * Uncomment dan modify untuk integrate dengan actual backend/Discord Gateway - */ - -/* -export async function callMascotAIBackend(message: string, context?: ChatContext): Promise { - try { - const response = await fetch('/api/mascot/chat', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message, context }), - }); - - if (!response.ok) throw new Error('Backend error'); - const data = await response.json(); - return data.response; - } catch (error) { - console.error('Error calling mascot backend:', error); - return generateIntelligentResponse(message, context); - } -} - -export async function callDiscordGateway(message: string, guildId: string): Promise { - // Call Discord Gateway untuk mendapat context lebih kaya - // Implementasi akan bergantung pada Discord API integration - try { - const response = await fetch('/api/discord/guild-context', { - method: 'POST', - body: JSON.stringify({ guildId, query: message }), - }); - const context = await response.json(); - return generateIntelligentResponse(message, context); - } catch (error) { - console.error('Error calling Discord Gateway:', error); - return generateIntelligentResponse(message); - } -} -*/ diff --git a/services/frontend/src/widgets/DashboardLayout.tsx b/services/frontend/src/widgets/DashboardLayout.tsx index f4fd9a4..158dd2f 100644 --- a/services/frontend/src/widgets/DashboardLayout.tsx +++ b/services/frontend/src/widgets/DashboardLayout.tsx @@ -1,8 +1,7 @@ import { motion } from "framer-motion"; import type { ReactNode } from "react"; -import { useMemo } from "react"; import type { DashboardTab } from "../entities/ui/types"; -import type { VoiceStatus } from "../shared/api/client"; +import type { MessageRecord, VoiceStatus } from "../shared/api/client"; import { fadeSlideUp } from "../shared/hooks/useFramerStagger"; import { useMascotSummary } from "../shared/hooks/useMascotSummary"; import type { WsStatus } from "../shared/ws/socket"; @@ -16,7 +15,9 @@ interface DashboardLayoutProps { voiceStatus: VoiceStatus; onTabChange: (tab: DashboardTab) => void; children: ReactNode; - recentMessages?: any[]; + recentMessages?: MessageRecord[]; + guildId?: string; + channelId?: string; } export function DashboardLayout({ @@ -26,6 +27,8 @@ export function DashboardLayout({ onTabChange, children, recentMessages = [], + guildId, + channelId, }: DashboardLayoutProps) { // Generate mascot summary from recent messages const mascotSummary = useMascotSummary({ @@ -42,6 +45,9 @@ export function DashboardLayout({ activeTab={activeTab} onTabChange={onTabChange} mascotChatMessage={mascotSummary} + recentMessages={recentMessages} + guildId={guildId} + channelId={channelId} />
= @@ -17,6 +20,9 @@ interface SidebarProps { onTabChange: (tab: DashboardTab) => void; collapsed?: boolean; mascotChatMessage?: string; + recentMessages?: MessageRecord[]; + guildId?: string; + channelId?: string; } export function Sidebar({ @@ -24,8 +30,19 @@ export function Sidebar({ onTabChange, collapsed = true, mascotChatMessage = "", + recentMessages = [], + guildId, + channelId, }: SidebarProps) { const [showChat, setShowChat] = useState(false); + const mascotChat = useMascotChat({ + messageCount: recentMessages.length, + activeParticipants: new Set(recentMessages.map((message) => message.user_id)).size, + lastActivity: recentMessages.length > 0 ? "Active" : "Idle", + topicsDiscussed: ["Messages", "Moderation", "Analytics"], + guildId, + channelId, + }); useEffect(() => { if (mascotChatMessage) { @@ -35,7 +52,7 @@ export function Sidebar({ return ( diff --git a/services/frontend/src/widgets/mascot/MascotChatbot.tsx b/services/frontend/src/widgets/mascot/MascotChatbot.tsx index 1e5bca5..64bc358 100644 --- a/services/frontend/src/widgets/mascot/MascotChatbot.tsx +++ b/services/frontend/src/widgets/mascot/MascotChatbot.tsx @@ -1,5 +1,5 @@ -import { motion, AnimatePresence } from "framer-motion"; -import { Send, X, MessageCircle, Minimize2, Maximize2 } from "lucide-react"; +import { AnimatePresence, motion } from "framer-motion"; +import { Maximize2, MessageCircle, Minimize2, Send, X } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { cn } from "../../shared/lib/utils"; @@ -12,23 +12,21 @@ export interface ChatMessage { } interface MascotChatbotProps { - onOpen?: () => void; onClose?: () => void; isOpen?: boolean; - onSetIsOpen?: (isOpen: boolean) => void; onSendMessage?: (message: string) => Promise; mascotName?: string; mascotAvatar?: string; + className?: string; } export function MascotChatbot({ - onOpen, onClose, isOpen = false, - onSetIsOpen, onSendMessage, mascotName = "Mascot", mascotAvatar = "https://raw.githubusercontent.com/IMPHNEN/imphnen-frontend-service/develop/apps/dimentorin/public/image/mascot-1.png", + className, }: MascotChatbotProps) { const [messages, setMessages] = useState([ { @@ -51,7 +49,7 @@ export function MascotChatbot({ scrollToBottom(); }, [messages]); - const handleSendMessage = async (e: React.FormEvent) => { + const handleSendMessage = async (e: { preventDefault: () => void }) => { e.preventDefault(); if (!input.trim() || loading) return; @@ -98,22 +96,7 @@ export function MascotChatbot({ } }; - if (!isOpen) { - return ( - { - onSetIsOpen?.(true); - onOpen?.(); - }} - className="fixed bottom-6 right-6 bg-gradient-to-br from-primary to-primary/80 text-white rounded-full p-4 shadow-lg hover:shadow-xl transition-all" - title="Buka chat mascot" - > - - - ); - } + if (!isOpen) return null; return ( @@ -122,8 +105,9 @@ export function MascotChatbot({ animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: 20, scale: 0.95 }} className={cn( - "fixed bottom-6 right-6 w-96 bg-white rounded-2xl shadow-2xl border border-primary/10 overflow-hidden flex flex-col", - isMinimized ? "h-16" : "h-[600px]" + "w-96 bg-white rounded-2xl shadow-2xl border border-primary/10 overflow-hidden flex flex-col", + isMinimized ? "h-16" : "h-[520px]", + className, )} > {/* Header */}