datamatters24 commited on
Commit
d09bb98
·
verified ·
1 Parent(s): 36d1c5d

Upload web/src/views/document.php with huggingface_hub

Browse files
Files changed (1) hide show
  1. web/src/views/document.php +635 -0
web/src/views/document.php ADDED
@@ -0,0 +1,635 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Document Viewer - Split Pane
4
+ *
5
+ * Variables:
6
+ * $document - Document record with: id, file_path, pages, file_size, source_section, processed_at
7
+ * $pages - Array of page metadata: page_number, has_ocr, has_entities
8
+ * $entities - Array of entities: entity_text, entity_type, page_number, confidence
9
+ */
10
+
11
+ $docId = $document['id'] ?? 0;
12
+ $filename = basename($document['file_path'] ?? 'Unknown Document');
13
+ $totalPages = $document['pages'] ?? 0;
14
+ $sourceSection = $document['source_section'] ?? '';
15
+
16
+ $collectionNames = [
17
+ 'doj_disclosures' => 'DOJ Disclosures (Epstein Files)',
18
+ 'lincoln_archives' => 'Lincoln Archives',
19
+ 'house_resolutions' => 'House Resolutions',
20
+ 'area51_cia' => 'Area 51 / CIA Declassified',
21
+ 'jfk_assassination' => 'JFK Assassination Records',
22
+ 'court_records' => 'Court Records',
23
+ 'foia' => 'FOIA Releases',
24
+ 'house_oversight' => 'House Oversight',
25
+ ];
26
+
27
+ $collectionName = $collectionNames[$sourceSection] ?? ucwords(str_replace('_', ' ', $sourceSection));
28
+
29
+ // Group entities by type
30
+ $entityGroups = [];
31
+ if (!empty($entities)) {
32
+ foreach ($entities as $ent) {
33
+ $type = $ent['entity_type'] ?? 'OTHER';
34
+ if (!isset($entityGroups[$type])) {
35
+ $entityGroups[$type] = [];
36
+ }
37
+ $entityGroups[$type][] = $ent;
38
+ }
39
+ ksort($entityGroups);
40
+ }
41
+
42
+ $entityTypeColors = [
43
+ 'PERSON' => 'bg-blue-100 text-blue-800',
44
+ 'ORGANIZATION' => 'bg-purple-100 text-purple-800',
45
+ 'ORG' => 'bg-purple-100 text-purple-800',
46
+ 'LOCATION' => 'bg-green-100 text-green-800',
47
+ 'LOC' => 'bg-green-100 text-green-800',
48
+ 'GPE' => 'bg-green-100 text-green-800',
49
+ 'DATE' => 'bg-amber-100 text-amber-800',
50
+ 'MONEY' => 'bg-emerald-100 text-emerald-800',
51
+ 'LAW' => 'bg-red-100 text-red-800',
52
+ 'EVENT' => 'bg-pink-100 text-pink-800',
53
+ 'NORP' => 'bg-indigo-100 text-indigo-800',
54
+ 'FAC' => 'bg-teal-100 text-teal-800',
55
+ 'PRODUCT' => 'bg-orange-100 text-orange-800',
56
+ 'WORK_OF_ART' => 'bg-rose-100 text-rose-800',
57
+ ];
58
+
59
+ function getEntityColor(string $type, array $colors): string {
60
+ return $colors[$type] ?? 'bg-gray-100 text-gray-800';
61
+ }
62
+
63
+ $title = htmlspecialchars($filename) . ' - Research Document Archive';
64
+ $content = '';
65
+ ob_start();
66
+ ?>
67
+
68
+ <!-- Breadcrumb -->
69
+ <nav class="flex items-center text-sm text-gray-500 mb-4" aria-label="Breadcrumb">
70
+ <a href="/" class="hover:text-gray-700 transition-colors">Home</a>
71
+ <svg class="h-4 w-4 mx-2 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
72
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
73
+ </svg>
74
+ <a href="/browse/<?= htmlspecialchars(urlencode($sourceSection)) ?>" class="hover:text-gray-700 transition-colors">
75
+ <?= htmlspecialchars($collectionName) ?>
76
+ </a>
77
+ <svg class="h-4 w-4 mx-2 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
78
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
79
+ </svg>
80
+ <span class="text-gray-900 font-medium truncate max-w-xs"><?= htmlspecialchars($filename) ?></span>
81
+ </nav>
82
+
83
+ <!-- Document Header -->
84
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-6">
85
+ <div>
86
+ <h1 class="text-2xl font-bold text-gray-900 break-all"><?= htmlspecialchars($filename) ?></h1>
87
+ <div class="mt-1 flex items-center space-x-3 text-sm text-gray-500">
88
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-700">
89
+ <?= htmlspecialchars($collectionName) ?>
90
+ </span>
91
+ <span><?= $totalPages ?> page<?= $totalPages !== 1 ? 's' : '' ?></span>
92
+ </div>
93
+ </div>
94
+ <a href="/pdf/<?= (int)$docId ?>" target="_blank"
95
+ class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
96
+ <svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
97
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
98
+ </svg>
99
+ Download PDF
100
+ </a>
101
+ </div>
102
+
103
+ <!-- Page Navigation Bar -->
104
+ <div class="bg-white border border-gray-200 rounded-lg shadow-sm p-3 mb-4">
105
+ <div class="flex items-center justify-between">
106
+ <button id="prev-page-btn" disabled
107
+ class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 border border-gray-300 rounded-md hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
108
+ <svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
109
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
110
+ </svg>
111
+ Previous
112
+ </button>
113
+
114
+ <div class="flex items-center space-x-2">
115
+ <span class="text-sm text-gray-500">Page</span>
116
+ <select id="page-selector" class="rounded-md border-gray-300 text-sm py-1.5 pl-3 pr-8 focus:border-blue-500 focus:ring-blue-500">
117
+ <?php for ($i = 1; $i <= $totalPages; $i++): ?>
118
+ <option value="<?= $i ?>"><?= $i ?></option>
119
+ <?php endfor; ?>
120
+ </select>
121
+ <span class="text-sm text-gray-500">of <?= $totalPages ?></span>
122
+ </div>
123
+
124
+ <div class="flex items-center space-x-2">
125
+ <a id="find-similar-btn" href="/similar/0" target="_blank"
126
+ class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-purple-700 bg-purple-50 border border-purple-200 rounded-md hover:bg-purple-100 transition-colors">
127
+ <svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
128
+ <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
129
+ </svg>
130
+ Find Similar
131
+ </a>
132
+ <button id="next-page-btn"
133
+ class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 border border-gray-300 rounded-md hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
134
+ Next
135
+ <svg class="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
136
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
137
+ </svg>
138
+ </button>
139
+ </div>
140
+ </div>
141
+ </div>
142
+
143
+ <!-- Split Pane Layout -->
144
+ <div class="flex flex-col lg:flex-row gap-4" style="min-height: 70vh;">
145
+
146
+ <!-- Left: PDF Viewer -->
147
+ <div class="lg:w-3/5 bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden flex flex-col">
148
+ <iframe id="pdf-viewer"
149
+ src="/pdf/<?= (int)$docId ?>"
150
+ class="flex-1 w-full"
151
+ style="min-height: 600px; border: none;"
152
+ title="PDF Document Viewer">
153
+ </iframe>
154
+ </div>
155
+
156
+ <!-- Right: OCR Text / Entities Tabs -->
157
+ <div class="lg:w-2/5 bg-white border border-gray-200 rounded-lg shadow-sm flex flex-col overflow-hidden">
158
+
159
+ <!-- Tab Headers -->
160
+ <div class="flex border-b border-gray-200 bg-gray-50">
161
+ <button id="tab-ocr"
162
+ class="flex-1 px-4 py-3 text-sm font-medium text-blue-600 border-b-2 border-blue-600 bg-white transition-colors"
163
+ data-tab="ocr">
164
+ <svg class="inline h-4 w-4 mr-1.5 -mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
165
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12" />
166
+ </svg>
167
+ OCR Text
168
+ </button>
169
+ <button id="tab-entities"
170
+ class="flex-1 px-4 py-3 text-sm font-medium text-gray-500 border-b-2 border-transparent hover:text-gray-700 transition-colors"
171
+ data-tab="entities">
172
+ <svg class="inline h-4 w-4 mr-1.5 -mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
173
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" />
174
+ </svg>
175
+ Entities
176
+ <?php if (!empty($entities)): ?>
177
+ <span class="ml-1 inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-gray-200 text-gray-700">
178
+ <?= count($entities) ?>
179
+ </span>
180
+ <?php endif; ?>
181
+ </button>
182
+ <button id="tab-topics"
183
+ class="flex-1 px-4 py-3 text-sm font-medium text-gray-500 border-b-2 border-transparent hover:text-gray-700 transition-colors"
184
+ data-tab="topics">
185
+ <svg class="inline h-4 w-4 mr-1.5 -mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
186
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" />
187
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
188
+ </svg>
189
+ Topics
190
+ <?php if (!empty($topics)): ?>
191
+ <span class="ml-1 inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-700">
192
+ <?= count(array_filter($topics, fn($s) => $s > 0.3)) ?>
193
+ </span>
194
+ <?php endif; ?>
195
+ </button>
196
+ <button id="tab-crisis"
197
+ class="flex-1 px-4 py-3 text-sm font-medium text-gray-500 border-b-2 border-transparent hover:text-gray-700 transition-colors"
198
+ data-tab="crisis">
199
+ <svg class="inline h-4 w-4 mr-1.5 -mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
200
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
201
+ </svg>
202
+ Crisis
203
+ <?php if (!empty($crisisEvents)): ?>
204
+ <span class="ml-1 inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700">
205
+ <?= count($crisisEvents) ?>
206
+ </span>
207
+ <?php endif; ?>
208
+ </button>
209
+ </div>
210
+
211
+ <!-- OCR Text Panel -->
212
+ <div id="panel-ocr" class="flex-1 overflow-auto p-4">
213
+ <div id="ocr-loading" class="flex items-center justify-center py-12">
214
+ <svg class="animate-spin h-6 w-6 text-blue-500 mr-3" fill="none" viewBox="0 0 24 24">
215
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
216
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
217
+ </svg>
218
+ <span class="text-sm text-gray-500">Loading OCR text...</span>
219
+ </div>
220
+ <div id="ocr-content" class="hidden">
221
+ <pre id="ocr-text" class="text-sm text-gray-800 whitespace-pre-wrap font-sans leading-relaxed"></pre>
222
+ </div>
223
+ <div id="ocr-empty" class="hidden text-center py-12">
224
+ <svg class="mx-auto h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
225
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12" />
226
+ </svg>
227
+ <p class="mt-3 text-sm text-gray-500">No OCR text available for this page.</p>
228
+ </div>
229
+ <div id="ocr-error" class="hidden text-center py-12">
230
+ <svg class="mx-auto h-10 w-10 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
231
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
232
+ </svg>
233
+ <p class="mt-3 text-sm text-red-600">Failed to load OCR text.</p>
234
+ </div>
235
+ </div>
236
+
237
+ <!-- Entities Panel -->
238
+ <div id="panel-entities" class="flex-1 overflow-auto p-4 hidden">
239
+ <?php if (!empty($entityGroups)): ?>
240
+ <div class="space-y-5">
241
+ <?php foreach ($entityGroups as $type => $ents): ?>
242
+ <div>
243
+ <h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
244
+ <?= htmlspecialchars($type) ?>
245
+ <span class="text-gray-400 font-normal">(<?= count($ents) ?>)</span>
246
+ </h4>
247
+ <div class="flex flex-wrap gap-2">
248
+ <?php
249
+ // Deduplicate entities by text
250
+ $seen = [];
251
+ foreach ($ents as $ent):
252
+ $text = $ent['entity_text'] ?? '';
253
+ $key = strtolower(trim($text));
254
+ if (isset($seen[$key])) continue;
255
+ $seen[$key] = true;
256
+ $colorClass = getEntityColor($type, $entityTypeColors);
257
+ ?>
258
+ <span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium <?= $colorClass ?>">
259
+ <?= htmlspecialchars($text) ?>
260
+ <?php if (!empty($ent['page_number'])): ?>
261
+ <span class="ml-1.5 opacity-60">p.<?= (int)$ent['page_number'] ?></span>
262
+ <?php endif; ?>
263
+ </span>
264
+ <?php endforeach; ?>
265
+ </div>
266
+ </div>
267
+ <?php endforeach; ?>
268
+ </div>
269
+ <?php else: ?>
270
+ <div class="text-center py-12">
271
+ <svg class="mx-auto h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
272
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" />
273
+ </svg>
274
+ <p class="mt-3 text-sm text-gray-500">No entities extracted for this document.</p>
275
+ </div>
276
+ <?php endif; ?>
277
+ </div>
278
+
279
+ <!-- Topics & Keywords Panel -->
280
+ <div id="panel-topics" class="flex-1 overflow-auto p-4 hidden">
281
+ <?php if (!empty($topics)): ?>
282
+ <div class="space-y-6">
283
+ <!-- Topic Classifications -->
284
+ <div>
285
+ <h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Topic Classifications</h4>
286
+ <div class="space-y-2">
287
+ <?php
288
+ $topicColors = [
289
+ 'intelligence operations' => 'bg-red-500',
290
+ 'national security' => 'bg-orange-500',
291
+ 'military operations' => 'bg-amber-600',
292
+ 'surveillance' => 'bg-yellow-500',
293
+ 'assassination' => 'bg-red-700',
294
+ 'congressional legislation' => 'bg-blue-500',
295
+ 'government oversight' => 'bg-indigo-500',
296
+ 'civil rights' => 'bg-purple-500',
297
+ 'foreign policy' => 'bg-teal-500',
298
+ 'law enforcement' => 'bg-cyan-600',
299
+ 'financial regulation' => 'bg-emerald-500',
300
+ 'public health' => 'bg-green-500',
301
+ 'human experimentation' => 'bg-rose-600',
302
+ 'scientific research' => 'bg-violet-500',
303
+ 'judicial proceedings' => 'bg-slate-500',
304
+ ];
305
+ foreach ($topics as $topic => $score):
306
+ if ($score < 0.1) continue;
307
+ $pct = min(100, $score * 100);
308
+ $barColor = $topicColors[$topic] ?? 'bg-gray-500';
309
+ ?>
310
+ <div>
311
+ <div class="flex justify-between items-center mb-1">
312
+ <span class="text-sm text-gray-700 capitalize"><?= htmlspecialchars($topic) ?></span>
313
+ <span class="text-xs font-mono text-gray-500"><?= number_format($pct, 1) ?>%</span>
314
+ </div>
315
+ <div class="w-full bg-gray-200 rounded-full h-2">
316
+ <div class="<?= $barColor ?> h-2 rounded-full transition-all" style="width: <?= $pct ?>%"></div>
317
+ </div>
318
+ </div>
319
+ <?php endforeach; ?>
320
+ </div>
321
+ </div>
322
+
323
+ <?php if (!empty($keywords)): ?>
324
+ <!-- Keywords -->
325
+ <div>
326
+ <h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Extracted Keywords</h4>
327
+ <div class="flex flex-wrap gap-2">
328
+ <?php foreach ($keywords as $kw):
329
+ $opacity = max(0.4, min(1.0, (float)$kw['score']));
330
+ ?>
331
+ <span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-blue-50 text-blue-700 border border-blue-200"
332
+ style="opacity: <?= $opacity ?>">
333
+ <?= htmlspecialchars($kw['keyword']) ?>
334
+ </span>
335
+ <?php endforeach; ?>
336
+ </div>
337
+ </div>
338
+ <?php endif; ?>
339
+
340
+ <?php
341
+ $stamps = $forensic['stamps'] ?? [];
342
+ $pdfMeta = $forensic['pdf_metadata'] ?? [];
343
+ if (!empty($stamps) || !empty($pdfMeta) || !empty($redactionSummary)):
344
+ ?>
345
+ <!-- Forensic Analysis -->
346
+ <div>
347
+ <h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Forensic Analysis</h4>
348
+
349
+ <?php if (!empty($stamps)): ?>
350
+ <div class="mb-3">
351
+ <p class="text-[10px] font-medium text-gray-400 uppercase mb-1">Classification Stamps</p>
352
+ <div class="flex flex-wrap gap-1.5">
353
+ <?php
354
+ $stampColors = [
355
+ 'TOP SECRET' => 'bg-red-100 text-red-800 border-red-200',
356
+ 'SECRET' => 'bg-orange-100 text-orange-800 border-orange-200',
357
+ 'CONFIDENTIAL' => 'bg-yellow-100 text-yellow-800 border-yellow-200',
358
+ 'CLASSIFIED' => 'bg-amber-100 text-amber-800 border-amber-200',
359
+ 'UNCLASSIFIED' => 'bg-green-100 text-green-800 border-green-200',
360
+ 'DECLASSIFIED' => 'bg-emerald-100 text-emerald-800 border-emerald-200',
361
+ 'EYES ONLY' => 'bg-red-100 text-red-800 border-red-200',
362
+ 'NOFORN' => 'bg-rose-100 text-rose-800 border-rose-200',
363
+ 'REDACTED' => 'bg-gray-800 text-white border-gray-900',
364
+ ];
365
+ foreach ($stamps as $s):
366
+ $color = $stampColors[$s['stamp']] ?? 'bg-gray-100 text-gray-700 border-gray-200';
367
+ ?>
368
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold border <?= $color ?>">
369
+ <?= htmlspecialchars($s['stamp']) ?>
370
+ <?php if ($s['count'] > 1): ?>
371
+ <span class="ml-1 opacity-60">x<?= $s['count'] ?></span>
372
+ <?php endif; ?>
373
+ </span>
374
+ <?php endforeach; ?>
375
+ </div>
376
+ </div>
377
+ <?php endif; ?>
378
+
379
+ <?php if (!empty($redactionSummary) && ($redactionSummary['total_redactions'] ?? 0) > 0): ?>
380
+ <div class="mb-3">
381
+ <p class="text-[10px] font-medium text-gray-400 uppercase mb-1">Redaction Detection</p>
382
+ <div class="bg-gray-50 rounded-lg p-3 border border-gray-200">
383
+ <div class="flex items-center justify-between">
384
+ <span class="text-sm text-gray-700">
385
+ <span class="font-semibold text-red-700"><?= $redactionSummary['total_redactions'] ?></span>
386
+ redacted region<?= $redactionSummary['total_redactions'] != 1 ? 's' : '' ?> detected
387
+ </span>
388
+ <span class="text-xs text-gray-500">
389
+ max <?= number_format($redactionSummary['max_page_area_pct'] ?? 0, 1) ?>% area
390
+ </span>
391
+ </div>
392
+ <div class="mt-1 w-full bg-gray-200 rounded-full h-1.5">
393
+ <div class="bg-red-600 h-1.5 rounded-full" style="width: <?= min(100, ($redactionSummary['max_page_area_pct'] ?? 0)) ?>%"></div>
394
+ </div>
395
+ </div>
396
+ </div>
397
+ <?php endif; ?>
398
+
399
+ <?php if (!empty($pdfMeta)): ?>
400
+ <div>
401
+ <p class="text-[10px] font-medium text-gray-400 uppercase mb-1">PDF Metadata</p>
402
+ <dl class="text-xs space-y-1">
403
+ <?php foreach ($pdfMeta as $key => $val): ?>
404
+ <div class="flex">
405
+ <dt class="w-24 text-gray-500 flex-shrink-0"><?= htmlspecialchars(ucfirst($key)) ?></dt>
406
+ <dd class="text-gray-700 truncate"><?= htmlspecialchars($val) ?></dd>
407
+ </div>
408
+ <?php endforeach; ?>
409
+ </dl>
410
+ </div>
411
+ <?php endif; ?>
412
+ </div>
413
+ <?php endif; ?>
414
+ </div>
415
+ <?php else: ?>
416
+ <div class="text-center py-12">
417
+ <svg class="mx-auto h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
418
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" />
419
+ </svg>
420
+ <p class="mt-3 text-sm text-gray-500">No topic classifications available yet.</p>
421
+ <p class="mt-1 text-xs text-gray-400">The ML pipeline may still be processing.</p>
422
+ </div>
423
+ <?php endif; ?>
424
+ </div>
425
+
426
+ <!-- Crisis Context Panel -->
427
+ <div id="panel-crisis" class="flex-1 overflow-auto p-4 hidden">
428
+ <?php if (!empty($crisisEvents)): ?>
429
+ <div class="space-y-4">
430
+ <?php
431
+ $crisisMethodColors = [
432
+ 'date' => 'bg-blue-50 text-blue-700',
433
+ 'keyword' => 'bg-green-50 text-green-700',
434
+ 'entity' => 'bg-purple-50 text-purple-700',
435
+ 'collection' => 'bg-amber-50 text-amber-700',
436
+ ];
437
+ foreach ($crisisEvents as $ce):
438
+ $methods = $ce['match_methods'] ?? '[]';
439
+ if (is_string($methods)) $methods = json_decode($methods, true) ?: [];
440
+ $details = $ce['details'] ?? '{}';
441
+ if (is_string($details)) $details = json_decode($details, true) ?: [];
442
+ $score = (float)($ce['relevance_score'] ?? 0);
443
+ $scorePercent = min(100, $score * 100);
444
+ $startDate = $ce['start_date'] ?? '';
445
+ $endDate = $ce['end_date'] ?? '';
446
+ $dateStr = $startDate ? date('M j, Y', strtotime($startDate)) : '';
447
+ if ($endDate && $endDate !== $startDate) {
448
+ $dateStr .= ' — ' . date('M j, Y', strtotime($endDate));
449
+ }
450
+ ?>
451
+ <a href="/crisis/<?= (int)$ce['id'] ?>"
452
+ class="block bg-gray-50 rounded-lg p-4 hover:bg-gray-100 transition-colors border border-gray-200">
453
+ <div class="flex items-start justify-between gap-2">
454
+ <div>
455
+ <h4 class="text-sm font-semibold text-gray-900">
456
+ <?= htmlspecialchars($ce['event_name'] ?? '') ?>
457
+ </h4>
458
+ <p class="text-xs text-gray-500 mt-0.5"><?= $dateStr ?></p>
459
+ </div>
460
+ <span class="flex-shrink-0 inline-flex items-center px-2 py-1 rounded-full text-xs font-bold
461
+ <?= $scorePercent >= 60 ? 'bg-red-100 text-red-800' :
462
+ ($scorePercent >= 30 ? 'bg-yellow-100 text-yellow-800' : 'bg-gray-100 text-gray-700') ?>">
463
+ <?= number_format($scorePercent, 0) ?>%
464
+ </span>
465
+ </div>
466
+
467
+ <!-- Relevance bar -->
468
+ <div class="mt-2">
469
+ <div class="w-full bg-gray-200 rounded-full h-1.5">
470
+ <div class="bg-blue-500 h-1.5 rounded-full" style="width: <?= $scorePercent ?>%"></div>
471
+ </div>
472
+ </div>
473
+
474
+ <!-- Match methods -->
475
+ <div class="mt-2 flex flex-wrap gap-1">
476
+ <?php foreach ($methods as $m):
477
+ $mColor = $crisisMethodColors[$m] ?? 'bg-gray-50 text-gray-700';
478
+ ?>
479
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium <?= $mColor ?>">
480
+ <?= htmlspecialchars($m) ?>
481
+ </span>
482
+ <?php endforeach; ?>
483
+ </div>
484
+
485
+ <!-- Matched keywords if present -->
486
+ <?php if (!empty($details['matched_keywords'])): ?>
487
+ <div class="mt-2">
488
+ <p class="text-[10px] font-medium text-gray-400 uppercase mb-1">Matched Keywords</p>
489
+ <div class="flex flex-wrap gap-1">
490
+ <?php foreach ($details['matched_keywords'] as $kw): ?>
491
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-[10px] bg-green-50 text-green-700">
492
+ <?= htmlspecialchars($kw) ?>
493
+ </span>
494
+ <?php endforeach; ?>
495
+ </div>
496
+ </div>
497
+ <?php endif; ?>
498
+ </a>
499
+ <?php endforeach; ?>
500
+ </div>
501
+ <?php else: ?>
502
+ <div class="text-center py-12">
503
+ <svg class="mx-auto h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
504
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
505
+ </svg>
506
+ <p class="mt-3 text-sm text-gray-500">No crisis correlations found for this document.</p>
507
+ <p class="mt-1 text-xs text-gray-400">The ML pipeline may still be processing.</p>
508
+ </div>
509
+ <?php endif; ?>
510
+ </div>
511
+ </div>
512
+ </div>
513
+
514
+ <script>
515
+ (function() {
516
+ const docId = <?= (int)$docId ?>;
517
+ const totalPages = <?= (int)$totalPages ?>;
518
+ let currentPage = 1;
519
+
520
+ // Page metadata from server (for "Find Similar" page IDs)
521
+ const pagesMeta = <?= json_encode($pages ?? []) ?>;
522
+
523
+ // DOM elements
524
+ const pageSelector = document.getElementById('page-selector');
525
+ const prevBtn = document.getElementById('prev-page-btn');
526
+ const nextBtn = document.getElementById('next-page-btn');
527
+ const findSimilarBtn = document.getElementById('find-similar-btn');
528
+ const ocrLoading = document.getElementById('ocr-loading');
529
+ const ocrContent = document.getElementById('ocr-content');
530
+ const ocrText = document.getElementById('ocr-text');
531
+ const ocrEmpty = document.getElementById('ocr-empty');
532
+ const ocrError = document.getElementById('ocr-error');
533
+
534
+ // Tab elements
535
+ const tabs = {
536
+ ocr: { tab: document.getElementById('tab-ocr'), panel: document.getElementById('panel-ocr') },
537
+ entities: { tab: document.getElementById('tab-entities'), panel: document.getElementById('panel-entities') },
538
+ topics: { tab: document.getElementById('tab-topics'), panel: document.getElementById('panel-topics') },
539
+ crisis: { tab: document.getElementById('tab-crisis'), panel: document.getElementById('panel-crisis') },
540
+ };
541
+
542
+ // Tab switching
543
+ function switchTab(active) {
544
+ Object.keys(tabs).forEach(function(key) {
545
+ var t = tabs[key];
546
+ if (key === active) {
547
+ t.tab.classList.add('text-blue-600', 'border-blue-600', 'bg-white');
548
+ t.tab.classList.remove('text-gray-500', 'border-transparent');
549
+ t.panel.classList.remove('hidden');
550
+ } else {
551
+ t.tab.classList.remove('text-blue-600', 'border-blue-600', 'bg-white');
552
+ t.tab.classList.add('text-gray-500', 'border-transparent');
553
+ t.panel.classList.add('hidden');
554
+ }
555
+ });
556
+ }
557
+
558
+ Object.keys(tabs).forEach(function(key) {
559
+ tabs[key].tab.addEventListener('click', function() { switchTab(key); });
560
+ });
561
+
562
+ // Load OCR text for a page
563
+ function loadOcrText(pageNum) {
564
+ ocrLoading.classList.remove('hidden');
565
+ ocrContent.classList.add('hidden');
566
+ ocrEmpty.classList.add('hidden');
567
+ ocrError.classList.add('hidden');
568
+
569
+ fetch('/api/page/' + docId + '/' + pageNum)
570
+ .then(function(response) {
571
+ if (!response.ok) throw new Error('HTTP ' + response.status);
572
+ return response.json();
573
+ })
574
+ .then(function(data) {
575
+ ocrLoading.classList.add('hidden');
576
+ var text = data.ocr_text || data.text || '';
577
+ if (text.trim().length > 0) {
578
+ ocrText.textContent = text;
579
+ ocrContent.classList.remove('hidden');
580
+ } else {
581
+ ocrEmpty.classList.remove('hidden');
582
+ }
583
+ })
584
+ .catch(function() {
585
+ ocrLoading.classList.add('hidden');
586
+ ocrError.classList.remove('hidden');
587
+ });
588
+ }
589
+
590
+ // Update page navigation state
591
+ function updateNavigation() {
592
+ prevBtn.disabled = (currentPage <= 1);
593
+ nextBtn.disabled = (currentPage >= totalPages);
594
+ pageSelector.value = currentPage;
595
+
596
+ // Update "Find Similar" link
597
+ var pageId = 0;
598
+ for (var i = 0; i < pagesMeta.length; i++) {
599
+ if (pagesMeta[i].page_number == currentPage) {
600
+ pageId = pagesMeta[i].id || 0;
601
+ break;
602
+ }
603
+ }
604
+ findSimilarBtn.href = '/similar/' + pageId;
605
+ }
606
+
607
+ // Navigate to a page
608
+ function goToPage(pageNum) {
609
+ if (pageNum < 1 || pageNum > totalPages) return;
610
+ currentPage = pageNum;
611
+ updateNavigation();
612
+ loadOcrText(currentPage);
613
+ }
614
+
615
+ // Event listeners
616
+ prevBtn.addEventListener('click', function() { goToPage(currentPage - 1); });
617
+ nextBtn.addEventListener('click', function() { goToPage(currentPage + 1); });
618
+ pageSelector.addEventListener('change', function() { goToPage(parseInt(this.value, 10)); });
619
+
620
+ // Keyboard navigation
621
+ document.addEventListener('keydown', function(e) {
622
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return;
623
+ if (e.key === 'ArrowLeft') { goToPage(currentPage - 1); }
624
+ if (e.key === 'ArrowRight') { goToPage(currentPage + 1); }
625
+ });
626
+
627
+ // Initial load
628
+ goToPage(1);
629
+ })();
630
+ </script>
631
+
632
+ <?php
633
+ $content = ob_get_clean();
634
+ include __DIR__ . '/layout.php';
635
+ ?>