This commit is contained in:
2026-06-09 11:26:28 +03:00
parent 94735fd540
commit 244935e4ac
21 changed files with 886 additions and 15 deletions
+47 -4
View File
@@ -1,8 +1,22 @@
import { FormEvent, useEffect, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import { api, ChatMessage, ChatSession } from "../api/client";
import PomodoroWidget from "../components/PomodoroWidget";
import { usePomodoro } from "../hooks/usePomodoro";
import "./Chat.css";
function shouldShowMessage(msg: ChatMessage): boolean {
if (msg.role === "tool") return false;
if (msg.role === "assistant" && !msg.content.trim()) return false;
return true;
}
function roleLabel(role: string): string {
if (role === "notice") return "таймер";
if (role === "user") return "вы";
return role;
}
export default function Chat() {
const [sessions, setSessions] = useState<ChatSession[]>([]);
const [activeId, setActiveId] = useState<number | null>(null);
@@ -10,7 +24,9 @@ export default function Chat() {
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [streaming, setStreaming] = useState("");
const [liveNotices, setLiveNotices] = useState<string[]>([]);
const bottomRef = useRef<HTMLDivElement>(null);
const { refresh: refreshPomodoro } = usePomodoro();
const loadSessions = async () => {
const data = await api.listSessions();
@@ -37,13 +53,14 @@ export default function Chat() {
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, streaming]);
}, [messages, streaming, liveNotices]);
const handleNewChat = async () => {
const session = await api.createSession();
await loadSessions();
setActiveId(session.id);
setMessages([]);
setLiveNotices([]);
};
const handleDelete = async (id: number) => {
@@ -53,6 +70,7 @@ export default function Chat() {
if (activeId === id) {
setActiveId(data[0]?.id ?? null);
setMessages([]);
setLiveNotices([]);
}
};
@@ -64,6 +82,7 @@ export default function Chat() {
setInput("");
setLoading(true);
setStreaming("");
setLiveNotices([]);
const tempUser: ChatMessage = {
id: Date.now(),
@@ -78,10 +97,18 @@ export default function Chat() {
if (chunk.event === "token") {
setStreaming((prev) => prev + chunk.data.content);
}
if (chunk.event === "notice") {
setLiveNotices((prev) => [...prev, chunk.data.content]);
refreshPomodoro();
}
if (chunk.event === "pomodoro") {
refreshPomodoro();
}
if (chunk.event === "done") {
await loadMessages(activeId);
await loadSessions();
setStreaming("");
setLiveNotices([]);
}
if (chunk.event === "error") {
throw new Error(chunk.data.message);
@@ -90,17 +117,23 @@ export default function Chat() {
} catch (err) {
console.error(err);
setStreaming("");
setLiveNotices([]);
} finally {
setLoading(false);
}
};
const visibleMessages = messages.filter(shouldShowMessage);
return (
<div className="chat-layout">
<aside className="chat-sidebar">
<button className="primary-btn" onClick={handleNewChat}>
+ Новый чат
</button>
<PomodoroWidget />
<ul className="session-list">
{sessions.map((session) => (
<li key={session.id} className={activeId === session.id ? "active" : ""}>
@@ -119,11 +152,11 @@ export default function Chat() {
) : (
<>
<div className="messages">
{messages.map((msg) => (
{visibleMessages.map((msg) => (
<div key={msg.id} className={`message message-${msg.role}`}>
<div className="message-role">{msg.role}</div>
<div className="message-role">{roleLabel(msg.role)}</div>
<div className="message-content">
{msg.role === "assistant" ? (
{msg.role === "assistant" || msg.role === "notice" ? (
<ReactMarkdown>{msg.content}</ReactMarkdown>
) : (
msg.content
@@ -131,6 +164,16 @@ export default function Chat() {
</div>
</div>
))}
{liveNotices.map((notice, idx) => (
<div key={`notice-${idx}`} className="message message-notice">
<div className="message-role">таймер</div>
<div className="message-content">
<ReactMarkdown>{notice}</ReactMarkdown>
</div>
</div>
))}
{streaming && (
<div className="message message-assistant">
<div className="message-role">assistant</div>