File size: 27,032 Bytes
415a17d
55b1a24
213c234
415a17d
 
55b1a24
edc4c90
213c234
55b1a24
fd1786e
415a17d
55b1a24
415a17d
24561f7
 
55b1a24
415a17d
 
 
55b1a24
 
415a17d
55b1a24
415a17d
 
 
 
08f60f4
 
b63d7b9
 
6a8fa4a
bf70839
 
 
a46ce65
b63d7b9
9090bd9
b63d7b9
 
08f60f4
 
 
 
a46ce65
 
2531fcd
 
08f60f4
a46ce65
08f60f4
415a17d
e223b2b
415a17d
 
e223b2b
415a17d
27fe49f
213c234
 
 
 
 
 
 
 
 
55b1a24
213c234
55b1a24
 
 
213c234
 
 
 
 
 
 
 
 
 
 
 
 
 
415a17d
24561f7
415a17d
 
 
 
 
 
213c234
 
 
 
 
 
 
 
 
 
415a17d
 
 
 
 
 
08f60f4
415a17d
 
 
08f60f4
d9c705e
 
 
08f60f4
415a17d
 
55b1a24
 
600cde8
415a17d
 
 
e6edab1
 
 
 
 
 
 
 
 
 
 
 
 
 
415a17d
 
 
 
 
e6edab1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415a17d
 
 
 
 
 
 
e6edab1
381170a
 
 
 
 
 
 
 
e6edab1
 
08f60f4
e6edab1
55b1a24
08f60f4
e6edab1
 
 
415a17d
 
 
08f60f4
 
 
415a17d
55b1a24
2531fcd
415a17d
 
08f60f4
55b1a24
415a17d
08f60f4
 
 
 
24561f7
 
e6edab1
55b1a24
08f60f4
544b046
24561f7
e6edab1
08f60f4
6a18e94
08f60f4
 
 
 
 
 
 
 
 
 
 
 
 
e6edab1
415a17d
 
08f60f4
 
415a17d
 
2531fcd
55b1a24
2531fcd
 
 
 
 
 
 
e6edab1
 
2531fcd
e6edab1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edc4c90
 
d9c705e
edc4c90
55b1a24
d9c705e
 
 
 
 
edc4c90
d9c705e
edc4c90
d9c705e
55b1a24
d9c705e
 
 
 
 
e6edab1
 
 
 
 
415a17d
 
 
d9c705e
 
 
55b1a24
24561f7
d9c705e
 
b097ab2
 
08f60f4
fd1786e
55b1a24
fd1786e
 
ec2b26e
55b1a24
fd1786e
08f60f4
fd1786e
 
 
 
8e9e5e5
 
55b1a24
08f60f4
fd1786e
 
b097ab2
 
 
 
 
 
fd1786e
 
 
 
 
 
 
 
 
 
 
 
08f60f4
 
 
 
 
b097ab2
fd1786e
930c40e
 
08f60f4
 
 
 
2531fcd
08f60f4
 
 
 
 
 
 
 
 
 
2531fcd
08f60f4
 
 
b097ab2
 
 
 
381170a
08f60f4
 
 
6a8fa4a
d9c705e
 
 
 
6a18e94
4fcb3d1
 
1ac7e36
4fcb3d1
52e410c
1ac7e36
 
e3ff9e1
d9c705e
 
 
d44161a
d9c705e
 
 
 
 
 
 
13cae40
 
 
d9c705e
 
 
 
3c39754
 
 
 
 
 
13cae40
 
3c39754
fd1786e
2531fcd
3c39754
 
 
 
 
 
 
 
 
 
b097ab2
 
 
 
 
 
 
 
 
 
 
 
8e9e5e5
2531fcd
8e9e5e5
 
13cae40
381170a
13cae40
 
 
 
 
 
 
 
 
 
 
3c9abfc
c076000
 
 
 
 
3c9abfc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55b1a24
 
d9c705e
c076000
d9c705e
 
 
 
 
 
 
 
 
55b1a24
 
600cde8
 
 
 
 
544b046
55b1a24
544b046
55b1a24
 
 
 
