Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories
Technical DocumentationTools ImplementationUser Response System Implementation

User Response System Implementation

✍️ Complete Write → Save → Share Workflow

MyStoryFlow’s user response system enables users to create, save, and share their own content based on AI-generated prompts. This system follows the exact patterns established in the story-prompts implementation and provides a complete user-generated content workflow.

🏗️ System Architecture Overview

Response System Components

User Response Flow: AI Content → User Inspiration → Response Creation → Auto-Save → Manual Save → Public Sharing → Community Discovery Technical Components: ├── Response Database Tables ├── Writer Component (Rich text editor) ├── Auto-save System (Background persistence) ├── Manual Save (User-triggered with feedback) ├── Public/Private Toggle ├── Response Analytics ├── Moderation Queue ├── SEO-Optimized Public Pages └── Community Discovery

Database Architecture

The response system extends the main tool tables with dedicated response tables following this exact pattern:

-- =========================================== -- USER RESPONSES TABLE (UNIVERSAL PATTERN) -- =========================================== CREATE TABLE IF NOT EXISTS tools_{tool_name}_responses ( -- Identity & Relationship id UUID PRIMARY KEY DEFAULT gen_random_uuid(), {tool_name}_id UUID REFERENCES tools_{tool_name}(id) ON DELETE CASCADE, -- Ownership (Session-based + User-based) user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, session_id TEXT NOT NULL, -- Content Data title TEXT NOT NULL, content TEXT NOT NULL, content_type TEXT DEFAULT 'text/plain', -- Automated Metrics 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, -- Visibility & Moderation 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), -- Engagement Analytics view_count INTEGER DEFAULT 0, like_count INTEGER DEFAULT 0, share_count INTEGER DEFAULT 0, flag_count INTEGER DEFAULT 0, -- Quality Scoring 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 for Public Responses seo_title TEXT, seo_description TEXT, slug TEXT UNIQUE, -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), published_at TIMESTAMPTZ, -- When made public last_edited_at TIMESTAMPTZ DEFAULT NOW(), -- Constraints 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) ) );

🎨 Writer Component Implementation

Universal Writer Component Pattern

