|
|
@@ -1,6 +1,11 @@
|
|
|
-import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
import type { FC } from "react";
|
|
|
import { traceApi } from "../../api/traceApi";
|
|
|
+import type {
|
|
|
+ ToolApprovalBatchV1,
|
|
|
+ ToolApprovalDecision,
|
|
|
+ ToolApprovalDecisionInput,
|
|
|
+} from "../../api/traceApi";
|
|
|
import styles from "./AgentControlPanel.module.css";
|
|
|
|
|
|
interface LogLine {
|
|
|
@@ -22,6 +27,9 @@ interface BackendLogEntry {
|
|
|
message: string;
|
|
|
}
|
|
|
|
|
|
+type ApprovalDecisions = Record<string, ToolApprovalDecision | undefined>;
|
|
|
+type ApprovalDrafts = Record<string, Record<string, string>>;
|
|
|
+
|
|
|
// WebSocket trace 事件类型
|
|
|
interface TraceEvent {
|
|
|
event?: string;
|
|
|
@@ -58,6 +66,19 @@ function now() {
|
|
|
return new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
|
}
|
|
|
|
|
|
+function formatJson(value: unknown): string {
|
|
|
+ if (value === undefined) return "null";
|
|
|
+ return JSON.stringify(value, null, 2);
|
|
|
+}
|
|
|
+
|
|
|
+function errorMessage(error: unknown): string {
|
|
|
+ if (typeof error === "object" && error !== null) {
|
|
|
+ const response = (error as { response?: { data?: { detail?: unknown } } }).response;
|
|
|
+ if (typeof response?.data?.detail === "string") return response.data.detail;
|
|
|
+ }
|
|
|
+ return error instanceof Error ? error.message : "工具审批请求失败";
|
|
|
+}
|
|
|
+
|
|
|
const API_BASE =
|
|
|
(typeof window !== "undefined"
|
|
|
? (window as unknown as { CONFIG?: { API_BASE_URL?: string } }).CONFIG?.API_BASE_URL
|
|
|
@@ -88,6 +109,12 @@ export const AgentControlPanel: FC<AgentControlPanelProps> = ({
|
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
const [isReflecting, setIsReflecting] = useState(false);
|
|
|
const [isCompacting, setIsCompacting] = useState(false);
|
|
|
+ const [approvalBatch, setApprovalBatch] = useState<ToolApprovalBatchV1 | null>(null);
|
|
|
+ const [approvalDecisions, setApprovalDecisions] = useState<ApprovalDecisions>({});
|
|
|
+ const [approvalDrafts, setApprovalDrafts] = useState<ApprovalDrafts>({});
|
|
|
+ const [isApprovalLoading, setIsApprovalLoading] = useState(false);
|
|
|
+ const [isApprovalSubmitting, setIsApprovalSubmitting] = useState(false);
|
|
|
+ const [approvalError, setApprovalError] = useState<string | null>(null);
|
|
|
const logsEndRef = useRef<HTMLDivElement>(null);
|
|
|
const backendLogsEndRef = useRef<HTMLDivElement>(null);
|
|
|
const wsRef = useRef<WebSocket | null>(null);
|
|
|
@@ -134,6 +161,8 @@ export const AgentControlPanel: FC<AgentControlPanelProps> = ({
|
|
|
|
|
|
if (data.status === "running") {
|
|
|
appendLog({ time: now(), type: "system", text: "执行已恢复运行。" });
|
|
|
+ } else if (data.status === "waiting_confirmation") {
|
|
|
+ appendLog({ time: now(), type: "warn", text: "工具调用已暂停,等待整批审批。" });
|
|
|
} else if (data.status === "stopped") {
|
|
|
appendLog({ time: now(), type: "warn", text: "执行已挂起,等待人工干预..." });
|
|
|
} else if (data.status === "completed") {
|
|
|
@@ -182,11 +211,12 @@ export const AgentControlPanel: FC<AgentControlPanelProps> = ({
|
|
|
|
|
|
// 初始加载 trace 状态(作为备用手段,主要依靠 connected 事件)
|
|
|
traceApi.fetchTraceDetail(traceId).then((detail) => {
|
|
|
+ const trace = detail.trace;
|
|
|
// 仅在状态未被 WebSocket 初始化时才覆盖
|
|
|
- setTraceStatus(s => s === "unknown" ? (detail.status || "unknown") : s);
|
|
|
+ setTraceStatus(s => s === "unknown" ? (trace.status || "unknown") : s);
|
|
|
setIsStatusLoaded(true);
|
|
|
- setTotalCost(detail.total_cost ?? 0);
|
|
|
- setTotalSteps(detail.total_messages ?? 0);
|
|
|
+ setTotalCost(trace.total_cost ?? 0);
|
|
|
+ setTotalSteps(trace.total_messages ?? 0);
|
|
|
}).catch(() => { });
|
|
|
|
|
|
// --- 新增:全局后端日志监听 ---
|
|
|
@@ -209,6 +239,72 @@ export const AgentControlPanel: FC<AgentControlPanelProps> = ({
|
|
|
};
|
|
|
}, [traceId, appendLog]);
|
|
|
|
|
|
+ // Trace 切换时不保留上一批工具的本地决定。
|
|
|
+ useEffect(() => {
|
|
|
+ setApprovalBatch(null);
|
|
|
+ setApprovalDecisions({});
|
|
|
+ setApprovalDrafts({});
|
|
|
+ setApprovalError(null);
|
|
|
+ setIsApprovalLoading(false);
|
|
|
+ setIsApprovalSubmitting(false);
|
|
|
+ }, [traceId]);
|
|
|
+
|
|
|
+ // waiting_confirmation 可能比批次读取稍早到达,因此在空结果时短轮询。
|
|
|
+ useEffect(() => {
|
|
|
+ if (!traceId || traceStatus !== "waiting_confirmation" || approvalBatch) return;
|
|
|
+
|
|
|
+ let active = true;
|
|
|
+ let retryTimer: number | undefined;
|
|
|
+
|
|
|
+ const loadApproval = async () => {
|
|
|
+ setIsApprovalLoading(true);
|
|
|
+ try {
|
|
|
+ const response = await traceApi.fetchPendingToolApproval(traceId);
|
|
|
+ if (!active) return;
|
|
|
+ setApprovalError(null);
|
|
|
+ if (response.batch) {
|
|
|
+ setApprovalBatch(response.batch);
|
|
|
+ } else {
|
|
|
+ retryTimer = window.setTimeout(() => void loadApproval(), 1200);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ if (!active) return;
|
|
|
+ setApprovalError(errorMessage(error));
|
|
|
+ retryTimer = window.setTimeout(() => void loadApproval(), 2000);
|
|
|
+ } finally {
|
|
|
+ if (active) setIsApprovalLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ void loadApproval();
|
|
|
+ return () => {
|
|
|
+ active = false;
|
|
|
+ if (retryTimer !== undefined) window.clearTimeout(retryTimer);
|
|
|
+ };
|
|
|
+ }, [traceId, traceStatus, approvalBatch]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (traceStatus === "unknown" || traceStatus === "waiting_confirmation") return;
|
|
|
+ setApprovalBatch(null);
|
|
|
+ setApprovalDecisions({});
|
|
|
+ setApprovalDrafts({});
|
|
|
+ setApprovalError(null);
|
|
|
+ }, [traceStatus]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (!approvalBatch) return;
|
|
|
+ const drafts: ApprovalDrafts = {};
|
|
|
+ for (const call of approvalBatch.calls) {
|
|
|
+ if (call.decision !== "pending") continue;
|
|
|
+ drafts[call.tool_call_id] = {};
|
|
|
+ for (const param of call.editable_params) {
|
|
|
+ drafts[call.tool_call_id][param] = formatJson(call.original_arguments[param]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ setApprovalDecisions({});
|
|
|
+ setApprovalDrafts(drafts);
|
|
|
+ }, [approvalBatch]);
|
|
|
+
|
|
|
// 自动滚动 (逻辑复用)
|
|
|
useEffect(() => {
|
|
|
if (!autoScrollRef.current) return;
|
|
|
@@ -226,6 +322,93 @@ export const AgentControlPanel: FC<AgentControlPanelProps> = ({
|
|
|
autoScrollRef.current = isAtBottom;
|
|
|
};
|
|
|
|
|
|
+ const pendingApprovalCalls = useMemo(
|
|
|
+ () => approvalBatch?.calls.filter((call) => call.decision === "pending") ?? [],
|
|
|
+ [approvalBatch],
|
|
|
+ );
|
|
|
+
|
|
|
+ const approvalDraftErrors = useMemo(() => {
|
|
|
+ const errors: Record<string, string> = {};
|
|
|
+ for (const call of pendingApprovalCalls) {
|
|
|
+ if (approvalDecisions[call.tool_call_id] !== "approve") continue;
|
|
|
+ for (const param of call.editable_params) {
|
|
|
+ const key = `${call.tool_call_id}:${param}`;
|
|
|
+ try {
|
|
|
+ JSON.parse(approvalDrafts[call.tool_call_id]?.[param] ?? "null");
|
|
|
+ } catch {
|
|
|
+ errors[key] = "请输入有效 JSON";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return errors;
|
|
|
+ }, [approvalDecisions, approvalDrafts, pendingApprovalCalls]);
|
|
|
+
|
|
|
+ const allApprovalDecisionsReady =
|
|
|
+ pendingApprovalCalls.length > 0 &&
|
|
|
+ pendingApprovalCalls.every((call) => Boolean(approvalDecisions[call.tool_call_id])) &&
|
|
|
+ Object.keys(approvalDraftErrors).length === 0;
|
|
|
+
|
|
|
+ const chooseApprovalDecision = (
|
|
|
+ toolCallId: string,
|
|
|
+ decision: ToolApprovalDecision,
|
|
|
+ ) => {
|
|
|
+ setApprovalDecisions((previous) => ({ ...previous, [toolCallId]: decision }));
|
|
|
+ setApprovalError(null);
|
|
|
+ };
|
|
|
+
|
|
|
+ const updateApprovalDraft = (toolCallId: string, param: string, value: string) => {
|
|
|
+ setApprovalDrafts((previous) => ({
|
|
|
+ ...previous,
|
|
|
+ [toolCallId]: {
|
|
|
+ ...previous[toolCallId],
|
|
|
+ [param]: value,
|
|
|
+ },
|
|
|
+ }));
|
|
|
+ setApprovalError(null);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSubmitApprovals = async () => {
|
|
|
+ if (!traceId || !approvalBatch || !allApprovalDecisionsReady) return;
|
|
|
+
|
|
|
+ const decisions: ToolApprovalDecisionInput[] = pendingApprovalCalls.map((call) => {
|
|
|
+ const decision = approvalDecisions[call.tool_call_id];
|
|
|
+ if (decision === "reject") {
|
|
|
+ return { tool_call_id: call.tool_call_id, decision };
|
|
|
+ }
|
|
|
+
|
|
|
+ const editedArguments = { ...call.original_arguments };
|
|
|
+ for (const param of call.editable_params) {
|
|
|
+ editedArguments[param] = JSON.parse(
|
|
|
+ approvalDrafts[call.tool_call_id]?.[param] ?? "null",
|
|
|
+ ) as unknown;
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ tool_call_id: call.tool_call_id,
|
|
|
+ decision: "approve",
|
|
|
+ edited_arguments: editedArguments,
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ setIsApprovalSubmitting(true);
|
|
|
+ setApprovalError(null);
|
|
|
+ try {
|
|
|
+ await traceApi.decideToolApproval(traceId, approvalBatch.batch_id, decisions);
|
|
|
+ setTraceStatus("running");
|
|
|
+ setApprovalBatch(null);
|
|
|
+ setApprovalDecisions({});
|
|
|
+ setApprovalDrafts({});
|
|
|
+ appendLog({
|
|
|
+ time: now(),
|
|
|
+ type: "system",
|
|
|
+ text: `已提交 ${decisions.length} 项工具决定,Agent 继续执行。`,
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ setApprovalError(errorMessage(error));
|
|
|
+ } finally {
|
|
|
+ setIsApprovalSubmitting(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
// 轮询等待 trace 停止
|
|
|
const waitUntilStopped = async (id: string, maxMs = 30000): Promise<boolean> => {
|
|
|
const interval = 300;
|
|
|
@@ -233,7 +416,7 @@ export const AgentControlPanel: FC<AgentControlPanelProps> = ({
|
|
|
while (waited < maxMs) {
|
|
|
try {
|
|
|
const detail = await traceApi.fetchTraceDetail(id);
|
|
|
- if (detail.status !== "running") return true;
|
|
|
+ if (detail.trace.status !== "running") return true;
|
|
|
} catch {
|
|
|
return true;
|
|
|
}
|
|
|
@@ -344,10 +527,12 @@ export const AgentControlPanel: FC<AgentControlPanelProps> = ({
|
|
|
};
|
|
|
|
|
|
const isRunning = traceStatus === "running";
|
|
|
+ const isWaitingForApproval = traceStatus === "waiting_confirmation";
|
|
|
const isStopped = traceStatus === "stopped" || traceStatus === "completed" || traceStatus === "failed";
|
|
|
|
|
|
const statusLabel: Record<string, string> = {
|
|
|
running: "RUNNING",
|
|
|
+ waiting_confirmation: "APPROVAL REQUIRED",
|
|
|
stopped: "PAUSED",
|
|
|
completed: "COMPLETED",
|
|
|
failed: "FAILED",
|
|
|
@@ -367,7 +552,14 @@ export const AgentControlPanel: FC<AgentControlPanelProps> = ({
|
|
|
</span>
|
|
|
</div>
|
|
|
<div className={styles.headerRight}>
|
|
|
- {isRunning ? (
|
|
|
+ {isWaitingForApproval ? (
|
|
|
+ <button
|
|
|
+ className={`${styles.btn} ${styles.btnApprovalWaiting}`}
|
|
|
+ disabled
|
|
|
+ >
|
|
|
+ ◆ 等待审批
|
|
|
+ </button>
|
|
|
+ ) : isRunning ? (
|
|
|
<button
|
|
|
className={`${styles.btn} ${styles.btnPause}`}
|
|
|
onClick={handlePause}
|
|
|
@@ -398,6 +590,166 @@ export const AgentControlPanel: FC<AgentControlPanelProps> = ({
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ {/* === 工具执行闸门:整批决定后才恢复 Agent === */}
|
|
|
+ {isWaitingForApproval && (
|
|
|
+ <section className={styles.approvalSection} aria-labelledby="tool-approval-title">
|
|
|
+ <div className={styles.approvalHeader}>
|
|
|
+ <div>
|
|
|
+ <span className={styles.approvalEyebrow}>EXECUTION GATE</span>
|
|
|
+ <h2 id="tool-approval-title" className={styles.approvalTitle}>
|
|
|
+ 工具执行等待你的决定
|
|
|
+ </h2>
|
|
|
+ <p className={styles.approvalDescription}>
|
|
|
+ 逐项批准或拒绝;全部决定会一次提交,期间不会执行任何工具。
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ {approvalBatch && (
|
|
|
+ <div className={styles.approvalBatchMeta}>
|
|
|
+ <span>{pendingApprovalCalls.length} 项待确认</span>
|
|
|
+ <code>SEQ {approvalBatch.assistant_sequence}</code>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {approvalError && (
|
|
|
+ <div className={styles.approvalError} role="alert">
|
|
|
+ {approvalError}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {!approvalBatch ? (
|
|
|
+ <div className={styles.approvalLoading} aria-live="polite">
|
|
|
+ <span className={styles.approvalLoadingMark} />
|
|
|
+ {isApprovalLoading ? "正在读取待确认调用…" : "暂未读取到审批批次,即将重试。"}
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ <div className={styles.approvalCallList}>
|
|
|
+ {approvalBatch.calls.map((call, index) => {
|
|
|
+ const isPending = call.decision === "pending";
|
|
|
+ const selectedDecision = approvalDecisions[call.tool_call_id];
|
|
|
+ const argumentNames = Array.from(new Set([
|
|
|
+ ...Object.keys(call.original_arguments),
|
|
|
+ ...call.editable_params,
|
|
|
+ ]));
|
|
|
+
|
|
|
+ return (
|
|
|
+ <article
|
|
|
+ className={`${styles.approvalCall} ${
|
|
|
+ selectedDecision === "reject" ? styles.approvalCallRejected : ""
|
|
|
+ }`}
|
|
|
+ key={call.tool_call_id}
|
|
|
+ >
|
|
|
+ <div className={styles.approvalCallHeader}>
|
|
|
+ <span className={styles.approvalCallIndex}>
|
|
|
+ {String(index + 1).padStart(2, "0")}
|
|
|
+ </span>
|
|
|
+ <div className={styles.approvalCallIdentity}>
|
|
|
+ <strong>{call.tool_name}</strong>
|
|
|
+ <code>{call.tool_call_id}</code>
|
|
|
+ </div>
|
|
|
+ {!isPending && (
|
|
|
+ <span className={styles.autoApprovalBadge}>随批次自动执行</span>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className={styles.approvalArguments}>
|
|
|
+ {argumentNames.length === 0 ? (
|
|
|
+ <span className={styles.approvalNoArguments}>无参数</span>
|
|
|
+ ) : argumentNames.map((param) => {
|
|
|
+ const editable = isPending && call.editable_params.includes(param);
|
|
|
+ const draftKey = `${call.tool_call_id}:${param}`;
|
|
|
+ return (
|
|
|
+ <div className={styles.approvalArgument} key={param}>
|
|
|
+ <div className={styles.approvalArgumentLabel}>
|
|
|
+ <code>{param}</code>
|
|
|
+ <span>{editable ? "可编辑 · JSON" : "只读"}</span>
|
|
|
+ </div>
|
|
|
+ {editable ? (
|
|
|
+ <>
|
|
|
+ <textarea
|
|
|
+ aria-label={`编辑 ${call.tool_name}.${param}`}
|
|
|
+ className={`${styles.approvalArgumentEditor} ${
|
|
|
+ approvalDraftErrors[draftKey] ? styles.approvalArgumentInvalid : ""
|
|
|
+ }`}
|
|
|
+ value={approvalDrafts[call.tool_call_id]?.[param] ?? "null"}
|
|
|
+ onChange={(event) => updateApprovalDraft(
|
|
|
+ call.tool_call_id,
|
|
|
+ param,
|
|
|
+ event.target.value,
|
|
|
+ )}
|
|
|
+ disabled={selectedDecision === "reject" || isApprovalSubmitting}
|
|
|
+ rows={Math.min(5, Math.max(2, (approvalDrafts[call.tool_call_id]?.[param] ?? "").split("\n").length))}
|
|
|
+ />
|
|
|
+ {approvalDraftErrors[draftKey] && (
|
|
|
+ <span className={styles.approvalFieldError}>
|
|
|
+ {approvalDraftErrors[draftKey]}
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ <pre className={styles.approvalArgumentValue}>
|
|
|
+ {formatJson(call.original_arguments[param])}
|
|
|
+ </pre>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {isPending && (
|
|
|
+ <div className={styles.approvalDecisionGroup}>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ aria-pressed={selectedDecision === "approve"}
|
|
|
+ aria-label={`批准 ${call.tool_name}`}
|
|
|
+ className={`${styles.approvalDecisionButton} ${
|
|
|
+ selectedDecision === "approve" ? styles.approvalDecisionApprove : ""
|
|
|
+ }`}
|
|
|
+ onClick={() => chooseApprovalDecision(call.tool_call_id, "approve")}
|
|
|
+ disabled={isApprovalSubmitting}
|
|
|
+ >
|
|
|
+ 批准执行
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ aria-pressed={selectedDecision === "reject"}
|
|
|
+ aria-label={`拒绝 ${call.tool_name}`}
|
|
|
+ className={`${styles.approvalDecisionButton} ${
|
|
|
+ selectedDecision === "reject" ? styles.approvalDecisionReject : ""
|
|
|
+ }`}
|
|
|
+ onClick={() => chooseApprovalDecision(call.tool_call_id, "reject")}
|
|
|
+ disabled={isApprovalSubmitting}
|
|
|
+ >
|
|
|
+ 拒绝调用
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </article>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className={styles.approvalFooter}>
|
|
|
+ <span>
|
|
|
+ {Object.values(approvalDecisions).filter(Boolean).length}/{pendingApprovalCalls.length} 项已决定
|
|
|
+ </span>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className={styles.approvalSubmitButton}
|
|
|
+ onClick={() => void handleSubmitApprovals()}
|
|
|
+ disabled={!allApprovalDecisionsReady || isApprovalSubmitting}
|
|
|
+ >
|
|
|
+ {isApprovalSubmitting
|
|
|
+ ? "正在提交整批决定…"
|
|
|
+ : `提交 ${pendingApprovalCalls.length} 项决定并继续`}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </section>
|
|
|
+ )}
|
|
|
+
|
|
|
{/* === 控制台日志区 === */}
|
|
|
<div className={styles.logSection}>
|
|
|
<div className={styles.logHeader}>
|
|
|
@@ -468,7 +820,7 @@ export const AgentControlPanel: FC<AgentControlPanelProps> = ({
|
|
|
<button
|
|
|
className={`${styles.actionCard} ${isReflecting ? styles.actionCardActive : ""}`}
|
|
|
onClick={handleReflect}
|
|
|
- disabled={isReflecting || isRunning}
|
|
|
+ disabled={isReflecting || isRunning || isWaitingForApproval}
|
|
|
>
|
|
|
<span className={styles.actionIcon}>🧠</span>
|
|
|
<span className={styles.actionLabel}>{isReflecting ? "反思中..." : "触发反思"}</span>
|
|
|
@@ -476,7 +828,7 @@ export const AgentControlPanel: FC<AgentControlPanelProps> = ({
|
|
|
<button
|
|
|
className={`${styles.actionCard} ${isCompacting ? styles.actionCardActive : ""}`}
|
|
|
onClick={handleCompact}
|
|
|
- disabled={isCompacting || isRunning}
|
|
|
+ disabled={isCompacting || isRunning || isWaitingForApproval}
|
|
|
>
|
|
|
<span className={styles.actionIcon}>⊞</span>
|
|
|
<span className={styles.actionLabel}>{isCompacting ? "压缩中..." : "压缩上下文"}</span>
|
|
|
@@ -490,6 +842,7 @@ export const AgentControlPanel: FC<AgentControlPanelProps> = ({
|
|
|
placeholder="插入人工干预消息..."
|
|
|
value={interventionText}
|
|
|
onChange={(e) => setInterventionText(e.target.value)}
|
|
|
+ disabled={isWaitingForApproval}
|
|
|
rows={3}
|
|
|
onKeyDown={(e) => {
|
|
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
|
|
@@ -501,7 +854,7 @@ export const AgentControlPanel: FC<AgentControlPanelProps> = ({
|
|
|
<button
|
|
|
className={styles.submitBtn}
|
|
|
onClick={handleSubmitIntervention}
|
|
|
- disabled={isSubmitting || !interventionText.trim()}
|
|
|
+ disabled={isSubmitting || isWaitingForApproval || !interventionText.trim()}
|
|
|
>
|
|
|
{isSubmitting ? "提交中..." : "提交指令"}
|
|
|
</button>
|