/** * Chat Provider V2 * * Гибридная архитектура: UI локально, история чата через backend */ import React, { createContext, useCallback, useContext, useEffect, useState } from "react" import { getBackendSync } from "@/features/app-state/services/backend-sync" import { ProjectState } from "@/types/generated/tauri-bindings" // Базовые типы для чата interface ChatMessage { id: string content: string role: "user" | "assistant" | "system" timestamp: Date metadata?: Record } interface ChatSession { id: string name: string messages: ChatMessage[] createdAt: Date updatedAt: Date } interface ChatContextType { // Текущая сессия (локальное состояние) currentSession: ChatSession | null isLoading: boolean error: string | null // История сессий (backend) sessions: any[] // UI состояние (локальное) isOpen: boolean inputText: string isStreaming: boolean // Действия для сессий (backend) createSession: (name?: string) => Promise switchToSession: (sessionId: string) => Promise // Действия для сообщений (backend) sendMessage: (content: string) => Promise clearCurrentSession: () => Promise // UI действия (локальные) setIsOpen: (isOpen: boolean) => void setInputText: (text: string) => void setIsStreaming: (isStreaming: boolean) => void // Обратная совместимость со старым интерфейсом chatMessages: ChatMessage[] sendChatMessage: (content: string) => Promise receiveChatMessage: (content: string) => void selectedAgentId: string | null selectAgent: (agentId: string) => void isProcessing: boolean setProcessing: (processing: boolean) => void currentSessionId: string | null isCreatingNewChat: boolean createNewChat: () => Promise switchSession: (sessionId: string) => Promise deleteSession: (sessionId: string) => Promise updateSessions: (sessions?: any[]) => Promise clearMessages: () => Promise setError: (error: string | null) => void removeMessage: (messageId: string) => void } const ChatContext = createContext(undefined) interface ChatProviderProps { children: React.ReactNode } export function ChatProvider({ children }: ChatProviderProps) { const [backendSync] = useState(() => getBackendSync()) const [backendState, setBackendState] = useState(null) // Backend состояние const [sessions, setSessions] = useState([]) const [currentSession, setCurrentSession] = useState(null) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) // UI состояние (локальное) const [isOpen, setIsOpen] = useState(false) const [inputText, setInputText] = useState("") const [isStreaming, setIsStreaming] = useState(false) // Обратная совместимость со старым интерфейсом const [selectedAgentId, setSelectedAgentId] = useState("claude-4-sonnet") const [isProcessing, setIsProcessing] = useState(false) const [isCreatingNewChat, setIsCreatingNewChat] = useState(false) // Подписка на backend состояние useEffect(() => { const unsubscribe = backendSync.onStateChange((state: ProjectState) => { setBackendState(state) setError(null) // Извлекаем сессии чата из состояния проекта // TODO: Пока backend не поддерживает чат, используем локальное хранилище // if (state.project?.metadata?.chatSessions) { // setSessions(state.project.metadata.chatSessions) // } }) return unsubscribe }, [backendSync]) // Флаг для отслеживания был ли вызван updateSessions const [wasUpdated, setWasUpdated] = useState(false) // Инициализация дефолтной сессии только если не было вызвано updateSessions useEffect(() => { const timer = setTimeout(() => { if (!wasUpdated && !currentSession && sessions.length === 0) { const defaultSession: ChatSession = { id: "default-session", name: "Новый чат", messages: [], createdAt: new Date(), updatedAt: new Date(), } setCurrentSession(defaultSession) setSessions([defaultSession]) } }, 100) // Небольшая задержка для тестов return () => clearTimeout(timer) }, []) // Функция для выполнения backend команд const executeCommand = useCallback( async (command: any) => { try { setIsLoading(true) setError(null) const result = await backendSync.executeCommand(command) if (!result.success) { throw new Error(result.error || "Command failed") } return result.data } catch (err) { const errorMessage = err instanceof Error ? err.message : "Unknown error" setError(errorMessage) console.error("Chat command failed:", err) throw err } finally { setIsLoading(false) } }, [backendSync], ) // Действия для сессий const createSession = useCallback(async (name?: string): Promise => { const sessionName = name || `Чат ${new Date().toLocaleString()}` // Пока backend не поддерживает чат, создаем локально const newSession: ChatSession = { id: `session_${Date.now()}`, name: sessionName, messages: [], createdAt: new Date(), updatedAt: new Date(), } setSessions((prev) => [...prev, newSession]) setCurrentSession(newSession) console.warn("Chat sessions not yet integrated with backend") return newSession // В будущем это будет: // return await executeCommand({ // type: 'CreateChatSession', // params: { name: sessionName } // }) }, []) const deleteSession = useCallback( async (sessionId: string) => { setSessions((prev) => prev.filter((s) => s.id !== sessionId)) if (currentSession?.id === sessionId) { setCurrentSession(null) } console.warn("Chat session deletion not yet integrated with backend") // В будущем это будет: // await executeCommand({ // type: 'DeleteChatSession', // params: { sessionId } // }) }, [currentSession], ) const switchToSession = useCallback(async (sessionId: string) => { // Используем функциональное обновление для получения актуального состояния setSessions((prevSessions) => { const session = prevSessions.find((s) => s.id === sessionId) if (session) { setCurrentSession(session) return prevSessions } // Создаем новую сессию с указанным ID для обратной совместимости const newSession: ChatSession = { id: sessionId, name: "Новый чат", messages: [], createdAt: new Date(), updatedAt: new Date(), } setCurrentSession(newSession) return [...prevSessions, newSession] }) }, []) // Действия для сообщений const sendMessage = useCallback( async (content: string) => { if (!currentSession) { // Создаем новую сессию если её нет const newSession = await createSession() setCurrentSession(newSession) } const userMessage: ChatMessage = { id: `msg_${Date.now()}`, content, role: "user", timestamp: new Date(), } // Добавляем сообщение пользователя setCurrentSession((prev) => { if (!prev) return prev const updatedSession = { ...prev, messages: [...prev.messages, userMessage], updatedAt: new Date(), } // Обновляем в списке сессий setSessions((prevSessions) => prevSessions.map((s) => (s.id === prev.id ? updatedSession : s))) return updatedSession }) // Симулируем ответ AI (пока нет интеграции с backend) setIsStreaming(true) setTimeout(() => { const aiMessage: ChatMessage = { id: `msg_${Date.now() + 1}`, content: `Это ответ на ваше сообщение: "${content}"`, role: "assistant", timestamp: new Date(), } setCurrentSession((prev) => { if (!prev) return prev const updatedSession = { ...prev, messages: [...prev.messages, aiMessage], updatedAt: new Date(), } setSessions((prevSessions) => prevSessions.map((s) => (s.id === prev.id ? updatedSession : s))) return updatedSession }) setIsStreaming(false) }, 1000) console.warn("Chat AI integration not yet implemented in backend") // В будущем это будет: // await executeCommand({ // type: 'SendChatMessage', // params: { sessionId: currentSession.id, content } // }) }, [currentSession, createSession], ) const clearCurrentSession = useCallback(async () => { if (!currentSession) return setCurrentSession((prev) => { if (!prev) return prev const clearedSession = { ...prev, messages: [], updatedAt: new Date(), } setSessions((prevSessions) => prevSessions.map((s) => (s.id === prev.id ? clearedSession : s))) return clearedSession }) console.warn("Chat session clearing not yet integrated with backend") // В будущем это будет: // await executeCommand({ // type: 'ClearChatSession', // params: { sessionId: currentSession.id } // }) }, [currentSession]) // Функции обратной совместимости const sendChatMessage = useCallback( async (content: string) => { setIsProcessing(true) try { await sendMessage(content) } finally { setIsProcessing(false) } }, [sendMessage], ) const receiveChatMessage = useCallback( (content: string) => { if (!currentSession) return const newMessage: ChatMessage = { id: Date.now().toString(), content, role: "assistant", timestamp: new Date(), } setCurrentSession((prev) => { if (!prev) return prev const updatedSession = { ...prev, messages: [...prev.messages, newMessage], updatedAt: new Date(), } setSessions((prevSessions) => prevSessions.map((s) => (s.id === prev.id ? updatedSession : s))) return updatedSession }) }, [currentSession], ) const selectAgent = useCallback((agentId: string) => { setSelectedAgentId(agentId) }, []) const setProcessing = useCallback((processing: boolean) => { setIsProcessing(processing) }, []) const createNewChat = useCallback(async () => { setIsCreatingNewChat(true) try { await createSession() } finally { setIsCreatingNewChat(false) } }, [createSession]) const switchSession = useCallback( async (sessionId: string) => { await switchToSession(sessionId) }, [switchToSession], ) const updateSessions = useCallback( async (newSessions?: any[]) => { setWasUpdated(true) if (newSessions) { // Для тестов и обратной совместимости - принимаем массив сессий const convertedSessions: ChatSession[] = newSessions.map((s) => { // Создаем пустые сообщения, если указан messageCount const messages: ChatMessage[] = s.messages || [] if (s.messageCount && !s.messages) { // Создаем фиктивные сообщения для соответствия messageCount for (let i = 0; i < s.messageCount; i++) { messages.push({ id: `msg_${s.id}_${i}`, content: `Message ${i + 1}`, role: i % 2 === 0 ? "user" : "assistant", timestamp: new Date(), }) } } return { id: s.id, name: s.title || s.name || "Чат", messages, createdAt: s.createdAt || new Date(), updatedAt: s.updatedAt || s.lastMessageAt || new Date(), } }) // Полностью заменяем все сессии новыми (удаляем default-session если она есть) setSessions(convertedSessions) // Если текущая сессия не в новом списке, сбрасываем её if (currentSession && !convertedSessions.find((s) => s.id === currentSession.id)) { setCurrentSession(null) } } else { // Обновление сессий уже происходит через backend state console.log("Sessions updated through backend state") } }, [currentSession], ) const clearMessages = useCallback(async () => { await clearCurrentSession() }, [clearCurrentSession]) // Методы для обратной совместимости const setErrorCompat = useCallback((errorMessage: string | null) => { setError(errorMessage) }, []) const removeMessage = useCallback((messageId: string) => { setCurrentSession((prev) => { if (!prev) return prev const updatedSession = { ...prev, messages: prev.messages.filter((msg) => msg.id !== messageId), updatedAt: new Date(), } // Обновляем в списке сессий setSessions((prevSessions) => prevSessions.map((s) => (s.id === prev.id ? updatedSession : s))) return updatedSession }) }, []) // Контекстное значение const contextValue: ChatContextType = { // Текущая сессия currentSession, isLoading, error, // История сессий - конвертируем в ChatListItem для обратной совместимости sessions: sessions.map((s) => { const item: any = { id: s.id, title: s.name, lastMessageAt: s.updatedAt, messageCount: s.messages.length, } // Добавляем lastMessage только если есть сообщения if (s.messages.length > 0) { item.lastMessage = s.messages[s.messages.length - 1]?.content } return item }), // UI состояние isOpen, inputText, isStreaming, // Действия для сессий createSession, deleteSession, switchToSession, // Действия для сообщений sendMessage, clearCurrentSession, // UI действия setIsOpen, setInputText, setIsStreaming, // Обратная совместимость со старым интерфейсом chatMessages: currentSession?.messages || [], sendChatMessage, receiveChatMessage, selectedAgentId, selectAgent, isProcessing, setProcessing, currentSessionId: currentSession?.id || null, isCreatingNewChat, createNewChat, switchSession, updateSessions, clearMessages, setError: setErrorCompat, removeMessage, } return {children} } export function useChat(): ChatContextType { const context = useContext(ChatContext) if (!context) { throw new Error("useChat должен использоваться внутри ChatProvider") } return context } // Экспорт типов export type { ChatContextType } // Экспорт контекста для типизации export { ChatContext }