@blueking/chat-x 0.0.4 → 0.0.6
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.
- package/dist/index.css +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/generated/docs/activity-message.md +428 -0
- package/dist/mcp/generated/docs/ai-image.md +227 -0
- package/dist/mcp/generated/docs/ai-loading.md +129 -0
- package/dist/mcp/generated/docs/ai-selection.md +436 -0
- package/dist/mcp/generated/docs/animation-text.md +199 -0
- package/dist/mcp/generated/docs/assistant-message.md +424 -0
- package/dist/mcp/generated/docs/chat-container.md +365 -0
- package/dist/mcp/generated/docs/chat-input.md +625 -0
- package/dist/mcp/generated/docs/cite-content.md +138 -0
- package/dist/mcp/generated/docs/code-content.md +199 -0
- package/dist/mcp/generated/docs/common-error-content.md +70 -0
- package/dist/mcp/generated/docs/constants.md +216 -0
- package/dist/mcp/generated/docs/content-render.md +238 -0
- package/dist/mcp/generated/docs/delete-tool.md +188 -0
- package/dist/mcp/generated/docs/desc-panel.md +139 -0
- package/dist/mcp/generated/docs/execution-summary.md +126 -0
- package/dist/mcp/generated/docs/file-content.md +300 -0
- package/dist/mcp/generated/docs/file-upload-btn.md +174 -0
- package/dist/mcp/generated/docs/flow-message.md +305 -0
- package/dist/mcp/generated/docs/highlight-keyword.md +144 -0
- package/dist/mcp/generated/docs/image-content.md +178 -0
- package/dist/mcp/generated/docs/image-preview-group.md +181 -0
- package/dist/mcp/generated/docs/image-preview.md +224 -0
- package/dist/mcp/generated/docs/info-message.md +124 -0
- package/dist/mcp/generated/docs/key-value-content.md +124 -0
- package/dist/mcp/generated/docs/latex-content.md +196 -0
- package/dist/mcp/generated/docs/loading-message.md +171 -0
- package/dist/mcp/generated/docs/markdown-content.md +186 -0
- package/dist/mcp/generated/docs/markdown-latex.md +208 -0
- package/dist/mcp/generated/docs/markdown-mermaid.md +250 -0
- package/dist/mcp/generated/docs/mermaid-content.md +185 -0
- package/dist/mcp/generated/docs/message-container.md +534 -0
- package/dist/mcp/generated/docs/message-render.md +329 -0
- package/dist/mcp/generated/docs/message-tools.md +376 -0
- package/dist/mcp/generated/docs/messages.md +472 -0
- package/dist/mcp/generated/docs/overflow-tips.md +209 -0
- package/dist/mcp/generated/docs/reasoning-message.md +233 -0
- package/dist/mcp/generated/docs/reference-content.md +132 -0
- package/dist/mcp/generated/docs/scroll-btn.md +155 -0
- package/dist/mcp/generated/docs/selection-footer.md +75 -0
- package/dist/mcp/generated/docs/shortcut-btn.md +202 -0
- package/dist/mcp/generated/docs/shortcut-btns.md +264 -0
- package/dist/mcp/generated/docs/shortcut-render.md +418 -0
- package/dist/mcp/generated/docs/text-content.md +74 -0
- package/dist/mcp/generated/docs/theme.md +388 -0
- package/dist/mcp/generated/docs/tool-btn.md +254 -0
- package/dist/mcp/generated/docs/tool-message.md +217 -0
- package/dist/mcp/generated/docs/toolcall-render.md +299 -0
- package/dist/mcp/generated/docs/use-animation-text.md +198 -0
- package/dist/mcp/generated/docs/use-clipboard.md +206 -0
- package/dist/mcp/generated/docs/use-command-selection.md +128 -0
- package/dist/mcp/generated/docs/use-container-scroll.md +56 -0
- package/dist/mcp/generated/docs/use-custom-tab.md +122 -0
- package/dist/mcp/generated/docs/use-global-config.md +154 -0
- package/dist/mcp/generated/docs/use-menu-keydown.md +164 -0
- package/dist/mcp/generated/docs/use-message-group.md +175 -0
- package/dist/mcp/generated/docs/use-observer-visible-list.md +189 -0
- package/dist/mcp/generated/docs/use-parent-scrolling.md +46 -0
- package/dist/mcp/generated/docs/user-feedback.md +229 -0
- package/dist/mcp/generated/docs/user-message.md +347 -0
- package/dist/mcp/generated/index.json +1311 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +42 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +43 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools/get-component-doc.d.ts +19 -0
- package/dist/mcp/tools/get-component-doc.js +60 -0
- package/dist/mcp/tools/get-component-doc.js.map +1 -0
- package/dist/mcp/tools/list-components.d.ts +35 -0
- package/dist/mcp/tools/list-components.js +147 -0
- package/dist/mcp/tools/list-components.js.map +1 -0
- package/dist/mcp/tools/search-docs.d.ts +14 -0
- package/dist/mcp/tools/search-docs.js +82 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/dist/mcp/utils/doc-loader.d.ts +35 -0
- package/dist/mcp/utils/doc-loader.js +64 -0
- package/dist/mcp/utils/doc-loader.js.map +1 -0
- package/package.json +5 -7
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<!-- AI SUMMARY -->
|
|
2
|
+
## 快速了解
|
|
3
|
+
|
|
4
|
+
useCustomTabProvider 返回 tabs、selectedTab、isCollapse 及 add/remove/selectCustomTab,并通过 provide 共享;可选 onTabChange 在切换时拉取数据。 useCustomTabConsumer 在后代注入同一套 API,常用于侧栏动态节点详情等。EXECUTION_TAB_NAME 标识默认「执行情况」Tab。 ChatContainer 侧栏集成 Provider 与 Tab UI。
|
|
5
|
+
|
|
6
|
+
### 关联组件
|
|
7
|
+
- **chat-container** — Provider 与侧栏 Tab 主场景
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
<!-- FULL DOC -->
|
|
11
|
+
|
|
12
|
+
# useCustomTab 自定义 Tab 管理
|
|
13
|
+
|
|
14
|
+
> **分类**:composable
|
|
15
|
+
|
|
16
|
+
Provider/Consumer 模式的自定义 Tab 管理,用于 `ChatContainer` 侧边栏的 Tab 动态管理。Provider 在 `ChatContainer` 中创建,Consumer 在任意后代组件中注入使用。
|
|
17
|
+
|
|
18
|
+
## 函数签名
|
|
19
|
+
|
|
20
|
+
### useCustomTabProvider
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
function useCustomTabProvider<T extends Record<string, unknown>>(options: {
|
|
24
|
+
onTabChange?: (tab: CustomTab<T>) => void;
|
|
25
|
+
}): {
|
|
26
|
+
tabs: ShallowRef<CustomTab<T>[]>;
|
|
27
|
+
selectedTab: Ref<CustomTab<T>>;
|
|
28
|
+
isCollapse: ShallowRef<boolean>;
|
|
29
|
+
addCustomTab: (tab: CustomTab<T>) => void;
|
|
30
|
+
removeCustomTab: (tabName: string) => void;
|
|
31
|
+
selectCustomTab: (tab: CustomTab<T>) => void;
|
|
32
|
+
};
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### useCustomTabConsumer
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
function useCustomTabConsumer<T extends Record<string, unknown>>():
|
|
39
|
+
| undefined
|
|
40
|
+
| {
|
|
41
|
+
tabs: ShallowRef<CustomTab<T>[]>;
|
|
42
|
+
selectedTab: ShallowRef<CustomTab<T> | null>;
|
|
43
|
+
addCustomTab: (tab: CustomTab<T>) => void;
|
|
44
|
+
removeCustomTab: (tabName: string) => void;
|
|
45
|
+
selectCustomTab: (tab: CustomTab<T>) => void;
|
|
46
|
+
};
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 使用示例
|
|
50
|
+
|
|
51
|
+
### Provider(容器组件)
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { useCustomTabProvider, EXECUTION_TAB_NAME } from '@blueking/chat-x';
|
|
55
|
+
|
|
56
|
+
const { tabs, selectedTab, isCollapse, addCustomTab, removeCustomTab, selectCustomTab } = useCustomTabProvider({
|
|
57
|
+
onTabChange: async tab => {
|
|
58
|
+
// Tab 切换时加载数据
|
|
59
|
+
const data = await fetchTabData(tab.name);
|
|
60
|
+
return data;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Consumer(后代组件)
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { useCustomTabConsumer } from '@blueking/chat-x';
|
|
69
|
+
|
|
70
|
+
const tabManager = useCustomTabConsumer();
|
|
71
|
+
|
|
72
|
+
// 添加一个自定义 Tab
|
|
73
|
+
tabManager?.addCustomTab({
|
|
74
|
+
name: 'node-detail-123',
|
|
75
|
+
label: '节点详情',
|
|
76
|
+
data: {
|
|
77
|
+
component: NodeDetailComponent,
|
|
78
|
+
props: { nodeId: '123' },
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// 移除 Tab
|
|
83
|
+
tabManager?.removeCustomTab('node-detail-123');
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 内置常量
|
|
87
|
+
|
|
88
|
+
| 常量名 | 值 | 说明 |
|
|
89
|
+
| -------------------- | ------------- | ------------------------- |
|
|
90
|
+
| `EXECUTION_TAB_NAME` | `'execution'` | 执行情况 Tab 的固定标识 |
|
|
91
|
+
| `CUSTOM_TAB_TOKEN` | `Symbol` | provide/inject 注入 Token |
|
|
92
|
+
|
|
93
|
+
## 返回值说明
|
|
94
|
+
|
|
95
|
+
| 属性/方法名 | 类型 | 说明 |
|
|
96
|
+
| --------------- | --------------------------- | ------------------------------------------------- |
|
|
97
|
+
| tabs | `ShallowRef<CustomTab[]>` | 所有 Tab 列表(含默认的执行情况 Tab) |
|
|
98
|
+
| selectedTab | `Ref<CustomTab>` | 当前选中的 Tab |
|
|
99
|
+
| isCollapse | `ShallowRef<boolean>` | 侧边栏折叠状态;`addCustomTab` 时自动设为 `false` |
|
|
100
|
+
| addCustomTab | `(tab: CustomTab) => void` | 添加 Tab(同名 Tab 不重复添加) |
|
|
101
|
+
| removeCustomTab | `(tabName: string) => void` | 移除指定 Tab |
|
|
102
|
+
| selectCustomTab | `(tab: CustomTab) => void` | 切换到指定 Tab,触发 `onTabChange` 回调 |
|
|
103
|
+
|
|
104
|
+
## 类型定义
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
interface CustomTab<T = Record<string, unknown>> {
|
|
108
|
+
label: string;
|
|
109
|
+
name: string;
|
|
110
|
+
data?: T;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 设计特点
|
|
115
|
+
|
|
116
|
+
- `tabs` 使用 `shallowRef`,`addCustomTab` 通过展开新数组触发更新,避免 `UnwrapRef<T>` 类型问题
|
|
117
|
+
- 默认的「执行情况」Tab 始终存在且不可关闭
|
|
118
|
+
- `addCustomTab` 同时展开侧边栏(`isCollapse = false`)并在 `nextTick` 后自动选中新 Tab
|
|
119
|
+
|
|
120
|
+
## 关联组件
|
|
121
|
+
|
|
122
|
+
- [ChatContainer](../components/molecular/chat-container.md) — 侧栏 Tab 与自定义面板
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<!-- AI SUMMARY -->
|
|
2
|
+
## 快速了解
|
|
3
|
+
|
|
4
|
+
useGlobalConfig 提供 messageSlotId('#ai-blueking-message-slot')与 rawMessageSlotId,并 provide AI_BLUEKING_MESSAGE_SLOT_ID。 useMessageSlotId 在子组件 inject 得到 ShallowRef,供 Teleport :to 挂载到消息容器内节点。名称易误解为全局配置,仅与 Teleport 插槽相关。 ChatContainer 使用 useMessageSlotId 与 messageSlotId;FlowMessage 等用 Teleport 展示详情。
|
|
5
|
+
|
|
6
|
+
### 关联组件
|
|
7
|
+
- **chat-container** — 侧栏 messageSlotId 与自定义 Tab 内容区
|
|
8
|
+
- **flow-message** — useMessageSlotId + Teleport 流程详情
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
<!-- FULL DOC -->
|
|
12
|
+
|
|
13
|
+
# useGlobalConfig 消息面板 Teleport 插槽
|
|
14
|
+
|
|
15
|
+
> **分类**:composable
|
|
16
|
+
|
|
17
|
+
为消息容器内的子组件提供共享 **Teleport 目标 ID** 的 Provider/Consumer 工具。
|
|
18
|
+
|
|
19
|
+
根容器注册一个 `id="ai-blueking-message-slot"` 的 DOM 节点,并通过 `provide` 将其 CSS 选择器下发;子组件(如 `FlowMessage`)通过 `inject` 取到该选择器,作为 `<Teleport :to="...">` 的挂载目标,从而将详情面板等覆盖层 Teleport 到消息容器内部的指定位置,而非 `body`。
|
|
20
|
+
|
|
21
|
+
> 名称"useGlobalConfig"具有一定误导性——该模块实际上**只负责管理 Teleport 插槽 ID**,不涉及任何 locale / zIndex 等通用全局配置。
|
|
22
|
+
|
|
23
|
+
## 工作原理
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
根容器组件
|
|
27
|
+
├── useGlobalConfig()
|
|
28
|
+
│ ├── provide(AI_BLUEKING_MESSAGE_SLOT_ID, computed(() => '#ai-blueking-message-slot'))
|
|
29
|
+
│ └── return { messageSlotId: '#ai-blueking-message-slot', rawMessageSlotId: 'ai-blueking-message-slot' }
|
|
30
|
+
│
|
|
31
|
+
└── <div :id="rawMessageSlotId" /> ← Teleport 实际挂载的 DOM 节点
|
|
32
|
+
|
|
33
|
+
后代子组件(如 FlowMessage)
|
|
34
|
+
├── useMessageSlotId()
|
|
35
|
+
│ ├── onMounted → watchEffect → inject(AI_BLUEKING_MESSAGE_SLOT_ID).value
|
|
36
|
+
│ └── return { messageSlotId: ShallowRef<'#ai-blueking-message-slot' | undefined> }
|
|
37
|
+
│
|
|
38
|
+
└── <Teleport :to="messageSlotId"> ← Teleport 至根容器内
|
|
39
|
+
<FlowDetail />
|
|
40
|
+
</Teleport>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 渲染示例
|
|
44
|
+
|
|
45
|
+
## 根容器用法(useGlobalConfig)
|
|
46
|
+
|
|
47
|
+
```vue
|
|
48
|
+
<template>
|
|
49
|
+
<div class="chat-container">
|
|
50
|
+
<!-- 1. 创建 Teleport 挂载目标 DOM 节点 -->
|
|
51
|
+
<div
|
|
52
|
+
:id="rawMessageSlotId"
|
|
53
|
+
class="ai-blueking-container-slot"
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
<!-- 2. 消息列表(FlowMessage 等子组件从此处注入插槽 ID) -->
|
|
57
|
+
<MessageRender :messages="messages" />
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<script setup lang="ts">
|
|
62
|
+
import { useGlobalConfig } from '@blueking/chat-x';
|
|
63
|
+
|
|
64
|
+
// provide 插槽 ID 给所有后代,同时获取 rawMessageSlotId 作为 DOM id
|
|
65
|
+
const { rawMessageSlotId } = useGlobalConfig();
|
|
66
|
+
</script>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 子组件用法(useMessageSlotId)
|
|
70
|
+
|
|
71
|
+
```vue
|
|
72
|
+
<!-- FlowMessage 内部(简化) -->
|
|
73
|
+
<template>
|
|
74
|
+
<div class="flow-message">
|
|
75
|
+
<!-- 节点列表... -->
|
|
76
|
+
|
|
77
|
+
<!-- 点击节点后,将详情面板 Teleport 到根容器的插槽节点 -->
|
|
78
|
+
<template v-if="detailVisible && messageSlotId">
|
|
79
|
+
<Teleport :to="messageSlotId">
|
|
80
|
+
<FlowDetail
|
|
81
|
+
:node="selectedNode"
|
|
82
|
+
@close="detailVisible = false"
|
|
83
|
+
/>
|
|
84
|
+
</Teleport>
|
|
85
|
+
</template>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<script setup lang="ts">
|
|
90
|
+
import { useMessageSlotId } from '@blueking/chat-x';
|
|
91
|
+
|
|
92
|
+
// 注入根容器提供的 CSS 选择器('#ai-blueking-message-slot')
|
|
93
|
+
const { messageSlotId } = useMessageSlotId();
|
|
94
|
+
</script>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## API
|
|
98
|
+
|
|
99
|
+
### useGlobalConfig()
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
function useGlobalConfig(): {
|
|
103
|
+
messageSlotId: ComputedRef<string>; // '#ai-blueking-message-slot'(CSS 选择器)
|
|
104
|
+
readonly rawMessageSlotId: string; // 'ai-blueking-message-slot'(DOM id 属性值)
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- 调用后立即 `provide(AI_BLUEKING_MESSAGE_SLOT_ID, messageSlotId)` 向后代注入
|
|
109
|
+
- 必须在 Vue 组件的 `setup()` 中调用(有组件上下文才能 `provide`)
|
|
110
|
+
|
|
111
|
+
### useMessageSlotId()
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
function useMessageSlotId(): {
|
|
115
|
+
messageSlotId: ShallowRef<string | undefined>;
|
|
116
|
+
// 初始值:undefined(onMounted 前)
|
|
117
|
+
// 挂载后:'#ai-blueking-message-slot'(由 inject 注入)
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- `onMounted` 后通过 `watchEffect` + `inject` 填充 `messageSlotId.value`
|
|
122
|
+
- 无父级 Provider 时 `inject` 返回 `undefined`,`messageSlotId.value` 保持 `undefined`
|
|
123
|
+
- 使用前建议配合 `v-if="messageSlotId"` 防止 Teleport 找不到目标
|
|
124
|
+
|
|
125
|
+
### getMessageSlotId()(内部辅助)
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
function getMessageSlotId(): ComputedRef<string> | undefined;
|
|
129
|
+
// = inject(AI_BLUEKING_MESSAGE_SLOT_ID)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- 由 `useMessageSlotId` 内部调用,通常不需要直接使用
|
|
133
|
+
|
|
134
|
+
### 导出常量
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// Teleport 目标元素的 id 属性值
|
|
138
|
+
export const MESSAGE_SLOT_ID = 'ai-blueking-message-slot';
|
|
139
|
+
|
|
140
|
+
// provide/inject 使用的 Symbol key
|
|
141
|
+
export const AI_BLUEKING_MESSAGE_SLOT_ID: unique symbol;
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## 注意事项
|
|
145
|
+
|
|
146
|
+
1. **`useGlobalConfig` 必须在祖先组件调用**:子组件的 `useMessageSlotId` 通过 `inject` 获取,必须在同一组件树内存在对应 Provider
|
|
147
|
+
2. **`messageSlotId` 在 `onMounted` 后才有值**:`useMessageSlotId` 的返回值初始为 `undefined`,应配合 `v-if="messageSlotId"` 防止 Teleport 报错
|
|
148
|
+
3. **DOM 节点必须与 `rawMessageSlotId` 对应**:根容器需确保 `<div :id="rawMessageSlotId">` 存在于 DOM 中,Teleport 才能找到目标
|
|
149
|
+
4. **Teleport 目标在消息容器内**:相比直接 Teleport 到 `body`,此方案让覆盖层随消息容器的布局、z-index、overflow 上下文保持一致
|
|
150
|
+
|
|
151
|
+
## 关联组件
|
|
152
|
+
|
|
153
|
+
- [ChatContainer](../components/molecular/chat-container.md) — 侧栏插槽节点与 messageSlotId
|
|
154
|
+
- [FlowMessage](../components/molecular/flow-message.md) — Teleport 流程节点详情
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<!-- AI SUMMARY -->
|
|
2
|
+
## 快速了解
|
|
3
|
+
|
|
4
|
+
useMenuKeydown 接收 items、menuRef、onSelect,在 window 捕获阶段监听 keydown;菜单不可见(offsetParent 为空)或列表为空时不响应。 维护 activeIndex,处理 ArrowUp/ArrowDown 循环与 Enter 选中,并 scrollIntoView 当前 .is-active 项。 AiSlashMenu、AiPromptList(ChatInput 子模块)内部使用。
|
|
5
|
+
|
|
6
|
+
### 关联组件
|
|
7
|
+
- **chat-input** — AiSlashMenu / AiPromptList 键盘导航
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
<!-- FULL DOC -->
|
|
11
|
+
|
|
12
|
+
# useMenuKeydown 菜单键盘导航
|
|
13
|
+
|
|
14
|
+
> **分类**:composable
|
|
15
|
+
|
|
16
|
+
为弹出菜单提供键盘导航能力的组合式函数。在 `onMounted` 时于 **`window` 捕获阶段**注册 `keydown` 监听,在 `onScopeDispose` 时自动移除,通过 `menuRef.offsetParent` 检测菜单可见性来决定是否响应按键。
|
|
17
|
+
|
|
18
|
+
内部维护 `activeIndex`(高亮项索引),由调用方将其绑定到列表的 `.is-active` 样式;`handleKeydown` 完全内部管理,**无需手动绑定到模板**。
|
|
19
|
+
|
|
20
|
+
## 工作原理
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
onMounted:window.addEventListener('keydown', handleKeydown, true) ← 捕获阶段
|
|
24
|
+
onScopeDispose:window.removeEventListener('keydown', handleKeydown, true)
|
|
25
|
+
|
|
26
|
+
handleKeydown(e):
|
|
27
|
+
├── !menuRef.value?.offsetParent → return(菜单不可见,忽略)
|
|
28
|
+
├── !items.value?.length → return(列表为空,忽略)
|
|
29
|
+
│
|
|
30
|
+
├── ArrowUp → e.preventDefault/stopPropagation
|
|
31
|
+
│ activeIndex = (activeIndex - 1 + length) % length ← 循环到末尾
|
|
32
|
+
│ scrollToActive()
|
|
33
|
+
├── ArrowDown → e.preventDefault/stopPropagation
|
|
34
|
+
│ activeIndex = (activeIndex + 1) % length ← 循环到开头
|
|
35
|
+
│ scrollToActive()
|
|
36
|
+
└── Enter / NumpadEnter → e.preventDefault/stopPropagation
|
|
37
|
+
onSelect(items[activeIndex])
|
|
38
|
+
|
|
39
|
+
scrollToActive():nextTick → menuRef.querySelector('.is-active')?.scrollIntoView({ block: 'nearest' })
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> **`.is-active` 依赖**:`scrollToActive` 通过 CSS 类名 `.is-active` 定位当前高亮项,调用方必须在模板中将 `activeIndex` 对应的项加上此类名。
|
|
43
|
+
|
|
44
|
+
## 渲染示例
|
|
45
|
+
|
|
46
|
+
使用 ↑ ↓ 键移动高亮,Enter 选中:
|
|
47
|
+
|
|
48
|
+
## 基础用法
|
|
49
|
+
|
|
50
|
+
```vue
|
|
51
|
+
<template>
|
|
52
|
+
<!-- 菜单容器,ref 传给 useMenuKeydown 用于可见性检测 -->
|
|
53
|
+
<div
|
|
54
|
+
ref="menuRef"
|
|
55
|
+
class="prompt-menu"
|
|
56
|
+
>
|
|
57
|
+
<div
|
|
58
|
+
v-for="(item, i) in items"
|
|
59
|
+
:key="item.id"
|
|
60
|
+
:class="['menu-item', { 'is-active': activeIndex === i }]"
|
|
61
|
+
@click="onSelect(item)"
|
|
62
|
+
@mouseenter="activeIndex = i"
|
|
63
|
+
>
|
|
64
|
+
{{ item.name }}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<script setup lang="ts">
|
|
70
|
+
import { shallowRef, useTemplateRef } from 'vue';
|
|
71
|
+
import { useMenuKeydown } from '@blueking/chat-x';
|
|
72
|
+
|
|
73
|
+
const menuRef = useTemplateRef<HTMLElement>('menuRef');
|
|
74
|
+
const items = shallowRef([
|
|
75
|
+
{ id: 'ask', name: '问问小鲸' },
|
|
76
|
+
{ id: 'translate', name: '翻译文本' },
|
|
77
|
+
{ id: 'review', name: '代码审查' },
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const onSelect = (item: (typeof items.value)[0]) => {
|
|
81
|
+
console.log('选中:', item.name);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// activeIndex 由 composable 内部管理,无需手动传入
|
|
85
|
+
// window keydown 监听自动注册,无需手动绑定 @keydown
|
|
86
|
+
const { activeIndex } = useMenuKeydown({ items, menuRef, onSelect });
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<style scoped>
|
|
90
|
+
.menu-item.is-active {
|
|
91
|
+
background: #e1ecff;
|
|
92
|
+
color: #3a84ff;
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 在 AiSlashInput 内部的实际用法
|
|
98
|
+
|
|
99
|
+
`useMenuKeydown` 被 `AiSlashMenu`(`@` 资源菜单)和 `AiPromptList`(`/` 提示词列表)内部使用:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// AiPromptList 中
|
|
103
|
+
const promptListRef = useTemplateRef<HTMLElement>('promptListRef');
|
|
104
|
+
const { activeIndex } = useMenuKeydown<string>({
|
|
105
|
+
items: computed(() => props.prompts), // ComputedRef 也可传入
|
|
106
|
+
onSelect: props.onSelect,
|
|
107
|
+
menuRef: promptListRef,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// AiSlashMenu 中
|
|
111
|
+
const menuRef = useTemplateRef<HTMLElement>('menuRef');
|
|
112
|
+
const { activeIndex } = useMenuKeydown<IAiSlashMenuItem>({
|
|
113
|
+
items: sortedResourceList,
|
|
114
|
+
onSelect: props.onSelect,
|
|
115
|
+
menuRef,
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## API
|
|
120
|
+
|
|
121
|
+
### 参数
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
useMenuKeydown<T>(props: {
|
|
125
|
+
items: ShallowRef<T[]>; // 菜单项列表
|
|
126
|
+
menuRef: Readonly<ShallowRef<HTMLElement | null>>; // 菜单容器 DOM 引用(用于可见性检测和滚动定位)
|
|
127
|
+
onSelect: (item: T) => void; // 选中回调
|
|
128
|
+
}): { activeIndex: ShallowRef<number> }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 参数说明
|
|
132
|
+
|
|
133
|
+
| 参数 | 类型 | 说明 |
|
|
134
|
+
| ---------- | --------------------------------- | ----------------------------------------------------------- |
|
|
135
|
+
| `items` | `ShallowRef<T[]>` | 菜单项数组;为空时所有按键均不响应 |
|
|
136
|
+
| `menuRef` | `ShallowRef<HTMLElement \| null>` | 菜单容器引用;`offsetParent === null`(不可见)时按键不响应 |
|
|
137
|
+
| `onSelect` | `(item: T) => void` | Enter 确认时触发,传入当前 `activeIndex` 对应的项 |
|
|
138
|
+
|
|
139
|
+
### 返回值
|
|
140
|
+
|
|
141
|
+
| 属性名 | 类型 | 初始值 | 说明 |
|
|
142
|
+
| ------------- | -------------------- | ------ | ------------------------------------------------------------------------------ |
|
|
143
|
+
| `activeIndex` | `ShallowRef<number>` | `0` | 当前高亮项索引;需在模板中绑定 `.is-active` 类;鼠标 `mouseenter` 也可修改此值 |
|
|
144
|
+
|
|
145
|
+
### 按键行为(硬编码,不可自定义)
|
|
146
|
+
|
|
147
|
+
| 按键 | 行为 |
|
|
148
|
+
| ----------------------- | -------------------------------------- |
|
|
149
|
+
| `ArrowUp` | 高亮上一项(从第 0 项循环到末尾) |
|
|
150
|
+
| `ArrowDown` | 高亮下一项(从末尾循环到第 0 项) |
|
|
151
|
+
| `Enter` / `NumpadEnter` | 选中当前高亮项,调用 `onSelect` |
|
|
152
|
+
| `Escape` | 无处理(不在此 composable 负责范围内) |
|
|
153
|
+
|
|
154
|
+
## 注意事项
|
|
155
|
+
|
|
156
|
+
1. **`handleKeydown` 内部自动注册**:在 `window` 捕获阶段(第三参数 `true`)绑定,优先于页面其他元素;调用方**无需**手动绑定 `@keydown`
|
|
157
|
+
2. **`.is-active` 类名约定**:`scrollToActive` 通过 `menuRef.querySelector('.is-active')` 定位元素,项目模板必须在 `activeIndex === i` 时添加此类
|
|
158
|
+
3. **`activeIndex` 不自动重置**:列表内容(`items`)变化时,`activeIndex` 保持不变。如需重置(如过滤后),需在外部 `watch` items 并手动将 `activeIndex.value = 0`
|
|
159
|
+
4. **可见性检测依赖 `offsetParent`**:元素通过 `display:none` 隐藏时 `offsetParent === null`,按键会被忽略;`visibility:hidden` 或 `opacity:0` 不会被忽略
|
|
160
|
+
5. **捕获阶段拦截**:`ArrowUp`/`ArrowDown`/`Enter` 均调用 `e.preventDefault()` 和 `e.stopPropagation()`,防止方向键滚动页面或 Enter 提交表单
|
|
161
|
+
|
|
162
|
+
## 关联组件
|
|
163
|
+
|
|
164
|
+
- [ChatInput](../components/molecular/chat-input.md) — `@` 菜单与 `/` 提示词列表
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<!-- AI SUMMARY -->
|
|
2
|
+
## 快速了解
|
|
3
|
+
|
|
4
|
+
useMessageGroup 接收 keyword、messages、selectedUserMessages,通过 watchEffect 产出 messageGroups(User/Assistant/Tool 合并、末尾 Loading 注入、pause 与分享勾选等)。 executionGroups 供侧边执行摘要过滤,并暴露 isShareMode、全选与 onConfirmShare。 ChatContainer 组装后传给 MessageContainer;ExecutionSummary 消费 executionGroups。
|
|
5
|
+
|
|
6
|
+
### 关联组件
|
|
7
|
+
- **chat-container** — 调用并传入 MessageContainer
|
|
8
|
+
- **message-container** — 必填 messageGroups 数据源
|
|
9
|
+
- **execution-summary** — 使用 executionGroups 与定位
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
<!-- FULL DOC -->
|
|
13
|
+
|
|
14
|
+
# useMessageGroup 消息分组
|
|
15
|
+
|
|
16
|
+
> **分类**:composable
|
|
17
|
+
|
|
18
|
+
核心消息分组逻辑,将原始 `Message[]` 数组转换为结构化的 `MessageGroup[]`。处理 Tool 消息合并、Loading 自动注入、执行摘要过滤和消息多选/分享等逻辑。
|
|
19
|
+
|
|
20
|
+
## 函数签名
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
function useMessageGroup(options: {
|
|
24
|
+
keyword?: ShallowRef<string>;
|
|
25
|
+
messages: ComputedRef<Message[]>;
|
|
26
|
+
selectedUserMessages: Ref<Message[] | undefined>;
|
|
27
|
+
}): {
|
|
28
|
+
messageGroups: Ref<MessageGroup[]>;
|
|
29
|
+
executionGroups: ComputedRef<MessageGroup[]>;
|
|
30
|
+
isShareMode: ShallowRef<boolean>;
|
|
31
|
+
isAllSelected: ComputedRef<boolean>;
|
|
32
|
+
onToggleShareAll: (isAllSelected: boolean) => void;
|
|
33
|
+
onCancelShare: () => void;
|
|
34
|
+
onConfirmShare: () => Message[];
|
|
35
|
+
};
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 分组规则
|
|
39
|
+
|
|
40
|
+
`watchEffect` 遍历 `messages` 数组,按以下规则分组:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
messages 原始数组(按顺序处理)
|
|
44
|
+
│
|
|
45
|
+
┌────┴────┐────────────┐
|
|
46
|
+
│ │ │
|
|
47
|
+
role=user role=tool 其他 role
|
|
48
|
+
│ │ │
|
|
49
|
+
① 将累积的 ② 通过 ③ 累积到
|
|
50
|
+
assistant toolCallId assistantMessages
|
|
51
|
+
消息推入 找到对应的 等待 user 消息
|
|
52
|
+
list 作为 Assistant 触发分组
|
|
53
|
+
一组,当前 消息,注入
|
|
54
|
+
user 单独 toolMessage
|
|
55
|
+
成组 后 continue
|
|
56
|
+
|
|
57
|
+
④ 遍历结束后将剩余 assistantMessages 推入 list
|
|
58
|
+
⑤ 末尾为 user 消息 → 追加 Loading 消息组
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Tool 消息处理
|
|
62
|
+
|
|
63
|
+
`role: 'tool'` 消息不会独立渲染,而是通过 `toolCallId` 注入到对应 AssistantMessage 的 `toolCall.toolMessage` 字段:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// 查找对应的 Assistant 消息
|
|
67
|
+
const toolMessage = messages.find(m => m.role === 'assistant' && m.toolCalls?.some(t => t.id === message.toolCallId));
|
|
68
|
+
// 注入到 toolCall
|
|
69
|
+
const toolCall = toolMessage.toolCalls.find(t => t.id === message.toolCallId);
|
|
70
|
+
toolCall.toolMessage = message;
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### pause 字段
|
|
74
|
+
|
|
75
|
+
每个 Assistant 消息组计算 `pause` 属性:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
pause = assistantMessages.some(m => m.property?.extra?.pause) ?? false;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`pause` 为 `true` 时,`MessageContainer` 不渲染该组的 `MessageTools` 工具栏。
|
|
82
|
+
|
|
83
|
+
## executionGroups
|
|
84
|
+
|
|
85
|
+
`executionGroups` 从 `messageGroups` 中过滤出执行类消息,供 `ExecutionSummary` 使用。每个执行组会自动从前一组用户消息中提取 `userMessageTitle`,作为执行摘要的标题显示;若无前置用户消息则回退为当前时间戳:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
const isExecutionMessage = (m: Message): boolean => {
|
|
89
|
+
return (
|
|
90
|
+
// 带 toolCalls 的 assistant 消息
|
|
91
|
+
(m.role === 'assistant' && !!m.toolCalls?.length) ||
|
|
92
|
+
// FlowAgent 类型的 activity 消息
|
|
93
|
+
(m.role === 'activity' && m.activityType === 'flow_agent')
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
支持关键词过滤,通过 `SEARCH_TEXT_EXTRACTORS` 注册表扩展可搜索文本:
|
|
99
|
+
|
|
100
|
+
| 消息类型 | 搜索范围 |
|
|
101
|
+
| ---------- | ------------------------------------------------------------ |
|
|
102
|
+
| toolCall | `function.name`、`mcpName`、`description`、`arguments`、`id` |
|
|
103
|
+
| flow_agent | `task_name`、各节点 `name` |
|
|
104
|
+
|
|
105
|
+
## 分享模式
|
|
106
|
+
|
|
107
|
+
`useMessageGroup` 提供完整的分享模式支持:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const {
|
|
111
|
+
isShareMode, // 是否处于分享模式
|
|
112
|
+
isAllSelected, // 是否全选
|
|
113
|
+
onToggleShareAll, // 切换全选
|
|
114
|
+
onCancelShare, // 取消分享(清空选中 + 退出分享模式)
|
|
115
|
+
onConfirmShare, // 确认分享(返回选中的消息)
|
|
116
|
+
} = useMessageGroup(options);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
选中联动规则:
|
|
120
|
+
|
|
121
|
+
- 选中用户消息组 → 其后紧邻的 AI 回复组视觉联动选中
|
|
122
|
+
- 取消用户消息组 → 关联 AI 回复组同时取消
|
|
123
|
+
|
|
124
|
+
## 使用示例
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { computed, ref as deepRef, shallowRef } from 'vue';
|
|
128
|
+
import { useMessageGroup, type Message } from '@blueking/chat-x';
|
|
129
|
+
|
|
130
|
+
const keyword = shallowRef('');
|
|
131
|
+
const messages = computed(() => props.messages);
|
|
132
|
+
const selectedUserMessages = deepRef<Message[]>([]);
|
|
133
|
+
|
|
134
|
+
const { messageGroups, executionGroups, isShareMode, isAllSelected, onToggleShareAll, onCancelShare, onConfirmShare } =
|
|
135
|
+
useMessageGroup({
|
|
136
|
+
keyword,
|
|
137
|
+
messages,
|
|
138
|
+
selectedUserMessages,
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## 返回值说明
|
|
143
|
+
|
|
144
|
+
| 属性/方法名 | 类型 | 说明 |
|
|
145
|
+
| ---------------- | ----------------------------- | --------------------------------------------------------------------------- |
|
|
146
|
+
| messageGroups | `Ref<MessageGroup[]>` | 完整消息分组列表 |
|
|
147
|
+
| executionGroups | `ComputedRef<MessageGroup[]>` | 仅包含执行类消息的分组(工具调用 + FlowAgent),自动提取 `userMessageTitle` |
|
|
148
|
+
| isShareMode | `ShallowRef<boolean>` | 是否处于分享模式 |
|
|
149
|
+
| isAllSelected | `ComputedRef<boolean>` | 所有用户消息组是否全部选中 |
|
|
150
|
+
| onToggleShareAll | `(checked: boolean) => void` | 切换全选 |
|
|
151
|
+
| onCancelShare | `() => void` | 取消分享模式 |
|
|
152
|
+
| onConfirmShare | `() => Message[]` | 确认分享,返回选中的消息数组 |
|
|
153
|
+
|
|
154
|
+
## 类型定义
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { type MessageGroup } from '@blueking/chat-x';
|
|
158
|
+
|
|
159
|
+
interface MessageGroup {
|
|
160
|
+
uuid: string;
|
|
161
|
+
type: MessageRole;
|
|
162
|
+
messages: Message[];
|
|
163
|
+
checked: boolean;
|
|
164
|
+
isHover: boolean;
|
|
165
|
+
pause?: boolean;
|
|
166
|
+
startTime?: number;
|
|
167
|
+
userMessageTitle?: number | string;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## 关联组件
|
|
172
|
+
|
|
173
|
+
- [ChatContainer](../components/molecular/chat-container.md) — 调用 useMessageGroup 并下传分组
|
|
174
|
+
- [MessageContainer](../components/molecular/message-container.md) — 渲染 messageGroups
|
|
175
|
+
- [ExecutionSummary](../components/molecular/execution-summary.md) — 消费 executionGroups
|