Skip to content

Commit 12ce545

Browse files
committed
feat: HuggingFace image generation (SDXL + FLUX.1) + game gen improvements
- Add Stable Diffusion XL and FLUX.1 Schnell via HuggingFace Inference API - New ai-worker-hf-image.js Web Worker for HF image generation - Register hf-sdxl and hf-flux as cloud-image models in ai-models.js - Add API_KEY_HF storage key for HuggingFace tokens - Default Image tag model changed from imagen-ultra to hf-sdxl (free tier) - Add Save/Download button for generated images (ai-image.js) - Short gen-img: placeholder IDs in editor instead of base64 (renderer.js) - Add maxTokensOverride to requestAiTask API for game generation (4096 tokens) - Fix game prompt parsing regexes and HTML fence extraction - Delete image-gen-test.html comparison page
1 parent 11ccd7c commit 12ce545

20 files changed

Lines changed: 1124 additions & 43 deletions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# CHANGELOG — HuggingFace Image Generation + Game Gen Improvements
2+
3+
**Date:** 2026-03-16
4+
5+
## 🖼️ HuggingFace Image Generation (SDXL + FLUX.1)
6+
7+
Integrated **Stable Diffusion XL** and **FLUX.1 Schnell** as new image generation models via the free HuggingFace Inference API, alongside existing Imagen 4 Ultra and Nano Banana 2.
8+
9+
### New Files
10+
- `public/ai-worker-hf-image.js` — Web Worker for HuggingFace Inference API image generation (supports both SDXL and FLUX.1 via `workerModelId`)
11+
12+
### Modified Files
13+
- `js/storage-keys.js` — Added `API_KEY_HF` storage key for HuggingFace tokens
14+
- `js/ai-models.js` — Registered `hf-sdxl` (Stable Diffusion XL) and `hf-flux` (FLUX.1 Schnell) as cloud-image models
15+
- `js/ai-docgen.js` — Default Image tag model changed from `imagen-ultra` to `hf-sdxl` (free tier)
16+
- `js/ai-docgen-generate.js` — Image generation fallback updated to `hf-sdxl`; images now stored in memory registry with short `gen-img:` IDs instead of inline base64
17+
- `js/ai-image.js` — Added ⬇ **Save** button for downloading generated images as PNG; fallback model updated to `hf-sdxl`; images use short `gen-img:` placeholder IDs in editor
18+
- `js/renderer.js` — Custom image renderer resolves `gen-img:` URLs from in-memory registry to actual data URIs
19+
- `js/run-requirements.js` — Updated specialized models list and Image fallback to `hf-sdxl`
20+
- `tests/feature/model-tag.spec.js` — Updated Image tag default model test to expect `hf-sdxl`
21+
22+
### Deleted Files
23+
- `image-gen-test.html` — Removed comparison test page (no longer needed)
24+
25+
## 🎮 Game Generation Improvements
26+
27+
- `js/ai-assistant.js` — Added `maxTokensOverride` parameter to `requestAiTask()` API
28+
- `public/ai-worker.js``generate()` now accepts `maxTokensOverride` to override default token limits
29+
- `public/ai-worker-groq.js`, `public/ai-worker-gemini.js`, `public/ai-worker-lfm.js`, `public/ai-worker-openrouter.js` — Same `maxTokensOverride` support
30+
- `js/game-docgen.js` — Fixed prompt parsing regexes for single-line tags; improved HTML fence extraction; added debug logging; uses `maxTokensOverride: 4096` for game generation

