From dafc009e2dc26bc36f8a8063d3e956c2cabec115 Mon Sep 17 00:00:00 2001 From: maomaochong-ai Date: Sun, 6 Sep 2026 20:28:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E7=BB=9F=E8=AE=A1=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E6=8C=87=E6=A0=87=E6=8E=A5=E5=85=A5=20count-up=20?= =?UTF-8?q?=E5=8A=A8=E7=94=BB=E5=B9=B6=E5=AE=8C=E5=96=84=E7=94=BB=E5=B8=83?= =?UTF-8?q?=20Agent=20=E8=81=8A=E5=A4=A9=20UI=20(=E5=90=B8=E6=94=B6=20Boar?= =?UTF-8?q?dUI,=20=E5=B0=8A=E9=87=8D=20reduced-motion)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../canvas/canvas-agent-chat-ui.tsx | 212 +++++++++++++++--- web/src/hooks/use-count-up.ts | 48 ++++ .../admin/components/analytics-panel.tsx | 12 +- 3 files changed, 233 insertions(+), 39 deletions(-) create mode 100644 web/src/hooks/use-count-up.ts diff --git a/web/src/components/canvas/canvas-agent-chat-ui.tsx b/web/src/components/canvas/canvas-agent-chat-ui.tsx index 35285788f..6e940c819 100644 --- a/web/src/components/canvas/canvas-agent-chat-ui.tsx +++ b/web/src/components/canvas/canvas-agent-chat-ui.tsx @@ -50,7 +50,69 @@ export function extractCanvasAgentQuickActions(text: string): CanvasAgentQuickAc const WORKING_TEXT = "正在推演..."; -export function AgentChatMessage({ item, theme, user, isStreaming = false, retrying = false, onRejectTool, onApproveTool, onQuickAction, onRetry }: { item: CanvasAgentChatMessage; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; user: LocalUser | null; isStreaming?: boolean; retrying?: boolean; onRejectTool?: (id: string) => void; onApproveTool?: (id: string) => void; onQuickAction?: (prompt: string) => void; onRetry?: () => void }) { +// Agent 工作指示器:3×3 点阵对角波光。吸收自 boardui agent-thinking 的 +// wave 形态(不引 UI 资产,仅点阵节奏与降级规范);reduced-motion 下保持静态帧。 +const WORKING_DOTS = 3; +const WORKING_DOTS_TICK_MS = 90; +const WORKING_DOTS_MIN_OPACITY = 0.14; +const WORKING_DOTS_PHASE_STEP = 1 / 8; +const WORKING_DOTS_TRAIL = 0.32; + +function AgentWorkingDots() { + const reducedMotion = useReducedMotion(); + const [phase, setPhase] = useState(0); + useEffect(() => { + if (reducedMotion) return; + const timer = window.setInterval(() => setPhase((value) => (value + WORKING_DOTS_PHASE_STEP) % 1), WORKING_DOTS_TICK_MS); + return () => window.clearInterval(timer); + }, [reducedMotion]); + return ( + + {Array.from({ length: WORKING_DOTS * WORKING_DOTS }, (_, index) => { + const col = index % WORKING_DOTS; + const row = Math.floor(index / WORKING_DOTS); + // 对角波相位 0..<1:波前扫过点阵后折返,读作光在格内往返。 + const scalar = ((col + row) / (2 * (WORKING_DOTS - 1))) * (WORKING_DOTS / (WORKING_DOTS + 1)); + const behind = (phase - scalar + 1) % 1; + const lit = Math.max(0, 1 - behind / WORKING_DOTS_TRAIL) ** 1.5; + const opacity = WORKING_DOTS_MIN_OPACITY + (1 - WORKING_DOTS_MIN_OPACITY) * lit; + return ; + })} + + ); +} + +function AgentWorkingElapsed() { + const [elapsed, setElapsed] = useState(0); + useEffect(() => { + const started = performance.now(); + const timer = window.setInterval(() => setElapsed(Math.floor((performance.now() - started) / 1000)), 500); + return () => window.clearInterval(timer); + }, []); + return {elapsed}s; +} + +export function AgentChatMessage({ + item, + theme, + user, + isStreaming = false, + retrying = false, + onRejectTool, + onApproveTool, + onQuickAction, + onRetry, +}: { + item: CanvasAgentChatMessage; + theme: (typeof canvasThemes)[keyof typeof canvasThemes]; + user: LocalUser | null; + isStreaming?: boolean; + retrying?: boolean; + onRejectTool?: (id: string) => void; + onApproveTool?: (id: string) => void; + onQuickAction?: (prompt: string) => void; + onRetry?: () => void; +}) { const isUser = item.role === "user"; const isSystem = item.role === "system"; const color = item.role === "error" ? "#dc2626" : item.role === "tool" ? "#2563eb" : theme.node.text; @@ -78,7 +140,13 @@ export function AgentChatMessage({ item, theme, user, isStreaming = false, retry
{!isUser ? : null}
- {item.role === "assistant" ? {item.text} :
{item.text}
} + {item.role === "assistant" ? ( + + {item.text} + + ) : ( +
{item.text}
+ )} {item.role === "error" && onRetry ? (
{impact?.operationCount ? ( @@ -134,11 +206,27 @@ export function AgentPendingToolCard({ summary, detail, theme, onReject, onAppro 0} theme={theme} /> 0} theme={theme} />
- {impact.items.length ?
{impact.items.map((item, index) =>
{item}
)}
: null} + {impact.items.length ? ( +
+ {impact.items.map((item, index) => ( +
+ + {item} +
+ ))} +
+ ) : null} {impact.warning ?
{impact.warning}
: null} ) : null} - {detail ?
技术详情
: null} + {detail ? ( +
+ + 技术详情 + + +
+ ) : null} {onReject || onApprove ? (
{text} @@ -200,20 +301,12 @@ export function AgentToolCard({ title, text, detail, theme }: { title: string; t ); } -export function AgentWorkingMessage({ theme }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) { - const [length, setLength] = useState(1); - useEffect(() => { - const timer = window.setInterval(() => setLength((value) => (value >= WORKING_TEXT.length + 4 ? 1 : value + 1)), 120); - return () => window.clearInterval(timer); - }, [setLength]); +export function AgentWorkingMessage({ theme, label = WORKING_TEXT }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes]; label?: string }) { return ( -
- -
-
- {WORKING_TEXT.slice(0, Math.min(length, WORKING_TEXT.length))} -
-
+
+ + {label} +
); } @@ -275,11 +368,7 @@ export function AgentChatComposer({ const applySlashSkill = (skill: Skill) => { const token = `@[skill:${skill.skill_id}] `; - const next = slash - ? `${prompt.slice(0, slash.start)}${token}${prompt.slice(slash.start + slash.query.length)}` - : prompt - ? `${prompt.replace(/\s+$/u, "")} ${token}` - : token; + const next = slash ? `${prompt.slice(0, slash.start)}${token}${prompt.slice(slash.start + slash.query.length)}` : prompt ? `${prompt.replace(/\s+$/u, "")} ${token}` : token; setSlash(null); setSlashIndex(0); onPromptChange(next); @@ -321,20 +410,35 @@ export function AgentChatComposer({ return (
event.stopPropagation()}>
+ {sending && !reducedMotion ? ( + + ) : null} {attachments.length ? (
{attachments.map((item) => (
{item.name} {onRemoveAttachment ? ( - ) : null} @@ -387,12 +491,27 @@ export function AgentChatComposer({
{onAddFiles ? ( <> - { - void onAddFiles(event.target.files); - event.target.value = ""; - }} /> + { + void onAddFiles(event.target.files); + event.target.value = ""; + }} + /> -