npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@kernelift/ai-chat

v2.1.2

Published

kernelift 前端 AI chat 聊天框组件

Readme

@kernelift/ai-chat

npm version License: GPL-3.0

基于 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