@@ -166,7 +158,7 @@ export const ChatBox: React.FC<{
aria-label={t('Debug Panel')}
icon={
}
type="text"
- onClick={() => setShowDebugPanel(!showDebugPanel)}
+ onClick={() => chatBoxModel.setShowDebugPanel(!showDebugPanel)}
/>
) : null}
@@ -180,7 +172,7 @@ export const ChatBox: React.FC<{
icon={
}
type="text"
onClick={() => {
- setMinimize(true);
+ chatBoxModel.setMinimize(true);
}}
/>
@@ -192,9 +184,9 @@ export const ChatBox: React.FC<{
type="text"
onClick={() => {
if (!expanded) {
- setShowDebugPanel(false);
+ chatBoxModel.setShowDebugPanel(false);
}
- setExpanded(!expanded);
+ chatBoxModel.setExpanded(!expanded);
}}
/>
@@ -209,7 +201,7 @@ export const ChatBox: React.FC<{
onClose();
return;
}
- setOpen(false);
+ chatBoxModel.setOpen(false);
}}
/>
@@ -223,20 +215,8 @@ export const ChatBox: React.FC<{
}}
>
-
- {t('AI disclaimer')}
-
);
-};
+});
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/chatbox/components/ChatBoxLayout.tsx b/packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/chatbox/components/ChatBoxLayout.tsx
index 21b9dff5ac6..d2954d43477 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/chatbox/components/ChatBoxLayout.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/chatbox/components/ChatBoxLayout.tsx
@@ -20,34 +20,42 @@ import { AISelection } from '../../AISelection';
import { AISelectionControl } from '../../AISelectionControl';
import { avatars } from '../../avatars';
import { dialogController } from '../../stores/dialog-controller';
-import { useChatBoxStore } from '../stores/chat-box';
-import { useChatToolsStore } from '../stores/chat-tools';
import { useChatConversationActions } from '../hooks/useChatConversationActions';
import { useChatBoxActions } from '../hooks/useChatBoxActions';
import { useAIConfigRepository } from '../../../repositories/hooks/useAIConfigRepository';
import { AI_EMPLOYEE_TRIGGER_TASK_EVENT } from '../../../manager/ai-manager';
import { useT } from '../../../locale';
import type { PluginAIClientV2 } from '../../../plugin';
-import {
- normalizeTriggerTaskOptions,
- type RunJSAIEmployeeTriggerTaskOptions,
-} from '../utils/normalizeTriggerTaskOptions';
+import { normalizeTriggerTaskOptions, type RunJSAIEmployeeTriggerTaskOptions } from '../utils';
+import { getMountedChatBox } from '../stores/mounted-chat-boxes';
+import { ChatBoxRuntimeProvider, getGlobalChatBoxRuntime, useChatBoxRuntime } from '../stores/runtime';
const { Text } = Typography;
export const ChatBoxLayout: React.FC<{
children?: React.ReactNode;
}> = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const ChatBoxLayoutContent: React.FC<{
+ children?: React.ReactNode;
+}> = observer(({ children }) => {
const app = useApp();
const { isMobileLayout } = useMobileLayout();
- const open = useChatBoxStore.use.open();
- const expanded = useChatBoxStore.use.expanded();
- const showDebugPanel = useChatBoxStore.use.showDebugPanel();
- const setOpen = useChatBoxStore.use.setOpen();
- const activeTool = useChatToolsStore.use.activeTool();
+ const { chatBoxModel, chatToolModel } = useChatBoxRuntime();
+ const open = chatBoxModel.open;
+ const expanded = chatBoxModel.expanded;
+ const showDebugPanel = chatBoxModel.showDebugPanel;
+ const activeTool = chatToolModel.activeTool;
const { loadUnreadCounts } = useChatConversationActions();
const { triggerTask } = useChatBoxActions();
const aiConfigRepository = useAIConfigRepository();
+ const t = useT();
const refreshUnreadCounts = useCallback(() => {
loadUnreadCounts().catch(console.error);
@@ -77,6 +85,17 @@ export const ChatBoxLayout: React.FC<{
if (!normalized) {
return undefined;
}
+ const targetChatBoxUid = normalized.chatBoxUid;
+ const targetChatBox = targetChatBoxUid ? getMountedChatBox(targetChatBoxUid) : undefined;
+ if (targetChatBoxUid && !targetChatBox) {
+ notification.error({
+ message: t('AI chat box not found', { uid: targetChatBoxUid }),
+ });
+ return undefined;
+ }
+ if (targetChatBox) {
+ return targetChatBox.triggerTask(normalized);
+ }
return triggerTask(normalized);
})
.catch(console.error);
@@ -88,7 +107,7 @@ export const ChatBoxLayout: React.FC<{
aiManager?.onChatBoxUnmounted();
app.eventBus.removeEventListener(AI_EMPLOYEE_TRIGGER_TASK_EVENT, handler);
};
- }, [aiConfigRepository, app.apiClient, app.eventBus, app.pm, triggerTask]);
+ }, [aiConfigRepository, app.apiClient, app.eventBus, app.pm, t, triggerTask]);
const panelWidth = 450;
const zIndex = 1100;
@@ -128,7 +147,7 @@ html body {
panelWidth={panelWidth}
zIndex={zIndex}
onClose={() => {
- setOpen(false);
+ chatBoxModel.setOpen(false);
}}
/>
) : null}
@@ -138,7 +157,7 @@ html body {
{showDebugPanel ?
: null}
>
);
-};
+});
const ChatBoxWrapper: React.FC<{
expanded: boolean;
@@ -148,7 +167,8 @@ const ChatBoxWrapper: React.FC<{
onClose: () => void;
}> = observer(({ expanded, isMobileLayout, panelWidth, zIndex, onClose }) => {
const { token } = theme.useToken();
- const minimize = useChatBoxStore.use.minimize();
+ const { chatBoxModel } = useChatBoxRuntime();
+ const minimize = chatBoxModel.minimize;
const dialogZIndex = dialogController.shouldHide ? -1 : zIndex;
if (isMobileLayout) {
@@ -232,10 +252,9 @@ const MobileLayoutChatBox: React.FC<{
});
const ChatBoxMinimizeControl: React.FC = () => {
- const currentEmployee = useChatBoxStore.use.currentEmployee();
- const minimize = useChatBoxStore.use.minimize();
- const setMinimize = useChatBoxStore.use.setMinimize();
- const setOpen = useChatBoxStore.use.setOpen();
+ const { chatBoxModel } = useChatBoxRuntime();
+ const currentEmployee = chatBoxModel.currentEmployee;
+ const minimize = chatBoxModel.minimize;
const t = useT();
const [api, contextHolder] = notification.useNotification();
const key = React.useRef(`ai-chat-box-minimize-control-${Date.now()}`);
@@ -257,8 +276,8 @@ const ChatBoxMinimizeControl: React.FC = () => {
type="text"
onClick={(event) => {
event.stopPropagation();
- setOpen(false);
- setMinimize(false);
+ chatBoxModel.setOpen(false);
+ chatBoxModel.setMinimize(false);
}}
/>
@@ -269,7 +288,7 @@ const ChatBoxMinimizeControl: React.FC = () => {
width: 200,
},
onClick() {
- setMinimize(false);
+ chatBoxModel.setMinimize(false);
},
});
} else {
@@ -279,7 +298,7 @@ const ChatBoxMinimizeControl: React.FC = () => {
return () => {
api.destroy(notificationKey);
};
- }, [api, currentEmployeeAvatar, minimize, setMinimize, setOpen, t]);
+ }, [api, chatBoxModel, currentEmployeeAvatar, minimize, t]);
return <>{contextHolder}>;
};
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/chatbox/components/ChatBoxUnreadBadge.tsx b/packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/chatbox/components/ChatBoxUnreadBadge.tsx
new file mode 100644
index 00000000000..4b9d3c1b2b9
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/chatbox/components/ChatBoxUnreadBadge.tsx
@@ -0,0 +1,41 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+import React from 'react';
+import { Badge, type BadgeProps } from 'antd';
+import { observer } from '@nocobase/flow-engine';
+import { useChatBoxRuntime } from '../stores/runtime';
+
+export type ChatBoxUnreadBadgeProps = {
+ children?: React.ReactNode;
+ className?: string;
+ offset?: BadgeProps['offset'];
+ showCount?: boolean;
+};
+
+export const ChatBoxUnreadBadge: React.FC
= observer(
+ ({ children, className, offset, showCount = false }) => {
+ const runtime = useChatBoxRuntime();
+ const unreadCount =
+ runtime.mode === 'block'
+ ? runtime.chatConversationModel.conversations.filter((conversation) => !conversation.read).length
+ : runtime.chatConversationModel.unreadCount + runtime.workflowTaskModel.unreadCount;
+
+ return (
+ 0}
+ offset={offset}
+ >
+ {children}
+
+ );
+ },
+);
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/chatbox/components/ChatButton.tsx b/packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/chatbox/components/ChatButton.tsx
index babb0f4a527..d2b56507428 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/chatbox/components/ChatButton.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/chatbox/components/ChatButton.tsx
@@ -18,7 +18,7 @@ import { useChat } from '../hooks/useChat';
import { useChatBoxActions } from '../hooks/useChatBoxActions';
import { useChatConversationActions } from '../hooks/useChatConversationActions';
import { useWorkflowTasks } from '../hooks/useWorkflowTasks';
-import { useChatBoxStore } from '../stores/chat-box';
+import { useChatBoxRuntime } from '../stores/runtime';
const icon = new URL('../../icon.svg', import.meta.url).toString();
@@ -35,10 +35,9 @@ export const ChatButton: React.FC = observer(() => {
const repository = useAIConfigRepository();
const aiEmployees = repository.aiEmployees;
const [dropdownOpen, setDropdownOpen] = useState(false);
- const open = useChatBoxStore.use.open();
+ const { chatBoxModel } = useChatBoxRuntime();
+ const open = chatBoxModel.open;
const chat = useChat();
- const setOpen = useChatBoxStore.use.setOpen();
- const setReadonly = useChatBoxStore.use.setReadonly();
const [badgeAnimating, setBadgeAnimating] = useState(false);
const prevUnreadCountRef = useRef(0);
const badgeAnimationTimerRef = useRef | null>(null);
@@ -85,9 +84,9 @@ export const ChatButton: React.FC = observer(() => {
}
setBadgeAnimating(false);
setDropdownOpen(false);
- setReadonly(false);
+ chatBoxModel.setReadonly(false);
chat.setResponseLoading(false);
- setOpen(true);
+ chatBoxModel.setOpen(true);
const leaderEmployee = aiEmployees.find((employee) => employee.builtIn && employee.username === 'atlas');
if (leaderEmployee) {
switchAIEmployee(leaderEmployee);
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/chatbox/components/Conversations.tsx b/packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/chatbox/components/Conversations.tsx
index f429c041c8d..ab7a162e279 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/chatbox/components/Conversations.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/chatbox/components/Conversations.tsx
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
App as AntdApp,
Badge,
@@ -27,7 +27,7 @@ import {
theme,
} from 'antd';
import { DeleteOutlined, EditOutlined, FilterOutlined } from '@ant-design/icons';
-import { Conversations as AntConversations, type ConversationsProps } from '@ant-design/x';
+import { Conversations as AntConversations, type ConversationsProps as AntConversationsProps } from '@ant-design/x';
import { useApp } from '@nocobase/client-v2';
import { dayjs } from '@nocobase/utils/client';
import { useT } from '../../../locale';
@@ -37,18 +37,38 @@ import { useChatBoxActions } from '../hooks/useChatBoxActions';
import { useChatConversationActions } from '../hooks/useChatConversationActions';
import { useChatMessageActions } from '../hooks/useChatMessageActions';
import { useWorkflowTasks } from '../hooks/useWorkflowTasks';
-import { useChatBoxStore, type ModelRef } from '../stores/chat-box';
-import { useChatConversationsStore } from '../stores/chat-conversations';
-import { useWorkflowTasksStore } from '../stores/workflow-tasks';
+import { type ModelRef } from '../stores/chat-box';
import type { WorkflowTask } from '../stores/workflow-tasks';
import { useAIConfigRepository } from '../../../repositories/hooks/useAIConfigRepository';
+import { observer } from '@nocobase/flow-engine';
+import { useChatBoxRuntime } from '../stores/runtime';
type RenameTarget = {
key: string;
title: string;
} | null;
-export const Conversations: React.FC = memo(() => {
+export const getConversationItems = (
+ conversations: Conversation[],
+ t: (key: string) => string,
+): AntConversationsProps['items'] => {
+ return conversations.map((item) => {
+ const title = item.title || t('New conversation');
+ return {
+ key: item.sessionId,
+ title,
+ label: title,
+ icon: !item.read ? : undefined,
+ timestamp: item.updatedAt ? new Date(item.updatedAt).getTime() : undefined,
+ };
+ });
+};
+
+export type ConversationsProps = {
+ onOpen?: () => void;
+};
+
+export const Conversations: React.FC = observer(({ onOpen }) => {
const t = useT();
const app = useApp();
const { modal, message } = AntdApp.useApp();
@@ -58,26 +78,22 @@ export const Conversations: React.FC = memo(() => {
const [pendingWorkflowTask, setPendingWorkflowTask] = useState();
const aiConfigRepository = useAIConfigRepository();
const aiEmployeesMap = aiConfigRepository.getAIEmployeesMap();
- const conversations = useChatConversationsStore.use.conversations();
- const currentConversation = useChatConversationsStore.use.currentConversation();
- const conversationSegmented = useChatConversationsStore.use.conversationSegmented();
- const setConversationSegmented = useChatConversationsStore.use.setConversationSegmented();
- const keyword = useChatConversationsStore.use.keyword();
- const setKeyword = useChatConversationsStore.use.setKeyword();
- const setCurrentConversation = useChatConversationsStore.use.setCurrentConversation();
- const setCurrentEmployee = useChatBoxStore.use.setCurrentEmployee();
- const setReadonly = useChatBoxStore.use.setReadonly();
- const setShowConversations = useChatBoxStore.use.setShowConversations();
- const setModel = useChatBoxStore.use.setModel();
- const expanded = useChatBoxStore.use.expanded();
- const setCurrentWorkflowTask = useWorkflowTasksStore.use.setCurrentWorkflowTask();
+ const runtime = useChatBoxRuntime();
+ const { chatBoxModel, chatConversationModel, workflowTaskModel } = runtime;
+ const showWorkflowTasks = runtime.mode !== 'block';
+ const conversations = chatConversationModel.conversations;
+ const currentConversation = chatConversationModel.currentConversation;
+ const conversationSegmented = chatConversationModel.conversationSegmented;
+ const activeSegmented = showWorkflowTasks ? conversationSegmented : 'conversations';
+ const keyword = chatConversationModel.keyword;
+ const expanded = chatBoxModel.expanded;
const {
refresh,
runSearch: runSearchConversations,
conversationsService,
lastConversationRef,
unreadCount: unreadConversationCount,
- } = useChatConversationActions();
+ } = useChatConversationActions(runtime);
const {
refresh: refreshWorkflowTasks,
runSearch: runSearchWorkflowTasks,
@@ -91,10 +107,10 @@ export const Conversations: React.FC = memo(() => {
unreadCount: unreadWorkflowTaskCount,
acceptWorkflowTask,
getWorkflowTaskBySession,
- } = useWorkflowTasks();
- const { loadMessages, getConversationLLMActiveState, resumeStream } = useChatMessageActions();
- const { startNewConversation, clear } = useChatBoxActions();
- const chat = useChat(currentConversation);
+ } = useWorkflowTasks(runtime);
+ const { loadMessages, getConversationLLMActiveState, resumeStream } = useChatMessageActions(runtime);
+ const { startNewConversation, clear } = useChatBoxActions(runtime);
+ const chat = useChat(currentConversation, runtime);
const latestOpenVersionRef = useRef(0);
const hasActiveStream = useCallback(
@@ -116,7 +132,7 @@ export const Conversations: React.FC = memo(() => {
!aiEmployee ||
hasActiveStream(sessionId) ||
latestOpenVersionRef.current !== openVersion ||
- useChatConversationsStore.getState().currentConversation !== sessionId
+ chatConversationModel.currentConversation !== sessionId
) {
return;
}
@@ -125,7 +141,7 @@ export const Conversations: React.FC = memo(() => {
if (
hasActiveStream(sessionId) ||
latestOpenVersionRef.current !== openVersion ||
- useChatConversationsStore.getState().currentConversation !== sessionId ||
+ chatConversationModel.currentConversation !== sessionId ||
llmActiveState !== 'streaming'
) {
return;
@@ -136,7 +152,7 @@ export const Conversations: React.FC = memo(() => {
aiEmployee,
});
},
- [aiEmployeesMap, getConversationLLMActiveState, hasActiveStream, loadMessages, resumeStream],
+ [aiEmployeesMap, chatConversationModel, getConversationLLMActiveState, hasActiveStream, loadMessages, resumeStream],
);
useEffect(() => {
@@ -144,12 +160,12 @@ export const Conversations: React.FC = memo(() => {
}, [aiConfigRepository]);
useEffect(() => {
- if (conversationSegmented === 'conversations') {
+ if (activeSegmented === 'conversations') {
refresh();
} else {
refreshWorkflowTasks();
}
- }, [conversationSegmented, refresh, refreshWorkflowTasks]);
+ }, [activeSegmented, refresh, refreshWorkflowTasks]);
useEffect(() => {
const lastItem = listRef.current?.querySelector('.ant-conversations-item:last-child');
@@ -173,19 +189,20 @@ export const Conversations: React.FC = memo(() => {
}, [currentConversation, pendingWorkflowTask]);
const openConversation = useCallback(
- (sessionId: string, username?: string, model?: ModelRef) => {
+ (sessionId: string, username?: string, model?: ModelRef | null) => {
if (sessionId === currentConversation) {
- setShowConversations(false);
+ chatBoxModel.setShowConversations(false);
+ onOpen?.();
return;
}
const conversation = conversations.find((item) => item.sessionId === sessionId);
- setCurrentConversation(sessionId);
+ chatConversationModel.setCurrentConversation(sessionId);
const aiEmployee = username ? aiEmployeesMap[username] : conversation?.aiEmployee;
if (username) {
- setCurrentEmployee(aiEmployee);
+ chatBoxModel.setCurrentEmployee(aiEmployee);
} else {
- setCurrentEmployee(conversation?.aiEmployee);
+ chatBoxModel.setCurrentEmployee(conversation?.aiEmployee);
}
const sessionChat = chat.for(sessionId);
const sessionState = sessionChat.getState();
@@ -204,27 +221,27 @@ export const Conversations: React.FC = memo(() => {
sessionChat.setMessages([]);
clear(undefined, sessionId);
}
- setModel(model ?? (conversation ? getConversationModel(conversation) : null));
+ chatBoxModel.setModel(model ?? (conversation ? getConversationModel(conversation) : null));
if (!shouldReuseLocalSession) {
resumeAfterLoad(sessionId, aiEmployee?.username).catch(console.error);
}
if (!expanded) {
- setShowConversations(false);
+ chatBoxModel.setShowConversations(false);
}
+ onOpen?.();
},
[
aiEmployeesMap,
+ chatBoxModel,
chat,
clear,
conversations,
currentConversation,
expanded,
hasActiveStream,
+ onOpen,
resumeAfterLoad,
- setCurrentConversation,
- setCurrentEmployee,
- setModel,
- setShowConversations,
+ chatConversationModel,
],
);
@@ -234,16 +251,16 @@ export const Conversations: React.FC = memo(() => {
try {
await acceptWorkflowTask(sessionId);
const task = await getWorkflowTaskBySession(sessionId);
- setReadonly(task?.readonly === true);
+ chatBoxModel.setReadonly(task?.readonly === true);
chat.for(sessionId).setResponseLoading(task?.status === 'processing');
- setShowConversations(false);
+ chatBoxModel.setShowConversations(false);
openConversation(sessionId, task?.config?.username, task?.config?.model ?? undefined);
} catch (error) {
setPendingWorkflowTask(undefined);
throw error;
}
},
- [acceptWorkflowTask, chat, getWorkflowTaskBySession, openConversation, setReadonly, setShowConversations],
+ [acceptWorkflowTask, chat, chatBoxModel, getWorkflowTaskBySession, openConversation],
);
const deleteConversation = useCallback(
@@ -269,18 +286,8 @@ export const Conversations: React.FC = memo(() => {
[deleteConversation, modal, t],
);
- const items = useMemo(
- () =>
- conversations.map((item) => {
- const title = item.title || t('New conversation');
- return {
- key: item.sessionId,
- title,
- label: title,
- icon: !item.read ? : undefined,
- timestamp: item.updatedAt ? new Date(item.updatedAt).getTime() : undefined,
- };
- }),
+ const items = useMemo(
+ () => getConversationItems(conversations, t),
[conversations, t],
);
@@ -332,18 +339,18 @@ export const Conversations: React.FC = memo(() => {
{
- setKeyword(event.target.value);
+ chatConversationModel.setKeyword(event.target.value);
}}
placeholder={t('Search')}
onSearch={(value) => {
- if (conversationSegmented === 'conversations') {
+ if (activeSegmented === 'conversations') {
runSearchConversations(value);
} else {
runSearchWorkflowTasks(value);
}
}}
onClear={() => {
- if (conversationSegmented === 'conversations') {
+ if (activeSegmented === 'conversations') {
runSearchConversations('');
} else {
runSearchWorkflowTasks('');
@@ -351,34 +358,36 @@ export const Conversations: React.FC = memo(() => {
}}
allowClear
/>
-
- {t('Conversations')}
-
-
- ),
- value: 'conversations',
- },
- {
- label: (
-
- {t('Workflow tasks')}
-
-
- ),
- value: 'workflowTasks',
- },
- ]}
- value={conversationSegmented}
- onChange={(value) => {
- setConversationSegmented(String(value));
- }}
- />
+ {showWorkflowTasks ? (
+
+ {t('Conversations')}
+
+
+ ),
+ value: 'conversations',
+ },
+ {
+ label: (
+
+ {t('Workflow tasks')}
+
+
+ ),
+ value: 'workflowTasks',
+ },
+ ]}
+ value={conversationSegmented}
+ onChange={(value) => {
+ chatConversationModel.setConversationSegmented(String(value));
+ }}
+ />
+ ) : null}