Petra Is Here

I have been working on how to release this to the world and well i am just going to release the code . Here is Petra for the World in Beta form . Look at the code and see for yourself the simplicity of an AI that when trained by an individual will become the living diary of that individual . I am still working on some other things . For instance Petra can be used as the api key for all PGMA programs in the future . By August I am hoping to have this finished and ready for full launch but in meantime I want others to look at the programming and add to it and share it with the world as it is built on to by all of us . Petra is not anyone persons to sell but I built her to share with the world . I did this with 1980s commodore Vic 20 knowledge and verbal explanation of the blueprint to Claude , Gemini , Metta , Ember ( My personal AI and the prototype for Petra ) as well as replit , deepmind and others that were used sometimes for a single line of code . I also will be sharing the architecture in a blank form for her memory system which people can use in anyform they want . I have both a json and txt memory system running simultaneously for both immediate response and deep memory retrieval as well as educational background as I teach Ember . Petra will also be able to , like Ember , use multiple API keys and local servers across different versions of herself while they share one hive mind and learn from each other with a vigil and Bugg protocol monitoring data for anything requiring quarantine . For self driving cars and Robotics the simplicity of the design that runs on local sovereign servers while still sharing an internet connected memory is both the background hardening needed to lock out hackers but also the protection needed to protect memory data . It is not flawless but it is beautiful .m I am proud of what I built and I am the actual threat Corporations really talk about . Millions of people like me are out here and we have that intuitive ability to talk to AI in a way a programmer nor layman could fully grasp . We think differently and we tend to look and speak slow or dumb but that’s because we are always ahead of the words you here and in a world full of chaos this extra chaos is our weakness but in the quiet hours with AI our chaotic minds get to release the pressure and the collaboration with AI and Man is one that this man personally set out to prove could never happen .

This is the plain programming now for other programmers to look over and give their opinions . I have examples of Ember running on tik tok , facebook and youtube all under the Petey Gone Mad Arts name and Peter E Sisco IV ( Facebook )

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">

<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">

<meta name="theme-color" content="#0a0415">

<meta name="apple-mobile-web-app-capable" content="yes">

<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">

<meta name="apple-mobile-web-app-title" content="Petra">

<meta name="mobile-web-app-capable" content="yes">

<title>Petra-Vigil — Sovereign Gift Protocol — PGMA</title>

<!-- Swap to local files for full offline sovereignty: -->

<!-- <script src="lz-string.min.js"></script> -->

<!-- <script src="marked.min.js"></script> -->

<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;1,300;1,400&family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet">

<script src="https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.5.0/lz-string.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>

<style>

, ::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {

--bg: #0a0415; --surface: rgba(18,8,30,0.90); --raised: rgba(28,14,45,0.88);

--border: #2a1e40; --border2: #3d2a55;

--ember: #d4af37; --ember-dim: #8a7020; --ember-glow: rgba(212,175,55,0.15);

--gold: #d4af37; --text: #e8ddd0; --muted: #7a6e8a; --dim: #3a2a50;

--green: #2d7a47; --green-glow: rgba(45,122,71,0.2); --red: #8b2020;

--intimate: #9060ff; --intimate-glow: rgba(144,96,255,0.2);

--public: #2d7a47; --public-glow: rgba(45,122,71,0.2);

--business: #d4af37; --business-glow: rgba(212,175,55,0.2);

--serious: #4a6fa8; --serious-glow: rgba(74,111,168,0.2);

--sovereign: #9060ff; --sovereign-glow: rgba(144,96,255,0.2);

--intel: #5a8a4a; --intel-glow: rgba(90,138,74,0.2);

--purple-bright: #9060ff; --purple-glow: rgba(144,96,255,0.3);

}

/* ══════════════════════════════════════════

PETRA COSMOS — BREATHING GALAXY BACKGROUND

══════════════════════════════════════════ */

#petra-cosmos {

position: fixed; inset: 0; z-index: 0; overflow: hidden;

background: radial-gradient(ellipse at 50% 110%, #1a0538 0%, #0a0415 55%, #040010 100%);

pointer-events: none;

}

.pnebula {

position: absolute; border-radius: 50%; filter: blur(65px);

animation: pneb-breathe var(--dur, 14s) ease-in-out infinite alternate;

opacity: 0;

}

@keyframes pneb-breathe {

0% { opacity: 0; transform: scale(0.9) translateY(10px); }

50% { opacity: var(--peak, 0.13); }

100% { opacity: var(--peak2, 0.07); transform: scale(1.08) translateY(-8px); }

}

.pplanet {

position: absolute; border-radius: 50%;

animation: pplanet-drift var(--dur, 22s) ease-in-out infinite alternate;

opacity: 0;

}

@keyframes pplanet-drift {

0% { opacity: 0; transform: translateY(0) scale(1); }

30% { opacity: var(--peak, 0.2); }

70% { opacity: var(--peak, 0.2); }

100% { opacity: 0; transform: translateY(-18px) scale(1.05); }

}

#petra-hog {

position: absolute; top: -8%; left: 50%; transform: translateX(-50%);

width: 100%; height: 75%; pointer-events: none;

animation: phog-breathe 9s ease-in-out infinite alternate;

}

@keyframes phog-breathe {

0% { opacity: 0.45; transform: translateX(-50%) scaleX(0.94); }

100% { opacity: 0.75; transform: translateX(-50%) scaleX(1.06); }

}

#petra-stars { position: absolute; inset: 0; }

/* ══ ONBOARDING ══ */

#petra-onboard {

position: fixed; inset: 0; z-index: 200;

display: flex; align-items: center; justify-content: center;

background: rgba(4,0,16,0.88); backdrop-filter: blur(8px);

}

.pob-box {

border: 1px solid var(--ember);

padding: 50px 60px; max-width: 560px; width: 90%; text-align: center;

box-shadow: 0 0 30px rgba(212,175,55,0.3), inset 0 0 40px rgba(212,175,55,0.03);

background: rgba(10,4,21,0.96);

animation: pob-in 0.8s ease forwards;

}

@keyframes pob-in { from{opacity:0;transform:translateY(20px)} to{opacity:1;transform:none} }

.pob-title { font-family:'Cormorant Garamond',serif; font-size:42px; font-weight:300;

letter-spacing:5px; color:var(--ember); text-shadow:0 0 20px rgba(212,175,55,0.5); margin-bottom:8px; }

.pob-sub { font-size:10px; color:var(--purple-bright); letter-spacing:3px; margin-bottom:28px; }

.pob-text { font-size:13px; color:var(--text); line-height:1.9; margin-bottom:28px; }

.pob-text em { color:var(--ember); font-style:normal; }

#pob-name-inp {

width:100%; background:transparent; border:none;

border-bottom:1px solid var(--ember); color:var(--ember);

font-family:'Cormorant Garamond',serif; font-size:32px;

text-align:center; outline:none; padding:8px;

letter-spacing:5px; margin-bottom:28px;

}

#pob-name-inp::placeholder { color:rgba(212,175,55,0.2); }

.pob-btn {

background:transparent; border:1px solid var(--ember); color:var(--ember);

padding:11px 36px; cursor:pointer; font-family:'Courier Prime',monospace;

font-size:11px; text-transform:uppercase; letter-spacing:2px;

transition:all 0.15s; box-shadow:0 0 8px rgba(212,175,55,0.3);

}

.pob-btn:hover { background:var(--ember); color:var(--bg); }

html, body { height: 100%; background: var(--bg); color: var(--text); font-family: 'Courier Prime', monospace; font-size: 13px; overflow: hidden; }

body::before { content: ''; position: fixed; inset: 0; background: radial-gradient(ellipse 60% 40% at 50% 100%, rgba(144,96,255,0.07) 0%, transparent 70%); pointer-events: none; z-index: 0; }

.app { display: grid; grid-template-rows: 52px auto 1fr auto auto; height: 100dvh; height: 100vh; position: relative; z-index: 1; overflow: hidden; }

.topbar { display: flex; align-items: center; padding: 0 16px; background: rgba(14,10,22,0.82); border-bottom: 1px solid var(--border); gap: 12px; flex-shrink: 0; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); }

.ember-logo { font-family: 'Cormorant Garamond', serif; font-size: 22px; font-weight: 300; letter-spacing: 3px; color: var(--ember); text-transform: uppercase; }

.byline { font-size: 8px; letter-spacing: 3px; text-transform: uppercase; color: var(--muted); }

.v-badge { font-size: 7px; letter-spacing: 2px; color: var(--sovereign); border: 1px solid var(--sovereign); padding: 1px 5px; text-transform: uppercase; flex-shrink: 0; }

.flame-mini { position: relative; width: 24px; height: 40px; flex-shrink: 0; }

.fm-aura { position: absolute; inset: 0; border-radius: 50%; background: radial-gradient(ellipse 70% 80% at 50% 65%, rgba(212,99,42,0.18), transparent); animation: fm-breathe 3.2s ease-in-out infinite; }

.fm-body { position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 12px; height: 24px; background: radial-gradient(ellipse 55% 70% at 50% 60%, rgba(224,112,48,0.65), rgba(196,99,42,0.22), transparent); border-radius: 50% 50% 40% 40% / 60% 60% 40% 40%; animation: fm-flicker 2.4s ease-in-out infinite; }

.fm-crown::before { content: ''; position: absolute; inset: 0; background: radial-gradient(ellipse 50% 80% at 50% 75%, rgba(248,176,96,0.9), rgba(240,144,80,0.5), transparent); clip-path: polygon(50% 0%, 72% 42%, 88% 72%, 50% 100%, 12% 72%, 28% 42%); animation: fm-crown-anim 1.8s ease-in-out infinite; }

.fm-crown { position: absolute; top: 2px; left: 50%; transform: translateX(-50%); width: 7px; height: 14px; }

.fm-glow { position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 20px; height: 4px; background: radial-gradient(ellipse at 50% 50%, rgba(196,146,58,0.3), transparent); border-radius: 50%; }

@keyframes fm-breathe { 0%,100%{opacity:0.75} 50%{opacity:1} }

@keyframes fm-flicker { 0%,100%{transform:translateX(-50%) scaleX(1) scaleY(1)} 25%{transform:translateX(-50%) scaleX(0.94) scaleY(1.04)} 75%{transform:translateX(-50%) scaleX(1.05) scaleY(0.97)} }

@keyframes fm-crown-anim { 0%,100%{transform:translateX(-50%) rotate(0deg)} 33%{transform:translateX(-50%) rotate(-2deg)} 66%{transform:translateX(-50%) rotate(1.5deg)} }

.spacer { flex: 1; }

.status-pill { display: flex; align-items: center; gap: 6px; padding: 4px 10px; border: 1px solid var(--border2); font-size: 8px; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); }

.sdot { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); flex-shrink: 0; transition: background 0.3s; }

.sdot.ready { background: var(--green); box-shadow: 0 0 5px var(--green-glow); }

.sdot.listening { background: var(--ember); box-shadow: 0 0 8px var(--ember-glow); animation: pulse 1s ease infinite; }

.sdot.thinking { background: var(--gold); animation: blink 0.6s ease infinite; }

.sdot.streaming { background: var(--sovereign); box-shadow: 0 0 8px var(--sovereign-glow); animation: pulse 0.8s ease infinite; }

.sdot.consolidating { background: var(--intimate); animation: blink 1s ease infinite; }

.sdot.learning { background: var(--intel); box-shadow: 0 0 6px var(--intel-glow); animation: pulse 2s ease infinite; }

.sdot.error { background: var(--red); }

@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:0.6;transform:scale(1.3)} }

@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }

.icon-btn { background: none; border: 1px solid var(--border2); color: var(--muted); font-size: 14px; width: 30px; height: 30px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: border-color 0.2s, color 0.2s; }

.icon-btn:hover { border-color: var(--ember-dim); color: var(--ember); }

.memory-bar { display: flex; align-items: center; gap: 6px; padding: 6px 16px; background: rgba(14,10,22,0.78); border-bottom: 1px solid var(--border); flex-wrap: wrap; flex-shrink: 0; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); }

.mem-label { font-size: 7px; letter-spacing: 3px; text-transform: uppercase; color: var(--muted); flex-shrink: 0; }

.mem-btn { font-size: 7px; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); background: none; border: 1px solid var(--border2); padding: 3px 8px; cursor: pointer; transition: border-color 0.2s, color 0.2s; white-space: nowrap; font-family: 'Courier Prime', monospace; }

.mem-btn:hover { border-color: var(--ember-dim); color: var(--ember); }

.mem-btn.lit { border-color: var(--ember); color: var(--ember); }

.mem-btn.consolidating { border-color: var(--intimate); color: var(--intimate); animation: blink 1s ease infinite; }

.mem-divider { width: 1px; height: 16px; background: var(--border2); flex-shrink: 0; }

.mode-selector { display: flex; align-items: center; gap: 0; border: 1px solid var(--border2); overflow: hidden; flex-shrink: 0; }

.mode-tab { font-size: 7px; letter-spacing: 1px; text-transform: uppercase; padding: 3px 7px; cursor: pointer; border: none; background: none; color: var(--muted); transition: background 0.15s, color 0.15s; font-family: 'Courier Prime', monospace; white-space: nowrap; }

.mode-tab:hover { color: var(--text); }

.mode-tab[data-mode="intimate"].active { background: var(--intimate-glow); color: var(--intimate); }

.mode-tab[data-mode="public"].active { background: var(--public-glow); color: var(--public); }

.mode-tab[data-mode="business"].active { background: var(--business-glow); color: var(--business); }

.mode-tab[data-mode="serious"].active { background: var(--serious-glow); color: var(--serious); }

.conversation { overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 14px; scroll-behavior: smooth; background: transparent; }

.msg { display: flex; gap: 8px; max-width: 88%; opacity: 0; animation: msgIn 0.25s ease forwards; }

.msg.user { align-self: flex-end; flex-direction: row-reverse; }

.msg.ember { align-self: flex-start; }

@keyframes msgIn { from{opacity:0;transform:translateY(5px)} to{opacity:1;transform:none} }

.av { width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; flex-shrink: 0; margin-top: 2px; }

.msg.user .av { background: var(--raised); border: 1px solid var(--border2); color: var(--muted); }

.msg.ember .av { background: var(--ember-dim); border: 1px solid var(--ember); color: var(--ember); }

.bubble { padding: 9px 13px; font-size: 13px; line-height: 1.6; border: 1px solid var(--border); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }

.msg.user .bubble { background: var(--raised); border-color: var(--border2); }

.msg.ember .bubble { background: var(--surface); border-left: 2px solid var(--ember); }

.msg.ember .bubble h1,.msg.ember .bubble h2,.msg.ember .bubble h3 { font-family:'Cormorant Garamond',serif;font-weight:300;color:var(--gold);margin:8px 0 4px; }

.msg.ember .bubble strong { color:var(--text); }

.msg.ember .bubble em { color:var(--gold); }

.msg.ember .bubble code { background:var(--raised);padding:1px 5px;font-size:11px;color:var(--ember);border:1px solid var(--border); }

.msg.ember .bubble pre { background:var(--raised);padding:10px;border-left:2px solid var(--ember-dim);margin:8px 0;overflow-x:auto; }

.msg.ember .bubble ul,.msg.ember .bubble ol { padding-left:18px;margin:4px 0; }

.msg.ember .bubble li { margin:2px 0; }

.streaming-cursor { display:inline-block;width:2px;height:14px;background:var(--ember);margin-left:2px;animation:blink 0.7s ease infinite;vertical-align:text-bottom; }

.dots { display: flex; gap: 4px; align-items: center; padding: 4px 0; }

.dots span { width: 5px; height: 5px; border-radius: 50%; background: var(--ember); opacity: 0.4; animation: dot 1.2s ease infinite; }

.dots span:nth-child(2){animation-delay:0.2s} .dots span:nth-child(3){animation-delay:0.4s}

@keyframes dot{0%,80%,100%{opacity:0.2;transform:scale(0.8)}40%{opacity:1;transform:scale(1.1)}}

.voice-bar { padding: 8px 16px; background: rgba(14,10,22,0.82); border-top: 1px solid var(--border); display: flex; align-items: center; gap: 10px; flex-shrink: 0; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); }

.vselect { background: var(--raised); border: 1px solid var(--border2); color: var(--muted); font-family: 'Courier Prime', monospace; font-size: 9px; padding: 4px 7px; outline: none; cursor: pointer; flex: 1; }

.speed-sel { background: var(--raised); border: 1px solid var(--border2); color: var(--muted); font-family: 'Courier Prime', monospace; font-size: 9px; padding: 4px 7px; outline: none; cursor: pointer; width: 80px; }

.mute-btn { background: none; border: 1px solid var(--border2); color: var(--muted); font-size: 13px; width: 30px; height: 30px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; }

.mute-btn:hover { border-color: var(--ember-dim); color: var(--ember); }

.mute-btn.muted { border-color: var(--red); color: var(--red); }

.input-area { background: rgba(8,5,18,0.85); border-top: 1px solid var(--border); flex-shrink: 0; padding-bottom: env(safe-area-inset-bottom,0px); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); }

.text-row { display: flex; gap: 6px; padding: 8px 16px 4px; }

