@kernelift/ai-chat
v2.1.2
Published
kernelift 前端 AI chat 聊天框组件
Maintainers
Readme
@kernelift/ai-chat
基于 Vue 3 + TypeScript 的现代化 AI 聊天框组件,提供企业级的对话界面解决方案。
✨ 特性
- 🚀 现代化架构 - 基于 Vue 3 Composition API + TypeScript
- 💬 流式对话 - 支持 SSE 实时通信和流式消息显示
- 🎨 主题系统 - 内置亮色/暗色主题,支持自定义主题色
- 📱 响应式设计 - 完美适配桌面端和移动端
- 📚 历史管理 - 智能的对话历史记录管理
- 🛠️ 高度可定制 - 丰富的插槽和配置选项
- 🌐 国际化 - 内置中英文支持
- 🧠 AI 能力 - 支持深度思考、联网搜索等 AI 功能
📦 安装
# 使用 pnpm
pnpm add @kernelift/ai-chat
# 使用 npm
npm install @kernelift/ai-chat
# 使用 yarn
yarn add @kernelift/ai-chat依赖要求
- Vue 3.3+
- TypeScript 5.0+
- @kernelift/markdown (workspace:*)
🚀 快速开始
基础用法
<template>
<ChatContainer
v-model:messages="messages"
v-model:inputText="inputText"
v-model:loading="loading"
:records="records"
@send="handleSend"
@bubble-event="handleBubbleEvent"
/>
</template>
<script setup>
import { ref } from 'vue'
import { ChatContainer } from '@kernelift/ai-chat'
import '@kernelift/ai-chat/style.css'
import type { ChatMessage, ChatRecord, BubbleEvent } from '@kernelift/ai-chat'
const messages = ref<ChatMessage[]>([])
const inputText = ref('')
const loading = ref(false)
const records = ref<ChatRecord[]>([])
const handleSend = (text: string) => {
console.log('发送消息:', text)
}
const handleBubbleEvent = (eventName: BubbleEvent, data: ChatMessage) => {
console.log('气泡事件:', eventName, data)
}
</script>📚 目录
🏗️ 组件架构
组件层次结构
ChatContainer (主容器)
├── ChatSidebar (侧边栏)
│ ├── Logo 区域
│ ├── 新建聊天按钮
│ ├── 聊天记录列表
│ └── 记录操作菜单
├── ChatHeader (头部)
│ ├── Logo 显示
│ └── 主题切换按钮
├── 消息区域
│ ├── ChatBubble (消息气泡)
│ │ ├── 用户消息
│ │ └── AI 助手消息
│ │ ├── 思考过程 (ThinkingProcess)
│ │ ├── 消息内容
│ │ └── 操作按钮
│ └── 空状态提示
└── ChatSender (发送器)
├── 工具按钮区域
├── 输入框
└── 发送按钮数据流
用户输入 → ChatSender → send事件 → 父组件处理 → 更新messages → ChatBubble渲染
↓
AI响应处理 → 流式更新 → 实时显示🧩 组件详解
ChatContainer - 主容器
主容器组件,负责整体布局和状态管理。
<template>
<ChatContainer
v-model:messages="messages"
v-model:inputText="inputText"
v-model:loading="loading"
v-model:record-id="activeRecordId"
v-model:enable-think="enableThink"
v-model:enable-net="enableNet"
:records="records"
:theme-mode="themeMode"
:primary-color="#615ced"
:has-header="true"
:has-theme-mode="true"
:show-workspace="true"
:input-height="140"
@send="handleSend"
@bubble-event="handleBubbleEvent"
@create-record="handleCreateRecord"
@change-record="handleChangeRecord"
@change-theme="handleThemeChange"
>
<!-- 插槽内容 -->
</ChatContainer>
</template>ChatBubble - 消息气泡
负责显示单条消息,支持多种消息类型和交互操作。
<template>
<ChatBubble
v-model="message"
:is-last="isLastMessage"
:markdown-class="customMarkdownClass"
@bubble-event="handleBubbleEvent"
>
<!-- 自定义消息头部 -->
<template #header>
<div class="message-time">
{{ formatTime(message.timestamp) }}
</div>
</template>
<!-- 自定义思考过程头部 -->
<template #thinking-header>
<div class="thinking-label">推理过程</div>
</template>
<!-- 自定义操作按钮 -->
<template #bubble-event>
<el-button @click="handleCustomAction(message)"> 自定义操作 </el-button>
</template>
</ChatBubble>
</template>消息类型支持
- 用户消息 (
role: 'user') - 右对齐,蓝色背景 - AI 助手消息 (
role: 'assistant') - 左对齐,白色背景 - 系统消息 (
role: 'system') - 居中显示,特殊样式
消息状态
loading- 正在生成中,显示加载动画isThinking- 正在思考,显示思考过程isError- 错误状态,显示错误信息isTerminated- 已终止生成
ChatSender - 消息发送器
提供消息输入、工具按钮和发送功能。
<template>
<ChatSender
v-model="inputText"
:loading="isSending"
:has-thinking="true"
:has-net-search="true"
@send="handleSend"
>
<!-- 自定义工具 -->
<template #tools="{ value, loading }">
<el-tooltip content="清空输入">
<el-button :disabled="!value" @click="inputText = ''">
<IconRender icon="material-symbols:clear" />
</el-button>
</el-tooltip>
</template>
<!-- 自定义发送按钮 -->
<template #send-button="{ state, execute }">
<button :disabled="!state.inputValue || state.loading" @click="execute">
{{ state.loading ? '停止' : '发送' }}
</button>
</template>
</ChatSender>
</template>功能特性
- 自动调整高度 - 根据内容自动调整输入框高度
- 快捷键支持 - Enter 发送,Shift+Enter 换行
- 工具按钮 - 支持深度思考、联网搜索等功能
- 粘贴处理 - 智能处理粘贴的文本和图片
ChatSidebar - 侧边栏
管理对话历史记录和导航。
<template>
<ChatSidebar
v-model="activeRecord"
:data="records"
:collapse="isCollapsed"
:theme-mode="theme"
@change="handleRecordChange"
@change-collapse="handleCollapse"
@change-theme="handleThemeChange"
@click-logo="handleLogoClick"
>
<!-- 自定义 Logo -->
<template #logo>
<div class="brand-logo">
<img src="/brand-logo.png" alt="Brand" />
<span>AI对话平台</span>
</div>
</template>
<!-- 自定义新建按钮 -->
<template #new-chat-button>
<button class="new-chat-btn">+ 新建对话</button>
</template>
</ChatSidebar>
</template>功能特性
- 历史记录管理 - 创建、切换、删除对话记录
- 搜索功能 - 快速搜索历史对话
- 折叠展开 - 支持侧边栏折叠以节省空间
- 拖拽调整 - 可拖拽调整侧边栏宽度
ThinkingProcess - 思考过程
展示 AI 的思考推理过程。
<template>
<ThinkingProcess
v-model:collapse="isThoughtCollapsed"
:data="thoughtContent"
:loading="isThinking"
:markdown-class="thoughtStyle"
>
<!-- 自定义头部 -->
<template #header>
<div class="thought-title">
<IconRender icon="material-symbols:psychology" />
AI思考过程
</div>
</template>
</ThinkingProcess>
</template>功能特性
- 折叠展开 - 支持思考过程的折叠和展开
- Markdown 渲染 - 思考过程支持 Markdown 格式
- 实时更新 - 思考过程中实时显示内容
🎯 事件处理
消息发送事件
// 发送消息
const handleSend = (text: string, isEnableThink?: boolean, isEnableNet?: boolean) => {
// 1. 添加用户消息
messages.value.push({
id: generateId(),
role: 'user',
content: text,
timestamp: Date.now(),
isThinking: isEnableThink,
extraData: {
question: text
}
});
// 2. 调用AI接口
callAI(text, isEnableThink, isEnableNet).then((response) => {
// 处理AI响应
});
};气泡交互事件
const handleBubbleEvent = (eventName: BubbleEvent, message: ChatMessage) => {
switch (eventName) {
case 'like':
// 点赞处理
message.isLiked = !message.isLiked;
message.isDisliked = false;
break;
case 'dislike':
// 点踩处理
message.isDisliked = !message.isDisliked;
message.isLiked = false;
break;
case 'copy':
// 复制消息
navigator.clipboard.writeText(message.content);
ElMessage.success('已复制到剪贴板');
break;
case 'reload':
// 重新生成
handleRegenerate(message);
break;
case 'terminate':
// 终止生成
handleTerminate(message);
break;
case 'bookmark':
// 收藏消息
message.isBookmarked = !message.isBookmarked;
break;
}
};记录管理事件
// 创建新记录
const handleCreateRecord = (messages: ChatMessage[]) => {
const newRecord: ChatRecord = {
id: generateId(),
name: messages[0]?.content.slice(0, 20) + '...' || '新对话',
content: messages[0]?.content || '',
type: 'text',
createTime: new Date().toLocaleDateString(),
userId: 'current-user',
extraData: { messages }
};
records.value.unshift(newRecord);
activeRecordId.value = newRecord.id;
};
// 切换记录
const handleChangeRecord = (record?: ChatRecord) => {
if (record) {
messages.value = record.extraData?.messages || [];
activeRecordId.value = record.id;
}
};
// 删除记录
const handleDeleteRecord = (record: ChatRecord) => {
const index = records.value.findIndex((r) => r.id === record.id);
if (index > -1) {
records.value.splice(index, 1);
if (activeRecordId.value === record.id) {
activeRecordId.value = null;
messages.value = [];
}
}
};流式消息处理
import { SSEClient } from '@kernelift/ai-chat';
const handleStreamResponse = async (question: string, enableThink?: boolean) => {
const client = new SSEClient('your-token', 'https://api.example.com');
let currentMessage: ChatMessage = {
id: generateId(),
role: 'assistant',
content: '',
timestamp: Date.now(),
loading: true,
isThinking: enableThink
};
messages.value.push(currentMessage);
const handlers = {
onContent: (content: string) => {
currentMessage.content += content;
},
onThinkingDelta: (content: string) => {
if (!currentMessage.thoughtProcess) {
currentMessage.thoughtProcess = '';
}
currentMessage.thoughtProcess += content;
},
onToolCallDelta: (data: any) => {
// 处理工具调用
if (!currentMessage.toolCalls) {
currentMessage.toolCalls = [];
}
currentMessage.toolCalls.push(data);
},
onComplete: (finalData: { content: string; toolCalls: any[] }) => {
currentMessage.content = finalData.content;
currentMessage.toolCalls = finalData.toolCalls;
currentMessage.loading = false;
},
onError: (error: Error) => {
console.error('流式响应错误:', error);
currentMessage.loading = false;
currentMessage.isError = true;
currentMessage.error = error.message;
}
};
await client.connect('/api/chat/stream', handlers, {
body: { question, enable_thinking: enableThink }
});
};🎨 样式定制
CSS 变量定制
/* 全局样式变量 */
:root {
/* 主题色 */
--kl-chat-primary-color: #615ced;
--kl-chat-primary-rgb: 97, 92, 237;
/* 文本颜色 */
--kl-text-color: #1b1b1b;
--kl-note-color: #9ca3af;
--kl-process-text-color: #61666b;
/* 背景色 */
--kl-background-color: #fff;
--kl-main-background-color: #f7f8fc;
--kl-sender-background-color: #fff;
/* 输入框 */
--kl-sender-text-color: #4a4a4a;
--kl-tool-button-default-color: #7d7d7d;
/* 边框 */
--kl-border-color: #d1d5db;
/* 主题色衍生 */
--kl-color-primary: var(--kl-chat-primary-color);
--kl-color-primary-light-3: #8a86f1;
--kl-color-primary-light-5: #a9a6f5;
--kl-color-primary-light-7: #c8c6f8;
--kl-color-primary-light-8: #dddafc;
--kl-color-primary-light-9: #f2f1fe;
}
/* 暗色主题 */
.dark {
--kl-chat-primary-color: #8a86f1;
--kl-text-color: #e5e7eb;
--kl-note-color: #9ca3af;
--kl-background-color: #1a1a1a;
--kl-main-background-color: #262626;
--kl-sender-background-color: #2d2d2d;
--kl-sender-text-color: #e5e7eb;
--kl-border-color: #374151;
--kl-process-text-color: #9ca3af;
}SCSS 样式定制
// 自定义主题
.kernelift-chat-container {
// 主容器样式
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
&__aside {
// 侧边栏样式
background: linear-gradient(180deg, #ffffff 0%, #f8f9fa 100%);
border-right: 1px solid var(--kl-border-color);
}
&__messages-section {
// 消息区域样式
background-color: var(--kl-main-background-color);
}
&__sender {
// 发送器样式
background-color: var(--kl-sender-background-color);
border-top: 1px solid var(--kl-border-color);
}
}
// 自定义消息气泡
.custom-chat-bubble {
.kernelift-chat-bubble__assistant {
background: linear-gradient(135deg, #ffffff 0%, #f0f4ff 100%);
border: 1px solid #e1e8ff;
border-radius: 12px;
&:hover {
box-shadow: 0 4px 12px rgba(97, 92, 237, 0.1);
}
}
.kernelift-chat-bubble__user {
&-content {
background: linear-gradient(135deg, var(--kl-chat-primary-color) 0%, #8a86f1 100%);
color: white;
border-radius: 12px;
&:hover {
opacity: 0.9;
}
}
}
}
// 自定义发送器
.custom-chat-sender {
.kernelift-chat-sender__textarea {
border-radius: 8px;
border: 2px solid var(--kl-border-color);
&:focus {
border-color: var(--kl-chat-primary-color);
box-shadow: 0 0 0 3px rgba(97, 92, 237, 0.1);
}
}
.kernelift-chat-sender__send-button {
background: linear-gradient(135deg, var(--kl-chat-primary-color) 0%, #8a86f1 100%);
border-radius: 8px;
&:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(97, 92, 237, 0.3);
}
}
}响应式样式
// 移动端适配
@media (max-width: 768px) {
.kernelift-chat-container {
height: 100vh;
border-radius: 0;
&__aside {
position: fixed;
left: -100%;
top: 0;
height: 100vh;
width: 80vw;
z-index: 1000;
transition: left 0.3s ease;
&.mobile-open {
left: 0;
}
}
&__main {
margin-left: 0;
}
&__sender {
height: 160px;
}
}
.kernelift-chat-bubble {
margin: 8px 12px;
&__actions {
opacity: 1; // 移动端始终显示操作按钮
}
}
}💻 完整示例
基础聊天应用
<template>
<div class="chat-app">
<ChatContainer
v-model="inputText"
v-model:loading="senderLoading"
v-model:messages="messages"
v-model:record-id="activeRecordId"
:records="records"
:theme-mode="themeMode"
:is-generate-loading="generateLoading"
:record-actions="recordActions"
has-theme-mode
has-thinking
:markdown-class-name="themeMode === 'dark' ? 'prose-invert' : 'prose'"
@send="handleSend"
@cancel="handleCancel"
@create-record="handleCreateRecord"
@change-record="handleChangeRecord"
@bubble-event="handleBubbleEvent"
@change-theme="(mode) => (themeMode = mode)"
>
<!-- 自定义空状态 -->
<template #empty>
<div class="empty-state">
<div class="welcome-title">AI 助手</div>
<div class="welcome-desc">你好!有什么可以帮助你的吗?</div>
</div>
</template>
<!-- 自定义 Logo -->
<template #logo>
<div class="brand-logo">
<IconRender icon="material-symbols:chat" />
<span>AI 对话</span>
</div>
</template>
</ChatContainer>
</div>
</template>
<script setup lang="ts">
import { ref, onUnmounted } from 'vue';
import {
ChatContainer,
type BubbleEvent,
type ChatMessage,
type ChatRecord,
type ChatRecordAction
} from '@kernelift/ai-chat';
import '@kernelift/ai-chat/style.css';
import { useStorage } from '@vueuse/core';
// 状态管理
const inputText = ref('');
const messages = ref<ChatMessage[]>([]);
const records = useStorage<ChatRecord[]>('chat-records', []);
const activeRecordId = ref<string | null>(null);
const senderLoading = ref(false);
const generateLoading = ref(false);
const themeMode = ref<'light' | 'dark'>('light');
// 记录操作
const recordActions: ChatRecordAction[] = [
{
id: 'edit',
name: '编辑',
icon: 'edit',
action: (record) => {
console.log('编辑记录:', record);
}
},
{
id: 'delete',
name: '删除',
icon: 'delete',
action: (record) => {
records.value = records.value.filter((r) => r.id !== record.id);
}
}
];
// 发送消息
const handleSend = async (text: string, enableThink?: boolean) => {
inputText.value = '';
// 添加用户消息
messages.value.push({
id: Date.now().toString(),
role: 'user',
content: text,
timestamp: Date.now(),
isThinking: enableThink
});
senderLoading.value = true;
generateLoading.value = true;
try {
// 模拟 AI 响应
await simulateAIResponse(text, enableThink);
} catch (error) {
console.error('发送失败:', error);
} finally {
senderLoading.value = false;
generateLoading.value = false;
}
};
// 模拟 AI 响应
const simulateAIResponse = async (text: string, enableThink?: boolean) => {
const responseId = Date.now().toString();
messages.value.push({
id: responseId,
role: 'assistant',
content: '',
timestamp: Date.now(),
loading: true,
isThinking: enableThink
});
const targetMessage = messages.value.find((m) => m.id === responseId)!;
// 模拟思考过程
if (enableThink) {
targetMessage.thoughtProcess = '正在分析用户问题...\n';
await new Promise((resolve) => setTimeout(resolve, 1000));
targetMessage.thoughtProcess += '整理相关信息...\n';
await new Promise((resolve) => setTimeout(resolve, 1000));
targetMessage.thoughtProcess += '生成回答...\n';
await new Promise((resolve) => setTimeout(resolve, 500));
targetMessage.isThinking = false;
}
// 模拟流式响应
const response = `这是对"${text}"的回答。`;
for (let i = 0; i < response.length; i++) {
targetMessage.content += response[i];
await new Promise((resolve) => setTimeout(resolve, 50));
}
targetMessage.loading = false;
};
// 取消生成
const handleCancel = () => {
generateLoading.value = false;
senderLoading.value = false;
const lastMessage = messages.value[messages.value.length - 1];
if (lastMessage?.loading) {
lastMessage.loading = false;
lastMessage.isTerminated = true;
}
};
// 处理气泡事件
const handleBubbleEvent = (event: BubbleEvent, data: ChatMessage) => {
switch (event) {
case 'like':
data.isLiked = !data.isLiked;
data.isDisliked = false;
break;
case 'dislike':
data.isDisliked = !data.isDisliked;
data.isLiked = false;
break;
case 'copy':
navigator.clipboard.writeText(data.content);
break;
case 'bookmark':
data.isBookmarked = !data.isBookmarked;
break;
}
};
// 创建记录
const handleCreateRecord = (msgs: ChatMessage[]) => {
const newRecord: ChatRecord = {
id: Date.now().toString(),
name: msgs[0]?.content.slice(0, 20) + '...' || '新对话',
content: msgs[0]?.content || '',
type: 'text',
createTime: new Date().toLocaleDateString(),
userId: 'current-user',
extraData: { messages: msgs }
};
records.value.unshift(newRecord);
activeRecordId.value = newRecord.id;
};
// 切换记录
const handleChangeRecord = (record?: ChatRecord) => {
if (record) {
messages.value = record.extraData?.messages || [];
activeRecordId.value = record.id;
} else {
messages.value = [];
activeRecordId.value = null;
}
};
</script>
<style scoped>
.chat-app {
height: 100vh;
padding: 20px;
background: #f5f5f5;
}
.empty-state {
text-align: center;
padding: 60px 20px;
}
.welcome-title {
font-size: 32px;
font-weight: bold;
margin-bottom: 16px;
color: var(--kl-chat-primary-color);
}
.welcome-desc {
font-size: 16px;
color: var(--kl-note-color);
line-height: 1.6;
}
.brand-logo {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: bold;
}
</style>📖 API 文档
CSS 变量定制
/* 在全局样式或组件样式中覆盖 */
:root {
--kl-chat-primary-color: #615ced;
--kl-chat-primary-rgb: 97, 92, 237;
--kl-color-primary: var(--kl-chat-primary-color);
--kl-color-primary-light-3: v-bind(lightColors[0]);
--kl-color-primary-light-5: v-bind(lightColors[1]);
--kl-color-primary-light-7: v-bind(lightColors[2]);
--kl-color-primary-light-8: v-bind(lightColors[3]);
--kl-color-primary-light-9: v-bind(lightColors[4]);
--kl-text-color: #1b1b1b;
--kl-note-color: #9ca3af;
--kl-background-color: #fff;
--kl-main-background-color: #f7f8fc;
--kl-sender-background-color: #fff;
--kl-sender-text-color: #4a4a4a;
--kl-tool-button-default-color: #7d7d7d;
--kl-border-color: #d1d5db;
--kl-process-text-color: #61666b;
}
/* 暗色主题 */
.dark {
--kl-chat-primary-color: #8a86f1;
--dark-background-color: #1a1a1a;
}SCSS 样式定制
// 自定义主题
.kernelift-chat-container {
// 修改主容器样式
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
&__aside {
// 修改侧边栏样式
background: linear-gradient(180deg, #ffffff 0%, #f8f9fa 100%);
}
&__messages-section {
// 修改消息区域样式
background-color: #fafbfc;
}
}
// 自定义消息气泡
.custom-chat-bubble {
.kernelift-chat-bubble__assistant {
background: linear-gradient(135deg, #ffffff 0%, #f0f4ff 100%);
border: 1px solid #e1e8ff;
}
.kernelift-chat-bubble__user {
&-content {
background: linear-gradient(135deg, var(--kl-chat-primary-color) 0%, #8a86f1 100%);
color: white;
}
}
}使用示例
完整聊天应用
<script setup lang="ts">
import {
ChatContainer,
type BubbleEvent,
type ChatMessage,
type ChatRecord,
type ChatRecordAction
} from '@kernelift/ai-chat';
import '@kernelift/ai-chat/style.css';
import OpenAI from 'openai';
import { useStorage } from '@vueuse/core';
import type { ChatCompletionCreateParamsStreaming } from 'openai/resources';
import { onUnmounted, ref, shallowRef } from 'vue';
// 当前显示的消息列表
const demoMessages = ref<ChatMessage[]>([]);
// 所有消息记录
const demoRecords = useStorage<ChatRecord[]>('demo-records', []);
// 显示工作区
const showWorkspace = ref(false);
// 发送中
const senderLoading = ref(false);
// 生成中
const generateLoading = ref(false);
// 当前激活的记录
const activeRecordId = ref<string | null>(null);
//
// 流式传输
const streamMode = ref<boolean>(true);
// 输入问题
const userQuestion = ref('');
// const chatModel = ref('deepseek-ai/DeepSeek-V3.1-Terminus');
const client = new OpenAI({
apiKey: 'sk-xxx',
baseURL: 'https://api.siliconflow.cn/v1',
// 危险,此处仅作为示范使用
dangerouslyAllowBrowser: true
});
// 创建 AbortController
const controller = shallowRef(new AbortController());
/**
* @description 发送消息
* @param value
* @param enableThink
* @param enableNet
*/
async function handleSend(value: string, enableThink?: boolean) {
// 1. 清空输入框
userQuestion.value = '';
// 2. 添加用户输入记录
demoMessages.value.push({
id: Date.now().toString(),
role: 'user',
content: value,
timestamp: Date.now(),
isThinking: enableThink,
extraData: {
question: value
}
});
// 3. 添加机器人输入记录
senderLoading.value = true;
generateLoading.value = true;
if (streamMode.value) {
try {
const stream = await client.chat.completions.create(
{
model: 'deepseek-ai/DeepSeek-V3.1-Terminus',
messages: demoMessages.value.map((item) => {
return {
role: item.role,
content: item.content
};
}),
stream: true,
enable_thinking: !!enableThink
} as ChatCompletionCreateParamsStreaming,
{
signal: controller.value.signal
}
);
// 4. 载入响应数据,并关闭生成加载
generateLoading.value = false;
const targetId = Date.now().toString();
demoMessages.value.push({
id: targetId,
role: 'assistant',
content: '',
timestamp: Date.now(),
isThinking: false
});
const targetMessage = demoMessages.value.find((item) => item.id === targetId)!;
for await (const chunk of stream) {
targetMessage.loading = true;
if (
enableThink &&
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(chunk.choices[0]?.delta as any).reasoning_content &&
targetMessage.content.length === 0 &&
!chunk.choices[0]?.delta.content
) {
if (!targetMessage.thoughtProcess) {
targetMessage.thoughtProcess = '';
}
targetMessage.isThinking = true;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
targetMessage.thoughtProcess += (chunk.choices[0]?.delta as any).reasoning_content || '';
} else {
targetMessage.isThinking = false;
}
targetMessage.content = targetMessage.content + (chunk.choices[0]?.delta.content || '');
targetMessage.timestamp = Date.now();
}
targetMessage.loading = false;
} catch {
// 请求失败处理
// TODO
// ElNotification.error('请求失败,请稍后重试');
const targetMessage = demoMessages.value[demoMessages.value.length - 1]!;
targetMessage.loading = false;
targetMessage.timestamp = Date.now();
targetMessage.error = '请求失败,请稍后重试';
} finally {
senderLoading.value = false;
}
} else {
try {
const response = await client.chat.completions.create(
{
model: 'deepseek-ai/DeepSeek-V3.1-Terminus',
messages: demoMessages.value.map((item) => {
return {
role: item.role,
content: item.content
};
}),
stream: false
},
{
signal: controller.value.signal
}
);
demoMessages.value.push({
id: Date.now().toString(),
role: 'assistant',
content: response.choices[0]?.message.content || '请求失败,请稍后重试',
timestamp: Date.now(),
isThinking: false
});
} catch {
// TODO: 错误处理
// ElNotification.error('请求失败,请稍后重试');
} finally {
// 4. 载入响应数据,并关闭生成加载
senderLoading.value = false;
generateLoading.value = false;
}
}
}
function handleCancel() {
controller.value.abort();
controller.value = new AbortController();
if (generateLoading.value) {
}
generateLoading.value = false;
senderLoading.value = false;
}
onUnmounted(() => {
controller.value.abort();
});
function handleCreateRecord(messages: ChatMessage[]) {
const targetId = Date.now().toString();
demoRecords.value.push({
id: targetId,
name: '新记录',
content: messages[0]?.content || '',
type: 'text',
createTime: new Date(messages[0]?.timestamp || Date.now()).toLocaleDateString(),
userId: 'demo-user',
extraData: {
messages
}
});
activeRecordId.value = targetId;
}
function handleChangeRecord(record?: ChatRecord) {
demoMessages.value = record?.extraData?.messages || [];
}
function handleBubbleEvent(event: BubbleEvent, data: ChatMessage) {
switch (event) {
case 'like':
data.isLiked = !data.isLiked;
break;
case 'dislike':
data.isDisliked = !data.isDisliked;
break;
case 'bookmark':
data.isBookmarked = !data.isBookmarked;
break;
case 'terminate':
data.isTerminated = true;
break;
case 'copy':
navigator.clipboard.writeText(data.content);
// TODO: 添加复制成功提示
// ElMessage.success('复制成功');
}
}
const recordButtons: ChatRecordAction[] = [
{
id: 'edit',
name: '编辑',
icon: 'edit',
action: () => {}
},
{
id: 'delete',
name: '删除',
icon: 'delete',
action: (record) => {
demoRecords.value = demoRecords.value.filter((item) => item.id !== record.id);
}
}
];
const themeMode = ref<'light' | 'dark'>('light');
function handleScrollBottom() {
// TODO: 滚动到底部
console.log('滚动到底部');
}
</script>
<template>
<div class="w-full h-full relative">
<ChatContainer
v-model="userQuestion"
v-model:loading="senderLoading"
v-model:messages="demoMessages"
v-model:record-id="activeRecordId"
:is-generate-loading="generateLoading"
:records="demoRecords"
:record-actions="recordButtons"
:show-workspace="showWorkspace"
:has-sender-tools="true"
:show-sender="true"
has-theme-mode
:markdown-class-name="themeMode === 'dark' ? 'prose-invert' : 'prose'"
:enable-think="true"
:enable-net="false"
:theme-mode="themeMode"
:input-height="80"
@send="handleSend"
@cancel="handleCancel"
@create-record="handleCreateRecord"
@close-workspace="showWorkspace = false"
@change-record="handleChangeRecord"
@bubble-event="handleBubbleEvent"
@change-theme="(mode) => (themeMode = mode)"
@scroll-bottom="handleScrollBottom"
>
<template #sender-tools>
<div class="px-3 flex items-center h-full">
<div
class="border border-amber-600 rounded py-1 px-2 text-sm ml-auto text-amber-600 hover:brightness-125 hover:bg-amber-500/15 cursor-pointer"
@click="showWorkspace = !showWorkspace"
>
会话空间
</div>
</div>
</template>
<template #empty>
<div class="text-center">
<div class="italic font-bold text-3xl mb-3">AI计量助手</div>
<div>
你好!很高兴见到你!😊
<div>
有什么我可以帮助你的吗?无论是回答问题、聊天还是其他任何需要,我都很乐意为你提供帮助!
</div>
</div>
</div>
</template>
<template #logo>
<div class="text-xl mb-2 font-bold">AI计量助手</div>
</template>
<template #header-logo>
<div class="text-lg font-bold ml-3">AI计量助手</div>
</template>
<template #workspace="{ record }">
<div class="p-3 relative overflow-auto w-full h-full workspace-area">
在工作区展示记录的一些详细信息或额外内容
<div class="text-base mt-8 whitespace-pre-line bg-gray-200 p-3">
{{ record }}
</div>
</div>
</template>
<template #record-dropdown>
<div class="text-gray-300 italic absolute top-0 right-0">记录下拉菜单</div>
</template>
<template #bubble-header="{ data }">
<div v-if="data.id === '1765436189938'" class="text-gray-300 italic">气泡头部</div>
</template>
<template #bubble-footer="{ data }">
<div v-if="data.id === '1765436189938'" class="text-gray-300 italic">
气泡底部会覆盖掉操作按钮
</div>
</template>
<template #bubble-event="{ data }">
<div v-if="data.id === '1765436289340'" class="ml-auto">可以有很多其他按钮</div>
</template>
<template #bubble-content-header="{ data }">
<div v-if="data.id === '1765436189938'" class="text-gray-300 italic">
我这里可以插入头部
</div>
</template>
<template #bubble-content-footer="{ data }">
<div v-if="data.id === '1765436189938'" class="text-gray-300 italic">
我这里可以插入底部
</div>
</template>
<template #bubble-thinking-header="{ data }">
<div v-if="data.id === '1765436189938'" class="text-gray-300 italic">
在思考区域搞点事情
</div>
</template>
<template #sender-footer-tools>
<div class="text-gray-300 italic">这里可以插入一些按钮元素</div>
</template>
<template #sender-button>
<div class="border border-gray-300 bg-amber-600">发送</div>
</template>
</ChatContainer>
</div>
</template>
<style lang="scss">
.workspace-area {
--scrollbar-width: 8px;
--scrollbar-border-radius: 4px;
--scrollbar-track-color: transparent;
--scrollbar-thumb-color: rgba(var(--kl-chat-primary-rgb), 0.3);
--scrollbar-thumb-hover-color: rgba(var(--kl-chat-primary-rgb), 0.6);
--scrollbar-thumb-active-color: rgba(var(--kl-chat-primary-rgb), 0.8);
&::-webkit-scrollbar {
width: var(--scrollbar-width);
height: var(--scrollbar-width);
opacity: 0;
transition: opacity 0.3s ease;
}
&::-webkit-scrollbar-track {
background: var(--scrollbar-track-color);
border-radius: var(--scrollbar-border-radius);
margin: 2px;
}
&::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb-color);
border-radius: var(--scrollbar-border-radius);
transition: all 0.3s ease;
cursor: pointer;
&:hover {
background: var(--scrollbar-thumb-hover-color);
}
&:active {
background: var(--scrollbar-thumb-active-color);
}
}
}
</style>Props 属性
| 属性名 | 类型 | 默认值 | 说明 |
| ------------------- | ------------------------ | ----------- | ------------------ |
| records | ChatRecord[] | [] | 聊天记录列表 |
| recordActions | ChatRecordAction[] | [] | 记录操作按钮配置 |
| hasHeader | boolean | true | 是否显示头部 |
| hasThemeMode | boolean | false | 是否支持主题切换 |
| hasThinking | boolean | true | 是否支持深度思考 |
| hasNetSearch | boolean | false | 是否支持联网搜索 |
| hasSenderTools | boolean | false | 是否显示发送工具区 |
| showWorkspace | boolean | true | 是否显示工作区 |
| showSender | boolean | true | 是否显示发送框 |
| isGenerateLoading | boolean | undefined | 是否正在生成 |
| defaultRecordId | string | undefined | 默认记录ID |
| defaultCollapse | boolean | false | 侧边栏默认折叠 |
| defaultAsideWidth | number | 250 | 侧边栏默认宽度 |
| markdownClassName | string | undefined | Markdown 样式类名 |
| primaryColor | string | '#615ced' | 主题色 |
| themeMode | 'light' \| 'dark' | 'light' | 主题模式 |
| enableNet | boolean | undefined | 联网搜索启用状态 |
| enableThink | boolean | undefined | 深度思考启用状态 |
| inputHeight | number | 140 | 输入框高度 |
| onCopy | (code: string) => void | undefined | 复制代码回调 |
| i18n | Record<string, any> | zhCN | 国际化配置 |
v-model 双向绑定
| 属性名 | 类型 | 说明 |
| --------------------- | ---------------- | ---------------- |
| v-model | string | 输入框文本 |
| v-model:messages | ChatMessage[] | 消息列表 |
| v-model:loading | boolean | 发送加载状态 |
| v-model:recordId | string \| null | 当前记录ID |
| v-model:enableThink | boolean | 深度思考启用状态 |
| v-model:enableNet | boolean | 联网搜索启用状态 |
Events 事件
| 事件名 | 参数 | 说明 |
| -------------------- | ------------------------------------------------------------ | -------------- |
| send | (text: string, enableThink?: boolean, enableNet?: boolean) | 发送消息 |
| cancel | - | 取消生成 |
| clear | - | 清空聊天 |
| create-record | (messages: ChatMessage[]) | 创建记录 |
| change-record | (record?: ChatRecord) | 切换记录 |
| change-collapse | (collapse: boolean) | 折叠状态改变 |
| change-theme | (theme: 'light' \| 'dark') | 主题切换 |
| change-aside-width | (width: number) | 侧边栏宽度改变 |
| click-logo | - | 点击Logo |
| bubble-event | (event: BubbleEvent, message: ChatMessage) | 气泡交互事件 |
| close-workspace | - | 关闭工作区 |
| scroll-bottom | - | 滚动到底部 |
Slots 插槽
| 插槽名 | 参数 | 说明 |
| ------------------------ | ----------------------------------------------------------------------- | -------------- |
| left-aside | { mobile: boolean } | 左侧边栏 |
| aside | { record: ChatRecord \| undefined, mobile: boolean } | 主侧边栏 |
| logo | { mobile: boolean } | Logo区域 |
| new-chat-button | { mobile: boolean } | 新建聊天按钮 |
| record-dropdown | { mobile: boolean } | 记录下拉菜单 |
| header | { record: ChatRecord \| undefined, mobile: boolean } | 头部区域 |
| header-logo | { mobile: boolean } | 头部Logo |
| bubble-header | { data: ChatMessage, mobile: boolean } | 气泡头部 |
| bubble-footer | { data: ChatMessage, mobile: boolean } | 气泡底部 |
| bubble-event | { data: ChatMessage, mobile: boolean } | 气泡操作区 |
| bubble-content-header | { data: ChatMessage, mobile: boolean } | 气泡内容头部 |
| bubble-content-footer | { data: ChatMessage, mobile: boolean } | 气泡内容底部 |
| bubble-thinking-header | { data: ChatMessage, mobile: boolean } | 思考过程头部 |
| bubble-loading-content | { mobile: boolean } | 加载内容 |
| empty | { mobile: boolean } | 空状态 |
| sender-tools | { mobile: boolean } | 发送工具区 |
| sender-footer-tools | { value: string, loading: boolean, mobile: boolean } | 发送器底部工具 |
| footer | { mobile: boolean } | 底部区域 |
| workspace | { record: ChatRecord \| undefined, mobile: boolean } | 工作区 |
| send-button | { state: object, execute: Function, mobile: boolean } | 发送按钮 |
| think-button | { state: object, execute: Function, mobile: boolean } | 思考按钮 |
| net-button | { state: object, execute: Function, mobile: boolean } | 联网按钮 |
| sender-textarea | { state: object, execute: Function, mobile: boolean, height: number } | 输入框 |
类型定义
ChatMessage
interface ChatMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
loading?: boolean;
isThinking?: boolean;
thoughtCollapse?: boolean;
thoughtProcess?: string;
timestamp: number;
isTerminated?: boolean;
isLiked?: boolean;
isDisliked?: boolean;
isError?: boolean;
error?: string;
isBookmarked?: boolean;
nextTips?: string[];
toolCalls?: any[];
hideFooterTools?: boolean;
extraData?: Record<string, any>;
}ChatRecord
interface ChatRecord {
id: string;
name: string;
content: string;
type: string;
createTime: string;
userId: string;
updateTime?: string;
extraData?: Record<string, any>;
}BubbleEvent
type BubbleEvent = 'like' | 'dislike' | 'bookmark' | 'terminate' | 'reload' | 'copy';ChatRecordAction
interface ChatRecordAction {
id: string;
name: string;
icon?: string | Component;
divided?: boolean;
disabled?: boolean;
action: (record: ChatRecord) => void;
}❓ 常见问题
Q: 如何自定义主题色?
A: 通过 primary-color 属性和 CSS 变量可以自定义主题色:
<ChatContainer primary-color="#ff6b6b" />:root {
--kl-chat-primary-color: #ff6b6b;
}Q: 如何集成 AI 服务?
A: 在 send 事件中调用你的 AI 服务接口:
const handleSend = async (text: string) => {
// 添加用户消息
messages.value.push({
id: generateId(),
role: 'user',
content: text,
timestamp: Date.now()
});
try {
// 调用 AI 服务
const response = await yourAIService.chat(text);
// 添加 AI 响应
messages.value.push({
id: generateId(),
role: 'assistant',
content: response.content,
timestamp: Date.now()
});
} catch (error) {
console.error('AI 服务错误:', error);
}
};Q: 如何实现流式响应?
A: 使用内置的 SSEClient 或其他流式处理方案:
import { SSEClient } from '@kernelift/ai-chat';
const handleStreamResponse = async (question: string) => {
const client = new SSEClient('token', 'https://api.example.com');
const currentMessage = {
id: generateId(),
role: 'assistant',
content: '',
timestamp: Date.now(),
loading: true
};
messages.value.push(currentMessage);
await client.connect('/chat/stream', {
onContent: (content) => {
currentMessage.content += content;
},
onComplete: () => {
currentMessage.loading = false;
},
onError: (error) => {
currentMessage.loading = false;
currentMessage.isError = true;
}
});
};Q: 如何在移动端使用?
A: 组件内置了响应式支持,移动端会自动适配。可以通过 mobile 插槽参数进行移动端特定定制:
<template #sender-footer-tools="{ mobile }">
<div v-if="mobile" class="mobile-tools">
<!-- 移动端特定工具 -->
</div>
</template>Q: 如何持久化聊天记录?
A: 使用 useStorage 或其他持久化方案:
import { useStorage } from '@vueuse/core';
const records = useStorage<ChatRecord[]>('chat-records', []);
const messages = useStorage<ChatMessage[]>('current-messages', []);Q: 如何处理长对话?
A: 建议实现分页加载或记录分割:
const handleLoadMore = async () => {
const olderMessages = await loadOlderMessages(currentPage);
messages.value.unshift(...olderMessages);
};Q: 如何添加自定义工具按钮?
A: 通过 sender-tools 插槽添加:
<template #sender-tools>
<el-tooltip content="自定义工具">
<el-button @click="handleCustomTool">
<IconRender icon="custom-icon" />
</el-button>
</el-tooltip>
</template>📄 许可证
GPL-3.0 License
🤝 贡献
欢迎提交 Issue 和 Pull Request!
版本: 2.0.0
更新时间: 2024-12-25
