forked from arthurpanhku/DocSentinel
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdemo.html
More file actions
555 lines (497 loc) · 22.6 KB
/
Copy pathdemo.html
File metadata and controls
555 lines (497 loc) · 22.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DocSentinel - Security Assessment</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Vue 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
colors: {
primary: '#3b82f6',
'primary-dark': '#2563eb',
secondary: '#64748b',
success: '#10b981',
warning: '#f59e0b',
danger: '#ef4444',
dark: '#0f172a',
'dark-surface': '#1e293b',
}
}
}
}
</script>
<style>
body {
background-color: #0f172a;
color: #e2e8f0;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
</head>
<body class="antialiased min-h-screen flex flex-col">
<div id="app" class="flex-grow flex flex-col">
<!-- Header -->
<header class="bg-dark-surface border-b border-gray-700 sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="bg-primary/20 p-2 rounded-lg">
<i data-lucide="shield-check" class="text-primary w-6 h-6"></i>
</div>
<div>
<h1 class="text-xl font-bold tracking-tight text-white">DocSentinel</h1>
<p class="text-xs text-gray-400">Automated Security Assessment</p>
</div>
</div>
<div class="flex items-center gap-4">
<button @click="showConfig = !showConfig"
class="p-2 text-gray-400 hover:text-white transition-colors rounded-full hover:bg-gray-700">
<i data-lucide="settings" class="w-5 h-5"></i>
</button>
<a href="https://github.com/arthurpanhku/DocSentinel" target="_blank"
class="text-gray-400 hover:text-white transition-colors">
<i data-lucide="github" class="w-5 h-5"></i>
</a>
</div>
</div>
<!-- Config Panel -->
<transition name="fade">
<div v-if="showConfig" class="border-t border-gray-700 bg-gray-900/50 backdrop-blur-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div class="flex items-center gap-4">
<div class="flex-1 max-w-md">
<label class="block text-xs font-medium text-gray-400 mb-1">API Base URL</label>
<input v-model="baseUrl" type="text"
class="w-full bg-dark-surface border border-gray-600 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all"
placeholder="http://localhost:8000">
</div>
<div class="flex-1 max-w-md">
<label class="block text-xs font-medium text-gray-400 mb-1">Scenario ID</label>
<input v-model="scenarioId" type="text"
class="w-full bg-dark-surface border border-gray-600 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all"
placeholder="default">
</div>
</div>
</div>
</div>
</transition>
</header>
<!-- Main Content -->
<main class="flex-grow max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 w-full">
<!-- Upload Section -->
<transition name="fade" mode="out-in">
<div v-if="!report && !loading" class="max-w-3xl mx-auto mt-12 text-center">
<h2 class="text-3xl font-bold text-white mb-4">Security Assessment</h2>
<p class="text-gray-400 mb-8 text-lg">Upload your security documents, questionnaires, or reports for instant
AI-powered assessment.</p>
<div
class="border-2 border-dashed border-gray-600 rounded-xl p-12 bg-dark-surface/50 hover:bg-dark-surface hover:border-primary transition-all cursor-pointer group relative overflow-hidden"
@dragover.prevent="dragOver = true" @dragleave.prevent="dragOver = false" @drop.prevent="handleDrop"
@click="$refs.fileInput.click()" :class="{'border-primary bg-primary/5': dragOver}">
<input type="file" ref="fileInput" class="hidden" @change="handleFileSelect" multiple
accept=".txt,.md,.pdf,.docx,.xlsx,.pptx">
<div class="relative z-10 flex flex-col items-center">
<div
class="w-16 h-16 bg-gray-800 rounded-full flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300">
<i data-lucide="upload-cloud" class="w-8 h-8 text-primary"></i>
</div>
<h3 class="text-xl font-semibold text-white mb-2">Drop your document here</h3>
<p class="text-gray-400 text-sm mb-6">Support for PDF, Word, Excel, PowerPoint, Markdown</p>
<button
class="px-6 py-2.5 bg-primary hover:bg-primary-dark text-white font-medium rounded-lg transition-colors shadow-lg shadow-primary/20">
Browse Files
</button>
</div>
</div>
<div v-if="error"
class="mt-6 p-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm flex items-center gap-3 text-left">
<i data-lucide="alert-circle" class="w-5 h-5 flex-shrink-0"></i>
<pre class="whitespace-pre-wrap font-sans">{{ error }}</pre>
</div>
</div>
<!-- Loading State -->
<div v-else-if="loading" class="flex flex-col items-center justify-center mt-24">
<div class="relative w-24 h-24 mb-8">
<div class="absolute inset-0 border-4 border-gray-700 rounded-full"></div>
<div class="absolute inset-0 border-4 border-primary rounded-full border-t-transparent animate-spin"></div>
<i data-lucide="file-search" class="absolute inset-0 m-auto w-8 h-8 text-white animate-pulse"></i>
</div>
<h3 class="text-2xl font-bold text-white mb-2">Analyzing Document</h3>
<p class="text-gray-400 max-w-md text-center">{{ progressText || 'Our AI agent is reading your document,
checking against security policies, and identifying risks...' }}</p>
<div class="mt-8 w-64 h-1.5 bg-gray-800 rounded-full overflow-hidden">
<div class="h-full bg-primary animate-progress origin-left"></div>
</div>
</div>
<!-- Report View -->
<div v-else class="space-y-6">
<!-- Actions Bar -->
<div class="flex items-center justify-between">
<button @click="reset" class="flex items-center gap-2 text-gray-400 hover:text-white transition-colors">
<i data-lucide="arrow-left" class="w-4 h-4"></i>
Back to Upload
</button>
<div class="flex items-center gap-3">
<span class="text-sm text-gray-500">Task ID: {{ report.task_id }}</span>
<span
class="px-2 py-1 rounded text-xs font-medium bg-green-500/10 text-green-400 border border-green-500/20">
{{ report.status }}
</span>
</div>
</div>
<!-- Summary Card -->
<div class="bg-dark-surface border border-gray-700 rounded-xl p-6 shadow-xl">
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<i data-lucide="activity" class="w-5 h-5 text-primary"></i>
Executive Summary
</h2>
<p class="text-gray-300 leading-relaxed">{{ report.summary }}</p>
<!-- Stats Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
<div class="bg-gray-800/50 rounded-lg p-4 border border-gray-700">
<div class="text-2xl font-bold text-white">{{ report.risk_items?.length || 0 }}</div>
<div class="text-xs text-gray-400 uppercase tracking-wider mt-1">Total Risks</div>
</div>
<div class="bg-red-500/10 rounded-lg p-4 border border-red-500/20">
<div class="text-2xl font-bold text-red-400">{{ countSeverity('high') + countSeverity('critical') }}
</div>
<div class="text-xs text-red-300/70 uppercase tracking-wider mt-1">High/Critical</div>
</div>
<div class="bg-yellow-500/10 rounded-lg p-4 border border-yellow-500/20">
<div class="text-2xl font-bold text-yellow-400">{{ countSeverity('medium') }}</div>
<div class="text-xs text-yellow-300/70 uppercase tracking-wider mt-1">Medium</div>
</div>
<div class="bg-blue-500/10 rounded-lg p-4 border border-blue-500/20">
<div class="text-2xl font-bold text-blue-400">{{ report.compliance_gaps?.length || 0 }}</div>
<div class="text-xs text-blue-300/70 uppercase tracking-wider mt-1">Compliance Gaps</div>
</div>
</div>
</div>
<!-- Tabs -->
<div class="flex border-b border-gray-700 space-x-6 overflow-x-auto">
<button v-for="tab in tabs" :key="tab.id" @click="currentTab = tab.id"
class="pb-3 px-1 text-sm font-medium transition-colors relative whitespace-nowrap"
:class="currentTab === tab.id ? 'text-primary' : 'text-gray-400 hover:text-white'">
{{ tab.label }}
<span v-if="currentTab === tab.id"
class="absolute bottom-0 left-0 w-full h-0.5 bg-primary rounded-t-full"></span>
</button>
</div>
<!-- Tab Content -->
<div class="min-h-[400px]">
<!-- Risks Tab -->
<div v-if="currentTab === 'risks'" class="space-y-4">
<div v-if="!report.risk_items?.length" class="text-center py-12 text-gray-500">
<i data-lucide="check-circle" class="w-12 h-12 mx-auto mb-3 opacity-50"></i>
<p>No risks identified.</p>
</div>
<div v-for="risk in report.risk_items" :key="risk.id"
class="bg-dark-surface border border-gray-700 rounded-lg p-5 hover:border-gray-600 transition-colors">
<div class="flex items-start justify-between gap-4 mb-2">
<h3 class="font-semibold text-white text-lg">{{ risk.title }}</h3>
<span :class="getSeverityClass(risk.severity)"
class="px-2.5 py-0.5 rounded text-xs font-bold uppercase tracking-wide shrink-0">
{{ risk.severity }}
</span>
</div>
<p class="text-gray-300 text-sm mb-3">{{ risk.description }}</p>
<div class="flex items-center gap-4 text-xs text-gray-500">
<span v-if="risk.category" class="flex items-center gap-1 bg-gray-800 px-2 py-1 rounded">
<i data-lucide="tag" class="w-3 h-3"></i> {{ risk.category }}
</span>
<span v-if="risk.source_ref" class="flex items-center gap-1 bg-gray-800 px-2 py-1 rounded"
title="Source Reference">
<i data-lucide="file-text" class="w-3 h-3"></i> {{ risk.source_ref }}
</span>
</div>
</div>
</div>
<!-- Compliance Gaps Tab -->
<div v-if="currentTab === 'compliance'" class="space-y-4">
<div v-if="!report.compliance_gaps?.length" class="text-center py-12 text-gray-500">
<i data-lucide="check-circle" class="w-12 h-12 mx-auto mb-3 opacity-50"></i>
<p>No compliance gaps found.</p>
</div>
<div v-for="gap in report.compliance_gaps" :key="gap.id"
class="bg-dark-surface border border-gray-700 rounded-lg overflow-hidden">
<div class="bg-gray-800/50 px-5 py-3 border-b border-gray-700 flex justify-between items-center">
<span class="font-mono text-sm text-primary">{{ gap.control_or_clause }}</span>
<span v-if="gap.framework" class="text-xs text-gray-500 uppercase">{{ gap.framework }}</span>
</div>
<div class="p-5">
<div class="mb-4">
<h4 class="text-xs text-gray-500 uppercase tracking-wider mb-1">Gap Description</h4>
<p class="text-gray-300 text-sm">{{ gap.gap_description }}</p>
</div>
<div v-if="gap.evidence_suggestion">
<h4 class="text-xs text-gray-500 uppercase tracking-wider mb-1">Suggested Evidence</h4>
<div class="bg-gray-800/50 rounded p-3 text-sm text-gray-400 italic border border-gray-700/50">
{{ gap.evidence_suggestion }}
</div>
</div>
</div>
</div>
</div>
<!-- Remediation Tab -->
<div v-if="currentTab === 'remediation'" class="space-y-4">
<div v-if="!report.remediations?.length" class="text-center py-12 text-gray-500">
<p>No remediation steps needed.</p>
</div>
<div v-for="item in report.remediations" :key="item.id"
class="flex gap-4 bg-dark-surface border border-gray-700 rounded-lg p-5">
<div class="shrink-0 mt-1">
<div :class="getPriorityColor(item.priority)"
class="w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs border">
{{ (item.priority || 'M').charAt(0).toUpperCase() }}
</div>
</div>
<div class="flex-grow">
<p class="text-white font-medium mb-2">{{ item.action }}</p>
<div class="flex flex-wrap gap-2">
<span v-for="rid in item.related_risk_ids" :key="rid"
class="text-xs px-2 py-0.5 rounded bg-red-500/10 text-red-400 border border-red-500/20">
Risk: {{ rid }}
</span>
<span v-for="gid in item.related_gap_ids" :key="gid"
class="text-xs px-2 py-0.5 rounded bg-blue-500/10 text-blue-400 border border-blue-500/20">
Gap: {{ gid }}
</span>
</div>
</div>
</div>
</div>
<!-- Raw JSON Tab -->
<div v-if="currentTab === 'json'" class="relative">
<div class="absolute top-2 right-2">
<button @click="copyJson"
class="p-2 bg-gray-800 hover:bg-gray-700 rounded text-gray-400 hover:text-white transition-colors">
<i data-lucide="copy" class="w-4 h-4"></i>
</button>
</div>
<pre
class="bg-dark-surface border border-gray-700 rounded-lg p-4 overflow-x-auto text-xs font-mono text-gray-300 leading-relaxed">{{ JSON.stringify(report, null, 2) }}</pre>
</div>
</div>
</div>
</transition>
</main>
<!-- Footer -->
<footer class="border-t border-gray-800 py-6 mt-auto">
<div class="max-w-7xl mx-auto px-4 text-center text-sm text-gray-500">
<p>© 2026 DocSentinel. All rights reserved.</p>
</div>
</footer>
</div>
<script>
const { createApp, ref, computed, nextTick } = Vue;
createApp({
setup() {
const baseUrl = ref('http://localhost:8000');
const scenarioId = ref('default');
const showConfig = ref(false);
const loading = ref(false);
const progressText = ref('');
const error = ref(null);
const report = ref(null);
const dragOver = ref(false);
const fileInput = ref(null);
const currentTab = ref('risks');
const tabs = [
{ id: 'risks', label: 'Risk Items' },
{ id: 'compliance', label: 'Compliance Gaps' },
{ id: 'remediation', label: 'Remediation' },
{ id: 'json', label: 'Raw Data' }
];
const handleFileSelect = (e) => {
const files = e.target.files;
if (files.length) processFiles(files);
};
const handleDrop = (e) => {
dragOver.value = false;
const files = e.dataTransfer.files;
if (files.length) processFiles(files);
};
const processFiles = async (files) => {
loading.value = true;
error.value = null;
report.value = null;
progressText.value = 'Initializing upload...';
const api = baseUrl.value.replace(/\/$/, '') + '/api/v1';
try {
// 1. Upload
const fd = new FormData();
for (let i = 0; i < files.length; i++) {
fd.append('files', files[i]);
}
fd.append('scenario_id', scenarioId.value);
progressText.value = `Uploading ${files.length} file(s)...`;
const uploadRes = await fetch(api + '/assessments', {
method: 'POST',
body: fd
});
if (!uploadRes.ok) throw new Error(`Upload failed: ${uploadRes.statusText}`);
const uploadData = await uploadRes.json();
if (!uploadData.task_id) throw new Error('No task ID returned from server');
// 2. Poll for results
await pollResult(api, uploadData.task_id);
} catch (e) {
error.value = `Error: ${e.message}\n\nIs the backend running at ${baseUrl.value}?`;
loading.value = false;
}
};
const pollResult = async (api, taskId) => {
const maxRetries = 60; // 2 minutes approx
let retries = 0;
const check = async () => {
try {
const res = await fetch(`${api}/assessments/${taskId}`);
if (!res.ok) throw new Error(`Status check failed: ${res.statusText}`);
const data = await res.json();
// Update progress based on status if possible
if (data.status === 'processing') {
progressText.value = 'Analyzing document content...';
} else if (data.status === 'pending') {
progressText.value = 'Queued for assessment...';
}
// Check if status is completed or report content is available
if (data.status === 'completed' || (data.report && data.report.summary) || data.summary) {
// If the API returns a wrapper with 'report' field (per spec)
if (data.report) {
report.value = {
...data.report,
task_id: data.task_id || data.report.task_id,
status: data.status || 'completed'
};
} else {
// Fallback if the API returns the report directly or flat
report.value = {
...data,
status: data.status || 'completed'
};
}
loading.value = false;
nextTick(() => lucide.createIcons());
return;
}
if (data.status === 'failed') {
throw new Error('Assessment task failed on server.');
}
if (retries++ < maxRetries) {
setTimeout(check, 2000);
} else {
throw new Error('Timeout waiting for assessment results.');
}
} catch (e) {
error.value = e.message;
loading.value = false;
}
};
check();
};
const reset = () => {
report.value = null;
error.value = null;
if (fileInput.value) fileInput.value.value = '';
};
const getSeverityClass = (sev) => {
const map = {
'critical': 'bg-red-500 text-white',
'high': 'bg-red-500/20 text-red-400 border border-red-500/30',
'medium': 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/30',
'low': 'bg-blue-500/20 text-blue-400 border border-blue-500/30'
};
return map[sev?.toLowerCase()] || 'bg-gray-700 text-gray-300';
};
const getPriorityColor = (prio) => {
const map = {
'high': 'bg-red-500/20 text-red-400 border-red-500/30',
'medium': 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
'low': 'bg-blue-500/20 text-blue-400 border-blue-500/30'
};
return map[prio?.toLowerCase()] || 'bg-gray-700 text-gray-400 border-gray-600';
};
const countSeverity = (sev) => {
return report.value?.risk_items?.filter(r => r.severity?.toLowerCase() === sev).length || 0;
};
const copyJson = () => {
navigator.clipboard.writeText(JSON.stringify(report.value, null, 2));
};
// Initialize icons on mount
nextTick(() => {
lucide.createIcons();
});
return {
baseUrl,
scenarioId,
showConfig,
progressText,
loading,
error,
report,
dragOver,
fileInput,
currentTab,
tabs,
handleFileSelect,
handleDrop,
reset,
getSeverityClass,
getPriorityColor,
countSeverity,
copyJson
};
},
updated() {
nextTick(() => lucide.createIcons());
}
}).mount('#app');
</script>
<style>
@keyframes progress {
0% {
transform: scaleX(0);
}
50% {
transform: scaleX(0.5);
}
100% {
transform: scaleX(1);
}
}
.animate-progress {
animation: progress 2s infinite ease-in-out;
}
</style>
</body>
</html>