409 lines
17 KiB
HTML
409 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Astraea — Washington Family Law, Decoded</title>
|
|
<meta name="description" content="A constellation of Washington State family-law specialist agents. Ask about divorce, custody, child support, property, and protection orders.">
|
|
<style>
|
|
:root {
|
|
--bg: #070b1a;
|
|
--bg2: #0b1228;
|
|
--panel: #101832;
|
|
--panel2: #141d3d;
|
|
--line: #233055;
|
|
--text: #e8edfb;
|
|
--muted: #8b98c4;
|
|
--accent: #7dd3fc;
|
|
--gold: #f5d06f;
|
|
}
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
html, body { height: 100%; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
background: radial-gradient(1200px 700px at 50% -10%, #18254f 0%, var(--bg2) 45%, var(--bg) 100%);
|
|
color: var(--text);
|
|
overflow-x: hidden;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
#stars { position: fixed; inset: 0; z-index: 0; pointer-events: none; }
|
|
|
|
.wrap { position: relative; z-index: 1; max-width: 1120px; margin: 0 auto; padding: 0 22px 80px; }
|
|
|
|
header {
|
|
padding: 46px 0 8px;
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
}
|
|
.brand { display: flex; align-items: center; gap: 14px; }
|
|
.logo {
|
|
width: 46px; height: 46px; border-radius: 13px;
|
|
background: linear-gradient(135deg, #6ea8ff, #a78bfa);
|
|
display: grid; place-items: center; font-size: 26px;
|
|
box-shadow: 0 0 26px rgba(124, 155, 255, .55);
|
|
}
|
|
.brand h1 { font-size: 26px; font-weight: 700; letter-spacing: .4px; }
|
|
.brand h1 span { color: var(--gold); }
|
|
.brand .sub { color: var(--muted); font-size: 13px; margin-top: 2px; letter-spacing: .3px; }
|
|
.pill {
|
|
font-size: 12px; color: var(--muted); border: 1px solid var(--line);
|
|
padding: 6px 12px; border-radius: 999px; display: inline-flex; align-items: center; gap: 7px;
|
|
}
|
|
.dot { width: 8px; height: 8px; border-radius: 50%; background: #4ade80; box-shadow: 0 0 10px #4ade80; }
|
|
|
|
.hero { padding: 26px 0 34px; max-width: 720px; }
|
|
.hero h2 { font-size: clamp(28px, 5vw, 46px); line-height: 1.12; font-weight: 800; letter-spacing: -.5px; }
|
|
.hero h2 .grad {
|
|
background: linear-gradient(90deg, #7dd3fc, #a78bfa, #f0abfc);
|
|
-webkit-background-clip: text; background-clip: text; color: transparent;
|
|
}
|
|
.hero p { color: var(--muted); font-size: 16.5px; line-height: 1.6; margin-top: 14px; max-width: 640px; }
|
|
|
|
.section-label { font-size: 12px; text-transform: uppercase; letter-spacing: 2px; color: var(--muted); margin: 10px 0 16px; }
|
|
|
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; }
|
|
.card {
|
|
background: linear-gradient(180deg, var(--panel2), var(--panel));
|
|
border: 1px solid var(--line); border-radius: 16px; padding: 18px 18px 16px;
|
|
cursor: pointer; transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease;
|
|
position: relative; overflow: hidden;
|
|
}
|
|
.card::before {
|
|
content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
|
|
background: var(--c, var(--accent)); opacity: .85;
|
|
}
|
|
.card:hover { transform: translateY(-3px); border-color: var(--c, var(--accent)); box-shadow: 0 14px 34px rgba(0,0,0,.4); }
|
|
.card.active { border-color: var(--c, var(--accent)); box-shadow: 0 0 0 1px var(--c, var(--accent)), 0 16px 40px rgba(0,0,0,.5); }
|
|
.card .emoji { font-size: 30px; line-height: 1; }
|
|
.card h3 { font-size: 16.5px; font-weight: 700; margin: 12px 0 3px; }
|
|
.card .tag { font-size: 12px; color: var(--c, var(--accent)); font-weight: 600; margin-bottom: 8px; }
|
|
.card p { font-size: 13px; color: var(--muted); line-height: 1.5; }
|
|
|
|
.chat-panel {
|
|
margin-top: 38px;
|
|
background: linear-gradient(180deg, var(--panel2), var(--panel));
|
|
border: 1px solid var(--line); border-radius: 20px;
|
|
display: flex; flex-direction: column; overflow: hidden;
|
|
box-shadow: 0 30px 70px rgba(0,0,0,.45);
|
|
}
|
|
.chat-head {
|
|
padding: 16px 20px; border-bottom: 1px solid var(--line);
|
|
display: flex; align-items: center; gap: 13px;
|
|
}
|
|
.chat-head .emoji { font-size: 26px; }
|
|
.chat-head .who { font-weight: 700; font-size: 15.5px; }
|
|
.chat-head .who .scope { color: var(--muted); font-weight: 500; font-size: 12.5px; margin-top: 1px; }
|
|
.chips { margin-left: auto; display: flex; gap: 7px; flex-wrap: wrap; justify-content: flex-end; }
|
|
.chip {
|
|
font-size: 12px; padding: 6px 11px; border-radius: 999px; cursor: pointer;
|
|
border: 1px solid var(--line); color: var(--muted); background: transparent;
|
|
transition: all .15s ease; white-space: nowrap;
|
|
}
|
|
.chip:hover { color: var(--text); border-color: var(--muted); }
|
|
.chip.on { color: #06121f; background: var(--c, var(--accent)); border-color: var(--c, var(--accent)); font-weight: 700; }
|
|
|
|
.messages { padding: 22px; min-height: 240px; max-height: 520px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
|
|
.msg { max-width: 78%; display: flex; flex-direction: column; }
|
|
.msg.user { align-self: flex-end; align-items: flex-end; }
|
|
.msg.agent { align-self: flex-start; align-items: flex-start; }
|
|
.bubble {
|
|
padding: 13px 16px; border-radius: 16px; font-size: 15px; line-height: 1.6; white-space: pre-wrap; word-break: break-word;
|
|
}
|
|
.msg.user .bubble { background: linear-gradient(135deg, #3b82f6, #6366f1); color: #fff; border-bottom-right-radius: 4px; }
|
|
.msg.agent .bubble { background: var(--panel2); border: 1px solid var(--line); border-bottom-left-radius: 4px; }
|
|
.msg .who-label { font-size: 11.5px; color: var(--muted); margin: 0 4px 5px; }
|
|
|
|
.citations { margin-top: 10px; width: 100%; }
|
|
.cite-toggle {
|
|
font-size: 12px; color: var(--gold); cursor: pointer; border: none; background: none;
|
|
display: inline-flex; align-items: center; gap: 5px; padding: 4px 2px;
|
|
}
|
|
.cite-list { margin-top: 8px; display: none; flex-direction: column; gap: 8px; }
|
|
.cite-list.open { display: flex; }
|
|
.cite {
|
|
background: #0c142e; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px;
|
|
font-size: 12.5px; color: var(--muted);
|
|
}
|
|
.cite b { color: var(--text); display: block; margin-bottom: 3px; font-size: 13px; }
|
|
.cite .src { color: var(--gold); font-size: 11px; }
|
|
|
|
.typing { display: inline-flex; gap: 5px; align-items: center; padding: 6px 2px; }
|
|
.typing i { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); animation: blink 1.2s infinite; }
|
|
.typing i:nth-child(2) { animation-delay: .2s; }
|
|
.typing i:nth-child(3) { animation-delay: .4s; }
|
|
@keyframes blink { 0%,100% { opacity: .25; } 50% { opacity: 1; } }
|
|
|
|
.composer { border-top: 1px solid var(--line); padding: 14px 16px; display: flex; gap: 10px; align-items: flex-end; }
|
|
.composer textarea {
|
|
flex: 1; resize: none; background: #0c142e; border: 1px solid var(--line); color: var(--text);
|
|
border-radius: 12px; padding: 12px 14px; font-size: 15px; font-family: inherit; line-height: 1.5;
|
|
max-height: 140px; outline: none; transition: border-color .15s ease;
|
|
}
|
|
.composer textarea:focus { border-color: var(--c, var(--accent)); }
|
|
.send {
|
|
border: none; border-radius: 12px; padding: 13px 20px; font-size: 15px; font-weight: 700;
|
|
color: #06121f; background: var(--c, var(--accent)); cursor: pointer; transition: transform .1s ease, opacity .2s;
|
|
}
|
|
.send:active { transform: scale(.96); }
|
|
.send:disabled { opacity: .45; cursor: default; }
|
|
.hint { font-size: 11.5px; color: var(--muted); text-align: center; padding: 0 22px 14px; }
|
|
|
|
.disclaimer {
|
|
margin-top: 26px; font-size: 12.5px; color: var(--muted); text-align: center; line-height: 1.6;
|
|
border-top: 1px solid var(--line); padding-top: 20px;
|
|
}
|
|
footer { margin-top: 20px; text-align: center; }
|
|
.bmac {
|
|
display: inline-flex; align-items: center; gap: 9px; color: var(--text); text-decoration: none;
|
|
background: var(--panel2); border: 1px solid var(--line); padding: 10px 18px; border-radius: 999px;
|
|
font-size: 13.5px; font-weight: 600; transition: all .15s ease;
|
|
}
|
|
.bmac:hover { border-color: var(--gold); box-shadow: 0 0 18px rgba(245,208,111,.25); }
|
|
.bmac .cup { font-size: 17px; }
|
|
|
|
#mascot {
|
|
position: fixed; z-index: 2; pointer-events: none; will-change: transform;
|
|
font-size: 24px; filter: drop-shadow(0 0 8px rgba(255,220,150,.8)); opacity: .9;
|
|
}
|
|
@media (max-width: 640px) {
|
|
.chips { margin-left: 0; width: 100%; justify-content: flex-start; }
|
|
.chat-head { flex-wrap: wrap; }
|
|
.msg { max-width: 92%; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="stars"></canvas>
|
|
<div id="mascot">✦</div>
|
|
|
|
<div class="wrap">
|
|
<header>
|
|
<div class="brand">
|
|
<div class="logo">⚖️</div>
|
|
<div>
|
|
<h1>ASTRA<span>EA</span></h1>
|
|
<div class="sub">Washington Family Law, decoded by specialist agents</div>
|
|
</div>
|
|
</div>
|
|
<div class="pill"><span class="dot"></span> <span id="statusText">Engines online</span></div>
|
|
</header>
|
|
|
|
<section class="hero">
|
|
<h2>Eight specialists.<br>One <span class="grad">constellation</span> of Washington family law.</h2>
|
|
<p>Ask a divorce, custody, child-support, property, or protection-order question. Each specialist answers from the actual Revised Code of Washington — with citations — not from thin air.</p>
|
|
</section>
|
|
|
|
<div class="section-label">Choose your specialist</div>
|
|
<div class="grid" id="grid"></div>
|
|
|
|
<section class="chat-panel" id="chatPanel">
|
|
<div class="chat-head">
|
|
<div class="emoji" id="headEmoji">🧭</div>
|
|
<div>
|
|
<div class="who" id="headName">Navigator</div>
|
|
<div class="who"><div class="scope" id="headScope">Where to start</div></div>
|
|
</div>
|
|
<div class="chips" id="chips"></div>
|
|
</div>
|
|
<div class="messages" id="messages"></div>
|
|
<div class="composer">
|
|
<textarea id="input" rows="1" placeholder="Ask a Washington family-law question…"></textarea>
|
|
<button class="send" id="sendBtn">Ask</button>
|
|
</div>
|
|
<div class="hint">Answers cite the Revised Code of Washington. This is legal information, not advice.</div>
|
|
</section>
|
|
|
|
<div class="disclaimer">{{ disclaimer }}</div>
|
|
<footer>
|
|
<a class="bmac" href="https://buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener">
|
|
<span class="cup">☕</span> Support Astraea
|
|
</a>
|
|
</footer>
|
|
</div>
|
|
|
|
<script>
|
|
const AGENTS = {{ agents | tojson }};
|
|
const DISCLAIMER = {{ disclaimer | tojson }};
|
|
|
|
// ---- Starfield + cursor mascot ----
|
|
const cvs = document.getElementById('stars');
|
|
const ctx = cvs.getContext('2d');
|
|
let stars = [];
|
|
function sizeCanvas() {
|
|
cvs.width = window.innerWidth; cvs.height = window.innerHeight;
|
|
stars = Array.from({length: 130}, () => ({
|
|
x: Math.random() * cvs.width, y: Math.random() * cvs.height,
|
|
r: Math.random() * 1.4 + .3, tw: Math.random() * Math.PI * 2,
|
|
sp: Math.random() * .02 + .005
|
|
}));
|
|
}
|
|
sizeCanvas();
|
|
window.addEventListener('resize', sizeCanvas);
|
|
function drawStars() {
|
|
ctx.clearRect(0, 0, cvs.width, cvs.height);
|
|
for (const s of stars) {
|
|
s.tw += s.sp;
|
|
const a = .35 + Math.abs(Math.sin(s.tw)) * .65;
|
|
ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
|
|
ctx.fillStyle = `rgba(190,210,255,${a})`; ctx.fill();
|
|
}
|
|
requestAnimationFrame(drawStars);
|
|
}
|
|
drawStars();
|
|
|
|
const mascot = document.getElementById('mascot');
|
|
let mx = innerWidth/2, my = innerHeight/3, cx = mx, cy = my;
|
|
addEventListener('mousemove', e => { mx = e.clientX; my = e.clientY; });
|
|
(function follow() {
|
|
cx += (mx - cx) * .08; cy += (my - cy) * .08;
|
|
mascot.style.transform = `translate(${cx - 8}px, ${cy - 30}px) rotate(${Math.sin(Date.now()/700)*20}deg)`;
|
|
requestAnimationFrame(follow);
|
|
})();
|
|
|
|
// ---- App state ----
|
|
const grid = document.getElementById('grid');
|
|
const chips = document.getElementById('chips');
|
|
const messages = document.getElementById('messages');
|
|
const input = document.getElementById('input');
|
|
const sendBtn = document.getElementById('sendBtn');
|
|
const statusText = document.getElementById('statusText');
|
|
let active = AGENTS[0].id;
|
|
const history = {}; // per-agent turn history
|
|
AGENTS.forEach(a => history[a.id] = []);
|
|
|
|
function accent(id) { const a = AGENTS.find(x => x.id === id); return a ? a.accent : '#7dd3fc'; }
|
|
|
|
function renderGrid() {
|
|
grid.innerHTML = '';
|
|
AGENTS.forEach(a => {
|
|
const c = document.createElement('div');
|
|
c.className = 'card' + (a.id === active ? ' active' : '');
|
|
c.style.setProperty('--c', a.accent);
|
|
c.innerHTML = `<div class="emoji">${a.emoji}</div><h3>${a.name}</h3>
|
|
<div class="tag">${a.tagline}</div><p>${a.description}</p>`;
|
|
c.onclick = () => selectAgent(a.id, true);
|
|
grid.appendChild(c);
|
|
});
|
|
}
|
|
|
|
function renderChips() {
|
|
chips.innerHTML = '';
|
|
AGENTS.forEach(a => {
|
|
const b = document.createElement('button');
|
|
b.className = 'chip' + (a.id === active ? ' on' : '');
|
|
b.style.setProperty('--c', a.accent);
|
|
b.textContent = a.emoji + ' ' + a.name.split(' & ')[0].split(' ')[0];
|
|
b.onclick = () => selectAgent(a.id, false);
|
|
chips.appendChild(b);
|
|
});
|
|
}
|
|
|
|
function selectAgent(id, scroll) {
|
|
active = id;
|
|
const a = AGENTS.find(x => x.id === id);
|
|
document.getElementById('headEmoji').textContent = a.emoji;
|
|
document.getElementById('headName').textContent = a.name;
|
|
document.getElementById('headScope').textContent = a.tagline;
|
|
document.getElementById('chatPanel').style.setProperty('--c', a.accent);
|
|
document.getElementById('chatPanel').style.borderColor = a.accent + '55';
|
|
renderGrid(); renderChips();
|
|
messages.innerHTML = '';
|
|
renderHistory();
|
|
if (scroll) document.getElementById('chatPanel').scrollIntoView({behavior: 'smooth', block: 'start'});
|
|
input.focus();
|
|
}
|
|
|
|
function renderHistory() {
|
|
const h = history[active] || [];
|
|
h.forEach(t => addBubble(t.role, t.content, t.citations));
|
|
}
|
|
|
|
function addBubble(role, text, citations) {
|
|
const m = document.createElement('div');
|
|
m.className = 'msg ' + role;
|
|
const label = document.createElement('div');
|
|
label.className = 'who-label';
|
|
label.textContent = role === 'user' ? 'You' : (AGENTS.find(x => x.id === active)?.name || 'Agent');
|
|
const b = document.createElement('div');
|
|
b.className = 'bubble';
|
|
b.textContent = text;
|
|
m.appendChild(label); m.appendChild(b);
|
|
if (role === 'assistant' && citations && citations.length) {
|
|
const cw = document.createElement('div'); cw.className = 'citations';
|
|
const t = document.createElement('button'); t.className = 'cite-toggle';
|
|
t.innerHTML = '🔗 ' + citations.length + ' source' + (citations.length > 1 ? 's' : '');
|
|
const list = document.createElement('div'); list.className = 'cite-list';
|
|
citations.forEach(c => {
|
|
const item = document.createElement('div'); item.className = 'cite';
|
|
item.innerHTML = `<b>${c.title}</b><div>${c.snippet}</div><div class="src">${c.source}</div>`;
|
|
list.appendChild(item);
|
|
});
|
|
t.onclick = () => list.classList.toggle('open');
|
|
cw.appendChild(t); cw.appendChild(list);
|
|
m.appendChild(cw);
|
|
}
|
|
messages.appendChild(m);
|
|
messages.scrollTop = messages.scrollHeight;
|
|
}
|
|
|
|
function typingIndicator(on) {
|
|
document.querySelectorAll('.typing').forEach(e => e.remove());
|
|
if (!on) return;
|
|
const m = document.createElement('div'); m.className = 'msg agent';
|
|
const t = document.createElement('div'); t.className = 'typing';
|
|
t.innerHTML = '<i></i><i></i><i></i>';
|
|
m.appendChild(t); messages.appendChild(m);
|
|
messages.scrollTop = messages.scrollHeight;
|
|
}
|
|
|
|
async function send() {
|
|
const text = input.value.trim();
|
|
if (!text) return;
|
|
input.value = ''; input.style.height = 'auto';
|
|
addBubble('user', text);
|
|
history[active].push({role: 'user', content: text});
|
|
typingIndicator(true); sendBtn.disabled = true;
|
|
try {
|
|
const r = await fetch('/api/chat', {
|
|
method: 'POST', headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({agent_id: active, message: text, history: history[active].slice(0, -1)})
|
|
});
|
|
const d = await r.json();
|
|
typingIndicator(false);
|
|
if (d.answer) {
|
|
addBubble('assistant', d.answer, d.citations);
|
|
history[active].push({role: 'assistant', content: d.answer, citations: d.citations});
|
|
} else {
|
|
addBubble('assistant', d.error || 'Something went wrong. Please try again.', null);
|
|
}
|
|
} catch (e) {
|
|
typingIndicator(false);
|
|
addBubble('assistant', 'Could not reach the legal engine. Please try again.', null);
|
|
} finally {
|
|
sendBtn.disabled = false; input.focus();
|
|
}
|
|
}
|
|
|
|
sendBtn.onclick = send;
|
|
input.addEventListener('keydown', e => {
|
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
|
|
});
|
|
input.addEventListener('input', () => {
|
|
input.style.height = 'auto'; input.style.height = Math.min(input.scrollHeight, 140) + 'px';
|
|
});
|
|
|
|
// ---- Boot ----
|
|
renderGrid(); renderChips(); selectAgent(active, false);
|
|
fetch('/api/health').then(r => r.json()).then(d => {
|
|
if (d.ok) {
|
|
statusText.textContent = d.index.ready
|
|
? `${d.agent_count} agents · ${d.index.chunks} law passages indexed`
|
|
: 'Indexing law library…';
|
|
}
|
|
}).catch(() => { statusText.textContent = 'Engine warming up'; });
|
|
</script>
|
|
|
|
<!-- Umami analytics beacon -->
|
|
<script async src="https://analytics.thetempleofdoom.com/script.js" data-website-id="c8c2f69c-4448-40ab-9848-02179f10c001"></script>
|
|
</body>
|
|
</html>
|