- Specialist Tuning now in a separate editor
- typeBadge formatter completed
This commit is contained in:
257
eveai_app/templates/interaction/tune_specialist.html
Normal file
257
eveai_app/templates/interaction/tune_specialist.html
Normal file
@@ -0,0 +1,257 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user