// این فایل script.js نسخه نهایی شده بر اساس کد اصلی شماست. // شامل تمام اصلاحات لازم برای کار در Hugging Face Space جدید با مدل heydariAI/persian-embeddings. // تمام قابلیت های اضافه شده و رفع اشکالات شناسایی شده در فرآیند عیب یابی در آن لحاظ شده است. // این نسخه شامل مدیریت بصری وضعیت (لودینگ، آماده) با رنگ و اسپینر است. // ****** تعریف متغیرهای عناصر HTML در بالاترین اسکوپ ****** // این متغیرها در بلوک DOMContentLoaded پس از بارگذاری صفحه مقداردهی اولیه می شوند. let searchButton; let userQuestionInput; // <--- مطابق با ID در index.html شما let searchResultsContainer; // <--- مطابق با ID در index.html شما let loadingStatusElement; // <--- مطابق با ID در index.html شما let selectionErrorElement; // <--- مطابق با ID در index.html شما let selectAllCheckbox; // <--- مطابق با ID در index.html شما let bookCheckboxes; // <--- مطابق با class در index.html شما (HTMLCollection از المان ها) let resultsPerPageSelect; // <--- مطابق با ID در index.html شما let similarityThresholdInput; // <--- مطابق با ID در index.html شما let loadingSpinnerElement; // <--- متغیر برای المان لودینگ اسپینر // ************************************************* // ****** تعریف URL سرور پایتون برای دریافت Embedding سوال ****** // آدرس نسبی برای ارتباط با Backend در همان سرور Flask که Frontend را سرویس می دهد. const EMBEDDING_SERVER_URL = '/get_embedding'; // <--- آدرس نسبی برای Space مشترک // ****** نگاشت نام فایل JSON به نام کامل کتاب (برای نمایش در نتایج و فیلتر) ****** // این map اطلاعات نمایشی کتاب را بر اساس value چک باکس (نام فایل JSON) فراهم می کند. // مطمئن شوید که value چک باکس ها در index.html شما با کلیدهای این map مطابقت دارد. const bookInfo = { // 'نام_فایل_json_در_value_چک_باکس': 'نام کامل نمایشی کتاب', 'jabe_siah.json': 'جعبه سیاه (منتخب خاطرات اسدالله علم)', // مثال: value چک باکس : نام نمایشی کتاب // اگر چک باکس های بیشتری در HTML دارید و فایل های JSON متناظر دارید، ورودی های متناظر را اینجا اضافه کنید: // 'ketab_dovom.json': 'نام کتاب دوم', // 'ketab_sevom.json': 'نام کتاب سوم', // مطمئن شوید که فایل های JSON متناظر (مثلا ketab_dovom.json) در Space آپلود شده اند. }; // ****** آستانه شباهت پیش فرض ****** // اگر عنصر ورودی آستانه پیدا نشد یا مقدار آن نامعتبر بود، از این مقدار استفاده می شود. const DEFAULT_SIMILARITY_THRESHOLD = 0.15; // ****** متغیر سراسری برای نگهداری داده های بارگذاری شده از کتاب های انتخاب شده ****** let memoirsWithEmbeddings = []; // این آرایه حاوی خاطرات با بردارهای embedding از کتاب های انتخاب شده است. // ****** توابع کمکی برای مدیریت پیام ها و وضعیت UI ****** // تابع کمکی برای نمایش پیام وضعیت بارگذاری/پردازش در المان loadingStatusElement // Optional state parameter: 'loading', 'ready', 'error', '' (یا هر پیام دیگر که وضعیت خاصی را نشان ندهد) function updateStatus(message, isError = false, state = '') { if (loadingStatusElement) { loadingStatusElement.textContent = message; // Set text first // پاک کردن کلاس های وضعیت قبلی loadingStatusElement.classList.remove('loading', 'ready', 'error'); // اضافه کردن کلاس وضعیت مناسب بر اساس isError یا پارامتر state if (isError) { loadingStatusElement.classList.add('error'); // رنگ و فونت ضخیم توسط کلاس .error-message یا .status-message.error در CSS مدیریت می شود loadingStatusElement.style.color = ''; // ریست کردن استایل های اینلاین احتمالی loadingStatusElement.style.fontWeight = ''; // ریست کردن استایل های اینلاین احتمالی } else if (state === 'loading') { loadingStatusElement.classList.add('loading'); loadingStatusElement.style.color = ''; loadingStatusElement.style.fontWeight = ''; } else if (state === 'ready') { loadingStatusElement.classList.add('ready'); loadingStatusElement.style.color = ''; loadingStatusElement.style.fontWeight = ''; } else { // وضعیت پیش فرض یا پیام های عادی بدون وضعیت خاص loadingStatusElement.style.color = '#666'; // رنگ پیش فرض loadingStatusElement.style.fontWeight = 'normal'; // فونت نرمال پیش فرض } // مدیریت نمایش اسپینر (اسپینر با CSS و کلاس .status-message.loading کنترل می شود) // نیازی به نمایش/پنهان کردن مستقیم اسپینر اینجا نیست اگر CSS به درستی تنظیم شده باشد. loadingStatusElement.style.display = message ? 'flex' : 'none'; // استفاده از flex برای نمایش و مرکز کردن محتوا (متن و اسپینر) // اگر پیام خالی باشد، المان مخفی می شود. } else { console.log("Status:", message, "isError:", isError, "state:", state); // لاگ برای توسعه } // پاک کردن پیام خطای انتخاب کتاب اگر یک پیام وضعیت نمایش داده می شود if (message && selectionErrorElement) { selectionErrorElement.textContent = ''; selectionErrorElement.style.display = 'none'; selectionErrorElement.classList.remove('error'); // پاک کردن کلاس خطا } } // تابع کمکی برای نمایش خطای انتخاب کتاب یا بارگذاری داده در المان selectionErrorElement function updateSelectionError(message) { if (selectionErrorElement) { selectionErrorElement.textContent = message; selectionErrorElement.style.color = 'red'; // رنگ قرمز selectionErrorElement.style.fontWeight = 'bold'; // فونت ضخیم selectionErrorElement.style.display = message ? 'flex' : 'none'; // نمایش با فلکس selectionErrorElement.classList.add('error'); // اضافه کردن کلاس خطا } else { console.error("Selection Error Element not found."); // لاگ برای توسعه } // پاک کردن پیام وضعیت اگر یک پیام خطای انتخاب نمایش داده می شود if (message && loadingStatusElement) { loadingStatusElement.textContent = ''; loadingStatusElement.style.display = 'none'; loadingStatusElement.classList.remove('loading', 'ready'); // پاک کردن کلاس های وضعیت // اسپینر نیز توسط CSS و کلاس مدیریت می شود. } } // تابع کمکی برای فعال/غیرفعال کردن دکمه جستجو function setButtonEnabled(enabled) { if (searchButton) { searchButton.disabled = !enabled; // console.log("Search button enabled state:", enabled); // لاگ برای توسعه } else { console.error("Search button element not found."); } } // ****** تابع برای بررسی وضعیت و فعال/غیرفعال کردن دکمه جستجو ****** // دکمه فقط زمانی فعال می شود که داده بارگذاری شده، کادر سوال خالی نیست و آستانه معتبر است. function checkAndEnableSearchButton() { const isDataLoaded = memoirsWithEmbeddings.length > 0; // اطمینان از وجود userQuestionInput قبل از دسترسی به value const isQueryNotEmpty = userQuestionInput && userQuestionInput.value.trim() !== ''; // همچنین بررسی می کنیم که مقدار آستانه شباهت معتبر باشد اگر عنصر ورودی وجود دارد let isThresholdInputValid = true; if (similarityThresholdInput) { const inputValue = parseFloat(similarityThresholdInput.value); // چک می کنیم عدد معتبر باشد و در محدوده [0, 1] باشد if (isNaN(inputValue) || inputValue < 0.0 || inputValue > 1.0) { isThresholdInputValid = false; // مقدار نامعتبر است } } else { // اگر عنصر ورودی پیدا نشد، از آستانه پیش فرض استفاده می کنیم و فرض می کنیم معتبر است console.warn("Similarity threshold input element not found. Assuming default threshold is valid."); } // دکمه فقط زمانی فعال می شود که تمام شرایط لازم برقرار باشد setButtonEnabled(isDataLoaded && isQueryNotEmpty && isThresholdInputValid); } // ****** تابع اصلی برای بارگذاری داده‌ها از فایل‌های JSON کتاب‌های انتخاب شده ****** // این تابع هر زمان که انتخاب کتاب ها تغییر می کند، داده ها را بارگذاری مجدد می کند. async function updateSelectedBooksData() { console.log("Updating selected books data..."); // لاگ برای توسعه // تنظیم وضعیت به "در حال بارگذاری" با رنگ و اسپینر updateStatus("در حال بارگذاری داده‌ها...", false, 'loading'); updateSelectionError(""); // پاک کردن پیام خطاهای قبلی setButtonEnabled(false); // غیرفعال کردن دکمه جستجو if (searchResultsContainer) { // پاک کردن نتایج قبلی // نمایش پیام اولیه واضح در بخش نتایج searchResultsContainer.innerHTML = '

