// web/src/App.tsx import React, { useState, useEffect } from 'react'; import { Header } from './components/Header'; import { LeftSidebar } from './components/LeftSidebar'; import { ChatArea } from './components/ChatArea'; import { RightPanel } from './components/RightPanel'; import { FloatingActionButtons } from './components/FloatingActionButtons'; import { LoginScreen } from './components/LoginScreen'; import { ProfileEditor } from './components/ProfileEditor'; import { X } from 'lucide-react'; import { Button } from './components/ui/button'; import { Toaster } from './components/ui/sonner'; import { ChevronLeft, ChevronRight } from 'lucide-react'; import { toast } from 'sonner'; import { apiLogin, apiChat, apiUpload, apiExport, apiSummary, type LearningMode, type Language, type FileType, type User as ApiUser, } from './lib/api'; export interface Message { id: string; role: 'user' | 'assistant'; content: string; timestamp: Date; references?: string[]; sender?: GroupMember; // For group chat } export interface User { name: string; email: string; } export interface GroupMember { id: string; name: string; email: string; avatar?: string; isAI?: boolean; } export type SpaceType = 'individual' | 'group'; export interface Workspace { id: string; name: string; type: SpaceType; avatar: string; members?: GroupMember[]; } export interface UploadedFile { file: File; type: FileType; } function App() { const [isDarkMode, setIsDarkMode] = useState(() => { const saved = localStorage.getItem('theme'); return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches); }); const [user, setUser] = useState(null); const [messages, setMessages] = useState([ { id: '1', role: 'assistant', content: "👋 Hi! I'm Clare, your AI teaching assistant. I'm here to help you learn through personalized tutoring. Feel free to ask me anything about the course materials, or upload your documents to get started!", timestamp: new Date(), }, ]); const [learningMode, setLearningMode] = useState('concept'); const [language, setLanguage] = useState('auto'); const [uploadedFiles, setUploadedFiles] = useState([]); // You can later wire this to /api/memoryline const [memoryProgress] = useState(36); const [leftSidebarOpen, setLeftSidebarOpen] = useState(false); const [leftPanelVisible, setLeftPanelVisible] = useState(true); const [rightPanelOpen, setRightPanelOpen] = useState(false); const [rightPanelVisible, setRightPanelVisible] = useState(true); const [showProfileEditor, setShowProfileEditor] = useState(false); const [exportResult, setExportResult] = useState(''); const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null); // Mock group members (still fine; AI responder uses backend now) const [groupMembers] = useState([ { id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true }, { id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' }, { id: '2', name: 'Michael Chen', email: 'michael.c@university.edu' }, { id: '3', name: 'Emma Williams', email: 'emma.w@university.edu' }, ]); const [workspaces, setWorkspaces] = useState([]); const [currentWorkspaceId, setCurrentWorkspaceId] = useState('individual'); useEffect(() => { if (user) { const userAvatar = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`; setWorkspaces([ { id: 'individual', name: 'My Space', type: 'individual', avatar: userAvatar, }, { id: 'group-1', name: 'CS 101 Study Group', type: 'group', avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=cs101group', members: groupMembers, }, { id: 'group-2', name: 'AI Ethics Team', type: 'group', avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=aiethicsteam', members: groupMembers, }, ]); } }, [user, groupMembers]); const currentWorkspace = workspaces.find((w) => w.id === currentWorkspaceId) || workspaces[0]; const spaceType: SpaceType = currentWorkspace?.type || 'individual'; useEffect(() => { document.documentElement.classList.toggle('dark', isDarkMode); localStorage.setItem('theme', isDarkMode ? 'dark' : 'light'); }, [isDarkMode]); const asApiUser = (u: User): ApiUser => ({ name: u.name, email: u.email }); const handleSendMessage = async (content: string) => { if (!content.trim() || !user) return; const sender: GroupMember | undefined = spaceType === 'group' ? { id: user.email, name: user.name, email: user.email } : undefined; const userMessage: Message = { id: crypto.randomUUID(), role: 'user', content, timestamp: new Date(), sender, }; setMessages((prev) => [...prev, userMessage]); const shouldAIRespond = spaceType === 'individual' || content.toLowerCase().includes('@clare'); if (!shouldAIRespond) return; const assistantId = crypto.randomUUID(); const assistantPlaceholder: Message = { id: assistantId, role: 'assistant', content: 'Thinking...', timestamp: new Date(), sender: spaceType === 'group' ? groupMembers.find((m) => m.isAI) : undefined, }; setMessages((prev) => [...prev, assistantPlaceholder]); try { const data = await apiChat({ user: asApiUser(user), message: content, learningMode, language, docType: 'Syllabus', }); const references = (data.refs || []) .map((r) => [r.source_file, r.section].filter(Boolean).join(' — ')) .filter(Boolean); setMessages((prev) => prev.map((m) => m.id === assistantId ? { ...m, content: data.reply || '', references: references.length ? references : undefined, } : m ) ); } catch (err: any) { setMessages((prev) => prev.map((m) => m.id === assistantId ? { ...m, content: `Sorry — request failed.\n${err?.message ?? String(err)}` } : m ) ); } }; const handleFileUpload = async (files: File[]) => { if (!user) return; const newFiles: UploadedFile[] = files.map((file) => ({ file, type: 'other', })); setUploadedFiles((prev) => [...prev, ...newFiles]); for (const f of files) { try { const r = await apiUpload({ user: asApiUser(user), file: f, fileType: 'other' }); toast.success(r.status_md || `Uploaded: ${f.name}`); } catch (e: any) { toast.error(e?.message ?? `Upload failed: ${f.name}`); } } }; const handleRemoveFile = (index: number) => { setUploadedFiles((prev) => prev.filter((_, i) => i !== index)); }; const handleFileTypeChange = async (index: number, type: FileType) => { setUploadedFiles((prev) => prev.map((f, i) => (i === index ? { ...f, type } : f))); if (!user) return; const target = uploadedFiles[index]; if (!target) return; try { const r = await apiUpload({ user: asApiUser(user), file: target.file, fileType: type, }); toast.success(r.status_md || `Updated type: ${target.file.name}`); } catch (e: any) { toast.error(e?.message ?? `Failed to update type: ${target.file.name}`); } }; const handleClearConversation = () => { setMessages([ { id: '1', role: 'assistant', content: "👋 Hi! I'm Clare, your AI teaching assistant. I'm here to help you learn through personalized tutoring. Feel free to ask me anything about the course materials, or upload your documents to get started!", timestamp: new Date(), }, ]); toast.success('Conversation cleared'); }; const handleExport = async () => { if (!user) return; try { const r = await apiExport({ user: asApiUser(user), learningMode }); setExportResult(r.markdown || ''); setResultType('export'); toast.success('Conversation exported!'); } catch (e: any) { toast.error(e?.message ?? 'Export failed'); } }; const handleSummary = async () => { if (!user) return; try { const r = await apiSummary({ user: asApiUser(user), learningMode, language }); setExportResult(r.markdown || ''); setResultType('summary'); toast.success('Summary generated!'); } catch (e: any) { toast.error(e?.message ?? 'Summary failed'); } }; if (!user) { return ( { setUser(u); try { await apiLogin(asApiUser(u)); } catch (e: any) { toast.error(e?.message ?? 'Login sync failed'); } }} /> ); } return (
setLeftSidebarOpen(!leftSidebarOpen)} onUserClick={() => setRightPanelOpen(!rightPanelOpen)} isDarkMode={isDarkMode} onToggleDarkMode={() => setIsDarkMode(!isDarkMode)} language={language} onLanguageChange={setLanguage} workspaces={workspaces} currentWorkspace={currentWorkspace} onWorkspaceChange={setCurrentWorkspaceId} /> {showProfileEditor && user && ( setShowProfileEditor(false)} /> )}
e.stopPropagation()} style={{ overscrollBehavior: 'none' }} > {/* Mobile Sidebar Toggle - Left */} {leftSidebarOpen && (
setLeftSidebarOpen(false)} /> )} {/* Left Sidebar */} {leftPanelVisible ? ( ) : ( )} {/* Left Sidebar - Mobile */} {/* Main Chat Area */}
{/* Mobile Sidebar Toggle - Right */} {rightPanelOpen && (
setRightPanelOpen(false)} /> )} {/* Right Panel */} {rightPanelVisible ? ( ) : ( )} {/* Right Panel - Mobile */} {/* Floating Action Buttons - Desktop only, when panel is closed */} {!rightPanelVisible && ( setRightPanelVisible(true)} onExport={handleExport} onSummary={handleSummary} /> )}
); } export default App;