open-notebook / frontend /src /lib /hooks /useNotebookChat.ts
baveshraam's picture
FIX: SurrealDB 2.0 migration syntax and Frontend/CORS link
f871fed
'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
}
}