plexdx commited on
Commit
f09b792
·
verified ·
1 Parent(s): f589dab

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +82 -707
app.py CHANGED
@@ -1,713 +1,88 @@
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.
 
1
  """
2
+ app.py — Hugging Face Spaces entry point (Gradio SDK).
3
 
4
+ HF Spaces Gradio SDK always looks for app.py.
5
+ We mount our FastAPI app inside Gradio so both the
6
+ /demo dashboard and /ws WebSocket work correctly.
 
 
 
 
 
 
7
  """
8
  from __future__ import annotations
 
 
 
 
 
 
 
9
 
10
+ import os
11
+ import threading
12
+ import uvicorn
13
+ import gradio as gr
14
+
15
+ # ── Import the full FastAPI app (demo UI + WS + HTTP analyze) ─────────────────
16
+ from hf_main import app as fastapi_app # FastAPI instance
17
+
18
+ # ── Port ──────────────────────────────────────────────────────────────────────
19
+ PORT = int(os.getenv("PORT", "7860"))
20
+
21
+
22
+ # ── Option A: Mount FastAPI directly into Gradio (recommended for HF) ─────────
23
+ # This lets Gradio handle auth/OAuth while we serve our own routes.
24
+
25
+ def create_gradio_app():
26
+ with gr.Blocks(
27
+ title="⚡ Fact & Hallucination Intelligence Engine",
28
+ theme=gr.themes.Base(
29
+ primary_hue="blue",
30
+ neutral_hue="slate",
31
+ ),
32
+ css="""
33
+ body, .gradio-container { background: #07080f !important; }
34
+ #main-iframe { width:100%; height:100vh; border:none; }
35
+ .hide-gradio-footer footer { display: none !important; }
36
+ """,
37
+ ) as demo:
38
+ gr.HTML("""
39
+ <style>
40
+ .gradio-container { padding: 0 !important; max-width: 100% !important; }
41
+ footer { display: none !important; }
42
+ </style>
43
+ <iframe
44
+ id="main-iframe"
45
+ src="/demo"
46
+ style="width:100%;height:100vh;border:none;display:block;background:#07080f"
47
+ ></iframe>
48
+ """)
49
+
50
+ return demo
51
+
52
+
53
+ # ── Mount Gradio into FastAPI so /demo and /ws are siblings ───────────────────
54
+ gradio_demo = create_gradio_app()
55
+
56
+ # Mount Gradio at root ("/") — our custom routes (/demo, /ws, /health, /analyze)
57
+ # are already registered on fastapi_app and take priority via route order.
58
+ app = gr.mount_gradio_app(
59
+ fastapi_app, # existing FastAPI app
60
+ gradio_demo, # Gradio Blocks
61
+ path="/gradio", # Gradio UI at /gradio, our /demo stays at /demo
62
+ )
63
+
64
+
65
+ # ── Redirect root "/" to /demo so the Space landing page is our dashboard ──────
66
+ from fastapi.responses import RedirectResponse
67
+
68
+ @fastapi_app.get("/", include_in_schema=False)
69
+ async def root_redirect():
70
+ return RedirectResponse(url="/demo")
71
+
72
+
73
+ # ── Launch ────────────────────────────────────────────────────────────────────
74
+ if __name__ == "__main__":
75
+ print(f"[fact-engine] Starting on port {PORT}")
76
+ print(f"[fact-engine] Demo UI → http://0.0.0.0:{PORT}/demo")
77
+ print(f"[fact-engine] WebSocket → ws://0.0.0.0:{PORT}/ws/{{client_id}}")
78
+ print(f"[fact-engine] Health → http://0.0.0.0:{PORT}/health")
79
+
80
+ uvicorn.run(
81
+ app, # the gr.mount_gradio_app-wrapped app
82
+ host="0.0.0.0",
83
+ port=PORT,
84
+ log_level="info",
85
+ ws_ping_interval=20,
86
+ ws_ping_timeout=30,
87
+ timeout_keep_alive=30,
88
  )