import { useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { useCellValue } from "@mdxeditor/gurx"; import { $getSelection, $isElementNode, $isNodeSelection, $isRangeSelection, COMMAND_PRIORITY_HIGH, DELETE_CHARACTER_COMMAND, DELETE_LINE_COMMAND, DELETE_WORD_COMMAND, mergeRegister, REMOVE_TEXT_COMMAND, } from "lexical"; import { SidecarCard } from "../card"; import { SendAction } from "../send-action"; import { Fold, Swap, useLast } from "./research-motion"; import { widgets$ } from "../widget-options"; import { $isResearchNode } from "@chopin/dialect"; import { CloseIcon, SparkleIcon, WarningIcon } from "@chopin/icons"; import type { FormEvent, MouseEvent, ReactNode } from "react"; import type { Research } from "@chopin/protocol"; import type { ResearchNode } from "@chopin/dialect"; import type { LexicalEditor, LexicalNode, RangeSelection } from "lexical"; import type { ResearchStore } from "../widget-options"; const WORDS: Record = { queued: "Waiting to start", searching: "Searching sources", analyzing: "Reading sources", writing: "Writing report", publishing: "Publishing", ready: "Research", failed: "Research failed", cancelled: "Cancelled", }; const TRACK: Research.RequestStage[] = [ "queued", "searching", "analyzing", "writing", "publishing", ]; const REMOVABLE_STAGES = new Set(["failed", "cancelled", "ready"]); export type ResearchComposerProps = { blocked?: string; cancelDisabled?: boolean; cancelLabel?: string; dismissible?: boolean; error?: string; /** Quiet context that does not block submission. */ notice?: string; onCancel: () => void; onChange: (value: string) => void; onEscape?: () => void; onSubmit: () => void; question: string; questionLocked?: boolean; submitLabel?: string; submitting?: boolean; }; export function researchComposerKey(event: { key: string; metaKey: boolean; ctrlKey: boolean; isComposing: boolean; }): "dismiss" | "ignore" | "newline" | "submit" { if (event.isComposing) return "ignore"; if (event.key === "Escape") return "dismiss"; if (event.key !== "Enter") return "ignore"; return event.metaKey || event.ctrlKey ? "newline" : "submit"; } type ResearchComposerKeyEvent = { ctrlKey: boolean; currentTarget: { readOnly: boolean; selectionEnd: number; selectionStart: number; setRangeText(replacement: string, start: number, end: number, selectionMode: "end"): void; value: string; }; key: string; keyCode: number; metaKey: boolean; nativeEvent: { isComposing: boolean }; preventDefault(): void; stopPropagation(): void; }; export function handleResearchComposerKey( event: ResearchComposerKeyEvent, actions: { dismissible: boolean; onChange: (value: string) => void; onDismiss: () => void; onSubmit: () => void; }, ) { let action = researchComposerKey({ key: event.key, metaKey: event.metaKey, ctrlKey: event.ctrlKey, isComposing: event.nativeEvent.isComposing || event.keyCode === 229, }); if (action === "ignore") return; event.preventDefault(); event.stopPropagation(); if (action === "dismiss") { if (actions.dismissible) actions.onDismiss(); return; } if (action === "submit") { actions.onSubmit(); return; } let textarea = event.currentTarget; if (textarea.readOnly) return; textarea.setRangeText( "\n", textarea.selectionStart, textarea.selectionEnd, "end", ); actions.onChange(textarea.value); } export function ResearchComposer( { blocked, cancelDisabled, cancelLabel, dismissible = true, error, notice, onCancel, onChange, onEscape, onSubmit, question, questionLocked, submitLabel = "Start research", submitting, }: ResearchComposerProps, ) { let submit = (event: FormEvent) => { event.preventDefault(); onSubmit(); }; let message = error ?? blocked ?? notice; return (