npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

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

About

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

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

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

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

Open Software & Tools

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

© 2026 – Pkg Stats / Ryan Hefner

@fv-ui/ai-chat

v0.10.1

Published

基于 Vue3 + Element Plus 的 AI 对话组件库

Readme

@fv-ui/ai-chat

version vue element-plus

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

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

安装

pnpm add @fv-ui/ai-chat

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

快速开始

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

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

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

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

基础组件层 API

XMarkdown

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

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

| Ref | 说明 | | --- | --- | | getHtml() | 取当前渲染后的 HTML 字符串 |

Typewriter

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

| Prop | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | content | string | —(必填) | 全文(流式时增量传入,子集扩展自动续打) | | isMarkdown | boolean | false | 走 XMarkdown 渲染(此时 suffix 不追加) | | isFog | boolean | { bgColor?; width? } | false | 打字中尾部雾化遮罩 | | typing | boolean | { step?; interval?; suffix? } | false | 开启打字机;对象形态可配步长/间隔/光标后缀 |

| Event | 说明 | | --- | --- | | start / writing / finish | 开始打字 / 每次推进 / 全部打完 |

| Ref | 说明 | | --- | --- | | interrupt() / continue() / restart() / destroy() | 暂停 / 继续 / 重打 / 重置清空 | | renderedContent / isTyping / progress | 已输出文本 / 是否打字中 / 进度 0-1 |

Bubble

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

