104 lines
4.4 KiB
TypeScript
104 lines
4.4 KiB
TypeScript
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<string>();
|
|
const anchored = new Set<string>();
|
|
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 };
|
|
}
|