chore: update

This commit is contained in:
xilesun 2025-04-11 16:05:01 +08:00
parent 421b2827d6
commit cb44488fca
5 changed files with 120 additions and 61 deletions

View File

@ -143,7 +143,7 @@ export const useSetChatBoxContext = () => {
...options,
onConversationCreate: (sessionId: string) => {
setCurrentConversation(sessionId);
conversationsService.refresh();
conversationsService.run();
},
};
const hasInfoFormValues = Object.values(infoForm?.values || []).filter(Boolean).length;

View File

@ -8,13 +8,17 @@
*/
import { useAPIClient, useRequest } from '@nocobase/client';
import React, { createContext, useContext, useState } from 'react';
import React, { createContext, useCallback, useContext, useRef, useState } from 'react';
import { Conversation } from '../types';
import { useLoadMoreObserver } from './useLoadMoreObserver';
import { ConversationsProps } from '@ant-design/x';
type ChatConversationContextValue = {
currentConversation?: string;
setCurrentConversation: (sessionId?: string) => void;
conversationsService: any;
lastConversationRef: (node: HTMLDivElement | null) => void;
conversations: ConversationsProps['items'];
};
export const ChatConversationsContext = createContext<ChatConversationContextValue | null>(null);
@ -24,25 +28,57 @@ export const useChatConversations = () => useContext(ChatConversationsContext);
export const ChatConversationsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const api = useAPIClient();
const [currentConversation, setCurrentConversation] = useState<string>();
const [conversations, setConversations] = useState<ConversationsProps['items']>([]);
const conversationsService = useRequest<Conversation[]>(
() =>
(page = 1) =>
api
.resource('aiConversations')
.list({
sort: ['-updatedAt'],
appends: ['aiEmployee'],
page,
pageSize: 15,
})
.then((res) => res?.data?.data),
.then((res) => res?.data),
{
manual: true,
onSuccess: (data, params) => {
const page = params[0];
if (!data?.data?.length) {
return;
}
const conversations: ConversationsProps['items'] = data.data.map((conversation) => ({
key: conversation.sessionId,
label: conversation.title,
timestamp: new Date(conversation.updatedAt).getTime(),
}));
if (!page || page === 1) {
setConversations(conversations);
} else {
setConversations((prev) => [...prev, ...conversations]);
}
},
},
);
const conversationsServiceRef = useRef<any>();
conversationsServiceRef.current = conversationsService;
const loadMoreConversations = useCallback(async () => {
const conversationsService = conversationsServiceRef.current;
const meta = conversationsService.data?.meta;
if (conversationsService.loading || (meta && meta.page >= meta.totalPage)) {
return;
}
await conversationsService.runAsync(meta.page + 1);
}, []);
const { ref: lastConversationRef } = useLoadMoreObserver({ loadMore: loadMoreConversations });
const value = {
currentConversation,
setCurrentConversation,
conversationsService,
lastConversationRef,
conversations,
};
return <ChatConversationsContext.Provider value={value}>{children}</ChatConversationsContext.Provider>;

View File

