usups commited on
Commit
a2b08f5
·
verified ·
1 Parent(s): 95101f0

Add 2 files

Browse files
Files changed (2) hide show
  1. README.md +6 -4
  2. index.html +1501 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Stock
3
- emoji: 🔥
4
  colorFrom: blue
5
- colorTo: gray
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: stock
3
+ emoji: 🐳
4
  colorFrom: blue
5
+ colorTo: blue
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,1501 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Microstock Image Analyzer | Gemini-Powered SEO Metadata</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
8
+ <style>
9
+ :root {
10
+ --primary: #4a6bff;
11
+ --secondary: #f8f9fa;
12
+ --dark: #343a40;
13
+ --light: #ffffff;
14
+ --success: #28a745;
15
+ --info: #17a2b8;
16
+ --warning: #ffc107;
17
+ --danger: #dc3545;
18
+ --gemini-color: #0468d7;
19
+ }
20
+
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
26
+ }
27
+
28
+ body {
29
+ background-color: #f5f7ff;
30
+ color: var(--dark);
31
+ line-height: 1.6;
32
+ }
33
+
34
+ .container {
35
+ max-width: 1200px;
36
+ margin: 0 auto;
37
+ padding: 2rem;
38
+ }
39
+
40
+ header {
41
+ text-align: center;
42
+ margin-bottom: 2rem;
43
+ position: relative;
44
+ }
45
+
46
+ h1 {
47
+ color: var(--primary);
48
+ margin-bottom: 0.5rem;
49
+ font-weight: 700;
50
+ }
51
+
52
+ .subtitle {
53
+ color: #6c757d;
54
+ font-size: 1.1rem;
55
+ margin-bottom: 1.5rem;
56
+ }
57
+
58
+ .upload-container {
59
+ background-color: var(--light);
60
+ border-radius: 10px;
61
+ padding: 2rem;
62
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
63
+ margin-bottom: 2rem;
64
+ }
65
+
66
+ .upload-area {
67
+ border: 2px dashed #d3d3d3;
68
+ border-radius: 8px;
69
+ padding: 3rem 2rem;
70
+ text-align: center;
71
+ cursor: pointer;
72
+ transition: all 0.3s ease;
73
+ margin-bottom: 1.5rem;
74
+ }
75
+
76
+ .upload-area:hover {
77
+ border-color: var(--primary);
78
+ background-color: rgba(74, 107, 255, 0.05);
79
+ }
80
+
81
+ .upload-area i {
82
+ font-size: 3rem;
83
+ color: var(--primary);
84
+ margin-bottom: 1rem;
85
+ }
86
+
87
+ .upload-area h3 {
88
+ margin-bottom: 0.5rem;
89
+ }
90
+
91
+ .upload-area p {
92
+ color: #6c757d;
93
+ margin-bottom: 1rem;
94
+ }
95
+
96
+ #file-input {
97
+ display: none;
98
+ }
99
+
100
+ .btn {
101
+ display: inline-block;
102
+ padding: 0.6rem 1.2rem;
103
+ background-color: var(--primary);
104
+ color: white;
105
+ border: none;
106
+ border-radius: 5px;
107
+ cursor: pointer;
108
+ font-size: 1rem;
109
+ font-weight: 500;
110
+ transition: all 0.3s ease;
111
+ text-align: center;
112
+ }
113
+
114
+ .btn:hover {
115
+ background-color: #3a56d4;
116
+ transform: translateY(-2px);
117
+ box-shadow: 0 4px 12px rgba(74, 107, 255, 0.3);
118
+ }
119
+
120
+ .btn-secondary {
121
+ background-color: #6c757d;
122
+ }
123
+
124
+ .btn-secondary:hover {
125
+ background-color: #5a6268;
126
+ }
127
+
128
+ .btn-warning {
129
+ background-color: var(--warning);
130
+ color: var(--dark);
131
+ }
132
+
133
+ .btn-warning:hover {
134
+ background-color: #e0a800;
135
+ }
136
+
137
+ .btn-success {
138
+ background-color: var(--success);
139
+ }
140
+
141
+ .btn-success:hover {
142
+ background-color: #218838;
143
+ }
144
+
145
+ .btn-gemini {
146
+ background-color: var(--gemini-color);
147
+ }
148
+
149
+ .btn-gemini:hover {
150
+ background-color: #0352a8;
151
+ }
152
+
153
+ .btn-block {
154
+ display: block;
155
+ width: 100%;
156
+ }
157
+
158
+ .settings-panel {
159
+ background-color: var(--secondary);
160
+ border-radius: 8px;
161
+ padding: 1.5rem;
162
+ margin-bottom: 1.5rem;
163
+ }
164
+
165
+ .settings-grid {
166
+ display: grid;
167
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
168
+ gap: 1rem;
169
+ }
170
+
171
+ .form-group {
172
+ margin-bottom: 1rem;
173
+ }
174
+
175
+ label {
176
+ display: block;
177
+ margin-bottom: 0.5rem;
178
+ font-weight: 500;
179
+ }
180
+
181
+ select, input[type="text"], textarea {
182
+ width: 100%;
183
+ padding: 0.6rem;
184
+ border: 1px solid #ced4da;
185
+ border-radius: 5px;
186
+ background-color: var(--light);
187
+ }
188
+
189
+ textarea {
190
+ min-height: 100px;
191
+ resize: vertical;
192
+ }
193
+
194
+ .preview-container {
195
+ background-color: var(--light);
196
+ border-radius: 10px;
197
+ padding: 2rem;
198
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
199
+ margin-bottom: 2rem;
200
+ }
201
+
202
+ .result-grid {
203
+ display: grid;
204
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
205
+ gap: 2rem;
206
+ }
207
+
208
+ .image-card {
209
+ background-color: var(--secondary);
210
+ border-radius: 8px;
211
+ overflow: hidden;
212
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
213
+ transition: transform 0.3s ease;
214
+ }
215
+
216
+ .image-card:hover {
217
+ transform: translateY(-5px);
218
+ box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);
219
+ }
220
+
221
+ .image-preview {
222
+ width: 100%;
223
+ height: 200px;
224
+ object-fit: contain;
225
+ background-color: #e9ecef;
226
+ }
227
+
228
+ .card-body {
229
+ padding: 1.5rem;
230
+ }
231
+
232
+ .card-title {
233
+ font-size: 1.25rem;
234
+ margin-bottom: 0.75rem;
235
+ color: var(--dark);
236
+ }
237
+
238
+ .card-text {
239
+ color: #6c757d;
240
+ margin-bottom: 1rem;
241
+ font-size: 0.9rem;
242
+ }
243
+
244
+ .card-keywords {
245
+ display: flex;
246
+ flex-wrap: wrap;
247
+ gap: 0.5rem;
248
+ margin-bottom: 1rem;
249
+ }
250
+
251
+ .keyword-chip {
252
+ background-color: var(--primary);
253
+ color: white;
254
+ padding: 0.3rem 0.6rem;
255
+ border-radius: 20px;
256
+ font-size: 0.75rem;
257
+ display: inline-block;
258
+ }
259
+
260
+ .card-actions {
261
+ display: flex;
262
+ gap: 0.75rem;
263
+ margin-top: 1rem;
264
+ }
265
+
266
+ .copy-btn {
267
+ background-color: var(--success);
268
+ padding: 0.5rem 1rem;
269
+ border-radius: 5px;
270
+ color: white;
271
+ border: none;
272
+ cursor: pointer;
273
+ display: flex;
274
+ align-items: center;
275
+ gap: 0.5rem;
276
+ font-size: 0.85rem;
277
+ }
278
+
279
+ .regenerate-btn {
280
+ background-color: var(--info);
281
+ padding: 0.5rem 1rem;
282
+ border-radius: 5px;
283
+ color: white;
284
+ border: none;
285
+ cursor: pointer;
286
+ display: flex;
287
+ align-items: center;
288
+ gap: 0.5rem;
289
+ font-size: 0.85rem;
290
+ }
291
+
292
+ .action-bar {
293
+ display: flex;
294
+ justify-content: space-between;
295
+ align-items: center;
296
+ margin-bottom: 1.5rem;
297
+ }
298
+
299
+ .loading {
300
+ display: none;
301
+ text-align: center;
302
+ padding: 2rem;
303
+ }
304
+
305
+ .spinner {
306
+ width: 50px;
307
+ height: 50px;
308
+ border: 5px solid rgba(74, 107, 255, 0.2);
309
+ border-radius: 50%;
310
+ border-top-color: var(--primary);
311
+ animation: spin 1s ease-in-out infinite;
312
+ margin: 0 auto 1rem;
313
+ }
314
+
315
+ @keyframes spin {
316
+ to { transform: rotate(360deg); }
317
+ }
318
+
319
+ .download-all {
320
+ background-color: var(--success);
321
+ margin-bottom: 2rem;
322
+ }
323
+
324
+ .file-list {
325
+ list-style-type: none;
326
+ margin-bottom: 1rem;
327
+ max-height: 200px;
328
+ overflow-y: auto;
329
+ border: 1px solid #eee;
330
+ border-radius: 5px;
331
+ padding: 0.5rem;
332
+ }
333
+
334
+ .file-list li {
335
+ padding: 0.5rem;
336
+ border-bottom: 1px solid #eee;
337
+ display: flex;
338
+ justify-content: space-between;
339
+ }
340
+
341
+ .file-list li:last-child {
342
+ border-bottom: none;
343
+ }
344
+
345
+ .file-name {
346
+ white-space: nowrap;
347
+ overflow: hidden;
348
+ text-overflow: ellipsis;
349
+ max-width: 80%;
350
+ }
351
+
352
+ .file-size {
353
+ color: #6c757d;
354
+ font-size: 0.8rem;
355
+ }
356
+
357
+ .remove-file {
358
+ color: #dc3545;
359
+ cursor: pointer;
360
+ margin-left: 0.5rem;
361
+ }
362
+
363
+ .api-settings {
364
+ background-color: #fff8e1;
365
+ border: 1px solid #ffe0b2;
366
+ border-radius: 8px;
367
+ padding: 1.5rem;
368
+ margin-bottom: 1.5rem;
369
+ }
370
+
371
+ .api-toggle {
372
+ display: flex;
373
+ align-items: center;
374
+ margin-bottom: 1rem;
375
+ }
376
+
377
+ .api-toggle label {
378
+ margin-bottom: 0;
379
+ margin-left: 0.5rem;
380
+ cursor: pointer;
381
+ }
382
+
383
+ .api-credentials {
384
+ display: none;
385
+ margin-top: 1rem;
386
+ }
387
+
388
+ .api-credentials.active {
389
+ display: block;
390
+ }
391
+
392
+ .tab-container {
393
+ margin-bottom: 1.5rem;
394
+ }
395
+
396
+ .tabs {
397
+ display: flex;
398
+ border-bottom: 1px solid #ddd;
399
+ margin-bottom: 1rem;
400
+ }
401
+
402
+ .tab {
403
+ padding: 0.75rem 1.5rem;
404
+ cursor: pointer;
405
+ border-bottom: 3px solid transparent;
406
+ transition: all 0.2s ease;
407
+ }
408
+
409
+ .tab.active {
410
+ border-bottom: 3px solid var(--primary);
411
+ color: var(--primary);
412
+ font-weight: 500;
413
+ }
414
+
415
+ .tab-content {
416
+ display: none;
417
+ }
418
+
419
+ .tab-content.active {
420
+ display: block;
421
+ }
422
+
423
+ .bulk-edit-container {
424
+ margin-top: 1.5rem;
425
+ }
426
+
427
+ .alert {
428
+ padding: 1rem;
429
+ border-radius: 5px;
430
+ margin-bottom: 1rem;
431
+ display: flex;
432
+ align-items: flex-start;
433
+ gap: 0.75rem;
434
+ }
435
+
436
+ .alert-info {
437
+ background-color: #e7f5ff;
438
+ border-left: 3px solid var(--info);
439
+ color: var(--dark);
440
+ }
441
+
442
+ .alert-warning {
443
+ background-color: #fff4e6;
444
+ border-left: 3px solid var(--warning);
445
+ color: var(--dark);
446
+ }
447
+
448
+ .alert-success {
449
+ background-color: #e6f5ea;
450
+ border-left: 3px solid var(--success);
451
+ color: var(--dark);
452
+ }
453
+
454
+ .alert-error {
455
+ background-color: #fee;
456
+ border-left: 3px solid var(--danger);
457
+ color: var(--dark);
458
+ }
459
+
460
+ .badge {
461
+ display: inline-block;
462
+ padding: 0.25rem 0.5rem;
463
+ border-radius: 20px;
464
+ font-size: 0.75rem;
465
+ font-weight: 500;
466
+ }
467
+
468
+ .badge-gemini {
469
+ background-color: var(--gemini-color);
470
+ color: white;
471
+ }
472
+
473
+ .badge-basic {
474
+ background-color: #6c757d;
475
+ color: white;
476
+ }
477
+
478
+ .powered-by {
479
+ display: flex;
480
+ align-items: center;
481
+ justify-content: center;
482
+ gap: 0.5rem;
483
+ margin-top: 1rem;
484
+ color: var(--gemini-color);
485
+ font-weight: 500;
486
+ }
487
+
488
+ footer {
489
+ text-align: center;
490
+ margin-top: 3rem;
491
+ color: #6c757d;
492
+ font-size: 0.9rem;
493
+ }
494
+
495
+ @media (max-width: 768px) {
496
+ .container {
497
+ padding: 1rem;
498
+ }
499
+
500
+ .settings-grid {
501
+ grid-template-columns: 1fr;
502
+ }
503
+
504
+ .result-grid {
505
+ grid-template-columns: 1fr;
506
+ }
507
+
508
+ .action-bar {
509
+ flex-direction: column;
510
+ align-items: flex-start;
511
+ gap: 1rem;
512
+ }
513
+ }
514
+ </style>
515
+ </head>
516
+ <body>
517
+ <div class="container">
518
+ <header>
519
+ <h1>AI Microstock Image Analyzer</h1>
520
+ <p class="subtitle">Generate SEO-optimized metadata with Google Gemini AI for your stock photos</p>
521
+ </header>
522
+
523
+ <div class="api-settings">
524
+ <h3>
525
+ <i class="fas fa-key" style="color: var(--gemini-color);"></i>
526
+ Gemini API Configuration
527
+ </h3>
528
+ <div class="api-toggle">
529
+ <input type="checkbox" id="use-gemini-api" checked>
530
+ <label for="use-gemini-api">Enable Google Gemini API Image Analysis</label>
531
+ </div>
532
+
533
+ <div class="api-credentials active" id="api-credentials">
534
+ <div class="form-group">
535
+ <label for="api-key">Gemini API Key</label>
536
+ <input type="text" id="api-key" placeholder="Enter your Google Gemini API key (required)">
537
+ <small>Get your API key from <a href="https://ai.google.dev/" target="_blank" style="color: var(--gemini-color);">Google AI Studio</a></small>
538
+ </div>
539
+ <button id="test-api-btn" class="btn btn-gemini">
540
+ <i class="fas fa-plug"></i> Test API Connection
541
+ </button>
542
+ <button id="save-api-btn" class="btn">
543
+ <i class="fas fa-save"></i> Save Settings
544
+ </button>
545
+ </div>
546
+ </div>
547
+
548
+ <div class="tab-container">
549
+ <div class="tabs">
550
+ <div class="tab active" data-tab="upload">Upload Images</div>
551
+ <div class="tab" data-tab="bulk-edit">Bulk Edit</div>
552
+ </div>
553
+
554
+ <div class="tab-content active" id="upload-tab">
555
+ <div class="upload-container">
556
+ <div class="upload-area" id="upload-area">
557
+ <i class="fas fa-cloud-upload-alt"></i>
558
+ <h3>Upload Your Microstock Images</h3>
559
+ <p>Drag & drop your files here or click to browse</p>
560
+ <button class="btn">Select Files</button>
561
+ <input type="file" id="file-input" accept="image/*" multiple>
562
+ </div>
563
+
564
+ <div class="file-list-container" id="file-list-container" style="display: none;">
565
+ <h4>Selected Files (<span id="file-count">0</span>)</h4>
566
+ <ul class="file-list" id="file-list"></ul>
567
+ </div>
568
+ </div>
569
+
570
+ <div class="settings-panel">
571
+ <h3>Generation Settings</h3>
572
+ <div class="settings-grid">
573
+ <div class="form-group">
574
+ <label for="image-category">Image Category</label>
575
+ <select id="image-category">
576
+ <option value="general">General</option>
577
+ <option value="business">Business</option>
578
+ <option value="technology">Technology</option>
579
+ <option value="nature">Nature</option>
580
+ <option value="people">People</option>
581
+ <option value="food">Food</option>
582
+ <option value="travel">Travel</option>
583
+ <option value="health">Health</option>
584
+ <option value="education">Education</option>
585
+ <option value="holiday">Holiday</option>
586
+ </select>
587
+ </div>
588
+
589
+ <div class="form-group">
590
+ <label for="image-style">Image Style</label>
591
+ <select id="image-style">
592
+ <option value="realistic">Realistic</option>
593
+ <option value="flat">Flat Design</option>
594
+ <option value="3d">3D Rendering</option>
595
+ <option value="illustration">Illustration</option>
596
+ <option value="hand-drawn">Hand Drawn</option>
597
+ <option value="vector">Vector</option>
598
+ <option value="minimal">Minimal</option>
599
+ <option value="isometric">Isometric</option>
600
+ </select>
601
+ </div>
602
+
603
+ <div class="form-group">
604
+ <label for="language">Content Language</label>
605
+ <select id="language">
606
+ <option value="en">English</option>
607
+ <option value="es">Spanish</option>
608
+ <option value="fr">French</option>
609
+ <option value="de">German</option>
610
+ <option value="pt">Portuguese</option>
611
+ </select>
612
+ </div>
613
+
614
+ <div class="form-group">
615
+ <label for="target-platform">Target Platform</label>
616
+ <select id="target-platform">
617
+ <option value="all">All Platforms</option>
618
+ <option value="freepik">Freepik</option>
619
+ <option value="shutterstock">Shutterstock</option>
620
+ <option value="adobe">Adobe Stock</option>
621
+ <option value="iconscout">IconScout</option>
622
+ </select>
623
+ </div>
624
+ </div>
625
+ </div>
626
+
627
+ <button id="generate-btn" class="btn btn-block btn-gemini">
628
+ <i class="fas fa-magic"></i> Generate SEO Metadata with Gemini
629
+ </button>
630
+
631
+ <div class="alert alert-info">
632
+ <i class="fas fa-info-circle"></i>
633
+ <div>
634
+ <strong>Gemini AI Tip:</strong> For best results, describe the image category, style, and target platform above.
635
+ Gemini will generate highly relevant metadata based on your visual content and these specifications.
636
+ </div>
637
+ </div>
638
+
639
+ <div class="loading" id="loading">
640
+ <div class="spinner"></div>
641
+ <p id="loading-text">Gemini is analyzing your images and generating SEO metadata...</p>
642
+ <div class="powered-by">
643
+ <img src="https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg" alt="Gemini" width="20">
644
+ Powered by Google Gemini AI
645
+ </div>
646
+ </div>
647
+
648
+ <div class="preview-container" id="results-container" style="display: none;">
649
+ <div class="action-bar">
650
+ <h3>Generated Results</h3>
651
+ <div>
652
+ <button id="download-all" class="btn download-all">
653
+ <i class="fas fa-download"></i> Download All as CSV
654
+ </button>
655
+ <button id="regenerate-all" class="btn btn-warning">
656
+ <i class="fas fa-sync-alt"></i> Regenerate All
657
+ </button>
658
+ </div>
659
+ </div>
660
+
661
+ <div class="result-grid" id="result-grid"></div>
662
+ </div>
663
+ </div>
664
+
665
+ <div class="tab-content" id="bulk-edit-tab">
666
+ <div class="alert alert-warning">
667
+ <i class="fas fa-exclamation-triangle"></i>
668
+ <div>
669
+ <strong>Notice:</strong> Please upload files first, then you can edit all metadata in bulk here.
670
+ Bulk edits will override existing metadata.
671
+ </div>
672
+ </div>
673
+
674
+ <div class="bulk-edit-container" id="bulk-edit-fields" style="display: none;">
675
+ <div class="form-group">
676
+ <label for="bulk-title">Bulk Edit Title</label>
677
+ <input type="text" id="bulk-title" placeholder="Will be applied to all images">
678
+ </div>
679
+
680
+ <div class="form-group">
681
+ <label for="bulk-description">Bulk Edit Description</label>
682
+ <textarea id="bulk-description" placeholder="Will be applied to all images"></textarea>
683
+ </div>
684
+
685
+ <div class="form-group">
686
+ <label for="bulk-keywords">Bulk Edit Keywords (comma separated)</label>
687
+ <textarea id="bulk-keywords" placeholder="Will replace all keywords for all images"></textarea>
688
+ </div>
689
+
690
+ <button id="apply-bulk-edit" class="btn">Apply Bulk Changes to All Images</button>
691
+ </div>
692
+ </div>
693
+ </div>
694
+
695
+ <div class="alert alert-info" id="api-status" style="display: none;">
696
+ <i class="fas fa-info-circle"></i>
697
+ <div id="api-status-text"></div>
698
+ </div>
699
+ </div>
700
+
701
+ <footer>
702
+ <p>AI Microstock Image Analyzer &copy; 2023 | Powered by Google Gemini AI</p>
703
+ </footer>
704
+
705
+ <script>
706
+ // DOM elements
707
+ const uploadArea = document.getElementById('upload-area');
708
+ const fileInput = document.getElementById('file-input');
709
+ const fileListContainer = document.getElementById('file-list-container');
710
+ const fileList = document.getElementById('file-list');
711
+ const fileCount = document.getElementById('file-count');
712
+ const generateBtn = document.getElementById('generate-btn');
713
+ const loadingIndicator = document.getElementById('loading');
714
+ const loadingText = document.getElementById('loading-text');
715
+ const resultsContainer = document.getElementById('results-container');
716
+ const resultGrid = document.getElementById('result-grid');
717
+ const downloadAllBtn = document.getElementById('download-all');
718
+ const regenerateAllBtn = document.getElementById('regenerate-all');
719
+ const applyBulkEditBtn = document.getElementById('apply-bulk-edit');
720
+ const useGeminiApiCheckbox = document.getElementById('use-gemini-api');
721
+ const apiCredentialsSection = document.getElementById('api-credentials');
722
+ const saveApiBtn = document.getElementById('save-api-btn');
723
+ const testApiBtn = document.getElementById('test-api-btn');
724
+ const apiKeyInput = document.getElementById('api-key');
725
+ const tabs = document.querySelectorAll('.tab');
726
+ const tabContents = document.querySelectorAll('.tab-content');
727
+ const bulkEditFields = document.getElementById('bulk-edit-fields');
728
+ const apiStatusEl = document.getElementById('api-status');
729
+ const apiStatusText = document.getElementById('api-status-text');
730
+
731
+ // Store uploaded files
732
+ let uploadedFiles = [];
733
+ let geminiApiKey = '';
734
+ let useGeminiApi = true;
735
+ let apiConnected = false;
736
+
737
+ // Initialize the app
738
+ function init() {
739
+ // Check saved API settings
740
+ const savedApiKey = localStorage.getItem('geminiApiKey');
741
+ const savedApiEnabled = localStorage.getItem('useGeminiApi');
742
+
743
+ if (savedApiKey) {
744
+ apiKeyInput.value = savedApiKey;
745
+ geminiApiKey = savedApiKey;
746
+ }
747
+
748
+ if (savedApiEnabled !== null) {
749
+ useGeminiApi = savedApiEnabled === 'true';
750
+ useGeminiApiCheckbox.checked = useGeminiApi;
751
+ apiCredentialsSection.classList.toggle('active', useGeminiApi);
752
+ }
753
+
754
+ // Show status if we have an API key
755
+ if (geminiApiKey) {
756
+ showApiStatus('Using saved Gemini API key. Click "Test Connection" to verify.', 'info');
757
+ }
758
+ }
759
+
760
+ // Event listeners
761
+ uploadArea.addEventListener('click', () => fileInput.click());
762
+ uploadArea.addEventListener('dragover', (e) => {
763
+ e.preventDefault();
764
+ uploadArea.style.borderColor = 'var(--primary)';
765
+ uploadArea.style.backgroundColor = 'rgba(74, 107, 255, 0.05)';
766
+ });
767
+
768
+ uploadArea.addEventListener('dragleave', () => {
769
+ uploadArea.style.borderColor = '#d3d3d3';
770
+ uploadArea.style.backgroundColor = 'transparent';
771
+ });
772
+
773
+ uploadArea.addEventListener('drop', (e) => {
774
+ e.preventDefault();
775
+ uploadArea.style.borderColor = '#d3d3d3';
776
+ uploadArea.style.backgroundColor = 'transparent';
777
+
778
+ if (e.dataTransfer.files.length) {
779
+ handleFiles(e.dataTransfer.files);
780
+ }
781
+ });
782
+
783
+ fileInput.addEventListener('change', () => {
784
+ if (fileInput.files.length) {
785
+ handleFiles(fileInput.files);
786
+ }
787
+ });
788
+
789
+ generateBtn.addEventListener('click', generateMetadata);
790
+ downloadAllBtn.addEventListener('click', downloadAllAsCSV);
791
+ regenerateAllBtn.addEventListener('click', regenerateAllMetadata);
792
+ applyBulkEditBtn.addEventListener('click', applyBulkEdit);
793
+
794
+ useGeminiApiCheckbox.addEventListener('change', toggleGeminiApi);
795
+ saveApiBtn.addEventListener('click', saveApiSettings);
796
+ testApiBtn.addEventListener('click', testApiConnection);
797
+
798
+ // Tab switching
799
+ tabs.forEach(tab => {
800
+ tab.addEventListener('click', () => {
801
+ const tabId = tab.getAttribute('data-tab');
802
+
803
+ // Set active tab
804
+ tabs.forEach(t => t.classList.remove('active'));
805
+ tab.classList.add('active');
806
+
807
+ // Show corresponding content
808
+ tabContents.forEach(content => content.classList.remove('active'));
809
+ document.getElementById(`${tabId}-tab`).classList.add('active');
810
+
811
+ // Show bulk edit fields if files are uploaded
812
+ if (tabId === 'bulk-edit' && uploadedFiles.length > 0) {
813
+ bulkEditFields.style.display = 'block';
814
+ }
815
+ });
816
+ });
817
+
818
+ // Show API status message
819
+ function showApiStatus(message, type = 'info') {
820
+ apiStatusEl.style.display = 'flex';
821
+ apiStatusText.innerHTML = message;
822
+
823
+ // Set appropriate class
824
+ apiStatusEl.className = 'alert';
825
+ if (type === 'info') apiStatusEl.classList.add('alert-info');
826
+ else if (type === 'error') apiStatusEl.classList.add('alert-error');
827
+ else if (type === 'success') apiStatusEl.classList.add('alert-success');
828
+ }
829
+
830
+ // Hide API status message
831
+ function hideApiStatus() {
832
+ apiStatusEl.style.display = 'none';
833
+ }
834
+
835
+ // Handle uploaded files
836
+ function handleFiles(files) {
837
+ // Limit to 20 files for this demo
838
+ if (files.length > 20) {
839
+ alert('For demo purposes, please upload up to 20 files at a time.');
840
+ return;
841
+ }
842
+
843
+ uploadedFiles = Array.from(files).filter(file => file.type.startsWith('image/'));
844
+ updateFileList();
845
+
846
+ if (uploadedFiles.length > 0) {
847
+ fileListContainer.style.display = 'block';
848
+
849
+ // Show bulk edit fields if on that tab
850
+ if (document.querySelector('.tab.active').dataset.tab === 'bulk-edit') {
851
+ bulkEditFields.style.display = 'block';
852
+ }
853
+ } else {
854
+ fileListContainer.style.display = 'none';
855
+ bulkEditFields.style.display = 'none';
856
+ }
857
+ }
858
+
859
+ // Update the file list UI
860
+ function updateFileList() {
861
+ fileList.innerHTML = '';
862
+ fileCount.textContent = uploadedFiles.length;
863
+
864
+ uploadedFiles.forEach((file, index) => {
865
+ const li = document.createElement('li');
866
+
867
+ const nameSpan = document.createElement('span');
868
+ nameSpan.className = 'file-name';
869
+ nameSpan.textContent = file.name;
870
+
871
+ const sizeSpan = document.createElement('span');
872
+ sizeSpan.className = 'file-size';
873
+ sizeSpan.textContent = formatFileSize(file.size);
874
+
875
+ const removeSpan = document.createElement('span');
876
+ removeSpan.className = 'remove-file';
877
+ removeSpan.innerHTML = '<i class="fas fa-times"></i>';
878
+ removeSpan.addEventListener('click', () => removeFile(index));
879
+
880
+ li.appendChild(nameSpan);
881
+ li.appendChild(sizeSpan);
882
+ li.appendChild(removeSpan);
883
+ fileList.appendChild(li);
884
+ });
885
+ }
886
+
887
+ // Remove a file from the list
888
+ function removeFile(index) {
889
+ uploadedFiles.splice(index, 1);
890
+ updateFileList();
891
+
892
+ if (uploadedFiles.length === 0) {
893
+ fileListContainer.style.display = 'none';
894
+ bulkEditFields.style.display = 'none';
895
+ resultsContainer.style.display = 'none';
896
+ }
897
+ }
898
+
899
+ // Format file size
900
+ function formatFileSize(bytes) {
901
+ if (bytes === 0) return '0 Bytes';
902
+
903
+ const k = 1024;
904
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
905
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
906
+
907
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
908
+ }
909
+
910
+ // Toggle Gemini API settings
911
+ function toggleGeminiApi() {
912
+ useGeminiApi = useGeminiApiCheckbox.checked;
913
+ apiCredentialsSection.classList.toggle('active', useGeminiApi);
914
+
915
+ if (!useGeminiApi) {
916
+ showApiStatus('Gemini API is disabled. Using basic image analysis.', 'warning');
917
+ } else {
918
+ if (geminiApiKey) {
919
+ showApiStatus('Gemini API is enabled but not verified. Click "Test Connection" to verify.', 'info');
920
+ } else {
921
+ showApiStatus('Gemini API is not configured. Please enter your API key.', 'error');
922
+ }
923
+ }
924
+ }
925
+
926
+ // Save API settings
927
+ function saveApiSettings() {
928
+ const apiKey = apiKeyInput.value.trim();
929
+
930
+ if (useGeminiApi && !apiKey) {
931
+ showApiStatus('Please enter your Gemini API key', 'error');
932
+ return;
933
+ }
934
+
935
+ geminiApiKey = apiKey;
936
+ localStorage.setItem('geminiApiKey', geminiApiKey);
937
+ localStorage.setItem('useGeminiApi', useGeminiApi);
938
+
939
+ showApiStatus('API settings saved successfully!', 'success');
940
+ setTimeout(hideApiStatus, 3000);
941
+
942
+ // If we don't have an active connection, suggest testing it
943
+ if (!apiConnected && useGeminiApi && geminiApiKey) {
944
+ showApiStatus('Settings saved. Click "Test Connection" to verify the API key.', 'info');
945
+ }
946
+ }
947
+
948
+ // Test API connection
949
+ async function testApiConnection() {
950
+ const apiKey = apiKeyInput.value.trim();
951
+
952
+ if (!apiKey) {
953
+ showApiStatus('Please enter your Gemini API key first', 'error');
954
+ return;
955
+ }
956
+
957
+ testApiBtn.disabled = true;
958
+ testApiBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Testing...';
959
+
960
+ try {
961
+ // Simple test call to verify the API key
962
+ await callGeminiAPI(
963
+ apiKey,
964
+ "What is 2 + 2?", // Simple question to test the API
965
+ false // Don't include image
966
+ );
967
+
968
+ apiConnected = true;
969
+ showApiStatus('✅ Gemini API connection successful! You can now analyze images.', 'success');
970
+ testApiBtn.innerHTML = '<i class="fas fa-check-circle"></i> Connection Verified';
971
+ testApiBtn.className = 'btn btn-success';
972
+
973
+ // After 5 seconds, reset the button
974
+ setTimeout(() => {
975
+ testApiBtn.innerHTML = '<i class="fas fa-plug"></i> Test API Connection';
976
+ testApiBtn.className = 'btn btn-gemini';
977
+ testApiBtn.disabled = false;
978
+ }, 5000);
979
+
980
+ } catch (error) {
981
+ apiConnected = false;
982
+ console.error('API test failed:', error);
983
+ testApiBtn.innerHTML = '<i class="fas fa-exclamation-circle"></i> Connection Failed';
984
+ testApiBtn.className = 'btn btn-secondary';
985
+
986
+ let errorMessage = 'Failed to connect to Gemini API. ';
987
+ if (error.message.includes('API_KEY_INVALID')) {
988
+ errorMessage += 'The API key is invalid. Please check and try again.';
989
+ } else if (error.message.includes('quota')) {
990
+ errorMessage += 'You may have exceeded your quota or the API may not be enabled.';
991
+ } else {
992
+ errorMessage += 'Please check your network connection and try again.';
993
+ }
994
+
995
+ showApiStatus(errorMessage, 'error');
996
+
997
+ // Reset button after 5 seconds
998
+ setTimeout(() => {
999
+ testApiBtn.innerHTML = '<i class="fas fa-plug"></i> Test API Connection';
1000
+ testApiBtn.className = 'btn btn-gemini';
1001
+ testApiBtn.disabled = false;
1002
+ }, 5000);
1003
+ }
1004
+ }
1005
+
1006
+ // Call Gemini API
1007
+ async function callGeminiAPI(apiKey, prompt, includeImage, imageBase64 = null) {
1008
+ const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent?key=${apiKey}`;
1009
+
1010
+ // Prepare the request payload
1011
+ let requestBody = {
1012
+ contents: [{
1013
+ parts: [
1014
+ { text: prompt }
1015
+ ]
1016
+ }]
1017
+ };
1018
+
1019
+ if (includeImage && imageBase64) {
1020
+ requestBody.contents[0].parts.push({
1021
+ inlineData: {
1022
+ mimeType: "image/jpeg",
1023
+ data: imageBase64.split(',')[1] // Remove the data URL prefix
1024
+ }
1025
+ });
1026
+ }
1027
+
1028
+ const response = await fetch(apiUrl, {
1029
+ method: 'POST',
1030
+ headers: {
1031
+ 'Content-Type': 'application/json'
1032
+ },
1033
+ body: JSON.stringify(requestBody)
1034
+ });
1035
+
1036
+ if (!response.ok) {
1037
+ const errorData = await response.json();
1038
+ throw new Error(errorData.error?.message || 'Unknown API error');
1039
+ }
1040
+
1041
+ const data = await response.json();
1042
+ return data;
1043
+ }
1044
+
1045
+ // Generate metadata for all images
1046
+ async function generateMetadata() {
1047
+ if (uploadedFiles.length === 0) {
1048
+ showApiStatus('Please upload at least one image first.', 'error');
1049
+ return;
1050
+ }
1051
+
1052
+ if (useGeminiApi && !geminiApiKey) {
1053
+ showApiStatus('Gemini API is enabled but no API key is provided. Please enter your API key.', 'error');
1054
+ return;
1055
+ }
1056
+
1057
+ loadingIndicator.style.display = 'block';
1058
+ resultsContainer.style.display = 'none';
1059
+ generateBtn.disabled = true;
1060
+
1061
+ // Clear previous results
1062
+ resultGrid.innerHTML = '';
1063
+
1064
+ // Process images sequentially
1065
+ for (let i = 0; i < uploadedFiles.length; i++) {
1066
+ loadingText.textContent = `Analyzing images with ${useGeminiApi ? 'Gemini AI' : 'basic analysis'} (${i+1}/${uploadedFiles.length})...`;
1067
+
1068
+ try {
1069
+ await processSingleImage(uploadedFiles[i], i);
1070
+ } catch (error) {
1071
+ console.error(`Error processing image ${i}:`, error);
1072
+
1073
+ // Fallback to basic analysis if Gemini fails
1074
+ if (useGeminiApi) {
1075
+ loadingText.textContent = `Gemini API failed, falling back to basic analysis (${i+1}/${uploadedFiles.length})...`;
1076
+ uploadedFiles[i].metadata = generateDummyMetadata(uploadedFiles[i]);
1077
+ createImageCard(uploadedFiles[i], i);
1078
+ }
1079
+ }
1080
+ }
1081
+
1082
+ loadingIndicator.style.display = 'none';
1083
+ resultsContainer.style.display = 'block';
1084
+ generateBtn.disabled = false;
1085
+
1086
+ // Scroll to results
1087
+ window.scrollTo({
1088
+ top: resultsContainer.offsetTop - 20,
1089
+ behavior: 'smooth'
1090
+ });
1091
+ }
1092
+
1093
+ // Process a single image
1094
+ async function processSingleImage(file, index) {
1095
+ return new Promise((resolve) => {
1096
+ const reader = new FileReader();
1097
+
1098
+ reader.onload = async function(e) {
1099
+ const imageDataUrl = e.target.result;
1100
+
1101
+ // Generate metadata
1102
+ let metadata;
1103
+ let source = '';
1104
+
1105
+ if (useGeminiApi && geminiApiKey) {
1106
+ try {
1107
+ metadata = await generateMetadataWithGemini(imageDataUrl, file);
1108
+ source = 'gemini';
1109
+ } catch (error) {
1110
+ console.error('Gemini API error, falling back to basic analysis:', error);
1111
+ metadata = generateDummyMetadata(file);
1112
+ source = 'basic';
1113
+ }
1114
+ } else {
1115
+ metadata = generateDummyMetadata(file);
1116
+ source = 'basic';
1117
+ }
1118
+
1119
+ // Store metadata with the file
1120
+ uploadedFiles[index].metadata = metadata;
1121
+ uploadedFiles[index].source = source;
1122
+
1123
+ createImageCard(uploadedFiles[index], index);
1124
+ resolve();
1125
+ };
1126
+
1127
+ reader.readAsDataURL(file);
1128
+ });
1129
+ }
1130
+
1131
+ // Create image card in the results grid
1132
+ function createImageCard(file, index) {
1133
+ const metadata = file.metadata;
1134
+ const imageDataUrl = URL.createObjectURL(file);
1135
+
1136
+ const card = document.createElement('div');
1137
+ card.className = 'image-card';
1138
+ card.innerHTML = `
1139
+ <img src="${imageDataUrl}" alt="Preview" class="image-preview">
1140
+ <div class="card-body">
1141
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
1142
+ <h4 class="card-title">${metadata.title}</h4>
1143
+ <span class="badge ${file.source === 'gemini' ? 'badge-gemini' : 'badge-basic'}">
1144
+ ${file.source === 'gemini' ? 'Gemini AI' : 'Basic'}
1145
+ </span>
1146
+ </div>
1147
+ <p class="card-text">${metadata.description}</p>
1148
+
1149
+ <div class="card-keywords">
1150
+ ${metadata.keywords.slice(0, 5).map(keyword =>
1151
+ `<span class="keyword-chip">${keyword.trim()}</span>`
1152
+ ).join('')}
1153
+ ${metadata.keywords.length > 5 ? '<span class="keyword-chip">+'+ (metadata.keywords.length - 5) +'</span>' : ''}
1154
+ </div>
1155
+
1156
+ <div class="card-actions">
1157
+ <button class="copy-btn" onclick="copyMetadata(${index})">
1158
+ <i class="fas fa-copy"></i> Copy
1159
+ </button>
1160
+ <button class="regenerate-btn" onclick="regenerateMetadata(${index})">
1161
+ <i class="fas fa-sync-alt"></i> Regenerate
1162
+ </button>
1163
+ </div>
1164
+ </div>
1165
+ `;
1166
+
1167
+ resultGrid.appendChild(card);
1168
+ }
1169
+
1170
+ // Generate metadata using Gemini API
1171
+ async function generateMetadataWithGemini(imageDataUrl, file) {
1172
+ const category = document.getElementById('image-category').value;
1173
+ const style = document.getElementById('image-style').value;
1174
+ const platform = document.getElementById('target-platform').value;
1175
+ const language = document.getElementById('language').value;
1176
+
1177
+ const prompt = `
1178
+ Act as a professional microstock image analyst. Analyze this image and generate comprehensive metadata.
1179
+
1180
+ Please provide:
1181
+ 1. A title (60-100 characters) that is SEO-friendly, contains relevant keywords, and accurately describes the image
1182
+ 2. A concise description (70-150 characters) that highlights key visual elements and potential use cases
1183
+ 3. A list of 50 relevant keywords (comma-separated) that would help this image be discovered on stock platforms
1184
+
1185
+ Important considerations:
1186
+ - Image category: ${category}
1187
+ - Visual style: ${style}
1188
+ - Target platform: ${platform === 'all' ? 'general stock platforms' : platform}
1189
+ - Preferred language: ${language}
1190
+ - Keywords should be in English but relevant for the target language
1191
+
1192
+ Format your response as a valid JSON object with these fields:
1193
+ - "title": string
1194
+ - "description": string
1195
+ - "keywords": array of strings (maximum 50)
1196
+ `;
1197
+
1198
+ // Call Gemini API with the image and prompt
1199
+ const response = await callGeminiAPI(geminiApiKey, prompt, true, imageDataUrl);
1200
+
1201
+ // Process the API response
1202
+ if (!response.candidates || !response.candidates[0].content.parts[0].text) {
1203
+ throw new Error('Invalid response from Gemini API');
1204
+ }
1205
+
1206
+ // Extract the JSON from the response text
1207
+ const responseText = response.candidates[0].content.parts[0].text;
1208
+ let metadata;
1209
+
1210
+ try {
1211
+ // Find JSON in the response (Gemini might surround it with markdown or explanations)
1212
+ const jsonMatch = responseText.match(/\{[\s\S]*\}/);
1213
+ if (jsonMatch) {
1214
+ metadata = JSON.parse(jsonMatch[0]);
1215
+ } else {
1216
+ metadata = JSON.parse(responseText);
1217
+ }
1218
+
1219
+ // Validate the response structure
1220
+ if (!metadata.title || !metadata.description || !metadata.keywords) {
1221
+ throw new Error('Missing required fields in Gemini response');
1222
+ }
1223
+
1224
+ // Ensure keywords is an array and trim each keyword
1225
+ if (Array.isArray(metadata.keywords)) {
1226
+ metadata.keywords = metadata.keywords.map(k => k.trim());
1227
+ } else if (typeof metadata.keywords === 'string') {
1228
+ metadata.keywords = metadata.keywords.split(',').map(k => k.trim());
1229
+ }
1230
+
1231
+ // Limit to 50 keywords
1232
+ if (metadata.keywords.length > 50) {
1233
+ metadata.keywords = metadata.keywords.slice(0, 50);
1234
+ }
1235
+
1236
+ return metadata;
1237
+
1238
+ } catch (error) {
1239
+ console.error('Error parsing Gemini response:', error);
1240
+ console.log('Original response:', responseText);
1241
+ throw new Error('Failed to parse metadata from Gemini response');
1242
+ }
1243
+ }
1244
+
1245
+ // Generate dummy metadata based on image and settings
1246
+ function generateDummyMetadata(file) {
1247
+ // This is a fallback when Gemini is not available or fails
1248
+ // Similar implementation as before, but moved into its own function
1249
+ const category = document.getElementById('image-category').value;
1250
+ const style = document.getElementById('image-style').value;
1251
+ const platform = document.getElementById('target-platform').value;
1252
+
1253
+ const baseName = file.name.replace(/\.[^/.]+$/, "").replace(/[^a-zA-Z0-9]/g, ' ').trim();
1254
+
1255
+ // Generate title (50-100 chars)
1256
+ const title = `${capitalizeWords(baseName)} ${style} ${category} image for ${platform} stock`.substring(0, 75);
1257
+
1258
+ // Generate description (50-150 chars)
1259
+ const description = `High-quality ${style} ${category} stock image featuring ${baseName}. Perfect for ${platform} and commercial use in ${category} projects.`;
1260
+
1261
+ // Generate 50 keywords
1262
+ let keywords = [
1263
+ ...baseName.toLowerCase().split(' '),
1264
+ category, style, platform,
1265
+ 'stock photo', 'royalty free', 'commercial use',
1266
+ 'high resolution', 'premium', 'content', 'resource'
1267
+ ];
1268
+
1269
+ // Add some random elements to fill up to 50 keywords
1270
+ for (let i = keywords.length; i < 50; i++) {
1271
+ keywords.push(`keyword${i + 1}`);
1272
+ }
1273
+
1274
+ // Remove duplicates
1275
+ keywords = [...new Set(keywords)].slice(0, 50);
1276
+
1277
+ return {
1278
+ title: title,
1279
+ description: description,
1280
+ keywords: keywords
1281
+ };
1282
+ }
1283
+
1284
+ // Copy metadata to clipboard
1285
+ function copyMetadata(index) {
1286
+ const file = uploadedFiles[index];
1287
+ const metadata = file.metadata;
1288
+
1289
+ const textToCopy = `Title: ${metadata.title}\n\n` +
1290
+ `Description: ${metadata.description}\n\n` +
1291
+ `Keywords: ${metadata.keywords.join(', ')}`;
1292
+
1293
+ navigator.clipboard.writeText(textToCopy).then(() => {
1294
+ showApiStatus('Metadata copied to clipboard!', 'success');
1295
+ setTimeout(hideApiStatus, 3000);
1296
+ }).catch(err => {
1297
+ console.error('Failed to copy: ', err);
1298
+ showApiStatus('Failed to copy metadata', 'error');
1299
+ });
1300
+ }
1301
+
1302
+ // Regenerate metadata for a single image
1303
+ async function regenerateMetadata(index) {
1304
+ const category = document.getElementById('image-category').value;
1305
+ const style = document.getElementById('image-style').value;
1306
+ const platform = document.getElementById('target-platform').value;
1307
+ const language = document.getElementById('language').value;
1308
+
1309
+ if (useGeminiApi && !geminiApiKey) {
1310
+ showApiStatus('Gemini API is enabled but no API key is provided.', 'error');
1311
+ return;
1312
+ }
1313
+
1314
+ let newMetadata;
1315
+ let source = '';
1316
+
1317
+ if (useGeminiApi && geminiApiKey) {
1318
+ try {
1319
+ const reader = new FileReader();
1320
+
1321
+ await new Promise((resolve) => {
1322
+ reader.onload = async function(e) {
1323
+ const imageDataUrl = e.target.result;
1324
+ newMetadata = await generateMetadataWithGemini(imageDataUrl, uploadedFiles[index]);
1325
+ source = 'gemini';
1326
+ resolve();
1327
+ };
1328
+
1329
+ reader.readAsDataURL(uploadedFiles[index]);
1330
+ });
1331
+ } catch (error) {
1332
+ console.error('Gemini API error, falling back to basic analysis:', error);
1333
+ newMetadata = generateDummyMetadata(uploadedFiles[index]);
1334
+ source = 'basic';
1335
+ }
1336
+ } else {
1337
+ newMetadata = generateDummyMetadata(uploadedFiles[index]);
1338
+ source = 'basic';
1339
+ }
1340
+
1341
+ uploadedFiles[index].metadata = newMetadata;
1342
+ uploadedFiles[index].source = source;
1343
+
1344
+ // Update the UI
1345
+ const cards = document.querySelectorAll('.image-card');
1346
+ if (cards[index]) {
1347
+ const cardBody = cards[index].querySelector('.card-body');
1348
+ if (cardBody) {
1349
+ cardBody.querySelector('.card-title').textContent = newMetadata.title;
1350
+ cardBody.querySelector('.card-text').textContent = newMetadata.description;
1351
+ cardBody.querySelector('.badge').className = `badge ${source === 'gemini' ? 'badge-gemini' : 'badge-basic'}`;
1352
+ cardBody.querySelector('.badge').textContent = source === 'gemini' ? 'Gemini AI' : 'Basic';
1353
+
1354
+ const keywordsContainer = cardBody.querySelector('.card-keywords');
1355
+ keywordsContainer.innerHTML = `
1356
+ ${newMetadata.keywords.slice(0, 5).map(keyword =>
1357
+ `<span class="keyword-chip">${keyword.trim()}</span>`
1358
+ ).join('')}
1359
+ ${newMetadata.keywords.length > 5 ? '<span class="keyword-chip">+'+ (newMetadata.keywords.length - 5) +'</span>' : ''}
1360
+ `;
1361
+ }
1362
+ }
1363
+
1364
+ showApiStatus(`${source === 'gemini' ? 'Gemini' : 'Basic'} metadata regenerated for this image.`, 'success');
1365
+ setTimeout(hideApiStatus, 3000);
1366
+ }
1367
+
1368
+ // Regenerate metadata for all images
1369
+ async function regenerateAllMetadata() {
1370
+ if (uploadedFiles.length === 0) return;
1371
+
1372
+ if (useGeminiApi && !geminiApiKey) {
1373
+ showApiStatus('Gemini API is enabled but no API key is provided.', 'error');
1374
+ return;
1375
+ }
1376
+
1377
+ loadingIndicator.style.display = 'block';
1378
+ loadingText.textContent = 'Regenerating metadata for all images...';
1379
+ generateBtn.disabled = true;
1380
+ regenerateAllBtn.disabled = true;
1381
+
1382
+ // Process images sequentially
1383
+ for (let i = 0; i < uploadedFiles.length; i++) {
1384
+ await regenerateMetadata(i);
1385
+ }
1386
+
1387
+ loadingIndicator.style.display = 'none';
1388
+ generateBtn.disabled = false;
1389
+ regenerateAllBtn.disabled = false;
1390
+ }
1391
+
1392
+ // Apply bulk edits to all images
1393
+ function applyBulkEdit() {
1394
+ const bulkTitle = document.getElementById('bulk-title').value.trim();
1395
+ const bulkDescription = document.getElementById('bulk-description').value.trim();
1396
+ const bulkKeywords = document.getElementById('bulk-keywords').value.trim();
1397
+
1398
+ if (!bulkTitle && !bulkDescription && !bulkKeywords) {
1399
+ showApiStatus('Please enter at least one field to apply bulk changes', 'error');
1400
+ return;
1401
+ }
1402
+
1403
+ uploadedFiles.forEach((file, index) => {
1404
+ if (!file.metadata) {
1405
+ file.metadata = generateDummyMetadata(file);
1406
+ }
1407
+
1408
+ if (bulkTitle) {
1409
+ file.metadata.title = bulkTitle;
1410
+ }
1411
+
1412
+ if (bulkDescription) {
1413
+ file.metadata.description = bulkDescription;
1414
+ }
1415
+
1416
+ if (bulkKeywords) {
1417
+ file.metadata.keywords = bulkKeywords.split(',').map(k => k.trim()).filter(k => k);
1418
+ }
1419
+
1420
+ // Mark as manually edited
1421
+ file.source = 'manual';
1422
+
1423
+ // Update the UI if these cards exist
1424
+ const cards = document.querySelectorAll('.image-card');
1425
+ if (cards[index]) {
1426
+ const cardBody = cards[index].querySelector('.card-body');
1427
+ if (cardBody) {
1428
+ if (bulkTitle) {
1429
+ cardBody.querySelector('.card-title').textContent = file.metadata.title;
1430
+ }
1431
+ if (bulkDescription) {
1432
+ cardBody.querySelector('.card-text').textContent = file.metadata.description;
1433
+ }
1434
+ if (bulkKeywords) {
1435
+ const keywordsContainer = cardBody.querySelector('.card-keywords');
1436
+ keywordsContainer.innerHTML = `
1437
+ ${file.metadata.keywords.slice(0, 5).map(keyword =>
1438
+ `<span class="keyword-chip">${keyword.trim()}</span>`
1439
+ ).join('')}
1440
+ ${file.metadata.keywords.length > 5 ? '<span class="keyword-chip">+'+ (file.metadata.keywords.length - 5) +'</span>' : ''}
1441
+ `;
1442
+ }
1443
+ cardBody.querySelector('.badge').className = 'badge badge-basic';
1444
+ cardBody.querySelector('.badge').textContent = 'Manual';
1445
+ }
1446
+ }
1447
+ });
1448
+
1449
+ showApiStatus('Bulk changes applied to all images!', 'success');
1450
+ setTimeout(hideApiStatus, 3000);
1451
+ }
1452
+
1453
+ // Download all metadata as CSV
1454
+ function downloadAllAsCSV() {
1455
+ let csvContent = "data:text/csv;charset=utf-8,";
1456
+
1457
+ // CSV header
1458
+ csvContent += "Filename,Title,Description,Keywords,Source\n";
1459
+
1460
+ // Add rows
1461
+ uploadedFiles.forEach(file => {
1462
+ if (!file.metadata) {
1463
+ file.metadata = generateDummyMetadata(file);
1464
+ }
1465
+
1466
+ const metadata = file.metadata;
1467
+ const row = [
1468
+ file.name,
1469
+ `"${metadata.title}"`,
1470
+ `"${metadata.description}"`,
1471
+ `"${metadata.keywords.join(', ')}"`,
1472
+ file.source || 'basic'
1473
+ ].join(',');
1474
+
1475
+ csvContent += row + "\n";
1476
+ });
1477
+
1478
+ // Create download link
1479
+ const encodedUri = encodeURI(csvContent);
1480
+ const link = document.createElement("a");
1481
+ link.setAttribute("href", encodedUri);
1482
+ link.setAttribute("download", "microstock_metadata.csv");
1483
+ document.body.appendChild(link);
1484
+
1485
+ // Trigger download
1486
+ link.click();
1487
+ document.body.removeChild(link);
1488
+ }
1489
+
1490
+ // Helper function to capitalize words
1491
+ function capitalizeWords(str) {
1492
+ return str.split(' ').map(word =>
1493
+ word.charAt(0).toUpperCase() + word.slice(1)
1494
+ ).join(' ');
1495
+ }
1496
+
1497
+ // Initialize the app
1498
+ init();
1499
+ </script>
1500
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body>
1501
+ </html>