nihalaninihal commited on
Commit
76006d4
·
verified ·
1 Parent(s): b049a63

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +538 -224
index.html CHANGED
@@ -221,6 +221,22 @@
221
  100% { transform: scale(1); }
222
  }
223
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  /* Responsive Design */
225
  /* Stack columns on smaller screens */
226
  .main-container {
@@ -296,17 +312,45 @@
296
  gap: 0.75rem; /* Space between grid items */
297
  }
298
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  </style>
300
  </head>
301
- <body class="text-sm md:text-base"> <div class="main-container">
 
 
302
 
 
303
  <div class="chat-section">
304
  <div class="text-center mb-3 md:mb-4">
305
  <h1 class="text-2xl md:text-3xl font-bold text-purple-300">G.E.N.I.E.</h1>
306
  <p class="text-purple-100 text-xs md:text-sm">
307
  GitHub Enhanced Natural Intelligence Engine <br class="md:hidden">
308
  <span class="hidden md:inline"> - </span>
309
- A voice-controlled AI that grants your GitHub wishes.”
310
  </p>
311
  </div>
312
 
@@ -343,10 +387,18 @@
343
  </div>
344
 
345
  <div class="status-section">
346
- <div class="status-content"> {/* Wrapper for centering content */}
347
  <h2 id="agentStatusTitle" class="text-lg md:text-xl font-semibold text-purple-400 text-center">G.E.N.I.E. Status</h2>
348
  <div id="genieOrb" class="genie-orb"></div>
349
  <button id="interruptButton" class="interrupt-button hidden">Interrupt G.E.N.I.E.</button>
 
 
 
 
 
 
 
 
350
  <div class="w-full glass-panel p-3 md:p-4 h-32 md:h-40 overflow-y-auto custom-scrollbar">
351
  <h3 class="text-base md:text-lg font-medium text-purple-300 mb-2 border-b border-purple-700 pb-1">G.E.N.I.E. Output:</h3>
352
  <p id="genieOutput" class="text-xs md:text-sm whitespace-pre-wrap">Awaiting your command...</p>
@@ -358,9 +410,9 @@
358
  <script>
359
  // --- DOM Elements ---
360
  const repoUrlInput = document.getElementById('repoUrl');
361
- const githubTokenInput = document.getElementById('githubToken'); // New
362
- const userTypeSelect = document.getElementById('userType'); // New
363
- const responseDetailSelect = document.getElementById('responseDetail'); // New
364
  const userQueryInput = document.getElementById('userQuery');
365
  const sendButton = document.getElementById('sendButton');
366
  const micButton = document.getElementById('micButton');
@@ -369,53 +421,345 @@
369
  const genieOrb = document.getElementById('genieOrb');
370
  const genieOutput = document.getElementById('genieOutput');
371
  const agentStatusTitle = document.getElementById('agentStatusTitle');
 
 
 
372
 
373
  // --- State ---
374
  let isListening = false;
 
375
  let botResponseTimeoutId = null;
376
  let thinkingMessageElement = null;
377
- let isBotActive = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
 
379
- // --- Event Listeners ---
380
- sendButton.addEventListener('click', handleSendQuery);
381
- userQueryInput.addEventListener('keypress', (event) => {
382
- if (event.key === 'Enter' && !event.shiftKey) { // Send on Enter, allow Shift+Enter for newline
383
- event.preventDefault(); // Prevent default newline behavior
384
- handleSendQuery();
 
 
 
385
  }
386
- });
387
- userQueryInput.addEventListener('input', handleTypingInterrupt);
388
- micButton.addEventListener('click', handleMicButtonClick);
389
- interruptButton.addEventListener('click', handleInterrupt);
 
 
 
 
 
 
 
 
 
390
 
391
- // --- Functions ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
 
393
- function handleMicButtonClick() {
 
394
  // If the bot is actively processing, the mic button acts as an interrupt
395
  if (isBotActive) {
396
  handleInterrupt();
397
- } else {
398
- // Otherwise, toggle listening mode
399
- toggleListeningMode();
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  }
401
  }
402
 
