feat(hub): dashboard SSE push, light-theme cards, simplify AI coach
Replace dashboard polling with backend SSE and snapshot refresh. Restyle for light/dark theme with soft card glow instead of neon. Remove Today's Summary from AI page; keep trading and general chat only. Update hub documentation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3142,26 +3142,11 @@
|
||||
showToast("已添加一行,请填写 URL 后点「保存设置」");
|
||||
};
|
||||
|
||||
let aiSummaryLoading = false;
|
||||
let aiChatLoading = false;
|
||||
let aiChatSessionCache = null;
|
||||
let aiChatSessionsCache = [];
|
||||
let aiSelectedBotMode = "trading";
|
||||
|
||||
function aiPnlClass(v) {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n) || Math.abs(n) < 1e-9) return "";
|
||||
return n > 0 ? "pos" : "neg";
|
||||
}
|
||||
|
||||
function aiPnlSigned(v, digits) {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n)) return "—";
|
||||
const abs = fmt(Math.abs(n), digits);
|
||||
if (Math.abs(n) < 1e-9) return `${abs}U`;
|
||||
return `${n > 0 ? "+" : "-"}${abs}U`;
|
||||
}
|
||||
|
||||
function renderHubMarkdown(text) {
|
||||
const raw = String(text || "");
|
||||
if (typeof window !== "undefined" && window.AiReviewRender && window.AiReviewRender.renderMarkdown) {
|
||||
@@ -3172,154 +3157,6 @@
|
||||
.replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
function renderAiMarkdown(text) {
|
||||
return renderHubMarkdown(text);
|
||||
}
|
||||
|
||||
function enhanceHubSummaryMarkdown(md) {
|
||||
let out = String(md || "");
|
||||
out = out.replace(/\*\*今日交易总结(([^)]+))\*\*/g, "# 📋 今日交易总结($1)");
|
||||
out = out.replace(/\*\*1\.\s*(?:📊\s*)?总览\*\*/g, "## 1. 📊 总览");
|
||||
out = out.replace(/\*\*2\.\s*(?:👥\s*)?分户明细\*\*/g, "## 2. 👥 分户明细");
|
||||
out = out.replace(/\*\*3\.\s*(?:⚠️\s*)?需关注\*\*/g, "## 3. ⚠️ 需关注");
|
||||
out = out.replace(/\*\*4\.\s*(?:ℹ️\s*)?数据说明\*\*/g, "## 4. ℹ️ 数据说明");
|
||||
out = out.replace(/\*\*5\.\s*(?:💡\s*)?操作建议\*\*/g, "## 5. 💡 操作建议");
|
||||
return out;
|
||||
}
|
||||
|
||||
function aiFmtFund(v) {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n)) return "—";
|
||||
return `${fmt(n, 2)}U`;
|
||||
}
|
||||
|
||||
function aiPnlCellHtml(v, digits) {
|
||||
const cls = aiPnlClass(v);
|
||||
const valCls = cls ? ` ai-stat-val ${cls}` : " ai-stat-val";
|
||||
return `<span class="${valCls.trim()}">${aiPnlSigned(v, digits)}</span>`;
|
||||
}
|
||||
|
||||
function aiAccountStatusClass(status) {
|
||||
const s = String(status || "");
|
||||
if (s === "未监控") return "ai-ac-unmon";
|
||||
if (s.includes("异常")) return "ai-ac-err";
|
||||
if (s.includes("需关注")) return "ai-ac-warn";
|
||||
return "";
|
||||
}
|
||||
|
||||
function renderAiAccountTable(snapshot) {
|
||||
const accounts = snapshot && snapshot.by_account;
|
||||
if (!accounts || typeof accounts !== "object") return "";
|
||||
const rows = Object.values(accounts);
|
||||
if (!rows.length) return "";
|
||||
const head =
|
||||
"<thead><tr>" +
|
||||
"<th>账户</th><th>状态</th><th>资金账户</th><th>交易账户</th><th>今日盈亏</th><th>笔数</th><th>浮盈亏</th><th>备注</th>" +
|
||||
"</tr></thead>";
|
||||
const body = rows
|
||||
.map((ac) => {
|
||||
const closedPnl = Number(ac.pnl_u);
|
||||
const floatPnl = Number(ac.float_pnl_u);
|
||||
const remark =
|
||||
ac.remark ||
|
||||
(Array.isArray(ac.issues) && ac.issues.length ? ac.issues.join(";") : "无");
|
||||
const statusCls = aiAccountStatusClass(ac.status);
|
||||
const countLabel = `${Number(ac.closed_count) || 0}${Number(ac.closed_count_yesterday) ? ` / 昨${Number(ac.closed_count_yesterday)}` : ""}`;
|
||||
return (
|
||||
"<tr>" +
|
||||
`<td class="ai-ac-name">${esc(ac.name || "—")}</td>` +
|
||||
`<td class="${statusCls}">${esc(ac.status || "—")}</td>` +
|
||||
`<td>${aiFmtFund(ac.funding_usdt)}</td>` +
|
||||
`<td>${aiFmtFund(ac.trading_usdt)}</td>` +
|
||||
`<td>${aiPnlCellHtml(closedPnl, 2)}</td>` +
|
||||
`<td>${countLabel}</td>` +
|
||||
`<td>${aiPnlCellHtml(floatPnl, 2)}</td>` +
|
||||
`<td class="ai-ac-remark">${esc(remark)}</td>` +
|
||||
"</tr>"
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
return `<div class="ai-ac-table-wrap"><table class="ai-ac-table">${head}<tbody>${body}</tbody></table></div>`;
|
||||
}
|
||||
|
||||
function renderAiClosedTradesBlock(snapshot) {
|
||||
const rows = (snapshot && snapshot.closed_trades) || [];
|
||||
if (!rows.length) return "";
|
||||
const head =
|
||||
"<thead><tr><th>交易日</th><th>账户</th><th>合约</th><th>方向</th><th>结果</th><th>盈亏</th><th>时间</th></tr></thead>";
|
||||
const body = rows
|
||||
.map((t) => {
|
||||
const pnl = Number(t.pnl_amount);
|
||||
return (
|
||||
"<tr>" +
|
||||
`<td>${esc(t.trading_day || "—")}</td>` +
|
||||
`<td>${esc(t.account_name || "—")}</td>` +
|
||||
`<td>${esc(t.symbol || "—")}</td>` +
|
||||
`<td>${esc(t.direction || "—")}</td>` +
|
||||
`<td>${esc(t.result || "—")}</td>` +
|
||||
`<td>${aiPnlCellHtml(pnl, 2)}</td>` +
|
||||
`<td class="ai-ac-remark">${esc(t.closed_at || "—")}</td>` +
|
||||
"</tr>"
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
return (
|
||||
`<div class="ai-closed-trades-wrap">` +
|
||||
`<h4 class="ai-closed-trades-title">平仓明细(今日)</h4>` +
|
||||
`<div class="ai-ac-table-wrap"><table class="ai-ac-table ai-closed-trades-table">${head}<tbody>${body}</tbody></table></div>` +
|
||||
`</div>`
|
||||
);
|
||||
}
|
||||
|
||||
function renderAiSummaryBody(contentMd, snapshot) {
|
||||
const md = enhanceHubSummaryMarkdown(contentMd);
|
||||
const sec2 = /##\s*2\.\s*👥\s*分户明细/;
|
||||
const sec3 = /##\s*3\.\s*⚠️\s*需关注/;
|
||||
const i2 = md.search(sec2);
|
||||
const i3 = md.search(sec3);
|
||||
const tableHtml = renderAiAccountTable(snapshot);
|
||||
const closedHtml = renderAiClosedTradesBlock(snapshot);
|
||||
if (i2 >= 0 && i3 > i2 && tableHtml) {
|
||||
const headEnd = i2 + md.slice(i2).match(sec2)[0].length;
|
||||
const part1 = md.slice(0, headEnd);
|
||||
const part2 = md.slice(i3);
|
||||
return renderHubMarkdown(part1) + tableHtml + closedHtml + renderHubMarkdown(part2);
|
||||
}
|
||||
return renderHubMarkdown(md) + (tableHtml ? tableHtml + closedHtml : "");
|
||||
}
|
||||
|
||||
function setAiSummaryMarkdown(body, contentMd, snapshot) {
|
||||
if (!body) return;
|
||||
body.classList.add("ai-result-md");
|
||||
body.innerHTML = renderAiSummaryBody(contentMd, snapshot);
|
||||
}
|
||||
|
||||
function setAiSummaryPlaceholder(body, html) {
|
||||
if (!body) return;
|
||||
body.classList.remove("ai-result-md");
|
||||
body.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderAiSummaryStats(snapshot) {
|
||||
const el = document.getElementById("ai-summary-stats");
|
||||
if (!el) return;
|
||||
if (!snapshot || !snapshot.totals) {
|
||||
el.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
const t = snapshot.totals;
|
||||
const closedPnl = Number(t.total_pnl_u);
|
||||
const floatPnl = Number(t.float_pnl_u);
|
||||
const closedCls = aiPnlClass(closedPnl);
|
||||
const floatCls = aiPnlClass(floatPnl);
|
||||
el.innerHTML = [
|
||||
`<span class="ai-stat-chip"><strong>交易日</strong>${esc(t.trading_day || "—")}</span>`,
|
||||
`<span class="ai-stat-chip ${closedCls}"><strong>平仓盈亏</strong><span class="ai-stat-val ${closedCls}">${aiPnlSigned(closedPnl, 2)}</span></span>`,
|
||||
`<span class="ai-stat-chip"><strong>笔数</strong>${t.closed_count || 0}(胜${t.win_count || 0}/负${t.loss_count || 0})</span>`,
|
||||
`<span class="ai-stat-chip ${floatCls}"><strong>浮盈亏</strong><span class="ai-stat-val ${floatCls}">${aiPnlSigned(floatPnl, 2)}</span></span>`,
|
||||
].join("");
|
||||
}
|
||||
|
||||
function scrollAiChatToEnd() {
|
||||
const box = document.getElementById("ai-chat-messages");
|
||||
if (!box) return;
|
||||
@@ -3476,21 +3313,6 @@
|
||||
if (input) input.disabled = busy;
|
||||
}
|
||||
|
||||
async function loadAiSummary() {
|
||||
const body = document.getElementById("ai-summary-body");
|
||||
try {
|
||||
const r = await apiFetch("/api/ai/summary");
|
||||
const j = await r.json();
|
||||
const latest = j.latest;
|
||||
if (latest && latest.content_md) {
|
||||
if (body) setAiSummaryMarkdown(body, latest.content_md, latest.stats_snapshot);
|
||||
renderAiSummaryStats(latest.stats_snapshot);
|
||||
}
|
||||
} catch (e) {
|
||||
if (body) setAiSummaryPlaceholder(body, `<p class="ai-placeholder">${esc(String(e))}</p>`);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAiChatSession() {
|
||||
const r = await apiFetch("/api/ai/chat/session");
|
||||
const j = await r.json();
|
||||
@@ -3547,7 +3369,7 @@
|
||||
|
||||
async function loadAiPage() {
|
||||
applyAiMobileTab();
|
||||
await Promise.all([loadAiSummary(), loadAiChatSession()]);
|
||||
await loadAiChatSession();
|
||||
if (isMobileLayout() && (localStorage.getItem(AI_MOBILE_TAB_KEY) || "chat") === "chat") {
|
||||
const input = document.getElementById("ai-chat-input");
|
||||
if (input && !aiChatLoading) {
|
||||
@@ -3556,38 +3378,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function generateAiSummary() {
|
||||
if (aiSummaryLoading) return;
|
||||
aiSummaryLoading = true;
|
||||
const btn = document.getElementById("btn-ai-summary");
|
||||
const body = document.getElementById("ai-summary-body");
|
||||
if (btn) btn.disabled = true;
|
||||
if (body) setAiSummaryPlaceholder(body, '<p class="ai-placeholder">正在聚合四户数据并生成总结…</p>');
|
||||
try {
|
||||
const r = await apiFetch("/api/ai/summary/generate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ force: true }),
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!r.ok) throw new Error(j.detail || j.msg || "生成失败");
|
||||
if (!j.ok && j.detail) throw new Error(j.detail);
|
||||
const sum = j.summary;
|
||||
if (sum && sum.content_md && body) {
|
||||
setAiSummaryMarkdown(body, sum.content_md, sum.stats_snapshot);
|
||||
renderAiSummaryStats(sum.stats_snapshot);
|
||||
}
|
||||
showToast(j.cached ? "已是最新上下文,返回缓存总结" : "今日总结已生成");
|
||||
await loadAiSummary();
|
||||
} catch (e) {
|
||||
showToast(String(e), true);
|
||||
if (body) setAiSummaryPlaceholder(body, `<p class="ai-placeholder">${esc(String(e))}</p>`);
|
||||
} finally {
|
||||
aiSummaryLoading = false;
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function newAiChat(botMode) {
|
||||
const mode = botMode === "general" ? "general" : "trading";
|
||||
try {
|
||||
@@ -3663,8 +3453,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
const aiSummaryBtn = document.getElementById("btn-ai-summary");
|
||||
if (aiSummaryBtn) aiSummaryBtn.onclick = () => generateAiSummary();
|
||||
const aiChatNewBtn = document.getElementById("btn-ai-chat-new");
|
||||
if (aiChatNewBtn) aiChatNewBtn.onclick = () => newAiChat(aiSelectedBotMode);
|
||||
const aiChatForm = document.getElementById("ai-chat-form");
|
||||
|
||||
Reference in New Issue
Block a user