diff --git a/apps/x/apps/renderer/src/extensions/calendar-block.tsx b/apps/x/apps/renderer/src/extensions/calendar-block.tsx index f72dc5d4..9f0eec02 100644 --- a/apps/x/apps/renderer/src/extensions/calendar-block.tsx +++ b/apps/x/apps/renderer/src/extensions/calendar-block.tsx @@ -9,12 +9,15 @@ function formatTime(dateStr: string): string { return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) } -function getDateParts(dateStr: string): { day: number; month: string; weekday: string } { +function getDateParts(dateStr: string): { day: number; month: string; weekday: string; isToday: boolean } { const d = new Date(dateStr) + const now = new Date() + const isToday = d.getDate() === now.getDate() && d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear() return { day: d.getDate(), - month: d.toLocaleDateString([], { month: 'long' }), - weekday: d.toLocaleDateString([], { weekday: 'short' }), + month: d.toLocaleDateString([], { month: 'short' }).toUpperCase(), + weekday: d.toLocaleDateString([], { weekday: 'short' }).toUpperCase(), + isToday, } } @@ -62,7 +65,8 @@ interface ResolvedEvent { conferenceLink?: string } -const EVENT_BAR_COLOR = '#7ec8c8' +const GCAL_EVENT_COLOR = '#039be5' +const GCAL_TODAY_COLOR = '#1a73e8' function JoinMeetingSplitButton({ onJoinAndNotes, onNotesOnly }: { onJoinAndNotes: () => void @@ -273,11 +277,8 @@ function CalendarBlockView({ node, deleteNode }: { node: { attrs: Record {parts ? ( <> - {parts.day} -
- {parts.month} - {parts.weekday} -
+ {parts.weekday} + {parts.day} ) : ( ? @@ -288,16 +289,13 @@ function CalendarBlockView({ node, deleteNode }: { node: { attrs: Record e.stopPropagation()} onClick={(e) => { e.stopPropagation(); handleEventClick(event) }} > -
- {event.summary || 'Untitled event'} + {event.summary || '(No title)'}
{getTimeRange(event)} diff --git a/apps/x/apps/renderer/src/extensions/email-block.tsx b/apps/x/apps/renderer/src/extensions/email-block.tsx index 9be8c72c..7356c94c 100644 --- a/apps/x/apps/renderer/src/extensions/email-block.tsx +++ b/apps/x/apps/renderer/src/extensions/email-block.tsx @@ -1,8 +1,9 @@ import { mergeAttributes, Node } from '@tiptap/react' import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react' -import { X, Mail, ChevronDown, ExternalLink, Copy, Check, Sparkles, Loader2, MessageSquare } from 'lucide-react' +import { X, Mail, ChevronDown, ExternalLink, Copy, Check, MessageSquare } from 'lucide-react' import { blocks } from '@x/shared' import { useState, useEffect, useRef, useCallback } from 'react' +import { useTheme } from '@/contexts/theme-context' // --- Helpers --- @@ -17,8 +18,10 @@ function formatEmailDate(dateStr: string): string { } } -function getInitials(name: string): string { - return name.split(/\s+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase() +/** Extract just the name part from "Name " format */ +function senderFirstName(from: string): string { + const name = from.replace(/<.*>/, '').trim() + return name.split(/\s+/)[0] || name } declare global { @@ -45,27 +48,15 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { const hasDraft = !!config?.draft_response const hasPastSummary = !!config?.past_summary - const responseMode = config?.response_mode || 'both' + + const { resolvedTheme } = useTheme() // Local draft state for editing const [draftBody, setDraftBody] = useState(config?.draft_response || '') - const [contextExpanded, setContextExpanded] = useState(false) + const [emailExpanded, setEmailExpanded] = useState(false) const [copied, setCopied] = useState(false) - const [generating, setGenerating] = useState(false) - const [responseSplitOpen, setResponseSplitOpen] = useState(false) - const responseSplitRef = useRef(null) const bodyRef = useRef(null) - // Close split dropdown on outside click - useEffect(() => { - if (!responseSplitOpen) return - const handler = (e: MouseEvent) => { - if (responseSplitRef.current && !responseSplitRef.current.contains(e.target as globalThis.Node)) setResponseSplitOpen(false) - } - document.addEventListener('mousedown', handler) - return () => document.removeEventListener('mousedown', handler) - }, [responseSplitOpen]) - // Sync draft from external changes useEffect(() => { try { @@ -89,53 +80,23 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { } catch { /* ignore */ } }, [raw, updateAttributes]) - const generateResponse = useCallback(async () => { - if (!config || generating) return - setGenerating(true) - try { - const ipc = (window as unknown as { ipc: { invoke: (channel: string, args: Record) => Promise<{ response?: string }> } }).ipc - // Build context for the agent - let noteContent = `# Email: ${config.subject || 'No subject'}\n\n` - noteContent += `**From:** ${config.from || 'Unknown'}\n` - noteContent += `**Date:** ${config.date || 'Unknown'}\n\n` - noteContent += `## Latest email\n\n${config.latest_email}\n\n` - if (config.past_summary) { - noteContent += `## Earlier conversation summary\n\n${config.past_summary}\n\n` - } - - const result = await ipc.invoke('inline-task:process', { - instruction: `Draft a concise, professional response to this email. Return only the email body text, no subject line or headers.`, - noteContent, - notePath: '', - }) - - if (result.response) { - // Clean up the response — strip any markdown headers the agent may add - const cleaned = result.response.replace(/^#+\s+.*\n*/gm, '').trim() - setDraftBody(cleaned) - // Update the block data to include the draft - const current = JSON.parse(raw) as Record - updateAttributes({ data: JSON.stringify({ ...current, draft_response: cleaned }) }) - } - } catch (err) { - console.error('[email-block] Failed to generate response:', err) - } finally { - setGenerating(false) - } - }, [config, generating, raw, updateAttributes]) - const draftWithAssistant = useCallback(() => { if (!config) return - let prompt = `Help me draft a response to this email` + let prompt = draftBody + ? `Help me refine this draft response to an email` + : `Help me draft a response to this email` if (config.threadId) { prompt += `. Read the full thread at gmail_sync/${config.threadId}.md for context` } prompt += `.\n\n` prompt += `**From:** ${config.from || 'Unknown'}\n` prompt += `**Subject:** ${config.subject || 'No subject'}\n` + if (draftBody) { + prompt += `\n**Current draft:**\n${draftBody}\n` + } window.__pendingEmailDraft = { prompt } window.dispatchEvent(new Event('email-block:draft-with-assistant')) - }, [config]) + }, [config, draftBody]) if (!config) { return ( @@ -152,185 +113,112 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { ? `https://mail.google.com/mail/u/0/#all/${config.threadId}` : null - // --- Render: Draft mode (draft_response present) --- - if (hasDraft) { - return ( - -
e.stopPropagation()}> - - {/* Draft header */} - {config.to && ( -
-
- To - {config.to} -
- {config.subject && ( -
- Subject - {config.subject} + // Build summary: use explicit summary, or auto-generate from sender + subject + const summary = config.summary + || (config.from && config.subject + ? `${senderFirstName(config.from)} reached out about ${config.subject}` + : config.subject || 'New email') + + return ( + +
e.stopPropagation()}> + + + {/* Header: Email badge */} +
+ + Email +
+ + {/* Summary */} +
{summary}
+ + {/* Expandable email details */} + + + {emailExpanded && ( +
+
+
+
+
+
{config.from || 'Unknown'}
+ {config.date &&
{formatEmailDate(config.date)}
} +
+ {config.subject &&
Subject: {config.subject}
}
- )} +
+
{config.latest_email}
- )} - {/* Editable draft body */} -