// این کد، فایل script.js به‌روزرسانی شده نهایی است که دکمه جستجو را زمانی فعال می‌کند که هم داده‌ها بارگذاری شده باشند و هم متنی در کادر جستجو وارد شده باشد. // ****** تعریف متغیرهای عناصر HTML در بالاترین اسکوپ ****** let searchButton; let questionInput; let resultsDiv; let loadingStatusParagraph; let selectionErrorParagraph; let selectAllCheckbox; let bookCheckboxes; // NodeList از تمام چک باکس های کتاب ها (به جز انتخاب همه) let resultsPerPageSelect; // المان select برای تعداد نتایج // ****** تعریف URL سرور پایتون برای دریافت Embedding سوال ****** const EMBEDDING_SERVER_URL = 'https://montaghem630-khatere-khan.hf.space/get_embedding'; // ****** متغیر برای نگهداری داده‌های ترکیب شده از کتاب‌های انتخاب شده ****** let memoirsWithEmbeddings = []; // ****** نگاشت نام فایل JSON به نام کامل کتاب (برای نمایش در نتایج) ****** // این لیست باید با مقادیر value چک باکس ها و نام های نمایشی در HTML مطابقت داشته باشد const bookInfo = { 'jabe_siah.json': 'جعبه سیاه (منتخب خاطرات اسدالله علم)', // اگر کتاب های دیگری دارید، اینجا اضافه کنید // 'ketab_dovom.json': 'نام کتاب دوم', // 'ketab_sevom.json': 'نام کتاب سوم', }; // ***************************************************************** // تابع کمکی برای نمایش پیام وضعیت بارگذاری/پردازش function updateStatus(message, isError = false) { if (loadingStatusParagraph) { loadingStatusParagraph.textContent = message; loadingStatusParagraph.style.color = isError ? 'red' : '#666'; } else { console.log("Status:", message); // لاگ برای توسعه } } // تابع کمکی برای نمایش خطای انتخاب کتاب function updateSelectionError(message) { if (selectionErrorParagraph) { selectionErrorParagraph.textContent = message; selectionErrorParagraph.style.color = 'red'; } else { console.error("Selection Error:", message); // لاگ برای توسعه } } // تابع کمکی برای فعال/غیرفعال کردن دکمه جستجو function setButtonEnabled(enabled) { if (searchButton) { searchButton.disabled = !enabled; } } // ****** تابع جدید برای بررسی وضعیت و فعال/غیرفعال کردن دکمه جستجو ****** // این تابع بررسی می کند که آیا داده ها بارگذاری شده اند و آیا کادر سوال خالی نیست function checkAndEnableSearchButton() { const isDataLoaded = memoirsWithEmbeddings.length > 0; const isQueryNotEmpty = questionInput && questionInput.value.trim() !== ''; // چک کردن وجود questionInput قبل از دسترسی به value // دکمه فقط زمانی فعال می شود که هم داده بارگذاری شده باشد و هم متن سوال خالی نباشد setButtonEnabled(isDataLoaded && isQueryNotEmpty); console.log(`Check Button State: Data Loaded = ${isDataLoaded}, Query Not Empty = ${isQueryNotEmpty}, Button Enabled = ${isDataLoaded && isQueryNotEmpty}`); // لاگ برای توسعه } // ****** تابع اصلی برای بارگذاری داده‌ها از فایل‌های JSON کتاب‌های انتخاب شده ****** // این تابع هر زمان که انتخاب کتاب ها تغییر می کند، داده ها را بارگذاری مجدد می کند async function updateSelectedBooksData() { console.log("Updating selected books data..."); // لاگ برای توسعه updateStatus("در حال بارگذاری داده‌ها..."); // پیام برای کاربر updateSelectionError(""); // پاک کردن پیام خطای قبلی // setButtonEnabled(false); // غیرفعال کردن دکمه جستجو حین بارگذاری (توسط checkAndEnableSearchButton انجام می شود) checkAndEnableSearchButton(); // در ابتدای بارگذاری، دکمه غیرفعال خواهد شد اگر هنوز متن نیست یا داده خالی می شود // پیدا کردن چک باکس های کتاب ها که انتخاب شده اند const selectedBookFiles = Array.from(bookCheckboxes) .filter(checkbox => checkbox.checked) .map(checkbox => checkbox.value); // value چک باکس ها نام فایل JSON است console.log("Selected book files:", selectedBookFiles); // لاگ برای توسعه // ****** چک کردن اینکه حداقل یک کتاب انتخاب شده باشد ****** if (selectedBookFiles.length === 0) { updateStatus(""); // پاک کردن پیام وضعیت updateSelectionError("لطفاً حداقل یک کتاب برای جستجو انتخاب کنید."); // پیام خطا برای کاربر console.warn("No books selected. Cannot load data."); // لاگ برای توسعه memoirsWithEmbeddings = []; // پاک کردن داده های قبلی resultsDiv.innerHTML = '

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

