From 370be96565da7f0dab09937b5314f35d79b36862 Mon Sep 17 00:00:00 2001 From: Jon Saad-Falcon <41205309+jonsaadfalcon@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:31:04 -0700 Subject: [PATCH] feat: add document import via paste text or file upload Adds a new Upload / Paste data source that lets users paste text or upload documents (.txt, .md, .pdf, .docx, .csv) directly into the knowledge base. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/pages/DataSourcesPage.tsx | 206 ++++++++++++++++++++++- frontend/src/types/connectors.ts | 19 ++- src/openjarvis/server/app.py | 2 + src/openjarvis/server/upload_router.py | 221 +++++++++++++++++++++++++ 4 files changed, 444 insertions(+), 4 deletions(-) create mode 100644 src/openjarvis/server/upload_router.py diff --git a/frontend/src/pages/DataSourcesPage.tsx b/frontend/src/pages/DataSourcesPage.tsx index 1033cc2f..be729b74 100644 --- a/frontend/src/pages/DataSourcesPage.tsx +++ b/frontend/src/pages/DataSourcesPage.tsx @@ -10,6 +10,7 @@ import { sendblueHealth, } from '../lib/api'; import type { ChannelBinding, ManagedAgent } from '../lib/api'; +import { getBase } from '../lib/api'; import { Database, MessageSquare, Loader2 } from 'lucide-react'; import { SOURCE_CATALOG } from '../types/connectors'; import type { ConnectRequest } from '../types/connectors'; @@ -87,6 +88,191 @@ function InlineConnectForm({ ); } +// --------------------------------------------------------------------------- +// Upload / Paste form +// --------------------------------------------------------------------------- + +const ACCEPTED_EXTENSIONS = '.txt,.md,.pdf,.docx,.csv'; + +function UploadForm({ onDone }: { onDone?: () => void }) { + const [tab, setTab] = useState<'paste' | 'upload'>('paste'); + const [title, setTitle] = useState(''); + const [content, setContent] = useState(''); + const [files, setFiles] = useState([]); + const [busy, setBusy] = useState(false); + const [result, setResult] = useState(''); + const [error, setError] = useState(''); + + const handlePaste = async () => { + if (!content.trim()) return; + setBusy(true); + setError(''); + setResult(''); + try { + const res = await fetch(`${getBase()}/v1/connectors/upload/ingest`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: title.trim(), content }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({ detail: res.statusText })); + throw new Error(err.detail || `Upload failed: ${res.status}`); + } + const data = await res.json(); + setResult(`Added ${data.chunks_added} chunk${data.chunks_added !== 1 ? 's' : ''} to knowledge base`); + setTitle(''); + setContent(''); + onDone?.(); + } catch (err: any) { + setError(err.message || 'Upload failed'); + } finally { + setBusy(false); + } + }; + + const handleUpload = async () => { + if (files.length === 0) return; + setBusy(true); + setError(''); + setResult(''); + try { + const formData = new FormData(); + for (const f of files) formData.append('files', f); + if (title.trim()) formData.append('title', title.trim()); + + const res = await fetch(`${getBase()}/v1/connectors/upload/ingest/files`, { + method: 'POST', + body: formData, + }); + if (!res.ok) { + const err = await res.json().catch(() => ({ detail: res.statusText })); + throw new Error(err.detail || `Upload failed: ${res.status}`); + } + const data = await res.json(); + setResult(`Added ${data.chunks_added} chunk${data.chunks_added !== 1 ? 's' : ''} from ${files.length} file${files.length !== 1 ? 's' : ''}`); + setFiles([]); + setTitle(''); + onDone?.(); + } catch (err: any) { + setError(err.message || 'Upload failed'); + } finally { + setBusy(false); + } + }; + + const tabStyle = (active: boolean): React.CSSProperties => ({ + flex: 1, padding: '6px 0', textAlign: 'center', + fontSize: 12, fontWeight: 600, cursor: 'pointer', + background: active ? '#7c3aed' : 'transparent', + color: active ? 'white' : 'var(--color-text-secondary)', + border: 'none', borderRadius: 4, + }); + + const inputStyle: React.CSSProperties = { + width: '100%', padding: '7px 10px', + background: 'var(--color-bg)', + border: '1px solid var(--color-border)', + borderRadius: 4, color: 'var(--color-text)', + fontSize: 12, marginBottom: 6, + boxSizing: 'border-box' as const, + }; + + return ( +
+ {/* Tab bar */} +
+ + +
+ + {/* Title input (shared) */} + setTitle(e.target.value)} + placeholder="Title (optional)" + style={inputStyle} + /> + + {tab === 'paste' && ( + <> +