mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
wording and overseer tool embeds
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,8 +11,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-3_2nPjxD.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-LJWnKvT4.css">
|
||||
<script type="module" crossorigin src="/assets/index-BWk3SALU.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BtUMEmG6.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -100,6 +100,21 @@ function buildRoverCtxSnapshot(roverId) {
|
||||
function buildMessage(socket, text, meta = {}) {
|
||||
const roverId = meta.roverId || resolveRoverId(socket?.id);
|
||||
const roverColor = meta.roverColor ?? resolveRoverColor(roverId);
|
||||
const toolCalls = Array.isArray(meta.toolCalls)
|
||||
? meta.toolCalls
|
||||
.map((entry) => {
|
||||
if (!entry || typeof entry !== 'object') return null;
|
||||
return {
|
||||
tool: String(entry.tool || '').trim() || 'unknown',
|
||||
status: String(entry.status || '').trim() || 'unknown',
|
||||
args: entry.args && typeof entry.args === 'object' ? entry.args : {},
|
||||
result: entry.result && typeof entry.result === 'object' ? entry.result : null,
|
||||
error: entry.error ? String(entry.error) : null,
|
||||
durationMs: Number.isFinite(entry.durationMs) ? Math.max(0, Math.round(entry.durationMs)) : null,
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
: null;
|
||||
return {
|
||||
id: uuidv4(),
|
||||
ts: Date.now(),
|
||||
@@ -119,6 +134,7 @@ function buildMessage(socket, text, meta = {}) {
|
||||
profileImage: normalizeProfileImageUrl(meta.profileImage),
|
||||
roverCtx: meta.roverCtx || null,
|
||||
text,
|
||||
toolCalls,
|
||||
tts: meta.tts || null,
|
||||
bot: Boolean(meta.bot),
|
||||
};
|
||||
|
||||
@@ -11,7 +11,8 @@ const { registerChatSocketHooks } = require('./socketHooks');
|
||||
function sendSystemMessage(text, options = {}) {
|
||||
const normalized = normalizeUserText(text);
|
||||
const clean = normalized.trim();
|
||||
if (!clean) return null;
|
||||
const hasToolCalls = Array.isArray(options.toolCalls) && options.toolCalls.length > 0;
|
||||
if (!clean && !hasToolCalls) return null;
|
||||
const safe = clean;
|
||||
const message = buildMessage(null, safe, {
|
||||
nickname: String(options.nickname || 'The Overseer'),
|
||||
@@ -19,6 +20,7 @@ function sendSystemMessage(text, options = {}) {
|
||||
fromDiscord: false,
|
||||
bot: options.bot !== false,
|
||||
profileImage: options.profileImage || null,
|
||||
toolCalls: hasToolCalls ? options.toolCalls : null,
|
||||
});
|
||||
broadcastMessage(message);
|
||||
return message;
|
||||
|
||||
@@ -3,6 +3,21 @@
|
||||
// Scope: Handles inbound Discord messages plus outbound webhook and typing relay.
|
||||
const { WebhookClient } = require('discord.js');
|
||||
|
||||
function summarizeToolCall(entry = {}) {
|
||||
const tool = String(entry?.tool || 'unknown');
|
||||
const status = String(entry?.status || 'unknown').toLowerCase();
|
||||
if (status === 'ok') return `✅ ${tool}`;
|
||||
if (status === 'blocked') return `⛔ ${tool} — ${String(entry?.error || 'blocked')}`;
|
||||
if (status === 'error') return `❌ ${tool} — ${String(entry?.error || 'failed')}`;
|
||||
return `• ${tool}`;
|
||||
}
|
||||
|
||||
function formatToolCallsCodeBlock(toolCalls = []) {
|
||||
const rows = (toolCalls || []).map((entry) => summarizeToolCall(entry));
|
||||
if (!rows.length) return '';
|
||||
return `\`\`\`txt\nTool calls:\n${rows.join('\n')}\n\`\`\``;
|
||||
}
|
||||
|
||||
function createChatBridgeHandlers(deps) {
|
||||
const {
|
||||
logger,
|
||||
@@ -48,7 +63,15 @@ function createChatBridgeHandlers(deps) {
|
||||
const guildConfigs = listGuildConfigs();
|
||||
if (!guildConfigs.length) return;
|
||||
|
||||
const text = payload.text?.length > 1900 ? `${payload.text.slice(0, 1897)}...` : payload.text;
|
||||
const baseText = String(payload.text || '');
|
||||
const toolCalls = Array.isArray(payload.toolCalls) ? payload.toolCalls : [];
|
||||
const toolsBlock = formatToolCallsCodeBlock(toolCalls);
|
||||
let text = baseText;
|
||||
if (toolsBlock) {
|
||||
text = text ? `${text}\n\n${toolsBlock}` : `Overseer ran tools (no chat line).\n\n${toolsBlock}`;
|
||||
}
|
||||
if (text.length > 1900) text = `${text.slice(0, 1897)}...`;
|
||||
if (!text.trim()) return;
|
||||
const username = formatWebhookUsername(payload);
|
||||
const avatarURL = payload.profileImage || (payload.fromDiscord ? payload.discordUserAvatarUrl || null : client.user?.displayAvatarURL?.({ extension: 'png', size: 128 }) || null);
|
||||
const typingId = getTypingId(payload);
|
||||
|
||||
@@ -207,6 +207,44 @@ function normalizeChatDraft(text) {
|
||||
return next;
|
||||
}
|
||||
|
||||
function summarizeResult(result) {
|
||||
if (!result || typeof result !== 'object') return null;
|
||||
if (Object.prototype.hasOwnProperty.call(result, 'ok')) return { ok: Boolean(result.ok) };
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildToolCallFeedEntries(requestedActions = [], actionResults = []) {
|
||||
const resultsByTool = new Map();
|
||||
(actionResults || []).forEach((entry) => {
|
||||
if (!entry || entry.kind !== 'tool') return;
|
||||
const key = String(entry.tool || '');
|
||||
if (!key) return;
|
||||
if (!resultsByTool.has(key)) resultsByTool.set(key, []);
|
||||
resultsByTool.get(key).push(entry);
|
||||
});
|
||||
return (requestedActions || []).map((action) => {
|
||||
const tool = String(action?.tool || '').trim() || 'unknown';
|
||||
const bucket = resultsByTool.get(tool) || [];
|
||||
const resultEntry = bucket.length ? bucket.shift() : null;
|
||||
const ok = Boolean(resultEntry?.ok);
|
||||
const errText = resultEntry?.error ? String(resultEntry.error) : '';
|
||||
const status = resultEntry
|
||||
? ok
|
||||
? 'ok'
|
||||
: errText.includes('blocked') || errText.includes('unavailable')
|
||||
? 'blocked'
|
||||
: 'error'
|
||||
: 'started';
|
||||
return {
|
||||
tool,
|
||||
status,
|
||||
args: action?.args && typeof action.args === 'object' ? action.args : {},
|
||||
result: summarizeResult(resultEntry?.result),
|
||||
error: errText || null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function runDecision(triggerReason) {
|
||||
const runId = runtime.tickCount;
|
||||
updateStatus({ phase: 'context_build', currentRunId: runId, lastTriggerReason: triggerReason, lastError: null, lastErrorDetails: null });
|
||||
@@ -280,11 +318,6 @@ async function runDecision(triggerReason) {
|
||||
const reason = observeOnly ? 'observe-only mode' : null;
|
||||
|
||||
if (!observeOnly) {
|
||||
if ((decision === 'CHAT' || decision === 'ACTION+CHAT') && chatDraft) {
|
||||
sendSystemMessage(chatDraft, { nickname: name, bot: true, profileImage: profileImageUrl });
|
||||
actionResults.push({ kind: 'chat', ok: true });
|
||||
}
|
||||
|
||||
if (decision === 'ACTION' || decision === 'ACTION+CHAT') {
|
||||
for (const action of requestedActions) {
|
||||
pushLiveToolCall({ phase: 'start', tool: action.tool, args: action.args });
|
||||
@@ -315,6 +348,17 @@ async function runDecision(triggerReason) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toolCallFeed = buildToolCallFeedEntries(requestedActions, actionResults);
|
||||
if (toolCallFeed.length > 0) {
|
||||
if ((decision === 'CHAT' || decision === 'ACTION+CHAT') && chatDraft) {
|
||||
sendSystemMessage(chatDraft, { nickname: name, bot: true, profileImage: profileImageUrl, toolCalls: toolCallFeed });
|
||||
} else {
|
||||
sendSystemMessage('', { nickname: name, bot: true, profileImage: profileImageUrl, toolCalls: toolCallFeed });
|
||||
}
|
||||
} else if ((decision === 'CHAT' || decision === 'ACTION+CHAT') && chatDraft) {
|
||||
sendSystemMessage(chatDraft, { nickname: name, bot: true, profileImage: profileImageUrl });
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Chat Message Row
|
||||
// Purpose: Defines the Chat Message Row module and the local helpers/components used in this file.
|
||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||
import { useState } from 'react';
|
||||
import { FaDiscord } from 'react-icons/fa';
|
||||
import { roverBadgeStyle } from '../../lib/roverColor.js';
|
||||
|
||||
@@ -133,12 +134,40 @@ function chatRowClass(message) {
|
||||
|
||||
export default function ChatMessageRow({ message }) {
|
||||
const isBot = isBotMessage(message);
|
||||
const [open, setOpen] = useState(false);
|
||||
const toolCalls = Array.isArray(message?.toolCalls) ? message.toolCalls : [];
|
||||
const hasText = Boolean(String(message?.text || '').trim());
|
||||
return (
|
||||
<div className={chatRowClass(message)}>
|
||||
<span
|
||||
className={`min-w-0 flex-1 break-words leading-tight whitespace-pre-wrap ${isBot ? 'text-emerald-100' : 'text-slate-100'}`}
|
||||
>
|
||||
<ChatIdentity message={message} /> {message.text}
|
||||
<ChatIdentity message={message} />{hasText ? ` ${message.text}` : ''}
|
||||
{toolCalls.length > 0 ? (
|
||||
<span className="mt-0.5 block">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-slate-600/70 bg-slate-800/60 px-1 py-[1px] text-[0.65rem] text-slate-200 hover:bg-slate-700/70"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
>
|
||||
{open ? '▼' : '▶'} Tools ({toolCalls.length})
|
||||
</button>
|
||||
{open ? (
|
||||
<div className="mt-0.5 rounded border border-slate-700/70 bg-slate-900/70 p-0.5 text-[0.68rem] text-slate-200">
|
||||
{toolCalls.map((entry, idx) => {
|
||||
const status = String(entry?.status || 'unknown');
|
||||
const icon = status === 'ok' ? '✅' : status === 'blocked' ? '⛔' : status === 'error' ? '❌' : '•';
|
||||
return (
|
||||
<div key={`${entry?.tool || 'tool'}-${idx}`} className="mb-0.5 last:mb-0">
|
||||
<div>{icon} {entry?.tool || 'unknown'}</div>
|
||||
{entry?.error ? <div className="text-rose-300">error: {String(entry.error)}</div> : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="ml-auto shrink-0 text-[0.65rem] text-slate-400/60">
|
||||
{formatTime(message.ts)}
|
||||
|
||||
@@ -127,7 +127,7 @@ export default function DriveDockAction({
|
||||
steps: [
|
||||
'Line up the center dot of your rover with the center of the dock',
|
||||
'The UI will indicate when you are successfully docked',
|
||||
'When good contact is made, charging will start in about 5 seconds.',
|
||||
'When good contact is made, the dock will stop flashing.',
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user