1
- import { useState , useEffect , useCallback } from "react" ;
1
+ import { useState , useEffect , useCallback , useRef } from "react" ;
2
2
import {
3
3
SettingsService ,
4
4
SETTINGS_CHANGE_EVENT ,
@@ -18,6 +18,7 @@ export const ImageGenerationPage = () => {
18
18
const [ error , setError ] = useState < Error | null > ( null ) ;
19
19
const [ isApiKeyMissing , setIsApiKeyMissing ] = useState ( true ) ;
20
20
const [ aspectRatio , setAspectRatio ] = useState ( "1:1" ) ;
21
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
21
22
const [ imageCount , setImageCount ] = useState ( 1 ) ;
22
23
const [ randomSeed , setRandomSeed ] = useState (
23
24
Math . floor ( Math . random ( ) * 1000000 ) . toString ( )
@@ -27,6 +28,10 @@ export const ImageGenerationPage = () => {
27
28
) ;
28
29
const [ historyResults , setHistoryResults ] = useState < ImageGenerationResult [ ] > ( [ ] ) ;
29
30
const [ isLoadingHistory , setIsLoadingHistory ] = useState ( true ) ;
31
+ const [ isSettingsOpen , setIsSettingsOpen ] = useState ( false ) ;
32
+
33
+ const settingsButtonRef = useRef < HTMLButtonElement > ( null ) ;
34
+ const settingsPopupRef = useRef < HTMLDivElement > ( null ) ;
30
35
31
36
// Load image generation history from database
32
37
const refreshImageHistory = useCallback ( async ( ) => {
@@ -90,6 +95,25 @@ export const ImageGenerationPage = () => {
90
95
} ;
91
96
} , [ ] ) ;
92
97
98
+ // Handle clicks outside the settings popup
99
+ useEffect ( ( ) => {
100
+ const handleClickOutside = ( event : MouseEvent ) => {
101
+ if (
102
+ settingsPopupRef . current &&
103
+ ! settingsPopupRef . current . contains ( event . target as Node ) &&
104
+ settingsButtonRef . current &&
105
+ ! settingsButtonRef . current . contains ( event . target as Node )
106
+ ) {
107
+ setIsSettingsOpen ( false ) ;
108
+ }
109
+ } ;
110
+
111
+ document . addEventListener ( 'mousedown' , handleClickOutside ) ;
112
+ return ( ) => {
113
+ document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
114
+ } ;
115
+ } , [ ] ) ;
116
+
93
117
// Handle generating an image using OpenAI's DALL-E 3
94
118
const handleGenerateImage = async ( ) => {
95
119
if ( ! prompt . trim ( ) ) return ;
@@ -196,6 +220,11 @@ export const ImageGenerationPage = () => {
196
220
) ;
197
221
} ;
198
222
223
+ // Toggle settings popup
224
+ const toggleSettings = ( ) => {
225
+ setIsSettingsOpen ( ! isSettingsOpen ) ;
226
+ } ;
227
+
199
228
return (
200
229
< div className = "flex flex-col w-full h-full bg-white" >
201
230
{ isApiKeyMissing && (
@@ -223,8 +252,8 @@ export const ImageGenerationPage = () => {
223
252
224
253
< div className = "flex flex-row gap-2 p-2 border border-gray-300 rounded-lg shadow-sm" >
225
254
< button
226
- onClick = { handleGenerateImage }
227
- disabled = { ! prompt . trim ( ) || isApiKeyMissing || isAnyImageGenerating ( ) }
255
+ ref = { settingsButtonRef }
256
+ onClick = { toggleSettings }
228
257
className = "px-4 py-2.5 text-nowrap flex flex-row gap-1 text-white text-center confirm-btn"
229
258
>
230
259
< Settings > </ Settings >
@@ -292,167 +321,151 @@ export const ImageGenerationPage = () => {
292
321
</ div >
293
322
</ div >
294
323
295
- { /* Right side - Results */ }
296
- < div className = "hidden w-[420px] h-full p-6 overflow-y-auto" >
297
- { /* Provider selection */ }
298
- < div className = "mb-6" >
299
- < label className = "block mb-2 text-sm font-medium text-gray-700" >
300
- { t ( "imageGeneration.provider" ) }
301
- </ label >
302
- < div className = "relative" >
303
- < button
304
- className = "flex items-center justify-between w-full p-3 text-left input-box"
305
- disabled = { true }
306
- >
307
- < span > OpenAI</ span >
308
- < ChevronDown size = { 18 } className = "text-gray-500" />
309
- </ button >
310
- </ div >
311
- </ div >
312
-
313
- { /* Model selection */ }
314
- < div className = "mb-6" >
315
- < label className = "block mb-2 text-sm font-medium text-gray-700" >
316
- { t ( "imageGeneration.model" ) }
317
- </ label >
318
- < div className = "relative" >
319
- < button
320
- className = "flex items-center justify-between w-full p-3 text-left input-box"
321
- disabled = { true }
322
- >
323
- < span > DALL-E 3</ span >
324
- < ChevronDown size = { 18 } className = "text-gray-500" />
325
- </ button >
324
+ { /* Settings popup */ }
325
+ { isSettingsOpen && (
326
+ < div
327
+ ref = { settingsPopupRef }
328
+ className = "absolute z-10 p-4 bg-white border border-gray-300 rounded-lg shadow-lg image-generation-settings-popup"
329
+ style = { {
330
+ top : settingsButtonRef . current ?
331
+ settingsButtonRef . current . getBoundingClientRect ( ) . top + 5 : 100 ,
332
+ left : settingsButtonRef . current ?
333
+ settingsButtonRef . current . getBoundingClientRect ( ) . left - 130 : 100 ,
334
+ width : '320px'
335
+ } }
336
+ >
337
+ < div className = "mb-4" >
338
+ < h3 className = "mb-2 text-base font-medium text-gray-800" >
339
+ { t ( "imageGeneration.imageSize" ) }
340
+ </ h3 >
341
+ < div className = "grid grid-cols-3 gap-2" >
342
+ < button
343
+ onClick = { ( ) => setAspectRatio ( "1:1" ) }
344
+ className = { `p-2 text-center border rounded-lg aspect-ratio-button ${
345
+ aspectRatio === "1:1" ? "active" : ""
346
+ } `}
347
+ >
348
+ < div className = "flex justify-center mb-1" >
349
+ < div className = "w-6 h-6 border border-gray-500 rounded-sm" > </ div >
350
+ </ div >
351
+ < span className = "text-xs" > 1:1</ span >
352
+ </ button >
353
+ < button
354
+ onClick = { ( ) => setAspectRatio ( "3:2" ) }
355
+ className = { `p-2 text-center border rounded-lg aspect-ratio-button ${
356
+ aspectRatio === "3:2" ? "active" : ""
357
+ } `}
358
+ >
359
+ < div className = "flex justify-center mb-1" >
360
+ < div className = "w-6 h-4 border border-gray-500 rounded-sm" > </ div >
361
+ </ div >
362
+ < span className = "text-xs" > 3:2</ span >
363
+ </ button >
364
+ < button
365
+ onClick = { ( ) => setAspectRatio ( "16:9" ) }
366
+ className = { `p-2 text-center border rounded-lg aspect-ratio-button ${
367
+ aspectRatio === "16:9" ? "active" : ""
368
+ } `}
369
+ >
370
+ < div className = "flex justify-center mb-1" >
371
+ < div className = "w-6 h-3.5 border border-gray-500 rounded-sm" > </ div >
372
+ </ div >
373
+ < span className = "text-xs" > 16:9</ span >
374
+ </ button >
375
+ < button
376
+ onClick = { ( ) => setAspectRatio ( "1:2" ) }
377
+ className = { `p-2 text-center border rounded-lg aspect-ratio-button ${
378
+ aspectRatio === "1:2" ? "active" : ""
379
+ } `}
380
+ >
381
+ < div className = "flex justify-center mb-1" >
382
+ < div className = "w-4 h-6 border border-gray-500 rounded-sm" > </ div >
383
+ </ div >
384
+ < span className = "text-xs" > 1:2</ span >
385
+ </ button >
386
+ < button
387
+ onClick = { ( ) => setAspectRatio ( "3:4" ) }
388
+ className = { `p-2 text-center border rounded-lg aspect-ratio-button ${
389
+ aspectRatio === "3:4" ? "active" : ""
390
+ } `}
391
+ >
392
+ < div className = "flex justify-center mb-1" >
393
+ < div className = "w-[1rem] h-[1.5rem] border border-gray-500 rounded-sm" > </ div >
394
+ </ div >
395
+ < span className = "text-xs" > 3:4</ span >
396
+ </ button >
397
+ < button
398
+ onClick = { ( ) => setAspectRatio ( "9:16" ) }
399
+ className = { `p-2 text-center border rounded-lg aspect-ratio-button ${
400
+ aspectRatio === "9:16" ? "active" : ""
401
+ } `}
402
+ >
403
+ < div className = "flex justify-center mb-1" >
404
+ < div className = "w-3.5 h-6 border border-gray-500 rounded-sm" > </ div >
405
+ </ div >
406
+ < span className = "text-xs" > 9:16</ span >
407
+ </ button >
408
+ </ div >
326
409
</ div >
327
- </ div >
328
410
329
- { /* Aspect ratio selection */ }
330
- < div className = "mb-6" >
331
- < label className = "block mb-2 text-sm font-medium text-gray-700" >
332
- { t ( "imageGeneration.imageSize" ) }
333
- </ label >
334
- < div className = "grid grid-cols-6 gap-1" >
335
- < button
336
- onClick = { ( ) => setAspectRatio ( "1:1" ) }
337
- className = { `p-2 text-center border rounded-lg aspect-ratio-button ${
338
- aspectRatio === "1:1" ? "active" : ""
339
- } `}
340
- >
341
- < div className = "flex justify-center mb-1" >
342
- < div className = "w-6 h-6 border border-gray-500 rounded-sm" > </ div >
343
- </ div >
344
- < span className = "text-xs" > 1:1</ span >
345
- </ button >
346
- < button
347
- onClick = { ( ) => setAspectRatio ( "1:2" ) }
348
- className = { `p-2 text-center border rounded-lg aspect-ratio-button ${
349
- aspectRatio === "1:2" ? "active" : ""
350
- } `}
351
- >
352
- < div className = "flex justify-center mb-1" >
353
- < div className = "w-4 h-6 border border-gray-500 rounded-sm" > </ div >
354
- </ div >
355
- < span className = "text-xs" > 1:2</ span >
356
- </ button >
357
- < button
358
- onClick = { ( ) => setAspectRatio ( "3:2" ) }
359
- className = { `p-2 text-center border rounded-lg aspect-ratio-button ${
360
- aspectRatio === "3:2" ? "active" : ""
361
- } `}
362
- >
363
- < div className = "flex justify-center mb-1" >
364
- < div className = "w-6 h-4 border border-gray-500 rounded-sm" > </ div >
365
- </ div >
366
- < span className = "text-xs" > 3:2</ span >
367
- </ button >
368
- < button
369
- onClick = { ( ) => setAspectRatio ( "3:4" ) }
370
- className = { `p-2 text-center border rounded-lg aspect-ratio-button ${
371
- aspectRatio === "3:4" ? "active" : ""
372
- } `}
373
- >
374
- < div className = "flex justify-center mb-1" >
375
- < div className = "w-[1rem] h-[1.5rem] border border-gray-500 rounded-sm" > </ div >
376
- </ div >
377
- < span className = "text-xs" > 3:4</ span >
378
- </ button >
379
- < button
380
- onClick = { ( ) => setAspectRatio ( "16:9" ) }
381
- className = { `p-2 text-center border rounded-lg aspect-ratio-button ${
382
- aspectRatio === "16:9" ? "active" : ""
383
- } `}
384
- >
385
- < div className = "flex justify-center mb-1" >
386
- < div className = "w-6 h-3.5 border border-gray-500 rounded-sm" > </ div >
387
- </ div >
388
- < span className = "text-xs" > 16:9</ span >
389
- </ button >
390
- < button
391
- onClick = { ( ) => setAspectRatio ( "9:16" ) }
392
- className = { `p-2 text-center border rounded-lg aspect-ratio-button ${
393
- aspectRatio === "9:16" ? "active" : ""
394
- } `}
395
- >
396
- < div className = "flex justify-center mb-1" >
397
- < div className = "w-3.5 h-6 border border-gray-500 rounded-sm" > </ div >
411
+ < div className = "mb-4" >
412
+ < label className = "flex items-center block mb-2 text-sm font-medium text-gray-700" >
413
+ { t ( "imageGeneration.randomSeed" ) }
414
+ < div
415
+ className = "flex items-center justify-center w-4 h-4 ml-1 text-xs text-gray-500 bg-gray-200 rounded-full cursor-help"
416
+ title = { t ( "imageGeneration.seedHelp" ) }
417
+ >
418
+ ?
398
419
</ div >
399
- < span className = "text-xs" > 9:16</ span >
400
- </ button >
420
+ </ label >
421
+ < div className = "flex" >
422
+ < input
423
+ type = "text"
424
+ value = { randomSeed }
425
+ onChange = { ( e ) => setRandomSeed ( e . target . value ) }
426
+ className = "flex-grow p-3 mr-2 input-box"
427
+ />
428
+ < button
429
+ onClick = { generateNewSeed }
430
+ className = "p-2 rounded-lg image-generation-refresh-button"
431
+ title = { t ( "imageGeneration.randomSeed" ) }
432
+ >
433
+ < RefreshCw size = { 20 } />
434
+ </ button >
435
+ </ div >
401
436
</ div >
402
- </ div >
403
437
404
- { /* Image count */ }
405
- < div className = "mb-6" >
406
- < label className = "flex items-center block mb-2 text-sm font-medium text-gray-700" >
407
- { t ( "imageGeneration.generationCount" ) }
408
- < div
409
- className = "flex items-center justify-center w-4 h-4 ml-1 text-xs text-gray-500 bg-gray-200 rounded-full cursor-help"
410
- title = { t ( "imageGeneration.generationCount" ) }
411
- >
412
- ?
438
+ < div className = "mb-4" >
439
+ < label className = "flex items-center block mb-2 text-sm font-medium text-gray-700" >
440
+ { t ( "imageGeneration.provider" ) }
441
+ </ label >
442
+ < div className = "relative" >
443
+ < button
444
+ className = "flex items-center justify-between w-full p-3 text-left input-box"
445
+ disabled = { true }
446
+ >
447
+ < span > OpenAI</ span >
448
+ < ChevronDown size = { 18 } className = "text-gray-500" />
449
+ </ button >
413
450
</ div >
414
- </ label >
415
- < input
416
- type = "number"
417
- value = { imageCount }
418
- onChange = { ( e ) => setImageCount ( parseInt ( e . target . value ) || 1 ) }
419
- min = "1"
420
- max = "4"
421
- className = "w-full p-3 input-box"
422
- disabled = { true }
423
- />
424
- </ div >
451
+ </ div >
425
452
426
- { /* Random seed */ }
427
- < div className = "mb-6" >
428
- < label className = "flex items-center block mb-2 text-sm font-medium text-gray-700" >
429
- { t ( "imageGeneration.randomSeed" ) }
430
- < div
431
- className = "flex items-center justify-center w-4 h-4 ml-1 text-xs text-gray-500 bg-gray-200 rounded-full cursor-help"
432
- title = { t ( "imageGeneration.seedHelp" ) }
433
- >
434
- ?
453
+ < div className = "mb-4" >
454
+ < label className = "flex items-center block mb-2 text-sm font-medium text-gray-700" >
455
+ { t ( "imageGeneration.model" ) }
456
+ </ label >
457
+ < div className = "relative" >
458
+ < button
459
+ className = "flex items-center justify-between w-full p-3 text-left input-box"
460
+ disabled = { true }
461
+ >
462
+ < span > DALL-E 3</ span >
463
+ < ChevronDown size = { 18 } className = "text-gray-500" />
464
+ </ button >
435
465
</ div >
436
- </ label >
437
- < div className = "flex" >
438
- < input
439
- type = "text"
440
- value = { randomSeed }
441
- onChange = { ( e ) => setRandomSeed ( e . target . value ) }
442
- className = "flex-grow p-3 mr-2 input-box"
443
- disabled = { true }
444
- />
445
- < button
446
- onClick = { generateNewSeed }
447
- className = "p-2 rounded-lg image-generation-refresh-button"
448
- title = { t ( "imageGeneration.randomSeed" ) }
449
- disabled = { true }
450
- >
451
- < RefreshCw size = { 20 } />
452
- </ button >
453
466
</ div >
454
467
</ div >
455
- </ div >
468
+ ) }
456
469
</ div >
457
470
</ div >
458
471
) ;
0 commit comments