feat: add document content search
This commit is contained in:
+13
-4
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user