Skip to content

QuickAssist 智能帮助 ​

QuickAssist 是一个与框架无关的智能帮助组件,可以用在需要在页面中进行轻量AI交互的场景。当前提供了“划词”和主动 API 两种触发方式,以及输入交互,用户选中页面中的文字后,可以查看推荐问题、编辑问题,并把问题和有限上下文交给应用已有的 AI 对话面板。它只负责划词触发和输入交互,不包含回答展示、模型调用或完整聊天界面。

安装与引入 ​

NPM 方式 ​

bash
npm install @opentiny/tiny-robot

从 TinyRobot NPM 包的/vanilla 子路径导入,并显式加载组件样式:

ts
import { createQuickAssist } from '@opentiny/tiny-robot/vanilla'
import '@opentiny/tiny-robot/vanilla/style.css'

QuickAssist 的运行时代码不依赖 Vue、Angular 等框架或其他界面组件。可以在任何技术栈项目中单独使用该组件,且产物不会包含任何框架运行时。

CDN 方式 ​

通过 CDN 加载脚本后,使用 window.TinyRobotVanilla 创建组件,无需单独引入样式。将 VERSION 替换为包含 vanilla.global.js 的已发布 TinyRobot 版本号:

html
<script src="https://cdn.jsdelivr.net/npm/@opentiny/tiny-robot@VERSION/dist/vanilla/vanilla.global.js"></script>
<script>
  const quickAssist = window.TinyRobotVanilla.createQuickAssist({
    adapter: {
      submit(request) {
        openAssistantAndSend(request.prompt, request.context)
      },
    },
  })
</script>

也可以将 npm 包中的 dist/vanilla/vanilla.global.js 部署到自己的 CDN 或静态资源目录,再替换上面的脚本地址。此文件是整个无框架组件集合的入口,提供 createQuickAssist、configureVanillaTheme 等 API。

CDN 方式会在创建实例时自动注入组件样式。如果页面的内容安全策略禁止动态注入 <style>,可以使用 npm 方式并显式加载 CSS。

代码示例 ​

基础用法 ​

使用 createQuickAssist 可以创建选词智能帮助的UI实例。支持通过传入参数来进行各类自定义配置。
adapter 参数负责连接应用现有的 AI 对话能力,例如用户发送问题后或者点击推荐问题后显示下一步AI对话窗口。提交时应处理 prompt 和 context;只发送 query 会丢掉推荐问题和页面上下文。

ts
import { createQuickAssist } from '@opentiny/tiny-robot/vanilla'
import '@opentiny/tiny-robot/vanilla/style.css'

const quickAssist = createQuickAssist({
  include: ['#app'],
  adapter: {
    submit(request) {
      // 替换为应用自己的“打开面板并自动发送”逻辑。
      assistantPanel.open()
      assistantChat.send({
        content: request.prompt,
        context: request.context,
      })
    },
  },
  onEvent(event) {
    // 可在宿主接入埋点;事件不包含默认选区文本或完整上下文。
    console.debug('[QuickAssist]', event.type)
  },
})

// 单页应用在路由离开当前页面时调用 quickAssist.close()。
// 页面/应用销毁时调用:
// quickAssist.destroy()

adapter 收到的 AIRequest 包含 source、query、prompt 和脱敏后的 context;点击推荐项时还包含 suggestion。组件先关闭划词界面,再调用 adapter;异步提交失败会通过 error 事件报告,不会重新打开已关闭的界面。

下面的示例使用 TinyRobot 的 TrBubbleList 与 TrSender 呈现对话界面,并用本地模拟回复代替后端。QuickAssist 的 adapter 会打开面板,并把 prompt 与脱敏后的 context 一起交给对话函数。

loading

HTML 接入 ​

下面的 HTML 示例把请求对象安全地显示在模拟对话面板中,不连接实际模型服务。

loading

延迟显示入口 ​

设置 trigger.showDelay 可在选区稳定后延迟显示划词入口,单位为毫秒;默认 0,保持立即显示。连续拖选时从最后一次有效选区重新计时,并等待鼠标松开;选区失效、关闭、停用或销毁时会取消待显示的入口。延迟只影响入口,不影响点击后打开输入浮层。

loading

划词方向与入口位置 ​

默认仅正向划词触发入口,设置 selection.forwardOnly: false 可允许双向划词。方向按文档顺序判断,跨行向后选择仍视为正向,即使结束位置在起始位置左侧;键盘 Shift 选词采用相同规则。自定义 selection.validate 不会覆盖方向限制。此规则面向横排中文等常规阅读场景,不按 RTL 或竖排文字的视觉方向判断。

