Files
gott/wwwroot/js/pages/session-page.js
T
Meleki 057d37d76f 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
2026-03-06 10:59:09 +01:00

412 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── Session Edit Page ──
let _sessionImageData = null;
let _sessionPasteHandler = null;
let _sessionContextMde = null;
registerPage('session', {
async init(params) {
const sessionId = params.id;
const isNew = !sessionId;
let session = null;
if (!isNew) {
session = await apiGetSession(sessionId);
if (!session) return navigateTo('dashboard');
}
const titleEl = document.getElementById('sessionPageTitle');
const nameEl = document.getElementById('sessionName');
const descEl = document.getElementById('sessionDescription');
const contextEl = document.getElementById('sessionContext');
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');
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 || '';
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 = `<img src="${session.image}" alt="" style="scale: ${session.avatarScale || 1}">`;
scaleRow.hidden = false;
}
// Custom Commands laden
loadCommandsIntoUI(commandsList, session.customCommands);
} else {
titleEl.textContent = 'Neue Session';
deleteBtn.hidden = true;
}
// EasyMDE für Kontext
_sessionContextMde = new EasyMDE({
element: contextEl,
initialValue: (session && session.context) || '',
toolbar: ['bold', 'italic', 'heading', '|', 'unordered-list', 'ordered-list', '|', 'link', 'code', '|', 'preview'],
status: false,
spellChecker: false,
autoDownloadFontAwesome: false,
minHeight: '120px',
placeholder: 'Zusätzlicher Kontext für den System-Prompt (optional)',
previewClass: ['editor-preview', 'markdown']
});
_sessionContextMde.codemirror.setOption('theme', 'default');
_sessionContextMde.codemirror.getWrapperElement().closest('.EasyMDEContainer').classList.add('fe-mde-wrap');
// Befehl hinzufügen
addCommandBtn.addEventListener('click', () => createCommandRow(commandsList));
// Telegram-Section nur für existierende Sessions
if (!isNew) {
initSessionTelegram(sessionId);
}
// Agents laden
const agents = await apiGetAgents();
agentSelect.innerHTML = '<option value="">-- Kein Agent --</option>';
for (const a of agents) {
const opt = document.createElement('option');
opt.value = a.agentId;
opt.textContent = `${a.name} (${a.model})`;
agentSelect.appendChild(opt);
}
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', () => {
const file = imageInput.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
_sessionImageData = reader.result;
imagePreview.innerHTML = `<img src="${reader.result}" alt="">`;
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 = `<img src="${reader.result}" alt="">`;
scaleRow.hidden = false;
updatePreviewScale();
};
reader.readAsDataURL(file);
return;
}
}
});
more.addEventListener('click', () => {
const content = more.querySelector('[hidden]');
while (content.firstChild)
more.parentNode.insertBefore(content.firstChild, more);
more.remove();
});
// Workspace Browse
const workspaceBrowse = document.getElementById('sessionWorkspaceBrowse');
if (workspaceBrowse) {
workspaceBrowse.addEventListener('click', () => {
const defaultPath = (currentUser && currentUser.defaultPath) || '';
const currentVal = workspaceEl.value.trim();
// Für den Folder-Browser den absoluten Startpfad zusammenbauen
let startPath = '/';
if (defaultPath && currentVal) {
startPath = defaultPath.replace(/\/+$/, '') + '/' + currentVal;
} else if (defaultPath) {
startPath = defaultPath;
} else if (currentVal) {
startPath = currentVal;
}
openFolderBrowser({
startPath,
basePath: defaultPath || null,
onSelect(absolutePath) {
// Relativen Pfad zum DefaultPath berechnen
if (defaultPath && absolutePath.startsWith(defaultPath)) {
let rel = absolutePath.slice(defaultPath.length).replace(/^\/+/, '');
workspaceEl.value = rel || '';
} else {
// Kein DefaultPath oder Pfad außerhalb → absoluten Pfad beibehalten (Legacy)
workspaceEl.value = absolutePath;
}
}
});
});
}
// Speichern
saveBtn.addEventListener('click', async () => {
const commands = getCommandsFromUI(commandsList);
const data = {
name: nameEl.value.trim() || 'Unbenannt',
description: descEl.value.trim(),
image: _sessionImageData,
agentId: agentSelect.value || null,
context: _sessionContextMde.value().trim() || null,
workspace: workspaceEl.value.trim() || null,
customCommands: commands,
avatarScale: parseFloat(scaleInput.value) || 1
};
if (isNew) {
await apiCreateSession(data);
} else {
await apiUpdateSession(sessionId, data);
// Telegram-Config mitspeichern (falls Section sichtbar)
var tgToken = document.getElementById('sessionTelegramToken');
var tgSecret = document.getElementById('sessionTelegramSecret');
if (tgToken) {
try {
await saveTelegramConfig(sessionId, tgToken, tgSecret);
} catch(e) {
showToast('Session gespeichert, aber Telegram-Fehler: ' + e.message, 'error');
return;
}
}
}
navigateTo('dashboard');
});
// Abbrechen
cancelBtn.addEventListener('click', () => navigateTo('dashboard'));
// Löschen
deleteBtn.addEventListener('click', async () => {
if (!confirm('Session wirklich löschen?')) return;
await apiDeleteSession(sessionId);
navigateTo('dashboard');
});
},
destroy() {
if (_sessionContextMde) {
_sessionContextMde.toTextArea();
_sessionContextMde = null;
}
if (_sessionPasteHandler) {
document.removeEventListener('paste', _sessionPasteHandler);
_sessionPasteHandler = null;
}
_sessionImageData = null;
}
});
function initSessionTelegram(sessionId) {
var tokenInput = document.getElementById('sessionTelegramToken');
var secretInput = document.getElementById('sessionTelegramSecret');
var registerBtn = document.getElementById('sessionTelegramRegisterBtn');
var deleteBtn = document.getElementById('sessionTelegramDeleteBtn');
var statusBox = document.getElementById('sessionTelegramStatusBox');
// Vorhandene Config laden + Status abrufen
apiSessionTelegramGet(sessionId).then(async function(config) {
if (config.botToken) tokenInput.value = config.botToken;
if (config.webhookSecret) {
secretInput.value = config.webhookSecret;
} else {
// Secret einmalig generieren und sofort persistent speichern
var newSecret = crypto.randomUUID().replace(/-/g, '');
secretInput.value = newSecret;
await apiSessionTelegramSave(sessionId, config.botToken || '', newSecret);
}
// Status automatisch abrufen wenn Token konfiguriert ist
if (config.botToken) {
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
} else {
showTelegramUnconfigured(statusBox, registerBtn, deleteBtn);
}
}).catch(function() {
showTelegramUnconfigured(statusBox, registerBtn, deleteBtn);
});
// Webhook registrieren
registerBtn.addEventListener('click', async function() {
var baseUrl = prompt('Base-URL der App:', window.location.origin);
if (!baseUrl) return;
registerBtn.disabled = true;
registerBtn.textContent = 'Wird registriert...';
try {
// Erst Token + Secret speichern (falls geändert)
await saveTelegramConfig(sessionId, tokenInput, secretInput);
var res = await apiSessionTelegramRegisterWebhook(sessionId, baseUrl.trim());
if (res.ok) {
showToast('Webhook registriert!', 'success');
} else {
showToast('Antwort: ' + JSON.stringify(res), 'error');
}
} catch(e) {
showToast('Fehler: ' + e.message, 'error');
} finally {
registerBtn.disabled = false;
registerBtn.textContent = 'Webhook registrieren';
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
}
});
// Webhook entfernen
deleteBtn.addEventListener('click', async function() {
if (!confirm('Webhook wirklich entfernen?')) return;
deleteBtn.disabled = true;
deleteBtn.textContent = 'Wird entfernt...';
try {
var res = await apiSessionTelegramDeleteWebhook(sessionId);
if (res.ok) {
showToast('Webhook entfernt', 'success');
} else {
showToast('Antwort: ' + JSON.stringify(res), 'error');
}
} catch(e) {
showToast('Fehler: ' + e.message, 'error');
} finally {
deleteBtn.disabled = false;
deleteBtn.textContent = 'Webhook entfernen';
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
}
});
}
function showTelegramUnconfigured(statusBox, registerBtn, deleteBtn) {
statusBox.innerHTML = '<p>⚠️ Kein Bot-Token konfiguriert. Speichere zuerst die Session mit einem Token.</p>';
statusBox.hidden = false;
registerBtn.hidden = true;
deleteBtn.hidden = true;
}
async function refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn) {
statusBox.hidden = true;
try {
var info = await apiSessionTelegramWebhookInfo(sessionId);
var result = info.result || {};
var url = result.url || '';
var pending = result.pending_update_count || 0;
var lastError = result.last_error_message;
var active = !!url;
var html = '';
if (active) {
html += '<p>✅ <strong>Webhook aktiv</strong></p>';
html += '<p><strong>URL:</strong> ' + escapeHtml(url) + '</p>';
html += '<p><strong>Ausstehend:</strong> ' + pending + '</p>';
if (lastError) html += '<p class="tg-error"><strong>Letzter Fehler:</strong> ' + escapeHtml(lastError) + '</p>';
} else {
html += '<p>⚠️ <strong>Webhook nicht registriert</strong></p>';
}
statusBox.innerHTML = html;
statusBox.hidden = false;
// Buttons ein/ausblenden je nach Status
registerBtn.hidden = active;
deleteBtn.hidden = !active;
} catch(e) {
statusBox.innerHTML = '<p class="tg-error">Status konnte nicht abgerufen werden: ' + escapeHtml(e.message) + '</p>';
statusBox.hidden = false;
registerBtn.hidden = false;
deleteBtn.hidden = true;
}
}
async function saveTelegramConfig(sessionId, tokenInput, secretInput) {
var token = tokenInput.value.trim();
var secret = secretInput.value.trim();
await apiSessionTelegramSave(sessionId, token, secret);
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}