@kernelift/ai-chat 2.0.0 → 2.1.0
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/CHANGELOG.md +13 -0
- package/README.md +925 -244
- package/SSE-Client.md +492 -451
- package/dist/ai-chat.css +1 -1
- package/dist/index.d.ts +20 -4
- package/dist/index.js +5053 -5015
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,94 +1,128 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kernelift/ai-chat
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/js/%40kernelift%2Fai-chat)
|
|
4
|
+
[](https://opensource.org/licenses/GPL-3.0)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
基于 Vue 3 + TypeScript 的现代化 AI 聊天框组件,提供企业级的对话界面解决方案。
|
|
6
7
|
|
|
7
|
-
##
|
|
8
|
+
## ✨ 特性
|
|
8
9
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
- [API 参考](#api-参考)
|
|
10
|
+
- 🚀 **现代化架构** - 基于 Vue 3 Composition API + TypeScript
|
|
11
|
+
- 💬 **流式对话** - 支持 SSE 实时通信和流式消息显示
|
|
12
|
+
- 🎨 **主题系统** - 内置亮色/暗色主题,支持自定义主题色
|
|
13
|
+
- 📱 **响应式设计** - 完美适配桌面端和移动端
|
|
14
|
+
- 📚 **历史管理** - 智能的对话历史记录管理
|
|
15
|
+
- 🛠️ **高度可定制** - 丰富的插槽和配置选项
|
|
16
|
+
- 🌐 **国际化** - 内置中英文支持
|
|
17
|
+
- 🧠 **AI 能力** - 支持深度思考、联网搜索等 AI 功能
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## 📦 安装
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
```bash
|
|
22
|
+
# 使用 pnpm
|
|
23
|
+
pnpm add @kernelift/ai-chat
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
# 使用 npm
|
|
26
|
+
npm install @kernelift/ai-chat
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- 📚 **历史记录** - 侧边栏对话历史管理
|
|
29
|
-
- 🛠️ **工具集成** - 支持深度思考、联网搜索等功能
|
|
30
|
-
- 🎯 **可定制** - 丰富的插槽和配置选项
|
|
28
|
+
# 使用 yarn
|
|
29
|
+
yarn add @kernelift/ai-chat
|
|
30
|
+
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
### 依赖要求
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
- Vue 3.3+
|
|
35
|
+
- TypeScript 5.0+
|
|
36
|
+
- @kernelift/markdown (workspace:\*)
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
# 安装核心依赖
|
|
38
|
-
pnpm install @kernelift/markdown @kernelift/ai-chat
|
|
39
|
-
```
|
|
38
|
+
## 🚀 快速开始
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
### 基础用法
|
|
42
41
|
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
```vue
|
|
43
|
+
<template>
|
|
44
|
+
<ChatContainer
|
|
45
|
+
v-model:messages="messages"
|
|
46
|
+
v-model:inputText="inputText"
|
|
47
|
+
v-model:loading="loading"
|
|
48
|
+
:records="records"
|
|
49
|
+
@send="handleSend"
|
|
50
|
+
@bubble-event="handleBubbleEvent"
|
|
51
|
+
/>
|
|
52
|
+
</template>
|
|
46
53
|
|
|
47
|
-
|
|
54
|
+
<script setup>
|
|
55
|
+
import { ref } from 'vue'
|
|
56
|
+
import { ChatContainer } from '@kernelift/ai-chat'
|
|
57
|
+
import '@kernelift/ai-chat/style.css'
|
|
58
|
+
import type { ChatMessage, ChatRecord, BubbleEvent } from '@kernelift/ai-chat'
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
import '@kernelift/ai-chat/style.css';
|
|
60
|
+
const messages = ref<ChatMessage[]>([])
|
|
61
|
+
const inputText = ref('')
|
|
62
|
+
const loading = ref(false)
|
|
63
|
+
const records = ref<ChatRecord[]>([])
|
|
54
64
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
const handleSend = (text: string) => {
|
|
66
|
+
console.log('发送消息:', text)
|
|
67
|
+
}
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
import {
|
|
63
|
-
ChatContainer,
|
|
64
|
-
ChatBubble,
|
|
65
|
-
ChatSender,
|
|
66
|
-
ChatSidebar,
|
|
67
|
-
ChatHeader,
|
|
68
|
-
ThinkingProcess
|
|
69
|
-
} from '@kernelift/ai-chat';
|
|
70
|
-
import '@kernelift/ai-chat/style.css';
|
|
71
|
-
import type { ChatMessage, ChatRecord, BubbleEvent } from '@kernelift/ai-chat';
|
|
69
|
+
const handleBubbleEvent = (eventName: BubbleEvent, data: ChatMessage) => {
|
|
70
|
+
console.log('气泡事件:', eventName, data)
|
|
71
|
+
}
|
|
72
72
|
</script>
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
##
|
|
75
|
+
## 📚 目录
|
|
76
|
+
|
|
77
|
+
- [组件架构](#组件架构)
|
|
78
|
+
- [组件详解](#组件详解)
|
|
79
|
+
- [事件处理](#事件处理)
|
|
80
|
+
- [样式定制](#样式定制)
|
|
81
|
+
- [完整示例](#完整示例)
|
|
82
|
+
- [API 文档](#api-文档)
|
|
83
|
+
- [常见问题](#常见问题)
|
|
84
|
+
|
|
85
|
+
## 🏗️ 组件架构
|
|
76
86
|
|
|
77
|
-
###
|
|
87
|
+
### 组件层次结构
|
|
78
88
|
|
|
79
89
|
```
|
|
80
90
|
ChatContainer (主容器)
|
|
81
91
|
├── ChatSidebar (侧边栏)
|
|
92
|
+
│ ├── Logo 区域
|
|
93
|
+
│ ├── 新建聊天按钮
|
|
94
|
+
│ ├── 聊天记录列表
|
|
95
|
+
│ └── 记录操作菜单
|
|
82
96
|
├── ChatHeader (头部)
|
|
83
|
-
├──
|
|
84
|
-
|
|
85
|
-
├──
|
|
86
|
-
|
|
97
|
+
│ ├── Logo 显示
|
|
98
|
+
│ └── 主题切换按钮
|
|
99
|
+
├── 消息区域
|
|
100
|
+
│ ├── ChatBubble (消息气泡)
|
|
101
|
+
│ │ ├── 用户消息
|
|
102
|
+
│ │ └── AI 助手消息
|
|
103
|
+
│ │ ├── 思考过程 (ThinkingProcess)
|
|
104
|
+
│ │ ├── 消息内容
|
|
105
|
+
│ │ └── 操作按钮
|
|
106
|
+
│ └── 空状态提示
|
|
107
|
+
└── ChatSender (发送器)
|
|
108
|
+
├── 工具按钮区域
|
|
109
|
+
├── 输入框
|
|
110
|
+
└── 发送按钮
|
|
87
111
|
```
|
|
88
112
|
|
|
89
|
-
|
|
113
|
+
### 数据流
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
用户输入 → ChatSender → send事件 → 父组件处理 → 更新messages → ChatBubble渲染
|
|
117
|
+
↓
|
|
118
|
+
AI响应处理 → 流式更新 → 实时显示
|
|
119
|
+
```
|
|
90
120
|
|
|
91
|
-
|
|
121
|
+
## 🧩 组件详解
|
|
122
|
+
|
|
123
|
+
### ChatContainer - 主容器
|
|
124
|
+
|
|
125
|
+
主容器组件,负责整体布局和状态管理。
|
|
92
126
|
|
|
93
127
|
```vue
|
|
94
128
|
<template>
|
|
@@ -96,38 +130,30 @@ ChatContainer (主容器)
|
|
|
96
130
|
v-model:messages="messages"
|
|
97
131
|
v-model:inputText="inputText"
|
|
98
132
|
v-model:loading="loading"
|
|
99
|
-
:
|
|
133
|
+
v-model:record-id="activeRecordId"
|
|
134
|
+
v-model:enable-think="enableThink"
|
|
135
|
+
v-model:enable-net="enableNet"
|
|
136
|
+
:records="records"
|
|
137
|
+
:theme-mode="themeMode"
|
|
138
|
+
:primary-color="#615ced"
|
|
139
|
+
:has-header="true"
|
|
140
|
+
:has-theme-mode="true"
|
|
141
|
+
:show-workspace="true"
|
|
142
|
+
:input-height="140"
|
|
100
143
|
@send="handleSend"
|
|
101
144
|
@bubble-event="handleBubbleEvent"
|
|
102
|
-
|
|
145
|
+
@create-record="handleCreateRecord"
|
|
146
|
+
@change-record="handleChangeRecord"
|
|
147
|
+
@change-theme="handleThemeChange"
|
|
148
|
+
>
|
|
149
|
+
<!-- 插槽内容 -->
|
|
150
|
+
</ChatContainer>
|
|
103
151
|
</template>
|
|
104
|
-
|
|
105
|
-
<script setup lang="ts">
|
|
106
|
-
import { ref } from 'vue';
|
|
107
|
-
import type { ChatMessage, ChatRecord, BubbleEvent } from '@kernelift/ai-chat';
|
|
108
|
-
|
|
109
|
-
const messages = ref<ChatMessage[]>([]);
|
|
110
|
-
const inputText = ref('');
|
|
111
|
-
const loading = ref(false);
|
|
112
|
-
const chatRecords = ref<ChatRecord[]>([]);
|
|
113
|
-
|
|
114
|
-
const handleSend = (text: string) => {
|
|
115
|
-
// 处理发送消息
|
|
116
|
-
console.log('发送消息:', text);
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const handleBubbleEvent = (eventName: BubbleEvent, data: ChatMessage) => {
|
|
120
|
-
// 处理气泡事件
|
|
121
|
-
console.log('气泡事件:', eventName, data);
|
|
122
|
-
};
|
|
123
|
-
</script>
|
|
124
152
|
```
|
|
125
153
|
|
|
126
|
-
## 子组件详解
|
|
127
|
-
|
|
128
154
|
### ChatBubble - 消息气泡
|
|
129
155
|
|
|
130
|
-
|
|
156
|
+
负责显示单条消息,支持多种消息类型和交互操作。
|
|
131
157
|
|
|
132
158
|
```vue
|
|
133
159
|
<template>
|
|
@@ -157,9 +183,22 @@ const handleBubbleEvent = (eventName: BubbleEvent, data: ChatMessage) => {
|
|
|
157
183
|
</template>
|
|
158
184
|
```
|
|
159
185
|
|
|
186
|
+
#### 消息类型支持
|
|
187
|
+
|
|
188
|
+
- **用户消息** (`role: 'user'`) - 右对齐,蓝色背景
|
|
189
|
+
- **AI 助手消息** (`role: 'assistant'`) - 左对齐,白色背景
|
|
190
|
+
- **系统消息** (`role: 'system'`) - 居中显示,特殊样式
|
|
191
|
+
|
|
192
|
+
#### 消息状态
|
|
193
|
+
|
|
194
|
+
- `loading` - 正在生成中,显示加载动画
|
|
195
|
+
- `isThinking` - 正在思考,显示思考过程
|
|
196
|
+
- `isError` - 错误状态,显示错误信息
|
|
197
|
+
- `isTerminated` - 已终止生成
|
|
198
|
+
|
|
160
199
|
### ChatSender - 消息发送器
|
|
161
200
|
|
|
162
|
-
|
|
201
|
+
提供消息输入、工具按钮和发送功能。
|
|
163
202
|
|
|
164
203
|
```vue
|
|
165
204
|
<template>
|
|
@@ -178,13 +217,27 @@ const handleBubbleEvent = (eventName: BubbleEvent, data: ChatMessage) => {
|
|
|
178
217
|
</el-button>
|
|
179
218
|
</el-tooltip>
|
|
180
219
|
</template>
|
|
220
|
+
|
|
221
|
+
<!-- 自定义发送按钮 -->
|
|
222
|
+
<template #send-button="{ state, execute }">
|
|
223
|
+
<button :disabled="!state.inputValue || state.loading" @click="execute">
|
|
224
|
+
{{ state.loading ? '停止' : '发送' }}
|
|
225
|
+
</button>
|
|
226
|
+
</template>
|
|
181
227
|
</ChatSender>
|
|
182
228
|
</template>
|
|
183
229
|
```
|
|
184
230
|
|
|
231
|
+
#### 功能特性
|
|
232
|
+
|
|
233
|
+
- **自动调整高度** - 根据内容自动调整输入框高度
|
|
234
|
+
- **快捷键支持** - Enter 发送,Shift+Enter 换行
|
|
235
|
+
- **工具按钮** - 支持深度思考、联网搜索等功能
|
|
236
|
+
- **粘贴处理** - 智能处理粘贴的文本和图片
|
|
237
|
+
|
|
185
238
|
### ChatSidebar - 侧边栏
|
|
186
239
|
|
|
187
|
-
|
|
240
|
+
管理对话历史记录和导航。
|
|
188
241
|
|
|
189
242
|
```vue
|
|
190
243
|
<template>
|
|
@@ -205,13 +258,25 @@ const handleBubbleEvent = (eventName: BubbleEvent, data: ChatMessage) => {
|
|
|
205
258
|
<span>AI对话平台</span>
|
|
206
259
|
</div>
|
|
207
260
|
</template>
|
|
261
|
+
|
|
262
|
+
<!-- 自定义新建按钮 -->
|
|
263
|
+
<template #new-chat-button>
|
|
264
|
+
<button class="new-chat-btn">+ 新建对话</button>
|
|
265
|
+
</template>
|
|
208
266
|
</ChatSidebar>
|
|
209
267
|
</template>
|
|
210
268
|
```
|
|
211
269
|
|
|
270
|
+
#### 功能特性
|
|
271
|
+
|
|
272
|
+
- **历史记录管理** - 创建、切换、删除对话记录
|
|
273
|
+
- **搜索功能** - 快速搜索历史对话
|
|
274
|
+
- **折叠展开** - 支持侧边栏折叠以节省空间
|
|
275
|
+
- **拖拽调整** - 可拖拽调整侧边栏宽度
|
|
276
|
+
|
|
212
277
|
### ThinkingProcess - 思考过程
|
|
213
278
|
|
|
214
|
-
展示AI的思考推理过程。
|
|
279
|
+
展示 AI 的思考推理过程。
|
|
215
280
|
|
|
216
281
|
```vue
|
|
217
282
|
<template>
|
|
@@ -232,53 +297,71 @@ const handleBubbleEvent = (eventName: BubbleEvent, data: ChatMessage) => {
|
|
|
232
297
|
</template>
|
|
233
298
|
```
|
|
234
299
|
|
|
235
|
-
|
|
300
|
+
#### 功能特性
|
|
301
|
+
|
|
302
|
+
- **折叠展开** - 支持思考过程的折叠和展开
|
|
303
|
+
- **Markdown 渲染** - 思考过程支持 Markdown 格式
|
|
304
|
+
- **实时更新** - 思考过程中实时显示内容
|
|
236
305
|
|
|
237
|
-
|
|
306
|
+
## 🎯 事件处理
|
|
307
|
+
|
|
308
|
+
### 消息发送事件
|
|
238
309
|
|
|
239
310
|
```typescript
|
|
240
|
-
//
|
|
241
|
-
const handleSend = (text: string) => {
|
|
242
|
-
// 添加用户消息
|
|
311
|
+
// 发送消息
|
|
312
|
+
const handleSend = (text: string, isEnableThink?: boolean, isEnableNet?: boolean) => {
|
|
313
|
+
// 1. 添加用户消息
|
|
243
314
|
messages.value.push({
|
|
244
315
|
id: generateId(),
|
|
245
316
|
role: 'user',
|
|
246
317
|
content: text,
|
|
247
|
-
timestamp: Date.now()
|
|
318
|
+
timestamp: Date.now(),
|
|
319
|
+
isThinking: isEnableThink,
|
|
320
|
+
extraData: {
|
|
321
|
+
question: text
|
|
322
|
+
}
|
|
248
323
|
});
|
|
249
324
|
|
|
250
|
-
// 调用AI接口
|
|
251
|
-
callAI(text).then((response) => {
|
|
325
|
+
// 2. 调用AI接口
|
|
326
|
+
callAI(text, isEnableThink, isEnableNet).then((response) => {
|
|
252
327
|
// 处理AI响应
|
|
253
328
|
});
|
|
254
329
|
};
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### 气泡交互事件
|
|
255
333
|
|
|
256
|
-
|
|
334
|
+
```typescript
|
|
257
335
|
const handleBubbleEvent = (eventName: BubbleEvent, message: ChatMessage) => {
|
|
258
336
|
switch (eventName) {
|
|
259
337
|
case 'like':
|
|
260
338
|
// 点赞处理
|
|
261
|
-
message.isLiked =
|
|
339
|
+
message.isLiked = !message.isLiked;
|
|
262
340
|
message.isDisliked = false;
|
|
263
341
|
break;
|
|
342
|
+
|
|
264
343
|
case 'dislike':
|
|
265
344
|
// 点踩处理
|
|
345
|
+
message.isDisliked = !message.isDisliked;
|
|
266
346
|
message.isLiked = false;
|
|
267
|
-
message.isDisliked = true;
|
|
268
347
|
break;
|
|
348
|
+
|
|
269
349
|
case 'copy':
|
|
270
350
|
// 复制消息
|
|
271
351
|
navigator.clipboard.writeText(message.content);
|
|
272
352
|
ElMessage.success('已复制到剪贴板');
|
|
273
353
|
break;
|
|
354
|
+
|
|
274
355
|
case 'reload':
|
|
275
356
|
// 重新生成
|
|
276
357
|
handleRegenerate(message);
|
|
277
358
|
break;
|
|
359
|
+
|
|
278
360
|
case 'terminate':
|
|
279
361
|
// 终止生成
|
|
280
362
|
handleTerminate(message);
|
|
281
363
|
break;
|
|
364
|
+
|
|
282
365
|
case 'bookmark':
|
|
283
366
|
// 收藏消息
|
|
284
367
|
message.isBookmarked = !message.isBookmarked;
|
|
@@ -287,12 +370,52 @@ const handleBubbleEvent = (eventName: BubbleEvent, message: ChatMessage) => {
|
|
|
287
370
|
};
|
|
288
371
|
```
|
|
289
372
|
|
|
373
|
+
### 记录管理事件
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
// 创建新记录
|
|
377
|
+
const handleCreateRecord = (messages: ChatMessage[]) => {
|
|
378
|
+
const newRecord: ChatRecord = {
|
|
379
|
+
id: generateId(),
|
|
380
|
+
name: messages[0]?.content.slice(0, 20) + '...' || '新对话',
|
|
381
|
+
content: messages[0]?.content || '',
|
|
382
|
+
type: 'text',
|
|
383
|
+
createTime: new Date().toLocaleDateString(),
|
|
384
|
+
userId: 'current-user',
|
|
385
|
+
extraData: { messages }
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
records.value.unshift(newRecord);
|
|
389
|
+
activeRecordId.value = newRecord.id;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// 切换记录
|
|
393
|
+
const handleChangeRecord = (record?: ChatRecord) => {
|
|
394
|
+
if (record) {
|
|
395
|
+
messages.value = record.extraData?.messages || [];
|
|
396
|
+
activeRecordId.value = record.id;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// 删除记录
|
|
401
|
+
const handleDeleteRecord = (record: ChatRecord) => {
|
|
402
|
+
const index = records.value.findIndex((r) => r.id === record.id);
|
|
403
|
+
if (index > -1) {
|
|
404
|
+
records.value.splice(index, 1);
|
|
405
|
+
if (activeRecordId.value === record.id) {
|
|
406
|
+
activeRecordId.value = null;
|
|
407
|
+
messages.value = [];
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
```
|
|
412
|
+
|
|
290
413
|
### 流式消息处理
|
|
291
414
|
|
|
292
415
|
```typescript
|
|
293
|
-
import { SSEClient } from '
|
|
416
|
+
import { SSEClient } from '@kernelift/ai-chat';
|
|
294
417
|
|
|
295
|
-
const handleStreamResponse = async (question: string) => {
|
|
418
|
+
const handleStreamResponse = async (question: string, enableThink?: boolean) => {
|
|
296
419
|
const client = new SSEClient('your-token', 'https://api.example.com');
|
|
297
420
|
|
|
298
421
|
let currentMessage: ChatMessage = {
|
|
@@ -300,7 +423,8 @@ const handleStreamResponse = async (question: string) => {
|
|
|
300
423
|
role: 'assistant',
|
|
301
424
|
content: '',
|
|
302
425
|
timestamp: Date.now(),
|
|
303
|
-
loading: true
|
|
426
|
+
loading: true,
|
|
427
|
+
isThinking: enableThink
|
|
304
428
|
};
|
|
305
429
|
|
|
306
430
|
messages.value.push(currentMessage);
|
|
@@ -310,6 +434,13 @@ const handleStreamResponse = async (question: string) => {
|
|
|
310
434
|
currentMessage.content += content;
|
|
311
435
|
},
|
|
312
436
|
|
|
437
|
+
onThinkingDelta: (content: string) => {
|
|
438
|
+
if (!currentMessage.thoughtProcess) {
|
|
439
|
+
currentMessage.thoughtProcess = '';
|
|
440
|
+
}
|
|
441
|
+
currentMessage.thoughtProcess += content;
|
|
442
|
+
},
|
|
443
|
+
|
|
313
444
|
onToolCallDelta: (data: any) => {
|
|
314
445
|
// 处理工具调用
|
|
315
446
|
if (!currentMessage.toolCalls) {
|
|
@@ -333,12 +464,426 @@ const handleStreamResponse = async (question: string) => {
|
|
|
333
464
|
};
|
|
334
465
|
|
|
335
466
|
await client.connect('/api/chat/stream', handlers, {
|
|
336
|
-
body: { question }
|
|
467
|
+
body: { question, enable_thinking: enableThink }
|
|
337
468
|
});
|
|
338
469
|
};
|
|
339
470
|
```
|
|
340
471
|
|
|
341
|
-
## 样式定制
|
|
472
|
+
## 🎨 样式定制
|
|
473
|
+
|
|
474
|
+
### CSS 变量定制
|
|
475
|
+
|
|
476
|
+
```css
|
|
477
|
+
/* 全局样式变量 */
|
|
478
|
+
:root {
|
|
479
|
+
/* 主题色 */
|
|
480
|
+
--kl-chat-primary-color: #615ced;
|
|
481
|
+
--kl-chat-primary-rgb: 97, 92, 237;
|
|
482
|
+
|
|
483
|
+
/* 文本颜色 */
|
|
484
|
+
--kl-text-color: #1b1b1b;
|
|
485
|
+
--kl-note-color: #9ca3af;
|
|
486
|
+
--kl-process-text-color: #61666b;
|
|
487
|
+
|
|
488
|
+
/* 背景色 */
|
|
489
|
+
--kl-background-color: #fff;
|
|
490
|
+
--kl-main-background-color: #f7f8fc;
|
|
491
|
+
--kl-sender-background-color: #fff;
|
|
492
|
+
|
|
493
|
+
/* 输入框 */
|
|
494
|
+
--kl-sender-text-color: #4a4a4a;
|
|
495
|
+
--kl-tool-button-default-color: #7d7d7d;
|
|
496
|
+
|
|
497
|
+
/* 边框 */
|
|
498
|
+
--kl-border-color: #d1d5db;
|
|
499
|
+
|
|
500
|
+
/* 主题色衍生 */
|
|
501
|
+
--kl-color-primary: var(--kl-chat-primary-color);
|
|
502
|
+
--kl-color-primary-light-3: #8a86f1;
|
|
503
|
+
--kl-color-primary-light-5: #a9a6f5;
|
|
504
|
+
--kl-color-primary-light-7: #c8c6f8;
|
|
505
|
+
--kl-color-primary-light-8: #dddafc;
|
|
506
|
+
--kl-color-primary-light-9: #f2f1fe;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* 暗色主题 */
|
|
510
|
+
.dark {
|
|
511
|
+
--kl-chat-primary-color: #8a86f1;
|
|
512
|
+
--kl-text-color: #e5e7eb;
|
|
513
|
+
--kl-note-color: #9ca3af;
|
|
514
|
+
--kl-background-color: #1a1a1a;
|
|
515
|
+
--kl-main-background-color: #262626;
|
|
516
|
+
--kl-sender-background-color: #2d2d2d;
|
|
517
|
+
--kl-sender-text-color: #e5e7eb;
|
|
518
|
+
--kl-border-color: #374151;
|
|
519
|
+
--kl-process-text-color: #9ca3af;
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### SCSS 样式定制
|
|
524
|
+
|
|
525
|
+
```scss
|
|
526
|
+
// 自定义主题
|
|
527
|
+
.kernelift-chat-container {
|
|
528
|
+
// 主容器样式
|
|
529
|
+
border-radius: 16px;
|
|
530
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
|
531
|
+
overflow: hidden;
|
|
532
|
+
|
|
533
|
+
&__aside {
|
|
534
|
+
// 侧边栏样式
|
|
535
|
+
background: linear-gradient(180deg, #ffffff 0%, #f8f9fa 100%);
|
|
536
|
+
border-right: 1px solid var(--kl-border-color);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
&__messages-section {
|
|
540
|
+
// 消息区域样式
|
|
541
|
+
background-color: var(--kl-main-background-color);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
&__sender {
|
|
545
|
+
// 发送器样式
|
|
546
|
+
background-color: var(--kl-sender-background-color);
|
|
547
|
+
border-top: 1px solid var(--kl-border-color);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// 自定义消息气泡
|
|
552
|
+
.custom-chat-bubble {
|
|
553
|
+
.kernelift-chat-bubble__assistant {
|
|
554
|
+
background: linear-gradient(135deg, #ffffff 0%, #f0f4ff 100%);
|
|
555
|
+
border: 1px solid #e1e8ff;
|
|
556
|
+
border-radius: 12px;
|
|
557
|
+
|
|
558
|
+
&:hover {
|
|
559
|
+
box-shadow: 0 4px 12px rgba(97, 92, 237, 0.1);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.kernelift-chat-bubble__user {
|
|
564
|
+
&-content {
|
|
565
|
+
background: linear-gradient(135deg, var(--kl-chat-primary-color) 0%, #8a86f1 100%);
|
|
566
|
+
color: white;
|
|
567
|
+
border-radius: 12px;
|
|
568
|
+
|
|
569
|
+
&:hover {
|
|
570
|
+
opacity: 0.9;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// 自定义发送器
|
|
577
|
+
.custom-chat-sender {
|
|
578
|
+
.kernelift-chat-sender__textarea {
|
|
579
|
+
border-radius: 8px;
|
|
580
|
+
border: 2px solid var(--kl-border-color);
|
|
581
|
+
|
|
582
|
+
&:focus {
|
|
583
|
+
border-color: var(--kl-chat-primary-color);
|
|
584
|
+
box-shadow: 0 0 0 3px rgba(97, 92, 237, 0.1);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.kernelift-chat-sender__send-button {
|
|
589
|
+
background: linear-gradient(135deg, var(--kl-chat-primary-color) 0%, #8a86f1 100%);
|
|
590
|
+
border-radius: 8px;
|
|
591
|
+
|
|
592
|
+
&:hover:not(:disabled) {
|
|
593
|
+
transform: translateY(-1px);
|
|
594
|
+
box-shadow: 0 4px 12px rgba(97, 92, 237, 0.3);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### 响应式样式
|
|
601
|
+
|
|
602
|
+
```scss
|
|
603
|
+
// 移动端适配
|
|
604
|
+
@media (max-width: 768px) {
|
|
605
|
+
.kernelift-chat-container {
|
|
606
|
+
height: 100vh;
|
|
607
|
+
border-radius: 0;
|
|
608
|
+
|
|
609
|
+
&__aside {
|
|
610
|
+
position: fixed;
|
|
611
|
+
left: -100%;
|
|
612
|
+
top: 0;
|
|
613
|
+
height: 100vh;
|
|
614
|
+
width: 80vw;
|
|
615
|
+
z-index: 1000;
|
|
616
|
+
transition: left 0.3s ease;
|
|
617
|
+
|
|
618
|
+
&.mobile-open {
|
|
619
|
+
left: 0;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
&__main {
|
|
624
|
+
margin-left: 0;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
&__sender {
|
|
628
|
+
height: 160px;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.kernelift-chat-bubble {
|
|
633
|
+
margin: 8px 12px;
|
|
634
|
+
|
|
635
|
+
&__actions {
|
|
636
|
+
opacity: 1; // 移动端始终显示操作按钮
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
## 💻 完整示例
|
|
643
|
+
|
|
644
|
+
### 基础聊天应用
|
|
645
|
+
|
|
646
|
+
```vue
|
|
647
|
+
<template>
|
|
648
|
+
<div class="chat-app">
|
|
649
|
+
<ChatContainer
|
|
650
|
+
v-model="inputText"
|
|
651
|
+
v-model:loading="senderLoading"
|
|
652
|
+
v-model:messages="messages"
|
|
653
|
+
v-model:record-id="activeRecordId"
|
|
654
|
+
:records="records"
|
|
655
|
+
:theme-mode="themeMode"
|
|
656
|
+
:is-generate-loading="generateLoading"
|
|
657
|
+
:record-actions="recordActions"
|
|
658
|
+
has-theme-mode
|
|
659
|
+
has-thinking
|
|
660
|
+
:markdown-class-name="themeMode === 'dark' ? 'prose-invert' : 'prose'"
|
|
661
|
+
@send="handleSend"
|
|
662
|
+
@cancel="handleCancel"
|
|
663
|
+
@create-record="handleCreateRecord"
|
|
664
|
+
@change-record="handleChangeRecord"
|
|
665
|
+
@bubble-event="handleBubbleEvent"
|
|
666
|
+
@change-theme="(mode) => (themeMode = mode)"
|
|
667
|
+
>
|
|
668
|
+
<!-- 自定义空状态 -->
|
|
669
|
+
<template #empty>
|
|
670
|
+
<div class="empty-state">
|
|
671
|
+
<div class="welcome-title">AI 助手</div>
|
|
672
|
+
<div class="welcome-desc">你好!有什么可以帮助你的吗?</div>
|
|
673
|
+
</div>
|
|
674
|
+
</template>
|
|
675
|
+
|
|
676
|
+
<!-- 自定义 Logo -->
|
|
677
|
+
<template #logo>
|
|
678
|
+
<div class="brand-logo">
|
|
679
|
+
<IconRender icon="material-symbols:chat" />
|
|
680
|
+
<span>AI 对话</span>
|
|
681
|
+
</div>
|
|
682
|
+
</template>
|
|
683
|
+
</ChatContainer>
|
|
684
|
+
</div>
|
|
685
|
+
</template>
|
|
686
|
+
|
|
687
|
+
<script setup lang="ts">
|
|
688
|
+
import { ref, onUnmounted } from 'vue';
|
|
689
|
+
import {
|
|
690
|
+
ChatContainer,
|
|
691
|
+
type BubbleEvent,
|
|
692
|
+
type ChatMessage,
|
|
693
|
+
type ChatRecord,
|
|
694
|
+
type ChatRecordAction
|
|
695
|
+
} from '@kernelift/ai-chat';
|
|
696
|
+
import '@kernelift/ai-chat/style.css';
|
|
697
|
+
import { useStorage } from '@vueuse/core';
|
|
698
|
+
|
|
699
|
+
// 状态管理
|
|
700
|
+
const inputText = ref('');
|
|
701
|
+
const messages = ref<ChatMessage[]>([]);
|
|
702
|
+
const records = useStorage<ChatRecord[]>('chat-records', []);
|
|
703
|
+
const activeRecordId = ref<string | null>(null);
|
|
704
|
+
const senderLoading = ref(false);
|
|
705
|
+
const generateLoading = ref(false);
|
|
706
|
+
const themeMode = ref<'light' | 'dark'>('light');
|
|
707
|
+
|
|
708
|
+
// 记录操作
|
|
709
|
+
const recordActions: ChatRecordAction[] = [
|
|
710
|
+
{
|
|
711
|
+
id: 'edit',
|
|
712
|
+
name: '编辑',
|
|
713
|
+
icon: 'edit',
|
|
714
|
+
action: (record) => {
|
|
715
|
+
console.log('编辑记录:', record);
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
id: 'delete',
|
|
720
|
+
name: '删除',
|
|
721
|
+
icon: 'delete',
|
|
722
|
+
action: (record) => {
|
|
723
|
+
records.value = records.value.filter((r) => r.id !== record.id);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
];
|
|
727
|
+
|
|
728
|
+
// 发送消息
|
|
729
|
+
const handleSend = async (text: string, enableThink?: boolean) => {
|
|
730
|
+
inputText.value = '';
|
|
731
|
+
|
|
732
|
+
// 添加用户消息
|
|
733
|
+
messages.value.push({
|
|
734
|
+
id: Date.now().toString(),
|
|
735
|
+
role: 'user',
|
|
736
|
+
content: text,
|
|
737
|
+
timestamp: Date.now(),
|
|
738
|
+
isThinking: enableThink
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
senderLoading.value = true;
|
|
742
|
+
generateLoading.value = true;
|
|
743
|
+
|
|
744
|
+
try {
|
|
745
|
+
// 模拟 AI 响应
|
|
746
|
+
await simulateAIResponse(text, enableThink);
|
|
747
|
+
} catch (error) {
|
|
748
|
+
console.error('发送失败:', error);
|
|
749
|
+
} finally {
|
|
750
|
+
senderLoading.value = false;
|
|
751
|
+
generateLoading.value = false;
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// 模拟 AI 响应
|
|
756
|
+
const simulateAIResponse = async (text: string, enableThink?: boolean) => {
|
|
757
|
+
const responseId = Date.now().toString();
|
|
758
|
+
|
|
759
|
+
messages.value.push({
|
|
760
|
+
id: responseId,
|
|
761
|
+
role: 'assistant',
|
|
762
|
+
content: '',
|
|
763
|
+
timestamp: Date.now(),
|
|
764
|
+
loading: true,
|
|
765
|
+
isThinking: enableThink
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
const targetMessage = messages.value.find((m) => m.id === responseId)!;
|
|
769
|
+
|
|
770
|
+
// 模拟思考过程
|
|
771
|
+
if (enableThink) {
|
|
772
|
+
targetMessage.thoughtProcess = '正在分析用户问题...\n';
|
|
773
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
774
|
+
targetMessage.thoughtProcess += '整理相关信息...\n';
|
|
775
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
776
|
+
targetMessage.thoughtProcess += '生成回答...\n';
|
|
777
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
778
|
+
targetMessage.isThinking = false;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// 模拟流式响应
|
|
782
|
+
const response = `这是对"${text}"的回答。`;
|
|
783
|
+
for (let i = 0; i < response.length; i++) {
|
|
784
|
+
targetMessage.content += response[i];
|
|
785
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
targetMessage.loading = false;
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
// 取消生成
|
|
792
|
+
const handleCancel = () => {
|
|
793
|
+
generateLoading.value = false;
|
|
794
|
+
senderLoading.value = false;
|
|
795
|
+
|
|
796
|
+
const lastMessage = messages.value[messages.value.length - 1];
|
|
797
|
+
if (lastMessage?.loading) {
|
|
798
|
+
lastMessage.loading = false;
|
|
799
|
+
lastMessage.isTerminated = true;
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
// 处理气泡事件
|
|
804
|
+
const handleBubbleEvent = (event: BubbleEvent, data: ChatMessage) => {
|
|
805
|
+
switch (event) {
|
|
806
|
+
case 'like':
|
|
807
|
+
data.isLiked = !data.isLiked;
|
|
808
|
+
data.isDisliked = false;
|
|
809
|
+
break;
|
|
810
|
+
case 'dislike':
|
|
811
|
+
data.isDisliked = !data.isDisliked;
|
|
812
|
+
data.isLiked = false;
|
|
813
|
+
break;
|
|
814
|
+
case 'copy':
|
|
815
|
+
navigator.clipboard.writeText(data.content);
|
|
816
|
+
break;
|
|
817
|
+
case 'bookmark':
|
|
818
|
+
data.isBookmarked = !data.isBookmarked;
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
// 创建记录
|
|
824
|
+
const handleCreateRecord = (msgs: ChatMessage[]) => {
|
|
825
|
+
const newRecord: ChatRecord = {
|
|
826
|
+
id: Date.now().toString(),
|
|
827
|
+
name: msgs[0]?.content.slice(0, 20) + '...' || '新对话',
|
|
828
|
+
content: msgs[0]?.content || '',
|
|
829
|
+
type: 'text',
|
|
830
|
+
createTime: new Date().toLocaleDateString(),
|
|
831
|
+
userId: 'current-user',
|
|
832
|
+
extraData: { messages: msgs }
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
records.value.unshift(newRecord);
|
|
836
|
+
activeRecordId.value = newRecord.id;
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
// 切换记录
|
|
840
|
+
const handleChangeRecord = (record?: ChatRecord) => {
|
|
841
|
+
if (record) {
|
|
842
|
+
messages.value = record.extraData?.messages || [];
|
|
843
|
+
activeRecordId.value = record.id;
|
|
844
|
+
} else {
|
|
845
|
+
messages.value = [];
|
|
846
|
+
activeRecordId.value = null;
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
</script>
|
|
850
|
+
|
|
851
|
+
<style scoped>
|
|
852
|
+
.chat-app {
|
|
853
|
+
height: 100vh;
|
|
854
|
+
padding: 20px;
|
|
855
|
+
background: #f5f5f5;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.empty-state {
|
|
859
|
+
text-align: center;
|
|
860
|
+
padding: 60px 20px;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
.welcome-title {
|
|
864
|
+
font-size: 32px;
|
|
865
|
+
font-weight: bold;
|
|
866
|
+
margin-bottom: 16px;
|
|
867
|
+
color: var(--kl-chat-primary-color);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.welcome-desc {
|
|
871
|
+
font-size: 16px;
|
|
872
|
+
color: var(--kl-note-color);
|
|
873
|
+
line-height: 1.6;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.brand-logo {
|
|
877
|
+
display: flex;
|
|
878
|
+
align-items: center;
|
|
879
|
+
gap: 8px;
|
|
880
|
+
font-size: 18px;
|
|
881
|
+
font-weight: bold;
|
|
882
|
+
}
|
|
883
|
+
</style>
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
## 📖 API 文档
|
|
342
887
|
|
|
343
888
|
### CSS 变量定制
|
|
344
889
|
|
|
@@ -809,152 +1354,288 @@ function handleScrollBottom() {
|
|
|
809
1354
|
</style>
|
|
810
1355
|
```
|
|
811
1356
|
|
|
812
|
-
###
|
|
1357
|
+
### Props 属性
|
|
1358
|
+
|
|
1359
|
+
| 属性名 | 类型 | 默认值 | 说明 |
|
|
1360
|
+
| ------------------- | ------------------------ | ----------- | ------------------ |
|
|
1361
|
+
| `records` | `ChatRecord[]` | `[]` | 聊天记录列表 |
|
|
1362
|
+
| `recordActions` | `ChatRecordAction[]` | `[]` | 记录操作按钮配置 |
|
|
1363
|
+
| `hasHeader` | `boolean` | `true` | 是否显示头部 |
|
|
1364
|
+
| `hasThemeMode` | `boolean` | `false` | 是否支持主题切换 |
|
|
1365
|
+
| `hasThinking` | `boolean` | `true` | 是否支持深度思考 |
|
|
1366
|
+
| `hasNetSearch` | `boolean` | `false` | 是否支持联网搜索 |
|
|
1367
|
+
| `hasSenderTools` | `boolean` | `false` | 是否显示发送工具区 |
|
|
1368
|
+
| `showWorkspace` | `boolean` | `true` | 是否显示工作区 |
|
|
1369
|
+
| `showSender` | `boolean` | `true` | 是否显示发送框 |
|
|
1370
|
+
| `isGenerateLoading` | `boolean` | `undefined` | 是否正在生成 |
|
|
1371
|
+
| `defaultRecordId` | `string` | `undefined` | 默认记录ID |
|
|
1372
|
+
| `defaultCollapse` | `boolean` | `false` | 侧边栏默认折叠 |
|
|
1373
|
+
| `defaultAsideWidth` | `number` | `250` | 侧边栏默认宽度 |
|
|
1374
|
+
| `markdownClassName` | `string` | `undefined` | Markdown 样式类名 |
|
|
1375
|
+
| `primaryColor` | `string` | `'#615ced'` | 主题色 |
|
|
1376
|
+
| `themeMode` | `'light' \| 'dark'` | `'light'` | 主题模式 |
|
|
1377
|
+
| `enableNet` | `boolean` | `undefined` | 联网搜索启用状态 |
|
|
1378
|
+
| `enableThink` | `boolean` | `undefined` | 深度思考启用状态 |
|
|
1379
|
+
| `inputHeight` | `number` | `140` | 输入框高度 |
|
|
1380
|
+
| `onCopy` | `(code: string) => void` | `undefined` | 复制代码回调 |
|
|
1381
|
+
| `i18n` | `Record<string, any>` | `zhCN` | 国际化配置 |
|
|
1382
|
+
|
|
1383
|
+
### v-model 双向绑定
|
|
1384
|
+
|
|
1385
|
+
| 属性名 | 类型 | 说明 |
|
|
1386
|
+
| --------------------- | ---------------- | ---------------- |
|
|
1387
|
+
| `v-model` | `string` | 输入框文本 |
|
|
1388
|
+
| `v-model:messages` | `ChatMessage[]` | 消息列表 |
|
|
1389
|
+
| `v-model:loading` | `boolean` | 发送加载状态 |
|
|
1390
|
+
| `v-model:recordId` | `string \| null` | 当前记录ID |
|
|
1391
|
+
| `v-model:enableThink` | `boolean` | 深度思考启用状态 |
|
|
1392
|
+
| `v-model:enableNet` | `boolean` | 联网搜索启用状态 |
|
|
1393
|
+
|
|
1394
|
+
### Events 事件
|
|
1395
|
+
|
|
1396
|
+
| 事件名 | 参数 | 说明 |
|
|
1397
|
+
| -------------------- | ------------------------------------------------------------ | -------------- |
|
|
1398
|
+
| `send` | `(text: string, enableThink?: boolean, enableNet?: boolean)` | 发送消息 |
|
|
1399
|
+
| `cancel` | - | 取消生成 |
|
|
1400
|
+
| `clear` | - | 清空聊天 |
|
|
1401
|
+
| `create-record` | `(messages: ChatMessage[])` | 创建记录 |
|
|
1402
|
+
| `change-record` | `(record?: ChatRecord)` | 切换记录 |
|
|
1403
|
+
| `change-collapse` | `(collapse: boolean)` | 折叠状态改变 |
|
|
1404
|
+
| `change-theme` | `(theme: 'light' \| 'dark')` | 主题切换 |
|
|
1405
|
+
| `change-aside-width` | `(width: number)` | 侧边栏宽度改变 |
|
|
1406
|
+
| `click-logo` | - | 点击Logo |
|
|
1407
|
+
| `bubble-event` | `(event: BubbleEvent, message: ChatMessage)` | 气泡交互事件 |
|
|
1408
|
+
| `close-workspace` | - | 关闭工作区 |
|
|
1409
|
+
| `scroll-bottom` | - | 滚动到底部 |
|
|
1410
|
+
|
|
1411
|
+
### Slots 插槽
|
|
1412
|
+
|
|
1413
|
+
| 插槽名 | 参数 | 说明 |
|
|
1414
|
+
| ------------------------ | ----------------------------------------------------------------------- | -------------- |
|
|
1415
|
+
| `left-aside` | `{ mobile: boolean }` | 左侧边栏 |
|
|
1416
|
+
| `aside` | `{ record: ChatRecord \| undefined, mobile: boolean }` | 主侧边栏 |
|
|
1417
|
+
| `logo` | `{ mobile: boolean }` | Logo区域 |
|
|
1418
|
+
| `new-chat-button` | `{ mobile: boolean }` | 新建聊天按钮 |
|
|
1419
|
+
| `record-dropdown` | `{ mobile: boolean }` | 记录下拉菜单 |
|
|
1420
|
+
| `header` | `{ record: ChatRecord \| undefined, mobile: boolean }` | 头部区域 |
|
|
1421
|
+
| `header-logo` | `{ mobile: boolean }` | 头部Logo |
|
|
1422
|
+
| `bubble-header` | `{ data: ChatMessage, mobile: boolean }` | 气泡头部 |
|
|
1423
|
+
| `bubble-footer` | `{ data: ChatMessage, mobile: boolean }` | 气泡底部 |
|
|
1424
|
+
| `bubble-event` | `{ data: ChatMessage, mobile: boolean }` | 气泡操作区 |
|
|
1425
|
+
| `bubble-content-header` | `{ data: ChatMessage, mobile: boolean }` | 气泡内容头部 |
|
|
1426
|
+
| `bubble-content-footer` | `{ data: ChatMessage, mobile: boolean }` | 气泡内容底部 |
|
|
1427
|
+
| `bubble-thinking-header` | `{ data: ChatMessage, mobile: boolean }` | 思考过程头部 |
|
|
1428
|
+
| `bubble-loading-content` | `{ mobile: boolean }` | 加载内容 |
|
|
1429
|
+
| `empty` | `{ mobile: boolean }` | 空状态 |
|
|
1430
|
+
| `sender-tools` | `{ mobile: boolean }` | 发送工具区 |
|
|
1431
|
+
| `sender-footer-tools` | `{ value: string, loading: boolean, mobile: boolean }` | 发送器底部工具 |
|
|
1432
|
+
| `footer` | `{ mobile: boolean }` | 底部区域 |
|
|
1433
|
+
| `workspace` | `{ record: ChatRecord \| undefined, mobile: boolean }` | 工作区 |
|
|
1434
|
+
| `send-button` | `{ state: object, execute: Function, mobile: boolean }` | 发送按钮 |
|
|
1435
|
+
| `think-button` | `{ state: object, execute: Function, mobile: boolean }` | 思考按钮 |
|
|
1436
|
+
| `net-button` | `{ state: object, execute: Function, mobile: boolean }` | 联网按钮 |
|
|
1437
|
+
| `sender-textarea` | `{ state: object, execute: Function, mobile: boolean, height: number }` | 输入框 |
|
|
1438
|
+
|
|
1439
|
+
### 类型定义
|
|
1440
|
+
|
|
1441
|
+
#### ChatMessage
|
|
1442
|
+
|
|
1443
|
+
```typescript
|
|
1444
|
+
interface ChatMessage {
|
|
1445
|
+
id: string;
|
|
1446
|
+
role: 'user' | 'assistant' | 'system';
|
|
1447
|
+
content: string;
|
|
1448
|
+
loading?: boolean;
|
|
1449
|
+
isThinking?: boolean;
|
|
1450
|
+
thoughtCollapse?: boolean;
|
|
1451
|
+
thoughtProcess?: string;
|
|
1452
|
+
timestamp: number;
|
|
1453
|
+
isTerminated?: boolean;
|
|
1454
|
+
isLiked?: boolean;
|
|
1455
|
+
isDisliked?: boolean;
|
|
1456
|
+
isError?: boolean;
|
|
1457
|
+
error?: string;
|
|
1458
|
+
isBookmarked?: boolean;
|
|
1459
|
+
nextTips?: string[];
|
|
1460
|
+
toolCalls?: any[];
|
|
1461
|
+
hideFooterTools?: boolean;
|
|
1462
|
+
extraData?: Record<string, any>;
|
|
1463
|
+
}
|
|
1464
|
+
```
|
|
1465
|
+
|
|
1466
|
+
#### ChatRecord
|
|
1467
|
+
|
|
1468
|
+
```typescript
|
|
1469
|
+
interface ChatRecord {
|
|
1470
|
+
id: string;
|
|
1471
|
+
name: string;
|
|
1472
|
+
content: string;
|
|
1473
|
+
type: string;
|
|
1474
|
+
createTime: string;
|
|
1475
|
+
userId: string;
|
|
1476
|
+
updateTime?: string;
|
|
1477
|
+
extraData?: Record<string, any>;
|
|
1478
|
+
}
|
|
1479
|
+
```
|
|
1480
|
+
|
|
1481
|
+
#### BubbleEvent
|
|
1482
|
+
|
|
1483
|
+
```typescript
|
|
1484
|
+
type BubbleEvent = 'like' | 'dislike' | 'bookmark' | 'terminate' | 'reload' | 'copy';
|
|
1485
|
+
```
|
|
1486
|
+
|
|
1487
|
+
#### ChatRecordAction
|
|
1488
|
+
|
|
1489
|
+
```typescript
|
|
1490
|
+
interface ChatRecordAction {
|
|
1491
|
+
id: string;
|
|
1492
|
+
name: string;
|
|
1493
|
+
icon?: string | Component;
|
|
1494
|
+
divided?: boolean;
|
|
1495
|
+
disabled?: boolean;
|
|
1496
|
+
action: (record: ChatRecord) => void;
|
|
1497
|
+
}
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
## ❓ 常见问题
|
|
1501
|
+
|
|
1502
|
+
### Q: 如何自定义主题色?
|
|
1503
|
+
|
|
1504
|
+
A: 通过 `primary-color` 属性和 CSS 变量可以自定义主题色:
|
|
813
1505
|
|
|
814
1506
|
```vue
|
|
815
|
-
<
|
|
816
|
-
|
|
817
|
-
v-model:messages="messages"
|
|
818
|
-
v-model:inputText="inputText"
|
|
819
|
-
:records="records"
|
|
820
|
-
:has-header="true"
|
|
821
|
-
:has-sender-tools="false"
|
|
822
|
-
:show-workspace="false"
|
|
823
|
-
@send="handleSend"
|
|
824
|
-
>
|
|
825
|
-
<!-- 移动端优化的发送器 -->
|
|
826
|
-
<template #sender-footer-tools="{ value, loading, mobile }">
|
|
827
|
-
<div class="mobile-tools">
|
|
828
|
-
<el-button :disabled="!value || loading" size="small" @click="handleQuickAction">
|
|
829
|
-
快捷操作
|
|
830
|
-
</el-button>
|
|
831
|
-
</div>
|
|
832
|
-
</template>
|
|
833
|
-
</ChatContainer>
|
|
834
|
-
</template>
|
|
1507
|
+
<ChatContainer primary-color="#ff6b6b" />
|
|
1508
|
+
```
|
|
835
1509
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
}
|
|
1510
|
+
```css
|
|
1511
|
+
:root {
|
|
1512
|
+
--kl-chat-primary-color: #ff6b6b;
|
|
1513
|
+
}
|
|
1514
|
+
```
|
|
842
1515
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1516
|
+
### Q: 如何集成 AI 服务?
|
|
1517
|
+
|
|
1518
|
+
A: 在 `send` 事件中调用你的 AI 服务接口:
|
|
1519
|
+
|
|
1520
|
+
```typescript
|
|
1521
|
+
const handleSend = async (text: string) => {
|
|
1522
|
+
// 添加用户消息
|
|
1523
|
+
messages.value.push({
|
|
1524
|
+
id: generateId(),
|
|
1525
|
+
role: 'user',
|
|
1526
|
+
content: text,
|
|
1527
|
+
timestamp: Date.now()
|
|
1528
|
+
});
|
|
846
1529
|
|
|
847
|
-
|
|
848
|
-
|
|
1530
|
+
try {
|
|
1531
|
+
// 调用 AI 服务
|
|
1532
|
+
const response = await yourAIService.chat(text);
|
|
1533
|
+
|
|
1534
|
+
// 添加 AI 响应
|
|
1535
|
+
messages.value.push({
|
|
1536
|
+
id: generateId(),
|
|
1537
|
+
role: 'assistant',
|
|
1538
|
+
content: response.content,
|
|
1539
|
+
timestamp: Date.now()
|
|
1540
|
+
});
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
console.error('AI 服务错误:', error);
|
|
849
1543
|
}
|
|
850
|
-
}
|
|
851
|
-
|
|
1544
|
+
};
|
|
1545
|
+
```
|
|
1546
|
+
|
|
1547
|
+
### Q: 如何实现流式响应?
|
|
1548
|
+
|
|
1549
|
+
A: 使用内置的 SSEClient 或其他流式处理方案:
|
|
1550
|
+
|
|
1551
|
+
```typescript
|
|
1552
|
+
import { SSEClient } from '@kernelift/ai-chat';
|
|
1553
|
+
|
|
1554
|
+
const handleStreamResponse = async (question: string) => {
|
|
1555
|
+
const client = new SSEClient('token', 'https://api.example.com');
|
|
1556
|
+
|
|
1557
|
+
const currentMessage = {
|
|
1558
|
+
id: generateId(),
|
|
1559
|
+
role: 'assistant',
|
|
1560
|
+
content: '',
|
|
1561
|
+
timestamp: Date.now(),
|
|
1562
|
+
loading: true
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
messages.value.push(currentMessage);
|
|
1566
|
+
|
|
1567
|
+
await client.connect('/chat/stream', {
|
|
1568
|
+
onContent: (content) => {
|
|
1569
|
+
currentMessage.content += content;
|
|
1570
|
+
},
|
|
1571
|
+
onComplete: () => {
|
|
1572
|
+
currentMessage.loading = false;
|
|
1573
|
+
},
|
|
1574
|
+
onError: (error) => {
|
|
1575
|
+
currentMessage.loading = false;
|
|
1576
|
+
currentMessage.isError = true;
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
};
|
|
1580
|
+
```
|
|
1581
|
+
|
|
1582
|
+
### Q: 如何在移动端使用?
|
|
1583
|
+
|
|
1584
|
+
A: 组件内置了响应式支持,移动端会自动适配。可以通过 `mobile` 插槽参数进行移动端特定定制:
|
|
1585
|
+
|
|
1586
|
+
```vue
|
|
1587
|
+
<template #sender-footer-tools="{ mobile }">
|
|
1588
|
+
<div v-if="mobile" class="mobile-tools">
|
|
1589
|
+
<!-- 移动端特定工具 -->
|
|
1590
|
+
</div>
|
|
1591
|
+
</template>
|
|
1592
|
+
```
|
|
1593
|
+
|
|
1594
|
+
### Q: 如何持久化聊天记录?
|
|
1595
|
+
|
|
1596
|
+
A: 使用 `useStorage` 或其他持久化方案:
|
|
1597
|
+
|
|
1598
|
+
```typescript
|
|
1599
|
+
import { useStorage } from '@vueuse/core';
|
|
1600
|
+
|
|
1601
|
+
const records = useStorage<ChatRecord[]>('chat-records', []);
|
|
1602
|
+
const messages = useStorage<ChatMessage[]>('current-messages', []);
|
|
1603
|
+
```
|
|
1604
|
+
|
|
1605
|
+
### Q: 如何处理长对话?
|
|
1606
|
+
|
|
1607
|
+
A: 建议实现分页加载或记录分割:
|
|
1608
|
+
|
|
1609
|
+
```typescript
|
|
1610
|
+
const handleLoadMore = async () => {
|
|
1611
|
+
const olderMessages = await loadOlderMessages(currentPage);
|
|
1612
|
+
messages.value.unshift(...olderMessages);
|
|
1613
|
+
};
|
|
1614
|
+
```
|
|
1615
|
+
|
|
1616
|
+
### Q: 如何添加自定义工具按钮?
|
|
1617
|
+
|
|
1618
|
+
A: 通过 `sender-tools` 插槽添加:
|
|
1619
|
+
|
|
1620
|
+
```vue
|
|
1621
|
+
<template #sender-tools>
|
|
1622
|
+
<el-tooltip content="自定义工具">
|
|
1623
|
+
<el-button @click="handleCustomTool">
|
|
1624
|
+
<IconRender icon="custom-icon" />
|
|
1625
|
+
</el-button>
|
|
1626
|
+
</el-tooltip>
|
|
1627
|
+
</template>
|
|
852
1628
|
```
|
|
853
1629
|
|
|
854
|
-
##
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
| ------------------- | ------------------------ | ----------- | ---------------------------------- |
|
|
862
|
-
| `records` | `ChatRecord[]` | `[]` | 聊天记录列表,默认为空数组 |
|
|
863
|
-
| `recordActions` | `ChatRecordAction[]` | `[]` | 聊天记录操作按钮配置,默认为空数组 |
|
|
864
|
-
| `hasHeader` | `boolean` | `true` | 是否显示聊天区域头部,默认显示 |
|
|
865
|
-
| `hasThemeMode` | `boolean` | `false` | 是否支持主题切换,默认不支持 |
|
|
866
|
-
| `hasThinking` | `boolean` | `true` | 是否支持深度思考功能,默认支持 |
|
|
867
|
-
| `hasNetSearch` | `boolean` | `false` | 是否支持联网搜索功能,默认不支持 |
|
|
868
|
-
| `hasSenderTools` | `boolean` | `false` | 是否显示发送工具区域,默认不显示 |
|
|
869
|
-
| `showWorkspace` | `boolean` | `true` | 是否显示工作区,默认显示 |
|
|
870
|
-
| `showSender` | `boolean` | `true` | 是否显示发送框,默认显示 |
|
|
871
|
-
| `isGenerateLoading` | `boolean` | `undefined` | 是否处于生成加载状态,无默认值 |
|
|
872
|
-
| `defaultRecordId` | `string` | `undefined` | 默认选中聊天记录ID,无默认值 |
|
|
873
|
-
| `defaultCollapse` | `boolean` | `false` | 侧边栏默认折叠状态,默认展开 |
|
|
874
|
-
| `defaultAsideWidth` | `number` | `250` | 侧边栏默认宽度,250像素 |
|
|
875
|
-
| `markdownClassName` | `string` | `undefined` | Markdown内容自定义类名,无默认值 |
|
|
876
|
-
| `primaryColor` | `string` | `'#615ced'` | 主题主色调,默认紫色 `#615ced` |
|
|
877
|
-
| `themeMode` | `'light' \| 'dark'` | `'light'` | 主题模式,默认亮色模式 |
|
|
878
|
-
| `enableNet` | `boolean` | `undefined` | 联网搜索启用状态,无默认值 |
|
|
879
|
-
| `enableThink` | `boolean` | `undefined` | 深度思考启用状态,无默认值 |
|
|
880
|
-
| `inputHeight` | `number` | `140` | 发送框高度 |
|
|
881
|
-
| `onChat` | `(code: string) => void` | `undefined` | 自定义点击复制代码触发 |
|
|
882
|
-
|
|
883
|
-
#### 注意事项
|
|
884
|
-
|
|
885
|
-
1. **无默认值属性**:以下属性没有默认值,使用时需要检查是否为 `undefined`:
|
|
886
|
-
- `isGenerateLoading`
|
|
887
|
-
- `defaultRecordId`
|
|
888
|
-
- `markdownClassName`
|
|
889
|
-
- `enableNet`
|
|
890
|
-
- `enableThink`
|
|
891
|
-
2. **默认显示**:以下属性默认值为 `true`,表示默认会显示或启用:
|
|
892
|
-
- `hasHeader` - 显示头部
|
|
893
|
-
- `showWorkspace` - 显示工作区
|
|
894
|
-
- `hasThinking` - 支持深度思考
|
|
895
|
-
3. **默认不显示/不支持**:以下属性默认值为 `false`,表示默认不显示或不支持:
|
|
896
|
-
- `hasThemeMode` - 主题切换功能
|
|
897
|
-
- `hasNetSearch` - 联网搜索功能
|
|
898
|
-
- `hasSenderTools` - 发送工具区域
|
|
899
|
-
4. **尺寸相关**:`defaultAsideWidth` 默认值为 `250`,表示侧边栏默认宽度为250像素
|
|
900
|
-
5. **颜色主题**:
|
|
901
|
-
- `primaryColor` 默认紫色 (`#615ced`)
|
|
902
|
-
- `themeMode` 默认亮色模式 (`light`)
|
|
903
|
-
|
|
904
|
-
### ChatEvents 接口事件说明
|
|
905
|
-
|
|
906
|
-
| 事件名 | 参数 | 返回值类型 | 说明 |
|
|
907
|
-
| -------------------- | ------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------- |
|
|
908
|
-
| `send` | `value: string`, `isEnableThink?: boolean`, `isEnableNet?: boolean` | `void` | 发送消息时触发,携带消息内容、是否启用思考和是否启用联网搜索参数 |
|
|
909
|
-
| `cancel` | 无参数 | `void` | 取消消息生成/发送时触发 |
|
|
910
|
-
| `clear` | 无参数 | `void` | 清空当前聊天记录时触发 |
|
|
911
|
-
| `create-record` | `data: ChatMessage[]` | `void` | 创建新聊天记录时触发,携带消息数据数组 |
|
|
912
|
-
| `change-record` | `value?: ChatRecord` | `void` | 切换聊天记录时触发,携带选中的聊天记录(可选) |
|
|
913
|
-
| `change-collapse` | `collapse: boolean` | `void` | 侧边栏折叠状态改变时触发,携带当前折叠状态 |
|
|
914
|
-
| `change-theme` | `themeMode: 'light' \| 'dark'` | `void` | 主题切换时触发,携带当前主题模式 |
|
|
915
|
-
| `change-aside-width` | `width: number` | `void` | 侧边栏宽度改变时触发,携带当前宽度值(像素) |
|
|
916
|
-
| `click-logo` | 无参数 | `void` | 点击Logo时触发 |
|
|
917
|
-
| `bubble-event` | `eventName: BubbleEvent`, `data: ChatMessage` | `void` | 消息气泡交互事件触发,携带事件名称和对应的消息数据 |
|
|
918
|
-
| `close-workspace` | 无参数 | `void` | 关闭工作区(右侧面板)时触发 |
|
|
919
|
-
| `scroll-bottom` | 无参数 | `void` | 聊天记录(左侧面板)滚动至底部时触发 |
|
|
920
|
-
|
|
921
|
-
### 补充说明
|
|
922
|
-
|
|
923
|
-
2. **受控属性**:`enableNet` 和 `enableThink` 是受控属性,需要通过 `v-model:enable-net` 和 `v-model:enable-think` 进行双向绑定
|
|
924
|
-
3. **事件参数**:所有事件均通过 `emit` 方法触发,父组件可以通过 `@事件名` 监听
|
|
925
|
-
4. **类型定义**:表格中的 `ChatRecord`、`ChatRecordAction`、`ChatMessage`、`BubbleEvent` 等类型需要根据实际项目中的定义进行替换
|
|
926
|
-
5. **默认值**:所有属性的默认值均为 `undefined`,表示组件内部会有相应的默认处理逻辑
|
|
927
|
-
|
|
928
|
-
### 插槽列表
|
|
929
|
-
|
|
930
|
-
| Slot 名称 | 作用域参数 | 说明 |
|
|
931
|
-
| ------------------------ | --------------------------------------------------------------------------------------------------------- | ------------------------------ |
|
|
932
|
-
| `left-aside` | `{ mobile: boolean }` | 左侧侧边栏区域 |
|
|
933
|
-
| `aside` | `{ record: ChatRecord \| undefined, mobile: boolean }` | 主侧边栏区域,包含聊天记录列表 |
|
|
934
|
-
| `logo` | `{ mobile: boolean }` | 侧边栏 Logo 区域 |
|
|
935
|
-
| `new-chat-button` | `{ mobile: boolean }` | 新建聊天按钮区域 |
|
|
936
|
-
| `record-dropdown` | `{ mobile: boolean }` | 聊天记录下拉菜单区域 |
|
|
937
|
-
| `header` | `{ record: ChatRecord \| undefined, mobile: boolean }` | 聊天头部区域 |
|
|
938
|
-
| `header-logo` | `{ mobile: boolean }` | 折叠状态下的头部 Logo |
|
|
939
|
-
| `bubble-header` | `{ data: ChatMessage, mobile: boolean }` | 消息气泡头部 |
|
|
940
|
-
| `bubble-footer` | `{ data: ChatMessage, mobile: boolean }` | 消息气泡底部 |
|
|
941
|
-
| `bubble-event` | `{ data: ChatMessage, mobile: boolean }` | 消息气泡事件处理区域 |
|
|
942
|
-
| `bubble-content-header` | `{ data: ChatMessage, mobile: boolean }` | 消息气泡内容顶部 |
|
|
943
|
-
| `bubble-content-footer` | `{ data: ChatMessage, mobile: boolean }` | 消息气泡内容底部 |
|
|
944
|
-
| `bubble-thinking-header` | `{ data: ChatMessage, mobile: boolean }` | 思考过程头部 |
|
|
945
|
-
| `bubble-loading-content` | `{ mobile: boolean }` | 加载中的消息内容 |
|
|
946
|
-
| `empty` | `{ mobile: boolean }` | 空状态显示内容 |
|
|
947
|
-
| `sender-tools` | `{ mobile: boolean }` | 发送工具区域 |
|
|
948
|
-
| `sender-footer-tools` | `{ value: string, loading: boolean, mobile: boolean }` | 发送器底部工具区域 |
|
|
949
|
-
| `footer` | `{ mobile: boolean }` | 页面底部区域 |
|
|
950
|
-
| `workspace` | `{ record: ChatRecord \| undefined, mobile: boolean }` | 工作区内容 |
|
|
951
|
-
| `send-button` | `{ state: { inputValue: string, loading: boolean }, execute: Function, mobile: boolean }` | 发送按钮 |
|
|
952
|
-
| `think-button` | `{ state: { enableThink: boolean, hasThinking: boolean }, execute: Function, mobile: boolean }` | 思考按钮 |
|
|
953
|
-
| `net-button` | `{ state: { enableNet: boolean, hasNetSearch: boolean }, execute: Function, mobile: boolean }` | 联网按钮 |
|
|
954
|
-
| `sender-textarea` | `{ state: { inputValue: string, loading: boolean }, execute: Function, mobile: boolean, height: number }` | 发送输入文本框 |
|
|
1630
|
+
## 📄 许可证
|
|
1631
|
+
|
|
1632
|
+
GPL-3.0 License
|
|
1633
|
+
|
|
1634
|
+
## 🤝 贡献
|
|
1635
|
+
|
|
1636
|
+
欢迎提交 Issue 和 Pull Request!
|
|
955
1637
|
|
|
956
1638
|
---
|
|
957
1639
|
|
|
958
|
-
**版本**:
|
|
959
|
-
|
|
960
|
-
**License**: 商业使用需联系授权
|
|
1640
|
+
**版本**: 2.0.0
|
|
1641
|
+
**更新时间**: 2024-12-25
|