{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "ai-config-editor", "title": "AI Config Editor", "description": "Controlled configuration editor shell with form/JSON modes, validation fallback, secret, collection, map, and JSON field primitives", "dependencies": ["daisyui", "lucide-react"], "files": [ { "path": "registry/default/ui/ai-config-editor/ai-config-types.ts", "content": "import type { ComponentType, ReactNode } from 'react';\n\nexport type AiConfigEditorMode = 'form' | 'json';\nexport type AiConfigValidationMode = 'change' | 'submit';\n\nexport type AiConfigSafeParseResult = { success: true; data: T } | { success: false; error: unknown };\n\nexport type AiConfigSchema = {\n\tsafeParse: (value: unknown) => AiConfigSafeParseResult;\n};\n\nexport type AiConfigEditorIssue = {\n\tpath: string;\n\tmessage: string;\n};\n\nexport type AiConfigFieldSlotProps = {\n\tname: string;\n\tdraft: T;\n\tdisabled: boolean;\n\treadOnly: boolean;\n\terror?: string;\n\tdefaultField: ReactNode;\n\tonDraftChange: (value: T) => void;\n};\n\nexport type AiConfigEditorSlots = {\n\tbeforeForm?: ReactNode;\n\tafterForm?: ReactNode;\n\theaderActions?: ReactNode;\n\tfieldSlots?: Readonly>>>;\n\tsummary?: ComponentType;\n};\n\nexport type AiConfigSummarySlotProps = {\n\tissues: readonly AiConfigEditorIssue[];\n\tdefaultSummary: ReactNode;\n};\n\nexport type AiConfigDraftController = {\n\tdraft: T;\n\tdirty: boolean;\n\texternalInvalid: boolean;\n\texternalError?: string;\n\tissues: readonly AiConfigEditorIssue[];\n\tvisibleIssues: readonly AiConfigEditorIssue[];\n\tjsonText: string;\n\tjsonError?: string;\n\tcanSubmit: boolean;\n\tsetDraft: (value: T) => void;\n\tsetJsonText: (value: string) => void;\n\tapplyJson: () => boolean;\n\treset: () => void;\n\tsubmit: () => boolean;\n};\n\nexport type AiConfigEditorMessages = {\n\tformMode: string;\n\tjsonMode: string;\n\treset: string;\n\tsubmit: string;\n\tapplyJson: string;\n\tinvalidExternalTitle: string;\n\tinvalidExternalDescription: string;\n\tinvalidJson: string;\n\tvalidationTitle: string;\n\tvalidationCount: (count: number) => string;\n\tjsonLabel: string;\n\treadOnly: string;\n};\n\nexport type AiConfigEditorMessageOverrides = Partial;\n\nexport type AiConfigEditorOption = {\n\tvalue: string;\n\tlabel: string;\n\tdescription?: string;\n\tdisabled?: boolean;\n};\n\nexport type AiResourceEditorMessages = {\n\ttitle: string;\n\tdescription: string;\n\tfields: Record;\n\tsections: Record;\n\teditor?: AiConfigEditorMessageOverrides;\n};\n\nexport type AiResourceEditorMessageOverrides = {\n\ttitle?: string;\n\tdescription?: string;\n\tfields?: Partial>;\n\tsections?: Partial>;\n\teditor?: AiConfigEditorMessageOverrides;\n};\n\nexport function mergeResourceEditorMessages(\n\tdefaults: AiResourceEditorMessages,\n\toverrides?: AiResourceEditorMessageOverrides,\n): AiResourceEditorMessages {\n\treturn {\n\t\ttitle: overrides?.title ?? defaults.title,\n\t\tdescription: overrides?.description ?? defaults.description,\n\t\tfields: { ...defaults.fields, ...overrides?.fields },\n\t\tsections: { ...defaults.sections, ...overrides?.sections },\n\t\teditor: { ...defaults.editor, ...overrides?.editor },\n\t};\n}\n", "type": "registry:lib", "target": "@components/ui/ai-config-editor/ai-config-types.ts" }, { "path": "registry/default/ui/ai-config-editor/ai-config-messages.ts", "content": "import type { AiConfigEditorMessageOverrides, AiConfigEditorMessages } from './ai-config-types';\n\nexport const defaultAiConfigEditorMessages: AiConfigEditorMessages = {\n\tformMode: '表单',\n\tjsonMode: 'JSON',\n\treset: '重置',\n\tsubmit: '应用配置',\n\tapplyJson: '应用 JSON',\n\tinvalidExternalTitle: '外部配置无效',\n\tinvalidExternalDescription: '当前显示安全回退草稿;原始值未被修改。修正后才会发出变更。',\n\tinvalidJson: 'JSON 内容无法解析或不符合配置约束。',\n\tvalidationTitle: '请检查以下配置问题',\n\tvalidationCount: (count) => `${count} 个问题`,\n\tjsonLabel: '配置 JSON',\n\treadOnly: '只读',\n};\n\nexport function mergeAiConfigEditorMessages(overrides?: AiConfigEditorMessageOverrides): AiConfigEditorMessages {\n\treturn { ...defaultAiConfigEditorMessages, ...overrides };\n}\n", "type": "registry:lib", "target": "@components/ui/ai-config-editor/ai-config-messages.ts" }, { "path": "registry/default/ui/ai-config-editor/ai-config-validation.ts", "content": "import type { AiConfigEditorIssue, AiConfigSafeParseResult, AiConfigSchema } from './ai-config-types';\n\nexport function safeParseAiConfig(schema: AiConfigSchema, value: unknown): AiConfigSafeParseResult {\n\ttry {\n\t\treturn schema.safeParse(value);\n\t} catch (error) {\n\t\treturn { success: false, error };\n\t}\n}\n\nexport function aiConfigIssues(error: unknown): AiConfigEditorIssue[] {\n\tconst issueValues = readIssueArray(error);\n\tif (!issueValues) return [{ path: '', message: errorMessage(error) }];\n\tconst issues = issueValues.map(toEditorIssue).filter((issue): issue is AiConfigEditorIssue => Boolean(issue));\n\treturn issues.length ? issues : [{ path: '', message: errorMessage(error) }];\n}\n\nexport function aiConfigFieldError(issues: readonly AiConfigEditorIssue[], path: string): string | undefined {\n\treturn issues.find((issue) => issue.path === path || issue.path.startsWith(`${path}.`))?.message;\n}\n\nexport function stringifyAiConfig(value: unknown, indentation = 2): string {\n\ttry {\n\t\treturn JSON.stringify(value, null, indentation) ?? 'null';\n\t} catch {\n\t\treturn '';\n\t}\n}\n\nexport function compactAiConfig(value: unknown): string {\n\treturn stringifyAiConfig(value, 0);\n}\n\nfunction readIssueArray(error: unknown): unknown[] | undefined {\n\tif (!isRecord(error) || !Array.isArray(error.issues)) return undefined;\n\treturn error.issues;\n}\n\nfunction toEditorIssue(value: unknown): AiConfigEditorIssue | undefined {\n\tif (!isRecord(value)) return undefined;\n\tconst path = Array.isArray(value.path) ? value.path.map(String).join('.') : '';\n\tconst message = typeof value.message === 'string' ? value.message : '配置值无效';\n\treturn { path, message };\n}\n\nfunction errorMessage(error: unknown): string {\n\tif (error instanceof Error && error.message) return error.message;\n\tif (typeof error === 'string' && error) return error;\n\treturn '配置值无效';\n}\n\nfunction isRecord(value: unknown): value is Record {\n\treturn typeof value === 'object' && value !== null;\n}\n", "type": "registry:lib", "target": "@components/ui/ai-config-editor/ai-config-validation.ts" }, { "path": "registry/default/ui/ai-config-editor/use-ai-config-draft.ts", "content": "'use client';\n\nimport { useCallback, useEffect, useMemo, useState } from 'react';\nimport type {\n\tAiConfigDraftController,\n\tAiConfigEditorIssue,\n\tAiConfigSchema,\n\tAiConfigValidationMode,\n} from './ai-config-types';\nimport { aiConfigIssues, compactAiConfig, safeParseAiConfig, stringifyAiConfig } from './ai-config-validation';\n\nexport type UseAiConfigDraftOptions = {\n\tvalue: T;\n\tfallbackValue: T;\n\tschema: AiConfigSchema;\n\tonChange: (value: T) => void;\n\tonSubmit?: (value: T) => void;\n\tvalidationMode?: AiConfigValidationMode;\n};\n\ntype InitialDraft = {\n\tdraft: T;\n\texternalInvalid: boolean;\n\texternalError?: string;\n\tissues: AiConfigEditorIssue[];\n};\n\nexport function useAiConfigDraft({\n\tvalue,\n\tfallbackValue,\n\tschema,\n\tonChange,\n\tonSubmit,\n\tvalidationMode = 'change',\n}: UseAiConfigDraftOptions): AiConfigDraftController {\n\tconst initial = useMemo(() => initialDraft(schema, value, fallbackValue), [fallbackValue, schema, value]);\n\tconst [draft, setDraftState] = useState(initial.draft);\n\tconst [externalInvalid, setExternalInvalid] = useState(initial.externalInvalid);\n\tconst [externalError, setExternalError] = useState(initial.externalError);\n\tconst [issues, setIssues] = useState(initial.issues);\n\tconst [submitted, setSubmitted] = useState(false);\n\tconst [jsonText, setJsonTextState] = useState(() => stringifyAiConfig(initial.draft));\n\tconst [jsonError, setJsonError] = useState();\n\tconst externalDraft = initial.draft;\n\n\tuseEffect(() => {\n\t\tsetDraftState(initial.draft);\n\t\tsetExternalInvalid(initial.externalInvalid);\n\t\tsetExternalError(initial.externalError);\n\t\tsetIssues(initial.issues);\n\t\tsetJsonTextState(stringifyAiConfig(initial.draft));\n\t\tsetJsonError(undefined);\n\t\tsetSubmitted(false);\n\t}, [initial]);\n\n\tconst setDraft = useCallback(\n\t\t(next: T) => {\n\t\t\tsetDraftState(next);\n\t\t\tsetJsonTextState(stringifyAiConfig(next));\n\t\t\tsetJsonError(undefined);\n\t\t\tconst parsed = safeParseAiConfig(schema, next);\n\t\t\tif (!parsed.success) {\n\t\t\t\tsetIssues(aiConfigIssues(parsed.error));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsetIssues([]);\n\t\t\tonChange(parsed.data);\n\t\t},\n\t\t[onChange, schema],\n\t);\n\n\tconst setJsonText = useCallback((next: string) => {\n\t\tsetJsonTextState(next);\n\t\tsetJsonError(undefined);\n\t}, []);\n\n\tconst applyJson = useCallback(() => {\n\t\tlet candidate: unknown;\n\t\ttry {\n\t\t\tcandidate = JSON.parse(jsonText);\n\t\t} catch (error) {\n\t\t\tsetJsonError(error instanceof Error ? error.message : 'JSON parse failed');\n\t\t\treturn false;\n\t\t}\n\t\tconst parsed = safeParseAiConfig(schema, candidate);\n\t\tif (!parsed.success) {\n\t\t\tconst nextIssues = aiConfigIssues(parsed.error);\n\t\t\tsetIssues(nextIssues);\n\t\t\tsetJsonError(nextIssues[0]?.message ?? 'Invalid configuration');\n\t\t\treturn false;\n\t\t}\n\t\tsetDraftState(parsed.data);\n\t\tsetIssues([]);\n\t\tsetJsonError(undefined);\n\t\tsetJsonTextState(stringifyAiConfig(parsed.data));\n\t\tonChange(parsed.data);\n\t\treturn true;\n\t}, [jsonText, onChange, schema]);\n\n\tconst reset = useCallback(() => {\n\t\tsetDraftState(externalDraft);\n\t\tsetIssues(initial.issues);\n\t\tsetJsonTextState(stringifyAiConfig(externalDraft));\n\t\tsetJsonError(undefined);\n\t\tsetSubmitted(false);\n\t}, [externalDraft, initial.issues]);\n\n\tconst submit = useCallback(() => {\n\t\tsetSubmitted(true);\n\t\tconst parsed = safeParseAiConfig(schema, draft);\n\t\tif (!parsed.success) {\n\t\t\tsetIssues(aiConfigIssues(parsed.error));\n\t\t\treturn false;\n\t\t}\n\t\tsetIssues([]);\n\t\tonSubmit?.(parsed.data);\n\t\treturn true;\n\t}, [draft, onSubmit, schema]);\n\n\tconst dirty = compactAiConfig(draft) !== compactAiConfig(externalDraft);\n\tconst visibleIssues = validationMode === 'change' || submitted ? issues : [];\n\treturn {\n\t\tdraft,\n\t\tdirty,\n\t\texternalInvalid,\n\t\texternalError,\n\t\tissues,\n\t\tvisibleIssues,\n\t\tjsonText,\n\t\tjsonError,\n\t\tcanSubmit: issues.length === 0,\n\t\tsetDraft,\n\t\tsetJsonText,\n\t\tapplyJson,\n\t\treset,\n\t\tsubmit,\n\t};\n}\n\nfunction initialDraft(schema: AiConfigSchema, value: T, fallbackValue: T): InitialDraft {\n\tconst parsed = safeParseAiConfig(schema, value);\n\tif (parsed.success) return { draft: parsed.data, externalInvalid: false, issues: [] };\n\tconst fallback = safeParseAiConfig(schema, fallbackValue);\n\tconst issues = aiConfigIssues(parsed.error);\n\treturn {\n\t\tdraft: fallback.success ? fallback.data : fallbackValue,\n\t\texternalInvalid: true,\n\t\texternalError: issues[0]?.message,\n\t\tissues,\n\t};\n}\n", "type": "registry:hook", "target": "@components/ui/ai-config-editor/use-ai-config-draft.ts" }, { "path": "registry/default/ui/ai-config-editor/use-stable-list-entries.ts", "content": "'use client';\n\nimport { useLayoutEffect, useState } from 'react';\n\nexport type StableListEntry = { item: T; key: string };\n\nexport function useStableListEntries(items: readonly T[], prefix = 'item'): StableListEntry[] {\n\tconst [committed, setCommitted] = useState>(() =>\n\t\tprojectStableList(emptyStableList(prefix), items, prefix),\n\t);\n\tconst projected = projectStableList(committed, items, prefix);\n\tuseLayoutEffect(() => {\n\t\tsetCommitted((current) =>\n\t\t\tsameStableList(current, items, prefix) ? current : projectStableList(current, items, prefix),\n\t\t);\n\t}, [items, prefix]);\n\treturn items.map((item, index) => ({ item, key: projected.keys[index] }));\n}\n\ntype StableListState = {\n\titems: readonly T[];\n\tkeys: readonly string[];\n\tprefix: string;\n\tsequence: number;\n};\n\nfunction emptyStableList(prefix: string): StableListState {\n\treturn { items: [], keys: [], prefix, sequence: 0 };\n}\n\nfunction projectStableList(previous: StableListState, items: readonly T[], prefix: string): StableListState {\n\tconst keys: Array = new Array(items.length);\n\tconst usedPrevious = new Set();\n\tlet sequence = previous.sequence;\n\n\tfor (const [index, item] of items.entries()) {\n\t\tconst previousIndex = previous.items.findIndex(\n\t\t\t(candidate, candidateIndex) => !usedPrevious.has(candidateIndex) && Object.is(candidate, item),\n\t\t);\n\t\tif (previousIndex < 0) continue;\n\t\tkeys[index] = previous.keys[previousIndex];\n\t\tusedPrevious.add(previousIndex);\n\t}\n\n\tfor (const index of items.keys()) {\n\t\tif (keys[index]) continue;\n\t\tif (index < previous.keys.length && !usedPrevious.has(index)) {\n\t\t\tkeys[index] = previous.keys[index];\n\t\t\tusedPrevious.add(index);\n\t\t} else {\n\t\t\tkeys[index] = `${prefix}-${++sequence}`;\n\t\t}\n\t}\n\n\treturn { items: [...items], keys: keys as string[], prefix, sequence };\n}\n\nfunction sameStableList(state: StableListState, items: readonly T[], prefix: string) {\n\treturn (\n\t\tstate.prefix === prefix &&\n\t\tstate.items.length === items.length &&\n\t\tstate.items.every((item, index) => Object.is(item, items[index]))\n\t);\n}\n", "type": "registry:hook", "target": "@components/ui/ai-config-editor/use-stable-list-entries.ts" }, { "path": "registry/default/ui/ai-config-editor/ai-config-fields.tsx", "content": "'use client';\n\nimport { Eye, EyeOff, X } from 'lucide-react';\nimport type { ComponentPropsWithRef, ReactNode } from 'react';\nimport { useEffect, useId, useState } from 'react';\nimport type { AiConfigEditorOption } from './ai-config-types';\n\nexport type AiConfigFieldBase = {\n\tlabel: string;\n\tdescription?: string;\n\terror?: string;\n\trequired?: boolean;\n\twrapperClassName?: string;\n};\n\nexport type AiConfigTextFieldProps = Omit, 'value' | 'onChange' | 'type'> &\n\tAiConfigFieldBase & {\n\t\tvalue?: string;\n\t\tonValueChange: (value: string) => void;\n\t\ttype?: 'text' | 'url' | 'date' | 'email';\n\t};\n\nexport function AiConfigTextField({\n\tlabel,\n\tdescription,\n\terror,\n\trequired,\n\twrapperClassName,\n\tvalue = '',\n\tonValueChange,\n\tclassName,\n\tid: providedId,\n\t...props\n}: AiConfigTextFieldProps) {\n\tconst generatedId = useId();\n\tconst id = providedId ?? generatedId;\n\tconst errorId = error ? `${id}-error` : undefined;\n\treturn (\n\t\t\n\t\t\t onValueChange(event.target.value)}\n\t\t\t\t{...props}\n\t\t\t/>\n\t\t\n\t);\n}\n\nexport type AiConfigTextareaFieldProps = Omit, 'value' | 'onChange'> &\n\tAiConfigFieldBase & {\n\t\tvalue?: string;\n\t\tonValueChange: (value: string) => void;\n\t};\n\nexport function AiConfigTextareaField({\n\tlabel,\n\tdescription,\n\terror,\n\trequired,\n\twrapperClassName,\n\tvalue = '',\n\tonValueChange,\n\tclassName,\n\tid: providedId,\n\trows = 4,\n\t...props\n}: AiConfigTextareaFieldProps) {\n\tconst generatedId = useId();\n\tconst id = providedId ?? generatedId;\n\tconst errorId = error ? `${id}-error` : undefined;\n\treturn (\n\t\t\n\t\t\t onValueChange(event.target.value)}\n\t\t\t\t{...props}\n\t\t\t/>\n\t\t\n\t);\n}\n\nexport type AiConfigSelectFieldProps = Omit, 'value' | 'onChange'> &\n\tAiConfigFieldBase & {\n\t\tvalue?: string;\n\t\tonValueChange: (value: string) => void;\n\t\toptions: readonly AiConfigEditorOption[];\n\t\tplaceholder?: string;\n\t};\n\nexport function AiConfigSelectField({\n\tlabel,\n\tdescription,\n\terror,\n\trequired,\n\twrapperClassName,\n\tvalue = '',\n\tonValueChange,\n\toptions,\n\tplaceholder = '请选择',\n\tclassName,\n\tid: providedId,\n\t...props\n}: AiConfigSelectFieldProps) {\n\tconst generatedId = useId();\n\tconst id = providedId ?? generatedId;\n\tconst errorId = error ? `${id}-error` : undefined;\n\tconst known = !value || options.some((option) => option.value === value);\n\treturn (\n\t\t\n\t\t\t onValueChange(event.target.value)}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t\n\t\t\t\t{!known ? : null}\n\t\t\t\t{options.map((option) => (\n\t\t\t\t\t\n\t\t\t\t))}\n\t\t\t\n\t\t\n\t);\n}\n\nexport type AiConfigNumberFieldProps = Omit, 'value' | 'onChange' | 'type'> &\n\tAiConfigFieldBase & {\n\t\tvalue?: number | null;\n\t\tonValueChange: (value: number | undefined) => void;\n\t\tinteger?: boolean;\n\t};\n\nexport function AiConfigNumberField({\n\tlabel,\n\tdescription,\n\terror,\n\trequired,\n\twrapperClassName,\n\tvalue,\n\tonValueChange,\n\tinteger = false,\n\tclassName,\n\tid: providedId,\n\t...props\n}: AiConfigNumberFieldProps) {\n\tconst generatedId = useId();\n\tconst id = providedId ?? generatedId;\n\tconst external = value === undefined || value === null ? '' : String(value);\n\tconst [draft, setDraft] = useState(external);\n\tuseEffect(() => setDraft(external), [external]);\n\tconst issueId = error ? `${id}-error` : undefined;\n\treturn (\n\t\t\n\t\t\t {\n\t\t\t\t\tconst next = event.target.value;\n\t\t\t\t\tsetDraft(next);\n\t\t\t\t\tif (next === '') onValueChange(undefined);\n\t\t\t\t\telse {\n\t\t\t\t\t\tconst number = Number(next);\n\t\t\t\t\t\tif (Number.isFinite(number) && (!integer || Number.isInteger(number))) onValueChange(number);\n\t\t\t\t\t}\n\t\t\t\t}}\n\t\t\t\t{...props}\n\t\t\t/>\n\t\t\n\t);\n}\n\nexport type AiConfigToggleFieldProps = Omit, 'checked' | 'onChange' | 'type'> &\n\tAiConfigFieldBase & {\n\t\tchecked: boolean;\n\t\tonCheckedChange: (checked: boolean) => void;\n\t};\n\nexport function AiConfigToggleField({\n\tlabel,\n\tdescription,\n\terror,\n\twrapperClassName,\n\tchecked,\n\tonCheckedChange,\n\tclassName,\n\tid: providedId,\n\t...props\n}: AiConfigToggleFieldProps) {\n\tconst generatedId = useId();\n\tconst id = providedId ?? generatedId;\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t{label}\n\t\t\t\t\t{description ? {description} : null}\n\t\t\t\t\n\t\t\t\t onCheckedChange(event.target.checked)}\n\t\t\t\t\t{...props}\n\t\t\t\t/>\n\t\t\t\n\t\t\t{error ? (\n\t\t\t\t

\n\t\t\t\t\t{error}\n\t\t\t\t

\n\t\t\t) : null}\n\t\t
\n\t);\n}\n\nexport type AiConfigSecretFieldProps = Omit & {\n\tclearLabel?: string;\n\trevealLabel?: string;\n\thideLabel?: string;\n\trevealIdentity?: number | string;\n};\n\nexport function AiConfigSecretField({\n\tlabel,\n\tvalue = '',\n\tonValueChange,\n\tclearLabel = '清除密钥',\n\trevealLabel = '显示密钥',\n\thideLabel = '隐藏密钥',\n\trevealIdentity,\n\tdescription,\n\terror,\n\trequired,\n\twrapperClassName,\n\tclassName,\n\tid: providedId,\n\treadOnly,\n\tdisabled,\n\t...inputProps\n}: AiConfigSecretFieldProps) {\n\tconst [revealed, setRevealed] = useState(false);\n\tuseEffect(() => setRevealed(false), [revealIdentity]);\n\tconst generatedId = useId();\n\tconst id = providedId ?? generatedId;\n\tconst errorId = error ? `${id}-error` : undefined;\n\treturn (\n\t\t\n\t\t\t
\n\t\t\t\t onValueChange(event.target.value)}\n\t\t\t\t\t{...inputProps}\n\t\t\t\t/>\n\t\t\t\t setRevealed((current) => !current)}\n\t\t\t\t>\n\t\t\t\t\t{revealed ?
\n\t\t\n\t);\n}\n\nexport type AiConfigFieldFrameProps = ComponentPropsWithRef<'div'> & {\n\tlabel: string;\n\tdescription?: string;\n\terror?: string;\n\terrorId?: string;\n\trequired?: boolean;\n\thtmlFor?: string;\n\tchildren: ReactNode;\n};\n\nexport function AiConfigFieldFrame({\n\tlabel,\n\tdescription,\n\terror,\n\terrorId,\n\trequired,\n\thtmlFor,\n\tchildren,\n\tclassName,\n\t...props\n}: AiConfigFieldFrameProps) {\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t{description ?

{description}

: null}\n\t\t\t{children}\n\t\t\t{error ? (\n\t\t\t\t\n\t\t\t) : null}\n\t\t
\n\t);\n}\n", "type": "registry:component", "target": "@components/ui/ai-config-editor/ai-config-fields.tsx" }, { "path": "registry/default/ui/ai-config-editor/ai-config-collections.tsx", "content": "'use client';\n\nimport { Plus, Trash2, X } from 'lucide-react';\nimport type { ComponentPropsWithRef, KeyboardEvent } from 'react';\nimport { useEffect, useId, useRef, useState } from 'react';\nimport { AiConfigFieldFrame } from './ai-config-fields';\nimport { useStableListEntries } from './use-stable-list-entries';\n\nexport type AiConfigStringListFieldProps = Omit, 'onChange'> & {\n\tlabel: string;\n\tvalue?: readonly string[];\n\tonChange: (value: string[]) => void;\n\tdescription?: string;\n\terror?: string;\n\tplaceholder?: string;\n\taddLabel?: string;\n\tremoveLabel?: (value: string) => string;\n\tdisabled?: boolean;\n\treadOnly?: boolean;\n\tmaxItems?: number;\n};\n\nexport function AiConfigStringListField({\n\tlabel,\n\tvalue = [],\n\tonChange,\n\tdescription,\n\terror,\n\tplaceholder = '输入后按回车',\n\taddLabel = '添加',\n\tremoveLabel = (item) => `移除 ${item}`,\n\tdisabled = false,\n\treadOnly = false,\n\tmaxItems = 128,\n\tclassName,\n\t...props\n}: AiConfigStringListFieldProps) {\n\tconst id = useId();\n\tconst composing = useRef(false);\n\tconst [draft, setDraft] = useState('');\n\tconst entries = useStableListEntries(value, 'string-list-item');\n\tconst add = () => {\n\t\tconst next = draft.trim();\n\t\tif (!next || value.includes(next) || value.length >= maxItems) return;\n\t\tonChange([...value, next]);\n\t\tsetDraft('');\n\t};\n\treturn (\n\t\t\n\t\t\t{!readOnly ? (\n\t\t\t\t
\n\t\t\t\t\t= maxItems}\n\t\t\t\t\t\tonChange={(event) => setDraft(event.target.value)}\n\t\t\t\t\t\tonCompositionStart={() => {\n\t\t\t\t\t\t\tcomposing.current = true;\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonCompositionEnd={() => {\n\t\t\t\t\t\t\tcomposing.current = false;\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonKeyDown={(event) => submitOnEnter(event, add, composing.current)}\n\t\t\t\t\t/>\n\t\t\t\t\t= maxItems}\n\t\t\t\t\t\tonClick={add}\n\t\t\t\t\t>\n\t\t\t\t\t\t
\n\t\t\t) : null}\n\t\t\t
\n\t\t\t\t{value.length ? (\n\t\t\t\t\tentries.map(({ item, key }, index) => (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{item}\n\t\t\t\t\t\t\t{!readOnly ? (\n\t\t\t\t\t\t\t\t onChange(value.filter((_, itemIndex) => itemIndex !== index))}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t))\n\t\t\t\t) : (\n\t\t\t\t\t暂无条目\n\t\t\t\t)}\n\t\t\t
\n\t\t\n\t);\n}\n\nexport type AiConfigKeyValueFieldProps = Omit, 'onChange'> & {\n\tlabel: string;\n\tvalue?: Readonly>;\n\tonChange: (value: Record) => void;\n\tdescription?: string;\n\terror?: string;\n\tkeyLabel?: string;\n\tvalueLabel?: string;\n\taddLabel?: string;\n\tremoveLabel?: (key: string) => string;\n\tdisabled?: boolean;\n\treadOnly?: boolean;\n\tmaxItems?: number;\n};\n\nexport function AiConfigKeyValueField({\n\tlabel,\n\tvalue = {},\n\tonChange,\n\tdescription,\n\terror,\n\tkeyLabel = '键',\n\tvalueLabel = '值',\n\taddLabel = '添加键值',\n\tremoveLabel = (key) => `移除 ${key}`,\n\tdisabled = false,\n\treadOnly = false,\n\tmaxItems = 128,\n\tclassName,\n\t...props\n}: AiConfigKeyValueFieldProps) {\n\tconst composing = useRef(false);\n\tconst [newKey, setNewKey] = useState('');\n\tconst [newValue, setNewValue] = useState('');\n\tconst entries = Object.entries(value);\n\tconst add = () => {\n\t\tconst key = newKey.trim();\n\t\tif (!key || Object.hasOwn(value, key) || entries.length >= maxItems) return;\n\t\tonChange(withAiConfigMapEntry(value, key, newValue));\n\t\tsetNewKey('');\n\t\tsetNewValue('');\n\t};\n\treturn (\n\t\t\n\t\t\t
\n\t\t\t\t{entries.map(([key, entryValue]) => (\n\t\t\t\t\t {\n\t\t\t\t\t\t\tif (!nextKey || (nextKey !== key && Object.hasOwn(value, nextKey))) return;\n\t\t\t\t\t\t\tonChange(withAiConfigMapEntry(value, nextKey, nextValue, key));\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonRemove={() => onChange(withoutAiConfigMapEntry(value, key))}\n\t\t\t\t\t\tremoveLabel={removeLabel(key)}\n\t\t\t\t\t/>\n\t\t\t\t))}\n\t\t\t
\n\t\t\t{!readOnly ? (\n\t\t\t\t
\n\t\t\t\t\t= maxItems}\n\t\t\t\t\t\tonChange={(event) => setNewKey(event.target.value)}\n\t\t\t\t\t/>\n\t\t\t\t\t= maxItems}\n\t\t\t\t\t\tonChange={(event) => setNewValue(event.target.value)}\n\t\t\t\t\t\tonCompositionStart={() => {\n\t\t\t\t\t\t\tcomposing.current = true;\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonCompositionEnd={() => {\n\t\t\t\t\t\t\tcomposing.current = false;\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonKeyDown={(event) => submitOnEnter(event, add, composing.current)}\n\t\t\t\t\t/>\n\t\t\t\t\t= maxItems}\n\t\t\t\t\t\tonClick={add}\n\t\t\t\t\t>\n\t\t\t\t\t\t
\n\t\t\t) : null}\n\t\t
\n\t);\n}\n\ntype AiConfigMapRowProps = {\n\tentryKey: string;\n\tentryValue: string;\n\tkeyLabel: string;\n\tvalueLabel: string;\n\tdisabled: boolean;\n\treadOnly: boolean;\n\tremoveLabel: string;\n\tonCommit: (key: string, value: string) => void;\n\tonRemove: () => void;\n};\n\nfunction AiConfigMapRow({\n\tentryKey,\n\tentryValue,\n\tkeyLabel,\n\tvalueLabel,\n\tdisabled,\n\treadOnly,\n\tremoveLabel,\n\tonCommit,\n\tonRemove,\n}: AiConfigMapRowProps) {\n\tconst [keyDraft, setKeyDraft] = useState(entryKey);\n\tconst [valueDraft, setValueDraft] = useState(entryValue);\n\tuseEffect(() => setKeyDraft(entryKey), [entryKey]);\n\tuseEffect(() => setValueDraft(entryValue), [entryValue]);\n\tif (readOnly)\n\t\treturn (\n\t\t\t
\n\t\t\t\t{entryKey}\n\t\t\t\t{entryValue}\n\t\t\t
\n\t\t);\n\treturn (\n\t\t
\n\t\t\t setKeyDraft(event.target.value)}\n\t\t\t\tonBlur={() => onCommit(keyDraft.trim(), valueDraft)}\n\t\t\t/>\n\t\t\t {\n\t\t\t\t\tconst next = event.target.value;\n\t\t\t\t\tsetValueDraft(next);\n\t\t\t\t\tonCommit(keyDraft.trim(), next);\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t\n\t\t\t\t
\n\t);\n}\n\nexport function withAiConfigMapEntry(\n\tvalue: Readonly>,\n\tkey: string,\n\tentryValue: string,\n\tpreviousKey?: string,\n): Record {\n\tconst next = copyAiConfigMap(value, previousKey);\n\tObject.defineProperty(next, key, { value: entryValue, enumerable: true, configurable: true, writable: true });\n\treturn next;\n}\n\nexport function withoutAiConfigMapEntry(value: Readonly>, key: string): Record {\n\treturn copyAiConfigMap(value, key);\n}\n\nfunction copyAiConfigMap(value: Readonly>, omittedKey?: string): Record {\n\tconst next = Object.create(null) as Record;\n\tfor (const [key, entryValue] of Object.entries(value)) {\n\t\tif (key === omittedKey) continue;\n\t\tObject.defineProperty(next, key, { value: entryValue, enumerable: true, configurable: true, writable: true });\n\t}\n\treturn next;\n}\n\nfunction submitOnEnter(event: KeyboardEvent, submit: () => void, composing: boolean) {\n\tif (event.key !== 'Enter' || composing || event.nativeEvent.isComposing) return;\n\tevent.preventDefault();\n\tsubmit();\n}\n", "type": "registry:component", "target": "@components/ui/ai-config-editor/ai-config-collections.tsx" }, { "path": "registry/default/ui/ai-config-editor/ai-config-layout.tsx", "content": "import type { ComponentPropsWithRef, ReactNode } from 'react';\n\nexport type AiConfigFieldGridProps = ComponentPropsWithRef<'div'>;\n\nexport function AiConfigFieldGrid({ className, ...props }: AiConfigFieldGridProps) {\n\treturn
;\n}\n\nexport type AiConfigFormSectionProps = ComponentPropsWithRef<'section'> & {\n\ttitle: ReactNode;\n\tdescription?: ReactNode;\n\tactions?: ReactNode;\n};\n\nexport function AiConfigFormSection({\n\ttitle,\n\tdescription,\n\tactions,\n\tchildren,\n\tclassName,\n\t...props\n}: AiConfigFormSectionProps) {\n\treturn (\n\t\t\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t

{title}

\n\t\t\t\t\t{description ?
{description}
: null}\n\t\t\t\t
\n\t\t\t\t{actions ?
{actions}
: null}\n\t\t\t
\n\t\t\t{children}\n\t\t\n\t);\n}\n", "type": "registry:component", "target": "@components/ui/ai-config-editor/ai-config-layout.tsx" }, { "path": "registry/default/ui/ai-config-editor/json-value-editor.tsx", "content": "'use client';\n\nimport { Check, RotateCcw } from 'lucide-react';\nimport type { ComponentPropsWithRef } from 'react';\nimport { useEffect, useState } from 'react';\nimport { stringifyAiConfig } from './ai-config-validation';\n\nexport type JsonValueEditorProps = Omit, 'onChange'> & {\n\tlabel: string;\n\tvalue: unknown;\n\tonChange: (value: unknown) => void;\n\tdescription?: string;\n\tdisabled?: boolean;\n\treadOnly?: boolean;\n\terror?: string;\n\trows?: number;\n\tapplyLabel?: string;\n\tresetLabel?: string;\n};\n\nexport function JsonValueEditor({\n\tlabel,\n\tvalue,\n\tonChange,\n\tdescription,\n\tdisabled = false,\n\treadOnly = false,\n\terror,\n\trows = 7,\n\tapplyLabel = '应用 JSON',\n\tresetLabel = '重置 JSON',\n\tclassName,\n\t...props\n}: JsonValueEditorProps) {\n\tconst serialized = stringifyAiConfig(value);\n\tconst [draft, setDraft] = useState(serialized);\n\tconst [parseError, setParseError] = useState();\n\tuseEffect(() => {\n\t\tsetDraft(serialized);\n\t\tsetParseError(undefined);\n\t}, [serialized]);\n\tconst issue = parseError ?? error;\n\tconst apply = () => {\n\t\ttry {\n\t\t\tonChange(JSON.parse(draft));\n\t\t\tsetParseError(undefined);\n\t\t} catch (caught) {\n\t\t\tsetParseError(caught instanceof Error ? caught.message : 'JSON 内容无效');\n\t\t}\n\t};\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t{description ?

{description}

: null}\n\t\t\t\t
\n\t\t\t\t{!readOnly ? (\n\t\t\t\t\t
\n\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\tsetDraft(serialized);\n\t\t\t\t\t\t\t\tsetParseError(undefined);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t
\n\t\t\t\t) : null}\n\t\t\t
\n\t\t\t {\n\t\t\t\t\tsetDraft(event.target.value);\n\t\t\t\t\tsetParseError(undefined);\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t{issue ? (\n\t\t\t\t

\n\t\t\t\t\t{issue}\n\t\t\t\t

\n\t\t\t) : null}\n\t\t
\n\t);\n}\n", "type": "registry:component", "target": "@components/ui/ai-config-editor/json-value-editor.tsx" }, { "path": "registry/default/ui/ai-config-editor/ai-config-editor.tsx", "content": "'use client';\n\nimport { Braces, FilePenLine, RotateCcw, Save } from 'lucide-react';\nimport type { ComponentPropsWithRef, ReactNode } from 'react';\nimport { useId, useState } from 'react';\nimport { mergeAiConfigEditorMessages } from './ai-config-messages';\nimport type {\n\tAiConfigDraftController,\n\tAiConfigEditorMessageOverrides,\n\tAiConfigEditorMode,\n\tAiConfigEditorSlots,\n\tAiConfigFieldSlotProps,\n} from './ai-config-types';\n\nexport type AiConfigEditorProps = Omit, 'children' | 'title'> & {\n\ttitle: ReactNode;\n\tdescription?: ReactNode;\n\tcontroller: AiConfigDraftController;\n\tchildren: ReactNode;\n\tmessages?: AiConfigEditorMessageOverrides;\n\tslots?: AiConfigEditorSlots;\n\tdisabled?: boolean;\n\treadOnly?: boolean;\n\tshowSubmit?: boolean;\n\tinitialMode?: AiConfigEditorMode;\n};\n\nexport function AiConfigEditor({\n\ttitle,\n\tdescription,\n\tcontroller,\n\tchildren,\n\tmessages: messageOverrides,\n\tslots,\n\tdisabled = false,\n\treadOnly = false,\n\tshowSubmit = false,\n\tinitialMode = 'form',\n\tclassName,\n\t...props\n}: AiConfigEditorProps) {\n\tconst messages = mergeAiConfigEditorMessages(messageOverrides);\n\tconst jsonId = useId();\n\tconst [mode, setMode] = useState(initialMode);\n\tconst issues = controller.visibleIssues;\n\tconst Summary = slots?.summary;\n\tconst defaultSummary = issues.length ? (\n\t\t
\n\t\t\t
\n\t\t\t\t{messages.validationTitle}\n\t\t\t\t{messages.validationCount(issues.length)}\n\t\t\t
\n\t\t\t
    \n\t\t\t\t{issues.slice(0, 8).map((issue) => (\n\t\t\t\t\t
  • \n\t\t\t\t\t\t{issue.path ? `${issue.path}: ` : ''}\n\t\t\t\t\t\t{issue.message}\n\t\t\t\t\t
  • \n\t\t\t\t))}\n\t\t\t
\n\t\t
\n\t) : null;\n\treturn (\n\t\t\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t

{title}

\n\t\t\t\t\t\t{readOnly ? {messages.readOnly} : null}\n\t\t\t\t\t
\n\t\t\t\t\t{description ?
{description}
: null}\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t setMode('form')}>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t setMode('json')}>\n\t\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t{slots?.headerActions}\n\t\t\t\t\t{!readOnly ? (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n\t\t\t
\n\t\t\t
\n\t\t\t\t{controller.externalInvalid ? (\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
{messages.invalidExternalTitle}
\n\t\t\t\t\t\t\t
{messages.invalidExternalDescription}
\n\t\t\t\t\t\t\t{controller.externalError ? {controller.externalError} : null}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t) : null}\n\t\t\t\t{Summary ? : defaultSummary}\n\t\t\t\t{mode === 'form' ? (\n\t\t\t\t\t<>\n\t\t\t\t\t\t{slots?.beforeForm}\n\t\t\t\t\t\t
{children}
\n\t\t\t\t\t\t{slots?.afterForm}\n\t\t\t\t\t\n\t\t\t\t) : (\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t controller.setJsonText(event.target.value)}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{controller.jsonError ? (\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t\t\t{messages.invalidJson}: {controller.jsonError}\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t{!readOnly ? (\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t
\n\t\t\t\t)}\n\t\t\t
\n\t\t
\n\t);\n}\n\nexport type AiConfigFieldSlotPropsWithSlots = AiConfigFieldSlotProps & {\n\tslots?: AiConfigEditorSlots;\n};\n\nexport function AiConfigFieldSlot({ slots, ...props }: AiConfigFieldSlotPropsWithSlots) {\n\tconst Slot =\n\t\tslots?.fieldSlots && Object.hasOwn(slots.fieldSlots, props.name) ? slots.fieldSlots[props.name] : undefined;\n\treturn Slot ? : props.defaultField;\n}\n\nfunction ModeButton({\n\tactive,\n\tlabel,\n\tonClick,\n\tchildren,\n}: {\n\tactive: boolean;\n\tlabel: string;\n\tonClick: () => void;\n\tchildren: ReactNode;\n}) {\n\treturn (\n\t\t\n\t\t\t{children}\n\t\t\t{label}\n\t\t\n\t);\n}\n", "type": "registry:component", "target": "@components/ui/ai-config-editor/ai-config-editor.tsx" }, { "path": "registry/default/ui/ai-config-editor/index.ts", "content": "export * from './ai-config-collections';\nexport * from './ai-config-editor';\nexport * from './ai-config-fields';\nexport * from './ai-config-layout';\nexport * from './ai-config-messages';\nexport * from './ai-config-types';\nexport * from './ai-config-validation';\nexport * from './json-value-editor';\nexport * from './use-ai-config-draft';\nexport * from './use-stable-list-entries';\n", "type": "registry:component", "target": "@components/ui/ai-config-editor/index.ts" } ], "type": "registry:ui" }