mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
draft email options
This commit is contained in:
parent
c41586b85d
commit
1a63b1a800
4 changed files with 190 additions and 9 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>(null)
|
||||
const bodyRef = useRef<HTMLTextAreaElement>(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 (
|
||||
<NodeViewWrapper className="email-block-wrapper" data-type="email-block">
|
||||
|
|
@ -250,14 +282,56 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: {
|
|||
{contextExpanded ? 'Hide' : 'Show'} context
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="email-block-gmail-btn email-block-generate-btn"
|
||||
onClick={generateResponse}
|
||||
disabled={generating}
|
||||
>
|
||||
{generating ? <Loader2 size={13} className="email-block-spinner" /> : <Sparkles size={13} />}
|
||||
{generating ? 'Generating...' : 'Generate response'}
|
||||
</button>
|
||||
{responseMode === 'inline' && (
|
||||
<button
|
||||
className="email-block-gmail-btn email-block-generate-btn"
|
||||
onClick={generateResponse}
|
||||
disabled={generating}
|
||||
>
|
||||
{generating ? <Loader2 size={13} className="email-block-spinner" /> : <Sparkles size={13} />}
|
||||
{generating ? 'Generating...' : 'Generate response'}
|
||||
</button>
|
||||
)}
|
||||
{responseMode === 'assistant' && (
|
||||
<button
|
||||
className="email-block-gmail-btn email-block-generate-btn"
|
||||
onClick={draftWithAssistant}
|
||||
>
|
||||
<MessageSquare size={13} />
|
||||
Draft with assistant
|
||||
</button>
|
||||
)}
|
||||
{responseMode === 'both' && (
|
||||
<div className="email-block-response-split" ref={responseSplitRef}>
|
||||
<button
|
||||
className="email-block-split-main"
|
||||
onClick={generateResponse}
|
||||
disabled={generating}
|
||||
>
|
||||
{generating ? <Loader2 size={13} className="email-block-spinner" /> : <Sparkles size={13} />}
|
||||
{generating ? 'Generating...' : 'Generate response'}
|
||||
</button>
|
||||
<button
|
||||
className={`email-block-split-chevron ${responseSplitOpen ? 'email-block-split-chevron-open' : ''}`}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => { e.stopPropagation(); setResponseSplitOpen(!responseSplitOpen) }}
|
||||
>
|
||||
<ChevronDown size={12} />
|
||||
</button>
|
||||
{responseSplitOpen && (
|
||||
<div className="email-block-split-dropdown">
|
||||
<button
|
||||
className="email-block-split-option"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => { e.stopPropagation(); setResponseSplitOpen(false); draftWithAssistant() }}
|
||||
>
|
||||
<MessageSquare size={13} />
|
||||
Draft with assistant
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{gmailUrl && (
|
||||
<button
|
||||
className="email-block-gmail-btn"
|
||||
|
|
|
|||
|
|
@ -1298,6 +1298,98 @@
|
|||
animation: email-block-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Email block split button (generate/assistant) */
|
||||
.tiptap-editor .ProseMirror .email-block-response-split {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-response-split > button {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-split-main {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 5px 8px 5px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
background: color-mix(in srgb, var(--foreground) 5%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 12%, transparent);
|
||||
border-right: none;
|
||||
border-radius: 6px 0 0 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.12s ease;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-split-main:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--foreground) 10%, transparent);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-split-main:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-split-chevron {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px 6px;
|
||||
color: color-mix(in srgb, var(--foreground) 60%, transparent);
|
||||
background: color-mix(in srgb, var(--foreground) 5%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 12%, transparent);
|
||||
border-left: 1px solid color-mix(in srgb, var(--foreground) 8%, transparent);
|
||||
border-radius: 0 6px 6px 0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.12s ease;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-split-chevron:hover {
|
||||
background: color-mix(in srgb, var(--foreground) 10%, transparent);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-split-chevron-open {
|
||||
border-radius: 0 6px 0 0;
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-split-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% - 1px);
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
background: color-mix(in srgb, var(--foreground) 5%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 12%, transparent);
|
||||
border-top: none;
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-split-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
white-space: nowrap;
|
||||
padding: 5px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: color-mix(in srgb, var(--foreground) 60%, transparent);
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0 0 6px 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.12s ease;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-split-option:hover {
|
||||
background: color-mix(in srgb, var(--foreground) 10%, transparent);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
/* Email draft block */
|
||||
.tiptap-editor .ProseMirror .email-draft-block-header {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export const EmailBlockSchema = z.object({
|
|||
latest_email: z.string(),
|
||||
past_summary: z.string().optional(),
|
||||
draft_response: z.string().optional(),
|
||||
response_mode: z.enum(['inline', 'assistant', 'both']).optional(),
|
||||
});
|
||||
|
||||
export type EmailBlock = z.infer<typeof EmailBlockSchema>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue