Masters-four-Tab-OpenAI / frontend /src /utils /responseShellMarkdown.ts
Pete Dunn
chore: commit remaining UI polish, tests, and release gate artifacts
6132f30
Raw
History Blame Contribute Delete
2.17 kB
export type ResponseShellSections = {
result: string;
why: string;
nextAction: string;
rest: string;
hasShell: boolean;
};
type HeadingKind = "result" | "why" | "nextAction";
const SHELL_HEADING_RE = /^\s*\*\*(Result|Why|Next action|Action)\*\*\s*$/gim;
function headingKind(raw: string): HeadingKind | null {
const low = String(raw || "").trim().toLowerCase();
if (low === "result") return "result";
if (low === "why") return "why";
if (low === "next action" || low === "action") return "nextAction";
return null;
}
export function splitResponseShellMarkdown(content: string): ResponseShellSections {
const text = String(content || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const matches: Array<{ kind: HeadingKind; start: number; bodyStart: number }> = [];
let match: RegExpExecArray | null = null;
while ((match = SHELL_HEADING_RE.exec(text)) !== null) {
const kind = headingKind(match[1]);
if (!kind) continue;
matches.push({ kind, start: match.index, bodyStart: SHELL_HEADING_RE.lastIndex });
}
if (!matches.length) {
return {
result: text.trim(),
why: "",
nextAction: "",
rest: "",
hasShell: false,
};
}
const buckets: Record<HeadingKind, string> = {
result: "",
why: "",
nextAction: "",
};
const consumed: Array<[number, number]> = [];
for (let i = 0; i < matches.length; i += 1) {
const curr = matches[i];
const next = matches[i + 1];
const end = next ? next.start : text.length;
const body = text.slice(curr.bodyStart, end).trim();
if (body && !buckets[curr.kind]) buckets[curr.kind] = body;
consumed.push([curr.start, end]);
}
let rest = "";
let cursor = 0;
for (const [start, end] of consumed.sort((a, b) => a[0] - b[0])) {
if (start > cursor) rest += text.slice(cursor, start);
cursor = Math.max(cursor, end);
}
if (cursor < text.length) rest += text.slice(cursor);
return {
result: buckets.result.trim(),
why: buckets.why.trim(),
nextAction: buckets.nextAction.trim(),
rest: rest.trim(),
hasShell: Boolean(buckets.result || buckets.why || buckets.nextAction),
};
}