This commit is contained in:
dekun
2026-05-27 16:32:30 +08:00
parent b9af1f69fe
commit bb8aca0cb3
10 changed files with 366 additions and 36 deletions
+16 -8
View File
@@ -182,10 +182,11 @@ def ai_generate(
def ai_review(trades_text: str, period_title: str, image_paths=None) -> str:
n_img = len(image_paths or [])
period_label = "" if "" in str(period_title) else ""
attach_note = (
f"【系统说明:已向模型附带 {n_img} 张复盘附图(自动K线或上传截图),请结合附图分析第5节。】\n\n"
f"【系统说明:已向模型附带 {n_img} 张复盘附图(自动K线或上传截图),请结合附图分析第5节。】\n\n"
if n_img
else "【系统说明:本次未附带复盘附图,第5节请写明「无附图,无法看图」;保存复盘记录时可勾选「自动生成K线图」。】\n\n"
else "【系统说明:本次未附带复盘附图,第5节请写明「无附图,无法看图」;保存复盘记录时可勾选「自动生成K线图」。】\n\n"
)
prompt = f"""
你是一位专业交易教练。下面是用户的{period_title}交易记录,请做简洁、可执行的复盘(中文)。
@@ -198,12 +199,19 @@ def ai_review(trades_text: str, period_title: str, image_paths=None) -> str:
- 禁止用语:人身攻击、夸张定性(如「致命伤」「灾难」);语气克制、对事不对人。
- 若有截图且你能辨认,再结合图讨论;看不清或无明确定位则明确说「无法从图确认」,不得虚构 K 线故事。
【输出结构
1. 总体盈亏结构(紧扣笔数、盈亏数字与 RR,少形容词)
2. 心态与执行(每笔 1–10 分 + 一句依据;依据必须对应记录字段)
3. 行为标签(提前离场 / 乱开仓 / 扛单等):仅在有字段或自述支撑时点名;否则写「记录未勾选或未描述,不作强加」
4. 改进建议(最多 3 条,每条具体可执行)
5. 图表(若有且可读):结合价格行为简述;否则一两句说明无法看图分析
【输出格式 — Markdown,必须严格遵守
- 第一行:**交易复盘报告({period_label}度)**
- 五个大节标题必须**完全一致**(含 emoji,不要用其它编号或改名):
**1. 📊 总体盈亏结构**
**2. 🧠 心态与执行**
**3. 🏷️ 行为标签**
**4. ✅ 改进建议**
**5. 📈 图表分析**
- 每节正文用 `- **子项名**:内容` 列表;第4节改进建议用有序列表 `1. 2. 3.`
- 第1节至少包含:**笔数/盈亏**、**风险回报比**、**总结**
- 第2节至少包含:**得分**(1–10)、**依据**(对应记录字段)
- 第5节至少包含:**趋势确认**、**执行路径**(记录不足则写明)
- 语气简洁,少形容词;不要输出代码块、不要表格
交易记录:
{trades_text}
+11 -1
View File
@@ -1,4 +1,4 @@
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, Response
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, Response, send_file
import sqlite3
import csv
from io import StringIO
@@ -7517,6 +7517,16 @@ def api_reviews():
return jsonify([row_to_dict(r) for r in rows])
_AI_REVIEW_RENDER_JS = os.path.join(os.path.dirname(BASE_DIR), "static", "ai_review_render.js")
@app.route("/static/ai_review_render.js")
def static_ai_review_render_js():
if not os.path.isfile(_AI_REVIEW_RENDER_JS):
return Response("not found", status=404, mimetype="text/plain; charset=utf-8")
return send_file(_AI_REVIEW_RENDER_JS, mimetype="application/javascript; charset=utf-8")
@app.route("/export/review_md/<rid>")
@login_required
def export_review_md(rid):
+43 -6
View File
@@ -62,6 +62,15 @@
.pnl-loss{color:#ff6666;font-weight:600}
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
.ai-result-md ul,.ai-result-md ol,.detail-modal .panel-body.md-review ul,.detail-modal .panel-body.md-review ol{margin:6px 0 8px 1.25em;padding:0}
.ai-result-md li,.detail-modal .panel-body.md-review li{margin:5px 0;line-height:1.5}
.ai-result-md strong,.detail-modal .panel-body.md-review strong{color:#f0f3ff;font-weight:600}
.ai-result-md h2,.detail-modal .panel-body.md-review h2{font-size:1.02rem;color:#b8c8ff;margin:14px 0 8px;padding-bottom:4px;border-bottom:1px solid #2e2e45}
.ai-result-md h3,.detail-modal .panel-body.md-review h3{font-size:.92rem;color:#c9d4ff;margin:10px 0 6px}
.ai-result-md code,.detail-modal .panel-body.md-review code{background:#252538;padding:1px 4px;border-radius:4px;font-size:.82em}
.ai-result-md .md-raw-block-title,.detail-modal .panel-body.md-review .md-raw-block-title{margin-top:14px;padding-top:10px;border-top:1px dashed #3a3a55;color:#a8b0d8;font-weight:600}
.price-up{color:#4cd97f}
.price-down{color:#ff6666}
.price-flat{color:#cfd3ef}
@@ -852,6 +861,7 @@
</div>
</div>
<script src="/static/ai_review_render.js?v=1"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
@@ -916,12 +926,39 @@ document.addEventListener("keydown", function(e){
const card = document.getElementById("review-card");
if(card && card.classList.contains("is-fullscreen")){ toggleReviewCardFullscreen(); }
});
function setAiReviewMarkdown(el, rawText){
if(!el) return;
if(window.AiReviewRender && AiReviewRender.setElementMarkdown){
AiReviewRender.setElementMarkdown(el, rawText || "");
} else {
el.classList.remove("ai-result-md");
el.innerText = rawText || "";
}
}
function setDetailBodyPlain(text){
const body = document.getElementById("detailBody");
if(!body) return;
body.classList.remove("md-review");
body.innerText = text || "";
}
function setDetailBodyMarkdown(text){
const body = document.getElementById("detailBody");
if(!body) return;
if(window.AiReviewRender && AiReviewRender.setElementMarkdown){
body.classList.add("md-review");
AiReviewRender.setElementMarkdown(body, text || "");
} else {
setDetailBodyPlain(text);
}
}
function openAiInlineResultFullscreen(title, elementId){
const el = document.getElementById(elementId || "daily_result");
const text = String((el && el.innerText) || "").trim();
const text = (window.AiReviewRender && AiReviewRender.getElementMarkdown)
? String(AiReviewRender.getElementMarkdown(el) || "").trim()
: String((el && el.innerText) || "").trim();
if(!text){ alert("暂无内容"); return; }
document.getElementById("detailTitle").innerText = title || "AI复盘";
document.getElementById("detailBody").innerText = text;
setDetailBodyMarkdown(text);
const imgEl = document.getElementById("detailImage");
imgEl.src = "";
imgEl.style.display = "none";
@@ -960,7 +997,7 @@ function openJournalDetail(id){
`备注:${o.note || "无"}`,
].join("\n");
document.getElementById("detailTitle").innerText = `交易复盘详情|${o.coin || "-"} ${o.tf || "-"}`;
document.getElementById("detailBody").innerText = detail;
setDetailBodyPlain(detail);
const imgEl = document.getElementById("detailImage");
if(o.image){
imgEl.src = `/static/images/${o.image}`;
@@ -977,7 +1014,7 @@ function openReviewDetail(id, fullscreen){
const r = reviewCache[id];
if(!r){ return; }
document.getElementById("detailTitle").innerText = `${r.review_type === "daily" ? "日复盘" : "周复盘"}${r.target_date || "-"}`;
document.getElementById("detailBody").innerText = r.content || "";
setDetailBodyMarkdown(r.content || "");
const imgEl = document.getElementById("detailImage");
imgEl.src = "";
imgEl.style.display = "none";
@@ -1189,7 +1226,7 @@ function genDaily(){
.then(r=>r.json()).then(data=>{
const el=document.getElementById("daily_result");
const wrap=document.getElementById("daily_result_wrap");
el.innerText=data.result;
setAiReviewMarkdown(el, data.result);
if(wrap){ wrap.style.display="block"; }
else { el.style.display="block"; }
loadReviews();
@@ -1204,7 +1241,7 @@ function genWeekly(){
.then(r=>r.json()).then(data=>{
const el=document.getElementById("weekly_result");
const wrap=document.getElementById("weekly_result_wrap");
el.innerText=data.result;
setAiReviewMarkdown(el, data.result);
if(wrap){ wrap.style.display="block"; }
else { el.style.display="block"; }
loadReviews();
+11 -1
View File
@@ -1,4 +1,4 @@
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, Response
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, Response, send_file
import sqlite3
import csv
from io import StringIO
@@ -7597,6 +7597,16 @@ def api_reviews():
return jsonify([row_to_dict(r) for r in rows])
_AI_REVIEW_RENDER_JS = os.path.join(os.path.dirname(BASE_DIR), "static", "ai_review_render.js")
@app.route("/static/ai_review_render.js")
def static_ai_review_render_js():
if not os.path.isfile(_AI_REVIEW_RENDER_JS):
return Response("not found", status=404, mimetype="text/plain; charset=utf-8")
return send_file(_AI_REVIEW_RENDER_JS, mimetype="application/javascript; charset=utf-8")
@app.route("/export/review_md/<rid>")
@login_required
def export_review_md(rid):
+43 -6
View File
@@ -62,6 +62,15 @@
.pnl-loss{color:#ff6666;font-weight:600}
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
.ai-result-md ul,.ai-result-md ol,.detail-modal .panel-body.md-review ul,.detail-modal .panel-body.md-review ol{margin:6px 0 8px 1.25em;padding:0}
.ai-result-md li,.detail-modal .panel-body.md-review li{margin:5px 0;line-height:1.5}
.ai-result-md strong,.detail-modal .panel-body.md-review strong{color:#f0f3ff;font-weight:600}
.ai-result-md h2,.detail-modal .panel-body.md-review h2{font-size:1.02rem;color:#b8c8ff;margin:14px 0 8px;padding-bottom:4px;border-bottom:1px solid #2e2e45}
.ai-result-md h3,.detail-modal .panel-body.md-review h3{font-size:.92rem;color:#c9d4ff;margin:10px 0 6px}
.ai-result-md code,.detail-modal .panel-body.md-review code{background:#252538;padding:1px 4px;border-radius:4px;font-size:.82em}
.ai-result-md .md-raw-block-title,.detail-modal .panel-body.md-review .md-raw-block-title{margin-top:14px;padding-top:10px;border-top:1px dashed #3a3a55;color:#a8b0d8;font-weight:600}
.price-up{color:#4cd97f}
.price-down{color:#ff6666}
.price-flat{color:#cfd3ef}
@@ -852,6 +861,7 @@
</div>
</div>
<script src="/static/ai_review_render.js?v=1"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
@@ -916,12 +926,39 @@ document.addEventListener("keydown", function(e){
const card = document.getElementById("review-card");
if(card && card.classList.contains("is-fullscreen")){ toggleReviewCardFullscreen(); }
});
function setAiReviewMarkdown(el, rawText){
if(!el) return;
if(window.AiReviewRender && AiReviewRender.setElementMarkdown){
AiReviewRender.setElementMarkdown(el, rawText || "");
} else {
el.classList.remove("ai-result-md");
el.innerText = rawText || "";
}
}
function setDetailBodyPlain(text){
const body = document.getElementById("detailBody");
if(!body) return;
body.classList.remove("md-review");
body.innerText = text || "";
}
function setDetailBodyMarkdown(text){
const body = document.getElementById("detailBody");
if(!body) return;
if(window.AiReviewRender && AiReviewRender.setElementMarkdown){
body.classList.add("md-review");
AiReviewRender.setElementMarkdown(body, text || "");
} else {
setDetailBodyPlain(text);
}
}
function openAiInlineResultFullscreen(title, elementId){
const el = document.getElementById(elementId || "daily_result");
const text = String((el && el.innerText) || "").trim();
const text = (window.AiReviewRender && AiReviewRender.getElementMarkdown)
? String(AiReviewRender.getElementMarkdown(el) || "").trim()
: String((el && el.innerText) || "").trim();
if(!text){ alert("暂无内容"); return; }
document.getElementById("detailTitle").innerText = title || "AI复盘";
document.getElementById("detailBody").innerText = text;
setDetailBodyMarkdown(text);
const imgEl = document.getElementById("detailImage");
imgEl.src = "";
imgEl.style.display = "none";
@@ -960,7 +997,7 @@ function openJournalDetail(id){
`备注:${o.note || "无"}`,
].join("\n");
document.getElementById("detailTitle").innerText = `交易复盘详情|${o.coin || "-"} ${o.tf || "-"}`;
document.getElementById("detailBody").innerText = detail;
setDetailBodyPlain(detail);
const imgEl = document.getElementById("detailImage");
if(o.image){
imgEl.src = `/static/images/${o.image}`;
@@ -977,7 +1014,7 @@ function openReviewDetail(id, fullscreen){
const r = reviewCache[id];
if(!r){ return; }
document.getElementById("detailTitle").innerText = `${r.review_type === "daily" ? "日复盘" : "周复盘"}${r.target_date || "-"}`;
document.getElementById("detailBody").innerText = r.content || "";
setDetailBodyMarkdown(r.content || "");
const imgEl = document.getElementById("detailImage");
imgEl.src = "";
imgEl.style.display = "none";
@@ -1189,7 +1226,7 @@ function genDaily(){
.then(r=>r.json()).then(data=>{
const el=document.getElementById("daily_result");
const wrap=document.getElementById("daily_result_wrap");
el.innerText=data.result;
setAiReviewMarkdown(el, data.result);
if(wrap){ wrap.style.display="block"; }
else { el.style.display="block"; }
loadReviews();
@@ -1204,7 +1241,7 @@ function genWeekly(){
.then(r=>r.json()).then(data=>{
const el=document.getElementById("weekly_result");
const wrap=document.getElementById("weekly_result_wrap");
el.innerText=data.result;
setAiReviewMarkdown(el, data.result);
if(wrap){ wrap.style.display="block"; }
else { el.style.display="block"; }
loadReviews();
+11 -1
View File
@@ -1,4 +1,4 @@
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, Response
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, Response, send_file
import sqlite3
import csv
from io import StringIO
@@ -7004,6 +7004,16 @@ def api_reviews():
return jsonify([row_to_dict(r) for r in rows])
_AI_REVIEW_RENDER_JS = os.path.join(os.path.dirname(BASE_DIR), "static", "ai_review_render.js")
@app.route("/static/ai_review_render.js")
def static_ai_review_render_js():
if not os.path.isfile(_AI_REVIEW_RENDER_JS):
return Response("not found", status=404, mimetype="text/plain; charset=utf-8")
return send_file(_AI_REVIEW_RENDER_JS, mimetype="application/javascript; charset=utf-8")
@app.route("/export/review_md/<rid>")
@login_required
def export_review_md(rid):
+43 -6
View File
@@ -63,6 +63,15 @@
.pnl-neutral{color:#cfd3ef;font-weight:600}
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
.ai-result-md ul,.ai-result-md ol,.detail-modal .panel-body.md-review ul,.detail-modal .panel-body.md-review ol{margin:6px 0 8px 1.25em;padding:0}
.ai-result-md li,.detail-modal .panel-body.md-review li{margin:5px 0;line-height:1.5}
.ai-result-md strong,.detail-modal .panel-body.md-review strong{color:#f0f3ff;font-weight:600}
.ai-result-md h2,.detail-modal .panel-body.md-review h2{font-size:1.02rem;color:#b8c8ff;margin:14px 0 8px;padding-bottom:4px;border-bottom:1px solid #2e2e45}
.ai-result-md h3,.detail-modal .panel-body.md-review h3{font-size:.92rem;color:#c9d4ff;margin:10px 0 6px}
.ai-result-md code,.detail-modal .panel-body.md-review code{background:#252538;padding:1px 4px;border-radius:4px;font-size:.82em}
.ai-result-md .md-raw-block-title,.detail-modal .panel-body.md-review .md-raw-block-title{margin-top:14px;padding-top:10px;border-top:1px dashed #3a3a55;color:#a8b0d8;font-weight:600}
.price-up{color:#4cd97f}
.price-down{color:#ff6666}
.price-flat{color:#cfd3ef}
@@ -691,6 +700,7 @@
</div>
</div>
<script src="/static/ai_review_render.js?v=1"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
@@ -754,12 +764,39 @@ document.addEventListener("keydown", function(e){
const card = document.getElementById("review-card");
if(card && card.classList.contains("is-fullscreen")){ toggleReviewCardFullscreen(); }
});
function setAiReviewMarkdown(el, rawText){
if(!el) return;
if(window.AiReviewRender && AiReviewRender.setElementMarkdown){
AiReviewRender.setElementMarkdown(el, rawText || "");
} else {
el.classList.remove("ai-result-md");
el.innerText = rawText || "";
}
}
function setDetailBodyPlain(text){
const body = document.getElementById("detailBody");
if(!body) return;
body.classList.remove("md-review");
body.innerText = text || "";
}
function setDetailBodyMarkdown(text){
const body = document.getElementById("detailBody");
if(!body) return;
if(window.AiReviewRender && AiReviewRender.setElementMarkdown){
body.classList.add("md-review");
AiReviewRender.setElementMarkdown(body, text || "");
} else {
setDetailBodyPlain(text);
}
}
function openAiInlineResultFullscreen(title, elementId){
const el = document.getElementById(elementId || "daily_result");
const text = String((el && el.innerText) || "").trim();
const text = (window.AiReviewRender && AiReviewRender.getElementMarkdown)
? String(AiReviewRender.getElementMarkdown(el) || "").trim()
: String((el && el.innerText) || "").trim();
if(!text){ alert("暂无内容"); return; }
document.getElementById("detailTitle").innerText = title || "AI复盘";
document.getElementById("detailBody").innerText = text;
setDetailBodyMarkdown(text);
const imgEl = document.getElementById("detailImage");
imgEl.src = "";
imgEl.style.display = "none";
@@ -834,7 +871,7 @@ function openJournalDetail(id){
`备注:${o.note || "无"}`,
].join("\n");
document.getElementById("detailTitle").innerText = `交易复盘详情|${o.coin || "-"} ${o.tf || "-"}`;
document.getElementById("detailBody").innerText = detail;
setDetailBodyPlain(detail);
const imgEl = document.getElementById("detailImage");
if(o.image){
imgEl.src = `/static/images/${o.image}`;
@@ -851,7 +888,7 @@ function openReviewDetail(id, fullscreen){
const r = reviewCache[id];
if(!r){ return; }
document.getElementById("detailTitle").innerText = `${r.review_type === "daily" ? "日复盘" : "周复盘"}${r.target_date || "-"}`;
document.getElementById("detailBody").innerText = r.content || "";
setDetailBodyMarkdown(r.content || "");
const imgEl = document.getElementById("detailImage");
imgEl.src = "";
imgEl.style.display = "none";
@@ -1022,7 +1059,7 @@ function genDaily(){
.then(r=>r.json()).then(data=>{
const el=document.getElementById("daily_result");
const wrap=document.getElementById("daily_result_wrap");
el.innerText=data.result;
setAiReviewMarkdown(el, data.result);
if(wrap){ wrap.style.display="block"; }
else { el.style.display="block"; }
loadReviews();
@@ -1037,7 +1074,7 @@ function genWeekly(){
.then(r=>r.json()).then(data=>{
const el=document.getElementById("weekly_result");
const wrap=document.getElementById("weekly_result_wrap");
el.innerText=data.result;
setAiReviewMarkdown(el, data.result);
if(wrap){ wrap.style.display="block"; }
else { el.style.display="block"; }
loadReviews();
+11 -1
View File
@@ -1,4 +1,4 @@
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, Response
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, Response, send_file
import sqlite3
import csv
from io import StringIO
@@ -6912,6 +6912,16 @@ def api_reviews():
return jsonify([row_to_dict(r) for r in rows])
_AI_REVIEW_RENDER_JS = os.path.join(os.path.dirname(BASE_DIR), "static", "ai_review_render.js")
@app.route("/static/ai_review_render.js")
def static_ai_review_render_js():
if not os.path.isfile(_AI_REVIEW_RENDER_JS):
return Response("not found", status=404, mimetype="text/plain; charset=utf-8")
return send_file(_AI_REVIEW_RENDER_JS, mimetype="application/javascript; charset=utf-8")
@app.route("/export/review_md/<rid>")
@login_required
def export_review_md(rid):
+43 -6
View File
@@ -62,6 +62,15 @@
.pnl-loss{color:#ff6666;font-weight:600}
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
.ai-result-md ul,.ai-result-md ol,.detail-modal .panel-body.md-review ul,.detail-modal .panel-body.md-review ol{margin:6px 0 8px 1.25em;padding:0}
.ai-result-md li,.detail-modal .panel-body.md-review li{margin:5px 0;line-height:1.5}
.ai-result-md strong,.detail-modal .panel-body.md-review strong{color:#f0f3ff;font-weight:600}
.ai-result-md h2,.detail-modal .panel-body.md-review h2{font-size:1.02rem;color:#b8c8ff;margin:14px 0 8px;padding-bottom:4px;border-bottom:1px solid #2e2e45}
.ai-result-md h3,.detail-modal .panel-body.md-review h3{font-size:.92rem;color:#c9d4ff;margin:10px 0 6px}
.ai-result-md code,.detail-modal .panel-body.md-review code{background:#252538;padding:1px 4px;border-radius:4px;font-size:.82em}
.ai-result-md .md-raw-block-title,.detail-modal .panel-body.md-review .md-raw-block-title{margin-top:14px;padding-top:10px;border-top:1px dashed #3a3a55;color:#a8b0d8;font-weight:600}
.price-up{color:#4cd97f}
.price-down{color:#ff6666}
.price-flat{color:#cfd3ef}
@@ -861,6 +870,7 @@
</div>
</div>
<script src="/static/ai_review_render.js?v=1"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
@@ -925,12 +935,39 @@ document.addEventListener("keydown", function(e){
const card = document.getElementById("review-card");
if(card && card.classList.contains("is-fullscreen")){ toggleReviewCardFullscreen(); }
});
function setAiReviewMarkdown(el, rawText){
if(!el) return;
if(window.AiReviewRender && AiReviewRender.setElementMarkdown){
AiReviewRender.setElementMarkdown(el, rawText || "");
} else {
el.classList.remove("ai-result-md");
el.innerText = rawText || "";
}
}
function setDetailBodyPlain(text){
const body = document.getElementById("detailBody");
if(!body) return;
body.classList.remove("md-review");
body.innerText = text || "";
}
function setDetailBodyMarkdown(text){
const body = document.getElementById("detailBody");
if(!body) return;
if(window.AiReviewRender && AiReviewRender.setElementMarkdown){
body.classList.add("md-review");
AiReviewRender.setElementMarkdown(body, text || "");
} else {
setDetailBodyPlain(text);
}
}
function openAiInlineResultFullscreen(title, elementId){
const el = document.getElementById(elementId || "daily_result");
const text = String((el && el.innerText) || "").trim();
const text = (window.AiReviewRender && AiReviewRender.getElementMarkdown)
? String(AiReviewRender.getElementMarkdown(el) || "").trim()
: String((el && el.innerText) || "").trim();
if(!text){ alert("暂无内容"); return; }
document.getElementById("detailTitle").innerText = title || "AI复盘";
document.getElementById("detailBody").innerText = text;
setDetailBodyMarkdown(text);
const imgEl = document.getElementById("detailImage");
imgEl.src = "";
imgEl.style.display = "none";
@@ -969,7 +1006,7 @@ function openJournalDetail(id){
`备注:${o.note || "无"}`,
].join("\n");
document.getElementById("detailTitle").innerText = `交易复盘详情|${o.coin || "-"} ${o.tf || "-"}`;
document.getElementById("detailBody").innerText = detail;
setDetailBodyPlain(detail);
const imgEl = document.getElementById("detailImage");
if(o.image){
imgEl.src = `/static/images/${o.image}`;
@@ -986,7 +1023,7 @@ function openReviewDetail(id, fullscreen){
const r = reviewCache[id];
if(!r){ return; }
document.getElementById("detailTitle").innerText = `${r.review_type === "daily" ? "日复盘" : "周复盘"}${r.target_date || "-"}`;
document.getElementById("detailBody").innerText = r.content || "";
setDetailBodyMarkdown(r.content || "");
const imgEl = document.getElementById("detailImage");
imgEl.src = "";
imgEl.style.display = "none";
@@ -1198,7 +1235,7 @@ function genDaily(){
.then(r=>r.json()).then(data=>{
const el=document.getElementById("daily_result");
const wrap=document.getElementById("daily_result_wrap");
el.innerText=data.result;
setAiReviewMarkdown(el, data.result);
if(wrap){ wrap.style.display="block"; }
else { el.style.display="block"; }
loadReviews();
@@ -1213,7 +1250,7 @@ function genWeekly(){
.then(r=>r.json()).then(data=>{
const el=document.getElementById("weekly_result");
const wrap=document.getElementById("weekly_result_wrap");
el.innerText=data.result;
setAiReviewMarkdown(el, data.result);
if(wrap){ wrap.style.display="block"; }
else { el.style.display="block"; }
loadReviews();
+134
View File
@@ -0,0 +1,134 @@
/**
* AI 日复盘 / 周复盘Markdown 子集渲染 + 五节大标题图标兜底
*/
(function (global) {
"use strict";
var SECTION_FIXES = [
{ re: /^\*\*1\.\s*(?!📊)总体盈亏结构\*\*/m, rep: "**1. 📊 总体盈亏结构**" },
{ re: /^\*\*2\.\s*(?!🧠)心态与执行\*\*/m, rep: "**2. 🧠 心态与执行**" },
{ re: /^\*\*3\.\s*(?!🏷️)行为标签\*\*/m, rep: "**3. 🏷️ 行为标签**" },
{ re: /^\*\*4\.\s*(?!✅)改进建议\*\*/m, rep: "**4. ✅ 改进建议**" },
{ re: /^\*\*5\.\s*(?!📈)图表(?:分析)?\*\*/m, rep: "**5. 📈 图表分析**" },
{ re: /^1\.\s*(?!📊)总体盈亏结构/m, rep: "**1. 📊 总体盈亏结构**" },
{ re: /^2\.\s*(?!🧠)心态与执行/m, rep: "**2. 🧠 心态与执行**" },
{ re: /^3\.\s*(?!🏷️)行为标签/m, rep: "**3. 🏷️ 行为标签**" },
{ re: /^4\.\s*(?!✅)改进建议/m, rep: "**4. ✅ 改进建议**" },
{ re: /^5\.\s*(?!📈)图表/m, rep: "**5. 📈 图表分析**" },
];
function escapeHtml(s) {
return String(s || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function parseInline(raw) {
var s = escapeHtml(raw);
s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
return s;
}
function enhanceReviewHeadings(text) {
var out = String(text || "");
SECTION_FIXES.forEach(function (item) {
out = out.replace(item.re, item.rep);
});
if (/^【系统说明/m.test(out) && !/^️/m.test(out)) {
out = out.replace(/^【系统说明/gm, "️ 【系统说明");
}
if (/^原始记录:/m.test(out) && !/^📎/m.test(out)) {
out = out.replace(/^原始记录:/gm, "📎 **原始记录**");
}
return out;
}
function renderMarkdown(text) {
var src = enhanceReviewHeadings(text);
var lines = src.replace(/\r\n/g, "\n").split("\n");
var html = [];
var inUl = false;
var inOl = false;
function closeLists() {
if (inUl) {
html.push("</ul>");
inUl = false;
}
if (inOl) {
html.push("</ol>");
inOl = false;
}
}
lines.forEach(function (line) {
var trimmed = line.trim();
if (!trimmed) {
closeLists();
return;
}
var hm = trimmed.match(/^(#{1,3})\s+(.+)$/);
if (hm) {
closeLists();
var level = hm[1].length + 1;
if (level > 4) level = 4;
html.push("<h" + level + ">" + parseInline(hm[2]) + "</h" + level + ">");
return;
}
var ulm = trimmed.match(/^[-*]\s+(.+)$/);
if (ulm) {
if (!inUl) {
closeLists();
html.push("<ul>");
inUl = true;
}
html.push("<li>" + parseInline(ulm[1]) + "</li>");
return;
}
var olm = trimmed.match(/^\d+\.\s+(.+)$/);
if (olm && !/^\*\*/.test(trimmed)) {
if (!inOl) {
closeLists();
html.push("<ol>");
inOl = true;
}
html.push("<li>" + parseInline(olm[1]) + "</li>");
return;
}
closeLists();
if (/^📎\s*\*\*原始记录\*\*/.test(trimmed) || /^原始记录:/.test(trimmed)) {
html.push('<div class="md-raw-block-title">' + parseInline(trimmed) + "</div>");
return;
}
html.push("<p>" + parseInline(trimmed) + "</p>");
});
closeLists();
return html.join("\n");
}
function setElementMarkdown(el, rawText) {
if (!el) return;
var raw = String(rawText || "");
el.dataset.markdownRaw = raw;
el.classList.add("ai-result-md");
el.innerHTML = renderMarkdown(raw);
}
function getElementMarkdown(el) {
if (!el) return "";
if (el.dataset && el.dataset.markdownRaw != null) {
return el.dataset.markdownRaw;
}
return el.innerText || "";
}
global.AiReviewRender = {
enhanceReviewHeadings: enhanceReviewHeadings,
renderMarkdown: renderMarkdown,
setElementMarkdown: setElementMarkdown,
getElementMarkdown: getElementMarkdown,
};
})(typeof window !== "undefined" ? window : this);