mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-31 09:05:30 +08:00
Feat: New search page components and features (#9344)
### What problem does this PR solve? Feat: New search page components and features #3221 - Added search homepage, search settings, and ongoing search components - Implemented features such as search app list, creating search apps, and deleting search apps - Optimized the multi-select component, adding disabled state and suffix display - Adjusted navigation hooks to support search page navigation ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
209
web/src/pages/next-searches/hooks.ts
Normal file
209
web/src/pages/next-searches/hooks.ts
Normal file
@ -0,0 +1,209 @@
|
||||
// src/pages/next-searches/hooks.ts
|
||||
|
||||
import searchService from '@/services/search-service';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { message } from 'antd';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'umi';
|
||||
|
||||
interface CreateSearchProps {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface CreateSearchResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const useCreateSearch = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
mutateAsync: createSearchMutation,
|
||||
} = useMutation<CreateSearchResponse, Error, CreateSearchProps>({
|
||||
mutationKey: ['createSearch'],
|
||||
mutationFn: async (props) => {
|
||||
const { data: response } = await searchService.createSearch(props);
|
||||
if (response.code !== 0) {
|
||||
throw new Error(response.message || 'Failed to create search');
|
||||
}
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
message.success(t('message.created'));
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(t('message.error', { error: error.message }));
|
||||
},
|
||||
});
|
||||
|
||||
const createSearch = useCallback(
|
||||
(props: CreateSearchProps) => {
|
||||
return createSearchMutation(props);
|
||||
},
|
||||
[createSearchMutation],
|
||||
);
|
||||
|
||||
return { data, isLoading, isError, createSearch };
|
||||
};
|
||||
|
||||
export interface SearchListParams {
|
||||
keywords?: string;
|
||||
parser_id?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
orderby?: string;
|
||||
desc?: boolean;
|
||||
owner_ids?: string;
|
||||
}
|
||||
export interface ISearchAppProps {
|
||||
avatar: any;
|
||||
create_time: number;
|
||||
created_by: string;
|
||||
description: string;
|
||||
id: string;
|
||||
name: string;
|
||||
nickname: string;
|
||||
status: string;
|
||||
tenant_avatar: any;
|
||||
tenant_id: string;
|
||||
update_time: number;
|
||||
}
|
||||
interface SearchListResponse {
|
||||
code: number;
|
||||
data: {
|
||||
search_apps: Array<ISearchAppProps>;
|
||||
total: number;
|
||||
};
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const useFetchSearchList = (params?: SearchListParams) => {
|
||||
const [searchParams, setSearchParams] = useState<SearchListParams>({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
...params,
|
||||
});
|
||||
|
||||
const { data, isLoading, isError } = useQuery<SearchListResponse, Error>({
|
||||
queryKey: ['searchList', searchParams],
|
||||
queryFn: async () => {
|
||||
const { data: response } =
|
||||
await searchService.getSearchList(searchParams);
|
||||
if (response.code !== 0) {
|
||||
throw new Error(response.message || 'Failed to fetch search list');
|
||||
}
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
const setSearchListParams = (newParams: SearchListParams) => {
|
||||
setSearchParams((prevParams) => ({
|
||||
...prevParams,
|
||||
...newParams,
|
||||
}));
|
||||
};
|
||||
|
||||
return { data, isLoading, isError, searchParams, setSearchListParams };
|
||||
};
|
||||
|
||||
interface DeleteSearchProps {
|
||||
search_id: string;
|
||||
}
|
||||
|
||||
interface DeleteSearchResponse {
|
||||
code: number;
|
||||
data: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const useDeleteSearch = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
mutateAsync: deleteSearchMutation,
|
||||
} = useMutation<DeleteSearchResponse, Error, DeleteSearchProps>({
|
||||
mutationKey: ['deleteSearch'],
|
||||
mutationFn: async (props) => {
|
||||
const response = await searchService.deleteSearch(props);
|
||||
if (response.code !== 0) {
|
||||
throw new Error(response.message || 'Failed to delete search');
|
||||
}
|
||||
return response;
|
||||
},
|
||||
onSuccess: () => {
|
||||
message.success(t('message.deleted'));
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(t('message.error', { error: error.message }));
|
||||
},
|
||||
});
|
||||
|
||||
const deleteSearch = useCallback(
|
||||
(props: DeleteSearchProps) => {
|
||||
return deleteSearchMutation(props);
|
||||
},
|
||||
[deleteSearchMutation],
|
||||
);
|
||||
|
||||
return { data, isLoading, isError, deleteSearch };
|
||||
};
|
||||
|
||||
export interface ISearchAppDetailProps {
|
||||
avatar: any;
|
||||
created_by: string;
|
||||
description: string;
|
||||
id: string;
|
||||
name: string;
|
||||
search_config: {
|
||||
cross_languages: string[];
|
||||
doc_ids: string[];
|
||||
highlight: boolean;
|
||||
kb_ids: string[];
|
||||
keyword: boolean;
|
||||
query_mindmap: boolean;
|
||||
related_search: boolean;
|
||||
rerank_id: string;
|
||||
similarity_threshold: number;
|
||||
summary: boolean;
|
||||
top_k: number;
|
||||
use_kg: boolean;
|
||||
vector_similarity_weight: number;
|
||||
web_search: boolean;
|
||||
};
|
||||
tenant_id: string;
|
||||
update_time: number;
|
||||
}
|
||||
|
||||
interface SearchDetailResponse {
|
||||
code: number;
|
||||
data: ISearchAppDetailProps;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const useFetchSearchDetail = () => {
|
||||
const { id } = useParams();
|
||||
const { data, isLoading, isError } = useQuery<SearchDetailResponse, Error>({
|
||||
queryKey: ['searchDetail', id],
|
||||
queryFn: async () => {
|
||||
const { data: response } = await searchService.getSearchDetail({
|
||||
search_id: id,
|
||||
});
|
||||
if (response.code !== 0) {
|
||||
throw new Error(response.message || 'Failed to fetch search detail');
|
||||
}
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, isLoading, isError };
|
||||
};
|
||||
@ -1,20 +1,65 @@
|
||||
import ListFilterBar from '@/components/list-filter-bar';
|
||||
import { Input } from '@/components/originui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useFetchFlowList } from '@/hooks/flow-hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { pick } from 'lodash';
|
||||
import { Plus, Search } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { useCreateSearch, useFetchSearchList } from './hooks';
|
||||
import { SearchCard } from './search-card';
|
||||
const searchFormSchema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: 'Name is required',
|
||||
}),
|
||||
});
|
||||
|
||||
type SearchFormValues = z.infer<typeof searchFormSchema>;
|
||||
export default function SearchList() {
|
||||
const { data } = useFetchFlowList();
|
||||
// const { data } = useFetchFlowList();
|
||||
const { t } = useTranslate('search');
|
||||
const [searchName, setSearchName] = useState('');
|
||||
const { isLoading, isError, createSearch } = useCreateSearch();
|
||||
const {
|
||||
data: list,
|
||||
searchParams,
|
||||
setSearchListParams,
|
||||
} = useFetchSearchList();
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const form = useForm<SearchFormValues>({
|
||||
resolver: zodResolver(searchFormSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
const handleSearchChange = (value: string) => {
|
||||
console.log(value);
|
||||
};
|
||||
|
||||
const onSubmit = async (values: SearchFormValues) => {
|
||||
await createSearch({ name: values.name });
|
||||
if (!isLoading) {
|
||||
setOpenCreateModal(false);
|
||||
}
|
||||
form.reset({ name: '' });
|
||||
};
|
||||
const openCreateModalFun = () => {
|
||||
setOpenCreateModal(true);
|
||||
};
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
setSearchListParams({ ...searchParams, page, page_size: pageSize });
|
||||
};
|
||||
return (
|
||||
<section>
|
||||
<div className="px-8 pt-8">
|
||||
@ -31,35 +76,7 @@ export default function SearchList() {
|
||||
<Button
|
||||
variant={'default'}
|
||||
onClick={() => {
|
||||
Modal.show({
|
||||
title: (
|
||||
<div className="rounded-sm bg-emerald-400 bg-gradient-to-t from-emerald-400 via-emerald-400 to-emerald-200 p-1 size-6 flex justify-center items-center">
|
||||
<Search size={14} className="font-bold m-auto" />
|
||||
</div>
|
||||
),
|
||||
titleClassName: 'border-none',
|
||||
footerClassName: 'border-none',
|
||||
visible: true,
|
||||
children: (
|
||||
<div>
|
||||
<div>{t('createSearch')}</div>
|
||||
<div>name:</div>
|
||||
<Input
|
||||
defaultValue={searchName}
|
||||
onChange={(e) => {
|
||||
console.log(e.target.value, e);
|
||||
setSearchName(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
onOk: () => {
|
||||
console.log('ok', searchName);
|
||||
},
|
||||
onVisibleChange: (e) => {
|
||||
Modal.hide();
|
||||
},
|
||||
});
|
||||
openCreateModalFun();
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
@ -68,10 +85,72 @@ export default function SearchList() {
|
||||
</ListFilterBar>
|
||||
</div>
|
||||
<div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 max-h-[84vh] overflow-auto px-8">
|
||||
{data.map((x) => {
|
||||
{list?.data.search_apps.map((x) => {
|
||||
return <SearchCard key={x.id} data={x}></SearchCard>;
|
||||
})}
|
||||
{/* {data.map((x) => {
|
||||
return <SearchCard key={x.id} data={x}></SearchCard>;
|
||||
})} */}
|
||||
</div>
|
||||
{list?.data.total && (
|
||||
<RAGFlowPagination
|
||||
{...pick(searchParams, 'current', 'pageSize')}
|
||||
total={list?.data.total}
|
||||
onChange={handlePageChange}
|
||||
on
|
||||
/>
|
||||
)}
|
||||
<Modal
|
||||
open={openCreateModal}
|
||||
onOpenChange={(open) => {
|
||||
setOpenCreateModal(open);
|
||||
}}
|
||||
title={
|
||||
<div className="rounded-sm bg-emerald-400 bg-gradient-to-t from-emerald-400 via-emerald-400 to-emerald-200 p-1 size-6 flex justify-center items-center">
|
||||
<Search size={14} className="font-bold m-auto" />
|
||||
</div>
|
||||
}
|
||||
className="!w-[480px] rounded-xl"
|
||||
titleClassName="border-none"
|
||||
footerClassName="border-none"
|
||||
showfooter={false}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="text-base mb-4">{t('createSearch')}</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<span className="text-destructive mr-1"> *</span>Name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-8 mb-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setOpenCreateModal(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading ? 'Confirm...' : 'Confirm'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</Modal>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,53 +1,58 @@
|
||||
import { MoreButton } from '@/components/more-button';
|
||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { IFlow } from '@/interfaces/database/flow';
|
||||
import { formatPureDate } from '@/utils/date';
|
||||
import { ChevronRight, Trash2 } from 'lucide-react';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { ISearchAppProps } from './hooks';
|
||||
import { SearchDropdown } from './search-dropdown';
|
||||
|
||||
interface IProps {
|
||||
data: IFlow;
|
||||
data: ISearchAppProps;
|
||||
}
|
||||
|
||||
export function SearchCard({ data }: IProps) {
|
||||
const { navigateToSearch } = useNavigatePage();
|
||||
|
||||
return (
|
||||
<Card className="border-colors-outline-neutral-standard">
|
||||
<Card
|
||||
className="bg-bg-card border-colors-outline-neutral-standard"
|
||||
onClick={() => {
|
||||
navigateToSearch(data?.id);
|
||||
}}
|
||||
>
|
||||
<CardContent className="p-4 flex gap-2 items-start group">
|
||||
<div className="flex justify-between mb-4">
|
||||
<RAGFlowAvatar
|
||||
className="w-[70px] h-[70px]"
|
||||
className="w-[32px] h-[32px]"
|
||||
avatar={data.avatar}
|
||||
name={data.title}
|
||||
name={data.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-col gap-1 flex-1">
|
||||
<section className="flex justify-between">
|
||||
<div className="text-[20px] font-bold size-7 leading-5">
|
||||
{data.title}
|
||||
<div className="text-[20px] font-bold w-80% leading-5">
|
||||
{data.name}
|
||||
</div>
|
||||
<MoreButton></MoreButton>
|
||||
<SearchDropdown dataset={data}>
|
||||
<MoreButton></MoreButton>
|
||||
</SearchDropdown>
|
||||
</section>
|
||||
|
||||
<div>An app that does things An app that does things</div>
|
||||
<div>{data.description}</div>
|
||||
<section className="flex justify-between">
|
||||
<div>
|
||||
Search app
|
||||
<p className="text-sm opacity-80">
|
||||
{formatPureDate(data.update_time)}
|
||||
{formatDate(data.update_time)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-x-2 invisible group-hover:visible">
|
||||
{/* <div className="space-x-2 invisible group-hover:visible">
|
||||
<Button variant="icon" size="icon" onClick={navigateToSearch}>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</Button>
|
||||
<Button variant="icon" size="icon">
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</div>
|
||||
</div> */}
|
||||
</section>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
50
web/src/pages/next-searches/search-dropdown.tsx
Normal file
50
web/src/pages/next-searches/search-dropdown.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { MouseEventHandler, PropsWithChildren, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ISearchAppProps, useDeleteSearch } from './hooks';
|
||||
|
||||
export function SearchDropdown({
|
||||
children,
|
||||
dataset,
|
||||
}: PropsWithChildren & {
|
||||
dataset: ISearchAppProps;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { deleteSearch } = useDeleteSearch();
|
||||
|
||||
const handleDelete: MouseEventHandler<HTMLDivElement> = useCallback(() => {
|
||||
deleteSearch({ search_id: dataset.id });
|
||||
}, [dataset.id, deleteSearch]);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{/* <DropdownMenuItem onClick={handleShowDatasetRenameModal}>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user