{% extends "scitex_ui/standalone_shell.html" %} {% load static i18n %} {% comment %} Extends scitex-ui's shared workspace shell, so scholar renders inside the same frame as every other leaf (scitex-storage, scitex-writer, figrecipe) instead of shipping its own document. Block names -- extra_css / app_content / extra_js -- were verified against the INSTALLED scitex_ui in site-packages, not read off a source tree and not guessed. NO VERSION NUMBER HERE ON PURPOSE. This comment used to say "verified against the INSTALLED scitex_ui 0.17.0". The installed version moved to 0.18.0 and the sentence did not, so it became a confident false claim that nothing could fail on -- and it did mislead: on 2026-08-23 I quoted that 0.17.0 to another package as a MEASUREMENT of what was installed here, when the real answer was 0.18.0 and I had read a comment instead of running `md.version("scitex-ui")`. A pinned version in prose is a fact with no owner and no test. The floor lives in pyproject.toml, where a resolver reads it; ask the interpreter for what is installed. DROPPED deliberately, because the shell already provides them: , , , charset, viewport, , and scitex-ui's own theme.css. Any of those repeated here would either duplicate or fight the shell. KEPT deliberately: the stx-mount marker below. The shell does NOT emit it (checked), so the mount contract remains scholar's to declare. COMPASS 2026-09-10 (search-first researcher experience): the DEFAULT tab is now Search, not Citation Graph. Search is the primary action; the advanced query syntax, the search source (mode), the "Ignore cache" control, and the CrossRef API status moved OUT of the always-visible sidebar into a collapsed "Advanced" section so a first-time researcher sees a keyword box, not backend API infrastructure. {% endcomment %} {% block extra_css %} {% comment %} scitex-app's `stx-mount` contract: the server declares where this app is mounted so client code never has to guess. Always ends in "/", so JS joins endpoint names WITHOUT a leading slash and never normalises. Emitted here rather than by the SDK because `scitex_editor_page` -- which injects it automatically -- serves built SPA shells, and this is a Django-rendered template. {% endcomment %} {% comment %} NO |default FILTER. The root prefix is "" under scitex-app >= 0.8.0, and Django's `default` fires on FALSY -- so `|default:'/'` silently rewrote the correct "" back into the old "/", making every root request build "//api/..." : protocol-relative, off-origin. That is the exact failure the convention reversal exists to prevent, and it passed a test asserting the OLD value. This block used to be `{# ... #}`, which DJANGO ONLY STRIPS ON A SINGLE LINE -- a multi-line `{# #}` is emitted verbatim into the page. It was rendering as visible text at the top of the UI; the operator saw it in the browser before any test did. {% endcomment %} <meta name="stx-mount" content="{{ stx_mount }}"> {% comment %} PWA (installable): scholar's own manifest + theme + iOS icon. The shared shell deliberately stays PWA-agnostic (every leaf has its own name/icons), so the app declares its own. No service worker: the standalone app is stateless and a cache layer would risk stale-data surprises — installable is the goal here, offline is a separate decision. {% endcomment %} <link rel="manifest" href="{% static 'scholar/pwa/manifest.json' %}"> <meta name="theme-color" content="#0d0d0d"> <link rel="apple-touch-icon" href="{% static 'scholar/pwa/apple-touch-icon.png' %}"> <link rel="stylesheet" href="{% static 'scholar/css/scholar.css' %}"> {% endblock %} {% block app_content %} <!-- Header --> <header class="app-header"> <h1>{% trans "SciTeX Scholar" %}</h1> <span class="subtitle">{% trans "Scientific Literature Management" %}</span> </header> <div class="app-container"> <!-- Sidebar --> <aside class="app-sidebar"> <!-- Service Status --> <div class="sidebar-section"> <div class="sidebar-section__header"> <span class="sidebar-section__title">{% trans "Service Status" %}</span> </div> <div class="sidebar-section__content"> <div id="serviceStatus"> <span class="status-indicator status-checking">● {% trans "Checking..." %}</span> </div> </div> </div> <!-- Controls (graph interaction, shown only while the Graph tab is active) --> <div class="sidebar-section graph-controls-hint" data-graph-hint> <div class="sidebar-section__header"> <span class="sidebar-section__title">{% trans "Graph Controls" %}</span> </div> <div class="sidebar-section__content" style="font-size: 11px; color: var(--text-muted)"> <div style="margin-bottom: 6px;"> <kbd style="background: var(--bg-tertiary); border: 1px solid var(--border-default); padding: 1px 4px; font-size: 10px">{% trans "Scroll" %}</kbd> {% trans "Zoom" %} </div> <div style="margin-bottom: 6px;"> <kbd style="background: var(--bg-tertiary); border: 1px solid var(--border-default); padding: 1px 4px; font-size: 10px">{% trans "Drag" %}</kbd> {% trans "Pan" %} </div> <div> <kbd style="background: var(--bg-tertiary); border: 1px solid var(--border-default); padding: 1px 4px; font-size: 10px">{% trans "Click" %}</kbd> {% trans "Select node" %} </div> </div> </div> <!-- The CrossRef API status section was moved into the Search tab's Advanced section (compass 2026-09-10, L653): backend API infrastructure is no longer the default researcher view. --> </aside> <!-- Main Content --> <main class="app-main"> <!-- Tab Navigation: Search is the primary, default action. --> <nav class="tab-nav"> <button class="tab-btn active" data-tab="search">{% trans "Search databases" %}</button> <button class="tab-btn" data-tab="library">{% trans "Library" %}</button> <button class="tab-btn" data-tab="graph">{% trans "Citation Graph" %}</button> </nav> <!-- Search Tab (default) --> <div id="tab-search" class="tab-panel active"> <div class="citation-graph-container"> <!-- Input Form --> <div class="graph-card"> <div class="graph-card__header"> <span class="graph-card__title">{% trans "Search Papers" %}</span> </div> <div class="graph-card__body"> <p class="graph-description"> {% blocktrans %}Search academic databases (OpenAlex, Crossref, PubMed, Semantic Scholar, …) by keyword. This searches <strong>external databases</strong>, not your Library (a separate tab).{% endblocktrans %} </p> <form id="searchForm" class="graph-form"> <div class="form-row"> <div class="form-group--doi"> <label class="form-label" for="searchInput">{% trans "Query" %}</label> <div class="input-wrapper"> <input type="text" id="searchInput" class="form-input" placeholder="e.g. graph neural networks for molecular property prediction"> <button type="submit" class="btn-primary">{% trans "Search databases" %}</button> </div> </div> <div class="form-group--options"> <label class="form-label" for="searchMaxResults">{% trans "Results" %}</label> <select id="searchMaxResults" class="form-select"> <option value="10">10</option> <option value="20" selected>20</option> <option value="50">50</option> <option value="100">100</option> </select> </div> </div> </form> <!-- Advanced: collapsed by default. Houses the query-syntax help, the search source (mode), the "Ignore cache" control, and the CrossRef API status -- things a researcher does not need on the first screen (compass L653). --> <details class="search-advanced"> <summary class="search-advanced__summary">{% trans "Advanced" %}</summary> <div class="search-advanced__body"> <p class="search-advanced__syntax"> {% blocktrans %}Query syntax: <code>-word</code> excludes a term, <code>year:2020-2024</code> bounds the publication year, <code>if:>5</code> filters by impact factor.{% endblocktrans %} </p> <div class="search-advanced__row"> <label class="form-label" for="searchMode">{% trans "Search source" %}</label> <select id="searchMode" class="form-select"> <option value="parallel" selected>{% trans "All sources (parallel)" %}</option> <option value="single">{% trans "Single source" %}</option> </select> </div> <div class="search-advanced__row"> <label class="search-advanced__check"> <input type="checkbox" id="searchNoCache"> {% trans "Ignore cache" %} </label> </div> <div class="search-advanced__row search-advanced__api"> <span class="form-label">{% trans "CrossRef API" %}</span> {% if api_available %} <span style="color: var(--accent);">●</span> {% trans "Configured" %} {% else %} <span style="color: var(--status-error);">●</span> {% trans "Not configured" %} {% endif %} </div> </div> </details> </div> </div> <!-- Loading --> <div id="searchLoading" class="graph-loading hidden"> <div class="loading-spinner"> <div class="spinner-ring"></div> <div class="spinner-ring"></div> <div class="spinner-ring"></div> </div> <div class="loading-text">{% trans "Searching databases... This may take a moment." %}</div> </div> <!-- Error --> <div id="searchError" class="graph-error hidden"> <p id="searchErrorMessage">{% trans "An error occurred" %}</p> <button class="btn-build" onclick="document.getElementById('searchError').classList.add('hidden')"> {% trans "Dismiss" %} </button> </div> <!-- Results --> <div id="searchResults" class="hidden related-papers-section"> <div class="graph-card"> <div class="graph-card__header"> <span class="graph-card__title"> {% trans "Results" %} <small id="searchStats" class="card-subtitle"></small> </span> </div> <div class="graph-card__body" id="searchResultsContent"></div> </div> </div> </div> </div> <!-- Library Tab: the user's local library, listed here with a contextual (per-paper) Enrichment action. Enrichment is an operation ON a library item (#106), not a top-level tab (#105 kept it off the nav). --> <div id="tab-library" class="tab-panel"> <div class="citation-graph-container"> <div class="graph-card"> <div class="graph-card__header"> <span class="graph-card__title">{% trans "Library" %}</span> <small id="libraryStats" class="card-subtitle"></small> </div> <div class="graph-card__body"> <p class="graph-description"> {% blocktrans %}Your papers, stored locally. Use <strong>Enrich</strong> to fill in metadata (abstract, citations, impact factor) from the databases, or <strong>Import</strong> BibTeX and <strong>Export</strong> your library.{% endblocktrans %} </p> <!-- Import / Export (#106 / L327): library-level operations, thin wrappers over the package's BibTeX handler + formatter. --> <div class="library-io"> <label class="library-io__label" for="libraryExportFormat">{% trans "Export" %}</label> <select id="libraryExportFormat" class="form-select library-io__select"> <option value="bibtex" selected">{% trans "BibTeX (.bib)" %}</option> <option value="ris">{% trans "RIS (.ris)" %}</option> <option value="endnote">{% trans "EndNote (.enw)" %}</option> </select> <button type="button" id="libraryExportBtn" class="btn-control-library">{% trans "Export" %}</button> <span class="library-io__sep"></span> <input type="file" id="libraryImportFile" class="library-io__file" accept=".bib,.bibtex,.txt" hidden> <button type="button" id="libraryImportBtn" class="btn-control-library">{% trans "Import BibTeX" %}</button> </div> <div id="libraryIoStatus" class="library-io__status"></div> <!-- CSRF: emitted by the standard {% csrf_token %} tag. Inert standalone (no CsrfViewMiddleware, value empty); when the mounted hub enables CSRF, it carries the real token that the Library Import POST sends as X-CSRFToken. We deliberately do NOT @csrf_exempt the view. --> {% csrf_token %} <div id="libraryLoading" class="graph-loading hidden"> <div class="loading-spinner"> <div class="spinner-ring"></div> <div class="spinner-ring"></div> <div class="spinner-ring"></div> </div> <div class="loading-text">{% trans "Loading your library..." %}</div> </div> <div id="libraryError" class="graph-error hidden"> <p id="libraryErrorMessage">{% trans "An error occurred" %}</p> <button class="btn-build" onclick="document.getElementById('libraryError').classList.add('hidden')"> {% trans "Dismiss" %} </button> </div> <div id="libraryList" class="library-list"></div> </div> </div> </div> </div> <!-- Citation Graph Tab (demoted: no longer the default) --> <div id="tab-graph" class="tab-panel"> <div class="citation-graph-container"> <!-- Input Form --> <div class="graph-card"> <div class="graph-card__header"> <span class="graph-card__title">{% trans "Build Citation Network" %}</span> </div> <div class="graph-card__body"> <p class="graph-description"> {% trans "Enter a DOI to build an interactive citation network. The graph shows related papers based on bibliographic coupling, co-citation, and direct citations." %} </p> <form id="graphForm" class="graph-form"> <div class="form-row"> <div class="form-group--doi"> <label class="form-label" for="doiInput">{% trans "DOI" %}</label> <div class="input-wrapper"> <input type="text" id="doiInput" class="form-input" placeholder="e.g. 10.1038/s41586-020-2008-3" value="10.1038/s41586-020-2008-3"> <button type="submit" class="btn-build">{% trans "Build Graph" %}</button> </div> </div> <div class="form-group--options"> <label class="form-label" for="topN">{% trans "Papers" %}</label> <select id="topN" class="form-select"> <option value="10">10</option> <option value="20" selected>20</option> <option value="30">30</option> <option value="50">50</option> </select> </div> </div> </form> </div> </div> <!-- Loading --> <div id="graphLoading" class="graph-loading hidden"> <div class="loading-spinner"> <div class="spinner-ring"></div> <div class="spinner-ring"></div> <div class="spinner-ring"></div> </div> <div class="loading-text">{% trans "Building citation network... This may take a moment." %}</div> </div> <!-- Error --> <div id="graphError" class="graph-error hidden"> <p id="graphErrorMessage">{% trans "An error occurred" %}</p> <button class="btn-build" onclick="document.getElementById('graphError').classList.add('hidden')"> {% trans "Dismiss" %} </button> </div> <!-- Visualization --> <div id="graphVisualization" class="hidden"> <div class="graph-card graph-card--full"> <div class="graph-card__header"> <span class="graph-card__title"> <span id="graphTitle">{% trans "Citation Network" %}</span> <small id="graphStats" style="font-weight: 400; color: var(--text-muted); margin-left: 12px"></small> </span> <div class="graph-controls"> <button id="fitViewBtn" class="btn-control" title="{% trans "Fit to view" %}">⇱</button> <button id="resetZoomBtn" class="btn-control" title="{% trans "Reset zoom" %}">↺</button> <button id="downloadSvgBtn" class="btn-control" title="{% trans "Download SVG" %}">⇩</button> </div> </div> <div class="graph-card__body" style="padding: 0; position: relative;"> <div id="graphCanvas" class="graph-canvas-wrapper"></div> <div id="nodeDetailsPanel" class="node-details-panel hidden"></div> </div> </div> </div> <!-- Related Papers --> <div id="relatedPapersList" class="hidden related-papers-section"> <div class="graph-card"> <div class="graph-card__header"> <span class="graph-card__title">{% trans "Related Papers" %}</span> </div> <div class="graph-card__body" id="relatedPapersContent"></div> </div> </div> </div> </div> </main> </div> <!-- Scripts (order matters: ForceSimulation < GraphRenderer < citation-graph) --> {% endblock %} {% block extra_js %} <!-- Must load FIRST: declares STX_MOUNT, which every other script uses. --> <!-- i18n: the JS-only strings, translated server-side for the current request language (LocaleMiddleware). Rendered as a <script type="application/json" id="SCHOLAR_I18N"> block; the client reads it via window["SCHOLAR_I18N"] (see scholarT in stx-mount.js). The whole page flips together, so there is never an EN/JA mix. --> <!-- json_script, not a plain variable: autoescape turns " into ", which a script body never decodes. --> {{ scholar_i18n|json_script:"SCHOLAR_I18N" }} <script src="{% static 'scholar/js/stx-mount.js' %}"></script> <script src="{% static 'scholar/js/app.js' %}"></script> <script src="{% static 'scholar/js/search.js' %}"></script> <script src="{% static 'scholar/js/library.js' %}"></script> <script src="{% static 'scholar/js/graph/ForceSimulation.js' %}"></script> <script src="{% static 'scholar/js/graph/GraphRenderer.js' %}"></script> <script src="{% static 'scholar/js/graph/citation-graph.js' %}"></script> {% endblock %}