@fv-ui/ai-chat

基于 Vue 3 + Element Plus 的 AI 对话组件库,复刻 element-plus-x 与 open-webui 对话框:从底层气泡、发送框、Markdown 渲染,到上层的对话整框(<FvAiChat>)、分支树消息、Artifacts 预览,一次给全。
- 14 个基础组件(Bubble/Sender/Markdown/Thinking/...)自由拼装
- 对话应用层整框
<FvAiChat>:顶栏模型多选、消息列表、分支切换、Artifacts 面板、发送停止一体化
- 树形消息模型(同构 open-webui):一条提问可挂多个模型回答,分支可切换、可重生成
ChatAdapter 适配器协议:内置 OpenAI 兼容实现,可替换自研后端/Ollama
- 流式打字机、思考链折叠、KaTeX/代码高亮(Prism)开箱即用
安装
pnpm add @fv-ui/ai-chat
peerDependencies: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"。
Vite 项目必读:排除依赖预打包
内置头像/欢迎页 GIF 在 es 产物中以 new URL(..., import.meta.url) 按需引用。Vite dev server 默认把依赖交给 esbuild 预打包(optimizeDeps),而预打包不会复制该类资产——GIF 404 不显示(已知限制 vitejs/vite#8427)。需在 vite.config.ts 排除预打包,让本包走源码 ESM 管线:
// vite.config.ts
export default defineConfig({
optimizeDeps: {
// @fv-ui/ai-chat:es 产物内置 GIF 以 new URL(..., import.meta.url) 引用,
// esbuild 预打包不复制该类资产(vitejs/vite#8427),须排除预打包走源码 ESM 管线
exclude: ['@fv-ui/ai-chat']
}
})
说明:仅 Vite dev 需要;生产 build 走 Rollup 管线会自动把 GIF 复制进产物并重写路径,不受影响。UMD(<script> 直用 lib/index.js)不涉及。
基础组件层 API
FvMarkdown
Markdown 渲染(Sanitized by DOMPurify),内置 Prism 代码高亮与 KaTeX。
| Prop |
类型 |
默认 |
说明 |
| content |
string |
(必填) |
Markdown 源文本 |
| mdPlugins |
PluginWithParams[] |
|
额外 markdown-it 插件 |
| highlight |
(str, lang) => string |
内置 Prism |
自定义代码高亮 |
| isTyping |
boolean |
|
打字机模式样式钩子(透传占位) |
| codeTriggerLangs |
string[] |
|
代码块「预览」触发语言(fence 原始 lang 命中列表时注入预览按钮,点击行为由消费方事件委托实现) |
| tableToolbar |
boolean |
true |
表格工具栏:表格上方注入复制(复制表格 markdown 源文本)/下载(导出 CSV,带 BOM)单图标按钮 |
| locale |
LocaleInput(string | 对象) |
undefined |
语言包:'zh-CN'/'en'/'ko'/'vi'/'zh-HK' 整包或部分文案对象(优先于祖先注入与全局设置) |
| Ref |
说明 |
| getHtml() |
取当前渲染后的 HTML 字符串 |
Typewriter
打字机输出,支持续打/重打/雾化遮罩。
| Prop |
类型 |
默认 |
说明 |
| content |
string |
(必填) |
全文(流式时增量传入,子集扩展自动续打) |
| isMarkdown |
boolean |
false |
走 FvMarkdown 渲染(此时 suffix 不追加) |
| isFog |
boolean | { bgColor?; width? } |
false |
打字中尾部雾化遮罩 |
| typing |
boolean | { step?; interval?; suffix? } |
false |
开启打字机;对象形态可配步长/间隔/光标后缀 |
| codeTriggerLangs |
string[] |
|
代码块「预览」触发语言(isMarkdown 模式透传 Markdown) |
| locale |
LocaleInput |
undefined |
语言包(透传内嵌 Markdown 的代码块「预览」按钮文案) |
| Event |
说明 |
| start / writing / finish |
开始打字 / 每次推进 / 全部打完 |
| Ref |
说明 |
| interrupt() / continue() / restart() / destroy() |
暂停 / 继续 / 重打 / 重置清空 |
| renderedContent / isTyping / progress |
已输出文本 / 是否打字中 / 进度 0-1 |
Bubble
单条气泡(内部按需挂载 Typewriter/Markdown)。
| 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 触发索引 |
| locale |
LocaleInput(string | 对象) |
undefined |
语言包:'zh-CN'/'en'/'ko'/'vi'/'zh-HK' 整包或部分文案对象(优先于祖先注入与全局设置) |
| 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 |
'' |
占位文案(缺省回落语言包 sender.inputPlaceholder) |
| autosize |
{ minRows?; maxRows? } |
{ minRows: 3, maxRows: 7 } |
自增高(行高 22px;默认 3 行起步,内容多时自动加高至最多 7 行后内部滚动;显式传 {} 退回固定单行) |
| submitType |
'enter' | 'shiftEnter' | 'cmdOrCtrlEnter' | 'altEnter' |
'enter' |
提交按键模式 |
| loading |
boolean |
false |
加载态(发送键变停止键,输入区禁用;停止键出现后 600ms 降险窗口内不可停,吸收发送瞬间的快速连点) |
| 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 |
允许空内容提交(附件场景:只发文件不输文字) |
| locale |
LocaleInput(string | 对象) |
undefined |
语言包:'zh-CN'/'en'/'ko'/'vi'/'zh-HK' 整包或部分文案对象(优先于祖先注入与全局设置) |
| Event |
说明 |
| update:modelValue / change |
输入值变化 |
| submit |
(value) 提交(提交后内部清空) |
| cancel |
点击停止按钮(loading 翻 true 后 600ms 内的点击被吞不发——防连点误停) |
| 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,后端过滤) |
| locale |
LocaleInput(string | 对象) |
undefined |
语言包:'zh-CN'/'en'/'ko'/'vi'/'zh-HK' 整包或部分文案对象(候选加载文案);内嵌 Sender 的文案经祖先注入(如 AiChat)或全局设置生效 |
事件: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 |
| locale |
LocaleInput(string | 对象) |
undefined |
语言包:'zh-CN'/'en'/'ko'/'vi'/'zh-HK' 整包或部分文案对象(优先于祖先注入与全局设置) |
| 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 插槽嵌 FvMarkdown) |
| status |
'start' | 'thinking' | 'end' | 'interrupted' | 'error' |
'start' |
思考状态(interrupted 为用户中断态,警告橙图标) |
| disabled |
boolean |
false |
禁用切换 |
| autoCollapse |
boolean |
false |
status 变为 end 时自动收起 |
| hideHeader |
boolean |
false |
隐藏折叠头(单 item 链平铺形态):不渲染切换按钮,内容区常驻展开 |
| buttonWidth |
string |
'' |
切换按钮区宽度 |
| maxWidth |
string |
'' |
内容区最大宽度 |
| color / backgroundColor |
string |
'' |
前景色 / 背景色 |
| locale |
LocaleInput(string | 对象) |
undefined |
语言包:'zh-CN'/'en'/'ko'/'vi'/'zh-HK' 整包或部分文案对象(优先于祖先注入与全局设置) |
| Event |
说明 |
| update:modelValue |
展开/收起同步 |
| Slot |
说明 |
| status-icon / label / arrow |
自定义状态图标 / 文案 / 箭头 |
| content / error |
思考内容 / error 态内容 |
| Ref |
说明 |
| modelValue / toggle() |
展开状态 / 手动切换 |
ThoughtChain
思维链步骤条。text 步(kind: 'text')为「正在思考/思考完成」标题行 + 可折叠 description 滚动区(默认收起;展开时 ElScrollbar 限高 269px,流式期间自动滚底;内容被滚动裁掉的方向在上/下缘渲染 35px 渐隐条 scroll-edge-fade-top / scroll-edge-fade-bottom,贴顶/贴底各自隐藏);工具步 kind: 'tool' 为普通步骤行。
| CSS 变量 |
默认 |
说明 |
| --fv-chain-edge-fade-color |
var(--el-bg-color) |
text 步滚动区上/下缘渐隐条颜色(默认跟随页面底色,暗色模式经 element-plus 变量自动适配;思考链叠在非底色卡片上时可覆盖) |
| Prop |
类型 |
默认 |
说明 |
| items |
ThoughtChainItem[] |
[] |
步骤项 { key, title?, status?: 'pending'/'success'/'loading'/'error', icon?, description?, kind?: 'tool'/'text', typing?, closed? } |
| direction |
'vertical' | 'horizontal' |
'vertical' |
排列方向 |
| collapsible |
boolean |
false |
工具步长文案展开/收起(超 3 行截断 + 行尾「展开」) |
| showDuration |
boolean |
false |
显示工具步用时 description(text 步 description 不受影响) |
| locale |
LocaleInput(string | 对象) |
undefined |
语言包:'zh-CN'/'en'/'ko'/'vi'/'zh-HK' 整包或部分文案对象(优先于祖先注入与全局设置) |
| Slot |
说明 |
| content |
覆盖默认 title/description(作用域 { item, index }) |
Welcome
空态欢迎页。icon/name/description 均未传时显示内置品牌默认欢迎(「你好,我是 Gizmo」问候高亮 + IP 形象动图 + 默认描述),逐 prop 独立覆盖。
结构:图标区(DOM 序在前)+ 主列(.fv-welcome-main:文案列 + 建议区);纵排图标居中显示在文案上方;横排主列为唯一文档流内容(左对齐),吉祥物浮层悬于文案右上(经 flex order 排到主列右侧)——建议区与文案同列,不会把吉祥物顶高。
| Prop |
类型 |
默认 |
说明 |
| icon |
string | Component |
'' |
图标(字符串为图片地址;未传渲染内置品牌欢迎动图,横排/纵排同图,仅尺寸不同:横排大图 108px×scale1.8,纵排 96px) |
| name |
string |
'' |
名称(未传回落内置问候语「你好,我是 Gizmo」) |
| description |
string |
'' |
描述(纯文本;未传回落内置默认描述) |
| layout |
'vertical' | 'horizontal' |
'vertical' |
布局:vertical 纵排(吉祥物居中在文案上方)/ horizontal 横排(文案左对齐,欢迎吉祥物浮层悬于文案右上,空态欢迎形态,原型 .intro 同构) |
| locale |
LocaleInput |
undefined |
语言包(内置欢迎文案) |
| Slot |
说明 |
| icon / name / description |
覆盖对应区块 |
| suggestions |
建议区(AiMessages 在此渲染 Prompts 快捷条;横排时与文案同列靠左) |
CSS 变量
| CSS 变量 |
默认 |
说明 |
| --fv-welcome-icon-size |
纵排 96px |
内置品牌动图尺寸(纵排;横排吉祥物固定 108px×scale1.8 对齐原型) |
| --fv-welcome-max-width |
780px |
空态欢迎块宽度(AiMessages 空态轴线;文案/吉祥物/快捷条共用。AiChat 整框内空态时与输入区同轴,回落 --fv-empty-max-width;独立使用 AiMessages 时为 780px,可覆盖) |
| --fv-welcome-desc-max-width |
590px |
横排描述文字最大宽度(原型 .intro p) |
Prompts
建议 prompt 网格。
| Prop |
类型 |
默认 |
说明 |
| prompts |
PromptItem[] |
[] |
建议项 { key(必填), label?, description?, icon?(EP 图标名字符串或组件), disabled? } |
| title |
string |
'' |
网格标题 |
| wrap |
boolean |
true |
换行模式(默认 true 自动换行;false 不换行,超出部分横向滚动) |
| Event |
说明 |
| select |
(item) 点击(disabled 项不触发) |
| Slot |
说明 |
| default |
覆盖整卡(作用域 { item, index }) |
| footer |
列表尾部追加内容(如「换一批」按钮):换行模式作为 flex item 接在最后一个条目后同行,不换行/列模式占满一行 |
FilesCard
单个文件卡片。文档模式按扩展名自动映射设计稿类型图标(f-*.svg):xlsx/xls/csv 等表格→excel、pdf→pdf、pptx/ppt→ppt、docx/doc/wps→word、txt/md/json/log 等文本→txt,未命中回落通用 file 图标;图标 fill=currentColor 随主题色走。
| 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 红边 + 失败原因 |
| errorText |
string |
'' |
失败原因红字(error 态展示于卡片下方,超宽截断 title 全文) |
| imageSize |
string |
'' |
缩略图尺寸(width/height 同值) |
| onDelete |
() => void |
— |
删除按钮回调(按钮常驻右上角,hover 浮现) |
| onDownload |
() => void |
— |
下载回调:注入时右上角渲染下载按钮(28px hover 浮现,比删除按钮大一圈,垂直居中)。消息附件卡走此路(内置触发浏览器下载),输入区附件卡不注入 |
| locale |
LocaleInput(string | 对象) |
undefined |
语言包:'zh-CN'/'en'/'ko'/'vi'/'zh-HK' 整包或部分文案对象(优先于祖先注入与全局设置) |
| 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 拦截 |
| upload |
(file, item) => Promise<Partial<AttachmentItem> | undefined | void> | ... |
|
上传钩子(先传后发):选文件即调——入列即 loading,成功返回附件描述(含服务端附件 id)回填列表项置 success,失败(抛错/resolve 错误文案串)置 error 带失败原因(可重传再走钩子);缺省不调(纯本地待发列表,入列直接 success) |
| disabled |
boolean |
false |
整体禁用(触发按钮不可点;生成中锁定输入场景,单飞行闸语义) |
| layout |
'inline' | 'triggerOnly' | 'listOnly' |
'inline' |
布局:inline 回形针+列表+提示一行;triggerOnly 只渲染回形针按钮;listOnly 只渲染文件列表(拆分布局供整框组合:按钮进操作栏、列表进输入区上方) |
| locale |
LocaleInput(string | 对象) |
undefined |
语言包:'zh-CN'/'en'/'ko'/'vi'/'zh-HK' 整包或部分文案对象(优先于祖先注入与全局设置) |
| Event |
说明 |
| update:modelValue / change |
列表变化(全量数组) |
| exceed |
(file) 超出 count |
| delete |
(item) 删除某项 |
| uploaded |
(item, result?) 上传钩子落定(单项):成功回填后的列表项 + 上传结果;失败时 result 为 undefined(失败原因在 item.errMessage,FilesCard 红字展示) |
| 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 |
— |
分组排序比较器(缺省保持插入顺序) |
| locale |
LocaleInput(string | 对象) |
undefined |
语言包:'zh-CN'/'en'/'ko'/'vi'/'zh-HK' 整包或部分文案对象(优先于祖先注入与全局设置) |
| 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 |
FvBlobHalo
blob 光晕(FvBlobHalo,基础组件):多色模糊「色团」氛围光——内联 SVG 8 个 blob(每色一组正体 + alt 淡体),path 以 d: path() 关键帧在形状配方间连续变形(蠕变),每个色团各自异速旋转,全部居中叠放:中心混色、随转动流动;第 4 槽位是高光位(fill 走径向渐变,中心实色 → 向外透明渐大,模拟高光柔光)。FvAiChatLauncher 内嵌同款效果;亦可独立用在任意需要氛围光的场景(装饰背景、头像/卡片区光效等)。无定位(inline-flex 占位,pointer-events: none),挂载位置与层叠由使用方决定。
<FvBlobHalo :size="88" :opacity="0.8" :colors="['#ff8c42', '#fcaf58', '#f9c784', '#ffffff']" />
| Prop |
类型 |
默认 |
说明 |
| colors |
(string | FvBlobHaloColor | null)[] |
主题色 + AI 紫(#bb74ff)+ 浅紫(#a78bfa)+ 白 |
色团色板,每色一组正体 + alt 淡体,最多 4 色;槽位传色值字符串或 { color, opacity?, altOpacity?, mid?, scale? } 对象(按色覆盖透明度/高光渐变/缩放,缺省回落内置);null/undefined 保留默认色(部分自定义) |
| size |
number |
88 |
光晕尺寸(px,正圆直径) |
| scale |
number |
1 |
相对放大倍率(>1 越出挂载盒,需使用方容器 overflow 可见) |
| opacity |
number |
0.8 |
正体透明度(0~1) |
| altOpacity |
number |
0.35 |
alt 淡体透明度(0~1) |
| rotateDuration |
number |
25 |
旋转一周时长(s;各槽位按内置比例差异化;0 停转只留变形蠕动) |
| paused |
boolean |
false |
暂停全部动画(静帧装饰) |
说明:
- 第 4 槽位为「高光位」:fill 用径向渐变(中心 100% 实色 → 半径 50% 处浓度
mid(默认 .45)→ 边缘全透明),自带「中心实、向外透明渐大」的高光过渡;mid 越小淡出越快;其余槽位彩团平涂色 + blur(叠色饱满)
- 色板槽位有内置「角色」:第 1 色大清晰(主题位,scale .8)、第 2/3 色更小更快(辅色位,.78/.76)、第 4 色即高光位(.62);槽位对象
scale 可按色覆盖缩放
- 按色调透明度/缩放:
:colors="[null, { color: '#ff0000', opacity: 1, scale: 0.6 }]"(对象槽位;缺省项回落内置)
- 默认色板中的 AI 紫可用 CSS 变量
--fv-blob-purple/--fv-blob-purple-2 覆盖(无需传 colors)
prefers-reduced-motion 下自动停动
d: path() 变形仅 Chromium 支持,其他内核形状静止在初始形(旋转照常);path 带初始 d 兜底可渲染
FvSvgIcon
通用 SVG 图标(FvSvgIcon,基础组件):按 name 引用库内 assests/icons/<name>.svg,以 ?raw 源文本内联渲染(v-html)——svg 吃 CSS 继承,fill="currentColor" 强制覆写,颜色随文字色/亮暗主题/hover 态走(这是不用 <img src> 的原因:外部文档吃不到 currentColor)。换图标只替换 assests/icons/ 里的 svg 文件,代码零改动;新增图标丢文件进目录即自动可用(构建期 glob 收集)。svg 文件内的 width/height(设计稿常见 width="200")与硬编码 fill/class 会被剥除,统一由组件控制尺寸;viewBox 保留。
<FvSvgIcon name="maximize" :size="18" />
| Prop |
类型 |
默认 |
说明 |
| name |
string |
(必填) |
图标名(assests/icons/ 下文件名去扩展名;内置 bulb/dislike[-filled]/f-{excel,pdf,ppt,word,txt,file}/like[-filled]/maximize/sidebar/sparkle/to-{window,drawer}/win-restore) |
| size |
number | string |
16 |
尺寸:number 按 px;字符串原样透传(如 '1em' 跟字号) |
另导出 iconRegistry(Record<string, string>,name → svg 源文本映射),可查询可用图标名或取源文本自行处理。
对话应用层 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 |
'' |
顶栏标题(同时作空态欢迎页名称;未传时欢迎页显示内置默认问候「你好,我是 Gizmo」) |
| showHeader |
boolean |
false |
显示顶栏(标题 + 模型选择器;models 多选选择器在顶栏内,需开启才可见) |
| centerWelcome |
boolean |
true |
空态居中:未开始对话时欢迎内容与输入区整体垂直居中,首条消息后输入区恢复固定底部 |
| showScrollButton |
boolean |
true |
显示滚到底部悬浮按钮(透传 AiMessages;距底超过 scrollButtonThreshold 时出现) |
| scrollButtonThreshold |
number |
100 |
滚到底部悬浮按钮显示阈值(px,透传 AiMessages) |
| scrollThreshold |
number |
100 |
用户上滚免打扰阈值(px,透传 AiMessages):距底不超过该值视为「钉在底部」,流式期间自动跟随滚动;调小可恢复更灵敏的跟随 |
| copyToClipboard |
boolean |
true |
复制按钮内置剪贴板写入(透传 AiMessages→AiMessage):false 时只上抛 copy 事件不写剪贴板,宿主在事件里自行接管写入(避免与内置写入互相覆盖) |
| showUserAvatar |
boolean |
false |
显示用户头像(默认隐藏;false 时用户消息不渲染头像;助手头像不受影响) |
| showUserName |
boolean |
false |
显示用户名称(默认隐藏;false 时用户消息不渲染头部名称行;助手名称不受影响) |
| showAssistantAvatar |
boolean |
true |
显示助手头像(false 时助手消息不渲染头像;用户头像不受影响) |
| showAssistantName |
boolean |
false |
显示助手名称(默认隐藏;false 时助手消息不渲染头部名称行;用户名称不受影响) |
| userAvatar |
string |
'' |
用户头像图片 URL(空串回落内置图标;user-avatar 插槽优先) |
| assistantAvatar |
string |
'' |
助手头像图片 URL(传入完全覆盖不分状态;空串按状态回落内置 GIF:thinking=fvai-thinking.gif,done/error=fvai-idle.gif;assistant-avatar 插槽优先) |
| assistantAvatarScale |
number |
1 |
助手头像缩放系数(默认 1 不缩放;亦可用 CSS 变量 --fv-msg-avatar-scale 覆盖,prop 优先) |
| avatarCircle |
boolean |
true |
头像圆形模式(默认 true:圆角 50%;false 回落默认圆角;亦可用 CSS 变量 --fv-msg-avatar-radius 直接覆盖圆角取值,变量优先于开关) |
| userName |
string |
'' |
用户名称(空串回落「你」;user-name 插槽优先) |
| assistantName |
string |
'' |
助手名称(空串回落「Gizmo」;assistant-name 插槽优先) |
| placeholder |
string |
'' |
输入框占位(缺省回落语言包 sender.inputPlaceholder:zh「输入你的问题,查询数据、分析问题或生成报表…」) |
| senderAutosize |
{ minRows?; maxRows? } |
{ minRows: 3, maxRows: 7 } |
输入区自增高(透传 Sender autosize;默认 3 行起步,内容多时自动加高至最多 7 行后内部滚动) |
| toolStepLabel |
(toolName) => string |
undefined |
工具名 → 步骤展示名(透传 useChat toolStepLabel) |
| typing |
boolean | { step?; interval? } |
false |
打字机(透传 AiMessages;仅 assistant 流式期间逐字平滑打出,落定/历史消息整段渲染) |
| autoCollapse |
boolean |
true |
正文出完自动收起思考链(透传 AiMessage;status 落 done 且正文打完才收;error 不收) |
| collapsible |
boolean |
false |
思考链工具步长文案展开/收起(透传 ThoughtChain;超 3 行截断 + 行尾「展开/收起」) |
| showDuration |
boolean |
false |
显示工具步用时 description(透传 ThoughtChain;默认关闭) |
| showTime |
boolean |
true |
显示消息时间(操作区按钮之后;时间源:message.createdTime——Fv 系后端直灌,秒级/毫秒级时间戳或 ISO 字符串自动识别——优先,缺省回落 message.createdAt。助手消息 SSE 生成中不显示,正文打完字(未启用打字机则落定)后才显示;error 即显;user 消息恒显示) |
| timeFormat |
(message, timestamp) => string |
undefined |
消息时间自定义格式化(透传消息与已归一为 ms 的时间戳;缺省 'YYYY-MM-DD HH:mm',本机时区) |
| senderShadow |
boolean |
true |
输入区阴影效果(透传 Sender 的 shadow) |
| suggestions |
AiChatSuggestion[] |
[] |
空态建议列表(Prompts 项,key 必填;空态快捷条为胶囊形态,description 不显示) |
| suggestionsWrap |
boolean |
true |
空态建议换行模式(true 自动换行;false 不换行,超出横向滚动) |
| welcomeLayout |
'horizontal' | 'vertical' |
'horizontal' |
空态欢迎页布局:horizontal 横排(默认)/ vertical 纵排(悬浮窗等窄宽场景) |
| showRefreshSuggestions |
boolean |
false |
空态建议区底部「换一批」按钮(透传 AiMessages,由其在空态建议区渲染;组件只上抛 @refresh-suggestions 事件,轮换数据由消费方处理) |
| refreshSuggestionsText |
string |
'' |
「换一批」按钮文案(透传 AiMessages;缺省回落 locale welcome.refreshSuggestions) |
| followupStyle |
'list' | 'chips' |
'list' |
追问建议展示样式(透传 AiMessages→AiMessage):'list' 纵向列表一行一条,渲染在回答操作区下方,顶部浅灰分隔线 + 「追问」标题(不加粗,与正文同色,字号随正文,标题文案经语言包 aiMessage.followupsTitle 自定义),条目 100% 宽、下划线分隔(浅灰,末条不出线)、字号随正文、文案次级灰,历史消息同样渲染;'chips' 为旧横向气泡形态(换行铺排,无标题)。正文打字效果完成后才渲染(流式 suggestions 事件到达不显示,落定+打完字整体浮现) |
| features |
AiChatFeatures |
{} |
特性开关 { artifacts?, tts?, queue?, continueGenerate?, fullscreenEditor? }(当前仅 artifacts 生效);artifacts 传 true 用默认配置,传对象 { width?, minWidth?, maxWidth?, autoOpen?, codeBlockTrigger? } 自定义:宽度三参控面板尺寸(默认 420px/360px/60%),autoOpen(默认 true)检测到代码块自动弹面板,codeBlockTrigger(默认 true)在消息 html/svg 代码块右上角显示「预览」按钮、点击打开对应面板 |
| allowAttachments |
boolean |
false |
开启输入区附件上传(回形针在操作栏左侧与提交按钮同行,已选列表在输入框上方;只传附件不输文字也可提交) |
| accept |
string |
'' |
附件 input accept 属性(透传 Attachments),如 image/*,.pdf,.txt。缺省 '' 不限制(zip/exe 等是否可传由消费方自主决定);显式传值时除选择器过滤外组件内注入 beforeUpload 硬校验——白名单外文件(选择器切「所有文件」绕过 accept)不入列,自动 toast 提示(文案经语言包 errors.attachmentRejected 自定义,{name} 为文件名)并上抛 @attachment-reject |
| attachmentLimit |
number |
0 |
附件数量上限(0 不限,超限走 @attachment-exceed) |
| uploadAttachment |
(file, item) => Promise<Partial<AttachmentItem> | string | undefined | void> | ... |
undefined |
附件上传钩子(先传后发):选文件即调,成功返回附件描述(id/name/type 等回填列表项,id 随发送进入 adapter 的 user 消息 files);返回错误文案串/抛错置 error 可重传。缺省不调(附件仅本地待发展示,不落服务端);上传中(loading 项)提交按钮禁用拒发,error 残留项不随发送 |
| resolveFileUrl |
(file: ChatFile) => Promise<string> | string |
undefined |
附件地址解析钩子(鉴权取流):消息气泡渲染附件 url 前调用,返回可直渲地址(如 JWT fetch 换 blob objectURL);缺省原样使用 url(blob/dataURL/公开地址)。远端 url 解析结果组件内按 url 缓存 + 在途去重;解析失败回落原 url 不阻塞渲染 |
| feedbackHandler |
(message, action: 'LIKE' | 'DISLIKE' | 'CLEAR', comment?) => Promise<unknown> | unknown |
undefined |
消息评价提交钩子(透传 AiMessages→AiMessage;仅 assistant 最终回答行渲染 👍/👎,缺省不渲染按钮)。消费方在此接后端评价端点(如 Fv 系 POST /message/feedback):resolved 即成功;返回错误文案串/抛错触发 feedback-error(乐观更新已回滚);action=CLEAR 取消评价(comment 忽略)。交互语义:点击 👍 直接提交(不弹附言),点击 👎 弹居中附言弹窗(textarea ≤500 字 + 提交/跳过),再次点击当前态按钮发 CLEAR 取消评价(附言一并清空);👍↔👎 任意切换,后一次整体覆盖前一次(含附言);提交中按钮禁用防连点 |
| theme |
'light' | 'dark' | 'auto' |
'light' |
主题(向 html 注入/移除 dark class) |
| locale |
LocaleInput(string | 对象) |
undefined |
语言包:'zh-CN'/'en'/'ko'/'vi'/'zh-HK' 整包或部分文案对象(解析后注入整框后代) |
v-model:messages 半受控语义:外部 messages 仅在 setup 时一次性深拷贝灌入(用于恢复历史);组件内部每次变更全量上抛 update:messages(同帧去抖),父组件需经 v-model 回写维持单向数据流;父组件后续直接改 prop 不会回灌生效。
| Event |
说明 |
| update:messages |
(MessageNode[]) 全量消息链(去抖后) |
| send |
(text, files?) 用户发送(allowAttachments 开启时 files 为降维后的 ChatFile[]) |
| suggestion-select |
(item) 点击空态建议项(先于 send 触发;label 为空的项不发) |
| followup-select |
(item, message) 点击回答尾部追问建议(先于 send 触发;组件内部已接住转 send 开启新一轮,事件仅供消费方可观测/埋点;assistant 消息带 suggestions 即渲染,不再限链尾——历史消息同样显示;正文打字效果完成后才渲染) |
| message-complete |
(message) 链尾 assistant 生成完成 |
| error |
(err, message?) 生成出错 |
| branch-change |
(dir, message) 分支切换 |
| attachment-exceed |
(file) 附件超过 attachmentLimit 上限 |
| attachment-reject |
(file) 附件被类型白名单拦截(显式 accept 的白名单外文件,如 zip/exe;文件选择器切「所有文件」绕过 accept 时由 beforeUpload 兜底拦截)。组件已自动 toast 提示,事件仅供消费方埋点/追加处理 |
| attachment-uploaded |
(item, { ok, message? }) 附件上传钩子落定(allowAttachments + uploadAttachment 场景):ok=false 时 message 为失败原因,宿主可 toast/埋点 |
| message-file-click |
(file, message) 点击消息气泡附件 chips:默认行为——图片类经 resolveFileUrl 解析后打开内置 ElImageViewer 放大(同消息多图轮播),文档类无内置行为,宿主接此事件自行下载 |
| feedback-change |
(message, action: 'LIKE' | 'DISLIKE' | 'CLEAR') 评价提交成功(乐观落定后触发;message.feedback 已更新),宿主可埋点/联动 |
| feedback-error |
(message: string) 评价提交失败(钩子 reject/返回错误文案;乐观更新已回滚,message 为错误文案) |
| scroll |
(event) 消息区滚动(原生 scroll 事件透传;空态无滚动容器不触发) |
| refresh-suggestions |
() 空态建议区「换一批」点击(AiMessages 层实现并上抛;消费方监听后自行更换建议数据) |
| Slot |
说明 |
| message-avatar / message-actions |
透传给 AiMessages(作用域见 AiMessage;message-avatar 优先于细分头像插槽) |
| user-avatar / assistant-avatar |
自定义用户/助手头像(assistant-avatar 作用域 { status, message },status 为 thinking/done/error,可按状态换头像;优先于同名 props;未传插槽且未传 assistantAvatar 时内置头像本身随状态切换) |
| user-name / assistant-name |
自定义用户/助手名称(作用域 { message },优先于同名 props) |
| welcome-name / welcome-description / welcome-icon |
单独配置欢迎页主标题/描述/吉祥物(转发 Welcome 同名插槽;优先于 title 等默认文案) |
| welcome-suggestions |
单独配置欢迎页建议区(整体替换默认 Prompts 快捷条;建议渲染在文案下方与文案同列) |
| refresh-suggestions |
单独配置欢迎页「换一批」按钮(整体替换默认按钮;替换后点击行为由消费方自管,组件不再自动触发事件) |
| suggestions-footer |
欢迎页建议区底部追加内容(与「换一批」按钮同行,单独使用时也渲染底部容器;AiMessages 层插槽) |
| empty |
覆盖空态整区 |
| scroll-button |
自定义滚到底部悬浮按钮整块(作用域 { scrollToBottom, awayFromBottom };覆盖后内置按钮与显隐逻辑均由插槽内容接管) |
| scroll-button-icon |
仅自定义悬浮按钮图标内容(按钮本体/显隐不变) |
| sender-prefix |
输入区前缀 |
| CSS 变量 |
默认 |
说明 |
| --fv-empty-max-width |
780px |
空态(welcome 态)轴线最大宽度:欢迎块/快捷条与输入区(Sender)共用同轴线,宽屏居中;仅空态生效,首条消息后恢复全宽 |
| --fv-msg-avatar-size |
50px |
消息头像块边长(宽高同值;内置图标字号随块尺寸自适应) |
| --fv-msg-avatar-radius |
50%(avatarCircle 默认开) |
消息头像圆角(avatarCircle=false 时回落默认圆角;直接赋值可自定义任意圆角,优先于开关) |
| --fv-msg-avatar-scale |
1 |
助手头像缩放系数(默认 1 不缩放;放大可裁掉图片四周留白;assistantAvatarScale prop 优先于此变量) |
| Ref |
说明 |
| sendMessage(text, files?) |
程序化发送 |
| stop() |
停止生成(保留半截内容,静默收口不发 complete/error) |
| regenerate(messageId?) |
重生成(缺省链尾 assistant) |
| appendToMessage(id, text) |
向指定消息追加文本 |
| clearMessages() |
清空消息树 |
| exportTxt() |
导出当前链为纯文本 |
| openArtifact(key) |
程序化打开某个 artifact |
| expandThoughtChains() / collapseThoughtChains() |
全部展开/折叠当前链上消息的思考链(user 消息、无思考链消息、单 item 平铺形态不受控) |
| scrollTo(options) |
消息区滚动到指定位置(options 同原生 ScrollToOptions,如 { top: 0, behavior: 'smooth' };也可传 number) |
AiMessages
消息列表(空态渲染 Welcome,建议快捷条经 Welcome #suggestions 插槽渲染在文案下方),吸底滚动。
| Prop |
类型 |
默认 |
说明 |
| messages |
MessageNode[] |
(必填) |
当前链消息(根 → 焦点) |
| nodes |
Record<string, MessageNode> |
{} |
全量节点表(分支计算需查父 childrenIds) |
| icon |
string | Component |
'' |
空态欢迎页图标 |
| name |
string |
'' |
空态欢迎页名称 |
| description |
string |
'' |
空态欢迎页描述 |
| suggestions |
AiMessagesSuggestion[] |
[] |
空态建议列表 |
| suggestionsTitle |
string |
'' |
空态建议区标题 |
| suggestionsWrap |
boolean |
true |
空态建议换行模式(true 自动换行;false 不换行,超出横向滚动) |
| showRefreshSuggestions |
boolean |
false |
空态建议区底部「换一批」按钮(本组件实现:渲染在空态建议区,横排随气泡同行;只上抛 @refresh-suggestions 事件,轮换数据由消费方处理) |
| refreshSuggestionsText |
string |
'' |
「换一批」按钮文案(缺省回落 locale welcome.refreshSuggestions) |
| scrollThreshold |
number |
100 |
用户上滚免打扰阈值(px):距底不超过该值视为「钉在底部」,流式期间自动跟随滚动 |
| showScrollButton |
boolean |
true |
显示滚到底部悬浮按钮(空态不渲染) |
| scrollButtonThreshold |
number |
100 |
距底超过该值(px)才显示滚到底部悬浮按钮 |
| showActions |
boolean |
true |
显示消息操作按钮(复制/重生成;生成中 pending/streaming 不渲染) |
| copyToClipboard |
boolean |
true |
复制按钮内置剪贴板写入(透传 AiMessage):false 时只上抛 copy 事件不写剪贴板,宿主在事件里自行接管写入 |
| showBranchSwitch |
boolean |
true |
显示分支切换器 |
| showUserAvatar |
boolean |
false |
显示用户头像(默认隐藏;助手头像不受影响) |
| showUserName |
boolean |
false |
显示用户名称(默认隐藏;助手名称不受影响) |
| showAssistantAvatar |
boolean |
true |
显示助手头像(用户头像不受影响) |
| showAssistantName |
boolean |
false |
显示助手名称(默认隐藏;用户名称不受影响) |
| userAvatar |
string |
'' |
用户头像图片 URL(透传 AiMessage) |
| assistantAvatar |
string |
'' |
助手头像图片 URL(透传 AiMessage;传入完全覆盖不分状态,空串按状态回落内置 GIF) |
| assistantAvatarScale |
number |
1 |
助手头像缩放系数(透传 AiMessage;默认 1 不缩放) |
| userName |
string |
'' |
用户名称(透传 AiMessage) |
| assistantName |
string |
'' |
助手名称(透传 AiMessage;空串回落「Gizmo」) |
| showTime |
boolean |
true |
显示消息时间(透传 AiMessage;时间源 createdTime 优先,缺省回落 createdAt。助手消息生成中不显示,打完字后才显示) |
| timeFormat |
(message, timestamp) => string |
undefined |
消息时间自定义格式化(透传 AiMessage) |
| autoCollapse |
boolean |
true |
正文出完自动收起思考链(透传 AiMessage;status 落 done 且正文打完才收;error 不收) |
| resolveFileUrl |
(file: ChatFile) => Promise<string> | string |
undefined |
附件地址解析钩子(透传 AiMessage;鉴权取流换可直渲地址,见 AiChat) |
| followupStyle |
'list' | 'chips' |
'list' |
追问建议展示样式(透传 AiMessage;默认 'list' 纵向列表——顶部灰色分隔线 + 加粗「追问」标题(字号/颜色随正文)、条目 100% 宽下划线分隔字号随正文;'chips' 旧横向气泡,见 AiChat)。正文打字效果完成后才渲染(流式 suggestions 事件到达不显示) |
| feedbackHandler |
(message, action, comment?) => Promise<unknown> | unknown |
undefined |
消息评价提交钩子(透传 AiMessage;语义见 AiChat,缺省不渲染 👍/👎) |
| locale |
LocaleInput(string | 对象) |
undefined |
语言包:'zh-CN'/'en'/'ko'/'vi'/'zh-HK' 整包或部分文案对象(优先于祖先注入与全局设置) |
| Event |
说明 |
| select / copy / regenerate / branch-change |
建议选中 / 复制 / 重生成 / 分支切换,载荷同 AiChat |
| followup-select |
(item, message) 点击追问建议 chip(透传 AiMessage,载荷同 AiChat) |
| message-file-click |
(file, message) 点击消息附件 chips(透传 AiMessage;图片类默认打开内置放大预览,文档类宿主自行下载) |
| feedback-change / feedback-error |
评价提交成功(message, action)/ 失败(错误文案;透传 AiMessage,语义见 AiChat) |
| scroll |
(event) 滚动容器滚动(原生 scroll 事件透传;空态无滚动容器不触发) |
| refresh-suggestions |
() 空态建议区「换一批」点击(消费方监听后自行更换建议数据) |
| Slot |
说明 |
| empty |
覆盖空态整区 |
| welcome-name / welcome-description / welcome-icon |
单独配置欢迎页主标题/描述/吉祥物(转发 Welcome 同名插槽) |
| welcome-suggestions |
单独配置欢迎页建议区(整体替换默认 Prompts 快捷条) |
| refresh-suggestions |
单独配置欢迎页「换一批」按钮(整体替换默认按钮;替换后点击行为由消费方自管) |
| suggestions-footer |
欢迎页建议区底部追加内容(与「换一批」按钮同行) |
| scroll-button-icon |
仅自定义悬浮按钮图标内容(按钮本体/显隐不变) |
| message-avatar / message-actions |
透传给每条 AiMessage(message-avatar 优先于细分头像插槽) |
| user-avatar / assistant-avatar |
自定义用户/助手头像(assistant-avatar 作用域 { status, message },status 为 thinking/done/error,可按状态换头像;优先于同名 props;未传插槽且未传 assistantAvatar 时内置头像本身随状态切换) |
| user-name / assistant-name |
自定义用户/助手名称(作用域 { message },优先于同名 props) |
| CSS 变量 |
默认 |
说明 |
| --fv-messages-max-width |
none |
滚动内容最大宽度;超出部分左右居中(内层 .fv-messages-inner,配合 margin: 0 auto) |
| --fv-messages-top-gap |
22px |
消息区滚动容器顶部留白:首条消息距顶部的间距,不贴顶;空态不生效(无滚动容器) |
| --fv-messages-bottom-gap |
72px |
消息区滚动容器底部留白:滚到底时最后一条消息距底部的间距,不贴输入区(sender);空态不生效(无滚动容器) |
| Ref |
说明 |
| scrollTo(options) |
滚动到指定位置(options 同原生 ScrollToOptions,也可传 number) |
| scrollToBottom(smooth?) / scrollToTop() |
滚动控制 |
| expandThoughtChains() / collapseThoughtChains() |
全部展开/折叠当前链上消息的思考链 |
AiMessage
单条消息(Markdown 正文 + 思考链折叠 + 附件 + 操作区)。
| Prop |
类型 |
默认 |
说明 |
| message |
MessageNode |
(必填) |
消息节点 |
| branch |
AiMessageBranchInfo |
undefined |
分支元数据 { count, index };缺省视为单分支 |
| isLast |
boolean |
false |
是否当前链最后一条消息(由 AiMessages 注入)。追问建议已不再限链尾(历史消息同样渲染),本 prop 仅作透传兼容保留,不参与任何显隐判定 |
| showActions |
boolean |
true |
显示复制/重生成。assistant 待回答落定(status=done)才渲染——生成中(pending/streaming)不出复制/重生成/评价等工具按钮;error 无正文也不出;user 仅有文案(content 非空)时才渲染复制按钮——纯附件无文字的消息复制的是一段空串,按钮无意义不出;分支切换器不受此门控 |
| copyToClipboard |
boolean |
true |
复制按钮内置剪贴板写入:点击即写 message.content 并给 is-copied 成功反馈。false 时不写剪贴板、只上抛 copy 事件——宿主在事件里自行接管写入(如渲染后纯文本/带元信息),避免与内置写入互相覆盖 |
| showBranchSwitch |
boolean |
true |
显示分支切换器(branch.count > 1 时) |
| showUserAvatar |
boolean |
false |
显示用户头像(默认隐藏;false 时用户消息不渲染头像块) |
| showUserName |
boolean |
false |
显示用户名称(默认隐藏;false 时用户消息不渲染头部名称行) |
| showAssistantAvatar |
boolean |
true |
显示助手头像(false 时助手消息不渲染头像块) |
| showAssistantName |
boolean |
false |
显示助手名称(默认隐藏;false 时助手消息不渲染头部名称行) |
| userAvatar |
string |
'' |
用户头像图片 URL(空串回落内置图标) |
| assistantAvatar |
string |
'' |
助手头像图片 URL(传入完全覆盖不分状态;空串按状态回落内置 GIF:thinking=fvai-thinking.gif,done/error=fvai-idle.gif) |
| assistantAvatarScale |
number |
1 |
助手头像缩放系数(默认 1 不缩放;CSS 变量 --fv-msg-avatar-scale 同效,prop 优先) |
| avatarCircle |
boolean |
true |
头像圆形模式(透传 AiMessage;默认 true:圆角 50%;--fv-msg-avatar-radius 变量优先于开关) |
| userName |
string |
'' |
用户名称(空串回落「你」) |
| assistantName |
string |
'' |
助手名称(空串回落「Gizmo」) |
| autoCollapse |
boolean |
true |
正文出完自动收起思考链(status 落 done 且正文打字机不再打字时折叠;error 不收,保留链供排查;历史消息不受影响,仍默认收起) |
| typing |
boolean | { step?; interval? } |
false |
打字机(透传 Typewriter;仅 assistant 流式期间生效,落定/历史消息整段渲染) |
| collapsible |
boolean |
false |
思考链工具步长文案展开/收起开关(透传 ThoughtChain) |
| showDuration |
boolean |
false |
显示工具步用时 description(透传 ThoughtChain;默认关闭) |
| showTime |
boolean |
true |
显示消息时间(操作区按钮之后;时间源:message.createdTime(秒级/毫秒级时间戳或 ISO 字符串自动识别)优先,缺省回落 message.createdAt;两者皆无则不渲染。助手消息 SSE 生成中不显示,正文打完字(未启用打字机则落定)后才显示;error 即显;user 消息恒显示) |
| timeFormat |
(message, timestamp) => string |
undefined |
消息时间自定义格式化(透传消息与已归一为 ms 的时间戳;缺省 'YYYY-MM-DD HH:mm',本机时区) |
| codeTriggerLangs |
string[] |
— |
代码块「预览」触发语言(透传 Markdown/Typewriter;命中语言的代码块右上角注入按钮,点击行为由消费方事件委托实现) |
| followupStyle |
'list' | 'chips' |
'list' |
追问建议展示样式:'list'(默认)纵向列表一行一条,渲染在操作区(工具栏)下方,顶部浅灰分隔线 + 「追问」标题(不加粗,与正文同色,字号随正文,经语言包 aiMessage.followupsTitle 自定义),条目 100% 宽、下划线分隔(浅灰,末条不出线)、字号随正文、文案次级灰;'chips' 旧横向气泡形态(换行铺排,无标题)。assistant 消息带 suggestions 即渲染——历史消息同样显示,不再限链尾;正文打字效果完成后才渲染(流式 suggestions 事件到达不显示,落定+打完字整体浮现) |
| resolveFileUrl |
(file: ChatFile) => Promise<string> | string |
undefined |
附件地址解析钩子:渲染附件 url 前调用,返回可直渲地址(如 JWT fetch 换 blob objectURL);缺省原样使用 url(blob/dataURL/公开地址),远端地址解析结果按 url 缓存 + 在途去重,失败回落原 url |
| feedbackHandler |
(message, action: 'LIKE' | 'DISLIKE' | 'CLEAR', comment?) => Promise<unknown> | unknown |
undefined |
消息评价提交钩子:仅 assistant 最终回答行(role=assistant 且 status=done,error/中断/生成中不渲染)渲染 👍/👎,缺省不渲染按钮。点击 👍 直接提交,点击 👎 弹居中附言弹窗(ElDialog,textarea ≤500 字 + 提交/跳过,点遮罩/ESC 关闭草稿保留、再点 👎 复填),再次点击当前态按钮发 CLEAR;乐观更新——先翻转 message.feedback 再调钩子,失败回滚并经 feedback-error 上抛,成功经 feedback-change 上抛;返回 resolved 即成功,返回错误文案串/抛错均视为失败。弹窗标题/占位可自定义(语言包 aiMessage.feedback.dislikeTitle/dislikePlaceholder;标题 {name} 占位渲染时替换为 assistantName,空串回落 defaultAssistantName) |
| locale |
LocaleInput(string | 对象) |
undefined |
语言包:'zh-CN'/'en'/'ko'/'vi'/'zh-HK' 整包或部分文案对象(优先于祖先注入与全局设置) |
用户消息操作区:恒渲染复制按钮,默认透明,鼠标移上消息(或键盘聚焦)时浮现(open-webui 交互)。
| Event |
说明 |
| copy / regenerate / branch-change |
(message) / (message) / (dir, message);copy 内置行为:先发起剪贴板写入(message.content)、成功后再上抛 copy 事件,宿主处理器抛错不阻断写入;成功后按钮图标短暂换 √+成功色(1.5s 还原,写入失败/无 clipboard 静默);copyToClipboard=false 关闭内置写入(只上抛事件,宿主接管) |
| followup-select |
(item, message) 点击追问建议(item 为建议文本;assistant 消息带 suggestions 即渲染——历史消息同样显示,不限链尾;正文打字效果完成后才渲染,落定后可点;AiChat 内部已接住转 send,直接用 AiMessage 时的消费方需自行接管发送) |
| file-click |
(file, message) 点击附件:图片缩略图默认打开内置 ElImageViewer 放大(同消息多图轮播,url 经 resolveFileUrl 解析);文档附件渲染为 FilesCard 卡片(类型图标 + 名称/大小,复用输入区同款),卡片右上角 28px 下载按钮(hover 浮现)——点击内置触发浏览器下载(url 经 resolveFileUrl 解析),不触发本事件;点击卡片本体仅上抛本事件(无内置下载行为,宿主可埋点/接管) |
| feedback-change |
(message, action: 'LIKE' | 'DISLIKE' | 'CLEAR') 评价提交成功(乐观落定后触发;message.feedback 已更新,宿主可埋点/联动) |
| feedback-error |
(message: string) 评价提交失败(钩子 reject/返回错误文案;乐观更新已回滚,message 为错误文案) |
| typing |
打字机输出中(流式期间节奏性触发) |
| Slot |
说明 |
| message-avatar |
自定义头像(作用域 { message };优先于细分头像插槽,两条角色消息均生效) |
| user-avatar |
自定义用户头像(无作用域;优先于 userAvatar props) |
| assistant-avatar |
自定义助手头像(作用域 { status, message },status 为 thinking/done/error,可按状态换头像;优先于 assistantAvatar props;未传插槽且未传 assistantAvatar 时内置头像本身随状态切换) |
| user-name |
自定义用户名称(作用域 { message };优先于 userName props) |
| assistant-name |
自定义助手名称(作用域 { message };优先于 assistantName props) |
| message-files |
自定义附件渲染(作用域 { message, files };默认:图片缩略图按钮(点击内置放大预览),文档 FilesCard 卡片(类型图标 + 名称/大小,复用输入区同款,卡片右上 28px 下载按钮 hover 浮现可下载);user 消息只传附件无文字时不渲染空正文气泡) |
| message-actions |
自定义操作区(作用域 { message }) |
| Ref |
说明 |
| message |
当前消息节点(computed) |
| expandThoughtChain() / collapseThoughtChain() |
展开/折叠本条消息的思考链(user 消息、无思考链、单 item 平铺形态调用无效) |
AiArtifacts
Artifacts 预览面板(iframe 沙箱,从消息 html / svg 代码块提取)。
| Prop |
类型 |
默认 |
说明 |
| artifacts |
AiArtifactItem[] |
[] |
artifact 列表 { key(必填), type: 'html'/'svg', content, title? } |
| activeKey |
string |
'' |
当前展示的 key;为空时面板隐藏 |
| locale |
LocaleInput(string | 对象) |
undefined |
语言包:'zh-CN'/'en'/'ko'/'vi'/'zh-HK' 整包或部分文案对象(优先于祖先注入与全局设置) |
| Event |
说明 |
| close / change |
关闭 / 切换(key) |
| navigate |
(href) iframe 内点击外链被拦截上抛 |
| Ref |
说明 |
| open(key) / close() |
程序化显隐 |
AiSessionSidebar
可收起的会话侧栏(FvAiSessionSidebar):品牌行 + 新对话 + 未归档/已归档 tab + 搜索 + 会话列表。新建仅切草稿态(DRAFT_SESSION_ID 哨兵,不发 service.create);列表触底分页加载;菜单内置重命名/删除(确认弹窗)与归档/恢复(service.archive 未实现时自动隐藏)。通常与 FvAiChatPanel 并排使用,由宿主编排(选中会话 → 传 Panel 的 sessionId;session-created/round-complete → 调 refresh 刷列表)。
<script setup lang="ts">
import { ref } from 'vue'
import { FvAiSessionSidebar, DRAFT_SESSION_ID } from '@fv-ui/ai-chat'
import type { FvAiSessionService } from '@fv-ui/ai-chat'
const service: FvAiSessionService = { /* 宿主胶水,见下节 */ } as FvAiSessionService
const activeId = ref(DRAFT_SESSION_ID)
const collapsed = ref(false)
const sidebarRef = ref()
</script>
<template>
<FvAiSessionSidebar
ref="sidebarRef"
:service="service"
:active-id="activeId"
v-model:collapsed="collapsed"
@select="(id) => (activeId = id)"
@new-chat="() => (activeId = DRAFT_SESSION_ID)"
@removed="(id) => id === activeId && (activeId = DRAFT_SESSION_ID)"
/>
</template>
| Prop |
类型 |
默认 |
说明 |
| service |
FvAiSessionService |
(必填) |
会话数据服务(宿主注入;archive 可选,未实现时隐藏归档 tab 与菜单项) |
| activeId |
string |
(必填) |
当前会话 id(草稿哨兵 DRAFT_SESSION_ID 或真实 id) |
| v-model:collapsed |
boolean |
false |
收起态(默认展开);收起态窄条竖排「展开 + 新建 + 搜索」三个图标按钮 |
| pageSize |
number |
20 |
列表分页大小 |
| searchable |
boolean |
true |
显示搜索框(后端标题模糊匹配,输入防抖 400ms,清空即恢复) |
| brandName |
string |
'Gizmo' |
品牌名称 |
| brandIcon |
string |
内置品牌头像 |
品牌徽章图片 URL(默认内置 fvai-avatar.svg) |
| locale |
LocaleInput(string | 对象) |
undefined |
语言包:'zh-CN'/'en-US' 整包或部分文案对象(优先于祖先注入与全局设置) |
| Event |
说明 |
| update:collapsed |
收起/展开同步(v-model:collapsed) |
| select |
(sessionId, status?: 'ACTIVE' | 'ARCHIVED') 选中会话切换;新建时载荷为 DRAFT_SESSION_ID(status 供父层记录会话状态) |
| new-chat |
点了「新对话」(无论此前是否已在草稿态;父层用于草稿态重播欢迎动画) |
| removed |
(sessionId) 删除了当前会话(父层需切走) |
| Slot |
说明 |
| brand |
覆盖品牌行整行(徽章 + 名称 + 折叠按钮) |
| new-chat |
覆盖「新对话」按钮内容(按钮元素保留,点击行为不变) |
| menu |
覆盖会话行 hover 菜单项(作用域 { item };默认重命名/归档/删除) |
| item |
覆盖会话行内容(作用域 { item, active }) |
| Ref |
说明 |
| refresh() |
静默刷新列表(不打 loading 遮罩;供父层在懒创建落库/首轮回复完成后调,刷新标题/活跃时间) |
| reload() |
重载首页列表(与内部首载同路径) |
AiChatPanel
对话面板(FvAiChatPanel,无头设计:会话状态/标题由侧栏表达,面板只承担对话):内嵌 FvAiChat + 会话历史加载 + 草稿懒创建。「加载更早」按钮悬浮消息区顶部(仅已拉详情且贴顶时出现),按 seq 游标向前翻页合并。
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { FvAiChatPanel, FvCommonAdapter, DRAFT_SESSION_ID } from '@fv-ui/ai-chat'
import type { FvAiSessionService } from '@fv-ui/ai-chat'
const service: FvAiSessionService = { /* 宿主胶水,见下节 */ } as FvAiSessionService
const adapter = FvCommonAdapter({ baseURL: '/api', sessionId: () => sessionId.value })
const sessionId = ref<string>(DRAFT_SESSION_ID)
const panelRef = ref()
</script>
<template>
<FvAiChatPanel
ref="panelRef"
:service="service"
:adapter="adapter"
:session-id="sessionId"
style="height: 600px"
@session-created="(id) => (sessionId = id)"
@error="(err) => ElMessage.error(err.message)"
/>
</template>
Props 按用途分四组:
数据(会话/聊天接入):
| Prop |
类型 |
默认 |
说明 |
| service |
FvAiSessionService |
(必填) |
会话数据服务(list/rename/remove 面板不消费,接口完整传入) |
| adapter |
ChatAdapter |
(必填) |
聊天适配器(草稿懒创建由面板包装层承担) |
| sessionId |
string |
(必填) |
当前会话 id(草稿哨兵或真实 id) |
| detailLimit |
number |
100 |
首屏 detail 条数 |
| historyLimit |
number |
50 |
「加载更早」每次拉取条数 |
展示(透传 FvAiChat):
| Prop |
类型 |
默认 |
说明 |
| welcomeLayout |
'horizontal' | 'vertical' |
'horizontal' |
空态欢迎页布局(悬浮窗等窄宽场景用 vertical) |
| centerWelcome |
boolean |
true |
空态欢迎内容垂直居中(悬浮窗等窄高场景传 false:欢迎页贴上、输入区贴底,透传 FvAiChat centerWelcome) |
| placeholder |
string |
'' |
输入框占位 |
| suggestions |
AiChatSuggestion[] |
[] |
空态建议列表(建议池属宿主业务,库内不做抽样) |
| suggestionsTitle |
string |
'' |
空态建议标题 |
| suggestionsWrap |
boolean |
true |
空态建议换行模式 |
| showRefreshSuggestions |
boolean |
false |
空态建议区底部「换一批」按钮(透传 FvAiChat;组件只上抛 @refresh-suggestions 事件) |
| refreshSuggestionsText |
string |
'' |
「换一批」按钮文案(缺省回落 locale) |
| typing |
boolean | { step?; interval? } |
true |
打字机 |
| collapsible |
boolean |
true |
思考链长文案展开/收起开关 |
| showTime |
boolean |
true |
显示消息时间 |
| theme |
'light' | 'dark' | 'auto' |
'light' |
主题 |
| locale |
LocaleInput |
undefined |
语言配置(透传 FvAiChat + 面板自身文案) |
| toolStepLabel |
(toolName: string) => string |
undefined |
工具名 → 步骤展示名 |
功能(附件/评价钩子,透传 FvAiChat;草稿态懒创建由面板包装层承担):
| Prop |
类型 |
默认 |
说明 |
| allowAttachments |
boolean |
false |
开启输入区附件上传 |
| accept |
string |
'' |
附件 input accept 属性 |
| attachmentLimit |
number |
0 |
附件数量上限 |
| uploadAttachment |
(file, item) => ... |
undefined |
附件上传钩子(语义同 AiChat) |
| resolveFileUrl |
(file: ChatFile) => Promise<string> | string |
undefined |
附件地址解析钩子 |
| feedbackHandler |
FeedbackHandler |
undefined |
消息评价提交钩子 |
布局(限宽,CSS 变量注入):
| Prop |
类型 |
默认 |
说明 |
| messagesMaxWidth |
string |
'1160px' |
消息区限宽 |
| senderMaxWidth |
string |
'1200px' |
输入区限宽(非草稿态) |
| draftSenderMaxWidth |
string |
'780px' |
草稿态输入区限宽(聚焦欢迎页视觉中心) |
| Event |
说明 |
| session-created |
(sessionId) 草稿首条消息懒创建成功(父层换真实 id;本组件守卫跳过重载) |
| round-complete |
一轮回复完成(父层延时刷新会话列表标题/活跃时间) |
| error |
(err) 生成/懒创建失败(错误上抛,toast 留给宿主) |
| message-complete / attachment-exceed / attachment-uploaded / message-file-click / feedback-change / feedback-error |
FvAiChat 事件透传(载荷语义同 AiChat) |
| refresh-suggestions |
() 空态建议区「换一批」点击(透传 FvAiChat;消费方监听后自行更换建议数据) |
| Slot |
说明 |
| empty / welcome-name / welcome-description / welcome-icon / welcome-suggestions |
FvAiChat 空态插槽透传 |
| refresh-suggestions / suggestions-footer |
欢迎页「换一批」按钮替换 / 建议区底部追加内容(透传 FvAiChat) |
| user-avatar / assistant-avatar / user-name / assistant-name / message-avatar / message-actions |
FvAiChat 消息插槽透传(作用域同 AiChat) |
| sender-prefix |
输入区前缀(转发 FvAiChat #prefix) |
| Ref |
说明 |
| replayWelcome() |
重播欢迎页(草稿态下重复点「新对话」sessionId 不变,父层经此方法让整框重建、动画从头跑一遍;非草稿态 no-op) |
| loadEarlier() |
手动触发「加载更早」(与按钮同路径;无更早消息/草稿态/加载中 no-op) |
| refresh() |
直通刷新:对当前会话重新拉 detail 并重建视图(草稿态 no-op) |
AiChatWidget
悬浮对话面板(FvAiChatWidget):Teleport 到 body,双形态——drawer 抽屉(默认:贴 position 侧全屏高,offsetTop/offsetBottom 各自留白)/ floating 悬窗(独立窗口,高度默认视口高 - 上下留白 - 80,可拖 header 自由移动,整窗限视口内不出界);宽度可经拖拽手柄调整(minWidthmaxWidth,默认 450640px;抽屉仅内缘一个手柄,悬窗左右两缘均可调,拖左缘时窗口同步平移、右缘不动),拖拽落定经 localStorage 记忆(storageKey 自定义)。头部左侧「头像 + AI 名称」(默认内置品牌头像),右侧图标按钮均带 tooltip:「历史会话 + 新对话 + 模式切换 + 最大化 + 关闭」(模式切换按钮默认隐藏,showModeToggle 开启;隐藏时仍可拖 header 超 100px 转悬窗、悬窗贴边吸附回抽屉);点历史会话图标在对话区覆盖式下拉(层挂在 body 内,header 露出不遮挡;顶部「返回」工具条收层,内嵌 FvConversations:「今天/以往」分组 + hover 菜单重命名/删除 + 触底分页,打开即拉取,点行切会话);主体内嵌 FvAiChatPanel(常规态 welcomeLayout 固定 vertical、centerWelcome 固定 false 窄宽形态:欢迎页贴上、输入区贴底,建议列表为 FAQ 式行列表——行间细分隔线、行尾箭头指示、hover 底色对称淡入淡出)。最大化(maximizable,默认 true;关闭按钮左侧按钮,亦经 toggleMaximize() 编程式切换):inset 0 占满视口的覆盖态,退出后回到进入前的 drawer/floating 原形态;最大化态头部收敛(头像/标题/历史/新对话/模式切换隐藏,标识由侧栏品牌行承担,仅留还原/关闭;关闭面板即退出最大化,再开回常规形态),FvAiSessionSidebar 升为根级全高覆盖(选中/新建/删除/搜索/归档全能力,service/locale/品牌透传;默认收起为 44px 悬浮窄条、可展开 260px,对话区经 padding-left 让位/收回),Panel 欢迎页转横排 + 垂直居中,限宽三参回库默认 1160/1200/780px 聚焦中栏,建议列表转大屏胶囊横排;拖拽/调宽/头部按钮随态禁用。面板底带主色角部微光渐变(科技感,亮暗两态;暗色下透明度升档 + 边框调亮 + 投影加深,面板与同色系页面背景可分层)。会话编排收在内部:草稿哨兵起步、session-created 换真实 id 并刷历史列表、round-complete 延时 2.5s 刷列表。关闭保活(keepAlive,默认 true):关闭仅隐藏(v-show),实例/对话树原样保留,再开不重渲染、不重拉历史。
<script setup lang="ts">
import { ref } from 'vue'
import { FvAiChatWidget, FvAiChatLauncher, FvCommonAdapter } from '@fv-ui/ai-chat'
import type { FvAiSessionService } from '@fv-ui/ai-chat'
const service: FvAiSessionService = { /* 宿主胶水,见下节 */ } as FvAiSessionService
const adapter = FvCommonAdapter({ baseURL: '/api' })
const open = ref(false) // 两个 v-model 接同一 ref
</script>
<template>
<FvAiChatLauncher v-model="open" position="right" />
<FvAiChatWidget
v-model="open"
:service="service"
:adapter="adapter"
title="AI 助手"
position="right"
/>
</template>
| Prop |
类型 |
默认 |
说明 |
| modelValue |
boolean(v-model) |
false |
显隐 |
| width |
string |
'450px' |
面板宽度(css 值;拖拽调宽在其上生效) |
| minWidth / maxWidth |
number |
450 / 640 |
拖拽调宽下/上界(px) |
| position |
'right' | 'left' |
'right' |
贴靠侧(滑入方向随侧;抽屉拖拽手柄在内侧对缘,悬窗两缘均可调宽) |
| offsetTop |
number |
0 |
面板距屏幕上边缘留白(px) |
| offsetBottom |
number |
0 |
面板距屏幕下边缘留白(px) |
| mode |
'drawer' | 'floating' |
undefined(挂载按位置记忆还原上次形态) |
面板形态:抽屉(贴侧全屏高,拖 header 超 100px 转悬窗并就地接管)/ 悬窗(独立定高,可拖 header 移动;拖到左/右视口缘持续 0.3s 出抽屉吸附预览,松开即吸附为对应侧抽屉并同步内部贴靠侧)。显式传值受控优先,记忆只补细节(贴靠侧/坐标)不改形态 |
| floatingHeight |
string |
''(视口高 - 上下留白 - 80) |
悬窗模式高度(css 值;drawer 模式忽略) |
| showModeToggle |
boolean |
false |
显示头部「抽屉/悬窗」模式切换按钮;隐藏时仍可拖 header 超 100px 转悬窗、悬窗贴边吸附回抽屉 |
| storageKey |
string |
'fv-ai-chat-widget' |
宽度/位置记忆 storage key:以 ! 结尾视为完整 key,否则作前缀拼 -width/-position;空串禁用记忆 |
| rememberPosition |
boolean |
true |
悬窗位置记忆总开关:开启时悬窗拖动/调宽落定持久化 left/top、挂载进悬窗时回灌(越界夹回视口内);关闭时位置读写全跳过(宽度记忆不受影响,仍只看 storageKey) |
| keepAlive |
boolean |
true |
关闭保活:关闭仅隐藏(v-show),实例/对话树原样保留,再开不重渲染、不重拉历史;设 false 则关闭即销毁(v-if),每次打开重建 |
| maximizable |
boolean |
true |
支持最大化:头部显示最大化按钮(关闭按钮左侧);最大化占满视口、头部头像/标题/历史/新对话/模式按钮收敛(标识由侧栏品牌行承担),FvAiSessionSidebar 升为根级全高覆盖(盖住 header 左列,logo 不重复),欢迎页转大屏横排 |
| title |
string |
'' |
头部标题(空时回落 locale AI 名称) |
| avatar |
string |
内置品牌头像 |
头部头像图片 URL |
| service |
FvAiSessionService |
(必填) |
会话数据服务(透传 Panel 与 Sidebar) |
| adapter |
ChatAdapter |
(必填) |
聊天适配器(透传 Panel) |
| sessionId |
string |
'' |
当前会话 id(缺省内部 DRAFT 编排;传入则受控优先) |
| placeholder / suggestions / suggestionsTitle / suggestionsWrap / showRefreshSuggestions / refreshSuggestionsText / typing / collapsible / showTime / theme / locale / toolStepLabel |
|
|
FvAiChatPanel 同名 props 透传 |
| allowAttachments / accept / attachmentLimit / uploadAttachment / resolveFileUrl / feedbackHandler |
|
|
FvAiChatPanel 附件/评价钩子透传 |
| detailLimit / historyLimit |
number |
100 / 50 |
首屏 detail / 「加载更早」每次条数(透传 Panel) |
注:Panel 的 welcomeLayout 在 Widget 内常规态固定为 vertical、centerWelcome 固定为 false(欢迎页贴上、输入区贴底),限宽三参(messagesMaxWidth 等)固定为 'none',均不透传;最大化态自动切为 horizontal + 居中 + 限宽回库默认;消息头像经 --fv-msg-avatar-size 固定 38px(库默认 50px)。
| CSS 变量 |
默认 |
说明 |
| --fv-ai-widget-z-index |
2000 |
面板层叠层级(EP 弹层量级);吸附预览恒为其 -1。宿主固定布局件(topbar/侧栏/tagsView 等)同量级或更高时,在 :root 覆写整体抬层,无需逐条覆写组件样式 |
| Event |
说明 |
| update:modelValue |
显隐同步 |
| open / close |
面板打开 / 关闭(受控 modelValue 由 false→true / true→false 时上抛) |
| session-change |
(sessionId) 内部会话切换(选中会话/新对话/删除切草稿/回退上一次对话;宿主可选监听对账) |
| session-created / round-complete / error / message-complete / attachment-exceed / attachment-uploaded / message-file-click / feedback-change / feedback-error |
FvAiChatPanel 事件透传(载荷语义同 AiChatPanel) |
| refresh-suggestions |
() 空态建议区「换一批」点击(透传 Panel;消费方监听后自行更换建议数据) |
| Slot |
说明 |
| header-avatar / header-title / header-actions |
Widget 头部:替换头像 / 替换标题 / 在标题与默认按钮之间追加自定义按钮(交互可配合 newChat()/switchToPrevious()) |
| empty / welcome-name / welcome-description / welcome-icon / welcome-suggestions |
原样转发 FvAiChatPanel 空态插槽 |
| refresh-suggestions / suggestions-footer |
欢迎页「换一批」按钮替换 / 建议区底部追加内容(原样转发 FvAiChatPanel) |
| user-avatar / assistant-avatar / user-name / assistant-name / message-avatar / message-actions / sender-prefix |
原样转发 FvAiChatPanel 消息/输入区插槽(作用域同 AiChatPanel) |
| Ref | 说明 |
| --- | --- | --- |
| open() / close() | 编程式开合(与 v-model 等价) |
| toggleMode() | 编程式切换 drawer/floating 形态(与头部模式按钮等价) |
| toggleMaximize() | 编程式切换最大化(与头部最大化按钮等价:最大化 ⇄ 还原为进入前形态) |
| newChat() | 新建对话(与头部「新对话」按钮等价:切草稿;已在草稿则重播欢迎页) |
| switchToPrevious() | 切回上一次对话(来路栈回退,浏览器后退式;栈空回落历史列表首条真实会话);返回 boolean 表示是否切换成功 |
| refresh() | 刷新当前会话(透传 Panel.refresh:重新拉 detail 并重建视图;草稿态 no-op) |
AiChatLauncher
悬浮入口按钮(FvAiChatLauncher):blob 光晕(内嵌 FvBlobHalo 基础组件:主题色 + AI 紫 + 浅紫 + 白色四色色团,d: path() 变形 + 异速旋转,全部居中叠放混色;光晕约 1.6 倍按钮径。暗色下紫系压暗、白层变黑层;无白色图标底,头像直接铺满圆心)。与 FvAiChatWidget 组合时宿主把两个 v-model 接到同一 ref;亦可独立控任意目标。双形态:floating(默认)贴角悬浮;floating: false 文档流内联,可嵌进顶栏/卡片/按钮组;interactive: false 渲染为非交互 <span>(光晕照常动),可作纯展示图标引用。中间图标优先级:#icon 插槽 > iconComponent 组件 > icon 图片 URL > 内置品牌头像(内联 fvai-avatar.svg 矢量图,支持眼神跟随:eyeTrack(默认 true)下指针进入约 6 倍按钮径范围时眼睛朝指针方向偏移,离开回正,reduced-motion 降级;自定义 icon/插槽替换后自动失效);halo 对象整体透传内嵌 FvBlobHalo(色板/缩放/透明度/转速/暂停)。
<FvAiChatLauncher v-model="open" placement="bottom-right" :size="48" />
<!-- 内联非交互小图标(顶栏引用) -->
<FvAiChatLauncher :interactive="false" :floating="false" :size="32" />
<!-- 光晕调参 -->
<FvAiChatLauncher v-model="open" :halo="{ colors: ['#ff8c42', '#fcaf58', '#f9c784', '#fff'], rotateDuration: 15 }" />
| Prop |
类型 |
默认 |
说明 |
| modelValue |
boolean(v-model) |
false |
按钮激活态(面板开时按钮高亮,仅视觉;点击 emit click + update:modelValue 取反;interactive=false 时不渲染激活态) |
| floating |
boolean |
true |
是否悬浮贴角;false 时文档流内联(placement/offset 失效),可嵌进顶栏/卡片/按钮组 |
| interactive |
boolean |
true |
是否可交互;false 时渲染 <span>,无点击/hover/aria,光晕照常动,作纯展示图标引用 |
| placement |
'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' |
'bottom-right' |
悬浮四角定位(floating 时生效) |
| position |
'right' | 'left' |
|
废弃(兼容保留):未传 placement 时按 bottom-{position} 推导;与 placement 同传以 placement 为准(dev 警告) |
| offsetX / offsetY |
number |
回落 offset |
距角横/纵轴留白(px,分轴设置) |
| offset |
number |
24 |
距角双轴统一留白(px 简写;offsetX/offsetY 优先) |
| draggable |
boolean |
true |
悬浮态可拖动改位(仅 floating+interactive 时生效):pointer 手势拖动,全程钳制视口内不出窗;落定后内联 left/top 接管 placement 贴角,位移不足 4px 仍视为点击;落定位置经 storageKey 持久化、下次挂载回灌(见 storageKey/rememberPosition) |
| storageKey |
string |
'fv-ai-launcher' |
拖动位置记忆 storage key:以 ! 结尾视为完整 key,否则作前缀拼 -position;空串禁用记忆 |
| rememberPosition |
boolean |
true |
拖动位置记忆总开关:开启时拖动落定持久化、挂载回灌(越界夹回视口内);关闭时位置读写全跳过 |
| size |
number |
42 |
按钮直径(px) |
| zIndex |
number |
2000 |
悬浮形态层叠层级(仅 floating 生效;经 CSS 变量 --fv-launcher-z-index 下发):宿主固定布局件(topbar/侧栏/tagsView 等)同量级或更高时抬层,避免被盖住 |
| ariaLabel |
string |
'' |
无障碍标签(空时回落 locale aiChatPanel.launcher;interactive 时生效) |
| icon |
string |
'' |
默认图标图片 URL(缺省内置品牌头像矢量图;iconComponent/#icon 插槽优先) |
| iconComponent |
Component |
|
组件形态图标(与 icon 冲突时优先;#icon 插槽仍最高) |
| eyeTrack |
boolean |
true |
眼神跟随鼠标(仅内置头像形态生效,自定义 icon/插槽替换后无效果):指针进入约 6 倍按钮径范围时眼睛朝指针方向偏移,离开回正;prefers-reduced-motion 降级为不跟随 |
| halo |
FvAiChatLauncherHalo |
|
光晕属性整体透传内嵌 FvBlobHalo:{ colors?, scale?, opacity?, altOpacity?, rotateDuration?, paused? };覆盖 Launcher 内置默认(scale 1.6 / opacity .1 / altOpacity .35) |
| locale |
LocaleInput |
undefined |
语言包 |
| Event |
说明 |
| update:modelValue |
点击后激活态取反 |
| click |
(MouseEvent) 原生点击 |
| drag-end |
({ left, top }) 拖动落定(钳制后的视口坐标;仅 draggable 生效的悬浮态;默认同时持久化位置记忆) |
| Slot |
说明 |
| default |
整体替换按钮内容(光晕层恒在,内容铺在光晕之上;存在时 #icon 不渲染) |
| icon |
覆盖按钮图标(默认内置品牌头像矢量图,圆形裁切铺满) |
FvAiSessionService(会话数据服务)
会话数据服务接口:宿主实现后经 prop 注入 FvAiSessionSidebar / FvAiChatPanel / FvAiChatWidget。库内不内置网络实现;错误提示(toast)由宿主胶水层统一承担(库对 service 失败:侧栏静默、面板空态重建)。
import { DRAFT_SESSION_ID } from '@fv-ui/ai-chat'
import type { FvAiSessionService, FvAiSessionPage, FvAiMessagePage } from '@fv-ui/ai-chat'
/** 草稿会话哨兵:进入模块/新建/删除当前会话即切草稿态,不调 service.create;
* 首条消息发出时由 FvAiChatPanel 懒创建 service.create 后上抛 session-created 换真实 id */
DRAFT_SESSION_ID // '__draft__'
interface FvAiSessionService {
/** 会话列表(分页,updatedTime DESC;status 筛选与 keyword 搜索正交同传) */
list(q: { pageNum: number; pageSize: number; status?: 'ACTIVE' | 'ARCHIVED'; keyword?: string }): Promise<FvAiSessionPage>
/** 新建会话,返回新会话 id */
create(): Promise<string>
/** 会话详情消息(尾部 limit 条,seq 升序) */
detail(sessionId: string, limit?: number): Promise<FvCommonMessage[]>
/** 加载更早消息(beforeSeq 游标) */
history(sessionId: string, beforeSeq: number, limit?: number): Promise<FvAiMessagePage>
/** 重命名 */
rename(args: { sessionId: string; title: string }): Promise<void>
/** 删除 */
remove(sessionId: string): Promise<void>
/** 归档/恢复(可选;未实现时侧栏隐藏归档 tab 与菜单项) */
archive?(args: { sessionId: string; archived: boolean }): Promise<void>
}
// 归一化形状(宿主 IPage 格式由胶水转换)
interface FvAiSessionPage { items: FvCommonSessionItem[]; hasMore: boolean }
interface FvAiMessagePage { messages: FvCommonMessage[]; hasMore: boolean }
宿主胶水示例(det-oms 风格:list/create/detail/history/rename/remove 转发到宿主 httpRequest,并把后端 IPage 归一化为 { items, hasMore };错误 toast 由胶水层承担):
import { ElMessage } from 'element-plus'
import type { FvAiSessionService } from '@fv-ui/ai-chat'
/** 宿主统一请求封装(JSON 返回;非 2xx/业务错误码直接 reject) */
declare function httpRequest<T>(url: string, data?: unknown): Promise<T>
export const sessionService: FvAiSessionService = {
async list(q) {
const page = await httpRequest('/ai-chat/session/list', q) // IPage<T>
return { items: page.records, hasMore: page.current * page.size < page.total }
},
create: () => httpRequest<{ id: string }>('/ai-chat/session/create').then((r) => r.id),
detail: (sessionId, limit = 100) =>
httpRequest('/ai-chat/session/detail', { sessionId, limit }),
history: async (sessionId, beforeSeq, limit = 50) => {
const page = await httpRequest('/ai-chat/session/history', { sessionId, beforeSeq, limit })
return { messages: page.records, hasMore: page.records.length >= limit }
},
rename: (args) => httpRequest('/ai-chat/session/rename', args),
remove: (sessionId) => httpRequest('/ai-chat/session/remove', { sessionId })
}
// 错误 toast:胶水层统一承担(库对 service 失败静默)
// httpRequest 内部 catch → ElMessage.error(后端 message 文案)后 reject
mapHistoryToNodes / sessionToConversationItem(历史回放映射)
src/core/historyMapper.ts 导出的两个纯映射函数(Fv 系后端消息行 → MessageNode 树 / FvConversations 项),独立使用时的签名:
import { mapHistoryToNodes, sessionToConversationItem, type AiChatT } from '@fv-ui/ai-chat'
/** 后端消息行(seq 升序)→ MessageNode 树(历史回放状态机):
* user 行 → user 节点;assistant 中间轮 + tool 结果行按 toolCallId 配对
* 累积进本轮最终 assistant 的 toolSteps;GENERATING → 「回复生成中断」灰条;
* FAILED/INTERRUPTED → error 态;附件/评价/追问建议一并还原 */
function mapHistoryToNodes(messages: FvCommonMessage[], t?: AiChatT): MessageNode[]
/** 会话列表项 → FvConversations item(key=id;group 仅作归档兜底分组,
* 展示分组由消费方按「今天/以往」覆写) */
function sessionToConversationItem(s: FvCommonSessionItem, t?: AiChatT): {
key: string; label: string; group?: string; disabled?: boolean
}
t 参数为翻译函数((key: string, named?: Record<string, string>) => string,vue-i18n t 的结构子类型):文案 key 为库 locale historyMapper 分组的中文原文(如「回复生成中断」「用时 {s}」);缺省回落库内 locale(传入组件 locale 即随语言切换,无需自己拼 t)。带 named 占位参数时做 {name} 简单插值,未知占位符原样保留。
FvAiChatPanel 内部已按上述口径注入 t 后调用;仅独立使用该函数时需要关心。
人工验证点(文档站)
- 历史会话下拉覆盖层(Widget 头部时钟图标:仅遮对话区不遮 header、返回按钮收层、分组列表、打开即拉取、点行切会话)
- 炫光动画帧率(Launcher conic 旋转 + 光晕 breathing,低端机不掉帧)
- 窄宽欢迎页布局(Widget 内 welcomeLayout=vertical,输入区/建议条不溢出)
- 暗色主题下面板与按钮观感(theme=dark 下 Widget/Launcher 颜色对比)
Composables
useChat(options)
聊天编排:发送 → 流式回填 → 队列/重生成/续写。
import { useChat, zhCN, en, ko, vi, zhHK } from '@fv-ui/ai-chat' // 内置语言包(可换成自建包)
const lang = ref<'zh-CN' | 'en' | 'ko' | 'vi' | 'zh-HK'>('zh-CN')
const LOCALES = { 'zh-CN': zhCN, en, ko, vi, 'zh-HK': zhHK }
const chat = useChat({
adapter: () => OpenAIAdapter({ baseURL, apiKey }), // 实例或工厂;工厂每次 send 惰性取用
models: () => selectedModels.value, // 数组或工厂;多模型并行生成兄弟分支
initial: { messages, currentId }, // 恢复会话
onToolEvent: (ev) => { /* Fv 系通用 tool{status},展示「正在查询库存…」*/ },
// 语言包工厂:返回 LocaleMessages 包对象(内置包或自建包),非语言名字符串
locale: () => LOCALES[lang.value]
})
// 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 回调的透传挂点(如 FvCommonAdapter 的 tool{status} 事件)
locale:语言包工厂 () => LocaleMessages,惰性取用(与 adapter/models 工厂同语义);影响附件 system prompt、工具用时、续写 prompt 等发给后端的文案,缺省读全局设置,见国际化
FvCommonAdapter(config)
Fv 系后端通用聊天适配器(/api/ai-chat/*):六事件 SSE 协议(message_start / tool / delta / reasoning / message_end / error),POST /ai-chat/message/send。适用于 Fv 各项目组返回此结构的后端。会话管理(列表/归档/删除等)由消费方直接调后端,不经此适配器。
const adapter = FvCommonAdapter({
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 的 FvCommonError)
message_end 的 messageId(最终文本行 id)经 onBackendMessageId 回填到 MessageNode.backendId——多轮工具循环下与 message_start 的首轮 id 不同,UI 落库/对账以它为准
tool 事件经 onTool → useChat.onToolEvent → AiChat @tool-event 上抛,消费方展示「正在查询库存…」等中间态
- 追问建议(V58+):
suggestions 事件(流式收尾 0..1 次,最后一个 delta 后、message_end 前,载荷 {items: string[]})经可选 onSuggestions 上抛,useChat 回写 MessageNode.suggestions,AiMessage 对带 suggestions 的 assistant 消息渲染追问(默认纵向列表样式——顶部灰色分隔线 + 加粗「追问」标题、条目 100% 宽下划线分隔字号随正文,followupStyle='chips' 切回横向气泡;正文打字效果完成后才渲染,流式事件到达不显示,历史消息同样渲染,不再限链尾);开关关闭/模型未产出/中断/失败无此事件;历史恢复用导出的 parseSuggestions(raw) 安全解析 FvCommonMessage.suggestions(JSON 数组原文透传,脏数据按无建议)
- 消息评价(V58+):
FvCommonMessage.feedback(LIKE/DISLIKE,未评价/旧数据恒 null)、feedbackComment(附言 ≤500 字)、feedbackTime(评价落库时间)三字段同款容错;历史恢复由消费方映射为 MessageNode.feedback 灌入展示;评价提交走独立端点(POST /message/feedback),不经本适配器——接 AiChat 的 feedbackHandler 钩子即可
- 附件快照(1.4.1+):user 行
FvCommonMessage.attachments(id/originalName/type/suffix/sizeBytes/downloadUrl,旧数据/无附件恒 null),历史回放映射 MessageNode.files 渲染(图片缩略图 + 文档 FilesCard 卡片,卡片右上 28px 下载按钮 hover 浮现可下载;downloadUrl 需鉴权取流时配 resolveFileUrl 钩子,下载/预览均走解析后地址)
- 配套 DTO 类型:
FvCommonSessionItem / FvCommonMessage / FvCommonSessionDetail / FvCommonMessagePage(消费方直接调会话管理端点时用)
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