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:
parent
5687fa1c22
commit
12e9e79f25
16 changed files with 662 additions and 3702 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -11,3 +11,6 @@ book/
|
|||
# Generated audio files in language examples
|
||||
public/languages/**/*.mp3
|
||||
*.mp3
|
||||
|
||||
# Bundled extension build artifact
|
||||
public/uncloseai-bundle.js
|
||||
|
|
|
|||
10
Makefile
10
Makefile
|
|
@ -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..."
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"!**/css/pico.classless.min.css",
|
||||
"!**/node_modules",
|
||||
"!**/*.min.js",
|
||||
"!**/*.min.css"
|
||||
"!**/*.min.css",
|
||||
"!**/uncloseai-bundle.js"
|
||||
]
|
||||
},
|
||||
"linter": {
|
||||
|
|
|
|||
35
docs/tickets/001-extension-fails-to-load.md
Normal file
35
docs/tickets/001-extension-fails-to-load.md
Normal 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.
|
||||
31
docs/tickets/002-tts-stops-after-first-sentence.md
Normal file
31
docs/tickets/002-tts-stops-after-first-sentence.md
Normal 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.
|
||||
32
docs/tickets/003-audio-icon-freezes-window.md
Normal file
32
docs/tickets/003-audio-icon-freezes-window.md
Normal 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.
|
||||
33
docs/tickets/004-working-indicators.md
Normal file
33
docs/tickets/004-working-indicators.md
Normal 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.
|
||||
38
docs/tickets/005-context-overflow-silent-failure.md
Normal file
38
docs/tickets/005-context-overflow-silent-failure.md
Normal 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
15
docs/tickets/index.md
Normal 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
4045
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
56
scripts/bundle-extension.mjs
Normal file
56
scripts/bundle-extension.mjs
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue