From eb7fc77677b1a45743a17ee162394ec6850ee487 Mon Sep 17 00:00:00 2001 From: daguanren21 <1363917353@qq.com> Date: Wed, 9 Sep 2026 10:04:19 +0800 Subject: [PATCH 1/2] feat(agent-ui): add linked RAG pipeline motion --- .changeset/long-ads-enjoy.md | 5 + apps/docs/ai/index.md | 41 +- apps/docs/en/ai/index.md | 41 +- apps/playground-h5/src/agent-rag.test.ts | 116 ++ .../components/agent-ui/AgentRagPipeline.vue | 1078 +++++++++++++++++ .../src/components/agent-ui/index.ts | 243 ++-- .../src/components/agent-ui/rag-icons.ts | 9 + .../src/components/agent-ui/rag-pipeline.ts | 81 ++ .../agent-workspace/AgentWorkspaceDemo.vue | 97 +- .../src/components/blocks/agent-workspace.vue | 36 +- apps/playground-h5/src/features/useRagDemo.ts | 151 +++ apps/playground-h5/src/styles/varo.css | 10 + apps/playground-weapp/src/agent-rag.test.ts | 98 ++ .../components/agent-ui/AgentRagPipeline.vue | 800 ++++++++++++ .../src/components/agent-ui/AgentShell.vue | 3 +- .../src/components/agent-ui/rag-icons.ts | 9 + .../src/components/agent-ui/rag-pipeline.ts | 81 ++ .../src/components/blocks/agent-workspace.vue | 44 +- .../src/features/useRagDemo.ts | 151 +++ .../src/pages/agent-workspace/index.vue | 161 +-- apps/playground-weapp/src/styles/varo.css | 10 + apps/playground-weapp/vite.config.ts | 1 + packages/cli/src/index.ts | 8 +- packages/cli/tsdown.config.ts | 14 + registry/blocks/agent-workspace/h5.vue | 36 +- .../blocks/agent-workspace/weapp-vite.vue | 44 +- .../components/agent-ui/AgentRagPipeline.vue | 800 ++++++++++++ registry/components/agent-ui/AgentShell.vue | 3 +- .../agent-ui/h5/AgentRagPipeline.vue | 1078 +++++++++++++++++ registry/components/agent-ui/h5/index.ts | 9 + registry/components/agent-ui/rag-icons.ts | 9 + registry/components/agent-ui/rag-pipeline.ts | 81 ++ registry/components/agent-ui/registry.json | 30 + registry/themes/base/h5.css | 10 + registry/themes/base/weapp-vite.css | 10 + 35 files changed, 5067 insertions(+), 331 deletions(-) create mode 100644 .changeset/long-ads-enjoy.md create mode 100644 apps/playground-h5/src/agent-rag.test.ts create mode 100644 apps/playground-h5/src/components/agent-ui/AgentRagPipeline.vue create mode 100644 apps/playground-h5/src/components/agent-ui/rag-icons.ts create mode 100644 apps/playground-h5/src/components/agent-ui/rag-pipeline.ts create mode 100644 apps/playground-h5/src/features/useRagDemo.ts create mode 100644 apps/playground-weapp/src/agent-rag.test.ts create mode 100644 apps/playground-weapp/src/components/agent-ui/AgentRagPipeline.vue create mode 100644 apps/playground-weapp/src/components/agent-ui/rag-icons.ts create mode 100644 apps/playground-weapp/src/components/agent-ui/rag-pipeline.ts create mode 100644 apps/playground-weapp/src/features/useRagDemo.ts create mode 100644 packages/cli/tsdown.config.ts create mode 100644 registry/components/agent-ui/AgentRagPipeline.vue create mode 100644 registry/components/agent-ui/h5/AgentRagPipeline.vue create mode 100644 registry/components/agent-ui/rag-icons.ts create mode 100644 registry/components/agent-ui/rag-pipeline.ts diff --git a/.changeset/long-ads-enjoy.md b/.changeset/long-ads-enjoy.md new file mode 100644 index 00000000..83eb279d --- /dev/null +++ b/.changeset/long-ads-enjoy.md @@ -0,0 +1,5 @@ +--- +"@varo-ui/cli": minor +--- + +Add a five-stage RAG pipeline with linked citation motion, cancellation and reduced-motion support; keep native workspace close controls aligned to the trailing edge. diff --git a/apps/docs/ai/index.md b/apps/docs/ai/index.md index b96b937e..8b96fa04 100644 --- a/apps/docs/ai/index.md +++ b/apps/docs/ai/index.md @@ -64,18 +64,19 @@ pnpm dlx @varo-ui/cli add --target weapp blocks/agent-workspace ## 上下文、引用与产物 -| 组件 | 用途 | -| ------------------------------------------ | -------------------------- | -| [AgentArtifact](./artifact) | 代码、文档、文件和图片产物 | -| [AgentAttachmentList](./attachments) | 附件预览与移除 | -| [AgentSourceList](./sources) | 来源列表 | -| [AgentCitations](./citations) | 可折叠引用集合 | -| `AgentRetrievalProgress` | 可见检索队列与失败重试 | -| `AgentSourceReceipt` | 回答完成后的来源读取回执 | -| [AgentContextCard](./context-card) | 检索知识块 | -| [AgentCodeBlock](./code-block) | 独立流式代码块 | -| [AgentFileDiff](./file-diff) | 文件级差异 | -| [AgentImageGeneration](./image-generation) | 图片生成生命周期 | +| 组件 | 用途 | +| ------------------------------------------ | ------------------------------------ | +| [AgentArtifact](./artifact) | 代码、文档、文件和图片产物 | +| [AgentAttachmentList](./attachments) | 附件预览与移除 | +| [AgentSourceList](./sources) | 来源列表 | +| [AgentCitations](./citations) | 可折叠引用集合 | +| `AgentRetrievalProgress` | 可见检索队列与失败重试 | +| `AgentRagPipeline` | 五阶段 RAG、上下文组装与流式引用联动 | +| `AgentSourceReceipt` | 回答完成后的来源读取回执 | +| [AgentContextCard](./context-card) | 检索知识块 | +| [AgentCodeBlock](./code-block) | 独立流式代码块 | +| [AgentFileDiff](./file-diff) | 文件级差异 | +| [AgentImageGeneration](./image-generation) | 图片生成生命周期 | ## 结构化数据与工作区 @@ -97,6 +98,7 @@ pnpm dlx @varo-ui/cli add --target weapp blocks/agent-workspace | ------------------------ | ------------------------------------- | ---------------------------------------------------- | | `AgentComposerScope` | `sources`、`usagePercent`、`disabled` | `toggle(source, enabled)`、`connect(source)` | | `AgentRetrievalProgress` | `items` | `retry(item)` | +| `AgentRagPipeline` | `query`、`steps`、`sources`、`answer` | `run`、`cancel`、`selectSource(source)` | | `AgentSourceReceipt` | `items`、`summary` | `open(item)`、`connect(item)` | | `AgentTaskRunner` | `tasks`、`busy` | `retry(task)`、`approve(task)`、`cancel` | | `AgentThreadVersions` | `versions`、`activeId` | `select(version)`、`branch(version)`、`pin(version)` | @@ -104,6 +106,21 @@ pnpm dlx @varo-ui/cli add --target weapp blocks/agent-workspace `createAgentThreadController()` 在 `@varo-ui/ai` 中拥有不可变、无环的版本图;组件只渲染 snapshot 并转发决策。`AgentWorkspace` 使用 `toggleSource`、`retryRetrieval`、`retryTask`、`selectVersion` 等 qualified events,避免不同 surface 的 `retry` / `connect` 冲突。 +### RAG 流程与动效 + +`AgentRagPipeline` 随 `components/agent-ui` 安装。它只投影受控快照,不执行检索、模型请求或来源授权。业务层按实际进度替换输入: + +- `steps`:固定 `query`、`embed`、`retrieve`、`assemble`、`generate` 五个阶段;状态为 `waiting`、`running`、`completed` 或 `failed`。可提供 `detail` 与 `durationMs`。仍有阶段运行时,整体保持运行状态并允许停止;没有运行阶段后再呈现失败结果。 +- `sources`:稳定 `id` 与 `title`,可附带 `excerpt`、0–1 的 `score`、上下文 `tokens` 和 `blue/violet/rose` 色调。仅当所有来源都有有限正数 `tokens` 时按权重分配片段,否则等分;片段保留可读的最小宽度。 +- `answer`:带稳定 `id` 的文本片段 `{ type: 'text', text }` 或引用 `{ type: 'citation', sourceId }`。引用通过来源 ID 关联,不依赖数组位置。删除或更换引用映射会清理已失效的来源反馈,不重播未变化的引用。 +- `title`、`elapsedMs`、`className` 控制标题、总耗时与容器;`reducedMotion` 关闭非必要动效。 + +新引用出现时,对应来源与上下文片段短暂高亮;H5 的悬停、键盘聚焦及点击均能联动来源,小程序使用点击选择。来源缺失时,H5 显示禁用引用按钮,小程序显示不可交互的 `[?]` 文本,并通过可访问标签说明来源不可用;正文仍可阅读。停止、重播与卸载会清理演示定时器;重播沿用当前查询,组件的业务状态不等待动画结束。 + +H5 自动遵循 `prefers-reduced-motion`,小程序同时提供 `reducedMotion` 属性与媒体查询样式。动效使用主题中的 `--varo-agent-motion-*` 变量以及 transform/opacity,不引入动画运行时依赖。 + +`AgentWorkspace` 的 `execution` 插槽可放入此组件,替换默认的对话、检索、任务与回执区域;来源授权、版本与输入区仍由 Workspace 组合。Weapp 的 `weapp-vite` 配置应设置 `weapp.vue.template.scopedSlotsRequireProps: true`,让没有插槽参数的 `#execution` 保持普通命名插槽,避免 7.0.4 的泛型插槽自动 `setData.pick` 遗漏父级状态。两端 playground 提供可取消、重播的演示数据流程。 + ## 完整 Block - [AgentChat](./agent-chat):组合消息历史、流式回答、推理、工具、审批和输入区。 diff --git a/apps/docs/en/ai/index.md b/apps/docs/en/ai/index.md index 77ed5931..601af693 100644 --- a/apps/docs/en/ai/index.md +++ b/apps/docs/en/ai/index.md @@ -64,18 +64,19 @@ pnpm dlx @varo-ui/cli add --target weapp blocks/agent-workspace ## Context, Citations, and Artifacts -| Component | Responsibility | -| ------------------------------------------ | --------------------------------------------- | -| [AgentArtifact](./artifact) | Code, document, file, and image artifacts | -| [AgentAttachmentList](./attachments) | Attachment preview and removal | -| [AgentSourceList](./sources) | Source list | -| [AgentCitations](./citations) | Collapsible citations | -| `AgentRetrievalProgress` | Visible retrieval queue and failed-item retry | -| `AgentSourceReceipt` | Post-answer source read receipt | -| [AgentContextCard](./context-card) | Retrieved knowledge chunks | -| [AgentCodeBlock](./code-block) | Standalone streaming code block | -| [AgentFileDiff](./file-diff) | File-level diff | -| [AgentImageGeneration](./image-generation) | Image generation lifecycle | +| Component | Responsibility | +| ------------------------------------------ | ---------------------------------------------------------------- | +| [AgentArtifact](./artifact) | Code, document, file, and image artifacts | +| [AgentAttachmentList](./attachments) | Attachment preview and removal | +| [AgentSourceList](./sources) | Source list | +| [AgentCitations](./citations) | Collapsible citations | +| `AgentRetrievalProgress` | Visible retrieval queue and failed-item retry | +| `AgentRagPipeline` | Five-stage RAG, context assembly, and linked streaming citations | +| `AgentSourceReceipt` | Post-answer source read receipt | +| [AgentContextCard](./context-card) | Retrieved knowledge chunks | +| [AgentCodeBlock](./code-block) | Standalone streaming code block | +| [AgentFileDiff](./file-diff) | File-level diff | +| [AgentImageGeneration](./image-generation) | Image generation lifecycle | ## Structured Data and Workspace @@ -97,6 +98,7 @@ pnpm dlx @varo-ui/cli add --target weapp blocks/agent-workspace | ------------------------ | ------------------------------------- | ---------------------------------------------------- | | `AgentComposerScope` | `sources`, `usagePercent`, `disabled` | `toggle(source, enabled)`, `connect(source)` | | `AgentRetrievalProgress` | `items` | `retry(item)` | +| `AgentRagPipeline` | `query`, `steps`, `sources`, `answer` | `run`, `cancel`, `selectSource(source)` | | `AgentSourceReceipt` | `items`, `summary` | `open(item)`, `connect(item)` | | `AgentTaskRunner` | `tasks`, `busy` | `retry(task)`, `approve(task)`, `cancel` | | `AgentThreadVersions` | `versions`, `activeId` | `select(version)`, `branch(version)`, `pin(version)` | @@ -104,6 +106,21 @@ pnpm dlx @varo-ui/cli add --target weapp blocks/agent-workspace `createAgentThreadController()` in `@varo-ui/ai` owns the immutable acyclic version graph; components only render its snapshot and forward decisions. `AgentWorkspace` exposes qualified events such as `toggleSource`, `retryRetrieval`, `retryTask`, and `selectVersion` so unrelated `retry` and `connect` surfaces never collide. +### RAG Pipeline and Motion + +`AgentRagPipeline` is installed with `components/agent-ui`. It projects controlled snapshots; it does not perform retrieval, model requests, or source authorization. Replace its inputs as real work progresses: + +- `steps`: the fixed `query`, `embed`, `retrieve`, `assemble`, and `generate` stages, each with `waiting`, `running`, `completed`, or `failed` status. Optional `detail` and `durationMs` describe progress. While any stage is running, the overall flow remains active and cancellable; failure takes precedence once no stage remains active. +- `sources`: stable `id` and `title`, with optional `excerpt`, a 0–1 `score`, context `tokens`, and a `blue/violet/rose` tone. Segments use token weights only when every source has a finite positive token count; otherwise they share space equally and retain a readable minimum width. +- `answer`: text parts `{ type: 'text', text }` or citations `{ type: 'citation', sourceId }`, each carrying a stable `id`. Citations resolve by source ID, not array position. Removing or remapping citations clears obsolete source feedback without replaying unchanged citations. +- `title`, `elapsedMs`, and `className` control the heading, total elapsed time, and container; `reducedMotion` disables nonessential motion. + +A newly arriving citation briefly highlights its source and context segment. H5 links pointer hover, keyboard focus, and selection; Weapp uses tap selection. Missing sources produce a disabled citation button on H5 and noninteractive `[?]` text on Weapp, with an accessible label describing the unavailable source; readable answer text is preserved. Cancellation, replay, and unmount clean up demo timers. Replay retains the current query, and business state never waits for an animation to finish. + +H5 follows `prefers-reduced-motion` automatically. Weapp provides the `reducedMotion` prop and media-query styles. Motion uses theme `--varo-agent-motion-*` variables with transform/opacity and adds no animation runtime dependency. + +Place the component in the `AgentWorkspace` `execution` slot to replace its default conversation, retrieval, task, and receipt region while retaining source permissions, thread versions, and input. In Weapp, set `weapp.vue.template.scopedSlotsRequireProps: true` in the `weapp-vite` configuration so a `#execution` slot without scope parameters remains an ordinary named slot. This avoids version 7.0.4's generic-slot automatic `setData.pick` omitting parent state. Both playgrounds provide a cancellable, replayable demo-data flow. + ## Complete Block - [AgentChat](./agent-chat): composes history, streaming responses, reasoning, tools, approval, and prompt input. diff --git a/apps/playground-h5/src/agent-rag.test.ts b/apps/playground-h5/src/agent-rag.test.ts new file mode 100644 index 00000000..69975fb7 --- /dev/null +++ b/apps/playground-h5/src/agent-rag.test.ts @@ -0,0 +1,116 @@ +// @vitest-environment jsdom +import type { AgentRagAnswerPart, AgentRagSource } from './components/agent-ui/rag-pipeline' +import { enableAutoUnmount, mount } from '@vue/test-utils' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { defineComponent } from 'vue' +import AgentRagPipeline from './components/agent-ui/AgentRagPipeline.vue' +import { useRagDemo } from './features/useRagDemo' + +enableAutoUnmount(afterEach) +afterEach(() => vi.useRealTimers()) + +const source: AgentRagSource = { id: 'source-a', title: 'Source A', tone: 'blue' } +const answer: AgentRagAnswerPart[] = [ + { id: 'text-a', type: 'text', text: 'An answer with evidence ' }, + { id: 'citation-a', type: 'citation', sourceId: source.id }, +] + +describe('RAG source references', () => { + it('keeps the answer readable while source metadata disappears and returns', async () => { + const wrapper = mount(AgentRagPipeline, { props: { sources: [source], answer } }) + await wrapper.setProps({ sources: [] }) + expect(wrapper.text()).toContain('An answer with evidence') + expect(wrapper.get('[data-rag-citation="source-a"]').attributes('disabled')).toBeDefined() + + await wrapper.setProps({ sources: [source] }) + const citation = wrapper.get('[data-rag-citation="source-a"]') + expect(citation.attributes('disabled')).toBeUndefined() + await citation.trigger('focus') + expect(wrapper.get('[data-rag-source="source-a"]').classes()).toContain('is-highlighted') + }) + + it('does not replay citation pings for text-only updates', async () => { + vi.useFakeTimers() + const wrapper = mount(AgentRagPipeline, { props: { sources: [source], answer: [] } }) + await wrapper.setProps({ answer }) + expect(wrapper.get('[data-rag-source="source-a"]').classes()).toContain('is-pinging') + await vi.runOnlyPendingTimersAsync() + expect(wrapper.get('[data-rag-source="source-a"]').classes()).not.toContain('is-pinging') + + await wrapper.setProps({ answer: [...answer, { id: 'text-b', type: 'text', text: 'and more text.' }] }) + expect(wrapper.get('[data-rag-source="source-a"]').classes()).not.toContain('is-pinging') + }) + + it('follows the latest pointer or keyboard source interaction', async () => { + const secondSource: AgentRagSource = { id: 'source-b', title: 'Source B', tone: 'violet' } + const wrapper = mount(AgentRagPipeline, { + props: { + sources: [source, secondSource], + answer: [...answer, { id: 'citation-b', type: 'citation', sourceId: secondSource.id }], + }, + }) + const first = wrapper.get('[data-rag-source="source-a"]') + const second = wrapper.get('[data-rag-source="source-b"]') + const citation = wrapper.get('[data-rag-citation="source-b"]') + await first.trigger('focus') + await citation.trigger('mouseenter') + expect(second.classes()).toContain('is-highlighted') + expect(first.classes()).not.toContain('is-highlighted') + + await first.trigger('focus') + expect(first.classes()).toContain('is-highlighted') + await citation.trigger('mousemove') + expect(second.classes()).toContain('is-highlighted') + await first.trigger('click') + expect(first.classes()).toContain('is-highlighted') + expect(second.classes()).not.toContain('is-highlighted') + }) + + it('preserves a submitted query across cancellation and completed replays', async () => { + vi.useFakeTimers() + let demo!: ReturnType + mount(defineComponent({ + setup() { + demo = useRagDemo(() => [{ id: 'support', label: 'Support', enabled: true, status: 'available' }], 'en') + return () => null + }, + })) + const query = 'Explain recovery after a payment failure' + const firstRun = demo.run(query) + await vi.advanceTimersByTimeAsync(400) + demo.cancel() + await firstRun + + const replay = demo.run() + expect(demo.snapshot.value.query).toBe(query) + await vi.runAllTimersAsync() + await replay + + const completedReplay = demo.run() + expect(demo.snapshot.value.query).toBe(query) + await vi.runAllTimersAsync() + await completedReplay + expect(demo.snapshot.value.steps.every(step => step.status === 'completed')).toBe(true) + }) + + it('removes only feedback whose source loses its last citation', async () => { + vi.useFakeTimers() + const second: AgentRagSource = { id: 'source-b', title: 'Source B', tone: 'violet' } + const secondCitation: AgentRagAnswerPart = { id: 'citation-b', type: 'citation', sourceId: second.id } + const wrapper = mount(AgentRagPipeline, { props: { sources: [source, second], answer: [] } }) + await wrapper.setProps({ answer: [...answer, secondCitation] }) + await wrapper.setProps({ answer: [secondCitation] }) + expect(wrapper.get('[data-rag-source="source-a"]').classes()).not.toContain('is-pinging') + expect(wrapper.get('[data-rag-source="source-b"]').classes()).toContain('is-pinging') + }) + + it('reconciles a citation whose source mapping changes', async () => { + vi.useFakeTimers() + const second: AgentRagSource = { id: 'source-b', title: 'Source B', tone: 'violet' } + const wrapper = mount(AgentRagPipeline, { props: { sources: [source, second], answer: [] } }) + await wrapper.setProps({ answer }) + await wrapper.setProps({ answer: [{ id: 'citation-a', type: 'citation', sourceId: second.id }] }) + expect(wrapper.get('[data-rag-source="source-a"]').classes()).not.toContain('is-pinging') + expect(wrapper.get('[data-rag-source="source-b"]').classes()).toContain('is-pinging') + }) +}) diff --git a/apps/playground-h5/src/components/agent-ui/AgentRagPipeline.vue b/apps/playground-h5/src/components/agent-ui/AgentRagPipeline.vue new file mode 100644 index 00000000..6bd149f3 --- /dev/null +++ b/apps/playground-h5/src/components/agent-ui/AgentRagPipeline.vue @@ -0,0 +1,1078 @@ + + + + + diff --git a/apps/playground-h5/src/components/agent-ui/index.ts b/apps/playground-h5/src/components/agent-ui/index.ts index 5030c6e5..59e6df17 100644 --- a/apps/playground-h5/src/components/agent-ui/index.ts +++ b/apps/playground-h5/src/components/agent-ui/index.ts @@ -3,10 +3,9 @@ import type { AgentPartStatus, AgentStreamSnapshot, AgentStreamStatus, - AgentToolPart, AgentThreadVersion, + AgentToolPart, } from '@varo-ui/ai' -import { useBodyScrollLock } from '@varo-ui/headless' import type { PropType, ShallowRef } from 'vue' import type { ClassValue } from '../../lib/cn' import type { @@ -15,6 +14,7 @@ import type { AgentSourceReceiptItem, AgentWorkspacePlacement, } from './advanced-types' +import { useBodyScrollLock } from '@varo-ui/headless' import { computed, defineComponent, @@ -92,14 +92,28 @@ export interface AgentAttachmentItem { } const baseButton = 'inline-flex min-h-10 items-center justify-center rounded-xl border px-3 text-xs font-bold transition-colors disabled:cursor-not-allowed disabled:opacity-45' -const primaryButton = `${baseButton} border-teal-700 bg-teal-700 text-white hover:bg-teal-800` -const quietButton = `${baseButton} border-slate-200 bg-white text-slate-700 hover:bg-slate-50` +const primaryButton = `${baseButton} border-[var(--varo-agent-primary)] bg-[var(--varo-agent-primary)] text-[var(--varo-agent-primary-foreground)] hover:opacity-90` +const quietButton = `${baseButton} border-[var(--varo-agent-border)] bg-[var(--varo-agent-surface)] text-[var(--varo-agent-foreground)] hover:bg-[var(--varo-agent-surface-strong)]` function partMark(status: AgentPartStatus) { - if (status === 'completed') { return 'bg-emerald-500' } - if (status === 'failed') { return 'bg-red-500' } - if (status === 'running') { return 'agent-ui__pulse bg-teal-600' } - return 'bg-slate-300' + if (status === 'completed') { return 'bg-[var(--varo-agent-success)]' } + if (status === 'failed') { return 'bg-[var(--varo-agent-danger)]' } + if (status === 'running') { return 'agent-ui__pulse bg-[var(--varo-agent-primary)]' } + return 'bg-[var(--varo-agent-border-strong)]' +} + +function renderCheckIcon() { + return h('svg', { + 'fill': 'none', + 'stroke': 'currentColor', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + 'stroke-width': 2.2, + 'viewBox': '0 0 24 24', + 'width': 14, + 'height': 14, + 'aria-hidden': 'true', + }, [h('path', { d: 'm5 12 4 4L19 6' })]) } export const AgentLoading = defineComponent({ @@ -130,11 +144,11 @@ export const AgentLoading = defineComponent({ onMounted(start) onBeforeUnmount(stop) - return () => h('div', { class: 'flex min-h-12 items-center gap-3 text-slate-700', role: 'status' }, [ + return () => h('div', { class: 'agent-loading flex min-h-12 items-center gap-3 text-[var(--varo-agent-foreground)]', role: 'status' }, [ h('span', { 'class': 'agent-ui__loading flex h-6 w-8 items-center justify-center gap-1', 'aria-hidden': 'true' }, Array.from({ length: props.variant === 'grid' ? 6 : 3 }, (_, index) => - h('i', { class: 'h-1.5 w-1.5 rounded-full bg-teal-700', style: { animationDelay: `${index * 80}ms` } }))), + h('i', { class: 'h-1.5 w-1.5 rounded-full bg-[var(--varo-agent-primary)]', style: { animationDelay: `${index * 80}ms` } }))), h('span', { class: 'min-w-0 flex-1 truncate text-[13px] font-semibold' }, props.label), - h('span', { class: 'text-[11px] tabular-nums text-slate-400' }, `${elapsed.value.toFixed(1)}s`), + h('span', { class: 'text-[12px] tabular-nums text-[var(--varo-agent-muted)]' }, `${elapsed.value.toFixed(1)}s`), ]) }, }) @@ -151,10 +165,10 @@ export const AgentMessage = defineComponent({ 'class': ['agent-message flex w-full min-w-0 items-start gap-2.5', props.role === 'user' && 'justify-end'], 'data-role': props.role, }, [ - props.role !== 'user' ? h('span', { 'class': 'grid h-8 w-8 flex-none place-items-center rounded-[10px] bg-teal-700 text-xs font-black text-white', 'aria-hidden': 'true' }, 'V') : null, + props.role !== 'user' ? h('span', { 'class': 'grid h-8 w-8 flex-none place-items-center rounded-[10px] bg-[var(--varo-agent-primary)] text-xs font-black text-[var(--varo-agent-primary-foreground)]', 'aria-hidden': 'true' }, 'V') : null, h('div', { class: ['grid min-w-0 max-w-[82%] gap-1', props.role === 'user' && 'justify-items-end'] }, [ props.label || props.timestamp - ? h('header', { class: 'flex w-full items-center justify-between gap-3 px-1 text-[10px] text-slate-400' }, [ + ? h('header', { class: 'flex w-full items-center justify-between gap-3 px-1 text-[11px] text-[var(--varo-agent-muted)]' }, [ h('span', props.label || (props.role === 'assistant' ? 'Varo Agent' : props.role === 'user' ? '你' : '系统')), props.timestamp ? h('time', props.timestamp) : null, ]) @@ -163,8 +177,8 @@ export const AgentMessage = defineComponent({ class: [ 'min-w-11 max-w-full overflow-hidden break-words border px-3.5 py-2.5 shadow-sm', props.role === 'user' - ? 'rounded-[16px_4px_16px_16px] border-teal-700 bg-teal-700 text-white' - : 'rounded-[4px_16px_16px_16px] border-slate-200 bg-white text-slate-800', + ? 'rounded-[16px_4px_16px_16px] border-[var(--varo-agent-primary)] bg-[var(--varo-agent-primary)] text-[var(--varo-agent-primary-foreground)]' + : 'rounded-[4px_16px_16px_16px] border-[var(--varo-agent-border)] bg-[var(--varo-agent-surface)] text-[var(--varo-agent-foreground)]', ], }, slots.default?.()), ]), @@ -188,7 +202,7 @@ export const AgentThinking = defineComponent({ const completed = computed(() => props.steps.filter(step => step.status === 'completed').length) return () => h('section', { - 'class': cn('agent-thinking overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm', props.className), + 'class': cn('agent-thinking overflow-hidden rounded-2xl border border-[var(--varo-agent-border)] bg-[var(--varo-agent-surface)] shadow-sm', props.className), 'data-open': String(currentOpen.value), }, [ h('button', { @@ -260,11 +274,11 @@ export const AgentToolChip = defineComponent({ tool: { type: Object as PropType, required: true }, }, setup(props) { - return () => h('span', { class: 'inline-flex max-w-full items-center gap-2 rounded-xl border border-slate-200 bg-white px-2.5 py-1.5' }, [ + return () => h('span', { class: 'inline-flex max-w-full items-center gap-2 rounded-xl border border-[var(--varo-agent-border)] bg-[var(--varo-agent-surface)] px-2.5 py-1.5' }, [ h('i', { class: ['h-2 w-2 flex-none rounded-full', partMark(props.tool.status)] }), h('span', { class: 'grid min-w-0' }, [ - h('strong', { class: 'truncate text-[11px] text-slate-700' }, props.tool.name), - props.tool.summary && !props.compact ? h('small', { class: 'truncate text-[10px] text-slate-400' }, props.tool.summary) : null, + h('strong', { class: 'truncate text-[12px] text-[var(--varo-agent-foreground)]' }, props.tool.name), + props.tool.summary && !props.compact ? h('small', { class: 'truncate text-[11px] text-[var(--varo-agent-muted)]' }, props.tool.summary) : null, ]), ]) }, @@ -278,16 +292,16 @@ export const AgentTaskList = defineComponent({ }, setup(props) { const completed = computed(() => props.tasks.filter(task => task.status === 'completed').length) - return () => h('section', { 'class': 'overflow-hidden rounded-2xl border border-slate-200 bg-white', 'aria-live': 'polite' }, [ - h('header', { class: 'flex min-h-11 items-center justify-between border-b border-slate-100 px-3.5' }, [ - h('strong', { class: 'text-[13px] text-slate-950' }, props.title), - h('span', { class: 'text-[11px] tabular-nums text-slate-400' }, `${completed.value}/${props.tasks.length}`), + return () => h('section', { 'class': 'overflow-hidden rounded-2xl border border-[var(--varo-agent-border)] bg-[var(--varo-agent-surface)]', 'aria-live': 'polite' }, [ + h('header', { class: 'flex min-h-11 items-center justify-between border-b border-[var(--varo-agent-border)] px-3.5' }, [ + h('strong', { class: 'text-[13px] text-[var(--varo-agent-foreground)]' }, props.title), + h('span', { class: 'text-[12px] tabular-nums text-[var(--varo-agent-muted)]' }, `${completed.value}/${props.tasks.length}`), ]), - ...props.tasks.map((task, index) => h('div', { class: 'flex min-h-[50px] items-center gap-2.5 border-b border-slate-50 px-3.5 py-2 last:border-0', key: task.id }, [ - h('span', { class: ['grid h-6 w-6 flex-none place-items-center rounded-full border text-[10px] font-bold', task.status === 'completed' ? 'border-emerald-200 bg-emerald-50 text-emerald-700' : 'border-slate-200 text-slate-500'] }, task.status === 'completed' ? '✓' : String(index + 1)), + ...props.tasks.map((task, index) => h('div', { class: 'flex min-h-[50px] items-center gap-2.5 border-b border-[var(--varo-agent-border)] px-3.5 py-2 last:border-0', key: task.id }, [ + h('span', { class: ['grid h-6 w-6 flex-none place-items-center rounded-full border text-[11px] font-bold', task.status === 'completed' ? 'border-[var(--varo-agent-success)] bg-[var(--varo-agent-success-soft)] text-[var(--varo-agent-success)]' : 'border-[var(--varo-agent-border)] text-[var(--varo-agent-text)]'] }, task.status === 'completed' ? renderCheckIcon() : String(index + 1)), h('span', { class: 'grid min-w-0 flex-1 gap-1' }, [ - h('span', { class: 'flex justify-between gap-2 text-xs font-semibold text-slate-700' }, [task.title, task.meta ? h('small', { class: 'text-slate-400' }, task.meta) : null]), - task.progress === undefined ? null : h('span', { class: 'h-1 overflow-hidden rounded-full bg-slate-100' }, [h('i', { class: 'block h-full bg-teal-600', style: { width: `${Math.min(100, Math.max(0, task.progress))}%` } })]), + h('span', { class: 'flex justify-between gap-2 text-xs font-semibold text-[var(--varo-agent-foreground)]' }, [task.title, task.meta ? h('small', { class: 'text-[var(--varo-agent-muted)]' }, task.meta) : null]), + task.progress === undefined ? null : h('span', { class: 'h-1 overflow-hidden rounded-full bg-[var(--varo-agent-fill)]' }, [h('i', { class: 'block h-full bg-[var(--varo-agent-primary)]', style: { width: `${Math.min(100, Math.max(0, task.progress))}%` } })]), ]), ])), ]) @@ -373,7 +387,7 @@ export const AgentComposerScope = defineComponent({ h('span', { class: 'text-[11px] tabular-nums text-[var(--varo-agent-muted)]' }, `${enabledCount.value} 个已启用`), ]), props.sources.length - ? h('div', { class: 'grid gap-1 p-2' }, props.sources.map(source => { + ? h('div', { class: 'grid gap-1 p-2' }, props.sources.map((source) => { const status = source.status ?? 'available' return h('div', { 'class': 'flex min-h-[58px] items-center gap-2.5 rounded-xl bg-[var(--varo-agent-surface-strong)] px-2.5 py-2', 'data-status': status, 'key': source.id }, [ h('i', { 'class': ['h-3 w-3 flex-none rounded-full', contextSourceStatusClasses[status]], 'aria-hidden': 'true' }), @@ -857,27 +871,68 @@ export const AgentApproval = defineComponent({ 'update:value': (_value: string) => true, }, setup(props, { emit, slots }) { - return () => h('section', { 'class': 'grid gap-3.5 rounded-2xl border border-amber-200 bg-gradient-to-br from-white to-amber-50 p-4 shadow-sm', 'role': 'group', 'aria-label': props.title }, [ - h('header', { class: 'flex items-start gap-3' }, [ - h('span', { class: 'grid h-9 w-9 flex-none place-items-center rounded-xl bg-amber-100 font-black text-amber-700' }, '!'), - h('span', { class: 'grid min-w-0 gap-0.5' }, [ - h('small', { class: 'font-extrabold tracking-widest text-amber-700' }, '需要你的确认'), - h('strong', { class: 'text-[15px] leading-snug text-amber-950' }, props.title), - props.description ? h('span', { class: 'text-xs leading-5 text-amber-800' }, props.description) : null, + return () => h('section', { 'class': 'agent-approval', 'role': 'group', 'aria-label': props.title }, [ + h('header', { class: 'agent-approval__header' }, [ + h('span', { 'class': 'agent-approval__icon', 'aria-hidden': 'true' }, [ + h('svg', { + 'fill': 'none', + 'stroke': 'currentColor', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + 'stroke-width': 1.8, + 'viewBox': '0 0 24 24', + }, [ + h('path', { d: 'm12 3-7 3v5c0 4.6 3 8.6 7 10 4-1.4 7-5.4 7-10V6z' }), + h('path', { d: 'M12 8v4 M12 16h.01' }), + ]), + ]), + h('span', { class: 'agent-approval__heading' }, [ + h('small', { class: 'agent-approval__eyebrow' }, '需要你的确认'), + h('strong', { class: 'agent-approval__title' }, props.title), + props.description + ? h('span', { class: 'agent-approval__description' }, props.description) + : null, ]), ]), props.choices.length > 0 - ? h('div', { class: 'grid gap-2' }, props.choices.map(choice => - h('label', { class: ['flex min-h-11 cursor-pointer items-center gap-2 rounded-xl border px-3 text-xs', choice.value === props.value ? 'border-teal-600 bg-teal-50 text-teal-900' : 'border-slate-200 bg-white text-slate-700', choice.disabled && 'cursor-not-allowed opacity-45'], key: choice.value }, [ - h('input', { checked: choice.value === props.value, disabled: choice.disabled, name: `agent-approval-${props.title}`, type: 'radio', value: choice.value, onChange: () => emit('update:value', choice.value) }), - h('span', { class: 'grid' }, [h('strong', choice.label), choice.description ? h('small', { class: 'text-slate-500' }, choice.description) : null]), + ? h('div', { class: 'agent-approval__choices' }, props.choices.map(choice => + h('label', { + 'class': 'agent-approval__choice', + 'data-disabled': String(Boolean(choice.disabled)), + 'data-selected': String(choice.value === props.value), + 'key': choice.value, + }, [ + h('input', { + class: 'agent-approval__radio', + checked: choice.value === props.value, + disabled: choice.disabled, + name: `agent-approval-${props.title}`, + type: 'radio', + value: choice.value, + onChange: () => emit('update:value', choice.value), + }), + h('span', { class: 'agent-approval__choice-copy' }, [ + h('strong', choice.label), + choice.description + ? h('small', choice.description) + : null, + ]), ]), )) : null, slots.default?.(), - h('footer', { class: 'flex justify-end gap-2' }, [ - h('button', { class: quietButton, type: 'button', onClick: () => emit('reject') }, props.rejectText), - h('button', { class: primaryButton, disabled: props.choices.length > 0 && !props.value, type: 'button', onClick: () => emit('approve', props.value) }, props.approveText), + h('footer', { class: 'agent-approval__footer' }, [ + h('button', { + class: 'agent-approval__reject', + type: 'button', + onClick: () => emit('reject'), + }, props.rejectText), + h('button', { + class: 'agent-approval__approve', + disabled: props.choices.length > 0 && !props.value, + type: 'button', + onClick: () => emit('approve', props.value), + }, props.approveText), ]), ]) }, @@ -894,10 +949,10 @@ export const AgentRecommendation = defineComponent({ emits: { accept: () => true }, setup(props, { emit, slots }) { const confidence = computed(() => Math.min(100, Math.max(0, props.confidence))) - return () => h('section', { class: 'grid gap-3 rounded-2xl border border-blue-100 bg-gradient-to-br from-white to-blue-50 p-4 shadow-sm' }, [ - h('header', { class: 'flex justify-between text-[10px] font-extrabold tracking-widest text-blue-700' }, [h('span', 'AGENT 建议'), h('span', `${confidence.value}%`)]), - h('strong', { class: 'text-[15px] text-slate-950' }, props.title), - props.description ? h('p', { class: 'm-0 text-xs leading-5 text-slate-600' }, props.description) : null, + return () => h('section', { class: 'agent-recommendation grid gap-3 rounded-2xl border border-[var(--varo-agent-border)] bg-[var(--varo-agent-surface)] p-4 shadow-[var(--varo-agent-shadow)]' }, [ + h('header', { class: 'flex justify-between text-[11px] font-extrabold tracking-widest text-[var(--varo-agent-primary)]' }, [h('span', 'AGENT 建议'), h('span', `${confidence.value}%`)]), + h('strong', { class: 'text-[15px] text-[var(--varo-agent-foreground)]' }, props.title), + props.description ? h('p', { class: 'm-0 text-xs leading-5 text-[var(--varo-agent-text)]' }, props.description) : null, h('span', { class: 'h-1.5 overflow-hidden rounded-full bg-blue-100' }, [h('i', { class: 'block h-full rounded-full bg-blue-600', style: { width: `${confidence.value}%` } })]), slots.default?.(), h('footer', { class: 'flex justify-end gap-2' }, [slots.secondary?.(), h('button', { class: primaryButton, type: 'button', onClick: () => emit('accept') }, props.acceptText)]), @@ -911,7 +966,7 @@ export const AgentPromptSuggestions = defineComponent({ emits: { select: (_value: string) => true }, setup(props, { emit }) { return () => h('div', { class: 'flex max-w-full gap-2 overflow-x-auto pb-1' }, props.suggestions.map(suggestion => - h('button', { class: 'min-h-9 flex-none rounded-full border border-slate-200 bg-white px-3 text-[11px] font-semibold text-slate-600 hover:border-teal-300 hover:text-teal-800', key: suggestion, type: 'button', onClick: () => emit('select', suggestion) }, suggestion), + h('button', { class: 'min-h-9 flex-none rounded-full border border-[var(--varo-agent-border)] bg-[var(--varo-agent-surface)] px-3 text-[12px] font-semibold text-[var(--varo-agent-text)] hover:border-[var(--varo-agent-primary)] hover:text-[var(--varo-agent-primary)]', key: suggestion, type: 'button', onClick: () => emit('select', suggestion) }, suggestion), )) }, }) @@ -919,6 +974,7 @@ export const AgentPromptSuggestions = defineComponent({ export const AgentComposer = defineComponent({ name: 'AgentComposer', props: { + ariaLabel: { type: String, default: 'Agent 输入' }, busy: Boolean, maxLength: { type: Number, default: 4000 }, modelValue: { type: String, default: '' }, @@ -935,21 +991,22 @@ export const AgentComposer = defineComponent({ if (!normalized || props.busy) { return } emit('submit', normalized) } - return () => h('div', { class: 'grid w-full min-w-0 gap-2.5' }, [ + return () => h('div', { class: 'agent-composer grid w-full min-w-0 gap-2.5' }, [ h(AgentPromptSuggestions, { suggestions: props.suggestions, onSelect: submit }), - h('div', { class: 'flex min-h-14 min-w-0 items-center gap-2 rounded-[18px] border border-slate-200 bg-white p-2 shadow-lg' }, [ + h('div', { class: 'agent-composer__shell flex min-h-14 min-w-0 items-center gap-2 rounded-[18px] border border-[var(--varo-agent-border)] bg-[var(--varo-agent-surface)] p-2 shadow-lg' }, [ slots.leading?.(), h('textarea', { - class: 'max-h-40 min-h-10 min-w-0 flex-1 resize-none border-0 bg-transparent px-2 py-2.5 text-sm leading-5 text-slate-900 outline-none placeholder:text-slate-400', - disabled: props.busy, - maxlength: props.maxLength, - placeholder: props.placeholder, - rows: 1, - value: props.modelValue, - onInput: (event: Event) => { + 'aria-label': props.ariaLabel, + 'class': 'max-h-40 min-h-10 min-w-0 flex-1 resize-none border-0 bg-transparent px-2 py-2.5 text-sm leading-5 text-[var(--varo-agent-foreground)] outline-none placeholder:text-[var(--varo-agent-muted)]', + 'disabled': props.busy, + 'maxlength': props.maxLength, + 'placeholder': props.placeholder, + 'rows': 1, + 'value': props.modelValue, + 'onInput': (event: Event) => { if (event.target instanceof HTMLTextAreaElement) { emit('update:modelValue', event.target.value) } }, - onKeydown: (event: KeyboardEvent) => { + 'onKeydown': (event: KeyboardEvent) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault() submit() @@ -961,7 +1018,7 @@ export const AgentComposer = defineComponent({ 'button', { 'aria-label': props.busy ? 'Agent 正在处理' : '发送', - 'class': 'grid h-10 w-10 flex-none place-items-center self-center rounded-full bg-teal-700 text-lg font-bold text-white shadow-sm transition-transform active:scale-95 disabled:opacity-45', + 'class': 'grid h-10 w-10 flex-none place-items-center self-center rounded-full bg-[var(--varo-agent-primary)] text-lg font-bold text-[var(--varo-agent-primary-foreground)] shadow-sm transition-transform active:translate-y-px disabled:opacity-45', 'disabled': props.busy || !props.modelValue.trim(), 'type': 'button', 'onClick': () => submit(), @@ -976,7 +1033,20 @@ export const AgentComposer = defineComponent({ style: { animationDelay: `${index * 90}ms` }, })), ) - : h('span', { 'aria-hidden': 'true', 'class': '-translate-y-px' }, '↑'), + : h('svg', { + 'fill': 'none', + 'stroke': 'currentColor', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + 'stroke-width': 1.9, + 'viewBox': '0 0 24 24', + 'width': 20, + 'height': 20, + 'aria-hidden': 'true', + }, [ + h('path', { d: 'm4 12 16-8-5 16-3.5-6.5L4 12Z' }), + h('path', { d: 'M11.5 13.5 20 4' }), + ]), ), ]), ]) @@ -995,10 +1065,10 @@ export const AgentStream = defineComponent({ }, emits: { retry: () => true }, setup(props, { emit, slots }) { - return () => h('div', { 'class': cn('agent-stream text-sm leading-7 text-slate-800', props.className), 'data-status': props.status, 'aria-live': 'polite' }, [ + return () => h('div', { 'class': cn('agent-stream text-sm leading-7 text-[var(--varo-agent-foreground)]', props.className), 'data-status': props.status, 'aria-live': 'polite' }, [ h(AgentMarkdown, { content: props.content, final: props.final || props.status === 'completed' }), - props.cursor && props.status === 'streaming' ? h('i', { class: 'agent-ui__cursor ml-1 inline-block h-[1.15em] w-0.5 rounded-full bg-teal-700 align-[-.18em]' }) : null, - props.status === 'failed' ? h('div', { class: 'mt-2.5 flex min-h-11 items-center justify-between gap-3 rounded-xl bg-red-50 px-3 py-2.5 text-xs text-red-700', role: 'alert' }, [h('span', props.error || '生成失败,请重试'), h('button', { class: quietButton, type: 'button', onClick: () => emit('retry') }, '重试')]) : null, + props.cursor && props.status === 'streaming' ? h('i', { class: 'agent-ui__cursor ml-1 inline-block h-[1.15em] w-0.5 rounded-full bg-[var(--varo-agent-primary)] align-[-.18em]' }) : null, + props.status === 'failed' ? h('div', { class: 'mt-2.5 flex min-h-11 items-center justify-between gap-3 rounded-xl bg-[var(--varo-agent-danger-soft)] px-3 py-2.5 text-xs text-[var(--varo-agent-danger)]', role: 'alert' }, [h('span', props.error || '生成失败,请重试'), h('button', { class: quietButton, type: 'button', onClick: () => emit('retry') }, '重试')]) : null, props.status === 'completed' ? h('div', { class: 'mt-3 flex flex-wrap gap-2' }, slots.actions?.()) : null, ]) }, @@ -1039,13 +1109,13 @@ export const AgentArtifact = defineComponent({ h('article', { class: 'overflow-hidden rounded-2xl border border-slate-800 bg-slate-950 shadow-lg' }, [ h('header', { class: 'flex min-h-14 items-center justify-between gap-3 border-b border-slate-800 bg-slate-900 px-4' }, [ h('span', { class: 'grid min-w-0 gap-0.5' }, [ - h('small', { class: 'text-[9px] font-black uppercase tracking-[0.16em] text-teal-300' }, props.artifact.kind || 'artifact'), + h('small', { class: 'text-[10px] font-black uppercase tracking-[0.16em] text-teal-300' }, props.artifact.kind || 'artifact'), h('strong', { class: 'truncate text-xs text-slate-100' }, props.artifact.title), ]), h( 'button', { - class: 'min-h-8 flex-none rounded-lg border border-slate-700 bg-slate-800 px-3 text-[10px] font-bold text-slate-200 transition-colors hover:border-teal-500 hover:text-white', + class: 'min-h-8 flex-none rounded-lg border border-slate-700 bg-slate-800 px-3 text-[11px] font-bold text-slate-200 transition-colors hover:border-teal-500 hover:text-white', type: 'button', onClick: () => emit('open', props.artifact), }, @@ -1055,7 +1125,7 @@ export const AgentArtifact = defineComponent({ props.artifact.content ? h( 'pre', - { class: 'm-0 max-h-72 overflow-auto whitespace-pre-wrap break-words bg-slate-950 px-4 py-3.5 font-mono text-[11px] leading-5 text-slate-200' }, + { class: 'm-0 max-h-72 overflow-auto whitespace-pre-wrap break-words bg-slate-950 px-4 py-3.5 font-mono text-[12px] leading-5 text-slate-200' }, props.artifact.content, ) : null, @@ -1072,13 +1142,13 @@ export const AgentSourceList = defineComponent({ emits: { open: (_source: AgentSourceItem) => true }, setup(props, { emit }) { return () => - h('section', { class: 'grid gap-2.5 rounded-2xl border border-slate-200 bg-slate-50 p-3 shadow-sm' }, [ + h('section', { class: 'grid gap-2.5 rounded-2xl border border-[var(--varo-agent-border)] bg-[var(--varo-agent-surface-strong)] p-3 shadow-sm' }, [ h('header', { class: 'flex items-end justify-between gap-3 px-0.5' }, [ h('span', { class: 'grid gap-0.5' }, [ - h('small', { class: 'text-[9px] font-black uppercase tracking-[0.16em] text-teal-700' }, 'Sources'), - h('strong', { class: 'text-xs text-slate-800' }, props.title), + h('small', { class: 'text-[10px] font-black uppercase tracking-[0.16em] text-[var(--varo-agent-primary)]' }, 'Sources'), + h('strong', { class: 'text-xs text-[var(--varo-agent-foreground)]' }, props.title), ]), - h('small', { class: 'rounded-full bg-white px-2 py-1 text-[9px] font-bold text-slate-500 ring-1 ring-slate-200' }, `${props.sources.length} refs`), + h('small', { class: 'rounded-full bg-[var(--varo-agent-surface)] px-2 py-1 text-[10px] font-bold text-[var(--varo-agent-text)] ring-1 ring-slate-200' }, `${props.sources.length} refs`), ]), h( 'div', @@ -1087,7 +1157,7 @@ export const AgentSourceList = defineComponent({ h( 'a', { - class: 'group flex min-h-14 items-center gap-3 rounded-xl border border-slate-200 bg-white px-3 text-xs text-slate-700 shadow-sm transition-all hover:-translate-y-px hover:border-teal-300 hover:shadow-md', + class: 'group flex min-h-14 items-center gap-3 rounded-xl border border-[var(--varo-agent-border)] bg-[var(--varo-agent-surface)] px-3 text-xs text-[var(--varo-agent-foreground)] shadow-sm transition-all hover:-translate-y-px hover:border-[var(--varo-agent-primary)] hover:shadow-md', href: source.url, key: source.id, rel: 'noreferrer noopener', @@ -1095,12 +1165,12 @@ export const AgentSourceList = defineComponent({ onClick: () => emit('open', source), }, [ - h('span', { class: 'grid h-8 w-8 flex-none place-items-center rounded-xl bg-teal-50 text-[11px] font-black text-teal-700' }, String(index + 1).padStart(2, '0')), + h('span', { class: 'grid h-8 w-8 flex-none place-items-center rounded-xl bg-[var(--varo-agent-primary-soft)] text-[12px] font-black text-[var(--varo-agent-primary)]' }, String(index + 1).padStart(2, '0')), h('span', { class: 'grid min-w-0 flex-1 gap-0.5' }, [ - h('strong', { class: 'truncate text-[11px] text-slate-800' }, source.title), - h('small', { class: 'truncate text-[9px] text-slate-400' }, source.domain || source.description || source.url), + h('strong', { class: 'truncate text-[12px] text-[var(--varo-agent-foreground)]' }, source.title), + h('small', { class: 'truncate text-[10px] text-[var(--varo-agent-muted)]' }, source.domain || source.description || source.url), ]), - h('span', { 'aria-hidden': 'true', 'class': 'text-xs text-slate-300 transition-colors group-hover:text-teal-600' }, '↗'), + h('span', { 'aria-hidden': 'true', 'class': 'text-xs text-slate-300 transition-colors group-hover:text-[var(--varo-agent-primary)]' }, '↗'), ], ), ), @@ -1115,29 +1185,29 @@ export const AgentAttachmentList = defineComponent({ emits: { remove: (_item: AgentAttachmentItem) => true }, setup(props, { emit }) { return () => - h('section', { class: 'grid gap-2.5 rounded-2xl border border-slate-200 bg-slate-50 p-3 shadow-sm' }, [ + h('section', { class: 'grid gap-2.5 rounded-2xl border border-[var(--varo-agent-border)] bg-[var(--varo-agent-surface-strong)] p-3 shadow-sm' }, [ h('header', { class: 'flex items-end justify-between gap-3 px-0.5' }, [ h('span', { class: 'grid gap-0.5' }, [ - h('small', { class: 'text-[9px] font-black uppercase tracking-[0.16em] text-teal-700' }, 'Files'), - h('strong', { class: 'text-xs text-slate-800' }, '附件'), + h('small', { class: 'text-[10px] font-black uppercase tracking-[0.16em] text-[var(--varo-agent-primary)]' }, 'Files'), + h('strong', { class: 'text-xs text-[var(--varo-agent-foreground)]' }, '附件'), ]), - h('small', { class: 'text-[9px] font-bold text-slate-400' }, `${props.attachments.length} items`), + h('small', { class: 'text-[10px] font-bold text-[var(--varo-agent-muted)]' }, `${props.attachments.length} items`), ]), h( 'div', { class: 'grid gap-2' }, props.attachments.map(item => - h('article', { class: 'flex min-h-14 min-w-0 items-center gap-3 rounded-xl border border-slate-200 bg-white px-3 shadow-sm', key: item.id }, [ + h('article', { class: 'flex min-h-14 min-w-0 items-center gap-3 rounded-xl border border-[var(--varo-agent-border)] bg-[var(--varo-agent-surface)] px-3 shadow-sm', key: item.id }, [ item.previewUrl ? h('img', { alt: '', class: 'h-9 w-9 flex-none rounded-xl object-cover', src: item.previewUrl }) : h( 'i', - { class: 'grid h-9 w-9 flex-none place-items-center rounded-xl bg-slate-900 text-[9px] font-black not-italic text-white' }, + { class: 'grid h-9 w-9 flex-none place-items-center rounded-xl bg-slate-900 text-[10px] font-black not-italic text-white' }, item.name.split('.').pop()?.slice(0, 4).toUpperCase() || 'FILE', ), h('span', { class: 'grid min-w-0 flex-1 gap-0.5' }, [ - h('strong', { class: 'truncate text-[11px] text-slate-800' }, item.name), - h('small', { class: 'text-[9px] text-slate-400' }, [item.size, item.mimeType].filter(Boolean).join(' · ')), + h('strong', { class: 'truncate text-[12px] text-[var(--varo-agent-foreground)]' }, item.name), + h('small', { class: 'text-[10px] text-[var(--varo-agent-muted)]' }, [item.size, item.mimeType].filter(Boolean).join(' · ')), ]), h( 'button', @@ -1205,4 +1275,13 @@ export const AgentEventRenderer = defineComponent({ export { AgentMarkdown } export * from './advanced' +export { default as AgentRagPipeline } from './AgentRagPipeline.vue' +export type { + AgentRagAnswerPart, + AgentRagPipelineProps, + AgentRagSource, + AgentRagSourceTone, + AgentRagStageId, + AgentRagStep, +} from './rag-pipeline' export type { AgentStreamSnapshot, AgentStreamStatus, AgentToolPart } from '@varo-ui/ai' diff --git a/apps/playground-h5/src/components/agent-ui/rag-icons.ts b/apps/playground-h5/src/components/agent-ui/rag-icons.ts new file mode 100644 index 00000000..d7336904 --- /dev/null +++ b/apps/playground-h5/src/components/agent-ui/rag-icons.ts @@ -0,0 +1,9 @@ +import type { AgentRagStageId } from './rag-pipeline' + +export const agentRagIcons: Record = { + query: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIxLjgiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0iTTcgNGgxMGEzIDMgMCAwIDEgMyAzdjdhMyAzIDAgMCAxLTMgM2gtNmwtNCAzdi0zYTMgMyAwIDAgMS0zLTNWN2EzIDMgMCAwIDEgMy0zWiIvPjxwYXRoIGQ9Ik05LjUgOWEyLjUgMi41IDAgMCAxIDUgLjVjMCAxLjUtMi41IDEuNS0yLjUgM00xMiAxNC41aC4wMSIvPjwvc3ZnPg==', + embed: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIxLjgiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0iTTQgOGgxNk00IDE2aDE2TTkgNCA3IDIwTTE3IDRsLTIgMTYiLz48L3N2Zz4=', + retrieve: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIxLjgiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PGNpcmNsZSBjeD0iMTAuNSIgY3k9IjEwLjUiIHI9IjYuNSIvPjxwYXRoIGQ9Im0xNiAxNiA0IDQiLz48L3N2Zz4=', + assemble: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIxLjgiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0iTTcgM2g3bDUgNXYxM0g1VjNoMlpNMTQgM3Y2aDVNOCAxM2g4TTggMTdoNiIvPjwvc3ZnPg==', + generate: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIxLjgiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0ibTEyIDMgMi4zIDYuN0wyMSAxMmwtNi43IDIuM0wxMiAyMWwtMi4zLTYuN0wzIDEybDYuNy0yLjNMMTIgM1pNMjAgMnY0TTE4IDRoNCIvPjwvc3ZnPg==', +} diff --git a/apps/playground-h5/src/components/agent-ui/rag-pipeline.ts b/apps/playground-h5/src/components/agent-ui/rag-pipeline.ts new file mode 100644 index 00000000..c5be7a0e --- /dev/null +++ b/apps/playground-h5/src/components/agent-ui/rag-pipeline.ts @@ -0,0 +1,81 @@ +import type { ClassValue } from '../../lib/cn' +import type { AgentAdvancedStatus } from './advanced-types' + +export type AgentRagStageId = 'query' | 'embed' | 'retrieve' | 'assemble' | 'generate' +export type AgentRagSourceTone = 'blue' | 'violet' | 'rose' + +export interface AgentRagStep { + id: AgentRagStageId + status: AgentAdvancedStatus + detail?: string + durationMs?: number +} + +export interface AgentRagSource { + id: string + title: string + excerpt?: string + score?: number + tokens?: number + tone?: AgentRagSourceTone +} + +export type AgentRagAnswerPart + = | { id: string, type: 'text', text: string } + | { id: string, type: 'citation', sourceId: string } + +export interface AgentRagPipelineProps { + className?: ClassValue + title?: string + query?: string + steps?: readonly AgentRagStep[] + sources?: readonly AgentRagSource[] + answer?: readonly AgentRagAnswerPart[] + elapsedMs?: number + reducedMotion?: boolean +} + +export const agentRagStages = [ + { id: 'query', label: '理解问题', description: '确认本次查询与来源范围' }, + { id: 'embed', label: '向量编码', description: '将问题转换为检索表示' }, + { id: 'retrieve', label: '检索来源', description: '查找与问题相关的片段' }, + { id: 'assemble', label: '组装上下文', description: '将来源片段组织为上下文' }, + { id: 'generate', label: '引用回答', description: '生成可追溯到来源的回答' }, +] as const + +const statusLabels: Record = { + waiting: '等待', + running: '处理中', + completed: '已完成', + failed: '失败', +} + +export function resolveRagSteps(steps: readonly AgentRagStep[]) { + const byId = new Map() + for (const step of steps) { + if (byId.has(step.id)) { throw new Error(`Duplicate RAG stage: ${step.id}`) } + byId.set(step.id, step) + } + return agentRagStages.map((definition) => { + const step = byId.get(definition.id) + const status = step?.status ?? 'waiting' + return { + ...definition, + status, + detail: step?.detail ?? definition.description, + statusLabel: statusLabels[status], + durationLabel: step?.durationMs === undefined ? '' : `${Math.round(step.durationMs)} ms`, + } + }) +} + +const sourceTones = ['blue', 'violet', 'rose'] as const + +export function getRagSourceTone(source: AgentRagSource): AgentRagSourceTone { + if (source.tone) { return source.tone } + let hash = 0 + for (let index = 0; index < source.id.length; index += 1) { + hash = (Math.imul(hash, 31) + source.id.charCodeAt(index)) | 0 + } + return sourceTones[(hash >>> 0) % 3]! +} diff --git a/apps/playground-h5/src/components/agent-workspace/AgentWorkspaceDemo.vue b/apps/playground-h5/src/components/agent-workspace/AgentWorkspaceDemo.vue index dc515b5f..2eb7b689 100644 --- a/apps/playground-h5/src/components/agent-workspace/AgentWorkspaceDemo.vue +++ b/apps/playground-h5/src/components/agent-workspace/AgentWorkspaceDemo.vue @@ -1,48 +1,26 @@ @@ -64,44 +48,59 @@ function submit(value: string) {
-

Grounded Agent Workspace

+

+ Grounded Agent Workspace +

Context, execution, and evidence stay visible

- - - + + +
-

{{ activity }}

+

+ {{ activity }} +

+ > + +
@@ -161,8 +160,8 @@ function submit(value: string) { } .agent-workspace-demo__activity { - margin: 0; padding: 10px 12px; + margin: 0; font-size: 12px; color: var(--varo-agent-text); background: var(--varo-agent-fill); @@ -171,8 +170,8 @@ function submit(value: string) { @media (max-width: 720px) { .agent-workspace-demo__header { - align-items: stretch; flex-direction: column; + align-items: stretch; } .agent-workspace-demo__placements { diff --git a/apps/playground-h5/src/components/blocks/agent-workspace.vue b/apps/playground-h5/src/components/blocks/agent-workspace.vue index 9e20bf76..f8f38ce4 100644 --- a/apps/playground-h5/src/components/blocks/agent-workspace.vue +++ b/apps/playground-h5/src/components/blocks/agent-workspace.vue @@ -116,23 +116,25 @@ function forwardSourceToggle(source: AgentContextSource, enabled: boolean) { @pin="emit('pinVersion', $event)" @select="emit('selectVersion', $event)" /> - - - - + + + + + +