Spaces:
Running
Running
| <!-- SILENTPATTERN FINAL BUILD: 2025-12-15 | pages: 10 | features: hash-deep-linking, lab-navigator, access-modal, form-validation, chat-console, export-transcript --> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1.0" /> | |
| <title>SILENTPATTERN — Console</title> | |
| <!-- Tailwind --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Three.js + Vanta (pinned) --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vanta.net.min.js"></script> | |
| <!-- Icons + Font --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"> | |
| <style> | |
| body { font-family: 'Inter', sans-serif; } | |
| .gradient-text { | |
| background: linear-gradient(90deg, #6366f1, #8b5cf6, #ec4899); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| color: transparent; | |
| } | |
| .conscious-element { transition: all 0.3s ease; } | |
| .conscious-element:hover { transform: scale(1.02); } | |
| .chat-container { | |
| height: 520px; | |
| overflow-y: auto; | |
| scrollbar-width: thin; | |
| scrollbar-color: #4f46e5 #1e1b4b; | |
| } | |
| .chat-container::-webkit-scrollbar { width: 6px; } | |
| .chat-container::-webkit-scrollbar-track { background: #1e1b4b; } | |
| .chat-container::-webkit-scrollbar-thumb { background-color: #4f46e5; border-radius: 3px; } | |
| /* Typing indicator that actually animates (no content-keyframes) */ | |
| .typing-indicator { display: inline-flex; align-items: center; gap: 6px; } | |
| .typing-dots { display: inline-flex; gap: 3px; } | |
| .typing-dots span { | |
| width: 4px; height: 4px; | |
| border-radius: 999px; | |
| background: rgba(148,163,184,0.9); | |
| opacity: 0.35; | |
| animation: dotPulse 1.2s infinite; | |
| } | |
| .typing-dots span:nth-child(2) { animation-delay: 0.15s; } | |
| .typing-dots span:nth-child(3) { animation-delay: 0.30s; } | |
| @keyframes dotPulse { | |
| 0%, 100% { opacity: 0.25; transform: translateY(0); } | |
| 50% { opacity: 0.95; transform: translateY(-1px); } | |
| } | |
| .modal { transition: opacity 0.3s ease, transform 0.3s ease; } | |
| .modal-hidden { opacity: 0; transform: translateY(20px); pointer-events: none; } | |
| .modal-visible { opacity: 1; transform: translateY(0); } | |
| .aura { | |
| background: | |
| radial-gradient(circle at 25% 15%, rgba(99,102,241,0.20), transparent 42%), | |
| radial-gradient(circle at 70% 70%, rgba(236,72,153,0.12), transparent 46%), | |
| radial-gradient(circle at 50% 45%, rgba(139,92,246,0.10), transparent 55%); | |
| } | |
| .focus-ring:focus { outline: none; box-shadow: 0 0 0 2px rgba(99,102,241,0.65); } | |
| /* Active states */ | |
| .lab-node.active { | |
| border-color: rgba(99,102,241,0.55) ; | |
| box-shadow: 0 0 0 1px rgba(99,102,241,0.22), 0 0 28px rgba(99,102,241,0.08); | |
| transform: translateY(-1px); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-black text-white overflow-x-hidden"> | |
| <!-- Animated background --> | |
| <div id="vanta-bg" class="fixed top-0 left-0 w-full h-full z-0"></div> | |
| <!-- Top bar --> | |
| <nav class="relative z-10 py-6 px-8 flex justify-between items-center backdrop-blur-sm"> | |
| <a href="index.html" class="flex items-center space-x-2"> | |
| <div class="w-8 h-8 rounded-full bg-indigo-600 flex items-center justify-center"> | |
| <div class="w-2 h-2 rounded-full bg-white animate-pulse"></div> | |
| </div> | |
| <span class="text-xl font-semibold">SILENTPATTERN</span> | |
| </a> | |
| <div class="flex items-center space-x-3"> | |
| <button id="lab-nav-btn" | |
| class="w-10 h-10 rounded-full border border-indigo-500/40 bg-gray-900/20 hover:bg-gray-900/40 backdrop-blur-sm transition flex items-center justify-center focus-ring" | |
| aria-label="Open Lab Navigator" title="Lab Navigator" | |
| aria-controls="lab-navigator" aria-haspopup="dialog"> | |
| <i class="fas fa-asterisk text-indigo-300 text-sm"></i> | |
| </button> | |
| <button id="access-btn" | |
| class="px-6 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 rounded-full hover:opacity-90 transition focus-ring" | |
| aria-controls="access-modal" aria-haspopup="dialog"> | |
| Access | |
| </button> | |
| </div> | |
| </nav> | |
| <!-- Console --> | |
| <section class="relative z-10 px-6 py-16"> | |
| <div class="max-w-5xl mx-auto"> | |
| <div class="text-center mb-10"> | |
| <div class="inline-flex items-center space-x-3 px-4 py-2 rounded-full border border-gray-800 bg-gray-900/20 backdrop-blur-sm"> | |
| <span class="w-2 h-2 rounded-full bg-indigo-400 animate-pulse"></span> | |
| <span class="text-xs text-gray-300 tracking-widest uppercase">Console</span> | |
| <span class="text-xs text-gray-500">Interactive session</span> | |
| </div> | |
| <h1 class="mt-6 text-3xl md:text-5xl font-bold"> | |
| <span class="gradient-text">Conversation</span> Interface | |
| </h1> | |
| <p class="mt-3 text-gray-300 max-w-2xl mx-auto"> | |
| A controlled channel into SILENTPATTERN's systems. Minimal surface; auditable outputs. | |
| </p> | |
| </div> | |
| <div class="relative rounded-2xl border border-gray-800 bg-gray-900/30 overflow-hidden aura"> | |
| <div class="bg-gray-800/40 px-6 py-4 border-b border-gray-800 flex items-center justify-between"> | |
| <div class="flex items-center"> | |
| <div class="w-3 h-3 rounded-full bg-red-500 mr-2"></div> | |
| <div class="w-3 h-3 rounded-full bg-yellow-500 mr-2"></div> | |
| <div class="w-3 h-3 rounded-full bg-green-500 mr-4"></div> | |
| <div class="flex items-center"> | |
| <div class="w-6 h-6 rounded-full bg-indigo-600 flex items-center justify-center mr-3"> | |
| <div class="w-1.5 h-1.5 rounded-full bg-white animate-pulse"></div> | |
| </div> | |
| <div> | |
| <div class="text-sm font-medium">SILENTPATTERN Interface</div> | |
| <div class="text-xs text-gray-500">Session: local</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="text-xs px-2.5 py-1 rounded-full border border-indigo-500/30 text-indigo-200 bg-indigo-900/15"> | |
| DRAFT | |
| </div> | |
| </div> | |
| <div id="chat-messages" class="chat-container p-6 space-y-4" aria-live="polite" aria-label="Chat messages"> | |
| <div class="flex items-start" data-seed="system"> | |
| <div class="w-8 h-8 rounded-full bg-indigo-600 flex-shrink-0 flex items-center justify-center mr-3"> | |
| <i class="fas fa-robot text-white text-sm"></i> | |
| </div> | |
| <div class="bg-gray-800/70 rounded-lg p-4 max-w-[85%]"> | |
| <p class="text-gray-100" id="seed-system-text"> | |
| Acknowledged. This console is a controlled interface. State your objective; I will respond with constraints, assumptions, and next steps. | |
| </p> | |
| <p class="text-gray-400 text-xs mt-2">Note: do not paste secrets or API keys into chat.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="px-6 py-4 border-t border-gray-800 bg-black/10"> | |
| <form id="chat-form" class="flex items-center" autocomplete="off" aria-label="Send message"> | |
| <input id="chat-input" type="text" | |
| placeholder="Describe a task (e.g., 'Draft a research note for MCAP')..." | |
| class="flex-1 bg-gray-800/50 border border-gray-700 rounded-l-xl px-4 py-3 focus-ring" | |
| aria-label="Type your message" /> | |
| <button id="send-btn" type="submit" | |
| class="bg-indigo-600 hover:bg-indigo-700 px-6 py-3 rounded-r-xl transition disabled:opacity-60 disabled:cursor-not-allowed focus-ring" | |
| aria-label="Send"> | |
| <i class="fas fa-paper-plane"></i> | |
| </button> | |
| </form> | |
| <div class="mt-2 flex justify-between items-center text-sm text-gray-500"> | |
| <div class="flex items-center gap-3"> | |
| <button type="button" id="clear-btn" class="hover:text-indigo-400 transition focus-ring" aria-label="Clear session"> | |
| <i class="fas fa-broom mr-1"></i> Clear | |
| </button> | |
| <button type="button" id="export-btn" class="hover:text-indigo-400 transition focus-ring" aria-label="Export transcript"> | |
| <i class="fas fa-file-arrow-down mr-1"></i> Export | |
| </button> | |
| </div> | |
| <div> | |
| <span id="typing-indicator" class="hidden"> | |
| <span class="typing-indicator"> | |
| <span>System is typing</span> | |
| <span class="typing-dots" aria-hidden="true"><span></span><span></span><span></span></span> | |
| </span> | |
| </span> | |
| </div> | |
| </div> | |
| <div class="mt-3 text-xs text-gray-600"> | |
| Integration note: do not call OpenAI directly from the browser. Use a server endpoint (example: | |
| <span class="text-gray-400">/api/chat</span>) to keep keys private. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <footer class="relative z-10 px-6 pb-10"> | |
| <div class="max-w-5xl mx-auto border-t border-gray-800/60 pt-8 flex flex-col md:flex-row justify-between items-center gap-4"> | |
| <div class="text-sm text-gray-500">© 2025 SILENTPATTERN. All rights reserved.</div> | |
| <div class="text-sm text-gray-500 flex gap-6"> | |
| <a href="research.html" class="hover:text-indigo-400 transition">Research</a> | |
| <a href="privacy.html" class="hover:text-indigo-400 transition">Privacy</a> | |
| <a href="terms.html" class="hover:text-indigo-400 transition">Terms</a> | |
| <a href="contact.html" class="hover:text-indigo-400 transition">Contact</a> | |
| </div> | |
| </div> | |
| </footer> | |
| <!-- ACCESS MODAL --> | |
| <div id="access-modal" | |
| class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm modal modal-hidden" | |
| role="dialog" aria-modal="true" aria-labelledby="access-modal-title" tabindex="-1"> | |
| <div class="bg-gray-900/90 border border-gray-800 rounded-xl max-w-md w-full mx-4 relative overflow-hidden"> | |
| <div class="absolute inset-x-0 top-0 h-1 bg-gradient-to-r from-indigo-600 to-purple-600"></div> | |
| <div class="p-6"> | |
| <div class="flex justify-between items-start mb-6"> | |
| <div> | |
| <h3 class="text-xl font-bold" id="access-modal-title">Request Access</h3> | |
| <p class="text-gray-400 mt-1">Limited availability for qualified researchers</p> | |
| </div> | |
| <button id="close-access-modal" class="text-gray-400 hover:text-white focus-ring" aria-label="Close"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <!-- Inline feedback (replaces alerts) --> | |
| <div id="access-feedback" | |
| class="hidden mb-4 rounded-lg border border-gray-800 bg-black/25 px-4 py-3 text-sm" | |
| role="status" aria-live="polite"></div> | |
| <form id="access-form" class="space-y-4" novalidate> | |
| <div> | |
| <label for="name" class="block text-sm font-medium mb-1">Full Name</label> | |
| <input type="text" id="name" class="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-4 py-2 focus-ring" autocomplete="name"> | |
| <p id="name-error" class="hidden mt-1 text-xs text-red-300">Please enter your full name.</p> | |
| </div> | |
| <div> | |
| <label for="email" class="block text-sm font-medium mb-1">Email</label> | |
| <input type="email" id="email" class="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-4 py-2 focus-ring" autocomplete="email"> | |
| <p id="email-error" class="hidden mt-1 text-xs text-red-300">Please enter a valid email address.</p> | |
| </div> | |
| <div> | |
| <label for="institution" class="block text-sm font-medium mb-1">Institution/Organization</label> | |
| <input type="text" id="institution" class="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-4 py-2 focus-ring" autocomplete="organization"> | |
| <p id="institution-error" class="hidden mt-1 text-xs text-red-300">Please enter your institution/organization.</p> | |
| </div> | |
| <div> | |
| <label for="purpose" class="block text-sm font-medium mb-1">Purpose of Access</label> | |
| <select id="purpose" class="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-4 py-2 focus-ring"> | |
| <option value="">Select a purpose</option> | |
| <option value="research">Academic Research</option> | |
| <option value="development">AI Development</option> | |
| <option value="policy">Policy Research</option> | |
| <option value="partnership">Partnership</option> | |
| <option value="other">Other</option> | |
| </select> | |
| <p id="purpose-error" class="hidden mt-1 text-xs text-red-300">Please select a purpose.</p> | |
| </div> | |
| <div class="pt-2"> | |
| <button type="submit" | |
| class="w-full py-3 bg-gradient-to-r from-indigo-600 to-purple-600 rounded-lg hover:opacity-90 transition focus-ring"> | |
| Submit Request | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- LAB NAVIGATOR --> | |
| <div id="lab-navigator" | |
| class="fixed inset-0 z-[60] bg-black/80 backdrop-blur-md modal modal-hidden" | |
| role="dialog" aria-modal="true" aria-label="Lab Navigator" tabindex="-1"> | |
| <div class="absolute inset-0" data-lab-close="true"></div> | |
| <div class="relative w-full h-full flex items-center justify-center p-6"> | |
| <div class="w-full max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
| <div class="relative rounded-2xl border border-gray-800 bg-gray-900/20 overflow-hidden"> | |
| <div class="flex items-center justify-between px-5 py-4 border-b border-gray-800/60"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="w-7 h-7 rounded-full bg-indigo-600 flex items-center justify-center"> | |
| <div class="w-1.5 h-1.5 rounded-full bg-white animate-pulse"></div> | |
| </div> | |
| <div> | |
| <div class="text-sm text-gray-300 tracking-wide">SILENTPATTERN</div> | |
| <div class="text-xs text-gray-500">Lab Navigator</div> | |
| </div> | |
| </div> | |
| <button id="lab-nav-close" | |
| class="w-9 h-9 rounded-full border border-gray-800 bg-gray-900/30 hover:bg-gray-900/50 transition flex items-center justify-center focus-ring" | |
| aria-label="Close Lab Navigator"> | |
| <i class="fas fa-times text-gray-300 text-sm"></i> | |
| </button> | |
| </div> | |
| <div class="relative p-6 min-h-[420px]"> | |
| <div class="absolute inset-0 opacity-70 pointer-events-none" | |
| style="background: radial-gradient(circle at 30% 20%, rgba(99,102,241,0.18), transparent 45%), | |
| radial-gradient(circle at 70% 70%, rgba(236,72,153,0.10), transparent 50%);"></div> | |
| <div class="relative grid grid-cols-1 sm:grid-cols-2 gap-3"> | |
| <button class="lab-node text-left rounded-xl border border-gray-800 bg-gray-900/30 hover:border-indigo-500/50 hover:bg-gray-900/45 transition p-4 focus-ring" | |
| data-dossier="start" aria-current="false"> | |
| <div class="text-sm text-gray-200 font-medium">Start Here</div> | |
| <div class="text-xs text-gray-500 mt-1">Return to the main interface</div> | |
| </button> | |
| <button class="lab-node text-left rounded-xl border border-gray-800 bg-gray-900/30 hover:border-indigo-500/50 hover:bg-gray-900/45 transition p-4 focus-ring" | |
| data-dossier="console" aria-current="false"> | |
| <div class="text-sm text-gray-200 font-medium">Console</div> | |
| <div class="text-xs text-gray-500 mt-1">This page: controlled chat</div> | |
| </button> | |
| <button class="lab-node text-left rounded-xl border border-gray-800 bg-gray-900/30 hover:border-indigo-500/50 hover:bg-gray-900/45 transition p-4 focus-ring" | |
| data-dossier="programs" aria-current="false"> | |
| <div class="text-sm text-gray-200 font-medium">Programs</div> | |
| <div class="text-xs text-gray-500 mt-1">MCAP · CHAI · Quantum Lambda</div> | |
| </button> | |
| <button class="lab-node text-left rounded-xl border border-gray-800 bg-gray-900/30 hover:border-indigo-500/50 hover:bg-gray-900/45 transition p-4 focus-ring" | |
| data-dossier="ai_scientist" aria-current="false"> | |
| <div class="text-sm text-gray-200 font-medium">AI Scientist</div> | |
| <div class="text-xs text-gray-500 mt-1">Hypothesis → experiment → report</div> | |
| </button> | |
| <button class="lab-node text-left rounded-xl border border-gray-800 bg-gray-900/30 hover:border-indigo-500/50 hover:bg-gray-900/45 transition p-4 focus-ring sm:col-span-2" | |
| data-dossier="access" aria-current="false"> | |
| <div class="text-sm text-gray-200 font-medium">Access</div> | |
| <div class="text-xs text-gray-500 mt-1">Request access to demos and research</div> | |
| </button> | |
| </div> | |
| <div class="relative mt-6 text-xs text-gray-500"> | |
| Tip: Press <span class="text-gray-300">Esc</span> to close. | |
| </div> | |
| </div> | |
| </div> | |
| <div class="relative rounded-2xl border border-gray-800 bg-gray-900/30 overflow-hidden"> | |
| <div class="px-6 py-5 border-b border-gray-800/60"> | |
| <div class="flex items-start justify-between gap-4"> | |
| <div> | |
| <div id="dossier-title" class="text-lg font-semibold text-gray-100">Lab Dossier</div> | |
| <div id="dossier-subtitle" class="text-xs text-gray-500 mt-1">Select a node to open a file.</div> | |
| </div> | |
| <div id="dossier-status" | |
| class="text-xs px-2.5 py-1 rounded-full border border-indigo-500/30 text-indigo-200 bg-indigo-900/15"> | |
| DRAFT | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-6 space-y-5 max-h-[560px] overflow-auto thin-scroll"> | |
| <div id="dossier-body" class="text-sm text-gray-300 leading-relaxed"> | |
| This console is designed for controlled interaction. Dossiers expose depth by intent. | |
| </div> | |
| <div class="rounded-xl border border-gray-800 bg-black/20 p-4"> | |
| <div class="text-xs text-gray-400 uppercase tracking-wider mb-2">Evidence Capsule</div> | |
| <ul id="dossier-evidence" class="text-sm text-gray-300 space-y-1"> | |
| <li class="text-gray-500">No dossier selected.</li> | |
| </ul> | |
| </div> | |
| <div class="flex flex-col sm:flex-row gap-3"> | |
| <button id="dossier-primary" | |
| class="flex-1 px-5 py-3 rounded-xl bg-gradient-to-r from-indigo-600 to-purple-600 hover:opacity-90 transition focus-ring"> | |
| Open | |
| </button> | |
| <button id="dossier-secondary" | |
| class="flex-1 px-5 py-3 rounded-xl border border-gray-700 bg-gray-900/20 hover:bg-gray-900/35 transition focus-ring"> | |
| View Note | |
| </button> | |
| </div> | |
| <div id="dossier-meta" class="text-xs text-gray-500"> | |
| Last updated: <span class="text-gray-300">—</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Site-wide configuration | |
| const CONFIG = { | |
| MODAL_HASHES: new Set(['lab', 'access']) | |
| }; | |
| /* ------------------------------------------------------------- | |
| UTILITY FUNCTIONS (consistent with other pages) | |
| ------------------------------------------------------------- */ | |
| function escapeHtml(str) { | |
| return String(str) | |
| .replaceAll('&', '&') | |
| .replaceAll('<', '<') | |
| .replaceAll('>', '>') | |
| .replaceAll('"', '"') | |
| .replaceAll("'", '''); | |
| } | |
| function isValidEmail(email) { | |
| return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); | |
| } | |
| function currentHashKey() { | |
| const h = (window.location.hash || '').replace('#', '').trim(); | |
| return h; | |
| } | |
| function setHash(key, replace = false) { | |
| if (!key) return; | |
| if (window.location.hash.replace('#', '') === key) return; | |
| if (replace) { | |
| history.replaceState(null, '', '#' + key); | |
| } else { | |
| history.pushState(null, '', '#' + key); | |
| } | |
| } | |
| function clearHashIf(key) { | |
| const h = currentHashKey(); | |
| if (!h) return; | |
| if (!key || h === key) { | |
| history.replaceState(null, '', window.location.pathname + window.location.search); | |
| } | |
| } | |
| /* ------------------------------------------------------------- | |
| MODAL ACCESSIBILITY (focus trap + restore opener focus) | |
| ------------------------------------------------------------- */ | |
| function trapFocus(modal) { | |
| const focusable = modal.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'); | |
| if (!focusable.length) return; | |
| const first = focusable[0]; | |
| const last = focusable[focusable.length - 1]; | |
| function handler(e) { | |
| if (e.key === 'Tab') { | |
| if (e.shiftKey) { | |
| if (document.activeElement === first) { e.preventDefault(); last.focus(); } | |
| } else { | |
| if (document.activeElement === last) { e.preventDefault(); first.focus(); } | |
| } | |
| } | |
| } | |
| modal.addEventListener('keydown', handler); | |
| modal._focusHandler = handler; | |
| } | |
| function untrapFocus(modal) { | |
| if (modal._focusHandler) { | |
| modal.removeEventListener('keydown', modal._focusHandler); | |
| delete modal._focusHandler; | |
| } | |
| } | |
| const toggleModal = (modal, show) => { | |
| if (show) { | |
| modal.classList.remove('modal-hidden'); | |
| modal.classList.add('modal-visible'); | |
| document.body.style.overflow = 'hidden'; | |
| setTimeout(() => { modal.focus(); trapFocus(modal); }, 0); | |
| } else { | |
| modal.classList.remove('modal-visible'); | |
| modal.classList.add('modal-hidden'); | |
| document.body.style.overflow = ''; | |
| untrapFocus(modal); | |
| } | |
| }; | |
| /* ------------------------------------------------------------- | |
| VANTA (guarded) | |
| ------------------------------------------------------------- */ | |
| let vantaEffect = null; | |
| try { | |
| if (window.VANTA && typeof VANTA.NET === 'function') { | |
| vantaEffect = VANTA.NET({ | |
| el: "#vanta-bg", | |
| mouseControls: true, | |
| touchControls: true, | |
| gyroControls: false, | |
| minHeight: 200.00, | |
| minWidth: 200.00, | |
| scale: 1.00, | |
| scaleMobile: 1.00, | |
| color: 0x4f46e5, | |
| backgroundColor: 0x020617, | |
| points: 12.00, | |
| maxDistance: 20.00, | |
| spacing: 15.00 | |
| }); | |
| } | |
| } catch (_) { | |
| vantaEffect = null; | |
| } | |
| window.addEventListener('resize', () => { | |
| if (vantaEffect && typeof vantaEffect.resize === 'function') vantaEffect.resize(); | |
| }); | |
| /* ------------------------------------------------------------- | |
| ACCESS MODAL (inline feedback) | |
| ------------------------------------------------------------- */ | |
| const accessModal = document.getElementById('access-modal'); | |
| const accessBtn = document.getElementById('access-btn'); | |
| const closeAccessModal = document.getElementById('close-access-modal'); | |
| const accessForm = document.getElementById('access-form'); | |
| const accessFeedback = document.getElementById('access-feedback'); | |
| const nameEl = document.getElementById('name'); | |
| const emailEl = document.getElementById('email'); | |
| const institutionEl = document.getElementById('institution'); | |
| const purposeEl = document.getElementById('purpose'); | |
| const nameErr = document.getElementById('name-error'); | |
| const emailErr = document.getElementById('email-error'); | |
| const institutionErr = document.getElementById('institution-error'); | |
| const purposeErr = document.getElementById('purpose-error'); | |
| function setAccessFeedback(kind, text) { | |
| if (!accessFeedback) return; | |
| accessFeedback.classList.remove('hidden'); | |
| accessFeedback.classList.remove('border-red-500/30', 'bg-red-900/10', 'text-red-200'); | |
| accessFeedback.classList.remove('border-emerald-500/30', 'bg-emerald-900/10', 'text-emerald-200'); | |
| accessFeedback.classList.remove('border-indigo-500/30', 'bg-indigo-900/10', 'text-indigo-200'); | |
| if (kind === 'error') { | |
| accessFeedback.classList.add('border-red-500/30', 'bg-red-900/10', 'text-red-200'); | |
| } else if (kind === 'success') { | |
| accessFeedback.classList.add('border-emerald-500/30', 'bg-emerald-900/10', 'text-emerald-200'); | |
| } else { | |
| accessFeedback.classList.add('border-indigo-500/30', 'bg-indigo-900/10', 'text-indigo-200'); | |
| } | |
| accessFeedback.textContent = text; | |
| } | |
| function hideAccessFeedback() { | |
| if (!accessFeedback) return; | |
| accessFeedback.textContent = ''; | |
| accessFeedback.classList.add('hidden'); | |
| accessFeedback.classList.remove( | |
| 'border-red-500/30','bg-red-900/10','text-red-200', | |
| 'border-emerald-500/30','bg-emerald-900/10','text-emerald-200', | |
| 'border-indigo-500/30','bg-indigo-900/10','text-indigo-200' | |
| ); | |
| } | |
| function setFieldError(inputEl, errorEl, isError) { | |
| if (!inputEl || !errorEl) return; | |
| if (isError) { | |
| errorEl.classList.remove('hidden'); | |
| inputEl.setAttribute('aria-invalid', 'true'); | |
| inputEl.classList.add('border-red-500/60'); | |
| } else { | |
| errorEl.classList.add('hidden'); | |
| inputEl.removeAttribute('aria-invalid'); | |
| inputEl.classList.remove('border-red-500/60'); | |
| } | |
| } | |
| function resetAccessErrors() { | |
| hideAccessFeedback(); | |
| setFieldError(nameEl, nameErr, false); | |
| setFieldError(emailEl, emailErr, false); | |
| setFieldError(institutionEl, institutionErr, false); | |
| setFieldError(purposeEl, purposeErr, false); | |
| } | |
| function openAccessModal(setHashFlag = true) { | |
| resetAccessErrors(); | |
| toggleModal(accessModal, true); | |
| if (setHashFlag) setHash('access'); | |
| setTimeout(() => nameEl && nameEl.focus(), 80); | |
| } | |
| function closeAccessModal(clearHashFlag = true) { | |
| toggleModal(accessModal, false); | |
| if (clearHashFlag) clearHashIf('access'); | |
| } | |
| accessBtn.addEventListener('click', () => openAccessModal(true)); | |
| closeAccessModal.addEventListener('click', () => closeAccessModal(true)); | |
| accessModal.addEventListener('click', (e) => { | |
| if (e.target === accessModal) closeAccessModal(true); | |
| }); | |
| if (accessForm) { | |
| // Clear per-field errors on input | |
| [nameEl, emailEl, institutionEl, purposeEl].forEach(el => { | |
| if (!el) return; | |
| el.addEventListener('input', () => { | |
| if (el === nameEl) setFieldError(nameEl, nameErr, !nameEl.value.trim()); | |
| if (el === emailEl) setFieldError(emailEl, emailErr, !isValidEmail(emailEl.value.trim())); | |
| if (el === institutionEl) setFieldError(institutionEl, institutionErr, !institutionEl.value.trim()); | |
| if (el === purposeEl) setFieldError(purposeEl, purposeErr, !purposeEl.value); | |
| }); | |
| el.addEventListener('change', () => el.dispatchEvent(new Event('input'))); | |
| }); | |
| accessForm.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| resetAccessErrors(); | |
| const name = (nameEl?.value || '').trim(); | |
| const email = (emailEl?.value || '').trim(); | |
| const institution = (institutionEl?.value || '').trim(); | |
| const purpose = (purposeEl?.value || '').trim(); | |
| let ok = true; | |
| if (!name) { setFieldError(nameEl, nameErr, true); ok = false; } | |
| if (!email || !isValidEmail(email)) { setFieldError(emailEl, emailErr, true); ok = false; } | |
| if (!institution) { setFieldError(institutionEl, institutionErr, true); ok = false; } | |
| if (!purpose) { setFieldError(purposeEl, purposeErr, true); ok = false; } | |
| if (!ok) { | |
| setAccessFeedback('error', 'Please correct the highlighted fields and resubmit.'); | |
| return; | |
| } | |
| setAccessFeedback('success', 'Request received. You will be contacted after review.'); | |
| accessForm.reset(); | |
| }); | |
| } | |
| /* ------------------------------------------------------------- | |
| LAB NAVIGATOR | |
| ------------------------------------------------------------- */ | |
| const labNav = document.getElementById('lab-navigator'); | |
| const labNavBtn = document.getElementById('lab-nav-btn'); | |
| const labNavClose = document.getElementById('lab-nav-close'); | |
| const DOSSIERS = { | |
| start: { | |
| title: "Start Here", | |
| subtitle: "Return to the main interface", | |
| status: "ACTIVE", | |
| body: "SILENTPATTERN is presented as a lab interface: minimal surface, deep artifacts. The index is the entrypoint.", | |
| evidence: ["Public entry layer", "Dossiers reveal depth", "Controlled demos by access"], | |
| primary: { label: "Go to Index", action: () => { window.location.href = "index.html"; } }, | |
| secondary: { label: "Research", action: () => { window.location.href = "research.html"; } }, | |
| updated: "—" | |
| }, | |
| console: { | |
| title: "Console", | |
| subtitle: "Controlled interaction channel", | |
| status: "DRAFT", | |
| body: "This console is a staging environment. It will connect to a server endpoint and produce auditable outputs (transcripts, notes, reports).", | |
| evidence: ["Client: UI only", "Server: keys + policy + logging", "Exportable transcript"], | |
| primary: { label: "Close Navigator", action: () => closeLabNav(true) }, | |
| secondary: { label: "Export", action: () => { closeLabNav(true); document.getElementById('export-btn').click(); } }, | |
| updated: "—" | |
| }, | |
| programs: { | |
| title: "Programs", | |
| subtitle: "MCAP · CHAI · Quantum Lambda", | |
| status: "DRAFT", | |
| body: "Programs are introduced as research artifacts with maturity levels (Concept → Prototype → Validated) and evidence capsules.", | |
| evidence: ["MCAP: causal abstraction principle", "CHAI: forecasting/regime modeling", "Quantum Lambda: high-frequency decision systems"], | |
| primary: { label: "Open Programs", action: () => { window.location.href = "capabilities.html"; } }, | |
| secondary: { label: "Research Notes", action: () => { window.location.href = "research.html"; } }, | |
| updated: "—" | |
| }, | |
| ai_scientist: { | |
| title: "AI Scientist", | |
| subtitle: "Hypothesis → experiment → report", | |
| status: "DRAFT", | |
| body: "The AI Scientist is positioned as the lab's instrument: it standardizes experiments, enforces reproducibility, and produces reports with uncertainty.", | |
| evidence: ["Experiment harnesses", "Evaluation baselines", "Report generation"], | |
| primary: { label: "View Research", action: () => { window.location.href = "research.html"; } }, | |
| secondary: { label: "Contact", action: () => { window.location.href = "contact.html"; } }, | |
| updated: "—" | |
| }, | |
| access: { | |
| title: "Access", | |
| subtitle: "Request access to demos and research", | |
| status: "ACTIVE", | |
| body: "Access is curated. The objective is qualified users, high-signal feedback, and responsible scaling.", | |
| evidence: ["Application-based", "Segmented by intent", "Controlled demos"], | |
| primary: { label: "Request Access", action: () => { closeLabNav(true); openAccessModal(true); } }, | |
| secondary: { label: "Contact", action: () => { window.location.href = "contact.html"; } }, | |
| updated: "—" | |
| } | |
| }; | |
| const dossierTitle = document.getElementById('dossier-title'); | |
| const dossierSubtitle = document.getElementById('dossier-subtitle'); | |
| const dossierStatus = document.getElementById('dossier-status'); | |
| const dossierBody = document.getElementById('dossier-body'); | |
| const dossierEvidence = document.getElementById('dossier-evidence'); | |
| const dossierPrimary = document.getElementById('dossier-primary'); | |
| const dossierSecondary = document.getElementById('dossier-secondary'); | |
| const dossierMeta = document.getElementById('dossier-meta'); | |
| function setActiveLabNode(key) { | |
| document.querySelectorAll('.lab-node').forEach(btn => { | |
| const isActive = btn.getAttribute('data-dossier') === key; | |
| btn.classList.toggle('active', isActive); | |
| btn.setAttribute('aria-current', isActive ? 'true' : 'false'); | |
| }); | |
| } | |
| function renderDossier(key) { | |
| const d = DOSSIERS[key]; | |
| if (!d) return; | |
| setActiveLabNode(key); | |
| dossierTitle.textContent = d.title; | |
| dossierSubtitle.textContent = d.subtitle; | |
| dossierStatus.textContent = d.status; | |
| dossierBody.textContent = d.body; | |
| dossierEvidence.innerHTML = ""; | |
| d.evidence.forEach(item => { | |
| const li = document.createElement('li'); | |
| li.textContent = item; | |
| dossierEvidence.appendChild(li); | |
| }); | |
| dossierPrimary.textContent = d.primary.label; | |
| dossierPrimary.onclick = d.primary.action; | |
| dossierSecondary.textContent = d.secondary.label; | |
| dossierSecondary.onclick = d.secondary.action; | |
| dossierMeta.innerHTML = `Last updated: <span class="text-gray-300">${d.updated}</span>`; | |
| } | |
| function openLabNav(setHashFlag = true) { | |
| toggleModal(labNav, true); | |
| if (setHashFlag) setHash('lab'); | |
| setTimeout(() => labNav.focus(), 0); | |
| // Ensure a stable default selection when opened | |
| const alreadyActive = document.querySelector('.lab-node.active'); | |
| if (!alreadyActive) renderDossier('console'); | |
| } | |
| function closeLabNav(clearHashFlag = true) { | |
| toggleModal(labNav, false); | |
| if (clearHashFlag) clearHashIf('lab'); | |
| } | |
| labNavBtn.addEventListener('click', () => openLabNav(true)); | |
| labNavClose.addEventListener('click', () => closeLabNav(true)); | |
| labNav.addEventListener('click', (e) => { | |
| const shouldClose = e.target && e.target.getAttribute('data-lab-close') === 'true'; | |
| if (shouldClose) closeLabNav(true); | |
| }); | |
| document.querySelectorAll('.lab-node').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const key = btn.getAttribute('data-dossier'); | |
| renderDossier(key); | |
| // Apply the implied navigation behavior | |
| if (key === 'start') window.location.href = "index.html"; | |
| if (key === 'programs') window.location.href = "capabilities.html"; | |
| if (key === 'ai_scientist') window.location.href = "research.html"; | |
| if (key === 'access') { closeLabNav(true); openAccessModal(true); } | |
| // console -> stays here | |
| }); | |
| }); | |
| /* ------------------------------------------------------------- | |
| CHAT: secure-by-design client | |
| ------------------------------------------------------------- */ | |
| const chatForm = document.getElementById('chat-form'); | |
| const chatInput = document.getElementById('chat-input'); | |
| const chatMessages = document.getElementById('chat-messages'); | |
| const typingIndicator = document.getElementById('typing-indicator'); | |
| const sendBtn = document.getElementById('send-btn'); | |
| const clearBtn = document.getElementById('clear-btn'); | |
| const exportBtn = document.getElementById('export-btn'); | |
| const transcript = []; // {role:'user'|'system', content:string, ts:number} | |
| function seedTranscript() { | |
| const seed = document.getElementById('seed-system-text'); | |
| if (seed && seed.textContent.trim()) { | |
| transcript.push({ role: 'system', content: seed.textContent.trim(), ts: Date.now() }); | |
| } | |
| } | |
| seedTranscript(); | |
| function addMessage(text, isUser = false) { | |
| const safe = escapeHtml(text); | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `flex items-start ${isUser ? 'justify-end' : ''}`; | |
| if (!isUser) { | |
| messageDiv.innerHTML = ` | |
| <div class="w-8 h-8 rounded-full bg-indigo-600 flex-shrink-0 flex items-center justify-center mr-3" aria-hidden="true"> | |
| <i class="fas fa-robot text-white text-sm"></i> | |
| </div> | |
| <div class="bg-gray-800/70 rounded-lg p-4 max-w-[85%]"> | |
| <p class="text-gray-100">${safe}</p> | |
| </div> | |
| `; | |
| } else { | |
| messageDiv.innerHTML = ` | |
| <div class="bg-indigo-900/50 rounded-lg p-4 max-w-[85%]"> | |
| <p class="text-gray-100">${safe}</p> | |
| </div> | |
| `; | |
| } | |
| chatMessages.appendChild(messageDiv); | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| transcript.push({ role: isUser ? 'user' : 'system', content: text, ts: Date.now() }); | |
| } | |
| function setBusy(isBusy) { | |
| if (isBusy) { | |
| typingIndicator.classList.remove('hidden'); | |
| sendBtn.disabled = true; | |
| chatInput.disabled = true; | |
| } else { | |
| typingIndicator.classList.add('hidden'); | |
| sendBtn.disabled = false; | |
| chatInput.disabled = false; | |
| chatInput.focus(); | |
| } | |
| } | |
| async function callServerChat(userMessage) { | |
| // This would be a server endpoint in production | |
| // For static demo, always fall back to local response | |
| throw new Error('Server endpoint not available in static demo'); | |
| } | |
| function localDemoResponse(userMessage) { | |
| const msg = userMessage.toLowerCase(); | |
| if (msg.includes('mcap')) { | |
| return "MCAP acknowledged. Provide: (1) the abstraction mapping you propose, (2) how you test causal fidelity, (3) baseline comparisons. I will draft a research-note structure next."; | |
| } | |
| if (msg.includes('chai')) { | |
| return "CHAI acknowledged. I will assume a regime-aware forecasting stack. Specify assets, horizon, labeling rules, and walk-forward protocol. Then we can define an evaluation harness."; | |
| } | |
| if (msg.includes('quantum lambda')) { | |
| return "Quantum Lambda acknowledged. For HFT, the first gate is market microstructure constraints and realistic latency/slippage. Share the execution assumptions and risk limits before discussing accuracy."; | |
| } | |
| return "Acknowledged. State (a) objective, (b) constraints, (c) what evidence you have today. I will respond with a plan and a minimal next experiment."; | |
| } | |
| chatForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const message = chatInput.value.trim(); | |
| if (!message) return; | |
| addMessage(message, true); | |
| chatInput.value = ''; | |
| setBusy(true); | |
| try { | |
| const reply = await callServerChat(message); | |
| addMessage(reply, false); | |
| } catch (_) { | |
| addMessage(localDemoResponse(message), false); | |
| } finally { | |
| setBusy(false); | |
| } | |
| }); | |
| clearBtn.addEventListener('click', () => { | |
| const nodes = Array.from(chatMessages.children); | |
| for (let i = 1; i < nodes.length; i++) nodes[i].remove(); | |
| transcript.length = 0; | |
| seedTranscript(); | |
| addMessage('Session cleared.', false); | |
| }); | |
| exportBtn.addEventListener('click', () => { | |
| const payload = { | |
| product: "SILENTPATTERN", | |
| page: "chat.html", | |
| exported_at: new Date().toISOString(), | |
| transcript | |
| }; | |
| const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `silentpattern_transcript_${Date.now()}.json`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| URL.revokeObjectURL(url); | |
| }); | |
| /* ------------------------------------------------------------- | |
| HASH ROUTER: handle modal hashes | |
| ------------------------------------------------------------- */ | |
| function applyHashState() { | |
| const key = currentHashKey(); | |
| if (CONFIG.MODAL_HASHES.has(key)) { | |
| if (key === 'lab') openLabNav(false); | |
| if (key === 'access') openAccessModal(false); | |
| } | |
| } | |
| window.addEventListener('hashchange', applyHashState); | |
| applyHashState(); // Initial load | |
| /* ------------------------------------------------------------- | |
| GLOBAL ESC | |
| ------------------------------------------------------------- */ | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') { | |
| if (labNav && !labNav.classList.contains('modal-hidden')) closeLabNav(true); | |
| if (accessModal && !accessModal.classList.contains('modal-hidden')) closeAccessModal(true); | |
| } | |
| }); | |
| // Initial dossier render | |
| renderDossier('console'); | |
| </script> | |
| </body> | |
| </html> |