khatere_khan / script.js
montaghem630's picture
Upload 10 files
e2ce00f verified
// این کد، فایل 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 = '<p>پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.</p>'; // بازگرداندن پیام اولیه
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 === '<p>پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.</p>') {
resultsDiv.innerHTML = '';
}
} catch (error) {
console.error("Error loading selected books data:", error); // لاگ برای توسعه
updateStatus("خطا در بارگذاری داده‌ها.", true); // پیام برای کاربر
updateSelectionError(`خطا در بارگذاری داده از کتاب‌های انتخاب شده: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر.`); // پیام خطا برای کاربر
memoirsWithEmbeddings = []; // اطمینان از خالی بودن داده در صورت خطا
// setButtonEnabled(false); // غیرفعال نگه داشتن دکمه جستجو (توسط checkAndEnableSearchButton انجام می شود)
checkAndEnableSearchButton(); // مطمئن می شویم دکمه غیرفعال بماند
resultsDiv.innerHTML = '<p>پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.</p>'; // بازگرداندن پیام اولیه
}
}
// تابع کمکی برای محاسبه شباهت کسینوسی بین دو بردار (بدون تغییر)
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 = `<p>لطفاً عبارت مورد نظر برای جستجو را وارد کنید.</p>`; // پیام برای کاربر
}
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 = `<p>نتیجه مرتبطی یافت نشد.</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');
// حذف نمایش امتیاز شباهت
// 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 = `<strong>مرجع:</strong> ${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 = `<p style="color: red;">هنگام جستجو خطایی رخ داد: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر موجود است.</p>`;
}
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 = `<p style="color: red;">${errorMessage}</p>`;
}
updateStatus("راه‌اندازی اولیه با خطا مواجه شد.", true); // پیام برای کاربر
}
});