montaghem630 commited on
Commit
e2ce00f
·
verified ·
1 Parent(s): 6d6eb2e

Upload 10 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # استفاده از یک تصویر استاندارد پایتون به عنوان پایه
2
+ FROM python:3.9
3
+
4
+ # تنظیم دایرکتوری کاری داخل کانتینر
5
+ WORKDIR /app
6
+
7
+ # کپی کردن فایل requirements.txt و نصب وابستگی‌ها
8
+ # این کار به Docker اجازه می‌دهد تا مرحله نصب را کش کند اگر requirements.txt تغییر نکرده باشد
9
+ COPY requirements.txt .
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ # کپی کردن بقیه فایل‌های پروژه به داخل کانتینر در دایرکتوری کاری (/app)
13
+ COPY . /app
14
+
15
+ # دستوری که هنگام اجرای کانتینر اجرا می‌شود
16
+ # از Gunicorn برای اجرای برنامه Flask استفاده می‌کنیم
17
+ # 'server:app' یعنی برنامه Flask با نام 'app' در فایل 'server.py' قرار دارد
18
+ # --bind 0.0.0.0:7860 به Gunicorn می‌گوید روی تمام رابط‌ها و پورت 7860 گوش دهد
19
+ # Hugging Face Spaces معمولا انتظار دارد برنامه روی این پورت اجرا شود
20
+ CMD exec gunicorn --bind 0.0.0.0:7860 server:app
fonts/Vazirmatn-Bold.woff2 ADDED
Binary file (51 kB). View file
 
fonts/Vazirmatn-Regular.woff2 ADDED
Binary file (50.7 kB). View file
 
