diff --git a/.gitignore b/.gitignore index c5a687ca..ac2a837a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ data/ !pytests/A_memorix_test/data/real_dialogues/private_alice_weekend.json pytests/A_memorix_test/data/benchmarks/results/ data1/ +mai_knowledge/knowledge.json mongodb/ NapCat.Framework.Windows.Once/ NapCat.Framework.Windows.OneKey/ diff --git a/dashboard/src/components/dynamic-form/DynamicConfigForm.tsx b/dashboard/src/components/dynamic-form/DynamicConfigForm.tsx index cd04b78f..abb377c2 100644 --- a/dashboard/src/components/dynamic-form/DynamicConfigForm.tsx +++ b/dashboard/src/components/dynamic-form/DynamicConfigForm.tsx @@ -1,5 +1,14 @@ import * as React from 'react' +import * as LucideIcons from 'lucide-react' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { Separator } from '@/components/ui/separator' import type { ConfigSchema, FieldSchema } from '@/types/config-schema' import { fieldHooks, type FieldHookRegistry } from '@/lib/field-hooks' @@ -9,7 +18,10 @@ export interface DynamicConfigFormProps { schema: ConfigSchema values: Record onChange: (field: string, value: unknown) => void + basePath?: string hooks?: FieldHookRegistry + /** 嵌套层级:0 = tab 内容层, 1 = section 内容层, 2+ = 更深嵌套 */ + level?: number } /** @@ -19,21 +31,32 @@ export interface DynamicConfigFormProps { * 1. Hook 系统:通过 FieldHookRegistry 自定义字段渲染 * - replace 模式:完全替换默认渲染 * - wrapper 模式:包装默认渲染(通过 children 传递) - * 2. 嵌套 schema:递归渲染 schema.nested 中的子配置 + * 2. 嵌套 schema:递归渲染 schema.nested 中的子配置,使用 Card 容器区分层级 * 3. 默认渲染:使用 DynamicField 组件 */ export const DynamicConfigForm: React.FC = ({ schema, values, onChange, + basePath = '', hooks = fieldHooks, // 默认使用全局单例 + level = 0, }) => { + const fieldMap = React.useMemo( + () => new Map(schema.fields.map((field) => [field.name, field])), + [schema.fields] + ) + + const buildFieldPath = (fieldName: string) => { + return basePath ? `${basePath}.${fieldName}` : fieldName + } + /** * 渲染单个字段 * 检查是否有注册的 Hook,根据 Hook 类型选择渲染方式 */ const renderField = (field: FieldSchema) => { - const fieldPath = field.name + const fieldPath = buildFieldPath(field.name) // 检查是否有注册的 Hook if (hooks.has(fieldPath)) { @@ -49,6 +72,7 @@ export const DynamicConfigForm: React.FC = ({ fieldPath={fieldPath} value={values[field.name]} onChange={(v) => onChange(field.name, v)} + schema={field} /> ) } else { @@ -58,6 +82,7 @@ export const DynamicConfigForm: React.FC = ({ fieldPath={fieldPath} value={values[field.name]} onChange={(v) => onChange(field.name, v)} + schema={field} > = ({ ) } + /** 渲染 section 图标 */ + const renderSectionIcon = (iconName?: string) => { + if (!iconName) return null + const IconComponent = LucideIcons[iconName as keyof typeof LucideIcons] as + | React.ComponentType<{ className?: string }> + | undefined + if (!IconComponent) return null + return + } + + // 过滤出不属于 nested 的顶层字段 + const topLevelFields = schema.fields.filter( + (field) => !schema.nested?.[field.name] + ) + return ( -
+
{/* 渲染顶层字段 */} - {schema.fields.map((field) => ( -
{renderField(field)}
- ))} + {topLevelFields.length > 0 && ( +
+ {topLevelFields.map((field, index) => ( + + {index > 0 && field.type !== 'boolean' && topLevelFields[index - 1]?.type !== 'boolean' && ( + + )} +
{renderField(field)}
+
+ ))} +
+ )} {/* 渲染嵌套 schema */} {schema.nested && - Object.entries(schema.nested).map(([key, nestedSchema]) => ( -
- {/* 嵌套 schema 标题 */} -
-

{nestedSchema.className}

- {nestedSchema.classDoc && ( -

{nestedSchema.classDoc}

- )} -
+ Object.entries(schema.nested).map(([key, nestedSchema]) => { + const nestedField = fieldMap.get(key) + const nestedFieldPath = buildFieldPath(key) - {/* 递归渲染嵌套表单 */} - ) || {}} - onChange={(field, value) => onChange(`${key}.${field}`, value)} - hooks={hooks} - /> -
- ))} + // Hook 系统处理 + if (hooks.has(nestedFieldPath)) { + const hookEntry = hooks.get(nestedFieldPath) + if (!hookEntry) return null + + const HookComponent = hookEntry.component + if (hookEntry.type === 'replace') { + return ( +
+ onChange(key, v)} + schema={nestedField ?? nestedSchema} + /> +
+ ) + } + + return ( +
+ onChange(key, v)} + schema={nestedField ?? nestedSchema} + > + ) || {}} + onChange={(field, value) => onChange(`${key}.${field}`, value)} + basePath={nestedFieldPath} + hooks={hooks} + level={level + 1} + /> + +
+ ) + } + + const sectionTitle = + nestedSchema.uiLabel || nestedSchema.classDoc || nestedSchema.className + const sectionDescription = + nestedSchema.classDoc && nestedSchema.classDoc !== sectionTitle + ? nestedSchema.classDoc + : undefined + + // 一级嵌套:使用 Card 包裹,清晰的 section 边界 + if (level === 0) { + return ( + + +
+ {renderSectionIcon(nestedSchema.uiIcon)} + {sectionTitle} +
+ {sectionDescription && ( + {sectionDescription} + )} +
+ + ) || {}} + onChange={(field, value) => onChange(`${key}.${field}`, value)} + basePath={nestedFieldPath} + hooks={hooks} + level={level + 1} + /> + +
+ ) + } + + // 二级及更深嵌套:使用左侧指示条 + 轻量分组 + return ( +
+
+
+ {renderSectionIcon(nestedSchema.uiIcon)} +

{sectionTitle}

+
+ {sectionDescription && ( +

+ {sectionDescription} +

+ )} +
+ + ) || {}} + onChange={(field, value) => onChange(`${key}.${field}`, value)} + basePath={nestedFieldPath} + hooks={hooks} + level={level + 1} + /> +
+ ) + })}
) } diff --git a/dashboard/src/components/dynamic-form/DynamicField.tsx b/dashboard/src/components/dynamic-form/DynamicField.tsx index 875ec721..b1e4dc59 100644 --- a/dashboard/src/components/dynamic-form/DynamicField.tsx +++ b/dashboard/src/components/dynamic-form/DynamicField.tsx @@ -2,6 +2,7 @@ import * as React from "react" import * as LucideIcons from "lucide-react" import { Input } from "@/components/ui/input" +import { KeyValueEditor } from "@/components/ui/key-value-editor" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Slider } from "@/components/ui/slider" @@ -29,6 +30,57 @@ export const DynamicField: React.FC = ({ value, onChange, }) => { + const renderPrimitiveArrayEditor = () => { + const itemType = schema.items?.type ?? 'string' + const arrayValue = Array.isArray(value) + ? value + : Array.isArray(schema.default) + ? schema.default + : [] + + const textareaValue = arrayValue.map((item) => String(item ?? '')).join('\n') + + return ( +