Optimize academy
This commit is contained in:
@@ -52,6 +52,181 @@ function serializeStructuredJson(value) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseStructuredJson(value) {
|
||||
if (value == null || value === '') return null
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim()
|
||||
|
||||
if (!trimmed) {
|
||||
return null
|
||||
}
|
||||
|
||||
return JSON.parse(trimmed)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function toDisplayText(value) {
|
||||
if (value == null) return ''
|
||||
if (typeof value === 'string') return value.trim()
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
|
||||
if (Array.isArray(value)) return value.map((item) => toDisplayText(item)).filter(Boolean).join(', ')
|
||||
|
||||
try {
|
||||
return JSON.stringify(value)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function humanizePlaceholderKey(value) {
|
||||
const normalized = String(value || '')
|
||||
.replace(/[_-]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
|
||||
if (!normalized) {
|
||||
return 'Placeholder'
|
||||
}
|
||||
|
||||
return normalized
|
||||
.split(' ')
|
||||
.map((part) => part ? `${part.charAt(0).toUpperCase()}${part.slice(1).toLowerCase()}` : '')
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
function buildPlaceholderSeedValues(placeholder, limit = 5) {
|
||||
const readableLabel = humanizePlaceholderKey(placeholder?.label || placeholder?.key || 'Placeholder')
|
||||
const seeded = [
|
||||
placeholder?.example,
|
||||
placeholder?.default,
|
||||
...(Array.isArray(placeholder?.examples) ? placeholder.examples : []),
|
||||
...(Array.isArray(placeholder?.options) ? placeholder.options : []),
|
||||
...(Array.isArray(placeholder?.choices) ? placeholder.choices : []),
|
||||
...(Array.isArray(placeholder?.values) ? placeholder.values : []),
|
||||
]
|
||||
.map((entry) => toDisplayText(entry))
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const unique = Array.from(new Set(seeded))
|
||||
|
||||
while (unique.length < limit) {
|
||||
unique.push(`${readableLabel} ${unique.length + 1}`)
|
||||
}
|
||||
|
||||
return unique.slice(0, limit)
|
||||
}
|
||||
|
||||
function normalizePromptPlaceholders(value) {
|
||||
if (!Array.isArray(value)) return []
|
||||
|
||||
return value
|
||||
.map((placeholder) => {
|
||||
if (!placeholder || typeof placeholder !== 'object') return null
|
||||
|
||||
const key = String(placeholder.key || '').trim()
|
||||
const label = String(placeholder.label || '').trim()
|
||||
|
||||
if (!key && !label) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...placeholder,
|
||||
key,
|
||||
label,
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function applyPlaceholderValuesToPrompt(template, placeholderValues, placeholders) {
|
||||
let nextText = String(template || '')
|
||||
let replacementCount = 0
|
||||
|
||||
placeholders.forEach((placeholder) => {
|
||||
const key = String(placeholder?.key || '').trim()
|
||||
if (!key) return
|
||||
|
||||
const replacement = toDisplayText(placeholderValues[key])
|
||||
if (!replacement) return
|
||||
|
||||
const patterns = [
|
||||
new RegExp(`\\[${escapeRegExp(key)}\\]`, 'g'),
|
||||
new RegExp(`\\{\\{\\s*${escapeRegExp(key)}\\s*\\}\\}`, 'g'),
|
||||
new RegExp(`\\{${escapeRegExp(key)}\\}`, 'g'),
|
||||
new RegExp(`<${escapeRegExp(key)}>`, 'g'),
|
||||
]
|
||||
|
||||
patterns.forEach((pattern) => {
|
||||
nextText = nextText.replace(pattern, () => {
|
||||
replacementCount += 1
|
||||
return replacement
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
if (replacementCount === 0 && placeholders.length > 0) {
|
||||
const placeholderSummary = placeholders
|
||||
.map((placeholder) => {
|
||||
const key = String(placeholder?.key || '').trim()
|
||||
if (!key) return null
|
||||
|
||||
const readableLabel = humanizePlaceholderKey(placeholder.label || key)
|
||||
const replacement = toDisplayText(placeholderValues[key])
|
||||
|
||||
return replacement ? `- ${readableLabel}: ${replacement}` : null
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
|
||||
if (placeholderSummary) {
|
||||
nextText = `${nextText.trim()}\n\nPlaceholder values:\n${placeholderSummary}`.trim()
|
||||
}
|
||||
}
|
||||
|
||||
return nextText.trim()
|
||||
}
|
||||
|
||||
function buildStarterFilledExamples({ title, excerpt, prompt, negativePrompt, placeholders }) {
|
||||
const normalizedPlaceholders = normalizePromptPlaceholders(placeholders)
|
||||
const exampleCount = Math.min(5, Math.max(1, normalizedPlaceholders.length ? 5 : 1))
|
||||
const fallbackTitle = stripPlainText(title) || 'Prompt'
|
||||
const fallbackDescription = stripPlainText(excerpt) || 'Starter example generated from the current placeholders. Review and refine before publishing.'
|
||||
|
||||
return Array.from({ length: exampleCount }, (_, index) => {
|
||||
const placeholderValues = normalizedPlaceholders.reduce((accumulator, placeholder) => {
|
||||
const key = String(placeholder?.key || '').trim()
|
||||
if (!key) return accumulator
|
||||
|
||||
const seeds = buildPlaceholderSeedValues(placeholder, 5)
|
||||
accumulator[key] = seeds[index % seeds.length]
|
||||
return accumulator
|
||||
}, {})
|
||||
|
||||
const titleParts = Object.values(placeholderValues)
|
||||
.map((value) => stripPlainText(value))
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
|
||||
return {
|
||||
title: titleParts.length > 0
|
||||
? `Example ${index + 1} · ${titleParts.join(' · ')}`.slice(0, 180)
|
||||
: `Example ${index + 1} · ${fallbackTitle}`.slice(0, 180),
|
||||
description: `${fallbackDescription} Starter ${index + 1} for editors.`.trim(),
|
||||
placeholder_values: placeholderValues,
|
||||
prompt: applyPlaceholderValuesToPrompt(prompt, placeholderValues, normalizedPlaceholders),
|
||||
negative_prompt: negativePrompt ? applyPlaceholderValuesToPrompt(negativePrompt, placeholderValues, normalizedPlaceholders) : '',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function copyTextToClipboard(text) {
|
||||
const source = String(text || '')
|
||||
if (!source) return Promise.reject(new Error('Nothing to copy'))
|
||||
@@ -229,6 +404,7 @@ const PROMPT_FIELD_TAB_MAP = {
|
||||
placeholders: 'advanced',
|
||||
helper_prompts: 'advanced',
|
||||
prompt_variants: 'advanced',
|
||||
filled_examples: 'advanced',
|
||||
preview_image: 'media',
|
||||
preview_image_file: 'media',
|
||||
published_at: 'publish',
|
||||
@@ -436,6 +612,7 @@ function parsePromptImport(rawText, categoryOptions) {
|
||||
if (parsed.placeholders != null) apply('placeholders', serializeStructuredJson(parsed.placeholders))
|
||||
if (parsed.helper_prompts != null) apply('helper_prompts', serializeStructuredJson(parsed.helper_prompts))
|
||||
if (parsed.prompt_variants != null) apply('prompt_variants', serializeStructuredJson(parsed.prompt_variants))
|
||||
if (parsed.filled_examples != null) apply('filled_examples', serializeStructuredJson(parsed.filled_examples))
|
||||
if (parsed.preview_image != null) apply('preview_image', String(parsed.preview_image))
|
||||
if (parsed.preview_image_url != null && parsed.preview_image == null) apply('preview_image', String(parsed.preview_image_url))
|
||||
if (parsed.published_at != null) apply('published_at', String(parsed.published_at))
|
||||
@@ -619,6 +796,30 @@ function PromptJsonImportDialog({ open, value, error, onChange, onClose, onApply
|
||||
active: true,
|
||||
},
|
||||
],
|
||||
filled_examples: [
|
||||
{
|
||||
title: 'Alpine sunrise travel poster',
|
||||
description: 'A scenic poster version tuned for crisp mountain light and clean copy-safe composition.',
|
||||
placeholder_values: {
|
||||
LOCATION: 'Lake Bled, Slovenia',
|
||||
SEASON: 'spring',
|
||||
MOOD: 'calm sunrise',
|
||||
},
|
||||
prompt: 'Create a calm sunrise travel poster of Lake Bled in spring, with clear mountain reflections, light mist, soft golden light, and a clean editorial composition.',
|
||||
negative_prompt: 'muddy light, cluttered foreground, oversharpening, distorted architecture',
|
||||
},
|
||||
{
|
||||
title: 'Misty forest variant',
|
||||
description: 'Leans into atmosphere and fog while keeping the same placeholder structure.',
|
||||
placeholder_values: {
|
||||
LOCATION: 'Triglav National Park',
|
||||
SEASON: 'autumn',
|
||||
MOOD: 'misty cinematic',
|
||||
},
|
||||
prompt: 'Create a cinematic autumn landscape in Triglav National Park with layered mist, warm foliage, soft directional light, and strong depth.',
|
||||
negative_prompt: 'flat composition, weak fog, repetitive trees, blown highlights',
|
||||
},
|
||||
],
|
||||
preview_image: 'https://files.skinbase.org/prompts/peaceful-fantasy-forest.webp',
|
||||
featured: false,
|
||||
prompt_of_week: false,
|
||||
@@ -659,6 +860,7 @@ Recommended fields:
|
||||
- placeholders: array of prompt variable objects
|
||||
- helper_prompts: array of supporting prompts used before or after the main prompt
|
||||
- prompt_variants: array of alternative prompt versions
|
||||
- filled_examples: array of up to 5 filled prompt examples with placeholder_values and final prompts
|
||||
- preview_image: path or URL
|
||||
- featured: boolean
|
||||
- prompt_of_week: boolean
|
||||
@@ -698,10 +900,18 @@ prompt_variants object fields:
|
||||
- risk_notes
|
||||
- active boolean
|
||||
|
||||
filled_examples object fields:
|
||||
- title
|
||||
- description
|
||||
- placeholder_values: object keyed by placeholder name
|
||||
- prompt
|
||||
- negative_prompt
|
||||
|
||||
Rules:
|
||||
- Return one JSON object only.
|
||||
- Keep excerpt concise and readable in cards.
|
||||
- Keep tags relevant and production-usable.
|
||||
- Include exactly 5 filled_examples whenever the prompt uses placeholders or has clear user-editable parameters.
|
||||
- If you include tool_notes, keep them normalized and consistent.`
|
||||
|
||||
const aiPromptExamples = [
|
||||
@@ -714,6 +924,7 @@ Create a Skinbase Academy prompt template JSON object from the following creativ
|
||||
- Write a prompt that is immediately usable.
|
||||
- Write an excerpt that works in cards and search results.
|
||||
- Add 5 to 12 focused tags.
|
||||
- Include 5 filled_examples with realistic placeholder_values and ready-to-copy final prompts.
|
||||
- Include 2 to 4 tool_notes comparisons when the brief mentions multiple AI providers.
|
||||
|
||||
Creative brief:
|
||||
@@ -727,6 +938,7 @@ Generate a prompt template JSON object for Skinbase Academy.
|
||||
- Focus on the same core prompt being tested across multiple AI image providers.
|
||||
- Include tool_notes entries for each provider.
|
||||
- Each tool_notes item should explain settings, strengths, weaknesses, and best_for in plain production language.
|
||||
- Include 5 filled_examples that show how users would swap placeholder values in real projects.
|
||||
- Return JSON only.
|
||||
|
||||
Source notes:
|
||||
@@ -740,6 +952,7 @@ Convert the following source prompt page into structured Skinbase Academy prompt
|
||||
- Preserve the core instruction intent.
|
||||
- Normalize tags and metadata.
|
||||
- Convert provider reviews into tool_notes.
|
||||
- Generate 5 filled_examples that demonstrate realistic filled-in prompt runs for end users.
|
||||
- Use category/category_slug when category_id is unknown.
|
||||
- Return JSON only.
|
||||
|
||||
@@ -832,6 +1045,7 @@ Source content:
|
||||
<p>usage_notes, workflow_notes</p>
|
||||
<p>documentation, placeholders</p>
|
||||
<p>helper_prompts, prompt_variants</p>
|
||||
<p>filled_examples</p>
|
||||
<p>preview_image, preview_image_url</p>
|
||||
<p>published_at, seo_title, seo_description</p>
|
||||
<p>featured, prompt_of_week, active</p>
|
||||
@@ -855,7 +1069,7 @@ Source content:
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Notes</div>
|
||||
<div className="mt-3 space-y-3 leading-6 text-slate-400">
|
||||
<p>`tool_notes` can be an array of comparison objects or a simpler array under `comparisons`.</p>
|
||||
<p>`documentation`, `placeholders`, `helper_prompts`, and `prompt_variants` can be nested JSON and are preserved during import.</p>
|
||||
<p>`documentation`, `placeholders`, `helper_prompts`, `prompt_variants`, and `filled_examples` can be nested JSON and are preserved during import.</p>
|
||||
<p>`tags` can be strings or objects with `name`, `label`, `title`, or `slug`.</p>
|
||||
<p>`preview_image` accepts either a stored path or an external URL.</p>
|
||||
</div>
|
||||
@@ -876,6 +1090,7 @@ Source content:
|
||||
<p><strong className="text-slate-200">placeholders</strong> - prompt variables such as `CITY_NAME` or `MONTHLY_WEATHER_DATA`.</p>
|
||||
<p><strong className="text-slate-200">helper_prompts</strong> - supporting prompts for data collection, validation, or refinement.</p>
|
||||
<p><strong className="text-slate-200">prompt_variants</strong> - alternative versions of the same prompt for safer or model-specific output.</p>
|
||||
<p><strong className="text-slate-200">filled_examples</strong> - up to 5 ready-to-copy filled prompt runs that show real placeholder substitutions.</p>
|
||||
<p><strong className="text-slate-200">tool_notes</strong> - structured comparison notes for provider/model variants.</p>
|
||||
<p><strong className="text-slate-200">preview_image</strong> - existing asset URL or stored path. File upload still happens separately.</p>
|
||||
<p><strong className="text-slate-200">category_id</strong> is preferred when known. `category` or `category_slug` are used for best-effort matching.</p>
|
||||
@@ -889,7 +1104,7 @@ Source content:
|
||||
<p>Use JSON booleans for featured, prompt_of_week, and active.</p>
|
||||
<p>Use `YYYY-MM-DD HH:MM:SS` for `published_at` when scheduling is needed.</p>
|
||||
<p>Use `documentation` for longer public guidance, and keep `usage_notes` short and practical.</p>
|
||||
<p>Use `helper_prompts` for data collection or validation prompts, and `prompt_variants` for safer or model-specific alternatives.</p>
|
||||
<p>Use `helper_prompts` for data collection or validation prompts, `prompt_variants` for safer or model-specific alternatives, and `filled_examples` for ready-made filled prompt runs.</p>
|
||||
<p>Keep comparison rows normalized so provider/model names remain consistent in the frontend.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -915,7 +1130,7 @@ Source content:
|
||||
<div className="mt-3 space-y-2 leading-6 text-slate-400">
|
||||
<p>Tell the model to return JSON only, with no explanation text.</p>
|
||||
<p>Ask for `tool_notes` when you want provider-by-provider comparison output.</p>
|
||||
<p>Ask for `documentation`, `placeholders`, `helper_prompts`, and `prompt_variants` only when the prompt needs advanced structure.</p>
|
||||
<p>Ask for `documentation`, `placeholders`, `helper_prompts`, `prompt_variants`, and `filled_examples` when the prompt needs advanced structure and user-ready examples.</p>
|
||||
<p>Tell the model to keep titles and tags production-ready, not overly verbose.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1799,6 +2014,7 @@ function PromptEditor({ title, subtitle, fields, record, submitUrl, indexUrl, de
|
||||
const placeholdersField = useMemo(() => getField(fields, 'placeholders'), [fields])
|
||||
const helperPromptsField = useMemo(() => getField(fields, 'helper_prompts'), [fields])
|
||||
const promptVariantsField = useMemo(() => getField(fields, 'prompt_variants'), [fields])
|
||||
const filledExamplesField = useMemo(() => getField(fields, 'filled_examples'), [fields])
|
||||
const slugTouchedRef = useRef(Boolean(String(record.slug || '').trim()))
|
||||
const [activeTab, setActiveTab] = useState('overview')
|
||||
const [jsonImportOpen, setJsonImportOpen] = useState(false)
|
||||
@@ -1876,6 +2092,60 @@ function PromptEditor({ title, subtitle, fields, record, submitUrl, indexUrl, de
|
||||
}
|
||||
}
|
||||
|
||||
const generateStarterFilledExamples = () => {
|
||||
let parsedPlaceholders
|
||||
|
||||
try {
|
||||
parsedPlaceholders = parseStructuredJson(form.data.placeholders)
|
||||
} catch {
|
||||
const message = `${placeholdersField?.label || 'Placeholders JSON'} must be valid JSON before generating filled examples.`
|
||||
form.setError('placeholders', message)
|
||||
setActiveTab('advanced')
|
||||
showToast(message, 'error')
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedPlaceholders = normalizePromptPlaceholders(parsedPlaceholders)
|
||||
|
||||
if (normalizedPlaceholders.length === 0) {
|
||||
const message = 'Add at least one placeholder before generating starter filled examples.'
|
||||
form.setError('placeholders', message)
|
||||
setActiveTab('advanced')
|
||||
showToast(message, 'error')
|
||||
return
|
||||
}
|
||||
|
||||
const promptText = String(form.data.prompt || '').trim()
|
||||
|
||||
if (!promptText) {
|
||||
const message = 'Write the main prompt before generating starter filled examples.'
|
||||
form.setError('prompt', message)
|
||||
setActiveTab('prompt')
|
||||
showToast(message, 'error')
|
||||
return
|
||||
}
|
||||
|
||||
const existingExamples = String(form.data.filled_examples || '').trim()
|
||||
|
||||
if (existingExamples && typeof window !== 'undefined' && !window.confirm('Replace the current filled examples with a new 5-example starter set?')) {
|
||||
return
|
||||
}
|
||||
|
||||
const generatedExamples = buildStarterFilledExamples({
|
||||
title: form.data.title,
|
||||
excerpt: form.data.excerpt,
|
||||
prompt: promptText,
|
||||
negativePrompt: form.data.negative_prompt,
|
||||
placeholders: normalizedPlaceholders,
|
||||
})
|
||||
|
||||
form.clearErrors('placeholders')
|
||||
form.clearErrors('filled_examples')
|
||||
form.setData('filled_examples', serializeStructuredJson(generatedExamples))
|
||||
setActiveTab('advanced')
|
||||
showToast('Generated 5 starter filled examples. Review them before saving.', 'success')
|
||||
}
|
||||
|
||||
const submit = (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -1884,6 +2154,7 @@ function PromptEditor({ title, subtitle, fields, record, submitUrl, indexUrl, de
|
||||
{ name: 'placeholders', label: placeholdersField?.label || 'Placeholders JSON' },
|
||||
{ name: 'helper_prompts', label: helperPromptsField?.label || 'Helper Prompts JSON' },
|
||||
{ name: 'prompt_variants', label: promptVariantsField?.label || 'Prompt Variants JSON' },
|
||||
{ name: 'filled_examples', label: filledExamplesField?.label || 'Filled Examples JSON' },
|
||||
]
|
||||
const parsedJsonFields = {}
|
||||
|
||||
@@ -1943,6 +2214,12 @@ function PromptEditor({ title, subtitle, fields, record, submitUrl, indexUrl, de
|
||||
form.post(submitUrl, submitOptions)
|
||||
}
|
||||
|
||||
const hasRequiredCategory = useMemo(() => {
|
||||
const existing = String(form.data.category_id || '').trim()
|
||||
const named = String(form.data.new_category_name || '').trim()
|
||||
return Boolean(existing || named)
|
||||
}, [form.data.category_id, form.data.new_category_name])
|
||||
|
||||
return (
|
||||
<AdminLayout title={title} subtitle={subtitle}>
|
||||
<Head title={`Admin · ${title}`} />
|
||||
@@ -1996,7 +2273,7 @@ function PromptEditor({ title, subtitle, fields, record, submitUrl, indexUrl, de
|
||||
<i className="fa-solid fa-file-import text-xs" />
|
||||
<span>Import JSON</span>
|
||||
</button>
|
||||
<button type="submit" disabled={form.processing} className="inline-flex items-center gap-2 whitespace-nowrap rounded-2xl border border-sky-300/25 bg-sky-300/18 px-4 py-2 text-sm font-semibold text-sky-100 shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] transition hover:bg-sky-300/24">
|
||||
<button type="submit" disabled={form.processing || !hasRequiredCategory} className="inline-flex items-center gap-2 whitespace-nowrap rounded-2xl border border-sky-300/25 bg-sky-300/18 px-4 py-2 text-sm font-semibold text-sky-100 shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] transition hover:bg-sky-300/24">
|
||||
<i className="fa-solid fa-floppy-disk text-xs" />
|
||||
<span>{form.processing ? 'Saving...' : 'Save prompt'}</span>
|
||||
</button>
|
||||
@@ -2034,6 +2311,9 @@ function PromptEditor({ title, subtitle, fields, record, submitUrl, indexUrl, de
|
||||
}
|
||||
}} options={categoryOptions} searchable searchPlaceholder="Filter categories..." className="rounded-2xl bg-black/20" error={form.errors.category_id} /> : null}
|
||||
<TextField label="Or enter new category" value={form.data.new_category_name || ''} onChange={(event) => form.setData('new_category_name', event.target.value)} error={form.errors.new_category_name} placeholder="New prompt category name" />
|
||||
{!hasRequiredCategory ? (
|
||||
<div className="mt-2 text-xs text-rose-300">Choose an existing category or enter a new category name before saving.</div>
|
||||
) : null}
|
||||
{difficultyField ? <NovaSelect label={difficultyField.label} value={form.data.difficulty ?? ''} onChange={(nextValue) => form.setData('difficulty', nextValue ?? '')} options={difficultyField.options || []} searchable={false} className="rounded-2xl bg-black/20" error={form.errors.difficulty} /> : null}
|
||||
</div>
|
||||
|
||||
@@ -2085,6 +2365,16 @@ function PromptEditor({ title, subtitle, fields, record, submitUrl, indexUrl, de
|
||||
<TextAreaField label={helperPromptsField?.label || 'Helper Prompts JSON'} value={form.data.helper_prompts || ''} onChange={(event) => form.setData('helper_prompts', event.target.value)} error={form.errors.helper_prompts} rows={12} hint="Array of supporting prompts used for data collection, preparation, validation, or refinement." />
|
||||
</div>
|
||||
<TextAreaField label={promptVariantsField?.label || 'Prompt Variants JSON'} value={form.data.prompt_variants || ''} onChange={(event) => form.setData('prompt_variants', event.target.value)} error={form.errors.prompt_variants} rows={12} hint="Array of alternative prompt versions with prompt, negative_prompt, recommended flags, and risk notes." />
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-[24px] border border-white/10 bg-black/20 px-4 py-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">Starter filled examples</p>
|
||||
<p className="mt-1 text-xs leading-5 text-slate-400">Generate 5 editable examples from the current placeholders, prompt text, and negative prompt.</p>
|
||||
</div>
|
||||
<button type="button" onClick={generateStarterFilledExamples} className="rounded-full border border-amber-300/25 bg-amber-300/12 px-4 py-2.5 text-sm font-semibold text-amber-100 transition hover:bg-amber-300/18">
|
||||
Generate 5 starter examples
|
||||
</button>
|
||||
</div>
|
||||
<TextAreaField label={filledExamplesField?.label || 'Filled Examples JSON'} value={form.data.filled_examples || ''} onChange={(event) => form.setData('filled_examples', event.target.value)} error={form.errors.filled_examples} rows={12} hint="Array of up to 5 filled prompt examples with title, description, placeholder_values, prompt, and optional negative_prompt." />
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard eyebrow="Structured blocks" title="AI model comparisons" description="Add reusable same-prompt comparison notes without burying provider-specific behavior inside the main prompt body." className={sectionClassName('prompt-comparisons')}>
|
||||
|
||||
Reference in New Issue
Block a user