Jahadona commited on
Commit
f862c6e
·
verified ·
1 Parent(s): 2a02c89

Update script.js

Browse files
Files changed (1) hide show
  1. script.js +610 -377
script.js CHANGED
@@ -1,446 +1,679 @@
1
- // این خط در فایل script.js قرار دارد و برای Space جدیدی که Frontend و Backend در آن مشترک هستند، استفاده می شود.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  // آدرس لوکال برای ارتباط با Backend در همان کانتینر Docker روی پورت 7860
3
- const EMBEDDING_SERVER_URL = 'http://localhost:7860/get_embedding';
4
-
5
- // لیست مسیر فایل های JSON خاطرات شما در هاست (نسبت به ریشه وب سایت)
6
- // مطمئن شوید که این مسیرها صحیح هستند و این فایل ها در Space آپلود شده اند.
7
- const MEMOIRS_DATA_FILES = [
8
- 'jabe_siah.json',
9
- // اگر کتاب های دیگری هم دارید، مسیر فایل JSON آن ها را در اینجا اضافه کنید:
10
- // 'ketab_dovom.json',
11
- // ... فایل های بیشتر
12
- ];
13
-
14
- // متغیر برای ذخیره داده های بارگذاری شده از فایل های JSON
15
- let allMemoirsData = [];
16
-
17
- // تعداد نتایج نمایش داده شده در هر صفحه (اختیاری)
18
- const RESULTS_PER_PAGE = 10; // می توانید این مقدار را تغییر دهید
19
-
20
- // حداقل امتیاز شباهت برای نمایش نتیجه
21
- let similarityThreshold = 0.15; // می توانید مقدار پیش فرض را تنظیم کنید
22
-
23
- // متغیرها برای کنترل Pagination (در صورت نیاز به پیاده سازی Pagination واقعی)
24
- // در حال حاضر فقط تعداد نتایج برتر را نمایش می دهیم، نه pagination واقعی
25
- // let currentPage = 1;
26
- // let totalResults = 0;
27
- // let currentSearchResults = []; // برای ذخیره نتایج جستجوی فعلی قبل از Pagination
28
-
29
- // ****** المان های DOM ******
30
- // دقت کنید: ID ها دقیقاً با index.html مطابقت دارند (حروف کوچک و بزرگ)
31
- const searchInput = document.getElementById('search-input');
32
- const searchButton = document.getElementById('searchButton'); // <--- اصلاح شد: ID مطابقت دارد با HTML
33
- const resultsContainer = document.getElementById('results-container');
34
- const loadingSpinner = document.getElementById('loading-spinner');
35
- const statusMessage = document.getElementById('status-message');
36
- const booksCheckboxesContainer = document.getElementById('books-checkboxes-container');
37
- const similarityThresholdInput = document.getElementById('similarity-threshold'); // <--- اصلاح شد: ID مطابقت دارد با HTML
38
- const similarityThresholdValueSpan = document.getElementById('similarity-threshold-value'); // <--- اصلاح شد: ID مطابقت دارد با HTML
39
- // *************************
40
-
41
-
42
- // ****** توابع کمکی ******
43
-
44
- // تابع برای نمایش پیام وضعیت
45
- function showStatus(message, isError = false) {
46
- statusMessage.textContent = message;
47
- statusMessage.style.color = isError ? 'red' : 'black';
48
- statusMessage.style.display = 'block';
49
  }
50
 
51
- // تابع برای مخفی کردن پیام وضعیت
52
- function hideStatus() {
53
- statusMessage.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
54
  }
55
 
56
- // تابع برای نمایش اسپینر بارگذاری
57
- function showLoading() {
58
- loadingSpinner.style.display = 'block';
59
- searchButton.disabled = true; // غیرفعال کردن دکمه جستجو حین بارگذاری
60
- hideStatus(); // مخفی کردن پیام وضعیت هنگام بارگذاری
 
 
 
61
  }
62
 
63
- // تابع برای مخفی کردن اسپینر بارگذاری
64
- function hideLoading() {
65
- loadingSpinner.style.display = 'none';
66
- searchButton.disabled = false; // فعال کردن دکمه جستجو پس از بارگذاری
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  }
68
 
69
 
70
- // تابع برای محاسبه شباهت کسینوسی بین دو بردار
71
- function cosineSimilarity(vecA, vecB) {
72
- let dotProduct = 0;
73
- // بردارهای تولید شده توسط Sentence-Transformers معمولاً نرمالیزه شده اند (طول 1 دارند).
74
- // اگر مطمئن هستید که بردارها نرمالیزه شده اند، می توانید فقط ضرب نقطه ای را محاسبه کنید.
75
- for (let i = 0; i < vecA.length; i++) {
76
- dotProduct += vecA[i] * vecB[i];
 
 
77
  }
78
- return dotProduct; // برگرداندن ضرب نقطه ای برای بردارهای نرمالیزه شده
79
- }
80
 
81
- // تابع برای دریافت بردار (Embedding) یک عبارت جستجو از سرور Backend
82
- async function getEmbedding(query) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  try {
84
- const response = await fetch(EMBEDDING_SERVER_URL, {
85
- method: 'POST',
86
- headers: {
87
- 'Content-Type': 'application/json',
88
- },
89
- body: JSON.stringify({ query: query }),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  });
91
 
92
- if (!response.ok) {
93
- const errorText = await response.text();
94
- throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
95
- }
96
 
97
- const data = await response.json();
98
- if (!data || !data.embedding || !Array.isArray(data.embedding)) {
99
- throw new Error('Invalid response format from embedding server.');
100
- }
101
- return data.embedding; // برگرداندن آرایه بردار
102
 
103
- } catch (error) {
104
- console.error('Error getting embedding:', error);
105
- showStatus(`Error getting embedding: ${error.message}`, true);
106
- return null; // برگرداندن null در صورت خطا
107
- }
108
- }
109
 
110
- // تابع برای بارگذاری داده های خاطرات از فایل های JSON
111
- async function loadMemoirsData() {
112
- allMemoirsData = []; // پاک کردن داده های قبلی
 
 
 
 
 
113
 
114
- if (MEMOIRS_DATA_FILES.length === 0) {
115
- showStatus("No memoir data files specified.", true);
116
- return;
117
- }
118
 
119
- showStatus("Loading memoir data...");
 
 
 
 
120
 
121
- try {
122
- for (const file of MEMOIRS_DATA_FILES) {
123
- const response = await fetch(file);
124
- if (!response.ok) {
125
- throw new Error(`HTTP error! status: ${response.status} for file ${file}`);
126
  }
127
- const data = await response.json();
128
- if (!Array.isArray(data)) {
129
- console.warn(`Warning: Data in file ${file} is not an array. Skipping.`);
130
- continue; // اگر فایل JSON آرایه نبود، آن را نادیده بگیر
131
- }
132
- allMemoirsData = allMemoirsData.concat(data); // اضافه کردن داده های هر فایل به لیست کلی
 
 
 
 
 
 
 
 
 
133
 
134
- }
135
- showStatus(`Successfully loaded data from ${MEMOIRS_DATA_FILES.length} files (${allMemoirsData.length} passages).`);
136
- console.log(`Total passages loaded: ${allMemoirsData.length}`);
137
- renderBookCheckboxes(); // نمایش چک باکس کتاب ها پس از بارگذاری داده ها
138
 
139
  } catch (error) {
140
- console.error('Error loading memoir data:', error);
141
- showStatus(`Error loading memoir data: ${error.message}`, true);
 
 
 
 
 
 
 
 
 
142
  }
143
  }
144
 
