Переглянути джерело

Feat: Render dialog list #3221 (#9249)

### What problem does this PR solve?

Feat: Render dialog list #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
tags/v0.20.1
balibabu 3 місяці тому
джерело
коміт
a264c629b5
Аккаунт користувача з таким Email не знайдено

+ 6
- 3
web/src/hooks/logic-hooks/navigate-hooks.ts Переглянути файл

navigate(Routes.Chats); navigate(Routes.Chats);
}, [navigate]); }, [navigate]);


const navigateToChat = useCallback(() => {
navigate(Routes.Chat);
}, [navigate]);
const navigateToChat = useCallback(
(id: string) => () => {
navigate(`${Routes.Chat}/${id}`);
},
[navigate],
);


const navigateToAgents = useCallback(() => { const navigateToAgents = useCallback(() => {
navigate(Routes.Agents); navigate(Routes.Agents);

+ 96
- 3
web/src/hooks/use-chat-request.ts Переглянути файл

import message from '@/components/ui/message';
import { ChatSearchParams } from '@/constants/chat'; import { ChatSearchParams } from '@/constants/chat';
import { IDialog } from '@/interfaces/database/chat'; import { IDialog } from '@/interfaces/database/chat';
import chatService from '@/services/chat-service'; import chatService from '@/services/chat-service';
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { history, useSearchParams } from 'umi'; import { history, useSearchParams } from 'umi';
import {
useGetPaginationWithRouter,
useHandleSearchChange,
} from './logic-hooks';

export const enum ChatApiAction {
FetchDialogList = 'fetchDialogList',
RemoveDialog = 'removeDialog',
SetDialog = 'setDialog',
}


export const useGetChatSearchParams = () => { export const useGetChatSearchParams = () => {
const [currentQueryParameters] = useSearchParams(); const [currentQueryParameters] = useSearchParams();
export const useFetchDialogList = (pureFetch = false) => { export const useFetchDialogList = (pureFetch = false) => {
const { handleClickDialog } = useClickDialogCard(); const { handleClickDialog } = useClickDialogCard();
const { dialogId } = useGetChatSearchParams(); const { dialogId } = useGetChatSearchParams();
const { searchString, handleInputChange } = useHandleSearchChange();
const { pagination, setPagination } = useGetPaginationWithRouter();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });


const { const {
data, data,
isFetching: loading, isFetching: loading,
refetch, refetch,
} = useQuery<IDialog[]>({ } = useQuery<IDialog[]>({
queryKey: ['fetchDialogList'],
queryKey: [
ChatApiAction.FetchDialogList,
{
debouncedSearchString,
...pagination,
},
],
initialData: [], initialData: [],
gcTime: 0, gcTime: 0,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}, },
}); });


return { data, loading, refetch };
const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
handleInputChange(e);
},
[handleInputChange],
);

return {
data,
loading,
refetch,
searchString,
handleInputChange: onInputChange,
pagination: { ...pagination, total: data?.total },
setPagination,
};
};

export const useRemoveDialog = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();

const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [ChatApiAction.RemoveDialog],
mutationFn: async (dialogIds: string[]) => {
const { data } = await chatService.removeDialog({ dialogIds });
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['fetchDialogList'] });

message.success(t('message.deleted'));
}
return data.code;
},
});

return { data, loading, removeDialog: mutateAsync };
};

export const useSetDialog = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();

const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [ChatApiAction.SetDialog],
mutationFn: async (params: IDialog) => {
const { data } = await chatService.setDialog(params);
if (data.code === 0) {
queryClient.invalidateQueries({
exact: false,
queryKey: ['fetchDialogList'],
});

queryClient.invalidateQueries({
queryKey: ['fetchDialog'],
});
message.success(
t(`message.${params.dialog_id ? 'modified' : 'created'}`),
);
}
return data?.code;
},
});

return { data, loading, setDialog: mutateAsync };
}; };

+ 1
- 0
web/src/locales/en.ts Переглянути файл

