@blueking/chat-x 0.0.23 → 0.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,8 +20,8 @@ AI 助手消息展示组件,负责渲染 AI 回复的文本内容和工具调
20
20
  ## 渲染管线
21
21
 
22
22
  ```
23
- AssistantMessage
24
- ├── assistant-message-content(内容区)
23
+ AssistantMessage(根类名:ai-assistant-message)
24
+ ├── ai-assistant-message-content(内容区)
25
25
  │ └── [default slot] 或 ContentRender → MarkdownContent(Markdown 渲染)
26
26
  └── ToolCallRender × N(每个 toolCall 独立渲染,不受 slot 影响)
27
27
  ```
@@ -273,7 +273,7 @@ AI 可在一次回复中发起多个工具调用,组件依次渲染:
273
273
  默认插槽替换**内容区**的渲染(即 `ContentRender` 部分),工具调用仍在内容区外独立渲染,不受插槽影响:
274
274
 
275
275
  ```
276
- [自定义 slot 内容] ← 替换这里
276
+ [自定义 slot 内容] ← 替换 ai-assistant-message-content 内默认渲染
277
277
  [ToolCallRender] ← 不受影响,仍正常渲染
278
278
  [ToolCallRender]
279
279
  ```
@@ -337,11 +337,12 @@ ChatContainer 的 Props 继承自 `ChatInputProps` 和 `MessageContainerProps`
337
337
 
338
338
  ### v-model
339
339
 
340
- | 属性名 | 类型 | 说明 |
341
- | ---------------- | --------------------- | -------------------------------- |
342
- | modelValue | `string \| TagSchema` | 输入框内容,支持纯文本或标签结构 |
343
- | selectedShortcut | `Shortcut \| null` | 当前选中的快捷指令 |
344
- | cite | `string` | 引用内容 |
340
+ | 属性名 | 类型 | 说明 |
341
+ | ---------------- | --------------------- | ------------------------------------------------------------ |
342
+ | modelValue | `string \| TagSchema` | 输入框内容,支持纯文本或标签结构 |
343
+ | selectedShortcut | `Shortcut \| null` | 当前选中的快捷指令 |
344
+ | cite | `string` | 引用内容 |
345
+ | renderMode | `RenderMode` | 渲染模式(默认 `Chat`)。`Share` 模式隐藏侧边栏和折叠按钮;`Test` 模式隐藏分享按钮 |
345
346
 
346
347
  ### Events
347
348
 
@@ -373,10 +374,39 @@ ChatContainer 的 Props 继承自 `ChatInputProps` 和 `MessageContainerProps`
373
374
  | removeCustomTab | `(tabName: string) => void` | 移除自定义 Tab |
374
375
  | selectCustomTab | `(tab: CustomTab) => void` | 切换到指定 Tab |
375
376
 
377
+ ## 渲染模式
378
+
379
+ 通过 `v-model:render-mode` 控制容器的渲染行为:
380
+
381
+ | `renderMode` | 侧边栏 Tab / 折叠按钮 | 底部输入区域 | MessageTools 工具栏 | 说明 |
382
+ | ------------ | ---------------------- | --------------------------------- | --------------------- | -------------------------------- |
383
+ | `Chat` | 正常显示 | 正常显示(ChatInput / ShortcutRender / SelectionFooter) | 全部工具按钮 | 默认对话模式 |
384
+ | `Share` | **隐藏** | **隐藏** | **隐藏**(多选模式) | 分享预览模式,仅展示消息 |
385
+ | `Test` | 正常显示 | 正常显示 | 过滤掉「分享」按钮 | 测试/嵌入模式,隐藏分享入口 |
386
+
387
+ ```vue
388
+ <template>
389
+ <ChatContainer
390
+ v-model="inputValue"
391
+ v-model:render-mode="renderMode"
392
+ :messages="messages"
393
+ :message-status="messageStatus"
394
+ :on-send-message="handleSendMessage"
395
+ />
396
+ </template>
397
+
398
+ <script setup lang="ts">
399
+ import { shallowRef } from 'vue';
400
+ import { ChatContainer, RenderMode } from '@blueking/chat-x';
401
+
402
+ const renderMode = shallowRef<RenderMode>(RenderMode.Chat);
403
+ </script>
404
+ ```
405
+
376
406
  ## 类型定义