css/game-docgen.css

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,135 @@
429429
.dark-mode .ai-game-import-hint {
430430
color: #9ca3af;
431431
}
432+
433+
/* ─── Skills Picker Dropdown ─── */
434+
.ai-game-skill-select {
435+
border: 1px solid rgba(16, 185, 129, 0.3);
436+
border-radius: 6px;
437+
padding: 3px 6px;
438+
font-size: 0.75rem;
439+
background: transparent;
440+
color: inherit;
441+
max-width: 150px;
442+
cursor: pointer;
443+
transition: border-color 0.2s;
444+
}
445+
.ai-game-skill-select:hover {
446+
border-color: #10b981;
447+
}
448+
.ai-game-skill-select:focus {
449+
outline: none;
450+
border-color: #10b981;
451+
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.15);
452+
}
453+
454+
/* ─── Skill Pills ─── */
455+
.ai-game-skill-pills {
456+
display: flex;
457+
flex-wrap: wrap;
458+
gap: 6px;
459+
padding: 8px 14px;
460+
background: rgba(16, 185, 129, 0.04);
461+
border-bottom: 1px solid rgba(16, 185, 129, 0.1);
462+
}
463+
464+
.ai-game-skill-pill {
465+
display: inline-flex;
466+
align-items: center;
467+
gap: 4px;
468+
padding: 3px 10px;
469+
border-radius: 14px;
470+
background: rgba(16, 185, 129, 0.1);
471+
color: #059669;
472+
font-size: 0.72rem;
473+
font-weight: 500;
474+
cursor: default;
475+
transition: all 0.15s;
476+
position: relative;
477+
user-select: none;
478+
}
479+
.ai-game-skill-pill:hover {
480+
background: rgba(16, 185, 129, 0.18);
481+
}
482+
483+
.ai-game-skill-pill-remove {
484+
border: none;
485+
background: none;
486+
color: #6b7280;
487+
cursor: pointer;
488+
font-size: 0.65rem;
489+
padding: 0 2px;
490+
line-height: 1;
491+
border-radius: 50%;
492+
transition: all 0.15s;
493+
display: inline-flex;
494+
align-items: center;
495+
margin-left: 2px;
496+
}
497+
.ai-game-skill-pill-remove:hover {
498+
color: #ef4444;
499+
background: rgba(239, 68, 68, 0.1);
500+
}
501+
502+
/* ─── Skill Tooltip ─── */
503+
.ai-game-skill-tooltip {
504+
position: absolute;
505+
bottom: calc(100% + 8px);
506+
left: 50%;
507+
transform: translateX(-50%);
508+
background: #1f2937;
509+
color: #e5e7eb;
510+
padding: 8px 12px;
511+
border-radius: 8px;
512+
font-size: 0.72rem;
513+
font-weight: 400;
514+
line-height: 1.4;
515+
white-space: normal;
516+
min-width: 200px;
517+
max-width: 300px;
518+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
519+
z-index: 100;
520+
pointer-events: none;
521+
opacity: 0;
522+
transition: opacity 0.15s;
523+
}
524+
.ai-game-skill-tooltip.visible {
525+
opacity: 1;
526+
}
527+
.ai-game-skill-tooltip::after {
528+
content: '';
529+
position: absolute;
530+
top: 100%;
531+
left: 50%;
532+
transform: translateX(-50%);
533+
border: 5px solid transparent;
534+
border-top-color: #1f2937;
535+
}
536+
537+
/* ─── Dark Mode: Skills ─── */
538+
[data-theme="dark"] .ai-game-skill-select,
539+
.dark-mode .ai-game-skill-select {
540+
border-color: rgba(16, 185, 129, 0.4);
541+
}
542+
543+
[data-theme="dark"] .ai-game-skill-pills,
544+
.dark-mode .ai-game-skill-pills {
545+
background: rgba(16, 185, 129, 0.06);
546+
border-bottom-color: rgba(16, 185, 129, 0.15);
547+
}
548+
549+
[data-theme="dark"] .ai-game-skill-pill,
550+
.dark-mode .ai-game-skill-pill {
551+
background: rgba(16, 185, 129, 0.15);
552+
color: #34d399;
553+
}
554+
555+
[data-theme="dark"] .ai-game-skill-pill:hover,
556+
.dark-mode .ai-game-skill-pill:hover {
557+
background: rgba(16, 185, 129, 0.25);
558+
}
559+
560+
[data-theme="dark"] .ai-game-skill-pill-remove,
561+
.dark-mode .ai-game-skill-pill-remove {
562+
color: #6b7280;
563+
}

js/ai-assistant.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,7 +1171,7 @@
11711171
// ===========================================================
11721172
// Public AI Request API — for non-chat modules (e.g. ai-docgen)
11731173
// ===========================================================
1174-
M.requestAiTask = function ({ taskType, context, userPrompt, enableThinking, onToken, silent, attachments }) {
1174+
M.requestAiTask = function ({ taskType, context, userPrompt, enableThinking, onToken, silent, attachments, maxTokensOverride }) {
11751175
return new Promise(function (resolve, reject) {
11761176
// Block if another generation is in progress
11771177
if (aiIsGenerating) {
@@ -1233,7 +1233,8 @@
12331233
userPrompt: userPrompt || '',
12341234
messageId: messageId,
12351235
enableThinking: !!enableThinking,
1236-
attachments: attachments || []
1236+
attachments: attachments || [],
1237+
maxTokensOverride: maxTokensOverride || 0
12371238
});
12381239
});
12391240
};

js/ai-docgen-generate.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@
360360

