mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-24 20:28:16 +02:00
email reply all and first name signoff
This commit is contained in:
parent
f848d235d8
commit
a0429fc037
5 changed files with 31 additions and 19 deletions
|
|
@ -906,7 +906,7 @@ const AI_GENERATE_SYSTEM =
|
|||
'<the email body as plain text>\n' +
|
||||
'Do not use markdown. Do not add any commentary, labels, or surrounding quotes. ' +
|
||||
'When recipient names are provided, address them naturally (e.g. "Hi <first name>,"). ' +
|
||||
'When the sender\'s name is provided, sign off with it; otherwise omit the sign-off name ' +
|
||||
'When the sender\'s first name is provided, sign off with that first name only; otherwise omit the sign-off name ' +
|
||||
'(never write a placeholder like "[Your Name]").'
|
||||
|
||||
const AI_REWRITE_SYSTEM =
|
||||
|
|
@ -931,8 +931,13 @@ function parseGeneratedEmail(text: string): { subject: string | null; body: stri
|
|||
return { subject: null, body: text }
|
||||
}
|
||||
|
||||
// Guarantee the sender's name signs off the email. If the model already ended
|
||||
// with the name (e.g. "Best,\nHarsh"), leave it; otherwise append it.
|
||||
function firstNameFromDisplayName(name: string): string {
|
||||
const trimmed = name.trim().replace(/^["']|["']$/g, '')
|
||||
return trimmed.split(/\s+/)[0] || ''
|
||||
}
|
||||
|
||||
// Guarantee the sender's first name signs off the email. If the model already
|
||||
// ended with the name (e.g. "Best,\nHarsh"), leave it; otherwise append it.
|
||||
function ensureSignature(body: string, name: string): string {
|
||||
const signer = name.trim()
|
||||
if (!signer) return body
|
||||
|
|
@ -977,7 +982,7 @@ const ComposeBox = memo(function ComposeBox({
|
|||
const [showCc, setShowCc] = useState<boolean>(initialRecipients.cc.length > 0)
|
||||
const [showBcc, setShowBcc] = useState<boolean>(false)
|
||||
const [subject, setSubject] = useState<string>(() => (thread ? composeSubject(mode, thread.subject) : ''))
|
||||
const modeLabel = isNew ? 'New message' : mode === 'forward' ? 'Forward' : mode === 'replyAll' ? 'Reply all' : 'Reply'
|
||||
const modeLabel = isNew ? 'New message' : mode === 'forward' ? 'Forward' : mode === 'replyAll' ? 'Reply All' : 'Reply'
|
||||
|
||||
const initialContent = useMemo(() => {
|
||||
if (!thread) return ''
|
||||
|
|
@ -1050,6 +1055,7 @@ const ComposeBox = memo(function ComposeBox({
|
|||
|
||||
// The signed-in account's display name, used to sign off AI-generated emails.
|
||||
const [selfName, setSelfName] = useState<string>('')
|
||||
const selfFirstName = useMemo(() => firstNameFromDisplayName(selfName), [selfName])
|
||||
useEffect(() => {
|
||||
if (!isNew) return
|
||||
let cancelled = false
|
||||
|
|
@ -1104,7 +1110,7 @@ const ComposeBox = memo(function ComposeBox({
|
|||
})
|
||||
.filter(Boolean)
|
||||
if (recipientNames.length) ctx.push(`Recipient(s): ${recipientNames.join(', ')}`)
|
||||
if (selfName) ctx.push(`Sender's name (sign off as this): ${selfName}`)
|
||||
if (selfFirstName) ctx.push(`Sender's first name (sign off as this): ${selfFirstName}`)
|
||||
if (subject.trim()) ctx.push(`Desired subject hint: ${subject.trim()}`)
|
||||
if (current) ctx.push(`Existing draft (revise or build on it):\n${current}`)
|
||||
prompt = `${ctx.length ? ctx.join('\n') + '\n\n' : ''}Instruction: ${instruction.trim()}`
|
||||
|
|
@ -1130,8 +1136,8 @@ const ComposeBox = memo(function ComposeBox({
|
|||
if (aiMode === 'generate') {
|
||||
const { subject: generatedSubject, body } = parseGeneratedEmail(res.text)
|
||||
if (generatedSubject) setSubject(generatedSubject)
|
||||
// Always sign off with the account name, even if the model omitted it.
|
||||
const signed = ensureSignature(body, selfName)
|
||||
// Always sign off with the account first name, even if the model omitted it.
|
||||
const signed = ensureSignature(body, selfFirstName)
|
||||
editor.chain().focus().selectAll().insertContent(plainTextToHtml(signed)).run()
|
||||
setHasGenerated(true)
|
||||
} else {
|
||||
|
|
@ -1495,10 +1501,17 @@ function ThreadDetail({
|
|||
return () => { cancelled = true }
|
||||
}, [])
|
||||
|
||||
const canReplyAll = useMemo(() => {
|
||||
const { to, cc } = buildRecipients('replyAll', thread, selfEmail)
|
||||
return cc.length > 0 || to.length > 1
|
||||
}, [thread, selfEmail])
|
||||
const replyAllRecipients = useMemo(
|
||||
() => buildRecipients('replyAll', thread, selfEmail),
|
||||
[thread, selfEmail],
|
||||
)
|
||||
const canReplyAll = replyAllRecipients.cc.length > 0 || replyAllRecipients.to.length > 1
|
||||
const replyAllButton = canReplyAll ? (
|
||||
<button type="button" onClick={() => setComposeMode('replyAll')}>
|
||||
<ReplyAll size={16} />
|
||||
Reply All
|
||||
</button>
|
||||
) : null
|
||||
|
||||
const toggleExpand = useCallback((index: number) => {
|
||||
setExpandedIndices((prev) => {
|
||||
|
|
@ -1568,16 +1581,11 @@ function ThreadDetail({
|
|||
</div>
|
||||
|
||||
<div className="gmail-thread-actions">
|
||||
{replyAllButton}
|
||||
<button type="button" onClick={() => setComposeMode('reply')}>
|
||||
<Reply size={16} />
|
||||
Reply
|
||||
</button>
|
||||
{canReplyAll && (
|
||||
<button type="button" onClick={() => setComposeMode('replyAll')}>
|
||||
<ReplyAll size={16} />
|
||||
Reply all
|
||||
</button>
|
||||
)}
|
||||
<button type="button" onClick={() => setComposeMode('forward')}>
|
||||
<Forward size={16} />
|
||||
Forward
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@ Subject: Re: {original_subject}
|
|||
**Drafting Guidelines:**
|
||||
- Draft ONE email - do not offer multiple versions or options unless explicitly asked
|
||||
- Be concise and professional
|
||||
- If you include a sign-off name, use only the user's first name, never their full name
|
||||
- For scheduling: propose specific times based on calendar availability
|
||||
- For inquiries: answer directly or indicate what info is needed
|
||||
- Reference any relevant context from memory naturally - show you remember past interactions
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export interface Classification {
|
|||
const ClassificationSchema = z.object({
|
||||
importance: z.enum(['important', 'other']).describe('important = real correspondence, action-required, or content worth referencing later. other = newsletters, marketing, automated notifications, transactional receipts, cold outreach.'),
|
||||
summary: z.string().optional().describe('One or two sentences capturing what the thread is about and any implied action. Required when importance is important. Omit when other.'),
|
||||
draftResponse: z.string().optional().describe('A complete draft reply the user can send as-is or edit. Plain text with real line breaks (\\n): greeting on its own line, a blank line between paragraphs, and the sign-off on its own line(s) — e.g. "Hi Tyrone,\\n\\nThanks for the follow-up.\\n\\nBest,\\nJohn". Required when importance is important AND the thread implies a response is wanted. Omit when other, or when no response is appropriate (e.g. an FYI from a colleague that does not need a reply).'),
|
||||
draftResponse: z.string().optional().describe('A complete draft reply the user can send as-is or edit. Plain text with real line breaks (\\n): greeting on its own line, a blank line between paragraphs, and the sign-off on its own line(s) — e.g. "Hi Tyrone,\\n\\nThanks for the follow-up.\\n\\nBest,\\nJohn". If a sign-off name is included, use only the user\'s first name. Required when importance is important AND the thread implies a response is wanted. Omit when other, or when no response is appropriate (e.g. an FYI from a colleague that does not need a reply).'),
|
||||
});
|
||||
|
||||
const SYSTEM_PROMPT = `You classify a Gmail thread for a personal inbox view and, when appropriate, draft a reply on behalf of the user.
|
||||
|
|
@ -139,6 +139,8 @@ Could you resend it with a bit more context so I can get back to you properly?
|
|||
Best,
|
||||
John
|
||||
|
||||
If you include the user's name in the sign-off, use only their first name, never their full name.
|
||||
|
||||
When an email-style guide is provided below, it takes precedence: follow it for greeting, tone, sign-off, length, and phrasing patterns (while keeping the line-break structure shown above). If no style guide is provided, default to a brief, warm, professional voice.
|
||||
|
||||
For scheduling-related threads (where the sender proposes meeting times, asks for the user's availability, or follows up on a meeting request), look at the user's upcoming calendar (provided below) and either:
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ If there are events, include them:
|
|||
1. Use \`file-list\` with path \`gmail_sync\` to list files (skip \`sync_state.json\` and \`attachments/\`)
|
||||
2. Use \`file-readText\` to read the email markdown files (e.g. \`gmail_sync/threadid123.md\`)
|
||||
3. Check the frontmatter \`action\` field — emails with \`action: reply\` or \`action: respond\` need a response
|
||||
4. Output ALL emails (both action items and FYI) in a single \\\`\\\`\\\`emails block as a JSON array. Emails needing a response get a \`draft_response\`. Write drafts in the user's voice — direct, informal, no fluff. Example:
|
||||
4. Output ALL emails (both action items and FYI) in a single \\\`\\\`\\\`emails block as a JSON array. Emails needing a response get a \`draft_response\`. Write drafts in the user's voice — direct, informal, no fluff. If a draft includes a sign-off name, use only the user's first name, never their full name. Example:
|
||||
|
||||
\`\`\`
|
||||
\\\`\\\`\\\`emails
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ Subject: Re: {original_subject}
|
|||
|
||||
**Drafting Guidelines:**
|
||||
- Be concise and professional
|
||||
- If you include a sign-off name, use only the user's first name, never their full name
|
||||
- For scheduling: propose specific times based on calendar availability
|
||||
- For inquiries: answer directly or indicate what info is needed
|
||||
- Reference any relevant context from memory naturally
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue