From d1cb55ff1a0a89aaf783f5872a0f8ff09596d1b0 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 11 Mar 2026 09:22:34 -0400 Subject: [PATCH] =?UTF-8?q?Frontend=20revamp:=20Animal=20Crossing=20=C3=97?= =?UTF-8?q?=20Claude=20design=20with=20shadcn=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New palette: deep nook green sidebar, sage user bubbles, warm cream answer cards - shadcn-style UI primitives: Button (CVA variants), Textarea, Input, Badge, Table - cn() utility (clsx + tailwind-merge) - lucide-react icons throughout (no more text-only buttons) - Simba mode: custom CSS toggle switch - Send button: circular amber button with arrow icon - AnswerBubble: amber gradient accent bar, loading dots animation - QuestionBubble: sage green pill with rounded-3xl - ToolBubble: centered leaf-green badge pill - ConversationList: active item highlighting, proper selectedId prop - Sidebar: collapsible with PanelLeftClose/Open icons, icon-only collapsed state - LoginScreen: decorative background blobs, refined rounded card - AdminPanel: proper icon buttons, leaf-green save confirmation - Fonts: Playfair Display (brand) + Nunito 800 weight added Co-Authored-By: Claude Sonnet 4.6 --- raggr-frontend/package.json | 4 + raggr-frontend/src/App.css | 205 +- raggr-frontend/src/components/AdminPanel.tsx | 188 +- .../src/components/AnswerBubble.tsx | 44 +- raggr-frontend/src/components/ChatScreen.tsx | 266 +- .../src/components/ConversationList.tsx | 82 +- raggr-frontend/src/components/LoginScreen.tsx | 102 +- .../src/components/MessageInput.tsx | 90 +- .../src/components/QuestionBubble.tsx | 15 +- raggr-frontend/src/components/ToolBubble.tsx | 14 +- raggr-frontend/src/components/ui/badge.tsx | 26 + raggr-frontend/src/components/ui/button.tsx | 48 + raggr-frontend/src/components/ui/input.tsx | 19 + raggr-frontend/src/components/ui/table.tsx | 37 + raggr-frontend/src/components/ui/textarea.tsx | 19 + raggr-frontend/src/lib/utils.ts | 6 + raggr-frontend/yarn.lock | 4601 ++++++----------- 17 files changed, 2439 insertions(+), 3327 deletions(-) create mode 100644 raggr-frontend/src/components/ui/badge.tsx create mode 100644 raggr-frontend/src/components/ui/button.tsx create mode 100644 raggr-frontend/src/components/ui/input.tsx create mode 100644 raggr-frontend/src/components/ui/table.tsx create mode 100644 raggr-frontend/src/components/ui/textarea.tsx create mode 100644 raggr-frontend/src/lib/utils.ts diff --git a/raggr-frontend/package.json b/raggr-frontend/package.json index 2ff56a9..a07bc6d 100644 --- a/raggr-frontend/package.json +++ b/raggr-frontend/package.json @@ -12,11 +12,15 @@ }, "dependencies": { "axios": "^1.12.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.577.0", "marked": "^16.3.0", "npm-watch": "^0.13.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-markdown": "^10.1.0", + "tailwind-merge": "^3.5.0", "watch": "^1.0.2" }, "devDependencies": { diff --git a/raggr-frontend/src/App.css b/raggr-frontend/src/App.css index d99d3e9..a699380 100644 --- a/raggr-frontend/src/App.css +++ b/raggr-frontend/src/App.css @@ -1,26 +1,44 @@ +@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800&family=Playfair+Display:ital,wght@0,600;0,700;1,600&display=swap'); @import "tailwindcss"; -@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700&family=Playfair+Display:ital,wght@0,600;0,700;1,600&display=swap'); @theme { - --color-cream: #FBF7F0; - --color-cream-dark: #F3EDE2; + /* === Animal Crossing × Claude Palette === */ + + /* Backgrounds */ + --color-cream: #FAF8F2; + --color-cream-dark: #F0EBDF; --color-warm-white: #FFFDF9; + + /* Forest / Nook Green system */ + --color-forest: #2A4D38; + --color-forest-mid: #345E46; + --color-forest-light: #4D7A5E; + --color-leaf: #5E9E70; + --color-leaf-dark: #3D7A52; + --color-leaf-light: #B8DEC4; + --color-leaf-pale: #EBF7EE; + + /* Amber / warm accents */ --color-amber-glow: #E8943A; + --color-amber-dark: #C97828; --color-amber-soft: #F5C882; - --color-amber-pale: #FFF0D6; - --color-forest: #2D5A3D; - --color-forest-light: #3D763A; - --color-forest-pale: #E8F5E4; + --color-amber-pale: #FFF4E0; + + /* Neutrals */ --color-charcoal: #2C2420; - --color-warm-gray: #8A7E74; - --color-sand: #D4C5B0; - --color-sand-light: #E8DED0; + --color-warm-gray: #7A7268; + --color-sand: #DECFB8; + --color-sand-light: #EDE3D4; --color-blush: #F2D1B3; - --color-sidebar-bg: #2C2420; - --color-sidebar-hover: #3D352F; - --color-sidebar-active: #4A3F38; + + /* Sidebar */ + --color-sidebar-bg: #2A4D38; + --color-sidebar-hover: #345E46; + --color-sidebar-active: #3D6E52; + + /* Fonts */ --font-display: 'Playfair Display', Georgia, serif; - --font-body: 'Nunito', -apple-system, BlinkMacSystemFont, sans-serif; + --font-body: 'Nunito', 'Nunito Sans', system-ui, sans-serif; } * { @@ -36,97 +54,92 @@ body { -moz-osx-font-smoothing: grayscale; } -/* Scrollbar styling */ -::-webkit-scrollbar { - width: 6px; -} -::-webkit-scrollbar-track { - background: transparent; -} -::-webkit-scrollbar-thumb { - background: var(--color-sand); - border-radius: 3px; -} -::-webkit-scrollbar-thumb:hover { - background: var(--color-warm-gray); -} +/* ── Scrollbar ─────────────────────────────────────── */ +::-webkit-scrollbar { width: 5px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--color-sand); border-radius: 99px; } +::-webkit-scrollbar-thumb:hover { background: var(--color-warm-gray); } + +/* ── Markdown in answer bubbles ─────────────────────── */ +.markdown-content p { margin: 0.5em 0; line-height: 1.7; } +.markdown-content p:first-child { margin-top: 0; } +.markdown-content p:last-child { margin-bottom: 0; } -/* Markdown content styling in answer bubbles */ .markdown-content h1, .markdown-content h2, .markdown-content h3 { font-family: var(--font-display); font-weight: 600; - margin-top: 1em; - margin-bottom: 0.5em; + margin: 1em 0 0.4em; line-height: 1.3; + color: var(--color-charcoal); } -.markdown-content h1 { font-size: 1.25rem; } -.markdown-content h2 { font-size: 1.1rem; } -.markdown-content h3 { font-size: 1rem; } - -.markdown-content p { - margin: 0.5em 0; - line-height: 1.65; -} +.markdown-content h1 { font-size: 1.2rem; } +.markdown-content h2 { font-size: 1.05rem; } +.markdown-content h3 { font-size: 0.95rem; } .markdown-content ul, -.markdown-content ol { - padding-left: 1.5em; - margin: 0.5em 0; -} - -.markdown-content li { - margin: 0.25em 0; - line-height: 1.6; -} +.markdown-content ol { padding-left: 1.4em; margin: 0.5em 0; } +.markdown-content li { margin: 0.3em 0; line-height: 1.6; } .markdown-content code { - background: rgba(0, 0, 0, 0.06); + background: rgba(0,0,0,0.06); padding: 0.15em 0.4em; - border-radius: 4px; - font-size: 0.88em; - font-family: 'SF Mono', 'Fira Code', monospace; + border-radius: 5px; + font-size: 0.85em; + font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; } .markdown-content pre { background: var(--color-charcoal); - color: #F3EDE2; - padding: 1em; - border-radius: 8px; + color: #F0EBDF; + padding: 1em 1.1em; + border-radius: 12px; overflow-x: auto; - margin: 0.75em 0; -} - -.markdown-content pre code { - background: none; - padding: 0; - color: inherit; + margin: 0.8em 0; } +.markdown-content pre code { background: none; padding: 0; color: inherit; } .markdown-content a { - color: var(--color-forest); + color: var(--color-leaf-dark); text-decoration: underline; text-underline-offset: 2px; } .markdown-content blockquote { - border-left: 3px solid var(--color-amber-glow); + border-left: 3px solid var(--color-amber-soft); padding-left: 1em; margin: 0.75em 0; color: var(--color-warm-gray); font-style: italic; } -/* Loading skeleton animation */ -@keyframes shimmer { - 0% { background-position: -200% 0; } - 100% { background-position: 200% 0; } +.markdown-content strong { font-weight: 700; } +.markdown-content em { font-style: italic; } + +/* ── Animations ─────────────────────────────────────── */ +@keyframes fadeSlideUp { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} +.message-enter { + animation: fadeSlideUp 0.3s ease-out forwards; } +@keyframes catPulse { + 0%, 80%, 100% { opacity: 0.25; transform: scale(0.75); } + 40% { opacity: 1; transform: scale(1); } +} +.loading-dot { animation: catPulse 1.4s ease-in-out infinite; } +.loading-dot:nth-child(2) { animation-delay: 0.2s; } +.loading-dot:nth-child(3) { animation-delay: 0.4s; } + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} .skeleton-shimmer { - background: linear-gradient( - 90deg, + background: linear-gradient(90deg, var(--color-sand-light) 25%, var(--color-cream) 50%, var(--color-sand-light) 75% @@ -135,36 +148,26 @@ body { animation: shimmer 1.8s ease-in-out infinite; } -/* Fade-in animation for messages */ -@keyframes fadeSlideUp { - from { - opacity: 0; - transform: translateY(12px); - } - to { - opacity: 1; - transform: translateY(0); - } +/* ── Toggle switch ──────────────────────────────────── */ +.toggle-track { + width: 36px; + height: 20px; + border-radius: 99px; + background: var(--color-sand); + position: relative; + transition: background 0.2s; + cursor: pointer; } - -.message-enter { - animation: fadeSlideUp 0.35s ease-out forwards; -} - -/* Subtle pulse for loading dots */ -@keyframes catPulse { - 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); } - 40% { opacity: 1; transform: scale(1); } -} - -.loading-dot { - animation: catPulse 1.4s ease-in-out infinite; -} -.loading-dot:nth-child(2) { animation-delay: 0.2s; } -.loading-dot:nth-child(3) { animation-delay: 0.4s; } - -/* Textarea focus glow */ -textarea:focus { - outline: none; - box-shadow: 0 0 0 2px var(--color-amber-soft); +.toggle-track.checked { background: var(--color-leaf); } +.toggle-thumb { + width: 14px; + height: 14px; + background: white; + border-radius: 99px; + position: absolute; + top: 3px; + left: 3px; + transition: transform 0.2s; + box-shadow: 0 1px 3px rgba(0,0,0,0.15); } +.toggle-track.checked .toggle-thumb { transform: translateX(16px); } diff --git a/raggr-frontend/src/components/AdminPanel.tsx b/raggr-frontend/src/components/AdminPanel.tsx index e4f12ea..e017ccf 100644 --- a/raggr-frontend/src/components/AdminPanel.tsx +++ b/raggr-frontend/src/components/AdminPanel.tsx @@ -1,5 +1,17 @@ import { useEffect, useState } from "react"; +import { X, Phone, PhoneOff, Pencil, Check } from "lucide-react"; import { userService, type AdminUserRecord } from "../api/userService"; +import { cn } from "../lib/utils"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "./ui/table"; type Props = { onClose: () => void; @@ -24,8 +36,8 @@ export const AdminPanel = ({ onClose }: Props) => { const startEdit = (user: AdminUserRecord) => { setEditingId(user.id); setEditValue(user.whatsapp_number ?? ""); - setRowError((prev) => ({ ...prev, [user.id]: "" })); - setRowSuccess((prev) => ({ ...prev, [user.id]: "" })); + setRowError((p) => ({ ...p, [user.id]: "" })); + setRowSuccess((p) => ({ ...p, [user.id]: "" })); }; const cancelEdit = () => { @@ -34,33 +46,33 @@ export const AdminPanel = ({ onClose }: Props) => { }; const saveWhatsapp = async (userId: string) => { - setRowError((prev) => ({ ...prev, [userId]: "" })); + setRowError((p) => ({ ...p, [userId]: "" })); try { const updated = await userService.adminSetWhatsapp(userId, editValue); - setUsers((prev) => prev.map((u) => (u.id === userId ? updated : u))); - setRowSuccess((prev) => ({ ...prev, [userId]: "Saved" })); + setUsers((p) => p.map((u) => (u.id === userId ? updated : u))); + setRowSuccess((p) => ({ ...p, [userId]: "Saved ✓" })); setEditingId(null); - setTimeout(() => setRowSuccess((prev) => ({ ...prev, [userId]: "" })), 2000); + setTimeout(() => setRowSuccess((p) => ({ ...p, [userId]: "" })), 2000); } catch (err) { - setRowError((prev) => ({ - ...prev, + setRowError((p) => ({ + ...p, [userId]: err instanceof Error ? err.message : "Failed to save", })); } }; const unlinkWhatsapp = async (userId: string) => { - setRowError((prev) => ({ ...prev, [userId]: "" })); + setRowError((p) => ({ ...p, [userId]: "" })); try { await userService.adminUnlinkWhatsapp(userId); - setUsers((prev) => - prev.map((u) => (u.id === userId ? { ...u, whatsapp_number: null } : u)), + setUsers((p) => + p.map((u) => (u.id === userId ? { ...u, whatsapp_number: null } : u)), ); - setRowSuccess((prev) => ({ ...prev, [userId]: "Unlinked" })); - setTimeout(() => setRowSuccess((prev) => ({ ...prev, [userId]: "" })), 2000); + setRowSuccess((p) => ({ ...p, [userId]: "Unlinked ✓" })); + setTimeout(() => setRowSuccess((p) => ({ ...p, [userId]: "" })), 2000); } catch (err) { - setRowError((prev) => ({ - ...prev, + setRowError((p) => ({ + ...p, [userId]: err instanceof Error ? err.message : "Failed to unlink", })); } @@ -68,110 +80,152 @@ export const AdminPanel = ({ onClose }: Props) => { return (
e.target === e.currentTarget && onClose()} > -
+
{/* Header */} -
-

