diff --git a/frontend/src/app/pages/AgentChat/CalendarCard.tsx b/frontend/src/app/pages/AgentChat/CalendarCard.tsx new file mode 100644 index 00000000..0c79991f --- /dev/null +++ b/frontend/src/app/pages/AgentChat/CalendarCard.tsx @@ -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; 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 ( + + {!hideHeader && ( + + + + {single.summary || '(no title)'} + + + )} + + {start && ( + + Start + {formatTimestamp(start)} + + )} + {end && ( + + End + {formatTimestamp(end)} + + )} + {single.location && ( + + Where + {single.location} + + )} + {single.description && ( + +
+                {single.description.slice(0, 300)}
+                {single.description.length > 300 ? '…' : ''}
+              
+
+ )} +
+
+ ); + } + + if (items.length > 0) { + return ( + + {items.slice(0, 6).map((item: any, i: number) => ( + + + {item.summary || '(no title)'} + + + {formatTimestamp(item.start?.dateTime || item.start?.date || item.start)} + + + ))} + {items.length > 6 && ( + + +{items.length - 6} more + + )} + + ); + } + + return null; +}; diff --git a/frontend/src/app/pages/AgentChat/DriveCard.tsx b/frontend/src/app/pages/AgentChat/DriveCard.tsx new file mode 100644 index 00000000..8c408ff2 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/DriveCard.tsx @@ -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 }> = ({ 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 ( + + + + + {single.name} + + {single.mimeType && ( + {single.mimeType} + )} + + + ); + } + + if (files.length > 0) { + return ( + + {files.slice(0, 8).map((f: any, i: number) => ( + + + {f.name || f.id} + {f.mimeType && ( + + {f.mimeType.split('/').pop()} + + )} + + ))} + + ); + } + + return null; +}; diff --git a/frontend/src/app/pages/AgentChat/GenericMcpCard.tsx b/frontend/src/app/pages/AgentChat/GenericMcpCard.tsx new file mode 100644 index 00000000..498e23f7 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/GenericMcpCard.tsx @@ -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 }> = ({ data }) => { + const c = useClaudeTokens(); + const { TC_DIM, TC_BODY } = useCardColors(); + const entries = Object.entries(data).filter(([, v]) => v != null); + + if (entries.length === 0) + return (empty response); + + return ( + + {entries.slice(0, 20).map(([key, val], i) => { + const isLong = typeof val === 'string' && val.length > 100; + const isObj = typeof val === 'object'; + return ( + + + {key} + + {isObj ? ( +
+                {JSON.stringify(val, null, 2).slice(0, 500)}
+              
+ ) : isLong ? ( +
+                {String(val).slice(0, 500)}{String(val).length > 500 ? '…' : ''}
+              
+ ) : ( + {String(val)} + )} +
+ ); + })} + {entries.length > 20 && ( + + +{entries.length - 20} more fields + + )} +
+ ); +}; diff --git a/frontend/src/app/pages/AgentChat/GmailCard.tsx b/frontend/src/app/pages/AgentChat/GmailCard.tsx new file mode 100644 index 00000000..dd85d653 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/GmailCard.tsx @@ -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; 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 ( + + {messages.slice(0, 5).map((msg: any, i: number) => { + const m = extractEmailFields(msg); + return ( + + + + {m.subject} + + {m.date && ( + + {m.date} + + )} + + {m.from && ( + + {m.from} + + )} + {(m.snippet || m.bodyPreview) && ( + + {(m.snippet || m.bodyPreview).slice(0, 120)} + {(m.snippet || m.bodyPreview).length > 120 ? '…' : ''} + + )} + + ); + })} + {messages.length > 5 && ( + + +{messages.length - 5} more + + )} + + ); + } + + return ( + + {!hideSubjectHeader && ( + + {isSend ? ( + + ) : ( + + )} + + {email.subject} + + + )} + + + {(email.from || email.to || email.date) && ( + + {email.from && ( + + From + {email.from} + + )} + {email.to && ( + + To + {email.to} + + )} + {email.date && ( + + Date + {email.date} + + )} + + )} + + {labels.length > 0 && ( + + {labels.map((l: string, i: number) => ( + + {l} + + ))} + + )} + + {(email.snippet || email.bodyPreview) && ( + + {children} }} + > + {email.bodyPreview || email.snippet} + + + )} + + {attachments.length > 0 && ( + + {attachments.map((a: any, i: number) => ( + + + + {a.filename || a.name || 'attachment'} + + + ))} + + )} + + + ); +}; diff --git a/frontend/src/app/pages/AgentChat/GoogleServiceIcon.tsx b/frontend/src/app/pages/AgentChat/GoogleServiceIcon.tsx new file mode 100644 index 00000000..71e0f2d8 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/GoogleServiceIcon.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +export const GoogleServiceIcon: React.FC<{ service: string; size?: number }> = ({ service, size = 14 }) => { + if (service === 'gmail') { + return ( + + + + + + + + + ); + } + if (service === 'calendar') { + return ( + + + + 31 + + ); + } + if (service === 'drive' || service === 'sheets') { + return ( + + + + + + + ); + } + return null; +}; diff --git a/frontend/src/app/pages/AgentChat/McpResultCard.tsx b/frontend/src/app/pages/AgentChat/McpResultCard.tsx new file mode 100644 index 00000000..0fcd4071 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/McpResultCard.tsx @@ -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 ( + + + {data.error || data.message || JSON.stringify(data, null, 2)} + + + ); + } + + if (service === 'gmail') return ; + if (service === 'calendar') return ; + if (service === 'drive' || service === 'sheets') return ; + + // 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 ( + + + {preview} + + + ); + } + + return ; +}; diff --git a/frontend/src/app/pages/AgentChat/cardColors.ts b/frontend/src/app/pages/AgentChat/cardColors.ts new file mode 100644 index 00000000..5f332017 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/cardColors.ts @@ -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; +} diff --git a/frontend/src/app/pages/AgentChat/mcpCardHelpers.ts b/frontend/src/app/pages/AgentChat/mcpCardHelpers.ts new file mode 100644 index 00000000..cbcae2e6 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/mcpCardHelpers.ts @@ -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 }; +}