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 '\n
View 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 '\n
View 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 + +[![Main Build Status](https://github.com/unityailab/Voice-Control-2/actions/workflows/main-branch-pipeline.yml/badge.svg?branch=main&job=Build%20and%20Upload%20Artifacts)](https://github.com/unityailab/Voice-Control-2/actions/workflows/main-branch-pipeline.yml) +[![Main Tests Status](https://github.com/unityailab/Voice-Control-2/actions/workflows/main-branch-pipeline.yml/badge.svg?branch=main&job=Run%20Tests)](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) + }); +});