feat: auto-resize textarea, persistent token stats
This commit is contained in:
+79
-13
@@ -409,6 +409,13 @@ app.put("/api/sessions/:storyId/:sessionId", requireAuth, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const sessions = db.collection("game_sessions");
|
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
|
// Преобразуем timestamp строки обратно в Date
|
||||||
const messages = (req.body.messages || []).map((msg) => ({
|
const messages = (req.body.messages || []).map((msg) => ({
|
||||||
...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" });
|
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 });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Update session error:", error);
|
console.error("Update session error:", error);
|
||||||
@@ -582,6 +609,7 @@ app.get("/api/admin/stats", requireAuth, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const stories = db.collection("stories");
|
const stories = db.collection("stories");
|
||||||
const gameSessions = db.collection("game_sessions");
|
const gameSessions = db.collection("game_sessions");
|
||||||
|
const tokenUsage = db.collection("token_usage");
|
||||||
|
|
||||||
// Получаем все истории пользователя
|
// Получаем все истории пользователя
|
||||||
const userStories = await stories
|
const userStories = await stories
|
||||||
@@ -590,39 +618,77 @@ app.get("/api/admin/stats", requireAuth, async (req, res) => {
|
|||||||
|
|
||||||
// Получаем все сессии пользователя
|
// Получаем все сессии пользователя
|
||||||
const userSessions = await gameSessions
|
const userSessions = await gameSessions
|
||||||
.find({ storyId: { $in: userStories.map((s) => s._id.toString()) } })
|
.find({ userId: req.session.userId })
|
||||||
.toArray();
|
.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 storyStats = userStories.map((story) => {
|
||||||
const session = userSessions.find(
|
const storySessions = userSessions.filter(
|
||||||
(s) => s.storyId === story._id.toString(),
|
(s) => s.storyId === story._id.toString(),
|
||||||
);
|
);
|
||||||
const messages = session?.messages || [];
|
const messageCount = storySessions.reduce(
|
||||||
const messageCount = messages.length;
|
(sum, s) => sum + (s.messages?.length || 0),
|
||||||
|
|
||||||
// Примерный подсчёт токенов (1 токен ≈ 3 символа для русского)
|
|
||||||
const totalChars = messages.reduce(
|
|
||||||
(sum, msg) => sum + (msg.content?.length || 0),
|
|
||||||
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 {
|
return {
|
||||||
id: story._id.toString(),
|
id: story._id.toString(),
|
||||||
title: story.title,
|
title: story.title,
|
||||||
|
sessionsCount: storySessions.length,
|
||||||
messageCount,
|
messageCount,
|
||||||
tokens,
|
tokens,
|
||||||
lastPlayed: session?.updatedAt || null,
|
lastPlayed: lastSession?.updatedAt || null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Сортируем по токенам (больше сверху)
|
// Сортируем по токенам (больше сверху)
|
||||||
storyStats.sort((a, b) => b.tokens - a.tokens);
|
storyStats.sort((a, b) => b.tokens - a.tokens);
|
||||||
|
|
||||||
// Общая статистика
|
|
||||||
const totalTokens = storyStats.reduce((sum, s) => sum + s.tokens, 0);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
totalStories: userStories.length,
|
totalStories: userStories.length,
|
||||||
totalSessions: userSessions.length,
|
totalSessions: userSessions.length,
|
||||||
|
|||||||
@@ -420,9 +420,9 @@
|
|||||||
|
|
||||||
.input-container textarea {
|
.input-container textarea {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 50px;
|
min-height: 44px;
|
||||||
max-height: 120px;
|
max-height: 150px;
|
||||||
padding: 0.875rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
background: #1a1a1a;
|
background: #1a1a1a;
|
||||||
border: 2px solid #333;
|
border: 2px solid #333;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -430,7 +430,9 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
resize: none;
|
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 {
|
.input-container textarea:focus {
|
||||||
|
|||||||
+11
-2
@@ -240,6 +240,10 @@ export default function GamePage() {
|
|||||||
const tempSession = { ...session, messages: updatedMessages };
|
const tempSession = { ...session, messages: updatedMessages };
|
||||||
setSession(tempSession);
|
setSession(tempSession);
|
||||||
setInput("");
|
setInput("");
|
||||||
|
// Reset textarea height
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.style.height = "auto";
|
||||||
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setStreamingContent("");
|
setStreamingContent("");
|
||||||
@@ -588,11 +592,16 @@ export default function GamePage() {
|
|||||||
<textarea
|
<textarea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setInput(e.target.value);
|
||||||
|
// Auto-resize
|
||||||
|
e.target.style.height = "auto";
|
||||||
|
e.target.style.height = Math.min(e.target.scrollHeight, 150) + "px";
|
||||||
|
}}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Что ты хочешь сделать?..."
|
placeholder="Что ты хочешь сделать?..."
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
rows={2}
|
rows={1}
|
||||||
/>
|
/>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<button onClick={handleStop} className="send-btn stop-btn">
|
<button onClick={handleStop} className="send-btn stop-btn">
|
||||||
|
|||||||
Reference in New Issue
Block a user