Saivineeth147
Add sessionStorage for chat persistence
31df046
Raw
History Blame Contribute Delete
13.7 kB
import { useState, useEffect, useRef } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { Upload, Send, X, FileText, MessageCircle, Key, CheckCircle, AlertCircle, Loader2, Zap } from 'lucide-react'
// Generate session ID once
const SESSION_ID = crypto.randomUUID()
// Star Background Component
function StarField() {
return (
<div className="star-field">
{Array.from({ length: 100 }, (_, i) => (
<div key={i} className="star" />
))}
</div>
)
}
// Main App
export default function App() {
// Load initial state from sessionStorage
const [documents, setDocuments] = useState(() => {
const saved = sessionStorage.getItem('pa_documents')
return saved ? JSON.parse(saved) : []
})
const [messages, setMessages] = useState(() => {
const saved = sessionStorage.getItem('pa_messages')
return saved ? JSON.parse(saved) : []
})
const [input, setInput] = useState('')
const [loading, setLoading] = useState(false)
const [uploading, setUploading] = useState(false)
const [dragOver, setDragOver] = useState(false)
const [provider, setProvider] = useState(() => {
return sessionStorage.getItem('pa_provider') || 'groq'
})
const [models, setModels] = useState([])
const [selectedModel, setSelectedModel] = useState(() => {
return sessionStorage.getItem('pa_model') || ''
})
const [loadingModels, setLoadingModels] = useState(false)
const [apiKey, setApiKey] = useState(() => {
return sessionStorage.getItem('pa_apikey') || ''
})
const [connectionStatus, setConnectionStatus] = useState(null)
const [apiKeyError, setApiKeyError] = useState(null)
const messagesEndRef = useRef(null)
const fileInputRef = useRef(null)
// Test connection handler
const testConnection = async () => {
if (!apiKey) return
setConnectionStatus('testing')
setApiKeyError(null)
try {
const res = await fetch('/assistant/models', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider, api_key: apiKey })
})
const data = await res.json()
if (data.models?.length > 0) {
setModels(data.models)
setSelectedModel(data.models[0].id)
setConnectionStatus('success')
localStorage.setItem(`${provider}_api_key`, apiKey)
} else {
setConnectionStatus('error')
setApiKeyError('No models found. Check your API key.')
}
} catch (err) {
setConnectionStatus('error')
setApiKeyError('Connection failed: ' + err.message)
}
}
// Persist to sessionStorage
useEffect(() => {
sessionStorage.setItem('pa_messages', JSON.stringify(messages))
}, [messages])
useEffect(() => {
sessionStorage.setItem('pa_documents', JSON.stringify(documents))
}, [documents])
useEffect(() => {
sessionStorage.setItem('pa_provider', provider)
}, [provider])
useEffect(() => {
if (selectedModel) sessionStorage.setItem('pa_model', selectedModel)
}, [selectedModel])
useEffect(() => {
if (apiKey) sessionStorage.setItem('pa_apikey', apiKey)
}, [apiKey])
// Reset models when provider changes
useEffect(() => {
setModels([])
setSelectedModel('')
setConnectionStatus(null)
setApiKeyError(null)
}, [provider])
// Scroll to bottom on new messages
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
// API helpers
const headers = {
'X-Session-ID': SESSION_ID,
'Content-Type': 'application/json'
}
// Upload document
const handleUpload = async (file) => {
const formData = new FormData()
formData.append('file', file)
setUploading(true)
try {
const res = await fetch('/assistant/upload', {
method: 'POST',
headers: { 'X-Session-ID': SESSION_ID },
body: formData
})
if (!res.ok) {
const error = await res.json()
alert(error.detail || 'Upload failed')
return
}
const doc = await res.json()
setDocuments(prev => [...prev, doc])
} catch (err) {
console.error('Upload error:', err)
alert('Failed to upload file')
} finally {
setUploading(false)
}
}
// Delete document
const handleDelete = async (docId) => {
try {
await fetch(`/assistant/documents/${docId}`, {
method: 'DELETE',
headers
})
setDocuments(prev => prev.filter(d => d.id !== docId))
} catch (err) {
console.error('Delete error:', err)
}
}
// Send message
const handleSend = async () => {
if (!input.trim() || loading) return
const userMessage = input.trim()
setInput('')
setMessages(prev => [...prev, { role: 'user', content: userMessage }])
setLoading(true)
try {
const res = await fetch('/assistant/chat', {
method: 'POST',
headers,
body: JSON.stringify({
message: userMessage,
provider: provider,
model: selectedModel || null,
api_key: apiKey || null
})
})
const data = await res.json()
setMessages(prev => [...prev, {
role: 'assistant',
content: data.answer,
sources: data.sources
}])
} catch (err) {
console.error('Chat error:', err)
setMessages(prev => [...prev, {
role: 'assistant',
content: 'Sorry, something went wrong. Please try again.'
}])
} finally {
setLoading(false)
}
}
// Handle key press
const handleKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}
// Drag and drop handlers
const handleDragOver = (e) => {
e.preventDefault()
setDragOver(true)
}
const handleDragLeave = () => setDragOver(false)
const handleDrop = (e) => {
e.preventDefault()
setDragOver(false)
const file = e.dataTransfer.files[0]
if (file) handleUpload(file)
}
const handleFileSelect = (e) => {
const file = e.target.files[0]
if (file) handleUpload(file)
}
return (
<>
<StarField />
<div className="app-container">
{/* Header */}
<header className="header">
<h1>🤖 Portfolio Assistant</h1>
<p>Upload your documents and ask questions</p>
</header>
{/* Settings Panel */}
<div className="settings-panel">
{/* Provider Card */}
<div className="settings-card">
<div className="card-header">
<Zap size={16} />
<span>Provider</span>
</div>
<select
className="form-select"
value={provider}
onChange={(e) => {
setProvider(e.target.value)
setConnectionStatus(null)
setModels([])
}}
>
<option value="groq">Groq (Fast)</option>
<option value="huggingface">HuggingFace</option>
</select>
<div className="form-group">
<label className="form-label">
Model
{loadingModels && <Loader2 size={12} className="spinner" />}
{models.length > 0 && <span className="model-count">({models.length})</span>}
</label>
<select
className="form-select"
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
disabled={models.length === 0}
>
{models.length > 0 ? (
models.map(m => (
<option key={m.id} value={m.id}>{m.name}</option>
))
) : (
<option value="">Enter API key to load models</option>
)}
</select>
</div>
</div>
{/* API Key Card */}
<div className="settings-card">
<div className="card-header">
<Key size={16} />
<span>API Key</span>
{connectionStatus === 'success' && (
<span className="status-badge success">
<CheckCircle size={12} /> Connected
</span>
)}
</div>
<input
type="password"
className={`form-input ${apiKeyError ? 'error' : ''}`}
value={apiKey}
onChange={(e) => {
setApiKey(e.target.value)
setConnectionStatus(null)
}}
placeholder={`Paste your ${provider === 'groq' ? 'Groq' : 'HuggingFace'} API key`}
/>
{apiKeyError ? (
<span className="status-text error">
<AlertCircle size={12} /> {apiKeyError}
</span>
) : (
<span className={`status-text ${models.length > 0 ? 'success' : ''}`}>
{models.length > 0
? `✓ ${models.length} models loaded`
: 'Models will load when you test connection'}
</span>
)}
<button
className="test-connection-btn"
onClick={testConnection}
disabled={!apiKey || connectionStatus === 'testing'}
>
{connectionStatus === 'testing' ? (
<><Loader2 size={14} className="spinner" /> Testing...</>
) : connectionStatus === 'error' ? (
<><AlertCircle size={14} /> Retry</>
) : connectionStatus === 'success' ? (
<><CheckCircle size={14} /> Connected!</>
) : (
<><Zap size={14} /> Test Connection</>
)}
</button>
<a
className="api-key-link"
href={provider === 'groq' ? 'https://console.groq.com/keys' : 'https://huggingface.co/settings/tokens'}
target="_blank"
rel="noopener noreferrer"
>
🔑 Get your {provider === 'groq' ? 'Groq' : 'HuggingFace'} API key →
</a>
</div>
</div>
{/* Upload Zone */}
<div
className={`upload-zone ${dragOver ? 'dragover' : ''}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
>
<Upload size={32} />
<p>{uploading ? 'Uploading...' : 'Drag & drop files here or click to browse'}</p>
<p className="formats">Supported: PDF, DOCX, TXT, Markdown, JSON</p>
<input
ref={fileInputRef}
type="file"
accept=".pdf,.docx,.txt,.md,.json"
onChange={handleFileSelect}
style={{ display: 'none' }}
/>
</div>
{/* Document List */}
{documents.length > 0 && (
<div className="documents">
{documents.map(doc => (
<div key={doc.id} className="doc-chip">
<FileText size={14} />
<span className="type">{doc.type}</span>
<span>{doc.filename}</span>
<button onClick={() => handleDelete(doc.id)}>
<X size={14} />
</button>
</div>
))}
</div>
)}
{/* Chat Container */}
<div className="chat-container">
<div className="messages">
{messages.length === 0 ? (
<div className="welcome-message">
<MessageCircle size={48} />
<h3>Start a conversation</h3>
<p>Upload documents above, then ask questions about them.</p>
</div>
) : (
messages.map((msg, idx) => (
<div key={idx} className={`message ${msg.role}`}>
{msg.role === 'assistant' ? (
<>
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{msg.content}
</ReactMarkdown>
{msg.sources?.length > 0 && (
<div className="sources">
Sources:
{[...new Set(msg.sources.map(s => s.source))].map((source, i) => (
<span key={i} className="source-chip">{source}</span>
))}
</div>
)}
</>
) : (
msg.content
)}
</div>
))
)}
{loading && (
<div className="message assistant">
<div className="loading-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div className="chat-input-container">
<textarea
className="chat-input"
placeholder="Ask a question about your documents..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
rows={1}
/>
<button
className="send-button"
onClick={handleSend}
disabled={!input.trim() || loading}
>
<Send size={18} />
Send
</button>
</div>
</div>
</div>
</>
)
}