unsloth/studio/frontend/src/features/chat/utils/parse-assistant-content.ts
Daniel Han 85314ed162
Studio frontend: reduce and tighten code comments (#6099)
Trim and tighten code comments across studio/frontend TS/JS. Comment-only: every changed file verified code-identical to main via the TypeScript printer signature comparison.
2026-06-08 23:10:35 -07:00

69 lines
2 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import type { ChatModelRunResult } from "@assistant-ui/react";
type ContentPart = NonNullable<ChatModelRunResult["content"]>[number];
const THINK_OPEN_TAG = "<think>";
const THINK_CLOSE_TAG = "</think>";
// ContentPart from @assistant-ui/react has readonly fields, so coalescing via
// `last.text += text` fails (TS2540). Instead replace the last element with a
// fresh merged object: same allocation cost as mutation but type-safe.
function appendTextPart(parts: ContentPart[], text: string): void {
if (!text) return;
const last = parts.at(-1);
if (last?.type === "text") {
parts[parts.length - 1] = { type: "text", text: last.text + text };
return;
}
parts.push({ type: "text", text });
}
function appendReasoningPart(parts: ContentPart[], text: string): void {
if (!text) return;
const last = parts.at(-1);
if (last?.type === "reasoning") {
parts[parts.length - 1] = { type: "reasoning", text: last.text + text };
return;
}
parts.push({ type: "reasoning", text });
}
export function parseAssistantContent(
raw: string,
): ContentPart[] {
const parts: ContentPart[] = [];
if (!raw) {
return parts;
}
let cursor = 0;
while (cursor < raw.length) {
const openIndex = raw.indexOf(THINK_OPEN_TAG, cursor);
if (openIndex === -1) {
appendTextPart(parts, raw.slice(cursor));
break;
}
appendTextPart(parts, raw.slice(cursor, openIndex));
const reasoningStart = openIndex + THINK_OPEN_TAG.length;
const closeIndex = raw.indexOf(THINK_CLOSE_TAG, reasoningStart);
if (closeIndex === -1) {
appendReasoningPart(parts, raw.slice(reasoningStart));
break;
}
appendReasoningPart(parts, raw.slice(reasoningStart, closeIndex));
cursor = closeIndex + THINK_CLOSE_TAG.length;
}
return parts;
}
export function hasClosedThinkTag(raw: string): boolean {
return raw.includes(THINK_CLOSE_TAG);
}