输入框

输入框的文字与状态在 settings 里配置,业务组件放进输入框周围的插槽。

用法

<Chatkit
  sessions={sessions}
  settings={{
    placeholder: '问点什么…',
    composer: {
      disabled: !online,
      disabledReason: '网络已断开,恢复后可继续发送',
      sendTip: '发送消息',
    },
  }}
/>

配置项

配置项类型默认说明
settings.placeholderstring随 locale输入框占位文字。
settings.composer.sendTipstring—发送按钮的提示文字;空草稿禁用时仍可悬停显示。
settings.composer.hiddenbooleanfalse隐藏输入框(只读回放)。
settings.composer.disabledbooleanfalse禁用输入框:不能输入也不能发送。
settings.composer.disabledReasonstring—禁用时替换占位文字显示的原因,比如「网络已断开,恢复后可继续发送」。
settings.composer.allowSendWhileSendingbooleantrue回答生成中是否允许继续发送。发送队列由 ChatKit 管理;设为 false 时,生成中按钮固定为停止,回车变成换行。
settings.maxWidthnumber800消息区与输入框的最大宽度(像素)。

sendTip 是发送按钮的悬停/聚焦提示;草稿为空、按钮禁用时仍可悬停查看,但不能发送。

composer-controls.tsx
import '@volcengine/chatkit/styles.css';

import { Chatkit, ComposerAddAttachment, useChatRuntime } from '@volcengine/chatkit';
import { createMockAdapter } from '@volcengine/chatkit/adapters/mock';
import { useState } from 'react';

import { localUpload } from '../backends/local-upload';

const adapter = createMockAdapter();

export default function ComposerControls() {
  const runtime = useChatRuntime({ sessionId: 'composer-controls', adapter });
  const [customClicks, setCustomClicks] = useState(0);
  if (!runtime) return null;

  return (
    <div style={{ height: '100%', minHeight: 400, display: 'flex', flexDirection: 'column' }}>
      <p>自定义项点击次数:{customClicks}(点击后菜单保持打开)</p>
      <div style={{ flex: 1, minHeight: 0 }}>
        <Chatkit
          runtime={runtime}
          settings={{ composer: { sendTip: '发送消息(空草稿时也可查看提示)' } }}
          features={{
            attachments: {
              upload: localUpload,
              slot: false,
            },
          }}
          slots={{
            'composer.toolbarStart': (
              <ComposerAddAttachment
                items={[
                  { type: 'file' },
                  {
                    type: 'custom',
                    key: 'keep-open',
                    label: '计数并保持菜单打开',
                    keepMenuOpen: true,
                    onClick: () => setCustomClicks(count => count + 1),
                  },
                  { type: 'custom', key: 'disabled', label: '暂不可用', disabled: true },
                ]}
              />
            ),
          }}
        />
      </div>
    </div>
  );
}
发送提示与自定义附件菜单新窗口打开

输入建议

features.promptCompletion 默认关闭。提供候选即可在空输入框启用:

<Chatkit
  runtime={runtime}
  features={{
    promptCompletion: {
      items: [
        { key: 'summary', prompt: '总结这份文档' },
        { key: 'plan', prompt: '帮我制定一份计划' },
      ],
    },
  }}
/>
  1. 空输入框显示候选和「Tab 输入」chip。按无修饰键的 Tab 或激活 chip 只填入草稿,不发送;焦点和光标留在编辑器末尾,可直接输入。
  2. 有多条候选时,刚填入且尚未编辑的草稿可再次按 Tab 或激活「Tab 换一个」chip 来替换。手打、粘贴的相同文字不会被覆盖;只有一条候选时,再按 Tab 正常导航。
  3. 在输入框按 Escape 可退出本次补全,随后 Tab 正常导航;离开输入区后重新聚焦可再次使用。
配置字段默认值用途
features.promptCompletion关闭配置对象启用功能;传 false 关闭。
items必填候选 { key, prompt }[];空数组或只有空白文字时不显示。
rotateIntervalMs5000 毫秒空态自动轮播间隔;设为 false 关闭。
renderChip内置「Tab 输入/Tab 换一个」外观按当前 item、action 返回非交互视觉内容。
chipLayout'auto'满足同行条件时与末行并排;'new-line' 始终另起一行。
onAccept无成功写入后收到 item 和 { trigger, action },用于通知宿主。

空草稿只显示一种提示:

  • 可编辑、有候选:显示候选和 chip,暂时隐藏 settings.placeholder。
  • 可编辑、无候选:显示 placeholder。
  • 禁用:隐藏候选和 chip,不能输入;设置了 settings.composer.disabledReason 则显示禁用原因。

聚焦只暂停轮播,不隐藏候选。禁用原因仅在禁用且草稿为空时显示;已有草稿需要持续提示时,请在输入框外展示。

prompt-completion.tsx
import '@volcengine/chatkit/styles.css';

import { Chatkit, type ChatkitPromptCompletionChipProps, useChatRuntime } from '@volcengine/chatkit';
import { createMockAdapter } from '@volcengine/chatkit/adapters/mock';
import { useState } from 'react';

