Spaces:
Running
Running
| 'use client' | |
| import { useState, useCallback, useEffect } from 'react' | |
| import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' | |
| import { toast } from 'sonner' | |
| import { chatApi } from '@/lib/api/chat' | |
| import { QUERY_KEYS } from '@/lib/api/query-client' | |
| import { | |
| NotebookChatMessage, | |
| CreateNotebookChatSessionRequest, | |
| UpdateNotebookChatSessionRequest, | |
| SourceListResponse, | |
| NoteResponse | |
| } from '@/lib/types/api' | |
| import { ContextSelections } from '@/app/(dashboard)/notebooks/[id]/page' | |
| interface UseNotebookChatParams { | |
| notebookId: string | |
| sources: SourceListResponse[] | |
| notes: NoteResponse[] | |
| contextSelections: ContextSelections | |
| } | |
| export function useNotebookChat({ notebookId, sources, notes, contextSelections }: UseNotebookChatParams) { | |
| const queryClient = useQueryClient() | |
| const [currentSessionId, setCurrentSessionId] = useState<string | null>(null) | |
| const [messages, setMessages] = useState<NotebookChatMessage[]>([]) | |
| const [isSending, setIsSending] = useState(false) | |
| const [tokenCount, setTokenCount] = useState<number>(0) | |
| const [charCount, setCharCount] = useState<number>(0) | |
| // Pending model override for when user changes model before a session exists | |
| const [pendingModelOverride, setPendingModelOverride] = useState<string | null>(null) | |
| // Fetch sessions for this notebook | |
| const { | |
| data: sessions = [], | |
| isLoading: loadingSessions, | |
| refetch: refetchSessions | |
| } = useQuery({ | |
| queryKey: QUERY_KEYS.notebookChatSessions(notebookId), | |
| queryFn: () => chatApi.listSessions(notebookId), | |
| enabled: !!notebookId | |
| }) | |
| // Fetch current session with messages | |
| const { | |
| data: currentSession, | |
| refetch: refetchCurrentSession | |
| } = useQuery({ | |
| queryKey: QUERY_KEYS.notebookChatSession(currentSessionId!), | |
| queryFn: () => chatApi.getSession(currentSessionId!), | |
| enabled: !!notebookId && !!currentSessionId | |
| }) | |
| // Update messages when current session changes | |
| useEffect(() => { | |
| if (currentSession?.messages) { | |
| setMessages(currentSession.messages) | |
| } | |
| }, [currentSession]) | |
| // Auto-select most recent session when sessions are loaded | |
| useEffect(() => { | |
| if (sessions.length > 0 && !currentSessionId) { | |
| // Sessions are sorted by created date desc from API | |
| const mostRecentSession = sessions[0] | |
| setCurrentSessionId(mostRecentSession.id) | |
| } | |
| }, [sessions, currentSessionId]) | |
| // Create session mutation | |
| const createSessionMutation = useMutation({ | |
| mutationFn: (data: CreateNotebookChatSessionRequest) => | |
| chatApi.createSession(data), | |
| onSuccess: (newSession) => { | |
| queryClient.invalidateQueries({ | |
| queryKey: QUERY_KEYS.notebookChatSessions(notebookId) | |
| }) | |
| setCurrentSessionId(newSession.id) | |
| toast.success('Chat session created') | |
| }, | |
| onError: () => { | |
| toast.error('Failed to create chat session') | |
| } | |
| }) | |
| // Update session mutation | |
| const updateSessionMutation = useMutation({ | |
| mutationFn: ({ sessionId, data }: { | |
| sessionId: string | |
| data: UpdateNotebookChatSessionRequest | |
| }) => chatApi.updateSession(sessionId, data), | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ | |
| queryKey: QUERY_KEYS.notebookChatSessions(notebookId) | |
| }) | |
| queryClient.invalidateQueries({ | |
| queryKey: QUERY_KEYS.notebookChatSession(currentSessionId!) | |
| }) | |
| toast.success('Session updated') | |
| }, | |
| onError: () => { | |
| toast.error('Failed to update session') | |
| } | |
| }) | |
| // Delete session mutation | |
| const deleteSessionMutation = useMutation({ | |
| mutationFn: (sessionId: string) => | |
| chatApi.deleteSession(sessionId), | |
| onSuccess: (_, deletedId) => { | |
| queryClient.invalidateQueries({ | |
| queryKey: QUERY_KEYS.notebookChatSessions(notebookId) | |
| }) | |
| if (currentSessionId === deletedId) { | |
| setCurrentSessionId(null) | |
| setMessages([]) | |
| } | |
| toast.success('Session deleted') | |
| }, | |
| onError: () => { | |
| toast.error('Failed to delete session') | |
| } | |
| }) | |
| // Build context from sources and notes based on user selections | |
| const buildContext = useCallback(async () => { | |
| // Build context_config mapping IDs to selection modes | |
| const context_config: { sources: Record<string, string>, notes: Record<string, string> } = { | |
| sources: {}, | |
| notes: {} | |
| } | |
| // Map source selections | |
| sources.forEach(source => { | |
| const mode = contextSelections.sources[source.id] | |
| if (mode === 'insights') { | |
| context_config.sources[source.id] = 'insights' | |
| } else if (mode === 'full') { | |
| context_config.sources[source.id] = 'full content' | |
| } else { | |
| context_config.sources[source.id] = 'not in' | |
| } | |
| }) | |
| // Map note selections | |
| notes.forEach(note => { | |
| const mode = contextSelections.notes[note.id] | |
| if (mode === 'full') { | |
| context_config.notes[note.id] = 'full content' | |
| } else { | |
| context_config.notes[note.id] = 'not in' | |
| } | |
| }) | |
| // Call API to build context with actual content | |
| const response = await chatApi.buildContext({ | |
| notebook_id: notebookId, | |
| context_config | |
| }) | |
| // Store token and char counts | |
| setTokenCount(response.token_count) | |
| setCharCount(response.char_count) | |
| return response.context | |
| }, [notebookId, sources, notes, contextSelections]) | |
| // Send message (synchronous, no streaming) | |
| const sendMessage = useCallback(async (message: string, modelOverride?: string) => { | |
| let sessionId = currentSessionId | |
| // Auto-create session if none exists | |
| if (!sessionId) { | |
| try { | |
| const defaultTitle = message.length > 30 | |
| ? `${message.substring(0, 30)}...` | |
| : message | |
| const newSession = await chatApi.createSession({ | |
| notebook_id: notebookId, | |
| title: defaultTitle, | |
| // Include pending model override when creating session | |
| model_override: pendingModelOverride ?? undefined | |
| }) | |
| sessionId = newSession.id | |
| setCurrentSessionId(sessionId) | |
| // Clear pending model override now that it's applied to the session | |
| setPendingModelOverride(null) | |
| queryClient.invalidateQueries({ | |
| queryKey: QUERY_KEYS.notebookChatSessions(notebookId) | |
| }) | |
| } catch { | |
| toast.error('Failed to create chat session') | |
| return | |
| } | |
| } | |
| // Add user message optimistically | |
| const userMessage: NotebookChatMessage = { | |
| id: `temp-${Date.now()}`, | |
| type: 'human', | |
| content: message, | |
| timestamp: new Date().toISOString() | |
| } | |
| setMessages(prev => [...prev, userMessage]) | |
| setIsSending(true) | |
| try { | |
| // Build context and send message | |
| const context = await buildContext() | |
| const response = await chatApi.sendMessage({ | |
| session_id: sessionId, | |
| message, | |
| context, | |
| model_override: modelOverride ?? (currentSession?.model_override ?? undefined) | |
| }) | |
| // Update messages with API response | |
| setMessages(response.messages) | |
| // Refetch current session to get updated data | |
| await refetchCurrentSession() | |
| } catch (error) { | |
| console.error('Error sending message:', error) | |
| toast.error('Failed to send message') | |
| // Remove optimistic message on error | |
| setMessages(prev => prev.filter(msg => !msg.id.startsWith('temp-'))) | |
| } finally { | |
| setIsSending(false) | |
| } | |
| }, [ | |
| notebookId, | |
| currentSessionId, | |
| currentSession, | |
| pendingModelOverride, | |
| buildContext, | |
| refetchCurrentSession, | |
| queryClient | |
| ]) | |
| // Switch session | |
| const switchSession = useCallback((sessionId: string) => { | |
| setCurrentSessionId(sessionId) | |
| }, []) | |
| // Create session | |
| const createSession = useCallback((title?: string) => { | |
| return createSessionMutation.mutate({ | |
| notebook_id: notebookId, | |
| title | |
| }) | |
| }, [createSessionMutation, notebookId]) | |
| // Update session | |
| const updateSession = useCallback((sessionId: string, data: UpdateNotebookChatSessionRequest) => { | |
| return updateSessionMutation.mutate({ | |
| sessionId, | |
| data | |
| }) | |
| }, [updateSessionMutation]) | |
| // Delete session | |
| const deleteSession = useCallback((sessionId: string) => { | |
| return deleteSessionMutation.mutate(sessionId) | |
| }, [deleteSessionMutation]) | |
| // Set model override - handles both existing sessions and pending state | |
| const setModelOverride = useCallback((model: string | null) => { | |
| if (currentSessionId) { | |
| // Session exists - update it directly | |
| updateSessionMutation.mutate({ | |
| sessionId: currentSessionId, | |
| data: { model_override: model } | |
| }) | |
| } else { | |
| // No session yet - store as pending | |
| setPendingModelOverride(model) | |
| } | |
| }, [currentSessionId, updateSessionMutation]) | |
| // Update token/char counts when context selections change | |
| useEffect(() => { | |
| const updateContextCounts = async () => { | |
| try { | |
| await buildContext() | |
| } catch (error) { | |
| console.error('Error updating context counts:', error) | |
| } | |
| } | |
| updateContextCounts() | |
| }, [buildContext]) | |
| return { | |
| // State | |
| sessions, | |
| currentSession: currentSession || sessions.find(s => s.id === currentSessionId), | |
| currentSessionId, | |
| messages, | |
| isSending, | |
| loadingSessions, | |
| tokenCount, | |
| charCount, | |
| pendingModelOverride, | |
| // Actions | |
| createSession, | |
| updateSession, | |
| deleteSession, | |
| switchSession, | |
| sendMessage, | |
| setModelOverride, | |
| refetchSessions | |
| } | |
| } | |