Files
next-tjwater-drainage-frontend/features/agent/components/agent-operational-brief.tsx
T

123 lines
5.2 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="surface-control flex h-9 items-center justify-center gap-1.5 rounded-xl border px-3 text-xs font-semibold text-slate-700 transition-colors 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="surface-reading min-w-0 rounded-xl border 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}”的证据链,按数据来源、风险和不确定性汇总。`
};
}