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.
- package/lib/controllers.js +11 -1
- package/lib/gemini-chat.js +25 -8
- package/library.js +6 -0
- package/package.json +1 -1
- package/static/lib/main.js +1 -1
- package/static/templates/admin/plugins/pdf-secure.tpl +21 -0
- package/static/viewer.html +441 -17
package/lib/controllers.js
CHANGED
|
@@ -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
|
-
|
|
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' });
|
package/lib/gemini-chat.js
CHANGED
|
@@ -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
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
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
|
|
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
package/static/lib/main.js
CHANGED
|
@@ -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
|
|
package/static/viewer.html
CHANGED
|
@@ -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">×</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]
|
|
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
|
-
|
|
4121
|
+
// Normal text — process block and inline formatting
|
|
4122
|
+
renderBlocks(part, frag);
|
|
3944
4123
|
}
|
|
3945
4124
|
return frag;
|
|
3946
4125
|
}
|
|
3947
4126
|
|
|
3948
|
-
|
|
3949
|
-
|
|
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
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
var
|
|
3959
|
-
|
|
3960
|
-
|
|
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();
|