66 lines
12 KiB
HTML
66 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}AlphaX Agent — AI 记录{% endblock %}
|
|
{% block extra_head_css %}
|
|
<style>
|
|
.shell{width:min(100% - 40px,1280px);margin:0 auto;padding:24px 0 44px}.page-head{display:flex;justify-content:space-between;align-items:flex-end;gap:14px;margin-bottom:16px;flex-wrap:wrap}.page-head h1{font-size:26px;font-weight:900;letter-spacing:-.6px;color:var(--ink)}.page-head p{margin-top:4px;color:var(--stone);font-size:13px;line-height:1.6}.controls{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.select,.btn{height:38px;border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-md);padding:0 12px;font-size:13px;font-weight:800;color:var(--ink)}.btn{cursor:pointer}.layout{display:grid;grid-template-columns:430px minmax(0,1fr);gap:14px;align-items:start}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);min-width:0;overflow:hidden}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:13px 14px;border-bottom:1px solid var(--hairline-soft)}.panel-title{font-size:14px;font-weight:900;color:var(--ink)}.panel-note{font-size:11px;color:var(--stone);font-weight:800}.list{max-height:calc(100vh - 178px);overflow:auto}.row{padding:13px 14px;border-bottom:1px solid var(--hairline-soft);cursor:pointer;transition:.12s;background:var(--canvas)}.row:hover{background:var(--surface)}.row.active{background:rgba(66,98,255,.06);box-shadow:inset 3px 0 0 var(--blue)}.row-top{display:flex;align-items:center;justify-content:space-between;gap:8px}.type{font-size:12px;font-weight:900;color:var(--blue);background:rgba(66,98,255,.08);border-radius:999px;padding:4px 8px;white-space:nowrap}.type.failed{color:var(--red);background:var(--red-light)}.type.skipped{color:var(--stone);background:var(--surface);border:1px solid var(--hairline-soft)}.subject{margin-top:8px;font-size:14px;font-weight:900;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.summary{margin-top:5px;color:var(--slate);font-size:12px;line-height:1.55;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:8px;color:var(--stone);font-size:11px;font-weight:800}.detail{min-height:560px}.detail-body{padding:16px}.hero-card{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--surface);padding:15px;margin-bottom:12px}.hero-title{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.hero-title b{font-size:18px;color:var(--ink)}.badge{display:inline-flex;border-radius:999px;padding:4px 9px;font-size:11px;font-weight:900;background:var(--canvas);border:1px solid var(--hairline-soft);color:var(--slate)}.badge.ok{color:var(--green);background:var(--green-light);border-color:rgba(0,180,115,.18)}.badge.fail{color:var(--red);background:var(--red-light);border-color:rgba(229,62,62,.18)}.plain{margin-top:10px;color:var(--slate);font-size:13px;line-height:1.7}.cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-bottom:12px}.mini{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--surface);padding:11px;min-width:0}.mini span{display:block;color:var(--stone);font-size:10px;font-weight:900}.mini b{display:block;margin-top:4px;color:var(--ink);font-size:13px;word-break:break-word}.section{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--canvas);margin-bottom:12px;overflow:hidden}.section h3{font-size:13px;font-weight:900;color:var(--ink);padding:12px 13px;border-bottom:1px solid var(--hairline-soft);background:var(--surface)}.kv{display:grid;gap:8px;padding:12px}.kv-row{display:grid;grid-template-columns:140px minmax(0,1fr);gap:10px;font-size:12px}.kv-row label{color:var(--stone);font-weight:900}.kv-row div{color:var(--slate);line-height:1.55;word-break:break-word}.chips{display:flex;gap:5px;flex-wrap:wrap}.chip{display:inline-flex;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:999px;padding:4px 8px;font-size:11px;font-weight:800;color:var(--slate)}.json-box{max-height:320px;overflow:auto;background:#101423;color:#dce6ff;border-radius:var(--radius-md);padding:12px;font-size:12px;line-height:1.55;white-space:pre-wrap;word-break:break-word}.empty,.loading{padding:34px 20px;text-align:center;color:var(--stone);font-size:13px}.pager{display:flex;gap:8px;align-items:center}.page-btn{height:34px;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);background:var(--canvas);font-size:12px;font-weight:900;color:var(--ink);padding:0 10px;cursor:pointer}.page-btn:disabled{opacity:.45;cursor:default}@media(max-width:980px){.layout{grid-template-columns:1fr}.list{max-height:none}.cards{grid-template-columns:1fr}}@media(max-width:560px){.shell{width:min(100% - 24px,1280px);padding-top:18px}.kv-row{grid-template-columns:1fr}.page-head h1{font-size:22px}}
|
|
</style>
|
|
{% endblock %}
|
|
{% block content %}
|
|
<div class="shell">
|
|
<div class="page-head">
|
|
<div>
|
|
<h1>AI 记录</h1>
|
|
<p>查看每一次 LLM 解释任务:它看了什么结构化输入、调用了哪个模型、产出了什么解释,以及是否失败。</p>
|
|
</div>
|
|
<div class="controls">
|
|
<select class="select" id="typeSel" onchange="reloadFirst()">
|
|
<option value="">全部类型</option>
|
|
<option value="recommendation">推荐解释</option>
|
|
<option value="sentiment">舆情解读</option>
|
|
<option value="sentiment_batch">批量舆情分析</option>
|
|
<option value="review">复盘 memo</option>
|
|
</select>
|
|
<select class="select" id="statusSel" onchange="reloadFirst()">
|
|
<option value="">全部状态</option>
|
|
<option value="success">成功</option>
|
|
<option value="failed">失败</option>
|
|
<option value="skipped">跳过</option>
|
|
</select>
|
|
<button class="btn" onclick="reloadFirst()">刷新</button>
|
|
</div>
|
|
</div>
|
|
<div class="layout">
|
|
<section class="panel">
|
|
<div class="panel-head">
|
|
<div class="panel-title">调用记录</div>
|
|
<div class="pager"><button class="page-btn" id="prevBtn" onclick="page(-1)">上一页</button><button class="page-btn" id="nextBtn" onclick="page(1)">下一页</button><span class="panel-note" id="countText">--</span></div>
|
|
</div>
|
|
<div class="list" id="list"><div class="loading">加载中...</div></div>
|
|
</section>
|
|
<section class="panel detail">
|
|
<div class="panel-head"><div class="panel-title">这次 AI 做了什么</div><div class="panel-note" id="detailTime">--</div></div>
|
|
<div class="detail-body" id="detail"><div class="empty">选择左侧一条记录查看详情</div></div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
{% block extra_script %}
|
|
<script>
|
|
var state={items:[],selected:null,offset:0,limit:20,total:0,hasMore:false};
|
|
function $(id){return document.getElementById(id);}
|
|
function esc(v){return String(v==null?'':v).replace(/[&<>"']/g,function(c){return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];});}
|
|
function fmtTime(t){if(!t)return'--';var d=new Date(t);if(isNaN(d.getTime()))return t;return (d.getMonth()+1)+'/'+d.getDate()+' '+String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');}
|
|
function statusClass(s){return s==='success'?'ok':s==='failed'?'fail':'';}
|
|
function chipList(items){items=Array.isArray(items)?items:[];return items.length?'<div class="chips">'+items.slice(0,8).map(function(x){return '<span class="chip">'+esc(x)+'</span>';}).join('')+'</div>':'--';}
|
|
function short(v,n){v=String(v||'');return v.length>n?v.slice(0,n)+'...':v;}
|
|
async function load(){try{var url='/api/llm/insights?limit='+state.limit+'&offset='+state.offset+'&target_type='+encodeURIComponent($('typeSel').value||'')+'&status='+encodeURIComponent($('statusSel').value||'');var d=await (await fetch(url)).json();state.items=d.items||[];state.total=d.total||0;state.hasMore=!!d.has_more;if(state.selected&&!state.items.some(function(x){return x.id===state.selected;}))state.selected=null;if(!state.selected&&state.items[0])state.selected=state.items[0].id;renderList();if(state.selected)selectItem(state.selected,true);else $('detail').innerHTML='<div class="empty">暂无 LLM 调用记录</div>';}catch(e){$('list').innerHTML='<div class="empty">加载失败</div>';}}
|
|
function renderList(){var start=state.offset+1,end=state.offset+state.items.length;$('countText').textContent=state.total?start+'-'+end+' / '+state.total:'0 条';$('prevBtn').disabled=state.offset<=0;$('nextBtn').disabled=!state.hasMore;if(!state.items.length){$('list').innerHTML='<div class="empty">暂无记录</div>';return;}$('list').innerHTML=state.items.map(function(x){var active=x.id===state.selected?' active':'';var cls=x.status==='failed'?' failed':x.status==='skipped'?' skipped':'';return '<div class="row'+active+'" onclick="selectItem('+x.id+')"><div class="row-top"><span class="type'+cls+'">'+esc(x.type_label)+'</span><span class="badge '+statusClass(x.status)+'">'+esc(x.status_label)+'</span></div><div class="subject">'+esc(x.subject||'未命名对象')+'</div><div class="summary">'+esc(short(x.summary||x.error||'暂无输出摘要',150))+'</div><div class="meta"><span>'+fmtTime(x.updated_at)+'</span><span>'+esc(x.model||'未记录模型')+'</span><span>'+esc(x.prompt_version||'--')+'</span></div></div>';}).join('');}
|
|
async function selectItem(id,quiet){state.selected=id;renderList();$('detail').innerHTML='<div class="loading">读取详情...</div>';try{var d=await (await fetch('/api/llm/insights/'+id)).json();renderDetail(d);}catch(e){$('detail').innerHTML='<div class="empty">详情加载失败</div>';}}
|
|
function page(step){var next=state.offset+step*state.limit;if(next<0)return;state.offset=next;state.selected=null;load();}
|
|
function reloadFirst(){state.offset=0;state.selected=null;load();}
|
|
function kv(label,val){return '<div class="kv-row"><label>'+esc(label)+'</label><div>'+val+'</div></div>';}
|
|
function renderDetail(d){$('detailTime').textContent=fmtTime(d.updated_at);var input=d.input||{},out=d.content||{};var rawIn=JSON.stringify(input,null,2);var rawOut=JSON.stringify(out,null,2);var evidence=out.key_evidence||out.key_tags||out.theme_tags||[];var risks=out.risk_flags||out.risk_types||[];var watch=out.watch_points||[];var invalid=out.invalid_if||[];var title=d.subject||input.symbol||input.title||input.run_date||'未命名对象';$('detail').innerHTML='<div class="hero-card"><div class="hero-title"><b>'+esc(title)+'</b><span class="badge '+statusClass(d.status)+'">'+esc(d.status_label)+'</span><span class="badge">'+esc(d.type_label)+'</span></div><div class="plain">'+esc(d.summary||d.error||'暂无摘要')+'</div></div><div class="cards"><div class="mini"><span>模型</span><b>'+esc(d.model||'未记录')+'</b></div><div class="mini"><span>提示词版本</span><b>'+esc(d.prompt_version||'--')+'</b></div><div class="mini"><span>对象类型</span><b>'+esc(d.target_type||'--')+'</b></div><div class="mini"><span>对象 ID</span><b>'+esc(d.target_id||'--')+'</b></div></div><div class="section"><h3>AI 输出</h3><div class="kv">'+kv('摘要',esc(out.summary||out.memo||out.why_now_or_not||'--'))+kv('为什么',esc(out.why_now_or_not||out.why_it_matters||'--'))+kv('关键证据',chipList(evidence))+kv('风险提示',chipList(risks))+kv('观察点',chipList(watch))+kv('失效条件',chipList(invalid))+kv('错误信息',esc(d.error||'--'))+'</div></div><div class="section"><h3>这次喂给 AI 的结构化输入</h3><div class="kv">'+kv('标的/主题',esc(input.symbol||input.related_symbol||input.title||input.run_date||'--'))+kv('状态/建议',esc(input.execution_label||input.action_status||input.decision||'--'))+kv('信号',chipList(input.signals||[]))+kv('价格上下文',esc([input.current_price?'现价 '+input.current_price:'',input.entry_price?'参考 '+input.entry_price:'',input.stop_loss?'止损 '+input.stop_loss:'',input.tp1?'TP1 '+input.tp1:''].filter(Boolean).join(' · ')||'--'))+'</div></div><div class="section"><h3>原始输入 JSON</h3><pre class="json-box">'+esc(rawIn)+'</pre></div><div class="section"><h3>原始输出 JSON</h3><pre class="json-box">'+esc(rawOut)+'</pre></div>';}
|
|
load();
|
|
</script>
|
|
{% endblock %}
|