Fraser commited on
Commit
415a17d
·
1 Parent(s): fa49a30
src/App.svelte CHANGED
@@ -3,9 +3,7 @@
3
  import { authStore } from './lib/stores/auth';
4
  import SignInButton from './lib/components/Auth/SignInButton.svelte';
5
  import AuthBanner from './lib/components/Auth/AuthBanner.svelte';
6
- import FluxGenerator from './lib/components/ImageGeneration/FluxGenerator.svelte';
7
- import JoyCaption from './lib/components/ImageCaption/JoyCaption.svelte';
8
- import RWKVGenerator from './lib/components/TextGeneration/RWKVGenerator.svelte';
9
  import type { HuggingFaceLibs, GradioLibs, GradioClient, FluxGenerationResult } from './lib/types';
10
 
11
  // These will be loaded from window after HF libs are loaded
@@ -17,8 +15,6 @@
17
  let joyCaptionClient: GradioClient | null = $state(null);
18
  let rwkvClient: GradioClient | null = $state(null);
19
 
20
- // Current generated image for passing to caption component
21
- let currentImageBlob: Blob | null = $state(null);
22
 
23
  // Auth state from store
24
  const auth = $derived(authStore);
@@ -96,19 +92,13 @@
96
  authStore.setBannerMessage(`❌ Failed to connect: ${err}`);
97
  }
98
  }
99
-
100
- function handleImageGenerated(result: FluxGenerationResult, imageBlob: Blob) {
101
- currentImageBlob = imageBlob;
102
- }
103
  </script>
104
 
105
  <div class="app">
106
  <div class="card">
107
- <h1>FLUX-1 Schnell + Joy Caption + RWKV Space</h1>
108
  <p>
109
- This Svelte-powered Space demonstrates how to call remote Gradio Spaces while
110
- letting <strong>each visitor's own Hugging Face subscription</strong> cover
111
- the compute costs. Generate images with FLUX-1, caption them with Joy Caption, and generate text with RWKV.
112
  </p>
113
 
