📦 feat: Model & Assistants Combobox for Side Panel (#2380)

* WIP: dynamic settings

* WIP: update tests and validations

* refactor(SidePanel): use hook for Links

* WIP: dynamic settings, slider implemented

* feat(useDebouncedInput): dynamic typing with generic

* refactor(generate): add `custom` optionType to be non-conforming to conversation schema

* feat: DynamicDropdown

* refactor(DynamicSlider): custom optionType handling and useEffect for conversation updates elsewhere

* refactor(Panel): add more test cases

* chore(DynamicSlider): note

* refactor(useDebouncedInput): import defaultDebouncedDelay from ~/common`

* WIP: implement remaining ComponentTypes

* chore: add com_sidepanel_parameters

* refactor: add langCode handling for dynamic settings

* chore(useOriginNavigate): change path to '/c/'

* refactor: explicit textarea focus on new convo, share textarea idea via ~/common

* refactor: useParameterEffects: reset if convo or preset Ids change, share and maintain statefulness in side panel

* wip: combobox

* chore: minor styling for Select components

* wip: combobox select styling for side panel

* feat: complete combobox

* refactor: model select for side panel switcher

* refactor(Combobox): add portal

* chore: comment out dynamic parameters panel for future PR and delete prompt files

* refactor(Combobox): add icon field for options, change hover bg-color, add displayValue

* fix(useNewConvo): proper textarea focus with setTimeout

* refactor(AssistantSwitcher): use Combobox

* refactor(ModelSwitcher): add textarea focus on model switch
This commit is contained in:
Danny Avila 2024-04-10 14:27:22 -04:00 committed by GitHub
parent f64a2cb0b0
commit 8e5f1ad575
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 2850 additions and 462 deletions

View file

@ -0,0 +1,525 @@
/* eslint-disable jest/no-conditional-expect */
import { ZodError, z } from 'zod';
import { generateDynamicSchema, validateSettingDefinitions } from '../src/generate';
import type { SettingsConfiguration } from '../src/generate';
describe('generateDynamicSchema', () => {
it('should generate a schema for number settings with range', () => {
const settings: SettingsConfiguration = [
{
key: 'testNumber',
description: 'A test number setting',
type: 'number',
default: 5,
range: { min: 1, max: 10, step: 1 },
component: 'slider',
optionType: 'conversation',
columnSpan: 2,
label: 'Test Number Slider',
},
];
const schema = generateDynamicSchema(settings);
const result = schema.safeParse({ testNumber: 6 });
expect(result.success).toBeTruthy();
expect(result['data']).toEqual({ testNumber: 6 });
});
it('should generate a schema for boolean settings', () => {
const settings: SettingsConfiguration = [
{
key: 'testBoolean',
description: 'A test boolean setting',
type: 'boolean',
default: true,
component: 'switch',
optionType: 'model', // Only if relevant to your application's context
columnSpan: 1,
label: 'Test Boolean Switch',
},
];
const schema = generateDynamicSchema(settings);
const result = schema.safeParse({ testBoolean: false });
expect(result.success).toBeTruthy();
expect(result['data']).toEqual({ testBoolean: false });
});
it('should generate a schema for string settings', () => {
const settings: SettingsConfiguration = [
{
key: 'testString',
description: 'A test string setting',
type: 'string',
default: 'default value',
component: 'input',
optionType: 'model', // Optional and only if relevant
columnSpan: 3,
label: 'Test String Input',
placeholder: 'Enter text here...',
minText: 0, // Optional
maxText: 100, // Optional
},
];
const schema = generateDynamicSchema(settings);
const result = schema.safeParse({ testString: 'custom value' });
expect(result.success).toBeTruthy();
expect(result['data']).toEqual({ testString: 'custom value' });
});
it('should generate a schema for enum settings', () => {
const settings: SettingsConfiguration = [
{
key: 'testEnum',
description: 'A test enum setting',
type: 'enum',
default: 'option1',
options: ['option1', 'option2', 'option3'],
enumMappings: {
option1: 'First Option',
option2: 'Second Option',
option3: 'Third Option',
},
component: 'dropdown',
columnSpan: 2,
label: 'Test Enum Dropdown',
},
];
const schema = generateDynamicSchema(settings);
const result = schema.safeParse({ testEnum: 'option2' });
expect(result.success).toBeTruthy();
expect(result['data']).toEqual({ testEnum: 'option2' });
});
it('should fail for incorrect enum value', () => {
const settings: SettingsConfiguration = [
{
key: 'testEnum',
description: 'A test enum setting',
type: 'enum',
default: 'option1',
options: ['option1', 'option2', 'option3'],
component: 'dropdown',
},
];
const schema = generateDynamicSchema(settings);
const result = schema.safeParse({ testEnum: 'option4' }); // This option does not exist
expect(result.success).toBeFalsy();
});
});
describe('validateSettingDefinitions', () => {
// Test for valid setting configurations
test('should not throw error for valid settings', () => {
const validSettings: SettingsConfiguration = [
{
key: 'themeColor',
component: 'input',
type: 'string',
default: '#ffffff',
label: 'Theme Color',
columns: 2,
columnSpan: 1,
optionType: 'model',
},
{
key: 'fontSize',
component: 'slider',
type: 'number',
range: { min: 8, max: 36 },
default: 14,
columnSpan: 2,
},
];
expect(() => validateSettingDefinitions(validSettings)).not.toThrow();
});
// Test for incorrectly configured columns
test('should throw error for invalid columns configuration', () => {
const invalidSettings: SettingsConfiguration = [
{
key: 'themeColor',
component: 'input',
type: 'string',
columns: 5,
},
];
expect(() => validateSettingDefinitions(invalidSettings)).toThrow(ZodError);
});
test('should correctly handle columnSpan defaulting based on columns', () => {
const settingsWithColumnAdjustment: SettingsConfiguration = [
{
key: 'fontSize',
component: 'slider',
type: 'number',
columns: 4,
range: { min: 8, max: 14 },
default: 11,
},
];
expect(() => validateSettingDefinitions(settingsWithColumnAdjustment)).not.toThrow();
});
// Test for label defaulting to key if not provided
test('label should default to key if not explicitly set', () => {
const settingsWithDefaultLabel: SettingsConfiguration = [
{ key: 'fontWeight', component: 'dropdown', type: 'string', options: ['normal', 'bold'] },
];
expect(() => validateSettingDefinitions(settingsWithDefaultLabel)).not.toThrow();
expect(settingsWithDefaultLabel[0].label).toBe('fontWeight');
});
// Test for minText and maxText in input/textarea component
test('should throw error for negative minText or maxText', () => {
const settingsWithNegativeTextLimits: SettingsConfiguration = [
{ key: 'biography', component: 'textarea', type: 'string', minText: -1 },
];
expect(() => validateSettingDefinitions(settingsWithNegativeTextLimits)).toThrow(ZodError);
});
// Validate optionType with tConversationSchema
test('should throw error for optionType "conversation" not matching schema', () => {
const settingsWithInvalidConversationOptionType: SettingsConfiguration = [
{ key: 'userAge', component: 'input', type: 'number', optionType: 'conversation' },
];
expect(() => validateSettingDefinitions(settingsWithInvalidConversationOptionType)).toThrow(
ZodError,
);
});
// Test for columnSpan defaulting and label defaulting to key
test('columnSpan defaults based on columns and label defaults to key if not set', () => {
const settings: SettingsConfiguration = [
{
key: 'textSize',
type: 'number',
component: 'slider',
range: { min: 10, max: 20 },
columns: 4,
},
];
validateSettingDefinitions(settings); // Perform validation which also mutates settings with default values
expect(settings[0].columnSpan).toBe(2); // Expects columnSpan to default based on columns
expect(settings[0].label).toBe('textSize'); // Expects label to default to key
});
// Test for errors thrown due to invalid columns value
test('throws error if columns value is out of range', () => {
const settings: SettingsConfiguration = [
{
key: 'themeMode',
type: 'string',
component: 'dropdown',
options: ['dark', 'light'],
columns: 5,
},
];
expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
});
// Test range validation for slider component
test('slider component range validation', () => {
const settings: SettingsConfiguration = [
{ key: 'volume', type: 'number', component: 'slider' }, // Missing range
];
expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
});
// Test options validation for enum type in slider component
test('slider component with enum type requires at least 2 options', () => {
const settings: SettingsConfiguration = [
{ key: 'color', type: 'enum', component: 'slider', options: ['red'] }, // Not enough options
];
expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
});
// Test checkbox component options validation
test('checkbox component must have 1-2 options if options are provided', () => {
const settings: SettingsConfiguration = [
{
key: 'agreeToTerms',
type: 'boolean',
component: 'checkbox',
options: ['Yes', 'No', 'Maybe'],
}, // Too many options
];
expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
});
// Test dropdown component options validation
test('dropdown component requires at least 2 options', () => {
const settings: SettingsConfiguration = [
{ key: 'country', type: 'enum', component: 'dropdown', options: ['USA'] }, // Not enough options
];
expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
});
// Validate minText and maxText constraints in input and textarea
test('validate minText and maxText constraints', () => {
const settings: SettingsConfiguration = [
{ key: 'biography', type: 'string', component: 'textarea', minText: 10, maxText: 5 }, // Incorrect minText and maxText
];
expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
});
// Validate optionType constraint with tConversationSchema
test('validate optionType constraint with tConversationSchema', () => {
const settings: SettingsConfiguration = [
{ key: 'userAge', type: 'number', component: 'input', optionType: 'conversation' }, // No corresponding schema in tConversationSchema
];
expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
});
// Validate correct handling of boolean settings with default values
test('correct handling of boolean settings with defaults', () => {
const settings: SettingsConfiguration = [
{ key: 'enableFeatureX', type: 'boolean', component: 'switch' }, // Missing default, should default to false
];
validateSettingDefinitions(settings); // This would populate default values where missing
expect(settings[0].default).toBe(false); // Expects default to be false for boolean without explicit default
});
// Validate that number slider without default uses middle of range
test('number slider without default uses middle of range', () => {
const settings: SettingsConfiguration = [
{ key: 'brightness', type: 'number', component: 'slider', range: { min: 0, max: 100 } }, // Missing default
];
validateSettingDefinitions(settings); // This would populate default values where missing
expect(settings[0].default).toBe(50); // Expects default to be midpoint of range
});
});
const settingsConfiguration: SettingsConfiguration = [
{
key: 'temperature',
description:
'Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.',
type: 'number',
default: 1,
range: {
min: 0,
max: 2,
step: 0.01,
},
component: 'slider',
},
{
key: 'top_p',
description:
'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We recommend altering this or temperature but not both.',
type: 'number',
default: 1,
range: {
min: 0,
max: 1,
step: 0.01,
},
component: 'slider',
},
{
key: 'presence_penalty',
description:
'Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model\'s likelihood to talk about new topics.',
type: 'number',
default: 0,
range: {
min: -2,
max: 2,
step: 0.01,
},
component: 'slider',
},
{
key: 'frequency_penalty',
description:
'Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model\'s likelihood to repeat the same line verbatim.',
type: 'number',
default: 0,
range: {
min: -2,
max: 2,
step: 0.01,
},
component: 'slider',
},
{
key: 'resendFiles',
description:
'Resend all previously attached files. Note: this will increase token cost and you may experience errors with many attachments.',
type: 'boolean',
default: true,
component: 'switch',
},
{
key: 'imageDetail',
description:
'The resolution for Vision requests. "Low" is cheaper and faster, "High" is more detailed and expensive, and "Auto" will automatically choose between the two based on the image resolution.',
type: 'enum',
default: 'auto',
options: ['low', 'high', 'auto'],
component: 'slider',
},
{
key: 'promptPrefix',
type: 'string',
default: '',
component: 'input',
placeholder: 'Set custom instructions to include in System Message. Default: none',
},
{
key: 'chatGptLabel',
type: 'string',
default: '',
component: 'input',
placeholder: 'Set a custom name for your AI',
},
];
describe('Settings Validation and Schema Generation', () => {
// Test 1: Validate settings definitions do not throw for valid configuration
test('validateSettingDefinitions does not throw for valid configuration', () => {
expect(() => validateSettingDefinitions(settingsConfiguration)).not.toThrow();
});
test('validateSettingDefinitions throws for invalid type in settings', () => {
const settingsWithInvalidType = [
...settingsConfiguration,
{
key: 'newSetting',
description: 'A setting with an unsupported type',
type: 'unsupportedType', // Assuming 'unsupportedType' is not supported
component: 'input',
},
];
expect(() =>
validateSettingDefinitions(settingsWithInvalidType as SettingsConfiguration),
).toThrow();
});
test('validateSettingDefinitions throws for missing required fields', () => {
const settingsMissingRequiredField = [
...settingsConfiguration,
{
key: 'incompleteSetting',
type: 'number',
// Missing 'component',
},
];
expect(() =>
validateSettingDefinitions(settingsMissingRequiredField as SettingsConfiguration),
).toThrow();
});
test('validateSettingDefinitions throws for default value out of range', () => {
const settingsOutOfRange = [
...settingsConfiguration,
{
key: 'rangeTestSetting',
description: 'A setting with default value out of specified range',
type: 'number',
default: 5,
range: {
min: 0,
max: 1,
},
component: 'slider',
},
];
expect(() => validateSettingDefinitions(settingsOutOfRange as SettingsConfiguration)).toThrow();
});
test('validateSettingDefinitions throws for enum setting with incorrect default', () => {
const settingsWithIncorrectEnumDefault = [
...settingsConfiguration,
{
key: 'enumSetting',
description: 'Enum setting with a default not in options',
type: 'enum',
default: 'unlistedOption',
options: ['option1', 'option2'],
component: 'dropdown',
},
];
expect(() =>
validateSettingDefinitions(settingsWithIncorrectEnumDefault as SettingsConfiguration),
).toThrow();
});
// Test 2: Generate dynamic schema and validate correct input
test('generateDynamicSchema generates a schema that validates correct input', () => {
const schema = generateDynamicSchema(settingsConfiguration);
const validInput = {
temperature: 0.5,
top_p: 0.8,
presence_penalty: 1,
frequency_penalty: -1,
resendFiles: true,
imageDetail: 'high',
promptPrefix: 'Hello, AI.',
chatGptLabel: 'My Custom AI',
};
expect(schema.parse(validInput)).toEqual(validInput);
});
// Test 3: Generate dynamic schema and catch invalid input
test('generateDynamicSchema generates a schema that catches invalid input and provides detailed errors', async () => {
const schema = generateDynamicSchema(settingsConfiguration);
const invalidInput: z.infer<typeof schema> = {
temperature: 2.5, // Out of range
top_p: -0.5, // Out of range
presence_penalty: 3, // Out of range
frequency_penalty: -3, // Out of range
resendFiles: 'yes', // Wrong type
imageDetail: 'ultra', // Invalid option
promptPrefix: 123, // Wrong type
chatGptLabel: true, // Wrong type
};
const result = schema.safeParse(invalidInput);
expect(result.success).toBeFalsy();
if (!result.success) {
const errorPaths = result.error.issues.map((issue) => issue.path.join('.'));
expect(errorPaths).toContain('temperature');
expect(errorPaths).toContain('top_p');
expect(errorPaths).toContain('presence_penalty');
expect(errorPaths).toContain('frequency_penalty');
expect(errorPaths).toContain('resendFiles');
expect(errorPaths).toContain('imageDetail');
expect(errorPaths).toContain('promptPrefix');
expect(errorPaths).toContain('chatGptLabel');
}
});
});

View file

@ -0,0 +1,474 @@
import { z, ZodError, ZodIssueCode } from 'zod';
import { tConversationSchema, googleSettings as google, openAISettings as openAI } from './schemas';
import type { ZodIssue } from 'zod';
import type { TConversation, TSetOption } from './schemas';
export type GoogleSettings = Partial<typeof google>;
export type OpenAISettings = Partial<typeof google>;
export type ComponentType = 'input' | 'textarea' | 'slider' | 'checkbox' | 'switch' | 'dropdown';
export type OptionType = 'conversation' | 'model' | 'custom';
export enum ComponentTypes {
Input = 'input',
Textarea = 'textarea',
Slider = 'slider',
Checkbox = 'checkbox',
Switch = 'switch',
Dropdown = 'dropdown',
}
export enum OptionTypes {
Conversation = 'conversation',
Model = 'model',
Custom = 'custom',
}
export interface SettingDefinition {
key: string;
description?: string;
type: 'number' | 'boolean' | 'string' | 'enum';
default?: number | boolean | string;
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;
includeInput?: boolean; // Specific to slider component
}
export type DynamicSettingProps = Partial<SettingDefinition> & {
readonly?: boolean;
settingKey: string;
setOption: TSetOption;
defaultValue?: number | boolean | 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 } = setting;
if (type === '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 === 'boolean') {
const schema = z.boolean();
if (typeof defaultValue === 'boolean') {
schemaFields[key] = schema.default(defaultValue);
} else {
schemaFields[key] = schema;
}
continue;
}
if (type === '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 === '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;
}
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;
/**
* Validates the provided setting using the constraints unique to each component type.
* @throws {ZodError} Throws a ZodError if any validation fails.
*/
export function validateSettingDefinitions(settings: SettingsConfiguration): void {
const errors: ZodIssue[] = [];
// Validate columns
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],
});
}
}
// check accepted types
if (!['number', 'boolean', 'string', 'enum'].includes(setting.type)) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid type for setting ${setting.key}. Must be one of 'number', 'boolean', 'string', 'enum'.`,
path: ['type'],
});
}
// Predefined constraints based on components
if (setting.component === 'input' || setting.component === 'textarea') {
if (setting.type === 'number' && setting.component === 'textarea') {
errors.push({
code: ZodIssueCode.custom,
message: `Textarea component for setting ${setting.key} must have type string.`,
path: ['type'],
});
// continue;
}
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'],
});
// continue;
}
if (!setting.placeholder) {
setting.placeholder = '';
} // Default placeholder
}
if (setting.component === 'slider') {
if (setting.type === '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'],
});
// continue;
}
if (
setting.type === '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'],
});
// continue;
}
setting.includeInput = setting.type === 'number' ? setting.includeInput ?? true : false; // Default to true if type is number
}
if (setting.component === 'slider' && setting.type === 'number') {
if (setting.default === undefined && setting.range) {
// Set default to the middle of the range if unspecified
setting.default = Math.round((setting.range.min + setting.range.max) / 2);
}
}
if (setting.component === 'checkbox' || setting.component === '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'],
});
// continue;
}
if (!setting.default && setting.type === 'boolean') {
setting.default = false; // Default to false if type is boolean
}
}
if (setting.component === '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'],
});
// continue;
}
if (!setting.default && setting.options && setting.options.length > 0) {
setting.default = setting.options[0]; // Default to first option if not specified
}
}
// Default columnSpan
if (!setting.columnSpan) {
setting.columnSpan = Math.floor(columns / 2);
}
// Default label to key
if (!setting.label) {
setting.label = setting.key;
}
// Validate minText and maxText for input/textarea
if (setting.component === 'input' || setting.component === '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'],
});
}
}
// Validate optionType and conversation schema
if (setting.optionType !== OptionTypes.Custom) {
const conversationSchema = tConversationSchema.shape[setting.key as keyof TConversation];
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'],
});
}
}
}
/* Default value checks */
if (setting.type === 'number' && isNaN(setting.default as number)) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be a number.`,
path: ['default'],
});
}
if (setting.type === 'boolean' && typeof setting.default !== 'boolean') {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be a boolean.`,
path: ['default'],
});
}
if (
(setting.type === 'string' || setting.type === 'enum') &&
typeof setting.default !== 'string'
) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be a string.`,
path: ['default'],
});
}
if (
setting.type === '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 === '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 (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,
})
.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,
}))
.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,
}));
};
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,
})
.transform((obj) => {
const isGeminiPro = obj?.model?.toLowerCase()?.includes('gemini-pro');
const maxOutputTokensMax = isGeminiPro
? defaults.maxOutputTokens.maxGeminiPro
: defaults.maxOutputTokens.max;
const maxOutputTokensDefault = isGeminiPro
? defaults.maxOutputTokens.defaultGeminiPro
: defaults.maxOutputTokens.default;
let maxOutputTokens = obj.maxOutputTokens ?? maxOutputTokensDefault;
maxOutputTokens = Math.min(maxOutputTokens, maxOutputTokensMax);
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,
topP: obj.topP ?? defaults.topP.default,
topK: obj.topK ?? defaults.topK.default,
};
})
.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,
}));
};

View file

@ -4,6 +4,7 @@ export * from './config';
export * from './file-config';
/* schema helpers */
export * from './parsers';
export * from './generate';
/* types (exports schemas from `./types` as they contain needed in other defs) */
export * from './types';
export * from './types/assistants';

View file

@ -17,6 +17,26 @@ export enum EModelEndpoint {
custom = 'custom',
}
export enum ImageDetail {
low = 'low',
auto = 'auto',
high = 'high',
}
export const imageDetailNumeric = {
[ImageDetail.low]: 0,
[ImageDetail.auto]: 1,
[ImageDetail.high]: 2,
};
export const imageDetailValue = {
0: ImageDetail.low,
1: ImageDetail.auto,
2: ImageDetail.high,
};
export const eImageDetailSchema = z.nativeEnum(ImageDetail);
export const defaultAssistantFormValues = {
assistant: '',
id: '',
@ -46,38 +66,77 @@ export const ImageVisionTool: FunctionTool = {
export const isImageVisionTool = (tool: FunctionTool | FunctionToolCall) =>
tool.type === 'function' && tool.function?.name === ImageVisionTool?.function?.name;
export const endpointSettings = {
[EModelEndpoint.google]: {
model: {
default: 'chat-bison',
},
maxOutputTokens: {
min: 1,
max: 2048,
step: 1,
default: 1024,
maxGeminiPro: 8192,
defaultGeminiPro: 8192,
},
temperature: {
min: 0,
max: 1,
step: 0.01,
default: 0.2,
},
topP: {
min: 0,
max: 1,
step: 0.01,
default: 0.8,
},
topK: {
min: 1,
max: 40,
step: 0.01,
default: 40,
},
export const openAISettings = {
model: {
default: 'gpt-3.5-turbo',
},
temperature: {
min: 0,
max: 1,
step: 0.01,
default: 1,
},
top_p: {
min: 0,
max: 1,
step: 0.01,
default: 1,
},
presence_penalty: {
min: 0,
max: 2,
step: 0.01,
default: 0,
},
frequency_penalty: {
min: 0,
max: 2,
step: 0.01,
default: 0,
},
resendFiles: {
default: true,
},
imageDetail: {
default: ImageDetail.auto,
},
};
export const googleSettings = {
model: {
default: 'chat-bison',
},
maxOutputTokens: {
min: 1,
max: 2048,
step: 1,
default: 1024,
maxGeminiPro: 8192,
defaultGeminiPro: 8192,
},
temperature: {
min: 0,
max: 1,
step: 0.01,
default: 0.2,
},
topP: {
min: 0,
max: 1,
step: 0.01,
default: 0.8,
},
topK: {
min: 1,
max: 40,
step: 0.01,
default: 40,
},
};
export const endpointSettings = {
[EModelEndpoint.openAI]: openAISettings,
[EModelEndpoint.google]: googleSettings,
};
const google = endpointSettings[EModelEndpoint.google];
@ -86,26 +145,6 @@ export const eModelEndpointSchema = z.nativeEnum(EModelEndpoint);
export const extendedModelEndpointSchema = z.union([eModelEndpointSchema, z.string()]);
export enum ImageDetail {
low = 'low',
auto = 'auto',
high = 'high',
}
export const imageDetailNumeric = {
[ImageDetail.low]: 0,
[ImageDetail.auto]: 1,
[ImageDetail.high]: 2,
};
export const imageDetailValue = {
0: ImageDetail.low,
1: ImageDetail.auto,
2: ImageDetail.high,
};
export const eImageDetailSchema = z.nativeEnum(ImageDetail);
export const tPluginAuthConfigSchema = z.object({
authField: z.string(),
label: z.string(),
@ -278,12 +317,14 @@ export const tPresetUpdateSchema = tConversationSchema.merge(
export type TPreset = z.infer<typeof tPresetSchema>;
export type TSetOption = (
param: number | string,
) => (newValue: number | string | boolean | Partial<TPreset>) => void;
export type TConversation = z.infer<typeof tConversationSchema> & {
presetOverride?: Partial<TPreset>;
};
// type DefaultSchemaValues = Partial<typeof google>;
export const openAISchema = tConversationSchema
.pick({
model: true,
@ -298,26 +339,27 @@ export const openAISchema = tConversationSchema
})
.transform((obj) => ({
...obj,
model: obj.model ?? 'gpt-3.5-turbo',
model: obj.model ?? openAISettings.model.default,
chatGptLabel: obj.chatGptLabel ?? null,
promptPrefix: obj.promptPrefix ?? null,
temperature: obj.temperature ?? 1,
top_p: obj.top_p ?? 1,
presence_penalty: obj.presence_penalty ?? 0,
frequency_penalty: obj.frequency_penalty ?? 0,
resendFiles: typeof obj.resendFiles === 'boolean' ? obj.resendFiles : true,
imageDetail: obj.imageDetail ?? ImageDetail.auto,
temperature: obj.temperature ?? openAISettings.temperature.default,
top_p: obj.top_p ?? openAISettings.top_p.default,
presence_penalty: obj.presence_penalty ?? openAISettings.presence_penalty.default,
frequency_penalty: obj.frequency_penalty ?? openAISettings.frequency_penalty.default,
resendFiles:
typeof obj.resendFiles === 'boolean' ? obj.resendFiles : openAISettings.resendFiles.default,
imageDetail: obj.imageDetail ?? openAISettings.imageDetail.default,
}))
.catch(() => ({
model: 'gpt-3.5-turbo',
model: openAISettings.model.default,
chatGptLabel: null,
promptPrefix: null,
temperature: 1,
top_p: 1,
presence_penalty: 0,
frequency_penalty: 0,
resendFiles: true,
imageDetail: ImageDetail.auto,
temperature: openAISettings.temperature.default,
top_p: openAISettings.top_p.default,
presence_penalty: openAISettings.presence_penalty.default,
frequency_penalty: openAISettings.frequency_penalty.default,
resendFiles: openAISettings.resendFiles.default,
imageDetail: openAISettings.imageDetail.default,
}));
export const googleSchema = tConversationSchema
@ -674,53 +716,3 @@ export const compactPluginsSchema = tConversationSchema
return removeNullishValues(newObj);
})
.catch(() => ({}));
// const createGoogleSchema = (customGoogle: DefaultSchemaValues) => {
// const defaults = { ...google, ...customGoogle };
// return tConversationSchema
// .pick({
// model: true,
// modelLabel: true,
// promptPrefix: true,
// examples: true,
// temperature: true,
// maxOutputTokens: true,
// topP: true,
// topK: true,
// })
// .transform((obj) => {
// const isGeminiPro = obj?.model?.toLowerCase()?.includes('gemini-pro');
// const maxOutputTokensMax = isGeminiPro
// ? defaults.maxOutputTokens.maxGeminiPro
// : defaults.maxOutputTokens.max;
// const maxOutputTokensDefault = isGeminiPro
// ? defaults.maxOutputTokens.defaultGeminiPro
// : defaults.maxOutputTokens.default;
// let maxOutputTokens = obj.maxOutputTokens ?? maxOutputTokensDefault;
// maxOutputTokens = Math.min(maxOutputTokens, maxOutputTokensMax);
// 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,
// topP: obj.topP ?? defaults.topP.default,
// topK: obj.topK ?? defaults.topK.default,
// };
// })
// .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,
// }));
// };