'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { Button } from '@mystoryflow/ui/button' import { Card } from '@mystoryflow/ui/card' import { Input } from '@mystoryflow/ui/input' import { Textarea } from '@mystoryflow/ui/textarea' import { Badge } from '@mystoryflow/ui/badge' import { useToast } from '@mystoryflow/ui/use-toast' import { Save, Share2, Download, Eye, EyeOff, Clock, CheckCircle, AlertCircle, Loader2 } from 'lucide-react' // =========================================== // INTERFACES (UNIVERSAL) // =========================================== interface WriterProps { parentId: string // ID of the AI-generated content toolName: string // Tool identifier (e.g., 'plot-twists') sessionId: string // User session ID initialData?: ResponseData // For editing existing responses onResponseCreated?: (response: ResponseData) => void onResponseUpdated?: (response: ResponseData) => void } interface ResponseData { id?: string title: string content: string wordCount: number characterCount: number isPublic: boolean createdAt?: string updatedAt?: string lastSavedAt?: string } interface WriterState extends ResponseData { isSaving: boolean lastSaved: Date | null hasUnsavedChanges: boolean saveStatus: 'idle' | 'saving' | 'saved' | 'error' autoSaveEnabled: boolean } // =========================================== // WRITER COMPONENT (UNIVERSAL PATTERN) // =========================================== export function UniversalWriter({ parentId, toolName, sessionId, initialData, onResponseCreated, onResponseUpdated }: WriterProps) { // =========================================== // STATE MANAGEMENT // =========================================== const [state, setState] = useState<WriterState>({ title: initialData?.title || '', content: initialData?.content || '', wordCount: 0, characterCount: 0, isPublic: initialData?.isPublic || false, isSaving: false, lastSaved: null, hasUnsavedChanges: false, saveStatus: 'idle', autoSaveEnabled: true }) const autoSaveTimeoutRef = useRef<NodeJS.Timeout>() const { toast } = useToast() // =========================================== // AUTO-SAVE FUNCTIONALITY // =========================================== const saveContent = useCallback(async (showToast = false, isAutoSave = false) => { if (!state.title.trim() && !state.content.trim()) { if (showToast) { toast({ title: "Nothing to Save", description: "Please add a title and content before saving.", variant: "destructive" }) } return null } if (!state.title.trim() || !state.content.trim()) { if (showToast) { toast({ title: "Incomplete Content", description: "Both title and content are required.", variant: "destructive" }) } return null } setState(prev => ({ ...prev, isSaving: true, saveStatus: 'saving' })) try { const endpoint = initialData?.id ? `/api/${toolName}/responses/${initialData.id}` : `/api/${toolName}/${parentId}/responses` const method = initialData?.id ? 'PATCH' : 'POST' const response = await fetch(endpoint, { method, headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId }, body: JSON.stringify({ sessionId, title: state.title.trim(), content: state.content.trim(), isPublic: state.isPublic }) }) const data = await response.json() if (!data.success) { throw new Error(data.error?.message || 'Failed to save response') } const now = new Date() setState(prev => ({ ...prev, lastSaved: now, hasUnsavedChanges: false, saveStatus: 'saved' })) // Call appropriate callback if (initialData?.id) { onResponseUpdated?.(data.data) } else { onResponseCreated?.(data.data) } if (showToast) { toast({ title: "✅ Response Saved!", description: isAutoSave ? "Your response has been auto-saved." : "Your response has been saved successfully." }) } // Reset save status after 2 seconds setTimeout(() => { setState(prev => ({ ...prev, saveStatus: 'idle' })) }, 2000) return data.data } catch (error) { console.error('Save error:', error) setState(prev => ({ ...prev, saveStatus: 'error' })) if (showToast) { toast({ title: "Save Failed", description: error.message || "Please try again in a moment.", variant: "destructive" }) } return null } finally { setState(prev => ({ ...prev, isSaving: false })) } }, [state.title, state.content, state.isPublic, parentId, toolName, sessionId, initialData?.id, onResponseCreated, onResponseUpdated, toast]) // =========================================== // AUTO-SAVE TIMER MANAGEMENT // =========================================== useEffect(() => { if (!state.autoSaveEnabled || !state.hasUnsavedChanges) return // Clear existing timeout if (autoSaveTimeoutRef.current) { clearTimeout(autoSaveTimeoutRef.current) } // Set new auto-save timeout (30 seconds) autoSaveTimeoutRef.current = setTimeout(() => { if (state.hasUnsavedChanges && (state.title.trim() || state.content.trim())) { saveContent(false, true) // Auto-save without toast } }, 30000) return () => { if (autoSaveTimeoutRef.current) { clearTimeout(autoSaveTimeoutRef.current) } } }, [state.hasUnsavedChanges, state.autoSaveEnabled, saveContent]) // =========================================== // KEYBOARD SHORTCUTS // =========================================== useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Cmd/Ctrl + S for manual save if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault() saveContent(true) // Manual save with toast } // Escape to toggle auto-save if (e.key === 'Escape' && (e.metaKey || e.ctrlKey)) { setState(prev => ({ ...prev, autoSaveEnabled: !prev.autoSaveEnabled })) toast({ title: state.autoSaveEnabled ? "Auto-save Disabled" : "Auto-save Enabled", description: state.autoSaveEnabled ? "Your changes will not be saved automatically." : "Your changes will be saved every 30 seconds." }) } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [saveContent, state.autoSaveEnabled, toast]) // =========================================== // CONTENT METRICS UPDATE // =========================================== useEffect(() => { const content = state.content.trim() const words = content ? content.split(/\s+/).filter(word => word.length > 0) : [] setState(prev => ({ ...prev, wordCount: words.length, characterCount: content.length })) }, [state.content]) // =========================================== // EVENT HANDLERS // =========================================== const updateField = useCallback((field: keyof WriterState, value: any) => { setState(prev => ({ ...prev, [field]: value, hasUnsavedChanges: true, lastEditedAt: new Date() })) }, []) const handleManualSave = useCallback(() => { saveContent(true) // Manual save with toast }, [saveContent]) const handlePublicToggle = useCallback(() => { setState(prev => ({ ...prev, isPublic: !prev.isPublic, hasUnsavedChanges: true })) }, []) const handleShare = useCallback(async () => { // First save if there are unsaved changes const savedResponse = state.hasUnsavedChanges ? await saveContent(false) : null // Implement sharing logic if (savedResponse || !state.hasUnsavedChanges) { toast({ title: "Share Feature", description: "Sharing functionality will be implemented here." }) } }, [saveContent, state.hasUnsavedChanges, toast]) const handleExport = useCallback(async () => { // First save if there are unsaved changes const savedResponse = state.hasUnsavedChanges ? await saveContent(false) : null // Implement export logic if (savedResponse || !state.hasUnsavedChanges) { toast({ title: "Export Feature", description: "Export functionality will be implemented here." }) } }, [saveContent, state.hasUnsavedChanges, toast]) // =========================================== // RENDER STATUS INDICATOR // =========================================== const renderStatusIndicator = () => { const getStatusIcon = () => { switch (state.saveStatus) { case 'saving': return <Loader2 className="h-3 w-3 animate-spin" /> case 'saved': return <CheckCircle className="h-3 w-3 text-green-500" /> case 'error': return <AlertCircle className="h-3 w-3 text-red-500" /> default: return <Clock className="h-3 w-3" /> } } const getStatusText = () => { switch (state.saveStatus) { case 'saving': return 'Saving...' case 'saved': return 'Saved' case 'error': return 'Save failed' default: return state.lastSaved ? `Saved ${formatTimeAgo(state.lastSaved)}` : 'Not saved' } } return ( <div className="flex items-center gap-2 text-sm text-muted-foreground"> {getStatusIcon()} <span>{getStatusText()}</span> {state.hasUnsavedChanges && ( <Badge variant="outline" className="text-xs"> Unsaved changes </Badge> )} </div> ) } // =========================================== // MAIN RENDER // =========================================== return ( <Card className="p-6"> {/* Header */} <div className="flex items-center justify-between mb-4"> <h3 className="text-lg font-semibold"> {initialData?.id ? 'Edit Your Response' : 'Write Your Response'} </h3> <div className="flex items-center gap-3"> {renderStatusIndicator()} <Badge variant={state.autoSaveEnabled ? "secondary" : "outline"} className="text-xs cursor-pointer" onClick={() => updateField('autoSaveEnabled', !state.autoSaveEnabled)} > Auto-save {state.autoSaveEnabled ? 'ON' : 'OFF'} </Badge> </div> </div> <div className="space-y-4"> {/* Title Input */} <div> <Input size="sm" placeholder="Give your response a compelling title..." value={state.title} onChange={(e) => updateField('title', e.target.value)} className="text-lg font-medium" disabled={state.isSaving} /> </div> {/* Content Textarea */} <div> <Textarea placeholder="Share your thoughts, story, creative response, or ideas here. Let your creativity flow..." value={state.content} onChange={(e) => updateField('content', e.target.value)} rows={12} className="resize-none" disabled={state.isSaving} /> {/* Content Metrics */} <div className="flex items-center justify-between mt-2 text-sm text-muted-foreground"> <div className="flex items-center gap-4"> <span>{state.wordCount} words</span> <span>{state.characterCount} characters</span> </div> <span className="text-xs"> 💡 Tip: Press Cmd/Ctrl + S to save manually </span> </div> </div> {/* Action Bar */} <div className="flex items-center justify-between pt-4 border-t"> {/* Left side - Visibility toggle */} <div className="flex items-center gap-2"> <Button variant="outline" size="sm" onClick={handlePublicToggle} disabled={state.isSaving} className="flex items-center gap-1" > {state.isPublic ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />} {state.isPublic ? 'Public' : 'Private'} </Button> {state.isPublic && ( <Badge variant="outline" className="text-xs"> Will be visible to others </Badge> )} </div> {/* Right side - Action buttons */} <div className="flex items-center gap-2"> <Button variant="outline" size="sm" onClick={handleManualSave} disabled={ state.isSaving || (!state.title.trim() && !state.content.trim()) || !state.title.trim() || !state.content.trim() } className="flex items-center gap-1" > {state.isSaving ? ( <Loader2 className="h-4 w-4 animate-spin" /> ) : ( <Save className="h-4 w-4" /> )} {state.isSaving ? 'Saving...' : 'Save'} </Button> {state.lastSaved && ( <> <Button variant="outline" size="sm" onClick={handleShare} disabled={state.isSaving} className="flex items-center gap-1" > <Share2 className="h-4 w-4" /> Share </Button> <Button variant="outline" size="sm" onClick={handleExport} disabled={state.isSaving} className="flex items-center gap-1" > <Download className="h-4 w-4" /> Export </Button> </> )} </div> </div> {/* Help Text */} <div className="text-xs text-muted-foreground bg-muted/50 rounded p-3"> <strong>Writing Tips:</strong> <ul className="list-disc list-inside mt-1 space-y-1"> <li>Auto-save happens every 30 seconds when enabled</li> <li>Use Cmd/Ctrl + S to save manually anytime</li> <li>Toggle visibility to share with the community</li> <li>Private responses are only visible to you</li> </ul> </div> </div> </Card> ) } // =========================================== // HELPER FUNCTIONS // =========================================== function formatTimeAgo(date: Date): string { const now = new Date() const diff = now.getTime() - date.getTime() const minutes = Math.floor(diff / 60000) if (minutes < 1) return 'just now' if (minutes < 60) return `${minutes}m ago` const hours = Math.floor(minutes / 60) if (hours < 24) return `${hours}h ago` const days = Math.floor(hours / 24) return `${days}d ago` }

