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:
1 parent
4ebbeaa818
commit
057d37d76f
11 files changed
+221
-16
No files matched your search
+28
-2
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user