ChatContext.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. // Chat context for LLM assistant state management
  2. import { createContext, useContext, useState, useCallback, useEffect, ReactNode, useRef } from 'react'
  3. import * as chatApi from '@/api/chat'
  4. import type { ChatSession, ChatMessage, PageContext, LlmModel } from '@/types/chat'
  5. import { useAuth } from './AuthContext'
  6. interface ChatContextValue {
  7. // UI State
  8. isOpen: boolean
  9. isMinimized: boolean
  10. toggleChat: () => void
  11. minimizeChat: () => void
  12. expandChat: () => void
  13. closeChat: () => void
  14. // Session Management
  15. sessions: ChatSession[]
  16. currentSession: ChatSession | null
  17. isLoadingSessions: boolean
  18. createSession: (workspaceId?: string) => Promise<ChatSession>
  19. switchSession: (sessionId: string) => Promise<void>
  20. deleteSession: (sessionId: string) => Promise<void>
  21. refreshSessions: () => Promise<void>
  22. // Messages
  23. messages: ChatMessage[]
  24. isStreaming: boolean
  25. streamingContent: string
  26. sendMessage: (content: string) => Promise<void>
  27. clearMessages: () => Promise<void>
  28. // Models
  29. models: LlmModel[]
  30. selectedModelId: string | null
  31. setSelectedModelId: (modelId: string) => void
  32. loadModels: () => Promise<void>
  33. // Page Context
  34. currentPageContext: PageContext | null
  35. setCurrentPageContext: (context: PageContext | null) => void
  36. }
  37. const ChatContext = createContext<ChatContextValue | null>(null)
  38. interface ChatProviderProps {
  39. children: ReactNode
  40. }
  41. export function ChatProvider({ children }: ChatProviderProps) {
  42. const { isAuthenticated } = useAuth()
  43. // UI State
  44. const [isOpen, setIsOpen] = useState(false)
  45. const [isMinimized, setIsMinimized] = useState(true)
  46. // Session State
  47. const [sessions, setSessions] = useState<ChatSession[]>([])
  48. const [currentSession, setCurrentSession] = useState<ChatSession | null>(null)
  49. const [isLoadingSessions, setIsLoadingSessions] = useState(false)
  50. // Message State
  51. const [messages, setMessages] = useState<ChatMessage[]>([])
  52. const [isStreaming, setIsStreaming] = useState(false)
  53. const [streamingContent, setStreamingContent] = useState('')
  54. // Model State
  55. const [models, setModels] = useState<LlmModel[]>([])
  56. const [selectedModelId, setSelectedModelId] = useState<string | null>(null)
  57. // Page Context
  58. const [currentPageContext, setCurrentPageContext] = useState<PageContext | null>(null)
  59. // Abort controller for streaming
  60. const abortControllerRef = useRef<AbortController | null>(null)
  61. // UI Actions
  62. const toggleChat = useCallback(() => {
  63. if (isMinimized) {
  64. setIsMinimized(false)
  65. setIsOpen(true)
  66. } else {
  67. setIsMinimized(true)
  68. }
  69. }, [isMinimized])
  70. const minimizeChat = useCallback(() => {
  71. setIsMinimized(true)
  72. }, [])
  73. const expandChat = useCallback(() => {
  74. setIsMinimized(false)
  75. setIsOpen(true)
  76. }, [])
  77. const closeChat = useCallback(() => {
  78. setIsOpen(false)
  79. setIsMinimized(true)
  80. }, [])
  81. // Session Actions
  82. const refreshSessions = useCallback(async () => {
  83. if (!isAuthenticated) return
  84. setIsLoadingSessions(true)
  85. try {
  86. const response = await chatApi.listSessions(undefined, 50)
  87. setSessions(response.sessions || [])
  88. } catch (error) {
  89. console.error('Failed to load sessions:', error)
  90. } finally {
  91. setIsLoadingSessions(false)
  92. }
  93. }, [isAuthenticated])
  94. const createSession = useCallback(async (workspaceId?: string): Promise<ChatSession> => {
  95. const session = await chatApi.createSession({
  96. workspace_id: workspaceId,
  97. model_id: selectedModelId || undefined,
  98. })
  99. setSessions(prev => [session, ...prev])
  100. setCurrentSession(session)
  101. setMessages([])
  102. return session
  103. }, [selectedModelId])
  104. const switchSession = useCallback(async (sessionId: string) => {
  105. try {
  106. const session = await chatApi.getSession(sessionId)
  107. setCurrentSession(session)
  108. setMessages(session.messages || [])
  109. } catch (error) {
  110. console.error('Failed to switch session:', error)
  111. throw error
  112. }
  113. }, [])
  114. const deleteSession = useCallback(async (sessionId: string) => {
  115. await chatApi.deleteSession(sessionId)
  116. setSessions(prev => prev.filter(s => s.id !== sessionId))
  117. if (currentSession?.id === sessionId) {
  118. setCurrentSession(null)
  119. setMessages([])
  120. }
  121. }, [currentSession?.id])
  122. // Message Actions
  123. const sendMessage = useCallback(async (content: string) => {
  124. if (!currentSession) {
  125. // Create a new session first
  126. const session = await createSession()
  127. setCurrentSession(session)
  128. }
  129. const sessionId = currentSession?.id
  130. if (!sessionId) return
  131. // Add user message optimistically
  132. const userMessage: ChatMessage = {
  133. id: `temp-${Date.now()}`,
  134. role: 'user',
  135. content,
  136. created_at: Date.now() / 1000,
  137. page_context: currentPageContext || undefined,
  138. }
  139. setMessages(prev => [...prev, userMessage])
  140. // Start streaming
  141. setIsStreaming(true)
  142. setStreamingContent('')
  143. try {
  144. await chatApi.streamMessage(
  145. sessionId,
  146. {
  147. content,
  148. page_context: currentPageContext || undefined,
  149. },
  150. (chunk) => {
  151. if (chunk.delta) {
  152. setStreamingContent(prev => prev + chunk.delta)
  153. }
  154. if (chunk.finish_reason && chunk.message_id) {
  155. // Stream complete, add the full message
  156. const assistantMessage: ChatMessage = {
  157. id: chunk.message_id,
  158. role: 'assistant',
  159. content: '', // Will be set below
  160. created_at: Date.now() / 1000,
  161. }
  162. setMessages(prev => {
  163. // Get current streaming content
  164. const fullContent = prev.length > 0 ? streamingContent : ''
  165. return [...prev, { ...assistantMessage, content: fullContent }]
  166. })
  167. }
  168. },
  169. (error) => {
  170. console.error('Stream error:', error)
  171. }
  172. )
  173. // After streaming completes, refresh the session to get proper message IDs
  174. const updatedSession = await chatApi.getSession(sessionId)
  175. setMessages(updatedSession.messages || [])
  176. setCurrentSession(updatedSession)
  177. // Update session in list
  178. setSessions(prev => prev.map(s =>
  179. s.id === sessionId ? { ...s, title: updatedSession.title, updated_at: updatedSession.updated_at } : s
  180. ))
  181. } catch (error) {
  182. console.error('Failed to send message:', error)
  183. // Remove optimistic user message on error
  184. setMessages(prev => prev.filter(m => m.id !== userMessage.id))
  185. } finally {
  186. setIsStreaming(false)
  187. setStreamingContent('')
  188. }
  189. }, [currentSession, currentPageContext, createSession, streamingContent])
  190. const clearMessages = useCallback(async () => {
  191. if (!currentSession) return
  192. await chatApi.clearSessionMessages(currentSession.id)
  193. setMessages([])
  194. }, [currentSession])
  195. // Model Actions
  196. const loadModels = useCallback(async () => {
  197. try {
  198. const response = await chatApi.listModels()
  199. setModels(response.models || [])
  200. // Select first model if none selected
  201. if (!selectedModelId && response.models?.length > 0) {
  202. setSelectedModelId(response.models[0].id)
  203. }
  204. } catch (error) {
  205. console.error('Failed to load models:', error)
  206. }
  207. }, [selectedModelId])
  208. // Load sessions and models on mount when authenticated
  209. useEffect(() => {
  210. if (isAuthenticated) {
  211. refreshSessions()
  212. loadModels()
  213. }
  214. }, [isAuthenticated, refreshSessions, loadModels])
  215. // Cleanup on unmount
  216. useEffect(() => {
  217. return () => {
  218. if (abortControllerRef.current) {
  219. abortControllerRef.current.abort()
  220. }
  221. }
  222. }, [])
  223. const value: ChatContextValue = {
  224. // UI State
  225. isOpen,
  226. isMinimized,
  227. toggleChat,
  228. minimizeChat,
  229. expandChat,
  230. closeChat,
  231. // Session Management
  232. sessions,
  233. currentSession,
  234. isLoadingSessions,
  235. createSession,
  236. switchSession,
  237. deleteSession,
  238. refreshSessions,
  239. // Messages
  240. messages,
  241. isStreaming,
  242. streamingContent,
  243. sendMessage,
  244. clearMessages,
  245. // Models
  246. models,
  247. selectedModelId,
  248. setSelectedModelId,
  249. loadModels,
  250. // Page Context
  251. currentPageContext,
  252. setCurrentPageContext,
  253. }
  254. return (
  255. <ChatContext.Provider value={value}>
  256. {children}
  257. </ChatContext.Provider>
  258. )
  259. }
  260. export function useChat(): ChatContextValue {
  261. const context = useContext(ChatContext)
  262. if (!context) {
  263. throw new Error('useChat must be used within a ChatProvider')
  264. }
  265. return context
  266. }