diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx index c1821ba..84bdccc 100644 --- a/services/frontend/src/App.tsx +++ b/services/frontend/src/App.tsx @@ -17,11 +17,13 @@ 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) => ({ @@ -60,6 +62,15 @@ 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"; @@ -235,6 +246,13 @@ 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 new file mode 100644 index 0000000..c9aca61 --- /dev/null +++ b/services/frontend/src/shared/hooks/useMascotChat.ts @@ -0,0 +1,153 @@ +import { useCallback, useState } from "react"; + +/** + * useMascotChat — Hook untuk handle mascot chatbot responses + * Dapat di-extend dengan Discord Gateway atau API backend + */ + +interface ChatContext { + messageCount: number; + activeParticipants: number; + lastActivity: string; + topicsDiscussed: string[]; +} + +export function useMascotChat(context?: ChatContext) { + const [isOpen, setIsOpen] = useState(false); + + const handleSendMessage = useCallback( + async (message: string): Promise => { + // Simulate API call delay + await new Promise((resolve) => setTimeout(resolve, 500)); + + // For now, generate response based on keywords + // Later dapat di-replace dengan actual AI backend atau Discord integration + return generateIntelligentResponse(message, context); + }, + [context] + ); + + return { + isOpen, + setIsOpen, + handleSendMessage, + }; +} + +/** + * Intelligent response generator + * Can be extended to call backend API, Discord Gateway, atau AI service + */ +function generateIntelligentResponse( + 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!`; + } + } + + // 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! 🎯`; + } + + // 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! 🚀`; + } + + // 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? 🎯`; +} + +/** + * 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/mascot/MascotChatbot.tsx b/services/frontend/src/widgets/mascot/MascotChatbot.tsx new file mode 100644 index 0000000..1e5bca5 --- /dev/null +++ b/services/frontend/src/widgets/mascot/MascotChatbot.tsx @@ -0,0 +1,298 @@ +import { motion, AnimatePresence } from "framer-motion"; +import { Send, X, MessageCircle, Minimize2, Maximize2 } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { cn } from "../../shared/lib/utils"; + +export interface ChatMessage { + id: string; + role: "user" | "mascot"; + content: string; + timestamp: number; + avatar?: string; +} + +interface MascotChatbotProps { + onOpen?: () => void; + onClose?: () => void; + isOpen?: boolean; + onSetIsOpen?: (isOpen: boolean) => void; + onSendMessage?: (message: string) => Promise; + mascotName?: string; + mascotAvatar?: 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", +}: MascotChatbotProps) { + const [messages, setMessages] = useState([ + { + id: "init-1", + role: "mascot", + content: "Halo! 👋 Saya mascot mu. Ada yang bisa aku bantu tentang conversation atau analytics?", + timestamp: Date.now(), + }, + ]); + const [input, setInput] = useState(""); + const [loading, setLoading] = useState(false); + const [isMinimized, setIsMinimized] = useState(false); + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + const handleSendMessage = async (e: React.FormEvent) => { + e.preventDefault(); + if (!input.trim() || loading) return; + + const userMessage: ChatMessage = { + id: `user-${Date.now()}`, + role: "user", + content: input.trim(), + timestamp: Date.now(), + }; + + setMessages((prev) => [...prev, userMessage]); + setInput(""); + setLoading(true); + + try { + let response = "Aku sedang memproses pertanyaanmu..."; + + if (onSendMessage) { + response = await onSendMessage(input.trim()); + } else { + // Default mascot responses + response = generateMascotResponse(input.trim(), messages); + } + + const mascotMessage: ChatMessage = { + id: `mascot-${Date.now()}`, + role: "mascot", + content: response, + timestamp: Date.now(), + }; + + setMessages((prev) => [...prev, mascotMessage]); + } catch (error) { + console.error("Error sending message:", error); + const errorMessage: ChatMessage = { + id: `mascot-error-${Date.now()}`, + role: "mascot", + content: "Maaf, ada error saat aku memproses. Coba lagi ya! 😅", + timestamp: Date.now(), + }; + setMessages((prev) => [...prev, errorMessage]); + } finally { + setLoading(false); + } + }; + + 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" + > + + + ); + } + + return ( + + + {/* Header */} +
+
+
+ +
+
+

{mascotName}

+

+ {loading ? "Mengetik..." : "Online"} +

+
+
+
+ setIsMinimized(!isMinimized)} + className="p-1.5 hover:bg-white/20 rounded-lg transition-colors" + title={isMinimized ? "Maximize" : "Minimize"} + > + {isMinimized ? ( + + ) : ( + + )} + + { + onClose?.(); + }} + className="p-1.5 hover:bg-white/20 rounded-lg transition-colors" + title="Close" + > + + +
+
+ + {/* Messages */} + {!isMinimized && ( + <> +
+ {messages.map((message) => ( + + {message.role === "mascot" && ( + {mascotName} + )} +
+ {message.content} +
+
+ ))} + {loading && ( + + {mascotName} +
+
+ + + +
+
+
+ )} +
+
+ + {/* Input */} +
+
+ setInput(e.target.value)} + placeholder="Tanya mascot..." + disabled={loading} + className="flex-1 px-3 py-2 rounded-lg border border-primary/20 focus:outline-none focus:border-primary/50 text-sm disabled:opacity-50" + /> + + + +
+
+ + )} + + + ); +} + +// Default mascot responses based on keywords +function generateMascotResponse(input: string, messages: ChatMessage[]): string { + const lowerInput = input.toLowerCase(); + const responseMap: Record = { + halo: "Halo juga! 👋 Senang ketemu kamu. Ada yang bisa aku bantu?", + terima: "Sama-sama! 😊", + apa: "Aku adalah mascot virtual yang membantu kamu memahami conversation dan analytics. Tanya aku apa saja!", + siapa: "Aku mascot mu yang baik hati! Siap membantu dengan insights tentang chat dan analytics.", + chat: "Setiap chat yang terjadi di sini aku analisis untuk memberikan insights yang berguna. Keren kan? 😎", + pesan: "Aku bisa memberikan ringkasan tentang pesan-pesan yang dikirim, siapa yang paling aktif, dan topik populer!", + analitik: + "Analytics menunjukkan pola conversation, waktu aktif, partisipan utama, dan banyak hal menarik lainnya! 📊", + berapa: + "Tanya aku 'berapa pesan hari ini' atau 'berapa orang yang chat' dan aku akan jawab dengan data real-time!", + }; + + for (const [keyword, response] of Object.entries(responseMap)) { + if (lowerInput.includes(keyword)) { + return response; + } + } + + // Default response + if (messages.length < 5) { + return "Bagus! Aku akan belajar tentang apa yang kamu tanya. Coba tanya aku tentang chat, analytics, atau partisipan! 🎯"; + } + + return `Menarik! "${input}" - itu hal yang perlu diperhatikan. Ada yang lain ingin kamu ketahui? 🤔`; +}