fixed reasoning

This commit is contained in:
2026-06-10 15:09:36 +03:00
parent e9762d7921
commit 827f9016cd
8 changed files with 220 additions and 14 deletions
+6 -1
View File
@@ -2,7 +2,12 @@
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"
/>
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<title>Home AI Assistant</title>
</head>
<body>
+37 -1
View File
@@ -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;
}
}
+3
View File
@@ -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 (
<PomodoroProvider>
<div className="app">
+21
View File
@@ -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);
};
}, []);
}
+12 -1
View File
@@ -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,
+80 -4
View File
@@ -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;
}
}
+58 -6
View File
@@ -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<string[]>([]);
const bottomRef = useRef<HTMLDivElement>(null);
const messagesRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const scrollRafRef = useRef<number | null>(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() {
<div className="chat-empty">Создайте новый чат, чтобы начать</div>
) : (
<>
<div className="messages">
<div className="chat-mobile-bar">
<select
className="chat-session-select"
value={activeId}
onChange={(e) => setActiveId(Number(e.target.value))}
aria-label="Выбор чата"
>
{sessions.map((session) => (
<option key={session.id} value={session.id}>
{session.title}
</option>
))}
</select>
<button type="button" className="chat-mobile-new" onClick={handleNewChat}>
+ Новый
</button>
</div>
<div
className="messages"
ref={messagesRef}
onClick={dismissKeyboard}
>
{visibleMessages.map((msg) => (
<div key={msg.id} className={`message message-${msg.role}`}>
<div className="message-role">{roleLabel(msg.role, msg.content)}</div>
@@ -260,15 +309,18 @@ export default function Chat() {
</div>
</div>
)}
<div ref={bottomRef} />
<div className="messages-bottom-anchor" aria-hidden="true" />
</div>
<form className="chat-input" onSubmit={handleSubmit}>
<textarea
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Напишите сообщение..."
rows={2}
enterKeyHint="send"
autoComplete="off"
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
+3 -1
View File
@@ -2,7 +2,9 @@
display: grid;
grid-template-columns: 240px 1fr;
gap: 1rem;
height: calc(100vh - 80px);
height: 100%;
min-height: 0;
overflow: auto;
padding: 1rem;
}