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 = ({
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 */} ( @@ -225,13 +327,13 @@ 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`, };