114
  {#if $auth.showSignIn}
@@ -121,25 +111,21 @@
121
  />
122
 
123
  {#if $auth.userInfo}
124
- <p>Hello, {$auth.userInfo.name || $auth.userInfo.preferred_username}!</p>
125
- {/if}
126
-
127
- {#if fluxClient}
128
- <FluxGenerator
129
- client={fluxClient}
130
- onImageGenerated={handleImageGenerated}
131
- />
132
  {/if}
133
 
134
- {#if joyCaptionClient}
135
- <JoyCaption
136
- client={joyCaptionClient}
137
- currentImage={currentImageBlob}
 
 
138
  />
139
- {/if}
140
-
141
- {#if rwkvClient}
142
- <RWKVGenerator client={rwkvClient} />
 
143
  {/if}
144
 
145
  <hr />
@@ -191,4 +177,31 @@
191
  .footer a:hover {
192
  text-decoration: underline;
193
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  </style>
 
3
  import { authStore } from './lib/stores/auth';
4
  import SignInButton from './lib/components/Auth/SignInButton.svelte';
5
  import AuthBanner from './lib/components/Auth/AuthBanner.svelte';
6
+ import MonsterGenerator from './lib/components/MonsterGenerator/MonsterGenerator.svelte';
 
 
7
  import type { HuggingFaceLibs, GradioLibs, GradioClient, FluxGenerationResult } from './lib/types';
8
 
9
  // These will be loaded from window after HF libs are loaded
 
15
  let joyCaptionClient: GradioClient | null = $state(null);
16
  let rwkvClient: GradioClient | null = $state(null);
17
 
 
 
18
 
19
  // Auth state from store
20
  const auth = $derived(authStore);
 
92
  authStore.setBannerMessage(`❌ Failed to connect: ${err}`);
93
  }
94
  }
 
 
 
 
95
  </script>
96
 
97
  <div class="app">
98
  <div class="card">
99
+ <h1>👾 Monster Generator</h1>
100
  <p>
101
+ Transform your photos into unique monster creations using AI
 
 
102
  </p>
103
 
104
  {#if $auth.showSignIn}
 
111
  />
112
 
113
  {#if $auth.userInfo}
114
+ <p class="user-greeting">Hello, {$auth.userInfo.name || $auth.userInfo.preferred_username}!</p>
 
 
 
 
 
 
 
115
  {/if}
116
 
117
+ <!-- Monster Generator Content -->
118
+ {#if fluxClient && joyCaptionClient && rwkvClient}
119
+ <MonsterGenerator
120
+ {fluxClient}
121
+ {joyCaptionClient}
122
+ {rwkvClient}
123
  />
124
+ {:else}
125
+ <div class="loading-message">
126
+ <div class="spinner"></div>
127
+ <p>Connecting to AI services...</p>
128
+ </div>
129
  {/if}
130
 
131
  <hr />
 
177
  .footer a:hover {
178
  text-decoration: underline;
179
  }
180
+
181
+ .user-greeting {
182
+ text-align: center;
183
+ color: #666;
184
+ margin-bottom: 2rem;
185
+ }
186
+
187
+ .loading-message {
188
+ text-align: center;
189
+ padding: 3rem;
190
+ color: #666;
191
+ }
192
+
193
+ .spinner {
194
+ width: 60px;
195
+ height: 60px;
196
+ border: 3px solid #f3f3f3;
197
+ border-top: 3px solid #007bff;
198
+ border-radius: 50%;
199
+ animation: spin 1s linear infinite;
200
+ margin: 0 auto 2rem;
201
+ }
202
+
203
+ @keyframes spin {
204
+ 0% { transform: rotate(0deg); }
205
+ 100% { transform: rotate(360deg); }
206
+ }
207
  </style>
src/lib/components/MonsterGenerator/MonsterGenerator.svelte ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { MonsterGeneratorProps, MonsterWorkflowState, CaptionType, CaptionLength } from '$lib/types';
3
+ import UploadStep from './UploadStep.svelte';
4
+ import WorkflowProgress from './WorkflowProgress.svelte';
5
+ import MonsterResult from './MonsterResult.svelte';
6
+
7
+ interface Props extends MonsterGeneratorProps {}
8
+
9
+ let { joyCaptionClient, rwkvClient, fluxClient }: Props = $props();
10
+
11
+ let state: MonsterWorkflowState = $state({
12
+ currentStep: 'upload',
13
+ userImage: null,
14
+ imageCaption: null,
15
+ monsterConcept: null,
16
+ imagePrompt: null,
17
+ monsterImage: null,
18
+ error: null,
19
+ isProcessing: false
20
+ });
21
+
22
+ // Prompt templates
23
+ const MONSTER_CONCEPT_PROMPT = (caption: string) => `Based on this description of a person/photo: "${caption}"
24
+
25
+ Create a unique monster concept inspired by the key visual elements. Include:
26
+ - Physical appearance and distinguishing features
27
+ - Special abilities or powers
28
+ - Personality traits
29
+ - Habitat or origin
30
+
31
+ Be creative and detailed. The monster should be fantastical but somehow connected to the original image's essence.`;
32
+
33
+ const IMAGE_GENERATION_PROMPT = (concept: string) => `Convert this monster concept into a detailed image generation prompt:
34
+ "${concept}"
35
+
36
+ Create a vivid, artistic prompt focusing on visual details, art style, lighting, and atmosphere. Format it as a single paragraph optimized for image generation.`;
37
+
38
+ async function handleImageSelected(file: File) {
39
+ if (!joyCaptionClient || !rwkvClient || !fluxClient) {
40
+ state.error = "Services not connected. Please wait...";
41
+ return;
42
+ }
43
+
44
+ state.userImage = file;
45
+ state.error = null;
46
+ startWorkflow();
47
+ }
48
+
49
+ async function startWorkflow() {
50
+ state.isProcessing = true;
51
+
52
+ try {
53
+ // Step 1: Caption the image
54
+ await captionImage();
55
+ await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for state update
56
+
57
+ // Step 2: Generate monster concept
58
+ await generateMonsterConcept();
59
+ await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for state update
60
+
61
+ // Step 3: Generate image prompt
62
+ await generateImagePrompt();
63
+ await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for state update
64
+
65
+ // Step 4: Generate monster image
66
+ await generateMonsterImage();
67
+
68
+ state.currentStep = 'complete';
69
+ } catch (err) {
70
+ console.error('Workflow error:', err);
71
+ state.error = err instanceof Error ? err.message : 'An unknown error occurred';
72
+ } finally {
73
+ state.isProcessing = false;
74
+ }
75
+ }
76
+
77
+ async function captionImage() {
78
+ state.currentStep = 'captioning';
79
+
80
+ if (!joyCaptionClient || !state.userImage) {
81
+ throw new Error('Caption service not available or no image provided');
82
+ }
83
+
84
+ const output = await joyCaptionClient.predict("/stream_chat", [
85
+ state.userImage,
86
+ "Descriptive" as CaptionType,
87
+ "very long" as CaptionLength,
88
+ [], // extra_options
89
+ "", // name_input
90
+ "" // custom_prompt
91
+ ]);
92
+
93
+ const [prompt, caption] = output.data;
94
+ state.imageCaption = caption;
95
+ console.log('Caption generated:', caption);
96
+ }
97
+
98
+ async function generateMonsterConcept() {
99
+ state.currentStep = 'conceptualizing';
100
+
101
+ if (!rwkvClient || !state.imageCaption) {
102
+ throw new Error('Text generation service not available or no caption');
103
+ }
104
+
105
+ const conceptPrompt = MONSTER_CONCEPT_PROMPT(state.imageCaption);
106
+
107
+ console.log('Generating monster concept with prompt:', conceptPrompt);
108
+
109
+ const output = await rwkvClient.predict(0, [
110
+ conceptPrompt,
111
+ 300, // maxTokens
112
+ 1.2, // temperature (more creative)
113
+ 0.8, // topP
114
+ 0.1, // presencePenalty
115
+ 0.1 // countPenalty
116
+ ]);
117
+
118
+ console.log('RWKV output:', output);
119
+ state.monsterConcept = output.data[0];
120
+ console.log('Monster concept generated:', state.monsterConcept);
121
+
122
+ if (!state.monsterConcept || state.monsterConcept.trim() === '') {
123
+ throw new Error('Failed to generate monster concept - received empty response');
124
+ }
125
+ }
126
+
127
+ async function generateImagePrompt() {
128
+ state.currentStep = 'promptCrafting';
129
+
130
+ if (!rwkvClient || !state.monsterConcept) {
131
+ throw new Error('Text generation service not available or no concept');
132
+ }
133
+
134
+ const promptGenerationPrompt = IMAGE_GENERATION_PROMPT(state.monsterConcept);
135
+
136
+ console.log('Generating image prompt from concept');
137
+
138
+ const output = await rwkvClient.predict(0, [
139
+ promptGenerationPrompt,
140
+ 150, // maxTokens
141
+ 0.9, // temperature
142
+ 0.7, // topP
143
+ 0.1, // presencePenalty
144
+ 0.1 // countPenalty
145
+ ]);
146
+
147
+ console.log('Image prompt output:', output);
148
+ state.imagePrompt = output.data[0];
149
+ console.log('Image prompt generated:', state.imagePrompt);
150
+
151
+ if (!state.imagePrompt || state.imagePrompt.trim() === '') {
152
+ throw new Error('Failed to generate image prompt - received empty response');
153
+ }
154
+ }
155
+
156
+ async function generateMonsterImage() {
157
+ state.currentStep = 'generating';
158
+
159
+ if (!fluxClient || !state.imagePrompt) {
160
+ throw new Error('Image generation service not available or no prompt');
161
+ }
162
+
163
+ const output = await fluxClient.predict("/infer", [
164
+ state.imagePrompt,
165
+ 0, // seed
166
+ true, // randomizeSeed
167
+ 1024, // width
168
+ 1024, // height
169
+ 4 // steps
170
+ ]);
171
+
172
+ const [image, usedSeed] = output.data;
173
+ let url: string | undefined;
174
+
175
+ if (typeof image === "string") url = image;
176
+ else if (image && image.url) url = image.url;
177
+ else if (image && image.path) url = image.path;
178
+
179
+ if (url) {
180
+ state.monsterImage = {
181
+ imageUrl: url,
182
+ seed: usedSeed,
183
+ prompt: state.imagePrompt
184
+ };
185
+ } else {
186
+ throw new Error('Failed to generate monster image');
187
+ }
188
+ }
189
+
190
+ function reset() {
191
+ state = {
192
+ currentStep: 'upload',
193
+ userImage: null,
194
+ imageCaption: null,
195
+ monsterConcept: null,
196
+ imagePrompt: null,
197
+ monsterImage: null,
198
+ error: null,
199
+ isProcessing: false
200
+ };
201
+ }
202
+ </script>
203
+
204
+ <div class="monster-generator">
205
+
206
+ {#if state.currentStep !== 'upload'}
207
+ <WorkflowProgress currentStep={state.currentStep} error={state.error} />
208
+ {/if}
209
+
210
+ {#if state.currentStep === 'upload'}
211
+ <UploadStep
212
+ onImageSelected={handleImageSelected}
213
+ isProcessing={state.isProcessing}
214
+ />
215
+ {:else if state.currentStep === 'complete'}
216
+ <MonsterResult state={state} onReset={reset} />
217
+ {:else}
218
+ <div class="processing-container">
219
+ <div class="spinner"></div>
220
+ <p class="processing-text">
221
+ {#if state.currentStep === 'captioning'}
222
+ Analyzing your image...
223
+ {:else if state.currentStep === 'conceptualizing'}
224
+ Creating monster concept...
225
+ {:else if state.currentStep === 'promptCrafting'}
226
+ Crafting generation prompt...
227
+ {:else if state.currentStep === 'generating'}
228
+ Generating your monster...
229
+ {/if}
230
+ </p>
231
+
232
+ {#if state.imageCaption && state.currentStep !== 'captioning'}
233
+ <div class="intermediate-result">
234
+ <h4>Image Description:</h4>
235
+ <p>{state.imageCaption}</p>
236
+ </div>
237
+ {/if}
238
+
239
+ {#if state.monsterConcept && state.currentStep !== 'captioning' && state.currentStep !== 'conceptualizing'}
240
+ <div class="intermediate-result">
241
+ <h4>Monster Concept:</h4>
242
+ <p>{state.monsterConcept}</p>
243
+ </div>
244
+ {/if}
245
+
246
+ {#if state.imagePrompt && state.currentStep === 'generating'}
247
+ <div class="intermediate-result">
248
+ <h4>Generation Prompt:</h4>
249
+ <p>{state.imagePrompt}</p>
250
+ </div>
251
+ {/if}
252
+ </div>
253
+ {/if}
254
+ </div>
255
+
256
+ <style>
257
+ .monster-generator {
258
+ width: 100%;
259
+ max-width: 1200px;
260
+ margin: 0 auto;
261
+ padding: 2rem;
262
+ }
263
+
264
+
265
+ .processing-container {
266
+ display: flex;
267
+ flex-direction: column;
268
+ align-items: center;
269
+ padding: 3rem 1rem;
270
+ }
271
+
272
+ .spinner {
273
+ width: 60px;
274
+ height: 60px;
275
+ border: 3px solid #f3f3f3;
276
+ border-top: 3px solid #007bff;
277
+ border-radius: 50%;
278
+ animation: spin 1s linear infinite;
279
+ margin-bottom: 2rem;
280
+ }
281
+
282
+ @keyframes spin {
283
+ 0% { transform: rotate(0deg); }
284
+ 100% { transform: rotate(360deg); }
285
+ }
286
+
287
+ .processing-text {
288
+ font-size: 1.2rem;
289
+ color: #333;
290
+ margin-bottom: 2rem;
291
+ }
292
+
293
+ .intermediate-result {
294
+ width: 100%;
295
+ max-width: 700px;
296
+ background: #f8f9fa;
297
+ padding: 1.5rem;
298
+ border-radius: 8px;
299
+ margin-bottom: 1.5rem;
300
+ text-align: left;
301
+ }
302
+
303
+ .intermediate-result h4 {
304
+ margin: 0 0 0.5rem 0;
305
+ color: #495057;
306
+ }
307
+
308
+ .intermediate-result p {
309
+ margin: 0;
310
+ line-height: 1.6;
311
+ color: #333;
312
+ }
313
+ </style>
src/lib/components/MonsterGenerator/MonsterResult.svelte ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { MonsterWorkflowState } from '$lib/types';
3
+
4
+ interface Props {
5
+ state: MonsterWorkflowState;
6
+ onReset: () => void;
7
+ }
8
+
9
+ let { state, onReset }: Props = $props();
10
+
11
+ function downloadImage() {
12
+ if (!state.monsterImage?.imageUrl) return;
13
+
14
+ const link = document.createElement('a');
15
+ link.href = state.monsterImage.imageUrl;
16
+ link.download = `monster-${Date.now()}.png`;
17
+ link.click();
18
+ }
19
+
20
+ function copyPrompt() {
21
+ if (!state.imagePrompt) return;
22
+ navigator.clipboard.writeText(state.imagePrompt);
23
+ alert('Prompt copied to clipboard!');
24
+ }
25
+ </script>
26
+
27
+ <div class="result-container">
28
+ <h3>Your Monster Has Been Created!</h3>
29
+
30
+ {#if state.monsterImage}
31
+ <div class="monster-image-container">
32
+ <img
33
+ src={state.monsterImage.imageUrl}
34
+ alt="Generated Monster"
35
+ class="monster-image"
36
+ />
37
+ </div>
38
+ {/if}
39
+
40
+ <div class="results-grid">
41
+ <div class="result-section">
42
+ <h4>Original Description</h4>
43
+ <div class="result-content">
44
+ <p>{state.imageCaption || 'No caption available'}</p>
45
+ </div>
46
+ </div>
47
+
48
+ <div class="result-section">
49
+ <h4>Monster Concept</h4>
50
+ <div class="result-content">
51
+ <p>{state.monsterConcept || 'No concept available'}</p>
52
+ </div>
53
+ </div>
54
+
55
+ <div class="result-section">
56
+ <h4>Generation Prompt</h4>
57
+ <div class="result-content">
58
+ <p>{state.imagePrompt || 'No prompt available'}</p>
59
+ {#if state.imagePrompt}
60
+ <button class="copy-button" onclick={copyPrompt}>
61
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
62
+ <path d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2zm2 0v8h8V2H6zM2 6a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-2h-2v2H2V8h2V6H2z"/>
63
+ </svg>
64
+ Copy
65
+ </button>
66
+ {/if}
67
+ </div>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="action-buttons">
72
+ <button class="action-button download" onclick={downloadImage}>
73
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
74
+ <path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/>
75
+ </svg>
76
+ Download Monster
77
+ </button>
78
+ <button class="action-button reset" onclick={onReset}>
79
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
80
+ <path d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.058 7.293a1 1 0 01-1.414 1.414l-2.35-2.35A1 1 0 011 5.648V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.943 13H13a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"/>
81
+ </svg>
82
+ Create Another Monster
83
+ </button>
84
+ </div>
85
+ </div>
86
+
87
+ <style>
88
+ .result-container {
89
+ max-width: 900px;
90
+ margin: 0 auto;
91
+ padding: 2rem;
92
+ }
93
+
94
+ h3 {
95
+ text-align: center;
96
+ color: #333;
97
+ margin-bottom: 2rem;
98
+ }
99
+
100
+ .monster-image-container {
101
+ text-align: center;
102
+ margin-bottom: 3rem;
103
+ }
104
+
105
+ .monster-image {
106
+ max-width: 100%;
107
+ max-height: 600px;
108
+ border-radius: 12px;
109
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
110
+ }
111
+
112
+ .results-grid {
113
+ display: grid;
114
+ gap: 1.5rem;
115
+ margin-bottom: 3rem;
116
+ }
117
+
118
+ .result-section {
119
+ background: #f8f9fa;
120
+ padding: 1.5rem;
121
+ border-radius: 8px;
122
+ border: 1px solid #e9ecef;
123
+ }
124
+
125
+ .result-section h4 {
126
+ margin: 0 0 1rem 0;
127
+ color: #495057;
128
+ font-size: 1.1rem;
129
+ }
130
+
131
+ .result-content {
132
+ position: relative;
133
+ }
134
+
135
+ .result-content p {
136
+ margin: 0;
137
+ line-height: 1.6;
138
+ color: #333;
139
+ }
140
+
141
+ .copy-button {
142
+ position: absolute;
143
+ top: -8px;
144
+ right: -8px;
145
+ background: #007bff;
146
+ color: white;
147
+ border: none;
148
+ padding: 0.4rem 0.8rem;
149
+ border-radius: 4px;
150
+ font-size: 0.85rem;
151
+ cursor: pointer;
152
+ display: flex;
153
+ align-items: center;
154
+ gap: 0.3rem;
155
+ transition: background 0.2s;
156
+ }
157
+
158
+ .copy-button:hover {
159
+ background: #0056b3;
160
+ }
161
+
162
+ .action-buttons {
163
+ display: flex;
164
+ justify-content: center;
165
+ gap: 1rem;
166
+ flex-wrap: wrap;
167
+ }
168
+
169
+ .action-button {
170
+ display: flex;
171
+ align-items: center;
172
+ gap: 0.5rem;
173
+ padding: 0.8rem 1.5rem;
174
+ border: none;
175
+ border-radius: 6px;
176
+ font-size: 1rem;
177
+ font-weight: 500;
178
+ cursor: pointer;
179
+ transition: all 0.2s;
180
+ }
181
+
182
+ .action-button.download {
183
+ background: #28a745;
184
+ color: white;
185
+ }
186
+
187
+ .action-button.download:hover {
188
+ background: #218838;
189
+ }
190
+
191
+ .action-button.reset {
192
+ background: #6c757d;
193
+ color: white;
194
+ }
195
+
196
+ .action-button.reset:hover {
197
+ background: #5a6268;
198
+ }
199
+
200
+ @media (max-width: 768px) {
201
+ .result-container {
202
+ padding: 1rem;
203
+ }
204
+
205
+ .action-buttons {
206
+ flex-direction: column;
207
+ }
208
+
209
+ .action-button {
210
+ width: 100%;
211
+ justify-content: center;
212
+ }
213
+ }
214
+ </style>
src/lib/components/MonsterGenerator/UploadStep.svelte ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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);
11
+ let preview: string | null = $state(null);
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
+
20
+ function handleDrop(e: DragEvent) {
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
+
29
+ function handleDragOver(e: DragEvent) {
30
+ e.preventDefault();
31
+ dragActive = true;
32
+ }
33
+
34
+ function handleDragLeave(e: DragEvent) {
35
+ e.preventDefault();
36
+ dragActive = false;
37
+ }
38
+
39
+ function processFile(file: File) {
40
+ if (!file.type.startsWith('image/')) {
41
+ alert('Please upload an image file');
42
+ return;
43
+ }
44
+
45
+ // Create preview
46
+ const reader = new FileReader();
47
+ reader.onload = (e) => {
48
+ preview = e.target?.result as string;
49
+ };
50
+ reader.readAsDataURL(file);
51
+
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"
66
+ class:drag-active={dragActive}
67
+ class:has-preview={preview}
68
+ ondrop={handleDrop}
69
+ ondragover={handleDragOver}
70
+ ondragleave={handleDragLeave}
71
+ onclick={triggerFileSelect}
72
+ onkeypress={(e) => e.key === 'Enter' && triggerFileSelect()}
73
+ role="button"
74
+ tabindex="0"
75
+ >
76
+ {#if preview}
77
+ <img src={preview} alt="Preview" class="preview-image" />
78
+ <div class="overlay">
79
+ <p>Click to change image</p>
80
+ </div>
81
+ {:else}
82
+ <svg class="upload-icon" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
83
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
84
+ <polyline points="17 8 12 3 7 8"></polyline>
85
+ <line x1="12" y1="3" x2="12" y2="15"></line>
86
+ </svg>
87
+ <p class="upload-text">Drop an image here or click to upload</p>
88
+ <p class="upload-hint">Supports JPG, PNG, GIF</p>
89
+ {/if}
90
+ </div>
91
+
92
+ <input
93
+ type="file"
94
+ accept="image/*"
95
+ onchange={handleFileSelect}
96
+ bind:this={fileInput}
97
+ class="hidden-input"
98
+ disabled={isProcessing}
99
+ />
100
+ </div>
101
+
102
+ <style>
103
+ .upload-container {
104
+ max-width: 600px;
105
+ margin: 0 auto;
106
+ text-align: center;
107
+ }
108
+
109
+ h3 {
110
+ margin-bottom: 0.5rem;
111
+ color: #333;
112
+ }
113
+
114
+ .subtitle {
115
+ color: #666;
116
+ margin-bottom: 2rem;
117
+ }
118
+
119
+ .upload-area {
120
+ border: 2px dashed #ccc;
121
+ border-radius: 12px;
122
+ padding: 3rem;
123
+ background: #fafafa;
124
+ transition: all 0.3s ease;
125
+ cursor: pointer;
126
+ position: relative;
127
+ overflow: hidden;
128
+ }
129
+
130
+ .upload-area:hover {
131
+ border-color: #007bff;
132
+ background: #f0f7ff;
133
+ }
134
+
135
+ .upload-area.drag-active {
136
+ border-color: #007bff;
137
+ background: #e3f2ff;
138
+ transform: scale(1.02);
139
+ }
140
+
141
+ .upload-area.has-preview {
142
+ padding: 0;
143
+ background: #fff;
144
+ }
145
+
146
+ .upload-icon {
147
+ color: #007bff;
148
+ margin-bottom: 1rem;
149
+ }
150
+
151
+ .upload-text {
152
+ font-size: 1.1rem;
153
+ color: #333;
154
+ margin-bottom: 0.5rem;
155
+ }
156
+
157
+ .upload-hint {
158
+ font-size: 0.9rem;
159
+ color: #666;
160
+ }
161
+
162
+ .hidden-input {
163
+ display: none;
164
+ }
165
+
166
+ .preview-image {
167
+ width: 100%;
168
+ height: 400px;
169
+ object-fit: contain;
170
+ display: block;
171
+ }
172
+
173
+ .overlay {
174
+ position: absolute;
175
+ top: 0;
176
+ left: 0;
177
+ right: 0;
178
+ bottom: 0;
179
+ background: rgba(0, 0, 0, 0.7);
180
+ display: flex;
181
+ align-items: center;
182
+ justify-content: center;
183
+ opacity: 0;
184
+ transition: opacity 0.3s ease;
185
+ }
186
+
187
+ .upload-area:hover .overlay {
188
+ opacity: 1;
189
+ }
190
+
191
+ .overlay p {
192
+ color: white;
193
+ font-size: 1.1rem;
194
+ }
195
+ </style>
src/lib/components/MonsterGenerator/WorkflowProgress.svelte ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { MonsterWorkflowStep } from '$lib/types';
3
+
4
+ interface Props {
5
+ currentStep: MonsterWorkflowStep;
6
+ error?: string | null;
7
+ }
8
+
9
+ let { currentStep, error = null }: Props = $props();
10
+
11
+ interface StepInfo {
12
+ id: MonsterWorkflowStep;
13
+ label: string;
14
+ description: string;
15
+ }
16
+
17
+ const steps: StepInfo[] = [
18
+ {
19
+ id: 'upload',
20
+ label: 'Upload Photo',
21
+ description: 'Select your image'
22
+ },
23
+ {
24
+ id: 'captioning',
25
+ label: 'Analyzing',
26
+ description: 'Creating detailed description'
27
+ },
28
+ {
29
+ id: 'conceptualizing',
30
+ label: 'Conceptualizing',
31
+ description: 'Designing your monster'
32
+ },
33
+ {
34
+ id: 'promptCrafting',
35
+ label: 'Crafting Prompt',
36
+ description: 'Preparing image generation'
37
+ },
38
+ {
39
+ id: 'generating',
40
+ label: 'Generating',
41
+ description: 'Creating monster image'
42
+ },
43
+ {
44
+ id: 'complete',
45
+ label: 'Complete',
46
+ description: 'Your monster is ready!'
47
+ }
48
+ ];
49
+
50
+ function getStepIndex(step: MonsterWorkflowStep): number {
51
+ return steps.findIndex(s => s.id === step);
52
+ }
53
+
54
+ function getStepStatus(step: StepInfo): 'completed' | 'current' | 'pending' | 'error' {
55
+ const currentIndex = getStepIndex(currentStep);
56
+ const stepIndex = getStepIndex(step.id);
57
+
58
+ if (error && step.id === currentStep) return 'error';
59
+ if (stepIndex < currentIndex) return 'completed';
60
+ if (stepIndex === currentIndex) return 'current';
61
+ return 'pending';
62
+ }
63
+ </script>
64
+
65
+ <div class="workflow-progress">
66
+ <div class="steps-container">
67
+ {#each steps as step, i}
68
+ {@const status = getStepStatus(step)}
69
+ <div class="step" class:completed={status === 'completed'} class:current={status === 'current'} class:error={status === 'error'}>
70
+ <div class="step-indicator">
71
+ {#if status === 'completed'}
72
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
73
+ <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"/>
74
+ </svg>
75
+ {:else if status === 'error'}
76
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
77
+ <path d="M10 2a8 8 0 100 16 8 8 0 000-16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"/>
78
+ </svg>
79
+ {:else}
80
+ <span class="step-number">{i + 1}</span>
81
+ {/if}
82
+ </div>
83
+ <div class="step-content">
84
+ <div class="step-label">{step.label}</div>
85
+ <div class="step-description">{step.description}</div>
86
+ </div>
87
+ {#if i < steps.length - 1}
88
+ <div class="step-connector" class:active={status === 'completed'}></div>
89
+ {/if}
90
+ </div>
91
+ {/each}
92
+ </div>
93
+
94
+ {#if error}
95
+ <div class="error-message">
96
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
97
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"/>
98
+ </svg>
99
+ <span>{error}</span>
100
+ </div>
101
+ {/if}
102
+ </div>
103
+
104
+ <style>
105
+ .workflow-progress {
106
+ max-width: 800px;
107
+ margin: 2rem auto;
108
+ }
109
+
110
+ .steps-container {
111
+ display: flex;
112
+ justify-content: space-between;
113
+ position: relative;
114
+ padding: 0 20px;
115
+ }
116
+
117
+ .step {
118
+ display: flex;
119
+ flex-direction: column;
120
+ align-items: center;
121
+ position: relative;
122
+ flex: 1;
123
+ }
124
+
125
+ .step-indicator {
126
+ width: 40px;
127
+ height: 40px;
128
+ border-radius: 50%;
129
+ background: #e9ecef;
130
+ color: #6c757d;
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: center;
134
+ font-weight: 600;
135
+ margin-bottom: 0.5rem;
136
+ transition: all 0.3s ease;
137
+ z-index: 2;
138
+ }
139
+
140
+ .step.completed .step-indicator {
141
+ background: #28a745;
142
+ color: white;
143
+ }
144
+
145
+ .step.current .step-indicator {
146
+ background: #007bff;
147
+ color: white;
148
+ box-shadow: 0 0 0 4px rgba(0, 123, 255, 0.2);
149
+ animation: pulse 2s infinite;
150
+ }
151
+
152
+ .step.error .step-indicator {
153
+ background: #dc3545;
154
+ color: white;
155
+ }
156
+
157
+ .step-number {
158
+ font-size: 0.9rem;
159
+ }
160
+
161
+ .step-content {
162
+ text-align: center;
163
+ }
164
+
165
+ .step-label {
166
+ font-weight: 600;
167
+ color: #333;
168
+ margin-bottom: 0.25rem;
169
+ font-size: 0.9rem;
170
+ }
171
+
172
+ .step-description {
173
+ font-size: 0.8rem;
174
+ color: #666;
175
+ }
176
+
177
+ .step-connector {
178
+ position: absolute;
179
+ top: 20px;
180
+ left: 50%;
181
+ width: 100%;
182
+ height: 2px;
183
+ background: #e9ecef;
184
+ z-index: 1;
185
+ }
186
+
187
+ .step-connector.active {
188
+ background: #28a745;
189
+ }
190
+
191
+ .error-message {
192
+ margin-top: 2rem;
193
+ padding: 1rem;
194
+ background: #f8d7da;
195
+ color: #721c24;
196
+ border-radius: 6px;
197
+ display: flex;
198
+ align-items: center;
199
+ gap: 0.5rem;
200
+ }
201
+
202
+ @keyframes pulse {
203
+ 0% {
204
+ box-shadow: 0 0 0 0 rgba(0, 123, 255, 0.4);
205
+ }
206
+ 70% {
207
+ box-shadow: 0 0 0 10px rgba(0, 123, 255, 0);
208
+ }
209
+ 100% {
210
+ box-shadow: 0 0 0 0 rgba(0, 123, 255, 0);
211
+ }
212
+ }
213
+
214
+ @media (max-width: 768px) {
215
+ .steps-container {
216
+ flex-direction: column;
217
+ padding: 0;
218
+ }
219
+
220
+ .step {
221
+ flex-direction: row;
222
+ margin-bottom: 1rem;
223
+ }
224
+
225
+ .step-indicator {
226
+ margin-right: 1rem;
227
+ margin-bottom: 0;
228
+ }
229
+
230
+ .step-content {
231
+ text-align: left;
232
+ flex: 1;
233
+ }
234
+
235
+ .step-connector {
236
+ display: none;
237
+ }
238
+ }
239
+ </style>
src/lib/types/index.ts CHANGED
@@ -71,4 +71,30 @@ export interface GradioLibs {
71
  Client: {
72
  connect: (space: string, options?: { hf_token?: string }) => Promise<GradioClient>;
73
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  }
 
71
  Client: {
72
  connect: (space: string, options?: { hf_token?: string }) => Promise<GradioClient>;
73
  };
74
+ }
75
+
76
+ // Monster Generator Types
77
+ export type MonsterWorkflowStep =
78
+ | 'upload'
79
+ | 'captioning'
80
+ | 'conceptualizing'
81
+ | 'promptCrafting'
82
+ | 'generating'
83
+ | 'complete';
84
+
85
+ export interface MonsterWorkflowState {
86
+ currentStep: MonsterWorkflowStep;
87
+ userImage: Blob | null;
88
+ imageCaption: string | null;
89
+ monsterConcept: string | null;
90
+ imagePrompt: string | null;
91
+ monsterImage: FluxGenerationResult | null;
92
+ error: string | null;
93
+ isProcessing: boolean;
94
+ }
95
+
96
+ export interface MonsterGeneratorProps {
97
+ joyCaptionClient: GradioClient | null;
98
+ rwkvClient: GradioClient | null;
99
+ fluxClient: GradioClient | null;
100
  }