From 9b29dec4ba54e896bbc51b032457d5ac44e0aca7 Mon Sep 17 00:00:00 2001 From: steveseguin Date: Mon, 6 Jan 2025 01:41:25 -0500 Subject: [PATCH 1/7] top/live chat pane made small; not hidden --- ai.js | 188 +++++++++++++++++++++++++++---------------- background.js | 135 +++++++++++-------------------- manifest.json | 2 +- popup.html | 12 +++ popup.js | 30 +++---- sources/nicovideo.js | 7 ++ sources/youtube.js | 4 +- 7 files changed, 197 insertions(+), 181 deletions(-) diff --git a/ai.js b/ai.js index 34983e8e..e858435a 100644 --- a/ai.js +++ b/ai.js @@ -51,61 +51,85 @@ async function rebuildIndex() { globalLunrIndex = initLunrIndex(documents); } -async function getFirstAvailableModel() { +async function getFirstAvailableModel(exclude=null) { let ollamaendpoint = settings.ollamaendpoint?.textsetting || "http://localhost:11434"; - if (typeof ipcRenderer !== 'undefined') { - // Electron environment - return new Promise( async (resolve, reject) => { - let ccc = setTimeout(()=>{ - reject(new Error('Request timed out')); - },10000); - let xhr; - try { - xhr = await fetchNode(`${ollamaendpoint}/api/tags`); - } catch(e){ - clearTimeout(ccc); - reject(new Error('General fetch error')); - return; - } - - const datar = JSON.parse(xhr.data); - if (datar && datar.models && datar.models.length > 0) { - resolve(datar.models[0].name); - return; - } else { - reject(new Error('No models available')); - return; - } + const isLLMModel = (model) => { + const llmFamilies = ['llama', 'qwen2']; + return model.details?.families?.some(family => llmFamilies.includes(family)); + }; + + const getSizeInBillions = (model) => { + const sizeStr = model.details?.parameter_size; + return parseFloat(sizeStr?.replace('B', '')) || 0; + }; + + const findBestModel = (models) => { + const llmModels = models.filter(isLLMModel); + if (!llmModels.length) return models[0]?.name; + + const targetModels = llmModels.filter(m => { + const size = getSizeInBillions(m); + return size >= 2 && size <= 8; }); - - } else { - // Web environment - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('GET', `${ollamaendpoint}/api/tags`, true); - xhr.onload = function() { - if (xhr.status === 200) { - const datar = JSON.parse(xhr.responseText); - if (datar && datar.models && datar.models.length > 0) { - resolve(datar.models[0].name); - } else { - reject(new Error('No models available')); - } - } else { - reject(new Error('Failed to fetch models')); - } - }; - xhr.timeout = 10000; // 10 seconds timeout - xhr.ontimeout = function() { - reject(new Error('Request timed out')); - }; - xhr.onerror = function() { - reject(new Error('Network error while fetching models')); - }; - xhr.send(); + + if (targetModels.length) { + const model = targetModels[0].name !== exclude ? + targetModels[0] : targetModels[1] || targetModels[0]; + return model.name; + } + + const sizes = llmModels.map(m => ({ + model: m, + size: getSizeInBillions(m), + diff: Math.min( + Math.abs(getSizeInBillions(m) - 2), + Math.abs(getSizeInBillions(m) - 8) + ) + })); + + const closest = sizes.sort((a, b) => a.diff - b.diff)[0]; + return closest.model.name; + }; + + if (typeof ipcRenderer !== 'undefined') { + return new Promise(async (resolve, reject) => { + let ccc = setTimeout(() => reject(new Error('Request timed out')), 10000); + try { + const xhr = await fetchNode(`${ollamaendpoint}/api/tags`); + clearTimeout(ccc); + const datar = JSON.parse(xhr.data); + if (!datar?.models?.length) throw new Error('No models available'); + resolve(findBestModel(datar.models)); + } catch(e) { + clearTimeout(ccc); + reject(e); + } }); } + + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', `${ollamaendpoint}/api/tags`, true); + xhr.timeout = 10000; + + xhr.onload = function() { + if (xhr.status === 200) { + const datar = JSON.parse(xhr.responseText); + if (!datar?.models?.length) { + reject(new Error('No models available')); + return; + } + resolve(findBestModel(datar.models)); + } else { + reject(new Error('Failed to fetch models')); + } + }; + + xhr.ontimeout = () => reject(new Error('Request timed out')); + xhr.onerror = () => reject(new Error('Network error while fetching models')); + xhr.send(); + }); } @@ -207,9 +231,12 @@ async function callOllamaAPI(prompt, model = null, callback = null, abortControl let ollamamodel = model || settings.ollamamodel?.textsetting || tmpModelFallback || null; if (!ollamamodel) { - const availableModel0 = await getFirstAvailableModel(); - if (availableModel0) { - tmpModelFallback = availableModel0; + ollamamodel = await getFirstAvailableModel(); + if (ollamamodel) { + tmpModelFallback = ollamamodel; + setTimeout(() => { + tmpModelFallback = ""; + }, 60000); } else { console.error("No Ollama model found"); return; @@ -220,7 +247,7 @@ async function callOllamaAPI(prompt, model = null, callback = null, abortControl return result.response + "💥"; } else if (result.error && result.error === 404) { try { - const availableModel = await getFirstAvailableModel(); + const availableModel = await getFirstAvailableModel(ollamamodel); if (availableModel) { tmpModelFallback = availableModel; setTimeout(() => { @@ -353,7 +380,8 @@ async function callOllamaAPI(prompt, model = null, callback = null, abortControl const message = { model: currentModel, prompt: prompt, - stream: true + stream: true, + keep_alive: settings.ollamaKeepAlive ? parseInt(settings.ollamaKeepAlive.numbersetting)+"m" : "5m" }; if (images){ @@ -411,7 +439,8 @@ async function callOllamaAPI(prompt, model = null, callback = null, abortControl const message = { model: currentModel, prompt: prompt, - stream: isStreaming + stream: isStreaming, + keep_alive: settings.ollamaKeepAlive ? parseInt(settings.ollamaKeepAlive.numbersetting)+"m" : "5m" }; if (images){ @@ -838,8 +867,7 @@ async function processMessageWithOllama(data) { ollamaRateLimitPerTab = Math.max(0, parseInt(settings.ollamaRateLimitPerTab.numbersetting) || 0); } - if (data.type !== "stageten" && !settings.ollamaoverlayonly && data.tid && - lastResponseTime[data.tid] && (currentTime - lastResponseTime[data.tid] < ollamaRateLimitPerTab)) { + if (data.type !== "stageten" && !settings.ollamaoverlayonly && data.tid && lastResponseTime[data.tid] && (currentTime - lastResponseTime[data.tid] < ollamaRateLimitPerTab)) { isProcessing = false; return; } @@ -851,9 +879,7 @@ async function processMessageWithOllama(data) { } // Early return conditions - if ((data.type === "stageten" && botname === data.chatname) || - !data.chatmessage || - (!settings.noollamabotname && data.chatmessage.startsWith(botname + ":"))) { + if ((data.type === "stageten" && botname === data.chatname) || !data.chatmessage || (!settings.noollamabotname && data.chatmessage.startsWith(botname + ":"))) { isProcessing = false; return; } @@ -884,9 +910,10 @@ async function processMessageWithOllama(data) { } // Prevent self-replies - const score = levenshtein(cleanedText, lastSentMessage); - if (score < 7) { + const score = fastMessageSimilarity(cleanedText, lastSentMessage); + if (score > 0.5) { isProcessing = false; + console.log("RETURN", cleanedText, lastSentMessage); return; } @@ -895,14 +922,15 @@ async function processMessageWithOllama(data) { if (settings.ollamaprompt) { additionalInstructions = settings.ollamaprompt.textsetting; } - + console.log(additionalInstructions); const response = await processUserInput(cleanedText, data, additionalInstructions, botname); + console.log(response); // Handle response - if (response && !response.toLowerCase().startsWith("not available") && + if (response && !response.toLowerCase().startsWith("not available") && (settings.alwaysRespondLLM || ( !response.includes("NO_RESPONSE") && !response.startsWith("No ") && - !response.startsWith("NO ")) { + !response.startsWith("NO ")))) { // Send to overlay if enabled sendTargetP2P({ @@ -978,11 +1006,19 @@ async function processUserInput(userInput, data, additionalInstructions, botname if (await isRAGConfigured()) { const databaseDescriptor = localStorage.getItem('databaseDescriptor') || ''; - const ragPrompt = `${promptBase}\n\nDatabase info: ${databaseDescriptor}\n\n` + - 'Determine if this message requires searching the database. Format response as:\n' + - '[NEEDS_SEARCH]\nyes/no\n[/NEEDS_SEARCH]\n\n' + - '[SEARCH_QUERY]\nkeywords if search needed\n[/SEARCH_QUERY]\n\n' + - '[RESPONSE]\nDirect response if no search needed. Use NO_RESPONSE if no response warranted.\n[/RESPONSE]'; + if (settings.alwaysRespondLLM){ + var ragPrompt = `${promptBase}\n\nDatabase info: ${databaseDescriptor}\n\n` + + 'Determine if this message requires searching the database. Format response as:\n' + + '[NEEDS_SEARCH]\nyes/no\n[/NEEDS_SEARCH]\n\n' + + '[SEARCH_QUERY]\nkeywords if search needed\n[/SEARCH_QUERY]\n\n' + + '[RESPONSE]\nDirect response if no search needed.\n[/RESPONSE]'; + } else { + var ragPrompt = `${promptBase}\n\nDatabase info: ${databaseDescriptor}\n\n` + + 'Determine if this message requires searching the database. Format response as:\n' + + '[NEEDS_SEARCH]\nyes/no\n[/NEEDS_SEARCH]\n\n' + + '[SEARCH_QUERY]\nkeywords if search needed\n[/SEARCH_QUERY]\n\n' + + '[RESPONSE]\nDirect response if no search needed. Use NO_RESPONSE if no response warranted.\n[/RESPONSE]'; + } const ragDecision = await callOllamaAPI(ragPrompt); const decision = parseDecision(ragDecision); @@ -1007,6 +1043,13 @@ async function processUserInput(userInput, data, additionalInstructions, botname } else { promptBase += '\n\nRespond conversationally to the current group chat message only if the message seems directed at you specifically, doing so directly and succinctly, or instead reply with NO_RESPONSE if no response is neede, followed by why no response was needed.'; } + } else if (settings.alwaysRespondLLM){ + if (!settings.nollmcontext){ + promptBase += '\n\nRespond conversationally to the current message, doing so directly and succinctly.'; + } else { + promptBase += '\n\nRespond conversationally to the current group chat message, doing so directly and succinctly.'; + } + } else { if (!settings.nollmcontext){ promptBase += '\n\nRespond conversationally to the current message, if appropriate, doing so directly and succinctly, or instead reply with NO_RESPONSE to state you are choosing not to respond. Respond only with NO_RESPONSE if you have no reply.'; @@ -1020,6 +1063,9 @@ async function processUserInput(userInput, data, additionalInstructions, botname if (!response || response.toLowerCase().includes('no_response') || response.toLowerCase().startsWith('no ') || response.toLowerCase().startsWith('@@@@')) { console.log(response); + if (settings.alwaysRespondLLM && (response && !response.toLowerCase().startsWith('@@@@'))){ + return response; + } return false; } diff --git a/background.js b/background.js index 88f9fde1..2d2a3c4b 100644 --- a/background.js +++ b/background.js @@ -3487,90 +3487,6 @@ chrome.runtime.onMessage.addListener(async function (request, sender, sendRespon return response; }); -// The below "levenshtein" function is based on the follow work: -// https://github.com/gustf/js-levenshtein -// MIT License - Requires preservation of copyright and license notice -// Copyright (c) 2017 Gustaf Andersson -function levenshtein(a, b) { - if (a === b) { - return 0; - } - if (a.length > b.length) { - var tmp = a; - a = b; - b = tmp; - } - var la = a.length; - var lb = b.length; - while (la > 0 && a.charCodeAt(la - 1) === b.charCodeAt(lb - 1)) { - la--; - lb--; - } - var offset = 0; - while (offset < la && a.charCodeAt(offset) === b.charCodeAt(offset)) { - offset++; - } - la -= offset; - lb -= offset; - if (la === 0 || lb < 3) { - return lb; - } - var x = 0; - var y; - var d0; - var d1; - var d2; - var d3; - var dd; - var dy; - var ay; - var bx0; - var bx1; - var bx2; - var bx3; - var vector = []; - for (y = 0; y < la; y++) { - vector.push(y + 1); - vector.push(a.charCodeAt(offset + y)); - } - var len = vector.length - 1; - for (; x < lb - 3; ) { - bx0 = b.charCodeAt(offset + (d0 = x)); - bx1 = b.charCodeAt(offset + (d1 = x + 1)); - bx2 = b.charCodeAt(offset + (d2 = x + 2)); - bx3 = b.charCodeAt(offset + (d3 = x + 3)); - dd = x += 4; - for (y = 0; y < len; y += 2) { - dy = vector[y]; - ay = vector[y + 1]; - d0 = _min(dy, d0, d1, bx0, ay); - d1 = _min(d0, d1, d2, bx1, ay); - d2 = _min(d1, d2, d3, bx2, ay); - dd = _min(d2, d3, dd, bx3, ay); - vector[y] = dd; - d3 = d2; - d2 = d1; - d1 = d0; - d0 = dy; - } - } - for (; x < lb; ) { - bx0 = b.charCodeAt(offset + (d0 = x)); - dd = ++x; - for (y = 0; y < len; y += 2) { - dy = vector[y]; - vector[y] = dd = _min(dy, d0, dd, bx0, vector[y + 1]); - d0 = dy; - } - } - return dd; -} -function _min(d0, d1, d2, bx, ay) { - return d0 < d1 || d2 < d1 ? (d0 > d2 ? d2 + 1 : d0 + 1) : bx === ay ? d1 : d1 + 1; -} -//// End of levenshtein code -//////////////////////////// - function verifyOriginalNewIncomingMessage(msg, cleaned=false) { if (Date.now() - lastSentTimestamp > 5000) { @@ -3585,9 +3501,9 @@ function verifyOriginalNewIncomingMessage(msg, cleaned=false) { msg = decodeAndCleanHtml(msg); } - var score = levenshtein(msg, lastSentMessage); + var score = fastMessageSimilarity(msg, lastSentMessage); // console.log(msg, score); - if (score < 7) { // same message + if (score > 0.5) { // same message lastMessageCounter += 1; if (lastMessageCounter>1) { @@ -3607,6 +3523,52 @@ function verifyOriginalNewIncomingMessage(msg, cleaned=false) { } +function fastMessageSimilarity(a, b) { + if (a === b) return 1; + if (!a || !b) return 0; + + const normalize = str => str + .toLowerCase() + .replace(/[\u{1F300}-\u{1F9FF}]/gu, '') + .replace(/\s+/g, '') + .trim(); + + const normA = normalize(a); + const normB = normalize(b); + + // Handle exact match after normalization + if (normA === normB) return 1; + + const maxLen = Math.max(normA.length, normB.length); + const minLen = Math.min(normA.length, normB.length); + + // Check if one is prefix of the other + const shorter = normA.length < normB.length ? normA : normB; + const longer = normA.length < normB.length ? normB : normA; + + // For messages > 50 chars, if one is a prefix of the other + // and covers at least 90% of the shorter message, consider it similar + if (maxLen > 50 && longer.startsWith(shorter) && minLen / maxLen > 0.9) { + return 0.95; + } + + // For very short strings + if (maxLen < 10) { + const matched = [...normA].filter(char => normB.includes(char)).length; + return matched / maxLen; + } + + // Compute similarity based on character matches for position-sensitive comparison + let matches = 0; + const compareLen = Math.min(normA.length, normB.length); + + for (let i = 0; i < compareLen; i++) { + if (normA[i] === normB[i]) matches++; + } + + return matches / maxLen; +} + function ajax(object2send, url, ajaxType = "PUT", type = "application/json; charset=utf-8") { try { if (ajaxType == "PUT" && putNode) { @@ -7489,7 +7451,6 @@ async function applyBotActions(data, tab = false) { console.log(e); // ai.js file missing? } } - if (settings.ollama){ try{ processMessageWithOllama(data); diff --git a/manifest.json b/manifest.json index 2149ab71..5af634ed 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "name": "Social Stream Ninja", "description": "Powerful tooling to engage live chat on Youtube, Twitch, Zoom, and more.", "manifest_version": 3, - "version": "3.9.1", + "version": "3.9.2", "homepage_url": "http://socialstream.ninja/", "icons": { "128": "icons/icon-128.png" diff --git a/popup.html b/popup.html index 1d33c79a..f3c5c36f 100644 --- a/popup.html +++ b/popup.html @@ -4873,6 +4873,9 @@

Configure LLM API 🦙

+ +
+
+ + 🗣️ Have the bot respond to every message +
+
diff --git a/popup.js b/popup.js index 951ca788..9ed72385 100644 --- a/popup.js +++ b/popup.js @@ -1278,35 +1278,25 @@ function update(response, sync=true){ } if (key == "aiProvider"){ + document.getElementById("ollamamodel").classList.add("hidden"); + document.getElementById("ollamaendpoint").classList.add("hidden"); + document.getElementById("chatgptApiKey").classList.add("hidden"); + document.getElementById("ollamaKeepAlive").classList.add("hidden"); + document.getElementById("geminiApiKey").classList.add("hidden"); + document.getElementById("geminimodel").classList.add("hidden"); + document.getElementById("chatgptmodel").classList.add("hidden"); + if (ele.value == "ollama"){ document.getElementById("ollamamodel").classList.remove("hidden"); + document.getElementById("ollamaKeepAlive").classList.remove("hidden"); document.getElementById("ollamaendpoint").classList.remove("hidden"); - document.getElementById("chatgptApiKey").classList.add("hidden"); - document.getElementById("geminiApiKey").classList.add("hidden"); - document.getElementById("geminimodel").classList.add("hidden"); - document.getElementById("chatgptmodel").classList.add("hidden"); } else if (ele.value == "chatgpt"){ document.getElementById("chatgptApiKey").classList.remove("hidden"); - document.getElementById("ollamamodel").classList.add("hidden"); - document.getElementById("ollamaendpoint").classList.add("hidden"); - document.getElementById("geminiApiKey").classList.add("hidden"); - document.getElementById("geminimodel").classList.add("hidden"); document.getElementById("chatgptmodel").classList.remove("hidden"); } else if (ele.value == "gemini"){ document.getElementById("geminiApiKey").classList.remove("hidden"); - document.getElementById("ollamamodel").classList.add("hidden"); - document.getElementById("ollamaendpoint").classList.add("hidden"); - document.getElementById("chatgptApiKey").classList.add("hidden"); document.getElementById("geminimodel").classList.remove("hidden"); - document.getElementById("chatgptmodel").classList.add("hidden"); - } else { - document.getElementById("ollamamodel").classList.add("hidden"); - document.getElementById("ollamaendpoint").classList.add("hidden"); - document.getElementById("chatgptApiKey").classList.add("hidden"); - document.getElementById("geminiApiKey").classList.add("hidden"); - document.getElementById("geminimodel").classList.add("hidden"); - document.getElementById("chatgptmodel").classList.add("hidden"); - } + } } } diff --git a/sources/nicovideo.js b/sources/nicovideo.js index aec94f5f..79c4f7f8 100644 --- a/sources/nicovideo.js +++ b/sources/nicovideo.js @@ -139,6 +139,13 @@ function (request, sender, sendResponse) { try { if ("focusChat" == request){ // if (prev.querySelector('[id^="message-username-"]')){ //slateTextArea- + + document.querySelectorAll('iframe').forEach(frame => frame.remove()); + document.querySelectorAll('*').forEach(el => { + if (el.shadowRoot) { + el.shadowRoot.querySelectorAll('iframe').forEach(frame => frame.remove()); + } + }); document.querySelector('textarea,input[type="text"].comment-text-box').focus(); sendResponse(true); return; diff --git a/sources/youtube.js b/sources/youtube.js index b7e3430f..ea521b4b 100644 --- a/sources/youtube.js +++ b/sources/youtube.js @@ -1022,11 +1022,11 @@ if (document.querySelectorAll('#menu > a').length==2){ clearInterval(waitTilClear); document.querySelectorAll('#menu > a')[1].click() - document.querySelector("yt-live-chat-header-renderer").style.display = "none"; + document.querySelector("yt-live-chat-header-renderer").style.maxHeight = "10px"; } },100) } else if (document.querySelector("#trigger") && !settings.autoLiveYoutube && marked){ - document.querySelector("yt-live-chat-header-renderer").style.display = "unset"; + document.querySelector("yt-live-chat-header-renderer").style.maxHeight = "unset"; marked = false; } }, 1000); From b4bde615cb984e570a727a8510dc1f35e93c9ccd Mon Sep 17 00:00:00 2001 From: steveseguin Date: Mon, 6 Jan 2025 01:51:38 -0500 Subject: [PATCH 2/7] ugh. git --- manifest.json | 2 +- youtube.png | Bin 0 -> 36060 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 youtube.png diff --git a/manifest.json b/manifest.json index 5af634ed..b1662d83 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "name": "Social Stream Ninja", "description": "Powerful tooling to engage live chat on Youtube, Twitch, Zoom, and more.", "manifest_version": 3, - "version": "3.9.2", + "version": "3.9.3", "homepage_url": "http://socialstream.ninja/", "icons": { "128": "icons/icon-128.png" diff --git a/youtube.png b/youtube.png new file mode 100644 index 0000000000000000000000000000000000000000..2dd7ef84fa2fa4ee44cd86eb2526b5daf05dfceb GIT binary patch literal 36060 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 literal 0 HcmV?d00001 From 34d9daa05fe15edd089d67ee54daa2f0b57a5372 Mon Sep 17 00:00:00 2001 From: steveseguin Date: Mon, 6 Jan 2025 01:44:57 -0500 Subject: [PATCH 3/7] merge fix --- popup.html | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/popup.html b/popup.html index f3c5c36f..b27bfd50 100644 --- a/popup.html +++ b/popup.html @@ -190,6 +190,13 @@ border-radius: 1px; } +@keyframes shake { + 0%, 50%, 100% { transform: rotate(0deg); } + 70% { transform: rotate(-20deg); } + 90% { transform: rotate(20deg); } +} + + #searchInput.show { display: block; width: calc(100% - 35px); /* Adjust width as needed */ @@ -201,8 +208,8 @@ right: 0px; z-index: 100; cursor: pointer; + font-size: 150%; animation: shake 2s ease-in-out; - font-size: 150%; } #activeIcon { position: absolute; @@ -211,7 +218,7 @@ cursor: pointer; display: block; height: 0; - font-size: 150%; + font-size: 150%; } .tts-test-button { display: inline-block; From 98b2a19962f587a74ddf5eb9aba8c361293e160b Mon Sep 17 00:00:00 2001 From: steveseguin Date: Mon, 6 Jan 2025 11:29:32 -0500 Subject: [PATCH 4/7] api docu updated --- ai.js | 2 +- api.md | 133 +++++++++++++++++++++++++++++++++++++++++++++++++ background.js | 2 +- popup.html | 22 +++++--- sampleapi.html | 86 ++++++++++++++++++++++++++++++-- 5 files changed, 231 insertions(+), 14 deletions(-) diff --git a/ai.js b/ai.js index e858435a..b84ed4e9 100644 --- a/ai.js +++ b/ai.js @@ -922,7 +922,7 @@ async function processMessageWithOllama(data) { if (settings.ollamaprompt) { additionalInstructions = settings.ollamaprompt.textsetting; } - console.log(additionalInstructions); + console.log(additionalInstructions, cleanedText, botname, data); const response = await processUserInput(cleanedText, data, additionalInstructions, botname); console.log(response); diff --git a/api.md b/api.md index 3722e308..b596022d 100644 --- a/api.md +++ b/api.md @@ -774,3 +774,136 @@ The battle page relies on the extension for receiving data: 1. The extension uses `sendDataP2P()` to send data to the battle page 2. Data can be sent via WebRTC or fallback to WebSocket if available 3. The extension can trigger game actions like starting the game + +I'll create a guide focused on integrating Social Stream Ninja with StreamDeck, specifically for sending custom messages. + + +# StreamDeck Integration Guide for Social Stream Ninja + +## Quick Setup Method + +1. Open StreamDeck software +2. Add a new "Website" action to your StreamDeck +3. Configure the URL using this format: +``` +https://io.socialstream.ninja/YOUR_SESSION_ID/sendEncodedChat/null/YOUR_MESSAGE +``` + +Replace: +- `YOUR_SESSION_ID` with your Social Stream Ninja session ID +- `YOUR_MESSAGE` with your URL-encoded message + +For example, to send "Hello Stream!": +``` +https://io.socialstream.ninja/abc123/sendEncodedChat/null/Hello%20Stream! +``` + +## Advanced Setup with Multi Actions + +For more flexibility, you can use Multi Actions to send different messages: + +1. Create a new "Multi Action" on your StreamDeck +2. Add "Website" actions for each command +3. Use these URL patterns: + +**WebSocket (WSS)** +``` +https://io.socialstream.ninja/YOUR_SESSION_ID/sendChat/null/YOUR_MESSAGE +``` + +**HTTPS POST** +``` +https://io.socialstream.ninja/YOUR_SESSION_ID +``` +With body: +```json +{ + "action": "sendChat", + "value": "YOUR_MESSAGE", + "apiid": "YOUR_SESSION_ID" +} +``` + +## Tips for StreamDeck Setup + +- Use URL encoding for special characters in messages +- You can create multiple buttons for different preset messages +- Chain commands using Multi Actions for complex sequences +- Add a delay between actions if needed using StreamDeck's delay feature + +## Testing Your Setup + +1. Find your session ID from the Social Stream API Sandbox +2. Create a test button with a simple message +3. Press the button to verify the message appears in your social platforms +4. Check the Social Stream API Sandbox's incoming messages panel to confirm delivery + +## Channel-Specific Messages + +To send to specific channels, add the channel parameter: + +``` +https://io.socialstream.ninja/YOUR_SESSION_ID/sendChat/null/YOUR_MESSAGE?channel=2 +``` + +Channels: +- 1: General communication +- 2: Dock +- 3: Featured content +- 4-7: Custom channels + +I'll add a section about Bitfocus Companion integration with what we can confirm from the provided information: + +# Using Bitfocus Companion with Social Stream Ninja + +Bitfocus Companion enables the reasonably priced Elgato Streamdeck to be a professional shotbox surface for a huge amount of different presentation switchers, video playback software and broadcast equipment. It supports Social Stream Ninja and VDO.Ninja! + +https://bitfocus.io/companion +https://bitfocus.io/connections/socialstream-ninja + +## Initial Setup + +1. Enable API Control: + - Open Social Stream Ninja + - Go to `Global settings and tools` > `Mechanics` + - Enable `Enable remote API control of extension` + +2. Get Your Session ID: + - Navigate to `Global settings and tools` > `Session Options` + - Copy your Session ID + - Alternatively, find it in your URL after `?session=` + +3. Configure Companion: + - Install the Social Stream Ninja module in Companion + - Paste your Session ID into the module settings + +## Available Actions + +The following commands are confirmed available in Companion: + +- Clear featured message +- Clear all messages +- Next in queue +- Toggle auto-show +- Feature next un-featured +- Reset Poll +- Close Poll +- Waitlist Controls +- Text to Speech (TTS) Controls +- Send Chat Message + +## Variables + +Companion can access: +- `queue_size`: Shows the current queue size + +## Comparison with StreamDeck + +Advantages of using Companion: +- Native integration with Social Stream Ninja +- No need for URL encoding or complex HTTP requests +- Direct access to all core functionality +- Real-time queue size monitoring through variables +- Can be used alongside StreamDeck for more complex setups + +This makes Companion a simpler alternative to the StreamDeck HTTP method described above, especially for basic Social Stream Ninja control. diff --git a/background.js b/background.js index 2d2a3c4b..a67afad1 100644 --- a/background.js +++ b/background.js @@ -5548,7 +5548,7 @@ async function processIncomingRequest(request, UUID = false) { // from the dock fowardOBSCommand(request); } } else if (request.value && ("target" in request) && UUID && request.action === "chatbot"){ // target is the callback ID - if (isExtensionOn && settings.allowChatBot){ + if (isExtensionOn && settings.allowChatBot){ // private chat bot try { // ollama run technobyte/Llama-3.3-70B-Abliterated:IQ2_XS diff --git a/popup.html b/popup.html index b27bfd50..4dbf5ee9 100644 --- a/popup.html +++ b/popup.html @@ -4839,7 +4839,7 @@

Assign roles/classes to certain us
@@ -4895,7 +4895,9 @@

Configure LLM API 🦙

-

Censor bot options 🤖🚫🤬

+

Censor bot options 🤖🚫🤬

+ Does not respond to chat, but has access to the chat. It can censor, filter, and sanitize incoming live chat message
+
-

Chat bot 🤖💬

+

Chat bot 🤖💬

+ This bot has access to the live streaming chat and can either respond to users in chat or respond its own special chat overlay
+
-
+
- 🗣️ Have the bot respond to every message + 🗣️🗣 Do not screen out any of the bot's replies
@@ -5004,12 +5008,14 @@

> Additional Bot Instructions (Optional)

+

Give the above chat bot access to additional custom knowledge that you can provide

+
- Use a custom knowledge-base (RAG) + Enable the custom knowledge-base (RAG)


-
+

Standalone one-on-one chat bot

+ This is different than the above chat bot. It allows for private 1 on 1 conversation on a dedicated page, just in case you want to talk privately to it. It does not have access to the RAG knowledge dataset currently.

+