@ -28,7 +28,7 @@ interface ChatMessagesContextValue {
},
) => Promise<void>;
messagesService: any;
lastMessageRef: React.RefObject<any>;
lastMessageRef: (node: HTMLElement | null) => void;
}
export const ChatMessagesContext = createContext<ChatMessagesContextValue | null>(null);
@ -189,18 +189,18 @@ export const ChatMessagesProvider: React.FC<{ children: React.ReactNode }> = ({
hasMore?: boolean;
};
}>(
(cursor?: string) =>
(sessionId, cursor?: string) =>
api
.resource('aiConversations')
.getMessages({
sessionId: currentConversation,
sessionId,
cursor,
})
.then((res) => res?.data),
{
manual: true,
onSuccess: (data, params) => {
const cursor = params[0];
const cursor = params[1];
if (!data?.data?.length) {
return;
}
@ -219,15 +219,9 @@ export const ChatMessagesProvider: React.FC<{ children: React.ReactNode }> = ({
if (messagesService.loading || !messagesService.data?.meta?.hasMore) {
return;
}
await messagesService.runAsync(messagesService.data?.meta?.cursor);
}, []);
const { ref: lastMessageRef } = useLoadMoreObserver({ loadMore: loadMoreMessages });
useEffect(() => {
if (!currentConversation) {
return;
}
messagesServiceRef.current.run();
await messagesService.runAsync(currentConversation, messagesService.data?.meta?.cursor);
}, [currentConversation]);
const { ref: lastMessageRef } = useLoadMoreObserver({ loadMore: loadMoreMessages });
return (
<ChatMessagesContext.Provider

View File

@ -7,15 +7,15 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React from 'react';
import React, { useMemo } 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';
import { useChatConversations } from './ChatConversationsProvider';
import { useChatMessages } from './ChatMessagesProvider';
const { Header, Content } = Layout;
export const Conversations: React.FC = () => {
@ -23,17 +23,35 @@ export const Conversations: React.FC = () => {
const api = useAPIClient();
const { modal, message } = App.useApp();
const { token } = useToken();
const { currentConversation, setCurrentConversation, conversationsService } = useChatConversations();
const { currentConversation, setCurrentConversation, conversationsService, conversations, lastConversationRef } =
useChatConversations();
const { messagesService } = useChatMessages();
const startNewConversation = useChatBoxContext('startNewConversation');
const setCurrentEmployee = useChatBoxContext('setCurrentEmployee');
const setSenderValue = useChatBoxContext('setSenderValue');
const setSenderPlaceholder = useChatBoxContext('setSenderPlaceholder');
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 { loading: conversationsLoading, data: conversationsRes } = conversationsService;
const items = useMemo(() => {
const result = conversations.map((item, index) => ({
...item,
label: index === conversations.length - 1 ? <div ref={lastConversationRef}>{item.label}</div> : item.label,
}));
if (conversationsLoading) {
result.push({
key: 'loading',
label: (
<Spin
style={{
display: 'block',
margin: '8px auto',
}}
/>
),
});
}
return result;
}, [conversations, conversationsLoading, lastConversationRef]);
const deleteConversation = async (sessionId: string) => {
await api.resource('aiConversations').destroy({
@ -49,14 +67,19 @@ export const Conversations: React.FC = () => {
return;
}
setCurrentConversation(sessionId);
const conversation = conversationsRes.find((item) => item.sessionId === sessionId);
const conversation = conversationsRes?.data?.find((item) => item.sessionId === sessionId);
setCurrentEmployee(conversation?.aiEmployee);
setSenderValue('');
setSenderPlaceholder(conversation?.aiEmployee?.chatSettings?.senderPlaceholder);
messagesService.run(sessionId);
};
return (
<Layout>
<Layout
style={{
height: '100%',
}}
>
<Header
style={{
backgroundColor: token.colorBgContainer,
@ -67,38 +90,41 @@ export const Conversations: React.FC = () => {
>
<Input.Search style={{ verticalAlign: 'middle' }} />
</Header>
<Content>
<Spin spinning={ConversationsLoading}>
{conversations && conversations.length ? (
<AntConversations
activeKey={currentConversation}
onActiveChange={selectConversation}
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;
}
<Content
style={{
height: '100%',
overflow: 'auto',
}}
>
{conversations && conversations.length ? (
<AntConversations
activeKey={currentConversation}
onActiveChange={selectConversation}
items={items}
menu={(conversation) => ({
items: [
{
label: 'Delete',
key: 'delete',
icon: <DeleteOutlined />,
},
})}
/>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</Spin>
],
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} />
)}
</Content>
</Layout>
);

View File

@ -47,12 +47,15 @@ export const Messages: React.FC = () => {
<div>
{messages.map((msg, index) => {
const role = roles[msg.role];
if (!role) {
return null;
}
return index === 0 ? (
<div ref={lastMessageRef}>
<Bubble {...role} key={msg.key} content={msg.content} />
<div key={msg.key} ref={lastMessageRef}>
<Bubble {...role} loading={msg.loading} content={msg.content} />
</div>
) : (
<Bubble {...role} key={msg.key} content={msg.content} />
<Bubble {...role} key={msg.key} loading={msg.loading} content={msg.content} />
);
})}
</div>