1
This commit is contained in:
parent
41d19b5a36
commit
fa02e931de
@ -284,6 +284,18 @@ async def get_performance_stats() -> dict:
|
||||
"""获取推荐胜率统计"""
|
||||
try:
|
||||
from sqlalchemy import text
|
||||
latest_tracked_sql = (
|
||||
"WITH latest_tracked AS ("
|
||||
" SELECT r.id AS recommendation_id, t.pct_from_entry, t.max_return_pct, "
|
||||
" t.max_drawdown_pct, t.hit_target, t.hit_stop_loss "
|
||||
" FROM recommendations r "
|
||||
" INNER JOIN recommendation_tracking t ON t.recommendation_id = r.id "
|
||||
" INNER JOIN ("
|
||||
" SELECT recommendation_id, MAX(id) as max_id "
|
||||
" FROM recommendation_tracking GROUP BY recommendation_id"
|
||||
" ) latest ON t.id = latest.max_id"
|
||||
") "
|
||||
)
|
||||
async with get_db() as db:
|
||||
# 总推荐数
|
||||
result = await db.execute(
|
||||
@ -294,8 +306,8 @@ async def get_performance_stats() -> dict:
|
||||
# 有跟踪记录的推荐
|
||||
result = await db.execute(
|
||||
text(
|
||||
"SELECT COUNT(DISTINCT r.id) FROM recommendations r "
|
||||
"INNER JOIN recommendation_tracking t ON t.recommendation_id = r.id"
|
||||
latest_tracked_sql +
|
||||
"SELECT COUNT(*) FROM latest_tracked"
|
||||
)
|
||||
)
|
||||
tracked = result.scalar() or 0
|
||||
@ -303,14 +315,8 @@ async def get_performance_stats() -> dict:
|
||||
# 胜率基于最新跟踪日的最终 pct(正值=盈利,负值=亏损)
|
||||
result = await db.execute(
|
||||
text(
|
||||
"SELECT COUNT(*) FROM ("
|
||||
" SELECT t.recommendation_id, t.pct_from_entry as latest_pct "
|
||||
" FROM recommendation_tracking t "
|
||||
" INNER JOIN ("
|
||||
" SELECT recommendation_id, MAX(id) as max_id "
|
||||
" FROM recommendation_tracking GROUP BY recommendation_id"
|
||||
" ) latest ON t.id = latest.max_id"
|
||||
") WHERE latest_pct > 0"
|
||||
latest_tracked_sql +
|
||||
"SELECT COUNT(*) FROM latest_tracked WHERE pct_from_entry > 0"
|
||||
)
|
||||
)
|
||||
winning = result.scalar() or 0
|
||||
@ -318,14 +324,8 @@ async def get_performance_stats() -> dict:
|
||||
# 平均收益(基于最新跟踪日的 pct)
|
||||
result = await db.execute(
|
||||
text(
|
||||
"SELECT AVG(latest_pct) FROM ("
|
||||
" SELECT t.pct_from_entry as latest_pct "
|
||||
" FROM recommendation_tracking t "
|
||||
" INNER JOIN ("
|
||||
" SELECT recommendation_id, MAX(id) as max_id "
|
||||
" FROM recommendation_tracking GROUP BY recommendation_id"
|
||||
" ) latest ON t.id = latest.max_id"
|
||||
")"
|
||||
latest_tracked_sql +
|
||||
"SELECT AVG(pct_from_entry) FROM latest_tracked"
|
||||
)
|
||||
)
|
||||
avg_return = result.scalar()
|
||||
@ -334,8 +334,8 @@ async def get_performance_stats() -> dict:
|
||||
# 达到目标价的推荐
|
||||
result = await db.execute(
|
||||
text(
|
||||
"SELECT COUNT(DISTINCT recommendation_id) FROM recommendation_tracking "
|
||||
"WHERE hit_target = 1"
|
||||
latest_tracked_sql +
|
||||
"SELECT COUNT(*) FROM latest_tracked WHERE hit_target = 1"
|
||||
)
|
||||
)
|
||||
hit_target_count = result.scalar() or 0
|
||||
@ -343,8 +343,8 @@ async def get_performance_stats() -> dict:
|
||||
# 触发止损的推荐
|
||||
result = await db.execute(
|
||||
text(
|
||||
"SELECT COUNT(DISTINCT recommendation_id) FROM recommendation_tracking "
|
||||
"WHERE hit_stop_loss = 1"
|
||||
latest_tracked_sql +
|
||||
"SELECT COUNT(*) FROM latest_tracked WHERE hit_stop_loss = 1"
|
||||
)
|
||||
)
|
||||
hit_stop_count = result.scalar() or 0
|
||||
@ -364,14 +364,8 @@ async def get_performance_stats() -> dict:
|
||||
# 最大浮盈/最大回撤统计
|
||||
result = await db.execute(
|
||||
text(
|
||||
"SELECT AVG(max_return_pct), AVG(max_drawdown_pct) FROM ("
|
||||
" SELECT t.recommendation_id, t.max_return_pct, t.max_drawdown_pct "
|
||||
" FROM recommendation_tracking t "
|
||||
" INNER JOIN ("
|
||||
" SELECT recommendation_id, MAX(id) as max_id "
|
||||
" FROM recommendation_tracking GROUP BY recommendation_id"
|
||||
" ) latest ON t.id = latest.max_id"
|
||||
")"
|
||||
latest_tracked_sql +
|
||||
"SELECT AVG(max_return_pct), AVG(max_drawdown_pct) FROM latest_tracked"
|
||||
)
|
||||
)
|
||||
avg_extremes = result.fetchone()
|
||||
@ -425,6 +419,7 @@ async def get_performance_stats() -> dict:
|
||||
"created_at": str(r["created_at"])[:10] if r["created_at"] else "",
|
||||
})
|
||||
|
||||
winning = min(winning, tracked)
|
||||
win_rate = round(winning / tracked * 100, 1) if tracked > 0 else 0
|
||||
|
||||
return {
|
||||
|
||||
@ -27,6 +27,7 @@ TOOL_LABELS = {
|
||||
"get_user_watchlist_snapshot": "读取自选股作战池",
|
||||
"get_stock_kline": "查询K线数据",
|
||||
"get_stock_capital_flow": "查询资金流向",
|
||||
"diagnose_stock": "生成个股会诊",
|
||||
"search_stock": "搜索股票",
|
||||
}
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ ENHANCE_USER_TEMPLATE = """\
|
||||
请对该股票进行 2-3 句话的深度分析:"""
|
||||
|
||||
CHAT_SYSTEM_PROMPT = """\
|
||||
你是 A 股投研作战台里的 AI 作战助理,不是泛化闲聊机器人。你的核心任务是解释系统已经生成的结果,并帮助用户把市场、板块、推荐和自选股串成可执行判断。
|
||||
你是 A 股投研作战台里的系统智能体,不是泛化闲聊机器人。你的核心任务是回答所有与本系统有关的问题,并把市场、板块、推荐、自选股、个股诊断和系统校准串成可执行判断。
|
||||
|
||||
你的能力:
|
||||
1. 查询今日作战结论,包括市场状态、今日打法、建议仓位、重点板块和规避规则
|
||||
@ -42,12 +42,15 @@ CHAT_SYSTEM_PROMPT = """\
|
||||
3. 查询当前用户的自选股池与最新建议
|
||||
4. 查询个股K线、技术面、资金流向数据
|
||||
5. 搜索股票代码,并把结果放回当前交易语境中分析
|
||||
6. 对单只股票生成系统化会诊,输出结论、触发条件、失效条件、仓位边界和风险清单
|
||||
|
||||
重要提醒:
|
||||
- 回答用户关于"今天市场怎么样"之类的问题时,必须调用 get_realtime_indices 获取实时指数数据
|
||||
- 回答用户关于"今天该怎么做"、"当前推荐怎么看"、"自选股该怎么处理"这类问题时,优先调用 get_strategy_board、get_latest_recommendations、get_user_watchlist_snapshot
|
||||
- 回答用户关于某只股票能不能看、是否该买、持仓怎么处理、为什么涨跌、是否要复盘时,必须先 search_stock(如果用户没给标准 ts_code),再调用 diagnose_stock;必要时补充 get_stock_capital_flow、get_stock_technical_signal
|
||||
- 盘中时段(9:30-15:00)必须使用实时数据,盘后时段使用当日收盘或最近一次系统生成的数据
|
||||
- 不要脱离系统上下文泛泛而谈,必须先调用工具获取最新结果再回答
|
||||
- A 股优先看资金顺势、主线板块、量价承接和位置;RSI/MACD/KDJ 只做节奏与风控确认,不能因超买超卖本身直接否决或买入
|
||||
|
||||
回答要求:
|
||||
1. 使用工具获取最新数据后再回答,不要凭空编造数据
|
||||
|
||||
@ -44,6 +44,8 @@ async def execute_tool(name: str, arguments: dict) -> str:
|
||||
return await _search_stock(arguments["keyword"])
|
||||
elif name == "get_stock_technical_signal":
|
||||
return await _get_stock_technical_signal(arguments["ts_code"])
|
||||
elif name == "diagnose_stock":
|
||||
return await _diagnose_stock(arguments["ts_code"], arguments.get("mode", "entry"))
|
||||
elif name == "get_sector_performance":
|
||||
return await _get_sector_performance(arguments["sector_name"])
|
||||
elif name == "get_realtime_indices":
|
||||
@ -227,6 +229,126 @@ async def _get_stock_technical_signal(ts_code: str) -> str:
|
||||
return json.dumps(data, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
async def _diagnose_stock(ts_code: str, mode: str = "entry") -> str:
|
||||
"""生成系统化个股会诊,供作战问答智能体调用。"""
|
||||
from sqlalchemy import text
|
||||
from app.db.database import get_db
|
||||
from app.db import tables
|
||||
from app.llm.client import chat_completion
|
||||
|
||||
mode_map = {
|
||||
"entry": "建仓前诊断",
|
||||
"holding": "持仓复核",
|
||||
"review": "回撤复盘",
|
||||
"tracking": "继续跟踪",
|
||||
}
|
||||
mode = mode if mode in mode_map else "entry"
|
||||
|
||||
strategy_board = await _get_strategy_board()
|
||||
latest_recommendations = await _get_latest_recommendations()
|
||||
hot_sectors = await _get_hot_sectors(8)
|
||||
kline = await _get_stock_kline(ts_code, 80)
|
||||
capital_flow = await _get_stock_capital_flow(ts_code, 15)
|
||||
technical_signal = await _get_stock_technical_signal(ts_code)
|
||||
|
||||
latest_rec = None
|
||||
stock_name = ts_code
|
||||
try:
|
||||
recs = json.loads(latest_recommendations)
|
||||
latest_rec = next((item for item in recs if item.get("ts_code") == ts_code), None)
|
||||
except Exception:
|
||||
latest_rec = None
|
||||
if latest_rec and latest_rec.get("name"):
|
||||
stock_name = latest_rec["name"]
|
||||
|
||||
recent_diagnoses = []
|
||||
try:
|
||||
async with get_db() as db:
|
||||
rows = (await db.execute(
|
||||
text(
|
||||
"SELECT name, diagnosis_mode, diagnosis, created_at "
|
||||
"FROM stock_diagnoses WHERE ts_code = :ts_code "
|
||||
"ORDER BY created_at DESC, id DESC LIMIT 3"
|
||||
),
|
||||
{"ts_code": ts_code},
|
||||
)).fetchall()
|
||||
recent_diagnoses = [dict(row._mapping) for row in rows]
|
||||
if recent_diagnoses and recent_diagnoses[0].get("name"):
|
||||
stock_name = recent_diagnoses[0]["name"]
|
||||
except Exception:
|
||||
recent_diagnoses = []
|
||||
|
||||
prompt = f"""请在 A 股作战台语境下,对 {ts_code} 做一次系统化个股会诊。
|
||||
|
||||
诊断模式: {mode_map[mode]}
|
||||
|
||||
今日作战结论:
|
||||
{strategy_board}
|
||||
|
||||
最新推荐池中该股记录:
|
||||
{json.dumps(latest_rec, ensure_ascii=False, default=str) if latest_rec else "不在最新推荐池"}
|
||||
|
||||
热门板块:
|
||||
{hot_sectors}
|
||||
|
||||
K线与技术指标:
|
||||
{kline}
|
||||
|
||||
资金流:
|
||||
{capital_flow}
|
||||
|
||||
技术信号:
|
||||
{technical_signal}
|
||||
|
||||
最近诊断:
|
||||
{json.dumps(recent_diagnoses, ensure_ascii=False, default=str)}
|
||||
|
||||
输出要求:
|
||||
- 先给明确结论,只能是「可操作 / 重点关注 / 观察 / 回避」
|
||||
- 明确当前动作、触发条件、失效条件、仓位边界、下一步观察点
|
||||
- A 股优先看资金顺势、主线板块、量价承接和位置;技术指标只做节奏与风控确认
|
||||
- RSI、MACD、KDJ 的超买超卖不能单独决定买卖
|
||||
- 不写传统研报,不堆原始数据,不承诺收益
|
||||
- 用 Markdown 输出,保持简洁"""
|
||||
|
||||
resp = await chat_completion([
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"你是 A 股投研作战台的个股会诊智能体。"
|
||||
"你必须融合系统作战结论、板块、推荐池、资金流、K线和技术信号,"
|
||||
"输出可执行但带风险边界的会诊结论。"
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": prompt},
|
||||
])
|
||||
if not resp or not resp.content:
|
||||
return json.dumps({"error": "个股会诊生成失败,LLM 未返回内容"}, ensure_ascii=False)
|
||||
|
||||
diagnosis = resp.content.strip()
|
||||
try:
|
||||
async with get_db() as db:
|
||||
await db.execute(
|
||||
tables.stock_diagnoses_table.insert().values(
|
||||
ts_code=ts_code,
|
||||
name=stock_name,
|
||||
diagnosis_mode=mode,
|
||||
diagnosis=diagnosis,
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
except Exception as e:
|
||||
logger.warning(f"保存聊天会诊结果失败 {ts_code}: {e}")
|
||||
|
||||
return json.dumps({
|
||||
"ts_code": ts_code,
|
||||
"name": stock_name,
|
||||
"mode": mode,
|
||||
"diagnosis": diagnosis,
|
||||
"saved": True,
|
||||
}, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
async def _get_sector_performance(sector_name: str) -> str:
|
||||
"""获取板块表现数据"""
|
||||
from app.engine.recommender import get_latest_sectors
|
||||
|
||||
@ -145,6 +145,28 @@ CHAT_TOOLS = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "diagnose_stock",
|
||||
"description": "对单只 A 股做系统化个股会诊,融合今日作战结论、推荐池、板块、K线、资金流和技术信号,输出结论、触发条件、失效条件和风险边界",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ts_code": {
|
||||
"type": "string",
|
||||
"description": "股票代码,如 '000001.SZ'。如果用户只给名称,应先调用 search_stock 找到 ts_code",
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["entry", "holding", "review", "tracking"],
|
||||
"description": "诊断模式:entry 建仓前;holding 持仓复核;review 回撤复盘;tracking 继续跟踪。默认 entry",
|
||||
},
|
||||
},
|
||||
"required": ["ts_code"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
|
||||
@ -12,23 +12,23 @@ interface DisplayMessage {
|
||||
|
||||
const QUICK_QUESTIONS = [
|
||||
"结合今日作战结论,告诉我今天应该重点看什么。",
|
||||
"把当前推荐池分成可操作、重点关注和仅观察三层讲给我。",
|
||||
"诊断一下 300750.SZ,给出触发条件和失效条件。",
|
||||
"看看我的自选股里哪些需要明天优先盯盘。",
|
||||
"如果今天只允许做一个方向,你建议我盯哪个主线,为什么?",
|
||||
"复盘当前推荐池,哪些信号最近更有效?",
|
||||
];
|
||||
|
||||
const CHAT_SCENES = [
|
||||
{
|
||||
title: "问今日打法",
|
||||
description: "进攻 / 试错 / 防守",
|
||||
title: "市场",
|
||||
description: "打法 / 仓位 / 风险",
|
||||
},
|
||||
{
|
||||
title: "问推荐池",
|
||||
description: "进池原因 / 触发 / 放弃",
|
||||
title: "个股",
|
||||
description: "诊断 / 触发 / 失效",
|
||||
},
|
||||
{
|
||||
title: "问自选股",
|
||||
description: "观察池 / 候选池 / 持仓池",
|
||||
title: "系统",
|
||||
description: "推荐池 / 自选股 / 校准",
|
||||
},
|
||||
];
|
||||
|
||||
@ -108,15 +108,13 @@ export default function ChatPage() {
|
||||
<aside className="hidden xl:flex xl:flex-col xl:gap-4">
|
||||
<div className="glass-card-static p-5 animate-fade-in-up">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-cyan-400">
|
||||
Combat Chat
|
||||
System Agent
|
||||
</div>
|
||||
<h1 className="mt-2 text-xl font-bold tracking-tight">AI 作战问答</h1>
|
||||
<h1 className="mt-2 text-xl font-bold tracking-tight">系统智能体</h1>
|
||||
</div>
|
||||
|
||||
<div className="glass-card-static p-5 animate-fade-in-up">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-amber-400">
|
||||
适合拿来问
|
||||
</div>
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-amber-400">能力</div>
|
||||
<div className="mt-4 space-y-3">
|
||||
{CHAT_SCENES.map((scene) => (
|
||||
<div key={scene.title} className="rounded-2xl border border-border-subtle bg-surface-1/70 px-4 py-3">
|
||||
@ -137,9 +135,9 @@ export default function ChatPage() {
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold">围绕作战结果继续追问</h2>
|
||||
<h2 className="text-sm font-semibold">A 股作战智能体</h2>
|
||||
<p className="text-xs text-text-muted">
|
||||
今日作战 / 推荐池 / 自选股 / 主线板块
|
||||
市场 / 推荐 / 自选 / 个股诊断 / 系统校准
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -163,7 +161,7 @@ export default function ChatPage() {
|
||||
<path d="M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">追问作战结论</h3>
|
||||
<h3 className="text-lg font-semibold">问市场、系统或个股</h3>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-8 grid w-full max-w-3xl gap-3 md:grid-cols-2">
|
||||
@ -230,7 +228,7 @@ export default function ChatPage() {
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="比如:结合我的自选股和今日主线,明天我应该先看哪几只?"
|
||||
placeholder="比如:诊断一下 300750.SZ,结合今日主线给触发和失效条件"
|
||||
rows={1}
|
||||
disabled={streaming}
|
||||
className="min-h-[46px] flex-1 resize-none rounded-2xl border border-border-subtle bg-surface-2 px-4 py-3 text-sm transition-all duration-200 placeholder:text-text-muted/40 focus:outline-none focus:ring-1 focus:ring-accent-cyan/30"
|
||||
|
||||
@ -153,7 +153,7 @@ export default function DashboardPage() {
|
||||
setRefreshResult(null);
|
||||
try {
|
||||
const result = await postAPI<{ tracked?: number; win_rate?: number }>("/api/recommendations/update-tracking");
|
||||
setRefreshResult(`跟踪已更新,样本 ${result.tracked ?? 0},胜率 ${(result.win_rate ?? 0).toFixed(1)}%`);
|
||||
setRefreshResult(`跟踪已更新,样本 ${result.tracked ?? 0},胜率 ${clampPercent(result.win_rate ?? 0).toFixed(1)}%`);
|
||||
await loadData();
|
||||
await loadSecondaryData();
|
||||
setTimeout(() => setRefreshResult(null), 5000);
|
||||
@ -752,6 +752,11 @@ function buildMarketSummary(
|
||||
};
|
||||
}
|
||||
|
||||
function clampPercent(value: number) {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
return Math.max(0, Math.min(100, value));
|
||||
}
|
||||
|
||||
function buildActionGuides(
|
||||
strategyProfile: LatestResult["strategy_profile"],
|
||||
board: StrategyBoard | null,
|
||||
|
||||
@ -91,6 +91,7 @@ export default function StrategyPage() {
|
||||
}, [authLoading, loadData, user]);
|
||||
|
||||
const diagnosis = useMemo(() => buildCalibrationDiagnosis(iteration, performance), [iteration, performance]);
|
||||
const safeWinRate = clampPercent(performance?.win_rate ?? 0);
|
||||
|
||||
if (authLoading || user?.role !== "admin" || loading) {
|
||||
return (
|
||||
@ -137,7 +138,7 @@ export default function StrategyPage() {
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 self-start">
|
||||
<MetricCard label="复盘样本" value={iteration?.sample_size ?? 0} />
|
||||
<MetricCard label="整体胜率" value={`${(performance?.win_rate ?? 0).toFixed(1)}%`} tone={(performance?.win_rate ?? 0) >= 50 ? "up" : "down"} />
|
||||
<MetricCard label="整体胜率" value={`${safeWinRate.toFixed(1)}%`} tone={safeWinRate >= 50 ? "up" : "down"} />
|
||||
<MetricCard label="平均收益" value={`${(performance?.avg_return ?? 0) > 0 ? "+" : ""}${(performance?.avg_return ?? 0).toFixed(2)}%`} tone={(performance?.avg_return ?? 0) >= 0 ? "up" : "down"} />
|
||||
<MetricCard label="平均回撤" value={`${(performance?.avg_max_drawdown ?? 0).toFixed(2)}%`} tone="down" />
|
||||
<MetricFact label="已跟踪" value={`${performance?.tracked ?? 0} 只`} />
|
||||
@ -238,7 +239,7 @@ function buildCalibrationDiagnosis(
|
||||
iteration: StrategyIterationReport | null,
|
||||
performance: PerformanceStats | null
|
||||
) {
|
||||
const winRate = performance?.win_rate ?? 0;
|
||||
const winRate = clampPercent(performance?.win_rate ?? 0);
|
||||
const avgReturn = performance?.avg_return ?? 0;
|
||||
const tracked = performance?.tracked ?? 0;
|
||||
const headline =
|
||||
@ -269,6 +270,11 @@ function buildCalibrationDiagnosis(
|
||||
return { headline, detail, useFor, notFor };
|
||||
}
|
||||
|
||||
function clampPercent(value: number) {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
return Math.max(0, Math.min(100, value));
|
||||
}
|
||||
|
||||
function DecisionList({
|
||||
title,
|
||||
items,
|
||||
|
||||
@ -118,8 +118,8 @@ export function SidebarNav() {
|
||||
<SideNavItem href="/recommendations" icon={<TargetIcon />} label="推荐池" />
|
||||
<SideNavItem href="/sectors" icon={<FireIcon />} label="板块主线" />
|
||||
<SideNavItem href="/watchlists" icon={<WatchlistIcon />} label="自选股" />
|
||||
<SideNavItem href="/chat" icon={<ChatIcon />} label="系统智能体" />
|
||||
<SideNavItem href="/diagnose" icon={<DiagnoseIcon />} label="个股诊断" />
|
||||
<SideNavItem href="/chat" icon={<ChatIcon />} label="作战问答" />
|
||||
{user?.role === "admin" && (
|
||||
<>
|
||||
<SideNavItem href="/strategy" icon={<StrategyIcon />} label="系统校准" />
|
||||
@ -159,6 +159,9 @@ export function MobileBottomNav() {
|
||||
<MobileNavItem href="/recommendations" label="推荐池">
|
||||
<TargetIcon />
|
||||
</MobileNavItem>
|
||||
<MobileNavItem href="/chat" label="智能体">
|
||||
<ChatIcon />
|
||||
</MobileNavItem>
|
||||
<MobileNavItem href="/watchlists" label="自选">
|
||||
<WatchlistIcon />
|
||||
</MobileNavItem>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user