openvibers/openvibe.live Commits
PowerChat overlay: AI chat viewers relay too, with proper placeholder avatars
Changes
@@@ -266,9 +266,25 @@ class AiViewersEngine {const chatServer = require('../../chat/chat-server');chatServer.broadcastToStream(streamId, chatMsg);chatServer.forwardToGlobal(streamId, chatMsg);-- chatServer.synthesizeAndBroadcastTTS(streamId, bot.username, message, null, null, `aibot:${bot.username.toLowerCase()}`);++ chatServer.synthesizeAndBroadcastTTS(streamId, bot.username, message, null, null, `aibot:${bot.username.toLowerCase()}`, null, id ? `m${id}` : null);} catch (e) { console.warn('[AI-Viewers] broadcast failed:', e.message); }++ // ...and into the streamer's PowerChat unified overlay. AI viewers inject here++ // directly (never through handleChatMessage where the normal relay lives), so++ // they were invisible on PowerChat while every human chatter showed up.++ try {++ const pc = require('../../integrations/powerchat-platform');++ if (worker.userId && pc.channelRelayEnabled(worker.userId, streamId)) {++ pc.forwardChat(worker.userId, {++ chatterName: bot.username,++ externalChatterId: `ai:${bot.username.toLowerCase()}`,++ message,++ messageId: id ? `ov-${id}` : undefined,++ avatarFallback: '🤖',++ });++ }++ } catch { /* non-critical */ }++worker.lastPostAt = Date.now();worker.lastPoster = bot.id;worker.nextGapMs = this._nextGap((Date.now() - worker.lastRealInputAt) < ACTIVE_WINDOW_MS);@@@ -446,6 +446,9 @@ class ChatRelayService {message: chatMsg.message,messageId: chatMsg.id ? `ov-${chatMsg.id}` : undefined,avatarUrl: extras.avatar_url || undefined,++ // Placeholder letter from the REAL name — "[Twitch] name" would++ // otherwise render "[" for every relayed chatter.++ avatarFallback: [...String(username || '')][0] || undefined,});}} catch { /* non-critical */ }@@@ -145,7 +145,7 @@ function destRelayEnabled(destId) {// ── chat:write ───────────────────────────────────────────────const _chatBuckets = new Map(); // userId → { count, resetAt } (~120/min limit; we cap at 100)--async function forwardChat(streamerUserId, { chatterName, externalChatterId, message, messageId, avatarUrl, isModerator, isSubscriber } = {}) {++async function forwardChat(streamerUserId, { chatterName, externalChatterId, message, messageId, avatarUrl, avatarFallback, isModerator, isSubscriber } = {}) {if (!message || !chatterName) return;const conn = _connFor(streamerUserId, 'chat:write');if (!conn) return;@@@ -169,6 +169,9 @@ async function forwardChat(streamerUserId, { chatterName, externalChatterId, mesmessage: String(message).slice(0, 500),messageId: String(messageId || crypto.randomUUID()).slice(0, 128),...(absAvatar ? { avatarUrl: absAvatar } : {}),++ // Placeholder grapheme when no avatar URL resolves (e.g. 🤖 for AI++ // viewers, or a clean letter for "[RS] name"-style prefixed sources).++ ...(avatarFallback ? { avatarFallback: String(avatarFallback).slice(0, 8) } : {}),...(isModerator ? { isModerator: true } : {}),...(isSubscriber ? { isSubscriber: true } : {}),},@@@ -753,6 +753,9 @@ class RobotStreamerService {message: mirrored.message,messageId: mirrored.id ? `ov-${mirrored.id}` : undefined,avatarUrl: data.avatar || undefined,++ // Placeholder letter from the REAL name — "[RS] name"++ // would otherwise render "[" for every RS chatter.++ avatarFallback: [...String(rawUsername || '')][0] || undefined,});}} catch { /* non-critical */ }
@@ -266,9 +266,25 @@ class AiViewersEngine {
@@ -266,9 +266,25 @@ class AiViewersEngine {
const chatServer = require('../../chat/chat-server');
const chatServer = require('../../chat/chat-server');
chatServer.broadcastToStream(streamId, chatMsg);
chatServer.broadcastToStream(streamId, chatMsg);
chatServer.forwardToGlobal(streamId, chatMsg);
chatServer.forwardToGlobal(streamId, chatMsg);
- chatServer.synthesizeAndBroadcastTTS(streamId, bot.username, message, null, null, `aibot:${bot.username.toLowerCase()}`);
+ chatServer.synthesizeAndBroadcastTTS(streamId, bot.username, message, null, null, `aibot:${bot.username.toLowerCase()}`, null, id ? `m${id}` : null);
} catch (e) { console.warn('[AI-Viewers] broadcast failed:', e.message); }
} catch (e) { console.warn('[AI-Viewers] broadcast failed:', e.message); }
+ // ...and into the streamer's PowerChat unified overlay. AI viewers inject here
+ // directly (never through handleChatMessage where the normal relay lives), so
+ // they were invisible on PowerChat while every human chatter showed up.
+ try {
+ const pc = require('../../integrations/powerchat-platform');
+ if (worker.userId && pc.channelRelayEnabled(worker.userId, streamId)) {
+ pc.forwardChat(worker.userId, {
+ chatterName: bot.username,
+ externalChatterId: `ai:${bot.username.toLowerCase()}`,
+ message,
+ messageId: id ? `ov-${id}` : undefined,
+ avatarFallback: '🤖',
+ });
+ }
+ } catch { /* non-critical */ }
+
worker.lastPostAt = Date.now();
worker.lastPostAt = Date.now();
worker.lastPoster = bot.id;
worker.lastPoster = bot.id;
worker.nextGapMs = this._nextGap((Date.now() - worker.lastRealInputAt) < ACTIVE_WINDOW_MS);
worker.nextGapMs = this._nextGap((Date.now() - worker.lastRealInputAt) < ACTIVE_WINDOW_MS);
@@ -446,6 +446,9 @@ class ChatRelayService {
@@ -446,6 +446,9 @@ class ChatRelayService {
message: chatMsg.message,
message: chatMsg.message,
messageId: chatMsg.id ? `ov-${chatMsg.id}` : undefined,
messageId: chatMsg.id ? `ov-${chatMsg.id}` : undefined,
avatarUrl: extras.avatar_url || undefined,
avatarUrl: extras.avatar_url || undefined,
+ // Placeholder letter from the REAL name — "[Twitch] name" would
+ // otherwise render "[" for every relayed chatter.
+ avatarFallback: [...String(username || '')][0] || undefined,
});
});
}
}
} catch { /* non-critical */ }
} catch { /* non-critical */ }
@@ -145,7 +145,7 @@ function destRelayEnabled(destId) {
@@ -145,7 +145,7 @@ function destRelayEnabled(destId) {
// ── chat:write ───────────────────────────────────────────────
// ── chat:write ───────────────────────────────────────────────
const _chatBuckets = new Map(); // userId → { count, resetAt } (~120/min limit; we cap at 100)
const _chatBuckets = new Map(); // userId → { count, resetAt } (~120/min limit; we cap at 100)
-async function forwardChat(streamerUserId, { chatterName, externalChatterId, message, messageId, avatarUrl, isModerator, isSubscriber } = {}) {
+async function forwardChat(streamerUserId, { chatterName, externalChatterId, message, messageId, avatarUrl, avatarFallback, isModerator, isSubscriber } = {}) {
if (!message || !chatterName) return;
if (!message || !chatterName) return;
const conn = _connFor(streamerUserId, 'chat:write');
const conn = _connFor(streamerUserId, 'chat:write');
if (!conn) return;
if (!conn) return;
@@ -169,6 +169,9 @@ async function forwardChat(streamerUserId, { chatterName, externalChatterId, mes
@@ -169,6 +169,9 @@ async function forwardChat(streamerUserId, { chatterName, externalChatterId, mes
message: String(message).slice(0, 500),
message: String(message).slice(0, 500),
messageId: String(messageId || crypto.randomUUID()).slice(0, 128),
messageId: String(messageId || crypto.randomUUID()).slice(0, 128),
...(absAvatar ? { avatarUrl: absAvatar } : {}),
...(absAvatar ? { avatarUrl: absAvatar } : {}),
+ // Placeholder grapheme when no avatar URL resolves (e.g. 🤖 for AI
+ // viewers, or a clean letter for "[RS] name"-style prefixed sources).
+ ...(avatarFallback ? { avatarFallback: String(avatarFallback).slice(0, 8) } : {}),
...(isModerator ? { isModerator: true } : {}),
...(isModerator ? { isModerator: true } : {}),
...(isSubscriber ? { isSubscriber: true } : {}),
...(isSubscriber ? { isSubscriber: true } : {}),
},
},
@@ -753,6 +753,9 @@ class RobotStreamerService {
@@ -753,6 +753,9 @@ class RobotStreamerService {
message: mirrored.message,
message: mirrored.message,
messageId: mirrored.id ? `ov-${mirrored.id}` : undefined,
messageId: mirrored.id ? `ov-${mirrored.id}` : undefined,
avatarUrl: data.avatar || undefined,
avatarUrl: data.avatar || undefined,
+ // Placeholder letter from the REAL name — "[RS] name"
+ // would otherwise render "[" for every RS chatter.
+ avatarFallback: [...String(rawUsername || '')][0] || undefined,
});
});
}
}
} catch { /* non-critical */ }
} catch { /* non-critical */ }