diff --git a/web/src/components/dynamic-form.tsx b/web/src/components/dynamic-form.tsx index 7bd4083fb..6ef736a40 100644 --- a/web/src/components/dynamic-form.tsx +++ b/web/src/components/dynamic-form.tsx @@ -187,20 +187,23 @@ export const generateSchema = (fields: FormFieldConfig[]): ZodSchema => { // Handle required fields if (field.required) { + const requiredMessage = + field.validation?.message || `${field.label} is required`; + if (field.type === FormFieldType.Checkbox) { fieldSchema = (fieldSchema as z.ZodBoolean).refine( (val) => val === true, { - message: `${field.label} is required`, + message: requiredMessage, }, ); } else if (field.type === FormFieldType.Tag) { fieldSchema = (fieldSchema as z.ZodArray).min(1, { - message: `${field.label} is required`, + message: requiredMessage, }); } else { fieldSchema = (fieldSchema as z.ZodString).min(1, { - message: `${field.label} is required`, + message: requiredMessage, }); } } diff --git a/web/src/pages/user-setting/components/setting-title/index.tsx b/web/src/pages/user-setting/components/setting-title/index.tsx deleted file mode 100644 index 3fd4f424a..000000000 --- a/web/src/pages/user-setting/components/setting-title/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { SettingOutlined } from '@ant-design/icons'; -import { Button, Flex, Typography } from 'antd'; - -const { Title, Paragraph } = Typography; - -interface IProps { - title: string; - description: string; - showRightButton?: boolean; - clickButton?: () => void; -} - -const SettingTitle = ({ - title, - description, - clickButton, - showRightButton = false, -}: IProps) => { - const { t } = useTranslate('setting'); - - return ( - -
- {title} - {description} -
- {showRightButton && ( - - )} -
- ); -}; - -export default SettingTitle; diff --git a/web/src/pages/user-setting/setting-locale/translation-table.tsx b/web/src/pages/user-setting/setting-locale/translation-table.tsx index b064b699b..923855b6a 100644 --- a/web/src/pages/user-setting/setting-locale/translation-table.tsx +++ b/web/src/pages/user-setting/setting-locale/translation-table.tsx @@ -1,6 +1,14 @@ -import { Table } from 'antd'; -import type { ColumnsType } from 'antd/es/table'; -import React from 'react'; +import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react'; +import { useMemo, useState } from 'react'; type TranslationTableRow = { key: string; @@ -12,56 +20,218 @@ interface TranslationTableProps { languages: string[]; } +type FilterType = 'all' | 'show_empty' | 'show_non_empty'; +type SortOrder = 'asc' | 'desc' | null; + +interface ColumnState { + key: string; + sortOrder: SortOrder; + filter: FilterType; +} + const TranslationTable: React.FC = ({ data, languages, }) => { - // Define columns dynamically based on languages - const columns: ColumnsType = [ - { - title: 'Key', - dataIndex: 'key', - key: 'key', - fixed: 'left', - width: 200, - sorter: (a, b) => a.key.localeCompare(b.key), // Sorting by key - }, - ...languages.map((lang) => ({ - title: lang, - dataIndex: lang, - key: lang, - sorter: (a: any, b: any) => a[lang].localeCompare(b[lang]), // Sorting by language - // Example filter for each language - filters: [ - { - text: 'Show Empty', - value: 'show_empty', - }, - { - text: 'Show Non-Empty', - value: 'show_non_empty', - }, - ], - onFilter: (value: any, record: any) => { - if (value === 'show_empty') { - return !record[lang]; // Show rows with empty translations + const [columnStates, setColumnStates] = useState( + [{ key: 'key', sortOrder: null, filter: 'all' as FilterType }].concat( + languages.map((lang) => ({ + key: lang, + sortOrder: null, + filter: 'all' as FilterType, + })), + ), + ); + + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + // Get the active sort column + const activeSortColumn = useMemo(() => { + return columnStates.find((col) => col.sortOrder !== null); + }, [columnStates]); + + // Apply sorting and filtering + const processedData = useMemo(() => { + let filtered = [...data]; + + // Apply filters for all columns + columnStates.forEach((colState) => { + if (colState.filter !== 'all') { + filtered = filtered.filter((record) => { + const value = record[colState.key]; + if (colState.filter === 'show_empty') { + return !value || value.length === 0; + } + if (colState.filter === 'show_non_empty') { + return value && value.length > 0; + } + return true; + }); + } + }); + + // Apply sorting + if (activeSortColumn && activeSortColumn.sortOrder) { + filtered.sort((a, b) => { + const aValue = a[activeSortColumn.key] || ''; + const bValue = b[activeSortColumn.key] || ''; + const comparison = String(aValue).localeCompare(String(bValue)); + return activeSortColumn.sortOrder === 'asc' ? comparison : -comparison; + }); + } + + return filtered; + }, [data, columnStates, activeSortColumn]); + + // Apply pagination + const paginatedData = useMemo(() => { + const start = (currentPage - 1) * pageSize; + const end = start + pageSize; + return processedData.slice(start, end); + }, [processedData, currentPage, pageSize]); + + const handleSort = (columnKey: string) => { + setColumnStates((prev) => + prev.map((col) => { + if (col.key === columnKey) { + let newOrder: SortOrder = 'asc'; + if (col.sortOrder === 'asc') { + newOrder = 'desc'; + } else if (col.sortOrder === 'desc') { + newOrder = null; + } + return { ...col, sortOrder: newOrder }; } - if (value === 'show_non_empty') { - return record[lang] && record[lang].length > 0; // Show rows with non-empty translations - } - return true; - }, - })), - ]; + return { ...col, sortOrder: null }; + }), + ); + }; + + const handleFilter = (columnKey: string, filter: FilterType) => { + setColumnStates((prev) => + prev.map((col) => (col.key === columnKey ? { ...col, filter } : col)), + ); + setCurrentPage(1); + }; + + const renderSortIcon = (columnKey: string) => { + const colState = columnStates.find((col) => col.key === columnKey); + const sortOrder = colState?.sortOrder; + + if (sortOrder === 'asc') { + return ; + } else if (sortOrder === 'desc') { + return ; + } else { + return ; + } + }; + + const handlePageChange = (page: number, size: number) => { + setCurrentPage(page); + setPageSize(size); + }; return ( - +
+
+
+ + + handleSort('key')} + > +
+ Key + {renderSortIcon('key')} +
+
+ {languages.map((lang) => { + const colState = columnStates.find((col) => col.key === lang)!; + return ( + +
+
handleSort(lang)} + > + {lang} + {renderSortIcon(lang)} +
+
+ + + +
+
+
+ ); + })} +
+
+ + {paginatedData.length > 0 ? ( + paginatedData.map((record) => ( + + + {record.key} + + {languages.map((lang) => ( + + {record[lang] || ''} + + ))} + + )) + ) : ( + + + No data + + + )} + +
+ + + ); }; diff --git a/web/src/pages/user-setting/setting-model/modal/azure-openai-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/azure-openai-modal/index.tsx index 1bc44695f..f08770e07 100644 --- a/web/src/pages/user-setting/setting-model/modal/azure-openai-modal/index.tsx +++ b/web/src/pages/user-setting/setting-model/modal/azure-openai-modal/index.tsx @@ -1,17 +1,15 @@ -import { useTranslate } from '@/hooks/common-hooks'; +import { + DynamicForm, + FormFieldConfig, + FormFieldType, +} from '@/components/dynamic-form'; +import { Modal } from '@/components/ui/modal/modal'; +import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks'; import { IModalProps } from '@/interfaces/common'; import { IAddLlmRequestBody } from '@/interfaces/request/llm'; -import { Form, Input, InputNumber, Modal, Select, Switch } from 'antd'; -import omit from 'lodash/omit'; +import { FieldValues } from 'react-hook-form'; import { LLMHeader } from '../../components/llm-header'; -type FieldType = IAddLlmRequestBody & { - api_version: string; - vision: boolean; -}; - -const { Option } = Select; - const AzureOpenAIModal = ({ visible, hideModal, @@ -19,150 +17,143 @@ const AzureOpenAIModal = ({ loading, llmFactory, }: IModalProps & { llmFactory: string }) => { - const [form] = Form.useForm(); - const { t } = useTranslate('setting'); + const { t: tg } = useCommonTranslation(); + + const fields: FormFieldConfig[] = [ + { + name: 'model_type', + label: t('modelType'), + type: FormFieldType.Select, + required: true, + options: [ + { label: 'chat', value: 'chat' }, + { label: 'embedding', value: 'embedding' }, + { label: 'image2text', value: 'image2text' }, + ], + defaultValue: 'embedding', + validation: { + message: t('modelTypeMessage'), + }, + }, + { + name: 'api_base', + label: t('addLlmBaseUrl'), + type: FormFieldType.Text, + required: true, + placeholder: t('baseUrlNameMessage'), + validation: { + message: t('baseUrlNameMessage'), + }, + }, + { + name: 'api_key', + label: t('apiKey'), + type: FormFieldType.Text, + required: false, + placeholder: t('apiKeyMessage'), + }, + { + name: 'llm_name', + label: t('modelName'), + type: FormFieldType.Text, + required: true, + placeholder: t('modelNameMessage'), + defaultValue: 'gpt-3.5-turbo', + validation: { + message: t('modelNameMessage'), + }, + }, + { + name: 'api_version', + label: t('apiVersion'), + type: FormFieldType.Text, + required: false, + placeholder: t('apiVersionMessage'), + defaultValue: '2024-02-01', + }, + { + name: 'max_tokens', + label: t('maxTokens'), + type: FormFieldType.Number, + required: true, + placeholder: t('maxTokensTip'), + validation: { + min: 0, + message: t('maxTokensMessage'), + }, + }, + { + name: 'vision', + label: t('vision'), + type: FormFieldType.Switch, + defaultValue: false, + dependencies: ['model_type'], + shouldRender: (formValues: any) => { + return formValues?.model_type === 'chat'; + }, + }, + ]; + + const handleOk = async (values?: FieldValues) => { + if (!values) return; - const handleOk = async () => { - const values = await form.validateFields(); const modelType = values.model_type === 'chat' && values.vision ? 'image2text' : values.model_type; - const data = { - ...omit(values, ['vision']), - model_type: modelType, + const data: IAddLlmRequestBody & { api_version?: string } = { llm_factory: llmFactory, - max_tokens: values.max_tokens, + llm_name: values.llm_name as string, + model_type: modelType, + api_base: values.api_base as string, + api_key: values.api_key as string | undefined, + max_tokens: values.max_tokens as number, + api_version: values.api_version as string, }; - console.info(data); - onOk?.(data); - }; - const optionsMap = { - Default: [ - { value: 'chat', label: 'chat' }, - { value: 'embedding', label: 'embedding' }, - { value: 'image2text', label: 'image2text' }, - ], - }; - const getOptions = () => { - return optionsMap.Default; - }; - const handleKeyDown = async (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - await handleOk(); - } + await onOk?.(data); }; return ( } - open={visible} - onOk={handleOk} - onCancel={hideModal} - okButtonProps={{ loading }} + open={visible || false} + onOpenChange={(open) => !open && hideModal?.()} + maskClosable={false} + footer={
} > -
{ + console.log(data); + }} + defaultValues={ + { + model_type: 'embedding', + llm_name: 'gpt-3.5-turbo', + api_version: '2024-02-01', + vision: false, + } as FieldValues + } + labelClassName="font-normal" > - - label={t('modelType')} - name="model_type" - initialValue={'embedding'} - rules={[{ required: true, message: t('modelTypeMessage') }]} - > - - - - label={t('addLlmBaseUrl')} - name="api_base" - rules={[{ required: true, message: t('baseUrlNameMessage') }]} - > - + { + hideModal?.(); + }} /> - - - label={t('apiKey')} - name="api_key" - rules={[{ required: false, message: t('apiKeyMessage') }]} - > - - - - label={t('modelName')} - name="llm_name" - initialValue="gpt-3.5-turbo" - rules={[{ required: true, message: t('modelNameMessage') }]} - > - { + handleOk(values); + }} /> - - - label={t('apiVersion')} - name="api_version" - initialValue="2024-02-01" - rules={[{ required: false, message: t('apiVersionMessage') }]} - > - - - - label={t('maxTokens')} - name="max_tokens" - rules={[ - { required: true, message: t('maxTokensMessage') }, - { - type: 'number', - message: t('maxTokensInvalidMessage'), - }, - ({}) => ({ - validator(_, value) { - if (value < 0) { - return Promise.reject(new Error(t('maxTokensMinMessage'))); - } - return Promise.resolve(); - }, - }), - ]} - > - - - - - {({ getFieldValue }) => - getFieldValue('model_type') === 'chat' && ( - - - - ) - } - - + +
); };