npm.io
0.1.1 • Published 9h ago

@fv-ui/ai-chat

Licence
Version
0.1.1
Deps
5
Size
677 kB
Vulns
0
Weekly
0

@fv-ui/ai-chat

version vue element-plus

基于 Vue 3 + Element Plus 的 AI 对话组件库,复刻 element-plus-x 与 open-webui 对话框:从底层气泡、发送框、Markdown 渲染,到上层的对话整框(<FvAiChat>)、分支树消息、Artifacts 预览,一次给全。

  • 14 个基础组件(Bubble/Sender/XMarkdown/Thinking/...)自由拼装
  • 对话应用层整框 <FvAiChat>:顶栏模型多选、消息列表、分支切换、Artifacts 面板、发送停止一体化
  • 树形消息模型(同构 open-webui):一条提问可挂多个模型回答,分支可切换、可重生成
  • ChatAdapter 适配器协议:内置 OpenAI 兼容实现,可替换自研后端/Ollama
  • 流式打字机、思考链折叠、KaTeX/代码高亮(Prism)开箱即用

安装

pnpm add @fv-ui/ai-chat

peerDependencies:vue ^3.4.0element-plus ^2.7.0@element-plus/icons-vue ^2.3.0

快速开始

// main.ts —— 需要先引入 element-plus(组件库依赖其组件与暗色变量)
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).use(ElementPlus).mount('#app')
<!-- App.vue -->
<script setup lang="ts">
import { FvAiChat } from '@fv-ui/ai-chat'
import '@fv-ui/ai-chat/dist/style.css'
</script>

<template>
  <FvAiChat
    title="AI 助手"
    base-url="https://api.deepseek.com/v1"
    api-key="sk-xxx"
    model="deepseek-chat"
    style="height: 600px"
  />
</template>

<FvAiChat> 未传 adapter 时,用 baseURL/apiKey/model 构造内置 OpenAIAdapter(走 /chat/completions,SSE 流式)。两个都缺省时发送会报"未配置 adapter 或 baseURL"。

基础组件层 API

XMarkdown

Markdown 渲染(Sanitized by DOMPurify),内置 Prism 代码高亮与 KaTeX。

Prop 类型 默认 说明
content string —(必填) Markdown 源文本
mdPlugins PluginWithParams[] 额外 markdown-it 插件
highlight (str, lang) => string 内置 Prism 自定义代码高亮
isTyping boolean 打字机模式样式钩子(透传占位)
Ref 说明
getHtml() 取当前渲染后的 HTML 字符串
Typewriter

打字机输出,支持续打/重打/雾化遮罩。

Prop 类型 默认 说明
content string —(必填) 全文(流式时增量传入,子集扩展自动续打)
isMarkdown boolean false 走 XMarkdown 渲染(此时 suffix 不追加)
isFog boolean | { bgColor?; width? } false 打字中尾部雾化遮罩
typing boolean | { step?; interval?; suffix? } false 开启打字机;对象形态可配步长/间隔/光标后缀
Event 说明
start / writing / finish 开始打字 / 每次推进 / 全部打完
Ref 说明
interrupt() / continue() / restart() / destroy() 暂停 / 继续 / 重打 / 重置清空
renderedContent / isTyping / progress 已输出文本 / 是否打字中 / 进度 0-1
Bubble

单条气泡(内部按需挂载 Typewriter/XMarkdown)。

