feat: multiple sessions per story with streaming AI responses
This commit is contained in:
+192
-57
@@ -4,9 +4,13 @@ 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,
|
||||
@@ -45,6 +49,8 @@ export default function GamePage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const [story, setStory] = useState<Story | null>(null);
|
||||
const [sessionsList, setSessionsList] = useState<SessionListItem[]>([]);
|
||||
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
||||
const [session, setSession] = useState<GameSession | null>(null);
|
||||
const [playerCharacter, setPlayerCharacter] =
|
||||
useState<PlayerCharacter | null>(null);
|
||||
@@ -53,6 +59,7 @@ export default function GamePage() {
|
||||
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [streamingContent, setStreamingContent] = useState("");
|
||||
const [showSessionMenu, setShowSessionMenu] = useState(false);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -72,12 +79,11 @@ export default function GamePage() {
|
||||
};
|
||||
setStory(normalizedStory);
|
||||
|
||||
let existingSession = await getSession(id);
|
||||
console.log(
|
||||
"[GamePage] Loaded session:",
|
||||
existingSession?.messages?.length || 0,
|
||||
"messages",
|
||||
);
|
||||
// Загружаем список сессий
|
||||
const sessions = await getSessionsList(id);
|
||||
console.log("[GamePage] Sessions list:", sessions);
|
||||
setSessionsList(sessions);
|
||||
|
||||
const characterId = searchParams.get("character");
|
||||
|
||||
// Загружаем персонажа
|
||||
@@ -85,51 +91,42 @@ export default function GamePage() {
|
||||
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);
|
||||
// Если есть сессии, загружаем последнюю (или создаём новую)
|
||||
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);
|
||||
@@ -170,6 +167,7 @@ export default function GamePage() {
|
||||
storyData: Story,
|
||||
sessionData: GameSession,
|
||||
character: PlayerCharacter,
|
||||
sessionId: string,
|
||||
) => {
|
||||
// Если есть заготовленное первое сообщение, используем его
|
||||
if (storyData.firstMessage && storyData.firstMessage.trim()) {
|
||||
@@ -190,7 +188,7 @@ export default function GamePage() {
|
||||
messages: [assistantMessage],
|
||||
};
|
||||
|
||||
await apiSaveSession(storyData.id, updatedSession);
|
||||
await apiSaveSession(storyData.id, sessionId, updatedSession);
|
||||
setSession(updatedSession);
|
||||
return;
|
||||
}
|
||||
@@ -218,7 +216,7 @@ export default function GamePage() {
|
||||
messages: [assistantMessage],
|
||||
};
|
||||
|
||||
await apiSaveSession(storyData.id, updatedSession);
|
||||
await apiSaveSession(storyData.id, sessionId, updatedSession);
|
||||
setSession(updatedSession);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Произошла ошибка");
|
||||
@@ -228,7 +226,8 @@ export default function GamePage() {
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!input.trim() || !story || !session || isLoading) return;
|
||||
if (!input.trim() || !story || !session || !currentSessionId || isLoading)
|
||||
return;
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
id: generateId(),
|
||||
@@ -295,7 +294,11 @@ export default function GamePage() {
|
||||
storySummary: newSummary,
|
||||
};
|
||||
|
||||
const saved = await apiSaveSession(story.id, finalSession);
|
||||
const saved = await apiSaveSession(
|
||||
story.id,
|
||||
currentSessionId,
|
||||
finalSession,
|
||||
);
|
||||
console.log(
|
||||
"[GamePage] Session saved:",
|
||||
saved,
|
||||
@@ -317,7 +320,7 @@ export default function GamePage() {
|
||||
...session,
|
||||
messages: [...updatedMessages, partialMessage],
|
||||
};
|
||||
await apiSaveSession(story.id, partialSession);
|
||||
await apiSaveSession(story.id, currentSessionId, partialSession);
|
||||
setSession(partialSession);
|
||||
}
|
||||
} else {
|
||||
@@ -346,6 +349,89 @@ export default function GamePage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Функции управления сессиями
|
||||
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 (
|
||||
<div className="game-page">
|
||||
@@ -377,9 +463,58 @@ export default function GamePage() {
|
||||
</Link>
|
||||
<div className="header-info">
|
||||
<h1>{story.title}</h1>
|
||||
<span className="header-protagonist">
|
||||
👤 {playerCharacter?.name || "Герой"}
|
||||
</span>
|
||||
<div className="header-session">
|
||||
<button
|
||||
className="session-selector"
|
||||
onClick={() => setShowSessionMenu(!showSessionMenu)}
|
||||
>
|
||||
📖 {currentSessionName} ▾
|
||||
</button>
|
||||
{showSessionMenu && (
|
||||
<div className="session-menu">
|
||||
<div className="session-menu-header">
|
||||
<span>Сессии</span>
|
||||
<button
|
||||
className="new-session-btn"
|
||||
onClick={handleCreateNewSession}
|
||||
title="Новая сессия"
|
||||
>
|
||||
➕
|
||||
</button>
|
||||
</div>
|
||||
<div className="session-list">
|
||||
{sessionsList.map((s) => (
|
||||
<div
|
||||
key={s.id}
|
||||
className={`session-item ${s.id === currentSessionId ? "active" : ""}`}
|
||||
>
|
||||
<button
|
||||
className="session-name"
|
||||
onClick={() => handleSwitchSession(s.id)}
|
||||
>
|
||||
{s.name}
|
||||
<span className="session-messages">
|
||||
{s.messagesCount} сообщ.
|
||||
</span>
|
||||
</button>
|
||||
{sessionsList.length > 1 && (
|
||||
<button
|
||||
className="delete-session-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteSession(s.id);
|
||||
}}
|
||||
title="Удалить сессию"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="header-stats">
|
||||
<span className="stat-badge tokens">
|
||||
|
||||
Reference in New Issue
Block a user