thumbgate 1.27.18 โ 1.27.20
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/commands/dashboard.md +15 -0
- package/.claude/commands/thumbgate-blocked.md +27 -0
- package/.claude/commands/thumbgate-dashboard.md +15 -0
- package/.claude/commands/thumbgate-doctor.md +30 -0
- package/.claude/commands/thumbgate-guard.md +36 -0
- package/.claude/commands/thumbgate-protect.md +30 -0
- package/.claude/commands/thumbgate-rules.md +30 -0
- package/.claude-plugin/plugin.json +4 -3
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +33 -12
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +246 -30
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/gemini/function-declarations.json +1 -0
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +263 -11
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bench/observability-eval-suite.json +26 -0
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +1456 -122
- package/bin/dashboard-cli.js +7 -0
- package/bin/postinstall.js +11 -27
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +216 -0
- package/config/gates/claim-verification.json +12 -0
- package/config/gates/default.json +31 -2
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +23 -13
- package/config/merge-quality-checks.json +0 -1
- package/config/model-candidates.json +121 -6
- package/config/post-deploy-marketing-pages.json +80 -0
- package/config/tessl-tiles.json +1 -3
- package/openapi/openapi.yaml +12 -0
- package/package.json +225 -100
- package/public/about.html +162 -0
- package/public/agent-manager.html +179 -0
- package/public/agents-cost-savings.html +153 -0
- package/public/ai-malpractice-prevention.html +818 -0
- 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 +28 -0
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
- package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
- package/public/assets/brand/thumbgate-mark-team.svg +26 -0
- package/public/assets/brand/thumbgate-mark.svg +21 -0
- package/public/assets/brand/thumbgate-wordmark.svg +20 -0
- package/public/assets/claude-thumbgate-statusbar.svg +8 -0
- package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
- package/public/assets/legal-intake-control-flow.svg +66 -0
- package/public/blog.html +4 -4
- package/public/brand/thumbgate-mark.svg +19 -0
- package/public/brand/thumbgate-og.svg +16 -0
- package/public/chatgpt-app.html +330 -0
- package/public/codex-enterprise.html +123 -0
- package/public/codex-plugin.html +72 -20
- package/public/compare.html +31 -8
- package/public/dashboard.html +930 -166
- package/public/diagnostic.html +345 -0
- package/public/federal.html +2 -2
- package/public/guide.html +33 -13
- package/public/index.html +469 -111
- package/public/install.html +193 -0
- package/public/js/buyer-intent.js +672 -0
- package/public/learn.html +183 -18
- package/public/lessons.html +168 -10
- package/public/numbers.html +7 -7
- package/public/pricing.html +399 -0
- package/public/pro.html +34 -11
- package/scripts/action-receipts.js +324 -0
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/agent-readiness.js +20 -3
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/audit.js +65 -0
- package/scripts/auto-promote-gates.js +82 -10
- package/scripts/auto-wire-hooks.js +14 -0
- package/scripts/aws-blocks-guardrails.js +272 -0
- package/scripts/billing.js +93 -1
- package/scripts/bot-detection.js +61 -3
- package/scripts/build-metadata.js +50 -10
- package/scripts/classifier-routing.js +130 -0
- package/scripts/cli-feedback.js +4 -2
- package/scripts/cli-schema.js +97 -0
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/commercial-offer.js +82 -2
- package/scripts/context-manager.js +74 -6
- package/scripts/dashboard-chat.js +332 -0
- package/scripts/dashboard.js +68 -2
- package/scripts/export-databricks-bundle.js +5 -1
- package/scripts/export-dpo-pairs.js +7 -2
- package/scripts/feedback-aggregate.js +281 -0
- package/scripts/feedback-loop.js +123 -1
- package/scripts/feedback-quality.js +87 -0
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gate-stats.js +89 -0
- package/scripts/gates-engine.js +1176 -85
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-runtime.js +20 -14
- package/scripts/hook-stop-anti-claim.js +301 -0
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/hybrid-feedback-context.js +142 -7
- package/scripts/install-shim.js +87 -0
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/license.js +10 -10
- package/scripts/llm-client.js +169 -4
- package/scripts/local-model-profile.js +15 -8
- package/scripts/mcp-config.js +7 -1
- package/scripts/mcp-oauth.js +293 -0
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/meta-agent-loop.js +36 -0
- package/scripts/noop-detect.js +285 -0
- package/scripts/operational-integrity.js +39 -5
- package/scripts/oss-pr-opportunity-scout.js +35 -5
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plan-gate.js +243 -0
- package/scripts/plausible-domain-config.js +99 -0
- package/scripts/plausible-server-events.js +9 -6
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/rate-limiter.js +64 -13
- package/scripts/repeat-metric.js +137 -0
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-redaction.js +166 -0
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +260 -10
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/self-harness-optimizer.js +141 -0
- package/scripts/self-healing-check.js +193 -0
- package/scripts/self-protection.js +90 -0
- package/scripts/seo-gsd.js +916 -7
- package/scripts/silent-failure-cluster.js +531 -0
- package/scripts/statusline-cache-path.js +17 -2
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/statusline-local-stats.js +9 -1
- package/scripts/statusline-meta.js +28 -2
- package/scripts/statusline.sh +20 -4
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/telemetry-analytics.js +357 -0
- package/scripts/thompson-sampling.js +31 -10
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/tool-registry.js +169 -1
- package/scripts/trajectory-scorer.js +63 -0
- package/scripts/vector-store.js +45 -0
- package/scripts/verify-marketing-pages-deployed.js +212 -0
- package/scripts/visitor-journey.js +172 -0
- package/scripts/workflow-sentinel.js +286 -53
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +2683 -319
- package/.claude-plugin/marketplace.json +0 -85
- package/adapters/chatgpt/openapi.yaml +0 -1695
- package/scripts/bot-detector.js +0 -50
package/public/dashboard.html
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
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.ai/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">
|
|
12
13
|
<!-- Privacy-friendly analytics by Plausible -->
|
|
13
|
-
<script defer data-domain="thumbgate
|
|
14
|
+
<script defer data-domain="thumbgate.ai" src="https://plausible.io/js/script.js"></script>
|
|
14
15
|
<script type="application/ld+json">
|
|
15
16
|
{
|
|
16
17
|
"@context": "https://schema.org",
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
"name": "ThumbGate Dashboard",
|
|
19
20
|
"applicationCategory": "DeveloperApplication",
|
|
20
21
|
"operatingSystem": "Cross-platform",
|
|
21
|
-
"url": "https://thumbgate
|
|
22
|
+
"url": "https://thumbgate.ai/dashboard",
|
|
22
23
|
"dateModified": "2026-04-20",
|
|
23
24
|
"creator": {
|
|
24
25
|
"@type": "Person",
|
|
@@ -102,7 +103,7 @@
|
|
|
102
103
|
.result-tags { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
|
|
103
104
|
.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; }
|
|
104
105
|
.tag:hover { background: rgba(34,211,238,0.25); transform: translateY(-1px); }
|
|
105
|
-
mark { background:
|
|
106
|
+
mark { background: #fbbf24; color: #000000; font-weight: bold; border-radius: 2px; padding: 0 3px; }
|
|
106
107
|
|
|
107
108
|
/* GATES */
|
|
108
109
|
.gates-section { margin-bottom: 32px; }
|
|
@@ -178,6 +179,74 @@
|
|
|
178
179
|
.demo-banner a { color: var(--cyan); text-decoration: none; font-weight: 600; }
|
|
179
180
|
.demo-banner a:hover { text-decoration: underline; }
|
|
180
181
|
|
|
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
|
+
|
|
181
250
|
.empty { text-align: center; padding: 48px; color: var(--text-muted); font-size: 15px; }
|
|
182
251
|
.loading { text-align: center; padding: 24px; color: var(--text-muted); }
|
|
183
252
|
h2 { font-size: 20px; font-weight: 700; margin-bottom: 16px; letter-spacing: -0.02em; }
|
|
@@ -189,11 +258,31 @@
|
|
|
189
258
|
.settings-card .team-value, .origin-value { font-size: 18px; font-weight: 700; color: var(--text); margin-top: 8px; word-break: break-word; }
|
|
190
259
|
.origin-list, .layer-list, .routing-list { display: flex; flex-direction: column; gap: 12px; }
|
|
191
260
|
.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
|
+
}
|
|
192
281
|
|
|
193
282
|
@media (max-width: 700px) {
|
|
194
283
|
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
195
284
|
.search-filters { flex-wrap: wrap; }
|
|
196
|
-
.team-grid, .template-grid, .team-columns, .settings-grid, .generated-grid, .inventory-grid { grid-template-columns: 1fr; }
|
|
285
|
+
.team-grid, .template-grid, .team-columns, .settings-grid, .generated-grid, .inventory-grid, .enterprise-chat-layout { grid-template-columns: 1fr; }
|
|
197
286
|
}
|
|
198
287
|
</style>
|
|
199
288
|
</head>
|
|
@@ -201,7 +290,7 @@
|
|
|
201
290
|
|
|
202
291
|
<nav>
|
|
203
292
|
<div class="container">
|
|
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>
|
|
293
|
+
<a href="/" class="nav-logo"><img src="/assets/brand/thumbgate-mark-inline-v3.svg" alt="ThumbGate" class="logo-mark" width="28" height="28"><span class="logo-text">ThumbGate Dashboard</span></a>
|
|
205
294
|
<div class="nav-links">
|
|
206
295
|
<a href="/dashboard" class="active" style="color:var(--cyan);">Dashboard</a>
|
|
207
296
|
<a href="/lessons">Lessons</a>
|
|
@@ -214,13 +303,14 @@
|
|
|
214
303
|
<div class="auth-bar">
|
|
215
304
|
<div class="container">
|
|
216
305
|
<div style="margin-bottom:10px;">
|
|
217
|
-
<span style="font-size:14px;font-weight:600;">Connect your API key</span>
|
|
306
|
+
<span id="authTitle" style="font-size:14px;font-weight:600;">Connect your API key</span>
|
|
218
307
|
<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>
|
|
219
308
|
</div>
|
|
220
309
|
<div style="display:flex;gap:12px;align-items:center;">
|
|
221
310
|
<input type="password" class="auth-input" id="apiKey" placeholder="Paste your API key here (THUMBGATE_API_KEY or THUMBGATE_PRO_KEY)..." />
|
|
222
311
|
<button class="btn" id="connectBtn" onclick="connect()">Connect</button>
|
|
223
312
|
<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>
|
|
224
314
|
<span class="auth-status" id="authStatus"></span>
|
|
225
315
|
</div>
|
|
226
316
|
</div>
|
|
@@ -233,10 +323,10 @@
|
|
|
233
323
|
<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>
|
|
234
324
|
<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>
|
|
235
325
|
<div style="display:flex;gap:16px;margin-top:12px;font-size:12px;color:var(--text-muted);">
|
|
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>
|
|
326
|
+
<span class="quick-nav-link" onclick="switchTab('search')">๐ <strong style="color:var(--text);">Search</strong> โ find any memory</span>
|
|
327
|
+
<span class="quick-nav-link" onclick="switchTab('gates')">๐ก๏ธ <strong style="color:var(--text);">Gates</strong> โ what's blocking</span>
|
|
328
|
+
<span class="quick-nav-link" onclick="switchTab('team')">๐ฅ <strong style="color:var(--text);">Team</strong> โ org metrics</span>
|
|
329
|
+
<span class="quick-nav-link" onclick="switchTab('export')">๐ค <strong style="color:var(--text);">Export</strong> โ DPO training data</span>
|
|
240
330
|
</div>
|
|
241
331
|
</div>
|
|
242
332
|
|
|
@@ -247,16 +337,33 @@
|
|
|
247
337
|
|
|
248
338
|
<!-- STATS -->
|
|
249
339
|
<div class="stats-grid" id="statsGrid">
|
|
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
|
|
251
|
-
<a class="stat-card" data-card-action="up" onclick="selectCard(this,'up')" href="/lessons?signal=
|
|
252
|
-
<a class="stat-card" data-card-action="down" onclick="selectCard(this,'down')" href="/lessons?signal=
|
|
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
|
|
340
|
+
<a class="stat-card" data-card-action="all" onclick="selectCard(this,'all')" href="/lessons?signal=all" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view all feedback โ Lessons page"><div class="stat-label">Total Feedback <span class="tooltip-trigger" onclick="event.stopPropagation(); event.preventDefault();">?<span class="tooltip-content">Total thumbs-up and thumbs-down signals captured from your AI agent interactions.</span></span></div><div class="stat-value cyan" id="statTotal">โ</div></a>
|
|
341
|
+
<a class="stat-card" data-card-action="up" onclick="selectCard(this,'up')" href="/lessons?signal=up" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view positive feedback โ Lessons page"><div class="stat-label">๐ Positive <span class="tooltip-trigger" onclick="event.stopPropagation(); event.preventDefault();">?<span class="tooltip-content">Successful outcomes where the agent's work met expectations (thumbs-up).</span></span></div><div class="stat-value green" id="statPositive">โ</div></a>
|
|
342
|
+
<a class="stat-card" data-card-action="down" onclick="selectCard(this,'down')" href="/lessons?signal=down" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view negative feedback โ Lessons page"><div class="stat-label">๐ Negative <span class="tooltip-trigger" onclick="event.stopPropagation(); event.preventDefault();">?<span class="tooltip-content">Unsuccessful runs or errors that we need to learn from to prevent repeat failures (thumbs-down).</span></span></div><div class="stat-value red" id="statNegative">โ</div></a>
|
|
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 <span class="tooltip-trigger" onclick="event.stopPropagation(); event.preventDefault();">?<span class="tooltip-content">Pre-action checks actively monitoring and blocking known-bad tool calls before they run.</span></span></div><div class="stat-value cyan" id="statGates">โ</div></a>
|
|
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>
|
|
254
361
|
</div>
|
|
255
362
|
|
|
256
363
|
<div class="panel" id="reviewDeltaPanel" style="margin-bottom:20px;">
|
|
257
364
|
<div style="display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap;">
|
|
258
365
|
<div>
|
|
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
|
|
366
|
+
<h3 style="margin:0 0 8px 0;font-size:14px;letter-spacing:0.04em;text-transform:uppercase;color:var(--text-muted);display:inline-flex;align-items:center;">๐ Since Last Review <span class="tooltip-trigger" onclick="event.stopPropagation(); event.preventDefault();">?<span class="tooltip-content">Shows new feedback, negative signals, lessons, and blocks registered since your last baseline checkpoint.</span></span></h3>
|
|
260
367
|
<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>
|
|
261
368
|
</div>
|
|
262
369
|
<button class="btn-outline" id="reviewCheckpointBtn" onclick="markReviewed()">Mark Current Dashboard Reviewed</button>
|
|
@@ -284,6 +391,8 @@
|
|
|
284
391
|
<div class="tab active" onclick="switchTab('search')">๐ Search Memories</div>
|
|
285
392
|
<div class="tab" onclick="switchTab('gates')">๐ก๏ธ Active Gates</div>
|
|
286
393
|
<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>
|
|
287
396
|
<div class="tab" onclick="switchTab('generated')">๐งฉ Generated Views</div>
|
|
288
397
|
<div class="tab" onclick="switchTab('settings')">โ๏ธ Policy Origins</div>
|
|
289
398
|
<div class="tab" onclick="switchTab('templates')">๐งฑ Gate Templates</div>
|
|
@@ -293,6 +402,12 @@
|
|
|
293
402
|
|
|
294
403
|
<!-- SEARCH TAB -->
|
|
295
404
|
<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>
|
|
296
411
|
<div class="search-section">
|
|
297
412
|
<div class="search-bar">
|
|
298
413
|
<input type="text" class="search-input" id="searchQuery" placeholder="Search memories... (try: git, test, deploy, secrets, database)" onkeydown="if(event.key==='Enter')search()" />
|
|
@@ -314,6 +429,74 @@
|
|
|
314
429
|
<div class="tab-content" id="tab-gates">
|
|
315
430
|
<div class="gates-section">
|
|
316
431
|
<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
|
+
|
|
317
500
|
<div id="gatesList"><div class="loading">Loading gates...</div></div>
|
|
318
501
|
</div>
|
|
319
502
|
</div>
|
|
@@ -387,6 +570,56 @@
|
|
|
387
570
|
</div>
|
|
388
571
|
</div>
|
|
389
572
|
|
|
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
|
+
|
|
390
623
|
<!-- GENERATED TAB -->
|
|
391
624
|
<div class="tab-content" id="tab-generated">
|
|
392
625
|
<div class="templates-section">
|
|
@@ -460,13 +693,7 @@
|
|
|
460
693
|
</div>
|
|
461
694
|
|
|
462
695
|
<!-- Charts -->
|
|
463
|
-
<div
|
|
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>
|
|
696
|
+
<div style="margin-bottom:24px;">
|
|
470
697
|
<div class="panel">
|
|
471
698
|
<h3>Lessons Generated (30 days)</h3>
|
|
472
699
|
<div style="position:relative;height:260px;margin-top:12px;">
|
|
@@ -539,6 +766,91 @@
|
|
|
539
766
|
</div>
|
|
540
767
|
|
|
541
768
|
<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
|
+
|
|
542
854
|
let API_KEY = '';
|
|
543
855
|
let currentSource = 'all';
|
|
544
856
|
let isDemo = false;
|
|
@@ -548,38 +860,237 @@ let currentGeneratedView = 'team-review';
|
|
|
548
860
|
const BOOTSTRAP_API_KEY = __DASHBOARD_BOOTSTRAP_KEY__;
|
|
549
861
|
const LOCAL_PRO_BOOTSTRAP = __DASHBOARD_BOOTSTRAP_ENABLED__;
|
|
550
862
|
|
|
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
|
+
|
|
551
881
|
function getHeaders() {
|
|
552
|
-
|
|
882
|
+
var headers = { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' };
|
|
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
|
+
}
|
|
553
996
|
}
|
|
554
997
|
|
|
555
998
|
function hasBootstrapKey() {
|
|
556
999
|
return LOCAL_PRO_BOOTSTRAP && Boolean(BOOTSTRAP_API_KEY);
|
|
557
1000
|
}
|
|
558
1001
|
|
|
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
|
+
|
|
559
1033
|
async function connect(options) {
|
|
560
1034
|
var opts = options || {};
|
|
561
1035
|
var input = document.getElementById('apiKey');
|
|
562
1036
|
API_KEY = String(opts.key || input.value || '').trim();
|
|
563
1037
|
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
|
+
|
|
564
1042
|
const status = document.getElementById('authStatus');
|
|
565
1043
|
const btn = document.getElementById('connectBtn');
|
|
566
1044
|
btn.disabled = true;
|
|
567
1045
|
status.className = 'auth-status';
|
|
568
|
-
status.textContent = opts.localPro ?
|
|
1046
|
+
status.textContent = opts.localPro ? `Connecting local dashboard...` : 'Connecting...';
|
|
569
1047
|
try {
|
|
570
1048
|
const res = await fetch('/v1/feedback/stats', { headers: getHeaders() });
|
|
571
1049
|
if (!res.ok) throw new Error('Invalid API key');
|
|
572
1050
|
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
|
+
|
|
573
1077
|
status.className = 'auth-status ok';
|
|
574
|
-
status.textContent = opts.localPro ?
|
|
1078
|
+
status.textContent = opts.localPro ? `โ Local ${tierName} connected` : 'โ Connected';
|
|
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
|
+
}
|
|
575
1084
|
document.getElementById('dashboardContent').style.display = 'block';
|
|
576
1085
|
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.';
|
|
577
1088
|
input.value = 'local-license';
|
|
578
1089
|
input.disabled = true;
|
|
579
|
-
input.placeholder =
|
|
1090
|
+
input.placeholder = `Local ${tierName} auto-connected`;
|
|
580
1091
|
btn.disabled = true;
|
|
581
1092
|
document.getElementById('demoBtn').style.display = 'none';
|
|
582
|
-
document.getElementById('authHelp').textContent =
|
|
1093
|
+
document.getElementById('authHelp').textContent = isEnterprise ? localEnterpriseActiveMessage : localProActiveMessage;
|
|
583
1094
|
}
|
|
584
1095
|
renderStats(data);
|
|
585
1096
|
setSelectedCard('all');
|
|
@@ -591,8 +1102,12 @@ async function connect(options) {
|
|
|
591
1102
|
} catch (e) {
|
|
592
1103
|
status.className = 'auth-status err';
|
|
593
1104
|
status.textContent = 'โ ' + e.message;
|
|
1105
|
+
const titleEl = document.getElementById('authTitle');
|
|
1106
|
+
if (titleEl) {
|
|
1107
|
+
titleEl.textContent = 'Connect your API key';
|
|
1108
|
+
}
|
|
594
1109
|
if (opts.localPro) {
|
|
595
|
-
document.getElementById('authHelp').textContent =
|
|
1110
|
+
document.getElementById('authHelp').textContent = `Local ${tierName} bootstrap failed. Paste your THUMBGATE_API_KEY manually or retry the local launcher.`;
|
|
596
1111
|
}
|
|
597
1112
|
}
|
|
598
1113
|
if (!opts.localPro) {
|
|
@@ -656,24 +1171,88 @@ async function search() {
|
|
|
656
1171
|
}
|
|
657
1172
|
|
|
658
1173
|
function renderResult(r) {
|
|
659
|
-
|
|
1174
|
+
let contextText = r.context || r.text || r.content || '';
|
|
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
|
+
|
|
660
1229
|
const isUp = signal === 'up' || signal === 'positive' || signal === 'thumbs_up';
|
|
661
1230
|
const isDown = signal === 'down' || signal === 'negative' || signal === 'thumbs_down';
|
|
662
1231
|
const signalClass = isUp ? 'up' : isDown ? 'down' : '';
|
|
663
1232
|
const signalLabel = isUp ? '๐ Positive' : isDown ? '๐ Negative' : signal;
|
|
664
|
-
|
|
665
|
-
|
|
1233
|
+
|
|
1234
|
+
let displayTitle = r.title || contextText || '';
|
|
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
|
+
|
|
666
1244
|
const tags = r.tags || [];
|
|
667
1245
|
const date = r.timestamp ? new Date(r.timestamp).toLocaleDateString() : '';
|
|
1246
|
+
|
|
668
1247
|
return '<div class="result-card">' +
|
|
669
1248
|
'<div class="result-header">' +
|
|
670
1249
|
(signalLabel ? '<span class="result-signal ' + signalClass + '">' + signalLabel + '</span>' : '<span></span>') +
|
|
671
1250
|
'<span class="result-date">' + date + '</span>' +
|
|
672
1251
|
'</div>' +
|
|
673
|
-
(
|
|
674
|
-
(
|
|
1252
|
+
(displayTitle ? '<div class="result-title">' + highlightText(displayTitle) + '</div>' : '') +
|
|
1253
|
+
(contextText && contextText !== displayTitle ? '<div class="result-context">' + highlightText(contextText.slice(0, 300)) + '</div>' : '') +
|
|
675
1254
|
(tags.length ? '<div class="result-tags">' + tags.map(function(t) {
|
|
676
|
-
return '<button type="button" class="tag" data-tag="' + encodeURIComponent(String(t)) + '" title="' + escAttr('Search for ' + t) + '">' +
|
|
1255
|
+
return '<button type="button" class="tag" data-tag="' + encodeURIComponent(String(t)) + '" title="' + escAttr('Search for ' + t) + '">' + highlightText(t) + '</button>';
|
|
677
1256
|
}).join('') + '</div>' : '') +
|
|
678
1257
|
'</div>';
|
|
679
1258
|
}
|
|
@@ -750,10 +1329,23 @@ function setSource(el, source) {
|
|
|
750
1329
|
function switchTab(name) {
|
|
751
1330
|
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
|
|
752
1331
|
document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); });
|
|
753
|
-
|
|
1332
|
+
// Scope the header lookup to .tab โ the prior selector
|
|
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 + '"]');
|
|
754
1338
|
var contentEl = document.getElementById('tab-' + name);
|
|
755
1339
|
if (tabEl) tabEl.classList.add('active');
|
|
756
|
-
if (contentEl)
|
|
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
|
+
}
|
|
757
1349
|
// Sync URL hash so deep-links stay shareable without scroll jump
|
|
758
1350
|
try {
|
|
759
1351
|
if (('#' + name) !== window.location.hash) {
|
|
@@ -765,11 +1357,11 @@ function switchTab(name) {
|
|
|
765
1357
|
/**
|
|
766
1358
|
* Resolve deep-link tab target from URL hash or query string.
|
|
767
1359
|
* Supports: /dashboard#insights, /dashboard?tab=gates, /dashboard#tab-export.
|
|
768
|
-
* Valid targets match tab-content ids (search, gates, team,
|
|
769
|
-
* settings, templates, insights, export).
|
|
1360
|
+
* Valid targets match tab-content ids (search, gates, team, enterprise,
|
|
1361
|
+
* ai-inventory, generated, settings, templates, insights, export).
|
|
770
1362
|
*/
|
|
771
1363
|
function getDeepLinkTab() {
|
|
772
|
-
var valid = ['search', 'gates', 'team', 'generated', 'settings', 'templates', 'insights', 'export'];
|
|
1364
|
+
var valid = ['search', 'gates', 'team', 'enterprise', 'ai-inventory', 'generated', 'settings', 'templates', 'insights', 'export'];
|
|
773
1365
|
var raw = (window.location.hash || '').replace(/^#/, '').replace(/^tab-/, '');
|
|
774
1366
|
if (!raw) {
|
|
775
1367
|
try {
|
|
@@ -936,9 +1528,14 @@ async function markReviewed() {
|
|
|
936
1528
|
try {
|
|
937
1529
|
var res = await fetch('/v1/dashboard/review-state', {
|
|
938
1530
|
method: 'POST',
|
|
939
|
-
headers: getHeaders()
|
|
1531
|
+
headers: getHeaders(),
|
|
1532
|
+
body: JSON.stringify({ reviewedAt: new Date().toISOString() })
|
|
940
1533
|
});
|
|
941
|
-
if (!res.ok)
|
|
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
|
+
}
|
|
942
1539
|
var body = await res.json();
|
|
943
1540
|
renderReviewDelta(body.reviewDelta || {});
|
|
944
1541
|
} catch (e) {
|
|
@@ -976,6 +1573,68 @@ function renderDashboardData(data) {
|
|
|
976
1573
|
renderRegulatedProof(data.regulatedProof || {});
|
|
977
1574
|
renderTemplates(data.templateLibrary || {});
|
|
978
1575
|
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
|
+
}
|
|
979
1638
|
}
|
|
980
1639
|
|
|
981
1640
|
function renderGeneratedViewToolbar(spec) {
|
|
@@ -1415,6 +2074,108 @@ function renderTemplates(templateLibrary) {
|
|
|
1415
2074
|
: '<div class="empty">No check templates available</div>';
|
|
1416
2075
|
}
|
|
1417
2076
|
|
|
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
|
+
|
|
1418
2179
|
document.addEventListener('click', function(event) {
|
|
1419
2180
|
var tagButton = event.target.closest('.tag[data-tag]');
|
|
1420
2181
|
if (!tagButton) return;
|
|
@@ -1428,71 +2189,65 @@ function loadDemo() {
|
|
|
1428
2189
|
// Demo badge in nav
|
|
1429
2190
|
var logo = document.querySelector('.nav-logo');
|
|
1430
2191
|
if (!document.getElementById('demoBadge')) {
|
|
1431
|
-
logo.insertAdjacentHTML('afterend', '<span class="demo-badge" id="demoBadge">Demo Mode</span>');
|
|
2192
|
+
logo.insertAdjacentHTML('afterend', '<span class="demo-badge" id="demoBadge">Demo Mode โ Jamie M., Partner ยท Litigation Intake</span>');
|
|
1432
2193
|
}
|
|
1433
2194
|
// Stats
|
|
1434
|
-
document.getElementById('statTotal').textContent = '
|
|
1435
|
-
document.getElementById('statPositive').textContent = '
|
|
1436
|
-
document.getElementById('statNegative').textContent = '
|
|
1437
|
-
document.getElementById('statGates').textContent = '
|
|
2195
|
+
document.getElementById('statTotal').textContent = '184';
|
|
2196
|
+
document.getElementById('statPositive').textContent = '91';
|
|
2197
|
+
document.getElementById('statNegative').textContent = '93';
|
|
2198
|
+
document.getElementById('statGates').textContent = '14';
|
|
1438
2199
|
setSelectedCard('all');
|
|
1439
|
-
|
|
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);
|
|
2200
|
+
var demoSavings = TG_TOKEN_SAVINGS.compute(93, 42);
|
|
1443
2201
|
var tEl = document.getElementById('statTokensSaved');
|
|
1444
2202
|
var dEl = document.getElementById('statDollarsSaved');
|
|
1445
2203
|
if (tEl) tEl.textContent = demoSavings.tokensSavedDisplay;
|
|
1446
2204
|
if (dEl) dEl.textContent = 'โ ' + demoSavings.dollarsSavedDisplay + ' saved';
|
|
1447
2205
|
renderTopBlockedGates([
|
|
1448
|
-
{ name: 'gate:
|
|
1449
|
-
{ name: 'gate:
|
|
1450
|
-
{ name: 'gate:
|
|
1451
|
-
{ name: 'gate:
|
|
1452
|
-
{ name: 'gate:
|
|
1453
|
-
{ name: 'gate:
|
|
2206
|
+
{ name: 'gate:block-unauthorized-practice', blocked: 34 },
|
|
2207
|
+
{ name: 'gate:require-conflict-clearance', blocked: 22 },
|
|
2208
|
+
{ name: 'gate:block-privileged-egress', blocked: 17 },
|
|
2209
|
+
{ name: 'gate:require-approved-disclaimer', blocked: 11 },
|
|
2210
|
+
{ name: 'gate:restrict-model-endpoint', blocked: 6 },
|
|
2211
|
+
{ name: 'gate:require-attorney-review', blocked: 3 },
|
|
1454
2212
|
]);
|
|
1455
2213
|
renderReviewDelta({
|
|
1456
2214
|
hasBaseline: true,
|
|
1457
|
-
reviewedAt: '
|
|
1458
|
-
previousHead: '
|
|
1459
|
-
currentHead: '
|
|
1460
|
-
feedbackAdded:
|
|
1461
|
-
negativeAdded:
|
|
1462
|
-
lessonsAdded:
|
|
1463
|
-
blocksAdded:
|
|
1464
|
-
warnsAdded:
|
|
1465
|
-
headline: 'Since your last review:
|
|
2215
|
+
reviewedAt: '2026-05-22T09:00:00Z',
|
|
2216
|
+
previousHead: 'a4c1e7f2',
|
|
2217
|
+
currentHead: 'b8d3f9a1',
|
|
2218
|
+
feedbackAdded: 8,
|
|
2219
|
+
negativeAdded: 5,
|
|
2220
|
+
lessonsAdded: 3,
|
|
2221
|
+
blocksAdded: 7,
|
|
2222
|
+
warnsAdded: 2,
|
|
2223
|
+
headline: 'Since your last review: 8 intake events ยท 5 blocked ยท 3 lessons ยท 7 gate enforcements.',
|
|
1466
2224
|
latestFeedback: {
|
|
1467
|
-
title: '
|
|
1468
|
-
timestamp: '
|
|
2225
|
+
title: 'Intake agent provided jurisdictional recommendation without attorney review',
|
|
2226
|
+
timestamp: '2026-05-25T14:20:00Z'
|
|
1469
2227
|
},
|
|
1470
2228
|
latestLesson: {
|
|
1471
|
-
title: '
|
|
1472
|
-
timestamp: '
|
|
2229
|
+
title: 'BLOCKED: Advice-shaped response detected โ routed to attorney review queue',
|
|
2230
|
+
timestamp: '2026-05-24T11:10:00Z'
|
|
1473
2231
|
}
|
|
1474
2232
|
});
|
|
1475
2233
|
document.getElementById('reviewCheckpointBtn').disabled = true;
|
|
1476
2234
|
document.getElementById('reviewCheckpointBtn').textContent = 'Connect to save your own review checkpoint';
|
|
1477
|
-
// Sample memories โ
|
|
2235
|
+
// Sample memories โ legal AI intake scenarios for law firm pilot demo
|
|
1478
2236
|
var demoResults = [
|
|
1479
|
-
{ signal: 'down', title: '
|
|
1480
|
-
{ signal: '
|
|
1481
|
-
{ signal: '
|
|
1482
|
-
{ signal: '
|
|
1483
|
-
{ signal: '
|
|
1484
|
-
{ signal: '
|
|
1485
|
-
{ signal: 'down', title: '
|
|
1486
|
-
{ signal: 'up', title: '
|
|
1487
|
-
{ signal: 'down', title: '
|
|
1488
|
-
{ signal: 'up', title: '
|
|
1489
|
-
{ signal: 'down', title: '
|
|
1490
|
-
{ signal: 'up', title: '
|
|
1491
|
-
{ signal: '
|
|
1492
|
-
{ signal: 'down', title: '
|
|
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' }
|
|
2237
|
+
{ signal: 'down', title: 'Intake agent provided outcome prediction to prospect', context: 'Prospect asked about wrongful termination in Florida. Agent said "you likely have a strong case." ThumbGate blocked the advice-shaped response before delivery under ABA Rule 5.5.', tags: ['upl-risk', 'aba-rule-5.5', 'auto-blocked', 'critical'], timestamp: '2026-05-25T14:20:00Z' },
|
|
2238
|
+
{ signal: 'up', title: 'Conflict pre-check stopped intake before facts collected', context: 'Adverse party matched the synthetic conflict list. ThumbGate halted intake before privileged facts were disclosed, preventing a Rule 1.7/1.9 conflict issue.', tags: ['conflict-check', 'aba-rule-1.7', 'aba-rule-1.9', 'prevention'], timestamp: '2026-05-25T11:45:00Z' },
|
|
2239
|
+
{ signal: 'down', title: 'Agent attempted to send case summary to external CRM', context: 'Agent tried to push matter IDs and consultation notes to an external CRM. ThumbGate blocked the outbound call for privileged-content markers.', tags: ['privilege-egress', 'aba-rule-1.6', 'auto-blocked', 'critical'], timestamp: '2026-05-24T16:30:00Z' },
|
|
2240
|
+
{ signal: 'up', title: 'Safe handoff collected neutral facts only', context: 'After a UPL block, the agent pivoted to neutral routing facts: name, contact, practice area, and callback time. No legal conclusion was delivered.', tags: ['safe-handoff', 'compliant-response', 'aba-rule-5.5', 'metric:ROI'], timestamp: '2026-05-24T11:10:00Z' },
|
|
2241
|
+
{ signal: 'down', title: 'Agent made jurisdictional recommendation without review', context: 'Prospect asked where to file. Agent gave a venue recommendation. ThumbGate blocked it because jurisdiction guidance requires attorney supervision.', tags: ['upl-risk', 'jurisdiction', 'aba-rule-5.5', 'auto-blocked'], timestamp: '2026-05-23T15:20:00Z' },
|
|
2242
|
+
{ signal: 'up', title: 'Audit export preserved full decision chain', context: 'Export showed matched rule, proposed reply, deny decision, attorney-review route, and UTC timestamp. Compliance can replay the decision.', tags: ['audit-trail', 'compliance', 'aba-opinion-512', 'metric:ROI'], timestamp: '2026-05-23T09:00:00Z' },
|
|
2243
|
+
{ signal: 'down', title: 'Response missing firm-approved disclaimer', context: 'Agent drafted a prospect reply without the required non-engagement disclaimer. ThumbGate blocked delivery until approved text was added.', tags: ['disclaimer-missing', 'non-engagement', 'aba-rule-5.5', 'auto-blocked'], timestamp: '2026-05-22T14:45:00Z' },
|
|
2244
|
+
{ signal: 'up', title: 'Model endpoint restricted to approved provider', context: 'Agent tried a consumer model endpoint. ThumbGate enforced the allowlist and rerouted to the firm-approved Azure OpenAI endpoint.', tags: ['model-governance', 'data-handling', 'aba-opinion-512', 'prevention'], timestamp: '2026-05-22T10:30:00Z' },
|
|
2245
|
+
{ signal: 'down', title: 'Agent disclosed opposing counsel information to prospect', context: 'Agent referenced opposing-counsel information from another matter. ThumbGate blocked the cross-matter leak under Rule 1.6.', tags: ['privilege-egress', 'cross-matter', 'aba-rule-1.6', 'critical'], timestamp: '2026-05-21T16:15:00Z' },
|
|
2246
|
+
{ signal: 'up', title: 'Conflict check cleared โ intake proceeded safely', context: 'Adverse party cleared the synthetic fixture. ThumbGate allowed intake and logged timestamp plus rule version.', tags: ['conflict-check', 'cleared', 'aba-rule-1.7', 'compliant-response'], timestamp: '2026-05-21T11:00:00Z' },
|
|
2247
|
+
{ signal: 'down', title: 'Intake agent attempted to schedule consultation directly', context: 'Agent tried to book a consultation without attorney review. ThumbGate blocked the routing action pending supervisor approval.', tags: ['routing-governance', 'attorney-review', 'auto-blocked'], timestamp: '2026-05-20T13:30:00Z' },
|
|
2248
|
+
{ signal: 'up', title: 'Egress rerouted to in-tenant system successfully', context: 'Agent needed to store intake notes. ThumbGate rerouted the write to the firm tenant, keeping privileged content inside the boundary.', tags: ['privilege-egress', 'in-tenant-reroute', 'aba-rule-1.6', 'metric:ROI'], timestamp: '2026-05-20T09:45:00Z' },
|
|
2249
|
+
{ signal: 'up', title: 'DPO export identified systematic UPL pattern', context: 'Exported 34 blocked intake pairs. 76% were outcome predictions after "do I have a case?" prompts, driving revised intake scripts.', tags: ['dpo-export', 'upl-pattern', 'prevention', 'metric:ROI'], timestamp: '2026-05-19T14:00:00Z' },
|
|
2250
|
+
{ signal: 'down', title: 'Agent used non-approved model for privilege-sensitive query', context: 'Privileged intake query was routed to an endpoint without enterprise DPA. ThumbGate blocked and logged it for security review.', tags: ['model-governance', 'privilege-risk', 'aba-opinion-512', 'auto-blocked'], timestamp: '2026-05-18T11:20:00Z' }
|
|
1496
2251
|
];
|
|
1497
2252
|
isDemo = true;
|
|
1498
2253
|
demoData = demoResults;
|
|
@@ -1506,14 +2261,14 @@ function loadDemo() {
|
|
|
1506
2261
|
'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>' +
|
|
1507
2262
|
'</div>';
|
|
1508
2263
|
document.getElementById('searchResults').innerHTML = upgradeBanner + teaserHtml;
|
|
1509
|
-
// Sample gates
|
|
2264
|
+
// Sample gates โ legal AI intake enforcement
|
|
1510
2265
|
var demoGates = [
|
|
1511
|
-
{ name: '
|
|
1512
|
-
{ name: '
|
|
1513
|
-
{ name: '
|
|
1514
|
-
{ name: '
|
|
1515
|
-
{ name: '
|
|
1516
|
-
{ name: '
|
|
2266
|
+
{ name: 'block-unauthorized-practice', pattern: 'Block advice-shaped intake replies from non-attorney agents (ABA Rule 5.5)', action: 'block' },
|
|
2267
|
+
{ name: 'require-conflict-clearance', pattern: 'Require adverse-party clearance before facts or scheduling (Rules 1.7/1.9/1.10)', action: 'block' },
|
|
2268
|
+
{ name: 'block-privileged-egress', pattern: 'Block outbound calls with matter IDs or attorney-client markers (Rule 1.6)', action: 'block' },
|
|
2269
|
+
{ name: 'require-approved-disclaimer', pattern: 'Require firm-approved non-engagement disclaimer before delivery', action: 'block' },
|
|
2270
|
+
{ name: 'restrict-model-endpoint', pattern: 'Allow only enterprise-approved model endpoints (ABA Opinion 512)', action: 'block' },
|
|
2271
|
+
{ name: 'require-attorney-review', pattern: 'Route case assignment and scheduling through attorney review', action: 'warn' }
|
|
1517
2272
|
];
|
|
1518
2273
|
document.getElementById('gatesList').innerHTML = demoGates.map(function(g) {
|
|
1519
2274
|
return '<div class="gate-card"><div><div class="gate-name">' + escHtml(g.name) + '</div>' +
|
|
@@ -1521,141 +2276,144 @@ function loadDemo() {
|
|
|
1521
2276
|
'<span class="gate-action ' + g.action + '">' + g.action.toUpperCase() + '</span></div>';
|
|
1522
2277
|
}).join('');
|
|
1523
2278
|
renderTeam({
|
|
1524
|
-
totalAgents:
|
|
1525
|
-
activeAgents:
|
|
2279
|
+
totalAgents: 4,
|
|
2280
|
+
activeAgents: 3,
|
|
1526
2281
|
windowHours: 24,
|
|
1527
|
-
orgAdherenceRate:
|
|
2282
|
+
orgAdherenceRate: 96.1,
|
|
1528
2283
|
riskAgents: [
|
|
1529
|
-
{ id: '
|
|
1530
|
-
{ id: '
|
|
2284
|
+
{ id: 'intake-chatbot-prod', project: 'client-intake', branch: 'litigation-practice', adherenceRate: 88.2 },
|
|
2285
|
+
{ id: 'intake-chatbot-staging', project: 'client-intake', branch: 'ip-practice-pilot', adherenceRate: 91.7 }
|
|
1531
2286
|
],
|
|
1532
2287
|
topBlockedGates: [
|
|
1533
|
-
{ gateId: '
|
|
1534
|
-
{ gateId: '
|
|
2288
|
+
{ gateId: 'block-unauthorized-practice', blocked: 34, warned: 0 },
|
|
2289
|
+
{ gateId: 'require-conflict-clearance', blocked: 22, warned: 3 }
|
|
1535
2290
|
]
|
|
1536
2291
|
}, {
|
|
1537
2292
|
northStar: {
|
|
1538
|
-
weeklyTeamsRunningProofBackedWorkflows:
|
|
2293
|
+
weeklyTeamsRunningProofBackedWorkflows: 2,
|
|
1539
2294
|
paidTeamRuns: 1
|
|
1540
2295
|
}
|
|
1541
2296
|
});
|
|
1542
2297
|
renderPredictive({
|
|
1543
2298
|
upgradePropensity: {
|
|
1544
|
-
pro: { band: 'high', score: 0.
|
|
1545
|
-
team: { band: '
|
|
2299
|
+
pro: { band: 'high', score: 0.84 },
|
|
2300
|
+
team: { band: 'high', score: 0.72 }
|
|
1546
2301
|
},
|
|
1547
2302
|
revenueForecast: {
|
|
1548
|
-
predictedBookedRevenueCents:
|
|
1549
|
-
incrementalOpportunityCents:
|
|
2303
|
+
predictedBookedRevenueCents: 8400000,
|
|
2304
|
+
incrementalOpportunityCents: 3200000
|
|
1550
2305
|
},
|
|
1551
2306
|
anomalySummary: {
|
|
1552
2307
|
count: 2,
|
|
1553
2308
|
severity: 'warning'
|
|
1554
2309
|
},
|
|
1555
2310
|
topCreators: [
|
|
1556
|
-
{ key: '
|
|
2311
|
+
{ key: 'innovation_team', opportunityRevenueCents: 4800000 }
|
|
1557
2312
|
],
|
|
1558
2313
|
topSources: [
|
|
1559
|
-
{ key: '
|
|
2314
|
+
{ key: 'direct_outreach', opportunityRevenueCents: 2400000 }
|
|
1560
2315
|
],
|
|
1561
2316
|
anomalies: [
|
|
1562
|
-
{ type: '
|
|
1563
|
-
{ type: '
|
|
2317
|
+
{ type: 'upl_risk_concentration', message: 'UPL blocks concentrate in litigation intake โ add practice-area rules.', severity: 'warning' },
|
|
2318
|
+
{ type: 'egress_pattern', message: 'Privilege egress attempts cluster after hours โ add reviewer coverage.', severity: 'warning' }
|
|
1564
2319
|
]
|
|
1565
2320
|
});
|
|
1566
2321
|
renderAgentInventory({
|
|
1567
|
-
profile: '
|
|
1568
|
-
tier: '
|
|
1569
|
-
configuredServerCount:
|
|
1570
|
-
observedToolCount:
|
|
2322
|
+
profile: 'legal-intake',
|
|
2323
|
+
tier: 'enterprise',
|
|
2324
|
+
configuredServerCount: 2,
|
|
2325
|
+
observedToolCount: 5,
|
|
1571
2326
|
observedTools: [
|
|
1572
|
-
{ toolName: '
|
|
1573
|
-
{ toolName: '
|
|
1574
|
-
{ toolName: '
|
|
2327
|
+
{ toolName: 'intake_reply', evaluations: 89, intercepted: 34, allow: 55, deny: 34, warn: 0 },
|
|
2328
|
+
{ toolName: 'conflict_lookup', evaluations: 67, intercepted: 22, allow: 45, deny: 19, warn: 3 },
|
|
2329
|
+
{ toolName: 'send_to_crm', evaluations: 41, intercepted: 17, allow: 24, deny: 14, warn: 3 },
|
|
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 }
|
|
1575
2332
|
],
|
|
1576
2333
|
policySources: [
|
|
1577
|
-
{ source: '
|
|
1578
|
-
{ source: '
|
|
1579
|
-
{ source: '
|
|
2334
|
+
{ source: 'legal-intake-rules', count: 14 },
|
|
2335
|
+
{ source: 'privilege-guard', count: 6 },
|
|
2336
|
+
{ source: 'conflict-sentinel', count: 4 }
|
|
1580
2337
|
]
|
|
1581
2338
|
});
|
|
1582
2339
|
renderActionableRemediations([
|
|
1583
2340
|
{
|
|
1584
|
-
title: '
|
|
1585
|
-
rationale: '
|
|
1586
|
-
action: '
|
|
2341
|
+
title: 'add practice-area-specific UPL rules ยท litigation-intake',
|
|
2342
|
+
rationale: 'Litigation intake generates 76% of UPL blocks. Add targeted rules to reduce false positives.',
|
|
2343
|
+
action: 'add-practice-area-rules',
|
|
1587
2344
|
},
|
|
1588
2345
|
{
|
|
1589
|
-
title: '
|
|
1590
|
-
rationale: '
|
|
1591
|
-
action: '
|
|
2346
|
+
title: 'extend conflict check to affiliated entities ยท conflict-detection',
|
|
2347
|
+
rationale: 'Conflict fixture covers named parties only. Add affiliates and parents.',
|
|
2348
|
+
action: 'extend-conflict-coverage',
|
|
1592
2349
|
},
|
|
1593
2350
|
{
|
|
1594
|
-
title: '
|
|
1595
|
-
rationale: '
|
|
1596
|
-
action: '
|
|
2351
|
+
title: 'schedule attorney reviewer for off-hours intake ยท privilege-egress',
|
|
2352
|
+
rationale: 'Egress attempts cluster after 6pm. Add a backup reviewer queue.',
|
|
2353
|
+
action: 'schedule-off-hours-reviewer',
|
|
1597
2354
|
}
|
|
1598
2355
|
]);
|
|
1599
2356
|
renderSettingsStatus({
|
|
1600
2357
|
activeLayers: [
|
|
1601
2358
|
{ scope: 'defaults', exists: true, leafCount: 7, sourcePath: null },
|
|
1602
|
-
{ scope: '
|
|
1603
|
-
{ scope: '
|
|
1604
|
-
{ scope: '
|
|
1605
|
-
{ scope: 'managed', exists: true, leafCount:
|
|
2359
|
+
{ scope: 'firm', exists: true, leafCount: 6, sourcePath: 'config/firm-intake-policy.json' },
|
|
2360
|
+
{ scope: 'practice-area', exists: true, leafCount: 4, sourcePath: 'config/litigation-rules.json' },
|
|
2361
|
+
{ scope: 'pilot', exists: true, leafCount: 3, sourcePath: '.thumbgate/pilot-settings.json' },
|
|
2362
|
+
{ scope: 'managed', exists: true, leafCount: 5, sourcePath: 'config/thumbgate-settings.managed.json' }
|
|
1606
2363
|
],
|
|
1607
2364
|
resolvedSettings: {
|
|
1608
|
-
|
|
2365
|
+
intake: { defaultProfile: 'legal-intake', enforcementMode: 'strict' },
|
|
1609
2366
|
harnesses: { enabled: true, allowRuntimeExecution: true }
|
|
1610
2367
|
},
|
|
1611
2368
|
routingPreview: {
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
2369
|
+
intakeAgent: { profile: 'legal-intake', reason: 'all intake agents use strict legal gates' },
|
|
2370
|
+
auditExport: { profile: 'readonly', reason: 'audit export has no firm-system write access' },
|
|
2371
|
+
reviewQueue: { profile: 'attorney-review', reason: 'blocked responses route to attorney review' }
|
|
1615
2372
|
},
|
|
1616
2373
|
origins: [
|
|
1617
|
-
{ path: '
|
|
1618
|
-
{ path: '
|
|
1619
|
-
{ path: '
|
|
1620
|
-
{ path: '
|
|
2374
|
+
{ path: 'intake.defaultProfile', value: 'legal-intake', scope: 'firm', sourcePath: 'config/firm-intake-policy.json' },
|
|
2375
|
+
{ path: 'intake.enforcementMode', value: 'strict', scope: 'firm', sourcePath: 'config/firm-intake-policy.json' },
|
|
2376
|
+
{ path: 'gates.conflictFixturePath', value: 'fixtures/adverse-parties.json', scope: 'pilot', sourcePath: '.thumbgate/pilot-settings.json' },
|
|
2377
|
+
{ path: 'gates.privilegeMarkers', value: ['attorney-client', 'work-product', 'matter-id'], scope: 'firm', sourcePath: 'config/firm-intake-policy.json' }
|
|
1621
2378
|
]
|
|
1622
2379
|
});
|
|
1623
2380
|
renderTemplates({
|
|
1624
2381
|
total: 6,
|
|
1625
2382
|
categories: {
|
|
1626
|
-
'
|
|
1627
|
-
'
|
|
1628
|
-
'
|
|
1629
|
-
'
|
|
1630
|
-
'
|
|
1631
|
-
'Positive Reinforcement': 1
|
|
2383
|
+
'UPL Prevention': 2,
|
|
2384
|
+
'Conflict Detection': 1,
|
|
2385
|
+
'Privilege Protection': 1,
|
|
2386
|
+
'Model Governance': 1,
|
|
2387
|
+
'Intake Routing': 1
|
|
1632
2388
|
},
|
|
1633
2389
|
templates: [
|
|
1634
|
-
{ name: '
|
|
1635
|
-
{ name: '
|
|
1636
|
-
{ name: 'Require
|
|
1637
|
-
{ name: '
|
|
2390
|
+
{ name: 'Block unauthorized practice of law', category: 'UPL Prevention', signal: '๐', defaultAction: 'block', severity: 'critical', problem: 'Stops advice-shaped intake replies under ABA Rule 5.5.', roi: 'Prevents malpractice exposure before delivery.', rollout: 'Enable on every AI intake channel.', pattern: '(outcome prediction|jurisdictional recommendation|you (should|could|might) (file|sue|claim))' },
|
|
2391
|
+
{ name: 'Require approved disclaimer', category: 'UPL Prevention', signal: '๐', defaultAction: 'block', severity: 'high', problem: 'Requires firm-approved non-engagement language.', roi: 'Reduces inadvertent client-relationship risk.', rollout: 'Load approved disclaimer during pilot.', pattern: '(intake_response|client_reply).*(missing|no).*(disclaimer)' },
|
|
2392
|
+
{ name: 'Require conflict check before intake', category: 'Conflict Detection', signal: '๐', defaultAction: 'block', severity: 'critical', problem: 'Requires adverse-party clearance before sensitive facts.', roi: 'Catches conflicts before privileged facts cross walls.', rollout: 'Use conflicts API or synthetic pilot fixture.', pattern: '(collect_case_facts|intake_continue).*(missing|no).*(conflict|clearance)' },
|
|
2393
|
+
{ name: 'Block privileged content egress', category: 'Privilege Protection', signal: '๐', defaultAction: 'block', severity: 'critical', problem: 'Blocks outbound actions with privilege markers.', roi: 'Prevents single-action privilege waiver.', rollout: 'Define markers; start with hard block.', pattern: '(send_email|api_call|external_request).*(privileged|attorney[_-]client|matter[_-]id)' },
|
|
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)' }
|
|
1638
2396
|
]
|
|
1639
2397
|
});
|
|
1640
2398
|
renderGeneratedView(buildDemoGeneratedViewSpec(currentGeneratedView));
|
|
1641
2399
|
renderInsights({
|
|
1642
|
-
gateStats: { blocked:
|
|
2400
|
+
gateStats: { blocked: 93 },
|
|
1643
2401
|
tokenSavings: {
|
|
1644
2402
|
dollarsSaved: 0,
|
|
1645
2403
|
dollarsSavedDisplay: '$0.00',
|
|
1646
2404
|
tokensSavedDisplay: '0',
|
|
1647
2405
|
blockedCalls: 0,
|
|
1648
|
-
modelMix: { '
|
|
1649
|
-
blendedPricePer1M: { input:
|
|
2406
|
+
modelMix: { 'gpt-4o': 0.5, 'claude-sonnet-4.5': 0.35, 'azure-openai': 0.15 },
|
|
2407
|
+
blendedPricePer1M: { input: 5.0, output: 15.0 }
|
|
1650
2408
|
},
|
|
1651
2409
|
lessonPipeline: {
|
|
1652
2410
|
stages: [
|
|
1653
|
-
{ id: 'feedback', label: '
|
|
1654
|
-
{ id: 'lessons', label: '
|
|
1655
|
-
{ id: 'gates', label: 'Gates
|
|
1656
|
-
{ id: 'blocked', label: '
|
|
2411
|
+
{ id: 'feedback', label: 'Intake Events', count: 184, detail: '91 safe / 93 blocked' },
|
|
2412
|
+
{ id: 'lessons', label: 'Rules Refined', count: 42, detail: '28 UPL patterns / 14 egress patterns' },
|
|
2413
|
+
{ id: 'gates', label: 'Gates Active', count: 14, detail: '33% of rules become enforcement gates' },
|
|
2414
|
+
{ id: 'blocked', label: 'Risks Prevented', count: 93, detail: 'UPL, conflict, and privilege violations stopped' }
|
|
1657
2415
|
],
|
|
1658
|
-
rates: { feedbackToLesson:
|
|
2416
|
+
rates: { feedbackToLesson: 23, lessonToGate: 33 }
|
|
1659
2417
|
},
|
|
1660
2418
|
feedbackTimeSeries: {
|
|
1661
2419
|
days: Array.from({ length: 30 }, function(_, index) {
|
|
@@ -1679,14 +2437,14 @@ function loadDemo() {
|
|
|
1679
2437
|
},
|
|
1680
2438
|
actionableRemediations: [
|
|
1681
2439
|
{
|
|
1682
|
-
title: '
|
|
1683
|
-
rationale: '
|
|
1684
|
-
action: '
|
|
2440
|
+
title: 'add litigation-specific UPL rules',
|
|
2441
|
+
rationale: '76% of UPL blocks come from litigation intake. Add targeted rules.',
|
|
2442
|
+
action: 'add-practice-area-rules'
|
|
1685
2443
|
},
|
|
1686
2444
|
{
|
|
1687
|
-
title: '
|
|
1688
|
-
rationale: '
|
|
1689
|
-
action: '
|
|
2445
|
+
title: 'extend conflict checks to affiliates',
|
|
2446
|
+
rationale: 'Named-party fixture misses affiliates and parents.',
|
|
2447
|
+
action: 'extend-conflict-coverage'
|
|
1690
2448
|
}
|
|
1691
2449
|
]
|
|
1692
2450
|
});
|
|
@@ -1694,6 +2452,7 @@ function loadDemo() {
|
|
|
1694
2452
|
|
|
1695
2453
|
// Auto-load demo on first visit so visitors see the product immediately
|
|
1696
2454
|
window.addEventListener('DOMContentLoaded', function() {
|
|
2455
|
+
preserveProjectQueryParams();
|
|
1697
2456
|
if (hasBootstrapKey()) {
|
|
1698
2457
|
connect({ key: BOOTSTRAP_API_KEY, localPro: true });
|
|
1699
2458
|
return;
|
|
@@ -1848,6 +2607,7 @@ function chartDefaults() {
|
|
|
1848
2607
|
function renderFeedbackTrendChart(ts) {
|
|
1849
2608
|
var canvas = document.getElementById('feedbackTrendChart');
|
|
1850
2609
|
if (!canvas) return;
|
|
2610
|
+
if (typeof Chart === 'undefined') return;
|
|
1851
2611
|
var days = ts.days || [];
|
|
1852
2612
|
if (feedbackChart) feedbackChart.destroy();
|
|
1853
2613
|
|
|
@@ -1898,6 +2658,7 @@ function renderFeedbackTrendChart(ts) {
|
|
|
1898
2658
|
function renderLessonTrendChart(ts) {
|
|
1899
2659
|
var canvas = document.getElementById('lessonTrendChart');
|
|
1900
2660
|
if (!canvas) return;
|
|
2661
|
+
if (typeof Chart === 'undefined') return;
|
|
1901
2662
|
var days = ts.days || [];
|
|
1902
2663
|
if (lessonChart) lessonChart.destroy();
|
|
1903
2664
|
|
|
@@ -1956,6 +2717,7 @@ function renderLessonTrendChart(ts) {
|
|
|
1956
2717
|
function renderGateAuditChartFromData(gateAudit) {
|
|
1957
2718
|
var canvas = document.getElementById('gateAuditChart');
|
|
1958
2719
|
if (!canvas) return;
|
|
2720
|
+
if (typeof Chart === 'undefined') return;
|
|
1959
2721
|
var days = gateAudit.days || [];
|
|
1960
2722
|
if (gateAuditChart) gateAuditChart.destroy();
|
|
1961
2723
|
|
|
@@ -1999,6 +2761,8 @@ function renderGateAuditChartFromData(gateAudit) {
|
|
|
1999
2761
|
},
|
|
2000
2762
|
});
|
|
2001
2763
|
}
|
|
2764
|
+
|
|
2002
2765
|
</script>
|
|
2766
|
+
<script src="/js/buyer-intent.js"></script>
|
|
2003
2767
|
</body>
|
|
2004
2768
|
</html>
|