openclaw/apps/web/lib/report-blocks.ts
2026-02-11 18:35:35 -08:00

76 lines
1.9 KiB
TypeScript

/**
* Pure utility functions for parsing report-json blocks from chat text.
* Extracted from chat-message.tsx for testability.
*/
export type ReportConfig = {
version: number;
title: string;
description?: string;
panels: Array<{
id: string;
title: string;
type: string;
sql: string;
mapping: Record<string, unknown>;
size?: string;
}>;
filters?: Array<{
id: string;
type: string;
label: string;
column: string;
sql?: string;
}>;
};
export type ParsedSegment =
| { type: "text"; text: string }
| { type: "report-artifact"; config: ReportConfig };
/**
* Split text containing ```report-json ... ``` fenced blocks into
* alternating text and report-artifact segments.
*/
export function splitReportBlocks(text: string): ParsedSegment[] {
const reportFenceRegex = /```report-json\s*\n([\s\S]*?)```/g;
const segments: ParsedSegment[] = [];
let lastIndex = 0;
for (const match of text.matchAll(reportFenceRegex)) {
const before = text.slice(lastIndex, match.index);
if (before.trim()) {
segments.push({ type: "text", text: before });
}
try {
const config = JSON.parse(match[1]) as ReportConfig;
if (config.panels && Array.isArray(config.panels)) {
segments.push({ type: "report-artifact", config });
} else {
// Invalid report config -- render as plain text
segments.push({ type: "text", text: match[0] });
}
} catch {
// Invalid JSON -- render as plain text
segments.push({ type: "text", text: match[0] });
}
lastIndex = (match.index ?? 0) + match[0].length;
}
const remaining = text.slice(lastIndex);
if (remaining.trim()) {
segments.push({ type: "text", text: remaining });
}
return segments;
}
/**
* Check if text contains any report-json fenced blocks.
*/
export function hasReportBlocks(text: string): boolean {
return text.includes("```report-json");
}