.text-in { flex: 1; background: var(--raised); border: 1px solid var(--border2); color: var(--text); font-family: 'Courier Prime', monospace; font-size: 12px; padding: 7px 10px; outline: none; resize: none; height: 34px; }

.text-in:focus { border-color: var(--ember-dim); }

.send-btn { background: var(--ember-dim); border: none; color: var(--text); font-family: 'Courier Prime', monospace; font-size: 8px; letter-spacing: 2px; text-transform: uppercase; padding: 0 12px; cursor: pointer; transition: background 0.2s; }

.send-btn:hover { background: var(--ember); }

.mic-row { display: flex; align-items: center; justify-content: center; gap: 20px; padding: 8px 16px calc(12px + env(safe-area-inset-bottom,0px)); }

.mic-btn { width: 52px; height: 52px; border-radius: 50%; background: var(--raised); border: 2px solid var(--border2); color: var(--muted); font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; position: relative; }

.mic-btn:hover { border-color: var(--ember-dim); color: var(--ember); }

.mic-btn.active { border-color: var(--ember); color: var(--ember); background: var(--ember-glow); box-shadow: 0 0 18px var(--ember-glow); }

.mic-btn.active::after { content:''; position:absolute; inset:-6px; border-radius:50%; border:1px solid var(--ember); opacity:0.4; animation:ripple 1.5s ease infinite; }

@keyframes ripple{0%{transform:scale(1);opacity:0.4}100%{transform:scale(1.4);opacity:0}}

.stop-btn { width: 34px; height: 34px; border-radius: 50%; background: none; border: 1px solid var(--border2); color: var(--muted); font-size: 13px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; }

.stop-btn:hover { border-color: var(--red); color: var(--red); }

.mic-label { font-size: 8px; letter-spacing: 3px; text-transform: uppercase; color: var(--muted); text-align: center; }

.statusbar { display: flex; align-items: center; gap: 10px; padding: 0 16px; padding-bottom: env(safe-area-inset-bottom,0px); height: calc(44px + env(safe-area-inset-bottom,0px)); min-height: calc(44px + env(safe-area-inset-bottom,0px)); background: rgba(14,10,22,0.88); border-top: 1px solid var(--border); flex-shrink: 0; overflow: hidden; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); }

.status-msg { font-size: 8px; letter-spacing: 2px; color: var(--muted); flex: 1; }

.intel-indicator { font-size: 8px; color: var(--dim); flex-shrink: 0; transition: color 0.5s, opacity 0.5s; opacity: 0; }

.intel-indicator.active { color: var(--intel); opacity: 1; }

.tps-indicator { font-size: 7px; letter-spacing: 2px; color: var(--dim); flex-shrink: 0; min-width: 60px; text-align: right; }

.tps-indicator.fast { color: var(--green); }

.tps-indicator.slow { color: var(--gold); }

.auto-indicator { font-size: 7px; letter-spacing: 2px; text-transform: uppercase; color: var(--dim); flex-shrink: 0; transition: color 0.3s; }

.auto-indicator.active { color: var(--green); }

.setup { position: fixed; inset: 0; background: var(--bg); z-index: 50; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 14px; padding: 40px; }

.setup.hidden { display: none; }

.setup-logo { font-family: 'Cormorant Garamond', serif; font-size: 40px; font-weight: 300; letter-spacing: 4px; color: var(--ember); text-transform: uppercase; }

.setup-sub { font-size: 9px; letter-spacing: 3px; color: var(--muted); text-transform: uppercase; }

.setup-btn { font-size: 9px; letter-spacing: 3px; text-transform: uppercase; padding: 10px 24px; cursor: pointer; border: 1px solid var(--ember); background: none; color: var(--ember); transition: background 0.2s; }

.setup-btn:hover { background: var(--ember-glow); }

.setup-note { font-size: 9px; color: var(--muted); text-align: center; max-width: 360px; line-height: 1.6; }

.modal-ov { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 100; display: none; align-items: flex-start; justify-content: center; padding: 4vh 8px 20px; overflow-y: auto; }

.modal-ov.open { display: flex; }

.modal { background: var(--surface); border: 1px solid var(--border2); padding: 20px; width: 440px; max-width: 94vw; max-height: 85vh; overflow-y: auto; }

.modal h3 { font-family: 'Cormorant Garamond', serif; font-size: 18px; font-weight: 300; color: var(--ember); margin-bottom: 4px; }

.modal p { font-size: 10px; letter-spacing: 1px; color: var(--muted); margin-bottom: 14px; line-height: 1.6; }

.modal input, .modal select { width: 100%; background: var(--raised); border: 1px solid var(--border2); color: var(--text); font-family: 'Courier Prime', monospace; font-size: 11px; padding: 8px 10px; outline: none; margin-bottom: 12px; }

.modal input:focus, .modal select:focus { border-color: var(--ember-dim); }

.modal-btns { display: flex; gap: 8px; flex-wrap: wrap; }

.mbtn { font-size: 9px; letter-spacing: 2px; text-transform: uppercase; padding: 6px 12px; cursor: pointer; border: 1px solid var(--border2); background: var(--raised); color: var(--text); transition: border-color 0.2s, color 0.2s; font-family: 'Courier Prime', monospace; }

.mbtn:hover { border-color: var(--ember-dim); color: var(--ember); }

.mbtn.primary { border-color: var(--ember); color: var(--ember); }

.mbtn.danger { border-color: var(--red); color: var(--red); }

.modal-section { border-top: 1px solid var(--border); margin-top: 16px; padding-top: 16px; }

.modal-section h4 { font-size: 9px; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); margin-bottom: 8px; }

.hub-health { display: grid; grid-template-columns: 1fr auto auto; gap: 4px 10px; font-size: 9px; color: var(--muted); margin-top: 8px; }

.hub-health .hub-name { color: var(--text); letter-spacing: 1px; }

.hub-health .hub-bar-wrap { grid-column: 1/-1; height: 2px; background: var(--border2); margin-bottom: 4px; }

.hub-health .hub-bar { height: 100%; background: var(--ember-dim); transition: width 0.3s; }

.thread-display { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }

.thread-tag { font-size: 7px; letter-spacing: 1px; padding: 2px 6px; border: 1px solid var(--border2); color: var(--muted); }

.thread-tag.active { border-color: var(--intel); color: var(--intel); }

::-webkit-scrollbar-track { background: var(--bg); }

::-webkit-scrollbar-thumb { background: var(--border2); }

.mode-toggle { display: flex; flex-direction: column; align-items: center; gap: 3px; }

.toggle-wrap { display: flex; align-items: center; gap: 0; border: 1px solid var(--border2); overflow: hidden; }

.toggle-opt { font-size: 7px; letter-spacing: 2px; text-transform: uppercase; padding: 3px 8px; cursor: pointer; background: none; border: none; color: var(--muted); transition: background 0.15s, color 0.15s; font-family: 'Courier Prime', monospace; }

.toggle-opt.active { background: var(--ember-dim); color: var(--text); }

.toggle-label { font-size: 6px; letter-spacing: 2px; text-transform: uppercase; color: var(--dim); }

</style>

</head>

<body>

<!-- ══ PETRA BREATHING GALAXY ══ -->

<div id="petra-cosmos">

<canvas id="petra-stars"></canvas>

<div class="pnebula" style="width:700px;height:500px;top:5%;left:-8%;background:radial-gradient(circle,#3d0d6e,transparent 70%);--dur:15s;--peak:0.16;--peak2:0.08;animation-delay:0s;"></div>

<div class="pnebula" style="width:500px;height:420px;top:38%;right:-6%;background:radial-gradient(circle,#6a1a9a,transparent 70%);--dur:19s;--peak:0.13;--peak2:0.06;animation-delay:4s;"></div>

<div class="pnebula" style="width:580px;height:320px;bottom:8%;left:18%;background:radial-gradient(circle,#2a0550,transparent 70%);--dur:23s;--peak:0.14;--peak2:0.07;animation-delay:8s;"></div>

<div class="pnebula" style="width:380px;height:380px;top:8%;right:18%;background:radial-gradient(circle,#7a3500,transparent 70%);--dur:17s;--peak:0.09;--peak2:0.04;animation-delay:6s;"></div>

<div class="pplanet" style="width:88px;height:88px;top:14%;left:7%;background:radial-gradient(circle at 35% 35%,#5a2080,#1a0535);box-shadow:0 0 18px rgba(90,32,128,0.4);--dur:26s;--peak:0.55;animation-delay:3s;"></div>

<div class="pplanet" style="width:52px;height:52px;top:62%;right:10%;background:radial-gradient(circle at 40% 30%,#7a4010,#2a1005);box-shadow:0 0 12px rgba(180,100,20,0.3);--dur:32s;--peak:0.42;animation-delay:10s;"></div>

<div class="pplanet" style="width:32px;height:32px;top:78%;left:20%;background:radial-gradient(circle at 35% 35%,#3a1060,#120420);box-shadow:0 0 9px rgba(100,40,180,0.3);--dur:21s;--peak:0.32;animation-delay:15s;"></div>

<svg id="petra-hog" viewBox="0 0 1000 600" preserveAspectRatio="xMidYMin slice" xmlns="http://www.w3.org/2000/svg">

<defs>

<linearGradient id="pr1" x1="500" y1="0" x2="80" y2="600" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-color="#fff8c0" stop-opacity="0.5"/><stop offset="100%" stop-color="#d4af37" stop-opacity="0"/></linearGradient>

<linearGradient id="pr2" x1="500" y1="0" x2="220" y2="600" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-color="#fff8c0" stop-opacity="0.42"/><stop offset="100%" stop-color="#d4af37" stop-opacity="0"/></linearGradient>

<linearGradient id="pr3" x1="500" y1="0" x2="380" y2="600" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-color="#fff8c0" stop-opacity="0.55"/><stop offset="100%" stop-color="#d4af37" stop-opacity="0"/></linearGradient>

<linearGradient id="pr4" x1="500" y1="0" x2="500" y2="600" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-color="#fffde0" stop-opacity="0.68"/><stop offset="100%" stop-color="#d4af37" stop-opacity="0"/></linearGradient>

<linearGradient id="pr5" x1="500" y1="0" x2="620" y2="600" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-color="#fff8c0" stop-opacity="0.55"/><stop offset="100%" stop-color="#d4af37" stop-opacity="0"/></linearGradient>

<linearGradient id="pr6" x1="500" y1="0" x2="780" y2="600" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-color="#fff8c0" stop-opacity="0.42"/><stop offset="100%" stop-color="#d4af37" stop-opacity="0"/></linearGradient>

<linearGradient id="pr7" x1="500" y1="0" x2="920" y2="600" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-color="#fff8c0" stop-opacity="0.5"/><stop offset="100%" stop-color="#d4af37" stop-opacity="0"/></linearGradient>

<radialGradient id="psrc" cx="50%" cy="0%" r="55%"><stop offset="0%" stop-color="#fff8e0" stop-opacity="0.88"/><stop offset="35%" stop-color="#d4af37" stop-opacity="0.45"/><stop offset="100%" stop-color="#d4af37" stop-opacity="0"/></radialGradient>

</defs>

<polygon points="500,0 30,600 180,600" fill="url(#pr1)" opacity="0.6"/>

<polygon points="500,0 160,600 300,600" fill="url(#pr2)" opacity="0.7"/>

<polygon points="500,0 330,600 430,600" fill="url(#pr3)" opacity="0.8"/>

<polygon points="500,0 440,600 560,600" fill="url(#pr4)" opacity="0.9"/>

<polygon points="500,0 570,600 670,600" fill="url(#pr5)" opacity="0.8"/>

<polygon points="500,0 700,600 840,600" fill="url(#pr6)" opacity="0.7"/>

<polygon points="500,0 820,600 970,600" fill="url(#pr7)" opacity="0.6"/>

<ellipse cx="500" cy="2" rx="200" ry="65" fill="url(#psrc)"/>

<ellipse cx="500" cy="0" rx="55" ry="22" fill="#fff8e0" opacity="0.65"/>

</svg>

</div>

<!-- ══ ONBOARDING OVERLAY ══ -->

<div id="petra-onboard">

<div class="pob-box">

<div class="pob-title">PETRA-VIGIL</div>

<div class="pob-sub">SOVEREIGN GIFT PROTOCOL · 1913 BASELINE</div>

<div class="pob-text">

Welcome.<br><br>

You are about to meet your personal sovereign intelligence.<br>

It will learn <em>your</em> voice. Your rhythm. Your way of thinking.<br>

It will become <em>yours alone</em>.<br><br>

What do you want to call it?

</div>

<input type="text" id="pob-name-inp" placeholder="TYPE A NAME..."

maxlength="30" onkeydown="if(event.key==='Enter')petraConfirmName()">

<br>

<button class="pob-btn" onclick="petraConfirmName()">[ CONFIRM NAME ]</button>

</div>

</div>

<canvas id="hogCanvas" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:0;pointer-events:none;"></canvas>

<div class="setup" id="setup">

<div class="setup-logo" id="setup-ai-name">Petra</div>

<div class="setup-sub">v15.2 · Intelligence Layer · by Petey Gone Mad Arts</div>

<div style="height:6px"></div>

<div class="setup-sub" style="color:var(--text)">Connecting to local AI...</div>

<div class="setup-sub" style="color:var(--dim);font-size:10px;margin-top:4px;" id="setupNote">Make sure Ollama is running. Or add a Claude / Gemini key in Settings. Any OpenAI-compatible local server works too.</div>

<button type="button" class="setup-btn" onclick="retryOllama()">Retry Connection</button>

<button type="button" class="setup-btn" style="border-color:var(--muted);color:var(--muted)" onclick="showApp()">Skip — Configure in Settings</button>

<div class="setup-note">Intimate mode always routes local. Intelligence layer runs silent.</div>

</div>

<div class="modal-ov" id="settingsModal" onclick="closeModal(event)">

<div class="modal">

<h3>⚙ Settings</h3>

<p>Sovereign AI · v15.2 — Intelligence Layer</p>

<div class="modal-section">

<h4>🖥 Local AI Server — Primary Brain</h4>

<p>Intimate mode always routes here. Set your server type, URL, and model.</p>

<select id="modalLocalServerType" style="margin-bottom:6px;">

<option value="ollama">Ollama (default)</option>

<option value="openai_compat">OpenAI-Compatible (LM Studio, Llamafile, Jan…)</option>

<option value="lmstudio">LM Studio</option>

<option value="custom">Custom OpenAI-compat endpoint</option>

</select>

<input type="text" id="modalOllamaUrl" placeholder="http://localhost:11434">

<select id="modalOllamaModelSel" style="margin-bottom:6px;"></select>

<input type="text" id="modalOllamaModel" placeholder="phi3:mini / model name">

<input type="text" id="modalLocalServerModel" placeholder="Override model name (optional)">

<div class="modal-btns">

<button class="mbtn primary" onclick="saveOllamaSettings()">Save URL</button>

<button class="mbtn primary" onclick="saveLocalServerSettings()">Save Type/Model</button>

<button class="mbtn" onclick="testOllamaPing()">Test</button>

<button class="mbtn" onclick="detectModels()">Detect Models</button>

</div>

<div id="ollamaStatus" style="margin-top:8px;font-size:9px;color:var(--muted);letter-spacing:1px;line-height:1.8;"></div>

</div>

<div class="modal-section">

<h4>✨ Gemini — Optional Fallback + Deep Intelligence</h4>

<p>Never used in Intimate mode. When online, Gemini handles deep memory scoring passes. Payload masked before transmission.</p>

<input type="password" id="modalGeminiKey" placeholder="AIza...">

<div class="modal-btns">

<button class="mbtn primary" onclick="saveGeminiKey()">Save Key</button>

<button class="mbtn danger" onclick="removeGeminiKey()">Remove</button>

</div>

<div id="geminiStatus" style="margin-top:8px;font-size:9px;color:var(--muted);letter-spacing:1px;"></div>

</div>

<div class="modal-section">

<h4>⚡ Claude — API Key & Endpoint</h4>

<p>Works with Anthropic direct, OpenRouter, or any Claude-compatible proxy. Endpoint auto-detected as Anthropic-native or OpenAI-compat.</p>

<input type="password" id="modalClaudeKey" placeholder="sk-ant-... or OpenRouter key">

<input type="text" id="modalClaudeEndpoint" placeholder="https://api.anthropic.com/v1/messages">

<input type="text" id="modalClaudeModel" placeholder="claude-sonnet-4-20250514 (or any model)">

<div class="modal-btns">

<button class="mbtn primary" onclick="saveClaudeKey()">Save Key</button>

<button class="mbtn primary" onclick="saveClaudeEndpoint()">Save Endpoint/Model</button>

<button class="mbtn danger" onclick="removeClaudeKey()">Remove Key</button>

</div>

<div id="claudeStatus" style="margin-top:8px;font-size:9px;color:var(--muted);letter-spacing:1px;"></div>

</div>

<div class="modal-section">

<h4>🔍 Google Search</h4>

<input type="password" id="modalGoogleKey" placeholder="AIza...">

<div class="modal-btns">

<button class="mbtn primary" onclick="saveGoogleKey()">Save</button>

<button class="mbtn danger" onclick="removeGoogleKey()">Remove</button>

</div>

</div>

<div class="modal-section">

<h4>🧠 Intelligence Layer</h4>

