alphax/static/chat.html
2026-05-18 10:47:07 +08:00

157 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}AlphaX Agent — 智能问答{% endblock %}
{% block extra_head_css %}
<style>
.chat-shell{height:100vh;max-height:100vh;display:grid;grid-template-columns:300px minmax(0,1fr);background:#f4f5f7;overflow:hidden}
.session-pane{border-right:1px solid var(--hairline-soft);background:var(--canvas);display:flex;flex-direction:column;min-width:0}
.session-head{padding:18px 16px;border-bottom:1px solid var(--hairline-soft);display:flex;align-items:center;justify-content:space-between;gap:10px}
.session-title{font-size:17px;font-weight:950;letter-spacing:-.3px}
.icon-btn{width:34px;height:34px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--canvas);display:grid;place-items:center;cursor:pointer;color:var(--ink);font-weight:950}
.session-list{overflow:auto;min-height:0;flex:1;padding:8px}
.session-row{padding:12px;border-radius:var(--radius-md);cursor:pointer;border:1px solid transparent;background:transparent;transition:.12s}
.session-row:hover{background:var(--surface)}
.session-row.active{background:#1c1c1e;color:#fff}
.session-row b{display:block;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.session-row span{display:block;margin-top:4px;color:var(--stone);font-size:11px;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.session-row.active span{color:rgba(255,255,255,.66)}
.chat-main{display:grid;grid-template-rows:auto minmax(0,1fr) auto;min-width:0;max-height:100vh}
.hero{border-bottom:1px solid var(--hairline-soft);background:linear-gradient(180deg,#fff,#fafafa);padding:18px 22px;display:flex;justify-content:space-between;gap:16px;align-items:flex-start}
.hero h1{font-size:23px;font-weight:950;letter-spacing:-.7px;margin:0}
.hero p{margin-top:4px;color:var(--slate);font-size:13px;line-height:1.6}
.status-pill{display:inline-flex;align-items:center;height:30px;padding:0 10px;border:1px solid var(--hairline-soft);border-radius:999px;background:var(--surface);color:var(--stone);font-size:11px;font-weight:900;white-space:nowrap}
.messages{overflow:auto;min-height:0;padding:18px 22px 24px;display:flex;flex-direction:column;gap:14px}
.msg{display:grid;grid-template-columns:36px minmax(0,1fr);gap:10px;max-width:1120px}
.avatar{width:36px;height:36px;border-radius:10px;display:grid;place-items:center;font-weight:950;font-size:12px;background:var(--yellow);color:#1c1c1e}
.avatar.ai{background:#1c1c1e;color:#fff}
.bubble{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--canvas);padding:14px;box-shadow:0 8px 24px rgba(5,0,56,.035);min-width:0}
.msg.user .bubble{background:#1c1c1e;color:#fff;border-color:#1c1c1e}
.bubble-text{font-size:14px;line-height:1.75;white-space:pre-wrap}
.answer-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px}
.answer-head b{font-size:16px;color:var(--ink);line-height:1.35}
.tag{display:inline-flex;align-items:center;border-radius:999px;padding:4px 8px;background:rgba(66,98,255,.08);color:var(--blue);font-size:11px;font-weight:950;white-space:nowrap}
.answer-text{font-size:14px;color:var(--slate);line-height:1.75;margin-bottom:12px}
.answer-body{display:grid;gap:10px;margin-bottom:12px}
.answer-block{font-size:14px;color:var(--slate);line-height:1.75;white-space:pre-wrap}
.answer-block.is-title{font-size:13px;font-weight:950;color:var(--ink);background:var(--surface);border:1px solid var(--hairline-soft);border-radius:10px;padding:8px 10px;line-height:1.5}
.progress-box{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--surface);padding:11px 12px;margin-bottom:12px}
.progress-row{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:12px;color:var(--slate);padding:5px 0}
.progress-row strong{color:var(--ink);font-size:12px}
.progress-dots{display:inline-flex;gap:4px;align-items:center}
.progress-dots i{width:6px;height:6px;border-radius:50%;background:var(--stone);animation:pulse .9s infinite ease-in-out}
.progress-dots i:nth-child(2){animation-delay:.15s}
.progress-dots i:nth-child(3){animation-delay:.3s}
.evidence-grid{display:grid;grid-template-columns:1.1fr .9fr;gap:10px;margin-top:10px}
.panel{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--surface);padding:11px;min-width:0}
.panel h3{font-size:12px;font-weight:950;color:var(--ink);margin-bottom:8px}
.ev-list{display:grid;gap:7px}
.ev{font-size:12px;color:var(--slate);line-height:1.55;padding-left:11px;position:relative}
.ev:before{content:"";position:absolute;left:0;top:.65em;width:4px;height:4px;border-radius:50%;background:var(--blue)}
.record-list{display:flex;gap:6px;flex-wrap:wrap}
.record{border:1px solid var(--hairline);background:#fff;border-radius:999px;padding:5px 8px;font-size:11px;font-weight:850;color:var(--slate)}
.mini-section{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--surface);padding:11px;margin-top:10px}
.mini-section h3{font-size:12px;font-weight:950;color:var(--ink);margin-bottom:8px}
.brief-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:10px}
.brief{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:#fff;padding:10px;min-width:0}
.brief span{display:block;font-size:10px;color:var(--stone);font-weight:950}
.brief b{display:block;margin-top:4px;font-size:14px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.notice-box{border:1px solid rgba(255,183,77,.42);background:rgba(255,183,77,.11);border-radius:var(--radius-md);padding:12px;color:#6d4700;font-size:13px;line-height:1.65;margin-top:10px}
.tf-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-top:10px}
.tf{border:1px solid var(--hairline-soft);background:#fff;border-radius:var(--radius-md);padding:9px;min-width:0}
.tf span{display:block;color:var(--stone);font-size:10px;font-weight:950}
.tf b{display:block;margin-top:3px;font-size:13px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tf small{display:block;margin-top:3px;color:var(--stone);font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.composer{border-top:1px solid var(--hairline-soft);background:var(--canvas);padding:12px 18px 14px}
.composer-inner{max-width:1120px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:end}
.input{min-height:48px;max-height:120px;resize:none;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);padding:13px 14px;font-size:14px;line-height:1.55;outline:none;background:#fff;color:var(--ink);font-family:inherit}
.input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(66,98,255,.08)}
.send{height:48px;border:0;border-radius:var(--radius-md);background:#1c1c1e;color:#fff;padding:0 18px;font-weight:950;cursor:pointer}
.send:disabled{opacity:.45;cursor:not-allowed}
.empty{padding:40px 20px;text-align:center;color:var(--stone);font-size:13px}
.empty h2{font-size:24px;color:var(--ink);letter-spacing:-.7px;margin-bottom:8px}
.loading-dots{display:inline-flex;gap:4px}
.loading-dots i{width:6px;height:6px;border-radius:50%;background:var(--stone);animation:pulse .9s infinite ease-in-out}
.loading-dots i:nth-child(2){animation-delay:.15s}
.loading-dots i:nth-child(3){animation-delay:.3s}
@keyframes pulse{0%,80%,100%{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-3px)}}
@media(max-width:980px){.chat-shell{grid-template-columns:1fr}.session-pane{display:none}.hero{padding:14px}.messages{padding:14px}.evidence-grid{grid-template-columns:1fr}.brief-grid{grid-template-columns:1fr}.tf-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.composer{padding:10px}.chat-main{height:calc(100vh - 48px)}}
@media(max-width:520px){.composer-inner{grid-template-columns:1fr}.send{width:100%}.hero{display:block}.status-pill{margin-top:8px}.msg{grid-template-columns:30px minmax(0,1fr)}.avatar{width:30px;height:30px;border-radius:8px}.bubble{padding:12px}}
</style>
{% endblock %}
{% block content %}
<div class="chat-shell">
<aside class="session-pane">
<div class="session-head">
<div class="session-title">智能问答</div>
<button class="icon-btn" onclick="newSession()" title="新对话">+</button>
</div>
<div class="session-list" id="sessionList"><div class="empty">加载中...</div></div>
</aside>
<main class="chat-main">
<header class="hero">
<div>
<h1>Crypto 研究助手</h1>
<p>结合 AlphaX 现有数据与 Binance 多周期行情,提供 Crypto 研究参考。</p>
</div>
<span class="status-pill" id="chatStatus">研究参考</span>
</header>
<section class="messages" id="messages"></section>
<footer class="composer">
<div class="composer-inner">
<textarea class="input" id="messageInput" placeholder="问一个加密市场问题,比如:分析 SUI/USDT 现在的技术面,或者解释 DOGE 为什么等回踩。" onkeydown="handleKey(event)"></textarea>
<button class="send" id="sendBtn" onclick="sendMessage()">发送</button>
</div>
</footer>
</main>
</div>
{% endblock %}
{% block extra_script %}
<script>
var state={sessions:[],sessionId:0,messages:[],loading:false};
function esc(v){return String(v==null?'':v).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];});}
function fixMojibake(v){if(typeof v!=='string'||!/[ÃÂçèéåæäï¼ã]/.test(v))return v;try{var bytes=Uint8Array.from(Array.prototype.map.call(v,function(ch){return ch.charCodeAt(0)&255;}));var fixed=new TextDecoder('utf-8',{fatal:false}).decode(bytes);if(/[\u4e00-\u9fff《》]/.test(fixed))return fixed;}catch(e){}return v;}
function deepFix(v){if(typeof v==='string')return fixMojibake(v);if(Array.isArray(v))return v.map(deepFix);if(v&&typeof v==='object'){Object.keys(v).forEach(function(k){v[k]=deepFix(v[k]);});}return v;}
function short(v,n){v=String(v||'');return v.length>n?v.slice(0,n)+'...':v;}
function fmtNum(v){v=Number(v||0);if(!v)return'--';if(v>=100)return v.toFixed(2);if(v>=1)return v.toFixed(3);if(v>=0.01)return v.toFixed(4);return v.toFixed(8);}
function normMessages(items){return (items||[]).map(function(m){m=deepFix(m||{});return {id:m.id,role:m.role,text:m.content_text||'',content:m.content||{},context:m.context||{},created_at:m.created_at,intent:m.intent,symbol:m.symbol};});}
function renderSessions(){if(!state.sessions.length){sessionList.innerHTML='<div class="empty">暂无对话</div>';return;}sessionList.innerHTML=state.sessions.map(function(s){var active=s.id===state.sessionId?' active':'';return '<div class="session-row'+active+'" onclick="loadSession('+s.id+')"><b>'+esc(s.title||'新对话')+'</b><span>'+esc(short(s.last_message_text||s.summary||'还没有消息',88))+'</span></div>';}).join('');}
function renderEmpty(){messages.innerHTML='<div class="empty"><h2>问 AlphaX 一个 Crypto 问题</h2><p>直接输入你的问题即可,支持单币技术面、推荐解释、链上异动、舆情影响和复盘结果。</p></div>';}
function renderMessages(){if(!state.messages.length){renderEmpty();return;}messages.innerHTML=state.messages.map(renderMessage).join('');messages.scrollTop=messages.scrollHeight;}
function renderMessage(m){if(m.role==='user'){return '<div class="msg user"><div class="avatar">你</div><div class="bubble"><div class="bubble-text">'+esc(m.text)+'</div></div></div>';}return '<div class="msg"><div class="avatar ai">AI</div><div class="bubble">'+renderAnswer(m)+'</div></div>';}
function renderProgress(lines){if(!lines||!lines.length)return'';return '<div class="progress-box">'+lines.map(function(line,idx){return '<div class="progress-row"><span>'+esc(line)+'</span>'+(idx===0?'<span class="progress-dots"><i></i><i></i><i></i></span>':'')+'</div>';}).join('')+'</div>';}
function renderEvidenceList(items){if(!items||!items.length)return '<div class="ev">暂无明确证据,已降级为空态回答。</div>';return items.slice(0,8).map(function(x){if(typeof x==='string')return '<div class="ev">'+esc(x)+'</div>';if(Array.isArray(x))return '<div class="ev">'+esc(x.map(function(v){return typeof v==='string'?v:JSON.stringify(v);}).join(' · '))+'</div>';if(x&&typeof x==='object'){var label=x.label||x.name||x.title||x.reason||x.summary||x.key||'';var value=x.value||x.text||x.detail||x.note||x.signal||x.message||'';var extra=x.timeframe||x.symbol||x.period||x.source||'';var text=(label?label:'证据')+(value?(''+value):'')+(extra?(' · '+extra):'');return '<div class="ev">'+esc(text||JSON.stringify(x))+'</div>';}return '<div class="ev">'+esc(String(x))+'</div>';}).join('');}
function renderRecords(items){if(!items||!items.length)return '<span class="record">无直接记录</span>';return items.slice(0,8).map(function(r){if(typeof r==='string')return '<span class="record">'+esc(r)+'</span>';if(r&&typeof r==='object'){var parts=[];if(r.type)parts.push(r.type);if(r.label||r.title||r.name)parts.push(r.label||r.title||r.name);if(r.symbol)parts.push(r.symbol);if(r.status)parts.push(r.status);if(r.timeframe)parts.push(r.timeframe);if(r.created_at||r.detected_at)parts.push((r.created_at||r.detected_at).slice(0,16).replace('T',' '));var text=parts.filter(Boolean).join(' · ');return '<span class="record">'+esc(text||'记录')+'</span>';}return '<span class="record">'+esc(String(r))+'</span>';}).join('');}
function renderTfGrid(ctx){var tech=(ctx&&ctx.technicals)||{},tfs=tech.timeframes||{},order=['15m','1h','4h','1d'];var has=order.some(function(tf){return tfs[tf]&&tfs[tf].available;});if(!has)return'';return '<div class="tf-grid">'+order.map(function(tf){var x=tfs[tf]||{};if(!x.available)return '<div class="tf"><span>'+tf+'</span><b>无数据</b><small>'+esc(x.reason||'Binance 未返回')+'</small></div>';var trend={uptrend:'上行',rebound:'反弹',weak:'偏弱',downtrend:'下行',sideways:'震荡'}[x.trend]||x.trend||'--';var sub='RSI '+(x.rsi14||'--')+' · 量 '+(x.volume_ratio_20||0)+'x';return '<div class="tf"><span>'+tf+'</span><b>'+esc(trend)+' · $'+fmtNum(x.price)+'</b><small>'+esc(sub)+'</small></div>';}).join('')+'</div>';}
function styleFor(m,c,ctx){return c.answer_style||({coin_analysis:'technical',recommendation_explain:'decision',market_overview:'market',sentiment:'news',onchain:'onchain',review:'review',restricted:'notice',unsupported:'notice',help:'help'}[m.intent||ctx.intent]||'default');}
function splitPlainText(text){var raw=String(text||'').replace(/\r\n/g,'\n').trim();if(!raw)return[];var out=[];var current=[];var lines=raw.split('\n');function flush(){var block=current.join(' ').replace(/\s+/g,' ').trim();if(block)out.push(block);current=[];}
lines.forEach(function(line){var t=line.trim();if(!t){flush();return;}if(/^【[^】]+】/.test(t)||/^(结论|证据|风险|建议|判断|复盘|总结|要点|关注点|结论如下)[:]/.test(t)){flush();out.push(t);return;}var numbered=/^(\d+)[\.、]\s*(.+)$/.exec(t);if(numbered){flush();out.push(numbered[0]);return;}if(/^[-•]\s+/.test(t)){flush();out.push(t);return;}if(current.length&&current[current.length-1].length+t.length>220){flush();current.push(t);flush();return;}current.push(t);});
flush();if(!out.length)out.push(raw);return out;}
function renderPlainTextAnswer(text){var blocks=splitPlainText(text);return '<div class="answer-body">'+blocks.map(function(block){if(/^【[^】]+】/.test(block)||/^(结论|证据|风险|建议|判断|复盘|总结|要点|关注点|结论如下)[:]/.test(block)){return '<div class="answer-block is-title">'+esc(block)+'</div>';}return '<div class="answer-block">'+esc(block)+'</div>';}).join('')+'</div>';}
function renderEvidenceSection(title,items,empty){return '<div class="mini-section"><h3>'+esc(title)+'</h3><div class="ev-list">'+(items&&items.length?renderEvidenceList(items):'<div class="ev">'+esc(empty||'暂无明确数据。')+'</div>')+'</div></div>';}
function renderRecordSection(title,items){if(!items||!items.length)return'';return '<div class="mini-section"><h3>'+esc(title)+'</h3><div class="record-list">'+renderRecords(items)+'</div></div>';}
function renderMarketBrief(ctx){var src=(ctx&&ctx.technical_summary)||{},items=[];if(src.stance)items.push(['状态',src.stance]);if(src.risk_level)items.push(['风险',src.risk_level]);if(src.latest_price)items.push(['价格','$'+fmtNum(src.latest_price)]);if(!items.length)return'';return '<div class="brief-grid">'+items.map(function(x){return '<div class="brief"><span>'+esc(x[0])+'</span><b>'+esc(x[1])+'</b></div>';}).join('')+'</div>';}
function renderAnswer(m){var c=m.content||{},ctx=m.context||{},answer=String(c.answer||m.text||c.summary||'--');var evidence=Array.isArray(c.evidence)?c.evidence:[];var risks=Array.isArray(c.risk_flags)?c.risk_flags:[];var records=Array.isArray(c.related_records)?c.related_records:[];var style=styleFor(m,c,ctx);var tag=m.intent||ctx.intent||style||'回答';var headOnly='<div class="answer-head"><b>'+esc(c.summary||'研究结论')+'</b><span class="tag">'+esc(tag)+'</span></div>';var head=headOnly+renderPlainTextAnswer(answer);if(style==='notice'||style==='help'||m.intent==='error')return headOnly+renderPlainTextAnswer(answer)+'<div class="notice-box">'+esc(answer)+'</div>';if(style==='technical')return head+renderTfGrid(ctx)+renderEvidenceSection('关键判断',evidence,'暂无明确技术证据。')+(risks.length?renderEvidenceSection('风险提示',risks,''):'')+renderRecordSection('相关记录',records);if(style==='market')return head+renderMarketBrief(ctx)+renderEvidenceSection('市场要点',evidence,'暂无市场要点。');if(style==='decision')return head+renderEvidenceSection('决策依据',evidence,'暂无推荐依据。')+(risks.length?renderEvidenceSection('不成立条件',risks,''):'')+renderRecordSection('推荐记录',records);if(style==='news')return head+renderEvidenceSection('事件解读',evidence,'暂无舆情事件。')+renderRecordSection('新闻记录',records);if(style==='onchain')return head+renderEvidenceSection('链上信号',evidence,'暂无链上映射信号。')+renderRecordSection('相关事件',records);if(style==='review')return head+renderEvidenceSection('复盘发现',evidence,'暂无复盘样本。')+(risks.length?renderEvidenceSection('需要警惕',risks,''):'')+renderRecordSection('复盘记录',records);return head+renderEvidenceSection('要点',evidence,'暂无明确证据。')+renderRecordSection('相关记录',records);}
async function init(){try{var d=await (await fetch('/api/chat/bootstrap')).json();state.sessions=(d.sessions&&d.sessions.items)||[];renderSessions();if(state.sessions[0])loadSession(state.sessions[0].id);else renderEmpty();}catch(e){messages.innerHTML='<div class="empty">聊天助手加载失败</div>';}}
async function refreshSessions(){var d=await (await fetch('/api/chat/sessions?limit=20')).json();state.sessions=d.items||[];renderSessions();}
async function newSession(){state.sessionId=0;state.messages=[];renderSessions();renderEmpty();messageInput.focus();}
async function loadSession(id){state.sessionId=id;renderSessions();messages.innerHTML='<div class="empty">加载对话...</div>';try{var d=await (await fetch('/api/chat/sessions/'+id)).json();state.messages=normMessages((d.messages&&d.messages.items)||[]);renderMessages();}catch(e){messages.innerHTML='<div class="empty">读取对话失败</div>';}}
function handleKey(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage();}}
function appendLoading(){state.messages.push({role:'assistant',text:'',content:{summary:'正在读取数据',answer:'',evidence:[]},context:{},created_at:new Date().toISOString(),intent:'loading'});renderMessages();}
function removeLoading(){state.messages=state.messages.filter(function(m){return m.intent!=='loading';});}
async function sendMessage(){var text=messageInput.value.trim();if(!text||state.loading)return;state.loading=true;sendBtn.disabled=true;chatStatus.textContent='分析中';state.messages.push({role:'user',text:text,content:{text:text},created_at:new Date().toISOString()});messageInput.value='';appendLoading();try{var d=deepFix(await (await fetch('/api/chat/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({session_id:state.sessionId||0,message:text})})).json());removeLoading();state.sessionId=(d.session&&d.session.id)||state.sessionId;state.messages.push(d.assistant_message);renderMessages();await refreshSessions();}catch(e){removeLoading();state.messages.push({role:'assistant',content:{summary:'回答失败',answer:'这次请求没有完成,请稍后重试。',evidence:[]},context:{},created_at:new Date().toISOString(),intent:'error'});renderMessages();}finally{state.loading=false;sendBtn.disabled=false;chatStatus.textContent='研究参考';}}
var _renderAnswer=renderAnswer;
renderAnswer=function(m){
if(m.intent==='loading'){
return '<div class="answer-head"><b>正在分析中</b><span class="tag">分析中</span></div>'
+ '<div class="progress-box">'
+ '<div class="progress-row"><span>读取 AlphaX 当前数据</span><span class="progress-dots"><i></i><i></i><i></i></span></div>'
+ '<div class="progress-row"><span>汇总多周期行情与结构</span></div>'
+ '<div class="progress-row"><span>结合推荐、舆情、链上与复盘</span></div>'
+ '</div>'
+ '<div class="notice-box">稍等一下,我正在把不同数据源拼起来,随后会给你结论和依据。</div>';
}
return _renderAnswer(m);
}
init();
</script>
{% endblock %}