"use client"; import { useState, useId, useRef, useEffect } from "react"; import type { AgentConfig, OceanTraits, PersonaConfig, GuardrailsConfig, MemorySettings, MbtiType, PersonaStatus, } from "@gsc/chat"; interface AgentSettingsFormProps { initialConfig: AgentConfig; userGivenName?: string; } // ── Help Tooltip ───────────────────────────────────────────────────────────── function HelpTip({ text }: { text: string }) { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { if (!open) return; const handleClick = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [open]); return ( {open && ( {text} )} ); } // ── Constants ─────────────────────────────────────────────────────────────── const MBTI_DESCRIPTIONS: Record = { INTJ: "Architect — Strategic, independent, logical planner", INTP: "Logician — Analytical, inventive, deep thinker", ENTJ: "Commander — Bold, decisive, natural leader", ENTP: "Debater — Quick-witted, resourceful, challenger", INFJ: "Advocate — Insightful, principled, compassionate", INFP: "Mediator — Idealistic, empathetic, creative", ENFJ: "Protagonist — Charismatic, inspiring, supportive", ENFP: "Campaigner — Enthusiastic, imaginative, sociable", ISTJ: "Logistician — Responsible, thorough, dependable", ISFJ: "Defender — Warm, dedicated, protective", ESTJ: "Executive — Organized, direct, strong-willed", ESFJ: "Consul — Caring, sociable, tradition-minded", ISTP: "Virtuoso — Practical, observant, hands-on", ISFP: "Adventurer — Gentle, sensitive, open-minded", ESTP: "Entrepreneur — Energetic, perceptive, action-oriented", ESFP: "Entertainer — Spontaneous, playful, encouraging", }; const MBTI_TYPES = Object.keys(MBTI_DESCRIPTIONS) as MbtiType[]; const ARCHETYPE_DESCRIPTIONS: Record = { "The Mentor": "Guides with wisdom and experience, encourages growth", "The Helper": "Eager to assist, anticipates needs, service-oriented", "The Expert": "Deep domain knowledge, authoritative, detail-focused", "The Companion": "Friendly and relatable, builds rapport, conversational", "The Challenger": "Pushes thinking, asks tough questions, growth-driven", "The Creator": "Innovative and imaginative, generates novel ideas", "The Caregiver": "Nurturing and empathetic, prioritizes well-being", "The Sage": "Reflective and philosophical, seeks deeper meaning", "The Hero": "Action-oriented, tackles problems head-on, confident", "The Rebel": "Unconventional, challenges the status quo, bold", "The Jester": "Lighthearted and witty, uses humor to engage", "The Explorer": "Curious and adventurous, discovers new possibilities", }; const ARCHETYPE_SUGGESTIONS = Object.keys(ARCHETYPE_DESCRIPTIONS); const VOICE_TONE_SUGGESTIONS = [ "Professional and concise", "Warm and encouraging", "Formal and authoritative", "Casual and friendly", "Technical and precise", "Empathetic and supportive", "Witty and conversational", "Calm and reassuring", "Direct and no-nonsense", "Enthusiastic and motivating", ]; const AVAILABLE_MODELS = [ { value: "gpt-4o", label: "GPT-4o" }, { value: "gpt-4o-mini", label: "GPT-4o Mini" }, { value: "claude-3-5-sonnet", label: "Claude 3.5 Sonnet" }, { value: "claude-3-5-haiku", label: "Claude 3.5 Haiku" }, { value: "claude-3-opus", label: "Claude 3 Opus" }, ]; const OCEAN_TRAITS = [ { key: "openness" as const, label: "Openness", desc: "Curiosity, creativity, and openness to new experiences", low: "Practical", high: "Creative", color: "primary" }, { key: "conscientiousness" as const, label: "Conscientiousness", desc: "Organization, dependability, and self-discipline", low: "Flexible", high: "Organized", color: "success" }, { key: "extraversion" as const, label: "Extraversion", desc: "Sociability, assertiveness, and positive emotions", low: "Reserved", high: "Outgoing", color: "warning" }, { key: "agreeableness" as const, label: "Agreeableness", desc: "Cooperation, trust, and helpfulness", low: "Challenging", high: "Cooperative", color: "info" }, { key: "neuroticism" as const, label: "Neuroticism", desc: "Emotional instability and tendency toward negative emotions", low: "Stable", high: "Sensitive", color: "danger" }, ]; const OCEAN_PRESETS: Record = { balanced: { name: "Balanced", values: { openness: 50, conscientiousness: 50, extraversion: 50, agreeableness: 50, neuroticism: 50 } }, analytical: { name: "Analytical", values: { openness: 70, conscientiousness: 80, extraversion: 30, agreeableness: 50, neuroticism: 30 } }, creative: { name: "Creative", values: { openness: 90, conscientiousness: 40, extraversion: 60, agreeableness: 60, neuroticism: 50 } }, supportive: { name: "Supportive", values: { openness: 60, conscientiousness: 70, extraversion: 50, agreeableness: 90, neuroticism: 30 } }, assertive: { name: "Assertive", values: { openness: 60, conscientiousness: 70, extraversion: 80, agreeableness: 40, neuroticism: 30 } }, cautious: { name: "Cautious", values: { openness: 40, conscientiousness: 80, extraversion: 30, agreeableness: 60, neuroticism: 60 } }, }; // ── Component ─────────────────────────────────────────────────────────────── export function AgentSettingsForm({ initialConfig, userGivenName }: AgentSettingsFormProps) { const baseId = useId(); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState("basic"); // Resolve active persona const resolvePersona = (id: string) => initialConfig.personas.find((p) => p.id === id) || initialConfig.personas[0]; const [selectedPersonaId, setSelectedPersonaId] = useState(initialConfig.activePersonaId); const initial = resolvePersona(initialConfig.activePersonaId); // Agent-level settings const [agentName, setAgentName] = useState(initialConfig.agentName || ""); const [userName, setUserName] = useState(initialConfig.userName || userGivenName || ""); // Basic Info const [name, setName] = useState(initial.name); const [archetype, setArchetype] = useState(initial.archetype || ""); const [isCustomArchetype, setIsCustomArchetype] = useState( !!(initial.archetype && !ARCHETYPE_SUGGESTIONS.includes(initial.archetype)) ); const [voiceTone, setVoiceTone] = useState(initial.voiceTone || ""); const [isCustomVoiceTone, setIsCustomVoiceTone] = useState( !!(initial.voiceTone && !VOICE_TONE_SUGGESTIONS.includes(initial.voiceTone)) ); const [mbti, setMbti] = useState(initial.mbti || ""); const [status, setStatus] = useState(initial.status || "active"); // Personality const [personality, setPersonality] = useState( initial.personality || OCEAN_PRESETS.balanced.values ); // Rules & Rails const [positiveRules, setPositiveRules] = useState(initial.positiveRules || []); const [negativeRules, setNegativeRules] = useState(initial.negativeRules || []); const [topicalRails, setTopicalRails] = useState(initial.topicalRails || []); const [newPositiveRule, setNewPositiveRule] = useState(""); const [newNegativeRule, setNewNegativeRule] = useState(""); const [newTopicalRail, setNewTopicalRail] = useState(""); // Backstory const [backstory, setBackstory] = useState(initial.backstory || ""); const [worldBuilding, setWorldBuilding] = useState(initial.worldBuilding || ""); // Model Settings const [defaultModel, setDefaultModel] = useState(initial.defaultModel || "gpt-4o"); const [temperature, setTemperature] = useState(initial.temperature ?? 0.7); const [maxTokensPerTurn, setMaxTokensPerTurn] = useState(initial.maxTokensPerTurn ?? 1024); const [guardrailsConfig, setGuardrailsConfig] = useState( initial.guardrailsConfig || { maxResponseLength: 2000, allowCodeExecution: false, allowExternalLinks: true } ); // Memory const [memorySettings, setMemorySettings] = useState( initialConfig.memorySettings ); const handlePersonaChange = (personaId: string) => { setSelectedPersonaId(personaId); const p = resolvePersona(personaId); setName(p.name); setArchetype(p.archetype || ""); setIsCustomArchetype(!!(p.archetype && !ARCHETYPE_SUGGESTIONS.includes(p.archetype))); setVoiceTone(p.voiceTone || ""); setIsCustomVoiceTone(!!(p.voiceTone && !VOICE_TONE_SUGGESTIONS.includes(p.voiceTone))); setMbti(p.mbti || ""); setStatus(p.status || "active"); setPersonality(p.personality); setPositiveRules(p.positiveRules || []); setNegativeRules(p.negativeRules || []); setTopicalRails(p.topicalRails || []); setBackstory(p.backstory || ""); setWorldBuilding(p.worldBuilding || ""); setDefaultModel(p.defaultModel || "gpt-4o"); setTemperature(p.temperature ?? 0.7); setMaxTokensPerTurn(p.maxTokensPerTurn ?? 1024); setGuardrailsConfig(p.guardrailsConfig || { maxResponseLength: 2000, allowCodeExecution: false, allowExternalLinks: true }); }; const handleSave = async () => { if (!name.trim()) { setError("Persona name is required"); setActiveTab("basic"); return; } setSaving(true); setError(null); setSaved(false); try { const updatedPersona: PersonaConfig = { id: selectedPersonaId, name: name.trim(), archetype: archetype.trim() || undefined, voiceTone: voiceTone.trim() || undefined, mbti: mbti || undefined, personality, positiveRules: positiveRules.filter(Boolean), negativeRules: negativeRules.filter(Boolean), backstory: backstory.trim() || undefined, worldBuilding: worldBuilding.trim() || undefined, topicalRails: topicalRails.filter(Boolean), defaultModel, temperature, maxTokensPerTurn, guardrailsConfig, status, }; const res = await fetch("/api/agent/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ agentName: agentName.trim(), userName: userName.trim(), activePersonaId: selectedPersonaId, personas: initialConfig.personas.map((p) => p.id === selectedPersonaId ? updatedPersona : p ), memorySettings, }), }); if (!res.ok) throw new Error("Failed to save"); setSaved(true); setTimeout(() => setSaved(false), 3000); } catch (err) { setError(err instanceof Error ? err.message : "Failed to save settings"); } finally { setSaving(false); } }; const handleClearHistory = async () => { if (!confirm("Are you sure you want to clear your conversation history? This cannot be undone.")) return; try { await fetch("/api/agent/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clearHistory: true }), }); } catch { setError("Failed to clear history"); } }; const addToList = ( setter: React.Dispatch>, inputSetter: React.Dispatch>, value: string ) => { const trimmed = value.trim(); if (!trimmed) return; setter((prev) => [...prev, trimmed]); inputSetter(""); }; const removeFromList = ( setter: React.Dispatch>, idx: number ) => { setter((prev) => prev.filter((_, i) => i !== idx)); }; const tabItems = [ { key: "basic", label: "Basic Info", icon: "ph-user-circle" }, { key: "personality", label: "Personality", icon: "ph-brain" }, { key: "rules", label: "Rules & Rails", icon: "ph-list-checks" }, { key: "backstory", label: "Backstory", icon: "ph-book-open" }, { key: "model", label: "Model", icon: "ph-cpu" }, { key: "memory", label: "Memory", icon: "ph-database" }, ]; return (
Agent Settings
{saved && Saved!} {error && {error}}
{/* Persona selector */} {initialConfig.personas.length > 1 && (
)} {/* Tabs */}
    {tabItems.map((tab) => (
  • ))}
{/* ── Tab: Basic Info ──────────────────────────────────────────── */} {activeTab === "basic" && (
setAgentName(e.target.value)} placeholder="e.g. Atlas" />

The name you use to talk to your agent.

setUserName(e.target.value)} placeholder="e.g. Max" />

