akhaliq HF Staff commited on
Commit
ca9c6e4
Β·
1 Parent(s): a03138e

style: redesign UI with modern glassmorphism, ambient animations, and refreshed typography

Browse files
Files changed (1) hide show
  1. index.html +301 -200
index.html CHANGED
@@ -4,292 +4,374 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>OpenAI Privacy Filter</title>
7
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
8
  <style>
 
9
  :root {
10
- --bg-color: #0f172a;
11
- --panel-bg: rgba(30, 41, 59, 0.7);
12
- --border-color: rgba(255, 255, 255, 0.1);
13
- --text-main: #f8fafc;
14
- --text-muted: #94a3b8;
15
- --accent: #3b82f6;
16
- --accent-hover: #2563eb;
17
- --glass-blur: blur(12px);
 
18
 
19
  /* Entity Colors */
20
- --entity-account_number: #ef4444;
21
- --entity-private_address: #f59e0b;
22
- --entity-private_email: #10b981;
23
- --entity-private_person: #8b5cf6;
24
- --entity-private_phone: #ec4899;
25
- --entity-private_url: #06b6d4;
26
- --entity-private_date: #eab308;
27
- --entity-secret: #ef4444;
28
  }
29
-
30
  body {
31
  margin: 0;
32
  padding: 0;
 
 
33
  font-family: 'Inter', sans-serif;
34
- background: radial-gradient(circle at 50% -20%, #1e293b, var(--bg-color));
35
- color: var(--text-main);
36
  min-height: 100vh;
37
  display: flex;
38
  flex-direction: column;
39
- align-items: center;
40
  overflow-x: hidden;
41
  }
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  .container {
44
- width: 100%;
45
  max-width: 1000px;
46
- margin: 40px auto;
47
- padding: 0 20px;
 
48
  box-sizing: border-box;
 
49
  display: flex;
50
  flex-direction: column;
51
- gap: 24px;
52
  }
53
 
54
  header {
55
  text-align: center;
56
- margin-bottom: 20px;
57
- animation: fadeIn 0.8s ease-out;
 
 
 
58
  }
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  h1 {
61
- font-size: 2.5rem;
62
- font-weight: 700;
63
- margin-bottom: 10px;
64
- background: linear-gradient(to right, #60a5fa, #a78bfa);
 
 
 
65
  -webkit-background-clip: text;
66
  -webkit-text-fill-color: transparent;
67
  }
68
 
69
- p.subtitle {
70
- color: var(--text-muted);
71
- font-size: 1.1rem;
72
- max-width: 600px;
73
- margin: 0 auto;
74
  line-height: 1.6;
 
75
  }
76
 
77
  .glass-panel {
78
- background: var(--panel-bg);
79
- backdrop-filter: var(--glass-blur);
80
- -webkit-backdrop-filter: var(--glass-blur);
81
- border: 1px solid var(--border-color);
82
- border-radius: 16px;
83
- padding: 24px;
84
- box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
85
- animation: slideUp 0.6s ease-out;
 
 
 
 
 
86
  }
87
 
88
  textarea {
89
  width: 100%;
90
- height: 150px;
91
- background: rgba(15, 23, 42, 0.6);
92
- border: 1px solid var(--border-color);
93
  border-radius: 12px;
94
- color: var(--text-main);
95
  font-family: 'Inter', sans-serif;
96
- font-size: 1rem;
97
- padding: 16px;
98
- box-sizing: border-box;
99
  resize: vertical;
100
- transition: border-color 0.3s;
101
- line-height: 1.5;
102
  }
103
 
104
  textarea:focus {
105
  outline: none;
106
  border-color: var(--accent);
107
- box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
 
108
  }
109
 
110
- button {
111
- background: var(--accent);
 
 
112
  color: white;
113
  border: none;
114
- border-radius: 8px;
115
- padding: 12px 24px;
116
  font-size: 1rem;
117
  font-weight: 600;
118
  cursor: pointer;
119
- transition: all 0.3s;
120
  display: flex;
121
  align-items: center;
122
  justify-content: center;
123
- gap: 8px;
124
- width: fit-content;
 
125
  }
126
 
127
- button:hover {
128
- background: var(--accent-hover);
129
  transform: translateY(-2px);
130
- box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
131
- }
132
-
133
- button:active {
134
- transform: translateY(0);
135
  }
136
 
137
- button:disabled {
138
- background: #475569;
139
- cursor: not-allowed;
140
- transform: none;
141
- box-shadow: none;
142
- }
143
 
144
- .loading-spinner {
145
- width: 20px;
146
- height: 20px;
147
- border: 3px solid rgba(255,255,255,0.3);
 
148
  border-radius: 50%;
149
  border-top-color: white;
150
- animation: spin 1s ease-in-out infinite;
151
  display: none;
152
  }
153
 
154
- .results-area {
155
- display: flex;
156
- flex-direction: column;
157
- gap: 20px;
158
- }
159
 
160
- .highlighted-text {
161
- font-size: 1.1rem;
162
- line-height: 1.8;
163
- white-space: pre-wrap;
164
- background: rgba(15, 23, 42, 0.4);
165
- padding: 20px;
166
- border-radius: 12px;
167
- border: 1px solid rgba(255, 255, 255, 0.05);
168
- min-height: 100px;
169
  }
170
 
171
- .entity {
172
- display: inline-flex;
 
173
  align-items: center;
174
- border-radius: 4px;
175
- padding: 2px 6px;
176
- margin: 0 2px;
177
- font-weight: 500;
178
- position: relative;
179
- cursor: help;
180
- transition: all 0.2s;
181
  }
182
 
183
- .entity:hover {
184
- transform: scale(1.05);
185
- z-index: 10;
 
 
186
  }
187
 
188
- .entity-label {
189
- font-size: 0.7rem;
190
- text-transform: uppercase;
191
- letter-spacing: 0.5px;
192
- margin-left: 6px;
193
- opacity: 0.8;
194
- background: rgba(0,0,0,0.2);
195
- padding: 2px 4px;
196
- border-radius: 3px;
197
  }
198
 
199
- .summary-panel {
200
- display: grid;
201
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
202
- gap: 12px;
 
 
 
 
 
203
  }
 
 
204
 
205
- .summary-card {
206
- background: rgba(255, 255, 255, 0.03);
207
- border: 1px solid rgba(255, 255, 255, 0.05);
208
  border-radius: 8px;
209
- padding: 12px;
210
  display: flex;
211
- flex-direction: column;
212
- gap: 4px;
 
213
  }
214
 
215
- .summary-card .count {
216
- font-size: 1.5rem;
217
- font-weight: 700;
218
- }
219
 
220
- .summary-card .label {
221
- font-size: 0.85rem;
222
- color: var(--text-muted);
223
- text-transform: uppercase;
224
- letter-spacing: 0.5px;
 
 
 
 
225
  }
226
 
227
- @keyframes fadeIn {
228
- from { opacity: 0; }
229
- to { opacity: 1; }
 
 
 
 
 
 
 
 
230
  }
231
 
232
- @keyframes slideUp {
233
- from { opacity: 0; transform: translateY(20px); }
234
- to { opacity: 1; transform: translateY(0); }
235
- }
236
 
237
- @keyframes spin {
238
- to { transform: rotate(360deg); }
239
- }
240
-
241
- .controls {
242
- display: flex;
243
- justify-content: space-between;
244
- align-items: center;
245
- margin-top: 16px;
246
  }
247
-
 
248
  .legend {
249
  display: flex;
250
  flex-wrap: wrap;
251
- gap: 10px;
252
- margin-top: 20px;
253
  justify-content: center;
 
 
 
 
 
254
  }
255
-
256
  .legend-item {
257
  display: flex;
258
  align-items: center;
259
- gap: 6px;
260
  font-size: 0.85rem;
261
- color: var(--text-muted);
262
- }
263
-
264
- .legend-color {
265
- width: 12px;
266
- height: 12px;
267
- border-radius: 50%;
268
  }
269
 
 
 
270
  </style>
271
  </head>
272
  <body>
 
 
 
 
273
  <div class="container">
274
  <header>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  <h1>OpenAI Privacy Filter</h1>
276
- <p class="subtitle">Detect and mask personally identifiable information (PII) in text securely and efficiently using the bidirectional token-classification model.</p>
277
  </header>
278
 
279
  <div class="glass-panel">
280
- <textarea id="inputText" placeholder="Paste your text here... For example: My name is Alice Smith and my email is alice@example.com."></textarea>
281
- <div class="controls">
282
- <button id="analyzeBtn">
 
 
 
283
  <span class="btn-text">Analyze Text</span>
284
- <div class="loading-spinner" id="spinner"></div>
285
  </button>
286
  </div>
287
  </div>
288
 
289
- <div class="glass-panel results-area" style="display: none;" id="resultsPanel">
290
- <h3>Detection Results</h3>
291
- <div class="summary-panel" id="summaryPanel"></div>
292
- <div class="highlighted-text" id="outputText"></div>
 
 
 
 
 
 
293
  </div>
294
 
295
  <div class="legend" id="legend"></div>
@@ -310,12 +392,24 @@
310
  'secret': 'var(--entity-secret)'
311
  };
312
 
313
- // Setup legend
 
 
 
 
 
 
 
 
 
 
 
314
  const legendContainer = document.getElementById('legend');
315
  for (const [entity, color] of Object.entries(entityColors)) {
316
  const item = document.createElement('div');
317
  item.className = 'legend-item';
318
- item.innerHTML = `<div class="legend-color" style="background-color: ${color}"></div><span>${entity.replace('private_', '').toUpperCase()}</span>`;
 
319
  legendContainer.appendChild(item);
320
  }
321
 
@@ -323,9 +417,10 @@
323
  const analyzeBtn = document.getElementById('analyzeBtn');
324
  const spinner = document.getElementById('spinner');
325
  const btnText = document.querySelector('.btn-text');
 
326
  const resultsPanel = document.getElementById('resultsPanel');
327
  const outputText = document.getElementById('outputText');
328
- const summaryPanel = document.getElementById('summaryPanel');
329
 
330
  let client = null;
331
 
@@ -333,7 +428,7 @@
333
  try {
334
  client = await Client.connect(window.location.origin);
335
  } catch (error) {
336
- console.error("Failed to connect to Gradio server:", error);
337
  }
338
  }