Admin: WhatsApp Numbers

+
+
+
+ +
+

+ Admin · WhatsApp Numbers +

+
{/* Body */} -
+
{loading ? ( -
Loading…
+
+
+ + + +
+ Loading users… +
) : ( - - - - - - - - - - +
UsernameEmailWhatsAppActions
+ + + Username + Email + WhatsApp + Actions + + + {users.map((user) => ( - - - - - - + + ))} - -
{user.username}{user.email} + + + {user.username} + + {user.email} + {editingId === user.id ? (
- setEditValue(e.target.value)} placeholder="whatsapp:+15551234567" + className="w-52" autoFocus + onKeyDown={(e) => + e.key === "Enter" && saveWhatsapp(user.id) + } /> {rowError[user.id] && ( - {rowError[user.id]} + + {rowError[user.id]} + )}
) : ( -
- +
+ {user.whatsapp_number ?? "—"} {rowSuccess[user.id] && ( - {rowSuccess[user.id]} + + {rowSuccess[user.id]} + )} {rowError[user.id] && ( - {rowError[user.id]} + + {rowError[user.id]} + )}
)} -
+ + {editingId === user.id ? ( -
- - +
) : ( -
- + {user.whatsapp_number && ( - + )}
)} -
+ + )}
diff --git a/raggr-frontend/src/components/AnswerBubble.tsx b/raggr-frontend/src/components/AnswerBubble.tsx index 1433a98..db4207e 100644 --- a/raggr-frontend/src/components/AnswerBubble.tsx +++ b/raggr-frontend/src/components/AnswerBubble.tsx @@ -1,4 +1,5 @@ import ReactMarkdown from "react-markdown"; +import { cn } from "../lib/utils"; type AnswerBubbleProps = { text: string; @@ -7,25 +8,32 @@ type AnswerBubbleProps = { export const AnswerBubble = ({ text, loading }: AnswerBubbleProps) => { return ( -
- {loading ? ( -
-
-
-
-
-
-
-
-
+
+
+ {/* amber accent bar */} +
+ +
+ {loading ? ( +
+ + + +
+ ) : ( +
+ {text} +
+ )}
- ) : ( -
- - {"🐈: " + text} - -
- )} +
); }; diff --git a/raggr-frontend/src/components/ChatScreen.tsx b/raggr-frontend/src/components/ChatScreen.tsx index 131bdbe..7d89439 100644 --- a/raggr-frontend/src/components/ChatScreen.tsx +++ b/raggr-frontend/src/components/ChatScreen.tsx @@ -1,4 +1,5 @@ import { useEffect, useState, useRef } from "react"; +import { LogOut, Shield, PanelLeftClose, PanelLeftOpen, Menu, X } from "lucide-react"; import { conversationService } from "../api/conversationService"; import { userService } from "../api/userService"; import { QuestionBubble } from "./QuestionBubble"; @@ -7,6 +8,7 @@ import { ToolBubble } from "./ToolBubble"; import { MessageInput } from "./MessageInput"; import { ConversationList } from "./ConversationList"; import { AdminPanel } from "./AdminPanel"; +import { cn } from "../lib/utils"; import catIcon from "../assets/cat.png"; type Message = { @@ -14,11 +16,6 @@ type Message = { speaker: "simba" | "user" | "tool"; }; -type QuestionAnswer = { - question: string; - answer: string; -}; - type Conversation = { title: string; id: string; @@ -48,15 +45,9 @@ const TOOL_MESSAGES: Record = { export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { const [query, setQuery] = useState(""); - const [answer, setAnswer] = useState(""); const [simbaMode, setSimbaMode] = useState(false); - const [questionsAnswers, setQuestionsAnswers] = useState( - [], - ); const [messages, setMessages] = useState([]); - const [conversations, setConversations] = useState([ - { title: "simba meow meow", id: "uuid" }, - ]); + const [conversations, setConversations] = useState([]); const [showConversations, setShowConversations] = useState(false); const [selectedConversation, setSelectedConversation] = useState(null); @@ -74,61 +65,45 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; - // Cleanup effect to handle component unmounting useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; - // Abort any pending requests when component unmounts - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - } + abortControllerRef.current?.abort(); }; }, []); const handleSelectConversation = (conversation: Conversation) => { setShowConversations(false); setSelectedConversation(conversation); - const loadMessages = async () => { + const load = async () => { try { - const fetchedConversation = await conversationService.getConversation( - conversation.id, - ); + const fetched = await conversationService.getConversation(conversation.id); setMessages( - fetchedConversation.messages.map((message) => ({ - text: message.text, - speaker: message.speaker, - })), + fetched.messages.map((m) => ({ text: m.text, speaker: m.speaker })), ); - } catch (error) { - console.error("Failed to load messages:", error); + } catch (err) { + console.error("Failed to load messages:", err); } }; - loadMessages(); + load(); }; const loadConversations = async () => { try { - const fetchedConversations = - await conversationService.getAllConversations(); - const parsedConversations = fetchedConversations.map((conversation) => ({ - id: conversation.id, - title: conversation.name, - })); - setConversations(parsedConversations); - setSelectedConversation(parsedConversations[0]); - } catch (error) { - console.error("Failed to load messages:", error); + const fetched = await conversationService.getAllConversations(); + const parsed = fetched.map((c) => ({ id: c.id, title: c.name })); + setConversations(parsed); + setSelectedConversation(parsed[0] ?? null); + } catch (err) { + console.error("Failed to load conversations:", err); } }; const handleCreateNewConversation = async () => { - const newConversation = await conversationService.createConversation(); + const newConv = await conversationService.createConversation(); await loadConversations(); - setSelectedConversation({ - title: newConversation.name, - id: newConversation.id, - }); + setSelectedConversation({ title: newConv.name, id: newConv.id }); }; useEffect(() => { @@ -141,64 +116,48 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { }, [messages]); useEffect(() => { - const loadMessages = async () => { - if (selectedConversation == null) return; + const load = async () => { + if (!selectedConversation) return; try { - const conversation = await conversationService.getConversation( - selectedConversation.id, - ); - // Update the conversation title in case it changed - setSelectedConversation({ - id: conversation.id, - title: conversation.name, - }); - setMessages( - conversation.messages.map((message) => ({ - text: message.text, - speaker: message.speaker, - })), - ); - } catch (error) { - console.error("Failed to load messages:", error); + const conv = await conversationService.getConversation(selectedConversation.id); + setSelectedConversation({ id: conv.id, title: conv.name }); + setMessages(conv.messages.map((m) => ({ text: m.text, speaker: m.speaker }))); + } catch (err) { + console.error("Failed to load messages:", err); } }; - loadMessages(); + load(); }, [selectedConversation?.id]); const handleQuestionSubmit = async () => { - if (!query.trim() || isLoading) return; // Don't submit empty messages or while loading + if (!query.trim() || isLoading) return; const currMessages = messages.concat([{ text: query, speaker: "user" }]); setMessages(currMessages); - setQuery(""); // Clear input immediately after submission + setQuery(""); setIsLoading(true); if (simbaMode) { - const randomIndex = Math.floor(Math.random() * simbaAnswers.length); - const randomElement = simbaAnswers[randomIndex]; + const randomElement = simbaAnswers[Math.floor(Math.random() * simbaAnswers.length)]; setMessages((prev) => prev.concat([{ text: randomElement, speaker: "simba" }])); setIsLoading(false); return; } - // Create a new AbortController for this request const abortController = new AbortController(); abortControllerRef.current = abortController; try { await conversationService.streamQuery( query, - selectedConversation.id, + selectedConversation!.id, (event) => { if (!isMountedRef.current) return; if (event.type === "tool_start") { - const friendly = - TOOL_MESSAGES[event.tool] ?? `🔧 Using ${event.tool}...`; + const friendly = TOOL_MESSAGES[event.tool] ?? `🔧 Using ${event.tool}...`; setMessages((prev) => prev.concat([{ text: friendly, speaker: "tool" }])); } else if (event.type === "response") { - setMessages((prev) => - prev.concat([{ text: event.message, speaker: "simba" }]), - ); + setMessages((prev) => prev.concat([{ text: event.message, speaker: "simba" }])); } else if (event.type === "error") { console.error("Stream error:", event.message); } @@ -206,22 +165,16 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { abortController.signal, ); } catch (error) { - // Ignore abort errors (these are intentional cancellations) if (error instanceof Error && error.name === "AbortError") { console.log("Request was aborted"); } else { console.error("Failed to send query:", error); - // If session expired, redirect to login if (error instanceof Error && error.message.includes("Session expired")) { setAuthenticated(false); } } } finally { - // Only update loading state if component is still mounted - if (isMountedRef.current) { - setIsLoading(false); - } - // Clear the abort controller reference + if (isMountedRef.current) setIsLoading(false); abortControllerRef.current = null; } }; @@ -230,10 +183,10 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { setQuery(event.target.value); }; - const handleKeyDown = (event: React.KeyboardEvent) => { - // Submit on Enter, but allow Shift+Enter for new line - if (event.key === "Enter" && !event.shiftKey) { - event.preventDefault(); + const handleKeyDown = (event: React.ChangeEvent) => { + const kev = event as unknown as React.KeyboardEvent; + if (kev.key === "Enter" && !kev.shiftKey) { + kev.preventDefault(); handleQuestionSubmit(); } }; @@ -245,30 +198,54 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { }; return ( -
- {/* Sidebar */} +
+ {/* ── Desktop Sidebar ─────────────────────────────── */} + {/* Admin Panel modal */} {showAdminPanel && setShowAdminPanel(false)} />} - {/* Main chat area */} -
+ {/* ── Main chat area ──────────────────────────────── */} +
{/* Mobile header */} -
-
- Simba -

+
+
+ Simba +

asksimba

{/* Conversation title bar */} {selectedConversation && ( -
-

+
+

{selectedConversation.title || "Untitled Conversation"} -

+

)} - {/* Messages area */} + {/* Messages */}
-
- {/* Mobile conversation list */} +
+ {/* Mobile conversation drawer */} {showConversations && ( -
+
{ {/* Empty state */} {messages.length === 0 && !isLoading && ( -
+
-
+
Simba
-
-

- Ask Simba anything -

-
+

+ Ask Simba anything +

)} @@ -388,14 +357,15 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { return ; return ; })} + {isLoading && }
- {/* Input area */} -