diff --git a/services/frontend/src/app/(dashboard)/layout.tsx b/services/frontend/src/app/(dashboard)/layout.tsx index a1c5453..79990a6 100644 --- a/services/frontend/src/app/(dashboard)/layout.tsx +++ b/services/frontend/src/app/(dashboard)/layout.tsx @@ -2,17 +2,20 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Suspense } from "react"; - -import { Chatbot } from "@/components/chatbot/chatbot"; -import { AppHeader } from "@/components/layout/app-header"; -import { AppSidebar } from "@/components/layout/app-sidebar"; +import { TopNav } from "@/components/layout/top-nav"; import { MobileNav } from "@/components/layout/mobile-nav"; import { WsProvider } from "@/lib/ws/context"; +import { MascotProvider } from "@/components/mascot/mascot-context"; +import { MascotContainer } from "@/components/mascot/mascot-container"; +import { MiniPlayer } from "@/components/media/mini-player"; +import { MediaPlayerProvider } from "@/lib/hooks/use-media-player"; +import { HiddenSidebar } from "@/components/layout/hidden-sidebar"; +import { useState } from "react"; const queryClient = new QueryClient({ defaultOptions: { queries: { - staleTime: 15_000, + staleTime: 10_000, retry: 1, refetchOnWindowFocus: false, }, @@ -24,28 +27,38 @@ export default function DashboardLayout({ }: { children: React.ReactNode; }) { + const [guildId, setGuildId] = useState(""); + return ( -
- -
- -
- -
-
- } - > - {children} -
-
-
- -
- + + +
+ + setGuildId(g ?? "")} /> + + {/* Sub-nav space — filled per-page */} +
+
+ +
+
+ } + > + {children} +
+
+
+ + + + +
+
+
); diff --git a/services/frontend/src/components/chatbot/chatbot.tsx b/services/frontend/src/components/chatbot/chatbot.tsx deleted file mode 100644 index 5fabfd9..0000000 --- a/services/frontend/src/components/chatbot/chatbot.tsx +++ /dev/null @@ -1,227 +0,0 @@ -"use client"; - -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { - Bot, - Loader2, - MessageCircle, - Send, - Sparkles, - Trash2, - User, - X, -} from "lucide-react"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { chatbotApi } from "@/lib/api"; -import type { ChatHistoryMessage } from "@/lib/types"; -import { cn } from "@/lib/utils"; - -export function Chatbot() { - const [open, setOpen] = useState(false); - const [messages, setMessages] = useState([]); - const [input, setInput] = useState(""); - const scrollRef = useRef(null); - const qc = useQueryClient(); - - const { data: historyMessages = [] } = useQuery({ - queryKey: ["chatbot-history"], - queryFn: () => chatbotApi.getHistory(), - enabled: open, - }); - - useEffect(() => { - if (historyMessages.length > 0) setMessages(historyMessages); - }, [historyMessages]); - - const sendMut = useMutation({ - mutationFn: (text: string) => chatbotApi.send(text), - onSuccess: () => qc.invalidateQueries({ queryKey: ["chatbot-history"] }), - }); - - const clearMut = useMutation({ - mutationFn: () => chatbotApi.clearHistory(), - onSuccess: () => qc.setQueryData(["chatbot-history"], []), - }); - - useEffect(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }, []); - - const handleClear = useCallback(() => { - clearMut.mutate(); - setMessages([]); - }, [clearMut]); - - const handleSend = useCallback(async () => { - if (!input.trim() || sendMut.isPending) return; - const text = input.trim(); - setInput(""); - - // Add optimistic user message - setMessages((prev) => [ - ...prev, - { role: "user", content: text, timestamp: new Date().toISOString() }, - ]); - - try { - const resp = await sendMut.mutateAsync(text); - setMessages((prev) => [ - ...prev, - { - role: "assistant", - content: resp.response, - timestamp: resp.timestamp, - }, - ]); - } catch { - setMessages((prev) => [ - ...prev, - { - role: "assistant", - content: "Sorry, I couldn't process that request.", - timestamp: new Date().toISOString(), - }, - ]); - } - }, [input, sendMut]); - - return ( - <> - {/* Toggle button */} - - - {/* Chat panel */} - {open && ( - - - -
- -
- Chatbot - -
- {messages.length > 0 && ( - - )} - - - - - -
- {messages.length === 0 && ( -

- Ask me anything about the server! -

- )} - {messages.map((msg) => ( -
- - - {msg.role === "user" ? ( - - ) : ( - - )} - - -
- {msg.content} -
-
- ))} - {sendMut.isPending && ( -
- - - - - -
- -
-
- )} -
-
-
- - -
{ - e.preventDefault(); - handleSend(); - }} - className="flex w-full gap-2" - > - setInput(e.target.value)} - disabled={sendMut.isPending} - className="h-8 flex-1" - /> - -
-
- - )} - - ); -} diff --git a/services/frontend/src/components/layout/app-header.tsx b/services/frontend/src/components/layout/app-header.tsx deleted file mode 100644 index 4c4e8fb..0000000 --- a/services/frontend/src/components/layout/app-header.tsx +++ /dev/null @@ -1,167 +0,0 @@ -"use client"; - -import { Moon, PanelLeft, Sun } from "lucide-react"; -import { usePathname } from "next/navigation"; -import { useEffect, useState } from "react"; - -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { isActivePath, navItems } from "@/lib/navigation"; -import { cn } from "@/lib/utils"; -import { useWebSocket } from "@/lib/ws/context"; - -export function AppHeader() { - const pathname = usePathname(); - const { status } = useWebSocket(); - const [theme, setTheme] = useState<"light" | "dark">("dark"); - const [mobileOpen, setMobileOpen] = useState(false); - - useEffect(() => { - const stored = localStorage.getItem("theme") as "light" | "dark" | null; - if (stored) setTheme(stored); - }, []); - - const toggleTheme = () => { - const next = theme === "dark" ? "light" : "dark"; - setTheme(next); - localStorage.setItem("theme", next); - document.documentElement.classList.remove("light", "dark"); - document.documentElement.classList.add(next); - }; - - const pageTitle = - navItems - .filter((n) => isActivePath(pathname, n.matchPrefix)) - .map((n) => n.label) - .at(0) ?? "Dashboard"; - - const statusVariant = - status === "connected" - ? "default" - : status === "connecting" - ? "secondary" - : "destructive"; - - const statusLabel = - status === "connected" - ? "Connected" - : status === "connecting" - ? "Connecting" - : "Disconnected"; - - return ( - <> -
- {/* Mobile menu button */} - - -

{pageTitle}

- -
- - - - {statusLabel} - - - -
- - {/* Mobile overlay menu */} - {mobileOpen && ( -
- {/* biome-ignore lint/a11y/noStaticElementInteractions: overlay backdrop */} -
setMobileOpen(false)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") setMobileOpen(false); - }} - /> - -
- )} - - ); -} diff --git a/services/frontend/src/components/layout/app-sidebar.tsx b/services/frontend/src/components/layout/app-sidebar.tsx deleted file mode 100644 index 0306408..0000000 --- a/services/frontend/src/components/layout/app-sidebar.tsx +++ /dev/null @@ -1,101 +0,0 @@ -"use client"; - -import { usePathname, useRouter } from "next/navigation"; - -import { isActivePath, navItems } from "@/lib/navigation"; -import { cn } from "@/lib/utils"; -import { useWebSocket } from "@/lib/ws/context"; - -export function AppSidebar() { - const pathname = usePathname(); - const router = useRouter(); - const { status } = useWebSocket(); - - const connectionDot = { - connected: - "bg-emerald-500 shadow-[0_0_8px] shadow-emerald-500/60 animate-pulse", - connecting: "bg-amber-400 animate-pulse", - disconnected: "bg-destructive", - error: "bg-destructive", - }[status]; - - const connectionLabel = { - connected: "Connected", - connecting: "Connecting", - disconnected: "Disconnected", - error: "Error", - }[status]; - - return ( - - ); -}