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.
- package/lib/controllers.js +11 -1
- package/lib/gemini-chat.js +25 -7
- 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 +497 -17
- package/test/image.png +0 -0
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,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
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
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
|
|
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
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">
|
|
@@ -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">×</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]
|
|
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
|
-
|
|
4121
|
+
// Normal text — process block and inline formatting
|
|
4122
|
+
renderBlocks(part, frag);
|
|
3925
4123
|
}
|
|
3926
4124
|
return frag;
|
|
3927
4125
|
}
|
|
3928
4126
|
|
|
3929
|
-
|
|
3930
|
-
|
|
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
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
var
|
|
3940
|
-
|
|
3941
|
-
|
|
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
|