| | 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, |
| | })); |
| | }; |
| |
|