open-notebook/frontend/src/lib/hooks/use-ask.ts
Luis Novo 20e18fdd0d feat: improve error clarity for LLM provider failures (#506)
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.
2026-02-16 16:15:46 -03:00

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
}
}