Chatbot 对话窗口
开箱可用的 AI 对话窗口:消息流(Markdown 渲染、代码块复制、思考过程、附件、流式光标)、输入区(Enter 发送 / Shift+Enter 换行、输入法组字安全、字数与附件上限、停止生成)、动作条(复制 / 重新生成,键盘与触屏可达)。接口层完全由你承接——send 事件拿到输入,回写 modelValue 即完成闭环。
这个家族住在独立包里
本页组件属于 @wil-works/evoke-chat(2026-09 从 @wil-works/evoke-business-ui 拆出,marked / highlight.js 只在用得上时才进依赖树)。装两个包并各自注册一次即可:
import EvokeBusinessUI from '@wil-works/evoke-business-ui' // 底座:基础组件 + 设计令牌
import '@wil-works/evoke-business-ui/styles'
import EvokeChat from '@wil-works/evoke-chat'
import '@wil-works/evoke-chat/styles'
app.use(EvokeBusinessUI)
app.use(EvokeChat)基础对话
发送后回写消息数组,异步回复用 loading 显示打字态:
流式输出与思考过程
示例统一用 460~520px 的窗口高度:输入区固定约占 96px,正文区吃掉其余空间——高度给太小会只露出半条消息。
配合导出的 useChatEngine 组装流式会话:appendContent 逐段回写正文(状态自动进入 streaming,正文末尾出现流式光标,重解析按帧合并),appendThinkContent 写入思考内容(消息上方出现可折叠的思考块,流式期间自动展开、结束后可收起),completeMessage 收尾并自动记录回答用时。regenerate 事件里删掉旧回复后按上文重新流式输出即可:
不想等流式也能看清思考块的三种形态——思考中(自动展开、内容实时追加)、已深度思考(结束后收起、可点开)、带用时(结束时间点折成 thinkDuration,标题右侧给「(用时 X)」):
消息项上的思考字段:thinking: true 表示正在思考(流式期间强制展开),thinkContent 是思考正文,thinkDuration(毫秒)是思考耗时——由引擎的 appendThinkContent / stopThinking / completeMessage / cancelMessage 自动结算,宿主手写静态数据时也可直接给;thinkInterrupted: true 表示思考阶段就被停止,思考块标题据此改说「思考已中断」。
停止生成与中断态
stoppable 让发送钮在 loading 期间变成停止钮,点击抛 stop(AbortController 由你自持)。中断时用 cancelMessage 把该条消息置为 cancelled 而不是 error:已流出的正文原地保留,下面挂一行「已停止生成」灰标,而不是整段被红色错误块替换;若停的时候还在思考,思考块标题改说「思考已中断」并补上思考用时,不会谎报「已深度思考」。生成中输入框保持可打字,便于准备下一句;Enter 既不并发投递也不会误触中断。
头像、名称与纯文本
user-name / assistant-name 决定双方昵称与头像首字,avatar-user / avatar-assistant 传图片地址可替换为图片头像;render-mode 切换为 text 后消息不再解析 Markdown,适合展示日志、代码原文等纯文本。
show-avatar / show-name 把头像与昵称整块隐去(适合头像已由外层提供、或消息密集的窄栏),传 { user: false } / { assistant: false } 可分侧关闭;时间戳默认就在消息下方,与动作条同排、悬浮消息(或键盘聚焦)时一起出现,show-time 可整块关掉:
也想分侧控制时传对象——只隐去自己那侧(对方仍带头像昵称):
自定义动作与事件埋点
actions 在助手消息的动作条上追加自定义动作({ key, label, icon? },icon 使用组件库图标名);copy / regenerate / action 事件把消息对象透传给页面,在此上报埋点或执行业务逻辑。动作条悬停消息时显示:
试试复制、收藏或重新生成这条消息
追问建议、评价与编辑重发
editable 给你的消息加「编辑」,点开后原地变输入框,保存抛 edit(原文与新文两参);feedback 在助手回答下加点赞点踩,点踩会展开结构化原因与备注面板,提交抛 feedback(消息 + { value, reasons, note });回答尾部的追问 chips 来自消息的 suggestions 字段,点击抛 suggestion-click。三者都由 modelValue 承载状态,宿主负责回写。版面约定:追问 chips、评价、动作条落在消息下方同一行(chips 吃掉剩余宽度、可换行;评价与动作条靠右,动作条仍随悬浮出现);点踩展开原因面板时,评价块整行铺开——面板太窄读不了:
试试:点回答下方的追问 chips / 点赞或点踩 / 悬停你的提问点「编辑」
来源引用与行内上标
消息带 citations 数组即渲染来源卡列表(序号 / favicon / 标题 / 域名 / 摘要,可折叠;条目可带 status 说明定位状态——如「未定位」——与域名同行展示,不必把它拼进标题)。正文里的行内引用用 source: 协议书写——[1](source:c1) 渲染成可点击上标,点击会展开来源列表并高亮对应卡片,同时抛 citation-click:
链接文字是纯数字就直接用作序号;不是数字则按本次渲染递增分配。刻意不做裸 [1] 自动识别——那会和有序列表、脚注、代码里的方括号打架;宿主若拿到的是 Perplexity 风格的裸数字,在自己的 transport 里改写成 [n](source:id) 即可。
工具调用卡
消息带 toolCalls 数组即在正文之前渲染执行轨迹(先执行再作答):每步一行,带状态标记、状态文案与耗时,有参数或结果时可展开。多个步骤自动归到组标题下——跑着说「正在执行 N 个步骤」(带流光),跑完结算成「执行了 N 个步骤(用时 X)」。失败态给重试钮并在折叠行露出错误首行,点击抛 tool-retry(工具调用 + 消息两参)。
配合 useChatEngine 的状态机驱动:startToolCall(msgId, { name, args }) 新建并转执行中(传已存在的 id 则复用),addSubToolCall(msgId, parentCallId, { name, args }) 给某个调用挂子调用(返回子调用 id,与 startToolCall 一致;父不存在或超过 16 层返回 null),这三个写操作都按 id 递归作用到子调用上;appendToolCallResult(msgId, callId, chunk) 逐片回写工具输出(状态自动转执行中并打上 streaming,卡片露出光标、自动展开并贴底),completeToolCall(msgId, callId, result) 收尾并自动记耗时(省略 result 时保留已流出的输出),failToolCall(msgId, callId, error) 转失败。整轮被 cancelMessage 中断时,还在跑/等待的调用会落 cancelled(「已停止」),且不再接收迟到的增量或收尾。参数与结果默认走带环检测的 JSON <pre>,宿主可用 #args / #result 插槽换成 EbJsonViewer 等。
系统提示消息
role 为 system 或 notice 的消息按系统提示呈现:居中一行、弱化底色,不带头像 / 昵称 / 动作条,也不参与复制、评价与编辑。适合「以上为历史对话」「已切换到某模型」这类分隔性说明。正文仍走 Markdown,render-mode="text" 时原样显示。
message 插槽接管整条消息的渲染,作用域参数给 message / index / isLast / itemProps(itemProps 就是 ChatList 本来要传给内部 ChatMessage 的全量 props)。只想让某几类消息长得不一样、其余照旧,用 itemProps 显式回落即可,不必自己重接头像与动作条:
只换正文、保留消息外壳(头像 / 思考块 / 工具卡 / 来源卡 / 动作条)用 message-content,作用域参数给 message / content / renderMode / streaming。两者同时给以 message 为准。EbChatMessage 单独使用时对应插槽名为 content。
一点要注意:插槽产出为空时 Vue 会回落默认渲染(空模板、仅注释、v-if 为假都算空)。所以不能靠「把不想显示的行留空」来隐藏消息——那样得到的是默认渲染,不是空白。
附件与输入控制
max-length 限制输入长度,show-word-count 显示字数;max-attachments 限制附件数量。附件投递支持点 + 选文件、拖拽进输入区、直接粘贴剪贴板里的图片三种方式(allow-drop 可关)。
类型与体积由 accept / max-file-size 约束——浏览器对 input[accept] 只是建议,拖拽和粘贴路径组件会自己按同一套规则校验;不合格的文件经 attachment-reject(file, reason) 交回,reason 取 type / size / limit / empty,提示文案由你决定。
上传状态由你回写附件对象驱动(组件不自己发请求):status: 'uploading' + progress 显示进度条,'error' + error 显示失败原因,'done' 显示已上传;不写 status 的历史附件不出现任何状态位。
自定义区域与实例方法
header 定制面板标题栏,empty 定制空会话占位,tip 替换底部提示,sender-toolbar 在输入框工具栏追加按钮(示例配合 v-model:input-value 受控输入);实例方法 focus() / scrollToBottom(smooth) / reset() 支持外部操控,注意 reset() 不会回写 v-model,受控使用时需同步清空绑定数组:
还没有消息,输入问题开始对话
正文渲染配置
对话正文的 Markdown 管线从包入口导出四个函数,宿主可在挂载前统一调配置:
import {
configureChatMarkdown,
getChatMarkdownConfig,
renderChatMarkdown,
registerHighlightLanguage,
} from '@wil-works/evoke-chat'
// 自定义协议链接的主题色(默认 entity→primary、doc→info、action→danger …)
configureChatMarkdown({ protocolThemes: { ticket: 'warning' } })
// 放行宿主自己的协议;data: 默认不在链接白名单内(聊天内容不可信)
configureChatMarkdown({ standardProtocols: { add: ['https'] } })
// 默认高亮只带 highlight.js/lib/common 的 36 种语言,冷门语言自行注册
import cobol from 'highlight.js/lib/languages/cobol'
registerHighlightLanguage('cobol', cobol)数学公式与图表同理——渲染器由你注入,包内不引这两个依赖:
import katex from 'katex'
import 'katex/dist/katex.min.css' // 样式与字体要自己引
configureChatMarkdown({
// (tex, displayMode) => html;返回的内容按原样插入,转义由你的渲染器负责
math: (tex, display) => katex.renderToString(tex, { displayMode: display, throwOnError: false }),
})
// mermaid 是异步的,塞不进同步渲染管线:围栏保留为代码块,旁边多一个
// 「渲染图表」按钮,点击后调这个函数并把代码块换成返回的 SVG
configureChatMarkdown({
mermaid: async (source) => {
const { svg } = await mermaid.render(`m${Date.now()}`, source)
return svg
},
})不注入就不出现:$x$ 与 $$…$$ 原样保留,mermaid 围栏就是普通代码块。数学的行内识别带两条保守规则——$ 内侧不留空白,且纯数字内容($100 与 $200 这类金额写法)直接放行不当公式。宿主渲染器抛错时退回转义原文,不影响整篇。
安全边界(都是刻意选择,不是漏做):正文里的 raw HTML 一律转义为纯文本展示,<script> / <iframe> / 注释都进不来;链接协议走白名单,非白名单协议降级成可配置的引用芯片;图片 src 只放行 http / https / data,其余协议退回可读纯文本,并统一补 loading="lazy" 与 referrerpolicy="no-referrer"。
脚注:marked v18 不带脚注扩展,管线里自己接了一个。结论[^1] 配 [^1]: 出处说明 渲染成可点击上标 + 文末尾注列表——编号按定义出现顺序(与 GFM 一致),未被引用的定义不进尾注,没有对应定义的引用退回原样文本,代码块里的 [^1]: 不会被当成定义。上标与 source: 引用共用 .eb-chat-citation 类名,宿主可统一着色。
Mermaid 的图是点击后直接改 v-html 出来的 DOM——流式期间重渲染会覆盖它,所以按钮只在已经渲染完成的块上有意义。
接真实后端:会话日志层
默认接法(onSend 里自己回写引擎)在断线、重连、补历史时会丢状态——没有东西记得"读到哪了"。useChatSession 补的就是这一层:宿主只提供一个窄适配器,游标、缺口、幂等发送由它管。
import { useChatSession } from '@wil-works/evoke-chat'
const session = useChatSession({
sessionId: 's-1',
transport: {
open: ({ cursor, onEvent }) => subscribeMyStream({ from: cursor + 1 }, onEvent), // 返回 unsubscribe
page: ({ from, to }) => fetchEvents({ from, to }), // 补页:返回持久事件数组
send: ({ requestId, content, mode }) => rpc('session/prompt', { requestId, content, mode }),
cancel: ({ sessionId }) => rpc('session/cancel', { sessionId }), // 协作式中止
},
})
session.open({ cursor, records }) // 打开/重开:装快照并订阅
session.submit('帮我看下这个报错') // 幂等:requestId 关联乐观气泡,失败可 retrySend 同 id 重发
session.stop() // 停止(本层不持 AbortController)
session.messages // 折叠后的消息数组,直接喂 <eb-chatbot v-model>open() 与生命周期的三条口径:
submit前忘了open()也能跑——首次发送会自动补开并console.warn一声(不补的话走事件出口的流式事件会全部静默丢失,页面停在“生成中”)。但快照装载、断线重连的语义都在open(),正式接入请显式调用。- 切会话 = 再调一次
open()(换快照):旧订阅的迟到事件(在途轮的增量、收尾)由代际守卫就地丢弃,不会在新会话里复活幽灵消息;乐观气泡、审批、提问等瞬态同时清空,新视图从干净状态开始。 - 整个会话不再用了调
session.dispose():断开订阅、丢弃一切后续事件并通知transport.close;之后再open()可复活。组件卸载(scope 销毁)会自动 dispose。
事件契约(宿主把自家 wire 数据映射成这几类,其余类型按 ignorable 处理):
| 事件 | 数据 | 折叠成 |
|---|---|---|
user/message | { requestId?, message: { content, attachments? } } | 用户消息(同 requestId 的乐观气泡自动摘除) |
assistant/delta(瞬时) | { messageId, text?, think? } | appendContent / appendThinkContent |
assistant/progress(瞬时) | { messageId, label, detail?, elapsedMs?, percent? } | setProgress:消息上一行阶段读数(阶段名 + 细节/耗时);收尾态自动清空 |
assistant/progress-log(瞬时) | { messageId, label, detail?, elapsedMs?, percent? } | appendProgress:逐条追加成流水线轨迹(progressLog),当前行联动;≥2 条时历史收进「前 N 个阶段」折叠 |
assistant/message | { messageId, message: { content, thinkContent?, usage?, model?, provider? }, interrupted? } | 落定(model / provider 透传成底部模型归属读数);interrupted 走中断态 |
tool/call | { messageId, callId, name, args? } | 工具卡转执行中 |
tool/args(瞬时) | { messageId, callId, chunk } | appendToolCallArgs:入参 JSON 分片逐片拼接,收尾自动 parse 回对象 |
tool/result | { messageId, callId, result?, resultType?, error?, duration? } | 完成 / 失败;resultType: 'image' / 'file' 换非文本默认渲染;error.code === 'interrupted' 落「已停止」 |
turn/end | { messageId, reason: { kind } } | completed 收尾、max-tokens 截断保留、aborted/blocked 中断、error 红块 |
三条不变量(违反即上报 onViolation,绝不静默硬接):
- 连续性:
seq必须等于上一条 +1;跳号就挂起后到事件并请宿主补页(transport.page),补平才继续——事件顺序永不倒置。 - 游标只被持久事件推进:瞬时通知要显式标
transient: true,不碰游标、不进窗口;没有seq又没这个标记的按坏信封上报。 - 恢复不倒退:
open()之后服务端若从更早的seq重放,判stale-replay违规——静默接受会让窗口出现重影。
不认识的类型:ignorable: true 可安全跳过;否则标记 state.degraded 并回调 onDegraded,由宿主决定是否重拉整窗(不认识的事件可能携带视图状态,跳过会画错)。
纯逻辑内核
createSessionLog()与折叠函数applySessionEvent(engine, event)都单独导出:不用 Vue、不接网络,可以只取日志层,或只在测试里复用折叠规则。
给 AI 读的契约
文档站除了 prose 版的 llms.txt / llms-full.txt,还随站发布两份结构化契约:
| 文件 | 内容 |
|---|---|
| /chat/ai-contract | 硬规则 → 任务配方(含代码与坑)→ 逐组件的 props / 事件 / 插槽 / 实例方法表 |
| /ai/evoke-chat.components.json | 同一份契约的机器可读版(组件 + 具名导出 + 配方) |
两份产物由 packages/evoke-chat/scripts/gen-ai-docs.mjs 从源码抽取,--check 已进构建链:源码改了没重新生成,构建即失败(与文档 API 门同一思路)。
接入 OpenAI / Anthropic
两家模型服务各一个适配器(openai / anthropic),纯映射、零 SDK 依赖:请求体、SSE 解析、wire → 本库事件三件事都在里面,接上 useChatSession 就能跑。
import { createChatTransport, useChatSession } from '@wil-works/evoke-chat'
const transport = createChatTransport({
provider: 'anthropic', // 'openai' | 'anthropic',也可直接给自定义 adapter 对象(见下)
apiKey: '...', // 浏览器直连不安全:生产走你自己的后端代理(url 换成代理地址)
model: 'claude-sonnet-4-5',
system: '你是运营助手',
tools: [{ name: 'web_search', description: '联网检索', parameters: { type: 'object', properties: { query: { type: 'string' } } } }],
contextWindow: 32000, // 给了才会报上下文占用
getMessages: () => session.messages.value, // 历史来源(本库消息数组)
// 标准协议之外的载荷(RAG 引用 / 进度 / 运行卡)在这里拦截:
onExtension: ({ event, data, json }) => {
if (json?.type === 'citations') { session.receive(toCitationEvent(json)); return true }
// 返回 true = 本帧已被扩展消费,跳过标准映射;不返回则照常走 adapter
},
})
const session = useChatSession({ transport })
session.open({ cursor: 0, records: [] })
session.submit('帮我诊断渠道下滑')自研后端不必再手写整条 transport:provider 直接给一个实现了适配器契约的对象(createState / buildRequest / headersOf / frameToEvents / finalize),fetch 错误处理与「abort → turn/end(aborted)」的收尾广播都由 createChatTransport 复用。
映射表(各家差异都在适配器里吸收,宿主不用管):
| 环节 | OpenAI | Anthropic |
|---|---|---|
| 正文 | choices[0].delta.content | content_block_delta.text_delta |
| 思考 | delta.reasoning_content(非官方字段,有则透传) | content_block_delta.thinking_delta |
| 工具参数 | delta.tool_calls[].function.arguments:分片 JSON,按 index 拼完再 parse | content_block_delta.input_json_delta.partial_json:同理 |
| 工具交卡时机 | 流结束 finalize 时一次性交(参数必须拼完) | content_block_stop 时交 |
| 结束语义 | finish_reason:stop→完成、length→截断(保留已产出)、content_filter→失败 | stop_reason:end_turn→完成、max_tokens→截断、refusal→失败 |
| 用量 | usage.prompt_tokens / completion_tokens / total_tokens | message_start.input_tokens + message_delta.output_tokens(分两处,适配器合并) |
| 工具 schema | tools[].function.{name,description,parameters} | tools[].{name,description,input_schema} |
| 历史回灌 | role:'tool' + tool_call_id | tool_result 内容块 |
| 中断 | 客户端 AbortController;abort 会解除 SSE 挂起中的 read(signal 直通解析层,readSseFrames 也接受 { signal }),随后统一补一条 turn/end(aborted) | 同 |
边界(刻意不做的事):这两家没有 follow 流与补页协议,所以适配出来的 transport open 只登记事件出口、page 返回空——历史要宿主自己存(刷新后从你的后端拿);approve / answerQuestion 是应用级交互,不属于 provider 适配。想用完整的会话日志层(游标/缺口补齐/断线恢复),把 open / page 接到你自己的后端即可,其余照旧。
单测用录制的流式 fixture 覆盖:纯文本、思考、分片工具参数、用量、截断、HTTP 错误、中断保留已流出正文(见 test/chat-adapters.test.js)。
多语言
对话家族的文案只有一份,住在本包的语言包里(src/locale/zh-CN.js 与 en.js)。语言名由底座决定:组件读 EbConfigProvider 的 locale(或全局默认),拿到 name(zh-cn / en …)后在本包语言表里取译文,因此切换后已挂载的消息、动作条、输入区就地更新,不必重建组件。
<eb-config-provider :locale="en">
<eb-chatbot :model-value="messages" />
</eb-config-provider>要点:
- 局部生效:
EbConfigProvider只影响子树;不套 Provider 就跟随全局语言(app.use(EvokeBusinessUI, { locale }))。 - 缺失回退:没译文的语言(如
ja/ko)或某条键缺失时回退基准包zh-CN——显示中文,而不是actionbar.copy这样的裸键。 - 宿主传值优先:
placeholder/user-name/assistant-name这类可传入的文案,宿主给了就用宿主的;不传才取当前语言包的默认值(prop 默认值在模块加载时冻结,所以这是渲染期解析的)。 - 语言包入口:
@wil-works/evoke-chat/locale导出zhCN/en与getChatLocalePack(name);底座的语言包在@wil-works/evoke-business-ui/locale。 - 非组件调用:
renderChatMarkdown(content)不传第二参时用基准包静态文案;需要跟随语言的宿主可以自己传renderChatMarkdown(content, labels)。
API
Chatbot Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| modelValue | 消息数组,配合 v-model 使用;项为 { id, role, content, status, thinking?, attachments?, suggestions?, feedback?, feedbackReasons?, feedbackNote?, edited?, citations?, toolCalls? }(toolCalls 项可带 subCalls 形成子调用树;changes 为本轮改动汇总 { files, total?, added?, deleted? }),status 取 pending / streaming / done / error / cancelled | array | [] |
| role 取值 | user / assistant 为对话双方;system 与 notice 是系统提示,居中弱化呈现、不给头像昵称与动作条,也不参与复制与评价 | user | assistant | system | notice | — |
| input-value | 受控输入框内容,配合 v-model:input-value 使用 | string | — |
| loading | 回复生成中( assistant 打字态) | boolean | false |
| render-mode | 消息渲染方式:markdown / 纯文本 | markdown | text | markdown |
| placeholder | 输入框占位文案;不传时取当前语言包(zh-CN 默认为「输入消息,按 Enter 发送,Shift+Enter 换行」) | string | — |
| height / width | 容器尺寸 | string | number | 600px / 100% |
| send-on-enter | Enter 发送、Shift+Enter 换行;关闭后 Enter 换行。输入法组字中的 Enter 始终交还输入法,不会误发 | boolean | true |
| max-length / show-word-count | 输入上限(真正约束 textarea,传 0 不限长)与字数统计 | number / boolean | 2000 / false |
| stoppable | 生成中发送钮切换为停止钮,点击抛 stop | boolean | false |
| approval | 待审批请求 { id, toolName, reason?, detail?, status? };非空时审批面板接管输入区(Enter 允许一次 / Esc 拒绝) | object | null | null |
| question | 待回答请求 { id, items: [...] };审批缺席时提问面板接管输入区(Enter 前进 / Esc 取消),审批优先 | object | null | null |
| status | 状态条 { phase, label?, tool?, elapsed?, hint?, queue? };phase 为 idle/缺省时不渲染 | object | null | null |
| statusStoppable | 当前阶段能否中断(决定状态条是否给「停止」钮) | boolean | false |
| status-queue | 状态条上的排队计数被点击 | () => void | — |
| context | 上下文占用 { used, capacity, breakdown? };给了就在输入区上方显示占用环 | object | null | null |
| allow-attachments / max-attachments | 附件开关与上限 | boolean / number | true / 5 |
| accept | 附件类型白名单(.ext / mime/* / mime/type,逗号分隔);拖拽与粘贴路径同样按它校验 | string | — |
| max-file-size | 单个附件字节上限,0 为不限 | number | 0 |
| allow-drop | 允许拖拽与粘贴投递 | boolean | true |
| show-thinking | 是否展示消息的思考过程折叠块 | boolean | true |
| actions | 消息动作条自定义动作 { key, label, icon? } | array | [] |
| editable | 用户消息可原地编辑并重发(动作条加「编辑」) | boolean | false |
| edit-max-length | 编辑框输入上限,传 0 不限长 | number | 0 |
| feedback | 助手消息显示点赞点踩;点踩展开结构化原因面板 | boolean | false |
| feedback-reasons | 点踩原因词汇表;不传用内置六项 | array | [] |
| tool-retryable | 失败的工具调用卡是否给重试钮 | boolean | true |
| user-name / assistant-name | 双方显示名(同时决定默认头像首字);不传时取当前语言包 | string | 我 / AI助手(随语言包) |
| show-avatar / show-name | 隐去消息头部的头像 / 昵称;传 `{ user: false }` 只隐自己那侧、`{ assistant: false }` 只隐对方(隐藏后头部行按剩余内容决定是否渲染) | boolean | { user?, assistant? } | true / true |
| show-time | 时间戳:默认落在消息下方、与动作条同排,悬浮或键盘聚焦时显示;关掉则完全不渲染 | boolean | true |
| avatar-user / avatar-assistant | 双方头像图片地址 | string | |
| auto-scroll | 新消息自动滚动到底部 | boolean | true |
| show-tip | 是否展示底部提示,可用 #tip 插槽替换 | boolean | true |
| disabled | 整体禁用 | boolean | false |
Chatbot Events
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| send | 发送消息(文本 + 附件),回写 modelValue 完成闭环 | (text: string, attachments: array) => void | — |
| stop | 点击停止钮(stoppable 且生成中),在此中断请求并把该条消息置为 cancelled | () => void | — |
| approval-respond | 审批结论,两参 (outcome, request);outcome 取 allowed-once / rejected | (outcome, request) => void | — |
| question-respond | 提问结论,两参 (answer, request);answer.status 取 answered / cancelled | (answer, request) => void | — |
| copy | 消息复制(动作条透传) | (message) => void | — |
| regenerate | 重新生成(动作条透传,由页面删旧回复并重新请求) | (message) => void | — |
| action | actions 自定义动作点击 | (key: string, message) => void | — |
| edit | 用户消息编辑后保存(组件只交出文本,重发由你驱动引擎 editAndResend 或自行截断) | (message, content: string) => void | — |
| feedback | 评价提交;取消时 payload.value 为 null | (message, { value, reasons, note }) => void | — |
| suggestion-click | 点击回答尾部的追问 chip | (text: string, suggestion, message) => void | — |
| citation-click | 点击正文里的引用上标(来源卡会自动展开并高亮,此处供埋点或自定义跳转) | (id: string, message) => void | — |
| tool-retry | 点击失败工具调用卡的重试钮 | (toolCall, message) => void | — |
| attachment-add | 附件通过校验后触发,第二个参数为组件生成的附件对象(回写 status / progress 用) | (file: File, attachment) => void | — |
| attachment-reject | 附件被拒;reason 取 type / size / limit / empty | (file: File, reason: string) => void | — |
Chatbot Slots
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| header | 面板顶部标题栏 | — | — |
| empty | 空消息占位内容 | — | — |
| message-header | 消息区顶部(仅有消息时渲染) | — | — |
| sender-prepend / sender-append | 输入区左右扩展位 | — | — |
| sender-toolbar | 输入框工具栏(附件按钮右侧) | — | — |
| message | 接管整条消息渲染;作用域参数 { message, index, isLast, itemProps },用 itemProps 可回落默认 ChatMessage。产出为空时 Vue 回落默认渲染 | — | — |
| message-content | 只替换气泡内正文,保留消息外壳;作用域参数 { message, content, renderMode, streaming }。与 message 同时给以 message 为准 | — | — |
| tip | 底部提示内容 | — | 内容由 AI 生成,仅供参考 |
Chatbot Methods(defineExpose)
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| reset | 清空消息与输入区(含附件);不回写 v-model,受控使用需同步清空绑定数组 | () => void | — |
| focus | 聚焦输入框 | () => void | — |
| scrollToBottom | 滚动到消息底部,传 true 平滑滚动 | (smooth?: boolean) => void | — |