diff --git a/Jenkinsfile b/Jenkinsfile index 28fd7d9..2915aa0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -10,7 +10,7 @@ pipeline { agent { - label 'Мастер' + label 'мастер' } options { @@ -33,7 +33,7 @@ pipeline { ) string( name: 'BACKEND_HEALTH_URL', - defaultValue: 'http://127.0.0.1:8080/api/v1/health', + defaultValue: 'http://127.0.0.1:8202/api/v1/health', description: 'Проверка после деплоя' ) booleanParam( diff --git a/backend/app/chat/service.py b/backend/app/chat/service.py index cdc4dec..4042418 100644 --- a/backend/app/chat/service.py +++ b/backend/app/chat/service.py @@ -292,10 +292,14 @@ class ChatService: reasoning_details: list[Any] | None = None finish_reason = "" + # После tool-раунда стримим вживую; до tools — буфер (иначе текст «переписывает» notice). + stream_live = tools_executed > 0 + async for event in self.llm.stream_chat(messages, tools=TOOL_DEFINITIONS): if event["type"] == "content": content_parts.append(event["content"]) - yield self._sse("token", {"content": event["content"]}) + if stream_live: + yield self._sse("token", {"content": event["content"]}) elif event["type"] == "reasoning": reasoning = event.get("reasoning", "") or reasoning if event.get("reasoning_details"): @@ -390,8 +394,12 @@ class ChatService: continue + if content_parts and not stream_live: + for part in content_parts: + yield self._sse("token", {"content": part}) + final_content = "".join(content_parts).strip() - if not final_content and streamed_reply_parts: + if not final_content and streamed_reply_parts and tools_executed == 0: final_content = "".join(streamed_reply_parts).strip() if not final_content and reasoning: final_content = reasoning.strip() diff --git a/frontend/src/pages/Chat.tsx b/frontend/src/pages/Chat.tsx index c9c16bf..66e8eab 100644 --- a/frontend/src/pages/Chat.tsx +++ b/frontend/src/pages/Chat.tsx @@ -48,8 +48,8 @@ export default function Chat() { const [pendingPhase, setPendingPhase] = useState< "thinking" | "preparing" | "generating" | "tools" >("thinking"); - const [liveNotices, setLiveNotices] = useState([]); const [chatError, setChatError] = useState(null); + const tempMessageId = useRef(0); const messagesRef = useRef(null); const inputRef = useRef(null); const scrollRafRef = useRef(null); @@ -104,23 +104,38 @@ export default function Chat() { cancelAnimationFrame(scrollRafRef.current); } }; - }, [messages, streaming, liveNotices, loading, chatError, scrollToBottom]); + }, [messages, streaming, loading, chatError, scrollToBottom]); const dismissKeyboard = useCallback(() => { inputRef.current?.blur(); }, []); const waitingForStream = loading && !streaming; + const nextTempId = () => { + tempMessageId.current -= 1; + return tempMessageId.current; + }; + + const appendNotice = useCallback((content: string) => { + setMessages((prev) => [ + ...prev, + { + id: nextTempId(), + role: "notice", + content, + created_at: new Date().toISOString(), + }, + ]); + }, []); + const pendingLabel = pendingPhase === "tools" ? "Выполняю команды…" - : liveNotices.length > 0 - ? "Обрабатываю…" - : pendingPhase === "preparing" - ? "Собираю контекст…" - : pendingPhase === "generating" - ? "Генерирую ответ…" - : "Думаю…"; + : pendingPhase === "preparing" + ? "Собираю контекст…" + : pendingPhase === "generating" + ? "Генерирую ответ…" + : "Думаю…"; useEffect(() => { const seq = pomodoroStatus?.cycle?.chat_notify_seq ?? 0; @@ -177,7 +192,6 @@ export default function Chat() { await loadSessions(); setActiveId(session.id); setMessages([]); - setLiveNotices([]); }; const handleDelete = async (id: number) => { @@ -187,7 +201,6 @@ export default function Chat() { if (activeId === id) { setActiveId(data[0]?.id ?? null); setMessages([]); - setLiveNotices([]); } }; @@ -201,11 +214,10 @@ export default function Chat() { setLoading(true); setStreaming(""); setPendingPhase("thinking"); - setLiveNotices([]); setChatError(null); const tempUser: ChatMessage = { - id: Date.now(), + id: nextTempId(), role: "user", content: text, created_at: new Date().toISOString(), @@ -234,7 +246,7 @@ export default function Chat() { setStreaming(assistantText); } if (chunk.event === "notice") { - setLiveNotices((prev) => [...prev, chunk.data.content]); + appendNotice(chunk.data.content); if (String(chunk.data.content).startsWith("⏱")) { refreshPomodoro(); } @@ -243,8 +255,19 @@ export default function Chat() { refreshPomodoro(); } if (chunk.event === "done") { + const tail = assistantText.trim(); + if (tail) { + setMessages((prev) => [ + ...prev, + { + id: nextTempId(), + role: "assistant", + content: tail, + created_at: new Date().toISOString(), + }, + ]); + } setStreaming(""); - setLiveNotices([]); setChatError(null); await loadMessages(activeId); await loadSessions(); @@ -334,15 +357,6 @@ export default function Chat() { ))} - {liveNotices.map((notice, idx) => ( -
-
{noticeLabel(notice)}
-
- {notice} -
-
- ))} - {waitingForStream && (
assistant