入口默认显示在最后一行选中文字的右下方,入口左边缘对齐文字右边缘。top 系列位置使用第一行,bottom 系列使用最后一行;同一行的多个内联文本片段合并计算。start 表示入口向文字左侧展开(入口右边缘对齐文字左边缘),end 表示入口向文字右侧展开(入口左边缘对齐文字右边缘),不带后缀则居中;auto 优先在末行下方居中。空间不足时会翻转到另一侧,并使用对应的首行或末行,必要时向视口内调整。

点击入口后,输入浮层直接相对选区使用同一套 placement / offset 规则,默认间距为 8px;根据浮层自身尺寸进行翻转和视口避让。输入框聚焦和输入期间,原划词范围保留灰色高亮,关闭或提交后清除。高亮通过 CSS Custom Highlight API 绘制,不修改原文 DOM;不支持该 API 的浏览器仍可使用划词帮助,但不显示保留高亮。

ts
const quickAssist = createQuickAssist({
  adapter,
  selection: { forwardOnly: true },
  trigger: { placement: 'bottom-end', offset: 8 },
})

// updateOptions 中提供的嵌套配置会整体替换原分组。
quickAssist.updateOptions({
  selection: { forwardOnly: false },
  trigger: { placement: 'top-start' },
})
loading
ts
createQuickAssist({
  adapter,
  trigger: { showDelay: 300 },
})

推荐问题 ​

选中示例中的文字后点击划词入口,可以先看到静态推荐,再看到异步返回的推荐;点击任一推荐可查看提交结果。

loading

通过 suggestions 配置的问题称为“静态问题”,可包含根据选区生成文案的模板;与组件内置的默认推荐问题区分。QuickAssist 支持四种推荐模式:

配置方式打开输入浮层后的行为
省略 suggestions 与 getSuggestions立即显示“解释”和“适用场景”两个默认模板
仅配置 suggestions立即显示静态推荐;显式传 [] 表示不显示默认模板
仅配置 getSuggestions不显示内置默认模板;立即打开输入区,推荐区域显示加载状态,返回后只展示远程结果
两者都配置先显示静态模板,再合并异步推荐;默认静态在前、远程追加,按 id 保留首次出现项,最后由 maxSuggestions 裁剪;可用 mergeSuggestions 自定义覆盖策略

Suggestion 包含稳定的 id、展示用 label 和提交用 prompt。点击推荐项会直接提交,无须再次点击发送。手动输入则把编辑后的内容作为 query;可用 buildPrompt 自定义最终发送内容。

远程推荐覆盖静态问题 ​

如果问题全部由远程接口提供,只需配置 getSuggestions,省略 suggestions,内置的“解释”和“适用场景”模板就不会显示。如果希望加载期间先显示静态问题,远程返回后整体替换,可显式提供静态模板,并通过 mergeSuggestions 只返回远程结果。

下面的示例可切换“仅远程推荐”“远程整体替换”“空结果回退”和“远程优先”四种策略,以及成功、空数组、失败三种响应。示例用延迟模拟远程请求,无需后端即可运行;展开示例源码可查看完整配置。接入业务接口时,将 getSuggestions 中的模拟逻辑替换为 fetch,传入 signal 并返回 Suggestion[] 即可。

loading

“远程整体替换”会在远程成功返回空数组时清空静态推荐;“空结果回退”则保留静态问题。“远程优先”示例中远程与静态的解释问题具有相同 id,可以观察远程问题如何覆盖静态问题,同时保留未重复的静态问题。

请求失败时不会调用 mergeSuggestions,会保留已显示的静态问题并展示加载失败状态,同时触发 stage: 'suggestions' 的错误事件。仅配置远程推荐时没有静态问题可回退,用户仍可手动输入提交。

自定义合并策略 mergeSuggestions ​

mergeSuggestions(base, incoming, context) 是同步回调,在远程推荐成功返回并通过校验后调用。base 是静态推荐经模板求值、按 id 去重并按 maxSuggestions 裁剪后的结果;incoming 是过滤无效项后、尚未去重或裁剪的远程推荐数组;context 是本次推荐使用的脱敏上下文。回调必须返回 Suggestion[]。

默认策略相当于 [...base, ...incoming]:静态项优先,同 id 的远程项不会覆盖静态项;静态项占满数量上限时,远程项可能不展示。可以按业务需要返回不同顺序:

目标回调返回值
远程整体替换静态问题incoming
远程有结果时替换,否则保留静态问题incoming.length ? incoming : base
远程优先,保留未重复的静态问题[...incoming, ...base]
静态优先,追加未重复的远程问题(默认)[...base, ...incoming]

