Copy-ava-testing / chat.html
Testing347's picture
Update chat.html
cb13f5f verified
<!-- SILENTPATTERN FINAL BUILD: 2025-12-15 | pages: 10 | features: hash-deep-linking, lab-navigator, access-modal, form-validation, chat-console, export-transcript -->
<!DOCTYPE html>
<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) !important;
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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
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>