Quick Start Guide: Creating Your First Tool
🚀 30-Minute Tool Creation
This guide will walk you through creating a complete, production-ready tool in 30 minutes using MyStoryFlow’s standardized infrastructure. We’ll build a “Plot Twist Generator” as our example.
📋 Prerequisites
Required Skills
- Basic TypeScript/React knowledge
- Familiarity with Next.js App Router
- Understanding of SQL/database migrations
Environment Setup
# 1. Clone the repository (if not already done)
git clone https://github.com/your-org/mystoryflow-monorepo
cd mystoryflow-monorepo
# 2. Navigate to tools app
cd apps/tools-app
# 3. Install dependencies
npm install
# 4. Copy environment template
cp .env.example .env.local
# 5. Configure environment variables
nano .env.localRequired Environment Variables
# Main App Database (for admin tracking)
NEXT_PUBLIC_SUPABASE_URL=https://your-main-app.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
# Tools Database (separate instance)
NEXT_PUBLIC_TOOLS_DATABASE_URL=https://your-tools.supabase.co
NEXT_PUBLIC_TOOLS_DATABASE_ANON_KEY=your_tools_anon_key
# AI Services
OPENAI_API_KEY=sk-your-openai-key
# App URLs
NEXT_PUBLIC_TOOLS_APP_URL=https://tools.mystoryflow.com
WEB_APP_URL=https://mystoryflow.com🛠️ Step 1: Database Migration (5 minutes)
Create Migration File
Create: supabase/migrations/20240721_120000_create_plot_twist_generator.sql
-- Plot Twist Generator Migration
-- Created: 2024-07-21T12:00:00Z
-- Purpose: Create plot twist generator tool infrastructure
-- ===========================================
-- MAIN CONTENT TABLE
-- ===========================================
CREATE TABLE IF NOT EXISTS tools_plot_twists (
-- Universal Identity
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
share_code TEXT UNIQUE NOT NULL,
-- Ownership & Sessions
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
session_id TEXT NOT NULL,
-- Visibility & Quality Control
is_public BOOLEAN DEFAULT FALSE,
is_featured BOOLEAN DEFAULT FALSE,
is_reviewed BOOLEAN DEFAULT FALSE,
is_archived BOOLEAN DEFAULT FALSE,
-- Analytics & Performance
view_count INTEGER DEFAULT 0,
share_count INTEGER DEFAULT 0,
export_count INTEGER DEFAULT 0,
use_count INTEGER DEFAULT 0,
response_count INTEGER DEFAULT 0,
rating_sum INTEGER DEFAULT 0,
rating_count INTEGER DEFAULT 0,
average_rating DECIMAL(3,2) GENERATED ALWAYS AS (
CASE WHEN rating_count > 0 THEN rating_sum::DECIMAL / rating_count ELSE 0 END
) STORED,
-- AI Integration & Cost Tracking
ai_generation_prompt TEXT,
ai_tokens_used INTEGER DEFAULT 0,
ai_cost_cents INTEGER DEFAULT 0,
ai_model TEXT DEFAULT 'gpt-4o-mini-2024-07-18',
ai_generation_time_ms INTEGER,
ai_confidence_score DECIMAL(3,2),
ai_variant_type TEXT DEFAULT 'standard',
-- SEO & Discovery
seo_title TEXT,
seo_description TEXT,
keywords TEXT[] DEFAULT '{}',
canonical_url TEXT,
og_title TEXT,
og_description TEXT,
og_image_url TEXT,
-- Content Storage
content JSONB NOT NULL,
options JSONB DEFAULT '{}'::jsonb,
metadata JSONB DEFAULT '{}'::jsonb,
-- Lifecycle Management
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
last_accessed_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ,
-- Constraints
CONSTRAINT valid_content CHECK (content IS NOT NULL AND content != '{}'::jsonb),
CONSTRAINT valid_rating_bounds CHECK (rating_sum >= 0 AND rating_count >= 0),
CONSTRAINT valid_ai_confidence CHECK (ai_confidence_score IS NULL OR (ai_confidence_score >= 0 AND ai_confidence_score <= 1)),
CONSTRAINT valid_counts CHECK (
view_count >= 0 AND share_count >= 0 AND export_count >= 0 AND
use_count >= 0 AND response_count >= 0
),
CONSTRAINT valid_ownership CHECK (
(user_id IS NULL AND session_id IS NOT NULL) OR
(user_id IS NOT NULL AND session_id IS NOT NULL)
)
);
-- ===========================================
-- USER RESPONSES TABLE
-- ===========================================
CREATE TABLE IF NOT EXISTS tools_plot_twist_responses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
plot_twist_id UUID REFERENCES tools_plot_twists(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
session_id TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
content_type TEXT DEFAULT 'text/plain',
word_count INTEGER GENERATED ALWAYS AS (
array_length(string_to_array(trim(content), ' '), 1)
) STORED,
character_count INTEGER GENERATED ALWAYS AS (length(trim(content))) STORED,
is_public BOOLEAN DEFAULT FALSE,
is_featured BOOLEAN DEFAULT FALSE,
is_flagged BOOLEAN DEFAULT FALSE,
moderation_status TEXT DEFAULT 'pending' CHECK (
moderation_status IN ('pending', 'approved', 'rejected', 'review_required')
),
moderated_at TIMESTAMPTZ,
moderated_by UUID REFERENCES auth.users(id),
view_count INTEGER DEFAULT 0,
like_count INTEGER DEFAULT 0,
share_count INTEGER DEFAULT 0,
flag_count INTEGER DEFAULT 0,
quality_score DECIMAL(3,2) DEFAULT 0.0,
engagement_score DECIMAL(5,2) GENERATED ALWAYS AS (
(like_count * 1.0) + (share_count * 2.0) + (view_count * 0.1) - (flag_count * 3.0)
) STORED,
seo_title TEXT,
seo_description TEXT,
slug TEXT UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
published_at TIMESTAMPTZ,
CONSTRAINT valid_response_content CHECK (
length(trim(title)) >= 1 AND length(trim(content)) >= 1
),
CONSTRAINT valid_response_counts CHECK (
view_count >= 0 AND like_count >= 0 AND share_count >= 0 AND
flag_count >= 0 AND quality_score >= 0 AND quality_score <= 1
),
CONSTRAINT valid_response_ownership CHECK (
(user_id IS NULL AND session_id IS NOT NULL) OR
(user_id IS NOT NULL AND session_id IS NOT NULL)
)
);
-- ===========================================
-- PERFORMANCE INDEXES
-- ===========================================
-- Main table indexes
CREATE INDEX IF NOT EXISTS idx_tools_plot_twists_share_code ON tools_plot_twists(share_code);
CREATE INDEX IF NOT EXISTS idx_tools_plot_twists_session ON tools_plot_twists(session_id);
CREATE INDEX IF NOT EXISTS idx_tools_plot_twists_user ON tools_plot_twists(user_id) WHERE user_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_tools_plot_twists_public ON tools_plot_twists(created_at DESC) WHERE is_public = true AND is_archived = false;
CREATE INDEX IF NOT EXISTS idx_tools_plot_twists_featured ON tools_plot_twists(created_at DESC) WHERE is_featured = true AND is_archived = false;
CREATE INDEX IF NOT EXISTS idx_tools_plot_twists_quality ON tools_plot_twists(average_rating DESC, view_count DESC) WHERE is_public = true AND is_reviewed = true;
CREATE INDEX IF NOT EXISTS idx_tools_plot_twists_keywords ON tools_plot_twists USING GIN(keywords) WHERE is_public = true;
CREATE INDEX IF NOT EXISTS idx_tools_plot_twists_expires ON tools_plot_twists(expires_at) WHERE expires_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_tools_plot_twists_last_accessed ON tools_plot_twists(last_accessed_at);
-- Response table indexes
CREATE INDEX IF NOT EXISTS idx_tools_plot_twist_responses_parent ON tools_plot_twist_responses(plot_twist_id);
CREATE INDEX IF NOT EXISTS idx_tools_plot_twist_responses_user ON tools_plot_twist_responses(user_id) WHERE user_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_tools_plot_twist_responses_session ON tools_plot_twist_responses(session_id);
CREATE INDEX IF NOT EXISTS idx_tools_plot_twist_responses_public ON tools_plot_twist_responses(published_at DESC NULLS LAST) WHERE is_public = true AND moderation_status = 'approved';
CREATE INDEX IF NOT EXISTS idx_tools_plot_twist_responses_featured ON tools_plot_twist_responses(engagement_score DESC, published_at DESC) WHERE is_featured = true;
CREATE INDEX IF NOT EXISTS idx_tools_plot_twist_responses_moderation ON tools_plot_twist_responses(moderation_status, created_at) WHERE moderation_status IN ('pending', 'review_required');
CREATE INDEX IF NOT EXISTS idx_tools_plot_twist_responses_slug ON tools_plot_twist_responses(slug) WHERE slug IS NOT NULL;
-- ===========================================
-- ROW LEVEL SECURITY
-- ===========================================
ALTER TABLE tools_plot_twists ENABLE ROW LEVEL SECURITY;
ALTER TABLE tools_plot_twist_responses ENABLE ROW LEVEL SECURITY;
-- Public content access
DROP POLICY IF EXISTS "tools_plot_twists_public_select" ON tools_plot_twists;
CREATE POLICY "tools_plot_twists_public_select" ON tools_plot_twists
FOR SELECT USING (
is_public = true AND is_archived = false AND
(expires_at IS NULL OR expires_at > NOW())
);
-- Session-based access
DROP POLICY IF EXISTS "tools_plot_twists_session_access" ON tools_plot_twists;
CREATE POLICY "tools_plot_twists_session_access" ON tools_plot_twists
FOR ALL USING (
session_id = current_setting('app.session_id', true) OR
user_id = auth.uid()
);
-- Response access policies
DROP POLICY IF EXISTS "tools_plot_twist_responses_public_select" ON tools_plot_twist_responses;
CREATE POLICY "tools_plot_twist_responses_public_select" ON tools_plot_twist_responses
FOR SELECT USING (is_public = true AND moderation_status = 'approved');
DROP POLICY IF EXISTS "tools_plot_twist_responses_session_access" ON tools_plot_twist_responses;
CREATE POLICY "tools_plot_twist_responses_session_access" ON tools_plot_twist_responses
FOR ALL USING (
session_id = current_setting('app.session_id', true) OR
user_id = auth.uid()
);
-- ===========================================
-- UNIVERSAL TRIGGERS
-- ===========================================
-- Updated timestamp triggers
CREATE TRIGGER tools_plot_twists_updated_at_trigger
BEFORE UPDATE ON tools_plot_twists
FOR EACH ROW
EXECUTE FUNCTION update_tools_timestamp();
CREATE TRIGGER tools_plot_twist_responses_updated_at_trigger
BEFORE UPDATE ON tools_plot_twist_responses
FOR EACH ROW
EXECUTE FUNCTION update_tools_timestamp();
-- Response count maintenance
CREATE TRIGGER tools_plot_twists_response_count_trigger
AFTER INSERT OR DELETE ON tools_plot_twist_responses
FOR EACH ROW
EXECUTE FUNCTION maintain_response_count();
-- Auto-generate SEO slugs
CREATE TRIGGER tools_plot_twist_responses_slug_trigger
BEFORE INSERT OR UPDATE ON tools_plot_twist_responses
FOR EACH ROW
EXECUTE FUNCTION generate_response_slug();
-- ===========================================
-- PERMISSIONS
-- ===========================================
GRANT ALL ON tools_plot_twists TO authenticated;
GRANT ALL ON tools_plot_twist_responses TO authenticated;
-- Migration completion log
INSERT INTO migration_log (
migration_name,
table_affected,
completed_at,
description
) VALUES (
'create_plot_twist_generator',
'tools_plot_twists',
NOW(),
'Create plot twist generator tool infrastructure'
);Run Migration
# Apply the migration
supabase db reset --linked
# or if using a different Supabase setup:
psql -h your-db-host -U postgres -d your-db < supabase/migrations/20240721_120000_create_plot_twist_generator.sql🤖 Step 2: AI Service Implementation (8 minutes)
Create AI Service Class
Create: src/lib/ai/plot-twist-ai.ts
import OpenAI from 'openai'
import { storePromptInAdmin, trackAIUsage, getRecommendedToolsModel } from '@/lib/ai-usage-tracker'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
})
export interface PlotTwistOptions {
genre: string
storyType: string
intensity: string
plotElements?: string[]
customContext?: string
}
export interface PlotTwistResult {
id: string
title: string
content: {
twists: Array<{
twist: string
explanation: string
implementation: string
impact: string
}>
analysis: {
overallAssessment: string
genreAlignment: string
suggestions: string[]
}
}
seoMetadata: {
title: string
description: string
keywords: string[]
}
usage: {
total_tokens: number
prompt_tokens: number
completion_tokens: number
}
estimatedCost: number
processingTime: number
confidenceScore: number
model: string
promptUsed: string
variantType: string
}
export class PlotTwistAI {
async generatePlotTwists(
options: PlotTwistOptions,
context: { sessionId: string; requestId: string }
): Promise<PlotTwistResult> {
const startTime = Date.now()
try {
// Store prompt in admin database
const promptId = await storePromptInAdmin({
name: 'plot_twist_generation_system',
version: '1.0',
content: this.buildSystemPrompt(options),
category: 'tools',
tool_name: 'plot_twists'
})
// Build user prompt
const userPrompt = this.buildUserPrompt(options)
// Call OpenAI API
const completion = await openai.chat.completions.create({
model: getRecommendedToolsModel(),
messages: [
{
role: 'system',
content: this.buildSystemPrompt(options)
},
{
role: 'user',
content: userPrompt
}
],
temperature: 0.8,
max_tokens: 2000,
response_format: { type: 'json_object' }
})
const processingTime = Date.now() - startTime
const response = JSON.parse(completion.choices[0].message.content || '{}')
// Calculate cost (approximate)
const estimatedCost = (completion.usage?.total_tokens || 0) * 0.00015 / 1000
// Generate SEO metadata
const seoMetadata = this.generateSEOMetadata(response, options)
const result: PlotTwistResult = {
id: `plot-twist-${Date.now()}`,
title: response.title || 'Generated Plot Twists',
content: response,
seoMetadata,
usage: completion.usage || { total_tokens: 0, prompt_tokens: 0, completion_tokens: 0 },
estimatedCost,
processingTime,
confidenceScore: response.confidence || 0.8,
model: getRecommendedToolsModel(),
promptUsed: promptId,
variantType: 'standard'
}
return result
} catch (error) {
console.error('Plot twist generation error:', error)
throw new Error(`AI generation failed: ${error.message}`)
}
}
private buildSystemPrompt(options: PlotTwistOptions): string {
return `You are an expert plot twist generator specializing in ${options.genre} stories.
Your task is to generate compelling, unexpected plot twists that enhance storytelling.
Guidelines:
- Create twists that are surprising but logical in hindsight
- Ensure twists are appropriate for ${options.genre} genre
- Match the ${options.intensity} intensity level requested
- Provide clear implementation guidance for writers
- Include impact analysis for story development
Response format (JSON):
{
"title": "Descriptive title for the twist collection",
"twists": [
{
"twist": "The actual plot twist",
"explanation": "Why this twist works",
"implementation": "How to set it up in the story",
"impact": "Effect on story and characters"
}
],
"analysis": {
"overallAssessment": "Assessment of the twist quality",
"genreAlignment": "How well it fits the genre",
"suggestions": ["Writing tips", "Additional ideas"]
},
"confidence": 0.85
}`
}
private buildUserPrompt(options: PlotTwistOptions): string {
let prompt = `Generate 3-5 compelling plot twists for a ${options.genre} story.
Requirements:
- Genre: ${options.genre}
- Story Type: ${options.storyType}
- Intensity Level: ${options.intensity}
`
if (options.plotElements?.length) {
prompt += `\nInclude these plot elements: ${options.plotElements.join(', ')}`
}
if (options.customContext) {
prompt += `\nAdditional Context: ${options.customContext}`
}
prompt += `\nEnsure each twist is unique, unexpected, and enhances the ${options.genre} genre experience.`
return prompt
}
private generateSEOMetadata(
response: any,
options: PlotTwistOptions
): { title: string; description: string; keywords: string[] } {
const title = `${options.genre} Plot Twists - ${response.title || 'Story Ideas'}`
const description = `Discover unexpected ${options.genre.toLowerCase()} plot twists to enhance your storytelling. AI-generated twists with implementation guides.`
const keywords = [
'plot twists',
`${options.genre.toLowerCase()} writing`,
'story ideas',
'creative writing',
'storytelling',
'writing prompts',
`${options.storyType.toLowerCase()} plots`
]
return { title, description, keywords }
}
}📡 Step 3: API Endpoints (10 minutes)
Generate Endpoint
Create: src/app/api/plot-twists/generate/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { trackAIUsage, getRecommendedToolsModel } from '@/lib/ai-usage-tracker'
import { checkRateLimit } from '@/lib/rate-limiter'
import { PlotTwistAI, type PlotTwistOptions } from '@/lib/ai/plot-twist-ai'
import { generateUniqueShareCode } from '@/lib/share-codes'
interface GenerateRequestBody {
sessionId: string
options: PlotTwistOptions
humanVerification?: string
}
export async function POST(request: NextRequest) {
const startTime = Date.now()
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
try {
const body: GenerateRequestBody = await request.json()
if (!body.sessionId) {
return NextResponse.json({
success: false,
error: {
code: 'MISSING_SESSION_ID',
message: 'Session ID is required',
field: 'sessionId',
timestamp: new Date().toISOString()
}
}, { status: 400 })
}
// Rate limiting
const rateLimitResult = await checkRateLimit(request, 'plot_twists_generation')
if (!rateLimitResult.allowed) {
return NextResponse.json({
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: rateLimitResult.message,
timestamp: new Date().toISOString()
},
meta: {
rateLimit: {
limit: rateLimitResult.limit,
remaining: rateLimitResult.remaining,
resetTime: rateLimitResult.resetTime,
retryAfter: rateLimitResult.retryAfter
},
conversionOpportunity: true
}
}, { status: 429 })
}
// AI Generation
const plotTwistAI = new PlotTwistAI()
const generationResult = await plotTwistAI.generatePlotTwists(body.options, {
sessionId: body.sessionId,
requestId
})
// Track AI usage
await trackAIUsage({
sessionId: body.sessionId,
featureName: '[Tools] plot_twist_generation',
promptName: 'plot_twist_creation_v1',
modelUsed: getRecommendedToolsModel(),
provider: 'openai',
tokensConsumed: generationResult.usage.total_tokens,
costUsd: generationResult.estimatedCost,
responseTimeMs: generationResult.processingTime,
requestSuccess: true,
metadata: {
toolType: 'plot_twists',
requestId,
genre: body.options.genre,
intensity: body.options.intensity
}
})
// Save to database
const supabase = await createClient()
const shareCode = await generateUniqueShareCode('plot-twist')
const { data: savedContent, error: saveError } = await supabase
.from('tools_plot_twists')
.insert({
title: generationResult.title,
share_code: shareCode,
session_id: body.sessionId,
content: generationResult.content,
options: body.options,
ai_generation_prompt: generationResult.promptUsed,
ai_tokens_used: generationResult.usage.total_tokens,
ai_cost_cents: Math.round(generationResult.estimatedCost * 100),
ai_model: generationResult.model,
ai_generation_time_ms: generationResult.processingTime,
ai_confidence_score: generationResult.confidenceScore,
seo_title: generationResult.seoMetadata.title,
seo_description: generationResult.seoMetadata.description,
keywords: generationResult.seoMetadata.keywords
})
.select()
.single()
if (saveError) {
throw new Error(`Save failed: ${saveError.message}`)
}
// Success response with CORS
const response = NextResponse.json({
success: true,
data: {
id: savedContent.id,
shareCode: savedContent.share_code,
title: savedContent.title,
content: savedContent.content,
seoMetadata: generationResult.seoMetadata,
aiMetadata: {
model: generationResult.model,
tokensUsed: generationResult.usage.total_tokens,
processingTime: generationResult.processingTime,
confidenceScore: generationResult.confidenceScore
}
},
meta: {
timestamp: new Date().toISOString(),
requestId,
processingTime: Date.now() - startTime,
rateLimit: {
limit: rateLimitResult.limit,
remaining: rateLimitResult.remaining - 1,
resetTime: rateLimitResult.resetTime
},
conversionOpportunity: false
}
})
// CORS headers
response.headers.set('Access-Control-Allow-Origin', process.env.WEB_APP_URL || 'https://mystoryflow.com')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Session-ID')
response.headers.set('Access-Control-Allow-Credentials', 'true')
return response
} catch (error) {
console.error('Plot twist generation error:', error)
return NextResponse.json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred. Please try again.',
timestamp: new Date().toISOString()
},
meta: {
timestamp: new Date().toISOString(),
requestId,
processingTime: Date.now() - startTime
}
}, { status: 500 })
}
}
export async function OPTIONS(request: NextRequest) {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': process.env.WEB_APP_URL || 'https://mystoryflow.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Session-ID',
'Access-Control-Allow-Credentials': 'true'
}
})
}Browse Endpoint
Create: src/app/api/plot-twists/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
export async function GET(request: NextRequest) {
const startTime = Date.now()
try {
const { searchParams } = new URL(request.url)
const page = Math.max(1, parseInt(searchParams.get('page') || '1'))
const limit = Math.min(100, Math.max(1, parseInt(searchParams.get('limit') || '20')))
const sort = searchParams.get('sort') || 'newest'
const search = searchParams.get('search')
const publicOnly = searchParams.get('public') === 'true'
const featured = searchParams.get('featured') === 'true'
const offset = (page - 1) * limit
const supabase = await createClient()
let query = supabase
.from('tools_plot_twists')
.select(`
id,
title,
share_code,
content,
seo_title,
seo_description,
keywords,
view_count,
share_count,
response_count,
average_rating,
created_at,
updated_at
`, { count: 'exact' })
// Apply filters
if (publicOnly) query = query.eq('is_public', true)
if (featured) query = query.eq('is_featured', true)
query = query.eq('is_archived', false)
// Search
if (search) {
query = query.or(`
title.ilike.%${search}%,
seo_description.ilike.%${search}%,
keywords.cs.{${search}}
`)
}
// Sort
switch (sort) {
case 'popular':
query = query.order('view_count', { ascending: false })
break
case 'rating':
query = query.order('average_rating', { ascending: false })
break
case 'featured':
query = query.order('is_featured', { ascending: false }).order('created_at', { ascending: false })
break
case 'newest':
default:
query = query.order('created_at', { ascending: false })
break
}
// Pagination
query = query.range(offset, offset + limit - 1)
const { data, error, count } = await query
if (error) {
throw new Error(`Query failed: ${error.message}`)
}
const totalPages = Math.ceil((count || 0) / limit)
return NextResponse.json({
success: true,
data: data || [],
meta: {
timestamp: new Date().toISOString(),
processingTime: Date.now() - startTime,
pagination: {
page,
limit,
total: count || 0,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
}
}
})
} catch (error) {
console.error('Plot twist browse error:', error)
return NextResponse.json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'Failed to browse content',
timestamp: new Date().toISOString()
}
}, { status: 500 })
}
}🎨 Step 4: UI Components (7 minutes)
Main Landing Component
Create: src/components/plot-twists/PlotTwistsLanding.tsx
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Button } from '@mystoryflow/ui/button'
import { Card } from '@mystoryflow/ui/card'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@mystoryflow/ui/select'
import { Textarea } from '@mystoryflow/ui/textarea'
import { useToast } from '@mystoryflow/ui/use-toast'
import { Loader2, Sparkles, Eye, Share2, Download } from 'lucide-react'
interface PlotTwistOptions {
genre: string
storyType: string
intensity: string
plotElements: string[]
customContext: string
}
interface PlotTwistResult {
id: string
shareCode: string
title: string
content: {
twists: Array<{
twist: string
explanation: string
implementation: string
impact: string
}>
analysis: {
overallAssessment: string
genreAlignment: string
suggestions: string[]
}
}
seoMetadata: any
aiMetadata: any
}
export function PlotTwistsLanding() {
const [options, setOptions] = useState<PlotTwistOptions>({
genre: '',
storyType: '',
intensity: '',
plotElements: [],
customContext: ''
})
const [result, setResult] = useState<PlotTwistResult | null>(null)
const [isGenerating, setIsGenerating] = useState(false)
const [sessionId, setSessionId] = useState('')
const { toast } = useToast()
useEffect(() => {
let currentSessionId = localStorage.getItem('mystoryflow_session_id')
if (!currentSessionId) {
currentSessionId = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
localStorage.setItem('mystoryflow_session_id', currentSessionId)
}
setSessionId(currentSessionId)
}, [])
const handleGenerate = useCallback(async () => {
if (!sessionId || !options.genre || !options.storyType || !options.intensity) return
setIsGenerating(true)
setResult(null)
try {
const response = await fetch('/api/plot-twists/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Session-ID': sessionId
},
body: JSON.stringify({
sessionId,
options
})
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Generation failed')
}
setResult(data.data)
toast({
title: "✨ Plot Twists Generated!",
description: "Your plot twists are ready to enhance your story."
})
} catch (error) {
toast({
title: "Generation Failed",
description: error.message,
variant: "destructive"
})
} finally {
setIsGenerating(false)
}
}, [sessionId, options, toast])
const updateOption = useCallback((key: keyof PlotTwistOptions, value: any) => {
setOptions(prev => ({ ...prev, [key]: value }))
}, [])
const isFormValid = () => {
return options.genre && options.storyType && options.intensity
}
return (
<div className="max-w-4xl mx-auto">
{/* Hero Section */}
<div className="text-center mb-12">
<div className="flex items-center justify-center gap-2 mb-4">
<Sparkles className="h-8 w-8 text-primary" />
<h1 className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-primary via-purple-500 to-pink-500 bg-clip-text text-transparent">
Plot Twist Generator
</h1>
</div>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto mb-8">
Generate unexpected, compelling plot twists that will surprise your readers and enhance your storytelling.
</p>
<div className="flex items-center justify-center gap-4 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Sparkles className="h-4 w-4" />
AI-powered twists
</span>
<span className="flex items-center gap-1">
<Share2 className="h-4 w-4" />
Easy sharing
</span>
<span className="flex items-center gap-1">
<Download className="h-4 w-4" />
Multiple formats
</span>
</div>
</div>
{/* Options Form */}
<Card className="p-6 mb-8">
<h2 className="text-xl font-semibold mb-4">Customize Your Plot Twists</h2>
<div className="grid gap-4">
<div className="grid md:grid-cols-3 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Genre</label>
<Select value={options.genre} onValueChange={(value) => updateOption('genre', value)}>
<SelectTrigger size="sm">
<SelectValue placeholder="Select genre" />
</SelectTrigger>
<SelectContent>
<SelectItem value="mystery">Mystery</SelectItem>
<SelectItem value="thriller">Thriller</SelectItem>
<SelectItem value="romance">Romance</SelectItem>
<SelectItem value="fantasy">Fantasy</SelectItem>
<SelectItem value="sci-fi">Science Fiction</SelectItem>
<SelectItem value="horror">Horror</SelectItem>
<SelectItem value="drama">Drama</SelectItem>
<SelectItem value="adventure">Adventure</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Story Type</label>
<Select value={options.storyType} onValueChange={(value) => updateOption('storyType', value)}>
<SelectTrigger size="sm">
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="short-story">Short Story</SelectItem>
<SelectItem value="novel">Novel</SelectItem>
<SelectItem value="novella">Novella</SelectItem>
<SelectItem value="screenplay">Screenplay</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Twist Intensity</label>
<Select value={options.intensity} onValueChange={(value) => updateOption('intensity', value)}>
<SelectTrigger size="sm">
<SelectValue placeholder="Select intensity" />
</SelectTrigger>
<SelectContent>
<SelectItem value="subtle">Subtle</SelectItem>
<SelectItem value="moderate">Moderate</SelectItem>
<SelectItem value="dramatic">Dramatic</SelectItem>
<SelectItem value="mind-blowing">Mind-blowing</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Additional Context (Optional)</label>
<Textarea
placeholder="Describe your story setup, characters, or specific elements you want to incorporate..."
value={options.customContext}
onChange={(e) => updateOption('customContext', e.target.value)}
rows={3}
/>
</div>
</div>
</Card>
{/* Generate Button */}
<div className="text-center mb-8">
<Button
size="lg"
onClick={handleGenerate}
disabled={isGenerating || !isFormValid()}
className="px-8 py-3 text-lg font-semibold"
>
{isGenerating ? (
<>
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
Generating Plot Twists...
</>
) : (
<>
<Sparkles className="mr-2 h-5 w-5" />
Generate Plot Twists
</>
)}
</Button>
</div>
{/* Results */}
{result && (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold">{result.title}</h2>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<Share2 className="h-4 w-4 mr-1" />
Share
</Button>
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-1" />
Export
</Button>
</div>
</div>
<div className="space-y-6">
{result.content.twists.map((twist, index) => (
<div key={index} className="border-l-4 border-primary pl-4 space-y-2">
<h3 className="font-semibold text-lg">Twist #{index + 1}</h3>
<p className="text-lg font-medium text-primary">{twist.twist}</p>
<div className="grid md:grid-cols-3 gap-4 text-sm">
<div>
<strong>Why it works:</strong>
<p className="text-muted-foreground">{twist.explanation}</p>
</div>
<div>
<strong>How to implement:</strong>
<p className="text-muted-foreground">{twist.implementation}</p>
</div>
<div>
<strong>Story impact:</strong>
<p className="text-muted-foreground">{twist.impact}</p>
</div>
</div>
</div>
))}
<div className="mt-8 p-4 bg-muted rounded-lg">
<h3 className="font-semibold mb-2">Analysis & Suggestions</h3>
<p className="text-sm text-muted-foreground mb-2">{result.content.analysis.overallAssessment}</p>
<p className="text-sm text-muted-foreground mb-3"><strong>Genre Alignment:</strong> {result.content.analysis.genreAlignment}</p>
<div>
<strong className="text-sm">Additional Suggestions:</strong>
<ul className="list-disc list-inside text-sm text-muted-foreground mt-1">
{result.content.analysis.suggestions.map((suggestion, index) => (
<li key={index}>{suggestion}</li>
))}
</ul>
</div>
</div>
</div>
</Card>
)}
</div>
)
}Page Component
Create: src/app/plot-twists/page.tsx
import { PlotTwistsLanding } from '@/components/plot-twists/PlotTwistsLanding'
export default function PlotTwistsPage() {
return (
<div className="min-h-screen bg-background">
<main className="container mx-auto px-4 py-8">
<PlotTwistsLanding />
</main>
</div>
)
}✅ Final Steps & Testing
Test Your Tool
# 1. Start development server
npm run dev
# 2. Navigate to your tool
open http://localhost:3000/plot-twists
# 3. Test the complete flow:
# - Fill out the form
# - Generate plot twists
# - Verify database storage
# - Test sharing (optional)Add to Navigation (if needed)
Update your main navigation to include the new tool.
Deploy
# Push to your repository
git add .
git commit -m "Add plot twist generator tool"
git push origin main
# Deploy will happen automatically via Vercel/your deployment system🎉 Congratulations!
You’ve successfully created a complete, production-ready tool in 30 minutes! Your Plot Twist Generator includes:
- ✅ Complete database schema with RLS
- ✅ AI-powered content generation
- ✅ Rate limiting and security
- ✅ Admin usage tracking
- ✅ SEO optimization
- ✅ User-friendly interface
- ✅ Response system ready
- ✅ Export functionality ready
- ✅ Sharing system ready
🚀 Next Steps
- Add Browse Functionality: Implement the browse component
- Add Writer Component: Enable user responses
- Add Export Options: Implement multi-format exports
- Add Sharing: Implement public sharing pages
- SEO Optimization: Add sitemap entries and meta tags
Your tool now follows all MyStoryFlow standards and can be extended with additional features as needed!