diff --git a/.github/workflows/main-branch-pipeline.yml b/.github/workflows/main-branch-pipeline.yml
new file mode 100644
index 0000000..aa0490e
--- /dev/null
+++ b/.github/workflows/main-branch-pipeline.yml
@@ -0,0 +1,176 @@
+name: Main Branch Pipeline
+
+on:
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: main-branch-pipeline
+ cancel-in-progress: false
+
+jobs:
+ build:
+ name: Build and Upload Artifacts
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build static files
+ run: npm run build
+
+ - name: Upload build artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: site-dist
+ path: dist
+ if-no-files-found: error
+
+ - name: Prepare GitHub Pages artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: dist
+
+ report-build:
+ name: Report Build Status
+ runs-on: ubuntu-latest
+ needs: build
+ if: always()
+ steps:
+ - name: Summarize build outcome
+ run: |
+ result="${{ needs.build.result }}"
+ echo "### Main Branch Build Status" >> "$GITHUB_STEP_SUMMARY"
+ if [ "$result" = "success" ]; then
+ echo "- ✅ Build and artifact upload completed successfully." >> "$GITHUB_STEP_SUMMARY"
+ else
+ echo "- ❌ Build or artifact upload failed. Inspect the build job logs." >> "$GITHUB_STEP_SUMMARY"
+ fi
+
+ tests:
+ name: Run Tests
+ runs-on: ubuntu-latest
+ needs: build
+ if: ${{ needs.build.result == 'success' }}
+ outputs:
+ status: ${{ steps.run-tests.outputs.status }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run Pollinations tests
+ id: run-tests
+ run: |
+ mkdir -p reports
+ set +e
+ npm run test:pollinations > reports/main-tests.log 2>&1
+ exit_code=$?
+ set -e
+ if [ "$exit_code" -ne 0 ]; then
+ status="failed"
+ else
+ status="passed"
+ fi
+ echo "$status" > reports/status.txt
+ echo "$exit_code" > reports/exit-code.txt
+ echo "status=$status" >> "$GITHUB_OUTPUT"
+ exit 0
+
+ - name: Upload test artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: main-tests
+ path: reports
+ if-no-files-found: warn
+
+ report-tests:
+ name: Report Tests Statuses
+ runs-on: ubuntu-latest
+ needs:
+ - tests
+ - build
+ if: always()
+ steps:
+ - name: Download test artifacts
+ if: ${{ needs.tests.result != 'skipped' }}
+ uses: actions/download-artifact@v4
+ with:
+ name: main-tests
+ path: reports
+ continue-on-error: true
+
+ - name: Summarize test outcome
+ run: |
+ build_result="${{ needs.build.result }}"
+ test_result="${{ needs.tests.result }}"
+ status="${{ needs.tests.outputs.status }}"
+
+ echo "### Main Branch Test Statuses" >> "$GITHUB_STEP_SUMMARY"
+
+ if [ "$build_result" != "success" ]; then
+ echo "- ⚠️ Tests were not run because the build job did not succeed." >> "$GITHUB_STEP_SUMMARY"
+ exit 0
+ fi
+
+ case "$test_result" in
+ success)
+ echo "- ✅ Pollinations text generation test" >> "$GITHUB_STEP_SUMMARY"
+ ;;
+ failure)
+ echo "- ❌ Pollinations text generation test" >> "$GITHUB_STEP_SUMMARY"
+ if [ -f reports/main-tests.log ]; then
+ echo '\nView log
\n\n' >> "$GITHUB_STEP_SUMMARY"
+ tail -n 40 reports/main-tests.log >> "$GITHUB_STEP_SUMMARY"
+ echo '\n ' >> "$GITHUB_STEP_SUMMARY"
+ fi
+ ;;
+ skipped)
+ echo "- ⚠️ Pollinations text generation test was skipped." >> "$GITHUB_STEP_SUMMARY"
+ ;;
+ *)
+ if [ -n "$status" ]; then
+ echo "- ⚠️ Pollinations text generation test status: $status" >> "$GITHUB_STEP_SUMMARY"
+ else
+ echo "- ⚠️ Pollinations text generation test status is unknown." >> "$GITHUB_STEP_SUMMARY"
+ fi
+ ;;
+ esac
+
+ deploy:
+ name: Deploy to Pages
+ runs-on: ubuntu-latest
+ needs: build
+ if: ${{ needs.build.result == 'success' }}
+ environment:
+ name: github-pages
+ url: ${{ steps.deploy.outputs.page_url }}
+ steps:
+ - name: Deploy GitHub Pages
+ id: deploy
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/pages-build.yml b/.github/workflows/pages-build.yml
deleted file mode 100644
index 9dfd895..0000000
--- a/.github/workflows/pages-build.yml
+++ /dev/null
@@ -1,211 +0,0 @@
-name: Build and Deploy Unity Chat
-
-on:
- push:
- branches:
- - work
- workflow_dispatch:
-
-permissions:
- contents: read
- pages: write
- id-token: write
-
-concurrency:
- group: unity-chat-pages
- cancel-in-progress: false
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 20
-
- - name: Install dependencies
- run: npm install
-
- - name: Build static artifact
- run: npm run build
-
- - name: Upload build artifact
- uses: actions/upload-artifact@v4
- with:
- name: site-dist
- path: dist
- if-no-files-found: error
-
- test-validation:
- needs: build
- runs-on: ubuntu-latest
- env:
- POLLINATIONS_TOKEN: ${{ secrets.POLLINATIONS_TOKEN }}
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Download build artifact
- uses: actions/download-artifact@v4
- with:
- name: site-dist
- path: dist
-
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 20
-
- - name: Install dependencies
- run: npm install
-
- - name: Run validation suite
- run: npm test
-
- - name: Upload validation reports
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: reports-validation
- path: reports
- if-no-files-found: ignore
-
- test-artifact:
- needs: build
- runs-on: ubuntu-latest
- env:
- POLLINATIONS_TOKEN: ${{ secrets.POLLINATIONS_TOKEN }}
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Download build artifact
- uses: actions/download-artifact@v4
- with:
- name: site-dist
- path: dist
-
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 20
-
- - name: Install dependencies
- run: npm install
-
- - name: Run artifact integrity checks
- run: npm run test:artifact
-
- - name: Upload artifact reports
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: reports-artifact
- path: reports
- if-no-files-found: ignore
-
- test-models:
- needs: build
- runs-on: ubuntu-latest
- env:
- POLLINATIONS_TOKEN: ${{ secrets.POLLINATIONS_TOKEN }}
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Download build artifact
- uses: actions/download-artifact@v4
- with:
- name: site-dist
- path: dist
-
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 20
-
- - name: Install dependencies
- run: npm install
-
- - name: Run Pollinations model smoke tests
- run: npm run test:models
-
- - name: Upload model reports
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: reports-models
- path: reports
- if-no-files-found: ignore
-
- report:
- if: always()
- needs:
- - build
- - test-validation
- - test-artifact
- - test-models
- runs-on: ubuntu-latest
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Download test reports
- uses: actions/download-artifact@v4
- with:
- pattern: reports-*
- merge-multiple: true
- path: reports
- if-no-artifact-found: ignore
-
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 20
-
- - name: Generate aggregate report
- run: npm run report:generate
-
- - name: Upload combined summary
- uses: actions/upload-artifact@v4
- with:
- name: pipeline-summary
- path: reports/summary.md
- if-no-files-found: error
-
- build-page:
- needs: build
- runs-on: ubuntu-latest
- steps:
- - name: Download build artifact
- uses: actions/download-artifact@v4
- with:
- name: site-dist
- path: dist
-
- - name: Upload artifact for GitHub Pages
- uses: actions/upload-pages-artifact@v3
- with:
- path: dist
-
- deploy:
- needs:
- - build-page
- - report
- - test-validation
- - test-artifact
- - test-models
- runs-on: ubuntu-latest
- permissions:
- pages: write
- id-token: write
- environment:
- name: github-pages
- url: ${{ steps.deploy.outputs.page_url }}
- steps:
- - id: deploy
- uses: actions/deploy-pages@v4
diff --git a/.github/workflows/pull-request-checks.yml b/.github/workflows/pull-request-checks.yml
new file mode 100644
index 0000000..01d1e86
--- /dev/null
+++ b/.github/workflows/pull-request-checks.yml
@@ -0,0 +1,94 @@
+name: Pull Request Quality Checks
+
+on:
+ pull_request:
+ branches:
+ - main
+ - '*'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ run-tests:
+ name: Run Tests
+ runs-on: ubuntu-latest
+ outputs:
+ status: ${{ steps.run-tests.outputs.status }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run Tests
+ id: run-tests
+ run: |
+ mkdir -p reports
+ set +e
+ npm run test:pollinations:pr > reports/pull-request-tests.log 2>&1
+ exit_code=$?
+ set -e
+ if [ "$exit_code" -ne 0 ]; then
+ status="failed"
+ else
+ status="passed"
+ fi
+ echo "$status" > reports/status.txt
+ echo "$exit_code" > reports/exit-code.txt
+ echo "status=$status" >> "$GITHUB_OUTPUT"
+ exit 0
+
+ - name: Upload test artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: pull-request-tests
+ path: reports
+ if-no-files-found: warn
+
+ report:
+ name: Report Tests Statuses
+ runs-on: ubuntu-latest
+ needs: run-tests
+ if: always()
+ steps:
+ - name: Download test artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: pull-request-tests
+ path: reports
+ continue-on-error: true
+
+ - name: Summarize results
+ run: |
+ status="${{ needs.run-tests.outputs.status }}"
+ if [ -z "$status" ]; then
+ status="unknown"
+ fi
+
+ echo "### Pull Request Test Statuses" >> "$GITHUB_STEP_SUMMARY"
+ case "$status" in
+ passed)
+ echo "- ✅ Pollinations text generation test (PR)" >> "$GITHUB_STEP_SUMMARY"
+ ;;
+ failed)
+ echo "- ❌ Pollinations text generation test (PR)" >> "$GITHUB_STEP_SUMMARY"
+ if [ -f reports/pull-request-tests.log ]; then
+ echo '\nView log
\n\n' >> "$GITHUB_STEP_SUMMARY"
+ tail -n 40 reports/pull-request-tests.log >> "$GITHUB_STEP_SUMMARY"
+ echo '\n ' >> "$GITHUB_STEP_SUMMARY"
+ fi
+ ;;
+ *)
+ echo "- ⚠️ Pollinations text generation test status: $status" >> "$GITHUB_STEP_SUMMARY"
+ ;;
+ esac
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..02000af
--- /dev/null
+++ b/README.md
@@ -0,0 +1,56 @@
+# Unity Chat
+
+[](https://github.com/unityailab/Voice-Control-2/actions/workflows/main-branch-pipeline.yml)
+[](https://github.com/unityailab/Voice-Control-2/actions/workflows/main-branch-pipeline.yml)
+
+Unity Chat is a static Pollinations AI workspace with configurable models, voices, and visual themes. The interface is built for GitHub Pages deployment and includes automated workflows for validation, testing, and publishing.
+
+## Theme support
+
+The application dynamically loads every theme in the `themes/` directory. The interface stores the selected theme in local storage and keeps the dropdown in sync with the active choice, ensuring a consistent experience across reloads.
+
+## Automated workflows
+
+Two GitHub Actions workflows keep the project healthy:
+
+- **Pull Request Quality Checks** – runs the Pollinations text generation smoke test for every pull request and records the outcome in the job summary.
+- **Main Branch Pipeline** – builds the static site, runs the smoke test against the `/tests` suite, reports build and test status summaries, and deploys the latest build to GitHub Pages.
+
+The badges above surface the live build and test status from the `main` branch pipeline.
+
+## Local development
+
+Install dependencies once:
+
+```bash
+npm ci
+```
+
+Run the bundled validation checks:
+
+```bash
+npm test
+```
+
+Execute the Pollinations text smoke tests:
+
+```bash
+# Run the /test suite (matches pull request checks)
+npm run test:pollinations:pr
+
+# Run the /tests suite (matches the main pipeline)
+npm run test:pollinations
+```
+
+Build the static site locally:
+
+```bash
+npm run build
+```
+
+## Project structure
+
+- `script.js` – main application logic, including Pollinations API integration, state management, and theme handling.
+- `themes/` – CSS variable overrides for all interface themes.
+- `tests/` & `test/` – lightweight smoke tests that exercise the Pollinations text endpoint using Node's built-in test runner.
+- `.github/workflows/` – GitHub Actions workflows for pull request checks and main branch deployments.
diff --git a/package.json b/package.json
index 73e7a35..f748bd4 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,8 @@
"build": "node scripts/build.js",
"test:models": "node scripts/test-models.js",
"test:artifact": "node scripts/verify-dist.js",
+ "test:pollinations": "node --test tests",
+ "test:pollinations:pr": "node --test test",
"report:generate": "node scripts/generate-report.js"
},
"engines": {
diff --git a/script.js b/script.js
index 8006628..cfbe72e 100644
--- a/script.js
+++ b/script.js
@@ -16,6 +16,46 @@ const LOCAL_MODELS_PATH = 'data/models.json';
const API_SEED_LENGTH = 8;
let cryptoSeedWarningLogged = false;
+const FALLBACK_MODELS = [
+ {
+ id: 'openai',
+ label: 'OpenAI (GPT-4o mini)',
+ description: 'Pollinations gateway to GPT-4o mini for general creative work.',
+ tier: 'seed',
+ voices: ['alloy', 'nova', 'shimmer']
+ },
+ {
+ id: 'mistral',
+ label: 'Mistral',
+ description: 'Fast multilingual model tuned for product copy and ideation.',
+ tier: 'seed',
+ voices: ['alloy', 'fable']
+ },
+ {
+ id: 'llama',
+ label: 'LLaMA Fusion',
+ description: 'Community LLaMA fusion with extended context for research assistance.',
+ tier: 'community',
+ voices: ['echo', 'onyx']
+ },
+ {
+ id: 'deepseek',
+ label: 'DeepSeek',
+ description: 'Analytical reasoning model great for summarising and planning.',
+ tier: 'seed',
+ voices: ['echo', 'shimmer']
+ },
+ {
+ id: 'claude-hybridspace',
+ label: 'Claude HybridSpace',
+ description: 'Anthropic Claude via Pollinations for thoughtful long-form answers.',
+ tier: 'growth',
+ voices: ['nova', 'fable']
+ }
+];
+
+const FALLBACK_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
+
function sanitizeToken(value) {
if (typeof value !== 'string') {
return '';
diff --git a/test/pollinations-text.test.js b/test/pollinations-text.test.js
new file mode 100644
index 0000000..3680f50
--- /dev/null
+++ b/test/pollinations-text.test.js
@@ -0,0 +1,3 @@
+'use strict';
+
+require('../tests/shared/pollinations-text');
diff --git a/tests/pollinations-text.test.js b/tests/pollinations-text.test.js
new file mode 100644
index 0000000..8321b6a
--- /dev/null
+++ b/tests/pollinations-text.test.js
@@ -0,0 +1,3 @@
+'use strict';
+
+require('./shared/pollinations-text');
diff --git a/tests/shared/pollinations-text.js b/tests/shared/pollinations-text.js
new file mode 100644
index 0000000..bff2d28
--- /dev/null
+++ b/tests/shared/pollinations-text.js
@@ -0,0 +1,77 @@
+'use strict';
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+
+const API_ENDPOINT = 'https://text.pollinations.ai/openai';
+const TEST_MODEL = 'openai';
+const TEST_VOICE = 'alloy';
+const TEST_PROMPT = 'Reply with a short greeting from the Unity Chat automated test suite.';
+
+async function requestPollinationsResponse() {
+ const url = new URL(API_ENDPOINT);
+ url.searchParams.set('model', TEST_MODEL);
+
+ const payload = {
+ model: TEST_MODEL,
+ voice: TEST_VOICE,
+ private: true,
+ messages: [
+ { role: 'system', content: 'You are verifying the Pollinations text endpoint for Unity Chat.' },
+ { role: 'user', content: TEST_PROMPT }
+ ]
+ };
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(payload)
+ });
+
+ return { response, payload };
+}
+
+test('Pollinations text generation returns a non-empty message', { timeout: 20000 }, async (t) => {
+ let response;
+ let payload;
+
+ try {
+ ({ response, payload } = await requestPollinationsResponse());
+ } catch (error) {
+ const cause = error?.cause?.code || error?.cause?.errno || error?.code;
+ const message = cause ? `Pollinations API unreachable (${cause}).` : 'Pollinations API unreachable.';
+ t.diagnostic(message);
+ t.skip(message);
+ return;
+ }
+
+ if (!response.ok) {
+ const message = `Pollinations API responded with status ${response.status}.`;
+ t.diagnostic(message);
+ t.skip(message);
+ return;
+ }
+
+ let data;
+ try {
+ data = await response.json();
+ } catch (error) {
+ const message = `Unable to parse Pollinations response: ${error.message}`;
+ t.diagnostic(message);
+ t.skip(message);
+ return;
+ }
+
+ const content = data?.choices?.[0]?.message?.content || data?.message || '';
+
+ assert.equal(typeof content, 'string', 'Pollinations response content must be a string.');
+ assert.ok(content.trim().length > 0, 'Pollinations response should not be empty.');
+
+ t.diagnostic({
+ model: payload.model,
+ voice: payload.voice,
+ preview: content.slice(0, 120)
+ });
+});