Replace generic "An unexpected error occurred" messages with descriptive, user-friendly error messages when LLM operations fail. Errors like invalid API keys, wrong model names, and rate limits now surface clearly in the UI. Adds error classification utility, global FastAPI exception handlers, and frontend getApiErrorMessage() helper. Bumps version to 1.7.2.
171 lines
4.4 KiB
TypeScript
171 lines
4.4 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useCallback } from 'react'
|
|
import { toast } from 'sonner'
|
|
import { useTranslation } from '@/lib/hooks/use-translation'
|
|
import { getApiErrorMessage } from '@/lib/utils/error-handler'
|
|
import { searchApi } from '@/lib/api/search'
|
|
import { AskStreamEvent } from '@/lib/types/search'
|
|
|
|
interface AskModels {
|
|
strategy: string
|
|
answer: string
|
|
finalAnswer: string
|
|
}
|
|
|
|
interface StrategyData {
|
|
reasoning: string
|
|
searches: Array<{ term: string; instructions: string }>
|
|
}
|
|
|
|
interface AskState {
|
|
isStreaming: boolean
|
|
strategy: StrategyData | null
|
|
answers: string[]
|
|
finalAnswer: string | null
|
|
error: string | null
|
|
}
|
|
|
|
export function useAsk() {
|
|
const { t } = useTranslation()
|
|
const [state, setState] = useState<AskState>({
|
|
isStreaming: false,
|
|
strategy: null,
|
|
answers: [],
|
|
finalAnswer: null,
|
|
error: null
|
|
})
|
|
|
|
const sendAsk = useCallback(async (question: string, models: AskModels) => {
|
|
// Validate inputs
|
|
if (!question.trim()) {
|
|
toast.error(t('apiErrors.pleaseEnterQuestion'))
|
|
return
|
|
}
|
|
|
|
if (!models.strategy || !models.answer || !models.finalAnswer) {
|
|
toast.error(t('apiErrors.pleaseConfigureModels'))
|
|
return
|
|
}
|
|
|
|
// Reset state
|
|
setState({
|
|
isStreaming: true,
|
|
strategy: null,
|
|
answers: [],
|
|
finalAnswer: null,
|
|
error: null
|
|
})
|
|
|
|
try {
|
|
const response = await searchApi.askKnowledgeBase({
|
|
question,
|
|
strategy_model: models.strategy,
|
|
answer_model: models.answer,
|
|
final_answer_model: models.finalAnswer
|
|
})
|
|
|
|
if (!response) {
|
|
throw new Error('No response body received from server')
|
|
}
|
|
|
|
const reader = response.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')
|
|
|
|
// Keep the last incomplete line in buffer
|
|
buffer = lines.pop() || ''
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith('data: ')) {
|
|
try {
|
|
const jsonStr = line.slice(6).trim()
|
|
if (!jsonStr) continue
|
|
|
|
const data: AskStreamEvent = JSON.parse(jsonStr)
|
|
|
|
if (data.type === 'strategy') {
|
|
setState(prev => ({
|
|
...prev,
|
|
strategy: {
|
|
reasoning: data.reasoning || '',
|
|
searches: data.searches || []
|
|
}
|
|
}))
|
|
} else if (data.type === 'answer') {
|
|
setState(prev => ({
|
|
...prev,
|
|
answers: [...prev.answers, data.content || '']
|
|
}))
|
|
} else if (data.type === 'final_answer') {
|
|
setState(prev => ({
|
|
...prev,
|
|
finalAnswer: data.content || '',
|
|
isStreaming: false
|
|
}))
|
|
} else if (data.type === 'complete') {
|
|
setState(prev => ({
|
|
...prev,
|
|
isStreaming: false
|
|
}))
|
|
} else if (data.type === 'error') {
|
|
throw new Error(data.message || 'Stream error occurred')
|
|
}
|
|
} catch (e) {
|
|
if (e instanceof SyntaxError) {
|
|
console.error('Error parsing SSE data:', e, 'Line:', line)
|
|
// Don't throw - continue processing other lines
|
|
} else {
|
|
throw e
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure streaming is stopped
|
|
setState(prev => ({ ...prev, isStreaming: false }))
|
|
|
|
} catch (error) {
|
|
const err = error as { message?: string }
|
|
const errorMessage = err.message || 'An unexpected error occurred'
|
|
console.error('Ask error:', error)
|
|
|
|
setState(prev => ({
|
|
...prev,
|
|
isStreaming: false,
|
|
error: errorMessage
|
|
}))
|
|
|
|
toast.error(t('apiErrors.askFailed'), {
|
|
description: getApiErrorMessage(errorMessage, (key) => t(key))
|
|
})
|
|
}
|
|
}, [t])
|
|
|
|
const reset = useCallback(() => {
|
|
setState({
|
|
isStreaming: false,
|
|
strategy: null,
|
|
answers: [],
|
|
finalAnswer: null,
|
|
error: null
|
|
})
|
|
}, [])
|
|
|
|
return {
|
|
...state,
|
|
sendAsk,
|
|
reset
|
|
}
|
|
}
|