Download frontend/src/App.js from sibikrish/cr-agent: direct link, hf CLI and curl.
- Browser
- Download file 13.9 kB
-
https://huggingface.co/spaces/sibikrish/cr-agent/resolve/main/frontend/src/App.js
- Command line
-
hf download hf://spaces/sibikrish/cr-agent/frontend/src/App.js
-
curl -L -o App.js https://huggingface.co/spaces/sibikrish/cr-agent/resolve/main/frontend/src/App.js
13.9 kB
| import React, { useState, useRef, useEffect } from 'react'; | |
| import axios from 'axios'; | |
| import ReactMarkdown from 'react-markdown'; | |
| import { | |
| Send, | |
| Paperclip, | |
| Trash2, | |
| Bot, | |
| User, | |
| AlertCircle, | |
| FileText, | |
| Loader2, | |
| Cloud, | |
| Calendar, | |
| Database, | |
| File, | |
| X, | |
| Plus, | |
| MessageSquare, | |
| History, | |
| Menu, | |
| HardDrive // Added icon for storage | |
| } from 'lucide-react'; | |
| import './App.css'; | |
| import StorageManager from './components/StorageManager'; // Import StorageManager | |
| function App() { | |
| // Use environment variable for backend URL, default to http://localhost:7860 | |
| const BACKEND_URL = process.env.REACT_APP_BACKEND_URL || 'http://localhost:7860'; | |
| const [sessions, setSessions] = useState(() => { | |
| const saved = localStorage.getItem('chat_sessions'); | |
| return saved ? JSON.parse(saved) : []; | |
| }); | |
| const [currentSessionId, setCurrentSessionId] = useState(Date.now()); | |
| const [messages, setMessages] = useState([ | |
| { | |
| role: 'assistant', | |
| content: 'Hello! I\'m your **Multi-Agent AI assistant**. I can help with:\n\n- π€οΈ **Weather information**\n- π **Document analysis** (upload PDF/TXT/MD)\n- π **Meeting scheduling** with weather checks\n- πΎ **Database queries** about meetings\n\nHow can I help you today?' | |
| } | |
| ]); | |
| const [input, setInput] = useState(''); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [uploadedFile, setUploadedFile] = useState(null); | |
| const [isSidebarOpen, setIsSidebarOpen] = useState(true); | |
| const [isPersistentUpload, setIsPersistentUpload] = useState(false); // State for persistent upload toggle | |
| const [showStorageManager, setShowStorageManager] = useState(false); // State to toggle Storage Manager view | |
| const messagesEndRef = useRef(null); | |
| const fileInputRef = useRef(null); | |
| const textInputRef = useRef(null); | |
| const scrollToBottom = () => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | |
| }; | |
| useEffect(() => { | |
| scrollToBottom(); | |
| }, [messages]); | |
| // Save sessions to localStorage | |
| useEffect(() => { | |
| localStorage.setItem('chat_sessions', JSON.stringify(sessions)); | |
| }, [sessions]); | |
| // Update current session in sessions list | |
| useEffect(() => { | |
| if (messages.length <= 1 && !uploadedFile) return; | |
| setSessions(prev => { | |
| const existingIdx = prev.findIndex(s => s.id === currentSessionId); | |
| const title = messages.find(m => m.role === 'user')?.content.substring(0, 30) || 'New Chat'; | |
| const sessionData = { | |
| id: currentSessionId, | |
| title: title.length >= 30 ? title + '...' : title, | |
| messages, | |
| uploadedFile, | |
| timestamp: new Date().toISOString() | |
| }; | |
| if (existingIdx >= 0) { | |
| const newSessions = [...prev]; | |
| newSessions[existingIdx] = sessionData; | |
| return newSessions; | |
| } else { | |
| return [sessionData, ...prev]; | |
| } | |
| }); | |
| }, [messages, uploadedFile, currentSessionId]); | |
| const createNewChat = () => { | |
| setCurrentSessionId(Date.now()); | |
| setMessages([ | |
| { | |
| role: 'assistant', | |
| content: 'Hello! I\'m your **Multi-Agent AI assistant**. I can help with:\n\n- π€οΈ **Weather information**\n- π **Document analysis** (upload PDF/TXT/MD)\n- π **Meeting scheduling** with weather checks\n- πΎ **Database queries** about meetings\n\nHow can I help you today?' | |
| } | |
| ]); | |
| setUploadedFile(null); | |
| if (textInputRef.current) textInputRef.current.focus(); | |
| }; | |
| const loadSession = (session) => { | |
| setCurrentSessionId(session.id); | |
| setMessages(session.messages); | |
| setUploadedFile(session.uploadedFile); | |
| }; | |
| const deleteSession = (e, id) => { | |
| e.stopPropagation(); | |
| setSessions(prev => prev.filter(s => s.id !== id)); | |
| if (currentSessionId === id) { | |
| createNewChat(); | |
| } | |
| }; | |
| const handleFileUpload = async (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| if (isPersistentUpload) { | |
| formData.append('persistent', 'true'); | |
| } | |
| try { | |
| setIsLoading(true); | |
| const response = await axios.post(`${BACKEND_URL}/upload`, formData, { | |
| headers: { 'Content-Type': 'multipart/form-data' } | |
| }); | |
| setUploadedFile({ | |
| name: file.name, | |
| path: response.data.file_path, | |
| size: response.data.file_size, | |
| isPersistent: isPersistentUpload | |
| }); | |
| setMessages(prev => [...prev, { | |
| role: 'system', | |
| content: `π **File uploaded:** ${file.name} (${response.data.file_size}) ${isPersistentUpload ? '(Persistent)' : ''}\n\nYou can now ask questions about this document!` | |
| }]); | |
| } catch (error) { | |
| setMessages(prev => [...prev, { | |
| role: 'error', | |
| content: `β File upload failed: ${error.response?.data?.detail || error.message}` | |
| }]); | |
| } finally { | |
| setIsLoading(false); | |
| if (fileInputRef.current) { | |
| fileInputRef.current.value = ''; | |
| } | |
| } | |
| }; | |
| const removeFile = (e) => { | |
| e.stopPropagation(); | |
| setUploadedFile(null); | |
| if (fileInputRef.current) { | |
| fileInputRef.current.value = ''; | |
| } | |
| setMessages(prev => [...prev, { | |
| role: 'system', | |
| content: 'π File removed from context.' | |
| }]); | |
| }; | |
| const handleSubmit = async (e) => { | |
| e.preventDefault(); | |
| if (!input.trim() || isLoading) return; | |
| const userMessage = input.trim(); | |
| setInput(''); | |
| setMessages(prev => [...prev, { role: 'user', content: userMessage }]); | |
| setIsLoading(true); | |
| try { | |
| const response = await axios.post(`${BACKEND_URL}/chat`, { | |
| query: userMessage, | |
| file_path: uploadedFile?.path || null | |
| }); | |
| setMessages(prev => [...prev, { | |
| role: 'assistant', | |
| content: response.data.response | |
| }]); | |
| } catch (error) { | |
| setMessages(prev => [...prev, { | |
| role: 'error', | |
| content: `β Error: ${error.response?.data?.detail || error.message}` | |
| }]); | |
| } finally { | |
| setIsLoading(false); | |
| setTimeout(() => textInputRef.current?.focus(), 100); | |
| } | |
| }; | |
| const clearChat = () => { | |
| setMessages([{ | |
| role: 'assistant', | |
| content: 'Chat cleared! How can I help you?' | |
| }]); | |
| setUploadedFile(null); | |
| if (fileInputRef.current) { | |
| fileInputRef.current.value = ''; | |
| } | |
| if (textInputRef.current) { | |
| textInputRef.current.focus(); | |
| } | |
| }; | |
| const handleExampleClick = (text) => { | |
| setInput(text); | |
| if (textInputRef.current) { | |
| textInputRef.current.focus(); | |
| } | |
| }; | |
| const exampleQueries = [ | |
| { icon: <Cloud size={16} />, text: 'What\'s the weather in Chennai?' }, | |
| { icon: <Calendar size={16} />, text: 'Schedule a team meeting tomorrow at 2pm' }, | |
| { icon: <Database size={16} />, text: 'Show all meetings scheduled tomorrow' }, | |
| { icon: <FileText size={16} />, text: 'What is the remote work policy?' } | |
| ]; | |
| return ( | |
| <div className={`App ${!isSidebarOpen ? 'sidebar-closed' : ''}`}> | |
| <aside className="sidebar"> | |
| <div className="sidebar-header"> | |
| <button className="new-chat-btn" onClick={createNewChat}> | |
| <Plus size={18} /> | |
| <span>New Chat</span> | |
| </button> | |
| </div> | |
| <div className="sidebar-content"> | |
| <div className="sidebar-section"> | |
| <div className="section-title"> | |
| <History size={14} /> | |
| <span>Recent Chats</span> | |
| </div> | |
| <div className="sessions-list"> | |
| {sessions.map(session => ( | |
| <div | |
| key={session.id} | |
| className={`session-item ${currentSessionId === session.id ? 'active' : ''}`} | |
| onClick={() => loadSession(session)} | |
| > | |
| <MessageSquare size={16} /> | |
| <span className="session-title">{session.title}</span> | |
| <button | |
| className="delete-session-btn" | |
| onClick={(e) => deleteSession(e, session.id)} | |
| > | |
| <X size={14} /> | |
| </button> | |
| </div> | |
| ))} | |
| {sessions.length === 0 && ( | |
| <div className="no-sessions">No past chats yet</div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="sidebar-footer"> | |
| <div className="user-profile"> | |
| <div className="user-avatar">S</div> | |
| <div className="user-info"> | |
| <span className="user-name">Sibi Krishnamoorthy</span> | |
| <span className="user-status">Online</span> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <main className="main-content"> | |
| <div className="chat-container"> | |
| <div className="chat-header"> | |
| <div className="header-left"> | |
| <button | |
| className="btn-icon sidebar-toggle" | |
| onClick={() => setIsSidebarOpen(!isSidebarOpen)} | |
| title={isSidebarOpen ? "Close sidebar" : "Open sidebar"} | |
| > | |
| <Menu size={20} /> | |
| </button> | |
| <div className="header-content"> | |
| <h1><Bot className="header-icon" /> Multi-Agent AI Assistant</h1> | |
| <p>Weather β’ Documents β’ Meetings β’ SQL</p> | |
| </div> | |
| </div> | |
| <div className="header-actions"> | |
| <button | |
| className={`btn-icon ${showStorageManager ? 'active' : ''}`} | |
| onClick={() => setShowStorageManager(!showStorageManager)} | |
| title="Storage Manager" | |
| > | |
| <HardDrive size={20} /> | |
| </button> | |
| {uploadedFile && ( | |
| <div className="uploaded-file-badge"> | |
| <File size={14} /> | |
| <span className="file-name">{uploadedFile.name}</span> | |
| <button onClick={removeFile} className="remove-file-btn" title="Remove file"> | |
| <X size={12} /> | |
| </button> | |
| </div> | |
| )} | |
| <div className="upload-controls" style={{ display: 'flex', alignItems: 'center', gap: '5px' }}> | |
| <label style={{ fontSize: '0.8em', display: 'flex', alignItems: 'center', gap: '4px', cursor: 'pointer' }}> | |
| <input | |
| type="checkbox" | |
| checked={isPersistentUpload} | |
| onChange={(e) => setIsPersistentUpload(e.target.checked)} | |
| /> | |
| Persistent | |
| </label> | |
| <button onClick={() => fileInputRef.current?.click()} className="btn-icon" title="Upload file"> | |
| <Paperclip size={20} /> | |
| </button> | |
| </div> | |
| <button onClick={clearChat} className="btn-icon" title="Clear current chat"> | |
| <Trash2 size={20} /> | |
| </button> | |
| </div> | |
| <input | |
| ref={fileInputRef} | |
| type="file" | |
| accept=".pdf,.txt,.md,.docx" | |
| onChange={handleFileUpload} | |
| style={{ display: 'none' }} | |
| /> | |
| </div> | |
| {showStorageManager ? ( | |
| <div className="storage-manager-container" style={{ padding: '20px', overflowY: 'auto' }}> | |
| <StorageManager /> | |
| </div> | |
| ) : ( | |
| <> | |
| <div className="messages-container"> | |
| {messages.map((msg, idx) => ( | |
| <div key={idx} className={`message ${msg.role}`}> | |
| <div className="message-avatar"> | |
| {msg.role === 'user' ? <User size={20} /> : | |
| msg.role === 'error' ? <AlertCircle size={20} /> : | |
| msg.role === 'system' ? <FileText size={20} /> : | |
| <Bot size={20} />} | |
| </div> | |
| <div className="message-content"> | |
| <ReactMarkdown>{msg.content}</ReactMarkdown> | |
| </div> | |
| </div> | |
| ))} | |
| {isLoading && ( | |
| <div className="message assistant"> | |
| <div className="message-avatar"><Bot size={20} /></div> | |
| <div className="message-content loading"> | |
| <Loader2 className="spinner" size={20} /> | |
| <span>Thinking...</span> | |
| </div> | |
| </div> | |
| )} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| <div className="example-queries"> | |
| {exampleQueries.map((query, idx) => ( | |
| <button | |
| key={idx} | |
| onClick={() => handleExampleClick(query.text)} | |
| className="example-query" | |
| disabled={isLoading} | |
| > | |
| {query.icon} | |
| <span>{query.text}</span> | |
| </button> | |
| ))} | |
| </div> | |
| <form onSubmit={handleSubmit} className="input-container"> | |
| <input | |
| ref={textInputRef} | |
| type="text" | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| placeholder="Ask about weather, meetings, documents, or upload a file..." | |
| disabled={isLoading} | |
| className="chat-input" | |
| /> | |
| <button type="submit" disabled={isLoading || !input.trim()} className="send-button"> | |
| {isLoading ? <Loader2 className="spinner" size={20} /> : <Send size={20} />} | |
| </button> | |
| </form> | |
| </> | |
| )} | |
| </div> | |
| </main> | |
| </div> | |
| ); | |
| } | |
| export default App; | |