diff --git a/frontend/src/components/aiops/AIOpsAssistant.tsx b/frontend/src/components/aiops/AIOpsAssistant.tsx index 3be4f7d..d72f8e0 100644 --- a/frontend/src/components/aiops/AIOpsAssistant.tsx +++ b/frontend/src/components/aiops/AIOpsAssistant.tsx @@ -40,6 +40,7 @@ import { Loader2, CheckCircle2, Wifi, + Square, } from 'lucide-react'; // LLM 提供商列表(OpenAI 兼容接口) // Logan.Li @@ -133,6 +134,7 @@ interface Message { content: string; timestamp: string; isError?: boolean; + isStopped?: boolean; } // 格式化时间为 HH:MM // Logan.Li @@ -145,6 +147,10 @@ export function AIOpsAssistant() { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); + // 用于中止流式生成(点击"停止"按钮时 abort) // Logan.Li + const abortRef = useRef(null); + // 组件卸载时中止进行中的流式请求,避免 setState on unmounted // Logan.Li + useEffect(() => () => abortRef.current?.abort(), []); const [isConfigOpen, setIsConfigOpen] = useState(false); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); @@ -251,6 +257,9 @@ export function AIOpsAssistant() { ]; setMessages(newMessages); setIsLoading(true); + // 创建 AbortController 以支持\"停止生成\" // Logan.Li + const controller = new AbortController(); + abortRef.current = controller; // 用于在收到第一个 chunk 前显示"思考中"占位 let firstChunkArrived = false; try { @@ -266,22 +275,38 @@ export function AIOpsAssistant() { m.id === assistantId ? { ...m, content: m.content + delta } : m, ), ); - }); + }, controller.signal); } catch (e) { - const errMsg = e instanceof Error ? e.message : '对话失败'; - setMessages((prev) => - prev.map((m) => - m.id === assistantId - ? { - ...m, - content: firstChunkArrived ? m.content + `\n\n⚠️ ${errMsg}` : `⚠️ ${errMsg}`, - isError: !firstChunkArrived, - } - : m, - ), - ); + // 用户主动中止:保留已生成的部分内容,不当作错误 // Logan.Li + if (controller.signal.aborted) { + setMessages((prev) => + prev.map((m) => + m.id === assistantId + ? { + ...m, + content: m.content || '(已停止生成)', + isStopped: true, + } + : m, + ), + ); + } else { + const errMsg = e instanceof Error ? e.message : '对话失败'; + setMessages((prev) => + prev.map((m) => + m.id === assistantId + ? { + ...m, + content: firstChunkArrived ? m.content + `\n\n⚠️ ${errMsg}` : `⚠️ ${errMsg}`, + isError: !firstChunkArrived, + } + : m, + ), + ); + } } finally { setIsLoading(false); + abortRef.current = null; } }; @@ -485,7 +510,10 @@ export function AIOpsAssistant() { )} -
{fmtTime(message.timestamp)}
+
+ {fmtTime(message.timestamp)} + {message.isStopped && (已停止)} +
); @@ -508,6 +536,17 @@ export function AIOpsAssistant() { + {/* 流式生成中显示\"停止生成\"按钮,中止当前请求 // Logan.Li */} + {isLoading && ( + + )}