diff --git a/web/src/components/ui/multi-select.tsx b/web/src/components/ui/multi-select.tsx
index f3d5ba269..3433c21b0 100644
--- a/web/src/components/ui/multi-select.tsx
+++ b/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],
diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts
index 862b19522..a2b069b96 100644
--- a/web/src/locales/en.ts
+++ b/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',
diff --git a/web/src/pages/next-search/search-setting-aisummery-config.tsx b/web/src/pages/next-search/search-setting-aisummery-config.tsx
new file mode 100644
index 000000000..8bfcaa533
--- /dev/null
+++ b/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 (
+
+
(
+
+
+ *
+ {t('model')}
+
+
+
+
+
+
+ )}
+ />
+ (
+
+ {t('freedom')}
+
+
+
+
+
+
+
+ )}
+ />
+
+
+
+
+ {/* */}
+
+ );
+}
diff --git a/web/src/pages/next-search/search-setting.tsx b/web/src/pages/next-search/search-setting.tsx
index 739374596..b8f5793e5 100644
--- a/web/src/pages/next-search/search-setting.tsx
+++ b/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;
const SearchSetting: React.FC = ({
open = false,
setOpen,
@@ -49,51 +101,56 @@ const SearchSetting: React.FC = ({
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({
+ resolver: zodResolver(SearchSettingFormSchema),
});
+
const [avatarFile, setAvatarFile] = useState(null);
const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64
const [datasetList, setDatasetList] = useState([]);
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 = ({
})();
}
}, [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 = ({
} 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 (
= ({
width0,
className,
)}
- style={{ height: 'calc(100dvh - 170px)' }}
+ style={{ maxHeight: 'calc(100dvh - 170px)' }}
>
Search Settings
@@ -168,19 +263,26 @@ const SearchSetting: React.FC
= ({
)}
- {
const file = ev.target?.files?.[0];
if (
@@ -257,11 +359,7 @@ const SearchSetting: React.FC = ({
Description
-
+
@@ -271,7 +369,7 @@ const SearchSetting: React.FC = ({
{/* Datasets */}
(
@@ -288,6 +386,7 @@ const SearchSetting: React.FC = ({
placeholder={t('chat.knowledgeBasesMessage')}
variant="inverted"
maxCount={10}
+ defaultValue={field.value}
{...field}
/>
@@ -299,10 +398,13 @@ const SearchSetting: React.FC = ({
{/* Keyword Similarity Weight */}
(
- Keyword Similarity Weight
+
+ *Keyword
+ Similarity Weight
+
= ({
{/* Rerank Model */}
(
@@ -337,11 +439,60 @@ const SearchSetting: React.FC = ({
)}
/>
+ {rerankModelDisabled && (
+ <>
+ (
+
+
+ *Model
+
+
+
+
+
+
+ )}
+ />
+
+ (
+
+ Top K
+
+
+ field.onChange(values)}
+ >
+
+
+
+
+
+ )}
+ />
+ >
+ )}
{/* AI Summary */}
(
@@ -351,93 +502,21 @@ const SearchSetting: React.FC = ({
/>
AI Summary
-
)}
/>
- {/* Top K */}
- (
-
-
-
-
- Top K
-
- )}
- />
-
- {/* Search Method */}
- (
-
-
- *Search
- Method
-
-
-
-
-
-
- )}
- />
-
- {/* Model */}
- (
-
-
- *Model
-
-
-
-
-
-
- )}
- />
+ {aiSummaryDisabled && (
+
+ )}
{/* Feature Controls */}
(
@@ -453,7 +532,7 @@ const SearchSetting: React.FC = ({
(
@@ -469,7 +548,7 @@ const SearchSetting: React.FC = ({
(
@@ -483,7 +562,18 @@ const SearchSetting: React.FC = ({
)}
/>
{/* Submit Button */}
-
+
+
+
diff --git a/web/src/pages/next-searches/hooks.ts b/web/src/pages/next-searches/hooks.ts
index 66fdc4f50..98eff23aa 100644
--- a/web/src/pages/next-searches/hooks.ts
+++ b/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
& {
+ search_id: string;
+};
+
+export const useUpdateSearch = () => {
+ const { t } = useTranslation();
+
+ const {
+ data,
+ isLoading,
+ isError,
+ mutateAsync: updateSearchMutation,
+ } = useMutation({
+ 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 };
+};
diff --git a/web/src/services/search-service.ts b/web/src/services/search-service.ts
index 51b04300b..21af31d31 100644
--- a/web/src/services/search-service.ts
+++ b/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(methods, request);
diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts
index 1cf73997b..110f64969 100644
--- a/web/src/utils/api.ts
+++ b/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`,
};