mirror of
https://github.com/nocobase/nocobase.git
synced 2026-09-19 10:54:38 +08:00
refactor(ai): optimize the API request logic (#8714)
* refactor(ai): optimize the API request logic * fix: build * chore: optimize * chore: optimize * chore: optimize * chore: optimize * chore: optimize * chore: optimize * fix: useMemo
This commit is contained in:
@@ -40,4 +40,3 @@ export class DefaultToolsManager implements ToolsManager {
|
||||
}
|
||||
|
||||
export * from './types';
|
||||
export * from './hooks';
|
||||
|
||||
@@ -12,15 +12,14 @@ import { ensureModel } from '../../ai-employees/chatbox/model';
|
||||
|
||||
describe('chatbox model recovery', () => {
|
||||
it('resolves model when current selection is missing (historical conversation case)', async () => {
|
||||
const llmServicesRepository = {
|
||||
services: [
|
||||
const aiConfigRepository = {
|
||||
getLLMServices: vi.fn(() => [
|
||||
{
|
||||
llmService: 'svc-openai',
|
||||
llmServiceTitle: 'OpenAI',
|
||||
enabledModels: [{ label: 'GPT-4o', value: 'gpt-4o' }],
|
||||
},
|
||||
],
|
||||
load: vi.fn(async () => undefined),
|
||||
]),
|
||||
};
|
||||
const api = {
|
||||
storage: {
|
||||
@@ -31,27 +30,26 @@ describe('chatbox model recovery', () => {
|
||||
|
||||
const result = await ensureModel({
|
||||
api,
|
||||
llmServicesRepository: llmServicesRepository as any,
|
||||
aiConfigRepository: aiConfigRepository as any,
|
||||
username: 'orin',
|
||||
currentOverride: null,
|
||||
onResolved,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ llmService: 'svc-openai', model: 'gpt-4o' });
|
||||
expect(llmServicesRepository.load).toHaveBeenCalledTimes(1);
|
||||
expect(aiConfigRepository.getLLMServices).toHaveBeenCalledTimes(1);
|
||||
expect(onResolved).toHaveBeenCalledWith({ llmService: 'svc-openai', model: 'gpt-4o' });
|
||||
});
|
||||
|
||||
it('keeps current model when it is still valid', async () => {
|
||||
const llmServicesRepository = {
|
||||
services: [
|
||||
const aiConfigRepository = {
|
||||
getLLMServices: vi.fn(() => [
|
||||
{
|
||||
llmService: 'svc-openai',
|
||||
llmServiceTitle: 'OpenAI',
|
||||
enabledModels: [{ label: 'GPT-4o', value: 'gpt-4o' }],
|
||||
},
|
||||
],
|
||||
load: vi.fn(async () => undefined),
|
||||
]),
|
||||
};
|
||||
const api = {
|
||||
storage: {
|
||||
@@ -63,7 +61,7 @@ describe('chatbox model recovery', () => {
|
||||
|
||||
const result = await ensureModel({
|
||||
api,
|
||||
llmServicesRepository: llmServicesRepository as any,
|
||||
aiConfigRepository: aiConfigRepository as any,
|
||||
username: 'orin',
|
||||
currentOverride,
|
||||
onResolved,
|
||||
|
||||
+5
-11
@@ -12,16 +12,12 @@ import { AISelectionProvider } from './1.x/selector/AISelectorProvider';
|
||||
import { AISettingsProvider } from './AISettingsProvider';
|
||||
import { ChatBoxLayout } from './chatbox/ChatBoxLayout';
|
||||
import { AISelection } from './AISelection';
|
||||
import { ContextAwareTooltip } from './ContextAwareTooltip';
|
||||
import { AISelectionControl } from './AISelectionControl';
|
||||
import { CurrentUserContext, ToolsProvider, useApp } from '@nocobase/client';
|
||||
import { CurrentUserContext } from '@nocobase/client';
|
||||
|
||||
export const AIEmployeesProvider: React.FC<{
|
||||
children: React.ReactNode;
|
||||
}> = (props) => {
|
||||
const app = useApp();
|
||||
const { toolsManager } = app.aiManager;
|
||||
|
||||
const currentUserCtx = useContext(CurrentUserContext);
|
||||
if (!currentUserCtx?.data?.data) {
|
||||
return <>{props.children}</>;
|
||||
@@ -30,12 +26,10 @@ export const AIEmployeesProvider: React.FC<{
|
||||
return (
|
||||
<AISelectionProvider>
|
||||
<AISettingsProvider>
|
||||
<ToolsProvider toolsManager={toolsManager}>
|
||||
<ChatBoxLayout>{props.children}</ChatBoxLayout>
|
||||
{/* <ContextAwareTooltip /> */}
|
||||
<AISelection />
|
||||
<AISelectionControl />
|
||||
</ToolsProvider>
|
||||
<ChatBoxLayout>{props.children}</ChatBoxLayout>
|
||||
{/* <ContextAwareTooltip /> */}
|
||||
<AISelection />
|
||||
<AISelectionControl />
|
||||
</AISettingsProvider>
|
||||
</AISelectionProvider>
|
||||
);
|
||||
|
||||
+8
-3
@@ -13,12 +13,17 @@ import { useT } from '../locale';
|
||||
import { Tooltip, Avatar, Flex } from 'antd';
|
||||
import { contextAware } from './stores/context-aware';
|
||||
import { avatars } from './avatars';
|
||||
import { useAIEmployeesData } from './hooks/useAIEmployeesData';
|
||||
import { useToken } from '@nocobase/client';
|
||||
import { useRequest, useToken } from '@nocobase/client';
|
||||
import { useAIConfigRepository } from '../repositories/hooks/useAIConfigRepository';
|
||||
import { AIEmployee } from './types';
|
||||
|
||||
export const ContextAwareTooltip: React.FC = observer(() => {
|
||||
const t = useT();
|
||||
const { aiEmployeesMap } = useAIEmployeesData();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
useRequest<AIEmployee[]>(async () => {
|
||||
return aiConfigRepository.getAIEmployees();
|
||||
});
|
||||
const aiEmployeesMap = aiConfigRepository.getAIEmployeesMap();
|
||||
const { token } = useToken();
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
|
||||
@@ -13,14 +13,14 @@ import { useField } from '@formily/react';
|
||||
import { Field } from '@formily/core';
|
||||
import { useCollectionRecordData, useDataBlockRequest, useDataBlockResource } from '@nocobase/client';
|
||||
import { useT } from '../../locale';
|
||||
import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
|
||||
import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
|
||||
|
||||
export const EnableSwitch: React.FC = () => {
|
||||
const field = useField<Field>();
|
||||
const record = useCollectionRecordData();
|
||||
const resource = useDataBlockResource();
|
||||
const { refresh } = useDataBlockRequest();
|
||||
const { refresh: refreshAIEmployees } = useAIEmployeesData();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const { message } = App.useApp();
|
||||
const t = useT();
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -34,7 +34,7 @@ export const EnableSwitch: React.FC = () => {
|
||||
});
|
||||
message.success(t('Saved successfully'));
|
||||
refresh();
|
||||
refreshAIEmployees();
|
||||
await aiConfigRepository.refreshAIEmployees();
|
||||
} catch (error) {
|
||||
message.error(t('Failed to update'));
|
||||
} finally {
|
||||
|
||||
+12
-4
@@ -11,9 +11,11 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import { List, Button, Dropdown, Tooltip, Space, Segmented, Flex, Collapse, Switch } from 'antd';
|
||||
import { PlusOutlined, QuestionCircleOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { useT } from '../../locale';
|
||||
import { SchemaComponent, useCollectionRecordData, useToken, useTools } from '@nocobase/client';
|
||||
import { SchemaComponent, useCollectionRecordData, useToken } from '@nocobase/client';
|
||||
import { Schema, useField } from '@formily/react';
|
||||
import { Field } from '@formily/core';
|
||||
import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
|
||||
import { observer } from '@nocobase/flow-engine';
|
||||
|
||||
export const SkillsListItem: React.FC<{
|
||||
name: string;
|
||||
@@ -53,14 +55,20 @@ export const SkillsListItem: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
export const Skills: React.FC = () => {
|
||||
export const Skills: React.FC = observer(() => {
|
||||
const t = useT();
|
||||
const { token } = useToken();
|
||||
const field = useField<Field>();
|
||||
const { tools = [], loading } = useTools();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const loading = aiConfigRepository.aiToolsLoading;
|
||||
const tools = aiConfigRepository.aiTools;
|
||||
const record = useCollectionRecordData();
|
||||
const isBuiltIn = record?.builtIn;
|
||||
|
||||
useEffect(() => {
|
||||
aiConfigRepository.getAITools();
|
||||
}, [aiConfigRepository]);
|
||||
|
||||
const handleAdd = (name: string) => {
|
||||
const skills = [...(field.value || [])];
|
||||
if (!skills.some((s) => s.name === name)) {
|
||||
@@ -365,7 +373,7 @@ export const Skills: React.FC = () => {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export const SkillSettings: React.FC = () => {
|
||||
const t = useT();
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useT } from '../../locale';
|
||||
import { useForm } from '@formily/react';
|
||||
import { createForm } from '@formily/core';
|
||||
import { uid } from '@formily/shared';
|
||||
import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
|
||||
import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
|
||||
|
||||
export const useCreateFormProps = () => {
|
||||
const t = useT();
|
||||
@@ -80,7 +80,7 @@ export const useCreateActionProps = () => {
|
||||
const form = useForm();
|
||||
const api = useAPIClient();
|
||||
const { refresh } = useDataBlockRequest();
|
||||
const { refresh: refreshAIEmployees } = useAIEmployeesData();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const t = useT();
|
||||
|
||||
return {
|
||||
@@ -95,7 +95,7 @@ export const useCreateActionProps = () => {
|
||||
message.success(t('Saved successfully'));
|
||||
setVisible(false);
|
||||
form.reset();
|
||||
refreshAIEmployees();
|
||||
await aiConfigRepository.refreshAIEmployees();
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -106,7 +106,7 @@ export const useEditActionProps = () => {
|
||||
const form = useForm();
|
||||
const resource = useDataBlockResource();
|
||||
const { refresh } = useDataBlockRequest();
|
||||
const { refresh: refreshAIEmployees } = useAIEmployeesData();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const collection = useCollection();
|
||||
const filterTk = collection.getFilterTargetKey();
|
||||
const t = useT();
|
||||
@@ -134,7 +134,7 @@ export const useEditActionProps = () => {
|
||||
message.success(t('Saved successfully'));
|
||||
setVisible(false);
|
||||
form.reset();
|
||||
refreshAIEmployees();
|
||||
await aiConfigRepository.refreshAIEmployees();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
+128
-121
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
|
||||
import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
|
||||
import { useChatBoxStore } from '../chatbox/stores/chat-box';
|
||||
import { useChatBoxActions } from '../chatbox/hooks/useChatBoxActions';
|
||||
import { Avatar, Popover, Tooltip } from 'antd';
|
||||
@@ -16,7 +16,7 @@ import { useChatMessagesStore } from '../chatbox/stores/chat-messages';
|
||||
import { ProfileCard } from '../ProfileCard';
|
||||
import { avatars } from '../avatars';
|
||||
import { EditorRef } from '@nocobase/client';
|
||||
import { useFlowContext } from '@nocobase/flow-engine';
|
||||
import { observer, useFlowContext } from '@nocobase/flow-engine';
|
||||
import { isEngineer } from '../built-in/utils';
|
||||
import { Task } from '../types';
|
||||
import { useT } from '../../locale';
|
||||
@@ -30,122 +30,60 @@ export interface AICodingButtonProps {
|
||||
setActive: (key: string, active: boolean) => void;
|
||||
}
|
||||
|
||||
export const AICodingButton: React.FC<AICodingButtonProps> = ({ uid, scene, language, editorRef, setActive }) => {
|
||||
const t = useT();
|
||||
const { aiEmployees } = useAIEmployeesData();
|
||||
const open = useChatBoxStore.use.open();
|
||||
const currentEmployee = useChatBoxStore.use.currentEmployee();
|
||||
const { triggerTask } = useChatBoxActions();
|
||||
const addContextItems = useChatMessagesStore.use.addContextItems();
|
||||
const setEditorRef = useChatMessagesStore.use.setEditorRef();
|
||||
const setCurrentEditorRefUid = useChatMessagesStore.use.setCurrentEditorRefUid();
|
||||
const ctx = useFlowContext();
|
||||
export const AICodingButton: React.FC<AICodingButtonProps> = observer(
|
||||
({ uid, scene, language, editorRef, setActive }) => {
|
||||
const t = useT();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const aiEmployees = aiConfigRepository.aiEmployees;
|
||||
const open = useChatBoxStore.use.open();
|
||||
const currentEmployee = useChatBoxStore.use.currentEmployee();
|
||||
const { triggerTask } = useChatBoxActions();
|
||||
const addContextItems = useChatMessagesStore.use.addContextItems();
|
||||
const setEditorRef = useChatMessagesStore.use.setEditorRef();
|
||||
const setCurrentEditorRefUid = useChatMessagesStore.use.setCurrentEditorRefUid();
|
||||
const ctx = useFlowContext();
|
||||
|
||||
const aiEmployee = aiEmployees.filter((e) => isEngineer(e))[0];
|
||||
const aiEmployee = aiEmployees.filter((e) => isEngineer(e))[0];
|
||||
|
||||
useEffect(() => {
|
||||
setEditorRef(uid, editorRef);
|
||||
setCurrentEditorRefUid(uid);
|
||||
return () => {
|
||||
setEditorRef(uid, null);
|
||||
};
|
||||
}, [uid, editorRef, setEditorRef, setCurrentEditorRefUid]);
|
||||
useEffect(() => {
|
||||
aiConfigRepository.getAIEmployees();
|
||||
}, [aiConfigRepository]);
|
||||
|
||||
useEffect(() => {
|
||||
if (aiEmployee) {
|
||||
setActive('AICodingButton', true);
|
||||
} else {
|
||||
setActive('AICodingButton', false);
|
||||
}
|
||||
}, [aiEmployee, setActive]);
|
||||
useEffect(() => {
|
||||
setEditorRef(uid, editorRef);
|
||||
setCurrentEditorRefUid(uid);
|
||||
return () => {
|
||||
setEditorRef(uid, null);
|
||||
};
|
||||
}, [uid, editorRef, setEditorRef, setCurrentEditorRefUid]);
|
||||
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [errorOccurred, setErrorOccurred] = useState(false);
|
||||
useEffect(() => {
|
||||
const isError = editorRef.logs.find((log) => log.level === 'error') !== undefined;
|
||||
setErrorOccurred(isError);
|
||||
setShowTooltip(isError);
|
||||
if (isError) {
|
||||
setTimeout(() => {
|
||||
setShowTooltip(false);
|
||||
}, 2000);
|
||||
}
|
||||
}, [editorRef.logs]);
|
||||
useEffect(() => {
|
||||
if (aiEmployee) {
|
||||
setActive('AICodingButton', true);
|
||||
} else {
|
||||
setActive('AICodingButton', false);
|
||||
}
|
||||
}, [aiEmployee, setActive]);
|
||||
|
||||
const TaskTemplate = (prototype: Partial<Task>) => {
|
||||
const { message, ...rest } = prototype;
|
||||
return {
|
||||
message: {
|
||||
workContext: [
|
||||
{
|
||||
type: 'code-editor',
|
||||
uid,
|
||||
title: `${scene}(${language})`,
|
||||
content: {
|
||||
scene,
|
||||
language,
|
||||
code: editorRef?.read(),
|
||||
},
|
||||
},
|
||||
],
|
||||
...(message ?? {}),
|
||||
},
|
||||
autoSend: false,
|
||||
...rest,
|
||||
};
|
||||
};
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [errorOccurred, setErrorOccurred] = useState(false);
|
||||
useEffect(() => {
|
||||
const isError = editorRef.logs.find((log) => log.level === 'error') !== undefined;
|
||||
setErrorOccurred(isError);
|
||||
setShowTooltip(isError);
|
||||
if (isError) {
|
||||
setTimeout(() => {
|
||||
setShowTooltip(false);
|
||||
}, 2000);
|
||||
}
|
||||
}, [editorRef.logs]);
|
||||
|
||||
const taskMap: Record<string, Task> = {
|
||||
generateCode: TaskTemplate({ title: t('Start coding') }),
|
||||
codeReview: TaskTemplate({
|
||||
title: t('Code review'),
|
||||
message: { user: t('please review the code'), system: prompts.codeReview },
|
||||
}),
|
||||
logsDiagnosis: TaskTemplate({
|
||||
title: t('Diagnose and fix the error'),
|
||||
message: {
|
||||
user: t('please fix the error'),
|
||||
system: `Here is run logs:${JSON.stringify(
|
||||
editorRef?.logs,
|
||||
)} \n analyze the code and run logs, then fix the problems existing in the code`,
|
||||
},
|
||||
autoSend: errorOccurred,
|
||||
}),
|
||||
};
|
||||
|
||||
const tasks: Task[] = Object.values(taskMap);
|
||||
|
||||
// Store flow context for frontend context tools
|
||||
useChatMessagesStore.getState().setFlowContext(ctx);
|
||||
|
||||
return aiEmployee ? (
|
||||
<Tooltip
|
||||
placement="topRight"
|
||||
title={t('Oops! Something went wrong. Let me diagnose and fix it.')}
|
||||
open={showTooltip}
|
||||
styles={{ root: { maxWidth: 500 } }}
|
||||
>
|
||||
<Popover content={<ProfileCard aiEmployee={aiEmployee} tasks={tasks} />}>
|
||||
<Avatar
|
||||
src={avatars(aiEmployee.avatar)}
|
||||
size={32}
|
||||
shape="circle"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
border: '1px solid #eee',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!open || currentEmployee?.username !== aiEmployee.username) {
|
||||
if (editorRef.logs.find((log) => log.level === 'error')) {
|
||||
triggerTask({ aiEmployee, tasks: [taskMap['logsDiagnosis']] });
|
||||
} else {
|
||||
triggerTask({ aiEmployee, tasks });
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentEditorRefUid(uid);
|
||||
|
||||
addContextItems({
|
||||
const TaskTemplate = (prototype: Partial<Task>) => {
|
||||
const { message, ...rest } = prototype;
|
||||
return {
|
||||
message: {
|
||||
workContext: [
|
||||
{
|
||||
type: 'code-editor',
|
||||
uid,
|
||||
title: `${scene}(${language})`,
|
||||
@@ -154,12 +92,81 @@ export const AICodingButton: React.FC<AICodingButtonProps> = ({ uid, scene, lang
|
||||
language,
|
||||
code: editorRef?.read(),
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
},
|
||||
],
|
||||
...(message ?? {}),
|
||||
},
|
||||
autoSend: false,
|
||||
...rest,
|
||||
};
|
||||
};
|
||||
|
||||
const taskMap: Record<string, Task> = {
|
||||
generateCode: TaskTemplate({ title: t('Start coding') }),
|
||||
codeReview: TaskTemplate({
|
||||
title: t('Code review'),
|
||||
message: { user: t('please review the code'), system: prompts.codeReview },
|
||||
}),
|
||||
logsDiagnosis: TaskTemplate({
|
||||
title: t('Diagnose and fix the error'),
|
||||
message: {
|
||||
user: t('please fix the error'),
|
||||
system: `Here is run logs:${JSON.stringify(
|
||||
editorRef?.logs,
|
||||
)} \n analyze the code and run logs, then fix the problems existing in the code`,
|
||||
},
|
||||
autoSend: errorOccurred,
|
||||
}),
|
||||
};
|
||||
|
||||
const tasks: Task[] = Object.values(taskMap);
|
||||
|
||||
// Store flow context for frontend context tools
|
||||
useChatMessagesStore.getState().setFlowContext(ctx);
|
||||
|
||||
return aiEmployee ? (
|
||||
<Tooltip
|
||||
placement="topRight"
|
||||
title={t('Oops! Something went wrong. Let me diagnose and fix it.')}
|
||||
open={showTooltip}
|
||||
styles={{ root: { maxWidth: 500 } }}
|
||||
>
|
||||
<Popover content={<ProfileCard aiEmployee={aiEmployee} tasks={tasks} />}>
|
||||
<Avatar
|
||||
src={avatars(aiEmployee.avatar)}
|
||||
size={32}
|
||||
shape="circle"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
border: '1px solid #eee',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!open || currentEmployee?.username !== aiEmployee.username) {
|
||||
if (editorRef.logs.find((log) => log.level === 'error')) {
|
||||
triggerTask({ aiEmployee, tasks: [taskMap['logsDiagnosis']] });
|
||||
} else {
|
||||
triggerTask({ aiEmployee, tasks });
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentEditorRefUid(uid);
|
||||
|
||||
addContextItems({
|
||||
type: 'code-editor',
|
||||
uid,
|
||||
title: `${scene}(${language})`,
|
||||
content: {
|
||||
scene,
|
||||
language,
|
||||
code: editorRef?.read(),
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
+46
-41
@@ -7,10 +7,11 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Avatar, Button, Divider, Dropdown, Flex, Popover, Tag } from 'antd';
|
||||
import { UserAddOutlined, CloseCircleOutlined, CheckOutlined, DownOutlined } from '@ant-design/icons';
|
||||
import { useToken } from '@nocobase/client';
|
||||
import { observer } from '@nocobase/flow-engine';
|
||||
import { useT } from '../../locale';
|
||||
import { AIEmployeeListItem } from '../AIEmployeeListItem';
|
||||
import { avatars } from '../avatars';
|
||||
@@ -20,19 +21,23 @@ import { ContextItemsHeader } from './ContextItemsHeader';
|
||||
import { useChatBoxStore } from './stores/chat-box';
|
||||
import { useChatBoxActions } from './hooks/useChatBoxActions';
|
||||
import { EditMessageHeader } from './EditMessageHeader';
|
||||
import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
|
||||
import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
|
||||
|
||||
export const AIEmployeeSwitcher: React.FC = () => {
|
||||
export const AIEmployeeSwitcher: React.FC = observer(() => {
|
||||
const t = useT();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { aiEmployees } = useAIEmployeesData();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const aiEmployees = aiConfigRepository.aiEmployees;
|
||||
const currentEmployee = useChatBoxStore.use.currentEmployee();
|
||||
const { switchAIEmployee } = useChatBoxActions();
|
||||
const { token } = useToken();
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
if (!aiEmployees.length) {
|
||||
return [
|
||||
useEffect(() => {
|
||||
aiConfigRepository.getAIEmployees();
|
||||
}, [aiConfigRepository]);
|
||||
|
||||
const menuItems = !aiEmployees.length
|
||||
? [
|
||||
{
|
||||
key: 'empty',
|
||||
label: (
|
||||
@@ -41,23 +46,20 @@ export const AIEmployeeSwitcher: React.FC = () => {
|
||||
disabled: true,
|
||||
style: { cursor: 'default', padding: '16px 12px', height: 'auto', minHeight: 0 },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return aiEmployees.map((employee) => {
|
||||
const isSelected = currentEmployee?.username === employee.username;
|
||||
return {
|
||||
key: employee.username,
|
||||
label: (
|
||||
<span style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<AIEmployeeListItem aiEmployee={employee} />
|
||||
{isSelected && <CheckOutlined style={{ fontSize: 12, color: token.colorPrimary }} />}
|
||||
</span>
|
||||
),
|
||||
onClick: () => switchAIEmployee(employee),
|
||||
};
|
||||
});
|
||||
}, [aiEmployees, currentEmployee?.username, switchAIEmployee, t, token.colorPrimary, token.colorTextSecondary]);
|
||||
]
|
||||
: aiEmployees.map((employee) => {
|
||||
const isSelected = currentEmployee?.username === employee.username;
|
||||
return {
|
||||
key: employee.username,
|
||||
label: (
|
||||
<span style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<AIEmployeeListItem aiEmployee={employee} />
|
||||
{isSelected && <CheckOutlined style={{ fontSize: 12, color: token.colorPrimary }} />}
|
||||
</span>
|
||||
),
|
||||
onClick: () => switchAIEmployee(employee),
|
||||
};
|
||||
});
|
||||
|
||||
const hasEmployees = aiEmployees.length > 0;
|
||||
const currentLabel = currentEmployee ? currentEmployee.nickname : `${t('Select an')} ${t('AI employee')}`;
|
||||
@@ -105,10 +107,11 @@ export const AIEmployeeSwitcher: React.FC = () => {
|
||||
{dropdownContent}
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export const SenderHeader: React.FC = () => {
|
||||
const { aiEmployees } = useAIEmployeesData();
|
||||
export const SenderHeader: React.FC = observer(() => {
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const aiEmployees = aiConfigRepository.aiEmployees;
|
||||
const { token } = useToken();
|
||||
const t = useT();
|
||||
|
||||
@@ -117,19 +120,21 @@ export const SenderHeader: React.FC = () => {
|
||||
|
||||
const { switchAIEmployee } = useChatBoxActions();
|
||||
|
||||
const items = useMemo(() => {
|
||||
return aiEmployees?.map((employee) => ({
|
||||
key: employee.username,
|
||||
label: (
|
||||
<AIEmployeeListItem
|
||||
aiEmployee={employee}
|
||||
onClick={() => {
|
||||
switchAIEmployee(employee);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
}, [aiEmployees, switchAIEmployee]);
|
||||
useEffect(() => {
|
||||
aiConfigRepository.getAIEmployees();
|
||||
}, [aiConfigRepository]);
|
||||
|
||||
const items = aiEmployees?.map((employee) => ({
|
||||
key: employee.username,
|
||||
label: (
|
||||
<AIEmployeeListItem
|
||||
aiEmployee={employee}
|
||||
onClick={() => {
|
||||
switchAIEmployee(employee);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
const avatar = useMemo(() => {
|
||||
if (!currentEmployee) {
|
||||
@@ -222,4 +227,4 @@ export const SenderHeader: React.FC = () => {
|
||||
{currentEmployee ? <AttachmentsHeader /> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
+22
-20
@@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Avatar, Dropdown, FloatButton } from 'antd';
|
||||
import icon from '../icon.svg';
|
||||
import { css } from '@emotion/css';
|
||||
@@ -15,7 +15,7 @@ import { AIEmployeeListItem } from '../AIEmployeeListItem';
|
||||
import { useMobileLayout, useToken } from '@nocobase/client';
|
||||
import { useChatBoxStore } from './stores/chat-box';
|
||||
import { useChatBoxActions } from './hooks/useChatBoxActions';
|
||||
import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
|
||||
import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
|
||||
import { FlowRuntimeContext, observer, useFlowContext } from '@nocobase/flow-engine';
|
||||
import { isHide } from '../built-in/utils';
|
||||
import { useChatConversationsStore } from './stores/chat-conversations';
|
||||
@@ -26,7 +26,11 @@ export const ChatButton: React.FC = observer(() => {
|
||||
const isV1Page = ctx?.pageInfo?.version === 'v1';
|
||||
const { token } = useToken();
|
||||
|
||||
const { aiEmployees } = useAIEmployeesData();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const aiEmployees = aiConfigRepository.aiEmployees;
|
||||
React.useEffect(() => {
|
||||
aiConfigRepository.getAIEmployees();
|
||||
}, [aiConfigRepository]);
|
||||
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
|
||||
@@ -38,23 +42,21 @@ export const ChatButton: React.FC = observer(() => {
|
||||
|
||||
const setWebSearch = useChatConversationsStore.use.setWebSearch();
|
||||
|
||||
const items = useMemo(() => {
|
||||
return aiEmployees
|
||||
?.filter((employee) => !isHide(employee))
|
||||
.map((employee) => ({
|
||||
key: employee.username,
|
||||
label: (
|
||||
<AIEmployeeListItem
|
||||
aiEmployee={employee}
|
||||
onClick={() => {
|
||||
setWebSearch(true);
|
||||
setOpen(true);
|
||||
switchAIEmployee(employee);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
}, [aiEmployees]);
|
||||
const items = aiEmployees
|
||||
?.filter((employee) => !isHide(employee))
|
||||
.map((employee) => ({
|
||||
key: employee.username,
|
||||
label: (
|
||||
<AIEmployeeListItem
|
||||
aiEmployee={employee}
|
||||
onClick={() => {
|
||||
setWebSearch(true);
|
||||
setOpen(true);
|
||||
switchAIEmployee(employee);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
if (open || !aiEmployees?.length || isV1Page) {
|
||||
return null;
|
||||
|
||||
+8
-3
@@ -10,7 +10,7 @@
|
||||
import React, { memo, useEffect, useMemo, useRef } from 'react';
|
||||
import { Input, Empty, Spin, App } from 'antd';
|
||||
import { Conversations as AntConversations } from '@ant-design/x';
|
||||
import { SchemaComponent, useAPIClient, useActionContext } from '@nocobase/client';
|
||||
import { SchemaComponent, useAPIClient, useActionContext, useRequest } from '@nocobase/client';
|
||||
import { css } from '@emotion/css';
|
||||
import { useT } from '../../locale';
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
||||
@@ -23,7 +23,8 @@ import { useChatMessagesStore } from './stores/chat-messages';
|
||||
import { useChatMessageActions } from './hooks/useChatMessageActions';
|
||||
import { useChatBoxActions } from './hooks/useChatBoxActions';
|
||||
import { useChatBoxStore } from './stores/chat-box';
|
||||
import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
|
||||
import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
|
||||
import { AIEmployee } from '../types';
|
||||
|
||||
const useCloseActionProps = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
@@ -131,7 +132,11 @@ export const Conversations: React.FC = memo(() => {
|
||||
const t = useT();
|
||||
const api = useAPIClient();
|
||||
const { modal, message } = App.useApp();
|
||||
const { aiEmployeesMap } = useAIEmployeesData();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
useRequest<AIEmployee[]>(async () => {
|
||||
return aiConfigRepository.getAIEmployees();
|
||||
});
|
||||
const aiEmployeesMap = aiConfigRepository.getAIEmployeesMap();
|
||||
|
||||
const currentEmployee = useChatBoxStore.use.currentEmployee();
|
||||
const setCurrentEmployee = useChatBoxStore.use.setCurrentEmployee();
|
||||
|
||||
+10
-3
@@ -12,7 +12,7 @@ import { Button, Space, App, Alert, Flex, Collapse, Typography, Tooltip } from '
|
||||
import { CopyOutlined, ReloadOutlined, EditOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
|
||||
import { Attachments, Bubble } from '@ant-design/x';
|
||||
import { useT } from '../../locale';
|
||||
import { lazy, usePlugin, useToken, useTools, toToolsMap } from '@nocobase/client';
|
||||
import { lazy, usePlugin, useToken, toToolsMap } from '@nocobase/client';
|
||||
import PluginAIClient from '../..';
|
||||
import { cx, css } from '@emotion/css';
|
||||
import { Message, Task } from '../types';
|
||||
@@ -24,6 +24,8 @@ import { useChatBoxStore } from './stores/chat-box';
|
||||
import { useChatMessagesStore } from './stores/chat-messages';
|
||||
import { useChatBoxActions } from './hooks/useChatBoxActions';
|
||||
import _ from 'lodash';
|
||||
import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
|
||||
import { observer } from '@nocobase/flow-engine';
|
||||
|
||||
const { Markdown } = lazy(() => import('./markdown/Markdown'), 'Markdown');
|
||||
|
||||
@@ -106,12 +108,17 @@ const AIMessageRenderer: React.FC<{
|
||||
|
||||
export const AIMessage: React.FC<{
|
||||
msg: Message['content'];
|
||||
}> = memo(({ msg }) => {
|
||||
}> = observer(({ msg }) => {
|
||||
const t = useT();
|
||||
const { token } = useToken();
|
||||
const { message } = App.useApp();
|
||||
const { tools, loading: toolsLoading } = useTools();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const toolsLoading = aiConfigRepository.aiToolsLoading;
|
||||
const tools = aiConfigRepository.aiTools;
|
||||
const toolsMap = useMemo(() => toToolsMap(tools || []), [tools]);
|
||||
useEffect(() => {
|
||||
aiConfigRepository.getAITools();
|
||||
}, [aiConfigRepository]);
|
||||
const plugin = usePlugin('ai') as PluginAIClient;
|
||||
const provider = plugin.aiManager.llmProviders.get(msg.metadata?.provider);
|
||||
const hasCustomRenderer = !!provider?.components?.MessageRenderer;
|
||||
|
||||
+11
-3
@@ -37,6 +37,10 @@ export const ModelSwitcher: React.FC = observer(
|
||||
|
||||
const { repo, services: llmServices, loading, allModelsWithLabel, allModels } = useLLMServiceCatalog();
|
||||
|
||||
const servicesWithModels = llmServices.filter(
|
||||
(service) => Array.isArray(service.enabledModels) && service.enabledModels.length > 0,
|
||||
);
|
||||
|
||||
// Initialize: cache >> first model
|
||||
useEffect(() => {
|
||||
if (!currentEmployeeUsername || !allModels.length) return;
|
||||
@@ -101,12 +105,12 @@ export const ModelSwitcher: React.FC = observer(
|
||||
if (!currentEmployee) return null;
|
||||
if (loading && !llmServices.length) return <Spin size="small" />;
|
||||
|
||||
const hasModels = allModels.length > 0;
|
||||
const hasModels = servicesWithModels.length > 0;
|
||||
|
||||
// Build dropdown menu items
|
||||
const menuItems: any[] = [];
|
||||
|
||||
llmServices.forEach((service, sIndex) => {
|
||||
servicesWithModels.forEach((service, sIndex) => {
|
||||
if (sIndex > 0) {
|
||||
menuItems.push({ type: 'divider', key: `divider-${sIndex}` });
|
||||
}
|
||||
@@ -211,7 +215,11 @@ export const ModelSwitcher: React.FC = observer(
|
||||
{dropdownContent}
|
||||
</Dropdown>
|
||||
{hasConfigPermission && (
|
||||
<AddLLMModal open={addModalOpen} onClose={() => setAddModalOpen(false)} onSuccess={() => repo.refresh()} />
|
||||
<AddLLMModal
|
||||
open={addModalOpen}
|
||||
onClose={() => setAddModalOpen(false)}
|
||||
onSuccess={() => repo.refreshLLMServices()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -8,14 +8,15 @@
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { SchemaComponent, useAPIClient, useActionContext, useToken } from '@nocobase/client';
|
||||
import { SchemaComponent, useAPIClient, useActionContext, useRequest, useToken } from '@nocobase/client';
|
||||
import { useT } from '../../locale';
|
||||
import { Button, Popover, Card, Alert, App, Typography } from 'antd';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { useChatBoxStore } from './stores/chat-box';
|
||||
import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
|
||||
import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
|
||||
import { AIEmployee } from '../types';
|
||||
|
||||
const useCancelActionProps = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
@@ -38,8 +39,7 @@ const useEditActionProps = () => {
|
||||
|
||||
const currentEmployee = useChatBoxStore.use.currentEmployee();
|
||||
const setCurrentEmployee = useChatBoxStore.use.setCurrentEmployee();
|
||||
|
||||
const { refresh } = useAIEmployeesData();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
|
||||
return {
|
||||
type: 'primary',
|
||||
@@ -51,7 +51,7 @@ const useEditActionProps = () => {
|
||||
prompt: form.values?.prompt,
|
||||
},
|
||||
});
|
||||
refresh();
|
||||
await aiConfigRepository.refreshAIEmployees();
|
||||
setCurrentEmployee((prev) => ({
|
||||
...prev,
|
||||
userConfig: {
|
||||
@@ -134,6 +134,10 @@ const Edit: React.FC = () => {
|
||||
export const UserPrompt: React.FC = () => {
|
||||
const t = useT();
|
||||
const { token } = useToken();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
useRequest<AIEmployee[]>(async () => {
|
||||
return aiConfigRepository.getAIEmployees();
|
||||
});
|
||||
const currentEmployee = useChatBoxStore.use.currentEmployee();
|
||||
|
||||
return (
|
||||
|
||||
+12
-4
@@ -9,17 +9,21 @@
|
||||
|
||||
import React, { ComponentType, useEffect } from 'react';
|
||||
import { DefaultToolCard } from './DefaultToolCard';
|
||||
import { ToolsUIProperties, toToolsMap, useTools } from '@nocobase/client';
|
||||
import { ToolsUIProperties, toToolsMap } from '@nocobase/client';
|
||||
import { ToolCall } from '../../types';
|
||||
import { jsonrepair } from 'jsonrepair';
|
||||
import { useToolCallActions } from '../hooks/useToolCallActions';
|
||||
import { useAIConfigRepository } from '../../../repositories/hooks/useAIConfigRepository';
|
||||
import { observer } from '@nocobase/flow-engine';
|
||||
|
||||
export const ToolCard: React.FC<{
|
||||
messageId: string;
|
||||
toolCalls: ToolCall[];
|
||||
inlineActions?: React.ReactNode;
|
||||
}> = ({ toolCalls, messageId, inlineActions }) => {
|
||||
const { tools, loading } = useTools();
|
||||
}> = observer(({ toolCalls, messageId, inlineActions }) => {
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const loading = aiConfigRepository.aiToolsLoading;
|
||||
const tools = aiConfigRepository.aiTools;
|
||||
const toolsMap = toToolsMap(tools);
|
||||
const { getDecisionActions } = useToolCallActions({ messageId });
|
||||
const toolsWithUI: ({ C: ComponentType<ToolsUIProperties> } & ToolsUIProperties)[] = [];
|
||||
@@ -55,6 +59,10 @@ export const ToolCard: React.FC<{
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
aiConfigRepository.getAITools();
|
||||
}, [aiConfigRepository]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!messageId) {
|
||||
return;
|
||||
@@ -94,4 +102,4 @@ export const ToolCard: React.FC<{
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
+12
-5
@@ -7,15 +7,17 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { Modal, Select, message } from 'antd';
|
||||
import { useChatToolsStore } from '../stores/chat-tools';
|
||||
import { ToolsUIProperties, toToolsMap, useTools } from '@nocobase/client';
|
||||
import { ToolsUIProperties, toToolsMap } from '@nocobase/client';
|
||||
import { Schema } from '@formily/react';
|
||||
import { observer } from '@nocobase/flow-engine';
|
||||
import { useT } from '../../../locale';
|
||||
import { useChatMessageActions } from '../hooks/useChatMessageActions';
|
||||
import { useChatConversationsStore } from '../stores/chat-conversations';
|
||||
import { useToolCallActions } from '../hooks/useToolCallActions';
|
||||
import { useAIConfigRepository } from '../../../repositories/hooks/useAIConfigRepository';
|
||||
|
||||
const useDefaultOnOk = (decisions: ToolsUIProperties['decisions']) => {
|
||||
return {
|
||||
@@ -23,11 +25,16 @@ const useDefaultOnOk = (decisions: ToolsUIProperties['decisions']) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const ToolModal: React.FC = () => {
|
||||
export const ToolModal: React.FC = observer(() => {
|
||||
const t = useT();
|
||||
const { tools } = useTools();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const tools = aiConfigRepository.aiTools;
|
||||
const toolsMap = toToolsMap(tools);
|
||||
|
||||
useEffect(() => {
|
||||
aiConfigRepository.getAITools();
|
||||
}, [aiConfigRepository]);
|
||||
|
||||
const open = useChatToolsStore.use.openToolModal();
|
||||
const setOpen = useChatToolsStore.use.setOpenToolModal();
|
||||
const activeTool = useChatToolsStore.use.activeTool();
|
||||
@@ -126,4 +133,4 @@ export const ToolModal: React.FC = () => {
|
||||
{C ? <C tool={activeTool} saveToolArgs={saveToolArgs} /> : null}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
+10
-10
@@ -20,12 +20,12 @@ import { uid } from '@formily/shared';
|
||||
import { aiEmployeeRole } from '../roles';
|
||||
import { useChatToolsStore } from '../stores/chat-tools';
|
||||
import { useAPIClient } from '@nocobase/client';
|
||||
import { useLLMServicesRepository } from '../../../llm-services/hooks/useLLMServicesRepository';
|
||||
import { useAIConfigRepository } from '../../../repositories/hooks/useAIConfigRepository';
|
||||
import { getAllModels, isSameModel, isValidModel, resolveModel } from '../model';
|
||||
|
||||
export const useChatBoxActions = () => {
|
||||
const api = useAPIClient();
|
||||
const llmServicesRepository = useLLMServicesRepository();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const t = useT();
|
||||
|
||||
const open = useChatBoxStore.use.open();
|
||||
@@ -91,8 +91,7 @@ export const useChatBoxActions = () => {
|
||||
|
||||
const ensureModel = useCallback(
|
||||
async (aiEmployee: AIEmployee) => {
|
||||
await llmServicesRepository.load();
|
||||
const allModels = getAllModels(llmServicesRepository.services);
|
||||
const allModels = getAllModels(await aiConfigRepository.getLLMServices());
|
||||
const currentModel = useChatBoxStore.getState().model;
|
||||
const resolvedModel = resolveModel(api, aiEmployee.username, allModels, currentModel);
|
||||
if (!isSameModel(currentModel, resolvedModel)) {
|
||||
@@ -100,13 +99,12 @@ export const useChatBoxActions = () => {
|
||||
}
|
||||
return resolvedModel;
|
||||
},
|
||||
[api, llmServicesRepository, setModel],
|
||||
[api, aiConfigRepository, setModel],
|
||||
);
|
||||
|
||||
const resolveTaskModel = useCallback(
|
||||
async (aiEmployee: AIEmployee, taskModel?: { llmService: string; model: string } | null) => {
|
||||
await llmServicesRepository.load();
|
||||
const allModels = getAllModels(llmServicesRepository.services);
|
||||
const allModels = getAllModels(await aiConfigRepository.getLLMServices());
|
||||
if (isValidModel(taskModel, allModels)) {
|
||||
const currentModel = useChatBoxStore.getState().model;
|
||||
if (!isSameModel(currentModel, taskModel)) {
|
||||
@@ -121,7 +119,7 @@ export const useChatBoxActions = () => {
|
||||
}
|
||||
return resolvedModel;
|
||||
},
|
||||
[api, llmServicesRepository, setModel],
|
||||
[api, aiConfigRepository, setModel],
|
||||
);
|
||||
|
||||
const startNewConversation = useCallback(() => {
|
||||
@@ -205,7 +203,9 @@ export const useChatBoxActions = () => {
|
||||
model: taskModel,
|
||||
} = await parseTask(task);
|
||||
const resolvedModel = await resolveTaskModel(aiEmployee, taskModel);
|
||||
const service = llmServicesRepository.services.find((s) => s.llmService === resolvedModel?.llmService);
|
||||
const service = (await aiConfigRepository.getLLMServices()).find(
|
||||
(s) => s.llmService === resolvedModel?.llmService,
|
||||
);
|
||||
const resolvedWebSearch =
|
||||
service?.supportWebSearch === false ? false : typeof webSearch === 'boolean' ? webSearch : false;
|
||||
setWebSearch(resolvedWebSearch);
|
||||
@@ -249,7 +249,7 @@ export const useChatBoxActions = () => {
|
||||
});
|
||||
setMessages(msgs);
|
||||
},
|
||||
[open, currentConversation, ensureModel, llmServicesRepository, resolveTaskModel, setWebSearch],
|
||||
[open, currentConversation, ensureModel, aiConfigRepository, resolveTaskModel, setWebSearch],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
+8
-6
@@ -7,15 +7,15 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useChatBoxStore } from '../stores/chat-box';
|
||||
import { aiEmployeeRole, defaultRoles } from '../roles';
|
||||
import { useChatConversationActions } from './useChatConversationActions';
|
||||
import { useAIEmployeesData } from '../../hooks/useAIEmployeesData';
|
||||
import { useTools } from '@nocobase/client';
|
||||
import { useAIConfigRepository } from '../../../repositories/hooks/useAIConfigRepository';
|
||||
|
||||
export const useChatBoxEffect = () => {
|
||||
const { aiEmployees } = useAIEmployeesData();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const aiEmployees = aiConfigRepository.aiEmployees;
|
||||
|
||||
const open = useChatBoxStore.use.open();
|
||||
const senderRef = useChatBoxStore.use.senderRef();
|
||||
@@ -24,7 +24,9 @@ export const useChatBoxEffect = () => {
|
||||
|
||||
const { conversationsService } = useChatConversationActions();
|
||||
|
||||
const { refresh } = useTools();
|
||||
useEffect(() => {
|
||||
aiConfigRepository.getAIEmployees();
|
||||
}, [aiConfigRepository]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!aiEmployees) {
|
||||
@@ -55,7 +57,7 @@ export const useChatBoxEffect = () => {
|
||||
if (open) {
|
||||
conversationsService.run();
|
||||
senderRef?.current?.focus();
|
||||
refresh();
|
||||
aiConfigRepository.refreshAITools();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
|
||||
+4
-4
@@ -20,7 +20,7 @@ import { useChatBoxStore } from '../stores/chat-box';
|
||||
import { parseWorkContext } from '../utils';
|
||||
import { aiDebugLogger } from '../../../debug-logger'; // [AI_DEBUG]
|
||||
import { useChatToolCallStore } from '../stores/chat-tool-call';
|
||||
import { useLLMServicesRepository } from '../../../llm-services/hooks/useLLMServicesRepository';
|
||||
import { useAIConfigRepository } from '../../../repositories/hooks/useAIConfigRepository';
|
||||
import { ensureModel } from '../model';
|
||||
|
||||
export const useChatMessageActions = () => {
|
||||
@@ -28,7 +28,7 @@ export const useChatMessageActions = () => {
|
||||
const t = useT();
|
||||
const api = useAPIClient();
|
||||
const plugin = usePlugin('ai') as PluginAIClient;
|
||||
const llmServicesRepository = useLLMServicesRepository();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
|
||||
const setIsEditingMessage = useChatBoxStore.use.setIsEditingMessage();
|
||||
const setEditingMessageId = useChatBoxStore.use.setEditingMessageId();
|
||||
@@ -59,13 +59,13 @@ export const useChatMessageActions = () => {
|
||||
}
|
||||
return ensureModel({
|
||||
api,
|
||||
llmServicesRepository,
|
||||
aiConfigRepository,
|
||||
username: targetUsername,
|
||||
currentOverride: state.model,
|
||||
onResolved: setModel,
|
||||
});
|
||||
},
|
||||
[api, llmServicesRepository, setModel],
|
||||
[api, aiConfigRepository, setModel],
|
||||
);
|
||||
|
||||
const messagesService = useRequest<{
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { LLMServiceItem } from '../../llm-services/LLMServicesRepository';
|
||||
import type { LLMServicesRepository } from '../../llm-services/LLMServicesRepository';
|
||||
import { AIConfigRepository, LLMServiceItem } from '../../repositories/AIConfigRepository';
|
||||
import { ModelRef } from './stores/chat-box';
|
||||
|
||||
export const MODEL_PREFERENCE_STORAGE_KEY = 'ai_model_preference_';
|
||||
@@ -56,19 +55,18 @@ export const resolveModel = (api: any, username: string, allModels: ModelRef[],
|
||||
|
||||
export const ensureModel = async ({
|
||||
api,
|
||||
llmServicesRepository,
|
||||
aiConfigRepository,
|
||||
username,
|
||||
currentOverride,
|
||||
onResolved,
|
||||
}: {
|
||||
api: any;
|
||||
llmServicesRepository: LLMServicesRepository;
|
||||
aiConfigRepository: AIConfigRepository;
|
||||
username: string;
|
||||
currentOverride?: ModelRef | null;
|
||||
onResolved?: (override: ModelRef | null) => void;
|
||||
}): Promise<ModelRef | null> => {
|
||||
await llmServicesRepository.load();
|
||||
const allModels = getAllModels(llmServicesRepository.services);
|
||||
const allModels = getAllModels(await aiConfigRepository.getLLMServices());
|
||||
const resolvedOverride = resolveModel(api, username, allModels, currentOverride);
|
||||
if (!isSameModel(currentOverride, resolvedOverride)) {
|
||||
onResolved?.(resolvedOverride);
|
||||
|
||||
+7
-2
@@ -13,10 +13,12 @@ import React, { useMemo, useState } from 'react';
|
||||
import { Avatar, Button, Popover } from 'antd';
|
||||
import { ProfileCard } from '../ProfileCard';
|
||||
import { avatars } from '../avatars';
|
||||
import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
|
||||
import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
|
||||
import { useChatBoxStore } from '../chatbox/stores/chat-box';
|
||||
import { useChatBoxActions } from '../chatbox/hooks/useChatBoxActions';
|
||||
import { isDataModelingAssistant } from '../built-in/utils';
|
||||
import { useRequest } from '@nocobase/client';
|
||||
import { AIEmployee } from '../types';
|
||||
|
||||
export const setupDataModeling = (plugin: PluginAIClient) => {
|
||||
const dataSourceManager = plugin.pm.get<PluginDataSourceManagerClient>('data-source-manager');
|
||||
@@ -27,7 +29,10 @@ export const setupDataModeling = (plugin: PluginAIClient) => {
|
||||
|
||||
const AIButton = () => {
|
||||
const [focus, setFocus] = useState(false);
|
||||
const { aiEmployees } = useAIEmployeesData();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const { data: aiEmployees = [] } = useRequest<AIEmployee[]>(async () => {
|
||||
return aiConfigRepository.getAIEmployees();
|
||||
});
|
||||
const open = useChatBoxStore.use.open();
|
||||
const setOpen = useChatBoxStore.use.setOpen();
|
||||
const currentEmployee = useChatBoxStore.use.currentEmployee();
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* 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 { FlowEngineContext, PropertyOptions } from '@nocobase/flow-engine';
|
||||
import { AIEmployee } from '../../types';
|
||||
|
||||
export const aiEmployeesData: [string, PropertyOptions] = [
|
||||
'aiEmployeesData',
|
||||
{
|
||||
get: async (ctx: FlowEngineContext) => {
|
||||
const aiEmployees: AIEmployee[] = await ctx.api
|
||||
.resource('aiEmployees')
|
||||
.listByUser()
|
||||
.then((res) => res?.data?.data);
|
||||
|
||||
const aiEmployeesMap: {
|
||||
[username: string]: AIEmployee;
|
||||
} = (aiEmployees || []).reduce((acc, aiEmployee) => {
|
||||
acc[aiEmployee.username] = aiEmployee;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return { aiEmployees, aiEmployeesMap };
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './ai-employees-data';
|
||||
+1
-1
@@ -38,7 +38,7 @@ export class AIEmployeeActionModel extends ActionModel {
|
||||
static scene = ActionSceneEnum.all;
|
||||
|
||||
static async defineChildren(ctx: FlowModelContext) {
|
||||
const { aiEmployees } = ctx.aiEmployeesData;
|
||||
const aiEmployees = await ctx.aiConfigRepository.getAIEmployees();
|
||||
|
||||
return aiEmployees
|
||||
?.filter((aiEmployee: AIEmployee) => !isHide(aiEmployee))
|
||||
|
||||
+9
-6
@@ -14,8 +14,7 @@ import { avatars } from '../../avatars';
|
||||
import { AIEmployee, TriggerTaskOptions, ContextItem as ContextItemType } from '../../types';
|
||||
import { useChatBoxActions } from '../../chatbox/hooks/useChatBoxActions';
|
||||
import { ProfileCard } from '../../ProfileCard';
|
||||
import { RemoteSelect, TextAreaWithContextSelector, useToken } from '@nocobase/client';
|
||||
import { useAIEmployeesData } from '../../hooks/useAIEmployeesData';
|
||||
import { RemoteSelect, TextAreaWithContextSelector, useRequest, useToken } from '@nocobase/client';
|
||||
import { AddContextButton } from '../../AddContextButton';
|
||||
import { Schema, useField } from '@formily/react';
|
||||
import { ArrayField, ObjectField, Field } from '@formily/core';
|
||||
@@ -29,6 +28,7 @@ import { useLLMServiceCatalog } from '../../../llm-services/hooks/useLLMServiceC
|
||||
import { useLLMProviders } from '../../../llm-services/llm-providers';
|
||||
import { useT } from '../../../locale';
|
||||
import { buildProviderGroupedModelOptions, getServiceByOverride } from '../../../llm-services/utils';
|
||||
import { useAIConfigRepository } from '../../../repositories/hooks/useAIConfigRepository';
|
||||
|
||||
const { Meta } = Card;
|
||||
|
||||
@@ -58,8 +58,11 @@ const Shortcut: React.FC<ShortcutProps> = ({
|
||||
}) => {
|
||||
const { size, mask } = style;
|
||||
const [focus, setFocus] = useState(false);
|
||||
|
||||
const { loading, aiEmployeesMap } = useAIEmployeesData();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const { loading } = useRequest<AIEmployee[]>(async () => {
|
||||
return aiConfigRepository.getAIEmployees();
|
||||
});
|
||||
const aiEmployeesMap = aiConfigRepository.getAIEmployeesMap();
|
||||
const aiEmployee = aiEmployeesMap[username];
|
||||
|
||||
const { triggerTask } = useChatBoxActions();
|
||||
@@ -104,7 +107,6 @@ const Shortcut: React.FC<ShortcutProps> = ({
|
||||
}}
|
||||
onMouseLeave={() => setFocus(false)}
|
||||
onClick={() => {
|
||||
setWebSearch(true);
|
||||
triggerTask({ aiEmployee, tasks, auto });
|
||||
if (context?.workContext?.length) {
|
||||
addContextItems(context.workContext);
|
||||
@@ -321,7 +323,8 @@ AIEmployeeShortcutModel.registerFlow({
|
||||
};
|
||||
},
|
||||
uiSchema: async (ctx) => {
|
||||
const { aiEmployeesMap } = await ctx.aiEmployeesData;
|
||||
await ctx.aiConfigRepository.getAIEmployees();
|
||||
const aiEmployeesMap = ctx.aiConfigRepository.getAIEmployeesMap();
|
||||
return {
|
||||
profile: {
|
||||
type: 'void',
|
||||
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* 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 { useFlowEngine } from '@nocobase/flow-engine';
|
||||
import { AIEmployee } from '../types';
|
||||
import { useRequest } from '@nocobase/client';
|
||||
import { create } from 'zustand';
|
||||
import { createSelectors } from '../chatbox/stores/create-selectors';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const store = create<{
|
||||
signal: boolean;
|
||||
sendSignal: () => void;
|
||||
}>()((set) => ({
|
||||
signal: false,
|
||||
sendSignal: () => set((state) => ({ signal: !state.signal })),
|
||||
}));
|
||||
const useSignal = createSelectors(store);
|
||||
|
||||
export const useAIEmployeesData = () => {
|
||||
const flowEngine = useFlowEngine();
|
||||
const signal = useSignal.use.signal();
|
||||
const sendSignal = useSignal.use.sendSignal();
|
||||
|
||||
const { loading, data, refreshAsync } = useRequest<{
|
||||
aiEmployees: AIEmployee[];
|
||||
aiEmployeesMap: {
|
||||
[username: string]: AIEmployee;
|
||||
};
|
||||
}>(() => flowEngine.context.aiEmployeesData);
|
||||
const aiEmployees = data?.aiEmployees || [];
|
||||
const aiEmployeesMap = data?.aiEmployeesMap || {};
|
||||
|
||||
// AI员工管理页修改了数据后,ChatButton不会重新渲染
|
||||
// ChatButton所在父组件通过app.use注册,被useMemo包裹而且依赖是app,所以在父组件之外的hooks调用都不会触发ChatButton渲染
|
||||
// refresh方法被admin下的组件调用,admin并不和ChatButton在同一个组件树下。这时是没办法触发ChatButton渲染的,所以利用zustand来触发
|
||||
useEffect(() => {
|
||||
refreshAsync();
|
||||
}, [signal, refreshAsync]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
aiEmployees,
|
||||
aiEmployeesMap,
|
||||
refresh: () => {
|
||||
flowEngine.context.removeCache('aiEmployeesData');
|
||||
sendSignal();
|
||||
},
|
||||
};
|
||||
};
|
||||
+9
-3
@@ -10,12 +10,12 @@
|
||||
import { Button } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { AddSubModelButton, FlowModelRenderer } from '@nocobase/flow-engine';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useShortcuts } from './useShortcuts';
|
||||
import { useDesignable } from '@nocobase/client';
|
||||
import { AIEmployeeListItem } from '../AIEmployeeListItem';
|
||||
import { observer } from '@nocobase/flow-engine';
|
||||
import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
|
||||
import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
|
||||
import { isHide } from '../built-in/utils';
|
||||
|
||||
export const ShortcutList: React.FC = observer(() => {
|
||||
@@ -24,7 +24,13 @@ export const ShortcutList: React.FC = observer(() => {
|
||||
const designMode = designable && !builtIn;
|
||||
const hasShortcuts = model?.subModels?.shortcuts?.length > 0;
|
||||
|
||||
const { loading, aiEmployees } = useAIEmployeesData();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const loading = aiConfigRepository.aiEmployeesLoading;
|
||||
const aiEmployees = aiConfigRepository.aiEmployees;
|
||||
|
||||
useEffect(() => {
|
||||
aiConfigRepository.getAIEmployees();
|
||||
}, [aiConfigRepository]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -31,8 +31,7 @@ import {
|
||||
AIEmployeeButtonModel,
|
||||
} from './ai-employees/flow/models';
|
||||
import './ai-employees/flow/events';
|
||||
import { aiEmployeesData } from './ai-employees/flow/context';
|
||||
import { LLMServicesRepository } from './llm-services/LLMServicesRepository';
|
||||
import { AIConfigRepository } from './repositories/AIConfigRepository';
|
||||
import { FlowModelsContext } from './ai-employees/context/flow-models';
|
||||
import { DatasourceContext } from './ai-employees/context/datasource';
|
||||
import { CodeEditorContext } from './ai-employees/context/code-editor';
|
||||
@@ -138,10 +137,10 @@ export class PluginAIClient extends Plugin {
|
||||
}
|
||||
|
||||
async setupAIFeatures() {
|
||||
this.app.flowEngine.context.defineProperty(...aiEmployeesData);
|
||||
|
||||
const llmServicesRepository = new LLMServicesRepository(this.app.apiClient);
|
||||
this.app.flowEngine.context.defineProperty('llmServicesRepository', { value: llmServicesRepository });
|
||||
const aiConfigRepository = new AIConfigRepository(this.app.apiClient, {
|
||||
toolsManager: this.app.aiManager.toolsManager,
|
||||
});
|
||||
this.app.flowEngine.context.defineProperty('aiConfigRepository', { value: aiConfigRepository });
|
||||
|
||||
this.aiManager.registerLLMProvider('google-genai', googleGenAIProviderOptions);
|
||||
this.aiManager.registerLLMProvider('openai', openaiResponsesProviderOptions);
|
||||
@@ -193,12 +192,12 @@ export class PluginAIClient extends Plugin {
|
||||
export default PluginAIClient;
|
||||
export { ModelSelect, Chat };
|
||||
export type { LLMProviderOptions, ToolOptions } from './manager/ai-manager';
|
||||
export type { ToolCall } from './ai-employees/types';
|
||||
export type { AIEmployee, ToolCall } from './ai-employees/types';
|
||||
export * from './features';
|
||||
export { AIEmployeeActionModel } from './ai-employees/flow/models/AIEmployeeActionModel';
|
||||
export { useAIEmployeesData } from './ai-employees/hooks/useAIEmployeesData';
|
||||
export { useChatMessagesStore } from './ai-employees/chatbox/stores/chat-messages';
|
||||
export { useChatBoxStore } from './ai-employees/chatbox/stores/chat-box';
|
||||
export { useChatBoxActions } from './ai-employees/chatbox/hooks/useChatBoxActions';
|
||||
export { useAIConfigRepository } from './repositories/hooks/useAIConfigRepository';
|
||||
export { ProfileCard } from './ai-employees/ProfileCard';
|
||||
export { avatars } from './ai-employees/avatars';
|
||||
|
||||
@@ -43,7 +43,7 @@ import PluginAIClient from '..';
|
||||
import { LLMTestFlight } from './component/LLMTestFlight';
|
||||
import { EnabledModelsSelect } from './component/EnabledModelsSelect';
|
||||
import { ModelOptionsSettings } from './component/ModelOptionsSettings';
|
||||
import { useLLMServicesRepository } from './hooks/useLLMServicesRepository';
|
||||
import { useAIConfigRepository } from '../repositories/hooks/useAIConfigRepository';
|
||||
|
||||
const useCreateFormProps = () => {
|
||||
const form = useMemo(
|
||||
@@ -91,7 +91,7 @@ const useCreateActionProps = () => {
|
||||
const resource = useDataBlockResource();
|
||||
const { refresh } = useDataBlockRequest();
|
||||
const t = useT();
|
||||
const llmServicesRepo = useLLMServicesRepository();
|
||||
const llmServicesRepo = useAIConfigRepository();
|
||||
|
||||
return {
|
||||
type: 'primary',
|
||||
@@ -102,7 +102,7 @@ const useCreateActionProps = () => {
|
||||
values,
|
||||
});
|
||||
refresh();
|
||||
llmServicesRepo.refresh();
|
||||
llmServicesRepo.refreshLLMServices();
|
||||
message.success(t('Saved successfully'));
|
||||
setVisible(false);
|
||||
},
|
||||
@@ -118,7 +118,7 @@ const useEditActionProps = () => {
|
||||
const collection = useCollection();
|
||||
const filterTk = collection.getFilterTargetKey();
|
||||
const t = useT();
|
||||
const llmServicesRepo = useLLMServicesRepository();
|
||||
const llmServicesRepo = useAIConfigRepository();
|
||||
|
||||
return {
|
||||
type: 'primary',
|
||||
@@ -130,7 +130,7 @@ const useEditActionProps = () => {
|
||||
filterByTk: values[filterTk],
|
||||
});
|
||||
refresh();
|
||||
llmServicesRepo.refresh();
|
||||
llmServicesRepo.refreshLLMServices();
|
||||
message.success(t('Saved successfully'));
|
||||
setVisible(false);
|
||||
form.reset();
|
||||
@@ -250,7 +250,7 @@ const EnabledSwitch: React.FC = observer(
|
||||
const collection = useCollection();
|
||||
const filterTk = collection.getFilterTargetKey();
|
||||
const checked = field.value !== false;
|
||||
const llmServicesRepo = useLLMServicesRepository();
|
||||
const llmServicesRepo = useAIConfigRepository();
|
||||
|
||||
return (
|
||||
<Switch
|
||||
@@ -263,7 +263,7 @@ const EnabledSwitch: React.FC = observer(
|
||||
filterByTk: record[filterTk],
|
||||
});
|
||||
refresh();
|
||||
llmServicesRepo.refresh();
|
||||
llmServicesRepo.refreshLLMServices();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -273,24 +273,24 @@ const EnabledSwitch: React.FC = observer(
|
||||
|
||||
const useLLMDestroyActionProps = () => {
|
||||
const props = useDestroyActionProps();
|
||||
const llmServicesRepo = useLLMServicesRepository();
|
||||
const llmServicesRepo = useAIConfigRepository();
|
||||
return {
|
||||
...props,
|
||||
async onClick(e?, callBack?) {
|
||||
await props.onClick(e, callBack);
|
||||
llmServicesRepo.refresh();
|
||||
llmServicesRepo.refreshLLMServices();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const useLLMBulkDestroyActionProps = () => {
|
||||
const props = useBulkDestroyActionProps();
|
||||
const llmServicesRepo = useLLMServicesRepository();
|
||||
const llmServicesRepo = useAIConfigRepository();
|
||||
return {
|
||||
...props,
|
||||
async onClick(e?, callBack?) {
|
||||
await props.onClick(e, callBack);
|
||||
llmServicesRepo.refresh();
|
||||
llmServicesRepo.refreshLLMServices();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* 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 { define, observable } from '@formily/reactive';
|
||||
|
||||
export interface LLMServiceItem {
|
||||
llmService: string;
|
||||
llmServiceTitle: string;
|
||||
provider?: string;
|
||||
providerTitle?: string;
|
||||
enabledModels: { label: string; value: string }[];
|
||||
supportWebSearch?: boolean;
|
||||
isToolConflict?: boolean;
|
||||
}
|
||||
|
||||
export class LLMServicesRepository {
|
||||
services: LLMServiceItem[] = [];
|
||||
loading = false;
|
||||
private apiClient: any;
|
||||
private loadPromise: Promise<void> | null = null;
|
||||
|
||||
constructor(apiClient: any) {
|
||||
this.apiClient = apiClient;
|
||||
define(this, {
|
||||
services: observable.shallow,
|
||||
loading: observable.ref,
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this.loadPromise) return this.loadPromise;
|
||||
if (this.services.length > 0) return;
|
||||
this.loadPromise = this.fetchServices();
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.loadPromise = this.fetchServices();
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
private async fetchServices() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await this.apiClient.resource('ai').listAllEnabledModels();
|
||||
const data = res?.data?.data;
|
||||
this.services = Array.isArray(data) ? data : [];
|
||||
} catch {
|
||||
this.services = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.loadPromise = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -8,18 +8,18 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useLLMServicesRepository } from './useLLMServicesRepository';
|
||||
import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
|
||||
import { getAllModelsWithLabel } from '../utils';
|
||||
|
||||
export const useLLMServiceCatalog = () => {
|
||||
const repo = useLLMServicesRepository();
|
||||
const repo = useAIConfigRepository();
|
||||
|
||||
useEffect(() => {
|
||||
repo.load();
|
||||
repo.getLLMServices();
|
||||
}, [repo]);
|
||||
|
||||
const services = repo.services;
|
||||
const loading = repo.loading;
|
||||
const services = repo.llmServices;
|
||||
const loading = repo.llmServicesLoading;
|
||||
|
||||
const allModelsWithLabel = useMemo(() => getAllModelsWithLabel(services), [services]);
|
||||
const allModels = useMemo(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { LLMServiceItem } from './LLMServicesRepository';
|
||||
import { LLMServiceItem } from '../repositories/AIConfigRepository';
|
||||
|
||||
export type ModelWithLabel = {
|
||||
llmService: string;
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* 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 { define, observable } from '@formily/reactive';
|
||||
import { ToolsEntry, type ToolsManager } from '@nocobase/client';
|
||||
import { AIEmployee } from '../ai-employees/types';
|
||||
|
||||
export interface LLMServiceItem {
|
||||
llmService: string;
|
||||
llmServiceTitle: string;
|
||||
provider?: string;
|
||||
providerTitle?: string;
|
||||
enabledModels: { label: string; value: string }[];
|
||||
supportWebSearch?: boolean;
|
||||
isToolConflict?: boolean;
|
||||
}
|
||||
|
||||
export class AIConfigRepository {
|
||||
llmServices = observable.shallow<LLMServiceItem[]>([]);
|
||||
llmServicesLoading = false;
|
||||
aiEmployees = observable.shallow<AIEmployee[]>([]);
|
||||
aiEmployeesLoading = false;
|
||||
aiTools = observable.shallow<ToolsEntry[]>([]);
|
||||
aiToolsLoading = false;
|
||||
|
||||
private llmServicesLoaded = false;
|
||||
private aiEmployeesLoaded = false;
|
||||
private aiToolsLoaded = false;
|
||||
private llmServicesInFlight: Promise<LLMServiceItem[]> | null = null;
|
||||
private aiEmployeesInFlight: Promise<AIEmployee[]> | null = null;
|
||||
private aiToolsInFlight: Promise<ToolsEntry[]> | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly apiClient: any,
|
||||
private readonly options?: { toolsManager?: Pick<ToolsManager, 'listTools'> },
|
||||
) {
|
||||
define(this, {
|
||||
llmServices: observable.shallow,
|
||||
llmServicesLoading: observable.ref,
|
||||
aiEmployees: observable.shallow,
|
||||
aiEmployeesLoading: observable.ref,
|
||||
aiTools: observable.shallow,
|
||||
aiToolsLoading: observable.ref,
|
||||
});
|
||||
}
|
||||
|
||||
async getLLMServices(): Promise<LLMServiceItem[]> {
|
||||
if (this.llmServicesInFlight) {
|
||||
return this.llmServicesInFlight;
|
||||
}
|
||||
if (this.llmServicesLoaded) {
|
||||
return this.llmServices;
|
||||
}
|
||||
return this.startRefresh(
|
||||
this.llmServicesInFlight,
|
||||
(promise) => {
|
||||
this.llmServicesInFlight = promise;
|
||||
},
|
||||
() => this.doRefreshLLMServices(),
|
||||
() => this.llmServices,
|
||||
);
|
||||
}
|
||||
|
||||
async refreshLLMServices(): Promise<LLMServiceItem[]> {
|
||||
return this.startRefresh(
|
||||
this.llmServicesInFlight,
|
||||
(promise) => {
|
||||
this.llmServicesInFlight = promise;
|
||||
},
|
||||
() => this.doRefreshLLMServices(),
|
||||
() => this.llmServices,
|
||||
);
|
||||
}
|
||||
|
||||
async getAIEmployees(): Promise<AIEmployee[]> {
|
||||
if (this.aiEmployeesInFlight) {
|
||||
return this.aiEmployeesInFlight;
|
||||
}
|
||||
if (this.aiEmployeesLoaded) {
|
||||
return this.aiEmployees;
|
||||
}
|
||||
return this.startRefresh(
|
||||
this.aiEmployeesInFlight,
|
||||
(promise) => {
|
||||
this.aiEmployeesInFlight = promise;
|
||||
},
|
||||
() => this.doRefreshAIEmployees(),
|
||||
() => this.aiEmployees,
|
||||
);
|
||||
}
|
||||
|
||||
async refreshAIEmployees(): Promise<AIEmployee[]> {
|
||||
return this.startRefresh(
|
||||
this.aiEmployeesInFlight,
|
||||
(promise) => {
|
||||
this.aiEmployeesInFlight = promise;
|
||||
},
|
||||
() => this.doRefreshAIEmployees(),
|
||||
() => this.aiEmployees,
|
||||
);
|
||||
}
|
||||
|
||||
getAIEmployeesMap(): Record<string, AIEmployee> {
|
||||
return this.aiEmployees.reduce<Record<string, AIEmployee>>((acc, aiEmployee) => {
|
||||
acc[aiEmployee.username] = aiEmployee;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
async getAITools(): Promise<ToolsEntry[]> {
|
||||
if (this.aiToolsInFlight) {
|
||||
return this.aiToolsInFlight;
|
||||
}
|
||||
if (this.aiToolsLoaded) {
|
||||
return this.aiTools;
|
||||
}
|
||||
return this.startRefresh(
|
||||
this.aiToolsInFlight,
|
||||
(promise) => {
|
||||
this.aiToolsInFlight = promise;
|
||||
},
|
||||
() => this.doRefreshAITools(),
|
||||
() => this.aiTools,
|
||||
);
|
||||
}
|
||||
|
||||
async refreshAITools(): Promise<ToolsEntry[]> {
|
||||
return this.startRefresh(
|
||||
this.aiToolsInFlight,
|
||||
(promise) => {
|
||||
this.aiToolsInFlight = promise;
|
||||
},
|
||||
() => this.doRefreshAITools(),
|
||||
() => this.aiTools,
|
||||
);
|
||||
}
|
||||
|
||||
private startRefresh<T>(
|
||||
inFlight: Promise<T> | null,
|
||||
setInFlight: (promise: Promise<T> | null) => void,
|
||||
refresh: () => Promise<void>,
|
||||
getData: () => T,
|
||||
): Promise<T> {
|
||||
if (inFlight) {
|
||||
return inFlight;
|
||||
}
|
||||
const promise = refresh()
|
||||
.then(() => getData())
|
||||
.finally(() => {
|
||||
setInFlight(null);
|
||||
});
|
||||
setInFlight(promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
private async doRefreshLLMServices() {
|
||||
this.llmServicesLoading = true;
|
||||
try {
|
||||
const res = await this.apiClient.resource('ai').listAllEnabledModels();
|
||||
const data = Array.isArray(res?.data?.data) ? (res.data.data as LLMServiceItem[]) : [];
|
||||
this.llmServices = data;
|
||||
this.llmServicesLoaded = true;
|
||||
} catch {
|
||||
this.llmServices = [];
|
||||
this.llmServicesLoaded = false;
|
||||
} finally {
|
||||
this.llmServicesLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async doRefreshAIEmployees() {
|
||||
this.aiEmployeesLoading = true;
|
||||
try {
|
||||
const aiEmployees: AIEmployee[] = await this.apiClient
|
||||
.resource('aiEmployees')
|
||||
.listByUser()
|
||||
.then((res) => res?.data?.data);
|
||||
this.aiEmployees = aiEmployees || [];
|
||||
this.aiEmployeesLoaded = true;
|
||||
} catch {
|
||||
this.aiEmployees = [];
|
||||
this.aiEmployeesLoaded = false;
|
||||
} finally {
|
||||
this.aiEmployeesLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async doRefreshAITools() {
|
||||
this.aiToolsLoading = true;
|
||||
try {
|
||||
let tools: ToolsEntry[] = [];
|
||||
if (this.options?.toolsManager) {
|
||||
tools = await this.options.toolsManager.listTools();
|
||||
} else {
|
||||
const { data: res } = await this.apiClient.resource('aiTools').list({});
|
||||
tools = Array.isArray(res?.data) ? res.data : [];
|
||||
}
|
||||
this.aiTools = tools;
|
||||
this.aiToolsLoaded = true;
|
||||
} catch {
|
||||
this.aiTools = [];
|
||||
this.aiToolsLoaded = false;
|
||||
} finally {
|
||||
this.aiToolsLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -8,8 +8,8 @@
|
||||
*/
|
||||
|
||||
import { useFlowEngine } from '@nocobase/flow-engine';
|
||||
import { LLMServicesRepository } from '../LLMServicesRepository';
|
||||
import { AIConfigRepository } from '../AIConfigRepository';
|
||||
|
||||
export const useLLMServicesRepository = (): LLMServicesRepository => {
|
||||
return useFlowEngine().context.llmServicesRepository;
|
||||
export const useAIConfigRepository = (): AIConfigRepository => {
|
||||
return useFlowEngine().context.aiConfigRepository;
|
||||
};
|
||||
+11
-6
@@ -12,19 +12,20 @@ import { useT } from '../../locale';
|
||||
import { Avatar, Popover } from 'antd';
|
||||
import {
|
||||
useChatMessagesStore,
|
||||
useAIEmployeesData,
|
||||
useAIConfigRepository,
|
||||
useChatBoxStore,
|
||||
useChatBoxActions,
|
||||
ProfileCard,
|
||||
avatars,
|
||||
} from '@nocobase/plugin-ai/client';
|
||||
import type { EditorRef } from '@nocobase/client';
|
||||
import { DEFAULT_DATA_SOURCE_KEY } from '@nocobase/client';
|
||||
import { observer } from '@nocobase/flow-engine';
|
||||
import type { FlowSettingsContext } from '@nocobase/flow-engine';
|
||||
|
||||
export const DaraButton: React.FC<{ ctx: FlowSettingsContext<any> }> = ({ ctx }) => {
|
||||
export const DaraButton: React.FC<{ ctx: FlowSettingsContext<any> }> = observer(({ ctx }) => {
|
||||
const t = useT();
|
||||
const { aiEmployees } = useAIEmployeesData();
|
||||
const aiConfigRepository = useAIConfigRepository();
|
||||
const aiEmployees = aiConfigRepository.aiEmployees;
|
||||
const aiEmployee = aiEmployees?.find((e) => e.username === 'dara');
|
||||
const setEditorRef = useChatMessagesStore.use.setEditorRef();
|
||||
const setCurrentEditorRefUid = useChatMessagesStore.use.setCurrentEditorRefUid();
|
||||
@@ -85,11 +86,15 @@ export const DaraButton: React.FC<{ ctx: FlowSettingsContext<any> }> = ({ ctx })
|
||||
logs: [],
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
aiConfigRepository.getAIEmployees();
|
||||
}, [aiConfigRepository]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setEditorRef(uid, panelRef);
|
||||
setCurrentEditorRefUid(uid);
|
||||
return () => setEditorRef(uid, null);
|
||||
}, [uid]);
|
||||
}, [uid, setEditorRef, setCurrentEditorRefUid]);
|
||||
|
||||
const systemPrompt =
|
||||
'If you are not in SQL/Custom mode, first call the tool viz.switchModes; after editing SQL, if you need field samples, call the tool viz.runQuery. Use query.sqlDatasource as the current data source key when executing SQL. Do not render chart previews directly in the chat window.';
|
||||
@@ -159,6 +164,6 @@ export const DaraButton: React.FC<{ ctx: FlowSettingsContext<any> }> = ({ ctx })
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default DaraButton;
|
||||
|
||||
Reference in New Issue
Block a user