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' && ( + <> +