Testing347 commited on
Commit
9cc835c
·
verified ·
1 Parent(s): 563525b

Update chat.html

Browse files
Files changed (1) hide show
  1. chat.html +145 -53
chat.html CHANGED
@@ -8,9 +8,9 @@
8
  <!-- Tailwind -->
9
  <script src="https://cdn.tailwindcss.com"></script>
10
 
11
- <!-- Three.js + Vanta -->
12
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
13
- <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.net.min.js"></script>
14
 
15
  <!-- Icons + Font -->
16
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
@@ -39,17 +39,21 @@
39
  .chat-container::-webkit-scrollbar-track { background: #1e1b4b; }
40
  .chat-container::-webkit-scrollbar-thumb { background-color: #4f46e5; border-radius: 3px; }
41
 
42
- .typing-indicator::after {
43
- content: '...';
44
- animation: typing 1.5s infinite;
45
- display: inline-block;
46
- width: 20px;
47
- text-align: left;
 
 
 
48
  }
49
- @keyframes typing {
50
- 0% { content: '.'; }
51
- 33% { content: '..'; }
52
- 66% { content: '...'; }
 
53
  }
54
 
55
  .modal { transition: opacity 0.3s ease, transform 0.3s ease; }
@@ -83,12 +87,14 @@
83
  <div class="flex items-center space-x-3">
84
  <button id="lab-nav-btn"
85
  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"
86
- aria-label="Open Lab Navigator" title="Lab Navigator">
 
87
  <i class="fas fa-asterisk text-indigo-300 text-sm"></i>
88
  </button>
89
 
90
  <button id="access-btn"
91
- class="px-6 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 rounded-full hover:opacity-90 transition">
 
92
  Access
93
  </button>
94
  </div>
@@ -172,7 +178,12 @@
172
  </button>
173
  </div>
174
  <div>
175
- <span id="typing-indicator" class="typing-indicator hidden">System is typing</span>
 
 
 
 
 
176
  </div>
177
  </div>
178
 
@@ -214,18 +225,23 @@
214
  </button>
215
  </div>
216
 
217
- <form id="access-form" class="space-y-4">
 
 
 
 
 
218
  <div>
219
  <label for="name" class="block text-sm font-medium mb-1">Full Name</label>
220
- <input type="text" id="name" class="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-4 py-2 focus-ring">
221
  </div>
222
  <div>
223
  <label for="email" class="block text-sm font-medium mb-1">Email</label>
224
- <input type="email" id="email" class="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-4 py-2 focus-ring">
225
  </div>
226
  <div>
227
  <label for="institution" class="block text-sm font-medium mb-1">Institution/Organization</label>
228
- <input type="text" id="institution" class="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-4 py-2 focus-ring">
229
  </div>
230
  <div>
231
  <label for="purpose" class="block text-sm font-medium mb-1">Purpose of Access</label>
@@ -369,25 +385,38 @@
369
  </div>
370
 
371
  <script>
372
- /* VANTA */
373
- const vantaEffect = VANTA.NET({
374
- el: "#vanta-bg",
375
- mouseControls: true,
376
- touchControls: true,
377
- gyroControls: false,
378
- minHeight: 200.00,
379
- minWidth: 200.00,
380
- scale: 1.00,
381
- scaleMobile: 1.00,
382
- color: 0x4f46e5,
383
- backgroundColor: 0x020617,
384
- points: 12.00,
385
- maxDistance: 20.00,
386
- spacing: 15.00
 
 
 
 
 
 
 
 
 
 
 
 
387
  });
388
- window.addEventListener('resize', () => vantaEffect.resize());
389
 
390
- /* MODAL ACCESSIBILITY */
 
 
391
  function trapFocus(modal) {
392
  const focusable = modal.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
393
  if (!focusable.length) return;
@@ -401,21 +430,22 @@
401
  } else {
402
  if (document.activeElement === last) { e.preventDefault(); first.focus(); }
403
  }
404
- } else if (e.key === 'Escape') {
405
- toggleModal(modal, false);
406
  }
407
  }
408
  modal.addEventListener('keydown', handler);
409
  modal._focusHandler = handler;
410
  }
 
