style(agents): animate processor diagram connections

This commit is contained in:
RockChinQ
2026-08-27 15:18:34 +08:00
parent e20a73c83a
commit 05fe93e108
2 changed files with 59 additions and 1 deletions
@@ -5,6 +5,36 @@ import type { AgentKind } from '@/app/infra/entities/api';
const LANGBOT_BLUE = '#2288ee'; const LANGBOT_BLUE = '#2288ee';
const LANGBOT_CYAN = '#19b8c9'; const LANGBOT_CYAN = '#19b8c9';
function DiagramMotionStyles() {
return (
<style>{`
@keyframes processor-line-flow {
to { stroke-dashoffset: -30; }
}
@keyframes processor-link-breathe {
0%, 100% { stroke-opacity: 0.28; }
50% { stroke-opacity: 0.72; }
}
.processor-line-flow {
animation: processor-line-flow 1.8s linear infinite;
}
.processor-link-breathe {
animation: processor-link-breathe 2.8s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.processor-line-flow,
.processor-link-breathe {
animation: none;
}
}
`}</style>
);
}
function ArrowMarker({ id }: { id: string }) { function ArrowMarker({ id }: { id: string }) {
return ( return (
<defs> <defs>
@@ -80,6 +110,7 @@ function AgentDiagram() {
> >
<desc>{t('agents.agentDiagramDescription')}</desc> <desc>{t('agents.agentDiagramDescription')}</desc>
<ArrowMarker id={arrowId} /> <ArrowMarker id={arrowId} />
<DiagramMotionStyles />
<rect width="760" height="620" fill="var(--card)" /> <rect width="760" height="620" fill="var(--card)" />
@@ -100,8 +131,12 @@ function AgentDiagram() {
fill="none" fill="none"
stroke={LANGBOT_BLUE} stroke={LANGBOT_BLUE}
strokeWidth="2" strokeWidth="2"
strokeDasharray="4 11"
opacity="0.32" opacity="0.32"
markerEnd={`url(#${arrowId})`} markerEnd={`url(#${arrowId})`}
className="processor-line-flow"
data-motion="flow"
style={{ animationDelay: `${index * -0.35}s` }}
/> />
<rect <rect
x="64" x="64"
@@ -158,7 +193,7 @@ function AgentDiagram() {
{t('agents.diagramAgentCanUse')} {t('agents.diagramAgentCanUse')}
</text> </text>
{outputs.map((item) => ( {outputs.map((item, index) => (
<g key={item.label}> <g key={item.label}>
<path <path
d={`M436 310 C468 310 474 ${item.y + 29} 538 ${item.y + 29}`} d={`M436 310 C468 310 474 ${item.y + 29} 538 ${item.y + 29}`}
@@ -167,6 +202,9 @@ function AgentDiagram() {
strokeWidth="1.8" strokeWidth="1.8"
strokeDasharray="5 6" strokeDasharray="5 6"
opacity="0.48" opacity="0.48"
className="processor-link-breathe"
data-motion="relation"
style={{ animationDelay: `${index * -0.45}s` }}
/> />
<rect <rect
x="538" x="538"
@@ -224,6 +262,7 @@ function PipelineDiagram() {
> >
<desc>{t('agents.pipelineDiagramDescription')}</desc> <desc>{t('agents.pipelineDiagramDescription')}</desc>
<ArrowMarker id={arrowId} /> <ArrowMarker id={arrowId} />
<DiagramMotionStyles />
<rect width="760" height="620" fill="var(--card)" /> <rect width="760" height="620" fill="var(--card)" />
<text <text
@@ -243,6 +282,16 @@ function PipelineDiagram() {
opacity="0.32" opacity="0.32"
markerEnd={`url(#${arrowId})`} markerEnd={`url(#${arrowId})`}
/> />
<path
d="M156 310H724"
fill="none"
stroke={LANGBOT_BLUE}
strokeWidth="3"
strokeDasharray="4 14"
strokeLinecap="round"
className="processor-line-flow"
data-motion="flow"
/>
{steps.map((step, index) => { {steps.map((step, index) => {
const x = 40 + index * 142; const x = 40 + index * 142;
+9
View File
@@ -183,11 +183,20 @@ test.describe('frontend CRUD smoke flows', () => {
); );
await expect(agentTypeCard).toHaveAttribute('aria-checked', 'true'); await expect(agentTypeCard).toHaveAttribute('aria-checked', 'true');
await expect(page.getByTestId('agent-diagram')).toBeVisible(); await expect(page.getByTestId('agent-diagram')).toBeVisible();
await expect(
page.getByTestId('agent-diagram').locator('[data-motion="flow"]'),
).toHaveCount(3);
await expect(
page.getByTestId('agent-diagram').locator('[data-motion="relation"]'),
).toHaveCount(3);
await expect(page.getByTestId('pipeline-diagram')).toHaveCount(0); await expect(page.getByTestId('pipeline-diagram')).toHaveCount(0);
await page.getByRole('radio', { name: /^Pipeline/ }).click(); await page.getByRole('radio', { name: /^Pipeline/ }).click();
await expect(pipelineTypeCard).toHaveAttribute('aria-checked', 'true'); await expect(pipelineTypeCard).toHaveAttribute('aria-checked', 'true');
await expect(page.getByTestId('pipeline-diagram')).toBeVisible(); await expect(page.getByTestId('pipeline-diagram')).toBeVisible();
await expect(
page.getByTestId('pipeline-diagram').locator('[data-motion="flow"]'),
).toHaveCount(1);
await expect(page.getByTestId('agent-diagram')).toHaveCount(0); await expect(page.getByTestId('agent-diagram')).toHaveCount(0);
await expect(page.locator('input[name="name"]')).toBeVisible(); await expect(page.locator('input[name="name"]')).toBeVisible();