diff --git a/web/src/components/ragflow-avatar.tsx b/web/src/components/ragflow-avatar.tsx index e136e5806..63f492fd3 100644 --- a/web/src/components/ragflow-avatar.tsx +++ b/web/src/components/ragflow-avatar.tsx @@ -23,8 +23,8 @@ const getColorForName = (name: string): { from: string; to: string } => { const hue = hash % 360; return { - from: `hsl(${hue}, 70%, 80%)`, - to: `hsl(${hue}, 60%, 30%)`, + to: `hsl(${hue}, 70%, 80%)`, + from: `hsl(${hue}, 60%, 30%)`, }; }; export const RAGFlowAvatar = memo( diff --git a/web/src/components/ui/multi-select.tsx b/web/src/components/ui/multi-select.tsx index 3e28ef53d..d0f0f2d21 100644 --- a/web/src/components/ui/multi-select.tsx +++ b/web/src/components/ui/multi-select.tsx @@ -34,6 +34,7 @@ export type MultiSelectOptionType = { label: React.ReactNode; value: string; disabled?: boolean; + suffix?: React.ReactNode; icon?: React.ComponentType<{ className?: string }>; }; @@ -54,23 +55,41 @@ function MultiCommandItem({ return ( toggleOption(option.value)} - className="cursor-pointer" + onSelect={() => { + if (option.disabled) return false; + toggleOption(option.value); + }} + className={cn('cursor-pointer', { + 'cursor-not-allowed text-text-disabled': option.disabled, + })} >
{option.icon && ( - + + )} + + {option.label} + + {option.suffix && ( + + {option.suffix} + )} - {option.label}
); } @@ -156,6 +175,11 @@ interface MultiSelectProps * Optional, can be used to add custom styles. */ className?: string; + + /** + * If true, renders the multi-select component with a select all option. + */ + showSelectAll?: boolean; } export const MultiSelect = React.forwardRef< @@ -174,6 +198,7 @@ export const MultiSelect = React.forwardRef< modalPopover = false, asChild = false, className, + showSelectAll = true, ...props }, ref, @@ -340,23 +365,25 @@ export const MultiSelect = React.forwardRef< No results found. - -
- -
- (Select All) -
+
+ +
+ (Select All) + + )} {!options.some((x) => 'options' in x) && (options as unknown as MultiSelectOptionType[]).map( (option) => { diff --git a/web/src/hooks/logic-hooks/navigate-hooks.ts b/web/src/hooks/logic-hooks/navigate-hooks.ts index ab3e0ad66..4cd9aa6f1 100644 --- a/web/src/hooks/logic-hooks/navigate-hooks.ts +++ b/web/src/hooks/logic-hooks/navigate-hooks.ts @@ -72,9 +72,12 @@ export const useNavigatePage = () => { navigate(Routes.Searches); }, [navigate]); - const navigateToSearch = useCallback(() => { - navigate(Routes.Search); - }, [navigate]); + const navigateToSearch = useCallback( + (id: string) => { + navigate(`${Routes.Search}/${id}`); + }, + [navigate], + ); const navigateToChunkParsedResult = useCallback( (id: string, knowledgeId?: string) => () => { diff --git a/web/src/pages/next-search/index.less b/web/src/pages/next-search/index.less new file mode 100644 index 000000000..f359d1463 --- /dev/null +++ b/web/src/pages/next-search/index.less @@ -0,0 +1,108 @@ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translate3d(0, 100%, 0); + } + to { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} + +@keyframes fadeInLeft { + from { + opacity: 0; + transform: translate3d(-50%, 0, 0); + } + to { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} +@keyframes fadeInRight { + from { + opacity: 0; + transform: translate3d(50%, 0, 0); + } + to { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} +@keyframes fadeOutRight { + from { + opacity: 0; + transform: translate3d(0, 0, 0); + } + to { + opacity: 1; + transform: translate3d(120%, 0, 0); + } +} +@keyframes fadeInDown { + from { + opacity: 0; + transform: translate3d(0, -50%, 0); + } + to { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} + +.animate-fade-in-up { + animation-name: fadeInUp; + animation-duration: 0.5s; + animation-fill-mode: both; +} + +.animate-fade-in-down { + animation-name: fadeInDown; + animation-duration: 0.5s; + animation-fill-mode: both; +} + +.animate-fade-in-left { + animation-name: fadeInLeft; + animation-duration: 0.5s; + animation-fill-mode: both; +} + +.animate-fade-in-right { + animation-name: fadeInRight; + animation-duration: 0.5s; + animation-fill-mode: both; +} +.animate-fade-out-right { + animation-name: fadeOutRight; + animation-duration: 0.5s; + animation-fill-mode: both; +} + +.delay-100 { + animation-delay: 0.1s; +} + +.delay-200 { + animation-delay: 0.2s; +} + +.delay-300 { + animation-delay: 0.3s; +} + +.delay-400 { + animation-delay: 0.4s; +} + +.delay-500 { + animation-delay: 0.5s; +} + +.delay-600 { + animation-delay: 0.6s; +} + +.delay-700 { + animation-delay: 0.7s; +} diff --git a/web/src/pages/next-search/index.tsx b/web/src/pages/next-search/index.tsx index 391f15935..b20b1fc3f 100644 --- a/web/src/pages/next-search/index.tsx +++ b/web/src/pages/next-search/index.tsx @@ -1,21 +1,89 @@ import { PageHeader } from '@/components/page-header'; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from '@/components/ui/breadcrumb'; import { Button } from '@/components/ui/button'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; -import { EllipsisVertical } from 'lucide-react'; +import { Settings } from 'lucide-react'; +import { useState } from 'react'; +import { + ISearchAppDetailProps, + useFetchSearchDetail, +} from '../next-searches/hooks'; +import './index.less'; +import SearchHome from './search-home'; +import { SearchSetting } from './search-setting'; +import SearchingPage from './searching'; export default function SearchPage() { const { navigateToSearchList } = useNavigatePage(); + const [isSearching, setIsSearching] = useState(false); + const { data: SearchData } = useFetchSearchDetail(); + const [openSetting, setOpenSetting] = useState(false); return (
- -
- - -
+ + + + + + Search + + + + + {SearchData?.name} + + + +
+
+ {!isSearching && ( +
+ +
+ )} + {isSearching && ( +
+ +
+ )} +
+ {/* {openSetting && ( +
*/} + + {/*
+ )} */} +
+ +
+ +
); } diff --git a/web/src/pages/next-search/search-home.tsx b/web/src/pages/next-search/search-home.tsx new file mode 100644 index 000000000..413a58553 --- /dev/null +++ b/web/src/pages/next-search/search-home.tsx @@ -0,0 +1,93 @@ +import { Input } from '@/components/originui/input'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { Search } from 'lucide-react'; +import { Dispatch, SetStateAction } from 'react'; +import './index.less'; +import Spotlight from './spotlight'; + +export default function SearchPage({ + isSearching, + setIsSearching, +}: { + isSearching: boolean; + setIsSearching: Dispatch>; +}) { + return ( +
+
+

+ RAGFlow +

+ +
+ {!isSearching && } +
+ {!isSearching && ( + <> +

👋 Hi there

+

Welcome back, KiKi

+ + )} + +
+ + +
+
+
+ +
+

Related Search

+
+ + + + + +
+
+
+
+ ); +} diff --git a/web/src/pages/next-search/search-setting.tsx b/web/src/pages/next-search/search-setting.tsx new file mode 100644 index 000000000..22100e989 --- /dev/null +++ b/web/src/pages/next-search/search-setting.tsx @@ -0,0 +1,497 @@ +// src/pages/next-search/search-setting.tsx + +import { RAGFlowAvatar } from '@/components/ragflow-avatar'; +import { Button } from '@/components/ui/button'; +import { SingleFormSlider } from '@/components/ui/dual-range-slider'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + MultiSelect, + MultiSelectOptionType, +} from '@/components/ui/multi-select'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; +import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; +import { IKnowledge } from '@/interfaces/database/knowledge'; +import { cn } from '@/lib/utils'; +import { transformFile2Base64 } from '@/utils/file-util'; +import { t } from 'i18next'; +import { PanelRightClose, Pencil, Upload } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { ISearchAppDetailProps } from '../next-searches/hooks'; + +interface SearchSettingProps { + open: boolean; + setOpen: (open: boolean) => void; + className?: string; + data: ISearchAppDetailProps; +} + +const SearchSetting: React.FC = ({ + open = false, + setOpen, + className, + data, +}) => { + const [width0, setWidth0] = useState('w-[440px]'); + // "avatar": null, + // "created_by": "c3fb861af27a11efa69751e139332ced", + // "description": "My first search app", + // "id": "22e874584b4511f0aa1ac57b9ea5a68b", + // "name": "updated search app", + // "search_config": { + // "cross_languages": [], + // "doc_ids": [], + // "highlight": false, + // "kb_ids": [], + // "keyword": false, + // "query_mindmap": false, + // "related_search": false, + // "rerank_id": "", + // "similarity_threshold": 0.5, + // "summary": false, + // "top_k": 1024, + // "use_kg": true, + // "vector_similarity_weight": 0.3, + // "web_search": false + // }, + // "tenant_id": "c3fb861af27a11efa69751e139332ced", + // "update_time": 1750144129641 + const formMethods = useForm({ + defaultValues: { + id: '', + name: '', + avatar: '', + description: 'You are an intelligent assistant.', + datasets: '', + keywordSimilarityWeight: 20, + rerankModel: false, + aiSummary: false, + topK: true, + searchMethod: '', + model: '', + enableWebSearch: false, + enableRelatedSearch: true, + showQueryMindmap: true, + }, + }); + const [avatarFile, setAvatarFile] = useState(null); + const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64 + const [datasetList, setDatasetList] = useState([]); + const [datasetSelectEmbdId, setDatasetSelectEmbdId] = useState(''); + useEffect(() => { + if (!open) { + setTimeout(() => { + setWidth0('w-0 hidden'); + }, 500); + } else { + setWidth0('w-[440px]'); + } + }, [open]); + useEffect(() => { + if (!avatarFile) { + setAvatarBase64Str(data?.avatar); + } + }, [avatarFile, data?.avatar]); + useEffect(() => { + if (avatarFile) { + (async () => { + // make use of img compression transformFile2Base64 + setAvatarBase64Str(await transformFile2Base64(avatarFile)); + })(); + } + }, [avatarFile]); + const { list: datasetListOrigin, loading: datasetLoading } = + useFetchKnowledgeList(); + + useEffect(() => { + const datasetListMap = datasetListOrigin.map((item: IKnowledge) => { + return { + label: item.name, + suffix: ( +
+ {item.embd_id} +
+ ), + value: item.id, + disabled: + item.embd_id !== datasetSelectEmbdId && datasetSelectEmbdId !== '', + }; + }); + setDatasetList(datasetListMap); + }, [datasetListOrigin, datasetSelectEmbdId]); + + const handleDatasetSelectChange = (value, onChange) => { + console.log(value); + if (value.length) { + const data = datasetListOrigin?.find((item) => item.id === value[0]); + setDatasetSelectEmbdId(data?.embd_id ?? ''); + } else { + setDatasetSelectEmbdId(''); + } + onChange?.(value); + }; + return ( +
+
+
Search Settings
+
setOpen(false)}> + +
+
+
+
+ console.log(data))} + className="space-y-6" + > + {/* Name */} + ( + + + *Name + + + + + + + )} + /> + + {/* Avatar */} + ( + + Avatar + +
+ {!avatarBase64Str ? ( +
+
+ +

{t('common.upload')}

+
+
+ ) : ( +
+ +
+ +
+
+ )} + { + const file = ev.target?.files?.[0]; + if ( + /\.(jpg|jpeg|png|webp|bmp)$/i.test(file?.name ?? '') + ) { + setAvatarFile(file!); + } + ev.target.value = ''; + }} + /> +
+
+ +
+ )} + /> + + {/* Description */} + ( + + Description + + + + + + )} + /> + + {/* Datasets */} + ( + + + *Datasets + + + { + handleDatasetSelectChange(value, field.onChange); + }} + showSelectAll={false} + placeholder={t('chat.knowledgeBasesMessage')} + variant="inverted" + maxCount={10} + {...field} + /> + + + + )} + /> + + {/* Keyword Similarity Weight */} + ( + + Keyword Similarity Weight + +
+ field.onChange(values)} + > + +
+
+ +
+ )} + /> + + {/* Rerank Model */} + ( + + + + + Rerank Model + + )} + /> + + {/* AI Summary */} + ( + + + + + AI Summary + + + )} + /> + + {/* Top K */} + ( + + + + + Top K + + )} + /> + + {/* Search Method */} + ( + + + *Search + Method + + + + + + + )} + /> + + {/* Model */} + ( + + + *Model + + + + + + + )} + /> + + {/* Feature Controls */} + ( + + + + + Enable Web Search + + )} + /> + + ( + + + + + Enable Related Search + + )} + /> + + ( + + + + + Show Query Mindmap + + )} + /> + {/* Submit Button */} +
+ +
+ + +
+
+ ); +}; + +export { SearchSetting }; diff --git a/web/src/pages/next-search/searching.tsx b/web/src/pages/next-search/searching.tsx new file mode 100644 index 000000000..f79ed0ebf --- /dev/null +++ b/web/src/pages/next-search/searching.tsx @@ -0,0 +1,64 @@ +import { Input } from '@/components/originui/input'; +import { cn } from '@/lib/utils'; +import { Search, X } from 'lucide-react'; +import { Dispatch, SetStateAction } from 'react'; +import './index.less'; + +export default function SearchingPage({ + isSearching, + setIsSearching, +}: { + isSearching: boolean; + setIsSearching: Dispatch>; +}) { + return ( +
+
+

+ RAGFlow +

+ +
+
+
+ +
+ | + +
+
+
+
+
+
+ ); +} diff --git a/web/src/pages/next-search/spotlight.tsx b/web/src/pages/next-search/spotlight.tsx new file mode 100644 index 000000000..1e17b8e48 --- /dev/null +++ b/web/src/pages/next-search/spotlight.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +interface SpotlightProps { + className?: string; +} + +const Spotlight: React.FC = ({ className }) => { + return ( +
+
+
+ ); +}; + +export default Spotlight; diff --git a/web/src/pages/next-searches/hooks.ts b/web/src/pages/next-searches/hooks.ts new file mode 100644 index 000000000..66fdc4f50 --- /dev/null +++ b/web/src/pages/next-searches/hooks.ts @@ -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({ + 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; + total: number; + }; + message: string; +} + +export const useFetchSearchList = (params?: SearchListParams) => { + const [searchParams, setSearchParams] = useState({ + page: 1, + page_size: 10, + ...params, + }); + + const { data, isLoading, isError } = useQuery({ + 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({ + 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({ + 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 }; +}; diff --git a/web/src/pages/next-searches/index.tsx b/web/src/pages/next-searches/index.tsx index 1b825fe24..18b49b1c9 100644 --- a/web/src/pages/next-searches/index.tsx +++ b/web/src/pages/next-searches/index.tsx @@ -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; 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({ + 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 (
@@ -31,35 +76,7 @@ export default function SearchList() {
- {data.map((x) => { + {list?.data.search_apps.map((x) => { return ; })} + {/* {data.map((x) => { + return ; + })} */}
+ {list?.data.total && ( + + )} + { + setOpenCreateModal(open); + }} + title={ +
+ +
+ } + className="!w-[480px] rounded-xl" + titleClassName="border-none" + footerClassName="border-none" + showfooter={false} + maskClosable={false} + > +
+ +
{t('createSearch')}
+ + ( + + + *Name + + + + + + + )} + /> + +
+ + +
+ + +
); } diff --git a/web/src/pages/next-searches/search-card.tsx b/web/src/pages/next-searches/search-card.tsx index 5b1e5a385..830e92aaa 100644 --- a/web/src/pages/next-searches/search-card.tsx +++ b/web/src/pages/next-searches/search-card.tsx @@ -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 ( - + { + navigateToSearch(data?.id); + }} + >
-
+
-
- {data.title} +
+ {data.name}
- + + +
-
An app that does things An app that does things
+
{data.description}
Search app

- {formatPureDate(data.update_time)} + {formatDate(data.update_time)}

-
+ {/*
-
+
*/}
diff --git a/web/src/pages/next-searches/search-dropdown.tsx b/web/src/pages/next-searches/search-dropdown.tsx new file mode 100644 index 000000000..e2cc0dfac --- /dev/null +++ b/web/src/pages/next-searches/search-dropdown.tsx @@ -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 = useCallback(() => { + deleteSearch({ search_id: dataset.id }); + }, [dataset.id, deleteSearch]); + + return ( + + {children} + + {/* + {t('common.rename')} + + */} + + { + e.preventDefault(); + }} + onClick={(e) => { + e.stopPropagation(); + }} + > + {t('common.delete')} + + + + + ); +} diff --git a/web/src/routes.ts b/web/src/routes.ts index 3f2d04b26..f7c0874d9 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -230,7 +230,7 @@ const routes = [ ], }, { - path: Routes.Search, + path: `${Routes.Search}/:id`, layout: false, component: `@/pages${Routes.Search}`, }, diff --git a/web/src/services/search-service.ts b/web/src/services/search-service.ts new file mode 100644 index 000000000..51b04300b --- /dev/null +++ b/web/src/services/search-service.ts @@ -0,0 +1,23 @@ +import api from '@/utils/api'; +import registerServer from '@/utils/register-server'; +import request from '@/utils/request'; + +const { createSearch, getSearchList, deleteSearch, getSearchDetail } = api; +const methods = { + createSearch: { + url: createSearch, + method: 'post', + }, + getSearchList: { + url: getSearchList, + method: 'post', + }, + deleteSearch: { url: deleteSearch, method: 'post' }, + getSearchDetail: { + url: getSearchDetail, + method: 'get', + }, +} as const; +const searchService = registerServer(methods, request); + +export default searchService; diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 98eba37dd..452e0a73f 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -174,4 +174,10 @@ export default { testMcpServerTool: `${api_host}/mcp_server/test_tool`, cacheMcpServerTool: `${api_host}/mcp_server/cache_tools`, testMcpServer: `${api_host}/mcp_server/test_mcp`, + + // next-search + createSearch: `${api_host}/search/create`, + getSearchList: `${api_host}/search/list`, + deleteSearch: `${api_host}/search/rm`, + getSearchDetail: `${api_host}/search/detail`, };