diff --git a/web/src/App.tsx b/web/src/App.tsx deleted file mode 100644 index 5e6dd05b70c55e9fce998ff3b7a0b09ce17bab87..0000000000000000000000000000000000000000 --- a/web/src/App.tsx +++ /dev/null @@ -1,531 +0,0 @@ -// 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; diff --git a/web/src/Attributions.md b/web/src/Attributions.md deleted file mode 100644 index 9b7cd4e13487db2e20f4f3844255f0b967db8448..0000000000000000000000000000000000000000 --- a/web/src/Attributions.md +++ /dev/null @@ -1,3 +0,0 @@ -This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md). - -This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license). \ No newline at end of file diff --git a/web/src/assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png b/web/src/assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png deleted file mode 100644 index 40497e953690a6624da27b14e4fc718fc3bc77c8..0000000000000000000000000000000000000000 Binary files a/web/src/assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png and /dev/null differ diff --git a/web/src/components/ChatArea.tsx b/web/src/components/ChatArea.tsx deleted file mode 100644 index eefd65dca7e24961f0dc905d114290e08a3a8f1d..0000000000000000000000000000000000000000 --- a/web/src/components/ChatArea.tsx +++ /dev/null @@ -1,358 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { Button } from './ui/button'; -import { Textarea } from './ui/textarea'; -import { Send, ArrowDown, AlertCircle, Trash2, Share2 } from 'lucide-react'; -import { Message } from './Message'; -import { FileUploadArea } from './FileUploadArea'; -import { Alert, AlertDescription } from './ui/alert'; -import { Badge } from './ui/badge'; -import type { Message as MessageType, LearningMode, UploadedFile, FileType, SpaceType } from '../App'; -import { toast } from 'sonner'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from './ui/dropdown-menu'; -import clareAvatar from '../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png'; - -interface ChatAreaProps { - messages: MessageType[]; - onSendMessage: (content: string) => void; - uploadedFiles: UploadedFile[]; - onFileUpload: (files: File[]) => void; - onRemoveFile: (index: number) => void; - onFileTypeChange: (index: number, type: FileType) => void; - memoryProgress: number; - isLoggedIn: boolean; - learningMode: LearningMode; - onClearConversation: () => void; - onLearningModeChange: (mode: LearningMode) => void; - spaceType: SpaceType; -} - -export function ChatArea({ - messages, - onSendMessage, - uploadedFiles, - onFileUpload, - onRemoveFile, - onFileTypeChange, - memoryProgress, - isLoggedIn, - learningMode, - onClearConversation, - onLearningModeChange, - spaceType, -}: ChatAreaProps) { - const [input, setInput] = useState(''); - const [isTyping, setIsTyping] = useState(false); - const [showScrollButton, setShowScrollButton] = useState(false); - const messagesEndRef = useRef(null); - const scrollContainerRef = useRef(null); - - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }; - - useEffect(() => { - scrollToBottom(); - }, [messages]); - - useEffect(() => { - const handleScroll = () => { - if (scrollContainerRef.current) { - const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current; - setShowScrollButton(scrollHeight - scrollTop - clientHeight > 100); - } - }; - - const container = scrollContainerRef.current; - container?.addEventListener('scroll', handleScroll); - return () => container?.removeEventListener('scroll', handleScroll); - }, []); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!input.trim() || !isLoggedIn) return; - - onSendMessage(input); - setInput(''); - setIsTyping(true); - setTimeout(() => setIsTyping(false), 1500); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSubmit(e); - } - }; - - const modeLabels: Record = { - concept: 'Concept Explainer', - socratic: 'Socratic Tutor', - exam: 'Exam Prep', - assignment: 'Assignment Helper', - summary: 'Quick Summary', - }; - - const handleClearClick = () => { - if (messages.length <= 1) { - toast.info('No conversation to clear'); - return; - } - - if (window.confirm('Are you sure you want to clear the conversation? This cannot be undone.')) { - onClearConversation(); - toast.success('Conversation cleared'); - } - }; - - const handleShareClick = () => { - if (messages.length <= 1) { - toast.info('No conversation to share'); - return; - } - - // Create a shareable text version of the conversation - const conversationText = messages - .map(msg => `${msg.role === 'user' ? (msg.sender?.name ?? 'You') : 'Clare'}: ${msg.content}`) - .join('\n\n'); - - // Copy to clipboard - navigator.clipboard.writeText(conversationText).then(() => { - toast.success('Conversation copied to clipboard!'); - }).catch(() => { - toast.error('Failed to copy conversation'); - }); - }; - - return ( -
- {/* Chat Area with Floating Input */} -
- {/* Action Buttons - Fixed at top right */} - {messages.length > 1 && ( -
- - -
- )} - - {/* Messages Area */} -
{ - const container = scrollContainerRef.current; - if (!container) return; - - const { scrollTop, scrollHeight, clientHeight } = container; - const isScrollable = scrollHeight > clientHeight; - const isAtTop = scrollTop === 0; - const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1; - - // If scrolling up at top or down at bottom, prevent default to stop propagation - if (isScrollable && ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0))) { - e.preventDefault(); - } - - e.stopPropagation(); - e.nativeEvent.stopImmediatePropagation(); - }} - > -
- {messages.map((message) => ( - - ))} - - {isTyping && ( -
-
- Clare -
-
-
-
-
-
-
-
-
- )} - -
-
-
- - {/* Scroll to Bottom Button - Floating above input */} - {showScrollButton && ( -
- -
- )} - - {/* Floating Input Area */} -
-
-
-
- {/* Mode Selector - ChatGPT style at bottom left */} - - - - - - onLearningModeChange('concept')} - className={learningMode === 'concept' ? 'bg-accent' : ''} - > -
- Concept Explainer - - Get detailed explanations of concepts - -
-
- onLearningModeChange('socratic')} - className={learningMode === 'socratic' ? 'bg-accent' : ''} - > -
- Socratic Tutor - - Learn through guided questions - -
-
- onLearningModeChange('exam')} - className={learningMode === 'exam' ? 'bg-accent' : ''} - > -
- Exam Prep - - Practice with quiz questions - -
-
- onLearningModeChange('assignment')} - className={learningMode === 'assignment' ? 'bg-accent' : ''} - > -
- Assignment Helper - - Get help with assignments - -
-
- onLearningModeChange('summary')} - className={learningMode === 'summary' ? 'bg-accent' : ''} - > -
- Quick Summary - - Get concise summaries - -
-
-
-
- -