403
- function toggleListeningMode() {
404
- if (isBotActive) return; // Don't toggle if bot is busy
405
-
406
  isListening = !isListening;
407
  micButton.classList.toggle('listening', isListening);
408
  genieOrb.classList.toggle('listening', isListening);
409
-
410
  if (isListening) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  setBotActiveState(false); // Ensure bot is not marked active
412
  agentStatusTitle.textContent = "G.E.N.I.E. Status: Listening...";
413
- updateGenieText("Listening... (Simulation - Type your wish and press Ask)");
414
- userQueryInput.focus();
415
  micButton.title = "Stop Listening / Interrupt"; // Update tooltip
416
  } else {
417
- micButton.title = "Toggle Listening Mode / Interrupt"; // Reset tooltip
418
- // Only reset status if the bot isn't currently active (e.g., thinking)
 
 
419
  if (!isBotActive) {
420
  agentStatusTitle.textContent = "G.E.N.I.E. Status";
421
  // Only reset output if it was showing the listening message
@@ -438,76 +782,107 @@
438
  if (isListening) {
439
  toggleListeningMode();
440
  }
441
-
442
  // Prevent sending if bot is already active
443
  if (isBotActive) {
444
- console.log("G.E.N.I.E. is active, cannot send new query yet.");
445
- // Maybe add a subtle visual cue here later
446
- return;
447
  }
448
-
449
  const repoUrl = repoUrlInput.value.trim();
450
  const query = userQueryInput.value.trim();
451
- const githubToken = githubTokenInput.value.trim(); // Get token (optional)
452
- const userType = userTypeSelect.value; // Get user type
453
- const responseDetail = responseDetailSelect.value; // Get detail level
454
-
455
  // Basic Validations
456
  if (!query) {
457
- showTemporaryAlert(userQueryInput, 'Please state your wish (enter a query).');
458
  return;
459
  }
 
460
  if (!isValidHttpUrl(repoUrl)) {
461
- showTemporaryAlert(repoUrlInput, 'Please provide a valid GitHub repository URL (starting with http:// or https://).');
462
- return;
463
  }
464
-
 
 
 
 
 
 
 
 
 
 
 
465
  // Add user message to chat
466
  addMessageToChat(query, 'user');
467
  userQueryInput.value = ''; // Clear input field
468
-
469
- // Simulate bot processing and response
470
- simulateBotResponse(repoUrl, query, userType, responseDetail, githubToken);
471
- }
472
-
473
- function handleInterrupt() {
474
- if (botResponseTimeoutId) {
475
- console.log("Interrupt triggered by user.");
476
- clearTimeout(botResponseTimeoutId);
477
- botResponseTimeoutId = null;
478
-
479
- // Remove the "thinking" message if it exists
 
 
 
 
 
480
  if (thinkingMessageElement) {
481
  thinkingMessageElement.remove();
482
  thinkingMessageElement = null;
483
  }
484
-
485
- // Reset bot state
486
  setBotActiveState(false);
 
 
487
 
488
- // Add an interrupted message to chat
489
- addMessageToChat("Processing interrupted by user.", 'interrupted');
490
- updateGenieText("Interrupted. Ready for your next wish.");
491
-
492
- // Refocus input for convenience
493
- userQueryInput.focus();
494
- } else {
495
- console.log("Interrupt called but G.E.N.I.E. was not actively processing.");
 
 
 
 
 
 
 
 
 
 
496
  }
 
 
 
 
 
 
 
497
  }
498
 
499
  function addMessageToChat(message, type) {
500
  const messageElement = document.createElement('div');
501
  messageElement.classList.add('chat-bubble');
502
-
503
  // Stop listening visual cues if a message is added
504
- if (isListening) {
505
  genieOrb.classList.remove('listening');
506
  micButton.classList.remove('listening');
507
  isListening = false; // Ensure state is updated
508
  micButton.title = "Toggle Listening Mode / Interrupt"; // Reset tooltip
509
  }
510
-
511
  switch (type) {
512
  case 'user':
513
  messageElement.classList.add('user-bubble');
@@ -517,173 +892,68 @@
517
  messageElement.classList.add('bot-bubble');
518
  messageElement.textContent = message;
519
  updateGenieText(message); // Update status output
520
- // Short delay before resetting active state to allow UI update
521
- if (botResponseTimeoutId) clearTimeout(botResponseTimeoutId); // Clear any pending timeout
522
- botResponseTimeoutId = null; // Ensure timeout ID is cleared
523
- setTimeout(() => {
524
- if(isBotActive) setBotActiveState(false); // Reset state after response is shown
525
- }, 100); // Small delay
 
 
 
526
  break;
527
  case 'thinking':
528
  messageElement.classList.add('bot-bubble', 'thinking');
529
  messageElement.innerHTML = `Consulting the digital ether... <div class="dot-flashing"></div>`;
530
  updateGenieText("Processing your wish..."); // Update status output
531
- setBotActiveState(true); // Set bot to active state
532
  thinkingMessageElement = messageElement; // Store reference to remove later
533
  break;
534
  case 'interrupted':
535
- messageElement.classList.add('bot-bubble', 'interrupted');
536
- messageElement.textContent = message;
537
- // Status text is updated in handleInterrupt
538
- break;
539
  default:
540
  console.error("Unknown message type:", type);
541
  return null; // Don't add unknown types
542
  }
543
-
544
  chatHistory.appendChild(messageElement);
545
  // Scroll to the bottom smoothly after adding message
546
- // Use setTimeout to ensure element is rendered before scrolling
547
  setTimeout(() => {
548
  chatHistory.scrollTo({ top: chatHistory.scrollHeight, behavior: 'smooth' });
549
  }, 50);
550
-
551
  return messageElement; // Return the created element
552
  }
553
 
554
- function simulateBotResponse(repoUrl, query, userType, responseDetail, githubToken) {
555
- // Start the thinking state
556
- thinkingMessageElement = addMessageToChat('', 'thinking');
557
-
558
- // Simulate network delay/processing time
559
- const delay = 2000 + Math.random() * 3500; // 2 to 5.5 seconds
560
-
561
- // Clear any previous timeout just in case
562
- if (botResponseTimeoutId) clearTimeout(botResponseTimeoutId);
563
-
564
- botResponseTimeoutId = setTimeout(() => {
565
- // Check if it was interrupted *before* this timeout fired
566
- const wasInterrupted = !thinkingMessageElement;
567
- if (wasInterrupted) {
568
- console.log("G.E.N.I.E. response timeout fired, but was already interrupted.");
569
- botResponseTimeoutId = null; // Clear ID as it's no longer needed
570
- // No need to reset state here, handleInterrupt already did
571
- return;
572
- }
573
-
574
- // If not interrupted, remove the thinking message
575
- if (thinkingMessageElement) {
576
- thinkingMessageElement.remove();
577
- thinkingMessageElement = null;
578
- }
579
- botResponseTimeoutId = null; // Clear the timeout ID
580
-
581
- // Generate a sample response (incorporating preferences cosmetically)
582
- const botResponse = generateSampleResponse(repoUrl, query, userType, responseDetail, githubToken);
583
-
584
- // Add the actual bot response to the chat
585
- addMessageToChat(botResponse, 'bot');
586
- // The 'bot' case in addMessageToChat now handles resetting the active state
587
-
588
- }, delay);
589
- }
590
-
591
- // Updated to include preferences (cosmetic for now)
592
- function generateSampleResponse(repoUrl, query, userType, responseDetail, githubToken) {
593
- const repoName = repoUrl.substring(repoUrl.lastIndexOf('/') + 1) || "[Unknown Repo]";
594
- query = query.toLowerCase();
595
- let responsePrefix = "";
596
- let detailModifier = 1; // 1 = normal, <1 = concise, >1 = detailed
597
-
598
- // Adjust detail level (simple multiplier for example length)
599
- if (responseDetail === 'concise') detailModifier = 0.6;
600
- if (responseDetail === 'detailed') detailModifier = 1.5;
601
-
602
- // Add flavor based on user type
603
- switch (userType) {
604
- case 'manager': responsePrefix = "From a high-level view, "; break;
605
- case 'researcher': responsePrefix = "Digging into the details, "; break;
606
- case 'student': responsePrefix = "To help with your learning, "; break;
607
- // Coder gets no specific prefix (default)
608
- }
609
- // Acknowledge token if provided (SIMULATION ONLY)
610
- if (githubToken) {
611
- console.log("Simulating use of GitHub Token (not actually used):", githubToken.substring(0, 4) + "...");
612
- // You could add a note in the response, but be careful not to leak info
613
- // responsePrefix += "(Using provided token access) ";
614
- }
615
-
616
-
617
- // Basic query matching with some flavor
618
- if (query.includes("what is this repo") || query.includes("summarize")) {
619
- let base = `As you wish! The repository '${repoName}' appears to be `;
620
- let example = `[a ${Math.random() > 0.5 ? 'JavaScript library for UI components' : 'Python backend for data processing'}, focusing on ${Math.random() > 0.5 ? 'performance and modularity' : 'ease of use and integration'}].`;
621
- if (detailModifier > 1) example += ` It likely utilizes technologies like [React/Vue or Django/Flask] and follows standard project layout conventions. Recent activity suggests ongoing development.`;
622
- if (detailModifier < 1) example = `[a ${Math.random() > 0.5 ? 'JS UI library' : 'Python data backend'}].`;
623
- return responsePrefix + base + example;
624
- }
625
- if (query.includes("main language") || query.includes("written in")) {
626
- const languages = ["JavaScript", "Python", "Java", "TypeScript", "Go", "CSS", "Ruby", "PHP"];
627
- const randomLang = languages[Math.floor(Math.random() * languages.length)];
628
- let base = `Gazing into the code of '${repoName}', I reveal the primary language appears to be ${randomLang}. `;
629
- let example = `Hints of [${languages[Math.floor(Math.random() * languages.length)]}] may also be present.`;
630
- if (detailModifier > 1) example += ` Analysis suggests around ${Math.floor(Math.random()*30+60)}% of the codebase is ${randomLang}.`;
631
- if (detailModifier < 1) example = ""; // Concise just gives the main language
632
- return responsePrefix + base + example;
633
- }
634
- if (query.includes("how many files") || query.includes("file count")) {
635
- const fileCount = Math.floor(Math.random() * (800 * detailModifier) + 50);
636
- return responsePrefix + `By my count, the main branch of '${repoName}' contains approximately ${fileCount} files and directories.`;
637
- }
638
- if (query.includes("docker") || query.includes("container")) {
639
- let base = Math.random() > 0.4 ? `Indeed! A Dockerfile has manifested in '${repoName}'` : `Alas, a standard Dockerfile is hidden from my sight in the root of '${repoName}'`;
640
- let example = Math.random() > 0.4 ? `, indicating readiness for containerization using standard Docker tooling.` : `, but containerization magic might be conjured via other means like docker-compose or custom scripts.`;
641
- if (detailModifier < 1) example = Math.random() > 0.4 ? ", suggesting container support." : ", no obvious Dockerfile found.";
642
- return responsePrefix + base + example;
643
- }
644
- if (query.includes("test") || query.includes("coverage")) {
645
- let base = `The repository '${repoName}' reveals signs of testing charms. `;
646
- let example = `I see evidence of a '/tests' or '/spec' directory, possibly utilizing frameworks like [Jest/Pytest/RSpec].`;
647
- if (detailModifier > 1) example += ` Configuration files suggest integration with CI/CD pipelines for automated testing. Code coverage analysis might be configured.`;
648
- if (detailModifier < 1) example = `Testing files seem present.`;
649
- return responsePrefix + base + example;
650
- }
651
- if (query.includes("hello") || query.includes("hi") || query.includes("greetings")) {
652
- return `Greetings, Master ${userType}! How may G.E.N.I.E. assist you with the '${repoName}' repository today? State your wish!`;
653
- }
654
-
655
- // Default fallback response
656
- let fallback = `I have considered your wish regarding '${repoName}' concerning "${query.substring(0, 30)}...". `;
657
- let example = `My vision suggests that [${Math.random() > 0.5 ? 'the file structure seems reasonably organized' : 'recent commit activity implies active maintenance'}].`;
658
- if (detailModifier > 1) example += ` It likely utilizes common libraries for its domain, such as [mention a relevant library type like 'data visualization' or 'web framework']. Dependencies seem managed via [npm/pip/maven].`;
659
- if (detailModifier < 1) example = `It appears to be a standard project.`;
660
- fallback += example + ` For deeper secrets, phrase your wish more specifically.`;
661
- return responsePrefix + fallback;
662
- }
663
-
664
- /** Updates the G.E.N.I.E. output text in the status panel */
665
  function updateGenieText(text) {
666
  genieOutput.textContent = text;
667
  }
668
 
669
- /** Sets the visual state indicating if the bot is actively processing */
670
- function setBotActiveState(isActive) {
671
  isBotActive = isActive;
 
 
672
  genieOrb.classList.toggle('active', isActive);
673
  interruptButton.classList.toggle('hidden', !isActive);
674
- sendButton.disabled = isActive; // Disable send button while active
675
- userQueryInput.disabled = isActive; // Optionally disable query input too
676
- repoUrlInput.disabled = isActive; // Disable repo URL input
677
- githubTokenInput.disabled = isActive; // Disable token input
678
- userTypeSelect.disabled = isActive; // Disable user type select
679
- responseDetailSelect.disabled = isActive; // Disable detail select
680
-
681
  if (isActive) {
682
  // If activating, ensure listening mode is off
683
  if (isListening) {
684
  toggleListeningMode(); // Turn off listening visuals/state
685
  }
686
- agentStatusTitle.textContent = "G.E.N.I.E. Status: Fulfilling Wish...";
 
 
 
 
687
  micButton.title = "Interrupt G.E.N.I.E."; // Mic becomes interrupt
688
  } else {
689
  // Reset status title only if not currently listening
@@ -691,18 +961,19 @@
691
  agentStatusTitle.textContent = "G.E.N.I.E. Status";
692
  updateGenieText("Awaiting your command..."); // Reset output text
693
  }
694
- micButton.title = "Toggle Listening Mode / Interrupt"; // Reset mic title
695
-
 
696
  // Clear any lingering timeout or thinking message reference
697
  if (botResponseTimeoutId) {
698
  clearTimeout(botResponseTimeoutId);
699
  botResponseTimeoutId = null;
700
  }
 
701
  thinkingMessageElement = null;
702
  }
703
  }
