feat(06-03): add advisor API wrappers (streamChat, fetchConversations, fetchConversation)
- Types: ChatMessage, ConversationSummary, Conversation - streamChat: POST /api/advisor/chat with ReadableStream SSE parsing - fetchConversations: GET with 404-tolerant empty array fallback - fetchConversation: GET by id
This commit is contained in:
parent
ae34bc73fe
commit
811223ddf7
1 changed files with 117 additions and 0 deletions
117
web/src/api/advisor.ts
Normal file
117
web/src/api/advisor.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
// 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<ConversationSummary[]> {
|
||||
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<ConversationSummary[]>
|
||||
}
|
||||
|
||||
export async function fetchConversation(id: string): Promise<Conversation> {
|
||||
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<Conversation>
|
||||
}
|
||||
|
||||
export async function streamChat(
|
||||
params: { conversationId?: string; message: string; model: string },
|
||||
onToken: (token: string, conversationId: string) => void,
|
||||
onDone: () => void,
|
||||
onError: (err: string) => void,
|
||||
): Promise<void> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue