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
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<z.ZodString>).min(1, {
message: `${field.label} is required`,
message: requiredMessage,
});
} else {
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 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<TranslationTableProps> = ({
data,
languages,
}) => {
// Define columns dynamically based on languages
const columns: ColumnsType<TranslationTableRow> = [
{
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<ColumnState[]>(
[{ 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 <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 (
<Table
columns={columns}
dataSource={data}
rowKey="key"
pagination={{ pageSize: 10 }}
scroll={{ x: true }}
/>
<div className="flex flex-col gap-4">
<div className="rounded-lg bg-bg-input scrollbar-auto overflow-hidden border border-border-default">
<Table rootClassName="rounded-lg">
<TableHeader className="bg-bg-title">
<TableRow className="hover:bg-bg-title">
<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 { 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<IAddLlmRequestBody> & { llmFactory: string }) => {
const [form] = Form.useForm<FieldType>();
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 (
<Modal
title={<LLMHeader name={llmFactory} />}
open={visible}
onOk={handleOk}
onCancel={hideModal}
okButtonProps={{ loading }}
open={visible || false}
onOpenChange={(open) => !open && hideModal?.()}
maskClosable={false}
footer={<div className="p-4"></div>}
>
<Form
name="basic"
style={{ maxWidth: 600 }}
autoComplete="off"
layout={'vertical'}
form={form}
<DynamicForm.Root
fields={fields}
onSubmit={(data) => {
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"
>
<Form.Item<FieldType>
label={t('modelType')}
name="model_type"
initialValue={'embedding'}
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}
<div className="absolute bottom-0 right-0 left-0 flex items-center justify-end w-full gap-2 py-6 px-6">
<DynamicForm.CancelButton
handleCancel={() => {
hideModal?.();
}}
/>
</Form.Item>
<Form.Item<FieldType>
label={t('apiKey')}
name="api_key"
rules={[{ required: false, message: t('apiKeyMessage') }]}
>
<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}
<DynamicForm.SavingButton
submitLoading={loading || false}
buttonText={tg('ok')}
submitFunc={(values: FieldValues) => {
handleOk(values);
}}
/>
</Form.Item>
<Form.Item<FieldType>
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>
</div>
</DynamicForm.Root>
</Modal>
);
};