nodebb-plugin-pdf-secure2 1.3.1 → 1.3.2
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 +22 -13
- package/lib/gemini-chat.js +26 -6
- package/library.js +32 -4
- package/package.json +1 -1
- package/plugin.json +4 -0
- package/static/lib/admin.js +61 -1
- package/static/lib/main.js +9 -0
- package/static/templates/admin/plugins/pdf-secure.tpl +140 -52
- package/static/viewer.html +307 -34
package/lib/controllers.js
CHANGED
|
@@ -11,14 +11,16 @@ const Controllers = module.exports;
|
|
|
11
11
|
|
|
12
12
|
// Rate limiting: per-user message counter
|
|
13
13
|
const rateLimits = new Map(); // uid -> { count, windowStart }
|
|
14
|
-
const
|
|
15
|
-
|
|
14
|
+
const RATE_LIMITS = {
|
|
15
|
+
vip: { max: 50, window: 60 * 1000 },
|
|
16
|
+
premium: { max: 25, window: 60 * 1000 },
|
|
17
|
+
};
|
|
16
18
|
|
|
17
19
|
// Periodic cleanup of expired rate limit entries
|
|
18
20
|
setInterval(() => {
|
|
19
21
|
const now = Date.now();
|
|
20
22
|
for (const [uid, entry] of rateLimits.entries()) {
|
|
21
|
-
if (now - entry.windowStart >
|
|
23
|
+
if (now - entry.windowStart > RATE_LIMITS.premium.window * 2) {
|
|
22
24
|
rateLimits.delete(uid);
|
|
23
25
|
}
|
|
24
26
|
}
|
|
@@ -102,19 +104,23 @@ Controllers.handleChat = async function (req, res) {
|
|
|
102
104
|
]);
|
|
103
105
|
const isPremium = isAdmin || isGlobalMod || isPremiumMember || isVipMember;
|
|
104
106
|
if (!isPremium) {
|
|
105
|
-
return res.status(403).json({ error: '
|
|
107
|
+
return res.status(403).json({ error: 'Bu özellik Premium/VIP üyelere özeldir.' });
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
|
|
110
|
+
const isVip = isVipMember || isAdmin;
|
|
111
|
+
const tier = isVip ? 'vip' : 'premium';
|
|
112
|
+
const rateConfig = RATE_LIMITS[tier];
|
|
113
|
+
|
|
114
|
+
// Rate limiting (tier-based)
|
|
109
115
|
const now = Date.now();
|
|
110
116
|
let userRate = rateLimits.get(req.uid);
|
|
111
|
-
if (!userRate || now - userRate.windowStart >
|
|
117
|
+
if (!userRate || now - userRate.windowStart > rateConfig.window) {
|
|
112
118
|
userRate = { count: 0, windowStart: now };
|
|
113
119
|
rateLimits.set(req.uid, userRate);
|
|
114
120
|
}
|
|
115
121
|
userRate.count += 1;
|
|
116
|
-
if (userRate.count >
|
|
117
|
-
return res.status(429).json({ error: '
|
|
122
|
+
if (userRate.count > rateConfig.max) {
|
|
123
|
+
return res.status(429).json({ error: 'Çok hızlı mesaj gönderiyorsunuz. Lütfen biraz bekleyin.' });
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
// Body validation
|
|
@@ -151,20 +157,23 @@ Controllers.handleChat = async function (req, res) {
|
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
try {
|
|
154
|
-
const answer = await geminiChat.chat(safeName, trimmedQuestion, history || []);
|
|
160
|
+
const answer = await geminiChat.chat(safeName, trimmedQuestion, history || [], tier);
|
|
155
161
|
return res.json({ answer });
|
|
156
162
|
} catch (err) {
|
|
157
163
|
console.error('[PDF-Secure] Chat error:', err.message, err.status || '', err.code || '');
|
|
158
164
|
|
|
159
165
|
if (err.message === 'File not found') {
|
|
160
|
-
return res.status(404).json({ error: 'PDF
|
|
166
|
+
return res.status(404).json({ error: 'PDF bulunamadı.' });
|
|
167
|
+
}
|
|
168
|
+
if (err.message === 'PDF too large for AI chat') {
|
|
169
|
+
return res.status(413).json({ error: 'Bu PDF çok büyük. AI chat desteklemiyor.' });
|
|
161
170
|
}
|
|
162
171
|
if (err.status === 429 || err.message.includes('rate limit') || err.message.includes('quota')) {
|
|
163
|
-
return res.status(429).json({ error: 'AI
|
|
172
|
+
return res.status(429).json({ error: 'AI servisi şu an yoğun. Lütfen birkaç saniye sonra tekrar deneyin.' });
|
|
164
173
|
}
|
|
165
174
|
if (err.status === 401 || err.status === 403 || err.message.includes('API key')) {
|
|
166
|
-
return res.status(503).json({ error: 'AI
|
|
175
|
+
return res.status(503).json({ error: 'AI servisi yapılandırma hatası. Yöneticiyle iletişime geçin.' });
|
|
167
176
|
}
|
|
168
|
-
return res.status(500).json({ error: '
|
|
177
|
+
return res.status(500).json({ error: 'AI yanıt veremedi. Lütfen tekrar deneyin.' });
|
|
169
178
|
}
|
|
170
179
|
};
|
package/lib/gemini-chat.js
CHANGED
|
@@ -11,9 +11,21 @@ let ai = null;
|
|
|
11
11
|
const pdfDataCache = new Map(); // filename -> { base64, cachedAt }
|
|
12
12
|
const PDF_DATA_TTL = 30 * 60 * 1000; // 30 minutes
|
|
13
13
|
|
|
14
|
-
const SYSTEM_INSTRUCTION = `
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const SYSTEM_INSTRUCTION = `Sen bir PDF döküman asistanısın. Kurallar:
|
|
15
|
+
- Kullanıcının yazdığı dilde cevap ver
|
|
16
|
+
- Önce kısa ve net cevapla, gerekirse detay ekle
|
|
17
|
+
- Bilgiyi doğrudan PDF'ten al, sayfa/bölüm numarası belirt
|
|
18
|
+
- Bilmiyorsan "Bu bilgi dokümanda bulunamadı" de
|
|
19
|
+
- Liste/madde formatını tercih et
|
|
20
|
+
- Spekülasyon yapma, sadece dokümandaki bilgiye dayan`;
|
|
21
|
+
|
|
22
|
+
const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB
|
|
23
|
+
|
|
24
|
+
// Tier-based configuration
|
|
25
|
+
const TIER_CONFIG = {
|
|
26
|
+
vip: { maxHistory: 30, maxOutputTokens: 4096 },
|
|
27
|
+
premium: { maxHistory: 20, maxOutputTokens: 2048 },
|
|
28
|
+
};
|
|
17
29
|
|
|
18
30
|
const MODEL_NAME = 'gemini-2.5-flash';
|
|
19
31
|
|
|
@@ -59,6 +71,11 @@ async function getPdfBase64(filename) {
|
|
|
59
71
|
throw new Error('File not found');
|
|
60
72
|
}
|
|
61
73
|
|
|
74
|
+
const stats = await fs.promises.stat(filePath);
|
|
75
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
76
|
+
throw new Error('PDF too large for AI chat');
|
|
77
|
+
}
|
|
78
|
+
|
|
62
79
|
const fileBuffer = await fs.promises.readFile(filePath);
|
|
63
80
|
const base64 = fileBuffer.toString('base64');
|
|
64
81
|
|
|
@@ -66,17 +83,19 @@ async function getPdfBase64(filename) {
|
|
|
66
83
|
return base64;
|
|
67
84
|
}
|
|
68
85
|
|
|
69
|
-
GeminiChat.chat = async function (filename, question, history) {
|
|
86
|
+
GeminiChat.chat = async function (filename, question, history, tier) {
|
|
70
87
|
if (!ai) {
|
|
71
88
|
throw new Error('AI chat is not configured');
|
|
72
89
|
}
|
|
73
90
|
|
|
91
|
+
const config = TIER_CONFIG[tier] || TIER_CONFIG.premium;
|
|
74
92
|
const base64Data = await getPdfBase64(filename);
|
|
75
93
|
|
|
76
|
-
// Build conversation contents from history
|
|
94
|
+
// Build conversation contents from history (trimmed to last N entries)
|
|
77
95
|
const contents = [];
|
|
78
96
|
if (Array.isArray(history)) {
|
|
79
|
-
|
|
97
|
+
const trimmedHistory = history.slice(-config.maxHistory);
|
|
98
|
+
for (const entry of trimmedHistory) {
|
|
80
99
|
if (entry.role && entry.text) {
|
|
81
100
|
contents.push({
|
|
82
101
|
role: entry.role === 'user' ? 'user' : 'model',
|
|
@@ -113,6 +132,7 @@ GeminiChat.chat = async function (filename, question, history) {
|
|
|
113
132
|
contents: inlineContents,
|
|
114
133
|
config: {
|
|
115
134
|
systemInstruction: SYSTEM_INSTRUCTION,
|
|
135
|
+
maxOutputTokens: config.maxOutputTokens,
|
|
116
136
|
},
|
|
117
137
|
});
|
|
118
138
|
|
package/library.js
CHANGED
|
@@ -10,11 +10,13 @@ const controllers = require('./lib/controllers');
|
|
|
10
10
|
const nonceStore = require('./lib/nonce-store');
|
|
11
11
|
const pdfHandler = require('./lib/pdf-handler');
|
|
12
12
|
const geminiChat = require('./lib/gemini-chat');
|
|
13
|
+
const topics = require.main.require('./src/topics');
|
|
13
14
|
|
|
14
15
|
const plugin = {};
|
|
15
16
|
|
|
16
17
|
// Memory cache for viewer.html
|
|
17
18
|
let viewerHtmlCache = null;
|
|
19
|
+
let pluginSettings = {};
|
|
18
20
|
|
|
19
21
|
plugin.init = async (params) => {
|
|
20
22
|
const { router, middleware } = params;
|
|
@@ -61,12 +63,16 @@ plugin.init = async (params) => {
|
|
|
61
63
|
// Chat endpoint (Premium/VIP only)
|
|
62
64
|
router.post('/api/v3/plugins/pdf-secure/chat', controllers.handleChat);
|
|
63
65
|
|
|
66
|
+
// Load plugin settings
|
|
67
|
+
pluginSettings = await meta.settings.get('pdf-secure') || {};
|
|
68
|
+
|
|
64
69
|
// Initialize Gemini AI chat (if API key is configured)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
geminiChat.init(settings.geminiApiKey);
|
|
70
|
+
if (pluginSettings.geminiApiKey) {
|
|
71
|
+
geminiChat.init(pluginSettings.geminiApiKey);
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
const watermarkEnabled = pluginSettings.watermarkEnabled === 'on';
|
|
75
|
+
|
|
70
76
|
// Admin page route
|
|
71
77
|
routeHelpers.setupAdminPageRoute(router, '/admin/plugins/pdf-secure', controllers.renderAdminPage);
|
|
72
78
|
|
|
@@ -105,6 +111,7 @@ plugin.init = async (params) => {
|
|
|
105
111
|
groups.isMember(req.uid, 'Lite'),
|
|
106
112
|
]);
|
|
107
113
|
isPremium = isAdmin || isGlobalMod || isPremiumMember || isVipMember;
|
|
114
|
+
const isVip = isVipMember || isAdmin;
|
|
108
115
|
// Lite: full PDF access but restricted UI (no annotations, sidebar, etc.)
|
|
109
116
|
isLite = !isPremium && isLiteMember;
|
|
110
117
|
}
|
|
@@ -152,10 +159,12 @@ plugin.init = async (params) => {
|
|
|
152
159
|
dk: ${JSON.stringify(nonceData.dk)},
|
|
153
160
|
iv: ${JSON.stringify(nonceData.iv)},
|
|
154
161
|
isPremium: ${JSON.stringify(isPremium)},
|
|
162
|
+
isVip: ${JSON.stringify(isVip)},
|
|
155
163
|
isLite: ${JSON.stringify(isLite)},
|
|
156
164
|
uid: ${JSON.stringify(req.uid)},
|
|
157
165
|
totalPages: ${JSON.stringify(totalPages)},
|
|
158
|
-
chatEnabled: ${JSON.stringify(geminiChat.isAvailable())}
|
|
166
|
+
chatEnabled: ${JSON.stringify(geminiChat.isAvailable())},
|
|
167
|
+
watermarkEnabled: ${JSON.stringify(watermarkEnabled)}
|
|
159
168
|
};
|
|
160
169
|
</script>
|
|
161
170
|
</head>`);
|
|
@@ -221,6 +230,12 @@ plugin.filterMetaTags = async (hookData) => {
|
|
|
221
230
|
return hookData;
|
|
222
231
|
};
|
|
223
232
|
|
|
233
|
+
// Inject allowed categories into client-side config
|
|
234
|
+
plugin.filterConfig = async function (data) {
|
|
235
|
+
data.config.pdfSecureCategories = pluginSettings.allowedCategories || '';
|
|
236
|
+
return data;
|
|
237
|
+
};
|
|
238
|
+
|
|
224
239
|
// Transform PDF links to secure placeholders (server-side)
|
|
225
240
|
// This hides PDF URLs from: page source, API, RSS, ActivityPub
|
|
226
241
|
plugin.transformPdfLinks = async (data) => {
|
|
@@ -228,6 +243,19 @@ plugin.transformPdfLinks = async (data) => {
|
|
|
228
243
|
return data;
|
|
229
244
|
}
|
|
230
245
|
|
|
246
|
+
// Category check — only transform in allowed categories
|
|
247
|
+
const allowedStr = pluginSettings.allowedCategories || '';
|
|
248
|
+
const allowed = allowedStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
249
|
+
if (allowed.length > 0 && data.postData.tid) {
|
|
250
|
+
let cid = data.postData.cid;
|
|
251
|
+
if (!cid) {
|
|
252
|
+
cid = await topics.getTopicField(data.postData.tid, 'cid');
|
|
253
|
+
}
|
|
254
|
+
if (cid && !allowed.includes(String(cid))) {
|
|
255
|
+
return data;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
231
259
|
// Regex to match PDF links: <a href="...xxx.pdf">text</a>
|
|
232
260
|
// Captures: full URL path, filename, link text
|
|
233
261
|
const pdfLinkRegex = /<a\s+[^>]*href=["']([^"']*\/([^"'\/]+\.pdf))["'][^>]*>([^<]*)<\/a>/gi;
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/static/lib/admin.js
CHANGED
|
@@ -5,10 +5,17 @@ define('admin/plugins/pdf-secure', ['settings', 'alerts'], function (Settings, a
|
|
|
5
5
|
|
|
6
6
|
ACP.init = function () {
|
|
7
7
|
Settings.load('pdf-secure', $('#pdf-secure-settings'), function () {
|
|
8
|
-
|
|
8
|
+
loadCategories();
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
$('#save').on('click', function () {
|
|
12
|
+
// Collect checked category IDs into hidden input before save
|
|
13
|
+
var checked = [];
|
|
14
|
+
$('#categoryCheckboxes input[type="checkbox"]:checked').each(function () {
|
|
15
|
+
checked.push($(this).val());
|
|
16
|
+
});
|
|
17
|
+
$('#allowedCategories').val(checked.join(','));
|
|
18
|
+
|
|
12
19
|
Settings.save('pdf-secure', $('#pdf-secure-settings'), function () {
|
|
13
20
|
alerts.alert({
|
|
14
21
|
type: 'success',
|
|
@@ -19,7 +26,60 @@ define('admin/plugins/pdf-secure', ['settings', 'alerts'], function (Settings, a
|
|
|
19
26
|
});
|
|
20
27
|
});
|
|
21
28
|
});
|
|
29
|
+
|
|
30
|
+
$('#selectAllCats').on('click', function () {
|
|
31
|
+
$('#categoryCheckboxes input[type="checkbox"]').prop('checked', true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
$('#deselectAllCats').on('click', function () {
|
|
35
|
+
$('#categoryCheckboxes input[type="checkbox"]').prop('checked', false);
|
|
36
|
+
});
|
|
22
37
|
};
|
|
23
38
|
|
|
39
|
+
function loadCategories() {
|
|
40
|
+
$.getJSON(config.relative_path + '/api/categories', function (data) {
|
|
41
|
+
var categories = data.categories || [];
|
|
42
|
+
var allowed = ($('#allowedCategories').val() || '').split(',').filter(Boolean);
|
|
43
|
+
var container = $('#categoryCheckboxes');
|
|
44
|
+
container.empty();
|
|
45
|
+
|
|
46
|
+
if (!categories.length) {
|
|
47
|
+
container.html('<p class="text-muted mb-0">Kategori bulunamadi.</p>');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderCategory(cat, depth) {
|
|
52
|
+
var indent = depth * 24;
|
|
53
|
+
var isChecked = allowed.includes(String(cat.cid));
|
|
54
|
+
var iconColor = cat.bgColor || '#6c757d';
|
|
55
|
+
var iconHtml = cat.icon
|
|
56
|
+
? '<span class="d-inline-flex align-items-center justify-content-center rounded-1 me-2" style="width:22px;height:22px;background:' + iconColor + ';flex-shrink:0;"><i class="fa ' + cat.icon + '" style="font-size:11px;color:#fff;"></i></span>'
|
|
57
|
+
: '<span class="d-inline-flex align-items-center justify-content-center rounded-1 me-2" style="width:22px;height:22px;background:' + iconColor + ';flex-shrink:0;"><i class="fa fa-folder" style="font-size:11px;color:#fff;"></i></span>';
|
|
58
|
+
|
|
59
|
+
var html = '<div class="form-check py-1" style="margin-left:' + indent + 'px;">' +
|
|
60
|
+
'<input class="form-check-input" type="checkbox" value="' + cat.cid + '" id="cat_' + cat.cid + '"' + (isChecked ? ' checked' : '') + ' style="cursor:pointer;">' +
|
|
61
|
+
'<label class="form-check-label d-flex align-items-center" for="cat_' + cat.cid + '" style="cursor:pointer;">' +
|
|
62
|
+
iconHtml +
|
|
63
|
+
'<span>' + cat.name + '</span>' +
|
|
64
|
+
'<span class="text-muted ms-2" style="font-size:11px;">(' + (cat.topic_count || 0) + ' konu)</span>' +
|
|
65
|
+
'</label>' +
|
|
66
|
+
'</div>';
|
|
67
|
+
container.append(html);
|
|
68
|
+
|
|
69
|
+
if (cat.children && cat.children.length) {
|
|
70
|
+
cat.children.forEach(function (child) {
|
|
71
|
+
renderCategory(child, depth + 1);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
categories.forEach(function (cat) {
|
|
77
|
+
renderCategory(cat, 0);
|
|
78
|
+
});
|
|
79
|
+
}).fail(function () {
|
|
80
|
+
$('#categoryCheckboxes').html('<p class="text-danger mb-0">Kategoriler yuklenemedi.</p>');
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
24
84
|
return ACP;
|
|
25
85
|
});
|
package/static/lib/main.js
CHANGED
|
@@ -283,6 +283,15 @@
|
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
function interceptPdfLinks() {
|
|
286
|
+
// Category check — only intercept in allowed categories
|
|
287
|
+
var allowedCats = (config.pdfSecureCategories || '').split(',').filter(Boolean);
|
|
288
|
+
if (allowedCats.length > 0) {
|
|
289
|
+
var currentCid = ajaxify && ajaxify.data && String(ajaxify.data.cid || '');
|
|
290
|
+
if (currentCid && !allowedCats.includes(currentCid)) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
286
295
|
var postContents = document.querySelectorAll('[component="post/content"]');
|
|
287
296
|
|
|
288
297
|
postContents.forEach(function (content) {
|
|
@@ -1,52 +1,140 @@
|
|
|
1
|
-
<div class="acp-page-container">
|
|
2
|
-
<!-- IMPORT admin/partials/settings/header.tpl -->
|
|
3
|
-
|
|
4
|
-
<div class="row m-0">
|
|
5
|
-
<div id="spy-container" class="col-12
|
|
6
|
-
<form role="form" class="pdf-secure-settings" id="pdf-secure-settings">
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
</
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<div class="
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
1
|
+
<div class="acp-page-container">
|
|
2
|
+
<!-- IMPORT admin/partials/settings/header.tpl -->
|
|
3
|
+
|
|
4
|
+
<div class="row m-0">
|
|
5
|
+
<div id="spy-container" class="col-12 px-0 mb-4" tabindex="0">
|
|
6
|
+
<form role="form" class="pdf-secure-settings" id="pdf-secure-settings">
|
|
7
|
+
|
|
8
|
+
<!-- Hero Header -->
|
|
9
|
+
<div class="d-flex align-items-center gap-3 mb-4 pb-3 border-bottom">
|
|
10
|
+
<div class="d-flex align-items-center justify-content-center rounded-3" style="width:48px;height:48px;background:linear-gradient(135deg,#e81224 0%,#ff6b6b 100%);">
|
|
11
|
+
<svg viewBox="0 0 24 24" style="width:24px;height:24px;fill:#fff;"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
|
|
12
|
+
</div>
|
|
13
|
+
<div>
|
|
14
|
+
<h4 class="fw-bold mb-0">PDF Secure Viewer</h4>
|
|
15
|
+
<p class="text-muted mb-0" style="font-size:13px;">Guvenli PDF goruntuleyici, AI chat ve watermark ayarlari</p>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="row g-4">
|
|
20
|
+
<!-- Left Column: Groups & Watermark -->
|
|
21
|
+
<div class="col-12 col-lg-6">
|
|
22
|
+
<!-- Group Settings Card -->
|
|
23
|
+
<div class="card border-0 shadow-sm mb-4">
|
|
24
|
+
<div class="card-header bg-transparent border-bottom d-flex align-items-center gap-2 py-3">
|
|
25
|
+
<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:#6c757d;"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
|
|
26
|
+
<h6 class="fw-semibold mb-0">Kullanici Gruplari</h6>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="card-body">
|
|
29
|
+
<div class="mb-3">
|
|
30
|
+
<label class="form-label fw-medium" for="premiumGroup">
|
|
31
|
+
<span class="badge text-bg-primary me-1" style="font-size:10px;">PREMIUM</span>
|
|
32
|
+
Grup Adi
|
|
33
|
+
</label>
|
|
34
|
+
<input type="text" id="premiumGroup" name="premiumGroup" data-key="premiumGroup" title="Premium Group Name" class="form-control" placeholder="Premium" value="Premium">
|
|
35
|
+
<div class="form-text">Tam PDF erisimi, tum araclar ve AI chat ozelligi.</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="mb-3">
|
|
39
|
+
<label class="form-label fw-medium" for="vipGroup">
|
|
40
|
+
<span class="badge me-1" style="font-size:10px;background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff;">VIP</span>
|
|
41
|
+
Grup Adi
|
|
42
|
+
</label>
|
|
43
|
+
<input type="text" id="vipGroup" name="vipGroup" data-key="vipGroup" title="VIP Group Name" class="form-control" placeholder="VIP" value="VIP">
|
|
44
|
+
<div class="form-text">Premium ozellikleri + VIP rozeti, yuksek token/rate limitleri.</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="mb-0">
|
|
48
|
+
<label class="form-label fw-medium" for="liteGroup">
|
|
49
|
+
<span class="badge text-bg-secondary me-1" style="font-size:10px;">LITE</span>
|
|
50
|
+
Grup Adi
|
|
51
|
+
</label>
|
|
52
|
+
<input type="text" id="liteGroup" name="liteGroup" data-key="liteGroup" title="Lite Group Name" class="form-control" placeholder="Lite" value="Lite">
|
|
53
|
+
<div class="form-text">Tam PDF erisimi, sadece yakinlastirma ve tam ekran. Araclar yok.</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Watermark Card -->
|
|
59
|
+
<div class="card border-0 shadow-sm">
|
|
60
|
+
<div class="card-header bg-transparent border-bottom d-flex align-items-center gap-2 py-3">
|
|
61
|
+
<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:#6c757d;"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>
|
|
62
|
+
<h6 class="fw-semibold mb-0">Watermark</h6>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="card-body">
|
|
65
|
+
<div class="d-flex align-items-center justify-content-between">
|
|
66
|
+
<div>
|
|
67
|
+
<div class="fw-medium">Watermark Goster</div>
|
|
68
|
+
<div class="text-muted" style="font-size:13px;">PDF sayfalarinda Unicourse watermark overlay gosterir.</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="form-check form-switch mb-0 ms-3">
|
|
71
|
+
<input type="checkbox" class="form-check-input" id="watermarkEnabled" name="watermarkEnabled" data-key="watermarkEnabled" style="width:3em;height:1.5em;cursor:pointer;">
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- Right Column: AI Chat -->
|
|
79
|
+
<div class="col-12 col-lg-6">
|
|
80
|
+
<div class="card border-0 shadow-sm">
|
|
81
|
+
<div class="card-header bg-transparent border-bottom d-flex align-items-center gap-2 py-3">
|
|
82
|
+
<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:#6c757d;"><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>
|
|
83
|
+
<h6 class="fw-semibold mb-0">AI Chat (Gemini)</h6>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="card-body">
|
|
86
|
+
<div class="mb-3">
|
|
87
|
+
<label class="form-label fw-medium" for="geminiApiKey">Google Gemini API Key</label>
|
|
88
|
+
<div class="input-group">
|
|
89
|
+
<span class="input-group-text">
|
|
90
|
+
<svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:#6c757d;"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
|
|
91
|
+
</span>
|
|
92
|
+
<input type="password" id="geminiApiKey" name="geminiApiKey" data-key="geminiApiKey" title="Gemini API Key" class="form-control" placeholder="AIza...">
|
|
93
|
+
</div>
|
|
94
|
+
<div class="form-text">
|
|
95
|
+
<a href="https://ai.google.dev" target="_blank" rel="noopener">ai.google.dev</a> adresinden alinabilir.
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div class="alert alert-light border d-flex gap-2 mb-0" role="alert" style="font-size:13px;">
|
|
100
|
+
<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:#0d6efd;flex-shrink:0;margin-top:1px;"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>
|
|
101
|
+
<div>
|
|
102
|
+
<strong>Tier farklari:</strong><br>
|
|
103
|
+
<strong>Premium</strong> — 25 mesaj/dk, 2048 token, 20 gecmis<br>
|
|
104
|
+
<strong>VIP</strong> — 50 mesaj/dk, 4096 token, 30 gecmis
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- Category Selection -->
|
|
113
|
+
<div class="card border-0 shadow-sm mt-4">
|
|
114
|
+
<div class="card-header bg-transparent border-bottom d-flex align-items-center justify-content-between py-3">
|
|
115
|
+
<div class="d-flex align-items-center gap-2">
|
|
116
|
+
<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:#6c757d;"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>
|
|
117
|
+
<h6 class="fw-semibold mb-0">Aktif Kategoriler</h6>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="d-flex gap-2">
|
|
120
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" id="selectAllCats">Tumunu Sec</button>
|
|
121
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" id="deselectAllCats">Temizle</button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="card-body">
|
|
125
|
+
<p class="text-muted mb-3" style="font-size:13px;">
|
|
126
|
+
Plugin'in hangi kategorilerde calisacagini secin. Hicbiri secilmezse <strong>tum kategorilerde</strong> aktif olur.
|
|
127
|
+
</p>
|
|
128
|
+
<input type="hidden" id="allowedCategories" name="allowedCategories" data-key="allowedCategories">
|
|
129
|
+
<div id="categoryCheckboxes" style="max-height:320px;overflow-y:auto;border:1px solid var(--bs-border-color);border-radius:0.375rem;padding:12px;">
|
|
130
|
+
<div class="text-muted"><i class="fa fa-spinner fa-spin"></i> Kategoriler yukleniyor...</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
</form>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<!-- IMPORT admin/partials/settings/toc.tpl -->
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
package/static/viewer.html
CHANGED
|
@@ -655,13 +655,17 @@
|
|
|
655
655
|
width: 320px;
|
|
656
656
|
background: var(--bg-secondary);
|
|
657
657
|
border-left: 1px solid var(--border-color);
|
|
658
|
-
display:
|
|
658
|
+
display: flex;
|
|
659
659
|
flex-direction: column;
|
|
660
660
|
z-index: 50;
|
|
661
|
+
transform: translateX(100%);
|
|
662
|
+
transition: transform 0.25s cubic-bezier(0.32, 0.72, 0, 1);
|
|
663
|
+
pointer-events: none;
|
|
661
664
|
}
|
|
662
665
|
|
|
663
666
|
#chatSidebar.open {
|
|
664
|
-
|
|
667
|
+
transform: translateX(0);
|
|
668
|
+
pointer-events: auto;
|
|
665
669
|
}
|
|
666
670
|
|
|
667
671
|
#chatMessages {
|
|
@@ -727,18 +731,95 @@
|
|
|
727
731
|
padding: 0;
|
|
728
732
|
}
|
|
729
733
|
|
|
734
|
+
/* AI badge on AI messages */
|
|
735
|
+
.chatMsg.ai .aiBadge {
|
|
736
|
+
display: flex;
|
|
737
|
+
align-items: center;
|
|
738
|
+
gap: 3px;
|
|
739
|
+
font-size: 10px;
|
|
740
|
+
font-weight: 700;
|
|
741
|
+
color: var(--accent);
|
|
742
|
+
margin-bottom: 4px;
|
|
743
|
+
letter-spacing: 0.3px;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/* User message timestamp */
|
|
747
|
+
.chatMsg.user .chatTimestamp {
|
|
748
|
+
display: block;
|
|
749
|
+
font-size: 10px;
|
|
750
|
+
opacity: 0.7;
|
|
751
|
+
margin-top: 4px;
|
|
752
|
+
text-align: right;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/* Error message style */
|
|
756
|
+
.chatMsg.error {
|
|
757
|
+
align-self: center;
|
|
758
|
+
background: rgba(220, 38, 38, 0.15);
|
|
759
|
+
color: #f87171;
|
|
760
|
+
font-size: 12px;
|
|
761
|
+
text-align: center;
|
|
762
|
+
border: 1px solid rgba(220, 38, 38, 0.25);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/* Suggestion chips */
|
|
766
|
+
.chatSuggestions {
|
|
767
|
+
display: flex;
|
|
768
|
+
flex-wrap: wrap;
|
|
769
|
+
gap: 6px;
|
|
770
|
+
padding: 8px 0;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.chatSuggestionChip {
|
|
774
|
+
background: var(--bg-tertiary);
|
|
775
|
+
border: 1px solid var(--border-color);
|
|
776
|
+
border-radius: 16px;
|
|
777
|
+
padding: 6px 12px;
|
|
778
|
+
font-size: 12px;
|
|
779
|
+
color: var(--text-primary);
|
|
780
|
+
cursor: pointer;
|
|
781
|
+
transition: background 0.15s, border-color 0.15s;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.chatSuggestionChip:hover {
|
|
785
|
+
background: var(--accent);
|
|
786
|
+
color: #fff;
|
|
787
|
+
border-color: var(--accent);
|
|
788
|
+
}
|
|
789
|
+
|
|
730
790
|
/* Loading dots animation */
|
|
731
791
|
@keyframes chatDots {
|
|
732
792
|
0%, 20% { opacity: 0; }
|
|
733
793
|
50% { opacity: 1; }
|
|
734
794
|
100% { opacity: 0; }
|
|
735
795
|
}
|
|
736
|
-
.chatLoading span {
|
|
796
|
+
.chatLoading .chatLoadingDots span {
|
|
737
797
|
animation: chatDots 1.4s infinite;
|
|
738
798
|
display: inline-block;
|
|
739
799
|
}
|
|
740
|
-
.chatLoading span:nth-child(2) { animation-delay: 0.2s; }
|
|
741
|
-
.chatLoading span:nth-child(3) { animation-delay: 0.4s; }
|
|
800
|
+
.chatLoading .chatLoadingDots span:nth-child(2) { animation-delay: 0.2s; }
|
|
801
|
+
.chatLoading .chatLoadingDots span:nth-child(3) { animation-delay: 0.4s; }
|
|
802
|
+
.chatLoadingText {
|
|
803
|
+
font-size: 12px;
|
|
804
|
+
color: var(--text-secondary);
|
|
805
|
+
margin-right: 2px;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/* Character counter */
|
|
809
|
+
.chatCharCount {
|
|
810
|
+
font-size: 10px;
|
|
811
|
+
color: var(--text-secondary);
|
|
812
|
+
text-align: right;
|
|
813
|
+
padding: 0 12px;
|
|
814
|
+
opacity: 0;
|
|
815
|
+
transition: opacity 0.2s;
|
|
816
|
+
}
|
|
817
|
+
.chatCharCount.visible {
|
|
818
|
+
opacity: 1;
|
|
819
|
+
}
|
|
820
|
+
.chatCharCount.warning {
|
|
821
|
+
color: #f87171;
|
|
822
|
+
}
|
|
742
823
|
|
|
743
824
|
#chatInputArea {
|
|
744
825
|
padding: 8px 12px;
|
|
@@ -799,7 +880,7 @@
|
|
|
799
880
|
fill: #fff;
|
|
800
881
|
}
|
|
801
882
|
|
|
802
|
-
/* Chat upsell for non-premium users */
|
|
883
|
+
/* Chat upsell for non-premium users — gold theme matching page-lock */
|
|
803
884
|
.chatUpsell {
|
|
804
885
|
display: flex;
|
|
805
886
|
flex-direction: column;
|
|
@@ -812,40 +893,145 @@
|
|
|
812
893
|
|
|
813
894
|
.chatUpsellIcon {
|
|
814
895
|
margin-bottom: 16px;
|
|
815
|
-
opacity: 0.
|
|
896
|
+
opacity: 0.9;
|
|
816
897
|
}
|
|
817
898
|
|
|
818
899
|
.chatUpsellTitle {
|
|
819
900
|
font-size: 18px;
|
|
820
901
|
font-weight: 600;
|
|
902
|
+
color: #ffd700;
|
|
821
903
|
margin-bottom: 8px;
|
|
822
904
|
}
|
|
823
905
|
|
|
824
906
|
.chatUpsellText {
|
|
825
907
|
font-size: 13px;
|
|
826
|
-
color:
|
|
908
|
+
color: #a0a0a0;
|
|
827
909
|
line-height: 1.5;
|
|
828
910
|
margin-bottom: 20px;
|
|
911
|
+
max-width: 260px;
|
|
829
912
|
}
|
|
830
913
|
|
|
831
914
|
.chatUpsellText strong {
|
|
832
|
-
color:
|
|
915
|
+
color: #ffd700;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.chatUpsellActions {
|
|
919
|
+
display: flex;
|
|
920
|
+
flex-direction: column;
|
|
921
|
+
align-items: center;
|
|
922
|
+
gap: 0;
|
|
923
|
+
width: 90%;
|
|
924
|
+
max-width: 260px;
|
|
833
925
|
}
|
|
834
926
|
|
|
835
927
|
.chatUpsellBtn {
|
|
836
|
-
display:
|
|
837
|
-
|
|
838
|
-
|
|
928
|
+
display: flex;
|
|
929
|
+
align-items: center;
|
|
930
|
+
justify-content: center;
|
|
931
|
+
gap: 6px;
|
|
932
|
+
width: 100%;
|
|
933
|
+
padding: 11px 24px;
|
|
934
|
+
background: linear-gradient(135deg, #ffd700 0%, #ffaa00 100%);
|
|
935
|
+
color: #1a1a1a;
|
|
839
936
|
text-decoration: none;
|
|
840
|
-
|
|
841
|
-
border-radius: 8px;
|
|
937
|
+
border-radius: 10px;
|
|
842
938
|
font-size: 14px;
|
|
843
|
-
font-weight:
|
|
844
|
-
transition:
|
|
939
|
+
font-weight: 700;
|
|
940
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
941
|
+
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
|
|
942
|
+
box-sizing: border-box;
|
|
845
943
|
}
|
|
846
944
|
|
|
847
945
|
.chatUpsellBtn:hover {
|
|
848
|
-
|
|
946
|
+
transform: translateY(-2px);
|
|
947
|
+
box-shadow: 0 6px 20px rgba(255, 215, 0, 0.4);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.chatUpsellDivider {
|
|
951
|
+
display: flex;
|
|
952
|
+
align-items: center;
|
|
953
|
+
gap: 12px;
|
|
954
|
+
width: 100%;
|
|
955
|
+
margin: 12px 0;
|
|
956
|
+
color: #666;
|
|
957
|
+
font-size: 11px;
|
|
958
|
+
font-weight: 500;
|
|
959
|
+
letter-spacing: 0.5px;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
.chatUpsellDivider::before,
|
|
963
|
+
.chatUpsellDivider::after {
|
|
964
|
+
content: '';
|
|
965
|
+
flex: 1;
|
|
966
|
+
height: 1px;
|
|
967
|
+
background: rgba(255, 255, 255, 0.1);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
.chatUpsellBtnSecondary {
|
|
971
|
+
display: flex;
|
|
972
|
+
align-items: center;
|
|
973
|
+
justify-content: center;
|
|
974
|
+
gap: 6px;
|
|
975
|
+
width: 100%;
|
|
976
|
+
padding: 9px 24px;
|
|
977
|
+
background: transparent;
|
|
978
|
+
color: #ccc;
|
|
979
|
+
font-size: 13px;
|
|
980
|
+
font-weight: 600;
|
|
981
|
+
border-radius: 10px;
|
|
982
|
+
border: 1.5px solid rgba(255, 255, 255, 0.15);
|
|
983
|
+
text-decoration: none;
|
|
984
|
+
transition: transform 0.2s, border-color 0.2s, background 0.2s;
|
|
985
|
+
box-sizing: border-box;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.chatUpsellBtnSecondary:hover {
|
|
989
|
+
transform: translateY(-1px);
|
|
990
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
991
|
+
background: rgba(255, 255, 255, 0.05);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/* PRO badge on chat button for non-premium */
|
|
995
|
+
#chatBtn {
|
|
996
|
+
position: relative;
|
|
997
|
+
}
|
|
998
|
+
.proBadge {
|
|
999
|
+
position: absolute;
|
|
1000
|
+
top: 2px;
|
|
1001
|
+
right: 2px;
|
|
1002
|
+
background: linear-gradient(135deg, #ffd700, #ffaa00);
|
|
1003
|
+
color: #1a1a1a;
|
|
1004
|
+
font-size: 8px;
|
|
1005
|
+
font-weight: 700;
|
|
1006
|
+
padding: 1px 3px;
|
|
1007
|
+
border-radius: 3px;
|
|
1008
|
+
line-height: 1.2;
|
|
1009
|
+
pointer-events: none;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/* Header subtitle */
|
|
1013
|
+
.chatHeaderTitle {
|
|
1014
|
+
display: flex;
|
|
1015
|
+
align-items: center;
|
|
1016
|
+
gap: 8px;
|
|
1017
|
+
}
|
|
1018
|
+
.chatHeaderTitle > div {
|
|
1019
|
+
display: flex;
|
|
1020
|
+
flex-direction: column;
|
|
1021
|
+
}
|
|
1022
|
+
.chatHeaderTitle > div > span {
|
|
1023
|
+
font-weight: 600;
|
|
1024
|
+
}
|
|
1025
|
+
.chatHeaderTitle > div > small {
|
|
1026
|
+
font-size: 10px;
|
|
1027
|
+
color: var(--text-secondary);
|
|
1028
|
+
font-weight: 400;
|
|
1029
|
+
}
|
|
1030
|
+
.chatHeaderTitle svg {
|
|
1031
|
+
width: 20px;
|
|
1032
|
+
height: 20px;
|
|
1033
|
+
fill: var(--accent);
|
|
1034
|
+
flex-shrink: 0;
|
|
849
1035
|
}
|
|
850
1036
|
|
|
851
1037
|
#viewerContainer.withChatSidebar {
|
|
@@ -2478,12 +2664,19 @@
|
|
|
2478
2664
|
<!-- Chat Sidebar -->
|
|
2479
2665
|
<div id="chatSidebar">
|
|
2480
2666
|
<div class="sidebarHeader">
|
|
2481
|
-
<
|
|
2667
|
+
<div class="chatHeaderTitle">
|
|
2668
|
+
<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>
|
|
2669
|
+
<div>
|
|
2670
|
+
<span>PDF Chat</span>
|
|
2671
|
+
<small id="chatHeaderSubtitle">Yapay zeka ile</small>
|
|
2672
|
+
</div>
|
|
2673
|
+
</div>
|
|
2482
2674
|
<button class="closeBtn" id="closeChatSidebar">×</button>
|
|
2483
2675
|
</div>
|
|
2484
2676
|
<div id="chatMessages"></div>
|
|
2677
|
+
<div class="chatCharCount" id="chatCharCount"></div>
|
|
2485
2678
|
<div id="chatInputArea">
|
|
2486
|
-
<textarea id="chatInput" placeholder="PDF
|
|
2679
|
+
<textarea id="chatInput" placeholder="Bu PDF hakkında bir soru sorun..." rows="1"></textarea>
|
|
2487
2680
|
<button id="chatSendBtn">
|
|
2488
2681
|
<svg viewBox="0 0 24 24">
|
|
2489
2682
|
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
|
|
@@ -3313,8 +3506,9 @@
|
|
|
3313
3506
|
});
|
|
3314
3507
|
|
|
3315
3508
|
// ── Unicourse page watermark injection ──
|
|
3509
|
+
const watermarkOn = _cfg && _cfg.watermarkEnabled;
|
|
3316
3510
|
function injectPageWatermark(pageEl) {
|
|
3317
|
-
if (!pageEl) return;
|
|
3511
|
+
if (!pageEl || !watermarkOn) return;
|
|
3318
3512
|
var scale = pdfViewer ? pdfViewer.currentScale : 1;
|
|
3319
3513
|
var existing = pageEl.querySelector('.unicourse-page-wm');
|
|
3320
3514
|
if (existing) {
|
|
@@ -3408,17 +3602,39 @@
|
|
|
3408
3602
|
let chatSending = false;
|
|
3409
3603
|
let chatFirstOpen = true;
|
|
3410
3604
|
const chatIsPremium = _cfg && _cfg.isPremium;
|
|
3605
|
+
const chatIsVip = _cfg && _cfg.isVip;
|
|
3411
3606
|
|
|
3412
3607
|
// Show chat button if chatEnabled (visible to all users, not just premium)
|
|
3413
3608
|
if (_cfg && _cfg.chatEnabled) {
|
|
3414
3609
|
chatBtnEl.style.display = '';
|
|
3415
3610
|
}
|
|
3416
3611
|
|
|
3417
|
-
//
|
|
3612
|
+
// VIP badge in chat header subtitle
|
|
3613
|
+
if (chatIsVip) {
|
|
3614
|
+
var subtitle = document.getElementById('chatHeaderSubtitle');
|
|
3615
|
+
if (subtitle) {
|
|
3616
|
+
subtitle.textContent = 'VIP \u2022 Yapay zeka ile';
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
// Non-premium: hide input area entirely and show PRO badge
|
|
3418
3621
|
if (!chatIsPremium) {
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3622
|
+
document.getElementById('chatInputArea').style.display = 'none';
|
|
3623
|
+
document.getElementById('chatCharCount').style.display = 'none';
|
|
3624
|
+
// Add PRO badge to chat button
|
|
3625
|
+
var proBadge = document.createElement('span');
|
|
3626
|
+
proBadge.className = 'proBadge';
|
|
3627
|
+
proBadge.textContent = 'PRO';
|
|
3628
|
+
chatBtnEl.appendChild(proBadge);
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
// Auto-open chat sidebar for premium users
|
|
3632
|
+
if (_cfg && _cfg.chatEnabled && chatIsPremium) {
|
|
3633
|
+
chatSidebarEl.classList.add('open');
|
|
3634
|
+
chatBtnEl.classList.add('active');
|
|
3635
|
+
container.classList.add('withChatSidebar');
|
|
3636
|
+
chatFirstOpen = false;
|
|
3637
|
+
showChatWelcome();
|
|
3422
3638
|
}
|
|
3423
3639
|
|
|
3424
3640
|
function toggleChat() {
|
|
@@ -3431,7 +3647,7 @@
|
|
|
3431
3647
|
if (isOpening && chatFirstOpen) {
|
|
3432
3648
|
chatFirstOpen = false;
|
|
3433
3649
|
if (chatIsPremium) {
|
|
3434
|
-
|
|
3650
|
+
showChatWelcome();
|
|
3435
3651
|
} else {
|
|
3436
3652
|
showChatUpsell();
|
|
3437
3653
|
}
|
|
@@ -3482,7 +3698,15 @@
|
|
|
3482
3698
|
const msg = document.createElement('div');
|
|
3483
3699
|
msg.className = 'chatMsg ' + role;
|
|
3484
3700
|
if (role === 'ai') {
|
|
3485
|
-
msg.innerHTML = renderMarkdown(text);
|
|
3701
|
+
msg.innerHTML = '<div class="aiBadge"><svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M12 2L9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61z"/></svg> AI</div>' + renderMarkdown(text);
|
|
3702
|
+
} else if (role === 'user') {
|
|
3703
|
+
var now = new Date();
|
|
3704
|
+
var timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
|
|
3705
|
+
msg.textContent = text;
|
|
3706
|
+
var ts = document.createElement('span');
|
|
3707
|
+
ts.className = 'chatTimestamp';
|
|
3708
|
+
ts.textContent = timeStr;
|
|
3709
|
+
msg.appendChild(ts);
|
|
3486
3710
|
} else {
|
|
3487
3711
|
msg.textContent = text;
|
|
3488
3712
|
}
|
|
@@ -3495,7 +3719,7 @@
|
|
|
3495
3719
|
const msg = document.createElement('div');
|
|
3496
3720
|
msg.className = 'chatMsg ai chatLoading';
|
|
3497
3721
|
msg.id = 'chatLoadingMsg';
|
|
3498
|
-
msg.innerHTML = '<span>.</span><span>.</span><span>.</span>';
|
|
3722
|
+
msg.innerHTML = '<span class="chatLoadingText">Düşünüyor</span><span class="chatLoadingDots"><span>.</span><span>.</span><span>.</span></span>';
|
|
3499
3723
|
chatMessagesEl.appendChild(msg);
|
|
3500
3724
|
chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
|
|
3501
3725
|
return msg;
|
|
@@ -3506,17 +3730,46 @@
|
|
|
3506
3730
|
if (el) el.remove();
|
|
3507
3731
|
}
|
|
3508
3732
|
|
|
3733
|
+
function showChatWelcome() {
|
|
3734
|
+
addChatMessage('system', 'Bu PDF hakkında sorularınızı sorabilirsiniz.');
|
|
3735
|
+
var suggestions = document.createElement('div');
|
|
3736
|
+
suggestions.className = 'chatSuggestions';
|
|
3737
|
+
var chips = ['Bu döküman ne hakkında?', 'Özet çıkar', 'Ana noktalar neler?'];
|
|
3738
|
+
chips.forEach(function (text) {
|
|
3739
|
+
var chip = document.createElement('button');
|
|
3740
|
+
chip.className = 'chatSuggestionChip';
|
|
3741
|
+
chip.textContent = text;
|
|
3742
|
+
chip.onclick = function () {
|
|
3743
|
+
chatInputEl.value = text;
|
|
3744
|
+
sendChatMessage();
|
|
3745
|
+
suggestions.remove();
|
|
3746
|
+
};
|
|
3747
|
+
suggestions.appendChild(chip);
|
|
3748
|
+
});
|
|
3749
|
+
chatMessagesEl.appendChild(suggestions);
|
|
3750
|
+
chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3509
3753
|
function showChatUpsell() {
|
|
3754
|
+
var uid = (_cfg && _cfg.uid) || 0;
|
|
3755
|
+
var checkoutUrl = 'https://forum.ieu.app/pay/checkout?uid=' + uid;
|
|
3510
3756
|
const upsell = document.createElement('div');
|
|
3511
3757
|
upsell.className = 'chatUpsell';
|
|
3512
3758
|
upsell.innerHTML = '<div class="chatUpsellIcon">' +
|
|
3513
|
-
'<svg viewBox="0 0 24 24" width="48" height="48" fill="
|
|
3759
|
+
'<svg viewBox="0 0 24 24" width="48" height="48" fill="#ffd700">' +
|
|
3514
3760
|
'<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>' +
|
|
3515
3761
|
'</svg></div>' +
|
|
3516
3762
|
'<div class="chatUpsellTitle">PDF Chat</div>' +
|
|
3517
|
-
'<div class="chatUpsellText">Bu
|
|
3518
|
-
'
|
|
3519
|
-
'<
|
|
3763
|
+
'<div class="chatUpsellText">PDF\'ler hakkında <strong>yapay zeka</strong> ile sohbet edebilir, sorular sorabilirsiniz. Bu özellik <strong>Premium</strong> ve <strong>VIP</strong> üyelere özeldir.</div>' +
|
|
3764
|
+
'<div class="chatUpsellActions">' +
|
|
3765
|
+
'<a href="' + checkoutUrl + '" target="_blank" class="chatUpsellBtn">' +
|
|
3766
|
+
'<svg viewBox="0 0 24 24" width="18" height="18" fill="#1a1a1a"><path d="M16 18v2H8v-2h8zM11 7.99V16h2V7.99h3L12 4l-4 3.99h3z"/></svg>' +
|
|
3767
|
+
'Hesabını Yükselt</a>' +
|
|
3768
|
+
'<div class="chatUpsellDivider">ya da</div>' +
|
|
3769
|
+
'<a href="https://forum.ieu.app/material-info" target="_blank" class="chatUpsellBtnSecondary">' +
|
|
3770
|
+
'<svg viewBox="0 0 24 24" width="16" height="16" fill="#ccc"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11zM8 15.01l1.41 1.41L11 14.84V19h2v-4.16l1.59 1.59L16 15.01 12.01 11 8 15.01z"/></svg>' +
|
|
3771
|
+
'Materyal Yükle</a>' +
|
|
3772
|
+
'</div>';
|
|
3520
3773
|
chatMessagesEl.appendChild(upsell);
|
|
3521
3774
|
}
|
|
3522
3775
|
|
|
@@ -3561,24 +3814,44 @@
|
|
|
3561
3814
|
chatHistory = chatHistory.slice(-50);
|
|
3562
3815
|
}
|
|
3563
3816
|
} else {
|
|
3564
|
-
addChatMessage('
|
|
3817
|
+
addChatMessage('error', data.error || 'Bir hata oluştu. Tekrar deneyin.');
|
|
3565
3818
|
}
|
|
3566
3819
|
} catch (err) {
|
|
3567
3820
|
removeChatLoading();
|
|
3568
|
-
addChatMessage('
|
|
3821
|
+
addChatMessage('error', 'Bağlantı hatası. Tekrar deneyin.');
|
|
3569
3822
|
} finally {
|
|
3570
3823
|
chatSending = false;
|
|
3571
3824
|
chatSendBtnEl.disabled = false;
|
|
3825
|
+
updateSendBtnState();
|
|
3572
3826
|
chatInputEl.focus();
|
|
3573
3827
|
}
|
|
3574
3828
|
}
|
|
3575
3829
|
|
|
3576
3830
|
chatSendBtnEl.onclick = sendChatMessage;
|
|
3577
3831
|
|
|
3578
|
-
|
|
3832
|
+
var chatCharCountEl = document.getElementById('chatCharCount');
|
|
3833
|
+
|
|
3834
|
+
function updateSendBtnState() {
|
|
3835
|
+
var hasText = chatInputEl.value.trim().length > 0;
|
|
3836
|
+
chatSendBtnEl.style.opacity = hasText && !chatSending ? '1' : '0.4';
|
|
3837
|
+
}
|
|
3838
|
+
updateSendBtnState();
|
|
3839
|
+
|
|
3840
|
+
// Auto-grow textarea + character counter + send button state
|
|
3579
3841
|
chatInputEl.addEventListener('input', () => {
|
|
3580
3842
|
chatInputEl.style.height = 'auto';
|
|
3581
3843
|
chatInputEl.style.height = Math.min(chatInputEl.scrollHeight, 120) + 'px';
|
|
3844
|
+
updateSendBtnState();
|
|
3845
|
+
|
|
3846
|
+
// Character counter (show at 1800+)
|
|
3847
|
+
var len = chatInputEl.value.length;
|
|
3848
|
+
if (len >= 1800) {
|
|
3849
|
+
chatCharCountEl.textContent = len + '/2000';
|
|
3850
|
+
chatCharCountEl.classList.add('visible');
|
|
3851
|
+
chatCharCountEl.classList.toggle('warning', len >= 1950);
|
|
3852
|
+
} else {
|
|
3853
|
+
chatCharCountEl.classList.remove('visible');
|
|
3854
|
+
}
|
|
3582
3855
|
});
|
|
3583
3856
|
|
|
3584
3857
|
// Enter to send, Shift+Enter for newline
|