Spaces:
Runtime error
Runtime error
| // این فایل 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 = '<p>در حال بارگذاری اطلاعات کتابها. لطفاً صبر کنید تا دکمه جستجو فعال شود...</p>'; | |
| } | |
| // پیدا کردن چک باکس های کتاب ها که انتخاب شده اند | |
| // از 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 = '<p>پس از انتخاب کتابها و وارد کردن سوال، نتایج اینجا نمایش داده میشوند.</p>'; // بازگرداندن به پیام پیش فرض بخش نتایج | |
| } | |
| } | |
| // ************************************************************************ | |
| } | |
| checkAndEnableSearchButton(); // بررسی و فعال کردن دکمه | |
| } catch (error) { | |
| console.error("Error loading selected books data:", error); // لاگ خطا | |
| // تنظیم وضعیت به "خطا در بارگذاری" | |
| updateStatus("خطا در بارگذاری دادهها.", true, 'error'); | |
| // پیام خطای برای کاربر شامل جزئیات بیشتر | |
| updateSelectionError(`خطا در بارگذاری دادهها: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر.`); | |
| memoirsWithEmbeddings = []; // اطمینان از خالی بودن داده در صورت خطا | |
| checkAndEnableSearchButton(); // مطمئن می شویم دکمه غیرفعال بماند | |
| if (searchResultsContainer) { // بازگرداندن پیام اولیه | |
| searchResultsContainer.innerHTML = '<p>پس از انتخاب کتابها و وارد کردن سوال، نتایج اینجا نمایش داده میشوند.</p>'; | |
| } | |
| } 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 = `<p>لطفاً عبارت مورد نظر برای جستجو را وارد کنید.</p>`; // پیام | |
| } | |
| 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 = `<p>نتیجه مرتبطی با آستانه شباهت مورد نظر (${currentSimilarityThreshold.toFixed(2)}) یافت نشد. سعی کنید عبارت دیگری را جستجو کنید یا آستانه را کاهش دهید.</p>`; | |
| 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 = `<strong>مرجع:</strong> ${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 = `<p style=\"color: red;\">هنگام جستجو خطایی رخ داد: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر موجود است.</p>`; | |
| } | |
| // تنظیم وضعیت به "خطا" | |
| 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 = `<p style=\"color: red;\">${errorMessage}</p>`; | |
| } | |
| // نمایش خطا در المان های وضعیت و خطای جداگانه | |
| updateStatus("راهاندازی اولیه با خطا مواجه شد.", true, 'error'); | |
| updateSelectionError(errorMessage); | |
| // غیرفعال نگه داشتن دکمه جستجو | |
| if (searchButton) { | |
| setButtonEnabled(false); | |
| } | |
| // نیازی به return نیست. | |
| } | |
| }); | |
| // پایان بلوک DOMContentLoaded و پایان کامل فایل script.js |