Fraser commited on
Commit
d081d7a
·
1 Parent(s): 0d548ea

upload folders?!

Browse files
src/lib/components/PicletGenerator/PicletGenerator.svelte CHANGED
@@ -27,6 +27,10 @@
27
  isProcessing: false
28
  });
29
 
 
 
 
 
30
  const IMAGE_GENERATION_PROMPT = (concept: string) => `Extract ONLY the visual appearance from this monster concept and describe it in one concise sentence:
31
  "${concept}"
32
 
@@ -67,6 +71,10 @@ Focus on: colors, body shape, eyes, limbs, mouth, and key visual features. Omit
67
  return;
68
  }
69
 
 
 
 
 
70
  state.userImage = file;
71
  state.error = null;
72
 
@@ -81,6 +89,57 @@ Focus on: colors, body shape, eyes, limbs, mouth, and key visual features. Omit
81
  }
82
  }
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  async function startWorkflow() {
85
  state.isProcessing = true;
86
 
@@ -108,6 +167,11 @@ Focus on: colors, body shape, eyes, limbs, mouth, and key visual features. Omit
108
  await autoSavePiclet();
109
 
110
  state.currentStep = 'complete';
 
 
 
 
 
111
  } catch (err) {
112
  console.error('Workflow error:', err);
113
 
@@ -931,7 +995,10 @@ Write your response within \`\`\`json\`\`\``;
931
  {#if state.currentStep === 'upload'}
932
  <UploadStep
933
  onImageSelected={handleImageSelected}
 
934
  isProcessing={state.isProcessing}
 
 
935
  />
936
  {:else if state.currentStep === 'complete'}
937
  <PicletResult workflowState={state} onReset={reset} />
 
27
  isProcessing: false
28
  });
29
 
