### 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
| @@ -35,9 +35,12 @@ export const useNavigatePage = () => { | |||
| navigate(Routes.Chats); | |||
| }, [navigate]); | |||
| const navigateToChat = useCallback(() => { | |||
| navigate(Routes.Chat); | |||
| }, [navigate]); | |||
| const navigateToChat = useCallback( | |||
| (id: string) => () => { | |||
| navigate(`${Routes.Chat}/${id}`); | |||
| }, | |||
| [navigate], | |||
| ); | |||
| const navigateToAgents = useCallback(() => { | |||
| navigate(Routes.Agents); | |||
| @@ -1,9 +1,22 @@ | |||
| import message from '@/components/ui/message'; | |||
| import { ChatSearchParams } from '@/constants/chat'; | |||
| import { IDialog } from '@/interfaces/database/chat'; | |||
| 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 { useTranslation } from 'react-i18next'; | |||
| import { history, useSearchParams } from 'umi'; | |||
| import { | |||
| useGetPaginationWithRouter, | |||
| useHandleSearchChange, | |||
| } from './logic-hooks'; | |||
| export const enum ChatApiAction { | |||
| FetchDialogList = 'fetchDialogList', | |||
| RemoveDialog = 'removeDialog', | |||
| SetDialog = 'setDialog', | |||
| } | |||
| export const useGetChatSearchParams = () => { | |||
| const [currentQueryParameters] = useSearchParams(); | |||
| @@ -42,13 +55,22 @@ export const useClickDialogCard = () => { | |||
| export const useFetchDialogList = (pureFetch = false) => { | |||
| const { handleClickDialog } = useClickDialogCard(); | |||
| const { dialogId } = useGetChatSearchParams(); | |||
| const { searchString, handleInputChange } = useHandleSearchChange(); | |||
| const { pagination, setPagination } = useGetPaginationWithRouter(); | |||
| const debouncedSearchString = useDebounce(searchString, { wait: 500 }); | |||
| const { | |||
| data, | |||
| isFetching: loading, | |||
| refetch, | |||
| } = useQuery<IDialog[]>({ | |||
| queryKey: ['fetchDialogList'], | |||
| queryKey: [ | |||
| ChatApiAction.FetchDialogList, | |||
| { | |||
| debouncedSearchString, | |||
| ...pagination, | |||
| }, | |||
| ], | |||
| initialData: [], | |||
| gcTime: 0, | |||
| refetchOnWindowFocus: false, | |||
| @@ -73,5 +95,76 @@ export const useFetchDialogList = (pureFetch = 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 }; | |||
| }; | |||
| @@ -562,6 +562,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s | |||
| tavilyApiKeyHelp: 'How to get it?', | |||
| 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.`, | |||
| createChat: 'Create chat', | |||
| }, | |||
| setting: { | |||
| profile: 'Profile', | |||
| @@ -2,6 +2,7 @@ import Editor, { loader } from '@monaco-editor/react'; | |||
| import { INextOperatorForm } from '../../interface'; | |||
| import { FormContainer } from '@/components/form-container'; | |||
| import { useIsDarkTheme } from '@/components/theme-provider'; | |||
| import { | |||
| Form, | |||
| FormControl, | |||
| @@ -46,6 +47,7 @@ function CodeForm({ node }: INextOperatorForm) { | |||
| const formData = node?.data.form as ICodeForm; | |||
| const { t } = useTranslation(); | |||
| const values = useValues(node); | |||
| const isDarkTheme = useIsDarkTheme(); | |||
| const form = useForm<FormSchemaType>({ | |||
| defaultValues: values, | |||
| @@ -94,7 +96,7 @@ function CodeForm({ node }: INextOperatorForm) { | |||
| <FormControl> | |||
| <Editor | |||
| height={300} | |||
| theme="vs-dark" | |||
| theme={isDarkTheme ? 'vs-dark' : 'vs'} | |||
| language={formData.lang} | |||
| options={{ | |||
| minimap: { enabled: false }, | |||
| @@ -10,7 +10,7 @@ import { useCallback } from 'react'; | |||
| import { AgentCard } from './agent-card'; | |||
| import { useRenameAgent } from './use-rename-agent'; | |||
| export default function Agent() { | |||
| export default function Agents() { | |||
| const { data, pagination, setPagination, searchString, handleInputChange } = | |||
| useFetchAgentListByPage(); | |||
| const { navigateToAgentTemplates } = useNavigatePage(); | |||
| @@ -53,7 +53,7 @@ export default function Datasets() { | |||
| ); | |||
| return ( | |||
| <section className="py-4 text-foreground"> | |||
| <section className="py-4 flex-1 flex flex-col"> | |||
| <ListFilterBar | |||
| title={t('header.knowledgeBase')} | |||
| searchString={searchString} | |||
| @@ -69,16 +69,18 @@ export default function Datasets() { | |||
| {t('knowledgeList.createKnowledgeBase')} | |||
| </Button> | |||
| </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 className="mt-8 px-8"> | |||
| <RAGFlowPagination | |||
| @@ -1,52 +1,45 @@ | |||
| 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 { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | |||
| 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; | |||
| } | |||
| } & Pick<ReturnType<typeof useRenameChat>, 'showChatRenameModal'>; | |||
| export function ChatCard({ data }: IProps) { | |||
| export function ChatCard({ data, showChatRenameModal }: IProps) { | |||
| const { navigateToChat } = useNavigatePage(); | |||
| 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> | |||
| <ChatDropdown chat={data} showChatRenameModal={showChatRenameModal}> | |||
| <MoreButton></MoreButton> | |||
| </ChatDropdown> | |||
| </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> | |||
| </Card> | |||
| ); | |||
| @@ -0,0 +1,64 @@ | |||
| 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> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,45 @@ | |||
| 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, | |||
| }; | |||
| }; | |||
| @@ -1,25 +1,72 @@ | |||
| import ListFilterBar from '@/components/list-filter-bar'; | |||
| import { RenameDialog } from '@/components/rename-dialog'; | |||
| 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 { useCallback } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { ChatCard } from './chat-card'; | |||
| import { useRenameChat } from './hooks/use-rename-chat'; | |||
| 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 ( | |||
| <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> | |||
| {chatRenameVisible && ( | |||
| <RenameDialog | |||
| hideModal={hideChatRenameModal} | |||
| onOk={onChatRenameOk} | |||
| initialName={initialChatName} | |||
| loading={chatRenameLoading} | |||
| ></RenameDialog> | |||
| )} | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -214,7 +214,7 @@ const routes = [ | |||
| ], | |||
| }, | |||
| { | |||
| path: Routes.Chat, | |||
| path: Routes.Chat + '/:id', | |||
| layout: false, | |||
| component: `@/pages${Routes.Chats}/chat`, | |||
| }, | |||