feat: multiple sessions per story with streaming AI responses

This commit is contained in:
Alexej Wolff
2026-02-11 01:47:24 +01:00
parent 161ecd661e
commit 8c6d6591f8
7 changed files with 573 additions and 112 deletions
+23 -27
View File
@@ -4,18 +4,23 @@ import { useAuth } from "../contexts/AuthContext";
import {
getStory,
deleteStory as apiDeleteStory,
getSession,
getSessionsList,
getPlayerCharacters,
} from "../services/api";
import type { Story, GameSession, PlayerCharacter } from "../types";
import type { Story, PlayerCharacter } from "../types";
import "./StoryDetailPage.css";
interface SessionsInfo {
count: number;
totalMessages: number;
}
export default function StoryDetailPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { isAuthenticated } = useAuth();
const [story, setStory] = useState<Story | null>(null);
const [session, setSession] = useState<GameSession | null>(null);
const [sessionsInfo, setSessionsInfo] = useState<SessionsInfo | null>(null);
const [playerCharacters, setPlayerCharacters] = useState<PlayerCharacter[]>(
[],
);
@@ -39,10 +44,13 @@ export default function StoryDetailPage() {
...foundStory,
id: (foundStory as any)._id || foundStory.id,
});
const existingSession = await getSession(id);
if (existingSession) {
setSession(existingSession);
setSelectedCharacter(existingSession.playerId || null);
const sessions = await getSessionsList(id);
if (sessions.length > 0) {
const totalMessages = sessions.reduce((sum, s) => sum + s.messagesCount, 0);
setSessionsInfo({
count: sessions.length,
totalMessages,
});
}
}
@@ -81,7 +89,7 @@ export default function StoryDetailPage() {
return;
}
if (!session) {
if (!sessionsInfo) {
// Новая игра — показываем выбор персонажа
setShowCharacterSelect(true);
} else {
@@ -98,16 +106,6 @@ export default function StoryDetailPage() {
navigate(`/play/${story!.id}?character=${selectedCharacter}`);
};
// Примерный подсчёт токенов (1 токен ≈ 3 символа для русского текста)
const estimateTokens = (messages: GameSession["messages"] | undefined) => {
if (!messages || messages.length === 0) return 0;
const totalChars = messages.reduce(
(sum, msg) => sum + msg.content.length,
0,
);
return Math.round(totalChars / 3);
};
const formatTokens = (tokens: number) => {
if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`;
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`;
@@ -302,24 +300,22 @@ export default function StoryDetailPage() {
)}
</section>
{session && (
{sessionsInfo && (
<section className="detail-section session-info">
<h2>🎮 Текущий прогресс</h2>
<div className="session-stats">
<div className="stat">
<span className="stat-label">Сообщений</span>
<span className="stat-value">{session.messages.length}</span>
<span className="stat-label">Сессий</span>
<span className="stat-value">{sessionsInfo.count}</span>
</div>
<div className="stat">
<span className="stat-label">Локация</span>
<span className="stat-value">
{session.currentState.location || "Неизвестно"}
</span>
<span className="stat-label">Сообщений</span>
<span className="stat-value">{sessionsInfo.totalMessages}</span>
</div>
<div className="stat">
<span className="stat-label"> Токенов</span>
<span className="stat-value">
{formatTokens(estimateTokens(session.messages))}
{formatTokens(sessionsInfo.totalMessages * 50)}
</span>
</div>
</div>
@@ -328,7 +324,7 @@ export default function StoryDetailPage() {
<div className="story-actions">
<button onClick={handleStartGame} className="action-btn play-btn">
{session ? "🎮 Продолжить игру" : "🎮 Начать приключение"}
{sessionsInfo ? "🎮 Продолжить игру" : "🎮 Начать приключение"}
</button>
<Link to={`/edit/${story.id}`} className="action-btn edit-btn">
Редактировать