339
 
@@ -346,66 +441,74 @@
346
  if (!client) {
347
  await initClient();
348
  if (!client) {
349
- alert("Could not connect to the backend. Is it running?");
350
  return;
351
  }
352
  }
353
 
354
  analyzeBtn.disabled = true;
 
 
355
  spinner.style.display = 'block';
356
- btnText.textContent = 'Analyzing...';
357
  resultsPanel.style.display = 'none';
358
 
359
  try {
360
- // Call the Gradio API
361
  const result = await client.predict("/predict", { text });
362
  const entities = result.data[0];
363
-
364
  renderResults(text, entities);
365
 
366
  resultsPanel.style.display = 'flex';
367
- resultsPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
368
  } catch (error) {
369
- console.error("Analysis error:", error);
370
- alert("An error occurred during analysis: " + error.message);
371
  } finally {
372
  analyzeBtn.disabled = false;
373
- spinner.style.display = 'none';
374
  btnText.textContent = 'Analyze Text';
 
 
375
  }
376
  });
377
 
378
  function renderResults(text, entities) {
379
  if (!entities || entities.length === 0) {
380
  outputText.textContent = text;
381
- summaryPanel.innerHTML = '<div class="summary-card"><span class="count">0</span><span class="label">Entities Found</span></div>';
 
 
 
 
 
 
 
382
  return;
383
  }
384
 
385
- // Count summary
386
  const counts = {};
387
  entities.forEach(ent => {
388
  let type = ent.entity || ent.entity_group;
389
- if (type.startsWith('B-') || type.startsWith('I-') || type.startsWith('E-') || type.startsWith('S-')) {
390
- type = type.substring(2);
391
- }
392
  counts[type] = (counts[type] || 0) + 1;
393
  });
394
 
395
- summaryPanel.innerHTML = Object.entries(counts).map(([type, count]) => {
396
  const color = entityColors[type] || '#888';
397
  const label = type.replace('private_', '').replace('_', ' ');
398
- return `<div class="summary-card" style="border-left: 4px solid ${color}">
399
- <span class="count" style="color: ${color}">${count}</span>
400
- <span class="label">${label}</span>
401
- </div>`;
 
 
 
 
 
402
  }).join('');
403
 
404
- // Highlight text based on start/end
405
  let lastIdx = 0;
406
  let html = '';
407
-
408
- // Sort by start index
409
  entities.sort((a, b) => (a.start || 0) - (b.start || 0));
410
 
411
  for (const ent of entities) {
@@ -415,16 +518,14 @@
415
  }
416
 
417
  let type = ent.entity || ent.entity_group;
418
- if (type.startsWith('B-') || type.startsWith('I-') || type.startsWith('E-') || type.startsWith('S-')) {
419
- type = type.substring(2);
420
- }
421
 
422
  const color = entityColors[type] || '#888';
423
  const label = type.replace('private_', '');
424
 
425
- html += `<span class="entity" style="background-color: ${color}40; border: 1px solid ${color}80; color: #fff;">
426
  ${escapeHtml(text.substring(ent.start, ent.end))}
427
- <span class="entity-label" style="background-color: ${color}">${label}</span>
428
  </span>`;
429
 
430
  lastIdx = ent.end;
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>OpenAI Privacy Filter</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Outfit:wght@500;700;800&display=swap" rel="stylesheet">
8
  <style>
9
+ /* Modern CSS Reset & Variables */
10
  :root {
11
+ /* Palette */
12
+ --bg-color: #030712;
13
+ --surface: rgba(17, 24, 39, 0.65);
14
+ --surface-hover: rgba(31, 41, 55, 0.75);
15
+ --border: rgba(255, 255, 255, 0.08);
16
+ --text-primary: #f9fafb;
17
+ --text-secondary: #9ca3af;
18
+ --accent: #6366f1;
19
+ --accent-glow: rgba(99, 102, 241, 0.5);
20
 
21
  /* Entity Colors */
22
+ --entity-account_number: #ef4444; /* Red */
23
+ --entity-private_address: #f59e0b; /* Amber */
24
+ --entity-private_email: #10b981; /* Emerald */
25
+ --entity-private_person: #8b5cf6; /* Violet */
26
+ --entity-private_phone: #ec4899; /* Pink */
27
+ --entity-private_url: #06b6d4; /* Cyan */
28
+ --entity-private_date: #eab308; /* Yellow */
29
+ --entity-secret: #f43f5e; /* Rose */
30
  }
31
+
32
  body {
33
  margin: 0;
34
  padding: 0;
35
+ background-color: var(--bg-color);
36
+ color: var(--text-primary);
37
  font-family: 'Inter', sans-serif;
 
 
38
  min-height: 100vh;
39
  display: flex;
40
  flex-direction: column;
41
+ position: relative;
42
  overflow-x: hidden;
43
  }
44
 
45
+ /* Ambient Background Orbs */
46
+ .ambient-orb {
47
+ position: absolute;
48
+ border-radius: 50%;
49
+ filter: blur(120px);
50
+ z-index: -1;
51
+ animation: float 20s infinite ease-in-out alternate;
52
+ }
53
+ .orb-1 { width: 400px; height: 400px; background: rgba(99, 102, 241, 0.15); top: -100px; left: -100px; }
54
+ .orb-2 { width: 500px; height: 500px; background: rgba(139, 92, 246, 0.12); bottom: -150px; right: -100px; animation-delay: -5s; }
55
+ .orb-3 { width: 300px; height: 300px; background: rgba(16, 185, 129, 0.1); top: 40%; left: 50%; transform: translate(-50%, -50%); animation-delay: -10s; }
56
+
57
+ @keyframes float {
58
+ 0% { transform: translate(0, 0) scale(1); }
59
+ 100% { transform: translate(30px, 50px) scale(1.1); }
60
+ }
61
+
62
  .container {
 
63
  max-width: 1000px;
64
+ margin: 0 auto;
65
+ padding: 4rem 2rem;
66
+ width: 100%;
67
  box-sizing: border-box;
68
+ z-index: 1;
69
  display: flex;
70
  flex-direction: column;
71
+ gap: 2.5rem;
72
  }
73
 
74
  header {
75
  text-align: center;
76
+ display: flex;
77
+ flex-direction: column;
78
+ align-items: center;
79
+ gap: 1.25rem;
80
+ animation: fadeDown 0.8s ease-out;
81
  }
82
 
83
+ .badge-container {
84
+ display: flex;
85
+ gap: 0.75rem;
86
+ flex-wrap: wrap;
87
+ justify-content: center;
88
+ }
89
+
90
+ .badge {
91
+ background: rgba(255, 255, 255, 0.05);
92
+ border: 1px solid var(--border);
93
+ padding: 0.35rem 0.85rem;
94
+ border-radius: 100px;
95
+ font-size: 0.8rem;
96
+ font-weight: 500;
97
+ color: var(--text-secondary);
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 0.5rem;
101
+ backdrop-filter: blur(10px);
102
+ }
103
+
104
+ .badge svg { width: 14px; height: 14px; color: var(--accent); }
105
+
106
  h1 {
107
+ font-family: 'Outfit', sans-serif;
108
+ font-size: 3.5rem;
109
+ font-weight: 800;
110
+ margin: 0;
111
+ line-height: 1.1;
112
+ letter-spacing: -0.02em;
113
+ background: linear-gradient(135deg, #fff 0%, #a5b4fc 100%);
114
  -webkit-background-clip: text;
115
  -webkit-text-fill-color: transparent;
116
  }
117
 
118
+ .subtitle {
119
+ color: var(--text-secondary);
120
+ font-size: 1.15rem;
121
+ max-width: 680px;
 
122
  line-height: 1.6;
123
+ margin: 0;
124
  }
125
 
126
  .glass-panel {
127
+ background: var(--surface);
128
+ backdrop-filter: blur(24px);
129
+ -webkit-backdrop-filter: blur(24px);
130
+ border: 1px solid var(--border);
131
+ border-radius: 20px;
132
+ padding: 2.5rem;
133
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
134
+ transition: transform 0.3s ease, border-color 0.3s ease;
135
+ }
136
+
137
+ .input-wrapper {
138
+ position: relative;
139
+ margin-bottom: 1.5rem;
140
  }
141
 
142
  textarea {
143
  width: 100%;
144
+ min-height: 180px;
145
+ background: rgba(0, 0, 0, 0.2);
146
+ border: 1px solid var(--border);
147
  border-radius: 12px;
148
+ color: var(--text-primary);
149
  font-family: 'Inter', sans-serif;
150
+ font-size: 1.05rem;
151
+ line-height: 1.6;
152
+ padding: 1.25rem;
153
  resize: vertical;
154
+ box-sizing: border-box;
155
+ transition: all 0.3s ease;
156
  }
157
 
158
  textarea:focus {
159
  outline: none;
160
  border-color: var(--accent);
161
+ background: rgba(0, 0, 0, 0.3);
162
+ box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
163
  }
164
 
165
+ textarea::placeholder { color: #4b5563; }
166
+
167
+ .btn-primary {
168
+ background: linear-gradient(135deg, var(--accent) 0%, #4f46e5 100%);
169
  color: white;
170
  border: none;
171
+ border-radius: 10px;
172
+ padding: 0.875rem 2rem;
173
  font-size: 1rem;
174
  font-weight: 600;
175
  cursor: pointer;
 
176
  display: flex;
177
  align-items: center;
178
  justify-content: center;
179
+ gap: 0.5rem;
180
+ transition: all 0.2s ease;
181
+ box-shadow: 0 4px 14px 0 var(--accent-glow);
182
  }
183
 
184
+ .btn-primary:hover {
 
185
  transform: translateY(-2px);
186
+ box-shadow: 0 6px 20px 0 var(--accent-glow);
187
+ filter: brightness(1.1);
 
 
 
188
  }
189
 
190
+ .btn-primary:active { transform: translateY(0); }
191
+ .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; }
 
 
 
 
192
 
193
+ /* Spinner */
194
+ .spinner {
195
+ width: 18px;
196
+ height: 18px;
197
+ border: 2px solid rgba(255,255,255,0.3);
198
  border-radius: 50%;
199
  border-top-color: white;
200
+ animation: spin 0.8s linear infinite;
201
  display: none;
202
  }
203
 
204
+ @keyframes spin { 100% { transform: rotate(360deg); } }
205
+ @keyframes fadeDown { 0% { opacity: 0; transform: translateY(-20px); } 100% { opacity: 1; transform: translateY(0); } }
206
+ @keyframes fadeUp { 0% { opacity: 0; transform: translateY(20px); } 100% { opacity: 1; transform: translateY(0); } }
 
 
207
 
208
+ /* Results Area */
209
+ .results-container {
210
+ display: none;
211
+ animation: fadeUp 0.6s ease-out forwards;
212
+ gap: 1.5rem;
213
+ flex-direction: column;
 
 
 
214
  }
215
 
216
+ .results-header {
217
+ display: flex;
218
+ justify-content: space-between;
219
  align-items: center;
220
+ border-bottom: 1px solid var(--border);
221
+ padding-bottom: 1rem;
 
 
 
 
 
222
  }
223
 
224
+ .results-header h2 {
225
+ font-family: 'Outfit', sans-serif;
226
+ font-size: 1.5rem;
227
+ margin: 0;
228
+ font-weight: 600;
229
  }
230
 
231
+ .summary-grid {
232
+ display: grid;
233
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
234
+ gap: 1rem;
 
 
 
 
 
235
  }
236
 
237
+ .stat-card {
238
+ background: rgba(255, 255, 255, 0.02);
239
+ border: 1px solid var(--border);
240
+ border-radius: 12px;
241
+ padding: 1rem;
242
+ display: flex;
243
+ align-items: center;
244
+ gap: 1rem;
245
+ transition: transform 0.2s;
246
  }
247
+
248
+ .stat-card:hover { background: rgba(255, 255, 255, 0.04); transform: translateY(-2px); }
249
 
250
+ .stat-icon {
251
+ width: 40px;
252
+ height: 40px;
253
  border-radius: 8px;
 
254
  display: flex;
255
+ align-items: center;
256
+ justify-content: center;
257
+ font-size: 1.2rem;
258
  }
259
 
260
+ .stat-info { display: flex; flex-direction: column; }
261
+ .stat-value { font-size: 1.25rem; font-weight: 700; font-family: 'Outfit', sans-serif; }
262
+ .stat-label { font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-top: 0.2rem; }
 
263
 
264
+ .output-text {
265
+ background: rgba(0, 0, 0, 0.3);
266
+ border: 1px solid var(--border);
267
+ border-radius: 12px;
268
+ padding: 1.5rem;
269
+ font-size: 1.05rem;
270
+ line-height: 1.9;
271
+ white-space: pre-wrap;
272
+ min-height: 100px;
273
  }
274
 
275
+ .entity {
276
+ display: inline-flex;
277
+ align-items: center;
278
+ border-radius: 6px;
279
+ padding: 0.1rem 0.3rem 0.1rem 0.4rem;
280
+ margin: 0 0.2rem;
281
+ font-weight: 500;
282
+ position: relative;
283
+ cursor: default;
284
+ transition: all 0.2s;
285
+ color: var(--text-primary);
286
  }
287
 
288
+ .entity:hover { filter: brightness(1.2); }
 
 
 
289
 
290
+ .entity-tag {
291
+ font-size: 0.65rem;
292
+ text-transform: uppercase;
293
+ letter-spacing: 0.05em;
294
+ margin-left: 0.4rem;
295
+ background: rgba(0,0,0,0.5);
296
+ padding: 0.15rem 0.35rem;
297
+ border-radius: 4px;
298
+ font-weight: 600;
299
  }
300
+
301
+ /* Footer Legend */
302
  .legend {
303
  display: flex;
304
  flex-wrap: wrap;
305
+ gap: 1rem;
 
306
  justify-content: center;
307
+ margin-top: 1rem;
308
+ padding: 1.5rem;
309
+ background: var(--surface);
310
+ border: 1px solid var(--border);
311
+ border-radius: 16px;
312
  }
313
+
314
  .legend-item {
315
  display: flex;
316
  align-items: center;
317
+ gap: 0.5rem;
318
  font-size: 0.85rem;
319
+ color: var(--text-secondary);
 
 
 
 
 
 
320
  }
321
 
322
+ .legend-color { width: 10px; height: 10px; border-radius: 50%; }
323
+
324
  </style>
325
  </head>
326
  <body>
327
+ <div class="ambient-orb orb-1"></div>
328
+ <div class="ambient-orb orb-2"></div>
329
+ <div class="ambient-orb orb-3"></div>
330
+
331
  <div class="container">
332
  <header>
333
+ <div class="badge-container">
334
+ <div class="badge">
335
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 7h-3a2 2 0 0 1-2-2V2"/><path d="M9 18a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h7l4 4v10a2 2 0 0 1-2 2Z"/><path d="M3 15h6"/><path d="M3 18h6"/></svg>
336
+ 1.5B Parameters
337
+ </div>
338
+ <div class="badge">
339
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path d="M12 6v6l4 2"/></svg>
340
+ 128k Token Context
341
+ </div>
342
+ <div class="badge">
343
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
344
+ Apache 2.0 License
345
+ </div>
346
+ </div>
347
+
348
  <h1>OpenAI Privacy Filter</h1>
349
+ <p class="subtitle">A state-of-the-art bidirectional token-classification model for highly accurate PII detection and text sanitization. Built for high-throughput privacy workflows.</p>
350
  </header>
351
 
352
  <div class="glass-panel">
353
+ <div class="input-wrapper">
354
+ <textarea id="inputText" placeholder="Enter text to detect and mask sensitive information...&#10;For example: My name is Alice Smith, I live at 123 Main St, and my email is alice@example.com. Call me at 555-0198."></textarea>
355
+ </div>
356
+ <div style="display: flex; justify-content: flex-end;">
357
+ <button id="analyzeBtn" class="btn-primary">
358
+ <svg class="btn-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
359
  <span class="btn-text">Analyze Text</span>
360
+ <div class="spinner" id="spinner"></div>
361
  </button>
362
  </div>
363
  </div>
364
 
365
+ <div class="glass-panel results-container" id="resultsPanel">
366
+ <div class="results-header">
367
+ <h2>Analysis Results</h2>
368
+ </div>
369
+
370
+ <div class="summary-grid" id="summaryGrid">
371
+ <!-- Summary cards injected here -->
372
+ </div>
373
+
374
+ <div class="output-text" id="outputText"></div>
375
  </div>
376
 
377
  <div class="legend" id="legend"></div>
 
392
  'secret': 'var(--entity-secret)'
393
  };
394
 
395
+ const icons = {
396
+ 'account_number': 'πŸ’³',
397
+ 'private_address': 'πŸ“',
398
+ 'private_email': 'βœ‰οΈ',
399
+ 'private_person': 'πŸ‘€',
400
+ 'private_phone': 'πŸ“±',
401
+ 'private_url': 'πŸ”—',
402
+ 'private_date': 'πŸ“…',
403
+ 'secret': 'πŸ”‘'
404
+ };
405
+
406
+ // Initialize Legend
407
  const legendContainer = document.getElementById('legend');
408
  for (const [entity, color] of Object.entries(entityColors)) {
409
  const item = document.createElement('div');
410
  item.className = 'legend-item';
411
+ const label = entity.replace('private_', '').replace('_', ' ');
412
+ item.innerHTML = `<div class="legend-color" style="background-color: ${color}"></div><span style="text-transform: capitalize">${label}</span>`;
413
  legendContainer.appendChild(item);
414
  }
415
 
 
417
  const analyzeBtn = document.getElementById('analyzeBtn');
418
  const spinner = document.getElementById('spinner');
419
  const btnText = document.querySelector('.btn-text');
420
+ const btnIcon = document.querySelector('.btn-icon');
421
  const resultsPanel = document.getElementById('resultsPanel');
422
  const outputText = document.getElementById('outputText');
423
+ const summaryGrid = document.getElementById('summaryGrid');
424
 
425
  let client = null;
426
 
 
428
  try {
429
  client = await Client.connect(window.location.origin);
430
  } catch (error) {
431
+ console.error("Gradio connection error:", error);
432
  }
433
  }
434
 
 
441
  if (!client) {
442
  await initClient();
443
  if (!client) {
444
+ alert("Unable to connect to the analysis server.");
445
  return;
446
  }
447
  }
448
 
449
  analyzeBtn.disabled = true;
450
+ btnText.textContent = 'Processing...';
451
+ btnIcon.style.display = 'none';
452
  spinner.style.display = 'block';
 
453
  resultsPanel.style.display = 'none';
454
 
455
  try {
 
456
  const result = await client.predict("/predict", { text });
457
  const entities = result.data[0];
 
458
  renderResults(text, entities);
459
 
460
  resultsPanel.style.display = 'flex';
461
+ resultsPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
462
  } catch (error) {
463
+ console.error("Prediction failed:", error);
464
+ alert("An error occurred during text analysis.");
465
  } finally {
466
  analyzeBtn.disabled = false;
 
467
  btnText.textContent = 'Analyze Text';
468
+ btnIcon.style.display = 'block';
469
+ spinner.style.display = 'none';
470
  }
471
  });
