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 (
{Array.from({ length: 100 }, (_, i) => (
))}
)
}
// 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 (
<>
{/* Header */}
{/* Settings Panel */}
{/* Provider Card */}
Provider
{/* API Key Card */}
API Key
{connectionStatus === 'success' && (
Connected
)}
{
setApiKey(e.target.value)
setConnectionStatus(null)
}}
placeholder={`Paste your ${provider === 'groq' ? 'Groq' : 'HuggingFace'} API key`}
/>
{apiKeyError ? (
{apiKeyError}
) : (
0 ? 'success' : ''}`}>
{models.length > 0
? `✓ ${models.length} models loaded`
: 'Models will load when you test connection'}
)}
🔑 Get your {provider === 'groq' ? 'Groq' : 'HuggingFace'} API key →
{/* Upload Zone */}
fileInputRef.current?.click()}
>
{uploading ? 'Uploading...' : 'Drag & drop files here or click to browse'}
Supported: PDF, DOCX, TXT, Markdown, JSON
{/* Document List */}
{documents.length > 0 && (
{documents.map(doc => (
{doc.type}
{doc.filename}
))}
)}
{/* Chat Container */}
{messages.length === 0 ? (
Start a conversation
Upload documents above, then ask questions about them.
) : (
messages.map((msg, idx) => (
{msg.role === 'assistant' ? (
<>
{msg.content}
{msg.sources?.length > 0 && (
Sources:
{[...new Set(msg.sources.map(s => s.source))].map((source, i) => (
{source}
))}
)}
>
) : (
msg.content
)}
))
)}
{loading && (
)}
{/* Input */}
>
)
}