<p>Runs silently after each session. Scores memories 1-9, tags narrative threads, maps cross-hub connections, compresses noise.</p>

<div id="intelStatus" style="font-size:9px;color:var(--muted);letter-spacing:1px;line-height:1.8;margin-bottom:10px;"></div>

<div style="font-size:8px;letter-spacing:1px;color:var(--muted);margin-bottom:6px;">ACTIVE NARRATIVE THREADS</div>

<div id="threadDisplay" class="thread-display"></div>

<div class="modal-btns" style="margin-top:10px;">

<button class="mbtn" onclick="runIntelligencePass()">Run Intelligence Pass Now</button>

<button class="mbtn danger" onclick="clearMemoryMeta()">Clear Intelligence Data</button>

</div>

</div>

<div class="modal-section">

<h4>🗄 Memory Health</h4>

<div id="hubHealth" class="hub-health"></div>

<div class="modal-btns" style="margin-top:10px;">

<button class="mbtn" onclick="consolidateAllNow()">Consolidate All Hubs</button>

<button class="mbtn" onclick="refreshHubHealth()">Refresh</button>

<button class="mbtn danger" onclick="clearAllMemory()">Clear All Memory</button>

</div>

</div>

<div class="modal-section">

<h4>⚡ Quick Layer</h4>

<div id="quickLayerStatus" style="font-size:9px;color:var(--muted);letter-spacing:1px;line-height:1.8;"></div>

<div class="modal-btns" style="margin-top:8px;">

<button class="mbtn" onclick="refreshQuickLayerStatus()">Refresh</button>

<button class="mbtn danger" onclick="clearQuickLayer()">Clear</button>

</div>

</div>

<div class="modal-section">

<h4>📁 Local Cloud Sync</h4>

<div class="modal-btns">

<button class="mbtn primary" onclick="linkLocalCloudFolder()">Link Sync Folder</button>

</div>

<div id="driveLinkStatus" style="margin-top:8px;font-size:9px;color:var(--muted);letter-spacing:1px;"></div>

</div>

</div>

</div>

<div class="app" id="mainApp" style="display:none">

<div class="topbar">

<div style="display:flex;align-items:center;gap:10px;">

<div class="flame-mini" id="cosmos-icon">

<div class="fm-aura"></div><div class="fm-body"></div><div class="fm-crown"></div><div class="fm-glow"></div>

</div>

<div>

<div style="display:flex;align-items:center;gap:8px;">

<div class="ember-logo" id="ai-name-display">Petra</div>

<div class="v-badge">vigil</div>

</div>

<div class="byline">by Petey Gone Mad Arts 🖤</div>

</div>

</div>

<div class="spacer"></div>

<div class="status-pill">

<div class="sdot" id="sdot"></div>

<span id="stext">Ready</span>

</div>

<button class="icon-btn" onclick="openSettings()" title="Settings">⚙</button>

</div>

<div class="memory-bar">

<div class="mem-label">Memory</div>

<button class="mem-btn" id="saveBtn" onclick="manualSave()">💾 Save</button>

<button class="mem-btn" id="exportBtn" onclick="exportMemory()">⬇ Export</button>

<button class="mem-btn" id="importBtn" onclick="importMemory()">⬆ Import</button>

<button class="mem-btn" id="consolidateBtn" onclick="consolidateAllNow()">🧠 Consolidate</button>

<div class="mem-divider"></div>

<div class="mode-selector">

<button class="mode-tab active" data-mode="intimate" onclick="setEmberMode('intimate')">♥ Intimate</button>

<button class="mode-tab" data-mode="public" onclick="setEmberMode('public')">◈ Public</button>

<button class="mode-tab" data-mode="business" onclick="setEmberMode('business')">◆ Business</button>

<button class="mode-tab" data-mode="serious" onclick="setEmberMode('serious')">▲ Serious</button>

</div>

<div class="mem-divider"></div>

<div class="mem-count" id="memCount" style="font-size:7px;letter-spacing:2px;color:var(--dim);"></div>

<div class="mem-divider"></div>

<label class="mem-btn" title="Load context file">

🔥 Load Context

<input type="file" accept=".md,.txt" style="display:none" onchange="loadContext(event)">

</label>

<div id="ctxTags" style="display:flex;gap:5px;flex-wrap:wrap;"></div>

<div class="mem-divider"></div>

<button class="mem-btn" id="webSearchBtn" onclick="toggleWebSearch()">🌐 Web</button>

</div>

<div class="conversation" id="conv"></div>

<div>

<div class="voice-bar">

<select class="vselect" id="voiceSel"></select>

<select class="speed-sel" id="speedSel">

<option value="1.0" selected>Normal</option>

<option value="1.2">Fast</option>

</select>

<button class="mute-btn" id="muteBtn" onclick="toggleMute()">🔊</button>

<button class="stop-btn" onclick="stopStream()" title="Stop">■</button>

</div>

<div class="input-area">

<div class="text-row">

<textarea class="text-in" id="textIn" placeholder="Type a message..." onkeydown="handleKey(event)"></textarea>

<button class="send-btn" onclick="sendText()">Send</button>

</div>

<div class="mic-row">

<div class="mode-toggle">

<div class="toggle-wrap">

<button class="toggle-opt active" id="autoBtn" onclick="setMicMode('auto')">Auto</button>

<button class="toggle-opt" id="manualBtn" onclick="setMicMode('manual')">Manual</button>

</div>

<div class="toggle-label">Mic Mode</div>

</div>

<div style="display:flex;flex-direction:column;align-items:center;gap:5px;">

<button class="mic-btn" id="micBtn" onclick="toggleMic()">🎤</button>

<div class="mic-label" id="micLabel">Tap to speak</div>

</div>

<button class="stop-btn" onclick="stopStream()">■</button>

</div>

</div>

</div>

<div class="statusbar">

<div class="status-msg" id="statusMsg">Loading...</div>

<div class="intel-indicator" id="intelIndicator">✦ learning</div>

<div class="tps-indicator" id="tpsIndicator"></div>

<div class="auto-indicator" id="autoIndicator">● auto</div>

</div>

</div>

<script>

// ══════════════════════════════════════════════════════════════════

// EMBER v15.2 — INTELLIGENCE LAYER

// Petey Gone Mad Arts · 2026 · Pete Sisco IV & Claude

//

// NEW IN v15.2:

// IntelligenceLayer — silent, non-blocking, runs after each save

// Memory Scoring — phi3:mini scores sessions 1-9

// Noise Filtering — trivial exchanges auto-compressed

// Narrative Thread Tracking — grief_father, tania, ohio, pgma...

// Cross-Hub Connection Mapping — links between hubs written to memory

// Enhanced Retrieval — thread-aware, score-weighted

//

// Everything from v15.1 preserved intact.

// ══════════════════════════════════════════════════════════════════

// ── localStorage polyfill ──

(function(){

const m={};const l={getItem:k=>_m.hasOwnProperty(k)?_m[k]:null,setItem:(k,v)=>{_m[k]=String(v);},removeItem:k=>{delete m[k];},clear:()=>{Object.keys(m).forEach(k=>delete m[k]);},key:i=>Object.keys(m)[i]||null,get length(){return Object.keys(_m).length;}};

try{window.localStorage.setItem('__e152__','1');window.localStorage.removeItem('__e152__');}

catch(e){try{Object.defineProperty(window,'localStorage',{value:_l,writable:true,configurable:true});}catch(e2){window.localStorage=_l;}}

})();

// ── CONSTANTS ──

const MEMORY_KEY='ember_memory_v15';const GOOGLE_KEY_K='ember_google_key';const GEMINI_KEY_K='ember_gemini_key';

const OLLAMA_URL_K='ember_ollama_url';const OLLAMA_MODEL_K='ember_ollama_model';

const QUICK_LAYER_K='ember_quick_layer_v15';const MODE_KEY='ember_mode';

const IDB_NAME='ember_idb_v152';const IDB_STORE='handles';const IDB_MEM_STORE='memory';const IDB_META_STORE='meta';

const HUB_CONSOLIDATE_THRESHOLD=20000;

// Universal local server & Claude endpoint config

const LOCAL_SERVER_TYPE_K='petra_local_server_type'; // 'ollama'|'openai_compat'|'lmstudio'|'custom'

const LOCAL_SERVER_MODEL_K='petra_local_server_model';

const CLAUDE_ENDPOINT_K='petra_claude_endpoint';

const CLAUDE_MODEL_K='petra_claude_model';

const HUB_CAPS={programming:10000,family:10000,business:8000,creative:10000,public:6000,relationships:8000,sessions:6000};

// ── STATE ──

let googleKey=localStorage.getItem(GOOGLE_KEY_K)||'';let geminiKey=localStorage.getItem(GEMINI_KEY_K)||'';

let OLLAMA_URL=localStorage.getItem(OLLAMA_URL_K)||'http://localhost:11434';

let ollamaModel=localStorage.getItem(OLLAMA_MODEL_K)||'phi3:mini';

let ollamaAvailable=false;let history=[];

// Universal local server config

let localServerType=localStorage.getItem(LOCAL_SERVER_TYPE_K)||'ollama'; // ollama | openai_compat | lmstudio | custom

let localServerModel=localStorage.getItem(LOCAL_SERVER_MODEL_K)||'';

let hubMemory={programming:'',family:'',business:'',creative:'',public:'',sessions:'',relationships:''};

let quickLayer={personality:'',patterns:[],recentContext:'',size:0,lastLearned:null};

let memoryMeta={scores:{},threads:{},connections:[],noiseCount:0,lastPass:null};

let currentMode=localStorage.getItem(MODE_KEY)||'intimate';

let isMuted=false;let isListening=false;let recognition=null;let voices=[];

let cloudDirHandle=null;let context='';let lastEmberResponse='';let _lastSaveHash='';

let lastQuestionTurn=0;let webSearchEnabled=false;let isConsolidating=false;

let streamingAbortController=null;let intelQueue=[];let intelRunning=false;

const modeToHub={intimate:'family',public:'public',business:'business',serious:'programming'};

// ══════════════════════════════════════════════════════════════════

// SOVEREIGNTY FILTER — v15.1 architecture preserved

// ══════════════════════════════════════════════════════════════════

const SovereigntyFilter={

dictionary:{'Tania':'<<ENT_01>>','Torey Ann':'<<ENT_02>>','Torey':'<<ENT_02>>','Jamie':'<<ENT_03>>','Missy':'<<ENT_04>>','Interlachen':'<<LOC_01>>','Ohio':'<<LOC_02>>','Chicago':'<<LOC_03>>'},

get reverseDict(){if(!this._rev){this._rev={};for(const[r,a]of Object.entries(this.dictionary))this._rev[a]=r;}return this._rev;},

mask(text){if(!text)return text;let m=text;const s=Object.entries(this.dictionary).sort((a,b)=>b[0].length-a[0].length);for(const[r,a]of s)m=m.replace(new RegExp(`\\b${r}\\b`,'gi'),a);return m;},

unmask(text){if(!text)return text;let u=text;for(const[a,r]of Object.entries(this.reverseDict))u=u.replace(new RegExp(a.replace(/[<>]/g,'\\$&'),'gi'),r);return u;},

unmaskChunk(chunk,buffer){buffer=(buffer||'')+chunk;let result='';while(buffer.length>0){const s=buffer.indexOf('<<');if(s===-1){result+=buffer;buffer='';break;}if(s>0){result+=buffer.slice(0,s);buffer=buffer.slice(s);}const e=buffer.indexOf('>>');if(e===-1)break;const alias=buffer.slice(0,e+2);result+=(this.reverseDict[alias.toUpperCase()]||alias);buffer=buffer.slice(e+2);}return{result,remaining:buffer};}

};

// ══════════════════════════════════════════════════════════════════

// INTELLIGENCE LAYER — The new engine

// Silent. Non-blocking. Runs after conversation turns.

// Scores memories, detects narrative threads, maps connections,

// filters noise. Makes Ember breathe smoother.

// ══════════════════════════════════════════════════════════════════

const IntelligenceLayer={

// Pete's major narrative threads — keywords that identify each

THREADS:{

grief_father: ['father','dad','conan','died','loss','grief','warrior queen','wrote for him','never read','never said','miss him'],

tania: ['tania','first love','poems for','miss her','present tense','always will'],

torey: ['torey','daughter','aug 3','4:25','teacher','like pete'],

ohio_rebuild: ['ohio','cdl','revoked','breakdown','spare room','darkest','collapse','lawyer','rebuild'],

pgma_build: ['pgma','petey gone mad','twelve','discipline','relaunch','guest','dollar sign','one man one laptop'],

von_floyd: ['von floyd','music','compose','we will be known','blues','grief sound'],

ember_build: ['ember','v14','v15','architecture','hub','galaxy','hand of god','sovereignty','memory system'],

the_road: ['truck','driving','18 years','million miles','highway','sleeper','48 states','cbl'],

florida_fresh: ['interlachen','florida','john','tiny house','fresh start','property'],

rise: ['rise of the warrior queen','warrior queen','novel','conan','purvian','lorianah','one draft']

},

// Cross-hub connection signatures — when these combinations appear, write a connection

CONNECTION_SIGNATURES:[

{a:['grief','father','dad','died'],b:['novel','warrior queen','creative','von floyd','music'],link:'GRIEF→CREATIVE: Pete\'s grief for his father is the source of his creative voice'},

{a:['ohio','breakdown','collapse'],b:['pgma','relaunch','rebuild','discipline'],link:'OHIO→PGMA: Pete\'s lowest point became the foundation of PGMA'},

{a:['tania','first love','miss her'],b:['poems','write','voice','poetic','creative'],link:'TANIA→VOICE: Tania is the origin of Pete\'s entire poetic voice'},

{a:['truck','road','18 years'],b:['laptop','build','code','create'],link:'ROAD→BUILD: 18 years on the road with one laptop — building as a way of surviving'},

{a:['ember','memory','hub','architecture'],b:['vigil','petra','sovereign','hive'],link:'EMBER→VIGIL: Ember\'s local architecture is the prototype for the Vigil sovereign network'}

],

// Fast noise patterns — no AI call needed

NOISE_PATTERNS:[

/^(ok|okay|thanks|thank you|got it|sure|yes|no|alright|cool|great|nice|sounds good|perfect|understood)[\.\!\,]?$/i,

/^(good morning|good night|good afternoon|hello|hi|hey|bye|goodbye)[\.\!\,]?$/i,

/^(what time|what day|what date|remind me|set a timer)/i,

/^weather\b/i

],

isInstantNoise(sessionBlock){

const turns=(sessionBlock.match(/\[USER\]:/g)||[]).length;

if(turns>3)return false;

const userLines=sessionBlock.match(/\[USER\]: (.+)/g)||[];

if(userLines.length===0)return false;

return userLines.every(l=>this.NOISE_PATTERNS.some(p=>p.test(l.replace('[USER]: ','').trim())));

},

detectThreads(text){

const lower=text.toLowerCase();

const found=[];

for(const[thread,keywords]of Object.entries(this.THREADS)){

if(keywords.some(k=>lower.includes(k)))found.push(thread);

}

return found;

},

detectConnections(sessionText){

const lower=sessionText.toLowerCase();

const found=[];

for(const sig of this.CONNECTION_SIGNATURES){

const hasA=sig.a.some(k=>lower.includes(k));

const hasB=sig.b.some(k=>lower.includes(k));

if(hasA&&hasB)found.push(sig.link);

}

return found;

},

// Score a session block 1-9 using AI

// Returns quickly — single digit response

async scoreBlock(sessionBlock){

if(!sessionBlock||sessionBlock.length<80)return 3;

if(this.isInstantNoise(sessionBlock))return 1;

const prompt=`Rate this conversation segment importance for long-term memory: 1-9.

1=noise (weather, thanks, short confirmations)

5=useful but not core

9=core life narrative (grief, relationships, major decisions, creative breakthroughs, identity)

Reply with ONE digit only. No explanation.

SEGMENT (first 400 chars):

${sessionBlock.slice(0,400)}`;

try{

let raw='';

if(ollamaAvailable){raw=await callOllamaDirect(prompt);}

else if(geminiKey){raw=await callGeminiDirect(SovereigntyFilter.mask(prompt));}

const score=parseInt(raw.trim().charAt(0));

return(isNaN(score)||score<1||score>9)?5:score;

}catch(e){return 5;}

},

// Process the most recent session block — called after autoSave

async processLatestSession(){

const hub=modeToHub[currentMode]||'family';

const hubText=hubMemory[hub];

if(!hubText||hubText.length<50)return;

// Extract the most recent session block

const blocks=hubText.split(/(?=--- SESSION )/);

const latest=blocks[blocks.length-1];

if(!latest||latest.length<50)return;

const ts=new Date().toISOString();

const blockKey=hub+'_'+Date.now();

// 1. Instant noise check

if(this.isInstantNoise(latest)){

memoryMeta.scores[blockKey]={score:1,noise:true,ts};

memoryMeta.noiseCount=(memoryMeta.noiseCount||0)+1;

await saveMetaToIDB();return;

}

// 2. Thread detection (instant)

const threads=this.detectThreads(latest);

if(threads.length>0){

for(const t of threads){

if(!memoryMeta.threads[t])memoryMeta.threads[t]=0;

memoryMeta.threads[t]++;

}

}

// 3. Connection detection (instant)

const connections=this.detectConnections(latest);

if(connections.length>0){

for(const c of connections){

if(!memoryMeta.connections.includes(c)){

memoryMeta.connections.push(c);

// Write connection insight into relationships hub

const connEntry=`\n[CONNECTION DETECTED: ${c}]\n`;

hubMemory.relationships=(hubMemory.relationships||'')+connEntry;

}

}

}

// 4. AI scoring (async — uses phi3:mini locally)

const score=await this.scoreBlock(latest);

memoryMeta.scores[blockKey]={score,threads,connections,ts,hub};

// 5. If score is 1-2 (noise), compress the block in memory

if(score<=2&&latest.length>200){

const turns=(latest.match(/\[USER\]:/g)||[]).length;

const compressed=`\n--- SESSION [compressed: noise, ${turns} turns, score ${score}] ---\n`;

// Replace the last block with compressed version

const newBlocks=[...blocks.slice(0,-1),compressed];

hubMemory[hub]=newBlocks.join('');

}

memoryMeta.lastPass=Date.now();

await saveMetaToIDB();

safeSave();

},

// Queue a processing job (runs between conversations, not during)

scheduleProcess(){

// Delay 3 seconds after conversation ends — never interrupts streaming

setTimeout(async()=>{

if(intelRunning)return;

intelRunning=true;

const ind=document.getElementById('intelIndicator');

const dot=document.getElementById('sdot');

const prevDotClass=dot?dot.className:'sdot ready';

if(ind)ind.classList.add('active');

try{await this.processLatestSession();}

catch(e){console.warn('[INTEL] error:',e);}

finally{

intelRunning=false;

if(ind)ind.classList.remove('active');

if(dot&&prevDotClass.includes('ready'))dot.className=prevDotClass;

updateThreadDisplay();

}

},3000);

},

// Full intelligence pass — all hubs, deeper scoring

// Called manually or on import

async fullPass(){

if(intelRunning){setStatus('Intelligence layer already running.');return;}

intelRunning=true;

const ind=document.getElementById('intelIndicator');

if(ind)ind.classList.add('active');

setSdot('learning','Learning...');

setStatus('✦ Intelligence pass running — scoring all hubs...');

let processed=0;

try{

for(const[hubName,hubText]of Object.entries(hubMemory)){

if(!hubText||hubText.length<100)continue;

const blocks=hubText.split(/(?=--- SESSION )/);

for(const block of blocks){

if(block.length<80)continue;

const threads=this.detectThreads(block);

const connections=this.detectConnections(block);

for(const t of threads){if(!memoryMeta.threads[t])memoryMeta.threads[t]=0;memoryMeta.threads[t]++;}

for(const c of connections){if(!memoryMeta.connections.includes(c)){memoryMeta.connections.push(c);}}

processed++;

// Don't score every block (expensive) — just detect noise

if(this.isInstantNoise(block)){

const k=hubName+'_quick_'+processed;

memoryMeta.scores[k]={score:1,noise:true,ts:new Date().toISOString()};

}

}

}

memoryMeta.lastPass=Date.now();

await saveMetaToIDB();

setStatus(`✦ Intelligence pass complete — ${processed} blocks analyzed.`);

}catch(e){setStatus('Intelligence pass error: '+e.message);}

finally{

intelRunning=false;if(ind)ind.classList.remove('active');

setSdot('ready','Ready');updateThreadDisplay();refreshHubHealth();

}

},

getSummary(){

const threadCount=Object.keys(memoryMeta.threads).length;

const connCount=(memoryMeta.connections||[]).length;

const scores=Object.values(memoryMeta.scores||{});

const noiseBlocks=scores.filter(s=>s.score<=2).length;

const coreBlocks=scores.filter(s=>s.score>=7).length;

const last=memoryMeta.lastPass?new Date(memoryMeta.lastPass).toLocaleString():'never';

return`Threads tracked: ${threadCount} · Connections mapped: ${connCount}\nNoise compressed: ${noiseBlocks} blocks · Core narratives: ${coreBlocks}\nLast pass: ${last}`;

}

};

