nodebb-plugin-pdf-secure2 1.4.1 → 1.4.3

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.
@@ -331,7 +331,10 @@ Controllers.handleChat = async function (req, res) {
331
331
  return res.status(404).json({ error: 'PDF bulunamadı.', quota });
332
332
  }
333
333
  if (err.message === 'PDF too large for AI chat') {
334
- return res.status(413).json({ error: 'Bu PDF çok büyük. AI chat desteklemiyor.', quota });
334
+ const sizeMsg = tier === 'premium'
335
+ ? 'Bu PDF, Premium limiti (20MB) aşıyor. VIP üyelikle 50MB\'a kadar PDF\'lerle sohbet edebilirsiniz.'
336
+ : 'Bu PDF çok büyük. AI chat için maksimum dosya boyutu 50MB.';
337
+ return res.status(413).json({ error: sizeMsg, quota, showUpgrade: tier === 'premium' });
335
338
  }
336
339
  if (err.status === 429 || err.message.includes('rate limit') || err.message.includes('quota')) {
337
340
  return res.status(429).json({ error: 'AI servisi şu an yoğun. Lütfen birkaç saniye sonra tekrar deneyin.', quota });
@@ -345,6 +348,13 @@ Controllers.handleChat = async function (req, res) {
345
348
 
346
349
  // AI-powered question suggestions for a PDF
347
350
  const suggestionsRateLimit = new Map(); // uid -> lastRequestTime
351
+ // Periodic cleanup of stale rate limit entries (every 5 minutes)
352
+ setInterval(() => {
353
+ const cutoff = Date.now() - 60000; // entries older than 60s are stale
354
+ for (const [uid, ts] of suggestionsRateLimit.entries()) {
355
+ if (ts < cutoff) suggestionsRateLimit.delete(uid);
356
+ }
357
+ }, 5 * 60 * 1000).unref();
348
358
  Controllers.getSuggestions = async function (req, res) {
349
359
  if (!req.uid) {
350
360
  return res.status(401).json({ error: 'Authentication required' });
@@ -38,12 +38,13 @@ ${SECURITY_RULES}`;
38
38
 
39
39
  const SYSTEM_INSTRUCTION_VIP = `Sen bir PDF döküman asistanı ve ders öğretmenisin. Bu döküman bir üniversite ders materyalidir. Kurallar:
40
40
  - Kullanıcının yazdığı dilde cevap ver
41
- - Konuyu örneklerle ve analojilerle açıkla, soyut kavramları somutlaştır
42
- - Matematiksel işlemleri adım adım çöz, her adımı gerekçesiyle açıkla
43
- - Sınav ve quiz hazırlığı için ipuçları, stratejiler ve olası soru kalıpları ver
44
- - İlişkili konulara referans ver, kavramlar arası bağlantıları kur
45
- - Gerektiğinde karşılaştırma tabloları ve kavram haritaları oluştur
46
- - Ezber teknikleri ve hatırlatıcı kısayollar öner
41
+ - Soruya göre yanıt uzunluğunu ayarla: basit sorulara kısa (2-3 cümle), karmaşık sorulara daha detaylı (1-2 paragraf) cevap ver. Asla gereksiz uzatma
42
+ - Sadece cevabı değil, "neden böyle?" sorusunu da cevapla — kavramı bağlamına oturtur şekilde öğretici yaklaş
43
+ - Açıklamalarını örneklerle ve analojilerle zenginleştir ama kısa tut
44
+ - Yanıtlarını madde, başlık ve yapısal format kullanarak düzenli sun
45
+ - Karşılaştırma tabloları, kavram haritaları, ezber teknikleri ve sınav stratejilerini SADECE kullanıcı istediğinde ver
46
+ - Kullanıcı "detaylı anlat", "daha fazla açıkla", "örnek ver" derse o zaman genişlet
47
+ - Matematiksel işlemleri adım adım çöz, sadece kritik adımları göster
47
48
  - Bilgiyi doğrudan PDF'ten al, sayfa/bölüm numarası belirt
48
49
  - Dokümandaki konularla ilgili sorularda genel bilginle de destekle, ancak PDF'te olmayan bilgiyi "(Not: Bu bilgi doğrudan dokümanda geçmemektedir)" şeklinde belirt
49
50
  - Tamamen alakasız konularda "Bu konu dökümanın kapsamı dışındadır" de
@@ -139,6 +140,13 @@ const cleanupTimer = setInterval(() => {
139
140
  }, 10 * 60 * 1000);
140
141
  cleanupTimer.unref();
141
142
 
143
+ // Admin-configured custom prompts (override defaults when non-empty)
144
+ let customPrompts = { premium: '', vip: '' };
145
+
146
+ GeminiChat.setCustomPrompts = function (prompts) {
147
+ customPrompts = prompts || { premium: '', vip: '' };
148
+ };
149
+
142
150
  GeminiChat.init = function (apiKey) {
143
151
  if (!apiKey) {
144
152
  ai = null;
@@ -311,11 +319,21 @@ GeminiChat.chat = async function (filename, question, history, tier) {
311
319
  ...contents,
312
320
  ];
313
321
 
322
+ // Use admin-configured prompt if set, otherwise fall back to defaults
323
+ let systemInstruction = tier === 'vip'
324
+ ? (customPrompts.vip || SYSTEM_INSTRUCTION_VIP)
325
+ : (customPrompts.premium || SYSTEM_INSTRUCTION_PREMIUM);
326
+
327
+ // Always append security rules if not already present (admin can't bypass)
328
+ if (!systemInstruction.includes('Güvenlik kuralları')) {
329
+ systemInstruction += '\n' + SECURITY_RULES;
330
+ }
331
+
314
332
  const response = await ai.models.generateContent({
315
333
  model: MODEL_NAME,
316
334
  contents: fullContents,
317
335
  config: {
318
- systemInstruction: tier === 'vip' ? SYSTEM_INSTRUCTION_VIP : SYSTEM_INSTRUCTION_PREMIUM,
336
+ systemInstruction,
319
337
  maxOutputTokens: config.maxOutputTokens,
320
338
  },
321
339
  });
package/library.js CHANGED
@@ -76,6 +76,12 @@ plugin.init = async (params) => {
76
76
  // Apply admin-configured quota settings
77
77
  controllers.initQuotaSettings(pluginSettings);
78
78
 
79
+ // Apply admin-configured custom prompts
80
+ geminiChat.setCustomPrompts({
81
+ premium: pluginSettings.promptPremium || '',
82
+ vip: pluginSettings.promptVip || '',
83
+ });
84
+
79
85
  const watermarkEnabled = pluginSettings.watermarkEnabled === 'on';
80
86
 
81
87
  // Admin page route
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-pdf-secure2",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "Secure PDF viewer plugin for NodeBB - prevents downloading, enables canvas-only rendering with Premium group support",
5
5
  "main": "library.js",
6
6
  "repository": {
@@ -456,7 +456,7 @@
456
456
  iframe.src = config.relative_path + '/plugins/pdf-secure/viewer?file=' + encodeURIComponent(filename);
457
457
  iframe.setAttribute('frameborder', '0');
458
458
  iframe.setAttribute('allowfullscreen', 'true');
459
- iframe.setAttribute('allow', 'fullscreen');
459
+ iframe.setAttribute('allow', 'fullscreen; clipboard-write');
460
460
 
461
461
  // Store resolver for postMessage callback
462
462
  currentResolver = function () {
@@ -96,6 +96,27 @@
96
96
  </div>
97
97
  </div>
98
98
 
99
+ <hr class="my-3">
100
+ <h6 class="fw-semibold mb-3" style="font-size:13px;">AI Sistem Prompt'lari</h6>
101
+
102
+ <div class="mb-3">
103
+ <label class="form-label fw-medium" for="promptPremium" style="font-size:13px;">
104
+ <span class="badge text-bg-primary me-1" style="font-size:9px;">PREMIUM</span>
105
+ Sistem Prompt
106
+ </label>
107
+ <textarea id="promptPremium" name="promptPremium" data-key="promptPremium" class="form-control" rows="6" style="font-size:12px;" placeholder="Varsayilan prompt kullanilir..."></textarea>
108
+ <div class="form-text" style="font-size:11px;">Bos birakilirsa varsayilan prompt kullanilir. Guvenlik kurallari otomatik eklenir.</div>
109
+ </div>
110
+
111
+ <div class="mb-3">
112
+ <label class="form-label fw-medium" for="promptVip" style="font-size:13px;">
113
+ <span class="badge me-1" style="font-size:9px;background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff;">VIP</span>
114
+ Sistem Prompt
115
+ </label>
116
+ <textarea id="promptVip" name="promptVip" data-key="promptVip" class="form-control" rows="8" style="font-size:12px;" placeholder="Varsayilan prompt kullanilir..."></textarea>
117
+ <div class="form-text" style="font-size:11px;">Bos birakilirsa varsayilan prompt kullanilir. Guvenlik kurallari otomatik eklenir.</div>
118
+ </div>
119
+
99
120
  <hr class="my-3">
100
121
  <h6 class="fw-semibold mb-3" style="font-size:13px;">Token Kota Ayarlari</h6>
101
122
 
@@ -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">
@@ -653,6 +657,8 @@
653
657
  right: 0;
654
658
  bottom: 0;
655
659
  width: 320px;
660
+ min-width: 280px;
661
+ max-width: 600px;
656
662
  background: var(--bg-secondary);
657
663
  border-left: 1px solid var(--border-color);
658
664
  display: flex;
@@ -663,6 +669,22 @@
663
669
  pointer-events: none;
664
670
  }
665
671
 
672
+ /* Resize handle on the left edge of chat sidebar */
673
+ .chatResizeHandle {
674
+ position: absolute;
675
+ left: -4px;
676
+ top: 0;
677
+ bottom: 0;
678
+ width: 8px;
679
+ cursor: col-resize;
680
+ z-index: 51;
681
+ }
682
+ .chatResizeHandle:hover,
683
+ .chatResizeHandle.active {
684
+ background: var(--accent);
685
+ opacity: 0.3;
686
+ }
687
+
666
688
  #chatSidebar.open {
667
689
  transform: translateX(0);
668
690
  pointer-events: auto;
@@ -731,6 +753,110 @@
731
753
  padding: 0;
732
754
  }
733
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
+
734
860
  /* AI badge on AI messages */
735
861
  .chatMsg.ai .aiBadge {
736
862
  display: flex;
@@ -844,6 +970,40 @@
844
970
  body[data-tier="vip"] .chatSuggestionChip:hover { border-color: #ffd700; color: #ffd700; background: rgba(255, 215, 0, 0.12); }
845
971
  body[data-tier="vip"] .chatSummaryBtn:hover { color: #ffd700; border-color: #ffd700; }
846
972
 
973
+ /* VIP feature tips banner */
974
+ .vipTipsBanner {
975
+ display: none;
976
+ padding: 6px 12px;
977
+ background: linear-gradient(135deg, rgba(255,215,0,0.08), rgba(255,170,0,0.05));
978
+ border-bottom: 1px solid rgba(255,215,0,0.15);
979
+ font-size: 11px;
980
+ color: var(--text-secondary);
981
+ line-height: 1.4;
982
+ }
983
+ .vipTipsBanner .vipTipsLabel {
984
+ color: #ffd700;
985
+ font-weight: 600;
986
+ font-size: 10px;
987
+ letter-spacing: 0.3px;
988
+ margin-bottom: 2px;
989
+ }
990
+ .vipTipsBanner .vipTipsItems {
991
+ display: flex;
992
+ flex-wrap: wrap;
993
+ gap: 4px;
994
+ margin-top: 4px;
995
+ }
996
+ .vipTipsBanner .vipTip {
997
+ background: rgba(255,215,0,0.1);
998
+ border: 1px solid rgba(255,215,0,0.15);
999
+ border-radius: 10px;
1000
+ padding: 2px 8px;
1001
+ font-size: 10px;
1002
+ color: rgba(255,215,0,0.8);
1003
+ white-space: nowrap;
1004
+ }
1005
+ body[data-tier="vip"] .vipTipsBanner { display: block; }
1006
+
847
1007
  /* Suggestion chips */
848
1008
  .chatSuggestions {
849
1009
  display: flex;
@@ -2834,6 +2994,7 @@
2834
2994
 
2835
2995
  <!-- Chat Sidebar -->
2836
2996
  <div id="chatSidebar">
2997
+ <div class="chatResizeHandle" id="chatResizeHandle"></div>
2837
2998
  <div class="sidebarHeader">
2838
2999
  <div class="chatHeaderTitle">
2839
3000
  <svg viewBox="0 0 24 24"><path d="M19 9l1.25-2.75L23 5l-2.75-1.25L19 1l-1.25 2.75L15 5l2.75 1.25L19 9zm-7.5.5L9 4 6.5 9.5 1 12l5.5 2.5L9 20l2.5-5.5L17 12l-5.5-2.5z"/></svg>
@@ -2849,6 +3010,16 @@
2849
3010
  </button>
2850
3011
  <button class="closeBtn" id="closeChatSidebar">&times;</button>
2851
3012
  </div>
3013
+ <div class="vipTipsBanner" id="vipTipsBanner">
3014
+ <div class="vipTipsLabel">VIP — Benden isteyebileceklerin:</div>
3015
+ <div class="vipTipsItems">
3016
+ <span class="vipTip">detayli aciklama</span>
3017
+ <span class="vipTip">ornek ver</span>
3018
+ <span class="vipTip">sinav sorusu</span>
3019
+ <span class="vipTip">karsilastirma tablosu</span>
3020
+ <span class="vipTip">ezber teknigi</span>
3021
+ </div>
3022
+ </div>
2852
3023
  <div id="chatMessages"></div>
2853
3024
  <div class="chatCharCount" id="chatCharCount"></div>
2854
3025
  <div class="chatQuotaBar" id="chatQuotaBar">
@@ -3882,6 +4053,25 @@
3882
4053
 
3883
4054
  chatBtnEl.onclick = toggleChat;
3884
4055
 
4056
+ // VIP tip chips — click to populate chat input
4057
+ var vipTipChips = document.querySelectorAll('.vipTip');
4058
+ var vipTipPrompts = {
4059
+ 'detayli aciklama': 'Bu konuyu detayli aciklar misin?',
4060
+ 'ornek ver': 'Bununla ilgili somut bir ornek verir misin?',
4061
+ 'sinav sorusu': 'Bu konudan cikmis veya cikabilecek sinav sorulari neler?',
4062
+ 'karsilastirma tablosu': 'Bu konudaki kavramlari karsilastirma tablosu ile goster',
4063
+ 'ezber teknigi': 'Bu konuyu kolayca ezberlemek icin teknik onerir misin?',
4064
+ };
4065
+ vipTipChips.forEach(function (chip) {
4066
+ chip.onclick = function () {
4067
+ var prompt = vipTipPrompts[chip.textContent.trim()];
4068
+ if (prompt && chatInputEl) {
4069
+ chatInputEl.value = prompt;
4070
+ chatInputEl.focus();
4071
+ }
4072
+ };
4073
+ });
4074
+
3885
4075
  closeChatBtn.onclick = () => {
3886
4076
  chatSidebarEl.classList.remove('open');
3887
4077
  chatBtnEl.classList.remove('active');
@@ -3898,12 +4088,19 @@
3898
4088
 
3899
4089
  var frag = document.createDocumentFragment();
3900
4090
 
3901
- // Split into code blocks and normal text
3902
- var parts = text.split(/(```[\s\S]*?```)/g);
4091
+ // Split into code blocks, LaTeX blocks ($$...$$), and normal text
4092
+ var parts = text.split(/(```[\s\S]*?```|\$\$[\s\S]*?\$\$)/g);
3903
4093
  for (var pi = 0; pi < parts.length; pi++) {
3904
4094
  var part = parts[pi];
3905
4095
  if (!part) continue;
3906
4096
 
4097
+ // LaTeX block: $$...$$
4098
+ if (part.startsWith('$$') && part.endsWith('$$') && part.length > 4) {
4099
+ var latex = part.slice(2, -2).trim();
4100
+ frag.appendChild(renderMath(latex, true));
4101
+ continue;
4102
+ }
4103
+
3907
4104
  // Code block
3908
4105
  if (part.startsWith('```') && part.endsWith('```')) {
3909
4106
  var codeContent = part.slice(3, -3);
@@ -3916,31 +4113,248 @@
3916
4113
  var code = document.createElement('code');
3917
4114
  code.textContent = codeContent.trim();
3918
4115
  pre.appendChild(code);
4116
+ pre.appendChild(createCopyBtn(codeContent.trim()));
3919
4117
  frag.appendChild(pre);
3920
4118
  continue;
3921
4119
  }
3922
4120
 
3923
- // Normal text — process inline formatting
3924
- renderInlineMarkdown(part, frag);
4121
+ // Normal text — process block and inline formatting
4122
+ renderBlocks(part, frag);
3925
4123
  }
3926
4124
  return frag;
3927
4125
  }
3928
4126
 
3929
- function renderInlineMarkdown(text, parent) {
3930
- // Split by lines first
4127
+ // Block-level markdown: tables, lists, headers, HR
4128
+ // Falls back to inline formatting for regular text
4129
+ function renderBlocks(text, parent) {
3931
4130
  var lines = text.split('\n');
3932
- for (var li = 0; li < lines.length; li++) {
3933
- if (li > 0) parent.appendChild(document.createElement('br'));
3934
- var line = lines[li];
3935
- if (!line) continue;
3936
-
3937
- // Tokenize: inline code, bold, italic, page citations
3938
- // Process left-to-right with a simple state machine
3939
- var tokens = tokenizeInline(line);
3940
- for (var ti = 0; ti < tokens.length; ti++) {
3941
- parent.appendChild(tokens[ti]);
4131
+ var i = 0;
4132
+ var paraBuf = [];
4133
+ var hasOutput = false; // track if we've rendered anything yet
4134
+
4135
+ function flushPara() {
4136
+ if (paraBuf.length === 0) return;
4137
+ if (hasOutput) parent.appendChild(document.createElement('br'));
4138
+ for (var pi = 0; pi < paraBuf.length; pi++) {
4139
+ if (pi > 0) parent.appendChild(document.createElement('br'));
4140
+ var tokens = tokenizeInline(paraBuf[pi]);
4141
+ for (var t = 0; t < tokens.length; t++) {
4142
+ parent.appendChild(tokens[t]);
4143
+ }
3942
4144
  }
4145
+ paraBuf = [];
4146
+ hasOutput = true;
3943
4147
  }
4148
+
4149
+ while (i < lines.length) {
4150
+ var trimmed = lines[i].trim();
4151
+
4152
+ // Empty line — flush paragraph buffer
4153
+ if (!trimmed) { flushPara(); i++; continue; }
4154
+
4155
+ // Horizontal rule: --- or *** or ___
4156
+ if (/^[-*_]{3,}\s*$/.test(trimmed) && !/\S/.test(trimmed.replace(/[-*_]/g, ''))) {
4157
+ flushPara();
4158
+ parent.appendChild(document.createElement('hr'));
4159
+ hasOutput = true;
4160
+ i++; continue;
4161
+ }
4162
+
4163
+ // Header: # ## ### ####
4164
+ var hMatch = trimmed.match(/^(#{1,4})\s+(.+)/);
4165
+ if (hMatch) {
4166
+ flushPara();
4167
+ var hEl = document.createElement('h' + Math.min(hMatch[1].length + 2, 5));
4168
+ var hTokens = tokenizeInline(hMatch[2]);
4169
+ for (var t = 0; t < hTokens.length; t++) hEl.appendChild(hTokens[t]);
4170
+ parent.appendChild(hEl);
4171
+ hasOutput = true;
4172
+ i++; continue;
4173
+ }
4174
+
4175
+ // Table: lines containing | with a separator row
4176
+ if (trimmed.includes('|') && i + 1 < lines.length) {
4177
+ var tLines = [];
4178
+ var j = i;
4179
+ while (j < lines.length && lines[j].trim().includes('|')) {
4180
+ tLines.push(lines[j].trim());
4181
+ j++;
4182
+ }
4183
+ if (tLines.length >= 2 && /^\|?[\s:|-]+\|?$/.test(tLines[1]) && tLines[1].includes('-')) {
4184
+ flushPara();
4185
+ var table = renderTable(tLines);
4186
+ if (table) {
4187
+ var wrap = document.createElement('div');
4188
+ wrap.className = 'tableWrap';
4189
+ wrap.appendChild(table);
4190
+ wrap.appendChild(createCopyBtn(tableToText(table)));
4191
+ parent.appendChild(wrap);
4192
+ hasOutput = true;
4193
+ i = j; continue;
4194
+ }
4195
+ }
4196
+ }
4197
+
4198
+ // Unordered list: - item or * item (but not ---)
4199
+ if (/^[-*]\s+/.test(trimmed)) {
4200
+ flushPara();
4201
+ var ul = document.createElement('ul');
4202
+ while (i < lines.length) {
4203
+ var lt = lines[i].trim();
4204
+ var lm = lt.match(/^[-*]\s+(.*)/);
4205
+ if (lm) {
4206
+ var li2 = document.createElement('li');
4207
+ var liTokens = tokenizeInline(lm[1]);
4208
+ for (var t = 0; t < liTokens.length; t++) li2.appendChild(liTokens[t]);
4209
+ ul.appendChild(li2);
4210
+ i++;
4211
+ } else if (!lt) {
4212
+ i++;
4213
+ if (i < lines.length && /^[-*]\s+/.test(lines[i].trim())) continue;
4214
+ break;
4215
+ } else { break; }
4216
+ }
4217
+ parent.appendChild(ul);
4218
+ hasOutput = true;
4219
+ continue;
4220
+ }
4221
+
4222
+ // Ordered list: 1. item or 1) item
4223
+ if (/^\d+[.)]\s+/.test(trimmed)) {
4224
+ flushPara();
4225
+ var ol = document.createElement('ol');
4226
+ while (i < lines.length) {
4227
+ var lt = lines[i].trim();
4228
+ var om = lt.match(/^\d+[.)]\s+(.*)/);
4229
+ if (om) {
4230
+ var li3 = document.createElement('li');
4231
+ var oTokens = tokenizeInline(om[1]);
4232
+ for (var t = 0; t < oTokens.length; t++) li3.appendChild(oTokens[t]);
4233
+ ol.appendChild(li3);
4234
+ i++;
4235
+ } else if (!lt) {
4236
+ i++;
4237
+ if (i < lines.length && /^\d+[.)]\s+/.test(lines[i].trim())) continue;
4238
+ break;
4239
+ } else { break; }
4240
+ }
4241
+ parent.appendChild(ol);
4242
+ hasOutput = true;
4243
+ continue;
4244
+ }
4245
+
4246
+ // Regular text line — buffer for paragraph
4247
+ paraBuf.push(trimmed);
4248
+ i++;
4249
+ }
4250
+ flushPara();
4251
+ }
4252
+
4253
+ function renderTable(tLines) {
4254
+ var headerCells = parseTableRow(tLines[0]);
4255
+ if (!headerCells || headerCells.length === 0) return null;
4256
+
4257
+ // Parse alignments from separator row
4258
+ var sepCells = parseTableRow(tLines[1]);
4259
+ var aligns = sepCells ? sepCells.map(function (c) {
4260
+ c = c.trim();
4261
+ if (c.charAt(0) === ':' && c.charAt(c.length - 1) === ':') return 'center';
4262
+ if (c.charAt(c.length - 1) === ':') return 'right';
4263
+ return '';
4264
+ }) : [];
4265
+
4266
+ var table = document.createElement('table');
4267
+ var thead = document.createElement('thead');
4268
+ var hRow = document.createElement('tr');
4269
+ for (var c = 0; c < headerCells.length; c++) {
4270
+ var th = document.createElement('th');
4271
+ if (aligns[c]) th.style.textAlign = aligns[c];
4272
+ var tokens = tokenizeInline(headerCells[c].trim());
4273
+ for (var t = 0; t < tokens.length; t++) th.appendChild(tokens[t]);
4274
+ hRow.appendChild(th);
4275
+ }
4276
+ thead.appendChild(hRow);
4277
+ table.appendChild(thead);
4278
+
4279
+ var tbody = document.createElement('tbody');
4280
+ for (var r = 2; r < tLines.length; r++) {
4281
+ var cells = parseTableRow(tLines[r]);
4282
+ if (!cells) continue;
4283
+ var tr = document.createElement('tr');
4284
+ for (var c = 0; c < cells.length; c++) {
4285
+ var td = document.createElement('td');
4286
+ if (aligns[c]) td.style.textAlign = aligns[c];
4287
+ var tokens = tokenizeInline((cells[c] || '').trim());
4288
+ for (var t = 0; t < tokens.length; t++) td.appendChild(tokens[t]);
4289
+ tr.appendChild(td);
4290
+ }
4291
+ tbody.appendChild(tr);
4292
+ }
4293
+ table.appendChild(tbody);
4294
+ return table;
4295
+ }
4296
+
4297
+ function parseTableRow(line) {
4298
+ line = line.trim();
4299
+ if (line.charAt(0) === '|') line = line.slice(1);
4300
+ if (line.charAt(line.length - 1) === '|') line = line.slice(0, -1);
4301
+ if (!line) return null;
4302
+ return line.split('|');
4303
+ }
4304
+
4305
+ // Copy button helper (used for code blocks and tables)
4306
+ function createCopyBtn(textToCopy) {
4307
+ var btn = document.createElement('button');
4308
+ btn.className = 'chatCopyBtn';
4309
+ btn.textContent = 'Kopyala';
4310
+ btn.onclick = function (e) {
4311
+ e.stopPropagation();
4312
+ navigator.clipboard.writeText(textToCopy).then(function () {
4313
+ btn.textContent = 'Kopyalandi!';
4314
+ btn.classList.add('copied');
4315
+ setTimeout(function () {
4316
+ btn.textContent = 'Kopyala';
4317
+ btn.classList.remove('copied');
4318
+ }, 1500);
4319
+ }).catch(function () {
4320
+ btn.textContent = 'Hata!';
4321
+ setTimeout(function () { btn.textContent = 'Kopyala'; }, 1500);
4322
+ });
4323
+ };
4324
+ return btn;
4325
+ }
4326
+
4327
+ // Convert table element to tab-separated text for clipboard
4328
+ function tableToText(table) {
4329
+ var lines = [];
4330
+ var rows = table.querySelectorAll('tr');
4331
+ for (var r = 0; r < rows.length; r++) {
4332
+ var cells = rows[r].querySelectorAll('th, td');
4333
+ var line = [];
4334
+ for (var c = 0; c < cells.length; c++) {
4335
+ line.push(cells[c].textContent.trim());
4336
+ }
4337
+ lines.push(line.join('\t'));
4338
+ }
4339
+ return lines.join('\n');
4340
+ }
4341
+
4342
+ // Render LaTeX math expression (inline or block)
4343
+ function renderMath(latex, displayMode) {
4344
+ if (typeof katex === 'undefined') {
4345
+ // KaTeX not loaded, show raw LaTeX
4346
+ var fallback = document.createElement('code');
4347
+ fallback.textContent = (displayMode ? '$$' : '$') + latex + (displayMode ? '$$' : '$');
4348
+ return fallback;
4349
+ }
4350
+ var container = document.createElement('span');
4351
+ container.className = displayMode ? 'katex-block' : 'katex-inline';
4352
+ try {
4353
+ katex.render(latex, container, { displayMode: displayMode, throwOnError: false });
4354
+ } catch (e) {
4355
+ container.textContent = latex;
4356
+ }
4357
+ return container;
3944
4358
  }
3945
4359
 
3946
4360
  function tokenizeInline(line) {
@@ -3981,6 +4395,26 @@
3981
4395
  }
3982
4396
 
3983
4397
  while (i < line.length) {
4398
+ // Block math: $$...$$
4399
+ if (line[i] === '$' && line[i + 1] === '$') {
4400
+ var endM = line.indexOf('$$', i + 2);
4401
+ if (endM !== -1) {
4402
+ flushBuf();
4403
+ nodes.push(renderMath(line.slice(i + 2, endM), true));
4404
+ i = endM + 2;
4405
+ continue;
4406
+ }
4407
+ }
4408
+ // Inline math: $...$ (but not $$)
4409
+ if (line[i] === '$' && line[i + 1] !== '$') {
4410
+ var endM2 = line.indexOf('$', i + 1);
4411
+ if (endM2 !== -1 && endM2 > i + 1) {
4412
+ flushBuf();
4413
+ nodes.push(renderMath(line.slice(i + 1, endM2), false));
4414
+ i = endM2 + 1;
4415
+ continue;
4416
+ }
4417
+ }
3984
4418
  // Inline code: `...`
3985
4419
  if (line[i] === '`') {
3986
4420
  var end = line.indexOf('`', i + 1);
@@ -4405,7 +4839,16 @@
4405
4839
  chatHistory = chatHistory.slice(-50);
4406
4840
  }
4407
4841
  } else {
4408
- addChatMessage('error', data.error || 'Bir hata oluştu. Tekrar deneyin.');
4842
+ var errorMsg = addChatMessage('error', data.error || 'Bir hata oluştu. Tekrar deneyin.');
4843
+ // Show upgrade button for file size limit errors (Premium -> VIP)
4844
+ if (data.showUpgrade && errorMsg) {
4845
+ var uid = (_cfg && _cfg.uid) || 0;
4846
+ var upgradeBtn = document.createElement('button');
4847
+ 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;';
4848
+ upgradeBtn.textContent = 'VIP\'e Y\u00fckselt';
4849
+ upgradeBtn.addEventListener('click', function () { window.open('https://forum.ieu.app/pay/checkout?uid=' + uid, '_blank'); });
4850
+ errorMsg.appendChild(upgradeBtn);
4851
+ }
4409
4852
  }
4410
4853
  } catch (err) {
4411
4854
  removeChatLoading();
@@ -4456,6 +4899,43 @@
4456
4899
  }
4457
4900
  });
4458
4901
 
4902
+ // Chat sidebar resize (drag left edge to expand)
4903
+ (function () {
4904
+ var handle = document.getElementById('chatResizeHandle');
4905
+ if (!handle) return;
4906
+ var dragging = false;
4907
+ var startX = 0;
4908
+ var startWidth = 0;
4909
+
4910
+ handle.addEventListener('mousedown', function (e) {
4911
+ if (window.innerWidth < 600) return; // Disable on mobile
4912
+ dragging = true;
4913
+ startX = e.clientX;
4914
+ startWidth = chatSidebarEl.offsetWidth;
4915
+ handle.classList.add('active');
4916
+ chatSidebarEl.style.transition = 'none';
4917
+ document.body.style.cursor = 'col-resize';
4918
+ document.body.style.userSelect = 'none';
4919
+ e.preventDefault();
4920
+ });
4921
+
4922
+ document.addEventListener('mousemove', function (e) {
4923
+ if (!dragging) return;
4924
+ var newWidth = startWidth + (startX - e.clientX);
4925
+ newWidth = Math.max(280, Math.min(600, newWidth));
4926
+ chatSidebarEl.style.width = newWidth + 'px';
4927
+ });
4928
+
4929
+ document.addEventListener('mouseup', function () {
4930
+ if (!dragging) return;
4931
+ dragging = false;
4932
+ handle.classList.remove('active');
4933
+ chatSidebarEl.style.transition = '';
4934
+ document.body.style.cursor = '';
4935
+ document.body.style.userSelect = '';
4936
+ });
4937
+ })();
4938
+
4459
4939
  // Sepia Reading Mode
4460
4940
  let sepiaMode = false;
4461
4941
  document.getElementById('sepiaBtn').onclick = () => {
package/test/image.png DELETED
Binary file