feat: MVP webinterface
Server-rendered HTML via Jinja2 templates in FastAPI: - Homepage: zoekbalk, statistieken, inhoudsopgave Grondwet - /grondwet: volledige tekst als leesbare pagina - /grondwet/samenvatting: AI-samenvatting (B1-niveau) - /zoeken: keyword (Meilisearch) + semantisch (Qdrant) - /historie: tijdlijn van versies - /diff: visuele vergelijking tussen twee versies - Sober, responsive CSS (overheidsstijl) - Disclaimer op elke pagina flake.nix: jinja2 + markdown packages toegevoegd Sluit #39
This commit is contained in:
parent
b655f56f8c
commit
6db0f6afc3
12 changed files with 570 additions and 0 deletions
|
|
@ -32,6 +32,10 @@
|
|||
click
|
||||
rich # Terminal formatting
|
||||
|
||||
# Webinterface
|
||||
jinja2 # HTML templates
|
||||
markdown # Markdown → HTML rendering
|
||||
|
||||
# Testing
|
||||
pytest
|
||||
pytest-cov
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@ app.add_middleware(
|
|||
|
||||
store = RegelingStore(REPO_PATH)
|
||||
|
||||
# --- Webinterface ---
|
||||
from pathlib import Path as _Path
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from wetgit.web.routes import router as web_router, init_routes
|
||||
|
||||
app.mount("/static", StaticFiles(directory=str(_Path(__file__).parent.parent / "web" / "static")), name="static")
|
||||
init_routes(store)
|
||||
app.include_router(web_router)
|
||||
|
||||
|
||||
# --- Health ---
|
||||
|
||||
|
|
|
|||
1
src/wetgit/web/__init__.py
Normal file
1
src/wetgit/web/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""WetGit webinterface."""
|
||||
198
src/wetgit/web/routes.py
Normal file
198
src/wetgit/web/routes.py
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
"""Web routes — server-rendered HTML pagina's."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import markdown
|
||||
from fastapi import APIRouter, Query, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from wetgit.api.data import RegelingStore
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates"))
|
||||
|
||||
BWB_GRONDWET = "BWBR0001840"
|
||||
|
||||
# Store wordt gezet vanuit app.py
|
||||
_store: RegelingStore | None = None
|
||||
|
||||
|
||||
def init_routes(store: RegelingStore) -> None:
|
||||
"""Initialiseer de routes met een RegelingStore."""
|
||||
global _store
|
||||
_store = store
|
||||
|
||||
|
||||
def _get_store() -> RegelingStore:
|
||||
if _store is None:
|
||||
raise RuntimeError("Store niet geinitialiseerd")
|
||||
return _store
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
def homepage(request: Request) -> HTMLResponse:
|
||||
"""Homepage met zoekbalk en Grondwet overzicht."""
|
||||
store = _get_store()
|
||||
tekst = store.get_tekst(BWB_GRONDWET) or ""
|
||||
|
||||
# Tel artikelen en hoofdstukken
|
||||
artikelen = len(re.findall(r"^### Artikel", tekst, re.MULTILINE))
|
||||
versies = store.get_versies(BWB_GRONDWET)
|
||||
|
||||
# Extraheer hoofdstukken voor de inhoudsopgave
|
||||
hoofdstukken = []
|
||||
for match in re.finditer(r"^## (.+)$", tekst, re.MULTILINE):
|
||||
titel = match.group(1).strip()
|
||||
anchor = re.sub(r"[^\w\s-]", "", titel.lower()).replace(" ", "-")
|
||||
hoofdstukken.append({"titel": titel, "anchor": anchor})
|
||||
|
||||
return templates.TemplateResponse("index.html", {
|
||||
"request": request,
|
||||
"stats": {
|
||||
"artikelen": artikelen,
|
||||
"versies": len(versies),
|
||||
"regelingen": len(store.list_regelingen()),
|
||||
},
|
||||
"hoofdstukken": hoofdstukken,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/grondwet", response_class=HTMLResponse)
|
||||
def grondwet(request: Request) -> HTMLResponse:
|
||||
"""De Grondwet als leesbare pagina."""
|
||||
store = _get_store()
|
||||
tekst = store.get_tekst(BWB_GRONDWET)
|
||||
if not tekst:
|
||||
return HTMLResponse("<h1>Niet gevonden</h1>", status_code=404)
|
||||
|
||||
# Strip frontmatter
|
||||
if tekst.startswith("---"):
|
||||
_, _, tekst = tekst.split("---", 2)
|
||||
|
||||
# Markdown → HTML met anchor IDs op artikelen
|
||||
content_html = markdown.markdown(tekst, extensions=["toc"])
|
||||
|
||||
# Voeg ID's toe aan artikelen
|
||||
content_html = re.sub(
|
||||
r"<h3>Artikel (\S+)",
|
||||
r'<h3 id="artikel-\1">Artikel \1',
|
||||
content_html,
|
||||
)
|
||||
|
||||
regeling = store.get_regeling(BWB_GRONDWET) or {}
|
||||
versies = store.get_versies(BWB_GRONDWET)
|
||||
summary_path = Path(store.repo_path) / (regeling.get("pad", "") + "/summary.md") if regeling.get("pad") else None
|
||||
|
||||
return templates.TemplateResponse("regeling.html", {
|
||||
"request": request,
|
||||
"titel": regeling.get("titel", "Grondwet"),
|
||||
"bwb_id": BWB_GRONDWET,
|
||||
"type": regeling.get("type", "wet"),
|
||||
"versies": len(versies),
|
||||
"has_summary": summary_path and summary_path.exists() if summary_path else False,
|
||||
"content_html": content_html,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/grondwet/samenvatting", response_class=HTMLResponse)
|
||||
def samenvatting(request: Request) -> HTMLResponse:
|
||||
"""AI-samenvatting van de Grondwet."""
|
||||
store = _get_store()
|
||||
regeling = store.get_regeling(BWB_GRONDWET) or {}
|
||||
pad = regeling.get("pad", "")
|
||||
|
||||
summary_path = store.repo_path / pad / "summary.md"
|
||||
if not summary_path.exists():
|
||||
return HTMLResponse("<h1>Samenvatting niet beschikbaar</h1>", status_code=404)
|
||||
|
||||
tekst = summary_path.read_text(encoding="utf-8")
|
||||
content_html = markdown.markdown(tekst)
|
||||
|
||||
return templates.TemplateResponse("samenvatting.html", {
|
||||
"request": request,
|
||||
"content_html": content_html,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/zoeken", response_class=HTMLResponse)
|
||||
def zoeken_page(
|
||||
request: Request,
|
||||
q: str = Query("", description="Zoekterm"),
|
||||
mode: str = Query("keyword", description="Zoekmodus"),
|
||||
) -> HTMLResponse:
|
||||
"""Zoekpagina."""
|
||||
results = []
|
||||
|
||||
if q and len(q) >= 2:
|
||||
import os
|
||||
from wetgit.api.search import MeiliSearch
|
||||
from wetgit.ai.semantic import SemanticSearch
|
||||
|
||||
meili_url = os.environ.get("MEILI_URL", "http://127.0.0.1:7700")
|
||||
qdrant_url = os.environ.get("QDRANT_URL", "http://127.0.0.1:6333")
|
||||
|
||||
if mode == "semantic":
|
||||
sem = SemanticSearch(qdrant_url=qdrant_url)
|
||||
if sem.health():
|
||||
results = sem.search(q, limit=20)
|
||||
else:
|
||||
meili = MeiliSearch(url=meili_url)
|
||||
if meili.health():
|
||||
raw = meili.search(q, limit=20)
|
||||
results = [
|
||||
{
|
||||
"bwb_id": h["bwb_id"],
|
||||
"titel": h.get("regeling_titel", ""),
|
||||
"artikel": f"Artikel {h.get('artikel_nummer', '?')}",
|
||||
"context": h.get("tekst", "")[:200],
|
||||
}
|
||||
for h in raw.get("hits", [])
|
||||
]
|
||||
|
||||
return templates.TemplateResponse("zoeken.html", {
|
||||
"request": request,
|
||||
"query": q,
|
||||
"mode": mode,
|
||||
"results": results,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/historie", response_class=HTMLResponse)
|
||||
def historie(request: Request) -> HTMLResponse:
|
||||
"""Tijdlijn van versies."""
|
||||
store = _get_store()
|
||||
versies = store.get_versies(BWB_GRONDWET)
|
||||
|
||||
return templates.TemplateResponse("historie.html", {
|
||||
"request": request,
|
||||
"versies": versies,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/diff", response_class=HTMLResponse)
|
||||
def diff_page(
|
||||
request: Request,
|
||||
van: str = Query(..., description="Van datum"),
|
||||
tot: str = Query(..., description="Tot datum"),
|
||||
) -> HTMLResponse:
|
||||
"""Diff pagina."""
|
||||
store = _get_store()
|
||||
result = store.get_diff(BWB_GRONDWET, van, tot)
|
||||
|
||||
if not result:
|
||||
return HTMLResponse("<h1>Geen diff beschikbaar</h1>", status_code=404)
|
||||
|
||||
diff_lines = result["diff"].split("\n")
|
||||
|
||||
return templates.TemplateResponse("diff.html", {
|
||||
"request": request,
|
||||
"van": van,
|
||||
"tot": tot,
|
||||
"added": result["regels_toegevoegd"],
|
||||
"removed": result["regels_verwijderd"],
|
||||
"diff_lines": diff_lines,
|
||||
})
|
||||
164
src/wetgit/web/static/style.css
Normal file
164
src/wetgit/web/static/style.css
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
/* WetGit — sober, leesbaar, overheidsstijl */
|
||||
|
||||
:root {
|
||||
--color-bg: #ffffff;
|
||||
--color-text: #1a1a2e;
|
||||
--color-muted: #6b7280;
|
||||
--color-border: #e5e7eb;
|
||||
--color-accent: #1e40af;
|
||||
--color-accent-light: #dbeafe;
|
||||
--color-green: #166534;
|
||||
--color-green-bg: #dcfce7;
|
||||
--color-red: #991b1b;
|
||||
--color-red-bg: #fee2e2;
|
||||
--color-warn-bg: #fef3c7;
|
||||
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-mono: "SF Mono", "Fira Code", monospace;
|
||||
--max-width: 800px;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
line-height: 1.7;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
a { color: var(--color-accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
/* Layout */
|
||||
.container { max-width: var(--max-width); margin: 0 auto; padding: 0 1.5rem; }
|
||||
|
||||
header {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: 1rem 0;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
header .container { display: flex; align-items: center; justify-content: space-between; }
|
||||
header h1 { font-size: 1.3rem; font-weight: 700; }
|
||||
header h1 a { color: var(--color-text); }
|
||||
header nav a { margin-left: 1.5rem; color: var(--color-muted); font-size: 0.9rem; }
|
||||
header nav a:hover { color: var(--color-text); }
|
||||
|
||||
footer {
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding: 2rem 0;
|
||||
margin-top: 3rem;
|
||||
color: var(--color-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.search-box {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.search-box input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 0.7rem 1rem;
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
.search-box input:focus { outline: none; border-color: var(--color-accent); }
|
||||
.search-box select {
|
||||
padding: 0.7rem;
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
background: white;
|
||||
}
|
||||
.search-box button {
|
||||
padding: 0.7rem 1.5rem;
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-box button:hover { background: #1e3a8a; }
|
||||
|
||||
/* Content */
|
||||
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
h2 { font-size: 1.5rem; margin: 2rem 0 0.8rem; border-bottom: 1px solid var(--color-border); padding-bottom: 0.3rem; }
|
||||
h3 { font-size: 1.15rem; margin: 1.5rem 0 0.5rem; }
|
||||
|
||||
.subtitle { color: var(--color-muted); margin-bottom: 2rem; }
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background: var(--color-accent-light);
|
||||
color: var(--color-accent);
|
||||
padding: 0.15rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Disclaimer */
|
||||
.disclaimer {
|
||||
background: var(--color-warn-bg);
|
||||
border-left: 4px solid #f59e0b;
|
||||
padding: 0.8rem 1rem;
|
||||
margin: 1.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
/* Artikelen */
|
||||
.artikel { margin: 1.5rem 0; }
|
||||
.artikel h3 a { color: var(--color-text); }
|
||||
.artikel-tekst { padding-left: 0; }
|
||||
|
||||
/* Zoekresultaten */
|
||||
.result { border-bottom: 1px solid var(--color-border); padding: 1rem 0; }
|
||||
.result:last-child { border-bottom: none; }
|
||||
.result-title { font-weight: 600; }
|
||||
.result-context { color: var(--color-muted); font-size: 0.95rem; margin-top: 0.3rem; }
|
||||
.result-score { font-size: 0.8rem; color: var(--color-muted); }
|
||||
mark { background: #fef08a; padding: 0.1rem 0.2rem; border-radius: 2px; }
|
||||
|
||||
/* Diff */
|
||||
.diff-line { font-family: var(--font-mono); font-size: 0.85rem; white-space: pre-wrap; padding: 0.1rem 0.5rem; }
|
||||
.diff-add { background: var(--color-green-bg); color: var(--color-green); }
|
||||
.diff-del { background: var(--color-red-bg); color: var(--color-red); }
|
||||
.diff-hunk { color: var(--color-accent); font-weight: 600; margin-top: 1rem; }
|
||||
|
||||
/* Versie-tijdlijn */
|
||||
.timeline { list-style: none; border-left: 3px solid var(--color-accent-light); padding-left: 1.5rem; }
|
||||
.timeline li { margin: 1rem 0; position: relative; }
|
||||
.timeline li::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -1.85rem;
|
||||
top: 0.4rem;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: var(--color-accent);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.timeline-date { font-weight: 700; color: var(--color-accent); }
|
||||
|
||||
/* Hero (homepage) */
|
||||
.hero { text-align: center; padding: 3rem 0 2rem; }
|
||||
.hero h1 { font-size: 2.5rem; }
|
||||
.hero .subtitle { font-size: 1.2rem; }
|
||||
.hero blockquote {
|
||||
font-style: italic;
|
||||
color: var(--color-muted);
|
||||
margin: 1.5rem auto;
|
||||
max-width: 500px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* Stats */
|
||||
.stats { display: flex; gap: 2rem; justify-content: center; margin: 2rem 0; flex-wrap: wrap; }
|
||||
.stat { text-align: center; }
|
||||
.stat-number { font-size: 2rem; font-weight: 700; color: var(--color-accent); }
|
||||
.stat-label { font-size: 0.85rem; color: var(--color-muted); }
|
||||
35
src/wetgit/web/templates/base.html
Normal file
35
src/wetgit/web/templates/base.html
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}WetGit{% endblock %} — Nederlandse wetgeving als code</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<h1><a href="/">WetGit</a></h1>
|
||||
<nav>
|
||||
<a href="/grondwet">Grondwet</a>
|
||||
<a href="/zoeken">Zoeken</a>
|
||||
<a href="/historie">Historie</a>
|
||||
<a href="/api/docs">API</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>WetGit is geen officiele bron van wetgeving. Raadpleeg altijd
|
||||
<a href="https://wetten.overheid.nl">wetten.overheid.nl</a> voor de authentieke tekst.</p>
|
||||
<p>Wetteksten: CC0 (publiek domein) | Tooling: MIT |
|
||||
<a href="https://git.wetgit.nl/wetgit">git.wetgit.nl</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
21
src/wetgit/web/templates/diff.html
Normal file
21
src/wetgit/web/templates/diff.html
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Diff: {{ van }} → {{ tot }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Diff: Grondwet</h1>
|
||||
<p class="subtitle">{{ van }} → {{ tot }} | +{{ added }} / -{{ removed }} regels</p>
|
||||
|
||||
<div style="background: #f8f9fa; border-radius: 6px; padding: 1rem; overflow-x: auto;">
|
||||
{% for line in diff_lines %}
|
||||
{% if line.startswith('+') and not line.startswith('+++') %}
|
||||
<div class="diff-line diff-add">{{ line }}</div>
|
||||
{% elif line.startswith('-') and not line.startswith('---') %}
|
||||
<div class="diff-line diff-del">{{ line }}</div>
|
||||
{% elif line.startswith('@@') %}
|
||||
<div class="diff-line diff-hunk">{{ line }}</div>
|
||||
{% elif not line.startswith('diff ') and not line.startswith('index ') %}
|
||||
<div class="diff-line">{{ line }}</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
18
src/wetgit/web/templates/historie.html
Normal file
18
src/wetgit/web/templates/historie.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Historie — Grondwet{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Historie: Grondwet</h1>
|
||||
<p class="subtitle">{{ versies|length }} versies beschikbaar. Klik op twee versies om een diff te zien.</p>
|
||||
|
||||
<ul class="timeline">
|
||||
{% for v in versies %}
|
||||
<li>
|
||||
<span class="timeline-date">{{ v.datum }}</span>
|
||||
{% if not loop.last %}
|
||||
— <a href="/diff?van={{ versies[loop.index].datum }}&tot={{ v.datum }}">diff met vorige</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
47
src/wetgit/web/templates/index.html
Normal file
47
src/wetgit/web/templates/index.html
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="hero">
|
||||
<h1>WetGit</h1>
|
||||
<p class="subtitle">Nederlandse wetgeving als code.<br>
|
||||
Elke wet een Markdown-bestand. Elke wijziging een Git-commit.</p>
|
||||
|
||||
<blockquote>In de geest van Dirk Volkertzoon Coornhert (1522-1590) — humanist,
|
||||
graveur en voorvechter van een transparant rechtssysteem.</blockquote>
|
||||
</div>
|
||||
|
||||
<form class="search-box" action="/zoeken" method="get">
|
||||
<input type="text" name="q" placeholder="Zoek in de Grondwet... bijv. 'privacy' of 'mag mijn baas mijn e-mail lezen?'" autofocus>
|
||||
<select name="mode">
|
||||
<option value="keyword">Keyword</option>
|
||||
<option value="semantic">Semantisch</option>
|
||||
</select>
|
||||
<button type="submit">Zoek</button>
|
||||
</form>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-number">{{ stats.artikelen }}</div>
|
||||
<div class="stat-label">artikelen</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-number">{{ stats.versies }}</div>
|
||||
<div class="stat-label">historische versies</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-number">{{ stats.regelingen }}</div>
|
||||
<div class="stat-label">regelingen</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Grondwet voor het Koninkrijk der Nederlanden</h2>
|
||||
|
||||
<div class="disclaimer">
|
||||
Dit is een AI-verrijkte weergave van de Grondwet. Raadpleeg altijd
|
||||
<a href="https://wetten.overheid.nl/BWBR0001840">wetten.overheid.nl</a> voor de officiele tekst.
|
||||
</div>
|
||||
|
||||
{% for h in hoofdstukken %}
|
||||
<h3><a href="/grondwet#{{ h.anchor }}">{{ h.titel }}</a></h3>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
20
src/wetgit/web/templates/regeling.html
Normal file
20
src/wetgit/web/templates/regeling.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}{{ titel }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ titel }}</h1>
|
||||
<p class="subtitle">
|
||||
<span class="tag">{{ type }}</span>
|
||||
BWB-ID: {{ bwb_id }} |
|
||||
<a href="https://wetten.overheid.nl/{{ bwb_id }}">Officiele tekst</a> |
|
||||
<a href="/historie">{{ versies }} versies</a>
|
||||
{% if has_summary %} | <a href="/grondwet/samenvatting">AI-samenvatting</a>{% endif %}
|
||||
</p>
|
||||
|
||||
<div class="disclaimer">
|
||||
Dit is geen officiele bron. Raadpleeg altijd
|
||||
<a href="https://wetten.overheid.nl/{{ bwb_id }}">wetten.overheid.nl</a>.
|
||||
</div>
|
||||
|
||||
{{ content_html | safe }}
|
||||
{% endblock %}
|
||||
18
src/wetgit/web/templates/samenvatting.html
Normal file
18
src/wetgit/web/templates/samenvatting.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Samenvatting — Grondwet{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Samenvatting: Grondwet</h1>
|
||||
<p class="subtitle">AI-gegenereerde samenvatting in begrijpelijk Nederlands (B1-niveau)</p>
|
||||
|
||||
<div class="disclaimer">
|
||||
Dit is een AI-gegenereerde samenvatting en geen juridisch advies.
|
||||
Raadpleeg altijd <a href="https://wetten.overheid.nl/BWBR0001840">wetten.overheid.nl</a>.
|
||||
</div>
|
||||
|
||||
{{ content_html | safe }}
|
||||
|
||||
<p style="margin-top: 2rem;">
|
||||
<a href="/grondwet">Terug naar de volledige tekst</a>
|
||||
</p>
|
||||
{% endblock %}
|
||||
35
src/wetgit/web/templates/zoeken.html
Normal file
35
src/wetgit/web/templates/zoeken.html
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Zoeken{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Zoeken</h1>
|
||||
|
||||
<form class="search-box" action="/zoeken" method="get">
|
||||
<input type="text" name="q" value="{{ query }}" placeholder="Zoek in alle wetgeving..." autofocus>
|
||||
<select name="mode">
|
||||
<option value="keyword" {% if mode == 'keyword' %}selected{% endif %}>Keyword</option>
|
||||
<option value="semantic" {% if mode == 'semantic' %}selected{% endif %}>Semantisch</option>
|
||||
</select>
|
||||
<button type="submit">Zoek</button>
|
||||
</form>
|
||||
|
||||
{% if query %}
|
||||
<p class="subtitle">{{ results|length }} resultaten voor "{{ query }}"
|
||||
{% if mode == 'semantic' %} (semantisch zoeken){% endif %}</p>
|
||||
|
||||
{% for r in results %}
|
||||
<div class="result">
|
||||
<div class="result-title">
|
||||
<a href="/grondwet#artikel-{{ r.artikel.split(' ')[-1] }}">{{ r.artikel }}</a>
|
||||
— {{ r.titel }}
|
||||
{% if r.score %}<span class="result-score">({{ "%.1f"|format(r.score * 100) }}% match)</span>{% endif %}
|
||||
</div>
|
||||
<div class="result-context">{{ r.context[:200] }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if not results %}
|
||||
<p>Geen resultaten gevonden.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Loading…
Add table
Reference in a new issue