📡 API Implementation Patterns

Create Response Endpoint

// /api/{tool}/[id]/responses/route.ts import { NextRequest, NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' import { checkRateLimit } from '@/lib/rate-limiter' interface CreateResponseBody { sessionId: string title: string content: string isPublic?: boolean } export async function POST( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const startTime = Date.now() const requestId = `resp_create_${Date.now()}` const { id } = await params try { const body: CreateResponseBody = await request.json() // Validation if (!body.sessionId?.trim() || !body.title?.trim() || !body.content?.trim()) { return NextResponse.json({ success: false, error: { code: 'INVALID_INPUT', message: 'Session ID, title, and content are all required', timestamp: new Date().toISOString() } }, { status: 400 }) } // Content length validation if (body.title.length > 200) { return NextResponse.json({ success: false, error: { code: 'TITLE_TOO_LONG', message: 'Title must be 200 characters or less', field: 'title', timestamp: new Date().toISOString() } }, { status: 400 }) } if (body.content.length > 50000) { return NextResponse.json({ success: false, error: { code: 'CONTENT_TOO_LONG', message: 'Content must be 50,000 characters or less', field: 'content', timestamp: new Date().toISOString() } }, { status: 400 }) } // Rate limiting const rateLimitResult = await checkRateLimit(request, '{tool}_response_creation') if (!rateLimitResult.allowed) { return NextResponse.json({ success: false, error: { code: 'RATE_LIMIT_EXCEEDED', message: 'Too many responses created. Please wait before adding another.', timestamp: new Date().toISOString() }, meta: { rateLimit: { limit: rateLimitResult.limit, remaining: rateLimitResult.remaining, resetTime: rateLimitResult.resetTime, retryAfter: rateLimitResult.retryAfter } } }, { status: 429 }) } const supabase = await createClient() // Verify parent content exists and user has access const { data: parentContent, error: parentError } = await supabase .from('tools_{tool}') .select('id, title, session_id, user_id') .eq('id', id) .single() if (parentError || !parentContent) { return NextResponse.json({ success: false, error: { code: 'PARENT_NOT_FOUND', message: 'The original content was not found or you do not have access to it', timestamp: new Date().toISOString() } }, { status: 404 }) } // Create response with moderation status const { data: response, error: createError } = await supabase .from('tools_{tool}_responses') .insert({ {tool}_id: id, session_id: body.sessionId, title: body.title.trim(), content: body.content.trim(), is_public: body.isPublic || false, moderation_status: body.isPublic ? 'pending' : 'approved', // Public content needs moderation published_at: body.isPublic ? new Date().toISOString() : null }) .select(` id, title, content, word_count, character_count, is_public, moderation_status, created_at, updated_at `) .single() if (createError) { console.error('Response creation error:', createError) return NextResponse.json({ success: false, error: { code: 'CREATE_FAILED', message: 'Failed to save your response. Please try again.', timestamp: new Date().toISOString() } }, { status: 500 }) } // Update parent content response count await supabase .from('tools_{tool}') .update({ response_count: parentContent.response_count + 1, updated_at: new Date().toISOString() }) .eq('id', id) return NextResponse.json({ success: true, data: { id: response.id, title: response.title, content: response.content, wordCount: response.word_count, characterCount: response.character_count, isPublic: response.is_public, moderationStatus: response.moderation_status, createdAt: response.created_at, updatedAt: response.updated_at }, meta: { timestamp: new Date().toISOString(), requestId, processingTime: Date.now() - startTime } }) } catch (error) { console.error(`{tool} response creation error:`, error) return NextResponse.json({ success: false, error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred while saving your response', timestamp: new Date().toISOString() }, meta: { requestId, processingTime: Date.now() - startTime } }, { status: 500 }) } } // Get responses for a piece of content export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params try { const { searchParams } = new URL(request.url) const page = Math.max(1, parseInt(searchParams.get('page') || '1')) const limit = Math.min(50, Math.max(1, parseInt(searchParams.get('limit') || '10'))) const publicOnly = searchParams.get('public') === 'true' const sessionId = searchParams.get('sessionId') const offset = (page - 1) * limit const supabase = await createClient() let query = supabase .from('tools_{tool}_responses') .select(` id, title, content, word_count, character_count, is_public, view_count, like_count, share_count, quality_score, created_at, updated_at `, { count: 'exact' }) .eq('{tool}_id', id) // Apply visibility filters if (publicOnly) { query = query.eq('is_public', true).eq('moderation_status', 'approved') } else if (sessionId) { // Show user's own responses (public and private) plus public approved responses query = query.or(`session_id.eq.${sessionId},and(is_public.eq.true,moderation_status.eq.approved)`) } else { // Default: only public approved responses query = query.eq('is_public', true).eq('moderation_status', 'approved') } // Sort by creation date (newest first) query = query.order('created_at', { ascending: false }) // Apply pagination query = query.range(offset, offset + limit - 1) const { data: responses, error, count } = await query if (error) { throw new Error(`Failed to fetch responses: ${error.message}`) } const totalPages = Math.ceil((count || 0) / limit) return NextResponse.json({ success: true, data: responses || [], meta: { timestamp: new Date().toISOString(), pagination: { page, limit, total: count || 0, totalPages, hasNext: page < totalPages, hasPrev: page > 1 } } }) } catch (error) { console.error(`{tool} responses fetch error:`, error) return NextResponse.json({ success: false, error: { code: 'FETCH_FAILED', message: 'Failed to load responses', timestamp: new Date().toISOString() } }, { status: 500 }) } }