30
+ // Queue state for multi-image processing
31
+ let imageQueue: File[] = $state([]);
32
+ let currentImageIndex: number = $state(0);
33
+
34
  const IMAGE_GENERATION_PROMPT = (concept: string) => `Extract ONLY the visual appearance from this monster concept and describe it in one concise sentence:
35
  "${concept}"
36
 
 
71
  return;
72
  }
73
 
74
+ // Single image upload - clear queue and process normally
75
+ imageQueue = [];
76
+ currentImageIndex = 0;
77
+
78
  state.userImage = file;
79
  state.error = null;
80
 
 
89
  }
90
  }
91
 
92
+ async function handleImagesSelected(files: File[]) {
93
+ if (!joyCaptionClient || !fluxClient) {
94
+ state.error = "Services not connected. Please wait...";
95
+ return;
96
+ }
97
+
98
+ // Multi-image upload - set up queue and start with first image
99
+ imageQueue = files;
100
+ currentImageIndex = 0;
101
+
102
+ await processCurrentImage();
103
+ }
104
+
105
+ async function processCurrentImage() {
106
+ if (currentImageIndex >= imageQueue.length) {
107
+ // Queue completed
108
+ console.log('All images processed!');
109
+ return;
110
+ }
111
+
112
+ const currentFile = imageQueue[currentImageIndex];
113
+ state.userImage = currentFile;
114
+ state.error = null;
115
+
116
+ // Check if this is a piclet card with metadata
117
+ const picletData = await extractPicletMetadata(currentFile);
118
+ if (picletData) {
119
+ // Import existing piclet
120
+ await importPiclet(picletData);
121
+ // Auto-advance to next image after import
122
+ await advanceToNextImage();
123
+ } else {
124
+ // Generate new piclet
125
+ startWorkflow();
126
+ }
127
+ }
128
+
129
+ async function advanceToNextImage() {
130
+ currentImageIndex++;
131
+
132
+ if (currentImageIndex < imageQueue.length) {
133
+ // Process next image
134
+ setTimeout(() => processCurrentImage(), 1000); // Small delay for better UX
135
+ } else {
136
+ // Queue completed - reset to single image mode
137
+ imageQueue = [];
138
+ currentImageIndex = 0;
139
+ reset();
140
+ }
141
+ }
142
+
143
  async function startWorkflow() {
144
  state.isProcessing = true;
145
 
 
167
  await autoSavePiclet();
168
 
169
  state.currentStep = 'complete';
170
+
171
+ // If processing a queue, auto-advance to next image after a short delay
172
+ if (imageQueue.length > 1) {
173
+ setTimeout(() => advanceToNextImage(), 2000); // 2 second delay to show completion
174
+ }
175
  } catch (err) {
176
  console.error('Workflow error:', err);
177
 
 
995
  {#if state.currentStep === 'upload'}
996
  <UploadStep
997
  onImageSelected={handleImageSelected}
998
+ onImagesSelected={handleImagesSelected}
999
  isProcessing={state.isProcessing}
1000
+ imageQueue={imageQueue}
1001
+ currentImageIndex={currentImageIndex}
1002
  />
1003
  {:else if state.currentStep === 'complete'}
1004
  <PicletResult workflowState={state} onReset={reset} />
src/lib/components/PicletGenerator/UploadStep.svelte CHANGED
@@ -1,10 +1,13 @@
1
  <script lang="ts">
2
  interface Props {
3
  onImageSelected: (image: File) => void;
 
4
  isProcessing?: boolean;
 
 
5
  }
6
 
7
- let { onImageSelected, isProcessing = false }: Props = $props();
8
 
9
  let fileInput: HTMLInputElement;
10
  let dragActive = $state(false);
@@ -12,8 +15,12 @@
12
 
13
  function handleFileSelect(e: Event) {
14
  const target = e.target as HTMLInputElement;
15
- if (target.files && target.files[0]) {
16
- processFile(target.files[0]);
 
 
 
 
17
  }
18
  }
19
 
@@ -21,8 +28,12 @@
21
  e.preventDefault();
22
  dragActive = false;
23
 
24
- if (e.dataTransfer?.files && e.dataTransfer.files[0]) {
25
- processFile(e.dataTransfer.files[0]);
 
 
 
 
26
  }
27
  }
28
 
@@ -52,14 +63,66 @@
52
  onImageSelected(file);
53
  }
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  function triggerFileSelect() {
56
  fileInput.click();
57
  }
58
  </script>
59
 
60
  <div class="upload-container">
61
- <h3>Upload Your Photo</h3>
62
- <p class="subtitle">Upload a photo that will inspire your monster creation</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
  <div
65
  class="upload-area"
@@ -92,6 +155,7 @@
92
  <input
93
  type="file"
94
  accept="image/*"
 
95
  onchange={handleFileSelect}
96
  bind:this={fileInput}
97
  class="hidden-input"
@@ -192,4 +256,99 @@
192
  color: white;
193
  font-size: 1.1rem;
194
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  </style>
 
1
  <script lang="ts">
2
  interface Props {
3
  onImageSelected: (image: File) => void;
4
+ onImagesSelected: (images: File[]) => void;
5
  isProcessing?: boolean;
6
+ imageQueue?: File[];
7
+ currentImageIndex?: number;
8
  }
9
 
10
+ let { onImageSelected, onImagesSelected, isProcessing = false, imageQueue = [], currentImageIndex = 0 }: Props = $props();
11
 
12
  let fileInput: HTMLInputElement;
13
  let dragActive = $state(false);
 
15
 
16
  function handleFileSelect(e: Event) {
17
  const target = e.target as HTMLInputElement;
18
+ if (target.files) {
19
+ if (target.files.length === 1) {
20
+ processFile(target.files[0]);
21
+ } else if (target.files.length > 1) {
22
+ processMultipleFiles(Array.from(target.files));
23
+ }
24
  }
25
  }
26
 
 
28
  e.preventDefault();
29
  dragActive = false;
30
 
31
+ if (e.dataTransfer?.files) {
32
+ if (e.dataTransfer.files.length === 1) {
33
+ processFile(e.dataTransfer.files[0]);
34
+ } else if (e.dataTransfer.files.length > 1) {
35
+ processMultipleFiles(Array.from(e.dataTransfer.files));
36
+ }
37
  }
38
  }
39
 
 
63
  onImageSelected(file);
64
  }
65
 
66
+ function processMultipleFiles(files: File[]) {
67
+ // Filter to only image files
68
+ const imageFiles = files.filter(file => file.type.startsWith('image/'));
69
+
70
+ if (imageFiles.length === 0) {
71
+ alert('Please upload at least one image file');
72
+ return;
73
+ }
74
+
75
+ if (imageFiles.length !== files.length) {
76
+ alert(`${files.length - imageFiles.length} non-image files were skipped. Processing ${imageFiles.length} images.`);
77
+ }
78
+
79
+ // Create preview from first image
80
+ const reader = new FileReader();
81
+ reader.onload = (e) => {
82
+ preview = e.target?.result as string;
83
+ };
84
+ reader.readAsDataURL(imageFiles[0]);
85
+
86
+ onImagesSelected(imageFiles);
87
+ }
88
+
89
  function triggerFileSelect() {
90
  fileInput.click();
91
  }
92
  </script>
93
 
94
  <div class="upload-container">
95
+ <h3>Upload Your Photo{imageQueue.length > 1 ? 's' : ''}</h3>
96
+ <p class="subtitle">Upload {imageQueue.length > 1 ? 'photos' : 'a photo'} that will inspire your monster creation</p>
97
+
98
+ <!-- Image Queue Thumbnails -->
99
+ {#if imageQueue.length > 1}
100
+ <div class="queue-container">
101
+ <div class="queue-header">
102
+ <span class="queue-title">Processing {currentImageIndex + 1} of {imageQueue.length} images</span>
103
+ </div>
104
+ <div class="queue-thumbnails">
105
+ {#each imageQueue as image, index}
106
+ {@const isCurrentImage = index === currentImageIndex}
107
+ {@const isProcessed = index < currentImageIndex}
108
+ <div class="queue-thumbnail" class:current={isCurrentImage} class:processed={isProcessed}>
109
+ <img src={URL.createObjectURL(image)} alt="Queue thumbnail {index + 1}" />
110
+ <div class="thumbnail-overlay">
111
+ {#if isProcessed}
112
+ <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
113
+ <path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/>
114
+ </svg>
115
+ {:else if isCurrentImage}
116
+ <div class="processing-indicator"></div>
117
+ {:else}
118
+ <span class="queue-number">{index + 1}</span>
119
+ {/if}
120
+ </div>
121
+ </div>
122
+ {/each}
123
+ </div>
124
+ </div>
125
+ {/if}
126
 
127
  <div
128
  class="upload-area"
 
155
  <input
156
  type="file"
157
  accept="image/*"
158
+ multiple
159
  onchange={handleFileSelect}
160
  bind:this={fileInput}
161
  class="hidden-input"
 
256
  color: white;
257
  font-size: 1.1rem;
258
  }
259
+
260
+ /* Queue Thumbnails */
261
+ .queue-container {
262
+ margin-bottom: 2rem;
263
+ padding: 1rem;
264
+ background: #f8f9fa;
265
+ border-radius: 8px;
266
+ border: 1px solid #e9ecef;
267
+ }
268
+
269
+ .queue-header {
270
+ margin-bottom: 1rem;
271
+ text-align: center;
272
+ }
273
+
274
+ .queue-title {
275
+ font-weight: 600;
276
+ color: #495057;
277
+ font-size: 0.9rem;
278
+ }
279
+
280
+ .queue-thumbnails {
281
+ display: flex;
282
+ gap: 0.5rem;
283
+ flex-wrap: wrap;
284
+ justify-content: center;
285
+ }
286
+
287
+ .queue-thumbnail {
288
+ position: relative;
289
+ width: 80px;
290
+ height: 80px;
291
+ border-radius: 8px;
292
+ overflow: hidden;
293
+ border: 2px solid #dee2e6;
294
+ transition: all 0.3s ease;
295
+ }
296
+
297
+ .queue-thumbnail.current {
298
+ border-color: #007bff;
299
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.2);
300
+ }
301
+
302
+ .queue-thumbnail.processed {
303
+ border-color: #28a745;
304
+ opacity: 0.8;
305
+ }
306
+
307
+ .queue-thumbnail img {
308
+ width: 100%;
309
+ height: 100%;
310
+ object-fit: cover;
311
+ }
312
+
313
+ .thumbnail-overlay {
314
+ position: absolute;
315
+ top: 0;
316
+ right: 0;
317
+ width: 24px;
318
+ height: 24px;
319
+ background: rgba(0, 0, 0, 0.7);
320
+ border-radius: 0 6px 0 6px;
321
+ display: flex;
322
+ align-items: center;
323
+ justify-content: center;
324
+ color: white;
325
+ font-size: 0.75rem;
326
+ font-weight: 600;
327
+ }
328
+
329
+ .queue-thumbnail.processed .thumbnail-overlay {
330
+ background: #28a745;
331
+ }
332
+
333
+ .queue-thumbnail.current .thumbnail-overlay {
334
+ background: #007bff;
335
+ }
336
+
337
+ .processing-indicator {
338
+ width: 12px;
339
+ height: 12px;
340
+ border: 2px solid #ffffff;
341
+ border-top: 2px solid transparent;
342
+ border-radius: 50%;
343
+ animation: spin 1s linear infinite;
344
+ }
345
+
346
+ .queue-number {
347
+ font-size: 0.7rem;
348
+ }
349
+
350
+ @keyframes spin {
351
+ 0% { transform: rotate(0deg); }
352
+ 100% { transform: rotate(360deg); }
353
+ }
354
  </style>