fixed reasoning
This commit is contained in:
+6
-1
@@ -2,7 +2,12 @@
|
|||||||
<html lang="ru">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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>
|
<title>Home AI Assistant</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+37
-1
@@ -1,7 +1,9 @@
|
|||||||
.app {
|
.app {
|
||||||
min-height: 100vh;
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header {
|
.app-header {
|
||||||
@@ -39,4 +41,38 @@
|
|||||||
.app-main {
|
.app-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NavLink, Route, Routes } from "react-router-dom";
|
import { NavLink, Route, Routes } from "react-router-dom";
|
||||||
import PomodoroWidget from "./components/PomodoroWidget";
|
import PomodoroWidget from "./components/PomodoroWidget";
|
||||||
import { PomodoroProvider } from "./context/PomodoroContext";
|
import { PomodoroProvider } from "./context/PomodoroContext";
|
||||||
|
import { useVisualViewportHeight } from "./hooks/useVisualViewport";
|
||||||
import Character from "./pages/Character";
|
import Character from "./pages/Character";
|
||||||
import Chat from "./pages/Chat";
|
import Chat from "./pages/Chat";
|
||||||
import Fitness from "./pages/Fitness";
|
import Fitness from "./pages/Fitness";
|
||||||
@@ -10,6 +11,8 @@ import Pomodoro from "./pages/Pomodoro";
|
|||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
useVisualViewportHeight();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PomodoroProvider>
|
<PomodoroProvider>
|
||||||
<div className="app">
|
<div className="app">
|
||||||
|
|||||||
@@ -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
@@ -11,9 +11,20 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
min-height: 100%;
|
||||||
|
height: var(--app-height, 100dvh);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
.chat-layout {
|
.chat-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 280px 1fr;
|
grid-template-columns: 280px 1fr;
|
||||||
height: calc(100vh - 65px);
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-sidebar {
|
.chat-sidebar {
|
||||||
@@ -12,6 +14,7 @@
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
background: #12151c;
|
background: #12151c;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-btn {
|
.primary-btn {
|
||||||
@@ -30,6 +33,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-list li {
|
.session-list li {
|
||||||
@@ -66,6 +70,13 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #0f1115;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-mobile-bar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-empty {
|
.chat-empty {
|
||||||
@@ -75,11 +86,21 @@
|
|||||||
|
|
||||||
.messages {
|
.messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 1.5rem;
|
overflow-x: hidden;
|
||||||
|
padding: 1rem 1rem 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
touch-action: pan-y;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-bottom-anchor {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
@@ -176,27 +197,33 @@
|
|||||||
.chat-input {
|
.chat-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
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;
|
border-top: 1px solid #2a2f3a;
|
||||||
|
background: #0f1115;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input textarea {
|
.chat-input textarea {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
resize: none;
|
resize: none;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid #2f3748;
|
border: 1px solid #2f3748;
|
||||||
background: #12151c;
|
background: #12151c;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input button {
|
.chat-input button {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
background: #4f7cff;
|
background: #4f7cff;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 0.65rem 1.2rem;
|
padding: 0.65rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input button:disabled {
|
.chat-input button:disabled {
|
||||||
@@ -212,4 +239,53 @@
|
|||||||
.chat-sidebar {
|
.chat-sidebar {
|
||||||
display: none;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ReactMarkdown from "react-markdown";
|
||||||
import { api, ChatMessage, ChatSession } from "../api/client";
|
import { api, ChatMessage, ChatSession } from "../api/client";
|
||||||
import PomodoroWidget from "../components/PomodoroWidget";
|
import PomodoroWidget from "../components/PomodoroWidget";
|
||||||
@@ -42,7 +42,9 @@ export default function Chat() {
|
|||||||
"thinking",
|
"thinking",
|
||||||
);
|
);
|
||||||
const [liveNotices, setLiveNotices] = useState<string[]>([]);
|
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 { status: pomodoroStatus, refresh: refreshPomodoro } = usePomodoro();
|
||||||
const [lastNotifySeq, setLastNotifySeq] = useState(0);
|
const [lastNotifySeq, setLastNotifySeq] = useState(0);
|
||||||
|
|
||||||
@@ -69,9 +71,33 @@ export default function Chat() {
|
|||||||
}
|
}
|
||||||
}, [activeId]);
|
}, [activeId]);
|
||||||
|
|
||||||
|
const scrollToBottom = useCallback((smooth = false) => {
|
||||||
|
const container = messagesRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
container.scrollTo({
|
||||||
|
top: container.scrollHeight,
|
||||||
|
behavior: smooth ? "smooth" : "auto",
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
if (scrollRafRef.current !== null) {
|
||||||
}, [messages, streaming, liveNotices, loading]);
|
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 waitingForStream = loading && !streaming;
|
||||||
const pendingLabel =
|
const pendingLabel =
|
||||||
@@ -119,6 +145,7 @@ export default function Chat() {
|
|||||||
|
|
||||||
const text = input.trim();
|
const text = input.trim();
|
||||||
setInput("");
|
setInput("");
|
||||||
|
dismissKeyboard();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setStreaming("");
|
setStreaming("");
|
||||||
setPendingPhase("thinking");
|
setPendingPhase("thinking");
|
||||||
@@ -215,7 +242,29 @@ export default function Chat() {
|
|||||||
<div className="chat-empty">Создайте новый чат, чтобы начать</div>
|
<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) => (
|
{visibleMessages.map((msg) => (
|
||||||
<div key={msg.id} className={`message message-${msg.role}`}>
|
<div key={msg.id} className={`message message-${msg.role}`}>
|
||||||
<div className="message-role">{roleLabel(msg.role, msg.content)}</div>
|
<div className="message-role">{roleLabel(msg.role, msg.content)}</div>
|
||||||
@@ -260,15 +309,18 @@ export default function Chat() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div ref={bottomRef} />
|
<div className="messages-bottom-anchor" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form className="chat-input" onSubmit={handleSubmit}>
|
<form className="chat-input" onSubmit={handleSubmit}>
|
||||||
<textarea
|
<textarea
|
||||||
|
ref={inputRef}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
placeholder="Напишите сообщение..."
|
placeholder="Напишите сообщение..."
|
||||||
rows={2}
|
rows={2}
|
||||||
|
enterKeyHint="send"
|
||||||
|
autoComplete="off"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 240px 1fr;
|
grid-template-columns: 240px 1fr;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
height: calc(100vh - 80px);
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user