Files
deerflow-code/frontend-web/src/open-canvas/api/rewrite.ts
T
2026-05-17 20:18:11 +08:00

107 lines
3.4 KiB
TypeScript

import { apiFetch } from "@/core/api/fetch-client";
import { getBackendBaseURL } from "@/core/config";
export type RewriteAction = "polish" | "expand" | "shorten" | "tone";
export type RewriteTone =
| "professional"
| "casual"
| "readable"
| "academic"
| "expressive";
export interface RewriteRequest {
threadId?: string | null;
selection: { from: number; to: number; text: string };
action: RewriteAction;
tone?: RewriteTone;
modelName?: string;
surroundingContext?: { before?: string; after?: string };
customInstruction?: string;
followUpInstruction?: string;
previousResult?: string;
}
export type RewriteStreamEvent =
| { type: "thinking"; chunk: string }
| { type: "text"; chunk: string }
| { type: "done" }
| { type: "error"; message: string };
// ── SSE parser ────────────────────────────────────────────────────────────────
async function* parseSse(body: ReadableStream<Uint8Array>): AsyncGenerator<{ event: string; data: string }> {
const reader = body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let currentEvent = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
const trimmed = line.trimEnd();
if (trimmed.startsWith("event: ")) {
currentEvent = trimmed.slice(7);
} else if (trimmed.startsWith("data: ")) {
yield { event: currentEvent, data: trimmed.slice(6) };
currentEvent = "";
}
// empty line resets event field (SSE spec)
else if (trimmed === "") {
currentEvent = "";
}
}
}
} finally {
reader.releaseLock();
}
}
// ── Streaming entry point ─────────────────────────────────────────────────────
export async function* streamRewrite(
req: RewriteRequest,
signal?: AbortSignal,
): AsyncGenerator<RewriteStreamEvent> {
const res = await apiFetch(`${getBackendBaseURL()}/api/writing/rewrite`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(req),
signal,
});
if (!res.ok) {
const body = await res.json().catch(() => ({})) as Record<string, unknown>;
const msg = (body?.detail as string | undefined) ?? (body?.message as string | undefined) ?? `HTTP ${res.status}`;
throw new Error(msg);
}
if (!res.body) throw new Error("No response body");
for await (const { event, data } of parseSse(res.body)) {
if (signal?.aborted) return;
try {
const parsed = JSON.parse(data) as Record<string, unknown>;
if (event === "thinking") {
yield { type: "thinking", chunk: parsed.chunk as string };
} else if (event === "text") {
yield { type: "text", chunk: parsed.chunk as string };
} else if (event === "done") {
yield { type: "done" };
return;
} else if (event === "error") {
yield { type: "error", message: parsed.message as string };
return;
}
} catch {
// skip malformed frame
}
}
}