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, getSession, saveSession as apiSaveSession, getPlayerCharacter, } from "../services/api"; import { generateStoryResponse, 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(); } // Пытаемся определить локацию из последних сообщений function detectLocation(messages: ChatMessage[]): string { if (!messages || messages.length === 0) return "Неизвестно"; // Берём последние 3 сообщения ассистента const recentAssistant = messages .filter((m) => m.role === "assistant") .slice(-3) .map((m) => m.content) .join(" "); // Паттерны для определения локации const locationPatterns = [ /(?:находи(?:тесь|шься)|оказыва(?:етесь|ешься)|стои(?:те|шь))\s+(?:в|на|у)\s+([^.,!?]+)/i, /(?:вошл[аи]?|входи(?:те|шь)|попада(?:ете|ешь))\s+(?:в|на)\s+([^.,!?]+)/i, /(?:прибыл[аи]?|приход(?:ите|ишь)|добрал(?:ись|ась|ся))\s+(?:в|на|до)\s+([^.,!?]+)/i, /(?:комнат[аеуы]|зал[аеуы]?|пещер[аеуы]|лес[ау|замок|двор(?:ец)?|тавер[нуыа]|город[ауе]?|деревн[яюией]|тронн[ыйая]|подземель[яеи])\s*([^.,!?]*)/i, ]; for (const pattern of locationPatterns) { const match = recentAssistant.match(pattern); if (match && match[1]) { // Чистим и обрезаем результат let location = match[1].trim(); if (location.length > 25) location = location.substring(0, 25) + "..."; return location; } } // Простой поиск ключевых слов const simpleLocations: [RegExp, string][] = [ [/тронн(?:ый|ого|ом)\s*зал/i, "Тронный зал"], [/тавер[нуыа]/i, "Таверна"], [/замок|замк[ауе]/i, "Замок"], [/лес[ау]?/i, "Лес"], [/пещер[аеуы]/i, "Пещера"], [/город[ауе]?/i, "Город"], [/деревн[яюией]/i, "Деревня"], [/подземель[яеи]/i, "Подземелье"], [/двор(?:ец|ц[ауе])/i, "Дворец"], [/рын(?:ок|к[ауе])/i, "Рынок"], [/храм[ауе]?/i, "Храм"], [/библиотек/i, "Библиотека"], [/казарм/i, "Казармы"], ]; for (const [pattern, name] of simpleLocations) { if (pattern.test(recentAssistant)) { return name; } } return "Неизвестно"; } export default function GamePage() { const { id } = useParams<{ id: string }>(); const [searchParams] = useSearchParams(); const { isAuthenticated } = useAuth(); const [story, setStory] = 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 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); let existingSession = await getSession(id); console.log( "[GamePage] Loaded session:", existingSession?.messages?.length || 0, "messages", ); const characterId = searchParams.get("character"); // Загружаем персонажа let character: PlayerCharacter | null = null; if (characterId) { character = await getPlayerCharacter(characterId); setPlayerCharacter(character); } else if (existingSession?.playerId) { character = await getPlayerCharacter(existingSession.playerId); setPlayerCharacter(character); } if (!existingSession) { console.log("[GamePage] No existing session, creating new"); existingSession = { storyId: id, playerId: characterId || undefined, messages: [], currentState: { location: "Неизвестно", health: 100, inventory: [], questProgress: {}, }, }; } else if (characterId && existingSession.playerId !== characterId) { // Новый персонаж — новая сессия console.log("[GamePage] New character selected, resetting session"); existingSession = { storyId: id, playerId: characterId, messages: [], currentState: { location: "Неизвестно", health: 100, inventory: [], questProgress: {}, }, }; } else { console.log( "[GamePage] Using existing session with", existingSession.messages.length, "messages", ); } setSession(existingSession); // Начинаем историю если это новая сессия if (existingSession.messages.length === 0 && character) { startStory(normalizedStory, existingSession, character); } } 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, ) => { // Если есть заготовленное первое сообщение, используем его 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, 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, updatedSession); setSession(updatedSession); } catch (err) { setError(err instanceof Error ? err.message : "Произошла ошибка"); } finally { setIsLoading(false); } }; const handleSend = async () => { if (!input.trim() || !story || !session || 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(""); setIsLoading(true); setError(null); try { // Передаём session для оптимизированного контекста const response = await generateStoryResponse( story, session.messages, input.trim(), playerCharacter || undefined, session, ); 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, finalSession); console.log( "[GamePage] Session saved:", saved, "Messages:", allMessages.length, ); setSession(finalSession); } catch (err) { setError(err instanceof Error ? err.message : "Произошла ошибка"); // Откатываем сообщение пользователя при ошибке setSession(session); setInput(userMessage.content); } finally { setIsLoading(false); inputRef.current?.focus(); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }; if (isInitialLoading) { return (

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

); } if (!story) { return (

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

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

{story.title}

👤 {playerCharacter?.name || "Герой"}
🎟️ {formatTokens(estimateTokens(session?.messages || []))} 📍 {detectLocation(session?.messages || [])}
{session?.messages.map((message) => (
{message.content}
{new Date(message.timestamp).toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit", })}
))} {isLoading && (
)} {error && (
⚠️ {error}
)}
{/* RPG кнопки скрыты — раскомментировать при необходимости
*/}