refactor: Avatar-Editor als wiederverwendbares Modal-Modul

- Neues avatar-editor.js: Upload, Paste, AI-Generate, Scale, Clear in einem Modal
- Session + Agent Pages: ~280 Zeilen doppelter Avatar-Code → je 5 Zeilen AvatarEditor.init/destroy
- Avatar-Preview klickbar → öffnet Modal mit allen Einstellungen
- chat.css: Shared-Content Message-Styling (max-width, border)
This commit is contained in:
Meleki committed 2026-03-06 11:55:00 +01:00
1 parent 057d37d76f
commit 34cecd03a1
6 files changed
+384 -281

No files matched your search

+261
View File
@@ -0,0 +1,261 @@
// ── Avatar Editor Modal ──
// Wiederverwendbar für Session + Agent Avatar-Bearbeitung.
//
// Usage:
// const editor = AvatarEditor.init({
// previewEl: document.getElementById('myPreview'),
// initialImage: 'data:image/...' || null,
// initialScale: 1.5,
// onChange(imageData, scale) { ... }
// });
// // Cleanup:
// editor.destroy();
// // Get current values:
// editor.getImage(); // data-url or null
// editor.getScale(); // number
var AvatarEditor = (function () {
function init(opts) {
var previewEl = opts.previewEl;
var imageData = opts.initialImage || null;
var scale = opts.initialScale || 1;
var onChange = opts.onChange || function () {};
var overlay = null;
var pasteHandler = null;
var kreaAvailable = false;
// Krea-Status prüfen
apiKreaStatus().then(function (s) { kreaAvailable = s.configured; }).catch(function () {});
// Preview rendern
function renderPreview() {
if (imageData) {
previewEl.innerHTML = '<img src="' + imageData + '" alt="" style="scale: ' + scale + '">';
previewEl.classList.add('has-image');
} else {
previewEl.innerHTML = '?';
previewEl.classList.remove('has-image');
}
}
renderPreview();
// Click → Modal öffnen
previewEl.style.cursor = 'pointer';
previewEl.addEventListener('click', openModal);
// Paste Handler (global, immer aktiv)
document.addEventListener('paste', pasteHandler = function (e) {
if (overlay) return; // Modal offen → Paste dort behandeln
var items = e.clipboardData && e.clipboardData.items;
if (!items) return;
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image/') === 0) {
e.preventDefault();
readFile(items[i].getAsFile());
return;
}
}
});
function readFile(file) {
if (!file) return;
var reader = new FileReader();
reader.onload = function () {
imageData = reader.result;
renderPreview();
onChange(imageData, scale);
if (overlay) refreshModal();
};
reader.readAsDataURL(file);
}
function setImage(data) {
imageData = data;
scale = 1;
renderPreview();
onChange(imageData, scale);
}
function openModal() {
if (overlay) return;
overlay = document.createElement('div');
overlay.className = 'av-overlay';
overlay.innerHTML =
'<div class="av-modal">' +
'<div class="av-header">' +
'<h3>Avatar bearbeiten</h3>' +
'<button class="icon-btn av-close"><i class="fa fa-close"></i></button>' +
'</div>' +
'<div class="av-body">' +
'<div class="av-preview-large">' +
(imageData ? '<img src="' + imageData + '" alt="" style="scale: ' + scale + '">' : '?') +
'</div>' +
'<div class="av-controls">' +
'<div class="av-scale-row"' + (imageData ? '' : ' hidden') + '>' +
'<label>Zoom</label>' +
'<input type="range" class="av-scale" min="0.5" max="3" step="0.1" value="' + scale + '">' +
'<span class="av-scale-value">' + scale + '×</span>' +
'</div>' +
'<div class="av-actions">' +
'<button class="av-upload-btn"><i class="fa fa-upload"></i> Hochladen</button>' +
'<input type="file" class="av-file-input" accept="image/*" hidden>' +
'<button class="av-clear-btn danger-btn"' + (imageData ? '' : ' hidden') + '><i class="fa fa-trash"></i> Entfernen</button>' +
'</div>' +
'<div class="av-ai-section"' + (kreaAvailable ? '' : ' hidden') + '>' +
'<div class="input-with-btn">' +
'<input type="text" class="av-ai-prompt" placeholder="Bildbeschreibung eingeben...">' +
'<button type="button" class="icon-btn av-ai-btn" title="Bild generieren"><i class="fa fa-magic"></i></button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
document.body.appendChild(overlay);
// Refs
var modal = overlay.querySelector('.av-modal');
var closeBtn = overlay.querySelector('.av-close');
var previewLarge = overlay.querySelector('.av-preview-large');
var scaleRow = overlay.querySelector('.av-scale-row');
var scaleInput = overlay.querySelector('.av-scale');
var scaleValueEl = overlay.querySelector('.av-scale-value');
var uploadBtn = overlay.querySelector('.av-upload-btn');
var fileInput = overlay.querySelector('.av-file-input');
var clearBtn = overlay.querySelector('.av-clear-btn');
var aiPrompt = overlay.querySelector('.av-ai-prompt');
var aiBtn = overlay.querySelector('.av-ai-btn');
// Close
closeBtn.addEventListener('click', closeModal);
overlay.addEventListener('click', function (e) {
if (e.target === overlay) closeModal();
});
// Scale
scaleInput.addEventListener('input', function () {
scale = parseFloat(scaleInput.value);
scaleValueEl.textContent = scale + '×';
var img = previewLarge.querySelector('img');
if (img) img.style.scale = scale;
renderPreview();
onChange(imageData, scale);
});
// Upload
uploadBtn.addEventListener('click', function () { fileInput.click(); });
fileInput.addEventListener('change', function () {
if (fileInput.files[0]) readFile(fileInput.files[0]);
});
// Clear
clearBtn.addEventListener('click', function () {
imageData = null;
scale = 1;
scaleInput.value = 1;
scaleValueEl.textContent = '1×';
renderPreview();
onChange(imageData, scale);
refreshModal();
});
// AI Generate
aiBtn.addEventListener('click', generateAi);
aiPrompt.addEventListener('keydown', function (e) {
if (e.key === 'Enter') { e.preventDefault(); generateAi(); }
});
async function generateAi() {
var prompt = aiPrompt.value.trim();
if (!prompt) return;
aiBtn.disabled = true;
aiBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
try {
var result = await apiKreaGenerate(prompt);
if (result.files && result.files.length > 0) {
var imgResp = await fetch('/api/images/file/' + result.files[0]);
var blob = await imgResp.blob();
var reader = new FileReader();
reader.onload = function () {
imageData = reader.result;
scale = 1;
scaleInput.value = 1;
scaleValueEl.textContent = '1×';
renderPreview();
onChange(imageData, scale);
refreshModal();
};
reader.readAsDataURL(blob);
}
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
} finally {
aiBtn.disabled = false;
aiBtn.innerHTML = '<i class="fa fa-magic"></i>';
}
}
// Paste im Modal
modal.addEventListener('paste', function (e) {
var items = e.clipboardData && e.clipboardData.items;
if (!items) return;
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image/') === 0) {
e.preventDefault();
readFile(items[i].getAsFile());
return;
}
}
});
// Escape
overlay._keyHandler = function (e) {
if (e.key === 'Escape') closeModal();
};
document.addEventListener('keydown', overlay._keyHandler);
}
function refreshModal() {
if (!overlay) return;
var previewLarge = overlay.querySelector('.av-preview-large');
var scaleRow = overlay.querySelector('.av-scale-row');
var clearBtn = overlay.querySelector('.av-clear-btn');
if (imageData) {
previewLarge.innerHTML = '<img src="' + imageData + '" alt="" style="scale: ' + scale + '">';
scaleRow.hidden = false;
clearBtn.hidden = false;
} else {
previewLarge.innerHTML = '?';
scaleRow.hidden = true;
clearBtn.hidden = true;
}
}
function closeModal() {
if (!overlay) return;
document.removeEventListener('keydown', overlay._keyHandler);
overlay.remove();
overlay = null;
}
// Public API
return {
getImage: function () { return imageData; },
getScale: function () { return scale; },
destroy: function () {
closeModal();
if (pasteHandler) {
document.removeEventListener('paste', pasteHandler);
pasteHandler = null;
}
previewEl.removeEventListener('click', openModal);
previewEl.style.cursor = '';
}
};
}
return { init: init };
})();