411
  function untrapFocus(modal) {
412
  if (modal._focusHandler) {
413
  modal.removeEventListener('keydown', modal._focusHandler);
414
  delete modal._focusHandler;
415
  }
416
  }
 
417
  const toggleModal = (modal, show) => {
418
  if (show) {
 
419
  modal.classList.remove('modal-hidden');
420
  modal.classList.add('modal-visible');
421
  document.body.style.overflow = 'hidden';
@@ -425,37 +455,84 @@
425
  modal.classList.add('modal-hidden');
426
  document.body.style.overflow = '';
427
  untrapFocus(modal);
 
 
 
 
428
  }
429
  };
430
 
431
- /* ACCESS MODAL */
 
 
432
  const accessModal = document.getElementById('access-modal');
433
  const accessBtn = document.getElementById('access-btn');
434
  const closeAccessModal = document.getElementById('close-access-modal');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
 
436
  accessBtn.addEventListener('click', () => {
 
437
  toggleModal(accessModal, true);
438
  setTimeout(() => document.getElementById('name').focus(), 50);
439
  });
440
  closeAccessModal.addEventListener('click', () => toggleModal(accessModal, false));
441
  accessModal.addEventListener('click', (e) => { if (e.target === accessModal) toggleModal(accessModal, false); });
442
 
443
- document.getElementById('access-form').addEventListener('submit', (e) => {
444
  e.preventDefault();
445
  const name = document.getElementById('name').value.trim();
446
  const email = document.getElementById('email').value.trim();
447
  const institution = document.getElementById('institution').value.trim();
448
  const purpose = document.getElementById('purpose').value;
 
449
  if (!name || !email || !institution || !purpose) {
450
- alert('Please fill in all fields.');
451
  return;
452
  }
453
- alert('Request received. You will be contacted after review.');
454
- e.target.reset();
455
- toggleModal(accessModal, false);
 
 
 
 
 
456
  });
457
 
458
- /* LAB NAVIGATOR */
 
 
459
  const labNav = document.getElementById('lab-navigator');
460
  const labNavBtn = document.getElementById('lab-nav-btn');
461
  const labNavClose = document.getElementById('lab-nav-close');
@@ -535,6 +612,7 @@
535
  function renderDossier(key) {
536
  const d = DOSSIERS[key];
537
  if (!d) return;
 
538
  dossierTitle.textContent = d.title;
539
  dossierSubtitle.textContent = d.subtitle;
540
  dossierStatus.textContent = d.status;
@@ -557,10 +635,22 @@
557
  }
558
 
559
  document.querySelectorAll('.lab-node').forEach(btn => {
560
- btn.addEventListener('click', () => renderDossier(btn.getAttribute('data-dossier')));
 
 
 
 
 
 
 
 
 
 
561
  });
562
 
563
- /* CHAT: secure-by-design client */
 
 
564
  const API_ENDPOINT = "/api/chat"; // change if your deployment requires a prefix
565
 
566
  const chatForm = document.getElementById('chat-form');
@@ -622,9 +712,12 @@
622
  if (isBusy) {
623
  typingIndicator.classList.remove('hidden');
624
  sendBtn.disabled = true;
 
625
  } else {
626
  typingIndicator.classList.add('hidden');
627
  sendBtn.disabled = false;
 
 
628
  }
629
  }
630
 
@@ -676,7 +769,7 @@
676
  try {
677
  const reply = await callServerChat(message);
678
  addMessage(reply, false);
679
- } catch (err) {
680
  addMessage(localDemoResponse(message), false);
681
  } finally {
682
  setBusy(false);
@@ -684,15 +777,12 @@
684
  });
685
 
686
  clearBtn.addEventListener('click', () => {
687
- // Keep the seed greeting, remove everything after it
688
  const nodes = Array.from(chatMessages.children);
689
  for (let i = 1; i < nodes.length; i++) nodes[i].remove();
690
 
691
- // Reset transcript to the seed system message only
692
  transcript.length = 0;
693
  seedTranscript();
694
 
695
- // Add a visible confirmation message (this will also be recorded)
696
  addMessage('Session cleared.', false);
697
  });
698
 
@@ -714,7 +804,9 @@
714
  URL.revokeObjectURL(url);
715
  });
716
 
