diff --git a/MosaicIQ/src/components/Research/ResearchInspector.tsx b/MosaicIQ/src/components/Research/ResearchInspector.tsx new file mode 100644 index 0000000..2463632 --- /dev/null +++ b/MosaicIQ/src/components/Research/ResearchInspector.tsx @@ -0,0 +1,367 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { Pin, Archive, RefreshCw, Sparkles, Maximize2, Minimize2 } from 'lucide-react'; +import type { + GhostNote, + NoteAuditTrail, + NoteType, + ResearchNote, + ThesisStatus, +} from '../../types/research'; +import { ConfidenceBadge, EvidenceBadge, FreshnessBadge, NoteTypeBadge, TagChip } from './primitives/Badges'; +import { GHOST_CLASS_LABELS, NOTE_TYPE_LABELS } from './primitives/researchMeta'; + +interface ResearchInspectorProps { + note: ResearchNote | null; + ghost: GhostNote | null; + auditTrail: NoteAuditTrail | null; + isLoadingAuditTrail: boolean; + onRefreshAuditTrail: () => void; + onUpdateNote: (noteId: string, patch: { + rawText?: string; + title?: string; + noteType?: NoteType; + pinned?: boolean; + thesisStatus?: ThesisStatus; + }) => Promise; + onArchiveNote: (noteId: string, archived: boolean) => Promise; + onPromoteNote: (noteId: string, thesisStatus?: ThesisStatus, noteType?: NoteType) => Promise; + onReviewGhost: (ghostId: string, action: 'accept' | 'ignore' | 'dismiss' | 'pin') => Promise; +} + +const editableNoteTypes: NoteType[] = [ + 'fact', + 'management_signal', + 'claim', + 'thesis', + 'sub_thesis', + 'risk', + 'catalyst', + 'valuation_point', + 'question', + 'contradiction', + 'channel_check', + 'mosaic_insight', +]; + +export const ResearchInspector: React.FC = ({ + note, + ghost, + auditTrail, + isLoadingAuditTrail, + onRefreshAuditTrail, + onUpdateNote, + onArchiveNote, + onPromoteNote, + onReviewGhost, +}) => { + const [draftTitle, setDraftTitle] = useState(''); + const [draftBody, setDraftBody] = useState(''); + const [draftType, setDraftType] = useState('claim'); + const [isEditorExpanded, setIsEditorExpanded] = useState(false); + const expandedEditorRef = useRef(null); + + useEffect(() => { + if (!note) { + return; + } + + setDraftTitle(note.title ?? ''); + setDraftBody(note.rawText); + setDraftType(note.noteType); + }, [note]); + + useEffect(() => { + if (isEditorExpanded && expandedEditorRef.current) { + expandedEditorRef.current.focus(); + expandedEditorRef.current.setSelectionRange( + expandedEditorRef.current.value.length, + expandedEditorRef.current.value.length, + ); + } + }, [isEditorExpanded]); + + const handleExpandKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + setIsEditorExpanded(false); + } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setIsEditorExpanded(false); + } + }, + [], + ); + + if (!note && !ghost) { + return ( +
+
+

+ Inspector +

+

+ Select a note or ghost note to inspect provenance, links, evidence, and promotion controls. +

+
+
+ ); + } + + if (ghost) { + return ( +
+
+
Ghost Note
+

+ {ghost.headline} +

+

+ {ghost.body} +

+
+ +
+ + + +
+ +
+ + + +
+
+ ); + } + + return ( +
+
+ + + + +
+ +
+ +