Refactor: Refactoring AzureOpenAIModal using shadcn. #10427 (#12436)

### What problem does this PR solve?

Refactor: Refactoring AzureOpenAIModal using shadcn. #10427

### Type of change

- [x] Refactoring
This commit is contained in:
balibabu
2026-01-05 14:09:55 +08:00
committed by GitHub
parent 42461bc378
commit 4e9407b4ae
4 changed files with 348 additions and 224 deletions

View File

@ -187,20 +187,23 @@ export const generateSchema = (fields: FormFieldConfig[]): ZodSchema<any> => {
// Handle required fields // Handle required fields
if (field.required) { if (field.required) {
const requiredMessage =
field.validation?.message || `${field.label} is required`;
if (field.type === FormFieldType.Checkbox) { if (field.type === FormFieldType.Checkbox) {
fieldSchema = (fieldSchema as z.ZodBoolean).refine( fieldSchema = (fieldSchema as z.ZodBoolean).refine(
(val) => val === true, (val) => val === true,
{ {
message: `${field.label} is required`, message: requiredMessage,
}, },
); );
} else if (field.type === FormFieldType.Tag) { } else if (field.type === FormFieldType.Tag) {
fieldSchema = (fieldSchema as z.ZodArray<z.ZodString>).min(1, { fieldSchema = (fieldSchema as z.ZodArray<z.ZodString>).min(1, {
message: `${field.label} is required`, message: requiredMessage,
}); });
} else { } else {
fieldSchema = (fieldSchema as z.ZodString).min(1, { fieldSchema = (fieldSchema as z.ZodString).min(1, {
message: `${field.label} is required`, message: requiredMessage,
}); });
} }
} }

View File

@ -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 (
<Flex align="center" justify={'space-between'}>
<div>
<Title level={5}>{title}</Title>
<Paragraph>{description}</Paragraph>
</div>
{showRightButton && (
<Button type={'primary'} onClick={clickButton}>
<Flex align="center" gap={4}>
<SettingOutlined />
{t('systemModelSettings')}
</Flex>
</Button>
)}
</Flex>
);
};
export default SettingTitle;

View File

