From 2bfe8d95ccba2b429b787043bb0d81d87e59edf4 Mon Sep 17 00:00:00 2001 From: Alexej Wolff Date: Wed, 11 Feb 2026 01:07:43 +0100 Subject: [PATCH] fix: input alignment --- server/index.js | 4 +-- src/App.tsx | 41 ++++++++++++++-------- src/components/Footer.css | 73 +++++++++++++++++++++++++++++++++++++++ src/components/Footer.tsx | 48 +++++++++++++++++++++++++ src/components/Header.tsx | 3 -- src/pages/AdminPage.css | 4 ++- src/pages/AdminPage.tsx | 17 +++++---- src/pages/GamePage.css | 6 +++- src/pages/GamePage.tsx | 64 +--------------------------------- 9 files changed, 169 insertions(+), 91 deletions(-) create mode 100644 src/components/Footer.css create mode 100644 src/components/Footer.tsx diff --git a/server/index.js b/server/index.js index d20e1eb..832bacb 100644 --- a/server/index.js +++ b/server/index.js @@ -493,7 +493,7 @@ app.get("/api/admin/stats", requireAuth, async (req, res) => { // Считаем статистику для каждой истории const storyStats = userStories.map((story) => { const session = userSessions.find( - (s) => s.storyId === story._id.toString() + (s) => s.storyId === story._id.toString(), ); const messages = session?.messages || []; const messageCount = messages.length; @@ -501,7 +501,7 @@ app.get("/api/admin/stats", requireAuth, async (req, res) => { // Примерный подсчёт токенов (1 токен ≈ 3 символа для русского) const totalChars = messages.reduce( (sum, msg) => sum + (msg.content?.length || 0), - 0 + 0, ); const tokens = Math.round(totalChars / 3); diff --git a/src/App.tsx b/src/App.tsx index 18da0df..5b540af 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ -import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom"; import { AuthProvider } from "./contexts/AuthContext"; import { Header } from "./components/Header"; +import { Footer } from "./components/Footer"; import StoriesPage from "./pages/StoriesPage"; import StoryDetailPage from "./pages/StoryDetailPage"; import CreateStoryPage from "./pages/CreateStoryPage"; @@ -9,24 +10,34 @@ import GamePage from "./pages/GamePage"; import AdminPage from "./pages/AdminPage"; import "./App.css"; +function AppContent() { + const location = useLocation(); + const isGamePage = location.pathname.startsWith("/play/"); + + return ( +
+ {!isGamePage &&
} +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+ {!isGamePage &&
} +
+ ); +} + function App() { return ( -
-
-
- - } /> - } /> - } /> - } /> - } /> - } /> - } /> - -
-
+
); diff --git a/src/components/Footer.css b/src/components/Footer.css new file mode 100644 index 0000000..3a2f6a6 --- /dev/null +++ b/src/components/Footer.css @@ -0,0 +1,73 @@ +.footer { + background: #1a1a1a; + border-top: 1px solid #2a2a2a; + padding: 0.75rem 1.5rem; +} + +.footer-content { + max-width: 1200px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; +} + +.footer-left { + display: flex; + align-items: center; + gap: 1rem; +} + +.footer-brand { + font-size: 0.9rem; + color: #888; +} + +.footer-copyright { + font-size: 0.8rem; + color: #555; +} + +.footer-stats { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: rgba(100, 181, 246, 0.1); + border: 1px solid rgba(100, 181, 246, 0.2); + border-radius: 20px; + text-decoration: none; + transition: all 0.2s; +} + +.footer-stats:hover { + background: rgba(100, 181, 246, 0.15); + border-color: rgba(100, 181, 246, 0.3); +} + +.stats-icon { + font-size: 1rem; +} + +.stats-label { + font-size: 0.8rem; + color: #888; +} + +.stats-value { + font-size: 0.9rem; + font-weight: 600; + color: #64b5f6; +} + +@media (max-width: 600px) { + .footer-content { + flex-direction: column; + gap: 0.75rem; + } + + .footer-stats { + width: 100%; + justify-content: center; + } +} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..a2ea5f1 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,48 @@ +import { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { useAuth } from "../contexts/AuthContext"; +import { getAdminStats } from "../services/api"; +import "./Footer.css"; + +export function Footer() { + const { isAuthenticated } = useAuth(); + const [totalTokens, setTotalTokens] = useState(null); + + useEffect(() => { + const loadStats = async () => { + if (!isAuthenticated) return; + const stats = await getAdminStats(); + if (stats) { + setTotalTokens(stats.totalTokens); + } + }; + loadStats(); + }, [isAuthenticated]); + + const formatTokens = (tokens: number) => { + if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(2)}M`; + if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`; + return tokens.toString(); + }; + + return ( +
+
+
+ ⚔️ ReSekai + © 2026 +
+ + {isAuthenticated && ( + + 📊 + Токенов: + + {totalTokens !== null ? formatTokens(totalTokens) : "..."} + + + )} +
+
+ ); +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 99fc50f..6e28b52 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -26,9 +26,6 @@ export function Header() { Персонажи - - 📊 Статистика - )} diff --git a/src/pages/AdminPage.css b/src/pages/AdminPage.css index 5b4d006..c03d5b9 100644 --- a/src/pages/AdminPage.css +++ b/src/pages/AdminPage.css @@ -43,7 +43,9 @@ display: flex; align-items: center; gap: 1rem; - transition: transform 0.2s, box-shadow 0.2s; + transition: + transform 0.2s, + box-shadow 0.2s; } .stat-card:hover { diff --git a/src/pages/AdminPage.tsx b/src/pages/AdminPage.tsx index fd8c56e..15036b7 100644 --- a/src/pages/AdminPage.tsx +++ b/src/pages/AdminPage.tsx @@ -34,7 +34,7 @@ export default function AdminPage() { const loadStats = async () => { if (!isAuthenticated) return; - + setIsLoading(true); try { const data = await getAdminStats(); @@ -116,14 +116,16 @@ export default function AdminPage() {
🔤
- {formatTokens(stats.totalTokens)} + + {formatTokens(stats.totalTokens)} + Всего токенов

Статистика по историям

- +
@@ -137,7 +139,9 @@ export default function AdminPage() { {stats.stories.length === 0 ? ( - + ) : ( stats.stories.map((story) => ( @@ -155,8 +159,9 @@ export default function AdminPage() {

- 💡 Примечание: Токены рассчитаны приблизительно (1 токен ≈ 3 символа для русского текста). - Реальное потребление может отличаться. + 💡 Примечание: Токены рассчитаны приблизительно (1 + токен ≈ 3 символа для русского текста). Реальное потребление может + отличаться.

diff --git a/src/pages/GamePage.css b/src/pages/GamePage.css index 8f5e765..766082e 100644 --- a/src/pages/GamePage.css +++ b/src/pages/GamePage.css @@ -288,6 +288,7 @@ .input-container { display: flex; + align-items: center; gap: 0.75rem; padding: 1rem 1.5rem 1.5rem; background: #131313; @@ -296,6 +297,8 @@ .input-container textarea { flex: 1; + min-height: 50px; + max-height: 120px; padding: 0.875rem 1rem; background: #1a1a1a; border: 2px solid #333; @@ -318,6 +321,7 @@ .send-btn { width: 50px; + min-width: 50px; height: 50px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; @@ -328,7 +332,7 @@ transition: transform 0.2s, opacity 0.2s; - align-self: flex-end; + flex-shrink: 0; } .send-btn:hover:not(:disabled) { diff --git a/src/pages/GamePage.tsx b/src/pages/GamePage.tsx index b91143e..846dfa5 100644 --- a/src/pages/GamePage.tsx +++ b/src/pages/GamePage.tsx @@ -1,9 +1,5 @@ import { useState, useEffect, useRef } from "react"; -import { - useParams, - Link, - useSearchParams, -} from "react-router-dom"; +import { useParams, Link, useSearchParams } from "react-router-dom"; import ReactMarkdown from "react-markdown"; import { useAuth } from "../contexts/AuthContext"; import { @@ -44,61 +40,6 @@ function formatTokens(tokens: number): string { 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(); @@ -409,9 +350,6 @@ export default function GamePage() { 🎟️ {formatTokens(estimateTokens(session?.messages || []))} - - 📍 {detectLocation(session?.messages || [])} -
Нет историй + Нет историй +