377
407
 
378
408
  ```typescript
379
- import { ChatContainer, type CustomTab, type Shortcut, type Message } from '@blueking/chat-x';
409
+ import { ChatContainer, RenderMode, type CustomTab, type Shortcut, type Message } from '@blueking/chat-x';
380
410
 
381
411
  // 自定义 Tab
382
412
  interface CustomTab<T = Record<string, unknown>> {
@@ -37,6 +37,10 @@ chat-input-container
37
37
 
38
38
  > **注意**:`slot#attachment` 只替换快捷指令区,`FileUploadBtn` 在其外部,使用该 slot 不会移除上传按钮。`slot#send-icon` 只替换图标,按钮的点击处理和样式仍由组件控制。
39
39
 
40
+ ### AiSlashInput 与 modelValue 同步
41
+
42
+ 内部编辑器 `AiSlashInput` 在 **`modelValue` 由外部异步更新**(例如从历史会话回填、父组件重置)且与当前文档不一致时,会通过 `useCommandSelection` 提供的 `GetDocSnapshot` 读取编辑器快照,与 `docToString(modelValue)` 比对后,必要时执行 `ReplaceAll` 将编辑器内容同步为新的 `modelValue`,避免内外状态脱节。
43
+
40
44
  ## 基础用法
41
45
 
42
46
  ```vue
@@ -17,8 +17,10 @@ CodeContent 接收 markdown-it 的 fence/code_block token,按行 highlight.js
17
17
 
18
18
  ## 组件结构
19
19
 
20
+ 样式根选择器为 **`.ai-message-container .code-content-wrapper`**:代码块头部与 `pre` 区样式仅在消息容器(如 `MessageContainer` 根上的 `ai-message-container`)下生效。Wiki 与业务中独立演示时,请将 `CodeContent` 包在带 `ai-message-container` 类名的父节点内。
21
+
20
22
  ```
