diff --git a/app/research/page.tsx b/app/research/page.tsx index 5efe44a..a433263 100644 --- a/app/research/page.tsx +++ b/app/research/page.tsx @@ -6,6 +6,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { BookOpenText, Download, FilePlus2, Filter, FolderUp, Link2, NotebookPen, Search, ShieldCheck, Sparkles, Trash2 } from 'lucide-react'; import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; +import { ResearchCopilotPanel } from '@/components/research/research-copilot-panel'; import { AppShell } from '@/components/shell/app-shell'; import { Panel } from '@/components/ui/panel'; import { Button } from '@/components/ui/button'; @@ -31,6 +32,8 @@ import type { ResearchArtifact, ResearchArtifactKind, ResearchArtifactSource, + ResearchCopilotCitation, + ResearchCopilotSession, ResearchMemo, ResearchMemoSection, ResearchWorkspace @@ -104,6 +107,15 @@ const EMPTY_MEMO_FORM: MemoFormState = { nextActionsMarkdown: '' }; +const MEMO_FORM_FIELD_BY_SECTION: Record = { + thesis: 'thesisMarkdown', + variant_view: 'variantViewMarkdown', + catalysts: 'catalystsMarkdown', + risks: 'risksMarkdown', + disconfirming_evidence: 'disconfirmingEvidenceMarkdown', + next_actions: 'nextActionsMarkdown' +}; + function parseTags(value: string) { const unique = new Set(); @@ -197,6 +209,8 @@ function ResearchPageContent() { const [uploadTitle, setUploadTitle] = useState(''); const [uploadSummary, setUploadSummary] = useState(''); const [uploadTags, setUploadTags] = useState(''); + const [researchMode, setResearchMode] = useState<'workspace' | 'copilot'>('workspace'); + const [focusTab, setFocusTab] = useState<'library' | 'memo' | 'packet'>('library'); const ticker = useMemo(() => searchParams.get('ticker')?.trim().toUpperCase() ?? '', [searchParams]); const deferredSearch = useDeferredValue(searchInput); @@ -253,6 +267,7 @@ function ResearchPageContent() { const invalidateResearch = async (symbol: string) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.researchWorkspace(symbol) }), + queryClient.invalidateQueries({ queryKey: queryKeys.researchCopilotSession(symbol) }), queryClient.invalidateQueries({ queryKey: ['research', 'library', symbol] }), queryClient.invalidateQueries({ queryKey: queryKeys.researchMemo(symbol) }), queryClient.invalidateQueries({ queryKey: queryKeys.researchPacket(symbol) }), @@ -399,9 +414,369 @@ function ResearchPageContent() { } }; + const handleCopilotSessionChange = (session: ResearchCopilotSession) => { + setWorkspace((current) => current ? { ...current, copilotSession: session } : current); + }; + + const handleDraftNote = (input: { title: string; summary: string; bodyMarkdown: string }) => { + setNoteForm({ + id: null, + title: input.title, + summary: input.summary, + bodyMarkdown: input.bodyMarkdown, + tags: 'copilot' + }); + setNotice('Loaded copilot output into the note draft editor for review.'); + }; + + const handleDraftMemoSection = (section: ResearchMemoSection, contentMarkdown: string) => { + const field = MEMO_FORM_FIELD_BY_SECTION[section]; + setMemoForm((current) => ({ + ...current, + [field]: contentMarkdown + })); + setAttachSection(section); + setNotice(`Loaded copilot output into ${MEMO_SECTIONS.find((item) => item.value === section)?.label}.`); + }; + + const handleAttachCitation = async (citation: ResearchCopilotCitation, section: ResearchMemoSection) => { + if (!citation.artifactId) { + setError('This citation cannot be attached because no research artifact is available for it.'); + return; + } + + try { + const memoId = await ensureMemo(); + await addResearchMemoEvidence({ + memoId, + artifactId: citation.artifactId, + section + }); + setNotice(`Attached cited evidence to ${MEMO_SECTIONS.find((item) => item.value === section)?.label}.`); + await invalidateResearch(ticker); + await loadWorkspace(ticker); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unable to attach cited evidence'); + } + }; + const availableTags = workspace?.availableTags ?? []; const memoEvidenceCount = workspace?.packet.sections.reduce((sum, section) => sum + section.evidence.length, 0) ?? 0; + const renderQuickNotePanel = () => ( + } + > +
+ setNoteForm((current) => ({ ...current, title: event.target.value }))} placeholder="Headline or checkpoint title" /> + setNoteForm((current) => ({ ...current, summary: event.target.value }))} placeholder="One-line summary for skimming and search" /> +