Jahadona commited on
Commit
3fcfdc6
·
verified ·
1 Parent(s): 2c35027

Update script.js

Browse files
Files changed (1) hide show
  1. script.js +235 -110
script.js CHANGED
@@ -258,12 +258,15 @@ async function updateSelectedBooksData() {
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) {
@@ -299,6 +302,7 @@ function cosineSimilarity(vecA, vecB) {
299
  }
300
 
301
  // تابع کمکی برای حذف بخش کلمات کلیدی از متن Passage
 
302
  function cleanPassageTextForDisplay(passage) {
303
  if (!passage || typeof passage !== 'string') {
304
  console.warn("cleanPassageTextForDisplay received invalid input:", passage);
@@ -319,7 +323,7 @@ function cleanPassageTextForDisplay(passage) {
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}`); // لاگ برای توسعه
@@ -363,6 +367,7 @@ async function searchMemoirs() {
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: {
@@ -371,6 +376,7 @@ async function searchMemoirs() {
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); // لاگ خطا برای توسعه
@@ -394,9 +400,11 @@ async function 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); // پیام اصلی برای کاربر
@@ -408,16 +416,16 @@ async function searchMemoirs() {
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'}`);
@@ -426,95 +434,107 @@ async function searchMemoirs() {
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');
@@ -525,79 +545,40 @@ async function searchMemoirs() {
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
 
@@ -610,7 +591,7 @@ async function searchMemoirs() {
610
 
611
 
612
  } else {
613
- console.error("Could not find searchResultsContainer to display results.");
614
  updateStatus("جستجو با خطا مواجه شد.", true); // پیام خطا (رنگ قرمز)
615
  updateSelectionError("المان نمایش نتایج پیدا نشد. لطفاً ساختار HTML را بررسی کنید."); // پیام خطا در بخش انتخاب
616
  }
@@ -618,17 +599,15 @@ async function searchMemoirs() {
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()
@@ -644,14 +623,14 @@ document.addEventListener('DOMContentLoaded', () => {
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
 
@@ -676,4 +655,150 @@ document.addEventListener('DOMContentLoaded', () => {
676
  // فقط اگ�� دکمه جستجو فعال است، کلیک را شبیه سازی کن
677
  if (!searchButton.disabled) {
678
  searchButton.click();
679
- console.log("Enter key pressed in search input, simulating search button click."); // La
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  searchResultsContainer.innerHTML = '<p>پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.</p>';
259
  }
260
  } finally {
261
+ // در نهایت، چه بارگذاری موفقیت آمیز باشد چه با خطا مواجه شود، دکمه جستجو وضعیت خود را بررسی می کند.
262
+ // checkAndEnableSearchButton() این کار را انجام می دهد.
263
+ // checkAndEnableSearchButton(); // در اینجا نیازی نیست، چون در هر دو شاخه موفقیت و خطا صدا زده می شود.
264
  }
265
  }
266
 
267
 
268
  // ****** تابع کمکی برای محاسبه شباهت کسینوسی بین دو بردار ******
269
+ // این تابع همان تابع استاندارد محاسبه شباهت کسینوسی است.
270
  function cosineSimilarity(vecA, vecB) {
271
  // بررسی وجود و صحت بردارها قبل از شروع محاسبات
272
  if (!vecA || !vecB || vecA.length !== vecB.length || vecA.length === 0) {
 
302
  }
303
 
304
  // تابع کمکی برای حذف بخش کلمات کلیدی از متن Passage
305
+ // این تابع بر اساس فرمتی که در generate_embeddings.py برای ترکیب متن و کلیدواژه ها استفاده شده، کار می کند.
306
  function cleanPassageTextForDisplay(passage) {
307
  if (!passage || typeof passage !== 'string') {
308
  console.warn("cleanPassageTextForDisplay received invalid input:", passage);
 
323
 
324
 
325
  // ****** تعریف کامل تابع async function searchMemoirs() { ... } ******
326
+ // این تابع شامل منطق اصلی جستجو است: دریافت بردار سوال، محاسبه شباهت، فیلتر، مرتب سازی، نمایش نتایج.
327
  async function searchMemoirs() {
328
  console.log("Search triggered."); // لاگ برای توسعه
329
  console.log(`Data loaded state (passages count): ${memoirsWithEmbeddings.length}`); // لاگ برای توسعه
 
367
  try {
368
  console.log("Requesting query embedding from Python server..."); // لاگ برای توسعه
369
 
370
+ // ارسال درخواست به سرور Backend برای دریافت بردار سوال
371
  const serverResponse = await fetch(EMBEDDING_SERVER_URL, {
372
  method: 'POST',
373
  headers: {
 
376
  body: JSON.stringify({ query: query })
377
  });
378
 
379
+ // بررسی موفقیت آمیز بودن پاسخ سرور (کد وضعیت 2xx)
380
  if (!serverResponse.ok) {
381
  const errorBody = await serverResponse.text(); // بخوان به صورت متن برای اطلاعات بیشتر
382
  console.error(`Server responded with status ${serverResponse.status}:`, errorBody); // لاگ خطا برای توسعه
 
400
 
401
  }
402
 
403
+ // دریافت داده های JSON از پاسخ سرور
404
  const serverData = await serverResponse.json();
405
  const queryEmbeddingArray = serverData.embedding;
406
 
407
+ // بررسی اینکه بردار embedding معتبر و غیر خالی است
408
  if (!queryEmbeddingArray || !Array.isArray(queryEmbeddingArray) || queryEmbeddingArray.length === 0) {
409
  console.error("Server returned an invalid or empty embedding:", serverData); // لاگ خطا برای توسعه
410
  updateStatus("جستجو با خطا مواجه شد.", true); // پیام اصلی برای کاربر
 
416
  console.log(`Query embedding dimensions: ${queryEmbeddingArray.length}`); // لاگ برای توسعه
417
  console.log("Calculating similarities in browser..."); // لاگ برای توسعه
418
 
419
+ // محاسبه شباهت بین بردار سوال و بردارهای تمام خاطرات بارگذاری شده
420
  const searchResults = [];
421
+ // memoirsWithEmbeddings حاوی خاطرات با بردارهای embedding و اطلاعات کتاب است.
422
+ for (const memoir of memoirsWithEmbeddings) {
423
  // اطمینان از وجود و صحت بردار embedding در آیتم خاطره
424
+ // و اینکه بردار خاطره همان ابعاد بردار سوال را دارد (خیلی مهم)
425
  if (memoir.embedding && Array.isArray(memoir.embedding) && memoir.embedding.length === queryEmbeddingArray.length) {
426
  const similarity = cosineSimilarity(queryEmbeddingArray, memoir.embedding);
427
+ // اضافه کردن تمام فیلدهای اصلی خاطره و امتیاز شباهت به نتیجه جستجو
428
+ searchResults.push({ ...memoir, similarity: similarity }); // استفاده از نام فیلد similarity برای امتیاز شباهت
 
429
  } else {
430
  // هشدار برای آیتم های بدون بردار یا با ابعاد نامعتبر (فقط در کنسول)
431
  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'}`);
 
434
  console.log(`Similarity calculation complete. Found ${searchResults.length} results with valid embeddings.`); // لاگ برای توسعه
435
 
436
 
437
+ // ****** خواندن آستانه شباهت از ورودی کاربر و فیلتر کردن نتایج ******
438
+ let currentSimilarityThreshold = DEFAULT_SIMILARITY_THRESHOLD; // مقدار پیش فرض به عنوان fallback
439
+ if (similarityThresholdInput) { // اگر المان ورودی آستانه شباهت پیدا شد
440
  const inputValue = parseFloat(similarityThresholdInput.value);
441
  // چک می کنیم عدد معتبر باشد و در محدوده [0, 1] باشد
442
  if (!isNaN(inputValue) && inputValue >= 0.0 && inputValue <= 1.0) {
443
  currentSimilarityThreshold = inputValue;
444
+ console.log("Using user-defined similarity threshold:", currentSimilarityThreshold); // لاگ
445
  } else {
446
+ console.warn("Invalid similarity threshold input value, using default:", DEFAULT_SIMILARITY_THRESHOLD); // لاگ
447
  updateSelectionError(`مقدار آستانه شباهت وارد شده (${similarityThresholdInput.value}) معتبر نیست. از مقدار پیش فرض ${DEFAULT_SIMILARITY_THRESHOLD.toFixed(2)} استفاده می‌شود.`); // پیام خطا برای کاربر
448
+ currentSimilarityThreshold = DEFAULT_SIMILARITY_THRESHOLD; // بازگشت به مقدار پیش فرض
 
 
449
  }
450
  } else {
451
+ console.warn("Similarity threshold input element not found, using default threshold:", DEFAULT_SIMILARITY_THRESHOLD); // لاگ
452
  }
453
 
454
+ // فیلتر کردن نتایج بر اساس آستانه شباهت
455
  const filteredResults = searchResults.filter(result => result.similarity >= currentSimilarityThreshold);
456
+ console.log(`Filtered results based on threshold ${currentSimilarityThreshold.toFixed(2)}: ${filteredResults.length} results remaining.`); // لاگ
457
  // *************************************************************
458
 
459
 
460
+ console.log("Sorting results by similarity..."); // لاگ
461
+ // مرتب سازی نتایج فیلتر شده بر اساس امتیاز شباهت به صورت نزولی (بیشترین شباهت اول)
462
  filteredResults.sort((a, b) => b.similarity - a.similarity);
463
+ console.log("Filtered results sorted."); // لاگ
464
+
465
+ // ****** انتخاب تعداد نتایج برتر بر اساس انتخاب کاربر ******
466
+ let finalResultsPerPage = 10; // مقدار پیش فرض
467
+ if (resultsPerPageSelect) { // اگر المان انتخاب تعداد نتایج پیدا شد
468
+ const selectedValue = parseInt(resultsPerPageSelect.value, 10); // خواندن مقدار انتخاب شده
469
+ // اطمینان از اینکه عدد معتبر و مثبت است
470
+ finalResultsPerPage = (!isNaN(selectedValue) && selectedValue > 0) ? selectedValue : 10; // مقدار پیش فرض 10
471
  } else {
472
+ console.warn("Results per page select element not found, using default:", finalResultsPerPage); // لاگ
473
  }
474
 
475
+ // برش لیست نتایج برای نمایش فقط تعداد مورد نظر
476
+ const topResults = filteredResults.slice(0, finalResultsPerPage);
477
  console.log(`Displaying top ${topResults.length} results from filtered list based on user selection (${finalResultsPerPage} per page setting).`);
478
  // ********************************************
479
 
480
 
481
+ // ****** منطق نمایش نتایج در HTML ******
482
+ if (searchResultsContainer) { // اگر کانتینر نمایش نتایج پیدا شد
483
  // نتایج قبلی در ابتدای تابع پاک شده اند.
484
 
485
  if (topResults.length === 0) { // اگر هیچ نتیجه ای یافت نشد
486
+ // پیام مناسب برای کاربر، شامل آستانه شباهت استفاده شده
487
  searchResultsContainer.innerHTML = `<p>نتیجه مرتبطی با آستانه شباهت مورد نظر (${currentSimilarityThreshold.toFixed(2)}) یافت نشد. سعی کنید عبارت دیگری را جستجو کنید یا آستانه را کاهش دهید.</p>`;
488
+ console.log("No relevant results found after filtering and slicing."); // لاگ
489
  } else { // اگر نتایجی یافت شد
490
  console.log("Results found, updating DOM.");
491
 
492
+ // ایجاد کانتینر برای لیست نتایج برای کنترل بهتر استایل دهی (اختیاری اما مفید)
493
  const resultsList = document.createElement('div');
494
+ resultsList.classList.add('results-list'); // می توانید استایل هایی برای این کلاس در CSS اضافه کنید.
495
 
496
 
497
  topResults.forEach(result => {
498
  // ایجاد المان برای هر آیتم نتیجه
499
  const resultItem = document.createElement('div');
500
+ resultItem.classList.add('result-item'); // کلاس اصلی هر نتیجه
501
+
502
+
503
+ // ایجاد و اضافه کردن تمام بخش های یک نتیجه (امتیاز، نام کتاب، مرجع، متن خاطره، دکمه کپی)
504
 
505
  // ****** نمایش امتیاز شباهت ******
506
+ // استفاده از p مطابق با ساختار HTML و کلاس CSS شما
507
+ const similarityElement = document.createElement('p');
508
  similarityElement.classList.add('result-similarity');
509
  // نمایش امتیاز با 4 رقم اعشار
510
+ similarityElement.textContent = `شباهت: ${result.similarity !== undefined ? result.similarity.toFixed(4) : 'N/A'}`;
511
+ resultItem.appendChild(similarityElement); // اضافه کردن به آیتم نتیجه
512
  // ******************************
513
 
514
+
515
  // نمایش نام کتاب
516
+ // استفاده از p مطابق با ساختار HTML و کلاس CSS شما
517
+ const bookTitleElement = document.createElement('p');
518
  bookTitleElement.classList.add('result-book-title');
519
  bookTitleElement.textContent = `از کتاب: ${result.book_title || 'نامشخص'}`;
520
+ resultItem.appendChild(bookTitleElement); // اضافه کردن به آیتم نتیجه
521
+
522
 
523
  // نمایش مرجع خاطره
524
+ // استفاده از p مطابق با ساختار HTML و کلاس CSS شما
525
+ const referenceElement = document.createElement('p');
526
  referenceElement.classList.add('result-reference');
527
  referenceElement.innerHTML = `<strong>مرجع:</strong> ${result.reference || 'نامشخص'}`; // استفاده از innerHTML برای bold کردن "مرجع:"
528
+ resultItem.appendChild(referenceElement); // اضافه کردن به آیتم نتیجه
529
 
530
 
531
  // نمایش متن خاطره (با حذف کلمات کلیدی)
532
+ // استفاده از p مطابق با ساختار HTML و کلاس CSS شما
533
+ const passageElement = document.createElement('p');
534
  passageElement.classList.add('result-passage');
535
  passageElement.textContent = cleanPassageTextForDisplay(result.passage || ''); // استفاده از متن تمیز شده
536
+ resultItem.appendChild(passageElement); // اضافه کردن به آیتم نتیجه
537
+
538
 
539
  // ****** اضافه کردن دکمه کپی ******
540
  const copyButton = document.createElement('button');
 
545
  // این استایل ها برای کمک به ظاهر شدن دکمه اضافه شده اند.
546
  // اگر با این استایل ها دکمه ظاهر شد، مشکل در فایل style.css یا اعمال آن است.
547
  // پس از حل مشکل نمایش در style.css، این خطوط را حذف کنید.
548
+ copyButton.style.display = 'block'; // نمایش به صورت بلوک جداگانه
549
+ copyButton.style.marginTop = '10px'; // فاصله از بالا
550
+ copyButton.style.marginLeft = 'auto'; // راست چین کردن در RTL با margin auto و margin-right 0
551
  copyButton.style.marginRight = '0';
552
  copyButton.style.backgroundColor = '#007bff'; // رنگ آبی
553
  copyButton.style.color = 'white';
554
+ copyButton.style.padding = '5px 5px'; // padding
555
  copyButton.style.border = 'none';
556
+ copyButton.style.borderRadius = '4px'; // گوشه گرد
557
+ copyButton.style.cursor = 'pointer'; // نشانگر موس
558
+ copyButton.style.fontSize = '0.8em'; // اندازه فونت
559
+ copyButton.style.fontFamily = "'Vazirmatn', sans-serif"; // اعمال فونت
560
  // *****************************************************************
561
 
562
 
563
+ // Event listener برای دکمه کپی
564
+ // این Listener در بلوک DOMContentLoaded به صورت Delegation اضافه می شود.
565
+ // کد مربوط به Listener در انتهای بلوک DOMContentLoaded آورده شده است.
566
+ // در اینجا فقط دکمه را به آیتم نتیجه اضافه می کنیم.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
567
  resultItem.appendChild(copyButton); // دکمه کپی در انتها
568
 
569
  // **************************************************************************
570
 
571
 
572
+ resultsList.appendChild(resultItem); // اضافه کردن آیتم نتیجه به لیست نتایج
573
  });
574
 
575
  searchResultsContainer.appendChild(resultsList); // اضافه کردن لیست نتایج به کانتینر اصلی
576
  console.log("DOM updated with results.");
577
 
578
+ // لاگ کردن نتایج برتر نمایش داده شده برای عیب یابی
579
  console.log(`Top ${topResults.length} results displayed (reference, book, similarity, passage start):`);
580
  topResults.forEach(result => {
581
+ console.log(` Book: ${result.book_title || 'Unknown'}, Ref: ${result.reference || 'N/A'}, Sim: ${result.similarity !== undefined ? result.similarity.toFixed(4) : 'N/A'}, Passage: "${result.passage ? result.passage.substring(0, Math.min(result.passage.length, 50)).replace(/\n/g, ' ') + '...' : 'N/A'}"`); // لاگ جزئیات بیشتر و جایگزینی خط جدید
582
  });
583
  }
584
 
 
591
 
592
 
593
  } else {
594
+ console.error("Could not find searchResultsContainer to display results."); // لاگ
595
  updateStatus("جستجو با خطا مواجه شد.", true); // پیام خطا (رنگ قرمز)
596
  updateSelectionError("المان نمایش نتایج پیدا نشد. لطفاً ساختار HTML را بررسی کنید."); // پیام خطا در بخش انتخاب
597
  }
 
599
 
600
  } catch (error) {
601
  // مدیریت خطا هنگام درخواست سرور یا پردازش پاسخ
602
+ console.error("Error during search:", error); // لاگ
603
  if (searchResultsContainer) { // استفاده از نام متغیر صحیح
604
  searchResultsContainer.innerHTML = `<p style="color: red;">هنگام جستجو خطایی رخ داد: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر موجود است.</p>`;
605
  }
606
  updateStatus("جستجو با خطا مواجه شد.", true); // پیام خطا (رنگ قرمز)
607
  updateSelectionError(`هنگام جستجو خطایی رخ داد: ${error.message || 'خطای نامشخص'}.`); // نمایش خطای اصلی در بخش خطای انتخاب
608
  } finally {
609
+ // در نهایت، چه جستجو موفقیت آمیز باشد چه با خطا مواجه شود، دکمه جستجو وضعیت خود را بررسی می کند.
610
+ checkAndEnableSearchButton(); // فعال کردن مجدد دکمه جستجو (اگر شرایط فراهم باشد)
 
 
611
  }
612
  }
613
  // پایان تعریف تابع async function searchMemoirs()
 
623
  userQuestionInput = document.getElementById('userQuestion');
624
  searchResultsContainer = document.getElementById('searchResults');
625
  loadingStatusElement = document.getElementById('loadingStatus');
626
+ selectionErrorElement = document.getElementById('selectionError'); // مطابق با ID در HTML شما
627
 
628
+ selectAllCheckbox = document.getElementById('select_all_books'); // مطابق با ID در HTML شما
629
  // getElementsByClassName برمی گرداند HTMLCollection زنده.
630
+ bookCheckboxes = document.getElementsByClassName('book-checkbox'); // مطابق با class در HTML شما
631
 
632
+ resultsPerPageSelect = document.getElementById('resultsPerPage'); // مطابق با ID در HTML شما
633
+ similarityThresholdInput = document.getElementById('similarityThresholdInput'); // مطابق با ID در HTML شما
634
  // **********************************************************************************
635
 
636
 
 
655
  // فقط اگ�� دکمه جستجو فعال است، کلیک را شبیه سازی کن
656
  if (!searchButton.disabled) {
657
  searchButton.click();
658
+ console.log("Enter key pressed in search input, simulating search button click."); // Log
659
+ }
660
+ }
661
+ });
662
+ console.log("Search input keypress listener added.");
663
+
664
+
665
+ // Listener برای تغییر مقدار ورودی آستانه شباهت: به‌روزرسانی وضعیت دکمه (فعال/غیرفعال شدن)
666
+ // مقدار آستانه شباهت مستقیماً در تابع searchMemoirs از این input خوانده می شود.
667
+ similarityThresholdInput.addEventListener('input', () => {
668
+ console.log("Similarity threshold input value changed.");
669
+ checkAndEnableSearchButton(); // بررسی وضعیت دکمه پس از تغییر مقدار
670
+ });
671
+ console.log("Similarity threshold input listener added.");
672
+
673
+ // Listener برای تغییر انتخاب در نتایج در صفحه (resultsPerPageSelect)
674
+ // مقدار نتایج در صفحه مستقیماً در تابع searchMemoirs از این select خوانده می شود.
675
+ resultsPerPageSelect.addEventListener('change', () => {
676
+ console.log("Results per page setting changed.");
677
+ // نیازی به checkAndEnableSearchButton نیست چون تغییر این مقدار به تنهایی دکمه را فعال/غیرفعال نمی کند.
678
+ });
679
+ console.log("Results per page select listener added.");
680
+
681
+
682
+ // Listener ها برای چک باکس 'انتخاب همه' و چک باکس های تکی کتاب ها
683
+ // تغییر در این چک باکس ها باید منجر به بارگذاری مجدد داده ها شود.
684
+
685
+ if (selectAllCheckbox) {
686
+ selectAllCheckbox.addEventListener('change', () => {
687
+ const isChecked = selectAllCheckbox.checked; // استفاده مستقیم از selectAllCheckbox
688
+ // getElementsByClassName برمی گرداند HTMLCollection زنده، باید آن را به آرایه تبدیل کرد
689
+ Array.from(bookCheckboxes).forEach(cb => { // استفاده از bookCheckboxes
690
+ cb.checked = isChecked;
691
+ });
692
+ console.log(`'Select All' checkbox changed to ${isChecked}. All book checkboxes updated.`); // لاگ
693
+ // پس از تغییر انتخاب کتاب، باید داده ها را بارگذاری مجدد کنیم.
694
+ updateSelectedBooksData(); // <--- فراخوانی بارگذاری مجدد داده ها
695
+ });
696
+ console.log("'Select All' checkbox change listener added.");
697
+ } else {
698
+ console.error("'Select All' checkbox element with ID 'select_all_books' not found.");
699
+ // اگر این المان پیدا نشد، قابلیت "انتخاب همه" کار نخواهد کرد.
700
+ }
701
+
702
+ if (bookCheckboxes && bookCheckboxes.length > 0) {
703
+ Array.from(bookCheckboxes).forEach(cb => { // استفاده از bookCheckboxes
704
+ cb.addEventListener('change', () => {
705
+ // بررسی کنید که آیا همه چک باکس های تکی انتخاب شده اند
706
+ const allOthersChecked = Array.from(bookCheckboxes).every(cb => cb.checked); // استفاده از bookCheckboxes
707
+ if (selectAllCheckbox) { // اگر چک باکس "انتخاب همه" وجود دارد، وضعیت آن را بر اساس وضعیت چک باکس های تکی به‌روزرسانی کن
708
+ selectAllCheckbox.checked = allOthersChecked;
709
+ }
710
+ console.log("Individual book checkbox changed. Checking 'Select All' status."); // لاگ
711
+ // پس از تغییر انتخاب کتاب، باید داده ها را بارگذاری مجدد کنیم.
712
+ updateSelectedBooksData(); // <--- فراخوانی بارگذاری مجدد داده ها
713
+ });
714
+ });
715
+ console.log("Individual book checkboxes change listeners added.");
716
+ } else {
717
+ console.warn("No book checkboxes found with class 'book-checkbox'. Book selection filtering will not work as expected.");
718
+ // اگر المان های چک باکس تکی پیدا نشدند، فیلترینگ بر اساس کتاب انتخاب شده کار نخواهد کرد.
719
+ // در این حالت، updateSelectedBooksData با لیست خالی selectedBookFiles اجرا خواهد شد.
720
+ }
721
+
722
+
723
+ // Listener برای دکمه های کپی (به صورت Delegation در searchResultsContainer)
724
+ // این Listener روی کانتینر نتایج گوش می دهد و برای دکمه های کپی که به صورت پویا اضافه می شوند، فعال می شود.
725
+ searchResultsContainer.addEventListener('click', (event) => {
726
+ // چک می کنیم که آیا کلیک روی المانی با کلاس 'copy-button' رخ داده است؟
727
+ if (event.target && event.target.classList && event.target.classList.contains('copy-button')) {
728
+ // جلوگیری از رفتار پیش فرض دکمه (مثلا ارسال فرم اگر داخل فرم باشد)
729
+ event.preventDefault(); // اضافه شده
730
+
731
+ // المان والد نتیجه را پیدا می کنیم (.result-item)
732
+ const resultItemElement = event.target.closest('.result-item');
733
+ if (resultItemElement) {
734
+ // متن کپی شامل متن خاطره، مرجع، نام کتاب و شباهت است.
735
+ // این اطلاعات از متن نمایش داده شده در المان های نتیجه استخراج می شود.
736
+ const passageText = resultItemElement.querySelector('.result-passage')?.textContent || '';
737
+ // استفاده از ? در selector برای جلوگیری از خطا اگر المان پیدا نشد.
738
+ // جایگزینی 'مرجع:' و trim برای تمیز کردن متن استخراج شده.
739
+ const referenceText = resultItemElement.querySelector('.result-reference')?.textContent.replace('مرجع:', '').trim() || 'نامشخص';
740
+ const bookTitleText = resultItemElement.querySelector('.result-book-title')?.textContent.replace('از کتاب:', '').trim() || 'نامشخص';
741
+ const similarityText = resultItemElement.querySelector('.result-similarity')?.textContent.replace('شباهت:', '').trim() || 'N/A';
742
+
743
+ // ساخت متنی که باید کپی شود با فرمت دلخواه
744
+ const textToCopy = `خاطره:\n${passageText}\n\nمرجع: ${referenceText}\nاز کتاب: ${bookTitleText}\nشباهت: ${similarityText}`;
745
+
746
+ // کپی کردن متن به کلیپ بورد
747
+ navigator.clipboard.writeText(textToCopy)
748
+ .then(() => {
749
+ // نمایش پیام موفقیت روی همان دکمه
750
+ event.target.textContent = 'کپی شد!';
751
+ setTimeout(() => {
752
+ event.target.textContent = 'کپی متن';
753
+ }, 2000); // بازگرداندن متن دکمه بعد از 2 ثانیه
754
+ console.log("Passage, reference, book title, and similarity copied to clipboard."); // لاگ موفقیت
755
+ })
756
+ .catch(err => {
757
+ console.error('Failed to copy text: ', err); // لاگ خطا در کنسول
758
+ event.target.textContent = 'خطا در کپی'; // پیام بازخورد خطا
759
+ setTimeout(() => {
760
+ event.target.textContent = 'کپی متن';
761
+ }, 2000);
762
+ });
763
+ } else {
764
+ console.warn("Result item parent not found for copy button."); // لاگ
765
+ }
766
+ }
767
+ });
768
+ console.log("Copy button delegation click listener added.");
769
+
770
+
771
+ // ****** راه اندازی اولیه: بارگذاری داده ها هنگام بارگذاری صفحه ******
772
+ // این تابع فرآیند بارگذاری داده از فایل های JSON بر اساس چک باکس های پیش فرض انتخاب شده در HTML را شروع می کند.
773
+ updateSelectedBooksData(); // این فراخوانی در نهایت checkAndEnableSearchButton را صدا می زند.
774
+ console.log("Initial data loading process started.");
775
+
776
+
777
+ } else {
778
+ // اگر تمام عناصر مورد نیاز پیدا نشدند، پیام خطا در کنسول و روی صفحه نمایش داده میشود
779
+ const errorMessage = "خطا در بارگذاری صفحه: برخی یا تمام عناصر لازم (HTML) پیدا نشدند. شناسه‌های HTML و نام کلاس‌ها را در index.html بررسی کنید و مطمئن شوید همه عناصر ضروری وجود دارند."; // پیام خطا برای کاربر
780
+ console.error(errorMessage, {
781
+ searchButton: !!searchButton, // از !! برای تبدیل به boolean استفاده می کنیم
782
+ userQuestionInput: !!userQuestionInput,
783
+ searchResultsContainer: !!searchResultsContainer,
784
+ loadingStatusElement: !!loadingStatusElement,
785
+ selectionErrorElement: !!selectionErrorElement,
786
+ selectAllCheckbox: !!selectAllCheckbox,
787
+ bookCheckboxesCount: bookCheckboxes ? bookCheckboxes.length : 0, // تعداد المان ها را نشان می دهد
788
+ resultsPerPageSelect: !!resultsPerPageSelect,
789
+ similarityThresholdInput: !!similarityThresholdInput
790
+ });
791
+ if (searchResultsContainer) { // اگر کانتینر نتایج پیدا شد، خطا را در آن نمایش بده
792
+ searchResultsContainer.innerHTML = `<p style=\"color: red;\">${errorMessage}</p>`;
793
+ }
794
+ // نمایش خطا در المان های وضعیت و خطای جداگانه
795
+ updateStatus("راه‌اندازی اولیه با خطا مواجه شد.", true);
796
+ updateSelectionError(errorMessage);
797
+
798
+ // غیرفعال نگه داشتن دکمه جستجو اگر پیدا شد
799
+ if (searchButton) {
800
+ setButtonEnabled(false);
801
+ }
802
+ // نیازی به return نیست چون کد بعد از این بلوک اجرا نمی شود (به دلیل عدم وجود المان های ضروری)
803
+ }
804
+ });