refactor: replace antd with shadcn/ui

This commit is contained in:
Junyan Qin
2025-05-07 18:06:44 +08:00
parent f5cee8b6b5
commit d2fb0dd749
28 changed files with 2907 additions and 225 deletions
@@ -19,6 +19,7 @@
flex-direction: row;
gap: 0.8rem;
user-select: none;
/* background-color: aqua; */
}
.iconImage {
@@ -31,7 +32,7 @@
.basicInfoContainer {
display: flex;
flex-direction: column;
gap: 0.6rem;
gap: 0.2rem;
}
.basicInfoText {
@@ -1,5 +1,6 @@
import styles from './LLMCard.module.css';
import { LLMCardVO } from '@/app/home/models/component/llm-card/LLMCardVO';
import { Button } from '@/components/ui/button';
function checkAbilityBadges(abilities: string[]) {
@@ -1,12 +1,69 @@
import styles from '@/app/home/models/LLMConfig.module.css';
import { Button, Form, Input, Select, SelectProps, Space, Modal } from 'antd';
import { SelectProps } from 'antd';
import { ICreateLLMField } from '@/app/home/models/ICreateLLMField';
import { useEffect, useState } from 'react';
import { IChooseRequesterEntity } from '@/app/home/models/component/llm-form/ChooseAdapterEntity';
import { IChooseRequesterEntity } from '@/app/home/models/component/llm-form/ChooseRequesterEntity';
import { httpClient } from '@/app/infra/http/HttpClient';
import { LLMModel } from '@/app/infra/api/api-types';
import { UUID } from 'uuidjs';
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Checkbox } from "@/components/ui/checkbox"
const extraArgSchema = z.object({
key: z.string().min(1, { message: '键名不能为空' }),
type: z.enum(['string', 'number', 'boolean']),
value: z.string(),
}).superRefine((data, ctx) => {
if (data.type === 'number' && isNaN(Number(data.value))) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "必须是有效的数字",
path: ['value'],
});
}
if (data.type === 'boolean' && data.value !== 'true' && data.value !== 'false') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "必须是 true 或 false",
path: ['value'],
});
}
});
const formSchema = z.object({
name: z.string().min(1, { message: '模型名称不能为空' }),
model_provider: z.string().min(1, { message: '模型供应商不能为空' }),
url: z.string().min(1, { message: '请求URL不能为空' }),
api_key: z.string().min(1, { message: 'API Key不能为空' }),
abilities: z.array(z.string()),
extra_args: z.array(extraArgSchema).optional(),
})
export default function LLMForm({
editMode,
initLLMId,
@@ -16,40 +73,97 @@ export default function LLMForm({
}: {
editMode: boolean;
initLLMId?: string;
onFormSubmit: (value: ICreateLLMField) => void;
onFormCancel: (value: ICreateLLMField) => void;
onFormSubmit: (value: z.infer<typeof formSchema>) => void;
onFormCancel: () => void;
onLLMDeleted: () => void;
}) {
const [form] = Form.useForm<ICreateLLMField>();
const extraOptions: SelectProps['options'] = [];
const [initValue] = useState<ICreateLLMField>();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
model_provider: '',
url: '',
api_key: '',
abilities: [],
extra_args: [],
},
});
const [extraArgs, setExtraArgs] = useState<{key: string, type: 'string' | 'number' | 'boolean', value: string}[]>([]);
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false);
const abilityOptions: SelectProps['options'] = [
{
label: '视觉能力',
value: 'vision',
},
{
label: '函数调用',
value: 'func_call',
},
{
label: '图像识别',
value: 'vision',
},
];
const [requesterNameList, setRequesterNameList] = useState<
IChooseRequesterEntity[]
>([]);
const [requesterDefaultURLList, setRequesterDefaultURLList] = useState<
string[]
>([]);
useEffect(() => {
initLLMModelFormComponent();
if (editMode && initLLMId) {
getLLMConfig(initLLMId).then((val) => {
form.setFieldsValue(val);
form.setValue('name', val.name);
form.setValue('model_provider', val.model_provider);
form.setValue('url', val.url);
form.setValue('api_key', val.api_key);
form.setValue('abilities', val.abilities as ("vision" | "func_call")[]);
// 转换extra_args为新格式
if(val.extra_args) {
const args = val.extra_args.map(arg => {
const [key, value] = arg.split(':');
let type: 'string' | 'number' | 'boolean' = 'string';
if(!isNaN(Number(value))) {
type = 'number';
} else if(value === 'true' || value === 'false') {
type = 'boolean';
}
return {
key,
type,
value
};
});
setExtraArgs(args);
form.setValue('extra_args', args);
}
});
} else {
form.resetFields();
form.reset();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const addExtraArg = () => {
setExtraArgs([...extraArgs, {key: '', type: 'string', value: ''}]);
};
const updateExtraArg = (index: number, field: 'key' | 'type' | 'value', value: string) => {
const newArgs = [...extraArgs];
newArgs[index] = {
...newArgs[index],
[field]: value
};
setExtraArgs(newArgs);
form.setValue('extra_args', newArgs);
};
const removeExtraArg = (index: number) => {
const newArgs = extraArgs.filter((_, i) => i !== index);
setExtraArgs(newArgs);
form.setValue('extra_args', newArgs);
};
async function initLLMModelFormComponent() {
const requesterNameList = await httpClient.getProviderRequesters();
setRequesterNameList(
@@ -60,6 +174,17 @@ export default function LLMForm({
};
}),
);
setRequesterDefaultURLList(
requesterNameList.requesters.map((item) => {
const config = item.spec.config;
for (let i = 0; i < config.length; i++) {
if (config[i].name == 'base_url') {
return config[i].default;
}
}
return '';
}),
);
}
async function getLLMConfig(id: string): Promise<ICreateLLMField> {
@@ -75,42 +200,35 @@ export default function LLMForm({
model_provider: llmModel.model.requester,
url: llmModel.model.requester_config?.base_url,
api_key: llmModel.model.api_keys[0],
abilities: llmModel.model.abilities,
abilities: llmModel.model.abilities || [],
extra_args: fakeExtraArgs,
};
}
function handleFormSubmit(value: ICreateLLMField) {
function handleFormSubmit(value: z.infer<typeof formSchema>) {
if (editMode) {
// 暂不支持更改模型
// onSaveEdit(value)
} else {
onCreateLLM(value);
}
form.resetFields();
form.reset();
}
// function onSaveEdit(value: ICreateLLMField) {
// const requestParam: LLMModel = {
// uuid: UUID.generate(),
// name: value.name,
// description: "",
// requester: value.model_provider,
// requester_config: {
// "base_url": value.url,
// "timeout": 120
// },
// extra_args: value.extra_args,
// api_keys: [value.api_key],
// abilities: value.abilities,
// // created_at: 'Sun Apr 27 2025 21:56:35 GMT+0800',
// // updated_at: 'Sun Apr 27 2025 21:56:35 GMT+0800',
// };
// httpClient.createProviderLLMModel(requestParam).then(r => console.log(r))
// }
function onCreateLLM(value: ICreateLLMField) {
function onCreateLLM(value: z.infer<typeof formSchema>) {
console.log('create llm', value);
// 转换extra_args为对象格式
const extraArgsObj: Record<string, any> = {};
value.extra_args?.forEach(arg => {
if(arg.type === 'number') {
extraArgsObj[arg.key] = Number(arg.value);
} else if(arg.type === 'boolean') {
extraArgsObj[arg.key] = arg.value === 'true';
} else {
extraArgsObj[arg.key] = arg.value;
}
});
const requestParam: LLMModel = {
uuid: UUID.generate(),
name: value.name,
@@ -120,18 +238,16 @@ export default function LLMForm({
base_url: value.url,
timeout: 120,
},
extra_args: value.extra_args,
extra_args: extraArgsObj,
api_keys: [value.api_key],
abilities: value.abilities,
// created_at: 'Sun Apr 27 2025 21:56:35 GMT+0800',
// updated_at: 'Sun Apr 27 2025 21:56:35 GMT+0800',
};
httpClient.createProviderLLMModel(requestParam).then(() => {
onFormSubmit(value);
});
}
function handleAbilitiesChange() {}
function handleAbilitiesChange() { }
function deleteModel() {
if (initLLMId) {
@@ -143,145 +259,221 @@ export default function LLMForm({
return (
<div className={styles.modalContainer}>
<Modal
open={showDeleteConfirmModal}
title={'删除确认'}
onCancel={() => setShowDeleteConfirmModal(false)}
footer={
<div
style={{
width: '170px',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
}}
>
<Button
danger
onClick={() => {
deleteModel();
setShowDeleteConfirmModal(false);
}}
>
</Button>
<Button
onClick={() => {
setShowDeleteConfirmModal(false);
}}
>
<Dialog open={showDeleteConfirmModal} onOpenChange={setShowDeleteConfirmModal}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<DialogDescription>
</DialogDescription>
<DialogFooter>
<Button variant="outline" onClick={() => setShowDeleteConfirmModal(false)}>
</Button>
<Button variant="destructive" onClick={() => {
deleteModel();
setShowDeleteConfirmModal(false);
}}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleFormSubmit)} className="space-y-8">
<div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel><span className="text-red-500">*</span></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
<FormDescription>
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="model_provider"
render={({ field }) => (
<FormItem>
<FormLabel><span className="text-red-500">*</span></FormLabel>
<FormControl>
<Select onValueChange={(value) => {
field.onChange(value);
const index = requesterNameList.findIndex(item => item.value === value);
if(index !== -1) {
form.setValue('url', requesterDefaultURLList[index]);
}
}} value={field.value}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="选择模型供应商" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{requesterNameList.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>URL<span className="text-red-500">*</span></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="api_key"
render={({ field }) => (
<FormItem>
<FormLabel>API Key<span className="text-red-500">*</span></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="abilities"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<div className="mb-0">
<FormDescription>
</FormDescription>
</div>
{abilityOptions.map((item) => (
<FormField
key={item.value}
control={form.control}
name="abilities"
render={({ field }) => {
return (
<FormItem
key={item.value}
className="flex flex-row items-start space-x-1 space-y-0"
>
<FormControl>
<Checkbox
checked={Array.isArray(field.value) && field.value?.includes(item.value)}
onCheckedChange={(checked) => {
return checked
? field.onChange([...(field.value || []), item.value])
: field.onChange(
field.value?.filter(
(value) => value !== item.value
)
)
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.label}
</FormLabel>
</FormItem>
)
}}
/>
))}
<FormMessage />
</FormItem>
)}
/>
<FormItem>
<FormLabel></FormLabel>
<div className="space-y-2">
{extraArgs.map((arg, index) => (
<div key={index} className="flex gap-2">
<Input
placeholder="键名"
value={arg.key}
onChange={(e) => updateExtraArg(index, 'key', e.target.value)}
/>
<Select
value={arg.type}
onValueChange={(value) => updateExtraArg(index, 'type', value)}
>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="string"></SelectItem>
<SelectItem value="number"></SelectItem>
<SelectItem value="boolean"></SelectItem>
</SelectContent>
</Select>
<Input
placeholder="值"
value={arg.value}
onChange={(e) => updateExtraArg(index, 'value', e.target.value)}
/>
<button
type="button"
className="p-2 hover:bg-gray-100 rounded"
onClick={() => removeExtraArg(index)}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5 text-red-500">
<path d="M7 4V2H17V4H22V6H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V6H2V4H7ZM6 6V20H18V6H6ZM9 9H11V17H9V9ZM13 9H15V17H13V9Z"></path>
</svg>
</button>
</div>
))}
<Button type="button" variant="outline" onClick={addExtraArg}>
</Button>
</div>
<FormDescription>
max_tokens, temperature, top_p
</FormDescription>
<FormMessage />
</FormItem>
</div>
}
>
</Modal>
<Form
form={form}
labelCol={{ span: 4 }}
wrapperCol={{ span: 14 }}
layout="horizontal"
initialValues={{
...initValue,
}}
onFinish={handleFormSubmit}
clearOnDestroy={true}
disabled={editMode}
>
<Form.Item<ICreateLLMField>
label={'模型名称'}
name={'name'}
rules={[{ required: true, message: '该项为必填项哦~' }]}
>
<Input
placeholder={'为自己的大模型取个好听的名字~'}
style={{ width: 260 }}
></Input>
</Form.Item>
<Form.Item<ICreateLLMField>
label={'模型供应商'}
name={'model_provider'}
rules={[{ required: true, message: '该项为必填项哦~' }]}
>
<Select
style={{ width: 120 }}
onChange={() => {}}
options={requesterNameList}
/>
</Form.Item>
<Form.Item<ICreateLLMField>
label={'请求URL'}
name={'url'}
rules={[{ required: true, message: '该项为必填项哦~' }]}
>
<Input
placeholder="请求地址,一般是API提供商提供的URL"
style={{ width: 500 }}
></Input>
</Form.Item>
<Form.Item<ICreateLLMField>
label={'API Key'}
name={'api_key'}
rules={[{ required: true, message: '该项为必填项哦~' }]}
>
<Input placeholder="你的API Key" style={{ width: 500 }}></Input>
</Form.Item>
<Form.Item<ICreateLLMField> label={'开启能力'} name={'abilities'}>
<Select
mode="tags"
style={{ width: 500 }}
placeholder="选择模型能力,输入回车可自定义能力"
onChange={handleAbilitiesChange}
options={abilityOptions}
/>
</Form.Item>
<Form.Item<ICreateLLMField> label={'其他参数'} name={'extra_args'}>
<Select
mode="tags"
style={{ width: 500 }}
placeholder="输入后回车可自定义其他参数,例 key:value"
onChange={handleAbilitiesChange}
options={extraOptions}
/>
</Form.Item>
<Form.Item wrapperCol={{ offset: 4, span: 14 }}>
<Space>
<DialogFooter>
{!editMode && (
<Button type="primary" htmlType="submit">
</Button>
<Button type="submit"></Button>
)}
{editMode && (
<Button
color="danger"
variant="solid"
onClick={() => {
setShowDeleteConfirmModal(true);
}}
disabled={false}
>
<Button type="button" variant="destructive" onClick={() => setShowDeleteConfirmModal(true)}>
</Button>
)}
<Button
htmlType="button"
onClick={() => {
onFormCancel(form.getFieldsValue());
}}
disabled={false}
>
<Button type="button" onClick={() => onFormCancel()}>
</Button>
</Space>
</Form.Item>
</DialogFooter>
</form>
</Form>
</div>
);
}