600cde8
55b1a24
600cde8
55b1a24
 
600cde8
 
544b046
55b1a24
 
 
 
 
 
 
544b046
600cde8
55b1a24
 
 
 
600cde8
55b1a24
 
 
 
 
 
544b046
600cde8
 
 
 
415a17d
 
 
 
 
55b1a24
 
415a17d
55b1a24
415a17d
 
 
 
 
 
55b1a24
415a17d
 
 
 
 
 
 
 
 
 
 
55b1a24
415a17d
 
 
 
 
08f60f4
d9c705e
 
415a17d
 
 
 
 
 
 
 
 
55b1a24
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
<script lang="ts">
  import type { PicletGeneratorProps, PicletWorkflowState, CaptionType, CaptionLength, PicletStats } from '$lib/types';
  import type { PicletInstance } 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, monsterToPicletInstance } from '$lib/db/piclets';
  import { PicletType, TYPE_DATA } from '$lib/types/picletTypes';
  
  interface Props extends PicletGeneratorProps {}
  
  let { joyCaptionClient, zephyrClient, fluxClient }: Props = $props();
  
  let state: PicletWorkflowState = $state({
    currentStep: 'upload',
    userImage: null,
    imageCaption: null,
    picletConcept: null,
    picletStats: null,
    imagePrompt: null,
    picletImage: null,
    error: null,
    isProcessing: false
  });
  
  // Custom prompt for joy-caption-alpha-two to generate everything in one step
  const MONSTER_GENERATION_PROMPT = `Based on this image create a PokΓ©mon-style monster that transforms the object into an imaginative creature. The monster should clearly be inspired by the object's appearance but reimagined as a living monster.

Guidelines:
- Take the object's key visual elements (colors, shapes, materials) incorporating all of them into a single creature design
- Add eyes (can be glowing, mechanical, multiple, etc.) positioned where they make sense
- Include limbs (legs, arms, wings, tentacles) that grow from or replace parts of the object
- Add a mouth, beak, or feeding apparatus if appropriate
- Add creature elements like tail, fins, claws, horns, etc where fitting

Include:
- A creative name that hints at the original object
- Physical description showing how the object becomes a creature
- Personality traits based on the object's purpose

Format your response as:
\`\`\`
# Object Caption
{object description, also assess how rare the object is, the rarer the object the stronger the monster}
# Monster Name
{monster name}
## Monster Visual Description
{ensure the creature uses all the unique attributes of the object}
\`\`\``;

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

  async function handleImageSelected(file: File) {
    if (!joyCaptionClient || !fluxClient) {
      state.error = "Services not connected. Please wait...";
      return;
    }
    
    state.userImage = file;
    state.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 startWorkflow() {
    state.isProcessing = true;
    
    try {
      // Step 1: Generate monster concept with joy-caption (includes lore, visual description, and rarity)
      await captionImage();
      await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for state update
      
      // Step 2: Generate monster stats based on the concept
      await generateStats();
      await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for state update
      
      // Step 3: Generate monster image
      await generateMonsterImage();
      
      // Step 4: Auto-save the piclet
      await autoSavePiclet();
      
      state.currentStep = 'complete';
    } 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')) {
          state.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 {
          state.error = errorMessage;
        }
      } else if (err instanceof Error) {
        state.error = err.message;
      } else {
        state.error = 'An unknown error occurred';
      }
    } finally {
      state.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() {
    state.currentStep = 'captioning';
    
    if (!joyCaptionClient || !state.userImage) {
      throw new Error('Caption service not available or no image provided');
    }
    
    try {
      const output = await joyCaptionClient.predict("/stream_chat", [
        state.userImage,  // input_image
        "Descriptive",  // caption_type
        "very long",  // caption_length
        [],  // extra_options
        "",  // name_input
        MONSTER_GENERATION_PROMPT  // custom_prompt
      ]);
      
      const [prompt, caption] = output.data;
      // The caption now contains the full monster concept with lore, visual description, and rarity
      state.imageCaption = caption;
      state.picletConcept = caption; // Store as concept since it's the full monster details
      console.log('Monster concept generated:', caption);
    } catch (error) {
      handleAPIError(error);
    }
  }
  
  
  
  async function generateMonsterImage() {
    state.currentStep = 'generating';
    
    if (!fluxClient || !state.picletConcept || !state.picletStats) {
      throw new Error('Image generation service not available or no concept/stats');
    }
    
    // Extract the visual description from the joy-caption output
    const visualDescMatch = state.picletConcept.match(/## Monster Visual Description\s*\n([\s\S]*?)(?=##|$)/);
    
    if (visualDescMatch && visualDescMatch[1]) {
      state.imagePrompt = visualDescMatch[1].trim();
      console.log('Extracted visual description for image generation:', state.imagePrompt);
    } else {
      // Fallback: use text generation to extract visual description
      console.log('Using text generation for visual description extraction');
      
      const promptGenerationPrompt = IMAGE_GENERATION_PROMPT(state.picletConcept);
      const systemPrompt = "You are an expert at creating concise visual descriptions for image generation. Extract ONLY visual appearance details and describe them in ONE sentence (max 50 words). Focus on colors, shape, eyes, limbs, and distinctive features. Omit all non-visual information like abilities, personality, or backstory.";
      
      console.log('Using smart text generation for visual description extraction');
      
      try {
        const output = await zephyrClient!.predict("/chat", [
          promptGenerationPrompt, // message
          [],                     // chat_history
          systemPrompt,          // system_prompt
          1024,                   // max_new_tokens
          0.7,                   // temperature
          0.95,                  // top_p
          50,                    // top_k
          1.0                    // repetition_penalty
        ]);
        
        state.imagePrompt = output.data[0];
      } catch (error) {
        handleAPIError(error);
      }
    }
    
    if (!state.imagePrompt || state.imagePrompt.trim() === '') {
      throw new Error('Failed to extract visual description');
    }
    
    // Get tier for image quality enhancement
    const tier = state.picletStats.tier || 'medium';
    const tierDescriptions = {
      low: 'simple and basic design',
      medium: 'detailed and well-crafted design', 
      high: 'highly detailed and impressive design with special effects',
      legendary: 'extremely detailed and majestic design with dramatic lighting and aura effects'
    };
    
    try {
      const output = await fluxClient.predict("/infer", [
        `${state.imagePrompt}\nNow generate a PokΓ©mon-Anime-style image of the monster in an idle pose with a white background. This is a ${tier} tier monster with ${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);
          state.picletImage = {
            imageUrl: url,
            imageData: transparentBase64,
            seed: usedSeed,
            prompt: state.imagePrompt
          };
          console.log('Background removal completed successfully');
        } catch (processError) {
          console.error('Failed to process image for background removal:', processError);
          // Fallback to original image
          state.picletImage = {
            imageUrl: url,
            seed: usedSeed,
            prompt: state.imagePrompt
          };
        }
      } else {
        throw new Error('Failed to generate monster image');
      }
    } catch (error) {
      handleAPIError(error);
    }
  }
  
  async function generateStats() {
    state.currentStep = 'statsGenerating';
    
    if (!state.picletConcept) {
      throw new Error('No concept available for stats generation');
    }
    
    // Default tier (will be set from the generated stats)
    let tier: 'low' | 'medium' | 'high' | 'legendary' = 'medium';
    
    // Extract object description from the Object Caption section
    const objectMatch = state.picletConcept.match(/# Object Caption\s*\n([\s\S]*?)(?=^# )/m);
    const objectDescription = objectMatch ? objectMatch[1].trim() : '';
    
    // Extract monster name from the Monster Name section - similar to object extraction
    const monsterNameMatch = state.picletConcept.match(/# Monster Name\s*\n([\s\S]*?)(?=^## |$)/m);
    const monsterName = monsterNameMatch ? monsterNameMatch[1].trim() : 'Unknown Monster';
    
    // Debug logging
    console.log('Extracted object description:', objectDescription);
    console.log('Extracted monster name:', monsterName);
    
    // Create stats prompt - only ask for battle-related stats
    const statsPrompt = `Based on this monster concept, generate a JSON object with battle stats and abilities:
"${state.picletConcept}"

The original object is described as: "${objectDescription}"

First, assess the rarity of the original object based on real-world availability and value:
β€’ COMMON: Everyday items everyone has (stationery, grass, rocks, basic furniture, common tools)
β€’ UNCOMMON: Items that cost money but are widely available (electronics, appliances, vehicles, branded items)  
β€’ RARE: Expensive or specialized items (luxury goods, professional equipment, gold jewelry, antiques)
β€’ LEGENDARY: Priceless or one-of-a-kind items (crown jewels, world wonders, famous artifacts, masterpiece art)

Next, determine the monster's type based on its concept and appearance. Choose the most appropriate type from these options:
β€’ BEAST: Vertebrate wildlife β€” mammals, birds, reptiles. Raw physicality, instincts, region-based variants.
β€’ BUG: Arthropods β€” butterflies, beetles, mantises. Agile swarms, precision strikes, metamorph evolutions.
β€’ AQUATIC: Life that swims, dives, sloshes, seeps β€” fish, octopus, sentient puddles. Tides, mist, pressure.
β€’ FLORA: Plants and fungi β€” blooming or decaying. Growth, spores, vines, seasonal shifts.
β€’ MINERAL: Stones, crystals, metals β€” earth's depths. Durability, reflective armor, seismic shocks.
β€’ SPACE: Stars, moon, cosmic objects β€” not of this world. Celestial energy and cosmic forces.
β€’ MACHINA: Engineered devices β€” gadgets to machinery. Gears, circuits, drones, power surges.
β€’ STRUCTURE: Buildings, bridges, monuments β€” architectural titans. Fortification, terrain shaping.
β€’ CULTURE: Art, fashion, toys, symbols β€” creative expressions. Buffs, debuffs, illusion, stories.
β€’ CUISINE: Dishes, drinks, culinary art β€” flavors and aromas. Temperature, restorative, spicy offense.

The output should be formatted as a JSON instance that conforms to the JSON schema below.

\`\`\`json
{
  "properties": {
    "rarity": {"type": "string", "enum": ["common", "uncommon", "rare", "legendary"], "description": "Rarity of the original object based on real-world availability and value"},
    "picletType": {"type": "string", "enum": ["beast", "bug", "aquatic", "flora", "mineral", "space", "machina", "structure", "culture", "cuisine"], "description": "The type that best matches this monster's concept, appearance, and nature"},
    "height": {"type": "number", "minimum": 0.1, "maximum": 50.0, "description": "Height of the piclet in meters (e.g., 1.2, 0.5, 10.0)"},
    "weight": {"type": "number", "minimum": 0.1, "maximum": 10000.0, "description": "Weight of the piclet in kilograms (e.g., 25.4, 150.0, 0.8)"},
    "HP": {"type": "integer", "minimum": 0, "maximum": 100, "description": "Health/vitality stat (0=fragile, 100=incredibly tanky)"},
    "defence": {"type": "integer", "minimum": 0, "maximum": 100, "description": "Defensive/armor stat (0=paper thin, 100=impenetrable fortress)"},
    "attack": {"type": "integer", "minimum": 0, "maximum": 100, "description": "Physical attack power (0=harmless, 100=devastating force)"},
    "speed": {"type": "integer", "minimum": 0, "maximum": 100, "description": "Movement and reaction speed (0=immobile, 100=lightning fast)"},
    "monsterLore": {"type": "string", "description": "Write a detailed background story for this monster including its personality, habitat, behavior, and lore (2-3 sentences)"},
    "specialPassiveTraitDescription": {"type": "string", "description": "Describe a passive ability that gives this monster a unique advantage in battle"},
    "attackActionName": {"type": "string", "description": "Name of the monster's primary damage-dealing attack (e.g., 'Flame Burst', 'Toxic Bite')"},
    "attackActionDescription": {"type": "string", "description": "Describe how this attack damages the opponent and any special effects"},
    "buffActionName": {"type": "string", "description": "Name of the monster's self-enhancement ability (e.g., 'Iron Defense', 'Speed Boost')"},
    "buffActionDescription": {"type": "string", "description": "Describe which stats are boosted and how this improves the monster's battle performance"},
    "debuffActionName": {"type": "string", "description": "Name of the monster's enemy-weakening ability (e.g., 'Intimidate', 'Slow Poison')"},
    "debuffActionDescription": {"type": "string", "description": "Describe which enemy stats are lowered and how this weakens the opponent"},
    "specialActionName": {"type": "string", "description": "Name of the monster's ultimate move (one use per battle)"},
    "specialActionDescription": {"type": "string", "description": "Describe this powerful finishing move and its dramatic effects in battle"}
  },
  "required": ["rarity", "picletType", "height", "weight", "HP", "defence", "attack", "speed", "monsterLore", "specialPassiveTraitDescription", "attackActionName", "attackActionDescription", "buffActionName", "buffActionDescription", "debuffActionName", "debuffActionDescription", "specialActionName", "specialActionDescription"]
}
\`\`\`

Base the HP, defence, attack, and speed stats on the rarity level:
- common: stats should be 10-40
- uncommon: stats should be 30-60
- rare: stats should be 50-80
- legendary: stats should be 70-100

Write your response within \`\`\`json\`\`\``;
    
    const systemPrompt = "You are a game designer specializing in monster stats and abilities. You must ONLY output valid JSON that matches the provided schema exactly. Do not include any text before or after the JSON. Do not include null values in your JSON response. Your entire response should be wrapped in a ```json``` code block.";
    
    console.log('Generating monster stats from concept');
    
    try {
      const output = await zephyrClient!.predict("/chat", [
        statsPrompt,          // message
        [],                   // chat_history
        systemPrompt,         // system_prompt
        2048,                 // max_new_tokens
        0.3,                  // temperature
        0.95,                 // top_p
        50,                   // top_k
        1.0                   // repetition_penalty
      ]);
      
      console.log('Stats output:', output);
      let jsonString = output.data[0];
      
      // Extract JSON from the response (remove markdown if present)
      let cleanJson = jsonString;
      if (jsonString.includes('```')) {
        const matches = jsonString.match(/```(?:json)?\s*([\s\S]*?)```/);
        if (matches) {
          cleanJson = matches[1];
        } else {
          // If no closing ```, just remove the opening ```json
          cleanJson = jsonString.replace(/^```(?:json)?\s*/, '').replace(/```\s*$/, '');
        }
      }
      
      try {
        // Remove any trailing text after the JSON object
        const jsonMatch = cleanJson.match(/^\s*\{[\s\S]*?\}\s*/);
        if (jsonMatch) {
          cleanJson = jsonMatch[0];
        }
        
        const parsedStats = JSON.parse(cleanJson.trim());
        
        // Remove any extra fields not in our schema
        const allowedFields = ['rarity', 'picletType', 'height', 'weight', 'HP', 'defence', 'attack', 'speed',
          'monsterLore', 'specialPassiveTraitDescription', 'attackActionName', 'attackActionDescription',
          'buffActionName', 'buffActionDescription', 'debuffActionName', 'debuffActionDescription',
          'specialActionName', 'specialActionDescription', 'boostActionName', 'boostActionDescription',
          'disparageActionName', 'disparageActionDescription'];
        
        for (const key in parsedStats) {
          if (!allowedFields.includes(key)) {
            delete parsedStats[key];
          }
        }
        
        // Map rarity to tier
        if (parsedStats.rarity) {
          const tierMap: { [key: string]: 'low' | 'medium' | 'high' | 'legendary' } = {
            'common': 'low',
            'uncommon': 'medium',
            'rare': 'high',
            'legendary': 'legendary'
          };
          tier = tierMap[parsedStats.rarity.toLowerCase()] || 'medium';
        }
        
        // Add the name, description, and tier that we extracted/mapped
        parsedStats.name = monsterName;
        parsedStats.description = parsedStats.monsterLore || 'A mysterious creature with unknown origins.';
        parsedStats.tier = tier;
        
        // Ensure numeric fields are actually numbers
        const numericFields = ['HP', 'defence', 'attack', 'speed'];
        
        for (const field of numericFields) {
          if (parsedStats[field] !== undefined) {
            // Convert string numbers to actual numbers
            parsedStats[field] = parseInt(parsedStats[field]);
            
            // Clamp to 0-100 range
            parsedStats[field] = Math.max(0, Math.min(100, parsedStats[field]));
          }
        }
        
        // Map field names from schema to interface
        if (parsedStats.specialPassiveTraitDescription) {
          parsedStats.specialPassiveTrait = parsedStats.specialPassiveTraitDescription;
          delete parsedStats.specialPassiveTraitDescription;
        }
        
        // Handle potential old field names from LLM
        if (parsedStats.boostActionName) {
          parsedStats.buffActionName = parsedStats.boostActionName;
          delete parsedStats.boostActionName;
        }
        if (parsedStats.boostActionDescription) {
          parsedStats.buffActionDescription = parsedStats.boostActionDescription;
          delete parsedStats.boostActionDescription;
        }
        if (parsedStats.disparageActionName) {
          parsedStats.debuffActionName = parsedStats.disparageActionName;
          delete parsedStats.disparageActionName;
        }
        if (parsedStats.disparageActionDescription) {
          parsedStats.debuffActionDescription = parsedStats.disparageActionDescription;
          delete parsedStats.disparageActionDescription;
        }
        
        const stats: PicletStats = parsedStats;
        state.picletStats = stats;
        console.log('Monster stats generated:', stats);
        console.log('Monster stats JSON:', JSON.stringify(stats, null, 2));
      } catch (parseError) {
        console.error('Failed to parse JSON:', parseError, 'Raw output:', cleanJson);
        throw new Error('Failed to parse monster stats JSON');
      }
    } catch (error) {
      handleAPIError(error);
    }
  }
  
  async function autoSavePiclet() {
    if (!state.picletImage || !state.imageCaption || !state.picletConcept || !state.imagePrompt || !state.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(state.picletStats));
      
      const picletData = {
        name: state.picletStats.name,
        imageUrl: state.picletImage.imageUrl,
        imageData: state.picletImage.imageData,
        imageCaption: state.imageCaption,
        concept: state.picletConcept,
        imagePrompt: state.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
      const picletInstance = await monsterToPicletInstance(picletData);
      const id = await savePicletInstance(picletInstance);
      console.log('Piclet auto-saved with ID:', id);
    } catch (err) {
      console.error('Failed to auto-save piclet:', err);
      console.error('Piclet data that failed to save:', {
        name: state.picletStats?.name,
        hasImageUrl: !!state.picletImage?.imageUrl,
        hasImageData: !!state.picletImage?.imageData,
        hasStats: !!state.picletStats
      });
      // Don't throw - we don't want to interrupt the workflow
    }
  }
  
  function reset() {
    state = {
      currentStep: 'upload',
      userImage: null,
      imageCaption: null,
      picletConcept: null,
      picletStats: null,
      imagePrompt: null,
      picletImage: null,
      error: null,
      isProcessing: false
    };
  }
</script>

<div class="piclet-generator">
  
  {#if state.currentStep !== 'upload'}
    <WorkflowProgress currentStep={state.currentStep} error={state.error} />
  {/if}
  
  {#if state.currentStep === 'upload'}
    <UploadStep 
      onImageSelected={handleImageSelected}
      isProcessing={state.isProcessing}
    />
  {:else if state.currentStep === 'complete'}
    <PicletResult workflowState={state} onReset={reset} />
  {:else}
    <div class="processing-container">
      <div class="spinner"></div>
      <p class="processing-text">
        {#if state.currentStep === 'captioning'}
          Creating monster concept from your image...
        {:else if state.currentStep === 'statsGenerating'}
          Generating battle stats...
        {:else if state.currentStep === 'generating'}
          Generating your monster...
        {/if}
      </p>
    </div>
  {/if}
</div>

<style>
  .piclet-generator {
    width: 100%;
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
  }
  
  
  .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>