Spaces:
Runtime error
Runtime error
| export interface TextBlock { | |
| type?: string; | |
| text?: string; | |
| [key: string]: unknown; | |
| } | |
| export interface ChatMessageLike { | |
| role: string; | |
| content?: string | TextBlock[] | unknown[]; | |
| [key: string]: unknown; | |
| } | |
| export function isTextBlock(value: unknown): value is TextBlock { | |
| return ( | |
| !!value && | |
| typeof value === "object" && | |
| "text" in value && | |
| typeof (value as TextBlock).text === "string" && | |
| ((value as TextBlock).type === undefined || | |
| (value as TextBlock).type === "text" || | |
| (value as TextBlock).type === "input_text") | |
| ); | |
| } | |
| export function extractTextContent(content: ChatMessageLike["content"]): string { | |
| if (typeof content === "string") return content; | |
| if (!Array.isArray(content)) return ""; | |
| const textParts: string[] = []; | |
| for (const part of content) { | |
| if (isTextBlock(part) && part.text) { | |
| textParts.push(part.text); | |
| } | |
| } | |
| return textParts.join("\n"); | |
| } | |
| export function mapTextContent( | |
| msg: ChatMessageLike, | |
| transform: (text: string, index: number) => string | |
| ): ChatMessageLike { | |
| if (typeof msg.content === "string") { | |
| return { ...msg, content: transform(msg.content, 0) }; | |
| } | |
| if (!Array.isArray(msg.content)) return msg; | |
| let textIndex = 0; | |
| let changed = false; | |
| const content = msg.content.map((part) => { | |
| if (!isTextBlock(part)) return part; | |
| const nextText = transform(part.text ?? "", textIndex); | |
| textIndex++; | |
| if (nextText === part.text) return part; | |
| changed = true; | |
| return { ...part, text: nextText }; | |
| }); | |
| return changed ? { ...msg, content } : msg; | |
| } | |
| export function replaceTextContent(msg: ChatMessageLike, newText: string): ChatMessageLike { | |
| if (typeof msg.content === "string" || !Array.isArray(msg.content)) { | |
| return { ...msg, content: newText }; | |
| } | |
| // The first text block receives `newText`. Trailing text blocks are normally | |
| // already subsumed by `newText` (callers build it from the JOIN of all text | |
| // blocks via extractTextContent), so we drop them to avoid duplicating content. | |
| // But if a caller's `newText` does NOT contain a trailing block's text, dropping | |
| // it would silently lose content the model can no longer see (B-AGG-TEXTDROP) — | |
| // so in that case we keep the trailing block instead of returning []. | |
| let replaced = false; | |
| const content = msg.content.flatMap((part) => { | |
| if (!isTextBlock(part)) return [part]; | |
| if (!replaced) { | |
| replaced = true; | |
| return [{ ...part, text: newText }]; | |
| } | |
| const partText = part.text ?? ""; | |
| if (partText && !newText.includes(partText)) return [part]; | |
| return []; | |
| }); | |
| if (!replaced) { | |
| return { ...msg, content: [{ type: "text", text: newText }, ...msg.content] }; | |
| } | |
| return { ...msg, content }; | |
| } | |