添加全局 Copilot 聊天框组件
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import { apiFetch } from "@/lib/apiFetch";
|
||||
import { config } from "@config/config";
|
||||
|
||||
export type StreamEvent =
|
||||
| { type: "token"; conversationId: string; content: string }
|
||||
| { type: "done"; conversationId: string }
|
||||
| { type: "error"; conversationId?: string; message: string; detail?: string };
|
||||
|
||||
type StreamOptions = {
|
||||
message: string;
|
||||
conversationId?: string;
|
||||
signal?: AbortSignal;
|
||||
onEvent: (event: StreamEvent) => void;
|
||||
};
|
||||
|
||||
const parseEventBlock = (block: string): { event?: string; data?: string } => {
|
||||
const lines = block.split("\n");
|
||||
let event: string | undefined;
|
||||
const dataLines: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("event:")) {
|
||||
event = line.slice("event:".length).trim();
|
||||
} else if (line.startsWith("data:")) {
|
||||
dataLines.push(line.slice("data:".length).trim());
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
event,
|
||||
data: dataLines.length ? dataLines.join("\n") : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const streamCopilotChat = async ({
|
||||
message,
|
||||
conversationId,
|
||||
signal,
|
||||
onEvent,
|
||||
}: StreamOptions) => {
|
||||
const response = await apiFetch(`${config.BACKEND_URL}/api/v1/copilot/chat/stream`, {
|
||||
method: "POST",
|
||||
signal,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
conversation_id: conversationId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
const detail = await response.text();
|
||||
onEvent({
|
||||
type: "error",
|
||||
message: "stream request failed",
|
||||
detail,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const blocks = buffer.split("\n\n");
|
||||
buffer = blocks.pop() ?? "";
|
||||
|
||||
for (const block of blocks) {
|
||||
const { event, data } = parseEventBlock(block);
|
||||
if (!event || !data) continue;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data) as {
|
||||
conversationId?: string;
|
||||
content?: string;
|
||||
message?: string;
|
||||
detail?: string;
|
||||
};
|
||||
if (event === "token") {
|
||||
onEvent({
|
||||
type: "token",
|
||||
conversationId: parsed.conversationId ?? "",
|
||||
content: parsed.content ?? "",
|
||||
});
|
||||
} else if (event === "done") {
|
||||
onEvent({
|
||||
type: "done",
|
||||
conversationId: parsed.conversationId ?? "",
|
||||
});
|
||||
} else if (event === "error") {
|
||||
onEvent({
|
||||
type: "error",
|
||||
conversationId: parsed.conversationId,
|
||||
message: parsed.message ?? "unknown error",
|
||||
detail: parsed.detail,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
onEvent({
|
||||
type: "error",
|
||||
message: "invalid SSE data payload",
|
||||
detail: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user