chore: use-context-selector

This commit is contained in:
xilesun 2025-04-10 14:07:42 +08:00
parent c3b03776ae
commit f60cd60c33
18 changed files with 234 additions and 225 deletions

View File

@ -50,17 +50,6 @@ export function createDetailsWithPaginationUISchema(options: {
'x-read-pretty': true, 'x-read-pretty': true,
'x-use-component-props': 'useDetailsWithPaginationProps', 'x-use-component-props': 'useDetailsWithPaginationProps',
properties: { properties: {
[uid()]: {
type: 'void',
'x-initializer': hideActionInitializer ? undefined : 'aiEmployees:configure',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 16,
},
},
properties: {},
},
[uid()]: { [uid()]: {
type: 'void', type: 'void',
'x-initializer': hideActionInitializer ? undefined : 'details:configureActions', 'x-initializer': hideActionInitializer ? undefined : 'details:configureActions',

View File

@ -51,17 +51,6 @@ export function createDetailsUISchema(options: {
'x-read-pretty': true, 'x-read-pretty': true,
'x-use-component-props': 'useDetailsProps', 'x-use-component-props': 'useDetailsProps',
properties: { properties: {
[uid()]: {
type: 'void',
'x-initializer': 'aiEmployees:configure',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 16,
},
},
properties: {},
},
[uid()]: { [uid()]: {
type: 'void', type: 'void',
'x-initializer': 'details:configureActions', 'x-initializer': 'details:configureActions',

View File

@ -60,18 +60,6 @@ export function createCreateFormBlockUISchema(options: CreateFormBlockUISchemaOp
'x-initializer': 'form:configureFields', 'x-initializer': 'form:configureFields',
properties: {}, properties: {},
}, },
[uid()]: {
type: 'void',
'x-initializer': 'aiEmployees:configure',
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
style: {
marginBottom: 8,
},
},
properties: {},
},
[uid()]: { [uid()]: {
type: 'void', type: 'void',
'x-initializer': 'createForm:configureActions', 'x-initializer': 'createForm:configureActions',

View File

@ -17,6 +17,7 @@
"@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",
"snowflake-id": "^1.1.0" "snowflake-id": "^1.1.0",
"use-context-selector": "^2.0.0"
} }
} }

View File

@ -19,13 +19,11 @@ import { Sender } from '@ant-design/x';
import { ProfileCard } from '../ProfileCard'; import { ProfileCard } from '../ProfileCard';
export const AIEmployeeHeader: React.FC = () => { export const AIEmployeeHeader: React.FC = () => {
const t = useT();
const { token } = useToken();
const { const {
service: { loading }, service: { loading },
aiEmployees, aiEmployees,
} = useAIEmployeesContext(); } = useAIEmployeesContext();
const { switchAIEmployee } = useChatBoxContext(); const switchAIEmployee = useChatBoxContext('switchAIEmployee');
return ( return (
<Sender.Header closable={false}> <Sender.Header closable={false}>
<List <List

View File

@ -7,19 +7,21 @@
* 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, useState } from 'react'; import React, { useState } from 'react';
import { Layout, Card, Button } from 'antd'; import { Layout, Card, Button } from 'antd';
import { CloseOutlined, ExpandOutlined, EditOutlined, LayoutOutlined } from '@ant-design/icons'; import { CloseOutlined, ExpandOutlined, EditOutlined, LayoutOutlined } from '@ant-design/icons';
import { useToken } from '@nocobase/client'; import { useToken } from '@nocobase/client';
const { Header, Footer, Sider, Content } = Layout; const { Header, Footer, Sider, Content } = Layout;
import { ChatBoxContext } from './ChatBoxContext'; import { useChatBoxContext } from './ChatBoxContext';
import { Conversations } from './Conversations'; import { Conversations } from './Conversations';
import { Messages } from './Messages'; import { Messages } from './Messages';
import { Sender } from './Sender'; import { Sender } from './Sender';
import { useAISelectionContext } from '../selector/AISelectorProvider'; import { useAISelectionContext } from '../selector/AISelectorProvider';
export const ChatBox: React.FC = () => { export const ChatBox: React.FC = () => {
const { setOpen, startNewConversation, currentEmployee } = useContext(ChatBoxContext); const setOpen = useChatBoxContext('setOpen');
const startNewConversation = useChatBoxContext('startNewConversation');
const currentEmployee = useChatBoxContext('currentEmployee');
const { token } = useToken(); const { token } = useToken();
const [showConversations, setShowConversations] = useState(false); const [showConversations, setShowConversations] = useState(false);
const { selectable } = useAISelectionContext(); const { selectable } = useAISelectionContext();

View File

@ -19,7 +19,7 @@ import {
} from '../types'; } from '../types';
import { Avatar, GetProp, GetRef, Button, Alert, Space, Popover } from 'antd'; 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, useMemo } from 'react'; import React, { useContext, useEffect, useState, useRef, useMemo, useCallback } 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';
@ -31,8 +31,8 @@ import { uid } from '@formily/shared';
import { useT } from '../../locale'; import { useT } from '../../locale';
import { createForm, Form } from '@formily/core'; import { createForm, Form } from '@formily/core';
import { ProfileCard } from '../ProfileCard'; import { ProfileCard } from '../ProfileCard';
import _ from 'lodash';
import { InfoFormMessage } from './InfoForm'; import { InfoFormMessage } from './InfoForm';
import { createContext, useContextSelector } from 'use-context-selector';
export const ChatBoxContext = createContext<{ export const ChatBoxContext = createContext<{
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
@ -213,7 +213,8 @@ export const useSetChatBoxContext = () => {
conversations.refresh(); conversations.refresh();
}, },
}; };
if (!_.isEmpty(infoForm?.values)) { const hasInfoFormValues = Object.values(infoForm?.values || []).filter(Boolean).length;
if (hasInfoFormValues) {
sendOptions.infoFormValues = { ...infoForm.values }; sendOptions.infoFormValues = { ...infoForm.values };
} }
setSenderValue(''); setSenderValue('');
@ -230,7 +231,8 @@ export const useSetChatBoxContext = () => {
} }
}; };
const switchAIEmployee = (aiEmployee: AIEmployee) => { const switchAIEmployee = useCallback(
(aiEmployee: AIEmployee) => {
const greetingMsg = { const greetingMsg = {
key: uid(), key: uid(),
role: aiEmployee.username, role: aiEmployee.username,
@ -249,18 +251,21 @@ export const useSetChatBoxContext = () => {
addMessage(greetingMsg); addMessage(greetingMsg);
setSenderValue(''); setSenderValue('');
} }
}; },
[currentConversation, infoForm],
);
const startNewConversation = () => { const startNewConversation = useCallback(() => {
setCurrentConversation(undefined); setCurrentConversation(undefined);
setCurrentEmployee(null); setCurrentEmployee(null);
setSenderValue(''); setSenderValue('');
infoForm.reset(); infoForm.reset();
setMessages([]); setMessages([]);
senderRef.current?.focus(); senderRef.current?.focus();
}; }, [infoForm]);
const triggerShortcut = (options: ShortcutOptions) => { const triggerShortcut = useCallback(
(options: ShortcutOptions) => {
const { aiEmployee, message, infoFormValues, autoSend } = options; const { aiEmployee, message, infoFormValues, autoSend } = options;
updateRole(aiEmployee); updateRole(aiEmployee);
if (!open) { if (!open) {
@ -294,7 +299,9 @@ export const useSetChatBoxContext = () => {
messages: [message], messages: [message],
}); });
} }
}; },
[open, currentConversation, infoForm],
);
useEffect(() => { useEffect(() => {
if (!aiEmployees) { if (!aiEmployees) {
@ -343,6 +350,6 @@ export const useSetChatBoxContext = () => {
}; };
}; };
export const useChatBoxContext = () => { export const useChatBoxContext = (name: string) => {
return useContext(ChatBoxContext); return useContextSelector(ChatBoxContext, (v) => v[name]);
}; };

View File

@ -22,13 +22,11 @@ export const Conversations: React.FC = () => {
const api = useAPIClient(); const api = useAPIClient();
const { modal, message } = App.useApp(); const { modal, message } = App.useApp();
const { token } = useToken(); const { token } = useToken();
const { const conversationsService = useChatBoxContext('conversations');
conversations: conversationsService, const currentConversation = useChatBoxContext('currentConversation');
currentConversation, const setCurrentConversation = useChatBoxContext('setCurrentConversation');
setCurrentConversation, const setMessages = useChatBoxContext('setMessages');
setMessages, const startNewConversation = useChatBoxContext('startNewConversation');
startNewConversation,
} = useChatBoxContext();
const { loading: ConversationsLoading, data: conversationsRes } = conversationsService; const { loading: ConversationsLoading, data: conversationsRes } = conversationsService;
const conversations: ConversationsProps['items'] = (conversationsRes || []).map((conversation) => ({ const conversations: ConversationsProps['items'] = (conversationsRes || []).map((conversation) => ({
key: conversation.sessionId, key: conversation.sessionId,

View File

@ -14,7 +14,10 @@ import { useChatBoxContext } from './ChatBoxContext';
import { useAISelectionContext } from '../selector/AISelectorProvider'; import { useAISelectionContext } from '../selector/AISelectorProvider';
export const FieldSelector: React.FC = () => { export const FieldSelector: React.FC = () => {
const { currentEmployee, senderValue, setSenderValue, senderRef } = useChatBoxContext(); const currentEmployee = useChatBoxContext('currentEmployee');
const senderValue = useChatBoxContext('senderValue');
const setSenderValue = useChatBoxContext('setSenderValue');
const senderRef = useChatBoxContext('senderRef');
const { startSelect } = useAISelectionContext(); const { startSelect } = useAISelectionContext();
const handleSelect = () => { const handleSelect = () => {

View File

@ -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 React, { useMemo } from 'react'; import React, { memo, useMemo } from 'react';
import { AIEmployee } from '../types'; import { AIEmployee } from '../types';
import { SchemaComponent } from '@nocobase/client'; import { SchemaComponent } from '@nocobase/client';
import { BlockSelector } from '../selector/BlockSelector'; import { BlockSelector } from '../selector/BlockSelector';
@ -87,8 +87,8 @@ export const ReadPrettyInfoForm: React.FC<{
export const InfoFormMessage: React.FC<{ export const InfoFormMessage: React.FC<{
values: any; values: any;
}> = ({ values }) => { }> = memo(({ values }) => {
const { currentEmployee } = useChatBoxContext(); const currentEmployee = useChatBoxContext('currentEmployee');
const t = useT(); const t = useT();
const form = useMemo( const form = useMemo(
() => () =>
@ -122,4 +122,4 @@ export const InfoFormMessage: React.FC<{
/> />
</> </>
); );
}; });

View File

@ -13,7 +13,8 @@ import { useChatBoxContext } from './ChatBoxContext';
import { ReactComponent as EmptyIcon } from '../empty-icon.svg'; import { ReactComponent as EmptyIcon } from '../empty-icon.svg';
export const Messages: React.FC = () => { export const Messages: React.FC = () => {
const { messages, roles } = useChatBoxContext(); const messages = useChatBoxContext('messages');
const roles = useChatBoxContext('roles');
return ( return (
<> <>
{messages?.length ? ( {messages?.length ? (

View File

@ -19,17 +19,15 @@ import { SenderFooter } from './SenderFooter';
export const Sender: React.FC = memo(() => { export const Sender: React.FC = memo(() => {
const t = useT(); const t = useT();
const { const senderValue = useChatBoxContext('senderValue');
senderValue, const setSenderValue = useChatBoxContext('setSenderValue');
setSenderValue, const senderPlaceholder = useChatBoxContext('senderPlaceholder');
senderPlaceholder, const send = useChatBoxContext('send');
send, const currentConversation = useChatBoxContext('currentConversation');
currentConversation, const currentEmployee = useChatBoxContext('currentEmployee');
currentEmployee, const responseLoading = useChatBoxContext('responseLoading');
responseLoading, const showInfoForm = useChatBoxContext('showInfoForm');
showInfoForm, const senderRef = useChatBoxContext('senderRef');
senderRef,
} = useChatBoxContext();
return ( return (
<AntSender <AntSender
value={senderValue} value={senderValue}

View File

@ -18,7 +18,7 @@ export const SenderFooter: React.FC<{
}> = ({ components }) => { }> = ({ components }) => {
const t = useT(); const t = useT();
const { SendButton, LoadingButton } = components; const { SendButton, LoadingButton } = components;
const { responseLoading: loading } = useChatBoxContext(); const { responseLoading: loading } = useChatBoxContext('responseLoading');
return ( return (
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">

View File

@ -21,7 +21,10 @@ import { uid } from '@formily/shared';
export const SenderHeader: React.FC = () => { export const SenderHeader: React.FC = () => {
const t = useT(); const t = useT();
const { token } = useToken(); const { token } = useToken();
const { currentEmployee, startNewConversation, showInfoForm, infoForm: form } = useChatBoxContext(); const currentEmployee = useChatBoxContext('currentEmployee');
const startNewConversation = useChatBoxContext('startNewConversation');
const showInfoForm = useChatBoxContext('showInfoForm');
const form = useChatBoxContext('infoForm');
return currentEmployee ? ( return currentEmployee ? (
showInfoForm ? ( showInfoForm ? (
<Sender.Header <Sender.Header

View File

@ -8,12 +8,12 @@
*/ */
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { ChatBoxContext } from './ChatBoxContext'; import { useChatBoxContext } from './ChatBoxContext';
import { avatars } from '../avatars'; import { avatars } from '../avatars';
import { Avatar } from 'antd'; import { Avatar } from 'antd';
export const SenderPrefix: React.FC = () => { export const SenderPrefix: React.FC = () => {
const { currentEmployee } = useContext(ChatBoxContext); const currentEmployee = useChatBoxContext('currentEmployee');
return currentEmployee ? ( return currentEmployee ? (
<Avatar <Avatar
src={avatars(currentEmployee.avatar)} src={avatars(currentEmployee.avatar)}

View File

@ -67,7 +67,7 @@ export const AIEmployeeButton: React.FC<{
}; };
infoForm: any; infoForm: any;
}> = withDynamicSchemaProps(({ aiEmployee, taskDesc, message, infoForm: infoFormValues, autoSend }) => { }> = withDynamicSchemaProps(({ aiEmployee, taskDesc, message, infoForm: infoFormValues, autoSend }) => {
const { triggerShortcut } = useChatBoxContext(); const triggerShortcut = useChatBoxContext('triggerShortcut');
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { render } = useSchemaToolbarRender(fieldSchema); const { render } = useSchemaToolbarRender(fieldSchema);
const variables = useVariables(); const variables = useVariables();

View File

@ -204,7 +204,6 @@ it('should convert ui schema to json schema', async () => {
_isJSONSchemaObject: true, _isJSONSchemaObject: true,
version: '2.0', version: '2.0',
type: 'void', type: 'void',
'x-initializer': 'aiEmployees:configure',
'x-component': 'ActionBar', 'x-component': 'ActionBar',
'x-component-props': { 'x-component-props': {
layout: 'one-column', layout: 'one-column',

View File

@ -127,6 +127,8 @@ export default {
ctx.res.end(); ctx.res.end();
return next(); return next();
} }
let stream: any;
try {
const conversation = await ctx.db.getRepository('aiConversations').findOne({ const conversation = await ctx.db.getRepository('aiConversations').findOne({
filterByTk: sessionId, filterByTk: sessionId,
}); });
@ -172,6 +174,26 @@ export default {
return next(); return next();
} }
const historyMessages = await ctx.db.getRepository('aiConversations.messages', sessionId).find({
sort: ['messageId'],
});
const history = [];
for (const msg of historyMessages) {
let content = msg.content.content;
if (msg.content.type === 'info') {
content = await parseInfoMessage(ctx.db, employee, content);
}
let role = msg.role;
if (role === 'info' || role === 'user') {
role = 'user';
} else {
role = 'ai';
}
history.push({
role,
content,
});
}
try { try {
await ctx.db.getRepository('aiConversations.messages', sessionId).create({ await ctx.db.getRepository('aiConversations.messages', sessionId).create({
values: messages.map((message: any) => ({ values: messages.map((message: any) => ({
@ -196,6 +218,9 @@ export default {
if (msg.content.type === 'info') { if (msg.content.type === 'info') {
content = await parseInfoMessage(ctx.db, employee, content); content = await parseInfoMessage(ctx.db, employee, content);
} }
if (!content) {
continue;
}
userMessages.push({ userMessages.push({
role: 'user', role: 'user',
content, content,
@ -206,8 +231,10 @@ export default {
role: 'system', role: 'system',
content: employee.about, content: employee.about,
}, },
...history,
...userMessages, ...userMessages,
]; ];
console.log(msgs);
const Provider = providerOptions.provider; const Provider = providerOptions.provider;
const provider = new Provider({ const provider = new Provider({
app: ctx.app, app: ctx.app,
@ -217,7 +244,6 @@ export default {
messages: msgs, messages: msgs,
}, },
}); });
let stream: any;
try { try {
stream = await provider.stream(); stream = await provider.stream();
} catch (err) { } catch (err) {
@ -226,6 +252,12 @@ export default {
ctx.res.end(); ctx.res.end();
return next(); return next();
} }
} catch (err) {
ctx.log.error(err);
ctx.res.write(`data: ${JSON.stringify({ type: 'error', body: 'Chat error warning' })}\n\n`);
ctx.res.end();
return next();
}
let message = ''; let message = '';
for await (const chunk of stream) { for await (const chunk of stream) {
if (!chunk.content) { if (!chunk.content) {
@ -239,6 +271,7 @@ export default {
ctx.res.end(); ctx.res.end();
return next(); return next();
} }
console.log(message);
await ctx.db.getRepository('aiConversations.messages', sessionId).create({ await ctx.db.getRepository('aiConversations.messages', sessionId).create({
values: { values: {
messageId: snowflake.generate(), messageId: snowflake.generate(),