704
 
705
- /** Checks if a string is a valid HTTP/HTTPS URL */
706
  function isValidHttpUrl(string) {
707
  try {
708
  const url = new URL(string);
@@ -712,28 +983,71 @@
712
  }
713
  }
714
 
715
- /** Shows a temporary red border and message on an input element */
716
- function showTemporaryAlert(element, message) {
717
- element.style.borderColor = '#ff6b6b'; // Red border
718
- element.style.boxShadow = '0 0 10px rgba(255, 107, 107, 0.4)';
719
- // Optionally, display the message near the element or use a dedicated alert area
720
- console.warn("Validation Error:", message); // Log for debugging
721
- // You could add a small temporary message element below the input
722
  setTimeout(() => {
723
- element.style.borderColor = ''; // Reset border color
724
- element.style.boxShadow = ''; // Reset box shadow
725
- }, 2500); // Reset after 2.5 seconds
726
- element.focus();
727
  }
728
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
729
 
730
- // --- Initial Setup ---
731
- updateGenieText("Standing by. Please provide a GitHub repo URL, set preferences, and state your wish, or click 🎙️.");
732
- interruptButton.classList.add('hidden'); // Start hidden
733
- sendButton.disabled = false; // Start enabled
734
- // Ensure initial state is not active
735
- setBotActiveState(false);
736
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
737
  </script>
