nodebb-plugin-pdf-secure2 1.4.2 → 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,13 +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
- - Önce kısa ve net cevapla, sonra gerekirse detaylandır. Gereksiz yere uzatma
42
- - Konuyu örneklerle ve analojilerle açıkla, soyut kavramları somutlaştır
43
- - Matematiksel işlemleri adım adım çöz, her adımı gerekçesiyle açıkla
44
- - Sınav ve quiz hazırlığı için ipuçları, stratejiler ve olası soru kalıpları ver
45
- - İlişkili konulara referans ver, kavramlar arası bağlantıları kur
46
- - Gerektiğinde karşılaştırma tabloları ve kavram haritaları oluştur
47
- - 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
48
48
  - Bilgiyi doğrudan PDF'ten al, sayfa/bölüm numarası belirt
49
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
50
50
  - Tamamen alakasız konularda "Bu konu dökümanın kapsamı dışındadır" de
@@ -140,6 +140,13 @@ const cleanupTimer = setInterval(() => {
140
140
  }, 10 * 60 * 1000);
141
141
  cleanupTimer.unref();
142
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
+
143
150
  GeminiChat.init = function (apiKey) {
144
151
  if (!apiKey) {
145
152
  ai = null;
@@ -312,11 +319,21 @@ GeminiChat.chat = async function (filename, question, history, tier) {
312
319
  ...contents,
313
320
  ];
314
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
+
315
332
  const response = await ai.models.generateContent({
316
333
  model: MODEL_NAME,
317
334
  contents: fullContents,
318
335
  config: {
319
- systemInstruction: tier === 'vip' ? SYSTEM_INSTRUCTION_VIP : SYSTEM_INSTRUCTION_PREMIUM,
336
+ systemInstruction,
320
337
  maxOutputTokens: config.maxOutputTokens,
321
338
  },
322
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.2",
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">
@@ -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;
@@ -862,6 +970,40 @@
862
970
  body[data-tier="vip"] .chatSuggestionChip:hover { border-color: #ffd700; color: #ffd700; background: rgba(255, 215, 0, 0.12); }
863
971
  body[data-tier="vip"] .chatSummaryBtn:hover { color: #ffd700; border-color: #ffd700; }
864
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
+
865
1007
  /* Suggestion chips */
866
1008
  .chatSuggestions {
867
1009
  display: flex;
@@ -2868,6 +3010,16 @@
2868
3010
  </button>
2869
3011
  <button class="closeBtn" id="closeChatSidebar">&times;</button>
2870
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>
2871
3023
  <div id="chatMessages"></div>
2872
3024
  <div class="chatCharCount" id="chatCharCount"></div>
2873
3025
  <div class="chatQuotaBar" id="chatQuotaBar">
@@ -3901,6 +4053,25 @@
3901
4053
 
3902
4054
  chatBtnEl.onclick = toggleChat;
3903
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
+
3904
4075
  closeChatBtn.onclick = () => {
3905
4076
  chatSidebarEl.classList.remove('open');
3906
4077
  chatBtnEl.classList.remove('active');
@@ -3917,12 +4088,19 @@
3917
4088
 
3918
4089
  var frag = document.createDocumentFragment();
3919
4090
 
3920
- // Split into code blocks and normal text
3921
- 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);
3922
4093
  for (var pi = 0; pi < parts.length; pi++) {
3923
4094
  var part = parts[pi];
3924
4095
  if (!part) continue;
3925
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
+
3926
4104
  // Code block
3927
4105
  if (part.startsWith('```') && part.endsWith('```')) {
3928
4106
  var codeContent = part.slice(3, -3);
@@ -3935,31 +4113,248 @@
3935
4113
  var code = document.createElement('code');
3936
4114
  code.textContent = codeContent.trim();
3937
4115
  pre.appendChild(code);
4116
+ pre.appendChild(createCopyBtn(codeContent.trim()));
3938
4117
  frag.appendChild(pre);
3939
4118
  continue;
3940
4119
  }
3941
4120
 
3942
- // Normal text — process inline formatting
3943
- renderInlineMarkdown(part, frag);
4121
+ // Normal text — process block and inline formatting
4122
+ renderBlocks(part, frag);
3944
4123
  }
3945
4124
  return frag;
3946
4125
  }
3947
4126
 
3948
- function renderInlineMarkdown(text, parent) {
3949
- // 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) {
3950
4130
  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]);
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
+ }
3961
4144
  }
4145
+ paraBuf = [];
4146
+ hasOutput = true;
3962
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;
3963
4358
  }
3964
4359
 
3965
4360
  function tokenizeInline(line) {
@@ -4000,6 +4395,26 @@
4000
4395
  }
4001
4396
 
4002
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
+ }
4003
4418
  // Inline code: `...`
4004
4419
  if (line[i] === '`') {
4005
4420
  var end = line.indexOf('`', i + 1);
@@ -4424,7 +4839,16 @@
4424
4839
  chatHistory = chatHistory.slice(-50);
4425
4840
  }
4426
4841
  } else {
4427
- 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
+ }
4428
4852
  }
4429
4853
  } catch (err) {
4430
4854
  removeChatLoading();