自定义回调返回后,组件仍会校验推荐格式、按 id 保留首次出现项,并应用 maxSuggestions 上限(默认 3)。因此,远程优先策略也可以用来覆盖静态同 id 的问题。

远程问题校验 validateSuggestion ​

组件先检查远程响应是否为数组,再逐项检查 id、label、prompt 是否为非空字符串(纯空白也无效)。格式无效项会被过滤,不影响同批其他有效问题。静态问题及自定义合并结果也会进行格式校验,配置或合并结果无效时会报错。

可选的同步回调 validateSuggestion(suggestion, context): boolean 在基础校验后、合并前逐项执行,仅用于远程问题:

参数或返回值含义
suggestion已通过基础格式校验的单条推荐副本,包含 id、label、prompt 和可选 meta;该回调用于判断是否保留,不用于修改推荐内容
context本次推荐使用的脱敏上下文,可读取 text、页面、附近文本和业务字段
返回 true / false保留 / 丢弃该问题

下面的独立示例并列展示远程原始问题、逐项过滤原因和最终展示问题。选中“竞价计费”后打开智能帮助,查看同一批问题的校验结果;切换“启用业务校验”后重新划词,可对比哪些问题被保留。基础格式校验始终执行,不受该开关影响。

loading

示例业务规则限制标题不超过 40 字、问题不超过 1000 字,并要求问题包含选中文字,可按产品需要替换;组件本身不设置文字长度上限。切换为“全部异常问题”可观察全部无效时的静态问题回退,展开源码可查看完整的 validateSuggestion 配置。

响应不是数组、非空响应全部被过滤、回调抛出异常或返回非布尔值(包括 Promise)时,整批远程推荐失败,不会调用 mergeSuggestions,会保留静态问题并触发 stage: 'suggestions' 的错误事件。仅远程模式仍可手动输入提交。接口主动返回 [] 则属于成功的空结果,继续按合并策略处理,与全部被过滤的情况区分。

ValidateSuggestion 类型可从 @opentiny/tiny-robot/vanilla 导入。AI 原始输出的 JSON 解析、字段映射和缺失 id 的生成应在业务 getSuggestions 中完成,组件接收统一的 Suggestion[]。

页面上下文与敏感数据 ​

选中示例中的虚构租户编号并提交,可查看选区文本、附近文本和业务字段中的编号被统一脱敏后的上下文。另一个标为敏感的区域不能触发划词,也不会进入附近文本。

loading

默认上下文包含:

  • source: 'selection'、text 和 selection.text;
  • 页面标题与 URL;URL 只包含域名和路径,不采集查询参数或片段标识;
  • 限长的 nearbyText,从选区附近的有限语义块提取;找不到语义块时最多退回选中文字。

可以通过 getContext 添加业务字段,通过 getNearbyContext 定制附近文本,并通过 sanitizeContext 返回最终允许外发的完整上下文。sanitizeContext 在业务上下文构造后执行;脱敏完成前不会调用推荐函数或 adapter。

  • sanitizeContext 返回值必须保留 source、text 两个字符串字段,其余字段可删除。
  • 上下文中不要放函数、DOM 对象或共享内存数据,否则会报错并阻止提交。
  • 推荐函数和 buildPrompt 收到上下文副本,修改副本不会改变最终提交的上下文。

下面使用虚构租户编号演示脱敏。输入框可能保留选中的原文,因此还需通过 buildPrompt 对最终发送的问题脱敏;此例的宿主仅发送 prompt 和 context。

ts
const tenantId = 'tenant-demo-4812'
const redactTenant = (text: string) => text.replaceAll(tenantId, '[redacted]')

const quickAssist = createQuickAssist({
  adapter,
  getContext: () => ({ product: '云服务器', section: '创建实例', tenantId }),
  sanitizeContext: (context) => ({
    ...context,
    text: redactTenant(context.text),
    selection: context.selection ? { ...context.selection, text: redactTenant(context.selection.text) } : undefined,
    nearbyText: context.nearbyText ? redactTenant(context.nearbyText) : undefined,
    businessContext: {
      ...context.businessContext,
      tenantId: '[redacted]',
    },
  }),
  buildPrompt: ({ query, suggestion }) => redactTenant(suggestion?.prompt ?? query),
})

默认采集不会读取整页 body.innerText 或表单值。选区与附近文本会跳过常见编辑区和敏感标记,但通用规则不能识别所有业务秘密。请通过 sanitizeContext 删除访问密钥、令牌、用户或租户 ID、订单数据等,并在页面上为敏感区域添加 data-sensitive 或 data-ai-selection="false"。不要把 DOM、Range 或选区快照序列化后传给模型。

生效范围与选择长度 ​

