Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories

F018: Interactive Analysis Results UI (MyStoryFlow Integration)

Overview

Build an engaging, interactive interface for displaying manuscript analysis results within the MyStoryFlow Manuscript Analyzer app.

Dependencies

  • F000: MyStoryFlow Monorepo Structure
  • F000B: Shared Packages (@mystoryflow/*)
  • F009: Analysis Framework (data structure)
  • F010-F013: AI Analysis Methods

Quick Implementation

Using MyStoryFlow Components

  • Tabs for organizing analysis sections
  • Cards for metric displays with literary theme
  • Progress bars for scores (amber/gold colors)
  • Accordion for expandable details
  • Charts for data visualization
  • Tooltips for contextual help

New Requirements

  • Interactive chart library (@mystoryflow/charts)
  • Real-time data loading
  • Smooth animations
  • Mobile-optimized layouts

MVP Implementation

1. Analysis Results Page Structure

// apps/analyzer-app/src/app/manuscripts/[id]/analysis/page.tsx import { Suspense } from 'react' import { getSupabaseBrowserClient } from '@mystoryflow/supabase' import { AnalysisHeader, ScoreOverview, CategoryAnalysis, StrengthsWeaknesses, ImprovementSuggestions, MarketInsights } from '@/components/analysis' import { DashboardLayout } from '@mystoryflow/ui/layouts' export default async function AnalysisPage({ params }: { params: { id: string } }) { const supabase = getSupabaseBrowserClient() // Get latest analysis const { data: analysis } = await supabase .from('manuscript_analyses') .select(` *, manuscripts ( title, word_count, genre_detections ( primary_genre, primary_confidence ) ) `) .eq('manuscript_id', params.id) .order('created_at', { ascending: false }) .limit(1) .single() if (!analysis) { return <NoAnalysisFound manuscriptId={params.id} /> } return ( <DashboardLayout> <div className="container max-w-7xl py-8"> <AnalysisHeader manuscript={analysis.manuscripts} analysisDate={analysis.created_at} /> <div className="mt-8 space-y-8"> <Suspense fallback={<ScoreLoading />}> <ScoreOverview overallScore={analysis.overall_score} categoryScores={analysis.category_scores} publishingReadiness={analysis.publishing_readiness} /> </Suspense> <Suspense fallback={<CategoryLoading />}> <CategoryAnalysis categories={analysis.detailed_feedback} manuscriptId={params.id} /> </Suspense> <StrengthsWeaknesses strengths={analysis.strengths} weaknesses={analysis.weaknesses} /> <ImprovementSuggestions suggestions={analysis.improvement_suggestions} /> <MarketInsights insights={analysis.market_insights} genre={analysis.manuscripts.genre_detections[0]} /> </div> </div> </DashboardLayout> ) }

2. Score Overview Component

// apps/analyzer-app/src/components/analysis/ScoreOverview.tsx 'use client' import { Card, Progress, Badge, Typography } from '@mystoryflow/ui' import { RadialChart } from '@mystoryflow/charts' import { motion } from 'framer-motion' import { TrendingUp, TrendingDown, Minus, Book, Quill } from 'lucide-react' interface ScoreOverviewProps { overallScore: number categoryScores: Record<string, number> publishingReadiness: { level: string description: string timeToMarket: string } } export function ScoreOverview({ overallScore, categoryScores, publishingReadiness }: ScoreOverviewProps) { const getScoreColor = (score: number) => { if (score >= 85) return '#92400e' // dark amber if (score >= 70) return '#d97706' // amber return '#dc2626' // red } const getScoreLabel = (score: number) => { if (score >= 85) return 'Excellent' if (score >= 70) return 'Good' if (score >= 50) return 'Fair' return 'Needs Work' } return ( <div className="grid gap-6 md:grid-cols-3"> {/* Overall Score Card */} <Card className="md:col-span-2 p-8 bg-gradient-to-br from-amber-50 to-orange-50 border-amber-200"> <div className="flex items-center justify-between"> <div className="space-y-4"> <div className="flex items-center gap-3"> <Book className="w-6 h-6 text-amber-700" /> <Typography variant="h2" className="text-amber-900">Overall Score</Typography> </div> <motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ duration: 0.5 }} className="flex items-baseline gap-4" > <span className="text-6xl font-bold" style={{ color: getScoreColor(overallScore) }}> {overallScore}% </span> <Badge variant={overallScore >= 70 ? 'success' : 'warning'} size="lg" > {getScoreLabel(overallScore)} </Badge> </motion.div> <Progress value={overallScore} className="h-4" indicatorClassName="bg-gradient-to-r from-amber-500 to-orange-500" /> <Typography variant="body" className="text-amber-700"> Your manuscript scores better than{' '} <strong className="text-amber-900">{Math.round(overallScore * 0.85)}%</strong>{' '} of manuscripts in this genre </Typography> </div> <div className="hidden md:block"> <RadialChart data={[{ value: overallScore, fill: getScoreColor(overallScore) }]} className="h-48 w-48" /> </div> </div> </Card> {/* Publishing Readiness Card */} <Card className="p-6 bg-gradient-to-br from-amber-50/50 to-orange-50/50 border-amber-200"> <div className="flex items-center gap-2 mb-4"> <Quill className="w-5 h-5 text-amber-700" /> <Typography variant="h3" className="text-amber-900">Publishing Readiness</Typography> </div> <div className="space-y-3"> <Badge variant={ publishingReadiness.level === 'ready' ? 'success' : publishingReadiness.level === 'needs_revision' ? 'warning' : 'destructive' } className="w-full justify-center py-2" > {publishingReadiness.level.replace('_', ' ').toUpperCase()} </Badge> <Typography variant="body" className="text-amber-700"> {publishingReadiness.description} </Typography> <div className="pt-2 border-t border-amber-200"> <Typography variant="caption" className="text-amber-600"> Estimated time to market: </Typography> <Typography variant="body" className="font-medium text-amber-900"> {publishingReadiness.timeToMarket} </Typography> </div> </div> </Card> {/* Category Scores Grid */} <Card className="md:col-span-3 p-6 bg-white/80 backdrop-blur-sm border-amber-100"> <Typography variant="h3" className="text-amber-900 mb-6">Category Breakdown</Typography> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> {Object.entries(categoryScores).map(([category, score]) => { const prevScore = 75 // TODO: Get from previous analysis const trend = score > prevScore ? 'up' : score < prevScore ? 'down' : 'stable' return ( <motion.div key={category} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }} className="space-y-2" > <div className="flex items-center justify-between"> <Typography variant="body" className="font-medium text-amber-800"> {category.replace(/_/g, ' ').toUpperCase()} </Typography> <div className="flex items-center gap-2"> <span className="font-bold text-amber-900">{score}%</span> {trend === 'up' && <TrendingUp className="h-4 w-4 text-green-600" />} {trend === 'down' && <TrendingDown className="h-4 w-4 text-red-600" />} {trend === 'stable' && <Minus className="h-4 w-4 text-amber-400" />} </div> </div> <Progress value={score} className="h-2" indicatorClassName={`bg-gradient-to-r ${ score >= 85 ? 'from-amber-700 to-amber-800' : score >= 70 ? 'from-amber-500 to-amber-600' : 'from-red-500 to-red-600' }`} /> </motion.div> ) })} </div> </Card> </div> ) }

3. Category Analysis Component

// apps/analyzer-app/src/components/analysis/CategoryAnalysis.tsx 'use client' import { useState } from 'react' import { Card, Tabs, TabsContent, TabsList, TabsTrigger, Badge, Typography } from '@mystoryflow/ui' import { ChevronRight, AlertCircle, CheckCircle, Info } from 'lucide-react' import { motion, AnimatePresence } from 'framer-motion' interface CategoryAnalysisProps { categories: Record<string, { score: number feedback: string strengths: string[] weaknesses: string[] examples: { good: string[] needsWork: string[] } suggestions: string[] }> manuscriptId: string } export function CategoryAnalysis({ categories, manuscriptId }: CategoryAnalysisProps) { const [expandedCategory, setExpandedCategory] = useState<string | null>(null) const categoryIcons = { structure: '🏗️', character_development: '👥', dialogue: '💬', pacing: '⚡', plot_and_conflict: '📊', writing_craft: '✍️', world_building: '🌍', theme_and_meaning: '💡' } return ( <Card className="p-6 bg-gradient-to-br from-amber-50/50 to-orange-50/50 border-amber-200"> <Typography variant="h2" className="text-amber-900 mb-6">Detailed Analysis</Typography> <Tabs defaultValue={Object.keys(categories)[0]} className="space-y-4"> <TabsList className="grid grid-cols-4 lg:grid-cols-8 gap-2 bg-amber-100/50 border border-amber-200"> {Object.entries(categories).map(([key, data]) => ( <TabsTrigger key={key} value={key} className="flex flex-col items-center gap-1 text-xs data-[state=active]:bg-white" > <span className="text-lg">{categoryIcons[key] || '📝'}</span> <span className="hidden lg:inline"> {key.replace(/_/g, ' ')} </span> <Badge variant={data.score >= 70 ? 'success' : 'warning'} size="sm"> {data.score}% </Badge> </TabsTrigger> ))} </TabsList> {Object.entries(categories).map(([key, data]) => ( <TabsContent key={key} value={key} className="space-y-6"> {/* Score and Summary */} <div className="flex items-start justify-between"> <div className="space-y-2"> <Typography variant="h3" className="text-amber-900 capitalize"> {key.replace(/_/g, ' ')} Analysis </Typography> <Typography variant="body" className="text-amber-700 max-w-2xl"> {data.feedback} </Typography> </div> <div className="text-center"> <div className="text-3xl font-bold" style={{ color: data.score >= 70 ? '#92400e' : '#d97706' }}> {data.score}% </div> <Typography variant="caption" className="text-amber-600">Score</Typography> </div> </div> {/* Strengths and Weaknesses */} <div className="grid md:grid-cols-2 gap-6"> <div className="space-y-3"> <Typography variant="h4" className="font-medium flex items-center gap-2 text-amber-900"> <CheckCircle className="h-5 w-5 text-green-600" /> Strengths </Typography> <ul className="space-y-2"> {data.strengths.map((strength, i) => ( <li key={i} className="flex items-start gap-2"> <span className="text-green-600 mt-0.5">•</span> <Typography variant="body" className="text-amber-800">{strength}</Typography> </li> ))} </ul> </div> <div className="space-y-3"> <Typography variant="h4" className="font-medium flex items-center gap-2 text-amber-900"> <AlertCircle className="h-5 w-5 text-amber-600" /> Areas for Improvement </Typography> <ul className="space-y-2"> {data.weaknesses.map((weakness, i) => ( <li key={i} className="flex items-start gap-2"> <span className="text-amber-600 mt-0.5">•</span> <Typography variant="body" className="text-amber-800">{weakness}</Typography> </li> ))} </ul> </div> </div> {/* Examples Section */} {(data.examples.good.length > 0 || data.examples.needsWork.length > 0) && ( <div className="border-t border-amber-200 pt-6"> <button onClick={() => setExpandedCategory( expandedCategory === key ? null : key )} className="flex items-center gap-2 text-sm font-medium text-amber-700 hover:text-amber-900" > <Info className="h-4 w-4" /> View Examples from Your Manuscript <ChevronRight className={`h-4 w-4 transition-transform ${ expandedCategory === key ? 'rotate-90' : '' }`} /> </button> <AnimatePresence> {expandedCategory === key && ( <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} className="mt-4 space-y-4 overflow-hidden" > {data.examples.good.length > 0 && ( <div className="bg-green-50/50 backdrop-blur-sm p-4 rounded-lg border border-green-200"> <Typography variant="h5" className="font-medium text-green-700 mb-2"> Good Examples </Typography> {data.examples.good.map((example, i) => ( <blockquote key={i} className="italic border-l-2 border-green-600 pl-3 my-2"> <Typography variant="body" className="text-green-800"> "{example}" </Typography> </blockquote> ))} </div> )} {data.examples.needsWork.length > 0 && ( <div className="bg-amber-50/50 backdrop-blur-sm p-4 rounded-lg border border-amber-200"> <Typography variant="h5" className="font-medium text-amber-700 mb-2"> Needs Improvement </Typography> {data.examples.needsWork.map((example, i) => ( <blockquote key={i} className="italic border-l-2 border-amber-600 pl-3 my-2"> <Typography variant="body" className="text-amber-800"> "{example}" </Typography> </blockquote> ))} </div> )} </motion.div> )} </AnimatePresence> </div> )} {/* Suggestions */} <div className="bg-amber-50/50 backdrop-blur-sm p-4 rounded-lg border border-amber-200"> <Typography variant="h4" className="font-medium text-amber-900 mb-3">Recommendations</Typography> <ul className="space-y-2"> {data.suggestions.map((suggestion, i) => ( <li key={i} className="flex items-start gap-2"> <span className="text-amber-600">→</span> <Typography variant="body" className="text-amber-800">{suggestion}</Typography> </li> ))} </ul> </div> </TabsContent> ))} </Tabs> </Card> ) }

4. Strengths and Weaknesses Display

// apps/analyzer-app/src/components/analysis/StrengthsWeaknesses.tsx 'use client' import { Card, Typography } from '@mystoryflow/ui' import { motion } from 'framer-motion' import { Sparkles, AlertTriangle } from 'lucide-react' interface StrengthsWeaknessesProps { strengths: string[] weaknesses: string[] } export function StrengthsWeaknesses({ strengths, weaknesses }: StrengthsWeaknessesProps) { return ( <div className="grid md:grid-cols-2 gap-6"> {/* Strengths Card */} <Card className="p-6 border-green-200 bg-gradient-to-br from-green-50/50 to-emerald-50/50"> <div className="flex items-center gap-3 mb-4"> <div className="p-2 bg-green-100 rounded-lg"> <Sparkles className="h-6 w-6 text-green-600" /> </div> <Typography variant="h3" className="text-green-800">Top Strengths</Typography> </div> <ul className="space-y-3"> {strengths.slice(0, 5).map((strength, index) => ( <motion.li key={index} initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: index * 0.1 }} className="flex items-start gap-3" > <span className="text-green-600 text-lg leading-none mt-0.5">âś“</span> <Typography variant="body" className="text-green-700">{strength}</Typography> </motion.li> ))} </ul> </Card> {/* Weaknesses Card */} <Card className="p-6 border-amber-200 bg-gradient-to-br from-amber-50/50 to-orange-50/50"> <div className="flex items-center gap-3 mb-4"> <div className="p-2 bg-amber-100 rounded-lg"> <AlertTriangle className="h-6 w-6 text-amber-600" /> </div> <Typography variant="h3" className="text-amber-800">Key Areas to Improve</Typography> </div> <ul className="space-y-3"> {weaknesses.slice(0, 5).map((weakness, index) => ( <motion.li key={index} initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: index * 0.1 }} className="flex items-start gap-3" > <span className="text-amber-600 text-lg leading-none mt-0.5">!</span> <Typography variant="body" className="text-amber-700">{weakness}</Typography> </motion.li> ))} </ul> </Card> </div> ) }

5. Real-time Loading States

// apps/analyzer-app/src/components/analysis/loading-states.tsx import { Card, Skeleton } from '@mystoryflow/ui' export function ScoreLoading() { return ( <div className="grid gap-6 md:grid-cols-3"> <Card className="md:col-span-2 p-8"> <Skeleton className="h-8 w-48 mb-4" /> <Skeleton className="h-16 w-32 mb-4" /> <Skeleton className="h-4 w-full" /> </Card> <Card className="p-6"> <Skeleton className="h-6 w-32 mb-4" /> <Skeleton className="h-20 w-full" /> </Card> </div> ) } export function CategoryLoading() { return ( <Card className="p-6"> <Skeleton className="h-8 w-48 mb-6" /> <div className="space-y-4"> <Skeleton className="h-12 w-full" /> <Skeleton className="h-64 w-full" /> </div> </Card> ) }

MVP Acceptance Criteria

  • Real-time analysis display with smooth animations
  • Interactive category tabs with detailed feedback
  • Visual score representations (charts, progress bars)
  • Expandable examples from manuscript
  • Strengths and weaknesses visualization
  • Mobile-responsive layout
  • Loading states for better UX
  • Publishing readiness indicator
  • Literary theme integration (browns, golds, paper textures)

Post-MVP Enhancements

  • Heat map visualization for pacing
  • Chapter-by-chapter breakdown
  • Interactive improvement simulator
  • Comparison with genre benchmarks
  • Export analysis as PDF with MyStoryFlow branding
  • Share analysis results within MyStoryFlow platform
  • AI chat for clarification using @mystoryflow/ai
  • Video walkthrough generation
  • Integration with MyStoryFlow coaching marketplace

Implementation Time

  • Development: 2 days
  • Testing: 0.5 days
  • Total: 2.5 days

Integration Points

1. MyStoryFlow UI Components

  • Use @mystoryflow/ui for all components
  • Apply literary theme consistently
  • Use amber/gold color palette
  • Add paper texture backgrounds

2. Data Integration

  • Use @mystoryflow/supabase for data fetching
  • Implement real-time updates via @mystoryflow/realtime
  • Cache analysis results in @mystoryflow/cache

3. Chart Library

  • Use @mystoryflow/charts for visualizations
  • Customize with literary theme colors
  • Ensure mobile responsiveness

Testing Requirements

Unit Tests

describe('Analysis Display', () => { it('displays overall score correctly', () => {}) it('shows category breakdowns', () => {}) it('handles loading states', () => {}) it('expands/collapses examples', () => {}) it('applies correct color coding', () => {}) })

Integration Tests

  • Test data fetching from Supabase
  • Verify real-time updates
  • Test chart rendering
  • Verify mobile responsiveness

E2E Tests

  • Complete analysis viewing flow
  • Interactive element testing
  • Export functionality
  • Navigation between analyses

Next Steps

  • Implement F019: Progress Tracking
  • Add comparison view for multiple analyses
  • Integrate with coaching recommendations
  • Add social sharing within MyStoryFlow