Files
nofx/web/nofxi.html
shinchan-zhai fb456a3537 fix: NOFXi Agent real AI integration + quality improvements
Critical fixes:
1. Agent now uses NOFX's MCP client for AI calls (real models, not placeholder)
   - Gets AI client from first configured trader's MCP config
   - Added AutoTrader.GetMCPClient() export
2. /analyze uses REAL market data (market.Get → EMA/MACD/RSI/BB/OI/funding)
   then sends to AI — no more hallucinated prices
3. AI chat enriches prompts with live market data when user mentions a coin
4. Fallback: if AI unavailable, shows raw formatted market data (still useful)

Router improvements:
- Better natural language detection for Chinese
- '该不该买' '走势' '趋势' '行情' → analyze intent
- '多少钱' '赚了' '亏了' → query intent
- Fewer messages falling through to generic chat

Web UI:
- Endpoints updated to /api/agent/* namespace
- nofxi.html served at agent root (localhost:8900)

Result: AI analysis now shows real BTC price, real RSI, real EMA — not fiction.
2026-03-25 01:05:54 +08:00

419 lines
23 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.

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NOFXi — AI Trading Agent</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lightweight-charts@4.1.3/dist/lightweight-charts.standalone.production.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#09090b;--bg2:#0c0c12;--surface:#13131b;--surface2:#1a1a24;
--border:#1f1f2c;--border2:#2a2a38;
--text:#eaeaf0;--text2:#9898ab;--text3:#5c5c72;
--accent:#00e5a0;--accent-dim:rgba(0,229,160,.08);
--purple:#8b5cf6;--purple-dim:rgba(139,92,246,.08);
--blue:#3b82f6;--red:#ef4444;--green:#22c55e;--orange:#f59e0b;
--radius:10px;--radius-sm:6px;
}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);height:100vh;display:flex;flex-direction:column;overflow:hidden}
button{font-family:inherit;cursor:pointer}
select{font-family:inherit}
/* Header */
.hd{display:flex;align-items:center;justify-content:space-between;padding:0 20px;height:48px;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0}
.hd-left{display:flex;align-items:center;gap:14px}
.logo{font-size:17px;font-weight:800;display:flex;align-items:center;gap:6px}
.logo-mark{width:26px;height:26px;background:linear-gradient(135deg,var(--accent),var(--purple));border-radius:6px;display:grid;place-items:center;font-size:13px;color:#000;font-weight:800}
.logo-text{background:linear-gradient(135deg,var(--accent),var(--purple));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.tabs{display:flex;gap:1px;background:var(--surface);border-radius:var(--radius-sm);padding:2px;height:30px}
.tab{padding:0 14px;border:none;background:none;color:var(--text3);font-size:12px;font-weight:500;border-radius:calc(var(--radius-sm) - 1px);transition:all .15s;white-space:nowrap}
.tab:hover{color:var(--text2)}
.tab.on{background:var(--surface2);color:var(--text);box-shadow:0 1px 2px rgba(0,0,0,.3)}
.hd-right{display:flex;align-items:center;gap:8px}
.badge{display:flex;align-items:center;gap:5px;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:500}
.badge.ok{color:var(--green);background:rgba(34,197,94,.08);border:1px solid rgba(34,197,94,.15)}
.badge.err{color:var(--red);background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.15)}
.badge-dot{width:5px;height:5px;border-radius:50%;background:currentColor}
.lang-btn{padding:3px 8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);color:var(--text2);font-size:11px;font-weight:500;transition:all .15s}
.lang-btn:hover{border-color:var(--border2);color:var(--text)}
/* Layout */
.main{flex:1;display:flex;overflow:hidden}
/* Sidebar */
.sb{width:240px;background:var(--bg2);border-right:1px solid var(--border);overflow-y:auto;flex-shrink:0}
.sb::-webkit-scrollbar{width:3px}
.sb::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
.sb-sec{padding:12px}
.sb-sec+.sb-sec{border-top:1px solid var(--border)}
.sb-title{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:1px;color:var(--text3);margin-bottom:8px}
.sb-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px}
.sb-card{display:flex;flex-direction:column;align-items:center;gap:3px;padding:10px 6px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text2);font-size:10px;font-weight:500;transition:all .12s}
.sb-card:hover{background:var(--surface2);border-color:var(--border2);color:var(--text);transform:translateY(-1px)}
.sb-card .e{font-size:18px}
.sb-list{display:flex;flex-direction:column;gap:1px}
.sb-item{display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:var(--radius-sm);color:var(--text2);font-size:12px;border:none;background:none;width:100%;text-align:left;transition:all .12s}
.sb-item:hover{background:var(--surface);color:var(--text)}
.sb-ico{width:24px;height:24px;border-radius:5px;display:grid;place-items:center;font-size:12px;flex-shrink:0}
.sb-ico.g{background:rgba(34,197,94,.08)}.sb-ico.b{background:rgba(59,130,246,.08)}.sb-ico.p{background:rgba(139,92,246,.08)}.sb-ico.o{background:rgba(245,158,11,.08)}
/* Chat */
.chat{flex:1;display:flex;flex-direction:column;background:var(--bg)}
.chat.hide{display:none}
.msgs{flex:1;overflow-y:auto;padding:20px 16px;scroll-behavior:smooth}
.msgs::-webkit-scrollbar{width:3px}
.msgs::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
.m{display:flex;gap:10px;margin-bottom:16px;animation:fi .25s ease}
.m.u{flex-direction:row-reverse}
@keyframes fi{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.m-av{width:30px;height:30px;border-radius:8px;display:grid;place-items:center;font-size:15px;flex-shrink:0}
.m.b .m-av{background:linear-gradient(135deg,var(--accent-dim),var(--purple-dim));border:1px solid var(--border)}
.m.u .m-av{background:var(--purple-dim);border:1px solid rgba(139,92,246,.15)}
.m-body{max-width:640px}
.m-name{font-size:11px;font-weight:600;color:var(--text3);margin-bottom:2px}
.m.u .m-name{text-align:right}
.m-bub{padding:10px 14px;border-radius:14px;font-size:13px;line-height:1.65;white-space:pre-wrap;word-break:break-word}
.m.b .m-bub{background:var(--surface);border:1px solid var(--border);border-top-left-radius:3px}
.m.u .m-bub{background:linear-gradient(135deg,var(--purple),#6d28d9);color:#fff;border-top-right-radius:3px}
.m-t{font-size:10px;color:var(--text3);margin-top:2px}
.m.u .m-t{text-align:right}
.typing{display:none;padding:0 20px 8px;color:var(--text3);font-size:12px;align-items:center;gap:6px}
.typing.on{display:flex}
.dots{display:flex;gap:2px}
.dots span{width:4px;height:4px;background:var(--accent);border-radius:50%;animation:bou 1.4s infinite}
.dots span:nth-child(2){animation-delay:.2s}.dots span:nth-child(3){animation-delay:.4s}
@keyframes bou{0%,60%,100%{transform:translateY(0);opacity:.3}30%{transform:translateY(-5px);opacity:1}}
.ibar{padding:12px 16px 16px;background:var(--bg2);border-top:1px solid var(--border)}
.iwrap{display:flex;align-items:center;gap:8px;background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:3px 3px 3px 14px;transition:all .2s;max-width:800px;margin:0 auto}
.iwrap:focus-within{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-dim)}
.iwrap input{flex:1;background:none;border:none;color:var(--text);font-size:13px;outline:none;padding:8px 0}
.iwrap input::placeholder{color:var(--text3)}
.sbtn{width:36px;height:36px;border-radius:10px;border:none;background:linear-gradient(135deg,var(--accent),#00c896);color:#000;font-size:16px;display:grid;place-items:center;transition:all .12s;flex-shrink:0}
.sbtn:hover{transform:scale(1.05);box-shadow:0 2px 10px rgba(0,229,160,.25)}
.sbtn:disabled{opacity:.35;cursor:not-allowed;transform:none}
.ihint{font-size:10px;color:var(--text3);text-align:center;margin-top:6px}
.ihint kbd{background:var(--surface);padding:0 4px;border-radius:3px;border:1px solid var(--border);font-family:'JetBrains Mono',monospace;font-size:9px}
/* Chart */
.chart-p{flex:1;display:none;flex-direction:column;background:var(--bg)}
.chart-p.on{display:flex}
.ch-hd{display:flex;align-items:center;justify-content:space-between;padding:8px 16px;border-bottom:1px solid var(--border);background:var(--bg2);flex-wrap:wrap;gap:8px}
.ch-left{display:flex;align-items:center;gap:12px}
.ch-sym select{padding:4px 8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);color:var(--text);font-size:12px;font-family:'JetBrains Mono',monospace;outline:none}
.ch-price{font-size:22px;font-weight:700;font-family:'JetBrains Mono',monospace}
.ch-price.up{color:var(--green)}.ch-price.dn{color:var(--red)}
.ch-chg{font-size:12px;font-weight:500;padding:2px 6px;border-radius:4px}
.ch-chg.up{color:var(--green);background:rgba(34,197,94,.08)}.ch-chg.dn{color:var(--red);background:rgba(239,68,68,.08)}
.ch-tfs{display:flex;gap:2px}
.tf{padding:4px 10px;border:1px solid var(--border);border-radius:4px;background:none;color:var(--text3);font-size:11px;font-weight:500;font-family:'JetBrains Mono',monospace;transition:all .12s}
.tf:hover{color:var(--text);border-color:var(--border2)}
.tf.on{background:var(--accent);color:#000;border-color:var(--accent)}
.ch-box{flex:1;position:relative}
/* Info bar */
.info-bar{display:flex;gap:16px;padding:6px 16px;border-bottom:1px solid var(--border);background:var(--bg2);font-size:11px;color:var(--text3);font-family:'JetBrains Mono',monospace;overflow-x:auto;flex-shrink:0}
.info-item{display:flex;gap:4px;white-space:nowrap}
.info-item .val{color:var(--text2)}
.info-item .val.up{color:var(--green)}.info-item .val.dn{color:var(--red)}
@media(max-width:768px){
.sb{display:none}.tabs{display:none}.hd{padding:0 12px}
.msgs{padding:12px 8px}.ibar{padding:8px 8px 12px}
.m-body{max-width:85%}
.ch-hd{padding:6px 10px}.info-bar{padding:4px 10px}
}
</style>
</head>
<body>
<div class="hd">
<div class="hd-left">
<div class="logo"><div class="logo-mark">N</div><span class="logo-text">NOFXi</span></div>
<div class="tabs">
<button class="tab on" onclick="go('chat')" data-i18n="tab_chat">Chat</button>
<button class="tab" onclick="go('chart')" data-i18n="tab_chart">Chart</button>
<button class="tab" onclick="send('/positions');go('chat')" data-i18n="tab_positions">Positions</button>
</div>
</div>
<div class="hd-right">
<div class="badge ok" id="stBadge"><div class="badge-dot"></div><span id="stText">--</span></div>
<button class="lang-btn" id="langBtn" onclick="toggleLang()">EN</button>
</div>
</div>
<!-- Info ticker bar -->
<div class="info-bar" id="infoBar"></div>
<div class="main">
<div class="sb">
<div class="sb-sec">
<div class="sb-title" data-i18n="sb_analysis">Analysis</div>
<div class="sb-grid">
<button class="sb-card" onclick="send('/analyze BTC')"><span class="e"></span><span data-i18n="sb_btc_analysis">BTC</span></button>
<button class="sb-card" onclick="send('/analyze ETH')"><span class="e">Ξ</span><span data-i18n="sb_eth_analysis">ETH</span></button>
<button class="sb-card" onclick="send('/analyze SOL')"><span class="e"></span><span>SOL</span></button>
<button class="sb-card" onclick="send('/price BTCUSDT')"><span class="e">💲</span><span data-i18n="sb_price">Price</span></button>
</div>
</div>
<div class="sb-sec">
<div class="sb-title" data-i18n="sb_portfolio">Portfolio</div>
<div class="sb-list">
<button class="sb-item" onclick="send('/positions')"><div class="sb-ico g">💼</div><span data-i18n="sb_positions">Positions</span></button>
<button class="sb-item" onclick="send('/balance')"><div class="sb-ico b">💰</div><span data-i18n="sb_balance">Balance</span></button>
<button class="sb-item" onclick="send('/pnl')"><div class="sb-ico p">📊</div><span data-i18n="sb_pnl">P/L</span></button>
</div>
</div>
<div class="sb-sec">
<div class="sb-title" data-i18n="sb_monitor">Monitor</div>
<div class="sb-list">
<button class="sb-item" onclick="send('/watch BTCUSDT')"><div class="sb-ico o">👁</div><span data-i18n="sb_watch_btc">Watch BTC</span></button>
<button class="sb-item" onclick="send('/watch')"><div class="sb-ico b">📋</div><span data-i18n="sb_watchlist">Watchlist</span></button>
</div>
</div>
<div class="sb-sec">
<div class="sb-title" data-i18n="sb_strategy">Strategy</div>
<div class="sb-list">
<button class="sb-item" onclick="send('/strategy list')"><div class="sb-ico p">🤖</div><span data-i18n="sb_active_strategies">Strategies</span></button>
<button class="sb-item" onclick="send('/strategy start BTC 1h')"><div class="sb-ico g">🚀</div><span data-i18n="sb_start_btc">Start BTC AI</span></button>
</div>
</div>
</div>
<!-- Chat Panel -->
<div class="chat" id="chatP">
<div class="msgs" id="msgs"></div>
<div class="typing" id="typ"><div class="dots"><span></span><span></span><span></span></div><span data-i18n="thinking">Thinking...</span></div>
<div class="ibar">
<div class="iwrap">
<input id="inp" data-i18n-placeholder="input_placeholder" placeholder="Ask NOFXi anything..." autocomplete="off">
<button class="sbtn" id="sBtn" onclick="send()"></button>
</div>
<div class="ihint"><kbd>Enter</kbd> <span data-i18n="input_hint">to send</span> · /help, /analyze BTC, /buy ETH 0.1</div>
</div>
</div>
<!-- Chart Panel -->
<div class="chart-p" id="chartP">
<div class="ch-hd">
<div class="ch-left">
<div class="ch-sym">
<select id="symSel" onchange="loadChart()">
<option value="BTCUSDT">BTC/USDT</option>
<option value="ETHUSDT">ETH/USDT</option>
<option value="SOLUSDT">SOL/USDT</option>
<option value="BNBUSDT">BNB/USDT</option>
<option value="XRPUSDT">XRP/USDT</option>
<option value="DOGEUSDT">DOGE/USDT</option>
</select>
</div>
<span class="ch-price" id="cPr">--</span>
<span class="ch-chg" id="cChg">--</span>
</div>
<div class="ch-tfs">
<button class="tf" onclick="setTF('5m')">5m</button>
<button class="tf" onclick="setTF('15m')">15m</button>
<button class="tf on" onclick="setTF('1h')">1H</button>
<button class="tf" onclick="setTF('4h')">4H</button>
<button class="tf" onclick="setTF('1d')">1D</button>
<button class="tf" onclick="setTF('1w')">1W</button>
</div>
</div>
<div class="ch-box" id="chBox"></div>
</div>
</div>
<script>
// === i18n ===
const I18N = {
en: {
tab_chat:'Chat', tab_chart:'Chart', tab_positions:'Positions',
sb_analysis:'Analysis', sb_btc_analysis:'BTC', sb_eth_analysis:'ETH', sb_price:'Price',
sb_portfolio:'Portfolio', sb_positions:'Positions', sb_balance:'Balance', sb_pnl:'P/L',
sb_monitor:'Monitor', sb_watch_btc:'Watch BTC', sb_watchlist:'Watchlist',
sb_strategy:'Strategy', sb_active_strategies:'Strategies', sb_start_btc:'Start BTC AI',
thinking:'Thinking...', input_placeholder:'Ask NOFXi anything...', input_hint:'to send',
welcome:"Hey! 👋 I'm NOFXi, your AI trading agent.\n\nI can analyze markets, track prices, manage trades, and run automated strategies.\n\nTry: /help or click the sidebar.",
online:'Online', offline:'Offline', connecting:'...',
info_24h_vol:'24h Vol', info_24h_high:'High', info_24h_low:'Low', info_trades:'Trades', info_funding:'Funding',
},
zh: {
tab_chat:'对话', tab_chart:'行情', tab_positions:'持仓',
sb_analysis:'快速分析', sb_btc_analysis:'BTC', sb_eth_analysis:'ETH', sb_price:'价格',
sb_portfolio:'投资组合', sb_positions:'持仓', sb_balance:'余额', sb_pnl:'盈亏',
sb_monitor:'行情监控', sb_watch_btc:'关注 BTC', sb_watchlist:'关注列表',
sb_strategy:'策略', sb_active_strategies:'运行中策略', sb_start_btc:'启动 BTC AI',
thinking:'思考中...', input_placeholder:'跟 NOFXi 聊点什么...', input_hint:'发送',
welcome:"你好!👋 我是 NOFXi你的 AI 交易 Agent。\n\n我可以分析市场、监控行情、管理交易、运行自动策略。\n\n试试: /help 或点击左侧按钮。",
online:'在线', offline:'离线', connecting:'...',
info_24h_vol:'24h 成交量', info_24h_high:'最高', info_24h_low:'最低', info_trades:'成交笔数', info_funding:'资金费率',
}
};
let lang = localStorage.getItem('nofxi_lang') || 'zh';
function t(key){ return I18N[lang][key] || I18N.en[key] || key; }
function applyLang(){
document.querySelectorAll('[data-i18n]').forEach(el => {
const k = el.getAttribute('data-i18n');
if(I18N[lang][k]) el.textContent = I18N[lang][k];
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const k = el.getAttribute('data-i18n-placeholder');
if(I18N[lang][k]) el.placeholder = I18N[lang][k];
});
document.getElementById('langBtn').textContent = lang === 'zh' ? 'EN' : '中文';
localStorage.setItem('nofxi_lang', lang);
}
function toggleLang(){
lang = lang === 'zh' ? 'en' : 'zh';
applyLang();
}
// === Status ===
const API = location.origin;
async function checkSt(){
try{
const r=await fetch(API+'/health');
await r.json();
document.getElementById('stBadge').className='badge ok';
document.getElementById('stText').textContent=t('online');
}catch{
document.getElementById('stBadge').className='badge err';
document.getElementById('stText').textContent=t('offline');
}
}
checkSt(); setInterval(checkSt,30000);
// === Ticker Info Bar ===
async function loadInfoBar(){
try{
const r=await fetch(API+'/api/agent/ticker?symbol=BTCUSDT');
const d=await r.json();
if(!d.lastPrice) return;
const bar=document.getElementById('infoBar');
const vol=parseFloat(d.quoteVolume||0);
const hi=parseFloat(d.highPrice||0);
const lo=parseFloat(d.lowPrice||0);
const cnt=parseInt(d.count||0);
const chg=parseFloat(d.priceChangePercent||0);
bar.innerHTML=`
<div class="info-item">BTC/USDT <span class="val ${chg>=0?'up':'dn'}">$${parseFloat(d.lastPrice).toLocaleString('en',{minimumFractionDigits:2})}</span></div>
<div class="info-item">${t('info_24h_vol')} <span class="val">$${(vol/1e9).toFixed(2)}B</span></div>
<div class="info-item">${t('info_24h_high')} <span class="val">$${hi.toLocaleString('en',{minimumFractionDigits:2})}</span></div>
<div class="info-item">${t('info_24h_low')} <span class="val">$${lo.toLocaleString('en',{minimumFractionDigits:2})}</span></div>
<div class="info-item">${t('info_trades')} <span class="val">${(cnt/1e6).toFixed(1)}M</span></div>
`;
}catch{}
}
loadInfoBar(); setInterval(loadInfoBar,15000);
// === Chat ===
const msgsEl=document.getElementById('msgs');
const inpEl=document.getElementById('inp');
const typEl=document.getElementById('typ');
const sBtnEl=document.getElementById('sBtn');
function now(){return new Date().toLocaleTimeString(lang==='zh'?'zh-CN':'en-US',{hour:'2-digit',minute:'2-digit'})}
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
function addM(role,text){
const isU=role==='user';
const d=document.createElement('div');
d.className='m '+(isU?'u':'b');
d.innerHTML=`<div class="m-av">${isU?'👤':'⚡'}</div><div class="m-body"><div class="m-name">${isU?'You':'NOFXi'}</div><div class="m-bub">${esc(text)}</div><div class="m-t">${now()}</div></div>`;
msgsEl.appendChild(d);
msgsEl.scrollTop=msgsEl.scrollHeight;
}
// Welcome message
addM('bot', t('welcome'));
async function send(text){
if(!text){text=inpEl.value.trim();if(!text)return;inpEl.value=''}
addM('user',text);
typEl.classList.add('on');sBtnEl.disabled=true;inpEl.focus();
try{
const r=await fetch(API+'/api/agent/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:text,user_id:1,lang:lang})});
const d=await r.json();
addM('bot',d.response||d.error||'No response');
}catch(e){addM('bot','⚠️ Error: '+e.message)}
typEl.classList.remove('on');sBtnEl.disabled=false;inpEl.focus();
}
// === Tabs ===
function go(tab){
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('on',(tab==='chat'&&i===0)||(tab==='chart'&&i===1)));
const cp=document.getElementById('chatP'), chp=document.getElementById('chartP');
if(tab==='chart'){cp.classList.add('hide');chp.classList.add('on');if(!chart)initChart();loadChart()}
else{cp.classList.remove('hide');chp.classList.remove('on');if(chTimer)clearInterval(chTimer)}
}
// === Chart ===
let chart=null,cSeries=null,vSeries=null,curTF='1h',chTimer=null;
function initChart(){
const box=document.getElementById('chBox');
chart=LightweightCharts.createChart(box,{
width:box.clientWidth,height:box.clientHeight,
layout:{background:{color:'#09090b'},textColor:'#5c5c72',fontFamily:"'JetBrains Mono',monospace",fontSize:10},
grid:{vertLines:{color:'#15151f'},horzLines:{color:'#15151f'}},
crosshair:{mode:LightweightCharts.CrosshairMode.Normal,vertLine:{color:'#2a2a3a',labelBackgroundColor:'#1a1a24'},horzLine:{color:'#2a2a3a',labelBackgroundColor:'#1a1a24'}},
rightPriceScale:{borderColor:'#1f1f2c',scaleMargins:{top:.08,bottom:.22}},
timeScale:{borderColor:'#1f1f2c',timeVisible:true,secondsVisible:false},
});
cSeries=chart.addCandlestickSeries({upColor:'#22c55e',downColor:'#ef4444',borderUpColor:'#22c55e',borderDownColor:'#ef4444',wickUpColor:'#22c55e',wickDownColor:'#ef4444'});
vSeries=chart.addHistogramSeries({priceFormat:{type:'volume'},priceScaleId:'vol'});
chart.priceScale('vol').applyOptions({scaleMargins:{top:.82,bottom:0}});
new ResizeObserver(()=>chart.applyOptions({width:box.clientWidth,height:box.clientHeight})).observe(box);
}
function setTF(tf){
curTF=tf;
document.querySelectorAll('.tf').forEach(b=>b.classList.toggle('on',b.textContent.toLowerCase()===tf||b.textContent===tf.toUpperCase()));
loadChart();
}
async function loadChart(){
const sym=document.getElementById('symSel').value;
try{
const[kr,tr]=await Promise.all([fetch(`${API}/api/agent/klines?symbol=${sym}&interval=${curTF}&limit=300`),fetch(`${API}/api/agent/ticker?symbol=${sym}`)]);
const klines=await kr.json(), ticker=await tr.json();
if(klines?.length){
cSeries.setData(klines.map(k=>({time:k.time,open:k.open,high:k.high,low:k.low,close:k.close})));
vSeries.setData(klines.map(k=>({time:k.time,value:k.volume,color:k.close>=k.open?'rgba(34,197,94,.25)':'rgba(239,68,68,.25)'})));
}
if(ticker?.lastPrice){
const p=parseFloat(ticker.lastPrice),c=parseFloat(ticker.priceChangePercent);
const pe=document.getElementById('cPr'),ce=document.getElementById('cChg');
pe.textContent='$'+p.toLocaleString('en',{minimumFractionDigits:2,maximumFractionDigits:p>100?2:4});
pe.className='ch-price '+(c>=0?'up':'dn');
ce.textContent=(c>=0?'+':'')+c.toFixed(2)+'%';
ce.className='ch-chg '+(c>=0?'up':'dn');
}
}catch(e){console.error(e)}
if(chTimer)clearInterval(chTimer);
chTimer=setInterval(loadChart,30000);
}
// IME composition guard
let composing = false;
inpEl.addEventListener('compositionstart', () => { composing = true; });
inpEl.addEventListener('compositionend', () => { composing = false; });
inpEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey && !composing) {
e.preventDefault();
send();
}
});
// Init
applyLang();
inpEl.focus();
</script>
</body>
</html>