From 4775b8ded7e703752cbbafcf41a4117eb6e4f88a Mon Sep 17 00:00:00 2001 From: Meleki Date: Fri, 6 Mar 2026 21:14:51 +0100 Subject: [PATCH] feat: Avatar Position-Offset (translate X/Y) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - AgentRecord + SessionRecord: AvatarOffsetX, AvatarOffsetY Properties - DB-Migration: avatar_offset_x/y REAL DEFAULT 0 auf agents + sessions - Agent/Session Endpoints: Create + Update lesen/schreiben Offset-Felder - GetSessionsWithStats: Offset-Felder ans Frontend Frontend: - Avatar-Editor: X/Y Offset-Slider (-50% bis +50%) im Modal - buildTransformStyle() Helper für transform: scale() translate() - getOffsetX()/getOffsetY() Public API - Reset bei Clear/AI-Generate/Upload - ui.js: buildAgentCard + buildSessionCard rendern transform mit Offset - Agent-Page + Session-Page: initialOffsetX/Y + Save - CSS: .av-offset-row Styles (gruppiert mit .av-scale-row) --- Endpoints/AgentEndpoint.cs | 6 +- Endpoints/SessionEndpoint.cs | 4 +- Models/AgentRecord.cs | 2 + Models/SessionRecord.cs | 2 + Services/DatabaseService.cs | 22 ++++++-- wwwroot/css/form.css | 13 +++-- wwwroot/js/avatar-editor.js | 96 ++++++++++++++++++++++++++++---- wwwroot/js/pages/agent-page.js | 6 +- wwwroot/js/pages/session-page.js | 8 ++- wwwroot/js/ui.js | 14 +++-- 10 files changed, 143 insertions(+), 30 deletions(-) diff --git a/Endpoints/AgentEndpoint.cs b/Endpoints/AgentEndpoint.cs index 7c7ece3..fb572b1 100644 --- a/Endpoints/AgentEndpoint.cs +++ b/Endpoints/AgentEndpoint.cs @@ -65,7 +65,9 @@ public static class AgentEndpoint 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() ?? "" : "", - AvatarScale = body.TryGetProperty("avatarScale", out var asc) ? asc.GetDouble() : 1.0 + AvatarScale = body.TryGetProperty("avatarScale", out var asc) ? asc.GetDouble() : 1.0, + AvatarOffsetX = body.TryGetProperty("avatarOffsetX", out var aox) ? aox.GetDouble() : 0, + AvatarOffsetY = body.TryGetProperty("avatarOffsetY", out var aoy) ? aoy.GetDouble() : 0 }; agent = await db.CreateAgent(agent); @@ -102,6 +104,8 @@ public static class AgentEndpoint agent.UseWebSearch = !string.IsNullOrEmpty(agent.WebSearchVersion); } if (body.TryGetProperty("avatarScale", out var asc)) agent.AvatarScale = asc.GetDouble(); + if (body.TryGetProperty("avatarOffsetX", out var aox)) agent.AvatarOffsetX = aox.GetDouble(); + if (body.TryGetProperty("avatarOffsetY", out var aoy)) agent.AvatarOffsetY = aoy.GetDouble(); await db.UpdateAgent(agent); memoryService.InitializeAgentFiles(agentId, agent.Name); diff --git a/Endpoints/SessionEndpoint.cs b/Endpoints/SessionEndpoint.cs index 2823006..a4cb1bd 100644 --- a/Endpoints/SessionEndpoint.cs +++ b/Endpoints/SessionEndpoint.cs @@ -77,10 +77,12 @@ 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 avatarOffsetX = body.TryGetProperty("avatarOffsetX", out var aox) ? aox.GetDouble() : 0; + var avatarOffsetY = body.TryGetProperty("avatarOffsetY", out var aoy) ? aoy.GetDouble() : 0; var reviewAgentId = body.TryGetProperty("reviewAgentId", out var ra) ? ra.GetString() : null; var maxReviewIterations = body.TryGetProperty("maxReviewIterations", out var mri) ? mri.GetInt32() : 3; - var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands, avatarScale, reviewAgentId, maxReviewIterations); + var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands, avatarScale, avatarOffsetX, avatarOffsetY, reviewAgentId, maxReviewIterations); await ctx.Response.WriteAsJsonAsync(session); } diff --git a/Models/AgentRecord.cs b/Models/AgentRecord.cs index d273183..cfa8e5d 100644 --- a/Models/AgentRecord.cs +++ b/Models/AgentRecord.cs @@ -18,6 +18,8 @@ public class AgentRecord public bool UseWebSearch { get; set; } public string WebSearchVersion { get; set; } = ""; public double AvatarScale { get; set; } = 1.0; + public double AvatarOffsetX { get; set; } + public double AvatarOffsetY { get; set; } public string UserId { get; set; } public bool IsDeleted { get; set; } public string CreatedAt { get; set; } diff --git a/Models/SessionRecord.cs b/Models/SessionRecord.cs index 0f0f594..7ec208b 100644 --- a/Models/SessionRecord.cs +++ b/Models/SessionRecord.cs @@ -18,6 +18,8 @@ public class SessionRecord public string TelegramChatId { get; set; } public string TelegramAllowedChatIds { get; set; } public double AvatarScale { get; set; } = 1.0; + public double AvatarOffsetX { get; set; } + public double AvatarOffsetY { get; set; } public string ReviewAgentId { get; set; } public int MaxReviewIterations { get; set; } = 3; public string CreatedAt { get; set; } diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 834919e..ace0136 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -173,6 +173,10 @@ public class DatabaseService TryAddColumn(conn, "cronjobs", "notify_session_id", "TEXT"); TryAddColumn(conn, "sessions", "review_agent_id", "TEXT"); TryAddColumn(conn, "sessions", "max_review_iterations", "INTEGER NOT NULL DEFAULT 3"); + TryAddColumn(conn, "sessions", "avatar_offset_x", "REAL NOT NULL DEFAULT 0"); + TryAddColumn(conn, "sessions", "avatar_offset_y", "REAL NOT NULL DEFAULT 0"); + TryAddColumn(conn, "agents", "avatar_offset_x", "REAL NOT NULL DEFAULT 0"); + TryAddColumn(conn, "agents", "avatar_offset_y", "REAL NOT NULL DEFAULT 0"); conn.Execute(@" CREATE TABLE IF NOT EXISTS agent_skills ( @@ -325,8 +329,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, 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) + 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, avatar_offset_x, avatar_offset_y, is_deleted, created_at, updated_at) + VALUES (@AgentId, @UserId, @Name, @Avatar, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseWebSearch, @WebSearchVersion, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, 0, @CreatedAt, @UpdatedAt) ", agent); return agent; } @@ -354,7 +358,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, - avatar_scale = @AvatarScale, updated_at = @UpdatedAt + avatar_scale = @AvatarScale, avatar_offset_x = @AvatarOffsetX, avatar_offset_y = @AvatarOffsetY, updated_at = @UpdatedAt WHERE agent_id = @AgentId AND user_id = @UserId ", agent); } @@ -369,7 +373,7 @@ public class DatabaseService // ── Sessions ── - public async Task 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, string reviewAgentId = null, int maxReviewIterations = 3) + public async Task 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, double avatarOffsetX = 0, double avatarOffsetY = 0, string reviewAgentId = null, int maxReviewIterations = 3) { using var conn = CreateConnection(); var now = DateTime.UtcNow.ToString("o"); @@ -385,14 +389,16 @@ public class DatabaseService Context = context, CustomCommands = customCommands, AvatarScale = avatarScale, + AvatarOffsetX = avatarOffsetX, + AvatarOffsetY = avatarOffsetY, ReviewAgentId = reviewAgentId, MaxReviewIterations = maxReviewIterations, CreatedAt = now, UpdatedAt = now }; await conn.ExecuteAsync(@" - INSERT INTO sessions (session_id, user_id, name, description, image, agent_id, workspace, context, custom_commands, avatar_scale, review_agent_id, max_review_iterations, created_at, updated_at) - VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @ReviewAgentId, @MaxReviewIterations, @CreatedAt, @UpdatedAt) + INSERT INTO sessions (session_id, user_id, name, description, image, agent_id, workspace, context, custom_commands, avatar_scale, avatar_offset_x, avatar_offset_y, review_agent_id, max_review_iterations, created_at, updated_at) + VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @ReviewAgentId, @MaxReviewIterations, @CreatedAt, @UpdatedAt) ", session); return session; } @@ -436,6 +442,8 @@ public class DatabaseService s.Context, s.CustomCommands, s.AvatarScale, + s.AvatarOffsetX, + s.AvatarOffsetY, s.ReviewAgentId, s.MaxReviewIterations, s.IsDeleted, @@ -488,6 +496,8 @@ public class DatabaseService 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()); } + if (body.TryGetProperty("avatarOffsetX", out var aox)) { sets.Add("avatar_offset_x = @avatarOffsetX"); p.Add("avatarOffsetX", aox.GetDouble()); } + if (body.TryGetProperty("avatarOffsetY", out var aoy)) { sets.Add("avatar_offset_y = @avatarOffsetY"); p.Add("avatarOffsetY", aoy.GetDouble()); } if (body.TryGetProperty("reviewAgentId", out var ra)) { sets.Add("review_agent_id = @reviewAgentId"); p.Add("reviewAgentId", ra.ValueKind == System.Text.Json.JsonValueKind.Null ? null : ra.GetString()); } if (body.TryGetProperty("maxReviewIterations", out var mri)) { sets.Add("max_review_iterations = @maxReviewIterations"); p.Add("maxReviewIterations", mri.GetInt32()); } diff --git a/wwwroot/css/form.css b/wwwroot/css/form.css index e833a75..a19d99a 100644 --- a/wwwroot/css/form.css +++ b/wwwroot/css/form.css @@ -107,23 +107,28 @@ gap: var(--badge-spacing); } -.av-scale-row { +.av-scale-row, +.av-offset-row { display: flex; align-items: center; gap: var(--badge-spacing); } -.av-scale-row label { +.av-scale-row label, +.av-offset-row label { margin: 0; white-space: nowrap; + min-width: 2.5em; } -.av-scale-row input[type="range"] { +.av-scale-row input[type="range"], +.av-offset-row input[type="range"] { flex: 1; accent-color: var(--primary); } -.av-scale-row .av-scale-value { +.av-scale-row .av-scale-value, +.av-offset-row span[class*="-value"] { font-size: 0.85em; min-width: 2.5em; text-align: right; diff --git a/wwwroot/js/avatar-editor.js b/wwwroot/js/avatar-editor.js index 2c6cacd..bf36ecd 100644 --- a/wwwroot/js/avatar-editor.js +++ b/wwwroot/js/avatar-editor.js @@ -6,13 +6,17 @@ // previewEl: document.getElementById('myPreview'), // initialImage: 'data:image/...' || null, // initialScale: 1.5, -// onChange(imageData, scale) { ... } +// initialOffsetX: 0, +// initialOffsetY: 0, +// onChange(imageData, scale, offsetX, offsetY) { ... } // }); // // Cleanup: // editor.destroy(); // // Get current values: // editor.getImage(); // data-url or null // editor.getScale(); // number +// editor.getOffsetX(); // number (-50 to 50) +// editor.getOffsetY(); // number (-50 to 50) var AvatarEditor = (function () { @@ -20,6 +24,8 @@ var AvatarEditor = (function () { var previewEl = opts.previewEl; var imageData = opts.initialImage || null; var scale = opts.initialScale || 1; + var offsetX = opts.initialOffsetX || 0; + var offsetY = opts.initialOffsetY || 0; var onChange = opts.onChange || function () {}; var overlay = null; var pasteHandler = null; @@ -28,10 +34,17 @@ var AvatarEditor = (function () { // Krea-Status prüfen apiKreaStatus().then(function (s) { kreaAvailable = s.configured; }).catch(function () {}); + function buildTransformStyle() { + var parts = []; + if (scale !== 1) parts.push('scale(' + scale + ')'); + if (offsetX !== 0 || offsetY !== 0) parts.push('translate(' + offsetX + '%, ' + offsetY + '%)'); + return parts.length ? ' style="transform: ' + parts.join(' ') + '"' : ''; + } + // Preview rendern function renderPreview() { if (imageData) { - previewEl.innerHTML = ''; + previewEl.innerHTML = ''; previewEl.classList.add('has-image'); } else { previewEl.innerHTML = '?'; @@ -65,7 +78,7 @@ var AvatarEditor = (function () { reader.onload = function () { imageData = reader.result; renderPreview(); - onChange(imageData, scale); + onChange(imageData, scale, offsetX, offsetY); if (overlay) refreshModal(); }; reader.readAsDataURL(file); @@ -74,8 +87,10 @@ var AvatarEditor = (function () { function setImage(data) { imageData = data; scale = 1; + offsetX = 0; + offsetY = 0; renderPreview(); - onChange(imageData, scale); + onChange(imageData, scale, offsetX, offsetY); } function openModal() { @@ -91,7 +106,7 @@ var AvatarEditor = (function () { '' + '
' + '
' + - (imageData ? '' : '?') + + (imageData ? '' : '?') + '
' + '
' + '
' + @@ -99,6 +114,16 @@ var AvatarEditor = (function () { '' + '' + scale + '×' + '
' + + '
' + + '' + + '' + + '' + offsetX + '%' + + '
' + + '
' + + '' + + '' + + '' + offsetY + '%' + + '
' + '
' + '' + '' + @@ -123,6 +148,11 @@ var AvatarEditor = (function () { var scaleRow = overlay.querySelector('.av-scale-row'); var scaleInput = overlay.querySelector('.av-scale'); var scaleValueEl = overlay.querySelector('.av-scale-value'); + var offsetRows = overlay.querySelectorAll('.av-offset-row'); + var offsetXInput = overlay.querySelector('.av-offset-x'); + var offsetXValueEl = overlay.querySelector('.av-offset-x-value'); + var offsetYInput = overlay.querySelector('.av-offset-y'); + var offsetYValueEl = overlay.querySelector('.av-offset-y-value'); var uploadBtn = overlay.querySelector('.av-upload-btn'); var fileInput = overlay.querySelector('.av-file-input'); var clearBtn = overlay.querySelector('.av-clear-btn'); @@ -135,14 +165,41 @@ var AvatarEditor = (function () { if (e.target === overlay) closeModal(); }); + function updatePreviewLargeTransform() { + var img = previewLarge.querySelector('img'); + if (img) { + var parts = []; + if (scale !== 1) parts.push('scale(' + scale + ')'); + if (offsetX !== 0 || offsetY !== 0) parts.push('translate(' + offsetX + '%, ' + offsetY + '%)'); + img.style.transform = parts.length ? parts.join(' ') : ''; + } + } + // Scale scaleInput.addEventListener('input', function () { scale = parseFloat(scaleInput.value); scaleValueEl.textContent = scale + '×'; - var img = previewLarge.querySelector('img'); - if (img) img.style.scale = scale; + updatePreviewLargeTransform(); renderPreview(); - onChange(imageData, scale); + onChange(imageData, scale, offsetX, offsetY); + }); + + // Offset X + offsetXInput.addEventListener('input', function () { + offsetX = parseFloat(offsetXInput.value); + offsetXValueEl.textContent = offsetX + '%'; + updatePreviewLargeTransform(); + renderPreview(); + onChange(imageData, scale, offsetX, offsetY); + }); + + // Offset Y + offsetYInput.addEventListener('input', function () { + offsetY = parseFloat(offsetYInput.value); + offsetYValueEl.textContent = offsetY + '%'; + updatePreviewLargeTransform(); + renderPreview(); + onChange(imageData, scale, offsetX, offsetY); }); // Upload @@ -155,10 +212,16 @@ var AvatarEditor = (function () { clearBtn.addEventListener('click', function () { imageData = null; scale = 1; + offsetX = 0; + offsetY = 0; scaleInput.value = 1; scaleValueEl.textContent = '1×'; + offsetXInput.value = 0; + offsetXValueEl.textContent = '0%'; + offsetYInput.value = 0; + offsetYValueEl.textContent = '0%'; renderPreview(); - onChange(imageData, scale); + onChange(imageData, scale, offsetX, offsetY); refreshModal(); }); @@ -182,10 +245,16 @@ var AvatarEditor = (function () { reader.onload = function () { imageData = reader.result; scale = 1; + offsetX = 0; + offsetY = 0; scaleInput.value = 1; scaleValueEl.textContent = '1×'; + offsetXInput.value = 0; + offsetXValueEl.textContent = '0%'; + offsetYInput.value = 0; + offsetYValueEl.textContent = '0%'; renderPreview(); - onChange(imageData, scale); + onChange(imageData, scale, offsetX, offsetY); refreshModal(); }; reader.readAsDataURL(blob); @@ -222,14 +291,17 @@ var AvatarEditor = (function () { if (!overlay) return; var previewLarge = overlay.querySelector('.av-preview-large'); var scaleRow = overlay.querySelector('.av-scale-row'); + var offsetRows = overlay.querySelectorAll('.av-offset-row'); var clearBtn = overlay.querySelector('.av-clear-btn'); if (imageData) { - previewLarge.innerHTML = ''; + previewLarge.innerHTML = ''; scaleRow.hidden = false; + offsetRows.forEach(function (r) { r.hidden = false; }); clearBtn.hidden = false; } else { previewLarge.innerHTML = '?'; scaleRow.hidden = true; + offsetRows.forEach(function (r) { r.hidden = true; }); clearBtn.hidden = true; } } @@ -245,6 +317,8 @@ var AvatarEditor = (function () { return { getImage: function () { return imageData; }, getScale: function () { return scale; }, + getOffsetX: function () { return offsetX; }, + getOffsetY: function () { return offsetY; }, destroy: function () { closeModal(); if (pasteHandler) { diff --git a/wwwroot/js/pages/agent-page.js b/wwwroot/js/pages/agent-page.js index 52070ad..d89a9d1 100644 --- a/wwwroot/js/pages/agent-page.js +++ b/wwwroot/js/pages/agent-page.js @@ -38,7 +38,9 @@ registerPage('agent', { _agentAvatarEditor = AvatarEditor.init({ previewEl: avatarPreview, initialImage: (agent && agent.avatar) || null, - initialScale: (agent && agent.avatarScale) || 1 + initialScale: (agent && agent.avatarScale) || 1, + initialOffsetX: (agent && agent.avatarOffsetX) || 0, + initialOffsetY: (agent && agent.avatarOffsetY) || 0 }); if (agent) { @@ -100,6 +102,8 @@ registerPage('agent', { webSearchVersion: webSearchVersionEl.value, avatar: _agentAvatarEditor.getImage(), avatarScale: _agentAvatarEditor.getScale(), + avatarOffsetX: _agentAvatarEditor.getOffsetX(), + avatarOffsetY: _agentAvatarEditor.getOffsetY(), customCommands: getCommandsFromUI(commandsList) }; diff --git a/wwwroot/js/pages/session-page.js b/wwwroot/js/pages/session-page.js index cb67107..87ef22d 100644 --- a/wwwroot/js/pages/session-page.js +++ b/wwwroot/js/pages/session-page.js @@ -37,7 +37,9 @@ registerPage('session', { _sessionAvatarEditor = AvatarEditor.init({ previewEl: imagePreview, initialImage: (session && session.image) || null, - initialScale: (session && session.avatarScale) || 1 + initialScale: (session && session.avatarScale) || 1, + initialOffsetX: (session && session.avatarOffsetX) || 0, + initialOffsetY: (session && session.avatarOffsetY) || 0 }); if (session) { @@ -190,7 +192,9 @@ registerPage('session', { context: _sessionContextMde.value().trim() || null, workspace: workspaceEl.value.trim() || null, customCommands: commands, - avatarScale: _sessionAvatarEditor.getScale() + avatarScale: _sessionAvatarEditor.getScale(), + avatarOffsetX: _sessionAvatarEditor.getOffsetX(), + avatarOffsetY: _sessionAvatarEditor.getOffsetY() }; if (isNew) { diff --git a/wwwroot/js/ui.js b/wwwroot/js/ui.js index 41f0fc2..eefe341 100644 --- a/wwwroot/js/ui.js +++ b/wwwroot/js/ui.js @@ -7,8 +7,11 @@ function buildSessionCard(session, agent) { const card = document.createElement('div'); card.className = 'card'; - const scaleStyle = session.avatarScale && session.avatarScale !== 1 ? ` style="scale: ${session.avatarScale}"` : ''; - const imageHtml = session.image ? `` : ''; + const transformParts = []; + if (session.avatarScale && session.avatarScale !== 1) transformParts.push(`scale(${session.avatarScale})`); + if (session.avatarOffsetX || session.avatarOffsetY) transformParts.push(`translate(${session.avatarOffsetX || 0}%, ${session.avatarOffsetY || 0}%)`); + const transformStyle = transformParts.length ? ` style="transform: ${transformParts.join(' ')}"` : ''; + const imageHtml = session.image ? `` : ''; // Badges const badges = []; @@ -47,8 +50,11 @@ function buildAgentCard(agent, skills) { const card = document.createElement('div'); card.className = 'card'; - const scaleStyle = agent.avatarScale && agent.avatarScale !== 1 ? ` style="transform: scale(${agent.avatarScale})"` : ''; - const imageHtml = agent.avatar ? `` : ''; + const agentTransformParts = []; + if (agent.avatarScale && agent.avatarScale !== 1) agentTransformParts.push(`scale(${agent.avatarScale})`); + if (agent.avatarOffsetX || agent.avatarOffsetY) agentTransformParts.push(`translate(${agent.avatarOffsetX || 0}%, ${agent.avatarOffsetY || 0}%)`); + const agentTransformStyle = agentTransformParts.length ? ` style="transform: ${agentTransformParts.join(' ')}"` : ''; + const imageHtml = agent.avatar ? `` : ''; // Badges const badges = [];