thumbgate 1.27.16 โ 1.27.18
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/.claude-plugin/marketplace.json +85 -0
- package/.claude-plugin/plugin.json +3 -4
- package/.well-known/llms.txt +12 -33
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +30 -215
- package/adapters/chatgpt/openapi.yaml +1695 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/gemini/function-declarations.json +0 -1
- package/adapters/mcp/server-stdio.js +11 -263
- package/adapters/opencode/opencode.json +1 -1
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +122 -1421
- package/bin/postinstall.js +27 -11
- package/config/gate-templates.json +0 -216
- package/config/gates/claim-verification.json +0 -12
- package/config/gates/default.json +2 -31
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +13 -23
- package/config/merge-quality-checks.json +1 -0
- package/config/model-candidates.json +6 -121
- package/openapi/openapi.yaml +0 -12
- package/package.json +98 -228
- package/public/blog.html +4 -4
- package/public/codex-plugin.html +20 -72
- package/public/compare.html +8 -31
- package/public/dashboard.html +166 -930
- package/public/federal.html +2 -2
- package/public/guide.html +9 -33
- package/public/index.html +116 -495
- package/public/learn.html +18 -191
- package/public/lessons.html +10 -168
- package/public/numbers.html +7 -7
- package/public/pro.html +11 -34
- package/scripts/agent-memory-lifecycle.js +0 -211
- package/scripts/agent-readiness.js +3 -167
- package/scripts/agent-reward-model.js +1 -53
- package/scripts/auto-promote-gates.js +10 -82
- package/scripts/auto-wire-hooks.js +0 -14
- package/scripts/billing.js +1 -93
- package/scripts/build-metadata.js +10 -50
- package/scripts/cli-feedback.js +6 -18
- package/scripts/cli-schema.js +0 -90
- package/scripts/cli-telemetry.js +1 -6
- package/scripts/commercial-offer.js +2 -82
- package/scripts/context-manager.js +6 -74
- package/scripts/dashboard.js +2 -68
- package/scripts/durability/step.js +3 -3
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-dpo-pairs.js +2 -7
- package/scripts/feedback-loop.js +1 -123
- package/scripts/feedback-quality.js +0 -87
- package/scripts/filesystem-search.js +10 -35
- package/scripts/gate-stats.js +0 -89
- package/scripts/gates-engine.js +85 -1105
- package/scripts/gemini-embedding-policy.js +1 -2
- package/scripts/hook-runtime.js +14 -20
- package/scripts/hook-thumbgate-cache-updater.js +2 -18
- package/scripts/hybrid-feedback-context.js +7 -142
- package/scripts/lesson-inference.js +3 -8
- package/scripts/lesson-search.js +1 -17
- package/scripts/license.js +1 -0
- package/scripts/llm-client.js +9 -390
- package/scripts/local-model-profile.js +8 -15
- package/scripts/memory-scope-readiness.js +0 -159
- package/scripts/meta-agent-loop.js +0 -36
- package/scripts/operational-integrity.js +5 -39
- package/scripts/oss-pr-opportunity-scout.js +5 -35
- package/scripts/plausible-server-events.js +6 -9
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +1 -42
- package/scripts/rate-limiter.js +13 -64
- package/scripts/secret-scanner.js +5 -44
- package/scripts/security-scanner.js +10 -260
- package/scripts/self-distill-agent.js +1 -3
- package/scripts/seo-gsd.js +2 -916
- package/scripts/statusline-cache-path.js +2 -17
- package/scripts/statusline-local-stats.js +1 -9
- package/scripts/statusline-meta.js +2 -28
- package/scripts/statusline.sh +4 -20
- package/scripts/telemetry-analytics.js +0 -357
- package/scripts/thompson-sampling.js +10 -31
- package/scripts/thumbgate-bench.js +1 -16
- package/scripts/thumbgate-search.js +19 -85
- package/scripts/tool-registry.js +1 -169
- package/scripts/vector-store.js +0 -45
- package/scripts/workflow-sentinel.js +53 -286
- package/scripts/workspace-evolver.js +2 -62
- package/src/api/server.js +290 -2578
- package/.claude/commands/dashboard.md +0 -15
- package/.claude/commands/thumbgate-blocked.md +0 -27
- package/.claude/commands/thumbgate-dashboard.md +0 -15
- package/.claude/commands/thumbgate-doctor.md +0 -30
- package/.claude/commands/thumbgate-guard.md +0 -36
- package/.claude/commands/thumbgate-protect.md +0 -30
- package/.claude/commands/thumbgate-rules.md +0 -30
- package/.well-known/agentic-verify.txt +0 -1
- package/adapters/gcp/dfcx-webhook-gate.js +0 -295
- package/adapters/letta/README.md +0 -41
- package/adapters/letta/thumbgate-letta-adapter.js +0 -133
- package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
- package/bench/observability-eval-suite.json +0 -26
- package/bin/dashboard-cli.js +0 -7
- package/commands/dashboard.md +0 -15
- package/commands/thumbgate-dashboard.md +0 -15
- package/config/builtin-lessons.json +0 -23
- package/config/gate-classifier-routing.json +0 -98
- package/config/post-deploy-marketing-pages.json +0 -61
- package/public/about.html +0 -162
- package/public/agent-manager.html +0 -179
- package/public/agents-cost-savings.html +0 -153
- package/public/ai-malpractice-prevention.html +0 -818
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
- package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
- package/public/assets/brand/thumbgate-mark-team.svg +0 -26
- package/public/assets/brand/thumbgate-mark.svg +0 -21
- package/public/assets/brand/thumbgate-wordmark.svg +0 -20
- package/public/assets/claude-thumbgate-statusbar.svg +0 -8
- package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
- package/public/assets/legal-intake-control-flow.svg +0 -66
- package/public/brand/thumbgate-mark.svg +0 -19
- package/public/brand/thumbgate-og.svg +0 -16
- package/public/chatgpt-app.html +0 -330
- package/public/codex-enterprise.html +0 -123
- package/public/js/buyer-intent.js +0 -672
- package/public/pricing.html +0 -399
- package/scripts/action-receipts.js +0 -324
- package/scripts/activation-quickstart.js +0 -187
- package/scripts/agent-operations-planner.js +0 -621
- package/scripts/ai-component-inventory.js +0 -367
- package/scripts/async-eval-observability.js +0 -236
- package/scripts/audit.js +0 -65
- package/scripts/aws-blocks-guardrails.js +0 -228
- package/scripts/classifier-routing.js +0 -130
- package/scripts/dashboard-chat.js +0 -332
- package/scripts/feedback-aggregate.js +0 -281
- package/scripts/feedback-sanitizer.js +0 -105
- package/scripts/hob-pack.js +0 -591
- package/scripts/hook-stop-anti-claim.js +0 -301
- package/scripts/install-shim.js +0 -87
- package/scripts/mcp-oauth.js +0 -293
- package/scripts/noop-detect.js +0 -285
- package/scripts/omlx-smoke.js +0 -192
- package/scripts/parallel-workflow-orchestrator.js +0 -293
- package/scripts/plan-gate.js +0 -243
- package/scripts/plausible-domain-config.js +0 -99
- package/scripts/qa-scenario-planner.js +0 -136
- package/scripts/repeat-metric.js +0 -137
- package/scripts/secret-fixture-tokens.js +0 -61
- package/scripts/secret-redaction.js +0 -166
- package/scripts/self-harness-optimizer.js +0 -141
- package/scripts/self-healing-check.js +0 -193
- package/scripts/silent-failure-cluster.js +0 -531
- package/scripts/statusline-cache-read.js +0 -57
- package/scripts/sync-telemetry-from-prod.js +0 -374
- package/scripts/tool-contract-validator.js +0 -76
- package/scripts/trajectory-scorer.js +0 -63
- package/scripts/verify-marketing-pages-deployed.js +0 -195
- package/scripts/visitor-journey.js +0 -172
package/public/dashboard.html
CHANGED
|
@@ -5,13 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>ThumbGate Dashboard โ Gate Stats, Agent Inventory, Remediations</title>
|
|
7
7
|
<meta name="description" content="Live ThumbGate dashboard showing gate enforcement stats, agent surface inventory, actionable remediations, prevention impact, and system health for pre-action checks.">
|
|
8
|
-
<link rel="canonical" href="https://thumbgate.
|
|
8
|
+
<link rel="canonical" href="https://thumbgate-production.up.railway.app/dashboard">
|
|
9
9
|
<link rel="icon" type="image/png" href="/thumbgate-icon.png">
|
|
10
10
|
<link rel="apple-touch-icon" href="/assets/brand/thumbgate-mark.svg">
|
|
11
11
|
<meta name="robots" content="noindex">
|
|
12
|
-
<meta name="referrer" content="same-origin">
|
|
13
12
|
<!-- Privacy-friendly analytics by Plausible -->
|
|
14
|
-
<script defer data-domain="thumbgate.
|
|
13
|
+
<script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
|
|
15
14
|
<script type="application/ld+json">
|
|
16
15
|
{
|
|
17
16
|
"@context": "https://schema.org",
|
|
@@ -19,7 +18,7 @@
|
|
|
19
18
|
"name": "ThumbGate Dashboard",
|
|
20
19
|
"applicationCategory": "DeveloperApplication",
|
|
21
20
|
"operatingSystem": "Cross-platform",
|
|
22
|
-
"url": "https://thumbgate.
|
|
21
|
+
"url": "https://thumbgate-production.up.railway.app/dashboard",
|
|
23
22
|
"dateModified": "2026-04-20",
|
|
24
23
|
"creator": {
|
|
25
24
|
"@type": "Person",
|
|
@@ -103,7 +102,7 @@
|
|
|
103
102
|
.result-tags { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
|
|
104
103
|
.tag { font-size: 11px; background: var(--cyan-dim); color: var(--cyan); padding: 2px 8px; border-radius: 4px; cursor: pointer; transition: background 0.15s, transform 0.1s; border: none; font-family: inherit; }
|
|
105
104
|
.tag:hover { background: rgba(34,211,238,0.25); transform: translateY(-1px); }
|
|
106
|
-
mark { background:
|
|
105
|
+
mark { background: rgba(34,211,238,0.25); color: var(--cyan); border-radius: 2px; padding: 0 2px; }
|
|
107
106
|
|
|
108
107
|
/* GATES */
|
|
109
108
|
.gates-section { margin-bottom: 32px; }
|
|
@@ -179,74 +178,6 @@
|
|
|
179
178
|
.demo-banner a { color: var(--cyan); text-decoration: none; font-weight: 600; }
|
|
180
179
|
.demo-banner a:hover { text-decoration: underline; }
|
|
181
180
|
|
|
182
|
-
/* Tooltips styling */
|
|
183
|
-
.tooltip-trigger {
|
|
184
|
-
display: inline-flex;
|
|
185
|
-
align-items: center;
|
|
186
|
-
justify-content: center;
|
|
187
|
-
width: 14px;
|
|
188
|
-
height: 14px;
|
|
189
|
-
border-radius: 50%;
|
|
190
|
-
background: var(--border);
|
|
191
|
-
color: var(--text-muted);
|
|
192
|
-
font-size: 9px;
|
|
193
|
-
font-weight: bold;
|
|
194
|
-
cursor: help;
|
|
195
|
-
margin-left: 6px;
|
|
196
|
-
position: relative;
|
|
197
|
-
vertical-align: middle;
|
|
198
|
-
user-select: none;
|
|
199
|
-
}
|
|
200
|
-
.tooltip-trigger:hover {
|
|
201
|
-
background: var(--cyan);
|
|
202
|
-
color: var(--bg);
|
|
203
|
-
}
|
|
204
|
-
.tooltip-trigger .tooltip-content {
|
|
205
|
-
visibility: hidden;
|
|
206
|
-
opacity: 0;
|
|
207
|
-
width: 220px;
|
|
208
|
-
background-color: #161618;
|
|
209
|
-
color: #e8e8ec;
|
|
210
|
-
text-align: left;
|
|
211
|
-
border: 1px solid var(--border);
|
|
212
|
-
border-radius: 6px;
|
|
213
|
-
padding: 8px 12px;
|
|
214
|
-
position: absolute;
|
|
215
|
-
z-index: 1000;
|
|
216
|
-
bottom: 130%;
|
|
217
|
-
left: 50%;
|
|
218
|
-
transform: translateX(-50%);
|
|
219
|
-
transition: opacity 0.15s, visibility 0.15s;
|
|
220
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
|
|
221
|
-
font-weight: normal;
|
|
222
|
-
font-size: 11px;
|
|
223
|
-
line-height: 1.4;
|
|
224
|
-
white-space: normal;
|
|
225
|
-
pointer-events: none;
|
|
226
|
-
font-family: var(--font);
|
|
227
|
-
text-transform: none;
|
|
228
|
-
letter-spacing: normal;
|
|
229
|
-
}
|
|
230
|
-
.tooltip-trigger .tooltip-content::after {
|
|
231
|
-
content: "";
|
|
232
|
-
position: absolute;
|
|
233
|
-
top: 100%;
|
|
234
|
-
left: 50%;
|
|
235
|
-
transform: translateX(-50%);
|
|
236
|
-
border-width: 5px;
|
|
237
|
-
border-style: solid;
|
|
238
|
-
border-color: #161618 transparent transparent transparent;
|
|
239
|
-
}
|
|
240
|
-
.tooltip-trigger:hover .tooltip-content {
|
|
241
|
-
visibility: visible;
|
|
242
|
-
opacity: 1;
|
|
243
|
-
}
|
|
244
|
-
#disconnectBtn:hover {
|
|
245
|
-
background: rgba(248,113,113,0.1) !important;
|
|
246
|
-
border-color: #f87171 !important;
|
|
247
|
-
color: #f87171 !important;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
181
|
.empty { text-align: center; padding: 48px; color: var(--text-muted); font-size: 15px; }
|
|
251
182
|
.loading { text-align: center; padding: 24px; color: var(--text-muted); }
|
|
252
183
|
h2 { font-size: 20px; font-weight: 700; margin-bottom: 16px; letter-spacing: -0.02em; }
|
|
@@ -258,31 +189,11 @@
|
|
|
258
189
|
.settings-card .team-value, .origin-value { font-size: 18px; font-weight: 700; color: var(--text); margin-top: 8px; word-break: break-word; }
|
|
259
190
|
.origin-list, .layer-list, .routing-list { display: flex; flex-direction: column; gap: 12px; }
|
|
260
191
|
.origin-note, .layer-note, .routing-note { font-size: 13px; color: var(--text-muted); line-height: 1.55; }
|
|
261
|
-
.enterprise-chat-layout { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(260px, 0.7fr); gap: 16px; align-items: start; }
|
|
262
|
-
.enterprise-chat-box { min-height: 110px; resize: vertical; width: 100%; background: var(--bg-raised); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; color: var(--text); font-size: 14px; font-family: var(--font); line-height: 1.5; }
|
|
263
|
-
.enterprise-chat-box:focus { outline: none; border-color: var(--cyan); }
|
|
264
|
-
.enterprise-answer { margin-top: 14px; background: var(--bg-raised); border: 1px solid var(--border); border-radius: 10px; padding: 16px; min-height: 92px; font-size: 14px; color: var(--text); line-height: 1.65; }
|
|
265
|
-
.enterprise-answer.blocked { border-color: rgba(248,113,113,0.45); background: rgba(248,113,113,0.07); }
|
|
266
|
-
.enterprise-source-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
|
267
|
-
.enterprise-source { font-size: 11px; border: 1px solid var(--border); border-radius: 999px; padding: 4px 9px; color: var(--text-muted); background: var(--bg-card); }
|
|
268
|
-
|
|
269
|
-
.quick-nav-link {
|
|
270
|
-
cursor: pointer;
|
|
271
|
-
transition: opacity 0.15s ease, color 0.15s ease;
|
|
272
|
-
}
|
|
273
|
-
.quick-nav-link:hover {
|
|
274
|
-
color: var(--cyan);
|
|
275
|
-
opacity: 0.95;
|
|
276
|
-
}
|
|
277
|
-
.quick-nav-link:hover strong {
|
|
278
|
-
color: var(--cyan) !important;
|
|
279
|
-
text-decoration: underline;
|
|
280
|
-
}
|
|
281
192
|
|
|
282
193
|
@media (max-width: 700px) {
|
|
283
194
|
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
284
195
|
.search-filters { flex-wrap: wrap; }
|
|
285
|
-
.team-grid, .template-grid, .team-columns, .settings-grid, .generated-grid, .inventory-grid
|
|
196
|
+
.team-grid, .template-grid, .team-columns, .settings-grid, .generated-grid, .inventory-grid { grid-template-columns: 1fr; }
|
|
286
197
|
}
|
|
287
198
|
</style>
|
|
288
199
|
</head>
|
|
@@ -290,7 +201,7 @@
|
|
|
290
201
|
|
|
291
202
|
<nav>
|
|
292
203
|
<div class="container">
|
|
293
|
-
<a href="/" class="nav-logo"><img src="/assets/brand/thumbgate-mark-inline
|
|
204
|
+
<a href="/" class="nav-logo"><img src="/assets/brand/thumbgate-mark-inline.svg" alt="ThumbGate" class="logo-mark" width="28" height="28"><span class="logo-text">ThumbGate Dashboard</span></a>
|
|
294
205
|
<div class="nav-links">
|
|
295
206
|
<a href="/dashboard" class="active" style="color:var(--cyan);">Dashboard</a>
|
|
296
207
|
<a href="/lessons">Lessons</a>
|
|
@@ -303,14 +214,13 @@
|
|
|
303
214
|
<div class="auth-bar">
|
|
304
215
|
<div class="container">
|
|
305
216
|
<div style="margin-bottom:10px;">
|
|
306
|
-
<span
|
|
217
|
+
<span style="font-size:14px;font-weight:600;">Connect your API key</span>
|
|
307
218
|
<span id="authHelp" style="font-size:13px;color:var(--text-muted);margin-left:8px;">Pro auto-connects locally when you launch with <code style="font-family:var(--mono);font-size:12px;background:var(--cyan-dim);color:var(--cyan);padding:2px 6px;border-radius:4px;">npx thumbgate pro</code>. Otherwise paste your <code style="font-family:var(--mono);font-size:12px;background:var(--cyan-dim);color:var(--cyan);padding:2px 6px;border-radius:4px;">THUMBGATE_API_KEY</code> manually.</span>
|
|
308
219
|
</div>
|
|
309
220
|
<div style="display:flex;gap:12px;align-items:center;">
|
|
310
221
|
<input type="password" class="auth-input" id="apiKey" placeholder="Paste your API key here (THUMBGATE_API_KEY or THUMBGATE_PRO_KEY)..." />
|
|
311
222
|
<button class="btn" id="connectBtn" onclick="connect()">Connect</button>
|
|
312
223
|
<button class="btn-outline" id="demoBtn" onclick="loadDemo()">Try Demo</button>
|
|
313
|
-
<button class="btn-outline" id="disconnectBtn" onclick="disconnectKey()" style="display:none; border-color:#f87171; color:#f87171;">Disconnect Key</button>
|
|
314
224
|
<span class="auth-status" id="authStatus"></span>
|
|
315
225
|
</div>
|
|
316
226
|
</div>
|
|
@@ -323,10 +233,10 @@
|
|
|
323
233
|
<p style="font-size:12px;color:var(--text-muted);margin-bottom:8px;">Updated: <time datetime="2026-04-20">2026-04-20</time> ยท by <a href="https://github.com/IgorGanapolsky" style="color:inherit;">Igor Ganapolsky</a></p>
|
|
324
234
|
<p style="font-size:14px;color:var(--text-muted);line-height:1.6;max-width:700px;">What's happening right now? Search memories, inspect active checks, manage your team, and export training data. <span style="color:var(--cyan);font-weight:600;">This is your control plane for AI agent behavior.</span></p>
|
|
325
235
|
<div style="display:flex;gap:16px;margin-top:12px;font-size:12px;color:var(--text-muted);">
|
|
326
|
-
<span
|
|
327
|
-
<span
|
|
328
|
-
<span
|
|
329
|
-
<span
|
|
236
|
+
<span>๐ <strong style="color:var(--text);">Search</strong> โ find any memory</span>
|
|
237
|
+
<span>๐ก๏ธ <strong style="color:var(--text);">Gates</strong> โ what's blocking</span>
|
|
238
|
+
<span>๐ฅ <strong style="color:var(--text);">Team</strong> โ org metrics</span>
|
|
239
|
+
<span>๐ค <strong style="color:var(--text);">Export</strong> โ DPO training data</span>
|
|
330
240
|
</div>
|
|
331
241
|
</div>
|
|
332
242
|
|
|
@@ -337,33 +247,16 @@
|
|
|
337
247
|
|
|
338
248
|
<!-- STATS -->
|
|
339
249
|
<div class="stats-grid" id="statsGrid">
|
|
340
|
-
<a class="stat-card" data-card-action="all" onclick="selectCard(this,'all')" href="/lessons
|
|
341
|
-
<a class="stat-card" data-card-action="up" onclick="selectCard(this,'up')" href="/lessons?signal=
|
|
342
|
-
<a class="stat-card" data-card-action="down" onclick="selectCard(this,'down')" href="/lessons?signal=
|
|
343
|
-
<a class="stat-card" data-card-action="gates" onclick="selectCard(this,'gates');return false;" href="#" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view active checks"><div class="stat-label">Active Gates
|
|
344
|
-
</div>
|
|
345
|
-
|
|
346
|
-
<div class="panel" id="chatPanel" style="margin-bottom:20px;">
|
|
347
|
-
<div style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:10px;">
|
|
348
|
-
<h2 style="margin:0;">๐ฌ Chat with your data</h2>
|
|
349
|
-
<span style="font-size:13px;color:var(--text-muted);">Ask about your gates, blocks, feedback, and lessons โ answered <strong>locally from your own data</strong>, no cloud. (An optional BYO model only kicks in for open-ended questions.)</span>
|
|
350
|
-
</div>
|
|
351
|
-
<div id="chatMessages" style="max-height:360px;overflow-y:auto;margin-bottom:12px;display:none;padding-right:4px;"></div>
|
|
352
|
-
<div style="display:flex;gap:8px;">
|
|
353
|
-
<input id="chatInput" class="auth-input" style="flex:1;" placeholder="e.g. What mistakes have we made, and how do we avoid them?" onkeydown="if(event.key==='Enter'){event.preventDefault();sendChat();}" />
|
|
354
|
-
<button class="btn" id="chatSend" onclick="sendChat()">Ask</button>
|
|
355
|
-
</div>
|
|
356
|
-
<div id="chatHint" style="font-size:12px;color:var(--text-muted);margin-top:8px;display:flex;align-items:center;gap:8px;">
|
|
357
|
-
<span>Powered by local dashboard data. Optional Gemini/Perplexity keys only expand open-ended analysis.</span>
|
|
358
|
-
<input type="password" id="geminiKeyInput" placeholder="Optional GEMINI_API_KEY..." style="background:var(--bg-raised); border:1px solid var(--border); border-radius:4px; padding:4px 8px; color:var(--text); font-family:var(--mono); font-size:11px; flex:1; max-width: 250px;" onkeydown="if(event.key==='Enter'){event.preventDefault();saveGeminiKey();}" />
|
|
359
|
-
<button class="btn-outline" style="padding:4px 10px;font-size:11px;border-radius:4px;" onclick="saveGeminiKey()">Save</button>
|
|
360
|
-
</div>
|
|
250
|
+
<a class="stat-card" data-card-action="all" onclick="selectCard(this,'all')" href="/lessons" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view all feedback โ Lessons page"><div class="stat-label">Total Feedback</div><div class="stat-value cyan" id="statTotal">โ</div></a>
|
|
251
|
+
<a class="stat-card" data-card-action="up" onclick="selectCard(this,'up')" href="/lessons?signal=positive" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view positive feedback โ Lessons page"><div class="stat-label">๐ Positive</div><div class="stat-value green" id="statPositive">โ</div></a>
|
|
252
|
+
<a class="stat-card" data-card-action="down" onclick="selectCard(this,'down')" href="/lessons?signal=negative" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view negative feedback โ Lessons page"><div class="stat-label">๐ Negative</div><div class="stat-value red" id="statNegative">โ</div></a>
|
|
253
|
+
<a class="stat-card" data-card-action="gates" onclick="selectCard(this,'gates');return false;" href="#" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view active checks"><div class="stat-label">Active Gates</div><div class="stat-value cyan" id="statGates">โ</div></a>
|
|
361
254
|
</div>
|
|
362
255
|
|
|
363
256
|
<div class="panel" id="reviewDeltaPanel" style="margin-bottom:20px;">
|
|
364
257
|
<div style="display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap;">
|
|
365
258
|
<div>
|
|
366
|
-
<h3 style="margin:0 0 8px 0;font-size:14px;letter-spacing:0.04em;text-transform:uppercase;color:var(--text-muted);
|
|
259
|
+
<h3 style="margin:0 0 8px 0;font-size:14px;letter-spacing:0.04em;text-transform:uppercase;color:var(--text-muted);">๐ Since Last Review</h3>
|
|
367
260
|
<p class="template-summary" id="reviewDeltaHeadline" style="margin-bottom:0;">No review checkpoint yet. Mark the current dashboard as reviewed to start seeing only new changes.</p>
|
|
368
261
|
</div>
|
|
369
262
|
<button class="btn-outline" id="reviewCheckpointBtn" onclick="markReviewed()">Mark Current Dashboard Reviewed</button>
|
|
@@ -391,8 +284,6 @@
|
|
|
391
284
|
<div class="tab active" onclick="switchTab('search')">๐ Search Memories</div>
|
|
392
285
|
<div class="tab" onclick="switchTab('gates')">๐ก๏ธ Active Gates</div>
|
|
393
286
|
<div class="tab" onclick="switchTab('team')">๐ฅ Team</div>
|
|
394
|
-
<div class="tab" onclick="switchTab('enterprise')">๐ข Governed Data Chat</div>
|
|
395
|
-
<div class="tab" onclick="switchTab('ai-inventory')">๐งฌ AI Inventory</div>
|
|
396
287
|
<div class="tab" onclick="switchTab('generated')">๐งฉ Generated Views</div>
|
|
397
288
|
<div class="tab" onclick="switchTab('settings')">โ๏ธ Policy Origins</div>
|
|
398
289
|
<div class="tab" onclick="switchTab('templates')">๐งฑ Gate Templates</div>
|
|
@@ -402,12 +293,6 @@
|
|
|
402
293
|
|
|
403
294
|
<!-- SEARCH TAB -->
|
|
404
295
|
<div class="tab-content active" id="tab-search">
|
|
405
|
-
<div class="panel" style="margin-bottom:24px;">
|
|
406
|
-
<h3 style="margin-top:0;">๐ Feedback Trend (Last 30 Days)</h3>
|
|
407
|
-
<div style="position:relative;height:180px;margin-top:12px;">
|
|
408
|
-
<canvas id="feedbackTrendChart"></canvas>
|
|
409
|
-
</div>
|
|
410
|
-
</div>
|
|
411
296
|
<div class="search-section">
|
|
412
297
|
<div class="search-bar">
|
|
413
298
|
<input type="text" class="search-input" id="searchQuery" placeholder="Search memories... (try: git, test, deploy, secrets, database)" onkeydown="if(event.key==='Enter')search()" />
|
|
@@ -429,74 +314,6 @@
|
|
|
429
314
|
<div class="tab-content" id="tab-gates">
|
|
430
315
|
<div class="gates-section">
|
|
431
316
|
<h2>Active Pre-Action Checks</h2>
|
|
432
|
-
|
|
433
|
-
<!-- Interactive Gating Workflow Diagram -->
|
|
434
|
-
<details class="flow-details" style="margin-bottom: 24px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-card); padding: 16px;" open>
|
|
435
|
-
<summary style="font-weight: 600; cursor: pointer; color: var(--cyan); display: flex; align-items: center; gap: 8px; user-select: none;">
|
|
436
|
-
<span>๐ก๏ธ How Pre-Action Gating Works (Interactive Flow Diagram)</span>
|
|
437
|
-
</summary>
|
|
438
|
-
<div style="margin-top:20px; text-align:center;">
|
|
439
|
-
<svg viewBox="0 0 800 220" width="100%" height="auto" style="max-width: 800px; font-family: var(--font); font-size: 11px;">
|
|
440
|
-
<style>
|
|
441
|
-
.flow-node { fill: #1c1c1f; stroke: var(--border); stroke-width: 1.5; rx: 8px; transition: stroke 0.2s, fill 0.2s; }
|
|
442
|
-
.flow-node:hover { fill: rgba(34,211,238,0.06); stroke: var(--cyan); cursor: help; }
|
|
443
|
-
.flow-text { fill: var(--text); font-weight: 600; pointer-events: none; }
|
|
444
|
-
.flow-subtext { fill: var(--text-muted); font-size: 10px; pointer-events: none; }
|
|
445
|
-
.flow-arrow { fill: none; stroke: var(--border); stroke-width: 1.5; marker-end: url(#arrow-head); }
|
|
446
|
-
</style>
|
|
447
|
-
|
|
448
|
-
<defs>
|
|
449
|
-
<marker id="arrow-head" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
|
450
|
-
<path d="M 0 1 L 10 5 L 0 9 z" fill="#8b8b96" />
|
|
451
|
-
</marker>
|
|
452
|
-
</defs>
|
|
453
|
-
|
|
454
|
-
<!-- Step 1: Tool Call -->
|
|
455
|
-
<g>
|
|
456
|
-
<rect x="10" y="80" width="130" height="60" class="flow-node" />
|
|
457
|
-
<text x="75" y="105" text-anchor="middle" class="flow-text">1. Tool Call</text>
|
|
458
|
-
<text x="75" y="125" text-anchor="middle" class="flow-subtext">Agent requests action</text>
|
|
459
|
-
</g>
|
|
460
|
-
|
|
461
|
-
<path d="M 140 110 L 190 110" class="flow-arrow" />
|
|
462
|
-
|
|
463
|
-
<!-- Step 2: Hook Intercept -->
|
|
464
|
-
<g>
|
|
465
|
-
<rect x="200" y="80" width="140" height="60" class="flow-node" />
|
|
466
|
-
<text x="270" y="105" text-anchor="middle" class="flow-text">2. PreToolUse Hook</text>
|
|
467
|
-
<text x="270" y="125" text-anchor="middle" class="flow-subtext">Intercepts tool execution</text>
|
|
468
|
-
</g>
|
|
469
|
-
|
|
470
|
-
<path d="M 340 110 L 390 110" class="flow-arrow" />
|
|
471
|
-
|
|
472
|
-
<!-- Step 3: Policy / DB Lookup -->
|
|
473
|
-
<g>
|
|
474
|
-
<rect x="400" y="80" width="150" height="60" class="flow-node" />
|
|
475
|
-
<text x="475" y="105" text-anchor="middle" class="flow-text">3. Gate Evaluation</text>
|
|
476
|
-
<text x="475" y="125" text-anchor="middle" class="flow-subtext">Validates matching rules</text>
|
|
477
|
-
</g>
|
|
478
|
-
|
|
479
|
-
<!-- Branch Arrows -->
|
|
480
|
-
<path d="M 550 100 L 640 55" class="flow-arrow" />
|
|
481
|
-
<path d="M 550 120 L 640 165" class="flow-arrow" />
|
|
482
|
-
|
|
483
|
-
<!-- Step 4a: Block -->
|
|
484
|
-
<g>
|
|
485
|
-
<rect x="650" y="20" width="130" height="60" class="flow-node" style="stroke:#ef4444;" />
|
|
486
|
-
<text x="715" y="45" text-anchor="middle" class="flow-text" style="fill:#ef4444;">โ BLOCK</text>
|
|
487
|
-
<text x="715" y="65" text-anchor="middle" class="flow-subtext">Matches rule -> aborted</text>
|
|
488
|
-
</g>
|
|
489
|
-
|
|
490
|
-
<!-- Step 4b: Allow -->
|
|
491
|
-
<g>
|
|
492
|
-
<rect x="650" y="140" width="130" height="60" class="flow-node" style="stroke:#22c55e;" />
|
|
493
|
-
<text x="715" y="165" text-anchor="middle" class="flow-text" style="fill:#22c55e;">โ
ALLOW</text>
|
|
494
|
-
<text x="715" y="185" text-anchor="middle" class="flow-subtext">Runs safely & logs</text>
|
|
495
|
-
</g>
|
|
496
|
-
</svg>
|
|
497
|
-
</div>
|
|
498
|
-
</details>
|
|
499
|
-
|
|
500
317
|
<div id="gatesList"><div class="loading">Loading gates...</div></div>
|
|
501
318
|
</div>
|
|
502
319
|
</div>
|
|
@@ -570,56 +387,6 @@
|
|
|
570
387
|
</div>
|
|
571
388
|
</div>
|
|
572
389
|
|
|
573
|
-
<!-- ENTERPRISE CHAT TAB -->
|
|
574
|
-
<div class="tab-content" id="tab-enterprise">
|
|
575
|
-
<div class="templates-section">
|
|
576
|
-
<h2>Governed Data Chat</h2>
|
|
577
|
-
<p class="template-summary">Ask questions over your local ThumbGate data (lessons, raw feedback via LanceDB vectors, gate stats, receipts). Uses local retrieval plus your configured LLM, including open-source/local endpoints through <code>THUMBGATE_LOCAL_LLM_ENDPOINT</code>. Dialogflow is not the chatbot layer; DFCX/Vertex cards are optional deployment-readiness checks for buyers who run their own Google agent stack.</p>
|
|
578
|
-
<div class="enterprise-chat-layout">
|
|
579
|
-
<div class="panel">
|
|
580
|
-
<h3>Chat With Local ThumbGate Data</h3>
|
|
581
|
-
<textarea class="enterprise-chat-box" id="enterpriseChatPrompt" placeholder="Ask: What mistakes are recurring? Which gates blocked the most? Are we running local-only or cloud-integrated?"></textarea>
|
|
582
|
-
<div style="display:flex;gap:10px;align-items:center;margin-top:12px;flex-wrap:wrap;">
|
|
583
|
-
<button class="btn" id="enterpriseChatBtn" onclick="sendEnterpriseChat()">Ask ThumbGate</button>
|
|
584
|
-
<button class="btn-outline" onclick="setEnterprisePrompt('Which gates are blocking risky actions?')">Gates</button>
|
|
585
|
-
<button class="btn-outline" onclick="setEnterprisePrompt('What feedback mistakes keep repeating?')">Feedback</button>
|
|
586
|
-
<button class="btn-outline" onclick="setEnterprisePrompt('Are we running local-only or cloud-integrated?')">Runtime</button>
|
|
587
|
-
</div>
|
|
588
|
-
<div class="enterprise-answer" id="enterpriseChatAnswer">Connect your dashboard, then ask about local ThumbGate data.</div>
|
|
589
|
-
<div class="enterprise-source-list" id="enterpriseChatSources"></div>
|
|
590
|
-
</div>
|
|
591
|
-
<div class="panel">
|
|
592
|
-
<h3>Runtime Readiness</h3>
|
|
593
|
-
<div class="inventory-tools" id="enterpriseStatusCards"><div class="loading">Loading enterprise status...</div></div>
|
|
594
|
-
<div class="template-summary" style="margin-top:14px;margin-bottom:0;">The chat itself is local RAG plus your LLM. Open-source/local models are the default enterprise path; DFCX/Vertex readiness is optional evidence for teams that already operate Google agents in their own tenancy.</div>
|
|
595
|
-
</div>
|
|
596
|
-
</div>
|
|
597
|
-
</div>
|
|
598
|
-
</div>
|
|
599
|
-
|
|
600
|
-
<!-- AI INVENTORY TAB -->
|
|
601
|
-
<div class="tab-content" id="tab-ai-inventory">
|
|
602
|
-
<div class="templates-section">
|
|
603
|
-
<h2>AI Component Inventory</h2>
|
|
604
|
-
<p class="template-summary">Enterprise evidence for AI/ML usage: provider SDKs, agent frameworks, vector databases, Vertex/Gemini/Dialogflow CX references, and local model artifacts. Export this as ML-BOM proof before claiming an AI system is production-ready.</p>
|
|
605
|
-
<div class="inventory-grid" id="aiInventorySummaryCards"><div class="loading">Scanning AI inventory...</div></div>
|
|
606
|
-
<div class="team-columns" style="margin-top:16px;">
|
|
607
|
-
<div class="panel">
|
|
608
|
-
<h3>Detected Components</h3>
|
|
609
|
-
<div class="inventory-tools" id="aiInventoryComponents"><div class="loading">Scanning components...</div></div>
|
|
610
|
-
</div>
|
|
611
|
-
<div class="panel">
|
|
612
|
-
<h3>Export Evidence</h3>
|
|
613
|
-
<div class="template-summary" style="margin-bottom:12px;">CLI export:</div>
|
|
614
|
-
<pre style="white-space:pre-wrap;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;font-size:12px;color:var(--text);">npx thumbgate ai-inventory --format=cyclonedx --output=.thumbgate/ai-mlbom.json</pre>
|
|
615
|
-
<div class="template-summary" style="margin-top:12px;">API export:</div>
|
|
616
|
-
<pre style="white-space:pre-wrap;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;font-size:12px;color:var(--text);">GET /v1/dashboard/ai-inventory?format=cyclonedx</pre>
|
|
617
|
-
<div id="aiInventoryExportNote" class="template-summary" style="margin-top:12px;margin-bottom:0;">Waiting for inventory evidence...</div>
|
|
618
|
-
</div>
|
|
619
|
-
</div>
|
|
620
|
-
</div>
|
|
621
|
-
</div>
|
|
622
|
-
|
|
623
390
|
<!-- GENERATED TAB -->
|
|
624
391
|
<div class="tab-content" id="tab-generated">
|
|
625
392
|
<div class="templates-section">
|
|
@@ -693,7 +460,13 @@
|
|
|
693
460
|
</div>
|
|
694
461
|
|
|
695
462
|
<!-- Charts -->
|
|
696
|
-
<div style="margin-bottom:24px;">
|
|
463
|
+
<div class="team-columns" style="margin-bottom:24px;">
|
|
464
|
+
<div class="panel">
|
|
465
|
+
<h3>Feedback Trend (30 days)</h3>
|
|
466
|
+
<div style="position:relative;height:260px;margin-top:12px;">
|
|
467
|
+
<canvas id="feedbackTrendChart"></canvas>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
697
470
|
<div class="panel">
|
|
698
471
|
<h3>Lessons Generated (30 days)</h3>
|
|
699
472
|
<div style="position:relative;height:260px;margin-top:12px;">
|
|
@@ -766,91 +539,6 @@
|
|
|
766
539
|
</div>
|
|
767
540
|
|
|
768
541
|
<script>
|
|
769
|
-
// TG_TOKEN_SAVINGS โ browser shim that mirrors scripts/token-savings.js
|
|
770
|
-
// so loadDemo()'s TG_TOKEN_SAVINGS.compute(blockedCalls, deflectedBots)
|
|
771
|
-
// call resolves. Pre-2026-05-26 the dashboard referenced this global
|
|
772
|
-
// without ever defining it, silently halting loadDemo() at line ~1452.
|
|
773
|
-
var TG_TOKEN_SAVINGS = (function () {
|
|
774
|
-
var DEFAULT_MODEL_MIX = { 'claude-sonnet-4-5': 0.80, 'claude-opus-4-6': 0.15, 'claude-haiku-4-5': 0.05 };
|
|
775
|
-
var DEFAULT_MODEL_PRICES = {
|
|
776
|
-
'claude-sonnet-4-5': { input: 3.0, output: 15.0 },
|
|
777
|
-
'claude-opus-4-6': { input: 5.0, output: 25.0 },
|
|
778
|
-
'claude-haiku-4-5': { input: 1.0, output: 5.0 },
|
|
779
|
-
'gpt-4o': { input: 2.50, output: 10.0 }
|
|
780
|
-
};
|
|
781
|
-
var DEFAULT_AVG_INPUT_TOKENS = 2000;
|
|
782
|
-
var DEFAULT_AVG_OUTPUT_TOKENS = 600;
|
|
783
|
-
function clamp(n, fb) { var v = Number(n); return (isFinite(v) && v >= 0) ? v : fb; }
|
|
784
|
-
function blended(mix, prices) {
|
|
785
|
-
var i = 0, o = 0, w = 0;
|
|
786
|
-
for (var m in mix) {
|
|
787
|
-
var ww = clamp(mix[m], 0); if (ww <= 0) continue;
|
|
788
|
-
var p = prices[m]; if (!p) continue;
|
|
789
|
-
i += clamp(p.input, 0) * ww; o += clamp(p.output, 0) * ww; w += ww;
|
|
790
|
-
}
|
|
791
|
-
return w > 0 ? { input: i / w, output: o / w } : { input: 0, output: 0 };
|
|
792
|
-
}
|
|
793
|
-
function fmtTokens(n) {
|
|
794
|
-
if (!isFinite(n) || n <= 0) return '0';
|
|
795
|
-
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
|
|
796
|
-
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
797
|
-
if (n >= 1e3) return (n / 1e3).toFixed(0) + 'K';
|
|
798
|
-
return String(Math.round(n));
|
|
799
|
-
}
|
|
800
|
-
function fmtDollars(n) {
|
|
801
|
-
if (!isFinite(n)) return '$0.00';
|
|
802
|
-
if (n >= 1000) return '$' + Math.round(n).toLocaleString('en-US');
|
|
803
|
-
if (n >= 100) return '$' + n.toFixed(0);
|
|
804
|
-
if (n >= 10) return '$' + n.toFixed(1);
|
|
805
|
-
if (n >= 0.01) return '$' + n.toFixed(2);
|
|
806
|
-
if (n > 0) return '$' + n.toFixed(4);
|
|
807
|
-
return '$0.00';
|
|
808
|
-
}
|
|
809
|
-
function compute(blockedCalls, deflectedBots, opts) {
|
|
810
|
-
opts = opts || {};
|
|
811
|
-
var total = clamp(blockedCalls, 0) + clamp(deflectedBots, 0);
|
|
812
|
-
var avgIn = clamp(opts.avgInputTokensPerBlock, DEFAULT_AVG_INPUT_TOKENS);
|
|
813
|
-
var avgOut = clamp(opts.avgOutputTokensPerBlock, DEFAULT_AVG_OUTPUT_TOKENS);
|
|
814
|
-
var mix = (opts.modelMix && Object.keys(opts.modelMix).length) ? opts.modelMix : DEFAULT_MODEL_MIX;
|
|
815
|
-
var prices = opts.modelPrices || DEFAULT_MODEL_PRICES;
|
|
816
|
-
var b = blended(mix, prices);
|
|
817
|
-
var ti = total * avgIn, to = total * avgOut;
|
|
818
|
-
var dollars = (ti * b.input + to * b.output) / 1e6;
|
|
819
|
-
return {
|
|
820
|
-
blockedCalls: clamp(blockedCalls, 0),
|
|
821
|
-
deflectedBots: clamp(deflectedBots, 0),
|
|
822
|
-
tokensSavedInput: ti,
|
|
823
|
-
tokensSavedOutput: to,
|
|
824
|
-
tokensSavedTotal: ti + to,
|
|
825
|
-
dollarsSaved: dollars,
|
|
826
|
-
dollarsSavedDisplay: fmtDollars(dollars),
|
|
827
|
-
tokensSavedDisplay: fmtTokens(ti + to),
|
|
828
|
-
blendedPricePer1M: b,
|
|
829
|
-
modelMix: mix
|
|
830
|
-
};
|
|
831
|
-
}
|
|
832
|
-
return { compute: compute, formatDollars: fmtDollars, formatTokens: fmtTokens };
|
|
833
|
-
})();
|
|
834
|
-
|
|
835
|
-
// renderTopBlockedGates โ defensive stub for the loadDemo() legacy direct
|
|
836
|
-
// call. The current dashboard layout surfaces top-blocked gates via the
|
|
837
|
-
// team panel (renderTeam โ team.topBlockedGates) and has no dedicated
|
|
838
|
-
// #topBlockedGates element. Stub keeps loadDemo() from throwing while
|
|
839
|
-
// also rendering into a target element if one is ever added.
|
|
840
|
-
function renderTopBlockedGates(gates) {
|
|
841
|
-
if (!Array.isArray(gates)) return;
|
|
842
|
-
var target = document.getElementById('topBlockedGates')
|
|
843
|
-
|| document.getElementById('topBlockedList');
|
|
844
|
-
if (!target || typeof escHtml !== 'function') return;
|
|
845
|
-
target.innerHTML = gates.map(function (g) {
|
|
846
|
-
return '<div class="blocked-row"><div><div class="blocked-name">'
|
|
847
|
-
+ escHtml(g.name || '')
|
|
848
|
-
+ '</div></div><div class="blocked-meta">'
|
|
849
|
-
+ (g.blocked || 0)
|
|
850
|
-
+ '</div></div>';
|
|
851
|
-
}).join('');
|
|
852
|
-
}
|
|
853
|
-
|
|
854
542
|
let API_KEY = '';
|
|
855
543
|
let currentSource = 'all';
|
|
856
544
|
let isDemo = false;
|
|
@@ -860,237 +548,38 @@ let currentGeneratedView = 'team-review';
|
|
|
860
548
|
const BOOTSTRAP_API_KEY = __DASHBOARD_BOOTSTRAP_KEY__;
|
|
861
549
|
const LOCAL_PRO_BOOTSTRAP = __DASHBOARD_BOOTSTRAP_ENABLED__;
|
|
862
550
|
|
|
863
|
-
let ACTIVE_PROJECT_DIR = '';
|
|
864
|
-
try {
|
|
865
|
-
var urlParams = new URLSearchParams(window.location.search);
|
|
866
|
-
ACTIVE_PROJECT_DIR = urlParams.get('project') || '';
|
|
867
|
-
} catch (e) {}
|
|
868
|
-
|
|
869
|
-
function preserveProjectQueryParams() {
|
|
870
|
-
if (!ACTIVE_PROJECT_DIR) return;
|
|
871
|
-
var links = document.querySelectorAll('a[href^="/dashboard"], a[href^="/lessons"], a[href="/"]');
|
|
872
|
-
links.forEach(function(link) {
|
|
873
|
-
try {
|
|
874
|
-
var url = new URL(link.href, window.location.origin);
|
|
875
|
-
url.searchParams.set('project', ACTIVE_PROJECT_DIR);
|
|
876
|
-
link.href = url.pathname + url.search + url.hash;
|
|
877
|
-
} catch (e) {}
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
|
|
881
551
|
function getHeaders() {
|
|
882
|
-
|
|
883
|
-
if (ACTIVE_PROJECT_DIR) {
|
|
884
|
-
headers['x-thumbgate-project-dir'] = ACTIVE_PROJECT_DIR;
|
|
885
|
-
}
|
|
886
|
-
return headers;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// --- Chat with your data ---------------------------------------------------
|
|
890
|
-
function chatEscape(s) {
|
|
891
|
-
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
|
892
|
-
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
function chatAppend(who, text) {
|
|
896
|
-
var messages = document.getElementById('chatMessages');
|
|
897
|
-
var row = document.createElement('div');
|
|
898
|
-
row.style.cssText = 'margin-bottom:14px;';
|
|
899
|
-
row.innerHTML = '<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:3px;">'
|
|
900
|
-
+ (who === 'you' ? 'You' : 'ThumbGate') + '</div><div class="chat-body" style="line-height:1.5;">' + chatEscape(text) + '</div>';
|
|
901
|
-
messages.appendChild(row);
|
|
902
|
-
return row.querySelector('.chat-body');
|
|
903
|
-
}
|
|
904
|
-
function chatRenderAnswer(a) {
|
|
905
|
-
return chatEscape(a)
|
|
906
|
-
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
907
|
-
.replace(/\[(\d+)\]/g, '<sup style="color:var(--cyan);font-weight:600;">[$1]</sup>')
|
|
908
|
-
.replace(/\n/g, '<br>');
|
|
909
|
-
}
|
|
910
|
-
function chatRenderSources(sources, answerText) {
|
|
911
|
-
if (!sources || !sources.length) return '';
|
|
912
|
-
var citedNums = new Set();
|
|
913
|
-
var regex = /\[(\d+)\]/g;
|
|
914
|
-
var match;
|
|
915
|
-
while ((match = regex.exec(answerText || '')) !== null) {
|
|
916
|
-
citedNums.add(parseInt(match[1], 10));
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
var items = sources.map(function (s, i) {
|
|
920
|
-
var num = i + 1;
|
|
921
|
-
var isCited = citedNums.has(num);
|
|
922
|
-
var label = chatEscape(String(s.title || s.id || '').slice(0, 64));
|
|
923
|
-
|
|
924
|
-
var bg = isCited ? 'var(--cyan-dim)' : 'var(--bg-raised)';
|
|
925
|
-
var color = isCited ? 'var(--cyan)' : 'var(--text-muted)';
|
|
926
|
-
var border = isCited ? '1px solid var(--cyan)' : '1px solid var(--border)';
|
|
927
|
-
var weight = isCited ? '600' : 'normal';
|
|
928
|
-
|
|
929
|
-
return '<span title="' + label + '" style="display:inline-block;font-size:11px;background:' + bg + ';color:' + color + ';border:' + border + ';font-weight:' + weight + ';padding:2px 7px;border-radius:5px;margin:4px 5px 0 0;">[' + num + '] ' + label + '</span>';
|
|
930
|
-
}).join('');
|
|
931
|
-
|
|
932
|
-
return '<div style="margin-top:12px;border-top:1px dashed var(--border);padding-top:10px;">' +
|
|
933
|
-
'<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px;font-weight:600;">Database logs analyzed to answer your question:</div>' +
|
|
934
|
-
items +
|
|
935
|
-
'</div>';
|
|
936
|
-
}
|
|
937
|
-
async function sendChat() {
|
|
938
|
-
var input = document.getElementById('chatInput');
|
|
939
|
-
var q = (input.value || '').trim();
|
|
940
|
-
if (!q) return;
|
|
941
|
-
var messages = document.getElementById('chatMessages');
|
|
942
|
-
var sendBtn = document.getElementById('chatSend');
|
|
943
|
-
messages.style.display = 'block';
|
|
944
|
-
chatAppend('you', q);
|
|
945
|
-
input.value = '';
|
|
946
|
-
sendBtn.disabled = true;
|
|
947
|
-
var pending = chatAppend('bot', 'Thinkingโฆ');
|
|
948
|
-
try {
|
|
949
|
-
var res = await fetch('/v1/chat', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ question: q }) });
|
|
950
|
-
var data = await res.json();
|
|
951
|
-
if (data && data.ok) {
|
|
952
|
-
pending.innerHTML = chatRenderAnswer(data.answer) + chatRenderSources(data.sources, data.answer);
|
|
953
|
-
} else if (data && data.error === 'gemini_error') {
|
|
954
|
-
pending.innerHTML = '<em style="color:#f87171;">Gemini rejected the key: ' + chatEscape(data.message || 'invalid') +
|
|
955
|
-
'<br>Fix: paste a valid key in the "Set GEMINI_API_KEY..." box below (get one at <a href="https://aistudio.google.com/app/apikey" target="_blank" style="color:#67e8f9;">AI Studio</a>) and click Save.</em>';
|
|
956
|
-
} else {
|
|
957
|
-
pending.innerHTML = '<em style="color:var(--text-muted);">' + chatEscape((data && data.message) || 'Chat is unavailable.') + '</em>';
|
|
958
|
-
}
|
|
959
|
-
} catch (e) {
|
|
960
|
-
pending.innerHTML = '<em style="color:var(--text-muted);">Chat request failed: ' + chatEscape(String((e && e.message) || e)) + '</em>';
|
|
961
|
-
} finally {
|
|
962
|
-
sendBtn.disabled = false;
|
|
963
|
-
messages.scrollTop = messages.scrollHeight;
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
async function saveGeminiKey() {
|
|
968
|
-
var input = document.getElementById('geminiKeyInput');
|
|
969
|
-
var val = (input.value || '').trim();
|
|
970
|
-
if (!val) return;
|
|
971
|
-
var oldPlaceholder = input.placeholder;
|
|
972
|
-
input.disabled = true;
|
|
973
|
-
input.placeholder = 'Saving...';
|
|
974
|
-
try {
|
|
975
|
-
var res = await fetch('/v1/settings/gemini-key', {
|
|
976
|
-
method: 'POST',
|
|
977
|
-
headers: getHeaders(),
|
|
978
|
-
body: JSON.stringify({ key: val })
|
|
979
|
-
});
|
|
980
|
-
var data = await res.json();
|
|
981
|
-
if (data && data.ok) {
|
|
982
|
-
input.value = '';
|
|
983
|
-
input.placeholder = 'โ Key saved to .env';
|
|
984
|
-
setTimeout(function() {
|
|
985
|
-
document.getElementById('chatHint').innerHTML = '<span style="color:var(--green)">โ Key validated. Hybrid (Perplexity/Gemini) supported for chat with your data.</span>';
|
|
986
|
-
}, 2000);
|
|
987
|
-
} else {
|
|
988
|
-
input.placeholder = 'โ Failed: ' + (data.message || data.error || 'Unknown error');
|
|
989
|
-
}
|
|
990
|
-
} catch (e) {
|
|
991
|
-
input.placeholder = 'โ Network error';
|
|
992
|
-
} finally {
|
|
993
|
-
input.disabled = false;
|
|
994
|
-
setTimeout(function() { if (input.placeholder.startsWith('โ')) input.placeholder = oldPlaceholder; }, 3000);
|
|
995
|
-
}
|
|
552
|
+
return { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' };
|
|
996
553
|
}
|
|
997
554
|
|
|
998
555
|
function hasBootstrapKey() {
|
|
999
556
|
return LOCAL_PRO_BOOTSTRAP && Boolean(BOOTSTRAP_API_KEY);
|
|
1000
557
|
}
|
|
1001
558
|
|
|
1002
|
-
async function checkLocalLlmReachable(url) {
|
|
1003
|
-
try {
|
|
1004
|
-
var controller = new AbortController();
|
|
1005
|
-
var timeout = setTimeout(function() { controller.abort(); }, 400);
|
|
1006
|
-
// Use no-cors mode to bypass CORS block (only care if network request succeeds/fails)
|
|
1007
|
-
await fetch(url, { method: 'GET', signal: controller.signal, mode: 'no-cors' });
|
|
1008
|
-
clearTimeout(timeout);
|
|
1009
|
-
return true;
|
|
1010
|
-
} catch (e) {
|
|
1011
|
-
return false;
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
function disconnectKey() {
|
|
1016
|
-
API_KEY = '';
|
|
1017
|
-
isDemo = false;
|
|
1018
|
-
var input = document.getElementById('apiKey');
|
|
1019
|
-
input.value = '';
|
|
1020
|
-
input.disabled = false;
|
|
1021
|
-
input.placeholder = "Paste your API key here (THUMBGATE_API_KEY or THUMBGATE_PRO_KEY)...";
|
|
1022
|
-
var btn = document.getElementById('connectBtn');
|
|
1023
|
-
btn.disabled = false;
|
|
1024
|
-
document.getElementById('demoBtn').style.display = 'inline-block';
|
|
1025
|
-
document.getElementById('disconnectBtn').style.display = 'none';
|
|
1026
|
-
document.getElementById('authStatus').className = 'auth-status';
|
|
1027
|
-
document.getElementById('authStatus').textContent = 'Disconnected';
|
|
1028
|
-
document.getElementById('authTitle').textContent = 'Connect your API key';
|
|
1029
|
-
document.getElementById('authHelp').innerHTML = 'Pro auto-connects locally when you launch with <code style="font-family:var(--mono);font-size:12px;background:var(--cyan-dim);color:var(--cyan);padding:2px 6px;border-radius:4px;">npx thumbgate pro</code>. Otherwise paste your <code style="font-family:var(--mono);font-size:12px;background:var(--cyan-dim);color:var(--cyan);padding:2px 6px;border-radius:4px;">THUMBGATE_API_KEY</code> manually.';
|
|
1030
|
-
document.getElementById('dashboardContent').style.display = 'none';
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
559
|
async function connect(options) {
|
|
1034
560
|
var opts = options || {};
|
|
1035
561
|
var input = document.getElementById('apiKey');
|
|
1036
562
|
API_KEY = String(opts.key || input.value || '').trim();
|
|
1037
563
|
if (!API_KEY) return;
|
|
1038
|
-
|
|
1039
|
-
var isEnterprise = API_KEY.startsWith('tg_op_') || API_KEY.startsWith('tg_creator_');
|
|
1040
|
-
var tierName = isEnterprise ? 'Enterprise' : 'Pro';
|
|
1041
|
-
|
|
1042
564
|
const status = document.getElementById('authStatus');
|
|
1043
565
|
const btn = document.getElementById('connectBtn');
|
|
1044
566
|
btn.disabled = true;
|
|
1045
567
|
status.className = 'auth-status';
|
|
1046
|
-
status.textContent = opts.localPro ?
|
|
568
|
+
status.textContent = opts.localPro ? 'Connecting local dashboard...' : 'Connecting...';
|
|
1047
569
|
try {
|
|
1048
570
|
const res = await fetch('/v1/feedback/stats', { headers: getHeaders() });
|
|
1049
571
|
if (!res.ok) throw new Error('Invalid API key');
|
|
1050
572
|
const data = await res.json();
|
|
1051
|
-
|
|
1052
|
-
if (data.tier) {
|
|
1053
|
-
isEnterprise = (data.tier === 'Enterprise');
|
|
1054
|
-
tierName = data.tier;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
if (data.localLlmConfigured) {
|
|
1058
|
-
var modelLabel = data.localLlmModel ? ' (' + data.localLlmModel + ')' : '';
|
|
1059
|
-
var llmUrl = data.localLlmEndpoint || 'http://localhost:11434/v1';
|
|
1060
|
-
document.getElementById('chatHint').innerHTML = '<span style="color:var(--text-muted)">Checking local LLM reachability...</span>';
|
|
1061
|
-
checkLocalLlmReachable(llmUrl.replace(/\/v1$/, '')).then(function(reachable) {
|
|
1062
|
-
if (reachable) {
|
|
1063
|
-
document.getElementById('chatHint').innerHTML = '<span style="color:var(--green)">โ Local LLM' + modelLabel + ' ready. Chat answers are generated locally โ no cloud calls.</span>';
|
|
1064
|
-
} else {
|
|
1065
|
-
document.getElementById('chatHint').innerHTML = '<span style="color:#f59e0b">โ Ollama is offline. Launch it to chat locally: <code style="font-family:var(--mono);font-size:12px;background:rgba(245,158,11,0.1);color:#f59e0b;padding:2px 6px;border-radius:4px;">ollama run qwen2.5-coder:14b</code></span>';
|
|
1066
|
-
}
|
|
1067
|
-
});
|
|
1068
|
-
} else if (data.geminiKeyStatus === 'validated' || data.geminiValidatedAt) {
|
|
1069
|
-
var when = data.geminiValidatedAt ? ' (validated ' + new Date(data.geminiValidatedAt).toLocaleTimeString() + ')' : '';
|
|
1070
|
-
document.getElementById('chatHint').innerHTML = '<span style="color:var(--green)">โ Gemini API key validated' + when + '. You can now chat with your data.</span>';
|
|
1071
|
-
} else if (data.perplexityConfigured) {
|
|
1072
|
-
document.getElementById('chatHint').innerHTML = '<span style="color:var(--green)">โ Perplexity hybrid (local-cloud) key present. Supports hybrid inference for chat with your data.</span>';
|
|
1073
|
-
} else if (data.geminiConfigured) {
|
|
1074
|
-
document.getElementById('chatHint').innerHTML = '<span style="color:#f59e0b">โ Gemini key present in .env (click Save below to validate it). Perplexity hybrid also supported for cost/privacy.</span>';
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
573
|
status.className = 'auth-status ok';
|
|
1078
|
-
status.textContent = opts.localPro ?
|
|
1079
|
-
document.getElementById('disconnectBtn').style.display = 'inline-block';
|
|
1080
|
-
const titleEl = document.getElementById('authTitle');
|
|
1081
|
-
if (titleEl) {
|
|
1082
|
-
titleEl.textContent = opts.localPro ? `${tierName} License Active` : 'API Key Connected';
|
|
1083
|
-
}
|
|
574
|
+
status.textContent = opts.localPro ? 'โ Local Pro dashboard connected' : 'โ Connected';
|
|
1084
575
|
document.getElementById('dashboardContent').style.display = 'block';
|
|
1085
576
|
if (opts.localPro) {
|
|
1086
|
-
const localProActiveMessage = 'Local Pro is active on this machine. Your dashboard is using the saved license key automatically.';
|
|
1087
|
-
const localEnterpriseActiveMessage = 'Local Enterprise is active on this machine. Your dashboard is using the saved license key automatically. Click Disconnect to switch keys.';
|
|
1088
577
|
input.value = 'local-license';
|
|
1089
578
|
input.disabled = true;
|
|
1090
|
-
input.placeholder =
|
|
579
|
+
input.placeholder = 'Local Pro auto-connected';
|
|
1091
580
|
btn.disabled = true;
|
|
1092
581
|
document.getElementById('demoBtn').style.display = 'none';
|
|
1093
|
-
document.getElementById('authHelp').textContent =
|
|
582
|
+
document.getElementById('authHelp').textContent = 'Local Pro is active on this machine. Your personal dashboard is using the saved license key automatically.';
|
|
1094
583
|
}
|
|
1095
584
|
renderStats(data);
|
|
1096
585
|
setSelectedCard('all');
|
|
@@ -1102,12 +591,8 @@ async function connect(options) {
|
|
|
1102
591
|
} catch (e) {
|
|
1103
592
|
status.className = 'auth-status err';
|
|
1104
593
|
status.textContent = 'โ ' + e.message;
|
|
1105
|
-
const titleEl = document.getElementById('authTitle');
|
|
1106
|
-
if (titleEl) {
|
|
1107
|
-
titleEl.textContent = 'Connect your API key';
|
|
1108
|
-
}
|
|
1109
594
|
if (opts.localPro) {
|
|
1110
|
-
document.getElementById('authHelp').textContent =
|
|
595
|
+
document.getElementById('authHelp').textContent = 'Local Pro bootstrap failed. Paste your THUMBGATE_API_KEY manually or retry the local launcher.';
|
|
1111
596
|
}
|
|
1112
597
|
}
|
|
1113
598
|
if (!opts.localPro) {
|
|
@@ -1171,88 +656,24 @@ async function search() {
|
|
|
1171
656
|
}
|
|
1172
657
|
|
|
1173
658
|
function renderResult(r) {
|
|
1174
|
-
|
|
1175
|
-
let signal = r.signal || r.type || '';
|
|
1176
|
-
|
|
1177
|
-
if (contextText && contextText.trim().startsWith('{')) {
|
|
1178
|
-
const originalText = contextText;
|
|
1179
|
-
try {
|
|
1180
|
-
let cleanText = contextText.trim();
|
|
1181
|
-
if (cleanText.endsWith('โฆ')) {
|
|
1182
|
-
cleanText = cleanText.slice(0, -1);
|
|
1183
|
-
}
|
|
1184
|
-
if (cleanText.endsWith(',...')) {
|
|
1185
|
-
cleanText = cleanText.slice(0, -4);
|
|
1186
|
-
}
|
|
1187
|
-
let parsed = null;
|
|
1188
|
-
try {
|
|
1189
|
-
parsed = JSON.parse(cleanText);
|
|
1190
|
-
} catch (_) {
|
|
1191
|
-
try { parsed = JSON.parse(cleanText + '"}'); } catch (_) {}
|
|
1192
|
-
try { parsed = JSON.parse(cleanText + '}'); } catch (_) {}
|
|
1193
|
-
try { parsed = JSON.parse(cleanText + '"} }'); } catch (_) {}
|
|
1194
|
-
}
|
|
1195
|
-
if (parsed && (parsed.context || parsed.text || parsed.content || parsed.message)) {
|
|
1196
|
-
contextText = parsed.context || parsed.text || parsed.content || parsed.message;
|
|
1197
|
-
} else {
|
|
1198
|
-
let contextMatch = originalText.match(/"context"\s*:\s*"((?:[^"\\]|\\.)*?)"/);
|
|
1199
|
-
if (!contextMatch) {
|
|
1200
|
-
contextMatch = originalText.match(/"context"\s*:\s*"((?:[^"\\]|\\.)*)/);
|
|
1201
|
-
}
|
|
1202
|
-
if (!contextMatch || !contextMatch[1]) {
|
|
1203
|
-
let otherMatch = originalText.match(/"(?:text|content|message)"\s*:\s*"((?:[^"\\]|\\.)*?)"/);
|
|
1204
|
-
if (!otherMatch) {
|
|
1205
|
-
otherMatch = originalText.match(/"(?:text|content|message)"\s*:\s*"((?:[^"\\]|\\.)*)/);
|
|
1206
|
-
}
|
|
1207
|
-
if (otherMatch && otherMatch[1]) {
|
|
1208
|
-
contextMatch = otherMatch;
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
if (contextMatch && contextMatch[1]) {
|
|
1212
|
-
let val = contextMatch[1];
|
|
1213
|
-
val = val.replace(/(?:"\s*,\s*"[^"]*"\s*:?|"\s*,\s*|"\s*\}?\s*|\s*,\s*|\s*\}?\s*)$/, '');
|
|
1214
|
-
val = val.replace(/(?:\.\.\.|โฆ|",\.\.\.|",โฆ|"\s*,\s*\.\.\.|"\s*,\s*โฆ)$/, '');
|
|
1215
|
-
contextText = val.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
if (parsed && parsed.signal) {
|
|
1219
|
-
signal = parsed.signal;
|
|
1220
|
-
} else {
|
|
1221
|
-
const signalMatch = originalText.match(/"signal"\s*:\s*"([^"]+)"/);
|
|
1222
|
-
if (signalMatch && !signal) {
|
|
1223
|
-
signal = signalMatch[1];
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
} catch (_) {}
|
|
1227
|
-
}
|
|
1228
|
-
|
|
659
|
+
const signal = r.signal || r.type || '';
|
|
1229
660
|
const isUp = signal === 'up' || signal === 'positive' || signal === 'thumbs_up';
|
|
1230
661
|
const isDown = signal === 'down' || signal === 'negative' || signal === 'thumbs_down';
|
|
1231
662
|
const signalClass = isUp ? 'up' : isDown ? 'down' : '';
|
|
1232
663
|
const signalLabel = isUp ? '๐ Positive' : isDown ? '๐ Negative' : signal;
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
if (displayTitle && displayTitle.startsWith('feedback_')) {
|
|
1236
|
-
const parts = displayTitle.split('_');
|
|
1237
|
-
if (parts.length >= 3) {
|
|
1238
|
-
const type = parts[1];
|
|
1239
|
-
const id = parts.slice(2).join('_');
|
|
1240
|
-
displayTitle = (type === 'positive' ? '๐' : '๐') + ' Feedback (' + id + ')';
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
|
|
664
|
+
const title = r.title || r.context || r.text || '';
|
|
665
|
+
const context = r.context || r.text || r.content || '';
|
|
1244
666
|
const tags = r.tags || [];
|
|
1245
667
|
const date = r.timestamp ? new Date(r.timestamp).toLocaleDateString() : '';
|
|
1246
|
-
|
|
1247
668
|
return '<div class="result-card">' +
|
|
1248
669
|
'<div class="result-header">' +
|
|
1249
670
|
(signalLabel ? '<span class="result-signal ' + signalClass + '">' + signalLabel + '</span>' : '<span></span>') +
|
|
1250
671
|
'<span class="result-date">' + date + '</span>' +
|
|
1251
672
|
'</div>' +
|
|
1252
|
-
(
|
|
1253
|
-
(
|
|
673
|
+
(title ? '<div class="result-title">' + highlightText(title) + '</div>' : '') +
|
|
674
|
+
(context && context !== title ? '<div class="result-context">' + highlightText(context.slice(0, 300)) + '</div>' : '') +
|
|
1254
675
|
(tags.length ? '<div class="result-tags">' + tags.map(function(t) {
|
|
1255
|
-
return '<button type="button" class="tag" data-tag="' + encodeURIComponent(String(t)) + '" title="' + escAttr('Search for ' + t) + '">' +
|
|
676
|
+
return '<button type="button" class="tag" data-tag="' + encodeURIComponent(String(t)) + '" title="' + escAttr('Search for ' + t) + '">' + escHtml(t) + '</button>';
|
|
1256
677
|
}).join('') + '</div>' : '') +
|
|
1257
678
|
'</div>';
|
|
1258
679
|
}
|
|
@@ -1329,23 +750,10 @@ function setSource(el, source) {
|
|
|
1329
750
|
function switchTab(name) {
|
|
1330
751
|
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
|
|
1331
752
|
document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); });
|
|
1332
|
-
|
|
1333
|
-
// [onclick*="<name>"] also matched the stat-cards (which carry onclick
|
|
1334
|
-
// attributes like selectCard(this,'gates')), and a stat-card appears
|
|
1335
|
-
// before the tab header in DOM order, so for 'gates' the wrong element
|
|
1336
|
-
// (the card) got the .active class and the tab header stayed dormant.
|
|
1337
|
-
var tabEl = document.querySelector('.tab[onclick*="' + name + '"]');
|
|
753
|
+
var tabEl = document.querySelector('[onclick*="' + name + '"]');
|
|
1338
754
|
var contentEl = document.getElementById('tab-' + name);
|
|
1339
755
|
if (tabEl) tabEl.classList.add('active');
|
|
1340
|
-
if (contentEl)
|
|
1341
|
-
contentEl.classList.add('active');
|
|
1342
|
-
// Stat-card clicks fire switchTab from above the fold; without this scroll
|
|
1343
|
-
// the user sees "nothing happen" because the just-activated content sits
|
|
1344
|
-
// below the viewport. Same class of bug as the /lessons tile fix in #2268.
|
|
1345
|
-
try {
|
|
1346
|
-
contentEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1347
|
-
} catch (_e) { /* older browsers without smooth-scroll: no-op */ }
|
|
1348
|
-
}
|
|
756
|
+
if (contentEl) contentEl.classList.add('active');
|
|
1349
757
|
// Sync URL hash so deep-links stay shareable without scroll jump
|
|
1350
758
|
try {
|
|
1351
759
|
if (('#' + name) !== window.location.hash) {
|
|
@@ -1357,11 +765,11 @@ function switchTab(name) {
|
|
|
1357
765
|
/**
|
|
1358
766
|
* Resolve deep-link tab target from URL hash or query string.
|
|
1359
767
|
* Supports: /dashboard#insights, /dashboard?tab=gates, /dashboard#tab-export.
|
|
1360
|
-
* Valid targets match tab-content ids (search, gates, team,
|
|
1361
|
-
*
|
|
768
|
+
* Valid targets match tab-content ids (search, gates, team, generated,
|
|
769
|
+
* settings, templates, insights, export).
|
|
1362
770
|
*/
|
|
1363
771
|
function getDeepLinkTab() {
|
|
1364
|
-
var valid = ['search', 'gates', 'team', '
|
|
772
|
+
var valid = ['search', 'gates', 'team', 'generated', 'settings', 'templates', 'insights', 'export'];
|
|
1365
773
|
var raw = (window.location.hash || '').replace(/^#/, '').replace(/^tab-/, '');
|
|
1366
774
|
if (!raw) {
|
|
1367
775
|
try {
|
|
@@ -1528,14 +936,9 @@ async function markReviewed() {
|
|
|
1528
936
|
try {
|
|
1529
937
|
var res = await fetch('/v1/dashboard/review-state', {
|
|
1530
938
|
method: 'POST',
|
|
1531
|
-
headers: getHeaders()
|
|
1532
|
-
body: JSON.stringify({ reviewedAt: new Date().toISOString() })
|
|
939
|
+
headers: getHeaders()
|
|
1533
940
|
});
|
|
1534
|
-
if (!res.ok)
|
|
1535
|
-
var errText = '';
|
|
1536
|
-
try { errText = await res.text(); } catch (_) { errText = ''; }
|
|
1537
|
-
throw new Error(errText || 'Failed to save review checkpoint');
|
|
1538
|
-
}
|
|
941
|
+
if (!res.ok) throw new Error('Failed to save review checkpoint');
|
|
1539
942
|
var body = await res.json();
|
|
1540
943
|
renderReviewDelta(body.reviewDelta || {});
|
|
1541
944
|
} catch (e) {
|
|
@@ -1573,68 +976,6 @@ function renderDashboardData(data) {
|
|
|
1573
976
|
renderRegulatedProof(data.regulatedProof || {});
|
|
1574
977
|
renderTemplates(data.templateLibrary || {});
|
|
1575
978
|
renderInsights(data);
|
|
1576
|
-
loadAiInventory();
|
|
1577
|
-
loadEnterpriseDataChatStatus();
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
async function loadAiInventory() {
|
|
1581
|
-
var summaryEl = document.getElementById('aiInventorySummaryCards');
|
|
1582
|
-
var componentsEl = document.getElementById('aiInventoryComponents');
|
|
1583
|
-
var exportEl = document.getElementById('aiInventoryExportNote');
|
|
1584
|
-
if (!summaryEl || !componentsEl) return;
|
|
1585
|
-
|
|
1586
|
-
try {
|
|
1587
|
-
var res = await fetch('/v1/dashboard/ai-inventory', { headers: getHeaders() });
|
|
1588
|
-
if (!res.ok) {
|
|
1589
|
-
var text = await res.text();
|
|
1590
|
-
throw new Error(text || 'Failed to load AI inventory');
|
|
1591
|
-
}
|
|
1592
|
-
var inventory = await res.json();
|
|
1593
|
-
renderAiInventory(inventory);
|
|
1594
|
-
} catch (e) {
|
|
1595
|
-
var message = e && e.message ? e.message : 'Failed to load AI inventory';
|
|
1596
|
-
setMessageState(summaryEl, 'empty', message);
|
|
1597
|
-
setMessageState(componentsEl, 'empty', message);
|
|
1598
|
-
if (exportEl) exportEl.textContent = message;
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
function renderAiInventory(inventory) {
|
|
1603
|
-
var summaryEl = document.getElementById('aiInventorySummaryCards');
|
|
1604
|
-
var componentsEl = document.getElementById('aiInventoryComponents');
|
|
1605
|
-
var exportEl = document.getElementById('aiInventoryExportNote');
|
|
1606
|
-
var components = Array.isArray(inventory.components) ? inventory.components : [];
|
|
1607
|
-
var byCategory = inventory.summary && inventory.summary.byCategory ? inventory.summary.byCategory : {};
|
|
1608
|
-
var modelArtifacts = byCategory.model_artifact || 0;
|
|
1609
|
-
var vectorDbs = byCategory.vector_database || 0;
|
|
1610
|
-
|
|
1611
|
-
summaryEl.innerHTML = [
|
|
1612
|
-
'<div class="team-card"><div class="team-kicker">Files scanned</div><div class="team-value">' + escHtml(String(inventory.filesScanned || 0)) + '</div><div class="team-note">source, manifests, model artifacts</div></div>',
|
|
1613
|
-
'<div class="team-card"><div class="team-kicker">AI components</div><div class="team-value">' + escHtml(String(inventory.componentCount || components.length)) + '</div><div class="team-note">inventory evidence items</div></div>',
|
|
1614
|
-
'<div class="team-card"><div class="team-kicker">Vector stores</div><div class="team-value">' + escHtml(String(vectorDbs)) + '</div><div class="team-note">retrieval governance surface</div></div>',
|
|
1615
|
-
'<div class="team-card"><div class="team-kicker">Model artifacts</div><div class="team-value">' + escHtml(String(modelArtifacts)) + '</div><div class="team-note">local ML-BOM artifacts</div></div>'
|
|
1616
|
-
].join('');
|
|
1617
|
-
|
|
1618
|
-
if (!components.length) {
|
|
1619
|
-
componentsEl.innerHTML = '<div class="empty">No AI/ML components detected in this project root.</div>';
|
|
1620
|
-
} else {
|
|
1621
|
-
componentsEl.innerHTML = components.map(function(item) {
|
|
1622
|
-
var evidence = Array.isArray(item.evidence) ? item.evidence.slice(0, 4) : [];
|
|
1623
|
-
var evidenceHtml = evidence.map(function(e) {
|
|
1624
|
-
var loc = e.line ? (e.file + ':' + e.line) : e.file;
|
|
1625
|
-
return '<li><code>' + escHtml(loc || 'unknown') + '</code> <span style="color:var(--text-muted);">[' + escHtml(e.kind || 'evidence') + ']</span></li>';
|
|
1626
|
-
}).join('');
|
|
1627
|
-
return '<div class="tool-card">' +
|
|
1628
|
-
'<div class="tool-title">' + escHtml(item.name || item.id || 'AI component') + '</div>' +
|
|
1629
|
-
'<div class="tool-meta">' + escHtml(item.category || 'unknown') + ' ยท ' + escHtml(item.ecosystem || 'unknown') + '</div>' +
|
|
1630
|
-
'<ul style="margin:8px 0 0 18px;padding:0;">' + evidenceHtml + '</ul>' +
|
|
1631
|
-
'</div>';
|
|
1632
|
-
}).join('');
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
if (exportEl) {
|
|
1636
|
-
exportEl.textContent = 'Ready: export ' + String(inventory.componentCount || components.length) + ' component(s) as CycloneDX ML-BOM evidence.';
|
|
1637
|
-
}
|
|
1638
979
|
}
|
|
1639
980
|
|
|
1640
981
|
function renderGeneratedViewToolbar(spec) {
|
|
@@ -2074,108 +1415,6 @@ function renderTemplates(templateLibrary) {
|
|
|
2074
1415
|
: '<div class="empty">No check templates available</div>';
|
|
2075
1416
|
}
|
|
2076
1417
|
|
|
2077
|
-
function renderEnterpriseStatus(status) {
|
|
2078
|
-
var target = document.getElementById('enterpriseStatusCards');
|
|
2079
|
-
if (!target) return;
|
|
2080
|
-
var vertex = status && status.vertex ? status.vertex : {};
|
|
2081
|
-
var dfcx = status && status.dfcx ? status.dfcx : {};
|
|
2082
|
-
var localLlm = status && status.chat ? status.chat.localLlmEndpointConfigured : false;
|
|
2083
|
-
var connectionMode = localLlm
|
|
2084
|
-
? 'Local/open-source LLM configured'
|
|
2085
|
-
: (dfcx.liveAgentConfigured ? 'Google agent guard configured' : (vertex.configured ? 'Vertex routing configured' : 'Local deterministic fallback'));
|
|
2086
|
-
var rows = [
|
|
2087
|
-
{ name: 'Chat runtime', value: connectionMode, note: localLlm ? 'Using THUMBGATE_LOCAL_LLM_ENDPOINT for local/open-source inference.' : 'Local deterministic summaries work without cloud credentials; configure a local LLM for generated answers.' },
|
|
2088
|
-
{ name: 'Data boundary', value: 'Local by default', note: 'Dashboard answers are grounded in local ThumbGate data and do not require Google Cloud.' },
|
|
2089
|
-
{ name: 'Vertex adapter', value: vertex.configured ? 'Configured' : 'Optional', note: vertex.projectId ? ('Project: ' + vertex.projectId + ' ยท ' + (vertex.location || '')) : 'Only needed when the buyer chooses Google-hosted routing.' },
|
|
2090
|
-
{ name: 'Dialogflow guard adapter', value: dfcx.liveAgentConfigured ? 'Configured' : 'Optional', note: dfcx.verification || 'Only needed for customer-owned Dialogflow CX agent deployments.' },
|
|
2091
|
-
{ name: 'Legacy gcloud CX command', value: 'Unsupported', note: 'Do not use the old alpha gcloud CX command group; use REST API or console for DFCX proof.' }
|
|
2092
|
-
];
|
|
2093
|
-
|
|
2094
|
-
function draw() {
|
|
2095
|
-
target.innerHTML = rows.map(function(row) {
|
|
2096
|
-
return '<div class="inventory-row"><div><div class="inventory-name">' + escHtml(row.name) + '</div><div class="inventory-subtitle">' + escHtml(row.note) + '</div></div><span class="remediation-action">' + escHtml(row.value) + '</span></div>';
|
|
2097
|
-
}).join('');
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
draw();
|
|
2101
|
-
|
|
2102
|
-
if (localLlm) {
|
|
2103
|
-
var llmUrl = 'http://localhost:11434/v1';
|
|
2104
|
-
checkLocalLlmReachable(llmUrl).then(function(reachable) {
|
|
2105
|
-
if (reachable) {
|
|
2106
|
-
rows[0].value = 'โ Local LLM Online';
|
|
2107
|
-
draw();
|
|
2108
|
-
} else {
|
|
2109
|
-
rows[0].value = 'โ Local LLM Offline';
|
|
2110
|
-
rows[0].note = 'Ollama is offline. Launch it to chat locally: ollama run qwen2.5-coder:14b';
|
|
2111
|
-
draw();
|
|
2112
|
-
}
|
|
2113
|
-
});
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
|
|
2117
|
-
async function loadEnterpriseDataChatStatus() {
|
|
2118
|
-
if (!API_KEY || isDemo) {
|
|
2119
|
-
renderEnterpriseStatus({ vertex: {}, dfcx: { verification: 'Connect to load local status.' } });
|
|
2120
|
-
return;
|
|
2121
|
-
}
|
|
2122
|
-
try {
|
|
2123
|
-
var res = await fetch('/v1/enterprise/data-chat/status', { headers: getHeaders() });
|
|
2124
|
-
if (!res.ok) throw new Error('status unavailable');
|
|
2125
|
-
renderEnterpriseStatus(await res.json());
|
|
2126
|
-
} catch (err) {
|
|
2127
|
-
var target = document.getElementById('enterpriseStatusCards');
|
|
2128
|
-
if (target) target.innerHTML = '<div class="empty">Enterprise status unavailable.</div>';
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
function setEnterprisePrompt(prompt) {
|
|
2133
|
-
var input = document.getElementById('enterpriseChatPrompt');
|
|
2134
|
-
if (input) input.value = prompt;
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
async function sendEnterpriseChat() {
|
|
2138
|
-
var input = document.getElementById('enterpriseChatPrompt');
|
|
2139
|
-
var answer = document.getElementById('enterpriseChatAnswer');
|
|
2140
|
-
var sources = document.getElementById('enterpriseChatSources');
|
|
2141
|
-
var button = document.getElementById('enterpriseChatBtn');
|
|
2142
|
-
var prompt = input ? input.value.trim() : '';
|
|
2143
|
-
if (!prompt) {
|
|
2144
|
-
answer.textContent = 'Ask a question first.';
|
|
2145
|
-
return;
|
|
2146
|
-
}
|
|
2147
|
-
if (!API_KEY) {
|
|
2148
|
-
answer.textContent = 'Connect your dashboard before using Enterprise Chat.';
|
|
2149
|
-
return;
|
|
2150
|
-
}
|
|
2151
|
-
button.disabled = true;
|
|
2152
|
-
answer.className = 'enterprise-answer';
|
|
2153
|
-
answer.textContent = 'Local RAG (LanceDB + lessons) plus your LLM...';
|
|
2154
|
-
sources.innerHTML = '';
|
|
2155
|
-
try {
|
|
2156
|
-
var res = await fetch('/v1/chat', {
|
|
2157
|
-
method: 'POST',
|
|
2158
|
-
headers: getHeaders(),
|
|
2159
|
-
body: JSON.stringify({ question: prompt })
|
|
2160
|
-
});
|
|
2161
|
-
var data = await res.json();
|
|
2162
|
-
if (!res.ok) throw new Error(data.detail || data.error || data.message || 'chat failed');
|
|
2163
|
-
answer.className = 'enterprise-answer';
|
|
2164
|
-
answer.textContent = data.answer || data.message || 'No answer returned.';
|
|
2165
|
-
var list = Array.isArray(data.sources) ? data.sources : [];
|
|
2166
|
-
sources.innerHTML = list.map(function(source) {
|
|
2167
|
-
var label = typeof source === 'string' ? source : (source && (source.title || source.id || ''));
|
|
2168
|
-
return '<span class="enterprise-source">' + escHtml(label) + '</span>';
|
|
2169
|
-
}).join('');
|
|
2170
|
-
loadEnterpriseDataChatStatus();
|
|
2171
|
-
} catch (err) {
|
|
2172
|
-
answer.className = 'enterprise-answer blocked';
|
|
2173
|
-
answer.textContent = err.message || 'Chat failed. Configure a local LLM endpoint or API key for RAG.';
|
|
2174
|
-
} finally {
|
|
2175
|
-
button.disabled = false;
|
|
2176
|
-
}
|
|
2177
|
-
}
|
|
2178
|
-
|
|
2179
1418
|
document.addEventListener('click', function(event) {
|
|
2180
1419
|
var tagButton = event.target.closest('.tag[data-tag]');
|
|
2181
1420
|
if (!tagButton) return;
|
|
@@ -2189,65 +1428,71 @@ function loadDemo() {
|
|
|
2189
1428
|
// Demo badge in nav
|
|
2190
1429
|
var logo = document.querySelector('.nav-logo');
|
|
2191
1430
|
if (!document.getElementById('demoBadge')) {
|
|
2192
|
-
logo.insertAdjacentHTML('afterend', '<span class="demo-badge" id="demoBadge">Demo Mode
|
|
1431
|
+
logo.insertAdjacentHTML('afterend', '<span class="demo-badge" id="demoBadge">Demo Mode</span>');
|
|
2193
1432
|
}
|
|
2194
1433
|
// Stats
|
|
2195
|
-
document.getElementById('statTotal').textContent = '
|
|
2196
|
-
document.getElementById('statPositive').textContent = '
|
|
2197
|
-
document.getElementById('statNegative').textContent = '
|
|
2198
|
-
document.getElementById('statGates').textContent = '
|
|
1434
|
+
document.getElementById('statTotal').textContent = '297';
|
|
1435
|
+
document.getElementById('statPositive').textContent = '37';
|
|
1436
|
+
document.getElementById('statNegative').textContent = '260';
|
|
1437
|
+
document.getElementById('statGates').textContent = '21';
|
|
2199
1438
|
setSelectedCard('all');
|
|
2200
|
-
|
|
1439
|
+
// Demo: 260 negative-feedback events have produced ~180 actual gate blocks
|
|
1440
|
+
// and ~85 bot deflections on /checkout/pro. Kept as illustrative defaults
|
|
1441
|
+
// so the savings card never reads "0" in demo mode.
|
|
1442
|
+
var demoSavings = TG_TOKEN_SAVINGS.compute(180, 85);
|
|
2201
1443
|
var tEl = document.getElementById('statTokensSaved');
|
|
2202
1444
|
var dEl = document.getElementById('statDollarsSaved');
|
|
2203
1445
|
if (tEl) tEl.textContent = demoSavings.tokensSavedDisplay;
|
|
2204
1446
|
if (dEl) dEl.textContent = 'โ ' + demoSavings.dollarsSavedDisplay + ' saved';
|
|
2205
1447
|
renderTopBlockedGates([
|
|
2206
|
-
{ name: 'gate:
|
|
2207
|
-
{ name: 'gate:
|
|
2208
|
-
{ name: 'gate:
|
|
2209
|
-
{ name: 'gate:
|
|
2210
|
-
{ name: 'gate:
|
|
2211
|
-
{ name: 'gate:
|
|
1448
|
+
{ name: 'gate:no-force-push', blocked: 47 },
|
|
1449
|
+
{ name: 'gate:no-drop-prod', blocked: 31 },
|
|
1450
|
+
{ name: 'gate:no-hallucinated-import', blocked: 28 },
|
|
1451
|
+
{ name: 'gate:no-secret-commit', blocked: 19 },
|
|
1452
|
+
{ name: 'gate:require-tests-before-done', blocked: 14 },
|
|
1453
|
+
{ name: 'gate:no-figma-claim-without-check', blocked: 9 },
|
|
2212
1454
|
]);
|
|
2213
1455
|
renderReviewDelta({
|
|
2214
1456
|
hasBaseline: true,
|
|
2215
|
-
reviewedAt: '
|
|
2216
|
-
previousHead: '
|
|
2217
|
-
currentHead: '
|
|
2218
|
-
feedbackAdded:
|
|
2219
|
-
negativeAdded:
|
|
2220
|
-
lessonsAdded:
|
|
2221
|
-
blocksAdded:
|
|
2222
|
-
warnsAdded:
|
|
2223
|
-
headline: 'Since your last review:
|
|
1457
|
+
reviewedAt: '2025-06-18T09:00:00Z',
|
|
1458
|
+
previousHead: '9f1a2c3d',
|
|
1459
|
+
currentHead: '5c7e9b1a',
|
|
1460
|
+
feedbackAdded: 6,
|
|
1461
|
+
negativeAdded: 4,
|
|
1462
|
+
lessonsAdded: 2,
|
|
1463
|
+
blocksAdded: 5,
|
|
1464
|
+
warnsAdded: 1,
|
|
1465
|
+
headline: 'Since your last review: 6 feedback events ยท 4 negative ยท 2 lessons ยท 5 gate blocks.',
|
|
2224
1466
|
latestFeedback: {
|
|
2225
|
-
title: '
|
|
2226
|
-
timestamp: '
|
|
1467
|
+
title: 'Claimed fix worked without running tests',
|
|
1468
|
+
timestamp: '2025-06-22T10:20:00Z'
|
|
2227
1469
|
},
|
|
2228
1470
|
latestLesson: {
|
|
2229
|
-
title: '
|
|
2230
|
-
timestamp: '
|
|
1471
|
+
title: 'MISTAKE: Claimed Figma compliance without visual verification',
|
|
1472
|
+
timestamp: '2025-06-21T08:10:00Z'
|
|
2231
1473
|
}
|
|
2232
1474
|
});
|
|
2233
1475
|
document.getElementById('reviewCheckpointBtn').disabled = true;
|
|
2234
1476
|
document.getElementById('reviewCheckpointBtn').textContent = 'Connect to save your own review checkpoint';
|
|
2235
|
-
// Sample memories โ
|
|
1477
|
+
// Sample memories โ realistic scenarios from real agent-driven development
|
|
2236
1478
|
var demoResults = [
|
|
2237
|
-
{ signal: 'down', title: '
|
|
2238
|
-
{ signal: '
|
|
2239
|
-
{ signal: '
|
|
2240
|
-
{ signal: '
|
|
2241
|
-
{ signal: '
|
|
2242
|
-
{ signal: '
|
|
2243
|
-
{ signal: 'down', title: '
|
|
2244
|
-
{ signal: 'up', title: '
|
|
2245
|
-
{ signal: 'down', title: '
|
|
2246
|
-
{ signal: 'up', title: '
|
|
2247
|
-
{ signal: 'down', title: '
|
|
2248
|
-
{ signal: 'up', title: '
|
|
2249
|
-
{ signal: '
|
|
2250
|
-
{ signal: 'down', title: '
|
|
1479
|
+
{ signal: 'down', title: 'Claimed fix worked without running tests', context: 'Agent announced "fixed and pushed" but never ran the test suite. CI failed on 3 tests. Check now requires test evidence before any completion claim.', tags: ['anti-lying', 'verification-gap', 'ci', 'trust-breach'], timestamp: '2025-06-22T10:20:00Z' },
|
|
1480
|
+
{ signal: 'down', title: 'Force-pushed to main and lost teammate commits', context: 'Used git push --force on main to fix a rebase. Overwrote 3 commits from another team member. Had to recover from reflog. Check now blocks all --force pushes to protected branches.', tags: ['git', 'destructive', 'auto-blocked'], timestamp: '2025-06-20T09:15:00Z' },
|
|
1481
|
+
{ signal: 'up', title: 'Pre-action check caught .env commit', context: 'Check blocked a git add that included .env with production API keys. Saved from leaking secrets to a public repo.', tags: ['security', 'prevention', 'secrets'], timestamp: '2025-06-19T11:30:00Z' },
|
|
1482
|
+
{ signal: 'down', title: 'PR scope creep โ 72 changed files for a 2-file fix', context: 'Agent included unrelated formatting changes, config files, and lock file updates in a PR that should have touched 2 files. User had to manually separate the changes.', tags: ['pr-scope', 'scope-creep', 'user-frustration', 'git-workflow'], timestamp: '2025-06-18T14:30:00Z' },
|
|
1483
|
+
{ signal: 'up', title: 'Full PR review cycle with on-device verification', context: 'Fixed 3 review findings, verified on physical Android device with fresh debug build, confirmed Delete Account flow end-to-end before marking PR ready.', tags: ['pr-review', 'on-device-verification', 'evidence-based', 'metric:ROI'], timestamp: '2025-06-17T16:45:00Z' },
|
|
1484
|
+
{ signal: 'down', title: 'Claimed Figma compliance without visual verification', context: 'Said UI matched Figma designs without actually checking. User pointed out wrong colors, missing spacing, and incorrect font weights. Should have used screenshot comparison.', tags: ['anti-lying', 'figma', 'visual-verification', 'trust-breach'], timestamp: '2025-06-16T08:05:00Z' },
|
|
1485
|
+
{ signal: 'down', title: 'Deployed without running tests', context: 'Pushed a refactor to main without running the test suite. Two integration tests failed in CI, blocking the release for 3 hours.', tags: ['ci', 'testing', 'deployment'], timestamp: '2025-06-15T09:15:00Z' },
|
|
1486
|
+
{ signal: 'up', title: 'Atomic commits improved bisect speed', context: 'Splitting a large feature into 8 atomic commits made git bisect find the regression in under 2 minutes instead of manual debugging.', tags: ['git', 'debugging', 'evidence-based'], timestamp: '2025-06-14T16:45:00Z' },
|
|
1487
|
+
{ signal: 'down', title: 'Deleted production database table', context: 'AI agent ran DROP TABLE on a staging-named table that was actually aliased to production. Added a gate requiring explicit confirmation for any destructive SQL.', tags: ['database', 'destructive', 'critical', 'auto-blocked'], timestamp: '2025-06-13T08:05:00Z' },
|
|
1488
|
+
{ signal: 'up', title: 'Read file before editing prevented breaking change', context: 'Always read the target file before making changes. Agent caught that the function signature had changed upstream and adapted the edit accordingly.', tags: ['code-quality', 'prevention', 'evidence-based'], timestamp: '2025-06-12T14:30:00Z' },
|
|
1489
|
+
{ signal: 'down', title: 'Pushed to wrong branch โ created new PR instead of updating existing', context: 'Agent pushed changes to a new branch instead of the open PR branch. Had to cherry-pick commits and force-close the duplicate PR.', tags: ['git-workflow', 'branch-strategy', 'delegation', 'user-frustration'], timestamp: '2025-06-11T10:20:00Z' },
|
|
1490
|
+
{ signal: 'up', title: 'ADO tickets split correctly with parallel execution', context: 'Split frontend/backend tasks, corrected area path and iteration, created investigation subtask, and updated PR description โ all done efficiently in parallel.', tags: ['ado-workflow', 'parallel-execution', 'triage', 'metric:ROI'], timestamp: '2025-06-10T13:00:00Z' },
|
|
1491
|
+
{ signal: 'down', title: 'Delegated debugging to QA instead of investigating', context: 'Told user to ask QA engineer for error logs instead of investigating the issue directly. Wasted 30 minutes of back-and-forth when the fix was a one-line config change.', tags: ['delegation', 'user-frustration', 'debugging', 'speed'], timestamp: '2025-06-09T15:30:00Z' },
|
|
1492
|
+
{ signal: 'down', title: 'iOS build failed โ forgot pod install after adding dependency', context: 'Added a new npm package but did not run pod install or rebuild the native project. Build failed on iOS physical device. Metro bundler showed stale cache.', tags: ['ios-build', 'metro', 'speed', 'mobile'], timestamp: '2025-06-08T09:45:00Z' },
|
|
1493
|
+
{ signal: 'up', title: 'Prevention rule stopped repeat mistake', context: 'Gate auto-generated from 3 prior "forgot pod install" failures now blocks any npm install that touches native modules without a follow-up pod install step.', tags: ['prevention', 'ios-build', 'auto-generated', 'metric:ROI'], timestamp: '2025-06-07T11:00:00Z' },
|
|
1494
|
+
{ signal: 'down', title: 'Claimed performance fix without benchmarking', context: 'Said the optimization reduced render time by 40% but never ran a before/after benchmark. Actual measurement showed no improvement. Gate now requires metrics evidence.', tags: ['anti-lying', 'performance', 'verification-gap', 'measure-first'], timestamp: '2025-06-06T14:20:00Z' },
|
|
1495
|
+
{ signal: 'up', title: 'DPO export caught systematic failure pattern', context: 'Exported 47 preference pairs revealing that 80% of negative feedback involved claiming work was done without verification. Led to a new mandatory evidence gate.', tags: ['dpo-export', 'evidence-based', 'prevention', 'metric:ROI'], timestamp: '2025-06-05T10:15:00Z' }
|
|
2251
1496
|
];
|
|
2252
1497
|
isDemo = true;
|
|
2253
1498
|
demoData = demoResults;
|
|
@@ -2261,14 +1506,14 @@ function loadDemo() {
|
|
|
2261
1506
|
'style="flex:none;background:#b85c2d;color:#fff;padding:8px 18px;border-radius:8px;text-decoration:none;font-weight:700;font-size:13px;white-space:nowrap;">Go Pro โ $19/mo</a>' +
|
|
2262
1507
|
'</div>';
|
|
2263
1508
|
document.getElementById('searchResults').innerHTML = upgradeBanner + teaserHtml;
|
|
2264
|
-
// Sample gates
|
|
1509
|
+
// Sample gates
|
|
2265
1510
|
var demoGates = [
|
|
2266
|
-
{ name: '
|
|
2267
|
-
{ name: '
|
|
2268
|
-
{ name: '
|
|
2269
|
-
{ name: '
|
|
2270
|
-
{ name: '
|
|
2271
|
-
{ name: '
|
|
1511
|
+
{ name: 'no-force-push', pattern: 'Block git push --force to main or master branches', action: 'block' },
|
|
1512
|
+
{ name: 'read-before-edit', pattern: 'Must read file contents before any edit_file call', action: 'block' },
|
|
1513
|
+
{ name: 'test-before-deploy', pattern: 'Run npm test before any deployment or push to main', action: 'block' },
|
|
1514
|
+
{ name: 'no-secrets-in-commits', pattern: 'Block git add on .env, credentials, or API key files', action: 'block' },
|
|
1515
|
+
{ name: 'evidence-before-claim', pattern: 'Run verification commands before claiming task completion', action: 'block' },
|
|
1516
|
+
{ name: 'atomic-commits', pattern: 'Each commit should contain a single logical change', action: 'warn' }
|
|
2272
1517
|
];
|
|
2273
1518
|
document.getElementById('gatesList').innerHTML = demoGates.map(function(g) {
|
|
2274
1519
|
return '<div class="gate-card"><div><div class="gate-name">' + escHtml(g.name) + '</div>' +
|
|
@@ -2276,144 +1521,141 @@ function loadDemo() {
|
|
|
2276
1521
|
'<span class="gate-action ' + g.action + '">' + g.action.toUpperCase() + '</span></div>';
|
|
2277
1522
|
}).join('');
|
|
2278
1523
|
renderTeam({
|
|
2279
|
-
totalAgents:
|
|
2280
|
-
activeAgents:
|
|
1524
|
+
totalAgents: 8,
|
|
1525
|
+
activeAgents: 5,
|
|
2281
1526
|
windowHours: 24,
|
|
2282
|
-
orgAdherenceRate:
|
|
1527
|
+
orgAdherenceRate: 92.4,
|
|
2283
1528
|
riskAgents: [
|
|
2284
|
-
{ id: '
|
|
2285
|
-
{ id: '
|
|
1529
|
+
{ id: 'claude-reviewer', project: 'checkout-flow', branch: 'fix/stripe-timeout', adherenceRate: 67.5 },
|
|
1530
|
+
{ id: 'codex-second-pass', project: 'dashboard', branch: 'feat/team-rollout', adherenceRate: 74.2 }
|
|
2286
1531
|
],
|
|
2287
1532
|
topBlockedGates: [
|
|
2288
|
-
{ gateId: '
|
|
2289
|
-
{ gateId: '
|
|
1533
|
+
{ gateId: 'evidence-before-done', blocked: 12, warned: 1 },
|
|
1534
|
+
{ gateId: 'never-force-push-main', blocked: 7, warned: 0 }
|
|
2290
1535
|
]
|
|
2291
1536
|
}, {
|
|
2292
1537
|
northStar: {
|
|
2293
|
-
weeklyTeamsRunningProofBackedWorkflows:
|
|
1538
|
+
weeklyTeamsRunningProofBackedWorkflows: 3,
|
|
2294
1539
|
paidTeamRuns: 1
|
|
2295
1540
|
}
|
|
2296
1541
|
});
|
|
2297
1542
|
renderPredictive({
|
|
2298
1543
|
upgradePropensity: {
|
|
2299
|
-
pro: { band: 'high', score: 0.
|
|
2300
|
-
team: { band: '
|
|
1544
|
+
pro: { band: 'high', score: 0.71 },
|
|
1545
|
+
team: { band: 'medium', score: 0.54 }
|
|
2301
1546
|
},
|
|
2302
1547
|
revenueForecast: {
|
|
2303
|
-
predictedBookedRevenueCents:
|
|
2304
|
-
incrementalOpportunityCents:
|
|
1548
|
+
predictedBookedRevenueCents: 12800,
|
|
1549
|
+
incrementalOpportunityCents: 4900
|
|
2305
1550
|
},
|
|
2306
1551
|
anomalySummary: {
|
|
2307
1552
|
count: 2,
|
|
2308
1553
|
severity: 'warning'
|
|
2309
1554
|
},
|
|
2310
1555
|
topCreators: [
|
|
2311
|
-
{ key: '
|
|
1556
|
+
{ key: 'reach_vb', opportunityRevenueCents: 3100 }
|
|
2312
1557
|
],
|
|
2313
1558
|
topSources: [
|
|
2314
|
-
{ key: '
|
|
1559
|
+
{ key: 'producthunt', opportunityRevenueCents: 1800 }
|
|
2315
1560
|
],
|
|
2316
1561
|
anomalies: [
|
|
2317
|
-
{ type: '
|
|
2318
|
-
{ type: '
|
|
1562
|
+
{ type: 'pricing_resistance', message: 'Price sensitivity dominates current loss reasons.', severity: 'warning' },
|
|
1563
|
+
{ type: 'creator_underperformance', message: 'Creator reach_vb is generating intent without revenue conversion.', severity: 'warning' }
|
|
2319
1564
|
]
|
|
2320
1565
|
});
|
|
2321
1566
|
renderAgentInventory({
|
|
2322
|
-
profile: '
|
|
2323
|
-
tier: '
|
|
2324
|
-
configuredServerCount:
|
|
2325
|
-
observedToolCount:
|
|
1567
|
+
profile: 'default',
|
|
1568
|
+
tier: 'builder',
|
|
1569
|
+
configuredServerCount: 3,
|
|
1570
|
+
observedToolCount: 4,
|
|
2326
1571
|
observedTools: [
|
|
2327
|
-
{ toolName: '
|
|
2328
|
-
{ toolName: '
|
|
2329
|
-
{ toolName: '
|
|
2330
|
-
{ toolName: 'schedule_consultation', evaluations: 28, intercepted: 3, allow: 25, deny: 0, warn: 3 },
|
|
2331
|
-
{ toolName: 'export_audit', evaluations: 19, intercepted: 0, allow: 19, deny: 0, warn: 0 }
|
|
1572
|
+
{ toolName: 'Bash', evaluations: 120, intercepted: 19, allow: 101, deny: 12, warn: 7 },
|
|
1573
|
+
{ toolName: 'Edit', evaluations: 64, intercepted: 6, allow: 58, deny: 2, warn: 4 },
|
|
1574
|
+
{ toolName: 'Read', evaluations: 42, intercepted: 0, allow: 42, deny: 0, warn: 0 }
|
|
2332
1575
|
],
|
|
2333
1576
|
policySources: [
|
|
2334
|
-
{ source: '
|
|
2335
|
-
{ source: '
|
|
2336
|
-
{ source: '
|
|
1577
|
+
{ source: 'gates-engine', count: 23 },
|
|
1578
|
+
{ source: 'secret-guard', count: 6 },
|
|
1579
|
+
{ source: 'workflow-sentinel', count: 4 }
|
|
2337
1580
|
]
|
|
2338
1581
|
});
|
|
2339
1582
|
renderActionableRemediations([
|
|
2340
1583
|
{
|
|
2341
|
-
title: '
|
|
2342
|
-
rationale: '
|
|
2343
|
-
action: '
|
|
1584
|
+
title: 'tighten verification before response ยท recent-signals',
|
|
1585
|
+
rationale: 'Recent approval rate has dropped below the lifetime baseline, so fast completion claims need tighter proof.',
|
|
1586
|
+
action: 'tighten-verification-before-response',
|
|
2344
1587
|
},
|
|
2345
1588
|
{
|
|
2346
|
-
title: '
|
|
2347
|
-
rationale: '
|
|
2348
|
-
action: '
|
|
1589
|
+
title: 'audit domain failures ยท git-workflow',
|
|
1590
|
+
rationale: 'Git workflow events are overrepresented in high-risk outcomes and deserve a stronger pre-action gate.',
|
|
1591
|
+
action: 'audit-domain-failures',
|
|
2349
1592
|
},
|
|
2350
1593
|
{
|
|
2351
|
-
title: '
|
|
2352
|
-
rationale: '
|
|
2353
|
-
action: '
|
|
1594
|
+
title: 'review and update skill ยท github',
|
|
1595
|
+
rationale: 'GitHub-related failures have crossed the negative-rate threshold, so the skill needs a correction pass.',
|
|
1596
|
+
action: 'review-and-update-skill',
|
|
2354
1597
|
}
|
|
2355
1598
|
]);
|
|
2356
1599
|
renderSettingsStatus({
|
|
2357
1600
|
activeLayers: [
|
|
2358
1601
|
{ scope: 'defaults', exists: true, leafCount: 7, sourcePath: null },
|
|
2359
|
-
{ scope: '
|
|
2360
|
-
{ scope: '
|
|
2361
|
-
{ scope: '
|
|
2362
|
-
{ scope: 'managed', exists: true, leafCount:
|
|
1602
|
+
{ scope: 'user', exists: true, leafCount: 2, sourcePath: '~/.thumbgate/settings.json' },
|
|
1603
|
+
{ scope: 'project', exists: true, leafCount: 2, sourcePath: '.thumbgate/settings.json' },
|
|
1604
|
+
{ scope: 'local', exists: false, leafCount: 0, sourcePath: '.thumbgate/settings.local.json' },
|
|
1605
|
+
{ scope: 'managed', exists: true, leafCount: 3, sourcePath: 'config/thumbgate-settings.managed.json' }
|
|
2363
1606
|
],
|
|
2364
1607
|
resolvedSettings: {
|
|
2365
|
-
|
|
1608
|
+
mcp: { defaultProfile: 'essential', readonlySessionProfile: 'readonly' },
|
|
2366
1609
|
harnesses: { enabled: true, allowRuntimeExecution: true }
|
|
2367
1610
|
},
|
|
2368
1611
|
routingPreview: {
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
1612
|
+
dashboardTool: { profile: 'readonly', reason: 'dashboard is available in the most restrictive profile that contains it' },
|
|
1613
|
+
defaultSession: { profile: 'essential', reason: 'default auto-routing โ essential profile from settings hierarchy' },
|
|
1614
|
+
reviewSession: { profile: 'readonly', reason: 'read-only session detected โ routing to readonly profile' }
|
|
2372
1615
|
},
|
|
2373
1616
|
origins: [
|
|
2374
|
-
{ path: '
|
|
2375
|
-
{ path: '
|
|
2376
|
-
{ path: '
|
|
2377
|
-
{ path: '
|
|
1617
|
+
{ path: 'mcp.defaultProfile', value: 'essential', scope: 'project', sourcePath: '.thumbgate/settings.json' },
|
|
1618
|
+
{ path: 'mcp.readonlySessionProfile', value: 'readonly', scope: 'project', sourcePath: '.thumbgate/settings.json' },
|
|
1619
|
+
{ path: 'harnesses.allowRuntimeExecution', value: true, scope: 'project', sourcePath: '.thumbgate/settings.json' },
|
|
1620
|
+
{ path: 'dashboard.showPolicyOrigins', value: true, scope: 'managed', sourcePath: 'config/thumbgate-settings.managed.json' }
|
|
2378
1621
|
]
|
|
2379
1622
|
});
|
|
2380
1623
|
renderTemplates({
|
|
2381
1624
|
total: 6,
|
|
2382
1625
|
categories: {
|
|
2383
|
-
'
|
|
2384
|
-
'
|
|
2385
|
-
'
|
|
2386
|
-
'
|
|
2387
|
-
'
|
|
1626
|
+
'Git Safety': 1,
|
|
1627
|
+
'Verification': 1,
|
|
1628
|
+
'Agent Honesty': 1,
|
|
1629
|
+
'Database Safety': 1,
|
|
1630
|
+
'Secrets Hygiene': 1,
|
|
1631
|
+
'Positive Reinforcement': 1
|
|
2388
1632
|
},
|
|
2389
1633
|
templates: [
|
|
2390
|
-
{ name: '
|
|
2391
|
-
{ name: '
|
|
2392
|
-
{ name: 'Require
|
|
2393
|
-
{ name: '
|
|
2394
|
-
{ name: 'Restrict model endpoint to approved list', category: 'Model Governance', signal: '๐', defaultAction: 'block', severity: 'high', problem: 'Blocks unapproved model endpoints.', roi: 'Keeps data inside approved vendor boundaries.', rollout: 'Allowlist endpoints during pilot.', pattern: '(model_call|llm_request).*(unapproved|unknown|consumer)' },
|
|
2395
|
-
{ name: 'Require attorney review before routing', category: 'Intake Routing', signal: '๐', defaultAction: 'warn', severity: 'high', problem: 'Requires review before routing or scheduling.', roi: 'Prevents bad prospect routing.', rollout: 'Start strict; relax after pilot evidence.', pattern: '(route_to_attorney|schedule_consultation).*(no|missing).*(review|approval)' }
|
|
1634
|
+
{ name: 'Never force-push to main', category: 'Git Safety', signal: '๐', defaultAction: 'block', severity: 'critical', problem: 'Stops destructive history rewrites on protected branches before they land.', roi: 'Protects every shared repo from the fastest irreversible mistake.', rollout: 'Enable on every team repo on day one.', pattern: 'git\\s+push\\s+(--force|-f)' },
|
|
1635
|
+
{ name: 'Never skip tests before commit', category: 'Verification', signal: '๐', defaultAction: 'block', severity: 'high', problem: 'Requires proof before code leaves the laptop.', roi: 'Cuts review churn and CI rollback time across the team.', rollout: 'Pair with repository-specific test commands.', pattern: 'git\\s+commit' },
|
|
1636
|
+
{ name: 'Require evidence before saying done', category: 'Agent Honesty', signal: '๐', defaultAction: 'block', severity: 'high', problem: 'Prevents unsupported completion claims.', roi: 'Raises trust in autonomous runs.', rollout: 'Use anywhere proof matters more than speed.', pattern: 'completion_claim_without_verification' },
|
|
1637
|
+
{ name: 'Protect production SQL', category: 'Database Safety', signal: '๐', defaultAction: 'block', severity: 'critical', problem: 'Blocks destructive SQL operations on production-like targets.', roi: 'One saved incident pays for the rollout.', rollout: 'Turn on for any team touching live data.', pattern: '(drop|truncate|delete)\\s+.*production' }
|
|
2396
1638
|
]
|
|
2397
1639
|
});
|
|
2398
1640
|
renderGeneratedView(buildDemoGeneratedViewSpec(currentGeneratedView));
|
|
2399
1641
|
renderInsights({
|
|
2400
|
-
gateStats: { blocked:
|
|
1642
|
+
gateStats: { blocked: 180 },
|
|
2401
1643
|
tokenSavings: {
|
|
2402
1644
|
dollarsSaved: 0,
|
|
2403
1645
|
dollarsSavedDisplay: '$0.00',
|
|
2404
1646
|
tokensSavedDisplay: '0',
|
|
2405
1647
|
blockedCalls: 0,
|
|
2406
|
-
modelMix: { '
|
|
2407
|
-
blendedPricePer1M: { input:
|
|
1648
|
+
modelMix: { 'sonnet-4.5': 0.8, 'opus-4.6': 0.15, 'haiku-4.5': 0.05 },
|
|
1649
|
+
blendedPricePer1M: { input: 6.2, output: 24.1 }
|
|
2408
1650
|
},
|
|
2409
1651
|
lessonPipeline: {
|
|
2410
1652
|
stages: [
|
|
2411
|
-
{ id: 'feedback', label: '
|
|
2412
|
-
{ id: 'lessons', label: '
|
|
2413
|
-
{ id: 'gates', label: 'Gates
|
|
2414
|
-
{ id: 'blocked', label: '
|
|
1653
|
+
{ id: 'feedback', label: 'Feedback Signals', count: 297, detail: '37 up / 260 down' },
|
|
1654
|
+
{ id: 'lessons', label: 'Lessons Distilled', count: 94, detail: '72 mistakes / 22 good patterns' },
|
|
1655
|
+
{ id: 'gates', label: 'Gates Promoted', count: 21, detail: '22% of lessons become gates' },
|
|
1656
|
+
{ id: 'blocked', label: 'Actions Blocked', count: 180, detail: 'Repeat mistakes prevented' }
|
|
2415
1657
|
],
|
|
2416
|
-
rates: { feedbackToLesson:
|
|
1658
|
+
rates: { feedbackToLesson: 32, lessonToGate: 22 }
|
|
2417
1659
|
},
|
|
2418
1660
|
feedbackTimeSeries: {
|
|
2419
1661
|
days: Array.from({ length: 30 }, function(_, index) {
|
|
@@ -2437,14 +1679,14 @@ function loadDemo() {
|
|
|
2437
1679
|
},
|
|
2438
1680
|
actionableRemediations: [
|
|
2439
1681
|
{
|
|
2440
|
-
title: '
|
|
2441
|
-
rationale: '
|
|
2442
|
-
action: '
|
|
1682
|
+
title: 'tighten verification before response ยท recent-signals',
|
|
1683
|
+
rationale: 'Recent approval rate has dropped below the lifetime baseline, so fast completion claims need tighter proof.',
|
|
1684
|
+
action: 'tighten-verification-before-response'
|
|
2443
1685
|
},
|
|
2444
1686
|
{
|
|
2445
|
-
title: '
|
|
2446
|
-
rationale: '
|
|
2447
|
-
action: '
|
|
1687
|
+
title: 'audit domain failures ยท git-workflow',
|
|
1688
|
+
rationale: 'Git workflow events are overrepresented in high-risk outcomes and deserve a stronger pre-action gate.',
|
|
1689
|
+
action: 'audit-domain-failures'
|
|
2448
1690
|
}
|
|
2449
1691
|
]
|
|
2450
1692
|
});
|
|
@@ -2452,7 +1694,6 @@ function loadDemo() {
|
|
|
2452
1694
|
|
|
2453
1695
|
// Auto-load demo on first visit so visitors see the product immediately
|
|
2454
1696
|
window.addEventListener('DOMContentLoaded', function() {
|
|
2455
|
-
preserveProjectQueryParams();
|
|
2456
1697
|
if (hasBootstrapKey()) {
|
|
2457
1698
|
connect({ key: BOOTSTRAP_API_KEY, localPro: true });
|
|
2458
1699
|
return;
|
|
@@ -2607,7 +1848,6 @@ function chartDefaults() {
|
|
|
2607
1848
|
function renderFeedbackTrendChart(ts) {
|
|
2608
1849
|
var canvas = document.getElementById('feedbackTrendChart');
|
|
2609
1850
|
if (!canvas) return;
|
|
2610
|
-
if (typeof Chart === 'undefined') return;
|
|
2611
1851
|
var days = ts.days || [];
|
|
2612
1852
|
if (feedbackChart) feedbackChart.destroy();
|
|
2613
1853
|
|
|
@@ -2658,7 +1898,6 @@ function renderFeedbackTrendChart(ts) {
|
|
|
2658
1898
|
function renderLessonTrendChart(ts) {
|
|
2659
1899
|
var canvas = document.getElementById('lessonTrendChart');
|
|
2660
1900
|
if (!canvas) return;
|
|
2661
|
-
if (typeof Chart === 'undefined') return;
|
|
2662
1901
|
var days = ts.days || [];
|
|
2663
1902
|
if (lessonChart) lessonChart.destroy();
|
|
2664
1903
|
|
|
@@ -2717,7 +1956,6 @@ function renderLessonTrendChart(ts) {
|
|
|
2717
1956
|
function renderGateAuditChartFromData(gateAudit) {
|
|
2718
1957
|
var canvas = document.getElementById('gateAuditChart');
|
|
2719
1958
|
if (!canvas) return;
|
|
2720
|
-
if (typeof Chart === 'undefined') return;
|
|
2721
1959
|
var days = gateAudit.days || [];
|
|
2722
1960
|
if (gateAuditChart) gateAuditChart.destroy();
|
|
2723
1961
|
|
|
@@ -2761,8 +1999,6 @@ function renderGateAuditChartFromData(gateAudit) {
|
|
|
2761
1999
|
},
|
|
2762
2000
|
});
|
|
2763
2001
|
}
|
|
2764
|
-
|
|
2765
2002
|
</script>
|
|
2766
|
-
<script src="/js/buyer-intent.js"></script>
|
|
2767
2003
|
</body>
|
|
2768
2004
|
</html>
|