Optimize academy
This commit is contained in:
@@ -595,6 +595,192 @@ function PromptPlaceholderCard({ placeholder }) {
|
||||
)
|
||||
}
|
||||
|
||||
function PromptFilledExampleCard({ example, analytics, contentId, index }) {
|
||||
const placeholderEntries = Object.entries(example?.placeholder_values || {}).filter(([key, value]) => String(key || '').trim() && value != null && value !== '' && value !== false)
|
||||
|
||||
return (
|
||||
<article className="rounded-[28px] border border-white/10 bg-black/20 p-5 shadow-[0_18px_42px_rgba(2,6,23,0.16)]">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-violet-200/75">Filled example {index + 1}</p>
|
||||
<h3 className="mt-2 text-xl font-semibold tracking-[-0.03em] text-white">{example?.title || `Example ${index + 1}`}</h3>
|
||||
{example?.description ? <p className="mt-3 text-sm leading-7 text-slate-300">{example.description}</p> : null}
|
||||
</div>
|
||||
{example?.prompt ? (
|
||||
<PromptCopyButton
|
||||
prompt={example.prompt}
|
||||
label="Copy example"
|
||||
analytics={analytics}
|
||||
contentId={contentId}
|
||||
eventType="academy_prompt_filled_example_copy"
|
||||
metadata={{ copy_type: 'filled_example', filled_example_index: index, source: 'prompt_filled_examples' }}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{placeholderEntries.length ? (
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
{placeholderEntries.map(([key, value]) => (
|
||||
<span key={key} className="rounded-full border border-violet-300/20 bg-violet-300/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.16em] text-violet-100">
|
||||
{key}: <span className="normal-case tracking-normal text-white">{String(value)}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{example?.prompt ? <pre className="mt-5 whitespace-pre-wrap rounded-[24px] border border-white/10 bg-slate-950/80 p-4 text-sm leading-7 text-slate-100">{example.prompt}</pre> : null}
|
||||
|
||||
{example?.negative_prompt ? (
|
||||
<div className="mt-4 rounded-[24px] border border-white/10 bg-slate-950/60 p-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Negative prompt</p>
|
||||
<PromptCopyButton
|
||||
prompt={example.negative_prompt}
|
||||
label="Copy negative"
|
||||
analytics={analytics}
|
||||
contentId={contentId}
|
||||
eventType="academy_prompt_filled_example_negative_copy"
|
||||
metadata={{ copy_type: 'filled_example_negative', filled_example_index: index, source: 'prompt_filled_examples' }}
|
||||
/>
|
||||
</div>
|
||||
<pre className="mt-3 whitespace-pre-wrap text-sm leading-7 text-slate-200">{example.negative_prompt}</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function PromptFilledExamplesSection({ examples, analytics, contentId }) {
|
||||
const visibleExamples = Array.isArray(examples) ? examples.filter((example) => example && typeof example === 'object') : []
|
||||
const [activeExampleIndex, setActiveExampleIndex] = useState(0)
|
||||
const examplesScrollRef = useRef(null)
|
||||
const [canScrollExamplesLeft, setCanScrollExamplesLeft] = useState(false)
|
||||
const [canScrollExamplesRight, setCanScrollExamplesRight] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const updateExampleScrollState = () => {
|
||||
const element = examplesScrollRef.current
|
||||
if (!element) {
|
||||
setCanScrollExamplesLeft(false)
|
||||
setCanScrollExamplesRight(false)
|
||||
return
|
||||
}
|
||||
|
||||
const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth)
|
||||
setCanScrollExamplesLeft(element.scrollLeft > 6)
|
||||
setCanScrollExamplesRight(element.scrollLeft < maxScrollLeft - 6)
|
||||
}
|
||||
|
||||
updateExampleScrollState()
|
||||
|
||||
const element = examplesScrollRef.current
|
||||
if (!element) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
element.addEventListener('scroll', updateExampleScrollState, { passive: true })
|
||||
window.addEventListener('resize', updateExampleScrollState, { passive: true })
|
||||
|
||||
return () => {
|
||||
element.removeEventListener('scroll', updateExampleScrollState)
|
||||
window.removeEventListener('resize', updateExampleScrollState)
|
||||
}
|
||||
}, [visibleExamples.length])
|
||||
|
||||
useEffect(() => {
|
||||
if (!visibleExamples.length) {
|
||||
setActiveExampleIndex(0)
|
||||
return
|
||||
}
|
||||
|
||||
setActiveExampleIndex((current) => Math.max(0, Math.min(current, visibleExamples.length - 1)))
|
||||
}, [visibleExamples.length])
|
||||
|
||||
if (!visibleExamples.length) return null
|
||||
|
||||
const activeExample = visibleExamples[activeExampleIndex] || visibleExamples[0]
|
||||
const activeExampleLabel = String(activeExample?.title || '').trim() || `Example ${activeExampleIndex + 1}`
|
||||
const activeExampleDescription = String(activeExample?.description || '').trim()
|
||||
|
||||
const scrollExamples = (direction) => {
|
||||
const element = examplesScrollRef.current
|
||||
if (!element) return
|
||||
|
||||
const amount = Math.max(220, Math.floor(element.clientWidth * 0.65))
|
||||
element.scrollBy({
|
||||
left: direction === 'left' ? -amount : amount,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6 space-y-5">
|
||||
<div className="rounded-[24px] border border-violet-300/15 bg-violet-300/10 p-5 md:p-6">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-violet-100/80">Selected example</p>
|
||||
<h3 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-[2rem]">{activeExampleLabel}</h3>
|
||||
{activeExampleDescription ? <p className="mt-3 text-sm leading-7 text-slate-200 md:text-base">{activeExampleDescription}</p> : null}
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className={`pointer-events-none absolute inset-y-0 left-0 z-10 w-14 bg-gradient-to-r from-[#211c3a] via-[#211c3a]/85 to-transparent transition ${canScrollExamplesLeft ? 'opacity-100' : 'opacity-0'}`} aria-hidden="true" />
|
||||
<div className={`pointer-events-none absolute inset-y-0 right-0 z-10 w-14 bg-gradient-to-l from-[#211c3a] via-[#211c3a]/85 to-transparent transition ${canScrollExamplesRight ? 'opacity-100' : 'opacity-0'}`} aria-hidden="true" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Scroll filled examples left"
|
||||
onClick={() => scrollExamples('left')}
|
||||
className={`absolute left-2 top-1/2 z-20 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-white/12 bg-slate-950/80 text-white/80 shadow-[0_16px_36px_rgba(2,6,23,0.28)] backdrop-blur transition ${canScrollExamplesLeft ? 'opacity-100 hover:scale-105 hover:bg-slate-900/95' : 'pointer-events-none opacity-0'}`}
|
||||
>
|
||||
<i className="fa-solid fa-chevron-left text-sm" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Scroll filled examples right"
|
||||
onClick={() => scrollExamples('right')}
|
||||
className={`absolute right-2 top-1/2 z-20 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-white/12 bg-slate-950/80 text-white/80 shadow-[0_16px_36px_rgba(2,6,23,0.28)] backdrop-blur transition ${canScrollExamplesRight ? 'opacity-100 hover:scale-105 hover:bg-slate-900/95' : 'pointer-events-none opacity-0'}`}
|
||||
>
|
||||
<i className="fa-solid fa-chevron-right text-sm" />
|
||||
</button>
|
||||
|
||||
<div ref={examplesScrollRef} className="overflow-x-auto pb-1 scrollbar-none [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
<div className="inline-flex min-w-full gap-2.5 px-1 py-1">
|
||||
{visibleExamples.map((example, index) => {
|
||||
const isActive = index === activeExampleIndex
|
||||
const exampleLabel = String(example?.title || '').trim() || `Example ${index + 1}`
|
||||
|
||||
return (
|
||||
<button
|
||||
key={`${example.title || 'filled-example-tab'}-${index}`}
|
||||
type="button"
|
||||
onClick={() => setActiveExampleIndex(index)}
|
||||
aria-pressed={isActive}
|
||||
title={exampleLabel}
|
||||
className={`max-w-full whitespace-nowrap rounded-full border px-4 py-2.5 text-sm font-semibold uppercase tracking-[0.18em] transition ${isActive ? 'border-violet-300/30 bg-violet-300/18 text-white shadow-[0_12px_30px_rgba(76,29,149,0.24)]' : 'border-white/10 bg-white/[0.04] text-violet-100/80 hover:border-violet-300/20 hover:bg-violet-300/10 hover:text-white'}`}
|
||||
>
|
||||
<span className="block max-w-[240px] truncate">{exampleLabel}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PromptFilledExampleCard
|
||||
key={`${activeExample.title || 'filled-example-active'}-${activeExampleIndex}`}
|
||||
example={activeExample}
|
||||
analytics={analytics}
|
||||
contentId={contentId}
|
||||
index={activeExampleIndex}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PromptHelperPromptCard({ helperPrompt, analytics, contentId }) {
|
||||
if (!helperPrompt || typeof helperPrompt !== 'object') return null
|
||||
|
||||
@@ -1088,11 +1274,25 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat
|
||||
|| promptDocumentation.data_accuracy_notes.length,
|
||||
)
|
||||
const hasPromptPlaceholders = Boolean(item?.has_placeholder_inputs) && promptPlaceholders.length > 0
|
||||
const promptFilledExamples = Array.isArray(item?.filled_examples)
|
||||
? item.filled_examples.filter((example) => example && typeof example === 'object' && [
|
||||
example.title,
|
||||
example.description,
|
||||
example.prompt,
|
||||
example.negative_prompt,
|
||||
...(example.placeholder_values && typeof example.placeholder_values === 'object' ? Object.values(example.placeholder_values) : []),
|
||||
].some((value) => value != null && value !== '' && value !== false))
|
||||
: []
|
||||
const hasPromptFilledExamples = promptFilledExamples.length > 0
|
||||
const promptFilledExamplesTotal = Number(item?.filled_examples_total || promptFilledExamples.length || 0)
|
||||
const promptHasMoreFilledExamples = Boolean(item?.has_more_filled_examples) || promptFilledExamplesTotal > promptFilledExamples.length
|
||||
const promptHasFullFilledExamplesAccess = Boolean(item?.has_full_filled_examples_access)
|
||||
const promptHasLockedFilledExamples = Boolean(item?.has_filled_examples) && (!Boolean(item?.can_access_filled_examples) || promptHasMoreFilledExamples)
|
||||
const promptHasLockedHelperPrompts = Boolean(item?.has_helper_prompts) && !promptHasFullAccess
|
||||
const promptHasLockedVariants = Boolean(item?.has_prompt_variants) && !promptHasFullAccess
|
||||
const hasPromptHelperPrompts = promptHelperPrompts.length > 0
|
||||
const hasPromptVariants = promptVariants.length > 0
|
||||
const showPromptHelperPrompts = false
|
||||
const showPromptHelperPrompts = true
|
||||
const promptAccessRequirement = item?.access_requirement || promptRequirementText(item?.access_level)
|
||||
const promptUnlockTitle = item?.unlock_heading || promptUnlockHeading(item?.access_level)
|
||||
const promptUnlockDetails = item?.unlock_description || promptUnlockDescription(item?.access_level)
|
||||
@@ -2103,6 +2303,45 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{hasPromptFilledExamples ? (
|
||||
<section className="academy-paywalled-content rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(167,139,250,0.12),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 text-slate-200 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-8">
|
||||
<div className="max-w-3xl">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-violet-100/80">Filled examples</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl">
|
||||
{promptFilledExamplesTotal > 0 ? `${promptFilledExamplesTotal} ready-made prompt runs for real user inputs` : 'Ready-made prompt runs for real user inputs'}
|
||||
</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-slate-300 md:text-base">
|
||||
{promptHasMoreFilledExamples
|
||||
? `You can view ${promptFilledExamples.length} example${promptFilledExamples.length === 1 ? '' : 's'} right now. Upgrade to Pro to unlock all ${promptFilledExamplesTotal} filled prompt runs and copy a closer starting point instead of filling everything from scratch.`
|
||||
: 'These examples show how the prompt looks after swapping real placeholder values, so you can copy a closer starting point instead of filling everything from scratch.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<PromptFilledExamplesSection examples={promptFilledExamples} analytics={analytics} contentId={item.id} />
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{promptHasLockedFilledExamples ? (
|
||||
<section className="academy-paywalled-content rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(167,139,250,0.12),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 text-slate-200 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-8">
|
||||
<div className="max-w-3xl">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-violet-100/80">Filled examples</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl">
|
||||
{promptHasMoreFilledExamples && hasPromptFilledExamples
|
||||
? `${Math.max(promptFilledExamplesTotal - promptFilledExamples.length, 0)} more filled prompt example${promptFilledExamplesTotal - promptFilledExamples.length === 1 ? '' : 's'} are available`
|
||||
: `${promptFilledExamplesTotal || 5} filled prompt examples are included`}
|
||||
</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-slate-300 md:text-base">
|
||||
{promptHasMoreFilledExamples && hasPromptFilledExamples
|
||||
? 'Creator access includes a smaller set here. Upgrade to Academy Pro to unlock the remaining filled prompt runs.'
|
||||
: 'This prompt ships with ready-made filled examples for different user inputs, but they unlock only for Academy Pro members.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<LockedPanel pricingUrl={pricingUrl} label="prompt" accessLevel="pro" onUpgrade={() => trackUpgradeClick(analytics, { source: 'prompt_filled_examples_locked_panel' })} />
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{showPromptHelperPrompts && hasPromptHelperPrompts ? (
|
||||
<section className="academy-paywalled-content rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(255,183,139,0.12),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 text-slate-200 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-8">
|
||||
<div className="max-w-3xl">
|
||||
@@ -2230,4 +2469,4 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat
|
||||
<ImageLightbox gallery={lightboxGallery} onClose={() => setLightboxGallery(null)} onNavigate={navigateLightboxGallery} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user