/** * The sidecar. * * Everything the plan is waiting on, and everything it has settled: questions * the agent asked, and comments the room made on the prose. Both are decisions * in the same sense, so they share one list rather than two tabs — an accepted * comment *is* a decision, and would otherwise have to pick a side. * * Outstanding items come first, in document order, because one of those is * blocking somebody. Everything resolved collapses behind a disclosure: it is * the record, worth keeping and not worth scrolling past. A dismissed thread is * not shown at all; the transcript is where it left its trace. * * An item also knows where it lives. Hovering a question lights the passage it * concerns; hovering a comment lights the phrase it marks. The highlight is * written to the DOM rather than the document — it is one reader's pointer, not * a fact about the plan, and putting it in the document would send it to * everybody else and make it undoable. */ import { useEffect, useRef, useState } from "react"; import { DraftCard, ThreadCard } from "./comments"; import { useQuestionnaires } from "./questionnaires"; import { useThreads } from "./threads"; import { QuestionnaireCard } from "./widgets/questionnaire"; import type { Transport } from "@chopin/question/react"; import type { QuestionnaireEntry, QuestionnaireStore } from "./questionnaires"; import type { ThreadStore } from "./threads"; export type DecisionsProps = { store: QuestionnaireStore; threads: ThreadStore; wire?: Transport; connected?: boolean; /** * An item to bring into view. * * Carries a token as well as an id so asking for the same one twice still * scrolls — naming it again means "show me", not "it is already open". */ reveal?: { widget: string; token: number }; }; function undecided(entry: QuestionnaireEntry): boolean { return entry.value.questions.some(question => question.answer === undefined); } export function Decisions({ connected, reveal, store, threads, wire }: DecisionsProps) { let entries = useQuestionnaires(store); let state = useThreads(threads); let content = useRef(null); let [history, setHistory] = useState(false); // Leaving the pane should not leave the prose lit. A highlight belongs to // the pointer that asked for it. useEffect(() => () => store.clear(), [store]); useEffect(() => { if (!reveal) return; let target = content.current?.querySelector( `[data-plan-sidecar-questionnaire="${CSS.escape(reveal.widget)}"]`, ); target?.scrollIntoView({ block: "center", behavior: "smooth" }); }, [entries, reveal]); let open = state.threads.filter(view => view.thread.status === "open"); let accepted = state.threads.filter(view => view.thread.status === "accepted"); let waiting = entries.filter(undecided); let settled = entries.filter(entry => !undecided(entry)); let outstanding = waiting.length + open.length; let resolved = settled.length + accepted.length; let card = (view: (typeof state.threads)[number]) => ( threads.accept(view.thread.id)} onBlur={() => threads.focus(undefined)} onDismiss={() => threads.dismiss(view.thread.id)} onFocus={() => threads.focus(view.thread.id)} onReply={text => threads.reply(view.thread.id, text)} onRetry={() => threads.retry(view.thread.id)} onTyping={writing => threads.announce(view.thread.id, writing)} quote={view.quote} view={view} writing={state.writing[view.thread.id]} /> ); let question = (entry: QuestionnaireEntry) => ( store.highlight(entry.id, q, relation)} onRelationLeave={() => store.clear()} relations={store.counts(entry.id)} value={entry.value} wire={wire} /> ); return (
Decisions {outstanding > 0 && {outstanding}}
{state.draft && (
threads.draft(undefined)} onSend={text => threads.start(text)} quote={state.draft.quote} />
)} {state.error && (

{state.error} Select the passage again.

)} {outstanding === 0 && resolved === 0 && !state.draft ? (

Select any of the plan to comment on it. Questions the agent asks appear here too, and both stay as a record of what was decided.

) : (
{waiting.map(question)} {open.map(card)}
)} {resolved > 0 && (
0 || state.draft ? "mt-3" : ""}> {history && (
{accepted.map(card)} {settled.map(question)}
)}
)}
); }