- Add fetchTicketSummary() and TicketSummaryData type to lib/api.ts - Change TicketSummary props from tickets[] array to summary object with total, by_status, and by_priority fields - Update GeminiChat to fetch summary data instead of ticket list - TicketSummary now renders status and priority breakdowns from the summary endpoint instead of computing from raw ticket data Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
150 lines
4.7 KiB
TypeScript
150 lines
4.7 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();
|
|
}
|
|
|
|
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<TicketSummaryData> {
|
|
const res = await fetch(`${API_URL}/api/tickets/summary`);
|
|
if (!res.ok) throw new Error("Failed to fetch ticket summary");
|
|
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;
|
|
}
|