Akhil-Theerthala commited on
Commit
2cdfc6e
·
verified ·
1 Parent(s): 959fc91

Upload 25 files

Browse files
App.tsx CHANGED
@@ -1,10 +1,22 @@
1
  import React, { useState, useCallback, useEffect } from 'react';
2
  import StoryInput from './components/StoryInput';
3
- import LoadingIndicator from './components/LoadingIndicator';
4
- import ComicPreview from './components/ComicPreview';
5
  import ApiKeySetup from './components/ApiKeySetup';
 
6
  import { generateMangaScriptAndImages } from './services/geminiService';
7
- import type { MangaStyle, LoadingState } from './types';
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  function App() {
10
  const [apiKey, setApiKey] = useState<string>('');
@@ -12,9 +24,22 @@ function App() {
12
  const [story, setStory] = useState('');
13
  const [author, setAuthor] = useState('');
14
  const [style, setStyle] = useState<MangaStyle>('Shonen');
15
- const [loadingState, setLoadingState] = useState<LoadingState>({ isLoading: false, message: '', progress: 0 });
 
 
 
 
16
  const [generatedImages, setGeneratedImages] = useState<string[]>([]);
 
 
 
 
 
17
  const [error, setError] = useState<string | null>(null);
 
 
 
 
18
 
19
  // Check for stored API key on component mount
20
  useEffect(() => {
@@ -29,21 +54,35 @@ function App() {
29
 
30
  setError(null);
31
  setGeneratedImages([]);
32
- setLoadingState({ isLoading: true, message: 'Starting...', progress: 0 });
 
 
 
 
 
33
 
34
  try {
35
- const images = await generateMangaScriptAndImages(
36
- { title, story, author, style },
37
- (progressUpdate) => setLoadingState(progressUpdate),
38
  apiKey
39
  );
40
- setGeneratedImages(images);
 
 
 
 
 
 
 
 
 
41
  } catch (err) {
42
  const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred.';
43
  setError(`Manga generation failed: ${errorMessage} Please check the console for details and try again.`);
44
  console.error(err);
45
  } finally {
46
- setLoadingState({ isLoading: false, message: '', progress: 0 });
47
  }
48
  }, [title, story, author, style]);
49
 
@@ -53,13 +92,46 @@ function App() {
53
  setAuthor('');
54
  setStyle('Shonen');
55
  setGeneratedImages([]);
 
 
 
 
 
56
  setError(null);
57
- setLoadingState({ isLoading: false, message: '', progress: 0 });
 
 
 
 
58
  }, []);
59
 
60
  const handleApiKeySet = useCallback((newApiKey: string) => {
61
  setApiKey(newApiKey);
62
  }, []);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
  const renderContent = () => {
65
  // Show API key setup if no key is available
@@ -67,11 +139,30 @@ function App() {
67
  return <ApiKeySetup onApiKeySet={handleApiKeySet} />;
68
  }
69
 
70
- if (loadingState.isLoading) {
71
- return <LoadingIndicator loadingState={loadingState} />;
72
  }
73
  if (generatedImages.length > 0) {
74
- return <ComicPreview images={generatedImages} title={title} onReset={handleReset} />;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  }
76
  return (
77
  <StoryInput
@@ -84,26 +175,33 @@ function App() {
84
  style={style}
85
  onStyleChange={setStyle}
86
  onGenerate={handleGenerate}
87
- disabled={loadingState.isLoading}
 
 
88
  />
89
  );
90
  };
91
 
92
  return (
93
  <div className="min-h-screen text-white">
94
- <header className="relative overflow-hidden py-8">
95
- <div className="absolute inset-0 bg-gradient-to-br from-gray-800/20 to-gray-900/30 backdrop-blur-sm"></div>
96
  <div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
97
  <div className="text-center">
98
- <h1 className="text-4xl sm:text-6xl font-bold mb-4 animate-fade-in">
99
  Comic <span className="gradient-text">Genesis</span> AI
100
  </h1>
101
- <p className="text-xl sm:text-2xl text-white/80 mb-2 animate-slide-in">
102
  Professional Comic Book Creation Studio
103
  </p>
104
- <p className="text-lg text-white/60 animate-slide-in">
105
- Transform your stories into stunning visual narratives with AI-powered artistry
106
  </p>
 
 
 
 
 
107
  </div>
108
  </div>
109
  </header>
@@ -111,16 +209,18 @@ function App() {
111
  <main className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
112
  <div className="max-w-6xl mx-auto">
113
  {error && (
114
- <div className="glass-card border-red-400 bg-red-500/20 text-red-100 px-6 py-4 mb-8 animate-fade-in" role="alert">
115
- <div className="flex items-center">
116
- <div className="flex-shrink-0">
117
- <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
118
- <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
119
- </svg>
 
 
120
  </div>
121
- <div className="ml-3">
122
- <strong className="font-bold">Creation Error: </strong>
123
- <span>{error}</span>
124
  </div>
125
  </div>
126
  </div>
@@ -129,6 +229,15 @@ function App() {
129
  </div>
130
  </main>
131
 
 
 
 
 
 
 
 
 
 
132
  <footer className="text-center py-8 px-4 text-white/50 text-sm border-t border-white/10">
133
  <div className="max-w-4xl mx-auto">
134
  <p className="mb-2">🎨 Powered by Google Gemini Nanobanana • Built for Professional Comic Creators</p>
 
1
  import React, { useState, useCallback, useEffect } from 'react';
2
  import StoryInput from './components/StoryInput';
3
+ import EnhancedLoadingIndicator from './components/EnhancedLoadingIndicator';
4
+ import ComicSeriesManager from './components/ComicSeriesManager';
5
  import ApiKeySetup from './components/ApiKeySetup';
6
+ import ExplorationPrompt from './components/ExplorationPrompt';
7
  import { generateMangaScriptAndImages } from './services/geminiService';
8
+ import { PerformanceTracker } from './utils/imageOptimization';
9
+ import { createMangaPdf } from './services/pdfService';
10
+ import type {
11
+ MangaStyle,
12
+ GenerationProgress,
13
+ CharacterProfile,
14
+ MangaPage,
15
+ CharacterSketch,
16
+ EnvironmentSketch,
17
+ StoryArcSummary,
18
+ PerformanceTiming
19
+ } from './types';
20
 
21
  function App() {
22
  const [apiKey, setApiKey] = useState<string>('');
 
24
  const [story, setStory] = useState('');
25
  const [author, setAuthor] = useState('');
26
  const [style, setStyle] = useState<MangaStyle>('Shonen');
27
+ const [generationProgress, setGenerationProgress] = useState<GenerationProgress>({
28
+ phase: 'character_analysis',
29
+ message: '',
30
+ progress: 0
31
+ });
32
  const [generatedImages, setGeneratedImages] = useState<string[]>([]);
33
+ const [generatedCharacters, setGeneratedCharacters] = useState<CharacterProfile[]>([]);
34
+ const [generatedPages, setGeneratedPages] = useState<MangaPage[]>([]);
35
+ const [characterSketches, setCharacterSketches] = useState<CharacterSketch[]>([]);
36
+ const [environmentSketches, setEnvironmentSketches] = useState<EnvironmentSketch[]>([]);
37
+ const [storyArcSummary, setStoryArcSummary] = useState<StoryArcSummary | null>(null);
38
  const [error, setError] = useState<string | null>(null);
39
+ const [performanceTimings, setPerformanceTimings] = useState<PerformanceTiming[]>([]);
40
+ const [totalGenerationTime, setTotalGenerationTime] = useState<number>(0);
41
+ const [showExplorationPrompt, setShowExplorationPrompt] = useState(false);
42
+ const [includeDialogue, setIncludeDialogue] = useState<boolean>(true);
43
 
44
  // Check for stored API key on component mount
45
  useEffect(() => {
 
54
 
55
  setError(null);
56
  setGeneratedImages([]);
57
+ setGeneratedCharacters([]);
58
+ setGeneratedPages([]);
59
+ setCharacterSketches([]);
60
+ setEnvironmentSketches([]);
61
+ setStoryArcSummary(null);
62
+ setGenerationProgress({ phase: 'character_analysis', message: 'Starting...', progress: 0 });
63
 
64
  try {
65
+ const result = await generateMangaScriptAndImages(
66
+ { title, story, author, style, includeDialogue },
67
+ (progressUpdate) => setGenerationProgress(progressUpdate),
68
  apiKey
69
  );
70
+ setGeneratedImages(result.images);
71
+ setGeneratedCharacters(result.characters);
72
+ setGeneratedPages(result.pages);
73
+ setCharacterSketches(result.characterSketches);
74
+ setEnvironmentSketches(result.environmentSketches);
75
+ setStoryArcSummary(result.storyArcSummary);
76
+ setPerformanceTimings(result.performanceTimings);
77
+ setTotalGenerationTime(result.totalGenerationTime);
78
+
79
+ // Don't auto-show exploration prompt - only when user clicks download
80
  } catch (err) {
81
  const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred.';
82
  setError(`Manga generation failed: ${errorMessage} Please check the console for details and try again.`);
83
  console.error(err);
84
  } finally {
85
+ setGenerationProgress({ phase: 'completion', message: '', progress: 0 });
86
  }
87
  }, [title, story, author, style]);
88
 
 
92
  setAuthor('');
93
  setStyle('Shonen');
94
  setGeneratedImages([]);
95
+ setGeneratedCharacters([]);
96
+ setGeneratedPages([]);
97
+ setCharacterSketches([]);
98
+ setEnvironmentSketches([]);
99
+ setStoryArcSummary(null);
100
  setError(null);
101
+ setPerformanceTimings([]);
102
+ setTotalGenerationTime(0);
103
+ setShowExplorationPrompt(false);
104
+ setIncludeDialogue(true);
105
+ setGenerationProgress({ phase: 'character_analysis', message: '', progress: 0 });
106
  }, []);
107
 
108
  const handleApiKeySet = useCallback((newApiKey: string) => {
109
  setApiKey(newApiKey);
110
  }, []);
111
+
112
+ const handleExplorationContinue = useCallback(() => {
113
+ setShowExplorationPrompt(false);
114
+ // The ComicSeriesManager will handle the continuation logic
115
+ }, []);
116
+
117
+ const handleExplorationFinish = useCallback((removeApiKey: boolean) => {
118
+ console.log('handleExplorationFinish called - downloading PDF');
119
+ setShowExplorationPrompt(false);
120
+
121
+ // Download PDF automatically
122
+ if (generatedImages.length > 0) {
123
+ console.log('Calling createMangaPdf...');
124
+ createMangaPdf(generatedImages, title);
125
+ } else {
126
+ console.warn('No generated images available for PDF creation');
127
+ }
128
+
129
+ // No API key removal - keep it stored for future use
130
+ }, [generatedImages, title]);
131
+
132
+ const handleDownloadClick = useCallback(() => {
133
+ setShowExplorationPrompt(true);
134
+ }, []);
135
 
136
  const renderContent = () => {
137
  // Show API key setup if no key is available
 
139
  return <ApiKeySetup onApiKeySet={handleApiKeySet} />;
140
  }
141
 
142
+ if (generationProgress.progress > 0 && generationProgress.progress < 100) {
143
+ return <EnhancedLoadingIndicator generationProgress={generationProgress} />;
144
  }
145
  if (generatedImages.length > 0) {
146
+ return (
147
+ <ComicSeriesManager
148
+ initialImages={generatedImages}
149
+ title={title}
150
+ story={story}
151
+ author={author}
152
+ style={style}
153
+ apiKey={apiKey}
154
+ onReset={handleReset}
155
+ characters={generatedCharacters}
156
+ pages={generatedPages}
157
+ characterSketches={characterSketches}
158
+ environmentSketches={environmentSketches}
159
+ storyArcSummary={storyArcSummary}
160
+ performanceTimings={performanceTimings}
161
+ totalGenerationTime={totalGenerationTime}
162
+ onDownloadClick={handleDownloadClick}
163
+ includeDialogue={includeDialogue}
164
+ />
165
+ );
166
  }
167
  return (
168
  <StoryInput
 
175
  style={style}
176
  onStyleChange={setStyle}
177
  onGenerate={handleGenerate}
178
+ disabled={generationProgress.progress > 0 && generationProgress.progress < 100}
179
+ includeDialogue={includeDialogue}
180
+ onDialogueToggle={setIncludeDialogue}
181
  />
182
  );
183
  };
184
 
185
  return (
186
  <div className="min-h-screen text-white">
187
+ <header className="relative overflow-hidden py-12">
188
+ <div className="absolute inset-0 bg-gradient-to-br from-black/40 to-gray-900/20 backdrop-blur-sm"></div>
189
  <div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
190
  <div className="text-center">
191
+ <h1 className="text-5xl sm:text-7xl font-black mb-6 animate-fade-in tracking-tight">
192
  Comic <span className="gradient-text">Genesis</span> AI
193
  </h1>
194
+ <p className="text-xl sm:text-2xl text-white/90 mb-4 animate-slide-in font-medium">
195
  Professional Comic Book Creation Studio
196
  </p>
197
+ <p className="text-lg text-white/70 animate-slide-in max-w-2xl mx-auto leading-relaxed">
198
+ Transform your stories into stunning visual narratives with cutting-edge AI artistry
199
  </p>
200
+ <div className="mt-8 flex justify-center">
201
+ <div className="glass-card px-6 py-3 text-sm text-white/80">
202
+ ✨ Powered by Google Gemini 2.5 Flash • Professional Quality Results
203
+ </div>
204
+ </div>
205
  </div>
206
  </div>
207
  </header>
 
209
  <main className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
210
  <div className="max-w-6xl mx-auto">
211
  {error && (
212
+ <div className="glass-card border-red-400/30 bg-red-500/10 text-red-100 px-8 py-6 mb-8 animate-fade-in" role="alert">
213
+ <div className="flex items-start space-x-4">
214
+ <div className="flex-shrink-0 mt-1">
215
+ <div className="w-8 h-8 bg-red-500/20 rounded-full flex items-center justify-center">
216
+ <svg className="w-5 h-5 text-red-300" fill="currentColor" viewBox="0 0 20 20">
217
+ <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
218
+ </svg>
219
+ </div>
220
  </div>
221
+ <div className="flex-1">
222
+ <h3 className="font-bold text-red-200 mb-2">Creation Error</h3>
223
+ <p className="text-red-100/90 text-sm leading-relaxed">{error}</p>
224
  </div>
225
  </div>
226
  </div>
 
229
  </div>
230
  </main>
231
 
232
+ {/* Exploration Prompt */}
233
+ <ExplorationPrompt
234
+ showPrompt={showExplorationPrompt && generatedImages.length > 0 && !error}
235
+ onContinue={handleExplorationContinue}
236
+ onFinish={handleExplorationFinish}
237
+ generationTime={totalGenerationTime}
238
+ totalPages={generatedImages.length}
239
+ />
240
+
241
  <footer className="text-center py-8 px-4 text-white/50 text-sm border-t border-white/10">
242
  <div className="max-w-4xl mx-auto">
243
  <p className="mb-2">🎨 Powered by Google Gemini Nanobanana • Built for Professional Comic Creators</p>
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Manga Genesis AI
3
  emoji: 🎨
4
  colorFrom: purple
5
  colorTo: pink
 
1
  ---
2
+ title: Comic Genesis AI
3
  emoji: 🎨
4
  colorFrom: purple
5
  colorTo: pink
components/ApiKeySetup.tsx CHANGED
@@ -42,9 +42,14 @@ const ApiKeySetup: React.FC<ApiKeySetupProps> = ({ onApiKeySet }) => {
42
  className="w-full input-glass"
43
  required
44
  />
45
- <p className="text-white/60 text-xs mt-2">
46
- 🔒 Your API key is stored locally and never sent to our servers
47
- </p>
 
 
 
 
 
48
  </div>
49
 
50
  <button
@@ -65,17 +70,28 @@ const ApiKeySetup: React.FC<ApiKeySetupProps> = ({ onApiKeySet }) => {
65
  </button>
66
 
67
  {showInstructions && (
68
- <div className="mt-4 p-4 bg-white/10 rounded-lg text-sm text-white/80 animate-fade-in">
69
- <h3 className="font-semibold mb-3">📝 Getting Your Gemini API Key:</h3>
70
- <ol className="space-y-2 list-decimal list-inside">
71
- <li>Visit <a href="https://makersuite.google.com/app/apikey" target="_blank" rel="noopener noreferrer" className="text-blue-300 hover:text-blue-200 underline">Google AI Studio</a></li>
72
- <li>Sign in with your Google account</li>
73
- <li>Click "Create API Key"</li>
74
- <li>Copy the generated key</li>
75
- <li>Paste it above to start creating comics!</li>
76
- </ol>
77
- <div className="mt-4 p-3 bg-yellow-500/20 rounded-lg">
78
- <p className="text-yellow-200 text-xs">
 
 
 
 
 
 
 
 
 
 
 
79
  ⚡ <strong>Free Tier:</strong> Google provides generous free usage for Gemini API - perfect for creating comics!
80
  </p>
81
  </div>
 
42
  className="w-full input-glass"
43
  required
44
  />
45
+ <div className="flex items-center mt-2 text-white/60 text-xs">
46
+ <div className="w-4 h-4 bg-green-500/20 rounded-full flex items-center justify-center mr-2">
47
+ <svg className="w-2.5 h-2.5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
48
+ <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
49
+ </svg>
50
+ </div>
51
+ <span>Your API key is stored locally and never sent to our servers</span>
52
+ </div>
53
  </div>
54
 
55
  <button
 
70
  </button>
71
 
72
  {showInstructions && (
73
+ <div className="mt-4 glass-card p-6 text-sm text-white/80 animate-fade-in">
74
+ <h3 className="font-bold mb-4 gradient-text">📝 Getting Your Gemini API Key:</h3>
75
+ <div className="space-y-3">
76
+ <div className="flex items-start space-x-3">
77
+ <div className="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0 mt-0.5">1</div>
78
+ <div>Visit <a href="https://makersuite.google.com/app/apikey" target="_blank" rel="noopener noreferrer" className="text-blue-300 hover:text-blue-200 underline font-medium">Google AI Studio</a></div>
79
+ </div>
80
+ <div className="flex items-start space-x-3">
81
+ <div className="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0 mt-0.5">2</div>
82
+ <div>Sign in with your Google account</div>
83
+ </div>
84
+ <div className="flex items-start space-x-3">
85
+ <div className="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0 mt-0.5">3</div>
86
+ <div>Click "Create API Key" and copy the generated key</div>
87
+ </div>
88
+ <div className="flex items-start space-x-3">
89
+ <div className="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0 mt-0.5">4</div>
90
+ <div>Paste it above to start creating comics!</div>
91
+ </div>
92
+ </div>
93
+ <div className="mt-6 glass p-4 rounded-lg">
94
+ <p className="text-yellow-200 text-xs font-medium">
95
  ⚡ <strong>Free Tier:</strong> Google provides generous free usage for Gemini API - perfect for creating comics!
96
  </p>
97
  </div>
components/ComicPreview.tsx CHANGED
@@ -1,10 +1,15 @@
1
- import React from 'react';
2
  import { createMangaPdf } from '../services/pdfService';
 
 
 
3
 
4
  interface ComicPreviewProps {
5
  images: string[];
6
  title: string;
7
  onReset: () => void;
 
 
8
  }
9
 
10
  const getPageLabel = (index: number, totalImages: number): string => {
@@ -18,9 +23,25 @@ const getPageLabel = (index: number, totalImages: number): string => {
18
  };
19
 
20
 
21
- const ComicPreview: React.FC<ComicPreviewProps> = ({ images, title, onReset }) => {
 
 
22
  const handleDownload = () => {
23
- createMangaPdf(images, title);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  };
25
 
26
  return (
@@ -51,15 +72,91 @@ const ComicPreview: React.FC<ComicPreviewProps> = ({ images, title, onReset }) =
51
  </div>
52
  </div>
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  {/* Comic Pages Gallery */}
55
  <div className="panel-grid">
56
  {images.map((imgData, index) => (
57
- <div key={index} className="panel-item group">
 
 
 
 
58
  <div className="relative overflow-hidden">
59
- <img
60
- src={`data:image/jpeg;base64,${imgData}`}
61
  alt={`Generated comic page ${index + 1}`}
62
  className="w-full h-auto object-contain transition-transform duration-500 group-hover:scale-105"
 
63
  />
64
  <div className="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
65
  <div className="absolute bottom-4 left-4 right-4 transform translate-y-8 group-hover:translate-y-0 transition-transform duration-300">
@@ -68,7 +165,7 @@ const ComicPreview: React.FC<ComicPreviewProps> = ({ images, title, onReset }) =
68
  {getPageLabel(index, images.length)}
69
  </p>
70
  <p className="text-white/80 text-xs">
71
- Page {index + 1} of {images.length}
72
  </p>
73
  </div>
74
  </div>
@@ -89,6 +186,15 @@ const ComicPreview: React.FC<ComicPreviewProps> = ({ images, title, onReset }) =
89
  ))}
90
  </div>
91
 
 
 
 
 
 
 
 
 
 
92
  {/* Stats Section */}
93
  <div className="glass-card p-6">
94
  <div className="flex justify-center items-center gap-8 text-center">
@@ -110,6 +216,6 @@ const ComicPreview: React.FC<ComicPreviewProps> = ({ images, title, onReset }) =
110
  </div>
111
  </div>
112
  );
113
- };
114
 
115
  export default ComicPreview;
 
1
+ import React, { useState, memo } from 'react';
2
  import { createMangaPdf } from '../services/pdfService';
3
+ import ExpandableImageModal from './ExpandableImageModal';
4
+ import LazyImage from './LazyImage';
5
+ import type { StoryArcSummary } from '../types';
6
 
7
  interface ComicPreviewProps {
8
  images: string[];
9
  title: string;
10
  onReset: () => void;
11
+ storyArcSummary?: StoryArcSummary;
12
+ onDownloadClick?: () => void;
13
  }
14
 
15
  const getPageLabel = (index: number, totalImages: number): string => {
 
23
  };
24
 
25
 
26
+ const ComicPreview: React.FC<ComicPreviewProps> = memo(({ images, title, onReset, storyArcSummary, onDownloadClick }) => {
27
+ const [selectedImage, setSelectedImage] = useState<{ data: string; title: string; description?: string } | null>(null);
28
+
29
  const handleDownload = () => {
30
+ if (onDownloadClick) {
31
+ onDownloadClick();
32
+ } else {
33
+ createMangaPdf(images, title);
34
+ }
35
+ };
36
+
37
+ const handleImageClick = (imgData: string, index: number) => {
38
+ setSelectedImage({
39
+ data: imgData,
40
+ title: getPageLabel(index, images.length),
41
+ description: index === 0 ? 'Opening cover with title and characters' :
42
+ index === images.length - 1 ? 'Story conclusion and finale' :
43
+ 'Story development and character progression'
44
+ });
45
  };
46
 
47
  return (
 
72
  </div>
73
  </div>
74
 
75
+ {/* Story Arc Summary - Compact Version */}
76
+ {storyArcSummary && (
77
+ <div className="glass-card p-6">
78
+ <h3 className="text-xl font-bold text-white mb-4 text-center gradient-text">
79
+ 📖 Story Analysis
80
+ </h3>
81
+
82
+ {/* Compact Theme Display */}
83
+ <div className="bg-white/10 rounded-lg p-4 mb-4">
84
+ <div className="flex items-start space-x-3">
85
+ <span className="text-2xl">🎯</span>
86
+ <div className="flex-1">
87
+ <h4 className="text-sm font-semibold text-white mb-1">Central Theme</h4>
88
+ <p className="text-white/70 text-xs leading-relaxed">
89
+ {storyArcSummary.overallTheme.length > 120
90
+ ? `${storyArcSummary.overallTheme.slice(0, 120)}...`
91
+ : storyArcSummary.overallTheme}
92
+ </p>
93
+ </div>
94
+ </div>
95
+ </div>
96
+
97
+ {/* Compact Events & Development */}
98
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
99
+ <div className="bg-white/10 rounded-lg p-4">
100
+ <div className="flex items-center space-x-2 mb-3">
101
+ <span className="text-lg">🎬</span>
102
+ <h4 className="text-sm font-semibold text-white">Key Events</h4>
103
+ </div>
104
+ <div className="space-y-1">
105
+ {storyArcSummary.keyEvents.slice(0, 3).map((event, index) => (
106
+ <div key={index} className="text-white/70 text-xs flex items-start">
107
+ <span className="text-white/50 mr-1 mt-0.5">•</span>
108
+ <span className="leading-tight">
109
+ {event.length > 50 ? `${event.slice(0, 50)}...` : event}
110
+ </span>
111
+ </div>
112
+ ))}
113
+ {storyArcSummary.keyEvents.length > 3 && (
114
+ <div className="text-white/50 text-xs italic">
115
+ +{storyArcSummary.keyEvents.length - 3} more events
116
+ </div>
117
+ )}
118
+ </div>
119
+ </div>
120
+
121
+ <div className="bg-white/10 rounded-lg p-4">
122
+ <div className="flex items-center space-x-2 mb-3">
123
+ <span className="text-lg">👥</span>
124
+ <h4 className="text-sm font-semibold text-white">Character Growth</h4>
125
+ </div>
126
+ <div className="space-y-1">
127
+ {storyArcSummary.characterDevelopment.slice(0, 3).map((development, index) => (
128
+ <div key={index} className="text-white/70 text-xs flex items-start">
129
+ <span className="text-white/50 mr-1 mt-0.5">•</span>
130
+ <span className="leading-tight">
131
+ {development.length > 50 ? `${development.slice(0, 50)}...` : development}
132
+ </span>
133
+ </div>
134
+ ))}
135
+ {storyArcSummary.characterDevelopment.length > 3 && (
136
+ <div className="text-white/50 text-xs italic">
137
+ +{storyArcSummary.characterDevelopment.length - 3} more developments
138
+ </div>
139
+ )}
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ )}
145
+
146
  {/* Comic Pages Gallery */}
147
  <div className="panel-grid">
148
  {images.map((imgData, index) => (
149
+ <div
150
+ key={index}
151
+ className="panel-item group cursor-pointer"
152
+ onClick={() => handleImageClick(imgData, index)}
153
+ >
154
  <div className="relative overflow-hidden">
155
+ <LazyImage
156
+ src={imgData}
157
  alt={`Generated comic page ${index + 1}`}
158
  className="w-full h-auto object-contain transition-transform duration-500 group-hover:scale-105"
159
+ quality="compressed"
160
  />
161
  <div className="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
162
  <div className="absolute bottom-4 left-4 right-4 transform translate-y-8 group-hover:translate-y-0 transition-transform duration-300">
 
165
  {getPageLabel(index, images.length)}
166
  </p>
167
  <p className="text-white/80 text-xs">
168
+ Page {index + 1} of {images.length} • Click to expand
169
  </p>
170
  </div>
171
  </div>
 
186
  ))}
187
  </div>
188
 
189
+ {/* Expandable Image Modal */}
190
+ <ExpandableImageModal
191
+ isOpen={selectedImage !== null}
192
+ onClose={() => setSelectedImage(null)}
193
+ imageData={selectedImage?.data || ''}
194
+ title={selectedImage?.title || ''}
195
+ description={selectedImage?.description}
196
+ />
197
+
198
  {/* Stats Section */}
199
  <div className="glass-card p-6">
200
  <div className="flex justify-center items-center gap-8 text-center">
 
216
  </div>
217
  </div>
218
  );
219
+ });
220
 
221
  export default ComicPreview;
components/ComicSeriesManager.tsx ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, memo } from 'react';
2
+ import ComicPreview from './ComicPreview';
3
+ import StoryProgressionSelector from './StoryProgressionSelector';
4
+ import LoadingIndicator from './LoadingIndicator';
5
+ import PerformanceSummary from './PerformanceSummary';
6
+ import { analyzeStoryProgressions, generateComicContinuation } from '../services/geminiService';
7
+ import { createComicSeriesPdf, createMangaPdf } from '../services/pdfService';
8
+ import type {
9
+ ComicIteration,
10
+ ComicSeries,
11
+ StoryProgression,
12
+ LoadingState,
13
+ MangaPage,
14
+ CharacterProfile,
15
+ MangaStyle,
16
+ CharacterSketch,
17
+ EnvironmentSketch,
18
+ StoryArcSummary,
19
+ PerformanceTiming
20
+ } from '../types';
21
+
22
+ interface ComicSeriesManagerProps {
23
+ initialImages: string[];
24
+ title: string;
25
+ story: string;
26
+ author: string;
27
+ style: MangaStyle;
28
+ apiKey: string;
29
+ onReset: () => void;
30
+ characters?: CharacterProfile[];
31
+ pages?: MangaPage[];
32
+ characterSketches?: CharacterSketch[];
33
+ environmentSketches?: EnvironmentSketch[];
34
+ storyArcSummary?: StoryArcSummary | null;
35
+ performanceTimings?: PerformanceTiming[];
36
+ totalGenerationTime?: number;
37
+ onDownloadClick?: () => void;
38
+ includeDialogue?: boolean;
39
+ }
40
+
41
+ type SeriesState = 'preview' | 'analyzing' | 'selecting_progression' | 'generating_continuation' | 'series_complete';
42
+
43
+ const ComicSeriesManager: React.FC<ComicSeriesManagerProps> = ({
44
+ initialImages,
45
+ title,
46
+ story,
47
+ author,
48
+ style,
49
+ apiKey,
50
+ onReset,
51
+ characters = [],
52
+ pages = [],
53
+ characterSketches = [],
54
+ environmentSketches = [],
55
+ storyArcSummary = null,
56
+ performanceTimings = [],
57
+ totalGenerationTime = 0,
58
+ onDownloadClick,
59
+ includeDialogue = true
60
+ }) => {
61
+ const [seriesState, setSeriesState] = useState<SeriesState>('preview');
62
+ const [comicSeries, setComicSeries] = useState<ComicSeries | null>(null);
63
+ const [currentProgressions, setCurrentProgressions] = useState<StoryProgression[]>([]);
64
+ const [loadingState, setLoadingState] = useState<LoadingState>({ isLoading: false, message: '', progress: 0 });
65
+ const [error, setError] = useState<string | null>(null);
66
+
67
+ // Initialize the comic series with the first iteration
68
+ useEffect(() => {
69
+ const firstIteration: ComicIteration = {
70
+ id: 'chapter_1',
71
+ title,
72
+ story,
73
+ author,
74
+ style,
75
+ includeDialogue,
76
+ images: initialImages,
77
+ characters,
78
+ pages,
79
+ generatedAt: new Date()
80
+ };
81
+
82
+ const series: ComicSeries = {
83
+ seriesTitle: title,
84
+ author,
85
+ iterations: [firstIteration],
86
+ totalPages: initialImages.length,
87
+ createdAt: new Date(),
88
+ lastUpdated: new Date(),
89
+ totalGenerationTime: totalGenerationTime,
90
+ performanceSummary: performanceTimings
91
+ };
92
+
93
+ setComicSeries(series);
94
+ }, [initialImages, title, story, author, style, characters, pages, performanceTimings, totalGenerationTime, includeDialogue]);
95
+
96
+ const handleContinueStory = async () => {
97
+ if (!comicSeries || comicSeries.iterations.length === 0) return;
98
+
99
+ setError(null);
100
+ setSeriesState('analyzing');
101
+ setLoadingState({ isLoading: true, message: 'Analyzing your story for continuation possibilities...', progress: 20 });
102
+
103
+ try {
104
+ const currentIteration = comicSeries.iterations[comicSeries.iterations.length - 1];
105
+ const progressions = await analyzeStoryProgressions(currentIteration, apiKey);
106
+
107
+ setCurrentProgressions(progressions);
108
+ setSeriesState('selecting_progression');
109
+ setLoadingState({ isLoading: false, message: '', progress: 0 });
110
+ } catch (err) {
111
+ const errorMessage = err instanceof Error ? err.message : 'Failed to analyze story progressions.';
112
+ setError(errorMessage);
113
+ setSeriesState('preview');
114
+ setLoadingState({ isLoading: false, message: '', progress: 0 });
115
+ }
116
+ };
117
+
118
+ const handleProgressionSelect = async (selectedProgression: StoryProgression) => {
119
+ if (!comicSeries || comicSeries.iterations.length === 0) return;
120
+
121
+ setError(null);
122
+ setSeriesState('generating_continuation');
123
+
124
+ try {
125
+ const currentIteration = comicSeries.iterations[comicSeries.iterations.length - 1];
126
+ const newIteration = await generateComicContinuation(
127
+ currentIteration,
128
+ selectedProgression,
129
+ setLoadingState,
130
+ apiKey
131
+ );
132
+
133
+ // Update the series with the new iteration
134
+ const updatedSeries: ComicSeries = {
135
+ ...comicSeries,
136
+ iterations: [...comicSeries.iterations, newIteration],
137
+ totalPages: comicSeries.totalPages + newIteration.images.length,
138
+ lastUpdated: new Date()
139
+ };
140
+
141
+ setComicSeries(updatedSeries);
142
+ setSeriesState('series_complete');
143
+ setLoadingState({ isLoading: false, message: '', progress: 0 });
144
+ } catch (err) {
145
+ const errorMessage = err instanceof Error ? err.message : 'Failed to generate story continuation.';
146
+ setError(errorMessage);
147
+ setSeriesState('selecting_progression');
148
+ setLoadingState({ isLoading: false, message: '', progress: 0 });
149
+ }
150
+ };
151
+
152
+ const handleSkipContinuation = () => {
153
+ setSeriesState('preview');
154
+ };
155
+
156
+ const handleDownloadSingleComic = () => {
157
+ if (comicSeries && comicSeries.iterations.length > 0) {
158
+ createMangaPdf(comicSeries.iterations[0].images, comicSeries.seriesTitle);
159
+ }
160
+ };
161
+
162
+ const handleDownloadCompleteSeries = () => {
163
+ if (comicSeries) {
164
+ createComicSeriesPdf(comicSeries);
165
+ }
166
+ };
167
+
168
+ const handleContinueAgain = () => {
169
+ setSeriesState('preview');
170
+ setCurrentProgressions([]);
171
+ };
172
+
173
+ const getAllImages = (): string[] => {
174
+ if (!comicSeries) return [];
175
+ return comicSeries.iterations.flatMap(iteration => iteration.images);
176
+ };
177
+
178
+ const getCurrentTitle = (): string => {
179
+ if (!comicSeries || seriesState === 'preview') {
180
+ return title;
181
+ }
182
+ return `${comicSeries.seriesTitle} - Complete Series`;
183
+ };
184
+
185
+ // Render based on current state
186
+ const renderContent = () => {
187
+ if (loadingState.isLoading) {
188
+ return <LoadingIndicator loadingState={loadingState} />;
189
+ }
190
+
191
+ switch (seriesState) {
192
+ case 'selecting_progression':
193
+ return (
194
+ <StoryProgressionSelector
195
+ progressions={currentProgressions}
196
+ onProgressionSelect={handleProgressionSelect}
197
+ onSkip={handleSkipContinuation}
198
+ disabled={loadingState.isLoading}
199
+ currentIteration={comicSeries?.iterations[0]}
200
+ storyArcSummary={storyArcSummary}
201
+ />
202
+ );
203
+
204
+ case 'series_complete':
205
+ return (
206
+ <div className="space-y-8 animate-fade-in">
207
+ {/* Series Complete Header */}
208
+ <div className="glass-card p-8">
209
+ <div className="text-center">
210
+ <h2 className="text-4xl font-bold text-white mb-4">
211
+ 🎉 Series Complete!
212
+ </h2>
213
+ <p className="text-xl text-white/80 mb-2">
214
+ Your comic series "{comicSeries?.seriesTitle}" is ready
215
+ </p>
216
+ <p className="text-white/60">
217
+ {comicSeries?.iterations.length} chapters • {comicSeries?.totalPages} total pages
218
+ </p>
219
+ </div>
220
+ </div>
221
+
222
+ {/* Series Preview */}
223
+ <ComicSeriesPreview comicSeries={comicSeries!} />
224
+
225
+ {/* Performance Summary */}
226
+ {comicSeries?.performanceSummary && comicSeries.performanceSummary.length > 0 && (
227
+ <PerformanceSummary
228
+ timings={comicSeries.performanceSummary}
229
+ totalTime={comicSeries.totalGenerationTime || 0}
230
+ totalPages={comicSeries.totalPages}
231
+ />
232
+ )}
233
+
234
+ {/* Action Buttons */}
235
+ <div className="glass-card p-8">
236
+ <div className="flex flex-col sm:flex-row justify-center items-center gap-4">
237
+ <button
238
+ onClick={handleDownloadCompleteSeries}
239
+ className="btn-primary px-8 py-4 text-lg font-bold hover:scale-105 transition-all duration-300"
240
+ >
241
+ 📚 Download Complete Series PDF
242
+ </button>
243
+
244
+ <button
245
+ onClick={handleContinueAgain}
246
+ className="btn-secondary px-6 py-3 hover:scale-105 transition-all duration-300"
247
+ >
248
+ ➕ Continue Story Further
249
+ </button>
250
+
251
+ <button
252
+ onClick={onReset}
253
+ className="btn-secondary px-6 py-3 hover:scale-105 transition-all duration-300"
254
+ >
255
+ ✨ Create New Comic
256
+ </button>
257
+ </div>
258
+ </div>
259
+ </div>
260
+ );
261
+
262
+ default: // 'preview'
263
+ return (
264
+ <div className="space-y-8">
265
+ {/* Enhanced Comic Preview with Continue Option */}
266
+ <ComicPreview
267
+ images={comicSeries?.iterations[0]?.images || []}
268
+ title={getCurrentTitle()}
269
+ onReset={onReset}
270
+ storyArcSummary={storyArcSummary}
271
+ onDownloadClick={onDownloadClick}
272
+ />
273
+
274
+ {/* Performance Summary for single comic */}
275
+ {comicSeries?.performanceSummary && comicSeries.performanceSummary.length > 0 && (
276
+ <PerformanceSummary
277
+ timings={comicSeries.performanceSummary}
278
+ totalTime={comicSeries.totalGenerationTime || 0}
279
+ totalPages={comicSeries.totalPages}
280
+ />
281
+ )}
282
+
283
+ {/* Story Continuation Prompt */}
284
+ <div className="glass-card p-8 animate-fade-in">
285
+ <div className="text-center">
286
+ <h3 className="text-2xl font-bold text-white mb-4">
287
+ 📖 Continue Your Story?
288
+ </h3>
289
+ <p className="text-white/80 mb-6">
290
+ Your comic is complete, but every great story can continue!
291
+ Let AI analyze your story and suggest compelling directions to expand it into a series.
292
+ </p>
293
+
294
+ <div className="flex flex-col sm:flex-row justify-center items-center gap-4">
295
+ <button
296
+ onClick={handleContinueStory}
297
+ className="btn-primary px-8 py-4 text-lg font-bold hover:scale-105 transition-all duration-300"
298
+ >
299
+ 🚀 Explore Story Progressions
300
+ </button>
301
+
302
+ <button
303
+ onClick={handleDownloadSingleComic}
304
+ className="btn-secondary px-6 py-3 hover:scale-105 transition-all duration-300"
305
+ >
306
+ 📥 Download Current Comic Only
307
+ </button>
308
+ </div>
309
+ </div>
310
+ </div>
311
+ </div>
312
+ );
313
+ }
314
+ };
315
+
316
+ return (
317
+ <div className="space-y-8">
318
+ {error && (
319
+ <div className="glass-card border-red-400 bg-red-500/20 text-red-100 px-6 py-4 animate-fade-in" role="alert">
320
+ <div className="flex items-center">
321
+ <div className="flex-shrink-0">
322
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
323
+ <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
324
+ </svg>
325
+ </div>
326
+ <div className="ml-3">
327
+ <strong className="font-bold">Story Continuation Error: </strong>
328
+ <span>{error}</span>
329
+ </div>
330
+ </div>
331
+ </div>
332
+ )}
333
+
334
+ {renderContent()}
335
+ </div>
336
+ );
337
+ };
338
+
339
+ // Component to preview the complete series
340
+ const ComicSeriesPreview: React.FC<{ comicSeries: ComicSeries }> = ({ comicSeries }) => {
341
+ const [selectedIteration, setSelectedIteration] = useState(0);
342
+
343
+ return (
344
+ <div className="space-y-6">
345
+ {/* Series Navigation */}
346
+ <div className="glass-card p-6">
347
+ <h3 className="text-xl font-bold text-white mb-4 text-center">Series Chapters</h3>
348
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
349
+ {comicSeries.iterations.map((iteration, index) => (
350
+ <button
351
+ key={iteration.id}
352
+ onClick={() => setSelectedIteration(index)}
353
+ className={`p-4 rounded-lg transition-all duration-300 ${
354
+ selectedIteration === index
355
+ ? 'bg-white/20 border-2 border-white/50'
356
+ : 'bg-white/10 hover:bg-white/15 border-2 border-transparent'
357
+ }`}
358
+ >
359
+ <div className="text-white font-semibold">Chapter {index + 1}</div>
360
+ <div className="text-white/80 text-sm mt-1">{iteration.title}</div>
361
+ <div className="text-white/60 text-xs mt-1">{iteration.images.length} pages</div>
362
+ </button>
363
+ ))}
364
+ </div>
365
+ </div>
366
+
367
+ {/* Selected Chapter Preview */}
368
+ <div className="panel-grid">
369
+ {comicSeries.iterations[selectedIteration].images.map((imgData, index) => (
370
+ <div key={index} className="panel-item group">
371
+ <div className="relative overflow-hidden">
372
+ <img
373
+ src={`data:image/jpeg;base64,${imgData}`}
374
+ alt={`Chapter ${selectedIteration + 1} Page ${index + 1}`}
375
+ className="w-full h-auto object-contain transition-transform duration-500 group-hover:scale-105"
376
+ />
377
+ <div className="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
378
+ <div className="absolute bottom-4 left-4 right-4 transform translate-y-8 group-hover:translate-y-0 transition-transform duration-300">
379
+ <div className="bg-white/20 backdrop-blur-sm rounded-lg px-3 py-2">
380
+ <p className="text-white font-semibold text-sm">
381
+ Chapter {selectedIteration + 1} - Page {index + 1}
382
+ </p>
383
+ </div>
384
+ </div>
385
+ </div>
386
+ <div className="p-4">
387
+ <h3 className="text-white font-semibold">
388
+ Page {index + 1}
389
+ </h3>
390
+ <p className="text-white/70 text-sm mt-1">
391
+ {comicSeries.iterations[selectedIteration].title}
392
+ </p>
393
+ </div>
394
+ </div>
395
+ ))}
396
+ </div>
397
+
398
+ {/* Series Stats */}
399
+ <div className="glass-card p-6">
400
+ <div className="flex justify-center items-center gap-8 text-center">
401
+ <div>
402
+ <div className="text-2xl font-bold text-white">{comicSeries.iterations.length}</div>
403
+ <div className="text-white/70 text-sm">Chapters</div>
404
+ </div>
405
+ <div className="h-8 w-px bg-white/20"></div>
406
+ <div>
407
+ <div className="text-2xl font-bold text-white">{comicSeries.totalPages}</div>
408
+ <div className="text-white/70 text-sm">Total Pages</div>
409
+ </div>
410
+ <div className="h-8 w-px bg-white/20"></div>
411
+ <div>
412
+ <div className="text-2xl font-bold text-white">📚</div>
413
+ <div className="text-white/70 text-sm">Complete Series</div>
414
+ </div>
415
+ </div>
416
+ </div>
417
+ </div>
418
+ );
419
+ };
420
+
421
+ export default ComicSeriesManager;
components/EnhancedLoadingIndicator.tsx ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, memo } from 'react';
2
+ import LazyImage from './LazyImage';
3
+ import type { GenerationProgress } from '../types';
4
+
5
+ interface EnhancedLoadingIndicatorProps {
6
+ generationProgress: GenerationProgress;
7
+ }
8
+
9
+ const EnhancedLoadingIndicator: React.FC<EnhancedLoadingIndicatorProps> = memo(({ generationProgress }) => {
10
+ const [selectedSketchIndex, setSelectedSketchIndex] = useState(0);
11
+ const [selectedPageIndex, setSelectedPageIndex] = useState(0);
12
+
13
+ const getPhaseInfo = (phase: GenerationProgress['phase']) => {
14
+ switch (phase) {
15
+ case 'character_analysis':
16
+ return { icon: '🧠', title: 'Character Analysis', description: 'AI is analyzing your story and developing character personalities' };
17
+ case 'character_sketches':
18
+ return { icon: '🎨', title: 'Character Design', description: 'Creating visual designs for your main characters' };
19
+ case 'environment_sketches':
20
+ return { icon: '🏞️', title: 'Environment Design', description: 'Designing key locations and settings for your story' };
21
+ case 'script_writing':
22
+ return { icon: '📝', title: 'Script Writing', description: 'Writing professional manga script with panel layouts' };
23
+ case 'page_generation':
24
+ return { icon: '🖼️', title: 'Page Generation', description: 'Bringing your story to life with stunning artwork' };
25
+ case 'completion':
26
+ return { icon: '✨', title: 'Finalizing', description: 'Adding finishing touches and analyzing story arc' };
27
+ default:
28
+ return { icon: '🔄', title: 'Processing', description: 'Working on your comic...' };
29
+ }
30
+ };
31
+
32
+ const phaseInfo = getPhaseInfo(generationProgress.phase);
33
+
34
+ return (
35
+ <div className="min-h-screen flex items-center justify-center py-8">
36
+ <div className="max-w-6xl mx-auto px-4">
37
+ {/* Main Loading Header */}
38
+ <div className="glass-card p-8 mb-8 text-center animate-fade-in">
39
+ <div className="text-6xl mb-4 animate-pulse-slow">{phaseInfo.icon}</div>
40
+ <h2 className="text-3xl font-bold text-white mb-2">{phaseInfo.title}</h2>
41
+ <p className="text-white/80 text-lg mb-6">{generationProgress.message}</p>
42
+ <p className="text-white/60 text-sm mb-4">{phaseInfo.description}</p>
43
+
44
+ {/* Progress Bar */}
45
+ <div className="w-full bg-white/20 rounded-full h-6 mb-4 overflow-hidden">
46
+ <div
47
+ className="h-6 rounded-full transition-all duration-1000 ease-out bg-gradient-to-r from-gray-300 via-white to-gray-400 relative overflow-hidden"
48
+ style={{ width: `${generationProgress.progress}%` }}
49
+ >
50
+ <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-pulse"></div>
51
+ </div>
52
+ </div>
53
+ <div className="text-white/70 text-sm">
54
+ {generationProgress.progress}% Complete
55
+ </div>
56
+ </div>
57
+
58
+ {/* Character Sketches Display */}
59
+ {generationProgress.characterSketches && generationProgress.characterSketches.length > 0 && (
60
+ <div className="glass-card p-8 mb-8 animate-fade-in">
61
+ <h3 className="text-2xl font-bold text-white mb-6 text-center">
62
+ 🎭 Character Designs
63
+ </h3>
64
+
65
+ {/* Character Selection */}
66
+ <div className="flex justify-center mb-6">
67
+ <div className="flex gap-2 bg-white/10 backdrop-blur-sm rounded-xl p-3">
68
+ {generationProgress.characterSketches.map((character, index) => (
69
+ <button
70
+ key={index}
71
+ onClick={() => setSelectedSketchIndex(index)}
72
+ className={`px-4 py-2 rounded-lg transition-all duration-300 font-medium ${
73
+ selectedSketchIndex === index
74
+ ? 'bg-white/30 text-white scale-105 shadow-lg'
75
+ : 'bg-white/10 text-white/80 hover:bg-white/20 hover:scale-102'
76
+ }`}
77
+ >
78
+ {character.characterName}
79
+ </button>
80
+ ))}
81
+ </div>
82
+ </div>
83
+
84
+ {/* Selected Character Display */}
85
+ <div className="flex flex-col lg:flex-row gap-8 items-center">
86
+ <div className="flex-1 max-w-md">
87
+ <div className="glass rounded-xl p-4">
88
+ <LazyImage
89
+ src={generationProgress.characterSketches[selectedSketchIndex].sketchImage}
90
+ alt={generationProgress.characterSketches[selectedSketchIndex].characterName}
91
+ className="w-full rounded-lg shadow-lg"
92
+ quality="compressed"
93
+ />
94
+ </div>
95
+ </div>
96
+ <div className="flex-1 glass rounded-xl p-6">
97
+ <h4 className="text-2xl font-bold text-white mb-4 gradient-text">
98
+ {generationProgress.characterSketches[selectedSketchIndex].characterName}
99
+ </h4>
100
+ <p className="text-white/80 leading-relaxed text-sm">
101
+ {generationProgress.characterSketches[selectedSketchIndex].description}
102
+ </p>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ )}
107
+
108
+ {/* Environment Sketches Display */}
109
+ {generationProgress.environmentSketches && generationProgress.environmentSketches.length > 0 && (
110
+ <div className="glass-card p-8 mb-8 animate-fade-in">
111
+ <h3 className="text-2xl font-bold text-white mb-6 text-center">
112
+ 🏗️ Environment Designs
113
+ </h3>
114
+
115
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
116
+ {generationProgress.environmentSketches.map((environment, index) => (
117
+ <div key={index} className="glass-card hover:scale-102 transition-all duration-300">
118
+ <div className="aspect-video mb-4 overflow-hidden rounded-lg">
119
+ <LazyImage
120
+ src={environment.sketchImage}
121
+ alt={environment.environmentName}
122
+ className="w-full h-full object-cover"
123
+ quality="compressed"
124
+ />
125
+ </div>
126
+ <h4 className="text-lg font-bold text-white mb-2 gradient-text-subtle">
127
+ {environment.environmentName}
128
+ </h4>
129
+ <p className="text-white/70 text-sm leading-relaxed">
130
+ {environment.description}
131
+ </p>
132
+ </div>
133
+ ))}
134
+ </div>
135
+ </div>
136
+ )}
137
+
138
+ {/* Generated Pages Display */}
139
+ {generationProgress.generatedPages && generationProgress.generatedPages.length > 0 && (
140
+ <div className="glass-card p-8 mb-8 animate-fade-in">
141
+ <h3 className="text-2xl font-bold text-white mb-6 text-center">
142
+ 📚 Pages Generated
143
+ </h3>
144
+
145
+ {/* Page Navigation */}
146
+ <div className="flex justify-center mb-6">
147
+ <div className="flex gap-2 glass-card p-3 max-w-full overflow-x-auto">
148
+ {generationProgress.generatedPages.map((page, index) => (
149
+ <button
150
+ key={index}
151
+ onClick={() => setSelectedPageIndex(index)}
152
+ className={`px-4 py-2 rounded-lg transition-all duration-300 whitespace-nowrap font-medium ${
153
+ selectedPageIndex === index
154
+ ? 'bg-white/30 text-white scale-105 shadow-lg'
155
+ : 'bg-white/10 text-white/80 hover:bg-white/20 hover:scale-102'
156
+ }`}
157
+ >
158
+ Page {index + 1}
159
+ </button>
160
+ ))}
161
+ </div>
162
+ </div>
163
+
164
+ {/* Selected Page Display */}
165
+ <div className="text-center">
166
+ <div className="max-w-md mx-auto glass rounded-xl p-4">
167
+ <LazyImage
168
+ src={generationProgress.generatedPages[selectedPageIndex].image}
169
+ alt={generationProgress.generatedPages[selectedPageIndex].title}
170
+ className="w-full rounded-lg shadow-lg mb-4 hover:scale-105 transition-transform duration-300 cursor-pointer"
171
+ quality="compressed"
172
+ />
173
+ <h4 className="text-lg font-bold text-white gradient-text-subtle">
174
+ {generationProgress.generatedPages[selectedPageIndex].title}
175
+ </h4>
176
+ </div>
177
+ </div>
178
+
179
+ {/* Progress Summary */}
180
+ <div className="mt-6 text-center">
181
+ <p className="text-white/70">
182
+ Generated {generationProgress.generatedPages.length} page{generationProgress.generatedPages.length !== 1 ? 's' : ''} so far
183
+ </p>
184
+ </div>
185
+ </div>
186
+ )}
187
+
188
+ {/* Story Arc Summary Display */}
189
+ {generationProgress.storyArcSummary && (
190
+ <div className="glass-card p-8 animate-fade-in">
191
+ <h3 className="text-2xl font-bold text-white mb-6 text-center">
192
+ 📖 Story Arc Summary
193
+ </h3>
194
+
195
+ <div className="glass p-6 rounded-xl">
196
+ <div className="text-white/80 leading-relaxed text-sm space-y-4">
197
+ {generationProgress.storyArcSummary.split('\n\n').map((section, index) => (
198
+ <div key={index} className="bg-white/5 rounded-lg p-4">
199
+ <p className="whitespace-pre-wrap">{section.trim()}</p>
200
+ </div>
201
+ ))}
202
+ </div>
203
+ </div>
204
+ </div>
205
+ )}
206
+
207
+ {/* Spinner for ongoing processes */}
208
+ {generationProgress.progress < 100 && (
209
+ <div className="flex justify-center mt-8">
210
+ <div className="spinner"></div>
211
+ </div>
212
+ )}
213
+ </div>
214
+ </div>
215
+ );
216
+ });
217
+
218
+ export default EnhancedLoadingIndicator;
components/ExpandableImageModal.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect } from 'react';
2
+
3
+ interface ExpandableImageModalProps {
4
+ isOpen: boolean;
5
+ onClose: () => void;
6
+ imageData: string;
7
+ title: string;
8
+ description?: string;
9
+ }
10
+
11
+ const ExpandableImageModal: React.FC<ExpandableImageModalProps> = ({
12
+ isOpen,
13
+ onClose,
14
+ imageData,
15
+ title,
16
+ description
17
+ }) => {
18
+ // Handle escape key press
19
+ useEffect(() => {
20
+ const handleEscape = (event: KeyboardEvent) => {
21
+ if (event.key === 'Escape') {
22
+ onClose();
23
+ }
24
+ };
25
+
26
+ if (isOpen) {
27
+ document.addEventListener('keydown', handleEscape);
28
+ // Prevent body scrolling when modal is open
29
+ document.body.style.overflow = 'hidden';
30
+ }
31
+
32
+ return () => {
33
+ document.removeEventListener('keydown', handleEscape);
34
+ document.body.style.overflow = 'unset';
35
+ };
36
+ }, [isOpen, onClose]);
37
+
38
+ if (!isOpen) return null;
39
+
40
+ return (
41
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
42
+ {/* Backdrop */}
43
+ <div
44
+ className="absolute inset-0 bg-black/80 backdrop-blur-sm"
45
+ onClick={onClose}
46
+ />
47
+
48
+ {/* Modal Content */}
49
+ <div className="relative z-10 max-w-6xl max-h-full w-full glass-card p-6 animate-fade-in">
50
+ {/* Close Button */}
51
+ <button
52
+ onClick={onClose}
53
+ className="absolute top-4 right-4 z-20 w-10 h-10 bg-white/20 hover:bg-white/30 rounded-full flex items-center justify-center transition-all duration-300"
54
+ >
55
+ <svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
56
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
57
+ </svg>
58
+ </button>
59
+
60
+ {/* Title */}
61
+ <div className="mb-4">
62
+ <h2 className="text-2xl font-bold text-white">{title}</h2>
63
+ {description && (
64
+ <p className="text-white/70 mt-2">{description}</p>
65
+ )}
66
+ </div>
67
+
68
+ {/* Image Container */}
69
+ <div className="flex justify-center items-center max-h-[80vh] overflow-hidden rounded-lg">
70
+ <img
71
+ src={`data:image/jpeg;base64,${imageData}`}
72
+ alt={title}
73
+ className="max-w-full max-h-full object-contain rounded-lg shadow-lg"
74
+ style={{ maxHeight: 'calc(80vh - 100px)' }}
75
+ />
76
+ </div>
77
+
78
+ {/* Controls */}
79
+ <div className="flex justify-center mt-4 space-x-4">
80
+ <button
81
+ onClick={() => {
82
+ const link = document.createElement('a');
83
+ link.href = `data:image/jpeg;base64,${imageData}`;
84
+ link.download = `${title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.jpg`;
85
+ link.click();
86
+ }}
87
+ className="btn-secondary px-4 py-2 text-sm hover:scale-105 transition-all duration-300"
88
+ >
89
+ 📥 Download Image
90
+ </button>
91
+ <button
92
+ onClick={onClose}
93
+ className="btn-primary px-4 py-2 text-sm hover:scale-105 transition-all duration-300"
94
+ >
95
+ Close
96
+ </button>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ );
101
+ };
102
+
103
+ export default ExpandableImageModal;
components/ExplorationPrompt.tsx ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import type { ExplorationPrompt } from '../types';
3
+
4
+ const ExplorationPromptComponent: React.FC<ExplorationPrompt> = ({
5
+ showPrompt,
6
+ onContinue,
7
+ onFinish,
8
+ generationTime,
9
+ totalPages
10
+ }) => {
11
+ if (!showPrompt) return null;
12
+
13
+ const formatTime = (milliseconds: number): string => {
14
+ const seconds = Math.floor(milliseconds / 1000);
15
+ const minutes = Math.floor(seconds / 60);
16
+ const remainingSeconds = seconds % 60;
17
+
18
+ if (minutes > 0) {
19
+ return `${minutes}m ${remainingSeconds}s`;
20
+ }
21
+ return `${remainingSeconds}s`;
22
+ };
23
+
24
+ const handleDownloadFinish = () => {
25
+ // Trigger PDF download without any API key removal
26
+ onFinish(false);
27
+ };
28
+
29
+ return (
30
+ <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 animate-fade-in">
31
+ <div className="glass-card p-8 max-w-lg mx-4 animate-slide-in">
32
+ {/* Main Exploration Prompt */}
33
+ <div className="text-center mb-8">
34
+ <div className="text-4xl mb-4">🎉</div>
35
+ <h2 className="text-2xl font-bold text-white mb-4">
36
+ Comic Generation Complete!
37
+ </h2>
38
+ <div className="glass rounded-lg p-4 mb-6">
39
+ <div className="grid grid-cols-2 gap-4 text-center">
40
+ <div>
41
+ <div className="text-xl font-bold text-white">{totalPages}</div>
42
+ <div className="text-white/70 text-sm">Pages Created</div>
43
+ </div>
44
+ <div>
45
+ <div className="text-xl font-bold text-white">{formatTime(generationTime)}</div>
46
+ <div className="text-white/70 text-sm">Generation Time</div>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ <p className="text-white/80 mb-2">
51
+ Would you like to continue exploring story progressions?
52
+ </p>
53
+ <p className="text-white/60 text-sm">
54
+ You can create sequels, prequels, side stories, or alternate endings!
55
+ </p>
56
+ </div>
57
+
58
+ {/* Action Buttons */}
59
+ <div className="space-y-4">
60
+ <button
61
+ onClick={onContinue}
62
+ className="w-full btn-primary text-lg font-bold py-4 px-6 hover:scale-105 transition-all duration-300"
63
+ >
64
+ 🚀 Yes, Continue Exploring!
65
+ </button>
66
+
67
+ <button
68
+ onClick={handleDownloadFinish}
69
+ className="w-full btn-secondary py-3 px-6 hover:scale-105 transition-all duration-300"
70
+ >
71
+ 📥 No, Download & Finish
72
+ </button>
73
+ </div>
74
+
75
+ {/* Privacy Info */}
76
+ <div className="mt-6 glass p-3 rounded-lg">
77
+ <p className="text-white/60 text-xs text-center">
78
+ 🛡️ Your API key is stored locally in your browser only and never sent to our servers
79
+ </p>
80
+ </div>
81
+
82
+ {/* Info */}
83
+ <div className="mt-4 text-center">
84
+ <p className="text-white/50 text-xs">
85
+ 💡 Continuing will analyze your story and suggest new directions
86
+ </p>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ );
91
+ };
92
+
93
+ export default ExplorationPromptComponent;
components/LazyImage.tsx ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { createLazyLoader } from '../utils/imageOptimization';
3
+
4
+ interface LazyImageProps {
5
+ src: string;
6
+ alt: string;
7
+ className?: string;
8
+ fallbackClassName?: string;
9
+ onLoad?: () => void;
10
+ onError?: () => void;
11
+ quality?: 'compressed' | 'full';
12
+ }
13
+
14
+ const LazyImage: React.FC<LazyImageProps> = ({
15
+ src,
16
+ alt,
17
+ className = '',
18
+ fallbackClassName = 'bg-white/10 animate-pulse',
19
+ onLoad,
20
+ onError,
21
+ quality = 'compressed'
22
+ }) => {
23
+ const [isLoaded, setIsLoaded] = useState(false);
24
+ const [hasError, setHasError] = useState(false);
25
+ const imgRef = useRef<HTMLImageElement>(null);
26
+ const observerRef = useRef<IntersectionObserver | null>(null);
27
+
28
+ useEffect(() => {
29
+ if (!imgRef.current) return;
30
+
31
+ // Create lazy loader
32
+ observerRef.current = createLazyLoader({
33
+ rootMargin: '100px',
34
+ threshold: 0.1
35
+ }) as IntersectionObserver;
36
+
37
+ // Set up lazy loading
38
+ const img = imgRef.current;
39
+ img.dataset.src = `data:image/jpeg;base64,${src}`;
40
+ img.classList.add('lazy');
41
+
42
+ // Observe the image
43
+ if (observerRef.current) {
44
+ observerRef.current.observe(img);
45
+ }
46
+
47
+ return () => {
48
+ if (observerRef.current) {
49
+ observerRef.current.disconnect();
50
+ }
51
+ };
52
+ }, [src]);
53
+
54
+ const handleLoad = () => {
55
+ setIsLoaded(true);
56
+ setHasError(false);
57
+ onLoad?.();
58
+ };
59
+
60
+ const handleError = () => {
61
+ setHasError(true);
62
+ setIsLoaded(false);
63
+ onError?.();
64
+ };
65
+
66
+ return (
67
+ <div className="relative overflow-hidden">
68
+ {/* Loading placeholder */}
69
+ {!isLoaded && !hasError && (
70
+ <div className={`absolute inset-0 ${fallbackClassName} flex items-center justify-center`}>
71
+ <div className="flex flex-col items-center space-y-2">
72
+ <div className="spinner w-6 h-6"></div>
73
+ <span className="text-white/60 text-xs">Loading...</span>
74
+ </div>
75
+ </div>
76
+ )}
77
+
78
+ {/* Error placeholder */}
79
+ {hasError && (
80
+ <div className={`absolute inset-0 bg-red-500/20 flex items-center justify-center`}>
81
+ <div className="flex flex-col items-center space-y-2 text-red-300">
82
+ <svg className="w-8 h-8" fill="currentColor" viewBox="0 0 20 20">
83
+ <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
84
+ </svg>
85
+ <span className="text-xs">Failed to load</span>
86
+ </div>
87
+ </div>
88
+ )}
89
+
90
+ {/* Actual image */}
91
+ <img
92
+ ref={imgRef}
93
+ alt={alt}
94
+ className={`transition-opacity duration-500 ${
95
+ isLoaded ? 'opacity-100' : 'opacity-0'
96
+ } ${className}`}
97
+ onLoad={handleLoad}
98
+ onError={handleError}
99
+ loading="lazy"
100
+ />
101
+ </div>
102
+ );
103
+ };
104
+
105
+ export default LazyImage;
components/LoadingIndicator.tsx CHANGED
@@ -33,7 +33,7 @@ const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({ loadingState }) =>
33
  </div>
34
 
35
  {/* Progress Bar */}
36
- <div className="w-full bg-white/20 rounded-full h-6 overflow-hidden backdrop-blur-sm">
37
  <div
38
  className="h-6 rounded-full transition-all duration-1000 ease-out bg-gradient-to-r from-gray-300 via-white to-gray-400 relative overflow-hidden"
39
  style={{ width: `${loadingState.progress}%` }}
@@ -59,14 +59,14 @@ const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({ loadingState }) =>
59
  ].map((item, index) => (
60
  <div
61
  key={index}
62
- className={`p-4 rounded-lg border transition-all duration-500 ${
63
  item.active
64
- ? 'bg-white/20 border-white/40 text-white'
65
  : 'bg-white/5 border-white/10 text-white/50'
66
  }`}
67
  >
68
- <div className="text-2xl mb-2">{item.icon}</div>
69
- <div className="text-sm font-medium">{item.step}</div>
70
  </div>
71
  ))}
72
  </div>
@@ -74,12 +74,14 @@ const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({ loadingState }) =>
74
 
75
  {/* Footer */}
76
  <div className="pt-6 border-t border-white/10">
77
- <p className="text-sm text-white/60">
78
- Powered by advanced AI • Crafting your unique visual story
79
- </p>
80
- <p className="text-xs text-white/40 mt-2">
81
- Each page is being carefully composed with professional comic techniques
82
- </p>
 
 
83
  </div>
84
  </div>
85
  </div>
 
33
  </div>
34
 
35
  {/* Progress Bar */}
36
+ <div className="w-full bg-white/20 rounded-full h-6 overflow-hidden">
37
  <div
38
  className="h-6 rounded-full transition-all duration-1000 ease-out bg-gradient-to-r from-gray-300 via-white to-gray-400 relative overflow-hidden"
39
  style={{ width: `${loadingState.progress}%` }}
 
59
  ].map((item, index) => (
60
  <div
61
  key={index}
62
+ className={`glass rounded-xl p-4 transition-all duration-500 ${
63
  item.active
64
+ ? 'bg-white/20 border-white/40 text-white scale-105 shadow-lg'
65
  : 'bg-white/5 border-white/10 text-white/50'
66
  }`}
67
  >
68
+ <div className="text-3xl mb-3 ${item.active ? 'animate-pulse-slow' : ''}">{item.icon}</div>
69
+ <div className="text-sm font-semibold">{item.step}</div>
70
  </div>
71
  ))}
72
  </div>
 
74
 
75
  {/* Footer */}
76
  <div className="pt-6 border-t border-white/10">
77
+ <div className="glass-card p-4 text-center">
78
+ <p className="text-sm text-white/70 font-medium">
79
+ ⚡ Powered by Google Gemini 2.5 Flash • Professional Comic Creation
80
+ </p>
81
+ <p className="text-xs text-white/50 mt-2">
82
+ Each page is carefully composed with advanced AI artistry
83
+ </p>
84
+ </div>
85
  </div>
86
  </div>
87
  </div>
components/PerformanceSummary.tsx ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { memo } from 'react';
2
+ import type { PerformanceTiming } from '../types';
3
+
4
+ interface PerformanceSummaryProps {
5
+ timings: PerformanceTiming[];
6
+ totalTime: number;
7
+ totalPages: number;
8
+ }
9
+
10
+ const PerformanceSummary: React.FC<PerformanceSummaryProps> = memo(({
11
+ timings,
12
+ totalTime,
13
+ totalPages
14
+ }) => {
15
+ if (timings.length === 0) return null;
16
+
17
+ const formatTime = (ms: number): string => {
18
+ if (ms >= 60000) {
19
+ const minutes = Math.floor(ms / 60000);
20
+ const seconds = Math.floor((ms % 60000) / 1000);
21
+ return `${minutes}m ${seconds}s`;
22
+ }
23
+ return `${(ms / 1000).toFixed(1)}s`;
24
+ };
25
+
26
+ const getPerformanceClass = (duration: number, stepName: string): string => {
27
+ // Define performance thresholds based on step type
28
+ let threshold = { good: 10000, poor: 30000 }; // default
29
+
30
+ if (stepName.includes('analysis') || stepName.includes('script')) {
31
+ threshold = { good: 15000, poor: 45000 };
32
+ } else if (stepName.includes('generation') || stepName.includes('page')) {
33
+ threshold = { good: 60000, poor: 120000 };
34
+ } else if (stepName.includes('sketch')) {
35
+ threshold = { good: 20000, poor: 60000 };
36
+ }
37
+
38
+ if (duration <= threshold.good) return 'perf-excellent';
39
+ if (duration <= threshold.poor) return 'perf-good';
40
+ return 'perf-poor';
41
+ };
42
+
43
+ const averageTimePerPage = totalPages > 0 ? totalTime / totalPages : 0;
44
+
45
+ return (
46
+ <div className="glass-card p-6 animate-fade-in">
47
+ <h3 className="text-lg font-bold text-white mb-4 flex items-center">
48
+ 📊 Performance Summary
49
+ </h3>
50
+
51
+ {/* Overall Stats */}
52
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
53
+ <div className="glass rounded-lg p-4 text-center">
54
+ <div className="text-2xl font-bold text-white">{formatTime(totalTime)}</div>
55
+ <div className="text-white/70 text-sm">Total Time</div>
56
+ </div>
57
+ <div className="glass rounded-lg p-4 text-center">
58
+ <div className="text-2xl font-bold text-white">{totalPages}</div>
59
+ <div className="text-white/70 text-sm">Pages Generated</div>
60
+ </div>
61
+ <div className="glass rounded-lg p-4 text-center">
62
+ <div className="text-2xl font-bold text-white">{formatTime(averageTimePerPage)}</div>
63
+ <div className="text-white/70 text-sm">Avg per Page</div>
64
+ </div>
65
+ </div>
66
+
67
+ {/* Step Breakdown */}
68
+ <div className="space-y-2">
69
+ <h4 className="text-sm font-semibold text-white/90 mb-3">Generation Steps</h4>
70
+ {timings.map((timing, index) => {
71
+ const percentage = totalTime > 0 ? (timing.duration / totalTime) * 100 : 0;
72
+ const perfClass = getPerformanceClass(timing.duration, timing.stepName);
73
+
74
+ return (
75
+ <div key={index} className="flex items-center justify-between py-2 px-3 glass rounded-lg">
76
+ <div className="flex-1">
77
+ <div className="flex items-center justify-between">
78
+ <span className="text-white/90 text-sm font-medium">
79
+ {timing.stepName.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
80
+ </span>
81
+ <div className="flex items-center space-x-2">
82
+ <span className={`text-sm font-semibold ${perfClass}`}>
83
+ {formatTime(timing.duration)}
84
+ </span>
85
+ <span className="text-white/50 text-xs">
86
+ {percentage.toFixed(1)}%
87
+ </span>
88
+ </div>
89
+ </div>
90
+ {/* Progress bar */}
91
+ <div className="mt-2 w-full bg-white/10 rounded-full h-1.5">
92
+ <div
93
+ className={`h-1.5 rounded-full transition-all duration-500 ${
94
+ perfClass === 'perf-excellent' ? 'bg-green-500' :
95
+ perfClass === 'perf-good' ? 'bg-yellow-500' : 'bg-red-500'
96
+ }`}
97
+ style={{ width: `${percentage}%` }}
98
+ />
99
+ </div>
100
+ </div>
101
+ </div>
102
+ );
103
+ })}
104
+ </div>
105
+
106
+ {/* Performance Tips */}
107
+ <div className="mt-4 p-3 bg-white/5 rounded-lg">
108
+ <h5 className="text-xs font-semibold text-white/80 mb-2">💡 Performance Tips</h5>
109
+ <div className="text-xs text-white/60 space-y-1">
110
+ <div>• Generation time depends on story complexity and API response time</div>
111
+ <div>• Character sketches improve consistency but add generation time</div>
112
+ <div>• Simpler stories with fewer characters generate faster</div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ );
117
+ });
118
+
119
+ export default PerformanceSummary;
components/StoryInput.tsx CHANGED
@@ -12,6 +12,8 @@ interface StoryInputProps {
12
  onStyleChange: (style: MangaStyle) => void;
13
  onGenerate: () => void;
14
  disabled: boolean;
 
 
15
  }
16
 
17
  const styles: { name: MangaStyle; description: string; color: string }[] = [
@@ -80,7 +82,7 @@ const storyTemplates: StoryTemplate[] = [
80
  }
81
  ];
82
 
83
- const StoryInput: React.FC<StoryInputProps> = ({ title, onTitleChange, story, onStoryChange, author, onAuthorChange, style, onStyleChange, onGenerate, disabled }) => {
84
  const [selectedTemplate, setSelectedTemplate] = useState<string>('custom');
85
  const [showAdvanced, setShowAdvanced] = useState(false);
86
 
@@ -191,6 +193,35 @@ const StoryInput: React.FC<StoryInputProps> = ({ title, onTitleChange, story, on
191
  />
192
  </div>
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  <div className="space-y-4">
195
  <h3 className="text-lg font-semibold text-white/90 flex items-center">
196
  🎨 Art Style
 
12
  onStyleChange: (style: MangaStyle) => void;
13
  onGenerate: () => void;
14
  disabled: boolean;
15
+ includeDialogue: boolean;
16
+ onDialogueToggle: (include: boolean) => void;
17
  }
18
 
19
  const styles: { name: MangaStyle; description: string; color: string }[] = [
 
82
  }
83
  ];
84
 
85
+ const StoryInput: React.FC<StoryInputProps> = ({ title, onTitleChange, story, onStoryChange, author, onAuthorChange, style, onStyleChange, onGenerate, disabled, includeDialogue, onDialogueToggle }) => {
86
  const [selectedTemplate, setSelectedTemplate] = useState<string>('custom');
87
  const [showAdvanced, setShowAdvanced] = useState(false);
88
 
 
193
  />
194
  </div>
195
 
196
+ {/* Dialogue Toggle Section */}
197
+ <div className="space-y-4">
198
+ <h3 className="text-lg font-semibold text-white/90 flex items-center">
199
+ 💬 Text Options
200
+ </h3>
201
+ <div className="glass-card p-4">
202
+ <div className="flex items-center justify-between">
203
+ <div className="flex-1">
204
+ <h4 className="text-white font-semibold mb-1">Include Dialogue & Text</h4>
205
+ <p className="text-white/70 text-sm">
206
+ {includeDialogue
207
+ ? 'AI will generate dialogue and text within speech bubbles'
208
+ : 'Generate visual panels only - you can add text manually later'}
209
+ </p>
210
+ </div>
211
+ <label className="relative inline-flex items-center cursor-pointer ml-4">
212
+ <input
213
+ type="checkbox"
214
+ checked={includeDialogue}
215
+ onChange={(e) => onDialogueToggle(e.target.checked)}
216
+ disabled={disabled}
217
+ className="sr-only peer"
218
+ />
219
+ <div className="w-14 h-8 bg-white/20 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-6 peer-checked:after:border-white after:content-[''] after:absolute after:top-1 after:left-1 after:bg-white after:rounded-full after:h-6 after:w-6 after:transition-all peer-checked:bg-blue-600"></div>
220
+ </label>
221
+ </div>
222
+ </div>
223
+ </div>
224
+
225
  <div className="space-y-4">
226
  <h3 className="text-lg font-semibold text-white/90 flex items-center">
227
  🎨 Art Style
components/StoryProgressionSelector.tsx ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import type { StoryProgression, StoryArcSummary, ComicIteration } from '../types';
3
+
4
+ interface StoryProgressionSelectorProps {
5
+ progressions: StoryProgression[];
6
+ onProgressionSelect: (progression: StoryProgression) => void;
7
+ onSkip: () => void;
8
+ disabled: boolean;
9
+ currentIteration?: ComicIteration;
10
+ storyArcSummary?: StoryArcSummary | null;
11
+ }
12
+
13
+ const getProgressionIcon = (direction: StoryProgression['direction']): string => {
14
+ switch (direction) {
15
+ case 'sequel': return '⏭️';
16
+ case 'prequel': return '⏮️';
17
+ case 'side_story': return '🔀';
18
+ case 'alternate_ending': return '🔄';
19
+ default: return '📖';
20
+ }
21
+ };
22
+
23
+ const getProgressionColor = (direction: StoryProgression['direction']): string => {
24
+ switch (direction) {
25
+ case 'sequel': return 'from-blue-500 to-cyan-500';
26
+ case 'prequel': return 'from-purple-500 to-violet-500';
27
+ case 'side_story': return 'from-green-500 to-emerald-500';
28
+ case 'alternate_ending': return 'from-orange-500 to-red-500';
29
+ default: return 'from-gray-500 to-gray-600';
30
+ }
31
+ };
32
+
33
+ const getDirectionLabel = (direction: StoryProgression['direction']): string => {
34
+ switch (direction) {
35
+ case 'sequel': return 'Sequel';
36
+ case 'prequel': return 'Prequel';
37
+ case 'side_story': return 'Side Story';
38
+ case 'alternate_ending': return 'Alternate Ending';
39
+ default: return 'Story';
40
+ }
41
+ };
42
+
43
+ const StoryProgressionSelector: React.FC<StoryProgressionSelectorProps> = ({
44
+ progressions,
45
+ onProgressionSelect,
46
+ onSkip,
47
+ disabled,
48
+ currentIteration,
49
+ storyArcSummary
50
+ }) => {
51
+ const [selectedProgression, setSelectedProgression] = useState<StoryProgression | null>(null);
52
+
53
+ const handleSelect = (progression: StoryProgression) => {
54
+ if (!disabled) {
55
+ setSelectedProgression(progression);
56
+ }
57
+ };
58
+
59
+ const handleConfirm = () => {
60
+ if (selectedProgression) {
61
+ onProgressionSelect(selectedProgression);
62
+ }
63
+ };
64
+
65
+ return (
66
+ <div className="space-y-8 animate-fade-in">
67
+ {/* Header Section */}
68
+ <div className="glass-card p-8">
69
+ <div className="text-center">
70
+ <h2 className="text-4xl font-bold text-white mb-4">
71
+ 📚 Continue Your Story
72
+ </h2>
73
+ <p className="text-xl text-white/80 mb-2">
74
+ Your comic is complete! Would you like to continue the adventure?
75
+ </p>
76
+ <p className="text-white/60">
77
+ Choose one of these professionally crafted story progressions to expand your comic into a series
78
+ </p>
79
+ </div>
80
+ </div>
81
+
82
+ {/* Story Till Now Section - Compact */}
83
+ {storyArcSummary && (
84
+ <div className="glass-card p-6">
85
+ <h3 className="text-lg font-bold text-white mb-4 text-center gradient-text">
86
+ 📖 Story Till Now
87
+ </h3>
88
+
89
+ {/* Compact Single Row Layout */}
90
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
91
+ {/* Theme */}
92
+ <div className="glass rounded-lg p-4">
93
+ <div className="flex items-center space-x-2 mb-2">
94
+ <span className="text-xl">🎯</span>
95
+ <h4 className="text-sm font-bold text-white">Theme</h4>
96
+ </div>
97
+ <p className="text-white/70 text-xs leading-tight">
98
+ {storyArcSummary.overallTheme.length > 80
99
+ ? `${storyArcSummary.overallTheme.slice(0, 80)}...`
100
+ : storyArcSummary.overallTheme}
101
+ </p>
102
+ </div>
103
+
104
+ {/* Key Events */}
105
+ <div className="glass rounded-lg p-4">
106
+ <div className="flex items-center space-x-2 mb-2">
107
+ <span className="text-xl">🎬</span>
108
+ <h4 className="text-sm font-bold text-white">Key Events</h4>
109
+ </div>
110
+ <div className="space-y-1">
111
+ {storyArcSummary.keyEvents.slice(0, 2).map((event, index) => (
112
+ <div key={index} className="text-white/70 text-xs flex items-start">
113
+ <span className="text-white/50 mr-1 mt-0.5">•</span>
114
+ <span>{event.length > 40 ? `${event.slice(0, 40)}...` : event}</span>
115
+ </div>
116
+ ))}
117
+ {storyArcSummary.keyEvents.length > 2 && (
118
+ <div className="text-white/50 text-xs italic">
119
+ +{storyArcSummary.keyEvents.length - 2} more
120
+ </div>
121
+ )}
122
+ </div>
123
+ </div>
124
+
125
+ {/* Character Growth */}
126
+ <div className="glass rounded-lg p-4">
127
+ <div className="flex items-center space-x-2 mb-2">
128
+ <span className="text-xl">👥</span>
129
+ <h4 className="text-sm font-bold text-white">Growth</h4>
130
+ </div>
131
+ <div className="space-y-1">
132
+ {storyArcSummary.characterDevelopment.slice(0, 2).map((dev, index) => (
133
+ <div key={index} className="text-white/70 text-xs flex items-start">
134
+ <span className="text-white/50 mr-1 mt-0.5">•</span>
135
+ <span>{dev.length > 40 ? `${dev.slice(0, 40)}...` : dev}</span>
136
+ </div>
137
+ ))}
138
+ {storyArcSummary.characterDevelopment.length > 2 && (
139
+ <div className="text-white/50 text-xs italic">
140
+ +{storyArcSummary.characterDevelopment.length - 2} more
141
+ </div>
142
+ )}
143
+ </div>
144
+ </div>
145
+ </div>
146
+
147
+ {/* Current Story Stats - Compact */}
148
+ {currentIteration && (
149
+ <div className="glass rounded-lg p-4">
150
+ <div className="flex justify-center items-center gap-6 text-center">
151
+ <div>
152
+ <div className="text-xl font-bold text-white">{currentIteration.images.length}</div>
153
+ <div className="text-white/70 text-xs font-medium">Pages</div>
154
+ </div>
155
+ <div className="h-6 w-px bg-white/20"></div>
156
+ <div>
157
+ <div className="text-xl font-bold text-white">{currentIteration.characters.length}</div>
158
+ <div className="text-white/70 text-xs font-medium">Characters</div>
159
+ </div>
160
+ <div className="h-6 w-px bg-white/20"></div>
161
+ <div>
162
+ <div className="text-xl font-bold text-white">{currentIteration.pages.length}</div>
163
+ <div className="text-white/70 text-xs font-medium">Scenes</div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ )}
168
+ </div>
169
+ )}
170
+
171
+ {/* Progression Options */}
172
+ <div className="space-y-6">
173
+ <h3 className="text-2xl font-bold text-white text-center">
174
+ 🎭 Story Progression Options
175
+ </h3>
176
+
177
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
178
+ {progressions.map((progression, index) => (
179
+ <div
180
+ key={progression.id}
181
+ onClick={() => handleSelect(progression)}
182
+ className={`progression-card cursor-pointer ${
183
+ selectedProgression?.id === progression.id
184
+ ? 'selected ring-2 ring-white/50 scale-105'
185
+ : 'hover:bg-white/20 hover:scale-102'
186
+ } ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
187
+ >
188
+ {/* Header */}
189
+ <div className="flex items-center justify-between mb-4">
190
+ <div className={`px-3 py-1 rounded-full text-sm font-semibold bg-gradient-to-r ${getProgressionColor(progression.direction)} text-white`}>
191
+ {getProgressionIcon(progression.direction)} {getDirectionLabel(progression.direction)}
192
+ </div>
193
+ <div className="text-white/60 text-sm">
194
+ ~{progression.estimatedPages} pages
195
+ </div>
196
+ </div>
197
+
198
+ {/* Title */}
199
+ <h4 className="text-xl font-bold text-white mb-3">
200
+ {progression.title}
201
+ </h4>
202
+
203
+ {/* Description */}
204
+ <p className="text-white/80 text-sm mb-4 leading-relaxed">
205
+ {progression.description}
206
+ </p>
207
+
208
+ {/* Synopsis */}
209
+ <div className="bg-white/10 rounded-lg p-3 mb-4">
210
+ <h5 className="text-white font-semibold text-sm mb-2">📖 Synopsis</h5>
211
+ <p className="text-white/70 text-xs leading-relaxed">
212
+ {progression.synopsis}
213
+ </p>
214
+ </div>
215
+
216
+ {/* Thematic Focus */}
217
+ <div className="mb-4">
218
+ <h5 className="text-white font-semibold text-sm mb-2">🎯 Focus</h5>
219
+ <p className="text-white/70 text-xs">
220
+ {progression.thematicFocus}
221
+ </p>
222
+ </div>
223
+
224
+ {/* Plot Hooks */}
225
+ <div className="mb-4">
226
+ <h5 className="text-white font-semibold text-sm mb-2">🪝 Key Elements</h5>
227
+ <div className="flex flex-wrap gap-1">
228
+ {progression.plotHooks.slice(0, 3).map((hook, hookIndex) => (
229
+ <span
230
+ key={hookIndex}
231
+ className="bg-white/20 text-white/80 text-xs px-2 py-1 rounded-full"
232
+ >
233
+ {hook.length > 20 ? `${hook.slice(0, 20)}...` : hook}
234
+ </span>
235
+ ))}
236
+ </div>
237
+ </div>
238
+
239
+ {/* New Characters */}
240
+ {progression.newCharacters && progression.newCharacters.length > 0 && (
241
+ <div>
242
+ <h5 className="text-white font-semibold text-sm mb-2">👥 New Characters</h5>
243
+ <div className="flex flex-wrap gap-1">
244
+ {progression.newCharacters.slice(0, 3).map((character, charIndex) => (
245
+ <span
246
+ key={charIndex}
247
+ className="bg-gradient-to-r from-yellow-400/20 to-orange-400/20 text-yellow-100 text-xs px-2 py-1 rounded-full"
248
+ >
249
+ {character}
250
+ </span>
251
+ ))}
252
+ {progression.newCharacters.length > 3 && (
253
+ <span className="text-white/60 text-xs px-2 py-1">
254
+ +{progression.newCharacters.length - 3} more
255
+ </span>
256
+ )}
257
+ </div>
258
+ </div>
259
+ )}
260
+
261
+ {/* Selection Indicator */}
262
+ {selectedProgression?.id === progression.id && (
263
+ <div className="absolute top-4 right-4 w-6 h-6 bg-white rounded-full flex items-center justify-center">
264
+ <svg className="w-4 h-4 text-green-600" fill="currentColor" viewBox="0 0 20 20">
265
+ <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
266
+ </svg>
267
+ </div>
268
+ )}
269
+ </div>
270
+ ))}
271
+ </div>
272
+ </div>
273
+
274
+ {/* Action Buttons */}
275
+ <div className="glass-card p-8">
276
+ <div className="flex flex-col sm:flex-row justify-center items-center gap-4">
277
+ <button
278
+ onClick={handleConfirm}
279
+ disabled={disabled || !selectedProgression}
280
+ className={`btn-primary px-8 py-4 text-lg font-bold disabled:opacity-50 disabled:cursor-not-allowed ${
281
+ !disabled && selectedProgression ? 'hover:scale-105' : ''
282
+ } transition-all duration-300`}
283
+ >
284
+ {disabled ? (
285
+ <span className="flex items-center">
286
+ <div className="spinner mr-3"></div>
287
+ Generating Continuation...
288
+ </span>
289
+ ) : selectedProgression ? (
290
+ <span className="flex items-center">
291
+ 🚀 Continue with "{selectedProgression.title}"
292
+ </span>
293
+ ) : (
294
+ <span>Select a Progression to Continue</span>
295
+ )}
296
+ </button>
297
+
298
+ <button
299
+ onClick={onSkip}
300
+ disabled={disabled}
301
+ className={`btn-secondary px-6 py-3 ${
302
+ !disabled ? 'hover:scale-105' : 'opacity-50 cursor-not-allowed'
303
+ } transition-all duration-300`}
304
+ >
305
+ 📥 Skip & Download Current Comic
306
+ </button>
307
+ </div>
308
+ </div>
309
+
310
+ {/* Info Section */}
311
+ <div className="glass-card p-6">
312
+ <div className="text-center">
313
+ <h4 className="text-white font-semibold mb-2">🎨 What happens next?</h4>
314
+ <p className="text-white/70 text-sm">
315
+ When you select a progression, AI will generate a new comic chapter that continues your story.
316
+ All chapters will be compiled into a complete series PDF for download.
317
+ </p>
318
+ </div>
319
+ </div>
320
+ </div>
321
+ );
322
+ };
323
+
324
+ export default StoryProgressionSelector;
index.css CHANGED
@@ -1,13 +1,41 @@
1
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
2
 
3
  :root {
4
- --primary-gradient: linear-gradient(135deg, #1a1a1a 0%, #333333 100%);
5
- --secondary-gradient: linear-gradient(135deg, #444444 0%, #666666 100%);
6
- --accent-gradient: linear-gradient(135deg, #2a2a2a 0%, #1a1a1a 100%);
7
- --dark-gradient: linear-gradient(135deg, #000000 0%, #1a1a1a 100%);
8
- --glass-bg: rgba(255, 255, 255, 0.1);
9
- --glass-border: rgba(255, 255, 255, 0.2);
10
- --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  }
12
 
13
  * {
@@ -18,34 +46,53 @@ body {
18
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
19
  margin: 0;
20
  padding: 0;
21
- background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 25%, #2a2a2a 75%, #1a1a1a 100%);
22
  background-attachment: fixed;
23
  min-height: 100vh;
 
 
 
24
  }
25
 
26
- /* Glass morphism base styles */
27
  .glass {
28
  background: var(--glass-bg);
29
- backdrop-filter: blur(16px);
30
- -webkit-backdrop-filter: blur(16px);
31
- border-radius: 20px;
32
  border: 1px solid var(--glass-border);
33
  box-shadow: var(--glass-shadow);
 
34
  }
35
 
36
  .glass-card {
37
- background: rgba(255, 255, 255, 0.15);
38
- backdrop-filter: blur(20px);
39
- -webkit-backdrop-filter: blur(20px);
40
- border-radius: 16px;
41
- border: 1px solid rgba(255, 255, 255, 0.3);
42
- box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
43
- transition: all 0.3s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
 
46
  .glass-card:hover {
47
- transform: translateY(-4px);
48
- box-shadow: 0 16px 40px 0 rgba(31, 38, 135, 0.5);
 
 
49
  }
50
 
51
  /* Animation utilities */
@@ -92,59 +139,102 @@ body {
92
  animation: pulse 2s infinite;
93
  }
94
 
95
- /* Modern button styles */
96
  .btn-primary {
97
  background: var(--primary-gradient);
98
  border: none;
99
- border-radius: 12px;
100
  color: white;
101
  font-weight: 600;
102
- padding: 12px 24px;
103
- transition: all 0.3s ease;
104
- box-shadow: 0 4px 15px 0 rgba(31, 38, 135, 0.3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  }
106
 
107
  .btn-primary:hover {
108
  transform: translateY(-2px);
109
- box-shadow: 0 8px 25px 0 rgba(31, 38, 135, 0.4);
 
 
 
 
 
 
 
 
 
110
  }
111
 
112
  .btn-secondary {
113
- background: rgba(255, 255, 255, 0.2);
114
- backdrop-filter: blur(10px);
115
- border: 1px solid rgba(255, 255, 255, 0.3);
116
- border-radius: 12px;
117
  color: white;
118
- font-weight: 600;
119
- padding: 12px 24px;
120
- transition: all 0.3s ease;
 
 
121
  }
122
 
123
  .btn-secondary:hover {
124
- background: rgba(255, 255, 255, 0.3);
 
125
  transform: translateY(-2px);
126
  }
127
 
128
- /* Modern input styles */
 
 
 
 
129
  .input-glass {
130
- background: rgba(255, 255, 255, 0.2);
131
- backdrop-filter: blur(10px);
132
- border: 1px solid rgba(255, 255, 255, 0.3);
133
- border-radius: 12px;
134
  color: white;
135
- padding: 12px 16px;
136
- transition: all 0.3s ease;
 
 
 
137
  }
138
 
139
  .input-glass::placeholder {
140
- color: rgba(255, 255, 255, 0.7);
 
 
 
 
 
 
141
  }
142
 
143
  .input-glass:focus {
144
- outline: none;
145
- border-color: rgba(255, 255, 255, 0.5);
146
- background: rgba(255, 255, 255, 0.3);
147
- box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
 
 
 
148
  }
149
 
150
  /* Loading spinner */
@@ -162,6 +252,51 @@ body {
162
  100% { transform: rotate(360deg); }
163
  }
164
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  /* Panel composition grid */
166
  .panel-grid {
167
  display: grid;
@@ -194,9 +329,22 @@ body {
194
  }
195
  }
196
 
197
- /* Gradient text */
198
  .gradient-text {
199
- background: linear-gradient(135deg, #ffffff 0%, #cccccc 100%);
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  -webkit-background-clip: text;
201
  -webkit-text-fill-color: transparent;
202
  background-clip: text;
@@ -222,4 +370,180 @@ body {
222
  .template-card.selected {
223
  background: rgba(255, 255, 255, 0.3);
224
  border-color: rgba(255, 255, 255, 0.5);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  }
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
2
 
3
  :root {
4
+ /* Modern Black-to-White Gradient System */
5
+ --bg-gradient: linear-gradient(135deg, #000000 0%, #1a1a1a 25%, #2d2d2d 75%, #1a1a1a 100%);
6
+ --primary-gradient: linear-gradient(135deg, #000000 0%, #2d2d2d 50%, #4a4a4a 100%);
7
+ --secondary-gradient: linear-gradient(135deg, #1a1a1a 0%, #3d3d3d 100%);
8
+ --accent-gradient: linear-gradient(135deg, #2d2d2d 0%, #5a5a5a 100%);
9
+ --text-gradient: linear-gradient(135deg, #ffffff 0%, #e5e5e5 50%, #cccccc 100%);
10
+
11
+ /* Glass Morphism - Enhanced */
12
+ --glass-bg: rgba(255, 255, 255, 0.08);
13
+ --glass-bg-strong: rgba(255, 255, 255, 0.12);
14
+ --glass-border: rgba(255, 255, 255, 0.16);
15
+ --glass-border-strong: rgba(255, 255, 255, 0.24);
16
+ --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
17
+ --glass-shadow-strong: 0 16px 40px rgba(0, 0, 0, 0.6);
18
+
19
+ /* Interactive States */
20
+ --hover-bg: rgba(255, 255, 255, 0.16);
21
+ --active-bg: rgba(255, 255, 255, 0.20);
22
+ --focus-ring: rgba(255, 255, 255, 0.24);
23
+
24
+ /* Modern Spacing Scale */
25
+ --space-xs: 0.25rem;
26
+ --space-sm: 0.5rem;
27
+ --space-md: 1rem;
28
+ --space-lg: 1.5rem;
29
+ --space-xl: 2rem;
30
+ --space-2xl: 3rem;
31
+ --space-3xl: 4rem;
32
+
33
+ /* Border Radius Scale */
34
+ --radius-sm: 8px;
35
+ --radius-md: 12px;
36
+ --radius-lg: 16px;
37
+ --radius-xl: 20px;
38
+ --radius-2xl: 24px;
39
  }
40
 
41
  * {
 
46
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
47
  margin: 0;
48
  padding: 0;
49
+ background: var(--bg-gradient);
50
  background-attachment: fixed;
51
  min-height: 100vh;
52
+ font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
53
+ -webkit-font-smoothing: antialiased;
54
+ -moz-osx-font-smoothing: grayscale;
55
  }
56
 
57
+ /* Modern Glass Morphism System */
58
  .glass {
59
  background: var(--glass-bg);
60
+ backdrop-filter: blur(20px);
61
+ -webkit-backdrop-filter: blur(20px);
62
+ border-radius: var(--radius-xl);
63
  border: 1px solid var(--glass-border);
64
  box-shadow: var(--glass-shadow);
65
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
66
  }
67
 
68
  .glass-card {
69
+ background: var(--glass-bg-strong);
70
+ backdrop-filter: blur(24px);
71
+ -webkit-backdrop-filter: blur(24px);
72
+ border-radius: var(--radius-lg);
73
+ border: 1px solid var(--glass-border-strong);
74
+ box-shadow: var(--glass-shadow-strong);
75
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
76
+ position: relative;
77
+ overflow: hidden;
78
+ }
79
+
80
+ .glass-card::before {
81
+ content: '';
82
+ position: absolute;
83
+ top: 0;
84
+ left: 0;
85
+ right: 0;
86
+ height: 1px;
87
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
88
+ opacity: 0.6;
89
  }
90
 
91
  .glass-card:hover {
92
+ transform: translateY(-2px);
93
+ background: var(--hover-bg);
94
+ border-color: var(--glass-border-strong);
95
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7);
96
  }
97
 
98
  /* Animation utilities */
 
139
  animation: pulse 2s infinite;
140
  }
141
 
142
+ /* Modern Button System */
143
  .btn-primary {
144
  background: var(--primary-gradient);
145
  border: none;
146
+ border-radius: var(--radius-md);
147
  color: white;
148
  font-weight: 600;
149
+ padding: var(--space-lg) var(--space-xl);
150
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
151
+ box-shadow: var(--glass-shadow);
152
+ position: relative;
153
+ overflow: hidden;
154
+ cursor: pointer;
155
+ outline: none;
156
+ }
157
+
158
+ .btn-primary::before {
159
+ content: '';
160
+ position: absolute;
161
+ top: 0;
162
+ left: -100%;
163
+ width: 100%;
164
+ height: 100%;
165
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
166
+ transition: left 0.6s;
167
  }
168
 
169
  .btn-primary:hover {
170
  transform: translateY(-2px);
171
+ box-shadow: var(--glass-shadow-strong);
172
+ background: linear-gradient(135deg, #1a1a1a 0%, #3d3d3d 50%, #5a5a5a 100%);
173
+ }
174
+
175
+ .btn-primary:hover::before {
176
+ left: 100%;
177
+ }
178
+
179
+ .btn-primary:focus {
180
+ box-shadow: var(--glass-shadow-strong), 0 0 0 3px var(--focus-ring);
181
  }
182
 
183
  .btn-secondary {
184
+ background: var(--glass-bg-strong);
185
+ backdrop-filter: blur(12px);
186
+ border: 1px solid var(--glass-border);
187
+ border-radius: var(--radius-md);
188
  color: white;
189
+ font-weight: 500;
190
+ padding: var(--space-lg) var(--space-xl);
191
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
192
+ cursor: pointer;
193
+ outline: none;
194
  }
195
 
196
  .btn-secondary:hover {
197
+ background: var(--hover-bg);
198
+ border-color: var(--glass-border-strong);
199
  transform: translateY(-2px);
200
  }
201
 
202
+ .btn-secondary:focus {
203
+ box-shadow: 0 0 0 3px var(--focus-ring);
204
+ }
205
+
206
+ /* Modern Input System */
207
  .input-glass {
208
+ background: var(--glass-bg);
209
+ backdrop-filter: blur(12px);
210
+ border: 1px solid var(--glass-border);
211
+ border-radius: var(--radius-md);
212
  color: white;
213
+ padding: var(--space-lg) var(--space-lg);
214
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
215
+ font-size: 1rem;
216
+ font-weight: 400;
217
+ outline: none;
218
  }
219
 
220
  .input-glass::placeholder {
221
+ color: rgba(255, 255, 255, 0.6);
222
+ font-weight: 400;
223
+ }
224
+
225
+ .input-glass:hover {
226
+ background: var(--glass-bg-strong);
227
+ border-color: var(--glass-border-strong);
228
  }
229
 
230
  .input-glass:focus {
231
+ border-color: var(--focus-ring);
232
+ background: var(--glass-bg-strong);
233
+ box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.12);
234
+ }
235
+
236
+ .input-glass:focus::placeholder {
237
+ color: rgba(255, 255, 255, 0.4);
238
  }
239
 
240
  /* Loading spinner */
 
252
  100% { transform: rotate(360deg); }
253
  }
254
 
255
+ /* Skeleton loading animation */
256
+ .skeleton-loader {
257
+ background: linear-gradient(90deg, rgba(255, 255, 255, 0.1) 25%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.1) 75%);
258
+ background-size: 200% 100%;
259
+ animation: skeleton-loading 1.5s infinite;
260
+ }
261
+
262
+ @keyframes skeleton-loading {
263
+ 0% {
264
+ background-position: 200% 0;
265
+ }
266
+ 100% {
267
+ background-position: -200% 0;
268
+ }
269
+ }
270
+
271
+ /* Lazy image loading states */
272
+ .lazy {
273
+ filter: blur(5px);
274
+ transition: filter 0.3s;
275
+ }
276
+
277
+ .lazy.loaded {
278
+ filter: none;
279
+ }
280
+
281
+ /* Performance timing display */
282
+ .timing-badge {
283
+ background: rgba(0, 255, 0, 0.1);
284
+ border: 1px solid rgba(0, 255, 0, 0.3);
285
+ color: rgba(0, 255, 0, 0.8);
286
+ padding: 0.25rem 0.5rem;
287
+ border-radius: 0.5rem;
288
+ font-size: 0.75rem;
289
+ font-weight: 600;
290
+ display: inline-flex;
291
+ align-items: center;
292
+ gap: 0.25rem;
293
+ }
294
+
295
+ .timing-badge::before {
296
+ content: '⏱️';
297
+ font-size: 0.625rem;
298
+ }
299
+
300
  /* Panel composition grid */
301
  .panel-grid {
302
  display: grid;
 
329
  }
330
  }
331
 
332
+ /* Modern Text Gradients */
333
  .gradient-text {
334
+ background: var(--text-gradient);
335
+ -webkit-background-clip: text;
336
+ -webkit-text-fill-color: transparent;
337
+ background-clip: text;
338
+ font-weight: 700;
339
+ }
340
+
341
+ /* Performance indicator colors */
342
+ .perf-excellent { color: #10b981; }
343
+ .perf-good { color: #f59e0b; }
344
+ .perf-poor { color: #ef4444; }
345
+
346
+ .gradient-text-subtle {
347
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(255, 255, 255, 0.7) 100%);
348
  -webkit-background-clip: text;
349
  -webkit-text-fill-color: transparent;
350
  background-clip: text;
 
370
  .template-card.selected {
371
  background: rgba(255, 255, 255, 0.3);
372
  border-color: rgba(255, 255, 255, 0.5);
373
+ }
374
+
375
+ /* Story progression cards */
376
+ .progression-card {
377
+ background: rgba(255, 255, 255, 0.1);
378
+ backdrop-filter: blur(15px);
379
+ border: 1px solid rgba(255, 255, 255, 0.2);
380
+ border-radius: 16px;
381
+ padding: 24px;
382
+ transition: all 0.3s ease;
383
+ cursor: pointer;
384
+ position: relative;
385
+ min-height: 400px;
386
+ }
387
+
388
+ .progression-card:hover {
389
+ background: rgba(255, 255, 255, 0.2);
390
+ transform: translateY(-6px) scale(1.02);
391
+ box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2);
392
+ }
393
+
394
+ .progression-card.selected {
395
+ background: rgba(255, 255, 255, 0.25);
396
+ border-color: rgba(255, 255, 255, 0.5);
397
+ box-shadow: 0 0 30px rgba(255, 255, 255, 0.2);
398
+ }
399
+
400
+ /* Series chapter navigation */
401
+ .chapter-nav-button {
402
+ background: rgba(255, 255, 255, 0.1);
403
+ backdrop-filter: blur(10px);
404
+ border: 2px solid transparent;
405
+ border-radius: 12px;
406
+ padding: 16px;
407
+ transition: all 0.3s ease;
408
+ cursor: pointer;
409
+ }
410
+
411
+ .chapter-nav-button:hover {
412
+ background: rgba(255, 255, 255, 0.15);
413
+ transform: translateY(-2px);
414
+ }
415
+
416
+ .chapter-nav-button.active {
417
+ background: rgba(255, 255, 255, 0.2);
418
+ border-color: rgba(255, 255, 255, 0.5);
419
+ }
420
+
421
+ /* Enhanced animations for story progression */
422
+ @keyframes progressionSlideIn {
423
+ from {
424
+ opacity: 0;
425
+ transform: translateY(30px) scale(0.95);
426
+ }
427
+ to {
428
+ opacity: 1;
429
+ transform: translateY(0) scale(1);
430
+ }
431
+ }
432
+
433
+ .progression-card {
434
+ animation: progressionSlideIn 0.6s ease-out;
435
+ }
436
+
437
+ .progression-card:nth-child(2) {
438
+ animation-delay: 0.1s;
439
+ }
440
+
441
+ .progression-card:nth-child(3) {
442
+ animation-delay: 0.2s;
443
+ }
444
+
445
+ /* Series completion celebration effects */
446
+ @keyframes celebrationGlow {
447
+ 0%, 100% {
448
+ box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
449
+ }
450
+ 50% {
451
+ box-shadow: 0 0 40px rgba(255, 255, 255, 0.6);
452
+ }
453
+ }
454
+
455
+ .series-complete-glow {
456
+ animation: celebrationGlow 2s ease-in-out infinite;
457
+ }
458
+
459
+ /* Enhanced hover effects */
460
+ @keyframes scaleHover {
461
+ to {
462
+ transform: scale(1.02);
463
+ }
464
+ }
465
+
466
+ .hover\:scale-102:hover {
467
+ animation: scaleHover 0.2s ease-out forwards;
468
+ }
469
+
470
+ .hover\:scale-105:hover {
471
+ transform: scale(1.05);
472
+ }
473
+
474
+ /* Progress indicators */
475
+ .progress-step {
476
+ background: rgba(255, 255, 255, 0.2);
477
+ border-radius: 50%;
478
+ width: 12px;
479
+ height: 12px;
480
+ transition: all 0.3s ease;
481
+ }
482
+
483
+ .progress-step.active {
484
+ background: rgba(255, 255, 255, 0.8);
485
+ box-shadow: 0 0 15px rgba(255, 255, 255, 0.5);
486
+ }
487
+
488
+ .progress-step.completed {
489
+ background: rgba(34, 197, 94, 0.8);
490
+ box-shadow: 0 0 15px rgba(34, 197, 94, 0.5);
491
+ }
492
+
493
+ /* Series statistics display */
494
+ .series-stats {
495
+ display: flex;
496
+ justify-content: center;
497
+ align-items: center;
498
+ gap: 2rem;
499
+ padding: 1.5rem;
500
+ background: rgba(255, 255, 255, 0.1);
501
+ backdrop-filter: blur(10px);
502
+ border-radius: 16px;
503
+ border: 1px solid rgba(255, 255, 255, 0.2);
504
+ }
505
+
506
+ .series-stat-item {
507
+ text-align: center;
508
+ padding: 0 1rem;
509
+ }
510
+
511
+ .series-stat-value {
512
+ font-size: 2rem;
513
+ font-weight: 800;
514
+ color: white;
515
+ margin-bottom: 0.5rem;
516
+ }
517
+
518
+ .series-stat-label {
519
+ font-size: 0.875rem;
520
+ color: rgba(255, 255, 255, 0.7);
521
+ }
522
+
523
+ .series-stat-divider {
524
+ width: 1px;
525
+ height: 2rem;
526
+ background: rgba(255, 255, 255, 0.2);
527
+ }
528
+
529
+ /* Mobile responsiveness for new components */
530
+ @media (max-width: 768px) {
531
+ .progression-card {
532
+ padding: 16px;
533
+ min-height: 300px;
534
+ }
535
+
536
+ .series-stats {
537
+ flex-direction: column;
538
+ gap: 1rem;
539
+ }
540
+
541
+ .series-stat-divider {
542
+ width: 100%;
543
+ height: 1px;
544
+ }
545
+
546
+ .chapter-nav-button {
547
+ padding: 12px;
548
+ }
549
  }
index.html CHANGED
@@ -6,7 +6,6 @@
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>AI Manga Generator</title>
8
  <script src="https://cdn.tailwindcss.com"></script>
9
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
10
  <script type="importmap">
11
  {
12
  "imports": {
 
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>AI Manga Generator</title>
8
  <script src="https://cdn.tailwindcss.com"></script>
 
9
  <script type="importmap">
10
  {
11
  "imports": {
package.json CHANGED
@@ -13,6 +13,7 @@
13
  },
14
  "dependencies": {
15
  "@google/genai": "^1.17.0",
 
16
  "react": "^19.1.1",
17
  "react-dom": "^19.1.1"
18
  },
 
13
  },
14
  "dependencies": {
15
  "@google/genai": "^1.17.0",
16
+ "jspdf": "^3.0.2",
17
  "react": "^19.1.1",
18
  "react-dom": "^19.1.1"
19
  },
services/geminiService.ts CHANGED
@@ -1,5 +1,18 @@
1
  import { GoogleGenAI, Type, Modality } from "@google/genai";
2
- import type { LoadingState, MangaPage, CharacterProfile, MangaStyle } from '../types';
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  // Initialize GoogleGenAI client - will be created dynamically with user's API key
5
  let ai: GoogleGenAI;
@@ -9,6 +22,7 @@ interface StoryDetails {
9
  story: string;
10
  author: string;
11
  style: MangaStyle;
 
12
  }
13
 
14
  // Schema for character profile generation
@@ -65,14 +79,56 @@ const mangaPageSchema = {
65
  },
66
  };
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  /**
69
  * Generates character profiles from a story premise with advanced character development.
70
  */
71
  const generateCharacterProfiles = async (story: string, style: MangaStyle): Promise<CharacterProfile[]> => {
 
 
72
  const prompt = `You are a master character designer and development expert specializing in compelling visual storytelling. Create rich, multi-dimensional character profiles that will serve as the foundation for a professional comic series.
73
 
74
  ADVANCED CHARACTER DEVELOPMENT BRIEF:
75
- Style: ${style} manga aesthetic with professional character design principles
76
  Story Context: "${story}"
77
 
78
  CHARACTER CREATION GUIDELINES:
@@ -85,7 +141,7 @@ CHARACTER CREATION GUIDELINES:
85
  TECHNICAL SPECIFICATIONS:
86
  - Create 2-4 main characters (protagonists, deuteragonists, key supporting characters)
87
  - Each character needs comprehensive visual and personality descriptions
88
- - Include distinctive features that work well in ${style} art style
89
  - Consider character relationships and visual contrast between characters
90
  - Ensure characters can drive the story forward through their actions and conflicts
91
 
@@ -184,14 +240,30 @@ const analyzeNarrativeStructure = (story: string): { structure: string; keyBeats
184
  /**
185
  * Generates a manga script from a story premise and character profiles.
186
  */
187
- const generateMangaScript = async (story: string, style: MangaStyle, characters: CharacterProfile[]): Promise<MangaPage[]> => {
 
 
188
  const characterDescriptions = characters.map(c => `- ${c.name}: ${c.description}`).join('\n');
189
  const narrativeAnalysis = analyzeNarrativeStructure(story);
190
 
191
  const prompt = `You are a master storyteller and comic book editor with decades of experience creating compelling visual narratives. Your expertise spans multiple genres and you understand the delicate balance between visual storytelling and narrative pacing.
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  ADVANCED STORYTELLING ANALYSIS:
194
  Story Structure Detected: ${narrativeAnalysis.structure}
 
195
  Key Narrative Beats: ${narrativeAnalysis.keyBeats.join(' → ')}
196
 
197
  STORY ENHANCEMENT MISSION:
@@ -205,10 +277,10 @@ Transform this premise into a professional comic script that maximizes emotional
205
  6. CLIMAX: Build to a satisfying emotional and visual climax
206
 
207
  TECHNICAL SPECIFICATIONS:
208
- - Visual Style: ${style} manga aesthetic with professional composition
209
  - Page Count: 4-5 pages optimized for digital reading
210
  - Panel Density: 2-4 panels per page with dynamic layouts
211
- - Dialogue: Concise, character-driven, emotionally resonant
212
 
213
  CHARACTER CAST:
214
  ${characterDescriptions}
@@ -221,7 +293,7 @@ INSTRUCTIONS:
221
  2. Identify the key emotional moments and visual set pieces
222
  3. Plan panel compositions that support the narrative flow
223
  4. Create a script where each panel serves both story and visual impact
224
- 5. Ensure dialogue feels natural and advances both plot and character
225
 
226
  Generate a professional comic script that elevates this premise into a compelling visual narrative.`;
227
 
@@ -282,6 +354,10 @@ const analyzePageComposition = (page: MangaPage, style: MangaStyle): string => {
282
  return "SHOJO AESTHETIC LAYOUT: Flowing, organic panel shapes with decorative elements. Focus on character expressions.";
283
  } else if (style === 'Seinen') {
284
  return "MATURE COMPOSITION: Clean, sophisticated panel layouts with subtle visual metaphors and realistic proportions.";
 
 
 
 
285
  } else {
286
  return "BALANCED COMPOSITION: Professional comic layout with clear visual hierarchy and optimal reading flow.";
287
  }
@@ -290,15 +366,24 @@ const analyzePageComposition = (page: MangaPage, style: MangaStyle): string => {
290
  /**
291
  * Generates a single manga page image using gemini-2.5-flash-image-preview.
292
  */
293
- const generatePageImage = async (prompt: string, previousPageImage?: string, page?: MangaPage, style?: MangaStyle): Promise<string> => {
294
  try {
295
  const parts: any[] = [];
296
 
297
  // Add composition guidance if page data is available
298
  let enhancedPrompt = prompt;
299
  if (page && style) {
300
- const compositionGuide = analyzePageComposition(page, style);
301
- enhancedPrompt = `${prompt}\n\nCOMPOSITION GUIDANCE:\n${compositionGuide}\n\nPROFESSIONAL COMIC TECHNIQUES:\n- Use proper panel gutters and borders\n- Maintain clear reading flow (left to right, top to bottom)\n- Balance text and visual elements\n- Apply comic book color theory and contrast\n- Ensure character consistency and proportions\n- Use appropriate camera angles and perspectives`;
 
 
 
 
 
 
 
 
 
302
  }
303
 
304
  if (previousPageImage) {
@@ -340,45 +425,309 @@ const generatePageImage = async (prompt: string, previousPageImage?: string, pag
340
  };
341
 
342
  /**
343
- * Creates the main manga generation orchestrator.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  */
345
  export const generateMangaScriptAndImages = async (
346
  storyDetails: StoryDetails,
347
- updateProgress: (state: LoadingState) => void,
348
  apiKey: string
349
- ): Promise<string[]> => {
 
 
 
 
 
 
 
 
 
 
 
 
350
  // Initialize AI client with user's API key
351
  ai = new GoogleGenAI({ apiKey });
352
- const { title, story, author, style } = storyDetails;
 
 
353
  const allImages: string[] = [];
354
 
355
  // Step 1: Generate Character Profiles
356
- updateProgress({ isLoading: true, message: 'Analyzing story for characters...', progress: 10 });
357
- const characters = await generateCharacterProfiles(story, style);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
 
359
- // Step 2: Generate Manga Script
360
- updateProgress({ isLoading: true, message: 'Writing manga script...', progress: 25 });
361
- const scriptPages = await generateMangaScript(story, style, characters);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  if (scriptPages.length === 0) {
363
  throw new Error("The generated script was empty. Please try a different story.");
364
  }
365
 
366
  const totalPagesToGenerate = scriptPages.length + 2; // script pages + title + conclusion
367
  let pagesGenerated = 0;
 
368
 
369
  const characterPromptPart = characters.map(c => `${c.name}: ${c.description}`).join('; ');
370
 
371
  const updatePageProgress = () => {
372
  pagesGenerated++;
373
- // Progress from 40% to 95% is for image generation
374
- const progress = 40 + Math.round((pagesGenerated / totalPagesToGenerate) * 55);
375
  return progress;
376
  };
377
 
378
- // Step 3: Generate Title Page
379
- updateProgress({ isLoading: true, message: 'Creating title page...', progress: 40 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  const titlePagePrompt = `TASK: Create a dynamic manga title page.
381
- STYLE: ${style}, black and white.
382
  ASPECT RATIO: 3:4 vertical.
383
  TITLE: "${title}"
384
  AUTHOR: "${author}"
@@ -387,61 +736,110 @@ INSTRUCTIONS: Create a compelling cover image. Generate ONLY the image.`;
387
 
388
  let titleImage: string;
389
  try {
390
- titleImage = await generatePageImage(titlePagePrompt);
391
  } catch (error) {
392
  console.warn('Title page generation failed, retrying once...', error);
393
- titleImage = await generatePageImage(titlePagePrompt);
394
  }
395
  allImages.push(titleImage);
 
 
 
 
 
396
 
397
-
398
- // Step 4: Generate each page from the script
399
  for (const page of scriptPages) {
400
  const progress = updatePageProgress();
401
- updateProgress({ isLoading: true, message: `Drawing page ${page.pageNumber} of ${scriptPages.length}...`, progress });
 
 
 
 
 
 
 
402
 
403
  const panelPrompts = page.panels.map(p => {
404
  let panelString = `Panel ${p.panelNumber}: ${p.description}`;
405
- if (p.dialogue && p.speaker) {
406
  const speakingCharacter = characters.find(c => c.name === p.speaker);
407
  if (speakingCharacter) {
408
- // Planner Agent: Link the visual description directly to the dialogue.
409
  panelString += `\n - Dialogue: The character speaking is ${speakingCharacter.name} (${speakingCharacter.description}). They say: "${p.dialogue}"`;
410
  } else {
411
  panelString += `\n - Dialogue (${p.speaker}): "${p.dialogue}"`;
412
  }
 
 
413
  }
414
  return panelString;
415
  }).join('\n\n');
 
 
 
 
 
 
 
 
 
 
 
416
 
417
  const pagePrompt = `TASK: Create a manga page image.
418
- STYLE: ${style}, black and white.
419
  ASPECT RATIO: 3:4 vertical.
420
  CHARACTERS: ${characterPromptPart}.
421
- PAGE: ${page.pageNumber}, with ${page.panels.length} panels.
422
 
423
  PANEL INSTRUCTIONS:
424
  ${panelPrompts}
425
 
426
- FINAL INSTRUCTIONS: Arrange panels dynamically. Place dialogue in speech bubbles for the correct characters. Generate ONLY the image.`;
427
 
428
  // Pass the last generated image as a reference for consistency
429
  const previousImage = allImages[allImages.length - 1];
 
430
  let pageImage: string;
431
  try {
432
- pageImage = await generatePageImage(pagePrompt, previousImage, page, style);
433
  } catch (error) {
434
  console.warn(`Page ${page.pageNumber} generation failed, retrying once...`, error);
435
- pageImage = await generatePageImage(pagePrompt, previousImage, page, style);
436
  }
 
437
  allImages.push(pageImage);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  }
439
 
440
- // Step 5: Generate Conclusion Page
441
  const conclusionProgress = updatePageProgress();
442
- updateProgress({ isLoading: true, message: 'Creating conclusion page...', progress: conclusionProgress });
 
 
 
 
 
 
 
443
  const conclusionPagePrompt = `TASK: Create a final, evocative manga page.
444
- STYLE: ${style}, black and white.
445
  ASPECT RATIO: 3:4 vertical.
446
  STORY CONTEXT: The story was about "${story}".
447
  INSTRUCTIONS: The page should feel like a conclusion. Maybe a character looking towards the horizon or a symbolic image related to the story. Include a small, stylized "The End" text. Generate ONLY the image.`;
@@ -450,13 +848,355 @@ INSTRUCTIONS: The page should feel like a conclusion. Maybe a character looking
450
  const lastContentPage = allImages[allImages.length - 1];
451
  let conclusionImage: string;
452
  try {
453
- conclusionImage = await generatePageImage(conclusionPagePrompt, lastContentPage);
454
  } catch (error) {
455
  console.warn('Conclusion page generation failed, retrying once...', error);
456
- conclusionImage = await generatePageImage(conclusionPagePrompt, lastContentPage);
457
  }
458
  allImages.push(conclusionImage);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
 
460
- updateProgress({ isLoading: true, message: 'Finishing up...', progress: 100 });
461
- return allImages;
462
  };
 
1
  import { GoogleGenAI, Type, Modality } from "@google/genai";
2
+ import { PerformanceTracker } from '../utils/imageOptimization';
3
+ import type {
4
+ LoadingState,
5
+ MangaPage,
6
+ CharacterProfile,
7
+ MangaStyle,
8
+ StoryProgression,
9
+ ComicIteration,
10
+ CharacterSketch,
11
+ EnvironmentSketch,
12
+ GenerationProgress,
13
+ StoryArcSummary,
14
+ PerformanceTiming
15
+ } from '../types';
16
 
17
  // Initialize GoogleGenAI client - will be created dynamically with user's API key
18
  let ai: GoogleGenAI;
 
22
  story: string;
23
  author: string;
24
  style: MangaStyle;
25
+ includeDialogue: boolean;
26
  }
27
 
28
  // Schema for character profile generation
 
79
  },
80
  };
81
 
82
+ /**
83
+ * Automatically selects the most appropriate manga style based on story content
84
+ */
85
+ const selectOptimalStyle = (story: string): MangaStyle => {
86
+ const storyLower = story.toLowerCase();
87
+
88
+ // Action/adventure keywords suggest Shonen
89
+ const actionKeywords = ['fight', 'battle', 'war', 'hero', 'power', 'strength', 'adventure', 'quest', 'enemy'];
90
+ if (actionKeywords.some(keyword => storyLower.includes(keyword))) {
91
+ return 'Shonen';
92
+ }
93
+
94
+ // Romance/emotion keywords suggest Shojo
95
+ const romanceKeywords = ['love', 'romance', 'heart', 'relationship', 'feelings', 'beautiful', 'flower', 'tender'];
96
+ if (romanceKeywords.some(keyword => storyLower.includes(keyword))) {
97
+ return 'Shojo';
98
+ }
99
+
100
+ // Mature/complex keywords suggest Seinen
101
+ const matureKeywords = ['complex', 'psychology', 'society', 'work', 'adult', 'realistic', 'dark', 'serious'];
102
+ if (matureKeywords.some(keyword => storyLower.includes(keyword))) {
103
+ return 'Seinen';
104
+ }
105
+
106
+ // Wacky/absurd keywords suggest Wacky style
107
+ const wackyKeywords = ['funny', 'crazy', 'weird', 'silly', 'absurd', 'rubber', 'stretchy', 'pirates', 'treasure'];
108
+ if (wackyKeywords.some(keyword => storyLower.includes(keyword))) {
109
+ return 'Wacky';
110
+ }
111
+
112
+ // Cute/simple keywords suggest Chibi
113
+ const chibiKeywords = ['cute', 'small', 'tiny', 'children', 'school', 'friends', 'simple', 'innocent'];
114
+ if (chibiKeywords.some(keyword => storyLower.includes(keyword))) {
115
+ return 'Chibi';
116
+ }
117
+
118
+ // Default to Shonen for general stories
119
+ return 'Shonen';
120
+ };
121
+
122
  /**
123
  * Generates character profiles from a story premise with advanced character development.
124
  */
125
  const generateCharacterProfiles = async (story: string, style: MangaStyle): Promise<CharacterProfile[]> => {
126
+ // Auto-select style if "I don't know" is chosen
127
+ const actualStyle = style === "I don't know" ? selectOptimalStyle(story) : style;
128
  const prompt = `You are a master character designer and development expert specializing in compelling visual storytelling. Create rich, multi-dimensional character profiles that will serve as the foundation for a professional comic series.
129
 
130
  ADVANCED CHARACTER DEVELOPMENT BRIEF:
131
+ Style: ${actualStyle} manga aesthetic with professional character design principles
132
  Story Context: "${story}"
133
 
134
  CHARACTER CREATION GUIDELINES:
 
141
  TECHNICAL SPECIFICATIONS:
142
  - Create 2-4 main characters (protagonists, deuteragonists, key supporting characters)
143
  - Each character needs comprehensive visual and personality descriptions
144
+ - Include distinctive features that work well in ${actualStyle} art style
145
  - Consider character relationships and visual contrast between characters
146
  - Ensure characters can drive the story forward through their actions and conflicts
147
 
 
240
  /**
241
  * Generates a manga script from a story premise and character profiles.
242
  */
243
+ const generateMangaScript = async (story: string, style: MangaStyle, characters: CharacterProfile[], includeDialogue: boolean): Promise<MangaPage[]> => {
244
+ // Auto-select style if "I don't know" is chosen
245
+ const actualStyle = style === "I don't know" ? selectOptimalStyle(story) : style;
246
  const characterDescriptions = characters.map(c => `- ${c.name}: ${c.description}`).join('\n');
247
  const narrativeAnalysis = analyzeNarrativeStructure(story);
248
 
249
  const prompt = `You are a master storyteller and comic book editor with decades of experience creating compelling visual narratives. Your expertise spans multiple genres and you understand the delicate balance between visual storytelling and narrative pacing.
250
 
251
+ TEXT GENERATION OPTIONS:
252
+ ${includeDialogue ? `- Keep ALL dialogue extremely simple and short (max 8-10 words per speech bubble)
253
+ - Use basic vocabulary that's easy to understand
254
+ - Avoid complex sentence structures
255
+ - Break long thoughts into multiple short sentences
256
+ - Prioritize clear, simple communication over elaborate prose` : `- ABSOLUTELY NO DIALOGUE, speech bubbles, or any text content whatsoever
257
+ - ZERO text generation - this is purely visual storytelling
258
+ - Do NOT include any words, phrases, sound effects, or onomatopoeia
259
+ - Focus entirely on visual composition, character expressions, and body language
260
+ - Describe scenes as if they are silent films
261
+ - NO SPEECH BUBBLES, NO TEXT BALLOONS, NO WRITTEN WORDS OF ANY KIND
262
+ - Leave all text elements completely empty for manual addition later`}
263
+
264
  ADVANCED STORYTELLING ANALYSIS:
265
  Story Structure Detected: ${narrativeAnalysis.structure}
266
+ Selected Art Style: ${actualStyle} (${style === "I don't know" ? "auto-selected" : "user-chosen"})
267
  Key Narrative Beats: ${narrativeAnalysis.keyBeats.join(' → ')}
268
 
269
  STORY ENHANCEMENT MISSION:
 
277
  6. CLIMAX: Build to a satisfying emotional and visual climax
278
 
279
  TECHNICAL SPECIFICATIONS:
280
+ - Visual Style: ${actualStyle} manga aesthetic with professional composition
281
  - Page Count: 4-5 pages optimized for digital reading
282
  - Panel Density: 2-4 panels per page with dynamic layouts
283
+ - Dialogue: ${includeDialogue ? 'Concise, character-driven, emotionally resonant' : 'COMPLETELY FORBIDDEN - absolutely no text or speech of any kind'}
284
 
285
  CHARACTER CAST:
286
  ${characterDescriptions}
 
293
  2. Identify the key emotional moments and visual set pieces
294
  3. Plan panel compositions that support the narrative flow
295
  4. Create a script where each panel serves both story and visual impact
296
+ 5. ${includeDialogue ? 'Ensure dialogue feels natural and advances both plot and character' : 'CRITICAL: Generate ZERO dialogue or text - tell the story purely through visual elements, poses, expressions, and actions'}
297
 
298
  Generate a professional comic script that elevates this premise into a compelling visual narrative.`;
299
 
 
354
  return "SHOJO AESTHETIC LAYOUT: Flowing, organic panel shapes with decorative elements. Focus on character expressions.";
355
  } else if (style === 'Seinen') {
356
  return "MATURE COMPOSITION: Clean, sophisticated panel layouts with subtle visual metaphors and realistic proportions.";
357
+ } else if (style === 'Wacky') {
358
+ return "WACKY ONE PIECE LAYOUT: Exaggerated, rubber-hose animation style panels with impossible physics and zany expressions. Characters can stretch and deform wildly.";
359
+ } else if (style === 'Chibi') {
360
+ return "CHIBI CUTE LAYOUT: Simple, rounded panels with adorable character designs and minimal backgrounds. Focus on expressions.";
361
  } else {
362
  return "BALANCED COMPOSITION: Professional comic layout with clear visual hierarchy and optimal reading flow.";
363
  }
 
366
  /**
367
  * Generates a single manga page image using gemini-2.5-flash-image-preview.
368
  */
369
+ const generatePageImage = async (prompt: string, previousPageImage?: string, page?: MangaPage, style?: MangaStyle, includeDialogue: boolean = true): Promise<string> => {
370
  try {
371
  const parts: any[] = [];
372
 
373
  // Add composition guidance if page data is available
374
  let enhancedPrompt = prompt;
375
  if (page && style) {
376
+ const actualStyle = style === "I don't know" ? selectOptimalStyle("action adventure") : style; // fallback for image generation
377
+ const compositionGuide = analyzePageComposition(page, actualStyle);
378
+
379
+ let styleSpecificInstructions = "";
380
+ if (actualStyle === 'Wacky') {
381
+ styleSpecificInstructions = "\nWACKY STYLE SPECIFICS:\n- Characters can have impossible body proportions and physics-defying actions\n- Exaggerated facial expressions with bulging eyes and dropped jaws\n- Rubber-like stretching and deformation of characters\n- Cartoonish impact effects and motion lines\n- Bright, vibrant colors with high contrast";
382
+ } else if (actualStyle === 'Chibi') {
383
+ styleSpecificInstructions = "\nCHIBI STYLE SPECIFICS:\n- All characters have large heads (1:2 or 1:3 head-to-body ratio)\n- Simplified features with dot eyes and minimal noses\n- Rounded, soft shapes throughout\n- Pastel or bright cheerful colors\n- Minimal detailed backgrounds";
384
+ }
385
+
386
+ enhancedPrompt = `${prompt}\n\nCOMPOSITION GUIDANCE:\n${compositionGuide}${styleSpecificInstructions}\n\nPROFESSIONAL COMIC TECHNIQUES:\n- Use proper panel gutters and borders\n- Maintain clear reading flow (left to right, top to bottom)\n- Balance text and visual elements\n- Apply comic book color theory and contrast\n- Ensure character consistency and proportions\n- Use appropriate camera angles and perspectives\n\nTEXT HANDLING:\n${includeDialogue ? '- Keep all speech bubbles short and simple\n- Use basic vocabulary\n- Maximum 8-10 words per dialogue bubble' : '- ABSOLUTELY FORBIDDEN: No speech bubbles, dialogue, text, words, or any written content\n- CRITICAL: Do not include ANY text elements, sound effects, or onomatopoeia\n- MANDATORY: Focus exclusively on visual storytelling through character expressions, body language, and visual metaphors\n- IMPORTANT: Characters can mouth words but NO text should be visible\n- ESSENTIAL: Tell the story purely through visual composition and character acting\n- FINAL RULE: Zero text content - this is a silent comic panel'}`;
387
  }
388
 
389
  if (previousPageImage) {
 
425
  };
426
 
427
  /**
428
+ * Generates character sketches for main characters
429
+ */
430
+ const generateCharacterSketches = async (characters: CharacterProfile[], style: MangaStyle): Promise<CharacterSketch[]> => {
431
+ const sketches: CharacterSketch[] = [];
432
+
433
+ for (const character of characters.slice(0, 4)) { // Limit to 4 main characters
434
+ try {
435
+ const styleDescription = style === 'Wacky' ?
436
+ "Wacky One Piece-style with exaggerated, rubber-hose animation aesthetics" :
437
+ style === 'Chibi' ?
438
+ "Chibi style with cute, simplified characters having large heads" :
439
+ style;
440
+
441
+ const sketchPrompt = `TASK: Create a character design sketch.
442
+ STYLE: ${styleDescription} manga, black and white sketch style with clean line art.
443
+ CHARACTER: ${character.name} - ${character.description}
444
+ INSTRUCTIONS: Create a full-body character reference sketch showing the character from front view. Include facial expressions and key design elements. This is a character design sheet, not a comic panel. Generate ONLY the image.`;
445
+
446
+ const sketchImage = await generatePageImage(sketchPrompt);
447
+ sketches.push({
448
+ characterName: character.name,
449
+ sketchImage: sketchImage,
450
+ description: character.description
451
+ });
452
+ } catch (error) {
453
+ console.warn(`Failed to generate sketch for character ${character.name}:`, error);
454
+ }
455
+ }
456
+
457
+ return sketches;
458
+ };
459
+
460
+ /**
461
+ * Generates environment sketches for key locations
462
+ */
463
+ const generateEnvironmentSketches = async (story: string, style: MangaStyle): Promise<EnvironmentSketch[]> => {
464
+ const environments = analyzeStoryEnvironments(story);
465
+ const sketches: EnvironmentSketch[] = [];
466
+
467
+ for (const env of environments.slice(0, 3)) { // Limit to 3 key environments
468
+ try {
469
+ const styleDescription = style === 'Wacky' ?
470
+ "Wacky One Piece-style with exaggerated, cartoonish environments" :
471
+ style === 'Chibi' ?
472
+ "Chibi style with simple, cute environments" :
473
+ style;
474
+
475
+ const envPrompt = `TASK: Create an environment/location sketch.
476
+ STYLE: ${styleDescription} manga, black and white sketch style.
477
+ ENVIRONMENT: ${env.name} - ${env.description}
478
+ INSTRUCTIONS: Create a detailed background/environment sketch showing the key location. Focus on atmosphere and architectural/natural elements. This is a location reference sheet. Generate ONLY the image.`;
479
+
480
+ const envImage = await generatePageImage(envPrompt);
481
+ sketches.push({
482
+ environmentName: env.name,
483
+ sketchImage: envImage,
484
+ description: env.description
485
+ });
486
+ } catch (error) {
487
+ console.warn(`Failed to generate environment sketch for ${env.name}:`, error);
488
+ }
489
+ }
490
+
491
+ return sketches;
492
+ };
493
+
494
+ /**
495
+ * Analyzes story to identify key environments
496
+ */
497
+ const analyzeStoryEnvironments = (story: string): { name: string; description: string }[] => {
498
+ const storyLower = story.toLowerCase();
499
+ const environments: { name: string; description: string }[] = [];
500
+
501
+ // Common environment patterns
502
+ if (storyLower.includes('school') || storyLower.includes('classroom') || storyLower.includes('student')) {
503
+ environments.push({ name: 'School', description: 'Educational institution with classrooms and hallways' });
504
+ }
505
+ if (storyLower.includes('home') || storyLower.includes('house') || storyLower.includes('room')) {
506
+ environments.push({ name: 'Home', description: 'Residential living space with personal belongings' });
507
+ }
508
+ if (storyLower.includes('city') || storyLower.includes('urban') || storyLower.includes('street')) {
509
+ environments.push({ name: 'City Street', description: 'Urban environment with buildings and bustling activity' });
510
+ }
511
+ if (storyLower.includes('forest') || storyLower.includes('tree') || storyLower.includes('nature')) {
512
+ environments.push({ name: 'Forest', description: 'Natural woodland environment with trees and wildlife' });
513
+ }
514
+ if (storyLower.includes('space') || storyLower.includes('ship') || storyLower.includes('station')) {
515
+ environments.push({ name: 'Spaceship', description: 'Futuristic spacecraft interior with advanced technology' });
516
+ }
517
+ if (storyLower.includes('castle') || storyLower.includes('kingdom') || storyLower.includes('medieval')) {
518
+ environments.push({ name: 'Castle', description: 'Medieval fortress with stone walls and grand halls' });
519
+ }
520
+
521
+ // Default environments if none detected
522
+ if (environments.length === 0) {
523
+ environments.push(
524
+ { name: 'Main Setting', description: 'The primary location where the story takes place' },
525
+ { name: 'Character Home', description: 'Personal living space of the main character' }
526
+ );
527
+ }
528
+
529
+ return environments;
530
+ };
531
+
532
+ /**
533
+ * Generates story arc summary
534
+ */
535
+ const generateStoryArcSummary = async (pages: MangaPage[], characters: CharacterProfile[]): Promise<StoryArcSummary> => {
536
+ const storyContent = pages.map(page =>
537
+ page.panels.map(panel => `${panel.description}${panel.dialogue ? ` - "${panel.dialogue}"` : ''}`).join(' ')
538
+ ).join('\n');
539
+
540
+ const prompt = `Analyze this comic story and create a comprehensive story arc summary.
541
+
542
+ STORY CONTENT:
543
+ ${storyContent}
544
+
545
+ CHARACTERS:
546
+ ${characters.map(c => `${c.name}: ${c.description}`).join('; ')}
547
+
548
+ Create a detailed analysis covering:
549
+ 1. Overall theme and central message
550
+ 2. Key story events in chronological order
551
+ 3. Character development and growth arcs
552
+ 4. Narrative progression and story structure
553
+ 5. Emotional journey of the characters
554
+
555
+ Provide insights into how the story unfolds and what makes it compelling.`;
556
+
557
+ try {
558
+ const response = await ai.models.generateContent({
559
+ model: "gemini-2.5-flash",
560
+ contents: prompt,
561
+ });
562
+
563
+ const summaryText = response.text.trim();
564
+
565
+ // Parse the summary into structured format
566
+ const lines = summaryText.split('\n').filter(line => line.trim());
567
+ let currentSection = '';
568
+ const summary: StoryArcSummary = {
569
+ overallTheme: '',
570
+ keyEvents: [],
571
+ characterDevelopment: [],
572
+ narrativeProgression: '',
573
+ emotionalJourney: ''
574
+ };
575
+
576
+ for (const line of lines) {
577
+ if (line.toLowerCase().includes('theme') || line.toLowerCase().includes('message')) {
578
+ currentSection = 'theme';
579
+ summary.overallTheme += line.replace(/^\d+\.\s*/, '').replace(/^[^:]*:?\s*/, '') + ' ';
580
+ } else if (line.toLowerCase().includes('key') && line.toLowerCase().includes('event')) {
581
+ currentSection = 'events';
582
+ } else if (line.toLowerCase().includes('character') && line.toLowerCase().includes('development')) {
583
+ currentSection = 'character';
584
+ } else if (line.toLowerCase().includes('narrative') || line.toLowerCase().includes('progression')) {
585
+ currentSection = 'narrative';
586
+ summary.narrativeProgression += line.replace(/^\d+\.\s*/, '').replace(/^[^:]*:?\s*/, '') + ' ';
587
+ } else if (line.toLowerCase().includes('emotional') || line.toLowerCase().includes('journey')) {
588
+ currentSection = 'emotional';
589
+ summary.emotionalJourney += line.replace(/^\d+\.\s*/, '').replace(/^[^:]*:?\s*/, '') + ' ';
590
+ } else if (line.trim().startsWith('-') || line.trim().startsWith('•')) {
591
+ const cleanLine = line.replace(/^[-•]\s*/, '');
592
+ if (currentSection === 'events') {
593
+ summary.keyEvents.push(cleanLine);
594
+ } else if (currentSection === 'character') {
595
+ summary.characterDevelopment.push(cleanLine);
596
+ }
597
+ }
598
+ }
599
+
600
+ return summary;
601
+ } catch (error) {
602
+ console.warn('Failed to generate story arc summary:', error);
603
+ return {
604
+ overallTheme: 'A compelling narrative exploring themes of growth and adventure',
605
+ keyEvents: ['Story begins', 'Conflict arises', 'Resolution achieved'],
606
+ characterDevelopment: ['Characters face challenges', 'Personal growth occurs'],
607
+ narrativeProgression: 'The story follows a classic narrative structure with rising action leading to climax and resolution',
608
+ emotionalJourney: 'Characters experience a range of emotions leading to personal transformation'
609
+ };
610
+ }
611
+ };
612
+
613
+ /**
614
+ * Creates the main manga generation orchestrator with enhanced progress updates.
615
  */
616
  export const generateMangaScriptAndImages = async (
617
  storyDetails: StoryDetails,
618
+ updateProgress: (state: GenerationProgress) => void,
619
  apiKey: string
620
+ ): Promise<{
621
+ images: string[];
622
+ characters: CharacterProfile[];
623
+ pages: MangaPage[];
624
+ characterSketches: CharacterSketch[];
625
+ environmentSketches: EnvironmentSketch[];
626
+ storyArcSummary: StoryArcSummary;
627
+ performanceTimings: PerformanceTiming[];
628
+ totalGenerationTime: number;
629
+ }> => {
630
+ const performanceTracker = new PerformanceTracker();
631
+ const startTime = Date.now();
632
+ performanceTracker.startTimer('total_generation');
633
  // Initialize AI client with user's API key
634
  ai = new GoogleGenAI({ apiKey });
635
+ const { title, story, author, style, includeDialogue } = storyDetails;
636
+ // Auto-select style if "I don't know" is chosen
637
+ const actualStyle = style === "I don't know" ? selectOptimalStyle(story) : style;
638
  const allImages: string[] = [];
639
 
640
  // Step 1: Generate Character Profiles
641
+ performanceTracker.startTimer('character_analysis');
642
+ updateProgress({
643
+ phase: 'character_analysis',
644
+ message: 'Analyzing story and developing characters...',
645
+ progress: 10,
646
+ currentStepStartTime: Date.now(),
647
+ stepTimings: performanceTracker.getTimings()
648
+ });
649
+ const characters = await generateCharacterProfiles(story, actualStyle);
650
+ const charAnalysisTiming = performanceTracker.endTimer('character_analysis');
651
+
652
+ // Step 2: Generate Character Sketches
653
+ performanceTracker.startTimer('character_sketches');
654
+ updateProgress({
655
+ phase: 'character_sketches',
656
+ message: `Found ${characters.length} characters, creating design sketches (${(charAnalysisTiming.duration / 1000).toFixed(1)}s)...`,
657
+ progress: 20,
658
+ currentStepStartTime: Date.now(),
659
+ stepTimings: performanceTracker.getTimings()
660
+ });
661
+ const characterSketches = await generateCharacterSketches(characters, actualStyle);
662
+ const sketchTiming = performanceTracker.endTimer('character_sketches');
663
 
664
+ // Step 3: Generate Environment Sketches
665
+ performanceTracker.startTimer('environment_sketches');
666
+ updateProgress({
667
+ phase: 'environment_sketches',
668
+ message: `Generated ${characterSketches.length} character sketches (${(sketchTiming.duration / 1000).toFixed(1)}s), designing environments...`,
669
+ progress: 30,
670
+ characterSketches: characterSketches,
671
+ currentStepStartTime: Date.now(),
672
+ stepTimings: performanceTracker.getTimings()
673
+ });
674
+ const environmentSketches = await generateEnvironmentSketches(story, actualStyle);
675
+ const envTiming = performanceTracker.endTimer('environment_sketches');
676
+
677
+ // Step 4: Generate Manga Script
678
+ performanceTracker.startTimer('script_writing');
679
+ updateProgress({
680
+ phase: 'script_writing',
681
+ message: `Generated ${environmentSketches.length} environment designs (${(envTiming.duration / 1000).toFixed(1)}s), writing script...`,
682
+ progress: 40,
683
+ characterSketches: characterSketches,
684
+ environmentSketches: environmentSketches,
685
+ currentStepStartTime: Date.now(),
686
+ stepTimings: performanceTracker.getTimings()
687
+ });
688
+ const scriptPages = await generateMangaScript(story, actualStyle, characters, includeDialogue);
689
+ const scriptTiming = performanceTracker.endTimer('script_writing');
690
  if (scriptPages.length === 0) {
691
  throw new Error("The generated script was empty. Please try a different story.");
692
  }
693
 
694
  const totalPagesToGenerate = scriptPages.length + 2; // script pages + title + conclusion
695
  let pagesGenerated = 0;
696
+ const generatedPages: { pageNumber: number; image: string; title: string }[] = [];
697
 
698
  const characterPromptPart = characters.map(c => `${c.name}: ${c.description}`).join('; ');
699
 
700
  const updatePageProgress = () => {
701
  pagesGenerated++;
702
+ // Progress from 50% to 95% is for image generation
703
+ const progress = 50 + Math.round((pagesGenerated / totalPagesToGenerate) * 40);
704
  return progress;
705
  };
706
 
707
+ // Step 5: Generate Title Page
708
+ performanceTracker.startTimer('page_generation');
709
+ updateProgress({
710
+ phase: 'page_generation',
711
+ message: `Created ${scriptPages.length} page script (${(scriptTiming.duration / 1000).toFixed(1)}s), generating title page...`,
712
+ progress: 50,
713
+ characterSketches: characterSketches,
714
+ environmentSketches: environmentSketches,
715
+ generatedPages: generatedPages,
716
+ currentStepStartTime: Date.now(),
717
+ stepTimings: performanceTracker.getTimings()
718
+ });
719
+
720
+ let styleDescription = "";
721
+ if (actualStyle === 'Wacky') {
722
+ styleDescription = "Wacky One Piece-style with exaggerated, rubber-hose animation aesthetics, impossible physics, and zany character designs";
723
+ } else if (actualStyle === 'Chibi') {
724
+ styleDescription = "Chibi style with cute, simplified characters having large heads and minimal features";
725
+ } else {
726
+ styleDescription = actualStyle;
727
+ }
728
+
729
  const titlePagePrompt = `TASK: Create a dynamic manga title page.
730
+ STYLE: ${styleDescription}, black and white.
731
  ASPECT RATIO: 3:4 vertical.
732
  TITLE: "${title}"
733
  AUTHOR: "${author}"
 
736
 
737
  let titleImage: string;
738
  try {
739
+ titleImage = await generatePageImage(titlePagePrompt, undefined, undefined, undefined, includeDialogue);
740
  } catch (error) {
741
  console.warn('Title page generation failed, retrying once...', error);
742
+ titleImage = await generatePageImage(titlePagePrompt, undefined, undefined, undefined, includeDialogue);
743
  }
744
  allImages.push(titleImage);
745
+ generatedPages.push({
746
+ pageNumber: 0,
747
+ image: titleImage,
748
+ title: 'Title Page'
749
+ });
750
 
751
+ // Step 6: Generate each page from the script
 
752
  for (const page of scriptPages) {
753
  const progress = updatePageProgress();
754
+ updateProgress({
755
+ phase: 'page_generation',
756
+ message: `Drawing page ${page.pageNumber} of ${scriptPages.length}...`,
757
+ progress: progress,
758
+ characterSketches: characterSketches,
759
+ environmentSketches: environmentSketches,
760
+ generatedPages: generatedPages
761
+ });
762
 
763
  const panelPrompts = page.panels.map(p => {
764
  let panelString = `Panel ${p.panelNumber}: ${p.description}`;
765
+ if (includeDialogue && p.dialogue && p.speaker) {
766
  const speakingCharacter = characters.find(c => c.name === p.speaker);
767
  if (speakingCharacter) {
768
+ // Link the visual description directly to the dialogue
769
  panelString += `\n - Dialogue: The character speaking is ${speakingCharacter.name} (${speakingCharacter.description}). They say: "${p.dialogue}"`;
770
  } else {
771
  panelString += `\n - Dialogue (${p.speaker}): "${p.dialogue}"`;
772
  }
773
+ } else if (!includeDialogue) {
774
+ panelString += `\n - NO DIALOGUE: Focus on visual storytelling and character expressions`;
775
  }
776
  return panelString;
777
  }).join('\n\n');
778
+
779
+ // Enhanced prompt for first two pages with character/environment sketches
780
+ let additionalContext = "";
781
+ if (page.pageNumber <= 2) {
782
+ if (characterSketches.length > 0) {
783
+ additionalContext += `\n\nCHARACTER DESIGN REFERENCES (use these for visual consistency):\n${characterSketches.map(sketch => `${sketch.characterName}: ${sketch.description}`).join('\n')}`;
784
+ }
785
+ if (environmentSketches.length > 0) {
786
+ additionalContext += `\n\nENVIRONMENT REFERENCES (use for setting details):\n${environmentSketches.map(env => `${env.environmentName}: ${env.description}`).join('\n')}`;
787
+ }
788
+ }
789
 
790
  const pagePrompt = `TASK: Create a manga page image.
791
+ STYLE: ${styleDescription}, black and white.
792
  ASPECT RATIO: 3:4 vertical.
793
  CHARACTERS: ${characterPromptPart}.
794
+ PAGE: ${page.pageNumber}, with ${page.panels.length} panels.${additionalContext}
795
 
796
  PANEL INSTRUCTIONS:
797
  ${panelPrompts}
798
 
799
+ FINAL INSTRUCTIONS: Arrange panels dynamically. ${includeDialogue ? 'Place dialogue in speech bubbles for the correct characters.' : 'NO speech bubbles or text - pure visual storytelling.'} ${page.pageNumber <= 2 ? 'Use the character and environment references above for accurate visual consistency.' : ''} Generate ONLY the image.`;
800
 
801
  // Pass the last generated image as a reference for consistency
802
  const previousImage = allImages[allImages.length - 1];
803
+ const pageStartTime = Date.now();
804
  let pageImage: string;
805
  try {
806
+ pageImage = await generatePageImage(pagePrompt, previousImage, page, actualStyle, includeDialogue);
807
  } catch (error) {
808
  console.warn(`Page ${page.pageNumber} generation failed, retrying once...`, error);
809
+ pageImage = await generatePageImage(pagePrompt, previousImage, page, actualStyle, includeDialogue);
810
  }
811
+ const pageGenTime = Date.now() - pageStartTime;
812
  allImages.push(pageImage);
813
+ generatedPages.push({
814
+ pageNumber: page.pageNumber,
815
+ image: pageImage,
816
+ title: `Page ${page.pageNumber}`
817
+ });
818
+
819
+ // Update with timing info
820
+ updateProgress({
821
+ phase: 'page_generation',
822
+ message: `Generated page ${page.pageNumber} of ${scriptPages.length} (${(pageGenTime / 1000).toFixed(1)}s)`,
823
+ progress: progress,
824
+ characterSketches: characterSketches,
825
+ environmentSketches: environmentSketches,
826
+ generatedPages: generatedPages,
827
+ stepTimings: performanceTracker.getTimings()
828
+ });
829
  }
830
 
831
+ // Step 7: Generate Conclusion Page
832
  const conclusionProgress = updatePageProgress();
833
+ updateProgress({
834
+ phase: 'page_generation',
835
+ message: 'Creating conclusion page...',
836
+ progress: conclusionProgress,
837
+ characterSketches: characterSketches,
838
+ environmentSketches: environmentSketches,
839
+ generatedPages: generatedPages
840
+ });
841
  const conclusionPagePrompt = `TASK: Create a final, evocative manga page.
842
+ STYLE: ${styleDescription}, black and white.
843
  ASPECT RATIO: 3:4 vertical.
844
  STORY CONTEXT: The story was about "${story}".
845
  INSTRUCTIONS: The page should feel like a conclusion. Maybe a character looking towards the horizon or a symbolic image related to the story. Include a small, stylized "The End" text. Generate ONLY the image.`;
 
848
  const lastContentPage = allImages[allImages.length - 1];
849
  let conclusionImage: string;
850
  try {
851
+ conclusionImage = await generatePageImage(conclusionPagePrompt, lastContentPage, undefined, undefined, includeDialogue);
852
  } catch (error) {
853
  console.warn('Conclusion page generation failed, retrying once...', error);
854
+ conclusionImage = await generatePageImage(conclusionPagePrompt, lastContentPage, undefined, undefined, includeDialogue);
855
  }
856
  allImages.push(conclusionImage);
857
+ generatedPages.push({
858
+ pageNumber: scriptPages.length + 1,
859
+ image: conclusionImage,
860
+ title: 'Conclusion'
861
+ });
862
+
863
+ // Step 8: Generate Story Arc Summary
864
+ performanceTracker.startTimer('story_analysis');
865
+ updateProgress({
866
+ phase: 'completion',
867
+ message: 'Analyzing story arc and creating summary...',
868
+ progress: 95,
869
+ characterSketches: characterSketches,
870
+ environmentSketches: environmentSketches,
871
+ generatedPages: generatedPages,
872
+ currentStepStartTime: Date.now(),
873
+ stepTimings: performanceTracker.getTimings()
874
+ });
875
+ const storyArcSummary = await generateStoryArcSummary(scriptPages, characters);
876
+ const analysisTiming = performanceTracker.endTimer('story_analysis');
877
+ const pageGenTiming = performanceTracker.endTimer('page_generation');
878
+ const totalTiming = performanceTracker.endTimer('total_generation');
879
+ const totalTime = Date.now() - startTime;
880
+
881
+ updateProgress({
882
+ phase: 'completion',
883
+ message: `Comic generation complete! (Total: ${(totalTime / 1000).toFixed(1)}s)`,
884
+ progress: 100,
885
+ characterSketches: characterSketches,
886
+ environmentSketches: environmentSketches,
887
+ generatedPages: generatedPages,
888
+ storyArcSummary: `Theme: ${storyArcSummary.overallTheme}\n\nKey Events:\n${storyArcSummary.keyEvents.map(event => `• ${event}`).join('\n')}\n\nCharacter Development:\n${storyArcSummary.characterDevelopment.map(dev => `• ${dev}`).join('\n')}`,
889
+ stepTimings: performanceTracker.getTimings(),
890
+ estimatedTimeRemaining: 0
891
+ });
892
+
893
+ return {
894
+ images: allImages,
895
+ characters: characters,
896
+ pages: scriptPages,
897
+ characterSketches: characterSketches,
898
+ environmentSketches: environmentSketches,
899
+ storyArcSummary: storyArcSummary,
900
+ performanceTimings: performanceTracker.getTimings(),
901
+ totalGenerationTime: totalTime
902
+ };
903
+ };
904
+
905
+ // Schema for story progression generation
906
+ const storyProgressionSchema = {
907
+ type: Type.ARRAY,
908
+ items: {
909
+ type: Type.OBJECT,
910
+ properties: {
911
+ id: {
912
+ type: Type.STRING,
913
+ description: "Unique identifier for this progression option."
914
+ },
915
+ title: {
916
+ type: Type.STRING,
917
+ description: "Compelling title for this story progression."
918
+ },
919
+ description: {
920
+ type: Type.STRING,
921
+ description: "Brief description of what this progression offers."
922
+ },
923
+ direction: {
924
+ type: Type.STRING,
925
+ description: "Type of story progression: sequel, prequel, side_story, or alternate_ending"
926
+ },
927
+ synopsis: {
928
+ type: Type.STRING,
929
+ description: "Detailed synopsis of the progression storyline."
930
+ },
931
+ estimatedPages: {
932
+ type: Type.INTEGER,
933
+ description: "Estimated number of pages this progression would require."
934
+ },
935
+ thematicFocus: {
936
+ type: Type.STRING,
937
+ description: "Main theme or focus of this progression."
938
+ },
939
+ newCharacters: {
940
+ type: Type.ARRAY,
941
+ items: {
942
+ type: Type.STRING
943
+ },
944
+ description: "Names of potential new characters to be introduced."
945
+ },
946
+ plotHooks: {
947
+ type: Type.ARRAY,
948
+ items: {
949
+ type: Type.STRING
950
+ },
951
+ description: "Key plot hooks that would drive this progression."
952
+ }
953
+ },
954
+ required: ['id', 'title', 'description', 'direction', 'synopsis', 'estimatedPages', 'thematicFocus', 'plotHooks']
955
+ }
956
+ };
957
+
958
+ /**
959
+ * Analyzes a completed comic story and generates three compelling progression options
960
+ */
961
+ export const analyzeStoryProgressions = async (
962
+ currentIteration: ComicIteration,
963
+ apiKey: string
964
+ ): Promise<StoryProgression[]> => {
965
+ ai = new GoogleGenAI({ apiKey });
966
+
967
+ const characterDescriptions = currentIteration.characters.map(c => `${c.name}: ${c.description}`).join('; ');
968
+ const storyContext = currentIteration.pages.map(page =>
969
+ page.panels.map(panel => `Panel ${panel.panelNumber}: ${panel.description}${panel.dialogue ? ` - "${panel.dialogue}"` : ''}`).join(' ')
970
+ ).join('\n');
971
+
972
+ const prompt = `You are a master story architect and creative director specializing in compelling narrative continuation and expansion. You understand how to build upon existing stories while maintaining thematic coherence and character development.
973
+
974
+ STORY ANALYSIS MISSION:
975
+ Analyze this completed comic story and generate exactly 3 distinct, professionally compelling progression options that would make readers excited to continue the journey.
976
+
977
+ CURRENT STORY CONTEXT:
978
+ Title: "${currentIteration.title}"
979
+ Author: ${currentIteration.author}
980
+ Style: ${currentIteration.style}
981
+ Characters: ${characterDescriptions}
982
+
983
+ STORY CONTENT:
984
+ ${storyContext}
985
+
986
+ PROGRESSION REQUIREMENTS:
987
+ 1. Each option must offer a genuinely different storytelling approach
988
+ 2. Maintain thematic consistency with the original story
989
+ 3. Introduce compelling new conflicts or deepen existing ones
990
+ 4. Consider character growth opportunities
991
+ 5. Ensure each progression has strong visual storytelling potential
992
+ 6. Balance familiar elements with fresh narrative directions
993
+
994
+ PROGRESSION TYPES TO CONSIDER:
995
+ - SEQUEL: Continue the story forward in time, exploring consequences and new challenges
996
+ - PREQUEL: Explore backstory or origins that add depth to current events
997
+ - SIDE_STORY: Focus on secondary characters or parallel events during the main story
998
+ - ALTERNATE_ENDING: Explore "what if" scenarios from a key decision point
999
+
1000
+ CREATIVE GUIDELINES:
1001
+ - Think like a professional comic editor planning a series
1002
+ - Each progression should feel like a natural extension of the story world
1003
+ - Consider reader engagement and emotional investment
1004
+ - Include potential for visual spectacle and character development
1005
+ - Ensure each option offers unique thematic exploration
1006
+
1007
+ QUALITY STANDARDS:
1008
+ - Professional comic book industry storytelling standards
1009
+ - Character-driven narrative development
1010
+ - Visual storytelling opportunities
1011
+ - Reader engagement and page-turning moments
1012
+ - Thematic depth and emotional resonance
1013
+
1014
+ Generate 3 distinct story progression options that would make this comic into a compelling series.`;
1015
+
1016
+ try {
1017
+ const response = await ai.models.generateContent({
1018
+ model: "gemini-2.5-flash",
1019
+ contents: prompt,
1020
+ config: {
1021
+ responseMimeType: "application/json",
1022
+ responseSchema: storyProgressionSchema,
1023
+ },
1024
+ });
1025
+
1026
+ const jsonText = response.text.trim();
1027
+ return JSON.parse(jsonText) as StoryProgression[];
1028
+ } catch (e) {
1029
+ console.error("Error generating story progressions:", e);
1030
+ throw new Error("Failed to generate story progression options.");
1031
+ }
1032
+ };
1033
+
1034
+ /**
1035
+ * Generates a comic continuation based on selected story progression
1036
+ */
1037
+ export const generateComicContinuation = async (
1038
+ previousIteration: ComicIteration,
1039
+ selectedProgression: StoryProgression,
1040
+ updateProgress: (state: LoadingState) => void,
1041
+ apiKey: string
1042
+ ): Promise<ComicIteration> => {
1043
+ ai = new GoogleGenAI({ apiKey });
1044
+
1045
+ updateProgress({ isLoading: true, message: 'Developing continuation story...', progress: 10 });
1046
+
1047
+ // Create the continuation story details
1048
+ const continuationStory = `${selectedProgression.synopsis}\n\nBuilding upon: "${previousIteration.story}"\nProgression Focus: ${selectedProgression.thematicFocus}`;
1049
+
1050
+ // Generate new character profiles (combining existing + new characters)
1051
+ updateProgress({ isLoading: true, message: 'Expanding character cast...', progress: 25 });
1052
+ let allCharacters = [...previousIteration.characters];
1053
+
1054
+ if (selectedProgression.newCharacters && selectedProgression.newCharacters.length > 0) {
1055
+ const newCharacterPrompt = `Create detailed character profiles for these new characters to be introduced in the story continuation: ${selectedProgression.newCharacters.join(', ')}
1056
+
1057
+ Context: This is a continuation of "${previousIteration.title}" in the ${previousIteration.style} style.
1058
+ Story Direction: ${selectedProgression.direction}
1059
+ Thematic Focus: ${selectedProgression.thematicFocus}
1060
+
1061
+ Existing Characters: ${previousIteration.characters.map(c => `${c.name}: ${c.description}`).join('; ')}
1062
+
1063
+ Create characters that complement the existing cast and serve the new storyline.`;
1064
+
1065
+ try {
1066
+ const newCharacterResponse = await ai.models.generateContent({
1067
+ model: "gemini-2.5-flash",
1068
+ contents: newCharacterPrompt,
1069
+ config: {
1070
+ responseMimeType: "application/json",
1071
+ responseSchema: characterProfileSchema,
1072
+ },
1073
+ });
1074
+
1075
+ const newCharacters = JSON.parse(newCharacterResponse.text.trim()) as CharacterProfile[];
1076
+ allCharacters = [...allCharacters, ...newCharacters];
1077
+ } catch (e) {
1078
+ console.warn("Failed to generate new characters, continuing with existing cast:", e);
1079
+ }
1080
+ }
1081
+
1082
+ // Generate manga script for continuation
1083
+ updateProgress({ isLoading: true, message: 'Writing continuation script...', progress: 40 });
1084
+ const continuationPages = await generateMangaScript(continuationStory, previousIteration.style, allCharacters, previousIteration.includeDialogue ?? true);
1085
+
1086
+ if (continuationPages.length === 0) {
1087
+ throw new Error("The continuation script was empty. Please try a different progression.");
1088
+ }
1089
+
1090
+ // Generate images for continuation
1091
+ const totalPagesToGenerate = continuationPages.length + 1; // story pages + conclusion
1092
+ let pagesGenerated = 0;
1093
+ const allImages: string[] = [];
1094
+
1095
+ const updatePageProgress = () => {
1096
+ pagesGenerated++;
1097
+ const progress = 50 + Math.round((pagesGenerated / totalPagesToGenerate) * 45);
1098
+ return progress;
1099
+ };
1100
+
1101
+ const characterPromptPart = allCharacters.map(c => `${c.name}: ${c.description}`).join('; ');
1102
+
1103
+ // Use the last image from previous iteration as style reference
1104
+ const styleReference = previousIteration.images[previousIteration.images.length - 1];
1105
+
1106
+ // Generate each page from the continuation script
1107
+ for (const page of continuationPages) {
1108
+ const progress = updatePageProgress();
1109
+ updateProgress({ isLoading: true, message: `Drawing continuation page ${page.pageNumber} of ${continuationPages.length}...`, progress });
1110
+
1111
+ const panelPrompts = page.panels.map(p => {
1112
+ let panelString = `Panel ${p.panelNumber}: ${p.description}`;
1113
+ if (p.dialogue && p.speaker) {
1114
+ const speakingCharacter = allCharacters.find(c => c.name === p.speaker);
1115
+ if (speakingCharacter) {
1116
+ panelString += `\n - Dialogue: The character speaking is ${speakingCharacter.name} (${speakingCharacter.description}). They say: "${p.dialogue}"`;
1117
+ } else {
1118
+ panelString += `\n - Dialogue (${p.speaker}): "${p.dialogue}"`;
1119
+ }
1120
+ }
1121
+ return panelString;
1122
+ }).join('\n\n');
1123
+
1124
+ let styleDescription = "";
1125
+ if (previousIteration.style === 'Wacky') {
1126
+ styleDescription = "Wacky One Piece-style with exaggerated, rubber-hose animation aesthetics, impossible physics, and zany character designs";
1127
+ } else if (previousIteration.style === 'Chibi') {
1128
+ styleDescription = "Chibi style with cute, simplified characters having large heads and minimal features";
1129
+ } else {
1130
+ styleDescription = previousIteration.style;
1131
+ }
1132
+
1133
+ const pagePrompt = `TASK: Create a manga continuation page image.
1134
+ STYLE: ${styleDescription}, black and white.
1135
+ ASPECT RATIO: 3:4 vertical.
1136
+ CHARACTERS: ${characterPromptPart}.
1137
+ PAGE: ${page.pageNumber}, with ${page.panels.length} panels.
1138
+ STORY CONTINUATION: This continues the story "${previousIteration.title}" with progression: ${selectedProgression.title}
1139
+
1140
+ PANEL INSTRUCTIONS:
1141
+ ${panelPrompts}
1142
+
1143
+ FINAL INSTRUCTIONS: Maintain visual consistency with the original story. Arrange panels dynamically. ${(previousIteration.includeDialogue ?? true) ? 'Place dialogue in speech bubbles for the correct characters.' : 'DO NOT include any dialogue, speech bubbles, or text - this is pure visual storytelling.'} Generate ONLY the image.`;
1144
+
1145
+ let pageImage: string;
1146
+ try {
1147
+ pageImage = await generatePageImage(pagePrompt, styleReference, page, previousIteration.style, previousIteration.includeDialogue ?? true);
1148
+ } catch (error) {
1149
+ console.warn(`Continuation page ${page.pageNumber} generation failed, retrying once...`, error);
1150
+ pageImage = await generatePageImage(pagePrompt, styleReference, page, previousIteration.style, previousIteration.includeDialogue ?? true);
1151
+ }
1152
+ allImages.push(pageImage);
1153
+ }
1154
+
1155
+ // Generate conclusion page for continuation
1156
+ const conclusionProgress = updatePageProgress();
1157
+ updateProgress({ isLoading: true, message: 'Creating continuation conclusion...', progress: conclusionProgress });
1158
+
1159
+ let styleDescription = "";
1160
+ if (previousIteration.style === 'Wacky') {
1161
+ styleDescription = "Wacky One Piece-style with exaggerated, rubber-hose animation aesthetics, impossible physics, and zany character designs";
1162
+ } else if (previousIteration.style === 'Chibi') {
1163
+ styleDescription = "Chibi style with cute, simplified characters having large heads and minimal features";
1164
+ } else {
1165
+ styleDescription = previousIteration.style;
1166
+ }
1167
+
1168
+ const conclusionPagePrompt = `TASK: Create a final, evocative manga conclusion page for the continuation.
1169
+ STYLE: ${styleDescription}, black and white.
1170
+ ASPECT RATIO: 3:4 vertical.
1171
+ STORY CONTEXT: This concludes the continuation "${selectedProgression.title}" of the original story "${previousIteration.title}".
1172
+ THEMATIC FOCUS: ${selectedProgression.thematicFocus}
1173
+ INSTRUCTIONS: Create a satisfying conclusion that reflects the continuation's thematic focus. Include a small, stylized "To Be Continued..." or "End of Chapter" text. Generate ONLY the image.`;
1174
+
1175
+ const lastContentPage = allImages[allImages.length - 1];
1176
+ let conclusionImage: string;
1177
+ try {
1178
+ conclusionImage = await generatePageImage(conclusionPagePrompt, lastContentPage, undefined, undefined, previousIteration.includeDialogue ?? true);
1179
+ } catch (error) {
1180
+ console.warn('Continuation conclusion page generation failed, retrying once...', error);
1181
+ conclusionImage = await generatePageImage(conclusionPagePrompt, lastContentPage, undefined, undefined, previousIteration.includeDialogue ?? true);
1182
+ }
1183
+ allImages.push(conclusionImage);
1184
+
1185
+ updateProgress({ isLoading: true, message: 'Finalizing continuation...', progress: 100 });
1186
+
1187
+ // Create the new iteration
1188
+ const newIteration: ComicIteration = {
1189
+ id: `${previousIteration.id}_${selectedProgression.id}`,
1190
+ title: selectedProgression.title,
1191
+ story: continuationStory,
1192
+ author: previousIteration.author,
1193
+ style: previousIteration.style,
1194
+ includeDialogue: previousIteration.includeDialogue ?? true,
1195
+ images: allImages,
1196
+ characters: allCharacters,
1197
+ pages: continuationPages,
1198
+ generatedAt: new Date()
1199
+ };
1200
 
1201
+ return newIteration;
 
1202
  };
services/pdfService.ts CHANGED
@@ -1,39 +1,360 @@
1
-
2
- // Use global jspdf from CDN
3
- declare const jspdf: any;
4
 
5
  export const createMangaPdf = (images: string[], title: string): void => {
6
- const { jsPDF } = jspdf;
7
- // Using a common manga/comic book aspect ratio (3:4) for PDF pages.
8
- const pdfWidth = 150; // mm
9
- const pdfHeight = 200; // mm to maintain 3:4 aspect ratio
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  const doc = new jsPDF({
11
- orientation: 'portrait',
12
  unit: 'mm',
13
  format: [pdfWidth, pdfHeight]
14
  });
15
 
16
- images.forEach((imgData, index) => {
17
- if (index > 0) {
18
- doc.addPage();
19
- }
20
- const imgProps = doc.getImageProperties(`data:image/jpeg;base64,${imgData}`);
21
- const aspectRatio = imgProps.width / imgProps.height;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- let imgWidth = pdfWidth;
24
- let imgHeight = pdfWidth / aspectRatio;
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- if (imgHeight > pdfHeight) {
27
- imgHeight = pdfHeight;
28
- imgWidth = pdfHeight * aspectRatio;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  }
30
 
31
- const x = (pdfWidth - imgWidth) / 2;
32
- const y = (pdfHeight - imgHeight) / 2;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- doc.addImage(`data:image/jpeg;base64,${imgData}`, 'JPEG', x, y, imgWidth, imgHeight);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  });
36
 
37
- const safeTitle = title.replace(/[^a-z0-9]/gi, '_').toLowerCase();
38
- doc.save(`${safeTitle}_manga.pdf`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  };
 
1
+ import { jsPDF } from 'jspdf';
2
+ import type { ComicSeries, ComicIteration } from '../types';
 
3
 
4
  export const createMangaPdf = (images: string[], title: string): void => {
5
+ console.log('createMangaPdf called with:', { imageCount: images.length, title });
6
+
7
+ if (images.length === 0) {
8
+ console.error('No images provided for PDF creation');
9
+ return;
10
+ }
11
+
12
+ // Create PDF with first image dimensions to maintain aspect ratio
13
+ let doc: jsPDF | null = null;
14
+
15
+ images.forEach((imgData, index) => {
16
+ if (index === 0) {
17
+ // Use jsPDF's built-in method to get image properties
18
+ try {
19
+ const imgProps = (doc as any)?.getImageProperties ?
20
+ (doc as any).getImageProperties(`data:image/jpeg;base64,${imgData}`) :
21
+ null;
22
+
23
+ // If we can't get properties, create a temporary jsPDF instance to get them
24
+ if (!imgProps) {
25
+ const tempDoc = new jsPDF();
26
+ const tempImgProps = (tempDoc as any).getImageProperties(`data:image/jpeg;base64,${imgData}`);
27
+ var aspectRatio = tempImgProps.width / tempImgProps.height;
28
+ console.log('Image dimensions from temp doc:', { width: tempImgProps.width, height: tempImgProps.height, aspectRatio });
29
+ } else {
30
+ var aspectRatio = imgProps.width / imgProps.height;
31
+ console.log('Image dimensions:', { width: imgProps.width, height: imgProps.height, aspectRatio });
32
+ }
33
+ } catch (error) {
34
+ // Fallback: assume standard comic book aspect ratio (3:4 portrait)
35
+ console.warn('Could not determine image dimensions, using default comic aspect ratio:', error);
36
+ var aspectRatio = 3 / 4; // Standard comic book ratio
37
+ }
38
+
39
+ // Scale to reasonable print size while maintaining aspect ratio
40
+ // Use a base size and scale according to aspect ratio
41
+ const baseSize = 200; // mm - base dimension for the larger side
42
+
43
+ let pageWidth, pageHeight;
44
+ if (aspectRatio > 1) {
45
+ // Landscape/Wide format
46
+ pageWidth = baseSize;
47
+ pageHeight = baseSize / aspectRatio;
48
+ } else {
49
+ // Portrait/Tall format
50
+ pageHeight = baseSize;
51
+ pageWidth = baseSize * aspectRatio;
52
+ }
53
+
54
+ console.log('PDF page dimensions:', { pageWidth, pageHeight, aspectRatio });
55
+
56
+ doc = new jsPDF({
57
+ orientation: aspectRatio > 1 ? 'landscape' : 'portrait',
58
+ unit: 'mm',
59
+ format: [pageWidth, pageHeight]
60
+ });
61
+
62
+ // Add first image filling entire page with no margins
63
+ doc.addImage(`data:image/jpeg;base64,${imgData}`, 'JPEG', 0, 0, pageWidth, pageHeight);
64
+ } else {
65
+ // For subsequent pages, maintain the same format as the first page
66
+ if (doc) {
67
+ doc.addPage();
68
+ const pageSize = doc.internal.pageSize;
69
+ const currentWidth = pageSize.getWidth();
70
+ const currentHeight = pageSize.getHeight();
71
+ doc.addImage(`data:image/jpeg;base64,${imgData}`, 'JPEG', 0, 0, currentWidth, currentHeight);
72
+ }
73
+ }
74
+ });
75
+
76
+ const safeTitle = title.replace(/[^a-z0-9]/gi, '_').toLowerCase();
77
+ const filename = `${safeTitle}_manga.pdf`;
78
+ console.log('Saving PDF with filename:', filename);
79
+
80
+ try {
81
+ doc.save(filename);
82
+ console.log('PDF save command executed successfully');
83
+ } catch (error) {
84
+ console.error('Error saving PDF:', error);
85
+ }
86
+ };
87
+
88
+ /**
89
+ * Creates a comprehensive PDF from a complete comic series with multiple iterations
90
+ * Uses the aspect ratio of the first image in the series
91
+ */
92
+ export const createComicSeriesPdf = (comicSeries: ComicSeries): void => {
93
+ console.log('createComicSeriesPdf called with:', { iterations: comicSeries.iterations.length, seriesTitle: comicSeries.seriesTitle });
94
+
95
+ // Get dimensions from the first image in the first iteration
96
+ const firstIteration = comicSeries.iterations[0];
97
+ if (!firstIteration || firstIteration.images.length === 0) {
98
+ console.error('No images found in comic series');
99
+ return;
100
+ }
101
+
102
+ const firstImageData = firstIteration.images[0];
103
+
104
+ // Use jsPDF's built-in method to get image properties
105
+ let aspectRatio;
106
+ try {
107
+ const tempDoc = new jsPDF();
108
+ const imgProps = (tempDoc as any).getImageProperties(`data:image/jpeg;base64,${firstImageData}`);
109
+ aspectRatio = imgProps.width / imgProps.height;
110
+ console.log('Series image dimensions:', { width: imgProps.width, height: imgProps.height, aspectRatio });
111
+ } catch (error) {
112
+ console.warn('Could not determine series image dimensions, using default comic aspect ratio:', error);
113
+ aspectRatio = 3 / 4; // Standard comic book ratio
114
+ }
115
+
116
+ // Use same sizing logic as single comic PDF
117
+ const baseSize = 200;
118
+ let pdfWidth, pdfHeight;
119
+ if (aspectRatio > 1) {
120
+ pdfWidth = baseSize;
121
+ pdfHeight = baseSize / aspectRatio;
122
+ } else {
123
+ pdfHeight = baseSize;
124
+ pdfWidth = baseSize * aspectRatio;
125
+ }
126
+
127
+ console.log('Series PDF dimensions:', { pdfWidth, pdfHeight, aspectRatio });
128
+
129
  const doc = new jsPDF({
130
+ orientation: aspectRatio > 1 ? 'landscape' : 'portrait',
131
  unit: 'mm',
132
  format: [pdfWidth, pdfHeight]
133
  });
134
 
135
+ let isFirstPage = true;
136
+ let totalPages = 0;
137
+
138
+ // Add title page for the series
139
+ if (isFirstPage) {
140
+ // Create a series title page
141
+ doc.setFillColor(20, 20, 30);
142
+ doc.rect(0, 0, pdfWidth, pdfHeight, 'F');
143
+
144
+ // Title
145
+ doc.setTextColor(255, 255, 255);
146
+ doc.setFontSize(24);
147
+ doc.setFont('helvetica', 'bold');
148
+ const titleLines = doc.splitTextToSize(comicSeries.seriesTitle, pdfWidth - 20);
149
+ const titleY = pdfHeight / 2 - 30;
150
+ doc.text(titleLines, pdfWidth / 2, titleY, { align: 'center' });
151
+
152
+ // Author
153
+ doc.setFontSize(16);
154
+ doc.setFont('helvetica', 'normal');
155
+ doc.text(`By ${comicSeries.author}`, pdfWidth / 2, titleY + 20, { align: 'center' });
156
+
157
+ // Series info
158
+ doc.setFontSize(12);
159
+ doc.text(`Complete Series • ${comicSeries.iterations.length} Chapters • ${comicSeries.totalPages} Pages`,
160
+ pdfWidth / 2, titleY + 35, { align: 'center' });
161
 
162
+ // Created date
163
+ doc.setFontSize(10);
164
+ doc.setTextColor(180, 180, 180);
165
+ doc.text(`Created: ${comicSeries.createdAt.toLocaleDateString()}`,
166
+ pdfWidth / 2, titleY + 50, { align: 'center' });
167
+
168
+ // "Comic Genesis AI" attribution
169
+ doc.text('Generated by Comic Genesis AI',
170
+ pdfWidth / 2, pdfHeight - 20, { align: 'center' });
171
+
172
+ isFirstPage = false;
173
+ totalPages = 1;
174
+ }
175
 
176
+ // Add each iteration with separator pages
177
+ comicSeries.iterations.forEach((iteration, iterationIndex) => {
178
+ // Add chapter divider page (except for the first iteration)
179
+ if (iterationIndex > 0) {
180
+ doc.addPage();
181
+ totalPages++;
182
+
183
+ // Chapter divider page
184
+ doc.setFillColor(30, 30, 40);
185
+ doc.rect(0, 0, pdfWidth, pdfHeight, 'F');
186
+
187
+ doc.setTextColor(255, 255, 255);
188
+ doc.setFontSize(20);
189
+ doc.setFont('helvetica', 'bold');
190
+
191
+ const chapterTitle = `Chapter ${iterationIndex + 1}`;
192
+ doc.text(chapterTitle, pdfWidth / 2, pdfHeight / 2 - 20, { align: 'center' });
193
+
194
+ doc.setFontSize(16);
195
+ doc.setFont('helvetica', 'normal');
196
+ const iterationTitleLines = doc.splitTextToSize(iteration.title, pdfWidth - 20);
197
+ doc.text(iterationTitleLines, pdfWidth / 2, pdfHeight / 2, { align: 'center' });
198
+
199
+ doc.setFontSize(10);
200
+ doc.setTextColor(180, 180, 180);
201
+ doc.text(`${iteration.images.length} pages`,
202
+ pdfWidth / 2, pdfHeight / 2 + 20, { align: 'center' });
203
  }
204
 
205
+ // Add all images from this iteration
206
+ iteration.images.forEach((imgData, imageIndex) => {
207
+ if (!isFirstPage || imageIndex > 0) {
208
+ doc.addPage();
209
+ totalPages++;
210
+ }
211
+
212
+ try {
213
+ // Fill entire page with image
214
+ doc.addImage(`data:image/jpeg;base64,${imgData}`, 'JPEG', 0, 0, pdfWidth, pdfHeight);
215
+
216
+ // Add page number footer
217
+ doc.setFontSize(8);
218
+ doc.setTextColor(120, 120, 120);
219
+ doc.text(`${totalPages}`, pdfWidth - 10, pdfHeight - 5, { align: 'right' });
220
+
221
+ } catch (error) {
222
+ console.warn(`Failed to add image ${imageIndex} from iteration ${iterationIndex}:`, error);
223
+
224
+ // Add error page
225
+ doc.setFillColor(50, 50, 60);
226
+ doc.rect(0, 0, pdfWidth, pdfHeight, 'F');
227
+ doc.setTextColor(200, 200, 200);
228
+ doc.setFontSize(12);
229
+ doc.text('Image could not be loaded', pdfWidth / 2, pdfHeight / 2, { align: 'center' });
230
+ }
231
+
232
+ if (isFirstPage) {
233
+ isFirstPage = false;
234
+ }
235
+ });
236
+ });
237
+
238
+ // Add final credits/summary page
239
+ doc.addPage();
240
+ totalPages++;
241
+
242
+ doc.setFillColor(15, 15, 25);
243
+ doc.rect(0, 0, pdfWidth, pdfHeight, 'F');
244
+
245
+ doc.setTextColor(255, 255, 255);
246
+ doc.setFontSize(18);
247
+ doc.setFont('helvetica', 'bold');
248
+ doc.text('The End', pdfWidth / 2, pdfHeight / 2 - 30, { align: 'center' });
249
+
250
+ doc.setFontSize(12);
251
+ doc.setFont('helvetica', 'normal');
252
+ doc.text(`Complete Series: ${comicSeries.seriesTitle}`, pdfWidth / 2, pdfHeight / 2 - 10, { align: 'center' });
253
+ doc.text(`By ${comicSeries.author}`, pdfWidth / 2, pdfHeight / 2 + 5, { align: 'center' });
254
+
255
+ doc.setFontSize(10);
256
+ doc.setTextColor(180, 180, 180);
257
+ doc.text(`${comicSeries.iterations.length} chapters • ${totalPages} total pages`,
258
+ pdfWidth / 2, pdfHeight / 2 + 25, { align: 'center' });
259
+
260
+ doc.text(`Generated: ${new Date().toLocaleDateString()}`,
261
+ pdfWidth / 2, pdfHeight / 2 + 40, { align: 'center' });
262
+
263
+ doc.text('Powered by Comic Genesis AI & Google Gemini',
264
+ pdfWidth / 2, pdfHeight - 15, { align: 'center' });
265
 
266
+ // Save the PDF
267
+ const safeTitle = comicSeries.seriesTitle.replace(/[^a-z0-9]/gi, '_').toLowerCase();
268
+ doc.save(`${safeTitle}_complete_series.pdf`);
269
+ };
270
+
271
+ /**
272
+ * Creates a PDF from a single comic iteration with chapter formatting
273
+ * Uses the aspect ratio of the first image in the iteration
274
+ */
275
+ export const createIterationPdf = (iteration: ComicIteration, chapterNumber?: number): void => {
276
+ if (iteration.images.length === 0) {
277
+ console.error('No images found in iteration');
278
+ return;
279
+ }
280
+
281
+ // Get dimensions from the first image using jsPDF
282
+ const firstImageData = iteration.images[0];
283
+
284
+ let aspectRatio;
285
+ try {
286
+ const tempDoc = new jsPDF();
287
+ const imgProps = (tempDoc as any).getImageProperties(`data:image/jpeg;base64,${firstImageData}`);
288
+ aspectRatio = imgProps.width / imgProps.height;
289
+ console.log('Iteration image dimensions:', { width: imgProps.width, height: imgProps.height, aspectRatio });
290
+ } catch (error) {
291
+ console.warn('Could not determine iteration image dimensions, using default comic aspect ratio:', error);
292
+ aspectRatio = 3 / 4; // Standard comic book ratio
293
+ }
294
+
295
+ // Use same sizing logic as other PDF functions
296
+ const baseSize = 200;
297
+ let pdfWidth, pdfHeight;
298
+ if (aspectRatio > 1) {
299
+ pdfWidth = baseSize;
300
+ pdfHeight = baseSize / aspectRatio;
301
+ } else {
302
+ pdfHeight = baseSize;
303
+ pdfWidth = baseSize * aspectRatio;
304
+ }
305
+
306
+ const doc = new jsPDF({
307
+ orientation: aspectRatio > 1 ? 'landscape' : 'portrait',
308
+ unit: 'mm',
309
+ format: [pdfWidth, pdfHeight]
310
  });
311
 
312
+ let isFirstPage = true;
313
+
314
+ // Add chapter title page if chapter number is provided
315
+ if (chapterNumber) {
316
+ doc.setFillColor(20, 20, 30);
317
+ doc.rect(0, 0, pdfWidth, pdfHeight, 'F');
318
+
319
+ doc.setTextColor(255, 255, 255);
320
+ doc.setFontSize(20);
321
+ doc.setFont('helvetica', 'bold');
322
+ doc.text(`Chapter ${chapterNumber}`, pdfWidth / 2, pdfHeight / 2 - 20, { align: 'center' });
323
+
324
+ doc.setFontSize(16);
325
+ doc.setFont('helvetica', 'normal');
326
+ const titleLines = doc.splitTextToSize(iteration.title, pdfWidth - 20);
327
+ doc.text(titleLines, pdfWidth / 2, pdfHeight / 2, { align: 'center' });
328
+
329
+ doc.setFontSize(10);
330
+ doc.text(`By ${iteration.author}`, pdfWidth / 2, pdfHeight / 2 + 20, { align: 'center' });
331
+
332
+ isFirstPage = false;
333
+ }
334
+
335
+ // Add all images
336
+ iteration.images.forEach((imgData, index) => {
337
+ if (!isFirstPage || index > 0) {
338
+ doc.addPage();
339
+ }
340
+
341
+ try {
342
+ // Fill entire page with image
343
+ doc.addImage(`data:image/jpeg;base64,${imgData}`, 'JPEG', 0, 0, pdfWidth, pdfHeight);
344
+
345
+ } catch (error) {
346
+ console.warn(`Failed to add image ${index}:`, error);
347
+ }
348
+
349
+ if (isFirstPage) {
350
+ isFirstPage = false;
351
+ }
352
+ });
353
+
354
+ const safeTitle = iteration.title.replace(/[^a-z0-9]/gi, '_').toLowerCase();
355
+ const fileName = chapterNumber ?
356
+ `${safeTitle}_chapter_${chapterNumber}.pdf` :
357
+ `${safeTitle}.pdf`;
358
+
359
+ doc.save(fileName);
360
  };
types.ts CHANGED
@@ -33,9 +33,111 @@ export interface StoryTemplate {
33
  icon: string;
34
  }
35
 
 
 
 
 
 
 
 
 
36
  export interface NarrativeStructure {
37
  act: number;
38
  name: string;
39
  description: string;
40
  keyElements: string[];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  }
 
33
  icon: string;
34
  }
35
 
36
+ export interface ComicGenerationOptions {
37
+ includeDialogue: boolean;
38
+ title: string;
39
+ story: string;
40
+ author: string;
41
+ style: MangaStyle;
42
+ }
43
+
44
  export interface NarrativeStructure {
45
  act: number;
46
  name: string;
47
  description: string;
48
  keyElements: string[];
49
+ }
50
+
51
+ export interface StoryProgression {
52
+ id: string;
53
+ title: string;
54
+ description: string;
55
+ direction: 'sequel' | 'prequel' | 'side_story' | 'alternate_ending';
56
+ synopsis: string;
57
+ estimatedPages: number;
58
+ thematicFocus: string;
59
+ newCharacters?: string[];
60
+ plotHooks: string[];
61
+ }
62
+
63
+ export interface ComicIteration {
64
+ id: string;
65
+ title: string;
66
+ story: string;
67
+ author: string;
68
+ style: MangaStyle;
69
+ includeDialogue?: boolean;
70
+ images: string[];
71
+ characters: CharacterProfile[];
72
+ pages: MangaPage[];
73
+ generatedAt: Date;
74
+ }
75
+
76
+ export interface ComicSeries {
77
+ seriesTitle: string;
78
+ author: string;
79
+ iterations: ComicIteration[];
80
+ totalPages: number;
81
+ createdAt: Date;
82
+ lastUpdated: Date;
83
+ totalGenerationTime?: number;
84
+ performanceSummary?: PerformanceTiming[];
85
+ }
86
+
87
+ export interface ExplorationPrompt {
88
+ showPrompt: boolean;
89
+ onContinue: () => void;
90
+ onFinish: (removeApiKey: boolean) => void;
91
+ generationTime: number;
92
+ totalPages: number;
93
+ }
94
+
95
+ export interface OptimizedImage {
96
+ compressed: string; // For display (smaller size)
97
+ full: string; // For PDF generation (full quality)
98
+ width: number;
99
+ height: number;
100
+ compressedSize: number;
101
+ fullSize: number;
102
+ }
103
+
104
+ export interface CharacterSketch {
105
+ characterName: string;
106
+ sketchImage: string;
107
+ description: string;
108
+ }
109
+
110
+ export interface EnvironmentSketch {
111
+ environmentName: string;
112
+ sketchImage: string;
113
+ description: string;
114
+ }
115
+
116
+ export interface GenerationProgress {
117
+ phase: 'character_analysis' | 'character_sketches' | 'environment_sketches' | 'script_writing' | 'page_generation' | 'completion';
118
+ message: string;
119
+ progress: number;
120
+ characterSketches?: CharacterSketch[];
121
+ environmentSketches?: EnvironmentSketch[];
122
+ generatedPages?: { pageNumber: number; image: string; title: string }[];
123
+ storyArcSummary?: string;
124
+ currentStepStartTime?: number;
125
+ stepTimings?: PerformanceTiming[];
126
+ estimatedTimeRemaining?: number;
127
+ }
128
+
129
+ export interface PerformanceTiming {
130
+ stepName: string;
131
+ startTime: number;
132
+ endTime: number;
133
+ duration: number;
134
+ timestamp: string;
135
+ }
136
+
137
+ export interface StoryArcSummary {
138
+ overallTheme: string;
139
+ keyEvents: string[];
140
+ characterDevelopment: string[];
141
+ narrativeProgression: string;
142
+ emotionalJourney: string;
143
  }
utils/imageOptimization.ts ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface OptimizedImage {
2
+ compressed: string; // For display (smaller size)
3
+ full: string; // For PDF generation (full quality)
4
+ width: number;
5
+ height: number;
6
+ compressedSize: number;
7
+ fullSize: number;
8
+ }
9
+
10
+ export interface PerformanceTiming {
11
+ stepName: string;
12
+ startTime: number;
13
+ endTime: number;
14
+ duration: number;
15
+ timestamp: string;
16
+ }
17
+
18
+ /**
19
+ * Compress base64 image for display purposes while maintaining quality for PDF
20
+ */
21
+ export async function optimizeImage(base64Image: string, quality: number = 0.7): Promise<OptimizedImage> {
22
+ return new Promise((resolve, reject) => {
23
+ try {
24
+ const img = new Image();
25
+ img.onload = () => {
26
+ // Create canvas for compression
27
+ const canvas = document.createElement('canvas');
28
+ const ctx = canvas.getContext('2d')!;
29
+
30
+ // Set canvas size to image size
31
+ canvas.width = img.width;
32
+ canvas.height = img.height;
33
+
34
+ // Draw image on canvas
35
+ ctx.drawImage(img, 0, 0);
36
+
37
+ // Create compressed version for display
38
+ const compressedBase64 = canvas.toDataURL('image/jpeg', quality);
39
+
40
+ // Calculate sizes
41
+ const fullSize = base64Image.length * 0.75; // Approximate bytes
42
+ const compressedSize = compressedBase64.split(',')[1].length * 0.75;
43
+
44
+ resolve({
45
+ compressed: compressedBase64.split(',')[1], // Remove data:image/jpeg;base64, prefix
46
+ full: base64Image,
47
+ width: img.width,
48
+ height: img.height,
49
+ compressedSize: Math.round(compressedSize),
50
+ fullSize: Math.round(fullSize)
51
+ });
52
+ };
53
+
54
+ img.onerror = () => reject(new Error('Failed to load image for optimization'));
55
+ img.src = `data:image/jpeg;base64,${base64Image}`;
56
+ } catch (error) {
57
+ reject(error);
58
+ }
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Batch optimize multiple images
64
+ */
65
+ export async function optimizeImages(images: string[], quality: number = 0.7): Promise<OptimizedImage[]> {
66
+ const optimizationPromises = images.map(img => optimizeImage(img, quality));
67
+ return Promise.all(optimizationPromises);
68
+ }
69
+
70
+ /**
71
+ * Performance timing utilities
72
+ */
73
+ export class PerformanceTracker {
74
+ private timings: PerformanceTiming[] = [];
75
+ private activeTimers: Map<string, number> = new Map();
76
+
77
+ startTimer(stepName: string): void {
78
+ const startTime = performance.now();
79
+ this.activeTimers.set(stepName, startTime);
80
+ }
81
+
82
+ endTimer(stepName: string): PerformanceTiming {
83
+ const endTime = performance.now();
84
+ const startTime = this.activeTimers.get(stepName);
85
+
86
+ if (!startTime) {
87
+ throw new Error(`Timer for "${stepName}" was not started`);
88
+ }
89
+
90
+ const timing: PerformanceTiming = {
91
+ stepName,
92
+ startTime,
93
+ endTime,
94
+ duration: endTime - startTime,
95
+ timestamp: new Date().toISOString()
96
+ };
97
+
98
+ this.timings.push(timing);
99
+ this.activeTimers.delete(stepName);
100
+
101
+ return timing;
102
+ }
103
+
104
+ getTimings(): PerformanceTiming[] {
105
+ return [...this.timings];
106
+ }
107
+
108
+ getTotalDuration(): number {
109
+ return this.timings.reduce((total, timing) => total + timing.duration, 0);
110
+ }
111
+
112
+ getTimingByStep(stepName: string): PerformanceTiming | undefined {
113
+ return this.timings.find(timing => timing.stepName === stepName);
114
+ }
115
+
116
+ clear(): void {
117
+ this.timings = [];
118
+ this.activeTimers.clear();
119
+ }
120
+
121
+ getFormattedSummary(): string {
122
+ if (this.timings.length === 0) return 'No performance data available';
123
+
124
+ const summary = this.timings.map(timing =>
125
+ `${timing.stepName}: ${(timing.duration / 1000).toFixed(2)}s`
126
+ ).join('\n');
127
+
128
+ const total = (this.getTotalDuration() / 1000).toFixed(2);
129
+ return `${summary}\n\nTotal: ${total}s`;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Lazy loading utilities
135
+ */
136
+ export interface LazyLoadOptions {
137
+ rootMargin?: string;
138
+ threshold?: number;
139
+ fallbackDelay?: number;
140
+ }
141
+
142
+ export function createLazyLoader(options: LazyLoadOptions = {}) {
143
+ const { rootMargin = '50px', threshold = 0.1, fallbackDelay = 300 } = options;
144
+
145
+ if ('IntersectionObserver' in window) {
146
+ return new IntersectionObserver((entries) => {
147
+ entries.forEach(entry => {
148
+ if (entry.isIntersecting) {
149
+ const img = entry.target as HTMLImageElement;
150
+ const src = img.dataset.src;
151
+ if (src) {
152
+ img.src = src;
153
+ img.classList.remove('lazy');
154
+ img.classList.add('loaded');
155
+ }
156
+ }
157
+ });
158
+ }, { rootMargin, threshold });
159
+ }
160
+
161
+ // Fallback for older browsers
162
+ return {
163
+ observe: (element: Element) => {
164
+ setTimeout(() => {
165
+ const img = element as HTMLImageElement;
166
+ const src = img.dataset.src;
167
+ if (src) {
168
+ img.src = src;
169
+ img.classList.remove('lazy');
170
+ img.classList.add('loaded');
171
+ }
172
+ }, fallbackDelay);
173
+ },
174
+ disconnect: () => {},
175
+ unobserve: () => {}
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Memory usage monitoring
181
+ */
182
+ export function getMemoryUsage(): { used: number; total: number; percentage: number } | null {
183
+ if ('memory' in performance) {
184
+ const memory = (performance as any).memory;
185
+ return {
186
+ used: Math.round(memory.usedJSHeapSize / 1024 / 1024), // MB
187
+ total: Math.round(memory.totalJSHeapSize / 1024 / 1024), // MB
188
+ percentage: Math.round((memory.usedJSHeapSize / memory.totalJSHeapSize) * 100)
189
+ };
190
+ }
191
+ return null;
192
+ }
193
+
194
+ /**
195
+ * Debounce utility for performance
196
+ */
197
+ export function debounce<T extends (...args: any[]) => any>(
198
+ func: T,
199
+ wait: number
200
+ ): (...args: Parameters<T>) => void {
201
+ let timeout: NodeJS.Timeout;
202
+ return (...args: Parameters<T>) => {
203
+ clearTimeout(timeout);
204
+ timeout = setTimeout(() => func(...args), wait);
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Throttle utility for performance
210
+ */
211
+ export function throttle<T extends (...args: any[]) => any>(
212
+ func: T,
213
+ limit: number
214
+ ): (...args: Parameters<T>) => void {
215
+ let inThrottle: boolean;
216
+ return (...args: Parameters<T>) => {
217
+ if (!inThrottle) {
218
+ func(...args);
219
+ inThrottle = true;
220
+ setTimeout(() => inThrottle = false, limit);
221
+ }
222
+ };
223
+ }
vite.config.ts CHANGED
@@ -15,6 +15,9 @@ export default defineConfig(({ mode }) => {
15
  alias: {
16
  '@': path.resolve(__dirname, '.'),
17
  }
 
 
 
18
  }
19
  };
20
  });
 
15
  alias: {
16
  '@': path.resolve(__dirname, '.'),
17
  }
18
+ },
19
+ build: {
20
+ chunkSizeWarningLimit: 1000
21
  }
22
  };
23
  });