const adapter = createMockAdapter();
const items = [
  { key: 'summary', prompt: '请总结这段对话的重点' },
  { key: 'plan', prompt: '帮我制定下一步计划' },
  { key: 'questions', prompt: '还有哪些问题值得追问?' },
];
const longItems = [
  {
    key: 'detailed-review',
    prompt:
      '请完整梳理这段对话的背景、目标和已经确认的结论,按照事情发生的顺序说明每一步的依据。' +
      '对于尚未确认的信息,请标出是谁提出了疑问、目前缺少什么证据,以及需要向谁继续核实。' +
      '再把后续行动整理成清单,分别写明负责人、预期交付物、依赖关系和建议完成时间。' +
      '如果讨论中出现相互矛盾的说法,请并列呈现原意并解释可能造成的影响,不要自行猜测最终决定。' +
      '最后给出一段简短摘要,方便没有参加讨论的同事快速理解当前状态和下一步需要关注的风险。',
  },
  {
    key: 'detailed-plan',
    prompt:
      '请基于当前讨论拟定一份可执行的下一步计划,先列出已经具备的输入和仍待补充的资料。' +
      '把任务拆分为可以独立验收的小步骤,说明每一步的负责人、依赖、预计时间和完成标准。' +
      '针对进度延迟、需求变化和协作阻塞分别提出应对方式,并标记需要团队进一步确认的取舍。' +
      '请在计划末尾附上检查清单,让团队在下一次同步时能逐项核对进展、更新风险并决定是否调整优先级。',
  },
];

/** 只返回非交互视觉节点,不能嵌套 button/link/input/可聚焦元素;Chatkit 管理外层按钮。 */
function CustomChip({ item, action }: ChatkitPromptCompletionChipProps) {
  return (
    <span
      style={{
        display: 'inline-flex',
        alignItems: 'center',
        gap: 8,
        padding: '5px 10px',
        color: '#fff',
        background: '#365ac7',
        borderRadius: 999,
        fontSize: 12,
      }}
      data-suggestion={item.key}
    >
      <span>{action === 'fill' ? '使用建议' : '换一条建议'}</span>
      <kbd style={{ fontFamily: 'inherit' }}>Tab</kbd>
      <span aria-hidden="true">→</span>
    </span>
  );
}

export default function PromptCompletionDemo() {
  const [custom, setCustom] = useState(false);
  const [longExample, setLongExample] = useState(false);
  const [lastAcceptance, setLastAcceptance] = useState<{
    key: string;
    trigger: 'tab' | 'click';
    action: 'fill' | 'replace';
  } | null>(null);
  const [paragraphTheme, setParagraphTheme] = useState(false);
  const [alternateTheme, setAlternateTheme] = useState(false);
  const runtime = useChatRuntime({
    sessionId: longExample ? 'prompt-completion-demo-long' : 'prompt-completion-demo',
    adapter,
  });
  if (!runtime) return null;
  return (
    <div style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: 8 }}>
      <style>{`
        .chatkit-wrapper.prompt-box-theme .chatkit-composer-rich-text-paragraph,
        .chatkit-wrapper.prompt-box-theme-alt .chatkit-composer-rich-text-paragraph {
          box-sizing: border-box;
          width: 95%;
          margin-left: 4px;
          padding: 6px 28px 10px 12px;
          border: 2px solid currentColor;
          border-right-width: 5px;
          text-align: center;
        }
        .chatkit-wrapper.prompt-box-theme-alt .chatkit-composer-rich-text-paragraph {
          font-weight: 700;
          letter-spacing: 1px;
          padding-left: 20px;
        }
      `}</style>
      <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 12 }}>
        <label>
          <input
            type="checkbox"
            checked={longExample}
            onChange={event => {
              setLongExample(event.target.checked);
              setLastAcceptance(null);
            }}
          />{' '}
          长候选(切换示例会话,查看多行/滚动)
        </label>
        <label>
          <input type="checkbox" checked={custom} onChange={event => setCustom(event.target.checked)} /> 自定义 chip
          并独立换行
        </label>
        <label>
          <input type="checkbox" checked={paragraphTheme} onChange={event => setParagraphTheme(event.target.checked)} />{' '}
          非对称段落样式
        </label>
        <button type="button" onClick={() => setAlternateTheme(value => !value)} disabled={!paragraphTheme}>
          切换段落主题
        </button>
      </div>
      <output aria-live="polite" data-testid="prompt-last-accept">
        最近一次补全:
        {lastAcceptance ? `${lastAcceptance.action} · ${lastAcceptance.key} · ${lastAcceptance.trigger}` : '尚未填入'}
      </output>
      <div style={{ flex: 1, minHeight: 0 }}>
        <Chatkit
          runtime={runtime}
          root={{ className: paragraphTheme ? (alternateTheme ? 'prompt-box-theme-alt' : 'prompt-box-theme') : '' }}
          features={{
            promptCompletion: {
              items: longExample ? longItems : items,
              rotateIntervalMs: longExample ? false : 5000,
              renderChip: custom ? CustomChip : undefined,
              chipLayout: custom ? 'new-line' : 'auto',
              onAccept: (item, { trigger, action }) => setLastAcceptance({ key: item.key, trigger, action }),
            },
          }}
        />
      </div>
    </div>
  );
}
输入建议:填入与切换新窗口打开

