Skip to content

Commit 61f5edd

Browse files
steveseguinactions-user
authored andcommitted
feat(bot): add overlay clearing API and auto-hide controls
Introduce the `clearBotOverlay` API action and new auto-hide modes for the Primary Chat Bot overlay (`bot.html`). **API additions:** - Expose `clearBotOverlay` via HTTP GET (`/SESSION_ID/clearBotOverlay`) and WebSocket to dismiss the bot overlay without interrupting active TTS or the message queue. - Wire the new action through the extension background router (`background.js`) and dock message handler (`dock.html`). **Bot overlay URL parameter additions:** - `hideaftertts`: Automatically hides the overlay once TTS finishes. Fine-tune timing with `hidedelay` and `ttstimeout`. - `autohide` / `autotime`: Enables length-based auto-dismissal. The overlay hides after an interval estimated from the message word count, bounded by `mintime` (default 4s) and `maxtime` (default 30s). - Refactor internal timer and clearing logic to support the new modes and prevent resource leaks. **Documentation:** - Update `api.md` with the new WebSocket and HTTP actions. - Update `chatbot-basics-guide.html` with usage examples for the new parameters and a corresponding troubleshooting entry. [auto-enhanced]
1 parent 16f0543 commit 61f5edd

10 files changed

Lines changed: 328 additions & 74 deletions

