import { useCallback, useEffect, useRef, useState } from "react"; import { documentPath, researchWorkspacePath } from "@chopin/protocol/document-url"; import * as Api from "./api"; import { rememberChannel } from "./channel-recovery"; import { newestDocumentMetadata } from "./document-actions"; import { DocumentActionsMenu } from "./document-actions-menu"; import { useNavigationDocument } from "./navigation-shell"; import { activeResearchJob, artifactFromJob, beginResearchSubmission, completeResearchSubmission, editResearchSubmission, latestResearchJobRevision, mergeResearchWorkspaceDetail, } from "./research-workspace-model"; import { Wire } from "./wire"; import "./research-workspace.css"; import type { Job, Research, Session } from "@chopin/protocol"; import type { HostedWorkspaceProps } from "./hosted"; import type { DocumentMetadata } from "./document-actions"; import type { ResearchAnswerArtifact, ResearchContinuationArtifact, ResearchInitialArtifact, ResearchSource, ResearchSubmission, } from "./research-workspace-model"; import type { Status } from "./wire"; type ResearchWorkspaceProps = HostedWorkspaceProps & { workspaceId: string }; type ManagedHello = Session.Hello & { archivedAt?: string; canManage: boolean }; type ManagedChannel = Session.Channel & { archivedAt?: string; canManage: boolean }; type ManagedAccess = Session.Access & { canManage: boolean }; type WorkspaceMetadata = DocumentMetadata; function timestamp(value: string): string { let date = new Date(value); return Number.isNaN(date.getTime()) ? "Unknown time" : new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short", }).format(date); } function Time({ value }: { value: string }) { return ; } function parentChannel( room: string, repository: Api.Repository, metadata: { title: string; slug: string }, ): Api.ResearchParentChannel { return { id: room, repositoryId: repository.id, repositoryOwner: repository.owner, repositoryName: repository.name, title: metadata.title, slug: metadata.slug, }; } function answerArtifact( workspaceId: string, turn: Api.ResearchWorkspaceTurn, ): ResearchAnswerArtifact | undefined { let artifact = artifactFromJob(turn.answer); return artifact?.workspaceId === workspaceId && artifact.turnId === turn.id && artifact.kind === turn.kind ? artifact : undefined; } function activeTurn(turn: Api.ResearchWorkspaceTurn): boolean { return activeResearchJob(turn.evidence) || activeResearchJob(turn.answer); } function currentJob(turn: Api.ResearchWorkspaceTurn): Job.Detail | undefined { if (turn.answer) return turn.answer; return turn.evidence; } const STAGE_LABELS: Record = { "public-web": "Searching the public web", "private-document": "Analyzing the private document", "report-synthesis": "Writing the report", "private-answer": "Drafting the answer", }; function jobLabel(detail: Job.Detail | undefined): string { if (!detail) return "Queued"; switch (detail.job.state) { case "pending": return "Queued"; case "paused": return "Paused"; case "running": { let progress = detail.job.progress.toReversed().find(item => item.state === "started"); return progress ? STAGE_LABELS[progress.stage] ?? "Working" : "Working"; } case "completed": return "Completed"; case "failed": return "Could not complete"; case "cancelled": return "Cancelled"; case "superseded": return "Replaced by newer work"; } } function JobProgress( { canCancel, onCancel, turn, }: { canCancel: boolean; onCancel: () => void; turn: Api.ResearchWorkspaceTurn; }, ) { let detail = currentJob(turn); let active = activeTurn(turn); let stages = new Map(); for (let progress of detail?.job.progress ?? []) stages.set(progress.stage, progress); return (

{jobLabel(detail)}

{active && ( )}
{stages.size > 0 && (
    {[...stages.values()].map(progress => (
  • {STAGE_LABELS[progress.stage] ?? "Research work"} {progress.state === "completed" ? "Complete" : progress.state === "interrupted" ? "Interrupted" : "In progress"}
  • ))}
)} {detail && (

Updated

)}
); } function SourceCitations( { sources, urls }: { sources: ResearchSource[]; urls: string[] }, ) { return ( {urls.map(url => { let index = sources.findIndex(source => source.url === url); let source = sources[index]; if (!source) return null; return ( [{index + 1}] ); })} ); } function Sources({ sources }: { sources: ResearchSource[] }) { return (
    {sources.map((source, index) => (
  1. {source.title}
  2. ))}
); } function GeneratedReport( { artifact, createdAt, }: { artifact: ResearchInitialArtifact; createdAt: string; }, ) { let { report, sources } = artifact; return (

Generated report

{report.title}

Generated

{report.summary}

Findings

{report.findings.length > 0 ? (
    {report.findings.map((finding, index) => (
  1. {finding.text}
  2. ))}
) :

No findings were reported.

}

Caveats

{report.caveats.length > 0 ? (
    {report.caveats.map((caveat, index) =>
  • {caveat}
  • )}
) :

No caveats were reported.

}

Sources

{sources.length > 0 ? :

No public sources were used.

}
Document revision {artifact.documentRevision}{" "} was analyzed privately and separately from public web evidence.
); } function unavailableReason( connected: boolean, canEdit: boolean, agent: boolean, backgroundJobs: boolean, webResearch: boolean, ): string | undefined { if (!canEdit) return "Repository write access is required."; if (!agent) return "Research execution is unavailable in this deployment."; if (!backgroundJobs) return "Background work is unavailable in this deployment."; if (!webResearch) return "Public web research is unavailable in this deployment."; if (!connected) return "Reconnect to start public web research."; return undefined; } function DraftConfirmation( { backgroundJobs, canEdit, connected, agent, onSaved, webResearch, workspace, }: { agent: boolean; backgroundJobs: boolean; canEdit: boolean; connected: boolean; onSaved: (detail: Api.ResearchWorkspaceDetail) => void; webResearch: boolean; workspace: Research.WorkspaceSummary; }, ) { let [submission, setSubmission] = useState({ text: workspace.proposedQuestion, }); let [submitting, setSubmitting] = useState(false); let [error, setError] = useState(); let reason = unavailableReason(connected, canEdit, agent, backgroundJobs, webResearch); let submit = async () => { if (submitting || reason || !submission.text.trim()) return; let next = beginResearchSubmission(submission, "confirm"); setSubmission(next); setSubmitting(true); setError(undefined); try { let detail = await Api.confirmResearchWorkspace( workspace.channelId, workspace.id, next.submittedText, next.requestId, ); setSubmission(completeResearchSubmission()); onSaved(detail); } catch (failure) { setError(failure); setSubmitting(false); } }; return (

Private draft

Review before searching