123 lines
5.3 KiB
TypeScript
123 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
ClipboardCheck,
|
|
Crosshair,
|
|
MapPinned,
|
|
Radio,
|
|
Route
|
|
} from "lucide-react";
|
|
import type { ReactNode } from "react";
|
|
import { cn } from "@/lib/utils";
|
|
import type {
|
|
AgentChatMessage
|
|
} from "../types";
|
|
|
|
export function AgentOperationalBrief({
|
|
messages,
|
|
statusLabel,
|
|
streaming,
|
|
onSubmitCommand
|
|
}: {
|
|
messages: AgentChatMessage[];
|
|
statusLabel: string;
|
|
streaming: boolean;
|
|
onSubmitCommand: (prompt: string) => void;
|
|
}) {
|
|
const brief = createOperationalBrief(messages, statusLabel, streaming);
|
|
|
|
return (
|
|
<div className="agent-panel-control rounded-2xl p-3">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<span className={cn("h-2 w-2 rounded-full", brief.statusDotClassName)} />
|
|
<p className="truncate text-sm font-semibold text-slate-950">{brief.stateLabel}</p>
|
|
</div>
|
|
<p className="mt-1 truncate text-xs leading-5 text-slate-500" title={brief.task}>
|
|
{brief.task}
|
|
</p>
|
|
</div>
|
|
<span className="shrink-0 rounded-full bg-violet-100 px-2 py-1 text-xs font-semibold text-violet-700">
|
|
Agent 推断
|
|
</span>
|
|
</div>
|
|
|
|
<dl className="mt-3 grid grid-cols-3 gap-2">
|
|
<AgentBriefMetric icon={<Radio size={13} aria-hidden="true" />} label="证据" value={brief.evidence} />
|
|
<AgentBriefMetric icon={<Route size={13} aria-hidden="true" />} label="计划" value={brief.plan} />
|
|
<AgentBriefMetric icon={<ClipboardCheck size={13} aria-hidden="true" />} label="确认" value={brief.confirmation} />
|
|
</dl>
|
|
|
|
<div className="mt-3 grid grid-cols-2 gap-2">
|
|
<button
|
|
type="button"
|
|
className="agent-panel-primary-action flex h-9 items-center justify-center gap-1.5 rounded-xl px-3 text-xs font-semibold transition disabled:cursor-not-allowed disabled:opacity-50"
|
|
disabled={streaming}
|
|
onClick={() => onSubmitCommand(brief.primaryCommand)}
|
|
>
|
|
<MapPinned size={14} aria-hidden="true" />
|
|
预览影响
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="flex h-9 items-center justify-center gap-1.5 rounded-xl border border-slate-200 bg-[var(--glass-menu)] px-3 text-xs font-semibold text-slate-700 transition hover:border-blue-200 hover:bg-blue-50 hover:text-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
disabled={streaming}
|
|
onClick={() => onSubmitCommand(brief.secondaryCommand)}
|
|
>
|
|
<Crosshair size={14} aria-hidden="true" />
|
|
检查证据
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AgentBriefMetric({ icon, label, value }: { icon: ReactNode; label: string; value: string }) {
|
|
return (
|
|
<div className="min-w-0 rounded-xl border border-slate-200/70 bg-slate-50/90 px-2 py-2">
|
|
<dt className="flex items-center gap-1.5 text-xs font-semibold text-slate-500">
|
|
<span className="text-blue-600">{icon}</span>
|
|
{label}
|
|
</dt>
|
|
<dd className="mt-1 truncate text-xs font-semibold text-slate-900" title={value}>
|
|
{value}
|
|
</dd>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function createOperationalBrief(messages: AgentChatMessage[], statusLabel: string, streaming: boolean) {
|
|
const lastUserMessage = [...messages].reverse().find((message) => message.role === "user" && message.content.trim());
|
|
const progressItems = messages.flatMap((message) => message.progress ?? []);
|
|
const todoItems = messages.flatMap((message) => message.todos?.todos ?? []);
|
|
const permissionItems = messages.flatMap((message) => message.permissions ?? []);
|
|
const questionItems = messages.flatMap((message) => message.questions ?? []);
|
|
const latestProgress = progressItems.at(-1);
|
|
const activeTodo =
|
|
[...todoItems].reverse().find((todo) => todo.status === "in_progress") ??
|
|
[...todoItems].reverse().find((todo) => todo.status === "pending");
|
|
const pendingConfirmations =
|
|
permissionItems.filter((permission) => permission.status === "pending" || permission.status === "error").length +
|
|
questionItems.filter((question) => question.status === "pending" || question.status === "error").length;
|
|
const completedSteps = progressItems.filter((item) => item.status === "completed").length;
|
|
const task = lastUserMessage?.content.trim() || "等待调度指令";
|
|
const evidence = latestProgress?.detail || latestProgress?.title || statusLabel;
|
|
const plan = activeTodo?.content || (completedSteps > 0 ? `${completedSteps} 项已完成` : "待生成");
|
|
const confirmation = pendingConfirmations > 0 ? `${pendingConfirmations} 项待确认` : "无需确认";
|
|
const stateLabel = pendingConfirmations > 0 ? "等待人工确认" : streaming ? "Agent 分析中" : "分析完成";
|
|
const statusDotClassName = pendingConfirmations > 0 ? "bg-orange-500" : streaming ? "bg-blue-500" : "bg-emerald-500";
|
|
const promptContext = lastUserMessage?.content.trim() || "当前排水管网运行态势";
|
|
|
|
return {
|
|
task,
|
|
evidence,
|
|
plan,
|
|
confirmation,
|
|
stateLabel,
|
|
statusDotClassName,
|
|
primaryCommand: `请基于“${promptContext}”预览空间影响范围,并列出需要确认的操作。`,
|
|
secondaryCommand: `请检查“${promptContext}”的证据链,按数据来源、风险和不确定性汇总。`
|
|
};
|
|
}
|