import React, { useState, useRef } from "react"; import Markdown from "./Markdown"; import { Upload, FileText, Download, LoaderCircle, ArrowRight, Trash2, Files, MessageSquare, Settings2, Check, } from "lucide-react"; import { json, request, post, stream, saveResponse, setToken } from "./client"; export function Assistant({ language, onLanguage, enabled }) { const [file, setFile] = useState(null), [busy, setBusy] = useState(false), [error, setError] = useState(""), [question, setQuestion] = useState(""), [mode, setMode] = useState("answer"), [messages, setMessages] = useState([]), [running, setRunning] = useState(false), [term, setTerm] = useState(""), [column, setColumn] = useState(""), [rows, setRows] = useState(null); const abort = useRef(null), sequence = useRef(0); async function upload(selected) { if (!selected) return; if ( selected.size > (selected.name.toLowerCase().endsWith(".csv") ? 25 : 50) * 1024 * 1024 ) { setError("PDFs may be up to 50 MB; CSVs up to 25 MB."); return; } const seq = ++sequence.current; abort.current?.abort(); setRunning(false); setBusy(true); setError(""); try { if (file) await request("/api/uploads/" + file.id, { method: "DELETE" }).catch( () => {}, ); const form = new FormData(); form.append("file", selected); const result = await json("/api/uploads", { method: "POST", body: form }); if (seq !== sequence.current) return; setFile(result); setMessages([]); setRows(null); setTerm(""); setColumn(""); } catch (e) { setFile(null); setError(e.message); } finally { if (seq === sequence.current) setBusy(false); } } async function remove() { sequence.current++; abort.current?.abort(); setRunning(false); try { if (file) await request("/api/uploads/" + file.id, { method: "DELETE" }); setFile(null); setMessages([]); setRows(null); } catch (e) { setError(e.message); } } async function ask(e) { e.preventDefault(); if (!file || running) return; const controller = new AbortController(); abort.current = controller; const seq = sequence.current; setRunning(true); setError(""); setMessages((v) => [ ...v, { role: "user", content: question }, { role: "assistant", content: "" }, ]); const text = question; setQuestion(""); try { await stream( "/api/uploads/" + file.id + "/chat", { question: text, language, mode }, (part) => { if (seq === sequence.current) setMessages((v) => v.map((m, i) => i === v.length - 1 ? { ...m, content: m.content + part } : m, ), ); }, controller.signal, ); } catch (e) { if (e.name !== "AbortError" && seq === sequence.current) setError(e.message); } finally { if (seq === sequence.current) setRunning(false); } } async function exportFile(format) { try { await saveResponse( await request(`/api/uploads/${file.id}/download?format=${format}`), "document", ); } catch (e) { setError(e.message); } } async function filter(e) { e?.preventDefault(); try { setRows( await json( `/api/uploads/${file.id}/rows?term=${encodeURIComponent(term)}&column=${encodeURIComponent(column)}`, ), ); } catch (e) { setError(e.message); } } return (

Document & data assistant

Read a PDF or examine a CSV, then ask a question or draft an office note.

{error && (
{error}
)} {file && ( <>
{file.name} {file.kind === "pdf" ? `${file.page_count} pages · ${file.scanned_page_count} pages checked for OCR` : `${file.profile.row_count.toLocaleString()} rows · ${file.profile.column_count} columns`}
{(file.warnings || []).map((warning, i) => (
{warning}
))} {file.kind === "pdf" ? (
Read extracted text and physical page markers
{file.text}
) : ( <>
setTerm(e.target.value)} placeholder="Find an exact term across columns…" maxLength={300} />
{rows && (

{rows.total_matches.toLocaleString()} matching rows. Showing up to the first 100.

)}
{file.profile.columns.map((c) => ( ))} {(rows?.rows || file.preview).map((r, i) => ( {file.profile.columns.map((c) => ( ))} ))}
{c}
{r[c] === null ? "—" : String(r[c])}
Schema, missing values & summary statistics
{file.profile.columns.map((c) => ( ))}
Column Type Missing values
{c} {file.profile.dtypes[c]} {file.profile.null_counts[c]}
{JSON.stringify(file.profile.summary_stats, null, 2)}
{rows?.distribution && Object.keys(rows.distribution).length > 0 && (

{column} · largest categories

{Object.entries(rows.distribution).map(([key, value]) => (
{key} {value}
))}
)} )}

Ask about this file

Uploads expire after 30 minutes and are isolated to this browser session. Expired files are removed from memory automatically. Download anything you want to keep.

{messages.map((m, i) => (
{m.role === "user" ? "YOUR QUESTION" : "FILE-GROUNDED RESPONSE"}
{m.content || (running ? "Preparing the response…" : "No response received.")}
))}