File size: 65,625 Bytes
cd3f86a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
import type { MediaFile } from './types';
import { GenerationStatus } from './types';
import FileUploader from './components/FileUploader';
import MediaItem from './components/MediaItem';
import { generateCaption, refineCaption, checkCaptionQuality } from './services/geminiService';
import { generateCaptionQwen, refineCaptionQwen, checkQualityQwen } from './services/qwenService';
import { sendComfyPrompt } from './services/comfyService';
import { DownloadIcon, SparklesIcon, WandIcon, LoaderIcon, CopyIcon, UploadCloudIcon, XIcon, CheckCircleIcon, AlertTriangleIcon, StopIcon, TrashIcon } from './components/Icons';
import { DEFAULT_COMFY_WORKFLOW } from './constants/defaultWorkflow';

declare const process: {
  env: { API_KEY?: string; [key: string]: string | undefined; }
};

declare global {
  interface AIStudio {
    hasSelectedApiKey: () => Promise<boolean>;
    openSelectKey: () => Promise<void>;
  }
  interface Window { JSZip: any; aistudio?: AIStudio; }
}

type ApiProvider = 'gemini' | 'qwen';
type OSType = 'windows' | 'linux';

const GEMINI_MODELS = [
    { id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro (High Quality)' },
    { id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash (Fast)' },
    { id: 'gemini-2.5-pro-preview-09-2025', name: 'Gemini 2.5 Pro (Multimodal)' },
    { id: 'gemini-2.5-flash-native-audio-preview-09-2025', name: 'Gemini 2.5 Flash (Multimedia Speed)' }
];

const QWEN_MODELS = [
    { id: 'thesby/Qwen3-VL-8B-NSFW-Caption-V4.5', name: 'Thesby Qwen 3 VL 8B NSFW Caption V4.5' },
    { id: 'huihui-ai/Huihui-Qwen3-VL-8B-Instruct-abliterated', name: 'Huihui Qwen 3 VL 8B Abliterated (Uncensored)' },
    { id: 'Qwen/Qwen3-VL-8B-Instruct-FP8', name: 'Qwen 3 VL 8B FP8' },
];

const DEFAULT_BULK_INSTRUCTIONS = `Dont use ambiguous language "perhaps" for example. Describe EVERYTHING visible: characters, clothing, actions, background, objects, lighting, and camera angle. Refrain from using generic phrases like "character, male, figure of" and use specific terminology: "woman, girl, boy, man". Do not mention the art style.`;
const DEFAULT_REFINEMENT_INSTRUCTIONS = `Refine the caption to be more descriptive and cinematic. Ensure all colors and materials are mentioned.`;

const App: React.FC = () => {
  // --- STATE ---
  const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]);
  const [triggerWord, setTriggerWord] = useState<string>('MyStyle');
  const [apiProvider, setApiProvider] = useState<ApiProvider>('gemini');
  const [geminiApiKey, setGeminiApiKey] = useState<string>(process.env.API_KEY || '');
  const [geminiModel, setGeminiModel] = useState<string>(GEMINI_MODELS[0].id);
  const [hasSelectedKey, setHasSelectedKey] = useState<boolean>(false);

  // Qwen Options
  const [qwenEndpoint, setQwenEndpoint] = useState<string>('');
  const [useCustomQwenModel, setUseCustomQwenModel] = useState<boolean>(false);
  const [customQwenModelId, setCustomQwenModelId] = useState<string>('');
  const [qwenModel, setQwenModel] = useState<string>(QWEN_MODELS[0].id);
  const [qwenOsType, setQwenOsType] = useState<OSType>(() => navigator.userAgent.includes("Windows") ? 'windows' : 'linux');
  const [qwenInstallDir, setQwenInstallDir] = useState<string>(() => navigator.userAgent.includes("Windows") ? 'C:\\AI\\qwen_local' : '/home/user/ai/qwen_local');
  const [qwenMaxTokens, setQwenMaxTokens] = useState<number>(8192);
  const [qwen8Bit, setQwen8Bit] = useState<boolean>(false);
  const [qwenEager, setQwenEager] = useState<boolean>(false);
  const [qwenVideoFrameCount, setQwenVideoFrameCount] = useState<number>(8);
  
  // Offline Local Snapshot Options
  const [useOfflineSnapshot, setUseOfflineSnapshot] = useState<boolean>(false);
  const [snapshotPath, setSnapshotPath] = useState<string>('');
  const [virtualModelName, setVirtualModelName] = useState<string>('thesby/Qwen3-VL-8B-NSFW-Caption-V4.5');

  // ComfyUI Options
  const [isComfyEnabled, setIsComfyEnabled] = useState<boolean>(false);
  const [comfyUrl, setComfyUrl] = useState<string>('http://localhost:5000');
  const [comfyWorkflow, setComfyWorkflow] = useState<any>(DEFAULT_COMFY_WORKFLOW);
  const [comfyWorkflowName, setComfyWorkflowName] = useState<string>('Default Workflow');
  const [comfySeed, setComfySeed] = useState<number>(-1);
  const [comfySteps, setComfySteps] = useState<number>(4);
  const [activePreviewId, setActivePreviewId] = useState<string | null>(null);

  // Secure Bridge Options
  const [useSecureBridge, setUseSecureBridge] = useState<boolean>(false);
  const [isFirstTimeBridge, setIsFirstTimeBridge] = useState<boolean>(false);
  const [bridgeOsType, setBridgeOsType] = useState<OSType>(() => navigator.userAgent.includes("Windows") ? 'windows' : 'linux');
  const [bridgeInstallPath, setBridgeInstallPath] = useState<string>('/mnt/Goon/captioner');

  // Queue and Performance
  const [useRequestQueue, setUseRequestQueue] = useState<boolean>(true);
  const [concurrentTasks, setConcurrentTasks] = useState<number>(1);
  const [isQueueRunning, setIsQueueRunning] = useState<boolean>(false);

  // Dataset / Instructions
  const [bulkGenerationInstructions, setBulkGenerationInstructions] = useState<string>(DEFAULT_BULK_INSTRUCTIONS);
  const [bulkRefinementInstructions, setBulkRefinementInstructions] = useState<string>(DEFAULT_REFINEMENT_INSTRUCTIONS);
  const [autofitTextareas, setAutofitTextareas] = useState<boolean>(false);
  const [showSideBySidePreview, setShowSideBySidePreview] = useState<boolean>(false);
  const [datasetPrefix, setDatasetPrefix] = useState<string>('item');
  const [isCharacterTaggingEnabled, setIsCharacterTaggingEnabled] = useState<boolean>(false);
  const [characterShowName, setCharacterShowName] = useState<string>('');
  const [isExporting, setIsExporting] = useState<boolean>(false);

  const abortControllerRef = useRef<AbortController>(new AbortController());

  // --- EFFECTS ---
  useEffect(() => {
    if (window.aistudio) {
        window.aistudio.hasSelectedApiKey().then(setHasSelectedKey);
    }
    const isHttps = window.location.protocol === 'https:';
    if (!qwenEndpoint) {
        setQwenEndpoint(isHttps ? '' : 'http://localhost:8000/v1');
    }
  }, [qwenEndpoint]);

  // Handle Modal Keyboard Navigation
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (!activePreviewId) return;
      if (e.key === 'ArrowRight') handleNextPreview();
      if (e.key === 'ArrowLeft') handlePrevPreview();
      if (e.key === 'Escape') setActivePreviewId(null);
    };
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [activePreviewId, mediaFiles]);

  // --- MEMOIZED VALUES ---
  const hasValidConfig = useMemo(() => {
    if (apiProvider === 'gemini') return !!geminiApiKey;
    return qwenEndpoint !== '';
  }, [apiProvider, geminiApiKey, qwenEndpoint]);

  const selectedFiles = useMemo(() => {
    return (mediaFiles || []).filter(mf => mf.isSelected);
  }, [mediaFiles]);
  const currentPreviewItem = useMemo(() => (mediaFiles || []).find(m => m.id === activePreviewId), [mediaFiles, activePreviewId]);

  const qwenEffectiveModel = useMemo(() => {
    if (useOfflineSnapshot) return virtualModelName;
    return useCustomQwenModel ? customQwenModelId : qwenModel;
  }, [useOfflineSnapshot, virtualModelName, useCustomQwenModel, customQwenModelId, qwenModel]);

  const qwenStartCommand = useMemo(() => {
    const isWin = qwenOsType === 'windows';
    const path = qwenInstallDir.replace(/[\\/]+$/, '');
    
    // Model logic for command
    const modelToLoad = useOfflineSnapshot ? snapshotPath : (useCustomQwenModel ? customQwenModelId : qwenModel);
    
    const activate = isWin ? `venv\\Scripts\\activate` : `source venv/bin/activate`;
    const python = isWin ? `python` : `python3`;
    const offlineEnv = isWin ? `set HF_HUB_OFFLINE=1` : `export HF_HUB_OFFLINE=1`;
    
    let args = `--model "${modelToLoad}" --max-model-len ${qwenMaxTokens}`;
    if (useOfflineSnapshot) {
      args += ` --served-model-name "${virtualModelName}"`;
    }
    if (qwen8Bit) args += ` --load-format bitsandbytes --quantization bitsandbytes`;
    if (qwenEager) args += ` --enforce-eager`;
    
    const baseCmd = isWin 
      ? `cd /d "${path}" && ${useOfflineSnapshot ? `${offlineEnv} && ` : ''}${activate} && ${python} -m vllm.entrypoints.openai.api_server ${args}`
      : `cd "${path}" && ${useOfflineSnapshot ? `${offlineEnv} && ` : ''}${activate} && ${python} -m vllm.entrypoints.openai.api_server ${args}`;
    
    return baseCmd;
  }, [qwenOsType, qwenInstallDir, useCustomQwenModel, customQwenModelId, qwenModel, qwenMaxTokens, qwen8Bit, qwenEager, useOfflineSnapshot, snapshotPath, virtualModelName]);

  const bridgeStartCommand = useMemo(() => {
    const isWindows = bridgeOsType === 'windows';
    const path = bridgeInstallPath.replace(/[\\/]+$/, '');
    const activateCmd = isWindows ? `call venv\\Scripts\\activate` : `source venv/bin/activate`;
    const pipCmd = `pip install flask flask-cors requests`;
    const setupCmd = isWindows 
      ? `python -m venv venv && ${activateCmd} && ${pipCmd}`
      : `python3 -m venv venv && ${activateCmd} && ${pipCmd}`;
    return isWindows 
        ? `cd /d "${path}" && ${isFirstTimeBridge ? `${setupCmd} && ` : ''}${activateCmd} && python bridge.py`
        : `cd "${path}" && ${isFirstTimeBridge ? `${setupCmd} && ` : ''}${activateCmd} && python3 bridge.py`;
  }, [bridgeInstallPath, bridgeOsType, isFirstTimeBridge]);

  const isTunnelRequired = useMemo(() => {
    return window.location.protocol === 'https:' && (qwenEndpoint.includes('localhost') || qwenEndpoint.includes('127.0.0.1'));
  }, [qwenEndpoint]);

  // --- HANDLERS ---
  const handleSelectApiKey = async () => {
    if (window.aistudio) {
      await window.aistudio.openSelectKey();
      setHasSelectedKey(true);
    }
  };

  const updateFile = useCallback((id: string, updates: Partial<MediaFile>) => {
    setMediaFiles(prev => (prev || []).map(mf => (mf.id === id ? { ...mf, ...updates } : mf)));
  }, []);

  const handleFilesAdded = useCallback(async (files: File[]) => {
    const mediaUploads = files.filter(file => file.type.startsWith('image/') || file.type.startsWith('video/'));
    const newMediaFiles = await Promise.all(mediaUploads.map(async (file) => ({
      id: `${file.name}-${Math.random()}`,
      file,
      previewUrl: URL.createObjectURL(file),
      caption: '',
      status: GenerationStatus.IDLE,
      isSelected: false,
      customInstructions: '',
      comfyStatus: 'idle'
    } as MediaFile)));
    setMediaFiles(prev => [...(prev || []), ...newMediaFiles]);
  }, []);

  const handleCheckQuality = useCallback(async (id: string) => {
    const fileToProcess = (mediaFiles || []).find(mf => mf.id === id);
    if (!hasValidConfig || !fileToProcess || !fileToProcess.caption) return;

    updateFile(id, { status: GenerationStatus.CHECKING, errorMessage: undefined });

    try {
        const score = apiProvider === 'gemini'
            ? await checkCaptionQuality(fileToProcess.file, fileToProcess.caption, abortControllerRef.current.signal, geminiApiKey, geminiModel)
            : await checkQualityQwen('', qwenEndpoint, qwenEffectiveModel, fileToProcess.file, fileToProcess.caption, qwenVideoFrameCount, abortControllerRef.current.signal);
        
        updateFile(id, { qualityScore: score, status: GenerationStatus.SUCCESS });
    } catch (err: any) {
        if (err.name === 'AbortError' || err.message === 'AbortError') {
            updateFile(id, { status: GenerationStatus.IDLE, errorMessage: "Stopped by user" });
        } else {
            updateFile(id, { status: GenerationStatus.ERROR, errorMessage: err.message });
        }
    }
  }, [mediaFiles, apiProvider, qwenEndpoint, qwenEffectiveModel, qwenVideoFrameCount, hasValidConfig, updateFile, geminiApiKey, geminiModel]);

  const handleGenerateCaption = useCallback(async (id: string, itemInstructions?: string) => {
    const fileToProcess = (mediaFiles || []).find(mf => mf.id === id);
    if (!hasValidConfig || !fileToProcess) return;

    updateFile(id, { status: GenerationStatus.GENERATING, errorMessage: undefined, qualityScore: undefined });
    
    const combinedInstructions = `${bulkGenerationInstructions}\n\n${itemInstructions || ''}`.trim();

    try {
      const caption = apiProvider === 'gemini'
        ? await generateCaption(fileToProcess.file, triggerWord, combinedInstructions, isCharacterTaggingEnabled, characterShowName, abortControllerRef.current.signal, geminiApiKey, geminiModel)
        : await generateCaptionQwen('', qwenEndpoint, qwenEffectiveModel, fileToProcess.file, triggerWord, combinedInstructions, isCharacterTaggingEnabled, characterShowName, qwenVideoFrameCount, abortControllerRef.current.signal);
      
      updateFile(id, { caption, status: GenerationStatus.SUCCESS });
    } catch (err: any) {
      if (err.name === 'AbortError' || err.message === 'AbortError') {
          updateFile(id, { status: GenerationStatus.IDLE, errorMessage: "Stopped by user" });
      } else {
          updateFile(id, { status: GenerationStatus.ERROR, errorMessage: err.message });
      }
    }
  }, [mediaFiles, triggerWord, apiProvider, qwenEndpoint, qwenEffectiveModel, qwenVideoFrameCount, bulkGenerationInstructions, isCharacterTaggingEnabled, characterShowName, hasValidConfig, updateFile, geminiApiKey, geminiModel]);

  const handleRefineCaptionItem = useCallback(async (id: string, itemInstructions?: string) => {
    const fileToProcess = (mediaFiles || []).find(mf => mf.id === id);
    if (!hasValidConfig || !fileToProcess || !fileToProcess.caption) return;

    updateFile(id, { status: GenerationStatus.GENERATING, errorMessage: undefined });
    
    const combinedInstructions = `${bulkRefinementInstructions}\n\n${itemInstructions || ''}`.trim();

    try {
      const caption = apiProvider === 'gemini'
        ? await refineCaption(fileToProcess.file, fileToProcess.caption, combinedInstructions, abortControllerRef.current.signal, geminiApiKey, geminiModel)
        : await refineCaptionQwen('', qwenEndpoint, qwenEffectiveModel, fileToProcess.file, fileToProcess.caption, combinedInstructions, qwenVideoFrameCount, abortControllerRef.current.signal);
      
      updateFile(id, { caption, status: GenerationStatus.SUCCESS });
    } catch (err: any) {
      if (err.name === 'AbortError' || err.message === 'AbortError') {
          updateFile(id, { status: GenerationStatus.IDLE, errorMessage: "Stopped by user" });
      } else {
          updateFile(id, { status: GenerationStatus.ERROR, errorMessage: err.message });
      }
    }
  }, [mediaFiles, apiProvider, qwenEndpoint, qwenEffectiveModel, qwenVideoFrameCount, bulkRefinementInstructions, hasValidConfig, updateFile, geminiApiKey, geminiModel]);

  // --- QUEUE CONTROLLER ---
  const runTasksInQueue = async (tasks: (() => Promise<void>)[]) => {
    setIsQueueRunning(true);
    const pool = new Set<Promise<void>>();
    for (const task of tasks) {
      if (abortControllerRef.current.signal.aborted) break;
      const promise = task();
      pool.add(promise);
      promise.finally(() => pool.delete(promise));
      if (pool.size >= concurrentTasks) {
        await Promise.race(pool);
      }
    }
    await Promise.all(pool);
    setIsQueueRunning(false);
  };

  const handleBulkGenerate = () => {
    const tasks = selectedFiles.map(file => () => handleGenerateCaption(file.id, file.customInstructions));
    if (useRequestQueue) {
      runTasksInQueue(tasks);
    } else {
      tasks.forEach(t => t());
    }
  };

  const handleBulkRefine = () => {
    const tasks = selectedFiles.map(file => () => handleRefineCaptionItem(file.id, file.customInstructions));
    if (useRequestQueue) {
      runTasksInQueue(tasks);
    } else {
      tasks.forEach(t => t());
    }
  };

  const handleBulkQualityCheck = () => {
    const tasks = selectedFiles.map(file => () => handleCheckQuality(file.id));
    if (useRequestQueue) {
      runTasksInQueue(tasks);
    } else {
      tasks.forEach(t => t());
    }
  };

  const handleClearWorkflow = useCallback(() => {
    setComfyWorkflow(DEFAULT_COMFY_WORKFLOW);
    setComfyWorkflowName('Default Workflow');
  }, []);

  const handleComfyPreview = useCallback(async (id: string) => {
    const item = (mediaFiles || []).find(m => m.id === id);
    if (!item || !comfyWorkflow || !comfyUrl) return;

    updateFile(id, { comfyStatus: 'generating', comfyErrorMessage: undefined });
    try {
        const previewUrl = await sendComfyPrompt(comfyUrl, comfyWorkflow, item.caption, comfySeed, comfySteps, useSecureBridge, abortControllerRef.current.signal);
        updateFile(id, { comfyPreviewUrl: previewUrl, comfyStatus: 'success' });
    } catch (err: any) {
        if (err.name === 'AbortError' || err.message === 'Aborted') {
            updateFile(id, { comfyStatus: 'idle', comfyErrorMessage: "Stopped" });
        } else {
            updateFile(id, { comfyStatus: 'error', comfyErrorMessage: err.message });
        }
    }
  }, [mediaFiles, comfyWorkflow, comfyUrl, comfySeed, comfySteps, useSecureBridge, updateFile]);

  const handleBulkPreview = () => {
    selectedFiles.forEach(file => handleComfyPreview(file.id));
  };

  const handleDeleteSelected = useCallback(() => {
    setMediaFiles(prev => {
      const remaining = (prev || []).filter(mf => !mf.isSelected);
      return remaining || [];
    });
  }, []);

  const handleStopTasks = () => {
    abortControllerRef.current.abort();
    abortControllerRef.current = new AbortController();
    setIsQueueRunning(false);
    setMediaFiles(prev => (prev || []).map(mf => {
      if (mf.status === GenerationStatus.GENERATING || mf.status === GenerationStatus.CHECKING) {
        return { ...mf, status: GenerationStatus.IDLE, errorMessage: "Stopped by user" };
      }
      if (mf.comfyStatus === 'generating') {
        return { ...mf, comfyStatus: 'idle', comfyErrorMessage: "Stopped" };
      }
      return mf;
    }));
  };

  const handleExportDataset = useCallback(async () => {
    if (selectedFiles.length === 0) return;
    const JSZip = (window as any).JSZip;
    if (!JSZip) return alert("JSZip not loaded.");

    setIsExporting(true);
    try {
      const zip = new JSZip();
      const prefix = datasetPrefix.trim() || 'item';
      selectedFiles.forEach((mf, idx) => {
        const fileExt = mf.file.name.split('.').pop() || 'dat';
        const finalName = `${prefix}_${idx + 1}`;
        zip.file(`${finalName}.${fileExt}`, mf.file);
        zip.file(`${finalName}.txt`, mf.caption || "");
      });
      const content = await zip.generateAsync({ type: 'blob' });
      const link = document.createElement('a');
      link.href = URL.createObjectURL(content);
      link.download = `lora_dataset_${new Date().getTime()}.zip`;
      link.click();
    } catch (err: any) {
        alert("Export failed: " + err.message);
    } finally { setIsExporting(false); }
  }, [selectedFiles, datasetPrefix]);

  const handleNextPreview = useCallback(() => {
    if (!activePreviewId || (mediaFiles || []).length <= 1) return;
    const currentIndex = mediaFiles.findIndex(m => m.id === activePreviewId);
    const nextIndex = (currentIndex + 1) % mediaFiles.length;
    setActivePreviewId(mediaFiles[nextIndex].id);
  }, [activePreviewId, mediaFiles]);

  const handlePrevPreview = useCallback(() => {
    if (!activePreviewId || (mediaFiles || []).length <= 1) return;
    const currentIndex = mediaFiles.findIndex(m => m.id === activePreviewId);
    const prevIndex = (currentIndex - 1 + mediaFiles.length) % mediaFiles.length;
    setActivePreviewId(mediaFiles[prevIndex].id);
  }, [activePreviewId, mediaFiles]);

  const downloadQwenSetupScript = () => {
    const isWin = qwenOsType === 'windows';
    const content = isWin 
      ? `@echo off\npython -m venv venv\ncall venv\\Scripts\\activate\npip install vllm bitsandbytes\necho Setup Complete.`
      : `#!/bin/bash\npython3 -m venv venv\nsource venv/bin/activate\npip install vllm bitsandbytes\necho Setup Complete.`;
    const filename = isWin ? 'setup_qwen.bat' : 'setup_qwen.sh';
    const blob = new Blob([content], { type: 'text/plain' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    a.click();
    URL.revokeObjectURL(url);
  };

  const downloadBridgeScript = () => {
    const code = `import requests\nfrom flask import Flask, request, Response\nfrom flask_cors import CORS\napp = Flask(__name__)\nCORS(app)\nTARGET = "http://127.0.0.1:8188"\n@app.route('/', defaults={'path': ''}, methods=['GET','POST','PUT','DELETE','PATCH','OPTIONS'])\n@app.route('/<path:path>', methods=['GET','POST','PUT','DELETE','PATCH','OPTIONS'])\ndef proxy(path):\n  url = f"{TARGET}/{path}"\n  headers = {k:v for k,v in request.headers.items() if k.lower() not in ['host', 'origin', 'referer']}\n  resp = requests.request(method=request.method, url=url, headers=headers, data=request.get_data(), params=request.args, stream=True)\n  return Response(resp.content, resp.status_code, [(n,v) for n,v in resp.headers.items() if n.lower() not in ['content-encoding','content-length','transfer-encoding','connection']])\nif __name__ == '__main__': app.run(port=5000, host='0.0.0.0')`;
    const blob = new Blob([code], { type: 'text/x-python' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'bridge.py';
    a.click();
    URL.revokeObjectURL(url);
  };

  // --- RENDER ---
  return (
    <div className="min-h-screen bg-gray-950 text-gray-100 font-sans p-4 sm:p-8">
      {/* PREVIEW MODAL */}
      {activePreviewId && currentPreviewItem && (
          <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/95 backdrop-blur-sm animate-fade-in" onClick={() => setActivePreviewId(null)}>
              <div className="bg-gray-900 w-full max-w-6xl rounded-2xl border border-gray-700 overflow-hidden flex flex-col max-h-[95vh] animate-scale-up shadow-2xl relative" onClick={(e) => e.stopPropagation()}>
                  <button onClick={handlePrevPreview} className="absolute left-4 top-1/2 -translate-y-1/2 z-10 p-4 bg-gray-800/80 hover:bg-indigo-600 rounded-full text-white shadow-2xl transition-all border border-white/5 active:scale-90">
                    <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M15 19l-7-7 7-7"/></svg>
                  </button>
                  <button onClick={handleNextPreview} className="absolute right-4 top-1/2 -translate-y-1/2 z-10 p-4 bg-gray-800/80 hover:bg-indigo-600 rounded-full text-white shadow-2xl transition-all border border-white/5 active:scale-90">
                    <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M9 5l7 7-7 7"/></svg>
                  </button>
                  <div className="px-6 py-4 border-b border-gray-800 flex justify-between items-center bg-gray-850">
                      <div className="flex items-center gap-4">
                        <SparklesIcon className="w-5 h-5 text-indigo-400" />
                        <div className="flex flex-col">
                            <h3 className="text-xs font-black uppercase tracking-widest text-gray-400">{(mediaFiles || []).findIndex(m => m.id === activePreviewId) + 1} of {mediaFiles.length}</h3>
                            <h3 className="text-[11px] font-bold truncate max-w-md text-gray-500">{currentPreviewItem.file.name}</h3>
                        </div>
                      </div>
                      <div className="flex items-center gap-2">
                        <button onClick={handlePrevPreview} className="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-xl text-[10px] font-black uppercase transition-all">Prev</button>
                        <button onClick={handleNextPreview} className="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-xl text-[10px] font-black uppercase transition-all">Next</button>
                        <button onClick={() => setActivePreviewId(null)} className="ml-4 p-2 hover:bg-red-600/20 rounded-full transition-colors text-gray-500 hover:text-red-400"><XIcon className="w-5 h-5" /></button>
                      </div>
                  </div>
                  <div className="flex-grow overflow-y-auto p-6 space-y-8 bg-black/40">
                      <div className="grid grid-cols-1 md:grid-cols-2 gap-8 h-[450px]">
                          <div className="bg-black rounded-2xl border border-gray-800 flex items-center justify-center overflow-hidden relative shadow-inner">
                             {currentPreviewItem.file.type.startsWith('video/') ? <video src={currentPreviewItem.previewUrl} className="max-h-full" controls /> : <img src={currentPreviewItem.previewUrl} className="max-h-full object-contain" />}
                             <div className="absolute top-3 left-3 bg-black/70 backdrop-blur-md px-3 py-1 rounded-lg text-[10px] font-black uppercase text-white/80 border border-white/5">Original Data</div>
                          </div>
                          <div className="bg-black rounded-2xl border border-gray-800 flex items-center justify-center relative overflow-hidden shadow-inner">
                             {currentPreviewItem.comfyPreviewUrl ? <img src={currentPreviewItem.comfyPreviewUrl} className="max-h-full object-contain" /> : <div className="text-xs uppercase text-gray-700 tracking-widest font-black">No Preview Rendered</div>}
                             {currentPreviewItem.comfyStatus === 'generating' && <div className="absolute inset-0 bg-black/60 flex flex-col items-center justify-center gap-3"><LoaderIcon className="w-10 h-10 animate-spin text-orange-500" /><span className="text-xs font-black uppercase text-orange-400 tracking-widest">Rendering via ComfyUI...</span></div>}
                             <div className="absolute top-3 left-3 bg-orange-600/70 backdrop-blur-md px-3 py-1 rounded-lg text-[10px] font-black uppercase text-white/90 border border-white/5">ComfyUI Render</div>
                          </div>
                      </div>
                      <div className="space-y-6">
                          <textarea value={currentPreviewItem.caption} onChange={(e) => updateFile(currentPreviewItem.id, { caption: e.target.value })} className="w-full bg-gray-950 border border-gray-700 rounded-2xl p-6 text-sm h-40 outline-none focus:ring-2 focus:ring-indigo-500 transition-all shadow-inner leading-relaxed" />
                          <div className="flex gap-4">
                             <input type="text" value={currentPreviewItem.customInstructions} onChange={(e) => updateFile(currentPreviewItem.id, { customInstructions: e.target.value })} placeholder="Refine caption instructions..." className="flex-grow bg-gray-800 border border-gray-700 rounded-xl px-5 py-3 text-sm outline-none focus:ring-1 focus:ring-indigo-500 shadow-sm" />
                             <button onClick={() => handleGenerateCaption(currentPreviewItem.id, currentPreviewItem.customInstructions)} className="px-8 py-3 bg-green-600 hover:bg-green-500 text-white rounded-xl text-xs font-black uppercase transition-all shadow-xl active:scale-95">Re-Generate</button>
                             <button onClick={() => handleRefineCaptionItem(currentPreviewItem.id, currentPreviewItem.customInstructions)} className="px-8 py-3 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-xs font-black uppercase transition-all shadow-xl active:scale-95">Refine</button>
                             <button onClick={() => handleCheckQuality(currentPreviewItem.id)} className="px-8 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-xl text-xs font-black uppercase transition-all shadow-xl active:scale-95">Check Quality</button>
                             <button onClick={() => handleComfyPreview(currentPreviewItem.id)} className="px-8 py-3 bg-orange-600 hover:bg-orange-500 text-white rounded-xl text-xs font-black uppercase transition-all shadow-xl active:scale-95">Preview</button>
                          </div>
                      </div>
                  </div>
              </div>
          </div>
      )}

      <main className="max-w-6xl mx-auto space-y-8 animate-fade-in">
        <section className="bg-gray-900 border border-gray-800 p-8 rounded-3xl shadow-2xl space-y-12">
            <h2 className="text-3xl font-black flex items-center gap-4 uppercase tracking-tighter text-white">1. Global Settings & Actions</h2>
            
            <div className="grid grid-cols-1 lg:grid-cols-2 gap-16">
                <div className="space-y-10">
                    <div>
                        <label className="text-xs font-black text-gray-500 uppercase tracking-widest block mb-4">AI Provider</label>
                        <div className="flex p-1.5 bg-black rounded-2xl border border-gray-800 shadow-inner">
                            <button onClick={() => setApiProvider('gemini')} className={`flex-1 py-3 text-xs font-black uppercase rounded-xl transition-all ${apiProvider === 'gemini' ? 'bg-indigo-600 text-white shadow-lg' : 'text-gray-600 hover:text-gray-400'}`}>Google Gemini</button>
                            <button onClick={() => setApiProvider('qwen')} className={`flex-1 py-3 text-xs font-black uppercase rounded-xl transition-all ${apiProvider === 'qwen' ? 'bg-indigo-600 text-white shadow-lg' : 'text-gray-600 hover:text-gray-400'}`}>Local Qwen (GPU)</button>
                        </div>
                    </div>

                    {apiProvider === 'gemini' ? (
                        <div className="bg-indigo-500/5 border border-indigo-500/20 p-6 rounded-3xl space-y-6 animate-slide-down shadow-xl">
                            <div className="space-y-4">
                                <div className="flex justify-between items-center">
                                    <label className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Gemini Model Version</label>
                                </div>
                                <select 
                                    value={geminiModel} 
                                    onChange={(e) => setGeminiModel(e.target.value)}
                                    className="w-full p-3 bg-black border border-indigo-500/30 rounded-xl text-xs font-bold text-gray-300 shadow-inner focus:ring-1 focus:ring-indigo-500 outline-none"
                                >
                                    {GEMINI_MODELS.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
                                </select>
                            </div>

                            <div className="space-y-4">
                                <div className="flex justify-between items-center">
                                    <label className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Gemini API Key</label>
                                    {geminiApiKey && <span className="flex items-center gap-1.5 text-[9px] font-black uppercase text-green-400 bg-green-400/10 px-2 py-0.5 rounded-full"><CheckCircleIcon className="w-3 h-3"/> Configured</span>}
                                </div>
                                <div className="relative group">
                                    <input 
                                        type="password" 
                                        value={geminiApiKey} 
                                        onChange={(e) => setGeminiApiKey(e.target.value)}
                                        placeholder="Enter your Gemini API key here..."
                                        className="w-full py-4 px-5 bg-black border border-indigo-500/30 rounded-2xl text-xs font-mono shadow-inner focus:ring-1 focus:ring-indigo-500 outline-none hover:border-indigo-500/60 transition-all"
                                    />
                                    <div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-indigo-400/50 group-hover:text-indigo-400 transition-colors">
                                        <SparklesIcon className="w-5 h-5" />
                                    </div>
                                </div>
                            </div>
                            <p className="text-[10px] text-gray-500 flex items-center gap-1.5 px-1">
                                <AlertTriangleIcon className="w-3 h-3 text-indigo-400" />
                                Get an API key from 
                                <a href="https://aistudio.google.com/app/apikey" target="_blank" rel="noopener noreferrer" className="text-indigo-400 hover:underline font-bold">Google AI Studio</a>
                            </p>
                        </div>
                    ) : (
                        <div className="bg-gray-950 p-6 rounded-3xl border border-gray-800 space-y-6 animate-slide-down shadow-xl">
                            <div className="flex justify-between items-center mb-2">
                                <label className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Local Model Configuration</label>
                                <div className="flex items-center gap-4">
                                  <label className="flex items-center gap-2 cursor-pointer group">
                                      <input type="checkbox" checked={useOfflineSnapshot} onChange={e => setUseOfflineSnapshot(e.target.checked)} className="h-4 w-4 rounded bg-gray-800 border-gray-700 text-indigo-600" />
                                      <span className="text-[10px] font-bold text-orange-400 group-hover:text-orange-300">Use Offline Local Snapshot</span>
                                  </label>
                                  {!useOfflineSnapshot && (
                                    <label className="flex items-center gap-2 cursor-pointer group">
                                        <input type="checkbox" checked={useCustomQwenModel} onChange={e => setUseCustomQwenModel(e.target.checked)} className="h-4 w-4 rounded bg-gray-800 border-gray-700 text-indigo-600" />
                                        <span className="text-[10px] font-bold text-gray-500 group-hover:text-gray-300">Custom Model ID</span>
                                    </label>
                                  )}
                                </div>
                            </div>
                            
                            {useOfflineSnapshot ? (
                              <div className="space-y-4 animate-slide-down">
                                <div className="space-y-1">
                                    <label className="text-[9px] font-black text-gray-700 uppercase">Snapshot Directory Path</label>
                                    <input type="text" value={snapshotPath} onChange={e => setSnapshotPath(e.target.value)} placeholder="/path/to/hf_cache/.../snapshots/hash..." className="w-full p-2.5 bg-black border border-gray-800 rounded-xl text-xs font-mono shadow-inner" />
                                </div>
                                <div className="space-y-1">
                                    <label className="text-[9px] font-black text-gray-700 uppercase">Virtual Model Name (Served Name)</label>
                                    <input type="text" value={virtualModelName} onChange={e => setVirtualModelName(e.target.value)} placeholder="org/model-id..." className="w-full p-2.5 bg-black border border-gray-800 rounded-xl text-xs font-mono shadow-inner" />
                                </div>
                              </div>
                            ) : useCustomQwenModel ? (
                                <input type="text" value={customQwenModelId} onChange={e => setCustomQwenModelId(e.target.value)} placeholder="org/model-id..." className="w-full p-3 bg-black border border-gray-800 rounded-xl text-xs font-mono shadow-inner" />
                            ) : (
                                <select value={qwenModel} onChange={e => setQwenModel(e.target.value)} className="w-full p-3 bg-black border border-gray-800 rounded-xl text-xs font-bold text-gray-300 shadow-inner">
                                    {QWEN_MODELS.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
                                </select>
                            )}

                            <div className="pt-4 border-t border-gray-800 space-y-4">
                                <div className="flex justify-between items-center">
                                    <span className="text-[10px] font-black text-gray-600 uppercase">OS Type:</span>
                                    <div className="flex gap-2">
                                        <button onClick={() => setQwenOsType('windows')} className={`px-3 py-1 text-[9px] font-black uppercase rounded-lg transition-all ${qwenOsType === 'windows' ? 'bg-indigo-600 text-white' : 'text-gray-600 hover:text-gray-400'}`}>Windows</button>
                                        <button onClick={() => setQwenOsType('linux')} className={`px-3 py-1 text-[9px] font-black uppercase rounded-lg transition-all ${qwenOsType === 'linux' ? 'bg-indigo-600 text-white' : 'text-gray-600 hover:text-gray-400'}`}>Linux</button>
                                    </div>
                                </div>
                                
                                <div className="grid grid-cols-4 gap-4">
                                    <div className="col-span-3 space-y-1">
                                        <label className="text-[9px] font-black text-gray-700 uppercase">Install Path</label>
                                        <input type="text" value={qwenInstallDir} onChange={e => setQwenInstallDir(e.target.value)} className="w-full p-2.5 bg-black border border-gray-800 rounded-xl text-xs font-mono" />
                                    </div>
                                    <div className="space-y-1">
                                        <label className="text-[9px] font-black text-gray-700 uppercase">Max Tokens</label>
                                        <input type="number" value={qwenMaxTokens} onChange={e => setQwenMaxTokens(Number(e.target.value))} className="w-full p-2.5 bg-black border border-gray-800 rounded-xl text-xs text-center" />
                                    </div>
                                </div>

                                <div className="flex flex-col gap-2">
                                    <label className="flex items-center gap-2 cursor-pointer group">
                                        <input type="checkbox" checked={qwen8Bit} onChange={e => setQwen8Bit(e.target.checked)} className="h-4 w-4 rounded bg-gray-800 text-indigo-600" />
                                        <span className="text-[10px] font-bold text-gray-500 group-hover:text-gray-300">Enable 8-bit Quantization (bitsandbytes)</span>
                                    </label>
                                    <label className="flex items-center gap-2 cursor-pointer group">
                                        <input type="checkbox" checked={qwenEager} onChange={e => setQwenEager(e.target.checked)} className="h-4 w-4 rounded bg-gray-950 text-indigo-600" />
                                        <span className="text-[10px] font-bold text-gray-500 group-hover:text-gray-300">Enforce Eager Mode</span>
                                    </label>
                                </div>

                                <button onClick={downloadQwenSetupScript} className="w-full py-3 bg-green-700 hover:bg-green-600 text-white text-[10px] font-black uppercase rounded-xl transition-all shadow-lg">Download Setup Script</button>

                                <div className="space-y-2">
                                    <label className="text-[9px] font-black text-gray-700 uppercase">Local Start Command:</label>
                                    <div className="relative group">
                                        <div className="p-3 bg-black rounded-xl border border-gray-900 font-mono text-[10px] text-green-500/80 break-all leading-relaxed max-h-24 overflow-y-auto shadow-inner">
                                            {qwenStartCommand}
                                        </div>
                                        <button onClick={() => navigator.clipboard.writeText(qwenStartCommand)} className="absolute top-2 right-2 p-1.5 bg-gray-800 hover:bg-gray-700 text-gray-400 rounded-lg opacity-0 group-hover:opacity-100 transition-all"><CopyIcon className="w-3.5 h-3.5"/></button>
                                    </div>
                                </div>
                                
                                <div className="space-y-1">
                                    <label className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Endpoint URL (Tunnel or Local)</label>
                                    <input type="text" value={qwenEndpoint} onChange={e => setQwenEndpoint(e.target.value)} placeholder="http://localhost:8000/v1" className="w-full p-3 bg-black border border-gray-800 rounded-xl text-xs font-mono shadow-inner focus:ring-1 focus:ring-indigo-500 outline-none" />
                                </div>
                            </div>
                        </div>
                    )}

                    <div className="space-y-6">
                        <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
                            <div className="space-y-2">
                                <label className="text-[10px] font-black text-gray-500 uppercase tracking-widest">Trigger Word</label>
                                <input type="text" value={triggerWord} onChange={e => setTriggerWord(e.target.value)} className="w-full p-3 bg-gray-950 border border-gray-800 rounded-2xl text-sm font-bold shadow-inner" placeholder="MyStyle" />
                            </div>
                            <div className="space-y-2">
                                <label className="text-[10px] font-black text-gray-500 uppercase tracking-widest">File Prefix</label>
                                <input type="text" value={datasetPrefix} onChange={e => setDatasetPrefix(e.target.value)} className="w-full p-3 bg-gray-950 border border-gray-800 rounded-2xl text-sm font-bold shadow-inner" placeholder="item" />
                            </div>
                        </div>
                        <div className="bg-gray-800/40 p-5 rounded-3xl border border-gray-800 space-y-4 shadow-xl">
                            <label className="flex items-center gap-3 cursor-pointer group">
                                <input type="checkbox" checked={isCharacterTaggingEnabled} onChange={(e) => setIsCharacterTaggingEnabled(e.target.checked)} className="h-6 w-6 rounded-lg bg-gray-900 border-gray-700 text-indigo-600 transition-all shadow-sm" />
                                <span className="text-xs font-black text-gray-500 uppercase tracking-wider group-hover:text-gray-300 transition-colors">Character Tagging</span>
                            </label>
                            {isCharacterTaggingEnabled && (
                                <div className="animate-slide-down">
                                    <input type="text" value={characterShowName} onChange={(e) => setCharacterShowName(e.target.value)} placeholder="Enter show/series name..." className="w-full p-3 bg-gray-950 border border-gray-700 rounded-xl text-xs font-medium focus:ring-1 focus:ring-indigo-500 outline-none transition-all shadow-inner" />
                                </div>
                            )}
                        </div>
                    </div>
                </div>

                <div className="space-y-10">
                    <div className="space-y-8">
                        <div className="space-y-3">
                            <label className="text-xs font-black text-gray-500 uppercase tracking-widest block">System Instructions & Prompting</label>
                            <textarea value={bulkGenerationInstructions} onChange={(e) => setBulkGenerationInstructions(e.target.value)} className="w-full p-5 bg-gray-950 border border-gray-800 rounded-3xl text-[13px] h-40 leading-relaxed resize-none outline-none focus:ring-2 focus:ring-indigo-500 shadow-inner" placeholder="Enter global captioning rules..." />
                        </div>
                        <div className="space-y-3">
                            <label className="text-xs font-black text-indigo-400 uppercase tracking-widest block">Refinement Instructions</label>
                            <textarea value={bulkRefinementInstructions} onChange={(e) => setBulkRefinementInstructions(e.target.value)} className="w-full p-5 bg-gray-950 border border-indigo-500/20 rounded-3xl text-[13px] h-40 leading-relaxed resize-none outline-none focus:ring-2 focus:ring-indigo-500 shadow-inner" placeholder="Enter instructions for refining existing captions..." />
                        </div>
                    </div>

                    <div className="flex flex-col gap-6 pt-4 border-t border-gray-800">
                        <div className="flex flex-wrap gap-x-8 gap-y-4">
                            <label className="flex items-center gap-3 cursor-pointer group">
                                <input type="checkbox" checked={autofitTextareas} onChange={(e) => setAutofitTextareas(e.target.checked)} className="h-5 w-5 rounded-md bg-gray-900 border-gray-700 text-indigo-500 shadow-inner" />
                                <span className="text-xs font-bold text-gray-500 uppercase group-hover:text-gray-300 transition-colors">Autofit Textboxes</span>
                            </label>
                            <label className="flex items-center gap-3 cursor-pointer group">
                                <input type="checkbox" checked={showSideBySidePreview} onChange={(e) => setShowSideBySidePreview(e.target.checked)} className="h-5 w-5 rounded-md bg-gray-900 border-gray-700 text-indigo-500 shadow-inner" />
                                <span className="text-xs font-bold text-gray-500 uppercase group-hover:text-gray-300 transition-colors">Side-by-Side Comparison</span>
                            </label>
                            <label className="flex items-center gap-3 cursor-pointer group">
                                <input type="checkbox" checked={isComfyEnabled} onChange={(e) => setIsComfyEnabled(e.target.checked)} className="h-5 w-5 rounded-md bg-gray-900 border-gray-700 text-orange-500 shadow-inner" />
                                <span className="text-xs font-black text-orange-500 uppercase tracking-widest group-hover:text-orange-400 transition-colors">Enable ComfyUI Previews</span>
                            </label>
                        </div>
                        
                        <div className="bg-indigo-600/5 border border-indigo-600/20 p-6 rounded-3xl space-y-4">
                            <div className="flex justify-between items-center">
                                <label className="flex items-center gap-3 cursor-pointer group">
                                    <input type="checkbox" checked={useRequestQueue} onChange={(e) => setUseRequestQueue(e.target.checked)} className="h-5 w-5 rounded bg-gray-900 border-gray-700 text-indigo-500" />
                                    <span className="text-xs font-black text-indigo-400 uppercase tracking-widest group-hover:text-indigo-300 transition-colors">Enable Request Queue</span>
                                </label>
                                {useRequestQueue && (
                                    <div className="flex items-center gap-3">
                                        <label className="text-[10px] font-black text-gray-600 uppercase">Concurrent Tasks</label>
                                        <input type="number" min="1" max="10" value={concurrentTasks} onChange={(e) => setConcurrentTasks(Number(e.target.value))} className="w-16 p-1 bg-black border border-gray-800 rounded text-center text-xs font-bold" />
                                    </div>
                                )}
                            </div>
                            <p className="text-[10px] text-gray-600 italic">Recommended for Gemini Free Tier or Local GPU to prevent rate limits or OOM errors.</p>
                        </div>

                        {isComfyEnabled && (
                            <div className="bg-orange-600/5 border border-orange-600/20 p-6 rounded-3xl space-y-6 animate-slide-down shadow-xl">
                                <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
                                    <div className="space-y-2">
                                        <label className="text-[10px] font-black text-gray-600 uppercase">Endpoint</label>
                                        <input type="text" value={comfyUrl} onChange={(e) => setComfyUrl(e.target.value)} placeholder="http://127.0.0.1:8188" className="w-full p-3 bg-black border border-gray-800 rounded-xl text-xs font-mono shadow-inner" />
                                    </div>
                                    <div className="space-y-2">
                                        <label className="text-[10px] font-black text-gray-600 uppercase">Workflow ({comfyWorkflowName})</label>
                                        <div className="flex gap-2">
                                            <button onClick={() => document.getElementById('wf-up')?.click()} className="flex-1 py-2.5 bg-orange-600 hover:bg-orange-500 text-white rounded-xl shadow-lg transition-all active:scale-95 text-[10px] font-black uppercase tracking-widest">Load JSON</button>
                                            <button onClick={handleClearWorkflow} className="px-4 bg-gray-800 hover:bg-gray-700 text-gray-400 rounded-xl transition-all active:scale-95"><TrashIcon className="w-4 h-4"/></button>
                                            <input id="wf-up" type="file" accept=".json" onChange={(e) => {
                                                const f = e.target.files?.[0];
                                                if (f) {
                                                    const r = new FileReader();
                                                    r.onload = (ev) => {
                                                        try {
                                                            setComfyWorkflow(JSON.parse(ev.target?.result as string));
                                                            setComfyWorkflowName(f.name);
                                                        } catch { alert("Invalid Workflow JSON"); }
                                                    };
                                                    r.readAsText(f);
                                                }
                                            }} className="hidden" />
                                        </div>
                                    </div>
                                    <div className="space-y-2">
                                        <label className="text-[10px] font-black text-gray-600 uppercase">Default Seed (-1 for random)</label>
                                        <input type="number" value={comfySeed} onChange={(e) => setComfySeed(Number(e.target.value))} className="w-full p-3 bg-black border border-gray-800 rounded-xl text-xs shadow-inner" />
                                    </div>
                                    <div className="space-y-2">
                                        <label className="text-[10px] font-black text-gray-600 uppercase">Steps</label>
                                        <input type="number" value={comfySteps} onChange={(e) => setComfySteps(Number(e.target.value))} className="w-full p-3 bg-black border border-gray-800 rounded-xl text-xs shadow-inner" />
                                    </div>
                                </div>

                                {/* Secure Bridge Sub-section */}
                                <div className="pt-6 border-t border-orange-600/10 space-y-6">
                                    <div className="flex justify-between items-center">
                                        <h3 className="text-[11px] font-black text-orange-400 uppercase tracking-widest">Secure Bridge (for HTTPS/Remote access)</h3>
                                        <label className="flex items-center gap-3 cursor-pointer group">
                                            <input type="checkbox" checked={useSecureBridge} onChange={(e) => setUseSecureBridge(e.target.checked)} className="h-5 w-5 rounded bg-gray-900 border-gray-700 text-orange-500" />
                                            <span className="text-[10px] font-bold text-gray-500 group-hover:text-gray-300 transition-colors">Enable Bridge Proxy</span>
                                        </label>
                                    </div>

                                    {useSecureBridge && (
                                        <div className="space-y-6 animate-slide-down">
                                            <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
                                                <div className="space-y-2">
                                                    <label className="text-[9px] font-black text-gray-600 uppercase">Bridge OS</label>
                                                    <div className="flex gap-2">
                                                        <button onClick={() => setBridgeOsType('windows')} className={`flex-1 py-2 text-[10px] font-black uppercase rounded-lg transition-all ${bridgeOsType === 'windows' ? 'bg-orange-600 text-white' : 'bg-gray-800 text-gray-500'}`}>Windows</button>
                                                        <button onClick={() => setBridgeOsType('linux')} className={`flex-1 py-2 text-[10px] font-black uppercase rounded-lg transition-all ${bridgeOsType === 'linux' ? 'bg-orange-600 text-white' : 'bg-gray-800 text-gray-500'}`}>Linux</button>
                                                    </div>
                                                </div>
                                                <div className="space-y-2">
                                                    <label className="text-[9px] font-black text-gray-600 uppercase">Install Path</label>
                                                    <input type="text" value={bridgeInstallPath} onChange={(e) => setBridgeInstallPath(e.target.value)} className="w-full p-3 bg-black border border-gray-800 rounded-xl text-xs font-mono shadow-inner" />
                                                </div>
                                            </div>

                                            <div className="space-y-4">
                                                <label className="flex items-center gap-3 cursor-pointer group">
                                                    <input type="checkbox" checked={isFirstTimeBridge} onChange={(e) => setIsFirstTimeBridge(e.target.checked)} className="h-4 w-4 rounded bg-gray-950 border-gray-800 text-orange-500" />
                                                    <span className="text-[10px] font-bold text-gray-500 group-hover:text-gray-300">First-time Setup (Include VENV & Pip Install)</span>
                                                </label>
                                                <div className="flex gap-4">
                                                    <button onClick={downloadBridgeScript} className="flex-1 py-3 bg-orange-700 hover:bg-orange-600 text-white text-[10px] font-black uppercase rounded-xl transition-all shadow-lg">Download Bridge.py</button>
                                                </div>
                                            </div>

                                            <div className="space-y-2">
                                                <label className="text-[9px] font-black text-gray-700 uppercase tracking-widest">Start Command:</label>
                                                <div className="relative group">
                                                    <div className="p-3 bg-black rounded-xl border border-gray-900 font-mono text-[10px] text-green-500/80 break-all leading-relaxed shadow-inner">
                                                        {bridgeStartCommand}
                                                    </div>
                                                    <button onClick={() => navigator.clipboard.writeText(bridgeStartCommand)} className="absolute top-2 right-2 p-1.5 bg-gray-800 hover:bg-gray-700 text-gray-400 rounded-lg opacity-0 group-hover:opacity-100 transition-all"><CopyIcon className="w-3.5 h-3.5"/></button>
                                                </div>
                                                <p className="text-[9px] text-gray-600 italic">The bridge will proxy requests from this HTTPS app to your local HTTP ComfyUI server.</p>
                                            </div>
                                        </div>
                                    )}
                                </div>
                            </div>
                        )}
                    </div>
                </div>
            </div>

            <div className="border-t border-gray-800 pt-10 flex flex-col gap-6">
                <div className="flex flex-wrap gap-4 justify-end">
                    <button 
                      onClick={handleDeleteSelected} 
                      disabled={selectedFiles.length === 0}
                      className="px-6 py-4 bg-red-600/20 hover:bg-red-600/30 border border-red-600/30 rounded-2xl text-[11px] font-black uppercase text-red-400 flex items-center gap-3 transition-all active:scale-95 shadow-lg disabled:opacity-20 disabled:grayscale"
                    >
                      <TrashIcon className="w-5 h-5"/> Delete Selected ({selectedFiles.length})
                    </button>
                    <button onClick={handleStopTasks} className="px-6 py-4 bg-orange-600/20 hover:bg-orange-600/40 border border-orange-600/30 rounded-2xl text-[11px] font-black uppercase text-orange-400 flex items-center gap-3 transition-all active:scale-95 shadow-lg"><StopIcon className="w-5 h-5"/> Stop Tasks</button>
                    
                    <button onClick={handleBulkQualityCheck} disabled={selectedFiles.length === 0 || !hasValidConfig || isQueueRunning} className="px-6 py-4 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl text-[11px] font-black uppercase flex items-center gap-4 transition-all shadow-xl active:scale-95 disabled:opacity-40">
                      <CheckCircleIcon className="w-5 h-5" /> Check Quality Selected ({selectedFiles.length})
                    </button>

                    <button onClick={handleBulkGenerate} disabled={selectedFiles.length === 0 || !hasValidConfig || isQueueRunning} className="px-10 py-4 bg-green-600 hover:bg-green-500 text-white rounded-2xl text-xs font-black uppercase flex items-center gap-4 transition-all shadow-2xl shadow-green-900/30 active:scale-95 disabled:opacity-40">
                        <SparklesIcon className="w-6 h-6" /> Generate Selected ({selectedFiles.length})
                    </button>
                    
                    <button onClick={handleBulkRefine} disabled={selectedFiles.length === 0 || !hasValidConfig || isQueueRunning} className="px-10 py-4 bg-indigo-600 hover:bg-indigo-500 text-white rounded-2xl text-xs font-black uppercase flex items-center gap-4 transition-all shadow-xl active:scale-95 disabled:opacity-40">
                        <WandIcon className="w-6 h-6" /> Refine Selected ({selectedFiles.length})
                    </button>
                </div>
                <div className="flex flex-wrap gap-4 justify-end">
                    {isComfyEnabled && (
                      <button onClick={handleBulkPreview} disabled={selectedFiles.length === 0} className="px-10 py-4 bg-orange-600 hover:bg-orange-500 text-white rounded-2xl text-xs font-black uppercase flex items-center gap-4 transition-all shadow-xl shadow-orange-900/20 active:scale-95 disabled:opacity-40">
                        <WandIcon className="w-6 h-6" /> Preview Selected ({selectedFiles.length})
                      </button>
                    )}
                    <button onClick={handleExportDataset} disabled={selectedFiles.length === 0 || isExporting} className="w-full sm:w-auto px-16 py-5 bg-indigo-700 hover:bg-indigo-600 text-white rounded-2xl text-xs font-black uppercase flex items-center justify-center gap-4 transition-all shadow-2xl active:scale-95 disabled:opacity-40">
                        {isExporting ? <LoaderIcon className="w-6 h-6 animate-spin" /> : <DownloadIcon className="w-6 h-6" />}
                        {isExporting ? 'Packaging ZIP...' : 'Download Finished Dataset'}
                    </button>
                </div>
            </div>
        </section>

        <section className="bg-gray-900 border border-gray-800 p-8 rounded-3xl shadow-xl overflow-hidden relative">
            <div className="absolute top-0 right-0 p-8 opacity-10 pointer-events-none"><UploadCloudIcon className="w-32 h-32" /></div>
            <h2 className="text-xl font-black mb-6 uppercase tracking-widest text-gray-400">2. Upload Source Media</h2>
            <FileUploader onFilesAdded={handleFilesAdded} />
        </section>
        
        <section className="space-y-8 animate-slide-up min-h-[400px]">
          {mediaFiles && mediaFiles.length > 0 ? (
            <>
              <div className="flex justify-between items-center bg-gray-900/80 backdrop-blur-2xl p-6 rounded-3xl border border-gray-800 sticky top-4 z-40 shadow-[0_20px_50px_-10px_rgba(0,0,0,0.5)]">
                  <div className="flex items-center gap-4">
                      <div className="h-10 w-1.5 bg-indigo-500 rounded-full shadow-[0_0_15px_rgba(99,102,241,0.5)]"></div>
                      <div className="flex flex-col">
                          <h2 className="text-2xl font-black text-white uppercase tracking-tighter leading-none">3. Data Curation Workspace</h2>
                          <p className="text-[10px] font-black text-gray-600 uppercase tracking-widest mt-1">Ready for Parallel Processing ({mediaFiles.length} Loaded)</p>
                      </div>
                  </div>
                  <div className="flex items-center gap-6">
                      <div className="flex items-center gap-3 bg-black px-6 py-3 rounded-2xl border border-gray-800 shadow-inner group active:scale-95 transition-all">
                          <input type="checkbox" id="sel-all" className="h-6 w-6 rounded-lg bg-gray-900 border-gray-700 text-indigo-600 transition-all cursor-pointer shadow-sm" checked={mediaFiles.length > 0 && mediaFiles.every(f => f.isSelected)} onChange={(e) => setMediaFiles(prev => (prev || []).map(mf => ({ ...mf, isSelected: e.target.checked })))} />
                          <label htmlFor="sel-all" className="text-xs font-black text-gray-500 cursor-pointer group-hover:text-gray-300 transition-colors uppercase tracking-widest">Select All Items</label>
                      </div>
                  </div>
              </div>
              <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-12">
                {mediaFiles.map(item => (
                  <MediaItem 
                    key={item.id} 
                    item={item} 
                    autofit={autofitTextareas} 
                    isApiKeySet={hasValidConfig} 
                    isComfyEnabled={isComfyEnabled} 
                    showSideBySidePreview={showSideBySidePreview} 
                    onGenerate={handleGenerateCaption} 
                    onCheckQuality={handleCheckQuality}
                    onPreview={handleComfyPreview} 
                    onCaptionChange={(id, cap) => updateFile(id, { caption: cap })} 
                    onCustomInstructionsChange={(id, ins) => updateFile(id, { customInstructions: ins })} 
                    onSelectionChange={(id, sel) => updateFile(id, { isSelected: sel })} 
                    onOpenPreviewModal={setActivePreviewId} 
                  />
                ))}
              </div>
            </>
          ) : (
            <div className="flex flex-col items-center justify-center py-32 bg-gray-900/50 rounded-3xl border-2 border-dashed border-gray-800 text-gray-500 animate-pulse">
                <UploadCloudIcon className="w-16 h-16 mb-6 opacity-20" />
                <h3 className="text-lg font-black uppercase tracking-widest text-gray-700">No items uploaded yet</h3>
                <p className="text-xs mt-2 uppercase tracking-tight text-gray-600">Start by dropping files into the upload zone above</p>
            </div>
          )}
        </section>
      </main>
    </div>
  );
};

export default App;