Files
pn-new-crm/src/components/email/email-draft-button.tsx

192 lines
6.3 KiB
TypeScript
Raw Normal View History

'use client';
import { useState, useCallback } from 'react';
import { Loader2, Mail, Copy, Check } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetDescription,
} from '@/components/ui/sheet';
import { useFeatureFlag } from '@/hooks/use-feature-flag';
import { apiFetch } from '@/lib/api/client';
// ─── Types ────────────────────────────────────────────────────────────────────
interface EmailDraftButtonProps {
interestId: string;
clientId: string;
context?: 'follow_up' | 'introduction' | 'stage_update' | 'general';
additionalInstructions?: string;
}
interface DraftResult {
subject: string;
body: string;
generatedAt: string;
}
// ─── Polling helper ───────────────────────────────────────────────────────────
const POLL_INTERVAL_MS = 1500;
const POLL_MAX_ATTEMPTS = 20; // 30 s total
async function pollDraftResult(jobId: string): Promise<DraftResult> {
for (let attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) {
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
const res = await apiFetch<{ status: string; data?: DraftResult }>(
`/api/v1/ai/email-draft/${jobId}`,
);
if (res.status === 'complete' && res.data) {
return res.data;
}
}
throw new Error('Email draft generation timed out. Please try again.');
}
// ─── Component ────────────────────────────────────────────────────────────────
export function EmailDraftButton({
interestId,
clientId,
context = 'follow_up',
additionalInstructions,
}: EmailDraftButtonProps) {
const featureEnabled = useFeatureFlag('ai_email_drafts');
const [isLoading, setIsLoading] = useState(false);
const [sheetOpen, setSheetOpen] = useState(false);
const [draft, setDraft] = useState<DraftResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const handleGenerateDraft = useCallback(async () => {
setIsLoading(true);
setError(null);
setDraft(null);
try {
const { jobId } = await apiFetch<{ jobId: string }>('/api/v1/ai/email-draft', {
method: 'POST',
body: { interestId, clientId, context, additionalInstructions },
});
const result = await pollDraftResult(jobId);
setDraft(result);
setSheetOpen(true);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to generate draft');
} finally {
setIsLoading(false);
}
}, [interestId, clientId, context, additionalInstructions]);
const handleCopy = useCallback(async () => {
if (!draft) return;
const text = `Subject: ${draft.subject}\n\n${draft.body}`;
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}, [draft]);
if (!featureEnabled) return null;
return (
<>
<Button variant="outline" size="sm" onClick={handleGenerateDraft} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
Generating...
</>
) : (
<>
<Mail className="mr-1.5 h-3.5 w-3.5" />
Draft Email
</>
)}
</Button>
{error && <p className="text-xs text-destructive mt-1">{error}</p>}
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
<SheetContent side="right" className="w-full max-w-lg flex flex-col">
<SheetHeader>
<SheetTitle>Generated Email Draft</SheetTitle>
<SheetDescription>Review and edit the draft before sending.</SheetDescription>
</SheetHeader>
{draft && (
<div className="flex-1 overflow-y-auto mt-4 space-y-4">
{/* Subject */}
<div>
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Subject
</label>
<p
contentEditable
suppressContentEditableWarning
className="mt-1 text-sm font-medium border rounded-md px-3 py-2 focus:outline-hidden focus:ring-2 focus:ring-ring"
>
{draft.subject}
</p>
</div>
{/* Body */}
<div className="flex-1">
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Body
</label>
<pre
contentEditable
suppressContentEditableWarning
className="mt-1 text-sm whitespace-pre-wrap font-sans border rounded-md px-3 py-2 min-h-[300px] focus:outline-hidden focus:ring-2 focus:ring-ring"
>
{draft.body}
</pre>
</div>
{/* Actions */}
<div className="flex gap-2 pt-2 border-t">
<Button variant="outline" size="sm" onClick={handleCopy}>
{copied ? (
<>
<Check className="mr-1.5 h-3.5 w-3.5 text-green-600" />
Copied
</>
) : (
<>
<Copy className="mr-1.5 h-3.5 w-3.5" />
Copy to clipboard
</>
)}
</Button>
<Button
variant="outline"
size="sm"
onClick={handleGenerateDraft}
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
Regenerating...
</>
) : (
'Regenerate'
)}
</Button>
</div>
</div>
)}
</SheetContent>
</Sheet>
</>
);
}