mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-12 19:55:19 +02:00
handle drafts
This commit is contained in:
parent
10c65f097a
commit
93101b2e83
4 changed files with 43 additions and 11 deletions
|
|
@ -399,12 +399,15 @@ function ComposeBox({
|
|||
const to = mode === 'reply' ? extractAddress(latest?.from) : ''
|
||||
|
||||
const initialContent = useMemo(() => {
|
||||
if (mode !== 'reply' || !thread.draft_response) return ''
|
||||
return thread.draft_response
|
||||
if (mode !== 'reply') return ''
|
||||
// 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,}/)
|
||||
.map((para) => `<p>${escapeHtml(para).replace(/\n/g, '<br />')}</p>`)
|
||||
.join('')
|
||||
}, [mode, thread.draft_response])
|
||||
}, [mode, thread.gmail_draft, thread.draft_response])
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
|
|
|
|||
|
|
@ -196,9 +196,14 @@ function buildPrompt(
|
|||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export interface ClassifyOptions {
|
||||
skipDraft?: boolean;
|
||||
}
|
||||
|
||||
export async function classifyThread(
|
||||
snapshot: GmailThreadSnapshot,
|
||||
userEmail: string | null,
|
||||
options: ClassifyOptions = {},
|
||||
): Promise<Classification> {
|
||||
if (userReplied(snapshot, userEmail)) {
|
||||
return { importance: 'important' };
|
||||
|
|
@ -213,9 +218,13 @@ export async function classifyThread(
|
|||
const config = await resolveProviderConfig(provider);
|
||||
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({
|
||||
model,
|
||||
system: SYSTEM_PROMPT,
|
||||
system: systemPrompt,
|
||||
prompt: buildPrompt(snapshot, userEmail, styleGuide, calendar),
|
||||
schema: ClassificationSchema,
|
||||
});
|
||||
|
|
@ -231,7 +240,7 @@ export async function classifyThread(
|
|||
const out: Classification = { importance: result.object.importance };
|
||||
if (result.object.importance === 'important') {
|
||||
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;
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ export interface GmailThreadSnapshot {
|
|||
unread?: boolean;
|
||||
importance?: 'important' | 'other';
|
||||
draft_response?: string;
|
||||
gmail_draft?: string;
|
||||
messages: Array<{
|
||||
id?: string;
|
||||
from?: string;
|
||||
|
|
@ -452,6 +453,7 @@ export async function fetchThreadSnapshot(threadId: string, expectedHistoryId?:
|
|||
bodyHtml = parts.html;
|
||||
}
|
||||
}
|
||||
const isDraft = msg.labelIds?.includes('DRAFT') ?? false;
|
||||
return {
|
||||
id: msg.id || undefined,
|
||||
from: headerValue(headers, 'From') || 'Unknown',
|
||||
|
|
@ -463,11 +465,24 @@ export async function fetchThreadSnapshot(threadId: string, expectedHistoryId?:
|
|||
bodyHtml,
|
||||
unread: msg.labelIds?.includes('UNREAD') ?? false,
|
||||
bodyHeight: msg.id ? heightCarryover.get(msg.id) : undefined,
|
||||
isDraft,
|
||||
};
|
||||
}));
|
||||
|
||||
const latest = parsed[parsed.length - 1]!;
|
||||
const earlier = parsed.slice(0, -1);
|
||||
const sentMessages = parsed.filter((m) => !m.isDraft);
|
||||
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
|
||||
.map((msg) => {
|
||||
const date = msg.date ? ` (${msg.date})` : '';
|
||||
|
|
@ -480,19 +495,23 @@ export async function fetchThreadSnapshot(threadId: string, expectedHistoryId?:
|
|||
const snapshot: GmailThreadSnapshot = {
|
||||
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,
|
||||
to: latest.to,
|
||||
date: latest.date,
|
||||
latest_email: latest.body,
|
||||
past_summary: earlierSummary || undefined,
|
||||
unread: parsed.some((m) => m.unread),
|
||||
messages: parsed,
|
||||
unread: visibleMessages.some((m) => m.unread),
|
||||
messages: visibleMessages,
|
||||
gmail_draft: latestDraftBody || undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
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;
|
||||
if (classification.summary) snapshot.summary = classification.summary;
|
||||
if (classification.draftResponse) snapshot.draft_response = classification.draftResponse;
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ export const GmailThreadSchema = EmailBlockSchema.extend({
|
|||
threadUrl: z.string().url(),
|
||||
unread: z.boolean().optional(),
|
||||
importance: z.enum(['important', 'other']).optional(),
|
||||
gmail_draft: z.string().optional(),
|
||||
messages: z.array(GmailThreadMessageSchema),
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue