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