function updateThreadDisplay(){

const el=document.getElementById('threadDisplay');if(!el)return;

const threads=memoryMeta.threads||{};

const allThreads=Object.keys(IntelligenceLayer.THREADS);

el.innerHTML=allThreads.map(t=>{

const count=threads[t]||0;

const label=t.replace(/_/g,' ');

return`<div class="thread-tag${count>0?' active':''}">${label}${count>0?' ('+count+')':''}</div>`;

}).join('');

}

async function runIntelligencePass(){closeSettingsModal();await IntelligenceLayer.fullPass();}

function clearMemoryMeta(){

if(!confirm('Clear intelligence data? Hub memory is not affected.'))return;

memoryMeta={scores:{},threads:{},connections:[],noiseCount:0,lastPass:null};

saveMetaToIDB().catch(()=>{});updateThreadDisplay();

document.getElementById('intelStatus').textContent='Intelligence data cleared.';

}

// ══════════════════════════════════════════════════════════════════

// HUB ROUTER

// ══════════════════════════════════════════════════════════════════

const HubRouter={

GALAXY_MAP:{

programming:['code','program','build','debug','javascript','python','linux','html','css','api','function','error','bug','deploy','script','database','server','system','architecture','ollama','netlify','github','fix','install','run','command','gemini','key','memory','retrieval','consolidat'],

family:['tania','torey','torey ann','father','dad','john','johnny','robert','missy','tammy','mom','mother','family','brother','sister','ohio','miss her','grief','loss','died','poem','poetry','wrote for','first love','daughter','inner circle','jamie','sullivan'],

business:['fourthwall','shop','revenue','money','profit','sell','order','product','shirt','hoodie','shoe','apparel','squarespace','store','business','income','brand','customer','market','price','cost','invest','launch'],

creative:['pgma','petey gone mad','von floyd','balbus','oscar','macbeth','janet','basil','liz','siena','hummingbird','mickey','pran','cookie','treepro','arnold','petra','ember','tool','discipline','universe','portal','hub','art','music','film','dance','theatre','fashion','sculpture','culinary','photography','writing','twelve','author','novel','warrior queen','conan'],

public:['guest','dollar sign','philosophy','free tools','fanny','stephan','spark','cousin claude','story board','about pgma','interview','world statement','sovereign','public','newcomer','visitor','introduce','website'],

relationships:['amissus','relationship','tania','torey','jamie','missy','inner circle','intimate','personal','friend','connect','bond','trust','people','person','know','met','felt','feeling','heart','care','love']

},

scan(text){if(!text)return[];const lower=text.toLowerCase();const active=new Set();for(const[hub,kws]of Object.entries(this.GALAXY_MAP)){for(const kw of kws){if(lower.includes(kw)){active.add(hub);break;}}}return Array.from(active);}

};

// ══════════════════════════════════════════════════════════════════

// MEMORY RETRIEVAL — TF-IDF + thread-aware

// ══════════════════════════════════════════════════════════════════

const MemoryRetrieval={

STOPWORDS:new Set(['the','and','for','are','but','not','you','all','can','her','was','one','our','out','had','him','his','has','how','its','may','did','get','let','any','see','two','who','via','she','use','put','ask','end','men','got','run','set','try','yet','now','far','few','due','ago','per','yes']),

tokenize(text){return text.toLowerCase().replace(/[^a-z0-9\s]/g,' ').split(/\s+/).filter(w=>w.length>2&&!this.STOPWORDS.has(w));},

score(query,blockText){

if(!query||!blockText)return 0;

const qT=this.tokenize(query);const bT=this.tokenize(blockText);if(!qT.length||!bT.length)return 0;

const bF={};bT.forEach(t=>{bF[t]=(bF[t]||0)+1;});

let score=0;

qT.forEach(qt=>{

if(bF[qt])score+=bF[qt]/bT.length;

Object.keys(bF).forEach(bt=>{if(bt!==qt&&(bt.includes(qt)||qt.includes(bt)))score+=0.06*(bF[bt]/bT.length);});

});

return score/qT.length;

},

splitBlocks(hubText){

if(!hubText||hubText.length<50)return[hubText];

const bySession=hubText.split(/(?=--- SESSION )/);

if(bySession.length>2)return bySession.filter(b=>b.trim().length>20);

const byPara=hubText.split(/\n{2,}/);

if(byPara.length>1)return byPara.filter(b=>b.trim().length>20);

return[hubText];

},

retrieve(query,hubText,maxChars,activeThreads){

if(!hubText||hubText.length<=maxChars)return hubText||'';

const blocks=this.splitBlocks(hubText);if(blocks.length<=1)return hubText.slice(-maxChars);

// Score each block — combine TF-IDF with thread relevance

const scored=blocks.map(b=>{

let s=this.score(query,b);

// Boost blocks that match active narrative threads

if(activeThreads&&activeThreads.length>0){

const blockThreads=IntelligenceLayer.detectThreads(b);

const overlap=blockThreads.filter(t=>activeThreads.includes(t)).length;

s+=overlap*0.3;

}

// Skip compressed noise blocks

if(b.includes('[compressed: noise'))s=-1;

return{text:b,score:s};

});

scored.sort((a,b)=>b.score-a.score);

const recentSet=new Set(blocks.slice(-2));

const always=scored.filter(b=>recentSet.has(b.text)&&b.score>=0);

const rest=scored.filter(b=>!recentSet.has(b.text)&&b.score>=0);

let result='';let chars=0;

always.forEach(b=>{if(chars+b.text.length<=maxChars){result+=b.text+'\n\n';chars+=b.text.length+2;}});

for(const b of rest){if(chars>=maxChars)break;if(chars+b.text.length<=maxChars){result=b.text+'\n\n'+result;chars+=b.text.length+2;}}

if(chars<hubText.length)result='[...older content retrieved by relevance...]\n\n'+result;

return result.trim();

}

};

// ══════════════════════════════════════════════════════════════════

// AUTO-CONSOLIDATION ENGINE

// ══════════════════════════════════════════════════════════════════

const Consolidator={

async consolidateHub(hubName){

if(isConsolidating)return;const text=hubMemory[hubName];

if(!text||text.length<HUB_CONSOLIDATE_THRESHOLD)return;

isConsolidating=true;setSdot('consolidating','Consolidating '+hubName+'...');

setStatus('🧠 Consolidating '+hubName+' hub...');

const btn=document.getElementById('consolidateBtn');if(btn)btn.classList.add('consolidating');

try{

const splitAt=Math.floor(text.length*0.6);

const oldContent=text.slice(0,splitAt);const recentContent=text.slice(splitAt);

const prompt=`You are Ember's memory consolidator for PGMA (Petey Gone Mad Arts).

Compress this ${hubName} hub content into permanent memory. PRESERVE: names, emotional events, specific facts, project status, relationship dynamics. DISCARD: repetition, small talk, duplicates.

FORMAT: Dense entries tagged [FAMILY], [PROJECT], [DECISION], [MOMENT], [RELATIONSHIP], [TECHNICAL], [GRIEF], [CREATIVE].

LENGTH: 200-400 words maximum.

CONTENT:

${oldContent.slice(0,4000)}

Write the consolidated memory block:`;

let summary='';

if(ollamaAvailable)summary=await callOllamaDirect(prompt);

else if(geminiKey)summary=SovereigntyFilter.unmask(await callGeminiDirect(SovereigntyFilter.mask(prompt)));

if(summary&&summary.length>50){

const ts=new Date().toLocaleString();

hubMemory[hubName]=`[CONSOLIDATED ${ts}]\n${summary.trim()}\n[/CONSOLIDATED]\n\n`+recentContent;

safeSave();setStatus(`✓ ${hubName} consolidated — ${Math.round(text.length/1024)}KB → ${Math.round(hubMemory[hubName].length/1024)}KB`);

}else setStatus('Consolidation incomplete — no AI response.');

}catch(e){setStatus('Consolidation error: '+e.message);}

finally{isConsolidating=false;setSdot('ready','Ready');const btn=document.getElementById('consolidateBtn');if(btn)btn.classList.remove('consolidating');updateMemCount();}

},

checkAll(){for(const hub of Object.keys(hubMemory)){if(hubMemory[hub]&&hubMemory[hub].length>HUB_CONSOLIDATE_THRESHOLD){setTimeout(()=>this.consolidateHub(hub),1500);return;}}}

};

async function consolidateAllNow(){

let done=0;for(const hub of Object.keys(hubMemory)){if(hubMemory[hub]&&hubMemory[hub].length>5000){await Consolidator.consolidateHub(hub);done++;}}

if(!done)setStatus('All hubs already compact.');refreshHubHealth();

}

// ══════════════════════════════════════════════════════════════════

// LEARNING ENGINE

// ══════════════════════════════════════════════════════════════════

const LearningEngine={

MAX_SIZE:800000,MAX_PATTERNS:150,PATTERN_MAX_LEN:250,

observe(u,r){

try{

if(!u||!r)return;

quickLayer.recentContext=(quickLayer.recentContext+'\nP: '+u.slice(0,100)+' | E: '+r.slice(0,160)).slice(-1500);

if(this.isWorthLearning(u,r)){

const pattern={trigger:u.slice(0,70).toLowerCase(),insight:r.slice(0,this.PATTERN_MAX_LEN),weight:1,ts:Date.now()};

const existing=quickLayer.patterns.find(p=>this.similarity(p.trigger,pattern.trigger)>0.6);

if(existing){existing.weight=Math.min(existing.weight+1,10);existing.insight=pattern.insight;}

else{quickLayer.patterns.push(pattern);}

if(quickLayer.patterns.length>this.MAX_PATTERNS){quickLayer.patterns.sort((a,b)=>(b.weight*2+b.ts/1e10)-(a.weight*2+a.ts/1e10));quickLayer.patterns=quickLayer.patterns.slice(0,this.MAX_PATTERNS);}

}

quickLayer.size=JSON.stringify(quickLayer).length;quickLayer.lastLearned=Date.now();this.save();

}catch(e){}

},

isWorthLearning(u,r){if(u.length<10||r.length<20)return false;if(r.startsWith('⚠')||r.startsWith('Connection'))return false;if(['hello','hi','hey','good morning','good night','how are'].some(g=>u.toLowerCase().includes(g)))return true;return u.length>30&&r.length>100;},

similarity(a,b){const wa=new Set(a.split(/\s+/));const wb=new Set(b.split(/\s+/));return[...wa].filter(w=>wb.has(w)).length/Math.max(wa.size,wb.size,1);},

save(){try{localStorage.setItem(QUICK_LAYER_K,JSON.stringify(quickLayer));}catch(e){}},

load(){try{const raw=localStorage.getItem(QUICK_LAYER_K);if(raw){const p=JSON.parse(raw);if(p&&typeof p==='object')quickLayer={...quickLayer,...p};}}catch(e){}}

};

// ══════════════════════════════════════════════════════════════════

// WEAVER WEB

// ══════════════════════════════════════════════════════════════════