361361
// Generate image for an {{Image:}} block via the image worker
362362
async function generateImageForBlock(block, modelId) {
363-
var imageModelId = modelId || 'imagen-ultra';
363+
var imageModelId = modelId || 'hf-sdxl';
364364
var providers = M.getCloudProviders ? M.getCloudProviders() : {};
365365
var provider = providers[imageModelId];
366366

@@ -389,7 +389,12 @@
389389
if (data.type === 'image-complete') {
390390
worker.removeEventListener('message', onMessage);
391391
var mime = data.mimeType || 'image/png';
392-
var md = '![' + block.prompt.substring(0, 60) + '](data:' + mime + ';base64,' + data.imageBase64 + ')';
392+
// Store image in registry with short ID for clean editor text
393+
var dataUri = 'data:' + mime + ';base64,' + data.imageBase64;
394+
if (!M._genImages) M._genImages = {};
395+
var genId = Math.random().toString(36).substring(2, 10);
396+
M._genImages[genId] = dataUri;
397+
var md = '![' + block.prompt.substring(0, 60) + '](gen-img:' + genId + ')';
393398
resolve(md);
394399
} else if (data.type === 'image-error') {
395400
worker.removeEventListener('message', onMessage);

js/ai-docgen.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@
118118
var aiDefaultModel = (M.getCurrentAiModel ? M.getCurrentAiModel() : '') || '';
119119
M.wrapSelectionWith('{{@AI:\n @model: ' + (aiDefaultModel || 'qwen-local') + '\n @prompt: ', '\n}}', placeholder);
120120
} else if (type === 'Image') {
121-
M.wrapSelectionWith('{{@Image:\n @model: imagen-ultra\n @prompt: ', '\n}}', placeholder);
121+
M.wrapSelectionWith('{{@Image:\n @model: hf-sdxl\n @prompt: ', '\n}}', placeholder);
122122
} else {
123123
M.wrapSelectionWith('{{@' + type + ': ', '}}', placeholder);
124124
}

js/ai-image.js

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
function getImagenWorker() {
6060
if (imagenWorker && imagenWorkerReady) return imagenWorker;
6161

62-
var imagenConfig = window.AI_MODELS?.['imagen-ultra'];
62+
var imagenConfig = window.AI_MODELS?.['hf-sdxl'];
6363
if (!imagenConfig) return null;
6464

6565
var geminiKey = localStorage.getItem(imagenConfig.keyStorageKey);
@@ -104,7 +104,12 @@
104104
if (welcome) welcome.remove();
105105

106106
var dataUri = 'data:' + (mimeType || 'image/png') + ';base64,' + imageBase64;
107-
var mdText = '![' + prompt + '](' + dataUri + ')';
107+
108+
// Store image in registry with short ID for clean editor text
109+
if (!M._genImages) M._genImages = {};
110+
var genId = Math.random().toString(36).substring(2, 10);
111+
M._genImages[genId] = dataUri;
112+
var mdText = '![' + prompt + '](gen-img:' + genId + ')';
108113

109114
var msg = document.createElement('div');
110115
msg.className = 'ai-message ai-message-ai';
@@ -121,6 +126,9 @@
121126
' <button class="ai-msg-action-btn ai-img-copy-btn" title="Copy markdown">\n' +
122127
' <i class="bi bi-clipboard"></i> Copy MD\n' +
123128
' </button>\n' +
129+
' <button class="ai-msg-action-btn ai-img-save-btn" title="Download image as PNG">\n' +
130+
' <i class="bi bi-download"></i> Save\n' +
131+
' </button>\n' +
124132
' </div>\n';
125133
aiChatArea.appendChild(msg);
126134
aiChatArea.scrollTop = aiChatArea.scrollHeight;
@@ -145,13 +153,33 @@
145153
setTimeout(function () { self.innerHTML = '<i class="bi bi-clipboard"></i> Copy MD'; }, 1500);
146154
});
147155
});
156+
157+
// Wire up Save button — downloads the image as a PNG file
158+
msg.querySelector('.ai-img-save-btn').addEventListener('click', function () {
159+
var self = this;
160+
try {
161+
var a = document.createElement('a');
162+
a.href = dataUri;
163+
// Generate a safe filename from the prompt
164+
var safeName = prompt.replace(/[^a-zA-Z0-9 ]/g, '').trim().replace(/\s+/g, '_').substring(0, 40) || 'generated_image';
165+
var ext = (mimeType || 'image/png').split('/')[1] || 'png';
166+
a.download = safeName + '.' + ext;
167+
document.body.appendChild(a);
168+
a.click();
169+
document.body.removeChild(a);
170+
self.innerHTML = '<i class="bi bi-check-lg"></i> Saved';
171+
setTimeout(function () { self.innerHTML = '<i class="bi bi-download"></i> Save'; }, 1500);
172+
} catch (err) {
173+
M.showToast('Failed to save image: ' + err.message, 'error');
174+
}
175+
});
148176
}
149177

150178
// --- Generate image from prompt ---
151179
function generateImage(prompt, aspectRatio) {
152180
var currentModel = _ai.currentModel;
153181
var currentModelCfg = _ai.models[currentModel];
154-
var imageModelId = (currentModelCfg && currentModelCfg.isImageModel) ? currentModel : 'imagen-ultra';
182+
var imageModelId = (currentModelCfg && currentModelCfg.isImageModel) ? currentModel : 'hf-sdxl';
155183
var provider = _ai.CLOUD_PROVIDERS[imageModelId];
156184

157185
if (!provider) {
@@ -227,13 +255,13 @@
227255
var imageChip = document.getElementById('ai-image-chip');
228256
if (imageChip) {
229257
imageChip.addEventListener('click', function () {
230-
var imagenConfig = window.AI_MODELS?.['imagen-ultra'];
258+
var imagenConfig = window.AI_MODELS?.['hf-sdxl'];
231259
if (!imagenConfig) return;
232260

233261
var geminiKey = localStorage.getItem(imagenConfig.keyStorageKey);
234262
if (!geminiKey) {
235263
if (!_ai.panelOpen) M.openAiPanel();
236-
_ai.showApiKeyModal('imagen-ultra');
264+
_ai.showApiKeyModal('hf-sdxl');
237265
return;
238266
}
239267
showImageModal();

js/ai-models.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,48 @@
172172
category: 'cloud-image',
173173
},
174174

175+
// ── Cloud: Stable Diffusion XL (HuggingFace) ──────────
176+
'hf-sdxl': {
177+
label: 'SDXL · HuggingFace',
178+
badge: 'SDXL',
179+
icon: 'bi bi-brush',
180+
isImageModel: true,
181+
statusReady: 'Stable Diffusion XL · HuggingFace',
182+
workerFile: 'ai-worker-hf-image.js',
183+
workerModelId: 'stabilityai/stable-diffusion-xl-base-1.0',
184+
keyStorageKey: window.MDView.KEYS.API_KEY_HF,
185+
dialogTitle: 'Connect to HuggingFace',
186+
dialogDesc: 'Enter your free HuggingFace token to use <strong>Stable Diffusion XL</strong> image generation',
187+
dialogPlaceholder: 'hf_xxxxxxxxxxxxxxxxxxxx',
188+
dialogLink: 'https://huggingface.co/settings/tokens',
189+
dialogLinkText: 'huggingface.co/settings/tokens',
190+
dialogIcon: 'bi bi-brush',
191+
dropdownName: 'Stable Diffusion XL',
192+
dropdownDesc: 'HuggingFace · Free tier · Stability AI',
193+
category: 'cloud-image',
194+
},
195+
196+
// ── Cloud: FLUX.1 Schnell (HuggingFace) ──────────────
197+
'hf-flux': {
198+
label: 'FLUX.1 Schnell · HuggingFace',
199+
badge: 'FLUX.1',
200+
icon: 'bi bi-palette2',
201+
isImageModel: true,
202+
statusReady: 'FLUX.1 Schnell · HuggingFace',
203+
workerFile: 'ai-worker-hf-image.js',
204+
workerModelId: 'black-forest-labs/FLUX.1-schnell',
205+
keyStorageKey: window.MDView.KEYS.API_KEY_HF,
206+
dialogTitle: 'Connect to HuggingFace',
207+
dialogDesc: 'Enter your free HuggingFace token to use <strong>FLUX.1 Schnell</strong> image generation',
208+
dialogPlaceholder: 'hf_xxxxxxxxxxxxxxxxxxxx',
209+
dialogLink: 'https://huggingface.co/settings/tokens',
210+
dialogLinkText: 'huggingface.co/settings/tokens',
211+
dialogIcon: 'bi bi-palette2',
212+
dropdownName: 'FLUX.1 Schnell',
213+
dropdownDesc: 'HuggingFace · Free tier · Black Forest Labs',
214+
category: 'cloud-image',
215+
},
216+
175217
// ── Cloud: Grok 4.1 Fast via OpenRouter ───────────────
176218
'openrouter-grok': {
177219
label: 'Grok 4.1 Fast · xAI',

0 commit comments

Comments
 (0)