'; // بازگرداندن پیام اولیه checkAndEnableSearchButton(); // مطمئن می شویم دکمه غیرفعال شود return; // توقف فرآیند اگر هیچ کتابی انتخاب نشده است } // ******************************************************** memoirsWithEmbeddings = []; // پاک کردن داده‌های قبلی قبل از بارگذاری جدید try { // بارگذاری همزمان تمام فایل‌های JSON انتخاب شده const fetchPromises = selectedBookFiles.map(filename => { const filePath = `./${filename}`; console.log(`Attempting to fetch: ${filePath}`); // لاگ برای توسعه return fetch(filePath).then(response => { if (!response.ok) { throw new Error(`Error fetching file: ${filename} (Status: ${response.status})`); } return response.json(); }) .catch(error => { console.error(`Failed to fetch or parse file ${filename}:`, error); // لاگ خطا برای توسعه throw new Error(`Failed to load data for book file "${filename}".`); }); }); const booksData = await Promise.all(fetchPromises); // انتظار برای دانلود و تجزیه همه فایل ها // ترکیب داده‌ها از تمام فایل‌های JSON بارگذاری شده booksData.forEach(data => { if (Array.isArray(data)) { memoirsWithEmbeddings = memoirsWithEmbeddings.concat(data); } else { console.error("Fetched data is not an array:", data); // لاگ برای توسعه } }); const loadedBooksCount = selectedBookFiles.length; const totalPassagesLoaded = memoirsWithEmbeddings.length; console.log(`Successfully loaded data from ${loadedBooksCount} book(s). Total passages loaded: ${totalPassagesLoaded}`); // لاگ برای توسعه updateStatus(`داده‌ها از ${loadedBooksCount} کتاب با موفقیت بارگذاری شد. مجموع خاطرات: ${totalPassagesLoaded}. آماده جستجو هستید.`); // پیام برای کاربر // setButtonEnabled(true); // فعال کردن دکمه جستجو پس از بارگذاری موفقیت آمیز داده (توسط checkAndEnableSearchButton انجام می شود) checkAndEnableSearchButton(); // بررسی و فعال کردن دکمه پس از بارگذاری داده // نتایج قبلی را پاک نمی کنیم، فقط پیام اولیه را پاک میکنیم اگر هنوز نمایش داده می شود if (resultsDiv && resultsDiv.innerHTML === '

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

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

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

