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.
@@ -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 RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
15
- const RATE_LIMIT_MAX = 30; // messages per window
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 > RATE_LIMIT_WINDOW * 2) {
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: 'Premium membership required for AI chat' });
107
+ return res.status(403).json({ error: 'Bu özellik Premium/VIP üyelere özeldir.' });
106
108
  }
107
109
 
108
- // Rate limiting
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 > RATE_LIMIT_WINDOW) {
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 > RATE_LIMIT_MAX) {
117
- return res.status(429).json({ error: 'Rate limit exceeded. Please wait a moment.' });
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 not found' });
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 service rate limit exceeded. Please try again later.' });
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 service configuration error' });
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: 'Failed to get AI response. Please try again.' });
177
+ return res.status(500).json({ error: 'AI yanıt veremedi. Lütfen tekrar deneyin.' });
169
178
  }
170
179
  };
@@ -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 = `You are a helpful assistant that answers questions about the provided PDF document.
15
- Respond in the same language the user writes their question in.
16
- Be concise and accurate. When referencing specific information, mention the relevant page or section when possible.`;
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
- for (const entry of history) {
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
- const settings = await meta.settings.get('pdf-secure');
66
- if (settings && settings.geminiApiKey) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-pdf-secure2",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Secure PDF viewer plugin for NodeBB - prevents downloading, enables canvas-only rendering with Premium group support",
5
5
  "main": "library.js",
6
6
  "repository": {
package/plugin.json CHANGED
@@ -19,6 +19,10 @@
19
19
  "hook": "filter:meta.getMetaTags",
20
20
  "method": "filterMetaTags"
21
21
  },
22
+ {
23
+ "hook": "filter:config.get",
24
+ "method": "filterConfig"
25
+ },
22
26
  {
23
27
  "hook": "filter:parse.post",
24
28
  "method": "transformPdfLinks"
@@ -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
- console.log('[PDF-Secure] Admin settings loaded');
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
  });
@@ -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 col-md-8 px-0 mb-4" tabindex="0">
6
- <form role="form" class="pdf-secure-settings" id="pdf-secure-settings">
7
- <div class="mb-4">
8
- <h5 class="fw-bold tracking-tight settings-header">PDF Secure Viewer Settings</h5>
9
-
10
- <p class="lead">
11
- Configure the secure PDF viewer plugin settings below.
12
- </p>
13
-
14
- <div class="mb-3">
15
- <label class="form-label" for="premiumGroup">Premium Group Name</label>
16
- <input type="text" id="premiumGroup" name="premiumGroup" data-key="premiumGroup" title="Premium Group Name" class="form-control" placeholder="Premium" value="Premium">
17
- <div class="form-text">Users in this group can view full PDFs with all tools. Others can only see the first page.</div>
18
- </div>
19
-
20
- <div class="mb-3">
21
- <label class="form-label" for="vipGroup">VIP Group Name</label>
22
- <input type="text" id="vipGroup" name="vipGroup" data-key="vipGroup" title="VIP Group Name" class="form-control" placeholder="VIP" value="VIP">
23
- <div class="form-text">Users in this group get all Premium features plus VIP badge and early access to new features.</div>
24
- </div>
25
-
26
- <div class="mb-3">
27
- <label class="form-label" for="liteGroup">Lite Group Name</label>
28
- <input type="text" id="liteGroup" name="liteGroup" data-key="liteGroup" title="Lite Group Name" class="form-control" placeholder="Lite" value="Lite">
29
- <div class="form-text">Users in this group can view full PDFs but only with zoom and fullscreen. No annotations, sidebar, or other tools.</div>
30
- </div>
31
-
32
- <div class="form-check form-switch mb-3">
33
- <input type="checkbox" class="form-check-input" id="watermarkEnabled" name="watermarkEnabled" data-key="watermarkEnabled">
34
- <label for="watermarkEnabled" class="form-check-label">Enable Watermark</label>
35
- <div class="form-text">Show a watermark overlay on PDF pages.</div>
36
- </div>
37
- </div>
38
- <div class="mb-4">
39
- <h5 class="fw-bold tracking-tight settings-header">AI Chat Settings</h5>
40
-
41
- <div class="mb-3">
42
- <label class="form-label" for="geminiApiKey">Google Gemini API Key</label>
43
- <input type="password" id="geminiApiKey" name="geminiApiKey" data-key="geminiApiKey" title="Gemini API Key" class="form-control" placeholder="Enter your Gemini API key">
44
- <div class="form-text">Google Gemini API anahtarinizi girin. <a href="https://ai.google.dev" target="_blank" rel="noopener">ai.google.dev</a> adresinden alinabilir. Premium/VIP kullanicilar PDF Chat ozelligini kullanabilir.</div>
45
- </div>
46
- </div>
47
- </form>
48
- </div>
49
-
50
- <!-- IMPORT admin/partials/settings/toc.tpl -->
51
- </div>
52
- </div>
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>
@@ -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: none;
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
- display: flex;
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.8;
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: var(--text-secondary);
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: var(--accent);
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: inline-block;
837
- background: var(--accent);
838
- color: #fff;
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
- padding: 10px 24px;
841
- border-radius: 8px;
937
+ border-radius: 10px;
842
938
  font-size: 14px;
843
- font-weight: 600;
844
- transition: background 0.15s;
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
- background: var(--accent-hover);
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
- <span>PDF Chat</span>
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">&times;</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 hakkinda soru sorun..." rows="1"></textarea>
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
- // Non-premium: disable input area and show upsell
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
- chatInputEl.disabled = true;
3420
- chatInputEl.placeholder = 'Premium/VIP uyelere ozel...';
3421
- chatSendBtnEl.disabled = true;
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
- addChatMessage('system', 'Bu PDF hakkinda sorularinizi sorabilirsiniz.');
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="var(--accent)">' +
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 ozellik <strong>Premium</strong> ve <strong>VIP</strong> uyelere ozeldir. ' +
3518
- 'PDF\'ler hakkinda yapay zeka ile sohbet edebilir, sorular sorabilirsiniz.</div>' +
3519
- '<span class="chatUpsellBtn" style="cursor:default;">Abone Ol</span>';
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('system', data.error || 'Bir hata olustu. Tekrar deneyin.');
3817
+ addChatMessage('error', data.error || 'Bir hata oluştu. Tekrar deneyin.');
3565
3818
  }
3566
3819
  } catch (err) {
3567
3820
  removeChatLoading();
3568
- addChatMessage('system', 'Baglanti hatasi. Tekrar deneyin.');
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
- // Auto-grow textarea
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