mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-27 20:29:44 +02:00
handle drafts
This commit is contained in:
parent
b9cfa676e9
commit
233e83abe4
4 changed files with 43 additions and 11 deletions
|
|
@ -399,12 +399,15 @@ function ComposeBox({
|
||||||
const to = mode === 'reply' ? extractAddress(latest?.from) : ''
|
const to = mode === 'reply' ? extractAddress(latest?.from) : ''
|
||||||
|
|
||||||
const initialContent = useMemo(() => {
|
const initialContent = useMemo(() => {
|
||||||
if (mode !== 'reply' || !thread.draft_response) return ''
|
if (mode !== 'reply') return ''
|
||||||
return thread.draft_response
|
// Gmail-side draft (user's own work) wins over the AI-generated draft.
|
||||||
|
const source = thread.gmail_draft || thread.draft_response
|
||||||
|
if (!source) return ''
|
||||||
|
return source
|
||||||
.split(/\n{2,}/)
|
.split(/\n{2,}/)
|
||||||
.map((para) => `<p>${escapeHtml(para).replace(/\n/g, '<br />')}</p>`)
|
.map((para) => `<p>${escapeHtml(para).replace(/\n/g, '<br />')}</p>`)
|
||||||
.join('')
|
.join('')
|
||||||
}, [mode, thread.draft_response])
|
}, [mode, thread.gmail_draft, thread.draft_response])
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
|
|
|
||||||
|
|
@ -196,9 +196,14 @@ function buildPrompt(
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClassifyOptions {
|
||||||
|
skipDraft?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export async function classifyThread(
|
export async function classifyThread(
|
||||||
snapshot: GmailThreadSnapshot,
|
snapshot: GmailThreadSnapshot,
|
||||||
userEmail: string | null,
|
userEmail: string | null,
|
||||||
|
options: ClassifyOptions = {},
|
||||||
): Promise<Classification> {
|
): Promise<Classification> {
|
||||||
if (userReplied(snapshot, userEmail)) {
|
if (userReplied(snapshot, userEmail)) {
|
||||||
return { importance: 'important' };
|
return { importance: 'important' };
|
||||||
|
|
@ -213,9 +218,13 @@ export async function classifyThread(
|
||||||
const config = await resolveProviderConfig(provider);
|
const config = await resolveProviderConfig(provider);
|
||||||
const model = createProvider(config).languageModel(modelId);
|
const model = createProvider(config).languageModel(modelId);
|
||||||
|
|
||||||
|
const systemPrompt = options.skipDraft
|
||||||
|
? `${SYSTEM_PROMPT}\n\n# Skip the draft\n\nThe user already has their own draft in progress for this thread — DO NOT generate a draftResponse. Always omit the draftResponse field.`
|
||||||
|
: SYSTEM_PROMPT;
|
||||||
|
|
||||||
const result = await generateObject({
|
const result = await generateObject({
|
||||||
model,
|
model,
|
||||||
system: SYSTEM_PROMPT,
|
system: systemPrompt,
|
||||||
prompt: buildPrompt(snapshot, userEmail, styleGuide, calendar),
|
prompt: buildPrompt(snapshot, userEmail, styleGuide, calendar),
|
||||||
schema: ClassificationSchema,
|
schema: ClassificationSchema,
|
||||||
});
|
});
|
||||||
|
|
@ -231,7 +240,7 @@ export async function classifyThread(
|
||||||
const out: Classification = { importance: result.object.importance };
|
const out: Classification = { importance: result.object.importance };
|
||||||
if (result.object.importance === 'important') {
|
if (result.object.importance === 'important') {
|
||||||
if (result.object.summary) out.summary = result.object.summary;
|
if (result.object.summary) out.summary = result.object.summary;
|
||||||
if (result.object.draftResponse) out.draftResponse = result.object.draftResponse;
|
if (!options.skipDraft && result.object.draftResponse) out.draftResponse = result.object.draftResponse;
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ export interface GmailThreadSnapshot {
|
||||||
unread?: boolean;
|
unread?: boolean;
|
||||||
importance?: 'important' | 'other';
|
importance?: 'important' | 'other';
|
||||||
draft_response?: string;
|
draft_response?: string;
|
||||||
|
gmail_draft?: string;
|
||||||
messages: Array<{
|
messages: Array<{
|
||||||
id?: string;
|
id?: string;
|
||||||
from?: string;
|
from?: string;
|
||||||
|
|
@ -452,6 +453,7 @@ export async function fetchThreadSnapshot(threadId: string, expectedHistoryId?:
|
||||||
bodyHtml = parts.html;
|
bodyHtml = parts.html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const isDraft = msg.labelIds?.includes('DRAFT') ?? false;
|
||||||
return {
|
return {
|
||||||
id: msg.id || undefined,
|
id: msg.id || undefined,
|
||||||
from: headerValue(headers, 'From') || 'Unknown',
|
from: headerValue(headers, 'From') || 'Unknown',
|
||||||
|
|
@ -463,11 +465,24 @@ export async function fetchThreadSnapshot(threadId: string, expectedHistoryId?:
|
||||||
bodyHtml,
|
bodyHtml,
|
||||||
unread: msg.labelIds?.includes('UNREAD') ?? false,
|
unread: msg.labelIds?.includes('UNREAD') ?? false,
|
||||||
bodyHeight: msg.id ? heightCarryover.get(msg.id) : undefined,
|
bodyHeight: msg.id ? heightCarryover.get(msg.id) : undefined,
|
||||||
|
isDraft,
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const latest = parsed[parsed.length - 1]!;
|
const sentMessages = parsed.filter((m) => !m.isDraft);
|
||||||
const earlier = parsed.slice(0, -1);
|
const draftMessages = parsed.filter((m) => m.isDraft);
|
||||||
|
// Drop the isDraft helper field from outgoing messages — it's internal.
|
||||||
|
const visibleMessages = sentMessages.map(({ isDraft: _isDraft, ...rest }) => rest);
|
||||||
|
const latestDraftBody = draftMessages.length > 0
|
||||||
|
? draftMessages[draftMessages.length - 1]!.body.trim()
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// A thread with no sent messages (only a draft) shouldn't show up in the inbox —
|
||||||
|
// skip caching it. Once the user actually sends, the thread reappears with a real message.
|
||||||
|
if (visibleMessages.length === 0) return null;
|
||||||
|
|
||||||
|
const latest = visibleMessages[visibleMessages.length - 1]!;
|
||||||
|
const earlier = visibleMessages.slice(0, -1);
|
||||||
const earlierSummary = earlier
|
const earlierSummary = earlier
|
||||||
.map((msg) => {
|
.map((msg) => {
|
||||||
const date = msg.date ? ` (${msg.date})` : '';
|
const date = msg.date ? ` (${msg.date})` : '';
|
||||||
|
|
@ -480,19 +495,23 @@ export async function fetchThreadSnapshot(threadId: string, expectedHistoryId?:
|
||||||
const snapshot: GmailThreadSnapshot = {
|
const snapshot: GmailThreadSnapshot = {
|
||||||
threadId,
|
threadId,
|
||||||
threadUrl: `https://mail.google.com/mail/u/0/#all/${threadId}`,
|
threadUrl: `https://mail.google.com/mail/u/0/#all/${threadId}`,
|
||||||
subject: latest.subject || parsed[0]?.subject,
|
subject: latest.subject || visibleMessages[0]?.subject,
|
||||||
from: latest.from,
|
from: latest.from,
|
||||||
to: latest.to,
|
to: latest.to,
|
||||||
date: latest.date,
|
date: latest.date,
|
||||||
latest_email: latest.body,
|
latest_email: latest.body,
|
||||||
past_summary: earlierSummary || undefined,
|
past_summary: earlierSummary || undefined,
|
||||||
unread: parsed.some((m) => m.unread),
|
unread: visibleMessages.some((m) => m.unread),
|
||||||
messages: parsed,
|
messages: visibleMessages,
|
||||||
|
gmail_draft: latestDraftBody || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userEmail = await getUserEmail(auth);
|
const userEmail = await getUserEmail(auth);
|
||||||
const classification = await classifyThread(snapshot, userEmail);
|
// If the user already has a Gmail-side draft going, skip the AI draft generation —
|
||||||
|
// the renderer will prefer the Gmail draft anyway, and we save an LLM call.
|
||||||
|
const skipDraft = latestDraftBody.length > 0;
|
||||||
|
const classification = await classifyThread(snapshot, userEmail, { skipDraft });
|
||||||
snapshot.importance = classification.importance;
|
snapshot.importance = classification.importance;
|
||||||
if (classification.summary) snapshot.summary = classification.summary;
|
if (classification.summary) snapshot.summary = classification.summary;
|
||||||
if (classification.draftResponse) snapshot.draft_response = classification.draftResponse;
|
if (classification.draftResponse) snapshot.draft_response = classification.draftResponse;
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ export const GmailThreadSchema = EmailBlockSchema.extend({
|
||||||
threadUrl: z.string().url(),
|
threadUrl: z.string().url(),
|
||||||
unread: z.boolean().optional(),
|
unread: z.boolean().optional(),
|
||||||
importance: z.enum(['important', 'other']).optional(),
|
importance: z.enum(['important', 'other']).optional(),
|
||||||
|
gmail_draft: z.string().optional(),
|
||||||
messages: z.array(GmailThreadMessageSchema),
|
messages: z.array(GmailThreadMessageSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue