mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42: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:
@ -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) => () => {
|
||||
|
||||
108
web/src/pages/next-search/index.less
Normal file
108
web/src/pages/next-search/index.less
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
93
web/src/pages/next-search/search-home.tsx
Normal file
93
web/src/pages/next-search/search-home.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
497
web/src/pages/next-search/search-setting.tsx
Normal file
497
web/src/pages/next-search/search-setting.tsx
Normal file
@ -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 };
|
||||
64
web/src/pages/next-search/searching.tsx
Normal file
64
web/src/pages/next-search/searching.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
28
web/src/pages/next-search/spotlight.tsx
Normal file
28
web/src/pages/next-search/spotlight.tsx
Normal file
@ -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;
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -230,7 +230,7 @@ const routes = [
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.Search,
|
||||
path: `${Routes.Search}/:id`,
|
||||
layout: false,
|
||||
component: `@/pages${Routes.Search}`,
|
||||
},
|
||||
|
||||
23
web/src/services/search-service.ts
Normal file
23
web/src/services/search-service.ts
Normal file
@ -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`,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user