Jahadona commited on
Commit
0f89022
·
verified ·
1 Parent(s): 666afe8

Update script.js

Browse files
Files changed (1) hide show
  1. script.js +266 -283
script.js CHANGED
@@ -1,6 +1,8 @@
1
  // این فایل script.js نسخه نهایی شده بر اساس کد اصلی شماست.
2
  // شامل تمام اصلاحات لازم برای کار در Hugging Face Space جدید با مدل heydariAI/persian-embeddings.
3
  // تمام قابلیت های اضافه شده و رفع اشکالات شناسایی شده در فرآیند عیب یابی در آن لحاظ شده است.
 
 
4
 
5
  // ****** تعریف متغیرهای عناصر HTML در بالاترین اسکوپ ******
6
  // این متغیرها در بلوک DOMContentLoaded پس از بارگذاری صفحه مقداردهی اولیه می شوند.
@@ -15,7 +17,8 @@ let bookCheckboxes; // <--- مطابق با class در index.html شما (HTMLCo
15
 
16
  let resultsPerPageSelect; // <--- مطابق با ID در index.html شما
17
  let similarityThresholdInput; // <--- مطابق با ID در index.html شما
18
- // نکته: در index.html شما المانی برای نمایش عددی آستانه شباهت در کنار input type="number" وجود ندارد، پس نیازی به متغیر برای آن نیست.
 
19
 
20
  // *************************************************
21
 
@@ -50,19 +53,50 @@ let memoirsWithEmbeddings = []; // این آرایه حاوی خاطرات با
50
  // ****** توابع کمکی برای مدیریت پیام ها و وضعیت UI ******
51
 
52
  // تابع کمکی برای نمایش پیام وضعیت بارگذاری/پردازش در المان loadingStatusElement
53
- function updateStatus(message, isError = false) {
 
54
  if (loadingStatusElement) {
55
- loadingStatusElement.textContent = message;
56
- loadingStatusElement.style.color = isError ? 'red' : '#666'; // رنگ قرمز برای خطا، خاکستری برای وضعیت
57
- loadingStatusElement.style.fontWeight = isError ? 'bold' : 'normal'; // خطاها با فونت ضخیم تر
58
- loadingStatusElement.style.display = message ? 'block' : 'none'; // نمایش المان فقط وقتی پیام دارد
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  } else {
60
- console.log("Status:", message); // لاگ برای توسعه
61
  }
62
- // پاک کردن پیام خطای انتخاب کتاب اگر پیامی در status نمایش داده می شود
 
63
  if (message && selectionErrorElement) {
64
  selectionErrorElement.textContent = '';
65
  selectionErrorElement.style.display = 'none';
 
66
  }
67
  }
68
 