145
- // تابع برای نمایش نتایج جستجو در رابط کاربری
146
- function displayResults(results) {
147
- resultsContainer.innerHTML = ''; // پاک کردن نتایج قبلی
148
 
149
- if (results.length === 0) {
150
- resultsContainer.innerHTML = '<p>No results found.</p>';
151
- return;
152
- }
153
-
154
- // نمایش تنها تعداد نتایج مشخص در هر صفحه (اگر Pagination کامل پیاده سازی نشود)
155
- const resultsToDisplay = results.slice(0, RESULTS_PER_PAGE);
156
-
157
-
158
- resultsToDisplay.forEach(item => {
159
- // استفاده از 'passage_original' اگر وجود دارد، در غیر این صورت 'passage'
160
- const passageTextToDisplay = item.passage_original || item.passage;
161
-
162
- const resultElement = document.createElement('div');
163
- resultElement.classList.add('result-item');
164
-
165
- // فرمت کردن امتیاز شباهت
166
- const similarityScore = item.score !== undefined ? item.score.toFixed(4) : 'N/A'; // نمایش 4 رقم اعشار
167
-
168
- resultElement.innerHTML = `
169
- <p class="result-passage">${passageTextToDisplay}</p>
170
- <p class="result-meta">منبع: ${item.reference || 'نامشخص'} | کتاب: ${item.book_title || 'نامشخص'} | شباهت: ${similarityScore}</p>
171
- <button class="copy-button" style="display: inline-block; margin-left: 10px; padding: 5px 10px; cursor: pointer; background-color: #007bff; color: white; border: none; border-radius: 4px;">کپی متن</button>
172
- `;
173
- // استایل های موقت inline برای دکمه کپی برای اطمینان از نمایش در موبایل
174
-
175
-
176
- // اضافه کردن event listener برای دکمه کپی
177
- const copyButton = resultElement.querySelector('.copy-button');
178
- if (copyButton) { // اضافه کردن چک برای اطمینان که دکمه پیدا شده است
179
- copyButton.addEventListener('click', () => {
180
- // استفاده از Clipboard API برای کپی کردن متن
181
- navigator.clipboard.writeText(passageTextToDisplay)
182
- .then(() => {
183
- // نمایش پیام موفقیت (اختیاری)
184
- copyButton.textContent = 'کپی شد!';
185
- setTimeout(() => {
186
- copyButton.textContent = 'کپی متن';
187
- }, 2000); // بازگرداندن متن دکمه پس از 2 ثانیه
188
- })
189
- .catch(err => {
190
- console.error('Error copying text: ', err);
191
- copyButton.textContent = 'خطا!';
192
- setTimeout(() => {
193
- copyButton.textContent = 'کپی متن';
194
- }, 2000);
195
- });
196
- });
197
- }
198
 
 
 
 
199
 
200
- resultsContainer.appendChild(resultElement);
201
- });
202
- }
 
 
 
203
 
204
- // تابع برای رندر کردن چک باکس کتاب ها بر اساس داده های بارگذاری شده
205
- function renderBookCheckboxes() {
206
- booksCheckboxesContainer.innerHTML = ''; // پاک کردن چک باکس های قبلی
207
- const uniqueBooks = [...new Set(allMemoirsData.map(item => item.book_title))]; // گرفتن نام کتاب های منحصر به فرد
208
-
209
- if (uniqueBooks.length > 0) {
210
- // اضافه کردن گزینه "انتخاب همه"
211
- const allBooksCheckbox = document.createElement('input');
212
- allBooksCheckbox.type = 'checkbox';
213
- allBooksCheckbox.id = 'book-all';
214
- allBooksCheckbox.value = 'all';
215
- allBooksCheckbox.checked = true; // پیش فرض: همه انتخاب شده اند
216
- allBooksCheckbox.classList.add('book-checkbox');
217
-
218
- const allBooksLabel = document.createElement('label');
219
- allBooksLabel.htmlFor = 'book-all';
220
- allBooksLabel.textContent = 'انتخاب همه';
221
- allBooksLabel.style.fontWeight = 'bold'; // برجسته کردن لیبل "انتخاب همه"
222
-
223
- const allBooksDiv = document.createElement('div');
224
- allBooksDiv.appendChild(allBooksCheckbox);
225
- allBooksDiv.appendChild(allBooksLabel);
226
- booksCheckboxesContainer.appendChild(allBooksDiv);
227
-
228
-
229
- uniqueBooks.forEach(bookTitle => {
230
- const checkbox = document.createElement('input');
231
- checkbox.type = 'checkbox';
232
- checkbox.id = `book-${bookTitle.replace(/\s+/g, '-')}`; // ایجاد ID منحصر به فرد (جایگزینی فاصله ها با خط تیره)
233
- checkbox.value = bookTitle;
234
- checkbox.checked = true; // پیش فرض: همه انتخاب شده اند
235
- checkbox.classList.add('book-checkbox');
236
-
237
- const label = document.createElement('label');
238
- label.htmlFor = checkbox.id;
239
- label.textContent = bookTitle;
240
-
241
- const div = document.createElement('div');
242
- div.appendChild(checkbox);
243
- div.appendChild(label);
244
- booksCheckboxesContainer.appendChild(div);
245
- });
246
 
247
- // اضافه کردن event listener برای "انتخاب همه"
248
- allBooksCheckbox.addEventListener('change', (event) => {
249
- const isChecked = event.target.checked;
250
- document.querySelectorAll('.book-checkbox').forEach(cb => {
251
- if (cb.id !== 'book-all') {
252
- cb.checked = isChecked;
253
- }
254
- });
255
- });
256
 
257
- // اضافه کردن event listener برای چک باکس های تکی برای مدیریت "انتخاب همه"
258
- document.querySelectorAll('.book-checkbox').forEach(cb => {
259
- if (cb.id !== 'book-all') {
260
- cb.addEventListener('change', () => {
261
- const allOthersChecked = document.querySelectorAll('.book-checkbox:checked').length === uniqueBooks.length;
262
- allBooksCheckbox.checked = allOthersChecked;
263
- });
264
- }
265
- });
266
 
 
 
 
 
 
 
 
 
 
267
 
268
- } else {
269
- booksCheckboxesContainer.innerHTML = '<p>No books found in data.</p>';
270
  }
271
- }
272
 
 
 
 
 
273
 
274
- // ****** Event Listeners ******
275
 
