diff --git a/packages/client/src/components/character-form.tsx b/packages/client/src/components/character-form.tsx index b13aff7b9b1..37b07c00ca9 100644 --- a/packages/client/src/components/character-form.tsx +++ b/packages/client/src/components/character-form.tsx @@ -3,142 +3,126 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Separator } from "@/components/ui/separator"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { useToast } from "@/hooks/use-toast"; import type { Character } from "@elizaos/core"; import React, { useState, type FormEvent, type ReactNode } from "react"; -import { useNavigate } from "react-router-dom"; -import { useQueryClient } from "@tanstack/react-query"; -// Error Boundary Component -export class ErrorBoundary extends React.Component<{ children: ReactNode }, { hasError: boolean }> { - constructor(props: { children: ReactNode }) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError() { - return { hasError: true }; - } - - render() { - if (this.state.hasError) { - return ( -
-

Something went wrong

-

Please try refreshing the page.

-
- ); - } - - return this.props.children; - } -} - -type NestedObject = { - [key: string]: string | number | boolean | NestedObject; -}; - -type UpdateArrayPath = - | "bio" - | "topics" - | "adjectives" - | "plugins" - | "style.all" - | "style.chat" - | "style.post"; +type FieldType = "text" | "textarea" | "number" | "checkbox" | "select"; type InputField = { title: string; name: string; description?: string; getValue: (char: Character) => string; + fieldType: FieldType }; type ArrayField = { title: string; description?: string; - path: UpdateArrayPath; + path: string; getData: (char: Character) => string[]; }; -type CheckboxField = { - name: string; - label: string; - description?: string; - getValue: (char: Character) => boolean; -}; - -const TEXT_FIELDS: InputField[] = [ - { - title: "Name", - name: "name", - description: "The display name of your character", - getValue: (char) => char.name || '', - }, - { - title: "Username", - name: "username", - description: "Unique identifier for your character", - getValue: (char) => char.username || '', - }, - { - title: "System", - name: "system", - description: "System prompt for character behavior", - getValue: (char) => char.system || '', - }, - { - title: "Voice Model", - name: "settings.voice.model", - description: "Voice model used for speech synthesis", - getValue: (char) => char.settings?.voice?.model || '', - }, -]; +enum SECTION_TYPE { + INPUT = "input", + ARRAY = "array" +} -const ARRAY_FIELDS: ArrayField[] = [ +const CHARACTER_FORM_SCHEMA = [ { - title: "Bio", - description: "Key information about your character", - path: "bio", - getData: (char) => Array.isArray(char.bio) ? char.bio : [], + sectionTitle: "Basic Info", + sectionValue: "basic", + sectionType: SECTION_TYPE.INPUT, + fields: [ + { + title: "Name", + name: "name", + description: "The display name of your character", + fieldType: "text", + getValue: (char) => char.name || '', + }, + { + title: "Username", + name: "username", + description: "Unique identifier for your character", + fieldType: "text", + getValue: (char) => char.username || '', + }, + { + title: "System", + name: "system", + description: "System prompt for character behavior", + fieldType: "textarea", + getValue: (char) => char.system || '', + }, + { + title: "Voice Model", + name: "settings.voice.model", + description: "Voice model used for speech synthesis", + fieldType: "text", + getValue: (char) => char.settings?.voice?.model || '', + }, + ] as InputField[] }, { - title: "Topics", - description: "Topics your character is knowledgeable about", - path: "topics", - getData: (char) => char.topics || [], + sectionTitle: "Content", + sectionValue: "content", + sectionType: SECTION_TYPE.ARRAY, + fields: [ + { + title: "Bio", + description: "Key information about your character", + path: "bio", + getData: (char) => Array.isArray(char.bio) ? char.bio : [], + }, + { + title: "Topics", + description: "Topics your character is knowledgeable about", + path: "topics", + getData: (char) => char.topics || [], + }, + { + title: "Adjectives", + description: "Words that describe your character's personality", + path: "adjectives", + getData: (char) => char.adjectives || [], + }, + ] as ArrayField[] }, { - title: "Adjectives", - description: "Words that describe your character's personality", - path: "adjectives", - getData: (char) => char.adjectives || [], - }, -]; + sectionTitle: "Style", + sectionValue: "style", + sectionType: SECTION_TYPE.ARRAY, + fields: [ + { + title: "All", + description: "Style rules applied to all interactions", + path: "style.all", + getData: (char) => char.style?.all || [], + }, + { + title: "Chat", + description: "Style rules for chat interactions", + path: "style.chat", + getData: (char) => char.style?.chat || [], + }, + { + title: "Post", + description: "Style rules for social media posts", + path: "style.post", + getData: (char) => char.style?.post || [], + }, + ] as ArrayField[] + } +] -const STYLE_FIELDS: ArrayField[] = [ - { - title: "All", - description: "Style rules applied to all interactions", - path: "style.all", - getData: (char) => char.style?.all || [], - }, - { - title: "Chat", - description: "Style rules for chat interactions", - path: "style.chat", - getData: (char) => char.style?.chat || [], - }, - { - title: "Post", - description: "Style rules for social media posts", - path: "style.post", - getData: (char) => char.style?.post || [], - }, -]; +type customComponent = { + name: string, + component: ReactNode +} export type CharacterFormProps = { character: Character; @@ -148,12 +132,10 @@ export type CharacterFormProps = { onDelete?: () => Promise; onCancel?: () => void; onReset?: () => void; - showPlugins?: boolean; - showSettings?: boolean; submitButtonText?: string; deleteButtonText?: string; isAgent?: boolean; - settingsContent?: ReactNode; + customComponents?: customComponent[]; }; export default function CharacterForm({ @@ -164,16 +146,11 @@ export default function CharacterForm({ onDelete, onCancel, onReset, - showPlugins = false, - showSettings = false, submitButtonText = "Save Changes", deleteButtonText = "Delete", - isAgent = false, - settingsContent, + customComponents = [] }: CharacterFormProps) { const { toast } = useToast(); - const navigate = useNavigate(); - const queryClient = useQueryClient(); const [characterValue, setCharacterValue] = useState(character); const [isSubmitting, setIsSubmitting] = useState(false); @@ -187,13 +164,13 @@ export default function CharacterForm({ const parts = name.split('.'); setCharacterValue(prev => { const newValue = { ...prev }; - let current = newValue as unknown as NestedObject; + let current: Record = newValue; for (let i = 0; i < parts.length - 1; i++) { if (!current[parts[i]]) { current[parts[i]] = {}; } - current = current[parts[i]] as NestedObject; + current = current[parts[i]]; } current[parts[parts.length - 1]] = type === 'checkbox' ? checked : value; @@ -207,27 +184,27 @@ export default function CharacterForm({ } }; - const updateArray = (path: UpdateArrayPath, newData: string[]) => { + const updateArray = (path: string, newData: string[]) => { setCharacterValue(prev => { const newValue = { ...prev }; - - if (path.includes('.')) { - const [parent, child] = path.split('.') as ["style", "all" | "chat" | "post"]; - return { - ...newValue, - [parent]: { - ...(newValue[parent] || {}), - [child]: newData - } - }; + const keys = path.split("."); + let current: any = newValue; + + for (let i = 0; i < keys.length - 1; i++) { + const key = keys[i]; + + if (!current[key] || typeof current[key] !== "object") { + current[key] = {}; // Ensure path exists + } + current = current[key]; } - - return { - ...newValue, - [path]: newData - } as Character; + + current[keys[keys.length - 1]] = newData; // Update array + + return newValue; }); }; + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); @@ -264,6 +241,48 @@ export default function CharacterForm({ } }; + + const renderInputField = (field: InputField) => ( +
+ + {field.description &&

{field.description}

} + + {field.fieldType === "textarea" ? ( +