738
  </body>
739
- </html>
 
221
  100% { transform: scale(1); }
222
  }
223
 
224
+ /* Audio visualizer */
225
+ .box-container {
226
+ display: flex;
227
+ justify-content: space-between;
228
+ height: 64px;
229
+ width: 100%;
230
+ margin-top: 1rem;
231
+ }
232
+ .box {
233
+ height: 100%;
234
+ width: 8px;
235
+ background: var(--color-accent, #6366f1);
236
+ border-radius: 8px;
237
+ transition: transform 0.05s ease;
238
+ }
239
+
240
  /* Responsive Design */
241
  /* Stack columns on smaller screens */
242
  .main-container {
 
312
  gap: 0.75rem; /* Space between grid items */
313
  }
314
 
315
+ /* Toast notification */
316
+ .toast {
317
+ position: fixed;
318
+ top: 20px;
319
+ left: 50%;
320
+ transform: translateX(-50%);
321
+ padding: 16px 24px;
322
+ border-radius: 4px;
323
+ font-size: 14px;
324
+ z-index: 1000;
325
+ display: none;
326
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
327
+ }
328
+ .toast.error {
329
+ background-color: #f44336;
330
+ color: white;
331
+ }
332
+ .toast.warning {
333
+ background-color: #ffd700;
334
+ color: black;
335
+ }
336
+ .toast.info {
337
+ background-color: #2196F3;
338
+ color: white;
339
+ }
340
  </style>
341
  </head>
342
+ <body class="text-sm md:text-base">
343
+ <!-- Toast notification element -->
344
+ <div id="toast" class="toast"></div>
345
 
346
+ <div class="main-container">
347
  <div class="chat-section">
348
  <div class="text-center mb-3 md:mb-4">
349
  <h1 class="text-2xl md:text-3xl font-bold text-purple-300">G.E.N.I.E.</h1>
350
  <p class="text-purple-100 text-xs md:text-sm">
351
  GitHub Enhanced Natural Intelligence Engine <br class="md:hidden">
352
  <span class="hidden md:inline"> - </span>
353
+ A voice-controlled AI that "grants your GitHub wishes."
354
  </p>
355
  </div>
356
 
 
387
  </div>
388
 
389
  <div class="status-section">
390
+ <div class="status-content"> <!-- Wrapper for centering content -->
391
  <h2 id="agentStatusTitle" class="text-lg md:text-xl font-semibold text-purple-400 text-center">G.E.N.I.E. Status</h2>
392
  <div id="genieOrb" class="genie-orb"></div>
393
  <button id="interruptButton" class="interrupt-button hidden">Interrupt G.E.N.I.E.</button>
394
+
395
+ <!-- Audio visualizer -->
396
+ <div class="glass-panel p-3 md:p-4 w-full">
397
+ <div class="box-container" id="audioVisualizer">
398
+ <!-- Bars will be added dynamically -->
399
+ </div>
400
+ </div>
401
+
402
  <div class="w-full glass-panel p-3 md:p-4 h-32 md:h-40 overflow-y-auto custom-scrollbar">
403
  <h3 class="text-base md:text-lg font-medium text-purple-300 mb-2 border-b border-purple-700 pb-1">G.E.N.I.E. Output:</h3>
404
  <p id="genieOutput" class="text-xs md:text-sm whitespace-pre-wrap">Awaiting your command...</p>
 
410
  <script>
411
  // --- DOM Elements ---
412
  const repoUrlInput = document.getElementById('repoUrl');
413
+ const githubTokenInput = document.getElementById('githubToken');
414
+ const userTypeSelect = document.getElementById('userType');
415
+ const responseDetailSelect = document.getElementById('responseDetail');
416
  const userQueryInput = document.getElementById('userQuery');
417
  const sendButton = document.getElementById('sendButton');
418
  const micButton = document.getElementById('micButton');
 
421
  const genieOrb = document.getElementById('genieOrb');
422
  const genieOutput = document.getElementById('genieOutput');
423
  const agentStatusTitle = document.getElementById('agentStatusTitle');
424
+ const toast = document.getElementById('toast');
425
+ const audioVisualizer = document.getElementById('audioVisualizer');
426
+ const audioOutput = new Audio();
427
 
428
  // --- State ---
429
  let isListening = false;
430
+ let isBotActive = false;
431
  let botResponseTimeoutId = null;
432
  let thinkingMessageElement = null;
433
+ let webSocket = null;
434
+ let audioContext = null;
435
+ let audioAnalyser = null;
436
+ let microphoneStream = null;
437
+ let microphoneProcessor = null;
438
+ let audioSequence = 0;
439
+ let isConnected = false;
440
+ let isGeminiResponding = false;
441
+
442
+ // --- WebSocket and Audio Setup ---
443
+ function setupWebSocket() {
444
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
445
+ const wsUrl = `${protocol}//${window.location.host}/ws`;
446
+
447
+ webSocket = new WebSocket(wsUrl);
448
+
449
+ webSocket.onopen = () => {
450
+ console.log("WebSocket connection established");
451
+ isConnected = true;
452
+ showToast("Connected to G.E.N.I.E. server", "info");
453
+
454
+ // Send initial preferences when connection is established
455
+ sendPreferences();
456
+ };
457
+
458
+ webSocket.onmessage = (event) => {
459
+ const message = JSON.parse(event.data);
460
+
461
+ if (message.type === "audio") {
462
+ // Convert base64 to audio buffer and play
463
+ playAudioFromServer(message.payload);
464
+
465
+ // Set Gemini as responding when we get audio data
466
+ if (!isGeminiResponding) {
467
+ isGeminiResponding = true;
468
+ setBotActiveState(true, false); // Active but not thinking
469
+ }
470
+ } else if (message.type === "text") {
471
+ // Handle text responses
472
+ addMessageToChat(message.content, 'bot');
473
+ updateGenieText(message.content);
474
+
475
+ // End of response
476
+ if (message.turn_complete) {
477
+ isGeminiResponding = false;
478
+ setBotActiveState(false);
479
+ }
480
+ } else if (message.type === "status") {
481
+ // Handle status updates
482
+ console.log("Status update:", message.status, message.message);
483
+
484
+ if (message.status === "interrupted") {
485
+ isGeminiResponding = false;
486
+ setBotActiveState(false);
487
+
488
+ // Remove thinking message if it exists
489
+ if (thinkingMessageElement) {
490
+ thinkingMessageElement.remove();
491
+ thinkingMessageElement = null;
492
+ }
493
+
494
+ // Add interrupted message
495
+ addMessageToChat(message.message, 'interrupted');
496
+ }
497
+ }
498
+ };
499
+
500
+ webSocket.onclose = () => {
501
+ console.log("WebSocket connection closed");
502
+ isConnected = false;
503
+
504
+ // Attempt to reconnect after a delay
505
+ setTimeout(() => {
506
+ if (!isConnected) {
507
+ console.log("Attempting to reconnect...");
508
+ setupWebSocket();
509
+ }
510
+ }, 3000);
511
+ };
512
+
513
+ webSocket.onerror = (error) => {
514
+ console.error("WebSocket error:", error);
515
+ showToast("Connection error. Please try again later.", "error");
516
+ };
517
+ }
518
 
519
+ // Setup audio visualizer
520
+ function setupAudioVisualizer() {
521
+ // Create bars for the audio visualizer
522
+ audioVisualizer.innerHTML = ''; // Clear existing bars
523
+ const numBars = 32;
524
+ for (let i = 0; i < numBars; i++) {
525
+ const bar = document.createElement('div');
526
+ bar.className = 'box';
527
+ audioVisualizer.appendChild(bar);
528
  }
529
+ }
530
+
531
+ // Initialize audio context for visualization and recording
532
+ async function setupAudioContext() {
533
+ try {
534
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
535
+ return true;
536
+ } catch (e) {
537
+ console.error("Error creating audio context:", e);
538
+ showToast("Could not initialize audio. Please check your browser permissions.", "error");
539
+ return false;
540
+ }
541
+ }
542
 
543
+ // Request microphone access and set up stream
544
+ async function setupMicrophone() {
545
+ try {
546
+ if (!audioContext) {
547
+ const success = await setupAudioContext();
548
+ if (!success) return false;
549
+ }
550
+
551
+ // Get user media
552
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
553
+ microphoneStream = stream;
554
+
555
+ // Create analyzer for input visualization
556
+ const source = audioContext.createMediaStreamSource(stream);
557
+ audioAnalyser = audioContext.createAnalyser();
558
+ audioAnalyser.fftSize = 256;
559
+ source.connect(audioAnalyser);
560
+
561
+ // Create processor for sending audio
562
+ microphoneProcessor = audioContext.createScriptProcessor(4096, 1, 1);
563
+ source.connect(microphoneProcessor);
564
+ microphoneProcessor.connect(audioContext.destination);
565
+
566
+ microphoneProcessor.onaudioprocess = (e) => {
567
+ if (isListening && isConnected) {
568
+ const audioData = e.inputBuffer.getChannelData(0);
569
+ sendAudioToServer(audioData);
570
+ }
571
+ };
572
+
573
+ // Start visualizing input
574
+ visualizeAudio();
575
+
576
+ return true;
577
+ } catch (e) {
578
+ console.error("Error accessing microphone:", e);
579
+ showToast("Could not access your microphone. Please check your browser permissions.", "error");
580
+ return false;
581
+ }
582
+ }
583
+
584
+ // Visualize audio input/output
585
+ function visualizeAudio() {
586
+ if (!audioAnalyser) return;
587
+
588
+ const bufferLength = audioAnalyser.frequencyBinCount;
589
+ const dataArray = new Uint8Array(bufferLength);
590
+
591
+ function draw() {
592
+ if (!audioAnalyser) return;
593
+
594
+ audioAnalyser.getByteFrequencyData(dataArray);
595
+ const bars = audioVisualizer.querySelectorAll('.box');
596
+
597
+ // Update each bar height based on frequency data
598
+ let barIndex = 0;
599
+ const barCount = bars.length;
600
+ const step = Math.floor(bufferLength / barCount) || 1;
601
+
602
+ for (let i = 0; i < bufferLength; i += step) {
603
+ if (barIndex >= barCount) break;
604
+
605
+ // Get average of frequency range
606
+ let sum = 0;
607
+ for (let j = 0; j < step && i + j < bufferLength; j++) {
608
+ sum += dataArray[i + j];
609
+ }
610
+ const average = sum / step;
611
+
612
+ // Scale height (0.1 minimum to always show something)
613
+ const barHeight = Math.max(0.1, average / 255);
614
+ bars[barIndex].style.transform = `scaleY(${barHeight})`;
615
+ barIndex++;
616
+ }
617
+
618
+ // Keep animating
619
+ requestAnimationFrame(draw);
620
+ }
621
+
622
+ draw();
623
+ }
624
+
625
+ // Send audio data to server
626
+ function sendAudioToServer(audioData) {
627
+ if (!webSocket || webSocket.readyState !== WebSocket.OPEN) return;
628
+
629
+ // Convert float32 to int16
630
+ const pcmData = new Int16Array(audioData.length);
631
+ for (let i = 0; i < audioData.length; i++) {
632
+ pcmData[i] = Math.max(-32768, Math.min(32767, audioData[i] * 32768));
633
+ }
634
+
635
+ // Convert to base64
636
+ const buffer = new ArrayBuffer(pcmData.length * 2);
637
+ const view = new DataView(buffer);
638
+ for (let i = 0; i < pcmData.length; i++) {
639
+ view.setInt16(i * 2, pcmData[i], true);
640
+ }
641
+
642
+ const base64Audio = btoa(String.fromCharCode(...new Uint8Array(buffer)));
643
+
644
+ // Send with sequence number for ordering
645
+ webSocket.send(JSON.stringify({
646
+ type: "audio",
647
+ payload: base64Audio,
648
+ seq: audioSequence++
649
+ }));
650
+ }
651
+
652
+ // Play audio received from server
653
+ function playAudioFromServer(base64Audio) {
654
+ const audioData = atob(base64Audio);
655
+ const buffer = new ArrayBuffer(audioData.length);
656
+ const view = new Uint8Array(buffer);
657
+
658
+ for (let i = 0; i < audioData.length; i++) {
659
+ view[i] = audioData.charCodeAt(i);
660
+ }
661
+
662
+ // Create blob and play
663
+ const blob = new Blob([buffer], { type: 'audio/wav' });
664
+ const url = URL.createObjectURL(blob);
665
+
666
+ // Queue audio
667
+ const audio = new Audio(url);
668
+ audio.onended = () => URL.revokeObjectURL(url); // Clean up
669
+ audio.play().catch(e => console.error("Error playing audio:", e));
670
+
671
+ // If we have AudioContext, create analyzer for visualization
672
+ if (audioContext && !audioAnalyser) {
673
+ try {
674
+ const audioSource = audioContext.createMediaElementSource(audio);
675
+ audioAnalyser = audioContext.createAnalyser();
676
+ audioAnalyser.fftSize = 256;
677
+ audioSource.connect(audioAnalyser);
678
+ audioSource.connect(audioContext.destination);
679
+
680
+ // Start visualization if not already running
681
+ visualizeAudio();
682
+ } catch (e) {
683
+ console.warn("Could not create audio analyzer for playback:", e);
684
+ }
685
+ }
686
+ }
687
+
688
+ // Send preferences to server
689
+ function sendPreferences() {
690
+ if (!webSocket || webSocket.readyState !== WebSocket.OPEN) return;
691
+
692
+ const repoUrl = repoUrlInput.value.trim();
693
+ const githubToken = githubTokenInput.value.trim();
694
+ const userType = userTypeSelect.value;
695
+ const responseDetail = responseDetailSelect.value;
696
+
697
+ webSocket.send(JSON.stringify({
698
+ type: "init",
699
+ repo_url: repoUrl,
700
+ github_token: githubToken,
701
+ user_type: userType,
702
+ response_detail: responseDetail
703
+ }));
704
+ }
705
 
706
+ // --- Event Handlers ---
707
+ async function handleMicButtonClick() {
708
  // If the bot is actively processing, the mic button acts as an interrupt
709
  if (isBotActive) {
710
  handleInterrupt();
711
+ return;
712
+ }
713
+
714
+ // Otherwise, toggle listening mode
715
+ const wasListening = isListening;
716
+ await toggleListeningMode();
717
+
718
+ // If we just started listening and there's no active message
719
+ // Send an empty audio data packet to initiate connection
720
+ if (!wasListening && isListening && !thinkingMessageElement) {
721
+ microphoneStream = null; // Force new stream setup
722
+ const success = await setupMicrophone();
723
+
724
+ if (!success) {
725
+ toggleListeningMode(); // Turn off listening if mic setup failed
726
+ }
727
  }
728
  }
729
 
730
+ async function toggleListeningMode() {
731
+ if (isBotActive && !isListening) return; // Don't start listening if bot is busy (unless already listening)
732
+
733
  isListening = !isListening;
734
  micButton.classList.toggle('listening', isListening);
735
  genieOrb.classList.toggle('listening', isListening);
736
+
737
  if (isListening) {
738
+ // Ensure WebSocket is connected
739
+ if (!isConnected) {
740
+ setupWebSocket();
741
+ }
742
+
743
+ // Setup microphone if not already done
744
+ if (!microphoneStream) {
745
+ const success = await setupMicrophone();
746
+ if (!success) {
747
+ isListening = false;
748
+ micButton.classList.remove('listening');
749
+ genieOrb.classList.remove('listening');
750
+ return;
751
+ }
752
+ }
753
+
754
  setBotActiveState(false); // Ensure bot is not marked active
755
  agentStatusTitle.textContent = "G.E.N.I.E. Status: Listening...";
756
+ updateGenieText("Listening... Speak your wish or type a question.");
 
757
  micButton.title = "Stop Listening / Interrupt"; // Update tooltip
758
  } else {
759
+ // Stop sending audio
760
+ micButton.title = "Toggle Listening Mode / Interrupt"; // Reset tooltip
761
+
762
+ // Only reset status if the bot isn't currently active
763
  if (!isBotActive) {
764
  agentStatusTitle.textContent = "G.E.N.I.E. Status";
765
  // Only reset output if it was showing the listening message
 
782
  if (isListening) {
783
  toggleListeningMode();
784
  }
785
+
786
  // Prevent sending if bot is already active
787
  if (isBotActive) {
788
+ console.log("G.E.N.I.E. is active, cannot send new query yet.");
789
+ return;
 
790
  }
791
+
792
  const repoUrl = repoUrlInput.value.trim();
793
  const query = userQueryInput.value.trim();
794
+
 
 
 
795
  // Basic Validations
796
  if (!query) {
797
+ showToast('Please state your wish (enter a query).', 'warning');
798
  return;
799
  }
800
+
801
  if (!isValidHttpUrl(repoUrl)) {
802
+ showToast('Please provide a valid GitHub repository URL (starting with http:// or https://).', 'warning');
803
+ return;
804
  }
805
+
806
+ // Ensure WebSocket is connected
807
+ if (!isConnected) {
808
+ setupWebSocket();
809
+ setTimeout(() => sendTextQuery(query), 500); // Short delay to allow connection
810
+ return;
811
+ }
812
+
813
+ sendTextQuery(query);
814
+ }
815
+
816
+ function sendTextQuery(query) {
817
  // Add user message to chat
818
  addMessageToChat(query, 'user');
819
  userQueryInput.value = ''; // Clear input field
820
+
821
+ // Add thinking message
822
+ thinkingMessageElement = addMessageToChat('', 'thinking');
823
+
824
+ // Send preferences again in case they've changed
825
+ sendPreferences();
826
+
827
+ // Send the query to the server
828
+ if (webSocket && webSocket.readyState === WebSocket.OPEN) {
829
+ webSocket.send(JSON.stringify({
830
+ type: "text",
831
+ content: query
832
+ }));
833
+ } else {
834
+ showToast("Connection to server lost. Please try again.", "error");
835
+
836
+ // Remove thinking message
837
  if (thinkingMessageElement) {
838
  thinkingMessageElement.remove();
839
  thinkingMessageElement = null;
840
  }
841
+
 
842
  setBotActiveState(false);
843
+ }
844
+ }
845
 
846
+ function handleInterrupt() {
847
+ // Send interrupt signal to server
848
+ if (webSocket && webSocket.readyState === WebSocket.OPEN) {
849
+ webSocket.send(JSON.stringify({
850
+ type: "interrupt"
851
+ }));
852
+ }
853
+
854
+ // Clear local UI state
855
+ if (botResponseTimeoutId) {
856
+ clearTimeout(botResponseTimeoutId);
857
+ botResponseTimeoutId = null;
858
+ }
859
+
860
+ // Remove the "thinking" message if it exists
861
+ if (thinkingMessageElement) {
862
+ thinkingMessageElement.remove();
863
+ thinkingMessageElement = null;
864
  }
865
+
866
+ // Reset bot state
867
+ isGeminiResponding = false;
868
+ setBotActiveState(false);
869
+
870
+ // Refocus input for convenience
871
+ userQueryInput.focus();
872
  }
873
 
874
  function addMessageToChat(message, type) {
875
  const messageElement = document.createElement('div');
876
  messageElement.classList.add('chat-bubble');
877
+
878
  // Stop listening visual cues if a message is added
879
+ if (isListening && type !== 'thinking') {
880
  genieOrb.classList.remove('listening');
881
  micButton.classList.remove('listening');
882
  isListening = false; // Ensure state is updated
883
  micButton.title = "Toggle Listening Mode / Interrupt"; // Reset tooltip
884
  }
885
+
886
  switch (type) {
887
  case 'user':
888
  messageElement.classList.add('user-bubble');
 
892
  messageElement.classList.add('bot-bubble');
893
  messageElement.textContent = message;
894
  updateGenieText(message); // Update status output
895
+
896
+ // Reset the thinking message
897
+ if (thinkingMessageElement) {
898
+ thinkingMessageElement.remove();
899
+ thinkingMessageElement = null;
900
+ }
901
+
902
+ // After a bot message, ensure we're not in "thinking" mode
903
+ setBotActiveState(true, false); // Active but not thinking
904
  break;
905
  case 'thinking':
906
  messageElement.classList.add('bot-bubble', 'thinking');
907
  messageElement.innerHTML = `Consulting the digital ether... <div class="dot-flashing"></div>`;
908
  updateGenieText("Processing your wish..."); // Update status output
909
+ setBotActiveState(true, true); // Active and thinking
910
  thinkingMessageElement = messageElement; // Store reference to remove later
911
  break;
912
  case 'interrupted':
913
+ messageElement.classList.add('bot-bubble', 'interrupted');
914
+ messageElement.textContent = message;
915
+ break;
 
916
  default:
917
  console.error("Unknown message type:", type);
918
  return null; // Don't add unknown types
919
  }
920
+
921
  chatHistory.appendChild(messageElement);
922
  // Scroll to the bottom smoothly after adding message
 
923
  setTimeout(() => {
924
  chatHistory.scrollTo({ top: chatHistory.scrollHeight, behavior: 'smooth' });
925
  }, 50);
926
+
927
  return messageElement; // Return the created element
928
  }
929
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
930
  function updateGenieText(text) {
931
  genieOutput.textContent = text;
932
  }
933
 
934
+ function setBotActiveState(isActive, isThinking = true) {
 
935
  isBotActive = isActive;
936
+
937
+ // Always update these elements
938
  genieOrb.classList.toggle('active', isActive);
939
  interruptButton.classList.toggle('hidden', !isActive);
940
+ sendButton.disabled = isActive;
941
+ userQueryInput.disabled = isActive;
942
+ repoUrlInput.disabled = isActive;
943
+ githubTokenInput.disabled = isActive;
944
+ userTypeSelect.disabled = isActive;
945
+ responseDetailSelect.disabled = isActive;
946
+
947
  if (isActive) {
948
  // If activating, ensure listening mode is off
949
  if (isListening) {
950
  toggleListeningMode(); // Turn off listening visuals/state
951
  }
952
+
953
+ agentStatusTitle.textContent = isThinking ?
954
+ "G.E.N.I.E. Status: Fulfilling Wish..." :
955
+ "G.E.N.I.E. Status: Active";
956
+
957
  micButton.title = "Interrupt G.E.N.I.E."; // Mic becomes interrupt
958
  } else {
959
  // Reset status title only if not currently listening
 
961
  agentStatusTitle.textContent = "G.E.N.I.E. Status";
962
  updateGenieText("Awaiting your command..."); // Reset output text
963
  }
964
+
965
+ micButton.title = "Toggle Listening Mode / Interrupt"; // Reset mic title
966
+
967
  // Clear any lingering timeout or thinking message reference
968
  if (botResponseTimeoutId) {
969
  clearTimeout(botResponseTimeoutId);
970
  botResponseTimeoutId = null;
971
  }
972
+
973
  thinkingMessageElement = null;
974
  }
975
  }
976
 
 
977
  function isValidHttpUrl(string) {
978
  try {
979
  const url = new URL(string);
 
983
  }
984
  }
985
 
986
+ function showToast(message, type = 'error') {
987
+ toast.textContent = message;
988
+ toast.className = `toast ${type}`;
989
+ toast.style.display = 'block';
990
+
991
+ // Hide toast after 5 seconds
 
992
  setTimeout(() => {
993
+ toast.style.display = 'none';
994
+ }, 5000);
 
 
995
  }
996
 
997
+ // --- Event Listeners ---
998
+ sendButton.addEventListener('click', handleSendQuery);
999
+ userQueryInput.addEventListener('keypress', (event) => {
1000
+ if (event.key === 'Enter' && !event.shiftKey) {
1001
+ event.preventDefault(); // Prevent default newline behavior
1002
+ handleSendQuery();
1003
+ }
1004
+ });
1005
+ userQueryInput.addEventListener('input', handleTypingInterrupt);
1006
+ micButton.addEventListener('click', handleMicButtonClick);
1007
+ interruptButton.addEventListener('click', handleInterrupt);
1008
+
1009
+ // Listen for preference changes to update server
1010
+ repoUrlInput.addEventListener('change', sendPreferences);
1011
+ githubTokenInput.addEventListener('change', sendPreferences);
1012
+ userTypeSelect.addEventListener('change', sendPreferences);
1013
+ responseDetailSelect.addEventListener('change', sendPreferences);
1014
+
1015
+ // --- Initialization ---
1016
+ function init() {
1017
+ // Set up WebSocket connection
1018
+ setupWebSocket();
1019
+
1020
+ // Set up audio context and visualizer
1021
+ setupAudioVisualizer();
1022
+
1023
+ // Initialize state
1024
+ updateGenieText("Standing by. Please provide a GitHub repo URL, set preferences, and state your wish, or click 🎙️.");
1025
+ interruptButton.classList.add('hidden');
1026
+ sendButton.disabled = false;
1027
+ setBotActiveState(false);
1028
+ }
1029
 
1030
+ // Start initialization
1031
+ init();
 
 
 
 
1032
 
1033
+ // When page is closing, clean up WebSocket and audio resources
1034
+ window.addEventListener('beforeunload', () => {
1035
+ if (webSocket) {
1036
+ webSocket.close();
1037
+ }
1038
+
1039
+ if (microphoneStream) {
1040
+ microphoneStream.getTracks().forEach(track => track.stop());
1041
+ }
1042
+
1043
+ if (microphoneProcessor) {
1044
+ microphoneProcessor.disconnect();
1045
+ }
1046
+
1047
+ if (audioContext) {
1048
+ audioContext.close();
1049
+ }
1050
+ });
1051
  </script>
1052
  </body>
1053
+ </html>