nodebb-plugin-pdf-secure2 1.4.2 → 1.5.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.
@@ -7,6 +7,10 @@
7
7
  content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
8
8
  <title>PDF Viewer</title>
9
9
 
10
+ <!-- KaTeX (math rendering) -->
11
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css" crossorigin="anonymous">
12
+ <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js" crossorigin="anonymous"></script>
13
+
10
14
  <!-- PDF.js -->
11
15
  <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js" integrity="sha384-/1qUCSGwTur9vjf/z9lmu/eCUYbpOTgSjmpbMQZ1/CtX2v/WcAIKqRv+U1DUCG6e" crossorigin="anonymous"></script>
12
16
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css" integrity="sha384-OPLBIVpTWn5IeFhdZcMhX+XMJ4ZMKDN6ykKH+ZxgPvlpLnTW8qrpBePnYNjyeBQM" crossorigin="anonymous">
@@ -749,6 +753,110 @@
749
753
  padding: 0;
750
754
  }
751
755
 
756
+ /* Markdown tables in chat */
757
+ .chatMsg .tableWrap {
758
+ overflow-x: auto;
759
+ margin: 6px 0;
760
+ }
761
+ .chatMsg table {
762
+ width: 100%;
763
+ border-collapse: collapse;
764
+ font-size: 12px;
765
+ }
766
+ .chatMsg th, .chatMsg td {
767
+ border: 1px solid rgba(255,255,255,0.15);
768
+ padding: 4px 8px;
769
+ text-align: left;
770
+ }
771
+ .chatMsg th {
772
+ background: rgba(255,255,255,0.08);
773
+ font-weight: 600;
774
+ }
775
+ .chatMsg tbody tr:nth-child(even) {
776
+ background: rgba(255,255,255,0.03);
777
+ }
778
+
779
+ /* Markdown lists in chat */
780
+ .chatMsg ul, .chatMsg ol {
781
+ margin: 4px 0;
782
+ padding-left: 20px;
783
+ }
784
+ .chatMsg li {
785
+ margin: 2px 0;
786
+ }
787
+
788
+ /* Markdown headers in chat */
789
+ .chatMsg h3, .chatMsg h4, .chatMsg h5 {
790
+ margin: 8px 0 4px;
791
+ font-weight: 600;
792
+ line-height: 1.3;
793
+ }
794
+ .chatMsg h3 { font-size: 15px; }
795
+ .chatMsg h4 { font-size: 14px; }
796
+ .chatMsg h5 { font-size: 13px; }
797
+
798
+ /* Markdown HR in chat */
799
+ .chatMsg hr {
800
+ border: none;
801
+ border-top: 1px solid rgba(255,255,255,0.15);
802
+ margin: 8px 0;
803
+ }
804
+
805
+ /* Copy button for code blocks and tables */
806
+ .chatMsg pre, .chatMsg .tableWrap {
807
+ position: relative;
808
+ }
809
+ .chatCopyBtn {
810
+ position: absolute;
811
+ top: 4px;
812
+ right: 4px;
813
+ background: rgba(255,255,255,0.1);
814
+ border: 1px solid rgba(255,255,255,0.15);
815
+ border-radius: 4px;
816
+ color: var(--text-secondary);
817
+ font-size: 10px;
818
+ padding: 2px 6px;
819
+ cursor: pointer;
820
+ opacity: 0;
821
+ transition: opacity 0.15s;
822
+ z-index: 1;
823
+ display: flex;
824
+ align-items: center;
825
+ gap: 3px;
826
+ }
827
+ .chatMsg pre:hover .chatCopyBtn,
828
+ .chatMsg .tableWrap:hover .chatCopyBtn {
829
+ opacity: 1;
830
+ }
831
+ .chatCopyBtn:hover {
832
+ background: rgba(255,255,255,0.2);
833
+ color: var(--text-primary);
834
+ }
835
+ .chatCopyBtn.copied {
836
+ color: #4ade80;
837
+ opacity: 1;
838
+ }
839
+
840
+ /* LaTeX block math */
841
+ .chatMsg .katex-block {
842
+ overflow-x: auto;
843
+ margin: 6px 0;
844
+ padding: 4px 0;
845
+ }
846
+ .chatMsg .katex-inline {
847
+ display: inline;
848
+ }
849
+
850
+ /* VIP tip chips clickable */
851
+ .vipTipsBanner .vipTip {
852
+ cursor: pointer;
853
+ transition: background 0.15s, color 0.15s;
854
+ }
855
+ .vipTipsBanner .vipTip:hover {
856
+ background: rgba(255,215,0,0.22);
857
+ color: #ffd700;
858
+ }
859
+
752
860
  /* AI badge on AI messages */
753
861
  .chatMsg.ai .aiBadge {
754
862
  display: flex;
@@ -780,6 +888,84 @@
780
888
  border: 1px solid rgba(220, 38, 38, 0.25);
781
889
  }
782
890
 
