Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/long-ads-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@varo-ui/cli": minor
"@varo-ui/h5": patch
"@varo-ui/weapp": patch
---

Add a five-stage RAG pipeline with linked citation motion, cancellation and reduced-motion support; synchronize public H5/Weapp stylesheets with the Registry motion and source-color tokens, and keep native workspace close controls aligned to the trailing edge.
41 changes: 29 additions & 12 deletions apps/docs/ai/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) | 图片生成生命周期 |

## 结构化数据与工作区

Expand All @@ -97,13 +98,29 @@ 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)` |
| `AgentShell` | `placement`、`open`、`title` | `close` |

`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):组合消息历史、流式回答、推理、工具、审批和输入区。
Expand Down
41 changes: 29 additions & 12 deletions apps/docs/en/ai/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -97,13 +98,29 @@ 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)` |
| `AgentShell` | `placement`, `open`, `title` | `close` |

`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.
Expand Down
116 changes: 116 additions & 0 deletions apps/playground-h5/src/agent-rag.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof useRagDemo>
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')
})
})
Loading