试着选择示例中允许的文字、排除区域,以及超过长度限制的句子,比较划词入口是否出现。

loading
ts
const quickAssist = createQuickAssist({
  adapter,
  include: ['#app'], // 配置后,选区中的每个文本节点都必须位于匹配区域
  exclude: ['[data-qa-ignore]', '.private-value'], // 在内置排除项上追加
  selection: {
    maxTextLength: 300,
    validate(snapshot) {
      return snapshot.text.trim().length > 0
    },
  },
})

默认拒绝空白、纯数字、纯标点/符号、明显 URL、UUID 和超出长度的选区;不会用固定最短长度过滤 ECS、VPC、QoS 等短技术词。selection.validate 自定义文本判断,但仍不能绕过最大长度、include / exclude、编辑器和敏感区域检查。超长选区会被拒绝,不会悄悄截断后发送。

默认排除 input、textarea、select、可编辑的 contenteditable 区域、Monaco、CodeMirror、QuickAssist 自身界面,以及 [data-ai-selection="false"]、[data-sensitive]、[data-secret]、[data-password] 等标记。普通只读 contenteditable="false" 区域可以划词;若它位于可编辑祖先或其他排除区域内,仍会被排除。

支持范围

当前版本暂不支持移动端长按选择,以及 iframe 和 Shadow DOM 内的选区。

主动激活:按钮与自定义选区 ​

通过 activate() 可以主动提供文本和锚点,复用“智能帮助入口 → 输入浮层 → 推荐与提交”的流程。默认先展示入口;传入 open: 'popover' 可跳过入口,直接打开并聚焦输入框。它不会直接显示 AI 答案。

示例默认勾选“直接显示输入框”:点击术语按钮直接提问,悬停或键盘聚焦指标卡片可查看指标专属推荐;浮层打开后可移入其中操作。取消勾选即可对比先显示入口的流程。提交后,下方展示实际收到的内容与来源。

loading

最简单的方式是传入一个 DOM 元素作为锚点:

ts
const explainButton = document.querySelector<HTMLButtonElement>('#explain')!

explainButton.addEventListener('click', () => {
  quickAssist.activate({
    text: '需要解释的代码或业务术语',
    anchor: explainButton,
    open: 'popover', // 直接打开输入框;省略时先显示智能帮助入口
    // source 默认是 manual;可选 meta 会进入上下文脱敏管道。
    meta: { feature: 'code-explanation' },
  })
})

open: 'popover' 立即进入输入浮层,跳过 trigger.showDelay,不产生 trigger_show 或 trigger_click 事件,仍产生 selection 和 popover_show。上下文构造或脱敏未完成时可以编辑,但暂不能发送。open 只影响本次激活,普通页面划词不变。

为本次激活单独提供推荐,无需 updateOptions(),也不影响其他区域:

ts
quickAssist.activate({
  source: 'editor',
  text: selectedCode,
  anchor: codeElement,
  open: 'popover',
  suggestions: [
    {
      id: 'explain-code',
      getLabel: () => '解释这段代码',
      getPrompt: (context) => `请解释以下代码:\n${context.text}`,
    },
    {
      id: 'review-code',
      getLabel: () => '检查潜在问题',
      getPrompt: (context) => `请检查以下代码是否存在潜在问题:\n${context.text}`,
    },
  ],
})
本次激活参数行为
不传 suggestions / getSuggestions使用实例默认推荐配置
suggestions: [...]替换本次的静态推荐;不调用实例的远程推荐
suggestions: []不显示推荐,保留手动提问
getSuggestions: fn使用本次的异步推荐函数,可与本次静态推荐一起配置
getSuggestions: null本次禁用远程推荐,静态推荐仍按对应配置展示

推荐项的结构与实例级 suggestions 一致,支持固定问题与 getLabel / getPrompt 工厂。工厂与远程函数均在上下文脱敏后执行,数量限制、远程校验、合并策略仍使用实例配置;关闭或新激活会中止旧请求、忽略旧结果。固定项在激活时复制,后续不会因原数组被修改而变化。

元素移除、祖先隐藏或尺寸塌缩时会清理当前会话;实例停用或销毁后调用 activate() 不生效。显式再次调用可以重新打开已关闭的内容。

需要将入口放在编辑器选区、图表或自定义画布旁时,使用完整形式:

ts
quickAssist.activate({
  source: 'editor',
  text: selectedCode,
  anchorElement: editorContainer,
  getRect: () => getCurrentSelectionRect(),
  verify: () => isCapturedSelectionStillValid(),
  getNearbyText: () => capturedNearbyCode,
  onInvalidate(notify) {
    // notify 表示需要复检;verify 与 getRect 有效则重定位,否则关闭。
    return subscribeToSelectionAndLayoutChanges(notify)
  },
  meta: { language: 'typescript', fileName: 'price.ts' },
})