Prop 类型 默认 说明
content string '' 气泡内容
placement 'start' | 'end' 'start' start 靠左 / end 靠右
loading boolean false 加载态(三点动画,可被 #loading 覆盖)
typing boolean | TypingConfig false 打字机模式(透传 Typewriter)
isMarkdown boolean false 内容按 Markdown 渲染
isFog boolean | { bgColor?; width? } false 雾化(自定义气泡背景时传 bgColor)
variant 'filled' | 'borderless' | 'outlined' | 'shadow' 'filled' 气泡样式
shape 'round' | 'corner' 'round' 圆角 / 方角
avatar string '' 头像图片地址
avatarSize ElAvatar size | number '' 头像尺寸
avatarGap string '12px' 头像与内容间距
avatarShape 'circle' | 'square' 'circle' 头像形状
avatarIcon Component 头像图标(与 avatar 冲突时优先)
avatarSrcSet / avatarAlt string '' / '' 头像 srcSet/alt
avatarFit 'fill' | 'contain' | 'cover' | 'none' | 'scale-down' 'cover' 头像填充方式(el-avatar fit)
noPadding boolean false 内容区去内边距
noStyle boolean false 去气泡样式(透明/无框/无阴影)
Event 说明
avatar-error 头像加载失败
start / writing / finish typing 模式下的打字机事件透传
Slot 说明
avatar / header / content / loading / footer 自定义头像 / 头部 / 内容(优先于 content prop)/ 加载态 / 底部
Ref 说明
interrupt / continue / restart / destroy Typewriter 方法转发(非 typing 模式为 no-op)
renderedContent / isTyping / progress Typewriter 状态转发
twInstance 内部 Typewriter 原始实例(BubbleList @complete 透传用)
BubbleList

气泡列表 + 吸底/回底按钮。

Prop 类型 默认 说明
list BubbleListItemProps[] [] 列表项(Bubble props + 必填 key)
maxHeight string '' 滚动容器最大高度(如 '350px'/'60vh')
alwaysShowScrollbar boolean false 始终显示滚动条
showBackButton boolean true 是否显示回底按钮
backButtonThreshold number 80 距底超过该像素显示回底按钮
btnLoading boolean false 回底按钮加载态
btnColor string '' 回底按钮颜色(el-button color)
backButtonPosition { bottom?; left? } {} 回底按钮位置偏移
btnIconSize number 16 回底按钮图标尺寸
triggerIndices 'only-last' | 'all' | number[] 'only-last' @complete 触发索引
Event 说明
complete (typewriterInstance, index) 打字完成(按 triggerIndices 过滤后上抛)
Slot 说明
avatar / header / content / loading / footer 透传给每个 Bubble,作用域 { item, index }
Ref 说明
scrollToTop() / scrollToBottom() / scrollToBubble(index) 滚动控制
Sender

输入发送框。

Prop 类型 默认 说明
modelValue string(v-model) '' 输入值(缺省内部自持)
placeholder string '' 占位文案
autosize { minRows?; maxRows? } {} 自增高(行高 22px)
submitType 'enter' | 'shiftEnter' | 'cmdOrCtrlEnter' | 'altEnter' 'enter' 提交按键模式
loading boolean false 加载态(发送键变停止键,输入区禁用)
disabled boolean false 禁用(阻断提交)
readOnly boolean false 只读
clearable boolean false 显示清空按钮
inputWidth string '' 输入区宽度(variant=default 时有效)
variant 'default' | 'updown' 'default' default 同排 / updown 上输入下操作栏
submitBtnDisabled boolean false 附加禁用发送按钮
triggerStrings string[] [] 指令触发串(光标前命中弹指令框)
triggerPopoverVisible boolean(v-model:triggerPopoverVisible) false 指令弹框显隐
triggerPopoverWidth / triggerPopoverLeft string '' 指令弹框宽度 / left 偏移
triggerPopoverOffset number 0 弹框距触发字符偏移量
triggerPopoverPlacement ElPopover placement 'top-start' 弹框位置
allowSpeech boolean false 显示语音按钮(当前为桩,点击仅 warn)
allowEmptySubmit boolean false 允许空内容提交(附件场景:只发文件不输文字)
Event 说明
update:modelValue / change 输入值变化
submit (value) 提交(提交后内部清空)
cancel 点击停止按钮
trigger (keyword, visible) 命中指令串
recording-change 语音录制状态变化(预留)
paste-file (file) 粘贴文件
update:triggerPopoverVisible 指令弹框显隐同步
Slot 说明
header / footer 头部/底部(配合 ref.openHeader/closeHeader)
prefix 输入区前缀
action-list 自定义操作区(存在时隐藏内置按钮)
action-left 操作栏左侧附加区(与提交按钮同行靠左,如附件回形针)
trigger 指令弹框内容(作用域 { keyword })
Ref 说明
submit() / cancel() / clear() 提交 / 停止 / 清空
focus(pos?) / blur() pos: 'all' 全选 | 'start' 头 | 'end' 尾(缺省 end)
openHeader() / closeHeader() 显隐 #header 插槽
MentionSender

带 @提及 的 Sender(键盘 ↑↓ 导航 / Enter 确认 / Esc 关闭)。

Prop 类型 默认 说明
modelValue string(v-model) '' 输入值
userList MentionUserInfo[] [] @ 触发的候选用户(id/name/avatar/pinyin)
customTrigger MentionCustomTrigger[] [] 自定义触发组 { dialogTitle, prefix, tagList }
asyncMatchFun (search) => Promise<MentionUserInfo[]> 异步候选(存在时防抖 300ms,后端过滤)

事件:update:modelValue / change / submit / cancel / trigger / recording-change / paste-file / update:triggerPopoverVisible / show-at-dialog(弹层显隐);其余 props/事件经 $attrs 透传给 Sender,slot 经 forwardedSlots 显式转发(header/footer/prefix/action-list/trigger)。

Ref 说明
submit / cancel / clear / focus / blur / openHeader / closeHeader Sender 方法转发
setUserTag(userId) 光标处程序化插入 @用户
setCustomTag(prefix, id) 光标处插入自定义触发标签
EditorSender

富文本标签编辑器(contenteditable),open-webui 风格标签芯片。

Prop 类型 默认 说明
placeholder string '' 占位文案
device 'pc' | 'h5' 'pc' 设备形态(仅影响样式)
autoFocus boolean false 挂载后聚焦到末尾
variant 'default' | 'updown' 'default' 布局形态
userList EditorUser[] [] @ 候选用户
customTrigger EditorCustomTrigger[] [] 自定义触发组
selectList EditorSelectGroup[] [] 选择标签分组(openSelectDialog 弹窗数据)
maxLength number 0 纯文本最大长度(0 不限;性能开销大,仅显式开启)
submitType 'enter' | 'shiftEnter' 'enter' 提交按键模式
customStyle Record<string, any> {} 根节点自定义样式
loading boolean false 加载态(发送键变停止键)
disabled boolean false 禁用
clearable boolean false 显示清空按钮
headerAnimationTimer number 300 头部过渡时长(ms)
asyncMatchFun (search) => Promise<EditorUser[]> 异步 @ 候选(防抖 300ms)
customDialog boolean false 选择标签不自弹内置弹窗,仅上抛 show-select-dialog
Event 说明
submit / change (SubmitResult) SubmitResult = { text, html, tags: { userTags, selectTags, inputTags, customTags } }
cancel 点击停止按钮
show-at-dialog / show-select-dialog / show-tag-dialog 各弹层显隐
Slot 说明
header / footer / prefix / action-list 同 Sender
tag-tip 输入标签 tip 弹层内容(作用域 { nodeId })
Ref 说明
getCurrentValue() 取 { text, html, tags }
focusToStart() / focusToEnd() / blur() / selectAll() / clear() 光标与清空
setText(text) / setHtml(html) / setMixTags(rows) 程序化写入
setUserTag(id) / setCustomTag(prefix, id) / setInputTag(id, name) / setSelectTag(list) 插入各类标签
customSetUser(user) / customSetTag(tag) 外部直传对象插入(不经列表查表)
updateSelectTag(method, options) 选择标签更新:'insertTag'/'updateTagValue'/'deleteTag'/'updateTagStyle'
openSelectDialog() / openTipTag(nodeId?) / closeTipTag() 弹窗控制
openHeader() / closeHeader() 显隐 #header
chat / opNode(method, options) / chatState() 底层文档模型:chat.insert/update/delete/append;opNode 按方法名分发
Thinking

思考链折叠面板。

Prop 类型 默认 说明
modelValue boolean(v-model) false 展开/收起
content string '' 思考内容(常配合 #content 插槽嵌 XMarkdown)
status 'start' | 'thinking' | 'end' | 'error' 'start' 思考状态
disabled boolean false 禁用切换
autoCollapse boolean false status 变为 end 时自动收起
buttonWidth string '' 切换按钮区宽度
maxWidth string '' 内容区最大宽度
color / backgroundColor string '' 前景色 / 背景色
Event 说明
update:modelValue 展开/收起同步
Slot 说明
status-icon / label / arrow 自定义状态图标 / 文案 / 箭头
content / error 思考内容 / error 态内容
Ref 说明
modelValue / toggle() 展开状态 / 手动切换
ThoughtChain

思维链步骤条。

Prop 类型 默认 说明
items ThoughtChainItem[] [] 步骤项 { key, title?, status?: 'pending'/'success'/'loading'/'error', icon?, description? }
direction 'vertical' | 'horizontal' 'vertical' 排列方向
Slot 说明
content 覆盖默认 title/description(作用域 { item, index })
Welcome

空态欢迎页。

Prop 类型 默认 说明
icon string | Component '' 图标(字符串为图片地址;未传渲染内置对话图标)
name string '' 名称
description string '' 描述(纯文本)
Slot 说明
icon / name / description 覆盖对应区块
Prompts

建议 prompt 网格。

Prop 类型 默认 说明
prompts PromptItem[] [] 建议项 { key(必填), label?, description?, icon?(EP 图标名字符串或组件), disabled? }
title string '' 网格标题
Event 说明
select (item) 点击(disabled 项不触发)
Slot 说明
default 覆盖整卡(作用域 { item, index })
FilesCard

单个文件卡片。文档模式按扩展名自动映射类型图标:pdf/doc/txt→文档、xls/csv→图表、ppt→演示、mp4→胶片、mp3→耳机、zip→文件夹、html/url→链接、apk→手机、exe→芯片,未命中回落通用文档图标。

Prop 类型 默认 说明
name string '' 文件名
fileType 'image' | 'file' 'file' image 渲染缩略图,file 渲染文档图标卡
size number | string 0 文件大小(字节自动格式化为 B/KB/MB;字符串原样展示;0 不显示)
url string '' 图片地址(image 模式缩略图)
status 'loading' | 'success' | 'error' 'success' loading spinner / error 红边
imageSize string '' 缩略图尺寸(width/height 同值)
onDelete () => void 删除按钮回调
Slot 说明
image 覆盖图片区(默认 url 缩略图,无 url 渲染 Picture 占位)
Attachments

附件选择与列表(v-model)。

Prop 类型 默认 说明
modelValue object [] 附件列表(项含 uid/name/size/url/fileType/status/raw)
accept string '' input accept,如 'image/*'
tip string '' 内联提示文案
count number 0 最大附件数(超出走 @exceed;0 不限)
beforeUpload (file) => boolean | Promise 上传前校验,返回 false 拦截
layout 'inline' | 'triggerOnly' | 'listOnly' 'inline' 布局:inline 回形针+列表+提示一行;triggerOnly 只渲染回形针按钮;listOnly 只渲染文件列表(拆分布局供整框组合:按钮进操作栏、列表进输入区上方)
Event 说明
update:modelValue / change 列表变化(全量数组)
exceed (file) 超出 count
delete (item) 删除某项
Slot 说明
list-item 覆盖项渲染(作用域 { item, index })
list-thumb 覆盖图片缩略图(作用域 { item, index })
Ref 说明
openSelect() 程序化打开文件选择
retry(item) error 项重新置为 loading(上传逻辑由外部接线)
Conversations

会话列表(按 group 分组)。

Prop 类型 默认 说明
items ConversationItem[] [] 会话项 { key(必填), label(必填), group?, disabled? }
active string | number(v-model:active) undefined 当前激活会话 key
showBuiltInMenuType 'hover' | 'always' 'hover' 内置菜单按钮显隐方式
groupable (a, b) => number 分组排序比较器(缺省保持插入顺序)
Event 说明
update:active / change 选中会话(key / item)
menu-command ({ item, command: 'rename'/'delete', value? }) 菜单指令;rename 确认后带新名称
Slot 说明
group-title 分组标题(作用域 { group })
item 覆盖行内容(作用域 { item, active })
menu 覆盖菜单项(默认重命名/删除;作用域 { item })
Ref 说明
handleMenuCommand({ item, command }) 程序化触发菜单指令转发(纯转发,同步上抛 menu-command)
renamePrompt(key) 弹重命名输入框,确认后上抛 menu-command
deleteConfirm(key) 弹删除确认框,确认后上抛 menu-command

对话应用层 API

AiChat

对话整框:顶栏(标题+模型多选)、消息区(空态 Welcome/Prompts)、输入区(Sender)、可选 Artifacts 面板。

Prop 类型 默认 说明
model string '' 默认模型(未传 models 时作为单模型)
models string[] [] 可选模型列表(有值时顶栏多选选择器)
baseURL string '' OpenAI 兼容服务地址(未传 adapter 时构造 OpenAIAdapter)
apiKey string '' 服务密钥
adapter ChatAdapter undefined 聊天适配器(与 baseURL 二选一,优先)
messages MessageNode undefined 消息链(半受控,见下)
title string '' 顶栏标题(同时作空态欢迎页名称)
placeholder string '' 输入框占位
suggestions AiChatSuggestion[] [] 空态建议列表(Prompts 项,key 必填)
features AiChatFeatures {} 特性开关 { artifacts?, tts?, queue?, continueGenerate?, fullscreenEditor? }(当前仅 artifacts 生效)
allowAttachments boolean false 开启输入区附件上传(回形针在操作栏左侧与提交按钮同行,已选列表在输入框上方;只传附件不输文字也可提交)
accept string '' 附件 input accept 属性(透传 Attachments),如 'image/*'
attachmentLimit number 0 附件数量上限(0 不限,超限走 @attachment-exceed)
theme 'light' | 'dark' | 'auto' 'light' 主题(向 html 注入/移除 dark class)

v-model:messages 半受控语义:外部 messages 仅在 setup 时一次性深拷贝灌入(用于恢复历史);组件内部每次变更全量上抛 update:messages(同帧去抖),父组件需经 v-model 回写维持单向数据流;父组件后续直接改 prop 不会回灌生效。

Event 说明
update:messages (MessageNode[]) 全量消息链(去抖后)
send (text, files?) 用户发送(allowAttachments 开启时 files 为降维后的 ChatFile[])
message-complete (message) 链尾 assistant 生成完成
error (err, message?) 生成出错
branch-change (dir, message) 分支切换
attachment-exceed (file) 附件超过 attachmentLimit 上限
Slot 说明
message-avatar / message-actions 透传给 AiMessages(作用域见 AiMessage)
empty 覆盖空态整区
sender-prefix 输入区前缀
Ref 说明
sendMessage(text, files?) 程序化发送
stop() 停止生成(保留半截内容,静默收口不发 complete/error)
regenerate(messageId?) 重生成(缺省链尾 assistant)
appendToMessage(id, text) 向指定消息追加文本
clearMessages() 清空消息树
exportTxt() 导出当前链为纯文本
openArtifact(key) 程序化打开某个 artifact
AiMessages

消息列表(空态渲染 Welcome + Prompts),吸底滚动。

Prop 类型 默认 说明
messages MessageNode[] —(必填) 当前链消息(根 → 焦点)
nodes Record<string, MessageNode> {} 全量节点表(分支计算需查父 childrenIds)
icon string | Component '' 空态欢迎页图标
name string '' 空态欢迎页名称
description string '' 空态欢迎页描述
suggestions AiMessagesSuggestion[] [] 空态建议列表
suggestionsTitle string '' 空态建议区标题
scrollThreshold number 40 吸底阈值(px):距底不超过该值才自动跟随
showActions boolean true 显示消息操作按钮(复制/重生成)
showBranchSwitch boolean true 显示分支切换器
Event 说明
select / copy / regenerate / branch-change 建议选中 / 复制 / 重生成 / 分支切换,载荷同 AiChat
Slot 说明
empty 覆盖空态整区
message-avatar / message-actions 透传给每条 AiMessage
Ref 说明
scrollToBottom(smooth?) / scrollToTop() 滚动控制
AiMessage

单条消息(Markdown 正文 + 思考链折叠 + 附件 + 操作区)。

Prop 类型 默认 说明
message MessageNode —(必填) 消息节点
branch AiMessageBranchInfo undefined 分支元数据 { count, index };缺省视为单分支
showActions boolean true 显示复制/重生成(仅 assistant)
showBranchSwitch boolean true 显示分支切换器(branch.count > 1 时)
autoCollapse boolean true Thinking autoCollapse 透传
Event 说明
copy / regenerate / branch-change (message) / (message) / (dir, message)
Slot 说明
message-avatar 自定义头像(作用域 { message })
message-files 自定义附件渲染(作用域 { message, files })
message-actions 自定义操作区(作用域 { message })
Ref 说明
message 当前消息节点(computed)
AiArtifacts

Artifacts 预览面板(iframe 沙箱,从消息 html / svg 代码块提取)。

Prop 类型 默认 说明
artifacts AiArtifactItem[] [] artifact 列表 { key(必填), type: 'html'/'svg', content, title? }
activeKey string '' 当前展示的 key;为空时面板隐藏
Event 说明
close / change 关闭 / 切换(key)
navigate (href) iframe 内点击外链被拦截上抛
Ref 说明
open(key) / close() 程序化显隐

Composables

useChat(options)

聊天编排:发送 → 流式回填 → 队列/重生成/续写。

const chat = useChat({
  adapter: () => OpenAIAdapter({ baseURL, apiKey }), // 实例或工厂;工厂每次 send 惰性取用
  models: () => selectedModels.value,                // 数组或工厂;多模型并行生成兄弟分支
  initial: { messages, currentId },                  // 恢复会话
  onToolEvent: (ev) => { /* det-oms 风格 tool{status},展示「正在查询库存…」*/ }
})
// chat: { send, stop, regenerate, continueGenerate, enqueue, queue, isLoading,
//         msgs, messages, currentId, chain }
  • send(text, files?):生成中调用被拒;多模型时各模型一个 assistant 节点并发回填
  • stop():中止全部在途请求,在途节点置 done(保留半截内容)
  • regenerate(messageId?):回退焦点到父节点重发生成新分支(旧回答保留)
  • continueGenerate(messageId?):以既有内容为前缀续写,增量追加到同一节点
  • enqueue(text, files?):生成中入队,完成后自动 flush 队首
  • onToolEvent:适配器 onTool 回调的透传挂点(如 DetOmsChatAdapter 的 tool{status} 事件)
DetOmsChatAdapter(config)

det-oms(/api/ai-chat/*)风格聊天适配器:五事件 SSE 协议(message_start / tool / delta / message_end / error),POST /ai-chat/message/send。会话管理(列表/归档/删除等)由消费方直接调后端,不经此适配器。

const adapter = DetOmsChatAdapter({
  baseURL: '/api',                                     // 或 https://host
  headers: () => ({ Authorization: 'Bearer ' + token }), // 惰性取用
  sessionId: () => currentSessionId.value,             // 工厂:切换会话后下一次 send 生效
  errorMessages: { 90602: '已有进行中的对话' }           // 业务码 → 文案映射(可选)
})
  • 同步段失败(Content-Type 非 text/event-stream)按 JSON 错误 {code,message}onError;命中 errorMessages 时用映射文案(带 codeDetOmsError)
  • message_endmessageId(最终文本行 id)经 onBackendMessageId 回填到 MessageNode.backendId——多轮工具循环下与 message_start 的首轮 id 不同,UI 落库/对账以它为准
  • tool 事件经 onTooluseChat.onToolEventAiChat @tool-event 上抛,消费方展示「正在查询库存…」等中间态
  • 配套 DTO 类型:DetOmsSessionItem / DetOmsMessage / DetOmsSessionDetail / DetOmsMessagePage(消费方直接调会话管理端点时用)
useMessages(initial?)

树形消息状态中枢(响应式包装 createMessageTree)。

返回:messages(全量节点表)、currentIdchain(当前链 computed)、addremoveeditappendContentnavigate(id?, dir?)setCurrent(id)setStorage(adapter)load()flush()toJSON()。绑定存储后树变更自动去抖(150ms)落盘。

useXStream()

SSE 流消费:startStream({ readableStream, transformStream? })cancel()data: Ref<SSEEvent[]>errorisLoading。内部完成 \r\n/跨 chunk 边界归一化。

useSend() / XRequest

useSend():send({ url, method?, headers?, body?, transformStream?, onChunk? })(SSE data 为 JSON 且含 content 字段时逐块回调)、abort()loadingerror。在飞行守卫:上一个 send 未结束时新 send 被忽略。

XRequest(url, options):Promise 化,resolve 累计全文。

useRecord()

语音录制 + 实时转写:start() / stop() / isRecording / text / audioUrl / error。SpeechRecognition 不可用时降级为仅录音。

自定义 ChatAdapter

实现 ChatAdapter 接口即可接入任意后端(自研网关、Ollama、非 OpenAI 协议):

import type { ChatAdapter } from '@fv-ui/ai-chat'

const myAdapter: ChatAdapter = {
  async send(req, handlers) {
    // req: { messages: [{ role, content }], model?, stream?, signal? }
    try {
      const res = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(req),
        signal: req.signal
      })
      if (!res.ok) { handlers.onError(new Error(`HTTP ${res.status}`)); return }
      // 流式:自行逐块解析,回调增量
      handlers.onDelta('一段正文')
      handlers.onReasoning('一段思考链') // 可选
      handlers.onUsage({ prompt_tokens: 10, completion_tokens: 20 }) // 可选
      handlers.onDone() // 正常结束必须调用
    } catch (e) {
      if (req.signal?.aborted) return // 主动中止不算错误
      handlers.onError(e instanceof Error ? e : new Error(String(e)))
    }
  },
  abort() {
    // 中止全部在途请求
  }
}

// 用法
<FvAiChat title="AI" :adapter="myAdapter" />

一次 send 对应一次完整生成:持续回调 onDelta 增量,最后必须 onDone()onError();stop() 时组件会调 abort()

持久化可选 StorageAdapter(get/set/remove,async),内置 createLocalStorageAdapter()(localStorage 不可用时自动降级内存)与 createMemoryAdapter();经 useMessages().setStorage() 绑定。

主题定制

组件样式基于 Element Plus CSS 变量,覆盖 EP 变量即完成主题定制:

// 全局或局部覆盖(需在引入 dist/style.css 之后)
:root {
  --el-color-primary: #7c3aed;
}

高级定制可直接 @use 源 SCSS 入口:

@use '@fv-ui/ai-chat/src/styles/index.scss' with (
  // scss 变量(若组件有定义)
);

源码 SCSS 入口仅供源码依赖场景;npm 消费方推荐使用构建产物 dist/style.css + CSS 变量覆盖。

暗色模式

<FvAiChat theme="dark">theme="auto"(跟随 prefers-color-scheme):组件向 <html> 注入/移除 dark class,配合 element-plus 的暗色变量生效:

// main.ts 引入 EP 暗色变量
import 'element-plus/theme-chalk/dark/css-vars.css'
<FvAiChat theme="auto" ... />

注意:组件卸载时会移除 html.dark(单实例假设);多实例并用暗色需自行管理 class。

Prism 主题引入

代码高亮默认配色由组件样式提供;要换成其他 Prism 主题,在 dist/style.css 之后引入覆盖:

import '@fv-ui/ai-chat/dist/style.css'
import '@fv-ui/ai-chat/styles/prism-tomorrow.min.css'

内置主题:prism(默认)、prism-coyprism-darkprism-funkyprism-okaidiaprism-solarizedlightprism-tomorrowprism-twilight

FAQ

Q:页面是白屏/样式错乱? A:确认已引入 element-plus/dist/index.css@fv-ui/ai-chat/dist/style.css,且 app.use(ElementPlus)

Q:<FvAiChat> 发送报"未配置 adapter 或 baseURL"? A:adapterbaseURL 至少传一个。

Q:外层改了 v-model:messages 为什么界面没变? A:半受控语义:外部 messages 只在初始灌入一次,后续界面变化全量上抛 update:messages,请经 v-model 回写而不是直接改源数组。

Q:多模型怎么用? A:<FvAiChat model="a" :models="['a', 'b']"> 顶栏出现多选,选中的多个模型并行生成,回答互为分支可切换。

Q:features 里的 tts/queue/continueGenerate/fullscreenEditor? A:接口已预留,当前版本仅 artifacts 生效,其余待后续版本接线。

Q:KaTeX 公式没渲染? A:XMarkdown 内置 @vscode/markdown-it-katex,确认公式语法正确即可;无需额外引入。

Q:Nuxt/SSR 环境首渲染报错 "DOMPurify 不可用:当前环境无 window"? A:XMarkdown 依赖浏览器 window(DOMPurify 消毒),不支持服务端首渲染。Nuxt 下请用 <ClientOnly> 包裹(或客户端挂载后再渲染):

<ClientOnly>
  <FvAiChat ... />
</ClientOnly>

已知限制与 Roadmap

以下能力数据层已就绪,组合 UI 待后续版本接线(v0.2 计划):

  • 消息列表懒加载:useMessages 数据面支持分页注入,AiMessages 暂全量渲染;超长对话建议自行分页。
  • 消息操作条:当前内置复制/重新生成;编辑/删除/继续生成/朗读请经 useChat(editMessage/removeMessage/continueGenerate)或 #message-actions 插槽自建入口。
  • 队列卡片:useChat.enqueue 可用,features.queue 展示 UI 待接线。
  • 语音输入:SenderallowSpeech 当前为桩(点击仅提示);useRecord 可独立使用。
  • 多实例暗色互斥:theme 采用单写者假设,多实例同屏请自行管理 html.dark

类型与导出

包入口导出全部组件、useChat/useMessages/useXStream/useSend/XRequest/useRecordOpenAIAdapterDetOmsChatAdapter(+ DetOmsError 及配套 DTO 类型)、createMessageTreecreateLocalStorageAdapter/createMemoryAdaptercreateSSEParser 及全部公共类型(MessageNodeChatAdapterChatFileMessageUsageSSEEvent 等),消费方:

import { FvAiChat, OpenAIAdapter, useChat } from '@fv-ui/ai-chat'
import type { MessageNode, ChatAdapter } from '@fv-ui/ai-chat'