در حال بارگذاری اطلاعات کتاب‌ها. لطفاً صبر کنید تا دکمه جستجو فعال شود...

'; } // پیدا کردن چک باکس های کتاب ها که انتخاب شده اند // از getElementsByClassName استفاده می کنیم و آن را به آرایه تبدیل می کنیم if (!bookCheckboxes || bookCheckboxes.length === 0) { console.error("No book checkboxes found with class 'book-checkbox'. Cannot load data."); updateStatus(""); // پاک کردن پیام وضعیت updateSelectionError("المان‌های انتخاب کتاب یافت نشد. لطفاً ساختار HTML را بررسی کنید."); // پیام خطا memoirsWithEmbeddings = []; checkAndEnableSearchButton(); return; } // value چک باکس ها همان نام فایل JSON است که باید بارگذاری شود. const selectedBookFiles = Array.from(bookCheckboxes) .filter(checkbox => checkbox.checked) .map(checkbox => checkbox.value); console.log("Selected book files (from checkbox values):", selectedBookFiles); // لاگ برای توسعه // ****** چک کردن اینکه حداقل یک کتاب انتخاب شده باشد ****** if (selectedBookFiles.length === 0) { updateStatus(""); // پاک کردن پیام وضعیت updateSelectionError("لطفاً حداقل یک کتاب برای جستجو انتخاب کنید."); // پیام خطا console.warn("No books selected. Cannot load data."); // لاگ memoirsWithEmbeddings = []; // پاک کردن داده های قبلی checkAndEnableSearchButton(); // مطمئن می شویم دکمه غیرفعال بماند return; // توقف فرآیند } // ******************************************************** memoirsWithEmbeddings = []; // پاک کردن داده‌های قبلی قبل از بارگذاری جدید try { // بارگذاری همزمان تمام فایل‌های JSON انتخاب شده const fetchPromises = selectedBookFiles.map(filename => { // آدرس فایل JSON نسبت به محل قرارگیری index.html // فرض بر این است که فایل های JSON در کنار index.html در همان پوشه قرار دارند const filePath = `./${filename}`; console.log(`Attempting to fetch: ${filePath}`); // لاگ برای توسعه return fetch(filePath).then(response => { if (!response.ok) { // اگر فایل پیدا نشد یا خطای دیگری رخ داد (مثلاً 404) console.error(`Workspace error for ${filePath}: Status ${response.status}`); // لاگ خطا // تلاش برای خواندن متن خطا از پاسخ return response.text().then(text => { console.error(`Response body for ${filePath}: ${text}`); throw new Error(`Error fetching file: "${filename}" (Status: ${response.status}). Check if the file exists at "${filePath}".`); }).catch(() => { throw new Error(`Error fetching file: "${filename}" (Status: ${response.status}). Check if the file exists at "${filePath}".`); }); } // بررسی Content-Type پاسخ const contentType = response.headers.get("content-type"); if (!contentType || !contentType.includes("application/json")) { console.error(`Workspace error for ${filePath}: Expected application/json, but received ${contentType}`); // لاگ خطا throw new Error(`Error fetching file: "${filename}". Expected JSON, but received unexpected content type.`); } return response.json(); }) .catch(error => { // گرفتن خطاهای شبکه یا تجزیه JSON console.error(`Failed to fetch or parse file "${filename}":`, error); // لاگ خطا throw new Error(`Failed to load data for book file "${filename}". ${error.message || 'Unknown error'}. Check file name and location.`); }); }); const booksData = await Promise.all(fetchPromises); // انتظار برای دانلود و تجزیه همه فایل ها // ترکیب داده‌ها از تمام فایل‌های JSON بارگذاری شده و افزودن اطلاعات کتاب booksData.forEach((data, index) => { // اضافه کردن index const filename = selectedBookFiles[index]; // نام فایل JSON فعلی // پیدا کردن نام نمایشی کتاب از map bookInfo const bookDisplayName = bookInfo[filename] || filename; if (Array.isArray(data)) { // فیلتر کردن آیتم هایی که بردار embedding معتبر دارند و افزودن اطلاعات کتاب به آن ها const memoirsWithBookInfo = data.filter(item => item && typeof item === 'object' && item.embedding && Array.isArray(item.embedding) && item.embedding.length > 0) .map(item => ({ ...item, // کپی کردن تمام پراپرتی های موجود (passage_original, passage_combined, reference, embedding, etc.) book_title: bookDisplayName, // افزودن نام نمایشی book_file: filename // افزودن نام فایل })); memoirsWithEmbeddings = memoirsWithEmbeddings.concat(memoirsWithBookInfo); // هشدار برای آیتم های نامعتبر const invalidMemoirsCount = data.length - memoirsWithBookInfo.length; if(invalidMemoirsCount > 0){ console.warn(`Skipped ${invalidMemoirsCount} items from file "${filename}" due to missing or invalid embedding or format.`); // لاگ هشدار } } else { console.error(`Loaded data from "${filename}" is not an array:`, data); // لاگ خطا updateSelectionError(`خطا در فرمت داده از فایل "${filename}". انتظار آرایه داشتیم.`); // پیام خطا } }); const loadedBooksCount = selectedBookFiles.length; const totalPassagesLoaded = memoirsWithEmbeddings.length; if (totalPassagesLoaded === 0) { console.warn("No valid passages with embeddings were loaded..."); // لاگ هشدار // تنظیم وضعیت به "خطا در بارگذاری" updateStatus(`داده‌ها بارگذاری شد، اما هیچ خاطره‌ای با بردار معتبر از کتاب‌های انتخاب شده (${loadedBooksCount} کتاب) یافت نشد.`, true, 'error'); updateSelectionError("هیچ خاطره‌ای با بردار معتبر از کتاب‌های انتخاب شده یافت نشد. فرمت فایل‌های JSON و وجود فیلد embedding را بررسی کنید."); // پیام خطا } else { console.log(`Successfully loaded data from ${loadedBooksCount} book(s)...`); // لاگ // تنظیم وضعیت به "آماده" updateStatus(`داده‌ها از ${loadedBooksCount} کتاب با موفقیت بارگذاری شد. مجموع خاطرات قابل جستجو: ${totalPassagesLoaded}. آماده جستجو هستید.`, false, 'ready'); // ****** پاک کردن پیام اولیه در بخش نتایج پس از بارگذاری موفق داده ****** // این خط باعث می شود که پس از بارگذاری موفقیت آمیز داده ها، پیام "در حال بارگذاری..." از بخش نتایج پاک شود. if (searchResultsContainer) { // چک کردن وجود المان نتایج // بررسی اینکه آیا پیام فعلی در بخش نتایج همان پیام لودینگ اولیه است یا خیر // این کار از پاک شدن نتایج جستجو در صورت بارگذاری مجدد پس از جستجو جلوگیری می کند if (searchResultsContainer.innerHTML.includes('در حال بارگذاری اطلاعات کتاب‌ها. لطفاً صبر کنید')) { searchResultsContainer.innerHTML = '

پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.

'; // بازگرداندن به پیام پیش فرض بخش نتایج } } // ************************************************************************ } checkAndEnableSearchButton(); // بررسی و فعال کردن دکمه } catch (error) { console.error("Error loading selected books data:", error); // لاگ خطا // تنظیم وضعیت به "خطا در بارگذاری" updateStatus("خطا در بارگذاری داده‌ها.", true, 'error'); // پیام خطای برای کاربر شامل جزئیات بیشتر updateSelectionError(`خطا در بارگذاری داده‌ها: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر.`); memoirsWithEmbeddings = []; // اطمینان از خالی بودن داده در صورت خطا checkAndEnableSearchButton(); // مطمئن می شویم دکمه غیرفعال بماند if (searchResultsContainer) { // بازگرداندن پیام اولیه searchResultsContainer.innerHTML = '

پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.

'; } } finally { // checkAndEnableSearchButton در هر دو شاخه try/catch صدا زده می شود. } } // ****** تابع کمکی برای محاسبه شباهت کسینوسی بین دو بردار ****** function cosineSimilarity(vecA, vecB) { // بررسی وجود و صحت بردارها if (!vecA || !vecB || vecA.length !== vecB.length || vecA.length === 0) { console.error("Cosine Similarity Error: Invalid or empty vectors provided.", {vecA_length: vecA ? vecA.length : 'null', vecB_length: vecB ? vecB.length : 'null'}); // لاگ خطا return 0; // برگرداندن 0 } let dotProduct = 0; let magnitudeA = 0; let magnitudeB = 0; // محاسبه نقطه ضرب و مربع اندازه for (let i = 0; i < vecA.length; i++) { dotProduct += vecA[i] * vecB[i]; magnitudeA += vecA[i] * vecA[i]; magnitudeB += vecB[i] * vecB[i]; } // گرفتن ریشه دوم magnitudeA = Math.sqrt(magnitudeA); magnitudeB = Math.sqrt(magnitudeB); // جلوگیری از تقسیم بر صفر if (magnitudeA === 0 || magnitudeB === 0) { return 0; } // محاسبه شباهت const similarity = dotProduct / (magnitudeA * magnitudeB); return similarity; // برگرداندن امتیاز } // تابع کمکی برای حذف بخش کلمات کلیدی از متن Passage_combined // این تابع فعلاً برای نمایش passage_original مستقیماً استفاده نمی شود، اما نگه داشته شده است. function cleanPassageTextForDisplay(passage) { if (!passage || typeof passage !== 'string') { console.warn("cleanPassageTextForDisplay received invalid input:", passage); // لاگ هشدار return ''; // برگرداندن رشته خالی } const startDelimiter = ' <کلیدواژه ها: '; const startIndex = passage.indexOf(startDelimiter); if (startIndex === -1) { return passage.trim(); // اگر جداکننده پیدا نشد } let cleanText = passage.substring(0, startIndex); return cleanText.trim(); // حذف فاصله های اضافی } // ****** تعریف کامل تابع async function searchMemoirs() { ... } ****** async function searchMemoirs() { console.log("Search triggered."); // لاگ console.log(`Data loaded state (passages count): ${memoirsWithEmbeddings.length}`); // لاگ // ****** چک کردن اینکه داده ها بارگذاری شده باشند ****** if (memoirsWithEmbeddings.length === 0) { console.warn("No memoir data loaded. Cannot search."); // لاگ هشدار updateSelectionError("لطفاً ابتدا کتاب‌های مورد نظر برای جستجو را انتخاب کرده و منتظر بارگذاری داده‌ها بمانید."); // پیام خطا return; } // ************************************************************************** // اطمینان از وجود userQuestionInput if (!userQuestionInput) { console.error("Search input element not found."); // لاگ خطا updateSelectionError("المان ورودی جستجو پیدا نشد. امکان جستجو وجود ندارد."); // پیام خطا return; } const query = userQuestionInput.value.trim(); console.log(`Query text is: "${query}"`); // لاگ if (!query) { if (searchResultsContainer) { searchResultsContainer.innerHTML = `