getCurrentSelectionRect 等函数由宿主实现,以上是接口示意。矩形使用相对浏览器视口的 CSS 像素坐标,与 getBoundingClientRect() 一致;选区不可见或失效时返回 null,零宽但有高度的 caret 矩形也有效。编辑器的内部坐标需要加上容器的视口坐标,并由宿主判断编辑器自身的裁剪范围。

onInvalidate 从接受激活时开始订阅,覆盖 showDelay、入口和输入浮层阶段,关闭时调用返回的清理函数。未提供该订阅时,仅在滚动、窗口 resize、延迟显示等检查时发现失效;模型或布局变化需由宿主通知,或主动 close()。

text、meta 和 getNearbyText() 的结果在激活时捕获;附近文本受 nearbyContext.maxLength 限制,业务上下文和脱敏仍在打开输入浮层后执行。meta 仅放可 JSON 序列化的业务字段。不要传 DOM、编辑器实例、Range 或访问密钥。

主动激活遵循非空、最大文本长度和几何校验;include / exclude、selection.forwardOnly、旧的 selection.validate 与 getNearbyContext 仅作用于原生 DOM 划词。宿主应自行决定哪些自定义区域可以激活。代码场景可通过 selection.validateActivation 替换默认的纯数字/符号/URL 等文本规则,但不能绕过长度和几何限制:

ts
quickAssist.updateOptions({
  selection: {
    maxTextLength: 2000,
    validateActivation: ({ text }) => text.trim().length > 0,
  },
})

Monaco 选中代码后主动解释 ​

这个示例使用真实 Monaco 编辑器。鼠标拖选并松开,或用 Shift + 方向键完成选区后,页面自动读取编辑器模型并调用 activate(),把“智能帮助”入口定位到选区末端;点击入口后可发送“解释这段代码”。关闭后,同一选区不会因无关 keyup 立即重开,清空后重新选择即可再次触发。代码解释和问题检查通过本次 activate({ suggestions }) 配置,实例继续使用默认推荐。下方示例助手显示实际收到的请求,本例不调用模型。

loading

核心接线如下,完整可运行代码见示例源码。Monaco 需要由应用自行安装,并在浏览器挂载后创建:

ts
let lastIdentity: string | undefined

function explainSelection() {
  const selection = editor.getSelection()
  const model = editor.getModel()
  const container = editor.getDomNode()
  if (!selection || selection.isEmpty() || !model || !container) return

  const text = model.getValueInRange(selection)
  const version = model.getVersionId()
  const identity = `${model.uri}:${version}:${selection.selectionStartLineNumber}:${selection.selectionStartColumn}:${selection.positionLineNumber}:${selection.positionColumn}`
  if (identity === lastIdentity) return
  lastIdentity = identity
  const end = selection.getEndPosition()

  quickAssist.activate({
    source: 'monaco',
    text,
    suggestions: [
      {
        id: 'explain-code',
        getLabel: () => '解释这段代码',
        getPrompt: (context) => `请解释以下代码:\n${context.text}`,
      },
    ],
    anchorElement: container,
    meta: { language: model.getLanguageId() },
    verify: () => editor.getModel() === model && model.getVersionId() === version,
    getRect() {
      const position = editor.getScrolledVisiblePosition(end)
      const layout = editor.getLayoutInfo()
      if (
        !container.isConnected ||
        !container.getClientRects().length ||
        !position ||
        position.left < layout.contentLeft ||
        position.left > layout.contentLeft + layout.contentWidth ||
        position.top < 0 ||
        position.top + position.height > layout.height
      )
        return null
      const box = container.getBoundingClientRect()
      return new DOMRect(box.left + position.left, box.top + position.top, 0, position.height)
    },
  })
}

// 选择完成后调度,让 Monaco 先处理完事件;自动调用对相同选区去重。
const activateWhenSettled = () => requestAnimationFrame(() => explainSelection())
// 简化接线。完整示例还处理拖选期间的门控、指针取消和卸载时取消调度。
const disposables = [
  editor.onMouseUp(activateWhenSettled),
  editor.onKeyUp((event) => {
    if (!event.browserEvent.isComposing) activateWhenSettled()
  }),
  editor.onDidChangeCursorSelection(() => {
    if (editor.getSelection()?.isEmpty()) lastIdentity = undefined
    quickAssist.close()
  }),
  editor.onDidChangeModelContent(() => quickAssist.close()),
  editor.onDidScrollChange(() => quickAssist.close()),
  editor.onDidLayoutChange(() => quickAssist.close()),
]
// 卸载时 disposables.forEach(item => item.dispose()),并销毁助手、编辑器和模型。