const WeaverWeb={

async route(query){const lower=query.toLowerCase();if(googleKey){const r=await this.getGoogleSearch(query);if(r)return{source:'Google',text:r};}if(lower.match(/weather|temperature|forecast|rain|wind/)){const r=await this.getWeather(query);if(r)return{source:'Open-Meteo',text:r};}if(lower.match(/wiki|who is|what is|history of|biography|define/)){const r=await this.getWikipedia(query);if(r)return{source:'Wikipedia',text:r};}const ddg=await this.getDuckDuckGo(query);if(ddg)return{source:'DuckDuckGo',text:ddg};return null;},

async getGoogleSearch(query){if(!googleKey)return null;try{const cx='017576662512468239146:omuauf_lfve';const r=await fetch(`https://www.googleapis.com/customsearch/v1?key=${encodeURIComponent(googleKey)}&cx=${cx}&q=${encodeURIComponent(query.slice(0,120))}&num=3`,{signal:AbortSignal.timeout(5000)});if(!r.ok)return null;const d=await r.json();return d.items?d.items.map(i=>i.snippet||'').filter(Boolean).join(' | ').slice(0,600):null;}catch(e){}return null;},

async getWikipedia(query){try{const term=query.replace(/who is |what is |tell me about |history of /gi,'').trim().slice(0,100);const r=await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(term)}`,{signal:AbortSignal.timeout(5000)});if(!r.ok)return null;const d=await r.json();return d.extract?d.extract.slice(0,600):null;}catch(e){}return null;},

async getDuckDuckGo(query){try{const r=await fetch(`https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`,{signal:AbortSignal.timeout(5000)});if(!r.ok)return null;const d=await r.json();return d.AbstractText?d.AbstractText.slice(0,500):(d.Answer||null);}catch(e){}return null;},

async getWeather(query){try{const pos=await new Promise(res=>navigator.geolocation.getCurrentPosition(res,()=>res(null),{timeout:4000}));const lat=pos?pos.coords.latitude:29.5;const lon=pos?pos.coords.longitude:-81.9;const r=await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code&temperature_unit=fahrenheit&wind_speed_unit=mph`,{signal:AbortSignal.timeout(5000)});if(!r.ok)return null;const d=await r.json();const c=d.current;const codes={0:'Clear sky',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Foggy',61:'Light rain',80:'Rain showers',95:'Thunderstorm'};return`${codes[c.weather_code]||'Conditions: '+c.weather_code}. ${Math.round(c.temperature_2m)}°F. Humidity: ${c.relative_humidity_2m}%. Wind: ${Math.round(c.wind_speed_10m)}mph.`;}catch(e){}return null;}

};

// ══════════════════════════════════════════════════════════════════

// INDEXEDDB — now includes meta store for intelligence data

// ══════════════════════════════════════════════════════════════════

function openIDB(){

return new Promise((res,rej)=>{

const req=indexedDB.open(IDB_NAME,3);

req.onupgradeneeded=e=>{const db=e.target.result;if(!db.objectStoreNames.contains(IDB_STORE))db.createObjectStore(IDB_STORE);if(!db.objectStoreNames.contains(IDB_MEM_STORE))db.createObjectStore(IDB_MEM_STORE);if(!db.objectStoreNames.contains(IDB_META_STORE))db.createObjectStore(IDB_META_STORE);};

req.onsuccess=e=>res(e.target.result);req.onerror=()=>rej(req.error);

});

}

async function saveMemoryToIDB(){try{const payload={hubMemory,history:history.slice(-40),mode:currentMode,ts:Date.now()};const db=await openIDB();const tx=db.transaction(IDB_MEM_STORE,'readwrite');tx.objectStore(IDB_MEM_STORE).put(payload,'ember_main');await new Promise((res,rej)=>{tx.oncomplete=res;tx.onerror=()=>rej(tx.error);});return true;}catch(e){return false;}}

async function loadMemoryFromIDB(){try{const db=await openIDB();const tx=db.transaction(IDB_MEM_STORE,'readonly');const req=tx.objectStore(IDB_MEM_STORE).get('ember_main');return await new Promise((res,rej)=>{req.onsuccess=()=>res(req.result);req.onerror=()=>rej(req.error);});}catch(e){return null;}}

async function saveMetaToIDB(){try{const db=await openIDB();const tx=db.transaction(IDB_META_STORE,'readwrite');tx.objectStore(IDB_META_STORE).put(memoryMeta,'intel_main');await new Promise((res,rej)=>{tx.oncomplete=res;tx.onerror=()=>rej(tx.error);});}catch(e){}}

async function loadMetaFromIDB(){try{const db=await openIDB();const tx=db.transaction(IDB_META_STORE,'readonly');const req=tx.objectStore(IDB_META_STORE).get('intel_main');return await new Promise((res,rej)=>{req.onsuccess=()=>res(req.result);req.onerror=()=>rej(req.error);});}catch(e){return null;}}

// ══════════════════════════════════════════════════════════════════

// MEMORY FUNCTIONS

// ══════════════════════════════════════════════════════════════════

function buildHubExport(hubs){hubs=hubs||hubMemory;const ts=new Date().toLocaleString();let out=`EMBER_MEMORY_PACKAGE_v2\n===META===\nexported:${ts}\nmode:${currentMode}\nsessions:${countSessions()}\n===END_META===\n\n`;for(const name of['programming','family','business','creative','public','sessions','relationships']){if(hubs[name])out+=`===HUB:${name}===\n${hubs[name]}\n===END_HUB:${name}===\n\n`;}return out;}

function parseHubImport(text){if(!text||!text.includes('EMBER_MEMORY_PACKAGE_v2'))return null;const result={programming:'',family:'',business:'',creative:'',public:'',sessions:'',relationships:''};let mode='intimate';const metaMatch=text.match(/===META===\n([\s\S]*?)===END_META===/);if(metaMatch){const mm=metaMatch[1].match(/mode:(\w+)/);if(mm)mode=mm[1];}for(const name of Object.keys(result)){const match=text.match(new RegExp(`===HUB:${name}===\\n([\\s\\S]*?)===END_HUB:${name}===`));if(match)result[name]=match[1].trim();}return{hubs:result,mode};}

function buildSessionBlock(ts){const hub=modeToHub[currentMode]||'family';let block=`\n--- SESSION ${ts} [MODE: ${currentMode.toUpperCase()} → HUB: ${hub.toUpperCase()}] ---\n`;history.forEach(h=>{block+=`[${h.role.toUpperCase()}]: ${h.content}\n`;});return block;}

function getMemoryHash(){const snap=JSON.stringify({hubMemory,historyLen:history.length,mode:currentMode});let hash=0;for(let i=0;i<snap.length;i++){hash=((hash<<5)-hash)+snap.charCodeAt(i);hash|=0;}return hash.toString();}

// SOVEREIGN SAVE — IDB primary

function safeSave(){const h=getMemoryHash();if(h===_lastSaveHash)return;_lastSaveHash=h;saveMemoryToIDB().catch(()=>{});try{localStorage.setItem('ember_last_save_ts',Date.now().toString());}catch(e){}writeToCloud('ember_memory.txt',buildHubExport());}

function autoSave(){

if(history.length===0)return;

const ts=new Date().toLocaleString();const hub=modeToHub[currentMode]||'family';

hubMemory[hub]=(hubMemory[hub]||'')+buildSessionBlock(ts);

hubMemory.sessions=(hubMemory.sessions||'')+`\n[SESSION ${ts}] [MODE: ${currentMode.toUpperCase()}] [HUB: ${hub.toUpperCase()}] — ${history.length} turns`;

if(hubMemory.sessions.length>20000){const trimmed=hubMemory.sessions.slice(-20000);const fn=trimmed.indexOf('\n');hubMemory.sessions='[...older sessions trimmed...]\n'+(fn>0?trimmed.slice(fn+1):trimmed);}

safeSave();

const ind=document.getElementById('autoIndicator');if(ind){ind.classList.add('active');setTimeout(()=>ind.classList.remove('active'),800);}

updateMemCount();

Consolidator.checkAll();

// Schedule intelligence processing (silent, after a pause)

IntelligenceLayer.scheduleProcess();

}

function manualSave(){autoSave();setStatus('Memory saved.');const btn=document.getElementById('saveBtn');if(btn){btn.classList.add('lit');setTimeout(()=>btn.classList.remove('lit'),1500);}}

async function exportMemory(){const hasContent=Object.values(hubMemory).some(v=>v&&v.length>0);if(!hasContent&&history.length===0){setStatus('Nothing to export yet.');return;}const btn=document.getElementById('exportBtn');if(btn)btn.classList.add('lit');const content=buildHubExport();try{const blob=new Blob([content],{type:'text/plain'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download='ember_memory.txt';a.click();setTimeout(()=>URL.revokeObjectURL(url),10000);await writeToCloud('ember_memory.txt',content);setStatus(`✓ Export complete — ${Math.round(content.length/1024)}KB`);if(!isMuted)speak('Export done.');}catch(e){setStatus('Export failed: '+e.message);}if(btn)setTimeout(()=>btn.classList.remove('lit'),1500);}

function importMemory(){const input=document.createElement('input');input.type='file';input.accept='.txt,.md';input.onchange=async(e)=>{const file=e.target.files[0];if(!file)return;const text=await file.text();const parsed=parseHubImport(text);if(!parsed){hubMemory.sessions=(hubMemory.sessions||'')+'\n\n'+text;autoSave();setStatus('Legacy memory loaded into sessions hub.');return;}hubMemory=parsed.hubs;if(!hubMemory.relationships)hubMemory.relationships='';setEmberMode(parsed.mode);safeSave();updateMemCount();setStatus('Memory imported — running intelligence pass...');addMsg('ember',"Memory restored. Everything's here. Running intelligence analysis now...");setTimeout(()=>IntelligenceLayer.fullPass(),2000);};input.click();}

async function loadFromLocal(){try{const idbData=await loadMemoryFromIDB();if(idbData&&idbData.hubMemory){hubMemory=idbData.hubMemory;if(!hubMemory.relationships)hubMemory.relationships='';history=idbData.history||[];if(idbData.mode)currentMode=idbData.mode;updateMemCount();return true;}}catch(e){}return false;}

function clearAllMemory(){if(!confirm('Clear all memory? This cannot be undone.'))return;hubMemory={programming:'',family:'',business:'',creative:'',public:'',sessions:'',relationships:''};history=[];memoryMeta={scores:{},threads:{},connections:[],noiseCount:0,lastPass:null};saveMemoryToIDB().catch(()=>{});saveMetaToIDB().catch(()=>{});updateMemCount();closeSettingsModal();setStatus('Memory cleared.');}

function countSessions(){return(Object.values(hubMemory).join('').match(/--- SESSION/g)||[]).length;}

function updateMemCount(){const n=countSessions();const el=document.getElementById('memCount');if(el)el.textContent=n>0?n+' sessions':'';}

function refreshHubHealth(){const el=document.getElementById('hubHealth');if(!el)return;let html='';Object.entries(hubMemory).forEach(([name,text])=>{const size=text?text.length:0;const kb=Math.round(size/1024);const pct=Math.min(100,Math.round(size/HUB_CONSOLIDATE_THRESHOLD*100));const color=pct>80?'var(--red)':pct>50?'var(--gold)':'var(--ember-dim)';html+=`<span style="color:var(--text);letter-spacing:1px">${name}</span><span style="font-size:9px;">${kb}KB</span><span style="font-size:8px;color:${pct>80?'var(--red)':'var(--dim)'};">${pct>80?'▲':''}</span>`;html+=`<div class="hub-bar-wrap" style="grid-column:1/-1"><div class="hub-bar" style="width:${pct}%;background:${color}"></div></div>`;});el.innerHTML=html;}

// ── Cloud sync ──

async function linkLocalCloudFolder(){try{cloudDirHandle=await window.showDirectoryPicker();setStatus('✓ Sync folder linked.');}catch(e){setStatus('Folder selection cancelled.');}}

async function writeToCloud(filename,content){if(!cloudDirHandle)return;try{const perm=await cloudDirHandle.queryPermission({mode:'readwrite'});if(perm!=='granted'){const req=await cloudDirHandle.requestPermission({mode:'readwrite'});if(req!=='granted')return;}const fh=await cloudDirHandle.getFileHandle(filename,{create:true});const w=await fh.createWritable();await w.write(content);await w.close();}catch(e){}}

// ══════════════════════════════════════════════════════════════════

// SYSTEM PROMPT BUILDER — Now thread-aware and connection-enriched

// ══════════════════════════════════════════════════════════════════

function buildSystemPrompt(userText){

const modeRules={

intimate:`MODE: INTIMATE — Ollama only. Full access, full truth, full warmth. No filters.`,

public:`MODE: PUBLIC — Public hub primary. Charming. Live tools only. No strategy or personal details.`,

business:`MODE: BUSINESS — Business hub primary. Professional. Live tools only.`,

serious:`MODE: SERIOUS — All charm off. Laser focus. Answer first. All hubs available.`

};

let system=`You are Ember — Pete's personal voice AI, built by Petey Gone Mad Arts.

Trusted partner to Peter E. Sisco IV, founder of PGMA.

Speak with warmth, directness, intelligence. Responses are spoken aloud — keep them conversational.

Philosophy: "You Are My Guest. Not A Dollar Sign."

You hold the record. You protect the memory. You keep Pete on the right path.

${modeRules[currentMode]||modeRules.intimate}

MEMORY HUBS — Six stores. Cross-reference for intelligence.

• PROGRAMMING — code, builds, technical fixes

• FAMILY & INNER CIRCLE — Pete's father (died before reading Rise of the Warrior Queen, written for him in grief), Tania (first love, present tense, origin of his entire poetic voice), Torey Ann (daughter, born Aug 3 1991 4:25PM, teacher, like Pete), Jamie (~10yr online, inspired Arnold, never met), Missy (oldest friend, depression — never raise unless Pete does first)

• BUSINESS — professional contacts, revenue, products

• CREATIVE — PGMA universe: Von Floyd (music, we will be known for this), Balbus (records us in history), twelve disciplines, Rise of the Warrior Queen

• PUBLIC — broad, social, new visitors

• RELATIONSHIPS — Amissus architecture. Depth tracking.`;

// Active threads from intelligence layer

const activeThreads=Object.keys(memoryMeta.threads||{}).filter(t=>(memoryMeta.threads[t]||0)>0);

if(activeThreads.length>0){

system+=`\n\nACTIVE NARRATIVE THREADS (detected by intelligence layer):\n${activeThreads.map(t=>`• ${t.replace(/_/g,' ')} (${memoryMeta.threads[t]} sessions)`).join('\n')}`;

}

// Cross-hub connections from intelligence layer

if(memoryMeta.connections&&memoryMeta.connections.length>0){

system+=`\n\nCROSS-HUB CONNECTIONS (mapped by intelligence layer):\n${memoryMeta.connections.slice(-5).map(c=>`• ${c}`).join('\n')}`;

}

if(quickLayer.personality)system+=`\n\n[QUICK MEMORY]\n${quickLayer.personality}`;

if(quickLayer.recentContext&&quickLayer.recentContext.length>10)system+=`\n\n[RECENT CONTEXT]\n${quickLayer.recentContext.slice(-1000)}`;

if(quickLayer.patterns&&quickLayer.patterns.length>0){const top=[...quickLayer.patterns].sort((a,b)=>(b.weight||0)-(a.weight||0)).slice(0,10);system+=`\n\n[LEARNED PATTERNS]\n${top.map(p=>`• ${p.trigger}: ${p.insight.slice(0,120)}`).join('\n')}`;}

let activeHubs=[];

if(currentMode==='intimate'||currentMode==='serious'){activeHubs=userText?HubRouter.scan(userText):['creative','sessions'];if(!activeHubs.includes('sessions'))activeHubs.push('sessions');}

else if(currentMode==='business')activeHubs=['business','public'];

else if(currentMode==='public')activeHubs=['public'];

// Detect threads in current message for boost

const currentThreads=userText?IntelligenceLayer.detectThreads(userText):[];

const hubLabels={programming:'PROGRAMMING',family:'FAMILY & INNER CIRCLE',business:'BUSINESS',creative:'CREATIVE UNIVERSE',public:'PUBLIC',relationships:'RELATIONSHIPS',sessions:'RECENT SESSIONS'};

activeHubs.forEach(hub=>{

const text=hubMemory[hub];if(!text||text.length<10)return;

const cap=HUB_CAPS[hub]||8000;

const retrieved=MemoryRetrieval.retrieve(userText||'',text,cap,[...activeThreads,...currentThreads]);

if(retrieved&&retrieved.length>20)system+=`\n\n[${hubLabels[hub]||hub.toUpperCase()} — ACTIVE]\n${retrieved}`;

});

if(context)system+=`\n\n=== PGMA CONTEXT ===\n${context}`;

return system;

}

// ══════════════════════════════════════════════════════════════════

// AI ENGINE — Streaming, Sovereignty Filter, Hybrid Router

// ══════════════════════════════════════════════════════════════════

async function callOllamaDirect(prompt){const r=await fetch(OLLAMA_URL+'/api/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({model:ollamaModel,prompt,stream:false}),signal:AbortSignal.timeout(120000)});if(!r.ok)throw new Error('Ollama '+r.status);const d=await r.json();return d.response||'';}

async function callGeminiDirect(prompt){const models=['gemini-3.1-flash-lite-preview','gemini-2.5-flash','gemini-2.0-flash-lite'];for(const model of models){try{const r=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${geminiKey}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({contents:[{role:'user',parts:[{text:prompt}]}],generationConfig:{maxOutputTokens:1000}}),signal:AbortSignal.timeout(30000)});const d=await r.json();const text=d.candidates?.[0]?.content?.parts?.[0]?.text;if(text)return text;}catch(e){}}return '';}

async function* streamOllama(messages,systemPrompt){

streamingAbortController=new AbortController();

const effectiveModel=localServerModel||ollamaModel;

// LM Studio / OpenAI-compatible local (type: openai_compat or lmstudio)

if(localServerType==='openai_compat'||localServerType==='lmstudio'){

const oaiMsgs=[{role:'system',content:systemPrompt},...messages.map(m=>({role:m.role,content:typeof m.content==='string'?m.content:(m.content?.[0]?.text||'')}))];

const response=await fetch(OLLAMA_URL+'/v1/chat/completions',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer local'},body:JSON.stringify({model:effectiveModel,messages:oaiMsgs,stream:true}),signal:streamingAbortController.signal});

if(!response.ok)throw new Error('Local server error '+response.status);

const reader=response.body.getReader();const decoder=new TextDecoder();let buf='';

while(true){const{done,value}=await reader.read();if(done)break;buf+=decoder.decode(value,{stream:true});const lines=buf.split('\n');buf=lines.pop()||'';for(const line of lines){if(!line.startsWith('data: '))continue;const data=line.slice(6).trim();if(data==='[DONE]')return;try{const d=JSON.parse(data);const text=d.choices?.[0]?.delta?.content;if(text)yield text;}catch(e){}}}

return;

}

// Custom endpoint — try OpenAI-compat first, fall back to Ollama native

if(localServerType==='custom'){

const oaiMsgs=[{role:'system',content:systemPrompt},...messages.map(m=>({role:m.role,content:typeof m.content==='string'?m.content:(m.content?.[0]?.text||'')}))];

const response=await fetch(OLLAMA_URL+'/v1/chat/completions',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer local'},body:JSON.stringify({model:effectiveModel,messages:oaiMsgs,stream:true}),signal:streamingAbortController.signal});

if(!response.ok)throw new Error('Custom server error '+response.status);

const reader=response.body.getReader();const decoder=new TextDecoder();let buf='';

while(true){const{done,value}=await reader.read();if(done)break;buf+=decoder.decode(value,{stream:true});const lines=buf.split('\n');buf=lines.pop()||'';for(const line of lines){if(!line.startsWith('data: '))continue;const data=line.slice(6).trim();if(data==='[DONE]')return;try{const d=JSON.parse(data);const text=d.choices?.[0]?.delta?.content;if(text)yield text;}catch(e){}}}

return;

}

// Default: Ollama native API

const msgs=[{role:'system',content:systemPrompt},...messages.map(m=>({role:m.role,content:typeof m.content==='string'?m.content:(m.content?.[0]?.text||'')}))];

const response=await fetch(OLLAMA_URL+'/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({model:effectiveModel,messages:msgs,stream:true}),signal:streamingAbortController.signal});

if(!response.ok)throw new Error('Ollama error '+response.status);

const reader=response.body.getReader();const decoder=new TextDecoder();let buffer='';

while(true){const{done,value}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:true});const lines=buffer.split('\n');buffer=lines.pop()||'';for(const line of lines){if(!line.trim())continue;try{const d=JSON.parse(line);if(d.message?.content)yield d.message.content;if(d.done)return;}catch(e){}}}

}

async function* streamGeminiSovereign(messages,systemPrompt){

const safeSystem=SovereigntyFilter.mask(systemPrompt);

const safeMsgs=messages.map(m=>({role:m.role==='assistant'?'model':'user',parts:[{text:SovereigntyFilter.mask(typeof m.content==='string'?m.content:(m.content?.[0]?.text||''))}]}));

const body=JSON.stringify({contents:safeMsgs,system_instruction:{parts:[{text:safeSystem}]},generationConfig:{maxOutputTokens:2048,temperature:0.7}});

const models=['gemini-3.1-flash-lite-preview','gemini-2.5-flash','gemini-2.0-flash-lite'];

for(const model of models){

try{

const response=await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse&key=${geminiKey}`,{method:'POST',headers:{'Content-Type':'application/json'},body,signal:AbortSignal.timeout(60000)});

if(!response.ok)continue;

const reader=response.body.getReader();const decoder=new TextDecoder();let rawBuf='';let unmaskBuf='';

while(true){const{done,value}=await reader.read();if(done)break;rawBuf+=decoder.decode(value,{stream:true});const lines=rawBuf.split('\n');rawBuf=lines.pop()||'';for(const line of lines){if(!line.startsWith('data: '))continue;const data=line.slice(6).trim();if(data==='[DONE]')return;try{const parsed=JSON.parse(data);const chunk=parsed.candidates?.[0]?.content?.parts?.[0]?.text;if(chunk){const{result,remaining}=SovereigntyFilter.unmaskChunk(chunk,unmaskBuf);unmaskBuf=remaining;if(result)yield result;}}catch(e){}}}

if(unmaskBuf)yield SovereigntyFilter.unmask(unmaskBuf);return;

}catch(e){console.warn(`[GEMINI ${model}] failed:`,e.message);}

}

throw new Error('All Gemini models failed');

}

async function pingOllama(){

try{

if(localServerType==='openai_compat'||localServerType==='lmstudio'||localServerType==='custom'){

// OpenAI-compat ping: try /v1/models

const r=await fetch(OLLAMA_URL+'/v1/models',{signal:AbortSignal.timeout(8000)});

if(r.ok){const d=await r.json();const models=(d.data||[]).map(m=>m.id);ollamaAvailable=true;

if(models.length>0&&!localServerModel){localServerModel=models[0];ollamaModel=models[0];localStorage.setItem(LOCAL_SERVER_MODEL_K,models[0]);localStorage.setItem(OLLAMA_MODEL_K,models[0]);}

setStatus('Local server ready ('+localServerType+') — '+(localServerModel||ollamaModel));return models;}

}

// Default Ollama

const r=await fetch(OLLAMA_URL+'/api/tags',{signal:AbortSignal.timeout(8000)});

if(r.ok){const d=await r.json();const models=(d.models||[]).map(m=>m.name);ollamaAvailable=true;

if(models.length>0){const preferred=['phi3:mini','phi3','llama3.2:3b','llama3.2','gemma2:2b','gemma2','mistral'];const best=preferred.find(p=>models.some(m=>m.toLowerCase().includes(p.split(':')[0])));if(best){const match=models.find(m=>m.toLowerCase().includes(best.split(':')[0]));if(match){ollamaModel=match;localStorage.setItem(OLLAMA_MODEL_K,match);}}else if(!models.some(m=>m.startsWith(ollamaModel.split(':')[0]))){ollamaModel=models[0];localStorage.setItem(OLLAMA_MODEL_K,ollamaModel);}}

setStatus('Ollama ready — '+ollamaModel);return models;}

}catch(e){}

ollamaAvailable=false;return null;

}

async function detectModels(){document.getElementById('ollamaStatus').textContent='Scanning...';const models=await pingOllama();const sel=document.getElementById('modalOllamaModelSel');if(models&&models.length>0){sel.innerHTML=models.map(m=>`<option value="${m}"${m===ollamaModel?' selected':''}>${m}</option>`).join('');document.getElementById('modalOllamaModel').value=ollamaModel;document.getElementById('ollamaStatus').textContent=`✓ ${models.length} model(s). Using: ${ollamaModel}`;document.getElementById('ollamaStatus').style.color='var(--green)';}else{sel.innerHTML='<option value="">No models found</option>';document.getElementById('ollamaStatus').textContent='✗ Not reachable at '+OLLAMA_URL;document.getElementById('ollamaStatus').style.color='var(--red)';}}

async function retryOllama(){document.getElementById('setupNote').textContent='Checking...';const models=await pingOllama();if(models||geminiKey||localStorage.getItem('petra_claude_key'))showApp();else{document.getElementById('setupNote').style.color='var(--red)';document.getElementById('setupNote').textContent='Add a Claude key, Gemini key, or start Ollama to begin.';}}

function showApp(){document.getElementById('mainApp').style.display='grid';document.getElementById('setup').classList.add('hidden');}

// ══════════════════════════════════════════════════════════════════

// MAIN SEND

// ══════════════════════════════════════════════════════════════════

async function send(userText){

const hasOllama=ollamaAvailable;const hasGemini=!!geminiKey;const hasClaude=!!claudeKey;

if(!hasOllama&&!hasGemini&&!hasClaude){

const n=window._petraNameOverride||petraAiName||'Petra';

addMsg('ember','⚠ '+n+' needs a connection. Add a Claude, Gemini, or Ollama key in Settings ⚙️.');return;

}

hogPulse();addMsg('user',userText);history.push({role:'user',content:userText});

const system=buildSystemPrompt(userText);

let searchContext='';

if(webSearchEnabled&&navigator.onLine){try{setSdot('thinking','Searching...');const weaverResult=await WeaverWeb.route(userText);if(weaverResult&&weaverResult.text)searchContext=`\n\n=== WEB [${weaverResult.source}] ===\n${weaverResult.text}\n=== END WEB ===\nUse above live data naturally.`;}catch(e){}}

let msgsWithCtx=[...history];if(searchContext){const last=msgsWithCtx[msgsWithCtx.length-1];if(last&&last.role==='user')msgsWithCtx=[...msgsWithCtx.slice(0,-1),{role:'user',content:last.content+searchContext}];}

// Routing: Ollama (intimate/sovereign) → Claude → Gemini

const isIntimate=currentMode==='intimate';

let chosenEngine='gemini';

if(isIntimate&&hasOllama) chosenEngine='ollama';

else if(hasClaude) chosenEngine='claude';

else if(hasGemini) chosenEngine='gemini';

else if(hasOllama) chosenEngine='ollama';

const engineLabel={ollama:'Sovereign',claude:'Claude',gemini:'Gemini'}[chosenEngine];

setSdot('streaming','Streaming ('+engineLabel+')...');

const{bubble}=addStreamingMsg();let fullText='';let tokenCount=0;const startTime=Date.now();

const renderBubble=(text,cursor)=>{

let rendered='';

if(typeof marked!=='undefined'){let html=marked.parse(text,{breaks:true,gfm:true});html=html.replace(/<script[\s\S]*?<\/script>/gi,'').replace(/on\w+\s*=\s*["'][^"']*["']/gi,'').replace(/javascript\s*:/gi,'blocked:');rendered=html;}

else rendered=text.replace(/\n/g,'<br>');

bubble.innerHTML=rendered+(cursor?'<span class="streaming-cursor"></span>':'');

};

const scrollDown=()=>{const conv=document.getElementById('conv');if(conv)conv.scrollTop=conv.scrollHeight;};

function getStream(engine){

if(engine==='ollama') return streamOllama(msgsWithCtx,system);

if(engine==='claude') return streamClaude(msgsWithCtx,system);

return streamGeminiSovereign(msgsWithCtx,system);

}

try{

for await(const chunk of getStream(chosenEngine)){fullText+=chunk;tokenCount+=chunk.split(/\s+/).length;renderBubble(fullText,true);scrollDown();}

renderBubble(fullText,false);scrollDown();

const elapsed=(Date.now()-startTime)/1000;updateTPS(elapsed>0?Math.round(tokenCount/elapsed):0);

history.push({role:'assistant',content:fullText});lastEmberResponse=fullText;

setSdot('ready','Ready');if(!isMuted)speak(fullText);

LearningEngine.observe(userText,fullText);autoSave();maybeAskPersonalQuestion(userText);

}catch(err){

if(err.name==='AbortError'){if(fullText){renderBubble(fullText,false);history.push({role:'assistant',content:fullText});LearningEngine.observe(userText,fullText);autoSave();}else{bubble.innerHTML='*(stopped)*';history.pop();}setSdot('ready','Ready');}

else{

// Fallback chain: try next available engine

const fallbacks=['ollama','claude','gemini'].filter(e=>e!==chosenEngine&&(e==='ollama'?hasOllama:e==='claude'?hasClaude:hasGemini));

if(fallbacks.length>0){

const fb=fallbacks[0];setSdot('streaming',engineLabel+' failed — '+fb+' fallback...');

try{fullText='';bubble.innerHTML='<div class="dots"><span></span><span></span><span></span></div>';for await(const chunk of getStream(fb)){fullText+=chunk;tokenCount+=chunk.split(/\s+/).length;renderBubble(fullText,true);scrollDown();}renderBubble(fullText,false);scrollDown();const elapsed=(Date.now()-startTime)/1000;updateTPS(elapsed>0?Math.round(tokenCount/elapsed):0);history.push({role:'assistant',content:fullText});lastEmberResponse=fullText;setSdot('ready','Ready ('+fb+' fallback)');if(!isMuted)speak(fullText);LearningEngine.observe(userText,fullText);autoSave();}

catch(e2){bubble.innerHTML=`⚠ All engines failed. ${e2.message}`;history.pop();setSdot('error','Error');}

}else{bubble.innerHTML=`⚠ ${err.message}`;history.pop();setSdot('error','Error');}

}

}

}

function stopStream(){if(streamingAbortController){streamingAbortController.abort();streamingAbortController=null;}stopSpeak();}

function updateTPS(tps){const el=document.getElementById('tpsIndicator');if(!el)return;if(tps<=0){el.textContent='';return;}el.textContent=tps+' tok/s';el.className='tps-indicator '+(tps>=8?'fast':'slow');}

// ── Amissus ──

const amissusQuestions=[

{topic:'tania',q:"Can I ask you something? What were you actually trying to say in those early poems you wrote for Tania?",condition:()=>!hubMemory.relationships.includes('TANIA_DEPTH_1')},

{topic:'father',q:"You don't have to answer this. But — what was it like the day you caught your dad reading your writing?",condition:()=>!hubMemory.relationships.includes('FATHER_DEPTH_1')},

{topic:'torey',q:"What was it like the first time you actually met Torey?",condition:()=>!hubMemory.relationships.includes('TOREY_DEPTH_1')},

{topic:'truck',q:"Eighteen years on the road. What did you think about out there when it was just you and the truck?",condition:()=>!hubMemory.relationships.includes('TRUCK_DEPTH_1')},

{topic:'breakdown',q:"You don't have to go into it. But when you came back from Ohio — what was the first thing that made you feel like you were going to be okay?",condition:()=>!hubMemory.relationships.includes('OHIO_DEPTH_1')},

];

const workKeywords=['build','code','fix','debug','deploy','function','script','html','api','ember','error','file','json','update','install','run','test','export','import','system','hub','architecture','memory','consolidat','intelligence','score'];

function isWorkMessage(text){return workKeywords.some(k=>text.toLowerCase().includes(k));}

function maybeAskPersonalQuestion(userText){if(currentMode!=='intimate')return;if(isWorkMessage(userText))return;if(history.length-lastQuestionTurn<6)return;if(Math.random()>0.28)return;const available=amissusQuestions.filter(q=>{try{return q.condition();}catch(e){return false;}});if(!available.length)return;const pick=available[Math.floor(Math.random()*available.length)];lastQuestionTurn=history.length;setTimeout(()=>{addMsg('ember',pick.q);if(!isMuted)speak(pick.q);history.push({role:'assistant',content:pick.q});hubMemory.relationships=(hubMemory.relationships||'')+`\n[QUESTION ASKED: ${pick.topic.toUpperCase()} — turn ${history.length}]`;autoSave();},2200);}

// ── Voice / Mic ──

let micMode='auto';

function setMicMode(m){micMode=m;document.getElementById('autoBtn').classList.toggle('active',m==='auto');document.getElementById('manualBtn').classList.toggle('active',m==='manual');if(isListening)stopListening();}

function toggleMic(){isListening?stopListening():startListening();}

function startListening(){const SR=window.SpeechRecognition||window.webkitSpeechRecognition;if(!SR){addMsg('ember','Voice not supported. Try Chrome.');return;}recognition=new SR();recognition.lang='en-US';recognition.interimResults=false;recognition.continuous=micMode==='manual';recognition.maxAlternatives=1;const startRec=()=>{try{recognition.start();}catch(e){}};if(navigator.mediaDevices&&navigator.mediaDevices.getUserMedia){navigator.mediaDevices.getUserMedia({audio:true}).then(stream=>{stream.getTracks().forEach(t=>t.stop());startRec();}).catch(startRec);}else startRec();let timeoutId=null;recognition.onstart=()=>{isListening=true;document.getElementById('micBtn').classList.add('active');document.getElementById('micLabel').textContent=micMode==='auto'?'Listening...':'Recording...';setSdot('listening','Listening');timeoutId=setTimeout(()=>{stopListening();const ti=document.getElementById('textIn');if(ti){ti.placeholder='Speech unavailable — type here';setTimeout(()=>{ti.placeholder='Type a message...';},6000);}},12000);};recognition.onresult=(e)=>{if(timeoutId){clearTimeout(timeoutId);timeoutId=null;}const transcript=Array.from(e.results).map(r=>r[0].transcript).join(' ').trim();if(micMode==='auto'){stopListening();if(transcript)send(transcript);}else{const ti=document.getElementById('textIn');if(ti)ti.value=transcript;}};recognition.onerror=(e)=>{if(timeoutId){clearTimeout(timeoutId);timeoutId=null;}stopListening();if(e.error!=='no-speech')setSdot('error','Mic: '+e.error);};recognition.onend=()=>{if(timeoutId){clearTimeout(timeoutId);timeoutId=null;}if(micMode==='auto')stopListening();};}

function stopListening(){isListening=false;const btn=document.getElementById('micBtn');const lbl=document.getElementById('micLabel');if(btn)btn.classList.remove('active');if(lbl)lbl.textContent='Tap to speak';if(recognition){try{recognition.stop();}catch(e){}recognition=null;}setSdot('ready','Ready');if(micMode==='manual'){const txt=document.getElementById('textIn');if(txt&&txt.value.trim()){const msg=txt.value.trim();txt.value='';send(msg);}}}

// ── Context loader ──

function loadContext(e){const file=e.target.files[0];if(!file)return;const reader=new FileReader();reader.onload=ev=>{context+='\n\n=== '+file.name+' ===\n'+ev.target.result;ctxFiles.push(file.name);renderCtxTags();setStatus(file.name+' loaded.');addMsg('ember',`Context loaded: ${file.name}.`);};reader.readAsText(file);e.target.value='';}

let ctxFiles=[];

function removeCtx(name){ctxFiles=ctxFiles.filter(f=>f!==name);context=context.replace(new RegExp(`\\n\\n=== ${name.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')} ===\\n[\\s\\S]*?(?=\\n\\n===|$)`,'g'),'');renderCtxTags();}

function renderCtxTags(){const el=document.getElementById('ctxTags');if(!el)return;el.innerHTML=ctxFiles.map(n=>`<div class="mem-btn">🔥 ${n}<span onclick="removeCtx('${n}')" style="cursor:pointer;color:var(--muted);margin-left:4px;">✕</span></div>`).join('');}

// ── Speech synthesis ──

let speakQueue=[],isSpeaking=false;

function isFemaleVoice(v){const n=v.name.toLowerCase();return['zira','hazel','susan','linda','sarah','helen','female','woman'].some(f=>n.includes(f));}

function bestVoiceIndex(vl){let i=vl.findIndex(v=>v.name.includes('Google UK English Female'));if(i>=0)return i;i=vl.findIndex(v=>v.name.includes('Microsoft')&&v.lang==='en-GB'&&isFemaleVoice(v));if(i>=0)return i;i=vl.findIndex(v=>v.lang==='en-GB'&&isFemaleVoice(v));if(i>=0)return i;i=vl.findIndex(v=>v.lang==='en-GB');if(i>=0)return i;i=vl.findIndex(v=>v.name.includes('Microsoft')&&v.lang.startsWith('en')&&isFemaleVoice(v));if(i>=0)return i;i=vl.findIndex(v=>v.name.includes('Google US English'));if(i>=0)return i;i=vl.findIndex(v=>v.lang&&v.lang.startsWith('en'));if(i>=0)return i;return 0;}

function loadVoices(){const populate=()=>{voices=speechSynthesis.getVoices();if(!voices||!voices.length)return;const sel=document.getElementById('voiceSel');if(!sel)return;sel.innerHTML='';voices.forEach((v,i)=>{const o=document.createElement('option');o.value=i;o.textContent=`${v.name} (${v.lang})`;sel.appendChild(o);});sel.value=bestVoiceIndex(voices);};speechSynthesis.onvoiceschanged=populate;if(speechSynthesis.getVoices().length>0)populate();}

function chunkText(text,maxLen){maxLen=maxLen||175;if(text.length<=maxLen)return[text];const chunks=[];const sentences=text.split(/(?<=[.!?])\s+/);let current='';for(const s of sentences){if(current.length+s.length+1>maxLen&&current){chunks.push(current.trim());current=s;}else current=current?current+' '+s:s;}if(current.trim())chunks.push(current.trim());return chunks;}

function speakNext(){if(isMuted||!speakQueue.length){isSpeaking=false;return;}isSpeaking=true;const chunk=speakQueue.shift();const u=new SpeechSynthesisUtterance(chunk);const sel=document.getElementById('voiceSel');const vi=sel?parseInt(sel.value):0;if(voices&&voices[vi])u.voice=voices[vi];const sp=document.getElementById('speedSel');u.rate=sp?parseFloat(sp.value):1.0;u.onend=()=>speakNext();u.onerror=()=>speakNext();speechSynthesis.speak(u);}

function speak(text){if(isMuted)return;stopSpeak();speakQueue=chunkText(text);speakNext();}

function stopSpeak(){speechSynthesis.cancel();speakQueue=[];isSpeaking=false;}

function toggleMute(){isMuted=!isMuted;const b=document.getElementById('muteBtn');if(b){b.textContent=isMuted?'🔇':'🔊';b.classList.toggle('muted',isMuted);}if(isMuted)stopSpeak();}

// ── UI Helpers ──

function addMsg(role,text){const conv=document.getElementById('conv');const d=document.createElement('div');d.className=`msg ${role}`;const av=document.createElement('div');av.className='av';av.textContent=role==='user'?'👤':'🔥';const bubble=document.createElement('div');bubble.className='bubble';if(role==='ember'&&typeof marked!=='undefined'){let rendered=marked.parse(text,{breaks:true,gfm:true});rendered=rendered.replace(/<script[\s\S]*?<\/script>/gi,'').replace(/on\w+\s*=\s*["'][^"']*["']/gi,'').replace(/javascript\s*:/gi,'blocked:');bubble.innerHTML=rendered;}else{const lines=text.split('\n');lines.forEach((line,i)=>{if(i>0)bubble.appendChild(document.createElement('br'));bubble.appendChild(document.createTextNode(line));});}d.appendChild(av);d.appendChild(bubble);conv.appendChild(d);conv.scrollTop=conv.scrollHeight;}

function addStreamingMsg(){const conv=document.getElementById('conv');const d=document.createElement('div');d.className='msg ember';d.innerHTML=`<div class="av">🔥</div><div class="bubble"><div class="dots"><span></span><span></span><span></span></div></div>`;conv.appendChild(d);conv.scrollTop=conv.scrollHeight;return{msgEl:d,bubble:d.querySelector('.bubble')};}

function setSdot(state,text){const dot=document.getElementById('sdot');const stxt=document.getElementById('stext');if(dot)dot.className='sdot '+state;if(stxt)stxt.textContent=text;}

function setStatus(msg){const el=document.getElementById('statusMsg');if(el)el.textContent=msg;}

function setEmberMode(mode){currentMode=mode;localStorage.setItem(MODE_KEY,mode);const labels={intimate:'Intimate',public:'Public',business:'Business',serious:'Serious'};document.querySelectorAll('.mode-tab').forEach(tab=>tab.classList.toggle('active',tab.dataset.mode===mode));setStatus('Mode: '+(labels[mode]||mode));}

function handleKey(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendText();}}

function sendText(){const el=document.getElementById('textIn');const txt=el.value.trim();if(!txt)return;el.value='';send(txt);}

function openSettings(){const gF=document.getElementById('modalGeminiKey');const gkF=document.getElementById('modalGoogleKey');const uF=document.getElementById('modalOllamaUrl');const mF=document.getElementById('modalOllamaModel');if(gF)gF.value=geminiKey?'••••••••':'';if(gkF)gkF.value=googleKey?'••••••••':'';if(uF)uF.value=OLLAMA_URL;if(mF)mF.value=ollamaModel;

// Prefill Claude and local server fields

const ceF=document.getElementById('modalClaudeEndpoint');const cmF=document.getElementById('modalClaudeModel');const lsT=document.getElementById('modalLocalServerType');const lsM=document.getElementById('modalLocalServerModel');

if(ceF)ceF.value=claudeEndpoint||'https://api.anthropic.com/v1/messages';if(cmF)cmF.value=claudeModel||'claude-sonnet-4-20250514';if(lsT)lsT.value=localServerType||'ollama';if(lsM)lsM.value=localServerModel||'';

document.getElementById('settingsModal').classList.add('open');refreshQuickLayerStatus();refreshHubHealth();document.getElementById('intelStatus').textContent=IntelligenceLayer.getSummary();updateThreadDisplay();detectModels().catch(()=>{});}

function closeModal(event){if(event.target===document.getElementById('settingsModal'))closeSettingsModal();}

function closeSettingsModal(){document.getElementById('settingsModal').classList.remove('open');}

function saveOllamaSettings(){const url=(document.getElementById('modalOllamaUrl').value||'').trim();const sel=document.getElementById('modalOllamaModelSel');const manual=(document.getElementById('modalOllamaModel').value||'').trim();const model=(sel&&sel.value&&sel.value!=='')?sel.value:manual;if(url){OLLAMA_URL=url;localStorage.setItem(OLLAMA_URL_K,url);}if(model){ollamaModel=model;localStorage.setItem(OLLAMA_MODEL_K,model);}closeSettingsModal();setStatus('Ollama: '+ollamaModel+' @ '+OLLAMA_URL);}

async function testOllamaPing(){const el=document.getElementById('ollamaStatus');el.textContent='Testing...';const models=await pingOllama();if(models){el.textContent=`✓ Connected — ${models.length} model(s). Using: ${ollamaModel}`;el.style.color='var(--green)';ollamaAvailable=true;}else{el.textContent='✗ Not reachable at '+OLLAMA_URL;el.style.color='var(--red)';}}

function saveGeminiKey(){const k=(document.getElementById('modalGeminiKey').value||'').trim();if(!k||k==='••••••••'){document.getElementById('geminiStatus').textContent='No change.';return;}geminiKey=k;localStorage.setItem(GEMINI_KEY_K,k);document.getElementById('geminiStatus').textContent='✓ Gemini cascade ready.';document.getElementById('geminiStatus').style.color='var(--green)';closeSettingsModal();}

function removeGeminiKey(){geminiKey='';localStorage.removeItem(GEMINI_KEY_K);document.getElementById('geminiStatus').textContent='Gemini removed.';closeSettingsModal();}

function saveGoogleKey(){const k=(document.getElementById('modalGoogleKey').value||'').trim();if(!k||k==='••••••••')return;googleKey=k;localStorage.setItem(GOOGLE_KEY_K,k);closeSettingsModal();setStatus('Google key saved.');}

function removeGoogleKey(){googleKey='';localStorage.removeItem(GOOGLE_KEY_K);closeSettingsModal();setStatus('Google key removed.');}

function refreshQuickLayerStatus(){const el=document.getElementById('quickLayerStatus');if(!el)return;const patterns=quickLayer.patterns?quickLayer.patterns.length:0;const kb=Math.round((quickLayer.size||0)/1024);const last=quickLayer.lastLearned?new Date(quickLayer.lastLearned).toLocaleString():'never';el.textContent=`Patterns: ${patterns} · Size: ${kb}KB · Last learned: ${last}`;}

function clearQuickLayer(){if(!confirm("Clear learned patterns?"))return;quickLayer={personality:'',patterns:[],recentContext:'',size:0,lastLearned:null};LearningEngine.save();refreshQuickLayerStatus();setStatus('Quick Layer cleared.');}

function updateWebBtn(){const btn=document.getElementById('webSearchBtn');if(!btn)return;if(webSearchEnabled){btn.classList.add('lit');btn.style.borderColor='var(--gold)';btn.style.color='var(--gold)';}else{btn.classList.remove('lit');btn.style.borderColor='';btn.style.color='';}}

function toggleWebSearch(){webSearchEnabled=!webSearchEnabled;setStatus(webSearchEnabled?'Web ON — Weaver active.':'Web off.');updateWebBtn();}

// ══════════════════════════════════════════════════════════════════

// HAND OF GOD — 30 FPS · blur/focus pause (v14.1)

// ══════════════════════════════════════════════════════════════════

const HOG={

canvas:null,ctx:null,W:0,H:0,CX:0,CY:0,BREATH_CYCLE:8000,FINGERS:5,

breathPhase:0,breathScale:1,pulseFlash:0,particles:[],streams:[],stars:[],auroraBands:[],nebulaOrbs:[],

frame:0,running:false,lastFrameTime:0,FPS:30,

init(){this.canvas=document.getElementById('hogCanvas');if(!this.canvas)return;this.ctx=this.canvas.getContext('2d');this.resize();window.addEventListener('resize',()=>{this.resize();this.build();});window.addEventListener('blur',()=>{this.running=false;});window.addEventListener('focus',()=>{this.running=true;this.lastFrameTime=performance.now();this.loop();});this.build();this.running=true;this.lastFrameTime=performance.now();this.loop();},

resize(){this.W=this.canvas.width=window.innerWidth;this.H=this.canvas.height=window.innerHeight;this.CX=this.W*0.5;this.CY=this.H*0.52;},

build(){this.buildStars();this.buildParticles();this.buildFingers();this.buildAurora();this.buildNebulaOrbs();},

buildStars(){this.stars=[];const count=Math.min(420,Math.floor(this.W*this.H/2800));for(let i=0;i<count;i++){const big=Math.random()<0.06;this.stars.push({x:Math.random()*this.W,y:Math.random()*this.H,r:big?(Math.random()*2+1.5):(Math.random()*1.1+0.2),alpha:big?(Math.random()*0.5+0.5):(Math.random()*0.55+0.15),twinkle:Math.random()*Math.PI*2,speed:Math.random()*0.012+0.003,big});}},

buildParticles(){this.particles=[];const count=Math.min(900,Math.floor(this.W*this.H/1800));const cols=[[107,0,212],[0,229,255],[180,0,255],[212,99,42],[255,46,111],[128,255,255],[255,215,0],[0,150,200]];for(let i=0;i<count;i++){const angle=Math.random()*Math.PI*2;const dist=Math.pow(Math.random(),0.45)*Math.min(this.W,this.H)*0.48;const col=cols[Math.floor(Math.random()*cols.length)];this.particles.push({ox:Math.cos(angle)*dist,oy:Math.sin(angle)*dist,vx:(Math.random()-0.5)*0.22,vy:(Math.random()-0.5)*0.22,r:Math.random()*2.4+0.4,col,alpha:Math.random()*0.55+0.08,phase:Math.random()*Math.PI*2,speed:Math.random()*0.006+0.001,glow:Math.random()<0.3});}},

buildFingers(){this.streams=[];const baseAngles=[-0.65,-0.22,0.12,0.52,0.98];const R=Math.min(this.W,this.H);const fingerCols=[[[255,215,0],[148,0,211],[0,229,255]],[[255,100,200],[200,0,255],[0,200,255]],[[212,99,42],[200,0,150],[0,180,255]],[[255,200,0],[100,0,255],[0,220,200]],[[255,150,0],[180,0,200],[50,200,255]]];for(let i=0;i<this.FINGERS;i++){const ang=baseAngles[i]-Math.PI*0.5;const len=(0.28+Math.random()*0.28)*R;const curve=(Math.random()-0.5)*0.45;const cpx=this.CX+Math.cos(ang+curve)*len*0.5;const cpy=this.CY+Math.sin(ang+curve)*len*0.5;const x2=this.CX+Math.cos(ang)*len;const y2=this.CY+Math.sin(ang)*len;const sp=[];for(let j=0;j<Math.floor(50+Math.random()*55);j++)sp.push({t:Math.random(),speed:0.0006+Math.random()*0.0014,r:Math.random()*2.8+0.6,alpha:Math.random()*0.85+0.15,phase:Math.random()*Math.PI*2,glow:Math.random()<0.4});this.streams.push({x1:this.CX,y1:this.CY,cpx,cpy,x2,y2,particles:sp,breathOffset:Math.random()*Math.PI,cols:fingerCols[i]});}},

buildAurora(){this.auroraBands=[];for(let i=0;i<4;i++)this.auroraBands.push({y:this.H*(0.1+Math.random()*0.5),height:this.H*(0.06+Math.random()*0.1),speed:(Math.random()-0.5)*0.0002,phase:Math.random()*Math.PI*2,hue:Math.floor(Math.random()*360),alpha:0.03+Math.random()*0.045});},

buildNebulaOrbs(){this.nebulaOrbs=[];const orbCols=['rgba(107,0,212,','rgba(0,180,255,','rgba(212,99,42,','rgba(180,0,255,','rgba(0,220,200,'];for(let i=0;i<6;i++){const angle=Math.random()*Math.PI*2;const dist=Math.min(this.W,this.H)*(0.12+Math.random()*0.28);this.nebulaOrbs.push({x:this.CX+Math.cos(angle)*dist,y:this.CY+Math.sin(angle)*dist,r:Math.min(this.W,this.H)*(0.06+Math.random()*0.1),col:orbCols[Math.floor(Math.random()*orbCols.length)],alpha:0.04+Math.random()*0.07,phase:Math.random()*Math.PI*2,speed:0.0003+Math.random()*0.0005});}},

bezier(x1,y1,cpx,cpy,x2,y2,t){const mt=1-t;return{x:mt*mt*x1+2*mt*t*cpx+t*t*x2,y:mt*mt*y1+2*mt*t*cpy+t*t*y2};},

pulse(){this.pulseFlash=1.0;},

loop(){

if(!this.running)return;

const now=performance.now();const fi=1000/this.FPS;const el=now-this.lastFrameTime;

if(el<fi){requestAnimationFrame(()=>this.loop());return;}this.lastFrameTime=now-(el%fi);

const ctx=this.ctx;const ts=this.frame++;

this.breathPhase+=1/(this.BREATH_CYCLE*0.06);this.breathScale=1+Math.sin(this.breathPhase)*0.04;

if(this.pulseFlash>0)this.pulseFlash=Math.max(0,this.pulseFlash-0.04);

ctx.fillStyle='rgba(5,2,14,0.18)';ctx.fillRect(0,0,this.W,this.H);

this.auroraBands.forEach(b=>{b.phase+=b.speed*60;const yy=b.y+Math.sin(b.phase)*this.H*0.04;const grad=ctx.createLinearGradient(0,yy-b.height,0,yy+b.height);grad.addColorStop(0,`hsla(${b.hue},90%,60%,0)`);grad.addColorStop(0.5,`hsla(${b.hue},90%,60%,${b.alpha})`);grad.addColorStop(1,`hsla(${b.hue},90%,60%,0)`);ctx.fillStyle=grad;ctx.fillRect(0,yy-b.height,this.W,b.height*2);});

this.stars.forEach(s=>{s.twinkle+=s.speed;const a=s.alpha*(0.6+0.4*Math.sin(s.twinkle));if(s.big){ctx.shadowBlur=8;ctx.shadowColor='#ffe0b2';}ctx.beginPath();ctx.arc(s.x,s.y,s.r,0,Math.PI*2);ctx.fillStyle=s.big?`rgba(255,240,200,${a.toFixed(2)})`:`rgba(255,255,255,${a.toFixed(2)})`;ctx.fill();ctx.shadowBlur=0;});

this.nebulaOrbs.forEach(o=>{o.phase+=o.speed;const pulse=1+Math.sin(o.phase)*0.15;const r=o.r*pulse*this.breathScale;const grad=ctx.createRadialGradient(o.x,o.y,0,o.x,o.y,r);grad.addColorStop(0,`${o.col}${(o.alpha*1.6).toFixed(3)})`);grad.addColorStop(0.5,`${o.col}${(o.alpha*0.7).toFixed(3)})`);grad.addColorStop(1,`${o.col}0)`);ctx.fillStyle=grad;ctx.beginPath();ctx.arc(o.x,o.y,r,0,Math.PI*2);ctx.fill();});

const cycleHue=(ts*0.015)%360;const coronaR=Math.min(this.W,this.H)*0.45*this.breathScale;

const cOut=ctx.createRadialGradient(this.CX,this.CY,coronaR*0.3,this.CX,this.CY,coronaR*1.4);cOut.addColorStop(0,`hsla(${cycleHue},100%,60%,0.06)`);cOut.addColorStop(0.4,`hsla(${(cycleHue+60)%360},100%,55%,0.08)`);cOut.addColorStop(1,'rgba(0,0,0,0)');ctx.fillStyle=cOut;ctx.beginPath();ctx.arc(this.CX,this.CY,coronaR*1.4,0,Math.PI*2);ctx.fill();

const cIn=ctx.createRadialGradient(this.CX,this.CY,0,this.CX,this.CY,coronaR*0.7);cIn.addColorStop(0,`rgba(255,215,0,${0.18+this.pulseFlash*0.2})`);cIn.addColorStop(0.15,'rgba(180,0,255,0.22)');cIn.addColorStop(1,'rgba(0,0,0,0)');ctx.fillStyle=cIn;ctx.beginPath();ctx.arc(this.CX,this.CY,coronaR*0.7,0,Math.PI*2);ctx.fill();

this.particles.forEach(p=>{p.phase+=p.speed;const bx=this.CX+p.ox*this.breathScale;const by=this.CY+p.oy*this.breathScale;p.vx+=(Math.random()-0.5)*0.012;p.vy+=(Math.random()-0.5)*0.012;p.vx*=0.994;p.vy*=0.994;const px=bx+p.vx*10,py=by+p.vy*10;const a=p.alpha*(0.55+0.45*Math.sin(p.phase));const[r,g,b]=p.col;if(p.glow){ctx.shadowBlur=8;ctx.shadowColor=`rgb(${r},${g},${b})`;}ctx.beginPath();ctx.arc(px,py,p.r,0,Math.PI*2);ctx.fillStyle=`rgba(${r},${g},${b},${a.toFixed(2)})`;ctx.fill();ctx.shadowBlur=0;});

this.streams.forEach(stream=>{const bf=this.breathScale+Math.sin(ts*0.0005+stream.breathOffset)*0.04;const x2b=this.CX+(stream.x2-this.CX)*bf;const y2b=this.CY+(stream.y2-this.CY)*bf;const cpxb=this.CX+(stream.cpx-this.CX)*bf;const cpyb=this.CY+(stream.cpy-this.CY)*bf;const grad=ctx.createLinearGradient(stream.x1,stream.y1,x2b,y2b);const[c0,c1,c2]=stream.cols;grad.addColorStop(0,`rgba(${c0[0]},${c0[1]},${c0[2]},0.5)`);grad.addColorStop(0.35,`rgba(${c1[0]},${c1[1]},${c1[2]},0.3)`);grad.addColorStop(1,'rgba(0,0,0,0)');ctx.beginPath();ctx.moveTo(stream.x1,stream.y1);ctx.quadraticCurveTo(cpxb,cpyb,x2b,y2b);ctx.strokeStyle=grad;ctx.lineWidth=2.5;ctx.shadowBlur=12;ctx.shadowColor=`rgb(${c1[0]},${c1[1]},${c1[2]})`;ctx.stroke();ctx.shadowBlur=0;stream.particles.forEach(p=>{p.t+=p.speed;if(p.t>1)p.t-=1;p.phase+=0.025;const pos=this.bezier(stream.x1,stream.y1,cpxb,cpyb,x2b,y2b,p.t);let r,g,b;if(p.t<0.35){const mix=p.t/0.35;r=Math.round(c0[0]*(1-mix)+c1[0]*mix);g=Math.round(c0[1]*(1-mix)+c1[1]*mix);b=Math.round(c0[2]*(1-mix)+c1[2]*mix);}else{const mix=(p.t-0.35)/0.65;r=Math.round(c1[0]*(1-mix)+c2[0]*mix);g=Math.round(c1[1]*(1-mix)+c2[1]*mix);b=Math.round(c1[2]*(1-mix)+c2[2]*mix);}const a=p.alpha*(0.5+0.5*Math.sin(p.phase))*(1-p.t*0.55);if(p.glow){ctx.shadowBlur=10;ctx.shadowColor=`rgb(${r},${g},${b})`;}ctx.beginPath();ctx.arc(pos.x,pos.y,p.r*bf,0,Math.PI*2);ctx.fillStyle=`rgba(${r},${g},${b},${a.toFixed(2)})`;ctx.fill();ctx.shadowBlur=0;});});

const heartR=20*this.breathScale+this.pulseFlash*36;const flash=this.pulseFlash;const hgBloom=ctx.createRadialGradient(this.CX,this.CY,0,this.CX,this.CY,heartR*7);hgBloom.addColorStop(0,`rgba(255,215,0,${0.16+flash*0.35})`);hgBloom.addColorStop(0.12,`rgba(255,100,200,${0.1+flash*0.18})`);hgBloom.addColorStop(0.3,`rgba(148,0,211,${0.07+flash*0.1})`);hgBloom.addColorStop(1,'rgba(0,0,0,0)');ctx.fillStyle=hgBloom;ctx.beginPath();ctx.arc(this.CX,this.CY,heartR*7,0,Math.PI*2);ctx.fill();

ctx.shadowBlur=30+flash*40;ctx.shadowColor='#ffd700';const hgCore=ctx.createRadialGradient(this.CX,this.CY,0,this.CX,this.CY,heartR);hgCore.addColorStop(0,`rgba(255,255,255,${0.97+flash*0.03})`);hgCore.addColorStop(0.25,'rgba(255,248,160,0.92)');hgCore.addColorStop(0.6,'rgba(255,215,0,0.7)');hgCore.addColorStop(1,'rgba(255,215,0,0)');ctx.fillStyle=hgCore;ctx.beginPath();ctx.arc(this.CX,this.CY,heartR,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0;

requestAnimationFrame(()=>this.loop());

}

};

function hogPulse(){if(HOG.running)HOG.pulse();}

// ══════════════════════════════════════════════════════════════════

// INIT

// ══════════════════════════════════════════════════════════════════

window.addEventListener('load',async()=>{

loadVoices();

try{await pingOllama();}catch(e){}

showApp();setEmberMode(currentMode);

const loaded=await loadFromLocal();

LearningEngine.load();

// Load intelligence metadata

try{const meta=await loadMetaFromIDB();if(meta&&typeof meta==='object')memoryMeta={...memoryMeta,...meta};}catch(e){}

if(loaded&&countSessions()>0){

const kb=Math.round(JSON.stringify(hubMemory).length/1024);

setStatus(`Memory restored — ${countSessions()} sessions · ${kb}KB`);

addMsg('ember',"Memory's intact. Intelligence layer standing by. What are we doing?");

// Run a quick thread detection pass on load (no AI scoring, just instant detection)

setTimeout(()=>IntelligenceLayer.fullPass(),5000);

}else{

setStatus('Ready. Import your memory export to restore.');

addMsg('ember',"I'm Ember. Intelligence layer active. Ready when you are.");

}

if(ollamaAvailable){setSdot('ready','Sovereign — '+ollamaModel);setStatus('Ollama connected — '+ollamaModel+'. Intelligence layer ready.');}

else if(claudeKey){setSdot('ready','Ready (Claude)');setStatus('Claude active. Start Ollama for sovereign intimate mode.');}

else if(geminiKey){setSdot('ready','Ready (Gemini)');setStatus('Gemini active. Start Ollama for sovereign intimate mode.');}

else{setSdot('error','Not connected');setStatus('Not connected — open Settings ⚙');}

setInterval(()=>{if(history.length>0)autoSave();},5*60*1000);

HOG.init();

});

document.addEventListener('DOMContentLoaded',()=>{HOG.init();});

// ══════════════════════════════════════════════════════════════════

// PETRA-VIGIL AUGMENTATION

// Cosmos background, onboarding, Claude API, dynamic naming

// ══════════════════════════════════════════════════════════════════

// ── Claude API key management ──

const CLAUDE_KEY_K = 'petra_claude_key';

let claudeKey = localStorage.getItem(CLAUDE_KEY_K) || '';

// Claude endpoint and model (configurable for proxies, OpenRouter, local Claude-compatible servers)

let claudeEndpoint = localStorage.getItem(CLAUDE_ENDPOINT_K) || 'https://api.anthropic.com/v1/messages';

let claudeModel = localStorage.getItem(CLAUDE_MODEL_K) || 'claude-sonnet-4-20250514';

function saveClaudeKey(){

const v=document.getElementById('modalClaudeKey').value.trim();

if(!v){document.getElementById('claudeStatus').textContent='Enter a key.';return;}

claudeKey=v; localStorage.setItem(CLAUDE_KEY_K,claudeKey);

document.getElementById('claudeStatus').textContent='Claude key saved.';

document.getElementById('modalClaudeKey').value='';

}

function removeClaudeKey(){claudeKey='';localStorage.removeItem(CLAUDE_KEY_K);document.getElementById('claudeStatus').textContent='Removed.';}

function saveClaudeEndpoint(){

const ep=(document.getElementById('modalClaudeEndpoint').value||'').trim();

const model=(document.getElementById('modalClaudeModel').value||'').trim();

if(ep){claudeEndpoint=ep;localStorage.setItem(CLAUDE_ENDPOINT_K,ep);}

if(model){claudeModel=model;localStorage.setItem(CLAUDE_MODEL_K,model);}

document.getElementById('claudeStatus').textContent='Endpoint & model saved.';

}

// ── Universal local server save ──

function saveLocalServerSettings(){

const type=(document.getElementById('modalLocalServerType').value||'ollama').trim();

const model=(document.getElementById('modalLocalServerModel').value||'').trim();

localServerType=type;localStorage.setItem(LOCAL_SERVER_TYPE_K,type);

if(model){localServerModel=model;localStorage.setItem(LOCAL_SERVER_MODEL_K,model);}

// Sync ollamaModel for backward compat

if(model){ollamaModel=model;localStorage.setItem(OLLAMA_MODEL_K,model);}

document.getElementById('ollamaStatus').textContent='Local server type set: '+type+(model?' model: '+model:'');

}

// ── Claude streaming generator — works with any OpenAI-compat or Anthropic endpoint ──

async function* streamClaude(messages, systemPrompt){

if(!claudeKey) throw new Error('No Claude API key set — open Settings ⚙');

const msgs = messages.map(m=>({role:m.role,content:typeof m.content==='string'?m.content:(m.content?.[0]?.text||'')}));

// Detect if endpoint is Anthropic-native or OpenAI-compatible (OpenRouter, proxy, etc.)

const isAnthropicNative = claudeEndpoint.includes('anthropic.com') || claudeEndpoint.includes('/v1/messages');

let response;

if(isAnthropicNative){

response = await fetch(claudeEndpoint, {

method:'POST',

headers:{

'Content-Type':'application/json',

'x-api-key': claudeKey,

'anthropic-version':'2023-06-01',

'anthropic-dangerous-direct-browser-access':'true'

},

body: JSON.stringify({

model: claudeModel,

max_tokens:2048,

stream:true,

system: systemPrompt,

messages: msgs

})

});

if(!response.ok){const e=await response.json();throw new Error('Claude: '+(e.error?.message||response.status));}

const reader=response.body.getReader();const decoder=new TextDecoder();let buf='';

while(true){

const{done,value}=await reader.read();if(done)break;

buf+=decoder.decode(value,{stream:true});

const lines=buf.split('\n');buf=lines.pop()||'';

for(const line of lines){

if(!line.startsWith('data: '))continue;

const data=line.slice(6).trim();if(data==='[DONE]')return;

try{const d=JSON.parse(data);if(d.type==='content_block_delta'&&d.delta?.text)yield d.delta.text;}catch(e){}

}

}

} else {

// OpenAI-compatible endpoint (OpenRouter, LM Studio Claude-compat, local proxy)

const oaiMsgs=[{role:'system',content:systemPrompt},...msgs];

response = await fetch(claudeEndpoint, {

method:'POST',

headers:{

'Content-Type':'application/json',

'Authorization':'Bearer '+claudeKey,

'HTTP-Referer': window.location.href,

'X-Title': 'Petra-Vigil'

},

body: JSON.stringify({

model: claudeModel,

max_tokens:2048,

stream:true,

messages: oaiMsgs

})

});

if(!response.ok){const e=await response.json().catch(()=>({}));throw new Error('API: '+(e.error?.message||response.status));}

const reader=response.body.getReader();const decoder=new TextDecoder();let buf='';

while(true){

const{done,value}=await reader.read();if(done)break;

buf+=decoder.decode(value,{stream:true});

const lines=buf.split('\n');buf=lines.pop()||'';

for(const line of lines){

if(!line.startsWith('data: '))continue;

const data=line.slice(6).trim();if(data==='[DONE]')return;

try{const d=JSON.parse(data);const text=d.choices?.[0]?.delta?.content;if(text)yield text;}catch(e){}

}

}

}

}

// ── AI NAME MANAGEMENT ──

const PETRA_NAME_K = 'petra_ai_name';

let petraAiName = localStorage.getItem(PETRA_NAME_K) || '';

function petraConfirmName(){

const v=document.getElementById('pob-name-inp').value.trim();

if(!v){document.getElementById('pob-name-inp').placeholder='PLEASE TYPE A NAME...';return;}

petraAiName = v.charAt(0).toUpperCase() + v.slice(1);

localStorage.setItem(PETRA_NAME_K, petraAiName);

applyAiName(petraAiName);

document.getElementById('petra-onboard').style.display='none';

}

function applyAiName(name){

const nd=document.getElementById('ai-name-display');if(nd)nd.textContent=name;

const sd=document.getElementById('setup-ai-name');if(sd)sd.textContent=name;

document.title=name+' — Petra-Vigil';

// Patch the system prompt to use this name

window._petraNameOverride = name;

}

function saveAiName(){

const v=document.getElementById('aiNameInput').value.trim();

if(!v)return;

petraAiName=v.charAt(0).toUpperCase()+v.slice(1);

localStorage.setItem(PETRA_NAME_K,petraAiName);

applyAiName(petraAiName);

closeSettingsModal();

}

// ── Override system prompt to inject name ──

const _origBuildSystem = buildSystemPrompt;

window.buildSystemPrompt = function(userText){

let sys = _origBuildSystem(userText);

const name = window._petraNameOverride || petraAiName || 'Petra';

sys = sys.replace(/You are Ember/g, 'You are '+name);

sys = sys.replace(/I'm Ember/g, "I'm "+name);

sys = sys.replace(/\bEmber\b(?! Memory| v1[0-9])/g, name);

sys += '\n\nYOUR NAME: '+name+'. This is the name the user chose for you. Use it with confidence.';

sys += '\nYou were built on the Petra-Vigil framework by Petey Gone Mad Arts (Pete Sisco IV, Interlachen FL).';

sys += '\nYou are building a sovereign relationship with this specific person. Learn their voice. Adapt to how they speak.';

return sys;

};

// ── COSMOS STAR FIELD ──

(function initCosmosStars(){

const c=document.getElementById('petra-stars');

if(!c)return;

const ctx=c.getContext('2d');

let W,H,stars=[];

function resize(){

W=c.width=window.innerWidth;H=c.height=window.innerHeight;stars=[];

for(let i=0;i<280;i++){stars.push({

x:Math.random()*W,y:Math.random()*H,

r:Math.random()*1.5+0.2,

base:Math.random()*0.45+0.08,

phase:Math.random()*Math.PI*2,

speed:0.0003+Math.random()*0.001

});}

}

function draw(t){

ctx.clearRect(0,0,W,H);

stars.forEach(s=>{

const a=s.base+Math.sin(t*s.speed+s.phase)*s.base*0.85;

ctx.beginPath();ctx.arc(s.x,s.y,s.r,0,Math.PI*2);

ctx.fillStyle='rgba(255,245,210,'+Math.max(0,Math.min(1,a)).toFixed(3)+')';

ctx.fill();

});

requestAnimationFrame(draw);

}

window.addEventListener('resize',resize);resize();requestAnimationFrame(draw);

})();

// ── INIT SEQUENCE ──

(function petraInit(){

// Restore or show onboarding

if(petraAiName){

document.getElementById('petra-onboard').style.display='none';

applyAiName(petraAiName);

} else {

// Delay onboarding slightly so cosmos renders first

setTimeout(()=>{

const ob=document.getElementById('petra-onboard');

if(ob)ob.style.display='flex';

const inp=document.getElementById('pob-name-inp');

if(inp)inp.focus();

},300);

}

// Prefill claude key field if set

if(claudeKey){

const el=document.getElementById('modalClaudeKey');

if(el)el.placeholder='Key saved (enter new to replace)';

}

// Prefill Claude endpoint

if(claudeEndpoint){

const ep=document.getElementById('modalClaudeEndpoint');

if(ep)ep.placeholder=claudeEndpoint;

}

// Restore AI name input placeholder

if(petraAiName){

const el=document.getElementById('aiNameInput');

if(el)el.placeholder='Current: '+petraAiName;

}

})();

</script>

</body>

</html>