diff --git a/src/components/Eliza/CoreEliza.tsx b/src/components/Eliza/CoreEliza.tsx index adcb234c..06a83ae5 100644 --- a/src/components/Eliza/CoreEliza.tsx +++ b/src/components/Eliza/CoreEliza.tsx @@ -21,6 +21,7 @@ import { Text } from './components/Text'; import { pages } from './settings'; import { NewsletterSubscriber } from './components/NewsletterSubscriber'; import type { CaptureEventFn } from './types'; +import { FormProviderCharacterBuilder } from './hooks/useElizaForm'; interface ElizaCoreProps { isLoggedIn: UseDeployAIAgentProps['isLoggedIn']; @@ -202,12 +203,14 @@ export const CoreEliza: React.FC = ({ )} - {steps.map( - (step) => - step.condition && ( - {step.content} - ), - )} + + {steps.map( + (step) => + step.condition && ( + {step.content} + ), + )} + ); }; diff --git a/src/components/Eliza/components/Characterfile.tsx b/src/components/Eliza/components/Characterfile.tsx index ac44ee83..4f43881f 100644 --- a/src/components/Eliza/components/Characterfile.tsx +++ b/src/components/Eliza/components/Characterfile.tsx @@ -10,9 +10,6 @@ import type { GoToProps, Step, Template } from '../utils/types'; import type React from 'react'; import { INITIAL_FORM, - SECRETS_CLIENT_MAP, - SECRETS_MODEL_PROVIDER_MAP, - SECRETS_PLUGIN_MAP, TEMPLATE_CHARACTERFILES_MAP, TEMPLATES, TEMPLATES_MAP, @@ -27,7 +24,6 @@ import { StyleForm } from './StyleForm'; import { FaChevronLeft } from 'react-icons/fa6'; import { useState } from 'react'; import { Input } from './Input'; -import type { CharacterFormSchema } from '../utils/schema'; import { pages } from '../settings'; import { PluginsDropdown } from './PluginsDropdown'; @@ -136,42 +132,20 @@ export const Characterfile: React.FC = ({ handleSubmit, formState: { errors }, reset, - setValue, } = useElizaForm(); const hasErrors = Object.entries(errors).length > 0; - const mapSettingsSecretsAndUpdateForm = (data: CharacterFormSchema) => { - const { modelProvider, clients, plugins } = data; - const model = { ...SECRETS_MODEL_PROVIDER_MAP[modelProvider] }; - const client = clients.reduce((acc, client) => { - const clientData = SECRETS_CLIENT_MAP[client]; - return { ...acc, ...clientData }; - }, {}); - const plugin = plugins.reduce((acc, plugin) => { - const pluginData = SECRETS_PLUGIN_MAP[plugin]; - return { ...acc, ...pluginData }; - }, {}); - const updatedSecrets = { - ...model, - ...client, - ...plugin, - ...data.settings.secrets, - }; - setValue('settings.secrets', updatedSecrets); - }; - const onPrevious = () => { completeStep(0); reset(INITIAL_FORM); goTo('getStarted'); }; - const onSubmit = (data: CharacterFormSchema) => { + const onSubmit = () => { if (completedStep === 0) { completeStep(1); } - mapSettingsSecretsAndUpdateForm(data); goTo('settings'); }; diff --git a/src/components/Eliza/components/Collapsible.tsx b/src/components/Eliza/components/Collapsible.tsx index 71e8d80d..8a3117d0 100644 --- a/src/components/Eliza/components/Collapsible.tsx +++ b/src/components/Eliza/components/Collapsible.tsx @@ -7,18 +7,34 @@ import { cn } from '@utils/cn'; type CollapsibleProps = { header: React.ReactNode; details: React.ReactNode; + defaultOpen?: boolean; + container?: boolean; }; export const Collapsible: React.FC = ({ header, details, + defaultOpen = false, + container = false, }) => { - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(defaultOpen); return ( - + setIsOpen((prev) => !prev)} > {header} @@ -27,7 +43,13 @@ export const Collapsible: React.FC = ({ /> {isOpen && ( - + {details} )} diff --git a/src/components/Eliza/components/Input.tsx b/src/components/Eliza/components/Input.tsx index 1d88bddc..f57983bf 100644 --- a/src/components/Eliza/components/Input.tsx +++ b/src/components/Eliza/components/Input.tsx @@ -22,7 +22,8 @@ const inputVariants = cva( isLoading: 'border-elz-neutral-4 animate-pulse bg-elz-neutral-4', error: 'border-elz-danger-8 focus-within:border-elz-danger-8 focus-within:outline-elz-danger-8', - disabled: 'cursor-not-allowed bg-elz-neutral-3 border-elz-neutral-7', + disabled: + 'cursor-not-allowed bg-elz-neutral-3 border-elz-neutral-7 text-elz-neutral-11', }, }, compoundVariants: [ diff --git a/src/components/Eliza/components/Navigation.tsx b/src/components/Eliza/components/Navigation.tsx index 1740d813..d781b1e5 100644 --- a/src/components/Eliza/components/Navigation.tsx +++ b/src/components/Eliza/components/Navigation.tsx @@ -7,7 +7,6 @@ import { type NavigationState, } from '../utils/types'; import { Characterfile } from './Characterfile'; -import { FormProviderCharacterBuilder } from '../hooks/useElizaForm'; import type { UseDeployAIAgentProps } from '../hooks/useDeployAIAgent'; import { SettingsPage } from './SettingsPage'; import { ReviewPage } from './ReviewPage'; @@ -103,9 +102,5 @@ export const Navigation: React.FC = ({ ), }; - return ( - - {pages[navigationState.page]} - - ); + return pages[navigationState.page]; }; diff --git a/src/components/Eliza/components/ReviewPage.tsx b/src/components/Eliza/components/ReviewPage.tsx index ee9609b8..9edf0850 100644 --- a/src/components/Eliza/components/ReviewPage.tsx +++ b/src/components/Eliza/components/ReviewPage.tsx @@ -15,8 +15,14 @@ import { cn } from '@utils/cn'; import { Input } from './Input'; import { characterfileSchema } from '../utils/schema'; import { validateZod } from '../utils/validateHelper'; -import { INITIAL_FORM } from '../utils/constants'; +import { + CLIENTS_MAP, + INITIAL_FORM, + MODEL_PROVIDER_NAMES_MAP, + PLUGINS_MAP, +} from '../utils/constants'; import { Collapsible } from './Collapsible'; +import { Badge } from './Badge'; type HeaderProps = { from: Options['from']; @@ -87,6 +93,9 @@ export const ReviewPage: React.FC = ({ characterfile || JSON.stringify(transformedData, null, 2), ); const [errors, setErrors] = useState(INITIAL_ERRORS); + const [viewMode, setViewMode] = useState<'readable' | 'json'>( + from === 'upload' ? 'json' : 'readable', + ); const hasErrors = errors.json || errors.form.length > 0; @@ -119,6 +128,218 @@ export const ReviewPage: React.FC = ({ onDeployBtnClick(payload); }; + const changeViewMode = ( + + + + + ); + + const deployButton = ( + + ); + + if (viewMode === 'readable') + return ( + + +
+ Confirm agent details + + You will be deploying an agent with the information below. This is + the final step before your agent is deployed. + + + + {changeViewMode} + General} + details={ + <> + + Name + {data.name} + + + Model provider + + {MODEL_PROVIDER_NAMES_MAP[data.modelProvider]?.label || + data.modelProvider} + + + + Clients + + {data.clients.map((client) => ( + + {CLIENTS_MAP[client]?.label || client} + + ))} + + + {data.plugins.length > 0 && ( + + Plugins + + {data.plugins.map((plugin) => ( + + {PLUGINS_MAP[plugin]?.label || plugin} + + ))} + + + )} + + } + /> + Background} + details={ + <> + + Bio +
    + {data.bio.map((entry, idx) => ( +
  • {entry.name}
  • + ))} +
+
+ + Lore +
    + {data.lore.map((entry, idx) => ( +
  • {entry.name}
  • + ))} +
+
+ {data.knowledge && data.knowledge.length > 0 && ( + + Knowledge +
    + {data.knowledge.map((entry, idx) => ( +
  • {entry.name}
  • + ))} +
+
+ )} + + } + /> + Message examples} + details={ + <> + {data.messageExamples.map((example, idx) => ( + + Example #{idx + 1} + {example.map((msg, innerIdx) => { + const isEven = innerIdx % 2 === 0; + return ( + + + {isEven ? 'User' : data.name} + + {msg.content.text} + + ); + })} + + ))} + + } + /> + Communication style} + details={ + <> + + All +
    + {data.style.all.map((entry, idx) => ( +
  • {entry.name}
  • + ))} +
+
+ + Chat +
    + {data.style.chat.map((entry, idx) => ( +
  • {entry.name}
  • + ))} +
+
+ + Post +
    + {data.style.post.map((entry, idx) => ( +
  • {entry.name}
  • + ))} +
+
+ + } + /> + Interests} + details={ + <> + + Topics + + {data.topics.map((topic, idx) => ( + + {topic} + + ))} + + + + Adjectives + + {data.adjectives.map((adjective, idx) => ( + + {adjective} + + ))} + + + + } + /> +
+ {deployButton} + + ); + return ( @@ -129,55 +350,58 @@ export const ReviewPage: React.FC = ({ final step before your agent is deployed. - - - setErrors({ ...errors, json: !jsonError }) - } - className={cn({ 'border-elz-danger-8 transition-colors': hasErrors })} - /> - {errors.json && ( - - - JSON error: there was an error parsing your code. Please fix it or - revert the change above. - - )} - {errors.form.length > 0 && ( - - {errors.form.map((error, idx) => { - const isLastItem = idx === errors.form.length - 1; - const errorMsg = `${error.path}: ${error.message}`; - return ( - - {error.options ? ( - {errorMsg}} - details={ - - Supported options: {error.options.join(', ')} - - } - /> - ) : ( - {errorMsg} - )} - - ); + + {changeViewMode} + + + setErrors({ ...errors, json: !jsonError }) + } + className={cn({ + 'border-elz-danger-8 transition-colors': hasErrors, })} - - )} + /> + {errors.json && ( + + - JSON error: there was an error parsing your code. Please fix it + or revert the change above. + + )} + {errors.form.length > 0 && ( + + {errors.form.map((error, idx) => { + const isLastItem = idx === errors.form.length - 1; + const errorMsg = `${error.path}: ${error.message}`; + return ( + + {error.options ? ( + {errorMsg}} + details={ + + Supported options: {error.options.join(', ')} + + } + /> + ) : ( + {errorMsg} + )} + + ); + })} + + )} + - + {deployButton} ); }; diff --git a/src/components/Eliza/components/SettingsPage.tsx b/src/components/Eliza/components/SettingsPage.tsx index da5815fb..566fb066 100644 --- a/src/components/Eliza/components/SettingsPage.tsx +++ b/src/components/Eliza/components/SettingsPage.tsx @@ -6,12 +6,11 @@ import type { GoToProps, Step } from '../utils/types'; import type React from 'react'; import Link, { Target } from './Link'; import { useElizaForm } from '../hooks/useElizaForm'; -import { useState } from 'react'; -import { Controller } from 'react-hook-form'; -import FileEditor from '@components/Eliza/components/FileEditor'; -import { cn } from '@utils/cn'; +import { useForm } from 'react-hook-form'; import { Input } from './Input'; -import { transformErrors } from '../utils/transformData'; +import { settingsSchema, type SettingsSchema } from '../utils/schema'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { extractSecretsFromData } from '../utils/transformData'; type HeaderProps = { onPrevious: () => void; @@ -41,25 +40,32 @@ export const SettingsPage: React.FC = ({ completeStep, completedStep, }) => { - const [errorJson, setErrorJson] = useState(false); + const { getValues, setValue } = useElizaForm(); - const { control, formState, handleSubmit, reset } = useElizaForm(); + const data = getValues(); - const readableErrors = transformErrors(formState.errors.settings); - const hasErrors = readableErrors.length > 0 || errorJson; + const defaultValues = { + secrets: extractSecretsFromData(data), + voice: { model: data.settings.voice.model }, + }; + + const { reset, register, handleSubmit, formState } = useForm({ + defaultValues, + resolver: zodResolver(settingsSchema), + }); + + const formValues = Object.entries(defaultValues.secrets); const onPrevious = () => { reset(); goTo('characterfile'); }; - const onSubmit = () => { - if (errorJson) return; - + const onSubmit = (data: SettingsSchema) => { if (completedStep === 1) { completeStep(2); } - + setValue('settings', data); goTo('review'); }; @@ -81,54 +87,56 @@ export const SettingsPage: React.FC = ({ to view all the supported secrets. - - ( - { - try { - field.onChange(JSON.parse(data || '')); - } catch (e) { - console.warn( - `Settings have not been saved due to a malformed object that can't be parsed. The editor will try again in the next value change.`, - ); - } - }} - onValidation={(jsonError) => setErrorJson(!jsonError)} - className={cn({ - 'border-elz-danger-8 transition-colors': hasErrors, - })} - /> + + + + + Add secrets + + + These are required to connect with your model, clients and + plugins. + + + + Key + Value + + {formValues.map(([key]) => { + const error = formState.errors.secrets?.[key]; + return ( + + + + + + + + + ); + })} + {formState.errors.secrets && ( + Please fill the missing values. )} - /> - {errorJson && ( - + + + + + Voice + + Optional voice model + + + + + {formState.errors.voice && ( - There was an error parsing your code. Please fix it or revert the - change above. + {formState.errors.voice.model?.message} - - )} - {hasErrors && ( - - {readableErrors.map((error) => ( - - {readableErrors.length > 1 && '-'} {error.label}:{' '} - {error.message} - - ))} - - )} + )} + + diff --git a/src/components/Eliza/components/Text.tsx b/src/components/Eliza/components/Text.tsx index 20218e15..4a5d3d84 100644 --- a/src/components/Eliza/components/Text.tsx +++ b/src/components/Eliza/components/Text.tsx @@ -7,6 +7,8 @@ const textVariants = cva('font-elz-plex-sans', { variant: { title: 'text-balance font-elz-sans text-[3.6rem] font-semibold leading-[1.125] -tracking-2 text-elz-neutral-12 md:text-[5.2rem]', + subtitle: + 'font-elz-plex-sans text-[1.6rem] text-elz-neutral-12 font-semibold', description: 'text-[1.8rem] font-medium text-elz-neutral-11', feature: 'text-[1.2rem] font-medium uppercase tracking-[0.256rem] text-elz-neutral-11', diff --git a/src/components/Eliza/utils/schema.ts b/src/components/Eliza/utils/schema.ts index 941a77f0..6d579e26 100644 --- a/src/components/Eliza/utils/schema.ts +++ b/src/components/Eliza/utils/schema.ts @@ -2,12 +2,21 @@ import { z } from 'zod'; import { CLIENT_NAMES, MODEL_PROVIDER_NAMES, PLUGIN_NAMES } from './constants'; import type { Character } from './types'; +export const settingsSchema = z.object({ + secrets: z.record(z.string().min(1, 'value is missing')), + voice: z.object({ + model: z.string().optional(), + }), +}); + +export type SettingsSchema = z.infer; + /** Schema for the form builder, which is * slightly different than the final characterfile. * We need it due to how React Hook Form handles arrays. */ export const characterFormSchema = z.object({ - name: z.string().min(3, 'Name is required, minimum of 3 characters'), + name: z.string().min(1, 'Name is required'), username: z.string().optional(), plugins: z.array(z.enum(PLUGIN_NAMES)), modelProvider: z.enum(MODEL_PROVIDER_NAMES, { @@ -18,20 +27,15 @@ export const characterFormSchema = z.object({ clients: z .array(z.enum(CLIENT_NAMES)) .min(1, 'At least one client is required'), - settings: z.object({ - secrets: z.record(z.string().min(1, 'value is missing')), - voice: z.object({ - model: z.string().min(3, 'Voice model is required'), - }), - }), + settings: settingsSchema, bio: z.array( z.object({ - name: z.string().min(3, 'Bio is required, minimum of 3 characters'), + name: z.string().min(1, 'Bio is required'), }), ), lore: z.array( z.object({ - name: z.string().min(3, 'Lore is required, minimum of 3 characters'), + name: z.string().min(1, 'Lore is required'), }), ), knowledge: z.array(z.object({ name: z.string() })).optional(), @@ -51,31 +55,23 @@ export const characterFormSchema = z.object({ .min(1, 'At least one message example is required'), postExamples: z.array( z.object({ - name: z - .string() - .min(3, 'Post example is required, minimum of 3 characters'), + name: z.string().min(1, 'Post example is required'), }), ), style: z.object({ all: z.array( z.object({ - name: z - .string() - .min(3, `Style for 'All' is required, minimum of 3 characters`), + name: z.string().min(1, `Style for 'All' is required`), }), ), chat: z.array( z.object({ - name: z - .string() - .min(3, `Style for 'Chat' is required, minimum of 3 characters`), + name: z.string().min(1, `Style for 'Chat' is required`), }), ), post: z.array( z.object({ - name: z - .string() - .min(3, `Style for 'Post' is required, minimum of 3 characters`), + name: z.string().min(1, `Style for 'Post' is required`), }), ), }), @@ -89,7 +85,7 @@ export type CharacterFormSchema = z.infer; /** Schema for the characterfile JSON file */ export const characterfileSchema = z.object({ - name: z.string().min(3, 'Name is required, minimum of 3 characters'), + name: z.string().min(1, 'Name is required'), username: z.string().optional(), plugins: z.array( z.enum(PLUGIN_NAMES, { @@ -112,14 +108,9 @@ export const characterfileSchema = z.object({ }), ) .min(1, 'At least one client is required'), - settings: z.object({ - secrets: z.record(z.string().min(1, 'value is missing')), - voice: z.object({ - model: z.string().min(3, 'Voice model is required'), - }), - }), - bio: z.array(z.string().min(3, 'Bio is required, minimum of 3 characters')), - lore: z.array(z.string().min(3, 'Lore is required, minimum of 3 characters')), + settings: settingsSchema, + bio: z.array(z.string().min(1, 'Bio is required')), + lore: z.array(z.string().min(1, 'Lore is required')), knowledge: z.array(z.string()).optional(), messageExamples: z .array( @@ -128,30 +119,18 @@ export const characterfileSchema = z.object({ z.object({ user: z.string().min(1, 'User is required'), content: z.object({ - text: z.string().min(1, 'Message example is required'), + text: z.string(), }), }), ) .min(2), ) .min(1, 'At least one message example is required'), - postExamples: z.array( - z.string().min(3, 'Post example is required, minimum of 3 characters'), - ), + postExamples: z.array(z.string().min(1, 'Post example is required')), style: z.object({ - all: z.array( - z.string().min(3, `Style for 'All' is required, minimum of 3 characters`), - ), - chat: z.array( - z - .string() - .min(3, `Style for 'Chat' is required, minimum of 3 characters`), - ), - post: z.array( - z - .string() - .min(3, `Style for 'Post' is required, minimum of 3 characters`), - ), + all: z.array(z.string().min(1, `Style for 'All' is required`)), + chat: z.array(z.string().min(1, `Style for 'Chat' is required`)), + post: z.array(z.string().min(1, `Style for 'Post' is required`)), }), topics: z.array(z.string().min(1)).min(1, 'At least one topic is required'), adjectives: z diff --git a/src/components/Eliza/utils/transformData.ts b/src/components/Eliza/utils/transformData.ts index 04a00292..33156c09 100644 --- a/src/components/Eliza/utils/transformData.ts +++ b/src/components/Eliza/utils/transformData.ts @@ -2,6 +2,11 @@ import type { FieldError, FieldErrorsImpl, Merge } from 'react-hook-form'; import type { Character } from './types'; import type { CharacterfileSchema, CharacterFormSchema } from './schema'; import type { Primitive, ZodError } from 'zod'; +import { + SECRETS_CLIENT_MAP, + SECRETS_MODEL_PROVIDER_MAP, + SECRETS_PLUGIN_MAP, +} from './constants'; type TransformedError = { label: string; @@ -100,3 +105,22 @@ export const transformSchemaToCharacter = ( }, }; }; + +export const extractSecretsFromData = (data: CharacterFormSchema) => { + const { modelProvider, clients, plugins } = data; + const model = { ...SECRETS_MODEL_PROVIDER_MAP[modelProvider] }; + const client = clients.reduce( + (acc, client) => ({ ...acc, ...SECRETS_CLIENT_MAP[client] }), + {}, + ); + const plugin = plugins.reduce( + (acc, plugin) => ({ ...acc, ...SECRETS_PLUGIN_MAP[plugin] }), + {}, + ); + return { + ...model, + ...client, + ...plugin, + ...data.settings.secrets, + }; +};