fonts/vazirmatn.css ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* این فایل شامل قوانین @font-face برای فونت Vazirmatn است */
2
+ /* باید در پوشه fonts کنار فایل های فونت قرار گیرد */
3
+
4
+ @font-face {
5
+ font-family: 'Vazirmatn';
6
+ src: url('Vazirmatn-Regular.woff2') format('woff2');
7
+ font-weight: 400; /* وزن نرمال */
8
+ font-style: normal;
9
+ font-display: swap; /* برای نمایش متن با فونت پیشفرض تا بارگذاری فونت اصلی */
10
+ }
11
+
12
+ @font-face {
13
+ font-family: 'Vazirmatn';
14
+ src: url('Vazirmatn-Bold.woff2') format('woff2');
15
+ font-weight: 700; /* وزن بولد */
16
+ font-style: normal;
17
+ font-display: swap; /* برای نمایش متن با فونت پیشفرض تا بارگذاری فونت اصلی */
18
+ }
19
+
20
+ /* می توانید @font-face برای وزن های دیگر را نیز اگر فایل هایش را دارید اضافه کنید */
21
+ /*
22
+ @font-face {
23
+ font-family: 'Vazirmatn';
24
+ src: url('Vazirmatn-ExtraLight.woff2') format('woff2');
25
+ font-weight: 200;
26
+ font-style: normal;
27
+ font-display: swap;
28
+ }
29
+
30
+ @font-face {
31
+ font-family: 'Vazirmatn';
32
+ src: url('Vazirmatn-Light.woff2') format('woff2');
33
+ font-weight: 300;
34
+ font-style: normal;
35
+ font-display: swap;
36
+ }
37
+
38
+ @font-face {
39
+ font-family: 'Vazirmatn';
40
+ src: url('Vazirmatn-Medium.woff2') format('woff2');
41
+ font-weight: 500;
42
+ font-style: normal;
43
+ font-display: swap;
44
+ }
45
+
46
+ @font-face {
47
+ font-family: 'Vazirmatn';
48
+ src: url('Vazirmatn-SemiBold.woff2') format('woff2');
49
+ font-weight: 600;
50
+ font-style: normal;
51
+ font-display: swap;
52
+ }
53
+
54
+ @font-face {
55
+ font-family: 'Vazirmatn';
56
+ src: url('Vazirmatn-ExtraBold.woff2') format('woff2');
57
+ font-weight: 800;
58
+ font-style: normal;
59
+ font-display: swap;
60
+ }
61
+
62
+ @font-face {
63
+ font-family: 'Vazirmatn';
64
+ src: url('Vazirmatn-Black.woff2') format('woff2');
65
+ font-weight: 900;
66
+ font-style: normal;
67
+ font-display: swap;
68
+ }
69
+ */
index.html ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>جستجوی معنایی خاطرات</title>
7
+ <link rel="stylesheet" href="fonts/vazirmatn.css">
8
+ <style>
9
+ body {
10
+ font-family: 'Vazirmatn', sans-serif;
11
+ direction: rtl;
12
+ text-align: right;
13
+ margin: 20px;
14
+ line-height: 1.6;
15
+ background-color: #f4f4f4;
16
+ color: #333;
17
+ }
18
+ .container {
19
+ max-width: 800px;
20
+ margin: auto;
21
+ background: #fff;
22
+ padding: 20px;
23
+ border-radius: 8px;
24
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
25
+ }
26
+ h1, h2, h3 {
27
+ text-align: center;
28
+ color: #555;
29
+ font-family: 'Vazirmatn', sans-serif;
30
+ }
31
+ .search-section, .results-section, .book-selection-section, .results-control-section {
32
+ margin-bottom: 20px;
33
+ padding: 15px;
34
+ border: 1px solid #ddd;
35
+ border-radius: 5px;
36
+ background-color: #f9f9f9;
37
+ }
38
+ .book-selection-section h3, .results-control-section h3 {
39
+ margin-top: 0;
40
+ border-bottom: 1px solid #eee;
41
+ padding-bottom: 10px;
42
+ margin-bottom: 10px;
43
+ font-family: 'Vazirmatn', sans-serif;
44
+ }
45
+ .book-list label {
46
+ display: block;
47
+ margin-bottom: 8px;
48
+ cursor: pointer;
49
+ font-family: 'Vazirmatn', sans-serif;
50
+ }
51
+ .book-list input[type="checkbox"] {
52
+ margin-left: 5px;
53
+ }
54
+ #select_all_books_label {
55
+ font-weight: bold;
56
+ }
57
+ #loadingStatus, #selectionError {
58
+ margin-top: 10px;
59
+ font-style: italic;
60
+ font-weight: bold;
61
+ color: #666;
62
+ font-family: 'Vazirmatn', sans-serif;
63
+ }
64
+ #selectionError {
65
+ color: red;
66
+ }
67
+ label {
68
+ display: block;
69
+ margin-bottom: 8px;
70
+ font-weight: bold;
71
+ font-family: 'Vazirmatn', sans-serif;
72
+ }
73
+ input[type="text"], button, select {
74
+ padding: 10px;
75
+ border: 1px solid #ccc;
76
+ border-radius: 4px;
77
+ font-size: 1rem;
78
+ box-sizing: border-box;
79
+ font-family: 'Vazirmatn', sans-serif !important;
80
+ vertical-align: middle;
81
+ }
82
+ input[type="text"] {
83
+ min-height: 40px;
84
+ }
85
+ button {
86
+ background-color: #5cb85c;
87
+ color: white;
88
+ border: none;
89
+ cursor: pointer;
90
+ }
91
+ button:hover {
92
+ background-color: #4cae4c;
93
+ }
94
+ button:disabled {
95
+ background-color: #d3d3d3;
96
+ cursor: not-allowed;
97
+ }
98
+ select {
99
+ padding: 8px;
100
+ font-size: 0.9rem;
101
+ }
102
+ .search-input-container {
103
+ display: flex;
104
+ align-items: center;
105
+ gap: 10px;
106
+ width: 100%;
107
+ box-sizing: border-box;
108
+ max-width: 700px;
109
+ }
110
+ .search-input-container input[type="text"] {
111
+ flex: 1 1 auto;
112
+ min-width: 200px;
113
+ max-width: 600px;
114
+ margin: 0;
115
+ box-sizing: border-box;
116
+ }
117
+ .search-input-container button {
118
+ flex: 0 0 auto;
119
+ width: 90px;
120
+ margin: 0;
121
+ box-sizing: border-box;
122
+ height: 40px;
123
+ }
124
+ .results-control-section label {
125
+ display: inline-block;
126
+ margin-left: 10px;
127
+ font-weight: normal;
128
+ font-family: 'Vazirmatn', sans-serif;
129
+ }
130
+ .result-item {
131
+ border-bottom: 1px solid #eee;
132
+ padding: 10px 0;
133
+ margin-bottom: 10px;
134
+ }
135
+ .result-item:last-child {
136
+ border-bottom: none;
137
+ margin-bottom: 0;
138
+ }
139
+ .result-reference {
140
+ font-size: 0.9em;
141
+ color: #555;
142
+ margin-bottom: 5px;
143
+ font-family: 'Vazirmatn', sans-serif;
144
+ }
145
+ .result-book-title {
146
+ font-size: 0.8em;
147
+ color: #777;
148
+ margin-bottom: 5px;
149
+ font-style: italic;
150
+ font-family: 'Vazirmatn', sans-serif;
151
+ }
152
+ .result-passage {
153
+ margin-bottom: 5px;
154
+ font-family: 'Vazirmatn', sans-serif;
155
+ }
156
+ .result-similarity {
157
+ font-size: 0.8em;
158
+ color: #007bff;
159
+ text-align: left;
160
+ font-family: 'Vazirmatn', sans-serif;
161
+ }
162
+ </style>
163
+ </head>
164
+ <body>
165
+ <div class="container">
166
+ <h1>جستجوی معنایی خاطرات دوران پهلوی</h1>
167
+
168
+ <div class="book-selection-section">
169
+ <h3>انتخاب کتاب‌ها برای جستجو:</h3>
170
+ <div class="book-list">
171
+ <label id="select_all_books_label">
172
+ <input type="checkbox" id="select_all_books">
173
+ انتخاب همه موارد
174
+ </label>
175
+ <label>
176
+ <input type="checkbox" class="book-checkbox" id="jabe_siah_checkbox" value="jabe_siah.json" checked>
177
+ جعبه سیاه (منتخب خاطرات اسدالله علم)
178
+ </label>
179
+ </div>
180
+ <p id="loadingStatus"></p>
181
+ <p id="selectionError"></p>
182
+ </div>
183
+
184
+ <div class="search-section">
185
+ <label for="userQuestion">سوال شما:</label>
186
+ <div class="search-input-container">
187
+ <input type="text" id="userQuestion" placeholder="عبارت مورد نظر برای جستجو را وارد کنید...">
188
+ <button id="searchButton" disabled>جستجو</button>
189
+ </div>
190
+ </div>
191
+
192
+ <div class="results-control-section">
193
+ <h3>کنترل نمایش نتایج:</h3>
194
+ <label for="resultsPerPage">تعداد نتایج برتر برای نمایش:</label>
195
+ <select id="resultsPerPage">
196
+ <option value="10">10</option>
197
+ <option value="20">20</option>
198
+ <option value="30">30</option>
199
+ <option value="50">50</option>
200
+ <option value="100">100</option>
201
+ </select>
202
+ </div>
203
+
204
+ <div class="results-section">
205
+ <h2>نتایج جستجو:</h2>
206
+ <div id="searchResults">
207
+ <p>پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.</p>
208
+ </div>
209
+ </div>
210
+ </div>
211
+
212
+ <script src="script.js"></script>
213
+ </body>
214
+ </html>
jabe_siah.json ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ flask
2
+ flask-cors
3
+ sentence-transformers
4
+ gunicorn
script.js ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // این کد، فایل script.js به‌روزرسانی شده نهایی است که دکمه جستجو را زمانی فعال می‌کند که هم داده‌ها بارگذاری شده باشند و هم متنی در کادر جستجو وارد شده باشد.
2
+
3
+ // ****** تعریف متغیرهای عناصر HTML در بالاترین اسکوپ ******
4
+ let searchButton;
5
+ let questionInput;
6
+ let resultsDiv;
7
+ let loadingStatusParagraph;
8
+ let selectionErrorParagraph;
9
+ let selectAllCheckbox;
10
+ let bookCheckboxes; // NodeList از تمام چک باکس های کتاب ها (به جز انتخاب همه)
11
+ let resultsPerPageSelect; // المان select برای تعداد نتایج
12
+
13
+ // ****** تعریف URL سرور پایتون برای دریافت Embedding سوال ******
14
+ const EMBEDDING_SERVER_URL = 'https://montaghem630-khatere-khan.hf.space/get_embedding';
15
+
16
+ // ****** متغیر برای نگهداری داده‌های ترکیب شده از کتاب‌های انتخاب شده ******
17
+ let memoirsWithEmbeddings = [];
18
+
19
+ // ****** نگاشت نام فایل JSON به نام کامل کتاب (برای نمایش در نتایج) ******
20
+ // این لیست باید با مقادیر value چک باکس ها و نام های نمایشی در HTML مطابقت داشته باشد
21
+ const bookInfo = {
22
+ 'jabe_siah.json': 'جعبه سیاه (منتخب خاطرات اسدالله علم)',
23
+ // اگر کتاب های دیگری دارید، اینجا اضافه کنید
24
+ // 'ketab_dovom.json': 'نام کتاب دوم',
25
+ // 'ketab_sevom.json': 'نام کتاب سوم',
26
+ };
27
+ // *****************************************************************
28
+
29
+
30
+ // تابع کمکی برای نمایش پیام وضعیت بارگذاری/پردازش
31
+ function updateStatus(message, isError = false) {
32
+ if (loadingStatusParagraph) {
33
+ loadingStatusParagraph.textContent = message;
34
+ loadingStatusParagraph.style.color = isError ? 'red' : '#666';
35
+ } else {
36
+ console.log("Status:", message); // لاگ برای توسعه
37
+ }
38
+ }
39
+
40
+ // تابع کمکی برای نمایش خطای انتخاب کتاب
41
+ function updateSelectionError(message) {
42
+ if (selectionErrorParagraph) {
43
+ selectionErrorParagraph.textContent = message;
44
+ selectionErrorParagraph.style.color = 'red';
45
+ } else {
46
+ console.error("Selection Error:", message); // لاگ برای توسعه
47
+ }
48
+ }
49
+
50
+ // تابع کمکی برای فعال/غیرفعال کردن دکمه جستجو
51
+ function setButtonEnabled(enabled) {
52
+ if (searchButton) {
53
+ searchButton.disabled = !enabled;
54
+ }
55
+ }
56
+
57
+ // ****** تابع جدید برای بررسی وضعیت و فعال/غیرفعال کردن دکمه جستجو ******
58
+ // این تابع بررسی می کند که آیا داده ها بارگذاری شده اند و آیا کادر سوال خالی نیست
59
+ function checkAndEnableSearchButton() {
60
+ const isDataLoaded = memoirsWithEmbeddings.length > 0;
61
+ const isQueryNotEmpty = questionInput && questionInput.value.trim() !== ''; // چک کردن وجود questionInput قبل از دسترسی به value
62
+
63
+ // دکمه فقط زمانی فعال می شود که هم داده بارگذاری شده باشد و هم متن سوال خالی نباشد
64
+ setButtonEnabled(isDataLoaded && isQueryNotEmpty);
65
+
66
+ console.log(`Check Button State: Data Loaded = ${isDataLoaded}, Query Not Empty = ${isQueryNotEmpty}, Button Enabled = ${isDataLoaded && isQueryNotEmpty}`); // لاگ برای توسعه
67
+ }
68
+
69
+
70
+ // ****** تابع اصلی برای بارگذاری داده‌ها از فایل‌های JSON کتاب‌های انتخاب شده ******
71
+ // این تابع هر زمان که انتخاب کتاب ها تغییر می کند، داده ها را بارگذاری مجدد می کند
72
+ async function updateSelectedBooksData() {
73
+ console.log("Updating selected books data..."); // لاگ برای توسعه
74
+ updateStatus("در حال بارگذاری داده‌ها..."); // پیام برای کاربر
75
+ updateSelectionError(""); // پاک کردن پیام خطای قبلی
76
+ // setButtonEnabled(false); // غیرفعال کردن دکمه جستجو حین بارگذاری (توسط checkAndEnableSearchButton انجام می شود)
77
+ checkAndEnableSearchButton(); // در ابتدای بارگذاری، دکمه غیرفعال خواهد شد اگر هنوز متن نیست یا داده خالی می شود
78
+
79
+
80
+ // پیدا کردن چک باکس های کتاب ها که انتخاب شده اند
81
+ const selectedBookFiles = Array.from(bookCheckboxes)
82
+ .filter(checkbox => checkbox.checked)
83
+ .map(checkbox => checkbox.value); // value چک باکس ها نام فایل JSON است
84
+
85
+ console.log("Selected book files:", selectedBookFiles); // لاگ برای توسعه
86
+
87
+ // ****** چک کردن اینکه حداقل یک کتاب انتخاب شده باشد ******
88
+ if (selectedBookFiles.length === 0) {
89
+ updateStatus(""); // پاک کردن پیام وضعیت
90
+ updateSelectionError("لطفاً حداقل یک کتاب برای جستجو انتخاب کنید."); // پیام خطا برای کاربر
91
+ console.warn("No books selected. Cannot load data."); // لاگ برای توسعه
92
+ memoirsWithEmbeddings = []; // پاک کردن داده های قبلی
93
+ resultsDiv.innerHTML = '<p>پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.</p>'; // بازگرداندن پیام اولیه
94
+ checkAndEnableSearchButton(); // مطمئن می شویم دکمه غیرفعال شود
95
+ return; // توقف فرآیند اگر هیچ کتابی انتخاب نشده است
96
+ }
97
+ // ********************************************************
98
+
99
+ memoirsWithEmbeddings = []; // پاک کردن داده‌های قبلی قبل از بارگذاری جدید
100
+
101
+ try {
102
+ // بارگذاری همزمان تمام فایل‌های JSON انتخاب شده
103
+ const fetchPromises = selectedBookFiles.map(filename => {
104
+ const filePath = `./${filename}`;
105
+ console.log(`Attempting to fetch: ${filePath}`); // لاگ برای توسعه
106
+ return fetch(filePath).then(response => {
107
+ if (!response.ok) {
108
+ throw new Error(`Error fetching file: ${filename} (Status: ${response.status})`);
109
+ }
110
+ return response.json();
111
+ })
112
+ .catch(error => {
113
+ console.error(`Failed to fetch or parse file ${filename}:`, error); // لاگ خطا برای توسعه
114
+ throw new Error(`Failed to load data for book file "${filename}".`);
115
+ });
116
+ });
117
+
118
+
119
+ const booksData = await Promise.all(fetchPromises); // انتظار برای دانلود و تجزیه همه فایل ها
120
+
121
+ // ترکیب داده‌ها از تمام فایل‌های JSON بارگذاری شده
122
+ booksData.forEach(data => {
123
+ if (Array.isArray(data)) {
124
+ memoirsWithEmbeddings = memoirsWithEmbeddings.concat(data);
125
+ } else {
126
+ console.error("Fetched data is not an array:", data); // لاگ برای توسعه
127
+ }
128
+ });
129
+
130
+ const loadedBooksCount = selectedBookFiles.length;
131
+ const totalPassagesLoaded = memoirsWithEmbeddings.length;
132
+
133
+ console.log(`Successfully loaded data from ${loadedBooksCount} book(s). Total passages loaded: ${totalPassagesLoaded}`); // لاگ برای توسعه
134
+ updateStatus(`داده‌ها از ${loadedBooksCount} کتاب با موفقیت بارگذاری شد. مجموع خاطرات: ${totalPassagesLoaded}. آماده جستجو هستید.`); // پیام برای کاربر
135
+ // setButtonEnabled(true); // فعال کردن دکمه جستجو پس از بارگذاری موفقیت آمیز داده (توسط checkAndEnableSearchButton انجام می شود)
136
+ checkAndEnableSearchButton(); // بررسی و فعال کردن دکمه پس از بارگذاری داده
137
+ // نتایج قبلی را پاک نمی کنیم، فقط پیام اولیه را پاک میکنیم اگر هنوز نمایش داده می شود
138
+ if (resultsDiv && resultsDiv.innerHTML === '<p>پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.</p>') {
139
+ resultsDiv.innerHTML = '';
140
+ }
141
+
142
+
143
+ } catch (error) {
144
+ console.error("Error loading selected books data:", error); // لاگ برای توسعه
145
+ updateStatus("خطا در بارگذاری داده‌ها.", true); // پیام برای کاربر
146
+ updateSelectionError(`خطا در بارگذاری داده از کتاب‌های انتخاب شده: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر.`); // پیام خطا برای کاربر
147
+ memoirsWithEmbeddings = []; // اطمینان از خالی بودن داده در صورت خطا
148
+ // setButtonEnabled(false); // غیرفعال نگه داشتن دکمه جستجو (توسط checkAndEnableSearchButton انجام می شود)
149
+ checkAndEnableSearchButton(); // مطمئن می شویم دکمه غیرفعال بماند
150
+ resultsDiv.innerHTML = '<p>پس از انتخاب کتاب‌ها و وارد کردن سوال، نتایج اینجا نمایش داده می‌شوند.</p>'; // بازگرداندن پیام اولیه
151
+ }
152
+ }
153
+
154
+
155
+ // تابع کمکی برای محاسبه شباهت کسینوسی بین دو بردار (بدون تغییر)
156
+ function cosineSimilarity(vecA, vecB) {
157
+ if (!vecA || !vecB || vecA.length !== vecB.length || vecA.length === 0) {
158
+ console.error("Cosine Similarity Error: Invalid vectors.", {vecA_length: vecA ? vecA.length : 'null', vecB_length: vecB ? vecB.length : 'null'});
159
+ return 0;
160
+ }
161
+
162
+ let dotProduct = 0;
163
+ let magnitudeA = 0;
164
+ let magnitudeB = 0;
165
+
166
+ for (let i = 0; i < vecA.length; i++) {
167
+ dotProduct += vecA[i] * vecB[i];
168
+ magnitudeA += vecA[i] * vecA[i];
169
+ magnitudeB += vecB[i] * vecB[i];
170
+ }
171
+
172
+ magnitudeA = Math.sqrt(magnitudeA);
173
+ magnitudeB = Math.sqrt(magnitudeB);
174
+
175
+ if (magnitudeA === 0 || magnitudeB === 0) {
176
+ return 0;
177
+ }
178
+
179
+ const similarity = dotProduct / (magnitudeA * magnitudeB);
180
+ return similarity;
181
+ }
182
+
183
+ // تابع کمکی برای حذف بخش کلمات کلیدی از متن Passage (همانند قبل)
184
+ function cleanPassageTextForDisplay(passage) {
185
+ const startDelimiter = ' <کلیدواژه ها: ';
186
+ const startIndex = passage.indexOf(startDelimiter);
187
+
188
+ if (startIndex === -1) {
189
+ return passage;
190
+ }
191
+
192
+ let cleanText = passage.substring(0, startIndex);
193
+ return cleanText.trim();
194
+ }
195
+
196
+
197
+ // ****** تابع اصلی جستجو که هنگام کلیک دکمه یا فشردن Enter اجرا میشود ******
198
+ async function searchMemoirs() {
199
+ console.log("Search triggered - executing search"); // لاگ برای توسعه (مشخص نیست دکمه یا Enter)
200
+ console.log(`Data loaded state (passages count): ${memoirsWithEmbeddings.length}`); // لاگ برای توسعه
201
+
202
+ // ****** چک کردن اینکه داده ها (از کتاب های انتخاب شده) بارگذاری شده باشند ******
203
+ // این چک در checkAndEnableSearchButton هم هست، اما اینجا برای اطمینان بیشتر است
204
+ if (memoirsWithEmbeddings.length === 0) {
205
+ console.warn("No memoir data loaded. Cannot search."); // لاگ برای توسعه
206
+ updateSelectionError("لطفاً ابتدا کتاب‌های مورد نظر برای جستجو را انتخاب کرده و منتظر بارگذاری داده‌ها بمانید."); // پیام خطا برای کاربر
207
+ return;
208
+ }
209
+ // **************************************************************************
210
+
211
+ const query = questionInput.value.trim();
212
+ console.log(`Query text is: "${query}"`); // لاگ برای توسعه
213
+
214
+ if (!query) {
215
+ if (resultsDiv) {
216
+ resultsDiv.innerHTML = `<p>لطفاً عبارت مورد نظر برای جستجو را وارد کنید.</p>`; // پیام برای کاربر
217
+ }
218
+ console.warn("Search query is empty."); // لاگ برای توسعه
219
+ return;
220
+ }
221
+
222
+ updateStatus("در حال جستجو..."); // به‌روزرسانی پیام وضعیت به جستجو برای کاربر
223
+ resultsDiv.innerHTML = ''; // پاک کردن نتایج قبلی یا پیام اولیه
224
+
225
+ try {
226
+ console.log("Requesting query embedding from Python server..."); // لاگ برای توسعه
227
+
228
+ const serverResponse = await fetch(EMBEDDING_SERVER_URL, {
229
+ method: 'POST',
230
+ headers: {
231
+ 'Content-Type': 'application/json'
232
+ },
233
+ body: JSON.stringify({ query: query })
234
+ });
235
+
236
+ if (!serverResponse.ok) {
237
+ const errorBody = await serverResponse.text(); // بخوان به صورت متن برای اطلاعات بیشتر
238
+ console.error(`Server responded with status ${serverResponse.status}: ${errorBody}`); // لاگ خطا برای توسعه
239
+ throw new Error(`خطا از سرور (${serverResponse.status}). جزئیات بیشتر در کنسول مرورگر.`); // پیام خطا برای کاربر
240
+ }
241
+
242
+ const serverData = await serverResponse.json();
243
+ const queryEmbeddingArray = serverData.embedding;
244
+
245
+ if (!queryEmbeddingArray || !Array.isArray(queryEmbeddingArray) || queryEmbeddingArray.length === 0) {
246
+ console.error("Server returned an invalid or empty embedding:", serverData); // لاگ خطا برای توسعه
247
+ throw new Error("سرور بردار جستجو را به درستی برنگرداند. جزئیات در کنسول مرورگر."); // پیام خطا برای کاربر
248
+ }
249
+
250
+ console.log("Query embedding received from server successfully."); // لاگ برای توسعه
251
+ console.log("Calculating similarities in browser..."); // لاگ برای توسعه
252
+
253
+ const searchResults = [];
254
+ // محاسبه شباهت با تمام قطعات خاطره ای که از کتاب های انتخاب شده بارگذاری شده اند
255
+ for (const memoir of memoirsWithEmbeddings) {
256
+ // اطمینان از وجود و صحت بردار embedding در آیتم خاطره
257
+ if (memoir.embedding && Array.isArray(memoir.embedding) && memoir.embedding.length === queryEmbeddingArray.length) {
258
+ const similarity = cosineSimilarity(queryEmbeddingArray, memoir.embedding);
259
+ // اضافه کردن تمام فیلدهای اصلی خاطره و امتیاز شباهت به نتیجه
260
+ searchResults.push({ ...memoir, similarity: similarity });
261
+ } else {
262
+ // هشدار برای آیتم های بدون بردار یا با ابعاد نامعتبر (فقط در کنسول)
263
+ console.warn(`Skipping memoir due to missing or invalid embedding: ${memoir.book_title || 'Unknown Book'} - ${memoir.reference || 'Unknown Reference'}`);
264
+ }
265
+ }
266
+ console.log(`Similarity calculation complete. Found ${searchResults.length} results with valid embeddings.`); // لاگ برای توسعه
267
+
268
+ console.log("Sorting results by similarity..."); // لاگ برای توسعه
269
+ searchResults.sort((a, b) => b.similarity - a.similarity);
270
+ console.log("Results sorted."); // لاگ برای توسعه
271
+
272
+ // ****** انتخاب تعداد نتایج برتر بر اساس انتخاب کاربر ******
273
+ const resultsPerPage = parseInt(resultsPerPageSelect.value, 10); // خواندن مقدار انتخاب شده و تبدیل به عدد صحیح
274
+ const topResults = searchResults.slice(0, resultsPerPage); // انتخاب فقط N نتیجه برتر
275
+ console.log(`Displaying top ${topResults.length} results based on user selection.`); // لاگ برای توسعه
276
+ // ***********************************************************
277
+
278
+
279
+ // ****** منطق نمایش نتایج ******
280
+ if (resultsDiv) {
281
+ // نتایج قبلی را پاک کرده ایم
282
+
283
+ if (topResults.length === 0) { // اگر هیچ نتیجه ای یافت نشد
284
+ resultsDiv.innerHTML = `<p>نتیجه مرتبطی یافت نشد.</p>`; // پیام برای کاربر
285
+ console.log("No relevant results found."); // لاگ برای توسعه
286
+ } else { // اگر نتایجی یافت شد
287
+ console.log("Results found, updating DOM."); // لاگ برای توسعه
288
+
289
+ const resultsList = document.createElement('div');
290
+ resultsList.classList.add('results-list');
291
+
292
+
293
+ topResults.forEach(result => {
294
+ const resultItem = document.createElement('div');
295
+ resultItem.classList.add('result-item');
296
+
297
+ // حذف نمایش امتیاز شباهت
298
+ // const similarityElement = document.createElement('p');
299
+ // similarityElement.classList.add('result-similarity');
300
+ // similarityElement.textContent = `شباهت: ${result.similarity.toFixed(4)}`;
301
+
302
+ // نمایش نام کتاب
303
+ const bookTitleElement = document.createElement('p');
304
+ bookTitleElement.classList.add('result-book-title');
305
+ bookTitleElement.textContent = `از کتاب: ${result.book_title || 'نامشخص'}`;
306
+
307
+ // نمایش مرجع خاطره
308
+ const referenceElement = document.createElement('p');
309
+ referenceElement.classList.add('result-reference');
310
+ referenceElement.innerHTML = `<strong>مرجع:</strong> ${result.reference || 'نامشخص'}`;
311
+
312
+
313
+ // نمایش متن خاطره (با حذف کلمات کلیدی)
314
+ const passageElement = document.createElement('p');
315
+ passageElement.classList.add('result-passage');
316
+ passageElement.textContent = cleanPassageTextForDisplay(result.passage || '');
317
+
318
+
319
+ // اضافه کردن عناصر به آیتم نتیجه با ترتیب جدید (متن -> مرجع -> کتاب)
320
+ resultItem.appendChild(passageElement);
321
+ resultItem.appendChild(referenceElement);
322
+ resultItem.appendChild(bookTitleElement);
323
+
324
+
325
+ resultsList.appendChild(resultItem);
326
+ });
327
+
328
+ resultsDiv.appendChild(resultsList);
329
+ console.log("DOM updated with results."); // لاگ برای توسعه
330
+
331
+ // لاگ کردن نتایج برای توسعه
332
+ console.log(`Top ${topResults.length} results displayed (reference, book, and similarity):`);
333
+ topResults.forEach(result => {
334
+ console.log(` Book: ${result.book_title || 'Unknown'}, Ref: ${result.reference || 'N/A'}, Sim: ${result.similarity.toFixed(4)}`); // امتیاز را در لاگ نگه می داریم
335
+ });
336
+ }
337
+ updateStatus(`جستجو به پایان رسید. ${topResults.length} نتیجه برتر نمایش داده شد.`); // به‌روزرسانی پیام وضعیت پس از جستجو
338
+
339
+ } else {
340
+ console.error("Could not find resultsDiv to display results."); // لاگ برای توسعه
341
+ updateStatus("جستجو با خطا مواجه شد.", true); // پیام برای کاربر
342
+ }
343
+
344
+
345
+ } catch (error) {
346
+ console.error("Error during search:", error); // لاگ برای توسعه
347
+ if (resultsDiv) {
348
+ // نمایش پیام خطای عمومی به کاربر
349
+ resultsDiv.innerHTML = `<p style="color: red;">هنگام جستجو خطایی رخ داد: ${error.message || 'خطای نامشخص'}. جزئیات بیشتر در کنسول مرورگر موجود است.</p>`;
350
+ }
351
+ updateStatus("جستجو با خطا مواجه شد.", true); // پیام برای کاربر
352
+ } finally {
353
+ // در نهایت (چه موفقیت آمیز چه با خطا)، دکمه را دوباره بررسی و تنظیم وضعیت می کنیم
354
+ checkAndEnableSearchButton();
355
+ }
356
+ }
357
+
358
+
359
+ // ****** Event Listeners و مقداردهی اولیه در زمان بارگذاری صفحه ******
360
+ document.addEventListener('DOMContentLoaded', () => {
361
+ console.log("DOMContentLoaded fired. Attaching event listeners and initializing."); // لاگ برای توسعه
362
+
363
+ // پیدا کردن عناصر HTML با ID یا Class
364
+ searchButton = document.getElementById('searchButton');
365
+ questionInput = document.getElementById('userQuestion');
366
+ resultsDiv = document.getElementById('searchResults');
367
+ loadingStatusParagraph = document.getElementById('loadingStatus');
368
+ selectionErrorParagraph = document.getElementById('selectionError');
369
+ selectAllCheckbox = document.getElementById('select_all_books');
370
+ bookCheckboxes = document.querySelectorAll('.book-checkbox'); // انتخاب تمام چک باکس های با کلاس .book-checkbox
371
+ resultsPerPageSelect = document.getElementById('resultsPerPage'); // پیدا کردن المان select
372
+
373
+ // لاگ برای تأیید پیدا شدن عناصر HTML (برای توسعه)
374
+ console.log("Search Button found:", !!searchButton);
375
+ console.log("Question Input found:", !!questionInput);
376
+ console.log("Results Div found:", !!resultsDiv);
377
+ console.log("Loading Status found:", !!loadingStatusParagraph);
378
+ console.log("Selection Error found:", !!selectionErrorParagraph);
379
+ console.log("Select All Checkbox found:", !!selectAllCheckbox);
380
+ console.log(`Book Checkboxes found: ${bookCheckboxes.length}`);
381
+ console.log("Results Per Page Select found:", !!resultsPerPageSelect);
382
+
383
+
384
+ // اگر تمام عناصر مورد نیاز پیدا شدند، Event Listeners را اضافه کرده و مقداردهی اولیه را انجام دهید
385
+ if (searchButton && questionInput && resultsDiv && loadingStatusParagraph && selectionErrorParagraph && selectAllCheckbox && bookCheckboxes.length > 0 && resultsPerPageSelect) {
386
+
387
+ // ****** اضافه کردن Event Listener به دکمه جستجو ******
388
+ searchButton.addEventListener('click', searchMemoirs);
389
+ console.log("Search button event listener attached."); // لاگ برای توسعه
390
+
391
+ // ****** اضافه کردن Event Listener برای تغییر متن در کادر سوال ******
392
+ // از event 'input' استفاده می کنیم که با هر تغییری (تایپ، پیست و...) اجرا می شود
393
+ questionInput.addEventListener('input', () => {
394
+ checkAndEnableSearchButton(); // هر بار که متن تغییر کرد، وضعیت دکمه را بررسی کن
395
+ });
396
+ // Event listener برای keypress (Enter) برای اجرای جستجو باقی می ماند
397
+ questionInput.addEventListener('keypress', (event) => {
398
+ if (event.key === 'Enter' || event.keyCode === 13) {
399
+ event.preventDefault();
400
+ // فقط در صورتی جستجو را اجرا کن که دکمه فعال است (یعنی داده بارگذاری شده و متن خالی نیست)
401
+ if (!searchButton.disabled) {
402
+ searchMemoirs();
403
+ } else {
404
+ console.warn("Attempted to search with Enter, but button is disabled (data not loaded or query empty)."); // لاگ برای توسعه
405
+ }
406
+ }
407
+ });
408
+ console.log("Input change and keypress event listeners attached to question input."); // لاگ برای توسعه
409
+
410
+
411
+ // ****** منطق چک باکس 'انتخاب همه' ******
412
+ selectAllCheckbox.addEventListener('change', () => {
413
+ const isChecked = selectAllCheckbox.checked;
414
+ bookCheckboxes.forEach(checkbox => {
415
+ checkbox.checked = isChecked;
416
+ });
417
+ // بارگذاری مجد�� داده ها پس از تغییر انتخاب همه (با تاخیر کم برای جلوگیری از فشردگی)
418
+ setTimeout(updateSelectedBooksData, 50); // تاخیر 50 میلی ثانیه
419
+ });
420
+
421
+
422
+ // ****** اضافه کردن Event Listeners به چک باکس های کتاب ها ******
423
+ bookCheckboxes.forEach(checkbox => {
424
+ checkbox.addEventListener('change', () => {
425
+ // اگر یکی از چک باکس های کتاب از حالت انتخاب خارج شد، 'انتخاب همه' را هم از حالت انتخاب خارج کن
426
+ if (!checkbox.checked) {
427
+ selectAllCheckbox.checked = false;
428
+ }
429
+ // اگر تمام چک باکس های کتاب انتخاب شدند، 'انتخاب همه' را هم انتخاب کن
430
+ else {
431
+ const allBooksSelected = Array.from(bookCheckboxes).every(cb => cb.checked);
432
+ if (allBooksSelected) {
433
+ selectAllCheckbox.checked = true;
434
+ }
435
+ }
436
+ // بارگذاری مجدد داده ها پس از تغییر انتخاب کتاب (با تاخیر کم)
437
+ setTimeout(updateSelectedBooksData, 50); // تاخیر 50 میلی ثانیه
438
+ });
439
+ });
440
+
441
+ // ****** اضافه کردن Event Listener به انتخابگر تعداد نتایج ******
442
+ resultsPerPageSelect.addEventListener('change', () => {
443
+ console.log("Results per page changed to:", resultsPerPageSelect.value); // لاگ برای توسعه
444
+ });
445
+
446
+
447
+ // ****** بارگذاری اولیه داده ها بر اساس انتخاب های پیش فرض هنگام بارگذاری صفحه ******
448
+ // این تابع بر اساس چک باکس های پیش فرض (که در HTML تیک خورده اند) داده ها را بارگذاری می کند
449
+ updateSelectedBooksData(); // این فراخوانی در نهایت checkAndEnableSearchButton را صدا می زند
450
+
451
+ } else {
452
+ // اگر عناصر مورد نیاز پیدا نشدند، پیام خطا در کنسول و روی صفحه نمایش داده میشود
453
+ const errorMessage = "خطا: عناصر لازم صفحه پیدا نشدند. شناسه‌های HTML و نام کلاس‌ها را در index.html بررسی کنید."; // پیام خطا برای کاربر
454
+ console.error(errorMessage); // لاگ برای توسعه
455
+ if (resultsDiv) {
456
+ resultsDiv.innerHTML = `<p style="color: red;">${errorMessage}</p>`;
457
+ }
458
+ updateStatus("راه‌اندازی اولیه با خطا مواجه شد.", true); // پیام برای کاربر
459
+ }
460
+ });
server.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # این کد، برنامه سرور پایتون شما برای تولید Embedding سوال با قابلیت CORS است.
2
+ # از فریمورک Flask و افزونه Flask-CORS استفاده شده است.
3
+
4
+ from flask import Flask, request, jsonify
5
+ from flask_cors import CORS # ایمپورت کردن افزونه CORS
6
+ from sentence_transformers import SentenceTransformer
7
+ import numpy as np
8
+ import logging
9
+
10
+ # تنظیمات اولیه لاگینگ برای مشاهده پیام ها در کنسول سرور
11
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
12
+
13
+ app = Flask(__name__)
14
+ CORS(app, origins=["https://montaghem6630-khatere-khan.hf.space"])
15
+ # اگر می خواهید همچنان به صورت محلی تست کنید، می توانید هر دو آدرس را در لیست نگه دارید:
16
+ # CORS(app, origins=["http://localhost:8000", "https://montaghem6630-khatere-khan.hf.space"])
17
+
18
+ # ****** نام مدل Sentence Transformer که برای بردارسازی سوال استفاده می‌شود ******
19
+ # این مدل باید همان مدلی باشد که برای تولید Embeddings JSON استفاده شده است
20
+ # مدل PartAI/Tooka-SBERT با ابعاد 1024 - بهترین مدل در تست ها
21
+ model_name = 'PartAI/Tooka-SBERT'
22
+ # ******************************************************************************
23
+
24
+ model = None # متغیری برای نگهداری مدل بارگذاری شده
25
+
26
+ # تابع برای بارگذاری مدل هوش مصنوعی هنگام شروع سرور
27
+ def load_model(model_name):
28
+ global model
29
+ if model is None:
30
+ logging.info(f"Loading Sentence Transformer model: {model_name} for server...")
31
+ try:
32
+ # بارگذاری مدل
33
+ # ممکن است نیاز به تعیین device='cpu' باشد اگر به صورت خودکار شناسایی نشود
34
+ # import torch # اگر از torch استفاده میکنید
35
+ # device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
36
+ # model = SentenceTransformer(model_name, device=device)
37
+ model = SentenceTransformer(model_name) # بارگذاری با تشخیص خودکار دستگاه
38
+ logging.info("Model loaded successfully on the server.")
39
+ except Exception as e:
40
+ logging.error(f"Error loading model on the server: {e}")
41
+ model = None # در صورت خطا مدل را None نگه میداریم
42
+
43
+ # مسیر API برای دریافت سوال و ارسال بردار آن
44
+ @app.route('/get_embedding', methods=['POST']) # مسیر دریافت بردار سوال
45
+ def get_embedding():
46
+ # چک میکنیم که آیا مدل با موفقیت بارگذاری شده است یا خیر
47
+ if model is None:
48
+ logging.error("Attempted to process request but AI model is not loaded.")
49
+ return jsonify({"error": "AI model not loaded on server."}), 500
50
+
51
+ # چک میکنیم که درخواست دریافتی شامل داده JSON و فیلد 'query' باشد
52
+ if not request.json or 'query' not in request.json:
53
+ logging.warning("Received invalid request: Missing JSON or 'query' field.")
54
+ return jsonify({"error": "Invalid request. Please send JSON with a 'query' field."}), 400
55
+
56
+ query = request.json['query']
57
+ logging.info(f"Received query for embedding: '{query}'")
58
+
59
+ try:
60
+ # تولید بردار معنایی برای سوال دریافتی
61
+ # pooling='mean' و normalize=True باید با آنچه در تولید Embeddings JSON استفاده شد یکسان باشد
62
+ query_embedding = model.encode(query, convert_to_numpy=True, pooling_mode='mean', normalize=True)
63
+
64
+ # تبدیل بردار numpy به لیست پایتون برای ارسال در پاسخ JSON
65
+ query_embedding_list = query_embedding.tolist()
66
+
67
+ logging.info("Query embedding generated successfully.")
68
+ # ارسال بردار تولید شده به عنوان پاسخ JSON به مرورگر
69
+ return jsonify({"embedding": query_embedding_list})
70
+
71
+ except Exception as e:
72
+ logging.error(f"Error generating embedding on server: {e}", exc_info=True) # لاگ کامل خطا
73
+ return jsonify({"error": "Error generating embedding."}), 500
74
+
75
+ # تابع اصلی برای اجرای سرور
76
+ if __name__ == '__main__':
77
+ # بارگذاری مدل هنگام اجرای اسکریپت سرور
78
+ load_model(model_name)
79
+ # اجرای سرور Flask
80
+ # debug=True فقط برای توسعه - برای محیط نهایی باید False شود
81
+ # host='0.0.0.0' برای دسترسی از شبکه محلی
82
+ # port=5000 پورتی که سرور روی آن گوش میدهد
83
+ # threaded=True می تواند به مدیریت درخواست های همزمان کمک کند (برای توسعه کافی است)
84
+ app.run(debug=True, port=5000, host='0.0.0.0', threaded=True)
style.css ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* تعریف فونت وزیرمتن Regular */
2
+ @font-face {
3
+ font-family: 'Vazirmatn'; /* نامی که برای این فونت در CSS استفاده میکنیم */
4
+ src: url('./fonts/Vazirmatn-Regular.woff2') format('woff2'); /* مسیر فایل فونت و فرمت آن نسبت به فایل CSS */
5
+ font-weight: normal; /* این تعریف برای وزن معمولی فونت است */
6
+ font-style: normal; /* سبک عادی (مورب نیست) */
7
+ }
8
+
9
+ /* تعریف فونت وزیرمتن Bold */
10
+ @font-face {
11
+ font-family: 'Vazirmatn'; /* همان نام، اما برای وزن متفاوت */
12
+ src: url('./fonts/Vazirmatn-Bold.woff2') format('woff2'); /* مسیر فایل فونت Bold نسبت به فایل CSS */
13
+ font-weight: bold; /* این تعریف برای وزن پررنگ فونت است */
14
+ font-style: normal;
15
+ }
16
+
17
+ /* سبک‌های عمومی برای بدنه صفحه - حالا از فونت تعریف شده با @font-face استفاده میکنیم */
18
+ body {
19
+ font-family: 'Vazirmatn', sans-serif; /* استفاده از نامی که در @font-face تعریف کردیم */
20
+ margin: 0;
21
+ padding: 20px;
22
+ background-color: #f4f4f4;
23
+ color: #333;
24
+ direction: rtl;
25
+ text-align: right;
26
+ }
27
+
28
+ /* سبک برای کانتینر اصلی که محتوا را در بر می‌گیرد */
29
+ .container {
30
+ max-width: 800px;
31
+ margin: 20px auto;
32
+ padding: 20px;
33
+ background-color: #fff;
34
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
35
+ border-radius: 8px;
36
+ }
37
+
38
+ /* سبک برای عنوان اصلی */
39
+ h1 {
40
+ text-align: center;
41
+ color: #0056b3;
42
+ margin-bottom: 30px;
43
+ }
44
+
45
+ /* سبک برای بخش جستجو */
46
+ .search-section {
47
+ margin-bottom: 30px;
48
+ padding: 15px;
49
+ border: 1px solid #ddd;
50
+ border-radius: 5px;
51
+ background-color: #e9e9e9;
52
+ }
53
+
54
+ /* سبک برای برچسب کادر سؤال */
55
+ .search-section label {
56
+ display: block;
57
+ margin-bottom: 8px;
58
+ font-weight: bold;
59
+ }
60
+
61
+ /* سبک برای کادر ورود سؤال (textarea) */
62
+ #userQuestion {
63
+ width: 100%;
64
+ padding: 10px;
65
+ margin-bottom: 10px;
66
+ border: 1px solid #ccc;
67
+ border-radius: 4px;
68
+ box-sizing: border-box;
69
+ font-size: 1rem;
70
+ /* اعمال فونت در کادر متن - این قبلا هم اضافه شده بود */
71
+ font-family: 'Vazirmatn', sans-serif;
72
+ direction: rtl;
73
+ text-align: right;
74
+ resize: vertical;
75
+ }
76
+
77
+ /* سبک برای دکمه جستجو */
78
+ #searchButton {
79
+ display: block;
80
+ width: 100%;
81
+ padding: 10px;
82
+ background-color: #007bff;
83
+ color: white;
84
+ border: none;
85
+ border-radius: 4px;
86
+ cursor: pointer;
87
+ font-size: 1.1rem;
88
+ transition: background-color 0.3s ease;
89
+ /* ****** این خط اضافه شده است ****** */
90
+ font-family: 'Vazirmatn', sans-serif; /* اعمال فونت وزیرمتن به دکمه */
91
+ /* ********************************** */
92
+ }
93
+
94
+ /* سبک هنگام بردن ماوس روی دکمه */
95
+ #searchButton:hover {
96
+ background-color: #0056b3;
97
+ }
98
+
99
+ /* سبک برای بخش نمایش نتایج */
100
+ .results-section {
101
+ margin-top: 30px;
102
+ }
103
+
104
+ /* سبک برای عنوان نتایج */
105
+ .results-section h2 {
106
+ color: #0056b3;
107
+ margin-bottom: 15px;
108
+ text-align: right;
109
+ }
110
+
111
+ /* سبک برای ناحیه نمایش نتایج */
112
+ #searchResults {
113
+ border: 1px solid #ddd;
114
+ padding: 15px;
115
+ border-radius: 5px;
116
+ background-color: #fff;
117
+ min-height: 100px;
118
+ white-space: pre-wrap;
119
+ word-wrap: break-word;
120
+ }
121
+
122
+ /* سبک پیش‌فرض برای پاراگراف داخل نتایج (پیام اولیه) */
123
+ #searchResults p {
124
+ color: #666;
125
+ text-align: center;
126
+ }