Spaces:
Runtime error
Runtime error
File size: 51,566 Bytes
f862c6e f6b32fe 0f89022 f862c6e f6b32fe f862c6e 0f89022 f862c6e f6b32fe f862c6e 0f89022 f862c6e 0f89022 437867a 0f89022 f862c6e 0f89022 f862c6e 0f89022 f862c6e 0f89022 f862c6e f627f5b f862c6e 0f89022 437867a 0f89022 f862c6e 0f89022 f862c6e 0f89022 437867a f862c6e f627f5b f862c6e f627f5b f862c6e f6b32fe f862c6e f627f5b f862c6e 0f89022 437867a 0f89022 f627f5b f862c6e 0f89022 f862c6e 0f89022 f862c6e 0f89022 f862c6e f627f5b f862c6e 0f89022 f862c6e 437867a f862c6e 437867a f862c6e 0f89022 f862c6e 0f89022 f862c6e 0f89022 f862c6e f627f5b f862c6e f627f5b f862c6e 0f89022 f862c6e f627f5b f862c6e f6b32fe f862c6e f6b32fe 0f89022 f862c6e f627f5b f862c6e f627f5b 0f89022 f862c6e 437867a f862c6e f627f5b f862c6e 0f89022 f627f5b f862c6e 0f89022 f862c6e 0f89022 84da4b7 f862c6e 0f89022 f627f5b 0f89022 f862c6e 437867a f862c6e 0f89022 f627f5b f862c6e 0f89022 f862c6e 0f89022 f862c6e 2a02c89 f862c6e f627f5b 0f89022 f862c6e f627f5b 0f89022 f862c6e f627f5b 0f89022 f862c6e f627f5b 0f89022 f862c6e 0f89022 f862c6e f627f5b 437867a 0f89022 f862c6e 0f89022 f862c6e f627f5b f862c6e 0f89022 f627f5b f862c6e 0f89022 f862c6e f627f5b f862c6e 0f89022 f627f5b 0f89022 f862c6e 0f89022 f862c6e 2a02c89 0f89022 f862c6e 0f89022 f862c6e 0f89022 f862c6e 0f89022 437867a f862c6e 0f89022 f862c6e f627f5b 0f89022 437867a f862c6e 0f89022 f627f5b f862c6e 0f89022 f627f5b 0f89022 f862c6e 0f89022 f862c6e 0f89022 f862c6e f6b32fe 0f89022 f862c6e 0f89022 2a02c89 f627f5b 0f89022 f862c6e 2a02c89 0f89022 f862c6e 0f89022 f862c6e f627f5b 0f89022 f862c6e 0f89022 f862c6e 3fcfdc6 0f89022 f862c6e 0f89022 f862c6e 0f89022 2a02c89 f862c6e 0f89022 f862c6e 0f89022 f862c6e 3fcfdc6 f862c6e 0f89022 2a02c89 f862c6e 0f89022 f862c6e f627f5b f862c6e 3fcfdc6 f862c6e f627f5b 3fcfdc6 437867a 8a4e963 437867a 8a4e963 437867a 8a4e963 437867a 8a4e963 437867a 8a4e963 437867a 8a4e963 437867a 8a4e963 437867a 8a4e963 437867a 8a4e963 437867a 8a4e963 437867a 8a4e963 437867a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 |
// این فایل 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 |