'use client' import { useState, useCallback, useRef } from 'react' import type { ChatMessage, SourceReference, ArtifactSpec } from '@/types/chat' interface UseChatStreamOptions { onError?: (error: string) => void } interface UseChatStreamReturn { messages: ChatMessage[] isLoading: boolean isStreaming: boolean sessionId: string | null error: string | null toolsExecuting: string[] sendMessage: (message: string) => Promise loadSession: (sessionId: string) => Promise clearChat: () => void setSessionId: (id: string | null) => void } export function useChatStream(options: UseChatStreamOptions = {}): UseChatStreamReturn { const [messages, setMessages] = useState([]) const [isLoading, setIsLoading] = useState(false) const [isStreaming, setIsStreaming] = useState(false) const [sessionId, setSessionId] = useState(null) const [error, setError] = useState(null) const [toolsExecuting, setToolsExecuting] = useState([]) const abortControllerRef = useRef(null) const sendMessage = useCallback(async (message: string) => { if (!message.trim() || isLoading || isStreaming) return setError(null) setIsLoading(true) // Add user message immediately const userMessage: ChatMessage = { id: `temp-${Date.now()}`, session_id: sessionId || '', user_id: '', role: 'user', content: message.trim(), sources: [], created_at: new Date().toISOString(), } setMessages((prev) => [...prev, userMessage]) // Add placeholder for assistant response const assistantPlaceholder: ChatMessage = { id: `temp-assistant-${Date.now()}`, session_id: sessionId || '', user_id: '', role: 'assistant', content: '', sources: [], created_at: new Date().toISOString(), } setMessages((prev) => [...prev, assistantPlaceholder]) try { abortControllerRef.current = new AbortController() const response = await fetch('/api/extensions/ext/ai-chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message: message.trim(), session_id: sessionId, }), signal: abortControllerRef.current.signal, }) if (!response.ok) { const errorData = await response.json() throw new Error(errorData.error || 'Failed to send message') } setIsLoading(false) setIsStreaming(true) const reader = response.body?.getReader() if (!reader) throw new Error('No response body') const decoder = new TextDecoder() let accumulatedContent = '' let sources: SourceReference[] = [] let artifact: ArtifactSpec | null = null let newSessionId = sessionId let messageId: string | null = null while (true) { const { done, value } = await reader.read() if (done) break const chunk = decoder.decode(value) const lines = chunk.split('\n') for (const line of lines) { if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)) if (data.type === 'session') { newSessionId = data.session_id setSessionId(data.session_id) } else if (data.type === 'content') { accumulatedContent += data.content setToolsExecuting([]) // Clear tool indicators when content starts setMessages((prev) => { const newMessages = [...prev] const lastMsg = newMessages[newMessages.length - 1] if (lastMsg.role === 'assistant') { lastMsg.content = accumulatedContent } return newMessages }) } else if (data.type === 'sources') { sources = data.sources setMessages((prev) => { const newMessages = [...prev] const lastMsg = newMessages[newMessages.length - 1] if (lastMsg.role === 'assistant') { lastMsg.sources = sources } return newMessages }) } else if (data.type === 'tool_start') { setToolsExecuting((prev) => [...prev, data.toolName]) } else if (data.type === 'artifact') { artifact = data.artifact setMessages((prev) => { const newMessages = [...prev] const lastMsg = newMessages[newMessages.length - 1] if (lastMsg.role === 'assistant') { lastMsg.artifact = artifact } return newMessages }) } else if (data.type === 'done') { messageId = data.message_id setToolsExecuting([]) // Update the message IDs with real values setMessages((prev) => { const newMessages = [...prev] const lastMsg = newMessages[newMessages.length - 1] if (lastMsg.role === 'assistant' && messageId) { lastMsg.id = messageId lastMsg.session_id = newSessionId || '' } return newMessages }) } else if (data.type === 'error') { throw new Error(data.error) } } catch (parseError) { // Skip invalid JSON lines } } } } } catch (err) { if (err instanceof Error && err.name === 'AbortError') { // User cancelled, remove the placeholder setMessages((prev) => prev.slice(0, -1)) } else { const errorMessage = err instanceof Error ? err.message : 'An error occurred' setError(errorMessage) options.onError?.(errorMessage) // Update placeholder with error setMessages((prev) => { const newMessages = [...prev] const lastMsg = newMessages[newMessages.length - 1] if (lastMsg.role === 'assistant') { lastMsg.content = 'Ett fel uppstod. Försök igen.' } return newMessages }) } } finally { setIsLoading(false) setIsStreaming(false) abortControllerRef.current = null } }, [sessionId, isLoading, isStreaming, options]) const loadSession = useCallback(async (id: string) => { setIsLoading(true) setError(null) try { const response = await fetch(`/api/extensions/ext/ai-chat/sessions/${id}`) if (!response.ok) { throw new Error('Failed to load session') } const data = await response.json() setSessionId(id) setMessages(data.messages || []) } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to load session' setError(errorMessage) options.onError?.(errorMessage) } finally { setIsLoading(false) } }, [options]) const clearChat = useCallback(() => { if (abortControllerRef.current) { abortControllerRef.current.abort() } setMessages([]) setSessionId(null) setError(null) }, []) return { messages, isLoading, isStreaming, sessionId, error, toolsExecuting, sendMessage, loadSession, clearChat, setSessionId, } }