diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css index 952cc2d..7a66c0a 100644 --- a/wwwroot/css/chat.css +++ b/wwwroot/css/chat.css @@ -941,6 +941,16 @@ gap: var(--badge-spacing); } +.shared-content .msg { + max-width: unset; + align-self: stretch; +} + +.shared-content .msg.assistant { + background: unset; + border: unset; +} + .shared-loading { text-align: center; padding: 3em 1em; diff --git a/wwwroot/css/form.css b/wwwroot/css/form.css index 8e53ad7..57430c1 100644 --- a/wwwroot/css/form.css +++ b/wwwroot/css/form.css @@ -15,11 +15,7 @@ cursor: pointer; } -.avatar-section { - display: flex; - align-items: center; - gap: var(--box-spacing); -} +/* ── Avatar Preview (in Formularen) ── */ .avatar-preview { width: 8em; @@ -32,37 +28,114 @@ overflow: hidden; flex-shrink: 0; border-radius: var(--border-radius); + cursor: pointer; + transition: border-color var(--transition-duration); +} + +.avatar-preview:hover { + border-color: var(--primary); } .avatar-preview img { width: 100%; height: 100%; object-fit: cover; - transition: scale var(--transition-duration); } -.avatar-scale-row { +/* ── Avatar Editor Modal ── */ + +.av-overlay { + position: fixed; + inset: 0; + background: rgb(from var(--f1-bg) r g b / var(--semi-transparent)); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.av-modal { + background: var(--f2-bg); + border: var(--border-width) solid var(--f2-border); + border-radius: var(--border-radius); + width: 90vw; + max-width: 420px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.av-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--box-padding); + border-bottom: var(--border-width) solid var(--f1-border); + background: var(--f1-bg); +} + +.av-body { + padding: var(--box-padding); + display: flex; + flex-direction: column; + align-items: center; + gap: var(--box-spacing); +} + +.av-preview-large { + width: 16em; + height: 16em; + background: var(--f3-bg); + border: var(--border-width) solid var(--f2-border); + border-radius: var(--border-radius); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.av-preview-large img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.av-controls { + width: 100%; + display: flex; + flex-direction: column; + gap: var(--badge-spacing); +} + +.av-scale-row { display: flex; align-items: center; gap: var(--badge-spacing); - flex: 1; - padding: var(--box-padding); } -.avatar-scale-row label { + +.av-scale-row label { margin: 0; + white-space: nowrap; } -.avatar-scale-row input[type="range"] { + +.av-scale-row input[type="range"] { flex: 1; accent-color: var(--primary); } -.avatar-scale-row span { + +.av-scale-row .av-scale-value { font-size: 0.85em; min-width: 2.5em; text-align: right; opacity: 0.7; } -.avatar-ai-section { +.av-actions { + display: flex; + gap: var(--badge-spacing); +} + +.av-ai-section { width: 100%; } diff --git a/wwwroot/index.html b/wwwroot/index.html index 3c3a4e1..31150f8 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -100,23 +100,7 @@
-
-
?
- - - -
- - +
?
@@ -169,23 +153,7 @@
-
-
?
- - - -
- - +
?
@@ -538,6 +506,7 @@ + diff --git a/wwwroot/js/avatar-editor.js b/wwwroot/js/avatar-editor.js new file mode 100644 index 0000000..2c6cacd --- /dev/null +++ b/wwwroot/js/avatar-editor.js @@ -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 = ''; + 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 = + '
' + + '
' + + '

Avatar bearbeiten

' + + '' + + '
' + + '
' + + '
' + + (imageData ? '' : '?') + + '
' + + '
' + + '
' + + '' + + '' + + '' + scale + '×' + + '
' + + '
' + + '' + + '' + + '' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '
' + + '
'; + + 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 = ''; + 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 = ''; + } + } + + // 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 = ''; + 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 }; +})(); diff --git a/wwwroot/js/pages/agent-page.js b/wwwroot/js/pages/agent-page.js index 311fbcd..52070ad 100644 --- a/wwwroot/js/pages/agent-page.js +++ b/wwwroot/js/pages/agent-page.js @@ -1,7 +1,6 @@ // ── Agent Page ── -let _agentAvatarData = null; -let _agentPasteHandler = null; +let _agentAvatarEditor = null; registerPage('agent', { async init(params) { @@ -26,9 +25,6 @@ registerPage('agent', { const useMemoryEl = document.getElementById('agentUseMemory'); const webSearchVersionEl = document.getElementById('agentWebSearchVersion'); const avatarPreview = document.getElementById('avatarPreview'); - const avatarInput = document.getElementById('avatarInput'); - const avatarBtn = document.getElementById('avatarBtn'); - const avatarClearBtn = document.getElementById('avatarClearBtn'); const saveBtn = document.getElementById('agentSaveBtn'); const cancelBtn = document.getElementById('agentCancelBtn'); const deleteBtn = document.getElementById('agentDeleteBtn'); @@ -38,18 +34,12 @@ 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); + // Avatar Editor initialisieren + _agentAvatarEditor = AvatarEditor.init({ + previewEl: avatarPreview, + initialImage: (agent && agent.avatar) || null, + initialScale: (agent && agent.avatarScale) || 1 + }); if (agent) { title.textContent = agent.name; @@ -63,19 +53,8 @@ registerPage('agent', { useUserInfoEl.checked = agent.useUserInfo !== false; useMemoryEl.checked = agent.useMemory !== false; webSearchVersionEl.value = agent.webSearchVersion || ''; - _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 = `Avatar`; - scaleRow.hidden = false; - } - // Custom Commands laden loadCommandsIntoUI(commandsList, agent.customCommands); @@ -99,89 +78,6 @@ 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 = ''; - 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 = `Avatar`; - scaleRow.hidden = false; - updatePreviewScale(); - }; - reader.readAsDataURL(blob); - } - } catch (e) { - showToast('Fehler: ' + e.message, 'error'); - } finally { - aiGenerateBtn.disabled = false; - aiGenerateBtn.innerHTML = ''; - } - }); - aiPrompt.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { e.preventDefault(); aiGenerateBtn.click(); } - }); - - // Avatar Upload - avatarBtn.addEventListener('click', () => avatarInput.click()); - avatarInput.addEventListener('change', () => { - const file = avatarInput.files[0]; - if (!file) return; - const reader = new FileReader(); - reader.onload = () => { - _agentAvatarData = reader.result; - avatarPreview.innerHTML = `Avatar`; - scaleRow.hidden = false; - updatePreviewScale(); - }; - reader.readAsDataURL(file); - }); - - avatarClearBtn.addEventListener('click', () => { - _agentAvatarData = null; - avatarPreview.innerHTML = '?'; - avatarInput.value = ''; - scaleRow.hidden = true; - scaleInput.value = 1; - scaleValue.textContent = '1×'; - }); - - // Avatar Paste (Ctrl+V) - document.addEventListener('paste', _agentPasteHandler = (e) => { - const items = e.clipboardData?.items; - if (!items) return; - for (const item of items) { - if (item.type.startsWith('image/')) { - e.preventDefault(); - const file = item.getAsFile(); - if (!file) return; - const reader = new FileReader(); - reader.onload = () => { - _agentAvatarData = reader.result; - avatarPreview.innerHTML = `Avatar`; - scaleRow.hidden = false; - updatePreviewScale(); - }; - reader.readAsDataURL(file); - return; - } - } - }); - more.addEventListener('click', () => { const content = more.querySelector('[hidden]'); while (content.firstChild) @@ -202,8 +98,8 @@ registerPage('agent', { useUserInfo: useUserInfoEl.checked, useMemory: useMemoryEl.checked, webSearchVersion: webSearchVersionEl.value, - avatar: _agentAvatarData, - avatarScale: parseFloat(scaleInput.value) || 1, + avatar: _agentAvatarEditor.getImage(), + avatarScale: _agentAvatarEditor.getScale(), customCommands: getCommandsFromUI(commandsList) }; @@ -234,11 +130,10 @@ registerPage('agent', { }); }, destroy() { - if (_agentPasteHandler) { - document.removeEventListener('paste', _agentPasteHandler); - _agentPasteHandler = null; + if (_agentAvatarEditor) { + _agentAvatarEditor.destroy(); + _agentAvatarEditor = null; } - _agentAvatarData = null; } }); diff --git a/wwwroot/js/pages/session-page.js b/wwwroot/js/pages/session-page.js index 2e7eb1c..b0929de 100644 --- a/wwwroot/js/pages/session-page.js +++ b/wwwroot/js/pages/session-page.js @@ -1,7 +1,6 @@ // ── Session Edit Page ── -let _sessionImageData = null; -let _sessionPasteHandler = null; +let _sessionAvatarEditor = null; let _sessionContextMde = null; registerPage('session', { @@ -22,9 +21,6 @@ registerPage('session', { const workspaceEl = document.getElementById('sessionWorkspace'); const agentSelect = document.getElementById('agentSelect'); const imagePreview = document.getElementById('sessionImagePreview'); - const imageInput = document.getElementById('sessionImageInput'); - const imageBtn = document.getElementById('sessionImageBtn'); - const imageClearBtn = document.getElementById('sessionImageClearBtn'); const saveBtn = document.getElementById('sessionSaveBtn'); const cancelBtn = document.getElementById('sessionCancelBtn'); const deleteBtn = document.getElementById('sessionDeleteBtn'); @@ -32,37 +28,20 @@ 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); + // Avatar Editor initialisieren + _sessionAvatarEditor = AvatarEditor.init({ + previewEl: imagePreview, + initialImage: (session && session.image) || null, + initialScale: (session && session.avatarScale) || 1 + }); if (session) { titleEl.textContent = session.name || 'Unbenannt'; nameEl.value = session.name || ''; descEl.value = session.description || ''; workspaceEl.value = session.workspace || ''; - _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 = ``; - scaleRow.hidden = false; - } - // Custom Commands laden loadCommandsIntoUI(commandsList, session.customCommands); } else { @@ -104,89 +83,6 @@ 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 = ''; - 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 = ``; - scaleRow.hidden = false; - updatePreviewScale(); - }; - reader.readAsDataURL(blob); - } - } catch (e) { - showToast('Fehler: ' + e.message, 'error'); - } finally { - aiGenerateBtn.disabled = false; - aiGenerateBtn.innerHTML = ''; - } - }); - aiPrompt.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { e.preventDefault(); aiGenerateBtn.click(); } - }); - - // Bild Upload - imageBtn.addEventListener('click', () => imageInput.click()); - imageInput.addEventListener('change', () => { - const file = imageInput.files[0]; - if (!file) return; - const reader = new FileReader(); - reader.onload = () => { - _sessionImageData = reader.result; - imagePreview.innerHTML = ``; - scaleRow.hidden = false; - updatePreviewScale(); - }; - reader.readAsDataURL(file); - }); - - imageClearBtn.addEventListener('click', () => { - _sessionImageData = null; - imagePreview.innerHTML = '?'; - imageInput.value = ''; - scaleRow.hidden = true; - scaleInput.value = 1; - scaleValue.textContent = '1×'; - }); - - // Bild Paste (Ctrl+V) - document.addEventListener('paste', _sessionPasteHandler = (e) => { - const items = e.clipboardData?.items; - if (!items) return; - for (const item of items) { - if (item.type.startsWith('image/')) { - e.preventDefault(); - const file = item.getAsFile(); - if (!file) return; - const reader = new FileReader(); - reader.onload = () => { - _sessionImageData = reader.result; - imagePreview.innerHTML = ``; - scaleRow.hidden = false; - updatePreviewScale(); - }; - reader.readAsDataURL(file); - return; - } - } - }); - more.addEventListener('click', () => { const content = more.querySelector('[hidden]'); while (content.firstChild) @@ -232,12 +128,12 @@ registerPage('session', { const data = { name: nameEl.value.trim() || 'Unbenannt', description: descEl.value.trim(), - image: _sessionImageData, + image: _sessionAvatarEditor.getImage(), agentId: agentSelect.value || null, context: _sessionContextMde.value().trim() || null, workspace: workspaceEl.value.trim() || null, customCommands: commands, - avatarScale: parseFloat(scaleInput.value) || 1 + avatarScale: _sessionAvatarEditor.getScale() }; if (isNew) { @@ -274,11 +170,10 @@ registerPage('session', { _sessionContextMde.toTextArea(); _sessionContextMde = null; } - if (_sessionPasteHandler) { - document.removeEventListener('paste', _sessionPasteHandler); - _sessionPasteHandler = null; + if (_sessionAvatarEditor) { + _sessionAvatarEditor.destroy(); + _sessionAvatarEditor = null; } - _sessionImageData = null; } });