refactor: replace SSE streaming with simple JSON chat endpoint

Replace Server-Sent Events streaming implementation with straightforward
JSON request/response pattern to simplify debugging and reduce complexity.

Changes:
- server.js: Convert /api/chat/stream to /api/chat with JSON response
- src/lib/api.ts: Remove SSE parsing logic, add ChatResponse interface
- src/hooks/useChat.ts: Handle JSON response instead of stream chunks

This change makes the request flow easier to debug and troubleshoot
while maintaining the same conversation functionality.
This commit is contained in:
Danny
2026-01-20 13:49:44 -06:00
parent a4f9df6906
commit 0aaa393a6d
3 changed files with 74 additions and 209 deletions
+39 -95
View File
@@ -1,9 +1,7 @@
import type { ChatMode } from '@/types/chat'
// In production, use relative URL to route through the proxy server
// For local development, set VITE_API_URL=http://localhost:8000 in .env
const API_BASE_URL = '/api'
const API_ENDPOINT = import.meta.env.VITE_API_ENDPOINT || '/chat/stream'
const USE_MOCK_DATA = false // Set to true to use mock data for testing
// Session management
@@ -35,39 +33,34 @@ const MOCK_RESPONSES = {
],
}
// Mock streaming function
async function* mockChatStream(
// Chat response type from backend
export interface ChatResponse {
conversation_id: string
response: string
mode: string
sources: string[]
}
// Mock chat function
async function mockChat(
_question: string,
mode: ChatMode
): AsyncGenerator<StreamEvent> {
const sessionId = crypto.randomUUID()
yield { type: 'session_id', data: sessionId }
): Promise<ChatResponse> {
// Select a random response based on mode
const responses = MOCK_RESPONSES[mode]
const response = responses[Math.floor(Math.random() * responses.length)]
// Simulate streaming by yielding words with delays
const words = response.split(' ')
for (let i = 0; i < words.length; i++) {
// Add space before word (except first word)
const chunk = i === 0 ? words[i] : ' ' + words[i]
yield { type: 'chunk', data: chunk }
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 500))
// Random delay between 30-80ms to simulate typing
await new Promise(resolve => setTimeout(resolve, Math.random() * 50 + 30))
return {
conversation_id: crypto.randomUUID(),
response,
mode,
sources: [],
}
yield { type: 'done', data: null }
}
// SSE stream event types
export type StreamEvent =
| { type: 'chunk'; data: string }
| { type: 'done'; data: null }
| { type: 'error'; data: string }
| { type: 'session_id'; data: string }
class ApiClient {
private sessionId: string | null = null
@@ -85,89 +78,40 @@ class ApiClient {
clearChatSessionId()
}
// Streaming chat endpoint (SSE)
async *chatStream(
// Simple JSON chat endpoint
async chat(
question: string,
mode: ChatMode,
sessionId?: string
): AsyncGenerator<StreamEvent> {
): Promise<ChatResponse> {
// Use mock data if enabled
if (USE_MOCK_DATA) {
yield* mockChatStream(question, mode)
return
return mockChat(question, mode)
}
// Generate or reuse session ID
const actualSessionId = sessionId || this.getSessionId() || crypto.randomUUID()
this.setSessionId(actualSessionId)
// Yield session ID first
yield { type: 'session_id', data: actualSessionId }
const response = await fetch(`${API_BASE_URL}/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: question,
conversation_id: actualSessionId,
}),
})
try {
const response = await fetch(`${API_BASE_URL}${API_ENDPOINT}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: question,
conversation_id: actualSessionId,
}),
})
if (!response.ok) {
yield { type: 'error', data: `HTTP error! status: ${response.status}` }
return
}
if (!response.body) {
yield { type: 'error', data: 'Response body is null' }
return
}
const reader = response.body.getReader()
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) {
if (line.startsWith('data: ')) {
const data = line.slice(6)
try {
const parsed = JSON.parse(data)
if (parsed.type === 'chunk' && parsed.content) {
yield { type: 'chunk', data: parsed.content }
} else if (parsed.type === 'done') {
yield { type: 'done', data: null }
return
} else if (parsed.type === 'error') {
yield { type: 'error', data: parsed.message || 'Unknown error' }
return
}
} catch {
// Skip non-JSON lines
}
}
}
}
yield { type: 'done', data: null }
} catch (error) {
yield {
type: 'error',
data: error instanceof Error ? error.message : 'Network error',
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`)
}
const data: ChatResponse = await response.json()
this.setSessionId(data.conversation_id)
return data
}
}