import type { AgentChatMessage } from "../types"; import type { AnswerUiPart } from "./contract"; /** Short notes and map confirmations stay inline; dense data and reports can expand. */ export function canExpandAnswer(parts: AnswerUiPart[]): boolean { let textLength = 0; for (const part of parts) { if (part.type === "text") { textLength += part.text.replace(/\[\[ui:[^\]\r\n]*\]\]/g, "").length; } else { for (const node of Object.values(part.block.spec.elements)) { if (["Table", "Chart", "FollowUpForm"].includes(node.type)) return true; if (node.type === "Text" && typeof node.props.text === "string") textLength += node.props.text.length; if (node.type === "Evidence" && typeof node.props.summary === "string") textLength += node.props.summary.length; if (node.type === "Result" && typeof node.props.summary === "string") textLength += node.props.summary.length; } } } return textLength >= 1200; } /** Readable copy/TTS text for the same ordered parts shown in the answer. */ export function getAnswerPartsTranscript(parts: AnswerUiPart[]): string { return parts.reduce((text, part) => { const next = part.type === "text" ? part.text : part.block.fallback_text; if (!text || !next) return text + next; return text + (text.endsWith("\n") || next.startsWith("\n") ? "" : "\n\n") + next; }, ""); } /** Final report transcript excludes process blocks and resolves authorized anchors. */ export function getFinalAnswerTranscript(message: AgentChatMessage): string { return getAnswerPartsTranscript(getAnswerPresentation(message).report); } function splitAnchors(content: string): Array<{ text: string } | { id: string }> { const result: Array<{ text: string } | { id: string }> = []; let buffer = ""; let fence: { character: string; length: number } | undefined; for (const line of content.match(/[^\n]*\n|[^\n]+$/g) ?? []) { const bare = line.replace(/\r?\n$/, ""); const delimiter = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(bare); if (fence) { buffer += line; if (delimiter && delimiter[1][0] === fence.character && delimiter[1].length >= fence.length && !delimiter[2].trim()) fence = undefined; continue; } if (delimiter) { fence = { character: delimiter[1][0], length: delimiter[1].length }; buffer += line; continue; } const anchor = /^ {0,3}\[\[ui:([^\]\r\n]*)\]\][ \t]*$/.exec(bare); if (!anchor) { buffer += line; continue; } if (buffer.trim()) result.push({ text: buffer }); buffer = ""; result.push({ id: anchor[1] }); } if (buffer.trim()) result.push({ text: buffer }); return result; } export function getAnswerPresentation(message: AgentChatMessage): { report: AnswerUiPart[]; process: AnswerUiPart[]; final: boolean; } { const parts = message.answerParts ?? (message.content ? [{ type: "text" as const, text: message.content }] : []); if (!message.finalAnswer) { const isProcess = (part: AnswerUiPart) => part.type === "answer-ui" && part.block.phase === "process"; return { report: parts.filter((part) => !isProcess(part)), process: parts.filter(isProcess), final: false }; } const selected = new Set(message.finalAnswer.uiBlockIds); const available = new Map(parts.flatMap((part) => part.type === "answer-ui" ? [[part.block.block_id, part] as const] : [])); const mounted = new Set(); const anchored = new Set(); const report: AnswerUiPart[] = []; for (const segment of splitAnchors(message.finalAnswer.content)) { if ("text" in segment) { report.push({ type: "text", text: segment.text }); continue; } anchored.add(segment.id); const part = available.get(segment.id); if (!selected.has(segment.id) || !part || part.block.phase === "process") { report.push({ type: "text", text: "交互内容暂不可用。\n" }); } else if (!mounted.has(segment.id)) { mounted.add(segment.id); report.push(part); } } // Old reports carried references without positional anchors. for (const id of selected) { const part = available.get(id); if (part && part.block.phase !== "process" && !mounted.has(id) && !anchored.has(id)) { mounted.add(id); report.push(part); } } const process = parts.filter((part) => part.type === "answer-ui" ? !mounted.has(part.block.block_id) : part.text.trim() !== message.finalAnswer!.content.trim()); return { report, process, final: true }; }