'; // بازگرداندن پیام اولیه } } // تابع کمکی برای محاسبه شباهت کسینوسی بین دو بردار (بدون تغییر) function cosineSimilarity(vecA, vecB) { if (!vecA || !vecB || vecA.length !== vecB.length || vecA.length === 0) { console.error("Cosine Similarity Error: Invalid vectors.", {vecA_length: vecA ? vecA.length : 'null', vecB_length: vecB ? vecB.length : 'null'}); return 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 (همانند قبل) function cleanPassageTextForDisplay(passage) { const startDelimiter = ' <کلیدواژه ها: '; const startIndex = passage.indexOf(startDelimiter); if (startIndex === -1) { return passage; } let cleanText = passage.substring(0, startIndex); return cleanText.trim(); } // ****** تابع اصلی جستجو که هنگام کلیک دکمه یا فشردن Enter اجرا میشود ****** async function searchMemoirs() { console.log("Search triggered - executing search"); // لاگ برای توسعه (مشخص نیست دکمه یا Enter) console.log(`Data loaded state (passages count): ${memoirsWithEmbeddings.length}`); // لاگ برای توسعه // ****** چک کردن اینکه داده ها (از کتاب های انتخاب شده) بارگذاری شده باشند ****** // این چک در checkAndEnableSearchButton هم هست، اما اینجا برای اطمینان بیشتر است if (memoirsWithEmbeddings.length === 0) { console.warn("No memoir data loaded. Cannot search."); // لاگ برای توسعه updateSelectionError("لطفاً ابتدا کتاب‌های مورد نظر برای جستجو را انتخاب کرده و منتظر بارگذاری داده‌ها بمانید."); // پیام خطا برای کاربر return; } // ************************************************************************** const query = questionInput.value.trim(); console.log(`Query text is: "${query}"`); // لاگ برای توسعه if (!query) { if (resultsDiv) { resultsDiv.innerHTML = `

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

`; // پیام برای کاربر } console.warn("Search query is empty."); // لاگ برای توسعه return; } updateStatus("در حال جستجو..."); // به‌روزرسانی پیام وضعیت به جستجو برای کاربر resultsDiv.innerHTML = ''; // پاک کردن نتایج قبلی یا پیام اولیه try { console.log("Requesting query embedding from Python server..."); // لاگ برای توسعه 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(`Server responded with status ${serverResponse.status}: ${errorBody}`); // لاگ خطا برای توسعه throw new Error(`خطا از سرور (${serverResponse.status}). جزئیات بیشتر در کنسول مرورگر.`); // پیام خطا برای کاربر } const serverData = await serverResponse.json(); const queryEmbeddingArray = serverData.embedding; if (!queryEmbeddingArray || !Array.isArray(queryEmbeddingArray) || queryEmbeddingArray.length === 0) { console.error("Server returned an invalid or empty embedding:", serverData); // لاگ خطا برای توسعه throw new Error("سرور بردار جستجو را به درستی برنگرداند. جزئیات در کنسول مرورگر."); // پیام خطا برای کاربر } console.log("Query embedding received from server successfully."); // لاگ برای توسعه 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: ${memoir.book_title || 'Unknown Book'} - ${memoir.reference || 'Unknown Reference'}`); } } console.log(`Similarity calculation complete. Found ${searchResults.length} results with valid embeddings.`); // لاگ برای توسعه console.log("Sorting results by similarity..."); // لاگ برای توسعه searchResults.sort((a, b) => b.similarity - a.similarity); console.log("Results sorted."); // لاگ برای توسعه // ****** انتخاب تعداد نتایج برتر بر اساس انتخاب کاربر ****** const resultsPerPage = parseInt(resultsPerPageSelect.value, 10); // خواندن مقدار انتخاب شده و تبدیل به عدد صحیح const topResults = searchResults.slice(0, resultsPerPage); // انتخاب فقط N نتیجه برتر console.log(`Displaying top ${topResults.length} results based on user selection.`); // لاگ برای توسعه // *********************************************************** // ****** منطق نمایش نتایج ****** if (resultsDiv) { // نتایج قبلی را پاک کرده ایم if (topResults.length === 0) { // اگر هیچ نتیجه ای یافت نشد resultsDiv.innerHTML = `

نتیجه مرتبطی یافت نشد.

