diff --git a/templates/metadata/pipeline/ai.yaml b/templates/metadata/pipeline/ai.yaml index 8b7959f6..3bd9b731 100644 --- a/templates/metadata/pipeline/ai.yaml +++ b/templates/metadata/pipeline/ai.yaml @@ -40,9 +40,8 @@ stages: label: en_US: Model zh_CN: 模型 - type: select + type: llm-model-selector required: true - scope: /provider/models/llm - name: max-round label: en_US: Max Round @@ -54,16 +53,8 @@ stages: label: en_US: Prompt zh_CN: 提示词 - type: array + type: prompt-editor required: true - items: - type: object - properties: - role: - type: string - default: user - content: - type: string - name: dify-service-api label: en_US: Dify Service API diff --git a/templates/metadata/pipeline/trigger.yaml b/templates/metadata/pipeline/trigger.yaml index 78759b11..ea7c596a 100644 --- a/templates/metadata/pipeline/trigger.yaml +++ b/templates/metadata/pipeline/trigger.yaml @@ -28,11 +28,9 @@ stages: description: en_US: The prefix of the message zh_CN: 消息前缀 - type: array + type: array[string] required: true default: [] - items: - type: string - name: regexp label: en_US: Regexp @@ -40,11 +38,9 @@ stages: description: en_US: The regexp of the message zh_CN: 消息正则表达式 - type: array + type: array[string] required: true default: [] - items: - type: string - name: random label: en_US: Random @@ -83,20 +79,16 @@ stages: label: en_US: Blacklist zh_CN: 黑名单 - type: array + type: array[string] required: true default: [] - items: - type: string - name: whitelist label: en_US: Whitelist zh_CN: 白名单 - type: array + type: array[string] required: true default: [] - items: - type: string - name: ignore-rules label: en_US: Ignore Rules @@ -109,11 +101,9 @@ stages: description: en_US: The prefix of the message zh_CN: 消息前缀 - type: array + type: array[string] required: true default: [] - items: - type: string - name: regexp label: en_US: Regexp @@ -121,8 +111,6 @@ stages: description: en_US: The regexp of the message zh_CN: 消息正则表达式 - type: array + type: array[string] required: true default: [] - items: - type: string diff --git a/web/package-lock.json b/web/package-lock.json index eb709ffe..a1ade004 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -16,6 +16,7 @@ "@radix-ui/react-select": "^2.2.4", "@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-switch": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.11", "@radix-ui/react-toggle": "^1.1.8", "@radix-ui/react-toggle-group": "^1.1.9", "@tailwindcss/postcss": "^4.1.5", @@ -1503,6 +1504,36 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.11.tgz", + "integrity": "sha512-4FiKSVoXqPP/KfzlB7lwwqoFV6EPwkrrqGp9cUYXjwDYHhvpnqq79P+EPHKcdoTE7Rl8w/+6s9rTlsfXHES9GA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-roving-focus": "1.1.9", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toggle": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.8.tgz", diff --git a/web/package.json b/web/package.json index e800ae9b..2a1105bc 100644 --- a/web/package.json +++ b/web/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-select": "^2.2.4", "@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-switch": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.11", "@radix-ui/react-toggle": "^1.1.8", "@radix-ui/react-toggle-group": "^1.1.9", "@tailwindcss/postcss": "^4.1.5", diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx index 7a411fd6..d7332492 100644 --- a/web/src/app/home/bots/components/bot-form/BotForm.tsx +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -2,10 +2,10 @@ import { useEffect, useState } from 'react'; import { IChooseAdapterEntity, IPipelineEntity } from '@/app/home/bots/components/bot-form/ChooseEntity'; import { DynamicFormItemConfig, - IDynamicFormItemConfig, getDefaultValues, parseDynamicFormItemType, } from '@/app/home/components/dynamic-form/DynamicFormItemConfig'; +import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic'; import { UUID } from 'uuidjs'; import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; import { httpClient } from '@/app/infra/http/HttpClient'; @@ -76,7 +76,7 @@ export default function BotForm({ const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); const [adapterNameToDynamicConfigMap, setAdapterNameToDynamicConfigMap] = - useState(new Map()); + useState(new Map()); // const [form] = Form.useForm(); const [showDynamicForm, setShowDynamicForm] = useState(false); // const [dynamicForm] = Form.useForm(); @@ -95,7 +95,7 @@ export default function BotForm({ >([]); const [dynamicFormConfigList, setDynamicFormConfigList] = useState< - IDynamicFormItemConfig[] + IDynamicFormItemSchema[] >([]); const [isLoading, setIsLoading] = useState(false); diff --git a/web/src/app/home/bots/page.tsx b/web/src/app/home/bots/page.tsx index 8c2de108..1866d7c5 100644 --- a/web/src/app/home/bots/page.tsx +++ b/web/src/app/home/bots/page.tsx @@ -9,7 +9,7 @@ import BotForm from '@/app/home/bots/components/bot-form/BotForm'; import BotCard from '@/app/home/bots/components/bot-card/BotCard'; import CreateCardComponent from '@/app/infra/basic-component/create-card-component/CreateCardComponent'; import { httpClient } from '@/app/infra/http/HttpClient'; -import { Bot, Adapter } from '@/app/infra/api/api-types'; +import { Bot, Adapter } from '@/app/infra/entities/api'; import { Dialog, DialogContent, @@ -20,27 +20,14 @@ import { } from "@/components/ui/dialog" export default function BotConfigPage() { - const router = useRouter(); - const [pageShowRule, setPageShowRule] = useState( - BotConfigPageShowRule.NO_BOT, - ); const [modalOpen, setModalOpen] = useState(false); const [botList, setBotList] = useState([]); const [isEditForm, setIsEditForm] = useState(false); const [nowSelectedBotCard, setNowSelectedBotCard] = useState(); - const [isLoading, setIsLoading] = useState(false); useEffect(() => { // TODO:补齐加载转圈逻辑 - setIsLoading(true); - checkHasLLM().then((hasLLM) => { - if (hasLLM) { - getBotList(); - } else { - setPageShowRule(BotConfigPageShowRule.NO_LLM); - setIsLoading(false); - } - }); + }, []); async function checkHasLLM(): Promise { @@ -49,7 +36,6 @@ export default function BotConfigPage() { } async function getBotList() { - setIsLoading(true); const adapterListResp = await httpClient.getAdapters(); const adapterList = adapterListResp.adapters.map((adapter: Adapter) => { @@ -72,11 +58,6 @@ export default function BotConfigPage() { usePipelineName: bot.use_pipeline_name || '', }); }); - if (botList.length === 0) { - setPageShowRule(BotConfigPageShowRule.NO_BOT); - } else { - setPageShowRule(BotConfigPageShowRule.HAVE_BOT); - } setBotList(botList); }) .catch((err) => { @@ -89,7 +70,7 @@ export default function BotConfigPage() { // }); }) .finally(() => { - setIsLoading(false); + // setIsLoading(false); }); } @@ -112,46 +93,6 @@ export default function BotConfigPage() { return (
- {/* - setModalOpen(false)} - onCancel={() => setModalOpen(false)} - width={700} - footer={null} - destroyOnClose={true} - > - { - getBotList(); - setModalOpen(false); - }} - onFormCancel={() => setModalOpen(false)} - /> - - {pageShowRule === BotConfigPageShowRule.NO_LLM && ( - { - router.push('/home/models'); - }} - /> - )} - - {pageShowRule === BotConfigPageShowRule.NO_BOT && ( - - )} - */} @@ -176,7 +117,6 @@ export default function BotConfigPage() { {/* 注意:其余的返回内容需要保持在Spin组件外部 */} - {pageShowRule === BotConfigPageShowRule.HAVE_BOT && (
- )}
); } - -enum BotConfigPageShowRule { - NO_LLM, - NO_BOT, - HAVE_BOT, -} diff --git a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx index 77b527c5..490eafa3 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx @@ -1,4 +1,4 @@ -import { IDynamicFormItemConfig } from '@/app/home/components/dynamic-form/DynamicFormItemConfig'; +import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic'; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; @@ -18,7 +18,7 @@ export default function DynamicFormComponent({ onSubmit, initialValues, }: { - itemConfigList: IDynamicFormItemConfig[]; + itemConfigList: IDynamicFormItemSchema[]; onSubmit?: (val: object) => unknown; initialValues?: Record; }) { @@ -45,6 +45,15 @@ export default function DynamicFormComponent({ case 'select': fieldSchema = z.string(); break; + case 'llm-model-selector': + fieldSchema = z.string(); + break; + case 'prompt-editor': + fieldSchema = z.array(z.object({ + content: z.string(), + role: z.string(), + })); + break; default: fieldSchema = z.string(); } diff --git a/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx index ffef6e7d..bf18117d 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx @@ -1,20 +1,35 @@ -// import { Form, Input, InputNumber, Select, Switch } from 'antd'; import { DynamicFormItemType, - IDynamicFormItemConfig, -} from '@/app/home/components/dynamic-form/DynamicFormItemConfig'; + IDynamicFormItemSchema, +} from '@/app/infra/entities/form/dynamic'; import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Checkbox } from "@/components/ui/checkbox" +import { Switch } from "@/components/ui/switch" import { ControllerRenderProps } from "react-hook-form"; +import { Button } from "@/components/ui/button"; +import { useEffect, useState } from "react"; +import { httpClient } from "@/app/infra/http/HttpClient"; +import { LLMModel } from "@/app/infra/entities/api"; export default function DynamicFormItemComponent({ config, field, }: { - config: IDynamicFormItemConfig; + config: IDynamicFormItemSchema; field: ControllerRenderProps; }) { + const [llmModels, setLlmModels] = useState([]); + + useEffect(() => { + if (config.type === DynamicFormItemType.LLM_MODEL_SELECTOR) { + httpClient.getProviderLLMModels().then((resp) => { + setLlmModels(resp.models); + }).catch((err) => { + console.error('获取 LLM 模型列表失败:', err); + }); + } + }, [config.type]); + switch (config.type) { case DynamicFormItemType.INT: case DynamicFormItemType.FLOAT: @@ -31,7 +46,7 @@ export default function DynamicFormItemComponent({ case DynamicFormItemType.BOOLEAN: return ( - @@ -39,21 +54,42 @@ export default function DynamicFormItemComponent({ case DynamicFormItemType.STRING_ARRAY: return ( - +
+ {field.value.map((item: string, index: number) => ( +
+ { + const newValue = [...field.value]; + newValue[index] = e.target.value; + field.onChange(newValue); + }} + /> + +
+ ))} + +
); case DynamicFormItemType.SELECT: @@ -67,14 +103,107 @@ export default function DynamicFormItemComponent({ - {/* 这里需要根据实际情况添加选项 */} - 选项1 - 选项2 + {config.options?.map((option) => ( + + {option.label.zh_CN} + + ))} ); + case DynamicFormItemType.LLM_MODEL_SELECTOR: + return ( + + ); + + case DynamicFormItemType.PROMPT_EDITOR: + return ( +
+ {field.value.map((item: { role: string; content: string }, index: number) => ( +
+ {/* 角色选择 */} + {index === 0 ? ( +
system
+ ) : ( + + )} + {/* 内容输入 */} + { + const newValue = [...field.value]; + newValue[index] = { ...newValue[index], content: e.target.value }; + field.onChange(newValue); + }} + /> + {/* 删除按钮,第一轮不显示 */} + {index !== 0 && ( + + )} +
+ ))} + +
+ ); + default: return ; } diff --git a/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts b/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts index ce78622b..bb6db2a9 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts +++ b/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts @@ -1,23 +1,17 @@ -export interface IDynamicFormItemConfig { - id: string; - default: string | number | boolean | Array; - label: IDynamicFormItemLabel; - name: string; - required: boolean; - type: DynamicFormItemType; - description?: IDynamicFormItemLabel; -} +import { IDynamicFormItemSchema, DynamicFormItemType, IDynamicFormItemOption } from '@/app/infra/entities/form/dynamic'; +import { I18nLabel } from '@/app/infra/entities/common'; -export class DynamicFormItemConfig implements IDynamicFormItemConfig { +export class DynamicFormItemConfig implements IDynamicFormItemSchema { id: string; name: string; default: string | number | boolean | Array; - label: IDynamicFormItemLabel; + label: I18nLabel; required: boolean; type: DynamicFormItemType; - description?: IDynamicFormItemLabel; + description?: I18nLabel; + options?: IDynamicFormItemOption[]; - constructor(params: IDynamicFormItemConfig) { + constructor(params: IDynamicFormItemSchema) { this.id = params.id; this.name = params.name; this.default = params.default; @@ -25,23 +19,10 @@ export class DynamicFormItemConfig implements IDynamicFormItemConfig { this.required = params.required; this.type = params.type; this.description = params.description; + this.options = params.options; } } -export interface IDynamicFormItemLabel { - en_US: string; - zh_CN: string; -} - -export enum DynamicFormItemType { - INT = 'integer', - FLOAT = 'float', - BOOLEAN = 'boolean', - STRING = 'string', - STRING_ARRAY = 'array[string]', - SELECT = 'select', - UNKNOWN = 'unknown', -} export function isDynamicFormItemType( value: string, @@ -55,7 +36,7 @@ export function parseDynamicFormItemType(value: string): DynamicFormItemType { return isDynamicFormItemType(value) ? value : DynamicFormItemType.UNKNOWN; } -export function getDefaultValues(itemConfigList: IDynamicFormItemConfig[]): Record { +export function getDefaultValues(itemConfigList: IDynamicFormItemSchema[]): Record { return itemConfigList.reduce((acc, item) => { acc[item.name] = item.default; return acc; diff --git a/web/src/app/home/components/dynamic-form/testDynamicConfigList.ts b/web/src/app/home/components/dynamic-form/testDynamicConfigList.ts index 244d7645..ca19b8b2 100644 --- a/web/src/app/home/components/dynamic-form/testDynamicConfigList.ts +++ b/web/src/app/home/components/dynamic-form/testDynamicConfigList.ts @@ -1,10 +1,10 @@ import { - DynamicFormItemConfig, DynamicFormItemType, - IDynamicFormItemConfig, -} from '@/app/home/components/dynamic-form/DynamicFormItemConfig'; + IDynamicFormItemSchema, +} from '@/app/infra/entities/form/dynamic'; +import { DynamicFormItemConfig } from '@/app/home/components/dynamic-form/DynamicFormItemConfig'; -export const testDynamicConfigList: IDynamicFormItemConfig[] = [ +export const testDynamicConfigList: IDynamicFormItemSchema[] = [ new DynamicFormItemConfig({ default: '', id: '111', diff --git a/web/src/app/home/models/component/llm-form/LLMForm.tsx b/web/src/app/home/models/component/llm-form/LLMForm.tsx index fe68a54f..41ba4842 100644 --- a/web/src/app/home/models/component/llm-form/LLMForm.tsx +++ b/web/src/app/home/models/component/llm-form/LLMForm.tsx @@ -1,9 +1,8 @@ -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/ChooseRequesterEntity'; import { httpClient } from '@/app/infra/http/HttpClient'; -import { LLMModel } from '@/app/infra/api/api-types'; +import { LLMModel } from '@/app/infra/entities/api'; import { UUID } from 'uuidjs'; import { zodResolver } from "@hookform/resolvers/zod" @@ -91,7 +90,7 @@ export default function LLMForm({ const [extraArgs, setExtraArgs] = useState<{key: string, type: 'string' | 'number' | 'boolean', value: string}[]>([]); const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); - const abilityOptions: SelectProps['options'] = [ + const abilityOptions: { label: string, value: string }[] = [ { label: '视觉能力', value: 'vision', @@ -178,7 +177,7 @@ export default function LLMForm({ const config = item.spec.config; for (let i = 0; i < config.length; i++) { if (config[i].name == 'base_url') { - return config[i].default; + return config[i].default?.toString() || ''; } } return ''; diff --git a/web/src/app/home/models/page.tsx b/web/src/app/home/models/page.tsx index 1856111c..f0ca7f24 100644 --- a/web/src/app/home/models/page.tsx +++ b/web/src/app/home/models/page.tsx @@ -8,7 +8,7 @@ import LLMCard from '@/app/home/models/component/llm-card/LLMCard'; import LLMForm from '@/app/home/models/component/llm-form/LLMForm'; import CreateCardComponent from '@/app/infra/basic-component/create-card-component/CreateCardComponent'; import { httpClient } from '@/app/infra/http/HttpClient'; -import { LLMModel } from '@/app/infra/api/api-types'; +import { LLMModel } from '@/app/infra/entities/api'; import { Dialog, DialogContent, @@ -98,43 +98,27 @@ export default function LLMConfigPage() { /> +
- {cardList.length > 0 && ( -
- - - {cardList.map((cardVO) => { - return ( -
{ - selectLLM(cardVO); - }} - > - -
- ); - })} -
- )} - - {cardList.length === 0 && ( -
- { - handleCreateModelClick(); - }} - /> -
- )} + + {cardList.map((cardVO) => { + return ( +
{ + selectLLM(cardVO); + }} + > + +
+ ); + })} +
); } diff --git a/web/src/app/home/pipelines/components/pipeline-form/PipelineChildFormEntity.ts b/web/src/app/home/pipelines/components/pipeline-form/PipelineChildFormEntity.ts deleted file mode 100644 index 08296a7f..00000000 --- a/web/src/app/home/pipelines/components/pipeline-form/PipelineChildFormEntity.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { DynamicFormItemConfig } from '@/app/home/components/dynamic-form/DynamicFormItemConfig'; - -export interface IPipelineChildFormEntity { - name: string; - label: string; - formItems: DynamicFormItemConfig[]; -} - -export class PipelineChildFormEntity implements IPipelineChildFormEntity { - formItems: DynamicFormItemConfig[]; - label: string; - name: string; - - constructor(props: IPipelineChildFormEntity) { - this.label = props.label; - this.name = props.name; - this.formItems = props.formItems; - } -} diff --git a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx index 2bcb54d1..4bd5b34b 100644 --- a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx +++ b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx @@ -1,23 +1,29 @@ + +import { useEffect, useState } from 'react'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { Pipeline } from '@/app/infra/entities/api'; +import { PipelineFormEntity, PipelineConfigTab, PipelineConfigStage } from '@/app/infra/entities/pipeline'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; +import { Button } from '@/components/ui/button'; +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { Input } from "@/components/ui/input" import { Form, - Button, - Switch, - Select, - Input, - InputNumber, - SelectProps, -} from 'antd'; -import { CaretLeftOutlined, CaretRightOutlined } from '@ant-design/icons'; -import { useEffect, useState } from 'react'; -import styles from './pipelineFormStyle.module.css'; -import { httpClient } from '@/app/infra/http/HttpClient'; -import { LLMModel, Pipeline } from '@/app/infra/api/api-types'; -import { UUID } from 'uuidjs'; -import { PipelineFormEntity } from '@/app/home/pipelines/components/pipeline-form/PipelineFormEntity'; + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" + export default function PipelineFormComponent({ initValues, onFinish, + onNewPipelineCreated, isEditMode, pipelineId, disableForm, @@ -28,625 +34,287 @@ export default function PipelineFormComponent({ // 这里的写法很不安全不规范,未来流水线需要重新整理 initValues?: PipelineFormEntity; onFinish: () => void; + onNewPipelineCreated: (pipelineId: string) => void; }) { - const [nowFormIndex, setNowFormIndex] = useState(0); - const [nowAIRunner, setNowAIRunner] = useState(''); - const [llmModelList, setLlmModelList] = useState([]); + + const formSchema = isEditMode ? z.object({ + basic: z.object({ + name: z.string().min(1, { message: '名称不能为空' }), + description: z.string().min(1, { message: '描述不能为空' }), + }), + ai: z.record(z.string(), z.any()), + trigger: z.record(z.string(), z.any()), + safety: z.record(z.string(), z.any()), + output: z.record(z.string(), z.any()), + }) + : z.object({ + basic: z.object({ + name: z.string().min(1, { message: '名称不能为空' }), + description: z.string().min(1, { message: '描述不能为空' }), + }), + ai: z.record(z.string(), z.any()).optional(), + trigger: z.record(z.string(), z.any()).optional(), + safety: z.record(z.string(), z.any()).optional(), + output: z.record(z.string(), z.any()).optional(), + }); + + type FormValues = z.infer; // 这里不好,可以改成enum等 - const formLabelList: FormLabel[] = [ - { label: '基础', name: 'basic' }, + const formLabelList: FormLabel[] = isEditMode ? [ + { label: '基础信息', name: 'basic' }, { label: 'AI能力', name: 'ai' }, { label: '触发条件', name: 'trigger' }, { label: '安全能力', name: 'safety' }, { label: '输出处理', name: 'output' }, + ] : [ + { label: '基础信息', name: 'basic' }, ]; - const [basicForm] = Form.useForm(); - const [aiForm] = Form.useForm(); - const [triggerForm] = Form.useForm(); - const [safetyForm] = Form.useForm(); - const [outputForm] = Form.useForm(); + // const [basicForm] = Form.useForm(); + // const [aiForm] = Form.useForm(); + // const [triggerForm] = Form.useForm(); + // const [safetyForm] = Form.useForm(); + // const [outputForm] = Form.useForm(); + const [aiConfigTabSchema, setAIConfigTabSchema] = useState(); + const [triggerConfigTabSchema, setTriggerConfigTabSchema] = useState(); + const [safetyConfigTabSchema, setSafetyConfigTabSchema] = useState(); + const [outputConfigTabSchema, setOutputConfigTabSchema] = useState(); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + basic: {}, + ai: {}, + trigger: {}, + safety: {}, + output: {}, + }, + }); useEffect(() => { - getLLMModelList(); + + // get config schema from metadata + httpClient.getGeneralPipelineMetadata().then((resp) => { + for (const config of resp.configs) { + if (config.name === 'ai') { + setAIConfigTabSchema(config); + } else if (config.name === 'trigger') { + setTriggerConfigTabSchema(config); + } else if (config.name === 'safety') { + setSafetyConfigTabSchema(config); + } else if (config.name === 'output') { + setOutputConfigTabSchema(config); + } + } + }); }, []); useEffect(() => { - console.log('initValues change: ', initValues); if (initValues) { - basicForm.setFieldsValue(initValues.basic); - aiForm.setFieldsValue(initValues.ai); - triggerForm.setFieldsValue(initValues.trigger); - safetyForm.setFieldsValue(initValues.safety); - outputForm.setFieldsValue(initValues.output); + form.reset(initValues); } - }, [aiForm, basicForm, initValues, outputForm, safetyForm, triggerForm]); - function getLLMModelList() { - httpClient - .getProviderLLMModels() - .then((resp) => { - setLlmModelList( - resp.models.map((model: LLMModel) => { - return { - value: model.uuid, - label: model.name, - }; - }), - ); - }) - .catch((err) => { - console.error('get LLM model list error', err); + if (!isEditMode) { + form.reset({ + basic: { + name: '', + description: '', + }, }); - } - - function getNowFormLabel() { - return formLabelList[nowFormIndex]; - } - - function getPreFormLabel(): undefined | FormLabel { - if (nowFormIndex !== undefined && nowFormIndex > 0) { - return formLabelList[nowFormIndex - 1]; - } else { - return undefined; } - } + }, [initValues, form]); - function getNextFormLabel(): undefined | FormLabel { - if (nowFormIndex !== undefined && nowFormIndex < formLabelList.length - 1) { - return formLabelList[nowFormIndex + 1]; - } else { - return undefined; - } - } - - function addFormLabelIndex() { - if (nowFormIndex < formLabelList.length - 1) { - setNowFormIndex(nowFormIndex + 1); - } - } - - function reduceFormLabelIndex() { - if (nowFormIndex > 0) { - setNowFormIndex(nowFormIndex - 1); - } - } - - function handleCommit() { + function handleFormSubmit(values: FormValues) { + console.log('handleFormSubmit', values); if (isEditMode) { - handleModify(); + handleModify(values); } else { - handleCreate(); + handleCreate(values); } } - function handleCreate() { - Promise.all([ - basicForm.validateFields(), - aiForm.validateFields(), - triggerForm.validateFields(), - safetyForm.validateFields(), - outputForm.validateFields(), - ]) - .then(() => { - const pipeline = assembleForm(); - httpClient.createPipeline(pipeline).then(() => onFinish()); - }) - .catch((e) => { - console.error(e); - }); + function handleCreate(values: FormValues) { + console.log('handleCreate', values); + const pipeline: Pipeline = { + description: values.basic.description, + name: values.basic.name, + }; + httpClient.createPipeline(pipeline).then((resp) => { + onFinish(); + onNewPipelineCreated(resp.uuid); + }); } - function handleModify() { - Promise.all([ - basicForm.validateFields(), - aiForm.validateFields(), - triggerForm.validateFields(), - safetyForm.validateFields(), - outputForm.validateFields(), - ]) - .then(() => { - const pipeline = assembleForm(); - httpClient - .updatePipeline(pipelineId || '', pipeline) - .then(() => onFinish()); - }) - .catch((e) => { - console.error(e); - }); + function handleModify(values: FormValues) { + + const realConfig = { + ai: values.ai, + trigger: values.trigger, + safety: values.safety, + output: values.output, + }; + + const pipeline: Pipeline = { + config: realConfig, + // created_at: '', + description: values.basic.description, + // for_version: '', + name: values.basic.name, + // stages: [], + // updated_at: '', + // uuid: pipelineId || '', + // is_default: false, + }; + httpClient.updatePipeline(pipelineId || '', pipeline).then(() => onFinish()); } - // TODO 类型混乱,需要优化 - function assembleForm(): Pipeline { - console.log('basicForm:', basicForm.getFieldsValue()); - console.log('aiForm:', aiForm.getFieldsValue()); - console.log('triggerForm:', triggerForm.getFieldsValue()); - console.log('safetyForm:', safetyForm.getFieldsValue()); - console.log('outputForm:', outputForm.getFieldsValue()); - const config: object = { - ai: aiForm.getFieldsValue(), - trigger: triggerForm.getFieldsValue(), - safety: safetyForm.getFieldsValue(), - output: outputForm.getFieldsValue(), - }; + function renderDynamicForms(stage: PipelineConfigStage, formName: keyof FormValues) { + // 如果是 AI 配置,需要特殊处理 + if (formName === 'ai') { + // 获取当前选择的 runner + const currentRunner = form.watch('ai.runner.runner'); - return { - config, - created_at: '', - description: basicForm.getFieldsValue().description, - for_version: '', - name: basicForm.getFieldsValue().name, - stages: [], - updated_at: '', - uuid: UUID.generate(), - }; + // 如果是 runner 配置项,直接渲染 + if (stage.name === 'runner') { + return ( +
+
{stage.label.zh_CN}
+ {stage.description && ( +
{stage.description.zh_CN}
+ )} + )?.[stage.name] || {}} + onSubmit={(values) => { + const currentValues = form.getValues(formName) as Record || {}; + form.setValue(formName, { + ...currentValues, + [stage.name]: values, + }); + }} + /> +
+ ); + } + + // 如果不是当前选择的 runner 对应的配置项,则不渲染 + if (stage.name !== currentRunner) { + return null; + } + } + + return ( +
+
{stage.label.zh_CN}
+ {stage.description && ( +
{stage.description.zh_CN}
+ )} + )?.[stage.name] || {}} + onSubmit={(values) => { + const currentValues = form.getValues(formName) as Record || {}; + form.setValue(formName, { + ...currentValues, + [stage.name]: values, + }); + }} + /> +
+ ); } return (
-

{getNowFormLabel().label}

-
- - - + + + + + {formLabelList.map((formLabel) => ( + + {formLabel.label} + + ))} + - - - - - {/* AI能力表单 ai */} -
- {/* Runner 配置区块 */} -
运行器
- - - - - - - {/* TODO 这里要做转换处理 */} - - - - - )} - {/* Dify 服务 API 区块 */} - {nowAIRunner === 'dify-service-api' && ( - <> -
配置Dify服务API
- - - - - ...\<\/think\>', value: 'plain' }, - { label: '原始', value: 'original' }, - { label: '移除', value: 'remove' }, - ]} - /> - - - )} - {/* 阿里云百炼区块 */} - {nowAIRunner === 'dashscope-app-api' && ( - <> -
- 配置阿里云百炼平台 API + {formLabel.name === 'basic' && ( +
+ ( + + 名称* + + + + + + )} + /> + + ( + + 描述* + + + + + + )} + /> +
+ )} + + {isEditMode && ( + <> + {formLabel.name === 'ai' && aiConfigTabSchema && ( +
+ {aiConfigTabSchema.stages.map((stage) => renderDynamicForms(stage, 'ai'))} +
+ )} + + {formLabel.name === 'trigger' && triggerConfigTabSchema && ( +
+ {triggerConfigTabSchema.stages.map((stage) => renderDynamicForms(stage, 'trigger'))} +
+ )} + + {formLabel.name === 'safety' && safetyConfigTabSchema && ( +
+ {safetyConfigTabSchema.stages.map((stage) => renderDynamicForms(stage, 'safety'))} +
+ )} + + {formLabel.name === 'output' && outputConfigTabSchema && ( +
+ {outputConfigTabSchema.stages.map((stage) => renderDynamicForms(stage, 'output'))} +
+ )} + + )} + + ))} + + +
+
+ +
- - - - - - - - )} +
+ - - {/* 触发条件表单 trigger */} -
- {/* 群响应规则块 */} -
群响应规则
- - - - - - - - - -
访问控制
- - - - - - - - - - - - - - - - - {/* 速率限制块 rate-limit */} -
速率限制
- - - - - - - - - - - - - - {/* 强制延迟区块 */} -
强制延迟
- - - - - - - - {/* 杂项区块 */} -
杂项
- - - - - - - - - - - - -
- -
- - - - -
); } diff --git a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormEntity.ts b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormEntity.ts deleted file mode 100644 index 142e7898..00000000 --- a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormEntity.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface PipelineFormEntity { - basic: object; - ai: object; - trigger: object; - safety: object; - output: object; -} diff --git a/web/src/app/home/pipelines/page.tsx b/web/src/app/home/pipelines/page.tsx index e908530a..9e15b7d8 100644 --- a/web/src/app/home/pipelines/page.tsx +++ b/web/src/app/home/pipelines/page.tsx @@ -1,13 +1,20 @@ 'use client'; -import { Modal } from 'antd'; import { useState, useEffect } from 'react'; import CreateCardComponent from '@/app/infra/basic-component/create-card-component/CreateCardComponent'; import PipelineFormComponent from './components/pipeline-form/PipelineFormComponent'; import { httpClient } from '@/app/infra/http/HttpClient'; import { PipelineCardVO } from '@/app/home/pipelines/components/pipeline-card/PipelineCardVO'; import PipelineCard from '@/app/home/pipelines/components/pipeline-card/PipelineCard'; -import { PipelineFormEntity } from '@/app/home/pipelines/components/pipeline-form/PipelineFormEntity'; +import { PipelineFormEntity } from '@/app/infra/entities/pipeline'; import styles from './pipelineConfig.module.css'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" import { Button } from '@/components/ui/button'; export default function PluginConfigPage() { @@ -35,16 +42,16 @@ export default function PluginConfigPage() { .then((value) => { let currentTime = new Date(); const pipelineList = value.pipelines.map((pipeline) => { - let lastUpdatedTimeAgo = Math.floor((currentTime.getTime() - new Date(pipeline.updated_at).getTime()) / 1000 / 60 / 60 / 24); - + let lastUpdatedTimeAgo = Math.floor((currentTime.getTime() - new Date(pipeline.updated_at ?? currentTime.getTime()).getTime()) / 1000 / 60 / 60 / 24); + let lastUpdatedTimeAgoText = lastUpdatedTimeAgo > 0 ? ` ${lastUpdatedTimeAgo} 天前` : '今天'; - + return new PipelineCardVO({ lastUpdatedTimeAgo: lastUpdatedTimeAgoText, description: pipeline.description, - id: pipeline.uuid, + id: pipeline.uuid ?? '', name: pipeline.name, - isDefault: pipeline.is_default, + isDefault: pipeline.is_default ?? false, }); }); setPipelineList(pipelineList); @@ -73,58 +80,64 @@ export default function PluginConfigPage() { return (
- - setModalOpen(false)} - onCancel={() => setModalOpen(false)} - width={700} - footer={null} - > - { - getPipelines(); - setModalOpen(false); + + + + + + {isEditForm ? '编辑流水线' : '创建流水线'} + + +
+ { + setDisableForm(true); + setIsEditForm(true); + setModalOpen(true); + setSelectedPipelineId(pipelineId); + getSelectedPipelineForm(pipelineId); + }} + onFinish={() => { + getPipelines(); + setModalOpen(false); + }} + isEditMode={isEditForm} + pipelineId={selectedPipelineId} + disableForm={disableForm} + initValues={selectedPipelineFormValue} + /> +
+
+
+ +
+ { + setIsEditForm(false); + setModalOpen(true); }} - isEditMode={isEditForm} - pipelineId={selectedPipelineId} - disableForm={disableForm} - initValues={selectedPipelineFormValue} /> - - {pipelineList.length > 0 && ( -
- { - setModalOpen(true); - }} - /> - - {pipelineList.map((pipeline) => { - return ( -
{ - setDisableForm(true); - setIsEditForm(true); - setModalOpen(true); - setSelectedPipelineId(pipeline.id); - getSelectedPipelineForm(pipeline.id); - }} - > - -
- ); - })} -
- )} + {pipelineList.map((pipeline) => { + return ( +
{ + setDisableForm(true); + setIsEditForm(true); + setModalOpen(true); + setSelectedPipelineId(pipeline.id); + getSelectedPipelineForm(pipeline.id); + }} + > + +
+ ); + })} +
); } diff --git a/web/src/app/infra/api/api-types/index.ts b/web/src/app/infra/entities/api/index.ts similarity index 92% rename from web/src/app/infra/api/api-types/index.ts rename to web/src/app/infra/entities/api/index.ts index b4c2f1a6..f722c3f2 100644 --- a/web/src/app/infra/api/api-types/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -1,3 +1,7 @@ +import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic'; +import { I18nLabel } from '@/app/infra/entities/common'; +import { PipelineConfigTab } from '@/app/infra/entities/pipeline'; + export interface ApiResponse { code: number; data: T; @@ -26,7 +30,9 @@ export interface Requester { label: I18nText; description: I18nText; icon?: string; - spec: object; + spec: { + config: IDynamicFormItemSchema[]; + } } export interface ApiRespProviderLLMModels { @@ -58,15 +64,15 @@ export interface ApiRespPipelines { } export interface Pipeline { - uuid: string; + uuid?: string; name: string; description: string; - for_version: string; + for_version?: string; config: object; - stages: string[]; - is_default: boolean; - created_at: string; - updated_at: string; + stages?: string[]; + is_default?: boolean; + created_at?: string; + updated_at?: string; } export interface ApiRespPlatformAdapters { @@ -302,3 +308,7 @@ interface GetPipeline { export interface GetPipelineResponseData { pipeline: GetPipeline; } + +export interface GetPipelineMetadataResponseData { + configs: PipelineConfigTab[]; +} \ No newline at end of file diff --git a/web/src/app/infra/entities/common.ts b/web/src/app/infra/entities/common.ts new file mode 100644 index 00000000..3408c105 --- /dev/null +++ b/web/src/app/infra/entities/common.ts @@ -0,0 +1,5 @@ +export interface I18nLabel { + en_US: string; + zh_CN: string; + ja_JP?: string; +} diff --git a/web/src/app/infra/entities/form/dynamic.ts b/web/src/app/infra/entities/form/dynamic.ts new file mode 100644 index 00000000..6e70062b --- /dev/null +++ b/web/src/app/infra/entities/form/dynamic.ts @@ -0,0 +1,29 @@ +import { I18nLabel } from '@/app/infra/entities/common'; + +export interface IDynamicFormItemSchema { + id: string; + default: string | number | boolean | Array; + label: I18nLabel; + name: string; + required: boolean; + type: DynamicFormItemType; + description?: I18nLabel; + options?: IDynamicFormItemOption[]; + } + +export enum DynamicFormItemType { + INT = 'integer', + FLOAT = 'float', + BOOLEAN = 'boolean', + STRING = 'string', + STRING_ARRAY = 'array[string]', + SELECT = 'select', + LLM_MODEL_SELECTOR = 'llm-model-selector', + PROMPT_EDITOR = 'prompt-editor', + UNKNOWN = 'unknown', + } + +export interface IDynamicFormItemOption { + name: string; + label: I18nLabel; + } \ No newline at end of file diff --git a/web/src/app/infra/entities/pipeline/index.ts b/web/src/app/infra/entities/pipeline/index.ts new file mode 100644 index 00000000..2c364b08 --- /dev/null +++ b/web/src/app/infra/entities/pipeline/index.ts @@ -0,0 +1,23 @@ +import { I18nLabel } from '@/app/infra/entities/common'; +import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic'; + +export interface PipelineFormEntity { + basic: object; + ai: object; + trigger: object; + safety: object; + output: object; +} + +export interface PipelineConfigTab { + name: string; + label: I18nLabel; + stages: PipelineConfigStage[]; +} + +export interface PipelineConfigStage { + name: string; + label: I18nLabel; + description?: I18nLabel; + config: IDynamicFormItemSchema[]; +} \ No newline at end of file diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index 325627af..cf1f1546 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -28,7 +28,8 @@ import { ApiRespUserToken, MarketPluginResponse, GetPipelineResponseData, -} from '../api/api-types'; + GetPipelineMetadataResponseData +} from '@/app/infra/entities/api'; import { notification } from 'antd'; type JSONValue = string | number | boolean | JSONObject | JSONArray | null; @@ -249,7 +250,7 @@ class HttpClient { } // ============ Pipeline API ============ - public getGeneralPipelineMetadata(): Promise { + public getGeneralPipelineMetadata(): Promise { // as designed, this method will be deprecated, and only for developer to check the prefered config schema return this.get('/api/v1/pipelines/_/metadata'); } diff --git a/web/src/components/ui/tabs.tsx b/web/src/components/ui/tabs.tsx new file mode 100644 index 00000000..497ba5ea --- /dev/null +++ b/web/src/components/ui/tabs.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent }