| | interface StreamChatCallbacks { |
| | onText: (chunk: string) => void; |
| | onToolStart: (tool: string, callId: string) => void; |
| | onToolResult: (tool: string, callId: string, result: Record<string, unknown>) => void; |
| | onDone: () => void; |
| | onError: (message: string) => void; |
| | } |
| |
|
| | export async function streamChatMessage( |
| | patientId: string, |
| | content: string, |
| | image: File | null, |
| | callbacks: StreamChatCallbacks |
| | ): Promise<void> { |
| | try { |
| | const formData = new FormData(); |
| | formData.append('content', content); |
| | if (image) formData.append('image', image); |
| |
|
| | const response = await fetch(`/api/patients/${patientId}/chat`, { |
| | method: 'POST', |
| | body: formData, |
| | }); |
| |
|
| | const reader = response.body?.getReader(); |
| | const decoder = new TextDecoder(); |
| |
|
| | if (!reader) { |
| | callbacks.onDone(); |
| | return; |
| | } |
| |
|
| | while (true) { |
| | const { done, value } = await reader.read(); |
| | if (done) break; |
| |
|
| | const text = decoder.decode(value); |
| | const lines = text.split('\n').filter(l => l.startsWith('data: ')); |
| |
|
| | for (const line of lines) { |
| | const data = line.slice('data: '.length); |
| | try { |
| | const event = JSON.parse(data); |
| | if (event.type === 'text') { |
| | callbacks.onText(event.content ?? ''); |
| | } else if (event.type === 'tool_start') { |
| | callbacks.onToolStart(event.tool, event.call_id); |
| | } else if (event.type === 'tool_result') { |
| | callbacks.onToolResult(event.tool, event.call_id, event.result ?? {}); |
| | } else if (event.type === 'done') { |
| | callbacks.onDone(); |
| | return; |
| | } else if (event.type === 'error') { |
| | callbacks.onError(event.message ?? 'Unknown error'); |
| | } |
| | } catch { |
| | |
| | } |
| | } |
| | } |
| | callbacks.onDone(); |
| | } catch (error) { |
| | console.error('Stream error:', error); |
| | callbacks.onError(String(error)); |
| | } |
| | } |
| |
|