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