Spaces:
Sleeping
Sleeping
| <html lang="sa"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Sanskrit Smart Reader</title> | |
| <style> | |
| :root { | |
| --primary: #1e4d6b; | |
| --secondary: #8b6914; | |
| --accent: #8b3a1a; | |
| --bg: #faf8f3; | |
| --card-bg: #ffffff; | |
| --text: #1a1a1a; | |
| --muted: #5a5a5a; | |
| --border-soft: #d4d4d4; | |
| --border-medium: #b8b8b8; | |
| --entry-border: #e8e5df; | |
| } | |
| /* ===== PAGE ===== */ | |
| body { | |
| font-family: "Crimson Pro", "Georgia", "Times New Roman", serif; | |
| background: linear-gradient(to bottom, #f5f2eb 0%, #faf8f3 100%); | |
| color: var(--text); | |
| margin: 0; | |
| padding: 0; | |
| -webkit-font-smoothing: antialiased; | |
| min-height: 100vh; | |
| } | |
| .container { | |
| max-width: 960px; | |
| margin: auto; | |
| padding: 32px 24px; | |
| } | |
| /* ===== NAVBAR ===== */ | |
| nav { | |
| background: var(--card-bg); | |
| border-bottom: 1px solid var(--border-medium); | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| } | |
| .nav-container { | |
| max-width: 960px; | |
| margin: 0 auto; | |
| padding: 0 24px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| height: 64px; | |
| } | |
| .nav-brand { | |
| font-family: "Crimson Pro", Georgia, serif; | |
| font-size: 1.35rem; | |
| font-weight: 700; | |
| color: var(--primary); | |
| text-decoration: none; | |
| letter-spacing: -0.02em; | |
| } | |
| .nav-links { | |
| display: flex; | |
| gap: 8px; | |
| align-items: center; | |
| } | |
| .nav-links a { | |
| font-family: "Inter", system-ui, sans-serif; | |
| font-size: 0.95rem; | |
| font-weight: 600; | |
| color: var(--muted); | |
| text-decoration: none; | |
| padding: 8px 20px; | |
| border-radius: 8px; | |
| transition: all 0.2s ease; | |
| letter-spacing: 0.01em; | |
| } | |
| .nav-links a:hover { | |
| color: var(--primary); | |
| background: #f5f2eb; | |
| } | |
| .nav-links a.active { | |
| color: white; | |
| background: var(--primary); | |
| } | |
| /* ===== SEARCH ===== */ | |
| .search-container { | |
| background: var(--card-bg); | |
| padding: 22px 26px; | |
| border-radius: 12px; | |
| border: 1px solid var(--border-medium); | |
| box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); | |
| margin-bottom: 32px; | |
| display: flex; | |
| gap: 14px; | |
| } | |
| input[type="text"] { | |
| flex: 1; | |
| padding: 13px 16px; | |
| border: 1.5px solid var(--border-soft); | |
| border-radius: 8px; | |
| font-size: 1.05rem; | |
| font-family: "Inter", system-ui, sans-serif; | |
| background: #fafafa; | |
| transition: all 0.2s ease; | |
| } | |
| input[type="text"]:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| background: white; | |
| box-shadow: 0 0 0 3px rgba(30, 77, 107, 0.08); | |
| } | |
| button { | |
| padding: 12px 28px; | |
| background: var(--primary); | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| font-family: "Inter", system-ui, sans-serif; | |
| font-size: 0.95rem; | |
| letter-spacing: 0.015em; | |
| transition: background 0.2s ease; | |
| } | |
| button:hover { | |
| background: #163a52; | |
| } | |
| /* ===== TEXT READER ===== */ | |
| .text-box { | |
| background: var(--card-bg); | |
| padding: 42px 48px; | |
| border-radius: 12px; | |
| border: 1px solid var(--border-medium); | |
| box-shadow: 0 2px 16px rgba(0, 0, 0, 0.05); | |
| font-size: 1.65em; | |
| line-height: 2.1em; | |
| font-family: "Crimson Pro", Georgia, serif; | |
| } | |
| /* Clickable words */ | |
| .word { | |
| cursor: pointer; | |
| color: var(--primary); | |
| border-bottom: 1.5px dotted var(--secondary); | |
| transition: all 0.15s ease; | |
| padding-bottom: 2px; | |
| } | |
| .word:hover { | |
| color: var(--accent); | |
| border-bottom-color: var(--accent); | |
| } | |
| /* ===== MODAL ===== */ | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(20, 20, 20, 0.55); | |
| backdrop-filter: blur(6px); | |
| z-index: 1000; | |
| } | |
| .modal-content { | |
| background: var(--bg); | |
| margin: 5vh auto; | |
| padding: 32px; | |
| width: 92%; | |
| max-width: 760px; | |
| border-radius: 20px; | |
| position: relative; | |
| max-height: 85vh; | |
| overflow-y: auto; | |
| box-shadow: 0 30px 80px rgba(0, 0, 0, 0.25); | |
| } | |
| .close-btn { | |
| position: absolute; | |
| right: 22px; | |
| top: 18px; | |
| font-size: 26px; | |
| color: var(--muted); | |
| cursor: pointer; | |
| } | |
| /* ===== MEANINGS ===== */ | |
| .meaning-card { | |
| background: white; | |
| border-radius: 10px; | |
| padding: 28px 32px; | |
| margin-bottom: 28px; | |
| border: 1px solid var(--border-medium); | |
| box-shadow: 0 1px 8px rgba(0, 0, 0, 0.04); | |
| } | |
| .meaning-card:last-child { | |
| margin-bottom: 0; | |
| } | |
| .label-pill { | |
| display: inline-block; | |
| background: var(--primary); | |
| color: white; | |
| padding: 6px 14px; | |
| border-radius: 6px; | |
| font-size: 0.7em; | |
| font-weight: 700; | |
| letter-spacing: 0.04em; | |
| text-transform: uppercase; | |
| font-family: "Inter", system-ui, sans-serif; | |
| margin-bottom: 16px; | |
| } | |
| .dict-title { | |
| display: block; | |
| margin-top: 1.6em; | |
| margin-bottom: 0.5em; | |
| padding-top: 1.2em; | |
| border-top: 2px solid var(--entry-border); | |
| font-weight: 700; | |
| font-size: 1.1em; | |
| color: #2d3748; | |
| font-family: "Inter", system-ui, sans-serif; | |
| letter-spacing: -0.01em; | |
| } | |
| .dict-title:first-of-type { | |
| margin-top: 0.8em; | |
| padding-top: 0; | |
| border-top: none; | |
| } | |
| /* ===== DICTIONARY CONTENT ===== */ | |
| .def-content { | |
| margin-top: 12px; | |
| max-width: 75ch; | |
| color: #2d3748; | |
| line-height: 1.75; | |
| font-family: "Crimson Pro", Georgia, serif; | |
| } | |
| /* Critical normalization */ | |
| .def-content, | |
| .def-content * { | |
| font-size: 1.05rem ; | |
| line-height: 1.75; | |
| } | |
| /* Sanskrit */ | |
| .def-content .skt { | |
| font-family: "Noto Serif Devanagari", "Sanskrit 2003", serif; | |
| font-size: 1.15em ; | |
| font-weight: 600; | |
| color: #1a1a1a; | |
| } | |
| /* Abbreviations */ | |
| .def-content .abbr, | |
| .def-content abbr { | |
| font-size: 0.9em ; | |
| font-weight: 600; | |
| color: #4a5568; | |
| font-family: "Inter", system-ui, sans-serif; | |
| } | |
| /* Metadata */ | |
| .def-content .meta { | |
| font-size: 0.92em ; | |
| color: var(--muted); | |
| font-style: italic; | |
| } | |
| /* Language markers */ | |
| .def-content .lang { | |
| font-style: italic; | |
| font-size: 0.98em ; | |
| color: #4a5568; | |
| } | |
| /* POS (Part of Speech) */ | |
| .def-content .pos { | |
| font-weight: 700; | |
| color: #2d3748; | |
| font-family: "Inter", system-ui, sans-serif; | |
| font-size: 0.95em ; | |
| } | |
| /* Sources */ | |
| .def-content .source { | |
| font-size: 0.92em ; | |
| color: #718096; | |
| font-style: italic; | |
| } | |
| /* Superscripts */ | |
| .def-content sup { | |
| font-size: 0.7em ; | |
| vertical-align: super; | |
| line-height: 0; | |
| } | |
| /* Prevent nesting collapse */ | |
| .def-content span span span { | |
| font-size: inherit ; | |
| } | |
| /* List items within definitions */ | |
| .def-content ol, | |
| .def-content ul { | |
| margin: 0.8em 0; | |
| padding-left: 1.8em; | |
| } | |
| .def-content li { | |
| margin: 0.4em 0; | |
| line-height: 1.7; | |
| } | |
| /* Emphasis */ | |
| .def-content em, | |
| .def-content i { | |
| font-style: italic; | |
| color: #2d3748; | |
| } | |
| .def-content strong, | |
| .def-content b { | |
| font-weight: 700; | |
| color: #1a1a1a; | |
| } | |
| /* ===== COMPOUND UI ===== */ | |
| .split-container { | |
| display: flex; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| gap: 12px; | |
| margin: 22px 0; | |
| } | |
| .part-btn { | |
| background: white; | |
| border: 1.5px solid var(--primary); | |
| color: var(--primary); | |
| padding: 8px 18px; | |
| border-radius: 999px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| font-size: 0.95em; | |
| } | |
| .part-btn.active { | |
| background: var(--primary); | |
| color: white; | |
| } | |
| /* Mobile responsive */ | |
| @media (max-width: 640px) { | |
| .nav-container { | |
| flex-direction: column; | |
| height: auto; | |
| padding: 16px 24px; | |
| gap: 12px; | |
| } | |
| .nav-brand { | |
| font-size: 1.2rem; | |
| } | |
| .nav-links { | |
| width: 100%; | |
| justify-content: center; | |
| } | |
| .nav-links a { | |
| font-size: 0.9rem; | |
| padding: 6px 16px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- NAVBAR --> | |
| <nav> | |
| <div class="nav-container"> | |
| <a href="index.html" class="nav-brand">Sanskrit Smart Reader</a> | |
| <div class="nav-links"> | |
| <a href="index.html" class="active">Home</a> | |
| <a href="./frontend//html/about.html">About</a> | |
| <a href="./frontend//html/text_parser.html">Parser</a> | |
| </div> | |
| </div> | |
| </nav> | |
| <!-- MAIN CONTENT --> | |
| <div class="container"> | |
| <div class="search-container"> | |
| <input type="text" id="search-input" placeholder="Search word..." /> | |
| <button onclick="handleSearch()">Search</button> | |
| </div> | |
| <div class="text-box" id="reader"> | |
| तस्मात्त्वमुत्तिष्ठ यशो लभस्व जित्वा शत्रून्भुङ्क्ष्व राज्यं समृद्धम् । | |
| </div> | |
| <div id="page-results"></div> | |
| </div> | |
| <div id="wordModal" class="modal"> | |
| <div class="modal-content"> | |
| <span class="close-btn" onclick="closeModal()">×</span> | |
| <div id="modal-body"></div> | |
| </div> | |
| </div> | |
| <script> | |
| const base_api = "https://psugam-sanskrit-parser-api.hf.space/"; | |
| console.log("Using API base:", base_api); | |
| const reader = document.getElementById("reader"); | |
| const modal = document.getElementById("wordModal"); | |
| const modalBody = document.getElementById("modal-body"); | |
| const pageResults = document.getElementById("page-results"); | |
| const searchInput = document.getElementById("search-input"); | |
| window.onclick = (e) => { | |
| if (e.target === modal) closeModal(); | |
| }; | |
| function closeModal() { | |
| modal.style.display = "none"; | |
| modalBody.innerHTML = ""; | |
| } | |
| function sanitizeHTML(str) { | |
| const div = document.createElement("div"); | |
| div.textContent = str; | |
| return div.innerHTML; | |
| } | |
| /* ===== SANITIZERS ===== */ | |
| function sanitizeAllowHTML(input) { | |
| // 1. Decode HTML entities FIRST | |
| const decoder = document.createElement("textarea"); | |
| decoder.innerHTML = input; | |
| let html = decoder.value; | |
| // 2. Normalize Apte / MW structural junk | |
| html = html | |
| .replace(/<div[^>]*\/>/gi, "<br>") | |
| .replace(/<lbinfo[^>]*\/>/gi, "") | |
| .replace(/<\/?meta[^>]*>/gi, "") | |
| .replace(/<vlex[^>]*\/>/gi, ""); | |
| // 3. Convert dictionary semantic tags → spans | |
| const replacements = { | |
| s: 'span class="skt"', | |
| s1: 'span class="skt"', | |
| ab: 'span class="abbr"', | |
| lex: 'span class="pos"', | |
| info: 'span class="meta"', | |
| ls: 'span class="source"', | |
| lang: 'span class="lang"', | |
| hom: "sup", | |
| }; | |
| for (const [tag, repl] of Object.entries(replacements)) { | |
| html = html.replace(new RegExp(`<${tag}[^>]*>`, "gi"), `<${repl}>`); | |
| html = html.replace( | |
| new RegExp(`</${tag}>`, "gi"), | |
| `</${repl.split(" ")[0]}>` | |
| ); | |
| } | |
| // 4. REMOVE unknown tags but KEEP their content | |
| html = html.replace( | |
| /<(?!\/?(br|b|i|em|strong|sup|sub|span)\b)[^>]+>/gi, | |
| "" | |
| ); | |
| return html; | |
| } | |
| function sanitizeDictHTML(html) { | |
| return html | |
| .replace(/<vlex[^>]*\/>/gi, "") | |
| .replace(/<\/?meta[^>]*>/gi, "") | |
| .replace(/<\/?gk>|<\/?etym>|<\/?tib>/gi, "") | |
| .replace(/<\/span>\s*<\/span>\s*<\/span>/g, "</span>"); | |
| } | |
| /* ===== TEXT CLICKABLE WORDS ===== */ | |
| reader.innerHTML = reader.innerText | |
| .trim() | |
| .split(/(\s+|।)/) | |
| .map((w) => { | |
| const clean = w.replace(/[।\s]/g, ""); | |
| return clean | |
| ? `<span class="word" onclick="initiateProcess('${sanitizeHTML( | |
| clean | |
| )}', true)">${sanitizeHTML(w)}</span>` | |
| : sanitizeHTML(w); | |
| }) | |
| .join(""); | |
| function handleSearch() { | |
| const word = searchInput.value.trim(); | |
| if (word) initiateProcess(word, false); | |
| } | |
| async function initiateProcess(word, isPopup) { | |
| const target = isPopup ? modalBody : pageResults; | |
| target.innerHTML = `<p>Analyzing <b>${sanitizeHTML(word)}</b>…</p>`; | |
| if (isPopup) modal.style.display = "block"; | |
| const res = await fetch( | |
| `${base_api}/split?word=${encodeURIComponent(word)}` | |
| ); | |
| const data = await res.json(); | |
| if (data.is_compound) renderCompoundUI(word, data.components, target); | |
| else renderDirectMeaning(word, target); | |
| } | |
| function renderCompoundUI(word, parts, target) { | |
| target.innerHTML = ` | |
| <h3>${sanitizeHTML(word)}</h3> | |
| <div class="split-container"> | |
| ${parts | |
| .map( | |
| (p) => | |
| `<button class="part-btn" onclick="fetchPartMeaning('${sanitizeHTML( | |
| p | |
| )}','compound-res',this)">${sanitizeHTML(p)}</button>` | |
| ) | |
| .join("")} | |
| </div> | |
| <div id="compound-res"></div>`; | |
| } | |
| async function renderDirectMeaning(word, target) { | |
| target.innerHTML = `<h3>${sanitizeHTML( | |
| word | |
| )}</h3><div id="direct-res"></div>`; | |
| fetchPartMeaning(word, "direct-res"); | |
| } | |
| async function fetchPartMeaning(word, id, btn = null) { | |
| if (btn) { | |
| btn.parentElement | |
| .querySelectorAll(".part-btn") | |
| .forEach((b) => b.classList.remove("active")); | |
| btn.classList.add("active"); | |
| } | |
| const display = document.getElementById(id); | |
| display.innerHTML = "Fetching dictionary…"; | |
| const res = await fetch( | |
| `${base_api}/meaning?word=${encodeURIComponent(word)}` | |
| ); | |
| const data = await res.json(); | |
| const sourceMap = { | |
| mw: "Monier-Williams", | |
| ap90: "Apte", | |
| cae: "Cappeller", | |
| bhs: "Buddhist", | |
| }; | |
| display.innerHTML = data | |
| .map((e) => { | |
| // Format the grammar tags: e.g., "Nom, Sg | Acc, Sg" | |
| const tags = e.detected_tags.map((t) => t.join(", ")).join(" | "); | |
| return ` | |
| <div class="meaning-card"> | |
| <span class="label-pill">${e.type}</span> | |
| <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;"> | |
| ${sanitizeHTML(tags)} | |
| </div> | |
| <div style="margin-bottom: 10px;"><b>Stem:</b> ${sanitizeHTML( | |
| e.stem | |
| )}</div> | |
| ${Object.entries(e.definitions) | |
| .map( | |
| ([src, defs]) => ` | |
| <span class="dict-title">${sourceMap[src] || src}</span> | |
| <div class="def-content"> | |
| ${defs.map((d) => `• ${sanitizeAllowHTML(d)}`).join("<br>")} | |
| </div>` | |
| ) | |
| .join("")} | |
| </div>`; | |
| }) | |
| .join(""); | |
| } | |
| </script> | |
| </body> | |
| </html> | |