feat(ui): refactor HTML templates for consistent structure and improved styling
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 37s
build_systems / build-leviathan (pull_request) Successful in 1m20s
build_systems / build-jeeves (pull_request) Successful in 2m57s
build_systems / build-brain (pull_request) Successful in 3m3s
build_systems / build-rhapsody-in-green (pull_request) Successful in 3m25s
build_systems / build-bob (pull_request) Successful in 32m7s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 26s
build_systems / build-brain (push) Successful in 31s
build_systems / build-bob (push) Successful in 34s
build_systems / build-leviathan (push) Successful in 41s
build_systems / build-rhapsody-in-green (push) Successful in 46s
build_systems / build-jeeves (push) Successful in 2m10s

This commit was merged in pull request #27.
This commit is contained in:
2026-06-29 10:23:13 -04:00
parent 8804a8df83
commit 03f610feed
8 changed files with 499 additions and 193 deletions
+314 -49
View File
@@ -1,27 +1,210 @@
:root {
--bg: #f4f5f7;
--surface: #ffffff;
--border: #e3e5ea;
--text: #1c1f24;
--muted: #6b7280;
--accent: #4f46e5;
--accent-soft: #eef0fe;
--danger: #b42318;
--warn-bg: #fff8eb;
--warn-border: #e0a92e;
--warn-text: #7a5008;
--radius: 12px;
--shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.08);
}
html.theme-dark {
--bg: #0f1117;
--surface: #1a1d25;
--border: #2b303b;
--text: #e6e8ec;
--muted: #9aa1ad;
--accent: #818cf8;
--accent-soft: #262b45;
--danger: #f97066;
--warn-bg: #2a2410;
--warn-border: #b9881f;
--warn-text: #e8c97a;
--shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 3px rgba(0, 0, 0, 0.4);
color-scheme: dark;
}
* {
box-sizing: border-box;
}
body { body {
margin: 0; margin: 0;
background: #f7f7f4; background: var(--bg);
color: #202124; color: var(--text);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.55;
} }
main { main {
max-width: 960px; max-width: 820px;
margin: 0 auto; margin: 0 auto;
padding: 24px; padding: 32px 20px 64px;
} }
nav { /* Header / nav */
.site-header {
background: var(--surface);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 10;
}
.site-nav {
max-width: 820px;
margin: 0 auto;
padding: 12px 20px;
display: flex; display: flex;
gap: 12px;
align-items: center; align-items: center;
margin-bottom: 20px; gap: 20px;
} }
nav form { .brand {
font-weight: 700;
font-size: 1.05rem;
color: var(--text);
text-decoration: none;
}
.nav-links {
display: flex;
gap: 6px;
margin-right: auto;
}
.nav-links a {
padding: 6px 12px;
border-radius: 8px;
color: var(--muted);
text-decoration: none;
font-size: 0.94rem;
transition: background 0.15s, color 0.15s;
}
.nav-links a:hover {
background: var(--accent-soft);
color: var(--accent);
}
.dev-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
color: var(--muted);
cursor: pointer;
user-select: none;
}
.theme-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
padding: 0;
font-size: 1rem;
line-height: 1;
color: var(--text);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
cursor: pointer;
}
.theme-toggle:hover {
border-color: var(--accent);
filter: none;
}
h1 {
font-size: 1.6rem;
margin: 0 0 20px;
}
h2 {
font-size: 1.15rem;
margin: 0 0 8px;
}
/* Cards */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 20px;
}
/* Search form */
form {
margin: 0; margin: 0;
} }
label {
font-weight: 600;
font-size: 0.92rem;
}
textarea {
display: block;
width: 100%;
margin: 8px 0 16px;
padding: 12px 14px;
font: inherit;
color: var(--text);
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
resize: vertical;
transition: border-color 0.15s, box-shadow 0.15s;
}
textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.form-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
font: inherit;
font-weight: 600;
color: #fff;
background: var(--accent);
border: none;
border-radius: 10px;
cursor: pointer;
transition: filter 0.15s;
}
button:hover {
filter: brightness(1.08);
}
.check {
display: inline-flex;
gap: 8px;
align-items: center;
font-weight: 500;
color: var(--muted);
}
.actions { .actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -29,121 +212,203 @@ nav form {
margin-bottom: 24px; margin-bottom: 24px;
} }
textarea { /* Answer + results */
#results {
display: block; display: block;
width: 100%; margin-top: 28px;
margin: 8px 0 12px;
}
button {
padding: 8px 14px;
}
.check {
display: inline-flex;
gap: 8px;
align-items: center;
margin-right: 12px;
} }
.rank-label { .rank-label {
margin-top: 24px; font-size: 0.82rem;
font-weight: 700; font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
margin-bottom: 16px;
}
.answer {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 20px;
margin-bottom: 24px;
}
.answer p:last-child {
margin-bottom: 0;
} }
.results { .results {
padding-left: 24px; list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 16px;
} }
.meta, .results > li {
.scores, background: var(--surface);
.status { border: 1px solid var(--border);
color: #626a73; border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 18px 20px;
}
.results h2 {
font-size: 1.05rem;
}
.results h2 a {
color: var(--text);
text-decoration: none;
}
.results h2 a:hover {
color: var(--accent);
}
.meta {
color: var(--muted);
font-size: 0.88rem;
margin: 0 0 10px;
} }
.scores { .scores {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
margin: 12px 0; margin: 14px 0 0;
} }
.scores div { .scores div {
display: inline-flex; display: inline-flex;
gap: 4px; gap: 6px;
align-items: baseline; align-items: baseline;
padding: 3px 10px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 999px;
font-size: 0.78rem;
} }
.scores dt { .scores dt {
font-weight: 700; font-weight: 600;
color: var(--muted);
} }
.scores dd { .scores dd {
margin: 0; margin: 0;
font-variant-numeric: tabular-nums;
} }
/* Runtime — developer diagnostics, hidden unless dev mode is on */
.runtime { .runtime {
margin-top: 16px; display: none;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 18px 20px;
margin-bottom: 24px;
}
html.dev .runtime {
display: block;
} }
.timing-chart { .timing-chart {
display: grid; display: grid;
gap: 8px; gap: 8px;
padding: 0; padding: 0;
margin: 12px 0 0;
list-style: none; list-style: none;
} }
.timing-chart li { .timing-chart li {
display: grid; display: grid;
grid-template-columns: minmax(150px, 1fr) minmax(160px, 2fr) auto auto; grid-template-columns: minmax(150px, 1fr) minmax(160px, 2fr) auto auto;
gap: 8px; gap: 10px;
align-items: center; align-items: center;
font-size: 0.85rem;
} }
.timing-bar { .timing-bar {
height: 10px; height: 8px;
overflow: hidden; overflow: hidden;
background: #e5e5df; background: var(--bg);
border-radius: 999px;
} }
.timing-bar span { .timing-bar span {
display: block; display: block;
height: 100%; height: 100%;
background: #3767c8; background: var(--accent);
border-radius: 999px;
} }
.timing-value, .timing-value,
.timing-remaining { .timing-remaining {
color: #626a73; color: var(--muted);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
text-align: right;
} }
/* Tables */
table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
} }
th, th,
td { td {
padding: 8px; padding: 10px 14px;
border-bottom: 1px solid #d8d8d2; border-bottom: 1px solid var(--border);
text-align: left; text-align: left;
font-size: 0.9rem;
} }
th { th {
font-weight: 700; font-weight: 600;
color: var(--muted);
background: var(--bg);
} }
tbody tr:last-child td {
border-bottom: none;
}
dl dt {
font-weight: 600;
color: var(--muted);
font-size: 0.85rem;
}
dl dd {
margin: 0 0 12px;
}
/* States */
.error { .error {
color: #9f1d20; color: var(--danger);
font-weight: 700; font-weight: 600;
} }
.notice { .notice {
margin: 8px 0; margin: 12px 0;
padding: 8px 12px; padding: 10px 14px;
border-left: 4px solid #c8881d; border-left: 3px solid var(--warn-border);
background: #fcf3e2; border-radius: 6px;
color: #6b4a06; background: var(--warn-bg);
font-weight: 600; color: var(--warn-text);
font-weight: 500;
}
.status {
color: var(--muted);
} }
+43 -55
View File
@@ -1,57 +1,45 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="en">
<head> {% block title %}EPUB Admin{% endblock %}
<meta charset="UTF-8"> {% block head %}<script src="https://unpkg.com/htmx.org@2.0.4"></script>{% endblock %}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EPUB Admin</title> {% block content %}
<script src="https://unpkg.com/htmx.org@2.0.4"></script> <h1>Admin</h1>
<link rel="stylesheet" href="/static/style.css"> <section id="admin-status"></section>
</head> <section class="actions">
<body> <form hx-post="/admin/scan" hx-target="#admin-status" hx-swap="innerHTML">
<main> <button type="submit">Scan</button>
<nav> </form>
<a href="/">Search</a> <form hx-post="/admin/embed-missing" hx-target="#admin-status" hx-swap="innerHTML">
<a href="/books">Books</a> <button type="submit">Embed</button>
<a href="/admin">Admin</a> </form>
</nav> <form hx-post="/admin/embed-all" hx-target="#admin-status" hx-swap="innerHTML">
<h1>Admin</h1> <button type="submit">Embed all</button>
<section id="admin-status"></section> </form>
<section class="actions"> </section>
<form hx-post="/admin/scan" hx-target="#admin-status" hx-swap="innerHTML"> <section>
<button type="submit">Scan</button> <h2>Embeddings</h2>
</form> <table>
<form hx-post="/admin/embed-missing" hx-target="#admin-status" hx-swap="innerHTML"> <thead>
<button type="submit">Embed</button> <tr>
</form> <th>Model</th>
<form hx-post="/admin/embed-all" hx-target="#admin-status" hx-swap="innerHTML"> <th>Dimensions</th>
<button type="submit">Embed all</button> <th>Embedded</th>
</form> <th>Missing</th>
</section> <th>Total chunks</th>
<section> </tr>
<h2>Embeddings</h2> </thead>
<table> <tbody>
<thead> {% for item in stats %}
<tr> <tr>
<th>Model</th> <td>{{ item.model_name }}</td>
<th>Dimensions</th> <td>{{ item.dimension }}</td>
<th>Embedded</th> <td>{{ item.embedded_chunks }}</td>
<th>Missing</th> <td>{{ item.missing_chunks }}</td>
<th>Total chunks</th> <td>{{ item.total_chunks }}</td>
</tr> </tr>
</thead> {% endfor %}
<tbody> </tbody>
{% for item in stats %} </table>
<tr> </section>
<td>{{ item.model_name }}</td> {% endblock %}
<td>{{ item.dimension }}</td>
<td>{{ item.embedded_chunks }}</td>
<td>{{ item.missing_chunks }}</td>
<td>{{ item.total_chunks }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
</main>
</body>
</html>
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}EPUB Search{% endblock %}</title>
{% block head %}{% endblock %}
<link rel="stylesheet" href="/static/style.css?v={{ static_version('style.css') }}">
<script>
// Apply theme and dev mode before paint to avoid a flash of unstyled/wrong content.
(function () {
var stored = localStorage.getItem("ebook-theme");
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
var theme = stored || (prefersDark ? "dark" : "light");
document.documentElement.classList.add("theme-" + theme);
if (localStorage.getItem("ebook-dev-mode") === "on") {
document.documentElement.classList.add("dev");
}
})();
</script>
</head>
<body>
<header class="site-header">
<nav class="site-nav">
<a class="brand" href="/">EPUB Search</a>
<div class="nav-links">
<a href="/">Search</a>
<a href="/books">Books</a>
<a href="/admin">Admin</a>
</div>
<button type="button" id="theme-toggle" class="theme-toggle" title="Toggle light / dark theme" aria-label="Toggle theme"></button>
<label class="dev-toggle" title="Show developer diagnostics">
<input type="checkbox" id="dev-mode-toggle">
<span>Dev</span>
</label>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<script>
(function () {
var toggle = document.getElementById("dev-mode-toggle");
if (toggle) {
toggle.checked = document.documentElement.classList.contains("dev");
toggle.addEventListener("change", function () {
document.documentElement.classList.toggle("dev", toggle.checked);
localStorage.setItem("ebook-dev-mode", toggle.checked ? "on" : "off");
});
}
var themeButton = document.getElementById("theme-toggle");
if (themeButton) {
var root = document.documentElement;
var sync = function () {
var isDark = root.classList.contains("theme-dark");
themeButton.textContent = isDark ? "☀️" : "🌙";
};
sync();
themeButton.addEventListener("click", function () {
var next = root.classList.contains("theme-dark") ? "light" : "dark";
root.classList.remove("theme-dark", "theme-light");
root.classList.add("theme-" + next);
localStorage.setItem("ebook-theme", next);
sync();
});
}
})();
</script>
</body>
</html>
@@ -1,32 +1,20 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="en">
<head> {% block title %}{% if source %}{{ source.title }}{% else %}Book not found{% endif %}{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% block content %}
<title>{% if source %}{{ source.title }}{% else %}Book not found{% endif %}</title> {% if source %}
<link rel="stylesheet" href="/static/style.css"> <h1>{{ source.title }}</h1>
</head> <p class="meta">{{ source.author or "Unknown author" }}</p>
<body> <dl class="card">
<main> <dt>File</dt>
<nav> <dd>{{ source.file_path }}</dd>
<a href="/">Search</a> <dt>Chapters</dt>
<a href="/books">Books</a> <dd>{{ chapter_count }}</dd>
<a href="/admin">Admin</a> <dt>Chunks</dt>
</nav> <dd>{{ chunk_count }}</dd>
{% if source %} </dl>
<h1>{{ source.title }}</h1> {% else %}
<p class="meta">{{ source.author or "Unknown author" }}</p> <h1>Book not found</h1>
<dl> {% endif %}
<dt>File</dt> {% endblock %}
<dd>{{ source.file_path }}</dd>
<dt>Chapters</dt>
<dd>{{ chapter_count }}</dd>
<dt>Chunks</dt>
<dd>{{ chunk_count }}</dd>
</dl>
{% else %}
<h1>Book not found</h1>
{% endif %}
</main>
</body>
</html>
+19 -31
View File
@@ -1,31 +1,19 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="en">
<head> {% block title %}EPUB Books{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% block content %}
<title>EPUB Books</title> <h1>Books</h1>
<link rel="stylesheet" href="/static/style.css"> {% if sources %}
</head> <ol class="results">
<body> {% for source in sources %}
<main> <li>
<nav> <h2><a href="/books/{{ source.id }}">{{ source.title }}</a></h2>
<a href="/">Search</a> <p class="meta">{{ source.author or "Unknown author" }}</p>
<a href="/books">Books</a> </li>
<a href="/admin">Admin</a> {% endfor %}
</nav> </ol>
<h1>Books</h1> {% else %}
{% if sources %} <p>No EPUBs indexed.</p>
<ol class="results"> {% endif %}
{% for source in sources %} {% endblock %}
<li>
<h2><a href="/books/{{ source.id }}">{{ source.title }}</a></h2>
<p class="meta">{{ source.author or "Unknown author" }}</p>
</li>
{% endfor %}
</ol>
{% else %}
<p>No EPUBs indexed.</p>
{% endif %}
</main>
</body>
</html>
@@ -39,7 +39,13 @@
<ol class="results"> <ol class="results">
{% for result in response.results %} {% for result in response.results %}
<li> <li>
<h2>{{ result.source_title }}</h2> <h2>
{% if result.source_id %}
<a href="/books/{{ result.source_id }}">{{ result.source_title }}</a>
{% else %}
{{ result.source_title }}
{% endif %}
</h2>
<p class="meta"> <p class="meta">
{% if result.source_author %}{{ result.source_author }}{% endif %} {% if result.source_author %}{{ result.source_author }}{% endif %}
{% if result.chapter_title %} · {{ result.chapter_title }}{% endif %} {% if result.chapter_title %} · {{ result.chapter_title }}{% endif %}
+15 -25
View File
@@ -1,30 +1,20 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="en">
<head> {% block title %}EPUB Search{% endblock %}
<meta charset="UTF-8"> {% block head %}<script src="https://unpkg.com/htmx.org@2.0.4"></script>{% endblock %}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EPUB Search</title> {% block content %}
<script src="https://unpkg.com/htmx.org@2.0.4"></script> <h1>Search</h1>
<link rel="stylesheet" href="/static/style.css"> <form class="card" hx-post="/search" hx-target="#results" hx-swap="innerHTML">
</head> <label for="query">What are you looking for?</label>
<body> <textarea id="query" name="query" rows="4" placeholder="Ask a question or paste a passage…" required></textarea>
<main> <div class="form-row">
<nav>
<a href="/">Search</a>
<a href="/books">Books</a>
<a href="/admin">Admin</a>
</nav>
<h1>EPUB Search</h1>
<form hx-post="/search" hx-target="#results" hx-swap="innerHTML">
<label for="query">Search</label>
<textarea id="query" name="query" rows="4" required></textarea>
<label class="check"> <label class="check">
<input type="checkbox" name="rerank" value="true" {% if config.rerank.enabled %}checked{% endif %}> <input type="checkbox" name="rerank" value="true" {% if config.rerank.enabled %}checked{% endif %}>
Rerank Rerank
</label> </label>
<button type="submit">Search</button> <button type="submit">Search</button>
</form> </div>
<section id="results"></section> </form>
</main> <section id="results"></section>
</body> {% endblock %}
</html>
+10
View File
@@ -10,4 +10,14 @@ PACKAGE_DIR = Path(__file__).resolve().parent
TEMPLATE_DIR = PACKAGE_DIR / "templates" TEMPLATE_DIR = PACKAGE_DIR / "templates"
STATIC_DIR = PACKAGE_DIR / "static" STATIC_DIR = PACKAGE_DIR / "static"
def static_version(filename: str) -> int:
"""Return a cache-busting token for a static file based on its modification time."""
try:
return int((STATIC_DIR / filename).stat().st_mtime)
except OSError:
return 0
templates = Jinja2Templates(directory=TEMPLATE_DIR) templates = Jinja2Templates(directory=TEMPLATE_DIR)
templates.env.globals["static_version"] = static_version