This commit is contained in:
MythEclipse
2026-06-03 15:01:34 +07:00
parent 99190a830a
commit 7904d7f38d
9 changed files with 422 additions and 173 deletions
+2 -18
View File
@@ -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 })}
/>
<ModerationAlertListener />
<MascotChatbot
isOpen={isMascotChatOpen}
onSetIsOpen={setIsMascotChatOpen}
onSendMessage={mascotChat.handleSendMessage}
mascotName="Discord Watcher"
mascotAvatar="https://raw.githubusercontent.com/IMPHNEN/imphnen-frontend-service/develop/apps/dimentorin/public/image/mascot-1.png"
/>
</DashboardLayout>
);
}
@@ -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<string> => {
// 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<string> {
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<string> {
// 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);
}
}
*/
@@ -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}
/>
<main className="flex min-w-0 flex-1 flex-col">
<Header
+38 -7
View File
@@ -2,7 +2,10 @@ import { motion } from "framer-motion";
import { BarChart3, MessageSquare, Radio } from "lucide-react";
import { useEffect, useState } from "react";
import type { DashboardTab } from "../entities/ui/types";
import type { MessageRecord } from "../shared/api/client";
import { useMascotChat } from "../shared/hooks/useMascotChat";
import { cn } from "../shared/lib/utils";
import { MascotChatbot } from "./mascot/MascotChatbot";
import { MascotImage } from "./mascot/MascotImage";
const navItems: Array<{ id: DashboardTab; label: string; icon: typeof Radio }> =
@@ -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 (
<motion.nav
className={cn(
"hidden shrink-0 flex-col border-r border-[#7EC8E3]/20 bg-white/70 backdrop-blur-md transition-all duration-300 md:flex",
"relative hidden shrink-0 flex-col overflow-visible border-r border-[#7EC8E3]/20 bg-white/70 backdrop-blur-md transition-all duration-300 md:flex",
collapsed ? "w-16" : "w-64",
)}
layout
@@ -94,12 +111,26 @@ export function Sidebar({
{/* Spacer pushes mascot to bottom */}
<div className="flex-1" />
{/* Mascot PNG with chat bubble */}
<div className="flex justify-center pb-4">
<MascotImage
size="sm"
showChat={showChat && !collapsed}
chatMessage={mascotChatMessage}
{/* Mascot PNG with anchored chatbot */}
<div className="relative flex justify-center pb-4">
<button
type="button"
onClick={() => mascotChat.setIsOpen(!mascotChat.isOpen)}
className="rounded-2xl p-1 transition-transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-primary/40"
title="Chat dengan mascot"
>
<MascotImage
size="sm"
showChat={showChat && !mascotChat.isOpen}
chatMessage={mascotChatMessage}
/>
</button>
<MascotChatbot
isOpen={mascotChat.isOpen}
onClose={() => mascotChat.setIsOpen(false)}
onSendMessage={mascotChat.handleSendMessage}
mascotName="Discord Watcher"
className="absolute bottom-16 left-12 z-50"
/>
</div>
</motion.nav>
@@ -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<string>;
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<ChatMessage[]>([
{
@@ -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 (
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
onClick={() => {
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"
>
<MessageCircle className="h-6 w-6" />
</motion.button>
);
}
if (!isOpen) return null;
return (
<AnimatePresence>
@@ -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 */}