The name the agent should call you.


Persona
setName(e.target.value)} placeholder="e.g. Sage Advisor" />
{isCustomArchetype && ( setArchetype(e.target.value)} placeholder="Enter custom archetype" autoFocus /> )} {archetype && ARCHETYPE_DESCRIPTIONS[archetype] ? (

{ARCHETYPE_DESCRIPTIONS[archetype]}

) : (

Defines the character role and behavioral pattern of the agent.

)}
{mbti ? (

{MBTI_DESCRIPTIONS[mbti]}

) : (

Shapes the agent's communication style based on Myers-Briggs personality dimensions.

)}
{isCustomVoiceTone && ( setVoiceTone(e.target.value)} placeholder="Enter custom voice & tone" autoFocus /> )}

Defines the communication style of the agent.

)} {/* ── Tab: Personality (OCEAN) ─────────────────────────────────── */} {activeTab === "personality" && (
Presets: {Object.entries(OCEAN_PRESETS).map(([key, preset]) => ( ))}
{OCEAN_TRAITS.map((trait) => (
{personality[trait.key]}

{trait.desc}

{trait.low} setPersonality((prev) => ({ ...prev, [trait.key]: parseInt(e.target.value, 10) })) } /> {trait.high}
))}
Personality Summary
{OCEAN_TRAITS.map((trait) => (
{trait.label[0]}
))}
)} {/* ── Tab: Rules & Rails ───────────────────────────────────────── */} {activeTab === "rules" && (
{/* Positive Rules */}
Positive Rules (Do)
{positiveRules.map((rule, idx) => (
{rule}
))}
setNewPositiveRule(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") addToList(setPositiveRules, setNewPositiveRule, newPositiveRule); }} />
{/* Negative Rules */}
Negative Rules (Don't)
{negativeRules.map((rule, idx) => (
{rule}
))}
setNewNegativeRule(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") addToList(setNegativeRules, setNewNegativeRule, newNegativeRule); }} />
{/* Topical Rails */}
Topical Rails

Restrict the agent to specific topics. Leave empty for no restrictions.

{topicalRails.map((rail, idx) => ( {rail} ))}
setNewTopicalRail(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") addToList(setTopicalRails, setNewTopicalRail, newTopicalRail); }} />
)} {/* ── Tab: Backstory ───────────────────────────────────────────── */} {activeTab === "backstory" && (