472
 
473
  function renderResults(text, entities) {
474
  if (!entities || entities.length === 0) {
475
  outputText.textContent = text;
476
+ summaryGrid.innerHTML = `
477
+ <div class="stat-card">
478
+ <div class="stat-icon" style="background: rgba(255,255,255,0.05); color: #fff;">βœ…</div>
479
+ <div class="stat-info">
480
+ <span class="stat-value">0</span>
481
+ <span class="stat-label">Entities Found</span>
482
+ </div>
483
+ </div>`;
484
  return;
485
  }
486
 
487
+ // Group counts
488
  const counts = {};
489
  entities.forEach(ent => {
490
  let type = ent.entity || ent.entity_group;
491
+ if (/^[BIES]-/.test(type)) type = type.substring(2);
 
 
492
  counts[type] = (counts[type] || 0) + 1;
493
  });
494
 
495
+ summaryGrid.innerHTML = Object.entries(counts).map(([type, count]) => {
496
  const color = entityColors[type] || '#888';
497
  const label = type.replace('private_', '').replace('_', ' ');
498
+ const icon = icons[type] || 'πŸ“Œ';
499
+ return `
500
+ <div class="stat-card" style="border-left: 3px solid ${color}">
501
+ <div class="stat-icon" style="background: ${color}20">${icon}</div>
502
+ <div class="stat-info">
503
+ <span class="stat-value" style="color: ${color}">${count}</span>
504
+ <span class="stat-label">${label}</span>
505
+ </div>
506
+ </div>`;
507
  }).join('');
508
 
509
+ // Highlight text
510
  let lastIdx = 0;
511
  let html = '';
 
 
512
  entities.sort((a, b) => (a.start || 0) - (b.start || 0));
513
 
514
  for (const ent of entities) {
 
518
  }
519
 
520
  let type = ent.entity || ent.entity_group;
521
+ if (/^[BIES]-/.test(type)) type = type.substring(2);
 
 
522
 
523
  const color = entityColors[type] || '#888';
524
  const label = type.replace('private_', '');
525
 
526
+ html += `<span class="entity" style="background-color: ${color}20; border-bottom: 2px solid ${color};">
527
  ${escapeHtml(text.substring(ent.start, ent.end))}
528
+ <span class="entity-tag" style="color: ${color};">${label}</span>
529
  </span>`;
530
 
531
  lastIdx = ent.end;