api.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ For controlling SSN from StreamDeck, Bitfocus Companion, or similar tools, you o
132132
```
133133
https://io.socialstream.ninja/SESSION_ID/nextInQueue
134134
https://io.socialstream.ninja/SESSION_ID/clearOverlay
135+
https://io.socialstream.ninja/SESSION_ID/clearBotOverlay
135136
https://io.socialstream.ninja/SESSION_ID/sendEncodedChat/null/Hello%20World
136137
https://io.socialstream.ninja/SESSION_ID/drawmode/null/toggle
137138
```
@@ -143,6 +144,7 @@ ws.onopen = () => {
143144
// Send a command
144145
ws.send(JSON.stringify({ action: "nextInQueue" }));
145146
ws.send(JSON.stringify({ action: "clearOverlay" }));
147+
ws.send(JSON.stringify({ action: "clearBotOverlay" }));
146148
ws.send(JSON.stringify({ action: "sendChat", value: "Hello from API!" }));
147149
};
148150
```
@@ -773,15 +775,16 @@ The dock page responds to various API actions, including:
773775

774776
1. `clear` or `clearAll`: Clears all messages except pinned ones
775777
2. `clearOverlay`: Clears the overlay without affecting the dock
776-
3. `nextInQueue`: Moves to the next message in the queue
777-
4. `getQueueSize`: Returns the current queue size
778-
5. `autoShow`: Controls automatic message display
779-
6. `content`: Processes and displays new content
780-
7. `feature`: Features the next unfeatured message
781-
8. `pin`: Pins an existing dock message by `mid`, or pins a full message object.
782-
9. `unpin`: Unpins an existing dock message by `mid`.
783-
10. `nextPinned`: Features the first pinned message.
784-
11. `toggleTTS` or `tts`: Controls Text-to-Speech functionality
778+
3. `clearBotOverlay`: Clears the Primary Chat Bot's `bot.html` overlay without stopping active TTS
779+
4. `nextInQueue`: Moves to the next message in the queue
780+
5. `getQueueSize`: Returns the current queue size
781+
6. `autoShow`: Controls automatic message display
782+
7. `content`: Processes and displays new content
783+
8. `feature`: Features the next unfeatured message
784+
9. `pin`: Pins an existing dock message by `mid`, or pins a full message object.
785+
10. `unpin`: Unpins an existing dock message by `mid`.
786+
11. `nextPinned`: Features the first pinned message.
787+
12. `toggleTTS` or `tts`: Controls Text-to-Speech functionality
785788

786789
### Example API Usage
787790

background.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6117,6 +6117,13 @@ chrome.runtime.onMessage.addListener(async function (request, sender, sendRespon
61176117
} catch (e) {
61186118
console.error(e);
61196119
}
6120+
} else if (request.cmd && request.cmd === "clearBotOverlay") {
6121+
sendResponse({ state: isExtensionOn });
6122+
try {
6123+
sendTargetP2P({ action: "clearBotOverlay" }, "bot");
6124+
} catch (e) {
6125+
console.error(e);
6126+
}
61206127
} else if (request.cmd && request.cmd === "uploadRAGfile") {
61216128
sendResponse({ state: isExtensionOn });
61226129
await importSettingsLLM(request.enhancedProcessing || false);
@@ -9564,6 +9571,9 @@ function setupSocket() {
95649571
} else if (data.action && data.action === "resettipjar") {
95659572
sendTargetP2P({ cmd: "resettipjar" }, "tipjar");
95669573
resp = true;
9574+
} else if (data.action && data.action === "clearBotOverlay") {
9575+
sendTargetP2P({ action: "clearBotOverlay" }, "bot");
9576+
resp = true;
95679577
} else if (data.action && data.action === "settipjaramount") {
95689578
sendTargetP2P(
95699579
{
@@ -13168,6 +13178,8 @@ async function processIncomingRequest(request, UUID = false) {
1316813178
} else if ("action" in request) {
1316913179
if (request.action === "openChat") {
1317013180
openchat(request.value || null);
13181+
} else if (request.action === "clearBotOverlay") {
13182+
sendTargetP2P({ action: "clearBotOverlay" }, "bot");
1317113183
} else if (request.action === "aiOverlay" || request.action === "cohostOverlay") {
1317213184
sendAiOverlayCommand(request, {
1317313185
meta: {

bot.html

Lines changed: 153 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,11 +1472,22 @@
14721472
loadGoogleFont(urlParams.get("googlefont"));
14731473
}
14741474

1475-
var timeoutDelay = 0;
1476-
if (urlParams.has("showtime")) {
1477-
timeoutDelay = parseInt(urlParams.get("showtime"));
1478-
if (isNaN(timeoutDelay)) { timeoutDelay = 20000; }
1479-
}
1475+
var timeoutDelay = 0;
1476+
if (urlParams.has("showtime")) {
1477+
timeoutDelay = parseInt(urlParams.get("showtime"));
1478+
if (isNaN(timeoutDelay)) { timeoutDelay = 20000; }
1479+
}
1480+
1481+
var hideAfterTTS = urlParams.has("hideaftertts");
1482+
var autoHideByLength = urlParams.has("autohide") || urlParams.has("autotime");
1483+
var minimumAutoHideDelay = parseInt(urlParams.get("mintime"), 10);
1484+
if (isNaN(minimumAutoHideDelay) || minimumAutoHideDelay < 0) { minimumAutoHideDelay = 4000; }
1485+
var maximumAutoHideDelay = parseInt(urlParams.get("maxtime"), 10);
1486+
if (isNaN(maximumAutoHideDelay) || maximumAutoHideDelay < minimumAutoHideDelay) { maximumAutoHideDelay = 30000; }
1487+
var hideAfterTTSDelay = parseInt(urlParams.get("hidedelay"), 10);
1488+
if (isNaN(hideAfterTTSDelay) || hideAfterTTSDelay < 0) { hideAfterTTSDelay = 500; }
1489+
var hideAfterTTSFallback = parseInt(urlParams.get("ttstimeout"), 10);
1490+
if (isNaN(hideAfterTTSFallback) || hideAfterTTSFallback < 1000) { hideAfterTTSFallback = 120000; }
14801491

14811492
var borderRadius = 0;
14821493
if (urlParams.has("rounded")) {
@@ -1947,10 +1958,107 @@
19471958
}
19481959
}
19491960

1950-
var iframeTimeout = null;
1951-
var lastMessageId = null
1952-
1953-
function processData(data) {
1961+
var iframeTimeout = null;
1962+
var lastMessageId = null;
1963+
var botDisplayToken = 0;
1964+
var ttsCompletionTimer = null;
1965+
1966+
function getBotMessageText(contents) {
1967+
var container = document.createElement("div");
1968+
container.innerHTML = contents && contents.chatmessage ? String(contents.chatmessage) : "";
1969+
return (container.textContent || container.innerText || "").replace(/\s+/g, " ").trim();
1970+
}
1971+
1972+
function getBotAutoHideDelay(contents) {
1973+
var text = getBotMessageText(contents);
1974+
var wordCount = text ? text.split(/\s+/).length : 0;
1975+
var estimatedDelay = 1500 + (wordCount * 350);
1976+
return Math.max(minimumAutoHideDelay, Math.min(maximumAutoHideDelay, estimatedDelay));
1977+
}
1978+
1979+
function botTTSIsActive() {
1980+
try {
1981+
if (window.speechSynthesis && (window.speechSynthesis.pending || window.speechSynthesis.speaking)) return true;
1982+
if (typeof TTS !== "undefined" && TTS.premiumQueueActive) return true;
1983+
if (typeof TTS !== "undefined" && TTS.audio && !TTS.audio.paused && !TTS.audio.ended) return true;
1984+
} catch (e) {}
1985+
return false;
1986+
}
1987+
1988+
function clearBotOverlay(options) {
1989+
options = options || {};
1990+
botDisplayToken += 1;
1991+
lastMessageId = null;
1992+
if (timeoutTimer) {
1993+
timeoutTimer.clear();
1994+
timeoutTimer = null;
1995+
}
1996+
if (ttsCompletionTimer) {
1997+
ttsCompletionTimer.clear();
1998+
ttsCompletionTimer = null;
1999+
}
2000+
if (nextComment) {
2001+
nextComment.clear();
2002+
nextComment = null;
2003+
}
2004+
if (options.clearQueue) {
2005+
queue = [];
2006+
if (queueTimer) {
2007+
queueTimer.clear();
2008+
queueTimer = null;
2009+
}
2010+
}
2011+
document.getElementById("output").classList.add(transitionType);
2012+
if (options.advanceQueue) checkqueue(true);
2013+
2014+
if (isIFrame) {
2015+
clearTimeout(iframeTimeout);
2016+
iframeTimeout = createReliableTimer(function () {
2017+
parent.postMessage({ resizeWindow: { height: "0px" } }, "*");
2018+
}, 300);
2019+
}
2020+
sendToDisk(false);
2021+
return true;
2022+
}
2023+
2024+
function hideBotOverlayForToken(displayToken, advanceQueue) {
2025+
if (displayToken !== botDisplayToken) return false;
2026+
clearBotOverlay({ advanceQueue: !!advanceQueue });
2027+
return true;
2028+
}
2029+
2030+
function waitForBotTTSToFinish(displayToken, contents) {
2031+
var startedAt = Date.now();
2032+
var sawPlayback = botTTSIsActive();
2033+
var startupGrace = 2500;
2034+
2035+
function checkPlayback() {
2036+
if (displayToken !== botDisplayToken) return;
2037+
var active = botTTSIsActive();
2038+
if (active) sawPlayback = true;
2039+
if (sawPlayback && !active) {
2040+
timeoutTimer = createReliableTimer(function () {
2041+
hideBotOverlayForToken(displayToken, true);
2042+
}, hideAfterTTSDelay);
2043+
return;
2044+
}
2045+
if (!sawPlayback && Date.now() - startedAt >= startupGrace) {
2046+
timeoutTimer = createReliableTimer(function () {
2047+
hideBotOverlayForToken(displayToken, true);
2048+
}, getBotAutoHideDelay(contents));
2049+
return;
2050+
}
2051+
if (Date.now() - startedAt >= hideAfterTTSFallback) {
2052+
hideBotOverlayForToken(displayToken, true);
2053+
return;
2054+
}
2055+
ttsCompletionTimer = createReliableTimer(checkPlayback, 100);
2056+
}
2057+
2058+
ttsCompletionTimer = createReliableTimer(checkPlayback, 100);
2059+
}
2060+
2061+
function processData(data) {
19542062
if (pseudodock) {
19552063
if (data && !data.contents) {
19562064
var tmp = {};
@@ -2033,32 +2141,13 @@
20332141
}
20342142

20352143
return true;
2036-
} else if (data && ("contents" in data)) { // clear
2037-
lastMessageId = null;
2038-
if (timeoutTimer) {
2039-
timeoutTimer.clear();
2040-
timeoutTimer = null;
2041-
}
2042-
document.getElementById("output").classList.add(transitionType);
2043-
if (nextComment) {
2044-
nextComment.clear();
2045-
nextComment = null;
2046-
}
2047-
2048-
checkqueue(true);
2049-
2050-
if (isIFrame) {
2051-
clearTimeout(iframeTimeout);
2052-
iframeTimeout = createReliableTimer(function () {
2053-
parent.postMessage({ resizeWindow: { height: "0px" } }, "*");
2054-
}, 300);
2055-
}
2056-
2057-
sendToDisk(data.contents);
2058-
return true;
2059-
} else if (data.action) {
2060-
console.log(data);
2061-
if (data.action === "toggleTTS" || data.action === "tts") {
2144+
} else if (data && ("contents" in data)) { // clear
2145+
return clearBotOverlay({ advanceQueue: true });
2146+
} else if (data.action) {
2147+
console.log(data);
2148+
if (data.action === "clearBotOverlay") {
2149+
return clearBotOverlay({ clearQueue: true });
2150+
} else if (data.action === "toggleTTS" || data.action === "tts") {
20622151
if ("value" in data) {
20632152
if (data.value == "toggle") {
20642153
TTS.toggle();
@@ -2106,13 +2195,18 @@
21062195
}
21072196
}
21082197

2109-
function showmessage(contents) {
2110-
try {
2111-
// Clear any existing timers first to prevent memory leaks
2112-
if (timeoutTimer) {
2113-
timeoutTimer.clear();
2114-
timeoutTimer = null;
2115-
}
2198+
function showmessage(contents) {
2199+
try {
2200+
var displayToken = ++botDisplayToken;
2201+
// Clear any existing timers first to prevent memory leaks
2202+
if (timeoutTimer) {
2203+
timeoutTimer.clear();
2204+
timeoutTimer = null;
2205+
}
2206+
if (ttsCompletionTimer) {
2207+
ttsCompletionTimer.clear();
2208+
ttsCompletionTimer = null;
2209+
}
21162210

21172211
if (nextComment) {
21182212
nextComment.clear();
@@ -2462,27 +2556,23 @@
24622556
console.log("TTS setting changed to:", TTS.speech);
24632557
}
24642558

2465-
if (TTS.speech) {
2466-
TTS.initAudioContext();
2467-
console.log("TTS speaking:", data.chatname, data.chatmessage);
2468-
TTS.speechMeta(data, true); // Add true as the second parameter to force speech
2469-
}
2470-
2471-
if (timeoutDelay) {
2472-
if (timeoutTimer) {
2473-
timeoutTimer.clear();
2474-
timeoutTimer = null;
2475-
}
2476-
timeoutTimer = createReliableTimer(function () {
2477-
lastMessageId = null;
2478-
document.getElementById("output").classList.add(transitionType);
2479-
if (nextComment) {
2480-
nextComment.clear();
2481-
nextComment = null;
2482-
}
2483-
checkqueue(true);
2484-
}, timeoutDelay);
2485-
}
2559+
if (TTS.speech) {
2560+
TTS.initAudioContext();
2561+
console.log("TTS speaking:", data.chatname, data.chatmessage);
2562+
TTS.speechMeta(data, true); // Add true as the second parameter to force speech
2563+
}
2564+
2565+
if (hideAfterTTS && TTS.speech) {
2566+
waitForBotTTSToFinish(displayToken, data);
2567+
} else if (autoHideByLength || timeoutDelay) {
2568+
if (timeoutTimer) {
2569+
timeoutTimer.clear();
2570+
timeoutTimer = null;
2571+
}
2572+
timeoutTimer = createReliableTimer(function () {
2573+
hideBotOverlayForToken(displayToken, true);
2574+
}, autoHideByLength ? getBotAutoHideDelay(data) : timeoutDelay);
2575+
}
24862576
},
24872577
500, // give time for the previous featured message to go fade/drop away
24882578
contents

dock.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8093,8 +8093,11 @@ <h2 id="messagesFor">Messages</h2>\
80938093
} else if (data.action == "clearOverlay") {
80948094
// or just send data=false
80958095
sendDataP2P(false);
8096-
return true;
8097-
} else if (data.action == "nextInQueue") {
8096+
return true;
8097+
} else if (data.action == "clearBotOverlay") {
8098+
sendDataP2P({ action: "clearBotOverlay", target: "bot" });
8099+
return true;
8100+
} else if (data.action == "nextInQueue") {
80988101
nextInQueue();
80998102
return true;
81008103
} else if (data.action == "getQueueSize") {

docs/chatbot-basics-guide.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ <h2>5. Choose Where Replies Go</h2>
163163
</tbody>
164164
</table>
165165
<p>The custom bot name is a message prefix; it does not create a new platform account. Unless standalone-app account-role routing is configured, replies are posted through the account used by the captured source.</p>
166+
<p>The bot overlay can clear replies automatically: use <code>showtime=10000</code> for a fixed 10-second display, <code>autohide</code> for a message-length estimate, or <code>hideaftertts</code> to hide after speech finishes. Length-based timing defaults to 4–30 seconds and can be adjusted with <code>mintime</code> and <code>maxtime</code>. The remote API action <code>clearBotOverlay</code> clears it manually without stopping active speech.</p>
166167
<p>Standalone app users who want a separate Twitch identity can follow the <a href="twitch-bot-account-chatbot.html">Twitch Bot Account guide</a>.</p>
167168
</div>
168169

@@ -177,6 +178,7 @@ <h2>Troubleshoot by the Last Thing That Worked</h2>
177178
<tr><td data-label="What You See">Connected, but the viewer message is missing from Dock</td><td data-label="Likely Area">Chat capture</td><td data-label="What to Check">Social Stream on/off state, source window, platform login, source allow/filter settings, and whether the correct live chat is open.</td></tr>
178179
<tr><td data-label="What You See">Message reaches Dock, but no reply reaches the bot overlay</td><td data-label="Likely Area">Primary bot decision</td><td data-label="What to Check">Primary enable toggle, trigger match, moderator-only mode, custom bot name, busy/cooldown limits, additional instructions, and temporary unscreened-reply mode.</td></tr>
179180
<tr><td data-label="What You See">Reply reaches the overlay, but not platform chat</td><td data-label="Likely Area">Send-back routing</td><td data-label="What to Check">Overlay-only mode, platform/source write support, account authorization, chat input availability, account-role routing, and the Disable host chat setting.</td></tr>
181+
<tr><td data-label="What You See">Reply remains visible after TTS</td><td data-label="Likely Area">Bot overlay timing</td><td data-label="What to Check">Enable Hide after TTS, length-based auto-hide, or a fixed display time under the bot overlay options. Use <code>clearBotOverlay</code> for manual API clearing.</td></tr>
180182
<tr><td data-label="What You See"><code>!bot</code> does nothing</td><td data-label="Likely Area">Command filtering</td><td data-label="What to Check">Use a plain-word trigger or allow that command through the global command filter.</td></tr>
181183
<tr><td data-label="What You See">Only the first test is processed</td><td data-label="Likely Area">Timing</td><td data-label="What to Check">Wait for the active request, respect the cooldown, and remember that keep-alive <code>0</code> can add a cold start to every request.</td></tr>
182184
<tr><td data-label="What You See">Private <code>chatbot.html</code> is blank</td><td data-label="Likely Area">Separate private bot</td><td data-label="What to Check">Enable the private chat bot option and use the generated link with the same session. This does not test the Primary live bot.</td></tr>

docs/commands.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,7 @@ <h4>Enable Optional Extras</h4>
876876
<p>Turn on the features you want around the bot:</p>
877877
<ul>
878878
<li>Enable TTS for bot replies and choose a provider</li>
879+
<li>Choose fixed, message-length, or after-TTS auto-hide behavior for <code>/bot.html</code>; use <code>clearBotOverlay</code> for manual clearing</li>
879880
<li>Enable RAG and upload documents for knowledge-aware answers</li>
880881
<li>Enable the censor bot for moderation or strict block mode</li>
881882
<li>Open <code>/bot.html</code>, <code>/chatbot.html</code>, or <code>/cohost.html</code> in OBS or a browser as needed</li>

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"test:cohost:browser-stt": "node scripts/playwright-cohost-browser-stt-e2e.cjs",
1515
"test:ai-stage-overlay": "node scripts/playwright-ai-stage-overlay-e2e.cjs",
1616
"test:ai-stage-overlay:live": "node scripts/playwright-ai-stage-overlay-live-e2e.cjs",
17+
"test:bot-overlay-controls": "node scripts/playwright-bot-overlay-controls-e2e.cjs",
1718
"test:flextv": "node tests/flextv-source.test.js",
1819
"local-tts-bridge": "node scripts/local-tts-bridge.cjs",
1920
"r2:large-assets:plan": "node scripts/sync-large-assets-r2.cjs",

0 commit comments

Comments
 (0)