`; // پیام برای کاربر 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'); // حذف نمایش امتیاز شباهت // const similarityElement = document.createElement('p'); // similarityElement.classList.add('result-similarity'); // similarityElement.textContent = `شباهت: ${result.similarity.toFixed(4)}`; // نمایش نام کتاب const bookTitleElement = document.createElement('p'); bookTitleElement.classList.add('result-book-title'); bookTitleElement.textContent = `از کتاب: ${result.book_title || 'نامشخص'}`; // نمایش مرجع خاطره const referenceElement = document.createElement('p'); referenceElement.classList.add('result-reference'); referenceElement.innerHTML = `مرجع: ${result.reference || 'نامشخص'}`; // نمایش متن خاطره (با حذف کلمات کلیدی) const passageElement = document.createElement('p'); passageElement.classList.add('result-passage'); passageElement.textContent = cleanPassageTextForDisplay(result.passage || ''); // اضافه کردن عناصر به آیتم نتیجه با ترتیب جدید (متن -> مرجع -> کتاب) resultItem.appendChild(passageElement); resultItem.appendChild(referenceElement); resultItem.appendChild(bookTitleElement); resultsList.appendChild(resultItem); }); resultsDiv.appendChild(resultsList); console.log("DOM updated with results."); // لاگ برای توسعه // لاگ کردن نتایج برای توسعه console.log(`Top ${topResults.length} results displayed (reference, book, and similarity):`); topResults.forEach(result => { console.log(` Book: ${result.book_title || 'Unknown'}, Ref: ${result.reference || 'N/A'}, Sim: ${result.similarity.toFixed(4)}`); // امتیاز را در لاگ نگه می داریم }); } updateStatus(`جستجو به پایان رسید. ${topResults.length} نتیجه برتر نمایش داده شد.`); // به‌روزرسانی پیام وضعیت پس از جستجو } else { console.error("Could not find resultsDiv to display results."); // لاگ برای توسعه updateStatus("جستجو با خطا مواجه شد.", true); // پیام برای کاربر } } catch (error) { console.error("Error during search:", error); // لاگ برای توسعه if (resultsDiv) { // نمایش پیام خطای عمومی به کاربر resultsDiv.innerHTML = `

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