717
- /* GLOBAL ESC */
 
 
718
  document.addEventListener('keydown', (e) => {
719
  if (e.key === 'Escape') {
720
  if (labNav && !labNav.classList.contains('modal-hidden')) closeLabNav();
@@ -722,7 +814,7 @@
722
  }
723
  });
724
 
725
- // Optional: preselect a dossier for the right pane
726
  renderDossier('console');
727
  </script>
728
  </body>
 
8
  <!-- Tailwind -->
9
  <script src="https://cdn.tailwindcss.com"></script>
10
 
11
+ <!-- Three.js + Vanta (pinned) -->
12
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
13
+ <script src="https://cdn.jsdelivr.net/npm/vanta@0.5.24/dist/vanta.net.min.js"></script>
14
 
15
  <!-- Icons + Font -->
16
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
 
39
  .chat-container::-webkit-scrollbar-track { background: #1e1b4b; }
40
  .chat-container::-webkit-scrollbar-thumb { background-color: #4f46e5; border-radius: 3px; }
41
 
42
+ /* Typing indicator that actually animates (no content-keyframes) */
43
+ .typing-indicator { display: inline-flex; align-items: center; gap: 6px; }
44
+ .typing-dots { display: inline-flex; gap: 3px; }
45
+ .typing-dots span {
46
+ width: 4px; height: 4px;
47
+ border-radius: 999px;
48
+ background: rgba(148,163,184,0.9);
49
+ opacity: 0.35;
50
+ animation: dotPulse 1.2s infinite;
51
  }
52
+ .typing-dots span:nth-child(2) { animation-delay: 0.15s; }
53
+ .typing-dots span:nth-child(3) { animation-delay: 0.30s; }
54
+ @keyframes dotPulse {
55
+ 0%, 100% { opacity: 0.25; transform: translateY(0); }
56
+ 50% { opacity: 0.95; transform: translateY(-1px); }
57
  }
58
 
59
  .modal { transition: opacity 0.3s ease, transform 0.3s ease; }
 
87
  <div class="flex items-center space-x-3">
88
  <button id="lab-nav-btn"
89
  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"
90
+ aria-label="Open Lab Navigator" title="Lab Navigator"
91
+ aria-controls="lab-navigator" aria-haspopup="dialog">
92
  <i class="fas fa-asterisk text-indigo-300 text-sm"></i>
93
  </button>
94
 
95
  <button id="access-btn"
96
+ class="px-6 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 rounded-full hover:opacity-90 transition"
97
+ aria-controls="access-modal" aria-haspopup="dialog">
98
  Access
99
  </button>
100
  </div>
 
178
  </button>
179
  </div>
180
  <div>
181
+ <span id="typing-indicator" class="hidden">
182
+ <span class="typing-indicator">
183
+ <span>System is typing</span>
184
+ <span class="typing-dots" aria-hidden="true"><span></span><span></span><span></span></span>
185
+ </span>
186
+ </span>
187
  </div>
188
  </div>
189
 
 
225
  </button>
226
  </div>
227
 
228
+ <!-- Inline feedback (replaces alerts) -->
229
+ <div id="access-feedback"
230
+ class="hidden mb-4 rounded-lg border border-gray-800 bg-black/25 px-4 py-3 text-sm"
231
+ role="status" aria-live="polite"></div>
232
+
233
+ <form id="access-form" class="space-y-4" novalidate>
234
  <div>
235
  <label for="name" class="block text-sm font-medium mb-1">Full Name</label>
236
+ <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">
237
  </div>
238
  <div>
239
  <label for="email" class="block text-sm font-medium mb-1">Email</label>
240
+ <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">
241
  </div>
242
  <div>
243
  <label for="institution" class="block text-sm font-medium mb-1">Institution/Organization</label>
244
+ <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">
245
  </div>
246
  <div>
247
  <label for="purpose" class="block text-sm font-medium mb-1">Purpose of Access</label>
 
385
  </div>
386
 
387
  <script>
388
+ /* -------------------------------------------------------------
389
+ VANTA (guarded)
390
+ ------------------------------------------------------------- */
391
+ let vantaEffect = null;
392
+ try {
393
+ if (window.VANTA && typeof VANTA.NET === 'function') {
394
+ vantaEffect = VANTA.NET({
395
+ el: "#vanta-bg",
396
+ mouseControls: true,
397
+ touchControls: true,
398
+ gyroControls: false,
399
+ minHeight: 200.00,
400
+ minWidth: 200.00,
401
+ scale: 1.00,
402
+ scaleMobile: 1.00,
403
+ color: 0x4f46e5,
404
+ backgroundColor: 0x020617,
405
+ points: 12.00,
406
+ maxDistance: 20.00,
407
+ spacing: 15.00
408
+ });
409
+ }
410
+ } catch (_) {
411
+ vantaEffect = null;
412
+ }
413
+ window.addEventListener('resize', () => {
414
+ if (vantaEffect && typeof vantaEffect.resize === 'function') vantaEffect.resize();
415
  });
 
416
 
417
+ /* -------------------------------------------------------------
418
+ MODAL ACCESSIBILITY (focus trap + restore opener focus)
419
+ ------------------------------------------------------------- */
420
  function trapFocus(modal) {
421
  const focusable = modal.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
422
  if (!focusable.length) return;
 
430
  } else {
431
  if (document.activeElement === last) { e.preventDefault(); first.focus(); }
432
  }
 
 
433
  }
434
  }
