From 6bd6ddcdca277cae424ce8dacdb31b3257e419d4 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Mon, 17 Aug 2026 21:29:37 +0700 Subject: [PATCH] feat(frontend): mobile bottom nav, chatbot overhaul, responsive polish - MobileNav: safe-area-aware bottom tab bar (< md), mirrors desktop nav - Chatbot: timestamps, per-message copy, retry-on-fail, auto-grow composer, MarkdownLite (XSS-safe React nodes, no dangerouslySetInnerHTML) - AppFrame: NavRail (md+) + MobileNav (< md) + bottom content padding - Design system: SignalTone ambient (signal/amber/vermilion) driving WebGL haze + topbar status pill; SectionHeader/MetricTile; globals.css tokens - Views: dashboard hero + AmbientField, analysis/media responsive grids --- .../src/app/(dashboard)/analysis/view.tsx | 4 +- .../src/app/(dashboard)/dashboard/view.tsx | 4 +- .../src/app/(dashboard)/media/view.tsx | 2 +- services/frontend/src/app/globals.css | 3 + .../src/components/chatbot/chatbot.tsx | 228 +++++++++++++++--- .../frontend/src/components/shared/index.ts | 1 + .../src/components/shared/markdown.tsx | 89 +++++++ .../src/components/shared/section.tsx | 13 +- .../src/components/shell/ambient-app.tsx | 8 +- .../frontend/src/components/shell/index.ts | 1 + .../src/components/shell/mobile-nav.tsx | 43 ++++ .../src/components/shell/nav-rail.tsx | 6 +- .../frontend/src/components/shell/topbar.tsx | 4 +- 13 files changed, 356 insertions(+), 50 deletions(-) create mode 100644 services/frontend/src/components/shared/markdown.tsx create mode 100644 services/frontend/src/components/shell/mobile-nav.tsx diff --git a/services/frontend/src/app/(dashboard)/analysis/view.tsx b/services/frontend/src/app/(dashboard)/analysis/view.tsx index 3addbe5..7e3f6a8 100644 --- a/services/frontend/src/app/(dashboard)/analysis/view.tsx +++ b/services/frontend/src/app/(dashboard)/analysis/view.tsx @@ -47,7 +47,9 @@ export function AnalysisView() {
Semantic search
-

Search the archive

+

+ Search the archive +

diff --git a/services/frontend/src/app/(dashboard)/dashboard/view.tsx b/services/frontend/src/app/(dashboard)/dashboard/view.tsx index a10eed3..7301a93 100644 --- a/services/frontend/src/app/(dashboard)/dashboard/view.tsx +++ b/services/frontend/src/app/(dashboard)/dashboard/view.tsx @@ -74,10 +74,10 @@ export function DashboardView({
GMW · Operations Grid
-

+

Ambient Field

-

+

Real-time moderation, voice & media presence across the monitored guild. {formatNumber(s.total_messages)} messages captured.

diff --git a/services/frontend/src/app/(dashboard)/media/view.tsx b/services/frontend/src/app/(dashboard)/media/view.tsx index 50524dd..d4269e0 100644 --- a/services/frontend/src/app/(dashboard)/media/view.tsx +++ b/services/frontend/src/app/(dashboard)/media/view.tsx @@ -85,7 +85,7 @@ export function MediaView({ initialStatus }: { initialStatus?: MediaState }) {
Now playing
-

+

{current?.title ?? "Nothing queued"}

{current?.source && ( diff --git a/services/frontend/src/app/globals.css b/services/frontend/src/app/globals.css index f4f7bc6..d2483ef 100644 --- a/services/frontend/src/app/globals.css +++ b/services/frontend/src/app/globals.css @@ -171,6 +171,9 @@ .text-ink-soft { color: var(--color-ink-soft); } .text-ink-faint { color: var(--color-ink-faint); } + /* Fluid hero title — never overflows on small screens. */ + .hero-clamp { font-size: clamp(1.9rem, 1.2rem + 4vw, 2.6rem); } + .glow-signal { text-shadow: 0 0 22px var(--color-signal-glow); } diff --git a/services/frontend/src/components/chatbot/chatbot.tsx b/services/frontend/src/components/chatbot/chatbot.tsx index aecf7aa..db9dcf8 100644 --- a/services/frontend/src/components/chatbot/chatbot.tsx +++ b/services/frontend/src/components/chatbot/chatbot.tsx @@ -1,21 +1,51 @@ "use client"; -import { Bot, MessageCircle, Send, X } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; import { - Avatar, - Button, - GlassPanel, - Input, - toast, -} from "@/components/primitives"; + Bot, + Check, + Copy, + MessageCircle, + RotateCw, + Trash2, + X, +} from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { Avatar, Button, GlassPanel, toast } from "@/components/primitives"; +import { MarkdownLite } from "@/components/shared"; import { useChatbotUserId } from "@/hooks/use-chatbot-user"; import { chatbotApi } from "@/lib/api"; import { cn } from "@/lib/utils"; interface Msg { + id: string; role: "user" | "bot"; content: string; + ts: number; +} + +function formatTime(ts: number): string { + try { + return new Date(ts).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); + } catch { + return ""; + } +} + +function TypingDots() { + return ( +
+ {[0, 1, 2].map((i) => ( + + ))} +
+ ); } export function Chatbot() { @@ -24,7 +54,11 @@ export function Chatbot() { const [msgs, setMsgs] = useState([]); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); + const [failed, setFailed] = useState(null); + const [copiedId, setCopiedId] = useState(null); const listRef = useRef(null); + const taRef = useRef(null); + const bottomRef = useRef(null); useEffect(() => { if (!open || !userId) return; @@ -33,28 +67,81 @@ export function Chatbot() { .then((res) => { setMsgs( res.history.slice(-12).flatMap((h) => [ - { role: "user" as const, content: h.user_message }, - { role: "bot" as const, content: h.bot_response }, + { + id: `${h.id}-u`, + role: "user" as const, + content: h.user_message, + ts: Date.parse(h.created_at) || Date.now(), + }, + { + id: `${h.id}-b`, + role: "bot" as const, + content: h.bot_response, + ts: Date.parse(h.created_at) || Date.now(), + }, ]), ); }) .catch(() => {}); }, [open, userId]); + // Auto-scroll to newest whenever the thread or typing state changes. + // biome-ignore lint/correctness/useExhaustiveDependencies: scroll must fire on thread/typing changes even though the body only reads the ref useEffect(() => { - listRef.current?.scrollTo({ top: listRef.current.scrollHeight }); - }, []); + bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); + }, [msgs, loading, open]); - const send = async () => { - const text = input.trim(); - if (!text || loading || !userId) return; + // Auto-grow the composer. + // biome-ignore lint/correctness/useExhaustiveDependencies: height recompute is keyed to input changes; body only reads the textarea element + useEffect(() => { + const ta = taRef.current; + if (!ta) return; + ta.style.height = "auto"; + ta.style.height = `${Math.min(ta.scrollHeight, 140)}px`; + }, [input]); + + const copy = async (id: string, text: string) => { + try { + await navigator.clipboard.writeText(text); + setCopiedId(id); + window.setTimeout(() => setCopiedId((c) => (c === id ? null : c)), 1200); + } catch { + /* clipboard unavailable */ + } + }; + + const clearAll = async () => { + if (!userId) return; + setMsgs([]); + try { + await chatbotApi.clearHistory(userId); + } catch { + /* best-effort */ + } + }; + + const send = async (text: string) => { + if (!text.trim() || loading || !userId) return; + setFailed(null); setInput(""); - setMsgs((m) => [...m, { role: "user", content: text }]); + setMsgs((m) => [ + ...m, + { id: `u-${Date.now()}`, role: "user", content: text, ts: Date.now() }, + ]); setLoading(true); try { const res = await chatbotApi.send(text, undefined, userId); - setMsgs((m) => [...m, { role: "bot", content: res.response }]); + setMsgs((m) => [ + ...m, + { + id: `b-${Date.now()}`, + role: "bot", + content: res.response, + ts: Date.parse(res.timestamp) || Date.now(), + }, + ]); } catch (e) { + setFailed(text); toast({ title: "Chat error", description: String(e), tone: "vermilion" }); } finally { setLoading(false); @@ -67,7 +154,7 @@ export function Chatbot() { type="button" aria-label="Open assistant" onClick={() => setOpen((o) => !o)} - className="fixed bottom-5 right-5 z-50 flex items-center justify-center rounded-full bg-signal text-signal-ink shadow-[0_10px_30px_-8px_var(--color-signal-glow)] transition-transform hover:scale-105" + className="fixed right-4 bottom-24 z-50 flex items-center justify-center rounded-full bg-signal text-signal-ink shadow-[0_10px_30px_-8px_var(--color-signal-glow)] transition-transform hover:scale-105 md:right-5 md:bottom-5" style={{ width: 52, height: 52 }} > {open ? : } @@ -75,8 +162,11 @@ export function Chatbot() { {open && (
@@ -90,22 +180,30 @@ export function Chatbot() { context-aware
+
- {msgs.length === 0 && ( + {msgs.length === 0 && !loading && (
Ask about moderation, voice, or media.
)} - {msgs.map((m, i) => ( + {msgs.map((m) => (
@@ -116,15 +214,43 @@ export function Chatbot() { className="mt-0.5 bg-signal/15 text-signal" /> )} -
- {m.content} +
+
+ {m.role === "bot" ? ( + + ) : ( + + {m.content} + + )} +
+
+ {formatTime(m.ts)} + +
))} @@ -135,27 +261,49 @@ export function Chatbot() { size={26} className="bg-signal/15 text-signal" /> -
- … +
+
)} +
-
- + Send failed + +
+ )} + +
+