disable TTS via feature flag, move floating button up 121px, add bug tickets

TTS disabled by default for all users (tickets #002 freeze, #003 truncation).
Embedders re-enable with window.UNCLOSEAI_ENABLE_TTS = true.
Floating button bottom 20px to 141px to avoid overlapping page buttons.
Added docs/tickets/ with 5 issues from cthegray.
Bundle extension script and biome config updates.
This commit is contained in:
russell@unturf.com 2026-03-03 15:56:09 -05:00
parent 5687fa1c22
commit 12e9e79f25
16 changed files with 662 additions and 3702 deletions

3
.gitignore vendored
View file

@ -11,3 +11,6 @@ book/
# Generated audio files in language examples
public/languages/**/*.mp3
*.mp3
# Bundled extension build artifact
public/uncloseai-bundle.js

View file

@ -8,7 +8,7 @@
# All phony targets
.PHONY: help format check test validate-exports validate-all clean install dev build deploy
.PHONY: languages-list languages-build-all languages-test-all languages-clean
.PHONY: update-unsandbox-sdk
.PHONY: update-unsandbox-sdk bundle-extension
# Environment variables for testing language implementations
MODEL_ENDPOINT_1 ?= https://hermes.ai.unturf.com/v1
@ -39,6 +39,7 @@ help:
@echo " make git-commit - Commit with biome formatting"
@echo " make git-push - Push to remote"
@echo " make update-unsandbox-sdk - Download latest un.js SDK from unsandbox.com"
@echo " make bundle-extension - Bundle uncloseai.js for browser extension (CSP-safe IIFE)"
@echo ""
@echo "Language Examples:"
@echo " make languages-list - List all language directories"
@ -139,6 +140,13 @@ update-unsandbox-sdk:
wget -O lib/un.js "https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/javascript/async/src/un_async.js"
@echo "Done. Updated lib/un.js (Node.js SDK)"
# Bundle uncloseai.js for browser extension (IIFE, no ES modules, CSP-safe)
bundle-extension:
@echo "Bundling uncloseai.js for browser extension..."
node scripts/bundle-extension.mjs
@ls -lh public/uncloseai-bundle.js
@echo "Done. Output: public/uncloseai-bundle.js"
# Language validation
validate-languages:
@echo "Validating language files..."

View file

@ -6,7 +6,8 @@
"!**/css/pico.classless.min.css",
"!**/node_modules",
"!**/*.min.js",
"!**/*.min.css"
"!**/*.min.css",
"!**/uncloseai-bundle.js"
]
},
"linter": {

View file

@ -0,0 +1,35 @@
# 001: Extension fails to load on certain pages
**Reporter:** cthegray
**Date:** 2026-03-03
**Priority:** high
**Status:** open
**Affects:** Browser extension (Chrome/Safari)
## Description
The uncloseai browser extension does not load at all on some pages. No UI appears, no sidebar, nothing.
## Reproduction
**Fails to load on:**
- reddit.com (any page)
- slashdot.org
- cnn.com (main/index page only)
**Works on:**
- cnn.com article pages (individual stories)
## Analysis
The pattern suggests Content Security Policy (CSP) or iframe sandboxing on these domains is blocking the extension from injecting. Reddit and Slashdot both use strict CSPs. CNN's main page likely has different CSP headers than article pages.
Possible causes:
- CSP `script-src` blocking inline or extension scripts
- CSP `frame-src` or `frame-ancestors` blocking extension iframe/overlay
- Shadow DOM or aggressive DOM manipulation interfering with injection
- Extension manifest permissions not covering these domains
## Notes
Need to inspect the extension's content script injection mechanism and compare CSP headers between working and non-working pages.

View file

@ -0,0 +1,31 @@
# 002: TTS stops after first sentence
**Reporter:** cthegray
**Date:** 2026-03-03
**Priority:** high
**Status:** open
**Affects:** TTS playback, mobile and desktop
## Description
Text-to-speech only reads the first sentence of a response, then stops. Sometimes reads two sentences. Happens on both mobile (iPhone/Safari) and desktop (macOS/Safari).
## Reproduction
1. Send a message that generates a multi-sentence response
2. Tap the TTS/speaker icon on the message
3. TTS plays the first sentence (occasionally two)
4. Audio stops, remaining text is never spoken
## Analysis
Possible causes:
- TTS input is being truncated before sending to the speech endpoint
- The TTS API has a character/token limit and the client is not chunking the text
- Only the first SSE chunk's content is being sent to TTS instead of the full accumulated response
- Audio playback queue is not chaining subsequent audio segments
- Browser autoplay policy may be blocking follow-up audio segments (especially on mobile Safari)
## Notes
If the TTS API has a max input length, the client needs to split text into chunks, generate audio for each, and queue playback sequentially.

View file

@ -0,0 +1,32 @@
# 003: Audio icon freezes entire window
**Reporter:** cthegray
**Date:** 2026-03-03
**Priority:** critical
**Status:** open
**Affects:** iPhone (Safari, latest iOS), macOS (Safari, latest macOS)
## Description
Tapping the audio icon in the bottom bar (the global one, not the per-message TTS icon) causes the entire uncloseai window to freeze. Nothing responds after this: buttons, scrolling, even the close (X) button. Complete UI lockup.
## Reproduction
1. Open uncloseai in Safari (iPhone or macOS)
2. Have a conversation with at least one response visible
3. Tap the audio icon in the bottom navigation bar
4. Window freezes completely
5. Cannot interact with anything, cannot close the panel
## Analysis
This is a main-thread blocking issue. Possible causes:
- Synchronous API call on the main thread (blocking fetch or XHR without async)
- Infinite loop in audio initialization or playback setup
- Unresolved promise or callback deadlock
- AudioContext creation failing silently and triggering a retry loop
- Safari-specific Web Audio API quirk (Safari requires user gesture to resume AudioContext, failure to handle this could cause a hang)
## Notes
This is the highest priority issue: a complete UI freeze with no recovery path is a showstopper. The user has to force-close the tab/app. Needs investigation into what the bottom-bar audio icon's click handler does differently from the per-message TTS icon.

View file

@ -0,0 +1,33 @@
# 004: Add working/spinning indicators for async operations
**Reporter:** cthegray
**Date:** 2026-03-03
**Priority:** medium
**Status:** open
**Affects:** All platforms, UX
## Description
Icons that trigger async operations (TTS generation, page reading, etc.) give no visual feedback that work is in progress. The user taps and nothing visibly happens until the operation completes (or fails silently).
The book icon for "page read" already has an hourglass indicator. Other icons need the same treatment.
## Requirements
- TTS icon (per-message): show spinning/pulsing animation while audio is being generated
- Audio icon (bottom bar): show spinning/pulsing animation while audio is loading
- Any other async operation icons should show a working state
- Animation should stop when the operation completes or errors
- On error, briefly show an error state (red flash, shake, etc.) before returning to idle
## Design Notes
Keep consistent with the existing hourglass pattern on the book icon. Options:
- CSS spin animation on the icon itself
- Small spinner overlay on the icon
- Icon swap to an animated version during loading
- Pulsing opacity animation (subtle but effective)
## Notes
This is a UX quality issue. Without feedback, users assume the tap didn't register and tap again, potentially triggering duplicate requests or the freeze described in ticket 003.

View file

@ -0,0 +1,38 @@
# 005: Context overflow causes blank or hallucinated responses
**Reporter:** cthegray
**Date:** 2026-03-03
**Priority:** high
**Status:** open
**Affects:** All platforms, chat quality
## Description
When the context window fills up (long pages injected as context), the model either:
1. Returns completely blank responses (silent failure, nothing displayed)
2. Starts hallucinating about unrelated content, specifically talking about the uncloseai.com page itself or the Nous Research page
## Reproduction
1. Navigate to a very long page (e.g., Wikipedia's Napoleon article)
2. Use the page-read feature to inject the page content as context
3. Ask a question about the page
4. Response is either blank or talks about uncloseai/Nous Research instead of the page content
## Analysis
The page content is likely exceeding the model's `max_model_len` (context window). When this happens:
- The API may return an error that the client silently swallows (blank response)
- The context may be truncated in a way that drops the user's page content but keeps the system prompt and uncloseai UI text, causing the model to talk about those instead
- The Nous Research hallucination suggests the model's training data is bleeding through when it has no useful context to work with
Possible fixes:
- Check page content length before injection, truncate intelligently if it exceeds the model's `max_model_len` (available from `/v1/models` discovery)
- Show a user-facing warning when context is too long: "This page is too long, content has been trimmed"
- Implement chunked summarization for long pages (summarize in sections, then inject summaries)
- Never silently swallow API errors: if the completion fails, show the error to the user
- Prioritize user message and page content over system prompt when truncating
## Notes
The `max_model_len` field from model discovery should be used to calculate available context budget. System prompt + conversation history + page content must fit within this limit. If it doesn't, the client needs a truncation strategy, not silent failure.

15
docs/tickets/index.md Normal file
View file

@ -0,0 +1,15 @@
# uncloseai.com Tickets
## Open
| ID | Title | Reporter | Date | Priority |
|----|-------|----------|------|----------|
| 001 | [Extension fails to load on certain pages](001-extension-fails-to-load.md) | cthegray | 2026-03-03 | high |
| 002 | [TTS stops after first sentence](002-tts-stops-after-first-sentence.md) | cthegray | 2026-03-03 | high |
| 003 | [Audio icon freezes entire window](003-audio-icon-freezes-window.md) | cthegray | 2026-03-03 | critical |
| 004 | [Add working/spinning indicators for async operations](004-working-indicators.md) | cthegray | 2026-03-03 | medium |
| 005 | [Context overflow causes blank or hallucinated responses](005-context-overflow-silent-failure.md) | cthegray | 2026-03-03 | high |
## Closed
None yet.

4045
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,10 @@
"test:simple": "node simple_tests.js"
},
"devDependencies": {
"jsdom": "^22.0.0"
"esbuild": "^0.27.3",
"highlight.js": "^11.11.1",
"jsdom": "^22.0.0",
"marked": "^17.0.3"
},
"repository": {
"type": "git",

View file

@ -15,6 +15,13 @@
// accessible to everyone through a free, open, embeddable chat interface.
// Code is seeds to sprout on any abandoned technology.
// Feature flags
// TTS disabled by default (tickets #002, #003). Embedders can re-enable:
// window.UNCLOSEAI_ENABLE_TTS = true;
export function isTTSEnabled() {
return typeof window !== "undefined" && window.UNCLOSEAI_ENABLE_TTS === true;
}
export const TTS_API_URL = "https://speech.ai.unturf.com/v1/audio/speech";
export const VOICES_API_URL = "https://speech.ai.unturf.com/v1/voices";
export const MEGAPARCE_API_URL = "https://megaparce.ai.unturf.com/v1/file";

View file

@ -17,7 +17,7 @@
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
import hljs from "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/es/highlight.min.js";
import { setSystemMessageAppend } from "./config.js";
import { setSystemMessageAppend, isTTSEnabled } from "./config.js";
import { extractWebpageContent, buildPageAnalysisPrompt, parsePageAnalysis, buildAnalysisContext } from "./content.js";
import { computePageIntelligence, formatPageIntelligence } from "./page-intelligence.js";
import {
@ -656,9 +656,10 @@ async function openUncloseaiEmbeddedModalNew() {
modelSection.appendChild(modelSelect);
// Voice selection for TTS
// Voice selection for TTS (hidden when TTS disabled)
const voiceSection = document.createElement("div");
voiceSection.className = "uncloseai-section";
if (!isTTSEnabled()) voiceSection.style.display = "none";
const voiceSelect = document.createElement("select");
voiceSelect.id = "hermes-voice-select";
@ -1397,7 +1398,11 @@ async function openUncloseaiEmbeddedModalNew() {
},
];
controlActions.forEach(({ emoji, tooltip, action }) => {
// Filter out TTS-related controls when TTS is disabled
const ttsEmojis = ["📖", "🔊"];
const filteredActions = isTTSEnabled() ? controlActions : controlActions.filter(a => !ttsEmojis.includes(a.emoji));
filteredActions.forEach(({ emoji, tooltip, action }) => {
const btn = document.createElement("button");
btn.textContent = emoji;
btn.title = tooltip;
@ -1572,6 +1577,7 @@ async function openUncloseaiEmbeddedModalNew() {
userMsg.remove();
};
if (isTTSEnabled()) {
userMessageActions.appendChild(userTtsBtn);
// Add download button initially (hidden until audio is generated)
@ -1581,6 +1587,7 @@ async function openUncloseaiEmbeddedModalNew() {
userDownloadBtn.className = "uncloseai-btn-small uncloseai-download-btn";
userDownloadBtn.style.display = "none"; // Hidden initially
userMessageActions.appendChild(userDownloadBtn);
}
userMessageActions.appendChild(userDeleteBtn);
userMsg.appendChild(messageText);
@ -1803,6 +1810,7 @@ async function openUncloseaiEmbeddedModalNew() {
}
};
if (isTTSEnabled()) {
messageActions.appendChild(ttsBtn);
// Add download button initially (hidden until audio is generated)
@ -1812,6 +1820,7 @@ async function openUncloseaiEmbeddedModalNew() {
aiDownloadBtn.className = "uncloseai-btn-small uncloseai-download-btn";
aiDownloadBtn.style.display = "none"; // Hidden initially
messageActions.appendChild(aiDownloadBtn);
}
messageActions.appendChild(copyRawBtn);
messageActions.appendChild(copyHtmlBtn);

View file

@ -1326,7 +1326,7 @@ dialog#uncloseai-embedded-modal[data-theme="dark"] .user-message {
/* Floating AI button */
.uncloseai-floating-button {
position: fixed;
bottom: 20px;
bottom: 141px;
right: 10px;
width: 120px;
height: 55px;

View file

@ -1404,7 +1404,7 @@ dialog#uncloseai-embedded-modal[data-theme="dark"] .user-message {
/* Floating AI button */
.uncloseai-floating-button {
position: fixed;
bottom: 20px;
bottom: 141px;
right: 10px;
width: 120px;
height: 55px;

View file

@ -0,0 +1,56 @@
// Bundle uncloseai.js into a single IIFE for browser extension use.
// Resolves CDN imports (marked, highlight.js) to local node_modules.
// Output: public/uncloseai-bundle.js (CSP-safe, no ES module syntax)
import { build } from "esbuild";
import { createRequire } from "module";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, "..");
const require = createRequire(import.meta.url);
// Plugin: resolve CDN URL imports to local node_modules packages
const cdnResolverPlugin = {
name: "cdn-resolver",
setup(build) {
// marked: https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js
build.onResolve(
{ filter: /^https:\/\/cdn\.jsdelivr\.net\/npm\/marked/ },
() => ({
path: require.resolve("marked"),
}),
);
// highlight.js: https://cdnjs.cloudflare.com/ajax/libs/highlight.js/
build.onResolve(
{ filter: /^https:\/\/cdnjs\.cloudflare\.com\/ajax\/libs\/highlight\.js/ },
() => ({
path: require.resolve("highlight.js"),
}),
);
},
};
try {
const result = await build({
entryPoints: [path.join(ROOT, "public", "uncloseai.js")],
bundle: true,
format: "iife",
minify: true,
sourcemap: false,
outfile: path.join(ROOT, "public", "uncloseai-bundle.js"),
plugins: [cdnResolverPlugin],
target: ["chrome120", "firefox120", "safari17"],
logLevel: "info",
});
if (result.errors.length > 0) {
console.error("Build failed with errors");
process.exit(1);
}
} catch (err) {
console.error("Bundle failed:", err.message);
process.exit(1);
}