`; } updateStatus("جستجو با خطا مواجه شد.", true); // پیام برای کاربر } finally { // در نهایت (چه موفقیت آمیز چه با خطا)، دکمه را دوباره بررسی و تنظیم وضعیت می کنیم checkAndEnableSearchButton(); } } // ****** Event Listeners و مقداردهی اولیه در زمان بارگذاری صفحه ****** document.addEventListener('DOMContentLoaded', () => { console.log("DOMContentLoaded fired. Attaching event listeners and initializing."); // لاگ برای توسعه // پیدا کردن عناصر HTML با ID یا Class searchButton = document.getElementById('searchButton'); questionInput = document.getElementById('userQuestion'); resultsDiv = document.getElementById('searchResults'); loadingStatusParagraph = document.getElementById('loadingStatus'); selectionErrorParagraph = document.getElementById('selectionError'); selectAllCheckbox = document.getElementById('select_all_books'); bookCheckboxes = document.querySelectorAll('.book-checkbox'); // انتخاب تمام چک باکس های با کلاس .book-checkbox resultsPerPageSelect = document.getElementById('resultsPerPage'); // پیدا کردن المان select // لاگ برای تأیید پیدا شدن عناصر HTML (برای توسعه) console.log("Search Button found:", !!searchButton); console.log("Question Input found:", !!questionInput); console.log("Results Div found:", !!resultsDiv); console.log("Loading Status found:", !!loadingStatusParagraph); console.log("Selection Error found:", !!selectionErrorParagraph); console.log("Select All Checkbox found:", !!selectAllCheckbox); console.log(`Book Checkboxes found: ${bookCheckboxes.length}`); console.log("Results Per Page Select found:", !!resultsPerPageSelect); // اگر تمام عناصر مورد نیاز پیدا شدند، Event Listeners را اضافه کرده و مقداردهی اولیه را انجام دهید if (searchButton && questionInput && resultsDiv && loadingStatusParagraph && selectionErrorParagraph && selectAllCheckbox && bookCheckboxes.length > 0 && resultsPerPageSelect) { // ****** اضافه کردن Event Listener به دکمه جستجو ****** searchButton.addEventListener('click', searchMemoirs); console.log("Search button event listener attached."); // لاگ برای توسعه // ****** اضافه کردن Event Listener برای تغییر متن در کادر سوال ****** // از event 'input' استفاده می کنیم که با هر تغییری (تایپ، پیست و...) اجرا می شود questionInput.addEventListener('input', () => { checkAndEnableSearchButton(); // هر بار که متن تغییر کرد، وضعیت دکمه را بررسی کن }); // Event listener برای keypress (Enter) برای اجرای جستجو باقی می ماند questionInput.addEventListener('keypress', (event) => { if (event.key === 'Enter' || event.keyCode === 13) { event.preventDefault(); // فقط در صورتی جستجو را اجرا کن که دکمه فعال است (یعنی داده بارگذاری شده و متن خالی نیست) if (!searchButton.disabled) { searchMemoirs(); } else { console.warn("Attempted to search with Enter, but button is disabled (data not loaded or query empty)."); // لاگ برای توسعه } } }); console.log("Input change and keypress event listeners attached to question input."); // لاگ برای توسعه // ****** منطق چک باکس 'انتخاب همه' ****** selectAllCheckbox.addEventListener('change', () => { const isChecked = selectAllCheckbox.checked; bookCheckboxes.forEach(checkbox => { checkbox.checked = isChecked; }); // بارگذاری مجدد داده ها پس از تغییر انتخاب همه (با تاخیر کم برای جلوگیری از فشردگی) setTimeout(updateSelectedBooksData, 50); // تاخیر 50 میلی ثانیه }); // ****** اضافه کردن Event Listeners به چک باکس های کتاب ها ****** bookCheckboxes.forEach(checkbox => { checkbox.addEventListener('change', () => { // اگر یکی از چک باکس های کتاب از حالت انتخاب خارج شد، 'انتخاب همه' را هم از حالت انتخاب خارج کن if (!checkbox.checked) { selectAllCheckbox.checked = false; } // اگر تمام چک باکس های کتاب انتخاب شدند، 'انتخاب همه' را هم انتخاب کن else { const allBooksSelected = Array.from(bookCheckboxes).every(cb => cb.checked); if (allBooksSelected) { selectAllCheckbox.checked = true; } } // بارگذاری مجدد داده ها پس از تغییر انتخاب کتاب (با تاخیر کم) setTimeout(updateSelectedBooksData, 50); // تاخیر 50 میلی ثانیه }); }); // ****** اضافه کردن Event Listener به انتخابگر تعداد نتایج ****** resultsPerPageSelect.addEventListener('change', () => { console.log("Results per page changed to:", resultsPerPageSelect.value); // لاگ برای توسعه }); // ****** بارگذاری اولیه داده ها بر اساس انتخاب های پیش فرض هنگام بارگذاری صفحه ****** // این تابع بر اساس چک باکس های پیش فرض (که در HTML تیک خورده اند) داده ها را بارگذاری می کند updateSelectedBooksData(); // این فراخوانی در نهایت checkAndEnableSearchButton را صدا می زند } else { // اگر عناصر مورد نیاز پیدا نشدند، پیام خطا در کنسول و روی صفحه نمایش داده میشود const errorMessage = "خطا: عناصر لازم صفحه پیدا نشدند. شناسه‌های HTML و نام کلاس‌ها را در index.html بررسی کنید."; // پیام خطا برای کاربر console.error(errorMessage); // لاگ برای توسعه if (resultsDiv) { resultsDiv.innerHTML = `

${errorMessage}

`; } updateStatus("راه‌اندازی اولیه با خطا مواجه شد.", true); // پیام برای کاربر } });