Auto-save self-analysis reports to DB with browsable history
Database: - self_reports table: report_type, model, report text, data_size, timestamp - Reports auto-saved on generation (no extra step needed) API: - GET /api/self-reports — list all past reports (id, type, model, size, date) - GET /api/self-reports/:id — full report text UI: - "✓ Saved as report #N" indicator after generation - "Past Reports (N)" section below self-analysis buttons - Click any past report → expands inline (toggle on/off) - Shows: type, model, timestamp for each saved report - Reports persist across page refreshes and restarts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
28e641f939
commit
804898b658
118
llm_team_ui.py
118
llm_team_ui.py
@ -3388,6 +3388,7 @@ LAB_HTML = r"""
|
||||
<div class="card" style="border-color:rgba(74,222,128,0.2)">
|
||||
<h3 style="color:var(--green)">Self-Analysis <span style="font-size:9px;color:var(--text2);font-weight:400;text-transform:none;letter-spacing:0">AI reports from your own system data</span></h3>
|
||||
<div style="display:grid;gap:8px" id="self-reports"></div>
|
||||
<div id="past-reports" style="margin-top:12px"></div>
|
||||
</div>
|
||||
<div class="card" id="templates-card">
|
||||
<h3>Experiment Templates <span style="font-size:9px;color:var(--text2);font-weight:400;text-transform:none;letter-spacing:0">click to auto-fill the create form</span></h3>
|
||||
@ -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/<int:rid>")
|
||||
@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")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user