From efbe938679fd1fb1382853ac465b6259c19b636b Mon Sep 17 00:00:00 2001 From: gene9831 Date: Wed, 19 Nov 2025 11:38:41 +0800 Subject: [PATCH 01/61] feat(bubble): introduce BubbleItem and BubbleRenderer components for enhanced message handling and rendering --- TODO.md | 11 + docs/src/components/bubble.md | 343 ------------------ packages/components/src/bubble/Bubble.vue | 263 ++++---------- packages/components/src/bubble/BubbleItem.vue | 19 + packages/components/src/bubble/BubbleList.vue | 186 +++++----- .../components/src/bubble/BubbleProvider.vue | 33 +- .../components/src/bubble/BubbleRenderer.vue | 68 ++++ packages/components/src/bubble/DESIGN.md | 336 +++++++++++++++++ packages/components/src/bubble/constants.ts | 8 + packages/components/src/bubble/index.ts | 1 + packages/components/src/bubble/index.type.ts | 202 ++++++++--- packages/components/src/bubble/ren/Box.vue | 45 +++ packages/components/src/bubble/ren/Image.vue | 72 ++++ .../components/src/bubble/ren/ImageBox.vue | 29 ++ packages/components/src/bubble/ren/Text.vue | 27 ++ packages/components/src/index.ts | 6 +- packages/components/src/styles/animations.css | 26 ++ .../src/styles/components/bubble.less | 11 - packages/test2/.gitignore | 24 ++ packages/test2/README.md | 5 + packages/test2/index.html | 13 + packages/test2/package.json | 23 ++ packages/test2/public/vite.svg | 1 + packages/test2/src/App.vue | 109 ++++++ packages/test2/src/assets/vue.svg | 1 + packages/test2/src/components/Avatar.vue | 16 + packages/test2/src/components/HelloWorld.vue | 36 ++ packages/test2/src/icons/UserRound.vue | 28 ++ packages/test2/src/main.ts | 5 + packages/test2/src/style.css | 5 + packages/test2/tsconfig.app.json | 16 + packages/test2/tsconfig.json | 7 + packages/test2/tsconfig.node.json | 26 ++ packages/test2/vite.config.ts | 13 + 34 files changed, 1299 insertions(+), 715 deletions(-) create mode 100644 TODO.md create mode 100644 packages/components/src/bubble/BubbleItem.vue create mode 100644 packages/components/src/bubble/BubbleRenderer.vue create mode 100644 packages/components/src/bubble/DESIGN.md create mode 100644 packages/components/src/bubble/constants.ts create mode 100644 packages/components/src/bubble/ren/Box.vue create mode 100644 packages/components/src/bubble/ren/Image.vue create mode 100644 packages/components/src/bubble/ren/ImageBox.vue create mode 100644 packages/components/src/bubble/ren/Text.vue create mode 100644 packages/test2/.gitignore create mode 100644 packages/test2/README.md create mode 100644 packages/test2/index.html create mode 100644 packages/test2/package.json create mode 100644 packages/test2/public/vite.svg create mode 100644 packages/test2/src/App.vue create mode 100644 packages/test2/src/assets/vue.svg create mode 100644 packages/test2/src/components/Avatar.vue create mode 100644 packages/test2/src/components/HelloWorld.vue create mode 100644 packages/test2/src/icons/UserRound.vue create mode 100644 packages/test2/src/main.ts create mode 100644 packages/test2/src/style.css create mode 100644 packages/test2/tsconfig.app.json create mode 100644 packages/test2/tsconfig.json create mode 100644 packages/test2/tsconfig.node.json create mode 100644 packages/test2/vite.config.ts diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..b13e0072c --- /dev/null +++ b/TODO.md @@ -0,0 +1,11 @@ +# TODO + +- ~~docs 网站资源~~ +- README + +## Bubble 气泡重构 + +- 气泡平铺 +- 添加status状态?? +- 尽量不改变原始数据 +- user 发送的 content 数组类型(多态消息) diff --git a/docs/src/components/bubble.md b/docs/src/components/bubble.md index ca3272a27..090ff5c24 100644 --- a/docs/src/components/bubble.md +++ b/docs/src/components/bubble.md @@ -7,346 +7,3 @@ outline: [1, 3] Bubble 气泡组件用于展示消息气泡,支持流式文本、头像、位置、加载中、终止状态、操作按钮等功能。 ## 代码示例 - -### 基本示例 - -基本示例。使用 `content` 属性设置气泡内容,使用 css 变量 `--tr-bubble-content-bg` 设置气泡内容背景颜色。 - -> 更多 css 变量请参考 [CSS 变量](#css-变量) - - - -### 头像和位置 - -通过 `avatar` 设置自定义头像,通过 `placement` 设置位置,提供了 `start`、`end` 两个选项 - - - -### 气泡形状 - -通过 `shape` 设置气泡形状。目前提供了 `rounded` 和 `corner` 两个选项。默认为 `corner` - - - -### 加载中 - -通过 `loading` 设置加载中状态。或者使用 `loading` 插槽来实现自定义加载中状态 - -BubbleList 除了需要设置 `loading`,还需要设置 `loading-role`。需要注意的是,列表的加载中气泡实际上并没有新增一条消息,`loading` 设置为 `false` 后,加载中的气泡不会渲染 - - - -### 用户停止 - -通过 `aborted` 设置用户停止状态 - - - -### 最大宽度 - -通过 `maxWidth` 设置气泡最大宽度 - - - -### 渲染 markdown - - - -### 流式文本 - -`content` 属性是响应式的,动态设置 `content` 即可实现流式文本 - - - -### 多种消息格式 - -`BubbleProvider` 管理和注册消息渲染器。渲染器注册机制 - -当 Bubble 组件的 `content` 是长度大于0的数组时,系统会: - -1.检查每数组项的 `type` 字段 -2.在 `BubbleProvider` 中查找匹配的渲染器 -3.使用找到的渲染器渲染消息内容 -4.如果未找到匹配的渲染器,则使用默认渲染方式 - -有三种方式可以实现自定义消息渲染器: - -1.**函数式渲染器**: - -```typescript -const myRenderer: BubbleContentFunctionRenderer = (options) => { - return h('div', options.content) -} -``` - -2.**类式渲染器**: - -必须继承 `BubbleContentClassRenderer` 类 - -类渲染器通常用来复用复杂度较高的渲染器,比如MarkdownIt实例 - -```typescript -class MyRenderer extends BubbleContentClassRenderer { - render(options) { - return h('div', options.content) - } -} -``` - -注册时记得 new 一个实例,否则会导致渲染失败 - -```vue - - - -``` - -3.**Vue 组件**: - -content 对象中的所有属性都将传递给组件,onXXX会当作事件传递给组件,非props属性会当作attrs传递给组件 - -```vue - -``` - -目前内置直接可用的的渲染器类型有 - -- `text`(默认渲染器) -- `collapsible-text` -- `tool` - -内置需要自行导入的渲染有 - -- `BubbleMarkdownContentRenderer` 类渲染器 - - - -### 指定渲染属性 - -和大模型交互数据时,交互的原始数据中的 content 字段可能需要经过前端二次处理再展示到UI上,但此时我们又不想改动原始的 content 字段。此时可以通过 `customContentField` 属性来在前端指定你需要渲染的属性 - - - -### 插槽 - -气泡组件提供了四个插槽,分别是 默认插槽, `loading` 插槽、`footer` 插槽 和 `trailer` 插槽 - - - -### schema 卡片渲染 - - - -### 列表 - - - -### 隐藏角色 - -角色配置中使用 `hidden` 来隐藏这个角色的所有消息 - - - -## API - -### BubblePlacement - -气泡位置类型: - -```typescript -type BubblePlacement = 'start' | 'end' -``` - -- `'start'`: 气泡位于左侧/起始位置 -- `'end'`: 气泡位于右侧/结束位置 - -### BubbleCommonProps - -气泡通用属性配置。 - -| 属性 | 类型 | 默认值 | 说明 | -| -------------------- | ----------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| `placement` | `BubblePlacement` | - | 气泡对齐位置 (`'start'` 或 `'end'`) | -| `avatar` | `VNode` | - | 气泡头像部分的自定义 Vue 节点 | -| `shape` | `'rounded' \| 'corner'` | `'corner'` | 气泡形状 | -| `contentRenderer` | `BubbleContentRenderer` | - | 气泡内容渲染器(当 content 是非空数组时无效,使用 BubbleProvider 注册的渲染器) | -| `customContentField` | `string` | - | 自定义气泡内容字段。比如 customContentField 设置为 'my-content',则 Bubble 优先渲染 my-content 属性到气泡内容 | -| `abortedText` | `string` | - | 气泡中止文本 | -| `maxWidth` | `string \| number` | `'(用户停止)'` | 气泡内容的最大宽度 | - -### BubbleProps - -单个气泡的属性配置(继承自 BubbleCommonProps)。 - -| 属性 | 类型 | 默认值 | 说明 | -| --------- | ------------------------------- | ------- | ----------------------------------- | -| `content` | `string \| BubbleContentItem[]` | - | 气泡内容 | -| `id` | `string \| number \| symbol` | - | 气泡唯一标识 | -| `role` | `string` | - | 气泡角色标识,用于关联 `roles` 配置 | -| `loading` | `boolean` | `false` | 是否显示加载状态 | -| `aborted` | `boolean` | `false` | 是否显示为已中止状态 | - ---- - -### BubbleSlots - -气泡组件的插槽定义。 - -| 插槽名 | 参数 | 说明 | -| --------- | ---------------------------------------------- | ------------------------------------ | -| `default` | `{ bubbleProps: BubbleProps; index?: number }` | 默认内容插槽,用于自定义气泡内容 | -| `footer` | `{ bubbleProps: BubbleProps; index?: number }` | 底部插槽,用于在气泡底部添加内容 | -| `loading` | `{ bubbleProps: BubbleProps; index?: number }` | 加载状态插槽,用于自定义加载状态显示 | -| `trailer` | `{ bubbleProps: BubbleProps; index?: number }` | 尾部插槽,用于在气泡内容外部添加内容 | - -### BubbleRoleConfig - -角色配置类型(继承自 BubbleCommonProps)。 - -```ts -type BubbleRoleConfig = BubbleCommonProps & { - hidden?: boolean - slots?: BubbleSlots -} -``` - -### BubbleListProps - -气泡列表组件的属性配置。 - -| 属性 | 类型 | 默认值 | 说明 | -| ------------- | ------------------------------------------- | ------- | ---------------------------- | -| `items` | `(BubbleProps & { slots?: BubbleSlots })[]` | - | **必填**,气泡项数组 | -| `roles` | `Record` | - | 每个角色的默认配置项 | -| `loading` | `boolean` | `false` | 列表是否加载中 | -| `loadingRole` | `string` | - | 指定哪个角色可以有加载中状态 | -| `autoScroll` | `boolean` | `false` | 是否自动滚动到最新内容 | - -### BubbleProviderProps - -```ts -type BubbleProviderProps = { - contentRenderers?: Record -} -``` - -### BubbleContentItem - -单条消息对象的结构。 - -```typescript -interface BubbleContentItem { - type: string - [key: string]: any -} -``` - -| 属性 | 类型 | 说明 | -| --------------- | -------- | ------------------------------------------------ | -| `type` | `string` | 消息类型,用于选择对应的渲染器 | -| `[key: string]` | `any` | 其他字段可自由扩展,用于携带消息所需的自定义数据 | - -### BubbleContentRenderer - -用于渲染气泡消息内容的渲染器类型。 - -```typescript -type BubbleContentRenderer = BubbleContentFunctionRenderer | BubbleContentClassRenderer | Component -``` - -- `BubbleContentFunctionRenderer`: 函数式渲染器,返回 `VNode` -- `BubbleContentClassRenderer`: 基于类的渲染器,需实现 `.render()` 方法 -- `Component`: 任意 Vue 组件,也可以用作渲染器 - -### BubbleContentFunctionRenderer - -函数式消息渲染器: - -```typescript -type BubbleContentFunctionRenderer = (options: { [key: string]: any }) => VNode -``` - -| 参数 | 类型 | 说明 | -| --------- | ------------------------ | ------------------------------------------- | -| `options` | `{ [key: string]: any }` | 与消息类型 (`BubbleContentItem`) 对应的数据 | -| 返回值 | `VNode` | 渲染结果 | - -### BubbleContentClassRenderer - -基于类的消息渲染器: - -```typescript -abstract class BubbleContentClassRenderer { - abstract render(options: { [key: string]: any }): VNode -} -``` - -### CSS 变量 - -#### Bubble 组件变量 - -Bubble 根元素 - -| 变量名 | 说明 | -| ----------------------- | -------------- | -| `--tr-bubble-gap` | 头像与内容间距 | -| `--tr-bubble-max-width` | 气泡最大宽度 | - -avatar 头像 - -| 变量名 | 说明 | -| ------------------------- | -------- | -| `--tr-bubble-avatar-size` | 头像尺寸 | - -content 内容 - -| 变量名 | 说明 | -| ----------------------------------- | --------------------------------------------------- | -| `--tr-bubble-content-bg` | 内容背景色 | -| `--tr-bubble-content-border-radius` | 内容圆角大小 | -| `--tr-bubble-content-box-shadow` | 内容阴影效果 | -| `--tr-bubble-content-padding` | 内容内边距 | -| `--tr-bubble-content-border` | 内容边框样式 | -| `--tr-bubble-content-items-gap` | 内容项之间的间距(仅当 `content` 属性是数组时有效) | - -text 文本(仅当 `content` 属性是字符串时有效) - -| 变量名 | 说明 | -| ------------------------------ | ------------ | -| `--tr-bubble-text-color` | 文本文字颜色 | -| `--tr-bubble-text-font-size` | 文本字号 | -| `--tr-bubble-text-line-height` | 文本行高 | - -loading 加载 - -| 变量名 | 说明 | -| -------------------------- | ------------ | -| `--tr-bubble-loading-size` | 加载图标尺寸 | - -aborted 中止状态 - -| 变量名 | 说明 | -| ------------------------------- | ------------ | -| `--tr-bubble-aborted-color` | 中止文字颜色 | -| `--tr-bubble-aborted-font-size` | 中止文字字号 | - -footer 底部 - -| 变量名 | 说明 | -| --------------------------- | ---------- | -| `--tr-bubble-footer-margin` | 底部外边距 | - -#### BubbleList 容器变量 - -| 变量名 | 说明 | -| -------------------------- | ---------------- | -| `--tr-bubble-list-gap` | 气泡项之间的间距 | -| `--tr-bubble-list-padding` | 容器内边距 | diff --git a/packages/components/src/bubble/Bubble.vue b/packages/components/src/bubble/Bubble.vue index 327278781..04f7eaeec 100644 --- a/packages/components/src/bubble/Bubble.vue +++ b/packages/components/src/bubble/Bubble.vue @@ -1,241 +1,104 @@ + diff --git a/packages/components/src/bubble/BubbleItem.vue b/packages/components/src/bubble/BubbleItem.vue new file mode 100644 index 000000000..39e982330 --- /dev/null +++ b/packages/components/src/bubble/BubbleItem.vue @@ -0,0 +1,19 @@ + + + diff --git a/packages/components/src/bubble/BubbleList.vue b/packages/components/src/bubble/BubbleList.vue index aa33b3ad9..1c10c3445 100644 --- a/packages/components/src/bubble/BubbleList.vue +++ b/packages/components/src/bubble/BubbleList.vue @@ -1,101 +1,125 @@ diff --git a/packages/components/src/bubble/BubbleProvider.vue b/packages/components/src/bubble/BubbleProvider.vue index 55a11f1b2..dca1a0080 100644 --- a/packages/components/src/bubble/BubbleProvider.vue +++ b/packages/components/src/bubble/BubbleProvider.vue @@ -1,35 +1,4 @@ - + @@ -81,7 +81,7 @@ const rendererMessages = computed(() => { @@ -21,7 +21,7 @@ const props = defineProps() .tr-bubble__box { background-color: var(--tr-bubble-box-bg, var(--tr-container-bg-default)); padding: var(--tr-bubble-box-padding, 8px 16px); - border-radius: var(--tr-bubble-box-border-radius, 16px); + border-radius: var(--tr-bubble-box-border-radius, 0); box-shadow: var(--tr-bubble-box-shadow, var(--tr-shadow-md)); border: var(--tr-bubble-box-border, none); overflow: hidden; diff --git a/packages/components/src/bubble/ren/Image.vue b/packages/components/src/bubble/ren/Image.vue index a744a5ef7..a4e685aef 100644 --- a/packages/components/src/bubble/ren/Image.vue +++ b/packages/components/src/bubble/ren/Image.vue @@ -63,10 +63,7 @@ const handleError = () => { .tr-bubble__image { border: var(--tr-bubble-image-embedded-border, 1px solid rgba(0, 0, 0, 0.04)); border-radius: var(--tr-bubble-image-embedded-border-radius, 4px); - - &:last-child { - display: inline-block; - } + margin-block: var(--tr-bubble-image-embedded-margin-block, 4px); } } diff --git a/packages/components/src/bubble/ren/Reasoning.vue b/packages/components/src/bubble/ren/Reasoning.vue new file mode 100644 index 000000000..8e8b60add --- /dev/null +++ b/packages/components/src/bubble/ren/Reasoning.vue @@ -0,0 +1,26 @@ + + + diff --git a/packages/components/src/bubble/ren/Text.vue b/packages/components/src/bubble/ren/Text.vue index 7334620a1..af3cb7105 100644 --- a/packages/components/src/bubble/ren/Text.vue +++ b/packages/components/src/bubble/ren/Text.vue @@ -1,8 +1,8 @@ + + + + diff --git a/packages/components/src/bubble/ren/renderers.ts b/packages/components/src/bubble/ren/renderers.ts new file mode 100644 index 000000000..c472414dd --- /dev/null +++ b/packages/components/src/bubble/ren/renderers.ts @@ -0,0 +1,56 @@ +import { Component } from 'vue' +import type { BubbleRendererMessage, BubbleRendererProps } from '../index.type' +import Box from './Box.vue' +import Image from './Image.vue' +import ImageBox from './ImageBox.vue' +import Reasoning from './Reasoning.vue' +import Text from './Text.vue' + +const boxRendererMatches: Array<{ + find: (props: BubbleRendererProps) => boolean + renderer: Component +}> = [ + { + find: (props: BubbleRendererProps) => + props.messages.length === 1 && + typeof props.messages[0].content === 'object' && + props.messages[0].content?.type === 'image_url', + renderer: ImageBox, + }, +] + +const contentRendererMatches: Array<{ + find: (message: BubbleRendererMessage) => boolean + renderer: Component +}> = [ + { + find: (message) => typeof message.content === 'object' && message.content?.type === 'image_url', + renderer: Image, + }, + { + find: (message) => typeof message.reasoning_content === 'string', + renderer: Reasoning, + }, + { + find: (message) => typeof message.content === 'string', + renderer: Text, + }, +] + +export const getBoxRenderer = (props: BubbleRendererProps) => { + const foundRenderer = boxRendererMatches.find((match) => match.find(props))?.renderer + if (!foundRenderer) { + return Box + } + return foundRenderer +} + +export const getContentRenderer = (message: BubbleRendererMessage) => { + const foundRenderer = contentRendererMatches.find((renderer) => renderer.find(message))?.renderer + + if (!foundRenderer) { + return Text + } + + return foundRenderer +} diff --git a/packages/svgs/src/assets/atom.svg b/packages/svgs/src/assets/atom.svg new file mode 100644 index 000000000..7f15124da --- /dev/null +++ b/packages/svgs/src/assets/atom.svg @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/packages/test2/package.json b/packages/test2/package.json index bc2b4e29a..f50229f9e 100644 --- a/packages/test2/package.json +++ b/packages/test2/package.json @@ -10,7 +10,8 @@ }, "dependencies": { "@opentiny/tiny-robot": "workspace:*", - "vue": "^3.5.24" + "vue": "^3.5.24", + "vue-router": "^4.5.0" }, "devDependencies": { "@types/node": "^24.10.0", diff --git a/packages/test2/src/App.vue b/packages/test2/src/App.vue index 7b8d43668..f59faef8a 100644 --- a/packages/test2/src/App.vue +++ b/packages/test2/src/App.vue @@ -1,109 +1,42 @@ - + - - diff --git a/packages/test2/src/components/Avatar.vue b/packages/test2/src/demos/Avatar.vue similarity index 100% rename from packages/test2/src/components/Avatar.vue rename to packages/test2/src/demos/Avatar.vue diff --git a/packages/test2/src/demos/basic.vue b/packages/test2/src/demos/basic.vue new file mode 100644 index 000000000..08d9a16ab --- /dev/null +++ b/packages/test2/src/demos/basic.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/packages/test2/src/demos/list.vue b/packages/test2/src/demos/list.vue new file mode 100644 index 000000000..16785a73b --- /dev/null +++ b/packages/test2/src/demos/list.vue @@ -0,0 +1,106 @@ + + + + + + + diff --git a/packages/test2/src/main.ts b/packages/test2/src/main.ts index 2425c0f74..7b1991224 100644 --- a/packages/test2/src/main.ts +++ b/packages/test2/src/main.ts @@ -1,5 +1,6 @@ import { createApp } from 'vue' import './style.css' import App from './App.vue' +import router from './router' -createApp(App).mount('#app') +createApp(App).use(router).mount('#app') diff --git a/packages/test2/src/router.ts b/packages/test2/src/router.ts new file mode 100644 index 000000000..db7388bf8 --- /dev/null +++ b/packages/test2/src/router.ts @@ -0,0 +1,23 @@ +import { createRouter, createWebHistory } from 'vue-router' +import Basic from './demos/basic.vue' +import List from './demos/list.vue' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + redirect: '/basic', + }, + { + path: '/basic', + component: Basic, + }, + { + path: '/list', + component: List, + }, + ], +}) + +export default router diff --git a/packages/test2/src/style.css b/packages/test2/src/style.css index d6c9d9fdd..dedb1a8c7 100644 --- a/packages/test2/src/style.css +++ b/packages/test2/src/style.css @@ -3,3 +3,7 @@ *::before { box-sizing: border-box; } + +body { + margin: 0; +} From 217dc20d90580ee670502193cd95ed05964d3f1e Mon Sep 17 00:00:00 2001 From: gene9831 Date: Thu, 27 Nov 2025 16:46:06 +0800 Subject: [PATCH 03/61] feat(bubble): replace BubbleRenderer with BubbleContent for improved message rendering and context management --- packages/components/src/bubble/Bubble.vue | 13 +++++-- .../components/src/bubble/BubbleContent.vue | 22 +++++++++++ .../components/src/bubble/BubbleRenderer.vue | 17 --------- packages/components/src/bubble/DESIGN.md | 21 ++++++----- packages/components/src/bubble/constants.ts | 8 +++- packages/components/src/bubble/index.type.ts | 4 +- ...ggleContent.vue => CollapsibleContent.vue} | 37 ++++++++++++++----- .../components/src/bubble/ren/Reasoning.vue | 18 ++++++--- .../components/src/bubble/ren/renderers.ts | 18 +++++---- packages/test2/src/demos/basic.vue | 4 +- 10 files changed, 106 insertions(+), 56 deletions(-) create mode 100644 packages/components/src/bubble/BubbleContent.vue delete mode 100644 packages/components/src/bubble/BubbleRenderer.vue rename packages/components/src/bubble/ren/{ToggleContent.vue => CollapsibleContent.vue} (73%) diff --git a/packages/components/src/bubble/Bubble.vue b/packages/components/src/bubble/Bubble.vue index 79ea2607c..baf21d742 100644 --- a/packages/components/src/bubble/Bubble.vue +++ b/packages/components/src/bubble/Bubble.vue @@ -1,8 +1,9 @@ + + diff --git a/packages/components/src/bubble/BubbleRenderer.vue b/packages/components/src/bubble/BubbleRenderer.vue deleted file mode 100644 index 6e06c4ae6..000000000 --- a/packages/components/src/bubble/BubbleRenderer.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/packages/components/src/bubble/DESIGN.md b/packages/components/src/bubble/DESIGN.md index fa8a6a0d2..cde6c6a29 100644 --- a/packages/components/src/bubble/DESIGN.md +++ b/packages/components/src/bubble/DESIGN.md @@ -11,19 +11,20 @@ Bubble 组件系列用于构建 AI 对话界面的消息气泡,支持文本和 ```txt BubbleList (消息流管理) └─ BubbleItem (分组提供) - └─ Bubble (气泡渲染) - └─ BubbleRenderer (内容分发) - ├─ Box / ImageBox (容器) - ├─ Text (文本渲染器) - └─ Image (图片渲染器) + └─ Bubble (气泡渲染 + 内容分发) + ├─ Box / ImageBox (容器) + │ └─ BubbleContent (消息提供) + │ ├─ Text (文本渲染器) + │ ├─ Image (图片渲染器) + │ └─ Reasoning (推理渲染器) ``` **职责划分:** - `BubbleList`: 管理消息数组,根据策略分组 - `BubbleItem`: 通过 provide/inject 传递分组数据 -- `Bubble`: 渲染气泡外观(头像、容器),处理多态消息展开 -- `BubbleRenderer`: 根据内容类型选择渲染器和容器 +- `Bubble`: 渲染气泡外观(头像、容器),处理多态消息展开,选择内容渲染器 +- `BubbleContent`: 为每个消息 provide 上下文,使子孙组件可直接访问和修改消息数据 ### 消息类型 @@ -230,8 +231,8 @@ defineProps<{ content: { type: 'audio', audio_url: string } }>() ``` ```typescript -// BubbleRenderer.vue -import Audio from './ren/Audio.vue' +// ren/renderers.ts +import Audio from './Audio.vue' const contentRendererMatches = [ { @@ -245,7 +246,7 @@ const contentRendererMatches = [ ### 添加新容器渲染器 ```typescript -// BubbleRenderer.vue +// ren/renderers.ts const boxRendererMatches = [ { find: (props) => props.messages.length === 1 && props.messages[0].content?.type === 'video', diff --git a/packages/components/src/bubble/constants.ts b/packages/components/src/bubble/constants.ts index 0af33df54..df9378169 100644 --- a/packages/components/src/bubble/constants.ts +++ b/packages/components/src/bubble/constants.ts @@ -1,8 +1,14 @@ import { InjectionKey } from 'vue' -import { BubbleMessageGroup } from './index.type' +import { BubbleMessageGroup, BubbleRendererMessage } from './index.type' /** * Injection key for bubble message group * Used to provide/inject message group between BubbleItem and Bubble components */ export const BUBBLE_MESSAGE_GROUP_KEY: InjectionKey = Symbol('bubble-message-group') + +/** + * Injection key for bubble content message + * Used to provide/inject current message between BubbleContent and renderer components + */ +export const BUBBLE_CONTENT_MESSAGE_KEY: InjectionKey = Symbol('bubble-content-message') diff --git a/packages/components/src/bubble/index.type.ts b/packages/components/src/bubble/index.type.ts index 8c23e2679..c43604647 100644 --- a/packages/components/src/bubble/index.type.ts +++ b/packages/components/src/bubble/index.type.ts @@ -191,8 +191,8 @@ export type BubbleRendererMessage< } /** - * 渲染器组件 Props + * ContentBox 组件 Props */ -export type BubbleRendererProps = BubbleBoxProps & { +export type BubbleContentBoxProps = BubbleBoxProps & { messages: BubbleRendererMessage[] } diff --git a/packages/components/src/bubble/ren/ToggleContent.vue b/packages/components/src/bubble/ren/CollapsibleContent.vue similarity index 73% rename from packages/components/src/bubble/ren/ToggleContent.vue rename to packages/components/src/bubble/ren/CollapsibleContent.vue index 02acb454d..97a464a4d 100644 --- a/packages/components/src/bubble/ren/ToggleContent.vue +++ b/packages/components/src/bubble/ren/CollapsibleContent.vue @@ -1,23 +1,42 @@ diff --git a/packages/components/src/bubble/ren/Tool.vue b/packages/components/src/bubble/ren/Tool.vue new file mode 100644 index 000000000..754f86a62 --- /dev/null +++ b/packages/components/src/bubble/ren/Tool.vue @@ -0,0 +1,225 @@ + + + + + + + diff --git a/packages/components/src/bubble/ren/Tools.vue b/packages/components/src/bubble/ren/Tools.vue new file mode 100644 index 000000000..a53ecc939 --- /dev/null +++ b/packages/components/src/bubble/ren/Tools.vue @@ -0,0 +1,18 @@ + + + diff --git a/packages/components/src/bubble/ren/renderers.ts b/packages/components/src/bubble/ren/renderers.ts index 206f25052..1cfada539 100644 --- a/packages/components/src/bubble/ren/renderers.ts +++ b/packages/components/src/bubble/ren/renderers.ts @@ -1,10 +1,11 @@ import { Component } from 'vue' -import type { BubbleRendererMessage, BubbleContentBoxProps } from '../index.type' +import type { BubbleContentBoxProps, BubbleRendererMessage } from '../index.type' import Box from './Box.vue' import Image from './Image.vue' import ImageBox from './ImageBox.vue' import Reasoning from './Reasoning.vue' import Text from './Text.vue' +import Tools from './Tools.vue' const boxRendererMatches: Array<{ find: (props: BubbleContentBoxProps) => boolean @@ -33,8 +34,8 @@ const contentRendererMatches: Array<{ renderer: Reasoning, }, { - find: (message) => typeof message.content === 'string', - renderer: Text, + find: (message) => Array.isArray(message.tool_calls) && message.tool_calls.length > 0, + renderer: Tools, }, ] diff --git a/packages/test2/src/App.vue b/packages/test2/src/App.vue index f59faef8a..e0a077d85 100644 --- a/packages/test2/src/App.vue +++ b/packages/test2/src/App.vue @@ -4,7 +4,7 @@
diff --git a/packages/test2/src/demos/basic.vue b/packages/test2/src/demos/basic.vue deleted file mode 100644 index fa9d247bc..000000000 --- a/packages/test2/src/demos/basic.vue +++ /dev/null @@ -1,104 +0,0 @@ - - - - - diff --git a/packages/test2/src/demos/bubble.vue b/packages/test2/src/demos/bubble.vue new file mode 100644 index 000000000..3af08d55b --- /dev/null +++ b/packages/test2/src/demos/bubble.vue @@ -0,0 +1,34 @@ + + + + + + + diff --git a/packages/test2/src/demos/bubble/PolymorphicMessage.vue b/packages/test2/src/demos/bubble/PolymorphicMessage.vue new file mode 100644 index 000000000..efb862593 --- /dev/null +++ b/packages/test2/src/demos/bubble/PolymorphicMessage.vue @@ -0,0 +1,34 @@ + + + diff --git a/packages/test2/src/demos/bubble/ReasoningMessage.vue b/packages/test2/src/demos/bubble/ReasoningMessage.vue new file mode 100644 index 000000000..546395429 --- /dev/null +++ b/packages/test2/src/demos/bubble/ReasoningMessage.vue @@ -0,0 +1,63 @@ + + + diff --git a/packages/test2/src/demos/bubble/TextMessage.vue b/packages/test2/src/demos/bubble/TextMessage.vue new file mode 100644 index 000000000..e2c5c78fd --- /dev/null +++ b/packages/test2/src/demos/bubble/TextMessage.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/test2/src/demos/bubble/ToolCallMessage.vue b/packages/test2/src/demos/bubble/ToolCallMessage.vue new file mode 100644 index 000000000..56bcfa97d --- /dev/null +++ b/packages/test2/src/demos/bubble/ToolCallMessage.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/test2/src/router.ts b/packages/test2/src/router.ts index db7388bf8..b484ffae9 100644 --- a/packages/test2/src/router.ts +++ b/packages/test2/src/router.ts @@ -1,5 +1,5 @@ import { createRouter, createWebHistory } from 'vue-router' -import Basic from './demos/basic.vue' +import Bubble from './demos/bubble.vue' import List from './demos/list.vue' const router = createRouter({ @@ -7,11 +7,11 @@ const router = createRouter({ routes: [ { path: '/', - redirect: '/basic', + redirect: '/bubble', }, { - path: '/basic', - component: Basic, + path: '/bubble', + component: Bubble, }, { path: '/list', From a0d860e49efed9c31af30f048e062fe6371767fa Mon Sep 17 00:00:00 2001 From: gene9831 Date: Mon, 1 Dec 2025 16:16:04 +0800 Subject: [PATCH 05/61] feat(bubble): refactor bubble components to utilize new composables for renderer matching and enhance loading state handling --- packages/components/src/bubble/Bubble.vue | 10 +-- packages/components/src/bubble/BubbleBox.vue | 18 ++++++ .../components/src/bubble/BubbleContent.vue | 6 +- .../components/src/bubble/BubbleProvider.vue | 44 ++++++++++++- .../composables/useBubbleBoxRenderer.ts | 29 +++++++++ .../composables/useBubbleContentRenderer.ts | 32 ++++++++++ packages/components/src/bubble/constants.ts | 22 ++++++- packages/components/src/bubble/index.type.ts | 34 +++++++---- .../components/src/bubble/ren/Loading.vue | 24 ++++++++ .../components/src/bubble/ren/Reasoning.vue | 7 +-- packages/components/src/bubble/ren/Tools.vue | 11 ++-- .../src/bubble/ren/defaultRenderers.ts | 47 ++++++++++++++ .../components/src/bubble/ren/renderers.ts | 61 ------------------- packages/test2/src/demos/bubble.vue | 5 +- .../src/demos/bubble/LoadingMessage.vue.vue | 20 ++++++ packages/test2/src/demos/list.vue | 36 +---------- 16 files changed, 278 insertions(+), 128 deletions(-) create mode 100644 packages/components/src/bubble/BubbleBox.vue create mode 100644 packages/components/src/bubble/composables/useBubbleBoxRenderer.ts create mode 100644 packages/components/src/bubble/composables/useBubbleContentRenderer.ts create mode 100644 packages/components/src/bubble/ren/Loading.vue create mode 100644 packages/components/src/bubble/ren/defaultRenderers.ts delete mode 100644 packages/components/src/bubble/ren/renderers.ts create mode 100644 packages/test2/src/demos/bubble/LoadingMessage.vue.vue diff --git a/packages/components/src/bubble/Bubble.vue b/packages/components/src/bubble/Bubble.vue index baf21d742..516f37a60 100644 --- a/packages/components/src/bubble/Bubble.vue +++ b/packages/components/src/bubble/Bubble.vue @@ -1,9 +1,9 @@ + + diff --git a/packages/components/src/bubble/BubbleContent.vue b/packages/components/src/bubble/BubbleContent.vue index 117367354..e99317def 100644 --- a/packages/components/src/bubble/BubbleContent.vue +++ b/packages/components/src/bubble/BubbleContent.vue @@ -1,8 +1,8 @@ diff --git a/packages/components/src/bubble/BubbleProvider.vue b/packages/components/src/bubble/BubbleProvider.vue index dca1a0080..e7b42db79 100644 --- a/packages/components/src/bubble/BubbleProvider.vue +++ b/packages/components/src/bubble/BubbleProvider.vue @@ -1,4 +1,46 @@ - + - - diff --git a/packages/components/src/bubble/renderers/ImageBox.vue b/packages/components/src/bubble/renderers/ImageBox.vue index 5999f62a5..773426904 100644 --- a/packages/components/src/bubble/renderers/ImageBox.vue +++ b/packages/components/src/bubble/renderers/ImageBox.vue @@ -13,17 +13,13 @@ const props = defineProps() diff --git a/packages/components/src/bubble/renderers/Loading.vue b/packages/components/src/bubble/renderers/Loading.vue index f81d3b669..26238020b 100644 --- a/packages/components/src/bubble/renderers/Loading.vue +++ b/packages/components/src/bubble/renderers/Loading.vue @@ -14,11 +14,11 @@ defineProps() diff --git a/packages/components/src/bubble/renderers/Reasoning.vue b/packages/components/src/bubble/renderers/Reasoning.vue index fe2e21970..db64fca28 100644 --- a/packages/components/src/bubble/renderers/Reasoning.vue +++ b/packages/components/src/bubble/renderers/Reasoning.vue @@ -1,11 +1,11 @@ - - -``` - -### 场景 7:多角色对话 - -```vue - -``` - ---- - -## 分组策略详解 - -### Consecutive 策略 - -连续相同角色的消息合并为一组: - -```javascript -Input: [ - { role: 'user', content: 'A' }, - { role: 'user', content: 'B' }, - { role: 'assistant', content: 'C' }, -] - -Output: [ - { role: 'user', messages: ['A', 'B'], isPolymorphic: false }, - { role: 'assistant', messages: ['C'], isPolymorphic: false }, -] -``` - -### Divider 策略 - -按分割角色分组(默认 `user`): - -```javascript -Input: [ - { role: 'user', content: 'Q1' }, - { role: 'user', content: 'Q2' }, - { role: 'assistant', content: 'A1' }, - { role: 'assistant', content: 'A2' }, - { role: 'user', content: 'Q3' }, -] - -Output: [ - { role: 'user', messages: ['Q1', 'Q2'], isPolymorphic: false }, - { role: 'assistant', messages: ['A1', 'A2'], isPolymorphic: false }, - { role: 'user', messages: ['Q3'], isPolymorphic: false }, -] -``` - -### 多态消息的特殊处理 - -数组 `content` 会创建密封分组: - -```javascript -Input: [ - { role: 'user', content: 'Text 1' }, - { role: 'user', content: [{ type: 'text', text: 'Text 2' }] }, - { role: 'user', content: 'Text 3' } -] - -Output: [ - { role: 'user', messages: ['Text 1'], isPolymorphic: false }, - { role: 'user', messages: [[...]], isPolymorphic: true }, // 密封 - { role: 'user', messages: ['Text 3'], isPolymorphic: false } -] -``` - ---- - -## 渲染器扩展 - -### 添加新内容渲染器 - -```vue - - - - -``` - -```typescript -// 在业务侧通过 BubbleProvider 注入匹配项 -import { markRaw } from 'vue' -import Audio from './renderers/Audio.vue' - -const audioRendererMatches = [ - { - find: (message) => message.content?.type === 'audio', - renderer: markRaw(Audio), - priority: 5, - }, -] -``` - -```vue - - - -``` - -`BubbleProvider` 会将自定义匹配与默认匹配合并,并按 `priority` 从小到大排序,当 `find` 返回 `true` 时使用对应渲染器;若无匹配则回退到 `fallback-content-renderer`。 - -### 添加新容器渲染器 - -```typescript -import { markRaw } from 'vue' -import VideoBox from './renderers/VideoBox.vue' - -const videoBoxMatches = [ - { - find: (props) => props.messages.length === 1 && props.messages[0].content?.type === 'video', - renderer: markRaw(VideoBox), - priority: 5, - }, -] -``` - -```vue - - - -``` - -同理,可以通过 `fallback-box-renderer` 覆盖容器回退渲染器。 From d7ea95971061271a18ec095aed19ddbdece7a43b Mon Sep 17 00:00:00 2001 From: gene9831 Date: Tue, 9 Dec 2025 15:10:14 +0800 Subject: [PATCH 12/61] feat(bubble): integrate markdown rendering support with DOMPurify for enhanced content safety --- packages/components/package.json | 8 ++- .../components/src/bubble/BubbleProvider.vue | 5 +- packages/components/src/bubble/index.ts | 1 + packages/components/src/bubble/index.type.ts | 1 + .../src/bubble/renderers/Markdown.vue | 58 +++++++++++++++++++ .../components/src/bubble/renderers/Tool.vue | 12 +--- packages/components/src/bubble/utils.ts | 29 ++++++++++ packages/components/src/index.ts | 4 +- .../src/styles/components/bubble.less | 2 + packages/components/vite.config.ts | 2 +- 10 files changed, 106 insertions(+), 16 deletions(-) create mode 100644 packages/components/src/bubble/renderers/Markdown.vue create mode 100644 packages/components/src/bubble/utils.ts diff --git a/packages/components/package.json b/packages/components/package.json index e9ede3240..6c30f3eba 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -17,15 +17,15 @@ "type-check": "vue-tsc --noEmit" }, "peerDependencies": { + "dompurify": "^3.3.1", + "markdown-it": "^14.1.0", "vue": "^3.3.11" }, "dependencies": { "@opentiny/tiny-robot-svgs": "workspace:*", "@opentiny/vue": "^3.20.0", "@vueuse/core": "^13.1.0", - "dompurify": "^3.2.6", - "jsonrepair": "^3.13.1", - "markdown-it": "^14.1.0" + "jsonrepair": "^3.13.1" }, "devDependencies": { "@types/dom-speech-recognition": "^0.0.6", @@ -34,7 +34,9 @@ "@types/node": "^22.13.17", "@vitejs/plugin-vue": "^4.5.2", "@vitejs/plugin-vue-jsx": "^4.1.2", + "dompurify": "^3.3.1", "less": "^4.2.2", + "markdown-it": "^14.1.0", "typescript": "^5.2.2", "vite": "^5.0.8", "vite-plugin-dts": "^4.5.3", diff --git a/packages/components/src/bubble/BubbleProvider.vue b/packages/components/src/bubble/BubbleProvider.vue index 747c82c1f..ae8f849e6 100644 --- a/packages/components/src/bubble/BubbleProvider.vue +++ b/packages/components/src/bubble/BubbleProvider.vue @@ -1,7 +1,6 @@ + + + + diff --git a/packages/components/src/bubble/renderers/Tool.vue b/packages/components/src/bubble/renderers/Tool.vue index 5dff4ffbb..80fcaaf87 100644 --- a/packages/components/src/bubble/renderers/Tool.vue +++ b/packages/components/src/bubble/renderers/Tool.vue @@ -3,6 +3,7 @@ import { IconArrowDown, IconCancelled, IconError, IconLoading, IconPlugin } from import { type Component, computed, ref, useCssModule, watchEffect } from 'vue' import { useBubbleContentMessage, useBubbleStore } from '../composables' import { BubbleRendererMessage, ToolCall } from '../index.type' +import { getJsonrepair } from '../utils' const toolCallStatus = ['running', 'success', 'failed', 'cancelled'] as const type ToolCallStatus = (typeof toolCallStatus)[number] @@ -72,15 +73,6 @@ const highlightJSON = (json: T, space = 2): string => const detail = ref('') -// Lazy load jsonrepair module (only import once, cached by module system) -let jsonrepairPromise: Promise | null = null -const getJsonrepair = () => { - if (!jsonrepairPromise) { - jsonrepairPromise = import('jsonrepair') - } - return jsonrepairPromise -} - const store = useBubbleStore<{ toolCallResult?: Record }>() const toolCallResult = computed(() => { const toolCallId = props.extras?.tool_call.id @@ -157,6 +149,8 @@ const handleClick = () => { background-color: var(--tr-container-bg-default-2); border-radius: 12px; margin-block: var(--tr-bubble-tool-call-space-y); + min-width: var(--tr-bubble-tool-call-min-width); + max-width: var(--tr-bubble-tool-call-max-width); &:first-child { margin-top: 0; diff --git a/packages/components/src/bubble/utils.ts b/packages/components/src/bubble/utils.ts new file mode 100644 index 000000000..dfbb72072 --- /dev/null +++ b/packages/components/src/bubble/utils.ts @@ -0,0 +1,29 @@ +import type DOMPurify from 'dompurify' +import type MarkdownIt from 'markdown-it' + +let jsonrepairPromise: Promise | null = null + +export const getJsonrepair = () => { + if (!jsonrepairPromise) { + jsonrepairPromise = import('jsonrepair') + } + return jsonrepairPromise +} + +let markdownItAndDompurify: { markdown: typeof MarkdownIt; dompurify: typeof DOMPurify } | null = null + +export const getMarkdownItAndDompurify = async () => { + if (markdownItAndDompurify) { + return markdownItAndDompurify + } + + try { + const [md, dompurify] = await Promise.all([import('markdown-it'), import('dompurify')]) + markdownItAndDompurify = { markdown: md.default, dompurify: dompurify.default } + return markdownItAndDompurify + } catch { + console.warn('[BubbleMarkdownRenderer] install markdown-it and dompurify to use markdown renderer') + markdownItAndDompurify = null + return null + } +} diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 62e8690e3..a7b41a2e7 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -3,7 +3,7 @@ import './styles/components/index.css' import { App } from 'vue' import Attachments from './attachments' -import { Bubble, BubbleList, BubbleProvider, BubbleBasicBox } from './bubble' +import { Bubble, BubbleList, BubbleProvider, BubbleBasicBox, BubbleMarkdownRenderer } from './bubble' import Container from './container' import Conversations from './conversations' import DragOverlay from './drag-overlay' @@ -85,6 +85,8 @@ export { BubbleProvider as TrBubbleProvider, BubbleBasicBox, BubbleBasicBox as TrBubbleBasicBox, + BubbleMarkdownRenderer, + BubbleMarkdownRenderer as TrBubbleMarkdownRenderer, Container, Container as TrContainer, Conversations, diff --git a/packages/components/src/styles/components/bubble.less b/packages/components/src/styles/components/bubble.less index 37b2cb8f9..e90cb3905 100644 --- a/packages/components/src/styles/components/bubble.less +++ b/packages/components/src/styles/components/bubble.less @@ -33,6 +33,8 @@ image-embedded-margin-block: 4px; // Tool renderer tool-call-space-y: 8px; + tool-call-min-width: unset; + tool-call-max-width: unset; }; :root, diff --git a/packages/components/vite.config.ts b/packages/components/vite.config.ts index 6dc74347d..d05fff92c 100644 --- a/packages/components/vite.config.ts +++ b/packages/components/vite.config.ts @@ -43,7 +43,7 @@ export default defineConfig({ }, minify: true, rollupOptions: { - external: ['vue', 'vue-router', '@opentiny/vue', '@opentiny/tiny-robot-svgs'], + external: ['vue', 'vue-router', '@opentiny/vue', '@opentiny/tiny-robot-svgs', 'markdown-it', 'dompurify'], input: entries, output: { format: 'es', From 2590f0e0aa1d88cce1844763fd17a5af32128dea Mon Sep 17 00:00:00 2001 From: gene9831 Date: Tue, 9 Dec 2025 15:12:24 +0800 Subject: [PATCH 13/61] chore: initialize test2 package with Vue 3, TypeScript, and Vite setup, including demo components and routing --- .gitignore | 2 - TODO.md | 2 + packages/components/package.json | 2 +- packages/kit/package.json | 2 +- packages/svgs/package.json | 2 +- packages/test2/.gitignore | 24 ++ packages/test2/README.md | 5 + packages/test2/index.html | 13 + packages/test2/package.json | 32 +++ packages/test2/public/vite.svg | 1 + packages/test2/src/App.vue | 43 +++ packages/test2/src/assets/vue.svg | 1 + packages/test2/src/components/HelloWorld.vue | 36 +++ packages/test2/src/demos/Avatar.vue | 16 ++ packages/test2/src/demos/bubble.vue | 39 +++ packages/test2/src/demos/bubble/Hidden.vue | 20 ++ packages/test2/src/demos/bubble/Loading.vue | 20 ++ .../test2/src/demos/bubble/Polymorphic.vue | 42 +++ packages/test2/src/demos/bubble/Reasoning.vue | 64 +++++ packages/test2/src/demos/bubble/Slots.vue | 54 ++++ packages/test2/src/demos/bubble/Text.vue | 45 ++++ packages/test2/src/demos/bubble/ToolCall.vue | 78 ++++++ packages/test2/src/demos/list.vue | 251 ++++++++++++++++++ packages/test2/src/demos/test.vue | 31 +++ packages/test2/src/icons/UserRound.vue | 28 ++ packages/test2/src/main.ts | 6 + packages/test2/src/router.ts | 28 ++ packages/test2/src/style.css | 11 + packages/test2/tsconfig.app.json | 16 ++ packages/test2/tsconfig.json | 7 + packages/test2/tsconfig.node.json | 26 ++ packages/test2/vite.config.ts | 13 + 32 files changed, 955 insertions(+), 5 deletions(-) create mode 100644 packages/test2/.gitignore create mode 100644 packages/test2/README.md create mode 100644 packages/test2/index.html create mode 100644 packages/test2/package.json create mode 100644 packages/test2/public/vite.svg create mode 100644 packages/test2/src/App.vue create mode 100644 packages/test2/src/assets/vue.svg create mode 100644 packages/test2/src/components/HelloWorld.vue create mode 100644 packages/test2/src/demos/Avatar.vue create mode 100644 packages/test2/src/demos/bubble.vue create mode 100644 packages/test2/src/demos/bubble/Hidden.vue create mode 100644 packages/test2/src/demos/bubble/Loading.vue create mode 100644 packages/test2/src/demos/bubble/Polymorphic.vue create mode 100644 packages/test2/src/demos/bubble/Reasoning.vue create mode 100644 packages/test2/src/demos/bubble/Slots.vue create mode 100644 packages/test2/src/demos/bubble/Text.vue create mode 100644 packages/test2/src/demos/bubble/ToolCall.vue create mode 100644 packages/test2/src/demos/list.vue create mode 100644 packages/test2/src/demos/test.vue create mode 100644 packages/test2/src/icons/UserRound.vue create mode 100644 packages/test2/src/main.ts create mode 100644 packages/test2/src/router.ts create mode 100644 packages/test2/src/style.css create mode 100644 packages/test2/tsconfig.app.json create mode 100644 packages/test2/tsconfig.json create mode 100644 packages/test2/tsconfig.node.json create mode 100644 packages/test2/vite.config.ts diff --git a/.gitignore b/.gitignore index f4f3c8f94..462db155f 100644 --- a/.gitignore +++ b/.gitignore @@ -43,5 +43,3 @@ pnpm-lock.yaml yarn.lock packages/svgs/src/components cui.config.js - -packages/test2 diff --git a/TODO.md b/TODO.md index b0593470b..d3ecbe895 100644 --- a/TODO.md +++ b/TODO.md @@ -16,3 +16,5 @@ ## 对话套件 ## Bubble markdown优化 + +- useConversation 支持后台多会话 diff --git a/packages/components/package.json b/packages/components/package.json index 6c30f3eba..a388811f7 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@opentiny/tiny-robot", - "version": "0.3.0", + "version": "0.4.0-alpha.0", "publishConfig": { "access": "public" }, diff --git a/packages/kit/package.json b/packages/kit/package.json index 4ebed05bd..4588823aa 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -1,6 +1,6 @@ { "name": "@opentiny/tiny-robot-kit", - "version": "0.3.0", + "version": "0.4.0-alpha.0", "publishConfig": { "access": "public" }, diff --git a/packages/svgs/package.json b/packages/svgs/package.json index 6ec9b67a1..889e6c618 100644 --- a/packages/svgs/package.json +++ b/packages/svgs/package.json @@ -1,6 +1,6 @@ { "name": "@opentiny/tiny-robot-svgs", - "version": "0.3.0", + "version": "0.4.0-alpha.0", "publishConfig": { "access": "public" }, diff --git a/packages/test2/.gitignore b/packages/test2/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/packages/test2/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/test2/README.md b/packages/test2/README.md new file mode 100644 index 000000000..33895ab20 --- /dev/null +++ b/packages/test2/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/packages/test2/package.json b/packages/test2/package.json new file mode 100644 index 000000000..dd717d145 --- /dev/null +++ b/packages/test2/package.json @@ -0,0 +1,32 @@ +{ + "name": "test2", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@opentiny/tiny-robot": "workspace:*", + "dompurify": "^3.3.1", + "github-markdown-css": "^5.8.1", + "katex": "^0.16.25", + "markdown-it": "^14.1.0", + "markstream-vue": "0.0.2-beta.7", + "mermaid": "^11.12.1", + "shiki": "^3.17.1", + "stream-markdown": "^0.0.10", + "vue": "^3.5.24", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@types/node": "^24.10.0", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.8.1", + "typescript": "~5.9.3", + "vite": "^7.2.2", + "vue-tsc": "^3.1.3" + } +} diff --git a/packages/test2/public/vite.svg b/packages/test2/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/packages/test2/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/test2/src/App.vue b/packages/test2/src/App.vue new file mode 100644 index 000000000..fcc5ee2b7 --- /dev/null +++ b/packages/test2/src/App.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/packages/test2/src/assets/vue.svg b/packages/test2/src/assets/vue.svg new file mode 100644 index 000000000..770e9d333 --- /dev/null +++ b/packages/test2/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/test2/src/components/HelloWorld.vue b/packages/test2/src/components/HelloWorld.vue new file mode 100644 index 000000000..db4b7c37d --- /dev/null +++ b/packages/test2/src/components/HelloWorld.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/packages/test2/src/demos/Avatar.vue b/packages/test2/src/demos/Avatar.vue new file mode 100644 index 000000000..50acf759a --- /dev/null +++ b/packages/test2/src/demos/Avatar.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/test2/src/demos/bubble.vue b/packages/test2/src/demos/bubble.vue new file mode 100644 index 000000000..9b20b6096 --- /dev/null +++ b/packages/test2/src/demos/bubble.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/packages/test2/src/demos/bubble/Hidden.vue b/packages/test2/src/demos/bubble/Hidden.vue new file mode 100644 index 000000000..7605990b7 --- /dev/null +++ b/packages/test2/src/demos/bubble/Hidden.vue @@ -0,0 +1,20 @@ + + + diff --git a/packages/test2/src/demos/bubble/Loading.vue b/packages/test2/src/demos/bubble/Loading.vue new file mode 100644 index 000000000..89e066877 --- /dev/null +++ b/packages/test2/src/demos/bubble/Loading.vue @@ -0,0 +1,20 @@ + + + diff --git a/packages/test2/src/demos/bubble/Polymorphic.vue b/packages/test2/src/demos/bubble/Polymorphic.vue new file mode 100644 index 000000000..be3d7919c --- /dev/null +++ b/packages/test2/src/demos/bubble/Polymorphic.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/packages/test2/src/demos/bubble/Reasoning.vue b/packages/test2/src/demos/bubble/Reasoning.vue new file mode 100644 index 000000000..21411a685 --- /dev/null +++ b/packages/test2/src/demos/bubble/Reasoning.vue @@ -0,0 +1,64 @@ + + + diff --git a/packages/test2/src/demos/bubble/Slots.vue b/packages/test2/src/demos/bubble/Slots.vue new file mode 100644 index 000000000..ca65b7b8b --- /dev/null +++ b/packages/test2/src/demos/bubble/Slots.vue @@ -0,0 +1,54 @@ + + + diff --git a/packages/test2/src/demos/bubble/Text.vue b/packages/test2/src/demos/bubble/Text.vue new file mode 100644 index 000000000..457c83283 --- /dev/null +++ b/packages/test2/src/demos/bubble/Text.vue @@ -0,0 +1,45 @@ + + + diff --git a/packages/test2/src/demos/bubble/ToolCall.vue b/packages/test2/src/demos/bubble/ToolCall.vue new file mode 100644 index 000000000..1065248ff --- /dev/null +++ b/packages/test2/src/demos/bubble/ToolCall.vue @@ -0,0 +1,78 @@ + + + diff --git a/packages/test2/src/demos/list.vue b/packages/test2/src/demos/list.vue new file mode 100644 index 000000000..892b0ea55 --- /dev/null +++ b/packages/test2/src/demos/list.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/packages/test2/src/demos/test.vue b/packages/test2/src/demos/test.vue new file mode 100644 index 000000000..fbae3bbdc --- /dev/null +++ b/packages/test2/src/demos/test.vue @@ -0,0 +1,31 @@ + + + diff --git a/packages/test2/src/icons/UserRound.vue b/packages/test2/src/icons/UserRound.vue new file mode 100644 index 000000000..c03bb8ef9 --- /dev/null +++ b/packages/test2/src/icons/UserRound.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/test2/src/main.ts b/packages/test2/src/main.ts new file mode 100644 index 000000000..7b1991224 --- /dev/null +++ b/packages/test2/src/main.ts @@ -0,0 +1,6 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' +import router from './router' + +createApp(App).use(router).mount('#app') diff --git a/packages/test2/src/router.ts b/packages/test2/src/router.ts new file mode 100644 index 000000000..6f2994e5e --- /dev/null +++ b/packages/test2/src/router.ts @@ -0,0 +1,28 @@ +import { createRouter, createWebHistory } from 'vue-router' +import Bubble from './demos/bubble.vue' +import List from './demos/list.vue' +import Test from './demos/test.vue' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + redirect: '/bubble', + }, + { + path: '/bubble', + component: Bubble, + }, + { + path: '/list', + component: List, + }, + { + path: '/test', + component: Test, + }, + ], +}) + +export default router diff --git a/packages/test2/src/style.css b/packages/test2/src/style.css new file mode 100644 index 000000000..d284b63d5 --- /dev/null +++ b/packages/test2/src/style.css @@ -0,0 +1,11 @@ +@import 'github-markdown-css/github-markdown.css'; + +*, +*::after, +*::before { + box-sizing: border-box; +} + +body { + margin: 0; +} diff --git a/packages/test2/tsconfig.app.json b/packages/test2/tsconfig.app.json new file mode 100644 index 000000000..8d16e4255 --- /dev/null +++ b/packages/test2/tsconfig.app.json @@ -0,0 +1,16 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/packages/test2/tsconfig.json b/packages/test2/tsconfig.json new file mode 100644 index 000000000..1ffef600d --- /dev/null +++ b/packages/test2/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/packages/test2/tsconfig.node.json b/packages/test2/tsconfig.node.json new file mode 100644 index 000000000..8a67f62f4 --- /dev/null +++ b/packages/test2/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/test2/vite.config.ts b/packages/test2/vite.config.ts new file mode 100644 index 000000000..880a90ad7 --- /dev/null +++ b/packages/test2/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { fileURLToPath } from 'url' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@opentiny/tiny-robot': fileURLToPath(new URL('../../packages/components/src', import.meta.url)), + }, + }, +}) From b71a494b9728d6d5406d25405ca800c950f3ab98 Mon Sep 17 00:00:00 2001 From: gene9831 Date: Wed, 10 Dec 2025 11:20:41 +0800 Subject: [PATCH 14/61] feat(bubble): introduce fallback renderers and enhance renderer matching for improved message handling --- packages/components/src/bubble/Bubble.vue | 11 ++++- .../src/bubble/BubbleBoxWrapper.vue | 20 +++++---- .../src/bubble/BubbleContentWrapper.vue | 13 +++++- .../components/src/bubble/BubbleProvider.vue | 4 +- .../composables/useBubbleBoxRenderer.ts | 45 +++++++++++-------- .../composables/useBubbleContentRenderer.ts | 29 ++++++++---- packages/components/src/bubble/constants.ts | 7 +++ packages/components/src/bubble/index.ts | 1 + packages/components/src/bubble/index.type.ts | 29 +++++++----- .../src/bubble/renderers/defaultRenderers.ts | 25 ++++++----- packages/components/src/index.ts | 11 ++++- 11 files changed, 130 insertions(+), 65 deletions(-) diff --git a/packages/components/src/bubble/Bubble.vue b/packages/components/src/bubble/Bubble.vue index 049e2ed88..2763e9e2c 100644 --- a/packages/components/src/bubble/Bubble.vue +++ b/packages/components/src/bubble/Bubble.vue @@ -58,8 +58,9 @@ const rendererMessages = computed(() => { })) } + // 移除不需要的 props,与 BubbleRendererMessage 类型保持一致 // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { avatar, placement, shape, splitPolymorphic, ...rest } = props + const { avatar, placement, shape, splitPolymorphic, fallbackBoxRenderer, fallbackContentRenderer, ...rest } = props // 来源:props.content(多态内容) if (Array.isArray(props.content)) { @@ -144,8 +145,14 @@ const hidden = computed(() => { :placement="props.placement" :shape="props.shape" :messages="rendererMessages" + :fallback-renderer="props.fallbackBoxRenderer" > - +
diff --git a/packages/components/src/bubble/BubbleBoxWrapper.vue b/packages/components/src/bubble/BubbleBoxWrapper.vue index f11d44c60..7b2c607c2 100644 --- a/packages/components/src/bubble/BubbleBoxWrapper.vue +++ b/packages/components/src/bubble/BubbleBoxWrapper.vue @@ -1,14 +1,18 @@