لطفاً عبارت مورد نظر برای جستجو را وارد کنید.

`; // پیام } console.warn("Search query is empty."); // لاگ هشدار updateSelectionError("لطفاً عبارت مورد نظر برای جستجو را وارد کنید."); // پیام خطا return; } // نمایش پیام "در حال جستجو" با وضعیت لودینگ updateStatus("در حال جستجو...", false, 'loading'); updateSelectionError(""); // پاک کردن خطاهای قبلی if (searchResultsContainer) { // پاک کردن نتایج قبلی searchResultsContainer.innerHTML = ''; } setButtonEnabled(false); // غیرفعال کردن دکمه try { console.log("Requesting query embedding from Backend..."); // لاگ // ارسال درخواست به Backend const serverResponse = await fetch(EMBEDDING_SERVER_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: query }) }); // بررسی موفقیت آمیز بودن پاسخ if (!serverResponse.ok) { const errorBody = await serverResponse.text(); // خواندن متن خطا console.error(`Backend responded with status ${serverResponse.status}:`, errorBody); // لاگ خطا let serverErrorMessage = `خطا از Backend (${serverResponse.status}).`; try { // تلاش برای خواندن JSON خطا const errorJson = JSON.parse(errorBody); if (errorJson.error) { serverErrorMessage += ` پیام: ${errorJson.error}`; } } catch (e) { // اگر JSON نبود serverErrorMessage += ` پاسخ خام: ${errorBody.substring(0, Math.min(errorBody.length, 100))}...`; } // تنظیم وضعیت به "خطا" updateStatus("جستجو با خطا مواجه شد.", true, 'error'); updateSelectionError(serverErrorMessage + " جزئیات بیشتر در کنسول مرورگر."); // نمایش خطا return; // خروج } // دریافت و بررسی پاسخ JSON const serverData = await serverResponse.json(); const queryEmbeddingArray = serverData.embedding; if (!queryEmbeddingArray || !Array.isArray(queryEmbeddingArray) || queryEmbeddingArray.length === 0) { console.error("Backend returned an invalid or empty embedding:", serverData); // لاگ خطا // تنظیم وضعیت به "خطا" updateStatus("جستجو با خطا مواجه شد.", true, 'error'); updateSelectionError("Backend بردار جستجو را به درستی برنگرداند. جزئیات در کنسول مرورگر."); // نمایش خطا return; // خروج } console.log("Query embedding received from Backend successfully."); // لاگ console.log(`Query embedding dimensions: ${queryEmbeddingArray.length}`); // لاگ console.log("Calculating similarities in browser..."); // لاگ // محاسبه شباهت با تمام خاطرات const searchResults = []; for (const memoir of memoirsWithEmbeddings) { // اطمینان از وجود و صحت بردار embedding و تطابق ابعاد if (memoir.embedding && Array.isArray(memoir.embedding) && memoir.embedding.length === queryEmbeddingArray.length) { const similarity = cosineSimilarity(queryEmbeddingArray, memoir.embedding); searchResults.push({ ...memoir, similarity: similarity }); } else { 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'}`); // لاگ هشدار } } console.log(`Similarity calculation complete. Found ${searchResults.length} results...`); // لاگ // ****** فیلتر کردن نتایج بر اساس آستانه شباهت ****** let currentSimilarityThreshold = DEFAULT_SIMILARITY_THRESHOLD; if (similarityThresholdInput) { const inputValue = parseFloat(similarityThresholdInput.value); if (!isNaN(inputValue) && inputValue >= 0.0 && inputValue <= 1.0) { currentSimilarityThreshold = inputValue; console.log("Using user-defined similarity threshold:", currentSimilarityThreshold); // لاگ } else { console.warn("Invalid similarity threshold input value, using default:", DEFAULT_SIMILARITY_THRESHOLD); // لاگ هشدار updateSelectionError(`مقدار آستانه شباهت وارد شده (${similarityThresholdInput.value}) معتبر نیست. از مقدار پیش فرض ${DEFAULT_SIMILARITY_THRESHOLD.toFixed(2)} استفاده می‌شود.`); // پیام خطا currentSimilarityThreshold = DEFAULT_SIMILARITY_THRESHOLD; } } else { console.warn("Similarity threshold input element not found...", DEFAULT_SIMILARITY_THRESHOLD); // لاگ هشدار } const filteredResults = searchResults.filter(result => result.similarity >= currentSimilarityThreshold); console.log(`Filtered results based on threshold ${currentSimilarityThreshold.toFixed(2)}: ${filteredResults.length} results remaining.`); // لاگ // ************************************************************* console.log("Sorting results by similarity..."); // لاگ filteredResults.sort((a, b) => b.similarity - a.similarity); console.log("Filtered results sorted."); // لاگ // ****** انتخاب تعداد نتایج برتر ****** let finalResultsPerPage = 10; if (resultsPerPageSelect) { const selectedValue = parseInt(resultsPerPageSelect.value, 10); finalResultsPerPage = (!isNaN(selectedValue) && selectedValue > 0) ? selectedValue : 10; } else { console.warn("Results per page select element not found...", finalResultsPerPage); // لاگ هشدار } const topResults = filteredResults.slice(0, finalResultsPerPage); console.log(`Displaying top ${topResults.length} results...`); // لاگ // لاگ کردن نتایج برتر برای عیب یابی (اختیاری) // console.log("Top results data before display:", topResults); // ****** منطق نمایش نتایج در HTML ****** if (searchResultsContainer) { if (topResults.length === 0) { searchResultsContainer.innerHTML = `