435
  modal.addEventListener('keydown', handler);
436
  modal._focusHandler = handler;
437
  }
438
+
439
  function untrapFocus(modal) {
440
  if (modal._focusHandler) {
441
  modal.removeEventListener('keydown', modal._focusHandler);
442
  delete modal._focusHandler;
443
  }
444
  }
445
+
446
  const toggleModal = (modal, show) => {
447
  if (show) {
448
+ modal._opener = document.activeElement;
449
  modal.classList.remove('modal-hidden');
450
  modal.classList.add('modal-visible');
451
  document.body.style.overflow = 'hidden';
 
455
  modal.classList.add('modal-hidden');
456
  document.body.style.overflow = '';
457
  untrapFocus(modal);
458
+ if (modal._opener && typeof modal._opener.focus === 'function') {
459
+ modal._opener.focus();
460
+ }
461
+ modal._opener = null;
462
  }
463
  };
464
 
465
+ /* -------------------------------------------------------------
466
+ ACCESS MODAL (inline feedback)
467
+ ------------------------------------------------------------- */
468
  const accessModal = document.getElementById('access-modal');
469
  const accessBtn = document.getElementById('access-btn');
470
  const closeAccessModal = document.getElementById('close-access-modal');
471
+ const accessForm = document.getElementById('access-form');
472
+ const accessFeedback = document.getElementById('access-feedback');
473
+
474
+ function setAccessFeedback(kind, text) {
475
+ accessFeedback.classList.remove('hidden');
476
+ accessFeedback.classList.remove('border-red-500/30', 'bg-red-900/10', 'text-red-200');
477
+ accessFeedback.classList.remove('border-emerald-500/30', 'bg-emerald-900/10', 'text-emerald-200');
478
+ accessFeedback.classList.remove('border-indigo-500/30', 'bg-indigo-900/10', 'text-indigo-200');
479
+
480
+ if (kind === 'error') {
481
+ accessFeedback.classList.add('border-red-500/30', 'bg-red-900/10', 'text-red-200');
482
+ } else if (kind === 'success') {
483
+ accessFeedback.classList.add('border-emerald-500/30', 'bg-emerald-900/10', 'text-emerald-200');
484
+ } else {
485
+ accessFeedback.classList.add('border-indigo-500/30', 'bg-indigo-900/10', 'text-indigo-200');
486
+ }
487
+ accessFeedback.textContent = text;
488
+ }
489
+
490
+ function clearAccessFeedback() {
491
+ accessFeedback.textContent = '';
492
+ accessFeedback.classList.add('hidden');
493
+ accessFeedback.classList.remove(
494
+ 'border-red-500/30','bg-red-900/10','text-red-200',
495
+ 'border-emerald-500/30','bg-emerald-900/10','text-emerald-200',
496
+ 'border-indigo-500/30','bg-indigo-900/10','text-indigo-200'
497
+ );
498
+ }
499
+
500
+ function isValidEmail(email) {
501
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
502
+ }
503
 
504
  accessBtn.addEventListener('click', () => {
505
+ clearAccessFeedback();
506
  toggleModal(accessModal, true);
507
  setTimeout(() => document.getElementById('name').focus(), 50);
508
  });
509
  closeAccessModal.addEventListener('click', () => toggleModal(accessModal, false));
510
  accessModal.addEventListener('click', (e) => { if (e.target === accessModal) toggleModal(accessModal, false); });
511
 
512
+ accessForm.addEventListener('submit', (e) => {
513
  e.preventDefault();
514
  const name = document.getElementById('name').value.trim();
515
  const email = document.getElementById('email').value.trim();
516
  const institution = document.getElementById('institution').value.trim();
517
  const purpose = document.getElementById('purpose').value;
518
+
519
  if (!name || !email || !institution || !purpose) {
520
+ setAccessFeedback('error', 'Please fill in all fields.');
521
  return;
522
  }
523
+ if (!isValidEmail(email)) {
524
+ setAccessFeedback('error', 'Please enter a valid email address.');
525
+ return;
526
+ }
527
+
528
+ setAccessFeedback('success', 'Request received. You will be contacted after review.');
529
+ accessForm.reset();
530
+ setTimeout(() => toggleModal(accessModal, false), 900);
531
  });
532
 
533
+ /* -------------------------------------------------------------
534
+ LAB NAVIGATOR
535
+ ------------------------------------------------------------- */
536
  const labNav = document.getElementById('lab-navigator');
537
  const labNavBtn = document.getElementById('lab-nav-btn');
538
  const labNavClose = document.getElementById('lab-nav-close');
 
612
  function renderDossier(key) {
613
  const d = DOSSIERS[key];
614
  if (!d) return;
615
+
616
  dossierTitle.textContent = d.title;
617
  dossierSubtitle.textContent = d.subtitle;
618
  dossierStatus.textContent = d.status;
 
635
  }
636
 
637
  document.querySelectorAll('.lab-node').forEach(btn => {
638
+ btn.addEventListener('click', () => {
639
+ const key = btn.getAttribute('data-dossier');
640
+ renderDossier(key);
641
+
642
+ // Apply the implied navigation behavior (previously missing)
643
+ if (key === 'start') window.location.href = "index.html";
644
+ if (key === 'programs') window.location.href = "capabilities.html";
645
+ if (key === 'ai_scientist') window.location.href = "research.html";
646
+ if (key === 'access') { closeLabNav(); accessBtn.click(); }
647
+ // console -> stays here
648
+ });
649
  });
650
 
651
+ /* -------------------------------------------------------------
652
+ CHAT: secure-by-design client
653
+ ------------------------------------------------------------- */
654
  const API_ENDPOINT = "/api/chat"; // change if your deployment requires a prefix
655
 
656
  const chatForm = document.getElementById('chat-form');
 
712
  if (isBusy) {
713
  typingIndicator.classList.remove('hidden');
714
  sendBtn.disabled = true;
715
+ chatInput.disabled = true;
716
  } else {
717
  typingIndicator.classList.add('hidden');
718
  sendBtn.disabled = false;
719
+ chatInput.disabled = false;
720
+ chatInput.focus();
721
  }
722
  }
723
 
 
769
  try {
770
  const reply = await callServerChat(message);
771
  addMessage(reply, false);
772
+ } catch (_) {
773
  addMessage(localDemoResponse(message), false);
774
  } finally {
775
  setBusy(false);
 
777
  });
778
 
779
  clearBtn.addEventListener('click', () => {
 
780
  const nodes = Array.from(chatMessages.children);
781
  for (let i = 1; i < nodes.length; i++) nodes[i].remove();
782
 
 
783
  transcript.length = 0;
784
  seedTranscript();
785
 
 
786
  addMessage('Session cleared.', false);
787
  });
788
 
 
804
  URL.revokeObjectURL(url);
805
  });
806
 
807
+ /* -------------------------------------------------------------
808
+ GLOBAL ESC
809
+ ------------------------------------------------------------- */
810
  document.addEventListener('keydown', (e) => {
811
  if (e.key === 'Escape') {
812
  if (labNav && !labNav.classList.contains('modal-hidden')) closeLabNav();
 
814
  }
815
  });
816
 
817
+ // Preselect a dossier for the right pane
818
  renderDossier('console');
819
  </script>
820
  </body>