feat: Avatar Scale (0.5×–3×) + Krea AI Bildgenerierung für Avatare

Backend:
- SessionRecord + AgentRecord: neues Feld AvatarScale (double, Default 1.0)
- DatabaseService: avatar_scale Migration, in Create/Update/GetWithStats
- SessionEndpoint + AgentEndpoint: avatarScale in Create/Update
- api.js: apiKreaStatus(), apiKreaGenerate()

Frontend:
- Session + Agent Templates: Range-Slider (0.5×–3×) in Avatar-Section
- Krea AI Generate: Prompt-Input + Magic-Button (nur wenn KreaToken konfiguriert)
- Live-Preview: transform: scale() auf Avatar-Vorschau
- Dashboard: scale() auf Session- und Agent-Card-Images
- form.css: avatar-scale-row + avatar-ai-section Styling
This commit is contained in:
Meleki committed 2026-03-06 10:59:09 +01:00
1 parent 4ebbeaa818
commit 057d37d76f
11 files changed
+221 -16

No files matched your search

+28 -2
View File
@@ -22,8 +22,8 @@
}
.avatar-preview {
width: 64px;
height: 64px;
width: 8em;
height: 8em;
background: var(--f2-bg);
border: var(--border-width) solid var(--f2-border);
display: flex;
@@ -38,6 +38,32 @@
width: 100%;
height: 100%;
object-fit: cover;
transition: scale var(--transition-duration);
}
.avatar-scale-row {
display: flex;
align-items: center;
gap: var(--badge-spacing);
flex: 1;
padding: var(--box-padding);
}
.avatar-scale-row label {
margin: 0;
}
.avatar-scale-row input[type="range"] {
flex: 1;
accent-color: var(--primary);
}
.avatar-scale-row span {
font-size: 0.85em;
min-width: 2.5em;
text-align: right;
opacity: 0.7;
}
.avatar-ai-section {
width: 100%;
}
.input-with-btn {
+22
View File
@@ -106,6 +106,17 @@
<button id="sessionImageBtn">Bild ändern</button>
<button id="sessionImageClearBtn">Entfernen</button>
</div>
<div class="avatar-scale-row" id="sessionScaleRow" hidden>
<label>Zoom</label>
<input type="range" id="sessionAvatarScale" min="0.5" max="3" step="0.1" value="1">
<span id="sessionScaleValue">1×</span>
</div>
<div class="avatar-ai-section" id="sessionAiSection" hidden>
<div class="input-with-btn">
<input type="text" id="sessionAiPrompt" placeholder="Bildbeschreibung eingeben...">
<button type="button" id="sessionAiGenerateBtn" class="icon-btn" title="Bild generieren"><i class="fa fa-magic"></i></button>
</div>
</div>
<label>Name</label>
<input type="text" id="sessionName" placeholder="Name der Session">
<label>Agent</label>
@@ -164,6 +175,17 @@
<button id="avatarBtn">Avatar ändern</button>
<button id="avatarClearBtn">Entfernen</button>
</div>
<div class="avatar-scale-row" id="agentScaleRow" hidden>
<label>Zoom</label>
<input type="range" id="agentAvatarScale" min="0.5" max="3" step="0.1" value="1">
<span id="agentScaleValue">1×</span>
</div>
<div class="avatar-ai-section" id="agentAiSection" hidden>
<div class="input-with-btn">
<input type="text" id="agentAiPrompt" placeholder="Bildbeschreibung eingeben...">
<button type="button" id="agentAiGenerateBtn" class="icon-btn" title="Bild generieren"><i class="fa fa-magic"></i></button>
</div>
</div>
<label>Name</label>
<input type="text" id="agentName" placeholder="Name des Agents">
<label>Model</label>
+21
View File
@@ -472,6 +472,27 @@ async function apiAdminImpersonate(userId) {
return resp.json();
}
// ── Krea Image API ──
async function apiKreaStatus() {
const resp = await fetch('/api/images/status');
if (!resp.ok) return { configured: false };
return resp.json();
}
async function apiKreaGenerate(prompt, model, width, height) {
const resp = await fetch('/api/images/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, model: model || 'flux', width: width || 512, height: height || 512, batchSize: 1 })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Bildgenerierung fehlgeschlagen');
}
return resp.json();
}
// ── Backup API ──
async function apiBackupDb() {
+63 -1
View File
@@ -38,8 +38,19 @@ registerPage('agent', {
const skillList = document.getElementById('agentSkillList');
const more = document.querySelector('.more');
const scaleRow = document.getElementById('agentScaleRow');
const scaleInput = document.getElementById('agentAvatarScale');
const scaleValue = document.getElementById('agentScaleValue');
_agentAvatarData = null;
function updatePreviewScale() {
var img = avatarPreview.querySelector('img');
if (img) img.style.scale = scaleInput.value;
scaleValue.textContent = scaleInput.value + '×';
}
scaleInput.addEventListener('input', updatePreviewScale);
if (agent) {
title.textContent = agent.name;
nameEl.value = agent.name || '';
@@ -55,8 +66,14 @@ registerPage('agent', {
_agentAvatarData = agent.avatar || null;
deleteBtn.hidden = false;
if (agent.avatarScale && agent.avatarScale !== 1) {
scaleInput.value = agent.avatarScale;
scaleValue.textContent = agent.avatarScale + '×';
}
if (agent.avatar) {
avatarPreview.innerHTML = `<img src="${agent.avatar}" alt="Avatar">`;
avatarPreview.innerHTML = `<img src="${agent.avatar}" alt="Avatar" style="scale: ${agent.avatarScale || 1}">`;
scaleRow.hidden = false;
}
// Custom Commands laden
@@ -82,6 +99,43 @@ registerPage('agent', {
// Befehl hinzufügen
addCommandBtn.addEventListener('click', () => createCommandRow(commandsList));
// AI Bild-Generierung (nur wenn Krea konfiguriert)
const aiSection = document.getElementById('agentAiSection');
const aiPrompt = document.getElementById('agentAiPrompt');
const aiGenerateBtn = document.getElementById('agentAiGenerateBtn');
apiKreaStatus().then(status => {
if (status.configured) aiSection.hidden = false;
});
aiGenerateBtn.addEventListener('click', async () => {
const prompt = aiPrompt.value.trim();
if (!prompt) return;
aiGenerateBtn.disabled = true;
aiGenerateBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
try {
const result = await apiKreaGenerate(prompt);
if (result.files && result.files.length > 0) {
const imgResp = await fetch('/api/images/file/' + result.files[0]);
const blob = await imgResp.blob();
const reader = new FileReader();
reader.onload = () => {
_agentAvatarData = reader.result;
avatarPreview.innerHTML = `<img src="${reader.result}" alt="Avatar">`;
scaleRow.hidden = false;
updatePreviewScale();
};
reader.readAsDataURL(blob);
}
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
} finally {
aiGenerateBtn.disabled = false;
aiGenerateBtn.innerHTML = '<i class="fa fa-magic"></i>';
}
});
aiPrompt.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); aiGenerateBtn.click(); }
});
// Avatar Upload
avatarBtn.addEventListener('click', () => avatarInput.click());
avatarInput.addEventListener('change', () => {
@@ -91,6 +145,8 @@ registerPage('agent', {
reader.onload = () => {
_agentAvatarData = reader.result;
avatarPreview.innerHTML = `<img src="${reader.result}" alt="Avatar">`;
scaleRow.hidden = false;
updatePreviewScale();
};
reader.readAsDataURL(file);
});
@@ -99,6 +155,9 @@ registerPage('agent', {
_agentAvatarData = null;
avatarPreview.innerHTML = '?';
avatarInput.value = '';
scaleRow.hidden = true;
scaleInput.value = 1;
scaleValue.textContent = '1×';
});
// Avatar Paste (Ctrl+V)
@@ -114,6 +173,8 @@ registerPage('agent', {
reader.onload = () => {
_agentAvatarData = reader.result;
avatarPreview.innerHTML = `<img src="${reader.result}" alt="Avatar">`;
scaleRow.hidden = false;
updatePreviewScale();
};
reader.readAsDataURL(file);
return;
@@ -142,6 +203,7 @@ registerPage('agent', {
useMemory: useMemoryEl.checked,
webSearchVersion: webSearchVersionEl.value,
avatar: _agentAvatarData,
avatarScale: parseFloat(scaleInput.value) || 1,
customCommands: getCommandsFromUI(commandsList)
};
+4 -2
View File
@@ -51,8 +51,9 @@ registerPage('dashboard', {
if (s.context) sessionBadges.push(`<span class="badge badge-feature" title="${esc(s.context)}"><i class="fa fa-file-text-o"></i></span>`);
if (s.telegramConfigured) sessionBadges.push(`<span class="badge ${s.telegramLinked ? 'badge-feature' : 'badge-off'}" title="Telegram ${s.telegramLinked ? 'verbunden' : 'konfiguriert'}"><i class="fa fa-paper-plane"></i></span>`);
const scaleStyle = s.avatarScale && s.avatarScale !== 1 ? ` style="scale: ${s.avatarScale}"` : '';
card.innerHTML = `
<div class="card-image">${s.image ? `<img src="${s.image}" alt="">` : ''}</div>
<div class="card-image">${s.image ? `<img src="${s.image}" alt=""${scaleStyle}>` : ''}</div>
<div class="card-body">
<div class="card-title-row">
<span class="card-title">${esc(s.name || 'Unbenannt')}</span>
@@ -197,8 +198,9 @@ registerPage('dashboard', {
badges.push(`<span class="badge badge-skill" title="${esc(skill.description || skill.slug)}"><i class="fa fa-puzzle-piece"></i> ${esc(skill.name)}</span>`);
}
const agentScaleStyle = a.avatarScale && a.avatarScale !== 1 ? ` style="transform: scale(${a.avatarScale})"` : '';
card.innerHTML = `
<div class="card-image">${a.avatar ? `<img src="${a.avatar}" alt="">` : ''}</div>
<div class="card-image">${a.avatar ? `<img src="${a.avatar}" alt=""${agentScaleStyle}>` : ''}</div>
<div class="card-body">
<div class="card-title-row">
<span class="card-title">${esc(a.name)}</span>
+65 -3
View File
@@ -31,9 +31,20 @@ registerPage('session', {
const commandsList = document.getElementById('customCommandsList');
const addCommandBtn = document.getElementById('addCommandBtn');
const more = document.querySelector('.more');
const scaleRow = document.getElementById('sessionScaleRow');
const scaleInput = document.getElementById('sessionAvatarScale');
const scaleValue = document.getElementById('sessionScaleValue');
_sessionImageData = null;
function updatePreviewScale() {
var img = imagePreview.querySelector('img');
if (img) img.style.scale = scaleInput.value;
scaleValue.textContent = scaleInput.value + '×';
}
scaleInput.addEventListener('input', updatePreviewScale);
if (session) {
titleEl.textContent = session.name || 'Unbenannt';
nameEl.value = session.name || '';
@@ -42,8 +53,14 @@ registerPage('session', {
_sessionImageData = session.image || null;
deleteBtn.hidden = false;
if (session.avatarScale && session.avatarScale !== 1) {
scaleInput.value = session.avatarScale;
scaleValue.textContent = session.avatarScale + '×';
}
if (session.image) {
imagePreview.innerHTML = `<img src="${session.image}" alt="">`;
imagePreview.innerHTML = `<img src="${session.image}" alt="" style="scale: ${session.avatarScale || 1}">`;
scaleRow.hidden = false;
}
// Custom Commands laden
@@ -87,6 +104,43 @@ registerPage('session', {
}
if (session && session.agentId) agentSelect.value = session.agentId;
// AI Bild-Generierung (nur wenn Krea konfiguriert)
const aiSection = document.getElementById('sessionAiSection');
const aiPrompt = document.getElementById('sessionAiPrompt');
const aiGenerateBtn = document.getElementById('sessionAiGenerateBtn');
apiKreaStatus().then(status => {
if (status.configured) aiSection.hidden = false;
});
aiGenerateBtn.addEventListener('click', async () => {
const prompt = aiPrompt.value.trim();
if (!prompt) return;
aiGenerateBtn.disabled = true;
aiGenerateBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
try {
const result = await apiKreaGenerate(prompt);
if (result.files && result.files.length > 0) {
const imgResp = await fetch('/api/images/file/' + result.files[0]);
const blob = await imgResp.blob();
const reader = new FileReader();
reader.onload = () => {
_sessionImageData = reader.result;
imagePreview.innerHTML = `<img src="${reader.result}" alt="">`;
scaleRow.hidden = false;
updatePreviewScale();
};
reader.readAsDataURL(blob);
}
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
} finally {
aiGenerateBtn.disabled = false;
aiGenerateBtn.innerHTML = '<i class="fa fa-magic"></i>';
}
});
aiPrompt.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); aiGenerateBtn.click(); }
});
// Bild Upload
imageBtn.addEventListener('click', () => imageInput.click());
imageInput.addEventListener('change', () => {
@@ -96,6 +150,8 @@ registerPage('session', {
reader.onload = () => {
_sessionImageData = reader.result;
imagePreview.innerHTML = `<img src="${reader.result}" alt="">`;
scaleRow.hidden = false;
updatePreviewScale();
};
reader.readAsDataURL(file);
});
@@ -104,6 +160,9 @@ registerPage('session', {
_sessionImageData = null;
imagePreview.innerHTML = '?';
imageInput.value = '';
scaleRow.hidden = true;
scaleInput.value = 1;
scaleValue.textContent = '1×';
});
// Bild Paste (Ctrl+V)
@@ -119,6 +178,8 @@ registerPage('session', {
reader.onload = () => {
_sessionImageData = reader.result;
imagePreview.innerHTML = `<img src="${reader.result}" alt="">`;
scaleRow.hidden = false;
updatePreviewScale();
};
reader.readAsDataURL(file);
return;
@@ -175,7 +236,8 @@ registerPage('session', {
agentId: agentSelect.value || null,
context: _sessionContextMde.value().trim() || null,
workspace: workspaceEl.value.trim() || null,
customCommands: commands
customCommands: commands,
avatarScale: parseFloat(scaleInput.value) || 1
};
if (isNew) {