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
273 lines
10 KiB
JavaScript
273 lines
10 KiB
JavaScript
// ── Agent Page ──
|
||
|
||
let _agentAvatarData = null;
|
||
let _agentPasteHandler = null;
|
||
|
||
registerPage('agent', {
|
||
async init(params) {
|
||
const agentId = params.id;
|
||
const isNew = !agentId;
|
||
let agent = null;
|
||
|
||
if (!isNew) {
|
||
agent = await apiGetAgent(agentId);
|
||
if (!agent) return navigateTo('dashboard');
|
||
}
|
||
|
||
const title = document.getElementById('agentPageTitle');
|
||
const nameEl = document.getElementById('agentName');
|
||
const descEl = document.getElementById('agentDescription');
|
||
const modelEl = document.getElementById('agentModel');
|
||
const toolChoiceEl = document.getElementById('agentToolChoice');
|
||
const autoCommitEl = document.getElementById('agentAutoCommit');
|
||
const useSoulEl = document.getElementById('agentUseSoul');
|
||
const useIdentityEl = document.getElementById('agentUseIdentity');
|
||
const useUserInfoEl = document.getElementById('agentUseUserInfo');
|
||
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');
|
||
const commandsList = document.getElementById('agentCommandsList');
|
||
const addCommandBtn = document.getElementById('agentAddCommandBtn');
|
||
const skillsSection = document.getElementById('agentSkillsSection');
|
||
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 || '';
|
||
descEl.value = agent.description || '';
|
||
modelEl.value = agent.model || 'sonnet';
|
||
toolChoiceEl.checked = (agent.toolChoice || 'any') === 'any';
|
||
autoCommitEl.value = agent.autoCommit || '';
|
||
useSoulEl.checked = agent.useSoul !== false;
|
||
useIdentityEl.checked = agent.useIdentity !== false;
|
||
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 = `<img src="${agent.avatar}" alt="Avatar" style="scale: ${agent.avatarScale || 1}">`;
|
||
scaleRow.hidden = false;
|
||
}
|
||
|
||
// Custom Commands laden
|
||
loadCommandsIntoUI(commandsList, agent.customCommands);
|
||
|
||
// Skills laden
|
||
const [allSkills, agentSkills] = await Promise.all([
|
||
apiGetSkills(),
|
||
apiGetAgentSkills(agentId)
|
||
]);
|
||
const assignedSlugs = agentSkills.map(s => s.slug);
|
||
skillsSection.hidden = false;
|
||
renderSkillCheckboxes(skillList, allSkills, assignedSlugs);
|
||
} else {
|
||
// Neue Agents: Skills-Liste auch anzeigen
|
||
const allSkills = await apiGetSkills();
|
||
if (allSkills.length > 0) {
|
||
skillsSection.hidden = false;
|
||
renderSkillCheckboxes(skillList, allSkills, []);
|
||
}
|
||
}
|
||
|
||
// 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', () => {
|
||
const file = avatarInput.files[0];
|
||
if (!file) return;
|
||
const reader = new FileReader();
|
||
reader.onload = () => {
|
||
_agentAvatarData = reader.result;
|
||
avatarPreview.innerHTML = `<img src="${reader.result}" alt="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 = `<img src="${reader.result}" alt="Avatar">`;
|
||
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();
|
||
});
|
||
|
||
// Speichern
|
||
saveBtn.addEventListener('click', async () => {
|
||
const data = {
|
||
name: nameEl.value.trim() || 'Unbenannt',
|
||
description: descEl.value.trim(),
|
||
model: modelEl.value,
|
||
toolChoice: toolChoiceEl.checked ? 'any' : 'auto',
|
||
autoCommit: autoCommitEl.value,
|
||
useSoul: useSoulEl.checked,
|
||
useIdentity: useIdentityEl.checked,
|
||
useUserInfo: useUserInfoEl.checked,
|
||
useMemory: useMemoryEl.checked,
|
||
webSearchVersion: webSearchVersionEl.value,
|
||
avatar: _agentAvatarData,
|
||
avatarScale: parseFloat(scaleInput.value) || 1,
|
||
customCommands: getCommandsFromUI(commandsList)
|
||
};
|
||
|
||
if (isNew) {
|
||
const created = await apiCreateAgent(data);
|
||
// Skills zuweisen falls welche ausgewählt
|
||
const selectedSlugs = getSelectedSkills(skillList);
|
||
if (selectedSlugs.length > 0) {
|
||
await apiSetAgentSkills(created.agentId, selectedSlugs);
|
||
}
|
||
} else {
|
||
await apiUpdateAgent(agentId, data);
|
||
// Skills aktualisieren
|
||
const selectedSlugs = getSelectedSkills(skillList);
|
||
await apiSetAgentSkills(agentId, selectedSlugs);
|
||
}
|
||
navigateTo('dashboard');
|
||
});
|
||
|
||
// Abbrechen
|
||
cancelBtn.addEventListener('click', () => navigateTo('dashboard'));
|
||
|
||
// Löschen
|
||
deleteBtn.addEventListener('click', async () => {
|
||
if (!confirm('Agent wirklich löschen?')) return;
|
||
await apiDeleteAgent(agentId);
|
||
navigateTo('dashboard');
|
||
});
|
||
},
|
||
destroy() {
|
||
if (_agentPasteHandler) {
|
||
document.removeEventListener('paste', _agentPasteHandler);
|
||
_agentPasteHandler = null;
|
||
}
|
||
_agentAvatarData = null;
|
||
}
|
||
});
|
||
|
||
function renderSkillCheckboxes(container, allSkills, assignedSlugs) {
|
||
container.innerHTML = '';
|
||
if (allSkills.length === 0) {
|
||
container.innerHTML = '<div class="skill-empty">Keine Skills verfügbar. Lege Skills in <code>data/skills/</code> ab.</div>';
|
||
return;
|
||
}
|
||
for (const skill of allSkills) {
|
||
const checked = assignedSlugs.includes(skill.slug) ? 'checked' : '';
|
||
const disabled = !skill.hasPromptMd ? 'disabled' : '';
|
||
const version = skill.version ? ` <span class="skill-version">v${skill.version}</span>` : '';
|
||
const desc = skill.description ? `<span class="skill-desc">${skill.description}</span>` : '';
|
||
const warning = !skill.hasPromptMd ? '<span class="skill-warning">⚠ PROMPT.md fehlt – Skill nicht verwendbar</span>' : '';
|
||
const row = document.createElement('label');
|
||
row.className = 'skill-item' + (!skill.hasPromptMd ? ' skill-unavailable' : '');
|
||
row.innerHTML = `
|
||
<input type="checkbox" value="${skill.slug}" ${checked} ${disabled}>
|
||
<span class="skill-name">${skill.name}${version}</span>
|
||
${desc}
|
||
${warning}
|
||
`;
|
||
container.appendChild(row);
|
||
}
|
||
}
|
||
|
||
function getSelectedSkills(container) {
|
||
return Array.from(container.querySelectorAll('input[type="checkbox"]:checked'))
|
||
.map(cb => cb.value);
|
||
}
|