| --- |
| layout: null |
| title: Pashto Resource Search |
| description: Search verified Pashto (Pukhto/Pashto) datasets, models, tools, benchmarks, papers, and projects. |
| permalink: /search/ |
| --- |
| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1"> |
| <title>Pashto Resource Search</title> |
| <meta name="description" content="Search verified Pashto (Pukhto/Pashto) resources for Automatic Speech Recognition (ASR), Text-to-Speech (TTS), Natural Language Processing (NLP), machine translation, tools, datasets, models, and benchmarks."> |
| <meta name="keywords" content="Pashto resources, Pukhto resources, Pashto datasets, Pashto ASR, Pashto TTS, Pashto NLP, Pashto machine translation, Pashto benchmarks"> |
| <meta name="robots" content="index,follow,max-image-preview:large"> |
| <link rel="canonical" href="https://musawer1214.github.io/pashto-language-resources/search/"> |
| <meta property="og:type" content="website"> |
| <meta property="og:site_name" content="Pashto Language Resources Hub"> |
| <meta property="og:title" content="Pashto Resource Search"> |
| <meta property="og:description" content="Discover verified Pashto datasets, models, tools, benchmarks, projects, and papers."> |
| <meta property="og:url" content="https://musawer1214.github.io/pashto-language-resources/search/"> |
| <meta name="twitter:card" content="summary"> |
| <meta name="twitter:title" content="Pashto Resource Search"> |
| <meta name="twitter:description" content="Search and filter verified Pashto language technology resources."> |
| <script type="application/ld+json"> |
| { |
| "@context": "https://schema.org", |
| "@type": "CollectionPage", |
| "name": "Pashto Resource Search", |
| "url": "https://musawer1214.github.io/pashto-language-resources/search/", |
| "description": "Search verified and candidate Pashto resources for Automatic Speech Recognition (ASR), Text-to-Speech (TTS), Natural Language Processing (NLP), machine translation, tools, datasets, and benchmarks.", |
| "inLanguage": "en", |
| "about": [ |
| "Pashto datasets", |
| "Pashto ASR", |
| "Pashto TTS", |
| "Pashto NLP", |
| "Pashto machine translation" |
| ] |
| } |
| </script> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&family=Noto+Naskh+Arabic:wght@500;700&display=swap" rel="stylesheet"> |
| <style> |
| :root { |
| --bg: #f3f2ea; |
| --panel: #fffdf7; |
| --ink: #1d2628; |
| --muted: #55656a; |
| --line: #d5ddd8; |
| --brand: #0f6f61; |
| --brand-soft: #dcf2ee; |
| --accent: #b9621f; |
| --accent-soft: #f8e6d7; |
| --shadow: 0 14px 36px rgba(16, 29, 35, 0.12); |
| --radius-lg: 18px; |
| --radius-md: 12px; |
| --radius-sm: 9px; |
| } |
| |
| * { box-sizing: border-box; } |
| |
| body { |
| margin: 0; |
| color: var(--ink); |
| font-family: "Manrope", "Segoe UI", sans-serif; |
| min-height: 100vh; |
| background: |
| radial-gradient(circle at 8% 8%, #ffe0bf 0, rgba(255, 224, 191, 0) 30%), |
| radial-gradient(circle at 92% 7%, #d2ece4 0, rgba(210, 236, 228, 0) 35%), |
| var(--bg); |
| } |
| |
| a { color: var(--brand); } |
| |
| .container { |
| max-width: 1180px; |
| margin: 0 auto; |
| padding: 22px 18px 42px; |
| } |
| |
| .hero { |
| background: linear-gradient(130deg, #fffef9, #f5faf8); |
| border: 1px solid var(--line); |
| border-radius: var(--radius-lg); |
| box-shadow: var(--shadow); |
| padding: 20px; |
| margin-bottom: 14px; |
| } |
| |
| .hero-top { |
| display: flex; |
| justify-content: space-between; |
| align-items: flex-start; |
| gap: 14px; |
| flex-wrap: wrap; |
| } |
| |
| .eyebrow { |
| margin: 0 0 8px; |
| font-size: 12px; |
| letter-spacing: 0.08em; |
| text-transform: uppercase; |
| color: var(--accent); |
| font-weight: 800; |
| } |
| |
| h1 { |
| margin: 0; |
| font-size: 34px; |
| line-height: 1.08; |
| letter-spacing: -0.02em; |
| } |
| |
| .subtitle { |
| margin: 10px 0 0; |
| color: var(--muted); |
| line-height: 1.55; |
| max-width: 760px; |
| } |
| |
| .stat-row { |
| display: grid; |
| grid-template-columns: repeat(3, minmax(120px, 1fr)); |
| gap: 8px; |
| width: min(380px, 100%); |
| } |
| |
| .stat { |
| border: 1px solid var(--line); |
| border-radius: var(--radius-md); |
| background: var(--panel); |
| padding: 10px; |
| text-align: center; |
| } |
| |
| .stat strong { |
| display: block; |
| font-size: 20px; |
| line-height: 1; |
| color: var(--brand); |
| } |
| |
| .stat span { |
| display: block; |
| margin-top: 4px; |
| color: var(--muted); |
| font-size: 12px; |
| font-weight: 700; |
| text-transform: uppercase; |
| letter-spacing: 0.04em; |
| } |
| |
| .quick-links { |
| margin-top: 14px; |
| display: flex; |
| flex-wrap: wrap; |
| gap: 8px; |
| } |
| |
| .pill-link { |
| border: 1px solid #b8cec6; |
| border-radius: 999px; |
| background: #f8fcfa; |
| color: var(--brand); |
| text-decoration: none; |
| font-size: 13px; |
| font-weight: 700; |
| padding: 7px 12px; |
| } |
| |
| .toolbar { |
| margin-bottom: 12px; |
| display: grid; |
| gap: 12px; |
| } |
| |
| .panel { |
| border: 1px solid var(--line); |
| border-radius: var(--radius-lg); |
| background: var(--panel); |
| box-shadow: var(--shadow); |
| padding: 16px; |
| } |
| |
| .controls { |
| display: grid; |
| grid-template-columns: 2fr repeat(5, minmax(140px, 1fr)); |
| gap: 10px; |
| } |
| |
| .field { |
| display: flex; |
| flex-direction: column; |
| gap: 5px; |
| } |
| |
| .field label { |
| font-size: 11px; |
| letter-spacing: 0.05em; |
| text-transform: uppercase; |
| font-weight: 800; |
| color: var(--muted); |
| } |
| |
| input, select { |
| width: 100%; |
| border: 1px solid var(--line); |
| border-radius: var(--radius-sm); |
| background: #fff; |
| color: var(--ink); |
| padding: 10px; |
| font: inherit; |
| } |
| |
| input:focus, select:focus, button:focus { |
| outline: 2px solid #7bc7b8; |
| outline-offset: 1px; |
| border-color: #7bc7b8; |
| } |
| |
| .action-row { |
| margin-top: 10px; |
| display: flex; |
| flex-wrap: wrap; |
| justify-content: space-between; |
| gap: 10px; |
| align-items: center; |
| } |
| |
| .group { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 7px; |
| align-items: center; |
| } |
| |
| .btn { |
| border: 1px solid var(--line); |
| background: #fff; |
| color: var(--ink); |
| border-radius: 999px; |
| padding: 7px 12px; |
| font-size: 13px; |
| font-weight: 700; |
| cursor: pointer; |
| } |
| |
| .btn:hover { border-color: #98b9ae; } |
| |
| .btn.brand { |
| background: var(--brand); |
| color: #fff; |
| border-color: var(--brand); |
| } |
| |
| .btn.light { |
| background: #f6faf8; |
| color: var(--brand); |
| border-color: #b9d8cf; |
| } |
| |
| .btn.active { |
| background: var(--accent-soft); |
| border-color: #efc08f; |
| color: #915117; |
| } |
| |
| .intent-row { |
| margin-top: 10px; |
| display: flex; |
| flex-wrap: wrap; |
| gap: 7px; |
| } |
| |
| .intent { |
| border: 1px solid #cfdad4; |
| background: #fbfdfc; |
| border-radius: 999px; |
| padding: 6px 11px; |
| font-size: 13px; |
| font-weight: 700; |
| cursor: pointer; |
| color: var(--muted); |
| } |
| |
| .intent:hover { border-color: #9cb8af; color: var(--ink); } |
| |
| .summary { |
| margin: 0 2px 12px; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| gap: 10px; |
| color: var(--muted); |
| font-size: 14px; |
| } |
| |
| .badge { |
| background: var(--brand-soft); |
| color: var(--brand); |
| border: 1px solid #b7dccc; |
| padding: 4px 9px; |
| border-radius: 999px; |
| font-weight: 700; |
| font-size: 12px; |
| } |
| |
| .results { |
| list-style: none; |
| margin: 0; |
| padding: 0; |
| display: grid; |
| gap: 12px; |
| } |
| |
| .results.grid { grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); } |
| .results.list { grid-template-columns: 1fr; } |
| |
| .card { |
| border: 1px solid var(--line); |
| border-radius: var(--radius-md); |
| background: var(--panel); |
| box-shadow: var(--shadow); |
| padding: 13px; |
| display: flex; |
| flex-direction: column; |
| gap: 9px; |
| animation: cardIn 360ms ease; |
| } |
| |
| .chips { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 6px; |
| } |
| |
| .chip { |
| border-radius: 999px; |
| font-size: 10px; |
| font-weight: 800; |
| letter-spacing: 0.04em; |
| text-transform: uppercase; |
| padding: 3px 8px; |
| border: 1px solid transparent; |
| } |
| |
| .chip.category { background: var(--brand-soft); color: var(--brand); border-color: #b7dccc; } |
| .chip.source { background: var(--accent-soft); color: #915117; border-color: #efc89f; } |
| .chip.status { background: #edf2ff; color: #3f4f86; border-color: #d0dbf6; } |
| |
| .title { |
| margin: 0; |
| font-size: 17px; |
| line-height: 1.3; |
| overflow-wrap: anywhere; |
| word-break: break-word; |
| } |
| |
| .title a { |
| color: var(--ink); |
| text-decoration: none; |
| border-bottom: 1px solid transparent; |
| display: inline-block; |
| max-width: 100%; |
| overflow-wrap: anywhere; |
| word-break: break-word; |
| } |
| |
| .title a:hover { border-bottom-color: currentColor; } |
| |
| .text { |
| margin: 0; |
| color: var(--muted); |
| line-height: 1.48; |
| font-size: 13px; |
| overflow-wrap: anywhere; |
| word-break: break-word; |
| } |
| |
| .meta-links { |
| margin-top: auto; |
| display: flex; |
| flex-wrap: wrap; |
| gap: 8px; |
| align-items: center; |
| } |
| |
| .mini-link { |
| font-size: 12px; |
| font-weight: 700; |
| color: var(--brand); |
| text-decoration: none; |
| display: inline-block; |
| max-width: 100%; |
| overflow-wrap: anywhere; |
| word-break: break-word; |
| } |
| |
| .tasks { |
| font-size: 12px; |
| color: var(--muted); |
| margin: 0; |
| overflow-wrap: anywhere; |
| word-break: break-word; |
| } |
| |
| .empty { |
| border: 1px dashed #b9c4bd; |
| border-radius: var(--radius-md); |
| padding: 26px; |
| background: #fcfcfa; |
| color: var(--muted); |
| text-align: center; |
| } |
| |
| .crawl { |
| margin: 0 2px 12px; |
| color: var(--muted); |
| line-height: 1.5; |
| font-size: 14px; |
| } |
| |
| .crawl ul { |
| margin: 8px 0 0; |
| padding-left: 18px; |
| } |
| |
| @keyframes cardIn { |
| from { opacity: 0; transform: translateY(5px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| @media (max-width: 1020px) { |
| .controls { grid-template-columns: repeat(3, minmax(140px, 1fr)); } |
| .controls .field.search { grid-column: span 3; } |
| } |
| |
| @media (max-width: 720px) { |
| h1 { font-size: 30px; } |
| .controls { grid-template-columns: 1fr 1fr; } |
| .controls .field.search { grid-column: span 2; } |
| .summary { flex-direction: column; align-items: flex-start; } |
| .stat-row { width: 100%; } |
| } |
| |
| @media (max-width: 520px) { |
| .controls { grid-template-columns: 1fr; } |
| .controls .field.search { grid-column: span 1; } |
| h1 { font-size: 28px; } |
| } |
| </style> |
| </head> |
| <body> |
| <main class="container"> |
| <header class="hero"> |
| <div class="hero-top"> |
| <div> |
| <p class="eyebrow">Pukhto / Pashto Resources</p> |
| <h1>Pashto Technology Resource Search</h1> |
| <p class="subtitle"> |
| Filter verified and candidate resources for Automatic Speech Recognition (ASR), Text-to-Speech (TTS), Natural Language Processing (NLP), translation, tools, and benchmark work. |
| Share search states with URL parameters and jump quickly to high-intent pages. |
| </p> |
| </div> |
| <div class="stat-row" aria-label="Catalog metrics"> |
| <div class="stat"><strong id="statTotal">-</strong><span>Total</span></div> |
| <div class="stat"><strong id="statVerified">-</strong><span>Verified</span></div> |
| <div class="stat"><strong id="statTasks">-</strong><span>Task Tags</span></div> |
| </div> |
| </div> |
| <nav class="quick-links" aria-label="Quick links"> |
| <a class="pill-link" href="../">Docs Home</a> |
| <a class="pill-link" href="../pashto_datasets.html">Datasets Page</a> |
| <a class="pill-link" href="../pashto_asr.html">ASR Page</a> |
| <a class="pill-link" href="../pashto_tts.html">TTS Page</a> |
| <a class="pill-link" href="https://github.com/Musawer1214/pashto-language-resources">GitHub</a> |
| <a class="pill-link" href="https://huggingface.co/Musawer14/pashto-language-resources">Hugging Face</a> |
| </nav> |
| </header> |
|
|
| <section class="toolbar panel" aria-label="Search controls"> |
| <div class="controls"> |
| <div class="field search"> |
| <label for="q">Search</label> |
| <input id="q" type="search" placeholder="Try: Automatic Speech Recognition, PBT_Arab, Translation, Speech, Benchmark" autocomplete="off"> |
| </div> |
| <div class="field"> |
| <label for="category">Category</label> |
| <select id="category"></select> |
| </div> |
| <div class="field"> |
| <label for="source">Source</label> |
| <select id="source"></select> |
| </div> |
| <div class="field"> |
| <label for="task">Task</label> |
| <select id="task"></select> |
| </div> |
| <div class="field"> |
| <label for="status">Status</label> |
| <select id="status"></select> |
| </div> |
| <div class="field"> |
| <label for="sort">Sort</label> |
| <select id="sort"> |
| <option value="relevance">Relevance</option> |
| <option value="title_asc">Title (A-Z)</option> |
| <option value="title_desc">Title (Z-A)</option> |
| <option value="source_asc">Source (A-Z)</option> |
| </select> |
| </div> |
| </div> |
|
|
| <div class="action-row"> |
| <div class="group" role="group" aria-label="Result view mode"> |
| <button type="button" class="btn active" id="viewGrid">Grid View</button> |
| <button type="button" class="btn" id="viewList">List View</button> |
| </div> |
| <div class="group"> |
| <button type="button" class="btn light" id="clearFilters">Clear Filters</button> |
| <button type="button" class="btn brand" id="copyLink">Copy Search Link</button> |
| </div> |
| </div> |
|
|
| <div class="intent-row" aria-label="Quick intent filters"> |
| <button type="button" class="intent" data-intent="datasets">Datasets</button> |
| <button type="button" class="intent" data-intent="asr">ASR</button> |
| <button type="button" class="intent" data-intent="tts">TTS</button> |
| <button type="button" class="intent" data-intent="nlp">NLP</button> |
| <button type="button" class="intent" data-intent="mt">Translation</button> |
| <button type="button" class="intent" data-intent="verified">Verified Only</button> |
| </div> |
| </section> |
|
|
| <div class="summary" aria-live="polite"> |
| <span id="countText">Loading resources...</span> |
| <span class="badge" id="generatedAt">Catalog timestamp: -</span> |
| </div> |
|
|
| <section class="crawl" aria-label="Resource categories and crawlable links"> |
| Browse Pashto resource categories directly: |
| <ul> |
| <li><a href="https://github.com/Musawer1214/pashto-language-resources/blob/main/resources/datasets/README.md">Pashto datasets</a></li> |
| <li><a href="https://github.com/Musawer1214/pashto-language-resources/blob/main/resources/models/README.md">Pashto models</a></li> |
| <li><a href="https://github.com/Musawer1214/pashto-language-resources/blob/main/resources/benchmarks/README.md">Pashto benchmarks</a></li> |
| <li><a href="https://github.com/Musawer1214/pashto-language-resources/blob/main/resources/tools/README.md">Pashto tools</a></li> |
| <li><a href="https://github.com/Musawer1214/pashto-language-resources/blob/main/resources/papers/README.md">Pashto papers</a></li> |
| </ul> |
| </section> |
|
|
| <noscript> |
| JavaScript is needed for filtering. Category links above remain fully accessible. |
| </noscript> |
|
|
| <ul id="results" class="results grid"></ul> |
| </main> |
|
|
| <script> |
| const state = { |
| all: [], |
| filtered: [], |
| view: "grid" |
| }; |
| |
| const els = { |
| q: document.getElementById("q"), |
| category: document.getElementById("category"), |
| source: document.getElementById("source"), |
| task: document.getElementById("task"), |
| status: document.getElementById("status"), |
| sort: document.getElementById("sort"), |
| results: document.getElementById("results"), |
| countText: document.getElementById("countText"), |
| generatedAt: document.getElementById("generatedAt"), |
| statTotal: document.getElementById("statTotal"), |
| statVerified: document.getElementById("statVerified"), |
| statTasks: document.getElementById("statTasks"), |
| viewGrid: document.getElementById("viewGrid"), |
| viewList: document.getElementById("viewList"), |
| clearFilters: document.getElementById("clearFilters"), |
| copyLink: document.getElementById("copyLink"), |
| intents: [...document.querySelectorAll(".intent")] |
| }; |
| |
| const TASK_LABELS = { |
| "asr": "Automatic Speech Recognition (ASR)", |
| "tts": "Text-to-Speech (TTS)", |
| "nlp": "Natural Language Processing (NLP)", |
| "mt": "Machine Translation (MT)", |
| "llm": "Large Language Model (LLM)", |
| "ocr": "Optical Character Recognition (OCR)", |
| "pos-tagging": "Part-of-Speech Tagging", |
| "benchmarking": "Benchmarking", |
| "classification": "Classification", |
| "demo": "Demo", |
| "dictionary": "Dictionary", |
| "research": "Research", |
| "sentiment": "Sentiment Analysis", |
| "tooling": "Tooling", |
| "translation": "Translation" |
| }; |
| |
| const CATEGORY_LABELS = { |
| "dataset": "Dataset", |
| "model": "Model", |
| "benchmark": "Benchmark", |
| "tool": "Tool", |
| "paper": "Paper", |
| "project": "Project", |
| "code": "Code" |
| }; |
| |
| const SOURCE_LABELS = { |
| "huggingface": "Hugging Face", |
| "mozilla": "Mozilla", |
| "kaggle": "Kaggle", |
| "github": "GitHub", |
| "gitlab": "GitLab", |
| "arxiv": "arXiv", |
| "openalex": "OpenAlex", |
| "crossref": "Crossref", |
| "zenodo": "Zenodo", |
| "dataverse": "Dataverse", |
| "datacite": "DataCite", |
| "meta": "Meta", |
| "other": "Other" |
| }; |
| |
| const STATUS_LABELS = { |
| "verified": "Verified", |
| "candidate": "Candidate" |
| }; |
| |
| function toTitleCase(value) { |
| return String(value || "") |
| .replace(/[_-]+/g, " ") |
| .split(/\s+/) |
| .filter(Boolean) |
| .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) |
| .join(" "); |
| } |
| |
| function replaceAbbreviation(text, shortForm, expanded) { |
| const fullPhrase = expanded.replace(/\s*\([^)]*\)\s*$/, ""); |
| if (new RegExp(fullPhrase, "i").test(text)) { |
| return text; |
| } |
| return text.replace(new RegExp(`\\b${shortForm}\\b`, "gi"), expanded); |
| } |
| |
| function formatDisplayText(value) { |
| let text = String(value || "").trim(); |
| if (!text) return text; |
| |
| text = text |
| .replace(/\bhuggingface\b/gi, "Hugging Face") |
| .replace(/\bgithub\b/gi, "GitHub") |
| .replace(/\bgitlab\b/gi, "GitLab") |
| .replace(/\bopenalex\b/gi, "OpenAlex") |
| .replace(/\barxiv\b/gi, "arXiv"); |
| |
| text = replaceAbbreviation(text, "ASR", "Automatic Speech Recognition (ASR)"); |
| text = replaceAbbreviation(text, "TTS", "Text-to-Speech (TTS)"); |
| text = replaceAbbreviation(text, "NLP", "Natural Language Processing (NLP)"); |
| text = replaceAbbreviation(text, "MT", "Machine Translation (MT)"); |
| text = replaceAbbreviation(text, "LLM", "Large Language Model (LLM)"); |
| text = replaceAbbreviation(text, "OCR", "Optical Character Recognition (OCR)"); |
| |
| return text.charAt(0).toUpperCase() + text.slice(1); |
| } |
| |
| function formatTaskLabel(value) { |
| const key = String(value || "").toLowerCase(); |
| return TASK_LABELS[key] || toTitleCase(value); |
| } |
| |
| function formatCategoryLabel(value) { |
| const key = String(value || "").toLowerCase(); |
| return CATEGORY_LABELS[key] || toTitleCase(value); |
| } |
| |
| function formatSourceLabel(value) { |
| const key = String(value || "").toLowerCase(); |
| return SOURCE_LABELS[key] || toTitleCase(value); |
| } |
| |
| function formatStatusLabel(value) { |
| const key = String(value || "").toLowerCase(); |
| return STATUS_LABELS[key] || toTitleCase(value); |
| } |
| |
| function esc(value) { |
| return String(value || "") |
| .replace(/&/g, "&") |
| .replace(/</g, "<") |
| .replace(/>/g, ">") |
| .replace(/"/g, """) |
| .replace(/'/g, "'"); |
| } |
| |
| function uniqSorted(values) { |
| return [...new Set(values.filter(Boolean))].sort((a, b) => a.localeCompare(b)); |
| } |
| |
| function fillSelect(select, options, allLabel, labelFormatter = toTitleCase) { |
| select.innerHTML = ""; |
| const allOption = document.createElement("option"); |
| allOption.value = ""; |
| allOption.textContent = allLabel; |
| select.appendChild(allOption); |
| for (const opt of options) { |
| const entry = document.createElement("option"); |
| entry.value = opt; |
| entry.textContent = labelFormatter(opt); |
| select.appendChild(entry); |
| } |
| } |
| |
| function updateMetrics() { |
| const verified = state.all.filter((r) => r.status === "verified").length; |
| const tasks = uniqSorted(state.all.flatMap((r) => r.tasks || [])).length; |
| els.statTotal.textContent = String(state.all.length); |
| els.statVerified.textContent = String(verified); |
| els.statTasks.textContent = String(tasks); |
| } |
| |
| function setView(view) { |
| state.view = view === "list" ? "list" : "grid"; |
| els.results.classList.toggle("grid", state.view === "grid"); |
| els.results.classList.toggle("list", state.view === "list"); |
| els.viewGrid.classList.toggle("active", state.view === "grid"); |
| els.viewList.classList.toggle("active", state.view === "list"); |
| syncUrl(); |
| } |
| |
| function matchesQuery(resource, query) { |
| if (!query) return true; |
| const hay = [ |
| resource.title, |
| resource.summary, |
| resource.primary_use, |
| resource.category, |
| resource.source, |
| resource.status, |
| ...(resource.tags || []), |
| ...(resource.tasks || []), |
| ...(resource.markers || []), |
| resource.evidence_text |
| ].join(" ").toLowerCase(); |
| return hay.includes(query); |
| } |
| |
| function sortItems(items) { |
| const mode = els.sort.value; |
| const sorted = [...items]; |
| if (mode === "title_asc") { |
| sorted.sort((a, b) => (a.title || "").localeCompare(b.title || "")); |
| } else if (mode === "title_desc") { |
| sorted.sort((a, b) => (b.title || "").localeCompare(a.title || "")); |
| } else if (mode === "source_asc") { |
| sorted.sort((a, b) => (a.source || "").localeCompare(b.source || "") || (a.title || "").localeCompare(b.title || "")); |
| } else { |
| sorted.sort((a, b) => { |
| const av = a.status === "verified" ? 0 : 1; |
| const bv = b.status === "verified" ? 0 : 1; |
| if (av !== bv) return av - bv; |
| return (a.title || "").localeCompare(b.title || ""); |
| }); |
| } |
| return sorted; |
| } |
| |
| function applyFilters() { |
| const q = els.q.value.trim().toLowerCase(); |
| const category = els.category.value; |
| const source = els.source.value; |
| const task = els.task.value; |
| const status = els.status.value; |
| |
| state.filtered = sortItems(state.all.filter((resource) => { |
| if (!matchesQuery(resource, q)) return false; |
| if (category && resource.category !== category) return false; |
| if (source && resource.source !== source) return false; |
| if (status && resource.status !== status) return false; |
| if (task && !(resource.tasks || []).includes(task)) return false; |
| return true; |
| })); |
| |
| renderResults(); |
| syncUrl(); |
| } |
| |
| function chip(label, cls) { |
| return `<span class="chip ${cls}">${esc(label)}</span>`; |
| } |
| |
| function renderResults() { |
| const items = state.filtered; |
| els.countText.textContent = `${items.length} result${items.length === 1 ? "" : "s"} of ${state.all.length}`; |
| |
| if (!items.length) { |
| els.results.innerHTML = '<li class="empty">No Matches. Try Broadening Filters or Clearing One Filter at a Time.</li>'; |
| return; |
| } |
| |
| els.results.innerHTML = items.map((resource) => { |
| const title = esc(resource.title || "Untitled resource"); |
| const url = esc(resource.url || "#"); |
| const summary = esc(formatDisplayText(resource.summary || "No summary provided.")); |
| const primaryUse = esc(formatDisplayText(resource.primary_use || "Not available")); |
| const evidenceText = esc(formatDisplayText(resource.evidence_text || "Evidence link")); |
| const evidenceUrl = esc(resource.evidence_url || resource.url || "#"); |
| const tasks = (resource.tasks || []).map((t) => esc(formatTaskLabel(t))).join(", ") || "Not available"; |
| |
| return ` |
| <li class="card"> |
| <div class="chips"> |
| ${chip(formatCategoryLabel(resource.category || "unknown"), "category")} |
| ${chip(formatSourceLabel(resource.source || "unknown"), "source")} |
| ${chip(formatStatusLabel(resource.status || "unknown"), "status")} |
| </div> |
| <h2 class="title"><a href="${url}" target="_blank" rel="noreferrer">${title}</a></h2> |
| <p class="text">${summary}</p> |
| <p class="text"><strong>Primary Use:</strong> ${primaryUse}</p> |
| <p class="tasks"><strong>Tasks:</strong> ${tasks}</p> |
| <div class="meta-links"> |
| <a class="mini-link" href="${url}" target="_blank" rel="noreferrer">Open Resource</a> |
| <a class="mini-link" href="${evidenceUrl}" target="_blank" rel="noreferrer">Pashto Evidence: ${evidenceText}</a> |
| </div> |
| </li> |
| `; |
| }).join(""); |
| } |
| |
| function syncUrl() { |
| const params = new URLSearchParams(); |
| if (els.q.value.trim()) params.set("q", els.q.value.trim()); |
| if (els.category.value) params.set("category", els.category.value); |
| if (els.source.value) params.set("source", els.source.value); |
| if (els.task.value) params.set("task", els.task.value); |
| if (els.status.value) params.set("status", els.status.value); |
| if (els.sort.value && els.sort.value !== "relevance") params.set("sort", els.sort.value); |
| if (state.view !== "grid") params.set("view", state.view); |
| |
| const next = `${window.location.pathname}${params.toString() ? `?${params.toString()}` : ""}`; |
| window.history.replaceState({}, "", next); |
| } |
| |
| function applyUrlState() { |
| const params = new URLSearchParams(window.location.search); |
| if (params.get("q")) els.q.value = params.get("q"); |
| if (params.get("category")) els.category.value = params.get("category"); |
| if (params.get("source")) els.source.value = params.get("source"); |
| if (params.get("task")) els.task.value = params.get("task"); |
| if (params.get("status")) els.status.value = params.get("status"); |
| if (params.get("sort")) els.sort.value = params.get("sort"); |
| if (params.get("view")) setView(params.get("view")); |
| } |
| |
| function clearFilters() { |
| els.q.value = ""; |
| els.category.value = ""; |
| els.source.value = ""; |
| els.task.value = ""; |
| els.status.value = ""; |
| els.sort.value = "relevance"; |
| setView("grid"); |
| applyFilters(); |
| } |
| |
| function applyIntent(intent) { |
| if (intent === "datasets") { |
| els.category.value = "dataset"; |
| els.task.value = ""; |
| els.q.value = ""; |
| } else if (intent === "asr") { |
| els.task.value = "asr"; |
| } else if (intent === "tts") { |
| els.task.value = "tts"; |
| } else if (intent === "nlp") { |
| els.task.value = "nlp"; |
| } else if (intent === "mt") { |
| els.task.value = ""; |
| els.q.value = "translation"; |
| } else if (intent === "verified") { |
| els.status.value = "verified"; |
| } |
| applyFilters(); |
| } |
| |
| async function copySearchLink() { |
| try { |
| await navigator.clipboard.writeText(window.location.href); |
| els.copyLink.textContent = "Link Copied"; |
| setTimeout(() => { els.copyLink.textContent = "Copy Search Link"; }, 1200); |
| } catch (_error) { |
| els.copyLink.textContent = "Copy Failed"; |
| setTimeout(() => { els.copyLink.textContent = "Copy Search Link"; }, 1200); |
| } |
| } |
| |
| async function load() { |
| try { |
| const res = await fetch("./resources.json", { cache: "no-store" }); |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); |
| const payload = await res.json(); |
| |
| state.all = payload.resources || []; |
| |
| fillSelect(els.category, uniqSorted(state.all.map((r) => r.category)), "All Categories", formatCategoryLabel); |
| fillSelect(els.source, uniqSorted(state.all.map((r) => r.source)), "All Sources", formatSourceLabel); |
| fillSelect(els.status, uniqSorted(state.all.map((r) => r.status)), "All Statuses", formatStatusLabel); |
| fillSelect(els.task, uniqSorted(state.all.flatMap((r) => r.tasks || [])), "All Tasks", formatTaskLabel); |
| |
| updateMetrics(); |
| |
| const generated = payload.generated_on ? new Date(payload.generated_on) : null; |
| els.generatedAt.textContent = generated && !Number.isNaN(generated.getTime()) |
| ? `Catalog timestamp: ${generated.toISOString()}` |
| : "Catalog timestamp: unknown"; |
| |
| applyUrlState(); |
| applyFilters(); |
| } catch (err) { |
| els.countText.textContent = "Failed to load resources"; |
| els.results.innerHTML = `<li class="empty">Could not load search data. ${esc(String(err))}</li>`; |
| } |
| } |
| |
| [els.q, els.category, els.source, els.task, els.status, els.sort].forEach((el) => { |
| el.addEventListener("input", applyFilters); |
| el.addEventListener("change", applyFilters); |
| }); |
| |
| els.viewGrid.addEventListener("click", () => setView("grid")); |
| els.viewList.addEventListener("click", () => setView("list")); |
| els.clearFilters.addEventListener("click", clearFilters); |
| els.copyLink.addEventListener("click", copySearchLink); |
| els.intents.forEach((button) => { |
| button.addEventListener("click", () => applyIntent(button.dataset.intent)); |
| }); |
| |
| load(); |
| </script> |
| </body> |
| </html> |
|
|
|
|
|
|
|
|