import { usePrompt } from "./drafts.js"; import { LocalInput, TextArea, Markdown } from "@quixos/camino-react/ui"; import { useState } from "react"; import { referenceOf, useCaminoQuery, useCrdtText, useInterface, type ExactReference } from "@quixos/camino-react"; import { interfaces, queries } from "./gen/client.js"; import { accepted, asInterface, errorMessage, type TaskNode } from "./model.js"; export function FeatureConversation({ tasks, navigation, }: { library: ExactReference; reader: ExactReference; navigation: ExactReference; tasks: TaskNode[]; }) { if (!tasks.length) return null; return (

Authoring threads

{tasks.map((task) => ( ))}
); } function FeatureThread({ task, navigation }: { task: TaskNode; navigation: ExactReference }) { const [expanded, setExpanded] = useState( ["queued", "running", "waiting_input"].includes(task.thread?.active?.progress.status ?? task.data.status), ); return (
setExpanded(event.currentTarget.open)} > {snapshot(task.configuration)?.name || snapshot(task.configuration)?.instructions?.slice(0, 80) || "Annotator changes"} {task.thread?.active?.progress.status ?? task.data.status} {expanded && }
); } export function authoringTitle(task: TaskNode) { const config = snapshot(task.configuration); if (config?.name) return config.name; const name = task.thread?.data.title; if (name && name.length <= 64) return name; const types: Record = { overhead: "Text labels", highlight: "Highlight", sidebar: "Sidebar", tooltip: "Tooltip", replace: "Replacement", }; return config?.style ? `${types[config.style] ?? config.style} annotator` : "New annotator"; } type RequestSnapshot = { name?: string; style?: string; reveal?: string; color?: string; fontFamily?: string; fontSize?: number; placement?: "above" | "below"; instructions?: string; }; function snapshot(raw: string): RequestSnapshot | null { try { const value = JSON.parse(raw); return value && typeof value === "object" ? value : null; } catch { return null; } } export function RequestConfiguration({ value }: { value: RequestSnapshot }) { return (
{value.name || "New Annotator"}
{[ ["Type", value.style === "overhead" ? "Text labels" : value.style], [ "Position", value.style === "overhead" ? (value.placement === "below" ? "Below the text" : "Above the text") : null, ], [ "Show annotations", value.reveal === "always" ? "Always" : value.reveal === "hover" ? "When hovering" : value.reveal === "click" ? "When clicked" : value.reveal, ], ["Color", value.color], ["Font", value.fontFamily], ["Font size", value.fontSize], ] .filter(([, value]) => value) .map(([label, value]) => (
{label}
{value}
))}
{value.instructions &&

{value.instructions}

}
); } /** Transcript rendering is independent of the graph controller. Embedded tools * can supply a React view for their original structured request. */ export function ConversationTranscript({ items, initial, }: { items: { key: string; kind: string; text: string }[]; initial?: React.ReactNode; }) { return (
{initial} {items.map((item) => (
{item.kind === "user" ? "You" : "Agent"} {item.kind === "assistant" ? :

{item.text}

}
))}
); } export function Conversation({ task, navigation }: { task: TaskNode; navigation: ExactReference }) { const query = useTask(referenceOf(task)); if (query.status === "error") return (

Unable to load this conversation.

); if (query.status !== "ready") return

Loading conversation…

; return ; } function useTask(reference: ExactReference) { return useCaminoQuery(queries.ConversationView, { root: asInterface(reference, "job"), input: {}, admission: { query: queries.ConversationInputs, input: {} }, }); } type DetailedTask = Extract, { status: "ready" }>["data"]["root"]; function ConversationDetails({ task, navigation }: { task: DetailedTask; navigation: ExactReference }) { const draft = usePrompt(navigation, "followup"); const text = draft.text; const thread = useInterface( interfaces.AgentThread, task.thread ? referenceOf(task.thread) : undefined, !!task.thread, ); const current = task.thread?.active ?? task.request; const request = useInterface(interfaces.AgentRequest, current ? referenceOf(current) : undefined, !!current); const [busy, setBusy] = useState(false), [error, setError] = useState(""); const progress = current?.progress, active = progress && !["completed", "failed", "cancelled", "uncertain"].includes(progress.status); const config = snapshot(task.configuration); const items = [...(task.thread?.items.entries ?? [])] .sort((a, b) => Number(a.node.data.sequence) - Number(b.node.data.sequence)) .filter(({ node }) => ["user", "message", "assistant", "notice"].includes(node.data.kind) && node.data.text); const original = items.find( ({ node }) => node.data.kind === "user" && node.request._camino.ref.object === task.request?._camino.ref.object, )?.node.data.text; async function send() { setBusy(true); setError(""); try { await draft.flush(); if (active && request.status === "ready") await accepted( request.value.steer.call({ attempt: progress!.attempt, generation: progress!.generation, key: crypto.randomUUID(), text, profile: null, createdAt: new Date().toISOString(), }), ); else if (thread.status === "ready") await accepted( thread.value.send.call({ text, data: task.reader ? JSON.stringify({ authoringJob: referenceOf(task), reader: task.reader._camino.ref }) : null, profile: null, response: null, sourceKind: "human", sourceLabel: "Reading feature", publication: null, createdAt: new Date().toISOString(), contexts: task.reader ? [ { ...task.reader._camino.ref, expectation: { kind: "interface", id: "interface:org.quixos.agent:context-subject@1" }, }, ] : [], related: null, retryOf: null, root: null, causalDepth: 0, }), ); draft.clearSubmitted(text); } catch (error) { setError(errorMessage(error)); } finally { setBusy(false); } } return (
Annotator changes {active ? progress.status === "waiting_input" ? "Needs your answer" : "Working on your feature…" : progress?.status === "completed" ? "Agent finished" : progress?.status || task.data.status} {active && ( )}
!(node.data.kind === "user" && node.request._camino.ref.object === task.request?._camino.ref.object), ) .map(({ node }) => ({ key: referenceOf(node).object, kind: node.data.kind, text: node.data.text }))} initial={ config ? ( ) : original ? (
Original request

{original.slice(0, 240)} {original.length > 240 ? "…" : ""}

) : undefined } /> {!items.length && task.data.output &&

{task.data.output}

} {(current?.questions.entries ?? []) .filter(({ node }) => node.data.status === "open" || node.data.status === "pending") .map(({ node }) => ( ))} {(task.data.error || progress?.reason) && (

{task.data.error || progress?.reason}

)}
{ event.preventDefault(); void send(); }} >