نتیجه مرتبطی با آستانه شباهت مورد نظر (${currentSimilarityThreshold.toFixed(2)}) یافت نشد. سعی کنید عبارت دیگری را جستجو کنید یا آستانه را کاهش دهید.

`; console.log("No relevant results found..."); // لاگ } else { console.log("Results found, updating DOM."); const resultsList = document.createElement('div'); resultsList.classList.add('results-list'); topResults.forEach(result => { const resultItem = document.createElement('div'); resultItem.classList.add('result-item'); // نمایش امتیاز شباهت (float right دارد، ترتیب آن در اینجا تأثیر کمتری بر ترتیب بلوک اصلی دارد) const similarityElement = document.createElement('p'); similarityElement.classList.add('result-similarity'); similarityElement.textContent = `شباهت: ${result.similarity !== undefined ? result.similarity.toFixed(4) : 'N/A'}`; resultItem.appendChild(similarityElement); // اضافه کردن امتیاز شباهت (معمولاً اول یا زودتر) // ****** نمایش المان‌ها به ترتیب مورد نظر: خاطره، مرجع، نام کتاب ****** // ۱. نمایش متن خاطره const passageElement = document.createElement('p'); passageElement.classList.add('result-passage'); passageElement.textContent = result.passage_original || 'متن خاطره موجود نیست.'; // استفاده از passage_original resultItem.appendChild(passageElement); // اضافه کردن خاطره (اولین) // ۲. نمایش مرجع خاطره const referenceElement = document.createElement('p'); referenceElement.classList.add('result-reference'); referenceElement.innerHTML = `مرجع: ${result.reference || 'نامشخص'}`; resultItem.appendChild(referenceElement); // اضافه کردن مرجع (دومین) // ۳. نمایش نام کتاب const bookTitleElement = document.createElement('p'); bookTitleElement.classList.add('result-book-title'); bookTitleElement.textContent = `از کتاب: ${result.book_title || 'نامشخص'}`; resultItem.appendChild(bookTitleElement); // اضافه کردن نام کتاب (سومین) // ******************************************************************************** // ****** اضافه کردن دکمه کپی (بعد از محتوای اصلی) ****** const copyButton = document.createElement('button'); copyButton.classList.add('copy-button'); copyButton.textContent = 'کپی متن'; // استایل های موقت Inline برای تست - بهتر است در style.css تعریف شوند و کلاس copy-button به آنها لینک شود // در style.css ارائه شده قبلی این استایل ها موجود هستند و نیازی به اینجا نیست // copyButton.style.display = 'block'; // copyButton.style.marginTop = '15px'; // copyButton.style.marginLeft = 'auto'; // راست چین کردن در RTL // copyButton.style.marginRight = '0'; // copyButton.style.backgroundColor = '#007bff'; // copyButton.style.color = 'white'; // copyButton.style.padding = '5px 10px'; // copyButton.style.border = 'none'; // copyButton.style.borderRadius = '4px'; // copyButton.style.cursor = 'pointer'; // copyButton.style.fontSize = '0.85em'; // copyButton.style.fontFamily = "'Vazirmatn', sans-serif"; // ********************************** // Listener برای دکمه کپی به صورت Delegation در DOMContentLoaded اضافه شده است و نیازی به تعریف مجدد در اینجا نیست. resultItem.appendChild(copyButton); // اضافه کردن دکمه کپی (آخرین) // ************************************************************************** resultsList.appendChild(resultItem); // اضافه کردن آیتم نتیجه کامل شده به لیست نتایج }); // پایان حلقه forEach searchResultsContainer.appendChild(resultsList); console.log("DOM updated with results."); // لاگ کردن نتایج برتر نمایش داده شده (اختیاری) console.log(`Top ${topResults.length} results displayed (reference, book, similarity, passage start):`); topResults.forEach(result => { 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_original ? result.passage_original.substring(0, Math.min(result.passage_original.length, 50)).replace(/\n/g, ' ') + '...' : 'N/A'}"`); }); } // به‌روزرسانی پیام وضعیت پس از جستجو if (topResults.length > 0) { updateStatus(`جستجو به پایان رسید. ${topResults.length} نتیجه برتر (پس از فیلتر با آستانه ${currentSimilarityThreshold.toFixed(2)}) نمایش داده شد.`, false, 'ready'); // وضعیت "آماده" } else { updateStatus(`جستجو به پایان رسید. هیچ نتیجه مرتبطی با آستانه شباهت مورد نظر (${currentSimilarityThreshold.toFixed(2)}) یافت نشد. سعی کنید عبارت دیگری را جستجو کنید یا آستانه را کاهش دهید.`, false, 'ready'); // وضعیت "آماده" } } else { console.error("Could not find searchResultsContainer to display results."); // لاگ خطا updateStatus("جستجو با خطا مواجه شد.", true, 'error'); // وضعیت "خطا" updateSelectionError("المان نمایش نتایج پیدا نشد. لطفاً ساختار HTML را بررسی کنید."); // پیام خطا } } catch (error) { // مدیریت خطا هنگام درخواست به Backend یا پردازش پاسخ console.error("Error during search:", error); // لاگ خطا if (searchResultsContainer) { searchResultsContainer.innerHTML = `

