sanskrit-parser-api / index.html
psugam's picture
Upload 4 files
e33019a verified
<!DOCTYPE html>
<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 !important;
line-height: 1.75;
}
/* Sanskrit */
.def-content .skt {
font-family: "Noto Serif Devanagari", "Sanskrit 2003", serif;
font-size: 1.15em !important;
font-weight: 600;
color: #1a1a1a;
}
/* Abbreviations */
.def-content .abbr,
.def-content abbr {
font-size: 0.9em !important;
font-weight: 600;
color: #4a5568;
font-family: "Inter", system-ui, sans-serif;
}
/* Metadata */
.def-content .meta {
font-size: 0.92em !important;
color: var(--muted);
font-style: italic;
}
/* Language markers */
.def-content .lang {
font-style: italic;
font-size: 0.98em !important;
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 !important;
}
/* Sources */
.def-content .source {
font-size: 0.92em !important;
color: #718096;
font-style: italic;
}
/* Superscripts */
.def-content sup {
font-size: 0.7em !important;
vertical-align: super;
line-height: 0;
}
/* Prevent nesting collapse */
.def-content span span span {
font-size: inherit !important;
}
/* 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()">&times;</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>