普通页面仍默认排除 Monaco、CodeMirror 和输入框;主动调用不依赖解除这些过滤规则。此处的自动激活由示例页面接线实现,当前未提供官方 Monaco / CodeMirror 自动桥接器。

生命周期与路由 ​

以下示例可直接启用或停用划词,并模拟路由切换时调用 close();组件卸载时应调用 destroy()。

loading

Angular ​

在视图创建后初始化实例,并在组件销毁时调用 destroy()。QuickAssist 会自动响应浏览器 popstate 与 hashchange;Angular Router 通常通过 pushState 导航,不一定产生这两个事件,因此应在路由导航开始时调用 close()。

ts
import { AfterViewInit, Component, OnDestroy, inject } from '@angular/core'
import { NavigationStart, Router } from '@angular/router'
import { filter, Subscription } from 'rxjs'
import { createQuickAssist } from '@opentiny/tiny-robot/vanilla'
import '@opentiny/tiny-robot/vanilla/style.css'
import type { QuickAssistInstance } from '@opentiny/tiny-robot/vanilla'

@Component({ selector: 'app-shell', template: '<main id="app">...</main>' })
export class AppShellComponent implements AfterViewInit, OnDestroy {
  private readonly router = inject(Router)
  private routeSubscription?: Subscription
  private quickAssist?: QuickAssistInstance

  ngAfterViewInit() {
    this.quickAssist = createQuickAssist({
      include: ['#app'],
      adapter: {
        submit: (request) => {
          this.openAssistantAndSend(request.prompt, request.context)
        },
      },
    })
    this.routeSubscription = this.router.events
      .pipe(filter((event): event is NavigationStart => event instanceof NavigationStart))
      .subscribe(() => this.quickAssist?.close())
  }

  ngOnDestroy() {
    this.routeSubscription?.unsubscribe()
    this.quickAssist?.destroy()
  }

  private openAssistantAndSend(prompt: string, context: unknown) {
    // 接入应用已有面板与发送方法;不要只把 prompt 留在输入框里。
  }
}

如果 Angular 应用启用 SSR,请只在浏览器生命周期中创建实例。root 可传入 Document,不能传 ShadowRoot。

Vue ​

在 onMounted 中创建,在卸载前 destroy()。Vue Router 的 SPA 导航也建议用 afterEach 或路由监听调用 close()。

