@fv-ui/ai-chat

基于 Vue3 + Element Plus 的 AI 对话组件库
6,550
Install
npm install @fv-ui/ai-chat
Latest Version:1.10.2
Vue:3
Last Updated:Oct 22, 2026
Links: npm
Maintainer: waterbeside

@fv-ui/ai-chat

version vue element-plus

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

Version History

Version Released License
1.10.2
1.10.1
1.10.0
1.9.4
1.9.2