21
- .code-content-wrapper(width: 100%,margin-bottom: 12px)
23
+ .ai-message-container .code-content-wrapper(width: 100%,margin-bottom: 12px)
22
24
  ├── .code-content-header(height: 40px,bg: #2f333d,border: 1px solid #1a1a1a)
23
25
  │ ├── .code-header-language(color: #999,显示 token.info 原始字符串)
24
26
  │ ├── slot#header({ language, token })— 自定义头部操作按钮区域
@@ -40,10 +42,13 @@ CodeContent 接收 markdown-it 的 fence/code_block token,按行 highlight.js
40
42
 
41
43
  ```vue
42
44
  <template>
43
- <CodeContent
44
- :token="codeTokens"
45
- @mounted="handleMounted"
46
- />
45
+ <!-- 完整深色头部与 pre 样式依赖父级 .ai-message-container(与对话消息区一致) -->
46
+ <div class="ai-message-container">
47
+ <CodeContent
48
+ :token="codeTokens"
49
+ @mounted="handleMounted"
50
+ />
51
+ </div>
47
52
  </template>
48
53
 
49
54
  <script setup lang="ts">
@@ -26,14 +26,13 @@ DescPanel 将描述字符串尝试 JSON 解析为键值列表展示,否则按
26
26
  │ └── {{ title }}
27
27
  └── .desc-panel(flex column,gap: 4px)
28
28
  ├── [JSON 对象/数组] v-for 逐项渲染 .desc-panel-item
29
- │ ├── .desc-label → "{{ key }}:"
30
- │ └── .desc-value(overflow hidden,ellipsis,nowrap)
31
- └── v-overflow-tips hover 时显示完整内容
32
- │ · value 为对象/数组时,tooltip 显示 JSON.stringify(value)
33
- │ · value 为原始值时,tooltip 显示原始值
34
- └── [非 JSON / 解析失败] 直接渲染 {{ data }}(纯文本)
29
+ │ ├── .desc-label → HighlightKeyword(key)
30
+ │ └── .desc-value → HighlightKeyword(值的文本或 JSON 字符串),`word-break: break-all`
31
+ └── [非 JSON / 解析失败] HighlightKeyword(data),`word-break: break-all`
35
32
  ```
36
33
 
34
+ > **说明**:键值与纯文本均通过 `HighlightKeyword` 展示,长内容依赖换行与面板宽度展示,**不再**使用 `v-overflow-tips` 悬停气泡。
35
+
37
36
  ## desc 解析规则
38
37
 
39
38
  `data` 是一个 computed,逻辑如下:
@@ -60,7 +59,7 @@ const data = computed(() => {
60
59
  | `'普通文本'` | 解析抛出 | — | 纯文本(原始字符串) |
61
60
  | `''` / `undefined` | 解析抛出 | — | 纯文本(空白) |
62
61
 
63
- > **嵌套对象**:值本身是对象时,`{{ value }}` 渲染为 `[object Object]`,但 `v-overflow-tips` tooltip 会显示 `JSON.stringify(value)` 的完整内容,方便查看原始结构。
62
+ > **嵌套对象**:值本身是对象时,文本区域通过 `JSON.stringify(value)` 展示完整 JSON(`HighlightKeyword` + `word-break: break-all`),不再使用悬停 tooltip
64
63
 
65
64
  ## 基础用法:JSON 参数
66
65
 
@@ -105,7 +104,7 @@ JSON 数组同样被视为 `object`,以数组索引(`0:`、`1:`…)作为
105
104
 
106
105
  ## 嵌套 JSON
107
106
 
108
- 嵌套对象的值通过 `v-overflow-tips` 悬停后显示 JSON.stringify 结果,文本区域显示 `[object Object]`:
107
+ 嵌套对象的值在文本区域直接渲染为 `JSON.stringify(value)` 字符串,便于在面板内换行阅读:
109
108
 
110
109
  ```vue
111
110
  <template>
@@ -25,15 +25,15 @@ div.ai-key-value-content(flex column,gap: 8px,font-size: 12px,color: #4d
25
25
  │ ├── ThinkingIcon(14×14px)
26
26
  │ └── {{ title }}
27
27
  └── div.ai-key-value-content(flex column,无 gap) ← 与外层同名嵌套
28
- └── div.key-value-item × N(flex row,gap: 3px,height: 20px)
29
- ├── div.item-key → {{ item.key }}(font-weight: bold,color: #333)
28
+ └── div.key-value-item × N(flex row,gap: 3px,min-height: 20px)
29
+ ├── div.item-key → {{ item.key }}(flex: 0 0 auto,font-weight: bold,color: #333)
30
30
  ├── ":" → 硬编码文本节点,冒号不属于任何 div
31
- └── div.item-value → {{ item.value }}(overflow hidden,ellipsis,nowrap)
31
+ └── div.item-value → {{ item.value }}(overflow hidden,ellipsis,`word-break: break-all` 允许多行换行)
32
32
  ```
33
33
 
34
34
  > **注意**:内层也是 `.ai-key-value-content` 类(与外层同名),仅用于布局,无额外样式差异。
35
35
  > **注意**:`v-for` 使用 `item.key` 作为 `:key`,`content` 中的 `key` 字段必须唯一,否则触发 Vue 重复 key 警告。
36
- > **注意**:`item.value` 使用 `text-overflow: ellipsis` 截断,但**无 tooltip**(与 `DescPanel` 不同),过长内容会被静默截断。
36
+ > **注意**:`item.value` 在单行方向仍可能因 `text-overflow: ellipsis` 显示省略,但配合 `word-break: break-all` 长串会优先换行展示,**无 tooltip**(与 `DescPanel` 不同)。
37
37
 
38
38
  ## 基础用法
39
39
 
@@ -71,9 +71,9 @@ div.ai-key-value-content(flex column,gap: 8px,font-size: 12px,color: #4d
71
71
  </template>
72
72
  ```
73
73
 
74
- ## 超长 value 截断
74
+ ## 超长 value 换行
75
75
 
76
- `.item-value` 固定 `white-space: nowrap`,超出容器宽度时截断显示省略号,**悬停无 tooltip**:
76
+ `.item-value` 使用 `word-break: break-all`,长 URL 或长文本会在容器内换行;仍保留 `overflow: hidden` 与 `text-overflow: ellipsis` 以约束极端情况,**悬停无 tooltip**:
77
77
 
78
78
  ## API
79
79
 
@@ -91,7 +91,7 @@ props.content → completeMarkdownSyntax → md.parse → groupTokens → groupe
91
91
 
92
92
  ## 代码块
93
93
 
94
- 代码块由 `CodeContent` 渲染,支持 highlight.js 语法高亮、语言标签、一键复制:
94
+ 代码块由 `CodeContent` 渲染,支持 highlight.js 语法高亮、语言标签、一键复制。语法高亮主题样式由 `CodeContent` 侧引入(`github-dark`),`MarkdownContent` **不再**全局引入 `highlight.js` 主题 CSS,避免与代码块组件重复加载、并保持与消息区样式一致。
95
95
 
96
96
  ## 表格
97
97
 
@@ -113,8 +113,11 @@ MessageContainer 是消息列表的核心容器。接收父组件用 useMessageG
113
113
  - `role: 'tool'` 消息**不会独立渲染**,而是被注入到对应 AssistantMessage 的 `toolCall.toolMessage` 字段
114
114
  - 若 `toolMessage.error` 存在,AssistantMessage 的 `status` 会被强制设为 `MessageStatus.Error`
115
115
  - `MessageTools` 工具栏只在 `type === 'assistant'` 的消息组底部渲染(不依赖鼠标悬停,始终可见),且满足以下条件时**不渲染**:
116
+ - `renderMode === RenderMode.Share`(分享预览模式)
116
117
  - 消息组的 `pause` 为 `true`(来源于 `message.property?.extra?.pause`)
117
118
  - 多选模式(`enableSelection`)开启且消息组不是 Loading 类型
119
+ - `renderMode === RenderMode.Test` 时,工具栏会过滤掉「分享」按钮,其余正常
120
+ - `renderMode === RenderMode.Share` 时,`message-group-messages` 自动添加 `message-group-enabled-selection` 类名(与 `enableSelection: true` 一致的多选视觉效果)
118
121
  - Loading 消息组的 `type` 是 `MessageRole.Loading`,不显示工具栏和多选 Checkbox
119
122
 
120
123
  ## 等待响应(Loading 自动注入)
@@ -464,6 +467,7 @@ AI 回复状态为 `error` 时,消息以错误样式展示:
464
467
  | onUserAction | `(tool: IToolBtn, message: Message) => Promise<string[] \| void>` | — | 用户消息工具操作回调 |
465
468
  | onUserInputConfirm | `(message: Message, content: UserMessage['content'], docSchema: TagSchema) => Promise<void>` | — | 用户编辑消息确认回调 |
466
469
  | onUserShortcutConfirm | `(message: Message, formModel: Record<string, unknown>) => Promise<void>` | — | 用户快捷指令表单提交回调 |
470
+ | renderMode | `RenderMode` | — | 渲染模式。`Share` 模式下启用多选样式并隐藏工具栏;`Test` 模式下过滤掉「分享」按钮;不传或 `Chat` 为默认行为 |
467
471
 
468
472
  ### v-model
469
473
 
@@ -1,7 +1,7 @@
1
1
  <!-- AI SUMMARY -->
2
2
  ## 快速了解
3
3
 
4
- useCommandSelection 无参数,返回 commandSelection(ShallowRef 行列)与 GetCursorPosition(edix EditorCommand)。 editor.command(GetCursorPosition) 时把焦点端写入 commandSelection,供后续 DeleteTag、InsertTag 等命令计算范围。 仅在 AiSlashInput(@ 菜单插入)内部使用。
4
+ useCommandSelection 无参数,返回 commandSelection、docSnapshot、GetCursorPosition、GetDocSnapshot。 GetCursorPosition 写入光标行列;GetDocSnapshot 将当前文档快照写入 docSnapshot,供外部 modelValue 与编辑器比对同步。 仅在 AiSlashInput(@ 菜单插入与 modelValue 同步)内部使用。
5
5
 
6
6
  ### 关联组件
7
7
  - **chat-input** — AiSlashInput 内部使用
@@ -29,9 +29,16 @@ editor.command(GetCursorPosition)
29
29
  commandSelection.value = { column, line }
30
30
 
31
31
  commandSelection(shallowRef,初始值 { column: 0, line: 0 })
32
+
33
+ editor.command(GetDocSnapshot)
34
+ └── GetDocSnapshot(doc)
35
+ docSnapshot.value = doc // 当前文档快照,用于与 props.modelValue 比对
32
36
  ```
33
37
 
34
- **使用时机**:在 `@` 资源插入流程中,先执行 `GetCursorPosition` 快照当前光标,再据此计算删除范围和插入位置。
38
+ **使用时机**:
39
+
40
+ - 在 `@` 资源插入流程中,先执行 `GetCursorPosition` 快照当前光标,再据此计算删除范围和插入位置。
41
+ - 在外部 `modelValue` 变化时,先执行 `GetDocSnapshot` 取得编辑器当前文档,与 `docToString(modelValue)` 比对,决定是否 `ReplaceAll` 同步。
35
42
 
36
43
  ## 概念演示
37
44
 
@@ -42,20 +49,29 @@ editor.command(GetCursorPosition)
42
49
  ## 在 AiSlashInput 中的实际用法
43
50
 
44
51
  ```typescript
45
- const { commandSelection, GetCursorPosition } = useCommandSelection();
52
+ import { watch } from 'vue';
53
+
54
+ const { commandSelection, GetCursorPosition, GetDocSnapshot, docSnapshot } = useCommandSelection();
46
55
 
47
56
  // 用户从 @xxx 菜单中选择资源时:
48
57
  const insertTagAtCursor = (tag: IAiSlashMenuItem) => {
49
- // 1. 执行命令,快照当前光标位置 → 写入 commandSelection.value
50
58
  editor.command(GetCursorPosition);
51
-
52
- // 2. 读取快照的行列位置
53
59
  const { column, line } = commandSelection.value;
54
-
55
- // 3. 根据位置删除已输入的 "@keyword",插入 tag
56
60
  editor.command(DeleteTag, [line, column - keyword.value.length - 1], [line, column]);
57
61
  editor.command(InsertTag, [line, column], tag);
58
62
  };
63
+
64
+ // 外部 modelValue 变化时,与编辑器文档对齐(简化示意;需已存在 editor、props、text、docToString)
65
+ watch(
66
+ () => props.modelValue,
67
+ () => {
68
+ editor.command(GetDocSnapshot);
69
+ if (docToString(docSnapshot.value || []) !== docToString(text.value || [])) {
70
+ editor.command(ReplaceAll, docToString(text.value || []) as unknown as string);
71
+ }
72
+ },
73
+ { deep: false },
74
+ );
59
75
  ```
60
76
 
61
77
  ## API
@@ -69,7 +85,9 @@ const insertTagAtCursor = (tag: IAiSlashMenuItem) => {
69
85
  | 属性名 | 类型 | 初始值 | 说明 |
70
86
  | ------------------- | ---------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------ |
71
87
  | `commandSelection` | `ShallowRef<{ column: number; line: number }>` | `{ column: 0, line: 0 }` | 存储最近一次执行 `GetCursorPosition` 时的光标行列位置 |
88
+ | `docSnapshot` | `ShallowRef<DocFragment>` | `[]` | 存储最近一次执行 `GetDocSnapshot` 时的文档快照 |
72
89
  | `GetCursorPosition` | `EditorCommand<[]>` | — | 编辑器命令,由 `editor.command(GetCursorPosition)` 触发,将当前光标写入 `commandSelection` |
90
+ | `GetDocSnapshot` | `EditorCommand<[]>` | — | 编辑器命令,由 `editor.command(GetDocSnapshot)` 触发,将当前文档写入 `docSnapshot` |
73
91
 
74
92
  ## 类型说明
75
93
 
@@ -84,7 +102,9 @@ type EditorCommand<A extends unknown[]> = (
84
102
  // useCommandSelection 返回值
85
103
  interface UseCommandSelectionReturn {
86
104
  commandSelection: ShallowRef<{ column: number; line: number }>;
105
+ docSnapshot: ShallowRef<DocFragment>;
87
106
  GetCursorPosition: EditorCommand<[]>;
107
+ GetDocSnapshot: EditorCommand<[]>;
88
108
  }
89
109
  ```
90
110
 
@@ -92,26 +112,29 @@ interface UseCommandSelectionReturn {
92
112
 
93
113
  ```typescript
94
114
  import { shallowRef } from 'vue';
115
+
95
116
  import type { EditorCommand } from '../edix';
117
+ import type { DocFragment } from '../edix/doc/types';
96
118
 
97
119
  export const useCommandSelection = () => {
98
- // 存储最近一次快照的光标位置
99
- const commandSelection = shallowRef<{ column: number; line: number }>({
100
- column: 0,
101
- line: 0,
102
- });
120
+ const commandSelection = shallowRef<{ column: number; line: number }>({ column: 0, line: 0 });
121
+ const docSnapshot = shallowRef<DocFragment>([]);
103
122
 
104
- // EditorCommand:由 editor.command() 调用,注入 doc 和 selection
105
123
  const GetCursorPosition: EditorCommand<[]> = (_doc, selection) => {
106
- const [, focus] = selection; // 取 focus 端(忽略 anchor)
124
+ const [, focus] = selection;
107
125
  const [line, column] = focus;
108
126
  commandSelection.value = { column, line };
109
- // 不返回 Transaction,即只读取不修改文档
127
+ };
128
+
129
+ const GetDocSnapshot: EditorCommand<[]> = doc => {
130
+ docSnapshot.value = doc;
110
131
  };
111
132
 
112
133
  return {
113
134
  commandSelection,
135
+ docSnapshot,
114
136
  GetCursorPosition,
137
+ GetDocSnapshot,
115
138
  };
116
139
  };
117
140
  ```
@@ -121,7 +144,7 @@ export const useCommandSelection = () => {
121
144
  1. **只读命令**:`GetCursorPosition` 不返回 `Transaction`,不修改编辑器文档内容,仅记录位置
122
145
  2. **异步快照**:`commandSelection` 在 `editor.command(GetCursorPosition)` 执行后**同步**更新,下一行代码即可安全读取
123
146
  3. **`shallowRef` 而非 `ref`**:对象引用替换触发响应式,内部字段修改不触发(此处每次整体替换,无影响)
124
- 4. **仅适用于 edix 编辑器**:`GetCursorPosition` 依赖 edix `SelectionSnapshot` 格式,不适用于原生 contenteditable 或其他富文本库
147
+ 4. **仅适用于 edix 编辑器**:`GetCursorPosition` / `GetDocSnapshot` 依赖 edix 的文档与选区格式,不适用于原生 contenteditable 或其他富文本库
125
148
 
126
149
  ## 关联组件
127
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueking/chat-x",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "description": "蓝鲸智云 AI Chat 组件库 —— 遵循 AG-UI,为 AI Agent 和人类开发者共同设计的对话 UI 组件库。",
5
5
  "main": "index.js",
6
6
  "scripts": {