File size: 33,275 Bytes
415a17d
55b1a24
565e57b
415a17d
 
55b1a24
edc4c90
213c234
8db63ba
fd1786e
565e57b
 
a8636b2
82f6d8e
f0b54f8
82f6d8e
991f849
 
48e6066
 
 
d009748
 
de7bb17
82f6d8e
 
 
24561f7
48e6066
4164789
48e6066
 
4164789
48e6066
 
 
8d5e74d
48e6066
8d5e74d
 
 
 
0268d3c
48e6066
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4164789
 
 
415a17d
 
 
55b1a24
 
415a17d
55b1a24
415a17d
565e57b
 
 
 
 
 
 
415a17d
 
d081d7a
 
 
 
e223b2b
415a17d
 
e223b2b
415a17d
27fe49f
213c234
4164789
 
213c234
 
 
 
 
4164789
 
213c234
55b1a24
 
 
213c234
 
 
4164789
213c234
 
 
 
 
4164789
 
213c234
 
 
415a17d
24561f7
4164789
415a17d
 
 
d081d7a
 
 
 
4164789
 
213c234
 
 
 
 
 
 
 
 
 
415a17d
 
d081d7a
 
4164789
d081d7a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4164789
 
d081d7a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415a17d
4164789
9a3c041
415a17d
9a3c041
415a17d
4164789
9a3c041
0268d3c
782c122
4164789
9a3c041
 
c703ea3
4164789
9a3c041
0268d3c
782c122
4164789
9a3c041
 
415a17d
9a3c041
c703ea3
 
9a3c041
4164789
9a3c041
991f849
 
d081d7a
 
415a17d
 
9a3c041
e6edab1
 
 
 
4164789
e6edab1
4164789
e6edab1
 
4164789
e6edab1
4164789
e6edab1
415a17d
4164789
415a17d
 
 
e6edab1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415a17d
4164789
565e57b
4164789
415a17d
 
565e57b
e6edab1
ce1f183
565e57b
 
 
 
 
ce1f183
 
565e57b
9a3c041
 
e6edab1
 
 
415a17d
 
782c122
4164789
565e57b
 
 
 
 
 
 
 
ce1f183
 
782c122
 
ce1f183
 
 
782c122
ce1f183
 
 
 
 
782c122
ce1f183
5c6b653
ce1f183
 
 
 
 
 
fba600d
ce1f183
782c122
ce1f183
782c122
 
ce1f183
782c122
c703ea3
ce1f183
c703ea3
9a3c041
 
 
 
 
ce1f183
9a3c041
6a7fe5b
ce1f183
55f5c6c
 
ce1f183
 
 
 
 
 
 
 
 
 
782c122
4164789
ce1f183
 
 
 
 
 
 
 
 
 
 
01393b5
 
 
ce1f183
83f8807
 
 
 
 
 
 
 
01393b5
ce1f183
 
 
782c122
 
 
 
 
5c6b653
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55f5c6c
 
 
5c6b653
 
 
 
782c122
 
 
 
415a17d
782c122
4164789
415a17d
48e6066
 
415a17d
 
01393b5
 
415a17d
55f5c6c
 
 
 
782c122
 
0268d3c
782c122
 
 
 
4164789
782c122
 
 
 
 
4164789
782c122
 
 
 
 
4164789
 
782c122
 
415a17d
782c122
 
 
4164789
415a17d
4164789
782c122
415a17d
 
782c122
 
2531fcd
4164789
2531fcd
1a441d0
2531fcd
 
1a441d0
2531fcd
 
e6edab1
 
d5e2d02
e6edab1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edc4c90
 
d9c705e
edc4c90
4164789
d9c705e
 
 
4164789
d9c705e
edc4c90
d9c705e
edc4c90
d9c705e
4164789
d9c705e
 
4164789
d9c705e
 
e6edab1
 
 
 
 
415a17d
 
 
c703ea3
4164789
9a3c041
c703ea3
 
ac7c05c
9a3c041
d9c705e
01393b5
 
c703ea3
9a3c041
c703ea3
83f8807
c703ea3
 
782c122
c703ea3
 
 
 
9a3c041
01393b5
 
c703ea3
9a3c041
c703ea3
 
 
 
 
9a3c041
01393b5
 
c703ea3
83f8807
9a3c041
01393b5
 
ce1f183
 
 
 
 
9a3c041
01393b5
 
9a3c041
 
 
 
 
 
 
 
83f8807
 
9a3c041
 
01393b5
 
83f8807
9a3c041
 
c703ea3
 
 
 
