Astraea v2: auth+profiles+About-Me, documents, TTS, Twilio/SMTP missions, DV safety, conversation memory, FLUX landing
This commit is contained in:
@@ -3,406 +3,333 @@
|
||||
<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.">
|
||||
<title>Astraea — Your Attorney</title>
|
||||
<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; }
|
||||
:root { --bg:#070b1a; --panel:#101832; --panel2:#141d3d; --line:#233055; --text:#e8edfb; --muted:#8b98c4; --gold:#f5d06f; }
|
||||
* { box-sizing:border-box; margin:0; padding:0; }
|
||||
body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; background:radial-gradient(1200px 700px at 50% -10%,#18254f 0%,var(--bg) 60%); color:var(--text); min-height:100vh; }
|
||||
canvas { position:fixed; inset:0; z-index:0; pointer-events:none; }
|
||||
.app { position:relative; z-index:1; max-width:1120px; margin:0 auto; padding:0 20px 60px; }
|
||||
header { padding:20px 0; display:flex; align-items:center; justify-content:space-between; gap:14px; }
|
||||
.brand { display:flex; align-items:center; gap:11px; }
|
||||
.logo { width:40px; height:40px; border-radius:11px; background:linear-gradient(135deg,#6ea8ff,#a78bfa); display:grid; place-items:center; font-size:20px; }
|
||||
.brand h1 { font-size:20px; font-weight:800; }
|
||||
.brand h1 span { color:var(--gold); }
|
||||
.top-actions { display:flex; align-items:center; gap:10px; }
|
||||
.avatar-sm { width:36px; height:36px; border-radius:50%; display:grid; place-items:center; font-size:17px; color:#fff; cursor:pointer; }
|
||||
.btn { border:1px solid var(--line); background:var(--panel2); color:var(--text); border-radius:10px; padding:9px 15px; font-size:13.5px; font-weight:600; cursor:pointer; transition:.15s; }
|
||||
.btn:hover { border-color:#3b82f6; }
|
||||
.btn.primary { background:linear-gradient(135deg,#3b82f6,#6366f1); border:none; color:#fff; }
|
||||
.hero { padding:12px 0 20px; }
|
||||
.hero h2 { font-size:clamp(24px,4vw,34px); font-weight:800; letter-spacing:-.5px; }
|
||||
.hero p { color:var(--muted); margin-top:8px; font-size:15px; }
|
||||
.label { font-size:11.5px; text-transform:uppercase; letter-spacing:2px; color:var(--muted); margin:14px 0 12px; }
|
||||
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(235px,1fr)); gap:13px; }
|
||||
.card { background:linear-gradient(180deg,var(--panel2),var(--panel)); border:1px solid var(--line); border-radius:15px; padding:16px; cursor:pointer; position:relative; overflow:hidden; transition:.18s; }
|
||||
.card::before { content:""; position:absolute; inset:0 0 auto 0; height:3px; background:var(--c,#7dd3fc); }
|
||||
.card:hover { transform:translateY(-3px); border-color:var(--c,#7dd3fc); }
|
||||
.card.active { border-color:var(--c,#7dd3fc); box-shadow:0 0 0 1px var(--c,#7dd3fc),0 14px 34px rgba(0,0,0,.5); }
|
||||
.card .emoji { font-size:27px; }
|
||||
.card h3 { font-size:15.5px; font-weight:700; margin:10px 0 3px; }
|
||||
.card .tag { font-size:12px; color:var(--c,#7dd3fc); font-weight:600; margin-bottom:6px; }
|
||||
.card p { font-size:12.5px; color:var(--muted); line-height:1.45; }
|
||||
|
||||
.wrap { position: relative; z-index: 1; max-width: 1120px; margin: 0 auto; padding: 0 22px 80px; }
|
||||
.chat-panel { margin-top:30px; background:linear-gradient(180deg,var(--panel2),var(--panel)); border:1px solid var(--line); border-radius:18px; display:flex; flex-direction:column; overflow:hidden; }
|
||||
.chat-head { padding:14px 18px; border-bottom:1px solid var(--line); display:flex; align-items:center; gap:11px; flex-wrap:wrap; }
|
||||
.chat-head .emoji { font-size:24px; }
|
||||
.chat-head .who { font-weight:700; font-size:15px; }
|
||||
.chat-head .scope { color:var(--muted); font-weight:500; font-size:12px; }
|
||||
.chips { margin-left:auto; display:flex; gap:6px; flex-wrap:wrap; }
|
||||
.chip { font-size:11.5px; padding:5px 10px; border-radius:999px; border:1px solid var(--line); color:var(--muted); background:transparent; cursor:pointer; white-space:nowrap; }
|
||||
.chip.on { color:#06121f; background:var(--c,#7dd3fc); border-color:var(--c,#7dd3fc); font-weight:700; }
|
||||
.messages { padding:20px; min-height:220px; max-height:520px; overflow-y:auto; display:flex; flex-direction:column; gap:15px; }
|
||||
.msg { max-width:80%; display:flex; flex-direction:column; }
|
||||
.msg.user { align-self:flex-end; align-items:flex-end; }
|
||||
.msg.agent { align-self:flex-start; }
|
||||
.bubble { padding:12px 15px; border-radius:15px; font-size:14.5px; 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:11px; color:var(--muted); margin:0 4px 4px; }
|
||||
.speak { margin-top:6px; background:none; border:none; color:var(--gold); cursor:pointer; font-size:12px; }
|
||||
.citations { margin-top:9px; }
|
||||
.cite-toggle { font-size:12px; color:var(--gold); cursor:pointer; background:none; border:none; }
|
||||
.cite-list { margin-top:7px; display:none; flex-direction:column; gap:7px; }
|
||||
.cite-list.open { display:flex; }
|
||||
.cite { background:#0c142e; border:1px solid var(--line); border-radius:9px; padding:9px 11px; font-size:12px; color:var(--muted); }
|
||||
.cite b { color:var(--text); display:block; margin-bottom:2px; }
|
||||
.typing { display:inline-flex; gap:5px; padding:6px 2px; }
|
||||
.typing i { width:7px; height:7px; border-radius:50%; background:var(--muted); animation:bl 1.2s infinite; }
|
||||
.typing i:nth-child(2){ animation-delay:.2s; } .typing i:nth-child(3){ animation-delay:.4s; }
|
||||
@keyframes bl { 0%,100%{opacity:.25;} 50%{opacity:1;} }
|
||||
.composer { border-top:1px solid var(--line); padding:13px 15px; display:flex; gap:9px; align-items:flex-end; }
|
||||
.composer textarea { flex:1; resize:none; background:#0c142e; border:1px solid var(--line); color:var(--text); border-radius:11px; padding:11px 13px; font-size:14.5px; font-family:inherit; line-height:1.5; max-height:130px; outline:none; }
|
||||
.composer textarea:focus { border-color:var(--c,#7dd3fc); }
|
||||
.send { border:none; border-radius:11px; padding:12px 18px; font-size:14px; font-weight:700; color:#06121f; background:var(--c,#7dd3fc); cursor:pointer; }
|
||||
.send:disabled { opacity:.45; }
|
||||
.tools-row { display:flex; gap:8px; padding:0 15px 12px; flex-wrap:wrap; }
|
||||
.tools-row .btn { font-size:12.5px; padding:8px 13px; }
|
||||
|
||||
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%; }
|
||||
}
|
||||
.modal-bg { position:fixed; inset:0; background:rgba(3,5,12,.8); z-index:50; display:none; align-items:center; justify-content:center; padding:20px; }
|
||||
.modal-bg.open { display:flex; }
|
||||
.modal { background:linear-gradient(180deg,var(--panel2),var(--panel)); border:1px solid var(--line); border-radius:18px; width:100%; max-width:620px; max-height:90vh; overflow-y:auto; padding:26px; }
|
||||
.modal h2 { font-size:20px; font-weight:800; margin-bottom:4px; }
|
||||
.modal .sub { color:var(--muted); font-size:13px; margin-bottom:18px; }
|
||||
.modal .close { float:right; background:none; border:none; color:var(--muted); font-size:22px; cursor:pointer; }
|
||||
.fld { margin-bottom:15px; }
|
||||
.fld label { display:block; font-size:12.5px; color:var(--muted); margin-bottom:6px; }
|
||||
.fld input, .fld textarea, .fld select { width:100%; background:#0c142e; border:1px solid var(--line); color:var(--text); border-radius:10px; padding:11px 13px; font-size:14px; outline:none; font-family:inherit; }
|
||||
.fld textarea { min-height:110px; resize:vertical; }
|
||||
.fld input:focus, .fld textarea:focus { border-color:#6366f1; }
|
||||
.avatar-grid { display:grid; grid-template-columns:repeat(6,1fr); gap:10px; margin-bottom:18px; }
|
||||
.avatar-opt { aspect-ratio:1; border-radius:50%; display:grid; place-items:center; font-size:22px; color:#fff; cursor:pointer; border:2px solid transparent; }
|
||||
.avatar-opt.sel { border-color:#fff; box-shadow:0 0 0 3px rgba(255,255,255,.2); }
|
||||
.switch { display:flex; align-items:center; gap:10px; }
|
||||
.switch input { width:auto; }
|
||||
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:13px; }
|
||||
.doc-result { background:#0c142e; border:1px solid var(--line); border-radius:12px; padding:16px; font-size:13px; white-space:pre-wrap; max-height:50vh; overflow-y:auto; line-height:1.6; margin-top:14px; }
|
||||
footer { text-align:center; margin-top:26px; font-size:12px; color:var(--muted); }
|
||||
footer a { color:var(--muted); }
|
||||
@media (max-width:640px){ .msg{max-width:92%;} .grid2{grid-template-columns:1fr;} .avatar-grid{grid-template-columns:repeat(4,1fr);} }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="stars"></canvas>
|
||||
<div id="mascot">✦</div>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="app">
|
||||
<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 class="brand"><div class="logo">⚖️</div><div><h1>ASTRA<span>EA</span></h1></div></div>
|
||||
<div class="top-actions">
|
||||
<button class="btn" onclick="openDocs()">📄 Documents</button>
|
||||
<button class="btn" onclick="openSettings()">⚙️ Settings</button>
|
||||
<div class="avatar-sm" id="myAvatar" onclick="openProfile()" style="background:linear-gradient(135deg,#6ea8ff,#a78bfa)">⚖</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="hero">
|
||||
<h2>Eight specialists. Your case.</h2>
|
||||
<p>Ask anything. Every answer is grounded in Washington statute and verified case law — and it already knows your situation.</p>
|
||||
</div>
|
||||
|
||||
<div class="section-label">Choose your specialist</div>
|
||||
<div class="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><div class="who" id="headName">Navigator</div><div class="scope" id="headScope">Where to start</div></div>
|
||||
<div class="chips" id="chips"></div>
|
||||
</div>
|
||||
<div class="messages" id="messages"></div>
|
||||
<div class="tools-row">
|
||||
<button class="btn" onclick="openDocs()">📄 Prep a document</button>
|
||||
<button class="btn" onclick="openMission()">📨 Send on my behalf</button>
|
||||
<button class="btn" onclick="clearConv()">🗑 Clear chat</button>
|
||||
</div>
|
||||
<div class="composer">
|
||||
<textarea id="input" rows="1" placeholder="Ask a Washington family-law question…"></textarea>
|
||||
<button class="btn" id="micBtn" onclick="toggleMic()" title="Speak" style="flex:0 0 auto">🎤</button>
|
||||
<textarea id="input" rows="1" placeholder="Ask your attorney…"></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>
|
||||
<footer>Astraea gives practical legal guidance grounded in Washington law — not a substitute for a licensed attorney. · <a href="https://buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener">☕ Support</a></footer>
|
||||
</div>
|
||||
|
||||
<!-- Profile modal -->
|
||||
<div class="modal-bg" id="profileModal"><div class="modal">
|
||||
<button class="close" onclick="closeModal('profileModal')">×</button>
|
||||
<h2>Your profile</h2><div class="sub">This context shapes every answer — your attorney already knows your situation.</div>
|
||||
<div class="fld"><label>Display name</label><input id="pfName" placeholder="Your name"></div>
|
||||
<div class="fld"><label>Display picture</label></div>
|
||||
<div class="avatar-grid" id="avatarGrid"></div>
|
||||
<div class="fld"><label>About Me — everything your attorney should know (your story, dates, names, what you're going through)</label><textarea id="pfAbout" placeholder="e.g. Married 2011, two children (ages 9 and 12), separated June 2025, own a home in King County, spouse filed a parenting plan…"></textarea></div>
|
||||
<button class="btn primary" style="width:100%" onclick="saveProfile()">Save profile</button>
|
||||
</div></div>
|
||||
|
||||
<!-- Settings modal -->
|
||||
<div class="modal-bg" id="settingsModal"><div class="modal">
|
||||
<button class="close" onclick="closeModal('settingsModal')">×</button>
|
||||
<h2>Settings</h2><div class="sub">Comms channels your attorney can use for missions, and safety.</div>
|
||||
<div class="fld"><label>Twilio Account SID</label><input id="twSid" placeholder="AC…"></div>
|
||||
<div class="fld"><label>Twilio Auth Token</label><input id="twToken" type="password" placeholder="••••"></div>
|
||||
<div class="fld"><label>Twilio From number (E.164)</label><input id="twFrom" placeholder="+14255550123"></div>
|
||||
<div class="grid2">
|
||||
<div class="fld"><label>SMTP host</label><input id="smHost" placeholder="smtp.gmail.com"></div>
|
||||
<div class="fld"><label>SMTP port</label><input id="smPort" value="587"></div>
|
||||
</div>
|
||||
<div class="grid2">
|
||||
<div class="fld"><label>SMTP username</label><input id="smUser"></div>
|
||||
<div class="fld"><label>SMTP password</label><input id="smPass" type="password"></div>
|
||||
</div>
|
||||
<div class="fld"><label>Email "from" address</label><input id="emFrom" placeholder="you@example.com"></div>
|
||||
<div class="fld"><label>TTS voice</label><select id="ttsVoice"></select></div>
|
||||
<div class="fld switch"><label style="margin:0">🔒 No-contact order in effect</label><input type="checkbox" id="ncOrder"></div>
|
||||
<div class="fld"><label>Protected person's name (blocked from contact)</label><input id="ncName" placeholder="Full name of the person you must not contact"></div>
|
||||
<div class="fld switch"><label style="margin:0">⚠️ Domestic violence is a factor in my case</label><input type="checkbox" id="dvFactor"></div>
|
||||
<div class="fld switch"><label style="margin:0">🛡️ Active protection order in effect (RCW 7.105)</label><input type="checkbox" id="poOrder"></div>
|
||||
<div class="fld"><label>Safety notes — threats, incidents, what you're afraid of</label><textarea id="safetyNotes" style="min-height:70px" placeholder="e.g. There has been physical violence. I'm afraid for my safety and the children's safety."></textarea></div>
|
||||
<button class="btn primary" style="width:100%" onclick="saveSettings()">Save settings</button>
|
||||
</div></div>
|
||||
|
||||
<!-- Documents modal -->
|
||||
<div class="modal-bg" id="docsModal"><div class="modal">
|
||||
<button class="close" onclick="closeModal('docsModal')">×</button>
|
||||
<h2>Prepare a document</h2><div class="sub">Court-ready Washington divorce paperwork, drafted to your facts.</div>
|
||||
<div class="fld"><label>Document</label><select id="docType"></select></div>
|
||||
<div class="fld"><label>Specific instructions (optional)</label><textarea id="docInst" placeholder="e.g. I want primary residential time with the children, propose alternate weekends for the other parent."></textarea></div>
|
||||
<button class="btn primary" style="width:100%" id="docBtn" onclick="genDoc()">Generate document</button>
|
||||
<div class="doc-result" id="docResult" style="display:none"></div>
|
||||
</div></div>
|
||||
|
||||
<!-- Mission modal -->
|
||||
<div class="modal-bg" id="missionModal"><div class="modal">
|
||||
<button class="close" onclick="closeModal('missionModal')">×</button>
|
||||
<h2>Send on your behalf</h2><div class="sub">Your attorney will format this professionally. A no-contact order blocks protected recipients.</div>
|
||||
<div class="fld"><label>Type</label><select id="msType"><option value="sms">Text (SMS)</option><option value="email">Email</option><option value="call">Phone call</option></select></div>
|
||||
<div class="fld"><label>Recipient (phone E.164 or email)</label><input id="msRecipient" placeholder="+14255550123 or name@example.com"></div>
|
||||
<div class="fld" id="msSubjectWrap" style="display:none"><label>Subject</label><input id="msSubject"></div>
|
||||
<div class="fld"><label>Message</label><textarea id="msBody" placeholder="What do you want to say?"></textarea></div>
|
||||
<button class="btn primary" style="width:100%" onclick="doMission()">Send</button>
|
||||
<div id="msStatus" style="margin-top:12px;font-size:13px"></div>
|
||||
</div></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');
|
||||
const token = localStorage.getItem('astraea_token') || '';
|
||||
let active = AGENTS[0].id;
|
||||
const history = {}; // per-agent turn history
|
||||
let avatars = [];
|
||||
let myProfile = {display_name:'', avatar:0, about_me:''};
|
||||
let mySettings = {};
|
||||
const history = {};
|
||||
AGENTS.forEach(a => history[a.id] = []);
|
||||
|
||||
function accent(id) { const a = AGENTS.find(x => x.id === id); return a ? a.accent : '#7dd3fc'; }
|
||||
// starfield
|
||||
const cvs = document.getElementById('stars'), ctx = cvs.getContext('2d');
|
||||
let stars = [];
|
||||
(function size(){ cvs.width=innerWidth; cvs.height=innerHeight; stars=Array.from({length:120},()=>({x:Math.random()*cvs.width,y:Math.random()*cvs.height,r:Math.random()*1.3+.3,t:Math.random()*6.28,s:Math.random()*.02+.005})); })();
|
||||
addEventListener('resize',()=>{cvs.width=innerWidth;cvs.height=innerHeight;});
|
||||
(function draw(){ ctx.clearRect(0,0,cvs.width,cvs.height); for(const s of stars){ s.t+=s.s; const a=.3+Math.abs(Math.sin(s.t))*.7; ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,6.28); ctx.fillStyle=`rgba(190,210,255,${a})`; ctx.fill(); } requestAnimationFrame(draw); })();
|
||||
|
||||
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 authHeaders(){ return token ? {'Authorization':'Bearer '+token} : {}; }
|
||||
function accent(id){ const a=AGENTS.find(x=>x.id===id); return a?a.accent:'#7dd3fc'; }
|
||||
function closeModal(id){ document.getElementById(id).classList.remove('open'); }
|
||||
|
||||
function renderGrid(){
|
||||
const g=document.getElementById('grid'); g.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); g.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 renderChips(){
|
||||
const c=document.getElementById('chips'); c.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); c.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';
|
||||
async 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;
|
||||
const cp=document.getElementById('chatPanel'); cp.style.setProperty('--c',a.accent); cp.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);
|
||||
const m=document.getElementById('messages'); m.innerHTML='';
|
||||
if(token && !(history[id]||[]).length){
|
||||
try{ const r=await fetch('/api/conversation/'+id,{headers:authHeaders()}); const d=await r.json(); history[id]=d.history||[]; }catch(e){}
|
||||
}
|
||||
messages.appendChild(m);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
(history[id]||[]).forEach(t=>addBubble(t.role,t.content,t.citations));
|
||||
if(scroll) cp.scrollIntoView({behavior:'smooth',block:'start'});
|
||||
}
|
||||
|
||||
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 clearConv(){
|
||||
if(token){ try{ await fetch('/api/conversation/'+active,{method:'DELETE',headers:authHeaders()}); }catch(e){} }
|
||||
history[active]=[]; document.getElementById('messages').innerHTML='';
|
||||
}
|
||||
|
||||
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();
|
||||
let rec=null;
|
||||
function toggleMic(){
|
||||
const SR=window.SpeechRecognition||window.webkitSpeechRecognition;
|
||||
const mb=document.getElementById('micBtn');
|
||||
if(!SR){ mb.title='Voice input needs Chrome/Edge'; return; }
|
||||
if(rec){ rec.stop(); rec=null; mb.classList.remove('on'); mb.style.borderColor=''; return; }
|
||||
rec=new SR(); rec.lang='en-US'; rec.interimResults=false;
|
||||
rec.onresult=e=>{ const t=e.results[0][0].transcript; const i=document.getElementById('input'); i.value=(i.value?i.value+' ':'')+t; };
|
||||
rec.onend=()=>{ rec=null; mb.classList.remove('on'); mb.style.borderColor=''; };
|
||||
rec.onerror=()=>{ rec=null; mb.classList.remove('on'); mb.style.borderColor=''; };
|
||||
rec.start(); mb.classList.add('on'); mb.style.borderColor='#f87171';
|
||||
}
|
||||
function addBubble(role,text,citations){
|
||||
const m=document.createElement('div'); m.className='msg '+role;
|
||||
const l=document.createElement('div'); l.className='who-label'; l.textContent=role==='user'?'You':(AGENTS.find(x=>x.id===active)?.name||'Attorney');
|
||||
const b=document.createElement('div'); b.className='bubble'; b.textContent=text;
|
||||
m.appendChild(l); m.appendChild(b);
|
||||
if(role==='assistant'){
|
||||
const sp=document.createElement('button'); sp.className='speak'; sp.textContent='🔊 Listen'; sp.onclick=()=>speak(text); m.appendChild(sp);
|
||||
if(citations&&citations.length){ const cw=document.createElement('div'); cw.className='citations'; const t=document.createElement('button'); t.className='cite-toggle'; t.textContent='🔗 '+citations.length+' source'+(citations.length>1?'s':''); const cl=document.createElement('div'); cl.className='cite-list'; citations.forEach(c=>{ const it=document.createElement('div'); it.className='cite'; it.innerHTML=`<b>${c.title}</b>${c.snippet}<div style="color:var(--gold);font-size:10.5px">${c.source}</div>`; cl.appendChild(it); }); t.onclick=()=>cl.classList.toggle('open'); cw.appendChild(t); cw.appendChild(cl); m.appendChild(cw); }
|
||||
}
|
||||
document.getElementById('messages').appendChild(m);
|
||||
document.getElementById('messages').scrollTop=document.getElementById('messages').scrollHeight;
|
||||
}
|
||||
function typing(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); document.getElementById('messages').appendChild(m); }
|
||||
|
||||
async function speak(text){
|
||||
try{ const r=await fetch('/api/speak',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:text.replace(/\n+/g,'. ').substring(0,2500),voice:mySettings.tts_voice||'aria'})}); const b=await r.blob(); new Audio(URL.createObjectURL(b)).play(); }catch(e){}
|
||||
}
|
||||
async function send(){
|
||||
const t=document.getElementById('input').value.trim(); if(!t)return;
|
||||
document.getElementById('input').value='';
|
||||
addBubble('user',t); history[active].push({role:'user',content:t});
|
||||
typing(true); document.getElementById('sendBtn').disabled=true;
|
||||
try{
|
||||
const r=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify({agent_id:active,message:t,history:history[active].slice(0,-1)})});
|
||||
const d=await r.json(); typing(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.',null);
|
||||
}catch(e){ typing(false); addBubble('assistant','Could not reach the engine.',null); }
|
||||
finally{ document.getElementById('sendBtn').disabled=false; }
|
||||
}
|
||||
|
||||
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';
|
||||
});
|
||||
async function loadAvatars(){ const r=await fetch('/api/avatars'); avatars=await r.json(); const g=document.getElementById('avatarGrid'); g.innerHTML=''; avatars.forEach(a=>{ const d=document.createElement('div'); d.className='avatar-opt'; d.style.background=`linear-gradient(135deg,${a.c1},${a.c2})`; d.textContent=a.glyph; d.title=a.label; d.onclick=()=>{ myProfile.avatar=a.id; renderAvatarSel(); }; g.appendChild(d); }); }
|
||||
function renderAvatarSel(){ document.querySelectorAll('.avatar-opt').forEach((d,i)=>d.classList.toggle('sel',i===myProfile.avatar)); }
|
||||
function renderMyAvatar(){ const a=avatars[myProfile.avatar]||avatars[0]; const el=document.getElementById('myAvatar'); if(a){ el.style.background=`linear-gradient(135deg,${a.c1},${a.c2})`; el.textContent=a.glyph; } }
|
||||
function openProfile(){ document.getElementById('pfName').value=myProfile.display_name||''; document.getElementById('pfAbout').value=myProfile.about_me||''; renderAvatarSel(); document.getElementById('profileModal').classList.add('open'); }
|
||||
async function saveProfile(){
|
||||
const body={display_name:document.getElementById('pfName').value, about_me:document.getElementById('pfAbout').value, avatar:myProfile.avatar};
|
||||
const r=await fetch('/api/profile',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify(body)});
|
||||
myProfile=await r.json(); renderMyAvatar(); closeModal('profileModal');
|
||||
}
|
||||
function openSettings(){
|
||||
const s=mySettings; document.getElementById('twSid').value=s.twilio_sid||''; document.getElementById('twToken').value=''; document.getElementById('twFrom').value=s.twilio_from||''; document.getElementById('smHost').value=s.smtp_host||''; document.getElementById('smPort').value=s.smtp_port||587; document.getElementById('smUser').value=s.smtp_user||''; document.getElementById('emFrom').value=s.email_from||''; document.getElementById('ttsVoice').value=s.tts_voice||'aria'; document.getElementById('ncOrder').checked=!!s.no_contact_order; document.getElementById('ncName').value=s.partner_name||''; document.getElementById('dvFactor').checked=!!s.domestic_violence; document.getElementById('poOrder').checked=!!s.protection_order; document.getElementById('safetyNotes').value=s.safety_notes||'';
|
||||
document.getElementById('settingsModal').classList.add('open');
|
||||
}
|
||||
async function saveSettings(){
|
||||
const body={ twilio_sid:document.getElementById('twSid').value, twilio_from:document.getElementById('twFrom').value, smtp_host:document.getElementById('smHost').value, smtp_port:parseInt(document.getElementById('smPort').value||587), smtp_user:document.getElementById('smUser').value, email_from:document.getElementById('emFrom').value, tts_voice:document.getElementById('ttsVoice').value, no_contact_order:document.getElementById('ncOrder').checked?1:0, partner_name:document.getElementById('ncName').value, domestic_violence:document.getElementById('dvFactor').checked?1:0, protection_order:document.getElementById('poOrder').checked?1:0, safety_notes:document.getElementById('safetyNotes').value };
|
||||
const tw=document.getElementById('twToken').value; if(tw) body.twilio_auth_token=tw;
|
||||
const sp=document.getElementById('smPass').value; if(sp) body.smtp_pass=sp;
|
||||
const r=await fetch('/api/settings',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify(body)});
|
||||
mySettings=await r.json(); closeModal('settingsModal');
|
||||
}
|
||||
async function openDocs(){ const r=await fetch('/api/documents/types'); const types=await r.json(); const s=document.getElementById('docType'); s.innerHTML=''; types.forEach(t=>{ const o=document.createElement('option'); o.value=t.id; o.textContent=t.name+' ('+t.form+')'; s.appendChild(o); }); document.getElementById('docResult').style.display='none'; document.getElementById('docsModal').classList.add('open'); }
|
||||
async function genDoc(){
|
||||
document.getElementById('docBtn').disabled=true;
|
||||
const body={doc_type:document.getElementById('docType').value, instructions:document.getElementById('docInst').value};
|
||||
const r=await fetch('/api/documents',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify(body)});
|
||||
const d=await r.json(); document.getElementById('docBtn').disabled=false;
|
||||
const el=document.getElementById('docResult'); el.style.display='block'; el.textContent=d.document||d.error;
|
||||
}
|
||||
function openMission(){ document.getElementById('missionModal').classList.add('open'); }
|
||||
document.getElementById('msType').onchange=function(){ document.getElementById('msSubjectWrap').style.display=this.value==='email'?'block':'none'; };
|
||||
async function doMission(){
|
||||
const type=document.getElementById('msType').value, recipient=document.getElementById('msRecipient').value;
|
||||
const body={type,recipient,message:document.getElementById('msBody').value,subject:document.getElementById('msSubject').value,body:document.getElementById('msBody').value};
|
||||
const r=await fetch('/api/mission',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify(body)});
|
||||
const d=await r.json(); document.getElementById('msStatus').textContent=d.ok?'✓ Sent.':('✗ '+(d.error||'Failed')); document.getElementById('msStatus').style.color=d.ok?'#4ade80':'#fca5a5';
|
||||
}
|
||||
|
||||
// ---- 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'; });
|
||||
document.getElementById('sendBtn').onclick=send;
|
||||
document.getElementById('input').addEventListener('keydown',e=>{ if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();} });
|
||||
|
||||
async function boot(){
|
||||
if(!token){ window.location.href='/'; return; }
|
||||
try{ const r=await fetch('/api/me',{headers:authHeaders()}); if(!r.ok){ window.location.href='/'; return; } const d=await r.json(); myProfile=d.profile; mySettings=d.settings; }catch(e){}
|
||||
await loadAvatars(); renderMyAvatar(); renderGrid(); renderChips(); selectAgent(active,false);
|
||||
// populate tts voices
|
||||
try{ const vr=await fetch('/api/voices'); const vd=await vr.json(); const s=document.getElementById('ttsVoice'); s.innerHTML=''; vd.voices.forEach(v=>{ const o=document.createElement('option'); o.value=v.id; o.textContent=v.label; s.appendChild(o); }); s.value=mySettings.tts_voice||'aria'; }catch(e){}
|
||||
}
|
||||
boot();
|
||||
</script>
|
||||
|
||||
<!-- Umami analytics beacon -->
|
||||
<script async src="https://analytics.thetempleofdoom.com/script.js" data-website-id="c8c2f69c-4448-40ab-9848-02179f10c001"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user