// Typed API wrappers for the HWLab advisor endpoints export interface ChatMessage { id: string role: 'user' | 'assistant' | 'system' content: string created_at: string } export interface ConversationSummary { id: string started_at: string model: string message_count: number } export interface Conversation { id: string started_at: string model: string messages: ChatMessage[] } export async function fetchConversations(): Promise { const res = await fetch('/api/advisor/conversations') if (res.status === 404) return [] if (!res.ok) { const body = await res.json().catch(() => ({ error: res.statusText })) throw new Error((body as { error?: string }).error ?? `HTTP ${res.status}`) } return res.json() as Promise } export async function fetchConversation(id: string): Promise { const res = await fetch(`/api/advisor/conversations/${id}`) if (!res.ok) { const body = await res.json().catch(() => ({ error: res.statusText })) throw new Error((body as { error?: string }).error ?? `HTTP ${res.status}`) } return res.json() as Promise } export async function streamChat( params: { conversationId?: string; message: string; model: string }, onToken: (token: string, conversationId: string) => void, onDone: () => void, onError: (err: string) => void, ): Promise { let res: Response try { res = await fetch('/api/advisor/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conversation_id: params.conversationId, message: params.message, model: params.model, }), }) } catch (e) { onError((e as Error).message) return } if (!res.ok) { const body = await res.json().catch(() => ({ error: res.statusText })) onError((body as { error?: string }).error ?? `HTTP ${res.status}`) return } if (!res.body) { onError('No response body from server') return } const reader = res.body.getReader() const decoder = new TextDecoder() let buffer = '' try { while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) // Split on double newline (SSE event delimiter) const events = buffer.split('\n\n') // Keep any incomplete trailing fragment in buffer buffer = events.pop() ?? '' for (const event of events) { const dataLine = event .split('\n') .find((line) => line.startsWith('data: ')) if (!dataLine) continue const data = dataLine.slice('data: '.length).trim() if (data === '[DONE]') { onDone() return } try { const parsed = JSON.parse(data) as { token: string; conversation_id: string } onToken(parsed.token, parsed.conversation_id) } catch { onError(`Failed to parse SSE event: ${data}`) return } } } } catch (e) { onError((e as Error).message) } }