Spaces:
Running
Running
Download src/CustomGPTSchemaGenerator.jsx from Nuzhatwa/_customGPT_Generator_Pro_: direct link, hf CLI and curl.
- Browser
- Download file 24.7 kB
-
https://huggingface.co/spaces/Nuzhatwa/_customGPT_Generator_Pro_/resolve/main/src/CustomGPTSchemaGenerator.jsx
- Command line
-
hf download hf://spaces/Nuzhatwa/_customGPT_Generator_Pro_/src/CustomGPTSchemaGenerator.jsx
-
curl -L -o CustomGPTSchemaGenerator.jsx https://huggingface.co/spaces/Nuzhatwa/_customGPT_Generator_Pro_/resolve/main/src/CustomGPTSchemaGenerator.jsx
24.7 kB
| 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 <label className="text-[13px] font-medium text-gray-700">{children}</label>; | |
| } | |
| function TextArea({ label, value, onChange, rows = 6, placeholder }) { | |
| return ( | |
| <div className="flex flex-col gap-2"> | |
| <Label>{label}</Label> | |
| <textarea | |
| className="w-full rounded-2xl border border-gray-200 p-3 text-[15px] leading-6 focus:outline-none focus:ring-2 focus:ring-gray-300" | |
| value={value} | |
| onChange={(e) => onChange(e.target.value)} | |
| rows={rows} | |
| placeholder={placeholder} | |
| /> | |
| </div> | |
| ); | |
| } | |
| function Input({ label, value, onChange, placeholder, type = "text", inputMode }) { | |
| return ( | |
| <div className="flex flex-col gap-2"> | |
| <Label>{label}</Label> | |
| <input | |
| className="w-full rounded-2xl border border-gray-200 p-3 text-[15px] leading-6 focus:outline-none focus:ring-2 focus:ring-gray-300" | |
| value={value} | |
| onChange={(e) => onChange(e.target.value)} | |
| placeholder={placeholder} | |
| type={type} | |
| inputMode={inputMode} | |
| /> | |
| </div> | |
| ); | |
| } | |
| function Toggle({ label, checked, onChange }) { | |
| return ( | |
| <label className="flex items-center gap-3 cursor-pointer select-none"> | |
| <input className="h-5 w-5 accent-black" type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} /> | |
| <span className="text-[14px]">{label}</span> | |
| </label> | |
| ); | |
| } | |
| function Pill({ text, onRemove }) { | |
| return ( | |
| <span className="inline-flex items-center gap-2 rounded-full border border-gray-200 px-3 py-1 text-[12px]"> | |
| {text} | |
| {onRemove && ( | |
| <button onClick={onRemove} className="opacity-60 hover:opacity-100" aria-label="Remove"> | |
| × | |
| </button> | |
| )} | |
| </span> | |
| ); | |
| } | |
| function Section({ title, children, defaultOpen = false, hint }) { | |
| return ( | |
| <details className="rounded-2xl border border-gray-200 bg-white open:shadow-sm" open={defaultOpen}> | |
| <summary className="list-none cursor-pointer select-none px-4 py-3 text-[15px] font-semibold flex items-center justify-between"> | |
| <span>{title}</span> | |
| {hint && <span className="text-xs text-gray-500">{hint}</span>} | |
| </summary> | |
| <div className="p-4 pt-0"> | |
| <div className="h-px bg-gray-100 mb-4" /> | |
| {children} | |
| </div> | |
| </details> | |
| ); | |
| } | |
| function Toast({ show, text }) { | |
| return ( | |
| <div | |
| className={`fixed left-1/2 top-4 z-50 -translate-x-1/2 transform rounded-full bg-black px-4 py-2 text-white text-sm transition-opacity ${ | |
| show ? "opacity-100" : "opacity-0" | |
| }`} | |
| role="status" | |
| aria-live="polite" | |
| > | |
| {text} | |
| </div> | |
| ); | |
| } | |
| /* -------- 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 ( | |
| <div className="min-h-screen bg-gray-50"> | |
| <Toast show={copied} text="Copied ✓" /> | |
| {/* Header */} | |
| <header className="sticky top-0 z-40 border-b border-gray-200 bg-white/80 backdrop-blur"> | |
| <div className="mx-auto max-w-6xl px-4 py-3"> | |
| <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> | |
| <h1 className="text-xl sm:text-2xl font-semibold">Custom GPT Schema Generator</h1> | |
| <div className="grid grid-cols-2 gap-2 sm:flex sm:gap-3"> | |
| <button onClick={copyJSON} className="rounded-xl border border-gray-300 px-4 py-2 text-sm hover:shadow active:scale-[0.98]">Copy JSON</button> | |
| <button onClick={downloadJSON} className="rounded-xl border border-gray-300 px-4 py-2 text-sm hover:shadow active:scale-[0.98]">Download JSON</button> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| {/* Content */} | |
| <main className="mx-auto max-w-6xl px-4 py-5 space-y-5"> | |
| {/* Basic */} | |
| <Section title="Basic" defaultOpen hint="Name, language, description, instructions"> | |
| <div className="grid grid-cols-1 gap-4 md:grid-cols-2"> | |
| <Input label="Name" value={name} onChange={setName} placeholder="e.g., EduAssess Pro" /> | |
| <Input label="Language (IETF tag)" value={language} onChange={setLanguage} placeholder="e.g., en, ur, en-GB" /> | |
| <div className="md:col-span-2"> | |
| <Input label="Short Description" value={description} onChange={setDescription} placeholder="One-line purpose" /> | |
| </div> | |
| <div className="md:col-span-2"> | |
| <TextArea label="System Instructions" value={instructions} onChange={setInstructions} rows={8} placeholder="Define role, constraints, formatting rules, and tool-use strategy." /> | |
| </div> | |
| </div> | |
| </Section> | |
| {/* Persona */} | |
| <Section title="Persona & Style" hint="Tone, emoji, response length"> | |
| <div className="grid grid-cols-1 gap-4 md:grid-cols-3"> | |
| <div> | |
| <Label>Tone</Label> | |
| <select className="mt-2 w-full rounded-2xl border border-gray-200 p-3 text-[15px] focus:outline-none focus:ring-2 focus:ring-gray-300" value={tone} onChange={(e) => setTone(e.target.value)}> | |
| <option value="casual">casual</option> | |
| <option value="formal">formal</option> | |
| <option value="technical">technical</option> | |
| <option value="friendly">friendly</option> | |
| <option value="playful">playful</option> | |
| <option value="neutral">neutral</option> | |
| </select> | |
| </div> | |
| <div> | |
| <Label>Emoji Use</Label> | |
| <select className="mt-2 w-full rounded-2xl border border-gray-200 p-3 text-[15px] focus:outline-none focus:ring-2 focus:ring-gray-300" value={emoji} onChange={(e) => setEmoji(e.target.value)}> | |
| <option value="none">none</option> | |
| <option value="light">light</option> | |
| <option value="moderate">moderate</option> | |
| <option value="heavy">heavy</option> | |
| </select> | |
| </div> | |
| <div> | |
| <Label>Response Length</Label> | |
| <select className="mt-2 w-full rounded-2xl border border-gray-200 p-3 text-[15px] focus:outline-none focus:ring-2 focus:ring-gray-300" value={lengthPref} onChange={(e) => setLengthPref(e.target.value)}> | |
| <option value="short">short</option> | |
| <option value="medium">medium</option> | |
| <option value="long">long</option> | |
| </select> | |
| </div> | |
| </div> | |
| </Section> | |
| {/* Tools + Actions */} | |
| <Section title="Built-in Tools & Custom Actions" hint="web, code, retrieval, image, vision + APIs"> | |
| <div className="flex flex-wrap gap-4"> | |
| <Toggle label="Web Browsing" checked={toolWeb} onChange={setToolWeb} /> | |
| <Toggle label="Code Interpreter" checked={toolCode} onChange={setToolCode} /> | |
| <Toggle label="Retrieval / RAG" checked={toolRetrieval} onChange={setToolRetrieval} /> | |
| <Toggle label="Image Generation" checked={toolImage} onChange={setToolImage} /> | |
| <Toggle label="Vision (image understanding)" checked={toolVision} onChange={setToolVision} /> | |
| </div> | |
| <div className="pt-4 space-y-3"> | |
| <h3 className="text-[14px] font-semibold">Custom Actions</h3> | |
| <div className="grid grid-cols-1 gap-3 md:grid-cols-5"> | |
| <Input label="Name" value={actionDraft.name} onChange={(v) => setActionDraft((a) => ({ ...a, name: v }))} /> | |
| <div> | |
| <Label>Type</Label> | |
| <select | |
| className="mt-2 w-full rounded-2xl border border-gray-200 p-3 text-[15px] focus:outline-none focus:ring-2 focus:ring-gray-300" | |
| value={actionDraft.type} | |
| onChange={(e) => setActionDraft((a) => ({ ...a, type: e.target.value }))} | |
| > | |
| <option value="openapi">openapi</option> | |
| <option value="json-rpc">json-rpc</option> | |
| <option value="graphql">graphql</option> | |
| <option value="webhook">webhook</option> | |
| </select> | |
| </div> | |
| <Input label="Spec URL or Inline" value={actionDraft.specUrlOrInline || ""} onChange={(v) => setActionDraft((a) => ({ ...a, specUrlOrInline: v }))} placeholder="https://... or raw spec" /> | |
| <div> | |
| <Label>Auth</Label> | |
| <select | |
| className="mt-2 w-full rounded-2xl border border-gray-200 p-3 text-[15px] focus:outline-none focus:ring-2 focus:ring-gray-300" | |
| value={actionDraft.auth?.type || "none"} | |
| onChange={(e) => setActionDraft((a) => ({ ...a, auth: { ...(a.auth || {}), type: e.target.value } }))} | |
| > | |
| <option value="none">none</option> | |
| <option value="api_key">api_key</option> | |
| <option value="oauth2">oauth2</option> | |
| </select> | |
| </div> | |
| <Input label="Auth Instructions (optional)" value={actionDraft.auth?.instructions || ""} onChange={(v) => setActionDraft((a) => ({ ...a, auth: { ...(a.auth || { type: "none" }), instructions: v } }))} placeholder="Header, OAuth steps, etc." /> | |
| </div> | |
| <div className="flex justify-end pt-1"> | |
| <button onClick={addAction} className="w-full sm:w-auto rounded-xl border border-gray-300 px-4 py-2 text-sm hover:shadow active:scale-[0.98]">Add Action</button> | |
| </div> | |
| <div className="flex flex-wrap gap-2"> | |
| {actions.map((a, i) => ( | |
| <div key={i} className="flex items-center gap-2 rounded-2xl border border-gray-200 bg-gray-50 px-3 py-2 text-xs"> | |
| <span className="font-medium">{a.name}</span> | |
| <span className="opacity-70">{a.type}</span> | |
| {a.specUrlOrInline && <span className="opacity-70">• spec</span>} | |
| <button onClick={() => removeAction(i)} className="opacity-60 hover:opacity-100">Remove</button> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </Section> | |
| {/* Knowledge */} | |
| <Section title="Knowledge / RAG" hint="Attach doc names (hints only)"> | |
| <div className="space-y-2"> | |
| <Toggle label="Enable knowledge retrieval" checked={knowledgeEnabled} onChange={setKnowledgeEnabled} /> | |
| {knowledgeEnabled && ( | |
| <div className="space-y-2"> | |
| <div className="flex flex-col sm:flex-row gap-2"> | |
| <input className="flex-1 rounded-2xl border border-gray-200 p-3 text-[15px] focus:outline-none focus:ring-2 focus:ring-gray-300" placeholder="e.g., Pricing Deck v3.pdf" value={docDraft} onChange={(e) => setDocDraft(e.target.value)} /> | |
| <button onClick={addDoc} className="rounded-xl border border-gray-300 px-4 py-2 text-sm hover:shadow active:scale-[0.98]">Add</button> | |
| </div> | |
| <div className="flex flex-wrap gap-2"> | |
| {knowledgeDocs.map((d, i) => ( | |
| <Pill key={i} text={d} onRemove={() => removeDoc(i)} /> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </Section> | |
| {/* Memory & Safety */} | |
| <Section title="Memory & Safety" hint="Retention, PII, jailbreak"> | |
| <div className="grid grid-cols-1 gap-6 md:grid-cols-2"> | |
| <div className="space-y-3"> | |
| <Toggle label="Enable persistent memory" checked={memoryEnabled} onChange={setMemoryEnabled} /> | |
| {memoryEnabled && ( | |
| <div className="grid grid-cols-1 gap-3 md:grid-cols-3"> | |
| <div> | |
| <Label>Scope</Label> | |
| <select | |
| className="mt-2 w-full rounded-2xl border border-gray-200 p-3 text-[15px] focus:outline-none focus:ring-2 focus:ring-gray-300" | |
| value={memoryScope} | |
| onChange={(e) => setMemoryScope(e.target.value)} | |
| > | |
| <option value="conversation">conversation</option> | |
| <option value="user">user</option> | |
| </select> | |
| </div> | |
| <Input label="Retention Days" value={retention} onChange={setRetention} type="number" inputMode="numeric" placeholder="e.g., 180" /> | |
| </div> | |
| )} | |
| </div> | |
| <div className="space-y-3"> | |
| <Toggle label="Jailbreak defense" checked={jailbreakDefense} onChange={setJailbreakDefense} /> | |
| <Toggle label="Block disallowed content" checked={blockDisallowed} onChange={setBlockDisallowed} /> | |
| <Toggle label="PII redaction" checked={piiRedaction} onChange={setPiiRedaction} /> | |
| <div className="pt-2"> | |
| <div className="flex flex-col sm:flex-row gap-2"> | |
| <input className="flex-1 rounded-2xl border border-gray-200 p-3 text-[15px] focus:outline-none focus:ring-2 focus:ring-gray-300" placeholder="Add custom disallowed phrase" value={phraseDraft} onChange={(e) => setPhraseDraft(e.target.value)} /> | |
| <button onClick={addPhrase} className="rounded-xl border border-gray-300 px-4 py-2 text-sm hover:shadow active:scale-[0.98]">Add</button> | |
| </div> | |
| <div className="mt-2 flex flex-wrap gap-2"> | |
| {customPhrases.map((p, i) => ( | |
| <Pill key={i} text={p} onRemove={() => removePhrase(i)} /> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </Section> | |
| {/* UX polish */} | |
| <Section title="Conversation Starters & Hints" hint="Starters, sample Qs, tags"> | |
| <div className="grid grid-cols-1 gap-3 md:grid-cols-2"> | |
| <Input label="Starter Title" value={starterTitle} onChange={setStarterTitle} /> | |
| <Input label="Starter Prompt" value={starterPrompt} onChange={setStarterPrompt} /> | |
| </div> | |
| <div className="flex justify-end"> | |
| <button onClick={addStarter} className="w-full sm:w-auto rounded-xl border border-gray-300 px-4 py-2 text-sm hover:shadow active:scale-[0.98]">Add Starter</button> | |
| </div> | |
| <div className="flex flex-wrap gap-2"> | |
| {starters.map((s, i) => ( | |
| <div key={i} className="flex items-center gap-2 rounded-2xl border border-gray-200 bg-gray-50 px-3 py-2 text-xs"> | |
| <span className="font-medium">{s.title}</span> | |
| <span className="opacity-70">— {s.prompt}</span> | |
| <button onClick={() => removeStarter(i)} className="opacity-60 hover:opacity-100">Remove</button> | |
| </div> | |
| ))} | |
| </div> | |
| <div className="pt-4"> | |
| <div className="flex flex-col sm:flex-row gap-2"> | |
| <input className="flex-1 rounded-2xl border border-gray-200 p-3 text-[15px] focus:outline-none focus:ring-2 focus:ring-gray-300" placeholder="Sample question…" value={sampleDraft} onChange={(e) => setSampleDraft(e.target.value)} /> | |
| <button onClick={addSample} className="rounded-xl border border-gray-300 px-4 py-2 text-sm hover:shadow active:scale-[0.98]">Add</button> | |
| </div> | |
| <div className="mt-2 flex flex-wrap gap-2"> | |
| {sampleQs.map((q, i) => ( | |
| <Pill key={i} text={q} onRemove={() => removeSample(i)} /> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="pt-4"> | |
| <div className="flex flex-col sm:flex-row gap-2"> | |
| <input className="flex-1 rounded-2xl border border-gray-200 p-3 text-[15px] focus:outline-none focus:ring-2 focus:ring-gray-300" placeholder="Add tag…" value={tagDraft} onChange={(e) => setTagDraft(e.target.value)} /> | |
| <button onClick={addTag} className="rounded-xl border border-gray-300 px-4 py-2 text-sm hover:shadow active:scale-[0.98]">Add</button> | |
| </div> | |
| <div className="mt-2 flex flex-wrap gap-2"> | |
| {tags.map((t, i) => ( | |
| <Pill key={i} text={t} onRemove={() => removeTag(i)} /> | |
| ))} | |
| </div> | |
| </div> | |
| </Section> | |
| {/* Output preview */} | |
| <Section title="Generated Schema (JSON)" defaultOpen hint="Live preview"> | |
| <div className="rounded-2xl border border-gray-200 bg-white"> | |
| <pre className="max-h-[50vh] overflow-auto p-4 text-[13px] leading-6"> | |
| {JSON.stringify(output, null, 2)} | |
| </pre> | |
| </div> | |
| <p className="mt-2 text-xs text-gray-500">Tip: Use “Copy JSON” or “Download JSON” above. Convert to YAML externally if needed.</p> | |
| </Section> | |
| </main> | |
| {/* Bottom action bar on very small screens */} | |
| <div className="sticky bottom-0 z-30 border-t border-gray-200 bg-white/90 backdrop-blur px-4 py-2 sm:hidden"> | |
| <div className="grid grid-cols-2 gap-2"> | |
| <button onClick={copyJSON} className="rounded-xl border border-gray-300 px-4 py-2 text-sm hover:shadow active:scale-[0.98]">Copy</button> | |
| <button onClick={downloadJSON} className="rounded-xl border border-gray-300 px-4 py-2 text-sm hover:shadow active:scale-[0.98]">Download</button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |