Jatin Mehra commited on
Commit
e3257b5
·
1 Parent(s): a193f24

Add initial HTML, CSS, and JavaScript files for PDF Insight Beta application

Browse files
Files changed (3) hide show
  1. static/css/styles.css +715 -0
  2. static/index.html +126 -0
  3. static/js/app.js +504 -0
static/css/styles.css ADDED
@@ -0,0 +1,715 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Base styles */
2
+ :root {
3
+ --primary-color: #2563eb;
4
+ --primary-light: #3b82f6;
5
+ --primary-dark: #1d4ed8;
6
+ --secondary-color: #10b981;
7
+ --background-color: #f3f4f6;
8
+ --sidebar-color: #ffffff;
9
+ --text-color: #1f2937;
10
+ --text-light: #6b7280;
11
+ --border-color: #e5e7eb;
12
+ --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
13
+ --border-radius: 8px;
14
+ }
15
+
16
+ * {
17
+ margin: 0;
18
+ padding: 0;
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ body {
23
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
24
+ color: var(--text-color);
25
+ background-color: var(--background-color);
26
+ line-height: 1.5;
27
+ }
28
+
29
+ .hidden {
30
+ display: none !important;
31
+ }
32
+
33
+ /* App container layout */
34
+ .app-container {
35
+ display: grid;
36
+ grid-template-columns: 260px 1fr 300px;
37
+ height: 100vh;
38
+ overflow: hidden;
39
+ }
40
+
41
+ /* Sidebar styles */
42
+ .sidebar {
43
+ background-color: var(--sidebar-color);
44
+ border-right: 1px solid var(--border-color);
45
+ display: flex;
46
+ flex-direction: column;
47
+ overflow-y: auto;
48
+ }
49
+
50
+ .logo-container {
51
+ padding: 1.5rem;
52
+ border-bottom: 1px solid var(--border-color);
53
+ }
54
+
55
+ .logo-container h1 {
56
+ font-size: 1.5rem;
57
+ font-weight: 700;
58
+ color: var(--primary-color);
59
+ }
60
+
61
+ .beta {
62
+ background-color: rgba(37, 99, 235, 0.1);
63
+ color: var(--primary-color);
64
+ font-size: 0.7rem;
65
+ padding: 0.1rem 0.4rem;
66
+ border-radius: 4px;
67
+ vertical-align: super;
68
+ font-weight: 600;
69
+ }
70
+
71
+ .upload-container {
72
+ padding: 1.5rem;
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: 1.5rem;
76
+ }
77
+
78
+ .upload-box {
79
+ border: 2px dashed var(--border-color);
80
+ border-radius: var(--border-radius);
81
+ padding: 2rem 1rem;
82
+ display: flex;
83
+ flex-direction: column;
84
+ align-items: center;
85
+ justify-content: center;
86
+ cursor: pointer;
87
+ transition: all 0.2s ease;
88
+ }
89
+
90
+ .upload-box:hover {
91
+ border-color: var(--primary-light);
92
+ background-color: rgba(59, 130, 246, 0.05);
93
+ }
94
+
95
+ .upload-box .upload-icon {
96
+ font-size: 2rem;
97
+ color: var(--primary-color);
98
+ margin-bottom: 1rem;
99
+ }
100
+
101
+ .upload-box p {
102
+ color: var(--text-light);
103
+ font-weight: 500;
104
+ }
105
+
106
+ .model-selection {
107
+ display: flex;
108
+ flex-direction: column;
109
+ gap: 0.5rem;
110
+ }
111
+
112
+ .model-selection label {
113
+ font-weight: 500;
114
+ color: var(--text-light);
115
+ }
116
+
117
+ .model-selection select {
118
+ padding: 0.75rem;
119
+ border-radius: var(--border-radius);
120
+ border: 1px solid var(--border-color);
121
+ background-color: white;
122
+ width: 100%;
123
+ font-size: 0.9rem;
124
+ outline: none;
125
+ }
126
+
127
+ .session-info {
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 1rem;
131
+ margin-top: 1rem;
132
+ padding-top: 1rem;
133
+ border-top: 1px solid var(--border-color);
134
+ }
135
+
136
+ .file-info {
137
+ display: flex;
138
+ align-items: center;
139
+ gap: 0.5rem;
140
+ overflow: hidden;
141
+ }
142
+
143
+ .file-info i {
144
+ color: var(--primary-color);
145
+ }
146
+
147
+ .file-info span {
148
+ font-size: 0.9rem;
149
+ overflow: hidden;
150
+ text-overflow: ellipsis;
151
+ white-space: nowrap;
152
+ }
153
+
154
+ .btn-clear, .btn-new {
155
+ padding: 0.75rem;
156
+ border-radius: var(--border-radius);
157
+ border: none;
158
+ font-weight: 500;
159
+ cursor: pointer;
160
+ transition: all 0.2s;
161
+ width: 100%;
162
+ }
163
+
164
+ .btn-clear {
165
+ background-color: rgba(239, 68, 68, 0.1);
166
+ color: #ef4444;
167
+ }
168
+
169
+ .btn-clear:hover {
170
+ background-color: rgba(239, 68, 68, 0.2);
171
+ }
172
+
173
+ .btn-new {
174
+ background-color: rgba(37, 99, 235, 0.1);
175
+ color: var(--primary-color);
176
+ }
177
+
178
+ .btn-new:hover {
179
+ background-color: rgba(37, 99, 235, 0.2);
180
+ }
181
+
182
+ /* Main content area */
183
+ .main-content {
184
+ display: flex;
185
+ flex-direction: column;
186
+ height: 100%;
187
+ overflow: hidden;
188
+ }
189
+
190
+ .welcome-screen {
191
+ height: 100%;
192
+ display: flex;
193
+ flex-direction: column;
194
+ justify-content: center;
195
+ align-items: center;
196
+ padding: 2rem;
197
+ text-align: center;
198
+ }
199
+
200
+ .welcome-header {
201
+ margin-bottom: 3rem;
202
+ }
203
+
204
+ .welcome-header h1 {
205
+ font-size: 2.5rem;
206
+ margin-bottom: 1rem;
207
+ background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
208
+ -webkit-background-clip: text;
209
+ background-clip: text;
210
+ -webkit-text-fill-color: transparent;
211
+ }
212
+
213
+ .welcome-header p {
214
+ color: var(--text-light);
215
+ font-size: 1.2rem;
216
+ }
217
+
218
+ .features {
219
+ display: flex;
220
+ gap: 2rem;
221
+ margin-bottom: 3rem;
222
+ }
223
+
224
+ .feature {
225
+ background: white;
226
+ border-radius: var(--border-radius);
227
+ padding: 2rem;
228
+ box-shadow: var(--shadow);
229
+ width: 250px;
230
+ transition: transform 0.3s ease;
231
+ }
232
+
233
+ .feature:hover {
234
+ transform: translateY(-5px);
235
+ }
236
+
237
+ .feature i {
238
+ font-size: 2rem;
239
+ color: var(--primary-color);
240
+ margin-bottom: 1rem;
241
+ }
242
+
243
+ .feature h3 {
244
+ margin-bottom: 0.5rem;
245
+ }
246
+
247
+ .feature p {
248
+ color: var(--text-light);
249
+ font-size: 0.9rem;
250
+ }
251
+
252
+ .get-started {
253
+ margin-top: 2rem;
254
+ }
255
+
256
+ .btn-primary {
257
+ background-color: var(--primary-color);
258
+ color: white;
259
+ padding: 1rem 2rem;
260
+ border-radius: var(--border-radius);
261
+ border: none;
262
+ font-weight: 600;
263
+ cursor: pointer;
264
+ transition: background-color 0.2s;
265
+ font-size: 1rem;
266
+ }
267
+
268
+ .btn-primary:hover {
269
+ background-color: var(--primary-dark);
270
+ }
271
+
272
+ /* Chat container */
273
+ .chat-container {
274
+ display: flex;
275
+ flex-direction: column;
276
+ height: 100%;
277
+ overflow: hidden;
278
+ }
279
+
280
+ .chat-messages {
281
+ flex: 1;
282
+ overflow-y: auto;
283
+ padding: 1.5rem;
284
+ display: flex;
285
+ flex-direction: column;
286
+ gap: 1.5rem;
287
+ }
288
+
289
+ .system-message {
290
+ background-color: rgba(37, 99, 235, 0.05);
291
+ padding: 1rem;
292
+ border-radius: var(--border-radius);
293
+ border-left: 4px solid var(--primary-color);
294
+ }
295
+
296
+ .system-message p {
297
+ color: var(--text-color);
298
+ font-size: 0.9rem;
299
+ }
300
+
301
+ .message {
302
+ display: flex;
303
+ flex-direction: column;
304
+ max-width: 85%;
305
+ }
306
+
307
+ .user-message {
308
+ align-self: flex-end;
309
+ background-color: var(--primary-color);
310
+ color: white;
311
+ padding: 1rem;
312
+ border-radius: var(--border-radius);
313
+ border-bottom-right-radius: 0;
314
+ }
315
+
316
+ .assistant-message {
317
+ align-self: flex-start;
318
+ background-color: white;
319
+ padding: 1rem;
320
+ border-radius: var(--border-radius);
321
+ border-bottom-left-radius: 0;
322
+ box-shadow: var(--shadow);
323
+ }
324
+
325
+ .message-timestamp {
326
+ font-size: 0.75rem;
327
+ color: var(--text-light);
328
+ margin-top: 0.25rem;
329
+ }
330
+
331
+ .user-message .message-timestamp {
332
+ align-self: flex-end;
333
+ }
334
+
335
+ .assistant-message .message-timestamp {
336
+ align-self: flex-start;
337
+ }
338
+
339
+ .chat-input-container {
340
+ padding: 1rem;
341
+ border-top: 1px solid var(--border-color);
342
+ background-color: white;
343
+ }
344
+
345
+ .search-toggle {
346
+ display: flex;
347
+ align-items: center;
348
+ gap: 0.5rem;
349
+ margin-bottom: 0.75rem;
350
+ padding-left: 0.5rem;
351
+ }
352
+
353
+ .search-toggle label {
354
+ font-size: 0.9rem;
355
+ color: var(--text-light);
356
+ cursor: pointer;
357
+ }
358
+
359
+ .chat-input-box {
360
+ display: flex;
361
+ gap: 0.75rem;
362
+ align-items: flex-end;
363
+ }
364
+
365
+ #chat-input {
366
+ flex: 1;
367
+ border: 1px solid var(--border-color);
368
+ border-radius: var(--border-radius);
369
+ padding: 0.75rem;
370
+ resize: none;
371
+ min-height: 60px;
372
+ max-height: 120px;
373
+ outline: none;
374
+ font-family: inherit;
375
+ font-size: 0.95rem;
376
+ line-height: 1.5;
377
+ }
378
+
379
+ #chat-input:focus {
380
+ border-color: var(--primary-light);
381
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
382
+ }
383
+
384
+ #send-button {
385
+ padding: 0.75rem 1rem;
386
+ border-radius: var(--border-radius);
387
+ border: none;
388
+ background-color: var(--primary-color);
389
+ color: white;
390
+ cursor: pointer;
391
+ transition: background-color 0.2s;
392
+ }
393
+
394
+ #send-button:disabled {
395
+ background-color: #d1d5db;
396
+ cursor: not-allowed;
397
+ }
398
+
399
+ #send-button:not(:disabled):hover {
400
+ background-color: var(--primary-dark);
401
+ }
402
+
403
+ /* Context sidebar */
404
+ .context-sidebar {
405
+ background-color: white;
406
+ border-left: 1px solid var(--border-color);
407
+ display: flex;
408
+ flex-direction: column;
409
+ overflow: hidden;
410
+ transition: transform 0.3s ease;
411
+ }
412
+
413
+ .context-sidebar.collapsed {
414
+ transform: translateX(290px);
415
+ }
416
+
417
+ .context-header {
418
+ display: flex;
419
+ justify-content: space-between;
420
+ align-items: center;
421
+ padding: 1rem;
422
+ border-bottom: 1px solid var(--border-color);
423
+ }
424
+
425
+ .context-header h3 {
426
+ font-size: 1rem;
427
+ font-weight: 600;
428
+ }
429
+
430
+ .btn-toggle {
431
+ background: none;
432
+ border: none;
433
+ cursor: pointer;
434
+ color: var(--text-light);
435
+ width: 30px;
436
+ height: 30px;
437
+ display: flex;
438
+ align-items: center;
439
+ justify-content: center;
440
+ border-radius: 50%;
441
+ transition: background-color 0.2s;
442
+ }
443
+
444
+ .btn-toggle:hover {
445
+ background-color: var(--border-color);
446
+ }
447
+
448
+ .context-content {
449
+ padding: 1rem;
450
+ overflow-y: auto;
451
+ flex: 1;
452
+ }
453
+
454
+ .context-item {
455
+ margin-bottom: 1.5rem;
456
+ background-color: #f9fafb;
457
+ border-radius: var(--border-radius);
458
+ padding: 1rem;
459
+ border-left: 3px solid var(--primary-light);
460
+ }
461
+
462
+ .context-item:last-child {
463
+ margin-bottom: 0;
464
+ }
465
+
466
+ .context-score {
467
+ display: block;
468
+ font-size: 0.8rem;
469
+ color: var(--text-light);
470
+ margin-bottom: 0.5rem;
471
+ }
472
+
473
+ .context-text {
474
+ font-size: 0.9rem;
475
+ line-height: 1.5;
476
+ max-height: 200px;
477
+ overflow-y: auto;
478
+ }
479
+
480
+ .no-context {
481
+ color: var(--text-light);
482
+ font-style: italic;
483
+ text-align: center;
484
+ padding: 2rem 0;
485
+ }
486
+
487
+ /* Loading overlay */
488
+ .loading-overlay {
489
+ position: fixed;
490
+ top: 0;
491
+ left: 0;
492
+ right: 0;
493
+ bottom: 0;
494
+ background-color: rgba(0, 0, 0, 0.7);
495
+ display: flex;
496
+ flex-direction: column;
497
+ justify-content: center;
498
+ align-items: center;
499
+ z-index: 1000;
500
+ }
501
+
502
+ .spinner {
503
+ width: 50px;
504
+ height: 50px;
505
+ border-radius: 50%;
506
+ border: 5px solid rgba(255, 255, 255, 0.3);
507
+ border-top-color: var(--primary-color);
508
+ animation: spin 1s linear infinite;
509
+ }
510
+
511
+ #loading-text {
512
+ margin-top: 1rem;
513
+ color: white;
514
+ font-weight: 500;
515
+ }
516
+
517
+ @keyframes spin {
518
+ 0% {
519
+ transform: rotate(0deg);
520
+ }
521
+ 100% {
522
+ transform: rotate(360deg);
523
+ }
524
+ }
525
+
526
+ /* Message styling with Markdown support */
527
+ .message-content {
528
+ width: 100%;
529
+ word-break: break-word;
530
+ }
531
+
532
+ .message-content pre {
533
+ background: #f4f5f7;
534
+ border-radius: 5px;
535
+ padding: 0.75rem;
536
+ overflow-x: auto;
537
+ margin: 0.5rem 0;
538
+ }
539
+
540
+ .message-content code {
541
+ font-family: 'JetBrains Mono', monospace;
542
+ font-size: 0.9rem;
543
+ padding: 0.2rem 0.4rem;
544
+ background: #f1f1f1;
545
+ border-radius: 3px;
546
+ }
547
+
548
+ .message-content pre code {
549
+ background: transparent;
550
+ padding: 0;
551
+ white-space: pre;
552
+ color: #333;
553
+ line-height: 1.5;
554
+ }
555
+
556
+ .message-content p {
557
+ margin-bottom: 0.75rem;
558
+ }
559
+
560
+ .message-content p:last-child {
561
+ margin-bottom: 0;
562
+ }
563
+
564
+ .message-content ul, .message-content ol {
565
+ margin-left: 1.5rem;
566
+ margin-bottom: 0.75rem;
567
+ }
568
+
569
+ .message-content h1, .message-content h2, .message-content h3,
570
+ .message-content h4, .message-content h5, .message-content h6 {
571
+ margin-top: 1rem;
572
+ margin-bottom: 0.5rem;
573
+ font-weight: 600;
574
+ }
575
+
576
+ .message-content blockquote {
577
+ border-left: 4px solid #ddd;
578
+ padding-left: 1rem;
579
+ margin-left: 0.5rem;
580
+ color: #555;
581
+ }
582
+
583
+ .message-content a {
584
+ color: var(--primary-color);
585
+ text-decoration: underline;
586
+ }
587
+
588
+ .message-content table {
589
+ border-collapse: collapse;
590
+ width: 100%;
591
+ margin: 0.5rem 0;
592
+ }
593
+
594
+ .message-content table th,
595
+ .message-content table td {
596
+ border: 1px solid #ddd;
597
+ padding: 0.5rem;
598
+ text-align: left;
599
+ }
600
+
601
+ .message-content table th {
602
+ background-color: #f7f7f7;
603
+ }
604
+
605
+ /* Improved Responsive design */
606
+ @media (max-width: 1024px) {
607
+ .app-container {
608
+ grid-template-columns: 260px 1fr 0;
609
+ }
610
+
611
+ .context-sidebar {
612
+ position: fixed;
613
+ top: 0;
614
+ right: 0;
615
+ width: 300px;
616
+ height: 100vh;
617
+ z-index: 100;
618
+ transform: translateX(100%);
619
+ }
620
+
621
+ .context-sidebar:not(.collapsed) {
622
+ transform: translateX(0);
623
+ }
624
+
625
+ .message {
626
+ max-width: 90%;
627
+ }
628
+ }
629
+
630
+ @media (max-width: 768px) {
631
+ .app-container {
632
+ grid-template-columns: 1fr;
633
+ }
634
+
635
+ .sidebar {
636
+ position: fixed;
637
+ left: 0;
638
+ top: 0;
639
+ height: 100vh;
640
+ width: 260px;
641
+ z-index: 100;
642
+ transform: translateX(-100%);
643
+ }
644
+
645
+ .sidebar.show {
646
+ transform: translateX(0);
647
+ }
648
+
649
+ .menu-toggle {
650
+ display: flex;
651
+ position: fixed;
652
+ top: 1rem;
653
+ left: 1rem;
654
+ z-index: 101;
655
+ }
656
+
657
+ .main-content {
658
+ padding-top: 60px; /* Add spacing for the menu button */
659
+ }
660
+
661
+ .features {
662
+ flex-direction: column;
663
+ gap: 1rem;
664
+ }
665
+
666
+ .feature {
667
+ width: 100%;
668
+ }
669
+
670
+ .message {
671
+ max-width: 95%;
672
+ }
673
+
674
+ .welcome-header h1 {
675
+ font-size: 2rem;
676
+ }
677
+
678
+ .welcome-header p {
679
+ font-size: 1rem;
680
+ }
681
+
682
+ .chat-messages {
683
+ padding: 1rem;
684
+ }
685
+
686
+ .chat-input-box {
687
+ flex-direction: column;
688
+ }
689
+
690
+ #send-button {
691
+ align-self: flex-end;
692
+ }
693
+ }
694
+
695
+ @media (max-width: 480px) {
696
+ .message {
697
+ max-width: 100%;
698
+ }
699
+
700
+ .user-message, .assistant-message {
701
+ padding: 0.75rem;
702
+ }
703
+
704
+ .welcome-header h1 {
705
+ font-size: 1.5rem;
706
+ }
707
+
708
+ .features {
709
+ padding: 0 0.5rem;
710
+ }
711
+
712
+ .feature {
713
+ padding: 1.5rem 1rem;
714
+ }
715
+ }
static/index.html ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>PDF Insight Beta</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
8
+ <link rel="stylesheet" href="css/styles.css">
9
+ <!-- Add Highlight.js for code syntax highlighting -->
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
12
+ <!-- Add Marked.js for Markdown rendering -->
13
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
14
+ </head>
15
+ <body>
16
+ <div class="app-container">
17
+ <!-- Mobile menu toggle button -->
18
+ <button id="menu-toggle" class="menu-toggle hidden">
19
+ <i class="fas fa-bars"></i>
20
+ </button>
21
+
22
+ <!-- Sidebar -->
23
+ <div class="sidebar">
24
+ <div class="logo-container">
25
+ <h1>PDF Insight <span class="beta">Beta</span></h1>
26
+ </div>
27
+ <div class="upload-container">
28
+ <div class="upload-box" id="upload-box">
29
+ <i class="fas fa-file-pdf upload-icon"></i>
30
+ <p>Upload PDF Document</p>
31
+ <input type="file" id="pdf-upload" accept=".pdf,.txt" hidden>
32
+ </div>
33
+ <div class="model-selection">
34
+ <label for="model-select">Select Model:</label>
35
+ <select id="model-select">
36
+ <option value="meta-llama/llama-4-scout-17b-16e-instruct">Llama 4 Scout 17B</option>
37
+ <option value="llama-3.1-8b-instant">Llama 3.1 8B Instant</option>
38
+ <option value="llama-3.3-70b-versatile">Llama 3.3 70B Versatile</option>
39
+ </select>
40
+ </div>
41
+ <div class="session-info hidden" id="session-info">
42
+ <div class="file-info">
43
+ <i class="fas fa-file-pdf"></i>
44
+ <span id="current-file-name">No file loaded</span>
45
+ </div>
46
+ <button id="clear-history" class="btn-clear">Clear History</button>
47
+ <button id="new-chat" class="btn-new">New Document</button>
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ <!-- Main Content -->
53
+ <div class="main-content">
54
+ <!-- Welcome Screen -->
55
+ <div class="welcome-screen" id="welcome-screen">
56
+ <div class="welcome-header">
57
+ <h1>Welcome to PDF Insight Beta</h1>
58
+ <p>Upload a PDF document and start asking questions about it</p>
59
+ </div>
60
+ <div class="features">
61
+ <div class="feature">
62
+ <i class="fas fa-file-pdf"></i>
63
+ <h3>PDF Processing</h3>
64
+ <p>Upload any PDF document for instant analysis</p>
65
+ </div>
66
+ <div class="feature">
67
+ <i class="fas fa-robot"></i>
68
+ <h3>Agentic RAG</h3>
69
+ <p>Advanced retrieval augmented generation for accurate answers</p>
70
+ </div>
71
+ <div class="feature">
72
+ <i class="fas fa-search"></i>
73
+ <h3>Web Search Integration</h3>
74
+ <p>Verify document information with internet search capabilities</p>
75
+ </div>
76
+ </div>
77
+ <div class="get-started">
78
+ <button id="get-started-btn" class="btn-primary">Get Started</button>
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Chat Interface -->
83
+ <div class="chat-container hidden" id="chat-container">
84
+ <div class="chat-messages" id="chat-messages">
85
+ <div class="system-message">
86
+ <p>Upload successful! You can now ask questions about the document.</p>
87
+ </div>
88
+ </div>
89
+ <div class="chat-input-container">
90
+ <div class="search-toggle">
91
+ <input type="checkbox" id="search-toggle">
92
+ <label for="search-toggle">Use web search</label>
93
+ </div>
94
+ <div class="chat-input-box">
95
+ <textarea id="chat-input" placeholder="Ask a question about the document..."></textarea>
96
+ <button id="send-button" disabled>
97
+ <i class="fas fa-paper-plane"></i>
98
+ </button>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </div>
103
+
104
+ <!-- Context Sidebar -->
105
+ <div class="context-sidebar" id="context-sidebar">
106
+ <div class="context-header">
107
+ <h3>Context Used</h3>
108
+ <button id="toggle-context" class="btn-toggle">
109
+ <i class="fas fa-angle-right"></i>
110
+ </button>
111
+ </div>
112
+ <div class="context-content" id="context-content">
113
+ <p class="no-context">No context available yet. Ask a question first.</p>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <!-- Loading Overlay -->
119
+ <div class="loading-overlay hidden" id="loading-overlay">
120
+ <div class="spinner"></div>
121
+ <p id="loading-text">Processing Document...</p>
122
+ </div>
123
+
124
+ <script src="js/app.js"></script>
125
+ </body>
126
+ </html>
static/js/app.js ADDED
@@ -0,0 +1,504 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // DOM Elements
2
+ const uploadBox = document.getElementById('upload-box');
3
+ const pdfUpload = document.getElementById('pdf-upload');
4
+ const welcomeScreen = document.getElementById('welcome-screen');
5
+ const chatContainer = document.getElementById('chat-container');
6
+ const chatMessages = document.getElementById('chat-messages');
7
+ const chatInput = document.getElementById('chat-input');
8
+ const sendButton = document.getElementById('send-button');
9
+ const searchToggle = document.getElementById('search-toggle');
10
+ const modelSelect = document.getElementById('model-select');
11
+ const sessionInfo = document.getElementById('session-info');
12
+ const currentFileName = document.getElementById('current-file-name');
13
+ const clearHistoryBtn = document.getElementById('clear-history');
14
+ const newChatBtn = document.getElementById('new-chat');
15
+ const loadingOverlay = document.getElementById('loading-overlay');
16
+ const loadingText = document.getElementById('loading-text');
17
+ const getStartedBtn = document.getElementById('get-started-btn');
18
+ const contextContent = document.getElementById('context-content');
19
+ const contextSidebar = document.getElementById('context-sidebar');
20
+ const toggleContextBtn = document.getElementById('toggle-context');
21
+ const menuToggle = document.getElementById('menu-toggle');
22
+ const sidebar = document.querySelector('.sidebar');
23
+
24
+ // App state
25
+ let currentSessionId = null;
26
+ let lastContextData = null;
27
+ let isMobile = window.innerWidth <= 768;
28
+
29
+ // Event listeners
30
+ uploadBox.addEventListener('click', () => pdfUpload.click());
31
+ pdfUpload.addEventListener('change', handleFileUpload);
32
+ sendButton.addEventListener('click', sendMessage);
33
+ chatInput.addEventListener('keydown', (e) => {
34
+ if (e.key === 'Enter' && !e.shiftKey) {
35
+ e.preventDefault();
36
+ sendMessage();
37
+ }
38
+ });
39
+ chatInput.addEventListener('input', () => {
40
+ sendButton.disabled = chatInput.value.trim() === '';
41
+ });
42
+ clearHistoryBtn.addEventListener('click', clearChatHistory);
43
+ newChatBtn.addEventListener('click', resetApp);
44
+ getStartedBtn.addEventListener('click', () => {
45
+ uploadBox.click();
46
+ });
47
+ toggleContextBtn.addEventListener('click', toggleContextSidebar);
48
+
49
+ // Mobile menu event listeners
50
+ if (menuToggle) {
51
+ menuToggle.addEventListener('click', () => {
52
+ sidebar.classList.toggle('show');
53
+ });
54
+ }
55
+
56
+ // Handle window resize
57
+ window.addEventListener('resize', () => {
58
+ const wasMobile = isMobile;
59
+ isMobile = window.innerWidth <= 768;
60
+
61
+ // If we're transitioning between mobile/desktop
62
+ if (wasMobile !== isMobile) {
63
+ updateMobileUI();
64
+ }
65
+ });
66
+
67
+ // Initialize the app
68
+ initializeApp();
69
+
70
+ // Functions
71
+ function initializeApp() {
72
+ updateMobileUI();
73
+
74
+ // Check if we have a session in localStorage
75
+ const savedSession = localStorage.getItem('pdf_insight_session');
76
+ if (savedSession) {
77
+ try {
78
+ const session = JSON.parse(savedSession);
79
+ currentSessionId = session.id;
80
+ currentFileName.textContent = session.fileName;
81
+
82
+ // Show chat interface
83
+ welcomeScreen.classList.add('hidden');
84
+ chatContainer.classList.remove('hidden');
85
+ sessionInfo.classList.remove('hidden');
86
+
87
+ // Load chat history
88
+ fetchChatHistory();
89
+ } catch (e) {
90
+ console.error('Failed to load saved session:', e);
91
+ localStorage.removeItem('pdf_insight_session');
92
+ }
93
+ }
94
+ }
95
+
96
+ // Update UI based on mobile/desktop view
97
+ function updateMobileUI() {
98
+ if (isMobile) {
99
+ if (menuToggle) menuToggle.classList.remove('hidden');
100
+ contextSidebar.classList.add('collapsed');
101
+ } else {
102
+ if (menuToggle) menuToggle.classList.add('hidden');
103
+ sidebar.classList.remove('show');
104
+ }
105
+ }
106
+
107
+ async function handleFileUpload(e) {
108
+ const file = e.target.files[0];
109
+ if (!file) return;
110
+
111
+ try {
112
+ // Show loading overlay
113
+ showLoading('Processing Document...');
114
+
115
+ const formData = new FormData();
116
+ formData.append('file', file);
117
+ formData.append('model_name', modelSelect.value);
118
+
119
+ const response = await fetch('/upload-pdf', {
120
+ method: 'POST',
121
+ body: formData
122
+ });
123
+
124
+ const data = await response.json();
125
+
126
+ if (data.status === 'success') {
127
+ currentSessionId = data.session_id;
128
+ currentFileName.textContent = file.name;
129
+
130
+ // Save session to localStorage
131
+ localStorage.setItem('pdf_insight_session', JSON.stringify({
132
+ id: currentSessionId,
133
+ fileName: file.name
134
+ }));
135
+
136
+ // Show chat interface
137
+ welcomeScreen.classList.add('hidden');
138
+ chatContainer.classList.remove('hidden');
139
+ sessionInfo.classList.remove('hidden');
140
+
141
+ // Reset chat history view
142
+ chatMessages.innerHTML = `
143
+ <div class="system-message">
144
+ <p>Upload successful! You can now ask questions about "${file.name}".</p>
145
+ </div>
146
+ `;
147
+
148
+ // Enable input
149
+ chatInput.disabled = false;
150
+ chatInput.placeholder = 'Ask a question about the document...';
151
+
152
+ // Close sidebar on mobile after uploading
153
+ if (isMobile) {
154
+ sidebar.classList.remove('show');
155
+ }
156
+ } else {
157
+ // Enhanced error display
158
+ let errorDetails = '';
159
+ if (data.detail) {
160
+ errorDetails = data.detail;
161
+ }
162
+ if (data.type) {
163
+ errorDetails = `${data.type}: ${errorDetails}`;
164
+ }
165
+
166
+ showError('Error: ' + (errorDetails || 'Failed to process document'));
167
+
168
+ // Add a more detailed error in the chat area if we're already in chat mode
169
+ if (!welcomeScreen.classList.contains('hidden')) {
170
+ const errorMessageDiv = document.createElement('div');
171
+ errorMessageDiv.className = 'system-message error';
172
+ errorMessageDiv.innerHTML = `
173
+ <p><strong>Error Processing Document</strong></p>
174
+ <p>${errorDetails}</p>
175
+ <p>Please make sure you have set up all required API keys in the .env file.</p>
176
+ `;
177
+ chatMessages.appendChild(errorMessageDiv);
178
+ }
179
+ }
180
+ } catch (error) {
181
+ console.error('Error uploading file:', error);
182
+ showError('Failed to upload document. Please try again.');
183
+ } finally {
184
+ hideLoading();
185
+ }
186
+ }
187
+
188
+ async function sendMessage() {
189
+ const query = chatInput.value.trim();
190
+ if (!query || !currentSessionId) return;
191
+
192
+ // Disable input and show typing indicator
193
+ chatInput.disabled = true;
194
+ sendButton.disabled = true;
195
+
196
+ // Add user message to chat
197
+ const userMessageElement = createMessageElement('user', query);
198
+ chatMessages.appendChild(userMessageElement);
199
+ chatMessages.scrollTop = chatMessages.scrollHeight;
200
+
201
+ // Clear input
202
+ chatInput.value = '';
203
+
204
+ try {
205
+ // Show loading state
206
+ const typingIndicator = createTypingIndicator();
207
+ chatMessages.appendChild(typingIndicator);
208
+ chatMessages.scrollTop = chatMessages.scrollHeight;
209
+
210
+ const response = await fetch('/chat', {
211
+ method: 'POST',
212
+ headers: {
213
+ 'Content-Type': 'application/json'
214
+ },
215
+ body: JSON.stringify({
216
+ session_id: currentSessionId,
217
+ query: query,
218
+ use_search: searchToggle.checked,
219
+ model_name: modelSelect.value
220
+ })
221
+ });
222
+
223
+ const data = await response.json();
224
+
225
+ // Remove typing indicator
226
+ chatMessages.removeChild(typingIndicator);
227
+
228
+ if (data.status === 'success') {
229
+ // Add assistant message
230
+ const assistantMessageElement = createMessageElement('assistant', data.answer);
231
+ chatMessages.appendChild(assistantMessageElement);
232
+ chatMessages.scrollTop = chatMessages.scrollHeight;
233
+
234
+ // Apply syntax highlighting to code blocks
235
+ applyCodeHighlighting();
236
+
237
+ // Update context sidebar
238
+ updateContextSidebar(data.context_used);
239
+ lastContextData = data.context_used;
240
+ } else {
241
+ showError('Failed to get response: ' + data.detail);
242
+ }
243
+ } catch (error) {
244
+ console.error('Error sending message:', error);
245
+ showError('Failed to get response. Please try again.');
246
+
247
+ // Remove typing indicator if it exists
248
+ const indicator = document.querySelector('.typing-indicator');
249
+ if (indicator) {
250
+ chatMessages.removeChild(indicator);
251
+ }
252
+ } finally {
253
+ // Re-enable input
254
+ chatInput.disabled = false;
255
+ chatInput.focus();
256
+ }
257
+ }
258
+
259
+ async function fetchChatHistory() {
260
+ if (!currentSessionId) return;
261
+
262
+ try {
263
+ showLoading('Loading chat history...');
264
+
265
+ const response = await fetch('/chat-history', {
266
+ method: 'POST',
267
+ headers: {
268
+ 'Content-Type': 'application/json'
269
+ },
270
+ body: JSON.stringify({
271
+ session_id: currentSessionId
272
+ })
273
+ });
274
+
275
+ const data = await response.json();
276
+
277
+ if (data.status === 'success' && data.history.length > 0) {
278
+ // Clear chat messages and add system message
279
+ chatMessages.innerHTML = `
280
+ <div class="system-message">
281
+ <p>Continuing your conversation about "${currentFileName.textContent}".</p>
282
+ </div>
283
+ `;
284
+
285
+ // Add all messages from history
286
+ data.history.forEach(item => {
287
+ const userMessage = createMessageElement('user', item.user);
288
+ const assistantMessage = createMessageElement('assistant', item.assistant);
289
+ chatMessages.appendChild(userMessage);
290
+ chatMessages.appendChild(assistantMessage);
291
+ });
292
+
293
+ // Apply syntax highlighting to code blocks
294
+ applyCodeHighlighting();
295
+
296
+ // Scroll to bottom
297
+ chatMessages.scrollTop = chatMessages.scrollHeight;
298
+ }
299
+ } catch (error) {
300
+ console.error('Error fetching chat history:', error);
301
+ showError('Failed to load chat history.');
302
+ } finally {
303
+ hideLoading();
304
+ }
305
+ }
306
+
307
+ async function clearChatHistory() {
308
+ if (!currentSessionId) return;
309
+
310
+ try {
311
+ showLoading('Clearing chat history...');
312
+
313
+ const response = await fetch('/clear-history', {
314
+ method: 'POST',
315
+ headers: {
316
+ 'Content-Type': 'application/json'
317
+ },
318
+ body: JSON.stringify({
319
+ session_id: currentSessionId
320
+ })
321
+ });
322
+
323
+ const data = await response.json();
324
+
325
+ if (data.status === 'success') {
326
+ // Reset chat messages
327
+ chatMessages.innerHTML = `
328
+ <div class="system-message">
329
+ <p>Chat history cleared. You can continue asking questions about "${currentFileName.textContent}".</p>
330
+ </div>
331
+ `;
332
+
333
+ // Reset context
334
+ contextContent.innerHTML = '<p class="no-context">No context available yet. Ask a question first.</p>';
335
+ lastContextData = null;
336
+ } else {
337
+ showError('Failed to clear chat history: ' + data.detail);
338
+ }
339
+ } catch (error) {
340
+ console.error('Error clearing chat history:', error);
341
+ showError('Failed to clear chat history.');
342
+ } finally {
343
+ hideLoading();
344
+ }
345
+ }
346
+
347
+ function resetApp() {
348
+ // Clear current session
349
+ currentSessionId = null;
350
+ lastContextData = null;
351
+ localStorage.removeItem('pdf_insight_session');
352
+
353
+ // Reset UI
354
+ welcomeScreen.classList.remove('hidden');
355
+ chatContainer.classList.add('hidden');
356
+ sessionInfo.classList.add('hidden');
357
+
358
+ // Reset file input
359
+ pdfUpload.value = '';
360
+
361
+ // Reset chat messages
362
+ chatMessages.innerHTML = `
363
+ <div class="system-message">
364
+ <p>Upload successful! You can now ask questions about the document.</p>
365
+ </div>
366
+ `;
367
+
368
+ // Reset context sidebar
369
+ contextContent.innerHTML = '<p class="no-context">No context available yet. Ask a question first.</p>';
370
+ }
371
+
372
+ function createMessageElement(type, content) {
373
+ const div = document.createElement('div');
374
+ div.className = `message ${type}-message`;
375
+
376
+ // For user messages, just escape HTML
377
+ if (type === 'user') {
378
+ div.innerHTML = `
379
+ <div class="message-content">${escapeHTML(content)}</div>
380
+ <div class="message-timestamp">${formatTimestamp(new Date())}</div>
381
+ `;
382
+ }
383
+ // For assistant messages, render with Markdown
384
+ else {
385
+ // Configure marked.js options
386
+ marked.setOptions({
387
+ breaks: true, // Add <br> on single line breaks
388
+ gfm: true, // GitHub Flavored Markdown
389
+ sanitize: false // Allow HTML in the input
390
+ });
391
+
392
+ // Process the content with marked
393
+ const renderedContent = marked.parse(content);
394
+
395
+ div.innerHTML = `
396
+ <div class="message-content">${renderedContent}</div>
397
+ <div class="message-timestamp">${formatTimestamp(new Date())}</div>
398
+ `;
399
+ }
400
+
401
+ return div;
402
+ }
403
+
404
+ function createTypingIndicator() {
405
+ const div = document.createElement('div');
406
+ div.className = 'message assistant-message typing-indicator';
407
+ div.innerHTML = `
408
+ <div class="typing-animation">
409
+ <span class="dot"></span>
410
+ <span class="dot"></span>
411
+ <span class="dot"></span>
412
+ </div>
413
+ `;
414
+ return div;
415
+ }
416
+
417
+ function updateContextSidebar(contextData) {
418
+ if (!contextData || contextData.length === 0) {
419
+ contextContent.innerHTML = '<p class="no-context">No context available for this response.</p>';
420
+ return;
421
+ }
422
+
423
+ contextContent.innerHTML = '';
424
+
425
+ contextData.forEach((item, index) => {
426
+ const contextItem = document.createElement('div');
427
+ contextItem.className = 'context-item';
428
+
429
+ const score = Math.round((1 - item.score) * 100); // Convert distance to similarity score
430
+
431
+ contextItem.innerHTML = `
432
+ <span class="context-score">Relevance: ${score}%</span>
433
+ <div class="context-text">${truncateText(item.text, 300)}</div>
434
+ `;
435
+
436
+ contextContent.appendChild(contextItem);
437
+ });
438
+ }
439
+
440
+ function toggleContextSidebar() {
441
+ contextSidebar.classList.toggle('collapsed');
442
+
443
+ // Update icon
444
+ const icon = toggleContextBtn.querySelector('i');
445
+ if (contextSidebar.classList.contains('collapsed')) {
446
+ icon.className = 'fas fa-angle-left';
447
+ } else {
448
+ icon.className = 'fas fa-angle-right';
449
+ }
450
+ }
451
+
452
+ function applyCodeHighlighting() {
453
+ // Find all code blocks in the assistant messages
454
+ document.querySelectorAll('.assistant-message pre code').forEach(block => {
455
+ hljs.highlightElement(block);
456
+ });
457
+ }
458
+
459
+ function showLoading(message) {
460
+ loadingText.textContent = message || 'Loading...';
461
+ loadingOverlay.classList.remove('hidden');
462
+ }
463
+
464
+ function hideLoading() {
465
+ loadingOverlay.classList.add('hidden');
466
+ }
467
+
468
+ function showError(message) {
469
+ // Add error message to chat
470
+ const errorDiv = document.createElement('div');
471
+ errorDiv.className = 'system-message error';
472
+ errorDiv.innerHTML = `<p>${message}</p>`;
473
+
474
+ chatMessages.appendChild(errorDiv);
475
+ chatMessages.scrollTop = chatMessages.scrollHeight;
476
+
477
+ // Also show an alert for critical errors
478
+ if (message.includes('API_KEY') || message.includes('environment variables')) {
479
+ alert('Configuration Error: ' + message);
480
+ }
481
+
482
+ // Remove after 10 seconds
483
+ setTimeout(() => {
484
+ if (errorDiv.parentNode === chatMessages) {
485
+ chatMessages.removeChild(errorDiv);
486
+ }
487
+ }, 10000);
488
+ }
489
+
490
+ // Helper functions
491
+ function formatTimestamp(date) {
492
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
493
+ }
494
+
495
+ function truncateText(text, maxLength) {
496
+ if (text.length <= maxLength) return text;
497
+ return text.substring(0, maxLength) + '...';
498
+ }
499
+
500
+ function escapeHTML(text) {
501
+ const div = document.createElement('div');
502
+ div.textContent = text;
503
+ return div.innerHTML;
504
+ }