| Prop | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | content | string | '' | 气泡内容 | | placement | 'start' | 'end' | 'start' | start 靠左 / end 靠右 | | loading | boolean | false | 加载态(三点动画,可被 #loading 覆盖) | | typing | boolean | TypingConfig | false | 打字机模式(透传 Typewriter) | | isMarkdown | boolean | false | 内容按 Markdown 渲染 | | isFog | boolean | { bgColor?; width? } | false | 雾化(自定义气泡背景时传 bgColor) | | variant | 'filled' | 'borderless' | 'outlined' | 'shadow' | 'filled' | 气泡样式 | | shape | 'round' | 'corner' | 'round' | 圆角 / 方角 | | avatar | string | '' | 头像图片地址 | | avatarSize | ElAvatar size | number | '' | 头像尺寸 | | avatarGap | string | '12px' | 头像与内容间距 | | avatarShape | 'circle' | 'square' | 'circle' | 头像形状 | | avatarIcon | Component | — | 头像图标(与 avatar 冲突时优先) | | avatarSrcSet / avatarAlt | string | '' / '' | 头像 srcSet/alt | | avatarFit | 'fill' | 'contain' | 'cover' | 'none' | 'scale-down' | 'cover' | 头像填充方式(el-avatar fit) | | noPadding | boolean | false | 内容区去内边距 | | noStyle | boolean | false | 去气泡样式(透明/无框/无阴影) |

| Event | 说明 | | --- | --- | | avatar-error | 头像加载失败 | | start / writing / finish | typing 模式下的打字机事件透传 |

| Slot | 说明 | | --- | --- | | avatar / header / content / loading / footer | 自定义头像 / 头部 / 内容(优先于 content prop)/ 加载态 / 底部 |

| Ref | 说明 | | --- | --- | | interrupt / continue / restart / destroy | Typewriter 方法转发(非 typing 模式为 no-op) | | renderedContent / isTyping / progress | Typewriter 状态转发 | | twInstance | 内部 Typewriter 原始实例(BubbleList @complete 透传用) |

BubbleList

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

| Prop | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | list | BubbleListItemProps[] | [] | 列表项(Bubble props + 必填 key) | | maxHeight | string | '' | 滚动容器最大高度(如 '350px'/'60vh') | | alwaysShowScrollbar | boolean | false | 始终显示滚动条 | | showBackButton | boolean | true | 是否显示回底按钮 | | backButtonThreshold | number | 80 | 距底超过该像素显示回底按钮 | | btnLoading | boolean | false | 回底按钮加载态 | | btnColor | string | '' | 回底按钮颜色(el-button color) | | backButtonPosition | { bottom?; left? } | {} | 回底按钮位置偏移 | | btnIconSize | number | 16 | 回底按钮图标尺寸 | | triggerIndices | 'only-last' | 'all' | number[] | 'only-last' | @complete 触发索引 |

| Event | 说明 | | --- | --- | | complete | (typewriterInstance, index) 打字完成(按 triggerIndices 过滤后上抛) |

| Slot | 说明 | | --- | --- | | avatar / header / content / loading / footer | 透传给每个 Bubble,作用域 { item, index } |

| Ref | 说明 | | --- | --- | | scrollToTop() / scrollToBottom() / scrollToBubble(index) | 滚动控制 |

Sender

输入发送框。

| Prop | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | modelValue | string(v-model) | '' | 输入值(缺省内部自持) | | placeholder | string | '' | 占位文案 | | autosize | { minRows?; maxRows? } | {} | 自增高(行高 22px) | | submitType | 'enter' | 'shiftEnter' | 'cmdOrCtrlEnter' | 'altEnter' | 'enter' | 提交按键模式 | | loading | boolean | false | 加载态(发送键变停止键,输入区禁用) | | disabled | boolean | false | 禁用(阻断提交) | | readOnly | boolean | false | 只读 | | clearable | boolean | false | 显示清空按钮 | | round | boolean | true | 主按钮(发送/停止)为圆形(element-plus circle;false 时方按钮,停止键光带沿方框流转) | | shadow | boolean | false | 输入区阴影效果(浮起感,聚焦时加深) | | inputWidth | string | '' | 输入区宽度(variant=default 时有效) | | variant | 'default' | 'updown' | 'default' | default 同排 / updown 上输入下操作栏 | | submitBtnDisabled | boolean | false | 附加禁用发送按钮 | | triggerStrings | string[] | [] | 指令触发串(光标前命中弹指令框) | | triggerPopoverVisible | boolean(v-model:triggerPopoverVisible) | false | 指令弹框显隐 | | triggerPopoverWidth / triggerPopoverLeft | string | '' | 指令弹框宽度 / left 偏移 | | triggerPopoverOffset | number | 0 | 弹框距触发字符偏移量 | | triggerPopoverPlacement | ElPopover placement | 'top-start' | 弹框位置 | | allowSpeech | boolean | false | 开启语音输入:麦克风按钮 → 录音条(波形+时长,Esc 取消)→ 确认后转写文本插入光标处;浏览器 SpeechRecognition 实时转写 | | allowEmptySubmit | boolean | false | 允许空内容提交(附件场景:只发文件不输文字) |

| Event | 说明 | | --- | --- | | update:modelValue / change | 输入值变化 | | submit | (value) 提交(提交后内部清空) | | cancel | 点击停止按钮 | | trigger | (keyword, visible) 命中指令串 | | recording-change | 语音录制状态变化(预留) | | paste-file | (file) 粘贴文件 | | update:triggerPopoverVisible | 指令弹框显隐同步 |

| Slot | 说明 | | --- | --- | | header / footer | 头部/底部(配合 ref.openHeader/closeHeader) | | prefix | 输入区前缀 | | action-list | 自定义操作区(存在时隐藏内置按钮) | | action-left | 操作栏左侧附加区(与提交按钮同行靠左,如附件回形针) | | trigger | 指令弹框内容(作用域 { keyword }) |

| Ref | 说明 | | --- | --- | | submit() / cancel() / clear() | 提交 / 停止 / 清空 | | focus(pos?) / blur() | pos: 'all' 全选 | 'start' 头 | 'end' 尾(缺省 end) | | openHeader() / closeHeader() | 显隐 #header 插槽 |

MentionSender

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

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

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

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

EditorSender

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

| Prop | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | placeholder | string | '' | 占位文案 | | device | 'pc' | 'h5' | 'pc' | 设备形态(仅影响样式) | | autoFocus | boolean | false | 挂载后聚焦到末尾 | | variant | 'default' | 'updown' | 'default' | 布局形态 | | userList | EditorUser[] | [] | @ 候选用户 | | customTrigger | EditorCustomTrigger[] | [] | 自定义触发组 | | selectList | EditorSelectGroup[] | [] | 选择标签分组(openSelectDialog 弹窗数据) | | maxLength | number | 0 | 纯文本最大长度(0 不限;性能开销大,仅显式开启) | | submitType | 'enter' | 'shiftEnter' | 'enter' | 提交按键模式 | | customStyle | Record<string, any> | {} | 根节点自定义样式 | | loading | boolean | false | 加载态(发送键变停止键) | | disabled | boolean | false | 禁用 | | clearable | boolean | false | 显示清空按钮 | | headerAnimationTimer | number | 300 | 头部过渡时长(ms) | | asyncMatchFun | (search) => Promise<EditorUser[]> | — | 异步 @ 候选(防抖 300ms) | | customDialog | boolean | false | 选择标签不自弹内置弹窗,仅上抛 show-select-dialog |

| Event | 说明 | | --- | --- | | submit / change | (SubmitResult) SubmitResult = { text, html, tags: { userTags, selectTags, inputTags, customTags } } | | cancel | 点击停止按钮 | | show-at-dialog / show-select-dialog / show-tag-dialog | 各弹层显隐 |

| Slot | 说明 | | --- | --- | | header / footer / prefix / action-list | 同 Sender | | tag-tip | 输入标签 tip 弹层内容(作用域 { nodeId }) |

| Ref | 说明 | | --- | --- | | getCurrentValue() | 取 { text, html, tags } | | focusToStart() / focusToEnd() / blur() / selectAll() / clear() | 光标与清空 | | setText(text) / setHtml(html) / setMixTags(rows) | 程序化写入 | | setUserTag(id) / setCustomTag(prefix, id) / setInputTag(id, name) / setSelectTag(list) | 插入各类标签 | | customSetUser(user) / customSetTag(tag) | 外部直传对象插入(不经列表查表) | | updateSelectTag(method, options) | 选择标签更新:'insertTag'/'updateTagValue'/'deleteTag'/'updateTagStyle' | | openSelectDialog() / openTipTag(nodeId?) / closeTipTag() | 弹窗控制 | | openHeader() / closeHeader() | 显隐 #header | | chat / opNode(method, options) / chatState() | 底层文档模型:chat.insert/update/delete/append;opNode 按方法名分发 |

Thinking

思考链折叠面板。

| Prop | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | modelValue | boolean(v-model) | false | 展开/收起 | | content | string | '' | 思考内容(常配合 #content 插槽嵌 XMarkdown) | | status | 'start' | 'thinking' | 'end' | 'error' | 'start' | 思考状态 | | disabled | boolean | false | 禁用切换 | | autoCollapse | boolean | false | status 变为 end 时自动收起 | | buttonWidth | string | '' | 切换按钮区宽度 | | maxWidth | string | '' | 内容区最大宽度 | | color / backgroundColor | string | '' | 前景色 / 背景色 |

| Event | 说明 | | --- | --- | | update:modelValue | 展开/收起同步 |

| Slot | 说明 | | --- | --- | | status-icon / label / arrow | 自定义状态图标 / 文案 / 箭头 | | content / error | 思考内容 / error 态内容 |

| Ref | 说明 | | --- | --- | | modelValue / toggle() | 展开状态 / 手动切换 |

ThoughtChain

思维链步骤条。

| Prop | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | items | ThoughtChainItem[] | [] | 步骤项 { key, title?, status?: 'pending'/'success'/'loading'/'error', icon?, description? } | | direction | 'vertical' | 'horizontal' | 'vertical' | 排列方向 |

| Slot | 说明 | | --- | --- | | content | 覆盖默认 title/description(作用域 { item, index }) |

Welcome

空态欢迎页。

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

| Slot | 说明 | | --- | --- | | icon / name / description | 覆盖对应区块 |

Prompts

建议 prompt 网格。

| Prop | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | prompts | PromptItem[] | [] | 建议项 { key(必填), label?, description?, icon?(EP 图标名字符串或组件), disabled? } | | title | string | '' | 网格标题 |

| Event | 说明 | | --- | --- | | select | (item) 点击(disabled 项不触发) |

| Slot | 说明 | | --- | --- | | default | 覆盖整卡(作用域 { item, index }) |

FilesCard

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

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

| Slot | 说明 | | --- | --- | | image | 覆盖图片区(默认 url 缩略图,无 url 渲染 Picture 占位) |

Attachments

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

| Prop | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | modelValue | object | [] | 附件列表(项含 uid/name/size/url/fileType/status/raw) | | accept | string | '' | input accept,如 'image/*' | | tip | string | '' | 内联提示文案 | | count | number | 0 | 最大附件数(超出走 @exceed;0 不限) | | beforeUpload | (file) => boolean | Promise | — | 上传前校验,返回 false 拦截 | | layout | 'inline' | 'triggerOnly' | 'listOnly' | 'inline' | 布局:inline 回形针+列表+提示一行;triggerOnly 只渲染回形针按钮;listOnly 只渲染文件列表(拆分布局供整框组合:按钮进操作栏、列表进输入区上方) |

| Event | 说明 | | --- | --- | | update:modelValue / change | 列表变化(全量数组) | | exceed | (file) 超出 count | | delete | (item) 删除某项 |

| Slot | 说明 | | --- | --- | | list-item | 覆盖项渲染(作用域 { item, index }) | | list-thumb | 覆盖图片缩略图(作用域 { item, index }) |

| Ref | 说明 | | --- | --- | | openSelect() | 程序化打开文件选择 | | retry(item) | error 项重新置为 loading(上传逻辑由外部接线) |

Conversations

会话列表(按 group 分组)。

| Prop | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | items | ConversationItem[] | [] | 会话项 { key(必填), label(必填), group?, disabled? } | | active | string | number(v-model:active) | undefined | 当前激活会话 key | | showBuiltInMenuType | 'hover' | 'always' | 'hover' | 内置菜单按钮显隐方式 | | groupable | (a, b) => number | — | 分组排序比较器(缺省保持插入顺序) |

| Event | 说明 | | --- | --- | | update:active / change | 选中会话(key / item) | | menu-command | ({ item, command: 'rename'/'delete', value? }) 菜单指令;rename 确认后带新名称 |

| Slot | 说明 | | --- | --- | | group-title | 分组标题(作用域 { group }) | | item | 覆盖行内容(作用域 { item, active }) | | menu | 覆盖菜单项(默认重命名/删除;作用域 { item }) |

| Ref | 说明 | | --- | --- | | handleMenuCommand({ item, command }) | 程序化触发菜单指令转发(纯转发,同步上抛 menu-command) | | renamePrompt(key) | 弹重命名输入框,确认后上抛 menu-command | | deleteConfirm(key) | 弹删除确认框,确认后上抛 menu-command |

对话应用层 API

AiChat

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

| Prop | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | model | string | '' | 默认模型(未传 models 时作为单模型) | | models | string[] | [] | 可选模型列表(有值时顶栏多选选择器) | | baseURL | string | '' | OpenAI 兼容服务地址(未传 adapter 时构造 OpenAIAdapter) | | apiKey | string | '' | 服务密钥 | | adapter | ChatAdapter | undefined | 聊天适配器(与 baseURL 二选一,优先) | | messages | MessageNode | undefined | 消息链(半受控,见下) | | title | string | '' | 顶栏标题(同时作空态欢迎页名称) | | showHeader | boolean | false | 显示顶栏(标题 + 模型选择器;models 多选选择器在顶栏内,需开启才可见) | | centerWelcome | boolean | true | 空态居中:未开始对话时欢迎内容与输入区整体垂直居中,首条消息后输入区恢复固定底部 | | showScrollButton | boolean | true | 显示滚到底部悬浮按钮(透传 AiMessages;距底超过 scrollButtonThreshold 时出现) | | scrollButtonThreshold | number | 100 | 滚到底部悬浮按钮显示阈值(px,透传 AiMessages) | | scrollThreshold | number | 100 | 用户上滚免打扰阈值(px,透传 AiMessages):距底不超过该值视为「钉在底部」,流式期间自动跟随滚动;调小可恢复更灵敏的跟随 | | showUserAvatar | boolean | true | 显示用户头像(false 时用户消息不渲染头像;助手头像不受影响) | | showUserName | boolean | true | 显示用户名称(false 时用户消息不渲染头部名称行;助手名称不受影响) | | showAssistantAvatar | boolean | true | 显示助手头像(false 时助手消息不渲染头像;用户头像不受影响) | | showAssistantName | boolean | true | 显示助手名称(false 时助手消息不渲染头部名称行;用户名称不受影响) | | userAvatar | string | '' | 用户头像图片 URL(空串回落内置图标;user-avatar 插槽优先) | | assistantAvatar | string | '' | 助手头像图片 URL(空串回落内置默认头像「小维」;assistant-avatar 插槽优先) | | userName | string | '' | 用户名称(空串回落「你」;user-name 插槽优先) | | assistantName | string | '' | 助手名称(空串回落「小维」;assistant-name 插槽优先) | | placeholder | string | '' | 输入框占位 | | senderShadow | boolean | true | 输入区阴影效果(透传 Sender 的 shadow) | | suggestions | AiChatSuggestion[] | [] | 空态建议列表(Prompts 项,key 必填) | | features | AiChatFeatures | {} | 特性开关 { artifacts?, tts?, queue?, continueGenerate?, fullscreenEditor? }(当前仅 artifacts 生效) | | allowAttachments | boolean | false | 开启输入区附件上传(回形针在操作栏左侧与提交按钮同行,已选列表在输入框上方;只传附件不输文字也可提交) | | accept | string | '' | 附件 input accept 属性(透传 Attachments),如 'image/*' | | attachmentLimit | number | 0 | 附件数量上限(0 不限,超限走 @attachment-exceed) | | theme | 'light' | 'dark' | 'auto' | 'light' | 主题(向 html 注入/移除 dark class) |

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

| Event | 说明 | | --- | --- | | update:messages | (MessageNode[]) 全量消息链(去抖后) | | send | (text, files?) 用户发送(allowAttachments 开启时 files 为降维后的 ChatFile[]) | | message-complete | (message) 链尾 assistant 生成完成 | | error | (err, message?) 生成出错 | | branch-change | (dir, message) 分支切换 | | attachment-exceed | (file) 附件超过 attachmentLimit 上限 |

| Slot | 说明 | | --- | --- | | message-avatar / message-actions | 透传给 AiMessages(作用域见 AiMessage;message-avatar 优先于细分头像插槽) | | user-avatar / assistant-avatar | 自定义用户/助手头像(assistant-avatar 作用域 { status, message },status 为 thinking/done/error,可按状态换头像;优先于同名 props) | | user-name / assistant-name | 自定义用户/助手名称(作用域 { message },优先于同名 props) | | empty | 覆盖空态整区 | | scroll-button | 自定义滚到底部悬浮按钮整块(作用域 { scrollToBottom, awayFromBottom };覆盖后内置按钮与显隐逻辑均由插槽内容接管) | | scroll-button-icon | 仅自定义悬浮按钮图标内容(按钮本体/显隐不变) | | sender-prefix | 输入区前缀 |

| Ref | 说明 | | --- | --- | | sendMessage(text, files?) | 程序化发送 | | stop() | 停止生成(保留半截内容,静默收口不发 complete/error) | | regenerate(messageId?) | 重生成(缺省链尾 assistant) | | appendToMessage(id, text) | 向指定消息追加文本 | | clearMessages() | 清空消息树 | | exportTxt() | 导出当前链为纯文本 | | openArtifact(key) | 程序化打开某个 artifact |

AiMessages

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

| Prop | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | messages | MessageNode[] | —(必填) | 当前链消息(根 → 焦点) | | nodes | Record<string, MessageNode> | {} | 全量节点表(分支计算需查父 childrenIds) | | icon | string | Component | '' | 空态欢迎页图标 | | name | string | '' | 空态欢迎页名称 | | description | string | '' | 空态欢迎页描述 | | suggestions | AiMessagesSuggestion[] | [] | 空态建议列表 | | suggestionsTitle | string | '' | 空态建议区标题 | | scrollThreshold | number | 100 | 用户上滚免打扰阈值(px):距底不超过该值视为「钉在底部」,流式期间自动跟随滚动 | | showScrollButton | boolean | true | 显示滚到底部悬浮按钮(空态不渲染) | | scrollButtonThreshold | number | 100 | 距底超过该值(px)才显示滚到底部悬浮按钮 | | showActions | boolean | true | 显示消息操作按钮(复制/重生成) | | showBranchSwitch | boolean | true | 显示分支切换器 | | showUserAvatar | boolean | true | 显示用户头像(助手头像不受影响) | | showUserName | boolean | true | 显示用户名称(助手名称不受影响) | | showAssistantAvatar | boolean | true | 显示助手头像(用户头像不受影响) | | showAssistantName | boolean | true | 显示助手名称(用户名称不受影响) | | userAvatar | string | '' | 用户头像图片 URL(透传 AiMessage) | | assistantAvatar | string | '' | 助手头像图片 URL(透传 AiMessage;空串回落内置默认头像) | | userName | string | '' | 用户名称(透传 AiMessage) | | assistantName | string | '' | 助手名称(透传 AiMessage;空串回落「小维」) |

| Event | 说明 | | --- | --- | | select / copy / regenerate / branch-change | 建议选中 / 复制 / 重生成 / 分支切换,载荷同 AiChat |

| Slot | 说明 | | --- | --- | | empty | 覆盖空态整区 | | scroll-button | 自定义滚到底部悬浮按钮整块(作用域 { scrollToBottom, awayFromBottom };覆盖后内置按钮与显隐逻辑均由插槽内容接管) | | scroll-button-icon | 仅自定义悬浮按钮图标内容(按钮本体/显隐不变) | | message-avatar / message-actions | 透传给每条 AiMessage(message-avatar 优先于细分头像插槽) | | user-avatar / assistant-avatar | 自定义用户/助手头像(assistant-avatar 作用域 { status, message },status 为 thinking/done/error,可按状态换头像;优先于同名 props) | | user-name / assistant-name | 自定义用户/助手名称(作用域 { message },优先于同名 props) |

| CSS 变量 | 默认 | 说明 | | --- | --- | --- | | --fv-messages-max-width | none | 滚动内容最大宽度;超出部分左右居中(内层 .fv-messages-inner,配合 margin: 0 auto) |

| Ref | 说明 | | --- | --- | | scrollToBottom(smooth?) / scrollToTop() | 滚动控制 |

AiMessage

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

| Prop | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | message | MessageNode | —(必填) | 消息节点 | | branch | AiMessageBranchInfo | undefined | 分支元数据 { count, index };缺省视为单分支 | | showActions | boolean | true | 显示复制/重生成(仅 assistant) | | showBranchSwitch | boolean | true | 显示分支切换器(branch.count > 1 时) | | showUserAvatar | boolean | true | 显示用户头像(false 时用户消息不渲染头像块) | | showUserName | boolean | true | 显示用户名称(false 时用户消息不渲染头部名称行) | | showAssistantAvatar | boolean | true | 显示助手头像(false 时助手消息不渲染头像块) | | showAssistantName | boolean | true | 显示助手名称(false 时助手消息不渲染头部名称行) | | userAvatar | string | '' | 用户头像图片 URL(空串回落内置图标) | | assistantAvatar | string | '' | 助手头像图片 URL(空串回落内置默认头像「小维」) | | userName | string | '' | 用户名称(空串回落「你」) | | assistantName | string | '' | 助手名称(空串回落「小维」) |

用户消息操作区:恒渲染复制按钮,默认透明,鼠标移上消息(或键盘聚焦)时浮现(open-webui 交互)。 | autoCollapse | boolean | true | Thinking autoCollapse 透传 |

| Event | 说明 | | --- | --- | | copy / regenerate / branch-change | (message) / (message) / (dir, message) |

| Slot | 说明 | | --- | --- | | message-avatar | 自定义头像(作用域 { message };优先于细分头像插槽,两条角色消息均生效) | | user-avatar | 自定义用户头像(无作用域;优先于 userAvatar props) | | assistant-avatar | 自定义助手头像(作用域 { status, message },status 为 thinking/done/error,可按状态换头像;优先于 assistantAvatar props) | | user-name | 自定义用户名称(作用域 { message };优先于 userName props) | | assistant-name | 自定义助手名称(作用域 { message };优先于 assistantName props) | | message-files | 自定义附件渲染(作用域 { message, files }) | | message-actions | 自定义操作区(作用域 { message }) |

| Ref | 说明 | | --- | --- | | message | 当前消息节点(computed) |

AiArtifacts

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

| Prop | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | artifacts | AiArtifactItem[] | [] | artifact 列表 { key(必填), type: 'html'/'svg', content, title? } | | activeKey | string | '' | 当前展示的 key;为空时面板隐藏 |

| Event | 说明 | | --- | --- | | close / change | 关闭 / 切换(key) | | navigate | (href) iframe 内点击外链被拦截上抛 |

| Ref | 说明 | | --- | --- | | open(key) / close() | 程序化显隐 |

Composables

useChat(options)

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

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

DetOmsChatAdapter(config)

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

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

useMessages(initial?)

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

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

useXStream()

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

useSend() / XRequest

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

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

useRecord()

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

自定义 ChatAdapter

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

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

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

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

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

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

主题定制

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

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

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

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

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

暗色模式

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

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

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

Prism 主题引入

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

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

内置主题:prism(默认)、prism-coy、prism-dark、prism-funky、prism-okaidia、prism-solarizedlight、prism-tomorrow、prism-twilight。

FAQ

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

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

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

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

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

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

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

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

已知限制与 Roadmap

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

  • 消息列表懒加载:useMessages 数据面支持分页注入,AiMessages 暂全量渲染;超长对话建议自行分页。
  • 消息操作条:当前内置复制/重新生成;编辑/删除/继续生成/朗读请经 useChat(editMessage/removeMessage/continueGenerate)或 #message-actions 插槽自建入口。
  • 队列卡片:useChat.enqueue 可用,features.queue 展示 UI 待接线。
  • 语音输入:Sender 的 allowSpeech 依赖浏览器 SpeechRecognition(Chrome/Edge 可用,Firefox 不支持转写,降级为仅录音、确认后无文本);录音/波形需要 HTTPS 或 localhost 环境。
  • 多实例暗色互斥:theme 采用单写者假设,多实例同屏请自行管理 html.dark。

类型与导出

包入口导出全部组件、useChat/useMessages/useXStream/useSend/XRequest/useRecord、OpenAIAdapter、DetOmsChatAdapter(+ DetOmsError 及配套 DTO 类型)、createMessageTree、createLocalStorageAdapter/createMemoryAdapter、createSSEParser 及全部公共类型(MessageNode、ChatAdapter、ChatFile、MessageUsage、SSEEvent 等),消费方:

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