- Add pendingModelOverride state to useNotebookChat hook - Store model selection when no session exists yet - Apply pending model override when session is auto-created on first message - Simplify ChatColumn by using new setModelOverride function
314 lines
9.3 KiB
TypeScript
314 lines
9.3 KiB
TypeScript
'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
|
|
}
|
|
}
|