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 { 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 { 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 { 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 { 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 { 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(); } export interface TicketSummaryData { total: number; by_status: { pending: number; processing: number; resolved: number }; by_priority: { P0: number; P1: number; P2: number; P3: number }; } export async function fetchTicketSummary(): Promise { const res = await fetch(`${API_URL}/api/tickets/summary`); if (!res.ok) throw new Error("Failed to fetch ticket summary"); return res.json(); } // ── Attachments ───────────────────────────────────────────────────────────── export interface AttachmentData { id: string; filename: string; content_type: string; blob_url: string; size_bytes: number; created_at: string; } export async function uploadAttachment( file: File, conversationId?: string, ): Promise { const form = new FormData(); form.append("data", file); const params = conversationId ? `?conversation_id=${conversationId}` : ""; const res = await fetch(`${API_URL}/api/attachments/upload${params}`, { method: "POST", body: form, }); if (!res.ok) { const text = await res.text().catch(() => "Unknown error"); throw new Error(`Upload failed (${res.status}): ${text}`); } return res.json(); } export function getAttachmentDownloadUrl(attachmentId: string): string { return `${API_URL}/api/attachments/${attachmentId}/download`; } // ── SSE Chat Stream ────────────────────────────────────────────────────────── export interface ChatStreamEvent { type: "token" | "status" | "tool_start" | "tool_end" | "tool_error" | "done" | "error"; // token content?: string; // status stage?: string; message?: string; // tool_start / tool_end / tool_error tool?: string; title?: string; input_summary?: string; output_summary?: string; error_summary?: string; status?: "success" | "error"; duration_ms?: number; ts?: number; } export interface TraceItem { id: string; type: "status" | "tool_start" | "tool_end" | "tool_error"; tool?: string; title: string; message?: string; inputSummary?: string; outputSummary?: string; errorSummary?: string; itemStatus: "running" | "success" | "error" | "info"; durationMs?: number; startTs: number; } 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) { // Process any remaining data in buffer (last line may lack trailing newline) if (buffer.trim()) { const trimmed = buffer.trim(); if (trimmed.startsWith("data: ")) { const json = trimmed.slice(6); if (json) { try { const event: ChatStreamEvent = JSON.parse(json); onEvent(event); } catch { // skip malformed } } } } 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; }