هنگام جستجو خطایی رخ داد: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر موجود است.

`; } // تنظیم وضعیت به "خطا" updateStatus("جستجو با خطا مواجه شد.", true, 'error'); updateSelectionError(`هنگام جستجو خطایی رخ داد: ${error.message || 'خطای نامشخص'}.`); // نمایش خطا } finally { // در نهایت دکمه جستجو وضعیت خود را بررسی می کند. checkAndEnableSearchButton(); // فعال کردن مجدد دکمه جستجو (اگر شرایط فراهم باشد) } } // ****** بلوک DOMContentLoaded: این کد پس از بارگذاری کامل ساختار صفحه اجرا می شود ****** document.addEventListener('DOMContentLoaded', () => { console.log("DOM fully loaded and parsed. Initializing script."); // لاگ // ****** دریافت رفرنس المان های HTML (مطابق با ID ها و کلاس ها در index.html شما) ****** // این رفرنس ها متغیرهای سراسری تعریف شده در بالای فایل هستند. searchButton = document.getElementById('searchButton'); userQuestionInput = document.getElementById('userQuestion'); // <--- مطابق با ID searchResultsContainer = document.getElementById('searchResults'); // <--- مطابق با ID loadingStatusElement = document.getElementById('loadingStatus'); // <--- مطابق با ID selectionErrorElement = document.getElementById('selectionError'); // <--- مطابق با ID selectAllCheckbox = document.getElementById('select_all_books'); // <--- مطابق با ID // getElementsByClassName برمی گرداند HTMLCollection زنده. bookCheckboxes = document.getElementsByClassName('book-checkbox'); // <--- مطابق با class resultsPerPageSelect = document.getElementById('resultsPerPage'); // <--- مطابق با ID similarityThresholdInput = document.getElementById('similarityThresholdInput'); // <--- مطابق با ID loadingSpinnerElement = document.querySelector('#loadingStatus .loading-spinner'); // <--- دریافت رفرنس اسپینر // ********************************************************************************** // ****** چک کردن وجود المان های ضروری برای جلوگیری از خطا ****** // چک می کنیم که تمام المان های مورد نیاز برای اجرای اسکریپت پیدا شده باشند. // bookCheckboxes باید وجود داشته باشد و حداقل یک المان (چک باکس) داشته باشد. const requiredElementsFound = searchButton && userQuestionInput && searchResultsContainer && loadingStatusElement && selectionErrorElement && selectAllCheckbox && bookCheckboxes && bookCheckboxes.length > 0 && resultsPerPageSelect && similarityThresholdInput && loadingSpinnerElement; if (requiredElementsFound) { console.log("All critical DOM elements found. Proceeding with initialization."); // لاگ // ****** تنظیم Event Listeners ****** // Listener برای دکمه جستجو: فراخوانی تابع searchMemoirs هنگام کلیک searchButton.addEventListener('click', searchMemoirs); console.log("Search button click listener added."); // لاگ // Listener برای کلید Enter در کادر ورودی سوال: شبیه سازی کلیک روی دکمه جستجو userQuestionInput.addEventListener('keypress', (event) => { if (event.key === 'Enter') { event.preventDefault(); // جلوگیری از ارسال فرم if (!searchButton.disabled) { // فقط اگر دکمه فعال است searchButton.click(); console.log("Enter key pressed in search input, simulating search button click."); // لاگ } } }); console.log("Search input keypress listener added."); // لاگ // ****** Listener برای تغییر محتوای کادر سوال: بررسی وضعیت دکمه جستجو ****** // این Listener باعث می شود دکمه جستجو زمانی که متن وارد می شود فعال شود. userQuestionInput.addEventListener('input', () => { console.log("Search input value changed, checking button state."); // لاگ checkAndEnableSearchButton(); // <--- فراخوانی تابع بررسی وضعیت دکمه }); console.log("Search input 'input' listener added."); // ********************************************************************* // Listener برای تغییر مقدار ورودی آستانه شباهت: به‌روزرسانی وضعیت دکمه similarityThresholdInput.addEventListener('input', () => { console.log("Similarity threshold input value changed."); // لاگ checkAndEnableSearchButton(); // بررسی وضعیت دکمه (اگر مقدار نامعتبر شود غیرفعال می شود) }); console.log("Similarity threshold input listener added."); // لاگ // Listener برای تغییر انتخاب تعداد نتایج در صفحه resultsPerPageSelect.addEventListener('change', () => { console.log("Results per page setting changed."); // لاگ // نیازی به checkAndEnableSearchButton نیست. }); console.log("Results per page select listener added."); // لاگ // Listener ها برای چک باکس 'انتخاب همه' و چک باکس های تکی کتاب ها // تغییر در این چک باکس ها باید منجر به بارگذاری مجدد داده ها شود. if (selectAllCheckbox) { selectAllCheckbox.addEventListener('change', () => { const isChecked = selectAllCheckbox.checked; Array.from(bookCheckboxes).forEach(cb => { cb.checked = isChecked; }); console.log(`'Select All' checkbox changed to ${isChecked}. All book checkboxes updated.`); // لاگ updateSelectedBooksData(); // <--- فراخوانی بارگذاری مجدد داده ها }); console.log("'Select All' checkbox change listener added."); // لاگ } else { console.error("'Select All' checkbox element with ID 'select_all_books' not found."); // لاگ خطا } if (bookCheckboxes && bookCheckboxes.length > 0) { Array.from(bookCheckboxes).forEach(cb => { cb.addEventListener('change', () => { const allOthersChecked = Array.from(bookCheckboxes).every(cb => cb.checked); if (selectAllCheckbox) { selectAllCheckbox.checked = allOthersChecked; } console.log("Individual book checkbox changed. Checking 'Select All' status."); // لاگ updateSelectedBooksData(); // <--- فراخوانی بارگذاری مجدد داده ها }); }); console.log("Individual book checkboxes change listeners added."); // لاگ } else { console.warn("No book checkboxes found with class 'book-checkbox'..."); // لاگ هشدار } // Listener برای دکمه های کپی (به صورت Delegation) searchResultsContainer.addEventListener('click', (event) => { if (event.target && event.target.classList && event.target.classList.contains('copy-button')) { event.preventDefault(); // جلوگیری از رفتار پیش فرض const resultItemElement = event.target.closest('.result-item'); if (resultItemElement) { // استخراج متن از المان های نمایش داده شده const passageText = resultItemElement.querySelector('.result-passage')?.textContent || ''; const referenceText = resultItemElement.querySelector('.result-reference')?.textContent.replace('مرجع:', '').trim() || 'نامشخص'; const bookTitleText = resultItemElement.querySelector('.result-book-title')?.textContent.replace('از کتاب:', '').trim() || 'نامشخص'; // گرفتن متن کامل عنصر شباهت (مثلا "شباهت: 0.7500") const similarityElementText = resultItemElement.querySelector('.result-similarity')?.textContent || ''; // ساخت متنی که باید کپی شود const textToCopy = `خاطره:\n${passageText}\n\nمرجع: ${referenceText}\nاز کتاب: ${bookTitleText}\n${similarityElementText}`; // کپی کردن متن به کلیپ بورد navigator.clipboard.writeText(textToCopy) .then(() => { event.target.textContent = 'کپی شد!'; setTimeout(() => { event.target.textContent = 'کپی متن'; }, 2000); console.log("Passage, reference, book title, and similarity copied."); // لاگ }) .catch(err => { console.error('Failed to copy text: ', err); // لاگ خطا event.target.textContent = 'خطا در کپی'; setTimeout(() => { event.target.textContent = 'کپی متن'; }, 2000); }); } else { console.warn("Result item parent not found for copy button click."); // لاگ هشدار } } }); console.log("Copy button delegation click listener added."); // لاگ // ****** راه اندازی اولیه: بارگذاری داده ها هنگام بارگذاری صفحه ****** // این تابع فرآیند بارگذاری داده از فایل های JSON بر اساس چک باکس های پیش فرض انتخاب شده در HTML را شروع می کند. updateSelectedBooksData(); // این فراخوانی در نهایت checkAndEnableSearchButton را صدا می زند. console.log("Initial data loading process started."); // لاگ } else { // اگر تمام عناصر مورد نیاز پیدا نشدند const errorMessage = "خطا در بارگذاری صفحه: برخی یا تمام عناصر لازم (HTML) پیدا نشدند. شناسه‌های HTML و نام کلاس‌ها را در index.html بررسی کنید و مطمئن شوید همه عناصر ضروری وجود دارند."; // پیام خطا برای کاربر console.error(errorMessage, { // لاگ جزئیات خطا searchButton: !!searchButton, userQuestionInput: !!userQuestionInput, searchResultsContainer: !!searchResultsContainer, loadingStatusElement: !!loadingStatusElement, selectionErrorElement: !!selectionErrorElement, selectAllCheckbox: !!selectAllCheckbox, bookCheckboxesCount: bookCheckboxes ? bookCheckboxes.length : 0, resultsPerPageSelect: !!resultsPerPageSelect, similarityThresholdInput: !!similarityThresholdInput, loadingSpinnerElement: !!loadingSpinnerElement // بررسی وجود اسپینر }); if (searchResultsContainer) { // نمایش خطا روی صفحه searchResultsContainer.innerHTML = `

${errorMessage}

`; } // نمایش خطا در المان های وضعیت و خطای جداگانه updateStatus("راه‌اندازی اولیه با خطا مواجه شد.", true, 'error'); updateSelectionError(errorMessage); // غیرفعال نگه داشتن دکمه جستجو if (searchButton) { setButtonEnabled(false); } // نیازی به return نیست. } }); // پایان بلوک DOMContentLoaded و پایان کامل فایل script.js