聊天框
AIChatProvider 拥有一个对话场景的状态,AIChatWindow 负责会话列表、消息、输入区、员工和模型选择。容器组件决定窗口放在页面中、侧栏里还是 Dialog 中。
等配置就绪再挂载
NocoBaseAIRootProvider 会异步加载员工和模型,但不会推迟渲染子组件。如果 AIChatProvider 在加载完成前就挂载,界面上显示的是回退默认值,内部的选择和 Transport 却还停留在空配置上,第一次发送会失败或什么都没用上。在已经挂载的聊天上盖一层加载遮罩解决不了这个问题,因为聊天已经在下面初始化了。
所以要在 NocoBaseAIRootProvider 下面用一个读取 useAI() 的组件把整个 AIChatProvider 子树挡住,等配置就绪再挂载。三个信号要分开检查:configurationStatus 是 'ready' 时模型加载仍可能失败,models 里也可能只有一个 configured: false 的占位项,所以单看状态或数组长度都不能证明聊天可以发送。
下面的组件放在应用自己的目录里,比如 client/components/ai-chat-ready-gate.tsx:
import type { ReactNode } from 'react';
import { useAI } from '@/extensions/nocobase-ai/providers';
export function AIChatReadyGate({ children }: { children: ReactNode }) {
const {
configurationStatus,
configurationError,
modelConfigurationError,
employees,
hasEnabledModels,
} = useAI();
if (configurationStatus === 'loading') {
return <p role='status'>Loading AI configuration...</p>;
}
if (configurationStatus === 'error') {
return (
<p role='alert'>
{configurationError?.message ?? 'Unable to load AI configuration.'}{' '}
Check your connection and AI settings, then reload this page.
</p>
);
}
if (!employees.length) {
return <p role='alert'>No AI employees are available.</p>;
}
if (modelConfigurationError) {
return (
<p role='alert'>
{modelConfigurationError.message} Check and enable a model in AI
settings, then reload this page.
</p>
);
}
if (!hasEnabledModels) {
return (
<p role='alert'>
No enabled AI model is available. Configure and enable a model in AI
settings, then reload this page.
</p>
);
}
// 员工和模型都已就绪,这时才挂载聊天
return children;
}
提示文字按应用的语言包本地化。配置出错后的恢复方式是修好配置再刷新页面,不要通过切换员工或模型、或者给聊天加 key 强制重新初始化——那样会丢掉会话状态。
创建嵌入式聊天
import { AIChatWindow, ChatInline } from '@/extensions/nocobase-ai/components';
import { AIChatProvider } from '@/extensions/nocobase-ai/providers';
import { AIChatReadyGate } from '@/components/ai-chat-ready-gate';
export function CustomerAssistant() {
return (
<AIChatReadyGate>
<AIChatProvider
id='customer-assistant'
defaultEmployee='customer-success'
>
<ChatInline className='h-[640px] min-h-0'>
<AIChatWindow enableAttachments />
</ChatInline>
</AIChatProvider>
</AIChatReadyGate>
);
}
id 是场景标识,不是会话 ID。用户可以在同一个场景里创建多次会话,所以它应随页面结构保持稳定。不同聊天区域要使用不同 id。
选择员工和模型
defaultEmployee 填员工的 username,实际使用中不要省略。不传时聊天会打开 employees[0],也就是所有已启用员工中 sort 最小的那个;内置的 atlas 员工 sort 是 0,所以为应用自己员工准备的页面会打开在 atlas 上。这种情况下就绪检查照样通过,第一次发送也能成功,看起来没有任何问题,只是对话的员工不对。
还可以给场景设置网页搜索:
<AIChatProvider
id='customer-assistant'
defaultEmployee='customer-success'
webSearch={false}
>
<ChatInline className='h-[640px] min-h-0'>
<AIChatWindow />
</ChatInline>
</AIChatProvider>
AIChatProvider 初始使用服务返回的第一个可用模型;当前员工开启了自己的模型设置时,改为这个员工允许、并且仍启用的模型中的第一个,选择器也只列出这些模型。用户可以通过 AIChatWindow 的模型选择器切换。需要为某个业务任务固定模型时,在 AIEmployeeTask.model 中使用 { llmService, model },它同样要在员工允许的范围内,否则使用员工允许的第一个模型;llmService 是 config.yml 或 LLM 管理页中的服务 name,不是 Provider 名称。
扩展输入区
AIChatWindow.composerActions 可以增加页面元素选择、网页搜索或应用自己的输入区按钮。附件默认关闭,使用 enableAttachments 开启;实际文件保存在 AI 员工的附件存储中。
onToolCallDecision 只观察 AI Provider 已经处理完成的允许、拒绝或参数修改决定,适合记录审计或触发界面联动。不要在这个回调中重复执行 Tool。
切换容器
ChatSurface 支持 side-panel 和 dialog。切换时只改变 variant,让同一个 AIChatWindow 保持挂载:
<ChatSurface
open={open}
variant={expanded ? 'dialog' : 'side-panel'}
onOpenChange={setOpen}
width={450}
>
<AIChatWindow
headerActions={
<ChatSurfaceActions
expanded={expanded}
onExpandedChange={setExpanded}
onClose={() => setOpen(false)}
/>
}
/>
</ChatSurface>
相关链接