From 5428a33a0ce07e9b2d418e346e79e59601de4bfd Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Sat, 5 Sep 2026 19:49:47 +0800 Subject: [PATCH] =?UTF-8?q?V3.5.1=20=E4=BC=98=E5=8C=96=EF=BC=9A=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E5=AD=98=E4=B8=BA=E5=8F=82=E8=80=83/=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=A0=87=E7=AD=BE=E5=88=87=E6=8D=A2/=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E7=8B=AC=E7=AB=8B=E9=A6=96=E9=A1=B5/=E6=8A=A5?= =?UTF-8?q?=E8=A1=A8=E6=8C=89=E6=97=A5=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 项目一键保存为参考:项目卡片「⭐存为参考」按钮,复制项目+任务到从参考项目中新建; 参考列表显示来源标记,非内置可删除;内置参考受保护(ref_builtin),种子自愈补标记 2. 修复AI Worker页 大模型接口库/团队 标签点击无反应(workersTab被pageWorkers重置的bug) 3. 对话独立首页:导航改为 对话/仪表盘 并列;首页=上部仪表盘摘要(可点击跳仪表盘)+ 中下对话主体(更高面板、流式光标、每条消息带用量) 4. 成本报表新增「按日期」维度(近7/30天/全部),按天统计调用/输入/输出/缓存/成本 --- README.md | 13 ++++- app.py | 93 ++++++++++++++++++++++++++++++++--- db.py | 4 ++ static/app.js | 122 ++++++++++++++++++++++++++++++++++------------ static/index.html | 3 +- static/style.css | 8 +++ 6 files changed, 203 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index c0703de..82d9c35 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,18 @@ > 以「项目」为中心、以「AI Worker」为执行单元的项目管理平台。 > 把大模型团队变成一支可指挥、可审计、可控成本的"虚拟团队"。 -**当前版本:V3.5**(精细化用量统计 / 大模型接口库 / AI Worker 团队 / 对话融合仪表盘 / 系统工作目录 / 流式单token超时) +**当前版本:V3.5**(精细化用量统计 / 大模型接口库 / AI Worker 团队 / 对话独立首页 / 系统工作目录 / 流式单token超时 / 按日期报表) + +--- + +## 🚀 V3.5.1 优化(本轮) + +| 能力 | 说明 | +|---|---| +| ⭐ 项目一键存为参考 | 项目卡片新增「⭐ 存为参考」按钮,把任意已创建项目一键保存到「从参考项目中新建」列表(复制项目+任务,原项目不受影响);参考列表里显示「来自「原项目」」来源标记,非内置参考可删除,内置 3 个受保护 | +| 🐛 修复标签切换 | AI Worker 页「大模型接口库 / 团队」标签点击无反应的 bug(onclick 里页面状态被重置)已修复,三标签正常切换 | +| 💬 对话独立首页 | 左侧导航改为「💬 对话 / 📊 仪表盘」并列;首页为全新对话页:**上部=仪表盘关键信息摘要(项目/任务/Worker/成本/调用次数,点击即跳转仪表盘)**,中下部=主体对话界面(更高更宽,流式输出带闪烁光标,每条记录 tokens/缓存命中/成本/首字延迟) | +| 📅 成本报表按日期 | 报表新增「按日期」维度:按天统计调用次数/输入/输出/缓存命中/总Tokens/成本,支持 近7天/30天/全部 筛选 | --- diff --git a/app.py b/app.py index 611a3d2..d9d88be 100644 --- a/app.py +++ b/app.py @@ -239,9 +239,11 @@ def health(): # V3.5 内置参考测试项目(从参考项目新建用,不进普通项目列表) # --------------------------------------------------------------------------- def seed_reference_projects(): - """幂等:内置 3 个不同维度的简单参考测试项目,供「从参考项目中新建」快速复制。""" - if db.q('SELECT COUNT(*) c FROM projects WHERE is_reference=1')[0]['c'] > 0: - return + """幂等且自愈:内置 3 个不同维度的简单参考测试项目(ref_builtin=1,受保护), + 供「从参考项目中新建」快速复制;历史数据自动补标记,缺失自动重建。""" + # 兼容历史:已有同名内置参考项目补 ref_builtin 标记 + for name in ('产品文案速写(参考)', 'Python 小工具(参考)', '市场调研简报(参考)'): + db.w('UPDATE projects SET ref_builtin=1 WHERE name=? AND is_reference=1 AND ref_builtin=0', (name,)) ts = db.now() refs = [ { @@ -287,10 +289,14 @@ def seed_reference_projects(): }, ] for ref in refs: + exist = db.q('SELECT id FROM projects WHERE name=? AND is_reference=1 AND ref_builtin=1', + (ref['name'],), one=True) + if exist: + continue pid = db.w( 'INSERT INTO projects (name, description, objective, acceptance_criteria, status, ' - 'budget_limit, deliver_type, workspace_dir, is_reference, auto_status, created_at, updated_at) ' - 'VALUES (?,?,?,?,?,?,?,?,1,?,?,?)', + 'budget_limit, deliver_type, workspace_dir, is_reference, ref_builtin, auto_status, created_at, updated_at) ' + 'VALUES (?,?,?,?,?,?,?,?,1,1,?,?,?)', (ref['name'], '内置参考测试项目,可「从参考项目新建」快速复制', ref['objective'], ref['acceptance_criteria'], 'active', 0, 'web', '', 'none', ts, ts)) db.w('UPDATE projects SET workspace_dir=? WHERE id=?', (f'ref_{pid}', pid)) @@ -1005,20 +1011,72 @@ def team_detail(tid): @app.route('/api/reference_projects') @require_auth def reference_projects(): - """内置参考测试项目(3 个,含任务列表),供「从参考项目中新建」快速复制""" + """参考测试项目(内置 + 用户保存的),供「从参考项目中新建」快速复制""" err = _check_perm_point('project.view') if err: return err - rows = db.q('SELECT * FROM projects WHERE is_reference=1 ORDER BY id') + rows = db.q('SELECT * FROM projects WHERE is_reference=1 ORDER BY ref_builtin DESC, id') out = [] for r in rows: r['tasks'] = db.q('SELECT * FROM tasks WHERE project_id=? AND deleted=0 ORDER BY id', (r['id'],)) for t in r['tasks']: t['depends_on'] = _json.loads(t.get('depends_on') or '[]') + src = db.q('SELECT id, name FROM projects WHERE id=?', (r.get('ref_source_id') or 0,), one=True) + r['source_project'] = dict(src) if src else None out.append(r) return jsonify({'ok': True, 'data': out}) +@app.route('/api/projects//save_reference', methods=['POST']) +@require_auth +def project_save_reference(pid): + """把已创建的项目一键保存为参考项目(复制项目+任务,原项目不受影响),供「从参考项目中新建」使用""" + err = _check_project_perm(pid, 'view') + if err: + return err + proj = db.q('SELECT * FROM projects WHERE id=?', (pid,), one=True) + if not proj: + return jsonify({'ok': False, 'error': '项目不存在'}), 404 + if proj.get('is_reference'): + return jsonify({'ok': False, 'error': '该项目本身已是参考项目'}), 400 + ts = db.now() + new_name = (proj['name'].strip() or '参考项目') + '(参考)' + rpid = db.w( + 'INSERT INTO projects (name, description, objective, acceptance_criteria, status, ' + 'budget_limit, deliver_type, deliver_note, workspace_dir, is_reference, ref_builtin, ' + 'ref_source_id, auto_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,1,0,?,?,?,?)', + (new_name, (proj.get('description') or '') + '\n(由项目「' + proj['name'] + '」一键保存为参考)', + proj.get('objective') or '', proj.get('acceptance_criteria') or '', 'active', + proj.get('budget_limit') or 0, proj.get('deliver_type') or 'web', proj.get('deliver_note') or '', + '', pid, 'none', ts, ts)) + db.w('UPDATE projects SET workspace_dir=? WHERE id=?', (f'ref_{rpid}', rpid)) + # 复制任务(depends_on 重新映射) + n = _copy_tasks_from_reference(proj, rpid) + db.w("INSERT INTO project_logs (project_id, level, message, created_at) VALUES (?,?,?,?)", + (rpid, 'info', f'由项目「{proj["name"]}」一键保存为参考项目,复制 {n} 个任务', ts)) + enterprise.audit(enterprise.current_actor(), 'project.save_reference', f'project#{pid}', + f'「{proj["name"]}」保存为参考项目 #{rpid}({n} 任务)', request.remote_addr or '') + return jsonify({'ok': True, 'id': rpid, 'copied_tasks': n}) + + +@app.route('/api/reference_projects/', methods=['DELETE']) +@require_auth +def reference_project_delete(rid): + """删除参考项目(内置参考项目受保护)""" + err = _check_perm_point('project.manage') + if err: + return err + r = db.q('SELECT * FROM projects WHERE id=? AND is_reference=1', (rid,), one=True) + if not r: + return jsonify({'ok': False, 'error': '参考项目不存在'}), 404 + if r.get('ref_builtin'): + return jsonify({'ok': False, 'error': '内置参考项目受保护,不可删除'}), 400 + db.w('DELETE FROM task_logs WHERE task_id IN (SELECT id FROM tasks WHERE project_id=?)', (rid,)) + db.w('DELETE FROM tasks WHERE project_id=?', (rid,)) + db.w('DELETE FROM projects WHERE id=?', (rid,)) + return jsonify({'ok': True}) + + @app.route('/api/workspace/probe') @require_auth def workspace_probe(): @@ -1573,7 +1631,26 @@ def report_cost(): scope_sql = 'WHERE project_id IN (%s)' % ','.join('?' * len(visible)) scope_args = list(visible) group = request.args.get('group', 'project') - if group == 'worker': + if group == 'date': + # 按日期统计(V3.5):date(created_at) 以本地时区分天;days 限制最近 N 天 + days = 0 + try: + days = int(request.args.get('days') or 0) + except Exception: + days = 0 + extra_where = '' + extra_args = [] + if days > 0: + extra_where = (scope_sql + ' AND' if scope_sql else 'WHERE') + ' created_at>=?' + extra_args = [db.now() - days * 86400] + rows = db.q( + f'SELECT date(created_at, "unixepoch", "localtime") day, COUNT(*) runs, ' + f'COUNT(DISTINCT task_id) task_calls, ' + f'SUM(prompt_tokens) prompt_tokens, SUM(completion_tokens) completion_tokens, ' + f'SUM(cached_tokens) cached_tokens, SUM(total_tokens) tokens, SUM(cost) cost ' + f'FROM cost_records {scope_sql}{extra_where} GROUP BY day ORDER BY day DESC', + scope_args + extra_args) + elif group == 'worker': rows = db.q( 'SELECT worker_id, provider, model, COUNT(*) runs, COUNT(DISTINCT task_id) task_calls, ' 'SUM(prompt_tokens) prompt_tokens, SUM(completion_tokens) completion_tokens, ' diff --git a/db.py b/db.py index 79adef4..e6b8d3c 100644 --- a/db.py +++ b/db.py @@ -549,6 +549,10 @@ def _migrate(): pcols3 = {r['name'] for r in conn.execute('PRAGMA table_info(projects)')} if 'is_reference' not in pcols3: conn.execute('ALTER TABLE projects ADD COLUMN is_reference INTEGER DEFAULT 0') + if 'ref_builtin' not in pcols3: + conn.execute('ALTER TABLE projects ADD COLUMN ref_builtin INTEGER DEFAULT 0') + if 'ref_source_id' not in pcols3: + conn.execute('ALTER TABLE projects ADD COLUMN ref_source_id INTEGER DEFAULT 0') acols = {r['name'] for r in conn.execute('PRAGMA table_info(agent_runs)')} if 'workspace_dir' not in acols: conn.execute("ALTER TABLE agent_runs ADD COLUMN workspace_dir TEXT DEFAULT ''") diff --git a/static/app.js b/static/app.js index a8406a2..62a3603 100644 --- a/static/app.js +++ b/static/app.js @@ -192,7 +192,7 @@ setInterval(refreshAlertBadge, 30000); /* ---------- Router ---------- */ const routes = { - 'dashboard': pageDashboard, 'projects': pageProjects, 'project': pageProject, + 'dashboard': pageDashboard, 'chat': pageChat, 'projects': pageProjects, 'project': pageProject, 'workers': pageWorkers, 'reports': pageReports, 'logs': pageLogs, 'alerts': pageAlerts, 'api': pageApiTokens, 'settings': pageSettings, 'agents': pageAgents, 'eval': pageEval, 'templates': pageTemplates, @@ -201,13 +201,13 @@ const routes = { function router() { clearInterval(projAutoTimer); // 离开项目页时停止 AI 主管轮询 projAutoTimer = null; - const hash = location.hash.replace(/^#\//, '') || 'dashboard'; + const hash = location.hash.replace(/^#\//, '') || 'chat'; const parts = hash.split('/'); const name = parts[0]; // 离开项目页时丢弃项目缓存,避免下次进入用旧数据 if (name !== 'project') projCtx = null; - const fn = routes[name] || pageDashboard; - const navMap = {project:'projects', api:'api', settings:'settings', alerts:'alerts'}; + const fn = routes[name] || pageChat; + const navMap = {project:'projects', api:'api', settings:'settings', alerts:'alerts', chat:'chat'}; $$('#sidebar nav a').forEach(a => a.classList.toggle('active', a.dataset.route === (navMap[name] || name))); $('#main').innerHTML = '
加载中…
'; fn(parts.slice(1)).catch(e => { $('#main').innerHTML = `
${esc(e.message)}
`; }); @@ -231,14 +231,14 @@ function chatTargetLabel() { return (w?.name || 'AI Worker') + (c.tid === c.options?.main_worker_id ? ' ⭐主力' : ''); } -function chatPanelHtml() { +function chatPanelHtml(tall = false) { const c = chatCtx; const eps = (c.options?.endpoints || []).map(e => ``).join('') || ''; const ws = (c.options?.workers || []).map(w => ``).join('') || ''; const ts = (c.options?.teams || []).map(t => ``).join('') || ''; const sessOpts = (c.sessions || []).map(s => ``).join(''); return ` -
+
💬 对话 可选 大模型 / AI Worker / 团队 · 输出按 token 流式 · 默认主力 AI Worker
@@ -409,7 +409,11 @@ async function chatSend() { const msgWrap = document.createElement('div'); msgWrap.className = 'chat-msg ai'; bubble.className = 'chat-bubble ai'; - bubble.textContent = ''; + const txtSpan = document.createElement('span'); + const cursorSpan = document.createElement('span'); + cursorSpan.className = 'chat-cursor'; + bubble.appendChild(txtSpan); + bubble.appendChild(cursorSpan); msgWrap.appendChild(bubble); const usage = document.createElement('div'); usage.className = 'chat-usage'; @@ -451,7 +455,7 @@ async function chatSend() { try { evt = JSON.parse(line.slice(6)); } catch (e) { continue; } if (evt.type === 'delta') { acc += evt.content; - bubble.textContent = acc; + txtSpan.textContent = acc; body.scrollTop = body.scrollHeight; } else if (evt.type === 'done') { done_usage = evt.usage; @@ -460,6 +464,7 @@ async function chatSend() { } } } + cursorSpan.remove(); if (done_usage) { usage.textContent = `⚡ ${done_usage.total_tokens} tokens · 输入 ${done_usage.prompt_tokens} / 输出 ${done_usage.completion_tokens}${done_usage.cached_tokens ? ' / 缓存命中 ' + done_usage.cached_tokens : ''} · ${fmtMoney(done_usage.cost)} · 首字 ${done_usage.first_token_ms || '—'}ms · 总 ${done_usage.elapsed_ms}ms · ${esc(done_usage.model)}`; } @@ -467,22 +472,22 @@ async function chatSend() { const d = (await api(`/api/chat/sessions/${chatCtx.sid}`)).data; chatCtx.msgs = d.messages; } catch (e) { - usage.textContent = '❌ ' + e.message; + cursorSpan.remove(); + if (txtSpan.textContent) txtSpan.textContent += '\n\n❌ 输出中断:' + e.message; + else usage.textContent = '❌ ' + e.message; } finally { chatCtx.busy = false; if (sendBtn) sendBtn.disabled = false; } } -/* ---------- 仪表盘(含对话) ---------- */ +/* ---------- 仪表盘 ---------- */ async function pageDashboard() { - await chatInit(); const s = (await api('/api/stats')).data; const total = Object.values(s.by_status).reduce((a, b) => a + b, 0); const pct = n => total ? Math.round(n / total * 100) : 0; $('#main').innerHTML = ` - ${chatPanelHtml()} -

运营总览项目 · 任务 · AI Worker · 用量

+

仪表盘项目 · 任务 · AI Worker · 用量

项目
${s.projects}
进行中 ${s.by_status.done ?? 0} 个已完成任务
任务总数
${s.tasks}
完成率 ${pct(s.by_status.done ?? 0)}% · 待审核 ${s.by_status.review ?? 0}
@@ -522,6 +527,28 @@ async function pageDashboard() {
`; } +/* ---------- 💬 首页(对话):上=仪表盘摘要(可点击跳转) · 中下=对话主体 ---------- */ +async function pageChat() { + await chatInit(); + let s = {projects:0, tasks:0, workers:0, total_cost:0, total_tokens:0, total_calls:0, total_cached:0, + by_status:{}, recent:[]}; + try { s = (await api('/api/stats')).data; } catch (e) {} + const total = Object.values(s.by_status || {}).reduce((a, b) => a + b, 0); + const pct = n => total ? Math.round(n / total * 100) : 0; + const stat = (lbl, num, sub) => ``; + $('#main').innerHTML = ` +

首页 · 对话上=运营摘要(点击跳转仪表盘) · 中下=对话界面

+
+ ${stat('项目', s.projects, `完成 ${s.by_status.done ?? 0} 个任务`)} + ${stat('任务总数', s.tasks, `完成率 ${pct(s.by_status.done ?? 0)}% · 待审核 ${s.by_status.review ?? 0}`)} + ${stat('AI Worker', s.workers, '虚拟员工档案数')} + ${stat('累计成本', fmtMoney(s.total_cost), `${(s.total_tokens/1e6).toFixed(2)}M tokens`)} + ${stat('调用次数', s.total_calls ?? 0, `缓存命中 ${(s.total_cached ?? 0)/1e6 >= 0.001 ? (s.total_cached/1e6).toFixed(2)+'M' : (s.total_cached ?? 0)} tokens`)} +
+ ${chatPanelHtml(true)}`; +} + /* ---------- 项目列表 ---------- */ let projFilter = 'all'; const PROJ_STATUS = {planning:'规划中', active:'进行中', review:'待验收', done:'已完成', archived:'已归档'}; @@ -555,6 +582,7 @@ async function pageProjects() { ${Object.entries(PROJ_STATUS).map(([k, v]) => ``).join('')} `} ${p.perm !== 'view' ? `` : ''} +
${esc(p.objective || p.description || '暂无描述')}
@@ -586,6 +614,16 @@ async function delProject(pid) { } catch (e) { toast(e.message, 'err'); } } +/* 一键保存为参考项目(V3.5) */ +async function saveAsRef(pid) { + const name = projNameMap[pid] || ('#' + pid); + if (!confirm(`把项目「${name}」一键保存为参考项目?\n(将复制项目与任务到「从参考项目中新建」列表,原项目不受影响)`)) return; + try { + const r = await api(`/api/projects/${pid}/save_reference`, {method: 'POST'}); + toast(`✅ 已保存为参考项目(复制 ${r.copied_tasks} 个任务)`, 'ok'); + } catch (e) { toast(e.message, 'err'); } +} + function openProjectModal(p = {}) { const usersP = api('/api/users'); const workersP = api('/api/workers'); @@ -741,17 +779,31 @@ async function openRefProjectModal() { if (!refs.length) { toast('暂无参考项目', 'err'); return; } openModal(`

📋 从参考项目中新建

-
选择下面的内置简单测试项目作为参考,将复制其目标与任务列表,快速生成一个新项目(仍可继续编辑)。
+
内置参考项目 + 从已创建项目一键保存的参考项目。选择后复制其目标与任务列表生成新项目(仍可继续编辑)。
${refs.map(x => ` -
-
${esc(x.name)} ${x.tasks.length} 个任务
-
🎯 ${esc(x.objective)}
-
✅ 验收标准:${esc(x.acceptance_criteria)}
-
点击 → 以此参考新建项目
-
`).join('')} +
+
${esc(x.name)} ${x.tasks.length} 个任务 + ${x.ref_builtin ? '内置' : ''} + ${x.source_project ? `来自「${esc(x.source_project.name)}」` : ''} + ${!x.ref_builtin ? `` : ''} +
+
🎯 ${esc(x.objective)}
+
✅ 验收标准:${esc(x.acceptance_criteria)}
+
点击 → 以此参考新建项目
+
`).join('') || '
暂无参考项目
'} `); } +async function delRefProject(rid) { + if (!confirm('删除该参考项目?(不影响原项目)')) return; + try { + await api(`/api/reference_projects/${rid}`, {method: 'DELETE'}); + toast('已删除', 'ok'); + closeModal(); + openRefProjectModal(); + } catch (e) { toast(e.message, 'err'); } +} + async function openProjectModalFromRef(refId) { const d = (await api('/api/reference_projects')).data; const ref = d.find(x => x.id === refId); @@ -2053,9 +2105,9 @@ async function pageWorkers(args = []) { const canManage = CURRENT_ROLE === 'admin'; const tabBar = ` `; $('#main').innerHTML = `

AI Worker模型接口 + 角色提示词 + 工具权限 + 成本上限 = 虚拟员工档案

${tabBar}
`; if (workersTab === 'endpoints') renderEndpoints(eps.data, canManage); @@ -2387,28 +2439,38 @@ async function delTeam(tid) { /* ---------- 成本报表 / 用量明细 ---------- */ async function pageReports() { - const group = location.hash.includes('group=') ? location.hash.split('group=')[1] : 'project'; + const hashPart = location.hash.includes('group=') ? location.hash.split('group=')[1] : 'project'; + const group = hashPart.split('&')[0]; + const days = (hashPart.match(/days=(\d+)/) || [])[1] || 0; if (group === 'usage') return pageUsageReport(); - const r = (await api('/api/reports/cost?group=' + group)).data; + const r = (await api(`/api/reports/cost?group=${group}${days ? '&days=' + days : ''}`)).data; const s = (await api('/api/stats')).data; - const head = {project:['项目','调用次数','输入tokens','输出tokens','缓存命中','总Tokens','成本'], worker:['Worker','模型','调用次数','输入tokens','输出tokens','缓存命中','总Tokens','成本'], model:['供应商/模型','调用次数','输入tokens','输出tokens','缓存命中','总Tokens','成本']}[group]; + const head = {project:['项目','调用次数','输入tokens','输出tokens','缓存命中','总Tokens','成本'], worker:['Worker','模型','调用次数','输入tokens','输出tokens','缓存命中','总Tokens','成本'], model:['供应商/模型','调用次数','输入tokens','输出tokens','缓存命中','总Tokens','成本'], date:['日期','调用次数','输入tokens','输出tokens','缓存命中','总Tokens','成本']}[group]; const fmtT = n => (n / 1e6).toFixed(3) + 'M'; + const daySel = group === 'date' ? ` + 7天 + 30天 + 全部` : ''; $('#main').innerHTML = ` -

成本报表按项目 × 任务 × Worker × 模型 多维核算(含输入/输出/缓存命中 token 与调用次数)

+

成本报表按项目 × 任务 × Worker × 模型 × 日期 多维核算(含输入/输出/缓存命中 token 与调用次数)

按项目 按 Worker 按模型 + 📅 按日期 📊 用量明细(项目×智能体) + ${daySel} 累计成本 ${fmtMoney(s.total_cost)} · ${(s.total_tokens/1e6).toFixed(2)}M tokens · ${s.total_calls ?? 0} 次调用 · 缓存命中 ${(s.total_cached ?? 0)/1e6 >= 0.001 ? (s.total_cached/1e6).toFixed(2)+'M' : (s.total_cached ?? 0)} tokens
${head.map(h => ``).join('')} ${r.map(x => { const cost = x.cost || 0, tokens = x.tokens || 0; const maxCost = Math.max(...r.map(v => v.cost || 0), 0.001); - return `${group === 'project' ? `` : group === 'worker' ? - `` : - ``} + const nameCell = group === 'project' ? `` : group === 'worker' ? + `` : group === 'date' ? + `` : + ``; + return `${nameCell} diff --git a/static/index.html b/static/index.html index 640d032..378bf3d 100644 --- a/static/index.html +++ b/static/index.html @@ -11,7 +11,8 @@
${h}
${esc(x.project_name || '—')}${esc(x.worker_name || '—')}${esc(x.provider || '')} ${esc(x.model || '')}${esc(x.provider || '')} ${esc(x.model || '')}${esc(x.project_name || '—')}${esc(x.worker_name || '—')}${esc(x.provider || '')} ${esc(x.model || '')}${esc(x.day || '—')}${esc(x.provider || '')} ${esc(x.model || '')}
${x.runs} ${fmtT(x.prompt_tokens || 0)} ${fmtT(x.completion_tokens || 0)}