tavilyApiKeyHelp: 'How to get it?', tavilyApiKeyHelp: 'How to get it?',
crossLanguage: 'Cross-language search', crossLanguage: 'Cross-language search',
crossLanguageTip: `Select one or more languages for cross‑language search. If no language is selected, the system searches with the original query.`, crossLanguageTip: `Select one or more languages for cross‑language search. If no language is selected, the system searches with the original query.`,
createChat: 'Create chat',
}, },
setting: { setting: {
profile: 'Profile', profile: 'Profile',

+ 3
- 1
web/src/pages/agent/form/code-form/index.tsx Переглянути файл

import { INextOperatorForm } from '../../interface'; import { INextOperatorForm } from '../../interface';


import { FormContainer } from '@/components/form-container'; import { FormContainer } from '@/components/form-container';
import { useIsDarkTheme } from '@/components/theme-provider';
import { import {
Form, Form,
FormControl, FormControl,
const formData = node?.data.form as ICodeForm; const formData = node?.data.form as ICodeForm;
const { t } = useTranslation(); const { t } = useTranslation();
const values = useValues(node); const values = useValues(node);
const isDarkTheme = useIsDarkTheme();


const form = useForm<FormSchemaType>({ const form = useForm<FormSchemaType>({
defaultValues: values, defaultValues: values,
<FormControl> <FormControl>
<Editor <Editor
height={300} height={300}
theme="vs-dark"
theme={isDarkTheme ? 'vs-dark' : 'vs'}
language={formData.lang} language={formData.lang}
options={{ options={{
minimap: { enabled: false }, minimap: { enabled: false },

+ 1
- 1
web/src/pages/agents/index.tsx Переглянути файл

import { AgentCard } from './agent-card'; import { AgentCard } from './agent-card';
import { useRenameAgent } from './use-rename-agent'; import { useRenameAgent } from './use-rename-agent';


export default function Agent() {
export default function Agents() {
const { data, pagination, setPagination, searchString, handleInputChange } = const { data, pagination, setPagination, searchString, handleInputChange } =
useFetchAgentListByPage(); useFetchAgentListByPage();
const { navigateToAgentTemplates } = useNavigatePage(); const { navigateToAgentTemplates } = useNavigatePage();

+ 13
- 11
web/src/pages/datasets/index.tsx Переглянути файл

); );


return ( return (
<section className="py-4 text-foreground">
<section className="py-4 flex-1 flex flex-col">
<ListFilterBar <ListFilterBar
title={t('header.knowledgeBase')} title={t('header.knowledgeBase')}
searchString={searchString} searchString={searchString}
{t('knowledgeList.createKnowledgeBase')} {t('knowledgeList.createKnowledgeBase')}
</Button> </Button>
</ListFilterBar> </ListFilterBar>
<div className="flex flex-wrap gap-4 max-h-[78vh] overflow-auto px-8">
{kbs.map((dataset) => {
return (
<DatasetCard
dataset={dataset}
key={dataset.id}
showDatasetRenameModal={showDatasetRenameModal}
></DatasetCard>
);
})}
<div className="flex-1">
<div className="flex flex-wrap gap-4 max-h-[78vh] overflow-auto px-8">
{kbs.map((dataset) => {
return (
<DatasetCard
dataset={dataset}
key={dataset.id}
showDatasetRenameModal={showDatasetRenameModal}
></DatasetCard>
);
})}
</div>
</div> </div>
<div className="mt-8 px-8"> <div className="mt-8 px-8">
<RAGFlowPagination <RAGFlowPagination

+ 31
- 38
web/src/pages/next-chats/chat-card.tsx Переглянути файл

import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { MoreButton } from '@/components/more-button';
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { IDialog } from '@/interfaces/database/chat'; import { IDialog } from '@/interfaces/database/chat';
import { formatPureDate } from '@/utils/date';
import { ChevronRight, Trash2 } from 'lucide-react';
import { formatDate } from '@/utils/date';
import { ChatDropdown } from './chat-dropdown';
import { useRenameChat } from './hooks/use-rename-chat';


interface IProps {
export type IProps = {
data: IDialog; data: IDialog;
}
} & Pick<ReturnType<typeof useRenameChat>, 'showChatRenameModal'>;


export function ChatCard({ data }: IProps) {
export function ChatCard({ data, showChatRenameModal }: IProps) {
const { navigateToChat } = useNavigatePage(); const { navigateToChat } = useNavigatePage();


return ( return (
<Card className="bg-colors-background-inverse-weak border-colors-outline-neutral-standard">
<CardContent className="p-4">
<div className="flex justify-between mb-4">
{data.icon ? (
<div
className="w-[70px] h-[70px] rounded-xl bg-cover"
style={{ backgroundImage: `url(${data.icon})` }}
/>
) : (
<Avatar className="w-[70px] h-[70px]">
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
)}
</div>
<h3 className="text-xl font-bold mb-2">{data.name}</h3>
<p>An app that does things An app that does things</p>
<section className="flex justify-between pt-3">
<div>
Search app
<p className="text-sm opacity-80">
{formatPureDate(data.update_time)}
</p>
</div>
<div className="space-x-2">
<Button variant="icon" size="icon" onClick={navigateToChat}>
<ChevronRight className="h-6 w-6" />
</Button>
<Button variant="icon" size="icon">
<Trash2 />
</Button>
<Card key={data.id} className="w-40" onClick={navigateToChat(data.id)}>
<CardContent className="p-2.5 pt-2 group">
<section className="flex justify-between mb-2">
<div className="flex gap-2 items-center">
<RAGFlowAvatar
className="size-6 rounded-lg"
avatar={data.icon}
name={data.name || 'CN'}
></RAGFlowAvatar>
</div> </div>
<ChatDropdown chat={data} showChatRenameModal={showChatRenameModal}>
<MoreButton></MoreButton>
</ChatDropdown>
</section> </section>
<div className="flex justify-between items-end">
<div className="w-full">
<h3 className="text-lg font-semibold mb-2 line-clamp-1">
{data.name}
</h3>
<p className="text-xs text-text-sub-title">{data.description}</p>
<p className="text-xs text-text-sub-title">
{formatDate(data.update_time)}
</p>
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
); );

+ 64
- 0
web/src/pages/next-chats/chat-dropdown.tsx Переглянути файл

import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useRemoveDialog } from '@/hooks/use-chat-request';
import { IDialog } from '@/interfaces/database/chat';
import { PenLine, Trash2 } from 'lucide-react';
import { MouseEventHandler, PropsWithChildren, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useRenameChat } from './hooks/use-rename-chat';

export function ChatDropdown({
children,
showChatRenameModal,
chat,
}: PropsWithChildren &
Pick<ReturnType<typeof useRenameChat>, 'showChatRenameModal'> & {
chat: IDialog;
}) {
const { t } = useTranslation();
const { removeDialog } = useRemoveDialog();

const handleShowChatRenameModal: MouseEventHandler<HTMLDivElement> =
useCallback(
(e) => {
e.stopPropagation();
showChatRenameModal(chat);
},
[chat, showChatRenameModal],
);

const handleDelete: MouseEventHandler<HTMLDivElement> = useCallback(() => {
removeDialog([chat.id]);
}, [chat.id, removeDialog]);

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={handleShowChatRenameModal}>
{t('common.rename')} <PenLine />
</DropdownMenuItem>
<DropdownMenuSeparator />
<ConfirmDeleteDialog onOk={handleDelete}>
<DropdownMenuItem
className="text-text-delete-red"
onSelect={(e) => {
e.preventDefault();
}}
onClick={(e) => {
e.stopPropagation();
}}
>
{t('common.delete')} <Trash2 />
</DropdownMenuItem>
</ConfirmDeleteDialog>
</DropdownMenuContent>
</DropdownMenu>
);
}

+ 45
- 0
web/src/pages/next-chats/hooks/use-rename-chat.ts Переглянути файл

import { useSetModalState } from '@/hooks/common-hooks';
import { useSetDialog } from '@/hooks/use-chat-request';
import { IDialog } from '@/interfaces/database/chat';
import { useCallback, useState } from 'react';

export const useRenameChat = () => {
const [chat, setChat] = useState<IDialog>({} as IDialog);
const {
visible: chatRenameVisible,
hideModal: hideChatRenameModal,
showModal: showChatRenameModal,
} = useSetModalState();
const { setDialog, loading } = useSetDialog();

const onChatRenameOk = useCallback(
async (name: string) => {
const ret = await setDialog({
...chat,
name,
});

if (ret === 0) {
hideChatRenameModal();
}
},
[setDialog, chat, hideChatRenameModal],
);

const handleShowChatRenameModal = useCallback(
async (record: IDialog) => {
setChat(record);
showChatRenameModal();
},
[showChatRenameModal],
);

return {
chatRenameLoading: loading,
initialChatName: chat?.name,
onChatRenameOk,
chatRenameVisible,
hideChatRenameModal,
showChatRenameModal: handleShowChatRenameModal,
};
};

+ 60
- 13
web/src/pages/next-chats/index.tsx Переглянути файл

import ListFilterBar from '@/components/list-filter-bar'; import ListFilterBar from '@/components/list-filter-bar';
import { RenameDialog } from '@/components/rename-dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useFetchChatAppList } from '@/hooks/chat-hooks';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { useFetchDialogList } from '@/hooks/use-chat-request';
import { pick } from 'lodash';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { ChatCard } from './chat-card'; import { ChatCard } from './chat-card';
import { useRenameChat } from './hooks/use-rename-chat';


export default function ChatList() { export default function ChatList() {
const { data: chatList } = useFetchChatAppList();
const { data: chatList, setPagination, pagination } = useFetchDialogList();
const { t } = useTranslation();
const {
initialChatName,
chatRenameVisible,
showChatRenameModal,
hideChatRenameModal,
onChatRenameOk,
chatRenameLoading,
} = useRenameChat();

const handlePageChange = useCallback(
(page: number, pageSize?: number) => {
setPagination({ page, pageSize });
},
[setPagination],
);


return ( return (
<section className="p-8">
<ListFilterBar title="Chat apps">
<Button variant={'tertiary'} size={'sm'}>
<Plus className="mr-2 h-4 w-4" />
Create app
</Button>
</ListFilterBar>
<div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8">
{chatList.map((x) => {
return <ChatCard key={x.id} data={x}></ChatCard>;
})}
<section className="flex flex-col w-full flex-1">
<div className="px-8 pt-8">
<ListFilterBar title="Chat apps">
<Button>
<Plus className="size-2.5" />
{t('chat.createChat')}
</Button>
</ListFilterBar>
</div>
<div className="flex-1 overflow-auto">
<div className="flex flex-wrap gap-4 px-8">
{chatList.map((x) => {
return (
<ChatCard
key={x.id}
data={x}
showChatRenameModal={showChatRenameModal}
></ChatCard>
);
})}
</div>
</div>
<div className="mt-8 px-8 pb-8">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={pagination.total}
onChange={handlePageChange}
></RAGFlowPagination>
</div> </div>
{chatRenameVisible && (
<RenameDialog
hideModal={hideChatRenameModal}
onOk={onChatRenameOk}
initialName={initialChatName}
loading={chatRenameLoading}
></RenameDialog>
)}
</section> </section>
); );
} }

+ 1
- 1
web/src/routes.ts Переглянути файл

], ],
}, },
{ {
path: Routes.Chat,
path: Routes.Chat + '/:id',
layout: false, layout: false,
component: `@/pages${Routes.Chats}/chat`, component: `@/pages${Routes.Chats}/chat`,
}, },

Завантаження…
Відмінити
Зберегти