diff --git a/assets/css/tools.css b/assets/css/tools.css index 71bf298..6c364bc 100644 --- a/assets/css/tools.css +++ b/assets/css/tools.css @@ -511,3 +511,365 @@ p { grid-template-columns: 1fr; } } + +/* ─── Public showcase landing ──────────────────────────────────────────────── */ + +.showcase-page { + display: flex; + flex-direction: column; + min-height: 100vh; + background: var(--bg); +} + +.showcase-header { + background: + linear-gradient(135deg, rgba(15, 118, 110, 0.92), rgba(17, 94, 89, 0.97) 70%), + linear-gradient(315deg, rgba(194, 65, 12, 0.18), transparent 40%); + color: #fff; + padding: 64px 24px 72px; +} + +.showcase-header-inner { + max-width: 860px; + margin: 0 auto; + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 32px; + flex-wrap: wrap; +} + +.showcase-brand .eyebrow { + color: rgba(255,255,255,0.72); +} + +.showcase-title { + margin: 0 0 10px; + font-size: clamp(2rem, 5vw, 3rem); + font-weight: 800; + line-height: 1.1; + color: #fff; +} + +.showcase-tagline { + margin: 0 0 18px; + font-size: 1.1rem; + color: rgba(255,255,255,0.82); +} + +.powered-badge { + display: inline-block; + background: rgba(255,255,255,0.14); + border: 1px solid rgba(255,255,255,0.28); + border-radius: 999px; + padding: 4px 14px; + font-size: 0.78rem; + font-weight: 600; + color: rgba(255,255,255,0.9); + text-decoration: none; + transition: background 0.15s; +} + +.powered-badge:hover { + background: rgba(255,255,255,0.22); +} + +.cta-button { + display: inline-block; + background: var(--coral); + color: #fff; + text-decoration: none; + padding: 14px 28px; + border-radius: 8px; + font-weight: 700; + font-size: 1rem; + white-space: nowrap; + transition: background 0.15s, transform 0.1s; + flex-shrink: 0; +} + +.cta-button:hover { + background: #b83a0b; + transform: translateY(-1px); +} + +.section-inner { + max-width: 860px; + margin: 0 auto; + padding: 0 24px; +} + +.section-heading { + margin: 0 0 8px; + font-size: 1.5rem; + font-weight: 800; + color: var(--ink); +} + +.section-sub { + margin: 0 0 36px; + color: var(--muted); + font-size: 0.98rem; +} + +/* How it works */ +.hiw-section { + padding: 64px 0; + background: var(--panel); + border-bottom: 1px solid var(--line); +} + +.hiw-steps { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 28px; +} + +.hiw-step { + background: var(--bg); + border: 1px solid var(--line); + border-radius: 8px; + padding: 24px; +} + +.hiw-num { + font-size: 2rem; + font-weight: 800; + color: var(--teal); + opacity: 0.4; + margin-bottom: 12px; + line-height: 1; +} + +.hiw-step h3 { + margin: 0 0 8px; + font-size: 1rem; + font-weight: 700; +} + +.hiw-step p { + margin: 0; + color: var(--muted); + font-size: 0.9rem; + line-height: 1.6; +} + +/* Capabilities */ +.cap-section { + padding: 64px 0; + border-bottom: 1px solid var(--line); +} + +.cap-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(155px, 1fr)); + gap: 16px; +} + +.cap-card { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + padding: 20px; + transition: box-shadow 0.15s; +} + +.cap-card:hover { + box-shadow: 0 4px 16px rgba(15,118,110,0.1); +} + +.cap-label { + display: inline-block; + background: var(--soft-teal); + color: var(--teal-dark); + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + border-radius: 999px; + padding: 3px 10px; + margin-bottom: 10px; +} + +.cap-card h3 { + margin: 0 0 6px; + font-size: 0.95rem; + font-weight: 700; +} + +.cap-card p { + margin: 0; + color: var(--muted); + font-size: 0.85rem; + line-height: 1.55; +} + +/* Evidence trail explainer */ +.evidence-section { + padding: 64px 0; + background: var(--panel); + border-bottom: 1px solid var(--line); +} + +.evidence-inner { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 48px; + align-items: start; +} + +.evidence-copy h2 { + margin: 0 0 14px; + font-size: 1.5rem; + font-weight: 800; +} + +.evidence-copy p { + color: var(--muted); + line-height: 1.7; + margin: 0 0 18px; +} + +.evidence-list { + margin: 0; + padding-left: 20px; + color: var(--muted); + font-size: 0.9rem; + line-height: 1.8; +} + +.evidence-mock { + background: var(--bg); + border: 1px solid var(--line); + border-radius: 8px; + padding: 20px; +} + +.mock-label { + margin: 0 0 16px; + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--teal); +} + +.mock-step { + display: flex; + gap: 12px; + margin-bottom: 14px; + padding-bottom: 14px; + border-bottom: 1px solid var(--line); + font-size: 0.85rem; +} + +.mock-step:last-child { + border-bottom: 0; + margin-bottom: 0; + padding-bottom: 0; +} + +.mock-step p { + margin: 3px 0 0; + color: var(--muted); + font-size: 0.82rem; +} + +.mock-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--teal); + flex-shrink: 0; + margin-top: 4px; +} + +.mock-dot--amber { + background: var(--amber); +} + +.mock-done .mock-dot { + background: var(--teal); +} + +/* Access / login gate */ +.access-section { + padding: 64px 24px; + display: flex; + justify-content: center; + background: var(--bg); +} + +/* Showcase footer */ +.showcase-footer { + padding: 32px 24px; + text-align: center; + border-top: 1px solid var(--line); + color: var(--muted); + font-size: 0.85rem; +} + +.showcase-footer a { + color: var(--teal); + text-decoration: none; +} + +.showcase-footer a:hover { + text-decoration: underline; +} + +.footer-disclaimer { + margin: 6px 0 0; + font-size: 0.78rem; + opacity: 0.7; +} + +@media (max-width: 640px) { + .showcase-header-inner { + flex-direction: column; + align-items: flex-start; + } + + .evidence-inner { + grid-template-columns: 1fr; + } + + .cap-grid { + grid-template-columns: 1fr 1fr; + } +} + +/* Source card chunk toggle */ +.chunk-details { + margin-top: 0.625rem; +} + +.chunk-toggle { + cursor: pointer; + font-size: 0.75rem; + font-weight: 600; + color: var(--teal); + user-select: none; + list-style: none; +} + +.chunk-toggle::-webkit-details-marker { + display: none; +} + +.chunk-details[open] .chunk-toggle { + color: var(--teal-dark); +} + +.chunk-text { + margin-top: 0.5rem; + font-size: 0.7rem; + line-height: 1.55; + background: #f0fdf9; + border: 1px solid #ccfbf1; + border-radius: 6px; + padding: 0.625rem 0.75rem; + white-space: pre-wrap; + overflow-x: auto; + color: #374151; +} diff --git a/assets/js/tools.js b/assets/js/tools.js index 1f37c33..b8aafed 100644 --- a/assets/js/tools.js +++ b/assets/js/tools.js @@ -60,7 +60,7 @@ const els = {}; document.addEventListener('DOMContentLoaded', () => { Object.assign(els, { - gate: document.querySelector('#passcodeGate'), + gate: document.querySelector('#publicLanding'), app: document.querySelector('#appShell'), passcodeForm: document.querySelector('#passcodeForm'), loginEmail: document.querySelector('#loginEmail'), @@ -291,17 +291,26 @@ function renderEvidence(data) { function renderEvidenceItem(item) { const title = item.title || item.citation || 'Source'; const body = item.excerpt || item.why_it_matters || item.citation || ''; + const chunkText = item.chunk_text || ''; const meta = [ item.package_or_corpus, item.section, item.score !== undefined && item.score !== null ? `score ${item.score}` : '', ].filter(Boolean).join(' · '); + const chunkToggle = (chunkText && chunkText !== body) ? ` +
+ View chunk +
${escapeHtml(chunkText)}
+
+ ` : ''; + return `

${escapeHtml(title)}

${meta ? `

${escapeHtml(meta)}

` : ''}

${escapeHtml(body)}

+ ${chunkToggle}
`; } diff --git a/includes/LegalTools.php b/includes/LegalTools.php index bc8605a..935b6c8 100644 --- a/includes/LegalTools.php +++ b/includes/LegalTools.php @@ -70,7 +70,23 @@ final class DbnLegalToolsService } } - $hits = array_map(fn(array $chunk): array => $this->sourceFromChunk($chunk), array_slice($chunks, 0, $limit)); + $sharedDocIds = []; + foreach (array_slice($chunks, 0, $limit) as $chunk) { + if (($chunk['source_type'] ?? '') !== 'private' && isset($chunk['document_id'])) { + $sharedDocIds[(int)$chunk['document_id']] = true; + } + } + $docSummaries = $sharedDocIds ? $this->fetchDocSummaries(array_keys($sharedDocIds)) : []; + + $hits = array_map( + fn(array $chunk): array => $this->sourceFromChunk( + $chunk, + ($chunk['source_type'] ?? '') !== 'private' + ? ($docSummaries[(int)($chunk['document_id'] ?? 0)] ?? null) + : null + ), + array_slice($chunks, 0, $limit) + ); $confidence = $this->citationConfidence($hits); $trace[1] = $this->trace('Search tools used', $retrievalNote . '; returned ' . count($hits) . ' source hit(s).', 'complete'); @@ -461,23 +477,44 @@ PROMPT; return array_values(array_filter($trail, 'is_array')); } - private function sourceFromChunk(array $chunk): array + private function sourceFromChunk(array $chunk, ?string $docSummary = null): array { $title = (string)($chunk['document_title'] ?? $chunk['title'] ?? 'Untitled source'); $score = isset($chunk['similarity']) ? round((float)$chunk['similarity'], 4) : null; + $rawExcerpt = dbnToolsExcerpt((string)($chunk['content'] ?? ''), 620); return [ - 'title' => $title, - 'excerpt' => dbnToolsExcerpt((string)($chunk['content'] ?? ''), 620), + 'title' => $title, + 'excerpt' => $docSummary ?? $rawExcerpt, + 'chunk_text' => $rawExcerpt, 'package_or_corpus' => (string)($chunk['source_name'] ?? $chunk['source_type'] ?? 'Do Better Norge'), - 'score' => $score, - 'document_id' => isset($chunk['document_id']) ? (int)$chunk['document_id'] : null, - 'chunk_id' => isset($chunk['id']) ? (int)$chunk['id'] : null, - 'section' => $chunk['section_title'] ?? null, - 'authority_type' => $chunk['authority_type'] ?? null, - 'jurisdiction' => $chunk['jurisdiction'] ?? null, + 'score' => $score, + 'document_id' => isset($chunk['document_id']) ? (int)$chunk['document_id'] : null, + 'chunk_id' => isset($chunk['id']) ? (int)$chunk['id'] : null, + 'section' => $chunk['section_title'] ?? null, + 'authority_type' => $chunk['authority_type'] ?? null, + 'jurisdiction' => $chunk['jurisdiction'] ?? null, ]; } + private function fetchDocSummaries(array $docIds): array + { + if (!$docIds) { + return []; + } + try { + $db = dbnToolsRagDb(); + $placeholders = implode(',', array_fill(0, count($docIds), '?')); + $stmt = $db->prepare( + "SELECT document_id, summary FROM doc_summaries + WHERE document_id IN ({$placeholders}) AND summary != ''" + ); + $stmt->execute(array_values($docIds)); + return array_column($stmt->fetchAll(PDO::FETCH_ASSOC), 'summary', 'document_id'); + } catch (Throwable) { + return []; + } + } + private function citationConfidence(array $hits): string { if (!$hits) { diff --git a/index.php b/index.php index a4ea4bf..ddb731b 100644 --- a/index.php +++ b/index.php @@ -9,35 +9,147 @@ $authenticated = dbnToolsIsAuthenticated(); - Do Better Norge Legal Tools - - - - - + Do Better Norge — AI Legal Research + + + + + - + -
-
-

Do Better Norge

-

Legal Tools

-

Legal information and preparation support, not final legal advice.

-
- - - -
- - +
+ +
+
+
+

Do Better Norge

+

AI Legal Research

+

Source-cited answers from curated Norwegian law corpora

+ Powered by CaveauAI
-

- -
-
+ Access Legal Tools → + + + +
+
+

How it works

+

From question to reviewed, cited answer in three steps

+
+
+
01
+

Curated legal corpus

+

Norwegian family law, ECHR rulings, and Lovdata sources are indexed, chunked, and embedded by CaveauAI’s ingestion pipeline.

+
+
+
02
+

Hybrid retrieval

+

Your question triggers vector similarity search and keyword retrieval — sources are scored, re-ranked, and presented as a labelled evidence trail.

+
+
+
03
+

Reviewed, cited answer

+

A post-generation reviewer checks every claim against retrieved sources before the answer reaches you. Citations are attached to real corpus documents.

+
+
+
+
+ +
+
+

Five tools, one corpus

+
+
+ Ask +

Ask

+

Source-grounded legal questions with citations and explicit uncertainty notes.

+
+
+ Search +

Search

+

Retrieve up to seven relevant legal sources with titles, sections, and excerpts.

+
+
+ Summarize +

Summarize

+

Extract facts, dates, parties, and legal references from pasted text.

+
+
+ Timeline +

Timeline

+

Build a chronological event sequence from case notes or documents.

+
+
+ Redact +

Redact

+

Remove sensitive personal data with configurable Nordic / ECHR / Global profiles.

+
+
+
+
+ +
+
+
+

Every answer shows its work

+

Alongside each answer, the evidence trail shows which sources were retrieved, how they scored, which claims they support, and what remains uncertain. The legal reviewer pass checks all claims against the corpus before the answer is returned.

+
    +
  • Source document, section, and authority type
  • +
  • Similarity score per retrieved chunk
  • +
  • Reviewer decision: approved / revised / insufficient support
  • +
  • Explicit uncertainty statement when the corpus can’t support a claim
  • +
+
+ +
+
+ +
+
+

Do Better Norge

+

Access Legal Tools

+

Legal information and preparation support, not final legal advice.

+
+ + + +
+ + +
+

+
+
+
+ + + +