plexdx commited on
Commit
88cbe39
Β·
verified Β·
1 Parent(s): f09b792

Upload app_demo.py

Browse files
Files changed (1) hide show
  1. app_demo.py +713 -0
app_demo.py ADDED
@@ -0,0 +1,713 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app_demo.py β€” Hugging Face Spaces interactive demo.
3
+
4
+ A standalone FastAPI app that provides:
5
+ 1. The production WebSocket backend (from main.py)
6
+ 2. A beautiful HTML/JS demo dashboard at /demo
7
+ β€” Live claim submission with animated results
8
+ β€” WebSocket connection indicator
9
+ β€” Color-coded verdict cards with confidence arcs
10
+ β€” No browser extension required to test
11
+
12
+ Mount order: /demo static HTML, /ws WebSocket, all other routes from main.py
13
+ """
14
+ from __future__ import annotations
15
+ import os
16
+ from fastapi import FastAPI
17
+ from fastapi.responses import HTMLResponse
18
+ from fastapi.middleware.cors import CORSMiddleware
19
+
20
+ # Re-export the main app with the demo page bolted on
21
+ from main import app as fact_app
22
+
23
+ # Inject the demo route
24
+ @fact_app.get("/demo", response_class=HTMLResponse, include_in_schema=False)
25
+ async def demo_page():
26
+ return HTMLResponse(DEMO_HTML)
27
+
28
+ @fact_app.get("/favicon.ico", include_in_schema=False)
29
+ async def favicon():
30
+ from fastapi.responses import Response
31
+ # 1x1 transparent gif
32
+ return Response(
33
+ content=b"GIF89a\x01\x00\x01\x00\x00\xff\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x00;",
34
+ media_type="image/gif",
35
+ )
36
+
37
+
38
+ DEMO_HTML = r"""<!DOCTYPE html>
39
+ <html lang="en">
40
+ <head>
41
+ <meta charset="UTF-8" />
42
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
43
+ <title>⚑ Fact & Hallucination Intelligence Engine</title>
44
+ <link rel="preconnect" href="https://fonts.googleapis.com"/>
45
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"/>
46
+ <style>
47
+ :root {
48
+ --bg: #07080f;
49
+ --surface: #0d0f1a;
50
+ --surface2: #111422;
51
+ --border: #1a1d2e;
52
+ --border2: #252840;
53
+ --text: #e8eaf6;
54
+ --text2: #7c83b0;
55
+ --text3: #3d4268;
56
+ --accent: #4f7cff;
57
+ --green: #22c55e;
58
+ --yellow: #eab308;
59
+ --red: #ef4444;
60
+ --purple: #a855f7;
61
+ --green-bg: rgba(34,197,94,0.08);
62
+ --yellow-bg: rgba(234,179,8,0.08);
63
+ --red-bg: rgba(239,68,68,0.08);
64
+ --purple-bg: rgba(168,85,247,0.08);
65
+ --radius: 12px;
66
+ --font: 'Space Grotesk', sans-serif;
67
+ --mono: 'JetBrains Mono', monospace;
68
+ }
69
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
70
+ html { scroll-behavior: smooth; }
71
+ body {
72
+ font-family: var(--font);
73
+ background: var(--bg);
74
+ color: var(--text);
75
+ min-height: 100vh;
76
+ overflow-x: hidden;
77
+ -webkit-font-smoothing: antialiased;
78
+ }
79
+
80
+ /* ── Animated grid background ── */
81
+ body::before {
82
+ content: '';
83
+ position: fixed; inset: 0; z-index: 0; pointer-events: none;
84
+ background-image:
85
+ linear-gradient(rgba(79,124,255,0.03) 1px, transparent 1px),
86
+ linear-gradient(90deg, rgba(79,124,255,0.03) 1px, transparent 1px);
87
+ background-size: 48px 48px;
88
+ mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, black 40%, transparent 100%);
89
+ }
90
+
91
+ .noise {
92
+ position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: 0.025;
93
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
94
+ background-repeat: repeat;
95
+ background-size: 256px;
96
+ }
97
+
98
+ main { position: relative; z-index: 1; max-width: 900px; margin: 0 auto; padding: 40px 24px 80px; }
99
+
100
+ /* ── Header ── */
101
+ header { text-align: center; margin-bottom: 48px; }
102
+ .logo-row {
103
+ display: flex; align-items: center; justify-content: center; gap: 12px;
104
+ margin-bottom: 12px;
105
+ }
106
+ .logo-icon {
107
+ width: 44px; height: 44px; border-radius: 10px;
108
+ background: linear-gradient(135deg, #1a2040 0%, #0d1030 100%);
109
+ border: 1px solid rgba(79,124,255,0.3);
110
+ display: flex; align-items: center; justify-content: center;
111
+ font-size: 22px;
112
+ box-shadow: 0 0 30px rgba(79,124,255,0.15);
113
+ }
114
+ h1 {
115
+ font-size: clamp(22px, 4vw, 32px);
116
+ font-weight: 700; letter-spacing: -0.02em;
117
+ background: linear-gradient(135deg, #e8eaf6 0%, #7c83b0 100%);
118
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
119
+ background-clip: text;
120
+ }
121
+ .subtitle {
122
+ color: var(--text2); font-size: 14px; max-width: 520px;
123
+ margin: 0 auto; line-height: 1.6;
124
+ }
125
+
126
+ /* ── Status bar ── */
127
+ .status-bar {
128
+ display: flex; align-items: center; justify-content: center;
129
+ gap: 8px; margin-top: 20px;
130
+ font-size: 12px; color: var(--text2);
131
+ font-family: var(--mono);
132
+ }
133
+ .status-dot {
134
+ width: 8px; height: 8px; border-radius: 50%; background: var(--text3);
135
+ transition: background 400ms;
136
+ }
137
+ .status-dot.connected { background: var(--green); box-shadow: 0 0 8px var(--green); animation: pulse 2s infinite; }
138
+ .status-dot.reconnecting { background: var(--yellow); animation: spin 1s linear infinite; }
139
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
140
+ @keyframes spin { from{transform:rotate(0)} to{transform:rotate(360deg)} }
141
+
142
+ /* ── Input section ── */
143
+ .input-card {
144
+ background: var(--surface);
145
+ border: 1px solid var(--border2);
146
+ border-radius: var(--radius);
147
+ padding: 20px;
148
+ margin-bottom: 24px;
149
+ box-shadow: 0 4px 24px rgba(0,0,0,0.4);
150
+ transition: border-color 300ms;
151
+ }
152
+ .input-card:focus-within { border-color: rgba(79,124,255,0.4); }
153
+
154
+ .input-header {
155
+ display: flex; align-items: center; justify-content: space-between;
156
+ margin-bottom: 12px;
157
+ }
158
+ .input-label { font-size: 12px; font-weight: 600; color: var(--text2); letter-spacing: 0.08em; text-transform: uppercase; }
159
+
160
+ .platform-select {
161
+ display: flex; gap: 6px;
162
+ }
163
+ .platform-btn {
164
+ padding: 3px 10px; border-radius: 20px; font-size: 11px; font-weight: 600;
165
+ cursor: pointer; border: 1px solid var(--border2);
166
+ background: transparent; color: var(--text2);
167
+ transition: all 150ms; font-family: var(--font);
168
+ }
169
+ .platform-btn.active {
170
+ background: rgba(79,124,255,0.15); border-color: rgba(79,124,255,0.5);
171
+ color: #7da4ff;
172
+ }
173
+
174
+ textarea {
175
+ width: 100%; min-height: 90px; padding: 12px;
176
+ background: var(--surface2); border: 1px solid var(--border);
177
+ border-radius: 8px; color: var(--text);
178
+ font-family: var(--mono); font-size: 13px; line-height: 1.6;
179
+ resize: vertical; outline: none;
180
+ transition: border-color 200ms;
181
+ }
182
+ textarea::placeholder { color: var(--text3); }
183
+ textarea:focus { border-color: rgba(79,124,255,0.4); }
184
+
185
+ .input-footer {
186
+ display: flex; align-items: center; justify-content: space-between;
187
+ margin-top: 12px;
188
+ }
189
+ .char-count { font-size: 11px; color: var(--text3); font-family: var(--mono); }
190
+
191
+ .analyze-btn {
192
+ display: flex; align-items: center; gap: 8px;
193
+ padding: 10px 22px; border-radius: 8px;
194
+ background: linear-gradient(135deg, #3060e0, #4f7cff);
195
+ border: none; color: white; font-family: var(--font);
196
+ font-size: 13px; font-weight: 600; cursor: pointer;
197
+ box-shadow: 0 4px 20px rgba(79,124,255,0.3);
198
+ transition: transform 150ms, box-shadow 150ms;
199
+ }
200
+ .analyze-btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 6px 28px rgba(79,124,255,0.4); }
201
+ .analyze-btn:active:not(:disabled) { transform: scale(0.97); }
202
+ .analyze-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
203
+
204
+ /* ── Quick examples ── */
205
+ .examples-row {
206
+ display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 24px;
207
+ }
208
+ .example-chip {
209
+ padding: 5px 12px; border-radius: 20px; font-size: 11px;
210
+ border: 1px solid var(--border2); background: var(--surface);
211
+ color: var(--text2); cursor: pointer; font-family: var(--font);
212
+ transition: all 150ms; white-space: nowrap;
213
+ }
214
+ .example-chip:hover {
215
+ background: var(--surface2); border-color: var(--border2);
216
+ color: var(--text);
217
+ }
218
+ .example-chip .dot {
219
+ display: inline-block; width: 6px; height: 6px; border-radius: 50%;
220
+ margin-right: 5px; vertical-align: middle;
221
+ }
222
+
223
+ /* ── Results ── */
224
+ #results { display: flex; flex-direction: column; gap: 14px; }
225
+
226
+ .result-card {
227
+ background: var(--surface);
228
+ border-radius: var(--radius);
229
+ overflow: hidden;
230
+ border: 1px solid var(--border2);
231
+ animation: slideIn 350ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
232
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
233
+ }
234
+ @keyframes slideIn {
235
+ from { opacity: 0; transform: translateY(16px) scale(0.97); }
236
+ to { opacity: 1; transform: translateY(0) scale(1); }
237
+ }
238
+
239
+ .result-header {
240
+ display: flex; align-items: center; gap: 12px;
241
+ padding: 14px 16px;
242
+ }
243
+ .result-icon {
244
+ width: 36px; height: 36px; border-radius: 50%;
245
+ display: flex; align-items: center; justify-content: center;
246
+ font-size: 16px; font-weight: 700; flex-shrink: 0;
247
+ border: 2px solid;
248
+ }
249
+ .result-meta { flex: 1; min-width: 0; }
250
+ .result-verdict { font-size: 14px; font-weight: 600; color: var(--text); }
251
+ .result-text {
252
+ font-size: 11px; color: var(--text2); margin-top: 2px;
253
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
254
+ }
255
+ .result-confidence {
256
+ display: flex; align-items: center; gap: 6px;
257
+ font-size: 20px; font-weight: 700;
258
+ font-family: var(--mono); flex-shrink: 0;
259
+ }
260
+
261
+ .result-body {
262
+ padding: 0 16px 14px;
263
+ border-top: 1px solid var(--border);
264
+ }
265
+ .result-explanation {
266
+ font-size: 13px; color: var(--text2); line-height: 1.6;
267
+ margin: 12px 0 10px; padding: 10px 12px;
268
+ background: var(--surface2); border-radius: 8px;
269
+ border-left: 3px solid;
270
+ }
271
+
272
+ .result-meta-row {
273
+ display: flex; align-items: center; gap: 12px;
274
+ margin-bottom: 10px;
275
+ }
276
+ .trust-bar-wrap { flex: 1; }
277
+ .trust-bar-label { font-size: 10px; color: var(--text3); margin-bottom: 4px; font-family: var(--mono); }
278
+ .trust-bar-track { height: 4px; background: var(--border2); border-radius: 2px; overflow: hidden; }
279
+ .trust-bar-fill { height: 100%; border-radius: 2px; transition: width 800ms cubic-bezier(0.4,0,0.2,1); }
280
+
281
+ .badge-row { display: flex; flex-wrap: wrap; gap: 6px; }
282
+ .badge {
283
+ padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;
284
+ font-family: var(--mono); letter-spacing: 0.05em;
285
+ text-transform: uppercase;
286
+ }
287
+
288
+ .sources-list { display: flex; flex-direction: column; gap: 4px; }
289
+ .source-item {
290
+ display: flex; align-items: center; gap: 6px;
291
+ padding: 5px 8px; border-radius: 6px;
292
+ background: rgba(255,255,255,0.02);
293
+ border: 1px solid var(--border);
294
+ text-decoration: none; color: #60a5fa; font-size: 11px;
295
+ transition: background 120ms;
296
+ overflow: hidden;
297
+ }
298
+ .source-item:hover { background: rgba(96,165,250,0.06); }
299
+ .source-domain { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
300
+ .processing-time { font-size: 10px; color: var(--text3); font-family: var(--mono); margin-top: 8px; text-align: right; }
301
+
302
+ /* ── Spinner ── */
303
+ .spinner-card {
304
+ background: var(--surface); border: 1px solid var(--border2);
305
+ border-radius: var(--radius); padding: 24px;
306
+ display: flex; align-items: center; gap: 16px;
307
+ animation: fadeIn 200ms ease;
308
+ }
309
+ @keyframes fadeIn { from{opacity:0} to{opacity:1} }
310
+ .spinner {
311
+ width: 28px; height: 28px; border-radius: 50%;
312
+ border: 2.5px solid var(--border2); border-top-color: var(--accent);
313
+ animation: spin 0.9s linear infinite; flex-shrink: 0;
314
+ }
315
+ .spinner-text { color: var(--text2); font-size: 13px; }
316
+ .spinner-stage { color: var(--text3); font-size: 11px; font-family: var(--mono); margin-top: 3px; }
317
+
318
+ /* ── Stats strip ── */
319
+ .stats-strip {
320
+ display: grid; grid-template-columns: repeat(4,1fr); gap: 1px;
321
+ background: var(--border); border-radius: var(--radius);
322
+ overflow: hidden; margin-bottom: 24px;
323
+ border: 1px solid var(--border);
324
+ }
325
+ .stat-cell {
326
+ background: var(--surface); padding: 14px 12px; text-align: center;
327
+ }
328
+ .stat-value { font-size: 22px; font-weight: 700; font-family: var(--mono); color: var(--text); }
329
+ .stat-label { font-size: 10px; color: var(--text3); text-transform: uppercase; letter-spacing: 0.1em; margin-top: 2px; }
330
+
331
+ /* ── Color palette indicator ── */
332
+ .palette-row {
333
+ display: flex; gap: 8px; margin-bottom: 24px;
334
+ flex-wrap: wrap;
335
+ }
336
+ .palette-item {
337
+ flex: 1; min-width: 120px; padding: 12px;
338
+ border-radius: 8px; border: 1px solid; display: flex; align-items: center; gap: 8px;
339
+ }
340
+ .palette-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
341
+ .palette-label { font-size: 12px; font-weight: 600; }
342
+ .palette-desc { font-size: 10px; margin-top: 1px; opacity: 0.7; }
343
+
344
+ @media (max-width: 600px) {
345
+ .stats-strip { grid-template-columns: 1fr 1fr; }
346
+ .platform-select { flex-wrap: wrap; }
347
+ .palette-row { flex-direction: column; }
348
+ }
349
+ </style>
350
+ </head>
351
+ <body>
352
+ <div class="noise"></div>
353
+ <main>
354
+ <!-- Header -->
355
+ <header>
356
+ <div class="logo-row">
357
+ <div class="logo-icon">⚑</div>
358
+ <h1>Fact & Hallucination Intelligence Engine</h1>
359
+ </div>
360
+ <p class="subtitle">Real-time claim verification across Twitter/X, Instagram, YouTube, and AI chat interfaces. Powered by Groq Β· BGE-M3 Β· Qdrant Β· Memgraph.</p>
361
+ <div class="status-bar">
362
+ <div class="status-dot" id="statusDot"></div>
363
+ <span id="statusText">Connecting…</span>
364
+ <span style="color:var(--text3)">Β·</span>
365
+ <span id="clientIdSpan" style="color:var(--text3)">β€”</span>
366
+ </div>
367
+ </header>
368
+
369
+ <!-- Stats -->
370
+ <div class="stats-strip">
371
+ <div class="stat-cell"><div class="stat-value" id="statTotal">0</div><div class="stat-label">Analyzed</div></div>
372
+ <div class="stat-cell"><div class="stat-value" id="statFlagged" style="color:var(--red)">0</div><div class="stat-label">Flagged</div></div>
373
+ <div class="stat-cell"><div class="stat-value" id="statCached" style="color:var(--accent)">0</div><div class="stat-label">Cached</div></div>
374
+ <div class="stat-cell"><div class="stat-value" id="statAvgMs">β€”</div><div class="stat-label">Avg ms</div></div>
375
+ </div>
376
+
377
+ <!-- Color key -->
378
+ <div class="palette-row">
379
+ <div class="palette-item" style="border-color:rgba(34,197,94,0.3);background:rgba(34,197,94,0.05)">
380
+ <div class="palette-dot" style="background:#22c55e"></div>
381
+ <div><div class="palette-label" style="color:#22c55e">Verified</div><div class="palette-desc" style="color:#22c55e">Widely corroborated</div></div>
382
+ </div>
383
+ <div class="palette-item" style="border-color:rgba(234,179,8,0.3);background:rgba(234,179,8,0.05)">
384
+ <div class="palette-dot" style="background:#eab308"></div>
385
+ <div><div class="palette-label" style="color:#eab308">Unverified</div><div class="palette-desc" style="color:#eab308">Breaking / contested</div></div>
386
+ </div>
387
+ <div class="palette-item" style="border-color:rgba(239,68,68,0.3);background:rgba(239,68,68,0.05)">
388
+ <div class="palette-dot" style="background:#ef4444"></div>
389
+ <div><div class="palette-label" style="color:#ef4444">Misleading</div><div class="palette-desc" style="color:#ef4444">Debunked / false</div></div>
390
+ </div>
391
+ <div class="palette-item" style="border-color:rgba(168,85,247,0.3);background:rgba(168,85,247,0.05)">
392
+ <div class="palette-dot" style="background:#a855f7"></div>
393
+ <div><div class="palette-label" style="color:#a855f7">AI Hallucination</div><div class="palette-desc" style="color:#a855f7">Fabricated / impossible</div></div>
394
+ </div>
395
+ </div>
396
+
397
+ <!-- Input -->
398
+ <div class="input-card">
399
+ <div class="input-header">
400
+ <span class="input-label">Enter a claim to analyze</span>
401
+ <div class="platform-select" id="platformSelect">
402
+ <button class="platform-btn active" data-platform="web">Web</button>
403
+ <button class="platform-btn" data-platform="x">X / Twitter</button>
404
+ <button class="platform-btn" data-platform="youtube">YouTube</button>
405
+ <button class="platform-btn" data-platform="chatgpt">AI Chat</button>
406
+ </div>
407
+ </div>
408
+ <textarea id="claimInput" placeholder="e.g. The unemployment rate hit 4.2% in September 2024, the highest since early 2022…" maxlength="800" rows="3"></textarea>
409
+ <div class="input-footer">
410
+ <span class="char-count"><span id="charCount">0</span> / 800</span>
411
+ <button class="analyze-btn" id="analyzeBtn" onclick="analyze()">
412
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
413
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
414
+ </svg>
415
+ Analyze
416
+ </button>
417
+ </div>
418
+ </div>
419
+
420
+ <!-- Quick examples -->
421
+ <div class="examples-row" id="examplesRow">
422
+ <span style="font-size:11px;color:var(--text3);align-self:center;margin-right:4px;font-weight:600;text-transform:uppercase;letter-spacing:0.08em">Try:</span>
423
+ </div>
424
+
425
+ <!-- Results -->
426
+ <div id="results"></div>
427
+ </main>
428
+
429
+ <script>
430
+ // ── Config ────────────────────────────────────────────────────────────────────
431
+ const WS_BASE = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
432
+ const WS_URL = `${WS_BASE}//${window.location.host}`;
433
+ const CLIENT_ID = 'demo-' + Math.random().toString(36).slice(2,10);
434
+
435
+ // ── State ─────────────────────────────────────────────────────────────────────
436
+ let ws = null;
437
+ let wsStatus = 'offline';
438
+ let selectedPlatform = 'web';
439
+ let stats = { total: 0, flagged: 0, cached: 0, totalMs: 0 };
440
+ let reconnectDelay = 1000;
441
+
442
+ // ── Quick examples ────────────────────────────────────────────────────────────
443
+ const EXAMPLES = [
444
+ { text: "The US unemployment rate hit 4.2% in September 2024, the highest since early 2022.", color: "green" },
445
+ { text: "Scientists discovered vaccines contain microchips that transmit location data to governments.", color: "red" },
446
+ { text: "SpaceX's Starship achieved its first successful ocean splashdown on its fourth integrated flight test.", color: "green" },
447
+ { text: "According to a Harvard study published in Nature, AI systems have achieved human-level sentience as of 2024.", color: "purple", platform: "chatgpt" },
448
+ { text: "BREAKING: WHO declares emergency as novel pathogen spreads to 15 countries overnight.", color: "yellow" },
449
+ { text: "Eating 3 tablespoons of olive oil daily reduces Alzheimer's risk by 90% β€” study shows.", color: "red" },
450
+ ];
451
+
452
+ function initExamples() {
453
+ const row = document.getElementById('examplesRow');
454
+ EXAMPLES.forEach(ex => {
455
+ const colors = { green:'#22c55e', yellow:'#eab308', red:'#ef4444', purple:'#a855f7' };
456
+ const btn = document.createElement('button');
457
+ btn.className = 'example-chip';
458
+ btn.innerHTML = `<span class="dot" style="background:${colors[ex.color]}"></span>${ex.text.slice(0,42)}…`;
459
+ btn.onclick = () => {
460
+ document.getElementById('claimInput').value = ex.text;
461
+ updateCharCount();
462
+ if (ex.platform) selectPlatform(ex.platform);
463
+ analyze();
464
+ };
465
+ row.appendChild(btn);
466
+ });
467
+ }
468
+
469
+ // ── Platform selector ─────────────────────────────────────────────────────────
470
+ document.getElementById('platformSelect').addEventListener('click', e => {
471
+ const btn = e.target.closest('[data-platform]');
472
+ if (btn) selectPlatform(btn.dataset.platform);
473
+ });
474
+
475
+ function selectPlatform(p) {
476
+ selectedPlatform = p;
477
+ document.querySelectorAll('.platform-btn').forEach(b => {
478
+ b.classList.toggle('active', b.dataset.platform === p);
479
+ });
480
+ }
481
+
482
+ // ── Char count ──────────��─────────────────────────────────────────────────────
483
+ const claimInput = document.getElementById('claimInput');
484
+ claimInput.addEventListener('input', updateCharCount);
485
+ function updateCharCount() {
486
+ document.getElementById('charCount').textContent = claimInput.value.length;
487
+ }
488
+
489
+ // ── Enter to submit ───────────────────────────────────────────────────────────
490
+ claimInput.addEventListener('keydown', e => {
491
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) analyze();
492
+ });
493
+
494
+ // ── WebSocket ─────────────────────────────────────────────────────────────────
495
+ function connect() {
496
+ setStatus('reconnecting');
497
+ try {
498
+ ws = new WebSocket(`${WS_URL}/ws/${CLIENT_ID}`);
499
+ ws.onopen = () => {
500
+ setStatus('connected');
501
+ reconnectDelay = 1000;
502
+ document.getElementById('clientIdSpan').textContent = CLIENT_ID;
503
+ };
504
+ ws.onmessage = e => {
505
+ try { handleMessage(JSON.parse(e.data)); } catch {}
506
+ };
507
+ ws.onclose = () => {
508
+ setStatus('offline');
509
+ setTimeout(connect, reconnectDelay);
510
+ reconnectDelay = Math.min(reconnectDelay * 2, 30000);
511
+ };
512
+ ws.onerror = () => ws.close();
513
+ } catch {
514
+ setStatus('offline');
515
+ setTimeout(connect, reconnectDelay);
516
+ }
517
+ }
518
+
519
+ function setStatus(s) {
520
+ wsStatus = s;
521
+ const dot = document.getElementById('statusDot');
522
+ const txt = document.getElementById('statusText');
523
+ dot.className = 'status-dot ' + s;
524
+ const labels = { connected:'Engine online', reconnecting:'Connecting…', offline:'Offline' };
525
+ txt.textContent = labels[s] || s;
526
+ }
527
+
528
+ // ── Analyze ───────────────────────────────────────────────────────────────────
529
+ function analyze() {
530
+ const text = claimInput.value.trim();
531
+ if (!text || text.length < 20) {
532
+ claimInput.style.borderColor = 'rgba(239,68,68,0.5)';
533
+ setTimeout(() => claimInput.style.borderColor = '', 1000);
534
+ return;
535
+ }
536
+
537
+ const btn = document.getElementById('analyzeBtn');
538
+ btn.disabled = true;
539
+ btn.innerHTML = `<div class="spinner" style="width:14px;height:14px;border-width:2px"></div> Analyzing…`;
540
+
541
+ const spinnerId = 'spinner-' + Date.now();
542
+ prependSpinner(spinnerId, text);
543
+
544
+ const payload = {
545
+ client_id: CLIENT_ID,
546
+ claims: [text],
547
+ platform: selectedPlatform,
548
+ timestamp: Date.now() / 1000,
549
+ };
550
+
551
+ if (ws && ws.readyState === WebSocket.OPEN) {
552
+ ws.send(JSON.stringify(payload));
553
+ } else {
554
+ // Fallback: HTTP polling via /analyze endpoint if available
555
+ fetch('/analyze', {
556
+ method: 'POST',
557
+ headers: { 'Content-Type': 'application/json' },
558
+ body: JSON.stringify(payload),
559
+ })
560
+ .then(r => r.json())
561
+ .then(data => { if (data.results) handleResults(data.results, spinnerId); })
562
+ .catch(() => removeSpinner(spinnerId))
563
+ .finally(() => resetBtn());
564
+ }
565
+
566
+ // Timeout safety
567
+ setTimeout(() => { removeSpinner(spinnerId); resetBtn(); }, 15000);
568
+ }
569
+
570
+ function resetBtn() {
571
+ const btn = document.getElementById('analyzeBtn');
572
+ btn.disabled = false;
573
+ btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg> Analyze`;
574
+ }
575
+
576
+ function handleMessage(msg) {
577
+ if (msg.type === 'analysis_batch') {
578
+ // Find active spinner
579
+ const spinner = document.querySelector('[data-spinner]');
580
+ const spinnerId = spinner?.id;
581
+ handleResults(msg.results, spinnerId);
582
+ resetBtn();
583
+ }
584
+ }
585
+
586
+ function handleResults(results, spinnerId) {
587
+ if (spinnerId) removeSpinner(spinnerId);
588
+ results.forEach(r => {
589
+ renderResult(r);
590
+ updateStats(r);
591
+ });
592
+ }
593
+
594
+ // ── Spinner ───────────────────────────────────────────────────────────────────
595
+ const STAGES = ['Classifying claim…', 'Searching evidence…', 'Scoring trust graph…', 'Running agents…'];
596
+ function prependSpinner(id, text) {
597
+ const container = document.getElementById('results');
598
+ const div = document.createElement('div');
599
+ div.id = id;
600
+ div.setAttribute('data-spinner', '1');
601
+ div.className = 'spinner-card';
602
+ div.innerHTML = `
603
+ <div class="spinner"></div>
604
+ <div>
605
+ <div class="spinner-text">${escHtml(text.slice(0,80))}${text.length>80?'…':''}</div>
606
+ <div class="spinner-stage" id="${id}-stage">${STAGES[0]}</div>
607
+ </div>`;
608
+ container.prepend(div);
609
+ // Animate through stages
610
+ let si = 0;
611
+ const iv = setInterval(() => {
612
+ si = (si+1) % STAGES.length;
613
+ const el = document.getElementById(`${id}-stage`);
614
+ if (el) el.textContent = STAGES[si]; else clearInterval(iv);
615
+ }, 900);
616
+ div._stageInterval = iv;
617
+ }
618
+ function removeSpinner(id) {
619
+ const el = document.getElementById(id);
620
+ if (el) { clearInterval(el._stageInterval); el.remove(); }
621
+ }
622
+
623
+ // ── Result card ───────────────────────────────────────────────────────────────
624
+ const COLOR_CFG = {
625
+ green: { hex:'#22c55e', bg:'var(--green-bg)', emoji:'βœ“', label:'Verified' },
626
+ yellow: { hex:'#eab308', bg:'var(--yellow-bg)', emoji:'~', label:'Unverified' },
627
+ red: { hex:'#ef4444', bg:'var(--red-bg)', emoji:'βœ—', label:'Misleading' },
628
+ purple: { hex:'#a855f7', bg:'var(--purple-bg)', emoji:'?', label:'AI Hallucination' },
629
+ };
630
+
631
+ function renderResult(r) {
632
+ const cfg = COLOR_CFG[r.color] || COLOR_CFG.yellow;
633
+ const trustPct = Math.round((r.trust_score || 0.5) * 100);
634
+ const container = document.getElementById('results');
635
+
636
+ const card = document.createElement('div');
637
+ card.className = 'result-card';
638
+ card.style.borderColor = cfg.hex + '44';
639
+ card.style.animationDelay = '0ms';
640
+
641
+ const sourcesHtml = (r.sources || []).slice(0,3).map(url => {
642
+ let domain;
643
+ try { domain = new URL(url).hostname.replace('www.',''); } catch { domain = url.slice(0,30); }
644
+ return `<a class="source-item" href="${escHtml(url)}" target="_blank" rel="noreferrer">
645
+ <img src="https://www.google.com/s2/favicons?domain=${domain}&sz=16" width="12" height="12" style="border-radius:2px;flex-shrink:0" onerror="this.style.display='none'"/>
646
+ <span class="source-domain">${escHtml(domain)}</span>
647
+ </a>`;
648
+ }).join('');
649
+
650
+ const badges = [
651
+ r.cached ? `<span class="badge" style="background:rgba(79,124,255,0.15);color:#7da4ff">cached</span>` : '',
652
+ `<span class="badge" style="background:rgba(255,255,255,0.04);color:var(--text2)">${escHtml(r.platform||'web')}</span>`,
653
+ r.processing_ms ? `<span class="badge" style="background:rgba(255,255,255,0.03);color:var(--text3)">${Math.round(r.processing_ms)}ms</span>` : '',
654
+ ].filter(Boolean).join('');
655
+
656
+ card.innerHTML = `
657
+ <div class="result-header" style="background:${cfg.bg}">
658
+ <div class="result-icon" style="border-color:${cfg.hex};color:${cfg.hex};background:${cfg.hex}15">${cfg.emoji}</div>
659
+ <div class="result-meta">
660
+ <div class="result-verdict" style="color:${cfg.hex}">${escHtml(r.verdict || cfg.label)}</div>
661
+ <div class="result-text">${escHtml((r.claim_text||'').slice(0,90))}${(r.claim_text||'').length>90?'…':''}</div>
662
+ </div>
663
+ <div class="result-confidence" style="color:${cfg.hex}">${r.confidence}<span style="font-size:11px;color:var(--text3);margin-left:1px">%</span></div>
664
+ </div>
665
+ <div class="result-body">
666
+ <div class="result-explanation" style="border-left-color:${cfg.hex}">${escHtml(r.explanation||'No explanation available.')}</div>
667
+ <div class="result-meta-row">
668
+ <div class="trust-bar-wrap">
669
+ <div class="trust-bar-label">Trust Score Β· ${trustPct}%</div>
670
+ <div class="trust-bar-track"><div class="trust-bar-fill" style="width:0%;background:${cfg.hex}" data-target="${trustPct}"></div></div>
671
+ </div>
672
+ </div>
673
+ ${badges ? `<div class="badge-row" style="margin-bottom:10px">${badges}</div>` : ''}
674
+ ${sourcesHtml ? `<div class="sources-list">${sourcesHtml}</div>` : ''}
675
+ </div>`;
676
+
677
+ container.prepend(card);
678
+
679
+ // Animate trust bar after paint
680
+ requestAnimationFrame(() => {
681
+ const fill = card.querySelector('.trust-bar-fill');
682
+ if (fill) fill.style.width = fill.dataset.target + '%';
683
+ });
684
+ }
685
+
686
+ // ── Stats ─────────────────────────────────────────────────────────────────────
687
+ function updateStats(r) {
688
+ stats.total++;
689
+ if (r.color === 'red' || r.color === 'purple') stats.flagged++;
690
+ if (r.cached) stats.cached++;
691
+ if (r.processing_ms) stats.totalMs += r.processing_ms;
692
+
693
+ document.getElementById('statTotal').textContent = stats.total;
694
+ document.getElementById('statFlagged').textContent = stats.flagged;
695
+ document.getElementById('statCached').textContent = stats.cached;
696
+ const avgMs = stats.total > 0 ? Math.round(stats.totalMs / stats.total) : 0;
697
+ document.getElementById('statAvgMs').textContent = avgMs > 0 ? avgMs + 'ms' : 'β€”';
698
+ }
699
+
700
+ function escHtml(s) {
701
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
702
+ }
703
+
704
+ // ── Init ──────────────────────────────────────────────────────────────────────
705
+ initExamples();
706
+ connect();
707
+ </script>
708
+ </body>
709
+ </html>
710
+ """
711
+
712
+ # This module is imported by Dockerfile CMD via main.py
713
+ # For Hugging Face, rename this file to main.py or import it as the entry point.