@@ -7,19 +7,52 @@ import { ChevronDown, RefreshCw, Settings, Zap } from "lucide-react";
7
7
import { useTranslation } from "react-i18next" ;
8
8
import { AIService } from "../../services/ai-service" ;
9
9
import { OPENAI_PROVIDER_NAME } from "../../services/providers/openai-service" ;
10
+ import { ImageGenerationManager , ImageGenerationStatus , ImageGenerationHandler } from "../../services/image-generation-handler" ;
11
+ import { DatabaseIntegrationService } from "../../services/database-integration" ;
12
+ import { ImageGenerationResult } from "../../types/image" ;
13
+ import ImageGenerateHistoryItem from "../image/ImageGenerateHistoryItem" ;
10
14
11
15
export const ImageGenerationPage = ( ) => {
12
16
const { t } = useTranslation ( ) ;
13
17
const [ prompt , setPrompt ] = useState ( "" ) ;
14
- const [ isGenerating , setIsGenerating ] = useState ( false ) ;
15
- const [ imageResult , setImageResult ] = useState < string | null > ( null ) ;
16
18
const [ error , setError ] = useState < Error | null > ( null ) ;
17
19
const [ isApiKeyMissing , setIsApiKeyMissing ] = useState ( true ) ;
18
20
const [ aspectRatio , setAspectRatio ] = useState ( "1:1" ) ;
19
21
const [ imageCount , setImageCount ] = useState ( 1 ) ;
20
22
const [ randomSeed , setRandomSeed ] = useState (
21
23
Math . floor ( Math . random ( ) * 1000000 ) . toString ( )
22
24
) ;
25
+ const [ generationResults , setGenerationResults ] = useState < Map < string , ImageGenerationHandler > > (
26
+ new Map ( )
27
+ ) ;
28
+ const [ historyResults , setHistoryResults ] = useState < ImageGenerationResult [ ] > ( [ ] ) ;
29
+ const [ isLoadingHistory , setIsLoadingHistory ] = useState ( true ) ;
30
+
31
+ // Initialize image generation manager
32
+ useEffect ( ( ) => {
33
+ const imageManager = ImageGenerationManager . getInstance ( ) ;
34
+
35
+ // Register for updates on generation status changes
36
+ imageManager . setUpdateCallback ( ( handlers ) => {
37
+ setGenerationResults ( new Map ( handlers ) ) ;
38
+ } ) ;
39
+
40
+ // Load history from database
41
+ const loadHistoryResults = async ( ) => {
42
+ try {
43
+ setIsLoadingHistory ( true ) ;
44
+ const dbService = DatabaseIntegrationService . getInstance ( ) ;
45
+ await dbService . initialize ( ) ;
46
+ // TODO: Implement loading image history from database when available
47
+ setIsLoadingHistory ( false ) ;
48
+ } catch ( error ) {
49
+ console . error ( 'Error loading image history:' , error ) ;
50
+ setIsLoadingHistory ( false ) ;
51
+ }
52
+ } ;
53
+
54
+ loadHistoryResults ( ) ;
55
+ } , [ ] ) ;
23
56
24
57
// Check if API key is available
25
58
useEffect ( ( ) => {
@@ -40,7 +73,6 @@ export const ImageGenerationPage = () => {
40
73
const handleGenerateImage = async ( ) => {
41
74
if ( ! prompt . trim ( ) ) return ;
42
75
43
- setIsGenerating ( true ) ;
44
76
setError ( null ) ;
45
77
46
78
try {
@@ -51,6 +83,20 @@ export const ImageGenerationPage = () => {
51
83
throw new Error ( "OpenAI service not available" ) ;
52
84
}
53
85
86
+ // Create a new generation handler
87
+ const imageManager = ImageGenerationManager . getInstance ( ) ;
88
+ const handler = imageManager . createHandler ( {
89
+ prompt : prompt ,
90
+ seed : randomSeed ,
91
+ number : imageCount ,
92
+ aspectRatio : aspectRatio ,
93
+ provider : OPENAI_PROVIDER_NAME ,
94
+ model : "dall-e-3" ,
95
+ } ) ;
96
+
97
+ // Set status to generating
98
+ handler . setGenerating ( ) ;
99
+
54
100
// Map aspect ratio to size dimensions
55
101
const sizeMap : Record < string , `${number } x${number } `> = {
56
102
"1:1" : "1024x1024" ,
@@ -68,23 +114,28 @@ export const ImageGenerationPage = () => {
68
114
style : "vivid"
69
115
} ) ;
70
116
71
- // Set the result image
117
+ // Process the result
72
118
if ( images && images . length > 0 ) {
73
- // Check if the image is already a full data URL
74
- const base64Data = images [ 0 ] as string ;
75
- if ( base64Data . startsWith ( 'data:image' ) ) {
76
- setImageResult ( base64Data ) ;
77
- } else {
78
- // If it's just a base64 string without the data URI prefix, add it
79
- setImageResult ( `data:image/png;base64,${ base64Data } ` ) ;
80
- }
119
+ // Convert image data to full URLs if needed
120
+ const processedImages = images . map ( img => {
121
+ const base64Data = img as string ;
122
+ if ( base64Data . startsWith ( 'data:image' ) ) {
123
+ return base64Data ;
124
+ } else {
125
+ return `data:image/png;base64,${ base64Data } ` ;
126
+ }
127
+ } ) ;
128
+
129
+ // Update the handler with successful results
130
+ handler . setSuccess ( processedImages ) ;
131
+
132
+ // Generate new seed for next generation
133
+ generateNewSeed ( ) ;
81
134
} else {
82
135
throw new Error ( "No images generated" ) ;
83
136
}
84
137
} catch ( err ) {
85
138
setError ( err as Error ) ;
86
- } finally {
87
- setIsGenerating ( false ) ;
88
139
}
89
140
} ;
90
141
@@ -93,6 +144,25 @@ export const ImageGenerationPage = () => {
93
144
setRandomSeed ( Math . floor ( Math . random ( ) * 1000000 ) . toString ( ) ) ;
94
145
} ;
95
146
147
+ // Get all results to display in order (active generations first, then history)
148
+ const getAllResults = ( ) => {
149
+ const handlerResults = Array . from ( generationResults . values ( ) )
150
+ . map ( handler => handler . getResult ( ) )
151
+ . sort ( ( a , b ) => {
152
+ // Prioritize active generations first
153
+ if ( a . status === ImageGenerationStatus . GENERATING && b . status !== ImageGenerationStatus . GENERATING ) {
154
+ return - 1 ;
155
+ }
156
+ if ( b . status === ImageGenerationStatus . GENERATING && a . status !== ImageGenerationStatus . GENERATING ) {
157
+ return 1 ;
158
+ }
159
+ // Then sort by most recent first (assuming imageResultId is a UUID with timestamp components)
160
+ return b . imageResultId . localeCompare ( a . imageResultId ) ;
161
+ } ) ;
162
+
163
+ return [ ...handlerResults , ...historyResults ] ;
164
+ } ;
165
+
96
166
return (
97
167
< div className = "flex flex-col w-full h-full bg-white" >
98
168
{ isApiKeyMissing && (
@@ -121,19 +191,19 @@ export const ImageGenerationPage = () => {
121
191
< div className = "flex flex-row gap-2 p-2 border border-gray-300 rounded-lg shadow-sm" >
122
192
< button
123
193
onClick = { handleGenerateImage }
124
- disabled = { isGenerating || ! prompt . trim ( ) || isApiKeyMissing }
194
+ disabled = { ! prompt . trim ( ) || isApiKeyMissing }
125
195
className = "px-4 py-2.5 text-nowrap flex flex-row gap-1 text-white text-center confirm-btn"
126
196
>
127
197
< Settings > </ Settings >
128
198
{ t ( "imageGeneration.settingsButton" ) }
129
199
</ button >
130
200
< button
131
201
onClick = { handleGenerateImage }
132
- disabled = { isGenerating || ! prompt . trim ( ) || isApiKeyMissing }
202
+ disabled = { ! prompt . trim ( ) || isApiKeyMissing }
133
203
className = "px-4 py-2.5 text-nowrap flex flex-row gap-1 text-white text-center confirm-btn"
134
204
>
135
205
< Zap > </ Zap >
136
- { isGenerating
206
+ { Array . from ( generationResults . values ( ) ) . some ( h => h . getStatus ( ) === ImageGenerationStatus . GENERATING )
137
207
? t ( "imageGeneration.generating" )
138
208
: t ( "imageGeneration.generateButton" ) }
139
209
</ button >
@@ -154,41 +224,34 @@ export const ImageGenerationPage = () => {
154
224
</ div >
155
225
) }
156
226
157
- { /* Loading indicator */ }
158
- { isGenerating && (
159
- < div className = "flex items-center justify-center h-64 rounded-lg bg-gray-50" >
160
- < div className = "flex flex-col items-center" >
161
- < div className = "w-10 h-10 border-4 rounded-full border-primary-300 border-t-primary-600 animate-spin" > </ div >
162
- < p className = "mt-4 text-gray-600" >
163
- { t ( "imageGeneration.generating" ) }
164
- </ p >
165
- </ div >
166
- </ div >
167
- ) }
168
-
169
227
{ /* Results display */ }
170
- { imageResult && ! isGenerating && (
171
- < div className = "grid grid-cols-1 gap-4" >
172
- < div className = "overflow-hidden border rounded-lg image-result-area" >
173
- < img
174
- src = { imageResult }
175
- alt = "Generated from AI"
176
- className = "object-contain w-full"
177
- />
228
+ < div className = "grid grid-cols-1 gap-4" >
229
+ { /* Active generations and history */ }
230
+ { getAllResults ( ) . map ( result => (
231
+ < ImageGenerateHistoryItem
232
+ key = { result . imageResultId }
233
+ imageResult = { result }
234
+ />
235
+ ) ) }
236
+
237
+ { /* Loading indicator for history */ }
238
+ { isLoadingHistory && (
239
+ < div className = "flex items-center justify-center h-24 rounded-lg bg-gray-50" >
240
+ < div className = "w-8 h-8 border-4 rounded-full border-primary-300 border-t-primary-600 animate-spin" > </ div >
178
241
</ div >
179
- </ div >
180
- ) }
181
-
182
- { /* Placeholder when no results */ }
183
- { ! imageResult && ! isGenerating && (
184
- < div className = "flex items -center justify-center h-64 rounded-lg image-generation-result-placeholder " >
185
- < div className = "text-center " >
186
- < p className = "text-gray-500" >
187
- { t ( "imageGeneration.placeholderText" ) }
188
- </ p >
242
+ ) }
243
+
244
+ { /* Placeholder when no results */ }
245
+ { getAllResults ( ) . length === 0 && ! isLoadingHistory && (
246
+ < div className = "flex items-center justify-center h-64 rounded-lg image-generation-result-placeholder" >
247
+ < div className = "text -center" >
248
+ < p className = "text-gray-500 " >
249
+ { t ( "imageGeneration.placeholderText" ) }
250
+ </ p >
251
+ </ div >
189
252
</ div >
190
- </ div >
191
- ) }
253
+ ) }
254
+ </ div >
192
255
</ div >
193
256
194
257
{ /* Right side - Results */ }
0 commit comments