fix: unify chat process and reply presentation
This commit is contained in:
@@ -1,25 +1,103 @@
|
||||
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 ??
|
||||
const parts = message.answerParts ??
|
||||
(message.content ? [{ type: "text" as const, text: message.content }] : []);
|
||||
if (!message.finalAnswer) return { report: parts, process: [], final: false };
|
||||
const selected = new Set(message.finalAnswer.uiBlockIds);
|
||||
const report: AnswerUiPart[] = [{ type: "text", text: message.finalAnswer.content }];
|
||||
for (const id of selected) {
|
||||
const part = parts.find((item) => item.type === "answer-ui" && item.block.block_id === id);
|
||||
if (part) report.push(part);
|
||||
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 process = parts.filter((part) =>
|
||||
part.type === "answer-ui"
|
||||
? !selected.has(part.block.block_id)
|
||||
: part.text.trim() !== message.finalAnswer!.content.trim()
|
||||
);
|
||||
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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user