@ -1,6 +1,14 @@
import { Table } from 'antd'; import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import type { ColumnsType } from 'antd/es/table'; import {
import React from 'react'; Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react';
import { useMemo, useState } from 'react';
type TranslationTableRow = { type TranslationTableRow = {
key: string; key: string;
@ -12,56 +20,218 @@ interface TranslationTableProps {
languages: string[]; 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<TranslationTableProps> = ({ const TranslationTable: React.FC<TranslationTableProps> = ({
data, data,
languages, languages,
}) => { }) => {
// Define columns dynamically based on languages const [columnStates, setColumnStates] = useState<ColumnState[]>(
const columns: ColumnsType<TranslationTableRow> = [ [{ key: 'key', sortOrder: null, filter: 'all' as FilterType }].concat(
{ languages.map((lang) => ({
title: 'Key', key: lang,
dataIndex: 'key', sortOrder: null,
key: 'key', filter: 'all' as FilterType,
fixed: 'left', })),
width: 200, ),
sorter: (a, b) => a.key.localeCompare(b.key), // Sorting by key );
},
...languages.map((lang) => ({ const [currentPage, setCurrentPage] = useState(1);
title: lang, const [pageSize, setPageSize] = useState(10);
dataIndex: lang,
key: lang, // Get the active sort column
sorter: (a: any, b: any) => a[lang].localeCompare(b[lang]), // Sorting by language const activeSortColumn = useMemo(() => {
// Example filter for each language return columnStates.find((col) => col.sortOrder !== null);
filters: [ }, [columnStates]);
{
text: 'Show Empty', // Apply sorting and filtering
value: 'show_empty', const processedData = useMemo(() => {
}, let filtered = [...data];
{
text: 'Show Non-Empty', // Apply filters for all columns
value: 'show_non_empty', columnStates.forEach((colState) => {
}, if (colState.filter !== 'all') {
], filtered = filtered.filter((record) => {
onFilter: (value: any, record: any) => { const value = record[colState.key];
if (value === 'show_empty') { if (colState.filter === 'show_empty') {
return !record[lang]; // Show rows with empty translations 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 { ...col, sortOrder: null };
return record[lang] && record[lang].length > 0; // Show rows with non-empty translations }),
} );
return true; };
},
})), 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 <ArrowUp className="ml-1 h-4 w-4" />;
} else if (sortOrder === 'desc') {
return <ArrowDown className="ml-1 h-4 w-4" />;
} else {
return <ArrowUpDown className="ml-1 h-4 w-4" />;
}
};
const handlePageChange = (page: number, size: number) => {
setCurrentPage(page);
setPageSize(size);
};
return ( return (
<Table <div className="flex flex-col gap-4">
columns={columns} <div className="rounded-lg bg-bg-input scrollbar-auto overflow-hidden border border-border-default">
dataSource={data} <Table rootClassName="rounded-lg">
rowKey="key" <TableHeader className="bg-bg-title">
pagination={{ pageSize: 10 }} <TableRow className="hover:bg-bg-title">
scroll={{ x: true }} <TableHead
/> className="h-12 px-4 cursor-pointer sticky left-0 bg-bg-title"
onClick={() => handleSort('key')}
>
<div className="flex items-center min-w-[200px]">
Key
{renderSortIcon('key')}
</div>
</TableHead>
{languages.map((lang) => {
const colState = columnStates.find((col) => col.key === lang)!;
return (
<TableHead key={lang} className="h-12 px-4">
<div className="flex flex-col gap-2">
<div
className="flex items-center cursor-pointer"
onClick={() => handleSort(lang)}
>
{lang}
{renderSortIcon(lang)}
</div>
<div className="flex gap-1">
<button
className={`text-xs px-2 py-0.5 rounded ${
colState.filter === 'show_empty'
? 'bg-bg-card text-text-primary'
: 'bg-bg-title text-text-secondary hover:bg-bg-card'
}`}
onClick={() => handleFilter(lang, 'show_empty')}
>
Empty
</button>
<button
className={`text-xs px-2 py-0.5 rounded ${
colState.filter === 'show_non_empty'
? 'bg-bg-card text-text-primary'
: 'bg-bg-title text-text-secondary hover:bg-bg-card'
}`}
onClick={() => handleFilter(lang, 'show_non_empty')}
>
Non-Empty
</button>
<button
className={`text-xs px-2 py-0.5 rounded ${
colState.filter === 'all'
? 'bg-bg-card text-text-primary'
: 'bg-bg-title text-text-secondary hover:bg-bg-card'
}`}
onClick={() => handleFilter(lang, 'all')}
>
All
</button>
</div>
</div>
</TableHead>
);
})}
</TableRow>
</TableHeader>
<TableBody className="bg-bg-base">
{paginatedData.length > 0 ? (
paginatedData.map((record) => (
<TableRow key={record.key} className="hover:bg-bg-card">
<TableCell className="p-4 font-medium sticky left-0 bg-bg-base hover:bg-bg-card">
{record.key}
</TableCell>
{languages.map((lang) => (
<TableCell key={lang} className="p-4">
{record[lang] || ''}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={languages.length + 1}
className="h-24 text-center"
>
No data
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<RAGFlowPagination
current={currentPage}
pageSize={pageSize}
total={processedData.length}
onChange={handlePageChange}
/>
</div>
); );
}; };

View File

@ -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 { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm'; import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Form, Input, InputNumber, Modal, Select, Switch } from 'antd'; import { FieldValues } from 'react-hook-form';
import omit from 'lodash/omit';
import { LLMHeader } from '../../components/llm-header'; import { LLMHeader } from '../../components/llm-header';
type FieldType = IAddLlmRequestBody & {
api_version: string;
vision: boolean;
};
const { Option } = Select;
const AzureOpenAIModal = ({ const AzureOpenAIModal = ({
visible, visible,
hideModal, hideModal,
@ -19,150 +17,143 @@ const AzureOpenAIModal = ({
loading, loading,
llmFactory, llmFactory,
}: IModalProps<IAddLlmRequestBody> & { llmFactory: string }) => { }: IModalProps<IAddLlmRequestBody> & { llmFactory: string }) => {
const [form] = Form.useForm<FieldType>();
const { t } = useTranslate('setting'); 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 = const modelType =
values.model_type === 'chat' && values.vision values.model_type === 'chat' && values.vision
? 'image2text' ? 'image2text'
: values.model_type; : values.model_type;
const data = { const data: IAddLlmRequestBody & { api_version?: string } = {
...omit(values, ['vision']),
model_type: modelType,
llm_factory: llmFactory, 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); await 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();
}
}; };
return ( return (
<Modal <Modal
title={<LLMHeader name={llmFactory} />} title={<LLMHeader name={llmFactory} />}
open={visible} open={visible || false}
onOk={handleOk} onOpenChange={(open) => !open && hideModal?.()}
onCancel={hideModal} maskClosable={false}
okButtonProps={{ loading }} footer={<div className="p-4"></div>}
> >
<Form <DynamicForm.Root
name="basic" fields={fields}
style={{ maxWidth: 600 }} onSubmit={(data) => {
autoComplete="off" console.log(data);
layout={'vertical'} }}
form={form} defaultValues={
{
model_type: 'embedding',
llm_name: 'gpt-3.5-turbo',
api_version: '2024-02-01',
vision: false,
} as FieldValues
}
labelClassName="font-normal"
> >
<Form.Item<FieldType> <div className="absolute bottom-0 right-0 left-0 flex items-center justify-end w-full gap-2 py-6 px-6">
label={t('modelType')} <DynamicForm.CancelButton
name="model_type" handleCancel={() => {
initialValue={'embedding'} hideModal?.();
rules={[{ required: true, message: t('modelTypeMessage') }]} }}
>
<Select placeholder={t('modelTypeMessage')}>
{getOptions(llmFactory).map((option) => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item<FieldType>
label={t('addLlmBaseUrl')}
name="api_base"
rules={[{ required: true, message: t('baseUrlNameMessage') }]}
>
<Input
placeholder={t('baseUrlNameMessage')}
onKeyDown={handleKeyDown}
/> />
</Form.Item> <DynamicForm.SavingButton
<Form.Item<FieldType> submitLoading={loading || false}
label={t('apiKey')} buttonText={tg('ok')}
name="api_key" submitFunc={(values: FieldValues) => {
rules={[{ required: false, message: t('apiKeyMessage') }]} handleOk(values);
> }}
<Input placeholder={t('apiKeyMessage')} onKeyDown={handleKeyDown} />
</Form.Item>
<Form.Item<FieldType>
label={t('modelName')}
name="llm_name"
initialValue="gpt-3.5-turbo"
rules={[{ required: true, message: t('modelNameMessage') }]}
>
<Input
placeholder={t('modelNameMessage')}
onKeyDown={handleKeyDown}
/> />
</Form.Item> </div>
<Form.Item<FieldType> </DynamicForm.Root>
label={t('apiVersion')}
name="api_version"
initialValue="2024-02-01"
rules={[{ required: false, message: t('apiVersionMessage') }]}
>
<Input
placeholder={t('apiVersionMessage')}
onKeyDown={handleKeyDown}
/>
</Form.Item>
<Form.Item<FieldType>
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();
},
}),
]}
>
<InputNumber
placeholder={t('maxTokensTip')}
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item noStyle dependencies={['model_type']}>
{({ getFieldValue }) =>
getFieldValue('model_type') === 'chat' && (
<Form.Item
label={t('vision')}
valuePropName="checked"
name={'vision'}
>
<Switch />
</Form.Item>
)
}
</Form.Item>
</Form>
</Modal> </Modal>
); );
}; };