Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories
Technical DocumentationTools ImplementationQuick Start Guide: Creating Your First Tool

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.local

Required 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

  1. Add Browse Functionality: Implement the browse component
  2. Add Writer Component: Enable user responses
  3. Add Export Options: Implement multi-format exports
  4. Add Sharing: Implement public sharing pages
  5. SEO Optimization: Add sitemap entries and meta tags

Your tool now follows all MyStoryFlow standards and can be extended with additional features as needed!