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/uifor all components - Apply literary theme consistently
- Use amber/gold color palette
- Add paper texture backgrounds
2. Data Integration
- Use
@mystoryflow/supabasefor data fetching - Implement real-time updates via
@mystoryflow/realtime - Cache analysis results in
@mystoryflow/cache
3. Chart Library
- Use
@mystoryflow/chartsfor 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