import React, { useMemo, useState, useEffect } from "react";
/* -------- Types (JS version via JSDoc) -------- */
/** @typedef {"web"|"code"|"retrieval"|"image"|"vision"} BuiltInTool */
const defaultConfig = {
schemaVersion: "1.0",
name: "Untitled Custom GPT",
description: "",
instructions: "You are a helpful assistant.",
language: "en",
persona: { writingTone: "friendly", emojiUse: "light", responseLength: "medium" },
builtInTools: ["web"],
customActions: [],
knowledge: { enabled: false, documents: [] },
memory: { enabled: false, scope: "user", dataRetentionDays: 180 },
safety: { jailbreakDefense: true, blockDisallowedContent: true, piiRedaction: false, customDisallowedPhrases: [] },
conversationStarters: [{ title: "What can you do?", prompt: "Give me a quick overview of your abilities." }],
sampleQuestions: ["How do I get started?"],
tags: ["starter"],
createdAt: new Date().toISOString(),
};
/* -------- Small UI atoms -------- */
function Label({ children }) {
return ;
}
function TextArea({ label, value, onChange, rows = 6, placeholder }) {
return (
);
}
function Input({ label, value, onChange, placeholder, type = "text", inputMode }) {
return (
onChange(e.target.value)}
placeholder={placeholder}
type={type}
inputMode={inputMode}
/>
);
}
function Toggle({ label, checked, onChange }) {
return (
);
}
function Pill({ text, onRemove }) {
return (
{text}
{onRemove && (
)}
);
}
function Section({ title, children, defaultOpen = false, hint }) {
return (
{title}
{hint && {hint}}
);
}
function Toast({ show, text }) {
return (
{text}
);
}
/* -------- Main Component -------- */
export default function CustomGPTSchemaGenerator() {
const [cfg] = useState({ ...defaultConfig });
// Basic
const [name, setName] = useState(cfg.name);
const [description, setDescription] = useState(cfg.description || "");
const [instructions, setInstructions] = useState(cfg.instructions);
const [language, setLanguage] = useState(cfg.language || "en");
// Persona
const [tone, setTone] = useState(cfg.persona?.writingTone || "friendly");
const [emoji, setEmoji] = useState(cfg.persona?.emojiUse || "light");
const [lengthPref, setLengthPref] = useState(cfg.persona?.responseLength || "medium");
// Tools
const [toolWeb, setToolWeb] = useState(cfg.builtInTools?.includes("web") || false);
const [toolCode, setToolCode] = useState(cfg.builtInTools?.includes("code") || false);
const [toolRetrieval, setToolRetrieval] = useState(cfg.builtInTools?.includes("retrieval") || false);
const [toolImage, setToolImage] = useState(cfg.builtInTools?.includes("image") || false);
const [toolVision, setToolVision] = useState(cfg.builtInTools?.includes("vision") || false);
// Custom actions
const [actions, setActions] = useState(cfg.customActions || []);
const [actionDraft, setActionDraft] = useState({ name: "", type: "openapi", specUrlOrInline: "", description: "", auth: { type: "none", instructions: "" } });
// Knowledge
const [knowledgeEnabled, setKnowledgeEnabled] = useState(cfg.knowledge?.enabled || false);
const [knowledgeDocs, setKnowledgeDocs] = useState(cfg.knowledge?.documents || []);
const [docDraft, setDocDraft] = useState("");
// Memory & safety
const [memoryEnabled, setMemoryEnabled] = useState(cfg.memory?.enabled || false);
const [memoryScope, setMemoryScope] = useState(cfg.memory?.scope || "user");
const [retention, setRetention] = useState(String(cfg.memory?.dataRetentionDays || 180));
const [jailbreakDefense, setJailbreakDefense] = useState(!!cfg.safety?.jailbreakDefense);
const [blockDisallowed, setBlockDisallowed] = useState(!!cfg.safety?.blockDisallowedContent);
const [piiRedaction, setPiiRedaction] = useState(!!cfg.safety?.piiRedaction);
const [customPhrases, setCustomPhrases] = useState(cfg.safety?.customDisallowedPhrases || []);
const [phraseDraft, setPhraseDraft] = useState("");
// UX polish
const [starters, setStarters] = useState(cfg.conversationStarters || []);
const [starterTitle, setStarterTitle] = useState("");
const [starterPrompt, setStarterPrompt] = useState("");
const [sampleQs, setSampleQs] = useState(cfg.sampleQuestions || []);
const [sampleDraft, setSampleDraft] = useState("");
const [tags, setTags] = useState(cfg.tags || []);
const [tagDraft, setTagDraft] = useState("");
// Toast
const [copied, setCopied] = useState(false);
useEffect(() => {
if (!copied) return;
const t = setTimeout(() => setCopied(false), 1200);
return () => clearTimeout(t);
}, [copied]);
// Derived JSON
const output = useMemo(() => {
/** @type {BuiltInTool[]} */
const builtIn = [];
if (toolWeb) builtIn.push("web");
if (toolCode) builtIn.push("code");
if (toolRetrieval) builtIn.push("retrieval");
if (toolImage) builtIn.push("image");
if (toolVision) builtIn.push("vision");
return {
schemaVersion: "1.0",
name,
description,
instructions,
language,
persona: {
writingTone: tone,
emojiUse: emoji,
responseLength: lengthPref,
},
builtInTools: builtIn,
customActions: actions,
knowledge: { enabled: knowledgeEnabled, documents: knowledgeDocs },
memory: { enabled: memoryEnabled, scope: memoryScope, dataRetentionDays: Math.max(0, Number(retention) || 0) },
safety: { jailbreakDefense, blockDisallowedContent: blockDisallowed, piiRedaction, customDisallowedPhrases: customPhrases },
conversationStarters: starters,
sampleQuestions: sampleQs,
tags,
createdAt: new Date().toISOString(),
};
}, [
name, description, instructions, language,
tone, emoji, lengthPref,
toolWeb, toolCode, toolRetrieval, toolImage, toolVision,
actions, knowledgeEnabled, knowledgeDocs,
memoryEnabled, memoryScope, retention,
jailbreakDefense, blockDisallowed, piiRedaction, customPhrases,
starters, sampleQs, tags
]);
// Mutators
function addAction() {
if (!actionDraft.name.trim()) return;
setActions((prev) => [...prev, actionDraft]);
setActionDraft({ name: "", type: "openapi", specUrlOrInline: "", description: "", auth: { type: "none", instructions: "" } });
}
function removeAction(i) {
setActions((prev) => prev.filter((_, idx) => idx !== i));
}
function addDoc() {
if (!docDraft.trim()) return;
setKnowledgeDocs((p) => [...p, docDraft.trim()]);
setDocDraft("");
}
function removeDoc(i) {
setKnowledgeDocs((p) => p.filter((_, idx) => idx !== i));
}
function addPhrase() {
if (!phraseDraft.trim()) return;
setCustomPhrases((p) => [...p, phraseDraft.trim()]);
setPhraseDraft("");
}
function removePhrase(i) {
setCustomPhrases((p) => p.filter((_, idx) => idx !== i));
}
function addStarter() {
if (!starterTitle.trim() || !starterPrompt.trim()) return;
setStarters((s) => [...s, { title: starterTitle.trim(), prompt: starterPrompt.trim() }]);
setStarterTitle(""); setStarterPrompt("");
}
function removeStarter(i) {
setStarters((s) => s.filter((_, idx) => idx !== i));
}
function addSample() {
if (!sampleDraft.trim()) return;
setSampleQs((s) => [...s, sampleDraft.trim()]);
setSampleDraft("");
}
function removeSample(i) {
setSampleQs((s) => s.filter((_, idx) => idx !== i));
}
function addTag() {
if (!tagDraft.trim()) return;
setTags((t) => [...t, tagDraft.trim()]);
setTagDraft("");
}
function removeTag(i) {
setTags((t) => t.filter((_, idx) => idx !== i));
}
async function copyJSON() {
await navigator.clipboard.writeText(JSON.stringify(output, null, 2));
setCopied(true);
}
function downloadJSON() {
const blob = new Blob([JSON.stringify(output, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${name || "custom-gpt"}.json`;
a.click();
URL.revokeObjectURL(url);
}
return (
{/* Header */}
Custom GPT Schema Generator
{/* Content */}
{/* Basic */}
{/* Persona */}
{/* Tools + Actions */}
Custom Actions
{actions.map((a, i) => (
{a.name}
{a.type}
{a.specUrlOrInline && • spec}
))}
{/* Knowledge */}
{knowledgeEnabled && (
)}
{/* Memory & Safety */}
{/* UX polish */}
{/* Output preview */}
{JSON.stringify(output, null, 2)}
Tip: Use “Copy JSON” or “Download JSON” above. Convert to YAML externally if needed.
{/* Bottom action bar on very small screens */}
);
}