276
- // Listener برای اطمینان از اینکه DOM کاملاً بارگذاری شده است
277
- document.addEventListener('DOMContentLoaded', () => {
 
 
 
278
 
279
- // بارگذاری داده ها هنگام بارگذاری صفحه
280
- loadMemoirsData();
 
 
 
 
 
 
281
 
282
- // تنظیم مقدار اولیه نمایش داده شده برای آستانه شباهت
283
- if (similarityThresholdInput && similarityThresholdValueSpan) { // اضافه کردن چک برای اطمینان از پیدا شدن المان ها
284
- similarityThresholdInput.value = similarityThreshold;
285
- similarityThresholdValueSpan.textContent = similarityThreshold.toFixed(2); // نمایش 2 رقم اعشار
286
- } else {
287
- console.error("Similarity threshold input or value span not found.");
 
 
 
 
 
 
 
 
 
 
288
  }
289
 
 
 
 
 
 
 
 
290
 
291
- // event listener برای دکمه جستجو
292
- if (searchButton) { // اضافه کردن چک برای اطمینان از پیدا شدن المان
293
- searchButton.addEventListener('click', async () => {
294
- const query = searchInput.value.trim();
295
 
296
- if (!query) {
297
- showStatus("Please enter a search query.", true);
298
- return;
299
- }
300
 
301
- if (allMemoirsData.length === 0) {
302
- showStatus("Memoir data not loaded yet. Please wait or check console.", true);
303
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  }
305
 
306
- showLoading(); // نمایش اسپینر
 
 
 
307
 
308
- // 1. دریافت بردار سوال از Backend
309
- const queryEmbedding = await getEmbedding(query);
310
 
311
- if (!queryEmbedding) {
312
- hideLoading(); // مخفی کردن اسپینر در صورت خطا
313
- // پیام خطا در تابع getEmbedding نمایش داده شده است
314
- return;
315
- }
 
 
 
 
316
 
317
- // 2. فیلتر کردن داده ها بر اساس کتاب های انتخاب شده
318
- const selectedBooksCheckboxes = document.querySelectorAll('.book-checkbox:checked');
319
- const selectedBookTitles = Array.from(selectedBooksCheckboxes)
320
- .filter(cb => cb.value !== 'all') // حذف گزینه "انتخاب همه"
321
- .map(cb => cb.value);
322
-
323
- let filteredMemoirs = allMemoirsData;
324
- // اگر گزینه "انتخاب همه" انتخاب نشده و حداقل یک کتاب انتخاب شده باشد
325
- // یا اگر تعداد کتاب های انتخاب شده کمتر از تعداد کل کتاب های منحصر به فرد باشد
326
- const uniqueBooksCount = new Set(allMemoirsData.map(item => item.book_title)).size;
327
- if (selectedBookTitles.length > 0 && selectedBookTitles.length < uniqueBooksCount) {
328
- filteredMemoirs = allMemoirsData.filter(item => selectedBookTitles.includes(item.book_title));
329
- } else if (selectedBookTitles.length === 0 && uniqueBooksCount > 0) {
330
- // اگر هیچ کتابی انتخاب نشده باشد و حداقل یک کتاب در داده ها باشد
331
- filteredMemoirs = [];
332
- showStatus("No books selected. Select at least one book.", true);
 
333
  }
334
- // اگر selectedBookTitles.length برابر با uniqueBooksCount باشد، یعنی همه انتخاب شده اند و فیلتر لازم نیست.
335
-
336
-
337
- // 3. محاسبه شباهت و ذخیره نتایج
338
- const searchResults = [];
339
- if (filteredMemoirs.length > 0) {
340
- filteredMemoirs.forEach(item => {
341
- // اطمینان از وجود بردار و اینکه یک آرایه است و اندازه مناسبی دارد (اختیاری)
342
- if (item.embedding && Array.isArray(item.embedding) /*&& item.embedding.length === 1024*/) {
343
- const score = cosineSimilarity(queryEmbedding, item.embedding);
344
- // فیلتر بر اساس آستانه شباهت در اینجا
345
- if (score >= similarityThreshold) {
346
- searchResults.push({
347
- ...item, // کپی کردن تمام ویژگی های اصلی
348
- score: score // اضافه کردن امتیاز شباهت
349
- });
350
- }
351
- } else {
352
- console.warn(`Passage with reference "${item.reference}" has no valid embedding. Skipping.`);
353
- }
354
- });
355
  }
 
 
 
356
 
357
 
358
- // 4. مرتب سازی نتایج بر اساس امتیاز شباهت (نزولی)
359
- searchResults.sort((a, b) => b.score - a.score);
 
360
 
361
- hideLoading(); // مخفی کردن اسپینر
362
- hideStatus(); // مخفی کردن پیام وضعیت
363
 
364
- // 5. نمایش نتایج
365
- displayResults(searchResults);
 
 
366
 
367
- });
368
- } else {
369
- console.error("Search button element with ID 'searchButton' not found.");
370
- showStatus("Initialization failed: Search button not found.", true);
371
- }
 
 
 
 
372
 
373
 
374
- // event listener برای کلید Enter در کادر جستجو
375
- if (searchInput) { // اضافه کردن چک برای اطمینان از پیدا شدن المان
376
- searchInput.addEventListener('keypress', (event) => {
377
- // اگر کلید Enter فشرده شد (کد ۱۳)
378
- if (event.key === 'Enter') { // یا event.keyCode === 13
379
- event.preventDefault(); // جلوگیری از ارسال فرم (اگر در فرم بود)
380
- if (searchButton && !searchButton.disabled) { // اگر دکمه جستجو وجود دارد و فعال است
381
- searchButton.click(); // فراخوانی کلیک روی دکمه جستجو
382
- }
383
- }
384
- });
385
- } else {
386
- console.error("Search input element with ID 'search-input' not found.");
387
- showStatus("Initialization failed: Search input not found.", true);
388
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
 
 
 
390
 
391
- // event listener برای تغییر آستانه شباهت
392
- if (similarityThresholdInput) { // اضافه کردن چک برای اطمینان از پیدا شدن المان
393
- similarityThresholdInput.addEventListener('input', (event) => {
394
- similarityThreshold = parseFloat(event.target.value);
395
- if (similarityThresholdValueSpan) { // چک کردن نمایش دهنده مقدار
396
- similarityThresholdValueSpan.textContent = similarityThreshold.toFixed(2); // نمایش 2 رقم اعشار
397
- }
398
 
399
- // می توانید اینجا منطق فیلتر مجدد نتایج نمایش داده شده را اضافه کنید
400
- // اگر نتایج قبلی نمایش داده شده اند و می خواهید با تغییر آستانه، نمایش آن ها را به روز کنید.
401
- // اما برای سادگی فعلا این کار انجام نمی شود و فیلتر فقط هنگام جستجوی جدید اعمال می شود.
402
- });
403
- } else {
404
- console.error("Similarity threshold input element with ID 'similarity-threshold' not found.");
405
- // نمایش وضعیت خطا لود المان ممکن است قبلاً انجام شده باشد.
406
- }
407
 
408
 
409
- // Listener برای دکمه های کپی (به صورت Delegation)
410
- // این روش بهتر است چون دکمه های کپی به صورت پویا اضافه می شوند
411
- if (resultsContainer) { // اضافه کردن چک برای اطمینان از پیدا شدن المان
412
- resultsContainer.addEventListener('click', (event) => {
413
- // چک می کنیم که آیا کلیک روی المانی با کلاس 'copy-button' رخ داده است؟
414
- if (event.target.classList.contains('copy-button')) {
415
- // المان والد نتیجه را پیدا می کنیم (.result-item)
416
- const resultItemElement = event.target.closest('.result-item');
417
- if (resultItemElement) {
418
- // متن اصلی را از داخل نتیجه پیدا می کنیم
419
- const passageTextElement = resultItemElement.querySelector('.result-passage');
420
- if (passageTextElement) {
421
- navigator.clipboard.writeText(passageTextElement.textContent)
422
- .then(() => {
423
- // نمایش پیام موفقیت روی همان دکمه
424
- event.target.textContent = 'کپی شد!';
425
- setTimeout(() => {
426
- event.target.textContent = 'کپی متن';
427
- }, 2000);
428
- })
429
- .catch(err => {
430
- console.error('Error copying text: ', err);
431
- event.target.textContent = 'خطا!';
432
- setTimeout(() => {
433
- event.target.textContent = 'کپی متن';
434
- }, 2000);
435
- });
436
- }
437
- }
438
- }
439
- });
440
- } else {
441
- console.error("Results container element with ID 'results-container' not found.");
442
- showStatus("Initialization failed: Results container not found.", true);
443
  }
 
 
444
 
445
 
446
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // این فایل script.js نسخه نهایی شده بر اساس کد اصلی شماست.
2
+ // تغییرات لازم برای کار در Hugging Face Space جدید و مدل heydariAI/persian-embeddings اعمال شده است.
3
+ // شامل تمام قابلیت های اضافه شده و رفع اشکالات ID های شناسایی شده.
4
+
5
+ // ****** تعریف متغیرهای عناصر HTML در بالاترین اسکوپ ******
6
+ // این متغیرها در بلوک DOMContentLoaded مقداردهی اولیه می شوند.
7
+ let searchButton;
8
+ let userQuestionInput; // <--- مطابق با ID در index.html شما
9
+ let searchResultsContainer; // <--- مطابق با ID در index.html شما
10
+ let loadingStatusElement; // <--- مطابق با ID در index.html شما
11
+ let selectionErrorElement; // <--- مطابق با ID در index.html شما
12
+
13
+ let selectAllCheckbox; // <--- مطابق با ID در index.html شما
14
+ let bookCheckboxes; // <--- مطابق با class در index.html شما (HTMLCollection از المان ها)
15
+
16
+ let resultsPerPageSelect; // <--- مطابق با ID در index.html شما
17
+ let similarityThresholdInput; // <--- مطابق با ID در index.html شما
18
+ // نکته: در index.html شما المانی برای نمایش عددی آستانه شباهت در کنار input type="number" وجود ندارد، پس نیازی به متغیر برای آن نیست.
19
+
20
+ // *************************************************
21
+
22
+
23
+ // ****** تعریف URL سرور پایتون برای دریافت Embedding سوال ******
24
  // آدرس لوکال برای ارتباط با Backend در همان کانتینر Docker روی پورت 7860
25
+ // این آدرس ثابت است و نیازی به جایگزینی با آدرس عمومی Space ندارد.
26
+ const EMBEDDING_SERVER_URL = 'http://localhost:7860/get_embedding'; // <--- آدرس لوکال برای Space مشترک
27
+
28
+
29
+ // ****** نگاشت نام فایل JSON به نام کامل کتاب (برای نمایش در نتایج و فیلتر) ******
30
+ // این map اطلاعات نمایشی کتاب را بر اساس value چک باکس (نام فایل JSON) فراهم می کند.
31
+ // مطمئن شوید که value چک باکس ها در index.html شما با کلیدهای این map مطابقت دارد.
32
+ // همچنین book_title در فایل های JSON شما باید با مقادیر این map مطابقت داشته باشد
33
+ // زیرا فیلتر بر اساس book_title در JSON انجام می شود.
34
+ const bookInfo = {
35
+ // 'نام_فایل_json_در_value_چک_باکس': 'نام کامل نمایشی کتاب',
36
+ 'jabe_siah.json': 'جعبه سیاه (منتخب خاطرات اسدالله علم)', // مثال: value چک باکس : نام نمایشی کتاب
37
+ // اگر چک باکس های بیشتری در HTML دارید و فایل های JSON متناظر دارید، ورودی های متناظر را اینجا اضافه کنید:
38
+ // 'ketab_dovom.json': 'نام کتاب دوم',
39
+ // 'ketab_sevom.json': 'نام کتاب سوم',
40
+ // مطمئن شوید که فایل های JSON متناظر (مثلا ketab_dovom.json) در Space آپلود شده اند.
41
+ };
42
+
43
+
44
+ // ****** آستانه شباهت پیش فرض ******
45
+ // اگر عنصر ورودی آستانه پیدا نشد یا مقدار آن نامعتبر بود، از این مقدار استفاده می شود.
46
+ const DEFAULT_SIMILARITY_THRESHOLD = 0.15;
47
+
48
+
49
+ // ****** متغیر سراسری برای نگهداری داده های بارگذاری شده از کتاب های انتخاب شده ******
50
+ let memoirsWithEmbeddings = []; // این آرایه حاوی خاطرات با بردارهای embedding از کتاب های انتخاب شده است.
51
+
52
+
53
+ // ****** توابع کمکی برای مدیریت پیام ها و وضعیت UI ******
54
+
55
+ // تابع کمکی برای نمایش پیام وضعیت بارگذاری/پردازش در المان loadingStatusElement
56
+ function updateStatus(message, isError = false) {
57
+ if (loadingStatusElement) {
58
+ loadingStatusElement.textContent = message;
59
+ loadingStatusElement.style.color = isError ? 'red' : '#666'; // رنگ قرمز برای خطا، خاکستری برای وضعیت
60
+ loadingStatusElement.style.fontWeight = isError ? 'bold' : 'normal'; // خطاها با فونت ضخیم تر
61
+ loadingStatusElement.style.display = message ? 'block' : 'none'; // نمایش المان فقط وقتی پیام دارد
62
+ } else {
63
+ console.log("Status:", message); // لاگ برای توسعه
64
+ }
65
+ // پاک کردن پیام خطای انتخاب کتاب اگر پیامی در status نمایش داده می شود
66
+ if (message && selectionErrorElement) {
67
+ selectionErrorElement.textContent = '';
68
+ selectionErrorElement.style.display = 'none';
69
+ }
 
70
  }
71
 
72
+ // تابع کمکی برای نمایش خطای انتخاب کتاب یا بارگذاری داده در المان selectionErrorElement
73
+ function updateSelectionError(message) {
74
+ if (selectionErrorElement) {
75
+ selectionErrorElement.textContent = message;
76
+ selectionErrorElement.style.color = 'red';
77
+ selectionErrorElement.style.fontWeight = 'bold'; // خطاها با فونت ضخیم تر
78
+ selectionErrorElement.style.display = message ? 'block' : 'none'; // نمایش المان فقط وقتی پیام دارد
79
+ } else {
80
+ console.error("Selection Error Element not found."); // لاگ برای توسعه
81
+ }
82
+ // پاک کردن پیام وضعیت اگر پیامی در selectionError نمایش داده می شود
83
+ if (message && loadingStatusElement) {
84
+ loadingStatusElement.textContent = '';
85
+ loadingStatusElement.style.display = 'none';
86
+ }
87
  }
88
 
89
+ // تابع کمکی برای فعال/غیرفعال کردن دکمه جستجو
90
+ function setButtonEnabled(enabled) {
91
+ if (searchButton) {
92
+ searchButton.disabled = !enabled;
93
+ // console.log("Search button enabled state:", enabled); // لاگ برای توسعه
94
+ } else {
95
+ console.error("Search button element not found.");
96
+ }
97
  }
98
 
99
+ // ****** تابع برای بررسی وضعیت و فعال/غیرفعال کردن دکمه جستجو ******
100
+ // دکمه فقط زمانی فعال می شود که داده بارگذاری شده و کادر سوال خالی نیست و آستانه معتبر است.
101
+ function checkAndEnableSearchButton() {
102
+ const isDataLoaded = memoirsWithEmbeddings.length > 0;
103
+ // اطمینان از وجود userQuestionInput قبل از دسترسی به value
104
+ const isQueryNotEmpty = userQuestionInput && userQuestionInput.value.trim() !== '';
105
+
106
+ // همچنین بررسی می کنیم که مقدار آستانه شباهت معتبر باشد اگر عنصر ورودی وجود دارد
107
+ let isThresholdInputValid = true;
108
+ if (similarityThresholdInput) {
109
+ const inputValue = parseFloat(similarityThresholdInput.value);
110
+ // چک می کنیم عدد معتبر باشد و در محدوده [0, 1] باشد
111
+ if (isNaN(inputValue) || inputValue < 0.0 || inputValue > 1.0) {
112
+ isThresholdInputValid = false; // مقدار نامعتبر است
113
+ }
114
+ } else {
115
+ // اگر عنصر ورودی پیدا نشد، از آستانه پیش فرض استفاده می کنیم و فرض می کنیم معتبر است
116
+ console.warn("Similarity threshold input element not found. Assuming default threshold is valid.");
117
+ }
118
+
119
+ // دکمه فقط زمانی فعال می شود که تمام شرایط لازم برقرار باشد
120
+ setButtonEnabled(isDataLoaded && isQueryNotEmpty && isThresholdInputValid);
121
  }
122
 
123
 
124
+ // ****** تابع اصلی برای بارگذاری داده‌ها از فایل‌های JSON کتاب‌های انتخاب شده ******
125
+ // این تابع هر زمان که انتخاب کتاب ها تغییر می کند، داده ها را بارگذاری مجدد می کند.
126
+ async function updateSelectedBooksData() {
127
+ console.log("Updating selected books data..."); // لاگ برای توسعه
128
+ updateStatus("در حال بارگذاری داده‌ها..."); // پیام برای کاربر
129
+ updateSelectionError(""); // پاک کردن پیام خطای قبلی
130
+ setButtonEnabled(false); // غیرفعال کردن دکمه در طول بارگذاری داده ها
131
+ if (searchResultsContainer) { // پاک کردن نتایج قبلی
132
+ searchResultsContainer.innerHTML = '<p>پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.</p>'; // پیام اولیه
133
  }
 
 
134
 
135
+
136
+ // پیدا کردن چک باکس های کتاب ها که انتخاب شده اند
137
+ // از getElementsByClassName استفاده می کنیم و آن را به آرایه تبدیل می کنیم
138
+ if (!bookCheckboxes || bookCheckboxes.length === 0) {
139
+ console.error("No book checkboxes found with class 'book-checkbox'. Cannot load data.");
140
+ updateStatus(""); // پاک کردن پیام وضعیت
141
+ updateSelectionError("المان‌های انتخاب کتاب یافت نشد. لطفاً ساختار HTML را بررسی کنید."); // پیام خطا برای کاربر
142
+ memoirsWithEmbeddings = [];
143
+ checkAndEnableSearchButton();
144
+ return;
145
+ }
146
+
147
+ // value چک باکس ها همان نام فایل JSON است که باید بارگذاری شود.
148
+ const selectedBookFiles = Array.from(bookCheckboxes)
149
+ .filter(checkbox => checkbox.checked)
150
+ .map(checkbox => checkbox.value);
151
+
152
+ console.log("Selected book files (from checkbox values):", selectedBookFiles); // لاگ برای توسعه
153
+
154
+ // ****** چک کردن اینکه حداقل یک کتاب انتخاب شده باشد ******
155
+ if (selectedBookFiles.length === 0) {
156
+ updateStatus(""); // پاک کردن پیام وضعیت
157
+ updateSelectionError("لطفاً حداقل یک کتاب برای جستجو انتخاب کنید."); // پیام خطا برای کاربر
158
+ console.warn("No books selected. Cannot load data."); // لاگ برای توسعه
159
+ memoirsWithEmbeddings = []; // پاک کردن داده های قبلی
160
+ checkAndEnableSearchButton(); // مطمئن می شویم دکمه غیرفعال بماند
161
+ return; // توقف فرآیند اگر هیچ کتابی انتخاب نشده است
162
+ }
163
+ // ********************************************************
164
+
165
+ memoirsWithEmbeddings = []; // پاک کردن داده‌های قبلی قبل از بارگذاری جدید
166
+
167
  try {
168
+ // بارگذاری همزمان تمام فایل‌های JSON انتخاب شده
169
+ const fetchPromises = selectedBookFiles.map(filename => {
170
+ // آدرس فایل JSON نسبت به محل قرارگیری index.html
171
+ // فرض بر این است که فایل های JSON در کنار index.html در همان پوشه قرار دارند
172
+ const filePath = `./${filename}`;
173
+ console.log(`Attempting to fetch: ${filePath}`); // لاگ برای توسعه
174
+ return fetch(filePath).then(response => {
175
+ if (!response.ok) {
176
+ // اگر فایل پیدا نشد یا خطای دیگری رخ داد (مثلاً 404)
177
+ console.error(`Workspace error for ${filePath}: Status ${response.status}`); // لاگ خطا برای توسعه
178
+ // تلاش برای خواندن متن خطا از پاسخ برای اطلاعات بیشتر
179
+ return response.text().then(text => {
180
+ console.error(`Response body for ${filePath}: ${text}`);
181
+ throw new Error(`Error fetching file: "${filename}" (Status: ${response.status}). Check if the file exists at "${filePath}".`);
182
+ }).catch(() => {
183
+ // اگر خواندن متن خطا هم موفق نبود
184
+ throw new Error(`Error fetching file: "${filename}" (Status: ${response.status}). Check if the file exists at "${filePath}".`);
185
+ });
186
+
187
+ }
188
+ // بررسی Content-Type پاسخ برای اطمینان از اینکه واقعا JSON است
189
+ const contentType = response.headers.get("content-type");
190
+ if (!contentType || !contentType.includes("application/json")) {
191
+ console.error(`Workspace error for ${filePath}: Expected application/json, but received ${contentType}`);
192
+ throw new Error(`Error fetching file: "${filename}". Expected JSON, but received unexpected content type.`);
193
+ }
194
+ return response.json();
195
+ })
196
+ .catch(error => {
197
+ // گرفتن خطاهای شبکه یا تجزیه JSON
198
+ console.error(`Failed to fetch or parse file "${filename}":`, error); // لاگ خطا برای توسعه
199
+ throw new Error(`Failed to load data for book file "${filename}". ${error.message || 'Unknown error'}. Check file name and location.`);
200
+ });
201
  });
202
 
 
 
 
 
203
 
204
+ const booksData = await Promise.all(fetchPromises); // انتظار برای دانلود و تجزیه همه فایل ها
 
 
 
 
205
 
206
+ // ترکیب داده‌ها از تمام فایل‌های JSON بارگذاری شده و افزودن اطلاعات کتاب
207
+ booksData.forEach((data, index) => { // اضافه کردن index برای دسترسی به selectedBookFiles
208
+ const filename = selectedBookFiles[index]; // نام فایل JSON فعلی (value چک باکس)
209
+ // پیدا کردن نام نمایشی کتاب از map bookInfo بر اساس نام فایل
210
+ const bookDisplayName = bookInfo[filename] || filename;
 
211
 
212
+ if (Array.isArray(data)) {
213
+ // فیلتر کردن آیتم هایی که بردار embedding معتبر دارند و افزودن اطلاعات کتاب به آن ها
214
+ const memoirsWithBookInfo = data.filter(item => item && typeof item === 'object' && item.embedding && Array.isArray(item.embedding) && item.embedding.length > 0) // اضافه کردن چک item !== null و item از نوع object باشد
215
+ .map(item => ({
216
+ ...item, // کپی کردن تمام پراپرتی های موجود (passage, reference, embedding, passage_original, etc.)
217
+ book_title: bookDisplayName, // افزودن نام نمایشی کتاب
218
+ book_file: filename // افزودن نام فایل اصلی کتاب
219
+ }));
220
 
221
+ memoirsWithEmbeddings = memoirsWithEmbeddings.concat(memoirsWithBookInfo);
 
 
 
222
 
223
+ // هشدار برای آیتم هایی که بردار ندارند یا فرمتشان مشکل دارد (فقط در کنسول)
224
+ const invalidMemoirsCount = data.length - memoirsWithBookInfo.length; // تعداد کل - تعداد معتبر با اطلاعات کتاب
225
+ if(invalidMemoirsCount > 0){
226
+ console.warn(`Skipped ${invalidMemoirsCount} items from file "${filename}" due to missing or invalid embedding or format.`);
227
+ }
228
 
229
+ } else {
230
+ console.error(`Loaded data from "${filename}" is not an array:`, data); // لاگ برای توسعه
231
+ updateSelectionError(`خطا در فرمت داده از فایل "${filename}". انتظار آرایه داشتیم.`);
 
 
232
  }
233
+ });
234
+
235
+ const loadedBooksCount = selectedBookFiles.length;
236
+ const totalPassagesLoaded = memoirsWithEmbeddings.length;
237
+
238
+ if (totalPassagesLoaded === 0) {
239
+ console.warn("No valid passages with embeddings were loaded after processing all files.");
240
+ updateStatus(`داده‌ها بارگذاری شد، اما هیچ خاطره‌ای با بردار معتبر از کتاب‌های انتخاب شده (${loadedBooksCount} کتاب) یافت نشد.`, true); // پیام خطا برای کاربر
241
+ updateSelectionError("هیچ خاطره‌ای با بردار معتبر از کتاب‌های انتخاب شده یافت نشد. فرمت فایل‌های JSON و وجود فیلد embedding را بررسی کنید.");
242
+ } else {
243
+ console.log(`Successfully loaded data from ${loadedBooksCount} book(s). Total valid passages loaded: ${totalPassagesLoaded}`); // لاگ برای توسعه
244
+ updateStatus(`داده‌ها از ${loadedBooksCount} کتاب با موفقیت بارگذاری شد. مجموع خاطرات قابل جستجو: ${totalPassagesLoaded}. آماده جستجو هستید.`); // پیام برای کاربر
245
+ }
246
+
247
+ checkAndEnableSearchButton(); // بررسی و فعال کردن دکمه پس از بارگذاری داده
248
 
 
 
 
 
249
 
250
  } catch (error) {
251
+ console.error("Error loading selected books data:", error); // لاگ برای توسعه
252
+ updateStatus("خطا در بارگذاری داده‌ها.", true); // پیام برای کاربر
253
+ // پیام خطای برای کاربر شامل جزئیات بیشتر از علت خطا
254
+ updateSelectionError(`خطا در بارگذاری داده‌ها: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر.`);
255
+ memoirsWithEmbeddings = []; // اطمینان از خالی بودن داده در صورت خطا
256
+ checkAndEnableSearchButton(); // مطمئن می شویم دکمه غیرفعال بماند
257
+ if (searchResultsContainer) { // بازگرداندن پیام اولیه در صورت خطا
258
+ searchResultsContainer.innerHTML = '<p>پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.</p>';
259
+ }
260
+ } finally {
261
+ // این بخش نیازی به checkAndEnableSearchButton ندارد چون در بالا صدا زده می شود
262
  }
263
  }
264
 
 
 
 
265
 
266
+ // ****** تابع کمکی برای محاسبه شباهت کسینوسی بین دو بردار ******
267
+ function cosineSimilarity(vecA, vecB) {
268
+ // بررسی وجود و صحت بردارها قبل از شروع محاسبات
269
+ if (!vecA || !vecB || vecA.length !== vecB.length || vecA.length === 0) {
270
+ console.error("Cosine Similarity Error: Invalid or empty vectors provided.", {vecA_length: vecA ? vecA.length : 'null', vecB_length: vecB ? vecB.length : 'null'});
271
+ return 0; // برگرداندن 0 در صورت ورودی نامعتبر
272
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
 
274
+ let dotProduct = 0;
275
+ let magnitudeA = 0;
276
+ let magnitudeB = 0;
277
 
278
+ // محاسبه نقطه ضرب و مربع اندازه بردارها در یک حلقه برای بهینه سازی
279
+ for (let i = 0; i < vecA.length; i++) {
280
+ dotProduct += vecA[i] * vecB[i];
281
+ magnitudeA += vecA[i] * vecA[i];
282
+ magnitudeB += vecB[i] * vecB[i];
283
+ }
284
 
285
+ // گرفتن ریشه دوم برای اندازه بردارها
286
+ magnitudeA = Math.sqrt(magnitudeA);
287
+ magnitudeB = Math.sqrt(magnitudeB);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
+ // جلوگیری از تقسیم بر صفر اگر یکی از بردارها صفر باشد
290
+ if (magnitudeA === 0 || magnitudeB === 0) {
291
+ //console.warn("Cosine Similarity Warning: Magnitude is zero for one or both vectors."); // لاگ هشدار برای توسعه (اختیاری)
292
+ return 0;
293
+ }
 
 
 
 
294
 
295
+ // محاسبه شباهت کسینوسی
296
+ const similarity = dotProduct / (magnitudeA * magnitudeB);
297
+ //console.log(`Calculated similarity: ${similarity}`); // لاگ برای توسعه (اختیاری، ممکن است خیلی زیاد شود)
298
+ return similarity; // برگرداندن امتیاز شباهت
299
+ }
 
 
 
 
300
 
301
+ // تابع کمکی برای حذف بخش کلمات کلیدی از متن Passage
302
+ function cleanPassageTextForDisplay(passage) {
303
+ if (!passage || typeof passage !== 'string') {
304
+ console.warn("cleanPassageTextForDisplay received invalid input:", passage);
305
+ return ''; // برگرداندن رشته خالی برای ورودی نامعتبر
306
+ }
307
+ // فرض می کنیم فرمت ترکیب متن همان ' <کلیدواژه ها: ...>' است.
308
+ const startDelimiter = ' <کلیدواژه ها: ';
309
+ const startIndex = passage.indexOf(startDelimiter);
310
 
311
+ if (startIndex === -1) {
312
+ return passage.trim(); // اگر جداکننده پیدا نشد، کل متن را برگردان (با حذف فاصله های اضافی ابتدا و انتها)
313
  }
 
314
 
315
+ // اگر جداکننده پیدا شد، فقط بخش قبل از آن را برگردان
316
+ let cleanText = passage.substring(0, startIndex);
317
+ return cleanText.trim(); // حذف فاصله های اضافی ابتدا و انتهای متن تمیز شده
318
+ }
319
 
 
320
 
321
+ // ****** تعریف کامل تابع async function searchMemoirs() { ... } ******
322
+ // این تابع شامل منطق اصلی جستجو است.
323
+ async function searchMemoirs() {
324
+ console.log("Search triggered."); // لاگ برای توسعه
325
+ console.log(`Data loaded state (passages count): ${memoirsWithEmbeddings.length}`); // لاگ برای توسعه
326
 
327
+ // ****** چک کردن اینکه داده ها (از کتاب های انتخاب شده) بارگذاری شده باشند ******
328
+ if (memoirsWithEmbeddings.length === 0) {
329
+ console.warn("No memoir data loaded. Cannot search."); // ��اگ برای توسعه
330
+ updateSelectionError("لطفاً ابتدا کتاب‌های مورد نظر برای جستجو را انتخاب کرده و منتظر بارگذاری داده‌ها بمانید."); // پیام خطا برای کاربر
331
+ // searchResultsContainer در ابتدای updateSelectedBooksData() پاک می شود.
332
+ return;
333
+ }
334
+ // **************************************************************************
335
 
336
+ // اطمینان از وجود userQuestionInput قبل از دسترسی به value
337
+ if (!userQuestionInput) {
338
+ console.error("Search input element not found. Cannot get query.");
339
+ updateSelectionError("المان ورودی جستجو پیدا نشد. امکان جستجو وجود ندارد.");
340
+ return;
341
+ }
342
+ const query = userQuestionInput.value.trim();
343
+ console.log(`Query text is: "${query}"`); // لاگ برای توسعه
344
+
345
+ if (!query) {
346
+ if (searchResultsContainer) { // استفاده از نام متغیر صحیح
347
+ searchResultsContainer.innerHTML = `<p>لطفاً عبارت مورد نظر برای جستجو را وارد کنید.</p>`; // پیام برای کاربر
348
+ }
349
+ console.warn("Search query is empty."); // لاگ برای توسعه
350
+ updateSelectionError("لطفاً عبارت مورد نظر برای جستجو را وارد کنید."); // پیام خطا برای کاربر
351
+ return;
352
  }
353
 
354
+ // نمایش پیام "در حال جستجو" و غیرفعال کردن دکمه
355
+ updateStatus("در حال جستجو...", false); // رنگ خاکستری
356
+ updateSelectionError(""); // پاک کردن پیام خطای احتمالی قبلی
357
+ if (searchResultsContainer) { // پاک کردن نتایج قبلی
358
+ searchResultsContainer.innerHTML = '';
359
+ }
360
+ setButtonEnabled(false); // غیرفعال کردن دکمه در طول جستجو
361
 
 
 
 
 
362
 
363
+ try {
364
+ console.log("Requesting query embedding from Python server..."); // لاگ برای توسعه
 
 
365
 
366
+ const serverResponse = await fetch(EMBEDDING_SERVER_URL, {
367
+ method: 'POST',
368
+ headers: {
369
+ 'Content-Type': 'application/json'
370
+ },
371
+ body: JSON.stringify({ query: query })
372
+ });
373
+
374
+ if (!serverResponse.ok) {
375
+ const errorBody = await serverResponse.text(); // بخوان به صورت متن برای اطلاعات بیشتر
376
+ console.error(`Server responded with status ${serverResponse.status}:`, errorBody); // لاگ خطا برای توسعه
377
+
378
+ // تلاش برای تجزیه پاسخ خطا به صورت JSON اگر سرور خطای JSON برگردانده باشد
379
+ let serverErrorMessage = `خطا از سرور (${serverResponse.status}).`;
380
+ try {
381
+ const errorJson = JSON.parse(errorBody);
382
+ if (errorJson.error) {
383
+ serverErrorMessage += ` پیام: ${errorJson.error}`;
384
+ }
385
+ } catch (e) {
386
+ // اگر پاسخ خطا JSON نبود، متن خام را اضافه کن
387
+ serverErrorMessage += ` پاسخ خام: ${errorBody.substring(0, Math.min(errorBody.length, 100))}...`; // نمایش بخشی از پاسخ خام (حداکثر 100 کاراکتر)
388
  }
389
 
390
+ updateStatus("جستجو با خطا مواجه شد.", true); // پیام اصلی برای کاربر (رنگ قرمز)
391
+ updateSelectionError(serverErrorMessage + " جزئیات بیشتر در کنسول مرورگر."); // نمایش جزئیات خطا در بخش خطای انتخاب
392
+ // نیازی به throw new Error نیست چون در finally دکمه فعال می شود و پیام ها نمایش داده شده اند.
393
+ return; // خروج از تابع searchMemoirs پس از خطا
394
 
395
+ }
 
396
 
397
+ const serverData = await serverResponse.json();
398
+ const queryEmbeddingArray = serverData.embedding;
399
+
400
+ if (!queryEmbeddingArray || !Array.isArray(queryEmbeddingArray) || queryEmbeddingArray.length === 0) {
401
+ console.error("Server returned an invalid or empty embedding:", serverData); // لاگ خطا برای توسعه
402
+ updateStatus("جستجو با خطا مواجه شد.", true); // پیام اصلی برای کاربر
403
+ updateSelectionError("سرور بردار جستجو را به درستی برنگرداند. جزئیات در کنسول مرورگر."); // نمایش جزئیات خطا در بخش خطای انتخاب
404
+ return; // خروج از تابع searchMemoirs
405
+ }
406
 
407
+ console.log("Query embedding received from server successfully."); // لاگ برای توسعه
408
+ console.log(`Query embedding dimensions: ${queryEmbeddingArray.length}`); // لاگ برای توسعه
409
+ console.log("Calculating similarities in browser..."); // لاگ برای توسعه
410
+
411
+ const searchResults = [];
412
+ // محاسبه شباهت با تمام قطعات خاطره ای که از کتاب های انتخاب شده بارگذاری شده اند
413
+ for (const memoir of memoirsWithEmbeddings) { // memoirsWithEmbeddings حاوی book_title و book_file است
414
+ // اطمینان از وجود و صحت بردار embedding در آیتم خاطره
415
+ // همچنین اطمینان از این که بردار خاطره همان ابعاد بردار سوال را دارد
416
+ if (memoir.embedding && Array.isArray(memoir.embedding) && memoir.embedding.length === queryEmbeddingArray.length) {
417
+ const similarity = cosineSimilarity(queryEmbeddingArray, memoir.embedding);
418
+ // اضافه کردن تمام فیلدهای اصلی خاطره و امتیاز شباهت به نتیجه
419
+ // استفاده از نام فیلد similarity برای امتیاز شباهت
420
+ searchResults.push({ ...memoir, similarity: similarity });
421
+ } else {
422
+ // هشدار برای آیتم های بدون بردار یا با ابعاد نامعتبر (فقط در کنسول)
423
+ console.warn(`Skipping memoir due to missing or invalid embedding or dimension mismatch: ${memoir.book_title || memoir.book_file || 'Unknown Book'} - ${memoir.reference || 'Unknown Reference'}`);
424
  }
425
+ }
426
+ console.log(`Similarity calculation complete. Found ${searchResults.length} results with valid embeddings.`); // لاگ برای توسعه
427
+
428
+
429
+ // ****** خواندن آستانه شباهت از ورودی کاربر و فیلتر کردن ******
430
+ let currentSimilarityThreshold = DEFAULT_SIMILARITY_THRESHOLD; // از مقدار پیش فرض ثابت به عنوان fallback استفاده می کنیم
431
+ if (similarityThresholdInput) {
432
+ const inputValue = parseFloat(similarityThresholdInput.value);
433
+ // چک می کنیم عدد معتبر باشد و در محدوده [0, 1] باشد
434
+ if (!isNaN(inputValue) && inputValue >= 0.0 && inputValue <= 1.0) {
435
+ currentSimilarityThreshold = inputValue;
436
+ console.log("Using user-defined similarity threshold:", currentSimilarityThreshold); // لاگ برای توسعه
437
+ } else {
438
+ console.warn("Invalid similarity threshold input value, using default:", DEFAULT_SIMILARITY_THRESHOLD); // لاگ برای توسعه
439
+ updateSelectionError(`مقدار آستانه شباهت وارد شده (${similarityThresholdInput.value}) معتبر نیست. از مقدار پیش فرض ${DEFAULT_SIMILARITY_THRESHOLD.toFixed(2)} استفاده می‌شود.`); // پیام خطا برای کاربر
440
+ currentSimilarityThreshold = DEFAULT_SIMILARITY_THRESHOLD; // بازگشت به مقدار پیش فرض ثابت
441
+ // optional: set the input value back to the default if invalid
442
+ // similarityThresholdInput.value = DEFAULT_SIMILARITY_THRESHOLD.toFixed(2); // این خط می تواند مقدار نمایش داده شده را هم به پیش فرض برگرداند
 
 
 
443
  }
444
+ } else {
445
+ console.warn("Similarity threshold input element not found, using default threshold:", DEFAULT_SIMILARITY_THRESHOLD); // لاگ برای توسعه
446
+ }
447
 
448
 
449
+ const filteredResults = searchResults.filter(result => result.similarity >= currentSimilarityThreshold);
450
+ console.log(`Filtered results based on threshold ${currentSimilarityThreshold.toFixed(2)}: ${filteredResults.length} results remaining.`); // لاگ برای توسعه
451
+ // *************************************************************
452
 
 
 
453
 
454
+ console.log("Sorting results by similarity..."); // لاگ برای توسعه
455
+ // Sort the filtered results by similarity in descending order (highest similarity first)
456
+ filteredResults.sort((a, b) => b.similarity - a.similarity);
457
+ console.log("Filtered results sorted."); // لاگ برای توسعه
458
 
459
+ // ****** انتخاب تعداد نتایج برتر بر اساس انتخاب کاربر (از نتایج فیلتر شده) ******
460
+ let finalResultsPerPage = 10; // مقدار پیش فرض برای نتایج در صفحه
461
+ if (resultsPerPageSelect) {
462
+ const selectedValue = parseInt(resultsPerPageSelect.value, 10); // خواندن مقدار انتخاب شده و تبدیل به عدد صحیح
463
+ // اطمینان از اینکه selectedValue یک عدد معتبر و مثبت است
464
+ finalResultsPerPage = (!isNaN(selectedValue) && selectedValue > 0) ? selectedValue : 10; // مقدار پیش فرض 10 اگر نامعتبر باشد
465
+ } else {
466
+ console.warn("Results per page select element not found, using default:", finalResultsPerPage);
467
+ }
468
 
469
 
470
+ const topResults = filteredResults.slice(0, finalResultsPerPage); // انتخاب فقط N نتیجه برتر از لیست فیلتر شده
471
+ console.log(`Displaying top ${topResults.length} results from filtered list based on user selection (${finalResultsPerPage} per page setting).`);
472
+ // ********************************************
473
+
474
+
475
+ // ****** منطق نمایش نتایج و پیام ها ******
476
+ if (searchResultsContainer) { // استفاده از نام متغیر صحیح
477
+ // نتایج قبلی در ابتدای تابع پاک شده اند.
478
+
479
+ if (topResults.length === 0) { // اگر هیچ نتیجه ای یافت نشد
480
+ // پیام به کاربر، شامل مقدار آستانه ای که استفاده شده است
481
+ searchResultsContainer.innerHTML = `<p>نتیجه مرتبطی با آستانه شباهت مورد نظر (${currentSimilarityThreshold.toFixed(2)}) یافت نشد. سعی کنید عبارت دیگری را جستجو کنید یا آستانه را کاهش دهید.</p>`;
482
+ console.log("No relevant results found after filtering and slicing.");
483
+ } else { // اگر نتایجی یافت شد
484
+ console.log("Results found, updating DOM.");
485
+
486
+ // ایجاد کانتینر برای لیست نتایج برای کنترل بهتر استایل دهی
487
+ const resultsList = document.createElement('div');
488
+ resultsList.classList.add('results-list');
489
+
490
+
491
+ topResults.forEach(result => {
492
+ // ایجاد المان برای هر آیتم نتیجه
493
+ const resultItem = document.createElement('div');
494
+ resultItem.classList.add('result-item');
495
+
496
+ // ****** نمایش امتیاز شباهت ******
497
+ const similarityElement = document.createElement('p'); // استفاده از p مطابق با ساختار قبلی شما
498
+ similarityElement.classList.add('result-similarity');
499
+ // نمایش امتیاز با 4 رقم اعشار
500
+ similarityElement.textContent = `شباهت: ${result.similarity.toFixed(4)}`;
501
+ // ******************************
502
+
503
+ // نمایش نام کتاب
504
+ const bookTitleElement = document.createElement('p'); // استفاده از p مطابق با ساختار قبلی شما
505
+ bookTitleElement.classList.add('result-book-title');
506
+ bookTitleElement.textContent = `از کتاب: ${result.book_title || 'نامشخص'}`;
507
+
508
+ // نمایش مرجع خاطره
509
+ const referenceElement = document.createElement('p'); // استفاده از p مطابق با ساختار قبلی شما
510
+ referenceElement.classList.add('result-reference');
511
+ referenceElement.innerHTML = `<strong>مرجع:</strong> ${result.reference || 'نامشخص'}`; // استفاده از innerHTML برای bold کردن "مرجع:"
512
+
513
+
514
+ // نمایش متن خاطره (با حذف کلمات کلیدی)
515
+ const passageElement = document.createElement('p'); // استفاده از p مطابق با ساختار قبلی شما
516
+ passageElement.classList.add('result-passage');
517
+ passageElement.textContent = cleanPassageTextForDisplay(result.passage || ''); // استفاده از متن تمیز شده
518
+
519
+ // ****** اضافه کردن دکمه کپی ******
520
+ const copyButton = document.createElement('button');
521
+ copyButton.classList.add('copy-button'); // کلاس CSS برای استایل دهی
522
+ copyButton.textContent = 'کپی متن'; // متن دکمه
523
+
524
+ // ****** اضافه کردن استایل Inline برای تست عیب‌یابی (موقت) ******
525
+ // این استایل ها برای کمک به ظاهر شدن دکمه اضافه شده اند.
526
+ // اگر با این استایل ها دکمه ظاهر شد، مشکل در فایل style.css یا اعمال آن است.
527
+ // پس از حل مشکل نمایش در style.css، این خطوط را حذف کنید.
528
+ copyButton.style.display = 'block';
529
+ copyButton.style.marginTop = '10px';
530
+ copyButton.style.marginLeft = 'auto'; // برای راست چین کردن در RTL
531
+ copyButton.style.marginRight = '0';
532
+ copyButton.style.backgroundColor = '#007bff'; // رنگ آبی
533
+ copyButton.style.color = 'white';
534
+ copyButton.style.padding = '5px 5px'; // padding کمی کمتر
535
+ copyButton.style.border = 'none';
536
+ copyButton.style.borderRadius = '4px';
537
+ copyButton.style.cursor = 'pointer';
538
+ copyButton.style.fontSize = '0.8em'; // فونت کمی کوچکتر
539
+ copyButton.style.fontFamily = "'Vazirmatn', sans-serif"; // اطمینان از اعمال فونت
540
+ // *****************************************************************
541
+
542
+
543
+ // Event listener برای دکمه کپی (همان کد قبلی)
544
+ copyButton.addEventListener('click', async () => {
545
+ // ****** ساخت متنی که باید کپی شود (متن خاطره، مرجع، نام کتاب، شباهت) ******
546
+ const passageText = cleanPassageTextForDisplay(result.passage || ''); // متن تمیز شده خاطره
547
+ const referenceText = result.reference || 'نامشخص'; // متن مرجع
548
+ const bookTitleText = result.book_title || 'نامشخص'; // متن نام کتاب
549
+ const similarityText = result.similarity !== undefined ? result.similarity.toFixed(4) : 'N/A'; // متن شباهت
550
+
551
+ // ترکیب متن ها با فرمت دلخواه (مثلاً با خط جدید بین هر بخش)
552
+ const textToCopy = `خاطره:\n${passageText}\n\nمرجع: ${referenceText}\nاز کتاب: ${bookTitleText}\nشباهت: ${similarityText}`; // اضافه کردن امتیاز شباهت به متن کپی شده
553
+ // *****************************************************************
554
+
555
+ try {
556
+ // استفاده از Clipboard API برای کپی کردن متن
557
+ await navigator.clipboard.writeText(textToCopy);
558
+ copyButton.textContent = 'کپی شد!'; // پیام بازخورد کوتاه
559
+ // برگرداندن متن دکمه به حالت اولیه بعد از 2 ثانیه
560
+ setTimeout(() => {
561
+ copyButton.textContent = 'کپی متن';
562
+ }, 2000);
563
+ console.log("Passage, reference, book title, and similarity copied to clipboard."); // لاگ موفقیت
564
+ } catch (err) {
565
+ console.error('Failed to copy text: ', err); // لاگ خطا در کنسول
566
+ copyButton.textContent = 'خطا در کپی'; // پیام بازخورد خطا
567
+ setTimeout(() => {
568
+ copyButton.textContent = 'کپی متن';
569
+ }, 2000);
570
+ }
571
+ });
572
+ // ******************************
573
+
574
+
575
+ // اضافه کردن عناصر به آیتم نتیجه با ترتیب دلخواه
576
+ // ایجاد یک div یا span برای نگه داشتن مرجع، نام کتاب و شباهت برای کنترل بهتر استایل دهی (همانند CSS شما)
577
+ const metaInfoDiv = document.createElement('div');
578
+ // در HTML شما از p های جداگانه برای reference, book-title, similarity استفاده شده بود
579
+ // و استایل هایی برای کلاس های result-reference, result-book-title, result-similarity داشتید.
580
+ // بهتر است المان ها را جداگانه اضافه کنیم تا کلاس های CSS شما اعمال شوند.
581
+
582
+ resultItem.appendChild(passageElement); // اول متن
583
+ resultItem.appendChild(referenceElement); // بعد مرجع
584
+ resultItem.appendChild(bookTitleElement); // بعد نام کتاب
585
+ resultItem.appendChild(similarityElement); // بعد امتیاز شباهت (که با float در CSS سمت راست میرود)
586
+ resultItem.appendChild(copyButton); // دکمه کپی در انتها
587
+
588
+ // **************************************************************************
589
+
590
+
591
+ resultsList.appendChild(resultItem);
592
+ });
593
 
594
+ searchResultsContainer.appendChild(resultsList); // اضافه کردن لیست نتایج به کانتینر اصلی
595
+ console.log("DOM updated with results.");
596
 
597
+ // لاگ کردن نتایج برای توسعه (شامل جزئیات بیشتر)
598
+ console.log(`Top ${topResults.length} results displayed (reference, book, similarity, passage start):`);
599
+ topResults.forEach(result => {
600
+ console.log(` Book: ${result.book_title || 'Unknown'}, Ref: ${result.reference || 'N/A'}, Sim: ${result.similarity.toFixed(4)}, Passage: "${result.passage ? result.passage.substring(0, 50).replace(/\n/g, ' ') + '...' : 'N/A'}"`); // لاگ جزئیات بیشتر و جایگزینی خط جدید در لاگ
601
+ });
602
+ }
 
603
 
604
+ // به‌روزرسانی پیام وضعیت پس از جستجو
605
+ if (topResults.length > 0) {
606
+ updateStatus(`جستجو به پایان رسید. ${topResults.length} نتیجه برتر (پس از فیلتر با آستانه ${currentSimilarityThreshold.toFixed(2)}) نمایش داده شد.`, false); // پیام موفقیت (رنگ خاکستری)
607
+ } else {
608
+ updateStatus(`جستجو به پایان رسید. هیچ نتیجه مرتبطی با آستانه شباهت مورد نظر (${currentSimilarityThreshold.toFixed(2)}) یافت نشد. سعی کنید عبارت دیگری را جستجو کنید یا آستانه را کاهش دهید.`, false); // پیام وضعیت (رنگ خاکستری)
609
+ }
 
 
610
 
611
 
612
+ } else {
613
+ console.error("Could not find searchResultsContainer to display results.");
614
+ updateStatus("جستجو با خطا مواجه شد.", true); // پیام خطا (رنگ قرمز)
615
+ updateSelectionError("المان نمایش نتایج پیدا نشد. لطفاً ساختار HTML را بررسی کنید."); // پیام خطا در بخش انتخاب
616
+ }
617
+
618
+
619
+ } catch (error) {
620
+ // مدیریت خطا هنگام درخواست سرور یا پردازش پاسخ
621
+ console.error("Error during search:", error);
622
+ if (searchResultsContainer) { // استفاده از نام متغیر صحیح
623
+ searchResultsContainer.innerHTML = `<p style="color: red;">هنگام جستجو خطایی رخ داد: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر موجود است.</p>`;
624
+ }
625
+ updateStatus("جستجو با خطا مواجه شد.", true); // پیام خطا (رنگ قرمز)
626
+ updateSelectionError(`هنگام جستجو خطایی رخ داد: ${error.message || 'خطای نامشخص'}.`); // نمایش خطای اصلی در بخش خطای انتخاب
627
+ } finally {
628
+ // در نهایت، چه جستجو موفقیت آمیز باشد چه با خطا مواجه شود، دکمه جستجو را فعال می کنیم.
629
+ // این اطمینان را می دهد که دکمه همیشه پس از پایان فرآیند جستجو قابل استفاده مجدد است (مگر اینکه داده ها بارگذاری نشده باشند).
630
+ // checkAndEnableSearchButton() این کار را انجام می دهد.
631
+ checkAndEnableSearchButton(); // فعال کردن مجدد دکمه جستجو
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
  }
633
+ }
634
+ // پایان تعریف تابع async function searchMemoirs()
635
 
636
 
637
+ // ****** بلوک DOMContentLoaded: این کد پس از بارگذاری کامل ساختار صفحه اجرا می شود ******
638
+ document.addEventListener('DOMContentLoaded', () => {
639
+ console.log("DOM fully loaded and parsed. Initializing script.");
640
+
641
+ // ****** دریافت رفرنس المان های HTML (مطابق با ID ها و کلاس ها در index.html شما) ******
642
+ // این رفرنس ها متغیرهای سراسری تعریف شده در بالای فایل هستند.
643
+ searchButton = document.getElementById('searchButton');
644
+ userQuestionInput = document.getElementById('userQuestion');
645
+ searchResultsContainer = document.getElementById('searchResults');
646
+ loadingStatusElement = document.getElementById('loadingStatus');
647
+ selectionErrorElement = document.getElementById('selectionError');
648
+
649
+ selectAllCheckbox = document.getElementById('select_all_books');
650
+ // getElementsByClassName برمی گرداند HTMLCollection زنده.
651
+ bookCheckboxes = document.getElementsByClassName('book-checkbox');
652
+
653
+ resultsPerPageSelect = document.getElementById('resultsPerPage');
654
+ similarityThresholdInput = document.getElementById('similarityThresholdInput');
655
+ // **********************************************************************************
656
+
657
+
658
+ // ****** چک کردن وجود المان های ضروری برای جلوگیری از خطا ******
659
+ // چک می کنیم که تمام المان های مورد نیاز برای اجرای اسکریپت پیدا شده باشند.
660
+ // bookCheckboxes باید وجود داشته باشد و حداقل یک المان (چک باکس) داشته باشد.
661
+ const requiredElementsFound = searchButton && userQuestionInput && searchResultsContainer && loadingStatusElement && selectionErrorElement && selectAllCheckbox && bookCheckboxes && bookCheckboxes.length > 0 && resultsPerPageSelect && similarityThresholdInput;
662
+
663
+ if (requiredElementsFound) {
664
+ console.log("All critical DOM elements found. Proceeding with initialization.");
665
+
666
+ // ****** تنظیم Event Listeners ******
667
+
668
+ // Listener برای دکمه جستجو: فراخوانی تابع searchMemoirs هنگام کلیک
669
+ searchButton.addEventListener('click', searchMemoirs);
670
+ console.log("Search button click listener added.");
671
+
672
+ // Listener برای کلید Enter در کادر ورودی سوال: شبیه سازی کلیک روی دکمه جستجو
673
+ userQuestionInput.addEventListener('keypress', (event) => {
674
+ if (event.key === 'Enter') {
675
+ event.preventDefault(); // جلوگیری از ارسال فرم پیش فرض مرورگر
676
+ // فقط اگر دکمه جستجو فعال است، کلیک را شبیه سازی کن
677
+ if (!searchButton.disabled) {
678
+ searchButton.click();
679
+ console.log("Enter key pressed in search input, simulating search button click."); // La