fix: stabilize agent UI and SCADA card

This commit is contained in:
2026-07-20 17:17:55 +08:00
parent df2fb3ca38
commit 319c604e9d
12 changed files with 408 additions and 700 deletions
@@ -16,6 +16,7 @@ import { useState, type ReactNode } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/shared/ui/button";
import {
agentEase,
agentLayoutTransition,
agentPanelItemVariants,
agentPanelListVariants,
@@ -117,20 +118,20 @@ export function AgentMessageDetails({
}
return (
<motion.div className="space-y-2" layout variants={agentPanelListVariants} animate="animate">
<motion.div className="space-y-2" variants={agentPanelListVariants} animate="animate">
<AnimatePresence initial={false} mode="popLayout">
{message.todos ? (
<motion.div key="todos" layout initial="initial" animate="animate" exit="exit" variants={agentPanelItemVariants}>
<motion.div key="todos" initial="initial" animate="animate" exit="exit" variants={agentPanelItemVariants}>
<TodoCard todoUpdate={message.todos} />
</motion.div>
) : null}
{message.permissions?.length ? (
<motion.div key="permissions" layout initial="initial" animate="animate" exit="exit" variants={agentPanelItemVariants}>
<motion.div key="permissions" initial="initial" animate="animate" exit="exit" variants={agentPanelItemVariants}>
<PermissionList permissions={message.permissions} onReplyPermission={onReplyPermission} />
</motion.div>
) : null}
{message.questions?.length ? (
<motion.div key="questions" layout initial="initial" animate="animate" exit="exit" variants={agentPanelItemVariants}>
<motion.div key="questions" initial="initial" animate="animate" exit="exit" variants={agentPanelItemVariants}>
<QuestionList
questions={message.questions}
onReplyQuestion={onReplyQuestion}
@@ -184,7 +185,6 @@ function AgentNestedBlock({
function AnimatedDetailItem({ children }: { children: ReactNode }) {
return (
<motion.div
layout
initial="initial"
animate="animate"
exit="exit"
@@ -198,8 +198,8 @@ function AnimatedDetailItem({ children }: { children: ReactNode }) {
function ProgressList({ progress, running }: { progress: AgentChatProgress[]; running: boolean }) {
const [expanded, setExpanded] = useState(false);
const recentProgress = progress.slice(-3);
const visibleProgress = expanded ? progress : running ? recentProgress : [];
const visibleProgress = expanded ? progress : progress.slice(-1);
const listMode = expanded ? "expanded" : "summary";
return (
<div className="agent-panel-nested rounded-xl p-2.5">
@@ -216,7 +216,7 @@ function ProgressList({ progress, running }: { progress: AgentChatProgress[]; ru
{expanded
? `全部 ${progress.length}`
: running
? `最近 ${recentProgress.length}`
? "当前步骤"
: `${progress.length}`}
</span>
<ChevronDown
@@ -225,43 +225,68 @@ function ProgressList({ progress, running }: { progress: AgentChatProgress[]; ru
aria-hidden="true"
/>
</button>
<AnimatePresence initial={false}>
<AnimatePresence initial={false} mode="wait">
{visibleProgress.length ? (
<motion.ol
key={listMode}
aria-label={expanded ? "全部执行进度" : "当前执行进度"}
className="mt-2 space-y-1.5 overflow-hidden"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={agentLayoutTransition}
>
{visibleProgress.map((item) => (
<li key={item.id} className="grid grid-cols-[16px_1fr_auto] items-start gap-2 text-xs">
<span
className={cn(
"mt-0.5 h-3.5 w-3.5 rounded-full border",
progressStatusMeta[item.status].dotClassName
)}
/>
<span className="min-w-0">
<span className="block truncate font-semibold text-slate-800" title={item.title}>
{item.title}
</span>
{item.detail ? (
<span className="block line-clamp-2 whitespace-pre-wrap leading-5 text-slate-500">
{item.detail}
</span>
) : null}
</span>
<span
className={cn(
"rounded-full px-2 py-0.5 font-semibold",
progressStatusMeta[item.status].className
)}
<AnimatePresence initial={false} mode={expanded ? "sync" : "wait"}>
{visibleProgress.map((item) => (
<motion.li
key={item.id}
className="grid min-h-7 grid-cols-[16px_1fr_auto] items-start gap-2 text-xs"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -2 }}
transition={{ duration: 0.16, ease: agentEase }}
>
{progressStatusMeta[item.status].label}
</span>
</li>
))}
<motion.span
aria-hidden="true"
className={cn(
"mt-0.5 h-3.5 w-3.5 rounded-full border transition-colors duration-150",
progressStatusMeta[item.status].dotClassName
)}
animate={item.status === "running" ? { opacity: [0.45, 1, 0.45] } : { opacity: 1 }}
transition={
item.status === "running"
? { duration: 1.2, ease: "easeInOut", repeat: Infinity }
: { duration: 0.14 }
}
/>
<span className="min-w-0">
<span className="block truncate font-semibold text-slate-800" title={item.title}>
{item.title}
</span>
{item.detail ? (
<span className="block line-clamp-2 whitespace-pre-wrap leading-5 text-slate-500">
{item.detail}
</span>
) : null}
</span>
<AnimatePresence initial={false} mode="wait">
<motion.span
key={item.status}
className={cn(
"min-w-12 rounded-full px-2 py-0.5 text-center font-semibold",
progressStatusMeta[item.status].className
)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.14 }}
>
{progressStatusMeta[item.status].label}
</motion.span>
</AnimatePresence>
</motion.li>
))}
</AnimatePresence>
</motion.ol>
) : null}
</AnimatePresence>
@@ -277,7 +302,6 @@ function TodoCard({ todoUpdate }: { todoUpdate: AgentTodoUpdate }) {
{todoUpdate.todos.slice(0, 6).map((todo) => (
<motion.li
key={todo.id}
layout
className="grid grid-cols-[16px_1fr_auto] items-start gap-2 text-xs"
initial="initial"
animate="animate"