| import { z, ZodArray, ZodError, ZodIssueCode } from 'zod'; |
| import { tConversationSchema, googleSettings as google, openAISettings as openAI } from './schemas'; |
| import type { ZodIssue } from 'zod'; |
| import type { TConversation, TSetOption, TPreset } from './schemas'; |
|
|
| export type GoogleSettings = Partial<typeof google>; |
| export type OpenAISettings = Partial<typeof google>; |
|
|
| export type ComponentType = |
| | 'input' |
| | 'textarea' |
| | 'slider' |
| | 'checkbox' |
| | 'switch' |
| | 'dropdown' |
| | 'combobox' |
| | 'tags'; |
|
|
| export type OptionType = 'conversation' | 'model' | 'custom'; |
|
|
| export type Option = Record<string, unknown> & { |
| label?: string; |
| value: string | number | null; |
| }; |
|
|
| export type OptionWithIcon = Option & { icon?: React.ReactNode }; |
|
|
| export enum ComponentTypes { |
| Input = 'input', |
| Textarea = 'textarea', |
| Slider = 'slider', |
| Checkbox = 'checkbox', |
| Switch = 'switch', |
| Dropdown = 'dropdown', |
| Combobox = 'combobox', |
| Tags = 'tags', |
| } |
|
|
| export enum SettingTypes { |
| Number = 'number', |
| Boolean = 'boolean', |
| String = 'string', |
| Enum = 'enum', |
| Array = 'array', |
| } |
|
|
| export enum OptionTypes { |
| Conversation = 'conversation', |
| Model = 'model', |
| Custom = 'custom', |
| } |
| export interface SettingDefinition { |
| key: string; |
| description?: string; |
| type: 'number' | 'boolean' | 'string' | 'enum' | 'array'; |
| default?: number | boolean | string | string[]; |
| showLabel?: boolean; |
| showDefault?: boolean; |
| options?: string[]; |
| range?: SettingRange; |
| enumMappings?: Record<string, number | boolean | string>; |
| component: ComponentType; |
| optionType?: OptionType; |
| columnSpan?: number; |
| columns?: number; |
| label?: string; |
| placeholder?: string; |
| labelCode?: boolean; |
| placeholderCode?: boolean; |
| descriptionCode?: boolean; |
| minText?: number; |
| maxText?: number; |
| minTags?: number; |
| maxTags?: number; |
| includeInput?: boolean; |
| descriptionSide?: 'top' | 'right' | 'bottom' | 'left'; |
| items?: OptionWithIcon[]; |
| searchPlaceholder?: string; |
| selectPlaceholder?: string; |
| searchPlaceholderCode?: boolean; |
| selectPlaceholderCode?: boolean; |
| } |
|
|
| export type DynamicSettingProps = Partial<SettingDefinition> & { |
| readonly?: boolean; |
| settingKey: string; |
| setOption: TSetOption; |
| conversation: Partial<TConversation> | Partial<TPreset> | null; |
| defaultValue?: number | boolean | string | string[]; |
| className?: string; |
| inputClassName?: string; |
| }; |
|
|
| const requiredSettingFields = ['key', 'type', 'component']; |
|
|
| export interface SettingRange { |
| min: number; |
| max: number; |
| step?: number; |
| } |
|
|
| export type SettingsConfiguration = SettingDefinition[]; |
|
|
| export function generateDynamicSchema(settings: SettingsConfiguration) { |
| const schemaFields: { [key: string]: z.ZodTypeAny } = {}; |
|
|
| for (const setting of settings) { |
| const { |
| key, |
| type, |
| default: defaultValue, |
| range, |
| options, |
| minText, |
| maxText, |
| minTags, |
| maxTags, |
| } = setting; |
|
|
| if (type === SettingTypes.Number) { |
| let schema = z.number(); |
| if (range) { |
| schema = schema.min(range.min); |
| schema = schema.max(range.max); |
| } |
| if (typeof defaultValue === 'number') { |
| schemaFields[key] = schema.default(defaultValue); |
| } else { |
| schemaFields[key] = schema; |
| } |
| continue; |
| } |
|
|
| if (type === SettingTypes.Boolean) { |
| const schema = z.boolean(); |
| if (typeof defaultValue === 'boolean') { |
| schemaFields[key] = schema.default(defaultValue); |
| } else { |
| schemaFields[key] = schema; |
| } |
| continue; |
| } |
|
|
| if (type === SettingTypes.String) { |
| let schema = z.string(); |
| if (minText) { |
| schema = schema.min(minText); |
| } |
| if (maxText) { |
| schema = schema.max(maxText); |
| } |
| if (typeof defaultValue === 'string') { |
| schemaFields[key] = schema.default(defaultValue); |
| } else { |
| schemaFields[key] = schema; |
| } |
| continue; |
| } |
|
|
| if (type === SettingTypes.Enum) { |
| if (!options || options.length === 0) { |
| console.warn(`Missing or empty 'options' for enum setting '${key}'.`); |
| continue; |
| } |
|
|
| const schema = z.enum(options as [string, ...string[]]); |
| if (typeof defaultValue === 'string') { |
| schemaFields[key] = schema.default(defaultValue); |
| } else { |
| schemaFields[key] = schema; |
| } |
| continue; |
| } |
|
|
| if (type === SettingTypes.Array) { |
| let schema: z.ZodSchema = z.array(z.string().or(z.number())); |
| if (minTags && schema instanceof ZodArray) { |
| schema = schema.min(minTags); |
| } |
| if (maxTags && schema instanceof ZodArray) { |
| schema = schema.max(maxTags); |
| } |
|
|
| if (defaultValue && Array.isArray(defaultValue)) { |
| schema = schema.default(defaultValue); |
| } |
|
|
| schemaFields[key] = schema; |
| continue; |
| } |
|
|
| console.warn(`Unsupported setting type: ${type}`); |
| } |
|
|
| return z.object(schemaFields); |
| } |
|
|
| const ZodTypeToSettingType: Record<string, string | undefined> = { |
| ZodString: 'string', |
| ZodNumber: 'number', |
| ZodBoolean: 'boolean', |
| }; |
|
|
| const minColumns = 1; |
| const maxColumns = 4; |
| const minSliderOptions = 2; |
| const minDropdownOptions = 2; |
| const minComboboxOptions = 2; |
|
|
| |
| |
| |
| |
| export function validateSettingDefinitions(settings: SettingsConfiguration): void { |
| const errors: ZodIssue[] = []; |
| |
| const columnsSet = new Set<number>(); |
| for (const setting of settings) { |
| if (setting.columns !== undefined) { |
| if (setting.columns < minColumns || setting.columns > maxColumns) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Invalid columns value for setting ${setting.key}. Must be between ${minColumns} and ${maxColumns}.`, |
| path: ['columns'], |
| }); |
| } else { |
| columnsSet.add(setting.columns); |
| } |
| } |
| } |
|
|
| const columns = columnsSet.size === 1 ? columnsSet.values().next().value : 2; |
|
|
| for (const setting of settings) { |
| for (const field of requiredSettingFields) { |
| if (setting[field as keyof SettingDefinition] === undefined) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Missing required field ${field} for setting ${setting.key}.`, |
| path: [field], |
| }); |
| } |
| } |
|
|
| |
| const settingTypes = Object.values(SettingTypes); |
| if (!settingTypes.includes(setting.type as SettingTypes)) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Invalid type for setting ${setting.key}. Must be one of ${settingTypes.join( |
| ', ', |
| )}.`, |
| path: ['type'], |
| }); |
| } |
|
|
| |
| if ( |
| (setting.component === ComponentTypes.Tags && setting.type !== SettingTypes.Array) || |
| (setting.component !== ComponentTypes.Tags && setting.type === SettingTypes.Array) |
| ) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Tags component for setting ${setting.key} must have type array.`, |
| path: ['type'], |
| }); |
| } |
|
|
| if (setting.component === ComponentTypes.Tags) { |
| if (setting.minTags !== undefined && setting.minTags < 0) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Invalid minTags value for setting ${setting.key}. Must be non-negative.`, |
| path: ['minTags'], |
| }); |
| } |
| if (setting.maxTags !== undefined && setting.maxTags < 0) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Invalid maxTags value for setting ${setting.key}. Must be non-negative.`, |
| path: ['maxTags'], |
| }); |
| } |
| if (setting.default && !Array.isArray(setting.default)) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Invalid default value for setting ${setting.key}. Must be an array.`, |
| path: ['default'], |
| }); |
| } |
| if (setting.default && setting.maxTags && (setting.default as []).length > setting.maxTags) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Invalid default value for setting ${setting.key}. Must have at most ${setting.maxTags} tags.`, |
| path: ['default'], |
| }); |
| } |
| if (setting.default && setting.minTags && (setting.default as []).length < setting.minTags) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Invalid default value for setting ${setting.key}. Must have at least ${setting.minTags} tags.`, |
| path: ['default'], |
| }); |
| } |
| if (!setting.default) { |
| setting.default = []; |
| } |
| } |
|
|
| if ( |
| setting.component === ComponentTypes.Input || |
| setting.component === ComponentTypes.Textarea |
| ) { |
| if (setting.type === SettingTypes.Number && setting.component === ComponentTypes.Textarea) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Textarea component for setting ${setting.key} must have type string.`, |
| path: ['type'], |
| }); |
| |
| } |
|
|
| if ( |
| setting.minText !== undefined && |
| setting.maxText !== undefined && |
| setting.minText > setting.maxText |
| ) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `For setting ${setting.key}, minText cannot be greater than maxText.`, |
| path: [setting.key, 'minText', 'maxText'], |
| }); |
| |
| } |
| if (!setting.placeholder) { |
| setting.placeholder = ''; |
| } |
| } |
|
|
| if (setting.component === ComponentTypes.Slider) { |
| if (setting.type === SettingTypes.Number && !setting.range) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Slider component for setting ${setting.key} must have a range if type is number.`, |
| path: ['range'], |
| }); |
| |
| } |
| if ( |
| setting.type === SettingTypes.Enum && |
| (!setting.options || setting.options.length < minSliderOptions) |
| ) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Slider component for setting ${setting.key} requires at least ${minSliderOptions} options for enum type.`, |
| path: ['options'], |
| }); |
| |
| } |
| setting.includeInput = |
| setting.type === SettingTypes.Number ? (setting.includeInput ?? true) : false; |
| } |
|
|
| if (setting.component === ComponentTypes.Slider && setting.type === SettingTypes.Number) { |
| if (setting.default === undefined && setting.range) { |
| |
| setting.default = Math.round((setting.range.min + setting.range.max) / 2); |
| } |
| } |
|
|
| if ( |
| setting.component === ComponentTypes.Checkbox || |
| setting.component === ComponentTypes.Switch |
| ) { |
| if (setting.options && setting.options.length > 2) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Checkbox/Switch component for setting ${setting.key} must have 1-2 options.`, |
| path: ['options'], |
| }); |
| |
| } |
| if (!setting.default && setting.type === SettingTypes.Boolean) { |
| setting.default = false; |
| } |
| } |
|
|
| if (setting.component === ComponentTypes.Dropdown) { |
| if (!setting.options || setting.options.length < minDropdownOptions) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Dropdown component for setting ${setting.key} requires at least ${minDropdownOptions} options.`, |
| path: ['options'], |
| }); |
| |
| } |
| if (!setting.default && setting.options && setting.options.length > 0) { |
| setting.default = setting.options[0]; |
| } |
| } |
|
|
| if (setting.component === ComponentTypes.Combobox) { |
| if (!setting.options || setting.options.length < minComboboxOptions) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Combobox component for setting ${setting.key} requires at least ${minComboboxOptions} options.`, |
| path: ['options'], |
| }); |
| } |
| if (!setting.default && setting.options && setting.options.length > 0) { |
| setting.default = setting.options[0]; |
| } |
| } |
|
|
| |
| if (!setting.columnSpan) { |
| setting.columnSpan = Math.floor((columns ?? 0) / 2); |
| } |
|
|
| |
| if (!setting.label) { |
| setting.label = setting.key; |
| } |
|
|
| |
| if ( |
| setting.component === ComponentTypes.Input || |
| setting.component === ComponentTypes.Textarea |
| ) { |
| if (setting.minText !== undefined && setting.minText < 0) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Invalid minText value for setting ${setting.key}. Must be non-negative.`, |
| path: ['minText'], |
| }); |
| } |
| if (setting.maxText !== undefined && setting.maxText < 0) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Invalid maxText value for setting ${setting.key}. Must be non-negative.`, |
| path: ['maxText'], |
| }); |
| } |
| } |
|
|
| |
| if (setting.optionType !== OptionTypes.Custom) { |
| const conversationSchema = |
| tConversationSchema.shape[setting.key as keyof Omit<TConversation, 'disableParams'>]; |
| if (!conversationSchema) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Setting ${setting.key} with optionType "${setting.optionType}" must be defined in tConversationSchema.`, |
| path: ['optionType'], |
| }); |
| } else { |
| const zodType = conversationSchema._def.typeName; |
| const settingTypeEquivalent = ZodTypeToSettingType[zodType] || null; |
| if (settingTypeEquivalent !== setting.type) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Setting ${setting.key} with optionType "${setting.optionType}" must match the type defined in tConversationSchema.`, |
| path: ['optionType'], |
| }); |
| } |
| } |
| } |
|
|
| |
| if ( |
| setting.type === SettingTypes.Number && |
| isNaN(setting.default as number) && |
| setting.default != null |
| ) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Invalid default value for setting ${setting.key}. Must be a number.`, |
| path: ['default'], |
| }); |
| } |
|
|
| if ( |
| setting.type === SettingTypes.Boolean && |
| typeof setting.default !== 'boolean' && |
| setting.default != null |
| ) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Invalid default value for setting ${setting.key}. Must be a boolean.`, |
| path: ['default'], |
| }); |
| } |
|
|
| if ( |
| (setting.type === SettingTypes.String || setting.type === SettingTypes.Enum) && |
| typeof setting.default !== 'string' && |
| setting.default != null |
| ) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Invalid default value for setting ${setting.key}. Must be a string.`, |
| path: ['default'], |
| }); |
| } |
|
|
| if ( |
| setting.type === SettingTypes.Enum && |
| setting.options && |
| !setting.options.includes(setting.default as string) |
| ) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Invalid default value for setting ${ |
| setting.key |
| }. Must be one of the options: [${setting.options.join(', ')}].`, |
| path: ['default'], |
| }); |
| } |
|
|
| if ( |
| setting.type === SettingTypes.Number && |
| setting.range && |
| typeof setting.default === 'number' && |
| (setting.default < setting.range.min || setting.default > setting.range.max) |
| ) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Invalid default value for setting ${setting.key}. Must be within the range [${setting.range.min}, ${setting.range.max}].`, |
| path: ['default'], |
| }); |
| } |
|
|
| |
| if (setting.enumMappings && setting.type === SettingTypes.Enum && setting.options) { |
| for (const option of setting.options) { |
| if (!(option in setting.enumMappings)) { |
| errors.push({ |
| code: ZodIssueCode.custom, |
| message: `Missing enumMapping for option "${option}" in setting ${setting.key}.`, |
| path: ['enumMappings'], |
| }); |
| } |
| } |
| } |
| } |
|
|
| if (errors.length > 0) { |
| throw new ZodError(errors); |
| } |
| } |
|
|
| export const generateOpenAISchema = (customOpenAI: OpenAISettings) => { |
| const defaults = { ...openAI, ...customOpenAI }; |
| return tConversationSchema |
| .pick({ |
| model: true, |
| chatGptLabel: true, |
| promptPrefix: true, |
| temperature: true, |
| top_p: true, |
| presence_penalty: true, |
| frequency_penalty: true, |
| resendFiles: true, |
| imageDetail: true, |
| maxContextTokens: true, |
| }) |
| .transform((obj) => ({ |
| ...obj, |
| model: obj.model ?? defaults.model.default, |
| chatGptLabel: obj.chatGptLabel ?? null, |
| promptPrefix: obj.promptPrefix ?? null, |
| temperature: obj.temperature ?? defaults.temperature.default, |
| top_p: obj.top_p ?? defaults.top_p.default, |
| presence_penalty: obj.presence_penalty ?? defaults.presence_penalty.default, |
| frequency_penalty: obj.frequency_penalty ?? defaults.frequency_penalty.default, |
| resendFiles: |
| typeof obj.resendFiles === 'boolean' ? obj.resendFiles : defaults.resendFiles.default, |
| imageDetail: obj.imageDetail ?? defaults.imageDetail.default, |
| maxContextTokens: obj.maxContextTokens ?? undefined, |
| })) |
| .catch(() => ({ |
| model: defaults.model.default, |
| chatGptLabel: null, |
| promptPrefix: null, |
| temperature: defaults.temperature.default, |
| top_p: defaults.top_p.default, |
| presence_penalty: defaults.presence_penalty.default, |
| frequency_penalty: defaults.frequency_penalty.default, |
| resendFiles: defaults.resendFiles.default, |
| imageDetail: defaults.imageDetail.default, |
| maxContextTokens: undefined, |
| })); |
| }; |
|
|
| export const generateGoogleSchema = (customGoogle: GoogleSettings) => { |
| const defaults = { ...google, ...customGoogle }; |
| return tConversationSchema |
| .pick({ |
| model: true, |
| modelLabel: true, |
| promptPrefix: true, |
| examples: true, |
| temperature: true, |
| maxOutputTokens: true, |
| topP: true, |
| topK: true, |
| maxContextTokens: true, |
| }) |
| .transform((obj) => { |
| return { |
| ...obj, |
| model: obj.model ?? defaults.model.default, |
| modelLabel: obj.modelLabel ?? null, |
| promptPrefix: obj.promptPrefix ?? null, |
| examples: obj.examples ?? [{ input: { content: '' }, output: { content: '' } }], |
| temperature: obj.temperature ?? defaults.temperature.default, |
| maxOutputTokens: obj.maxOutputTokens ?? defaults.maxOutputTokens.default, |
| topP: obj.topP ?? defaults.topP.default, |
| topK: obj.topK ?? defaults.topK.default, |
| maxContextTokens: obj.maxContextTokens ?? undefined, |
| }; |
| }) |
| .catch(() => ({ |
| model: defaults.model.default, |
| modelLabel: null, |
| promptPrefix: null, |
| examples: [{ input: { content: '' }, output: { content: '' } }], |
| temperature: defaults.temperature.default, |
| maxOutputTokens: defaults.maxOutputTokens.default, |
| topP: defaults.topP.default, |
| topK: defaults.topK.default, |
| maxContextTokens: undefined, |
| })); |
| }; |
|
|