feat: add document content search

This commit is contained in:
2026-08-14 17:57:00 +02:00
parent 6f183daca8
commit a15b463589
4 changed files with 29 additions and 5 deletions
+13 -4
View File
@@ -12,6 +12,11 @@ const SOURCE_FILE_TYPES: { value: SourceFileType; label: string }[] = [
function uid(_prefix: string) { return crypto.randomUUID() }
function briefAcknowledgementKey(levelId: string) { return `gupi-osint-board:brief-acknowledged:${levelId}` }
function documentSearchText(document: CaseDocument) {
return [document.title, document.kind, document.date, document.publishedAt, document.fileName, document.mimeType,
...document.body, ...document.regions.flatMap(region => [region.label, region.excerpt, region.date]),
...Object.entries(document.metadata).flatMap(([key, value]) => [key, value])].filter(Boolean).join('\n').toLocaleLowerCase()
}
function connectionPoint(item: Evidence) {
return exhibitWidget(item.type).connectionPoint(item)
}
@@ -24,7 +29,8 @@ export function App() {
const [openDoc, setOpenDoc] = useState<CaseDocument | null>(null)
const [selected, setSelected] = useState<string | null>(null)
const [linkFrom, setLinkFrom] = useState<string | null>(null)
const [docsOpen, setDocsOpen] = useState(true)
const [docsOpen, setDocsOpen] = useState(false)
const [documentQuery, setDocumentQuery] = useState('')
const [helpOpen, setHelpOpen] = useState(false)
const [status, setStatus] = useState('CONNECTING TO ARCHIVE…')
const [clock, setClock] = useState('')
@@ -274,6 +280,8 @@ export function App() {
if (!caseState) return <div className="boot"><div className="seal">GU</div><p>GLITCH UNIVERSITY NETWORK TERMINAL</p><small>{status}</small></div>
const documentById = new Map(caseState.documents.map(document => [document.id, document]))
const normalizedDocumentQuery = documentQuery.trim().toLocaleLowerCase()
const filteredDocuments = normalizedDocumentQuery ? caseState.documents.filter(document => documentSearchText(document).includes(normalizedDocumentQuery)) : caseState.documents
const unresolvedConceptCount = caseState.brief.concepts.filter(concept => !concept.resolvedPartyExhibitId).length
const containedDocumentIds = new Set(caseState.relations.filter(relation => relation.type === 'contains').map(relation => relation.toWidgetId))
const temporalItems: TemporalItem[] = [
@@ -298,14 +306,15 @@ export function App() {
<section className="workspace">
<aside className={`documents-panel ${docsOpen ? '' : 'closed'}`}>
<div className="panel-heading"><div><small>CASE MATERIALS</small><h2>DOCUMENTS <sup>{caseState.documents.length}</sup></h2></div><button onClick={() => setDocsOpen(false)}><X size={17}/></button></div>
<div className="search"><Search size={15}/><span>Search case archive</span></div>
<div className="panel-heading"><div><small>CASE MATERIALS</small><h2>DOCUMENTS <sup>{normalizedDocumentQuery ? `${filteredDocuments.length}/${caseState.documents.length}` : caseState.documents.length}</sup></h2></div><button aria-label="Close documents" onClick={() => setDocsOpen(false)}><X size={17}/></button></div>
<label className="search"><Search size={15}/><input type="search" aria-label="Search inside documents" placeholder="Search inside documents…" value={documentQuery} onChange={event => setDocumentQuery(event.target.value)}/>{documentQuery && <button type="button" aria-label="Clear document search" onClick={() => setDocumentQuery('')}><X size={13}/></button>}</label>
{requestedEditMode && caseState.editingAllowed && <><button className="import-document" onClick={() => fileInputRef.current?.click()}><Upload size={15}/>{uploading ? `IMPORTING ${uploading}` : 'IMPORT DOCUMENT'}</button><input ref={fileInputRef} className="file-input" type="file" multiple onChange={e => { if (e.target.files) uploadFiles(e.target.files); e.target.value = '' }} /></>}
<div className="doc-list">
{caseState.documents.map((doc, index) => <button className="doc-row" data-temporal-id={`document:${doc.id}`} key={doc.id} onDoubleClick={() => setOpenDoc(doc)} onClick={() => setOpenDoc(doc)}>
{filteredDocuments.map((doc, index) => <button className="doc-row" data-temporal-id={`document:${doc.id}`} key={doc.id} onDoubleClick={() => setOpenDoc(doc)} onClick={() => setOpenDoc(doc)}>
<div className={`doc-icon tint-${index % 3}`}><FileText size={24}/><b>{doc.kind.slice(0, 3)}</b></div>
<div><strong>{doc.title}</strong><span>{doc.kind} · {doc.date}</span></div><ChevronRight size={16}/>
</button>)}
{normalizedDocumentQuery && filteredDocuments.length === 0 && <div className="no-document-results"><Search size={20}/><b>NO MATCHING DOCUMENTS</b><span>Searches titles, contents, extracts, and metadata.</span></div>}
</div>
<div className="panel-foot"><FolderOpen size={15}/> ARCHIVE MOUNTED <span>{requestedEditMode && caseState.editingAllowed ? 'AUTHORING' : 'READ ONLY'}</span></div>
</aside>