mc-pdf-studio 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1700 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>✦ MC PDF Studio</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>
9
+ <link rel="preconnect" href="https://fonts.googleapis.com"/>
10
+ <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;1,9..40,300&display=swap" rel="stylesheet"/>
11
+ <link rel="icon" type="image/x-icon" href="https://mohan-chinnappan-n5.github.io/dfv/img/mc_favIcon.ico" />
12
+ <style>
13
+ /* ── CSS Variables ── */
14
+ :root {
15
+ --ff-ui: 'DM Sans', sans-serif;
16
+ --ff-display: 'Syne', sans-serif;
17
+ --ff-mono: 'DM Mono', monospace;
18
+ --radius: 10px;
19
+ --radius-sm: 6px;
20
+ --sidebar-w: 200px;
21
+ --ai-w: 380px;
22
+ --header-h: 52px;
23
+ --transition: 0.22s cubic-bezier(0.4,0,0.2,1);
24
+ }
25
+
26
+ [data-theme="dark"] {
27
+ --bg-base: #0e0f13;
28
+ --bg-panel: #14161d;
29
+ --bg-card: #1c1f2a;
30
+ --bg-elevated: #232636;
31
+ --bg-input: #1a1d28;
32
+ --border: #2d3148;
33
+ --border-subtle: #222539;
34
+ --text-primary: #eef0f8;
35
+ --text-secondary: #8b90ab;
36
+ --text-muted: #545878;
37
+ --accent: #7c6af7;
38
+ --accent-glow: rgba(124,106,247,0.25);
39
+ --accent-soft: rgba(124,106,247,0.12);
40
+ --accent2: #f0886a;
41
+ --success: #4cc9a0;
42
+ --thumb: #3d4265;
43
+ --shadow: 0 4px 24px rgba(0,0,0,0.5);
44
+ --shadow-lg: 0 8px 48px rgba(0,0,0,0.7);
45
+ }
46
+
47
+ [data-theme="light"] {
48
+ --bg-base: #f4f5f9;
49
+ --bg-panel: #ffffff;
50
+ --bg-card: #f9fafb;
51
+ --bg-elevated: #ffffff;
52
+ --bg-input: #f0f1f6;
53
+ --border: #e0e2ef;
54
+ --border-subtle: #edeef5;
55
+ --text-primary: #1a1b2e;
56
+ --text-secondary: #5a5f7d;
57
+ --text-muted: #9699b0;
58
+ --accent: #6457e8;
59
+ --accent-glow: rgba(100,87,232,0.15);
60
+ --accent-soft: rgba(100,87,232,0.08);
61
+ --accent2: #e8603a;
62
+ --success: #2aa87a;
63
+ --thumb: #c8ccdf;
64
+ --shadow: 0 2px 16px rgba(0,0,0,0.08);
65
+ --shadow-lg: 0 8px 40px rgba(0,0,0,0.12);
66
+ }
67
+
68
+ /* ── Reset ── */
69
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
70
+ html, body { height: 100%; overflow: hidden; }
71
+ body {
72
+ font-family: var(--ff-ui);
73
+ background: var(--bg-base);
74
+ color: var(--text-primary);
75
+ font-size: 14px;
76
+ line-height: 1.5;
77
+ }
78
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
79
+ ::-webkit-scrollbar-track { background: transparent; }
80
+ ::-webkit-scrollbar-thumb { background: var(--thumb); border-radius: 99px; }
81
+
82
+ /* ── Layout ── */
83
+ #app { display: flex; flex-direction: column; height: 100vh; }
84
+
85
+ /* ── Header ── */
86
+ #header {
87
+ height: var(--header-h);
88
+ background: var(--bg-panel);
89
+ border-bottom: 1px solid var(--border-subtle);
90
+ display: flex;
91
+ align-items: center;
92
+ gap: 12px;
93
+ padding: 0 16px;
94
+ flex-shrink: 0;
95
+ position: relative;
96
+ z-index: 100;
97
+ }
98
+
99
+ .logo {
100
+ font-family: var(--ff-display);
101
+ font-weight: 800;
102
+ font-size: 15px;
103
+ letter-spacing: -0.02em;
104
+ color: var(--text-primary);
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 6px;
108
+ flex-shrink: 0;
109
+ }
110
+ .logo span { color: var(--accent); }
111
+
112
+ .header-sep {
113
+ width: 1px;
114
+ height: 22px;
115
+ background: var(--border);
116
+ flex-shrink: 0;
117
+ }
118
+
119
+ .file-info {
120
+ flex: 1;
121
+ min-width: 0;
122
+ display: flex;
123
+ align-items: center;
124
+ gap: 8px;
125
+ }
126
+ .file-name {
127
+ font-family: var(--ff-mono);
128
+ font-size: 12px;
129
+ color: var(--text-secondary);
130
+ white-space: nowrap;
131
+ overflow: hidden;
132
+ text-overflow: ellipsis;
133
+ }
134
+ .file-badge {
135
+ background: var(--accent-soft);
136
+ border: 1px solid var(--accent-glow);
137
+ color: var(--accent);
138
+ font-size: 10px;
139
+ font-family: var(--ff-mono);
140
+ font-weight: 500;
141
+ padding: 2px 7px;
142
+ border-radius: 99px;
143
+ flex-shrink: 0;
144
+ }
145
+
146
+ .header-controls { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
147
+
148
+ .btn-icon {
149
+ width: 32px;
150
+ height: 32px;
151
+ border: 1px solid var(--border);
152
+ background: var(--bg-card);
153
+ border-radius: var(--radius-sm);
154
+ cursor: pointer;
155
+ display: flex;
156
+ align-items: center;
157
+ justify-content: center;
158
+ color: var(--text-secondary);
159
+ transition: all var(--transition);
160
+ font-size: 15px;
161
+ }
162
+ .btn-icon:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
163
+ .btn-icon.active { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
164
+
165
+ .btn-primary {
166
+ height: 32px;
167
+ padding: 0 14px;
168
+ background: var(--accent);
169
+ color: #fff;
170
+ border: none;
171
+ border-radius: var(--radius-sm);
172
+ font-family: var(--ff-ui);
173
+ font-size: 13px;
174
+ font-weight: 500;
175
+ cursor: pointer;
176
+ transition: all var(--transition);
177
+ display: flex;
178
+ align-items: center;
179
+ gap: 6px;
180
+ white-space: nowrap;
181
+ }
182
+ .btn-primary:hover { opacity: 0.88; transform: translateY(-1px); }
183
+
184
+ .page-nav {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 4px;
188
+ }
189
+ .page-input {
190
+ width: 44px;
191
+ height: 28px;
192
+ text-align: center;
193
+ background: var(--bg-input);
194
+ border: 1px solid var(--border);
195
+ border-radius: var(--radius-sm);
196
+ color: var(--text-primary);
197
+ font-family: var(--ff-mono);
198
+ font-size: 12px;
199
+ outline: none;
200
+ transition: border-color var(--transition);
201
+ }
202
+ .page-input:focus { border-color: var(--accent); }
203
+ .page-total { font-size: 12px; color: var(--text-muted); font-family: var(--ff-mono); }
204
+
205
+ .zoom-control { display: flex; align-items: center; gap: 4px; }
206
+ .zoom-val {
207
+ font-family: var(--ff-mono);
208
+ font-size: 11px;
209
+ color: var(--text-secondary);
210
+ min-width: 38px;
211
+ text-align: center;
212
+ }
213
+
214
+ /* ── Main ── */
215
+ #main { display: flex; flex: 1; min-height: 0; overflow: hidden; }
216
+
217
+ /* ── Minimap Panel ── */
218
+ #minimap-panel {
219
+ width: var(--sidebar-w);
220
+ flex-shrink: 0;
221
+ background: var(--bg-panel);
222
+ border-right: 1px solid var(--border-subtle);
223
+ display: flex;
224
+ flex-direction: column;
225
+ overflow: hidden;
226
+ transition: width var(--transition), opacity var(--transition);
227
+ }
228
+ #minimap-panel.hidden { width: 0; opacity: 0; pointer-events: none; }
229
+
230
+ .panel-header {
231
+ padding: 10px 12px 8px;
232
+ font-family: var(--ff-display);
233
+ font-size: 10px;
234
+ font-weight: 700;
235
+ letter-spacing: 0.1em;
236
+ text-transform: uppercase;
237
+ color: var(--text-muted);
238
+ border-bottom: 1px solid var(--border-subtle);
239
+ flex-shrink: 0;
240
+ }
241
+
242
+ #minimap-scroll {
243
+ flex: 1;
244
+ overflow-y: auto;
245
+ overflow-x: hidden;
246
+ padding: 10px 8px;
247
+ display: flex;
248
+ flex-direction: column;
249
+ gap: 8px;
250
+ }
251
+
252
+ .mini-page {
253
+ position: relative;
254
+ cursor: pointer;
255
+ border-radius: 5px;
256
+ overflow: hidden;
257
+ border: 2px solid transparent;
258
+ transition: all var(--transition);
259
+ flex-shrink: 0;
260
+ }
261
+ .mini-page:hover { border-color: var(--accent); }
262
+ .mini-page.active { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-glow); }
263
+ .mini-page canvas { width: 100% !important; height: auto !important; display: block; }
264
+ .mini-label {
265
+ position: absolute;
266
+ bottom: 3px;
267
+ right: 5px;
268
+ font-family: var(--ff-mono);
269
+ font-size: 9px;
270
+ color: var(--text-muted);
271
+ background: var(--bg-base);
272
+ padding: 1px 4px;
273
+ border-radius: 3px;
274
+ opacity: 0.85;
275
+ }
276
+ .mini-page.active .mini-label { color: var(--accent); }
277
+
278
+ /* ── PDF Viewer ── */
279
+ #viewer-panel {
280
+ flex: 1;
281
+ display: flex;
282
+ flex-direction: column;
283
+ min-width: 0;
284
+ overflow: hidden;
285
+ background: var(--bg-base);
286
+ }
287
+
288
+ #pdf-scroll {
289
+ flex: 1;
290
+ overflow-y: auto;
291
+ overflow-x: auto;
292
+ padding: 24px;
293
+ display: flex;
294
+ flex-direction: column;
295
+ align-items: center;
296
+ gap: 20px;
297
+ }
298
+
299
+ .pdf-page-wrapper {
300
+ position: relative;
301
+ display: flex;
302
+ flex-direction: column;
303
+ align-items: center;
304
+ }
305
+
306
+ .pdf-page-canvas {
307
+ display: block;
308
+ background: white;
309
+ border-radius: 4px;
310
+ box-shadow: var(--shadow);
311
+ transition: box-shadow var(--transition);
312
+ }
313
+ .pdf-page-canvas:hover { box-shadow: var(--shadow-lg); }
314
+
315
+ .page-number-label {
316
+ margin-top: 8px;
317
+ font-family: var(--ff-mono);
318
+ font-size: 11px;
319
+ color: var(--text-muted);
320
+ }
321
+
322
+ /* ── Drop Zone ── */
323
+ #drop-zone {
324
+ position: absolute;
325
+ inset: 0;
326
+ display: flex;
327
+ flex-direction: column;
328
+ align-items: center;
329
+ justify-content: center;
330
+ gap: 16px;
331
+ z-index: 10;
332
+ }
333
+ #drop-zone.hidden { display: none; }
334
+ .drop-ring {
335
+ width: 160px;
336
+ height: 160px;
337
+ border: 2px dashed var(--border);
338
+ border-radius: 50%;
339
+ display: flex;
340
+ align-items: center;
341
+ justify-content: center;
342
+ font-size: 48px;
343
+ transition: all 0.3s;
344
+ animation: pulse-ring 2s ease-in-out infinite;
345
+ }
346
+ @keyframes pulse-ring {
347
+ 0%, 100% { border-color: var(--border); }
348
+ 50% { border-color: var(--accent); }
349
+ }
350
+ #drop-zone.drag-over .drop-ring { border-color: var(--accent); background: var(--accent-soft); transform: scale(1.05); }
351
+ .drop-title {
352
+ font-family: var(--ff-display);
353
+ font-size: 20px;
354
+ font-weight: 700;
355
+ color: var(--text-primary);
356
+ }
357
+ .drop-sub { font-size: 13px; color: var(--text-secondary); }
358
+ .drop-or {
359
+ font-size: 12px;
360
+ color: var(--text-muted);
361
+ display: flex;
362
+ align-items: center;
363
+ gap: 10px;
364
+ }
365
+ .drop-or::before, .drop-or::after {
366
+ content: '';
367
+ width: 40px;
368
+ height: 1px;
369
+ background: var(--border);
370
+ }
371
+
372
+ /* ── AI Sidekick Panel ── */
373
+ #ai-panel {
374
+ width: 0;
375
+ overflow: hidden;
376
+ background: var(--bg-panel);
377
+ border-left: 1px solid var(--border-subtle);
378
+ display: flex;
379
+ flex-direction: column;
380
+ transition: width var(--transition);
381
+ flex-shrink: 0;
382
+ }
383
+ #ai-panel.open { width: var(--ai-w); }
384
+
385
+ .ai-header {
386
+ padding: 14px 16px;
387
+ border-bottom: 1px solid var(--border-subtle);
388
+ flex-shrink: 0;
389
+ display: flex;
390
+ align-items: center;
391
+ gap: 10px;
392
+ }
393
+ .ai-icon {
394
+ width: 30px;
395
+ height: 30px;
396
+ border-radius: 8px;
397
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
398
+ display: flex;
399
+ align-items: center;
400
+ justify-content: center;
401
+ font-size: 15px;
402
+ flex-shrink: 0;
403
+ }
404
+ .ai-title {
405
+ flex: 1;
406
+ min-width: 0;
407
+ }
408
+ .ai-title h3 {
409
+ font-family: var(--ff-display);
410
+ font-size: 14px;
411
+ font-weight: 700;
412
+ color: var(--text-primary);
413
+ line-height: 1.2;
414
+ }
415
+ .ai-title p {
416
+ font-size: 11px;
417
+ color: var(--text-muted);
418
+ white-space: nowrap;
419
+ overflow: hidden;
420
+ text-overflow: ellipsis;
421
+ }
422
+ .ai-close {
423
+ background: none;
424
+ border: none;
425
+ color: var(--text-muted);
426
+ cursor: pointer;
427
+ font-size: 18px;
428
+ line-height: 1;
429
+ padding: 2px;
430
+ border-radius: 4px;
431
+ transition: all var(--transition);
432
+ }
433
+ .ai-close:hover { color: var(--text-primary); background: var(--bg-card); }
434
+
435
+ /* Quick prompts */
436
+ .ai-quick-wrap {
437
+ padding: 10px 14px;
438
+ border-bottom: 1px solid var(--border-subtle);
439
+ flex-shrink: 0;
440
+ display: flex;
441
+ flex-wrap: wrap;
442
+ gap: 6px;
443
+ }
444
+ .ai-quick-chip {
445
+ background: var(--bg-card);
446
+ border: 1px solid var(--border);
447
+ color: var(--text-secondary);
448
+ font-size: 11px;
449
+ padding: 4px 10px;
450
+ border-radius: 99px;
451
+ cursor: pointer;
452
+ transition: all var(--transition);
453
+ white-space: nowrap;
454
+ font-family: var(--ff-ui);
455
+ }
456
+ .ai-quick-chip:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
457
+
458
+ /* Chat messages */
459
+ #ai-messages {
460
+ flex: 1;
461
+ overflow-y: auto;
462
+ padding: 14px;
463
+ display: flex;
464
+ flex-direction: column;
465
+ gap: 14px;
466
+ min-height: 0;
467
+ }
468
+
469
+ .msg {
470
+ display: flex;
471
+ gap: 8px;
472
+ animation: msg-in 0.25s ease;
473
+ }
474
+ @keyframes msg-in {
475
+ from { opacity: 0; transform: translateY(8px); }
476
+ to { opacity: 1; transform: translateY(0); }
477
+ }
478
+
479
+ .msg-avatar {
480
+ width: 26px;
481
+ height: 26px;
482
+ border-radius: 7px;
483
+ flex-shrink: 0;
484
+ display: flex;
485
+ align-items: center;
486
+ justify-content: center;
487
+ font-size: 13px;
488
+ }
489
+ .msg.user .msg-avatar { background: var(--bg-card); border: 1px solid var(--border); margin-left: auto; order: 2; }
490
+ .msg.assistant .msg-avatar { background: linear-gradient(135deg, var(--accent), var(--accent2)); }
491
+
492
+ .msg-body { flex: 1; min-width: 0; }
493
+ .msg.user .msg-body { display: flex; justify-content: flex-end; }
494
+
495
+ .msg-bubble {
496
+ padding: 10px 13px;
497
+ border-radius: 12px;
498
+ font-size: 13px;
499
+ line-height: 1.6;
500
+ max-width: 100%;
501
+ word-break: break-word;
502
+ }
503
+ .msg.user .msg-bubble {
504
+ background: var(--accent);
505
+ color: #fff;
506
+ border-bottom-right-radius: 4px;
507
+ }
508
+ .msg.assistant .msg-bubble {
509
+ background: var(--bg-card);
510
+ border: 1px solid var(--border-subtle);
511
+ color: var(--text-primary);
512
+ border-bottom-left-radius: 4px;
513
+ }
514
+ .msg.assistant .msg-bubble p { margin: 0 0 6px; }
515
+ .msg.assistant .msg-bubble p:last-child { margin: 0; }
516
+ .msg.assistant .msg-bubble ul, .msg.assistant .msg-bubble ol { padding-left: 18px; margin: 4px 0; }
517
+ .msg.assistant .msg-bubble li { margin: 2px 0; }
518
+ .msg.assistant .msg-bubble strong { color: var(--text-primary); font-weight: 600; }
519
+ .msg.assistant .msg-bubble code {
520
+ background: var(--bg-elevated);
521
+ padding: 1px 5px;
522
+ border-radius: 4px;
523
+ font-family: var(--ff-mono);
524
+ font-size: 12px;
525
+ }
526
+ .msg.assistant .msg-bubble pre {
527
+ background: var(--bg-base);
528
+ border: 1px solid var(--border);
529
+ border-radius: 6px;
530
+ padding: 10px;
531
+ overflow-x: auto;
532
+ margin: 8px 0;
533
+ }
534
+ .msg.assistant .msg-bubble pre code { background: none; padding: 0; font-size: 12px; }
535
+
536
+ .msg-time {
537
+ font-size: 10px;
538
+ color: var(--text-muted);
539
+ margin-top: 4px;
540
+ font-family: var(--ff-mono);
541
+ }
542
+ .msg.user .msg-time { text-align: right; }
543
+
544
+ /* Typing indicator */
545
+ .typing-dots {
546
+ display: flex;
547
+ gap: 4px;
548
+ align-items: center;
549
+ padding: 12px 14px;
550
+ }
551
+ .typing-dots span {
552
+ width: 6px;
553
+ height: 6px;
554
+ border-radius: 50%;
555
+ background: var(--accent);
556
+ animation: dot-bounce 1.2s ease-in-out infinite;
557
+ }
558
+ .typing-dots span:nth-child(2) { animation-delay: 0.2s; }
559
+ .typing-dots span:nth-child(3) { animation-delay: 0.4s; }
560
+ @keyframes dot-bounce {
561
+ 0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
562
+ 40% { transform: translateY(-6px); opacity: 1; }
563
+ }
564
+
565
+ /* AI Input */
566
+ .ai-input-wrap {
567
+ padding: 12px 14px;
568
+ border-top: 1px solid var(--border-subtle);
569
+ flex-shrink: 0;
570
+ display: flex;
571
+ gap: 8px;
572
+ align-items: flex-end;
573
+ }
574
+ #ai-input {
575
+ flex: 1;
576
+ resize: none;
577
+ background: var(--bg-input);
578
+ border: 1px solid var(--border);
579
+ border-radius: var(--radius-sm);
580
+ padding: 8px 12px;
581
+ color: var(--text-primary);
582
+ font-family: var(--ff-ui);
583
+ font-size: 13px;
584
+ outline: none;
585
+ transition: border-color var(--transition);
586
+ min-height: 36px;
587
+ max-height: 120px;
588
+ line-height: 1.5;
589
+ }
590
+ #ai-input:focus { border-color: var(--accent); }
591
+ #ai-input::placeholder { color: var(--text-muted); }
592
+ .ai-send {
593
+ width: 34px;
594
+ height: 34px;
595
+ border-radius: var(--radius-sm);
596
+ background: var(--accent);
597
+ border: none;
598
+ cursor: pointer;
599
+ display: flex;
600
+ align-items: center;
601
+ justify-content: center;
602
+ transition: all var(--transition);
603
+ flex-shrink: 0;
604
+ }
605
+ .ai-send:hover { opacity: 0.85; transform: translateY(-1px); }
606
+ .ai-send:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
607
+ .ai-send svg { fill: white; width: 16px; height: 16px; }
608
+
609
+ /* AI disabled state */
610
+ .ai-disabled {
611
+ flex: 1;
612
+ display: flex;
613
+ flex-direction: column;
614
+ align-items: center;
615
+ justify-content: center;
616
+ gap: 12px;
617
+ padding: 24px;
618
+ text-align: center;
619
+ color: var(--text-secondary);
620
+ }
621
+ .ai-disabled .icon { font-size: 36px; opacity: 0.5; }
622
+ .ai-disabled p { font-size: 13px; line-height: 1.6; }
623
+ .ai-disabled code { font-family: var(--ff-mono); background: var(--bg-card); padding: 2px 6px; border-radius: 4px; font-size: 12px; color: var(--accent2); }
624
+
625
+ /* ── Loading ── */
626
+ #loading-overlay {
627
+ position: fixed;
628
+ inset: 0;
629
+ background: var(--bg-base);
630
+ display: flex;
631
+ align-items: center;
632
+ justify-content: center;
633
+ z-index: 1000;
634
+ transition: opacity 0.4s;
635
+ }
636
+ #loading-overlay.hidden { opacity: 0; pointer-events: none; }
637
+ .loading-content { text-align: center; display: flex; flex-direction: column; align-items: center; gap: 16px; }
638
+ .loading-spinner {
639
+ width: 44px;
640
+ height: 44px;
641
+ border: 3px solid var(--border);
642
+ border-top-color: var(--accent);
643
+ border-radius: 50%;
644
+ animation: spin 0.8s linear infinite;
645
+ }
646
+ @keyframes spin { to { transform: rotate(360deg); } }
647
+ .loading-text { font-family: var(--ff-display); font-size: 14px; color: var(--text-secondary); }
648
+
649
+ /* ── Toast ── */
650
+ #toast {
651
+ position: fixed;
652
+ bottom: 20px;
653
+ left: 50%;
654
+ transform: translateX(-50%) translateY(20px);
655
+ background: var(--bg-elevated);
656
+ border: 1px solid var(--border);
657
+ padding: 10px 18px;
658
+ border-radius: 99px;
659
+ font-size: 13px;
660
+ color: var(--text-primary);
661
+ opacity: 0;
662
+ pointer-events: none;
663
+ transition: all 0.3s;
664
+ z-index: 9999;
665
+ white-space: nowrap;
666
+ box-shadow: var(--shadow);
667
+ }
668
+ #toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
669
+
670
+ /* ── Scrollbar for AI messages ── */
671
+ #ai-messages::-webkit-scrollbar { width: 4px; }
672
+
673
+ /* ── Progress bar ── */
674
+ #load-progress {
675
+ position: fixed;
676
+ top: 0;
677
+ left: 0;
678
+ height: 2px;
679
+ background: linear-gradient(90deg, var(--accent), var(--accent2));
680
+ width: 0%;
681
+ transition: width 0.3s;
682
+ z-index: 9998;
683
+ border-radius: 0 2px 2px 0;
684
+ }
685
+
686
+ /* ── Upload input ── */
687
+ #file-input { display: none; }
688
+
689
+ /* ── Highlight drop ── */
690
+ #viewer-panel.dragover #drop-zone { display: flex !important; background: var(--bg-base); z-index: 100; }
691
+
692
+ /* ── Settings Modal ── */
693
+ #settings-overlay {
694
+ position: fixed; inset: 0;
695
+ background: rgba(0,0,0,0.6);
696
+ display: flex; align-items: center; justify-content: center;
697
+ z-index: 2000;
698
+ opacity: 0; pointer-events: none;
699
+ transition: opacity 0.2s;
700
+ }
701
+ #settings-overlay.open { opacity: 1; pointer-events: all; }
702
+ #settings-modal {
703
+ background: var(--bg-panel);
704
+ border: 1px solid var(--border);
705
+ border-radius: var(--radius);
706
+ width: 560px;
707
+ max-width: calc(100vw - 32px);
708
+ max-height: calc(100vh - 64px);
709
+ overflow-y: auto;
710
+ box-shadow: var(--shadow-lg);
711
+ animation: modal-in 0.2s ease;
712
+ }
713
+ @keyframes modal-in { from { transform: scale(0.96) translateY(8px); } to { transform: none; } }
714
+ .settings-header {
715
+ padding: 18px 20px 14px;
716
+ border-bottom: 1px solid var(--border-subtle);
717
+ display: flex; align-items: center; justify-content: space-between;
718
+ }
719
+ .settings-header h2 {
720
+ font-family: var(--ff-display);
721
+ font-size: 16px; font-weight: 700;
722
+ display: flex; align-items: center; gap: 8px;
723
+ }
724
+ .settings-close {
725
+ background: none; border: none; color: var(--text-muted);
726
+ cursor: pointer; font-size: 20px; line-height: 1;
727
+ padding: 2px 6px; border-radius: 4px; transition: all var(--transition);
728
+ }
729
+ .settings-close:hover { color: var(--text-primary); background: var(--bg-card); }
730
+ .settings-tabs {
731
+ display: flex; border-bottom: 1px solid var(--border-subtle);
732
+ padding: 0 16px; gap: 4px;
733
+ }
734
+ .settings-tab {
735
+ padding: 10px 14px; font-size: 13px; font-weight: 500;
736
+ border: none; background: none; color: var(--text-secondary);
737
+ cursor: pointer; border-bottom: 2px solid transparent;
738
+ transition: all var(--transition); margin-bottom: -1px;
739
+ }
740
+ .settings-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
741
+ .settings-tab-body { display: none; padding: 20px; }
742
+ .settings-tab-body.active { display: block; }
743
+ .settings-section-title {
744
+ font-size: 11px; font-weight: 700; letter-spacing: 0.08em;
745
+ text-transform: uppercase; color: var(--text-muted);
746
+ margin-bottom: 12px;
747
+ }
748
+ .provider-card {
749
+ background: var(--bg-card);
750
+ border: 2px solid var(--border);
751
+ border-radius: var(--radius);
752
+ padding: 14px 16px;
753
+ margin-bottom: 10px;
754
+ cursor: pointer;
755
+ transition: all var(--transition);
756
+ display: flex; flex-direction: column; gap: 8px;
757
+ }
758
+ .provider-card:hover { border-color: var(--accent); }
759
+ .provider-card.selected { border-color: var(--accent); background: var(--accent-soft); }
760
+ .provider-card-top {
761
+ display: flex; align-items: center; gap: 10px;
762
+ }
763
+ .provider-card-icon { font-size: 22px; flex-shrink: 0; }
764
+ .provider-card-name { font-weight: 600; font-size: 14px; flex: 1; }
765
+ .provider-badge {
766
+ font-size: 10px; font-family: var(--ff-mono); font-weight: 600;
767
+ padding: 2px 8px; border-radius: 99px;
768
+ }
769
+ .provider-badge.local { background: #22c55e22; color: #4ade80; border: 1px solid #4ade8044; }
770
+ .provider-badge.cloud { background: var(--accent-soft); color: var(--accent); border: 1px solid var(--accent-glow); }
771
+ .provider-use-btn {
772
+ align-self: flex-end;
773
+ padding: 5px 14px; font-size: 12px; font-weight: 600;
774
+ background: var(--accent); color: #fff;
775
+ border: none; border-radius: var(--radius-sm);
776
+ cursor: pointer; transition: all var(--transition);
777
+ }
778
+ .provider-use-btn:hover { opacity: 0.85; }
779
+ .provider-card.selected .provider-use-btn { background: var(--success); }
780
+ .provider-field { display: flex; flex-direction: column; gap: 4px; }
781
+ .provider-field label { font-size: 11px; font-weight: 600; color: var(--text-secondary); }
782
+ .provider-field input {
783
+ background: var(--bg-input); border: 1px solid var(--border);
784
+ border-radius: var(--radius-sm); color: var(--text-primary);
785
+ font-family: var(--ff-mono); font-size: 12px;
786
+ padding: 6px 10px; outline: none; transition: border-color var(--transition);
787
+ width: 100%;
788
+ }
789
+ .provider-field input:focus { border-color: var(--accent); }
790
+ .provider-model-chips {
791
+ display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px;
792
+ }
793
+ .model-chip {
794
+ background: var(--bg-elevated); border: 1px solid var(--border);
795
+ color: var(--text-secondary); font-size: 11px; font-family: var(--ff-mono);
796
+ padding: 3px 10px; border-radius: 99px; cursor: pointer;
797
+ transition: all var(--transition);
798
+ }
799
+ .model-chip:hover, .model-chip.active { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
800
+ .fetch-models-btn {
801
+ background: var(--bg-elevated); border: 1px solid var(--border);
802
+ color: var(--text-secondary); font-size: 11px; padding: 4px 12px;
803
+ border-radius: var(--radius-sm); cursor: pointer; transition: all var(--transition);
804
+ }
805
+ .fetch-models-btn:hover { border-color: var(--accent); color: var(--accent); }
806
+ .settings-footer {
807
+ padding: 14px 20px;
808
+ border-top: 1px solid var(--border-subtle);
809
+ display: flex; justify-content: flex-end; gap: 8px;
810
+ }
811
+ .btn-secondary {
812
+ height: 32px; padding: 0 16px;
813
+ background: var(--bg-card); border: 1px solid var(--border);
814
+ border-radius: var(--radius-sm); font-size: 13px; font-weight: 500;
815
+ color: var(--text-secondary); cursor: pointer; transition: all var(--transition);
816
+ }
817
+ .btn-secondary:hover { border-color: var(--accent); color: var(--accent); }
818
+ </style>
819
+ </head>
820
+ <body>
821
+
822
+ <div id="loading-overlay">
823
+ <div class="loading-content">
824
+ <div class="loading-spinner"></div>
825
+ <div class="loading-text">Loading PDF Viewer…</div>
826
+ </div>
827
+ </div>
828
+
829
+ <div id="load-progress"></div>
830
+
831
+ <div id="app">
832
+ <!-- ── Header ── -->
833
+ <header id="header">
834
+ <div class="logo">✦ MC <span>PDF</span> Studio</div>
835
+ <div class="header-sep"></div>
836
+
837
+ <div class="file-info">
838
+ <span class="file-name" id="file-name-display">No file open</span>
839
+ <span class="file-badge" id="page-count-badge" style="display:none"></span>
840
+ </div>
841
+
842
+ <div class="header-controls">
843
+ <!-- Page nav -->
844
+ <div class="page-nav" id="page-nav" style="display:none">
845
+ <button class="btn-icon" id="prev-page" title="Previous page">‹</button>
846
+ <input type="number" class="page-input" id="page-input" value="1" min="1"/>
847
+ <span class="page-total" id="page-total">/ 0</span>
848
+ <button class="btn-icon" id="next-page" title="Next page">›</button>
849
+ </div>
850
+
851
+ <div class="header-sep" id="nav-sep" style="display:none"></div>
852
+
853
+ <!-- Zoom -->
854
+ <div class="zoom-control" id="zoom-control" style="display:none">
855
+ <button class="btn-icon" id="zoom-out" title="Zoom out">−</button>
856
+ <span class="zoom-val" id="zoom-val">100%</span>
857
+ <button class="btn-icon" id="zoom-in" title="Zoom in">+</button>
858
+ <button class="btn-icon" id="zoom-fit" title="Fit to width" style="font-size:12px">⊡</button>
859
+ </div>
860
+
861
+ <div class="header-sep"></div>
862
+
863
+ <!-- Upload -->
864
+ <button class="btn-icon" id="upload-btn" title="Open PDF">
865
+ <svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path d="M10 3a1 1 0 01.707.293l4 4a1 1 0 01-1.414 1.414L11 6.414V13a1 1 0 11-2 0V6.414L6.707 8.707A1 1 0 015.293 7.293l4-4A1 1 0 0110 3zM5 16a1 1 0 100 2h10a1 1 0 100-2H5z"/></svg>
866
+ </button>
867
+
868
+ <!-- Minimap toggle -->
869
+ <button class="btn-icon" id="minimap-btn" title="Toggle minimap">
870
+ <svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path d="M3 4a1 1 0 011-1h3a1 1 0 010 2H4a1 1 0 01-1-1zm0 5a1 1 0 011-1h3a1 1 0 010 2H4a1 1 0 01-1-1zm0 5a1 1 0 011-1h3a1 1 0 010 2H4a1 1 0 01-1-1zm5-10a1 1 0 011-1h6a1 1 0 010 2H9a1 1 0 01-1-1zm0 5a1 1 0 011-1h6a1 1 0 010 2H9a1 1 0 01-1-1zm0 5a1 1 0 011-1h6a1 1 0 010 2H9a1 1 0 01-1-1z"/></svg>
871
+ </button>
872
+
873
+ <!-- Theme toggle -->
874
+ <button class="btn-icon" id="theme-btn" title="Toggle theme">🌙</button>
875
+
876
+ <!-- Settings -->
877
+ <button class="btn-icon" id="settings-btn" title="AI Settings">⚙️</button>
878
+
879
+ <!-- AI Sidekick -->
880
+ <button class="btn-primary" id="ai-toggle-btn" style="display:none">
881
+ <span>✦</span> AI Sidekick
882
+ </button>
883
+ </div>
884
+ </header>
885
+
886
+ <!-- ── Main ── -->
887
+ <div id="main">
888
+
889
+ <!-- Minimap -->
890
+ <div id="minimap-panel">
891
+ <div class="panel-header">Pages</div>
892
+ <div id="minimap-scroll"></div>
893
+ </div>
894
+
895
+ <!-- PDF Viewer -->
896
+ <div id="viewer-panel">
897
+ <div id="pdf-scroll">
898
+ <div id="drop-zone">
899
+ <div class="drop-ring">📄</div>
900
+ <div class="drop-title">Open a PDF</div>
901
+ <div class="drop-sub">Drag & drop a file here</div>
902
+ <div class="drop-or">or</div>
903
+ <button class="btn-primary" id="drop-upload-btn">
904
+ <svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><path d="M10 3a1 1 0 01.707.293l4 4a1 1 0 01-1.414 1.414L11 6.414V13a1 1 0 11-2 0V6.414L6.707 8.707A1 1 0 015.293 7.293l4-4A1 1 0 0110 3z"/></svg>
905
+ Browse File
906
+ </button>
907
+ </div>
908
+ <div id="pages-container" style="display:none; flex-direction:column; align-items:center; gap:20px; width:100%;"></div>
909
+ </div>
910
+ </div>
911
+
912
+ <!-- AI Panel -->
913
+ <div id="ai-panel">
914
+ <div class="ai-header">
915
+ <div class="ai-icon">✦</div>
916
+ <div class="ai-title">
917
+ <h3>AI Sidekick</h3>
918
+ <p id="ai-file-ctx">No document loaded</p>
919
+ </div>
920
+ <button class="ai-close" id="ai-close">×</button>
921
+ </div>
922
+
923
+ <div id="ai-body"></div>
924
+ </div>
925
+
926
+ </div>
927
+ </div>
928
+
929
+ <div id="toast"></div>
930
+ <input type="file" id="file-input" accept=".pdf,application/pdf"/>
931
+
932
+ <!-- ── Settings Modal ── -->
933
+ <div id="settings-overlay">
934
+ <div id="settings-modal">
935
+ <div class="settings-header">
936
+ <h2>⚙️ Settings</h2>
937
+ <button class="settings-close" id="settings-close">×</button>
938
+ </div>
939
+ <div class="settings-tabs">
940
+ <button class="settings-tab active" data-tab="ai">🤖 AI</button>
941
+ <button class="settings-tab" data-tab="invites">🔗 Invites</button>
942
+ <button class="settings-tab" data-tab="plugins">🔌 File Plugins</button>
943
+ </div>
944
+
945
+ <!-- AI Tab -->
946
+ <div class="settings-tab-body active" id="tab-ai">
947
+ <p style="font-size:13px; color:var(--text-secondary); margin-bottom:16px;">
948
+ Choose an AI provider to enable <strong>AI Sidekick</strong> chat about your PDFs.
949
+ </p>
950
+
951
+ <!-- Ollama -->
952
+ <div class="provider-card" data-provider="ollama" id="card-ollama">
953
+ <div class="provider-card-top">
954
+ <div class="provider-card-icon">🦙</div>
955
+ <div class="provider-card-name">Ollama</div>
956
+ <span class="provider-badge local">LOCAL</span>
957
+ <button class="provider-use-btn" data-provider="ollama">USE OLLAMA</button>
958
+ </div>
959
+ <p style="font-size:12px; color:var(--text-muted)">Run models locally — no API key needed.</p>
960
+ <div class="provider-field">
961
+ <label>Base URL</label>
962
+ <div style="display:flex; gap:8px; align-items:center;">
963
+ <input type="text" id="ollama-url" placeholder="http://localhost:11434" value="http://localhost:11434"/>
964
+ <button class="fetch-models-btn" id="fetch-ollama-models">Fetch Models</button>
965
+ </div>
966
+ </div>
967
+ <div class="provider-field">
968
+ <label>Model</label>
969
+ <input type="text" id="ollama-model" placeholder="llama3.2:latest"/>
970
+ <div class="provider-model-chips" id="ollama-model-chips"></div>
971
+ </div>
972
+ </div>
973
+
974
+ <!-- Claude / Anthropic -->
975
+ <div class="provider-card" data-provider="anthropic" id="card-anthropic">
976
+ <div class="provider-card-top">
977
+ <div class="provider-card-icon">🔵</div>
978
+ <div class="provider-card-name">Claude by Anthropic</div>
979
+ <span class="provider-badge cloud">CLOUD</span>
980
+ <button class="provider-use-btn" data-provider="anthropic">USE CLAUDE</button>
981
+ </div>
982
+ <div class="provider-field" style="margin-top:4px;">
983
+ <label>Model</label>
984
+ <input type="text" id="anthropic-model" placeholder="claude-sonnet-4-20250514"/>
985
+ <div class="provider-model-chips">
986
+ <span class="model-chip" data-model="claude-opus-4-5" data-for="anthropic">claude-opus-4-5</span>
987
+ <span class="model-chip" data-model="claude-sonnet-4-20250514" data-for="anthropic">claude-sonnet-4</span>
988
+ <span class="model-chip" data-model="claude-haiku-4-5-20251001" data-for="anthropic">claude-haiku-4-5</span>
989
+ </div>
990
+ </div>
991
+ </div>
992
+
993
+ <!-- OpenAI -->
994
+ <div class="provider-card" data-provider="openai" id="card-openai">
995
+ <div class="provider-card-top">
996
+ <div class="provider-card-icon">🤖</div>
997
+ <div class="provider-card-name">OpenAI</div>
998
+ <span class="provider-badge cloud">CLOUD</span>
999
+ <button class="provider-use-btn" data-provider="openai">USE OPENAI</button>
1000
+ </div>
1001
+ <div class="provider-field" style="margin-top:4px;">
1002
+ <label>Model</label>
1003
+ <input type="text" id="openai-model" placeholder="gpt-4o"/>
1004
+ <div class="provider-model-chips">
1005
+ <span class="model-chip" data-model="gpt-4o" data-for="openai">gpt-4o</span>
1006
+ <span class="model-chip" data-model="gpt-4o-mini" data-for="openai">gpt-4o-mini</span>
1007
+ <span class="model-chip" data-model="o1" data-for="openai">o1</span>
1008
+ </div>
1009
+ </div>
1010
+ </div>
1011
+
1012
+ <!-- xAI -->
1013
+ <div class="provider-card" data-provider="xai" id="card-xai">
1014
+ <div class="provider-card-top">
1015
+ <div class="provider-card-icon">✦</div>
1016
+ <div class="provider-card-name">xAI (Grok)</div>
1017
+ <span class="provider-badge cloud">CLOUD</span>
1018
+ <button class="provider-use-btn" data-provider="xai">USE XAI</button>
1019
+ </div>
1020
+ <div class="provider-field" style="margin-top:4px;">
1021
+ <label>Model</label>
1022
+ <input type="text" id="xai-model" placeholder="grok-beta"/>
1023
+ <div class="provider-model-chips">
1024
+ <span class="model-chip" data-model="grok-beta" data-for="xai">grok-beta</span>
1025
+ <span class="model-chip" data-model="grok-2" data-for="xai">grok-2</span>
1026
+ </div>
1027
+ </div>
1028
+ </div>
1029
+ </div>
1030
+
1031
+ <!-- Invites Tab (placeholder) -->
1032
+ <div class="settings-tab-body" id="tab-invites">
1033
+ <p style="font-size:13px; color:var(--text-secondary);">Invite features coming soon.</p>
1034
+ </div>
1035
+
1036
+ <!-- Plugins Tab (placeholder) -->
1037
+ <div class="settings-tab-body" id="tab-plugins">
1038
+ <p style="font-size:13px; color:var(--text-secondary);">File plugin support coming soon.</p>
1039
+ </div>
1040
+
1041
+ <div class="settings-footer">
1042
+ <button class="btn-secondary" id="settings-close-btn">Close</button>
1043
+ </div>
1044
+ </div>
1045
+ </div>
1046
+
1047
+ <script>
1048
+ // ── State ──────────────────────────────────────────────────────────────────
1049
+ const state = {
1050
+ pdfDoc: null,
1051
+ pageCount: 0,
1052
+ currentPage: 1,
1053
+ zoom: 1.2,
1054
+ filePath: null,
1055
+ fileName: null,
1056
+ aiEnabled: false,
1057
+ minimapVisible: true,
1058
+ theme: 'dark',
1059
+ rendering: false,
1060
+ chatHistory: [],
1061
+ aiTyping: false,
1062
+ };
1063
+
1064
+ let pdfjsLib;
1065
+
1066
+ // ── Init ───────────────────────────────────────────────────────────────────
1067
+ window.addEventListener('DOMContentLoaded', async () => {
1068
+ // Setup PDF.js
1069
+ pdfjsLib = window['pdfjs-dist/build/pdf'];
1070
+ pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
1071
+
1072
+ // Load config from server
1073
+ const config = await fetch('/api/config').then(r => r.json());
1074
+ state.aiEnabled = config.enableAi;
1075
+ state.minimapVisible = config.minimap;
1076
+ state.theme = config.theme;
1077
+
1078
+ applyTheme(state.theme);
1079
+ setupDragDrop();
1080
+ setupControls();
1081
+ setupAI(config.enableAi);
1082
+ setupSettings(config);
1083
+ updateMinimapVisibility();
1084
+
1085
+ // Load initial file if provided
1086
+ if (config.initialFile) {
1087
+ loadPDFFromUrl('/api/initial-pdf', config.initialFileName);
1088
+ }
1089
+
1090
+ setTimeout(() => {
1091
+ document.getElementById('loading-overlay').classList.add('hidden');
1092
+ }, 400);
1093
+ });
1094
+
1095
+ // ── Theme ──────────────────────────────────────────────────────────────────
1096
+ function applyTheme(theme) {
1097
+ document.documentElement.setAttribute('data-theme', theme);
1098
+ document.getElementById('theme-btn').textContent = theme === 'dark' ? '☀️' : '🌙';
1099
+ state.theme = theme;
1100
+ }
1101
+
1102
+ // ── Drag & Drop ────────────────────────────────────────────────────────────
1103
+ function setupDragDrop() {
1104
+ const vp = document.getElementById('viewer-panel');
1105
+ const dz = document.getElementById('drop-zone');
1106
+
1107
+ ['dragenter','dragover'].forEach(ev => {
1108
+ vp.addEventListener(ev, e => { e.preventDefault(); dz.classList.add('drag-over'); });
1109
+ });
1110
+ ['dragleave','drop'].forEach(ev => {
1111
+ vp.addEventListener(ev, () => dz.classList.remove('drag-over'));
1112
+ });
1113
+ vp.addEventListener('drop', e => {
1114
+ e.preventDefault();
1115
+ const file = e.dataTransfer.files[0];
1116
+ if (file?.type === 'application/pdf' || file?.name.endsWith('.pdf')) uploadFile(file);
1117
+ else toast('Please drop a PDF file');
1118
+ });
1119
+ }
1120
+
1121
+ // ── Controls ───────────────────────────────────────────────────────────────
1122
+ function setupControls() {
1123
+ const fi = document.getElementById('file-input');
1124
+ document.getElementById('upload-btn').onclick = () => fi.click();
1125
+ document.getElementById('drop-upload-btn').onclick = () => fi.click();
1126
+ fi.onchange = e => { if (e.target.files[0]) uploadFile(e.target.files[0]); fi.value = ''; };
1127
+
1128
+ document.getElementById('prev-page').onclick = () => goToPage(state.currentPage - 1);
1129
+ document.getElementById('next-page').onclick = () => goToPage(state.currentPage + 1);
1130
+ document.getElementById('page-input').onchange = e => goToPage(parseInt(e.target.value));
1131
+ document.getElementById('page-input').onkeydown = e => { if (e.key === 'Enter') e.target.blur(); };
1132
+
1133
+ document.getElementById('zoom-in').onclick = () => setZoom(state.zoom + 0.15);
1134
+ document.getElementById('zoom-out').onclick = () => setZoom(state.zoom - 0.15);
1135
+ document.getElementById('zoom-fit').onclick = fitWidth;
1136
+
1137
+ document.getElementById('minimap-btn').onclick = () => {
1138
+ state.minimapVisible = !state.minimapVisible;
1139
+ updateMinimapVisibility();
1140
+ document.getElementById('minimap-btn').classList.toggle('active', state.minimapVisible);
1141
+ };
1142
+ document.getElementById('minimap-btn').classList.toggle('active', state.minimapVisible);
1143
+
1144
+ document.getElementById('theme-btn').onclick = () => {
1145
+ applyTheme(state.theme === 'dark' ? 'light' : 'dark');
1146
+ };
1147
+
1148
+ document.getElementById('ai-toggle-btn').onclick = () => openAI();
1149
+ document.getElementById('ai-close').onclick = () => {
1150
+ document.getElementById('ai-panel').classList.remove('open');
1151
+ document.getElementById('ai-toggle-btn').classList.remove('active');
1152
+ };
1153
+
1154
+ // Scroll tracking for current page indicator
1155
+ document.getElementById('pdf-scroll').addEventListener('scroll', debounce(updateCurrentPageFromScroll, 100));
1156
+
1157
+ // Keyboard shortcuts
1158
+ document.addEventListener('keydown', e => {
1159
+ if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') return;
1160
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') goToPage(state.currentPage + 1);
1161
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') goToPage(state.currentPage - 1);
1162
+ if (e.key === '+' || e.key === '=') setZoom(state.zoom + 0.1);
1163
+ if (e.key === '-') setZoom(state.zoom - 0.1);
1164
+ if (e.key === '0') fitWidth();
1165
+ });
1166
+ }
1167
+
1168
+ // ── Upload File ────────────────────────────────────────────────────────────
1169
+ async function uploadFile(file) {
1170
+ toast('Uploading…');
1171
+ const formData = new FormData();
1172
+ formData.append('pdf', file);
1173
+ try {
1174
+ const res = await fetch('/api/upload', { method: 'POST', body: formData });
1175
+ const data = await res.json();
1176
+ if (data.error) throw new Error(data.error);
1177
+ state.filePath = data.filePath;
1178
+ loadPDFFromUrl(`/api/pdf?path=${encodeURIComponent(data.filePath)}`, data.fileName);
1179
+ } catch (err) {
1180
+ toast('Error: ' + err.message, 'error');
1181
+ }
1182
+ }
1183
+
1184
+ // ── Load PDF ───────────────────────────────────────────────────────────────
1185
+ async function loadPDFFromUrl(url, fileName) {
1186
+ showProgress(10);
1187
+ try {
1188
+ const res = await fetch(url);
1189
+ if (!res.ok) throw new Error('Failed to load PDF');
1190
+ const buf = await res.arrayBuffer();
1191
+ showProgress(40);
1192
+
1193
+ state.pdfDoc = await pdfjsLib.getDocument({ data: buf }).promise;
1194
+ state.pageCount = state.pdfDoc.numPages;
1195
+ state.currentPage = 1;
1196
+ state.fileName = fileName || 'document.pdf';
1197
+
1198
+ showProgress(70);
1199
+ updateHeaderInfo();
1200
+ await renderAllPages();
1201
+ renderMinimap();
1202
+ showProgress(100);
1203
+ setTimeout(() => setProgress(0), 600);
1204
+ toast(`Loaded "${state.fileName}"`);
1205
+
1206
+ // Update AI context
1207
+ document.getElementById('ai-file-ctx').textContent = `${state.fileName} • ${state.pageCount} pages`;
1208
+
1209
+ } catch (err) {
1210
+ toast('Error loading PDF: ' + err.message, 'error');
1211
+ setProgress(0);
1212
+ }
1213
+ }
1214
+
1215
+ // ── Render Pages ───────────────────────────────────────────────────────────
1216
+ async function renderAllPages() {
1217
+ const container = document.getElementById('pages-container');
1218
+ const dropZone = document.getElementById('drop-zone');
1219
+ container.innerHTML = '';
1220
+ container.style.display = 'flex';
1221
+ dropZone.classList.add('hidden');
1222
+
1223
+ const viewerWidth = document.getElementById('pdf-scroll').clientWidth - 48;
1224
+
1225
+ for (let i = 1; i <= state.pageCount; i++) {
1226
+ const page = await state.pdfDoc.getPage(i);
1227
+ const vp = page.getViewport({ scale: state.zoom });
1228
+
1229
+ // Fit to viewer width if needed
1230
+ let scale = state.zoom;
1231
+ if (vp.width > viewerWidth) {
1232
+ scale = state.zoom * (viewerWidth / vp.width);
1233
+ }
1234
+
1235
+ const viewport = page.getViewport({ scale });
1236
+ const wrapper = document.createElement('div');
1237
+ wrapper.className = 'pdf-page-wrapper';
1238
+ wrapper.dataset.page = i;
1239
+
1240
+ const canvas = document.createElement('canvas');
1241
+ canvas.className = 'pdf-page-canvas';
1242
+ canvas.width = viewport.width;
1243
+ canvas.height = viewport.height;
1244
+
1245
+ const label = document.createElement('div');
1246
+ label.className = 'page-number-label';
1247
+ label.textContent = `Page ${i}`;
1248
+
1249
+ wrapper.appendChild(canvas);
1250
+ wrapper.appendChild(label);
1251
+ container.appendChild(wrapper);
1252
+
1253
+ page.render({ canvasContext: canvas.getContext('2d'), viewport });
1254
+ showProgress(70 + Math.round((i / state.pageCount) * 28));
1255
+ }
1256
+ }
1257
+
1258
+ // ── Minimap ────────────────────────────────────────────────────────────────
1259
+ async function renderMinimap() {
1260
+ const scroll = document.getElementById('minimap-scroll');
1261
+ scroll.innerHTML = '';
1262
+ if (!state.pdfDoc) return;
1263
+
1264
+ const miniWidth = 160;
1265
+
1266
+ for (let i = 1; i <= state.pageCount; i++) {
1267
+ const page = await state.pdfDoc.getPage(i);
1268
+ const nativeVp = page.getViewport({ scale: 1 });
1269
+ const scale = miniWidth / nativeVp.width;
1270
+ const viewport = page.getViewport({ scale });
1271
+
1272
+ const div = document.createElement('div');
1273
+ div.className = 'mini-page';
1274
+ div.dataset.page = i;
1275
+ if (i === state.currentPage) div.classList.add('active');
1276
+
1277
+ const canvas = document.createElement('canvas');
1278
+ canvas.width = viewport.width;
1279
+ canvas.height = viewport.height;
1280
+
1281
+ const lbl = document.createElement('span');
1282
+ lbl.className = 'mini-label';
1283
+ lbl.textContent = i;
1284
+
1285
+ div.appendChild(canvas);
1286
+ div.appendChild(lbl);
1287
+ scroll.appendChild(div);
1288
+
1289
+ div.onclick = () => goToPage(i);
1290
+ page.render({ canvasContext: canvas.getContext('2d'), viewport });
1291
+ }
1292
+ }
1293
+
1294
+ function updateActiveMini(page) {
1295
+ document.querySelectorAll('.mini-page').forEach(el => {
1296
+ el.classList.toggle('active', parseInt(el.dataset.page) === page);
1297
+ });
1298
+ const activeMini = document.querySelector(`.mini-page[data-page="${page}"]`);
1299
+ if (activeMini) activeMini.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1300
+ }
1301
+
1302
+ // ── Navigation ─────────────────────────────────────────────────────────────
1303
+ function goToPage(n) {
1304
+ if (!state.pdfDoc) return;
1305
+ n = Math.max(1, Math.min(state.pageCount, n));
1306
+ state.currentPage = n;
1307
+ document.getElementById('page-input').value = n;
1308
+ updateActiveMini(n);
1309
+
1310
+ const wrapper = document.querySelector(`.pdf-page-wrapper[data-page="${n}"]`);
1311
+ if (wrapper) wrapper.scrollIntoView({ behavior: 'smooth', block: 'start' });
1312
+ }
1313
+
1314
+ function updateCurrentPageFromScroll() {
1315
+ const wrappers = document.querySelectorAll('.pdf-page-wrapper');
1316
+ const scroll = document.getElementById('pdf-scroll');
1317
+ const scrollTop = scroll.scrollTop;
1318
+ const scrollMid = scrollTop + scroll.clientHeight / 2;
1319
+
1320
+ let closest = 1;
1321
+ let minDist = Infinity;
1322
+ wrappers.forEach(w => {
1323
+ const mid = w.offsetTop + w.offsetHeight / 2;
1324
+ const dist = Math.abs(mid - scrollMid);
1325
+ if (dist < minDist) { minDist = dist; closest = parseInt(w.dataset.page); }
1326
+ });
1327
+
1328
+ if (closest !== state.currentPage) {
1329
+ state.currentPage = closest;
1330
+ document.getElementById('page-input').value = closest;
1331
+ updateActiveMini(closest);
1332
+ }
1333
+ }
1334
+
1335
+ // ── Zoom ───────────────────────────────────────────────────────────────────
1336
+ function setZoom(z) {
1337
+ state.zoom = Math.max(0.3, Math.min(4, z));
1338
+ document.getElementById('zoom-val').textContent = Math.round(state.zoom * 100) + '%';
1339
+ if (state.pdfDoc) renderAllPages();
1340
+ }
1341
+
1342
+ function fitWidth() {
1343
+ const viewerWidth = document.getElementById('pdf-scroll').clientWidth - 48;
1344
+ if (!state.pdfDoc) return;
1345
+ state.pdfDoc.getPage(1).then(page => {
1346
+ const vp = page.getViewport({ scale: 1 });
1347
+ setZoom(viewerWidth / vp.width);
1348
+ });
1349
+ }
1350
+
1351
+ // ── Header Info ────────────────────────────────────────────────────────────
1352
+ function updateHeaderInfo() {
1353
+ document.getElementById('file-name-display').textContent = state.fileName;
1354
+ const badge = document.getElementById('page-count-badge');
1355
+ badge.textContent = `${state.pageCount} pages`;
1356
+ badge.style.display = 'inline-flex';
1357
+
1358
+ const show = el => el.style.display = 'flex';
1359
+ show(document.getElementById('page-nav'));
1360
+ show(document.getElementById('zoom-control'));
1361
+ document.getElementById('nav-sep').style.display = 'block';
1362
+ document.getElementById('page-total').textContent = `/ ${state.pageCount}`;
1363
+ document.getElementById('page-input').max = state.pageCount;
1364
+ document.getElementById('zoom-val').textContent = Math.round(state.zoom * 100) + '%';
1365
+ }
1366
+
1367
+ function updateMinimapVisibility() {
1368
+ document.getElementById('minimap-panel').classList.toggle('hidden', !state.minimapVisible);
1369
+ }
1370
+
1371
+ // ── AI Sidekick ────────────────────────────────────────────────────────────
1372
+ function setupAI(enabled) {
1373
+ if (enabled) {
1374
+ document.getElementById('ai-toggle-btn').style.display = 'flex';
1375
+ buildAIChat();
1376
+ } else {
1377
+ buildAIDisabled();
1378
+ }
1379
+ }
1380
+
1381
+ function buildAIDisabled() {
1382
+ const aiToggle = document.getElementById('ai-toggle-btn');
1383
+ aiToggle.style.display = 'flex';
1384
+ aiToggle.style.opacity = '0.45';
1385
+ aiToggle.title = 'AI disabled — configure in Settings';
1386
+ aiToggle.onclick = () => { openAI(); };
1387
+
1388
+ document.getElementById('ai-body').innerHTML = `
1389
+ <div class="ai-disabled">
1390
+ <div class="icon">🤖</div>
1391
+ <p>AI Sidekick is disabled.<br/>Open <strong>Settings ⚙️</strong> to choose a provider (Ollama, Claude, OpenAI, xAI) and activate it.</p>
1392
+ <p style="margin-top:8px; font-size:12px; color: var(--text-muted)">Or restart with <code>--enable-ai</code> to enable from the CLI.</p>
1393
+ <button class="btn-primary" style="margin-top:12px;" onclick="closeAI(); openSettings();">Open Settings</button>
1394
+ </div>`;
1395
+ }
1396
+
1397
+ function closeAI() {
1398
+ document.getElementById('ai-panel').classList.remove('open');
1399
+ document.getElementById('ai-toggle-btn').classList.remove('active');
1400
+ }
1401
+
1402
+ function buildAIChat() {
1403
+ const body = document.getElementById('ai-body');
1404
+ body.style.display = 'contents';
1405
+ body.innerHTML = `
1406
+ <div class="ai-quick-wrap" id="ai-quick-wrap">
1407
+ <button class="ai-quick-chip" data-q="Summarize this document">📋 Summarize</button>
1408
+ <button class="ai-quick-chip" data-q="What are the key points?">🎯 Key points</button>
1409
+ <button class="ai-quick-chip" data-q="What is this document about?">❓ Overview</button>
1410
+ <button class="ai-quick-chip" data-q="Extract any important dates or numbers">🔢 Numbers & dates</button>
1411
+ <button class="ai-quick-chip" data-q="What are the main conclusions?">✅ Conclusions</button>
1412
+ </div>
1413
+ <div id="ai-messages">
1414
+ <div class="msg assistant">
1415
+ <div class="msg-avatar">✦</div>
1416
+ <div class="msg-body">
1417
+ <div class="msg-bubble">Hi! I'm your AI Sidekick. Ask me anything about the open PDF — I can summarize, answer questions, extract information, or explain content.</div>
1418
+ <div class="msg-time">Just now</div>
1419
+ </div>
1420
+ </div>
1421
+ </div>
1422
+ <div class="ai-input-wrap">
1423
+ <textarea id="ai-input" placeholder="Ask about this PDF…" rows="1"></textarea>
1424
+ <button class="ai-send" id="ai-send">
1425
+ <svg viewBox="0 0 20 20"><path d="M2.94 17.94l16-8a1 1 0 000-1.88l-16-8a1 1 0 00-1.36 1.21l2 5a1 1 0 00.9.63h5.32a1 1 0 010 2H4.58a1 1 0 00-.9.63l-2 5a1 1 0 001.26 1.21z"/></svg>
1426
+ </button>
1427
+ </div>`;
1428
+
1429
+ // Quick prompts
1430
+ document.querySelectorAll('.ai-quick-chip').forEach(chip => {
1431
+ chip.onclick = () => sendAIMessage(chip.dataset.q);
1432
+ });
1433
+
1434
+ // Send on click
1435
+ document.getElementById('ai-send').onclick = () => {
1436
+ const val = document.getElementById('ai-input').value.trim();
1437
+ if (val) sendAIMessage(val);
1438
+ };
1439
+
1440
+ // Send on Enter (Shift+Enter = newline)
1441
+ document.getElementById('ai-input').addEventListener('keydown', e => {
1442
+ if (e.key === 'Enter' && !e.shiftKey) {
1443
+ e.preventDefault();
1444
+ const val = e.target.value.trim();
1445
+ if (val) sendAIMessage(val);
1446
+ }
1447
+ });
1448
+
1449
+ // Auto-resize textarea
1450
+ document.getElementById('ai-input').addEventListener('input', e => {
1451
+ e.target.style.height = 'auto';
1452
+ e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px';
1453
+ });
1454
+ }
1455
+
1456
+ function openAI() {
1457
+ document.getElementById('ai-panel').classList.add('open');
1458
+ document.getElementById('ai-toggle-btn').classList.add('active');
1459
+ if (state.aiEnabled) setTimeout(() => document.getElementById('ai-input')?.focus(), 300);
1460
+ }
1461
+
1462
+ async function sendAIMessage(text) {
1463
+ if (state.aiTyping) return;
1464
+ const input = document.getElementById('ai-input');
1465
+ if (input) { input.value = ''; input.style.height = 'auto'; }
1466
+
1467
+ appendMessage('user', text);
1468
+ state.chatHistory.push({ role: 'user', content: text });
1469
+
1470
+ state.aiTyping = true;
1471
+ document.getElementById('ai-send').disabled = true;
1472
+
1473
+ // Show typing
1474
+ const typingId = 'typing-' + Date.now();
1475
+ const typingEl = document.createElement('div');
1476
+ typingEl.id = typingId;
1477
+ typingEl.className = 'msg assistant';
1478
+ typingEl.innerHTML = `
1479
+ <div class="msg-avatar">✦</div>
1480
+ <div class="msg-body">
1481
+ <div class="msg-bubble" style="padding:2px 4px">
1482
+ <div class="typing-dots"><span></span><span></span><span></span></div>
1483
+ </div>
1484
+ </div>`;
1485
+ document.getElementById('ai-messages').appendChild(typingEl);
1486
+ scrollAI();
1487
+
1488
+ try {
1489
+ const res = await fetch('/api/ai/chat', {
1490
+ method: 'POST',
1491
+ headers: { 'Content-Type': 'application/json' },
1492
+ body: JSON.stringify({
1493
+ message: text,
1494
+ history: state.chatHistory.slice(-10),
1495
+ filePath: state.filePath,
1496
+ currentPage: state.currentPage,
1497
+ }),
1498
+ });
1499
+ const data = await res.json();
1500
+ typingEl.remove();
1501
+
1502
+ if (data.error) throw new Error(data.error);
1503
+ appendMessage('assistant', data.reply);
1504
+ state.chatHistory.push({ role: 'assistant', content: data.reply });
1505
+ } catch(err) {
1506
+ typingEl.remove();
1507
+ appendMessage('assistant', `Sorry, I ran into an error: ${err.message}`);
1508
+ } finally {
1509
+ state.aiTyping = false;
1510
+ const sendBtn = document.getElementById('ai-send');
1511
+ if (sendBtn) sendBtn.disabled = false;
1512
+ }
1513
+ }
1514
+
1515
+ function appendMessage(role, content) {
1516
+ const msgs = document.getElementById('ai-messages');
1517
+ const div = document.createElement('div');
1518
+ div.className = `msg ${role}`;
1519
+
1520
+ const avatar = role === 'assistant' ? '✦' : '👤';
1521
+ const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1522
+
1523
+ const rendered = role === 'assistant' ? marked.parse(content) : escHtml(content);
1524
+
1525
+ div.innerHTML = `
1526
+ <div class="msg-avatar">${avatar}</div>
1527
+ <div class="msg-body">
1528
+ <div class="msg-bubble">${rendered}</div>
1529
+ <div class="msg-time">${time}</div>
1530
+ </div>`;
1531
+
1532
+ msgs.appendChild(div);
1533
+ scrollAI();
1534
+ }
1535
+
1536
+ function scrollAI() {
1537
+ const msgs = document.getElementById('ai-messages');
1538
+ if (msgs) msgs.scrollTop = msgs.scrollHeight;
1539
+ }
1540
+
1541
+ // ── Progress ───────────────────────────────────────────────────────────────
1542
+ function showProgress(pct) {
1543
+ document.getElementById('load-progress').style.width = pct + '%';
1544
+ }
1545
+ function setProgress(pct) {
1546
+ document.getElementById('load-progress').style.width = pct + '%';
1547
+ }
1548
+
1549
+ // ── Toast ──────────────────────────────────────────────────────────────────
1550
+ let toastTimer;
1551
+ function toast(msg) {
1552
+ const el = document.getElementById('toast');
1553
+ el.textContent = msg;
1554
+ el.classList.add('show');
1555
+ clearTimeout(toastTimer);
1556
+ toastTimer = setTimeout(() => el.classList.remove('show'), 2800);
1557
+ }
1558
+
1559
+ // ── Settings Modal ─────────────────────────────────────────────────────────
1560
+ const settingsState = {
1561
+ provider: 'anthropic',
1562
+ model: '',
1563
+ baseUrl: '',
1564
+ };
1565
+
1566
+ function openSettings() {
1567
+ document.getElementById('settings-overlay').classList.add('open');
1568
+ }
1569
+ function closeSettings() {
1570
+ document.getElementById('settings-overlay').classList.remove('open');
1571
+ }
1572
+
1573
+ function setupSettings(config) {
1574
+ settingsState.provider = config.aiProvider || 'anthropic';
1575
+ settingsState.model = config.aiModel || '';
1576
+ settingsState.baseUrl = config.aiBaseUrl || '';
1577
+
1578
+ // Pre-fill fields
1579
+ if (settingsState.baseUrl) document.getElementById('ollama-url').value = settingsState.baseUrl;
1580
+ if (settingsState.model) {
1581
+ const modelInput = document.getElementById(`${settingsState.provider}-model`);
1582
+ if (modelInput) modelInput.value = settingsState.model;
1583
+ }
1584
+
1585
+ highlightSelectedProvider();
1586
+
1587
+ // Open/close
1588
+ document.getElementById('settings-btn').onclick = openSettings;
1589
+ document.getElementById('settings-close').onclick = closeSettings;
1590
+ document.getElementById('settings-close-btn').onclick = closeSettings;
1591
+ document.getElementById('settings-overlay').onclick = e => {
1592
+ if (e.target === document.getElementById('settings-overlay')) closeSettings();
1593
+ };
1594
+
1595
+ // Tab switching
1596
+ document.querySelectorAll('.settings-tab').forEach(tab => {
1597
+ tab.onclick = () => {
1598
+ document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
1599
+ document.querySelectorAll('.settings-tab-body').forEach(b => b.classList.remove('active'));
1600
+ tab.classList.add('active');
1601
+ document.getElementById(`tab-${tab.dataset.tab}`).classList.add('active');
1602
+ };
1603
+ });
1604
+
1605
+ // Provider use buttons
1606
+ document.querySelectorAll('.provider-use-btn').forEach(btn => {
1607
+ btn.onclick = async (e) => {
1608
+ e.stopPropagation();
1609
+ const prov = btn.dataset.provider;
1610
+ settingsState.provider = prov;
1611
+ const modelInput = document.getElementById(`${prov}-model`);
1612
+ settingsState.model = modelInput ? modelInput.value.trim() : '';
1613
+ settingsState.baseUrl = prov === 'ollama'
1614
+ ? document.getElementById('ollama-url').value.trim()
1615
+ : '';
1616
+ await applyAISettings(true);
1617
+ highlightSelectedProvider();
1618
+ toast(`Using ${prov}${settingsState.model ? ' / ' + settingsState.model : ''}`);
1619
+ };
1620
+ });
1621
+
1622
+ // Model chips
1623
+ document.querySelectorAll('.model-chip').forEach(chip => {
1624
+ chip.onclick = () => {
1625
+ const prov = chip.dataset.for;
1626
+ const model = chip.dataset.model;
1627
+ const input = document.getElementById(`${prov}-model`);
1628
+ if (input) input.value = model;
1629
+ document.querySelectorAll(`.model-chip[data-for="${prov}"]`).forEach(c => c.classList.remove('active'));
1630
+ chip.classList.add('active');
1631
+ };
1632
+ });
1633
+
1634
+ // Fetch Ollama models
1635
+ document.getElementById('fetch-ollama-models').onclick = async () => {
1636
+ const baseUrl = document.getElementById('ollama-url').value.trim() || 'http://localhost:11434';
1637
+ try {
1638
+ const res = await fetch(`${baseUrl}/api/tags`);
1639
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1640
+ const data = await res.json();
1641
+ const chips = document.getElementById('ollama-model-chips');
1642
+ chips.innerHTML = '';
1643
+ (data.models || []).forEach(m => {
1644
+ const chip = document.createElement('span');
1645
+ chip.className = 'model-chip';
1646
+ chip.dataset.model = m.name;
1647
+ chip.dataset.for = 'ollama';
1648
+ chip.textContent = m.name;
1649
+ chip.onclick = () => {
1650
+ document.getElementById('ollama-model').value = m.name;
1651
+ chips.querySelectorAll('.model-chip').forEach(c => c.classList.remove('active'));
1652
+ chip.classList.add('active');
1653
+ };
1654
+ chips.appendChild(chip);
1655
+ });
1656
+ toast(`Found ${data.models?.length || 0} Ollama models`);
1657
+ } catch (err) {
1658
+ toast('Could not reach Ollama: ' + err.message);
1659
+ }
1660
+ };
1661
+ }
1662
+
1663
+ function highlightSelectedProvider() {
1664
+ document.querySelectorAll('.provider-card').forEach(card => {
1665
+ card.classList.toggle('selected', card.dataset.provider === settingsState.provider);
1666
+ const btn = card.querySelector('.provider-use-btn');
1667
+ if (btn) btn.textContent = card.dataset.provider === settingsState.provider ? '✓ ACTIVE' : `USE ${card.dataset.provider.toUpperCase()}`;
1668
+ });
1669
+ }
1670
+
1671
+ async function applyAISettings(enable) {
1672
+ await fetch('/api/ai/settings', {
1673
+ method: 'POST',
1674
+ headers: { 'Content-Type': 'application/json' },
1675
+ body: JSON.stringify({
1676
+ provider: settingsState.provider,
1677
+ model: settingsState.model || null,
1678
+ baseUrl: settingsState.baseUrl || null,
1679
+ enabled: enable,
1680
+ }),
1681
+ });
1682
+ state.aiEnabled = enable;
1683
+ if (enable) {
1684
+ document.getElementById('ai-toggle-btn').style.display = 'flex';
1685
+ document.getElementById('ai-toggle-btn').style.opacity = '1';
1686
+ buildAIChat();
1687
+ }
1688
+ }
1689
+
1690
+ // ── Utils ──────────────────────────────────────────────────────────────────
1691
+ function debounce(fn, ms) {
1692
+ let t;
1693
+ return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
1694
+ }
1695
+ function escHtml(s) {
1696
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
1697
+ }
1698
+ </script>
1699
+ </body>
1700
+ </html>