Update Response Endpoint

// /api/{tool}/responses/[responseId]/route.ts export async function PATCH( request: NextRequest, { params }: { params: Promise<{ responseId: string }> } ) { const startTime = Date.now() const { responseId } = await params try { const body = await request.json() // Validation if (!body.sessionId) { return NextResponse.json({ success: false, error: { code: 'MISSING_SESSION_ID', message: 'Session ID is required', timestamp: new Date().toISOString() } }, { status: 400 }) } const supabase = await createClient() // Verify ownership and get current response const { data: currentResponse, error: fetchError } = await supabase .from('tools_{tool}_responses') .select('*') .eq('id', responseId) .single() if (fetchError || !currentResponse) { return NextResponse.json({ success: false, error: { code: 'RESPONSE_NOT_FOUND', message: 'Response not found or access denied', timestamp: new Date().toISOString() } }, { status: 404 }) } // Check ownership if (currentResponse.session_id !== body.sessionId) { return NextResponse.json({ success: false, error: { code: 'ACCESS_DENIED', message: 'You can only edit your own responses', timestamp: new Date().toISOString() } }, { status: 403 }) } // Build update object const updates: any = { updated_at: new Date().toISOString() } if (body.title !== undefined) { if (!body.title.trim() || body.title.length > 200) { return NextResponse.json({ success: false, error: { code: 'INVALID_TITLE', message: 'Title must be between 1 and 200 characters', timestamp: new Date().toISOString() } }, { status: 400 }) } updates.title = body.title.trim() } if (body.content !== undefined) { if (!body.content.trim() || body.content.length > 50000) { return NextResponse.json({ success: false, error: { code: 'INVALID_CONTENT', message: 'Content must be between 1 and 50,000 characters', timestamp: new Date().toISOString() } }, { status: 400 }) } updates.content = body.content.trim() } if (body.isPublic !== undefined) { updates.is_public = body.isPublic // If making public, set moderation status and published date if (body.isPublic && !currentResponse.is_public) { updates.moderation_status = 'pending' updates.published_at = new Date().toISOString() } // If making private, clear published date if (!body.isPublic && currentResponse.is_public) { updates.published_at = null } } // Update response const { data: updatedResponse, error: updateError } = await supabase .from('tools_{tool}_responses') .update(updates) .eq('id', responseId) .select(` id, title, content, word_count, character_count, is_public, moderation_status, created_at, updated_at `) .single() if (updateError) { throw new Error(`Update failed: ${updateError.message}`) } return NextResponse.json({ success: true, data: { id: updatedResponse.id, title: updatedResponse.title, content: updatedResponse.content, wordCount: updatedResponse.word_count, characterCount: updatedResponse.character_count, isPublic: updatedResponse.is_public, moderationStatus: updatedResponse.moderation_status, createdAt: updatedResponse.created_at, updatedAt: updatedResponse.updated_at }, meta: { timestamp: new Date().toISOString(), processingTime: Date.now() - startTime } }) } catch (error) { console.error(`{tool} response update error:`, error) return NextResponse.json({ success: false, error: { code: 'UPDATE_FAILED', message: 'Failed to update response', timestamp: new Date().toISOString() } }, { status: 500 }) } }

