浏览代码

feat(next-search): Added AI summary functionality #3221 (#9402)

### What problem does this PR solve?

feat(next-search): Added AI summary functionality #3221

- Added the LlmSettingFieldItems component for AI summary settings
- Updated the SearchSetting component to integrate AI summary
functionality
- Added the updateSearch hook and related service methods
- Modified the ISearchAppDetailProps interface to add the llm_setting
field

### Type of change
- [x] New Feature (non-breaking change which adds functionality)
tags/v0.20.2
chanx 2 个月前
父节点
当前提交
735570486f
没有帐户链接到提交者的电子邮件

+ 4
- 0
web/src/components/ui/multi-select.tsx 查看文件

@@ -208,6 +208,10 @@ export const MultiSelect = React.forwardRef<
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const [isAnimating, setIsAnimating] = React.useState(false);

React.useEffect(() => {
setSelectedValues(defaultValue);
}, [defaultValue]);

const flatOptions = React.useMemo(() => {
return options.flatMap((option) =>
'options' in option ? option.options : [option],

+ 1
- 1
web/src/locales/en.ts 查看文件

@@ -471,7 +471,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
modelEnabledTools: 'Enabled tools',
modelEnabledToolsTip:
'Please select one or more tools for the chat model to use. It takes no effect for models not supporting tool call.',
freedom: 'Freedom',
freedom: 'Creativity',
improvise: 'Improvise',
precise: 'Precise',
balance: 'Balance',

+ 182
- 0
web/src/pages/next-search/search-setting-aisummery-config.tsx 查看文件

@@ -0,0 +1,182 @@
import { SliderInputSwitchFormField } from '@/components/llm-setting-items/slider';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
LlmModelType,
ModelVariableType,
settledModelVariableMap,
} from '@/constants/knowledge';
import { useTranslate } from '@/hooks/common-hooks';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
import { camelCase } from 'lodash';
import { useCallback } from 'react';
import { useFormContext } from 'react-hook-form';
import { z } from 'zod';

interface LlmSettingFieldItemsProps {
prefix?: string;
options?: any[];
}

export const LlmSettingSchema = {
llm_id: z.string(),
temperature: z.coerce.number(),
top_p: z.string(),
presence_penalty: z.coerce.number(),
frequency_penalty: z.coerce.number(),
temperatureEnabled: z.boolean(),
topPEnabled: z.boolean(),
presencePenaltyEnabled: z.boolean(),
frequencyPenaltyEnabled: z.boolean(),
maxTokensEnabled: z.boolean(),
};

export function LlmSettingFieldItems({
prefix,
options,
}: LlmSettingFieldItemsProps) {
const form = useFormContext();
const { t } = useTranslate('chat');

const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);

const handleChange = useCallback(
(parameter: string) => {
// const currentValues = { ...form.getValues() };
const values =
settledModelVariableMap[
parameter as keyof typeof settledModelVariableMap
];

// const nextValues = { ...currentValues, ...values };

for (const key in values) {
if (Object.prototype.hasOwnProperty.call(values, key)) {
const element = values[key];

form.setValue(`${prefix}.${key}`, element);
}
}
},
[form, prefix],
);

const parameterOptions = Object.values(ModelVariableType).map((x) => ({
label: t(camelCase(x)),
value: x,
}));

const getFieldWithPrefix = useCallback(
(name: string) => {
return prefix ? `${prefix}.${name}` : name;
},
[prefix],
);

return (
<div className="space-y-5">
<FormField
control={form.control}
name={getFieldWithPrefix('llm_id')}
render={({ field }) => (
<FormItem>
<FormLabel>
<span className="text-destructive mr-1"> *</span>
{t('model')}
</FormLabel>
<FormControl>
<SelectWithSearch
options={options || modelOptions}
triggerClassName="bg-bg-card"
{...field}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={getFieldWithPrefix('parameter')}
render={({ field }) => (
<FormItem className="flex justify-between gap-4 items-center">
<FormLabel>{t('freedom')}</FormLabel>
<FormControl>
<div className="w-28">
<Select
{...field}
onValueChange={(val) => {
handleChange(val);
field.onChange(val);
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{parameterOptions.map((x) => (
<SelectItem value={x.value} key={x.value}>
{x.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<SliderInputSwitchFormField
name={getFieldWithPrefix('temperature')}
checkName="temperatureEnabled"
label="temperature"
max={1}
step={0.01}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('top_p')}
checkName="topPEnabled"
label="topP"
max={1}
step={0.01}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('presence_penalty')}
checkName="presencePenaltyEnabled"
label="presencePenalty"
max={1}
step={0.01}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('frequency_penalty')}
checkName="frequencyPenaltyEnabled"
label="frequencyPenalty"
max={1}
step={0.01}
></SliderInputSwitchFormField>
{/* <SliderInputSwitchFormField
name={getFieldWithPrefix('max_tokens')}
checkName="maxTokensEnabled"
label="maxTokens"
max={128000}
></SliderInputSwitchFormField> */}
</div>
);
}

+ 240
- 150
web/src/pages/next-search/search-setting.tsx 查看文件

@@ -1,5 +1,6 @@
// src/pages/next-search/search-setting.tsx

import { Input } from '@/components/originui/input';
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Button } from '@/components/ui/button';
import { SingleFormSlider } from '@/components/ui/dual-range-slider';
@@ -11,29 +12,35 @@ import {
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 { RAGFlowSelect } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
import {
useComposeLlmOptionsByModelTypes,
useSelectLlmOptionsByModelType,
} from '@/hooks/llm-hooks';
import { useFetchTenantInfo } from '@/hooks/user-setting-hooks';
import { IKnowledge } from '@/interfaces/database/knowledge';
import { cn } from '@/lib/utils';
import { transformFile2Base64 } from '@/utils/file-util';
import { zodResolver } from '@hookform/resolvers/zod';
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';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
import { LlmModelType, ModelVariableType } from '../dataset/dataset/constant';
import {
ISearchAppDetailProps,
IUpdateSearchProps,
useUpdateSearch,
} from '../next-searches/hooks';
import { LlmSettingFieldItems } from './search-setting-aisummery-config';

interface SearchSettingProps {
open: boolean;
@@ -41,7 +48,52 @@ interface SearchSettingProps {
className?: string;
data: ISearchAppDetailProps;
}
const SearchSettingFormSchema = z
.object({
search_id: z.string().optional(),
name: z.string().min(1, 'Name is required'),
avatar: z.string().optional(),
description: z.string().optional(),
search_config: z.object({
kb_ids: z.array(z.string()).min(1, 'At least one dataset is required'),
vector_similarity_weight: z.number().min(0).max(100),
web_search: z.boolean(),
similarity_threshold: z.number(),
use_kg: z.boolean(),
rerank_id: z.string(),
use_rerank: z.boolean(),
top_k: z.number(),
summary: z.boolean(),
llm_setting: z.object({
llm_id: z.string(),
parameter: z.string(),
temperature: z.number(),
top_p: z.union([z.string(), z.number()]),
frequency_penalty: z.number(),
presence_penalty: z.number(),
}),
related_search: z.boolean(),
query_mindmap: z.boolean(),
}),
})
.superRefine((data, ctx) => {
if (data.search_config.use_rerank && !data.search_config.rerank_id) {
ctx.addIssue({
path: ['search_config', 'rerank_id'],
message: 'Rerank model is required when rerank is enabled',
code: z.ZodIssueCode.custom,
});
}

if (data.search_config.summary && !data.search_config.llm_setting?.llm_id) {
ctx.addIssue({
path: ['search_config', 'llm_setting', 'llm_id'],
message: 'Model is required when AI Summary is enabled',
code: z.ZodIssueCode.custom,
});
}
});
type SearchSettingFormData = z.infer<typeof SearchSettingFormSchema>;
const SearchSetting: React.FC<SearchSettingProps> = ({
open = false,
setOpen,
@@ -49,51 +101,56 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
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 { search_config } = data || {};
const { llm_setting } = search_config || {};
const formMethods = useForm<SearchSettingFormData>({
resolver: zodResolver(SearchSettingFormSchema),
});

const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64
const [datasetList, setDatasetList] = useState<MultiSelectOptionType[]>([]);
const [datasetSelectEmbdId, setDatasetSelectEmbdId] = useState('');

const resetForm = useCallback(() => {
formMethods.reset({
search_id: data?.id,
name: data?.name || '',
avatar: data?.avatar || '',
description: data?.description || 'You are an intelligent assistant.',
search_config: {
kb_ids: search_config?.kb_ids || [],
vector_similarity_weight: search_config?.vector_similarity_weight || 20,
web_search: search_config?.web_search || false,
doc_ids: [],
similarity_threshold: 0.0,
use_kg: false,
rerank_id: search_config?.rerank_id || '',
use_rerank: search_config?.rerank_id ? true : false,
top_k: search_config?.top_k || 1024,
summary: search_config?.summary || false,
chat_id: '',
llm_setting: {
llm_id: llm_setting?.llm_id || '',
parameter: llm_setting?.parameter || ModelVariableType.Improvise,
temperature: llm_setting?.temperature || 0.8,
top_p: llm_setting?.top_p || 0.9,
frequency_penalty: llm_setting?.frequency_penalty || 0.1,
presence_penalty: llm_setting?.presence_penalty || 0.1,
},
chat_settingcross_languages: [],
highlight: false,
keyword: false,
related_search: search_config?.related_search || false,
query_mindmap: search_config?.query_mindmap || false,
},
});
}, [data, search_config, llm_setting, formMethods]);

useEffect(() => {
resetForm();
}, [resetForm]);

useEffect(() => {
if (!open) {
setTimeout(() => {
@@ -116,7 +173,8 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
})();
}
}, [avatarFile]);
const { list: datasetListOrigin } = useFetchKnowledgeList();
const { list: datasetListOrigin, loading: datasetLoading } =
useFetchKnowledgeList();

useEffect(() => {
const datasetListMap = datasetListOrigin.map((item: IKnowledge) => {
@@ -143,8 +201,45 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
} else {
setDatasetSelectEmbdId('');
}
formMethods.setValue('search_config.kb_ids', value);
onChange?.(value);
};

const allOptions = useSelectLlmOptionsByModelType();
const rerankModelOptions = useMemo(() => {
return allOptions[LlmModelType.Rerank];
}, [allOptions]);

const aiSummeryModelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);

const rerankModelDisabled = useWatch({
control: formMethods.control,
name: 'search_config.use_rerank',
});
const aiSummaryDisabled = useWatch({
control: formMethods.control,
name: 'search_config.summary',
});

const { updateSearch, isLoading: isUpdating } = useUpdateSearch();
const { data: systemSetting } = useFetchTenantInfo();
const onSubmit = async (
formData: IUpdateSearchProps & { tenant_id: string },
) => {
try {
await updateSearch({
...formData,
tenant_id: systemSetting.tenant_id,
avatar: avatarBase64Str,
});
setOpen(false); // 关闭弹窗
} catch (error) {
console.error('Failed to update search:', error);
}
};
return (
<div
className={cn(
@@ -156,7 +251,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
width0,
className,
)}
style={{ height: 'calc(100dvh - 170px)' }}
style={{ maxHeight: 'calc(100dvh - 170px)' }}
>
<div className="flex justify-between items-center text-base mb-8">
<div className="text-text-primary">Search Settings</div>
@@ -168,19 +263,26 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
</div>
</div>
<div
style={{ height: 'calc(100dvh - 270px)' }}
style={{ maxHeight: 'calc(100dvh - 270px)' }}
className="overflow-y-auto scrollbar-auto p-1 text-text-secondary"
>
<Form {...formMethods}>
<form
onSubmit={formMethods.handleSubmit((data) => console.log(data))}
onSubmit={formMethods.handleSubmit(
(data) => {
console.log('Form submitted with data:', data);
onSubmit(data as IUpdateSearchProps);
},
(errors) => {
console.log('Validation errors:', errors);
},
)}
className="space-y-6"
>
{/* Name */}
<FormField
control={formMethods.control}
name="name"
rules={{ required: 'Name is required' }}
render={({ field }) => (
<FormItem>
<FormLabel>
@@ -225,13 +327,13 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
</div>
</div>
)}
<Input
<input
placeholder=""
// {...field}
type="file"
title=""
accept="image/*"
className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
className="absolute w-[64px] top-0 left-0 h-full opacity-0 cursor-pointer"
onChange={(ev) => {
const file = ev.target?.files?.[0];
if (
@@ -257,11 +359,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input
placeholder="Description"
{...field}
defaultValue="You are an intelligent assistant."
/>
<Input placeholder="Description" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -271,7 +369,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
{/* Datasets */}
<FormField
control={formMethods.control}
name="datasets"
name="search_config.kb_ids"
rules={{ required: 'Datasets is required' }}
render={({ field }) => (
<FormItem>
@@ -288,6 +386,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
placeholder={t('chat.knowledgeBasesMessage')}
variant="inverted"
maxCount={10}
defaultValue={field.value}
{...field}
/>
</FormControl>
@@ -299,10 +398,13 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
{/* Keyword Similarity Weight */}
<FormField
control={formMethods.control}
name="keywordSimilarityWeight"
name="search_config.vector_similarity_weight"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Keyword Similarity Weight</FormLabel>
<FormLabel>
<span className="text-destructive mr-1"> *</span>Keyword
Similarity Weight
</FormLabel>
<FormControl>
<div className="flex justify-between items-center">
<SingleFormSlider
@@ -324,7 +426,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
{/* Rerank Model */}
<FormField
control={formMethods.control}
name="rerankModel"
name="search_config.use_rerank"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
@@ -337,11 +439,60 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
</FormItem>
)}
/>
{rerankModelDisabled && (
<>
<FormField
control={formMethods.control}
name={'search_config.rerank_id'}
// rules={{ required: 'Model is required' }}
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>
<span className="text-destructive mr-1"> *</span>Model
</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={rerankModelOptions}
// disabled={disabled}
placeholder={'model'}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={formMethods.control}
name="search_config.top_k"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Top K</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>
)}
/>
</>
)}

{/* AI Summary */}
<FormField
control={formMethods.control}
name="aiSummary"
name="search_config.summary"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
@@ -351,93 +502,21 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
/>
</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>
)}
/>
{aiSummaryDisabled && (
<LlmSettingFieldItems
prefix="search_config.llm_setting"
options={aiSummeryModelOptions}
></LlmSettingFieldItems>
)}

{/* Feature Controls */}
<FormField
control={formMethods.control}
name="enableWebSearch"
name="search_config.web_search"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
@@ -453,7 +532,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({

<FormField
control={formMethods.control}
name="enableRelatedSearch"
name="search_config.related_search"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
@@ -469,7 +548,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({

<FormField
control={formMethods.control}
name="showQueryMindmap"
name="search_config.query_mindmap"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
@@ -483,7 +562,18 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
)}
/>
{/* Submit Button */}
<div className="flex justify-end">
<div className="flex justify-end"></div>
<div className="flex justify-end gap-2">
<Button
type="reset"
variant={'transparent'}
onClick={() => {
resetForm();
setOpen(false);
}}
>
Cancel
</Button>
<Button type="submit">Confirm</Button>
</div>
</form>

+ 51
- 0
web/src/pages/next-searches/hooks.ts 查看文件

@@ -158,6 +158,15 @@ export const useDeleteSearch = () => {
return { data, isLoading, isError, deleteSearch };
};

interface IllmSettingProps {
llm_id: string;
parameter: string;
temperature: number;
top_p: number;
frequency_penalty: number;
presence_penalty: number;
}

export interface ISearchAppDetailProps {
avatar: any;
created_by: string;
@@ -175,10 +184,12 @@ export interface ISearchAppDetailProps {
rerank_id: string;
similarity_threshold: number;
summary: boolean;
llm_setting: IllmSettingProps;
top_k: number;
use_kg: boolean;
vector_similarity_weight: number;
web_search: boolean;
chat_settingcross_languages: string[];
};
tenant_id: string;
update_time: number;
@@ -207,3 +218,43 @@ export const useFetchSearchDetail = () => {

return { data: data?.data, isLoading, isError };
};

export type IUpdateSearchProps = Omit<ISearchAppDetailProps, 'id'> & {
search_id: string;
};

export const useUpdateSearch = () => {
const { t } = useTranslation();

const {
data,
isLoading,
isError,
mutateAsync: updateSearchMutation,
} = useMutation<any, Error, IUpdateSearchProps>({
mutationKey: ['updateSearch'],
mutationFn: async (formData) => {
const { data: response } =
await searchService.updateSearchSetting(formData);
if (response.code !== 0) {
throw new Error(response.message || 'Failed to update search');
}
return response.data;
},
onSuccess: () => {
message.success(t('message.updated'));
},
onError: (error) => {
message.error(t('message.error', { error: error.message }));
},
});

const updateSearch = useCallback(
(formData: IUpdateSearchProps) => {
return updateSearchMutation(formData);
},
[updateSearchMutation],
);

return { data, isLoading, isError, updateSearch };
};

+ 11
- 1
web/src/services/search-service.ts 查看文件

@@ -2,7 +2,13 @@ import api from '@/utils/api';
import registerServer from '@/utils/register-server';
import request from '@/utils/request';

const { createSearch, getSearchList, deleteSearch, getSearchDetail } = api;
const {
createSearch,
getSearchList,
deleteSearch,
getSearchDetail,
updateSearchSetting,
} = api;
const methods = {
createSearch: {
url: createSearch,
@@ -17,6 +23,10 @@ const methods = {
url: getSearchDetail,
method: 'get',
},
updateSearchSetting: {
url: updateSearchSetting,
method: 'post',
},
} as const;
const searchService = registerServer<keyof typeof methods>(methods, request);


+ 1
- 0
web/src/utils/api.ts 查看文件

@@ -181,4 +181,5 @@ export default {
getSearchList: `${api_host}/search/list`,
deleteSearch: `${api_host}/search/rm`,
getSearchDetail: `${api_host}/search/detail`,
updateSearchSetting: `${api_host}/search/update`,
};

正在加载...
取消
保存