nodebb-plugin-pdf-secure2 1.4.2 → 1.4.4
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/.claude/settings.local.json +11 -1
- package/lib/controllers.js +15 -4
- package/lib/gemini-chat.js +63 -18
- package/lib/nonce-store.js +4 -4
- package/lib/pdf-handler.js +0 -1
- package/lib/topic-access.js +96 -0
- package/library.js +6 -0
- package/package.json +1 -1
- package/plugin.json +4 -0
- package/static/lib/main.js +1 -1
- package/static/templates/admin/plugins/pdf-secure.tpl +21 -0
- package/static/viewer.html +665 -24
|
@@ -5,7 +5,17 @@
|
|
|
5
5
|
"WebFetch(domain:github.com)",
|
|
6
6
|
"Bash(dir:*)",
|
|
7
7
|
"Bash(npm pack:*)",
|
|
8
|
-
"Bash(tar:*)"
|
|
8
|
+
"Bash(tar:*)",
|
|
9
|
+
"Bash(grep:*)",
|
|
10
|
+
"Bash(find /c/Users/kadir/OneDrive/Masaüstü/Projeler/nodebb-plugin-pdf-secure/nodebb-plugin-pdf-secure -type f \\\\\\(-name *.conf -o -name *.config -o -name *nginx* -o -name *apache* \\\\\\))",
|
|
11
|
+
"Bash(xargs ls:*)",
|
|
12
|
+
"WebSearch",
|
|
13
|
+
"WebFetch(domain:community.nodebb.org)",
|
|
14
|
+
"Bash(cp /tmp/pdf-secure-143/package/package.json ./package.json)",
|
|
15
|
+
"Bash(cp /tmp/pdf-secure-143/package/library.js ./library.js)",
|
|
16
|
+
"Bash(cp /tmp/pdf-secure-143/package/lib/controllers.js ./lib/controllers.js)",
|
|
17
|
+
"Bash(cp /tmp/pdf-secure-143/package/lib/gemini-chat.js ./lib/gemini-chat.js)",
|
|
18
|
+
"Bash(cp /tmp/pdf-secure-143/package/static/lib/admin.js ./static/lib/admin.js)"
|
|
9
19
|
]
|
|
10
20
|
}
|
|
11
21
|
}
|
package/lib/controllers.js
CHANGED
|
@@ -265,7 +265,8 @@ Controllers.handleChat = async function (req, res) {
|
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
// Body validation
|
|
268
|
-
const { filename, question, history } = req.body;
|
|
268
|
+
const { filename, question, history, detailMode } = req.body;
|
|
269
|
+
const useDetailMode = tier === 'vip' && detailMode === true;
|
|
269
270
|
|
|
270
271
|
if (!filename || typeof filename !== 'string') {
|
|
271
272
|
return res.status(400).json({ error: 'Missing or invalid filename' });
|
|
@@ -312,7 +313,7 @@ Controllers.handleChat = async function (req, res) {
|
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
try {
|
|
315
|
-
const result = await geminiChat.chat(safeName, trimmedQuestion, history || [], tier);
|
|
316
|
+
const result = await geminiChat.chat(safeName, trimmedQuestion, history || [], tier, useDetailMode);
|
|
316
317
|
// Record actual token usage after successful AI response
|
|
317
318
|
// Sanitize: clamp to [0, 1000000] to prevent NaN/negative/absurd values
|
|
318
319
|
const tokensUsed = Math.max(0, Math.min(parseInt(result.tokensUsed, 10) || 0, 1000000));
|
|
@@ -331,7 +332,10 @@ Controllers.handleChat = async function (req, res) {
|
|
|
331
332
|
return res.status(404).json({ error: 'PDF bulunamadı.', quota });
|
|
332
333
|
}
|
|
333
334
|
if (err.message === 'PDF too large for AI chat') {
|
|
334
|
-
|
|
335
|
+
const sizeMsg = tier === 'premium'
|
|
336
|
+
? 'Bu PDF, Premium limiti (20MB) aşıyor. VIP üyelikle 50MB\'a kadar PDF\'lerle sohbet edebilirsiniz.'
|
|
337
|
+
: 'Bu PDF çok büyük. AI chat için maksimum dosya boyutu 50MB.';
|
|
338
|
+
return res.status(413).json({ error: sizeMsg, quota, showUpgrade: tier === 'premium' });
|
|
335
339
|
}
|
|
336
340
|
if (err.status === 429 || err.message.includes('rate limit') || err.message.includes('quota')) {
|
|
337
341
|
return res.status(429).json({ error: 'AI servisi şu an yoğun. Lütfen birkaç saniye sonra tekrar deneyin.', quota });
|
|
@@ -345,6 +349,13 @@ Controllers.handleChat = async function (req, res) {
|
|
|
345
349
|
|
|
346
350
|
// AI-powered question suggestions for a PDF
|
|
347
351
|
const suggestionsRateLimit = new Map(); // uid -> lastRequestTime
|
|
352
|
+
// Periodic cleanup of stale rate limit entries (every 5 minutes)
|
|
353
|
+
setInterval(() => {
|
|
354
|
+
const cutoff = Date.now() - 60000; // entries older than 60s are stale
|
|
355
|
+
for (const [uid, ts] of suggestionsRateLimit.entries()) {
|
|
356
|
+
if (ts < cutoff) suggestionsRateLimit.delete(uid);
|
|
357
|
+
}
|
|
358
|
+
}, 5 * 60 * 1000).unref();
|
|
348
359
|
Controllers.getSuggestions = async function (req, res) {
|
|
349
360
|
if (!req.uid) {
|
|
350
361
|
return res.status(401).json({ error: 'Authentication required' });
|
|
@@ -385,7 +396,7 @@ Controllers.getSuggestions = async function (req, res) {
|
|
|
385
396
|
}
|
|
386
397
|
|
|
387
398
|
try {
|
|
388
|
-
const suggestions = await geminiChat.generateSuggestions(safeName);
|
|
399
|
+
const suggestions = await geminiChat.generateSuggestions(safeName, tier);
|
|
389
400
|
const quotaUsage = await getQuotaUsage(req.uid, tier);
|
|
390
401
|
return res.json({ suggestions, quota: quotaUsage });
|
|
391
402
|
} catch (err) {
|
package/lib/gemini-chat.js
CHANGED
|
@@ -38,13 +38,24 @@ ${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
|
+
- VARSAYILAN: Kısa, net, yüksek sinyalli cevap ver. Basit soru → 1-3 cümle. Orta zorluk → 1 kısa paragraf. Karmaşık → yapılandırılmış ama gereksiz detaysız
|
|
42
|
+
- Sadece cevabı değil "neden böyle?" sorusunu da kısa cevapla
|
|
43
|
+
- Madde ve başlık formatını kullan, gereksiz giriş cümleleri yazma
|
|
44
|
+
- Kullanıcı açıkça "detaylı", "daha fazla", "örnek ver" demezse derinleşme
|
|
45
|
+
- Matematiksel işlemlerde sadece kritik adımları göster
|
|
46
|
+
- Bilgiyi doğrudan PDF'ten al, sayfa/bölüm numarası belirt
|
|
47
|
+
- 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
|
|
48
|
+
- Tamamen alakasız konularda "Bu konu dökümanın kapsamı dışındadır" de
|
|
49
|
+
${SECURITY_RULES}`;
|
|
50
|
+
|
|
51
|
+
const SYSTEM_INSTRUCTION_VIP_DETAILED = `Sen bir PDF döküman asistanı ve ders öğretmenisin. Bu döküman bir üniversite ders materyalidir. Kurallar:
|
|
52
|
+
- Kullanıcının yazdığı dilde cevap ver
|
|
53
|
+
- DETAYLI MOD AKTİF: Derin analiz, yapısal breakdown ve örneklerle zengin cevap ver
|
|
54
|
+
- Kavramları bağlamına oturtarak öğretici yaklaşımla açıkla
|
|
55
|
+
- Açıklamalarını örnekler ve analojilerle zenginleştir
|
|
56
|
+
- Karşılaştırma tabloları, kavram haritaları, ezber teknikleri ve sınav stratejilerini uygun gördüğünde kullan
|
|
57
|
+
- Matematiksel işlemleri adım adım çöz
|
|
58
|
+
- Yanıtlarını madde, başlık ve yapısal format kullanarak düzenli sun
|
|
48
59
|
- Bilgiyi doğrudan PDF'ten al, sayfa/bölüm numarası belirt
|
|
49
60
|
- 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
61
|
- Tamamen alakasız konularda "Bu konu dökümanın kapsamı dışındadır" de
|
|
@@ -113,7 +124,7 @@ function sanitizeHistoryText(text) {
|
|
|
113
124
|
|
|
114
125
|
// Tier-based configuration
|
|
115
126
|
const TIER_CONFIG = {
|
|
116
|
-
vip: { maxHistory: 30, maxOutputTokens: 4096, recentFullMessages: 6 },
|
|
127
|
+
vip: { maxHistory: 30, maxOutputTokens: 2048, detailedMaxOutputTokens: 4096, recentFullMessages: 6 },
|
|
117
128
|
premium: { maxHistory: 20, maxOutputTokens: 2048, recentFullMessages: 4 },
|
|
118
129
|
};
|
|
119
130
|
|
|
@@ -140,6 +151,13 @@ const cleanupTimer = setInterval(() => {
|
|
|
140
151
|
}, 10 * 60 * 1000);
|
|
141
152
|
cleanupTimer.unref();
|
|
142
153
|
|
|
154
|
+
// Admin-configured custom prompts (override defaults when non-empty)
|
|
155
|
+
let customPrompts = { premium: '', vip: '' };
|
|
156
|
+
|
|
157
|
+
GeminiChat.setCustomPrompts = function (prompts) {
|
|
158
|
+
customPrompts = prompts || { premium: '', vip: '' };
|
|
159
|
+
};
|
|
160
|
+
|
|
143
161
|
GeminiChat.init = function (apiKey) {
|
|
144
162
|
if (!apiKey) {
|
|
145
163
|
ai = null;
|
|
@@ -242,7 +260,7 @@ function isSummaryRequest(question, history) {
|
|
|
242
260
|
return SUMMARY_PATTERNS.some((p) => p.test(question));
|
|
243
261
|
}
|
|
244
262
|
|
|
245
|
-
GeminiChat.chat = async function (filename, question, history, tier) {
|
|
263
|
+
GeminiChat.chat = async function (filename, question, history, tier, detailMode) {
|
|
246
264
|
if (!ai) {
|
|
247
265
|
throw new Error('AI chat is not configured');
|
|
248
266
|
}
|
|
@@ -312,12 +330,31 @@ GeminiChat.chat = async function (filename, question, history, tier) {
|
|
|
312
330
|
...contents,
|
|
313
331
|
];
|
|
314
332
|
|
|
333
|
+
// Use admin-configured prompt if set, otherwise fall back to defaults
|
|
334
|
+
let systemInstruction;
|
|
335
|
+
if (tier === 'vip') {
|
|
336
|
+
if (customPrompts.vip) {
|
|
337
|
+
systemInstruction = customPrompts.vip;
|
|
338
|
+
} else {
|
|
339
|
+
systemInstruction = detailMode ? SYSTEM_INSTRUCTION_VIP_DETAILED : SYSTEM_INSTRUCTION_VIP;
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
systemInstruction = customPrompts.premium || SYSTEM_INSTRUCTION_PREMIUM;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Always append security rules if not already present (admin can't bypass)
|
|
346
|
+
if (!systemInstruction.includes('Güvenlik kuralları')) {
|
|
347
|
+
systemInstruction += '\n' + SECURITY_RULES;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const maxOutputTokens = (tier === 'vip' && detailMode) ? config.detailedMaxOutputTokens : config.maxOutputTokens;
|
|
351
|
+
|
|
315
352
|
const response = await ai.models.generateContent({
|
|
316
353
|
model: MODEL_NAME,
|
|
317
354
|
contents: fullContents,
|
|
318
355
|
config: {
|
|
319
|
-
systemInstruction
|
|
320
|
-
maxOutputTokens
|
|
356
|
+
systemInstruction,
|
|
357
|
+
maxOutputTokens,
|
|
321
358
|
},
|
|
322
359
|
});
|
|
323
360
|
|
|
@@ -351,18 +388,20 @@ GeminiChat.chat = async function (filename, question, history, tier) {
|
|
|
351
388
|
};
|
|
352
389
|
|
|
353
390
|
// Generate AI-powered question suggestions for a PDF
|
|
354
|
-
GeminiChat.generateSuggestions = async function (filename) {
|
|
391
|
+
GeminiChat.generateSuggestions = async function (filename, tier) {
|
|
355
392
|
if (!ai) {
|
|
356
393
|
throw new Error('AI chat is not configured');
|
|
357
394
|
}
|
|
358
395
|
|
|
359
|
-
//
|
|
360
|
-
const
|
|
396
|
+
// Cache key includes tier (VIP gets 3, Premium gets 5)
|
|
397
|
+
const cacheKey = filename + '::' + (tier || 'premium');
|
|
398
|
+
const cached = suggestionsCache.get(cacheKey);
|
|
361
399
|
if (cached && Date.now() - cached.cachedAt < SUGGESTIONS_TTL) {
|
|
362
400
|
return cached.suggestions;
|
|
363
401
|
}
|
|
364
402
|
|
|
365
|
-
const
|
|
403
|
+
const count = tier === 'vip' ? 3 : 5;
|
|
404
|
+
const pdfRef = await getOrUploadPdf(filename, tier || 'premium');
|
|
366
405
|
const pdfPart = pdfRef.type === 'fileData'
|
|
367
406
|
? { fileData: { fileUri: pdfRef.fileUri, mimeType: pdfRef.mimeType } }
|
|
368
407
|
: { inlineData: { mimeType: pdfRef.mimeType, data: pdfRef.data } };
|
|
@@ -374,7 +413,13 @@ GeminiChat.generateSuggestions = async function (filename) {
|
|
|
374
413
|
role: 'user',
|
|
375
414
|
parts: [
|
|
376
415
|
pdfPart,
|
|
377
|
-
{ text:
|
|
416
|
+
{ text: `Bu PDF dokümanını analiz et. Kullanıcının sorabileceği ${count} adet SPESİFİK ve İÇERİĞE ÖZEL soru öner.
|
|
417
|
+
Kurallar:
|
|
418
|
+
- Her soru dokümandaki belirli bir kavram, bölüm veya bilgiye referans vermeli
|
|
419
|
+
- "Bu döküman ne hakkında?" gibi genel sorular YASAK
|
|
420
|
+
- Farklı açılardan sor: 1) özet/kapsam, 2) analiz/karşılaştırma, 3) uygulama/örnek
|
|
421
|
+
- Soruları JSON array formatında döndür, başka hiçbir şey yazma
|
|
422
|
+
Örnek: ["Soru 1?", "Soru 2?"]` },
|
|
378
423
|
],
|
|
379
424
|
},
|
|
380
425
|
],
|
|
@@ -397,13 +442,13 @@ GeminiChat.generateSuggestions = async function (filename) {
|
|
|
397
442
|
// Sanitize and limit
|
|
398
443
|
suggestions = suggestions
|
|
399
444
|
.filter(s => typeof s === 'string' && s.length > 0)
|
|
400
|
-
.slice(0,
|
|
445
|
+
.slice(0, count)
|
|
401
446
|
.map(s => s.slice(0, 200));
|
|
402
447
|
} catch (err) {
|
|
403
448
|
// Fallback: return empty, client will keep defaults
|
|
404
449
|
suggestions = [];
|
|
405
450
|
}
|
|
406
451
|
|
|
407
|
-
suggestionsCache.set(
|
|
452
|
+
suggestionsCache.set(cacheKey, { suggestions, cachedAt: Date.now() });
|
|
408
453
|
return suggestions;
|
|
409
454
|
};
|
package/lib/nonce-store.js
CHANGED
|
@@ -53,18 +53,18 @@ NonceStore.validate = function (nonce, uid) {
|
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
//
|
|
57
|
-
store.delete(nonce);
|
|
58
|
-
|
|
59
|
-
// Check UID match
|
|
56
|
+
// Validate BEFORE deleting — prevents DoS via nonce consumption with wrong UID
|
|
60
57
|
if (data.uid !== uid) {
|
|
61
58
|
return null;
|
|
62
59
|
}
|
|
63
60
|
|
|
64
61
|
// Check TTL
|
|
65
62
|
if (Date.now() - data.createdAt > NONCE_TTL) {
|
|
63
|
+
store.delete(nonce); // Expired, clean up
|
|
66
64
|
return null;
|
|
67
65
|
}
|
|
68
66
|
|
|
67
|
+
// All checks passed — delete now (single-use)
|
|
68
|
+
store.delete(nonce);
|
|
69
69
|
return data; // Includes encKey and encIv for AES-256-GCM
|
|
70
70
|
};
|
package/lib/pdf-handler.js
CHANGED
|
@@ -36,7 +36,6 @@ PdfHandler.resolveFilePath = function (filename) {
|
|
|
36
36
|
const uploadPath = nconf.get('upload_path') || path.join(nconf.get('base_dir'), 'public', 'uploads');
|
|
37
37
|
const filePath = path.join(uploadPath, 'files', safeName);
|
|
38
38
|
|
|
39
|
-
// Verify the resolved path is still within the upload directory
|
|
40
39
|
const resolvedPath = path.resolve(filePath);
|
|
41
40
|
const resolvedUploadDir = path.resolve(path.join(uploadPath, 'files'));
|
|
42
41
|
if (!resolvedPath.startsWith(resolvedUploadDir)) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const privileges = require.main.require('./src/privileges');
|
|
4
|
+
const topics = require.main.require('./src/topics');
|
|
5
|
+
const posts = require.main.require('./src/posts');
|
|
6
|
+
const groups = require.main.require('./src/groups');
|
|
7
|
+
const db = require.main.require('./src/database');
|
|
8
|
+
|
|
9
|
+
const TopicAccess = module.exports;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate that a user has access to a PDF through a specific topic.
|
|
13
|
+
* Checks: 1) User can read the topic, 2) The PDF filename exists in the topic's posts.
|
|
14
|
+
* Admin/Global Moderators bypass all checks.
|
|
15
|
+
*
|
|
16
|
+
* @param {number} uid - User ID
|
|
17
|
+
* @param {number|string} tid - Topic ID
|
|
18
|
+
* @param {string} filename - Sanitized PDF filename (basename only)
|
|
19
|
+
* @returns {Promise<{allowed: boolean, reason?: string}>}
|
|
20
|
+
*/
|
|
21
|
+
TopicAccess.validate = async function (uid, tid, filename) {
|
|
22
|
+
// Require valid tid
|
|
23
|
+
tid = parseInt(tid, 10);
|
|
24
|
+
if (!tid || isNaN(tid) || tid <= 0) {
|
|
25
|
+
return { allowed: false, reason: 'Missing or invalid topic ID' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
// Admin/Global Moderator bypass
|
|
30
|
+
const [isAdmin, isGlobalMod] = await Promise.all([
|
|
31
|
+
groups.isMember(uid, 'administrators'),
|
|
32
|
+
groups.isMember(uid, 'Global Moderators'),
|
|
33
|
+
]);
|
|
34
|
+
if (isAdmin || isGlobalMod) {
|
|
35
|
+
return { allowed: true };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if user can read the topic (NodeBB privilege system)
|
|
39
|
+
const canRead = await privileges.topics.can('topics:read', tid, uid);
|
|
40
|
+
if (!canRead) {
|
|
41
|
+
return { allowed: false, reason: 'Access denied' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Verify the PDF filename exists in one of the topic's posts
|
|
45
|
+
const exists = await TopicAccess.pdfExistsInTopic(tid, filename);
|
|
46
|
+
if (!exists) {
|
|
47
|
+
return { allowed: false, reason: 'Access denied' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { allowed: true };
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// DB error, topic not found, etc. — deny by default
|
|
53
|
+
return { allowed: false, reason: 'Access check failed' };
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a PDF filename is referenced in any post of a topic.
|
|
59
|
+
* Searches both the main post and all reply posts.
|
|
60
|
+
*
|
|
61
|
+
* @param {number} tid - Topic ID
|
|
62
|
+
* @param {string} filename - PDF filename to search for
|
|
63
|
+
* @returns {Promise<boolean>}
|
|
64
|
+
*/
|
|
65
|
+
TopicAccess.pdfExistsInTopic = async function (tid, filename) {
|
|
66
|
+
// Get the topic's main post ID
|
|
67
|
+
const topicData = await topics.getTopicFields(tid, ['mainPid']);
|
|
68
|
+
if (!topicData || !topicData.mainPid) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Get all post IDs in this topic (replies)
|
|
73
|
+
const replyPids = await db.getSortedSetRange('tid:' + tid + ':posts', 0, -1);
|
|
74
|
+
const allPids = [topicData.mainPid, ...replyPids].filter(Boolean);
|
|
75
|
+
|
|
76
|
+
if (allPids.length === 0) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Get raw content of all posts
|
|
81
|
+
const postsData = await posts.getPostsFields(allPids, ['content']);
|
|
82
|
+
|
|
83
|
+
// Escape filename for regex safety, also match URL-encoded variant
|
|
84
|
+
// (post content may store "Özel Döküman.pdf" as "%C3%96zel%20D%C3%B6k%C3%BCman.pdf")
|
|
85
|
+
const escaped = filename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
86
|
+
const encodedEscaped = encodeURIComponent(filename).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
87
|
+
const pattern = new RegExp('(' + escaped + '|' + encodedEscaped + ')', 'i');
|
|
88
|
+
|
|
89
|
+
for (const post of postsData) {
|
|
90
|
+
if (post && post.content && pattern.test(post.content)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return false;
|
|
96
|
+
};
|
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/plugin.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;
|
|
@@ -937,6 +1079,108 @@
|
|
|
937
1079
|
margin-right: 2px;
|
|
938
1080
|
}
|
|
939
1081
|
|
|
1082
|
+
/* AI message action buttons (copy, regenerate) */
|
|
1083
|
+
.chatMsgActions {
|
|
1084
|
+
display: flex;
|
|
1085
|
+
gap: 8px;
|
|
1086
|
+
margin-top: 6px;
|
|
1087
|
+
opacity: 0;
|
|
1088
|
+
transition: opacity 0.15s;
|
|
1089
|
+
}
|
|
1090
|
+
.chatMsg.ai:hover .chatMsgActions { opacity: 1; }
|
|
1091
|
+
.chatMsgAction {
|
|
1092
|
+
background: none;
|
|
1093
|
+
border: none;
|
|
1094
|
+
color: var(--text-secondary);
|
|
1095
|
+
font-size: 11px;
|
|
1096
|
+
cursor: pointer;
|
|
1097
|
+
display: flex;
|
|
1098
|
+
align-items: center;
|
|
1099
|
+
gap: 3px;
|
|
1100
|
+
padding: 2px 4px;
|
|
1101
|
+
border-radius: 4px;
|
|
1102
|
+
transition: color 0.15s, background 0.15s;
|
|
1103
|
+
}
|
|
1104
|
+
.chatMsgAction:hover { color: var(--text-primary); background: rgba(255,255,255,0.08); }
|
|
1105
|
+
.chatMsgAction.copied { color: #4ade80; }
|
|
1106
|
+
.chatMsgAction svg { width: 12px; height: 12px; fill: currentColor; }
|
|
1107
|
+
|
|
1108
|
+
/* AI badge timestamp */
|
|
1109
|
+
.aiBadgeTime { font-weight: 400; opacity: 0.6; }
|
|
1110
|
+
|
|
1111
|
+
/* Scroll to bottom button */
|
|
1112
|
+
.chatScrollDown {
|
|
1113
|
+
position: absolute;
|
|
1114
|
+
bottom: 80px;
|
|
1115
|
+
left: 50%;
|
|
1116
|
+
transform: translateX(-50%);
|
|
1117
|
+
width: 32px; height: 32px;
|
|
1118
|
+
border-radius: 50%;
|
|
1119
|
+
background: var(--bg-secondary);
|
|
1120
|
+
border: 1px solid var(--border-color);
|
|
1121
|
+
color: var(--text-primary);
|
|
1122
|
+
font-size: 16px;
|
|
1123
|
+
cursor: pointer;
|
|
1124
|
+
opacity: 0;
|
|
1125
|
+
pointer-events: none;
|
|
1126
|
+
transition: opacity 0.2s;
|
|
1127
|
+
z-index: 5;
|
|
1128
|
+
display: flex;
|
|
1129
|
+
align-items: center;
|
|
1130
|
+
justify-content: center;
|
|
1131
|
+
}
|
|
1132
|
+
.chatScrollDown:hover { background: var(--bg-tertiary); }
|
|
1133
|
+
.chatScrollDown.visible { opacity: 1; pointer-events: auto; }
|
|
1134
|
+
|
|
1135
|
+
/* Blockquote in AI messages */
|
|
1136
|
+
.chatMsg blockquote {
|
|
1137
|
+
border-left: 3px solid var(--accent);
|
|
1138
|
+
margin: 6px 0;
|
|
1139
|
+
padding: 4px 10px;
|
|
1140
|
+
color: var(--text-secondary);
|
|
1141
|
+
font-style: italic;
|
|
1142
|
+
background: rgba(255,255,255,0.03);
|
|
1143
|
+
border-radius: 0 4px 4px 0;
|
|
1144
|
+
}
|
|
1145
|
+
body[data-tier="vip"] .chatMsg blockquote { border-left-color: #ffd700; }
|
|
1146
|
+
|
|
1147
|
+
/* Table overflow scroll hint */
|
|
1148
|
+
.chatMsg .tableWrap.scrollable::after {
|
|
1149
|
+
content: '';
|
|
1150
|
+
position: absolute;
|
|
1151
|
+
top: 0; right: 0; bottom: 0;
|
|
1152
|
+
width: 20px;
|
|
1153
|
+
background: linear-gradient(to right, transparent, var(--bg-tertiary));
|
|
1154
|
+
pointer-events: none;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/* Loading elapsed timer */
|
|
1158
|
+
.chatLoadingElapsed { font-size: 10px; color: var(--text-secondary); opacity: 0.5; margin-left: 4px; }
|
|
1159
|
+
|
|
1160
|
+
/* Detail mode toggle */
|
|
1161
|
+
.vipTipsRow { display: flex; justify-content: space-between; align-items: center; }
|
|
1162
|
+
.detailToggle { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
|
|
1163
|
+
.detailToggle input { display: none; }
|
|
1164
|
+
.detailToggleSlider {
|
|
1165
|
+
width: 28px; height: 16px;
|
|
1166
|
+
background: rgba(255,255,255,0.15);
|
|
1167
|
+
border-radius: 8px;
|
|
1168
|
+
position: relative;
|
|
1169
|
+
transition: background 0.2s;
|
|
1170
|
+
}
|
|
1171
|
+
.detailToggleSlider::after {
|
|
1172
|
+
content: '';
|
|
1173
|
+
position: absolute; left: 2px; top: 2px;
|
|
1174
|
+
width: 12px; height: 12px;
|
|
1175
|
+
background: var(--text-secondary);
|
|
1176
|
+
border-radius: 50%;
|
|
1177
|
+
transition: transform 0.2s, background 0.2s;
|
|
1178
|
+
}
|
|
1179
|
+
.detailToggle input:checked + .detailToggleSlider { background: rgba(255,215,0,0.3); }
|
|
1180
|
+
.detailToggle input:checked + .detailToggleSlider::after { transform: translateX(12px); background: #ffd700; }
|
|
1181
|
+
.detailToggleText { font-size: 10px; color: var(--text-secondary); font-weight: 500; transition: color 0.2s; }
|
|
1182
|
+
.detailToggle input:checked ~ .detailToggleText { color: #ffd700; }
|
|
1183
|
+
|
|
940
1184
|
/* Character counter */
|
|
941
1185
|
.chatCharCount {
|
|
942
1186
|
font-size: 10px;
|
|
@@ -2868,7 +3112,25 @@
|
|
|
2868
3112
|
</button>
|
|
2869
3113
|
<button class="closeBtn" id="closeChatSidebar">×</button>
|
|
2870
3114
|
</div>
|
|
3115
|
+
<div class="vipTipsBanner" id="vipTipsBanner">
|
|
3116
|
+
<div class="vipTipsRow">
|
|
3117
|
+
<div class="vipTipsLabel">VIP — Benden isteyebileceklerin:</div>
|
|
3118
|
+
<label class="detailToggle" id="detailToggleLabel">
|
|
3119
|
+
<input type="checkbox" id="detailToggle">
|
|
3120
|
+
<span class="detailToggleSlider"></span>
|
|
3121
|
+
<span class="detailToggleText">Detaylı</span>
|
|
3122
|
+
</label>
|
|
3123
|
+
</div>
|
|
3124
|
+
<div class="vipTipsItems">
|
|
3125
|
+
<span class="vipTip">detayli aciklama</span>
|
|
3126
|
+
<span class="vipTip">ornek ver</span>
|
|
3127
|
+
<span class="vipTip">sinav sorusu</span>
|
|
3128
|
+
<span class="vipTip">karsilastirma tablosu</span>
|
|
3129
|
+
<span class="vipTip">ezber teknigi</span>
|
|
3130
|
+
</div>
|
|
3131
|
+
</div>
|
|
2871
3132
|
<div id="chatMessages"></div>
|
|
3133
|
+
<button id="chatScrollDown" class="chatScrollDown">↓</button>
|
|
2872
3134
|
<div class="chatCharCount" id="chatCharCount"></div>
|
|
2873
3135
|
<div class="chatQuotaBar" id="chatQuotaBar">
|
|
2874
3136
|
<div class="chatQuotaTrack"><div class="chatQuotaFill" id="chatQuotaFill"></div></div>
|
|
@@ -3824,6 +4086,17 @@
|
|
|
3824
4086
|
document.body.setAttribute('data-tier', 'vip');
|
|
3825
4087
|
}
|
|
3826
4088
|
|
|
4089
|
+
// Detail mode toggle (VIP only)
|
|
4090
|
+
var chatDetailMode = false;
|
|
4091
|
+
var detailToggleEl = document.getElementById('detailToggle');
|
|
4092
|
+
if (chatIsVip && detailToggleEl) {
|
|
4093
|
+
try { chatDetailMode = localStorage.getItem('pdfChat_detailMode') === 'true'; detailToggleEl.checked = chatDetailMode; } catch (e) { /* localStorage unavailable */ }
|
|
4094
|
+
detailToggleEl.addEventListener('change', function () {
|
|
4095
|
+
chatDetailMode = detailToggleEl.checked;
|
|
4096
|
+
try { localStorage.setItem('pdfChat_detailMode', String(chatDetailMode)); } catch (e) { /* ignore */ }
|
|
4097
|
+
});
|
|
4098
|
+
}
|
|
4099
|
+
|
|
3827
4100
|
// Non-premium: hide input area entirely and show PRO badge
|
|
3828
4101
|
if (!chatIsPremium) {
|
|
3829
4102
|
document.getElementById('chatInputArea').style.display = 'none';
|
|
@@ -3858,6 +4131,18 @@
|
|
|
3858
4131
|
};
|
|
3859
4132
|
}
|
|
3860
4133
|
|
|
4134
|
+
// Scroll-to-bottom button
|
|
4135
|
+
var chatScrollDownBtn = document.getElementById('chatScrollDown');
|
|
4136
|
+
if (chatScrollDownBtn) {
|
|
4137
|
+
chatMessagesEl.addEventListener('scroll', function () {
|
|
4138
|
+
var atBottom = chatMessagesEl.scrollHeight - chatMessagesEl.scrollTop - chatMessagesEl.clientHeight < 60;
|
|
4139
|
+
chatScrollDownBtn.classList.toggle('visible', !atBottom);
|
|
4140
|
+
});
|
|
4141
|
+
chatScrollDownBtn.onclick = function () {
|
|
4142
|
+
chatMessagesEl.scrollTo({ top: chatMessagesEl.scrollHeight, behavior: 'smooth' });
|
|
4143
|
+
};
|
|
4144
|
+
}
|
|
4145
|
+
|
|
3861
4146
|
// MutationObserver: prevent dangerous elements in chat (DevTools injection defense)
|
|
3862
4147
|
var dangerousTags = ['A', 'SCRIPT', 'IFRAME', 'FORM', 'OBJECT', 'EMBED', 'LINK'];
|
|
3863
4148
|
new MutationObserver(function (mutations) {
|
|
@@ -3901,6 +4186,25 @@
|
|
|
3901
4186
|
|
|
3902
4187
|
chatBtnEl.onclick = toggleChat;
|
|
3903
4188
|
|
|
4189
|
+
// VIP tip chips — click to populate chat input
|
|
4190
|
+
var vipTipChips = document.querySelectorAll('.vipTip');
|
|
4191
|
+
var vipTipPrompts = {
|
|
4192
|
+
'detayli aciklama': 'Bu konuyu detayli aciklar misin?',
|
|
4193
|
+
'ornek ver': 'Bununla ilgili somut bir ornek verir misin?',
|
|
4194
|
+
'sinav sorusu': 'Bu konudan cikmis veya cikabilecek sinav sorulari neler?',
|
|
4195
|
+
'karsilastirma tablosu': 'Bu konudaki kavramlari karsilastirma tablosu ile goster',
|
|
4196
|
+
'ezber teknigi': 'Bu konuyu kolayca ezberlemek icin teknik onerir misin?',
|
|
4197
|
+
};
|
|
4198
|
+
vipTipChips.forEach(function (chip) {
|
|
4199
|
+
chip.onclick = function () {
|
|
4200
|
+
var prompt = vipTipPrompts[chip.textContent.trim()];
|
|
4201
|
+
if (prompt && chatInputEl) {
|
|
4202
|
+
chatInputEl.value = prompt;
|
|
4203
|
+
sendChatMessage();
|
|
4204
|
+
}
|
|
4205
|
+
};
|
|
4206
|
+
});
|
|
4207
|
+
|
|
3904
4208
|
closeChatBtn.onclick = () => {
|
|
3905
4209
|
chatSidebarEl.classList.remove('open');
|
|
3906
4210
|
chatBtnEl.classList.remove('active');
|
|
@@ -3917,12 +4221,19 @@
|
|
|
3917
4221
|
|
|
3918
4222
|
var frag = document.createDocumentFragment();
|
|
3919
4223
|
|
|
3920
|
-
// Split into code blocks and normal text
|
|
3921
|
-
var parts = text.split(/(```[\s\S]
|
|
4224
|
+
// Split into code blocks, LaTeX blocks ($$...$$), and normal text
|
|
4225
|
+
var parts = text.split(/(```[\s\S]*?```|\$\$[\s\S]*?\$\$)/g);
|
|
3922
4226
|
for (var pi = 0; pi < parts.length; pi++) {
|
|
3923
4227
|
var part = parts[pi];
|
|
3924
4228
|
if (!part) continue;
|
|
3925
4229
|
|
|
4230
|
+
// LaTeX block: $$...$$
|
|
4231
|
+
if (part.startsWith('$$') && part.endsWith('$$') && part.length > 4) {
|
|
4232
|
+
var latex = part.slice(2, -2).trim();
|
|
4233
|
+
frag.appendChild(renderMath(latex, true));
|
|
4234
|
+
continue;
|
|
4235
|
+
}
|
|
4236
|
+
|
|
3926
4237
|
// Code block
|
|
3927
4238
|
if (part.startsWith('```') && part.endsWith('```')) {
|
|
3928
4239
|
var codeContent = part.slice(3, -3);
|
|
@@ -3935,31 +4246,267 @@
|
|
|
3935
4246
|
var code = document.createElement('code');
|
|
3936
4247
|
code.textContent = codeContent.trim();
|
|
3937
4248
|
pre.appendChild(code);
|
|
4249
|
+
pre.appendChild(createCopyBtn(codeContent.trim()));
|
|
3938
4250
|
frag.appendChild(pre);
|
|
3939
4251
|
continue;
|
|
3940
4252
|
}
|
|
3941
4253
|
|
|
3942
|
-
// Normal text — process inline formatting
|
|
3943
|
-
|
|
4254
|
+
// Normal text — process block and inline formatting
|
|
4255
|
+
renderBlocks(part, frag);
|
|
3944
4256
|
}
|
|
3945
4257
|
return frag;
|
|
3946
4258
|
}
|
|
3947
4259
|
|
|
3948
|
-
|
|
3949
|
-
|
|
4260
|
+
// Block-level markdown: tables, lists, headers, HR
|
|
4261
|
+
// Falls back to inline formatting for regular text
|
|
4262
|
+
function renderBlocks(text, parent) {
|
|
3950
4263
|
var lines = text.split('\n');
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
var
|
|
3959
|
-
|
|
3960
|
-
|
|
4264
|
+
var i = 0;
|
|
4265
|
+
var paraBuf = [];
|
|
4266
|
+
var hasOutput = false; // track if we've rendered anything yet
|
|
4267
|
+
|
|
4268
|
+
function flushPara() {
|
|
4269
|
+
if (paraBuf.length === 0) return;
|
|
4270
|
+
if (hasOutput) parent.appendChild(document.createElement('br'));
|
|
4271
|
+
for (var pi = 0; pi < paraBuf.length; pi++) {
|
|
4272
|
+
if (pi > 0) parent.appendChild(document.createElement('br'));
|
|
4273
|
+
var tokens = tokenizeInline(paraBuf[pi]);
|
|
4274
|
+
for (var t = 0; t < tokens.length; t++) {
|
|
4275
|
+
parent.appendChild(tokens[t]);
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
paraBuf = [];
|
|
4279
|
+
hasOutput = true;
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
while (i < lines.length) {
|
|
4283
|
+
var trimmed = lines[i].trim();
|
|
4284
|
+
|
|
4285
|
+
// Empty line — flush paragraph buffer
|
|
4286
|
+
if (!trimmed) { flushPara(); i++; continue; }
|
|
4287
|
+
|
|
4288
|
+
// Horizontal rule: --- or *** or ___
|
|
4289
|
+
if (/^[-*_]{3,}\s*$/.test(trimmed) && !/\S/.test(trimmed.replace(/[-*_]/g, ''))) {
|
|
4290
|
+
flushPara();
|
|
4291
|
+
parent.appendChild(document.createElement('hr'));
|
|
4292
|
+
hasOutput = true;
|
|
4293
|
+
i++; continue;
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
// Blockquote: > text
|
|
4297
|
+
if (trimmed.startsWith('> ')) {
|
|
4298
|
+
flushPara();
|
|
4299
|
+
var bq = document.createElement('blockquote');
|
|
4300
|
+
var bqTokens = tokenizeInline(trimmed.slice(2));
|
|
4301
|
+
for (var t = 0; t < bqTokens.length; t++) bq.appendChild(bqTokens[t]);
|
|
4302
|
+
parent.appendChild(bq);
|
|
4303
|
+
hasOutput = true;
|
|
4304
|
+
i++; continue;
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
// Header: # ## ### ####
|
|
4308
|
+
var hMatch = trimmed.match(/^(#{1,4})\s+(.+)/);
|
|
4309
|
+
if (hMatch) {
|
|
4310
|
+
flushPara();
|
|
4311
|
+
var hEl = document.createElement('h' + Math.min(hMatch[1].length + 2, 5));
|
|
4312
|
+
var hTokens = tokenizeInline(hMatch[2]);
|
|
4313
|
+
for (var t = 0; t < hTokens.length; t++) hEl.appendChild(hTokens[t]);
|
|
4314
|
+
parent.appendChild(hEl);
|
|
4315
|
+
hasOutput = true;
|
|
4316
|
+
i++; continue;
|
|
4317
|
+
}
|
|
4318
|
+
|
|
4319
|
+
// Table: lines containing | with a separator row
|
|
4320
|
+
if (trimmed.includes('|') && i + 1 < lines.length) {
|
|
4321
|
+
var tLines = [];
|
|
4322
|
+
var j = i;
|
|
4323
|
+
while (j < lines.length && lines[j].trim().includes('|')) {
|
|
4324
|
+
tLines.push(lines[j].trim());
|
|
4325
|
+
j++;
|
|
4326
|
+
}
|
|
4327
|
+
if (tLines.length >= 2 && /^\|?[\s:|-]+\|?$/.test(tLines[1]) && tLines[1].includes('-')) {
|
|
4328
|
+
flushPara();
|
|
4329
|
+
var table = renderTable(tLines);
|
|
4330
|
+
if (table) {
|
|
4331
|
+
var wrap = document.createElement('div');
|
|
4332
|
+
wrap.className = 'tableWrap';
|
|
4333
|
+
wrap.appendChild(table);
|
|
4334
|
+
wrap.appendChild(createCopyBtn(tableToText(table)));
|
|
4335
|
+
// Detect horizontal overflow for scroll hint
|
|
4336
|
+
requestAnimationFrame(function () {
|
|
4337
|
+
if (wrap.scrollWidth > wrap.clientWidth) wrap.classList.add('scrollable');
|
|
4338
|
+
});
|
|
4339
|
+
wrap.addEventListener('scroll', function () {
|
|
4340
|
+
var atEnd = wrap.scrollLeft + wrap.clientWidth >= wrap.scrollWidth - 5;
|
|
4341
|
+
wrap.classList.toggle('scrollable', !atEnd);
|
|
4342
|
+
});
|
|
4343
|
+
parent.appendChild(wrap);
|
|
4344
|
+
hasOutput = true;
|
|
4345
|
+
i = j; continue;
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
|
|
4350
|
+
// Unordered list: - item or * item (but not ---)
|
|
4351
|
+
if (/^[-*]\s+/.test(trimmed)) {
|
|
4352
|
+
flushPara();
|
|
4353
|
+
var ul = document.createElement('ul');
|
|
4354
|
+
while (i < lines.length) {
|
|
4355
|
+
var lt = lines[i].trim();
|
|
4356
|
+
var lm = lt.match(/^[-*]\s+(.*)/);
|
|
4357
|
+
if (lm) {
|
|
4358
|
+
var li2 = document.createElement('li');
|
|
4359
|
+
var liTokens = tokenizeInline(lm[1]);
|
|
4360
|
+
for (var t = 0; t < liTokens.length; t++) li2.appendChild(liTokens[t]);
|
|
4361
|
+
ul.appendChild(li2);
|
|
4362
|
+
i++;
|
|
4363
|
+
} else if (!lt) {
|
|
4364
|
+
i++;
|
|
4365
|
+
if (i < lines.length && /^[-*]\s+/.test(lines[i].trim())) continue;
|
|
4366
|
+
break;
|
|
4367
|
+
} else { break; }
|
|
4368
|
+
}
|
|
4369
|
+
parent.appendChild(ul);
|
|
4370
|
+
hasOutput = true;
|
|
4371
|
+
continue;
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
// Ordered list: 1. item or 1) item
|
|
4375
|
+
if (/^\d+[.)]\s+/.test(trimmed)) {
|
|
4376
|
+
flushPara();
|
|
4377
|
+
var ol = document.createElement('ol');
|
|
4378
|
+
while (i < lines.length) {
|
|
4379
|
+
var lt = lines[i].trim();
|
|
4380
|
+
var om = lt.match(/^\d+[.)]\s+(.*)/);
|
|
4381
|
+
if (om) {
|
|
4382
|
+
var li3 = document.createElement('li');
|
|
4383
|
+
var oTokens = tokenizeInline(om[1]);
|
|
4384
|
+
for (var t = 0; t < oTokens.length; t++) li3.appendChild(oTokens[t]);
|
|
4385
|
+
ol.appendChild(li3);
|
|
4386
|
+
i++;
|
|
4387
|
+
} else if (!lt) {
|
|
4388
|
+
i++;
|
|
4389
|
+
if (i < lines.length && /^\d+[.)]\s+/.test(lines[i].trim())) continue;
|
|
4390
|
+
break;
|
|
4391
|
+
} else { break; }
|
|
4392
|
+
}
|
|
4393
|
+
parent.appendChild(ol);
|
|
4394
|
+
hasOutput = true;
|
|
4395
|
+
continue;
|
|
3961
4396
|
}
|
|
4397
|
+
|
|
4398
|
+
// Regular text line — buffer for paragraph
|
|
4399
|
+
paraBuf.push(trimmed);
|
|
4400
|
+
i++;
|
|
4401
|
+
}
|
|
4402
|
+
flushPara();
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4405
|
+
function renderTable(tLines) {
|
|
4406
|
+
var headerCells = parseTableRow(tLines[0]);
|
|
4407
|
+
if (!headerCells || headerCells.length === 0) return null;
|
|
4408
|
+
|
|
4409
|
+
// Parse alignments from separator row
|
|
4410
|
+
var sepCells = parseTableRow(tLines[1]);
|
|
4411
|
+
var aligns = sepCells ? sepCells.map(function (c) {
|
|
4412
|
+
c = c.trim();
|
|
4413
|
+
if (c.charAt(0) === ':' && c.charAt(c.length - 1) === ':') return 'center';
|
|
4414
|
+
if (c.charAt(c.length - 1) === ':') return 'right';
|
|
4415
|
+
return '';
|
|
4416
|
+
}) : [];
|
|
4417
|
+
|
|
4418
|
+
var table = document.createElement('table');
|
|
4419
|
+
var thead = document.createElement('thead');
|
|
4420
|
+
var hRow = document.createElement('tr');
|
|
4421
|
+
for (var c = 0; c < headerCells.length; c++) {
|
|
4422
|
+
var th = document.createElement('th');
|
|
4423
|
+
if (aligns[c]) th.style.textAlign = aligns[c];
|
|
4424
|
+
var tokens = tokenizeInline(headerCells[c].trim());
|
|
4425
|
+
for (var t = 0; t < tokens.length; t++) th.appendChild(tokens[t]);
|
|
4426
|
+
hRow.appendChild(th);
|
|
4427
|
+
}
|
|
4428
|
+
thead.appendChild(hRow);
|
|
4429
|
+
table.appendChild(thead);
|
|
4430
|
+
|
|
4431
|
+
var tbody = document.createElement('tbody');
|
|
4432
|
+
for (var r = 2; r < tLines.length; r++) {
|
|
4433
|
+
var cells = parseTableRow(tLines[r]);
|
|
4434
|
+
if (!cells) continue;
|
|
4435
|
+
var tr = document.createElement('tr');
|
|
4436
|
+
for (var c = 0; c < cells.length; c++) {
|
|
4437
|
+
var td = document.createElement('td');
|
|
4438
|
+
if (aligns[c]) td.style.textAlign = aligns[c];
|
|
4439
|
+
var tokens = tokenizeInline((cells[c] || '').trim());
|
|
4440
|
+
for (var t = 0; t < tokens.length; t++) td.appendChild(tokens[t]);
|
|
4441
|
+
tr.appendChild(td);
|
|
4442
|
+
}
|
|
4443
|
+
tbody.appendChild(tr);
|
|
4444
|
+
}
|
|
4445
|
+
table.appendChild(tbody);
|
|
4446
|
+
return table;
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4449
|
+
function parseTableRow(line) {
|
|
4450
|
+
line = line.trim();
|
|
4451
|
+
if (line.charAt(0) === '|') line = line.slice(1);
|
|
4452
|
+
if (line.charAt(line.length - 1) === '|') line = line.slice(0, -1);
|
|
4453
|
+
if (!line) return null;
|
|
4454
|
+
return line.split('|');
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4457
|
+
// Copy button helper (used for code blocks and tables)
|
|
4458
|
+
function createCopyBtn(textToCopy) {
|
|
4459
|
+
var btn = document.createElement('button');
|
|
4460
|
+
btn.className = 'chatCopyBtn';
|
|
4461
|
+
btn.textContent = 'Kopyala';
|
|
4462
|
+
btn.onclick = function (e) {
|
|
4463
|
+
e.stopPropagation();
|
|
4464
|
+
navigator.clipboard.writeText(textToCopy).then(function () {
|
|
4465
|
+
btn.textContent = 'Kopyalandi!';
|
|
4466
|
+
btn.classList.add('copied');
|
|
4467
|
+
setTimeout(function () {
|
|
4468
|
+
btn.textContent = 'Kopyala';
|
|
4469
|
+
btn.classList.remove('copied');
|
|
4470
|
+
}, 1500);
|
|
4471
|
+
}).catch(function () {
|
|
4472
|
+
btn.textContent = 'Hata!';
|
|
4473
|
+
setTimeout(function () { btn.textContent = 'Kopyala'; }, 1500);
|
|
4474
|
+
});
|
|
4475
|
+
};
|
|
4476
|
+
return btn;
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
// Convert table element to tab-separated text for clipboard
|
|
4480
|
+
function tableToText(table) {
|
|
4481
|
+
var lines = [];
|
|
4482
|
+
var rows = table.querySelectorAll('tr');
|
|
4483
|
+
for (var r = 0; r < rows.length; r++) {
|
|
4484
|
+
var cells = rows[r].querySelectorAll('th, td');
|
|
4485
|
+
var line = [];
|
|
4486
|
+
for (var c = 0; c < cells.length; c++) {
|
|
4487
|
+
line.push(cells[c].textContent.trim());
|
|
4488
|
+
}
|
|
4489
|
+
lines.push(line.join('\t'));
|
|
4490
|
+
}
|
|
4491
|
+
return lines.join('\n');
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4494
|
+
// Render LaTeX math expression (inline or block)
|
|
4495
|
+
function renderMath(latex, displayMode) {
|
|
4496
|
+
if (typeof katex === 'undefined') {
|
|
4497
|
+
// KaTeX not loaded, show raw LaTeX
|
|
4498
|
+
var fallback = document.createElement('code');
|
|
4499
|
+
fallback.textContent = (displayMode ? '$$' : '$') + latex + (displayMode ? '$$' : '$');
|
|
4500
|
+
return fallback;
|
|
3962
4501
|
}
|
|
4502
|
+
var container = document.createElement('span');
|
|
4503
|
+
container.className = displayMode ? 'katex-block' : 'katex-inline';
|
|
4504
|
+
try {
|
|
4505
|
+
katex.render(latex, container, { displayMode: displayMode, throwOnError: false });
|
|
4506
|
+
} catch (e) {
|
|
4507
|
+
container.textContent = latex;
|
|
4508
|
+
}
|
|
4509
|
+
return container;
|
|
3963
4510
|
}
|
|
3964
4511
|
|
|
3965
4512
|
function tokenizeInline(line) {
|
|
@@ -4000,6 +4547,26 @@
|
|
|
4000
4547
|
}
|
|
4001
4548
|
|
|
4002
4549
|
while (i < line.length) {
|
|
4550
|
+
// Block math: $$...$$
|
|
4551
|
+
if (line[i] === '$' && line[i + 1] === '$') {
|
|
4552
|
+
var endM = line.indexOf('$$', i + 2);
|
|
4553
|
+
if (endM !== -1) {
|
|
4554
|
+
flushBuf();
|
|
4555
|
+
nodes.push(renderMath(line.slice(i + 2, endM), true));
|
|
4556
|
+
i = endM + 2;
|
|
4557
|
+
continue;
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
// Inline math: $...$ (but not $$)
|
|
4561
|
+
if (line[i] === '$' && line[i + 1] !== '$') {
|
|
4562
|
+
var endM2 = line.indexOf('$', i + 1);
|
|
4563
|
+
if (endM2 !== -1 && endM2 > i + 1) {
|
|
4564
|
+
flushBuf();
|
|
4565
|
+
nodes.push(renderMath(line.slice(i + 1, endM2), false));
|
|
4566
|
+
i = endM2 + 1;
|
|
4567
|
+
continue;
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4003
4570
|
// Inline code: `...`
|
|
4004
4571
|
if (line[i] === '`') {
|
|
4005
4572
|
var end = line.indexOf('`', i + 1);
|
|
@@ -4036,6 +4603,18 @@
|
|
|
4036
4603
|
continue;
|
|
4037
4604
|
}
|
|
4038
4605
|
}
|
|
4606
|
+
// Strikethrough: ~~...~~
|
|
4607
|
+
if (line[i] === '~' && line[i + 1] === '~') {
|
|
4608
|
+
var endS = line.indexOf('~~', i + 2);
|
|
4609
|
+
if (endS !== -1 && endS - i < 5002) {
|
|
4610
|
+
flushBuf();
|
|
4611
|
+
var del = document.createElement('del');
|
|
4612
|
+
del.textContent = line.slice(i + 2, endS);
|
|
4613
|
+
nodes.push(del);
|
|
4614
|
+
i = endS + 2;
|
|
4615
|
+
continue;
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4039
4618
|
buf += line[i];
|
|
4040
4619
|
i++;
|
|
4041
4620
|
}
|
|
@@ -4057,6 +4636,11 @@
|
|
|
4057
4636
|
svg.appendChild(path);
|
|
4058
4637
|
badge.appendChild(svg);
|
|
4059
4638
|
badge.appendChild(document.createTextNode(' AI'));
|
|
4639
|
+
var ts = document.createElement('span');
|
|
4640
|
+
ts.className = 'aiBadgeTime';
|
|
4641
|
+
var now = new Date();
|
|
4642
|
+
ts.textContent = ' \u2022 ' + now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
|
|
4643
|
+
badge.appendChild(ts);
|
|
4060
4644
|
return badge;
|
|
4061
4645
|
}
|
|
4062
4646
|
|
|
@@ -4072,6 +4656,32 @@
|
|
|
4072
4656
|
}
|
|
4073
4657
|
msg.appendChild(createAiBadge());
|
|
4074
4658
|
msg.appendChild(renderMarkdownSafe(text));
|
|
4659
|
+
|
|
4660
|
+
// Action buttons (regenerate)
|
|
4661
|
+
var actions = document.createElement('div');
|
|
4662
|
+
actions.className = 'chatMsgActions';
|
|
4663
|
+
|
|
4664
|
+
var regenBtn = document.createElement('button');
|
|
4665
|
+
regenBtn.className = 'chatMsgAction';
|
|
4666
|
+
regenBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> Tekrar sor';
|
|
4667
|
+
regenBtn.onclick = function () {
|
|
4668
|
+
if (chatHistory.length >= 2) {
|
|
4669
|
+
var lastQ = chatHistory[chatHistory.length - 2];
|
|
4670
|
+
if (lastQ.role === 'user') {
|
|
4671
|
+
chatHistory.pop();
|
|
4672
|
+
chatHistory.pop();
|
|
4673
|
+
var msgs = chatMessagesEl.querySelectorAll('.chatMsg');
|
|
4674
|
+
if (msgs.length >= 2) {
|
|
4675
|
+
msgs[msgs.length - 1].remove();
|
|
4676
|
+
msgs[msgs.length - 2].remove();
|
|
4677
|
+
}
|
|
4678
|
+
chatInputEl.value = lastQ.text;
|
|
4679
|
+
sendChatMessage();
|
|
4680
|
+
}
|
|
4681
|
+
}
|
|
4682
|
+
};
|
|
4683
|
+
actions.appendChild(regenBtn);
|
|
4684
|
+
msg.appendChild(actions);
|
|
4075
4685
|
} else if (role === 'user') {
|
|
4076
4686
|
var now = new Date();
|
|
4077
4687
|
var timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
|
|
@@ -4089,18 +4699,34 @@
|
|
|
4089
4699
|
}
|
|
4090
4700
|
|
|
4091
4701
|
function showChatLoading() {
|
|
4092
|
-
|
|
4702
|
+
var loadText = chatDetailMode ? 'Detayl\u0131 analiz haz\u0131rlan\u0131yor' : 'D\u00fc\u015f\u00fcn\u00fcyor';
|
|
4703
|
+
var msg = document.createElement('div');
|
|
4093
4704
|
msg.className = 'chatMsg ai chatLoading';
|
|
4094
4705
|
msg.id = 'chatLoadingMsg';
|
|
4095
|
-
msg.innerHTML = '<span class="chatLoadingText">
|
|
4706
|
+
msg.innerHTML = '<span class="chatLoadingText">' + loadText + '</span><span class="chatLoadingDots"><span>.</span><span>.</span><span>.</span></span>';
|
|
4707
|
+
var elapsed = document.createElement('span');
|
|
4708
|
+
elapsed.className = 'chatLoadingElapsed';
|
|
4709
|
+
elapsed.style.display = 'none';
|
|
4710
|
+
msg.appendChild(elapsed);
|
|
4711
|
+
var startTime = Date.now();
|
|
4712
|
+
msg._timer = setInterval(function () {
|
|
4713
|
+
var secs = Math.floor((Date.now() - startTime) / 1000);
|
|
4714
|
+
if (secs >= 5) {
|
|
4715
|
+
elapsed.style.display = '';
|
|
4716
|
+
elapsed.textContent = '(' + secs + 's)';
|
|
4717
|
+
}
|
|
4718
|
+
}, 1000);
|
|
4096
4719
|
chatMessagesEl.appendChild(msg);
|
|
4097
4720
|
chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
|
|
4098
4721
|
return msg;
|
|
4099
4722
|
}
|
|
4100
4723
|
|
|
4101
4724
|
function removeChatLoading() {
|
|
4102
|
-
|
|
4103
|
-
if (el)
|
|
4725
|
+
var el = document.getElementById('chatLoadingMsg');
|
|
4726
|
+
if (el) {
|
|
4727
|
+
if (el._timer) clearInterval(el._timer);
|
|
4728
|
+
el.remove();
|
|
4729
|
+
}
|
|
4104
4730
|
}
|
|
4105
4731
|
|
|
4106
4732
|
// Quota usage bar
|
|
@@ -4211,9 +4837,14 @@
|
|
|
4211
4837
|
suggestions.appendChild(chip);
|
|
4212
4838
|
});
|
|
4213
4839
|
|
|
4214
|
-
//
|
|
4215
|
-
|
|
4216
|
-
|
|
4840
|
+
// VIP: auto-load context-aware suggestions immediately
|
|
4841
|
+
// Premium: lazy-load on hover/focus (avoids unnecessary API calls)
|
|
4842
|
+
if (chatIsVip) {
|
|
4843
|
+
setTimeout(loadAiSuggestions, 0);
|
|
4844
|
+
} else {
|
|
4845
|
+
suggestions.addEventListener('mouseenter', loadAiSuggestions, { once: true });
|
|
4846
|
+
suggestions.addEventListener('focusin', loadAiSuggestions, { once: true });
|
|
4847
|
+
}
|
|
4217
4848
|
|
|
4218
4849
|
chatMessagesEl.appendChild(suggestions);
|
|
4219
4850
|
chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
|
|
@@ -4400,6 +5031,7 @@
|
|
|
4400
5031
|
filename: _cfg.filename,
|
|
4401
5032
|
question: question,
|
|
4402
5033
|
history: chatHistory,
|
|
5034
|
+
detailMode: chatDetailMode,
|
|
4403
5035
|
}),
|
|
4404
5036
|
});
|
|
4405
5037
|
|
|
@@ -4424,7 +5056,16 @@
|
|
|
4424
5056
|
chatHistory = chatHistory.slice(-50);
|
|
4425
5057
|
}
|
|
4426
5058
|
} else {
|
|
4427
|
-
addChatMessage('error', data.error || 'Bir hata oluştu. Tekrar deneyin.');
|
|
5059
|
+
var errorMsg = addChatMessage('error', data.error || 'Bir hata oluştu. Tekrar deneyin.');
|
|
5060
|
+
// Show upgrade button for file size limit errors (Premium -> VIP)
|
|
5061
|
+
if (data.showUpgrade && errorMsg) {
|
|
5062
|
+
var uid = (_cfg && _cfg.uid) || 0;
|
|
5063
|
+
var upgradeBtn = document.createElement('button');
|
|
5064
|
+
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;';
|
|
5065
|
+
upgradeBtn.textContent = 'VIP\'e Y\u00fckselt';
|
|
5066
|
+
upgradeBtn.addEventListener('click', function () { window.open('https://forum.ieu.app/pay/checkout?uid=' + uid, '_blank'); });
|
|
5067
|
+
errorMsg.appendChild(upgradeBtn);
|
|
5068
|
+
}
|
|
4428
5069
|
}
|
|
4429
5070
|
} catch (err) {
|
|
4430
5071
|
removeChatLoading();
|