Skip to content

Commit 71b7fa5

Browse files
committed
Schema validation on every generator + a11y pass on custom forms
Schema rollout (26 pages): - One file lib/schemas/generators.ts exporting Zod schemas for every generator that uses GeneratorShell with expectJson:true. Schemas are deliberately lenient (.passthrough(), most fields optional) so the retry only fires on genuinely broken shapes — not pedantic mismatches that would burn tokens. - Wired into the configs of all generate/* + optimize/* pages. - Discriminated unions for tiktok (hooks vs scripts), twitter (thread vs tweets), youtube (variants vs scripts vs headlines). - Every generator now self-corrects via the retry path added in d1d9da5 when the model returns the wrong shape. A11y label/input pairing on the 5 non-shell pages: - Settings: htmlFor/id on api-key input (per provider) + model select - Setup: htmlFor/id on api-key + website-url + brand-name + aria-label on the show/hide-key toggle button - Brand/new: aria-label on the Method-1 URL input, file input, and stop button (was a tooltip-only "Cancel") - Competitors: htmlFor/id on all 5 form fields (our_product, our_usp, competitor_name, target_platform, competitor_ads) + sr-only label on the library-search input + aria-required on the paste textarea - Launch wizard: htmlFor/id on all 6 form fields + role="group" + aria-labelledby on the platforms toggle bank + aria-pressed on each platform button + py-2 mobile / py-1 desktop touch targets Smoke filter: - Filter the benign "Failed to fetch RSC payload" prefetch race that Next 14 emits in Playwright when the test browser closes before a Link prefetch lands. Framework falls back to full navigation; not a real bug. Without this filter 29/47 routes false-flag. Tooling: - Added @playwright/test as devDep (was an implicit npx download before; now an explicit dependency for reproducible smoke runs). Verified: tsc clean, 53/53 unit tests pass, next build clean, 47/47 Playwright smoke pass against the production build on port 3055.
1 parent d1d9da5 commit 71b7fa5

35 files changed

Lines changed: 756 additions & 32 deletions

File tree

app/brand/new/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -722,13 +722,14 @@ function Inner() {
722722
onChange={(e) => setQuickUrl(e.target.value)}
723723
onKeyDown={(e) => { if (e.key === "Enter") quickAddFromUrl(); }}
724724
disabled={quickBusy}
725+
aria-label="Client website URL or business name"
725726
/>
726727
<button onClick={quickAddFromUrl} disabled={quickBusy || !quickUrl.trim()} className="btn-primary">
727728
{quickBusy ? <Loader2 size={11} className="animate-spin" /> : <Plus size={11} />}
728729
{quickBusy ? "extracting" : "extract"}
729730
</button>
730731
{quickBusy ? (
731-
<button onClick={stopExtraction} className="btn-ghost" title="Cancel the extraction (no charge for partial work)">
732+
<button onClick={stopExtraction} className="btn-ghost" title="Cancel the extraction (no charge for partial work)" aria-label="Cancel extraction">
732733
<StopCircle size={11} /> stop
733734
</button>
734735
) : null}
@@ -846,13 +847,14 @@ function Inner() {
846847
onChange={(e) => setVisionFile(e.target.files?.[0] ?? null)}
847848
disabled={quickBusy}
848849
className="text-[11px] font-mono text-ink-muted"
850+
aria-label="Upload homepage screenshot for vision extraction"
849851
/>
850852
<button onClick={quickAddFromVision} disabled={quickBusy || !visionFile} className="btn-primary">
851853
{quickBusy ? <Loader2 size={11} className="animate-spin" /> : <ImageIcon size={11} />}
852854
{quickBusy ? "reading…" : "extract from image"}
853855
</button>
854856
{quickBusy ? (
855-
<button onClick={stopExtraction} className="btn-ghost" title="Cancel">
857+
<button onClick={stopExtraction} className="btn-ghost" title="Cancel" aria-label="Cancel image extraction">
856858
<StopCircle size={11} /> stop
857859
</button>
858860
) : null}

app/generate/branded-hashtag-challenge/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Section, Pill, Kv } from "@/components/OutputBlocks";
55
import { CopyButton } from "@/components/CopyButton";
66
import { buildBhcPrompt, type BhcInput } from "@/lib/prompts/tiktok-bhc";
77
import type { GeneratorConfig } from "@/lib/generator-config";
8+
import { BrandedHashtagChallengeSchema } from "@/lib/schemas/generators";
89

910
const config: GeneratorConfig<BhcInput & Record<string, unknown>> = {
1011
title: "TikTok Branded Hashtag Challenge",
@@ -34,6 +35,7 @@ const config: GeneratorConfig<BhcInput & Record<string, unknown>> = {
3435
buildPrompt: (input) => buildBhcPrompt(input as unknown as BhcInput),
3536
buildTitle: (i: any) => `BHC · ${i.brand}`,
3637
expectJson: true,
38+
schema: BrandedHashtagChallengeSchema,
3739
renderJson: (json) => <BhcOutput json={json} />,
3840
};
3941

app/generate/creative-prompts/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
type CreativeToolLink,
1313
} from "@/lib/prompts/creative-prompts";
1414
import type { GeneratorConfig } from "@/lib/generator-config";
15+
import { CreativePromptsSchema } from "@/lib/schemas/generators";
1516

1617
const config: GeneratorConfig<CreativePromptInput & Record<string, unknown>> = {
1718
title: "AI Creative Prompt Generator",
@@ -89,6 +90,7 @@ const config: GeneratorConfig<CreativePromptInput & Record<string, unknown>> = {
8990
buildPrompt: (input) => buildCreativePromptPrompt(input as unknown as CreativePromptInput),
9091
buildTitle: (i: any) => `Creative · ${i.asset_type} · ${i.product?.slice(0, 24)}`,
9192
expectJson: true,
93+
schema: CreativePromptsSchema,
9294
renderJson: (json) => <CreativePromptOutput json={json} />,
9395
};
9496

app/generate/display/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CharBadge } from "@/components/CharBadge";
66
import { CopyButton } from "@/components/CopyButton";
77
import { buildDisplayPrompt, DISPLAY_SIZES, type DisplayInput } from "@/lib/prompts/display-ads";
88
import type { GeneratorConfig } from "@/lib/generator-config";
9+
import { DisplaySchema } from "@/lib/schemas/generators";
910

1011
const config: GeneratorConfig<DisplayInput & Record<string, unknown>> = {
1112
title: "Display Banners",
@@ -23,6 +24,7 @@ const config: GeneratorConfig<DisplayInput & Record<string, unknown>> = {
2324
buildPrompt: (input) => buildDisplayPrompt(input as unknown as DisplayInput),
2425
buildTitle: (i: any) => `Display · ${i.product}`,
2526
expectJson: true,
27+
schema: DisplaySchema,
2628
renderJson: (json) => <DisplayOutput json={json} />,
2729
};
2830

app/generate/email-subjects/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CharBadge } from "@/components/CharBadge";
66
import { CopyButton } from "@/components/CopyButton";
77
import { buildEmailSubjectsPrompt, EMAIL_SUBJECT_LIMITS, type EmailSubjectInput } from "@/lib/prompts/email-subjects";
88
import type { GeneratorConfig } from "@/lib/generator-config";
9+
import { EmailSubjectsSchema } from "@/lib/schemas/generators";
910

1011
const config: GeneratorConfig<EmailSubjectInput & Record<string, unknown>> = {
1112
title: "Email subject lines",
@@ -36,6 +37,7 @@ const config: GeneratorConfig<EmailSubjectInput & Record<string, unknown>> = {
3637
buildPrompt: (input) => buildEmailSubjectsPrompt(input as unknown as EmailSubjectInput),
3738
buildTitle: (i: any) => `Email · ${i.campaign_type} · ${i.product_or_topic?.slice(0, 24)}`,
3839
expectJson: true,
40+
schema: EmailSubjectsSchema,
3941
renderJson: (json) => <EmailOutput json={json} />,
4042
};
4143

app/generate/google-pmax/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CharBadge } from "@/components/CharBadge";
66
import { CopyButton } from "@/components/CopyButton";
77
import { buildPmaxPrompt, PMAX_LIMITS, type PmaxInput } from "@/lib/prompts/google-pmax";
88
import type { GeneratorConfig } from "@/lib/generator-config";
9+
import { GooglePmaxSchema } from "@/lib/schemas/generators";
910

1011
const config: GeneratorConfig<PmaxInput & Record<string, unknown>> = {
1112
title: "Google Performance Max",
@@ -35,6 +36,7 @@ const config: GeneratorConfig<PmaxInput & Record<string, unknown>> = {
3536
buildPrompt: (input) => buildPmaxPrompt(input as unknown as PmaxInput),
3637
buildTitle: (i: any) => `PMax · ${i.product}`,
3738
expectJson: true,
39+
schema: GooglePmaxSchema,
3840
renderJson: (json) => <PmaxOutput json={json} />,
3941
};
4042

app/generate/google-shopping/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CharBadge } from "@/components/CharBadge";
66
import { CopyButton } from "@/components/CopyButton";
77
import { buildShoppingPrompt, SHOPPING_LIMITS, type ShoppingInput } from "@/lib/prompts/google-shopping";
88
import type { GeneratorConfig } from "@/lib/generator-config";
9+
import { GoogleShoppingSchema } from "@/lib/schemas/generators";
910

1011
const config: GeneratorConfig<ShoppingInput & Record<string, unknown>> = {
1112
title: "Google Shopping Optimizer",
@@ -28,6 +29,7 @@ const config: GeneratorConfig<ShoppingInput & Record<string, unknown>> = {
2829
buildPrompt: (input) => buildShoppingPrompt(input as unknown as ShoppingInput),
2930
buildTitle: (i: any) => `Shopping · ${i.product_title?.slice(0, 30)}`,
3031
expectJson: true,
32+
schema: GoogleShoppingSchema,
3133
renderJson: (json) => <ShoppingOutput json={json} />,
3234
};
3335

app/generate/google/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
type GoogleRsaOutput,
1313
} from "@/lib/prompts/google-ads";
1414
import type { GeneratorConfig } from "@/lib/generator-config";
15+
import { GoogleSchema } from "@/lib/schemas/generators";
1516

1617
const config: GeneratorConfig<GoogleRsaInput & Record<string, unknown>> = {
1718
title: "Google Responsive Search Ad",
@@ -42,6 +43,7 @@ const config: GeneratorConfig<GoogleRsaInput & Record<string, unknown>> = {
4243
buildPrompt: (input) => buildGoogleRsaPrompt(input as unknown as GoogleRsaInput),
4344
buildTitle: (input) => `${(input as any).product}${(input as any).keyword}`,
4445
expectJson: true,
46+
schema: GoogleSchema,
4547
renderJson: (json: GoogleRsaOutput) => <RsaOutput json={json} />,
4648
};
4749

app/generate/hashtags/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Section, Pill } from "@/components/OutputBlocks";
55
import { CopyButton } from "@/components/CopyButton";
66
import { buildHashtagPrompt, type HashtagInput } from "@/lib/prompts/hashtags";
77
import type { GeneratorConfig } from "@/lib/generator-config";
8+
import { HashtagsSchema } from "@/lib/schemas/generators";
89

910
const config: GeneratorConfig<HashtagInput & Record<string, unknown>> = {
1011
title: "Hashtag Generator",
@@ -37,6 +38,7 @@ const config: GeneratorConfig<HashtagInput & Record<string, unknown>> = {
3738
buildPrompt: (input) => buildHashtagPrompt(input as unknown as HashtagInput),
3839
buildTitle: (i: any) => `Hashtags · ${i.platform} · ${i.title?.slice(0, 28)}`,
3940
expectJson: true,
41+
schema: HashtagsSchema,
4042
renderJson: (json) => <HashtagOutput json={json} />,
4143
};
4244

app/generate/lead-form/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Section, Pill } from "@/components/OutputBlocks";
55
import { CopyButton } from "@/components/CopyButton";
66
import { buildLeadFormPrompt, type LeadFormInput } from "@/lib/prompts/lead-form";
77
import type { GeneratorConfig } from "@/lib/generator-config";
8+
import { LeadFormSchema } from "@/lib/schemas/generators";
89

910
const config: GeneratorConfig<LeadFormInput & Record<string, unknown>> = {
1011
title: "Native Lead Form",
@@ -42,6 +43,7 @@ const config: GeneratorConfig<LeadFormInput & Record<string, unknown>> = {
4243
buildPrompt: (input) => buildLeadFormPrompt(input as unknown as LeadFormInput),
4344
buildTitle: (i: any) => `Lead Form · ${i.platform} · ${i.offer?.slice(0, 24)}`,
4445
expectJson: true,
46+
schema: LeadFormSchema,
4547
renderJson: (json) => <LeadFormOutput json={json} />,
4648
};
4749

0 commit comments

Comments
 (0)