diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 20fa5435..591ef21e 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -3490,6 +3490,20 @@ function App() { return () => window.removeEventListener('calendar-block:join-meeting', handler) }, []) + // Email block: draft with assistant + useEffect(() => { + const handler = () => { + const pending = window.__pendingEmailDraft + if (pending) { + setPresetMessage(pending.prompt) + setIsChatSidebarOpen(true) + window.__pendingEmailDraft = undefined + } + } + window.addEventListener('email-block:draft-with-assistant', handler) + return () => window.removeEventListener('email-block:draft-with-assistant', handler) + }, []) + const ensureWikiFile = useCallback(async (wikiPath: string) => { const resolvedPath = toKnowledgePath(wikiPath) if (!resolvedPath) return null diff --git a/apps/x/apps/renderer/src/extensions/email-block.tsx b/apps/x/apps/renderer/src/extensions/email-block.tsx index 084f3ba2..c17625f3 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 } from 'lucide-react' +import { X, Mail, ChevronDown, ExternalLink, Copy, Check, Sparkles, Loader2, MessageSquare } from 'lucide-react' import { blocks } from '@x/shared' import { useState, useEffect, useRef, useCallback } from 'react' @@ -21,6 +21,12 @@ function getInitials(name: string): string { return name.split(/\s+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase() } +declare global { + interface Window { + __pendingEmailDraft?: { prompt: string } + } +} + // --- Email Block --- function EmailBlockView({ node, deleteNode, updateAttributes }: { @@ -39,14 +45,27 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { const hasDraft = !!config?.draft_response const hasPastSummary = !!config?.past_summary + const responseMode = config?.response_mode || 'both' // Local draft state for editing const [draftBody, setDraftBody] = useState(config?.draft_response || '') const [contextExpanded, setContextExpanded] = 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 Node)) setResponseSplitOpen(false) + } + document.addEventListener('mousedown', handler) + return () => document.removeEventListener('mousedown', handler) + }, [responseSplitOpen]) + // Sync draft from external changes useEffect(() => { try { @@ -105,6 +124,19 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { } }, [config, generating, raw, updateAttributes]) + const draftWithAssistant = useCallback(() => { + if (!config) return + let prompt = `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` + window.__pendingEmailDraft = { prompt } + window.dispatchEvent(new Event('email-block:draft-with-assistant')) + }, [config]) + if (!config) { return ( @@ -250,14 +282,56 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { {contextExpanded ? 'Hide' : 'Show'} context )} - + {responseMode === 'inline' && ( + + )} + {responseMode === 'assistant' && ( + + )} + {responseMode === 'both' && ( +
+ + + {responseSplitOpen && ( +
+ +
+ )} +
+ )} {gmailUrl && (