ts
import { onBeforeUnmount, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { createQuickAssist } from '@opentiny/tiny-robot/vanilla'
import '@opentiny/tiny-robot/vanilla/style.css'

const router = useRouter()
let quickAssist: ReturnType<typeof createQuickAssist> | undefined
let removeAfterEach: (() => void) | undefined

onMounted(() => {
  quickAssist = createQuickAssist({ adapter })
  removeAfterEach = router.afterEach(() => quickAssist?.close())
})

onBeforeUnmount(() => {
  removeAfterEach?.()
  quickAssist?.destroy()
})

在全局布局组件统一创建可覆盖多页面;如果 QuickAssist 只属于某个视图,则在该视图的挂载/卸载边界创建和销毁。

主题配置 ​

独立入口内置亮暗配色,默认跟随 html[data-tr-color-mode],识别 light、dark,属性缺失或无效时使用亮色。已有 TinyRobot ThemeProvider 默认无需额外配置。

其他系统只需在应用初始化时指定主题来源,无需向组件传入 theme:

ts
import { configureVanillaTheme } from '@opentiny/tiny-robot/vanilla'

configureVanillaTheme({
  source: { type: 'attribute', attribute: 'data-cf-theme' },
})

source 支持以下配置,来源元素须在调用时存在:

字段类型默认值说明
type'attribute' | 'class'必填按属性值或 class 判断主题;省略整个 source 时默认使用属性来源
selectorstring'html'主题来源元素的 CSS 选择器,取首个匹配元素
attributestring'data-tr-color-mode'仅 attribute 类型:监听的属性名
values.lightstring'light'仅 attribute 类型:亮色对应的属性值
values.darkstring'dark'仅 attribute 类型:暗色对应的属性值
darkClassstring'dark'仅 class 类型:暗色 class 名,按完整 token 匹配
lightClassstring—仅 class 类型:可选亮色 class 名;亮暗同时存在时暗色优先
defaultMode'light' | 'dark''light'属性值未知、class 均未匹配或来源元素被移除时的回退模式
loading

也可以跟随 class,或直接指定颜色模式:

ts
configureVanillaTheme({ source: { type: 'class' } }) // 跟随 html.dark,否则亮色
configureVanillaTheme({ colorMode: 'dark' }) // 固定暗色,也支持 light
configureVanillaTheme({ colorMode: 'auto' }) // 跟随操作系统
configureVanillaTheme({}) // 恢复默认 TinyRobot 主题来源

配置按 Document 共享,对已有及后续组件生效,主题变化不会清空正在编辑的问题。每次调用与内置默认值合并,不累积上次配置;source 与 colorMode 不能同时设置。

CDN 方式使用相同 API,脚本加载完成后调用即可:

js
TinyRobotVanilla.configureVanillaTheme({
  source: { type: 'attribute', attribute: 'data-cf-theme' },
})

配色优先级为:组件变量覆盖 → 宿主 --tr-* 变量 → 内置配色。如果加载了 TinyRobot 全量样式,需同步其颜色模式;只使用组件自带配色时,加载 Vanilla CSS 即可(CDN 方式自动注入)。

浮层挂载在 body 下,局部主题来源只同步亮暗状态,不会复制局部品牌变量。自定义变量应放在 html、body 或组件根节点的全局样式中。iframe 需通过 configureVanillaTheme(config, iframeDocument) 配置其自身文档。

交互与限制 ​

  • 划词默认仅正向触发;入口默认出现在选区末行右下方,空间不足时会翻转或调整位置;打开输入浮层后仍会跟随入口对应的首末行位置。
  • 点击入口后输入框立即打开并聚焦,默认填入选中文字;按 Enter 发送,按 Shift+Enter 换行,输入法组合期间不会误发。
  • 等待业务上下文构造或脱敏时,仍可编辑输入,但暂时不能提交;脱敏完成前不会调用推荐函数。
  • 点击推荐项或手动发送后,组件先关闭划词界面,再调用 adapter。
  • 新选区会替换旧会话。点击外部、按 Esc、锚点失效、显著滚动、路由变化或调用生命周期方法时,会清理当前界面。
  • 浏览器的 popstate 和 hashchange 会自动关闭界面;其他单页应用导航应由路由钩子调用 close()。

功能边界

输入浮层不显示 AI 答案,也不提供流式渲染、重试、多轮对话或模型管理;这些仍由应用已有的 AI 面板负责。

Props ​

属性名类型默认值说明
rootDocumentdocument所在文档;不接受 ShadowRoot
enabledbooleantrue是否启用划词监听
adapterAIAdapter—必填;由 submit(request) 交给应用已有的对话能力
includestring[]—限定可触发的页面区域
excludestring[]—在内置排除规则上追加页面区域
selection.maxTextLengthnumber300可选文字的最大长度,必须为正整数
selection.forwardOnlybooleantrue仅正向选词触发;设为 false 允许双向选择
selection.validateActivation(view: QuickAssistActivationView) => boolean默认文本规则仅自定义激活的文本校验,不接收 DOM Range
selection.validate(snapshot) => boolean内置校验替代默认的文本内容校验
trigger.labelstring智能帮助划词入口文案
trigger.showDelaynumber0选区稳定后延迟显示入口,单位为毫秒,须为非负整数
trigger.offsetnumber8入口及输入浮层与选区的间距,单位为像素
trigger.placement'auto' | 'top' | 'bottom' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end''bottom-end'入口及输入浮层的优先位置;上方按首行、下方按末行,空间不足时调整
nearbyContext.maxLengthnumber500附近文本的最大字符数
nearbyContext.blockSelectorsstring[]内置选择器在默认语义块之外增加候选块
getNearbyContext(snapshot) => string | undefined—自定义附近文本;snapshot 含 DOM Range
getContext(context, signal) => Record | Promise<Record>—返回业务字段,合并到 businessContext
sanitizeContext(context, signal) => QuickAssistContext | Promise<QuickAssistContext>—返回最终允许外发的完整上下文
suggestionsQuickAssistSuggestionInput[]默认推荐静态推荐或推荐生成函数
getSuggestions(context, signal) => Suggestion[] | Promise<Suggestion[]>—使用脱敏后的上下文获取异步推荐
validateSuggestion(suggestion: Suggestion, context: QuickAssistContext) => boolean基础格式校验可选业务校验,仅过滤远程问题,在合并前执行,见远程问题校验
mergeSuggestions(base, incoming, context) => Suggestion[]静态优先追加远程成功返回后同步合并;可整体替换或远程优先;最终按 id 保留首次出现项并受数量上限约束,见自定义合并策略
maxSuggestionsnumber3最多展示的推荐数,必须为正整数
buildPrompt(input) => string内置构造自定义提交内容
attrsQuickAssistAttrs—为组件内部元素追加安全的 DOM 属性
messagesQuickAssistMessages内置中文文案覆盖界面文案和默认推荐的展示文字
onEvent(event) => void—事件回调;回调异常不会打断组件清理

attrs.suggestion 可以是属性对象,也可以是按推荐项返回属性对象的函数。事件处理器、style、写入 HTML 的属性、value、disabled、type、id 等会被忽略;class 只能追加,不能覆盖组件内部行为。

Methods ​

方法名参数说明
activateQuickAssistActivationInput | QuickAssistAnchorShortcut提供文本和锚点,显示智能帮助入口;详见主动激活示例
registerSourceQuickAssistSelectionSource注册自动来源,返回注销函数,随实例启停联动
enable—启动划词和全局事件监听;重复调用无副作用
disable—关闭当前输入浮层并停止监听;之后可再次调用 enable()
close—关闭当前会话,不销毁实例
updateOptionspartial: Partial<QuickAssistOptions>关闭当前会话、废弃旧异步结果并重建配置;顶层浅合并,嵌套对象整体替换
destroy—关闭并释放监听器与 DOM;重复调用无副作用

registerSource() 是已实现的高级接口,用于多个自定义事件源的统一启停、注销和来源级去重;它不会自动读取编辑器选区或生成坐标。上面的按钮、指标和 Monaco 示例直接使用 activate(),无需注册来源。没有多来源管理需求时,不必使用此方法。

root 创建后不可更换;updateOptions({ root: anotherDocument }) 会抛出错误,应先销毁旧实例再创建新实例。更新时省略 enabled 会保留实例当前启用/停用状态;显式传入时使用新值。

例如,原来配置 trigger: { label: '帮助', offset: 12 },随后调用 updateOptions({ trigger: { showDelay: 200 } }) 会把标签和偏移恢复到默认值。若要保留它们,需要传入完整分组:updateOptions({ trigger: { label: '帮助', offset: 12, showDelay: 200 } })。selection、nearbyContext、messages 和 attrs 同样遵循整体替换语义。

Events ​

事件名参数说明
selection—接受新的合法选区
trigger_show—展示划词入口
trigger_click—点击入口
popover_show—展示输入浮层
suggestion_clicksuggestionId点击推荐项
submit—提交请求;事件不包含选区文字或发送内容
closereasonoutside、escape、scroll、anchor-invalid、route、submit、disable、destroy、update、manual 或 reselection
errorstage、error异步上下文、推荐、提交内容构造、适配器或配置发生错误

sessionId 是组件实例内的划词会话编号:每次接受新的有效选区时递增,同一次划词从入口显示到关闭的事件使用相同编号,便于关联事件。它不是登录会话、用户 ID 或跨实例持久化的业务标识。配置错误发生在划词前,此时 sessionId 为 0。表中只列出部分事件的其他字段。事件不包含选区原文、query、prompt 或完整上下文;错误对象由应用按自己的日志规则处理。

CSS 变量 ​

QuickAssist 组件支持以下 CSS 变量来自定义样式,可在 .tr-quick-assist 上覆盖。部分默认值使用 TinyRobot 主题变量,未提供主题变量时使用 var() 中的回退值。

变量名说明默认值
--tr-quick-assist-bg入口、输入浮层及按钮的基础背景色var(--tr-container-bg-default, #fff)
--tr-quick-assist-bg-subtle入口悬停时背景色var(--tr-container-bg-default-2, #fafafa)
--tr-quick-assist-text主要文字及发送按钮颜色var(--tr-text-primary, #191919)
--tr-quick-assist-muted输入框占位文字及状态提示文字颜色var(--tr-text-secondary, #595959)
--tr-quick-assist-border边框及滚动条颜色#e6e6e6
--tr-quick-assist-border-hover弹窗及按钮悬停时的边框颜色color-mix(in srgb, var(--tr-quick-assist-bg) 65%, var(--tr-quick-assist-muted))
--tr-quick-assist-accent发送按钮悬停及键盘聚焦时的强调色var(--tr-text-primary, #191919)
--tr-quick-assist-icon-gradient-start星光图标渐变起始色(左上)#7d00ff
--tr-quick-assist-icon-gradient-middle星光图标渐变中间色#b927ff
--tr-quick-assist-icon-gradient-end星光图标渐变结束色(右下)#ff6bb5
--tr-quick-assist-radius输入浮层圆角16px
--tr-quick-assist-pill-radius入口、输入区及推荐问题按钮圆角var(--tr-radius-full, 9999px)
--tr-quick-assist-shadow入口及输入浮层阴影0 2px 8px 0 rgba(0, 0, 0, 0.16)
--tr-quick-assist-z-index组件层级var(--tr-z-index-popover, 1500)