### 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
| 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); |
| 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 }; | |||||
| }; | }; |
| 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', |
| 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 }, |
| 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(); |
| ); | ); | ||||
| 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 |
| 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> | ||||
| ); | ); |
| 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> | |||||
| ); | |||||
| } |
| 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, | |||||
| }; | |||||
| }; |
| 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> | ||||
| ); | ); | ||||
| } | } |
| ], | ], | ||||
| }, | }, | ||||
| { | { | ||||
| path: Routes.Chat, | |||||
| path: Routes.Chat + '/:id', | |||||
| layout: false, | layout: false, | ||||
| component: `@/pages${Routes.Chats}/chat`, | component: `@/pages${Routes.Chats}/chat`, | ||||
| }, | }, |