refactor(pose_3d): rimuovi agent/chat, aggiungi cloud+diagnostics+camera polso

Rimosso il modulo agent (LLM chat) e la relativa config OpenRouter.
La tab Pose 3D ora: espone i giunti reali via G1 /diagnostics per
specchiare l'URDF senza IK; acquisisce point cloud Nuwa+D435i
(/api/pose_3d/clouds); mostra il modello 3D della camera sul polso;
degrada gracefully a 200 (wrist=null) quando il robot è offline.
Il vecchio renderer canvas-2D (app.js view3d + diagnostic3d.js) è
stato consolidato nel viewport Three.js di pose_3d.js.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal PastroandClaude Opus 4.7 committed 2026-05-27 17:34:42 +02:00
1 parent 9d30073d57
commit 6c2452fe20
20 files changed
+152757 -2714

No files matched your search

-732
View File
@@ -642,392 +642,6 @@ $('unlockBtn').addEventListener('click', async () => {
} catch (e) { $('eeLog').textContent = e.message; }
});
// ==================== VISTA 3D ====================
// Minimal canvas-2D 3D renderer: orthographic camera with yaw/pitch around a
// target point, painter's-algorithm depth sorting, mouse drag/zoom/pan.
const view3dCanvas = $('view3dCanvas');
const view3dCtx = view3dCanvas.getContext('2d');
const view3d = {
// camera
yaw: -0.6, // around base Z
pitch: 0.5, // 0 = horizontal, π/2 = top-down
scale: 350, // pixels per meter
target: [0.30, 0.0, 0.20], // world point at canvas center
// scene data
wrist: null, // { left:{position,quaternion}, right:{...} }
object: null, // { points:[[x,y,z],...], centroid:[x,y,z], n_points }
plan: null, // { grasp:{position,quaternion_xyzw}, pre_grasp:{position} }
calibrated: true, // false → object data unavailable
lastErr: null,
};
// ---- math helpers ----
function quatToR(q) {
const x=q[0], y=q[1], z=q[2], w=q[3];
const n = x*x + y*y + z*z + w*w;
if (n === 0) return [[1,0,0],[0,1,0],[0,0,1]];
const s = 2/n;
return [
[1 - s*(y*y + z*z), s*(x*y - z*w), s*(x*z + y*w)],
[s*(x*y + z*w), 1 - s*(x*x + z*z), s*(y*z - x*w)],
[s*(x*z - y*w), s*(y*z + x*w), 1 - s*(x*x + y*y)],
];
}
// Project world point → {sx, sy, depth}. depth = camera-axis distance for sorting.
function project(p) {
const dx = p[0] - view3d.target[0];
const dy = p[1] - view3d.target[1];
const dz = p[2] - view3d.target[2];
const cy = Math.cos(view3d.yaw), sy = Math.sin(view3d.yaw);
// Yaw around Z
const x1 = cy*dx + sy*dy;
const y1 = -sy*dx + cy*dy;
const z1 = dz;
// Pitch around X (camera looks roughly toward +Y after yaw, tilting down by pitch)
const cp = Math.cos(view3d.pitch), sp = Math.sin(view3d.pitch);
const y2 = cp*y1 + sp*z1;
const z2 = -sp*y1 + cp*z1;
// Orthographic: screen X = +x1; screen Y = -z2 (so +Z up on screen)
const sx = view3dCanvas.width / 2 + view3d.scale * x1;
const sxy = view3dCanvas.height / 2 - view3d.scale * z2;
// depth: y2 increases away from viewer → larger y2 = farther
return { sx, sy: sxy, depth: y2 };
}
function drawSegment(items, p1, p2, color, width = 2) {
const a = project(p1), b = project(p2);
items.push({ depth: (a.depth + b.depth) / 2, draw: () => {
view3dCtx.strokeStyle = color;
view3dCtx.lineWidth = width;
view3dCtx.beginPath();
view3dCtx.moveTo(a.sx, a.sy);
view3dCtx.lineTo(b.sx, b.sy);
view3dCtx.stroke();
}});
}
function drawDot(items, p, color, radius = 3) {
const a = project(p);
items.push({ depth: a.depth, draw: () => {
view3dCtx.fillStyle = color;
view3dCtx.beginPath();
view3dCtx.arc(a.sx, a.sy, radius, 0, 2 * Math.PI);
view3dCtx.fill();
}});
}
function drawLabel(items, p, text, color, dx = 8, dy = -8) {
const a = project(p);
items.push({ depth: a.depth - 1e-6, draw: () => {
view3dCtx.fillStyle = color;
view3dCtx.font = 'bold 11px ui-monospace, monospace';
view3dCtx.fillText(text, a.sx + dx, a.sy + dy);
}});
}
function drawTriad(items, position, R, len = 0.06) {
// Columns of R = axis directions in base frame.
const axes = [
{ col: 0, color: '#e74c3c' }, // X
{ col: 1, color: '#3ecf8e' }, // Y
{ col: 2, color: '#4da3ff' }, // Z
];
for (const a of axes) {
const tip = [
position[0] + R[0][a.col] * len,
position[1] + R[1][a.col] * len,
position[2] + R[2][a.col] * len,
];
drawSegment(items, position, tip, a.color, 2);
}
}
function drawGroundGrid(items) {
if (!$('view3dShowGrid').checked) return;
const half = 0.6, step = 0.05;
const minor = '#1a1f27', major = '#2a2f37';
for (let x = -half; x <= half + 1e-6; x += step) {
const isMajor = Math.abs(x % 0.10) < 1e-6;
drawSegment(items, [x, -half, 0], [x, half, 0], isMajor ? major : minor, 1);
}
for (let y = -half; y <= half + 1e-6; y += step) {
const isMajor = Math.abs(y % 0.10) < 1e-6;
drawSegment(items, [-half, y, 0], [half, y, 0], isMajor ? major : minor, 1);
}
}
function drawBaseAxes(items) {
drawTriad(items, [0,0,0], [[1,0,0],[0,1,0],[0,0,1]], 0.12);
drawLabel(items, [0.12, 0, 0], 'X', '#e74c3c');
drawLabel(items, [0, 0.12, 0], 'Y', '#3ecf8e');
drawLabel(items, [0, 0, 0.12], 'Z', '#4da3ff');
drawLabel(items, [0,0,0], 'BASE', '#9aa0a6', 8, 14);
}
function drawWrist(items, side, pose, color) {
if (!pose || !pose.position || !pose.quaternion) return;
const p = pose.position;
const R = quatToR(pose.quaternion);
drawTriad(items, p, R, 0.08);
drawDot(items, p, color, 5);
drawLabel(items, p, `polso ${side}`, color, 10, -6);
}
function drawObject(items) {
if (!view3d.object) return;
if ($('view3dShowCloud').checked && view3d.object.points) {
// Cap render to avoid huge per-frame work.
const max = 1500;
const pts = view3d.object.points;
const stride = Math.max(1, Math.ceil(pts.length / max));
for (let i = 0; i < pts.length; i += stride) {
drawDot(items, pts[i], '#aaaaaa', 1.4);
}
}
if (view3d.object.centroid) {
drawDot(items, view3d.object.centroid, '#ffd54f', 6);
drawLabel(items, view3d.object.centroid, 'oggetto', '#ffd54f', 10, -8);
}
}
function drawPlan(items) {
if (!view3d.plan) return;
const g = view3d.plan.grasp, pg = view3d.plan.pre_grasp;
if (!g || !g.position) return;
const R = quatToR(g.quaternion_xyzw);
// grasp pose triad + marker
drawTriad(items, g.position, R, 0.07);
drawDot(items, g.position, '#ff5e7e', 6);
drawLabel(items, g.position, 'grasp', '#ff5e7e', 10, -8);
// pre-grasp marker + approach line
if (pg && pg.position) {
drawDot(items, pg.position, '#ffffff', 4);
drawLabel(items, pg.position, 'pre-grasp', '#ffffff', 10, -8);
drawSegment(items, pg.position, g.position, '#ffffff', 1.5);
}
}
function renderView3d() {
const ctx = view3dCtx;
ctx.fillStyle = '#0d1014';
ctx.fillRect(0, 0, view3dCanvas.width, view3dCanvas.height);
const items = [];
drawGroundGrid(items);
drawBaseAxes(items);
if (view3d.wrist) {
drawWrist(items, 'right', view3d.wrist.right, '#f0ad4e');
drawWrist(items, 'left', view3d.wrist.left, '#9b59b6');
}
drawObject(items);
drawPlan(items);
// Painter's algorithm: far first.
items.sort((a, b) => b.depth - a.depth);
items.forEach(it => it.draw());
// HUD
ctx.fillStyle = '#9aa0a6';
ctx.font = '11px ui-monospace, monospace';
ctx.fillText(`yaw=${(view3d.yaw*180/Math.PI).toFixed(0)}° pitch=${(view3d.pitch*180/Math.PI).toFixed(0)}° scale=${view3d.scale.toFixed(0)} px/m`, 8, 14);
ctx.fillText(`target=[${view3d.target.map(v => v.toFixed(2)).join(', ')}]`, 8, 28);
if (!view3d.calibrated) {
ctx.fillStyle = '#f0ad4e';
ctx.fillText('⚠ calibrazione mancante: punti oggetto non disponibili in frame base', 8, view3dCanvas.height - 8);
}
if (view3d.lastErr) {
ctx.fillStyle = '#e74c3c';
ctx.fillText(view3d.lastErr, 8, view3dCanvas.height - 22);
}
}
// ---- mouse interaction ----
let drag3d = null;
view3dCanvas.addEventListener('mousedown', (ev) => {
drag3d = { x: ev.clientX, y: ev.clientY, pan: ev.shiftKey };
});
window.addEventListener('mouseup', () => { drag3d = null; });
window.addEventListener('mousemove', (ev) => {
if (!drag3d) return;
const dx = ev.clientX - drag3d.x;
const dy = ev.clientY - drag3d.y;
drag3d.x = ev.clientX; drag3d.y = ev.clientY;
if (drag3d.pan) {
// Pan in screen plane → world XY (approx, ignoring pitch foreshortening on Z)
const cy = Math.cos(view3d.yaw), sy = Math.sin(view3d.yaw);
const wx = cy*dx - sy*(-dy) * Math.cos(view3d.pitch);
const wy = sy*dx + cy*(-dy) * Math.cos(view3d.pitch);
view3d.target[0] -= wx / view3d.scale;
view3d.target[1] -= wy / view3d.scale;
// also let vertical drag move target Z when looking near horizontal
view3d.target[2] += (-dy) * Math.sin(view3d.pitch) / view3d.scale;
} else {
view3d.yaw += dx * 0.01;
view3d.pitch += dy * 0.01;
view3d.pitch = Math.max(-Math.PI/2 + 0.05, Math.min(Math.PI/2 - 0.05, view3d.pitch));
}
renderView3d();
});
view3dCanvas.addEventListener('wheel', (ev) => {
ev.preventDefault();
const f = Math.exp(-ev.deltaY * 0.001);
view3d.scale = Math.max(50, Math.min(3000, view3d.scale * f));
renderView3d();
}, { passive: false });
view3dCanvas.addEventListener('dblclick', () => resetView3d());
function resetView3d() {
view3d.yaw = -0.6;
view3d.pitch = 0.5;
view3d.scale = 350;
view3d.target = [0.30, 0.0, 0.20];
renderView3d();
}
$('view3dResetBtn').addEventListener('click', resetView3d);
function autoFitView3d() {
if (!$('view3dAutoFit').checked) return;
const pts = [];
if (view3d.wrist) {
if (view3d.wrist.right && view3d.wrist.right.position) pts.push(view3d.wrist.right.position);
if (view3d.wrist.left && view3d.wrist.left.position) pts.push(view3d.wrist.left.position);
}
if (view3d.object && view3d.object.centroid) pts.push(view3d.object.centroid);
if (view3d.plan && view3d.plan.grasp && view3d.plan.grasp.position) pts.push(view3d.plan.grasp.position);
if (pts.length === 0) return;
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
for (const p of pts) for (let i = 0; i < 3; i++) {
if (p[i] < min[i]) min[i] = p[i];
if (p[i] > max[i]) max[i] = p[i];
}
view3d.target = [(min[0]+max[0])/2, (min[1]+max[1])/2, (min[2]+max[2])/2];
const span = Math.max(0.15, max[0]-min[0], max[1]-min[1], max[2]-min[2]);
// fit ~70% of canvas width
view3d.scale = Math.min(2000, (view3dCanvas.width * 0.7) / (span * 2.5));
}
// ---- data fetching ----
async function refreshView3d() {
view3d.lastErr = null;
const out = { wrist: null, object: null, plan: null, calibrated: true };
const summary = [];
// Wrist (always; doesn't need calibration — already in base frame).
try {
const w = await api('GET', '/api/wrist');
// Server returns {left_wrist, right_wrist}; tolerate {left,right} too.
const data = (w && w.data) || w || {};
out.wrist = {
left: data.left_wrist || data.left || null,
right: data.right_wrist || data.right || null,
};
summary.push(`wrist: ${out.wrist.right ? 'R✓' : 'R—'} ${out.wrist.left ? 'L✓' : 'L—'}`);
} catch (e) {
summary.push(`wrist: err (${e.message})`);
}
// Object points + centroid in base frame.
try {
const body = {
conf: +$('view3dConf').value || null,
class_name: $('view3dClass').value || null,
max_points: 3000,
};
const r = await api('POST', '/api/perception/object_points', body);
const pts = r.points_base || null;
if (!pts) {
out.calibrated = false;
summary.push('oggetto: serve calibrazione');
} else {
let cx=0, cy=0, cz=0;
for (const p of pts) { cx+=p[0]; cy+=p[1]; cz+=p[2]; }
const n = Math.max(1, pts.length);
out.object = {
points: pts,
centroid: [cx/n, cy/n, cz/n],
n_points: pts.length,
detection: r.detection,
};
summary.push(`oggetto: ${pts.length} pt`);
}
} catch (e) {
if (/^412\b/.test(e.message)) out.calibrated = false;
summary.push(`oggetto: ${e.message}`);
}
// Grasp plan (optional).
if ($('view3dShowPlan').checked && out.calibrated && out.object) {
try {
const body = {
conf: +$('view3dConf').value || null,
class_name: $('view3dClass').value || null,
};
const r = await api('POST', '/api/grasp/plan', body);
out.plan = r.plan || null;
if (out.plan && out.plan.grasp) {
const p = out.plan.grasp.position;
summary.push(`grasp: [${p.map(v => v.toFixed(2)).join(', ')}]`);
}
} catch (e) {
summary.push(`plan: ${e.message}`);
}
}
Object.assign(view3d, out);
autoFitView3d();
renderView3d();
$('view3dStatusLine').textContent = summary.join(' · ');
$('view3dStatusLine').style.color = view3d.lastErr ? 'var(--danger)' : 'var(--dim)';
$('view3dOut').textContent = JSON.stringify({
wrist: out.wrist,
object: out.object && {
n_points: out.object.n_points,
centroid: out.object.centroid,
detection: out.object.detection,
},
plan: out.plan,
}, null, 2);
}
$('view3dRefreshBtn').addEventListener('click', async () => {
setStatus('Aggiorno scena 3D…');
try { await refreshView3d(); setStatus('Scena 3D aggiornata', 'ok'); }
catch (e) { setStatus(e.message, 'err'); }
});
// loop
let view3dTimer = null;
let view3dInFlight = false;
async function view3dTick() {
if (view3dInFlight) return;
view3dInFlight = true;
try { await refreshView3d(); } catch {} finally { view3dInFlight = false; }
}
function startView3dLoop() {
if (view3dTimer) return;
const rate = Math.max(200, +$('view3dRateMs').value || 800);
view3dTick();
view3dTimer = setInterval(view3dTick, rate);
const b = $('view3dLoopBtn');
b.textContent = '■ Stop loop';
b.classList.add('running');
}
function stopView3dLoop() {
if (view3dTimer) { clearInterval(view3dTimer); view3dTimer = null; }
const b = $('view3dLoopBtn');
b.textContent = '▶ Loop';
b.classList.remove('running');
}
$('view3dLoopBtn').addEventListener('click', () => view3dTimer ? stopView3dLoop() : startView3dLoop());
// Pause loop when leaving the tab (mirrors the grasp-tab behavior).
document.querySelectorAll('.tab').forEach(btn => btn.addEventListener('click', () => {
if (btn.dataset.tab !== 'view3d' && view3dTimer) stopView3dLoop();
if (btn.dataset.tab === 'view3d') renderView3d();
}));
// Initial empty render so the canvas isn't blank when first shown.
renderView3d();
// ==================== HOME ====================
// Shows reachability + latency for every upstream microservice and lets the
// user switch the active robot (G1 ↔ H1). The orchestrator runs the actual
@@ -1136,349 +750,3 @@ startHomePoll();
} catch {}
refreshWrist();
})();
// ==================== ASSISTENTE AI ====================
// Curated tool-use chat: l'utente parla in linguaggio naturale, il backend
// passa la richiesta a OpenRouter con i tool definiti in agent/tools.py.
// La cronologia è tenuta lato browser e ribaltata indietro a ogni POST.
const agentState = {
history: [], // OpenAI-format messages (escluso system, ricostruito server-side)
visualMessages: [], // {role, content, traceIdx?} per il render
busy: false,
health: null,
};
function agentEscape(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function renderAgentChat() {
const host = $('agentChat');
host.innerHTML = '';
if (agentState.visualMessages.length === 0) {
const sys = document.createElement('div');
sys.className = 'agent-msg system';
sys.textContent = 'Conversazione vuota. Scrivi una richiesta sotto.';
host.appendChild(sys);
return;
}
for (const m of agentState.visualMessages) {
const div = document.createElement('div');
div.className = `agent-msg ${m.role}`;
div.textContent = m.content || '';
if (m.tools && m.tools.length) {
const meta = document.createElement('div');
meta.className = 'meta';
for (const t of m.tools) {
const pill = document.createElement('span');
pill.className = 'agent-tool-pill' + (t.ok ? '' : ' err');
pill.textContent = `${t.name} · ${t.latency_ms}ms`;
pill.title = JSON.stringify(t.args || {}, null, 2);
meta.appendChild(pill);
}
div.appendChild(meta);
}
host.appendChild(div);
}
host.scrollTop = host.scrollHeight;
}
async function refreshAgentHealth() {
try {
const h = await api('GET', '/api/agent/health');
agentState.health = h;
const el = $('agentHealth');
if (h.configured) {
el.textContent = `pronto · modello ${h.model}`;
el.style.color = 'var(--ok)';
$('agentSendBtn').disabled = false;
} else {
el.textContent = `API key mancante (env ${h.api_key_env}): chat disabilitata`;
el.style.color = 'var(--danger)';
$('agentSendBtn').disabled = true;
}
} catch (e) {
$('agentHealth').textContent = `errore: ${e.message}`;
$('agentHealth').style.color = 'var(--danger)';
}
// mirror del robot attivo nella pill della tab
try {
const r = await api('GET', '/api/home/robot');
const pill = $('agentActiveRobotPill');
if (pill) pill.textContent = `robot: ${(r.active || '—').toUpperCase()}`;
} catch {}
}
async function sendAgentMessage() {
if (agentState.busy) return;
const input = $('agentInput');
const message = input.value.trim();
if (!message) return;
agentState.busy = true;
$('agentSendBtn').disabled = true;
$('agentStatus').textContent = 'invio…';
$('agentStatus').style.color = 'var(--dim)';
agentState.visualMessages.push({ role: 'user', content: message });
renderAgentChat();
input.value = '';
try {
const r = await api('POST', '/api/agent/chat', {
message,
history: agentState.history,
});
agentState.history = r.history || [];
const tools = (r.tool_calls || []).map(tc => ({
name: tc.name, ok: tc.result && tc.result.ok !== false,
latency_ms: tc.latency_ms, args: tc.arguments,
}));
agentState.visualMessages.push({
role: 'assistant', content: r.reply || '(nessuna risposta)',
tools,
});
renderAgentChat();
// Tool trace dettagliata nel pannello di destra.
if (r.tool_calls && r.tool_calls.length) {
$('agentTrace').textContent = JSON.stringify(r.tool_calls, null, 2);
} else {
$('agentTrace').textContent = '(nessuna chiamata)';
}
const usage = r.usage || {};
const u = usage.total_tokens != null ? `· ${usage.total_tokens} tokens` : '';
$('agentStatus').textContent = `ok · ${r.iterations} iterazioni · ${tools.length} tool ${u}`;
$('agentStatus').style.color = 'var(--ok)';
// se la risposta ha cambiato robot attivo, aggiorna pill
refreshAgentHealth().catch(() => {});
} catch (e) {
agentState.visualMessages.push({
role: 'error', content: e.message,
});
renderAgentChat();
$('agentStatus').textContent = `errore: ${e.message}`;
$('agentStatus').style.color = 'var(--danger)';
} finally {
agentState.busy = false;
$('agentSendBtn').disabled = !(agentState.health && agentState.health.configured);
}
}
$('agentSendBtn').addEventListener('click', sendAgentMessage);
$('agentInput').addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
sendAgentMessage();
}
});
$('agentClearBtn').addEventListener('click', () => {
if (agentState.visualMessages.length === 0 && agentState.history.length === 0) return;
if (!confirm('Resetta la conversazione corrente?')) return;
agentState.history = [];
agentState.visualMessages = [];
renderAgentChat();
$('agentTrace').textContent = '(nessuna chiamata)';
$('agentStatus').textContent = 'conversazione svuotata';
});
// ----- Task salvati -----
async function loadAgentTasks() {
const ul = $('agentTaskList');
try {
const r = await api('GET', '/api/agent/tasks');
const items = r.tasks || [];
ul.innerHTML = '';
if (items.length === 0) {
ul.innerHTML = '<li class="dim">(nessun task salvato)</li>';
return;
}
for (const it of items) {
const li = document.createElement('li');
const name = document.createElement('span');
name.className = 'task-name';
name.textContent = it.name;
const desc = document.createElement('span');
desc.className = 'task-desc';
desc.textContent = it.description || it.prompt;
desc.title = it.prompt;
const useBtn = document.createElement('button');
useBtn.className = 'link-btn';
useBtn.textContent = '↥ usa';
useBtn.title = 'Carica questo prompt nella casella di input';
useBtn.addEventListener('click', () => {
$('agentInput').value = it.prompt;
$('agentInput').focus();
});
const runBtn = document.createElement('button');
runBtn.className = 'link-btn';
runBtn.textContent = '▶ esegui';
runBtn.addEventListener('click', () => {
$('agentInput').value = it.prompt;
sendAgentMessage();
});
const delBtn = document.createElement('button');
delBtn.className = 'link-btn';
delBtn.textContent = '✕';
delBtn.title = 'Elimina';
delBtn.addEventListener('click', async () => {
if (!confirm(`Eliminare task "${it.name}"?`)) return;
try {
await api('DELETE', `/api/agent/tasks/${encodeURIComponent(it.name)}`);
loadAgentTasks();
} catch (e) { toast('Eliminazione fallita', e.message, false); }
});
li.appendChild(name);
li.appendChild(desc);
li.appendChild(useBtn);
li.appendChild(runBtn);
li.appendChild(delBtn);
ul.appendChild(li);
}
} catch (e) {
ul.innerHTML = `<li class="err">errore: ${agentEscape(e.message)}</li>`;
}
}
$('agentTaskSaveBtn').addEventListener('click', async (ev) => {
const name = $('agentTaskName').value.trim();
const prompt = $('agentInput').value.trim();
const description = $('agentTaskDesc').value.trim() || null;
if (!name) { toast('Manca il nome', 'Indica un nome per il task', false); return; }
if (!prompt) { toast('Prompt vuoto', 'Scrivi prima il prompt nella casella', false); return; }
try {
await api('POST', '/api/agent/tasks', { name, prompt, description });
flashBtn(ev.currentTarget, true);
toast('Task salvato', name, true);
$('agentTaskName').value = '';
$('agentTaskDesc').value = '';
loadAgentTasks();
} catch (e) {
flashBtn(ev.currentTarget, false);
toast('Salvataggio fallito', e.message, false);
}
});
$('agentTaskReloadBtn').addEventListener('click', loadAgentTasks);
// Carica health + task quando si apre la tab Assistente AI.
document.querySelectorAll('.tab').forEach(btn => btn.addEventListener('click', () => {
if (btn.dataset.tab === 'agent') {
refreshAgentHealth();
loadAgentTasks();
}
}));
// Bootstrap iniziale (anche se la tab non è attiva, così la pill si popola).
refreshAgentHealth();
loadAgentTasks();
// ==================== IMPOSTAZIONI AI ====================
async function loadAiSettings() {
const select = $('aiModelSelect');
// Lista modelli (statica con override custom): si carica una sola volta.
if (select && select.options.length <= 1) {
try {
const r = await api('GET', '/api/agent/known_models');
select.innerHTML = '';
for (const m of r.models) {
const opt = document.createElement('option');
opt.value = m.id;
opt.textContent = `${m.id} — ${m.label}`;
select.appendChild(opt);
}
} catch (e) {
select.innerHTML = `<option value="">errore: ${e.message}</option>`;
}
}
// Carica config corrente
try {
const cfg = await api('GET', '/api/config');
const o = (cfg.chat && cfg.chat.openrouter) || {};
const model = o.model || 'anthropic/claude-sonnet-4.5';
if ([...$('aiModelSelect').options].some(opt => opt.value === model)) {
$('aiModelSelect').value = model;
$('aiModelCustom').value = '';
} else {
$('aiModelCustom').value = model;
}
$('aiTemperature').value = (o.temperature != null) ? o.temperature : 0.1;
$('aiMaxIterations').value = o.max_tool_iterations || 8;
$('aiTimeout').value = o.timeout_s || 60;
$('aiPromptAddendum').value = o.user_prompt_addendum || '';
$('aiSettingsStatus').textContent = 'caricato da config';
$('aiSettingsStatus').style.color = 'var(--dim)';
} catch (e) {
$('aiSettingsStatus').textContent = `errore: ${e.message}`;
$('aiSettingsStatus').style.color = 'var(--danger)';
}
}
async function saveAiSettings(btn) {
const customModel = $('aiModelCustom').value.trim();
const model = customModel || $('aiModelSelect').value;
if (!model) { toast('Modello mancante', 'Seleziona o scrivi un modello', false); return; }
const temperature = parseFloat($('aiTemperature').value);
const max_tool_iterations = parseInt($('aiMaxIterations').value, 10);
const timeout_s = parseFloat($('aiTimeout').value);
if (Number.isNaN(temperature) || temperature < 0 || temperature > 2) {
toast('Temperature non valido', '0..2', false); return;
}
if (Number.isNaN(max_tool_iterations) || max_tool_iterations < 1) {
toast('Max iterations non valido', '>=1', false); return;
}
const patch = {
chat: { openrouter: {
model,
temperature,
max_tool_iterations,
timeout_s,
user_prompt_addendum: $('aiPromptAddendum').value,
}},
};
try {
await api('PUT', '/api/config', patch);
flashBtn(btn, true);
toast('Salvato', `model=${model}, T=${temperature}`, true);
$('aiSettingsStatus').textContent = `salvato · model=${model} · T=${temperature}`;
$('aiSettingsStatus').style.color = 'var(--ok)';
refreshAgentHealth().catch(() => {});
} catch (e) {
flashBtn(btn, false);
toast('Errore salvataggio', e.message, false);
$('aiSettingsStatus').textContent = e.message;
$('aiSettingsStatus').style.color = 'var(--danger)';
}
}
async function previewSystemPrompt() {
const pre = $('aiPromptPreview');
pre.textContent = 'caricamento…';
try {
const r = await api('GET', '/api/agent/system_prompt_preview');
pre.textContent = r.system_prompt || '(vuoto)';
} catch (e) {
pre.textContent = `errore: ${e.message}`;
}
}
$('aiSaveBtn').addEventListener('click', (ev) => saveAiSettings(ev.currentTarget));
$('aiReloadBtn').addEventListener('click', loadAiSettings);
$('aiResetTempBtn').addEventListener('click', () => { $('aiTemperature').value = 0.1; });
$('aiPromptPreviewBtn').addEventListener('click', previewSystemPrompt);
$('aiPromptCopyBtn').addEventListener('click', async () => {
try {
await navigator.clipboard.writeText($('aiPromptPreview').textContent);
toast('Copiato', 'Prompt nel clipboard', true);
} catch (e) { toast('Copia fallita', e.message, false); }
});
// Quando l'utente seleziona un modello dalla combo, svuota il custom (priorità chiara).
$('aiModelSelect').addEventListener('change', () => { $('aiModelCustom').value = ''; });
// Carica i valori quando si apre la tab.
document.querySelectorAll('.tab').forEach(btn => btn.addEventListener('click', () => {
if (btn.dataset.tab === 'ai-settings') {
loadAiSettings();
previewSystemPrompt();
}
}));