Files
eveAI/eveai_app/templates/interaction/tune_specialist.html
Josako 3815399a7e - Specialist Tuning now in a separate editor
- typeBadge formatter completed
2025-11-24 15:54:47 +01:00

258 lines
12 KiB
HTML

{% extends 'base.html' %}
{% from "macros.html" import render_field, render_selectable_table %}
{% block title %}Tune Specialist{% endblock %}
{% block content_title %}Tune Specialist{% endblock %}
{% block content_description %}Edit agents, tasks and tools for specialist <b>{{ specialist.name }}</b> (ID: {{ specialist.id }}) <br>
({{ specialist.type }} - {{ specialist.type_version }}){% endblock %}
{% block content %}
<div class="container-fluid px-0">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mt-3 mb-3">
<div>
<a href="{{ prefixed_url_for('interaction_bp.specialists', for_redirect=True) }}" class="btn btn-outline-secondary btn-sm">Back to Specialists</a>
<a href="{{ prefixed_url_for('interaction_bp.edit_specialist', specialist_id=specialist.id, for_redirect=True) }}" class="btn btn-outline-primary btn-sm">Edit Specialist Metadata</a>
</div>
</div>
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">Components</h6>
{# Gebruik de generieke EveAI list view template zodat we dezelfde
Tabulator + EveAI.ListView infrastructuur hergebruiken als elders.
We mappen hier de component-variabelen naar de namen die
eveai_list_view.html verwacht. #}
{% with
title=components_title,
description=components_description,
data=components_data,
columns=components_columns,
actions=components_actions,
initial_sort=components_initial_sort,
table_id=components_table_id,
table_height=components_table_height
%}
{% include 'eveai_list_view.html' %}
{% endwith %}
</div>
</div>
</div>
</div>
</div>
<!-- Component Editor Modal (intentionally placed outside the main content to avoid nested forms) -->
<div class="modal fade" id="componentModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="componentModalLabel">Edit Component</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="componentModalBody">
<!-- Partial form will be injected here -->
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const componentModalEl = document.getElementById('componentModal');
const componentModalBody = document.getElementById('componentModalBody');
const componentModalLabel = document.getElementById('componentModalLabel');
let componentModal;
const tableId = '{{ components_table_id }}';
// Initialiseer de specialist components tabel via de generieke EveAI.ListView
function initializeComponentsTable(retriesLeft = 50) {
if (!window.EveAI || !window.EveAI.ListView || !window.EveAI.ListView.initialize) {
if (retriesLeft <= 0) {
console.error('EveAI.ListView.initialize niet beschikbaar voor components tabel');
return;
}
return setTimeout(() => initializeComponentsTable(retriesLeft - 1), 100);
}
// Bouw dezelfde configuratie op als in list_view.html, maar dan voor de components
const tableConfig = {
data: {{ components_data | tojson }},
columns: {{ components_columns | tojson }},
initialSort: {{ components_initial_sort | tojson }},
actions: {{ components_actions | tojson }},
tableHeight: {{ components_table_height|default(600) }},
selectable: true
};
const tabulatorTable = window.EveAI.ListView.initialize(tableId, tableConfig);
if (!tabulatorTable) {
console.error('Kon de Tabulator tabel voor specialist components niet initialiseren');
return;
}
console.log('Specialist components Tabulator tabel succesvol geïnitialiseerd');
}
// Registreer de pagina-specifieke embedded handler voor de actie 'edit_component'
function registerEmbeddedHandler(retriesLeft = 50) {
if (!window.EveAI || !window.EveAI.ListView || !window.EveAI.ListView.instances) {
if (retriesLeft <= 0) {
console.error('EveAI.ListView.instances niet beschikbaar voor components tabel');
return;
}
return setTimeout(() => registerEmbeddedHandler(retriesLeft - 1), 100);
}
const instances = window.EveAI.ListView.instances;
if (!instances[tableId] || !instances[tableId].table) {
if (retriesLeft <= 0) {
console.error(`Geen EveAI.ListView instance gevonden voor tabel ${tableId}`);
return;
}
return setTimeout(() => registerEmbeddedHandler(retriesLeft - 1), 100);
}
window.EveAI.ListView.embeddedHandlers = window.EveAI.ListView.embeddedHandlers || {};
window.EveAI.ListView.embeddedHandlers[tableId] = function(action, row, tId) {
if (action !== 'edit_component' || !row) return;
const id = row.id;
const type = row.type_name; // 'agent' | 'task' | 'tool'
// Build edit URL from server-side templates with placeholder 0
const editUrls = {
agent: "{{ prefixed_url_for('interaction_bp.edit_agent', agent_id=0) }}",
task: "{{ prefixed_url_for('interaction_bp.edit_task', task_id=0) }}",
tool: "{{ prefixed_url_for('interaction_bp.edit_tool', tool_id=0) }}",
};
const url = (editUrls[type] || '').replace('/0', `/${id}`);
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(resp => { if (!resp.ok) throw new Error(`HTTP ${resp.status}`); return resp.text(); })
.then(html => {
componentModalBody.innerHTML = html;
componentModalLabel.textContent = `Edit ${type.charAt(0).toUpperCase()+type.slice(1)}`;
componentModalEl.dataset.componentType = type;
componentModalEl.dataset.componentId = id;
// Helper om modal te openen zodra Bootstrap beschikbaar is
function openModal() {
try {
if (!componentModal) componentModal = new bootstrap.Modal(componentModalEl);
componentModal.show();
} catch (e) {
console.error('Failed to open Bootstrap modal:', e);
alert('Kan de editor niet openen (Bootstrap modal ontbreekt).');
}
}
if (window.bootstrap && typeof bootstrap.Modal === 'function') {
openModal();
} else {
// Fallback: laad Bootstrap bundle dynamisch en open daarna
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js';
script.onload = () => openModal();
script.onerror = () => {
console.error('Kon Bootstrap bundle niet laden');
alert('Kan de editor niet openen omdat Bootstrap JS ontbreekt.');
};
document.head.appendChild(script);
}
})
.catch(err => {
console.error('Error loading editor:', err);
alert('Error loading editor. Please try again.');
});
};
}
// Eerst de tabel initialiseren, daarna de embedded handler registreren
initializeComponentsTable();
registerEmbeddedHandler();
// Refresh de components list data na een succesvolle save
function refreshComponentsData() {
const url = "{{ prefixed_url_for('interaction_bp.specialist_components_data', specialist_id=specialist_id) }}";
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' }})
.then(resp => resp.json())
.then(payload => {
if (!window.EveAI || !window.EveAI.ListView || !window.EveAI.ListView.instances) return;
const instance = window.EveAI.ListView.instances[tableId];
if (instance && instance.table && Array.isArray(payload.data)) {
instance.table.setData(payload.data);
}
})
.catch(err => console.error('Failed to refresh components data', err));
}
// Intercepteer form submits binnen de modal (ook Enter-key)
componentModalBody.addEventListener('submit', function(e) {
const formEl = e.target.closest('#componentEditForm');
if (!formEl) return; // Niet ons form
e.preventDefault();
const formData = new FormData(formEl);
const componentType = componentModalEl.dataset.componentType;
const componentId = componentModalEl.dataset.componentId;
// Bouw prefix-bewuste absolute save URL op basis van server-side templates
const saveUrls = {
agent: "{{ prefixed_url_for('interaction_bp.save_agent', agent_id=0) }}",
task: "{{ prefixed_url_for('interaction_bp.save_task', task_id=0) }}",
tool: "{{ prefixed_url_for('interaction_bp.save_tool', tool_id=0) }}",
};
const urlTemplate = saveUrls[componentType];
const saveUrl = urlTemplate.replace('/0', `/${componentId}`);
fetch(saveUrl, {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(async resp => {
const ct = resp.headers.get('Content-Type') || '';
if (resp.ok) {
// Verwacht JSON succes
let data = null;
try { data = await resp.json(); } catch (_) {}
if (data && data.success) {
if (componentModal) {
componentModal.hide();
}
refreshComponentsData();
return;
}
throw new Error(data && data.message ? data.message : 'Save failed');
}
// Validatiefouten (400) → server stuurt HTML partial terug
if (resp.status === 400 && ct.includes('text/html')) {
const html = await resp.text();
componentModalBody.innerHTML = html;
return;
}
// Andere fouten
let message = 'Save failed';
if (ct.includes('application/json')) {
try {
const data = await resp.json();
if (data && data.message) message = data.message;
} catch (_) {}
}
throw new Error(message + ` (HTTP ${resp.status})`);
})
.catch(err => {
console.error('Error saving component:', err);
alert(err.message || 'Error saving component');
});
});
});
</script>
{% endblock %}