diff --git a/.gitignore b/.gitignore
index b2026fa..b1de687 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,5 @@ dist-ssr
.next/dev/trace
.next/dev/cache/turbopack/23c46498/CURRENT
.next/dev/cache/turbopack/23c46498/LOG
+.next/dev/cache/turbopack/23c46498/CURRENT
+.next/dev/cache/turbopack/23c46498/LOG
diff --git a/next-env.d.ts b/next-env.d.ts
index 9edff1c..c4b7818 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/types/routes.d.ts";
+import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/package-lock.json b/package-lock.json
index 37f592e..4e8fd07 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "plumeia",
"version": "0.1.0",
"dependencies": {
+ "@anthropic-ai/sdk": "^0.78.0",
"@google/genai": "^1.38.0",
"@prisma/adapter-pg": "^7.4.2",
"@prisma/client": "^7.4.1",
@@ -17,6 +18,7 @@
"lucide-react": "^0.563.0",
"next": "16.1.6",
"next-auth": "^5.0.0-beta.30",
+ "openai": "^6.25.0",
"pg": "^8.19.0",
"prisma": "^7.4.1",
"react": "19.2.3",
@@ -52,6 +54,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@anthropic-ai/sdk": {
+ "version": "0.78.0",
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.78.0.tgz",
+ "integrity": "sha512-PzQhR715td/m1UaaN5hHXjYB8Gl2lF9UVhrrGrZeysiF6Rb74Wc9GCB8hzLdzmQtBd1qe89F9OptgB9Za1Ib5w==",
+ "license": "MIT",
+ "dependencies": {
+ "json-schema-to-ts": "^3.1.1"
+ },
+ "bin": {
+ "anthropic-ai-sdk": "bin/cli"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@auth/core": {
"version": "0.41.0",
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.0.tgz",
@@ -273,6 +295,15 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
+ "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
@@ -6008,6 +6039,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-schema-to-ts": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
+ "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "ts-algebra": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -6962,6 +7006,27 @@
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"license": "MIT"
},
+ "node_modules/openai": {
+ "version": "6.25.0",
+ "resolved": "https://registry.npmjs.org/openai/-/openai-6.25.0.tgz",
+ "integrity": "sha512-mEh6VZ2ds2AGGokWARo18aPISI1OhlgdEIC1ewhkZr8pSIT31dec0ecr9Nhxx0JlybyOgoAT1sWeKtwPZzJyww==",
+ "license": "Apache-2.0",
+ "bin": {
+ "openai": "bin/cli"
+ },
+ "peerDependencies": {
+ "ws": "^8.18.0",
+ "zod": "^3.25 || ^4.0"
+ },
+ "peerDependenciesMeta": {
+ "ws": {
+ "optional": true
+ },
+ "zod": {
+ "optional": true
+ }
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -8472,6 +8537,12 @@
"node": ">=8.0"
}
},
+ "node_modules/ts-algebra": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
+ "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
+ "license": "MIT"
+ },
"node_modules/ts-api-utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
@@ -9056,7 +9127,7 @@
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/package.json b/package.json
index cbafdc3..deaa17b 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"lint": "next lint"
},
"dependencies": {
+ "@anthropic-ai/sdk": "^0.78.0",
"@google/genai": "^1.38.0",
"@prisma/adapter-pg": "^7.4.2",
"@prisma/client": "^7.4.1",
@@ -21,6 +22,7 @@
"lucide-react": "^0.563.0",
"next": "16.1.6",
"next-auth": "^5.0.0-beta.30",
+ "openai": "^6.25.0",
"pg": "^8.19.0",
"prisma": "^7.4.1",
"react": "19.2.3",
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 9feb9ff..a81cca8 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -40,6 +40,10 @@ model User {
planId String? @default("free")
subscriptionPlan Plan? @relation(fields: [planId], references: [id])
+ // Bring Your Own Key
+ customApiProvider String?
+ customApiKey String?
+
aiActionsUsed Int @default(0)
dailyWordGoal Int @default(500)
writingStreak Int @default(0)
diff --git a/prisma/seed.ts b/prisma/seed.ts
index df6fa76..f42ba8e 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -1,8 +1,13 @@
import { config } from 'dotenv';
-config({ path: '.env.local' });
-import getDB from '../src/lib/prisma';
+config();
+import { PrismaClient } from '@prisma/client';
+import { PrismaPg } from '@prisma/adapter-pg';
+import { Pool } from 'pg';
-const prisma = getDB();
+const connectionString = process.env.DATABASE_URL;
+const pool = new Pool({ connectionString });
+const adapter = new PrismaPg(pool);
+const prisma = new PrismaClient({ adapter });
async function main() {
console.log('Seeding plans...');
@@ -41,6 +46,17 @@ async function main() {
features: ['250 actions IA / mois', 'Accès Gemini 3 Pro', 'Bible du monde avancée', 'Outils de révision avancés'],
isPopular: false,
},
+ {
+ id: 'byok',
+ name: 'byok',
+ displayName: 'Clé Perso (BYOK)',
+ price: 4.99,
+ description: 'Utilisez vos propres clés API (ChatGPT, Claude, Gemini).',
+ maxProjects: -1,
+ maxAiActions: -1,
+ features: ['Tokens illimités via votre clé', 'Mode Bring Your Own Key', 'Choix du modèle IA', 'Projets illimités'],
+ isPopular: false,
+ },
];
for (const plan of plans) {
diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts
index 85f22ae..d29d4ce 100644
--- a/src/app/api/user/profile/route.ts
+++ b/src/app/api/user/profile/route.ts
@@ -54,6 +54,8 @@ export async function GET() {
dailyWordGoal: user.dailyWordGoal,
writingStreak: user.writingStreak,
lastWriteDate: user.lastWriteDate,
+ customApiProvider: user.customApiProvider,
+ customApiKey: user.customApiKey,
createdAt: user.createdAt,
totalWords,
});
@@ -78,6 +80,12 @@ export async function PUT(request: NextRequest) {
if (body.dailyWordGoal !== undefined) data.dailyWordGoal = body.dailyWordGoal;
if (body.writingStreak !== undefined) data.writingStreak = body.writingStreak;
if (body.lastWriteDate !== undefined) data.lastWriteDate = body.lastWriteDate ? new Date(body.lastWriteDate) : null;
+ if (body.customApiProvider !== undefined) data.customApiProvider = body.customApiProvider;
+ if (body.customApiKey !== undefined) data.customApiKey = body.customApiKey;
+ if (body.planId !== undefined) {
+ data.planId = body.planId;
+ data.plan = body.planId; // legacy sync
+ }
const updated = await prisma.user.update({
where: { id: session.user.id },
diff --git a/src/app/checkout/page.tsx b/src/app/checkout/page.tsx
index 9bd6f47..1edd52d 100644
--- a/src/app/checkout/page.tsx
+++ b/src/app/checkout/page.tsx
@@ -1,15 +1,67 @@
'use client';
import Checkout from '@/components/Checkout';
-import { useRouter } from 'next/navigation';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { useEffect, useState, Suspense } from 'react';
-export default function CheckoutPage() {
+function CheckoutContent() {
const router = useRouter();
+ const searchParams = useSearchParams();
+ const planId = searchParams.get('plan') || 'pro';
+
+ const [plan, setPlan] = useState<{ id: string, displayName: string, price: number } | null>(null);
+
+ useEffect(() => {
+ fetch('/api/plans', { cache: 'no-store' })
+ .then(res => res.json())
+ .then(data => {
+ const found = data.find((p: any) => p.id === planId);
+ if (found) {
+ setPlan(found);
+ } else {
+ setPlan(data.find((p: any) => p.id === 'pro') || { id: 'pro', displayName: 'Auteur Pro', price: 12.00 });
+ }
+ })
+ .catch(err => {
+ console.error(err);
+ setPlan({ id: 'pro', displayName: 'Auteur Pro', price: 12.00 });
+ });
+ }, [planId]);
+
+ const handleComplete = async () => {
+ if (plan) {
+ try {
+ await fetch('/api/user/profile', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ planId: plan.id })
+ });
+ // Force a full reload to update user context in providers
+ window.location.href = '/dashboard';
+ } catch (err) {
+ console.error('Failed to update plan', err);
+ router.push('/dashboard');
+ }
+ } else {
+ router.push('/dashboard');
+ }
+ };
+
+ if (!plan) return
Chargement...
;
return (
router.push('/dashboard')}
+ plan={plan}
+ onComplete={handleComplete}
onCancel={() => router.push('/pricing')}
/>
);
}
+
+export default function CheckoutPage() {
+ return (
+ Chargement...}>
+
+
+ );
+}
diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx
index c5e5227..31b3acc 100644
--- a/src/app/pricing/page.tsx
+++ b/src/app/pricing/page.tsx
@@ -31,7 +31,7 @@ export default function PricingPage() {
isLoading={isLoading}
currentPlan={user?.subscription.plan || 'free'}
onBack={() => router.push(user ? '/dashboard' : '/')}
- onSelectPlan={() => router.push(user ? '/checkout' : '/login')}
+ onSelectPlan={(id) => router.push(user ? `/checkout?plan=${id}` : '/login')}
/>
);
}
diff --git a/src/components/Checkout.tsx b/src/components/Checkout.tsx
index 6921112..1354d8b 100644
--- a/src/components/Checkout.tsx
+++ b/src/components/Checkout.tsx
@@ -6,11 +6,12 @@ import { CreditCard, Shield, Lock, ArrowRight, Loader2 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
interface CheckoutProps {
+ plan: { id: string; displayName: string; price: number };
onComplete: () => void;
onCancel: () => void;
}
-const Checkout: React.FC = ({ onComplete, onCancel }) => {
+const Checkout: React.FC = ({ plan, onComplete, onCancel }) => {
const [loading, setLoading] = useState(false);
const { t } = useLanguage();
@@ -28,10 +29,10 @@ const Checkout: React.FC = ({ onComplete, onCancel }) => {
{t('checkout.order')}
-
{t('checkout.pro_author')}12.00€
-
{t('checkout.vat')}2.40€
+
{plan.displayName}{plan.price.toFixed(2)}€
+
{t('checkout.vat')} (20%){(plan.price * 0.2).toFixed(2)}€
-
{t('checkout.total')}14.40€
+
{t('checkout.total')}{(plan.price * 1.2).toFixed(2)}€
diff --git a/src/components/UserProfileSettings.tsx b/src/components/UserProfileSettings.tsx
index 83634a5..7801150 100644
--- a/src/components/UserProfileSettings.tsx
+++ b/src/components/UserProfileSettings.tsx
@@ -6,6 +6,7 @@ import { UserProfile, UserPreferences } from '@/lib/types';
import { User, Settings, Globe, Shield, Bell, Save, Camera, Target, Flame, Layout } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
+import { useRouter } from 'next/navigation';
interface UserProfileSettingsProps {
user: UserProfile;
@@ -36,9 +37,13 @@ const UserProfileSettings: React.FC
= ({ user, onUpdat
panel: '#24283b',
text: '#c0caf5',
accent: '#7aa2f7'
- }
+ },
+ customApiProvider: user.customApiProvider || 'openai',
+ customApiKey: user.customApiKey || ''
});
+ const router = useRouter();
+
// Handle Live Preview of Theme Changes
useEffect(() => {
const root = document.documentElement;
@@ -114,7 +119,9 @@ const UserProfileSettings: React.FC = ({ user, onUpdat
theme: formData.theme,
dailyWordGoal: formData.dailyWordGoal,
customColors: formData.customColors
- }
+ },
+ customApiProvider: formData.customApiProvider as any,
+ customApiKey: formData.customApiKey
});
localStorage.setItem('plumeia_theme', formData.theme);
@@ -296,7 +303,12 @@ const UserProfileSettings: React.FC = ({ user, onUpdat
{t('profile.plan_title')} {(user.subscription.planDetails?.displayName || user.subscription.plan).toUpperCase()}
{t('profile.active_sub')}
-
+
@@ -309,7 +321,58 @@ const UserProfileSettings: React.FC = ({ user, onUpdat
/>
-
+ {/* BYOK Settings */}
+ {user.subscription.plan === 'byok' && (
+
+
+ Vos clés API (BYOK)
+
+
+ Vous avez l'abonnement "Clé Perso". Vous pouvez utiliser l'IA en illimité en renseignant votre propre clé API.
+
+
+
+
+
+
+
+
+
+
+ setFormData({ ...formData, customApiKey: e.target.value })}
+ placeholder="Collez votre clé secrète ici..."
+ className={`w-full p-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm ${themeInputBg}`}
+ />
+
+
+
+ )}
+
+ {user.subscription.plan !== 'byok' && user.subscription.plan !== 'master' && (
+
+
Passer à l'abonnement "Clé Perso (BYOK)" ?
+
Pour 4,99€/mois, utilisez vos propres clés API ChatGPT, Claude ou Gemini sans limite de tokens.
+
+
+ )}
+
+
diff --git a/src/lib/gemini.ts b/src/lib/gemini.ts
index 9e3d371..f33f4e3 100644
--- a/src/lib/gemini.ts
+++ b/src/lib/gemini.ts
@@ -2,6 +2,8 @@
// This file is only imported by API routes, never by client code
import { GoogleGenAI, Type } from "@google/genai";
+import OpenAI from "openai";
+import Anthropic from "@anthropic-ai/sdk";
import { BookProject, UserProfile } from "./types";
const truncate = (str: string, length: number) => {
@@ -10,7 +12,8 @@ const truncate = (str: string, length: number) => {
};
const checkUsage = (user: UserProfile) => {
- if (user.subscription.plan === 'master') return true;
+ // BYOK and Master plans have unlimited AI actions
+ if (user.subscription.plan === 'master' || user.subscription.plan === 'byok') return true;
return user.usage.aiActionsCurrent < user.usage.aiActionsLimit;
};
@@ -86,37 +89,77 @@ export const generateStoryContent = async (
}
try {
- const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const finalPrompt = buildContextPrompt(project, currentChapterId, userPrompt);
- const modelName = user.subscription.plan === 'master' ? 'gemini-3-pro-preview' : 'gemini-3-flash-preview';
+ // --- BYOK: Bring Your Own Key Logic ---
+ const isBYOK = user.subscription.plan === 'byok' && user.customApiKey;
+ const provider = isBYOK ? (user.customApiProvider || 'openai') : 'gemini';
- const response = await ai.models.generateContent({
- model: modelName,
- contents: finalPrompt,
- config: {
+ if (provider === 'openai' && isBYOK) {
+ const openai = new OpenAI({ apiKey: user.customApiKey });
+ const completion = await openai.chat.completions.create({
+ model: "gpt-4o-mini",
+ messages: [{ role: "user", content: finalPrompt }],
+ response_format: { type: "json_object" },
temperature: 0.7,
- responseMimeType: "application/json",
- responseSchema: {
- type: Type.OBJECT,
- properties: {
- responseType: {
- type: Type.STRING,
- enum: ["draft", "reflection"]
- },
- content: {
- type: Type.STRING
+ });
+ const result = JSON.parse(completion.choices[0].message.content || "{}");
+ return {
+ text: result.content || "Erreur de génération.",
+ type: result.responseType || "reflection"
+ };
+ }
+ else if (provider === 'anthropic' && isBYOK) {
+ const anthropic = new Anthropic({ apiKey: user.customApiKey });
+ // Anthropic doesn't have strict JSON mode matching OpenAI's structure natively at this tier,
+ // so we prompt it to return JSON.
+ const systemPrompt = "You must respond in valid JSON format with exact keys: `responseType` (either 'draft' or 'reflection') and `content` (string).";
+ const message = await anthropic.messages.create({
+ model: "claude-3-haiku-20240320",
+ max_tokens: 4000,
+ system: systemPrompt,
+ messages: [{ role: "user", content: finalPrompt }],
+ });
+ const textContent = message.content[0].type === 'text' ? message.content[0].text : '{}';
+ const result = JSON.parse(textContent);
+ return {
+ text: result.content || "Erreur de génération.",
+ type: result.responseType || "reflection"
+ };
+ }
+ else {
+ // Default: Pluume Gemini implementation
+ const ai = new GoogleGenAI({ apiKey: isBYOK && user.customApiProvider === 'gemini' ? user.customApiKey : process.env.GEMINI_API_KEY });
+ const modelName = user.subscription.plan === 'master' ? 'gemini-3-pro-preview' : 'gemini-3-flash-preview';
+
+ const response = await ai.models.generateContent({
+ model: modelName,
+ contents: finalPrompt,
+ config: {
+ temperature: 0.7,
+ responseMimeType: "application/json",
+ responseSchema: {
+ type: Type.OBJECT,
+ properties: {
+ responseType: {
+ type: Type.STRING,
+ enum: ["draft", "reflection"]
+ },
+ content: {
+ type: Type.STRING
+ }
}
}
}
- }
- });
+ });
+
+ const result = JSON.parse(response.text || "{}");
+ return {
+ text: result.content || "Erreur de génération.",
+ type: result.responseType || "reflection"
+ };
+ }
- const result = JSON.parse(response.text || "{}");
- return {
- text: result.content || "Erreur de génération.",
- type: result.responseType || "reflection"
- };
} catch (error) {
console.error("AI Generation Error:", error);
return { text: "Erreur lors de la communication avec l'IA.", type: 'reflection' };
@@ -130,11 +173,35 @@ export const transformTextServer = async (
user: UserProfile,
): Promise => {
if (!checkUsage(user)) return "Limite d'actions IA atteinte.";
+
try {
- const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
- const prompt = `Action: ${mode}. Texte: ${text}. Contexte: ${truncate(context, 1000)}. Renvoie juste le texte transformé.`;
- const response = await ai.models.generateContent({ model: 'gemini-3-flash-preview', contents: prompt });
- return response.text?.trim() || text;
+ const prompt = `Action: ${mode}. Texte: ${text}. Contexte: ${truncate(context, 1000)}. Renvoie juste le texte transformé sans commentaires.`;
+
+ const isBYOK = user.subscription.plan === 'byok' && user.customApiKey;
+ const provider = isBYOK ? (user.customApiProvider || 'openai') : 'gemini';
+
+ if (provider === 'openai' && isBYOK) {
+ const openai = new OpenAI({ apiKey: user.customApiKey });
+ const completion = await openai.chat.completions.create({
+ model: "gpt-4o-mini",
+ messages: [{ role: "user", content: prompt }],
+ });
+ return completion.choices[0].message.content || text;
+ }
+ else if (provider === 'anthropic' && isBYOK) {
+ const anthropic = new Anthropic({ apiKey: user.customApiKey });
+ const message = await anthropic.messages.create({
+ model: "claude-3-haiku-20240320",
+ max_tokens: 4000,
+ messages: [{ role: "user", content: prompt }],
+ });
+ return message.content[0].type === 'text' ? message.content[0].text : text;
+ }
+ else {
+ const ai = new GoogleGenAI({ apiKey: isBYOK && user.customApiProvider === 'gemini' ? user.customApiKey : process.env.GEMINI_API_KEY });
+ const response = await ai.models.generateContent({ model: 'gemini-3-flash-preview', contents: prompt });
+ return response.text?.trim() || text;
+ }
} catch {
return text;
}
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 230611f..e8e0200 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -164,6 +164,9 @@ export interface UserPreferences {
};
dailyWordGoal: number;
language: 'fr' | 'en';
+ customApiProvider?: 'openai' | 'anthropic' | 'gemini';
+ // We can store the masked key or omit it from the client
+ customApiKey?: string;
}
export interface UserStats {
@@ -182,6 +185,8 @@ export interface UserProfile {
usage: UserUsage;
preferences: UserPreferences;
stats: UserStats;
+ customApiProvider?: 'openai' | 'anthropic' | 'gemini';
+ customApiKey?: string;
}
export type ViewMode = 'write' | 'world_building' | 'workflow' | 'settings' | 'preview' | 'ideas' | 'landing' | 'features' | 'pricing' | 'checkout' | 'dashboard' | 'auth' | 'signup' | 'profile';