feat: AIOps智能助手支持停止生成(AbortController中止流式,保留部分内容+已停止提示)
This commit is contained in:
1 parent
5d19878b28
commit
ca8ebb1634
1 file changed
+53
-14
@@ -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<Message[]>([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
// 用于中止流式生成(点击"停止"按钮时 abort) // Logan.Li
|
||||
const abortRef = useRef<AbortController | null>(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() {
|
||||
<span className="inline-block w-2 h-4 ml-0.5 bg-purple-400 animate-pulse align-middle" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">{fmtTime(message.timestamp)}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">
|
||||
{fmtTime(message.timestamp)}
|
||||
{message.isStopped && <span className="ml-2 text-slate-600">(已停止)</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -508,6 +536,17 @@ export function AIOpsAssistant() {
|
||||
<Button className="bg-purple-600 hover:bg-purple-700" onClick={handleSend} disabled={isLoading || !input.trim()}>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
{/* 流式生成中显示\"停止生成\"按钮,中止当前请求 // Logan.Li */}
|
||||
{isLoading && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-red-500/40 text-red-400 hover:bg-red-500/10"
|
||||
onClick={() => abortRef.current?.abort()}
|
||||
title="停止生成"
|
||||
>
|
||||
<Square className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in new issue
Block a user