diff --git a/backend/app/chat/service.py b/backend/app/chat/service.py index 3a0cda2..e91f4cc 100644 --- a/backend/app/chat/service.py +++ b/backend/app/chat/service.py @@ -243,9 +243,6 @@ class ChatService: {"name": fn["name"], "result": json.loads(tool_result)} ) - if notices: - return "\n\n".join(notices), notices, pomodoro_events - followup = await self.llm.complete( messages, tools=None, @@ -384,6 +381,7 @@ class ChatService: {"name": fn["name"], "result": json.loads(result)}, ) + yield self._sse("status", {"phase": "tools"}) for notice in round_notices: yield self._sse("notice", {"content": notice}) @@ -394,9 +392,6 @@ class ChatService: final_content = "".join(streamed_reply_parts).strip() if not final_content and reasoning: final_content = reasoning.strip() - if not final_content and all_tool_notices: - final_content = "\n\n".join(all_tool_notices) - yield self._sse("token", {"content": final_content}) if not final_content and tools_executed: retry = await self.llm.complete( messages, @@ -407,6 +402,7 @@ class ChatService: final_content = (retry.get("content") or "").strip() if final_content: yield self._sse("token", {"content": final_content}) + # Notices уже в чате как role=notice — не дублируем в assistant. if not final_content: final_content, fb_notices, fb_pomodoro = await self._fallback_complete( messages, session_id diff --git a/frontend/src/pages/Chat.tsx b/frontend/src/pages/Chat.tsx index a042c3f..f5376a9 100644 --- a/frontend/src/pages/Chat.tsx +++ b/frontend/src/pages/Chat.tsx @@ -44,9 +44,9 @@ export default function Chat() { const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [streaming, setStreaming] = useState(""); - const [pendingPhase, setPendingPhase] = useState<"thinking" | "preparing" | "generating">( - "thinking", - ); + const [pendingPhase, setPendingPhase] = useState< + "thinking" | "preparing" | "generating" | "tools" + >("thinking"); const [liveNotices, setLiveNotices] = useState([]); const [chatError, setChatError] = useState(null); const messagesRef = useRef(null); @@ -54,6 +54,7 @@ export default function Chat() { const scrollRafRef = useRef(null); const { status: pomodoroStatus, refresh: refreshPomodoro } = usePomodoro(); const [lastNotifySeq, setLastNotifySeq] = useState(0); + const pendingHistoryReload = useRef(false); const loadSessions = async () => { const data = await api.listSessions(); @@ -108,22 +109,27 @@ export default function Chat() { const waitingForStream = loading && !streaming; const pendingLabel = - liveNotices.length > 0 - ? "Обрабатываю…" - : pendingPhase === "preparing" - ? "Собираю контекст…" - : pendingPhase === "generating" - ? "Генерирую ответ…" - : "Думаю…"; + pendingPhase === "tools" + ? "Выполняю команды…" + : liveNotices.length > 0 + ? "Обрабатываю…" + : pendingPhase === "preparing" + ? "Собираю контекст…" + : pendingPhase === "generating" + ? "Генерирую ответ…" + : "Думаю…"; useEffect(() => { const seq = pomodoroStatus?.cycle?.chat_notify_seq ?? 0; if (seq > lastNotifySeq) { setLastNotifySeq(seq); refreshPomodoro().catch(console.error); - // Не перезагружать историю во время стрима — ломает UI и сбивает ответ. - if (activeId && !loading) { - loadMessages(activeId).catch(console.error); + if (activeId) { + if (loading) { + pendingHistoryReload.current = true; + } else { + loadMessages(activeId).catch(console.error); + } } } }, [pomodoroStatus?.cycle?.chat_notify_seq, activeId, lastNotifySeq, refreshPomodoro, loading]); @@ -178,9 +184,15 @@ export default function Chat() { if (chunk.data.phase === "generating") { setPendingPhase("generating"); } + if (chunk.data.phase === "tools") { + setPendingPhase("tools"); + assistantText = ""; + setStreaming(""); + } } if (chunk.event === "token") { assistantText += chunk.data.content; + setPendingPhase("generating"); setStreaming(assistantText); } if (chunk.event === "notice") { @@ -196,17 +208,6 @@ export default function Chat() { setStreaming(""); setLiveNotices([]); setChatError(null); - if (assistantText.trim()) { - setMessages((prev) => [ - ...prev, - { - id: Date.now(), - role: "assistant", - content: assistantText, - created_at: new Date().toISOString(), - }, - ]); - } await loadMessages(activeId); await loadSessions(); } @@ -224,6 +225,10 @@ export default function Chat() { } } finally { setLoading(false); + if (pendingHistoryReload.current && activeId) { + pendingHistoryReload.current = false; + loadMessages(activeId).catch(console.error); + } } };