🌟 Integration with Main Tool Component

Adding Writer to Tool Landing

// In your main tool landing component import { UniversalWriter } from '@/components/shared/UniversalWriter' export function ToolLanding() { const [result, setResult] = useState<ToolResult | null>(null) const [responses, setResponses] = useState<ResponseData[]>([]) const handleResponseCreated = useCallback((newResponse: ResponseData) => { setResponses(prev => [newResponse, ...prev]) toast({ title: "Response Created!", description: "Your response has been saved and can be shared with others." }) }, [toast]) const handleResponseUpdated = useCallback((updatedResponse: ResponseData) => { setResponses(prev => prev.map(response => response.id === updatedResponse.id ? updatedResponse : response ) ) toast({ title: "Response Updated!", description: "Your changes have been saved." }) }, [toast]) return ( <div className="max-w-4xl mx-auto"> {/* Tool generation UI */} {/* Results display */} {result && ( <div className="space-y-6"> {/* Generated content display */} <Card className="p-6"> {/* Tool-specific content display */} </Card> {/* Writer component */} <UniversalWriter parentId={result.id} toolName="your-tool-name" sessionId={sessionId} onResponseCreated={handleResponseCreated} onResponseUpdated={handleResponseUpdated} /> {/* Community responses */} {responses.length > 0 && ( <div className="space-y-4"> <h3 className="text-xl font-semibold">Community Responses</h3> {responses.map(response => ( <ResponseCard key={response.id} response={response} /> ))} </div> )} </div> )} </div> ) }

🎯 Best Practices & Guidelines

Content Moderation

  1. Auto-approve private content - No moderation needed
  2. Queue public content - Require approval before visibility
  3. Implement flagging system - Community reporting
  4. Quality scoring - Automated quality assessment

Performance Optimization

  1. Debounced auto-save - Prevent excessive API calls
  2. Optimistic updates - Immediate UI feedback
  3. Lazy loading - Load responses on demand
  4. Caching strategy - Cache frequently accessed responses

User Experience

  1. Clear save status - Visual feedback on save state
  2. Keyboard shortcuts - Power user efficiency
  3. Auto-save toggle - User control over auto-save
  4. Unsaved changes warning - Prevent data loss

This comprehensive user response system provides a complete workflow for user-generated content, maintaining consistency with MyStoryFlow’s quality standards while enabling rich community interaction.