Files
socweb/frontend/lib/api.ts
T
gongzhiyongandClaude Sonnet 4.6 60acbac464 Fix 3 P0 issues from code review
FCR-1: Set ignoreBuildErrors to false in next.config.mjs
FCR-2: Default API_URL to production backend so static builds work correctly;
       .env.local overrides to localhost for local dev
FCR-3: Remove createConversation call in handleSend; generate UUID client-side
       and let backend streamChat auto-create the conversation to avoid duplicate writes

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 16:21:33 +08:00

138 lines
4.3 KiB
TypeScript

const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://soc-backend.azurewebsites.net";
// ── Conversations ────────────────────────────────────────────────────────────
export interface ConversationSummary {
id: string;
title: string;
created_at: string;
updated_at: string;
}
export interface ApiMessage {
id: string;
role: "human" | "ai";
content: string;
created_at: string;
}
export interface ConversationDetail extends ConversationSummary {
messages: ApiMessage[];
}
export async function fetchConversations(): Promise<ConversationSummary[]> {
const res = await fetch(`${API_URL}/api/conversations`);
if (!res.ok) throw new Error("Failed to fetch conversations");
return res.json();
}
export async function fetchConversation(id: string): Promise<ConversationDetail> {
const res = await fetch(`${API_URL}/api/conversations/${id}`);
if (!res.ok) throw new Error("Failed to fetch conversation");
return res.json();
}
export async function createConversation(title?: string): Promise<ConversationSummary> {
const res = await fetch(`${API_URL}/api/conversations`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: title ?? "New conversation" }),
});
if (!res.ok) throw new Error("Failed to create conversation");
return res.json();
}
export async function deleteConversation(id: string): Promise<void> {
const res = await fetch(`${API_URL}/api/conversations/${id}`, { method: "DELETE" });
if (!res.ok) throw new Error("Failed to delete conversation");
}
// ── Tickets ──────────────────────────────────────────────────────────────────
export interface TicketData {
id: string;
title: string;
status: "pending" | "processing" | "resolved";
priority: "P0" | "P1" | "P2" | "P3";
createdAt: string;
}
export async function fetchTickets(page = 1, pageSize = 20): Promise<TicketData[]> {
const res = await fetch(`${API_URL}/api/tickets?page=${page}&page_size=${pageSize}`);
if (!res.ok) throw new Error("Failed to fetch tickets");
return res.json();
}
// ── SSE Chat Stream ──────────────────────────────────────────────────────────
export interface ChatStreamEvent {
type: "token" | "tool_start" | "tool_end" | "done";
content?: string;
tool?: string;
}
export function streamChat(
message: string,
conversationId: string,
tools: string[],
model: "flash" | "pro",
onEvent: (event: ChatStreamEvent) => void,
onError: (error: Error) => void,
onDone: () => void,
): AbortController {
const controller = new AbortController();
fetch(`${API_URL}/api/chat/stream`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message, conversation_id: conversationId, tools, model }),
signal: controller.signal,
})
.then(async (res) => {
if (!res.ok) {
const text = await res.text().catch(() => "Unknown error");
throw new Error(`Chat stream failed (${res.status}): ${text}`);
}
const reader = res.body?.getReader();
if (!reader) throw new Error("No response body");
const decoder = new TextDecoder();
let buffer = "";
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.trim();
if (!trimmed.startsWith("data: ")) continue;
const json = trimmed.slice(6);
if (!json) continue;
try {
const event: ChatStreamEvent = JSON.parse(json);
onEvent(event);
if (event.type === "done") {
onDone();
return;
}
} catch {
// skip malformed events
}
}
}
onDone();
})
.catch((err) => {
if (err.name !== "AbortError") {
onError(err);
}
});
return controller;
}