- Delete dashboard_refresh.js (no longer needed) - Add api/dashboard_content.dspy returns dynamic UI with live data (queries llmusage + users tables server-side, returns full widget tree) - Simplify index.ui to use RefreshWidget with period_seconds=10 - ChartBar in dashboard_content.dspy auto-fetches top models via data_url - load_path.py updated separately in sage repo
101 lines
5.1 KiB
Plaintext
101 lines
5.1 KiB
Plaintext
"""返回 dashboard 动态 UI 描述(含实时数据)"""
|
||
from datetime import date, datetime, timedelta
|
||
|
||
db = DBPools()
|
||
today = date.today().isoformat()
|
||
now = datetime.now()
|
||
five_min_ago = (now - timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S')
|
||
|
||
# 今日用量
|
||
async with db.sqlorContext('sage') as sor:
|
||
usage = await sor.sqlExe(
|
||
"SELECT COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount FROM llmusage WHERE use_date = ${today}$",
|
||
{'today': today}
|
||
)
|
||
cnt = int(usage[0]['cnt']) if usage else 0
|
||
total_amount = float(usage[0]['total_amount']) if usage else 0.0
|
||
|
||
# 总用户数
|
||
async with db.sqlorContext('sage') as sor:
|
||
users = await sor.sqlExe("SELECT COUNT(*) as total_users FROM users", {})
|
||
total_users = int(users[0]['total_users']) if users else 0
|
||
|
||
# 并发用户
|
||
async with db.sqlorContext('sage') as sor:
|
||
conc = await sor.sqlExe(
|
||
"SELECT COUNT(DISTINCT userid) as concurrent_users FROM llmusage WHERE use_date = ${today}$ AND use_time >= ${five_min_ago}$",
|
||
{'today': today, 'five_min_ago': five_min_ago}
|
||
)
|
||
concurrent_users = int(conc[0]['concurrent_users']) if conc else 0
|
||
|
||
# ChartBar data_url — 用相对路径,bricks 会基于当前上下文解析
|
||
chart_data_url = 'api/get_top_models.dspy'
|
||
|
||
return {
|
||
"widgettype": "VBox",
|
||
"options": {"width": "100%"},
|
||
"subwidgets": [
|
||
{
|
||
"widgettype": "ResponsableBox",
|
||
"options": {"gap": "16px", "minWidth": "250px"},
|
||
"subwidgets": [
|
||
{
|
||
"widgettype": "VBox",
|
||
"id": "card_today_cnt",
|
||
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
|
||
"subwidgets": [
|
||
{"widgettype": "Text", "options": {"text": "今日调用笔数", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
|
||
{"widgettype": "Text", "id": "today_cnt_value", "options": {"text": str(cnt), "fontSize": "32px", "fontWeight": "bold", "color": "#1890ff"}}
|
||
]
|
||
},
|
||
{
|
||
"widgettype": "VBox",
|
||
"id": "card_today_amount",
|
||
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
|
||
"subwidgets": [
|
||
{"widgettype": "Text", "options": {"text": "今日交易金额", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
|
||
{"widgettype": "Text", "id": "today_amount_value", "options": {"text": f"\u00a5{total_amount:.2f}", "fontSize": "32px", "fontWeight": "bold", "color": "#52c41a"}}
|
||
]
|
||
},
|
||
{
|
||
"widgettype": "VBox",
|
||
"id": "card_total_users",
|
||
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
|
||
"subwidgets": [
|
||
{"widgettype": "Text", "options": {"text": "用户总数", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
|
||
{"widgettype": "Text", "id": "total_users_value", "options": {"text": str(total_users), "fontSize": "32px", "fontWeight": "bold", "color": "#722ed1"}}
|
||
]
|
||
},
|
||
{
|
||
"widgettype": "VBox",
|
||
"id": "card_concurrent_users",
|
||
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
|
||
"subwidgets": [
|
||
{"widgettype": "Text", "options": {"text": "当前并发用户", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}},
|
||
{"widgettype": "Text", "id": "concurrent_users_value", "options": {"text": str(concurrent_users), "fontSize": "32px", "fontWeight": "bold", "color": "#fa8c16"}}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"widgettype": "VBox",
|
||
"id": "chart_section",
|
||
"options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "marginTop": "20px", "minHeight": "350px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
|
||
"subwidgets": [
|
||
{"widgettype": "Text", "options": {"text": "Top 3 模型(今日)", "fontSize": "18px", "fontWeight": "bold", "color": "#333", "marginBottom": "16px"}},
|
||
{
|
||
"widgettype": "ChartBar",
|
||
"id": "top_models_chart",
|
||
"options": {
|
||
"height": "300px",
|
||
"width": "100%",
|
||
"data_url": chart_data_url,
|
||
"nameField": "model_name",
|
||
"valueFields": ["cnt", "total_amount"]
|
||
}
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|