候选随正文高度增长,达到编辑器 max-height 后在同一编辑器视口滚动,chip 留在文字滚动区外。候选以编辑器真实空段落内的视图装饰显示,不进入草稿、发送内容或撤销历史。相同原文、宿主样式与宽度下,提示和填入正文共用段落内容盒、字体、行高及换行,仅候选文字使用较浅的 --chatkit-color-text-tertiary;段落边框的 currentColor 不随之变色。chip 位于段落外,保留独立操作样式。公开的段落盒模型/排版支持范围与例外见主题与样式覆盖。若宿主通过公开的 slots['composer.overlay'] 在输入区放置覆盖层,建议会退到输入框下方;这条物理位置不同的路径不保证同位置对齐。

onAccept(item, context) 只在候选成功写入草稿后通知宿主,返回值不能取消写入;编辑器因长度限制等原因拒绝写入时不会调用。context.trigger: 'tab' 表示输入区的 Tab 快捷键;'click' 表示 chip 原生按钮被激活,包含鼠标/触摸点击和按钮聚焦后按 Enter/Space,并非仅指鼠标。context.action: 'fill' 是首次填入空草稿,'replace' 是切换本功能填入且未编辑的草稿。

宿主可通过 renderChip 传入 React 组件或渲染函数,接收当前 item 和 action(fill / replace),仅渲染非交互的视觉内容;Chatkit 保留外层原生按钮及点击、键盘、焦点和可访问名称。返回内容会嵌在该按钮内,不能包含 button、链接、input、select、textarea 或任何可聚焦后代(包括 tabIndex / contentEditable);不要直接返回设计系统的 Button 组件。ReactNode 类型无法静态识别这些后代,请按此契约接入。设置 chipLayout: 'new-line' 可让 chip 独占候选下方一行。默认 auto 仅在段落左对齐/起始对齐、文字方向非 RTL、候选末尾没有换行、末行仍在编辑器可见区内,且右侧容得下 8px 间距与整个 chip 时将 chip 放在同行;否则另起一行,长文达到编辑器最大高度后留在滚动区外。无需覆盖内部 CSS class。

import type { ChatkitPromptCompletionChipProps } from '@volcengine/chatkit';

// 只返回非交互视觉节点;不要在这里使用设计系统 Button 或可聚焦元素。
function MyChip({ item, action }: ChatkitPromptCompletionChipProps) {
  return (
    <span className="my-prompt-chip" data-suggestion={item.key}>
      {action === 'fill' ? '使用建议' : '换一条建议'} <kbd>Tab</kbd>
    </span>
  );
}

<Chatkit runtime={runtime} features={{ promptCompletion: { items, renderChip: MyChip, chipLayout: 'new-line' } }} />;

候选只在输入区没有焦点、草稿为空、页面可见时自动轮播;输入区获得焦点后候选保持稳定,可用 Tab 或 chip 主动切换。浏览器偏好减少动态效果时停止自动轮播,偏好变化会即时生效;如不需要自动轮播,可设置 rotateIntervalMs: false。功能关闭或组件卸载时也不轮播。草稿有提及、引用、技能或行内上传等结构化节点时不替换;独立附件保留。超过编辑器长度限制的候选不会写入,也不会触发 onAccept。

输入框周围的位置

位置在哪
composer.top输入框上方(输入框外)
composer.bodyTop输入框内、正文上方
composer.overlay盖在正文输入区上的一层
composer.bodyBottom输入框内、正文下方
composer.toolbarStart工具栏左侧
composer.toolbarEnd工具栏右侧,发送按钮之前
composer.sendButton发送按钮(只能放一项)
composer.bottom输入框下方(输入框外)

见插槽。

从别处操作输入框

useComposer() 可以在插槽里的组件,或 <ChatkitProvider> 下的任何组件里使用:填入文字、插入引用、加附件、聚焦、发送。见 Hook 参考。

说明

  • 默认回车发送,Shift+Enter 换行;输入法选词和候选面板的回车先交给对应交互处理。回答生成中允许排队时,有内容可继续发送;关闭 allowSendWhileSending 后,生成中回车换行。
  • 发送被受理(包括进入队列)后清空草稿;被 onSend 拦下、未受理或受理前抛错时保留。受理后这一轮执行失败,不会自动恢复草稿。
  • 上传未完成或失败、正文中存在未处理的附件时,发送会被阻止,需要等待、重试或移除附件。
  • settings.composer.hidden 只隐藏输入框,不会禁用推荐问题、编辑、重新生成或代码中的发送入口。只读页面还需移除这些操作;disabled 也只约束输入框交互。
  • 输入框隐藏或尚未挂载时,useComposer() 的写操作返回 false,读取返回空内容;submit() 的布尔值表示找到了输入框并触发提交,不代表发送已被受理。