diff --git a/llm_team_ui.py b/llm_team_ui.py
index d1b993f..fc6921a 100644
--- a/llm_team_ui.py
+++ b/llm_team_ui.py
@@ -3388,6 +3388,7 @@ LAB_HTML = r"""
Self-Analysis AI reports from your own system data
+
Experiment Templates click to auto-fill the create form
@@ -3866,14 +3867,84 @@ async function runSelfReport(type, card) {
content.textContent = d.report;
panel.appendChild(content);
card.parentNode.insertBefore(panel, card.nextSibling);
- toast('Report generated', true);
+ if (d.id) {
+ var saved = document.createElement('div');
+ saved.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:9px;color:var(--green);margin-top:8px;text-transform:uppercase;letter-spacing:1px';
+ saved.textContent = '✓ Saved as report #' + d.id;
+ panel.appendChild(saved);
+ }
+ toast('Report generated & saved', true);
+ loadPastReports();
} catch(e) { toast('Error: '+e.message, false); }
btn.textContent = 'Run →';
btn.style.color = 'var(--green)';
card.style.borderColor = origBorder;
}
+async function loadPastReports() {
+ var el = document.getElementById('past-reports');
+ if (!el) return;
+ try {
+ var r = await fetch('/api/self-reports');
+ var d = await r.json();
+ var reports = d.reports || [];
+ if (!reports.length) { el.textContent = ''; return; }
+ el.textContent = '';
+ var title = document.createElement('div');
+ title.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:8px;text-transform:uppercase;letter-spacing:2px;color:var(--text2);margin-bottom:8px;padding-top:8px;border-top:1px solid var(--border)';
+ title.textContent = 'Past Reports (' + reports.length + ')';
+ el.appendChild(title);
+ reports.forEach(function(rpt) {
+ var row = document.createElement('div');
+ row.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid rgba(42,45,53,0.3);cursor:pointer;font-size:11px';
+ row.onmouseenter = function(){row.style.background='rgba(74,222,128,0.03)'};
+ row.onmouseleave = function(){row.style.background='transparent'};
+ var typeEl = document.createElement('span');
+ typeEl.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:9px;text-transform:uppercase;letter-spacing:0.5px;color:var(--green);font-weight:700;min-width:130px';
+ typeEl.textContent = rpt.report_type.replace(/_/g,' ');
+ var modelEl = document.createElement('span');
+ modelEl.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:9px;color:var(--text2);min-width:90px';
+ modelEl.textContent = rpt.model;
+ var dateEl = document.createElement('span');
+ dateEl.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:9px;color:var(--text2);margin-left:auto';
+ dateEl.textContent = new Date(rpt.created_at).toLocaleString();
+ row.appendChild(typeEl); row.appendChild(modelEl); row.appendChild(dateEl);
+ row.onclick = function(){viewPastReport(rpt.id)};
+ el.appendChild(row);
+ });
+ } catch(e) {}
+}
+
+async function viewPastReport(id) {
+ var resultId = 'report-past-' + id;
+ var existing = document.getElementById(resultId);
+ if (existing) { existing.remove(); return; }
+ try {
+ var r = await fetch('/api/self-reports/' + id);
+ var d = await r.json();
+ if (d.error) return;
+ var panel = document.createElement('div');
+ panel.id = resultId;
+ panel.style.cssText = 'background:rgba(0,0,0,0.3);border:2px solid var(--green);border-radius:2px;padding:16px;margin:8px 0;max-height:500px;overflow-y:auto';
+ var title = document.createElement('div');
+ title.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:9px;text-transform:uppercase;letter-spacing:1.5px;color:var(--green);margin-bottom:8px;display:flex;justify-content:space-between';
+ title.textContent = d.report_type.replace(/_/g,' ') + ' — ' + d.model + ' — ' + new Date(d.created_at).toLocaleString();
+ var closeBtn = document.createElement('span');
+ closeBtn.style.cssText = 'cursor:pointer;opacity:0.6';
+ closeBtn.textContent = '✕';
+ closeBtn.onclick = function(e){e.stopPropagation();panel.remove()};
+ title.appendChild(closeBtn);
+ panel.appendChild(title);
+ var content = document.createElement('div');
+ content.style.cssText = 'font-size:12px;line-height:1.7;white-space:pre-wrap;color:var(--text)';
+ content.textContent = d.report;
+ panel.appendChild(content);
+ document.getElementById('past-reports').appendChild(panel);
+ } catch(e) {}
+}
+
renderSelfReports();
+loadPastReports();
function renderTemplates() {
var el = document.getElementById('template-list');
@@ -5689,7 +5760,50 @@ def self_analyze():
except Exception as e:
return jsonify({"error": str(e)}), 500
- return jsonify({"report": report, "type": report_type, "model": model, "data_size": len(context)})
+ # Save to DB
+ report_id = None
+ try:
+ with get_db() as conn:
+ with conn.cursor() as cur:
+ cur.execute("INSERT INTO self_reports (report_type, model, report, data_size) VALUES (%s,%s,%s,%s) RETURNING id",
+ (report_type, model, report, len(context)))
+ report_id = cur.fetchone()[0]
+ conn.commit()
+ except Exception:
+ pass
+
+ return jsonify({"report": report, "type": report_type, "model": model, "data_size": len(context), "id": report_id})
+
+
+@app.route("/api/self-reports")
+@admin_required
+def list_self_reports():
+ try:
+ with get_db() as conn:
+ with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
+ cur.execute("SELECT id, report_type, model, LENGTH(report) as report_len, data_size, created_at FROM self_reports ORDER BY created_at DESC LIMIT 50")
+ rows = cur.fetchall()
+ for r in rows:
+ r["created_at"] = r["created_at"].isoformat()
+ return jsonify({"reports": rows})
+ except Exception as e:
+ return jsonify({"reports": [], "error": str(e)})
+
+
+@app.route("/api/self-reports/")
+@admin_required
+def get_self_report(rid):
+ try:
+ with get_db() as conn:
+ with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
+ cur.execute("SELECT * FROM self_reports WHERE id = %s", (rid,))
+ row = cur.fetchone()
+ if not row:
+ return jsonify({"error": "not found"}), 404
+ row["created_at"] = row["created_at"].isoformat()
+ return jsonify(row)
+ except Exception as e:
+ return jsonify({"error": str(e)}), 500
@app.route("/api/pipelines")