legco_ai_assistant/frontend/src/lib/queries.tsx

344 lines
11 KiB
TypeScript

import React from 'react'
import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { queryDocument, queryDocumentStream, ingestDocument, listDocuments, listChunks, deleteDocument, deleteChunk, listPromptProfiles, getPromptProfile, activatePromptProfile, updatePrompt, updateAllPrompts, resetPrompts, exportProfile, importProfile, listQueryHistory, getQueryHistoryDetail, deleteQueryHistory, clearQueryHistory, getHistoryStats, uploadVideo } from './api'
import type { QueryRequest, QueryResponse, QueryStreamEvent, SourceMetadata, SubQuestionSources, IngestResponse, DocumentListResponse, ChunkInfo, DeleteResponse, PromptProfileListResponse, PromptSetResponse, PromptUpdateRequest, PromptBatchUpdateRequest, PromptActivateResponse, PromptStatusResponse, ProfileExportData, ProfileImportResponse, QueryHistoryList, QueryHistoryDetail, HistoryStats, HistoryDeleteResponse, VideoUploadResponse } from '../types'
import { useState, useCallback, useRef } from 'react'
export const queryClient = new QueryClient()
export const useQueryDocument = () => {
return useMutation<QueryResponse, Error, QueryRequest>({
mutationFn: queryDocument,
})
}
export interface QueryStreamState {
extractedQuestions: string[] | null
answer: string | null
sources: SourceMetadata[] | null
subQuestionSources: SubQuestionSources[] | null
phase: 'idle' | 'decomposing' | 'retrieving' | 'filtering' | 'generating' | 'completed' | 'error'
historyId: number | null
error: Error | null
}
export const useQueryDocumentStream = () => {
const [state, setState] = useState<QueryStreamState>({
extractedQuestions: null,
answer: null,
sources: null,
subQuestionSources: null,
phase: 'idle',
historyId: null,
error: null,
})
const abortRef = useRef<AbortController | null>(null)
const mutate = useCallback(async (request: QueryRequest) => {
setState({
extractedQuestions: null,
answer: null,
sources: null,
subQuestionSources: null,
phase: 'decomposing',
historyId: null,
error: null,
})
abortRef.current = new AbortController()
try {
await queryDocumentStream(request, (event: QueryStreamEvent) => {
switch (event.phase) {
case 'decomposed':
setState(prev => ({
...prev,
extractedQuestions: event.extracted_questions ?? null,
phase: 'retrieving',
}))
break
case 'retrieving':
setState(prev => ({ ...prev, phase: 'retrieving' }))
break
case 'filtering':
setState(prev => ({ ...prev, phase: 'filtering' }))
break
case 'generating':
setState(prev => ({ ...prev, phase: 'generating' }))
break
case 'generating_subquestion':
setState(prev => ({ ...prev, phase: 'generating' }))
break
case 'completed':
setState(prev => ({
...prev,
answer: event.answer ?? null,
sources: event.sources ?? null,
subQuestionSources: event.sub_question_sources ?? null,
phase: 'completed',
historyId: (event as any).history_id ?? null,
}))
break
case 'error':
setState(prev => ({
...prev,
phase: 'error',
error: new Error(event.message ?? 'Unknown error'),
}))
break
}
}, abortRef.current.signal)
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
setState(prev => ({ ...prev, phase: 'idle' }))
return
}
setState(prev => ({
...prev,
phase: 'error',
error: err instanceof Error ? err : new Error(String(err)),
}))
}
}, [])
const decomposeOnly = useCallback(async (request: QueryRequest) => {
setState({
extractedQuestions: null,
answer: null,
sources: null,
subQuestionSources: null,
phase: 'decomposing',
historyId: null,
error: null,
})
abortRef.current = new AbortController()
try {
await queryDocumentStream({ question: request.question, stop_after_decompose: true }, (event: QueryStreamEvent) => {
switch (event.phase) {
case 'decomposed':
setState(prev => ({
...prev,
extractedQuestions: event.extracted_questions ?? null,
phase: 'retrieving',
}))
break
case 'retrieving':
setState(prev => ({ ...prev, phase: 'retrieving' }))
break
case 'filtering':
setState(prev => ({ ...prev, phase: 'filtering' }))
break
case 'generating':
setState(prev => ({ ...prev, phase: 'generating' }))
break
case 'generating_subquestion':
setState(prev => ({ ...prev, phase: 'generating' }))
break
case 'completed':
setState(prev => ({
...prev,
answer: event.half_question ? null : (event.answer ?? null),
sources: event.sources ?? null,
subQuestionSources: event.sub_question_sources ?? null,
phase: 'completed',
historyId: (event as any).history_id ?? null,
}))
break
case 'error':
setState(prev => ({
...prev,
phase: 'error',
error: new Error(event.message ?? 'Unknown error'),
}))
break
}
}, abortRef.current.signal)
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
setState(prev => ({ ...prev, phase: 'idle' }))
return
}
setState(prev => ({
...prev,
phase: 'error',
error: err instanceof Error ? err : new Error(String(err)),
}))
}
}, [])
const reset = useCallback(() => {
abortRef.current?.abort()
setState({
extractedQuestions: null,
answer: null,
sources: null,
subQuestionSources: null,
phase: 'idle',
historyId: null,
error: null,
})
}, [])
return { ...state, mutate, decomposeOnly, reset }
}
export const useIngestDocument = () => {
return useMutation<IngestResponse, Error, File>({
mutationFn: ingestDocument,
})
}
export const useDocuments = () => {
return useQuery<DocumentListResponse, Error>({
queryKey: ['documents'],
queryFn: listDocuments,
})
}
export const useDocumentChunks = (documentId: string | null) => {
return useQuery<ChunkInfo[], Error>({
queryKey: ['documents', documentId, 'chunks'],
queryFn: () => listChunks(documentId!),
enabled: documentId !== null,
})
}
export const useDeleteDocument = () => {
const queryClient = useQueryClient()
return useMutation<DeleteResponse, Error, string>({
mutationFn: deleteDocument,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['documents'] })
},
})
}
export const useDeleteChunk = () => {
const queryClient = useQueryClient()
return useMutation<DeleteResponse, Error, string>({
mutationFn: deleteChunk,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['documents'] })
},
})
}
export const usePromptProfiles = () => {
return useQuery<PromptProfileListResponse, Error>({
queryKey: ['prompts', 'profiles'],
queryFn: listPromptProfiles,
})
}
export const usePromptProfile = (name: string | null) => {
return useQuery<PromptSetResponse, Error>({
queryKey: ['prompts', 'profiles', name],
queryFn: () => getPromptProfile(name!),
enabled: name !== null,
})
}
export const useActivateProfile = () => {
const queryClient = useQueryClient()
return useMutation<PromptActivateResponse, Error, string>({
mutationFn: activatePromptProfile,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['prompts'] })
},
})
}
export const useUpdatePrompt = () => {
const queryClient = useQueryClient()
return useMutation<PromptStatusResponse, Error, { name: string; step: string; request: PromptUpdateRequest }>({
mutationFn: ({ name, step, request }) => updatePrompt(name, step, request),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['prompts'] })
},
})
}
export const useUpdateAllPrompts = () => {
const queryClient = useQueryClient()
return useMutation<PromptStatusResponse, Error, { name: string; request: PromptBatchUpdateRequest }>({
mutationFn: ({ name, request }) => updateAllPrompts(name, request),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['prompts'] })
},
})
}
export const useResetPrompts = () => {
const queryClient = useQueryClient()
return useMutation<PromptStatusResponse, Error, { name: string; step?: string }>({
mutationFn: ({ name, step }) => resetPrompts(name, step),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['prompts'] })
},
})
}
export const useImportProfile = () => {
const queryClient = useQueryClient()
return useMutation<ProfileImportResponse, Error, { name: string; data: ProfileExportData }>({
mutationFn: ({ name, data }) => importProfile(name, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['prompts'] })
},
})
}
export const useQueryHistoryList = (limit = 50, offset = 0) => {
return useQuery<QueryHistoryList, Error>({
queryKey: ['history', { limit, offset }],
queryFn: () => listQueryHistory(limit, offset),
})
}
export const useQueryHistoryDetail = (id: number | null) => {
return useQuery<QueryHistoryDetail, Error>({
queryKey: ['history', id],
queryFn: () => getQueryHistoryDetail(id!),
enabled: id !== null,
})
}
export const useDeleteQueryHistory = () => {
const queryClient = useQueryClient()
return useMutation<HistoryDeleteResponse, Error, number>({
mutationFn: deleteQueryHistory,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['history'] })
},
})
}
export const useClearQueryHistory = () => {
const queryClient = useQueryClient()
return useMutation<HistoryDeleteResponse, Error, void>({
mutationFn: clearQueryHistory,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['history'] })
},
})
}
export const useHistoryStats = () => {
return useQuery<HistoryStats, Error>({
queryKey: ['history', 'stats'],
queryFn: getHistoryStats,
})
}
export const AppQueryProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
export const useVideoUpload = () => {
return useMutation<VideoUploadResponse, Error, { file: File; onProgress?: (pct: number) => void }>({
mutationFn: ({ file, onProgress }) => uploadVideo(file, onProgress),
})
}