psugam commited on
Commit
e33019a
·
verified ·
1 Parent(s): b6e41f9

Upload 4 files

Browse files
frontend/html/about.html ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>About - Sanskrit Smart Reader</title>
6
+ <style>
7
+ :root {
8
+ --primary: #1e4d6b;
9
+ --secondary: #8b6914;
10
+ --accent: #8b3a1a;
11
+ --bg: #faf8f3;
12
+ --card-bg: #ffffff;
13
+ --text: #1a1a1a;
14
+ --muted: #5a5a5a;
15
+ --border-soft: #d4d4d4;
16
+ --border-medium: #b8b8b8;
17
+ }
18
+
19
+ /* ===== PAGE ===== */
20
+ body {
21
+ font-family: "Crimson Pro", "Georgia", "Times New Roman", serif;
22
+ background: linear-gradient(to bottom, #f5f2eb 0%, #faf8f3 100%);
23
+ color: var(--text);
24
+ margin: 0;
25
+ padding: 0;
26
+ -webkit-font-smoothing: antialiased;
27
+ min-height: 100vh;
28
+ }
29
+
30
+ .container {
31
+ max-width: 960px;
32
+ margin: auto;
33
+ padding: 32px 24px;
34
+ }
35
+
36
+ /* ===== NAVBAR ===== */
37
+ nav {
38
+ background: var(--card-bg);
39
+ border-bottom: 1px solid var(--border-medium);
40
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
41
+ position: sticky;
42
+ top: 0;
43
+ z-index: 100;
44
+ }
45
+
46
+ .nav-container {
47
+ max-width: 960px;
48
+ margin: 0 auto;
49
+ padding: 0 24px;
50
+ display: flex;
51
+ justify-content: space-between;
52
+ align-items: center;
53
+ height: 64px;
54
+ }
55
+
56
+ .nav-brand {
57
+ font-family: "Crimson Pro", Georgia, serif;
58
+ font-size: 1.35rem;
59
+ font-weight: 700;
60
+ color: var(--primary);
61
+ text-decoration: none;
62
+ letter-spacing: -0.02em;
63
+ }
64
+
65
+ .nav-links {
66
+ display: flex;
67
+ gap: 8px;
68
+ align-items: center;
69
+ }
70
+
71
+ .nav-links a {
72
+ font-family: "Inter", system-ui, sans-serif;
73
+ font-size: 0.95rem;
74
+ font-weight: 600;
75
+ color: var(--muted);
76
+ text-decoration: none;
77
+ padding: 8px 20px;
78
+ border-radius: 8px;
79
+ transition: all 0.2s ease;
80
+ }
81
+
82
+ .nav-links a:hover {
83
+ color: var(--primary);
84
+ background: #f5f2eb;
85
+ }
86
+
87
+ .nav-links a.active {
88
+ color: white;
89
+ background: var(--primary);
90
+ }
91
+
92
+ /* ===== CONTENT ===== */
93
+ .content-box {
94
+ background: var(--card-bg);
95
+ padding: 42px 48px;
96
+ border-radius: 12px;
97
+ border: 1px solid var(--border-medium);
98
+ box-shadow: 0 2px 16px rgba(0, 0, 0, 0.05);
99
+ line-height: 1.8;
100
+ }
101
+
102
+ h2 {
103
+ color: var(--primary);
104
+ font-size: 2rem;
105
+ margin-top: 0;
106
+ letter-spacing: -0.02em;
107
+ }
108
+
109
+ p {
110
+ font-size: 1.1rem;
111
+ color: #2d3748;
112
+ }
113
+
114
+ ul {
115
+ padding-left: 1.5rem;
116
+ }
117
+
118
+ li {
119
+ margin-bottom: 0.5rem;
120
+ font-size: 1.1rem;
121
+ }
122
+
123
+ @media (max-width: 640px) {
124
+ .nav-container {
125
+ flex-direction: column;
126
+ height: auto;
127
+ padding: 16px 24px;
128
+ gap: 12px;
129
+ }
130
+ .nav-links {
131
+ width: 100%;
132
+ justify-content: center;
133
+ }
134
+ }
135
+ </style>
136
+ </head>
137
+ <body>
138
+ <nav>
139
+ <div class="nav-container">
140
+ <a href="../../../index.html" class="nav-brand"
141
+ >Sanskrit Smart Reader</a
142
+ >
143
+ <div class="nav-links">
144
+ <a href="../../../index.html">Home</a>
145
+ <a href="about.html" class="active">About</a>
146
+ <a href="text_parser.html">Parser</a>
147
+ </div>
148
+ </div>
149
+ </nav>
150
+
151
+ <div class="container">
152
+ <div class="content-box">
153
+ <h2>About Sanskrit Smart Reader</h2>
154
+ <p>
155
+ This tool helps students and scholars analyze Sanskrit texts. By
156
+ clicking on words, you can instantly retrieve morphological analysis
157
+ and dictionary definitions from multiple sources.
158
+ </p>
159
+ <p>Features:</p>
160
+ <ul>
161
+ <li>Instant dictionary lookup</li>
162
+ <li>Compound splitting</li>
163
+ <li>Morphological analysis</li>
164
+ </ul>
165
+ </div>
166
+ </div>
167
+ </body>
168
+ </html>
frontend/html/text_parser.html ADDED
@@ -0,0 +1,504 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Sanskrit Text Parser</title>
7
+ <style>
8
+ :root {
9
+ --primary: #1e4d6b;
10
+ --secondary: #8b6914;
11
+ --accent: #8b3a1a;
12
+ --bg: #faf8f3;
13
+ --card-bg: #ffffff;
14
+ --text: #1a1a1a;
15
+ --muted: #5a5a5a;
16
+ --border-soft: #d4d4d4;
17
+ --border-medium: #b8b8b8;
18
+ --entry-border: #e8e5df;
19
+ }
20
+
21
+ body {
22
+ font-family: "Crimson Pro", "Georgia", "Times New Roman", serif;
23
+ background: linear-gradient(to bottom, #f5f2eb 0%, #faf8f3 100%);
24
+ color: var(--text);
25
+ margin: 0;
26
+ padding: 0;
27
+ -webkit-font-smoothing: antialiased;
28
+ min-height: 100vh;
29
+ }
30
+
31
+ .container {
32
+ max-width: 960px;
33
+ margin: auto;
34
+ padding: 32px 24px;
35
+ }
36
+
37
+ /* ===== NAVBAR ===== */
38
+ nav {
39
+ background: var(--card-bg);
40
+ border-bottom: 1px solid var(--border-medium);
41
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
42
+ position: sticky;
43
+ top: 0;
44
+ z-index: 100;
45
+ }
46
+
47
+ .nav-container {
48
+ max-width: 960px;
49
+ margin: 0 auto;
50
+ padding: 0 24px;
51
+ display: flex;
52
+ justify-content: space-between;
53
+ align-items: center;
54
+ height: 64px;
55
+ }
56
+
57
+ .nav-brand {
58
+ font-family: "Crimson Pro", Georgia, serif;
59
+ font-size: 1.35rem;
60
+ font-weight: 700;
61
+ color: var(--primary);
62
+ text-decoration: none;
63
+ }
64
+
65
+ .nav-links {
66
+ display: flex;
67
+ gap: 8px;
68
+ align-items: center;
69
+ }
70
+
71
+ .nav-links a {
72
+ font-family: "Inter", system-ui, sans-serif;
73
+ font-size: 0.95rem;
74
+ font-weight: 600;
75
+ color: var(--muted);
76
+ text-decoration: none;
77
+ padding: 8px 20px;
78
+ border-radius: 8px;
79
+ transition: all 0.2s ease;
80
+ }
81
+
82
+ .nav-links a:hover {
83
+ color: var(--primary);
84
+ background: #f5f2eb;
85
+ }
86
+
87
+ .nav-links a.active {
88
+ color: white;
89
+ background: var(--primary);
90
+ }
91
+
92
+ /* ===== INPUT SECTION ===== */
93
+ .input-section {
94
+ background: var(--card-bg);
95
+ padding: 22px 26px;
96
+ border-radius: 12px;
97
+ border: 1px solid var(--border-medium);
98
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
99
+ margin-bottom: 32px;
100
+ }
101
+
102
+ textarea {
103
+ width: 100%;
104
+ height: 120px;
105
+ padding: 16px;
106
+ font-size: 1.1rem;
107
+ border: 1.5px solid var(--border-soft);
108
+ border-radius: 8px;
109
+ margin-bottom: 16px;
110
+ box-sizing: border-box;
111
+ font-family: "Crimson Pro", serif;
112
+ background: #fafafa;
113
+ transition: all 0.2s ease;
114
+ }
115
+
116
+ textarea:focus {
117
+ outline: none;
118
+ border-color: var(--primary);
119
+ background: white;
120
+ }
121
+
122
+ button {
123
+ padding: 12px 28px;
124
+ background: var(--primary);
125
+ color: white;
126
+ border: none;
127
+ border-radius: 8px;
128
+ cursor: pointer;
129
+ font-weight: 600;
130
+ font-family: "Inter", sans-serif;
131
+ font-size: 0.95rem;
132
+ }
133
+
134
+ /* ===== PARSED TEXT ===== */
135
+ #parsed-text {
136
+ background: var(--card-bg);
137
+ padding: 42px 48px;
138
+ border-radius: 12px;
139
+ border: 1px solid var(--border-medium);
140
+ box-shadow: 0 2px 16px rgba(0, 0, 0, 0.05);
141
+ font-size: 1.65em;
142
+ line-height: 2.1em;
143
+ margin-bottom: 32px;
144
+ min-height: 100px;
145
+ }
146
+
147
+ .word-token {
148
+ cursor: pointer;
149
+ color: var(--primary);
150
+ border-bottom: 1.5px dotted var(--secondary);
151
+ transition: 0.2s;
152
+ display: inline-block;
153
+ padding: 0 4px;
154
+ }
155
+
156
+ .word-token:hover {
157
+ color: var(--accent);
158
+ background-color: #f5f2eb;
159
+ }
160
+
161
+ /* ===== MODAL ===== */
162
+ .modal {
163
+ display: none;
164
+ position: fixed;
165
+ z-index: 1000;
166
+ left: 0;
167
+ top: 0;
168
+ width: 100%;
169
+ height: 100%;
170
+ background: rgba(0, 0, 0, 0.5);
171
+ backdrop-filter: blur(4px);
172
+ }
173
+
174
+ .modal-content {
175
+ background: var(--bg);
176
+ margin: 5% auto;
177
+ padding: 30px;
178
+ width: 85%;
179
+ max-width: 800px;
180
+ border-radius: 16px;
181
+ position: relative;
182
+ max-height: 85vh;
183
+ overflow-y: auto;
184
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
185
+ }
186
+
187
+ .close-btn {
188
+ position: absolute;
189
+ right: 20px;
190
+ top: 15px;
191
+ font-size: 35px;
192
+ cursor: pointer;
193
+ color: var(--accent);
194
+ line-height: 1;
195
+ }
196
+
197
+ /* ===== COMPOUND UI ===== */
198
+ .split-container {
199
+ display: flex;
200
+ flex-wrap: wrap;
201
+ justify-content: center;
202
+ gap: 12px;
203
+ margin: 22px 0;
204
+ }
205
+
206
+ .part-btn {
207
+ background: white;
208
+ border: 1.5px solid var(--primary);
209
+ color: var(--primary);
210
+ padding: 8px 18px;
211
+ border-radius: 999px;
212
+ cursor: pointer;
213
+ font-weight: 600;
214
+ font-size: 0.95em;
215
+ }
216
+
217
+ .part-btn.active {
218
+ background: var(--primary);
219
+ color: white;
220
+ }
221
+
222
+ /* ===== RESULTS ===== */
223
+ .meaning-card {
224
+ background: white;
225
+ border-radius: 10px;
226
+ padding: 28px 32px;
227
+ margin-top: 20px;
228
+ border: 1px solid var(--border-medium);
229
+ box-shadow: 0 1px 8px rgba(0, 0, 0, 0.04);
230
+ }
231
+
232
+ .label-pill {
233
+ display: inline-block;
234
+ background: var(--primary);
235
+ color: white;
236
+ padding: 6px 14px;
237
+ border-radius: 6px;
238
+ font-size: 0.7em;
239
+ font-weight: 700;
240
+ text-transform: uppercase;
241
+ font-family: "Inter", sans-serif;
242
+ margin-bottom: 16px;
243
+ }
244
+
245
+ .dict-title {
246
+ display: block;
247
+ margin-top: 1.6em;
248
+ margin-bottom: 0.5em;
249
+ padding-top: 1.2em;
250
+ border-top: 2px solid var(--entry-border);
251
+ font-weight: 700;
252
+ font-size: 1.1em;
253
+ color: #2d3748;
254
+ font-family: "Inter", system-ui, sans-serif;
255
+ }
256
+
257
+ .dict-title:first-of-type {
258
+ margin-top: 0.8em;
259
+ padding-top: 0;
260
+ border-top: none;
261
+ }
262
+
263
+ .def-content {
264
+ margin-top: 12px;
265
+ color: #2d3748;
266
+ line-height: 1.75;
267
+ font-family: "Crimson Pro", Georgia, serif;
268
+ }
269
+
270
+ .def-content .skt {
271
+ font-weight: 600;
272
+ color: #1a1a1a;
273
+ }
274
+ .def-content .abbr {
275
+ font-size: 0.9em;
276
+ font-weight: 600;
277
+ color: #4a5568;
278
+ }
279
+
280
+ @media (max-width: 640px) {
281
+ .nav-container {
282
+ flex-direction: column;
283
+ height: auto;
284
+ padding: 16px 24px;
285
+ gap: 12px;
286
+ }
287
+ .nav-links {
288
+ width: 100%;
289
+ justify-content: center;
290
+ }
291
+ }
292
+ </style>
293
+ </head>
294
+ <body>
295
+ <nav>
296
+ <div class="nav-container">
297
+ <a href="../../../index.html" class="nav-brand"
298
+ >Sanskrit Smart Reader</a
299
+ >
300
+ <div class="nav-links">
301
+ <a href="../../../index.html">Home</a>
302
+ <a href="about.html">About</a>
303
+ <a href="text_parser.html" class="active">Parser</a>
304
+ </div>
305
+ </div>
306
+ </nav>
307
+
308
+ <div class="container">
309
+ <h2 style="color: var(--primary); letter-spacing: -0.02em">
310
+ Sanskrit Text Parser
311
+ </h2>
312
+
313
+ <div class="input-section">
314
+ <p style="margin-top: 0; color: var(--muted); font-weight: 600">
315
+ Enter Sanskrit text below and click Analyze.
316
+ </p>
317
+ <textarea id="input-text" placeholder="Enter text here..."></textarea>
318
+ <button onclick="transformText()">Analyze</button>
319
+ </div>
320
+
321
+ <div id="parsed-text"></div>
322
+ </div>
323
+
324
+ <div id="wordModal" class="modal">
325
+ <div class="modal-content">
326
+ <span class="close-btn" onclick="closeModal()">&times;</span>
327
+ <div id="modal-body"></div>
328
+ </div>
329
+ </div>
330
+
331
+ <script>
332
+ const base_api = "https://psugam-sanskrit-parser-api.hf.space/";
333
+
334
+ console.log("Using API base:", base_api);
335
+
336
+ const parsedTextContainer = document.getElementById("parsed-text");
337
+ const modal = document.getElementById("wordModal");
338
+ const modalBody = document.getElementById("modal-body");
339
+
340
+ // Close modal logic
341
+ window.onclick = (e) => {
342
+ if (e.target == modal) closeModal();
343
+ };
344
+ function closeModal() {
345
+ modal.style.display = "none";
346
+ modalBody.innerHTML = "";
347
+ }
348
+
349
+ function sanitizeHTML(str) {
350
+ const div = document.createElement("div");
351
+ div.textContent = str;
352
+ return div.innerHTML;
353
+ }
354
+
355
+ function sanitizeAllowHTML(input) {
356
+ const decoder = document.createElement("textarea");
357
+ decoder.innerHTML = input;
358
+ let html = decoder.value;
359
+
360
+ html = html
361
+ .replace(/<div[^>]*\/>/gi, "<br>")
362
+ .replace(/<lbinfo[^>]*\/>/gi, "")
363
+ .replace(/<\/?meta[^>]*>/gi, "");
364
+
365
+ const replacements = {
366
+ s: 'span class="skt"',
367
+ ab: 'span class="abbr"',
368
+ lex: 'span class="pos"',
369
+ ls: 'span class="source"',
370
+ };
371
+
372
+ for (const [tag, repl] of Object.entries(replacements)) {
373
+ html = html.replace(new RegExp(`<${tag}[^>]*>`, "gi"), `<${repl}>`);
374
+ html = html.replace(
375
+ new RegExp(`</${tag}>`, "gi"),
376
+ `</${repl.split(" ")[0]}>`
377
+ );
378
+ }
379
+
380
+ html = html.replace(
381
+ /<(?!\/?(br|b|i|em|strong|sup|sub|span)\b)[^>]+>/gi,
382
+ ""
383
+ );
384
+ return html;
385
+ }
386
+
387
+ function transformText() {
388
+ const input = document.getElementById("input-text").value;
389
+ parsedTextContainer.innerHTML = "";
390
+
391
+ const tokens = input.split(/(\s+|[।.।])/);
392
+
393
+ tokens.forEach((token) => {
394
+ if (token.trim()) {
395
+ const clean = token.replace(/[।.।\s]/g, "");
396
+ if (clean) {
397
+ const span = document.createElement("span");
398
+ span.className = "word-token";
399
+ span.textContent = sanitizeHTML(token);
400
+ span.onclick = () => initiateProcess(clean);
401
+ parsedTextContainer.appendChild(span);
402
+ } else {
403
+ parsedTextContainer.appendChild(
404
+ document.createTextNode(sanitizeHTML(token))
405
+ );
406
+ }
407
+ }
408
+ });
409
+ }
410
+
411
+ async function initiateProcess(word) {
412
+ modalBody.innerHTML = `<p style="color: var(--muted)">Analyzing <b>${sanitizeHTML(
413
+ word
414
+ )}</b>…</p>`;
415
+ modal.style.display = "block";
416
+
417
+ const res = await fetch(
418
+ `${base_api}/split?word=${encodeURIComponent(word)}`
419
+ );
420
+ const data = await res.json();
421
+
422
+ if (data.is_compound)
423
+ renderCompoundUI(word, data.components, modalBody);
424
+ else renderDirectMeaning(word, modalBody);
425
+ }
426
+
427
+ function renderCompoundUI(word, parts, target) {
428
+ target.innerHTML = `
429
+ <h3 style="color: var(--primary)">${sanitizeHTML(word)}</h3>
430
+ <div class="split-container">
431
+ ${parts
432
+ .map(
433
+ (p) =>
434
+ `<button class="part-btn" onclick="fetchPartMeaning('${sanitizeHTML(
435
+ p
436
+ )}','compound-res',this)">${sanitizeHTML(p)}</button>`
437
+ )
438
+ .join("")}
439
+ </div>
440
+ <div id="compound-res"></div>`;
441
+ }
442
+
443
+ async function renderDirectMeaning(word, target) {
444
+ target.innerHTML = `<h3 style="color: var(--primary)">${sanitizeHTML(
445
+ word
446
+ )}</h3><div id="direct-res"></div>`;
447
+ fetchPartMeaning(word, "direct-res");
448
+ }
449
+
450
+ async function fetchPartMeaning(word, id, btn = null) {
451
+ if (btn) {
452
+ btn.parentElement
453
+ .querySelectorAll(".part-btn")
454
+ .forEach((b) => b.classList.remove("active"));
455
+ btn.classList.add("active");
456
+ }
457
+
458
+ const display = document.getElementById(id);
459
+ display.innerHTML = "Fetching dictionary…";
460
+
461
+ const res = await fetch(
462
+ `${base_api}/meaning?word=${encodeURIComponent(word)}`
463
+ );
464
+ const data = await res.json();
465
+
466
+ const sourceMap = {
467
+ mw: "Monier-Williams",
468
+ ap90: "Apte",
469
+ cae: "Cappeller",
470
+ bhs: "Buddhist",
471
+ };
472
+
473
+ display.innerHTML = data
474
+ .map((e) => {
475
+ // Format the grammar tags: e.g., "Nom, Sg | Acc, Sg"
476
+ const tags = e.detected_tags.map((t) => t.join(", ")).join(" | ");
477
+
478
+ return `
479
+ <div class="meaning-card">
480
+ <span class="label-pill">${sanitizeHTML(e.type)}</span>
481
+
482
+ <div class="grammar-tags" style="color: var(--accent); font-weight: bold; margin-bottom: 10px; font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.5px;">
483
+ ${sanitizeHTML(tags)}
484
+ </div>
485
+
486
+ <div style="margin-bottom: 10px;"><b>Stem:</b> ${sanitizeHTML(
487
+ e.stem
488
+ )}</div>
489
+ ${Object.entries(e.definitions)
490
+ .map(
491
+ ([src, defs]) => `
492
+ <span class="dict-title">${sourceMap[src] || src}</span>
493
+ <div class="def-content">
494
+ ${defs.map((d) => `• ${sanitizeAllowHTML(d)}`).join("<br>")}
495
+ </div>`
496
+ )
497
+ .join("")}
498
+ </div>`;
499
+ })
500
+ .join("");
501
+ }
502
+ </script>
503
+ </body>
504
+ </html>
frontend/js/sanitize.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ function sanitizeHTML(text) {
2
+ const temp = document.createElement('div');
3
+ temp.textContent = text;
4
+ return temp.innerHTML;
5
+ }
index.html ADDED
@@ -0,0 +1,620 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="sa">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Sanskrit Smart Reader</title>
7
+
8
+ <style>
9
+ :root {
10
+ --primary: #1e4d6b;
11
+ --secondary: #8b6914;
12
+ --accent: #8b3a1a;
13
+ --bg: #faf8f3;
14
+ --card-bg: #ffffff;
15
+ --text: #1a1a1a;
16
+ --muted: #5a5a5a;
17
+ --border-soft: #d4d4d4;
18
+ --border-medium: #b8b8b8;
19
+ --entry-border: #e8e5df;
20
+ }
21
+
22
+ /* ===== PAGE ===== */
23
+ body {
24
+ font-family: "Crimson Pro", "Georgia", "Times New Roman", serif;
25
+ background: linear-gradient(to bottom, #f5f2eb 0%, #faf8f3 100%);
26
+ color: var(--text);
27
+ margin: 0;
28
+ padding: 0;
29
+ -webkit-font-smoothing: antialiased;
30
+ min-height: 100vh;
31
+ }
32
+
33
+ .container {
34
+ max-width: 960px;
35
+ margin: auto;
36
+ padding: 32px 24px;
37
+ }
38
+
39
+ /* ===== NAVBAR ===== */
40
+ nav {
41
+ background: var(--card-bg);
42
+ border-bottom: 1px solid var(--border-medium);
43
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
44
+ position: sticky;
45
+ top: 0;
46
+ z-index: 100;
47
+ }
48
+
49
+ .nav-container {
50
+ max-width: 960px;
51
+ margin: 0 auto;
52
+ padding: 0 24px;
53
+ display: flex;
54
+ justify-content: space-between;
55
+ align-items: center;
56
+ height: 64px;
57
+ }
58
+
59
+ .nav-brand {
60
+ font-family: "Crimson Pro", Georgia, serif;
61
+ font-size: 1.35rem;
62
+ font-weight: 700;
63
+ color: var(--primary);
64
+ text-decoration: none;
65
+ letter-spacing: -0.02em;
66
+ }
67
+
68
+ .nav-links {
69
+ display: flex;
70
+ gap: 8px;
71
+ align-items: center;
72
+ }
73
+
74
+ .nav-links a {
75
+ font-family: "Inter", system-ui, sans-serif;
76
+ font-size: 0.95rem;
77
+ font-weight: 600;
78
+ color: var(--muted);
79
+ text-decoration: none;
80
+ padding: 8px 20px;
81
+ border-radius: 8px;
82
+ transition: all 0.2s ease;
83
+ letter-spacing: 0.01em;
84
+ }
85
+
86
+ .nav-links a:hover {
87
+ color: var(--primary);
88
+ background: #f5f2eb;
89
+ }
90
+
91
+ .nav-links a.active {
92
+ color: white;
93
+ background: var(--primary);
94
+ }
95
+
96
+ /* ===== SEARCH ===== */
97
+ .search-container {
98
+ background: var(--card-bg);
99
+ padding: 22px 26px;
100
+ border-radius: 12px;
101
+ border: 1px solid var(--border-medium);
102
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
103
+ margin-bottom: 32px;
104
+ display: flex;
105
+ gap: 14px;
106
+ }
107
+
108
+ input[type="text"] {
109
+ flex: 1;
110
+ padding: 13px 16px;
111
+ border: 1.5px solid var(--border-soft);
112
+ border-radius: 8px;
113
+ font-size: 1.05rem;
114
+ font-family: "Inter", system-ui, sans-serif;
115
+ background: #fafafa;
116
+ transition: all 0.2s ease;
117
+ }
118
+
119
+ input[type="text"]:focus {
120
+ outline: none;
121
+ border-color: var(--primary);
122
+ background: white;
123
+ box-shadow: 0 0 0 3px rgba(30, 77, 107, 0.08);
124
+ }
125
+
126
+ button {
127
+ padding: 12px 28px;
128
+ background: var(--primary);
129
+ color: white;
130
+ border: none;
131
+ border-radius: 8px;
132
+ cursor: pointer;
133
+ font-weight: 600;
134
+ font-family: "Inter", system-ui, sans-serif;
135
+ font-size: 0.95rem;
136
+ letter-spacing: 0.015em;
137
+ transition: background 0.2s ease;
138
+ }
139
+
140
+ button:hover {
141
+ background: #163a52;
142
+ }
143
+
144
+ /* ===== TEXT READER ===== */
145
+ .text-box {
146
+ background: var(--card-bg);
147
+ padding: 42px 48px;
148
+ border-radius: 12px;
149
+ border: 1px solid var(--border-medium);
150
+ box-shadow: 0 2px 16px rgba(0, 0, 0, 0.05);
151
+ font-size: 1.65em;
152
+ line-height: 2.1em;
153
+ font-family: "Crimson Pro", Georgia, serif;
154
+ }
155
+
156
+ /* Clickable words */
157
+ .word {
158
+ cursor: pointer;
159
+ color: var(--primary);
160
+ border-bottom: 1.5px dotted var(--secondary);
161
+ transition: all 0.15s ease;
162
+ padding-bottom: 2px;
163
+ }
164
+
165
+ .word:hover {
166
+ color: var(--accent);
167
+ border-bottom-color: var(--accent);
168
+ }
169
+
170
+ /* ===== MODAL ===== */
171
+ .modal {
172
+ display: none;
173
+ position: fixed;
174
+ inset: 0;
175
+ background: rgba(20, 20, 20, 0.55);
176
+ backdrop-filter: blur(6px);
177
+ z-index: 1000;
178
+ }
179
+
180
+ .modal-content {
181
+ background: var(--bg);
182
+ margin: 5vh auto;
183
+ padding: 32px;
184
+ width: 92%;
185
+ max-width: 760px;
186
+ border-radius: 20px;
187
+ position: relative;
188
+ max-height: 85vh;
189
+ overflow-y: auto;
190
+ box-shadow: 0 30px 80px rgba(0, 0, 0, 0.25);
191
+ }
192
+
193
+ .close-btn {
194
+ position: absolute;
195
+ right: 22px;
196
+ top: 18px;
197
+ font-size: 26px;
198
+ color: var(--muted);
199
+ cursor: pointer;
200
+ }
201
+
202
+ /* ===== MEANINGS ===== */
203
+ .meaning-card {
204
+ background: white;
205
+ border-radius: 10px;
206
+ padding: 28px 32px;
207
+ margin-bottom: 28px;
208
+ border: 1px solid var(--border-medium);
209
+ box-shadow: 0 1px 8px rgba(0, 0, 0, 0.04);
210
+ }
211
+
212
+ .meaning-card:last-child {
213
+ margin-bottom: 0;
214
+ }
215
+
216
+ .label-pill {
217
+ display: inline-block;
218
+ background: var(--primary);
219
+ color: white;
220
+ padding: 6px 14px;
221
+ border-radius: 6px;
222
+ font-size: 0.7em;
223
+ font-weight: 700;
224
+ letter-spacing: 0.04em;
225
+ text-transform: uppercase;
226
+ font-family: "Inter", system-ui, sans-serif;
227
+ margin-bottom: 16px;
228
+ }
229
+
230
+ .dict-title {
231
+ display: block;
232
+ margin-top: 1.6em;
233
+ margin-bottom: 0.5em;
234
+ padding-top: 1.2em;
235
+ border-top: 2px solid var(--entry-border);
236
+ font-weight: 700;
237
+ font-size: 1.1em;
238
+ color: #2d3748;
239
+ font-family: "Inter", system-ui, sans-serif;
240
+ letter-spacing: -0.01em;
241
+ }
242
+
243
+ .dict-title:first-of-type {
244
+ margin-top: 0.8em;
245
+ padding-top: 0;
246
+ border-top: none;
247
+ }
248
+
249
+ /* ===== DICTIONARY CONTENT ===== */
250
+ .def-content {
251
+ margin-top: 12px;
252
+ max-width: 75ch;
253
+ color: #2d3748;
254
+ line-height: 1.75;
255
+ font-family: "Crimson Pro", Georgia, serif;
256
+ }
257
+
258
+ /* Critical normalization */
259
+ .def-content,
260
+ .def-content * {
261
+ font-size: 1.05rem !important;
262
+ line-height: 1.75;
263
+ }
264
+
265
+ /* Sanskrit */
266
+ .def-content .skt {
267
+ font-family: "Noto Serif Devanagari", "Sanskrit 2003", serif;
268
+ font-size: 1.15em !important;
269
+ font-weight: 600;
270
+ color: #1a1a1a;
271
+ }
272
+
273
+ /* Abbreviations */
274
+ .def-content .abbr,
275
+ .def-content abbr {
276
+ font-size: 0.9em !important;
277
+ font-weight: 600;
278
+ color: #4a5568;
279
+ font-family: "Inter", system-ui, sans-serif;
280
+ }
281
+
282
+ /* Metadata */
283
+ .def-content .meta {
284
+ font-size: 0.92em !important;
285
+ color: var(--muted);
286
+ font-style: italic;
287
+ }
288
+
289
+ /* Language markers */
290
+ .def-content .lang {
291
+ font-style: italic;
292
+ font-size: 0.98em !important;
293
+ color: #4a5568;
294
+ }
295
+
296
+ /* POS (Part of Speech) */
297
+ .def-content .pos {
298
+ font-weight: 700;
299
+ color: #2d3748;
300
+ font-family: "Inter", system-ui, sans-serif;
301
+ font-size: 0.95em !important;
302
+ }
303
+
304
+ /* Sources */
305
+ .def-content .source {
306
+ font-size: 0.92em !important;
307
+ color: #718096;
308
+ font-style: italic;
309
+ }
310
+
311
+ /* Superscripts */
312
+ .def-content sup {
313
+ font-size: 0.7em !important;
314
+ vertical-align: super;
315
+ line-height: 0;
316
+ }
317
+
318
+ /* Prevent nesting collapse */
319
+ .def-content span span span {
320
+ font-size: inherit !important;
321
+ }
322
+
323
+ /* List items within definitions */
324
+ .def-content ol,
325
+ .def-content ul {
326
+ margin: 0.8em 0;
327
+ padding-left: 1.8em;
328
+ }
329
+
330
+ .def-content li {
331
+ margin: 0.4em 0;
332
+ line-height: 1.7;
333
+ }
334
+
335
+ /* Emphasis */
336
+ .def-content em,
337
+ .def-content i {
338
+ font-style: italic;
339
+ color: #2d3748;
340
+ }
341
+
342
+ .def-content strong,
343
+ .def-content b {
344
+ font-weight: 700;
345
+ color: #1a1a1a;
346
+ }
347
+
348
+ /* ===== COMPOUND UI ===== */
349
+ .split-container {
350
+ display: flex;
351
+ flex-wrap: wrap;
352
+ justify-content: center;
353
+ gap: 12px;
354
+ margin: 22px 0;
355
+ }
356
+
357
+ .part-btn {
358
+ background: white;
359
+ border: 1.5px solid var(--primary);
360
+ color: var(--primary);
361
+ padding: 8px 18px;
362
+ border-radius: 999px;
363
+ cursor: pointer;
364
+ font-weight: 600;
365
+ font-size: 0.95em;
366
+ }
367
+
368
+ .part-btn.active {
369
+ background: var(--primary);
370
+ color: white;
371
+ }
372
+
373
+ /* Mobile responsive */
374
+ @media (max-width: 640px) {
375
+ .nav-container {
376
+ flex-direction: column;
377
+ height: auto;
378
+ padding: 16px 24px;
379
+ gap: 12px;
380
+ }
381
+
382
+ .nav-brand {
383
+ font-size: 1.2rem;
384
+ }
385
+
386
+ .nav-links {
387
+ width: 100%;
388
+ justify-content: center;
389
+ }
390
+
391
+ .nav-links a {
392
+ font-size: 0.9rem;
393
+ padding: 6px 16px;
394
+ }
395
+ }
396
+ </style>
397
+ </head>
398
+
399
+ <body>
400
+ <!-- NAVBAR -->
401
+ <nav>
402
+ <div class="nav-container">
403
+ <a href="index.html" class="nav-brand">Sanskrit Smart Reader</a>
404
+ <div class="nav-links">
405
+ <a href="index.html" class="active">Home</a>
406
+ <a href="./frontend//html/about.html">About</a>
407
+ <a href="./frontend//html/text_parser.html">Parser</a>
408
+ </div>
409
+ </div>
410
+ </nav>
411
+
412
+ <!-- MAIN CONTENT -->
413
+ <div class="container">
414
+ <div class="search-container">
415
+ <input type="text" id="search-input" placeholder="Search word..." />
416
+ <button onclick="handleSearch()">Search</button>
417
+ </div>
418
+
419
+ <div class="text-box" id="reader">
420
+ तस्मात्त्वमुत्तिष्ठ यशो लभस्व जित्वा शत्रून्भुङ्क्ष्व राज्यं समृद्धम् ।
421
+ </div>
422
+
423
+ <div id="page-results"></div>
424
+ </div>
425
+
426
+ <div id="wordModal" class="modal">
427
+ <div class="modal-content">
428
+ <span class="close-btn" onclick="closeModal()">&times;</span>
429
+ <div id="modal-body"></div>
430
+ </div>
431
+ </div>
432
+
433
+ <script>
434
+ const base_api = "https://psugam-sanskrit-parser-api.hf.space/";
435
+
436
+ console.log("Using API base:", base_api);
437
+
438
+ const reader = document.getElementById("reader");
439
+ const modal = document.getElementById("wordModal");
440
+ const modalBody = document.getElementById("modal-body");
441
+ const pageResults = document.getElementById("page-results");
442
+ const searchInput = document.getElementById("search-input");
443
+
444
+ window.onclick = (e) => {
445
+ if (e.target === modal) closeModal();
446
+ };
447
+
448
+ function closeModal() {
449
+ modal.style.display = "none";
450
+ modalBody.innerHTML = "";
451
+ }
452
+
453
+ function sanitizeHTML(str) {
454
+ const div = document.createElement("div");
455
+ div.textContent = str;
456
+ return div.innerHTML;
457
+ }
458
+
459
+ /* ===== SANITIZERS ===== */
460
+ function sanitizeAllowHTML(input) {
461
+ // 1. Decode HTML entities FIRST
462
+ const decoder = document.createElement("textarea");
463
+ decoder.innerHTML = input;
464
+ let html = decoder.value;
465
+
466
+ // 2. Normalize Apte / MW structural junk
467
+ html = html
468
+ .replace(/<div[^>]*\/>/gi, "<br>")
469
+ .replace(/<lbinfo[^>]*\/>/gi, "")
470
+ .replace(/<\/?meta[^>]*>/gi, "")
471
+ .replace(/<vlex[^>]*\/>/gi, "");
472
+
473
+ // 3. Convert dictionary semantic tags → spans
474
+ const replacements = {
475
+ s: 'span class="skt"',
476
+ s1: 'span class="skt"',
477
+ ab: 'span class="abbr"',
478
+ lex: 'span class="pos"',
479
+ info: 'span class="meta"',
480
+ ls: 'span class="source"',
481
+ lang: 'span class="lang"',
482
+ hom: "sup",
483
+ };
484
+
485
+ for (const [tag, repl] of Object.entries(replacements)) {
486
+ html = html.replace(new RegExp(`<${tag}[^>]*>`, "gi"), `<${repl}>`);
487
+ html = html.replace(
488
+ new RegExp(`</${tag}>`, "gi"),
489
+ `</${repl.split(" ")[0]}>`
490
+ );
491
+ }
492
+
493
+ // 4. REMOVE unknown tags but KEEP their content
494
+ html = html.replace(
495
+ /<(?!\/?(br|b|i|em|strong|sup|sub|span)\b)[^>]+>/gi,
496
+ ""
497
+ );
498
+
499
+ return html;
500
+ }
501
+
502
+ function sanitizeDictHTML(html) {
503
+ return html
504
+ .replace(/<vlex[^>]*\/>/gi, "")
505
+ .replace(/<\/?meta[^>]*>/gi, "")
506
+ .replace(/<\/?gk>|<\/?etym>|<\/?tib>/gi, "")
507
+ .replace(/<\/span>\s*<\/span>\s*<\/span>/g, "</span>");
508
+ }
509
+
510
+ /* ===== TEXT CLICKABLE WORDS ===== */
511
+ reader.innerHTML = reader.innerText
512
+ .trim()
513
+ .split(/(\s+|।)/)
514
+ .map((w) => {
515
+ const clean = w.replace(/[।\s]/g, "");
516
+ return clean
517
+ ? `<span class="word" onclick="initiateProcess('${sanitizeHTML(
518
+ clean
519
+ )}', true)">${sanitizeHTML(w)}</span>`
520
+ : sanitizeHTML(w);
521
+ })
522
+ .join("");
523
+
524
+ function handleSearch() {
525
+ const word = searchInput.value.trim();
526
+ if (word) initiateProcess(word, false);
527
+ }
528
+
529
+ async function initiateProcess(word, isPopup) {
530
+ const target = isPopup ? modalBody : pageResults;
531
+ target.innerHTML = `<p>Analyzing <b>${sanitizeHTML(word)}</b>…</p>`;
532
+ if (isPopup) modal.style.display = "block";
533
+
534
+ const res = await fetch(
535
+ `${base_api}/split?word=${encodeURIComponent(word)}`
536
+ );
537
+ const data = await res.json();
538
+
539
+ if (data.is_compound) renderCompoundUI(word, data.components, target);
540
+ else renderDirectMeaning(word, target);
541
+ }
542
+
543
+ function renderCompoundUI(word, parts, target) {
544
+ target.innerHTML = `
545
+ <h3>${sanitizeHTML(word)}</h3>
546
+ <div class="split-container">
547
+ ${parts
548
+ .map(
549
+ (p) =>
550
+ `<button class="part-btn" onclick="fetchPartMeaning('${sanitizeHTML(
551
+ p
552
+ )}','compound-res',this)">${sanitizeHTML(p)}</button>`
553
+ )
554
+ .join("")}
555
+ </div>
556
+ <div id="compound-res"></div>`;
557
+ }
558
+
559
+ async function renderDirectMeaning(word, target) {
560
+ target.innerHTML = `<h3>${sanitizeHTML(
561
+ word
562
+ )}</h3><div id="direct-res"></div>`;
563
+ fetchPartMeaning(word, "direct-res");
564
+ }
565
+
566
+ async function fetchPartMeaning(word, id, btn = null) {
567
+ if (btn) {
568
+ btn.parentElement
569
+ .querySelectorAll(".part-btn")
570
+ .forEach((b) => b.classList.remove("active"));
571
+ btn.classList.add("active");
572
+ }
573
+
574
+ const display = document.getElementById(id);
575
+ display.innerHTML = "Fetching dictionary…";
576
+
577
+ const res = await fetch(
578
+ `${base_api}/meaning?word=${encodeURIComponent(word)}`
579
+ );
580
+ const data = await res.json();
581
+
582
+ const sourceMap = {
583
+ mw: "Monier-Williams",
584
+ ap90: "Apte",
585
+ cae: "Cappeller",
586
+ bhs: "Buddhist",
587
+ };
588
+
589
+ display.innerHTML = data
590
+ .map((e) => {
591
+ // Format the grammar tags: e.g., "Nom, Sg | Acc, Sg"
592
+ const tags = e.detected_tags.map((t) => t.join(", ")).join(" | ");
593
+
594
+ return `
595
+ <div class="meaning-card">
596
+ <span class="label-pill">${e.type}</span>
597
+
598
+ <div class="grammar-tags" style="color: var(--accent); font-weight: bold; margin-bottom: 10px; font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.5px;">
599
+ ${sanitizeHTML(tags)}
600
+ </div>
601
+
602
+ <div style="margin-bottom: 10px;"><b>Stem:</b> ${sanitizeHTML(
603
+ e.stem
604
+ )}</div>
605
+ ${Object.entries(e.definitions)
606
+ .map(
607
+ ([src, defs]) => `
608
+ <span class="dict-title">${sourceMap[src] || src}</span>
609
+ <div class="def-content">
610
+ ${defs.map((d) => `• ${sanitizeAllowHTML(d)}`).join("<br>")}
611
+ </div>`
612
+ )
613
+ .join("")}
614
+ </div>`;
615
+ })
616
+ .join("");
617
+ }
618
+ </script>
619
+ </body>
620
+ </html>