mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
chore: update
This commit is contained in:
parent
e79359c683
commit
c3b03776ae
@ -13,7 +13,7 @@
|
|||||||
"@nocobase/test": "1.x"
|
"@nocobase/test": "1.x"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/x": "^1.0.5",
|
"@ant-design/x": "^1.1.0",
|
||||||
"@langchain/core": "^0.3.39",
|
"@langchain/core": "^0.3.39",
|
||||||
"@langchain/deepseek": "^0.0.1",
|
"@langchain/deepseek": "^0.0.1",
|
||||||
"@langchain/openai": "^0.4.3",
|
"@langchain/openai": "^0.4.3",
|
||||||
|
@ -12,6 +12,7 @@ import { createContext } from 'react';
|
|||||||
import { ChatBoxProvider } from './chatbox/ChatBoxProvider';
|
import { ChatBoxProvider } from './chatbox/ChatBoxProvider';
|
||||||
import { useAPIClient, useRequest } from '@nocobase/client';
|
import { useAPIClient, useRequest } from '@nocobase/client';
|
||||||
import { AIEmployee } from './types';
|
import { AIEmployee } from './types';
|
||||||
|
import { AISelectionProvider } from './selector/AISelectorProvider';
|
||||||
|
|
||||||
export const AIEmployeesContext = createContext<{
|
export const AIEmployeesContext = createContext<{
|
||||||
aiEmployees: AIEmployee[];
|
aiEmployees: AIEmployee[];
|
||||||
@ -24,9 +25,11 @@ export const AIEmployeesProvider: React.FC<{
|
|||||||
const [aiEmployees, setAIEmployees] = React.useState<AIEmployee[]>(null);
|
const [aiEmployees, setAIEmployees] = React.useState<AIEmployee[]>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AIEmployeesContext.Provider value={{ aiEmployees, setAIEmployees }}>
|
<AISelectionProvider>
|
||||||
<ChatBoxProvider>{props.children}</ChatBoxProvider>
|
<AIEmployeesContext.Provider value={{ aiEmployees, setAIEmployees }}>
|
||||||
</AIEmployeesContext.Provider>
|
<ChatBoxProvider>{props.children}</ChatBoxProvider>
|
||||||
|
</AIEmployeesContext.Provider>
|
||||||
|
</AISelectionProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Avatar, Divider } from 'antd';
|
||||||
|
import { useToken } from '@nocobase/client';
|
||||||
|
import { AIEmployee } from './types';
|
||||||
|
import { avatars } from './avatars';
|
||||||
|
import { useT } from '../locale';
|
||||||
|
|
||||||
|
export const ProfileCard: React.FC<{
|
||||||
|
aiEmployee: AIEmployee;
|
||||||
|
taskDesc?: string;
|
||||||
|
}> = (props) => {
|
||||||
|
const { aiEmployee, taskDesc } = props;
|
||||||
|
const { token } = useToken();
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '300px',
|
||||||
|
padding: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{aiEmployee ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
src={avatars(aiEmployee.avatar)}
|
||||||
|
size={60}
|
||||||
|
style={{
|
||||||
|
boxShadow: `0px 0px 2px ${token.colorBorder}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: token.fontSizeLG,
|
||||||
|
fontWeight: token.fontWeightStrong,
|
||||||
|
margin: '8px 0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{aiEmployee.nickname}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider
|
||||||
|
orientation="left"
|
||||||
|
plain
|
||||||
|
style={{
|
||||||
|
fontStyle: 'italic',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Bio')}
|
||||||
|
</Divider>
|
||||||
|
<p>{aiEmployee.bio}</p>
|
||||||
|
{taskDesc && (
|
||||||
|
<>
|
||||||
|
<Divider
|
||||||
|
orientation="left"
|
||||||
|
plain
|
||||||
|
style={{
|
||||||
|
fontStyle: 'italic',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Task description')}
|
||||||
|
</Divider>
|
||||||
|
<p>{taskDesc}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* 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 { List, Popover, Button, Avatar, Divider } from 'antd';
|
||||||
|
import { useToken } from '@nocobase/client';
|
||||||
|
import { useAIEmployeesContext } from '../AIEmployeesProvider';
|
||||||
|
import { useT } from '../../locale';
|
||||||
|
import { useChatBoxContext } from './ChatBoxContext';
|
||||||
|
import { avatars } from '../avatars';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { Sender } from '@ant-design/x';
|
||||||
|
import { ProfileCard } from '../ProfileCard';
|
||||||
|
|
||||||
|
export const AIEmployeeHeader: React.FC = () => {
|
||||||
|
const t = useT();
|
||||||
|
const { token } = useToken();
|
||||||
|
const {
|
||||||
|
service: { loading },
|
||||||
|
aiEmployees,
|
||||||
|
} = useAIEmployeesContext();
|
||||||
|
const { switchAIEmployee } = useChatBoxContext();
|
||||||
|
return (
|
||||||
|
<Sender.Header closable={false}>
|
||||||
|
<List
|
||||||
|
loading={loading}
|
||||||
|
dataSource={aiEmployees || []}
|
||||||
|
split={false}
|
||||||
|
itemLayout="horizontal"
|
||||||
|
renderItem={(aiEmployee) => {
|
||||||
|
return (
|
||||||
|
<Popover content={<ProfileCard aiEmployee={aiEmployee} />}>
|
||||||
|
<Button
|
||||||
|
className={css`
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
margin-right: 3px;
|
||||||
|
`}
|
||||||
|
shape="circle"
|
||||||
|
onClick={() => switchAIEmployee(aiEmployee)}
|
||||||
|
>
|
||||||
|
<Avatar src={avatars(aiEmployee.avatar)} size={36} />
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Sender.Header>
|
||||||
|
);
|
||||||
|
};
|
@ -8,77 +8,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useContext, useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { Layout, Card, Divider, Button, Avatar, List, Input, Popover, Empty, Spin, App } from 'antd';
|
import { Layout, Card, Button } from 'antd';
|
||||||
import { Conversations, Sender, Bubble } from '@ant-design/x';
|
import { CloseOutlined, ExpandOutlined, EditOutlined, LayoutOutlined } from '@ant-design/icons';
|
||||||
import type { ConversationsProps } from '@ant-design/x';
|
import { useToken } from '@nocobase/client';
|
||||||
import { CloseOutlined, ExpandOutlined, EditOutlined, LayoutOutlined, DeleteOutlined } from '@ant-design/icons';
|
|
||||||
import { useAPIClient, useToken } from '@nocobase/client';
|
|
||||||
import { useT } from '../../locale';
|
|
||||||
const { Header, Footer, Sider, Content } = Layout;
|
const { Header, Footer, Sider, Content } = Layout;
|
||||||
import { avatars } from '../avatars';
|
|
||||||
import { useAIEmployeesContext } from '../AIEmployeesProvider';
|
|
||||||
import { css } from '@emotion/css';
|
|
||||||
import { ReactComponent as EmptyIcon } from '../empty-icon.svg';
|
|
||||||
import { Attachment } from './Attachment';
|
|
||||||
import { ChatBoxContext } from './ChatBoxContext';
|
import { ChatBoxContext } from './ChatBoxContext';
|
||||||
import { AIEmployee } from '../types';
|
import { Conversations } from './Conversations';
|
||||||
|
import { Messages } from './Messages';
|
||||||
|
import { Sender } from './Sender';
|
||||||
|
import { useAISelectionContext } from '../selector/AISelectorProvider';
|
||||||
|
|
||||||
export const ChatBox: React.FC = () => {
|
export const ChatBox: React.FC = () => {
|
||||||
const { modal, message } = App.useApp();
|
const { setOpen, startNewConversation, currentEmployee } = useContext(ChatBoxContext);
|
||||||
const api = useAPIClient();
|
|
||||||
const {
|
|
||||||
send,
|
|
||||||
setOpen,
|
|
||||||
filterEmployee,
|
|
||||||
setFilterEmployee,
|
|
||||||
conversations: conversationsService,
|
|
||||||
currentConversation,
|
|
||||||
setCurrentConversation,
|
|
||||||
messages,
|
|
||||||
setMessages,
|
|
||||||
roles,
|
|
||||||
attachments,
|
|
||||||
setAttachments,
|
|
||||||
responseLoading,
|
|
||||||
senderRef,
|
|
||||||
senderValue,
|
|
||||||
setSenderValue,
|
|
||||||
clear,
|
|
||||||
} = useContext(ChatBoxContext);
|
|
||||||
const { loading: ConversationsLoading, data: conversationsRes } = conversationsService;
|
|
||||||
const {
|
|
||||||
aiEmployees,
|
|
||||||
service: { loading },
|
|
||||||
} = useAIEmployeesContext();
|
|
||||||
const t = useT();
|
|
||||||
const { token } = useToken();
|
const { token } = useToken();
|
||||||
const [showConversations, setShowConversations] = useState(false);
|
const [showConversations, setShowConversations] = useState(false);
|
||||||
const aiEmployeesList = [{ username: 'all' } as AIEmployee, ...(aiEmployees || [])];
|
const { selectable } = useAISelectionContext();
|
||||||
const conversations: ConversationsProps['items'] = (conversationsRes || []).map((conversation) => ({
|
|
||||||
key: conversation.sessionId,
|
|
||||||
label: conversation.title,
|
|
||||||
timestamp: new Date(conversation.updatedAt).getTime(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const deleteConversation = async (sessionId: string) => {
|
|
||||||
await api.resource('aiConversations').destroy({
|
|
||||||
filterByTk: sessionId,
|
|
||||||
});
|
|
||||||
message.success(t('Deleted successfully'));
|
|
||||||
conversationsService.refresh();
|
|
||||||
clear();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMessages = async (sessionId: string) => {
|
|
||||||
const res = await api.resource('aiConversations').getMessages({
|
|
||||||
sessionId,
|
|
||||||
});
|
|
||||||
const messages = res?.data?.data;
|
|
||||||
if (!messages) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setMessages(messages.reverse());
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -90,111 +34,11 @@ export const ChatBox: React.FC = () => {
|
|||||||
maxWidth: '760px',
|
maxWidth: '760px',
|
||||||
height: '90%',
|
height: '90%',
|
||||||
maxHeight: '560px',
|
maxHeight: '560px',
|
||||||
zIndex: 1000,
|
zIndex: selectable ? -1 : 1000,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card style={{ height: '100%' }} bodyStyle={{ height: '100%', paddingTop: 0 }}>
|
<Card style={{ height: '100%' }} styles={{ body: { height: '100%', paddingTop: 0 } }}>
|
||||||
<Layout style={{ height: '100%' }}>
|
<Layout style={{ height: '100%' }}>
|
||||||
<Sider
|
|
||||||
width="42px"
|
|
||||||
style={{
|
|
||||||
backgroundColor: token.colorBgContainer,
|
|
||||||
marginRight: '5px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<List
|
|
||||||
loading={loading}
|
|
||||||
dataSource={aiEmployeesList}
|
|
||||||
split={false}
|
|
||||||
itemLayout="horizontal"
|
|
||||||
renderItem={(aiEmployee) => {
|
|
||||||
const highlight =
|
|
||||||
aiEmployee.username === filterEmployee
|
|
||||||
? `color: ${token.colorPrimary};
|
|
||||||
border-color: ${token.colorPrimary};`
|
|
||||||
: '';
|
|
||||||
return aiEmployee.username === 'all' ? (
|
|
||||||
<Button
|
|
||||||
onClick={() => setFilterEmployee(aiEmployee.username)}
|
|
||||||
className={css`
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
line-height: 40px;
|
|
||||||
font-weight: ${token.fontWeightStrong};
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
${highlight}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
ALL
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Popover
|
|
||||||
placement="rightTop"
|
|
||||||
content={
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '300px',
|
|
||||||
padding: '8px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
src={avatars(aiEmployee.avatar)}
|
|
||||||
size={60}
|
|
||||||
className={css``}
|
|
||||||
style={{
|
|
||||||
boxShadow: `0px 0px 2px ${token.colorBorder}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: token.fontSizeLG,
|
|
||||||
fontWeight: token.fontWeightStrong,
|
|
||||||
margin: '8px 0',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{aiEmployee.nickname}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Divider
|
|
||||||
orientation="left"
|
|
||||||
plain
|
|
||||||
style={{
|
|
||||||
fontStyle: 'italic',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Bio')}
|
|
||||||
</Divider>
|
|
||||||
<p>{aiEmployee.bio}</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className={css`
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
line-height: 40px;
|
|
||||||
padding: 0;
|
|
||||||
${highlight}
|
|
||||||
`}
|
|
||||||
onClick={() => {
|
|
||||||
clear();
|
|
||||||
setFilterEmployee(aiEmployee.username);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar src={avatars(aiEmployee.avatar)} shape="square" size={40} />
|
|
||||||
</Button>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Sider>
|
|
||||||
<Sider
|
<Sider
|
||||||
width="30%"
|
width="30%"
|
||||||
style={{
|
style={{
|
||||||
@ -203,57 +47,7 @@ export const ChatBox: React.FC = () => {
|
|||||||
marginRight: '5px',
|
marginRight: '5px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Layout>
|
<Conversations />
|
||||||
<Header
|
|
||||||
style={{
|
|
||||||
backgroundColor: token.colorBgContainer,
|
|
||||||
height: '48px',
|
|
||||||
lineHeight: '48px',
|
|
||||||
padding: '0 5px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input.Search style={{ verticalAlign: 'middle' }} />
|
|
||||||
</Header>
|
|
||||||
<Content>
|
|
||||||
<Spin spinning={ConversationsLoading}>
|
|
||||||
{conversations && conversations.length ? (
|
|
||||||
<Conversations
|
|
||||||
activeKey={currentConversation}
|
|
||||||
onActiveChange={(sessionId) => {
|
|
||||||
if (sessionId === currentConversation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setCurrentConversation(sessionId);
|
|
||||||
getMessages(sessionId);
|
|
||||||
}}
|
|
||||||
items={conversations}
|
|
||||||
menu={(conversation) => ({
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: 'Delete',
|
|
||||||
key: 'delete',
|
|
||||||
icon: <DeleteOutlined />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onClick: ({ key }) => {
|
|
||||||
switch (key) {
|
|
||||||
case 'delete':
|
|
||||||
modal.confirm({
|
|
||||||
title: t('Delete this conversation?'),
|
|
||||||
content: t('Are you sure to delete this conversation?'),
|
|
||||||
onOk: () => deleteConversation(conversation.key),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
||||||
)}
|
|
||||||
</Spin>
|
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
</Sider>
|
</Sider>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Header
|
<Header
|
||||||
@ -275,15 +69,7 @@ export const ChatBox: React.FC = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
onClick={() => setShowConversations(!showConversations)}
|
onClick={() => setShowConversations(!showConversations)}
|
||||||
/>
|
/>
|
||||||
{filterEmployee !== 'all' ? (
|
{currentEmployee ? <Button icon={<EditOutlined />} type="text" onClick={startNewConversation} /> : null}
|
||||||
<Button
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
type="text"
|
|
||||||
onClick={() => {
|
|
||||||
clear();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -301,27 +87,7 @@ export const ChatBox: React.FC = () => {
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{messages?.length ? (
|
<Messages />
|
||||||
<Bubble.List
|
|
||||||
style={{
|
|
||||||
marginRight: '8px',
|
|
||||||
}}
|
|
||||||
roles={roles}
|
|
||||||
items={messages}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
width: '64px',
|
|
||||||
top: '50%',
|
|
||||||
left: '50%',
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EmptyIcon />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Content>
|
</Content>
|
||||||
<Footer
|
<Footer
|
||||||
style={{
|
style={{
|
||||||
@ -329,61 +95,8 @@ export const ChatBox: React.FC = () => {
|
|||||||
padding: 0,
|
padding: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Sender
|
<Sender />
|
||||||
value={senderValue}
|
|
||||||
ref={senderRef}
|
|
||||||
onChange={(value) => {
|
|
||||||
setSenderValue(value);
|
|
||||||
}}
|
|
||||||
onSubmit={(content) =>
|
|
||||||
send({
|
|
||||||
sessionId: currentConversation,
|
|
||||||
aiEmployee: { username: filterEmployee },
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
content,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
header={
|
|
||||||
attachments.length ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: '8px 8px 0',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{attachments.map((attachment, index) => {
|
|
||||||
return (
|
|
||||||
<Attachment
|
|
||||||
key={index}
|
|
||||||
closeable={true}
|
|
||||||
onClose={() => {
|
|
||||||
setAttachments(attachments.filter((_, i) => i !== index));
|
|
||||||
}}
|
|
||||||
{...attachment}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
disabled={filterEmployee === 'all' && !currentConversation}
|
|
||||||
placeholder={filterEmployee === 'all' && !currentConversation ? t('Please choose an AI employee.') : ''}
|
|
||||||
loading={responseLoading}
|
|
||||||
/>
|
|
||||||
</Footer>
|
</Footer>
|
||||||
{/* </Layout> */}
|
|
||||||
{/* <Sider */}
|
|
||||||
{/* width="25%" */}
|
|
||||||
{/* style={{ */}
|
|
||||||
{/* backgroundColor: token.colorBgContainer, */}
|
|
||||||
{/* }} */}
|
|
||||||
{/* > */}
|
|
||||||
{/* <Conversations items={employees} /> */}
|
|
||||||
{/* </Sider> */}
|
|
||||||
{/* </Layout> */}
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -7,10 +7,19 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AttachmentProps, Conversation, Message, Action, SendOptions, AIEmployee } from '../types';
|
import {
|
||||||
import { Avatar, GetProp, GetRef, Button, Alert, Space } from 'antd';
|
AttachmentProps,
|
||||||
|
Conversation,
|
||||||
|
Message,
|
||||||
|
Action,
|
||||||
|
SendOptions,
|
||||||
|
AIEmployee,
|
||||||
|
MessageType,
|
||||||
|
ShortcutOptions,
|
||||||
|
} from '../types';
|
||||||
|
import { Avatar, GetProp, GetRef, Button, Alert, Space, Popover } from 'antd';
|
||||||
import type { Sender } from '@ant-design/x';
|
import type { Sender } from '@ant-design/x';
|
||||||
import React, { createContext, useContext, useEffect, useState, useRef } from 'react';
|
import React, { createContext, useContext, useEffect, useState, useRef, useMemo } from 'react';
|
||||||
import { Bubble } from '@ant-design/x';
|
import { Bubble } from '@ant-design/x';
|
||||||
import { useAPIClient, useRequest } from '@nocobase/client';
|
import { useAPIClient, useRequest } from '@nocobase/client';
|
||||||
import { AIEmployeesContext } from '../AIEmployeesProvider';
|
import { AIEmployeesContext } from '../AIEmployeesProvider';
|
||||||
@ -18,12 +27,18 @@ import { Attachment } from './Attachment';
|
|||||||
import { ReloadOutlined, CopyOutlined } from '@ant-design/icons';
|
import { ReloadOutlined, CopyOutlined } from '@ant-design/icons';
|
||||||
import { avatars } from '../avatars';
|
import { avatars } from '../avatars';
|
||||||
import { useChatMessages } from './useChatMessages';
|
import { useChatMessages } from './useChatMessages';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
import { useT } from '../../locale';
|
||||||
|
import { createForm, Form } from '@formily/core';
|
||||||
|
import { ProfileCard } from '../ProfileCard';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { InfoFormMessage } from './InfoForm';
|
||||||
|
|
||||||
export const ChatBoxContext = createContext<{
|
export const ChatBoxContext = createContext<{
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
filterEmployee: string;
|
currentEmployee: AIEmployee;
|
||||||
setFilterEmployee: React.Dispatch<React.SetStateAction<string>>;
|
setCurrentEmployee: React.Dispatch<React.SetStateAction<AIEmployee>>;
|
||||||
conversations: {
|
conversations: {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
data?: Conversation[];
|
data?: Conversation[];
|
||||||
@ -35,18 +50,19 @@ export const ChatBoxContext = createContext<{
|
|||||||
setMessages: React.Dispatch<React.SetStateAction<Message[]>>;
|
setMessages: React.Dispatch<React.SetStateAction<Message[]>>;
|
||||||
roles: { [role: string]: any };
|
roles: { [role: string]: any };
|
||||||
responseLoading: boolean;
|
responseLoading: boolean;
|
||||||
attachments: AttachmentProps[];
|
|
||||||
setAttachments: React.Dispatch<React.SetStateAction<AttachmentProps[]>>;
|
|
||||||
actions: Action[];
|
|
||||||
setActions: React.Dispatch<React.SetStateAction<Action[]>>;
|
|
||||||
senderValue: string;
|
senderValue: string;
|
||||||
setSenderValue: React.Dispatch<React.SetStateAction<string>>;
|
setSenderValue: React.Dispatch<React.SetStateAction<string>>;
|
||||||
senderRef: React.MutableRefObject<GetRef<typeof Sender>>;
|
senderRef: React.MutableRefObject<GetRef<typeof Sender>>;
|
||||||
clear: () => void;
|
senderPlaceholder: string;
|
||||||
|
infoForm: Form;
|
||||||
|
showInfoForm: boolean;
|
||||||
|
switchAIEmployee: (aiEmployee: AIEmployee) => void;
|
||||||
|
startNewConversation: () => void;
|
||||||
|
triggerShortcut: (options: ShortcutOptions) => void;
|
||||||
send(opts: SendOptions): void;
|
send(opts: SendOptions): void;
|
||||||
}>({} as any);
|
}>({} as any);
|
||||||
|
|
||||||
const defaultRoles = {
|
const defaultRoles: GetProp<typeof Bubble.List, 'roles'> = {
|
||||||
user: {
|
user: {
|
||||||
placement: 'end',
|
placement: 'end',
|
||||||
styles: {
|
styles: {
|
||||||
@ -81,6 +97,24 @@ const defaultRoles = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
info: {
|
||||||
|
placement: 'start',
|
||||||
|
avatar: { icon: '', style: { visibility: 'hidden' } },
|
||||||
|
typing: { step: 5, interval: 20 },
|
||||||
|
style: {
|
||||||
|
maxWidth: 400,
|
||||||
|
marginInlineEnd: 48,
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
footer: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// variant: 'borderless',
|
||||||
|
messageRender: (msg: any) => {
|
||||||
|
return <InfoFormMessage values={msg.content} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
action: {
|
action: {
|
||||||
placement: 'start',
|
placement: 'start',
|
||||||
avatar: { icon: '', style: { visibility: 'hidden' } },
|
avatar: { icon: '', style: { visibility: 'hidden' } },
|
||||||
@ -107,7 +141,11 @@ const defaultRoles = {
|
|||||||
|
|
||||||
const aiEmployeeRole = (aiEmployee: AIEmployee) => ({
|
const aiEmployeeRole = (aiEmployee: AIEmployee) => ({
|
||||||
placement: 'start',
|
placement: 'start',
|
||||||
avatar: aiEmployee.avatar ? <Avatar src={avatars(aiEmployee.avatar)} /> : null,
|
avatar: aiEmployee.avatar ? (
|
||||||
|
<Popover content={<ProfileCard aiEmployee={aiEmployee} />} placement="leftTop">
|
||||||
|
<Avatar src={avatars(aiEmployee.avatar)} />
|
||||||
|
</Popover>
|
||||||
|
) : null,
|
||||||
typing: { step: 5, interval: 20 },
|
typing: { step: 5, interval: 20 },
|
||||||
style: {
|
style: {
|
||||||
maxWidth: 400,
|
maxWidth: 400,
|
||||||
@ -139,63 +177,123 @@ const aiEmployeeRole = (aiEmployee: AIEmployee) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useChatBoxContext = () => {
|
export const useSetChatBoxContext = () => {
|
||||||
|
const t = useT();
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const { aiEmployees } = useContext(AIEmployeesContext);
|
const { aiEmployees } = useContext(AIEmployeesContext);
|
||||||
const [openChatBox, setOpenChatBox] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [filterEmployee, setFilterEmployee] = useState('all');
|
const [currentEmployee, setCurrentEmployee] = useState<AIEmployee>(null);
|
||||||
const [currentConversation, setCurrentConversation] = useState<string>();
|
const [currentConversation, setCurrentConversation] = useState<string>();
|
||||||
const { messages, setMessages, attachments, setAttachments, actions, setActions, responseLoading, sendMessages } =
|
const { messages, setMessages, responseLoading, addMessage, sendMessages } = useChatMessages();
|
||||||
useChatMessages();
|
|
||||||
const [senderValue, setSenderValue] = useState<string>('');
|
const [senderValue, setSenderValue] = useState<string>('');
|
||||||
|
const [senderPlaceholder, setSenderPlaceholder] = useState<string>('');
|
||||||
const senderRef = useRef<GetRef<typeof Sender>>(null);
|
const senderRef = useRef<GetRef<typeof Sender>>(null);
|
||||||
const [roles, setRoles] = useState<GetProp<typeof Bubble.List, 'roles'>>(defaultRoles);
|
const [roles, setRoles] = useState<GetProp<typeof Bubble.List, 'roles'>>(defaultRoles);
|
||||||
|
|
||||||
|
const infoForm = useMemo(() => createForm(), []);
|
||||||
|
|
||||||
const conversations = useRequest<Conversation[]>(
|
const conversations = useRequest<Conversation[]>(
|
||||||
() =>
|
() =>
|
||||||
api
|
api
|
||||||
.resource('aiConversations')
|
.resource('aiConversations')
|
||||||
.list({
|
.list({
|
||||||
sort: ['-updatedAt'],
|
sort: ['-updatedAt'],
|
||||||
...(filterEmployee !== 'all'
|
|
||||||
? {
|
|
||||||
filter: {
|
|
||||||
'aiEmployees.username': filterEmployee,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
})
|
})
|
||||||
.then((res) => res?.data?.data),
|
.then((res) => res?.data?.data),
|
||||||
{
|
{
|
||||||
ready: openChatBox,
|
ready: open,
|
||||||
refreshDeps: [filterEmployee],
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const clear = () => {
|
const send = (options: SendOptions) => {
|
||||||
setCurrentConversation(undefined);
|
const sendOptions = {
|
||||||
setMessages([]);
|
...options,
|
||||||
setAttachments([]);
|
onConversationCreate: (sessionId: string) => {
|
||||||
setActions([]);
|
setCurrentConversation(sessionId);
|
||||||
|
conversations.refresh();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (!_.isEmpty(infoForm?.values)) {
|
||||||
|
sendOptions.infoFormValues = { ...infoForm.values };
|
||||||
|
}
|
||||||
setSenderValue('');
|
setSenderValue('');
|
||||||
senderRef.current?.focus();
|
infoForm.reset();
|
||||||
|
sendMessages(sendOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
const send = async (options: SendOptions) => {
|
const updateRole = (aiEmployee: AIEmployee) => {
|
||||||
setSenderValue('');
|
|
||||||
const { aiEmployee } = options;
|
|
||||||
if (!roles[aiEmployee.username]) {
|
if (!roles[aiEmployee.username]) {
|
||||||
setRoles((prev) => ({
|
setRoles((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[aiEmployee.username]: aiEmployeeRole(aiEmployee),
|
[aiEmployee.username]: aiEmployeeRole(aiEmployee),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
sendMessages({
|
};
|
||||||
...options,
|
|
||||||
onConversationCreate: (sessionId: string) => {
|
const switchAIEmployee = (aiEmployee: AIEmployee) => {
|
||||||
setCurrentConversation(sessionId);
|
const greetingMsg = {
|
||||||
conversations.refresh();
|
key: uid(),
|
||||||
|
role: aiEmployee.username,
|
||||||
|
content: {
|
||||||
|
type: 'greeting',
|
||||||
|
content: aiEmployee.greeting || t('Default greeting message', { nickname: aiEmployee.nickname }),
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
setCurrentEmployee(aiEmployee);
|
||||||
|
setSenderPlaceholder(aiEmployee.chatSettings?.senderPlaceholder);
|
||||||
|
infoForm.reset();
|
||||||
|
senderRef.current?.focus();
|
||||||
|
if (!currentConversation) {
|
||||||
|
setMessages([greetingMsg]);
|
||||||
|
} else {
|
||||||
|
addMessage(greetingMsg);
|
||||||
|
setSenderValue('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startNewConversation = () => {
|
||||||
|
setCurrentConversation(undefined);
|
||||||
|
setCurrentEmployee(null);
|
||||||
|
setSenderValue('');
|
||||||
|
infoForm.reset();
|
||||||
|
setMessages([]);
|
||||||
|
senderRef.current?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerShortcut = (options: ShortcutOptions) => {
|
||||||
|
const { aiEmployee, message, infoFormValues, autoSend } = options;
|
||||||
|
updateRole(aiEmployee);
|
||||||
|
if (!open) {
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
if (currentConversation) {
|
||||||
|
setCurrentConversation(undefined);
|
||||||
|
setMessages([]);
|
||||||
|
}
|
||||||
|
setCurrentEmployee(aiEmployee);
|
||||||
|
if (message && message.type === 'text') {
|
||||||
|
setSenderValue(message.content);
|
||||||
|
} else {
|
||||||
|
setSenderValue('');
|
||||||
|
}
|
||||||
|
setMessages([
|
||||||
|
{
|
||||||
|
key: uid(),
|
||||||
|
role: aiEmployee.username,
|
||||||
|
content: {
|
||||||
|
type: 'greeting',
|
||||||
|
content: aiEmployee.greeting || t('Default greeting message', { nickname: aiEmployee.nickname }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
senderRef.current?.focus();
|
||||||
|
infoForm.setValues(infoFormValues);
|
||||||
|
if (autoSend) {
|
||||||
|
send({
|
||||||
|
aiEmployee,
|
||||||
|
messages: [message],
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -215,16 +313,16 @@ export const useChatBoxContext = () => {
|
|||||||
}, [aiEmployees]);
|
}, [aiEmployees]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openChatBox) {
|
if (open) {
|
||||||
senderRef.current?.focus();
|
senderRef.current?.focus();
|
||||||
}
|
}
|
||||||
}, [openChatBox]);
|
}, [open]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
open: openChatBox,
|
open,
|
||||||
setOpen: setOpenChatBox,
|
setOpen,
|
||||||
filterEmployee,
|
currentEmployee,
|
||||||
setFilterEmployee,
|
setCurrentEmployee,
|
||||||
conversations,
|
conversations,
|
||||||
currentConversation,
|
currentConversation,
|
||||||
setCurrentConversation,
|
setCurrentConversation,
|
||||||
@ -232,14 +330,19 @@ export const useChatBoxContext = () => {
|
|||||||
setMessages,
|
setMessages,
|
||||||
roles,
|
roles,
|
||||||
responseLoading,
|
responseLoading,
|
||||||
attachments,
|
|
||||||
setAttachments,
|
|
||||||
actions,
|
|
||||||
setActions,
|
|
||||||
senderRef,
|
senderRef,
|
||||||
senderValue,
|
senderValue,
|
||||||
setSenderValue,
|
setSenderValue,
|
||||||
|
senderPlaceholder,
|
||||||
|
showInfoForm: !!currentEmployee?.chatSettings?.infoForm?.length,
|
||||||
|
infoForm,
|
||||||
|
switchAIEmployee,
|
||||||
|
startNewConversation,
|
||||||
|
triggerShortcut,
|
||||||
send,
|
send,
|
||||||
clear,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useChatBoxContext = () => {
|
||||||
|
return useContext(ChatBoxContext);
|
||||||
|
};
|
||||||
|
@ -13,13 +13,13 @@ import { CurrentUserContext } from '@nocobase/client';
|
|||||||
import { ChatBox } from './ChatBox';
|
import { ChatBox } from './ChatBox';
|
||||||
import icon from '../icon.svg';
|
import icon from '../icon.svg';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { ChatBoxContext, useChatBoxContext } from './ChatBoxContext';
|
import { ChatBoxContext, useSetChatBoxContext } from './ChatBoxContext';
|
||||||
|
|
||||||
export const ChatBoxProvider: React.FC<{
|
export const ChatBoxProvider: React.FC<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const currentUserCtx = useContext(CurrentUserContext);
|
const currentUserCtx = useContext(CurrentUserContext);
|
||||||
const chatBoxCtx = useChatBoxContext();
|
const chatBoxCtx = useSetChatBoxContext();
|
||||||
const { open, setOpen } = chatBoxCtx;
|
const { open, setOpen } = chatBoxCtx;
|
||||||
|
|
||||||
if (!currentUserCtx?.data?.data) {
|
if (!currentUserCtx?.data?.data) {
|
||||||
|
@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Layout, Input, Empty, Spin, App } from 'antd';
|
||||||
|
import { Conversations as AntConversations } from '@ant-design/x';
|
||||||
|
import type { ConversationsProps } from '@ant-design/x';
|
||||||
|
import { useAPIClient, useToken } from '@nocobase/client';
|
||||||
|
import { useChatBoxContext } from './ChatBoxContext';
|
||||||
|
import { useT } from '../../locale';
|
||||||
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
|
const { Header, Content } = Layout;
|
||||||
|
|
||||||
|
export const Conversations: React.FC = () => {
|
||||||
|
const t = useT();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const { modal, message } = App.useApp();
|
||||||
|
const { token } = useToken();
|
||||||
|
const {
|
||||||
|
conversations: conversationsService,
|
||||||
|
currentConversation,
|
||||||
|
setCurrentConversation,
|
||||||
|
setMessages,
|
||||||
|
startNewConversation,
|
||||||
|
} = useChatBoxContext();
|
||||||
|
const { loading: ConversationsLoading, data: conversationsRes } = conversationsService;
|
||||||
|
const conversations: ConversationsProps['items'] = (conversationsRes || []).map((conversation) => ({
|
||||||
|
key: conversation.sessionId,
|
||||||
|
label: conversation.title,
|
||||||
|
timestamp: new Date(conversation.updatedAt).getTime(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const deleteConversation = async (sessionId: string) => {
|
||||||
|
await api.resource('aiConversations').destroy({
|
||||||
|
filterByTk: sessionId,
|
||||||
|
});
|
||||||
|
message.success(t('Deleted successfully'));
|
||||||
|
conversationsService.refresh();
|
||||||
|
startNewConversation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMessages = async (sessionId: string) => {
|
||||||
|
const res = await api.resource('aiConversations').getMessages({
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
const messages = res?.data?.data;
|
||||||
|
if (!messages) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setMessages(messages.reverse());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Header
|
||||||
|
style={{
|
||||||
|
backgroundColor: token.colorBgContainer,
|
||||||
|
height: '48px',
|
||||||
|
lineHeight: '48px',
|
||||||
|
padding: '0 5px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input.Search style={{ verticalAlign: 'middle' }} />
|
||||||
|
</Header>
|
||||||
|
<Content>
|
||||||
|
<Spin spinning={ConversationsLoading}>
|
||||||
|
{conversations && conversations.length ? (
|
||||||
|
<AntConversations
|
||||||
|
activeKey={currentConversation}
|
||||||
|
onActiveChange={(sessionId) => {
|
||||||
|
if (sessionId === currentConversation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCurrentConversation(sessionId);
|
||||||
|
getMessages(sessionId);
|
||||||
|
}}
|
||||||
|
items={conversations}
|
||||||
|
menu={(conversation) => ({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Delete',
|
||||||
|
key: 'delete',
|
||||||
|
icon: <DeleteOutlined />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onClick: ({ key }) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'delete':
|
||||||
|
modal.confirm({
|
||||||
|
title: t('Delete this conversation?'),
|
||||||
|
content: t('Are you sure to delete this conversation?'),
|
||||||
|
onOk: () => deleteConversation(conversation.key),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
)}
|
||||||
|
</Spin>
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Button } from 'antd';
|
||||||
|
import { SelectOutlined } from '@ant-design/icons';
|
||||||
|
import { useChatBoxContext } from './ChatBoxContext';
|
||||||
|
import { useAISelectionContext } from '../selector/AISelectorProvider';
|
||||||
|
|
||||||
|
export const FieldSelector: React.FC = () => {
|
||||||
|
const { currentEmployee, senderValue, setSenderValue, senderRef } = useChatBoxContext();
|
||||||
|
const { startSelect } = useAISelectionContext();
|
||||||
|
|
||||||
|
const handleSelect = () => {
|
||||||
|
startSelect('fields', {
|
||||||
|
onSelect: ({ value }) => {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSenderValue(senderValue + value);
|
||||||
|
senderRef.current?.focus();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Button disabled={!currentEmployee} type="text" icon={<SelectOutlined />} onClick={handleSelect} />;
|
||||||
|
};
|
@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* 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, { useMemo } from 'react';
|
||||||
|
import { AIEmployee } from '../types';
|
||||||
|
import { SchemaComponent } from '@nocobase/client';
|
||||||
|
import { BlockSelector } from '../selector/BlockSelector';
|
||||||
|
import { useChatBoxContext } from './ChatBoxContext';
|
||||||
|
import { useT } from '../../locale';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
import { createForm } from '@formily/core';
|
||||||
|
import { Card } from 'antd';
|
||||||
|
|
||||||
|
const schemaMap = {
|
||||||
|
blocks: {
|
||||||
|
'x-component': 'BlockSelector',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
collections: {
|
||||||
|
'x-component': 'CollectionSelect',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InfoForm: React.FC<{
|
||||||
|
aiEmployee: AIEmployee;
|
||||||
|
}> = ({ aiEmployee }) => {
|
||||||
|
const infoForm = aiEmployee?.chatSettings?.infoForm || [];
|
||||||
|
const schemaProperties = infoForm.map((field) => ({
|
||||||
|
name: field.name,
|
||||||
|
title: field.title,
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
...schemaMap[field.type],
|
||||||
|
}));
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
components={{ BlockSelector }}
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
properties: schemaProperties.reduce((acc, field) => {
|
||||||
|
acc[field.name] = field;
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReadPrettyInfoForm: React.FC<{
|
||||||
|
aiEmployee: AIEmployee;
|
||||||
|
}> = ({ aiEmployee }) => {
|
||||||
|
const infoForm = aiEmployee?.chatSettings?.infoForm || [];
|
||||||
|
const schemaProperties = infoForm.map((field) => ({
|
||||||
|
name: field.name,
|
||||||
|
title: field.title,
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-decorator-props': {
|
||||||
|
style: {
|
||||||
|
marginBottom: '5px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
}));
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
properties: schemaProperties.reduce((acc, field) => {
|
||||||
|
acc[field.name] = field;
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InfoFormMessage: React.FC<{
|
||||||
|
values: any;
|
||||||
|
}> = ({ values }) => {
|
||||||
|
const { currentEmployee } = useChatBoxContext();
|
||||||
|
const t = useT();
|
||||||
|
const form = useMemo(
|
||||||
|
() =>
|
||||||
|
createForm({
|
||||||
|
initialValues: values,
|
||||||
|
}),
|
||||||
|
[values],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{t('I will use the following information')}
|
||||||
|
<SchemaComponent
|
||||||
|
components={{ ReadPrettyInfoForm }}
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
[uid()]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormV2',
|
||||||
|
'x-decorator-props': {
|
||||||
|
form,
|
||||||
|
layout: 'horizontal',
|
||||||
|
},
|
||||||
|
'x-component': 'ReadPrettyInfoForm',
|
||||||
|
'x-component-props': {
|
||||||
|
aiEmployee: currentEmployee,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Bubble } from '@ant-design/x';
|
||||||
|
import { useChatBoxContext } from './ChatBoxContext';
|
||||||
|
import { ReactComponent as EmptyIcon } from '../empty-icon.svg';
|
||||||
|
|
||||||
|
export const Messages: React.FC = () => {
|
||||||
|
const { messages, roles } = useChatBoxContext();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{messages?.length ? (
|
||||||
|
<Bubble.List
|
||||||
|
style={{
|
||||||
|
marginRight: '8px',
|
||||||
|
}}
|
||||||
|
roles={roles}
|
||||||
|
items={messages}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: '64px',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EmptyIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 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, { memo } from 'react';
|
||||||
|
import { Sender as AntSender } from '@ant-design/x';
|
||||||
|
import { useChatBoxContext } from './ChatBoxContext';
|
||||||
|
import { SenderPrefix } from './SenderPrefix';
|
||||||
|
import { Attachment } from './Attachment';
|
||||||
|
import { AIEmployeeHeader } from './AIEmployeeHeader';
|
||||||
|
import { useT } from '../../locale';
|
||||||
|
import { SenderHeader } from './SenderHeader';
|
||||||
|
import { SenderFooter } from './SenderFooter';
|
||||||
|
|
||||||
|
export const Sender: React.FC = memo(() => {
|
||||||
|
const t = useT();
|
||||||
|
const {
|
||||||
|
senderValue,
|
||||||
|
setSenderValue,
|
||||||
|
senderPlaceholder,
|
||||||
|
send,
|
||||||
|
currentConversation,
|
||||||
|
currentEmployee,
|
||||||
|
responseLoading,
|
||||||
|
showInfoForm,
|
||||||
|
senderRef,
|
||||||
|
} = useChatBoxContext();
|
||||||
|
return (
|
||||||
|
<AntSender
|
||||||
|
value={senderValue}
|
||||||
|
ref={senderRef}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSenderValue(value);
|
||||||
|
}}
|
||||||
|
onSubmit={(content) =>
|
||||||
|
send({
|
||||||
|
sessionId: currentConversation,
|
||||||
|
aiEmployee: currentEmployee,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
prefix={currentConversation || !showInfoForm ? <SenderPrefix /> : null}
|
||||||
|
header={!currentConversation ? <SenderHeader /> : null}
|
||||||
|
loading={responseLoading}
|
||||||
|
footer={({ components }) => <SenderFooter components={components} />}
|
||||||
|
disabled={!currentEmployee}
|
||||||
|
placeholder={!currentEmployee ? t('Please choose an AI employee') : senderPlaceholder}
|
||||||
|
actions={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Divider, Flex } from 'antd';
|
||||||
|
import { useChatBoxContext } from './ChatBoxContext';
|
||||||
|
import { FieldSelector } from './FieldSelector';
|
||||||
|
import { useT } from '../../locale';
|
||||||
|
|
||||||
|
export const SenderFooter: React.FC<{
|
||||||
|
components: any;
|
||||||
|
}> = ({ components }) => {
|
||||||
|
const t = useT();
|
||||||
|
const { SendButton, LoadingButton } = components;
|
||||||
|
const { responseLoading: loading } = useChatBoxContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<Flex gap="small" align="center">
|
||||||
|
<FieldSelector />
|
||||||
|
<Divider type="vertical" />
|
||||||
|
</Flex>
|
||||||
|
<Flex align="center">
|
||||||
|
{loading ? <LoadingButton type="default" /> : <SendButton type="primary" disabled={false} />}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* 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, { useMemo } from 'react';
|
||||||
|
import { useChatBoxContext } from './ChatBoxContext';
|
||||||
|
import { Sender } from '@ant-design/x';
|
||||||
|
import { AIEmployeeHeader } from './AIEmployeeHeader';
|
||||||
|
import { Avatar } from 'antd';
|
||||||
|
import { avatars } from '../avatars';
|
||||||
|
import { useT } from '../../locale';
|
||||||
|
import { SchemaComponent, useToken } from '@nocobase/client';
|
||||||
|
import { InfoForm } from './InfoForm';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
|
||||||
|
export const SenderHeader: React.FC = () => {
|
||||||
|
const t = useT();
|
||||||
|
const { token } = useToken();
|
||||||
|
const { currentEmployee, startNewConversation, showInfoForm, infoForm: form } = useChatBoxContext();
|
||||||
|
return currentEmployee ? (
|
||||||
|
showInfoForm ? (
|
||||||
|
<Sender.Header
|
||||||
|
onOpenChange={startNewConversation}
|
||||||
|
title={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar src={avatars(currentEmployee.avatar)} />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginLeft: '4px',
|
||||||
|
paddingTop: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: token.colorTextDisabled,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Please tell me the following information')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SchemaComponent
|
||||||
|
components={{ InfoForm }}
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
[uid()]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormV2',
|
||||||
|
'x-decorator-props': {
|
||||||
|
form,
|
||||||
|
},
|
||||||
|
'x-component': 'InfoForm',
|
||||||
|
'x-component-props': {
|
||||||
|
aiEmployee: currentEmployee,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Sender.Header>
|
||||||
|
) : null
|
||||||
|
) : (
|
||||||
|
<AIEmployeeHeader />
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* 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, { useContext } from 'react';
|
||||||
|
import { ChatBoxContext } from './ChatBoxContext';
|
||||||
|
import { avatars } from '../avatars';
|
||||||
|
import { Avatar } from 'antd';
|
||||||
|
|
||||||
|
export const SenderPrefix: React.FC = () => {
|
||||||
|
const { currentEmployee } = useContext(ChatBoxContext);
|
||||||
|
return currentEmployee ? (
|
||||||
|
<Avatar
|
||||||
|
src={avatars(currentEmployee.avatar)}
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
};
|
@ -18,14 +18,6 @@ export const useChatMessages = () => {
|
|||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [responseLoading, setResponseLoading] = useState(false);
|
const [responseLoading, setResponseLoading] = useState(false);
|
||||||
const [attachments, setAttachments] = useState<AttachmentProps[]>([]);
|
|
||||||
const [actions, setActions] = useState<Action[]>([]);
|
|
||||||
|
|
||||||
const clearMessages = () => {
|
|
||||||
setMessages([]);
|
|
||||||
setAttachments([]);
|
|
||||||
setActions([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addMessage = (message: Message) => {
|
const addMessage = (message: Message) => {
|
||||||
setMessages((prev) => [...prev, message]);
|
setMessages((prev) => [...prev, message]);
|
||||||
@ -108,34 +100,24 @@ export const useChatMessages = () => {
|
|||||||
sessionId,
|
sessionId,
|
||||||
aiEmployee,
|
aiEmployee,
|
||||||
messages: sendMsgs,
|
messages: sendMsgs,
|
||||||
greeting,
|
infoFormValues,
|
||||||
onConversationCreate,
|
onConversationCreate,
|
||||||
}: SendOptions & {
|
}: SendOptions & {
|
||||||
onConversationCreate?: (sessionId: string) => void;
|
onConversationCreate?: (sessionId: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const msgs: Message[] = [];
|
const msgs: Message[] = [];
|
||||||
if (greeting) {
|
|
||||||
msgs.push({
|
|
||||||
key: uid(),
|
|
||||||
role: aiEmployee.username,
|
|
||||||
content: {
|
|
||||||
type: 'greeting',
|
|
||||||
content: aiEmployee.greeting || t('Default greeting message', { nickname: aiEmployee.nickname }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!sendMsgs.length) {
|
if (!sendMsgs.length) {
|
||||||
addMessages(msgs);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (attachments.length) {
|
if (infoFormValues) {
|
||||||
msgs.push(
|
msgs.push({
|
||||||
...attachments.map((attachment) => ({
|
key: uid(),
|
||||||
key: uid(),
|
role: 'info',
|
||||||
role: 'user',
|
content: {
|
||||||
content: attachment,
|
type: 'info',
|
||||||
})),
|
content: infoFormValues,
|
||||||
);
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
msgs.push(...sendMsgs.map((msg) => ({ key: uid(), role: 'user', content: msg })));
|
msgs.push(...sendMsgs.map((msg) => ({ key: uid(), role: 'user', content: msg })));
|
||||||
addMessages(msgs);
|
addMessages(msgs);
|
||||||
@ -152,7 +134,6 @@ export const useChatMessages = () => {
|
|||||||
sessionId = conversation.sessionId;
|
sessionId = conversation.sessionId;
|
||||||
onConversationCreate?.(sessionId);
|
onConversationCreate?.(sessionId);
|
||||||
}
|
}
|
||||||
setAttachments([]);
|
|
||||||
setResponseLoading(true);
|
setResponseLoading(true);
|
||||||
|
|
||||||
addMessage({
|
addMessage({
|
||||||
@ -183,34 +164,31 @@ export const useChatMessages = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { result, error } = await processStreamResponse(sendRes.data);
|
const { result, error } = await processStreamResponse(sendRes.data);
|
||||||
if (actions.length && !error) {
|
// if (actions.length && !error) {
|
||||||
addMessages(
|
// addMessages(
|
||||||
actions.map((action) => ({
|
// actions.map((action) => ({
|
||||||
key: uid(),
|
// key: uid(),
|
||||||
role: 'action',
|
// role: 'action',
|
||||||
content: {
|
// content: {
|
||||||
type: 'action',
|
// type: 'action',
|
||||||
icon: action.icon,
|
// icon: action.icon,
|
||||||
content: action.content,
|
// content: action.content,
|
||||||
onClick: () => {
|
// onClick: () => {
|
||||||
action.onClick(result);
|
// action.onClick(result);
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
})),
|
// })),
|
||||||
);
|
// );
|
||||||
setActions([]);
|
// setActions([]);
|
||||||
}
|
// }
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
|
addMessage,
|
||||||
|
addMessages,
|
||||||
setMessages,
|
setMessages,
|
||||||
attachments,
|
|
||||||
setAttachments,
|
|
||||||
actions,
|
|
||||||
setActions,
|
|
||||||
responseLoading,
|
responseLoading,
|
||||||
sendMessages,
|
sendMessages,
|
||||||
clearMessages,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -7,24 +7,20 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useContext } from 'react';
|
import React from 'react';
|
||||||
import { Avatar, Tag, Popover, Divider, Button } from 'antd';
|
import { Avatar, Popover, Button } from 'antd';
|
||||||
import { avatars } from '../avatars';
|
import { avatars } from '../avatars';
|
||||||
import {
|
import {
|
||||||
SortableItem,
|
SortableItem,
|
||||||
useBlockContext,
|
|
||||||
useLocalVariables,
|
useLocalVariables,
|
||||||
useSchemaToolbarRender,
|
useSchemaToolbarRender,
|
||||||
useToken,
|
|
||||||
useVariables,
|
useVariables,
|
||||||
withDynamicSchemaProps,
|
withDynamicSchemaProps,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { useFieldSchema } from '@formily/react';
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { useT } from '../../locale';
|
import { useChatBoxContext } from '../chatbox/ChatBoxContext';
|
||||||
import { css } from '@emotion/css';
|
|
||||||
import { useAIEmployeeChatContext } from '../AIEmployeeChatProvider';
|
|
||||||
import { ChatBoxContext } from '../chatbox/ChatBoxContext';
|
|
||||||
import { AIEmployee } from '../types';
|
import { AIEmployee } from '../types';
|
||||||
|
import { ProfileCard } from '../ProfileCard';
|
||||||
|
|
||||||
async function replaceVariables(template, variables, localVariables = {}) {
|
async function replaceVariables(template, variables, localVariables = {}) {
|
||||||
const regex = /\{\{\s*(.*?)\s*\}\}/g;
|
const regex = /\{\{\s*(.*?)\s*\}\}/g;
|
||||||
@ -64,17 +60,18 @@ async function replaceVariables(template, variables, localVariables = {}) {
|
|||||||
export const AIEmployeeButton: React.FC<{
|
export const AIEmployeeButton: React.FC<{
|
||||||
aiEmployee: AIEmployee;
|
aiEmployee: AIEmployee;
|
||||||
taskDesc?: string;
|
taskDesc?: string;
|
||||||
attachments: string[];
|
autoSend?: boolean;
|
||||||
actions: string[];
|
message: {
|
||||||
}> = withDynamicSchemaProps(({ aiEmployee, taskDesc, attachments: selectedAttachments, actions: selectedActions }) => {
|
type: string;
|
||||||
const t = useT();
|
content: string;
|
||||||
const { setOpen, send, setAttachments, setFilterEmployee, setActions, clear } = useContext(ChatBoxContext);
|
};
|
||||||
const { token } = useToken();
|
infoForm: any;
|
||||||
|
}> = withDynamicSchemaProps(({ aiEmployee, taskDesc, message, infoForm: infoFormValues, autoSend }) => {
|
||||||
|
const { triggerShortcut } = useChatBoxContext();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { render } = useSchemaToolbarRender(fieldSchema);
|
const { render } = useSchemaToolbarRender(fieldSchema);
|
||||||
const variables = useVariables();
|
const variables = useVariables();
|
||||||
const localVariables = useLocalVariables();
|
const localVariables = useLocalVariables();
|
||||||
const { attachments, actions } = useAIEmployeeChatContext();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SortableItem
|
<SortableItem
|
||||||
@ -82,110 +79,23 @@ export const AIEmployeeButton: React.FC<{
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
clear();
|
let msg;
|
||||||
setOpen(true);
|
if (message && message.content) {
|
||||||
setFilterEmployee(aiEmployee.username);
|
|
||||||
if (selectedAttachments && selectedAttachments.length) {
|
|
||||||
setAttachments((prev) => {
|
|
||||||
const newAttachments = selectedAttachments.map((name: string) => {
|
|
||||||
const attachment = attachments[name];
|
|
||||||
return {
|
|
||||||
type: attachment.type,
|
|
||||||
title: attachment.title,
|
|
||||||
content: attachment.content,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return [...prev, ...newAttachments];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (selectedActions && selectedActions.length) {
|
|
||||||
setActions((prev) => {
|
|
||||||
const newActions = selectedActions.map((name: string) => {
|
|
||||||
const action = actions[name];
|
|
||||||
return {
|
|
||||||
icon: action.icon,
|
|
||||||
content: action.title,
|
|
||||||
onClick: action.action,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return [...prev, ...newActions];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const messages = [];
|
|
||||||
const message = fieldSchema['x-component-props']?.message;
|
|
||||||
if (message) {
|
|
||||||
const content = await replaceVariables(message.content, variables, localVariables);
|
const content = await replaceVariables(message.content, variables, localVariables);
|
||||||
messages.push({
|
msg = {
|
||||||
type: message.type || 'text',
|
type: message.type || 'text',
|
||||||
content,
|
content,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
send({
|
triggerShortcut({
|
||||||
aiEmployee,
|
aiEmployee,
|
||||||
messages,
|
message: msg,
|
||||||
greeting: true,
|
autoSend,
|
||||||
|
infoFormValues,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Popover
|
<Popover content={<ProfileCard taskDesc={taskDesc} aiEmployee={aiEmployee} />}>
|
||||||
content={
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '300px',
|
|
||||||
padding: '8px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
src={avatars(aiEmployee.avatar)}
|
|
||||||
size={60}
|
|
||||||
className={css``}
|
|
||||||
style={{
|
|
||||||
boxShadow: `0px 0px 2px ${token.colorBorder}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: token.fontSizeLG,
|
|
||||||
fontWeight: token.fontWeightStrong,
|
|
||||||
margin: '8px 0',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{aiEmployee.nickname}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Divider
|
|
||||||
orientation="left"
|
|
||||||
plain
|
|
||||||
style={{
|
|
||||||
fontStyle: 'italic',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Bio')}
|
|
||||||
</Divider>
|
|
||||||
<p>{aiEmployee.bio}</p>
|
|
||||||
{taskDesc && (
|
|
||||||
<>
|
|
||||||
<Divider
|
|
||||||
orientation="left"
|
|
||||||
plain
|
|
||||||
style={{
|
|
||||||
fontStyle: 'italic',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Task description')}
|
|
||||||
</Divider>
|
|
||||||
<p>{taskDesc}</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
shape="circle"
|
shape="circle"
|
||||||
style={{
|
style={{
|
||||||
|
@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* 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 { SchemaComponent } from '@nocobase/client';
|
||||||
|
import { useT } from '../../locale';
|
||||||
|
import { InputsFormSettings } from './InputsFormSettings';
|
||||||
|
import { ArrayItems } from '@formily/antd-v5';
|
||||||
|
|
||||||
|
export const ChatSettings: React.FC = () => {
|
||||||
|
const t = useT();
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
components={{ InputsFormSettings, ArrayItems }}
|
||||||
|
schema={{
|
||||||
|
type: 'object',
|
||||||
|
name: 'chatSettings',
|
||||||
|
properties: {
|
||||||
|
newConversationSettings: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Divider',
|
||||||
|
'x-component-props': {
|
||||||
|
children: t('New conversation settings'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
senderPlaceholder: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
title: t('Sender placeholder'),
|
||||||
|
},
|
||||||
|
infoForm: {
|
||||||
|
type: 'array',
|
||||||
|
title: t('Required information form'),
|
||||||
|
description: t(
|
||||||
|
'Provide a form for the user to fill in the required information when starting a new conversation',
|
||||||
|
),
|
||||||
|
'x-component': 'ArrayItems',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
space: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Space',
|
||||||
|
properties: {
|
||||||
|
sort: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'ArrayItems.SortHandle',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {
|
||||||
|
placeholder: t('Field name'),
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {
|
||||||
|
placeholder: t('Field title'),
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-component-props': {
|
||||||
|
placeholder: t('Field type'),
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
enum: [
|
||||||
|
{
|
||||||
|
label: t('Blocks'),
|
||||||
|
value: 'blocks',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('Collections'),
|
||||||
|
value: 'collections',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'ArrayItems.Remove',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
add: {
|
||||||
|
type: 'void',
|
||||||
|
title: t('Add field'),
|
||||||
|
'x-component': 'ArrayItems.Addition',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -22,12 +22,13 @@ import { useT } from '../../locale';
|
|||||||
const { Meta } = Card;
|
const { Meta } = Card;
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
import { AvatarSelect } from './AvatarSelect';
|
|
||||||
import { useForm } from '@formily/react';
|
import { useForm } from '@formily/react';
|
||||||
import { createForm } from '@formily/core';
|
import { createForm } from '@formily/core';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { avatars } from '../avatars';
|
import { avatars } from '../avatars';
|
||||||
import { ModelSettings } from './ModelSettings';
|
import { ModelSettings } from './ModelSettings';
|
||||||
|
import { ProfileSettings } from './ProfileSettings';
|
||||||
|
import { ChatSettings } from './ChatSettings';
|
||||||
|
|
||||||
const EmployeeContext = createContext(null);
|
const EmployeeContext = createContext(null);
|
||||||
|
|
||||||
@ -38,63 +39,12 @@ const AIEmployeeForm: React.FC = () => {
|
|||||||
{
|
{
|
||||||
key: 'profile',
|
key: 'profile',
|
||||||
label: 'Profile',
|
label: 'Profile',
|
||||||
children: (
|
children: <ProfileSettings />,
|
||||||
<SchemaComponent
|
},
|
||||||
components={{ AvatarSelect }}
|
{
|
||||||
schema={{
|
key: 'chat',
|
||||||
type: 'void',
|
label: 'Chat settings',
|
||||||
properties: {
|
children: <ChatSettings />,
|
||||||
username: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Username',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'Input',
|
|
||||||
},
|
|
||||||
nickname: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Nickname',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'Input',
|
|
||||||
},
|
|
||||||
avatar: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Avatar',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'AvatarSelect',
|
|
||||||
},
|
|
||||||
bio: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Bio',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'Input.TextArea',
|
|
||||||
'x-component-props': {
|
|
||||||
placeholder:
|
|
||||||
'The introduction to the AI employee will inform human colleagues about its skills and how to use it. This information will be displayed on the employee’s profile. This will not be part of the prompt of this AI employee.',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
about: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'About me',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'Input.TextArea',
|
|
||||||
'x-component-props': {
|
|
||||||
placeholder:
|
|
||||||
'Define the AI employee’s role, guide its work, and instruct it complete user-assigned tasks. This will be part of the prompt of this AI employee.',
|
|
||||||
autoSize: {
|
|
||||||
minRows: 15,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
greeting: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Greeting message',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'Input.TextArea',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// key: 'skills',
|
// key: 'skills',
|
||||||
@ -178,41 +128,7 @@ const AIEmployeeForm: React.FC = () => {
|
|||||||
{
|
{
|
||||||
key: 'modelSettings',
|
key: 'modelSettings',
|
||||||
label: 'Model Settings',
|
label: 'Model Settings',
|
||||||
children: (
|
children: <ModelSettings />,
|
||||||
<SchemaComponent
|
|
||||||
components={{ ModelSettings }}
|
|
||||||
schema={{
|
|
||||||
type: 'object',
|
|
||||||
name: 'modelSettings',
|
|
||||||
properties: {
|
|
||||||
llmService: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'LLM service',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'RemoteSelect',
|
|
||||||
'x-component-props': {
|
|
||||||
manual: false,
|
|
||||||
fieldNames: {
|
|
||||||
label: 'title',
|
|
||||||
value: 'name',
|
|
||||||
},
|
|
||||||
service: {
|
|
||||||
resource: 'llmServices',
|
|
||||||
action: 'list',
|
|
||||||
params: {
|
|
||||||
fields: ['title', 'name'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'ModelSettings',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
@ -0,0 +1,242 @@
|
|||||||
|
/**
|
||||||
|
* 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, { useState } from 'react';
|
||||||
|
import { Card, Button, Empty, Modal, Collapse, Form, Input, Switch, Select, List, Tag } from 'antd';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { useT } from '../../locale';
|
||||||
|
import { useField } from '@formily/react';
|
||||||
|
import { ArrayField } from '@formily/core';
|
||||||
|
import { FormItem, FormLayout } from '@formily/antd-v5';
|
||||||
|
import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
const inputSourceLabels = {
|
||||||
|
manual: 'Manual input',
|
||||||
|
blocks: 'Blocks',
|
||||||
|
fields: 'Field values',
|
||||||
|
collections: 'Data sources & collections',
|
||||||
|
};
|
||||||
|
|
||||||
|
const InputsEditModal: React.FC<{
|
||||||
|
title: string;
|
||||||
|
open: boolean;
|
||||||
|
onOk: (value: any) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}> = ({ open, title, onOk, onCancel }) => {
|
||||||
|
const t = useT();
|
||||||
|
const [inputField, setInputField] = useState({
|
||||||
|
title: '',
|
||||||
|
sources: {
|
||||||
|
manual: {
|
||||||
|
enabled: true,
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
blocks: {
|
||||||
|
enabled: false,
|
||||||
|
value: [],
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
enabled: false,
|
||||||
|
value: [],
|
||||||
|
},
|
||||||
|
collections: {
|
||||||
|
enabled: false,
|
||||||
|
value: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTitleChange = (value: string) => {
|
||||||
|
setInputField((prev) => ({
|
||||||
|
...prev,
|
||||||
|
title: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSwitchChange = (key: string, value: boolean) => {
|
||||||
|
setInputField((prev) => ({
|
||||||
|
...prev,
|
||||||
|
sources: {
|
||||||
|
...prev.sources,
|
||||||
|
[key]: {
|
||||||
|
...prev.sources[key],
|
||||||
|
enabled: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValueChange = (key: string, value: any) => {
|
||||||
|
setInputField((prev) => ({
|
||||||
|
...prev,
|
||||||
|
sources: {
|
||||||
|
...prev.sources,
|
||||||
|
[key]: {
|
||||||
|
...prev.sources[key],
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} title={title} onOk={() => onOk(inputField)} onCancel={onCancel}>
|
||||||
|
<FormLayout layout="vertical">
|
||||||
|
<FormItem label={t('Title')}>
|
||||||
|
<Input value={inputField.title} onChange={(e) => handleTitleChange(e.target.value)} />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label={t('Input sources')}>
|
||||||
|
<Collapse
|
||||||
|
size="small"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'manual',
|
||||||
|
label: t('Manual input'),
|
||||||
|
children: (
|
||||||
|
<Form.Item>
|
||||||
|
<Input placeholder={t('Placeholder')} onChange={(v) => handleValueChange('manual', v)} />
|
||||||
|
</Form.Item>
|
||||||
|
),
|
||||||
|
extra: <Switch size="small" defaultChecked onChange={(v) => handleSwitchChange('manual', v)} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'blocks',
|
||||||
|
label: t('Blocks'),
|
||||||
|
children: (
|
||||||
|
<Form.Item>
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
mode="multiple"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
key: 'table',
|
||||||
|
value: 'table',
|
||||||
|
label: t('Table'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'form',
|
||||||
|
value: 'form',
|
||||||
|
label: t('Form'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(v) => handleValueChange('blocks', v)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
),
|
||||||
|
extra: <Switch size="small" onChange={(v) => handleSwitchChange('blocks', v)} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'fields',
|
||||||
|
label: t('Field values'),
|
||||||
|
children: (
|
||||||
|
<Form.Item>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
allowClear
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
key: 'input',
|
||||||
|
value: 'input',
|
||||||
|
label: t('Single line text'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(v) => handleValueChange('fields', v)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
),
|
||||||
|
extra: <Switch size="small" onChange={(v) => handleSwitchChange('fields', v)} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'collections',
|
||||||
|
label: t('Data sources & collections'),
|
||||||
|
children: (
|
||||||
|
<Form.Item>
|
||||||
|
<Select onChange={(v) => handleValueChange('collections', v)} />
|
||||||
|
</Form.Item>
|
||||||
|
),
|
||||||
|
extra: <Switch size="small" onChange={(v) => handleSwitchChange('collections', v)} />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</FormLayout>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InputsFormSettings: React.FC = () => {
|
||||||
|
const t = useT();
|
||||||
|
const field = useField<ArrayField>();
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const handleAdd = (value: any) => {
|
||||||
|
const enabledSources = {};
|
||||||
|
for (const key in value.sources) {
|
||||||
|
if (value.sources[key].enabled) {
|
||||||
|
enabledSources[key] = value.sources[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value.sources = enabledSources;
|
||||||
|
field.value = [...(field.value || []), value];
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
extra={
|
||||||
|
<>
|
||||||
|
<Button size="small" variant="dashed" color="primary" icon={<PlusOutlined />} onClick={() => setOpen(true)}>
|
||||||
|
{t('Add field')}
|
||||||
|
</Button>
|
||||||
|
<InputsEditModal title={t('Add field')} open={open} onOk={handleAdd} onCancel={() => setOpen(false)} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{field.value && field.value.length ? (
|
||||||
|
<List
|
||||||
|
size="small"
|
||||||
|
dataSource={field.value}
|
||||||
|
renderItem={(item) => {
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
actions={[
|
||||||
|
<Button key="edit" icon={<EditOutlined />} type="text" />,
|
||||||
|
<Button key="delete" icon={<DeleteOutlined />} type="text" />,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<List.Item.Meta title={item.title} />
|
||||||
|
<>
|
||||||
|
{Object.keys(item.sources || {}).map((source) => {
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
style={{
|
||||||
|
marginBottom: '3px',
|
||||||
|
}}
|
||||||
|
key={source}
|
||||||
|
>
|
||||||
|
{t(inputSourceLabels[source])}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</List.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
@ -8,17 +8,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { observer, useForm } from '@formily/react';
|
import { observer, useForm } from '@formily/react';
|
||||||
import { useAPIClient, usePlugin, useRequest } from '@nocobase/client';
|
import { SchemaComponent, useAPIClient, usePlugin, useRequest } from '@nocobase/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PluginAIClient from '../../';
|
import PluginAIClient from '../../';
|
||||||
|
|
||||||
export const useModelSettingsForm = (provider: string) => {
|
const useModelOptionsForm = (provider: string) => {
|
||||||
const plugin = usePlugin(PluginAIClient);
|
const plugin = usePlugin(PluginAIClient);
|
||||||
const p = plugin.aiManager.llmProviders.get(provider);
|
const p = plugin.aiManager.llmProviders.get(provider);
|
||||||
return p?.components?.ModelSettingsForm;
|
return p?.components?.ModelSettingsForm;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModelSettings = observer(
|
const ModelOptions = observer(
|
||||||
() => {
|
() => {
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
@ -33,11 +33,49 @@ export const ModelSettings = observer(
|
|||||||
refreshDeps: [form.values?.modelSettings?.llmService],
|
refreshDeps: [form.values?.modelSettings?.llmService],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const Component = useModelSettingsForm(data?.provider);
|
const Component = useModelOptionsForm(data?.provider);
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return Component ? <Component /> : null;
|
return Component ? <Component /> : null;
|
||||||
},
|
},
|
||||||
{ displayName: 'AIEmployeeModelSettingsForm' },
|
{ displayName: 'AIEmployeeModelOptionsForm' },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ModelSettings: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
components={{ ModelOptions }}
|
||||||
|
schema={{
|
||||||
|
type: 'object',
|
||||||
|
name: 'modelSettings',
|
||||||
|
properties: {
|
||||||
|
llmService: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'LLM service',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'RemoteSelect',
|
||||||
|
'x-component-props': {
|
||||||
|
manual: false,
|
||||||
|
fieldNames: {
|
||||||
|
label: 'title',
|
||||||
|
value: 'name',
|
||||||
|
},
|
||||||
|
service: {
|
||||||
|
resource: 'llmServices',
|
||||||
|
action: 'list',
|
||||||
|
params: {
|
||||||
|
fields: ['title', 'name'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ModelOptions',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* 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 { SchemaComponent } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { AvatarSelect } from './AvatarSelect';
|
||||||
|
|
||||||
|
export const ProfileSettings: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
components={{ AvatarSelect }}
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
username: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Username',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
nickname: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Nickname',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Avatar',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'AvatarSelect',
|
||||||
|
},
|
||||||
|
bio: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Bio',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input.TextArea',
|
||||||
|
'x-component-props': {
|
||||||
|
placeholder:
|
||||||
|
'The introduction to the AI employee will inform human colleagues about its skills and how to use it. This information will be displayed on the employee’s profile. This will not be part of the prompt of this AI employee.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
about: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'About me',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input.TextArea',
|
||||||
|
'x-component-props': {
|
||||||
|
placeholder:
|
||||||
|
'Define the AI employee’s role, guide its work, and instruct it complete user-assigned tasks. This will be part of the prompt of this AI employee.',
|
||||||
|
autoSize: {
|
||||||
|
minRows: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
greeting: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Greeting message',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input.TextArea',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* 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, { createContext, useContext, useState } from 'react';
|
||||||
|
import { Selector } from '../types';
|
||||||
|
|
||||||
|
export const AISelectionContext = createContext<{
|
||||||
|
selectable: string;
|
||||||
|
selector?: Selector;
|
||||||
|
startSelect: (selectType: string, selector?: Selector) => void;
|
||||||
|
stopSelect: () => void;
|
||||||
|
}>({
|
||||||
|
selectable: false,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
export const useAISelectionContext = () => {
|
||||||
|
return useContext(AISelectionContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AISelectionProvider: React.FC = (props) => {
|
||||||
|
const [selectable, setSelectable] = useState('');
|
||||||
|
const [selector, setSelector] = useState<Selector>(null);
|
||||||
|
|
||||||
|
const startSelect = (selectType: string, selector?: Selector) => {
|
||||||
|
if (selector) {
|
||||||
|
setSelector(selector);
|
||||||
|
}
|
||||||
|
setSelectable(selectType);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopSelect = () => {
|
||||||
|
setSelectable('');
|
||||||
|
setSelector(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AISelectionContext.Provider value={{ selectable, selector, startSelect, stopSelect }}>
|
||||||
|
{props.children}
|
||||||
|
</AISelectionContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Button } from 'antd';
|
||||||
|
import { useT } from '../../locale';
|
||||||
|
import { BuildOutlined } from '@ant-design/icons';
|
||||||
|
import { useAISelectionContext } from './AISelectorProvider';
|
||||||
|
import { useField } from '@formily/react';
|
||||||
|
import { Field } from '@formily/core';
|
||||||
|
import { CloseCircleOutlined, PauseOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
export const BlockSelector: React.FC<{
|
||||||
|
onSelect?: (ctx: { uid: string }) => void;
|
||||||
|
}> = ({ onSelect }) => {
|
||||||
|
const t = useT();
|
||||||
|
const { startSelect, stopSelect } = useAISelectionContext();
|
||||||
|
const [selecting, setSelecting] = React.useState(false);
|
||||||
|
const field = useField<Field>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="dashed"
|
||||||
|
color="primary"
|
||||||
|
icon={<BuildOutlined />}
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
if (selecting) {
|
||||||
|
setSelecting(false);
|
||||||
|
stopSelect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelecting(true);
|
||||||
|
startSelect('blocks', {
|
||||||
|
onSelect: (ctx) => {
|
||||||
|
onSelect?.(ctx);
|
||||||
|
field.value = ctx.uid;
|
||||||
|
setSelecting(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selecting ? (
|
||||||
|
<>
|
||||||
|
{t('Selecting...')} <PauseOutlined />
|
||||||
|
</>
|
||||||
|
) : field.value ? (
|
||||||
|
<>
|
||||||
|
{field.value}{' '}
|
||||||
|
<CloseCircleOutlined
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
field.value = null;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
t('Select block')
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* 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 { createStyles } from '@nocobase/client';
|
||||||
|
import React, { ComponentType, forwardRef } from 'react';
|
||||||
|
import { useAISelectionContext } from './AISelectorProvider';
|
||||||
|
import { useFieldSchema, useField } from '@formily/react';
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ token, css }) => {
|
||||||
|
return {
|
||||||
|
aiSelectable: css`
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
&:hover {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 210, 255, 0.2);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export function withAISelectable<T = any>(
|
||||||
|
WrappedComponent: React.ComponentType,
|
||||||
|
options: {
|
||||||
|
selectType: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const { selectType } = options;
|
||||||
|
const SelectableComponent: ComponentType<T> = (props) => {
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const { selectable, selector, stopSelect } = useAISelectionContext();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const field = useField();
|
||||||
|
|
||||||
|
const handleSelect = () => {
|
||||||
|
selector?.onSelect?.({
|
||||||
|
uid: fieldSchema['x-uid'],
|
||||||
|
value: field?.value,
|
||||||
|
});
|
||||||
|
stopSelect();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={selectable === selectType ? styles.aiSelectable : ''} onClick={handleSelect}>
|
||||||
|
<WrappedComponent {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return SelectableComponent;
|
||||||
|
}
|
@ -7,10 +7,11 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { memo, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
|
SchemaComponent,
|
||||||
SchemaSettings,
|
SchemaSettings,
|
||||||
SchemaSettingsModalItem,
|
SchemaSettingsItem,
|
||||||
useBlockContext,
|
useBlockContext,
|
||||||
useCollection,
|
useCollection,
|
||||||
useCollectionFilterOptions,
|
useCollectionFilterOptions,
|
||||||
@ -19,10 +20,14 @@ import {
|
|||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { useT } from '../../locale';
|
import { useT } from '../../locale';
|
||||||
import { avatars } from '../avatars';
|
import { avatars } from '../avatars';
|
||||||
import { Card, Avatar, Tooltip } from 'antd';
|
import { Card, Avatar, Tooltip, Modal } from 'antd';
|
||||||
const { Meta } = Card;
|
const { Meta } = Card;
|
||||||
import { Schema } from '@formily/react';
|
import { Schema } from '@formily/react';
|
||||||
import { useAIEmployeeChatContext } from '../AIEmployeeChatProvider';
|
import { createForm } from '@formily/core';
|
||||||
|
import { InfoForm } from '../chatbox/InfoForm';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
import { useAISelectionContext } from '../selector/AISelectorProvider';
|
||||||
|
import { AIEmployee } from '../types';
|
||||||
|
|
||||||
export const useAIEmployeeButtonVariableOptions = () => {
|
export const useAIEmployeeButtonVariableOptions = () => {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
@ -49,6 +54,138 @@ export const useAIEmployeeButtonVariableOptions = () => {
|
|||||||
}, [recordData, t, fields, blockType]);
|
}, [recordData, t, fields, blockType]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SettingsForm: React.FC<{
|
||||||
|
form: any;
|
||||||
|
aiEmployee: AIEmployee;
|
||||||
|
}> = memo(({ form, aiEmployee }) => {
|
||||||
|
const { dn } = useSchemaSettings();
|
||||||
|
const t = useT();
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
components={{ InfoForm }}
|
||||||
|
scope={{ useAIEmployeeButtonVariableOptions }}
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
[uid()]: {
|
||||||
|
'x-component': 'FormV2',
|
||||||
|
'x-component-props': {
|
||||||
|
form,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
profile: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': () => (
|
||||||
|
<Card
|
||||||
|
variant="borderless"
|
||||||
|
style={{
|
||||||
|
maxWidth: 520,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Meta
|
||||||
|
avatar={aiEmployee.avatar ? <Avatar src={avatars(aiEmployee.avatar)} size={48} /> : null}
|
||||||
|
title={aiEmployee.nickname}
|
||||||
|
description={aiEmployee.bio}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
taskDesc: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('Task description'),
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input.TextArea',
|
||||||
|
description: t(
|
||||||
|
'Displays the AI employee’s assigned tasks on the profile when hovering over the button.',
|
||||||
|
),
|
||||||
|
default: dn.getSchemaAttribute('x-component-props.taskDesc'),
|
||||||
|
},
|
||||||
|
messageDivider: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Divider',
|
||||||
|
'x-component-props': {
|
||||||
|
children: t('Default message'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
messageType: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('Message type'),
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Select',
|
||||||
|
enum: [
|
||||||
|
{
|
||||||
|
label: t('Text'),
|
||||||
|
value: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('Image'),
|
||||||
|
value: 'image',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'text',
|
||||||
|
'x-component-props': {
|
||||||
|
placeholder: t('Message type'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
title: t('Message content'),
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Variable.RawTextArea',
|
||||||
|
'x-component-props': {
|
||||||
|
scope: '{{ useAIEmployeeButtonVariableOptions }}',
|
||||||
|
changeOnSelect: true,
|
||||||
|
fieldNames: {
|
||||||
|
value: 'name',
|
||||||
|
label: 'title',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: dn.getSchemaAttribute('x-component-props.message'),
|
||||||
|
'x-reactions': {
|
||||||
|
dependencies: ['.manualMessage'],
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
visible: '{{ !$deps[0] }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
autoSend: {
|
||||||
|
type: 'boolean',
|
||||||
|
'x-content': t('Send default message automatically'),
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Checkbox',
|
||||||
|
default: dn.getSchemaAttribute('x-component-props.autoSend') || false,
|
||||||
|
},
|
||||||
|
formDivider: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Divider',
|
||||||
|
'x-component-props': {
|
||||||
|
children: t('Required information form'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
infoForm: {
|
||||||
|
type: 'object',
|
||||||
|
'x-component': 'InfoForm',
|
||||||
|
'x-component-props': {
|
||||||
|
aiEmployee,
|
||||||
|
},
|
||||||
|
default: dn.getSchemaAttribute('x-component-props.infoForm'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const aiEmployeeButtonSettings = new SchemaSettings({
|
export const aiEmployeeButtonSettings = new SchemaSettings({
|
||||||
name: 'aiEmployees:button',
|
name: 'aiEmployees:button',
|
||||||
items: [
|
items: [
|
||||||
@ -57,171 +194,46 @@ export const aiEmployeeButtonSettings = new SchemaSettings({
|
|||||||
Component: () => {
|
Component: () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const { dn } = useSchemaSettings();
|
const { dn } = useSchemaSettings();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const aiEmployee = dn.getSchemaAttribute('x-component-props.aiEmployee') || {};
|
const aiEmployee = dn.getSchemaAttribute('x-component-props.aiEmployee') || {};
|
||||||
const { attachments = [], actions = [] } = useAIEmployeeChatContext();
|
const form = useMemo(() => createForm({}), []);
|
||||||
const attachmentsOptions = useMemo(
|
const { selectable } = useAISelectionContext();
|
||||||
() =>
|
|
||||||
Object.entries(attachments).map(([name, item]) => ({
|
|
||||||
label: (
|
|
||||||
<Tooltip title={item.description} placement="right">
|
|
||||||
{item.title}
|
|
||||||
</Tooltip>
|
|
||||||
),
|
|
||||||
value: name,
|
|
||||||
})),
|
|
||||||
[attachments],
|
|
||||||
);
|
|
||||||
const actionsOptions = useMemo(
|
|
||||||
() =>
|
|
||||||
Object.entries(actions).map(([name, item]) => ({
|
|
||||||
label: (
|
|
||||||
<Tooltip title={item.description} placement="right">
|
|
||||||
{item.title}
|
|
||||||
</Tooltip>
|
|
||||||
),
|
|
||||||
value: name,
|
|
||||||
})),
|
|
||||||
[actions],
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<SchemaSettingsModalItem
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
scope={{ useAIEmployeeButtonVariableOptions }}
|
<SchemaSettingsItem title={t('Edit')} onClick={() => setOpen(true)} />
|
||||||
schema={{
|
<Modal
|
||||||
type: 'object',
|
styles={{
|
||||||
title: t('Edit'),
|
mask: {
|
||||||
properties: {
|
zIndex: selectable ? -1 : 1000,
|
||||||
profile: {
|
|
||||||
type: 'void',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': () => (
|
|
||||||
<Card
|
|
||||||
variant="borderless"
|
|
||||||
style={{
|
|
||||||
maxWidth: 520,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Meta
|
|
||||||
avatar={aiEmployee.avatar ? <Avatar src={avatars(aiEmployee.avatar)} size={48} /> : null}
|
|
||||||
title={aiEmployee.nickname}
|
|
||||||
description={aiEmployee.bio}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
taskDesc: {
|
wrapper: {
|
||||||
type: 'string',
|
zIndex: selectable ? -1 : 1000,
|
||||||
title: t('Task description'),
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'Input.TextArea',
|
|
||||||
description: t(
|
|
||||||
'Displays the AI employee’s assigned tasks on the profile when hovering over the button.',
|
|
||||||
),
|
|
||||||
default: dn.getSchemaAttribute('x-component-props.taskDesc'),
|
|
||||||
},
|
},
|
||||||
manualMessage: {
|
}}
|
||||||
type: 'boolean',
|
title={t('Edit')}
|
||||||
'x-content': t('Requires the user to enter a message manually.'),
|
open={open}
|
||||||
'x-decorator': 'FormItem',
|
onCancel={() => {
|
||||||
'x-component': 'Checkbox',
|
setOpen(false);
|
||||||
default: dn.getSchemaAttribute('x-component-props.manualMessage') || false,
|
}}
|
||||||
},
|
onOk={() => {
|
||||||
message: {
|
const { taskDesc, message, autoSend, infoForm } = form.values;
|
||||||
type: 'object',
|
dn.deepMerge({
|
||||||
properties: {
|
'x-uid': dn.getSchemaAttribute('x-uid'),
|
||||||
messageType: {
|
'x-component-props': {
|
||||||
type: 'string',
|
aiEmployee,
|
||||||
title: t('Message type'),
|
message,
|
||||||
'x-decorator': 'FormItem',
|
taskDesc,
|
||||||
'x-component': 'Select',
|
autoSend,
|
||||||
enum: [
|
infoForm,
|
||||||
{
|
|
||||||
label: t('Text'),
|
|
||||||
value: 'text',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('Image'),
|
|
||||||
value: 'image',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'text',
|
|
||||||
'x-component-props': {
|
|
||||||
placeholder: t('Message type'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
title: t('Message content'),
|
|
||||||
type: 'string',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'Variable.RawTextArea',
|
|
||||||
'x-component-props': {
|
|
||||||
scope: '{{ useAIEmployeeButtonVariableOptions }}',
|
|
||||||
changeOnSelect: true,
|
|
||||||
fieldNames: {
|
|
||||||
value: 'name',
|
|
||||||
label: 'title',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
default: dn.getSchemaAttribute('x-component-props.message'),
|
});
|
||||||
'x-reactions': {
|
setOpen(false);
|
||||||
dependencies: ['.manualMessage'],
|
}}
|
||||||
fulfill: {
|
>
|
||||||
state: {
|
<SettingsForm form={form} aiEmployee={aiEmployee} />
|
||||||
visible: '{{ !$deps[0] }}',
|
</Modal>
|
||||||
},
|
</div>
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
attachments: {
|
|
||||||
type: 'array',
|
|
||||||
title: t('Attachments'),
|
|
||||||
'x-component': 'Checkbox.Group',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
enum: attachmentsOptions,
|
|
||||||
default: dn.getSchemaAttribute('x-component-props.attachments'),
|
|
||||||
'x-reactions': {
|
|
||||||
target: 'attachments',
|
|
||||||
fulfill: {
|
|
||||||
state: {
|
|
||||||
visible: '{{$self.value.length}}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
type: 'array',
|
|
||||||
title: t('Actions'),
|
|
||||||
'x-component': 'Checkbox.Group',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
enum: actionsOptions,
|
|
||||||
default: dn.getSchemaAttribute('x-component-props.actions'),
|
|
||||||
'x-reactions': {
|
|
||||||
target: 'actions',
|
|
||||||
fulfill: {
|
|
||||||
state: {
|
|
||||||
visible: '{{$self.value.length}}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
title={t('Edit')}
|
|
||||||
onSubmit={({ message, taskDesc, manualMessage, attachments, actions }) => {
|
|
||||||
dn.deepMerge({
|
|
||||||
'x-uid': dn.getSchemaAttribute('x-uid'),
|
|
||||||
'x-component-props': {
|
|
||||||
aiEmployee,
|
|
||||||
message,
|
|
||||||
taskDesc,
|
|
||||||
manualMessage,
|
|
||||||
attachments,
|
|
||||||
actions,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -9,12 +9,24 @@
|
|||||||
|
|
||||||
import type { BubbleProps } from '@ant-design/x';
|
import type { BubbleProps } from '@ant-design/x';
|
||||||
|
|
||||||
|
export type Selector = {
|
||||||
|
onSelect?: (ctx: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export type AIEmployee = {
|
export type AIEmployee = {
|
||||||
username: string;
|
username: string;
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
greeting?: string;
|
greeting?: string;
|
||||||
|
chatSettings?: {
|
||||||
|
senderPlaceholder?: string;
|
||||||
|
infoForm?: {
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Conversation = {
|
export type Conversation = {
|
||||||
@ -41,10 +53,17 @@ export type Action = {
|
|||||||
|
|
||||||
export type SendOptions = {
|
export type SendOptions = {
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
greeting?: boolean;
|
|
||||||
aiEmployee?: AIEmployee;
|
aiEmployee?: AIEmployee;
|
||||||
messages: {
|
messages: {
|
||||||
type: MessageType;
|
type: MessageType;
|
||||||
content: string;
|
content: string;
|
||||||
}[];
|
}[];
|
||||||
|
infoFormValues?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShortcutOptions = {
|
||||||
|
aiEmployee: AIEmployee;
|
||||||
|
message: { type: MessageType; content: string };
|
||||||
|
infoFormValues: any;
|
||||||
|
autoSend: boolean;
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Plugin, lazy } from '@nocobase/client';
|
import { CardItem, CollectionField, FormV2, Plugin, lazy } from '@nocobase/client';
|
||||||
import { AIManager } from './manager/ai-manager';
|
import { AIManager } from './manager/ai-manager';
|
||||||
import { openaiProviderOptions } from './llm-providers/openai';
|
import { openaiProviderOptions } from './llm-providers/openai';
|
||||||
import { deepseekProviderOptions } from './llm-providers/deepseek';
|
import { deepseekProviderOptions } from './llm-providers/deepseek';
|
||||||
@ -19,6 +19,7 @@ import { namespace } from './locale';
|
|||||||
import { detailsAIEmployeesInitializer, formAIEmployeesInitializer } from './ai-employees/initializer/AIEmployees';
|
import { detailsAIEmployeesInitializer, formAIEmployeesInitializer } from './ai-employees/initializer/AIEmployees';
|
||||||
import { aiEmployeeButtonSettings } from './ai-employees/settings/AIEmployeeButton';
|
import { aiEmployeeButtonSettings } from './ai-employees/settings/AIEmployeeButton';
|
||||||
import { useDetailsAIEmployeeChatContext, useFormAIEmployeeChatContext } from './ai-employees/useBlockChatContext';
|
import { useDetailsAIEmployeeChatContext, useFormAIEmployeeChatContext } from './ai-employees/useBlockChatContext';
|
||||||
|
import { withAISelectable } from './ai-employees/selector/withAISelectable';
|
||||||
const { AIEmployeesProvider } = lazy(() => import('./ai-employees/AIEmployeesProvider'), 'AIEmployeesProvider');
|
const { AIEmployeesProvider } = lazy(() => import('./ai-employees/AIEmployeesProvider'), 'AIEmployeesProvider');
|
||||||
const { AIEmployeeChatProvider } = lazy(
|
const { AIEmployeeChatProvider } = lazy(
|
||||||
() => import('./ai-employees/AIEmployeeChatProvider'),
|
() => import('./ai-employees/AIEmployeeChatProvider'),
|
||||||
@ -45,6 +46,12 @@ export class PluginAIClient extends Plugin {
|
|||||||
this.app.addComponents({
|
this.app.addComponents({
|
||||||
AIEmployeeButton,
|
AIEmployeeButton,
|
||||||
AIEmployeeChatProvider,
|
AIEmployeeChatProvider,
|
||||||
|
CardItem: withAISelectable(CardItem, {
|
||||||
|
selectType: 'blocks',
|
||||||
|
}),
|
||||||
|
CollectionField: withAISelectable(CollectionField, {
|
||||||
|
selectType: 'fields',
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
this.app.addScopes({
|
this.app.addScopes({
|
||||||
useDetailsAIEmployeeChatContext,
|
useDetailsAIEmployeeChatContext,
|
||||||
|
@ -40,6 +40,10 @@ export default {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
interface: 'textarea',
|
interface: 'textarea',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'chatSettings',
|
||||||
|
type: 'jsonb',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'skills',
|
name: 'skills',
|
||||||
type: 'jsonb',
|
type: 'jsonb',
|
||||||
|
@ -21,20 +21,11 @@ export default defineCollection({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'content',
|
name: 'content',
|
||||||
type: 'text',
|
type: 'jsonb',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'role',
|
name: 'role',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'type',
|
|
||||||
type: 'string',
|
|
||||||
defaultValue: 'text',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'title',
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -10,8 +10,40 @@
|
|||||||
import actions, { Context, Next } from '@nocobase/actions';
|
import actions, { Context, Next } from '@nocobase/actions';
|
||||||
import snowflake from '../snowflake';
|
import snowflake from '../snowflake';
|
||||||
import PluginAIServer from '../plugin';
|
import PluginAIServer from '../plugin';
|
||||||
import { Model } from '@nocobase/database';
|
|
||||||
import { convertUiSchemaToJsonSchema } from '../utils';
|
import { convertUiSchemaToJsonSchema } from '../utils';
|
||||||
|
import { Database, Model } from '@nocobase/database';
|
||||||
|
|
||||||
|
async function parseInfoMessage(db: Database, aiEmployee: Model, content: Record<string, any>) {
|
||||||
|
const infoForm: {
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
type: 'blocks' | 'collections';
|
||||||
|
}[] = aiEmployee.chatSettings?.infoForm;
|
||||||
|
if (!infoForm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!content) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let info = '';
|
||||||
|
for (const key in content) {
|
||||||
|
const field = infoForm.find((item) => item.name === key);
|
||||||
|
if (field.type === 'blocks') {
|
||||||
|
const uiSchemaRepo = db.getRepository('uiSchemas') as any;
|
||||||
|
const schema = await uiSchemaRepo.getJsonSchema(content[key]);
|
||||||
|
if (!schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info += `${field.title}: ${JSON.stringify(schema)}; `;
|
||||||
|
} else {
|
||||||
|
info += `${field.title}: ${content[key]}; `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!info) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return `The following information you can utilize in your conversation: ${info}`;
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'aiConversations',
|
name: 'aiConversations',
|
||||||
@ -71,19 +103,10 @@ export default {
|
|||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
ctx.throw(400);
|
ctx.throw(400);
|
||||||
}
|
}
|
||||||
const rows = await ctx.db.getRepository('aiConversations.messages', sessionId).find({
|
ctx.body = await ctx.db.getRepository('aiConversations.messages', sessionId).find({
|
||||||
sort: ['-messageId'],
|
sort: ['-messageId'],
|
||||||
limit: 10,
|
limit: 10,
|
||||||
});
|
});
|
||||||
ctx.body = rows.map((row: Model) => ({
|
|
||||||
messageId: row.messageId,
|
|
||||||
role: row.role,
|
|
||||||
content: {
|
|
||||||
title: row.title,
|
|
||||||
content: row.content,
|
|
||||||
type: row.type,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
await next();
|
await next();
|
||||||
},
|
},
|
||||||
async sendMessages(ctx: Context, next: Next) {
|
async sendMessages(ctx: Context, next: Next) {
|
||||||
@ -154,9 +177,7 @@ export default {
|
|||||||
values: messages.map((message: any) => ({
|
values: messages.map((message: any) => ({
|
||||||
messageId: snowflake.generate(),
|
messageId: snowflake.generate(),
|
||||||
role: message.role,
|
role: message.role,
|
||||||
content: message.content.content,
|
content: message.content,
|
||||||
type: message.content.type,
|
|
||||||
title: message.content.title,
|
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -168,14 +189,12 @@ export default {
|
|||||||
ctx.status = 200;
|
ctx.status = 200;
|
||||||
const userMessages = [];
|
const userMessages = [];
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
if (msg.role !== 'user') {
|
if (msg.role !== 'user' && msg.role !== 'info') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let content = msg.content.content;
|
let content = msg.content.content;
|
||||||
if (msg.content.type === 'uiSchema') {
|
if (msg.content.type === 'info') {
|
||||||
const uiSchemaRepo = ctx.db.getRepository('uiSchemas');
|
content = await parseInfoMessage(ctx.db, employee, content);
|
||||||
const schema = await uiSchemaRepo.getJsonSchema(content);
|
|
||||||
content = JSON.stringify(convertUiSchemaToJsonSchema(schema));
|
|
||||||
}
|
}
|
||||||
userMessages.push({
|
userMessages.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
@ -215,12 +234,19 @@ export default {
|
|||||||
message += chunk.content;
|
message += chunk.content;
|
||||||
ctx.res.write(`data: ${JSON.stringify({ type: 'content', body: chunk.content })}\n\n`);
|
ctx.res.write(`data: ${JSON.stringify({ type: 'content', body: chunk.content })}\n\n`);
|
||||||
}
|
}
|
||||||
|
if (!message) {
|
||||||
|
ctx.res.write(`data: ${JSON.stringify({ type: 'error', body: 'No content' })}\n\n`);
|
||||||
|
ctx.res.end();
|
||||||
|
return next();
|
||||||
|
}
|
||||||
await ctx.db.getRepository('aiConversations.messages', sessionId).create({
|
await ctx.db.getRepository('aiConversations.messages', sessionId).create({
|
||||||
values: {
|
values: {
|
||||||
messageId: snowflake.generate(),
|
messageId: snowflake.generate(),
|
||||||
role: aiEmployee,
|
role: aiEmployee,
|
||||||
content: message,
|
content: {
|
||||||
type: 'text',
|
content: message,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
ctx.res.end();
|
ctx.res.end();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user