# @fv-ui/ai-chat

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

Latest version **0.1.1** (published 2026-09-24) · 0 weekly downloads

## Install

```sh
npm install @fv-ui/ai-chat
pnpm add @fv-ui/ai-chat
yarn add @fv-ui/ai-chat
bun add @fv-ui/ai-chat
```

## Health

**Score 65/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.1.1 |
| Published | 2026-09-24 |
| First published | 2026-09-23 |
| Weekly downloads | 0 |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 5 |
| Unpacked size | 676.8 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | waterbeside |

## Links

- npm: https://www.npmjs.com/package/@fv-ui/ai-chat
- npm.io page: https://npm.io/package/@fv-ui/ai-chat

## Dependencies (5)

- [katex](https://npm.io/package/katex.md) ^0.16.11
- [prismjs](https://npm.io/package/prismjs.md) ^1.29.0
- [dompurify](https://npm.io/package/dompurify.md) ^3.1.0
- [markdown-it](https://npm.io/package/markdown-it.md) ^14.1.0
- [@vscode/markdown-it-katex](https://npm.io/package/@vscode/markdown-it-katex.md) ^1.1.2

## Recent versions

- 0.1.1 (latest) — 2026-09-24
- 0.1.0 — 2026-09-23

## README

# @fv-ui/ai-chat

![version](https://img.shields.io/badge/version-0.1.0-blue)
![vue](https://img.shields.io/badge/vue-%5E3.4.0-brightgreen)
![element-plus](https://img.shields.io/badge/element--plus-%5E2.7.0-blue)

基于 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)开箱即用

## 安装

```bash
pnpm add @fv-ui/ai-chat
```

peerDependencies:`vue ^3.4.0`、`element-plus ^2.7.0`、`@element-plus/icons-vue ^2.3.0`。

## 快速开始

```ts
// 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')
```

```vue
<!-- App.vue -->
<script setup lang="ts">
import { FvAiChat } from '@fv-ui/ai-chat'
import '@fv-ui/ai-chat/dist/style.css'
</script>

<template>
  <FvAiChat
    title="AI 助手"
    base-url="https://api.deepseek.com/v1"
    api-key="sk-xxx"
    model="deepseek-chat"
    style="height: 600px"
  />
</template>
```

`<FvAiChat>` 未传 `adapter` 时,用 `baseURL`/`apiKey`/`model` 构造内置 `OpenAIAdapter`(走 `/chat/completions`,SSE 流式)。两个都缺省时发送会报"未配置 adapter 或 baseURL"。

## 基础组件层 API

### XMarkdown

Markdown 渲染(Sanitized by DOMPurify),内置 Prism 代码高亮与 KaTeX。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| content | string | —(必填) | Markdown 源文本 |
| mdPlugins | PluginWithParams[] | — | 额外 markdown-it 插件 |
| highlight | (str, lang) => string | 内置 Prism | 自定义代码高亮 |
| isTyping | boolean | — | 打字机模式样式钩子(透传占位) |

| Ref | 说明 |
| --- | --- |
| getHtml() | 取当前渲染后的 HTML 字符串 |

### Typewriter

打字机输出,支持续打/重打/雾化遮罩。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| content | string | —(必填) | 全文(流式时增量传入,子集扩展自动续打) |
| isMarkdown | boolean | false | 走 XMarkdown 渲染(此时 suffix 不追加) |
| isFog | boolean \| { bgColor?; width? } | false | 打字中尾部雾化遮罩 |
| typing | boolean \| { step?; interval?; suffix? } | false | 开启打字机;对象形态可配步长/间隔/光标后缀 |

| Event | 说明 |
| --- | --- |
| start / writing / finish | 开始打字 / 每次推进 / 全部打完 |

| Ref | 说明 |
| --- | --- |
| interrupt() / continue() / restart() / destroy() | 暂停 / 继续 / 重打 / 重置清空 |
| renderedContent / isTyping / progress | 已输出文本 / 是否打字中 / 进度 0-1 |

### Bubble

单条气泡(内部按需挂载 Typewriter/XMarkdown)。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| content | string | '' | 气泡内容 |
| placement | 'start' \| 'end' | 'start' | start 靠左 / end 靠右 |
| loading | boolean | false | 加载态(三点动画,可被 #loading 覆盖) |
| typing | boolean \| TypingConfig | false | 打字机模式(透传 Typewriter) |
| isMarkdown | boolean | false | 内容按 Markdown 渲染 |
| isFog | boolean \| { bgColor?; width? } | false | 雾化(自定义气泡背景时传 bgColor) |
| variant | 'filled' \| 'borderless' \| 'outlined' \| 'shadow' | 'filled' | 气泡样式 |
| shape | 'round' \| 'corner' | 'round' | 圆角 / 方角 |
| avatar | string | '' | 头像图片地址 |
| avatarSize | ElAvatar size \| number | '' | 头像尺寸 |
| avatarGap | string | '12px' | 头像与内容间距 |
| avatarShape | 'circle' \| 'square' | 'circle' | 头像形状 |
| avatarIcon | Component | — | 头像图标(与 avatar 冲突时优先) |
| avatarSrcSet / avatarAlt | string | '' / '' | 头像 srcSet/alt |
| avatarFit | 'fill' \| 'contain' \| 'cover' \| 'none' \| 'scale-down' | 'cover' | 头像填充方式(el-avatar fit) |
| noPadding | boolean | false | 内容区去内边距 |
| noStyle | boolean | false | 去气泡样式(透明/无框/无阴影) |

| Event | 说明 |
| --- | --- |
| avatar-error | 头像加载失败 |
| start / writing / finish | typing 模式下的打字机事件透传 |

| Slot | 说明 |
| --- | --- |
| avatar / header / content / loading / footer | 自定义头像 / 头部 / 内容(优先于 content prop)/ 加载态 / 底部 |

| Ref | 说明 |
| --- | --- |
| interrupt / continue / restart / destroy | Typewriter 方法转发(非 typing 模式为 no-op) |
| renderedContent / isTyping / progress | Typewriter 状态转发 |
| twInstance | 内部 Typewriter 原始实例(BubbleList @complete 透传用) |

### BubbleList

气泡列表 + 吸底/回底按钮。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| list | BubbleListItemProps[] | [] | 列表项(Bubble props + 必填 key) |
| maxHeight | string | '' | 滚动容器最大高度(如 '350px'/'60vh') |
| alwaysShowScrollbar | boolean | false | 始终显示滚动条 |
| showBackButton | boolean | true | 是否显示回底按钮 |
| backButtonThreshold | number | 80 | 距底超过该像素显示回底按钮 |
| btnLoading | boolean | false | 回底按钮加载态 |
| btnColor | string | '' | 回底按钮颜色(el-button color) |
| backButtonPosition | { bottom?; left? } | {} | 回底按钮位置偏移 |
| btnIconSize | number | 16 | 回底按钮图标尺寸 |
| triggerIndices | 'only-last' \| 'all' \| number[] | 'only-last' | @complete 触发索引 |

| Event | 说明 |
| --- | --- |
| complete | (typewriterInstance, index) 打字完成(按 triggerIndices 过滤后上抛) |

| Slot | 说明 |
| --- | --- |
| avatar / header / content / loading / footer | 透传给每个 Bubble,作用域 { item, index } |

| Ref | 说明 |
| --- | --- |
| scrollToTop() / scrollToBottom() / scrollToBubble(index) | 滚动控制 |

### Sender

输入发送框。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| modelValue | string(v-model) | '' | 输入值(缺省内部自持) |
| placeholder | string | '' | 占位文案 |
| autosize | { minRows?; maxRows? } | {} | 自增高(行高 22px) |
| submitType | 'enter' \| 'shiftEnter' \| 'cmdOrCtrlEnter' \| 'altEnter' | 'enter' | 提交按键模式 |
| loading | boolean | false | 加载态(发送键变停止键,输入区禁用) |
| disabled | boolean | false | 禁用(阻断提交) |
| readOnly | boolean | false | 只读 |
| clearable | boolean | false | 显示清空按钮 |
| inputWidth | string | '' | 输入区宽度(variant=default 时有效) |
| variant | 'default' \| 'updown' | 'default' | default 同排 / updown 上输入下操作栏 |
| submitBtnDisabled | boolean | false | 附加禁用发送按钮 |
| triggerStrings | string[] | [] | 指令触发串(光标前命中弹指令框) |
| triggerPopoverVisible | boolean(v-model:triggerPopoverVisible) | false | 指令弹框显隐 |
| triggerPopoverWidth / triggerPopoverLeft | string | '' | 指令弹框宽度 / left 偏移 |
| triggerPopoverOffset | number | 0 | 弹框距触发字符偏移量 |
| triggerPopoverPlacement | ElPopover placement | 'top-start' | 弹框位置 |
| allowSpeech | boolean | false | 显示语音按钮(当前为桩,点击仅 warn) |
| allowEmptySubmit | boolean | false | 允许空内容提交(附件场景:只发文件不输文字) |

| Event | 说明 |
| --- | --- |
| update:modelValue / change | 输入值变化 |
| submit | (value) 提交(提交后内部清空) |
| cancel | 点击停止按钮 |
| trigger | (keyword, visible) 命中指令串 |
| recording-change | 语音录制状态变化(预留) |
| paste-file | (file) 粘贴文件 |
| update:triggerPopoverVisible | 指令弹框显隐同步 |

| Slot | 说明 |
| --- | --- |
| header / footer | 头部/底部(配合 ref.openHeader/closeHeader) |
| prefix | 输入区前缀 |
| action-list | 自定义操作区(存在时隐藏内置按钮) |
| action-left | 操作栏左侧附加区(与提交按钮同行靠左,如附件回形针) |
| trigger | 指令弹框内容(作用域 { keyword }) |

| Ref | 说明 |
| --- | --- |
| submit() / cancel() / clear() | 提交 / 停止 / 清空 |
| focus(pos?) / blur() | pos: 'all' 全选 \| 'start' 头 \| 'end' 尾(缺省 end) |
| openHeader() / closeHeader() | 显隐 #header 插槽 |

### MentionSender

带 @提及 的 Sender(键盘 ↑↓ 导航 / Enter 确认 / Esc 关闭)。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| modelValue | string(v-model) | '' | 输入值 |
| userList | MentionUserInfo[] | [] | @ 触发的候选用户(id/name/avatar/pinyin) |
| customTrigger | MentionCustomTrigger[] | [] | 自定义触发组 { dialogTitle, prefix, tagList } |
| asyncMatchFun | (search) => Promise<MentionUserInfo[]> | — | 异步候选(存在时防抖 300ms,后端过滤) |

事件:`update:modelValue` / `change` / `submit` / `cancel` / `trigger` / `recording-change` / `paste-file` / `update:triggerPopoverVisible` / `show-at-dialog`(弹层显隐);其余 props/事件经 `$attrs` 透传给 Sender,slot 经 forwardedSlots 显式转发(header/footer/prefix/action-list/trigger)。

| Ref | 说明 |
| --- | --- |
| submit / cancel / clear / focus / blur / openHeader / closeHeader | Sender 方法转发 |
| setUserTag(userId) | 光标处程序化插入 @用户 |
| setCustomTag(prefix, id) | 光标处插入自定义触发标签 |

### EditorSender

富文本标签编辑器(contenteditable),open-webui 风格标签芯片。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| placeholder | string | '' | 占位文案 |
| device | 'pc' \| 'h5' | 'pc' | 设备形态(仅影响样式) |
| autoFocus | boolean | false | 挂载后聚焦到末尾 |
| variant | 'default' \| 'updown' | 'default' | 布局形态 |
| userList | EditorUser[] | [] | @ 候选用户 |
| customTrigger | EditorCustomTrigger[] | [] | 自定义触发组 |
| selectList | EditorSelectGroup[] | [] | 选择标签分组(openSelectDialog 弹窗数据) |
| maxLength | number | 0 | 纯文本最大长度(0 不限;性能开销大,仅显式开启) |
| submitType | 'enter' \| 'shiftEnter' | 'enter' | 提交按键模式 |
| customStyle | Record<string, any> | {} | 根节点自定义样式 |
| loading | boolean | false | 加载态(发送键变停止键) |
| disabled | boolean | false | 禁用 |
| clearable | boolean | false | 显示清空按钮 |
| headerAnimationTimer | number | 300 | 头部过渡时长(ms) |
| asyncMatchFun | (search) => Promise<EditorUser[]> | — | 异步 @ 候选(防抖 300ms) |
| customDialog | boolean | false | 选择标签不自弹内置弹窗,仅上抛 show-select-dialog |

| Event | 说明 |
| --- | --- |
| submit / change | (SubmitResult) SubmitResult = { text, html, tags: { userTags, selectTags, inputTags, customTags } } |
| cancel | 点击停止按钮 |
| show-at-dialog / show-select-dialog / show-tag-dialog | 各弹层显隐 |

| Slot | 说明 |
| --- | --- |
| header / footer / prefix / action-list | 同 Sender |
| tag-tip | 输入标签 tip 弹层内容(作用域 { nodeId }) |

| Ref | 说明 |
| --- | --- |
| getCurrentValue() | 取 { text, html, tags } |
| focusToStart() / focusToEnd() / blur() / selectAll() / clear() | 光标与清空 |
| setText(text) / setHtml(html) / setMixTags(rows) | 程序化写入 |
| setUserTag(id) / setCustomTag(prefix, id) / setInputTag(id, name) / setSelectTag(list) | 插入各类标签 |
| customSetUser(user) / customSetTag(tag) | 外部直传对象插入(不经列表查表) |
| updateSelectTag(method, options) | 选择标签更新:'insertTag'/'updateTagValue'/'deleteTag'/'updateTagStyle' |
| openSelectDialog() / openTipTag(nodeId?) / closeTipTag() | 弹窗控制 |
| openHeader() / closeHeader() | 显隐 #header |
| chat / opNode(method, options) / chatState() | 底层文档模型:chat.insert/update/delete/append;opNode 按方法名分发 |

### Thinking

思考链折叠面板。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| modelValue | boolean(v-model) | false | 展开/收起 |
| content | string | '' | 思考内容(常配合 #content 插槽嵌 XMarkdown) |
| status | 'start' \| 'thinking' \| 'end' \| 'error' | 'start' | 思考状态 |
| disabled | boolean | false | 禁用切换 |
| autoCollapse | boolean | false | status 变为 end 时自动收起 |
| buttonWidth | string | '' | 切换按钮区宽度 |
| maxWidth | string | '' | 内容区最大宽度 |
| color / backgroundColor | string | '' | 前景色 / 背景色 |

| Event | 说明 |
| --- | --- |
| update:modelValue | 展开/收起同步 |

| Slot | 说明 |
| --- | --- |
| status-icon / label / arrow | 自定义状态图标 / 文案 / 箭头 |
| content / error | 思考内容 / error 态内容 |

| Ref | 说明 |
| --- | --- |
| modelValue / toggle() | 展开状态 / 手动切换 |

### ThoughtChain

思维链步骤条。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| items | ThoughtChainItem[] | [] | 步骤项 { key, title?, status?: 'pending'/'success'/'loading'/'error', icon?, description? } |
| direction | 'vertical' \| 'horizontal' | 'vertical' | 排列方向 |

| Slot | 说明 |
| --- | --- |
| content | 覆盖默认 title/description(作用域 { item, index }) |

### Welcome

空态欢迎页。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| icon | string \| Component | '' | 图标(字符串为图片地址;未传渲染内置对话图标) |
| name | string | '' | 名称 |
| description | string | '' | 描述(纯文本) |

| Slot | 说明 |
| --- | --- |
| icon / name / description | 覆盖对应区块 |

### Prompts

建议 prompt 网格。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| prompts | PromptItem[] | [] | 建议项 { key(必填), label?, description?, icon?(EP 图标名字符串或组件), disabled? } |
| title | string | '' | 网格标题 |

| Event | 说明 |
| --- | --- |
| select | (item) 点击(disabled 项不触发) |

| Slot | 说明 |
| --- | --- |
| default | 覆盖整卡(作用域 { item, index }) |

### FilesCard

单个文件卡片。文档模式按扩展名自动映射类型图标:pdf/doc/txt→文档、xls/csv→图表、ppt→演示、mp4→胶片、mp3→耳机、zip→文件夹、html/url→链接、apk→手机、exe→芯片,未命中回落通用文档图标。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| name | string | '' | 文件名 |
| fileType | 'image' \| 'file' | 'file' | image 渲染缩略图,file 渲染文档图标卡 |
| size | number \| string | 0 | 文件大小(字节自动格式化为 B/KB/MB;字符串原样展示;0 不显示) |
| url | string | '' | 图片地址(image 模式缩略图) |
| status | 'loading' \| 'success' \| 'error' | 'success' | loading spinner / error 红边 |
| imageSize | string | '' | 缩略图尺寸(width/height 同值) |
| onDelete | () => void | — | 删除按钮回调 |

| Slot | 说明 |
| --- | --- |
| image | 覆盖图片区(默认 url 缩略图,无 url 渲染 Picture 占位) |

### Attachments

附件选择与列表(v-model)。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| modelValue | object[](v-model) | [] | 附件列表(项含 uid/name/size/url/fileType/status/raw) |
| accept | string | '' | input accept,如 'image/*' |
| tip | string | '' | 内联提示文案 |
| count | number | 0 | 最大附件数(超出走 @exceed;0 不限) |
| beforeUpload | (file) => boolean \| Promise<boolean> | — | 上传前校验,返回 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[](v-model:messages) | undefined | 消息链(半受控,见下) |
| title | string | '' | 顶栏标题(同时作空态欢迎页名称) |
| placeholder | string | '' | 输入框占位 |
| suggestions | AiChatSuggestion[] | [] | 空态建议列表(Prompts 项,key 必填) |
| features | AiChatFeatures | {} | 特性开关 { artifacts?, tts?, queue?, continueGenerate?, fullscreenEditor? }(当前仅 artifacts 生效) |
| allowAttachments | boolean | false | 开启输入区附件上传(回形针在操作栏左侧与提交按钮同行,已选列表在输入框上方;只传附件不输文字也可提交) |
| accept | string | '' | 附件 input accept 属性(透传 Attachments),如 'image/*' |
| attachmentLimit | number | 0 | 附件数量上限(0 不限,超限走 @attachment-exceed) |
| theme | 'light' \| 'dark' \| 'auto' | 'light' | 主题(向 html 注入/移除 dark class) |

**v-model:messages 半受控语义**:外部 `messages` 仅在 setup 时一次性深拷贝灌入(用于恢复历史);组件内部每次变更全量上抛 `update:messages`(同帧去抖),父组件需经 v-model 回写维持单向数据流;父组件后续直接改 prop **不会**回灌生效。

| Event | 说明 |
| --- | --- |
| update:messages | (MessageNode[]) 全量消息链(去抖后) |
| send | (text, files?) 用户发送(allowAttachments 开启时 files 为降维后的 ChatFile[]) |
| message-complete | (message) 链尾 assistant 生成完成 |
| error | (err, message?) 生成出错 |
| branch-change | (dir, message) 分支切换 |
| attachment-exceed | (file) 附件超过 attachmentLimit 上限 |

| Slot | 说明 |
| --- | --- |
| message-avatar / message-actions | 透传给 AiMessages(作用域见 AiMessage) |
| empty | 覆盖空态整区 |
| sender-prefix | 输入区前缀 |

| Ref | 说明 |
| --- | --- |
| sendMessage(text, files?) | 程序化发送 |
| stop() | 停止生成(保留半截内容,静默收口不发 complete/error) |
| regenerate(messageId?) | 重生成(缺省链尾 assistant) |
| appendToMessage(id, text) | 向指定消息追加文本 |
| clearMessages() | 清空消息树 |
| exportTxt() | 导出当前链为纯文本 |
| openArtifact(key) | 程序化打开某个 artifact |

### AiMessages

消息列表(空态渲染 Welcome + Prompts),吸底滚动。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| messages | MessageNode[] | —(必填) | 当前链消息(根 → 焦点) |
| nodes | Record<string, MessageNode> | {} | 全量节点表(分支计算需查父 childrenIds) |
| icon | string \| Component | '' | 空态欢迎页图标 |
| name | string | '' | 空态欢迎页名称 |
| description | string | '' | 空态欢迎页描述 |
| suggestions | AiMessagesSuggestion[] | [] | 空态建议列表 |
| suggestionsTitle | string | '' | 空态建议区标题 |
| scrollThreshold | number | 40 | 吸底阈值(px):距底不超过该值才自动跟随 |
| showActions | boolean | true | 显示消息操作按钮(复制/重生成) |
| showBranchSwitch | boolean | true | 显示分支切换器 |

| Event | 说明 |
| --- | --- |
| select / copy / regenerate / branch-change | 建议选中 / 复制 / 重生成 / 分支切换,载荷同 AiChat |

| Slot | 说明 |
| --- | --- |
| empty | 覆盖空态整区 |
| message-avatar / message-actions | 透传给每条 AiMessage |

| Ref | 说明 |
| --- | --- |
| scrollToBottom(smooth?) / scrollToTop() | 滚动控制 |

### AiMessage

单条消息(Markdown 正文 + 思考链折叠 + 附件 + 操作区)。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| message | MessageNode | —(必填) | 消息节点 |
| branch | AiMessageBranchInfo | undefined | 分支元数据 { count, index };缺省视为单分支 |
| showActions | boolean | true | 显示复制/重生成(仅 assistant) |
| showBranchSwitch | boolean | true | 显示分支切换器(branch.count > 1 时) |
| autoCollapse | boolean | true | Thinking autoCollapse 透传 |

| Event | 说明 |
| --- | --- |
| copy / regenerate / branch-change | (message) / (message) / (dir, message) |

| Slot | 说明 |
| --- | --- |
| message-avatar | 自定义头像(作用域 { message }) |
| message-files | 自定义附件渲染(作用域 { message, files }) |
| message-actions | 自定义操作区(作用域 { message }) |

| Ref | 说明 |
| --- | --- |
| message | 当前消息节点(computed) |

### AiArtifacts

Artifacts 预览面板(iframe 沙箱,从消息 ```html / ```svg 代码块提取)。

| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| artifacts | AiArtifactItem[] | [] | artifact 列表 { key(必填), type: 'html'/'svg', content, title? } |
| activeKey | string | '' | 当前展示的 key;为空时面板隐藏 |

| Event | 说明 |
| --- | --- |
| close / change | 关闭 / 切换(key) |
| navigate | (href) iframe 内点击外链被拦截上抛 |

| Ref | 说明 |
| --- | --- |
| open(key) / close() | 程序化显隐 |

## Composables

### useChat(options)

聊天编排:发送 → 流式回填 → 队列/重生成/续写。

```ts
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`。会话管理(列表/归档/删除等)由消费方直接调后端,不经此适配器。

```ts
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 协议):

```ts
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 变量即完成主题定制:

```scss
// 全局或局部覆盖(需在引入 dist/style.css 之后)
:root {
  --el-color-primary: #7c3aed;
}
```

高级定制可直接 `@use` 源 SCSS 入口:

```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 的暗色变量生效:

```ts
// main.ts 引入 EP 暗色变量
import 'element-plus/theme-chalk/dark/css-vars.css'
```

```vue
<FvAiChat theme="auto" ... />
```

注意:组件卸载时会移除 `html.dark`(单实例假设);多实例并用暗色需自行管理 class。

## Prism 主题引入

代码高亮默认配色由组件样式提供;要换成其他 Prism 主题,在 `dist/style.css` 之后引入覆盖:

```ts
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>` 包裹(或客户端挂载后再渲染):

```vue
<ClientOnly>
  <FvAiChat ... />
</ClientOnly>
```

## 已知限制与 Roadmap

以下能力数据层已就绪,组合 UI 待后续版本接线(v0.2 计划):

- **消息列表懒加载**:`useMessages` 数据面支持分页注入,`AiMessages` 暂全量渲染;超长对话建议自行分页。
- **消息操作条**:当前内置复制/重新生成;编辑/删除/继续生成/朗读请经 `useChat`(`editMessage`/`removeMessage`/`continueGenerate`)或 `#message-actions` 插槽自建入口。
- **队列卡片**:`useChat.enqueue` 可用,`features.queue` 展示 UI 待接线。
- **语音输入**:`Sender` 的 `allowSpeech` 当前为桩(点击仅提示);`useRecord` 可独立使用。
- **多实例暗色互斥**:`theme` 采用单写者假设,多实例同屏请自行管理 `html.dark`。

## 类型与导出

包入口导出全部组件、`useChat`/`useMessages`/`useXStream`/`useSend`/`XRequest`/`useRecord`、`OpenAIAdapter`、`DetOmsChatAdapter`(+ `DetOmsError` 及配套 DTO 类型)、`createMessageTree`、`createLocalStorageAdapter`/`createMemoryAdapter`、`createSSEParser` 及全部公共类型(`MessageNode`、`ChatAdapter`、`ChatFile`、`MessageUsage`、`SSEEvent` 等),消费方:

```ts
import { FvAiChat, OpenAIAdapter, useChat } from '@fv-ui/ai-chat'
import type { MessageNode, ChatAdapter } from '@fv-ui/ai-chat'
```

---
_Source: https://npm.io/package/@fv-ui/ai-chat · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
