import { useState, useEffect, useRef } from "react"; import { useParams, Link, useSearchParams } from "react-router-dom"; import ReactMarkdown from "react-markdown"; import { useAuth } from "../contexts/AuthContext"; import { getStory, getSessionsList, getSession, createSession, saveSession as apiSaveSession, deleteSession as apiDeleteSession, getPlayerCharacter, type SessionListItem, } from "../services/api"; import { generateStoryResponseStream, buildSystemPrompt, sendMessage, generateStorySummary, extractKeyEvents, } from "../services/deepseek"; import type { Story, GameSession, ChatMessage, PlayerCharacter, } from "../types"; import "./GamePage.css"; function generateId(): string { return Math.random().toString(36).substring(2) + Date.now().toString(36); } // Примерный подсчёт токенов (1 токен ≈ 3 символа для русского) function estimateTokens(messages: ChatMessage[]): number { if (!messages || messages.length === 0) return 0; const totalChars = messages.reduce((sum, msg) => sum + msg.content.length, 0); return Math.round(totalChars / 3); } function formatTokens(tokens: number): string { if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`; if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`; return tokens.toString(); } export default function GamePage() { const { id } = useParams<{ id: string }>(); const [searchParams] = useSearchParams(); const { isAuthenticated } = useAuth(); const [story, setStory] = useState(null); const [sessionsList, setSessionsList] = useState([]); const [currentSessionId, setCurrentSessionId] = useState(null); const [session, setSession] = useState(null); const [playerCharacter, setPlayerCharacter] = useState(null); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const [isInitialLoading, setIsInitialLoading] = useState(true); const [error, setError] = useState(null); const [streamingContent, setStreamingContent] = useState(""); const [showSessionMenu, setShowSessionMenu] = useState(false); const abortControllerRef = useRef(null); const messagesEndRef = useRef(null); const inputRef = useRef(null); useEffect(() => { const loadGame = async () => { if (!id || !isAuthenticated) { setIsInitialLoading(false); return; } const foundStory = await getStory(id); if (foundStory) { const normalizedStory = { ...foundStory, id: (foundStory as any)._id || foundStory.id, }; setStory(normalizedStory); // Загружаем список сессий const sessions = await getSessionsList(id); console.log("[GamePage] Sessions list:", sessions); setSessionsList(sessions); const characterId = searchParams.get("character"); // Загружаем персонажа let character: PlayerCharacter | null = null; if (characterId) { character = await getPlayerCharacter(characterId); setPlayerCharacter(character); } // Если есть сессии, загружаем последнюю (или создаём новую) if (sessions.length > 0) { const latestSession = sessions[0]; console.log("[GamePage] Loading latest session:", latestSession); setCurrentSessionId(latestSession.id); const sessionData = await getSession(id, latestSession.id); console.log("[GamePage] Session data loaded:", sessionData); if (sessionData) { setSession(sessionData); // Загружаем персонажа из сессии если не выбран if (!character && sessionData.playerId) { character = await getPlayerCharacter(sessionData.playerId); setPlayerCharacter(character); } } } else if (characterId) { // Нет сессий и выбран персонаж — создаём новую const newSession = await createSession(id, undefined, characterId); if (newSession) { setSessionsList([newSession]); setCurrentSessionId(newSession.id); const sessionData = await getSession(id, newSession.id); if (sessionData) { setSession(sessionData); if (character) { startStory( normalizedStory, sessionData, character, newSession.id, ); } } } } } setIsInitialLoading(false); }; loadGame(); }, [id, isAuthenticated, searchParams]); // Обновляем историю при возврате на страницу (после редактирования) useEffect(() => { const handleFocus = async () => { if (!id || !isAuthenticated || isInitialLoading) return; const updatedStory = await getStory(id); if (updatedStory) { const normalizedStory = { ...updatedStory, id: (updatedStory as any)._id || updatedStory.id, }; setStory(normalizedStory); console.log("[GamePage] История обновлена после возврата на страницу"); } }; window.addEventListener("focus", handleFocus); return () => window.removeEventListener("focus", handleFocus); }, [id, isAuthenticated, isInitialLoading]); useEffect(() => { scrollToBottom(); }, [session?.messages]); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; const startStory = async ( storyData: Story, sessionData: GameSession, character: PlayerCharacter, sessionId: string, ) => { // Если есть заготовленное первое сообщение, используем его if (storyData.firstMessage && storyData.firstMessage.trim()) { // Заменяем {user} на имя персонажа const firstMessageContent = storyData.firstMessage.replace( /\{user\}/gi, character.name, ); const assistantMessage: ChatMessage = { id: generateId(), role: "assistant", content: firstMessageContent, timestamp: new Date(), }; const updatedSession: GameSession = { ...sessionData, messages: [assistantMessage], }; await apiSaveSession(storyData.id, sessionId, updatedSession); setSession(updatedSession); return; } // Иначе генерируем через ИИ setIsLoading(true); setError(null); try { const systemPrompt = buildSystemPrompt(storyData, character); const response = await sendMessage([ { role: "system", content: systemPrompt }, { role: "user", content: "Начни историю." }, ]); const assistantMessage: ChatMessage = { id: generateId(), role: "assistant", content: response, timestamp: new Date(), }; const updatedSession: GameSession = { ...sessionData, messages: [assistantMessage], }; await apiSaveSession(storyData.id, sessionId, updatedSession); setSession(updatedSession); } catch (err) { setError(err instanceof Error ? err.message : "Произошла ошибка"); } finally { setIsLoading(false); } }; const handleSend = async () => { if (!input.trim() || !story || !session || !currentSessionId || isLoading) return; const userMessage: ChatMessage = { id: generateId(), role: "user", content: input.trim(), timestamp: new Date(), }; const updatedMessages = [...session.messages, userMessage]; const tempSession = { ...session, messages: updatedMessages }; setSession(tempSession); setInput(""); // Reset textarea height if (inputRef.current) { inputRef.current.style.height = "auto"; } setIsLoading(true); setError(null); setStreamingContent(""); // Создаём AbortController для возможности отмены abortControllerRef.current = new AbortController(); try { // Streaming ответ от AI const response = await generateStoryResponseStream( story, session.messages, input.trim(), (chunk) => { setStreamingContent((prev) => prev + chunk); }, playerCharacter || undefined, session, abortControllerRef.current.signal, ); const assistantMessage: ChatMessage = { id: generateId(), role: "assistant", content: response, timestamp: new Date(), }; const allMessages = [...updatedMessages, assistantMessage]; // Обновляем ключевые события const newKeyEvents = await extractKeyEvents( response, session.keyEvents || [], ); // Генерируем сводку каждые 20 сообщений let newSummary = session.storySummary; if (allMessages.length % 20 === 0 && allMessages.length > 0) { console.log("[GamePage] Generating story summary..."); newSummary = await generateStorySummary( story, allMessages, session.storySummary, ); } const finalSession: GameSession = { ...session, messages: allMessages, keyEvents: newKeyEvents, storySummary: newSummary, }; const saved = await apiSaveSession( story.id, currentSessionId, finalSession, ); console.log( "[GamePage] Session saved:", saved, "Messages:", allMessages.length, ); setSession(finalSession); } catch (err) { if (err instanceof Error && err.name === "AbortError") { // Пользователь отменил — сохраняем то что получили if (streamingContent.trim()) { const partialMessage: ChatMessage = { id: generateId(), role: "assistant", content: streamingContent, timestamp: new Date(), }; const partialSession: GameSession = { ...session, messages: [...updatedMessages, partialMessage], }; await apiSaveSession(story.id, currentSessionId, partialSession); setSession(partialSession); } } else { setError(err instanceof Error ? err.message : "Произошла ошибка"); setSession(session); setInput(userMessage.content); } } finally { setIsLoading(false); setStreamingContent(""); abortControllerRef.current = null; inputRef.current?.focus(); } }; const handleStop = () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }; // Функции управления сессиями const handleCreateNewSession = async () => { if (!story || !id) return; setShowSessionMenu(false); // Используем персонажа из текущей сессии или выбранного const characterId = playerCharacter?.id || session?.playerId; if (!characterId) { alert("Не выбран персонаж для игры"); return; } const name = `Сессия ${sessionsList.length + 1}`; const newSession = await createSession(id, name, characterId); if (newSession) { setSessionsList([newSession, ...sessionsList]); setCurrentSessionId(newSession.id); const sessionData = await getSession(id, newSession.id); if (sessionData) { setSession(sessionData); // Загружаем персонажа если нет let character = playerCharacter; if (!character && characterId) { character = await getPlayerCharacter(characterId); setPlayerCharacter(character); } if (character) { startStory(story, sessionData, character, newSession.id); } } } }; const handleSwitchSession = async (sessionId: string) => { console.log("[GamePage] Switching to session:", sessionId, "current:", currentSessionId); if (!id || sessionId === currentSessionId) { setShowSessionMenu(false); return; } setShowSessionMenu(false); setIsInitialLoading(true); const sessionData = await getSession(id, sessionId); console.log("[GamePage] Loaded session data:", sessionData); if (sessionData) { setCurrentSessionId(sessionId); setSession(sessionData); // Загружаем персонажа сессии if (sessionData.playerId) { const character = await getPlayerCharacter(sessionData.playerId); setPlayerCharacter(character); } } setIsInitialLoading(false); }; const handleDeleteSession = async (sessionId: string) => { if (!id || sessionsList.length <= 1) { alert("Нельзя удалить единственную сессию"); return; } if (!confirm("Удалить эту сессию? Это действие нельзя отменить.")) { return; } const success = await apiDeleteSession(id, sessionId); if (success) { const updatedList = sessionsList.filter((s) => s.id !== sessionId); setSessionsList(updatedList); // Если удалили текущую сессию — переключаемся на другую if (sessionId === currentSessionId && updatedList.length > 0) { await handleSwitchSession(updatedList[0].id); } } setShowSessionMenu(false); }; const currentSessionName = sessionsList.find((s) => s.id === currentSessionId)?.name || "Сессия"; if (isInitialLoading) { return (

Загрузка игры...

); } if (!story) { return (

История не найдена

← Вернуться к списку
); } return (

{story.title}

{showSessionMenu && (
Сессии
{sessionsList.map((s) => (
{sessionsList.length > 1 && ( )}
))}
)}
🎟️ {formatTokens(estimateTokens(session?.messages || []))}
{session?.messages.map((message) => (
{message.content}
{new Date(message.timestamp).toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit", })}
))} {isLoading && streamingContent && (
{streamingContent}
)} {isLoading && !streamingContent && (
)} {error && (
⚠️ {error}
)}
{/* RPG кнопки скрыты — раскомментировать при необходимости
*/}
{ e.preventDefault(); handleSend(); }}>