alphax/static/llm_insights.html
2026-06-08 09:34:34 +08:00

74 lines
18 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;align-items:center;gap:4px;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:999px;padding:4px 8px;font-size:11px;font-weight:800;color:var(--slate)}.chip.good{color:var(--green);background:var(--green-light);border-color:rgba(0,180,115,.18)}.chip.bad{color:var(--red);background:var(--red-light);border-color:rgba(229,62,62,.18)}.chip.warn{color:#a16207;background:#fff7d6;border-color:#fde68a}.insight-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;padding:12px}.insight-card{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:11px;min-width:0}.insight-card b{display:block;color:var(--ink);font-size:13px;margin-bottom:5px}.insight-card p{color:var(--slate);font-size:12px;line-height:1.6;margin:0}.data-table{width:100%;border-collapse:collapse;font-size:12px}.data-table th,.data-table td{border-bottom:1px solid var(--hairline-soft);padding:9px 10px;text-align:left;vertical-align:top}.data-table th{color:var(--stone);font-weight:900;background:var(--surface);white-space:nowrap}.data-table td{color:var(--slate);line-height:1.5}.scroll-x{overflow:auto}.muted{color:var(--stone)}.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,.insight-grid{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 {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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 directionLabel(v){return {positive:'利好',negative:'利空',risk:'风险',neutral:'中性',watch:'观察',avoid:'规避',verify:'验证'}[String(v||'')]||v||'--';}
function chipClass(v){v=String(v||'').toLowerCase();if(['positive','watch','verify','risk_on'].includes(v))return' good';if(['negative','avoid','risk_off','high'].includes(v))return' bad';if(['risk','medium','neutral'].includes(v))return' warn';return'';}
function table(headers,rows){if(!rows||!rows.length)return'<div class="empty">暂无结构化条目</div>';return'<div class="scroll-x"><table class="data-table"><thead><tr>'+headers.map(function(h){return'<th>'+esc(h[0])+'</th>';}).join('')+'</tr></thead><tbody>'+rows.map(function(r){return'<tr>'+headers.map(function(h){return'<td>'+h[1](r)+'</td>';}).join('')+'</tr>';}).join('')+'</tbody></table></div>';}
function textOrDash(v){return esc(v||'--');}
function renderSentimentOutput(out,d){var themes=out.hot_themes||[],coins=out.coin_impacts||[],risks=out.risk_events||[],suggestions=out.suggestions||[],watch=out.watchlist||[];return '<div class="section"><h3>AI 输出 · 舆情研判</h3><div class="kv">'+kv('市场情绪','<span class="chip'+chipClass(out.market_mood)+'">'+esc(out.market_mood||'--')+'</span>')+kv('核心摘要',textOrDash(out.summary))+kv('专业判断',textOrDash(out.professional_view))+kv('错误信息',textOrDash(d.error))+'</div></div><div class="section"><h3>主线叙事</h3><div class="insight-grid">'+(themes.length?themes.map(function(x){return'<div class="insight-card"><b>'+esc(x.theme||'未命名主题')+' <span class="chip'+chipClass(x.impact)+'">'+esc(directionLabel(x.impact))+'</span></b><p>'+esc(x.impact||x.reason||'')+'</p><div class="plain">置信度 '+esc(x.confidence||0)+' · '+esc((x.symbols||[]).join(', ')||'--')+'</div></div>';}).join(''):'<div class="empty">暂无主线</div>')+'</div></div><div class="section"><h3>币种影响</h3>'+table([['币种',function(x){return'<b>'+esc(x.symbol||'--')+'</b>'}],['方向',function(x){return'<span class="chip'+chipClass(x.direction)+'">'+esc(directionLabel(x.direction))+'</span>'}],['置信度',function(x){return esc(x.confidence||0)}],['窗口',function(x){return esc(x.impact_window||'--')}],['技术检查',function(x){return x.need_technical_check?'<span class="chip good">需要</span>':'<span class="chip">不需要</span>'}],['理由',function(x){return esc(x.reason||'--')}]],coins)+'</div><div class="section"><h3>建议与观察</h3>'+table([['类型',function(x){return'<span class="chip'+chipClass(x.type)+'">'+esc(directionLabel(x.type))+'</span>'}],['币种',function(x){return esc(x.symbol||'--')}],['原因',function(x){return esc(x.reason||x.why||'--')}],['下一步/触发',function(x){return esc(x.next_step||x.trigger||'--')}],['失效条件',function(x){return esc(x.invalid_if||'--')}]],suggestions.concat(watch.map(function(x){return Object.assign({type:'watch',reason:x.why,next_step:x.trigger},x)})))+'</div><div class="section"><h3>风险事件</h3>'+table([['事件',function(x){return esc(x.title||'--')}],['类型',function(x){return esc(x.risk_type||'--')}],['级别',function(x){return'<span class="chip'+chipClass(x.severity)+'">'+esc(x.severity||'--')+'</span>'}],['影响币种',function(x){return esc((x.symbols||[]).join(', ')||'--')}]],risks)+'</div>';}
function renderSentimentInput(input){var events=input.events||[];return '<div class="section"><h3>这次喂给 AI 的结构化输入 · 新闻事件</h3><div class="kv">'+kv('时间窗口',esc((input.hours||24)+' 小时'))+kv('事件数量',esc((input.event_count||events.length||0)+' / 原始 '+(input.source_event_count||input.event_count||events.length||0)))+kv('任务目标',esc(((input.instructions||{}).role)||'--'))+'</div>'+table([['#',function(x){return esc(x.rank||'--')}],['币种',function(x){return esc(x.symbol||x.base||'--')}],['重要性',function(x){return'<span class="chip'+chipClass(x.importance)+'">'+esc(x.importance||'--')+'</span>'}],['来源',function(x){return esc(x.source||'--')}],['标题',function(x){return esc(x.title||'--')}],['事件类型',function(x){return esc(x.event_type||'--')}],['技术分/趋势',function(x){return esc([x.tech_score!=null?'技术 '+x.tech_score:'',x.trend_rank?'趋势#'+x.trend_rank:''].filter(Boolean).join(' · ')||'--')}]],events)+'</div>';}
function renderGenericOutput(out,d){var evidence=out.key_evidence||out.key_tags||out.theme_tags||[];var risks=out.risk_flags||out.risk_types||[];var watch=out.watch_points||out.watchlist||[];var invalid=out.invalid_if||[];return '<div class="section"><h3>AI 输出</h3><div class="kv">'+kv('摘要',textOrDash(out.summary||out.memo||out.why_now_or_not||out.answer))+kv('专业判断',textOrDash(out.professional_view||out.why_now_or_not||out.why_it_matters))+kv('关键证据',chipList(evidence))+kv('风险提示',chipList(risks))+kv('观察点',Array.isArray(watch)?chipList(watch.map(function(x){return typeof x==='string'?x:(x.symbol||x.why||x.trigger||JSON.stringify(x));})):textOrDash(watch))+kv('失效条件',chipList(Array.isArray(invalid)?invalid:[invalid].filter(Boolean)))+kv('错误信息',textOrDash(d.error))+'</div></div>';}
function renderGenericInput(input){return '<div class="section"><h3>这次喂给 AI 的结构化输入</h3><div class="kv">'+kv('标的/主题',esc(input.symbol||input.related_symbol||input.title||input.run_date||input.target_id||'--'))+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>';}
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 title=d.subject||input.symbol||input.title||input.run_date||input.target_id||'未命名对象';var outputHtml=d.target_type==='sentiment_batch'?renderSentimentOutput(out,d):renderGenericOutput(out,d);var inputHtml=d.target_type==='sentiment_batch'?renderSentimentInput(input):renderGenericInput(input);$('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>'+outputHtml+inputHtml+'<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 %}