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
+220 -15

No files matched your search

+3 -1
View File
@@ -64,7 +64,8 @@ public static class AgentEndpoint
UseUserInfo = !body.TryGetProperty("useUserInfo", out var uu) || uu.GetBoolean(),
UseMemory = !body.TryGetProperty("useMemory", out var um) || um.GetBoolean(),
UseWebSearch = body.TryGetProperty("webSearchVersion", out var wsv) && !string.IsNullOrEmpty(wsv.GetString()),
WebSearchVersion = body.TryGetProperty("webSearchVersion", out var wsv2) ? wsv2.GetString() ?? "" : ""
WebSearchVersion = body.TryGetProperty("webSearchVersion", out var wsv2) ? wsv2.GetString() ?? "" : "",
AvatarScale = body.TryGetProperty("avatarScale", out var asc) ? asc.GetDouble() : 1.0
};
agent = await db.CreateAgent(agent);
@@ -100,6 +101,7 @@ public static class AgentEndpoint
agent.WebSearchVersion = wsv.GetString() ?? "";
agent.UseWebSearch = !string.IsNullOrEmpty(agent.WebSearchVersion);
}
if (body.TryGetProperty("avatarScale", out var asc)) agent.AvatarScale = asc.GetDouble();
await db.UpdateAgent(agent);
memoryService.InitializeAgentFiles(agentId, agent.Name);
+2 -1
View File
@@ -76,8 +76,9 @@ public static class SessionEndpoint
}
var context = body.TryGetProperty("context", out var ctxEl) ? ctxEl.GetString() : null;
var customCommands = body.TryGetProperty("customCommands", out var cc) && cc.ValueKind != JsonValueKind.Null ? cc.GetRawText() : null;
var avatarScale = body.TryGetProperty("avatarScale", out var asc) ? asc.GetDouble() : 1.0;
var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands);
var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands, avatarScale);
await ctx.Response.WriteAsJsonAsync(session);
}
+1
View File
@@ -17,6 +17,7 @@ public class AgentRecord
public bool UseMemory { get; set; } = true;
public bool UseWebSearch { get; set; }
public string WebSearchVersion { get; set; } = "";
public double AvatarScale { get; set; } = 1.0;
public string UserId { get; set; }
public bool IsDeleted { get; set; }
public string CreatedAt { get; set; }
+1
View File
@@ -16,6 +16,7 @@ public class SessionRecord
public string TelegramBotToken { get; set; }
public string TelegramWebhookSecret { get; set; }
public string TelegramChatId { get; set; }
public double AvatarScale { get; set; } = 1.0;
public string CreatedAt { get; set; }
public string UpdatedAt { get; set; }
}
+11 -6
View File
@@ -167,6 +167,8 @@ public class DatabaseService
TryAddColumn(conn, "users", "persona", "TEXT");
TryAddColumn(conn, "users", "settings", "TEXT");
TryAddColumn(conn, "users", "default_path", "TEXT");
TryAddColumn(conn, "sessions", "avatar_scale", "REAL NOT NULL DEFAULT 1.0");
TryAddColumn(conn, "agents", "avatar_scale", "REAL NOT NULL DEFAULT 1.0");
TryAddColumn(conn, "cronjobs", "notify_session_id", "TEXT");
conn.Execute(@"
@@ -320,8 +322,8 @@ public class DatabaseService
agent.CreatedAt = now;
agent.UpdatedAt = now;
await conn.ExecuteAsync(@"
INSERT INTO agents (agent_id, user_id, name, avatar, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_web_search, web_search_version, is_deleted, created_at, updated_at)
VALUES (@AgentId, @UserId, @Name, @Avatar, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseWebSearch, @WebSearchVersion, 0, @CreatedAt, @UpdatedAt)
INSERT INTO agents (agent_id, user_id, name, avatar, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_web_search, web_search_version, avatar_scale, is_deleted, created_at, updated_at)
VALUES (@AgentId, @UserId, @Name, @Avatar, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseWebSearch, @WebSearchVersion, @AvatarScale, 0, @CreatedAt, @UpdatedAt)
", agent);
return agent;
}
@@ -349,7 +351,7 @@ public class DatabaseService
model = @Model, tool_choice = @ToolChoice,
custom_commands = @CustomCommands, auto_commit = @AutoCommit,
use_soul = @UseSoul, use_identity = @UseIdentity, use_user_info = @UseUserInfo, use_memory = @UseMemory, use_web_search = @UseWebSearch, web_search_version = @WebSearchVersion,
updated_at = @UpdatedAt
avatar_scale = @AvatarScale, updated_at = @UpdatedAt
WHERE agent_id = @AgentId AND user_id = @UserId
", agent);
}
@@ -364,7 +366,7 @@ public class DatabaseService
// ── Sessions ──
public async Task<SessionRecord> CreateSession(string userId, string name = null, string description = null, string image = null, string agentId = null, string workspace = null, string context = null, string customCommands = null)
public async Task<SessionRecord> CreateSession(string userId, string name = null, string description = null, string image = null, string agentId = null, string workspace = null, string context = null, string customCommands = null, double avatarScale = 1.0)
{
using var conn = CreateConnection();
var now = DateTime.UtcNow.ToString("o");
@@ -379,12 +381,13 @@ public class DatabaseService
Workspace = workspace,
Context = context,
CustomCommands = customCommands,
AvatarScale = avatarScale,
CreatedAt = now,
UpdatedAt = now
};
await conn.ExecuteAsync(@"
INSERT INTO sessions (session_id, user_id, name, description, image, agent_id, workspace, context, custom_commands, created_at, updated_at)
VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @CreatedAt, @UpdatedAt)
INSERT INTO sessions (session_id, user_id, name, description, image, agent_id, workspace, context, custom_commands, avatar_scale, created_at, updated_at)
VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @CreatedAt, @UpdatedAt)
", session);
return session;
}
@@ -427,6 +430,7 @@ public class DatabaseService
s.Workspace,
s.Context,
s.CustomCommands,
s.AvatarScale,
s.IsDeleted,
s.CreatedAt,
s.UpdatedAt,
@@ -476,6 +480,7 @@ public class DatabaseService
if (body.TryGetProperty("workspace", out var w)) { sets.Add("workspace = @workspace"); p.Add("workspace", w.GetString()); }
if (body.TryGetProperty("context", out var ctx)) { sets.Add("context = @context"); p.Add("context", ctx.GetString()); }
if (body.TryGetProperty("customCommands", out var cc)) { sets.Add("custom_commands = @customCommands"); p.Add("customCommands", cc.ValueKind == System.Text.Json.JsonValueKind.Null ? null : cc.GetRawText()); }
if (body.TryGetProperty("avatarScale", out var asc)) { sets.Add("avatar_scale = @avatarScale"); p.Add("avatarScale", asc.GetDouble()); }
await conn.ExecuteAsync($"UPDATE sessions SET {string.Join(", ", sets)} WHERE session_id = @sessionId", p);
}
+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>
+64 -2
View File
@@ -32,8 +32,19 @@ registerPage('session', {
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) {