@@ -70,16 +104,20 @@ function updateStatus(message, isError = false) {
70
  function updateSelectionError(message) {
71
  if (selectionErrorElement) {
72
  selectionErrorElement.textContent = message;
73
- selectionErrorElement.style.color = 'red';
74
- selectionErrorElement.style.fontWeight = 'bold'; // خطاها با فونت ضخیم تر
75
- selectionErrorElement.style.display = message ? 'block' : 'none'; // نمایش المان فقط وقتی پیام دارد
 
 
76
  } else {
77
  console.error("Selection Error Element not found."); // لاگ برای توسعه
78
  }
79
- // پاک کردن پیام وضعیت اگر پیامی در selectionError نمایش داده می شود
80
  if (message && loadingStatusElement) {
81
  loadingStatusElement.textContent = '';
82
  loadingStatusElement.style.display = 'none';
 
 
83
  }
84
  }
85
 
@@ -122,11 +160,15 @@ function checkAndEnableSearchButton() {
122
  // این تابع هر زمان که انتخاب کتاب ها تغییر می کند، داده ها را بارگذاری مجدد می کند.
123
  async function updateSelectedBooksData() {
124
  console.log("Updating selected books data..."); // لاگ برای توسعه
125
- updateStatus("در حال بارگذاری داده‌ها..."); // پیام برای کاربر
126
- updateSelectionError(""); // پاک کردن پیام خطای قبلی
127
- setButtonEnabled(false); // غیرفعال کردن دکمه در طول بارگذاری داده ها
 
 
 
128
  if (searchResultsContainer) { // پاک کردن نتایج قبلی
129
- searchResultsContainer.innerHTML = '<p>پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.</p>'; // پیام اولیه
 
130
  }
131
 
132
 
@@ -135,7 +177,7 @@ async function updateSelectedBooksData() {
135
  if (!bookCheckboxes || bookCheckboxes.length === 0) {
136
  console.error("No book checkboxes found with class 'book-checkbox'. Cannot load data.");
137
  updateStatus(""); // پاک کردن پیام وضعیت
138
- updateSelectionError("المان‌های انتخاب کتاب یافت نشد. لطفاً ساختار HTML را بررسی کنید."); // پیام خطا برای کاربر
139
  memoirsWithEmbeddings = [];
140
  checkAndEnableSearchButton();
141
  return;
@@ -151,11 +193,11 @@ async function updateSelectedBooksData() {
151
  // ****** چک کردن اینکه حداقل یک کتاب انتخاب شده باشد ******
152
  if (selectedBookFiles.length === 0) {
153
  updateStatus(""); // پاک کردن پیام وضعیت
154
- updateSelectionError("لطفاً حداقل یک کتاب برای جستجو انتخاب کنید."); // پیام خطا برای کاربر
155
- console.warn("No books selected. Cannot load data."); // لاگ برای توسعه
156
  memoirsWithEmbeddings = []; // پاک کردن داده های قبلی
157
  checkAndEnableSearchButton(); // مطمئن می شویم دکمه غیرفعال بماند
158
- return; // توقف فرآیند اگر هیچ کتابی انتخاب نشده است
159
  }
160
  // ********************************************************
161
 
@@ -171,28 +213,27 @@ async function updateSelectedBooksData() {
171
  return fetch(filePath).then(response => {
172
  if (!response.ok) {
173
  // اگر فایل پیدا نشد یا خطای دیگری رخ داد (مثلاً 404)
174
- console.error(`Workspace error for ${filePath}: Status ${response.status}`); // لاگ خطا برای توسعه
175
- // تلاش برای خواندن متن خطا از پاسخ برای اطلاعات بیشتر
176
  return response.text().then(text => {
177
  console.error(`Response body for ${filePath}: ${text}`);
178
  throw new Error(`Error fetching file: "${filename}" (Status: ${response.status}). Check if the file exists at "${filePath}".`);
179
  }).catch(() => {
180
- // اگر خواندن متن خطا هم موفق نبود
181
  throw new Error(`Error fetching file: "${filename}" (Status: ${response.status}). Check if the file exists at "${filePath}".`);
182
  });
183
 
184
  }
185
- // بررسی Content-Type پاسخ برای اطمینان از اینکه واقعا JSON است
186
  const contentType = response.headers.get("content-type");
187
  if (!contentType || !contentType.includes("application/json")) {
188
- console.error(`Workspace error for ${filePath}: Expected application/json, but received ${contentType}`);
189
  throw new Error(`Error fetching file: "${filename}". Expected JSON, but received unexpected content type.`);
190
  }
191
  return response.json();
192
  })
193
  .catch(error => {
194
  // گرفتن خطاهای شبکه یا تجزیه JSON
195
- console.error(`Failed to fetch or parse file "${filename}":`, error); // لاگ خطا برای توسعه
196
  throw new Error(`Failed to load data for book file "${filename}". ${error.message || 'Unknown error'}. Check file name and location.`);
197
  });
198
  });
@@ -201,32 +242,31 @@ async function updateSelectedBooksData() {
201
  const booksData = await Promise.all(fetchPromises); // انتظار برای دانلود و تجزیه همه فایل ها
202
 
203
  // ترکیب داده‌ها از تمام فایل‌های JSON بارگذاری شده و افزودن اطلاعات کتاب
204
- booksData.forEach((data, index) => { // اضافه کردن index برای دسترسی به selectedBookFiles
205
- const filename = selectedBookFiles[index]; // نام فایل JSON فعلی (value چک باکس)
206
- // پیدا کردن نام نمایشی کتاب از map bookInfo بر اساس نام فایل
207
  const bookDisplayName = bookInfo[filename] || filename;
208
 
209
  if (Array.isArray(data)) {
210
  // فیلتر کردن آیتم هایی که بردار embedding معتبر دارند و افزودن اطلاعات کتاب به آن ها
211
- // مطمئن می شویم که آیتم یک آبجکت معتبر است و فیلد embedding را دارد
212
  const memoirsWithBookInfo = data.filter(item => item && typeof item === 'object' && item.embedding && Array.isArray(item.embedding) && item.embedding.length > 0)
213
  .map(item => ({
214
  ...item, // کپی کردن تمام پراپرتی های موجود (passage_original, passage_combined, reference, embedding, etc.)
215
- book_title: bookDisplayName, // افزودن نام نمایشی کتاب
216
- book_file: filename // افزودن نام فایل اصلی کتاب
217
  }));
218
 
219
  memoirsWithEmbeddings = memoirsWithEmbeddings.concat(memoirsWithBookInfo);
220
 
221
- // هشدار برای آیتم هایی که بردار ندارند یا فرمتشان مشکل دارد (فقط در کنسول)
222
- const invalidMemoirsCount = data.length - memoirsWithBookInfo.length; // تعداد کل - تعداد معتبر با اطلاعات کتاب
223
  if(invalidMemoirsCount > 0){
224
- console.warn(`Skipped ${invalidMemoirsCount} items from file "${filename}" due to missing or invalid embedding or format.`);
225
  }
226
 
227
  } else {
228
- console.error(`Loaded data from "${filename}" is not an array:`, data); // لاگ برای توسعه
229
- updateSelectionError(`خطا در فرمت داده از فایل "${filename}". انتظار آرایه داشتیم.`);
230
  }
231
  });
232
 
@@ -234,139 +274,131 @@ async function updateSelectedBooksData() {
234
  const totalPassagesLoaded = memoirsWithEmbeddings.length;
235
 
236
  if (totalPassagesLoaded === 0) {
237
- console.warn("No valid passages with embeddings were loaded after processing all files.");
238
- updateStatus(`داده‌ها بارگذاری شد، اما هیچ خاطره‌ای با بردار معتبر از کتاب‌های انتخاب شده (${loadedBooksCount} کتاب) یافت نشد.`, true); // پیام خطا برای کاربر
239
- updateSelectionError("هیچ خاطره‌ای با بردار معتبر از کتاب‌های انتخاب شده یافت نشد. فرمت فایل‌های JSON و وجود فیلد embedding را بررسی کنید.");
 
240
  } else {
241
- console.log(`Successfully loaded data from ${loadedBooksCount} book(s). Total valid passages loaded: ${totalPassagesLoaded}`); // لاگ برای توسعه
242
- updateStatus(`داده‌ها از ${loadedBooksCount} کتاب با موفقیت بارگذاری شد. مجموع خاطرات قابل جستجو: ${totalPassagesLoaded}. آماده جستجو هستید.`); // پیام برای کار
 
243
  }
244
 
245
- checkAndEnableSearchButton(); // بررسی و فعال کردن دکمه پس از بارگذاری داده
246
-
247
 
248
  } catch (error) {
249
- console.error("Error loading selected books data:", error); // لاگ برای توسعه
250
- updateStatus("خطا در بارگذاری داده‌ها.", true); // پیام برای کار
251
- // پیام خطای برای کاربر شامل جزئیات بیشتر از علت خطا
 
252
  updateSelectionError(`خطا در بارگذاری داده‌ها: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر.`);
253
  memoirsWithEmbeddings = []; // اطمینان از خالی بودن داده در صورت خطا
254
  checkAndEnableSearchButton(); // مطمئن می شویم دکمه غیرفعال بماند
255
- if (searchResultsContainer) { // بازگرداندن پیام اولیه در صورت خطا
256
  searchResultsContainer.innerHTML = '<p>پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.</p>';
257
  }
258
  } finally {
259
- // در نهایت، چه بارگذاری موفقیت آمیز باشد چه با خطا مواجه شود، دکمه جستجو وضعیت خود را بررسی می کند.
260
- // checkAndEnableSearchButton(); // این فراخوانی در پایان هر دو شاخه try/catch در بالا نیز وجود دارد.
261
  }
262
  }
263
 
264
 
265
  // ****** تابع کمکی برای محاسبه شباهت کسینوسی بین دو بردار ******
266
- // این تابع همان تابع استاندارد محاسبه شباهت کسینوسی است.
267
  function cosineSimilarity(vecA, vecB) {
268
- // بررسی وجود و صحت بردارها قبل از شروع محاسبات
269
  if (!vecA || !vecB || vecA.length !== vecB.length || vecA.length === 0) {
270
- console.error("Cosine Similarity Error: Invalid or empty vectors provided.", {vecA_length: vecA ? vecA.length : 'null', vecB_length: vecB ? vecB.length : 'null'});
271
- return 0; // برگرداندن 0 در صورت ورودی نامعتبر
272
  }
273
 
274
  let dotProduct = 0;
275
  let magnitudeA = 0;
276
  let magnitudeB = 0;
277
 
278
- // محاسبه نقطه ضرب و مربع اندازه بردارها در یک حلقه برای بهینه سازی
279
  for (let i = 0; i < vecA.length; i++) {
280
  dotProduct += vecA[i] * vecB[i];
281
  magnitudeA += vecA[i] * vecA[i];
282
  magnitudeB += vecB[i] * vecB[i];
283
  }
284
 
285
- // گرفتن ریشه دوم برای اندازه بردارها
286
  magnitudeA = Math.sqrt(magnitudeA);
287
  magnitudeB = Math.sqrt(magnitudeB);
288
 
289
- // جلوگیری از تقسیم بر صفر اگر یکی از بردارها صفر باشد
290
  if (magnitudeA === 0 || magnitudeB === 0) {
291
- //console.warn("Cosine Similarity Warning: Magnitude is zero for one or both vectors."); // لاگ هشدار برای توسعه (اختیاری)
292
  return 0;
293
  }
294
 
295
- // محاسبه شباهت کسینوسی
296
  const similarity = dotProduct / (magnitudeA * magnitudeB);
297
- //console.log(`Calculated similarity: ${similarity}`); // لاگ برای توسعه (اختیاری، ممکن است خیلی زیاد شود)
298
- return similarity; // برگرداندن امتیاز شباهت
299
  }
300
 
301
- // تابع کمکی برای حذف بخش کلمات کلیدی از متن Passage
302
- // این تابع بر اساس فرمتی که در generate_embeddings.py برای ترکیب متن و کلیدواژه ها استفاده شده، کار می کند.
303
- // با توجه به استفاده مستقیم از passage_original، این تابع ممکن است دیگر به صورت مستقیم برای نمایش متن اصلی استفاده نشود،
304
- // اما می تواند برای پردازش passage_combined اگر کاربر بخواهد استفاده شود.
305
  function cleanPassageTextForDisplay(passage) {
306
  if (!passage || typeof passage !== 'string') {
307
- console.warn("cleanPassageTextForDisplay received invalid input:", passage);
308
- return ''; // برگرداندن رشته خالی برای ورودی نامعتبر
309
  }
310
- // فرض می کنیم فرمت ترکیب متن همان ' <کلیدواژه ها: ...>' است.
311
  const startDelimiter = ' <کلیدواژه ها: ';
312
  const startIndex = passage.indexOf(startDelimiter);
313
 
314
  if (startIndex === -1) {
315
- return passage.trim(); // اگر جداکننده پیدا نشد، کل متن را برگردان (با حذف فاصله های اضافی ابتدا و انتها)
316
  }
317
 
318
- // اگر جداکننده پیدا شد، فقط بخش قبل از آن را برگردان
319
  let cleanText = passage.substring(0, startIndex);
320
- return cleanText.trim(); // حذف فاصله های اضافی ابتدا و ان��های متن تمیز شده
321
  }
322
 
323
 
324
  // ****** تعریف کامل تابع async function searchMemoirs() { ... } ******
325
- // این تابع شامل منطق اصلی جستجو است: دریافت بردار سوال، محاسبه شباهت، فیلتر، مرتب سازی، نمایش نتایج.
326
  async function searchMemoirs() {
327
- console.log("Search triggered."); // لاگ برای توسعه
328
- console.log(`Data loaded state (passages count): ${memoirsWithEmbeddings.length}`); // لاگ برای توسعه
329
 
330
- // ****** چک کردن اینکه داده ها (از کتاب های انتخاب شده) بارگذاری شده باشند ******
331
  if (memoirsWithEmbeddings.length === 0) {
332
- console.warn("No memoir data loaded. Cannot search."); // لاگ برای توسعه
333
- updateSelectionError("لطفاً ابتدا کتاب‌های مورد نظر برای جستجو را انتخاب کرده و منتظر بارگذاری داده‌ها بمانید."); // پیام خطا برای کاربر
334
- // searchResultsContainer در ابتدای updateSelectedBooksData() پاک می شود.
335
  return;
336
  }
337
  // **************************************************************************
338
 
339
- // اطمینان از وجود userQuestionInput قبل از دسترسی به value
340
  if (!userQuestionInput) {
341
- console.error("Search input element not found. Cannot get query.");
342
- updateSelectionError("المان ورودی جستجو پیدا نشد. امکان جستجو وجود ندارد.");
343
  return;
344
  }
345
  const query = userQuestionInput.value.trim();
346
- console.log(`Query text is: "${query}"`); // لاگ برای توسعه
347
 
348
  if (!query) {
349
- if (searchResultsContainer) { // استفاده از نام متغیر صحیح
350
- searchResultsContainer.innerHTML = `<p>لطفاً عبارت مورد نظر برای جستجو را وارد کنید.</p>`; // پیام برای کاربر
351
  }
352
- console.warn("Search query is empty."); // لاگ برای توسعه
353
- updateSelectionError("لطفاً عبارت مورد نظر برای جستجو را وارد کنید."); // پیام خطا برای کاربر
354
  return;
355
  }
356
 
357
- // نمایش پیام "در حال جستجو" و غیرفعال کردن دکمه
358
- updateStatus("در حال جستجو...", false); // رنگ خاکستری
359
- updateSelectionError(""); // پاک کردن پیام خطای احتمالی قبلی
360
  if (searchResultsContainer) { // پاک کردن نتایج قبلی
361
  searchResultsContainer.innerHTML = '';
362
  }
363
- setButtonEnabled(false); // غیرفعال کردن دکمه در طول جستجو
364
 
365
 
366
  try {
367
- console.log("Requesting query embedding from Backend..."); // لاگ برای توسعه
368
 
369
- // ارسال درخواست به سرور Backend برای دریافت بردار سوال
370
  const serverResponse = await fetch(EMBEDDING_SERVER_URL, {
371
  method: 'POST',
372
  headers: {
@@ -375,320 +407,283 @@ async function searchMemoirs() {
375
  body: JSON.stringify({ query: query })
376
  });
377
 
378
- // بررسی موفقیت آمیز بودن پاسخ سرور (کد وضعیت 2xx)
379
  if (!serverResponse.ok) {
380
- const errorBody = await serverResponse.text(); // بخوان به صورت متن برای اطلاعات بیشتر
381
- console.error(`Backend responded with status ${serverResponse.status}:`, errorBody); // لاگ خطا برای توسعه
382
 
383
- // تلاش برای تجزیه پاسخ خطا به صورت JSON اگر سرور خطای JSON برگردانده باشد
384
  let serverErrorMessage = `خطا از Backend (${serverResponse.status}).`;
385
- try {
386
  const errorJson = JSON.parse(errorBody);
387
  if (errorJson.error) {
388
  serverErrorMessage += ` پیام: ${errorJson.error}`;
389
  }
390
- } catch (e) {
391
- // اگر پاسخ خطا JSON نبود، متن خام را اضافه کن
392
- serverErrorMessage += ` پاسخ خام: ${errorBody.substring(0, Math.min(errorBody.length, 100))}...`; // نمایش بخشی از پاسخ خام (حداثر 100 کاراکتر)
393
  }
394
 
395
- updateStatus("جستجو با خطا مواجه شد.", true); // پیام اصلی برای کاربر (رنگ قرمز)
396
- updateSelectionError(serverErrorMessage + " جزئیات بیشتر در کنسول مرورگر."); // نمایش جزئیات خطا در بخش خطای انتخاب
397
- return; // خروج از تابع searchMemoirs پس از خطا
398
-
399
  }
400
 
401
- // دریافت داده های JSON از پاسخ سرور
402
  const serverData = await serverResponse.json();
403
  const queryEmbeddingArray = serverData.embedding;
404
 
405
- // بررسی اینکه بردار embedding معتبر و غیر خالی است
406
  if (!queryEmbeddingArray || !Array.isArray(queryEmbeddingArray) || queryEmbeddingArray.length === 0) {
407
- console.error("Backend returned an invalid or empty embedding:", serverData); // لاگ خطا برای توسعه
408
- updateStatus("جستجو با خطا مواجه شد.", true); // پیام اصلی برای کاربر
409
- updateSelectionError("Backend بردار جستجو را به درستی برنگرداند. جزئیات در کنسول مرورگر."); // نمایش جزئیات خطا در بخش خطای انتخاب
410
- return; // خروج از تابع searchMemoirs
 
411
  }
412
 
413
- console.log("Query embedding received from Backend successfully."); // لاگ برای توسعه
414
- console.log(`Query embedding dimensions: ${queryEmbeddingArray.length}`); // لاگ برای توسعه
415
- console.log("Calculating similarities in browser..."); // لاگ برای توسعه
416
 
417
- // محاسبه شباهت بین بردار سوال و بردارهای تمام خاطرات بارگذاری شده
418
  const searchResults = [];
419
- // memoirsWithEmbeddings حاوی خاطرات با بردارهای embedding و اطلاعات کتاب است.
420
  for (const memoir of memoirsWithEmbeddings) {
421
- // اطمینان از وجود و صحت بردار embedding در آیتم خاطره
422
- // و اینکه بردار خاطره همان ابعاد بردار سوال را دارد (خیلی مهم)
423
  if (memoir.embedding && Array.isArray(memoir.embedding) && memoir.embedding.length === queryEmbeddingArray.length) {
424
  const similarity = cosineSimilarity(queryEmbeddingArray, memoir.embedding);
425
- // اضافه کردن تمام فیلدهای اصلی خاطره و امتیاز شباهت به نتیجه جستجو
426
- searchResults.push({ ...memoir, similarity: similarity }); // استفاده از نام فیلد similarity برای امتیاز شباهت
427
  } else {
428
- // هشدار برای آیتم های بدون بردار یا با ابعاد نامعتبر (فقط در کنسول)
429
- 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'}`);
430
  }
431
  }
432
- console.log(`Similarity calculation complete. Found ${searchResults.length} results with valid embeddings.`); // لاگ برای توسعه
433
 
434
 
435
- // ****** خواندن آستانه شباهت از ورودی کاربر و فیلتر کردن نتایج ******
436
- let currentSimilarityThreshold = DEFAULT_SIMILARITY_THRESHOLD; // مقدار پیش فرض به عنوان fallback
437
- if (similarityThresholdInput) { // اگر المان ورودی آستانه شباهت پیدا شد
438
  const inputValue = parseFloat(similarityThresholdInput.value);
439
- // چک می کنیم عدد معتبر باشد و در محدوده [0, 1] باشد
440
  if (!isNaN(inputValue) && inputValue >= 0.0 && inputValue <= 1.0) {
441
  currentSimilarityThreshold = inputValue;
442
  console.log("Using user-defined similarity threshold:", currentSimilarityThreshold); // لاگ
443
  } else {
444
- console.warn("Invalid similarity threshold input value, using default:", DEFAULT_SIMILARITY_THRESHOLD); // لاگ
445
- updateSelectionError(`مقدار آستانه شباهت وارد شده (${similarityThresholdInput.value}) معتبر نیست. از مقدار پیش فرض ${DEFAULT_SIMILARITY_THRESHOLD.toFixed(2)} استفاده می‌شود.`); // پیام خطا برای کاربر
446
- currentSimilarityThreshold = DEFAULT_SIMILARITY_THRESHOLD; // بازگشت به مقدار پیش فرض
447
  }
448
  } else {
449
- console.warn("Similarity threshold input element not found, using default threshold:", DEFAULT_SIMILARITY_THRESHOLD); // لاگ
450
  }
451
 
452
- // فیلتر کردن نتایج بر اساس آستانه شباهت
453
  const filteredResults = searchResults.filter(result => result.similarity >= currentSimilarityThreshold);
454
  console.log(`Filtered results based on threshold ${currentSimilarityThreshold.toFixed(2)}: ${filteredResults.length} results remaining.`); // لاگ
455
  // *************************************************************
456
 
457
 
458
  console.log("Sorting results by similarity..."); // لاگ
459
- // مرتب سازی نتایج فیلتر شده بر اساس امتیاز شباهت به صورت نزولی (بیشترین شباهت اول)
460
  filteredResults.sort((a, b) => b.similarity - a.similarity);
461
  console.log("Filtered results sorted."); // لاگ
462
 
463
- // ****** انتخاب تعداد نتایج برتر بر اساس انتخاب کاربر ******
464
- let finalResultsPerPage = 10; // مقدار پیش فرض
465
- if (resultsPerPageSelect) { // اگر المان انتخاب تعداد نتایج پیدا شد
466
- const selectedValue = parseInt(resultsPerPageSelect.value, 10); // خواندن مقدار انتخاب شده
467
- // اطمینان از اینکه عدد معتبر و مثبت است
468
- finalResultsPerPage = (!isNaN(selectedValue) && selectedValue > 0) ? selectedValue : 10; // مقدار پیش فرض 10
469
  } else {
470
- console.warn("Results per page select element not found, using default:", finalResultsPerPage); // لاگ
471
  }
472
 
473
- // برش لیست نتایج برای نمایش فقط تعداد مورد نظر
474
  const topResults = filteredResults.slice(0, finalResultsPerPage);
475
- console.log(`Displaying top ${topResults.length} results from filtered list based on user selection (${finalResultsPerPage} per page setting).`);
476
 
477
- // لاگ کردن نتایج برتر نمایش داده شده برای عیب یابی (اختیاری، می توانید حذف کنید)
478
- console.log("Top results data before display:", topResults); // <-- لاگ برای بررسی داده های نتایج
479
- // ********************************************
480
 
481
 
482
  // ****** منطق نمایش نتایج در HTML ******
483
- if (searchResultsContainer) { // اگر کانتینر نمایش نتایج پیدا شد
484
- // نتایج قبلی در ابتدای تابع پاک شده اند.
485
-
486
- if (topResults.length === 0) { // اگر هیچ نتیجه ای یافت نشد
487
- // پیام مناسب برای کاربر، شامل آستانه شباهت استفاده شده
488
  searchResultsContainer.innerHTML = `<p>نتیجه مرتبطی با آستانه شباهت مورد نظر (${currentSimilarityThreshold.toFixed(2)}) یافت نشد. سعی کنید عبارت دیگری را جستجو کنید یا آستانه را کاهش دهید.</p>`;
489
- console.log("No relevant results found after filtering and slicing."); // لاگ
490
- } else { // اگر نتایجی یافت شد
491
  console.log("Results found, updating DOM.");
492
 
493
- // ایجاد کانتینر برای لیست نتایج برای کنترل بهتر استایل دهی (اختیاری اما مفید)
494
  const resultsList = document.createElement('div');
495
- resultsList.classList.add('results-list'); // می توانید استایل هایی برای این کلاس در CSS اضافه کنید.
496
-
497
 
498
  topResults.forEach(result => {
499
- // ایجاد المان برای هر آیتم نتیجه
500
  const resultItem = document.createElement('div');
501
- resultItem.classList.add('result-item'); // کلاس اصلی هر نتیجه
502
-
503
-
504
- // ایجاد و اضافه کردن تمام بخش های یک نتیجه (امتیاز، نام کتاب، مرجع، متن خاطره، دکمه کپی)
505
 
506
- // ****** نمایش امتیاز شباهت ******
507
- // استفاده از p مطابق با ساختار HTML و کلاس CSS شما
508
  const similarityElement = document.createElement('p');
509
  similarityElement.classList.add('result-similarity');
510
- // نمایش امتیاز با 4 رقم اعشار
511
  similarityElement.textContent = `شباهت: ${result.similarity !== undefined ? result.similarity.toFixed(4) : 'N/A'}`;
512
- // اضافه کردن به آیتم نتیجه (ترتیب مهم است برای فلوت float: right)
513
  resultItem.appendChild(similarityElement);
514
- // ******************************
515
-
516
 
517
  // نمایش نام کتاب
518
- // استفاده از p مطابق با ساختار HTML و کلاس CSS شما
519
  const bookTitleElement = document.createElement('p');
520
  bookTitleElement.classList.add('result-book-title');
521
  bookTitleElement.textContent = `از کتاب: ${result.book_title || 'نامشخص'}`;
522
- resultItem.appendChild(bookTitleElement); // اضا��ه کردن به آیتم نتیجه
523
-
524
 
525
  // نمایش مرجع خاطره
526
- // استفاده از p مطابق با ساختار HTML و کلاس CSS شما
527
  const referenceElement = document.createElement('p');
528
  referenceElement.classList.add('result-reference');
529
- referenceElement.innerHTML = `<strong>مرجع:</strong> ${result.reference || 'نامشخص'}`; // استفاده از innerHTML برای bold کردن "مرجع:"
530
- resultItem.appendChild(referenceElement); // اضافه کردن به آیتم نتیجه
531
 
532
 
533
  // ****** نمایش متن اصلی خاطره (استفاده از passage_original) ******
534
- // استفاده از p مطابق با ساختار HTML و کلاس CSS شما
535
  const passageElement = document.createElement('p');
536
  passageElement.classList.add('result-passage');
537
  // مستقیماً از فیلد passage_original استفاده می کنیم
538
  passageElement.textContent = result.passage_original || 'متن خاطره موجود نیست.';
539
- resultItem.appendChild(passageElement); // اضافه کردن به آیتم نتیجه
540
  // ***************************************************************
541
 
542
 
543
  // ****** اضافه کردن دکمه کپی ******
544
  const copyButton = document.createElement('button');
545
- copyButton.classList.add('copy-button'); // کلاس CSS برای استایل دهی
546
- copyButton.textContent = 'کپی متن'; // متن دکمه
547
-
548
- // ****** اضافه کردن استایل Inline برای تست عیب‌یابی (موقت) ******
549
- // این استایل ها برای کمک به ظاهر شدن دکمه اضافه شده اند.
550
- // اگر با این استایل ها دکمه ظاهر شد، مشکل در فایل style.css یا اعمال آن است.
551
- // پس از حل مشکل نمایش در style.css، این خطوط را حذف کنید.
552
- copyButton.style.display = 'block'; // نمایش به صورت بلوک جداگانه
553
- copyButton.style.marginTop = '10px'; // فاصله از بالا
554
- copyButton.style.marginLeft = 'auto'; // راست چین کردن در RTL با margin auto و margin-right 0
555
  copyButton.style.marginRight = '0';
556
- copyButton.style.backgroundColor = '#007bff'; // رنگ آبی
557
  copyButton.style.color = 'white';
558
- copyButton.style.padding = '5px 5px'; // padding
559
  copyButton.style.border = 'none';
560
- copyButton.style.borderRadius = '4px'; // گوشه گرد
561
- copyButton.style.cursor = 'pointer'; // نشانگر موس
562
- copyButton.style.fontSize = '0.8em'; // اندازه فونت
563
- copyButton.style.fontFamily = "'Vazirmatn', sans-serif"; // اعمال فونت
564
- // *****************************************************************
565
-
566
-
567
- // Event listener برای دکمه کپی
568
- // این Listener در بلوک DOMContentLoaded به صورت Delegation اضافه شده و در اینجا فقط دکمه به DOM اضافه می شود.
569
- resultItem.appendChild(copyButton); // دکمه کپی در انتها
570
-
571
  // **************************************************************************
572
 
573
 
574
- resultsList.appendChild(resultItem); // اضافه کردن آیتم نتیجه به لیست نتایج
575
  });
576
 
577
- searchResultsContainer.appendChild(resultsList); // اضافه کردن لیست نتایج به کانتینر اصلی
578
  console.log("DOM updated with results.");
579
 
580
- // لاگ کردن نتایج برتر نمایش داده شده برای عیب یابی (اختیاری، می توانید حذف کنید)
581
  console.log(`Top ${topResults.length} results displayed (reference, book, similarity, passage start):`);
582
  topResults.forEach(result => {
583
- 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'}"`); // لاگ جزئیات بیشتر و جایگزینی خط جدید
584
  });
585
  }
586
 
587
  // به‌روزرسانی پیام وضعیت پس از جستجو
588
  if (topResults.length > 0) {
589
- updateStatus(`جستجو به پایان رسید. ${topResults.length} نتیجه برتر (پس از فیلتر با آستانه ${currentSimilarityThreshold.toFixed(2)}) نمایش داده شد.`, false); // پیام موفقیت (رنگ خاکستری)
590
  } else {
591
- updateStatus(`جستجو به پایان رسید. هیچ نتیجه مرتبطی با آستانه شباهت مورد نظر (${currentSimilarityThreshold.toFixed(2)}) یافت نشد. سعی کنید عبارت دیگری را جستجو کنید یا آستانه را کاهش دهید.`, false); // پیام وضعیت (رنگ خاکستری)
592
  }
593
 
594
 
595
  } else {
596
- console.error("Could not find searchResultsContainer to display results."); // لاگ
597
- updateStatus("جستجو با خطا مواجه شد.", true); // پیام خطا (رنگ قرمز)
598
- updateSelectionError("المان نمایش نتایج پیدا نشد. لطفاً ساختار HTML را بررسی کنید."); // پیام خطا در بخش انتخاب
599
  }
600
 
601
 
602
  } catch (error) {
603
  // مدیریت خطا هنگام درخواست به Backend یا پردازش پاسخ
604
- console.error("Error during search:", error); // لاگ
605
- if (searchResultsContainer) { // استفاده از نام متغیر صحیح
606
  searchResultsContainer.innerHTML = `<p style=\"color: red;\">هنگام جستجو خطایی رخ داد: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر موجود است.</p>`;
607
  }
608
- updateStatus("جستجو با خطا مواجه شد.", true); // پیام خطا (رنگ قرمز)
609
- updateSelectionError(`هنگام جستجو خطایی رخ داد: ${error.message || 'خطای نامشخص'}.`); // نمایش خطای اصلی در بخش خطای انتخاب
 
610
  } finally {
611
- // در نهایت، چه جستجو موفقیت آمیز باشد چه با خطا مواجه شود، دکمه جستجو وضعیت خود را بررسی می کند.
612
  checkAndEnableSearchButton(); // فعال کردن مجدد دکمه جستجو (اگر شرایط فراهم باشد)
613
  }
614
  }
615
- // پایان تعریف تابع async function searchMemoirs()
616
 
617
 
618
  // ****** بلوک DOMContentLoaded: این کد پس از بارگذاری کامل ساختار صفحه اجرا می شود ******
619
  document.addEventListener('DOMContentLoaded', () => {
620
- console.log("DOM fully loaded and parsed. Initializing script.");
621
 
622
  // ****** دریافت رفرنس المان های HTML (مطابق با ID ها و کلاس ها در index.html شما) ******
623
  // این رفرنس ها متغیرهای سراسری تعریف شده در بالای فایل هستند.
624
  searchButton = document.getElementById('searchButton');
625
- userQuestionInput = document.getElementById('userQuestion'); // <--- مطابق با ID در index.html شما
626
- searchResultsContainer = document.getElementById('searchResults'); // <--- مطابق با ID در index.html شما
627
- loadingStatusElement = document.getElementById('loadingStatus'); // <--- مطابق با ID در index.html شما
628
- selectionErrorElement = document.getElementById('selectionError'); // <--- مطابق با ID در index.html شما
629
 
630
- selectAllCheckbox = document.getElementById('select_all_books'); // <--- مطابق با ID در index.html شما
631
  // getElementsByClassName برمی گرداند HTMLCollection زنده.
632
- bookCheckboxes = document.getElementsByClassName('book-checkbox'); // <--- مطابق با class در index.html شما
 
 
 
 
 
 
633
 
634
- resultsPerPageSelect = document.getElementById('resultsPerPage'); // <--- مطابق با ID در index.html شما
635
- similarityThresholdInput = document.getElementById('similarityThresholdInput'); // <--- مطابق با ID در index.html شما
636
  // **********************************************************************************
637
 
638
 
639
  // ****** چک کردن وجود المان های ضروری برای جلوگیری از خطا ******
640
  // چک می کنیم که تمام المان های مورد نیاز برای اجرای اسکریپت پیدا شده باشند.
641
  // bookCheckboxes باید وجود داشته باشد و حداقل یک المان (چک باکس) داشته باشد.
642
- const requiredElementsFound = searchButton && userQuestionInput && searchResultsContainer && loadingStatusElement && selectionErrorElement && selectAllCheckbox && bookCheckboxes && bookCheckboxes.length > 0 && resultsPerPageSelect && similarityThresholdInput;
643
 
644
  if (requiredElementsFound) {
645
- console.log("All critical DOM elements found. Proceeding with initialization.");
646
 
647
  // ****** تنظیم Event Listeners ******
648
 
649
  // Listener برای دکمه جستجو: فراخوانی تابع searchMemoirs هنگام کلیک
650
  searchButton.addEventListener('click', searchMemoirs);
651
- console.log("Search button click listener added.");
652
 
653
  // Listener برای کلید Enter در کادر ورودی سوال: شبیه سازی کلیک روی دکمه جستجو
654
  userQuestionInput.addEventListener('keypress', (event) => {
655
  if (event.key === 'Enter') {
656
- event.preventDefault(); // جلوگیری از ارسال فرم پیش فرض مرورگر
657
- // فقط اگر دکمه جستجو فعال است، کلیک را شبیه سازی کن
658
- if (!searchButton.disabled) {
659
  searchButton.click();
660
- console.log("Enter key pressed in search input, simulating search button click."); // Log
661
  }
662
  }
663
  });
664
- console.log("Search input keypress listener added.");
665
 
666
 
667
  // ****** Listener برای تغییر محتوای کادر سوال: بررسی وضعیت دکمه جستجو ******
668
  // این Listener باعث می شود دکمه جستجو زمانی که متن وارد می شود فعال شود.
669
  userQuestionInput.addEventListener('input', () => {
670
- console.log("Search input value changed, checking button state."); // لاگ برای توسعه
671
  checkAndEnableSearchButton(); // <--- فراخوانی تابع بررسی وضعیت دکمه
672
  });
673
  console.log("Search input 'input' listener added.");
674
  // *********************************************************************
675
 
676
 
677
- // Listener برای تغییر مقدار ورودی آستانه شباهت: به‌روزرسانی وضعیت دکمه (فعال/غیرفعال شدن)
678
- // مقدار آستانه شباهت مستقیماً در تابع searchMemoirs از این input خوانده می شود.
679
  similarityThresholdInput.addEventListener('input', () => {
680
  console.log("Similarity threshold input value changed."); // لاگ
681
- checkAndEnableSearchButton(); // بررسی وضعیت دکمه پس از تغییر مقدار (اگر مقدار نامعتبر شود دکمه غیرفعال می شود)
682
  });
683
- console.log("Similarity threshold input listener added.");
684
 
685
- // Listener برای تغییر انتخاب در نتایج در صفحه (resultsPerPageSelect)
686
- // مقدار نتایج در صفحه مستقیماً در تابع searchMemoirs از این select خوانده می شود.
687
  resultsPerPageSelect.addEventListener('change', () => {
688
  console.log("Results per page setting changed."); // لاگ
689
- // نیازی به checkAndEnableSearchButton نیست چون تغییر این مقدار به تنهایی دکمه را فعال/غیرفعال نمی کند.
690
  });
691
- console.log("Results per page select listener added.");
692
 
693
 
694
  // Listener ها برای چک باکس 'انتخاب همه' و چک باکس های تکی کتاب ها
@@ -696,123 +691,111 @@ document.addEventListener('DOMContentLoaded', () => {
696
 
697
  if (selectAllCheckbox) {
698
  selectAllCheckbox.addEventListener('change', () => {
699
- const isChecked = selectAllCheckbox.checked; // استفاده مستقیم از selectAllCheckbox
700
- // getElementsByClassName برمی گرداند HTMLCollection زنده، باید آن را به آرایه تبدیل کرد
701
- Array.from(bookCheckboxes).forEach(cb => { // استفاده از bookCheckboxes
702
  cb.checked = isChecked;
703
  });
704
  console.log(`'Select All' checkbox changed to ${isChecked}. All book checkboxes updated.`); // لاگ
705
- // پس از تغییر انتخاب کتاب، باید داده ها را بارگذاری مجدد کنیم.
706
  updateSelectedBooksData(); // <--- فراخوانی بارگذاری مجدد داده ها
707
  });
708
- console.log("'Select All' checkbox change listener added.");
709
  } else {
710
- console.error("'Select All' checkbox element with ID 'select_all_books' not found."); // لاگ
711
- // اگر این المان پیدا نشد، قابلیت "انتخاب همه" کار نخواهد کرد.
712
  }
713
 
714
  if (bookCheckboxes && bookCheckboxes.length > 0) {
715
- Array.from(bookCheckboxes).forEach(cb => { // استفاده از bookCheckboxes
716
  cb.addEventListener('change', () => {
717
- // بررسی کنید که آیا همه چک باکس های تکی انتخاب شده اند
718
- const allOthersChecked = Array.from(bookCheckboxes).every(cb => cb.checked); // استفاده از bookCheckboxes
719
- if (selectAllCheckbox) { // اگر چک باکس "انتخاب همه" وجود دارد، وضعیت آن را بر اساس وضعیت چک باکس های تکی به‌روزرسانی کن
720
  selectAllCheckbox.checked = allOthersChecked;
721
  }
722
  console.log("Individual book checkbox changed. Checking 'Select All' status."); // لاگ
723
- // پس از تغییر انتخاب کتاب، باید داده ها را بارگذاری مجدد کنیم.
724
  updateSelectedBooksData(); // <--- فراخوانی بارگذاری مجدد داده ها
725
  });
726
  });
727
- console.log("Individual book checkboxes change listeners added.");
728
  } else {
729
- console.warn("No book checkboxes found with class 'book-checkbox'. Book selection filtering will not work as expected."); // لاگ
730
- // اگر المان های چک باکس تکی پیدا نشدند، فیلترینگ بر اساس کتاب انتخاب شده کار نخواهد کرد.
731
- // در این حالت، updateSelectedBooksData با لیست خالی selectedBookFiles اجرا خواهد شد.
732
  }
733
 
734
 
735
- // Listener برای دکمه های کپی (به صورت Delegation در searchResultsContainer)
736
- // این Listener روی کانتینر نتایج گوش می دهد و برای دکمه های کپی که به صورت پویا اضافه می شوند، فعال می شود.
737
  searchResultsContainer.addEventListener('click', (event) => {
738
- // چک می کنیم که آیا کلیک روی المانی با کلاس 'copy-button' رخ داده است؟
739
  if (event.target && event.target.classList && event.target.classList.contains('copy-button')) {
740
- // جلوگیری از رفتار پیش فرض دکمه (مثلا ارسال فرم اگر داخل فرم باشد)
741
- event.preventDefault(); // اضافه شده
742
 
743
- // المان والد نتیجه را پیدا می کنیم (.result-item)
744
  const resultItemElement = event.target.closest('.result-item');
745
  if (resultItemElement) {
746
- // متن کپی شامل متن خاطره، مرجع، نام کتاب و شباهت است.
747
- // این اطلاعات از متن نمایش داده شده در المان های نتیجه استخراج می شود.
748
- // استفاده از ? در selector برای جلوگیری از خطا اگر المان پیدا نشد.
749
  const passageText = resultItemElement.querySelector('.result-passage')?.textContent || '';
750
- // جایگزینی 'مرجع:' و trim برای تمیز کردن متن استخراج شده.
751
  const referenceText = resultItemElement.querySelector('.result-reference')?.textContent.replace('مرجع:', '').trim() || 'نامشخص';
752
  const bookTitleText = resultItemElement.querySelector('.result-book-title')?.textContent.replace('از کتاب:', '').trim() || 'نامشخص';
753
- const similarityElementText = resultItemElement.querySelector('.result-similarity')?.textContent || ''; // گرفتن متن کامل عنصر شباهت (مثلا "شباهت: 0.7500")
 
 
754
 
755
- // ساخت متنی که باید کپی شود با فرمت دلخواه
756
- const textToCopy = `خاطره:\n${passageText}\n\nمرجع: ${referenceText}\nاز کتاب: ${bookTitleText}\n${similarityElementText}`; // اضافه کردن متن کامل شباهت
757
 
758
 
759
  // کپی کردن متن به کلیپ بورد
760
  navigator.clipboard.writeText(textToCopy)
761
  .then(() => {
762
- // نمایش پیام موفقیت روی همان دکمه
763
  event.target.textContent = 'کپی شد!';
764
  setTimeout(() => {
765
  event.target.textContent = 'کپی متن';
766
- }, 2000); // بازگرداندن متن دکمه بعد از 2 ثانیه
767
- console.log("Passage, reference, book title, and similarity copied to clipboard."); // لاگ موفقیت
768
  })
769
  .catch(err => {
770
- console.error('Failed to copy text: ', err); // لاگ خطا در کنسول
771
- event.target.textContent = 'خطا در کپی'; // پیام بازخورد خطا
772
  setTimeout(() => {
773
  event.target.textContent = 'کپی متن';
774
  }, 2000);
775
  });
776
  } else {
777
- console.warn("Result item parent not found for copy button click."); // لاگ
778
  }
779
  }
780
  });
781
- console.log("Copy button delegation click listener added.");
782
 
783
 
784
  // ****** راه اندازی اولیه: بارگذاری داده ها هنگام بارگذاری صفحه ******
785
  // این تابع فرآیند بارگذاری داده از فایل های JSON بر اساس چک باکس های پیش فرض انتخاب شده در HTML را شروع می کند.
786
  updateSelectedBooksData(); // این فراخوانی در نهایت checkAndEnableSearchButton را صدا می زند.
787
- console.log("Initial data loading process started.");
788
 
789
 
790
  } else {
791
- // اگر تمام عناصر مورد نیاز پیدا نشدند، پیام خطا در کنسول و روی صفحه نمایش داده میشود
792
  const errorMessage = "خطا در بارگذاری صفحه: برخی یا تمام عناصر لازم (HTML) پیدا نشدند. شناسه‌های HTML و نام کلاس‌ها را در index.html بررسی کنید و مطمئن شوید همه عناصر ضروری وجود دارند."; // پیام خطا برای کاربر
793
- console.error(errorMessage, {
794
- searchButton: !!searchButton, // از !! برای تبدیل به boolean استفاده می کنیم
795
  userQuestionInput: !!userQuestionInput,
796
  searchResultsContainer: !!searchResultsContainer,
797
  loadingStatusElement: !!loadingStatusElement,
798
  selectionErrorElement: !!selectionErrorElement,
799
  selectAllCheckbox: !!selectAllCheckbox,
800
- bookCheckboxesCount: bookCheckboxes ? bookCheckboxes.length : 0, // تعداد المان ها را نشان می دهد
801
  resultsPerPageSelect: !!resultsPerPageSelect,
802
- similarityThresholdInput: !!similarityThresholdInput
 
803
  });
804
- if (searchResultsContainer) { // اگر کانتینر نتایج پیدا شد، خطا را در آن نمایش بده
805
  searchResultsContainer.innerHTML = `<p style=\"color: red;\">${errorMessage}</p>`;
806
  }
807
  // نمایش خطا در المان های وضعیت و خطای جداگانه
808
- updateStatus("راه‌اندازی اولیه با خطا مواجه شد.", true);
809
  updateSelectionError(errorMessage);
810
 
811
- // غیرفعال نگه داشتن دکمه جستجو اگر پیدا شد
812
  if (searchButton) {
813
  setButtonEnabled(false);
814
  }
815
- // نیازی به return نیست چون کد بعد از این بلوک اجرا نمی شود (به دلیل عدم وجود المان های ضروری)
816
  }
817
  });
818
  // پایان بلوک DOMContentLoaded و پایان کامل فایل script.js
 
1
  // این فایل script.js نسخه نهایی شده بر اساس کد اصلی شماست.
2
  // شامل تمام اصلاحات لازم برای کار در Hugging Face Space جدید با مدل heydariAI/persian-embeddings.
3
  // تمام قابلیت های اضافه شده و رفع اشکالات شناسایی شده در فرآیند عیب یابی در آن لحاظ شده است.
4
+ // این نسخه شامل مدیریت بصری وضعیت (لودینگ، آماده) با رنگ و اسپینر است.
5
+
6
 
7
  // ****** تعریف متغیرهای عناصر HTML در بالاترین اسکوپ ******
8
  // این متغیرها در بلوک DOMContentLoaded پس از بارگذاری صفحه مقداردهی اولیه می شوند.
 
17
 
18
  let resultsPerPageSelect; // <--- مطابق با ID در index.html شما
19
  let similarityThresholdInput; // <--- مطابق با ID در index.html شما
20
+
21
+ let loadingSpinnerElement; // <--- متغیر برای المان لودینگ اسپینر
22
 
23
  // *************************************************
24
 
 
53
  // ****** توابع کمکی برای مدیریت پیام ها و وضعیت UI ******
54
 
55
  // تابع کمکی برای نمایش پیام وضعیت بارگذاری/پردازش در المان loadingStatusElement
56
+ // Optional state parameter: 'loading', 'ready', 'error', '' (یا هر پیام دیگر که وضعیت خاصی را نشان ندهد)
57
+ function updateStatus(message, isError = false, state = '') {
58
  if (loadingStatusElement) {
59
+ loadingStatusElement.textContent = message; // Set text first
60
+
61
+ // پاک کردن کلاس های وضعیت قبلی
62
+ loadingStatusElement.classList.remove('loading', 'ready', 'error');
63
+
64
+ // اضافه کردن کلاس وضعیت مناسب بر اساس isError یا پارامتر state
65
+ if (isError) {
66
+ loadingStatusElement.classList.add('error');
67
+ // رنگ و فونت ضخیم توسط کلاس .error-message یا .status-message.error در CSS مدیریت می شود
68
+ loadingStatusElement.style.color = ''; // ریست کردن استایل های اینلاین احتمالی
69
+ loadingStatusElement.style.fontWeight = ''; // ریست کردن استایل های اینلاین احتمالی
70
+ } else if (state === 'loading') {
71
+ loadingStatusElement.classList.add('loading');
72
+ loadingStatusElement.style.color = '';
73
+ loadingStatusElement.style.fontWeight = '';
74
+ } else if (state === 'ready') {
75
+ loadingStatusElement.classList.add('ready');
76
+ loadingStatusElement.style.color = '';
77
+ loadingStatusElement.style.fontWeight = '';
78
+ } else {
79
+ // وضعیت پیش فرض یا پیام های عادی بدون وضعیت خاص
80
+ loadingStatusElement.style.color = '#666'; // رنگ پیش فرض
81
+ loadingStatusElement.style.fontWeight = 'normal'; // فونت نرمال پیش فرض
82
+ }
83
+
84
+ // مدیریت نمایش اسپینر (اسپینر با CSS و کلاس .status-message.loading کنترل می شود)
85
+ // نیازی به نمایش/پنهان کردن مستقیم اسپینر اینجا نیست اگر CSS به درستی تنظیم شده باشد.
86
+
87
+ loadingStatusElement.style.display = message ? 'flex' : 'none'; // استفاده از flex برای نمایش و مرکز کردن محتوا (متن و اسپینر)
88
+ // اگر پیام خالی باشد، المان مخفی می شود.
89
+
90
+
91
  } else {
92
+ console.log("Status:", message, "isError:", isError, "state:", state); // لاگ برای توسعه
93
  }
94
+
95
+ // پاک کردن پیام خطای انتخاب کتاب اگر یک پیام وضعیت نمایش داده می شود
96
  if (message && selectionErrorElement) {
97
  selectionErrorElement.textContent = '';
98
  selectionErrorElement.style.display = 'none';
99
+ selectionErrorElement.classList.remove('error'); // پاک کردن کلاس خطا
100
  }
101
  }
102
 
 
104
  function updateSelectionError(message) {
105
  if (selectionErrorElement) {
106
  selectionErrorElement.textContent = message;
107
+ selectionErrorElement.style.color = 'red'; // رنگ قرمز
108
+ selectionErrorElement.style.fontWeight = 'bold'; // فونت ضخیم
109
+ selectionErrorElement.style.display = message ? 'flex' : 'none'; // نمایش با فلکس
110
+ selectionErrorElement.classList.add('error'); // اضافه کردن کلاس خطا
111
+
112
  } else {
113
  console.error("Selection Error Element not found."); // لاگ برای توسعه
114
  }
115
+ // پاک کردن پیام وضعیت اگر یک پیام خطای انتخاب نمایش داده می شود
116
  if (message && loadingStatusElement) {
117
  loadingStatusElement.textContent = '';
118
  loadingStatusElement.style.display = 'none';
119
+ loadingStatusElement.classList.remove('loading', 'ready'); // پاک کردن کلاس های وضعیت
120
+ // اسپینر نیز توسط CSS و کلاس مدیریت می شود.
121
  }
122
  }
123
 
 
160
  // این تابع هر زمان که انتخاب کتاب ها تغییر می کند، داده ها را بارگذاری مجدد می کند.
161
  async function updateSelectedBooksData() {
162
  console.log("Updating selected books data..."); // لاگ برای توسعه
163
+ // تنظیم وضعیت به "در حال بارگذاری" با رنگ و اسپینر
164
+ updateStatus("در حال بارگذاری داده‌ها...", false, 'loading');
165
+ updateSelectionError(""); // پاک کردن پیام خطاهای قبلی
166
+ setButtonEnabled(false); // غیرفعال کردن دکمه جستجو
167
+
168
+
169
  if (searchResultsContainer) { // پاک کردن نتایج قبلی
170
+ // نمایش پیام اولیه واضح در بخش نتایج
171
+ searchResultsContainer.innerHTML = '<p>در حال بارگذاری اطلاعات کتاب‌ها. لطفاً صبر کنید تا دکمه جستجو فعال شود...</p>';
172
  }
173
 
174
 
 
177
  if (!bookCheckboxes || bookCheckboxes.length === 0) {
178
  console.error("No book checkboxes found with class 'book-checkbox'. Cannot load data.");
179
  updateStatus(""); // پاک کردن پیام وضعیت
180
+ updateSelectionError("المان‌های انتخاب کتاب یافت نشد. لطفاً ساختار HTML را بررسی کنید."); // پیام خطا
181
  memoirsWithEmbeddings = [];
182
  checkAndEnableSearchButton();
183
  return;
 
193
  // ****** چک کردن اینکه حداقل یک کتاب انتخاب شده باشد ******
194
  if (selectedBookFiles.length === 0) {
195
  updateStatus(""); // پاک کردن پیام وضعیت
196
+ updateSelectionError("لطفاً حداقل یک کتاب برای جستجو انتخاب کنید."); // پیام خطا
197
+ console.warn("No books selected. Cannot load data."); // لاگ
198
  memoirsWithEmbeddings = []; // پاک کردن داده های قبلی
199
  checkAndEnableSearchButton(); // مطمئن می شویم دکمه غیرفعال بماند
200
+ return; // توقف فرآیند
201
  }
202
  // ********************************************************
203
 
 
213
  return fetch(filePath).then(response => {
214
  if (!response.ok) {
215
  // اگر فایل پیدا نشد یا خطای دیگری رخ داد (مثلاً 404)
216
+ console.error(`Workspace error for ${filePath}: Status ${response.status}`); // لاگ خطا
217
+ // تلاش برای خواندن متن خطا از پاسخ
218
  return response.text().then(text => {
219
  console.error(`Response body for ${filePath}: ${text}`);
220
  throw new Error(`Error fetching file: "${filename}" (Status: ${response.status}). Check if the file exists at "${filePath}".`);
221
  }).catch(() => {
 
222
  throw new Error(`Error fetching file: "${filename}" (Status: ${response.status}). Check if the file exists at "${filePath}".`);
223
  });
224
 
225
  }
226
+ // بررسی Content-Type پاسخ
227
  const contentType = response.headers.get("content-type");
228
  if (!contentType || !contentType.includes("application/json")) {
229
+ console.error(`Workspace error for ${filePath}: Expected application/json, but received ${contentType}`); // لاگ خطا
230
  throw new Error(`Error fetching file: "${filename}". Expected JSON, but received unexpected content type.`);
231
  }
232
  return response.json();
233
  })
234
  .catch(error => {
235
  // گرفتن خطاهای شبکه یا تجزیه JSON
236
+ console.error(`Failed to fetch or parse file "${filename}":`, error); // لاگ خطا
237
  throw new Error(`Failed to load data for book file "${filename}". ${error.message || 'Unknown error'}. Check file name and location.`);
238
  });
239
  });
 
242
  const booksData = await Promise.all(fetchPromises); // انتظار برای دانلود و تجزیه همه فایل ها
243
 
244
  // ترکیب داده‌ها از تمام فایل‌های JSON بارگذاری شده و افزودن اطلاعات کتاب
245
+ booksData.forEach((data, index) => { // اضافه کردن index
246
+ const filename = selectedBookFiles[index]; // نام فایل JSON فعلی
247
+ // پیدا کردن نام نمایشی کتاب از map bookInfo
248
  const bookDisplayName = bookInfo[filename] || filename;
249
 
250
  if (Array.isArray(data)) {
251
  // فیلتر کردن آیتم هایی که بردار embedding معتبر دارند و افزودن اطلاعات کتاب به آن ها
 
252
  const memoirsWithBookInfo = data.filter(item => item && typeof item === 'object' && item.embedding && Array.isArray(item.embedding) && item.embedding.length > 0)
253
  .map(item => ({
254
  ...item, // کپی کردن تمام پراپرتی های موجود (passage_original, passage_combined, reference, embedding, etc.)
255
+ book_title: bookDisplayName, // افزودن نام نمایشی
256
+ book_file: filename // افزودن نام فایل
257
  }));
258
 
259
  memoirsWithEmbeddings = memoirsWithEmbeddings.concat(memoirsWithBookInfo);
260
 
261
+ // هشدار برای آیتم های نامعتبر
262
+ const invalidMemoirsCount = data.length - memoirsWithBookInfo.length;
263
  if(invalidMemoirsCount > 0){
264
+ console.warn(`Skipped ${invalidMemoirsCount} items from file "${filename}" due to missing or invalid embedding or format.`); // لاگ هشدار
265
  }
266
 
267
  } else {
268
+ console.error(`Loaded data from "${filename}" is not an array:`, data); // لاگ خطا
269
+ updateSelectionError(`خطا در فرمت داده از فایل "${filename}". انتظار آرایه داشتیم.`); // پیام خطا
270
  }
271
  });
272
 
 
274
  const totalPassagesLoaded = memoirsWithEmbeddings.length;
275
 
276
  if (totalPassagesLoaded === 0) {
277
+ console.warn("No valid passages with embeddings were loaded..."); // لاگ هشدار
278
+ // تنظیم وضعیت به "خطا در بارگذاری"
279
+ updateStatus(`داده‌ها بارگذاری شد، اما هیچ خاطره‌ای با بردار معتبر از کتاب‌های انتخاب شده (${loadedBooksCount} کتاب) یافت نشد.`, true, 'error');
280
+ updateSelectionError("هیچ خاطره‌ای با بردار معتبر از کتاب‌های انتخاب شده یافت نشد. فرمت فایل‌های JSON و وجود فیلد embedding را بررسی کنید."); // پیام خطا
281
  } else {
282
+ console.log(`Successfully loaded data from ${loadedBooksCount} book(s)...`); // لاگ
283
+ // تنظیم وضعیت به "آماده"
284
+ updateStatus(`داده‌ها از ${loadedBooksCount} کتاب با موفقیت بارگذاری شد. مجموع خاطرات قابل جستجو: ${totalPassagesLoaded}. آماده جستجو هستید.`, false, 'ready');
285
  }
286
 
287
+ checkAndEnableSearchButton(); // بررسی و فعال کردن دکمه
 
288
 
289
  } catch (error) {
290
+ console.error("Error loading selected books data:", error); // لاگ خطا
291
+ // تنظیم وضعیت به "خطا در بارگذاری"
292
+ updateStatus("خطا در بارگذاری داده‌ها.", true, 'error');
293
+ // پیام خطای برای کاربر شامل جزئیات بیشتر
294
  updateSelectionError(`خطا در بارگذاری داده‌ها: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر.`);
295
  memoirsWithEmbeddings = []; // اطمینان از خالی بودن داده در صورت خطا
296
  checkAndEnableSearchButton(); // مطمئن می شویم دکمه غیرفعال بماند
297
+ if (searchResultsContainer) { // بازگرداندن پیام اولیه
298
  searchResultsContainer.innerHTML = '<p>پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.</p>';
299
  }
300
  } finally {
301
+ // checkAndEnableSearchButton در هر دو شاخه try/catch صدا زده می شود.
 
302
  }
303
  }
304
 
305
 
306
  // ****** تابع کمکی برای محاسبه شباهت کسینوسی بین دو بردار ******
 
307
  function cosineSimilarity(vecA, vecB) {
308
+ // بررسی وجود و صحت بردارها
309
  if (!vecA || !vecB || vecA.length !== vecB.length || vecA.length === 0) {
310
+ console.error("Cosine Similarity Error: Invalid or empty vectors provided.", {vecA_length: vecA ? vecA.length : 'null', vecB_length: vecB ? vecB.length : 'null'}); // لاگ خطا
311
+ return 0; // برگرداندن 0
312
  }
313
 
314
  let dotProduct = 0;
315
  let magnitudeA = 0;
316
  let magnitudeB = 0;
317
 
318
+ // محاسبه نقطه ضرب و مربع اندازه
319
  for (let i = 0; i < vecA.length; i++) {
320
  dotProduct += vecA[i] * vecB[i];
321
  magnitudeA += vecA[i] * vecA[i];
322
  magnitudeB += vecB[i] * vecB[i];
323
  }
324
 
325
+ // گرفتن ریشه دوم
326
  magnitudeA = Math.sqrt(magnitudeA);
327
  magnitudeB = Math.sqrt(magnitudeB);
328
 
329
+ // جلوگیری از تقسیم بر صفر
330
  if (magnitudeA === 0 || magnitudeB === 0) {
 
331
  return 0;
332
  }
333
 
334
+ // محاسبه شباهت
335
  const similarity = dotProduct / (magnitudeA * magnitudeB);
336
+ return similarity; // برگرداندن امتیاز
 
337
  }
338
 
339
+ // تابع کمکی برای حذف بخش کلمات کلیدی از متن Passage_combined
340
+ // این تابع فعلاً برای نمایش passage_original مستقیماً استفاده نمی شود، اما نگه داشته شده است.
 
 
341
  function cleanPassageTextForDisplay(passage) {
342
  if (!passage || typeof passage !== 'string') {
343
+ console.warn("cleanPassageTextForDisplay received invalid input:", passage); // لاگ هشدار
344
+ return ''; // برگرداندن رشته خالی
345
  }
 
346
  const startDelimiter = ' <کلیدواژه ها: ';
347
  const startIndex = passage.indexOf(startDelimiter);
348
 
349
  if (startIndex === -1) {
350
+ return passage.trim(); // اگر جداکننده پیدا نشد
351
  }
352
 
 
353
  let cleanText = passage.substring(0, startIndex);
354
+ return cleanText.trim(); // حذف فاصله های اضافی
355
  }
356
 
357
 
358
  // ****** تعریف کامل تابع async function searchMemoirs() { ... } ******
 
359
  async function searchMemoirs() {
360
+ console.log("Search triggered."); // لاگ
361
+ console.log(`Data loaded state (passages count): ${memoirsWithEmbeddings.length}`); // لاگ
362
 
363
+ // ****** چک کردن اینکه داده ها بارگذاری شده باشند ******
364
  if (memoirsWithEmbeddings.length === 0) {
365
+ console.warn("No memoir data loaded. Cannot search."); // لاگ هشدار
366
+ updateSelectionError("لطفاً ابتدا کتاب‌های مورد نظر برای جستجو را انتخاب کرده و منتظر بارگذاری داده‌ها بمانید."); // پیام خطا
 
367
  return;
368
  }
369
  // **************************************************************************
370
 
371
+ // اطمینان از وجود userQuestionInput
372
  if (!userQuestionInput) {
373
+ console.error("Search input element not found."); // لاگ خطا
374
+ updateSelectionError("المان ورودی جستجو پیدا نشد. امکان جستجو وجود ندارد."); // پیام خطا
375
  return;
376
  }
377
  const query = userQuestionInput.value.trim();
378
+ console.log(`Query text is: "${query}"`); // لاگ
379
 
380
  if (!query) {
381
+ if (searchResultsContainer) {
382
+ searchResultsContainer.innerHTML = `<p>لطفاً عبارت مورد نظر برای جستجو را وارد کنید.</p>`; // پیام
383
  }
384
+ console.warn("Search query is empty."); // لاگ هشدار
385
+ updateSelectionError("لطفاً عبارت مورد نظر برای جستجو را وارد کنید."); // پیام خطا
386
  return;
387
  }
388
 
389
+ // نمایش پیام "در حال جستجو" با وضعیت لودینگ
390
+ updateStatus("در حال جستجو...", false, 'loading');
391
+ updateSelectionError(""); // پاک کردن خطاهای قبلی
392
  if (searchResultsContainer) { // پاک کردن نتایج قبلی
393
  searchResultsContainer.innerHTML = '';
394
  }
395
+ setButtonEnabled(false); // غیرفعال کردن دکمه
396
 
397
 
398
  try {
399
+ console.log("Requesting query embedding from Backend..."); // لاگ
400
 
401
+ // ارسال درخواست به Backend
402
  const serverResponse = await fetch(EMBEDDING_SERVER_URL, {
403
  method: 'POST',
404
  headers: {
 
407
  body: JSON.stringify({ query: query })
408
  });
409
 
410
+ // بررسی موفقیت آمیز بودن پاسخ
411
  if (!serverResponse.ok) {
412
+ const errorBody = await serverResponse.text(); // خواندن متن خطا
413
+ console.error(`Backend responded with status ${serverResponse.status}:`, errorBody); // لاگ خطا
414
 
 
415
  let serverErrorMessage = `خطا از Backend (${serverResponse.status}).`;
416
+ try { // تلاش برای خواندن JSON خطا
417
  const errorJson = JSON.parse(errorBody);
418
  if (errorJson.error) {
419
  serverErrorMessage += ` پیام: ${errorJson.error}`;
420
  }
421
+ } catch (e) { // اگر JSON نبود
422
+ serverErrorMessage += ` پاسخ خام: ${errorBody.substring(0, Math.min(errorBody.length, 100))}...`;
 
423
  }
424
 
425
+ // تنظیم وضعیت به "خطا"
426
+ updateStatus("جستجو با خطا مواجه شد.", true, 'error');
427
+ updateSelectionError(serverErrorMessage + " جزئیات بیشتر در کنسول مرورگر."); // نمایش خطا
428
+ return; // خروج
429
  }
430
 
431
+ // دریافت و بررسی پاسخ JSON
432
  const serverData = await serverResponse.json();
433
  const queryEmbeddingArray = serverData.embedding;
434
 
 
435
  if (!queryEmbeddingArray || !Array.isArray(queryEmbeddingArray) || queryEmbeddingArray.length === 0) {
436
+ console.error("Backend returned an invalid or empty embedding:", serverData); // لاگ خطا
437
+ // تنظیم وضعیت به "خطا"
438
+ updateStatus("جستجو با خطا مواجه شد.", true, 'error');
439
+ updateSelectionError("Backend بردار جستجو را به درستی برنگرداند. جزئیات در کنسول مرورگر."); // نمایش خطا
440
+ return; // خروج
441
  }
442
 
443
+ console.log("Query embedding received from Backend successfully."); // لاگ
444
+ console.log(`Query embedding dimensions: ${queryEmbeddingArray.length}`); // لاگ
445
+ console.log("Calculating similarities in browser..."); // لاگ
446
 
447
+ // محاسبه شباهت با تمام خاطرات
448
  const searchResults = [];
 
449
  for (const memoir of memoirsWithEmbeddings) {
450
+ // اطمینان از وجود و صحت بردار embedding و تطابق ابعاد
 
451
  if (memoir.embedding && Array.isArray(memoir.embedding) && memoir.embedding.length === queryEmbeddingArray.length) {
452
  const similarity = cosineSimilarity(queryEmbeddingArray, memoir.embedding);
453
+ searchResults.push({ ...memoir, similarity: similarity });
 
454
  } else {
455
+ 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'}`); // لاگ هشدار
 
456
  }
457
  }
458
+ console.log(`Similarity calculation complete. Found ${searchResults.length} results...`); // لاگ
459
 
460
 
461
+ // ****** فیلتر کردن نتایج بر اساس آستانه شباهت ******
462
+ let currentSimilarityThreshold = DEFAULT_SIMILARITY_THRESHOLD;
463
+ if (similarityThresholdInput) {
464
  const inputValue = parseFloat(similarityThresholdInput.value);
 
465
  if (!isNaN(inputValue) && inputValue >= 0.0 && inputValue <= 1.0) {
466
  currentSimilarityThreshold = inputValue;
467
  console.log("Using user-defined similarity threshold:", currentSimilarityThreshold); // لاگ
468
  } else {
469
+ console.warn("Invalid similarity threshold input value, using default:", DEFAULT_SIMILARITY_THRESHOLD); // لاگ هشدار
470
+ updateSelectionError(`مقدار آستانه شباهت وارد شده (${similarityThresholdInput.value}) معتبر نیست. از مقدار پیش فرض ${DEFAULT_SIMILARITY_THRESHOLD.toFixed(2)} استفاده می‌شود.`); // پیام خطا
471
+ currentSimilarityThreshold = DEFAULT_SIMILARITY_THRESHOLD;
472
  }
473
  } else {
474
+ console.warn("Similarity threshold input element not found...", DEFAULT_SIMILARITY_THRESHOLD); // لاگ هشدار
475
  }
476
 
 
477
  const filteredResults = searchResults.filter(result => result.similarity >= currentSimilarityThreshold);
478
  console.log(`Filtered results based on threshold ${currentSimilarityThreshold.toFixed(2)}: ${filteredResults.length} results remaining.`); // لاگ
479
  // *************************************************************
480
 
481
 
482
  console.log("Sorting results by similarity..."); // لاگ
 
483
  filteredResults.sort((a, b) => b.similarity - a.similarity);
484
  console.log("Filtered results sorted."); // لاگ
485
 
486
+ // ****** انتخاب تعداد نتایج برتر ******
487
+ let finalResultsPerPage = 10;
488
+ if (resultsPerPageSelect) {
489
+ const selectedValue = parseInt(resultsPerPageSelect.value, 10);
490
+ finalResultsPerPage = (!isNaN(selectedValue) && selectedValue > 0) ? selectedValue : 10;
 
491
  } else {
492
+ console.warn("Results per page select element not found...", finalResultsPerPage); // لاگ هشدار
493
  }
494
 
 
495
  const topResults = filteredResults.slice(0, finalResultsPerPage);
496
+ console.log(`Displaying top ${topResults.length} results...`); // لاگ
497
 
498
+ // لاگ کردن نتایج برتر برای عیب یابی (اختیاری)
499
+ // console.log("Top results data before display:", topResults);
 
500
 
501
 
502
  // ****** منطق نمایش نتایج در HTML ******
503
+ if (searchResultsContainer) {
504
+ if (topResults.length === 0) {
 
 
 
505
  searchResultsContainer.innerHTML = `<p>نتیجه مرتبطی با آستانه شباهت مورد نظر (${currentSimilarityThreshold.toFixed(2)}) یافت نشد. سعی کنید عبارت دیگری را جستجو کنید یا آستانه را کاهش دهید.</p>`;
506
+ console.log("No relevant results found..."); // لاگ
507
+ } else {
508
  console.log("Results found, updating DOM.");
509
 
 
510
  const resultsList = document.createElement('div');
511
+ resultsList.classList.add('results-list');
 
512
 
513
  topResults.forEach(result => {
 
514
  const resultItem = document.createElement('div');
515
+ resultItem.classList.add('result-item');
 
 
 
516
 
517
+ // نمایش امتیاز شباهت
 
518
  const similarityElement = document.createElement('p');
519
  similarityElement.classList.add('result-similarity');
 
520
  similarityElement.textContent = `شباهت: ${result.similarity !== undefined ? result.similarity.toFixed(4) : 'N/A'}`;
 
521
  resultItem.appendChild(similarityElement);
 
 
522
 
523
  // نمایش نام کتاب
 
524
  const bookTitleElement = document.createElement('p');
525
  bookTitleElement.classList.add('result-book-title');
526
  bookTitleElement.textContent = `از کتاب: ${result.book_title || 'نامشخص'}`;
527
+ resultItem.appendChild(bookTitleElement);
 
528
 
529
  // نمایش مرجع خاطره
 
530
  const referenceElement = document.createElement('p');
531
  referenceElement.classList.add('result-reference');
532
+ referenceElement.innerHTML = `<strong>مرجع:</strong> ${result.reference || 'نامشخص'}`;
533
+ resultItem.appendChild(referenceElement);
534
 
535
 
536
  // ****** نمایش متن اصلی خاطره (استفاده از passage_original) ******
 
537
  const passageElement = document.createElement('p');
538
  passageElement.classList.add('result-passage');
539
  // مستقیماً از فیلد passage_original استفاده می کنیم
540
  passageElement.textContent = result.passage_original || 'متن خاطره موجود نیست.';
541
+ resultItem.appendChild(passageElement);
542
  // ***************************************************************
543
 
544
 
545
  // ****** اضافه کردن دکمه کپی ******
546
  const copyButton = document.createElement('button');
547
+ copyButton.classList.add('copy-button');
548
+ copyButton.textContent = 'کپی متن';
549
+ // استایل های اینلاین موقت برای تست - بهتر است به style.css منتقل شوند
550
+ copyButton.style.display = 'block';
551
+ copyButton.style.marginTop = '10px';
552
+ copyButton.style.marginLeft = 'auto';
 
 
 
 
553
  copyButton.style.marginRight = '0';
554
+ copyButton.style.backgroundColor = '#007bff';
555
  copyButton.style.color = 'white';
556
+ copyButton.style.padding = '5px 5px';
557
  copyButton.style.border = 'none';
558
+ copyButton.style.borderRadius = '4px';
559
+ copyButton.style.cursor = 'pointer';
560
+ copyButton.style.fontSize = '0.8em';
561
+ copyButton.style.fontFamily = "'Vazirmatn', sans-serif";
562
+ // **********************************
563
+ // Listener برای دکمه کپی به صورت Delegation در DOMContentLoaded اضافه شده است.
564
+ resultItem.appendChild(copyButton);
 
 
 
 
565
  // **************************************************************************
566
 
567
 
568
+ resultsList.appendChild(resultItem);
569
  });
570
 
571
+ searchResultsContainer.appendChild(resultsList);
572
  console.log("DOM updated with results.");
573
 
574
+ // لاگ کردن نتایج برتر نمایش داده شده (اختیاری)
575
  console.log(`Top ${topResults.length} results displayed (reference, book, similarity, passage start):`);
576
  topResults.forEach(result => {
577
+ 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'}"`);
578
  });
579
  }
580
 
581
  // به‌روزرسانی پیام وضعیت پس از جستجو
582
  if (topResults.length > 0) {
583
+ updateStatus(`جستجو به پایان رسید. ${topResults.length} نتیجه برتر (پس از فیلتر با آستانه ${currentSimilarityThreshold.toFixed(2)}) نمایش داده شد.`, false, 'ready'); // وضعیت "آماده"
584
  } else {
585
+ updateStatus(`جستجو به پایان رسید. هیچ نتیجه مرتبطی با آستانه شباهت مورد نظر (${currentSimilarityThreshold.toFixed(2)}) یافت نشد. سعی کنید عبارت دیگری را جستجو کنید یا آستانه را کاهش دهید.`, false, 'ready'); // وضعیت "آماده"
586
  }
587
 
588
 
589
  } else {
590
+ console.error("Could not find searchResultsContainer to display results."); // لاگ خطا
591
+ updateStatus("جستجو با خطا مواجه شد.", true, 'error'); // وضعیت "خطا"
592
+ updateSelectionError("المان نمایش نتایج پیدا نشد. لطفاً ساختار HTML را بررسی کنید."); // پیام خطا
593
  }
594
 
595
 
596
  } catch (error) {
597
  // مدیریت خطا هنگام درخواست به Backend یا پردازش پاسخ
598
+ console.error("Error during search:", error); // لاگ خطا
599
+ if (searchResultsContainer) {
600
  searchResultsContainer.innerHTML = `<p style=\"color: red;\">هنگام جستجو خطایی رخ داد: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر موجود است.</p>`;
601
  }
602
+ // تنظیم وضعیت به "خطا"
603
+ updateStatus("جستجو با خطا مواجه شد.", true, 'error');
604
+ updateSelectionError(`هنگام جستجو خطایی رخ داد: ${error.message || 'خطای نامشخص'}.`); // نمایش خطا
605
  } finally {
606
+ // در نهایت دکمه جستجو وضعیت خود را بررسی می کند.
607
  checkAndEnableSearchButton(); // فعال کردن مجدد دکمه جستجو (اگر شرایط فراهم باشد)
608
  }
609
  }
 
610
 
611
 
612
  // ****** بلوک DOMContentLoaded: این کد پس از بارگذاری کامل ساختار صفحه اجرا می شود ******
613
  document.addEventListener('DOMContentLoaded', () => {
614
+ console.log("DOM fully loaded and parsed. Initializing script."); // لاگ
615
 
616
  // ****** دریافت رفرنس المان های HTML (مطابق با ID ها و کلاس ها در index.html شما) ******
617
  // این رفرنس ها متغیرهای سراسری تعریف شده در بالای فایل هستند.
618
  searchButton = document.getElementById('searchButton');
619
+ userQuestionInput = document.getElementById('userQuestion'); // <--- مطابق با ID
620
+ searchResultsContainer = document.getElementById('searchResults'); // <--- مطابق با ID
621
+ loadingStatusElement = document.getElementById('loadingStatus'); // <--- مطابق با ID
622
+ selectionErrorElement = document.getElementById('selectionError'); // <--- مطابق با ID
623
 
624
+ selectAllCheckbox = document.getElementById('select_all_books'); // <--- مطابق با ID
625
  // getElementsByClassName برمی گرداند HTMLCollection زنده.
626
+ bookCheckboxes = document.getElementsByClassName('book-checkbox'); // <--- مطابق با class
627
+
628
+ resultsPerPageSelect = document.getElementById('resultsPerPage'); // <--- مطابق با ID
629
+ similarityThresholdInput = document.getElementById('similarityThresholdInput'); // <--- مطابق با ID
630
+
631
+ loadingSpinnerElement = document.querySelector('#loadingStatus .loading-spinner'); // <--- دریافت رفرنس اسپینر
632
+
633
 
 
 
634
  // **********************************************************************************
635
 
636
 
637
  // ****** چک کردن وجود المان های ضروری برای جلوگیری از خطا ******
638
  // چک می کنیم که تمام المان های مورد نیاز برای اجرای اسکریپت پیدا شده باشند.
639
  // bookCheckboxes باید وجود داشته باشد و حداقل یک المان (چک باکس) داشته باشد.
640
+ const requiredElementsFound = searchButton && userQuestionInput && searchResultsContainer && loadingStatusElement && selectionErrorElement && selectAllCheckbox && bookCheckboxes && bookCheckboxes.length > 0 && resultsPerPageSelect && similarityThresholdInput && loadingSpinnerElement;
641
 
642
  if (requiredElementsFound) {
643
+ console.log("All critical DOM elements found. Proceeding with initialization."); // لاگ
644
 
645
  // ****** تنظیم Event Listeners ******
646
 
647
  // Listener برای دکمه جستجو: فراخوانی تابع searchMemoirs هنگام کلیک
648
  searchButton.addEventListener('click', searchMemoirs);
649
+ console.log("Search button click listener added."); // لاگ
650
 
651
  // Listener برای کلید Enter در کادر ورودی سوال: شبیه سازی کلیک روی دکمه جستجو
652
  userQuestionInput.addEventListener('keypress', (event) => {
653
  if (event.key === 'Enter') {
654
+ event.preventDefault(); // جلوگیری از ارسال فرم
655
+ if (!searchButton.disabled) { // فقط اگر دکمه فعال است
 
656
  searchButton.click();
657
+ console.log("Enter key pressed in search input, simulating search button click."); // لاگ
658
  }
659
  }
660
  });
661
+ console.log("Search input keypress listener added."); // لاگ
662
 
663
 
664
  // ****** Listener برای تغییر محتوای کادر سوال: بررسی وضعیت دکمه جستجو ******
665
  // این Listener باعث می شود دکمه جستجو زمانی که متن وارد می شود فعال شود.
666
  userQuestionInput.addEventListener('input', () => {
667
+ console.log("Search input value changed, checking button state."); // لاگ
668
  checkAndEnableSearchButton(); // <--- فراخوانی تابع بررسی وضعیت دکمه
669
  });
670
  console.log("Search input 'input' listener added.");
671
  // *********************************************************************
672
 
673
 
674
+ // Listener برای تغییر مقدار ورودی آستانه شباهت: به‌روزرسانی وضعیت دکمه
 
675
  similarityThresholdInput.addEventListener('input', () => {
676
  console.log("Similarity threshold input value changed."); // لاگ
677
+ checkAndEnableSearchButton(); // بررسی وضعیت دکمه (اگر مقدار نامعتبر شود غیرفعال می شود)
678
  });
679
+ console.log("Similarity threshold input listener added."); // لاگ
680
 
681
+ // Listener برای تغییر انتخاب تعداد نتایج در صفحه
 
682
  resultsPerPageSelect.addEventListener('change', () => {
683
  console.log("Results per page setting changed."); // لاگ
684
+ // نیازی به checkAndEnableSearchButton نیست.
685
  });
686
+ console.log("Results per page select listener added."); // لاگ
687
 
688
 
689
  // Listener ها برای چک باکس 'انتخاب همه' و چک باکس های تکی کتاب ها
 
691
 
692
  if (selectAllCheckbox) {
693
  selectAllCheckbox.addEventListener('change', () => {
694
+ const isChecked = selectAllCheckbox.checked;
695
+ Array.from(bookCheckboxes).forEach(cb => {
 
696
  cb.checked = isChecked;
697
  });
698
  console.log(`'Select All' checkbox changed to ${isChecked}. All book checkboxes updated.`); // لاگ
 
699
  updateSelectedBooksData(); // <--- فراخوانی بارگذاری مجدد داده ها
700
  });
701
+ console.log("'Select All' checkbox change listener added."); // لاگ
702
  } else {
703
+ console.error("'Select All' checkbox element with ID 'select_all_books' not found."); // لاگ خطا
 
704
  }
705
 
706
  if (bookCheckboxes && bookCheckboxes.length > 0) {
707
+ Array.from(bookCheckboxes).forEach(cb => {
708
  cb.addEventListener('change', () => {
709
+ const allOthersChecked = Array.from(bookCheckboxes).every(cb => cb.checked);
710
+ if (selectAllCheckbox) {
 
711
  selectAllCheckbox.checked = allOthersChecked;
712
  }
713
  console.log("Individual book checkbox changed. Checking 'Select All' status."); // لاگ
 
714
  updateSelectedBooksData(); // <--- فراخوانی بارگذاری مجدد داده ها
715
  });
716
  });
717
+ console.log("Individual book checkboxes change listeners added."); // لاگ
718
  } else {
719
+ console.warn("No book checkboxes found with class 'book-checkbox'..."); // لاگ هشدار
 
 
720
  }
721
 
722
 
723
+ // Listener برای دکمه های کپی (به صورت Delegation)
 
724
  searchResultsContainer.addEventListener('click', (event) => {
 
725
  if (event.target && event.target.classList && event.target.classList.contains('copy-button')) {
726
+ event.preventDefault(); // جلوگیری از رفتار پیش فرض
 
727
 
 
728
  const resultItemElement = event.target.closest('.result-item');
729
  if (resultItemElement) {
730
+ // استخراج متن از المان های نمایش داده شده
 
 
731
  const passageText = resultItemElement.querySelector('.result-passage')?.textContent || '';
 
732
  const referenceText = resultItemElement.querySelector('.result-reference')?.textContent.replace('مرجع:', '').trim() || 'نامشخص';
733
  const bookTitleText = resultItemElement.querySelector('.result-book-title')?.textContent.replace('از کتاب:', '').trim() || 'نامشخص';
734
+ // گرفتن متن کامل عنصر شباهت (مثلا "شباهت: 0.7500")
735
+ const similarityElementText = resultItemElement.querySelector('.result-similarity')?.textContent || '';
736
+
737
 
738
+ // ساخت متنی که باید کپی شود
739
+ const textToCopy = `خاطره:\n${passageText}\n\nمرجع: ${referenceText}\nاز کتاب: ${bookTitleText}\n${similarityElementText}`;
740
 
741
 
742
  // کپی کردن متن به کلیپ بورد
743
  navigator.clipboard.writeText(textToCopy)
744
  .then(() => {
 
745
  event.target.textContent = 'کپی شد!';
746
  setTimeout(() => {
747
  event.target.textContent = 'کپی متن';
748
+ }, 2000);
749
+ console.log("Passage, reference, book title, and similarity copied."); // لاگ
750
  })
751
  .catch(err => {
752
+ console.error('Failed to copy text: ', err); // لاگ خطا
753
+ event.target.textContent = 'خطا در کپی';
754
  setTimeout(() => {
755
  event.target.textContent = 'کپی متن';
756
  }, 2000);
757
  });
758
  } else {
759
+ console.warn("Result item parent not found for copy button click."); // لاگ هشدار
760
  }
761
  }
762
  });
763
+ console.log("Copy button delegation click listener added."); // لاگ
764
 
765
 
766
  // ****** راه اندازی اولیه: بارگذاری داده ها هنگام بارگذاری صفحه ******
767
  // این تابع فرآیند بارگذاری داده از فایل های JSON بر اساس چک باکس های پیش فرض انتخاب شده در HTML را شروع می کند.
768
  updateSelectedBooksData(); // این فراخوانی در نهایت checkAndEnableSearchButton را صدا می زند.
769
+ console.log("Initial data loading process started."); // لاگ
770
 
771
 
772
  } else {
773
+ // اگر تمام عناصر مورد نیاز پیدا نشدند
774
  const errorMessage = "خطا در بارگذاری صفحه: برخی یا تمام عناصر لازم (HTML) پیدا نشدند. شناسه‌های HTML و نام کلاس‌ها را در index.html بررسی کنید و مطمئن شوید همه عناصر ضروری وجود دارند."; // پیام خطا برای کاربر
775
+ console.error(errorMessage, { // لاگ جزئیات خطا
776
+ searchButton: !!searchButton,
777
  userQuestionInput: !!userQuestionInput,
778
  searchResultsContainer: !!searchResultsContainer,
779
  loadingStatusElement: !!loadingStatusElement,
780
  selectionErrorElement: !!selectionErrorElement,
781
  selectAllCheckbox: !!selectAllCheckbox,
782
+ bookCheckboxesCount: bookCheckboxes ? bookCheckboxes.length : 0,
783
  resultsPerPageSelect: !!resultsPerPageSelect,
784
+ similarityThresholdInput: !!similarityThresholdInput,
785
+ loadingSpinnerElement: !!loadingSpinnerElement // بررسی وجود اسپینر
786
  });
787
+ if (searchResultsContainer) { // نمایش خطا روی ��فحه
788
  searchResultsContainer.innerHTML = `<p style=\"color: red;\">${errorMessage}</p>`;
789
  }
790
  // نمایش خطا در المان های وضعیت و خطای جداگانه
791
+ updateStatus("راه‌اندازی اولیه با خطا مواجه شد.", true, 'error');
792
  updateSelectionError(errorMessage);
793
 
794
+ // غیرفعال نگه داشتن دکمه جستجو
795
  if (searchButton) {
796
  setButtonEnabled(false);
797
  }
798
+ // نیازی به return نیست.
799
  }
800
  });
801
  // پایان بلوک DOMContentLoaded و پایان کامل فایل script.js