create-ailk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -3
- package/component-catalog.md +456 -0
- package/dist/cli.js +92 -16
- package/dist/component-catalog.d.ts +11 -0
- package/dist/component-catalog.js +39 -0
- package/dist/copy.d.ts +7 -0
- package/dist/copy.js +24 -0
- package/dist/derive-architecture.d.ts +13 -0
- package/dist/derive-architecture.js +238 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +19 -2
- package/dist/lib/extract-module-bundle.d.ts +16 -0
- package/dist/lib/extract-module-bundle.js +184 -0
- package/dist/lib/fetch-module.d.ts +55 -0
- package/dist/lib/fetch-module.js +125 -0
- package/dist/lib/module-license-gate.d.ts +48 -0
- package/dist/lib/module-license-gate.js +67 -0
- package/dist/lib/module-tier.d.ts +35 -0
- package/dist/lib/module-tier.js +37 -0
- package/dist/module-architecture.d.ts +175 -0
- package/dist/module-architecture.js +663 -0
- package/dist/parse-args.d.ts +47 -1
- package/dist/parse-args.js +143 -2
- package/dist/programmatic.d.ts +160 -3
- package/dist/programmatic.js +196 -7
- package/dist/surfaces.d.ts +211 -0
- package/dist/surfaces.js +532 -0
- package/dist/sync-routes.d.ts +92 -0
- package/dist/sync-routes.js +350 -0
- package/package.json +12 -5
- package/templates/.claude/rules/architecture.md +6 -2
- package/templates/.claude/skills/README.md +9 -9
- package/templates/.claude/skills/add-schema/SKILL.md +14 -7
- package/templates/.claude/skills/add-schema/references/schema-types.md +109 -6
- package/templates/.claude/skills/provision-config/SKILL.md +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +498 -1
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +370 -0
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +376 -0
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +471 -0
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +365 -0
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +60 -0
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +446 -0
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +4 -4
- package/templates/.env.example +126 -2
- package/templates/CLAUDE.md +11 -5
- package/templates/README.md +1 -1
- package/templates/apps/api/.env.example +38 -0
- package/templates/apps/api/CLAUDE.md +78 -1
- package/templates/apps/api/LICENSE +201 -0
- package/templates/apps/api/api/index.ts +6 -141
- package/templates/apps/api/jest.config.cjs +16 -0
- package/templates/apps/api/package.json +22 -6
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +141 -0
- package/templates/apps/api/src/__tests__/server.test.ts +46 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +211 -0
- package/templates/apps/api/src/bin/abandon-sweep.ts +42 -0
- package/templates/apps/api/src/bin/deliverable-resend.ts +144 -0
- package/templates/apps/api/src/bin/deliverable-upload.ts +114 -0
- package/templates/apps/api/src/bin/followup-sweep.ts +44 -0
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +46 -0
- package/templates/apps/api/src/bin/seed-presets.ts +58 -0
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +104 -0
- package/templates/apps/api/src/bin/session-retention-sweep.ts +46 -0
- package/templates/apps/api/src/cli.ts +5 -11
- package/templates/apps/api/src/config/index.ts +35 -0
- package/templates/apps/api/src/config/modules.ts +43 -0
- package/templates/apps/api/src/lib/__mocks__/prisma-client.js +11 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -0
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +2 -2
- package/templates/apps/api/src/lib/site.ts +16 -0
- package/templates/apps/api/src/lib/stripe.ts +22 -0
- package/templates/apps/api/src/lib/tenant-db.ts +225 -0
- package/templates/apps/api/src/lib/ws-token.ts +91 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +7 -1
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +7 -1
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +91 -4
- package/templates/apps/api/src/middleware/auth.ts +43 -8
- package/templates/apps/api/src/middleware/rate-limit.ts +78 -3
- package/templates/apps/api/src/middleware/tenant.ts +99 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +92 -0
- package/templates/apps/api/src/openapi/spec.ts +312 -3
- package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +175 -0
- package/templates/apps/api/src/routes/aeo/index.ts +107 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +5 -2
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +22 -3
- package/templates/apps/api/src/routes/auth/tokens.ts +13 -1
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +450 -0
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +262 -0
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +436 -0
- package/templates/apps/api/src/routes/billing/index.ts +36 -0
- package/templates/apps/api/src/routes/billing/portal.ts +182 -0
- package/templates/apps/api/src/routes/billing/read.ts +75 -0
- package/templates/apps/api/src/routes/billing/usage.ts +153 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +99 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +20 -8
- package/templates/apps/api/src/routes/content/create.ts +1 -0
- package/templates/apps/api/src/routes/content/delete.ts +1 -0
- package/templates/apps/api/src/routes/content/index.ts +42 -6
- package/templates/apps/api/src/routes/content/update.ts +1 -0
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +393 -0
- package/templates/apps/api/src/routes/deliverables/index.ts +200 -0
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +443 -0
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +82 -0
- package/templates/apps/api/src/routes/flows/README.md +147 -0
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +752 -0
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +671 -0
- package/templates/apps/api/src/routes/flows/index.ts +202 -0
- package/templates/apps/api/src/routes/flows/recommender.ts +443 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +438 -31
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +10 -10
- package/templates/apps/api/src/routes/leads/index.ts +43 -9
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +558 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +815 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +869 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +221 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +468 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +424 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +707 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +375 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +467 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +566 -0
- package/templates/apps/api/src/routes/project-listings/get.ts +70 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +120 -0
- package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
- package/templates/apps/api/src/routes/project-listings/me.ts +76 -0
- package/templates/apps/api/src/routes/project-listings/patch-copy.ts +222 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +97 -0
- package/templates/apps/api/src/routes/project-listings/patch.ts +136 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +52 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +37 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +22 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +106 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +122 -0
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +490 -0
- package/templates/apps/api/src/routes/schedule/index.ts +249 -0
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +385 -0
- package/templates/apps/api/src/routes/slack/actions.ts +177 -0
- package/templates/apps/api/src/routes/slack/index.ts +38 -0
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +16 -6
- package/templates/apps/api/src/routes/test/auth/session.ts +8 -6
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +577 -0
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +78 -0
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +41 -0
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +64 -0
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +42 -0
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +84 -0
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +119 -0
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +238 -0
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +101 -0
- package/templates/apps/api/src/routes/webhooks/README.md +35 -12
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +315 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +270 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +248 -20
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +356 -0
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +671 -0
- package/templates/apps/api/src/routes/workspaces/config.ts +125 -0
- package/templates/apps/api/src/routes/workspaces/index.ts +194 -0
- package/templates/apps/api/src/routes/workspaces/sessions.ts +218 -0
- package/templates/apps/api/src/server.ts +307 -7
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +489 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +475 -0
- package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +132 -0
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +523 -0
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +359 -0
- package/templates/apps/api/src/services/__tests__/entitlements.test.ts +142 -0
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +425 -0
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +2840 -0
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +393 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +8 -8
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +560 -0
- package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +684 -0
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +406 -0
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +290 -0
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +736 -0
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +475 -0
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +340 -0
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +983 -0
- package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +278 -0
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +71 -0
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +379 -0
- package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +941 -0
- package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +588 -0
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +768 -0
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +1314 -0
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +341 -0
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +567 -0
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +474 -0
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +354 -0
- package/templates/apps/api/src/services/abandon-email.ts +307 -0
- package/templates/apps/api/src/services/aeo-score.ts +288 -0
- package/templates/apps/api/src/services/answer-distributions.ts +138 -0
- package/templates/apps/api/src/services/consent.ts +236 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +523 -0
- package/templates/apps/api/src/services/entitlements.ts +102 -0
- package/templates/apps/api/src/services/flow-aggregate.ts +232 -0
- package/templates/apps/api/src/services/flow-checkouts.ts +123 -0
- package/templates/apps/api/src/services/flow-engine.ts +1296 -0
- package/templates/apps/api/src/services/lead-promotion.ts +176 -0
- package/templates/apps/api/src/services/lead-routing.ts +3 -3
- package/templates/apps/api/src/services/listing-config.ts +102 -0
- package/templates/apps/api/src/services/listing-csv-sweep.ts +455 -0
- package/templates/apps/api/src/services/listing-promotion.ts +342 -0
- package/templates/apps/api/src/services/playbook-compile.ts +398 -0
- package/templates/apps/api/src/services/playbook-render.ts +263 -0
- package/templates/apps/api/src/services/project-listing-decision.ts +510 -0
- package/templates/apps/api/src/services/project-listing-flow.ts +426 -0
- package/templates/apps/api/src/services/project-listing-issue.ts +251 -0
- package/templates/apps/api/src/services/project-listings.ts +1472 -0
- package/templates/apps/api/src/services/recommender-capture.ts +835 -0
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +392 -0
- package/templates/apps/api/src/services/scheduling/index.ts +63 -0
- package/templates/apps/api/src/services/scheduling/types.ts +88 -0
- package/templates/apps/api/src/services/slack-notify.ts +240 -0
- package/templates/apps/api/src/services/tenant-context.ts +451 -0
- package/templates/apps/api/src/services/usage-aggregation.ts +516 -0
- package/templates/apps/api/src/services/usage-credits.ts +340 -0
- package/templates/apps/api/src/services/usage-metering.ts +699 -0
- package/templates/apps/api/src/services/waitlist-dashboard.ts +947 -0
- package/templates/apps/api/src/services/waitlist-experiments.ts +213 -0
- package/templates/apps/api/src/services/waitlist-followup.ts +486 -0
- package/templates/apps/api/src/services/waitlist-scoring.ts +166 -0
- package/templates/apps/api/src/services/waitlist-signups.ts +165 -0
- package/templates/apps/api/src/vercel-handler.ts +165 -0
- package/templates/apps/api/tsconfig.json +0 -12
- package/templates/apps/mcp/CLAUDE.md +19 -9
- package/templates/apps/mcp/LICENSE +201 -0
- package/templates/apps/mcp/__tests__/create_page.test.ts +1 -0
- package/templates/apps/mcp/__tests__/delete_page.test.ts +1 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +105 -0
- package/templates/apps/mcp/__tests__/parity.test.ts +70 -6
- package/templates/apps/mcp/__tests__/request_callback.test.ts +4 -4
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +242 -0
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +4 -4
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +4 -4
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +47 -0
- package/templates/apps/mcp/__tests__/update_page.test.ts +1 -0
- package/templates/apps/mcp/package.json +6 -4
- package/templates/apps/mcp/src/config/modules.ts +37 -0
- package/templates/apps/mcp/src/server.ts +109 -18
- package/templates/apps/mcp/src/tools/capture_lead.ts +1 -1
- package/templates/apps/mcp/src/tools/create_booking.ts +58 -0
- package/templates/apps/mcp/src/tools/create_page.ts +1 -0
- package/templates/apps/mcp/src/tools/delete_page.ts +1 -0
- package/templates/apps/mcp/src/tools/get_event_meta.ts +55 -0
- package/templates/apps/mcp/src/tools/get_flow.ts +61 -0
- package/templates/apps/mcp/src/tools/index.ts +25 -0
- package/templates/apps/mcp/src/tools/list_availability.ts +67 -0
- package/templates/apps/mcp/src/tools/purchase.ts +3 -0
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +83 -0
- package/templates/apps/mcp/src/tools/update_page.ts +1 -0
- package/templates/apps/mcp/tsconfig.json +1 -9
- package/templates/apps/web/.env.example +44 -0
- package/templates/apps/web/CLAUDE.md +28 -0
- package/templates/apps/web/LICENSE +201 -0
- package/templates/apps/web/__tests__/__mocks__/code-highlight.jest-stub.ts +24 -0
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +33 -0
- package/templates/apps/web/__tests__/__mocks__/next-intl.ts +18 -0
- package/templates/apps/web/__tests__/__mocks__/server-only.ts +13 -0
- package/templates/apps/web/__tests__/__mocks__/style-mock.js +4 -0
- package/templates/apps/web/__tests__/__mocks__/vercel-analytics-server.ts +9 -0
- package/templates/apps/web/__tests__/jest-support/import-meta-transformer.cjs +90 -0
- package/templates/apps/web/__tests__/setup.ts +22 -32
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +6 -3
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +8 -2
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +260 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +263 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +39 -0
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +54 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +96 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +309 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +149 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +273 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/gate.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +171 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +179 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +116 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ForbiddenState.tsx +24 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +66 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +61 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +114 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +139 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +85 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +67 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/format.ts +111 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +179 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +10 -1
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +6 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/careers/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/clients/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +21 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +188 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +242 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +29 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/intake-analytics.test.ts +51 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +39 -15
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +21 -3
- package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +49 -0
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +66 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +14 -4
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/courses/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/downloads/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/events/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/faq/page.tsx +10 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +265 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +445 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +341 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +287 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +302 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +128 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +254 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +144 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +108 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +532 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +121 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +457 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +396 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/integrations/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/launch-service/page.tsx +14 -1
- package/templates/apps/web/app/[locale]/layout.tsx +127 -16
- package/templates/apps/web/app/[locale]/page.tsx +10 -1
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/partners/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/products/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +10 -0
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +110 -0
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +96 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/resources/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +96 -0
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +92 -0
- package/templates/apps/web/app/[locale]/schedule/page.tsx +82 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/services/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/sign-in/__tests__/next-path.test.ts +31 -0
- package/templates/apps/web/app/[locale]/sign-in/__tests__/page.test.tsx +152 -0
- package/templates/apps/web/app/[locale]/sign-in/next-path.ts +0 -0
- package/templates/apps/web/app/[locale]/sign-in/page.tsx +83 -0
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +123 -0
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +93 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +48 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/globals.css +35 -0
- package/templates/apps/web/app/llms.txt/route.ts +2 -1
- package/templates/apps/web/app/og/route.tsx +10 -2
- package/templates/apps/web/auth.ts +8 -0
- package/templates/apps/web/components/sign-out-trigger.tsx +42 -0
- package/templates/apps/web/content/en/blog/README.txt +25 -0
- package/templates/apps/web/content/en/docs/index.mdx +28 -0
- package/templates/apps/web/jest.config.cjs +90 -27
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +499 -0
- package/templates/apps/web/lib/account-config.ts +13 -0
- package/templates/apps/web/lib/server-api.ts +85 -0
- package/templates/apps/web/lib/site-brand.tsx +121 -0
- package/templates/apps/web/lib/site-theme.ts +74 -0
- package/templates/apps/web/messages/en.json +275 -7
- package/templates/apps/web/middleware.ts +11 -3
- package/templates/apps/web/next.config.mjs +8 -2
- package/templates/apps/web/package.json +17 -7
- package/templates/apps/web/postcss.config.mjs +23 -0
- package/templates/apps/web/public/.well-known/security.txt +3 -3
- package/templates/apps/web/src/lib/routes.ts +15 -2
- package/templates/apps/web/tsconfig.json +1 -16
- package/templates/content/_site.mdx +36 -25
- package/templates/content/en/AboutPage/about.mdx +24 -14
- package/templates/content/en/ContactPage/contact.mdx +23 -14
- package/templates/content/en/FAQPage/faq.mdx +10 -392
- package/templates/content/en/HomePage/home.mdx +14 -285
- package/templates/database/CHANGELOG.md +443 -0
- package/templates/database/LICENSE +201 -0
- package/templates/database/README.md +3 -1
- package/templates/database/__tests__/flows-listing-diagnostic.test.ts +392 -0
- package/templates/database/__tests__/flows-presets.test.ts +173 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +146 -0
- package/templates/database/__tests__/seed.test.ts +88 -0
- package/templates/database/__tests__/user-cascade.test.ts +19 -4
- package/templates/database/auth/schema.prisma +521 -0
- package/templates/database/config/schema.prisma +234 -0
- package/templates/database/content/schema.prisma +263 -0
- package/templates/database/flows/listing-diagnostic.ts +182 -0
- package/templates/database/flows/presets.ts +315 -0
- package/templates/database/inbox/__tests__/schema.test.ts +4 -0
- package/templates/database/inbox/schema.prisma +574 -3
- package/templates/database/jest.config.cjs +8 -0
- package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +201 -0
- package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +67 -0
- package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +46 -0
- package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +25 -0
- package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +49 -0
- package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +197 -0
- package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +32 -0
- package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +22 -0
- package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +151 -0
- package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +223 -0
- package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +22 -0
- package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +32 -0
- package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +40 -0
- package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +4 -0
- package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +2 -0
- package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +2 -0
- package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +29 -0
- package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +6 -0
- package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +39 -0
- package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +24 -0
- package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +3 -0
- package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +33 -0
- package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +30 -0
- package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +47 -0
- package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +16 -0
- package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +47 -0
- package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +10 -0
- package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +26 -0
- package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +90 -0
- package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +174 -0
- package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +14 -0
- package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +25 -0
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
- package/templates/database/ops/schema.prisma +27 -0
- package/templates/database/package.json +42 -4
- package/templates/database/prisma.config.ts +1 -1
- package/templates/database/schema.prisma +19 -435
- package/templates/database/seed-test.ts +14 -4
- package/templates/database/seed.ts +148 -0
- package/templates/database/tenancy.ts +45 -0
- package/templates/database/tsconfig.flows.json +15 -0
- package/templates/database/tsconfig.json +7 -1
- package/templates/database/tsconfig.tenancy.json +14 -0
- package/templates/package.json +30 -55
- package/templates/pnpm-workspace.yaml +0 -3
- package/templates/project.yaml +1 -1
- package/templates/tsconfig.json +1 -37
- package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +0 -94
- package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +0 -75
- package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +0 -82
- package/templates/apps/web/__tests__/auth/env-helpers.test.ts +0 -168
- package/templates/apps/web/__tests__/authority-list-routes.test.tsx +0 -208
- package/templates/apps/web/__tests__/better-stack.test.ts +0 -66
- package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +0 -87
- package/templates/apps/web/__tests__/blog-source-populated.test.ts +0 -130
- package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +0 -60
- package/templates/apps/web/__tests__/google-analytics.test.tsx +0 -104
- package/templates/apps/web/__tests__/i18n/middleware.test.ts +0 -119
- package/templates/apps/web/__tests__/i18n/routing.test.ts +0 -114
- package/templates/apps/web/__tests__/i18n/translations.test.ts +0 -191
- package/templates/apps/web/__tests__/json-ld.test.ts +0 -98
- package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +0 -580
- package/templates/apps/web/__tests__/layout-stealth.test.ts +0 -39
- package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +0 -103
- package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +0 -114
- package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +0 -94
- package/templates/apps/web/__tests__/page.test.tsx +0 -230
- package/templates/apps/web/__tests__/products-services-phase1.test.tsx +0 -321
- package/templates/apps/web/__tests__/projects-routes.test.tsx +0 -392
- package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +0 -475
- package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +0 -53
- package/templates/apps/web/__tests__/routes/blog-route.test.tsx +0 -191
- package/templates/apps/web/__tests__/routes/llms-txt.test.ts +0 -123
- package/templates/apps/web/__tests__/routes/robots-txt.test.ts +0 -52
- package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +0 -78
- package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +0 -203
- package/templates/apps/web/__tests__/section-routes.test.tsx +0 -693
- package/templates/apps/web/__tests__/security-headers.test.ts +0 -201
- package/templates/apps/web/__tests__/sentry-config.test.ts +0 -116
- package/templates/apps/web/__tests__/sitemap-locales.test.ts +0 -415
- package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +0 -844
- package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +0 -120
- package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +0 -202
- package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +0 -152
- package/templates/apps/web/content/en/blog/index.mdx +0 -15
- package/templates/apps/web/content/en/blog/launching-ailk.mdx +0 -20
- package/templates/apps/web/content/en/docs/getting-started.mdx +0 -30
- package/templates/content/.gitkeep +0 -0
- package/templates/content/CLAUDE.md +0 -85
- package/templates/content/ar/HomePage/home.mdx +0 -123
- package/templates/content/ar/_site.mdx +0 -35
- package/templates/content/de/_site.mdx +0 -29
- package/templates/content/en/.gitkeep +0 -0
- package/templates/content/en/AboutPage/about.schema.json +0 -82
- package/templates/content/en/BookCall/book-call.mdx +0 -19
- package/templates/content/en/BookCall/book-call.schema.json +0 -121
- package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +0 -20
- package/templates/content/en/CareersList/careers.mdx +0 -24
- package/templates/content/en/CaseStudiesList/case-studies.mdx +0 -22
- package/templates/content/en/CaseStudiesList/showcase.mdx +0 -8
- package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +0 -32
- package/templates/content/en/CaseStudy/schema-builder-migration.mdx +0 -41
- package/templates/content/en/ClientsList/clients.mdx +0 -38
- package/templates/content/en/ContactPage/contact.schema.json +0 -100
- package/templates/content/en/ContactPoint/primary.mdx +0 -14
- package/templates/content/en/Course/aeo-fundamentals.mdx +0 -37
- package/templates/content/en/Course/schema-builder-workshop.mdx +0 -62
- package/templates/content/en/CoursesList/courses.mdx +0 -22
- package/templates/content/en/DownloadsList/downloads.mdx +0 -22
- package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +0 -39
- package/templates/content/en/Event/ailk-launch-day-2026.mdx +0 -35
- package/templates/content/en/EventsList/events.mdx +0 -26
- package/templates/content/en/FAQPage/faq.schema.json +0 -231
- package/templates/content/en/GlossaryList/glossary.mdx +0 -19
- package/templates/content/en/GlossaryList/glossary.schema.json +0 -300
- package/templates/content/en/HomePage/home.schema.json +0 -88
- package/templates/content/en/HomePage/launch-service.mdx +0 -475
- package/templates/content/en/HomePage/launch-service.schema.json +0 -138
- package/templates/content/en/HowTo/add-mdx-content.mdx +0 -38
- package/templates/content/en/HowTo/configure-aeo-scoring.mdx +0 -35
- package/templates/content/en/HowTo/set-up-json-ld.mdx +0 -36
- package/templates/content/en/Integration/github-actions.mdx +0 -31
- package/templates/content/en/Integration/openai-api.mdx +0 -26
- package/templates/content/en/Integration/vercel.mdx +0 -32
- package/templates/content/en/IntegrationsList/integrations.mdx +0 -22
- package/templates/content/en/JobPosting/content-strategist.mdx +0 -46
- package/templates/content/en/JobPosting/developer-advocate.mdx +0 -47
- package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +0 -57
- package/templates/content/en/LandingPage/aeo-audit.mdx +0 -19
- package/templates/content/en/LandingPage/aeo-audit.schema.json +0 -92
- package/templates/content/en/LandingPage/agency-waitlist.mdx +0 -340
- package/templates/content/en/LandingPage/agency-waitlist.schema.json +0 -152
- package/templates/content/en/LandingPage/developers.mdx +0 -19
- package/templates/content/en/LandingPage/developers.schema.json +0 -109
- package/templates/content/en/LandingPage/for-agencies.mdx +0 -390
- package/templates/content/en/LandingPage/for-agencies.schema.json +0 -107
- package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +0 -417
- package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +0 -105
- package/templates/content/en/LandingPage/for-businesses.mdx +0 -19
- package/templates/content/en/LandingPage/for-businesses.schema.json +0 -96
- package/templates/content/en/LandingPage/platform.mdx +0 -369
- package/templates/content/en/LandingPage/platform.schema.json +0 -166
- package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +0 -19
- package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +0 -102
- package/templates/content/en/LandingPage/vs-wordpress.mdx +0 -19
- package/templates/content/en/LandingPage/vs-wordpress.schema.json +0 -53
- package/templates/content/en/NewsletterSignup/newsletter.mdx +0 -236
- package/templates/content/en/NewsletterSignup/newsletter.schema.json +0 -91
- package/templates/content/en/Organization/site-owner.mdx +0 -16
- package/templates/content/en/Partner/prisma.mdx +0 -31
- package/templates/content/en/Partner/supabase.mdx +0 -30
- package/templates/content/en/Partner/vercel.mdx +0 -28
- package/templates/content/en/PartnersList/partners.mdx +0 -22
- package/templates/content/en/Person/founder.mdx +0 -18
- package/templates/content/en/Pricing/pricing.mdx +0 -538
- package/templates/content/en/Pricing/pricing.schema.json +0 -209
- package/templates/content/en/PrivacyPage/privacy.mdx +0 -165
- package/templates/content/en/PrivacyPage/privacy.schema.json +0 -64
- package/templates/content/en/Product/ailk.mdx +0 -19
- package/templates/content/en/Product/ailk.schema.json +0 -94
- package/templates/content/en/Product/api-launch-kit.mdx +0 -26
- package/templates/content/en/Product/launch-kit.mdx +0 -28
- package/templates/content/en/ProductsList/products.mdx +0 -26
- package/templates/content/en/Project/ailk-phase-2.mdx +0 -54
- package/templates/content/en/ProjectsList/projects.mdx +0 -25
- package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +0 -27
- package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +0 -28
- package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +0 -39
- package/templates/content/en/ResourcesHub/resources.mdx +0 -22
- package/templates/content/en/Service/consulting.mdx +0 -22
- package/templates/content/en/Service/implementation.mdx +0 -30
- package/templates/content/en/Service/launch-service.mdx +0 -232
- package/templates/content/en/Service/launch.mdx +0 -228
- package/templates/content/en/ServicesList/services.mdx +0 -84
- package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +0 -44
- package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +0 -47
- package/templates/content/en/SoftwareProduct/ailk-schema.mdx +0 -49
- package/templates/content/en/TeamList/team.mdx +0 -63
- package/templates/content/en/TeamMember/founder.mdx +0 -72
- package/templates/content/en/TermsPage/terms.mdx +0 -19
- package/templates/content/en/TermsPage/terms.schema.json +0 -99
- package/templates/content/en/TestimonialsList/testimonials.mdx +0 -44
- package/templates/content/en/UseCasesList/use-cases.mdx +0 -30
- package/templates/content/en/Whitepaper/ax-first-product-development.mdx +0 -27
- package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +0 -27
- package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +0 -31
- package/templates/content/en-XA/AboutPage/about.mdx +0 -8
- package/templates/content/en-XA/FAQPage/faq.mdx +0 -8
- package/templates/content/en-XA/HomePage/home.mdx +0 -8
- package/templates/content/es/_site.mdx +0 -30
- package/templates/content/fr/_site.mdx +0 -30
- package/templates/content/ja/_site.mdx +0 -30
- package/templates/content/pt-BR/AboutPage/about.mdx +0 -8
- package/templates/content/pt-BR/FAQPage/faq.mdx +0 -8
- package/templates/content/pt-BR/HomePage/home.mdx +0 -23
package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ConversationalIntakeWrapper (#4534, epic #4521's integration
|
|
3
|
+
* leg).
|
|
4
|
+
*
|
|
5
|
+
* AC1 (integration test) — a completed conversation lands a lead through
|
|
6
|
+
* the EXISTING lead-capture endpoint. Only the outermost network
|
|
7
|
+
* boundary is mocked (`@working-theory/api-client`'s `leads.submit`) —
|
|
8
|
+
* the REAL `ConversationalIntake` block (#4543) and its engine (#4532),
|
|
9
|
+
* the REAL `captureLeadAction` (the SAME action `<ContactForm/>` already
|
|
10
|
+
* uses), and the REAL `submitLeadInputSchema` validation all run, proving
|
|
11
|
+
* the whole chain end to end. No file under apps/api is imported, mocked,
|
|
12
|
+
* or touched by this test or by the wiring it exercises.
|
|
13
|
+
* AC2 — Skip renders the existing form, carrying over answers already
|
|
14
|
+
* captured as `initialValues`.
|
|
15
|
+
* AC3 — per-turn abandonment analytics: `recordIntakeTurn`
|
|
16
|
+
* (`./intake-analytics.ts`, mocked at the `track()` boundary) fires once
|
|
17
|
+
* per turn with an increasing `turnIndex`, plus one terminal call tagging
|
|
18
|
+
* the outcome.
|
|
19
|
+
*/
|
|
20
|
+
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
21
|
+
|
|
22
|
+
jest.mock("next-intl", () => ({
|
|
23
|
+
useTranslations: () => (key: string) => key,
|
|
24
|
+
useLocale: () => "en",
|
|
25
|
+
useMessages: () => ({}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
jest.mock("@working-theory/api-client", () => ({
|
|
29
|
+
leads: {
|
|
30
|
+
submit: jest.fn(),
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
jest.mock("@working-theory/analytics/server", () => ({
|
|
35
|
+
track: jest.fn().mockResolvedValue(undefined),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// ConversationalIntakeWrapper's own responsibility under test here is the
|
|
39
|
+
// submission/skip/analytics wiring around the block — not ContactForm's own
|
|
40
|
+
// rendering, which apps/web's existing tests already document as crashing
|
|
41
|
+
// in jsdom (useFormState/useFormStatus; see page-renderer-migration.test.tsx).
|
|
42
|
+
// Narrow internal-module mock, same precedent as that file's ContactForm stub.
|
|
43
|
+
// eslint-disable-next-line no-restricted-syntax -- test-seam mock required; ContactForm uses useFormState/useFormStatus which crash in jsdom (see page-renderer-migration.test.tsx); narrowest possible scope
|
|
44
|
+
jest.mock("../ContactForm", () => ({
|
|
45
|
+
ContactForm: (props: { initialValues?: Record<string, string> }) =>
|
|
46
|
+
require("react").createElement("div", {
|
|
47
|
+
"data-testid": "contact-form-stub",
|
|
48
|
+
"data-initial-values": JSON.stringify(props.initialValues ?? {}),
|
|
49
|
+
}),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
import { leads } from "@working-theory/api-client";
|
|
53
|
+
import { track } from "@working-theory/analytics/server";
|
|
54
|
+
|
|
55
|
+
import { ConversationalIntakeWrapper } from "../ConversationalIntakeWrapper.js";
|
|
56
|
+
|
|
57
|
+
const mockSubmit = leads.submit as jest.Mock;
|
|
58
|
+
const mockTrack = track as jest.Mock;
|
|
59
|
+
|
|
60
|
+
const TEST_ID = "contact-conversational-intake";
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
jest.useFakeTimers();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
act(() => {
|
|
68
|
+
jest.runOnlyPendingTimers();
|
|
69
|
+
});
|
|
70
|
+
jest.useRealTimers();
|
|
71
|
+
jest.clearAllMocks();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** Advances past the thinking pause (600–900ms) and flushes the reveal. */
|
|
75
|
+
function advanceThinking(): void {
|
|
76
|
+
act(() => {
|
|
77
|
+
jest.advanceTimersByTime(1000);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Flushes the microtask queue — needed after a turn transition that
|
|
83
|
+
* triggers an async Server Action call (`onComplete`'s `await
|
|
84
|
+
* captureLeadAction(...)`), whose own state updates (fallback to the form,
|
|
85
|
+
* the submit-error message) land a tick after the timer-driven reveal
|
|
86
|
+
* `advanceThinking()` already flushed.
|
|
87
|
+
*/
|
|
88
|
+
async function flushMicrotasks(): Promise<void> {
|
|
89
|
+
await act(async () => {
|
|
90
|
+
await Promise.resolve();
|
|
91
|
+
await Promise.resolve();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function answer(text: string): void {
|
|
96
|
+
const input = screen.getByTestId(`${TEST_ID}-free-text-input`);
|
|
97
|
+
fireEvent.change(input, { target: { value: text } });
|
|
98
|
+
fireEvent.click(screen.getByTestId(`${TEST_ID}-free-text-submit`));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe("ConversationalIntakeWrapper — completion (AC1)", () => {
|
|
102
|
+
it("submits through the existing lead-capture endpoint's api-client surface", () => {
|
|
103
|
+
mockSubmit.mockResolvedValue({
|
|
104
|
+
ok: true,
|
|
105
|
+
body: { id: "lead_abc123", persisted: "db" },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
render(<ConversationalIntakeWrapper />);
|
|
109
|
+
advanceThinking(); // turn 1 (name) revealed
|
|
110
|
+
|
|
111
|
+
answer("Jane Smith");
|
|
112
|
+
advanceThinking(); // turn 2 (message) revealed
|
|
113
|
+
|
|
114
|
+
answer("I'd like a demo.");
|
|
115
|
+
advanceThinking(); // turn 3 (email) revealed
|
|
116
|
+
|
|
117
|
+
answer("jane@example.com");
|
|
118
|
+
advanceThinking(); // completion revealed, onComplete fires
|
|
119
|
+
|
|
120
|
+
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
|
121
|
+
const body = mockSubmit.mock.calls[0][0] as {
|
|
122
|
+
siteId: string;
|
|
123
|
+
source: string;
|
|
124
|
+
email: string;
|
|
125
|
+
payload: { name: string; message?: string };
|
|
126
|
+
};
|
|
127
|
+
expect(body.siteId).toBe("default");
|
|
128
|
+
expect(body.source).toBe("conversational-intake");
|
|
129
|
+
expect(body.email).toBe("jane@example.com");
|
|
130
|
+
expect(body.payload.name).toBe("Jane Smith");
|
|
131
|
+
expect(body.payload.message).toBe("I'd like a demo.");
|
|
132
|
+
|
|
133
|
+
// The block stays mounted on success — its own completion message is
|
|
134
|
+
// already the confirmation; no fallback form appears.
|
|
135
|
+
expect(screen.queryByTestId("contact-form-stub")).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("falls back to the existing form, prefilled, when the submit fails", async () => {
|
|
139
|
+
mockSubmit.mockResolvedValue({ ok: false, status: 503, body: {} });
|
|
140
|
+
|
|
141
|
+
render(<ConversationalIntakeWrapper />);
|
|
142
|
+
advanceThinking();
|
|
143
|
+
answer("Jane Smith");
|
|
144
|
+
advanceThinking();
|
|
145
|
+
answer("I'd like a demo.");
|
|
146
|
+
advanceThinking();
|
|
147
|
+
answer("jane@example.com");
|
|
148
|
+
advanceThinking();
|
|
149
|
+
await flushMicrotasks();
|
|
150
|
+
|
|
151
|
+
const stub = screen.getByTestId("contact-form-stub");
|
|
152
|
+
const initialValues = JSON.parse(
|
|
153
|
+
stub.getAttribute("data-initial-values") ?? "{}",
|
|
154
|
+
);
|
|
155
|
+
expect(initialValues).toMatchObject({
|
|
156
|
+
name: "Jane Smith",
|
|
157
|
+
email: "jane@example.com",
|
|
158
|
+
message: "I'd like a demo.",
|
|
159
|
+
});
|
|
160
|
+
expect(
|
|
161
|
+
screen.getByTestId("contact-intake-submit-error"),
|
|
162
|
+
).toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("ConversationalIntakeWrapper — Skip (AC2)", () => {
|
|
167
|
+
it("renders the existing form on Skip, carrying over the answers captured so far", () => {
|
|
168
|
+
render(<ConversationalIntakeWrapper />);
|
|
169
|
+
advanceThinking(); // turn 1 (name) revealed
|
|
170
|
+
|
|
171
|
+
answer("Jane Smith");
|
|
172
|
+
advanceThinking(); // turn 2 (message) revealed
|
|
173
|
+
|
|
174
|
+
fireEvent.click(screen.getByTestId(`${TEST_ID}-skip`));
|
|
175
|
+
advanceThinking(); // skip message revealed, onSkip fires
|
|
176
|
+
|
|
177
|
+
const stub = screen.getByTestId("contact-form-stub");
|
|
178
|
+
const initialValues = JSON.parse(
|
|
179
|
+
stub.getAttribute("data-initial-values") ?? "{}",
|
|
180
|
+
);
|
|
181
|
+
expect(initialValues).toMatchObject({ name: "Jane Smith" });
|
|
182
|
+
expect(mockSubmit).not.toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("ConversationalIntakeWrapper — per-turn abandonment analytics (AC3)", () => {
|
|
187
|
+
it("fires one in_progress event per turn, then a terminal completed event, with increasing turnIndex", () => {
|
|
188
|
+
mockSubmit.mockResolvedValue({
|
|
189
|
+
ok: true,
|
|
190
|
+
body: { id: "lead_abc123", persisted: "db" },
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
render(<ConversationalIntakeWrapper />);
|
|
194
|
+
advanceThinking();
|
|
195
|
+
answer("Jane Smith");
|
|
196
|
+
advanceThinking();
|
|
197
|
+
answer("I'd like a demo.");
|
|
198
|
+
advanceThinking();
|
|
199
|
+
answer("jane@example.com");
|
|
200
|
+
advanceThinking();
|
|
201
|
+
|
|
202
|
+
const calls = mockTrack.mock.calls.map(([name, props]) => ({
|
|
203
|
+
name,
|
|
204
|
+
...(props as { turnIndex: number; status: string; locale: string }),
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
expect(calls.every((c) => c.name === "intake-turn-abandonment")).toBe(
|
|
208
|
+
true,
|
|
209
|
+
);
|
|
210
|
+
const inProgress = calls.filter((c) => c.status === "in_progress");
|
|
211
|
+
expect(inProgress.map((c) => c.turnIndex)).toEqual([1, 2, 3, 4]);
|
|
212
|
+
|
|
213
|
+
const terminal = calls[calls.length - 1];
|
|
214
|
+
expect(terminal).toMatchObject({ status: "completed", turnIndex: 4 });
|
|
215
|
+
|
|
216
|
+
// PII-free: only turnIndex/status/locale ever leave the client.
|
|
217
|
+
for (const call of mockTrack.mock.calls) {
|
|
218
|
+
const props = call[1] as Record<string, unknown>;
|
|
219
|
+
expect(Object.keys(props).sort()).toEqual(
|
|
220
|
+
["turnIndex", "status", "locale"].sort(),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("tags an abandoned (skipped) session with the turn it stopped at", () => {
|
|
226
|
+
render(<ConversationalIntakeWrapper />);
|
|
227
|
+
advanceThinking();
|
|
228
|
+
answer("Jane Smith");
|
|
229
|
+
advanceThinking();
|
|
230
|
+
|
|
231
|
+
fireEvent.click(screen.getByTestId(`${TEST_ID}-skip`));
|
|
232
|
+
advanceThinking();
|
|
233
|
+
|
|
234
|
+
const calls = mockTrack.mock.calls.map(([, props]) => props) as Array<{
|
|
235
|
+
status: string;
|
|
236
|
+
turnIndex: number;
|
|
237
|
+
}>;
|
|
238
|
+
const terminal = calls[calls.length - 1];
|
|
239
|
+
expect(terminal).toMatchObject({ status: "skipped" });
|
|
240
|
+
expect(terminal?.turnIndex).toBeGreaterThan(0);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -149,4 +149,33 @@ describe("captureLeadAction — API error", () => {
|
|
|
149
149
|
message: expect.any(String),
|
|
150
150
|
});
|
|
151
151
|
});
|
|
152
|
+
|
|
153
|
+
// #4042 — the catch-all previously discarded a thrown error silently,
|
|
154
|
+
// leaving zero server-side trace of a production integration failure.
|
|
155
|
+
it("logs the thrown error server-side with a [captureLeadAction] prefix, with no PII from the submitted fields", async () => {
|
|
156
|
+
const thrown = new Error("Network error");
|
|
157
|
+
mockSubmit.mockRejectedValueOnce(thrown);
|
|
158
|
+
const consoleErrorSpy = jest
|
|
159
|
+
.spyOn(console, "error")
|
|
160
|
+
.mockImplementation(() => undefined);
|
|
161
|
+
|
|
162
|
+
const result = await captureLeadAction(
|
|
163
|
+
INITIAL_STATE,
|
|
164
|
+
makeFormData(VALID_FIELDS),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith("[captureLeadAction]", thrown);
|
|
168
|
+
const loggedArgs = consoleErrorSpy.mock.calls.flat();
|
|
169
|
+
for (const value of Object.values(VALID_FIELDS)) {
|
|
170
|
+
for (const arg of loggedArgs) {
|
|
171
|
+
expect(String(arg)).not.toContain(value);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
expect(result).toMatchObject({
|
|
175
|
+
kind: "error",
|
|
176
|
+
message: expect.any(String),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
consoleErrorSpy.mockRestore();
|
|
180
|
+
});
|
|
152
181
|
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for intake-analytics.ts's recordIntakeTurn (#4534, epic #4521's
|
|
3
|
+
* integration leg).
|
|
4
|
+
*
|
|
5
|
+
* Acceptance criteria verified here:
|
|
6
|
+
* - recordIntakeTurn calls track() with the turn index + status + locale.
|
|
7
|
+
* - Never leaks answer text (PII-free per AC3).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
jest.mock("@working-theory/analytics/server", () => ({
|
|
11
|
+
track: jest.fn().mockResolvedValue(undefined),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
import { track } from "@working-theory/analytics/server";
|
|
15
|
+
|
|
16
|
+
import { recordIntakeTurn } from "../intake-analytics.js";
|
|
17
|
+
|
|
18
|
+
const mockTrack = track as jest.Mock;
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("recordIntakeTurn", () => {
|
|
25
|
+
it("reports the turn index, status, and locale — nothing else", async () => {
|
|
26
|
+
await recordIntakeTurn({
|
|
27
|
+
turnIndex: 2,
|
|
28
|
+
status: "in_progress",
|
|
29
|
+
locale: "en",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(mockTrack).toHaveBeenCalledWith("intake-turn-abandonment", {
|
|
33
|
+
turnIndex: 2,
|
|
34
|
+
status: "in_progress",
|
|
35
|
+
locale: "en",
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("never receives answer text (PII-free)", async () => {
|
|
40
|
+
await recordIntakeTurn({
|
|
41
|
+
turnIndex: 3,
|
|
42
|
+
status: "completed",
|
|
43
|
+
locale: "en",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const props = mockTrack.mock.calls[0][1] as Record<string, unknown>;
|
|
47
|
+
expect(Object.keys(props).sort()).toEqual(
|
|
48
|
+
["turnIndex", "status", "locale"].sort(),
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -1,29 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTML snapshot regression tests for contact/page.tsx → @working-theory/page-renderer migration.
|
|
3
3
|
*
|
|
4
|
-
* Contact uses sections mode (not bodyMode: "mdx-body") with a
|
|
5
|
-
*
|
|
4
|
+
* Contact uses sections mode (not bodyMode: "mdx-body") with a
|
|
5
|
+
* ConversationalIntakeWrapper wrapper:
|
|
6
|
+
* <>{renderPage output}<ConversationalIntakeWrapper /></>
|
|
6
7
|
*
|
|
7
8
|
* Two-commit strategy:
|
|
8
9
|
* Commit 1: this file added; snapshots captured against pre-migration route (RED-passing).
|
|
9
10
|
* Commit 2: route migrated; same snapshots verify diff=0 (GREEN).
|
|
10
11
|
*
|
|
12
|
+
* #4534 (epic #4521's integration leg) swapped the page's direct
|
|
13
|
+
* `<ContactForm/>` mount for `<ConversationalIntakeWrapper/>` (which itself
|
|
14
|
+
* renders the existing ContactForm on Skip or a failed auto-submit — see
|
|
15
|
+
* that component's own doc-comment). The mock below + the snapshots'
|
|
16
|
+
* `contact-form-stub` testid moved with it, deliberately — this is the one
|
|
17
|
+
* legitimate case the file's own "DO NOT regenerate to pass" warning
|
|
18
|
+
* doesn't cover: the page's actual composition changed, not the renderer.
|
|
19
|
+
*
|
|
11
20
|
* Two states tested:
|
|
12
|
-
* - empty: getMdxSource returns null → renderPage empty-state +
|
|
13
|
-
* - populated: getMdxSource returns populated source → renderPage sections +
|
|
21
|
+
* - empty: getMdxSource returns null → renderPage empty-state + wrapper present
|
|
22
|
+
* - populated: getMdxSource returns populated source → renderPage sections + wrapper present
|
|
14
23
|
*
|
|
15
|
-
* Note on
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
24
|
+
* Note on ConversationalIntakeWrapper in tests: it composes ContactForm
|
|
25
|
+
* (skip/fallback surface), which is a "use client" component using
|
|
26
|
+
* useFormState/useFormStatus hooks — jsdom does not support these hooks.
|
|
27
|
+
* The wrapper renders as an opaque stub via the next-intl and react-dom
|
|
28
|
+
* mocks below. The snapshot asserts the wrapper is present in the output
|
|
29
|
+
* (non-null).
|
|
19
30
|
*
|
|
20
|
-
* Failure = snapshot diff > 0 bytes
|
|
31
|
+
* Failure = snapshot diff > 0 bytes beyond the deliberate #4534 rename.
|
|
21
32
|
*/
|
|
22
33
|
import React from "react";
|
|
23
34
|
import { render } from "@testing-library/react";
|
|
24
35
|
|
|
25
36
|
// ── @working-theory/ui mock ─────────────────────────────────────────────────────────────
|
|
26
37
|
jest.mock("@working-theory/ui", () => ({
|
|
38
|
+
// #4446/#4447: renderPage's own fallback/empty-state title now composes
|
|
39
|
+
// Heading (mobile type-budget classes, #4417) instead of a bare <h1>.
|
|
40
|
+
Heading: ({
|
|
41
|
+
children,
|
|
42
|
+
level,
|
|
43
|
+
}: {
|
|
44
|
+
children: React.ReactNode;
|
|
45
|
+
level: number;
|
|
46
|
+
}): React.ReactElement =>
|
|
47
|
+
React.createElement(`h${level}`, null, children),
|
|
27
48
|
SectionRenderer: ({ sections }: { sections: unknown[] }) =>
|
|
28
49
|
React.createElement("div", {
|
|
29
50
|
"data-testid": "section-renderer",
|
|
@@ -31,12 +52,15 @@ jest.mock("@working-theory/ui", () => ({
|
|
|
31
52
|
}),
|
|
32
53
|
}));
|
|
33
54
|
|
|
34
|
-
// ──
|
|
35
|
-
// ContactForm
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
// ── ConversationalIntakeWrapper mock ────────────────────────────────────────
|
|
56
|
+
// ConversationalIntakeWrapper composes ContactForm ("use client",
|
|
57
|
+
// useFormState/useFormStatus — crashes in jsdom) as its skip/fallback
|
|
58
|
+
// surface. Mock the wrapper itself as a deterministic stub so the snapshot
|
|
59
|
+
// is stable; the wrapper's own submission/skip/analytics wiring is covered
|
|
60
|
+
// by ConversationalIntakeWrapper.test.tsx.
|
|
61
|
+
// eslint-disable-next-line no-restricted-syntax -- test-seam mock required; composes ContactForm, which uses useFormState/useFormStatus that crash in jsdom; internal-module mock is the narrowest possible scope; tracked in #507
|
|
62
|
+
jest.mock("../ConversationalIntakeWrapper", () => ({
|
|
63
|
+
ConversationalIntakeWrapper: () =>
|
|
40
64
|
React.createElement("div", { "data-testid": "contact-form-stub" }),
|
|
41
65
|
}));
|
|
42
66
|
|
|
@@ -4,13 +4,24 @@
|
|
|
4
4
|
* captureLeadAction — Server Action paired with ailkCaptureLead MCP tool.
|
|
5
5
|
*
|
|
6
6
|
* Validates contact-form input with submitLeadInputSchema (shared schema per
|
|
7
|
-
* ADR 0013), calls POST /
|
|
7
|
+
* ADR 0013), calls POST /v1/leads/submit via @working-theory/api-client, and returns
|
|
8
8
|
* a discriminated ActionResult suitable for useFormState.
|
|
9
9
|
*
|
|
10
10
|
* Canonical filename: contact.actions.ts (*.actions.ts per ADR 0013 / U0 spec
|
|
11
11
|
* parity-test regex `ACTION_FILE_RE = /\.actions\.ts$/`).
|
|
12
12
|
*
|
|
13
13
|
* Honeypot + rate-limit enforcement lives in apps/api; no duplication here.
|
|
14
|
+
*
|
|
15
|
+
* `source` (#4534, epic #4521's integration leg): an optional hidden
|
|
16
|
+
* FormData field so `ConversationalIntakeWrapper` can tag leads it submits
|
|
17
|
+
* as `"conversational-intake"` rather than the plain form's `"web"` —
|
|
18
|
+
* reusing this SAME action (built imperatively from the conversation's
|
|
19
|
+
* answers via `new FormData()`, not a real `<form>` submit) instead of a
|
|
20
|
+
* second `*Action`-suffixed export, which would trip
|
|
21
|
+
* `apps/mcp/__tests__/parity.test.ts`'s Server-Action↔MCP-tool 1:1 count for
|
|
22
|
+
* a capability (lead capture) this repo already has an MCP twin for
|
|
23
|
+
* (`ailkCaptureLead`). Absent (the real `<form>` never sets it) → defaults
|
|
24
|
+
* to `"web"`, unchanged from before.
|
|
14
25
|
*/
|
|
15
26
|
|
|
16
27
|
import { leads } from "@working-theory/api-client";
|
|
@@ -50,9 +61,10 @@ export async function captureLeadAction(
|
|
|
50
61
|
|
|
51
62
|
const { name, email, message, phone } = parsed.data;
|
|
52
63
|
const siteId = process.env.AILK_SITE_ID ?? "default";
|
|
64
|
+
const source = (formData.get("source") as string | null) || "web";
|
|
53
65
|
const body = {
|
|
54
66
|
siteId,
|
|
55
|
-
source
|
|
67
|
+
source,
|
|
56
68
|
email,
|
|
57
69
|
payload: {
|
|
58
70
|
name,
|
|
@@ -70,7 +82,13 @@ export async function captureLeadAction(
|
|
|
70
82
|
};
|
|
71
83
|
}
|
|
72
84
|
return { kind: "success" };
|
|
73
|
-
} catch {
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// Log server-side for observability (#4042, sibling of flow.actions.ts's
|
|
87
|
+
// #3989 fix). The api-client's post() never attaches the request payload
|
|
88
|
+
// to a thrown error (packages/api-client/src/http.ts), so logging `err`
|
|
89
|
+
// itself is safe — no submitted form fields (name/email/message/phone)
|
|
90
|
+
// are included.
|
|
91
|
+
console.error("[captureLeadAction]", err);
|
|
74
92
|
return {
|
|
75
93
|
kind: "error",
|
|
76
94
|
message: "Something went wrong, please try again.",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* intake-analytics.ts — per-turn abandonment analytics for
|
|
5
|
+
* ConversationalIntakeWrapper (#4534, epic #4521's integration leg).
|
|
6
|
+
*
|
|
7
|
+
* `recordIntakeTurn` fires `track()` (`@working-theory/analytics/server`,
|
|
8
|
+
* the same fire-and-forget helper `apps/api/src/routes/leads/index.ts`
|
|
9
|
+
* already uses for "lead-captured") once per conversation turn, tagged with
|
|
10
|
+
* the turn index and the engine status as of that turn. A session's last
|
|
11
|
+
* "intake-turn-abandonment" event with `status: "in_progress"` marks the
|
|
12
|
+
* turn it stopped progressing past — the standard step-funnel shape for
|
|
13
|
+
* computing per-turn abandonment, without needing a bespoke "abandoned"
|
|
14
|
+
* signal (tab-close is indistinguishable from "still reading" client-side;
|
|
15
|
+
* the downstream analytics consumer infers abandonment from the absence of
|
|
16
|
+
* a later "completed"/"skipped" event for the session). PII-free by
|
|
17
|
+
* construction: only the turn index, status, and locale ever leave the
|
|
18
|
+
* client — never the answer text (honesty-by-design applies to what we
|
|
19
|
+
* track, too).
|
|
20
|
+
*
|
|
21
|
+
* Deliberately NOT named `recordIntakeTurnAction` / kept OUT of a
|
|
22
|
+
* `*.actions.ts` file: this is telemetry, not a business-capability Server
|
|
23
|
+
* Action, and `apps/mcp/__tests__/parity.test.ts` counts every
|
|
24
|
+
* `*.actions.ts` export matching `^[a-z][a-zA-Z0-9]*Action$` as one half of
|
|
25
|
+
* a Server-Action↔MCP-tool pair it expects to find a twin for — there is no
|
|
26
|
+
* (and should be no) `ailkRecordIntakeTurn` MCP tool, since this reports a
|
|
27
|
+
* pageview-shaped web-analytics event, not a capability an agent invokes.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { track } from "@working-theory/analytics/server";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The conversation's status as of the turn being reported. `"in_progress"`
|
|
34
|
+
* fires once per turn while the conversation is ongoing (including turn 1,
|
|
35
|
+
* on mount); `"completed"`/`"skipped"` fire once, at the terminal turn.
|
|
36
|
+
*/
|
|
37
|
+
export type IntakeTurnStatus = "in_progress" | "completed" | "skipped";
|
|
38
|
+
|
|
39
|
+
export async function recordIntakeTurn(input: {
|
|
40
|
+
turnIndex: number;
|
|
41
|
+
status: IntakeTurnStatus;
|
|
42
|
+
locale?: string;
|
|
43
|
+
}): Promise<void> {
|
|
44
|
+
await track("intake-turn-abandonment", {
|
|
45
|
+
turnIndex: input.turnIndex,
|
|
46
|
+
status: input.status,
|
|
47
|
+
locale: input.locale,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buildContactIntakeScript — the authored `IntakeScript` for the contact
|
|
3
|
+
* page's `ConversationalIntakeWrapper` (#4534, epic #4521's integration
|
|
4
|
+
* leg).
|
|
5
|
+
*
|
|
6
|
+
* Three open questions, deliberately keyed `name` / `message` / `email` —
|
|
7
|
+
* the SAME field names `submitLeadInputSchema` (packages/validation)
|
|
8
|
+
* expects. `ConversationalIntakeWrapper` reads `IntakeEngineState.answers`
|
|
9
|
+
* by these exact ids to build the `FormData` it hands to the existing
|
|
10
|
+
* `captureLeadAction` (`./contact.actions.ts`), so the script's question
|
|
11
|
+
* ids ARE the submission adapter's field-mapping contract; renaming a
|
|
12
|
+
* question id here requires updating that mapping too.
|
|
13
|
+
*
|
|
14
|
+
* Copy is translated (next-intl "contact" namespace, "intake.*" keys —
|
|
15
|
+
* see apps/web/messages/en.json) rather than hardcoded, consistent with
|
|
16
|
+
* ContactForm's own `useTranslations("contact")` usage. The wrapper resolves
|
|
17
|
+
* the strings via its own scoped translator and passes them in as plain
|
|
18
|
+
* data (`ContactIntakeCopy`) rather than passing the translator function
|
|
19
|
+
* itself — next-intl's `t` is typed to a specific literal key union per
|
|
20
|
+
* call site, which doesn't assign cleanly to a generic `(key: string) =>
|
|
21
|
+
* string` parameter; plain strings sidestep that entirely and keep this
|
|
22
|
+
* file decoupled from next-intl's types.
|
|
23
|
+
*/
|
|
24
|
+
import { intakeScriptSchema } from "@working-theory/validation";
|
|
25
|
+
import type { IntakeScript } from "@working-theory/validation";
|
|
26
|
+
|
|
27
|
+
export interface ContactIntakeCopy {
|
|
28
|
+
nameAck: string;
|
|
29
|
+
namePrompt: string;
|
|
30
|
+
messageAck: string;
|
|
31
|
+
messagePrompt: string;
|
|
32
|
+
emailAck: string;
|
|
33
|
+
emailPrompt: string;
|
|
34
|
+
skipMessage: string;
|
|
35
|
+
completionMessage: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildContactIntakeScript(
|
|
39
|
+
copy: ContactIntakeCopy,
|
|
40
|
+
): IntakeScript {
|
|
41
|
+
return intakeScriptSchema.parse({
|
|
42
|
+
slug: "contact-intake",
|
|
43
|
+
questions: [
|
|
44
|
+
{
|
|
45
|
+
id: "name",
|
|
46
|
+
kind: "open",
|
|
47
|
+
acknowledgement: copy.nameAck,
|
|
48
|
+
prompt: copy.namePrompt,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "message",
|
|
52
|
+
kind: "open",
|
|
53
|
+
acknowledgement: copy.messageAck,
|
|
54
|
+
prompt: copy.messagePrompt,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "email",
|
|
58
|
+
kind: "open",
|
|
59
|
+
acknowledgement: copy.emailAck,
|
|
60
|
+
prompt: copy.emailPrompt,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
skip: { message: copy.skipMessage },
|
|
64
|
+
completion: { message: copy.completionMessage },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// Contact page. Renders sections via renderPage (sections mode) then appends
|
|
2
|
-
//
|
|
2
|
+
// ConversationalIntakeWrapper — the chat-style intake (#4534, epic #4521's
|
|
3
|
+
// integration leg), which itself renders the existing ContactForm on Skip
|
|
4
|
+
// or a failed auto-submit. JSON-LD and translations handled by renderPage.
|
|
3
5
|
import type {
|
|
4
6
|
getMdxSource as defaultGetMdxSource,
|
|
5
7
|
getPageSections as defaultGetPageSections,
|
|
6
8
|
} from "@working-theory/page-renderer";
|
|
7
|
-
import { renderPage } from "@working-theory/page-renderer";
|
|
9
|
+
import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
|
|
10
|
+
import type { Metadata } from "next";
|
|
8
11
|
import type { ReactElement } from "react";
|
|
9
12
|
|
|
10
|
-
import {
|
|
13
|
+
import { ConversationalIntakeWrapper } from "./ConversationalIntakeWrapper";
|
|
11
14
|
|
|
12
15
|
interface Props {
|
|
13
16
|
params: Promise<{ locale: string }>;
|
|
@@ -15,6 +18,13 @@ interface Props {
|
|
|
15
18
|
getPageSections?: typeof defaultGetPageSections;
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
export async function generateMetadata({
|
|
22
|
+
params,
|
|
23
|
+
}: Pick<Props, "params">): Promise<Metadata> {
|
|
24
|
+
const { locale } = await params;
|
|
25
|
+
return renderPageMetadata({ type: "ContactPage", slug: "contact", locale });
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
export default async function Page({
|
|
19
29
|
params,
|
|
20
30
|
getMdxSource,
|
|
@@ -32,7 +42,7 @@ export default async function Page({
|
|
|
32
42
|
return (
|
|
33
43
|
<>
|
|
34
44
|
{rendered}
|
|
35
|
-
<
|
|
45
|
+
<ConversationalIntakeWrapper />
|
|
36
46
|
</>
|
|
37
47
|
);
|
|
38
48
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// Course detail page. Reads the Course MDX for the given slug, renders via
|
|
2
2
|
// @working-theory/page-renderer, and emits a JSON-LD <script>. Unknown slugs trigger 404.
|
|
3
3
|
import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getMdxSlugs,
|
|
6
|
+
renderPage,
|
|
7
|
+
renderPageMetadata,
|
|
8
|
+
} from "@working-theory/page-renderer";
|
|
9
|
+
import type { Metadata } from "next";
|
|
5
10
|
import type { ReactElement } from "react";
|
|
6
11
|
|
|
7
12
|
interface Props {
|
|
@@ -9,6 +14,13 @@ interface Props {
|
|
|
9
14
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
10
15
|
}
|
|
11
16
|
|
|
17
|
+
export async function generateMetadata({
|
|
18
|
+
params,
|
|
19
|
+
}: Pick<Props, "params">): Promise<Metadata> {
|
|
20
|
+
const { locale, slug } = await params;
|
|
21
|
+
return renderPageMetadata({ type: "Course", slug, locale });
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
export async function generateStaticParams({
|
|
13
25
|
params,
|
|
14
26
|
}: {
|