diff --git a/apps/x/apps/renderer/src/extensions/email-block.tsx b/apps/x/apps/renderer/src/extensions/email-block.tsx index 7cc42f67..7356c94c 100644 --- a/apps/x/apps/renderer/src/extensions/email-block.tsx +++ b/apps/x/apps/renderer/src/extensions/email-block.tsx @@ -1,6 +1,6 @@ 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' @@ -18,6 +18,11 @@ function formatEmailDate(dateStr: string): string { } } +/** 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 { interface Window { @@ -43,29 +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,192 +113,112 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { ? `https://mail.google.com/mail/u/0/#all/${config.threadId}` : null - const senderName = config.from || 'Unknown' + // 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') - // --- Render: Draft mode (draft_response present) --- - if (hasDraft) { - return ( - -
e.stopPropagation()}> - - {/* Draft header – Gmail compose style */} -
- {config.to && ( -
- To - {config.to} -
- )} - {config.subject && ( -
- Subject - {config.subject} -
- )} -
- {/* Editable draft body */} -