'use client'; import { useCallback, useRef, useState } from 'react'; import { streamMedicineConsult } from '../api'; export interface MedicineWidget { id: string; type: string; params: Record; } export interface MedicineChatMessage { id: string; role: 'user' | 'assistant'; content: string; isStreaming?: boolean; widgets: MedicineWidget[]; createdAt: Date; } function generateId(): string { return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; } export function useMedicineChat() { const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const abortControllerRef = useRef(null); const historyRef = useRef<[string, string][]>([]); const sendMessage = useCallback( async (content: string, city?: string) => { if (!content.trim() || isLoading) return; const userMessage: MedicineChatMessage = { id: generateId(), role: 'user', content: content.trim(), widgets: [], createdAt: new Date(), }; const assistantMessage: MedicineChatMessage = { id: generateId(), role: 'assistant', content: '', isStreaming: true, widgets: [], createdAt: new Date(), }; setMessages((prev) => [...prev, userMessage, assistantMessage]); setIsLoading(true); abortControllerRef.current?.abort(); const controller = new AbortController(); abortControllerRef.current = controller; try { let fullContent = ''; const widgets: MedicineWidget[] = []; const stream = streamMedicineConsult( content.trim(), city?.trim() || undefined, historyRef.current, controller.signal ); for await (const event of stream) { const chunkText = event.chunk || (event.data as Record)?.chunk; if (event.type === 'textChunk' && typeof chunkText === 'string') { fullContent += chunkText; setMessages((prev) => prev.map((m) => (m.id === assistantMessage.id ? { ...m, content: fullContent } : m)) ); } if (event.type === 'block' && event.block?.type === 'widget') { const data = event.block.data as { widgetType?: string; params?: Record }; if (!data.widgetType) continue; widgets.push({ id: event.block.id, type: data.widgetType, params: data.params || {}, }); setMessages((prev) => prev.map((m) => (m.id === assistantMessage.id ? { ...m, widgets: [...widgets] } : m)) ); } if (event.type === 'messageEnd') { setMessages((prev) => prev.map((m) => m.id === assistantMessage.id ? { ...m, isStreaming: false, content: fullContent, widgets: [...widgets] } : m ) ); } } if (fullContent.trim()) { historyRef.current.push([userMessage.content, fullContent]); historyRef.current = historyRef.current.slice(-8); } } catch (error) { const message = error instanceof Error ? error.message : 'Ошибка консультации'; setMessages((prev) => prev.map((m) => m.id === assistantMessage.id ? { ...m, content: `Ошибка: ${message}`, isStreaming: false } : m ) ); } finally { setIsLoading(false); abortControllerRef.current = null; } }, [isLoading] ); const stopGeneration = useCallback(() => { abortControllerRef.current?.abort(); setIsLoading(false); setMessages((prev) => prev.map((m) => (m.isStreaming ? { ...m, isStreaming: false } : m))); }, []); const clearChat = useCallback(() => { setMessages([]); historyRef.current = []; }, []); return { messages, isLoading, sendMessage, stopGeneration, clearChat, }; }