diff --git a/apps/web/public/tool-art/audio-generation.svg b/apps/web/public/tool-art/audio-generation.svg
new file mode 100644
index 000000000..d4895af46
--- /dev/null
+++ b/apps/web/public/tool-art/audio-generation.svg
@@ -0,0 +1 @@
+Audio Generation illustration Original geometric Pencil and Pixel artwork for the Audio Generation tool. Audio Generation
\ No newline at end of file
diff --git a/apps/web/public/tool-art/brand-story.svg b/apps/web/public/tool-art/brand-story.svg
new file mode 100644
index 000000000..391e9b2bf
--- /dev/null
+++ b/apps/web/public/tool-art/brand-story.svg
@@ -0,0 +1 @@
+Brand & Story illustration Original geometric Pencil and Pixel artwork for the Brand & Story tool. Brand & Story
\ No newline at end of file
diff --git a/apps/web/public/tool-art/character-chat.svg b/apps/web/public/tool-art/character-chat.svg
new file mode 100644
index 000000000..74b25c36f
--- /dev/null
+++ b/apps/web/public/tool-art/character-chat.svg
@@ -0,0 +1 @@
+Character Chat illustration Original geometric Pencil and Pixel artwork for the Character Chat tool. Character Chat
\ No newline at end of file
diff --git a/apps/web/public/tool-art/creative-director.svg b/apps/web/public/tool-art/creative-director.svg
new file mode 100644
index 000000000..79224f3c5
--- /dev/null
+++ b/apps/web/public/tool-art/creative-director.svg
@@ -0,0 +1 @@
+Creative Director illustration Original geometric Pencil and Pixel artwork for the Creative Director tool. Creative Director
\ No newline at end of file
diff --git a/apps/web/public/tool-art/explorer-hero.svg b/apps/web/public/tool-art/explorer-hero.svg
new file mode 100644
index 000000000..ca6f45b4c
--- /dev/null
+++ b/apps/web/public/tool-art/explorer-hero.svg
@@ -0,0 +1 @@
+A connected creative toolbox An original illustrated collage of image, video, audio and storytelling tools. VOICE & SOUND
\ No newline at end of file
diff --git a/apps/web/public/tool-art/image-studio.svg b/apps/web/public/tool-art/image-studio.svg
new file mode 100644
index 000000000..a7c794fc3
--- /dev/null
+++ b/apps/web/public/tool-art/image-studio.svg
@@ -0,0 +1 @@
+Image Studio illustration Original geometric Pencil and Pixel artwork for the Image Studio tool. Image Studio
\ No newline at end of file
diff --git a/apps/web/public/tool-art/mediakit.svg b/apps/web/public/tool-art/mediakit.svg
new file mode 100644
index 000000000..f616b9d16
--- /dev/null
+++ b/apps/web/public/tool-art/mediakit.svg
@@ -0,0 +1 @@
+MediaKit Tools illustration Original geometric Pencil and Pixel artwork for the MediaKit Tools tool. MediaKit Tools
\ No newline at end of file
diff --git a/apps/web/public/tool-art/precision-image.svg b/apps/web/public/tool-art/precision-image.svg
new file mode 100644
index 000000000..ff6c535c3
--- /dev/null
+++ b/apps/web/public/tool-art/precision-image.svg
@@ -0,0 +1 @@
+Precision Image Studio illustration Original geometric Pencil and Pixel artwork for the Precision Image Studio tool. Precision Image Studio
\ No newline at end of file
diff --git a/apps/web/public/tool-art/scriptwriter.svg b/apps/web/public/tool-art/scriptwriter.svg
new file mode 100644
index 000000000..90c0b3d9f
--- /dev/null
+++ b/apps/web/public/tool-art/scriptwriter.svg
@@ -0,0 +1 @@
+Scriptwriter illustration Original geometric Pencil and Pixel artwork for the Scriptwriter tool. Scriptwriter
\ No newline at end of file
diff --git a/apps/web/public/tool-art/spokesperson.svg b/apps/web/public/tool-art/spokesperson.svg
new file mode 100644
index 000000000..d8787d0a1
--- /dev/null
+++ b/apps/web/public/tool-art/spokesperson.svg
@@ -0,0 +1 @@
+AI Spokesperson illustration Original geometric Pencil and Pixel artwork for the AI Spokesperson tool. AI Spokesperson
\ No newline at end of file
diff --git a/apps/web/public/tool-art/templates.svg b/apps/web/public/tool-art/templates.svg
new file mode 100644
index 000000000..876dbaca2
--- /dev/null
+++ b/apps/web/public/tool-art/templates.svg
@@ -0,0 +1 @@
+Generation Templates illustration Original geometric Pencil and Pixel artwork for the Generation Templates tool. Generation Templates
\ No newline at end of file
diff --git a/apps/web/public/tool-art/transcription.svg b/apps/web/public/tool-art/transcription.svg
new file mode 100644
index 000000000..5dff75f47
--- /dev/null
+++ b/apps/web/public/tool-art/transcription.svg
@@ -0,0 +1 @@
+Speech & Transcription illustration Original geometric Pencil and Pixel artwork for the Speech & Transcription tool. Speech & Transcription
\ No newline at end of file
diff --git a/apps/web/public/tool-art/video-editor.svg b/apps/web/public/tool-art/video-editor.svg
new file mode 100644
index 000000000..4fc64071b
--- /dev/null
+++ b/apps/web/public/tool-art/video-editor.svg
@@ -0,0 +1 @@
+Video Editing Desk illustration Original geometric Pencil and Pixel artwork for the Video Editing Desk tool. Video Editing Desk
\ No newline at end of file
diff --git a/apps/web/public/tool-art/video-studio.svg b/apps/web/public/tool-art/video-studio.svg
new file mode 100644
index 000000000..ffaf03a1a
--- /dev/null
+++ b/apps/web/public/tool-art/video-studio.svg
@@ -0,0 +1 @@
+Video Studio illustration Original geometric Pencil and Pixel artwork for the Video Studio tool. Video Studio
\ No newline at end of file
diff --git a/apps/web/public/tool-art/voice-casting.svg b/apps/web/public/tool-art/voice-casting.svg
new file mode 100644
index 000000000..9d2384903
--- /dev/null
+++ b/apps/web/public/tool-art/voice-casting.svg
@@ -0,0 +1 @@
+Voice Casting Booth illustration Original geometric Pencil and Pixel artwork for the Voice Casting Booth tool. Voice Casting Booth
\ No newline at end of file
diff --git a/apps/web/public/tool-art/voice-studio.svg b/apps/web/public/tool-art/voice-studio.svg
new file mode 100644
index 000000000..7a8f570be
--- /dev/null
+++ b/apps/web/public/tool-art/voice-studio.svg
@@ -0,0 +1 @@
+Voice Studio illustration Original geometric Pencil and Pixel artwork for the Voice Studio tool. Voice Studio
\ No newline at end of file
diff --git a/apps/web/src/app/onboarding/page.tsx b/apps/web/src/app/onboarding/page.tsx
index 2808381cf..2e4f781d9 100644
--- a/apps/web/src/app/onboarding/page.tsx
+++ b/apps/web/src/app/onboarding/page.tsx
@@ -17,9 +17,11 @@ export default async function OnboardingPage({
searchParams: Promise<{ returnTo?: string }>;
}) {
const requested = safeInvitationRoute((await searchParams).returnTo);
- const learnReturn = requested.startsWith("/learn/start/")
- ? requested
- : undefined;
+ const contentReturn =
+ requested.startsWith("/learn/start/") ||
+ requested.startsWith("/tools/start/")
+ ? requested
+ : undefined;
const session = await requireRequestSession("/onboarding");
const membership = await db.membership.findFirst({
where: {
@@ -31,7 +33,7 @@ export default async function OnboardingPage({
});
if (membership) {
- redirect(learnReturn ?? `/app/${membership.organization.slug}`);
+ redirect(contentReturn ?? `/app/${membership.organization.slug}`);
}
return (
@@ -92,7 +94,7 @@ export default async function OnboardingPage({
-
+
{(
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index 10f7ba02c..8e8361f24 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -185,6 +185,7 @@ export default function HomePage() {
aria-label="Public navigation"
>
{[
+ ["/tools", "Explore Tools"],
["#platform", "Platform"],
["#models", "Models"],
["#workflow", "Workflow"],
diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts
index 0aba425d2..ec9d3d827 100644
--- a/apps/web/src/app/sitemap.ts
+++ b/apps/web/src/app/sitemap.ts
@@ -2,17 +2,30 @@ import type { MetadataRoute } from "next";
import { db } from "@aiwa/db";
import { publishedContent } from "@aiwa/learn";
import { learnOrigin } from "@/lib/learn/queries";
+import { publicTools } from "@/lib/public-tools";
+import { getPublicPricingCatalogue } from "@/lib/public-pricing";
export const dynamic = "force-dynamic";
export default async function sitemap(): Promise
{
const origin = learnOrigin();
- const posts = await db.learnPost.findMany({
- where: { publishedAt: { not: null } },
- take: 45000,
- select: { slug: true, published: true, modifiedAt: true },
- });
+ const [posts, pricing] = await Promise.all([
+ db.learnPost.findMany({
+ where: { publishedAt: { not: null } },
+ take: 45000,
+ select: { slug: true, published: true, modifiedAt: true },
+ }),
+ getPublicPricingCatalogue().catch(() => ({ models: [] })),
+ ]);
+ const operations = pricing.models.filter(
+ (item) => item.kind === "tool" && item.category === "tools",
+ );
return [
{ url: origin },
{ url: `${origin}/pricing` },
+ { url: `${origin}/tools` },
+ ...publicTools.map((tool) => ({ url: `${origin}/tools/${tool.slug}` })),
+ ...operations.map((item) => ({
+ url: `${origin}/tools/operations/${encodeURIComponent(item.id)}`,
+ })),
{ url: `${origin}/learn` },
...posts
.filter((p) => !publishedContent(p.published).noindex)
diff --git a/apps/web/src/app/tools/[slug]/page.tsx b/apps/web/src/app/tools/[slug]/page.tsx
new file mode 100644
index 000000000..e34742d8d
--- /dev/null
+++ b/apps/web/src/app/tools/[slug]/page.tsx
@@ -0,0 +1,342 @@
+import type { Metadata, Route } from "next";
+import Image from "next/image";
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { getPublicPricingCatalogue } from "@/lib/public-pricing";
+import {
+ publicToolById,
+ publicToolBySlug,
+ publicTools,
+ toolCategories,
+} from "@/lib/public-tools";
+import { ToolCard } from "@/components/marketing/tools/tool-explorer";
+import {
+ ToolsConversion,
+ ToolsFooter,
+ ToolsHeader,
+} from "@/components/marketing/tools/tools-shell";
+import { Icon } from "@/components/ui/icon";
+
+export const dynamic = "force-dynamic";
+type PageProps = { params: Promise<{ slug: string }> };
+export function generateStaticParams() {
+ return publicTools.map((tool) => ({ slug: tool.slug }));
+}
+export async function generateMetadata({
+ params,
+}: PageProps): Promise {
+ const tool = publicToolBySlug((await params).slug);
+ if (!tool) return { title: "Tool not found", robots: { index: false } };
+ return {
+ title: tool.title + " — " + tool.headline,
+ description: tool.summary.slice(0, 160),
+ alternates: { canonical: "/tools/" + tool.slug },
+ robots: { index: true, follow: true },
+ openGraph: {
+ title: tool.title + " · Aiwa Creators",
+ description: tool.summary,
+ type: "website",
+ images: [
+ {
+ url: tool.cover,
+ width: 700,
+ height: 430,
+ alt: tool.title + " illustration",
+ },
+ ],
+ },
+ };
+}
+export default async function ToolDetailPage({ params }: PageProps) {
+ const tool = publicToolBySlug((await params).slug);
+ if (!tool) notFound();
+ const related = tool.relatedIds
+ .map(publicToolById)
+ .filter((item) => item !== undefined);
+ const group = toolCategories.find(
+ (category) => category.id === tool.category,
+ );
+ const operationCatalogue =
+ tool.id === "mediakit"
+ ? await getPublicPricingCatalogue().catch(() => ({ models: [] }))
+ : null;
+ const operations =
+ operationCatalogue?.models.filter(
+ (item) => item.category === "tools" && item.kind === "tool",
+ ) ?? [];
+ const breadcrumbs = [
+ { name: "Home", item: "/" },
+ { name: "All tools", item: "/tools" },
+ { name: tool.title, item: "/tools/" + tool.slug },
+ ];
+ const origin = process.env.APP_URL ?? "https://creator.aiwamediagroup.com";
+ const breadcrumbData = {
+ "@context": "https://schema.org",
+ "@type": "BreadcrumbList",
+ itemListElement: breadcrumbs.map((crumb, index) => ({
+ "@type": "ListItem",
+ position: index + 1,
+ name: crumb.name,
+ item: new URL(crumb.item, origin).toString(),
+ })),
+ };
+ return (
+
+
+
+
+
+
+
+
+ Home
+
+ /
+
+ Tools
+
+ /
+
+ {tool.title}
+
+
+
+
+
+
+ {group?.label}
+
+
+ {tool.headline}
+
+
+ {tool.summary}
+
+
+ Ideal for: {tool.idealFor}
+
+
+
+ Start creating
+
+
+ Explore pricing
+
+
+
+ Browse without an account. Generation requires sign-in and a
+ current Studio quote.
+
+
+
+
+
+
+
+ {[
+ ["#use-cases", "What to create"],
+ ["#capabilities", "Capabilities"],
+ ["#how-it-works", "How it works"],
+ ["#related", "Related tools"],
+ ["/pricing", "Pricing"],
+ ].map(([href, label]) => (
+
+ {label}
+
+ ))}
+
+
+
+
+ Made for actual creative work
+
+
+ What could you make?
+
+
+ {tool.useCases.map((useCase, index) => (
+
+
+ {String(index + 1).padStart(2, "0")}
+
+
+ {useCase}
+
+
+ Start with your own brief, then choose the model and available
+ controls in the secure workspace.
+
+
+ ))}
+
+
+
+
+
+
+ The toolkit
+
+
+ The right controls for your idea.
+
+
+ Capabilities vary by enabled provider model or selected media
+ operation. The workspace presents only settings supported by your
+ choice.
+
+
+
+ {tool.capabilities.map((capability) => (
+
+ ))}
+
+
+
+
+
+ Three steps
+
+
+ From the brief to your workspace.
+
+
+ {tool.workflow.map((step, index) => (
+
+
+ {String(index + 1).padStart(2, "0")}
+
+
{step}
+
+ ))}
+
+
+ {tool.id === "mediakit" ? (
+
+
+
+ Published processing operations
+
+
+ Listed only when enabled, configured and included in the public
+ customer pricing catalogue.
+
+ {operations.length ? (
+
+ {operations.map((item) => (
+
+
{item.name}
+
+ {item.description}
+
+
+ Explore operation →
+
+
+ ))}
+
+ ) : (
+
+ No operations are currently published here. Please check the
+ Pricing catalogue for current availability.
+
+ )}
+
+
+ ) : null}
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/tools/operations/[id]/page.tsx b/apps/web/src/app/tools/operations/[id]/page.tsx
new file mode 100644
index 000000000..b0cf0a9d8
--- /dev/null
+++ b/apps/web/src/app/tools/operations/[id]/page.tsx
@@ -0,0 +1,114 @@
+import type { Metadata, Route } from "next";
+import Image from "next/image";
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { getPublicPricingCatalogue } from "@/lib/public-pricing";
+import {
+ ToolsConversion,
+ ToolsFooter,
+ ToolsHeader,
+} from "@/components/marketing/tools/tools-shell";
+
+export const dynamic = "force-dynamic";
+type Props = { params: Promise<{ id: string }> };
+async function findOperation(id: string) {
+ if (!id || id.length > 128 || !/^[a-zA-Z0-9_-]+$/.test(id)) return null;
+ const catalogue = await getPublicPricingCatalogue().catch(() => ({
+ models: [],
+ }));
+ return (
+ catalogue.models.find(
+ (item) =>
+ item.category === "tools" && item.kind === "tool" && item.id === id,
+ ) ?? null
+ );
+}
+export async function generateMetadata({ params }: Props): Promise {
+ const item = await findOperation((await params).id);
+ if (!item)
+ return {
+ title: "Operation unavailable",
+ robots: { index: false, follow: false },
+ };
+ return {
+ title: item.name + " — AI MediaKit",
+ description: item.description.slice(0, 160),
+ alternates: { canonical: "/tools/operations/" + item.id },
+ robots: { index: true, follow: true },
+ openGraph: {
+ title: item.name + " · Aiwa Creators",
+ description: item.description,
+ type: "website",
+ },
+ };
+}
+export default async function OperationPage({ params }: Props) {
+ const operation = await findOperation((await params).id);
+ if (!operation) notFound();
+ return (
+
+
+
+
+ Home
+ /
+ Tools
+ /
+ MediaKit
+ /
+ {operation.name}
+
+
+
+
+
+ Published MediaKit operation
+
+
+ {operation.name}
+
+
+ {operation.description}
+
+
+ This operation is currently included in Creators' public
+ customer pricing catalogue. Media inputs and account eligibility are
+ verified in the workspace before execution.
+
+
+
+ Open MediaKit →
+
+
+ View published pricing
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/tools/page.tsx b/apps/web/src/app/tools/page.tsx
new file mode 100644
index 000000000..b6fd6c3f0
--- /dev/null
+++ b/apps/web/src/app/tools/page.tsx
@@ -0,0 +1,390 @@
+import type { Metadata, Route } from "next";
+import Image from "next/image";
+import Link from "next/link";
+import { getPublicPricingCatalogue } from "@/lib/public-pricing";
+import { publicTools, toolHref } from "@/lib/public-tools";
+import {
+ ToolCard,
+ ToolDirectory,
+} from "@/components/marketing/tools/tool-explorer";
+import {
+ ToolsConversion,
+ ToolsFooter,
+ ToolsHeader,
+} from "@/components/marketing/tools/tools-shell";
+import { Icon } from "@/components/ui/icon";
+
+export const dynamic = "force-dynamic";
+export const metadata: Metadata = {
+ title: "Explore AI Creative Tools: Image, Video, Voice & More",
+ description:
+ "Find the right Aiwa Creators tool for your next project. Explore image, video, voice, editing, creative direction and currently published MediaKit tools.",
+ alternates: { canonical: "/tools" },
+ robots: { index: true, follow: true },
+ openGraph: {
+ title: "Every creative tool. One connected workspace. · Aiwa Creators",
+ description:
+ "Discover the right creative workflow, compare what tools do and start creating.",
+ type: "website",
+ images: [
+ {
+ url: "/tool-art/explorer-hero.svg",
+ width: 920,
+ height: 690,
+ alt: "Aiwa Creators illustrated creative toolbox",
+ },
+ ],
+ },
+};
+const faqs = [
+ {
+ q: "Do I need an account to explore the tools?",
+ a: "No. The directory, public tool pages and published pricing are accessible without signing in. Creating media requires an authenticated workspace.",
+ },
+ {
+ q: "Which tool should I choose first?",
+ a: "Search for an outcome, or use the guided tool finder to match your goal and starting material to an existing workspace tool.",
+ },
+ {
+ q: "How do I know what a generation costs?",
+ a: "Explore currently published customer rates on the Pricing page. Your signed-in Studio shows a quote for the actual settings and input before submission.",
+ },
+ {
+ q: "Can I use multiple tools for a campaign?",
+ a: "Yes. Use Creative Director to plan, Image and Video Studios to create, Voice Studio for narration, and your workspace to organize the resulting assets.",
+ },
+ {
+ q: "Are every model and MediaKit operation always available?",
+ a: "No. Provider configuration, published customer pricing and model capabilities determine availability. The MediaKit operation list reflects eligible published customer pricing.",
+ },
+];
+export default async function ToolsPage() {
+ const catalogue = await getPublicPricingCatalogue().catch(() => ({
+ models: [],
+ }));
+ const operations = catalogue.models
+ .filter((model) => model.category === "tools" && model.kind === "tool")
+ .map((model) => ({
+ id: model.id,
+ name: model.name,
+ description: model.description,
+ }));
+ const featured = publicTools.filter((tool) => tool.featured);
+ return (
+
+
+
+
+
+
+
+
+ The creative toolbox
+
+
+ Every creative tool.{" "}
+ One connected workspace.
+
+
+ Make original images, put stories into motion, create narration,
+ shape a campaign and finish your media. Find the right starting
+ point for what you want to make.
+
+
+
+ Explore all tools
+
+
+ Find my tool
+
+
+
+ ✓ Browse without signing in
+ ✓ Customer-focused guides
+
+ ✓ Explore published pricing ↗
+
+
+
+
+
+
+ Find your next idea ✦
+
+
+
+
+
+
+ {[
+ ["#featured", "Featured"],
+ ["#directory", "All tools"],
+ ["#tool-finder", "Find my tool"],
+ ["#comparison", "Compare"],
+ ["#workflow", "Workflow"],
+ ["#faq", "FAQs"],
+ ["/pricing", "Pricing"],
+ ].map(([href, label]) => (
+
+ {label}
+
+ ))}
+
+
+
+
+
+
+ Featured creative spaces
+
+
+ Big ideas start here.
+
+
+
+ See the full directory →
+
+
+
+ {featured.map((tool) => (
+
+ ))}
+
+
+
+
+
+ At a glance
+
+
+ Choose with confidence.
+
+
+ Each tool has its own job, but your creative work can continue across
+ the workspace. These are representative workflows, not a promise that
+ all provider models support every input.
+
+
+
+
+
+ {["Tool", "Starting point", "Best for", "Learn more"].map(
+ (heading) => (
+
+ {heading}
+
+ ),
+ )}
+
+
+
+ {publicTools
+ .filter((tool) => tool.id !== "templates")
+ .map((tool) => (
+
+
+ {tool.title}
+
+
+ {tool.workflow[0]}
+
+
+ {tool.idealFor}
+
+
+
+ Explore ↗
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ One connected workflow
+
+
+ From brief to campaign.
+
+
+ The most useful work often involves more than one tool. Develop a
+ concept, generate a visual, build motion, add narration and keep
+ the result together.
+
+
+ Understand generation pricing →
+
+
+
+ {[
+ [
+ "creative-director",
+ "Shape the direction",
+ "Start from a clear creative brief.",
+ ],
+ [
+ "image-studio",
+ "Make the campaign visual",
+ "Create imagery with model-supported references.",
+ ],
+ [
+ "video-studio",
+ "Bring it to life",
+ "Explore movement and sequence.",
+ ],
+ [
+ "voice-studio",
+ "Give it a voice",
+ "Create the accompanying narration.",
+ ],
+ [
+ "mediakit",
+ "Prepare for delivery",
+ "Use published processing tools when appropriate.",
+ ],
+ ].map(([id, title, description], index) => {
+ const tool = publicTools.find((item) => item.id === id);
+ return tool ? (
+
+
+ {String(index + 1).padStart(2, "0")}
+
+
+
+
+
+
{title}
+
+ {description}
+
+
+
+ Explore ↗
+
+
+ ) : null;
+ })}
+
+
+
+
+
+
+
+ A faster starting point
+
+
+ Start from an illustrated template.
+
+
+ Browse the ready-to-use recipes in your workspace and go directly
+ to the corresponding Studio to personalize the result.
+
+
+
+ Explore templates →
+
+
+
+
+
+ Before you begin
+
+
+ Frequently asked questions.
+
+
+ {faqs.map((item) => (
+
+
+ {item.q}
+
+
+
+ {item.a}
+
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/tools/start/[id]/page.tsx b/apps/web/src/app/tools/start/[id]/page.tsx
new file mode 100644
index 000000000..7261f7e10
--- /dev/null
+++ b/apps/web/src/app/tools/start/[id]/page.tsx
@@ -0,0 +1,65 @@
+import type { Metadata, Route } from "next";
+import { db } from "@aiwa/db";
+import Link from "next/link";
+import { notFound, redirect } from "next/navigation";
+import { ToolsHeader } from "@/components/marketing/tools/tools-shell";
+import { getRequestSession } from "@/lib/request-auth";
+import { publicToolById } from "@/lib/public-tools";
+
+export const metadata: Metadata = { robots: { index: false, follow: false } };
+export default async function ToolStart({
+ params,
+}: {
+ params: Promise<{ id: string }>;
+}) {
+ const { id } = await params;
+ const tool = publicToolById(id);
+ if (!tool) notFound();
+ const route = "/tools/start/" + tool.id;
+ const session = await getRequestSession();
+ if (!session)
+ return (
+
+
+
+
+ Your creative starting point
+
+
+ Continue to {tool.title}.
+
+
+ Create an account or sign in to open this tool in your workspace. No
+ generation is submitted and no credits are charged by this page.
+
+
+
+ Create an account
+
+
+ Sign in
+
+
+
+
+ );
+ const membership = await db.membership.findFirst({
+ where: { userId: session.user.id, organization: { status: "ACTIVE" } },
+ select: { organization: { select: { slug: true } } },
+ orderBy: { createdAt: "asc" },
+ });
+ if (!membership)
+ redirect(("/onboarding?returnTo=" + encodeURIComponent(route)) as Route);
+ redirect(
+ ("/app/" +
+ encodeURIComponent(membership.organization.slug) +
+ "/" +
+ tool.segment) as Route,
+ );
+}
diff --git a/apps/web/src/components/marketing/tools/tool-explorer.tsx b/apps/web/src/components/marketing/tools/tool-explorer.tsx
new file mode 100644
index 000000000..2fae07f7f
--- /dev/null
+++ b/apps/web/src/components/marketing/tools/tool-explorer.tsx
@@ -0,0 +1,482 @@
+"use client";
+
+import Image from "next/image";
+import Link from "next/link";
+import type { Route } from "next";
+import { useMemo, useState } from "react";
+import { Icon } from "@/components/ui/icon";
+import {
+ findPublicTools,
+ recommendTools,
+ toolCategories,
+ toolHref,
+ type PublicTool,
+ type PublicToolCategory,
+ type ToolIntent,
+ type ToolSource,
+} from "@/lib/public-tools";
+
+export type PublicOperation = {
+ id: string;
+ name: string;
+ description: string;
+};
+
+const intents: readonly { value: ToolIntent; label: string }[] = [
+ { value: "image", label: "Create an image" },
+ { value: "edit-image", label: "Improve a picture" },
+ { value: "video", label: "Make a video" },
+ { value: "presenter", label: "Create a presenter" },
+ { value: "voice", label: "Generate a voiceover" },
+ { value: "transcribe", label: "Make a transcript" },
+ { value: "plan", label: "Plan a campaign" },
+ { value: "finish", label: "Finish existing media" },
+];
+const sources: readonly { value: ToolSource; label: string }[] = [
+ { value: "idea", label: "An idea or some text" },
+ { value: "image", label: "An image" },
+ { value: "video", label: "A video" },
+ { value: "audio", label: "An audio recording" },
+];
+
+export function ToolCard({
+ tool,
+ featured = false,
+}: {
+ tool: PublicTool;
+ featured?: boolean;
+}) {
+ return (
+
+
+
+
+ {
+ toolCategories.find((category) => category.id === tool.category)
+ ?.label
+ }
+
+
+
+
+
+
+
+
+
+ {tool.title}
+
+
+ {tool.headline}
+
+
+
+
+ {tool.summary}
+
+ {featured ? (
+
+ Ideal for: {tool.idealFor}
+
+ ) : null}
+
+ {tool.capabilities.slice(0, 3).map((capability) => (
+
+ {capability}
+
+ ))}
+
+
+
+ Explore tool
+
+
+ Pricing ↗
+
+
+
+
+ );
+}
+export function ToolDirectory({
+ operations,
+}: {
+ operations: PublicOperation[];
+}) {
+ const [query, setQuery] = useState("");
+ const [category, setCategory] = useState("all");
+ const [intent, setIntent] = useState("");
+ const [source, setSource] = useState("");
+ const results = useMemo(
+ () => findPublicTools(query, category),
+ [query, category],
+ );
+ const ops = useMemo(
+ () =>
+ operations.filter((operation) => {
+ if (category !== "all" && category !== "finishing") return false;
+ const search = (
+ operation.name +
+ " " +
+ operation.description +
+ " mediakit finishing"
+ ).toLocaleLowerCase();
+ return search.includes(query.trim().toLocaleLowerCase());
+ }),
+ [operations, query, category],
+ );
+ const recommendations =
+ intent && source ? recommendTools(intent, source) : [];
+ return (
+ <>
+
+
+
+ The complete toolbox
+
+
+ Find a tool for the job.
+
+
+ Search by the outcome you want, the material you have, or the Studio
+ you already know. Browse every major workspace tool without signing
+ in.
+
+
+
+
+ Search creative tools
+
+
+
+ setQuery(event.target.value)}
+ placeholder="Try 'animate a photo', 'subtitles', 'product images'..."
+ className="min-w-0 flex-1 bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground"
+ />
+ {query ? (
+ setQuery("")}
+ className="rounded-lg px-2 py-1 text-xs font-semibold text-primary focus-visible:outline-2 focus-visible:outline-ring"
+ >
+ Clear
+
+ ) : null}
+
+
+ {[{ id: "all", label: "All tools" }, ...toolCategories].map(
+ (item) => (
+
+ setCategory(item.id as PublicToolCategory | "all")
+ }
+ className={
+ "shrink-0 rounded-full border px-3.5 py-2 text-xs font-bold transition focus-visible:outline-2 focus-visible:outline-ring " +
+ (category === item.id
+ ? "border-primary bg-primary text-primary-foreground"
+ : "border-border bg-card hover:bg-secondary")
+ }
+ >
+ {item.label}
+
+ ),
+ )}
+
+
+
+ {results.length + ops.length} matching{" "}
+ {results.length + ops.length === 1 ? "tool" : "tools"}
+
+ {results.length || ops.length ? (
+
+ {toolCategories
+ .filter((group) => category === "all" || category === group.id)
+ .map((group) => {
+ const items = results.filter(
+ (tool) => tool.category === group.id,
+ );
+ if (!items.length && (group.id !== "finishing" || !ops.length))
+ return null;
+ return (
+
+
+
+
+
+
+
+ {group.label}
+
+
+ {group.description}
+
+
+
+ {items.length ? (
+
+ {items.map((tool) => (
+
+ ))}
+
+ ) : null}
+ {group.id === "finishing" && ops.length ? (
+
+
+ Published MediaKit operations
+
+
+ These entries come from the enabled public customer
+ pricing catalogue, not from an unpublished provider
+ list.
+
+
+ {ops.map((op) => (
+
+
+
+
+
+
+
+ {op.name}
+
+
+ {op.description}
+
+
+
+
+ Explore operation →
+
+
+ ))}
+
+
+ ) : null}
+
+ );
+ })}
+
+ ) : (
+
+
+
+ No matching tools yet.
+
+
+ Try a broader description or choose a different category.
+
+
{
+ setQuery("");
+ setCategory("all");
+ }}
+ >
+ Clear filters
+
+
+ )}
+
+
+ >
+ );
+}
diff --git a/apps/web/src/components/marketing/tools/tools-shell.tsx b/apps/web/src/components/marketing/tools/tools-shell.tsx
new file mode 100644
index 000000000..aef3c0f23
--- /dev/null
+++ b/apps/web/src/components/marketing/tools/tools-shell.tsx
@@ -0,0 +1,120 @@
+import Link from "next/link";
+import type { Route } from "next";
+import { Brand } from "@/components/ui/brand";
+import { ThemeToggle } from "@/components/theme/theme-toggle";
+import { Icon } from "@/components/ui/icon";
+
+export function ToolsHeader() {
+ return (
+
+ );
+}
+export function ToolsFooter() {
+ return (
+
+
+
+
+
+ From first thought to final media.
+
+
+
+ Home
+ All tools
+ Pricing
+ Learn
+ Privacy
+ Terms
+
+
+
+ );
+}
+export function ToolsConversion({
+ title = "Your next creative project starts here.",
+ toolId,
+}: {
+ title?: string;
+ toolId?: string;
+}) {
+ return (
+
+
+
+
+
+ From idea to output
+
+
+ {title}
+
+
+ Explore your creative options, choose an available model and review
+ the Studio quote before any billable work starts.
+
+
+
+
+ Start creating
+
+
+ Explore pricing
+
+
+
+
+ );
+}
diff --git a/apps/web/src/lib/navigation.test.ts b/apps/web/src/lib/navigation.test.ts
index a91cb25d6..358422f4b 100644
--- a/apps/web/src/lib/navigation.test.ts
+++ b/apps/web/src/lib/navigation.test.ts
@@ -28,6 +28,22 @@ describe("safeInvitationRoute", () => {
).toBe("/invite/tok_0123456789abcdef?ref=mail#section");
});
+ it("allows a real public tool start and blocks guessed destinations", () => {
+ expect(safeInvitationRoute("/tools/start/image-studio")).toBe(
+ "/tools/start/image-studio",
+ );
+ expect(safeInvitationRoute("/tools/start/voice-studio")).toBe(
+ "/tools/start/voice-studio",
+ );
+ expect(safeInvitationRoute("/tools/start/unknown")).toBe("/onboarding");
+ expect(
+ safeInvitationRoute("/tools/start/image-studio?next=https://evil.com"),
+ ).toBe("/onboarding");
+ expect(safeInvitationRoute("//evil.com/tools/start/image-studio")).toBe(
+ "/onboarding",
+ );
+ });
+
it("defaults to /onboarding when no returnTo is provided", () => {
expect(safeInvitationRoute(undefined)).toBe("/onboarding");
expect(safeInvitationRoute("")).toBe("/onboarding");
diff --git a/apps/web/src/lib/navigation.ts b/apps/web/src/lib/navigation.ts
index bf296e2ce..7915dacf3 100644
--- a/apps/web/src/lib/navigation.ts
+++ b/apps/web/src/lib/navigation.ts
@@ -4,6 +4,7 @@ import {
type PlatformRole,
} from "@aiwa/authz";
import type { Route } from "next";
+import { WORKSPACE_TOOLS } from "./workspace-tools";
const internalOrigin = "https://internal.aiwa.invalid";
@@ -49,6 +50,11 @@ export function safeInvitationRoute(
if (/^\/learn\/start\/[a-zA-Z0-9_-]+$/.test(pathname)) return route;
+ if (route === pathname && /^\/tools\/start\/[a-z0-9-]+$/.test(pathname)) {
+ const id = pathname.slice("/tools/start/".length);
+ if (WORKSPACE_TOOLS.some((tool) => tool.id === id)) return route;
+ }
+
if (pathname.startsWith("/invite/")) {
const token = pathname.slice("/invite/".length);
if (token && /^[a-zA-Z0-9_-]+$/.test(token)) {
diff --git a/apps/web/src/lib/public-tools.test.ts b/apps/web/src/lib/public-tools.test.ts
new file mode 100644
index 000000000..3e9be5f48
--- /dev/null
+++ b/apps/web/src/lib/public-tools.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, it } from "vitest";
+import { WORKSPACE_TOOLS } from "./workspace-tools";
+import {
+ findPublicTools,
+ publicToolById,
+ publicToolBySlug,
+ publicTools,
+ recommendTools,
+ toolHref,
+} from "./public-tools";
+
+describe("public tool catalogue", () => {
+ it("covers the canonical workspace tools with unique, safe slugs", () => {
+ expect(publicTools).toHaveLength(WORKSPACE_TOOLS.length);
+ expect(new Set(publicTools.map((tool) => tool.id)).size).toBe(
+ publicTools.length,
+ );
+ expect(new Set(publicTools.map((tool) => tool.slug)).size).toBe(
+ publicTools.length,
+ );
+ for (const tool of publicTools) {
+ expect(tool.slug).toMatch(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
+ expect(tool.summary.length).toBeGreaterThan(80);
+ expect(publicToolBySlug(tool.slug)?.id).toBe(tool.id);
+ expect(publicToolById(tool.id)?.segment).toBe(tool.segment);
+ expect(toolHref(tool)).toBe("/tools/" + tool.slug);
+ expect(tool.useCases.length).toBeGreaterThanOrEqual(3);
+ expect(tool.workflow.length).toBe(3);
+ }
+ });
+ it("supports customer language and category filtering", () => {
+ expect(
+ findPublicTools("animate photo").some(
+ (tool) => tool.id === "video-studio",
+ ),
+ ).toBe(true);
+ expect(
+ findPublicTools("srt").some((tool) => tool.id === "transcription"),
+ ).toBe(true);
+ expect(findPublicTools("narration", "image")).toHaveLength(0);
+ expect(findPublicTools(" ")).toHaveLength(publicTools.length);
+ });
+ it("returns curated, real tool IDs without external execution", () => {
+ expect(recommendTools("video", "image")[0]?.id).toBe("video-studio");
+ expect(recommendTools("transcribe", "audio")[0]?.id).toBe("transcription");
+ expect(recommendTools("transcribe", "idea")).toHaveLength(0);
+ });
+});
diff --git a/apps/web/src/lib/public-tools.ts b/apps/web/src/lib/public-tools.ts
new file mode 100644
index 000000000..af8c02917
--- /dev/null
+++ b/apps/web/src/lib/public-tools.ts
@@ -0,0 +1,644 @@
+import { WORKSPACE_TOOLS } from "./workspace-tools";
+import type { IconName } from "@/components/ui/icon";
+
+export type PublicToolCategory =
+ "image" | "video" | "audio" | "creative" | "finishing";
+
+export const toolCategories: readonly {
+ id: PublicToolCategory;
+ label: string;
+ description: string;
+ icon: IconName;
+}[] = [
+ {
+ id: "image",
+ label: "Image & Design",
+ description: "Create an original image, then make every detail count.",
+ icon: "image",
+ },
+ {
+ id: "video",
+ label: "Video & Motion",
+ description: "Put ideas, portraits and still images into motion.",
+ icon: "video",
+ },
+ {
+ id: "audio",
+ label: "Audio & Speech",
+ description: "Make your words sound like they mean something.",
+ icon: "voice",
+ },
+ {
+ id: "creative",
+ label: "Creative Intelligence",
+ description: "Shape the idea before you spend time creating it.",
+ icon: "director",
+ },
+ {
+ id: "finishing",
+ label: "MediaKit & Finishing",
+ description: "Prepare, transform and refine media for delivery.",
+ icon: "wand",
+ },
+];
+
+type ToolEditorial = {
+ slug: string;
+ category: PublicToolCategory;
+ headline: string;
+ summary: string;
+ idealFor: string;
+ useCases: readonly string[];
+ capabilities: readonly string[];
+ workflow: readonly string[];
+ keywords: readonly string[];
+ relatedIds: readonly string[];
+ featured?: boolean;
+};
+
+const editorial: Record = {
+ "image-studio": {
+ slug: "ai-image-generator",
+ category: "image",
+ featured: true,
+ headline: "Make the image you can imagine.",
+ summary:
+ "Turn an idea into campaign concepts, product visuals, original illustrations and expressive art. Add references when the chosen model supports them.",
+ idealFor:
+ "Product launches, advertising visuals, moodboards and social posts.",
+ useCases: [
+ "Draft a product campaign before a photoshoot",
+ "Create an original scene from a short brief",
+ "Explore variations for a social-media concept",
+ ],
+ capabilities: [
+ "Text-led creation",
+ "Reference-aware workflows",
+ "Model-specific aspect ratios and output settings",
+ "Follow-up editing through supported workflows",
+ ],
+ workflow: [
+ "Describe the visual and its intended audience.",
+ "Pick an enabled model and choose its supported controls.",
+ "Review the signed Studio quote, then create and refine.",
+ ],
+ keywords: [
+ "text to image",
+ "image generation",
+ "seedream",
+ "product photo",
+ "picture",
+ "campaign",
+ ],
+ relatedIds: ["precision-image", "video-studio", "templates"],
+ },
+ "precision-image": {
+ slug: "ai-image-editor",
+ category: "image",
+ headline: "Keep the original. Improve the details.",
+ summary:
+ "Work from an existing image to crop, resize, transform and use model-supported AI retouching in a more controlled editing workflow.",
+ idealFor:
+ "Product image revisions, layout changes and finishing campaign imagery.",
+ useCases: [
+ "Adapt a product visual for a different format",
+ "Refine an area without restarting the entire concept",
+ "Prepare a finished image for production",
+ ],
+ capabilities: [
+ "Crop and resize",
+ "Rotate and transform",
+ "AI-assisted retouching where supported",
+ "Layer-oriented production workflow",
+ ],
+ workflow: [
+ "Choose or upload the image you own or may use.",
+ "Select the adjustment or supported AI operation.",
+ "Review any quote, finish and save the result.",
+ ],
+ keywords: [
+ "inpainting",
+ "outpainting",
+ "crop",
+ "photo editor",
+ "retouch",
+ "resize",
+ ],
+ relatedIds: ["image-studio", "mediakit"],
+ },
+ "video-studio": {
+ slug: "ai-video-generator",
+ category: "video",
+ featured: true,
+ headline: "Give your ideas a moving story.",
+ summary:
+ "Describe a scene or start with reference imagery to create clips, explore motion and build a more compelling campaign.",
+ idealFor:
+ "Short-form campaigns, product reveals, concept films and animated photographs.",
+ useCases: [
+ "Animate a still product visual",
+ "Explore a cinematic shot without a set",
+ "Prototype a video concept from a storyboard",
+ ],
+ capabilities: [
+ "Text-to-video",
+ "Image- and frame-led generation",
+ "Model-aware duration and format settings",
+ "Editing and extension where supported",
+ ],
+ workflow: [
+ "Start with a scene description or a supported media input.",
+ "Choose the model and the controls available for that task.",
+ "Review the credit quote before generating.",
+ ],
+ keywords: [
+ "text to video",
+ "image to video",
+ "animate photo",
+ "seedance",
+ "cinematic",
+ ],
+ relatedIds: ["video-editor", "creative-director", "image-studio"],
+ },
+ spokesperson: {
+ slug: "ai-spokesperson",
+ category: "video",
+ featured: true,
+ headline: "Give your message a face.",
+ summary:
+ "Create a portrait-led presenter video using approved source imagery and driving audio, with the controls supported by the active avatar workflow.",
+ idealFor:
+ "Explainers, product introductions and presenter-led promotional content.",
+ useCases: [
+ "Present a short product announcement",
+ "Turn approved presenter material into an explainer",
+ "Make an on-brand spokesperson concept",
+ ],
+ capabilities: [
+ "Portrait-driven workflow",
+ "Driving-audio input",
+ "Model-aware output controls",
+ "Save the result in your workspace",
+ ],
+ workflow: [
+ "Supply a portrait with appropriate rights and consent.",
+ "Add the required driving audio and choose supported settings.",
+ "Review the quote and generate the presenter video.",
+ ],
+ keywords: [
+ "avatar",
+ "omnihuman",
+ "talking portrait",
+ "presenter",
+ "lip sync",
+ ],
+ relatedIds: ["voice-studio", "video-studio", "mediakit"],
+ },
+ "video-editor": {
+ slug: "video-editor",
+ category: "video",
+ headline: "Make every second work harder.",
+ summary:
+ "Bring clips into one editing desk to trim, reorder and prepare a coherent finished video instead of treating each generation as an isolated output.",
+ idealFor:
+ "Social ads, short sequences and assembly of multiple generated clips.",
+ useCases: [
+ "Remove unwanted opening seconds",
+ "Combine related clips into a single sequence",
+ "Refine the order and pacing of a short campaign",
+ ],
+ capabilities: [
+ "Timeline-based clip ordering",
+ "Trimming and preview",
+ "Supported media adjustments",
+ "Export workflow",
+ ],
+ workflow: [
+ "Choose video assets from your workspace.",
+ "Arrange and refine the sequence.",
+ "Preview and export the finished result.",
+ ],
+ keywords: [
+ "trim",
+ "edit clip",
+ "video editing",
+ "combine clips",
+ "assemble",
+ ],
+ relatedIds: ["video-studio", "mediakit"],
+ },
+ "voice-studio": {
+ slug: "ai-voice-generator",
+ category: "audio",
+ featured: true,
+ headline: "Find the voice behind the story.",
+ summary:
+ "Transform written copy into narration using available voices and the supported speech controls, then carry the audio into your creative project.",
+ idealFor:
+ "Advertisement narration, explainers, social videos and spoken content.",
+ useCases: [
+ "Narrate a product advertisement",
+ "Produce voice for a video explainer",
+ "Compare a voiceover direction before final production",
+ ],
+ capabilities: [
+ "Text-to-speech",
+ "Voice selection",
+ "Supported pacing and speech controls",
+ "Reusable audio output",
+ ],
+ workflow: [
+ "Write or paste the narration.",
+ "Choose an available voice and its supported settings.",
+ "Review the quote and generate the audio.",
+ ],
+ keywords: ["tts", "text to speech", "narration", "speech", "voiceover"],
+ relatedIds: ["voice-casting", "audio-generation", "video-studio"],
+ },
+ "audio-generation": {
+ slug: "advanced-audio-generation",
+ category: "audio",
+ headline: "Shape audio around the feeling.",
+ summary:
+ "Use the dedicated advanced audio workspace for model-supported prompt-directed sound and voice generation, including reference-led workflows where available.",
+ idealFor:
+ "Expressive audio ideas and creative voice-production experiments.",
+ useCases: [
+ "Explore an expressive voice direction",
+ "Work from a permitted reference when supported",
+ "Create audio to accompany a wider campaign",
+ ],
+ capabilities: [
+ "Dedicated audio generation workspace",
+ "Prompt and supported references",
+ "Model-specific controls",
+ "Asset-backed results",
+ ],
+ workflow: [
+ "Choose the advanced audio workflow.",
+ "Supply text, direction or supported reference material.",
+ "Review the current quote before submission.",
+ ],
+ keywords: [
+ "seed audio",
+ "dola",
+ "advanced audio",
+ "voice matching",
+ "sound",
+ ],
+ relatedIds: ["voice-studio", "voice-casting"],
+ },
+ transcription: {
+ slug: "ai-transcription",
+ category: "audio",
+ headline: "Turn spoken content into something usable.",
+ summary:
+ "Convert source recordings into readable text and available subtitle formats for editing, review and accessible distribution.",
+ idealFor: "Video captions, interviews and speech-to-text workflows.",
+ useCases: [
+ "Prepare captions for a short video",
+ "Get a readable transcript from a recording",
+ "Export supported subtitle formats",
+ ],
+ capabilities: [
+ "Audio and supported video input",
+ "Speech transcription",
+ "SRT and VTT export",
+ "Saved result workflow",
+ ],
+ workflow: [
+ "Choose an eligible audio or video file.",
+ "Select the available transcription options.",
+ "Review the quote and export the result.",
+ ],
+ keywords: [
+ "speech to text",
+ "subtitles",
+ "captions",
+ "srt",
+ "vtt",
+ "transcript",
+ ],
+ relatedIds: ["video-editor", "voice-studio"],
+ },
+ "voice-casting": {
+ slug: "voice-casting",
+ category: "audio",
+ headline: "Cast a voice before recording the story.",
+ summary:
+ "Audition, compare and choose from verified voice options so a narration begins with the right creative direction.",
+ idealFor:
+ "Brand narration, character direction and repeatable voice selection.",
+ useCases: [
+ "Compare narration approaches for an ad",
+ "Choose a fitting brand voice",
+ "Prepare a voice choice before synthesis",
+ ],
+ capabilities: [
+ "Voice catalogue",
+ "Audition and comparison",
+ "Verified voice options",
+ "Connection to speech workflows",
+ ],
+ workflow: [
+ "Explore the available voice catalogue.",
+ "Audition and compare relevant options.",
+ "Continue into an appropriate speech workflow.",
+ ],
+ keywords: ["cast voice", "voice catalogue", "voice audition", "speaker"],
+ relatedIds: ["voice-studio", "audio-generation"],
+ },
+ "character-chat": {
+ slug: "character-chat",
+ category: "creative",
+ headline: "Converse with a character, not a blank prompt.",
+ summary:
+ "Explore ideas and dialogue through persona-led conversations with model-dependent capabilities and optional voice experiences.",
+ idealFor: "Character development, dialogue practice and creative roleplay.",
+ useCases: [
+ "Explore a fictional character's point of view",
+ "Workshop dialogue in context",
+ "Get a new perspective on a story concept",
+ ],
+ capabilities: [
+ "Multi-turn persona conversations",
+ "Built-in and custom personas",
+ "Supported voice interactions",
+ "Conversation continuity",
+ ],
+ workflow: [
+ "Choose a persona and an enabled conversation model.",
+ "Start your conversation with a scenario.",
+ "Refine it through follow-up turns.",
+ ],
+ keywords: ["ai character", "persona", "chat", "roleplay", "dialogue"],
+ relatedIds: ["brand-story", "scriptwriter"],
+ },
+ "creative-director": {
+ slug: "ai-creative-director",
+ category: "creative",
+ featured: true,
+ headline: "Start with direction, not guesswork.",
+ summary:
+ "Turn a rough campaign brief into clearer creative ideas, scene direction and structured production planning before creating media.",
+ idealFor:
+ "Agencies, promotional campaigns, concept development and storyboards.",
+ useCases: [
+ "Find an advertising concept from a client brief",
+ "Plan shots for a promotional video",
+ "Connect a storyboard to a production workflow",
+ ],
+ capabilities: [
+ "Creative brief development",
+ "Concept and shot direction",
+ "Storyboard-oriented planning",
+ "Follow-up refinement",
+ ],
+ workflow: [
+ "Describe your objective and audience.",
+ "Develop the creative direction with the chosen enabled model.",
+ "Carry the resulting brief into an appropriate studio.",
+ ],
+ keywords: [
+ "art director",
+ "campaign",
+ "storyboard",
+ "creative brief",
+ "concept",
+ ],
+ relatedIds: ["scriptwriter", "video-studio", "brand-story"],
+ },
+ scriptwriter: {
+ slug: "ai-scriptwriter",
+ category: "creative",
+ headline: "Get your message into the right words.",
+ summary:
+ "Develop commercial scripts, dialogue and story outlines in a focused writing workspace designed for iteration.",
+ idealFor: "Short video ads, brand storytelling and production scripts.",
+ useCases: [
+ "Outline a 30-second product advertisement",
+ "Draft narration before generating a voice",
+ "Explore alternative dialogue or story beats",
+ ],
+ capabilities: [
+ "Script drafting",
+ "Dialogue and narrative variants",
+ "Iterative revisions",
+ "Creative context",
+ ],
+ workflow: [
+ "Describe the audience, format and message.",
+ "Generate a first structured draft.",
+ "Edit and refine it before moving to production.",
+ ],
+ keywords: [
+ "script",
+ "screenplay",
+ "advertisement copy",
+ "dialogue",
+ "commercial",
+ ],
+ relatedIds: ["creative-director", "voice-studio", "brand-story"],
+ },
+ "brand-story": {
+ slug: "brand-and-story",
+ category: "creative",
+ headline: "Keep every creative decision on-brand.",
+ summary:
+ "Develop your brand's creative voice and plan story structure so the next campaign grows from a clear, consistent direction.",
+ idealFor: "Brand teams, story development and campaign continuity.",
+ useCases: [
+ "Define a direction for campaign messaging",
+ "Structure a longer story into beats",
+ "Build repeatable creative guidance",
+ ],
+ capabilities: [
+ "Brand-focused assistants",
+ "Story planning",
+ "Character and narrative development",
+ "Context-driven refinement",
+ ],
+ workflow: [
+ "Choose the brand or story task.",
+ "Add your constraints and creative direction.",
+ "Refine the result and continue into connected tools.",
+ ],
+ keywords: [
+ "brand assistant",
+ "brand voice",
+ "story planner",
+ "narrative",
+ "brand strategy",
+ ],
+ relatedIds: ["creative-director", "scriptwriter", "character-chat"],
+ },
+ mediakit: {
+ slug: "mediakit",
+ category: "finishing",
+ headline: "Give existing media its finishing touches.",
+ summary:
+ "Discover the available processing tools for image and video preparation, from compression and cropping to other published media operations.",
+ idealFor:
+ "Final asset preparation, image transformations and supported video processing.",
+ useCases: [
+ "Prepare images for easier delivery",
+ "Apply an approved watermark",
+ "Improve or assess compatible video media",
+ ],
+ capabilities: [
+ "Categorized image operations",
+ "Video-finishing operations",
+ "Source media validation",
+ "Pre-execution credit quote",
+ ],
+ workflow: [
+ "Choose a currently enabled operation.",
+ "Select the required source asset and controls.",
+ "Review its quote before processing.",
+ ],
+ keywords: [
+ "compress",
+ "crop",
+ "watermark",
+ "background",
+ "media processing",
+ "lip sync",
+ ],
+ relatedIds: ["precision-image", "video-editor", "spokesperson"],
+ },
+ templates: {
+ slug: "generation-templates",
+ category: "creative",
+ headline: "Start with a creative recipe.",
+ summary:
+ "Browse illustrated, reusable starting points that carry a brief and validated defaults into the relevant Studio without making you operate another composer.",
+ idealFor:
+ "Fast starts, repeatable campaign formats and familiar creative briefs.",
+ useCases: [
+ "Start a product visual from a prepared brief",
+ "Create a familiar social-media format",
+ "Explore a new workflow with less setup",
+ ],
+ capabilities: [
+ "Illustrated template gallery",
+ "Task-focused starting points",
+ "Direct Studio activation",
+ "Editable creation settings",
+ ],
+ workflow: [
+ "Browse a template that matches the outcome.",
+ "Open its linked workspace Studio.",
+ "Customize and review any generation quote.",
+ ],
+ keywords: ["recipe", "preset", "instagram", "youtube", "template"],
+ relatedIds: ["image-studio", "video-studio", "voice-studio"],
+ },
+};
+
+export type PublicTool = ToolEditorial & {
+ id: string;
+ title: string;
+ icon: IconName;
+ segment: string;
+ cover: string;
+};
+const cover = (id: string) => "/tool-art/" + id + ".svg";
+export const publicTools: readonly PublicTool[] = WORKSPACE_TOOLS.map(
+ (tool) => {
+ const item = editorial[tool.id];
+ if (!item) throw new Error("Missing public editorial for " + tool.id);
+ return {
+ ...item,
+ id: tool.id,
+ title: tool.title,
+ icon: tool.icon,
+ segment: tool.segment,
+ cover: cover(tool.id),
+ };
+ },
+);
+
+export function publicToolBySlug(slug: string): PublicTool | undefined {
+ return publicTools.find((tool) => tool.slug === slug);
+}
+export function publicToolById(id: string): PublicTool | undefined {
+ return publicTools.find((tool) => tool.id === id);
+}
+export function toolHref(tool: Pick): string {
+ return "/tools/" + tool.slug;
+}
+export function toolSearch(tool: PublicTool): string {
+ return [
+ tool.title,
+ tool.headline,
+ tool.summary,
+ tool.idealFor,
+ ...tool.useCases,
+ ...tool.capabilities,
+ ...tool.keywords,
+ ]
+ .join(" ")
+ .toLocaleLowerCase();
+}
+export function findPublicTools(query: string, category = "all"): PublicTool[] {
+ const q = query.trim().toLocaleLowerCase().slice(0, 100);
+ return publicTools.filter(
+ (tool) =>
+ (category === "all" || tool.category === category) &&
+ (!q || toolSearch(tool).includes(q)),
+ );
+}
+export type ToolIntent =
+ | "image"
+ | "edit-image"
+ | "video"
+ | "presenter"
+ | "voice"
+ | "transcribe"
+ | "plan"
+ | "finish";
+export type ToolSource = "idea" | "image" | "video" | "audio";
+const choices: Record>> = {
+ image: {
+ idea: ["image-studio", "templates"],
+ image: ["image-studio", "precision-image"],
+ },
+ "edit-image": {
+ image: ["precision-image", "mediakit"],
+ idea: ["image-studio"],
+ },
+ video: {
+ idea: ["video-studio", "creative-director"],
+ image: ["video-studio", "image-studio"],
+ video: ["video-editor", "mediakit"],
+ },
+ presenter: {
+ image: ["spokesperson", "voice-studio"],
+ audio: ["spokesperson", "voice-studio"],
+ idea: ["scriptwriter", "spokesperson"],
+ },
+ voice: {
+ idea: ["voice-studio", "voice-casting"],
+ audio: ["audio-generation", "voice-casting"],
+ },
+ transcribe: { audio: ["transcription"], video: ["transcription"] },
+ plan: {
+ idea: ["creative-director", "scriptwriter"],
+ image: ["creative-director", "image-studio"],
+ video: ["creative-director", "video-editor"],
+ audio: ["scriptwriter", "voice-studio"],
+ },
+ finish: {
+ image: ["mediakit", "precision-image"],
+ video: ["mediakit", "video-editor"],
+ },
+};
+export function recommendTools(
+ intent: ToolIntent,
+ source: ToolSource,
+): PublicTool[] {
+ return (choices[intent]?.[source] ?? []).flatMap((id) => {
+ const tool = publicToolById(id);
+ return tool ? [tool] : [];
+ });
+}
diff --git a/e2e/tools.spec.ts b/e2e/tools.spec.ts
new file mode 100644
index 000000000..b2a201ff9
--- /dev/null
+++ b/e2e/tools.spec.ts
@@ -0,0 +1,75 @@
+import { expect, test } from "@playwright/test";
+
+test("public tools directory remains usable at mobile, tablet and desktop widths", async ({
+ page,
+}) => {
+ const response = await page.goto("/tools");
+ expect(response?.status()).toBe(200);
+ await expect(
+ page.getByRole("heading", {
+ name: /every creative tool.*one connected workspace/i,
+ }),
+ ).toBeVisible();
+ await expect(
+ page.getByRole("heading", { name: "Find a tool for the job." }),
+ ).toBeVisible();
+ for (const width of [320, 375, 768, 1024, 1440]) {
+ await page.setViewportSize({ width, height: 850 });
+ const overflow = await page.evaluate(
+ () => document.documentElement.scrollWidth - window.innerWidth,
+ );
+ expect(
+ overflow,
+ "Unexpected overflow at " + width + "px",
+ ).toBeLessThanOrEqual(1);
+ }
+});
+
+test("search, filters and rule-based tool finder are accessible without login", async ({
+ page,
+}) => {
+ await page.goto("/tools");
+ await page
+ .getByRole("searchbox", { name: "Search creative tools" })
+ .fill("transcription");
+ await expect(
+ page
+ .locator("#directory")
+ .getByRole("heading", { name: "Speech / Transcription" }),
+ ).toBeVisible();
+ await page.getByRole("button", { name: "Image & Design" }).click();
+ await expect(page.getByText("No matching tools yet.")).toBeVisible();
+ await page.getByRole("button", { name: "Clear filters" }).click();
+ await page.getByRole("button", { name: "Make a video" }).click();
+ await page.getByRole("button", { name: "An image", exact: true }).click();
+ await expect(
+ page.locator("#tool-finder").getByText("Video Studio", { exact: true }),
+ ).toBeVisible();
+ await expect(
+ page.locator("#tool-finder").getByRole("link", { name: "Compare pricing" }),
+ ).toBeVisible();
+});
+
+test("public tool detail keeps signup destination allowlisted", async ({
+ page,
+}) => {
+ const response = await page.goto("/tools/ai-image-generator");
+ expect(response?.status()).toBe(200);
+ await expect(
+ page.getByRole("heading", { name: "Make the image you can imagine." }),
+ ).toBeVisible();
+ await expect(
+ page.getByRole("link", { name: "Explore pricing" }).first(),
+ ).toHaveAttribute("href", "/pricing");
+ await page.getByRole("link", { name: "Start creating" }).last().click();
+ await expect(page).toHaveURL(/\/tools\/start\/image-studio$/);
+ await expect(
+ page.getByRole("heading", { name: "Continue to Image Studio." }),
+ ).toBeVisible();
+ await expect(
+ page.getByRole("link", { name: "Create an account" }),
+ ).toHaveAttribute(
+ "href",
+ "/sign-up?returnTo=%2Ftools%2Fstart%2Fimage-studio",
+ );
+});