From 827f9016cdc6ead783465e1176d725f7ae386a56 Mon Sep 17 00:00:00 2001 From: grigo Date: Wed, 10 Jun 2026 15:09:36 +0300 Subject: [PATCH] fixed reasoning --- frontend/index.html | 7 ++- frontend/src/App.css | 38 ++++++++++- frontend/src/App.tsx | 3 + frontend/src/hooks/useVisualViewport.ts | 21 +++++++ frontend/src/index.css | 13 +++- frontend/src/pages/Chat.css | 84 +++++++++++++++++++++++-- frontend/src/pages/Chat.tsx | 64 +++++++++++++++++-- frontend/src/pages/Shopping.css | 4 +- 8 files changed, 220 insertions(+), 14 deletions(-) create mode 100644 frontend/src/hooks/useVisualViewport.ts diff --git a/frontend/index.html b/frontend/index.html index b0d097d..8eccb90 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,12 @@ - + + + Home AI Assistant diff --git a/frontend/src/App.css b/frontend/src/App.css index 57487f4..044cd00 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,7 +1,9 @@ .app { - min-height: 100vh; + height: 100%; + min-height: 0; display: flex; flex-direction: column; + overflow: hidden; } .app-header { @@ -39,4 +41,38 @@ .app-main { flex: 1; min-height: 0; + overflow: hidden; +} + +@media (max-width: 768px) { + .app-header { + padding: 0.55rem 0.75rem; + gap: 0.5rem; + flex-shrink: 0; + } + + .app-header h1 { + display: none; + } + + .app-header nav { + flex: 1; + overflow-x: auto; + flex-wrap: nowrap; + gap: 0.35rem; + padding-bottom: 0.1rem; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + } + + .app-header nav::-webkit-scrollbar { + display: none; + } + + .app-header nav a { + padding: 0.4rem 0.65rem; + font-size: 0.85rem; + white-space: nowrap; + flex-shrink: 0; + } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c697109..4ca5794 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import { NavLink, Route, Routes } from "react-router-dom"; import PomodoroWidget from "./components/PomodoroWidget"; import { PomodoroProvider } from "./context/PomodoroContext"; +import { useVisualViewportHeight } from "./hooks/useVisualViewport"; import Character from "./pages/Character"; import Chat from "./pages/Chat"; import Fitness from "./pages/Fitness"; @@ -10,6 +11,8 @@ import Pomodoro from "./pages/Pomodoro"; import "./App.css"; export default function App() { + useVisualViewportHeight(); + return (
diff --git a/frontend/src/hooks/useVisualViewport.ts b/frontend/src/hooks/useVisualViewport.ts new file mode 100644 index 0000000..03e1cb8 --- /dev/null +++ b/frontend/src/hooks/useVisualViewport.ts @@ -0,0 +1,21 @@ +import { useEffect } from "react"; + +export function useVisualViewportHeight() { + useEffect(() => { + const update = () => { + const height = window.visualViewport?.height ?? window.innerHeight; + document.documentElement.style.setProperty("--app-height", `${height}px`); + }; + + update(); + window.visualViewport?.addEventListener("resize", update); + window.visualViewport?.addEventListener("scroll", update); + window.addEventListener("resize", update); + + return () => { + window.visualViewport?.removeEventListener("resize", update); + window.visualViewport?.removeEventListener("scroll", update); + window.removeEventListener("resize", update); + }; + }, []); +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 75f1399..97cebee 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -11,9 +11,20 @@ box-sizing: border-box; } +html { + height: 100%; +} + body { margin: 0; - min-height: 100vh; + min-height: 100%; + height: var(--app-height, 100dvh); + overflow: hidden; +} + +#root { + height: 100%; + overflow: hidden; } button, diff --git a/frontend/src/pages/Chat.css b/frontend/src/pages/Chat.css index c7d8e39..36bd0bc 100644 --- a/frontend/src/pages/Chat.css +++ b/frontend/src/pages/Chat.css @@ -1,7 +1,9 @@ .chat-layout { display: grid; grid-template-columns: 280px 1fr; - height: calc(100vh - 65px); + height: 100%; + min-height: 0; + overflow: hidden; } .chat-sidebar { @@ -12,6 +14,7 @@ gap: 1rem; background: #12151c; min-height: 0; + overflow: hidden; } .primary-btn { @@ -30,6 +33,7 @@ display: flex; flex-direction: column; gap: 0.35rem; + min-height: 0; } .session-list li { @@ -66,6 +70,13 @@ display: flex; flex-direction: column; min-height: 0; + height: 100%; + overflow: hidden; + background: #0f1115; +} + +.chat-mobile-bar { + display: none; } .chat-empty { @@ -75,11 +86,21 @@ .messages { flex: 1; + min-height: 0; overflow-y: auto; - padding: 1.5rem; + overflow-x: hidden; + padding: 1rem 1rem 0.5rem; display: flex; flex-direction: column; gap: 1rem; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + touch-action: pan-y; +} + +.messages-bottom-anchor { + flex-shrink: 0; + height: 1px; } .message { @@ -176,27 +197,33 @@ .chat-input { display: flex; gap: 0.75rem; - padding: 1rem 1.5rem; + flex-shrink: 0; + padding: 0.75rem 1rem; + padding-bottom: max(0.75rem, env(safe-area-inset-bottom)); border-top: 1px solid #2a2f3a; + background: #0f1115; } .chat-input textarea { flex: 1; + min-width: 0; resize: none; border-radius: 10px; border: 1px solid #2f3748; background: #12151c; color: inherit; padding: 0.75rem 1rem; + font-size: 16px; } .chat-input button { align-self: flex-end; + flex-shrink: 0; background: #4f7cff; color: white; border: none; border-radius: 8px; - padding: 0.65rem 1.2rem; + padding: 0.65rem 1rem; } .chat-input button:disabled { @@ -212,4 +239,53 @@ .chat-sidebar { display: none; } + + .chat-mobile-bar { + display: flex; + align-items: center; + gap: 0.5rem; + flex-shrink: 0; + padding: 0.55rem 0.75rem; + border-bottom: 1px solid #2a2f3a; + background: #12151c; + } + + .chat-session-select { + flex: 1; + min-width: 0; + border-radius: 8px; + border: 1px solid #2f3748; + background: #0f1115; + color: inherit; + padding: 0.5rem 0.65rem; + font-size: 16px; + } + + .chat-mobile-new { + flex-shrink: 0; + border: none; + border-radius: 8px; + background: #4f7cff; + color: #fff; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + } + + .messages { + padding: 0.75rem 0.75rem 0.35rem; + } + + .message { + max-width: 92%; + } + + .chat-input { + padding: 0.65rem 0.75rem; + padding-bottom: max(0.65rem, env(safe-area-inset-bottom)); + gap: 0.5rem; + } + + .chat-input button { + padding: 0.65rem 0.85rem; + } } diff --git a/frontend/src/pages/Chat.tsx b/frontend/src/pages/Chat.tsx index ef7e985..7a264a9 100644 --- a/frontend/src/pages/Chat.tsx +++ b/frontend/src/pages/Chat.tsx @@ -1,4 +1,4 @@ -import { FormEvent, useEffect, useRef, useState } from "react"; +import { FormEvent, useCallback, useEffect, useRef, useState } from "react"; import ReactMarkdown from "react-markdown"; import { api, ChatMessage, ChatSession } from "../api/client"; import PomodoroWidget from "../components/PomodoroWidget"; @@ -42,7 +42,9 @@ export default function Chat() { "thinking", ); const [liveNotices, setLiveNotices] = useState([]); - const bottomRef = useRef(null); + const messagesRef = useRef(null); + const inputRef = useRef(null); + const scrollRafRef = useRef(null); const { status: pomodoroStatus, refresh: refreshPomodoro } = usePomodoro(); const [lastNotifySeq, setLastNotifySeq] = useState(0); @@ -69,9 +71,33 @@ export default function Chat() { } }, [activeId]); + const scrollToBottom = useCallback((smooth = false) => { + const container = messagesRef.current; + if (!container) return; + container.scrollTo({ + top: container.scrollHeight, + behavior: smooth ? "smooth" : "auto", + }); + }, []); + useEffect(() => { - bottomRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages, streaming, liveNotices, loading]); + if (scrollRafRef.current !== null) { + cancelAnimationFrame(scrollRafRef.current); + } + scrollRafRef.current = requestAnimationFrame(() => { + scrollToBottom(!streaming); + scrollRafRef.current = null; + }); + return () => { + if (scrollRafRef.current !== null) { + cancelAnimationFrame(scrollRafRef.current); + } + }; + }, [messages, streaming, liveNotices, loading, scrollToBottom]); + + const dismissKeyboard = useCallback(() => { + inputRef.current?.blur(); + }, []); const waitingForStream = loading && !streaming; const pendingLabel = @@ -119,6 +145,7 @@ export default function Chat() { const text = input.trim(); setInput(""); + dismissKeyboard(); setLoading(true); setStreaming(""); setPendingPhase("thinking"); @@ -215,7 +242,29 @@ export default function Chat() {
Создайте новый чат, чтобы начать
) : ( <> -
+
+ + +
+ +
{visibleMessages.map((msg) => (
{roleLabel(msg.role, msg.content)}
@@ -260,15 +309,18 @@ export default function Chat() {
)} -
+