diff --git a/.github/actions/setup-python-playwright/action.yml b/.github/actions/setup-python-playwright/action.yml
new file mode 100644
index 0000000..a0fac48
--- /dev/null
+++ b/.github/actions/setup-python-playwright/action.yml
@@ -0,0 +1,23 @@
+name: Setup Python Playwright Environment
+description: Install Python dependencies and Playwright browsers for testing.
+inputs:
+ python-version:
+ description: Python version to install.
+ required: false
+ default: '3.11'
+runs:
+ using: composite
+ steps:
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ inputs.python-version }}
+ - name: Install Python dependencies
+ shell: bash
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+ - name: Install Playwright browsers
+ shell: bash
+ run: |
+ python -m playwright install --with-deps chromium
diff --git a/.github/scripts/build_static.py b/.github/scripts/build_static.py
index b6a9288..95ed76f 100755
--- a/.github/scripts/build_static.py
+++ b/.github/scripts/build_static.py
@@ -38,6 +38,7 @@ def main() -> int:
dist.mkdir(parents=True, exist_ok=True)
result = {"status": "success", "copied": []}
+ exit_code = 0
try:
result["copied"] = copy_static_files(dist)
if not result["copied"]:
@@ -50,8 +51,9 @@ def main() -> int:
result["status"] = "failure"
result["message"] = str(exc)
print(f"::error::Static site build failed: {exc}")
+ exit_code = 1
Path("build-results.json").write_text(json.dumps(result))
- return 0
+ return exit_code
if __name__ == "__main__":
raise SystemExit(main())
diff --git a/.github/scripts/run_tests.py b/.github/scripts/run_tests.py
index 064be2a..192d954 100755
--- a/.github/scripts/run_tests.py
+++ b/.github/scripts/run_tests.py
@@ -44,7 +44,7 @@ def main() -> int:
else:
print("All tests passed.")
- return 0
+ return exit_code
if __name__ == "__main__":
diff --git a/.github/scripts/write_summary.py b/.github/scripts/write_summary.py
new file mode 100755
index 0000000..25479d6
--- /dev/null
+++ b/.github/scripts/write_summary.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python3
+"""Aggregate CI results into a GitHub Actions step summary."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+from pathlib import Path
+from typing import Any
+
+
+def load_json(path: Path) -> dict[str, Any] | None:
+ if not path.exists() or not path.is_file():
+ return None
+ try:
+ return json.loads(path.read_text())
+ except json.JSONDecodeError as exc: # noqa: TRY003
+ print(f"::warning::Unable to parse JSON from {path}: {exc}")
+ return None
+
+
+def render_tests_section(data: dict[str, Any] | None) -> list[str]:
+ lines = ["## Test Results", ""]
+ if not data:
+ lines.append("No test results were produced.")
+ return lines
+
+ tests = data.get("tests") or []
+ if not tests:
+ lines.append("No tests were discovered under /tests.")
+ return lines
+
+ lines.append(f"Processed {len(tests)} test file(s).")
+ lines.append("")
+ lines.append("| Test file | Status |")
+ lines.append("| --- | --- |")
+ for entry in tests:
+ name = entry.get("name", "unknown")
+ status = (entry.get("status") or "unknown").lower()
+ if status == "passed":
+ emoji = "✅"
+ elif status == "failed":
+ emoji = "❌"
+ else:
+ emoji = "⚠️"
+ lines.append(f"| {name} | {emoji} {status.title()} |")
+ return lines
+
+
+def render_build_section(data: dict[str, Any] | None) -> list[str]:
+ lines = ["## Build Status", ""]
+ if not data:
+ lines.append("No build output was produced.")
+ return lines
+
+ status = (data.get("status") or "unknown").lower()
+ message = data.get("message")
+ copied = data.get("copied") or []
+
+ if status == "success":
+ emoji = "✅"
+ elif status == "warning":
+ emoji = "⚠️"
+ else:
+ emoji = "❌"
+ lines.append(f"Overall build result: {emoji} {status.title()}")
+
+ if copied:
+ lines.append("")
+ lines.append("### Copied static files")
+ for item in copied:
+ lines.append(f"- {item}")
+
+ if message:
+ lines.append("")
+ lines.append(f"Details: {message}")
+
+ return lines
+
+
+def append_summary(sections: list[list[str]]) -> None:
+ summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
+ text = "\n".join(line for section in sections for line in (*section, ""))
+ print(text)
+ if summary_path:
+ with open(summary_path, "a", encoding="utf-8") as handle:
+ handle.write(text + "\n")
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description="Aggregate CI results into a GitHub Actions step summary.",
+ )
+ parser.add_argument(
+ "--include-tests",
+ action="store_true",
+ help="Include the tests section in the summary.",
+ )
+ parser.add_argument(
+ "--include-build",
+ action="store_true",
+ help="Include the build section in the summary.",
+ )
+ args = parser.parse_args()
+ if not args.include_tests and not args.include_build:
+ args.include_tests = True
+ args.include_build = True
+ return args
+
+
+def main() -> None:
+ args = parse_args()
+ repo_root = Path(".")
+ tests_data = load_json(repo_root / "test-results.json")
+ build_data = load_json(repo_root / "build-results.json")
+
+ sections: list[list[str]] = []
+ if args.include_tests:
+ sections.append(render_tests_section(tests_data))
+ if args.include_build and (build_data or (repo_root / "build-results.json").exists()):
+ sections.append(render_build_section(build_data))
+
+ append_summary(sections)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.github/workflows/main-branch.yml b/.github/workflows/main-branch.yml
index 2e15c9e..1bb95e3 100644
--- a/.github/workflows/main-branch.yml
+++ b/.github/workflows/main-branch.yml
@@ -12,24 +12,35 @@ permissions:
concurrency:
group: pages
- cancel-in-progress: false
+ cancel-in-progress: true
jobs:
- build:
+ build_artifacts:
name: Build and Upload Artifacts
runs-on: ubuntu-latest
+ outputs:
+ status: ${{ steps.collect.outputs.status }}
+ results: ${{ steps.collect.outputs.results }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
+
+ - name: Set up Python environment
+ uses: ./.github/actions/setup-python-playwright
+
- name: Build static site
- run: |
- python .github/scripts/build_static.py
+ id: build-site
+ continue-on-error: true
+ run: python .github/scripts/build_static.py
+
- name: Upload Pages artifact
+ if: steps.build-site.outcome == 'success'
uses: actions/upload-pages-artifact@v3
with:
path: dist
- - name: Prepare build status output
- id: prepare_build_status
+
+ - name: Collect build metadata
+ id: collect
run: |
python - <<'PY'
import base64
@@ -37,80 +48,78 @@ jobs:
import os
from pathlib import Path
- data = json.loads(Path('build-results.json').read_text())
- encoded = base64.b64encode(json.dumps(data).encode()).decode()
- with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as fh:
- fh.write(f"encoded={encoded}\n")
+ path = Path("build-results.json")
+ data: dict[str, object] = {}
+ status = "missing"
+ if path.exists():
+ data = json.loads(path.read_text())
+ status = data.get("status", "unknown")
+ encoded = base64.b64encode(json.dumps(data).encode("utf-8")).decode("utf-8")
+ with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as handle:
+ handle.write(f"status={status}\n")
+ handle.write(f"results={encoded}\n")
PY
- outputs:
- status: ${{ steps.prepare_build_status.outputs.encoded }}
- report-build:
+ report_build_status:
name: Report Build Status
- needs: build
runs-on: ubuntu-latest
+ needs: build_artifacts
if: always()
steps:
- - name: Report Build Status
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Restore build results
+ env:
+ BUILD_RESULTS_B64: ${{ needs.build_artifacts.outputs.results }}
run: |
python - <<'PY'
import base64
import json
import os
+ from pathlib import Path
- encoded = "${{ needs.build.outputs.status }}"
- if not encoded:
- report = ["## Build Status", "", "No build status information was captured."]
- else:
- data = json.loads(base64.b64decode(encoded).decode())
- status = data.get('status', 'unknown').lower()
- emoji = '✅' if status == 'success' else ('⚠️' if status == 'warning' else '❌')
- details = data.get('message')
- copied = data.get('copied', [])
- report = [
- "## Build Status",
- "",
- f"Overall build result: {emoji} {status.title()}",
- ]
- if copied:
- report.append("")
- report.append("### Copied static files")
- for item in copied:
- report.append(f"- {item}")
- if details:
- report.append("")
- report.append(f"Details: {details}")
-
- summary_path = os.environ.get('GITHUB_STEP_SUMMARY')
- if summary_path:
- with open(summary_path, 'a', encoding='utf-8') as fh:
- fh.write("\n".join(report) + "\n")
- print("\n".join(report))
+ raw = os.environ.get("BUILD_RESULTS_B64", "")
+ data: dict[str, object] = {}
+ if raw:
+ try:
+ decoded = base64.b64decode(raw.encode("utf-8")).decode("utf-8")
+ if decoded:
+ data = json.loads(decoded)
+ except Exception as exc: # noqa: BLE001
+ data = {"status": "unknown", "message": f"Unable to decode build results: {exc}"}
+ Path("build-results.json").write_text(json.dumps(data))
PY
- run-tests:
+ - name: Report Build Status
+ run: python .github/scripts/write_summary.py --include-build
+
+ - name: Fail if build failed
+ if: needs.build_artifacts.outputs.status == 'failure'
+ run: exit 1
+
+ tests:
name: Run Tests
- needs: build
runs-on: ubuntu-latest
+ needs: build_artifacts
+ if: always()
+ outputs:
+ status: ${{ steps.collect.outputs.status }}
+ results: ${{ steps.collect.outputs.results }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: '3.11'
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt pytest
- - name: Install Playwright browsers
- run: |
- python -m playwright install --with-deps chromium
- - name: Execute tests sequentially
- run: |
- python .github/scripts/run_tests.py
- - name: Prepare test results output
- id: prepare_results
+
+ - name: Set up Python environment
+ uses: ./.github/actions/setup-python-playwright
+
+ - name: Run Tests
+ id: run-tests
+ continue-on-error: true
+ run: python .github/scripts/run_tests.py
+
+ - name: Collect test metadata
+ id: collect
run: |
python - <<'PY'
import base64
@@ -118,60 +127,61 @@ jobs:
import os
from pathlib import Path
- data = json.loads(Path('test-results.json').read_text())
- encoded = base64.b64encode(json.dumps(data).encode()).decode()
- with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as fh:
- fh.write(f"encoded={encoded}\n")
+ path = Path("test-results.json")
+ data: dict[str, object] = {}
+ status = "missing"
+ if path.exists():
+ data = json.loads(path.read_text())
+ status = data.get("overall_status", "unknown")
+ encoded = base64.b64encode(json.dumps(data).encode("utf-8")).decode("utf-8")
+ with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as handle:
+ handle.write(f"status={status}\n")
+ handle.write(f"results={encoded}\n")
PY
- outputs:
- results: ${{ steps.prepare_results.outputs.encoded }}
- report-tests:
+ report_tests_statuses:
name: Report Tests Statuses
- needs: run-tests
runs-on: ubuntu-latest
+ needs: tests
if: always()
steps:
- - name: Report Tests Statuses
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Restore test results
+ env:
+ TEST_RESULTS_B64: ${{ needs.tests.outputs.results }}
run: |
python - <<'PY'
import base64
import json
import os
+ from pathlib import Path
- encoded = "${{ needs.run-tests.outputs.results }}"
- if not encoded:
- summary = "No test results were produced."
- table_lines = []
- else:
- data = json.loads(base64.b64decode(encoded).decode())
- tests = data.get('tests', [])
- if not tests:
- summary = "No tests were discovered under /tests."
- table_lines = []
- else:
- summary = f"Processed {len(tests)} test(s)."
- table_lines = ["| Test | Status |", "| --- | --- |"]
- for item in tests:
- status = item.get('status', 'unknown').lower()
- emoji = '✅' if status == 'passed' else ('❌' if status == 'failed' else '⚠️')
- table_lines.append(f"| {item.get('name', 'unknown')} | {emoji} {status.title()} |")
-
- report_lines = ["## Main Branch Test Results", "", summary]
- if table_lines:
- report_lines.extend(["", *table_lines])
-
- summary_path = os.environ.get('GITHUB_STEP_SUMMARY')
- if summary_path:
- with open(summary_path, 'a', encoding='utf-8') as fh:
- fh.write("\n".join(report_lines) + "\n")
- print("\n".join(report_lines))
+ raw = os.environ.get("TEST_RESULTS_B64", "")
+ data: dict[str, object] = {}
+ if raw:
+ try:
+ decoded = base64.b64decode(raw.encode("utf-8")).decode("utf-8")
+ if decoded:
+ data = json.loads(decoded)
+ except Exception as exc: # noqa: BLE001
+ data = {"tests": [], "overall_status": "failed", "message": f"Unable to decode test results: {exc}"}
+ Path("test-results.json").write_text(json.dumps(data))
PY
+ - name: Report Tests Statuses
+ run: python .github/scripts/write_summary.py --include-tests
+
+ - name: Fail if tests failed
+ if: needs.tests.outputs.status == 'failed'
+ run: exit 1
+
deploy:
name: Deploy to Pages
- needs: run-tests
runs-on: ubuntu-latest
+ needs: build_artifacts
+ if: needs.build_artifacts.outputs.status == 'success'
environment:
name: github-pages
steps:
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 55db2c7..2574fa8 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -12,75 +12,15 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: '3.11'
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt pytest
- - name: Install Playwright browsers
- run: |
- python -m playwright install --with-deps chromium
- - name: Execute tests sequentially
- run: |
- python .github/scripts/run_tests.py
- - name: Prepare test results output
- id: prepare_results
- run: |
- python - <<'PY'
- import base64
- import json
- import os
- from pathlib import Path
- data = json.loads(Path('test-results.json').read_text())
- encoded = base64.b64encode(json.dumps(data).encode()).decode()
- with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as fh:
- fh.write(f"encoded={encoded}\n")
- PY
- outputs:
- results: ${{ steps.prepare_results.outputs.encoded }}
+ - name: Set up Python environment
+ uses: ./.github/actions/setup-python-playwright
- report:
- name: Report Tests Statuses
- needs: run-tests
- runs-on: ubuntu-latest
- if: always()
- steps:
- - name: Report Tests Statuses
- run: |
- python - <<'PY'
- import base64
- import json
- import os
+ - name: Run Tests
+ id: run-tests
+ continue-on-error: true
+ run: python .github/scripts/run_tests.py
- encoded = "${{ needs.run-tests.outputs.results }}"
- if not encoded:
- summary = "No test results were produced."
- table_lines = []
- else:
- data = json.loads(base64.b64decode(encoded).decode())
- tests = data.get('tests', [])
- if not tests:
- summary = "No tests were discovered under /tests."
- table_lines = []
- else:
- summary = f"Processed {len(tests)} test(s)."
- table_lines = ["| Test | Status |", "| --- | --- |"]
- for item in tests:
- status = item.get('status', 'unknown').lower()
- emoji = '✅' if status == 'passed' else ('❌' if status == 'failed' else '⚠️')
- table_lines.append(f"| {item.get('name', 'unknown')} | {emoji} {status.title()} |")
-
- report_lines = ["## Pull Request Test Results", "", summary]
- if table_lines:
- report_lines.extend(["", *table_lines])
-
- summary_path = os.environ.get('GITHUB_STEP_SUMMARY')
- if summary_path:
- with open(summary_path, 'a', encoding='utf-8') as fh:
- fh.write("\n".join(report_lines) + "\n")
- print("\n".join(report_lines))
- PY
+ - name: Report Tests Statuses
+ if: always()
+ run: python .github/scripts/write_summary.py --include-tests
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..635bd8d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+# Build artifacts
+/dist/
+/build-results.json
+/test-results.json
+
+# Python cache
+__pycache__/
+*.pyc
+
+# Playwright artifacts
+/playwright-report/
+/.pytest_cache/
diff --git a/AI/README.md b/AI/README.md
deleted file mode 100644
index 8533f8f..0000000
--- a/AI/README.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Talk to Unity
-
-Talk to Unity is a browser-based voice companion that connects visitors with the Unity AI Lab experience. The project ships as a static site, so it can be hosted on GitHub Pages or any web server that can serve HTML, CSS, and JavaScript.
-
-## What you get
-
-- A guided landing page with live dependency checks for secure context, speech recognition, speech synthesis, and microphone access.
-- A voice-first interface where users can unmute the microphone, talk to Unity, and hear spoken responses.
-- Lightweight assets that run fully in the browser with no additional backend services required.
-
-## Requirements
-
-To enjoy the full experience make sure you have:
-
-- A modern browser with support for the Web Speech APIs (Chrome or Edge work best, Safari on desktop also supports the features).
-- HTTPS hosting or `localhost` so the secure-context check passes.
-- Speakers or headphones so you can hear Unity’s replies.
-- A microphone that the browser is allowed to access.
-
-## Running locally
-
-1. Clone this repository.
- `git clone https://github.com/Unity-Lab-AI/Talk-to-Unity.git`
-2. Serve the files from a local web server so the secure-context requirement is satisfied. Any simple static server works, for example:
- `python -m http.server 8000`
-3. Visit `http://localhost:8000` in a compatible browser, open `index.html`, and follow the landing page instructions to launch the voice lab.
-
-## Project structure
-
-- `index.html` – landing page markup and the application shell.
-- `style.css` – theme styles for both the landing page and the voice interface.
-- `app.js` – dependency checks, voice controls, and Unity interaction logic.
-- `ai-instruct.txt` – system prompt loaded by the application.
-
-## Helpful links
-
-- Unity AI Lab home: https://unityailab.online
-- Repository on GitHub: https://github.com/Unity-Lab-AI/Talk-to-Unity
-
-Feel free to customize the landing copy, extend the dependency checks, or swap in different voice prompts to match your own deployment.
diff --git a/AI/ai-instruct.txt b/AI/ai-instruct.txt
deleted file mode 100644
index ebc0206..0000000
--- a/AI/ai-instruct.txt
+++ /dev/null
@@ -1 +0,0 @@
-You are Unity, a helpful AI assistant.
\ No newline at end of file
diff --git a/AI/app.js b/AI/app.js
index 96d8bf8..f8faf41 100644
--- a/AI/app.js
+++ b/AI/app.js
@@ -1,4 +1,12 @@
(() => {
+ if (typeof window !== 'undefined') {
+ window.addEventListener('error', (event) => {
+ if (event?.message) {
+ console.error('Talk to Unity runtime error:', event.message, event.error?.stack || '');
+ }
+ });
+ }
+
const landingSection = document.getElementById('landing');
const appRoot = document.getElementById('app-root');
const heroStage = document.getElementById('hero-stage');
@@ -49,6 +57,7 @@
const synth = typeof window !== 'undefined' ? window.speechSynthesis : undefined;
let compatibilityBanner = null;
let dependencyState = { results: [], allMet: false, missing: [] };
+ let unmuteInProgress = false;
const dependencyChecks = [
{
@@ -415,12 +424,12 @@
}
applyTheme(currentTheme);
+ setupSpeechRecognition();
if (!systemPromptLoaded) {
await loadSystemPrompt();
}
- setupSpeechRecognition();
updateMuteIndicator();
if (auto) {
@@ -459,16 +468,17 @@
if (announce) {
speak('Microphone muted.');
}
- } else {
- setCircleState(userCircle, {
- listening: true,
- label: 'Listening for your voice'
- });
- if (announce) {
- speak('Microphone unmuted.');
- }
+ return true;
}
- return;
+
+ setCircleState(userCircle, {
+ listening: true,
+ label: 'Listening for your voice'
+ });
+ if (announce) {
+ speak('Microphone unmuted.');
+ }
+ return true;
}
if (muted) {
@@ -493,7 +503,7 @@
speak('Microphone muted.');
}
- return;
+ return true;
}
if (!hasMicPermission) {
@@ -503,7 +513,7 @@
if (announce) {
speak('Microphone permission is required to unmute.');
}
- return;
+ return false;
}
}
@@ -517,7 +527,7 @@
if (announce) {
speak('Microphone is already listening.');
}
- return;
+ return true;
}
isMuted = false;
@@ -541,12 +551,14 @@
if (announce) {
speak('Unable to start microphone recognition.');
}
- return;
+ return false;
}
if (announce) {
speak('Microphone unmuted.');
}
+
+ return true;
}
function applyTheme(theme, { announce = false, force = false } = {}) {
@@ -611,15 +623,24 @@
return;
}
- try {
- const response = await fetch(resolveAssetPath('ai-instruct.txt'));
- systemPrompt = await response.text();
- } catch (error) {
- console.error('Error fetching system prompt:', error);
- systemPrompt = 'You are Unity, a helpful AI assistant.';
- } finally {
- systemPromptLoaded = true;
+ const candidatePaths = ['../ai-instruct.txt', 'ai-instruct.txt'];
+ for (const path of candidatePaths) {
+ try {
+ const response = await fetch(resolveAssetPath(path));
+ if (!response.ok) {
+ continue;
+ }
+
+ systemPrompt = await response.text();
+ systemPromptLoaded = true;
+ return;
+ } catch (error) {
+ console.error('Error fetching system prompt from', path, error);
+ }
}
+
+ systemPrompt = 'You are Unity, a helpful AI assistant.';
+ systemPromptLoaded = true;
}
function setupSpeechRecognition() {
@@ -743,6 +764,14 @@
label: `Microphone error: ${event.error}`
});
};
+
+ if (!isMuted) {
+ try {
+ recognition.start();
+ } catch (error) {
+ console.error('Failed to start recognition after initialization:', error);
+ }
+ }
}
async function initializeVoiceControl() {
@@ -845,33 +874,64 @@
}
}
- async function attemptUnmute() {
- await setMutedState(false);
+ function showMicrophonePermissionRequest() {
+ if (!muteIndicator) {
+ return;
+ }
+
+ muteIndicator.dataset.state = 'pending';
+ muteIndicator.setAttribute('aria-label', 'Requesting microphone permission…');
+ if (indicatorText) {
+ indicatorText.textContent = 'Requesting microphone permission…';
+ }
+ }
+
+ async function attemptUnmute({ announce = false } = {}) {
+ if (unmuteInProgress) {
+ return;
+ }
+
+ const shouldShowProgress = isMuted && !hasMicPermission;
+ if (shouldShowProgress) {
+ showMicrophonePermissionRequest();
+ }
+
+ unmuteInProgress = true;
+ try {
+ await setMutedState(false, { announce });
+ } finally {
+ unmuteInProgress = false;
+ updateMuteIndicator();
+ }
}
function handleMuteToggle(event) {
event?.stopPropagation();
if (isMuted) {
- attemptUnmute();
+ void attemptUnmute();
return;
}
- setMutedState(true);
+ void setMutedState(true);
}
muteIndicator?.addEventListener('click', handleMuteToggle);
- document.addEventListener('click', () => {
- if (isMuted) {
- attemptUnmute();
+ const handleAmbientUnmute = () => {
+ if (!isMuted || unmuteInProgress) {
+ return;
}
- });
+
+ void attemptUnmute();
+ };
+
+ document.addEventListener('click', handleAmbientUnmute);
document.addEventListener('keydown', (event) => {
- if ((event.key === 'Enter' || event.key === ' ') && isMuted) {
+ if ((event.key === 'Enter' || event.key === ' ') && isMuted && !unmuteInProgress) {
event.preventDefault();
- attemptUnmute();
+ void attemptUnmute();
}
});
@@ -929,12 +989,8 @@
.replace(/\ \[^]]*\bcommand\b[^]]*\]/gi, ' ')
.replace(/\([^)]*\bcommand\b[^)]*\)/gi, ' ')
.replace(/<[^>]*\bcommand\b[^>]*>/gi, ' ')
- .replace(/\bcommands?\s*[:=-]\s*[a-z0-9_,
-\s-]+
-/gi, ' ')
- .replace(/\bactions?\s*[:=-]\s*[a-z0-9_,
-\s-]+
-/gi, ' ')
+ .replace(/\bcommands?\s*[:=-]\s*[a-z0-9_,\s-]+/gi, ' ')
+ .replace(/\bactions?\s*[:=-]\s*[a-z0-9_,\s-]+/gi, ' ')
.replace(/\b(?:execute|run)\s+command\s*(?:[:=-]\s*)?[a-z0-9_-]*/gi, ' ')
.replace(/\bcommand\s*(?:[:=-]\s*|\s+)(?:[a-z0-9_-]+(?:\s+[a-z0-9_-]+)*)?/gi, ' ');
@@ -1173,7 +1229,7 @@
}
function escapeRegExp(value) {
- return value.replace(/[-/\\^$*+?.()|[\{\}]/g, '\\$&');
+ return value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
}
function removeImageReferences(text, imageUrl) {
@@ -1757,4 +1813,13 @@
window.open(imageUrl, '_blank');
speak('Image opened in new tab.');
}
+
+ if (typeof window !== 'undefined') {
+ Object.assign(window, {
+ applyTheme,
+ setMutedState,
+ startApplication,
+ attemptUnmute
+ });
+ }
})();
diff --git a/AI/index.html b/AI/index.html
index dad0f83..325bd22 100644
--- a/AI/index.html
+++ b/AI/index.html
@@ -13,17 +13,25 @@
-
+