import type { ComponentType } from 'react' import { BookOpen, Building2, CalendarClock, FileText, Folder, FolderOpen, Image as ImageIcon, Pencil, UserRound } from 'lucide-react' import type { CaseDocument, DocumentExhibit, Evidence, Exhibit, ExhibitRelation, ExhibitType, SourceFileType, TemporalFact } from './types' export type WidgetCommand = | { type: 'open-document'; documentId: string } | { type: 'toggle-folder'; folderId: string } | { type: 'edit-folder'; folderId: string } | { type: 'edit-event'; eventId: string } | { type: 'edit-party'; partyId: string } | { type: 'edit-document'; documentId: string } | { type: 'update-memory-cue'; documentId: string; cue: string } export type ExhibitWidgetContext = { exhibits: Exhibit[] relations: ExhibitRelation[] dispatch: (command: WidgetCommand) => void } export type ExhibitWidgetProps = { exhibit: Exhibit; context: ExhibitWidgetContext } export type WidgetCapabilities = { movable: boolean; resizable: boolean; connectable: boolean; discardable: boolean; dockable: boolean } export type ConnectionPort = { id: string; x: number; y: number } export type ExhibitWidgetDefinition = { modelKind: 'exhibit' visualType: ExhibitType shell: 'card' | 'document' defaultSize: { width: number; height: number } capabilities: WidgetCapabilities heading: (exhibit: Exhibit, context: ExhibitWidgetContext) => string connectionPorts: (exhibit: Exhibit) => ConnectionPort[] temporalFacts: (exhibit: Exhibit) => TemporalFact[] searchText: (exhibit: Exhibit) => string Component: ComponentType } const relationsFrom = (context: ExhibitWidgetContext, exhibitId: string, type: ExhibitRelation['type']) => context.relations .filter(relation => relation.type === type && relation.fromExhibitId === exhibitId) .sort((a, b) => a.sortOrder - b.sortOrder) function FolderWidget({ exhibit, context }: ExhibitWidgetProps) { if (exhibit.type !== 'folder') return null const documents = relationsFrom(context, exhibit.id, 'contains').flatMap(relation => { const document = context.exhibits.find(candidate => candidate.id === relation.toExhibitId) return document?.type === 'document' ? [document] : [] }) return

{exhibit.title}

{exhibit.content}

{!exhibit.isOpen &&
{documents.slice(0,3).map(document => )}{documents.length > 3 && + {documents.length - 3} MORE FILES}
}
} function NoteWidget({ exhibit, context }: ExhibitWidgetProps) { if (exhibit.type !== 'note') return null const source = relationsFrom(context, exhibit.id, 'source')[0] return

{exhibit.title}

{exhibit.content}

{source && }
} function EventWidget({ exhibit, context }: ExhibitWidgetProps) { if (exhibit.type !== 'event') return null const supportCount = relationsFrom(context,exhibit.id,'supports').length return

{exhibit.title}

{exhibit.content}

{supportCount} SUPPORTING EXHIBIT{supportCount === 1 ? '' : 'S'}
} function PartyWidget({ exhibit, context }: ExhibitWidgetProps) { if (exhibit.type !== 'party') return null const person = exhibit.partyKind === 'person' const evidenceCount = relationsFrom(context,exhibit.id,'concerns').length return
{person ? : }

{exhibit.title}

{person ? 'PERSON' : (exhibit.organizationKind || 'ORGANIZATION').replaceAll('_',' ').toUpperCase()}

{exhibit.content || 'No dossier summary yet.'}

{exhibit.aliases.length > 0 &&
AKA · {exhibit.aliases.join(' · ')}
}
{evidenceCount} ASSOCIATED EXHIBITS
} function DocumentWidget({ exhibit }: ExhibitWidgetProps) { if (exhibit.type !== 'document') return null return {exhibit.title} } const standardPorts = (exhibit: Exhibit) => [{ id:'centre',x:exhibit.x + exhibit.width / 2,y:exhibit.y + exhibit.height / 2 }] const notePorts = (exhibit: Exhibit) => [{ id:'knot',x:exhibit.x + exhibit.width / 2,y:exhibit.y + 12 }] const standardCapabilities: WidgetCapabilities = { movable:true,resizable:false,connectable:true,discardable:true,dockable:false } const searchable = (exhibit: Exhibit) => exhibit.type === 'document' ? [exhibit.title,...exhibit.body,...Object.values(exhibit.metadata)].join('\n').toLocaleLowerCase() : [exhibit.title,exhibit.content].join('\n').toLocaleLowerCase() export const exhibitWidgetRegistry: Record = { folder: { modelKind:'exhibit',visualType:'folder',shell:'card',defaultSize:{width:260,height:166},capabilities:standardCapabilities, heading:(exhibit,context) => `EVIDENCE FOLDER / ${relationsFrom(context,exhibit.id,'contains').length}`,connectionPorts:standardPorts,temporalFacts:() => [],searchText:searchable,Component:FolderWidget }, document: { modelKind:'exhibit',visualType:'document',shell:'document',defaultSize:{width:174,height:145},capabilities:standardCapabilities, heading:exhibit => exhibit.type === 'document' ? documentWidget(exhibit.fileType).label.toUpperCase() : 'DOCUMENT',connectionPorts:standardPorts, temporalFacts:exhibit => exhibit.type === 'document' ? [ ...(exhibit.publishedAt ? [{ id:`${exhibit.id}:published`,exhibitId:exhibit.id,kind:'published' as const,start:exhibit.publishedAt,label:exhibit.title }] : []), ...(exhibit.capturedAt ? [{ id:`${exhibit.id}:captured`,exhibitId:exhibit.id,kind:'captured' as const,start:exhibit.capturedAt,label:`${exhibit.title} captured` }] : []), ...exhibit.regions.flatMap(region => region.date ? [{ id:`${exhibit.id}:region:${region.id}`,exhibitId:exhibit.id,kind:'region_date' as const,start:region.date,label:region.label }] : []), ] : [],searchText:searchable,Component:DocumentWidget }, note: { modelKind:'exhibit',visualType:'note',shell:'card',defaultSize:{width:108,height:154},capabilities:standardCapabilities, heading:() => 'INVESTIGATOR / NOTE',connectionPorts:notePorts,temporalFacts:() => [],searchText:searchable,Component:NoteWidget }, event: { modelKind:'exhibit',visualType:'event',shell:'card',defaultSize:{width:270,height:174},capabilities:standardCapabilities, heading:() => 'EVENT / THIS HAPPENED',connectionPorts:standardPorts,temporalFacts:exhibit => exhibit.type === 'event' && exhibit.eventDate ? [{ id:`${exhibit.id}:occurred`,exhibitId:exhibit.id,kind:'occurred',start:exhibit.eventDate,label:exhibit.content }] : [],searchText:searchable,Component:EventWidget }, party: { modelKind:'exhibit',visualType:'party',shell:'card',defaultSize:{width:280,height:190},capabilities:standardCapabilities, heading:exhibit => exhibit.type === 'party' && exhibit.partyKind === 'person' ? 'PARTY / PERSON DOSSIER' : 'PARTY / ORGANIZATION DOSSIER',connectionPorts:standardPorts,temporalFacts:() => [],searchText:searchable,Component:PartyWidget }, } export function exhibitWidget(type: ExhibitType) { return exhibitWidgetRegistry[type] } type DocumentWidgetProps = { document: CaseDocument; source: string; onMemoryCue?: (cue: string) => void } export type DocumentWidgetDefinition = { label:string; Preview:ComponentType; Asset:ComponentType } function ImagePreview({ document,source }: DocumentWidgetProps) { return document.assetId ? : } function GenericPreview({ document }: DocumentWidgetProps) { return
{document.fileType.toUpperCase()}
} function TextPreview({ document,onMemoryCue }: DocumentWidgetProps) { const excerpt = document.body.filter(Boolean).slice(0,2).join(' ') return

{excerpt || document.title}