1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <link rel="icon" type="image/x-icon" href="/favicon.ico"> 5 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 6 <link rel="apple-touch-icon" href="/apple-touch-icon.png"> 7 <meta charset="UTF-8"> 8 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 9 <title>Login</title> 10 <meta name="description" content="Immortalize the story of your life with our AI-powered memoir service. Capture and share your life's narrative, leaving a legacy that inspires generations."> 11 <link rel="canonical" href="https://www.memoirmaker.ai/auth.html"> 12 13 <!-- Open Graph --> 14 <meta property="og:type" content="website"> 15 <meta property="og:site_name" content="MemoirMaker.ai"> 16 <meta property="og:title" content="MemoirMaker.ai - Transform Memories into a Lasting Legacy"> 17 <meta property="og:description" content="Immortalize the story of your life with our AI-powered memoir service. Capture and share your life's narrative, leaving a legacy that inspires generations."> 18 <meta property="og:image" content="https://www.memoirmaker.ai/assets/img/custom/og-image.jpg"> 19 <meta property="og:url" content="https://www.memoirmaker.ai/auth.html"> 20 21 <!-- Twitter Card --> 22 <meta name="twitter:card" content="summary_large_image"> 23 24 <!-- Google Fonts --> 25 <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Open+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet"> 26 <link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet"> 27 28 <!-- Bootstrap 5 --> 29 <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> 30 <link href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css" rel="stylesheet"> 31 32 <!-- Custom CSS --> 33 <link href="/assets/css/style.css?v=20260619-intent" rel="stylesheet"> 34 35 36 37 38 39 40 41 42 43 44
45<script type="application/ld+json"> 46{"@context": "https://schema.org", "@graph": [{"@id": "https://www.memoirmaker.ai/#organization", "@type": "Organization", "description": "MemoirMaker.ai turns memories into a finished memoir \u2014 speak or type fragments, the AI shapes them into chapters in your voice. Co-author support, recurring characters, AI cover art, DOCX + PDF export. Start free, finish a memoir on the web, continue on mobile, then turn the same manuscript into a narrated film with MemoirMovies.com.", "logo": "https://www.memoirmaker.ai/assets/img/custom/logo.png", "name": "MemoirMaker.ai", "url": "https://www.memoirmaker.ai"}, {"@id": "https://www.memoirmaker.ai/#website", "@type": "WebSite", "description": "MemoirMaker.ai turns memories into a finished memoir \u2014 speak or type fragments, the AI shapes them into chapters in your voice. Co-author support, recurring characters, AI cover art, DOCX + PDF export. Start free, finish a memoir on the web, continue on mobile, then turn the same manuscript into a narrated film with MemoirMovies.com.", "inLanguage": "en-US", "name": "MemoirMaker.ai", "publisher": {"@id": "https://www.memoirmaker.ai/#organization"}, "url": "https://www.memoirmaker.ai"}]} 47</script>
vendor: 99 bytes, lines 47-57
47 48 49 50 51 52 53 54 55 56 57 <script async src="https://www.googletagmanager.com/gtag/js?id=
57G-QN7YTR6R5X
vendor: 16 bytes, lines 57-58
57"></script> 58
58<script> 59
vendor: 145 bytes, lines 59-62
59window.dataLayer = window.dataLayer || []; 60 function gtag(){dataLayer.push(arguments);} 61 gtag('js', new Date()); 62 gtag('config', '
62G-QN7YTR6R5X
vendor: 10 bytes, lines 62-63
62'); 63
63gtag('config', 'AW-11496617174'); 64 </script>
64 65 66 67
67<script>
vendor: 403 bytes, lines 67-69
67 68 !function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js'); 69 fbq('init', '
69320704642247362
vendor: 34 bytes, lines 69-70
69'); fbq('track', 'PageView'); 70
70</script>
70 71 <noscript><img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=320704642247362&ev=PageView&noscript=1"/></noscript> 72 73 74 75 <meta name="saashub-verification" content="e4socv195w2b" />
vendor: 64 bytes, lines 75-76
75 76<script async src="https://www.googletagmanager.com/gtag/js?id=
76AW-11496617174
vendor: 12 bytes, line 76
76"></script>
77</head> 78<body> 79 80 81 82 83 84 85 86 87<nav class="navbar navbar-expand-xl sticky-top shadow-sm hcwf-navbar" style="background: var(--primary-color, #2c3e50); padding: 8px 0; z-index: 1030;"> 88 <style> 89 .hcwf-navbar .container-fluid { display: flex; align-items: center; justify-content: space-between; } 90 .hcwf-navbar .navbar-brand { flex: 0 0 min(350px, calc(100% - 84px)); min-width: 0; width: min(350px, calc(100% - 84px)); max-width: min(350px, calc(100% - 84px)); margin-right: .75rem; overflow: visible; padding: 0; } 91 .hcwf-navbar .hcwf-brand-stack { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; } 92 .hcwf-navbar .hcwf-logo-link { display: block; width: 350px; max-width: 100%; line-height: 0; } 93 .hcwf-navbar .navbar-brand img { width: 350px !important; max-width: 100% !important; height: auto; max-height: none; object-fit: contain; object-position: left center; display: block; } 94 .hcwf-navbar .hcwf-support-phone { color: #ffffff !important; display: flex; align-items: center; justify-content: center; min-height: 44px; font-size: .875rem; font-weight: 600; line-height: 1.2; opacity: .9; text-decoration: none; text-align: center; width: 100%; white-space: nowrap; padding: .25rem .35rem; } 95 .hcwf-navbar .hcwf-support-phone:hover { opacity: 1; text-decoration: underline; } 96 .hcwf-navbar .navbar-toggler { flex: 0 0 58px; width: 58px; height: 58px; margin-left: auto; display: inline-flex; align-items: center; justify-content: center; padding: 0; border-color: rgba(255,255,255,.35); } 97 @media (min-width: 75rem) { .hcwf-navbar .navbar-toggler { display: none !important; } } 98 @media (max-width: 74.9375rem) { 99 .hcwf-navbar .container-fluid { flex-wrap: nowrap !important; gap: .5rem; } 100 .hcwf-navbar .navbar-brand { flex-basis: calc(100% - 74px); width: calc(100% - 74px); max-width: calc(100% - 74px); } 101 .hcwf-navbar .navbar-brand img { width: 100%; max-width: 350px !important; } 102 .hcwf-navbar .navbar-collapse { position: absolute; top: 100%; left: 0; right: 0; background: var(--primary-color, #2c3e50); padding: .75rem 1rem 1rem; border-top: 1px solid rgba(255,255,255,.08); box-shadow: 0 12px 24px rgba(0,0,0,.22); } 103 .hcwf-navbar .navbar-nav { align-items: flex-start !important; } 104 } 105 @media (max-width: 767px) { 106 .hcwf-navbar { padding-top: 6px !important; padding-bottom: 6px !important; } 107 .hcwf-navbar .container-fluid { gap: .35rem; } 108 .hcwf-navbar .navbar-brand { flex-basis: calc(100% - 66px); width: calc(100% - 66px); max-width: calc(100% - 66px); margin-right: .35rem; } 109 .hcwf-navbar .navbar-brand img { max-width: min(350px, 100%) !important; } 110 .hcwf-navbar .hcwf-support-phone { font-size: .875rem; } 111 .hcwf-navbar .navbar-toggler { flex-basis: 52px; width: 52px; height: 52px; } 112 } 113 114 .hcwf-navbar .nav-link.hcwf-nav-cta, 115 .hcwf-navbar .navbar-nav > .nav-item > .nav-link.hcwf-nav-cta { 116 color: var(--mm-accent-text, var(--hcwf-navbar-text, #FFFFFF)) !important; 117 } 118</style> 119 <div class="container-fluid d-flex align-items-center justify-content-between"> 120 <div class="navbar-brand hcwf-brand-stack"> 121 <a class="hcwf-logo-link" href="/" aria-label="MemoirMaker.ai home"> 122 <img src="https://www.memoirmaker.ai/assets/img/custom/logo.png" alt="MemoirMaker.ai"> 123 </a> 124 125 <a class="hcwf-support-phone" href="tel:+19786438662">24/7 Sales / Support: +1 (978) 643-8662</a> 126 127 </div> 128 <button class="navbar-toggler text-white" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-label="Open navigation menu"> 129 <i class="bi bi-list"></i> 130 </button> 131 <div class="collapse navbar-collapse" id="navbarNav"> 132 <ul class="navbar-nav ms-auto align-items-center"> 133 <li class="nav-item"><a class="nav-link px-3 text-white" href="/">Home</a></li> 134 <li class="nav-item"><a class="nav-link px-3 text-white" href="/#pricing">Pricing</a></li> 135 <li class="nav-item"><a class="nav-link px-3 text-white" href="/contact">Contact</a></li> 136 137 <li class="nav-item dropdown"> 138 <a class="nav-link px-3 text-white dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Resources</a> 139 <ul class="dropdown-menu dropdown-menu-end shadow border-0"> 140 <li><a class="dropdown-item py-2" href="/blog"><i class="bi bi-journal-text me-2"></i>Blog</a></li> 141 <li><a class="dropdown-item py-2" href="/learn/"><i class="bi bi-book me-2"></i>Guides</a></li> 142 143 <li><a class="dropdown-item py-2" href="/ai-memoir-writer"><i class="bi bi-stars me-2"></i>AI Memoir Writer</a></li> 144 145 <li><a class="dropdown-item py-2" href="/ai-memoir-writing-service"><i class="bi bi-magic me-2"></i>AI Memoir Writing Service</a></li> 146 147 <li><a class="dropdown-item py-2" href="/memoir-writing-app"><i class="bi bi-phone me-2"></i>Memoir Writing App</a></li> 148 149 <li><a class="dropdown-item py-2" href="/write-my-life-story"><i class="bi bi-pencil-square me-2"></i>Write My Life Story</a></li> 150 151 <li><a class="dropdown-item py-2" href="/autobiography-writing-help"><i class="bi bi-journal-richtext me-2"></i>Autobiography Writing Help</a></li> 152 153 <li><a class="dropdown-item py-2" href="/gift-a-memoir"><i class="bi bi-gift me-2"></i>Gift a Memoir</a></li> 154 155 <li><a class="dropdown-item py-2" href="/memoir-writing-service"><i class="bi bi-journal-text me-2"></i>Memoir Writing Service</a></li> 156 157 <li><a class="dropdown-item py-2" href="/memoir-ghostwriting">
157<i class="bi bi-person-lines-fill me-2"></i>Memoir Ghostwriting</a></li> 158 159 <li><a class="dropdown-item py-2" href="/memoirmaker-vs-ghostwriter"><i class="bi bi-columns-gap me-2"></i>MemoirMaker vs Ghostwriter</a></li> 160 161 <li><a class="dropdown-item py-2" href="/voice-to-memoir"><i class="bi bi-mic me-2"></i>Voice to Memoir</a></li> 162 163 <li><a class="dropdown-item py-2" href="/family-history-book"><i class="bi bi-people me-2"></i>Family History Book</a></li> 164 165 <li><a class="dropdown-item py-2" href="/memoir-writing-prompts"><i class="bi bi-card-checklist me-2"></i>Memoir Writing Prompts</a></li> 166 167 <li><a class="dropdown-item py-2" href="/how-to-write-a-memoir"><i class="bi bi-map me-2"></i>How to Write a Memoir</a></li> 168 169 <li><a class="dropdown-item py-2" href="/memoir-structure"><i class="bi bi-diagram-3 me-2"></i>Memoir Structure</a></li> 170 171 <li><a class="dropdown-item py-2" href="/faq"><i class="bi bi-question-circle me-2"></i>FAQ</a></li> 172 </ul> 173 </li> 174 175 <li class="nav-item"><a class="nav-link px-3 text-white" href="https://www.archieboy.com/affiliate/">Affiliates</a></li> 176 177 178 179<li class="nav-item"><a class="nav-link px-3 text-white" href="/auth.html" style="font-weight: 600;">Login</a></li> 180<li class="nav-item"><a class="nav-link px-3 ms-lg-2 hcwf-nav-cta" href="/register" style="background: #e8c547; --mm-accent-text: #1a1a2e; color: #1a1a2e; border-radius: 50px; padding: 8px 25px; font-weight: 600;">Join Free</a></li> 181 182 </ul> 183 </div> 184 </div> 185</nav> 186 187 <main id="main"> 188 189<style> 190 .auth-shell { 191 min-height: 70vh; 192 display: flex; 193 align-items: center; 194 justify-content: center; 195 padding: 3rem 1rem; 196 background: var(--page-bg, #f6f8fb); 197 } 198 .auth-card { 199 width: 100%; 200 max-width: 500px; 201 margin: 0 auto; 202 background: var(--surface-bg, #ffffff); 203 border: 1px solid var(--border-color, rgba(0,0,0,.08)); 204 border-radius: 20px; 205 box-shadow: 0 10px 35px rgba(0,0,0,.12); 206 padding: 2.5rem; 207 color: var(--text-dark, #213547); 208 font-family: var(--font-body, inherit); 209 } 210 .auth-card h1,.auth-card h2,.auth-card h3,.auth-card h4,.auth-card h5,.auth-card h6 { color: var(--text-dark, #213547); font-family: var(--font-display, var(--font-body, inherit)); } 211 .auth-title { text-align: center; margin-bottom: .5rem; color: var(--text-dark, #213547); } 212 .auth-subtitle { text-align: center; color: var(--muted-text, #5b6878); margin-bottom: 1.5rem; } 213 .auth-card .form-label { font-weight: 600; color: var(--text-dark, #213547); } 214 .auth-card .form-text, .auth-card .text-muted, .auth-card small, .auth-card p.small { color: var(--muted-text, #5b6878) !important; } 215 .auth-card .form-control { 216 border-radius: 12px; padding: .8rem .95rem; background: var(--surface-alt-bg, #ffffff); 217 border: 1px solid var(--border-color, #d9e2ec); color: var(--text-dark, #213547); 218 } 219 .auth-card .form-control::placeholder { color: var(--muted-text, #6c757d) !important; opacity: 1; } 220 .auth-card .form-control:focus { 221 background: var(--surface-alt-bg, #ffffff); border-color: var(--secondary-color, var(--accent_bg, #b7eaf8)); 222 color: var(--text-dark, #213547); box-shadow: 0 0 0 .2rem color-mix(in srgb, var(--secondary-color, #b7eaf8) 35%, transparent); 223 } 224 .auth-card .btn-primary { 225 border-radius: 999px; padding: .85rem 1.1rem; font-weight: 700; background: var(--primary-color, #2c3e50); 226 border: 1px solid var(--primary-color, #2c3e50); color: var(--mm-brand-text, #ffffff) !important; 227 } 228 .auth-card .btn-outline-secondary { 229 border-radius: 999px; padding: .8rem 1.1rem; color: var(--text-dark, #213547) !important; 230 border: 1px solid var(--border-color, #d9e2ec); background: transparent; 231 } 232 .auth-card .btn-outline-secondary:hover { background: var(--surface-alt-bg, #eef6fb); border-color: var(--secondary-color, #b7eaf8); } 233 .auth-brand-link { color: var(--primary-color, #2c3e50); text-decoration: none; font-weight: 600; } 234 .auth-brand-link:hover { color: color-mix(in srgb, var(--primary-color, #2c3e50) 82%, white); } 235 .auth-icon { color: var(--primary-color, #2c3e50); } 236 .auth-card hr { border-color: var(--border-color, #d9e2ec); opacity: .8; } 237</style> 238 239<section class="auth-shell"> 240 <div class="container"> 241 <div class="auth-card"> 242 <div id="login-form-content"> 243 <div class="text-center mb-4"> 244 <i class="bi bi-box-arrow-in-right auth-icon" style="font-size:2.5rem;"></i> 245 <h3 class="auth-title mt-2">Welcome Back</h3> 246 <p class="auth-subtitle small">Sign in to your account.</p> 247 </div> 248 249 250 251 <form id="login-form"> 252 <div class="mb-3"> 253 <label class="form-label small">Email Address</label> 254 <input type="text" class="form-control" id="login-username" required placeholder="[email protected]" inputmode="email" autocomplete="username" autocapitalize="off" spellcheck="false" autofocus> 255 </div> 256 <div class="mb-3"> 257 <label class="form-label small">Password</label> 258 <input type="password" class="form-control" id="login-password" required autocomplete="current-password"> 259 </div> 260 <div class="text-end mb-3"> 261 <a href="/forgot-password" class="small auth-brand-link">Forgot Password?</a> 262 </div> 263 <button type="submit" class="btn btn-primary w-100" id="login-btn" data-idle-label="Login">Login</button> 264 </form> 265 266 267 <div class="mt-4 text-center"> 268 <div class="d-flex align-items-center my-3"> 269 <hr class="flex-grow-1"> <span class="mx-3 small" style="color: var(--muted-text, #5b6878);">OR</span> <hr class="flex-grow-1"> 270 </div> 271 <a href="/login/google" class="btn btn-outline-secondary w-100 d-flex align-items-center justify-content-center py-2">
272 <img src="https://www.gstatic.com/images/branding/product/1x/gsa_512dp.png" style="height: 18px; margin-right: 10px;"> Sign in with Google 273 </a> 274 </div> 275 276 277 278 <div class="text-center mt-4"> 279 280 <p class="small text-muted">Don't have an account? <a href="/register" class="auth-brand-link fw-bold">Sign Up</a></p> 281 282 283 284 </div> 285 </div> 286 287 <div id="message-container" class="text-center" style="display:none;"> 288 <div id="status-icon" class="mb-3"></div> 289 <h4 id="status-title"></h4> 290 <p id="status-text" class="text-muted"></p> 291 </div> 292 </div> 293 </div> 294</section> 295 296 </main> 297 298 <footer id="footer" style="background: var(--mm-navbar-bg, #1a1a2e); padding: 50px 0 20px 0; color: #ccc; font-size: 14px;"> 299 <div class="container"> 300 <div class="row"> 301 <!-- C1: Company Info --> 302 <div class="col-lg-3 col-md-6 mb-4"> 303 <div class="mb-3"> 304 <a href="/" aria-label="MemoirMaker.ai home"><img src="https://www.memoirmaker.ai/assets/img/custom/logo.png" alt="MemoirMaker.ai" style="max-height: 45px;"></a> 305 </div> 306 <p style="line-height: 1.8; margin: 0;"> 307 Archieboy Holdings, LLC<br> 308 #1006, 771 Boston Post Rd STE 11<br> 309 Marlborough, MA 01752<br> 310 Phone: +1 978-643-8662<br> 311 Text: +1 978-643-8662<br> 312 Email: <a href="#" class="footer-email-link" style="color: var(--mm-accent, #C4A265);" 313 aria-label="Email MemoirMaker.ai" 314 data-name="hello" data-domain="memoirmaker.ai" 315 onclick="this.href='mai'+'lto:'+this.dataset.name+'@'+this.dataset.domain; return true;"> 316 <span class="footer-email" data-name="hello" data-domain="memoirmaker.ai">hello [at] memoirmaker.ai</span> 317 </a> 318 </p> 319 </div> 320 321 <!-- C2: Quick Links --> 322 <div class="col-lg-3 col-md-6 mb-4"> 323 <h5 class="text-white mb-3">Quick Links</h5> 324 <ul style="list-style: none; padding: 0; margin: 0; line-height: 2.2;"> 325 <li><i class="bi bi-chevron-right" style="color: var(--mm-accent, #C4A265); font-size: 14px;"></i> <a href="/" style="color: #ccc;">Home</a></li> 326 <li><i class="bi bi-chevron-right" style="color: var(--mm-accent, #C4A265); font-size: 14px;"></i> <a href="/blog" style="color: #ccc;">Blog</a></li> 327 <li><i class="bi bi-chevron-right" style="color: var(--mm-accent, #C4A265); font-size: 14px;"></i> <a href="/faq" style="color: #ccc;">FAQ</a></li> 328 <li><i class="bi bi-chevron-right" style="color: var(--mm-accent, #C4A265); font-size: 14px;"></i> <a href="/privacy" style="color: #ccc;">Privacy Policy</a></li> 329 <li><i class="bi bi-chevron-right" style="color: var(--mm-accent, #C4A265); font-size: 14px;"></i> <a href="/terms" style="color: #ccc;">Terms of Service</a></li> 330 331 </ul> 332 </div> 333 334 <div class="col-lg-3 col-md-6 mb-4"> 335 <h5 class="text-white mb-3">Archieboy Network</h5> 336 <div style="background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 14px;"> 337 <a href="https://www.archieboy.com/" target="_blank" rel="noopener" aria-label="Visit Archieboy Holdings"> 338 <img src="https://www.archieboy.com/static/img/logo.png" alt="Archieboy Holdings, LLC" style="width: 190px; max-width: 100%; height: auto; display: block; margin-bottom: 12px;"> 339 </a> 340 <p style="line-height: 1.7; margin: 0;"> 341 This site is part of the <a href="https://www.archieboy.com/" target="_blank" rel="noopener" style="color: var(--mm-accent, #C4A265);">Archieboy Holdings, LLC</a> network of websites. To become an affiliate of this website and dozens more in our network, visit <a href="https://www.archieboy.com/affiliate/" target="_blank" rel="noopener" style="color: var(--mm-accent, #C4A265);">Our Affiliate Program Page</a>. 342 </p> 343 </div> 344 </div> 345 346 <!-- C4: Latest Updates --> 347 <div class="col-lg-3 col-md-6 mb-4"> 348 <h5 class="text-white mb-3">Latest Updates</h5> 349 <div style="background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 12px; max-height: 150px; overflow-y: auto;"> 350 <ul id="updates-list" style="list-style: none; padding: 0; margin: 0;"> 351 352 <li style="margin-bottom: 10px; position: relative;" data-id="50">
353 <span style="font-weight: bold; color: var(--mm-accent, #C4A265);">• 2026-10-05:</span> Memoir films are now hand-drawn animated movies, scripted from your memoir and made one part at a time with your approval. 354 355 </li> 356 357 <li style="margin-bottom: 10px; position: relative;" data-id="49"> 358 <span style="font-weight: bold; color: var(--mm-accent, #C4A265);">• 2026-09-24:</span> We've upgraded the AI that writes your chapters â expect more thoughtful, natural-sounding memoir prose. 359 360 </li> 361 362 <li style="margin-bottom: 10px; position: relative;" data-id="48"> 363 <span style="font-weight: bold; color: var(--mm-accent, #C4A265);">• 2026-09-22:</span> iPhone Pro subscribers can now keep writing past three sections, start more memoirs, and download normally. 364 365 </li> 366 367 <li style="margin-bottom: 10px; position: relative;" data-id="47"> 368 <span style="font-weight: bold; color: var(--mm-accent, #C4A265);">• 2026-09-01:</span> Voice dictation now matches your memoir's language, for more accurate recording in Danish, Swedish, Norwegian, Finnish, Polish, and more. 369 370 </li> 371 372 <li style="margin-bottom: 10px; position: relative;" data-id="46"> 373 <span style="font-weight: bold; color: var(--mm-accent, #C4A265);">• 2026-08-27:</span> You can now enter a coupon code at checkout for 10% off your memoir credit pack. 374 375 </li> 376 377 </ul> 378 379 </div> 380 </div> 381 </div> 382 </div> 383 384 <!-- Copyright Bar --> 385 <div class="container text-center mt-4 pt-3" style="border-top: 1px solid rgba(255,255,255,0.1);"> 386 <p class="mb-0" style="color: #999;"> 387 © <a href="/api/login" style="color: #999; text-decoration: none;">Copyright</a> 2026, <strong><a href="https://www.archieboy.com" style="color: #999; text-decoration: none;" target="_blank">Archieboy Holdings, LLC.</a></strong> All Rights Reserved. 388 </p> 389 </div> 390</footer> 391 392<!-- Anti-spam email rendering -->
393<script> 394document.addEventListener('DOMContentLoaded', function() { 395 document.querySelectorAll('.footer-email').forEach(function(el) { 396 var n = el.dataset.name, d = el.dataset.domain; 397 el.textContent = n + '\u0040' + d; 398 }); 399}); 400</script>
400 401 402 403 404
404<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
404 405
405<script> 406 (function(){ 407 var key='memoirmaker_anon_id', anon=''; 408 try { 409 anon=localStorage.getItem(key)||''; 410 if(!anon){ anon=(crypto.randomUUID ? crypto.randomUUID() : String(Date.now())+Math.random()); localStorage.setItem(key,anon); } 411 } catch(e) {} 412 window.mmTrack=function(eventName, properties, bookid){ 413 var body=JSON.stringify({event:eventName,anonymous_id:anon,bookid:bookid||null,source:'web',properties:properties||{}}); 414 try { 415 if(navigator.sendBeacon){ navigator.sendBeacon('/api/product-event',new Blob([body],{type:'application/json'})); } 416 else { fetch('/api/product-event',{method:'POST',headers:{'Content-Type':'application/json'},body:body,keepalive:true}); } 417 } catch(e) {} 418 }; 419 document.addEventListener('click',function(e){ 420 var target=e.target.closest && e.target.closest('[data-mm-event]'); 421 if(target){ window.mmTrack(target.dataset.mmEvent,{label:(target.textContent||'').trim(),href:target.getAttribute('href')||''},target.dataset.mmBookid||null); } 422 }); 423 })(); 424 </script>
424 425
426<script> 427function showMessage(type, title, text, doRedirect) { 428 document.getElementById('login-form-content').style.display = 'none'; 429 const mc = document.getElementById('message-container'); 430 mc.style.display = 'block'; 431 document.getElementById('status-title').innerText = title; 432 document.getElementById('status-text').innerText = text; 433 if (type === 'success') { 434 document.getElementById('status-icon').innerHTML = '<i class="bi bi-check-circle-fill text-success" style="font-size:3rem;"></i>'; 435 if (doRedirect) setTimeout(() => { window.location.href = loginRedirectTarget(); }, 700); 436 } else { 437 document.getElementById('status-icon').innerHTML = '<i class="bi bi-exclamation-triangle-fill text-danger" style="font-size:3rem;"></i>'; 438 const btn = document.createElement('button'); 439 btn.className = 'btn btn-secondary mt-3'; 440 btn.innerText = 'Try Again'; 441 btn.onclick = () => { document.getElementById('login-form-content').style.display = 'block'; mc.style.display = 'none'; btn.remove(); }; 442 mc.appendChild(btn); 443 } 444} 445 446function loginRedirectTarget() { 447 const params = new URLSearchParams(window.location.search); 448 const next = params.get('next') || ''; 449 if (next.startsWith('/') && !next.startsWith('//')) return next; 450 return '/'; 451} 452 453document.getElementById('login-form').addEventListener('submit', async (e) => { 454 e.preventDefault(); 455 const btn = document.getElementById('login-btn'); 456 btn.disabled = true; 457 btn.innerText = 'Signing in...'; 458 try { 459 const resp = await fetch('/api/login', { 460 method: 'POST', 461 headers: { 'Content-Type': 'application/json' }, 462 body: JSON.stringify({ 463 username: document.getElementById('login-username').value, 464 password: document.getElementById('login-password').value 465 }) 466 }); 467 if (resp.ok || resp.redirected) { 468 showMessage('success', 'Login Successful', 'Welcome back!', true); 469 } else { 470 let msg = 'Invalid credentials'; 471 try { const result = await resp.json(); msg = result.error || msg; } catch(e) {} 472 showMessage('error', 'Login Failed', msg); 473 btn.disabled = false; 474 btn.innerText = btn.dataset.idleLabel || 'Login'; 475 } 476 } catch (err) { 477 window.location.href = '/'; 478 } 479}); 480</script>
480 481 482 483 484 485 486 487 488 489 490 491 492 493<div class="riley-dock" id="rileyDock"> 494 <button type="button" class="riley-launch" id="rileyLaunch" 495 aria-expanded="false" aria-controls="rileyWidget" 496 497 aria-label="Questions? Ask Riley"> 498 <img src="/riley/static/riley-96.jpg" alt="" width="34" height="34" aria-hidden="true"> 499 <span class="riley-launch-label">Questions? Ask Riley</span> 500 </button> 501 502<div class="riley-widget riley-engaged riley-floating" id="rileyWidget" 503 hidden 504 style="--riley-h: 480px;"> 505 506 <div class="riley-head"> 507 <img class="riley-face" src="/riley/static/riley-96.jpg" alt="Riley" width="38" height="38"> 508 <div class="riley-who"> 509 <strong>Riley</strong> 510 <span class="riley-sub" id="rileyStatus">Support · AI agent</span> 511 </div> 512 <button type="button" class="riley-mode" id="rileyModeBtn" 513 title="Switch to voice" aria-label="Talk with Riley"> 514 <span class="riley-mode-ico" aria-hidden="true"></span> 515 <span id="rileyModeLbl">Talk</span> 516 </button> 517 518 <button type="button" class="riley-close" id="rileyClose" 519 title="Close" aria-label="Close chat">×</button> 520 521 </div> 522 523 <div class="riley-body"> 524 <div class="riley-log" id="rileyLog" role="log" aria-live="polite"></div> 525 526 527 528 </div> 529 530 <form class="riley-input" id="rileyForm" autocomplete="off"> 531 <input type="text" id="rileyText" maxlength="1200" 532 placeholder="Ask me anything…" aria-label="Message to Riley"> 533 <button type="submit" id="rileySend" aria-label="Send">↑</button> 534 </form> 535 536 <p class="riley-foot"> 537 Riley is an AI agent. She can look things up, email you details, and pass 538 anything she can’t finish to a person. 539 </p> 540</div> 541 542</div> 543 544 545 546<style> 547/* Riley takes her colours from the site she is standing on. 548 * 549 * Every HCWF site injects a palette into :root at render time, and the pair we 550 * build on is --mm-navbar-bg / --mm-navbar-text. That pair is already a 551 * validated dark surface with a legible foreground on each site â it is what 552 * the header and footer use â so deriving from it keeps the widget on-brand 553 * without anyone hand-picking a colour per site. A site with a LIGHT navbar 554 * gets a light widget for the same reason, and it still reads correctly, 555 * because the foreground travels with the background. 556 * 557 * Everything else â muted text, bubbles, borders, the input well â is mixed 558 * from those two, so there are no third-party colours to drift out of step. 559 * Each mix has a plain rgba fallback declared first for anything too old to 560 * know color-mix(). 561 * 562 * Override per include with . 563 */ 564.riley-widget { 565 --rw-surface: var(--mm-navbar-bg, var(--primary-color, #1F3B5B)); 566 --rw-text: var(--mm-navbar-text, #ffffff); 567 --rw-accent: var(--mm-accent, var(--secondary-color, #F2DFAD)); 568 --rw-accent-text: var(--mm-accent-text, #213547); 569 570 display: flex; flex-direction: column; 571 height: var(--riley-h, 480px); 572 background: var(--rw-surface); 573 color: var(--rw-text); 574 border: 1px solid rgba(255,255,255,.16); 575 border: 1px solid color-mix(in srgb, var(--rw-text) 16%, transparent); 576 border-radius: 14px; 577 box-shadow: 0 14px 38px rgba(0,0,0,.28); 578 overflow: hidden; font-size: 15px; line-height: 1.45; 579 font-family: var(--font-body, inherit); 580} 581/* Light variant: swap only the surface pair â the rest re-derives itself. */ 582.riley-widget.riley-light { 583 --rw-surface: var(--surface-bg, #ffffff); 584 --rw-text: var(--text-dark, #213547); 585 box-shadow: 0 10px 30px rgba(0,0,0,.10); 586} 587 588.riley-head { 589 display: flex; align-items: center; gap: 10px; 590 padding: 12px 14px; flex: 0 0 auto; 591 border-bottom: 1px solid rgba(255,255,255,.12); 592 border-bottom: 1px solid color-mix(in srgb, var(--rw-text) 12%, transparent); 593 background: rgba(0,0,0,.12); 594 background: color-mix(in srgb, var(--rw-surface) 88%, #000); 595} 596.riley-face { width: 38px; height: 38px; border-radius: 50%; object-fit: cover; flex: 0 0 auto; } 597.riley-who { display: flex; flex-direction: column; line-height: 1.2; min-width: 0; } 598.riley-who strong { font-size: 15px; color: var(--rw-text); } 599.riley-sub { 600 font-size: .875rem; 601 color: rgba(255,255,255,.70); 602 color: color-mix(in srgb, var(--rw-text) 70%, transparent); 603} 604.riley-mode { 605 margin-left: auto; display: inline-flex; align-items: center; gap: 6px; 606 border-radius: 999px; padding: 6px 12px; font-size: .875rem; font-weight: 700; 607 cursor: pointer; white-space: nowrap; 608 background: var(--rw-accent); color: var(--rw-accent-text); border: 0; 609} 610.riley-mode:hover { filter: brightness(1.06); } 611.riley-mode.is-live { background: #c0392b; color: #fff; } 612.riley-mode-ico { 613 display: inline-block; width: .5625rem; height: .5625rem; border-radius: 50%; 614 background: currentColor; flex: 0 0 auto; 615} 616.riley-mode.is-live .riley-mode-ico { animation: riley-pulse 1.3s ease-in-out infinite; } 617@keyframes riley-pulse { 0%,100% { opacity: 1 } 50% { opacity: .25 } } 618 619/* The log and the landing screen share this box; the landing sits on top. */ 620.riley-body { position: relative; flex: 1 1 auto; min-height: 0; } 621.riley-log { 622 position: absolute; inset: 0; overflow-y: auto; padding: 14px; 623 display: flex; flex-direction: column; gap: 10px; overscroll-behavior: contain; 624} 625.riley-msg { max-width: 88%; padding: 9px 12px; border-radius: 13px; white-space: pre-wrap; 626 word-wrap: break-word; } 627.riley-msg.riley-them { 628 background: rgba(255,255,255,.11); 629 background: color-mix(in srgb, var(--rw-text) 11%, transparent); 630 color: var(--rw-text); 631 border-bottom-left-radius: 4px; align-self: flex-start; 632} 633/* The visitor's own words wear the site's accent â on SPP that is the same 634 gold as the primary call to action. */ 635.riley-msg.riley-me { 636 background: var(--rw-accent); color: var(--rw-accent-text); 637 border-bottom-right-radius: 4px; align-self: flex-end; 638} 639.riley-msg.riley-note { 640 background: none; font-size: .875rem; font-style: italic; 641 color: rgba(255,255,255,.62); 642 color: color-mix(in srgb, var(--rw-text) 62%, transparent); 643 align-self: center; text-align: center; max-width: 100%; padding: 2px 0; 644} 645.riley-typing { display: inline-flex; gap: 4px; align-items: center; } 646.riley-typing i { 647 width: 6px; height: 6px; border-radius: 50%; 648 background: rgba(255,255,255,.65); 649 background: color-mix(in srgb, var(--rw-text) 65%, transparent); 650 animation: riley-bounce 1.1s infinite; 651} 652.riley-typing i:nth-child(2) { animation-delay: .15s } 653.riley-typing i:nth-child(3) { animation-delay: .3s } 654@keyframes riley-bounce { 0%,60%,100% { transform: translateY(0); opacity:.5 } 655 30% { transform: translateY(-4px); opacity:1 } } 656 657/* Everything here is sized to survive the shortest widget we ship (480px) 658 without the microphone disclosure being pushed under the footer. Keep the 659 total under roughly 370px if you change it. */ 660.riley-landing { 661 position: absolute; inset: 0; background: var(--rw-surface); z-index: 2; 662 display: flex; flex-direction: column; align-items: center; justify-content: center; 663 text-align: center; padding: 14px 20px; overflow-y: auto; 664} 665.riley-portrait { 666 width: 112px; height: 112px; border-radius: 50%; object-fit: cover; 667 box-shadow: 0 6px 22px rgba(0,0,0,.35); flex: 0 0 auto; 668 border: 2px solid rgba(255,255,255,.22); 669 border: 2px solid color-mix(in srgb, var(--rw-text) 22%, transparent); 670} 671.riley-landing-title { 672 font-size: 20px; font-weight: 700; margin: 11px 0 0; color: var(--rw-text); 673 font-family: var(--font-display, inherit); 674} 675.riley-landing-copy { 676 font-size: 14px; margin: 6px 0 0; max-width: 34ch; 677 color: rgba(255,255,255,.78); 678 color: color-mix(in srgb, var(--rw-text) 78%, transparent); 679} 680.riley-landing-cta { 681 margin-top: 13px; border: 0; border-radius: 999px; cursor: pointer; 682 background: var(--rw-accent); color: var(--rw-accent-text); 683 font-weight: 700; font-size: 15.5px; padding: 12px 28px; 684 box-shadow: 0 4px 16px rgba(0,0,0,.28); 685} 686.riley-landing-cta:hover { filter: brightness(1.06); } 687.riley-landing-alt { 688 margin-top: 9px; background: none; border: 0; cursor: pointer; 689 font-size: 13.5px; text-decoration: underline; padding: 3px; 690 color: rgba(255,255,255,.80); 691 color: color-mix(in srgb, var(--rw-text) 80%, transparent); 692} 693.riley-landing-note { 694 font-size: 11px; line-height: 1.35; margin: 11px 0 0; max-width: 40ch; 695 color: rgba(255,255,255,.62); 696 color: color-mix(in srgb, var(--rw-text) 62%, transparent); 697} 698.riley-widget.riley-engaged .riley-landing { display: none; } 699 700.riley-input { 701 display: flex; gap: 8px; padding: 10px; flex: 0 0 auto; 702 border-top: 1px solid rgba(255,255,255,.12); 703 border-top: 1px solid color-mix(in srgb, var(--rw-text) 12%, transparent); 704 background: rgba(0,0,0,.10); 705 background: color-mix(in srgb, var(--rw-surface) 92%, #000); 706} 707.riley-input input { 708 flex: 1 1 auto; border-radius: 999px; padding: 10px 14px; 709 font: inherit; font-size: 15px; min-width: 0; 710 color: var(--rw-text); 711 background: rgba(255,255,255,.10); 712 background: color-mix(in srgb, var(--rw-text) 10%, transparent); 713 border: 1px solid rgba(255,255,255,.22); 714 border: 1px solid color-mix(in srgb, var(--rw-text) 22%, transparent); 715} 716.riley-input input::placeholder { 717 color: rgba(255,255,255,.55); 718 color: color-mix(in srgb, var(--rw-text) 55%, transparent); 719} 720.riley-input input:focus { outline: 2px solid var(--rw-accent); outline-offset: -1px; } 721.riley-input button { 722 flex: 0 0 auto; width: 40px; height: 40px; border-radius: 50%; border: 0; 723 background: var(--rw-accent); color: var(--rw-accent-text); 724 font-size: 17px; font-weight: 700; cursor: pointer; 725} 726.riley-input button:disabled { opacity: .45; cursor: default; } 727.riley-foot { 728 margin: 0; padding: 8px 14px 11px; font-size: .875rem; flex: 0 0 auto; 729 color: rgba(255,255,255,.64); 730 color: color-mix(in srgb, var(--rw-text) 64%, transparent); 731 background: rgba(0,0,0,.10); 732 background: color-mix(in srgb, var(--rw-surface) 92%, #000); 733} 734 735/* Before engagement, and while speaking, the typing row is out of the way. */ 736.riley-widget:not(.riley-engaged) .riley-input, 737.riley-widget.riley-voice .riley-input { display: none; } 738.riley-widget:not(.riley-engaged) .riley-mode { visibility: hidden; } 739 740/* ââ floating launcher ââââââââââââââââââââââââââââââââââââââââââââââââ 741 Fixed bottom-right, above ordinary page furniture but below anything that 742 deliberately takes over the screen. The panel is a sibling of the button so 743 the button keeps its place while the panel opens above it. */ 744.riley-dock { 745 position: fixed; right: 20px; bottom: 20px; z-index: 2147483000; 746 display: flex; flex-direction: column; align-items: flex-end; gap: 10px; 747 font-family: var(--font-body, inherit); 748} 749.riley-launch { 750 display: inline-flex; align-items: center; gap: 10px; cursor: pointer; 751 border: 0; border-radius: 999px; padding: 8px 18px 8px 8px; 752 background: var(--mm-navbar-bg, var(--primary-color, #1F3B5B)); 753 color: var(--mm-navbar-text, #fff); 754 box-shadow: 0 8px 26px rgba(0,0,0,.30); 755 font-size: 15px; font-weight: 600; line-height: 1; 756 max-width: min(78vw, 320px); 757} 758.riley-launch img { border-radius: 50%; object-fit: cover; flex: 0 0 auto; } 759.riley-launch:hover { filter: brightness(1.08); } 760.riley-launch-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 761.riley-dock.is-open .riley-launch { display: none; } 762 763.riley-widget.riley-floating { 764 width: min(92vw, 400px); 765 box-shadow: 0 18px 48px rgba(0,0,0,.34); 766} 767.riley-close { 768 background: none; border: 0; cursor: pointer; font-size: 24px; line-height: 1; 769 padding: 0 2px; margin-left: 6px; 770 color: rgba(255,255,255,.75); 771 color: color-mix(in srgb, var(--rw-text) 75%, transparent); 772} 773.riley-close:hover { color: var(--rw-text); } 774 775@media (max-width: 575.98px) { 776 /* On a phone the panel takes the width it needs; the launcher shrinks to 777 the avatar so it never straddles the screen. */ 778 .riley-dock { right: 12px; bottom: 12px; left: 12px; align-items: flex-end; } 779 .riley-widget.riley-floating { width: 100%; } 780 .riley-launch-label { display: none; } 781 .riley-launch { padding: 8px; } 782} 783@media (max-width: 575.98px) { 784 .riley-widget { height: min(70vh, 480px); font-size: 14.5px; } 785 .riley-portrait { width: 104px; height: 104px; } 786 .riley-landing-title { font-size: 19px; margin-top: 10px; } 787 .riley-landing-note { margin-top: 10px; } 788} 789@media (prefers-reduced-motion: reduce) { 790 .riley-typing i, .riley-mode.is-live .riley-mode-ico { animation: none; } 791} 792</style> 793 794
795<script> 796(function () { 797 "use strict"; 798 var root = document.getElementById('rileyWidget'); 799 if (!root || root.dataset.booted) { return; } 800 root.dataset.booted = '1'; 801 802 var logEl = document.getElementById('rileyLog'); 803 var formEl = document.getElementById('rileyForm'); 804 var textEl = document.getElementById('rileyText'); 805 var sendEl = document.getElementById('rileySend'); 806 var modeBtn = document.getElementById('rileyModeBtn'); 807 var modeLbl = document.getElementById('rileyModeLbl'); 808 var statusEl = document.getElementById('rileyStatus'); 809 var talkCta = document.getElementById('rileyLandingTalk'); 810 var typeCta = document.getElementById('rileyLandingType'); 811 var hasLanding = !!document.getElementById('rileyLanding'); 812 var dock = document.getElementById('rileyDock'); 813 var launchBtn = document.getElementById('rileyLaunch'); 814 var closeBtn = document.getElementById('rileyClose'); 815 var floating = !!dock; 816 817 // Carried across page loads so the thread survives navigation exactly, 818 // rather than relying on the server's two-hour resume window. 819 var STORE_CONV = 'rileyConvId', STORE_OPEN = 'rileyOpen'; 820 function remember(k, v) { try { sessionStorage.setItem(k, v); } catch (e) {} } 821 function recall(k) { try { return sessionStorage.getItem(k); } catch (e) { return null; } } 822 823 var convId = recall('rileyConvId') || null; 824 var busy = false; 825 var voice = null; // live voice session, or null 826 var connecting = false; // mint + mic permission in flight 827 var voiceAttempt = 0; // generation token; invalidates every older in-flight attempt 828 var started = false; // session fetched 829 var engaged = !hasLanding; // with no landing screen the chat is live at once 830 var pending = null; // session payload (held back only if there IS a landing) 831 var freshVisit = false; // greeting painted this visit but not yet stored 832 833 // ---- rendering ---------------------------------------------------------- 834 835 function bubble(who, text) { 836 var el = document.createElement('div'); 837 el.className = 'riley-msg riley-' + who; 838 el.textContent = text; 839 logEl.appendChild(el); 840 logEl.scrollTop = logEl.scrollHeight; 841 return el; 842 } 843 844 function note(text) { return bubble('note', text); } 845 846 function typing() { 847 var el = document.createElement('div'); 848 el.className = 'riley-msg riley-them'; 849 el.innerHTML = '<span class="riley-typing"><i></i><i></i><i></i></span>'; 850 logEl.appendChild(el); 851 logEl.scrollTop = logEl.scrollHeight; 852 return el; 853 } 854 855 function vEvent(what) { 856 // Voice outcomes, so a session that yields no transcript can be told 857 // apart from one the visitor never really started. 858 if (!convId) { return; } 859 post('/riley/api/log', { conv_id: convId, role: 'event', content: what }); 860 } 861 862 function post(path, body) { 863 return fetch(path, { 864 method: 'POST', 865 headers: { 'Content-Type': 'application/json' }, 866 credentials: 'same-origin', 867 body: JSON.stringify(body || {}) 868 }).then(function (r) { return r.json().catch(function () { return {}; }); }); 869 } 870 871 // ---- opening ------------------------------------------------------------ 872 // 873 // By default Riley says hello as soon as the panel is on screen and the text 874 // box is ready to type in. The earlier design held everything behind a 875 // "Talk with Riley" button and nobody pressed it. 876 // 877 // A return visit still opens on a clean greeting rather than a wall of last 878 // week's transcript â she carries the history in her own context, so she 879 // answers as though she remembers, which she does. 880 881 // `carryOver` is true when the panel was ALREADY open and the visitor simply 882 // moved to another page. Wiping the transcript there would be wrong â they 883 // are mid-conversation and following it around the site is the whole point 884 // of the launcher. A genuinely new visit still opens on a clean greeting. 885 var carryOver = false; 886 887 function paintOpening(d) { 888 var history = d.history || []; 889 if (carryOver && history.length) {
890 history.forEach(function (m) { 891 bubble(m.role === 'user' ? 'me' : 'them', m.content); 892 }); 893 freshVisit = false; 894 return; 895 } 896 if (d.opening) { bubble('them', d.opening); } 897 if (history.length) { 898 note('We have spoken before â I still have our conversation.'); 899 } 900 freshVisit = true; 901 } 902 903 function boot() { 904 if (started) { return; } 905 started = true; 906 post('/riley/api/session', { page_url: location.href, conv_id: convId }) 907 .then(function (d) { 908 if (d.error) { 909 pending = null; 910 if (!hasLanding) { note('Riley is offline just now.'); } 911 return; 912 } 913 convId = d.conv_id; 914 remember(STORE_CONV, convId); 915 pending = d; 916 if (!hasLanding) { paintOpening(d); } 917 }) 918 .catch(function () { 919 pending = null; 920 if (!hasLanding) { note('Riley is offline just now.'); } 921 }); 922 } 923 924 function engage(mode) { 925 if (engaged) { return; } 926 engaged = true; 927 root.classList.add('riley-engaged'); 928 929 var d = pending || {}; 930 var history = d.history || []; 931 if (history.length) { 932 note('Carrying on from last time.');
933 history.forEach(function (m) { 934 bubble(m.role === 'user' ? 'me' : 'them', m.content); 935 }); 936 } else if (d.opening) { 937 bubble('them', d.opening); 938 } else if (!pending) { 939 note('Riley is offline just now.'); 940 return; 941 } 942 943 if (mode === 'voice') { startVoice(); } 944 else { textEl.focus(); } 945 } 946 947 if (talkCta) { talkCta.addEventListener('click', function () { engage('voice'); }); } 948 if (typeCta) { typeCta.addEventListener('click', function () { engage('text'); }); } 949 950 // ---- text lane ---------------------------------------------------------- 951 952 formEl.addEventListener('submit', function (e) { 953 e.preventDefault(); 954 var msg = (textEl.value || '').trim(); 955 if (!msg || busy) { return; } 956 textEl.value = ''; 957 bubble('me', msg); 958 busy = true; sendEl.disabled = true; 959 var dots = typing(); 960 post('/riley/api/text', { message: msg, conv_id: convId, 961 page_url: location.href, fresh_visit: freshVisit }) 962 .then(function (d) { 963 dots.remove(); 964 if (d.conv_id) { convId = d.conv_id; remember(STORE_CONV, convId); } 965 bubble('them', d.reply || "Sorry â I didn't catch that. Try again?"); 966 }) 967 .catch(function () { 968 dots.remove(); 969 bubble('them', 'Sorry, something went wrong on my end. Try that once more?'); 970 }) 971 .finally(function () { 972 busy = false; sendEl.disabled = false; freshVisit = false; textEl.focus(); 973 }); 974 }); 975 976 // ---- voice lane: GPT-Live 1 / Gleam -------------------------------------- 977 // WebRTC owns continuous duplex audio. Server sideband owns tools, persisted 978 // transcripts and billing; this page forwards only observed function IDs. 979 function setVoiceUI(on, label) { 980 root.classList.toggle('riley-voice', on); 981 modeBtn.classList.toggle('is-live', on); 982 modeLbl.textContent = on ? 'Stop' : 'Talk'; 983 modeBtn.title = on ? 'Switch back to typing' : 'Switch to voice'; 984 modeBtn.setAttribute('aria-label', on ? 'Stop voice chat' : 'Talk with Riley'); 985 statusEl.innerHTML = label || (on ? 'Listening…' : 'Support · AI agent'); 986 } 987 988 function closeVoiceParts(parts, immediately) { 989 if (!parts || parts.disposed) return; 990 parts.closing = true; 991 try { parts.mic && parts.mic.getTracks().forEach(function(t){t.stop();}); } catch(e) {} 992 try { parts.audio && parts.audio.pause(); if(parts.audio) parts.audio.srcObject=null; } catch(e) {} 993 if (parts.timer) clearTimeout(parts.timer); 994 if (parts.meter) clearInterval(parts.meter); 995 try { parts.audioContext && parts.audioContext.close(); } catch(e) {} 996 function dispose() { 997 if(parts.disposed) return; 998 parts.disposed=true; 999 clearTimeout(parts.closeTimer);clearTimeout(parts.connectTimer); 1000 try { parts.dc && parts.dc.close(); } catch(e) {} 1001 try { parts.pc && parts.pc.close(); } catch(e) {} 1002 } 1003 parts.dispose=dispose; 1004 if (parts.liveId) { 1005 post('/riley/api/live-close',{conv_id:convId,live_id:parts.liveId}).catch(function(){}); 1006 } 1007 if (!immediately && parts.dc && parts.dc.readyState==='open') { 1008 try { parts.dc.send(JSON.stringify({type:'session.close'})); } catch(e) {} 1009 parts.closeTimer=setTimeout(dispose,15000); 1010 } else dispose(); 1011 } 1012 1013 function stopVoice(silent) { 1014 ++voiceAttempt; connecting=false; 1015 var old=voice;voice=null; 1016 closeVoiceParts(old); 1017 setVoiceUI(false); 1018 if(old && !silent) note('Back to typing â Riley still has the conversation.'); 1019 textEl.focus(); 1020 } 1021 1022 function caption(parts, role, msg) { 1023 if(!msg.delta) return; 1024 var entry=parts.captions[role]; 1025 if(entry && msg.start_ms!=null && entry.end!=null && msg.start_ms-entry.end>900) entry=null; 1026 if(!entry) { 1027 entry={element:bubble(role==='user'?'me':'them',''),text:'',end:null}; 1028 parts.captions[role]=entry; 1029 } 1030 entry.text+=msg.delta;entry.end=msg.end_ms; 1031 entry.element.textContent=entry.text; 1032 logEl.scrollTop=logEl.scrollHeight; 1033 } 1034 1035 function handleEvent(ev,parts) { 1036 var msg;try {msg=JSON.parse(ev.data);}catch(e){return;} 1037 if(msg.type==='session.closed') { 1038 if(parts.dispose) parts.dispose(); 1039 else if(voice===parts) stopVoice(true); 1040 return; 1041 } 1042 if(parts.closing || voice!==parts || parts.attempt!==voiceAttempt) return; 1043 if(msg.type==='session.started') { 1044 clearTimeout(parts.connectTimer);parts.ready=true;connecting=false; 1045 setVoiceUI(true,'Listening…');vEvent('voice_connected:gpt-live-1:gleam'); 1046 } else if(msg.type==='session.input_transcript.delta') { 1047 caption(parts,'user',msg); 1048 } else if(msg.type==='session.output_transcript.delta') { 1049 caption(parts,'assistant',msg); 1050 } else if(msg.type==='response.event') { 1051 var event=msg.event||{}, item=event.item||{}; 1052 if(event.type==='response.output_item.done' && item.type==='function_call' && !parts.calls[item.call_id]) { 1053 parts.calls[item.call_id]=true; 1054 function forward(retry) { 1055 return post('/riley/api/live-tool',{conv_id:convId,live_id:parts.liveId,call_id:item.call_id}) 1056 .then(function(d){ 1057 if(!d || !d.ok) { 1058 if(retry && !parts.closing) return new Promise(function(resolve){setTimeout(resolve,500);}).then(function(){return forward(false);}); 1059 throw new Error('tool not accepted'); 1060 } 1061 }); 1062 }
1063 forward(true).catch(function(){ 1064 if(voice===parts && !parts.closing) {note('Voice lost its support connection. Please try again or continue typing.');stopVoice(true);} 1065 }); 1066 } 1067 } else if(msg.type==='error') { 1068 vEvent('voice_failed:live_command');note('Voice encountered a connection problem. Typing still works.');stopVoice(true); 1069 } 1070 } 1071 1072 function monitorPlayback(parts,stream) { 1073 try { 1074 var Ctx=window.AudioContext||window.webkitAudioContext; 1075 var ctx=parts.audioContext=new Ctx();ctx.resume(); 1076 var source=ctx.createMediaStreamSource(stream),analyser=ctx.createAnalyser(); 1077 analyser.fftSize=256;source.connect(analyser);var samples=new Float32Array(256); 1078 parts.meter=setInterval(function(){ 1079 if(voice!==parts || !parts.ready || parts.closing) return; 1080 analyser.getFloatTimeDomainData(samples);var energy=0; 1081 for(var i=0;i<samples.length;i++) energy+=samples[i]*samples[i]; 1082 setVoiceUI(true,Math.sqrt(energy/samples.length)>.007 ? 'Riley is speaking…' : 'Listening…'); 1083 },150); 1084 } catch(e) {} // Voice itself does not depend on the level indicator. 1085 } 1086 1087 function gatherIce(pc) { 1088 if(pc.iceGatheringState==='complete') return Promise.resolve(); 1089 return new Promise(function(resolve,reject){ 1090 var timer=setTimeout(function(){pc.removeEventListener('icegatheringstatechange',check);reject(new Error('ICE timeout'));},10000); 1091 function check(){if(pc.iceGatheringState==='complete'){clearTimeout(timer);pc.removeEventListener('icegatheringstatechange',check);resolve();}} 1092 pc.addEventListener('icegatheringstatechange',check);check(); 1093 }); 1094 } 1095 1096 function startVoice() { 1097 if(voice || connecting) {stopVoice();return;} 1098 if(!navigator.mediaDevices || !window.RTCPeerConnection) {note("This browser can't do voice â typing works fine though.");return;} 1099 connecting=true;var attempt=++voiceAttempt,parts=null; 1100 setVoiceUI(true,'Connecting…'); 1101 navigator.mediaDevices.getUserMedia({audio:{channelCount:1,echoCancellation:true,noiseSuppression:true}}) 1102 .then(function(mic){ 1103 if(attempt!==voiceAttempt){mic.getTracks().forEach(function(t){t.stop();});throw new Error('aborted');} 1104 var pc=new RTCPeerConnection(),audio=new Audio();audio.autoplay=true;audio.setAttribute('playsinline',''); 1105 parts={pc:pc,audio:audio,mic:mic,dc:null,attempt:attempt,captions:{},calls:{},ready:false};voice=parts; 1106 pc.ontrack=function(e){ 1107 if(parts.closing)return; 1108 var stream=e.streams[0]||new MediaStream([e.track]);audio.srcObject=stream; 1109 audio.play().catch(function(){if(voice===parts)note('Your browser blocked audio playback. Stop voice and try Talk again.');}); 1110 monitorPlayback(parts,stream); 1111 }; 1112 pc.onconnectionstatechange=function(){ 1113 if(!parts.closing && voice===parts && pc.connectionState==='failed') {note('Voice disconnected. You can continue typing.');stopVoice(true);} 1114 }; 1115 mic.getTracks().forEach(function(t){pc.addTrack(t,mic);}); 1116 var dc=parts.dc=pc.createDataChannel('oai-events'); 1117 dc.onmessage=function(e){handleEvent(e,parts);}; 1118 dc.onclose=function(){if(!parts.closing && voice===parts){note('Voice conversation ended.');stopVoice(true);}}; 1119 parts.connectTimer=setTimeout(function(){if(voice===parts && !parts.ready){note('Voice could not connect. Please try again.');stopVoice(true);}},45000); 1120 return pc.createOffer().then(function(offer){return pc.setLocalDescription(offer);}) 1121 .then(function(){return gatherIce(pc);}) 1122 .then(function(){ 1123 if(attempt!==voiceAttempt || parts.closing)throw new Error('aborted'); 1124 return post('/riley/api/live-session',{conv_id:convId,page_url:location.href,sdp:pc.localDescription.sdp}); 1125 }).then(function(d){ 1126 if(!d || !d.sdp || !d.live_id)throw new Error((d&&d.detail)||'no voice session'); 1127 parts.liveId=d.live_id; 1128 if(attempt!==voiceAttempt || parts.closing) { 1129 post('/riley/api/live-close',{conv_id:d.conv_id||convId,live_id:d.live_id}).catch(function(){}); 1130 throw new Error('aborted'); 1131 } 1132 convId=d.conv_id||convId;remember(STORE_CONV,convId); 1133 parts.timer=setTimeout(function(){if(voice===parts){note('That is the limit for one voice session â carry on by typing.');stopVoice(true);}},((d.max_seconds||600)-10)*1000); 1134 return pc.setRemoteDescription({type:'answer',sdp:d.sdp}); 1135 }); 1136 }).catch(function(err){ 1137 closeVoiceParts(parts,true); 1138 if(voice===parts)voice=null; 1139 if(attempt!==voiceAttempt || (err&&err.message==='aborted'))return; 1140 connecting=false;setVoiceUI(false); 1141 if(err&&err.name==='NotAllowedError'){vEvent('voice_failed:mic_denied');note('No microphone access â happy to keep going by text.');} 1142 else {vEvent('voice_failed:live_start');note("Couldn't start voice just now. Typing still works.");}
1143 textEl.focus(); 1144 }); 1145 } 1146 1147 modeBtn.addEventListener('click',startVoice); 1148 window.addEventListener('beforeunload',function(){ 1149 if(voice && voice.liveId && navigator.sendBeacon)navigator.sendBeacon('/riley/api/live-close',new Blob([JSON.stringify({conv_id:convId,live_id:voice.liveId})],{type:'application/json'})); 1150 stopVoice(true); 1151 }); 1152 1153 // ---- floating launcher -------------------------------------------------- 1154 // 1155 // The panel is on every page now, so it must NOT open a conversation just 1156 // because a page loaded â that would create a row per pageview across the 1157 // whole site and drown the very numbers we are trying to read. A 1158 // conversation begins when somebody actually opens the chat. 1159 1160 function openPanel() { 1161 if (!floating) { return; } 1162 dock.classList.add('is-open'); 1163 root.hidden = false; 1164 launchBtn.setAttribute('aria-expanded', 'true'); 1165 remember(STORE_OPEN, '1'); 1166 boot(); 1167 if (engaged) { setTimeout(function () { textEl.focus(); }, 60); } 1168 } 1169 1170 function closePanel() { 1171 if (!floating) { return; } 1172 stopVoice(true); 1173 dock.classList.remove('is-open'); 1174 root.hidden = true; 1175 launchBtn.setAttribute('aria-expanded', 'false'); 1176 remember(STORE_OPEN, '0'); 1177 launchBtn.focus(); 1178 } 1179 1180 if (floating) { 1181 launchBtn.addEventListener('click', openPanel); 1182 if (closeBtn) { closeBtn.addEventListener('click', closePanel); } 1183 document.addEventListener('keydown', function (e) { 1184 if (e.key === 'Escape' && !root.hidden) { closePanel(); } 1185 }); 1186 // Stay open as they move around the site, transcript and all. 1187 if (recall(STORE_OPEN) === '1') { carryOver = true; openPanel(); } 1188 } 1189 1190 // Boot on first sight â inline mode only. Floating mode boots on open. 1191 if (floating) { 1192 /* handled by openPanel */ 1193 } else if (root.offsetParent !== null) { 1194 boot(); 1195 } else { 1196 var io = new IntersectionObserver(function (entries) { 1197 entries.forEach(function (en) { 1198 if (en.isIntersecting) { boot(); io.disconnect(); } 1199 }); 1200 }); 1201 io.observe(root); 1202 document.addEventListener('click', function once() { 1203 if (root.offsetParent !== null) { boot(); document.removeEventListener('click', once); } 1204 }); 1205 } 1206})(); 1207</script>
1207 1208 1209</body> 1210</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.