aims-workstation 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +16 -0
- package/README.md +49 -2
- package/bin/aims-workstation.js +4 -0
- package/package.json +31 -3
- package/scripts/check-panel.mjs +45 -0
- package/scripts/publish-nexus.sh +15 -0
- package/seed/panels/system-action-group.json +9 -0
- package/seed/panels/system-alarm-stream.json +5 -0
- package/seed/panels/system-device-grid.json +5 -0
- package/seed/panels/system-inspection-report.json +5 -0
- package/seed/panels/system-kpi-strip.json +5 -0
- package/server.js +2066 -0
- package/skills/workbench-dev/SKILL.md +63 -0
- package/skills/workbench-dev/references/panel-contract.md +73 -0
- package/skills/workbench-dev/scripts/check-panel.mjs +45 -0
- package/web/README.md +32 -0
- package/web/dist/assets/index-BDk4jeII.js +58 -0
- package/web/dist/assets/index-CfdLHVuy.css +2 -0
- package/web/dist/favicon.svg +1 -0
- package/web/dist/icons.svg +24 -0
- package/web/dist/index.html +14 -0
package/.env.example
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# AIMS 工业智能体平台 MVP 配置模板(复制为 .env 并填写)
|
|
2
|
+
PORT=5600
|
|
3
|
+
|
|
4
|
+
# 平台 LLM(OpenAI 兼容端点;也可在「系统设置」页运行时切换)
|
|
5
|
+
LLM_BASE_URL=https://open.bigmodel.cn/api/coding/paas/v4
|
|
6
|
+
LLM_API_KEY=<your-llm-api-key>
|
|
7
|
+
LLM_MODEL=glm-5.3
|
|
8
|
+
|
|
9
|
+
# ARM 系统(人事系统:助手档案 + 经验库)
|
|
10
|
+
ARM_MCP_URL=https://your-arm-host/api/mcp
|
|
11
|
+
# PAT 留空则自动读取 ~/.arm/config.json 的 token(仅经验写入需要)
|
|
12
|
+
ARM_PAT=
|
|
13
|
+
|
|
14
|
+
# ARM REST API 默认值(v2:助手检索/下载 + 知识混合检索;运行时可在「系统设置」页切换源)
|
|
15
|
+
ARM_API_BASE=http://localhost:3001
|
|
16
|
+
ARM_API_TOKEN=<arm_pat_xxx-or-leave-empty>
|
package/README.md
CHANGED
|
@@ -1,3 +1,50 @@
|
|
|
1
|
-
#
|
|
1
|
+
# aims-workstation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> AIMS 工作站:本地智能体工作站——把「智能体队伍」装到你自己的电脑上。
|
|
4
|
+
> 配套资源系统 [ARM](可私有化部署)提供助手档案与知识库;工作站负责本地运行:工作台、工作区、工具与审计。
|
|
5
|
+
|
|
6
|
+
## 它能做什么
|
|
7
|
+
|
|
8
|
+
- **助手本地化**:从 ARM 下载助手档案(含绑定知识全文)物化到本地,**离线可用**;版本升级、回滚、卸载
|
|
9
|
+
- **工作台**:每个助手一个专属操作面;区块自由布局,**对话式开发面板插件**(描述需求 → 二开助手生成 → 草稿审阅 → 发布)
|
|
10
|
+
- **工作区**:助手 1:N 工作区(文件现场隔离),`exec` 锚定工作区目录,数据集按工作区分库
|
|
11
|
+
- **分级自主**:L0 只读 → L2 确认执行(写操作挂起人工确认)→ L3 自主,危险命令永久拒绝;全程审计
|
|
12
|
+
- **并行会话**:多助手并行、后台运行进度跟踪、确认卡跨切换恢复
|
|
13
|
+
- **系统设置**:ARM 源 / LLM(任意 OpenAI 兼容端点)/ 全局场景画像,界面配置即时生效
|
|
14
|
+
|
|
15
|
+
## 快速开始
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx aims-workstation
|
|
19
|
+
# 打开 http://127.0.0.1:5600 → 系统设置里配置 ARM 源与 LLM 即可
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
要求 **Node ≥ 22.5**(`node:sqlite` 依赖)。服务默认只绑定 `127.0.0.1`(本机工具,勿暴露局域网)。
|
|
23
|
+
|
|
24
|
+
## 配置
|
|
25
|
+
|
|
26
|
+
无需配置文件——首启后在「系统设置」页配置即可。也可用 `AIMS_HOME/.env`(模板见 `.env.example`):
|
|
27
|
+
|
|
28
|
+
| 项 | 说明 | 优先级 |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| ARM 源(站点/Token) | 助手档案与知识库的来源,支持私有化部署 | 设置页 > 环境变量 > .env |
|
|
31
|
+
| LLM | OpenAI 兼容端点 + 模型 + Key | 同上 |
|
|
32
|
+
| 场景画像 | 全局一份的使用方环境事实,注入所有助手 | 设置页 |
|
|
33
|
+
|
|
34
|
+
全部运行时状态位于 `~/.aims-workstation/`(`AIMS_HOME` 可覆盖);包目录只含只读资产。
|
|
35
|
+
|
|
36
|
+
## 安全模型
|
|
37
|
+
|
|
38
|
+
- 助手凭据与提示词分离;密钥只存本机(界面永远掩码)
|
|
39
|
+
- 写操作 L2 挂起确认;毁灭性命令黑名单全级别拒绝
|
|
40
|
+
- 面板代理出网白名单(仅插件声明过的主机);数据集按助手×工作区物理隔离
|
|
41
|
+
|
|
42
|
+
## 开发
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone <repo> && cd aims-workstation
|
|
46
|
+
npm install && npm run dev # 前端: cd web && pnpm install && pnpm build
|
|
47
|
+
npm run test:guard # 守护栏单元测试
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
内部开发记录见 [DEVELOPMENT.md](DEVELOPMENT.md)(不入 npm 包)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aims-workstation",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"private": false,
|
|
4
|
+
"type": "commonjs",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "node server.js",
|
|
7
|
+
"build:web": "cd web && pnpm build",
|
|
8
|
+
"start": "node server.js",
|
|
9
|
+
"prepack": "npm run build:web",
|
|
10
|
+
"test:guard": "node server.js --test-guard"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@earendil-works/pi-ai": "^1.2.0"
|
|
14
|
+
},
|
|
15
|
+
"packageManager": "pnpm@11.21.0+sha512.521705bce689924eac72f5a3587122f362689ef6571e55ba80076fd637c11132ecffada26fad4ea79c485bfddbfd3d5a2a5b05805a77e893de71ec8a6cca3bb1",
|
|
16
|
+
"version": "0.1.1",
|
|
17
|
+
"description": "AIMS 工作站:本地智能体工作站——ARM 助手本地化(下载/离线/升级)、工作台对话式定制、多工作区隔离、分级自主与审计",
|
|
18
|
+
"bin": {
|
|
19
|
+
"aims-workstation": "bin/aims-workstation.js"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22.5.0"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"server.js",
|
|
26
|
+
"bin/",
|
|
27
|
+
"web/dist/",
|
|
28
|
+
"skills/",
|
|
29
|
+
"scripts/",
|
|
30
|
+
"seed/",
|
|
31
|
+
".env.example",
|
|
32
|
+
"README.md"
|
|
33
|
+
]
|
|
6
34
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 面板插件校验器(借鉴 a2ui checker 模式:产出前的测试套件)
|
|
3
|
+
// 用法: node check-panel.mjs < panel.js 或 node check-panel.mjs panel.js
|
|
4
|
+
// 退出码 0=通过(可有 warning),1=拒绝(error)
|
|
5
|
+
'use strict';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
|
|
8
|
+
const code = process.argv[2] ? readFileSync(process.argv[2], 'utf8') : readFileSync(0, 'utf8');
|
|
9
|
+
const errors = [], warnings = [];
|
|
10
|
+
|
|
11
|
+
// E1 契约:必须定义 Panel 组件
|
|
12
|
+
if (!/function\s+Panel\s*\(|const\s+Panel\s*=/.test(code)) errors.push('E1 契约缺失:必须定义 function Panel(props)');
|
|
13
|
+
// E2 沙箱:禁止模块系统与全局逃逸
|
|
14
|
+
if (/\b(import\s|require\()/.test(code)) errors.push('E2 沙箱违规:禁止 import/require(运行时无模块系统)');
|
|
15
|
+
if (/\b(window|document|globalThis)\b/.test(code)) errors.push('E2 沙箱违规:禁止 window/document/globalThis');
|
|
16
|
+
// E3 模板完整性:反引号必须成对
|
|
17
|
+
const ticks = (code.match(/`/g) || []).length;
|
|
18
|
+
if (ticks % 2 !== 0) errors.push(`E3 模板断裂:反引号数量为奇数(${ticks}),html\`\` 模板未闭合`);
|
|
19
|
+
// E4 编译检查:与运行时一致的 new Function 编译
|
|
20
|
+
try {
|
|
21
|
+
const factory = new Function('React', 'html', 'useState', 'useEffect', 'useRef', 'useMemo',
|
|
22
|
+
`"use strict";\n${code}\n;if (typeof Panel !== 'function') throw new Error('no Panel');\nreturn Panel;`);
|
|
23
|
+
if (typeof factory !== 'function') errors.push('E4 编译失败');
|
|
24
|
+
} catch (e) { errors.push(`E4 编译失败:${e.message}`); }
|
|
25
|
+
|
|
26
|
+
// W 类:质量提醒(不阻断)
|
|
27
|
+
const usesStoreShorthand = /(^|[^.\w])store\./.test(code); // 裸 store. 调用
|
|
28
|
+
const destructured = /function Panel\s*\(\s*\{[^}]*\bstore\b/.test(code); // 参数解构了 store
|
|
29
|
+
const usesPropsStore = /props\.store/.test(code);
|
|
30
|
+
if (usesStoreShorthand && !destructured && !usesPropsStore) {
|
|
31
|
+
warnings.push('W1 使用了 store 但 Panel 参数未解构 store(也未用 props.store),运行时会取到 undefined');
|
|
32
|
+
}
|
|
33
|
+
if (/onInput/.test(code) && /onChange/.test(code)) warnings.push('W2 同时使用 onInput 与 onChange,行为可能不一致,选其一');
|
|
34
|
+
if (!/key=/.test(code) && /\.map\(/.test(code)) warnings.push('W3 列表渲染(.map)缺少 key 属性');
|
|
35
|
+
if (/await /.test(code) && !/try\s*{|\.catch\(/.test(code)) warnings.push('W4 存在 await 但无 try/catch 或 .catch,失败会静默');
|
|
36
|
+
if (!/(loading|加载|…|\.\.\.)|useEffect/.test(code) && /store\.(list|create)/.test(code)) warnings.push('W5 异步读写无加载态提示,首帧可能空白');
|
|
37
|
+
|
|
38
|
+
const verdict = errors.length ? 'REJECT' : 'PASS';
|
|
39
|
+
const lines = [
|
|
40
|
+
`[check-panel] ${verdict}${warnings.length ? `(${warnings.length} 个警告)` : ''}`,
|
|
41
|
+
...errors.map((e) => ` ❌ ${e}`),
|
|
42
|
+
...warnings.map((w) => ` ⚠️ ${w}`),
|
|
43
|
+
];
|
|
44
|
+
console.log(lines.join('\n'));
|
|
45
|
+
process.exit(errors.length ? 1 : 0);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 发布到公司 Nexus npm 私仓(凭据经环境变量传入,绝不落盘入库)
|
|
3
|
+
# 用法: NPM_USER=xxx NPM_PASS=xxx ./scripts/publish-nexus.sh
|
|
4
|
+
# 前置: 账号需有 npm-hosted 的部署权限(nx-repository-view-npm-npm-hosted-add/edit)
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
REGISTRY="${NPM_REGISTRY:?需要 NPM_REGISTRY(如 https://nexus.example.com/repository/npm-hosted/)}"
|
|
7
|
+
: "${NPM_USER:?需要 NPM_USER}"; : "${NPM_PASS:?需要 NPM_PASS}"
|
|
8
|
+
KEY="$(echo "$REGISTRY" | sed 's|https\?://||; s|/$|/:|; s|^|//|')"
|
|
9
|
+
AUTH="$(printf '%s:%s' "$NPM_USER" "$NPM_PASS" | base64)"
|
|
10
|
+
CFG="$(mktemp)"; trap 'rm -f "$CFG"' EXIT
|
|
11
|
+
printf '%s_auth=%s\n' "$KEY" "$AUTH" > "$CFG"
|
|
12
|
+
echo "→ 目标: $REGISTRY ($(NPM_CONFIG_USERCONFIG="$CFG" npm whoami --registry="$REGISTRY" 2>/dev/null || echo '认证待验证'))"
|
|
13
|
+
NPM_CONFIG_USERCONFIG="$CFG" npm publish --registry="$REGISTRY"
|
|
14
|
+
echo "✅ 已发布。安装侧(npm-group 聚合 hosted+代理,依赖可一并解析):"
|
|
15
|
+
echo " npx --registry=$REGISTRY aims-workstation"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "system-action-group",
|
|
3
|
+
"name": "操作区(范本)",
|
|
4
|
+
"version": "v1",
|
|
5
|
+
"author": "system",
|
|
6
|
+
"description": "(已退役)旧版操作区按钮组,走 /api/action 直连 mock 编排——四件套移除后停用;按钮触发命令请用 params.actions + props.runAction(随草稿人审)",
|
|
7
|
+
"status": "deprecated",
|
|
8
|
+
"code": "function Panel({ params, selected }) {\n const actions = (params && params.actions) || [];\n const [confirming, setConfirming] = useState(null);\n const [result, setResult] = useState(null);\n const [busy, setBusy] = useState(false);\n const run = async (a) => {\n setBusy(true); setResult(null);\n try {\n const r = await fetch('/api/action', { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ agentId: params.agentId, actionId: a.id, device: selected }) });\n const j = await r.json();\n setResult(j.result || j.error || ('HTTP ' + r.status));\n } catch (e) { setResult('执行失败:' + e.message); }\n setBusy(false); setConfirming(null);\n };\n return html`<div className=\"space-y-2 p-2\">\n ${actions.map(a => html`\n <div key=${a.id}>\n ${confirming && confirming.id === a.id ? html`\n <div className=\"rounded-md border border-amber-500/50 bg-amber-500/10 p-2\">\n <div className=\"mb-1.5 text-[11px] text-amber-300\">⚠ ${(a.confirm && a.confirm.text || '确认执行?').replace('{device}', selected || '所选设备')}</div>\n ${a.confirm && a.confirm.double_check && html`<div className=\"mb-1.5 text-[10px] text-amber-300/80\">${a.confirm.double_check}</div>`}\n ${result ? html`<pre className=\"max-h-40 overflow-auto whitespace-pre-wrap text-[10px] leading-relaxed text-muted-foreground\">${result}</pre>`\n : html`<div className=\"flex gap-1.5\">\n <button disabled=${busy} onClick=${() => run(a)} className=\"rounded bg-primary px-2 py-1 text-[11px] text-primary-foreground disabled:opacity-50\">${busy ? '执行中…' : '确认执行'}</button>\n <button onClick=${() => { setConfirming(null); setResult(null); }} className=\"rounded border px-2 py-1 text-[11px]\">取消</button>\n </div>`}\n ${result && html`<button onClick=${() => { setConfirming(null); setResult(null); }} className=\"mt-1.5 rounded border px-2 py-0.5 text-[10px]\">关闭</button>`}\n </div>`\n : html`\n <button onClick=${() => { setConfirming(a); setResult(null); }} disabled=${result !== null && confirming === null}\n className=\"w-full rounded-md border bg-card px-2.5 py-2 text-left transition-colors hover:border-primary/60\">\n <div className=\"text-xs font-semibold\">▶ ${a.label}</div>\n <div className=\"mt-0.5 flex items-center gap-1 text-[10px] text-muted-foreground\">\n <span className=\"rounded border border-amber-500/40 px-1 text-amber-400\">${a.autonomy || 'L2'}·需确认</span>\n <span className=\"truncate font-mono\">${a.orchestration}</span>\n </div>\n </button>`}\n </div>`)}\n ${actions.length === 0 && html`<div className=\"py-3 text-center text-[11px] text-muted-foreground\">无操作(params.actions 配置)</div>`}\n </div>`;\n}"
|
|
9
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "system-alarm-stream", "name": "实时告警流(范本)", "version": "v1", "author": "system",
|
|
3
|
+
"description": "系统范本:告警流列表。参数 params.device_type 可过滤设备类型(如 服务器)。二开起点:改配色/加统计/联动。",
|
|
4
|
+
"status": "active", "code": "function Panel({ alarms, params }) {\n const t = params && params.device_type;\n const list = (alarms || []).filter(a => a.state !== 'active' ? true : !t || (a.device.indexOf('SRV') === 0) === (t === '服务器'));\n const active = list.filter(a => a.state === 'active');\n return html`<div className=\"p-2 text-xs\">\n <div className=\"mb-2 flex items-center justify-between\">\n <span className=\"font-semibold\">告警</span>\n <span className=\"text-[10px] text-muted-foreground\">活动 ${active.length} / 全部 ${list.length}</span>\n </div>\n ${list.length === 0 && html`<div className=\"py-3 text-center text-[11px] text-muted-foreground\">无告警</div>`}\n ${list.map(a => html`\n <div key=${a.id} className=${\"mb-1.5 rounded-md border-l-2 bg-card px-2 py-1.5 \" + (a.state !== 'active' ? \"border-emerald-500 opacity-50\" : a.level === '高' ? \"border-red-500\" : \"border-amber-500\")}>\n <div className=\"flex items-center gap-1.5\">\n <span className=${\"h-1.5 w-1.5 rounded-full \" + (a.state === 'active' ? (a.level === '高' ? \"bg-red-500\" : \"bg-amber-500\") : \"bg-emerald-500\")}></span>\n <span className=\"rounded bg-muted px-1 text-[10px]\">${a.level}</span>\n <span className=\"font-mono\">${a.device}</span>\n </div>\n <div className=\"mt-0.5 leading-relaxed\">${a.text}</div>\n <div className=\"mt-0.5 text-[10px] text-muted-foreground\">${a.id} · ${a.ts}${a.resolved_at ? ' · 已处置 ' + a.resolved_at : ''}</div>\n </div>`)}\n </div>`;\n}"
|
|
5
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "system-device-grid", "name": "设备状态矩阵(范本)", "version": "v1", "author": "system",
|
|
3
|
+
"description": "系统范本:设备卡片网格,点击选中(联动对话上下文)。参数 params.device_type 过滤。二开起点:加详情弹层/状态聚合。",
|
|
4
|
+
"status": "active", "code": "function Panel({ devices, selected, onSelect, params }) {\n const t = params && params.device_type;\n const list = t ? (devices || []).filter(d => d.type === t) : (devices || []);\n const meta = { normal: ['正常', 'bg-emerald-500/15 text-emerald-400'], warning: ['预警', 'bg-amber-500/15 text-amber-400'], alarm: ['告警', 'bg-red-500/15 text-red-400'] };\n return html`<div className=\"grid grid-cols-2 gap-2 p-2 md:grid-cols-3 xl:grid-cols-5\">\n ${list.map(d => { const m = meta[d.status] || meta.normal; return html`\n <button key=${d.id} onClick=${() => onSelect && onSelect(d.id)}\n className=${\"rounded-lg border bg-card p-2 text-left transition-colors hover:border-primary/60 \" + (selected === d.id ? \"border-primary ring-1 ring-primary/50\" : \"\")}>\n <div className=\"flex items-center justify-between\">\n <span className=\"font-mono text-xs font-semibold\">${d.id}</span>\n <span className=${\"rounded px-1.5 py-px text-[10px] \" + m[1]}>${m[0]}</span>\n </div>\n <div className=\"mt-0.5 text-[11px] text-muted-foreground\">${d.name}</div>\n <div className=${\"mt-1 line-clamp-2 h-7 text-[10px] leading-tight \" + (d.detail ? \"text-amber-400/90\" : \"text-transparent\")}>${d.detail || '.'}</div>\n </button>`; })}\n </div>`;\n}"
|
|
5
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "system-inspection-report", "name": "自检报告(范本·已停用)", "version": "v1", "author": "system",
|
|
3
|
+
"description": "系统范本:展示最近一次环境自检报告(props.inspections)。已停用:场景专用,二开后用于类似\"报告展示\"场景。",
|
|
4
|
+
"status": "deprecated", "code": "function Panel({ inspections, agentId }) {\n const r = (inspections || {})[agentId];\n if (!r) return html`<div className=\"p-3 text-center text-[11px] text-muted-foreground\">尚无自检报告<br/>(运行自检编排后生成)</div>`;\n const meta = { normal: ['正常', 'bg-emerald-500/15 text-emerald-400'], warning: ['预警', 'bg-amber-500/15 text-amber-400'], alarm: ['告警', 'bg-red-500/15 text-red-400'] };\n return html`<div className=\"p-2 text-xs\">\n <div className=\"mb-1.5 text-[10px] text-muted-foreground\">${r.ts} · 范围 ${r.scope} · ${r.items.length} 台</div>\n ${r.items.map(i => { const m = meta[i.status] || meta.normal; return html`\n <div key=${i.device} className=\"mb-1 flex items-center gap-2 rounded-md bg-card px-2 py-1\">\n <span className=${\"rounded px-1.5 py-px text-[10px] \" + m[1]}>${m[0]}</span>\n <span className=\"font-mono\">${i.device}</span>\n <span className=\"truncate text-[10px] text-muted-foreground\">${i.detail}</span>\n </div>`; })}\n <div className=${\"mt-1 rounded px-2 py-1.5 \" + (r.active_alarms ? \"bg-amber-500/10 text-amber-300\" : \"bg-emerald-500/10 text-emerald-400\")}>${r.conclusion}</div>\n </div>`;\n}"
|
|
5
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "system-kpi-strip", "name": "指标条(范本·已停用)", "version": "v1", "author": "system",
|
|
3
|
+
"description": "系统范本:KPI 卡片行(读 props.kpi)。已停用:示例仅绑定模拟指标,实际业务请二开绑定真实数据源。",
|
|
4
|
+
"status": "deprecated", "code": "function Panel({ kpi }) {\n return html`<div className=\"grid grid-cols-2 gap-2 p-2 lg:grid-cols-4\">\n ${Object.entries(kpi || {}).map(([k, v]) => html`\n <div key=${k} className=\"rounded-lg border bg-card p-2.5\">\n <div className=\"text-[11px] text-muted-foreground\">${k}</div>\n <div className=\"mt-0.5 truncate text-sm font-semibold\">${v}</div>\n </div>`)}\n </div>`;\n}"
|
|
5
|
+
}
|