alphax/static/chat.html
2026-05-18 10:04:50 +08:00

128 lines
16 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}
.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)}
.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}.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 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 progressHtml=m.intent==='loading'?renderProgress(['正在读取 AlphaX 数据','正在拉取当前币种行情','正在汇总推荐 / 舆情 / 链上 / 复盘上下文']):'';var tfHtml=renderTfGrid(ctx);return '<div class="answer-head"><b>'+esc(c.summary||'研究结论')+'</b><span class="tag">'+esc((m.intent||ctx.intent||'回答'))+'</span></div>'+progressHtml+'<div class="answer-text">'+esc(answer)+'</div>'+tfHtml+'<div class="evidence-grid"><div class="panel"><h3>证据</h3><div class="ev-list">'+renderEvidenceList(evidence)+(risks.length?renderEvidenceList(risks):'')+'</div></div><div class="panel"><h3>相关记录</h3><div class="record-list">'+renderRecords(records)+'</div></div></div>';}
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>正在读取 AlphaX 数据</b><span class="tag">分析中</span></div><div class="answer-text"><span class="loading-dots"><i></i><i></i><i></i></span></div>';}return _renderAnswer(m);}
init();
</script>
{% endblock %}