9a3c041
 
 
 
c703ea3
9a3c041
c703ea3
9a3c041
 
d9c705e
9a3c041
d9c705e
 
 
 
c703ea3
4164789
600cde8
 
 
 
 
544b046
4164789
544b046
55b1a24
4164789
 
 
 
 
 
55b1a24
 
600cde8
 
544b046
55b1a24
 
 
 
 
 
 
544b046
600cde8
c703ea3
b78dca2
 
fba600d
 
 
01393b5
b78dca2
82f6d8e
 
fba600d
01393b5
d009748
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23a36f6
c703ea3
a8636b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
600cde8
55b1a24
 
4164789
 
 
 
544b046
991f849
600cde8
 
 
 
415a17d
4164789
415a17d
 
 
55b1a24
 
415a17d
55b1a24
415a17d
01393b5
 
 
 
 
 
415a17d
 
 
 
55b1a24
415a17d
4164789
 
 
415a17d
 
4164789
415a17d
 
d081d7a
4164789
d081d7a
 
415a17d
4164789
 
415a17d
 
 
 
4164789
782c122
4164789
081e575
4164789
9a3c041
4164789
782c122
4164789
ab8a147
415a17d
 
 
 
 
 
 
55b1a24
415a17d
 
 
 
 
 
4164789
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415a17d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
<script lang="ts">
  import type { PicletGeneratorProps, PicletWorkflowState, CaptionType, CaptionLength, PicletStats } from '$lib/types';
  import type { PicletInstance, DiscoveryStatus } from '$lib/db/schema';
  import UploadStep from './UploadStep.svelte';
  import WorkflowProgress from './WorkflowProgress.svelte';
  import PicletResult from './PicletResult.svelte';
  import { removeBackground } from '$lib/utils/professionalImageProcessing';
  import { extractPicletMetadata } from '$lib/services/picletMetadata';
  import { savePicletInstance, generatedDataToPicletInstance } from '$lib/db/piclets';
  import { PicletType, TYPE_DATA } from '$lib/types/picletTypes';
  import { EnhancedCaptionService } from '$lib/services/enhancedCaption';
  import { CanonicalService } from '$lib/services/canonicalService';
  import { incrementDiscoveryCounter, addRarityScore, calculateRarityPoints } from '$lib/db/gameState';
  import { authStore } from '$lib/stores/auth';
  // import { withQwenTimeout } from '$lib/utils/qwenTimeout'; // Unused since qwen is disabled

  interface Props extends PicletGeneratorProps {}

  let {
    joyCaptionClient,
    fluxClient,
    gptOssClient,
    picletsServerClient
  }: Props = $props();

  // Get current user info for discoverer attribution
  const auth = $derived(authStore);
  
  // GPT-OSS-120B text generation
  const generateText = async (prompt: string): Promise<string> => {
    if (!gptOssClient) {
      throw new Error('GPT-OSS-120B client is not available');
    }

    console.log('Generating text with GPT-OSS-120B...');

    // ChatInterface expects: message, history, system_prompt, temperature
    const result = await gptOssClient.predict("/chat", [
      prompt,    // message
      [],        // history
      "You are a helpful assistant that creates Pokemon-style monster concepts based on real-world objects.", // system_prompt
      0.7        // temperature
    ]);

    // Extract Response section only (GPT-OSS formats with Analysis and Response)
    let responseText = result.data[0] || '';

    const responseMatch = responseText.match(/\*\*💬 Response:\*\*\s*\n\n([\s\S]*)/);
    if (responseMatch) {
      return responseMatch[1].trim();
    }

    // Fallback: extract after "assistantfinal"
    const finalMatch = responseText.match(/assistantfinal\s*([\s\S]*)/);
    if (finalMatch) {
      return finalMatch[1].trim();
    }

    return responseText;
  };
  
  let workflowState: PicletWorkflowState = $state({
    currentStep: 'upload',
    userImage: null,
    imageCaption: null,
    picletConcept: null,
    picletStats: null,
    imagePrompt: null,
    picletImage: null,
    error: null,
    isProcessing: false,
    // Discovery-specific state
    objectName: null,
    objectAttributes: [],
    visualDetails: null,
    discoveryStatus: null,
    canonicalPiclet: null
  });
  
  // Queue state for multi-image processing
  let imageQueue: File[] = $state([]);
  let currentImageIndex: number = $state(0);
  
  const IMAGE_GENERATION_PROMPT = (concept: string) => `Extract ONLY the visual appearance from this monster concept and describe it in one concise sentence:
"${concept}"

Focus on: colors, body shape, eyes, limbs, mouth, and key visual features. Omit backstory, abilities, and non-visual details.`;
  

  async function importPiclet(picletData: PicletInstance) {
    workflowState.isProcessing = true;
    workflowState.currentStep = 'complete';
    
    try {
      // Save the imported piclet
      const savedId = await savePicletInstance(picletData);
      
      // Create a success workflowState similar to generation
      workflowState.picletImage = {
        imageUrl: picletData.imageUrl,
        imageData: picletData.imageData,
        seed: 0,
        prompt: 'Imported piclet'
      };
      
      // Show import success
      workflowState.isProcessing = false;
      alert(`Successfully imported ${picletData.nickname || picletData.typeId}!`);
      
      // Reset to allow another import/generation
      setTimeout(() => reset(), 2000);
    } catch (error) {
      workflowState.error = `Failed to import piclet: ${error}`;
      workflowState.isProcessing = false;
    }
  }

  async function handleImageSelected(file: File) {
    if (!joyCaptionClient || !fluxClient) {
      workflowState.error = "Services not connected. Please wait...";
      return;
    }
    
    // Single image upload - clear queue and process normally
    imageQueue = [];
    currentImageIndex = 0;
    
    workflowState.userImage = file;
    workflowState.error = null;
    
    // Check if this is a piclet card with metadata
    const picletData = await extractPicletMetadata(file);
    if (picletData) {
      // Import existing piclet
      await importPiclet(picletData);
    } else {
      // Generate new piclet
      startWorkflow();
    }
  }
  
  async function handleImagesSelected(files: File[]) {
    if (!joyCaptionClient || !fluxClient) {
      workflowState.error = "Services not connected. Please wait...";
      return;
    }
    
    // Multi-image upload - set up queue and start with first image
    imageQueue = files;
    currentImageIndex = 0;
    
    await processCurrentImage();
  }
  
  async function processCurrentImage() {
    if (currentImageIndex >= imageQueue.length) {
      // Queue completed
      console.log('All images processed!');
      return;
    }
    
    const currentFile = imageQueue[currentImageIndex];
    workflowState.userImage = currentFile;
    workflowState.error = null;
    
    // Check if this is a piclet card with metadata
    const picletData = await extractPicletMetadata(currentFile);
    if (picletData) {
      // Import existing piclet
      await importPiclet(picletData);
      // Auto-advance to next image after import
      await advanceToNextImage();
    } else {
      // Generate new piclet
      startWorkflow();
    }
  }
  
  async function advanceToNextImage() {
    currentImageIndex++;
    
    if (currentImageIndex < imageQueue.length) {
      // Process next image
      setTimeout(() => processCurrentImage(), 1000); // Small delay for better UX
    } else {
      // Queue completed - reset to single image mode
      imageQueue = [];
      currentImageIndex = 0;
      reset();
    }
  }
  
  async function startWorkflow() {
    workflowState.isProcessing = true;

    try {
      // Step 1: Generate detailed object description with joy-caption (skip server lookup for now)
      await captionImage();
      await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for workflowState update

      // Step 2: Generate free-form monster concept with Qwen
      await generateConcept();
      await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for workflowState update

      // Step 3: Extract stats including physical characteristics
      await extractSimpleStats();
      await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for workflowState update

      // Step 4: Generate image prompt with Qwen
      await generateImagePrompt();
      await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for workflowState update

      // Step 5: Generate monster image with anime style
      await generateMonsterImage();

      // Step 6: Auto-save the piclet as caught (since scanning now auto-captures)
      await autoSavePicletAsCaught();

      workflowState.currentStep = 'complete';

      // If processing a queue, auto-advance to next image after a short delay
      if (imageQueue.length > 1) {
        setTimeout(() => advanceToNextImage(), 2000); // 2 second delay to show completion
      }
    } catch (err) {
      console.error('Workflow error:', err);

      // Check for GPU quota error
      if (err && typeof err === 'object' && 'message' in err) {
        const errorMessage = String(err.message);
        if (errorMessage.includes('exceeded your GPU quota') || errorMessage.includes('GPU quota')) {
          workflowState.error = 'GPU quota exceeded! You need to sign in with Hugging Face for free GPU time, or upgrade to Hugging Face Pro for more quota.';
        } else {
          workflowState.error = errorMessage;
        }
      } else if (err instanceof Error) {
        workflowState.error = err.message;
      } else {
        workflowState.error = 'An unknown error occurred';
      }
    } finally {
      workflowState.isProcessing = false;
    }
  }
  
  function handleAPIError(error: any): never {
    console.error('API Error:', error);
    
    // Check if it's a GPU quota error
    if (error && typeof error === 'object' && 'message' in error) {
      const errorMessage = String(error.message);
      if (errorMessage.includes('exceeded your GPU quota') || errorMessage.includes('GPU quota')) {
        throw new Error('GPU quota exceeded! You need to sign in with Hugging Face for free GPU time, or upgrade to Hugging Face Pro for more quota.');
      }
      throw new Error(errorMessage);
    }
    
    // Check if error has a different structure (like the status object from the logs)
    if (error && typeof error === 'object' && 'type' in error && error.type === 'status') {
      const statusError = error as any;
      if (statusError.message && statusError.message.includes('GPU quota')) {
        throw new Error('GPU quota exceeded! You need to sign in with Hugging Face for free GPU time, or upgrade to Hugging Face Pro for more quota.');
      }
      throw new Error(statusError.message || 'API request failed');
    }
    
    throw error;
  }
  
  async function captionImage() {
    workflowState.currentStep = 'captioning';

    if (!joyCaptionClient || !workflowState.userImage) {
      throw new Error('Caption service not available or no image provided');
    }

    try {
      // Get detailed scene description from Joy Caption
      const captionResult = await EnhancedCaptionService.generateEnhancedCaption(
        joyCaptionClient,
        workflowState.userImage
      );

      workflowState.imageCaption = captionResult.caption;
      console.log('Scene description:', captionResult.caption);

      // Skip server lookup for now - always create new piclet
      workflowState.discoveryStatus = 'new';
    } catch (error) {
      handleAPIError(error);
    }
  }
  
  async function generateConcept() {
    workflowState.currentStep = 'conceptualizing';

    // Skip if we have an existing canonical Piclet
    if (workflowState.discoveryStatus === 'existing' && workflowState.canonicalPiclet) {
      workflowState.picletConcept = workflowState.canonicalPiclet.concept;
      console.log('Using existing canonical concept');
      return;
    }

    if (!gptOssClient || !workflowState.imageCaption) {
      throw new Error('Cannot generate concept without scene description');
    }

    const conceptPrompt = `You are analyzing an image to create a Pokemon-style creature. Here's the image description:

"${workflowState.imageCaption}"

Your task:
1. Identify the PRIMARY PHYSICAL OBJECT with SPECIFICITY (e.g., "macbook" not "laptop", "eiffel tower" not "tower", "iphone" not "phone", "starbucks mug" not "mug")
2. Determine if there's a meaningful VARIATION (e.g., "silver", "pro", "night", "gaming", "vintage")
3. Assess rarity based on uniqueness
4. Create a complete Pokemon-style monster concept

Format your response EXACTLY as follows:
\`\`\`md
# Canonical Object
{Specific object name: "macbook", "eiffel tower", "iphone", "tesla", "le creuset mug", "nintendo switch"}
{NOT generic terms like: "laptop", "tower", "phone", "car", "mug", "console"}
{Include brand/model/landmark name when identifiable}

# Variation
{OPTIONAL: one distinctive attribute like "silver", "pro", "night", "gaming", OR use "canonical" if this is the standard/default version with no special variation}

# Object Rarity
{common, uncommon, rare, epic, or legendary based on object uniqueness}

# Monster Name
{Creative 8-11 letter name based on the SPECIFIC object, e.g., "Macbyte" for MacBook, "Towerfell" for Eiffel Tower}

# Primary Type
{beast, bug, aquatic, flora, mineral, space, machina, structure, culture, or cuisine}

# Physical Stats
Height: {e.g., "1.2m" or "3'5\""}
Weight: {e.g., "15kg" or "33 lbs"}

# Personality
{1-2 sentences describing personality traits}

# Monster Description
{2-3 paragraphs describing how the SPECIFIC object's features translate into monster features. Reference the actual object by name. This is the creature's bio.}

# Monster Image Prompt
{Concise visual description for anime-style image generation focusing on colors, shapes, and key features inspired by the specific object}
\`\`\`

CRITICAL RULES:
- Canonical Object MUST be SPECIFIC: "macbook" not "laptop", "big ben" not "clock tower", "coca cola" not "soda"
- If you can identify a brand, model, or proper name from the description, USE IT
- Variation should be meaningful and distinctive (material, style, color, context, or model variant)
- Monster Description must describe the CREATURE with references to the specific object's features
- Primary Type must match the object category (machina for electronics, structure for buildings, etc.)`;

    try {
      const responseText = await generateText(conceptPrompt);

      // Validate response has expected structure
      if (!responseText.includes('# Canonical Object') ||
          !responseText.includes('# Monster Name')) {
        console.error('GPT-OSS returned invalid response:', responseText);
        throw new Error('Failed to generate valid monster concept');
      }

      workflowState.picletConcept = responseText;

      // Extract and store canonical name and variation immediately for use in other steps
      // Handle both plain and bold markdown headers
      const canonicalMatch = responseText.match(/\*{0,2}#\s*Canonical Object\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#)/m);
      const variationMatch = responseText.match(/\*{0,2}#\s*Variation\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#)/m);

      // Clean up extracted values (remove curly braces and quotes that GPT-OSS sometimes adds)
      let objectName = canonicalMatch ? canonicalMatch[1].trim().toLowerCase() : 'unknown';
      objectName = objectName.replace(/^[{"]|["}]$/g, '').replace(/^.*:\s*["']|["']$/g, '').trim();

      let variationText = variationMatch ? variationMatch[1].trim() : '';
      variationText = variationText.replace(/^[{"]|["}]$/g, '').replace(/^.*:\s*["']|["']$/g, '').trim();

      workflowState.objectName = objectName;
      workflowState.objectAttributes = variationText && variationText !== 'NONE' && variationText !== 'canonical' ? [variationText.toLowerCase()] : [];

      console.log('Parsed specific object:', workflowState.objectName);
      console.log('Parsed variation:', workflowState.objectAttributes);
      
      if (!responseText || responseText.trim() === '') {
        throw new Error('Failed to generate monster concept');
      }
      
      // Parse markdown code block response
      let cleanedResponse = responseText.trim();
      
      // Check if response is wrapped in markdown code blocks
      if (cleanedResponse.includes('```')) {
        // Handle different code block formats: ```md, ```, ```markdown
        const codeBlockRegex = /```(?:md|markdown)?\s*\n([\s\S]*?)```/;
        const match = cleanedResponse.match(codeBlockRegex);
        
        if (match && match[1]) {
          cleanedResponse = match[1].trim();
          console.log('Extracted content from markdown code block');
        } else {
          // Fallback: try to extract content between any ``` blocks
          const simpleMatch = cleanedResponse.match(/```([\s\S]*?)```/);
          if (simpleMatch && simpleMatch[1]) {
            cleanedResponse = simpleMatch[1].trim();
            console.log('Extracted content from generic code block');
          }
        }
      }
      
      // Ensure the response contains expected markdown headers
      if (!cleanedResponse.includes('# Object Rarity') || !cleanedResponse.includes('# Monster Name') || !cleanedResponse.includes('# Monster Image Prompt')) {
        console.warn('Response does not contain expected markdown structure (missing Object Rarity, Monster Name, or Monster Image Prompt)');
      }
      
      workflowState.picletConcept = cleanedResponse;
      console.log('Monster concept generated:', cleanedResponse);
    } catch (error) {
      handleAPIError(error);
    }
  }
  
  async function generateImagePrompt() {
    workflowState.currentStep = 'promptCrafting';
    
    if (!gptOssClient || !workflowState.picletConcept || !workflowState.imageCaption) {
      throw new Error('Text generation service not available or no concept/caption available for prompt generation');
    }
    
    // Extract the Monster Image Prompt from the structured concept (handle both plain and bold markdown headers)
    const imagePromptMatch = workflowState.picletConcept.match(/\*{0,2}#\s*Monster Image Prompt\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#|$)/m);
    
    if (imagePromptMatch && imagePromptMatch[1]) {
      workflowState.imagePrompt = imagePromptMatch[1].trim();
      console.log('Extracted image prompt for generation:', workflowState.imagePrompt);
      return; // Skip fallback call since we have the prompt
    }
    
    // Fallback: if format parsing fails, use Qwen to extract visual description
    const imagePromptPrompt = `Based on this monster concept, extract ONLY the visual description for image generation:

MONSTER CONCEPT:
"""
${workflowState.picletConcept}
"""

Create a concise visual description (1-3 sentences, max 100 words). Focus only on colors, shapes, materials, eyes, limbs, mouth, and distinctive features. Omit all non-visual information like abilities and backstory.`;
    
    try {
      const responseText = await generateText(imagePromptPrompt);
      
      if (!responseText || responseText.trim() === '') {
        throw new Error('Failed to generate image prompt');
      }
      
      workflowState.imagePrompt = responseText.trim();
      console.log('Image prompt generated:', workflowState.imagePrompt);
    } catch (error) {
      handleAPIError(error);
    }
  }
  
  async function generateMonsterImage() {
    workflowState.currentStep = 'generating';
    
    if (!fluxClient || !workflowState.imagePrompt || !workflowState.picletStats) {
      throw new Error('Image generation service not available or no prompt/stats');
    }
    
    // The image prompt should already be generated by generateImagePrompt() in the workflow
    
    // Get tier for image quality enhancement
    const tier = workflowState.picletStats.tier || 'medium';
    const tierDescriptions = {
      low: 'simple and iconic design',
      medium: 'detailed and well-crafted design', 
      high: 'highly detailed and impressive design with special effects',
      legendary: 'highly detailed and majestic design with dramatic lighting and aura effects'
    };
    
    try {
      const output = await fluxClient.predict("/infer", [
        `${workflowState.imagePrompt}\nNow generate an Pokémon Anime image of the monster in an idle pose with a plain dark-grey background. This is a ${tier} tier monster with a ${tierDescriptions[tier as keyof typeof tierDescriptions]}. The monster should not be attacking or in motion. The full monster must be visible within the frame.`,
        0,      // seed
        true,   // randomizeSeed
        1024,   // width
        1024,   // height
        4       // steps
      ]);
      
      const [image, usedSeed] = output.data;
      let url: string | undefined;
      
      if (typeof image === "string") url = image;
      else if (image && image.url) url = image.url;
      else if (image && image.path) url = image.path;
      
      if (url) {
        // Process the image to remove background using professional AI method
        console.log('Processing image for background removal...');
        try {
          const transparentBase64 = await removeBackground(url);
          workflowState.picletImage = {
            imageUrl: url,
            imageData: transparentBase64,
            seed: usedSeed,
            prompt: workflowState.imagePrompt
          };
          console.log('Background removal completed successfully');
        } catch (processError) {
          console.error('Failed to process image for background removal:', processError);
          // Fallback to original image
          workflowState.picletImage = {
            imageUrl: url,
            seed: usedSeed,
            prompt: workflowState.imagePrompt
          };
        }
      } else {
        throw new Error('Failed to generate monster image');
      }
    } catch (error) {
      handleAPIError(error);
    }
  }
  
  async function extractSimpleStats() {
    workflowState.currentStep = 'statsGenerating';

    if (!workflowState.picletConcept) {
      throw new Error('No concept available for stats extraction');
    }

    try {
      // Extract monster name (handle both plain and bold markdown headers)
      const monsterNameMatch = workflowState.picletConcept.match(/\*{0,2}#\s*Monster Name\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#|$)/m);
      let monsterName = monsterNameMatch ? monsterNameMatch[1].trim() : 'Unknown Monster';

      // Clean and truncate name
      monsterName = monsterName.replace(/^[{"]|["}]$/g, '').trim(); // Remove curly braces and quotes
      if (monsterName.includes(',')) {
        monsterName = monsterName.split(',')[0].trim();
      }
      if (monsterName.length > 12) {
        monsterName = monsterName.substring(0, 12);
      }
      monsterName = monsterName.replace(/\*/g, ''); // Remove markdown asterisks

      // Extract rarity and convert to tier (handle both plain and bold markdown headers)
      const rarityMatch = workflowState.picletConcept.match(/\*{0,2}#\s*Object Rarity\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#)/m);
      const objectRarity = rarityMatch ? rarityMatch[1].trim().toLowerCase() : 'common';

      let tier: 'low' | 'medium' | 'high' | 'legendary' = 'medium';
      if (objectRarity.includes('common')) tier = 'low';
      else if (objectRarity.includes('uncommon')) tier = 'medium';
      else if (objectRarity.includes('rare')) tier = 'high';
      else if (objectRarity.includes('legendary') || objectRarity.includes('mythical')) tier = 'legendary';

      // Extract primary type (handle both plain and bold markdown headers)
      const primaryTypeMatch = workflowState.picletConcept.match(/\*{0,2}#\s*Primary Type\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#|$)/m);
      let primaryType: any = primaryTypeMatch ? primaryTypeMatch[1].trim().toLowerCase() : 'beast';
      primaryType = primaryType.replace(/^[{"]|["}]$/g, '').trim(); // Remove curly braces and quotes

      // Extract description (handle both plain and bold markdown headers)
      const descriptionMatch = workflowState.picletConcept.match(/\*{0,2}#\s*Monster Description\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#|$)/m);
      if (!descriptionMatch) {
        console.error('Monster description not found in concept:', workflowState.picletConcept);
        throw new Error('Failed to extract monster description from AI response');
      }
      let description = descriptionMatch[1].trim();

      // Extract physical stats (handle both plain and bold markdown headers)
      const physicalStatsMatch = workflowState.picletConcept.match(/\*{0,2}#\s*Physical Stats\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#|$)/m);
      let height: string | undefined;
      let weight: string | undefined;

      if (physicalStatsMatch) {
        const physicalStatsText = physicalStatsMatch[1];
        const heightMatch = physicalStatsText.match(/Height:\s*(.+)/i);
        const weightMatch = physicalStatsText.match(/Weight:\s*(.+)/i);

        height = heightMatch ? heightMatch[1].trim().replace(/^[{"]|["}]$/g, '').trim() : undefined;
        weight = weightMatch ? weightMatch[1].trim().replace(/^[{"]|["}]$/g, '').trim() : undefined;
      }

      // Extract personality (handle both plain and bold markdown headers)
      const personalityMatch = workflowState.picletConcept.match(/\*{0,2}#\s*Personality\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#|$)/m);
      let personality = personalityMatch ? personalityMatch[1].trim().replace(/^[{"]|["}]$/g, '').trim() : undefined;

      // Create stats with physical characteristics
      const stats: PicletStats = {
        name: monsterName,
        description: description,
        tier: tier,
        primaryType: primaryType,
        height,
        weight,
        personality
      };

      workflowState.picletStats = stats;
      console.log('Stats extracted:', stats);

    } catch (error) {
      console.error('Failed to extract stats:', error);
      handleAPIError(error);
    }
  }
  
  async function autoSavePicletAsCaught() {
    if (!workflowState.picletImage || !workflowState.imageCaption || !workflowState.picletConcept || !workflowState.imagePrompt || !workflowState.picletStats) {
      console.error('Cannot auto-save: missing required data');
      return;
    }
    
    try {
      // Create a clean copy of stats to ensure it's serializable
      const cleanStats = JSON.parse(JSON.stringify(workflowState.picletStats));
      
      const picletData = {
        name: workflowState.picletStats.name,
        imageUrl: workflowState.picletImage.imageUrl,
        imageData: workflowState.picletImage.imageData,
        imageCaption: workflowState.imageCaption,
        concept: workflowState.picletConcept,
        imagePrompt: workflowState.imagePrompt,
        stats: cleanStats,
        createdAt: new Date()
      };
      
      // Check for any non-serializable data
      console.log('Checking piclet data for serializability:');
      console.log('- name type:', typeof picletData.name);
      console.log('- imageUrl type:', typeof picletData.imageUrl);
      console.log('- imageData type:', typeof picletData.imageData, picletData.imageData ? `length: ${picletData.imageData.length}` : 'null/undefined');
      console.log('- imageCaption type:', typeof picletData.imageCaption);
      console.log('- concept type:', typeof picletData.concept);
      console.log('- imagePrompt type:', typeof picletData.imagePrompt);
      console.log('- stats:', cleanStats);
      
      // Convert to PicletInstance format and save as caught
      // Convert reactive Svelte state to plain values (removes Proxy wrapper for IndexedDB compatibility)
      const plainAttributes = workflowState.objectAttributes ? [...workflowState.objectAttributes] : [];

      const picletInstance = await generatedDataToPicletInstance(
        picletData,
        workflowState.objectName || undefined,
        plainAttributes,
        workflowState.visualDetails || undefined,
        $auth.userInfo // Pass user info for discoverer attribution
      );

      // Sync with server if available and user is authenticated
      if (picletsServerClient && $auth.session?.accessToken && workflowState.objectName) {
        try {
          console.log('Syncing piclet to server...', workflowState.objectName);

          // Search for existing canonical on server
          const searchResult = await CanonicalService.searchCanonical(
            picletsServerClient,
            workflowState.objectName,
            plainAttributes
          );

          console.log('Server search result:', searchResult?.status);

          if (searchResult?.status === 'new') {
            // Create new canonical on server
            console.log('Creating canonical piclet on server...');
            const serverResult = await CanonicalService.createCanonical(
              picletsServerClient,
              workflowState.objectName,
              picletInstance,
              $auth.session.accessToken
            );
            console.log('Server canonical creation result:', serverResult);
          } else if (searchResult?.status === 'new_variation' && searchResult.canonicalId) {
            // Create variation on server
            console.log('Creating variation on server...');
            const serverResult = await CanonicalService.createVariation(
              picletsServerClient,
              searchResult.canonicalId,
              workflowState.objectName,
              plainAttributes,
              picletInstance,
              $auth.session.accessToken
            );
            console.log('Server variation creation result:', serverResult);
          } else if (searchResult?.status === 'existing' || searchResult?.status === 'variation') {
            // Increment scan count for existing piclet
            const picletId = searchResult.piclet?.typeId || searchResult.canonicalId;
            if (picletId) {
              console.log('Incrementing scan count on server...');
              await CanonicalService.incrementScanCount(
                picletsServerClient,
                picletId,
                workflowState.objectName
              );
            }
          }
        } catch (serverError) {
          console.error('Server sync failed (continuing with local save):', serverError);
          // Don't throw - continue with local save even if server fails
        }
      }

      const picletId = await savePicletInstance(picletInstance);
      console.log('Piclet auto-saved as caught with ID:', picletId);

      // Update game state statistics
      await incrementDiscoveryCounter('totalDiscoveries');

      // Update canonical vs variation counters
      if (picletInstance.isCanonical) {
        await incrementDiscoveryCounter('uniqueDiscoveries');
      } else {
        await incrementDiscoveryCounter('variationsFound');
      }

      // Calculate and add rarity score
      const rarityPoints = calculateRarityPoints(picletInstance.scanCount);
      await addRarityScore(rarityPoints);

      console.log('Game state updated: +1 discovery, +' + rarityPoints + ' rarity points');
    } catch (err) {
      console.error('Failed to auto-save piclet:', err);
      console.error('Piclet data that failed to save:', {
        name: workflowState.picletStats?.name,
        hasImageUrl: !!workflowState.picletImage?.imageUrl,
        hasImageData: !!workflowState.picletImage?.imageData,
        hasStats: !!workflowState.picletStats
      });

      // Don't throw - we don't want to interrupt the workflow
    }
  }
  
  function reset() {
    workflowState = {
      currentStep: 'upload',
      userImage: null,
      imageCaption: null,
      picletConcept: null,
      picletStats: null,
      imagePrompt: null,
      picletImage: null,
      error: null,
      isProcessing: false,
      objectName: null,
      objectAttributes: [],
      visualDetails: null,
      discoveryStatus: null,
      canonicalPiclet: null
    };
  }
</script>

<div class="piclet-generator">
  
  
  {#if workflowState.currentStep !== 'upload'}
    <WorkflowProgress currentStep={workflowState.currentStep} error={workflowState.error} />
  {/if}
  
  {#if workflowState.currentStep === 'upload'}
    <UploadStep 
      onImageSelected={handleImageSelected}
      onImagesSelected={handleImagesSelected}
      isProcessing={workflowState.isProcessing}
      imageQueue={imageQueue}
      currentImageIndex={currentImageIndex}
    />
  {:else if workflowState.currentStep === 'complete'}
    <PicletResult workflowState={workflowState} onReset={reset} />
  {:else}
    <div class="processing-container">
      <div class="spinner"></div>
      <p class="processing-text">
        {#if workflowState.currentStep === 'captioning'}
          Analyzing your image...
        {:else if workflowState.currentStep === 'conceptualizing'}
          Creating Piclet concept...
        {:else if workflowState.currentStep === 'statsGenerating'}
          Generating piclet characteristics...
        {:else if workflowState.currentStep === 'promptCrafting'}
          Creating image prompt...
        {:else if workflowState.currentStep === 'generating'}
          Generating your Piclet...
        {/if}
      </p>
    </div>
  {/if}
</div>

<style>
  .piclet-generator {
    width: 100%;
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
  }
  
  /* Client selector styles (hidden since only HunyuanTurbos is active) */
  /*
  .client-selector {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 1rem;
    padding: 0.75rem;
    background: #f8f9fa;
    border-radius: 8px;
    border: 1px solid #dee2e6;
  }
  
  .client-selector label {
    font-weight: 500;
    color: #495057;
  }
  
  .client-selector select {
    padding: 0.25rem 0.5rem;
    border: 1px solid #ced4da;
    border-radius: 4px;
    background: white;
    color: #495057;
    font-size: 0.9rem;
  }
  */
  
  
  .processing-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 3rem 1rem;
  }
  
  .spinner {
    width: 60px;
    height: 60px;
    border: 3px solid #f3f3f3;
    border-top: 3px solid #007bff;
    border-radius: 50%;
    animation: spin 1s linear infinite;
    margin-bottom: 2rem;
  }
  
  @keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }
  
  .processing-text {
    font-size: 1.2rem;
    color: #333;
    margin-bottom: 2rem;
  }
  
</style>