// 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'; function roleColors(role) { switch (role) { case 'admin': case 'lockdown': case 'lockdown-admin': return 'text-amber-300'; case 'spectator': return 'text-slate-400'; default: return 'text-sky-300'; } } function isBotMessage(message) { return Boolean(message?.bot); } function formatTime(ts) { const date = new Date(ts); return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; } function displayName(message) { return message.nickname || message.socketId?.slice(0, 6) || 'unknown'; } function DiscordAvatar({ guildIconUrl, userAvatarUrl, label }) { if (!guildIconUrl && !userAvatarUrl) return null; return ( ); } function ProfileAvatar({ imageUrl, label }) { if (!imageUrl) return null; return ( ); } export function ChatIdentity({ message, toolsToggle = null }) { const discordLabel = message.fromDiscord ? `${message.discordGuildName || 'Discord'} · ${displayName(message)}` : null; const isBot = isBotMessage(message); const nameClass = isBot ? 'text-emerald-300' : roleColors(message.role); return ( {message.fromDiscord ? ( ) : null} {displayName(message)} {!message.profileImage ? ( ) : null} {isBot ? ( bot ) : null} {message.roverId && ( {message.roverId} )} {toolsToggle} ); } function chatRowClass(message) { if (isBotMessage(message)) { return 'surface-muted flex flex-col gap-0.5 border border-emerald-500/40 bg-emerald-900/15 text-sm'; } const isAdmin = message.role === 'admin' || message.role === 'lockdown' || message.role === 'lockdown-admin'; return `surface-muted flex flex-col gap-0.5 text-sm ${ isAdmin ? 'border border-amber-400/30' : message.fromDiscord ? 'border border-indigo-400/30 bg-indigo-900/20' : '' }`; } 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 (
0 ? ( ) : null } /> {!open && hasText ? ( {message.text} ) : ( )} {formatTime(message.ts)}
{open && toolCalls.length > 0 ? (
{toolCalls.map((entry, idx) => { const status = String(entry?.status || 'unknown'); const statusLabel = status === 'ok' ? 'ok' : status === 'blocked' ? 'blocked' : status === 'error' ? 'error' : 'unknown'; const argsText = JSON.stringify(entry?.args && typeof entry.args === 'object' ? entry.args : {}); return (
{statusLabel} {entry?.tool || 'unknown'} args={argsText}
{entry?.error ?
error: {String(entry.error)}
: null}
); })}
) : null} {open && hasText ? (
{message.text}
) : null}
); } export { chatRowClass };