From 863cf7f6b6cd8a1822e32fde03316a8d7ca978f8 Mon Sep 17 00:00:00 2001 From: Alexej Wolff Date: Wed, 11 Feb 2026 02:05:28 +0100 Subject: [PATCH] feat: auto-resize textarea, persistent token stats --- server/index.js | 92 ++++++++++++++++++++++++++++++++++++------ src/pages/GamePage.css | 10 +++-- src/pages/GamePage.tsx | 13 +++++- 3 files changed, 96 insertions(+), 19 deletions(-) diff --git a/server/index.js b/server/index.js index fc5a439..03578e2 100644 --- a/server/index.js +++ b/server/index.js @@ -409,6 +409,13 @@ app.put("/api/sessions/:storyId/:sessionId", requireAuth, async (req, res) => { try { const sessions = db.collection("game_sessions"); + // Получаем старую сессию для подсчёта новых токенов + const oldSession = await sessions.findOne({ + _id: new ObjectId(req.params.sessionId), + userId: req.session.userId, + }); + const oldMessageCount = oldSession?.messages?.length || 0; + // Преобразуем timestamp строки обратно в Date const messages = (req.body.messages || []).map((msg) => ({ ...msg, @@ -437,6 +444,26 @@ app.put("/api/sessions/:storyId/:sessionId", requireAuth, async (req, res) => { return res.status(404).json({ error: "Session not found" }); } + // Логируем новые токены (только для новых сообщений) + const newMessages = messages.slice(oldMessageCount); + if (newMessages.length > 0) { + const tokenUsage = db.collection("token_usage"); + const newTokens = newMessages.reduce((sum, msg) => { + return sum + Math.round((msg.content?.length || 0) / 3); + }, 0); + + if (newTokens > 0) { + await tokenUsage.insertOne({ + userId: req.session.userId, + storyId: req.params.storyId, + sessionId: req.params.sessionId, + tokens: newTokens, + messageCount: newMessages.length, + createdAt: new Date(), + }); + } + } + res.json({ success: true }); } catch (error) { console.error("Update session error:", error); @@ -582,6 +609,7 @@ app.get("/api/admin/stats", requireAuth, async (req, res) => { try { const stories = db.collection("stories"); const gameSessions = db.collection("game_sessions"); + const tokenUsage = db.collection("token_usage"); // Получаем все истории пользователя const userStories = await stories @@ -590,39 +618,77 @@ app.get("/api/admin/stats", requireAuth, async (req, res) => { // Получаем все сессии пользователя const userSessions = await gameSessions - .find({ storyId: { $in: userStories.map((s) => s._id.toString()) } }) + .find({ userId: req.session.userId }) .toArray(); + // Получаем общее количество токенов из лога (не зависит от удалённых сессий) + const tokenLogs = await tokenUsage + .find({ userId: req.session.userId }) + .toArray(); + const totalTokensLogged = tokenLogs.reduce( + (sum, log) => sum + (log.tokens || 0), + 0, + ); + + // Считаем токены в текущих сессиях (для сравнения) + const currentTokens = userSessions.reduce((sum, session) => { + const chars = (session.messages || []).reduce( + (s, msg) => s + (msg.content?.length || 0), + 0, + ); + return sum + Math.round(chars / 3); + }, 0); + + // Берём максимум: залогированные или текущие (для обратной совместимости) + const totalTokens = Math.max(totalTokensLogged, currentTokens); + // Считаем статистику для каждой истории const storyStats = userStories.map((story) => { - const session = userSessions.find( + const storySessions = userSessions.filter( (s) => s.storyId === story._id.toString(), ); - const messages = session?.messages || []; - const messageCount = messages.length; - - // Примерный подсчёт токенов (1 токен ≈ 3 символа для русского) - const totalChars = messages.reduce( - (sum, msg) => sum + (msg.content?.length || 0), + const messageCount = storySessions.reduce( + (sum, s) => sum + (s.messages?.length || 0), 0, ); - const tokens = Math.round(totalChars / 3); + + // Токены из лога для этой истории + const storyTokenLogs = tokenLogs.filter( + (l) => l.storyId === story._id.toString(), + ); + const loggedTokens = storyTokenLogs.reduce( + (sum, l) => sum + (l.tokens || 0), + 0, + ); + + // Текущие токены в сессиях + const currentStoryTokens = storySessions.reduce((sum, session) => { + const chars = (session.messages || []).reduce( + (s, msg) => s + (msg.content?.length || 0), + 0, + ); + return sum + Math.round(chars / 3); + }, 0); + + const tokens = Math.max(loggedTokens, currentStoryTokens); + + const lastSession = storySessions.sort( + (a, b) => new Date(b.updatedAt) - new Date(a.updatedAt), + )[0]; return { id: story._id.toString(), title: story.title, + sessionsCount: storySessions.length, messageCount, tokens, - lastPlayed: session?.updatedAt || null, + lastPlayed: lastSession?.updatedAt || null, }; }); // Сортируем по токенам (больше сверху) storyStats.sort((a, b) => b.tokens - a.tokens); - // Общая статистика - const totalTokens = storyStats.reduce((sum, s) => sum + s.tokens, 0); - res.json({ totalStories: userStories.length, totalSessions: userSessions.length, diff --git a/src/pages/GamePage.css b/src/pages/GamePage.css index 832cc56..d2a402d 100644 --- a/src/pages/GamePage.css +++ b/src/pages/GamePage.css @@ -420,9 +420,9 @@ .input-container textarea { flex: 1; - min-height: 50px; - max-height: 120px; - padding: 0.875rem 1rem; + min-height: 44px; + max-height: 150px; + padding: 0.75rem 1rem; background: #1a1a1a; border: 2px solid #333; border-radius: 12px; @@ -430,7 +430,9 @@ font-size: 1rem; font-family: inherit; resize: none; - transition: border-color 0.2s; + transition: border-color 0.2s, height 0.1s ease; + overflow-y: auto; + line-height: 1.4; } .input-container textarea:focus { diff --git a/src/pages/GamePage.tsx b/src/pages/GamePage.tsx index 6bfd437..6a5e330 100644 --- a/src/pages/GamePage.tsx +++ b/src/pages/GamePage.tsx @@ -240,6 +240,10 @@ export default function GamePage() { const tempSession = { ...session, messages: updatedMessages }; setSession(tempSession); setInput(""); + // Reset textarea height + if (inputRef.current) { + inputRef.current.style.height = "auto"; + } setIsLoading(true); setError(null); setStreamingContent(""); @@ -588,11 +592,16 @@ export default function GamePage() {