mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-26 14:32:22 +02:00
[eric] split: pull MCP result cards out of ToolCallBubble
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import EventIcon from '@mui/icons-material/Event';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useCardColors } from './cardColors';
|
||||
import { formatTimestamp } from './mcpCardHelpers';
|
||||
|
||||
export const CalendarCard: React.FC<{ data: Record<string, any>; hideHeader?: boolean }> = ({ data, hideHeader }) => {
|
||||
const c = useClaudeTokens();
|
||||
const { TC_BG, TC_BORDER, TC_HOVER, TC_HEADING, TC_BODY, TC_DIM, TC_SUCCESS } = useCardColors();
|
||||
const items: any[] = data.items || (Array.isArray(data) ? data : []);
|
||||
const single = !items.length ? data : null;
|
||||
|
||||
if (single && (single.summary || single.start)) {
|
||||
const start = single.start?.dateTime || single.start?.date || single.start || '';
|
||||
const end = single.end?.dateTime || single.end?.date || single.end || '';
|
||||
return (
|
||||
<Box sx={{
|
||||
...(hideHeader
|
||||
? { overflow: 'hidden' }
|
||||
: { bgcolor: TC_BG, border: `1px solid ${TC_BORDER}`, borderRadius: 1.5, mx: 1.5, my: 1, overflow: 'hidden' }),
|
||||
}}>
|
||||
{!hideHeader && (
|
||||
<Box sx={{
|
||||
display: 'flex', alignItems: 'center', gap: 0.75,
|
||||
px: 1.5, py: 0.85, borderBottom: `1px solid ${TC_BORDER}`,
|
||||
}}>
|
||||
<EventIcon sx={{ fontSize: 14, color: TC_SUCCESS, opacity: 0.8 }} />
|
||||
<span style={{ color: TC_HEADING, fontSize: '0.78rem', fontWeight: 600, fontFamily: c.font.sans }}>
|
||||
{single.summary || '(no title)'}
|
||||
</span>
|
||||
</Box>
|
||||
)}
|
||||
<Box sx={{ px: 1.5, py: 1, display: 'flex', flexDirection: 'column', gap: 0.3 }}>
|
||||
{start && (
|
||||
<Box sx={{ display: 'flex', gap: 0.75, fontSize: '0.7rem', alignItems: 'baseline' }}>
|
||||
<span style={{ color: TC_DIM, minWidth: 48, fontFamily: c.font.mono, fontSize: '0.62rem', textTransform: 'uppercase', letterSpacing: '0.04em' }}>Start</span>
|
||||
<span style={{ color: TC_BODY, fontFamily: c.font.sans }}>{formatTimestamp(start)}</span>
|
||||
</Box>
|
||||
)}
|
||||
{end && (
|
||||
<Box sx={{ display: 'flex', gap: 0.75, fontSize: '0.7rem', alignItems: 'baseline' }}>
|
||||
<span style={{ color: TC_DIM, minWidth: 48, fontFamily: c.font.mono, fontSize: '0.62rem', textTransform: 'uppercase', letterSpacing: '0.04em' }}>End</span>
|
||||
<span style={{ color: TC_BODY, fontFamily: c.font.sans }}>{formatTimestamp(end)}</span>
|
||||
</Box>
|
||||
)}
|
||||
{single.location && (
|
||||
<Box sx={{ display: 'flex', gap: 0.75, fontSize: '0.7rem', alignItems: 'baseline' }}>
|
||||
<span style={{ color: TC_DIM, minWidth: 48, fontFamily: c.font.mono, fontSize: '0.62rem', textTransform: 'uppercase', letterSpacing: '0.04em' }}>Where</span>
|
||||
<span style={{ color: TC_BODY, fontFamily: c.font.sans }}>{single.location}</span>
|
||||
</Box>
|
||||
)}
|
||||
{single.description && (
|
||||
<Box sx={{ mt: 0.3, pt: 0.5, borderTop: `1px solid ${TC_BORDER}` }}>
|
||||
<pre style={{
|
||||
margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
||||
fontFamily: c.font.sans, fontSize: '0.68rem', lineHeight: 1.5,
|
||||
color: TC_BODY,
|
||||
}}>
|
||||
{single.description.slice(0, 300)}
|
||||
{single.description.length > 300 ? '…' : ''}
|
||||
</pre>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (items.length > 0) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75, p: 1.5, pt: 1 }}>
|
||||
{items.slice(0, 6).map((item: any, i: number) => (
|
||||
<Box key={i} sx={{
|
||||
bgcolor: TC_BG, border: `1px solid ${TC_BORDER}`,
|
||||
borderRadius: 1.5, px: 1.25, py: 0.75,
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 1,
|
||||
transition: 'background-color 0.15s',
|
||||
'&:hover': { bgcolor: TC_HOVER },
|
||||
}}>
|
||||
<span style={{ color: TC_HEADING, fontSize: '0.72rem', fontWeight: 500, fontFamily: c.font.sans }}>
|
||||
{item.summary || '(no title)'}
|
||||
</span>
|
||||
<span style={{ color: TC_DIM, fontSize: '0.6rem', flexShrink: 0, fontFamily: c.font.mono }}>
|
||||
{formatTimestamp(item.start?.dateTime || item.start?.date || item.start)}
|
||||
</span>
|
||||
</Box>
|
||||
))}
|
||||
{items.length > 6 && (
|
||||
<span style={{ color: TC_DIM, fontSize: '0.64rem', fontStyle: 'italic', textAlign: 'center', display: 'block' }}>
|
||||
+{items.length - 6} more
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useCardColors } from './cardColors';
|
||||
|
||||
export const DriveCard: React.FC<{ data: Record<string, any> }> = ({ data }) => {
|
||||
const c = useClaudeTokens();
|
||||
const { TC_BG, TC_BORDER, TC_HOVER, TC_HEADING, TC_DIM, TC_WARNING } = useCardColors();
|
||||
const files: any[] = data.files || (Array.isArray(data) ? data : []);
|
||||
const single = !files.length && data.name ? data : null;
|
||||
|
||||
if (single) {
|
||||
return (
|
||||
<Box sx={{
|
||||
bgcolor: TC_BG, border: `1px solid ${TC_BORDER}`,
|
||||
borderRadius: 1.5, mx: 1.5, my: 1, px: 1.25, py: 0.85, display: 'flex', alignItems: 'center', gap: 0.75,
|
||||
}}>
|
||||
<FolderIcon sx={{ fontSize: 16, color: TC_WARNING, opacity: 0.7 }} />
|
||||
<Box>
|
||||
<span style={{ color: TC_HEADING, fontSize: '0.73rem', fontWeight: 500, display: 'block', fontFamily: c.font.sans }}>
|
||||
{single.name}
|
||||
</span>
|
||||
{single.mimeType && (
|
||||
<span style={{ color: TC_DIM, fontSize: '0.6rem', fontFamily: c.font.mono }}>{single.mimeType}</span>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, p: 1.5, pt: 1 }}>
|
||||
{files.slice(0, 8).map((f: any, i: number) => (
|
||||
<Box key={i} sx={{
|
||||
bgcolor: TC_BG, border: `1px solid ${TC_BORDER}`,
|
||||
borderRadius: 1.5, px: 1.25, py: 0.6, display: 'flex', alignItems: 'center', gap: 0.75,
|
||||
transition: 'background-color 0.15s',
|
||||
'&:hover': { bgcolor: TC_HOVER },
|
||||
}}>
|
||||
<FolderIcon sx={{ fontSize: 13, color: TC_WARNING, opacity: 0.5 }} />
|
||||
<span style={{ color: TC_HEADING, fontSize: '0.7rem', fontFamily: c.font.sans }}>{f.name || f.id}</span>
|
||||
{f.mimeType && (
|
||||
<span style={{ color: TC_DIM, fontSize: '0.58rem', flexShrink: 0, fontFamily: c.font.mono }}>
|
||||
{f.mimeType.split('/').pop()}
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useCardColors } from './cardColors';
|
||||
|
||||
export const GenericMcpCard: React.FC<{ data: Record<string, any> }> = ({ data }) => {
|
||||
const c = useClaudeTokens();
|
||||
const { TC_DIM, TC_BODY } = useCardColors();
|
||||
const entries = Object.entries(data).filter(([, v]) => v != null);
|
||||
|
||||
if (entries.length === 0)
|
||||
return <span style={{ color: TC_DIM, fontStyle: 'italic', fontSize: '0.7rem', padding: '8px 12px', display: 'block' }}>(empty response)</span>;
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.3, px: 1.5, py: 1 }}>
|
||||
{entries.slice(0, 20).map(([key, val], i) => {
|
||||
const isLong = typeof val === 'string' && val.length > 100;
|
||||
const isObj = typeof val === 'object';
|
||||
return (
|
||||
<Box key={i} sx={{ fontSize: '0.7rem', display: 'flex', gap: 0.75, lineHeight: 1.5 }}>
|
||||
<span style={{ color: TC_DIM, minWidth: 72, flexShrink: 0, fontWeight: 500, fontFamily: c.font.mono, fontSize: '0.62rem', textTransform: 'uppercase', letterSpacing: '0.03em', paddingTop: 1 }}>
|
||||
{key}
|
||||
</span>
|
||||
{isObj ? (
|
||||
<pre style={{
|
||||
margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
||||
color: TC_BODY, fontFamily: c.font.mono, fontSize: '0.68rem',
|
||||
}}>
|
||||
{JSON.stringify(val, null, 2).slice(0, 500)}
|
||||
</pre>
|
||||
) : isLong ? (
|
||||
<pre style={{
|
||||
margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
||||
color: TC_BODY, fontFamily: c.font.sans, fontSize: '0.68rem',
|
||||
}}>
|
||||
{String(val).slice(0, 500)}{String(val).length > 500 ? '…' : ''}
|
||||
</pre>
|
||||
) : (
|
||||
<span style={{ color: TC_BODY, fontFamily: c.font.sans }}>{String(val)}</span>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{entries.length > 20 && (
|
||||
<span style={{ color: TC_DIM, fontSize: '0.62rem', fontStyle: 'italic' }}>
|
||||
+{entries.length - 20} more fields
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,196 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import EmailIcon from '@mui/icons-material/Email';
|
||||
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useCardColors } from './cardColors';
|
||||
import { extractEmailFields } from './mcpCardHelpers';
|
||||
|
||||
export const GmailCard: React.FC<{ data: Record<string, any>; action: string; hideSubjectHeader?: boolean }> = ({ data, action, hideSubjectHeader }) => {
|
||||
const c = useClaudeTokens();
|
||||
const { TC_BG, TC_BORDER, TC_HOVER, TC_HEADING, TC_BODY, TC_MUTED, TC_DIM, TC_ACCENT, TC_SUCCESS, TC_WARNING } = useCardColors();
|
||||
const email = extractEmailFields(data);
|
||||
const labels = data.labelIds || data.labels || [];
|
||||
const attachments = data.attachments || [];
|
||||
|
||||
const isSend = action.includes('send');
|
||||
const isSearch = action.includes('search') || action.includes('list');
|
||||
const messages: any[] = data.messages || (isSearch && data.results ? data.results : []);
|
||||
|
||||
if (messages.length > 0) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75, p: 1.5, pt: 1 }}>
|
||||
{messages.slice(0, 5).map((msg: any, i: number) => {
|
||||
const m = extractEmailFields(msg);
|
||||
return (
|
||||
<Box key={i} sx={{
|
||||
bgcolor: TC_BG, border: `1px solid ${TC_BORDER}`, borderRadius: 1.5,
|
||||
px: 1.25, py: 1, display: 'flex', flexDirection: 'column', gap: 0.4,
|
||||
transition: 'background-color 0.15s',
|
||||
'&:hover': { bgcolor: TC_HOVER },
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 1 }}>
|
||||
<span style={{ color: TC_HEADING, fontSize: '0.74rem', fontWeight: 600, fontFamily: c.font.sans }}>
|
||||
{m.subject}
|
||||
</span>
|
||||
{m.date && (
|
||||
<span style={{ color: TC_DIM, fontSize: '0.6rem', flexShrink: 0, fontFamily: c.font.mono }}>
|
||||
{m.date}
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
{m.from && (
|
||||
<span style={{ color: TC_MUTED, fontSize: '0.68rem', fontFamily: c.font.sans }}>
|
||||
{m.from}
|
||||
</span>
|
||||
)}
|
||||
{(m.snippet || m.bodyPreview) && (
|
||||
<span style={{ color: TC_BODY, fontSize: '0.68rem', lineHeight: 1.45, fontFamily: c.font.sans }}>
|
||||
{(m.snippet || m.bodyPreview).slice(0, 120)}
|
||||
{(m.snippet || m.bodyPreview).length > 120 ? '…' : ''}
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{messages.length > 5 && (
|
||||
<span style={{ color: TC_DIM, fontSize: '0.66rem', fontStyle: 'italic', textAlign: 'center', display: 'block' }}>
|
||||
+{messages.length - 5} more
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
...(hideSubjectHeader
|
||||
? { overflow: 'hidden' }
|
||||
: { bgcolor: TC_BG, border: `1px solid ${TC_BORDER}`, borderRadius: 1.5, mx: 1.5, my: 1, overflow: 'hidden' }),
|
||||
}}>
|
||||
{!hideSubjectHeader && (
|
||||
<Box sx={{
|
||||
display: 'flex', alignItems: 'center', gap: 0.75,
|
||||
px: 1.5, py: 0.85,
|
||||
borderBottom: `1px solid ${TC_BORDER}`,
|
||||
}}>
|
||||
{isSend ? (
|
||||
<SendIcon sx={{ fontSize: 14, color: TC_SUCCESS, opacity: 0.8 }} />
|
||||
) : (
|
||||
<EmailIcon sx={{ fontSize: 14, color: TC_ACCENT, opacity: 0.8 }} />
|
||||
)}
|
||||
<span style={{ color: TC_HEADING, fontSize: '0.78rem', fontWeight: 600, flex: 1, fontFamily: c.font.sans }}>
|
||||
{email.subject}
|
||||
</span>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ px: 1.5, py: 1, display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
{(email.from || email.to || email.date) && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.3 }}>
|
||||
{email.from && (
|
||||
<Box sx={{ display: 'flex', gap: 0.75, fontSize: '0.7rem', alignItems: 'baseline' }}>
|
||||
<span style={{ color: TC_DIM, minWidth: 32, fontFamily: c.font.mono, fontSize: '0.62rem', textTransform: 'uppercase', letterSpacing: '0.04em' }}>From</span>
|
||||
<span style={{ color: TC_BODY, fontFamily: c.font.sans }}>{email.from}</span>
|
||||
</Box>
|
||||
)}
|
||||
{email.to && (
|
||||
<Box sx={{ display: 'flex', gap: 0.75, fontSize: '0.7rem', alignItems: 'baseline' }}>
|
||||
<span style={{ color: TC_DIM, minWidth: 32, fontFamily: c.font.mono, fontSize: '0.62rem', textTransform: 'uppercase', letterSpacing: '0.04em' }}>To</span>
|
||||
<span style={{ color: TC_BODY, fontFamily: c.font.sans }}>{email.to}</span>
|
||||
</Box>
|
||||
)}
|
||||
{email.date && (
|
||||
<Box sx={{ display: 'flex', gap: 0.75, fontSize: '0.7rem', alignItems: 'baseline' }}>
|
||||
<span style={{ color: TC_DIM, minWidth: 32, fontFamily: c.font.mono, fontSize: '0.62rem', textTransform: 'uppercase', letterSpacing: '0.04em' }}>Date</span>
|
||||
<span style={{ color: TC_BODY, fontFamily: c.font.sans }}>{email.date}</span>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{labels.length > 0 && (
|
||||
<Box sx={{ display: 'flex', gap: 0.4, flexWrap: 'wrap', mt: 0.15 }}>
|
||||
{labels.map((l: string, i: number) => (
|
||||
<Box key={i} sx={{
|
||||
display: 'inline-flex', alignItems: 'center',
|
||||
bgcolor: `${TC_ACCENT}18`, borderRadius: 0.75,
|
||||
px: 0.6, py: 0.1,
|
||||
}}>
|
||||
<span style={{ fontSize: '0.56rem', color: TC_ACCENT, fontFamily: c.font.mono, fontWeight: 500, textTransform: 'uppercase', letterSpacing: '0.03em' }}>{l}</span>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{(email.snippet || email.bodyPreview) && (
|
||||
<Box sx={{
|
||||
mt: 0.25, pt: 0.5, borderTop: `1px solid ${TC_BORDER}`,
|
||||
color: TC_BODY,
|
||||
fontFamily: c.font.sans,
|
||||
fontSize: '0.7rem',
|
||||
lineHeight: 1.6,
|
||||
overflowWrap: 'anywhere',
|
||||
wordBreak: 'break-word',
|
||||
'& p': { m: 0, mb: 0.75, '&:last-child': { mb: 0 } },
|
||||
'& h1, & h2, & h3, & h4, & h5, & h6': {
|
||||
color: TC_HEADING, fontFamily: c.font.sans,
|
||||
mt: 1, mb: 0.5, '&:first-of-type': { mt: 0 },
|
||||
},
|
||||
'& h1': { fontSize: '0.82rem' }, '& h2': { fontSize: '0.78rem' },
|
||||
'& h3': { fontSize: '0.74rem' }, '& h4, & h5, & h6': { fontSize: '0.7rem' },
|
||||
'& strong': { color: TC_HEADING, fontWeight: 600 },
|
||||
'& em': { fontStyle: 'italic' },
|
||||
'& a': { color: TC_ACCENT, textDecoration: 'none', '&:hover': { textDecoration: 'underline' } },
|
||||
'& ul, & ol': { pl: 2, mb: 0.75, mt: 0 },
|
||||
'& li': { mb: 0.2 },
|
||||
'& blockquote': {
|
||||
m: 0, mb: 0.75, pl: 1, ml: 0,
|
||||
borderLeft: `2px solid ${TC_BORDER}`,
|
||||
color: TC_MUTED, fontStyle: 'italic',
|
||||
},
|
||||
'& code': {
|
||||
bgcolor: `${TC_BORDER}`, px: 0.4, py: 0.15,
|
||||
borderRadius: 0.5, fontSize: '0.65rem', fontFamily: c.font.mono,
|
||||
},
|
||||
'& pre': {
|
||||
bgcolor: `${TC_BORDER}`, borderRadius: 1, p: 1,
|
||||
overflow: 'auto', fontSize: '0.65rem', fontFamily: c.font.mono,
|
||||
m: 0, mb: 0.75,
|
||||
},
|
||||
'& pre code': { bgcolor: 'transparent', p: 0 },
|
||||
'& hr': { border: 'none', borderTop: `1px solid ${TC_BORDER}`, my: 0.75 },
|
||||
'& img': { maxWidth: '100%', borderRadius: 1 },
|
||||
}}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{ a: ({ children, ...props }) => <a {...props}>{children}</a> }}
|
||||
>
|
||||
{email.bodyPreview || email.snippet}
|
||||
</ReactMarkdown>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{attachments.length > 0 && (
|
||||
<Box sx={{ display: 'flex', gap: 0.4, flexWrap: 'wrap', mt: 0.2 }}>
|
||||
{attachments.map((a: any, i: number) => (
|
||||
<Box key={i} sx={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.3,
|
||||
bgcolor: `${TC_WARNING}15`, borderRadius: 0.75,
|
||||
px: 0.6, py: 0.1,
|
||||
}}>
|
||||
<AttachFileIcon sx={{ fontSize: 9, color: TC_WARNING, opacity: 0.7 }} />
|
||||
<span style={{ fontSize: '0.58rem', color: TC_WARNING, fontFamily: c.font.mono }}>
|
||||
{a.filename || a.name || 'attachment'}
|
||||
</span>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
|
||||
export const GoogleServiceIcon: React.FC<{ service: string; size?: number }> = ({ service, size = 14 }) => {
|
||||
if (service === 'gmail') {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 10" fill="none" style={{ flexShrink: 0 , marginBottom: '6px'}}>
|
||||
<path d="M2 6.5V18a2 2 0 002 2h1V8l-3-1.5z" fill="#4285F4"/>
|
||||
<path d="M22 6.5V18a2 2 0 01-2 2h-1V8l3-1.5z" fill="#34A853"/>
|
||||
<path d="M5 8v12h2V10.2L12 14l5-3.8V20h2V8l-7 5.25L5 8z" fill="#EA4335"/>
|
||||
<path d="M4 4a2 2 0 00-2 2.5L5 8V4H4z" fill="#4285F4"/>
|
||||
<path d="M20 4a2 2 0 012 2.5L19 8V4h1z" fill="#FBBC04"/>
|
||||
<path d="M19 4H5v4l7 5.25L19 8V4z" fill="#EA4335"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (service === 'calendar') {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" style={{ flexShrink: 0 }}>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" fill="#fff" stroke="#4285F4" strokeWidth="1.5"/>
|
||||
<rect x="3" y="3" width="18" height="6" rx="2" fill="#4285F4"/>
|
||||
<text x="12" y="17.5" textAnchor="middle" fontSize="9" fontWeight="700" fill="#4285F4" fontFamily="sans-serif">31</text>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (service === 'drive' || service === 'sheets') {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" style={{ flexShrink: 0 }}>
|
||||
<path d="M8 2l7 12H1L8 2z" fill="#FBBC04"/>
|
||||
<path d="M15 2l7 12h-7L8 2h7z" fill="#34A853"/>
|
||||
<path d="M1 14h14l-3.5 6H4.5L1 14z" fill="#4285F4"/>
|
||||
<path d="M15 14h7l-3.5 6h-7L15 14z" fill="#EA4335"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { ParsedMcpResult } from './toolResultParsing';
|
||||
import { useTermColors } from './toolColorize';
|
||||
import { useCardColors } from './cardColors';
|
||||
import { GmailCard } from './GmailCard';
|
||||
import { CalendarCard } from './CalendarCard';
|
||||
import { DriveCard } from './DriveCard';
|
||||
import { GenericMcpCard } from './GenericMcpCard';
|
||||
|
||||
export const McpResultCard: React.FC<{ parsed: ParsedMcpResult; compact?: boolean }> = ({ parsed, compact }) => {
|
||||
const c = useClaudeTokens();
|
||||
const tc = useTermColors();
|
||||
const { TC_BODY } = useCardColors();
|
||||
const { service, action, data, rawText } = parsed;
|
||||
|
||||
if (data.error || data.is_error) {
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<span style={{ color: tc.STDERR_COLOR, fontSize: '0.73rem' }}>
|
||||
{data.error || data.message || JSON.stringify(data, null, 2)}
|
||||
</span>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (service === 'gmail') return <GmailCard data={data} action={action} hideSubjectHeader={compact} />;
|
||||
if (service === 'calendar') return <CalendarCard data={data} hideHeader={compact} />;
|
||||
if (service === 'drive' || service === 'sheets') return <DriveCard data={data} />;
|
||||
|
||||
// Plain-text MCP results: render rawText capped at 6000 chars (model still sees full payload).
|
||||
const hasData = data && Object.keys(data).length > 0;
|
||||
if (!hasData && rawText && rawText.trim()) {
|
||||
const DISPLAY_CAP = 6000;
|
||||
const preview = rawText.length > DISPLAY_CAP
|
||||
? rawText.slice(0, DISPLAY_CAP) + `\n… (${rawText.length - DISPLAY_CAP} more chars; model received full output)`
|
||||
: rawText;
|
||||
return (
|
||||
<Box sx={{ px: 1.5, py: 1 }}>
|
||||
<span style={{
|
||||
color: TC_BODY,
|
||||
fontSize: '0.72rem',
|
||||
fontFamily: c.font.sans,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
display: 'block',
|
||||
lineHeight: 1.55,
|
||||
}}>
|
||||
{preview}
|
||||
</span>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return <GenericMcpCard data={data} />;
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useThemeMode } from '@/shared/styles/ThemeContext';
|
||||
|
||||
export interface CardColors {
|
||||
TC_BG: string;
|
||||
TC_BORDER: string;
|
||||
TC_HOVER: string;
|
||||
TC_HEADING: string;
|
||||
TC_BODY: string;
|
||||
TC_MUTED: string;
|
||||
TC_DIM: string;
|
||||
TC_ACCENT: string;
|
||||
TC_SUCCESS: string;
|
||||
TC_WARNING: string;
|
||||
}
|
||||
|
||||
const darkCardColors: CardColors = {
|
||||
TC_BG: 'rgba(255,255,255,0.03)',
|
||||
TC_BORDER: 'rgba(255,255,255,0.06)',
|
||||
TC_HOVER: 'rgba(255,255,255,0.05)',
|
||||
TC_HEADING: '#C2C0B6',
|
||||
TC_BODY: '#9C9A92',
|
||||
TC_MUTED: '#85837C',
|
||||
TC_DIM: 'rgba(156,154,146,0.5)',
|
||||
TC_ACCENT: '#c4633a',
|
||||
TC_SUCCESS: '#7AB948',
|
||||
TC_WARNING: '#D1A041',
|
||||
};
|
||||
|
||||
const lightCardColors: CardColors = {
|
||||
TC_BG: 'rgba(0,0,0,0.03)',
|
||||
TC_BORDER: 'rgba(0,0,0,0.08)',
|
||||
TC_HOVER: 'rgba(0,0,0,0.05)',
|
||||
TC_HEADING: '#3D3D3A',
|
||||
TC_BODY: '#555550',
|
||||
TC_MUTED: '#73726C',
|
||||
TC_DIM: 'rgba(115,114,108,0.5)',
|
||||
TC_ACCENT: '#ae5630',
|
||||
TC_SUCCESS: '#265B19',
|
||||
TC_WARNING: '#805C1F',
|
||||
};
|
||||
|
||||
export function useCardColors(): CardColors {
|
||||
const { mode } = useThemeMode();
|
||||
return mode === 'dark' ? darkCardColors : lightCardColors;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { getGmailHeader } from './mcpToolName';
|
||||
|
||||
export function formatTimestamp(ts: string | number | undefined): string {
|
||||
if (!ts) return '';
|
||||
try {
|
||||
const d = typeof ts === 'number' ? new Date(ts) : new Date(ts);
|
||||
if (isNaN(d.getTime())) return String(ts);
|
||||
return d.toLocaleDateString('en-US', {
|
||||
weekday: 'short', month: 'short', day: 'numeric', year: 'numeric',
|
||||
hour: 'numeric', minute: '2-digit',
|
||||
});
|
||||
} catch { return String(ts); }
|
||||
}
|
||||
|
||||
export function stripHtml(html: string): string {
|
||||
const tmp = document.createElement('div');
|
||||
tmp.innerHTML = html;
|
||||
return tmp.textContent || tmp.innerText || '';
|
||||
}
|
||||
|
||||
export function extractEmailFields(msg: any) {
|
||||
const subject = msg.subject || getGmailHeader(msg, 'Subject') || '(no subject)';
|
||||
const from = msg.from || msg.sender || getGmailHeader(msg, 'From') || '';
|
||||
const to = msg.to || msg.recipient || getGmailHeader(msg, 'To') || '';
|
||||
const rawDate = msg.date || msg.internalDate || msg.receivedAt || getGmailHeader(msg, 'Date') || '';
|
||||
const date = formatTimestamp(rawDate);
|
||||
const snippet = msg.snippet || '';
|
||||
const body = msg.body || msg.text || msg.textBody || '';
|
||||
const htmlBody = msg.htmlBody || msg.html || '';
|
||||
const bodyPreview = body || (htmlBody ? stripHtml(htmlBody) : '');
|
||||
return { subject, from, to, date, snippet, bodyPreview };
|
||||
}
|
||||
Reference in New Issue
Block a user