Spaces:
Sleeping
Sleeping
Upload 10 files
Browse files- Dockerfile +20 -0
- fonts/Vazirmatn-Bold.woff2 +0 -0
- fonts/Vazirmatn-Regular.woff2 +0 -0
- fonts/vazirmatn.css +69 -0
- index.html +214 -0
- jabe_siah.json +0 -0
- requirements.txt +4 -0
- script.js +460 -0
- server.py +84 -0
- style.css +126 -0
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 |
+
}
|