891
+ /* AI message action buttons (copy, regenerate) */
892
+ .chatMsgActions {
893
+ display: flex;
894
+ gap: 8px;
895
+ margin-top: 6px;
896
+ opacity: 0;
897
+ transition: opacity 0.15s;
898
+ }
899
+ .chatMsg.ai:hover .chatMsgActions { opacity: 1; }
900
+ .chatMsgAction {
901
+ background: none;
902
+ border: none;
903
+ color: var(--text-secondary);
904
+ font-size: 11px;
905
+ cursor: pointer;
906
+ display: flex;
907
+ align-items: center;
908
+ gap: 3px;
909
+ padding: 2px 4px;
910
+ border-radius: 4px;
911
+ transition: color 0.15s, background 0.15s;
912
+ }
913
+ .chatMsgAction:hover { color: var(--text-primary); background: rgba(255,255,255,0.08); }
914
+ .chatMsgAction.copied { color: #4ade80; }
915
+ .chatMsgAction svg { width: 12px; height: 12px; fill: currentColor; }
916
+
917
+ /* AI badge timestamp */
918
+ .aiBadgeTime { font-weight: 400; opacity: 0.6; }
919
+
920
+ /* Scroll to bottom button */
921
+ .chatScrollDown {
922
+ position: absolute;
923
+ bottom: 80px;
924
+ left: 50%;
925
+ transform: translateX(-50%);
926
+ width: 32px; height: 32px;
927
+ border-radius: 50%;
928
+ background: var(--bg-secondary);
929
+ border: 1px solid var(--border-color);
930
+ color: var(--text-primary);
931
+ font-size: 16px;
932
+ cursor: pointer;
933
+ opacity: 0;
934
+ pointer-events: none;
935
+ transition: opacity 0.2s;
936
+ z-index: 5;
937
+ display: flex;
938
+ align-items: center;
939
+ justify-content: center;
940
+ }
941
+ .chatScrollDown:hover { background: var(--bg-tertiary); }
942
+ .chatScrollDown.visible { opacity: 1; pointer-events: auto; }
943
+
944
+ /* Blockquote in AI messages */
945
+ .chatMsg blockquote {
946
+ border-left: 3px solid var(--accent);
947
+ margin: 6px 0;
948
+ padding: 4px 10px;
949
+ color: var(--text-secondary);
950
+ font-style: italic;
951
+ background: rgba(255,255,255,0.03);
952
+ border-radius: 0 4px 4px 0;
953
+ }
954
+ body[data-tier="vip"] .chatMsg blockquote { border-left-color: #ffd700; }
955
+
956
+ /* Table overflow scroll hint */
957
+ .chatMsg .tableWrap.scrollable::after {
958
+ content: '';
959
+ position: absolute;
960
+ top: 0; right: 0; bottom: 0;
961
+ width: 20px;
962
+ background: linear-gradient(to right, transparent, var(--bg-tertiary));
963
+ pointer-events: none;
964
+ }
965
+
966
+ /* Loading elapsed timer */
967
+ .chatLoadingElapsed { font-size: 10px; color: var(--text-secondary); opacity: 0.5; margin-left: 4px; }
968
+
783
969
  /* Injection warning banner */
784
970
  .chatInjectionWarning {
785
971
  background: rgba(255, 170, 0, 0.12);
@@ -862,6 +1048,64 @@
862
1048
  body[data-tier="vip"] .chatSuggestionChip:hover { border-color: #ffd700; color: #ffd700; background: rgba(255, 215, 0, 0.12); }
863
1049
  body[data-tier="vip"] .chatSummaryBtn:hover { color: #ffd700; border-color: #ffd700; }
864
1050
 
1051
+ /* VIP feature tips banner */
1052
+ .vipTipsBanner {
1053
+ display: none;
1054
+ padding: 6px 12px;
1055
+ background: linear-gradient(135deg, rgba(255,215,0,0.08), rgba(255,170,0,0.05));
1056
+ border-bottom: 1px solid rgba(255,215,0,0.15);
1057
+ font-size: 11px;
1058
+ color: var(--text-secondary);
1059
+ line-height: 1.4;
1060
+ }
1061
+ .vipTipsBanner .vipTipsLabel {
1062
+ color: #ffd700;
1063
+ font-weight: 600;
1064
+ font-size: 10px;
1065
+ letter-spacing: 0.3px;
1066
+ margin-bottom: 2px;
1067
+ }
1068
+ .vipTipsBanner .vipTipsItems {
1069
+ display: flex;
1070
+ flex-wrap: wrap;
1071
+ gap: 4px;
1072
+ margin-top: 4px;
1073
+ }
1074
+ .vipTipsBanner .vipTip {
1075
+ background: rgba(255,215,0,0.1);
1076
+ border: 1px solid rgba(255,215,0,0.15);
1077
+ border-radius: 10px;
1078
+ padding: 2px 8px;
1079
+ font-size: 10px;
1080
+ color: rgba(255,215,0,0.8);
1081
+ white-space: nowrap;
1082
+ }
1083
+ body[data-tier="vip"] .vipTipsBanner { display: block; }
1084
+
1085
+ /* Detail mode toggle */
1086
+ .vipTipsRow { display: flex; justify-content: space-between; align-items: center; }
1087
+ .detailToggle { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
1088
+ .detailToggle input { display: none; }
1089
+ .detailToggleSlider {
1090
+ width: 28px; height: 16px;
1091
+ background: rgba(255,255,255,0.15);
1092
+ border-radius: 8px;
1093
+ position: relative;
1094
+ transition: background 0.2s;
1095
+ }
1096
+ .detailToggleSlider::after {
1097
+ content: '';
1098
+ position: absolute; left: 2px; top: 2px;
1099
+ width: 12px; height: 12px;
1100
+ background: var(--text-secondary);
1101
+ border-radius: 50%;
1102
+ transition: transform 0.2s, background 0.2s;
1103
+ }
1104
+ .detailToggle input:checked + .detailToggleSlider { background: rgba(255,215,0,0.3); }
1105
+ .detailToggle input:checked + .detailToggleSlider::after { transform: translateX(12px); background: #ffd700; }
1106
+ .detailToggleText { font-size: 10px; color: var(--text-secondary); font-weight: 500; transition: color 0.2s; }
1107
+ .detailToggle input:checked ~ .detailToggleText { color: #ffd700; }
1108
+
865
1109
  /* Suggestion chips */
866
1110
  .chatSuggestions {
867
1111
  display: flex;
@@ -2868,7 +3112,25 @@
2868
3112
  </button>
2869
3113
  <button class="closeBtn" id="closeChatSidebar">&times;</button>
2870
3114
  </div>
3115
+ <div class="vipTipsBanner" id="vipTipsBanner">
3116
+ <div class="vipTipsRow">
3117
+ <div class="vipTipsLabel">VIP — Benden isteyebileceklerin:</div>
3118
+ <label class="detailToggle" id="detailToggleLabel">
3119
+ <input type="checkbox" id="detailToggle">
3120
+ <span class="detailToggleSlider"></span>
3121
+ <span class="detailToggleText">Detaylı</span>
3122
+ </label>
3123
+ </div>
3124
+ <div class="vipTipsItems">
3125
+ <span class="vipTip">detayli aciklama</span>
3126
+ <span class="vipTip">ornek ver</span>
3127
+ <span class="vipTip">sinav sorusu</span>
3128
+ <span class="vipTip">karsilastirma tablosu</span>
3129
+ <span class="vipTip">ezber teknigi</span>
3130
+ </div>
3131
+ </div>
2871
3132
  <div id="chatMessages"></div>
3133
+ <button id="chatScrollDown" class="chatScrollDown">&#x2193;</button>
2872
3134
  <div class="chatCharCount" id="chatCharCount"></div>
2873
3135
  <div class="chatQuotaBar" id="chatQuotaBar">
2874
3136
  <div class="chatQuotaTrack"><div class="chatQuotaFill" id="chatQuotaFill"></div></div>
@@ -2937,6 +3199,18 @@
2937
3199
  return originalToBlob.apply(this, arguments);
2938
3200
  };
2939
3201
 
3202
+ // Block getImageData for PDF page canvases (prevents raw pixel extraction)
3203
+ const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
3204
+ CanvasRenderingContext2D.prototype.getImageData = function () {
3205
+ if (this.canvas && this.canvas.closest && this.canvas.closest('.page') && this.canvas.closest('#viewerContainer')) {
3206
+ // Return blank ImageData of requested size
3207
+ var w = arguments[2] || 1;
3208
+ var h = arguments[3] || 1;
3209
+ return new ImageData(w, h);
3210
+ }
3211
+ return originalGetImageData.apply(this, arguments);
3212
+ };
3213
+
2940
3214
  pdfjsLib.GlobalWorkerOptions.workerSrc = '';
2941
3215
 
2942
3216
  // State - now private, not accessible from console
@@ -3499,38 +3773,9 @@
3499
3773
  }
3500
3774
 
3501
3775
  try {
3502
- // ============================================
3503
- // SPA CACHE - Check if parent has cached buffer
3504
- // ============================================
3505
3776
  let pdfBuffer = null;
3506
3777
 
3507
- if (window.parent && window.parent !== window) {
3508
- // Request cached buffer from parent
3509
- const cachePromise = new Promise((resolve) => {
3510
- const handler = (event) => {
3511
- if (event.data && event.data.type === 'pdf-secure-cache-response' && event.data.filename === config.filename) {
3512
- window.removeEventListener('message', handler);
3513
- resolve(event.data.buffer);
3514
- }
3515
- };
3516
- window.addEventListener('message', handler);
3517
-
3518
- // Timeout after 100ms
3519
- setTimeout(() => {
3520
- window.removeEventListener('message', handler);
3521
- resolve(null);
3522
- }, 100);
3523
-
3524
- window.parent.postMessage({ type: 'pdf-secure-cache-request', filename: config.filename }, window.location.origin);
3525
- });
3526
-
3527
- pdfBuffer = await cachePromise;
3528
- if (pdfBuffer) {
3529
- }
3530
- }
3531
-
3532
- // If no cache, fetch from server
3533
- if (!pdfBuffer) {
3778
+ {
3534
3779
  // Nonce and key are embedded in HTML config (not fetched from API)
3535
3780
  const nonce = config.nonce;
3536
3781
  const decryptKey = config.dk;
@@ -3546,23 +3791,12 @@
3546
3791
 
3547
3792
  const encodedBuffer = await pdfRes.arrayBuffer();
3548
3793
 
3549
- // Decrypt AES-256-GCM encrypted data
3550
- if (decryptKey && decryptIv) {
3551
- pdfBuffer = await aesGcmDecode(encodedBuffer, decryptKey, decryptIv);
3552
- } else {
3553
- pdfBuffer = encodedBuffer;
3794
+ // Decrypt AES-256-GCM encrypted data (refuse to load without encryption)
3795
+ if (!decryptKey || !decryptIv) {
3796
+ throw new Error('Missing encryption parameters');
3554
3797
  }
3798
+ pdfBuffer = await aesGcmDecode(encodedBuffer, decryptKey, decryptIv);
3555
3799
 
3556
- // Send buffer to parent for caching (premium only - non-premium must not leak decoded buffer)
3557
- if ((_cfg.isPremium !== false || _cfg.isLite) && window.parent && window.parent !== window) {
3558
- // Clone buffer for parent (we keep original)
3559
- const bufferCopy = pdfBuffer.slice(0);
3560
- window.parent.postMessage({
3561
- type: 'pdf-secure-buffer',
3562
- filename: config.filename,
3563
- buffer: bufferCopy
3564
- }, window.location.origin, [bufferCopy]); // Transferable
3565
- }
3566
3800
  }
3567
3801
 
3568
3802
 
@@ -3606,6 +3840,18 @@
3606
3840
  };
3607
3841
  }
3608
3842
 
3843
+ // Security: Wipe decryption keys from config to prevent memory inspection
3844
+ if (config) {
3845
+ config.dk = null;
3846
+ config.iv = null;
3847
+ config.nonce = null;
3848
+ }
3849
+ if (_cfg) {
3850
+ _cfg.dk = null;
3851
+ _cfg.iv = null;
3852
+ _cfg.nonce = null;
3853
+ }
3854
+
3609
3855
 
3610
3856
  } catch (err) {
3611
3857
 
@@ -3810,6 +4056,17 @@
3810
4056
  const chatIsPremium = _cfg && _cfg.isPremium;
3811
4057
  const chatIsVip = _cfg && _cfg.isVip;
3812
4058
 
4059
+ // Detail mode toggle (VIP only)
4060
+ var chatDetailMode = false;
4061
+ var detailToggleEl = document.getElementById('detailToggle');
4062
+ if (chatIsVip && detailToggleEl) {
4063
+ try { chatDetailMode = localStorage.getItem('pdfChat_detailMode') === 'true'; detailToggleEl.checked = chatDetailMode; } catch (e) { /* localStorage unavailable */ }
4064
+ detailToggleEl.addEventListener('change', function () {
4065
+ chatDetailMode = detailToggleEl.checked;
4066
+ try { localStorage.setItem('pdfChat_detailMode', String(chatDetailMode)); } catch (e) { /* ignore */ }
4067
+ });
4068
+ }
4069
+
3813
4070
  // Show chat button if chatEnabled (visible to all users, not just premium)
3814
4071
  if (_cfg && _cfg.chatEnabled) {
3815
4072
  chatBtnEl.style.display = '';
@@ -3901,6 +4158,37 @@
3901
4158
 
3902
4159
  chatBtnEl.onclick = toggleChat;
3903
4160
 
4161
+ // Scroll-to-bottom button
4162
+ var chatScrollDownBtn = document.getElementById('chatScrollDown');
4163
+ if (chatScrollDownBtn) {
4164
+ chatMessagesEl.addEventListener('scroll', function () {
4165
+ var atBottom = chatMessagesEl.scrollHeight - chatMessagesEl.scrollTop - chatMessagesEl.clientHeight < 60;
4166
+ chatScrollDownBtn.classList.toggle('visible', !atBottom);
4167
+ });
4168
+ chatScrollDownBtn.onclick = function () {
4169
+ chatMessagesEl.scrollTo({ top: chatMessagesEl.scrollHeight, behavior: 'smooth' });
4170
+ };
4171
+ }
4172
+
4173
+ // VIP tip chips — click to populate chat input
4174
+ var vipTipChips = document.querySelectorAll('.vipTip');
4175
+ var vipTipPrompts = {
4176
+ 'detayli aciklama': 'Bu konuyu detayli aciklar misin?',
4177
+ 'ornek ver': 'Bununla ilgili somut bir ornek verir misin?',
4178
+ 'sinav sorusu': 'Bu konudan cikmis veya cikabilecek sinav sorulari neler?',
4179
+ 'karsilastirma tablosu': 'Bu konudaki kavramlari karsilastirma tablosu ile goster',
4180
+ 'ezber teknigi': 'Bu konuyu kolayca ezberlemek icin teknik onerir misin?',
4181
+ };
4182
+ vipTipChips.forEach(function (chip) {
4183
+ chip.onclick = function () {
4184
+ var prompt = vipTipPrompts[chip.textContent.trim()];
4185
+ if (prompt && chatInputEl) {
4186
+ chatInputEl.value = prompt;
4187
+ sendChatMessage();
4188
+ }
4189
+ };
4190
+ });
4191
+
3904
4192
  closeChatBtn.onclick = () => {
3905
4193
  chatSidebarEl.classList.remove('open');
3906
4194
  chatBtnEl.classList.remove('active');
@@ -3917,12 +4205,19 @@
3917
4205
 
3918
4206
  var frag = document.createDocumentFragment();
3919
4207
 
3920
- // Split into code blocks and normal text
3921
- var parts = text.split(/(```[\s\S]*?```)/g);
4208
+ // Split into code blocks, LaTeX blocks ($$...$$), and normal text
4209
+ var parts = text.split(/(```[\s\S]*?```|\$\$[\s\S]*?\$\$)/g);
3922
4210
  for (var pi = 0; pi < parts.length; pi++) {
3923
4211
  var part = parts[pi];
3924
4212
  if (!part) continue;
3925
4213
 
4214
+ // LaTeX block: $$...$$
4215
+ if (part.startsWith('$$') && part.endsWith('$$') && part.length > 4) {
4216
+ var latex = part.slice(2, -2).trim();
4217
+ frag.appendChild(renderMath(latex, true));
4218
+ continue;
4219
+ }
4220
+
3926
4221
  // Code block
3927
4222
  if (part.startsWith('```') && part.endsWith('```')) {
3928
4223
  var codeContent = part.slice(3, -3);
@@ -3935,31 +4230,267 @@
3935
4230
  var code = document.createElement('code');
3936
4231
  code.textContent = codeContent.trim();
3937
4232
  pre.appendChild(code);
4233
+ pre.appendChild(createCopyBtn(codeContent.trim()));
3938
4234
  frag.appendChild(pre);
3939
4235
  continue;
3940
4236
  }
3941
4237
 
3942
- // Normal text — process inline formatting
3943
- renderInlineMarkdown(part, frag);
4238
+ // Normal text — process block and inline formatting
4239
+ renderBlocks(part, frag);
3944
4240
  }
3945
4241
  return frag;
3946
4242
  }
3947
4243
 
3948
- function renderInlineMarkdown(text, parent) {
3949
- // Split by lines first
4244
+ // Block-level markdown: tables, lists, headers, HR
4245
+ // Falls back to inline formatting for regular text
4246
+ function renderBlocks(text, parent) {
3950
4247
  var lines = text.split('\n');
3951
- for (var li = 0; li < lines.length; li++) {
3952
- if (li > 0) parent.appendChild(document.createElement('br'));
3953
- var line = lines[li];
3954
- if (!line) continue;
3955
-
3956
- // Tokenize: inline code, bold, italic, page citations
3957
- // Process left-to-right with a simple state machine
3958
- var tokens = tokenizeInline(line);
3959
- for (var ti = 0; ti < tokens.length; ti++) {
3960
- parent.appendChild(tokens[ti]);
4248
+ var i = 0;
4249
+ var paraBuf = [];
4250
+ var hasOutput = false; // track if we've rendered anything yet
4251
+
4252
+ function flushPara() {
4253
+ if (paraBuf.length === 0) return;
4254
+ if (hasOutput) parent.appendChild(document.createElement('br'));
4255
+ for (var pi = 0; pi < paraBuf.length; pi++) {
4256
+ if (pi > 0) parent.appendChild(document.createElement('br'));
4257
+ var tokens = tokenizeInline(paraBuf[pi]);
4258
+ for (var t = 0; t < tokens.length; t++) {
4259
+ parent.appendChild(tokens[t]);
4260
+ }
3961
4261
  }
4262
+ paraBuf = [];
4263
+ hasOutput = true;
3962
4264
  }
4265
+
4266
+ while (i < lines.length) {
4267
+ var trimmed = lines[i].trim();
4268
+
4269
+ // Empty line — flush paragraph buffer
4270
+ if (!trimmed) { flushPara(); i++; continue; }
4271
+
4272
+ // Horizontal rule: --- or *** or ___
4273
+ if (/^[-*_]{3,}\s*$/.test(trimmed) && !/\S/.test(trimmed.replace(/[-*_]/g, ''))) {
4274
+ flushPara();
4275
+ parent.appendChild(document.createElement('hr'));
4276
+ hasOutput = true;
4277
+ i++; continue;
4278
+ }
4279
+
4280
+ // Header: # ## ### ####
4281
+ var hMatch = trimmed.match(/^(#{1,4})\s+(.+)/);
4282
+ if (hMatch) {
4283
+ flushPara();
4284
+ var hEl = document.createElement('h' + Math.min(hMatch[1].length + 2, 5));
4285
+ var hTokens = tokenizeInline(hMatch[2]);
4286
+ for (var t = 0; t < hTokens.length; t++) hEl.appendChild(hTokens[t]);
4287
+ parent.appendChild(hEl);
4288
+ hasOutput = true;
4289
+ i++; continue;
4290
+ }
4291
+
4292
+ // Blockquote: > text
4293
+ if (trimmed.startsWith('> ')) {
4294
+ flushPara();
4295
+ var bq = document.createElement('blockquote');
4296
+ var bqTokens = tokenizeInline(trimmed.slice(2));
4297
+ for (var t = 0; t < bqTokens.length; t++) bq.appendChild(bqTokens[t]);
4298
+ parent.appendChild(bq);
4299
+ hasOutput = true;
4300
+ i++; continue;
4301
+ }
4302
+
4303
+ // Table: lines containing | with a separator row
4304
+ if (trimmed.includes('|') && i + 1 < lines.length) {
4305
+ var tLines = [];
4306
+ var j = i;
4307
+ while (j < lines.length && lines[j].trim().includes('|')) {
4308
+ tLines.push(lines[j].trim());
4309
+ j++;
4310
+ }
4311
+ if (tLines.length >= 2 && /^\|?[\s:|-]+\|?$/.test(tLines[1]) && tLines[1].includes('-')) {
4312
+ flushPara();
4313
+ var table = renderTable(tLines);
4314
+ if (table) {
4315
+ var wrap = document.createElement('div');
4316
+ wrap.className = 'tableWrap';
4317
+ wrap.appendChild(table);
4318
+ wrap.appendChild(createCopyBtn(tableToText(table)));
4319
+ parent.appendChild(wrap);
4320
+ // Detect horizontal overflow for scroll hint
4321
+ requestAnimationFrame(function () {
4322
+ if (wrap.scrollWidth > wrap.clientWidth) wrap.classList.add('scrollable');
4323
+ });
4324
+ wrap.addEventListener('scroll', function () {
4325
+ var atEnd = wrap.scrollLeft + wrap.clientWidth >= wrap.scrollWidth - 5;
4326
+ wrap.classList.toggle('scrollable', !atEnd);
4327
+ });
4328
+ hasOutput = true;
4329
+ i = j; continue;
4330
+ }
4331
+ }
4332
+ }
4333
+
4334
+ // Unordered list: - item or * item (but not ---)
4335
+ if (/^[-*]\s+/.test(trimmed)) {
4336
+ flushPara();
4337
+ var ul = document.createElement('ul');
4338
+ while (i < lines.length) {
4339
+ var lt = lines[i].trim();
4340
+ var lm = lt.match(/^[-*]\s+(.*)/);
4341
+ if (lm) {
4342
+ var li2 = document.createElement('li');
4343
+ var liTokens = tokenizeInline(lm[1]);
4344
+ for (var t = 0; t < liTokens.length; t++) li2.appendChild(liTokens[t]);
4345
+ ul.appendChild(li2);
4346
+ i++;
4347
+ } else if (!lt) {
4348
+ i++;
4349
+ if (i < lines.length && /^[-*]\s+/.test(lines[i].trim())) continue;
4350
+ break;
4351
+ } else { break; }
4352
+ }
4353
+ parent.appendChild(ul);
4354
+ hasOutput = true;
4355
+ continue;
4356
+ }
4357
+
4358
+ // Ordered list: 1. item or 1) item
4359
+ if (/^\d+[.)]\s+/.test(trimmed)) {
4360
+ flushPara();
4361
+ var ol = document.createElement('ol');
4362
+ while (i < lines.length) {
4363
+ var lt = lines[i].trim();
4364
+ var om = lt.match(/^\d+[.)]\s+(.*)/);
4365
+ if (om) {
4366
+ var li3 = document.createElement('li');
4367
+ var oTokens = tokenizeInline(om[1]);
4368
+ for (var t = 0; t < oTokens.length; t++) li3.appendChild(oTokens[t]);
4369
+ ol.appendChild(li3);
4370
+ i++;
4371
+ } else if (!lt) {
4372
+ i++;
4373
+ if (i < lines.length && /^\d+[.)]\s+/.test(lines[i].trim())) continue;
4374
+ break;
4375
+ } else { break; }
4376
+ }
4377
+ parent.appendChild(ol);
4378
+ hasOutput = true;
4379
+ continue;
4380
+ }
4381
+
4382
+ // Regular text line — buffer for paragraph
4383
+ paraBuf.push(trimmed);
4384
+ i++;
4385
+ }
4386
+ flushPara();
4387
+ }
4388
+
4389
+ function renderTable(tLines) {
4390
+ var headerCells = parseTableRow(tLines[0]);
4391
+ if (!headerCells || headerCells.length === 0) return null;
4392
+
4393
+ // Parse alignments from separator row
4394
+ var sepCells = parseTableRow(tLines[1]);
4395
+ var aligns = sepCells ? sepCells.map(function (c) {
4396
+ c = c.trim();
4397
+ if (c.charAt(0) === ':' && c.charAt(c.length - 1) === ':') return 'center';
4398
+ if (c.charAt(c.length - 1) === ':') return 'right';
4399
+ return '';
4400
+ }) : [];
4401
+
4402
+ var table = document.createElement('table');
4403
+ var thead = document.createElement('thead');
4404
+ var hRow = document.createElement('tr');
4405
+ for (var c = 0; c < headerCells.length; c++) {
4406
+ var th = document.createElement('th');
4407
+ if (aligns[c]) th.style.textAlign = aligns[c];
4408
+ var tokens = tokenizeInline(headerCells[c].trim());
4409
+ for (var t = 0; t < tokens.length; t++) th.appendChild(tokens[t]);
4410
+ hRow.appendChild(th);
4411
+ }
4412
+ thead.appendChild(hRow);
4413
+ table.appendChild(thead);
4414
+
4415
+ var tbody = document.createElement('tbody');
4416
+ for (var r = 2; r < tLines.length; r++) {
4417
+ var cells = parseTableRow(tLines[r]);
4418
+ if (!cells) continue;
4419
+ var tr = document.createElement('tr');
4420
+ for (var c = 0; c < cells.length; c++) {
4421
+ var td = document.createElement('td');
4422
+ if (aligns[c]) td.style.textAlign = aligns[c];
4423
+ var tokens = tokenizeInline((cells[c] || '').trim());
4424
+ for (var t = 0; t < tokens.length; t++) td.appendChild(tokens[t]);
4425
+ tr.appendChild(td);
4426
+ }
4427
+ tbody.appendChild(tr);
4428
+ }
4429
+ table.appendChild(tbody);
4430
+ return table;
4431
+ }
4432
+
4433
+ function parseTableRow(line) {
4434
+ line = line.trim();
4435
+ if (line.charAt(0) === '|') line = line.slice(1);
4436
+ if (line.charAt(line.length - 1) === '|') line = line.slice(0, -1);
4437
+ if (!line) return null;
4438
+ return line.split('|');
4439
+ }
4440
+
4441
+ // Copy button helper (used for code blocks and tables)
4442
+ function createCopyBtn(textToCopy) {
4443
+ var btn = document.createElement('button');
4444
+ btn.className = 'chatCopyBtn';
4445
+ btn.textContent = 'Kopyala';
4446
+ btn.onclick = function (e) {
4447
+ e.stopPropagation();
4448
+ navigator.clipboard.writeText(textToCopy).then(function () {
4449
+ btn.textContent = 'Kopyalandi!';
4450
+ btn.classList.add('copied');
4451
+ setTimeout(function () {
4452
+ btn.textContent = 'Kopyala';
4453
+ btn.classList.remove('copied');
4454
+ }, 1500);
4455
+ }).catch(function () {
4456
+ btn.textContent = 'Hata!';
4457
+ setTimeout(function () { btn.textContent = 'Kopyala'; }, 1500);
4458
+ });
4459
+ };
4460
+ return btn;
4461
+ }
4462
+
4463
+ // Convert table element to tab-separated text for clipboard
4464
+ function tableToText(table) {
4465
+ var lines = [];
4466
+ var rows = table.querySelectorAll('tr');
4467
+ for (var r = 0; r < rows.length; r++) {
4468
+ var cells = rows[r].querySelectorAll('th, td');
4469
+ var line = [];
4470
+ for (var c = 0; c < cells.length; c++) {
4471
+ line.push(cells[c].textContent.trim());
4472
+ }
4473
+ lines.push(line.join('\t'));
4474
+ }
4475
+ return lines.join('\n');
4476
+ }
4477
+
4478
+ // Render LaTeX math expression (inline or block)
4479
+ function renderMath(latex, displayMode) {
4480
+ if (typeof katex === 'undefined') {
4481
+ // KaTeX not loaded, show raw LaTeX
4482
+ var fallback = document.createElement('code');
4483
+ fallback.textContent = (displayMode ? '$$' : '$') + latex + (displayMode ? '$$' : '$');
4484
+ return fallback;
4485
+ }
4486
+ var container = document.createElement('span');
4487
+ container.className = displayMode ? 'katex-block' : 'katex-inline';
4488
+ try {
4489
+ katex.render(latex, container, { displayMode: displayMode, throwOnError: false });
4490
+ } catch (e) {
4491
+ container.textContent = latex;
4492
+ }
4493
+ return container;
3963
4494
  }
3964
4495
 
3965
4496
  function tokenizeInline(line) {
@@ -4000,6 +4531,26 @@
4000
4531
  }
4001
4532
 
4002
4533
  while (i < line.length) {
4534
+ // Block math: $$...$$
4535
+ if (line[i] === '$' && line[i + 1] === '$') {
4536
+ var endM = line.indexOf('$$', i + 2);
4537
+ if (endM !== -1) {
4538
+ flushBuf();
4539
+ nodes.push(renderMath(line.slice(i + 2, endM), true));
4540
+ i = endM + 2;
4541
+ continue;
4542
+ }
4543
+ }
4544
+ // Inline math: $...$ (but not $$)
4545
+ if (line[i] === '$' && line[i + 1] !== '$') {
4546
+ var endM2 = line.indexOf('$', i + 1);
4547
+ if (endM2 !== -1 && endM2 > i + 1) {
4548
+ flushBuf();
4549
+ nodes.push(renderMath(line.slice(i + 1, endM2), false));
4550
+ i = endM2 + 1;
4551
+ continue;
4552
+ }
4553
+ }
4003
4554
  // Inline code: `...`
4004
4555
  if (line[i] === '`') {
4005
4556
  var end = line.indexOf('`', i + 1);
@@ -4036,6 +4587,18 @@
4036
4587
  continue;
4037
4588
  }
4038
4589
  }
4590
+ // Strikethrough: ~~...~~
4591
+ if (line[i] === '~' && line[i + 1] === '~') {
4592
+ var endS = line.indexOf('~~', i + 2);
4593
+ if (endS !== -1 && endS - i < 5002) {
4594
+ flushBuf();
4595
+ var del = document.createElement('del');
4596
+ del.textContent = line.slice(i + 2, endS);
4597
+ nodes.push(del);
4598
+ i = endS + 2;
4599
+ continue;
4600
+ }
4601
+ }
4039
4602
  buf += line[i];
4040
4603
  i++;
4041
4604
  }
@@ -4057,6 +4620,11 @@
4057
4620
  svg.appendChild(path);
4058
4621
  badge.appendChild(svg);
4059
4622
  badge.appendChild(document.createTextNode(' AI'));
4623
+ var ts = document.createElement('span');
4624
+ ts.className = 'aiBadgeTime';
4625
+ var now = new Date();
4626
+ ts.textContent = ' \u2022 ' + now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
4627
+ badge.appendChild(ts);
4060
4628
  return badge;
4061
4629
  }
4062
4630
 
@@ -4072,6 +4640,32 @@
4072
4640
  }
4073
4641
  msg.appendChild(createAiBadge());
4074
4642
  msg.appendChild(renderMarkdownSafe(text));
4643
+
4644
+ // Action buttons (regenerate)
4645
+ var actions = document.createElement('div');
4646
+ actions.className = 'chatMsgActions';
4647
+
4648
+ var regenBtn = document.createElement('button');
4649
+ regenBtn.className = 'chatMsgAction';
4650
+ regenBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> Tekrar sor';
4651
+ regenBtn.onclick = function () {
4652
+ if (chatHistory.length >= 2) {
4653
+ var lastQ = chatHistory[chatHistory.length - 2];
4654
+ if (lastQ.role === 'user') {
4655
+ chatHistory.pop();
4656
+ chatHistory.pop();
4657
+ var msgs = chatMessagesEl.querySelectorAll('.chatMsg');
4658
+ if (msgs.length >= 2) {
4659
+ msgs[msgs.length - 1].remove();
4660
+ msgs[msgs.length - 2].remove();
4661
+ }
4662
+ chatInputEl.value = lastQ.text;
4663
+ sendChatMessage();
4664
+ }
4665
+ }
4666
+ };
4667
+ actions.appendChild(regenBtn);
4668
+ msg.appendChild(actions);
4075
4669
  } else if (role === 'user') {
4076
4670
  var now = new Date();
4077
4671
  var timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
@@ -4089,18 +4683,34 @@
4089
4683
  }
4090
4684
 
4091
4685
  function showChatLoading() {
4092
- const msg = document.createElement('div');
4686
+ var loadText = chatDetailMode ? 'Detayl\u0131 analiz haz\u0131rlan\u0131yor' : 'D\u00fc\u015f\u00fcn\u00fcyor';
4687
+ var msg = document.createElement('div');
4093
4688
  msg.className = 'chatMsg ai chatLoading';
4094
4689
  msg.id = 'chatLoadingMsg';
4095
- msg.innerHTML = '<span class="chatLoadingText">Düşünüyor</span><span class="chatLoadingDots"><span>.</span><span>.</span><span>.</span></span>';
4690
+ msg.innerHTML = '<span class="chatLoadingText">' + loadText + '</span><span class="chatLoadingDots"><span>.</span><span>.</span><span>.</span></span>';
4691
+ var elapsed = document.createElement('span');
4692
+ elapsed.className = 'chatLoadingElapsed';
4693
+ elapsed.style.display = 'none';
4694
+ msg.appendChild(elapsed);
4695
+ var startTime = Date.now();
4696
+ msg._timer = setInterval(function () {
4697
+ var secs = Math.floor((Date.now() - startTime) / 1000);
4698
+ if (secs >= 5) {
4699
+ elapsed.style.display = '';
4700
+ elapsed.textContent = '(' + secs + 's)';
4701
+ }
4702
+ }, 1000);
4096
4703
  chatMessagesEl.appendChild(msg);
4097
4704
  chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
4098
4705
  return msg;
4099
4706
  }
4100
4707
 
4101
4708
  function removeChatLoading() {
4102
- const el = document.getElementById('chatLoadingMsg');
4103
- if (el) el.remove();
4709
+ var el = document.getElementById('chatLoadingMsg');
4710
+ if (el) {
4711
+ if (el._timer) clearInterval(el._timer);
4712
+ el.remove();
4713
+ }
4104
4714
  }
4105
4715
 
4106
4716
  // Quota usage bar
@@ -4172,7 +4782,7 @@
4172
4782
  var existingChips = suggestions.querySelectorAll('.chatSuggestionChip');
4173
4783
  existingChips.forEach(function (c) { c.classList.add('loading'); });
4174
4784
 
4175
- fetch((_cfg.relativePath || '') + '/api/v3/plugins/pdf-secure/suggestions?filename=' + encodeURIComponent(_cfg.filename), {
4785
+ fetch((_cfg.relativePath || '') + '/api/v3/plugins/pdf-secure/suggestions?filename=' + encodeURIComponent(_cfg.filename) + '&tid=' + encodeURIComponent(_cfg.tid || 0), {
4176
4786
  headers: { 'x-csrf-token': _cfg.csrfToken || '' },
4177
4787
  })
4178
4788
  .then(function (r) { return r.json(); })
@@ -4211,9 +4821,14 @@
4211
4821
  suggestions.appendChild(chip);
4212
4822
  });
4213
4823
 
4214
- // Trigger AI suggestions on hover/focus of the suggestions area (lazy)
4215
- suggestions.addEventListener('mouseenter', loadAiSuggestions, { once: true });
4216
- suggestions.addEventListener('focusin', loadAiSuggestions, { once: true });
4824
+ // VIP: auto-load context-aware suggestions immediately
4825
+ // Premium: lazy-load on hover/focus (avoids unnecessary API calls)
4826
+ if (chatIsVip) {
4827
+ setTimeout(loadAiSuggestions, 0);
4828
+ } else {
4829
+ suggestions.addEventListener('mouseenter', loadAiSuggestions, { once: true });
4830
+ suggestions.addEventListener('focusin', loadAiSuggestions, { once: true });
4831
+ }
4217
4832
 
4218
4833
  chatMessagesEl.appendChild(suggestions);
4219
4834
  chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
@@ -4398,8 +5013,10 @@
4398
5013
  },
4399
5014
  body: JSON.stringify({
4400
5015
  filename: _cfg.filename,
5016
+ tid: _cfg.tid || 0,
4401
5017
  question: question,
4402
5018
  history: chatHistory,
5019
+ detailMode: chatDetailMode,
4403
5020
  }),
4404
5021
  });
4405
5022
 
@@ -4424,7 +5041,16 @@
4424
5041
  chatHistory = chatHistory.slice(-50);
4425
5042
  }
4426
5043
  } else {
4427
- addChatMessage('error', data.error || 'Bir hata oluştu. Tekrar deneyin.');
5044
+ var errorMsg = addChatMessage('error', data.error || 'Bir hata oluştu. Tekrar deneyin.');
5045
+ // Show upgrade button for file size limit errors (Premium -> VIP)
5046
+ if (data.showUpgrade && errorMsg) {
5047
+ var uid = (_cfg && _cfg.uid) || 0;
5048
+ var upgradeBtn = document.createElement('button');
5049
+ upgradeBtn.style.cssText = 'margin-top:8px;padding:4px 12px;background:linear-gradient(135deg,#ffd700,#ffaa00);color:#1a1a1a;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;';
5050
+ upgradeBtn.textContent = 'VIP\'e Y\u00fckselt';
5051
+ upgradeBtn.addEventListener('click', function () { window.open('https://forum.ieu.app/pay/checkout?uid=' + uid, '_blank'); });
5052
+ errorMsg.appendChild(upgradeBtn);
5053
+ }
4428
5054
  }
4429
5055
  } catch (err) {
4430
5056
  removeChatLoading();