### 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)tags/v0.20.2
@@ -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( |
@@ -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 ( | |||
<CommandItem | |||
key={option.value} | |||
onSelect={() => 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, | |||
})} | |||
> | |||
<div | |||
className={cn( | |||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary', | |||
isSelected | |||
? 'bg-primary text-primary-foreground' | |||
: 'opacity-50 [&_svg]:invisible', | |||
isSelected ? 'bg-primary ' : 'opacity-50 [&_svg]:invisible', | |||
{ 'text-primary-foreground': !option.disabled }, | |||
{ 'text-text-disabled': option.disabled }, | |||
)} | |||
> | |||
<CheckIcon className="h-4 w-4" /> | |||
</div> | |||
{option.icon && ( | |||
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" /> | |||
<option.icon | |||
className={cn('mr-2 h-4 w-4 ', { | |||
'text-text-disabled': option.disabled, | |||
'text-muted-foreground': !option.disabled, | |||
})} | |||
/> | |||
)} | |||
<span className={cn({ 'text-text-disabled': option.disabled })}> | |||
{option.label} | |||
</span> | |||
{option.suffix && ( | |||
<span className={cn({ 'text-text-disabled': option.disabled })}> | |||
{option.suffix} | |||
</span> | |||
)} | |||
<span>{option.label}</span> | |||
</CommandItem> | |||
); | |||
} | |||
@@ -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< | |||
<CommandList> | |||
<CommandEmpty>No results found.</CommandEmpty> | |||
<CommandGroup> | |||
<CommandItem | |||
key="all" | |||
onSelect={toggleAll} | |||
className="cursor-pointer" | |||
> | |||
<div | |||
className={cn( | |||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary', | |||
selectedValues.length === flatOptions.length | |||
? 'bg-primary text-primary-foreground' | |||
: 'opacity-50 [&_svg]:invisible', | |||
)} | |||
{showSelectAll && ( | |||
<CommandItem | |||
key="all" | |||
onSelect={toggleAll} | |||
className="cursor-pointer" | |||
> | |||
<CheckIcon className="h-4 w-4" /> | |||
</div> | |||
<span>(Select All)</span> | |||
</CommandItem> | |||
<div | |||
className={cn( | |||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary', | |||
selectedValues.length === flatOptions.length | |||
? 'bg-primary text-primary-foreground' | |||
: 'opacity-50 [&_svg]:invisible', | |||
)} | |||
> | |||
<CheckIcon className="h-4 w-4" /> | |||
</div> | |||
<span>(Select All)</span> | |||
</CommandItem> | |||
)} | |||
{!options.some((x) => 'options' in x) && | |||
(options as unknown as MultiSelectOptionType[]).map( | |||
(option) => { |
@@ -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) => () => { |
@@ -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; | |||
} |
@@ -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 ( | |||
<section> | |||
<PageHeader back={navigateToSearchList} title="Search app 01"> | |||
<div className="flex items-center gap-2"> | |||
<Button variant={'icon'} size={'icon'}> | |||
<EllipsisVertical /> | |||
</Button> | |||
<Button size={'sm'}>Publish</Button> | |||
</div> | |||
<PageHeader> | |||
<Breadcrumb> | |||
<BreadcrumbList> | |||
<BreadcrumbItem> | |||
<BreadcrumbLink onClick={navigateToSearchList}> | |||
Search | |||
</BreadcrumbLink> | |||
</BreadcrumbItem> | |||
<BreadcrumbSeparator /> | |||
<BreadcrumbItem> | |||
<BreadcrumbPage>{SearchData?.name}</BreadcrumbPage> | |||
</BreadcrumbItem> | |||
</BreadcrumbList> | |||
</Breadcrumb> | |||
</PageHeader> | |||
<div className="flex gap-3 w-full"> | |||
<div className="flex-1"> | |||
{!isSearching && ( | |||
<div className="animate-fade-in-down"> | |||
<SearchHome | |||
setIsSearching={setIsSearching} | |||
isSearching={isSearching} | |||
/> | |||
</div> | |||
)} | |||
{isSearching && ( | |||
<div className="animate-fade-in-up"> | |||
<SearchingPage | |||
setIsSearching={setIsSearching} | |||
isSearching={isSearching} | |||
/> | |||
</div> | |||
)} | |||
</div> | |||
{/* {openSetting && ( | |||
<div className=" w-[440px]"> */} | |||
<SearchSetting | |||
className="mt-20 mr-2" | |||
open={openSetting} | |||
setOpen={setOpenSetting} | |||
data={SearchData as ISearchAppDetailProps} | |||
/> | |||
{/* </div> | |||
)} */} | |||
</div> | |||
<div className="absolute left-5 bottom-12 "> | |||
<Button | |||
variant="transparent" | |||
className="bg-bg-card" | |||
onClick={() => setOpenSetting(!openSetting)} | |||
> | |||
<Settings className="text-text-secondary" /> | |||
<div className="text-text-secondary">Search Settings</div> | |||
</Button> | |||
</div> | |||
</section> | |||
); | |||
} |
@@ -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<SetStateAction<boolean>>; | |||
}) { | |||
return ( | |||
<section className="relative w-full flex transition-all justify-center items-center mt-32"> | |||
<div className="relative z-10 px-8 pt-8 flex text-transparent flex-col justify-center items-center w-[780px]"> | |||
<h1 | |||
className={cn( | |||
'text-4xl font-bold bg-gradient-to-r from-sky-600 from-30% via-sky-500 via-60% to-emerald-500 bg-clip-text', | |||
)} | |||
> | |||
RAGFlow | |||
</h1> | |||
<div className="rounded-lg text-primary text-xl sticky flex justify-center w-full transform scale-100 mt-8 p-6 h-[230px] border"> | |||
{!isSearching && <Spotlight className="z-0" />} | |||
<div className="flex flex-col justify-center items-center w-2/3"> | |||
{!isSearching && ( | |||
<> | |||
<p className="mb-4 transition-opacity">👋 Hi there</p> | |||
<p className="mb-10 transition-opacity">Welcome back, KiKi</p> | |||
</> | |||
)} | |||
<div className="relative w-full "> | |||
<Input | |||
placeholder="How can I help you today?" | |||
className="w-full rounded-full py-6 px-4 pr-10 text-white text-lg bg-background delay-700" | |||
/> | |||
<button | |||
type="button" | |||
className="absolute right-2 top-1/2 -translate-y-1/2 transform rounded-full bg-white p-2 text-gray-800 shadow w-12" | |||
onClick={() => { | |||
setIsSearching(!isSearching); | |||
}} | |||
> | |||
<Search size={22} className="m-auto" /> | |||
</button> | |||
</div> | |||
</div> | |||
</div> | |||
<div className="mt-14 w-full overflow-hidden opacity-100 max-h-96"> | |||
<p className="text-text-primary mb-2 text-xl">Related Search</p> | |||
<div className="mt-2 flex flex-wrap justify-start gap-2"> | |||
<Button | |||
variant="transparent" | |||
className="bg-bg-card text-text-secondary" | |||
> | |||
Related Search | |||
</Button> | |||
<Button | |||
variant="transparent" | |||
className="bg-bg-card text-text-secondary" | |||
> | |||
Related Search Related SearchRelated Search | |||
</Button> | |||
<Button | |||
variant="transparent" | |||
className="bg-bg-card text-text-secondary" | |||
> | |||
Related Search Search | |||
</Button> | |||
<Button | |||
variant="transparent" | |||
className="bg-bg-card text-text-secondary" | |||
> | |||
Related Search Related SearchRelated Search | |||
</Button> | |||
<Button | |||
variant="transparent" | |||
className="bg-bg-card text-text-secondary" | |||
> | |||
Related Search | |||
</Button> | |||
</div> | |||
</div> | |||
</div> | |||
</section> | |||
); | |||
} |
@@ -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<SearchSettingProps> = ({ | |||
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<File | null>(null); | |||
const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64 | |||
const [datasetList, setDatasetList] = useState<MultiSelectOptionType[]>([]); | |||
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: ( | |||
<div className="text-xs px-4 p-1 bg-bg-card text-text-secondary rounded-lg border border-bg-card"> | |||
{item.embd_id} | |||
</div> | |||
), | |||
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 ( | |||
<div | |||
className={cn( | |||
'text-text-primary border p-4 rounded-lg', | |||
{ | |||
'animate-fade-in-right': open, | |||
'animate-fade-out-right': !open, | |||
}, | |||
width0, | |||
className, | |||
)} | |||
style={{ height: 'calc(100dvh - 170px)' }} | |||
> | |||
<div className="flex justify-between items-center text-base mb-8"> | |||
<div className="text-text-primary">Search Settings</div> | |||
<div onClick={() => setOpen(false)}> | |||
<PanelRightClose | |||
size={16} | |||
className="text-text-primary cursor-pointer" | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
style={{ height: 'calc(100dvh - 270px)' }} | |||
className="overflow-y-auto scrollbar-auto p-1 text-text-secondary" | |||
> | |||
<Form {...formMethods}> | |||
<form | |||
onSubmit={formMethods.handleSubmit((data) => console.log(data))} | |||
className="space-y-6" | |||
> | |||
{/* Name */} | |||
<FormField | |||
control={formMethods.control} | |||
name="name" | |||
rules={{ required: 'Name is required' }} | |||
render={({ field }) => ( | |||
<FormItem> | |||
<FormLabel> | |||
<span className="text-destructive mr-1"> *</span>Name | |||
</FormLabel> | |||
<FormControl> | |||
<Input placeholder="Name" {...field} /> | |||
</FormControl> | |||
<FormMessage /> | |||
</FormItem> | |||
)} | |||
/> | |||
{/* Avatar */} | |||
<FormField | |||
control={formMethods.control} | |||
name="avatar" | |||
render={() => ( | |||
<FormItem> | |||
<FormLabel>Avatar</FormLabel> | |||
<FormControl> | |||
<div className="relative group"> | |||
{!avatarBase64Str ? ( | |||
<div className="w-[64px] h-[64px] grid place-content-center border border-dashed rounded-md"> | |||
<div className="flex flex-col items-center"> | |||
<Upload /> | |||
<p>{t('common.upload')}</p> | |||
</div> | |||
</div> | |||
) : ( | |||
<div className="w-[64px] h-[64px] relative grid place-content-center"> | |||
<RAGFlowAvatar | |||
avatar={avatarBase64Str} | |||
name={data.name} | |||
className="w-[64px] h-[64px] rounded-md block" | |||
/> | |||
<div className="absolute inset-0 bg-[#000]/20 group-hover:bg-[#000]/60"> | |||
<Pencil | |||
size={20} | |||
className="absolute right-2 bottom-0 opacity-50 hidden group-hover:block" | |||
/> | |||
</div> | |||
</div> | |||
)} | |||
<Input | |||
placeholder="" | |||
// {...field} | |||
type="file" | |||
title="" | |||
accept="image/*" | |||
className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" | |||
onChange={(ev) => { | |||
const file = ev.target?.files?.[0]; | |||
if ( | |||
/\.(jpg|jpeg|png|webp|bmp)$/i.test(file?.name ?? '') | |||
) { | |||
setAvatarFile(file!); | |||
} | |||
ev.target.value = ''; | |||
}} | |||
/> | |||
</div> | |||
</FormControl> | |||
<FormMessage /> | |||
</FormItem> | |||
)} | |||
/> | |||
{/* Description */} | |||
<FormField | |||
control={formMethods.control} | |||
name="description" | |||
render={({ field }) => ( | |||
<FormItem> | |||
<FormLabel>Description</FormLabel> | |||
<FormControl> | |||
<Input | |||
placeholder="Description" | |||
{...field} | |||
defaultValue="You are an intelligent assistant." | |||
/> | |||
</FormControl> | |||
<FormMessage /> | |||
</FormItem> | |||
)} | |||
/> | |||
{/* Datasets */} | |||
<FormField | |||
control={formMethods.control} | |||
name="datasets" | |||
rules={{ required: 'Datasets is required' }} | |||
render={({ field }) => ( | |||
<FormItem> | |||
<FormLabel> | |||
<span className="text-destructive mr-1"> *</span>Datasets | |||
</FormLabel> | |||
<FormControl> | |||
<MultiSelect | |||
options={datasetList} | |||
onValueChange={(value) => { | |||
handleDatasetSelectChange(value, field.onChange); | |||
}} | |||
showSelectAll={false} | |||
placeholder={t('chat.knowledgeBasesMessage')} | |||
variant="inverted" | |||
maxCount={10} | |||
{...field} | |||
/> | |||
</FormControl> | |||
<FormMessage /> | |||
</FormItem> | |||
)} | |||
/> | |||
{/* Keyword Similarity Weight */} | |||
<FormField | |||
control={formMethods.control} | |||
name="keywordSimilarityWeight" | |||
render={({ field }) => ( | |||
<FormItem className="flex flex-col"> | |||
<FormLabel>Keyword Similarity Weight</FormLabel> | |||
<FormControl> | |||
<div className="flex justify-between items-center"> | |||
<SingleFormSlider | |||
max={100} | |||
step={1} | |||
value={field.value as number} | |||
onChange={(values) => field.onChange(values)} | |||
></SingleFormSlider> | |||
<Label className="w-10 h-6 bg-bg-card flex justify-center items-center rounded-lg ml-20"> | |||
{field.value} | |||
</Label> | |||
</div> | |||
</FormControl> | |||
<FormMessage /> | |||
</FormItem> | |||
)} | |||
/> | |||
{/* Rerank Model */} | |||
<FormField | |||
control={formMethods.control} | |||
name="rerankModel" | |||
render={({ field }) => ( | |||
<FormItem className="flex flex-row items-start space-x-3 space-y-0"> | |||
<FormControl> | |||
<Switch | |||
checked={field.value} | |||
onCheckedChange={field.onChange} | |||
/> | |||
</FormControl> | |||
<FormLabel>Rerank Model</FormLabel> | |||
</FormItem> | |||
)} | |||
/> | |||
{/* AI Summary */} | |||
<FormField | |||
control={formMethods.control} | |||
name="aiSummary" | |||
render={({ field }) => ( | |||
<FormItem className="flex flex-row items-start space-x-3 space-y-0"> | |||
<FormControl> | |||
<Switch | |||
checked={field.value} | |||
onCheckedChange={field.onChange} | |||
/> | |||
</FormControl> | |||
<FormLabel>AI Summary</FormLabel> | |||
<Label className="text-sm text-muted-foreground"> | |||
默认不打开 | |||
</Label> | |||
</FormItem> | |||
)} | |||
/> | |||
{/* Top K */} | |||
<FormField | |||
control={formMethods.control} | |||
name="topK" | |||
render={({ field }) => ( | |||
<FormItem className="flex flex-row items-start space-x-3 space-y-0"> | |||
<FormControl> | |||
<Switch | |||
checked={field.value} | |||
onCheckedChange={field.onChange} | |||
/> | |||
</FormControl> | |||
<FormLabel>Top K</FormLabel> | |||
</FormItem> | |||
)} | |||
/> | |||
{/* Search Method */} | |||
<FormField | |||
control={formMethods.control} | |||
name="searchMethod" | |||
rules={{ required: 'Search Method is required' }} | |||
render={({ field }) => ( | |||
<FormItem> | |||
<FormLabel> | |||
<span className="text-destructive mr-1"> *</span>Search | |||
Method | |||
</FormLabel> | |||
<FormControl> | |||
<Select | |||
onValueChange={field.onChange} | |||
defaultValue={field.value} | |||
> | |||
<SelectTrigger> | |||
<SelectValue placeholder="Select search method..." /> | |||
</SelectTrigger> | |||
<SelectContent> | |||
<SelectItem value="method1">Method 1</SelectItem> | |||
<SelectItem value="method2">Method 2</SelectItem> | |||
</SelectContent> | |||
</Select> | |||
</FormControl> | |||
<FormMessage /> | |||
</FormItem> | |||
)} | |||
/> | |||
{/* Model */} | |||
<FormField | |||
control={formMethods.control} | |||
name="model" | |||
rules={{ required: 'Model is required' }} | |||
render={({ field }) => ( | |||
<FormItem> | |||
<FormLabel> | |||
<span className="text-destructive mr-1"> *</span>Model | |||
</FormLabel> | |||
<FormControl> | |||
<Select | |||
onValueChange={field.onChange} | |||
defaultValue={field.value} | |||
> | |||
<SelectTrigger> | |||
<SelectValue placeholder="Select model..." /> | |||
</SelectTrigger> | |||
<SelectContent> | |||
<SelectItem value="model1">Model 1</SelectItem> | |||
<SelectItem value="model2">Model 2</SelectItem> | |||
</SelectContent> | |||
</Select> | |||
</FormControl> | |||
<FormMessage /> | |||
</FormItem> | |||
)} | |||
/> | |||
{/* Feature Controls */} | |||
<FormField | |||
control={formMethods.control} | |||
name="enableWebSearch" | |||
render={({ field }) => ( | |||
<FormItem className="flex flex-row items-start space-x-3 space-y-0"> | |||
<FormControl> | |||
<Switch | |||
checked={field.value} | |||
onCheckedChange={field.onChange} | |||
/> | |||
</FormControl> | |||
<FormLabel>Enable Web Search</FormLabel> | |||
</FormItem> | |||
)} | |||
/> | |||
<FormField | |||
control={formMethods.control} | |||
name="enableRelatedSearch" | |||
render={({ field }) => ( | |||
<FormItem className="flex flex-row items-start space-x-3 space-y-0"> | |||
<FormControl> | |||
<Switch | |||
checked={field.value} | |||
onCheckedChange={field.onChange} | |||
/> | |||
</FormControl> | |||
<FormLabel>Enable Related Search</FormLabel> | |||
</FormItem> | |||
)} | |||
/> | |||
<FormField | |||
control={formMethods.control} | |||
name="showQueryMindmap" | |||
render={({ field }) => ( | |||
<FormItem className="flex flex-row items-start space-x-3 space-y-0"> | |||
<FormControl> | |||
<Switch | |||
checked={field.value} | |||
onCheckedChange={field.onChange} | |||
/> | |||
</FormControl> | |||
<FormLabel>Show Query Mindmap</FormLabel> | |||
</FormItem> | |||
)} | |||
/> | |||
{/* Submit Button */} | |||
<div className="flex justify-end"> | |||
<Button type="submit">Confirm</Button> | |||
</div> | |||
</form> | |||
</Form> | |||
</div> | |||
</div> | |||
); | |||
}; | |||
export { SearchSetting }; |
@@ -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<SetStateAction<boolean>>; | |||
}) { | |||
return ( | |||
<section | |||
className={cn( | |||
'relative w-full flex transition-all justify-start items-center', | |||
)} | |||
> | |||
<div | |||
className={cn( | |||
'relative z-10 px-8 pt-8 flex text-transparent justify-start items-start w-full', | |||
)} | |||
> | |||
<h1 | |||
className={cn( | |||
'text-4xl font-bold bg-gradient-to-r from-sky-600 from-30% via-sky-500 via-60% to-emerald-500 bg-clip-text', | |||
)} | |||
> | |||
RAGFlow | |||
</h1> | |||
<div | |||
className={cn( | |||
' rounded-lg text-primary text-xl sticky flex justify-center w-2/3 max-w-[780px] transform scale-100 ml-16 ', | |||
)} | |||
> | |||
<div className={cn('flex flex-col justify-start items-start w-full')}> | |||
<div className="relative w-full text-primary"> | |||
<Input | |||
placeholder="How can I help you today?" | |||
className={cn( | |||
'w-full rounded-full py-6 pl-4 !pr-[8rem] text-primary text-lg bg-background', | |||
)} | |||
/> | |||
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform flex items-center gap-1"> | |||
<X />| | |||
<button | |||
type="button" | |||
className="rounded-full bg-white p-1 text-gray-800 shadow w-12 h-8 ml-4" | |||
onClick={() => { | |||
setIsSearching(!isSearching); | |||
}} | |||
> | |||
<Search size={22} className="m-auto" /> | |||
</button> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</section> | |||
); | |||
} |
@@ -0,0 +1,28 @@ | |||
import React from 'react'; | |||
interface SpotlightProps { | |||
className?: string; | |||
} | |||
const Spotlight: React.FC<SpotlightProps> = ({ className }) => { | |||
return ( | |||
<div | |||
className={`absolute inset-0 opacity-80 ${className} rounded-lg`} | |||
style={{ | |||
backdropFilter: 'blur(30px)', | |||
zIndex: -1, | |||
}} | |||
> | |||
<div | |||
className="absolute inset-0" | |||
style={{ | |||
background: | |||
'radial-gradient(circle at 50% 190%, #fff4 0%, #fff0 60%)', | |||
pointerEvents: 'none', | |||
}} | |||
></div> | |||
</div> | |||
); | |||
}; | |||
export default Spotlight; |
@@ -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> |
@@ -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> | |||
); | |||
} |
@@ -230,7 +230,7 @@ const routes = [ | |||
], | |||
}, | |||
{ | |||
path: Routes.Search, | |||
path: `${Routes.Search}/:id`, | |||
layout: false, | |||
component: `@/pages${Routes.Search}`, | |||
}, |
@@ -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<keyof typeof methods>(methods, request); | |||
export default searchService; |
@@ -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`, | |||
}; |