@fv-ui/ai-chat
v0.10.1
Published
基于 Vue3 + Element Plus 的 AI 对话组件库
Readme
@fv-ui/ai-chat
基于 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-chatpeerDependencies:vue ^3.4.0、element-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 | 显示清空按钮 | | round | boolean | true | 主按钮(发送/停止)为圆形(element-plus circle;false 时方按钮,停止键光带沿方框流转) | | shadow | 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 | 开启语音输入:麦克风按钮 → 录音条(波形+时长,Esc 取消)→ 确认后转写文本插入光标处;浏览器 SpeechRecognition 实时转写 | | 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 | '' | 顶栏标题(同时作空态欢迎页名称) |
| showHeader | boolean | false | 显示顶栏(标题 + 模型选择器;models 多选选择器在顶栏内,需开启才可见) |
| centerWelcome | boolean | true | 空态居中:未开始对话时欢迎内容与输入区整体垂直居中,首条消息后输入区恢复固定底部 |
| showScrollButton | boolean | true | 显示滚到底部悬浮按钮(透传 AiMessages;距底超过 scrollButtonThreshold 时出现) |
| scrollButtonThreshold | number | 100 | 滚到底部悬浮按钮显示阈值(px,透传 AiMessages) |
| scrollThreshold | number | 100 | 用户上滚免打扰阈值(px,透传 AiMessages):距底不超过该值视为「钉在底部」,流式期间自动跟随滚动;调小可恢复更灵敏的跟随 |
| showUserAvatar | boolean | true | 显示用户头像(false 时用户消息不渲染头像;助手头像不受影响) |
| showUserName | boolean | true | 显示用户名称(false 时用户消息不渲染头部名称行;助手名称不受影响) |
| showAssistantAvatar | boolean | true | 显示助手头像(false 时助手消息不渲染头像;用户头像不受影响) |
| showAssistantName | boolean | true | 显示助手名称(false 时助手消息不渲染头部名称行;用户名称不受影响) |
| userAvatar | string | '' | 用户头像图片 URL(空串回落内置图标;user-avatar 插槽优先) |
| assistantAvatar | string | '' | 助手头像图片 URL(空串回落内置默认头像「小维」;assistant-avatar 插槽优先) |
| userName | string | '' | 用户名称(空串回落「你」;user-name 插槽优先) |
| assistantName | string | '' | 助手名称(空串回落「小维」;assistant-name 插槽优先) |
| placeholder | string | '' | 输入框占位 |
| senderShadow | boolean | true | 输入区阴影效果(透传 Sender 的 shadow) |
| 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;message-avatar 优先于细分头像插槽) | | user-avatar / assistant-avatar | 自定义用户/助手头像(assistant-avatar 作用域 { status, message },status 为 thinking/done/error,可按状态换头像;优先于同名 props) | | user-name / assistant-name | 自定义用户/助手名称(作用域 { message },优先于同名 props) | | empty | 覆盖空态整区 | | scroll-button | 自定义滚到底部悬浮按钮整块(作用域 { scrollToBottom, awayFromBottom };覆盖后内置按钮与显隐逻辑均由插槽内容接管) | | scroll-button-icon | 仅自定义悬浮按钮图标内容(按钮本体/显隐不变) | | 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 | 100 | 用户上滚免打扰阈值(px):距底不超过该值视为「钉在底部」,流式期间自动跟随滚动 | | showScrollButton | boolean | true | 显示滚到底部悬浮按钮(空态不渲染) | | scrollButtonThreshold | number | 100 | 距底超过该值(px)才显示滚到底部悬浮按钮 | | showActions | boolean | true | 显示消息操作按钮(复制/重生成) | | showBranchSwitch | boolean | true | 显示分支切换器 | | showUserAvatar | boolean | true | 显示用户头像(助手头像不受影响) | | showUserName | boolean | true | 显示用户名称(助手名称不受影响) | | showAssistantAvatar | boolean | true | 显示助手头像(用户头像不受影响) | | showAssistantName | boolean | true | 显示助手名称(用户名称不受影响) | | userAvatar | string | '' | 用户头像图片 URL(透传 AiMessage) | | assistantAvatar | string | '' | 助手头像图片 URL(透传 AiMessage;空串回落内置默认头像) | | userName | string | '' | 用户名称(透传 AiMessage) | | assistantName | string | '' | 助手名称(透传 AiMessage;空串回落「小维」) |
| Event | 说明 | | --- | --- | | select / copy / regenerate / branch-change | 建议选中 / 复制 / 重生成 / 分支切换,载荷同 AiChat |
| Slot | 说明 | | --- | --- | | empty | 覆盖空态整区 | | scroll-button | 自定义滚到底部悬浮按钮整块(作用域 { scrollToBottom, awayFromBottom };覆盖后内置按钮与显隐逻辑均由插槽内容接管) | | scroll-button-icon | 仅自定义悬浮按钮图标内容(按钮本体/显隐不变) | | message-avatar / message-actions | 透传给每条 AiMessage(message-avatar 优先于细分头像插槽) | | user-avatar / assistant-avatar | 自定义用户/助手头像(assistant-avatar 作用域 { status, message },status 为 thinking/done/error,可按状态换头像;优先于同名 props) | | user-name / assistant-name | 自定义用户/助手名称(作用域 { message },优先于同名 props) |
| CSS 变量 | 默认 | 说明 |
| --- | --- | --- |
| --fv-messages-max-width | none | 滚动内容最大宽度;超出部分左右居中(内层 .fv-messages-inner,配合 margin: 0 auto) |
| Ref | 说明 | | --- | --- | | scrollToBottom(smooth?) / scrollToTop() | 滚动控制 |
AiMessage
单条消息(Markdown 正文 + 思考链折叠 + 附件 + 操作区)。
| Prop | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | message | MessageNode | —(必填) | 消息节点 | | branch | AiMessageBranchInfo | undefined | 分支元数据 { count, index };缺省视为单分支 | | showActions | boolean | true | 显示复制/重生成(仅 assistant) | | showBranchSwitch | boolean | true | 显示分支切换器(branch.count > 1 时) | | showUserAvatar | boolean | true | 显示用户头像(false 时用户消息不渲染头像块) | | showUserName | boolean | true | 显示用户名称(false 时用户消息不渲染头部名称行) | | showAssistantAvatar | boolean | true | 显示助手头像(false 时助手消息不渲染头像块) | | showAssistantName | boolean | true | 显示助手名称(false 时助手消息不渲染头部名称行) | | userAvatar | string | '' | 用户头像图片 URL(空串回落内置图标) | | assistantAvatar | string | '' | 助手头像图片 URL(空串回落内置默认头像「小维」) | | userName | string | '' | 用户名称(空串回落「你」) | | assistantName | string | '' | 助手名称(空串回落「小维」) |
用户消息操作区:恒渲染复制按钮,默认透明,鼠标移上消息(或键盘聚焦)时浮现(open-webui 交互)。 | autoCollapse | boolean | true | Thinking autoCollapse 透传 |
| Event | 说明 | | --- | --- | | copy / regenerate / branch-change | (message) / (message) / (dir, message) |
| Slot | 说明 | | --- | --- | | message-avatar | 自定义头像(作用域 { message };优先于细分头像插槽,两条角色消息均生效) | | user-avatar | 自定义用户头像(无作用域;优先于 userAvatar props) | | assistant-avatar | 自定义助手头像(作用域 { status, message },status 为 thinking/done/error,可按状态换头像;优先于 assistantAvatar props) | | user-name | 自定义用户名称(作用域 { message };优先于 userName props) | | assistant-name | 自定义助手名称(作用域 { message };优先于 assistantName props) | | 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时用映射文案(带code的DetOmsError) message_end的messageId(最终文本行 id)经onBackendMessageId回填到MessageNode.backendId——多轮工具循环下与message_start的首轮 id 不同,UI 落库/对账以它为准tool事件经onTool→useChat.onToolEvent→AiChat @tool-event上抛,消费方展示「正在查询库存…」等中间态- 配套 DTO 类型:
DetOmsSessionItem/DetOmsMessage/DetOmsSessionDetail/DetOmsMessagePage(消费方直接调会话管理端点时用)
useMessages(initial?)
树形消息状态中枢(响应式包装 createMessageTree)。
返回:messages(全量节点表)、currentId、chain(当前链 computed)、add、remove、edit、appendContent、navigate(id?, dir?)、setCurrent(id)、setStorage(adapter)、load()、flush()、toJSON()。绑定存储后树变更自动去抖(150ms)落盘。
useXStream()
SSE 流消费:startStream({ readableStream, transformStream? })、cancel()、data: Ref<SSEEvent[]>、error、isLoading。内部完成 \r\n/跨 chunk 边界归一化。
useSend() / XRequest
useSend():send({ url, method?, headers?, body?, transformStream?, onChunk? })(SSE data 为 JSON 且含 content 字段时逐块回调)、abort()、loading、error。在飞行守卫:上一个 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-coy、prism-dark、prism-funky、prism-okaidia、prism-solarizedlight、prism-tomorrow、prism-twilight。
FAQ
Q:页面是白屏/样式错乱?
A:确认已引入 element-plus/dist/index.css 与 @fv-ui/ai-chat/dist/style.css,且 app.use(ElementPlus)。
Q:<FvAiChat> 发送报"未配置 adapter 或 baseURL"?
A:adapter 与 baseURL 至少传一个。
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 待接线。 - 语音输入:
Sender的allowSpeech依赖浏览器SpeechRecognition(Chrome/Edge 可用,Firefox 不支持转写,降级为仅录音、确认后无文本);录音/波形需要 HTTPS 或 localhost 环境。 - 多实例暗色互斥:
theme采用单写者假设,多实例同屏请自行管理html.dark。
类型与导出
包入口导出全部组件、useChat/useMessages/useXStream/useSend/XRequest/useRecord、OpenAIAdapter、DetOmsChatAdapter(+ DetOmsError 及配套 DTO 类型)、createMessageTree、createLocalStorageAdapter/createMemoryAdapter、createSSEParser 及全部公共类型(MessageNode、ChatAdapter、ChatFile、MessageUsage、SSEEvent 等),消费方:
import { FvAiChat, OpenAIAdapter, useChat } from '@fv-ui/ai-chat'
import type { MessageNode, ChatAdapter } from '@fv-ui/ai-chat'