Spaces:
Configuration error
Configuration error
| import React, { useState, useEffect } from 'react'; | |
| import { Send, Sparkles, AlertCircle, CheckCircle } from 'lucide-react'; | |
| import { API_BASE_URL } from '@/app/config'; | |
| interface StreamingDraftProps { | |
| emailId: string; | |
| emailSubject?: string; | |
| emailSender: string; | |
| onSendEmail: (to: string, subject: string, body: string) => Promise<boolean>; | |
| } | |
| export default function StreamingDraft({ | |
| emailId, | |
| emailSubject, | |
| emailSender, | |
| onSendEmail, | |
| }: StreamingDraftProps) { | |
| const [tone, setTone] = useState<'professional' | 'casual' | 'short'>('professional'); | |
| const [draftContent, setDraftContent] = useState(''); | |
| const [isGenerating, setIsGenerating] = useState(false); | |
| const [isSending, setIsSending] = useState(false); | |
| const [sendStatus, setSendStatus] = useState<'idle' | 'success' | 'error'>('idle'); | |
| const [errorMsg, setErrorMsg] = useState(''); | |
| // Clear draft when email selection changes | |
| useEffect(() => { | |
| setDraftContent(''); | |
| setSendStatus('idle'); | |
| }, [emailId]); | |
| const generateDraft = async () => { | |
| setIsGenerating(true); | |
| setDraftContent(''); | |
| setSendStatus('idle'); | |
| // Connect to FastAPI SSE endpoint | |
| const url = `${API_BASE_URL}/api/emails/${emailId}/draft?tone=${tone}`; | |
| // Access token from localStorage | |
| const token = localStorage.getItem('token') || ''; | |
| try { | |
| const response = await fetch(url, { | |
| headers: { | |
| 'Authorization': `Bearer ${token}`, | |
| }, | |
| }); | |
| if (!response.ok) { | |
| throw new Error('Failed to generate draft stream.'); | |
| } | |
| const reader = response.body?.getReader(); | |
| const decoder = new TextDecoder(); | |
| if (!reader) { | |
| throw new Error('No readable stream body.'); | |
| } | |
| let done = false; | |
| let fullText = ''; | |
| while (!done) { | |
| const { value, done: doneReading } = await reader.read(); | |
| done = doneReading; | |
| if (value) { | |
| const chunkStr = decoder.decode(value); | |
| // Parse Server Sent Events format "data: text\n\n" | |
| const lines = chunkStr.split('\n'); | |
| for (const line of lines) { | |
| if (line.startsWith('data: ')) { | |
| const content = line.substring(6); | |
| fullText += content; | |
| setDraftContent(fullText); | |
| } | |
| } | |
| } | |
| } | |
| } catch (err: any) { | |
| console.error(err); | |
| setErrorMsg(err.message || 'Connection lost during drafting.'); | |
| setSendStatus('error'); | |
| } finally { | |
| setIsGenerating(false); | |
| } | |
| }; | |
| const handleSend = async () => { | |
| if (!draftContent.trim()) return; | |
| setIsSending(true); | |
| setSendStatus('idle'); | |
| // Extract target email address | |
| let toEmail = emailSender; | |
| const match = emailSender.match(/<([^>]+)>/); | |
| if (match) { | |
| toEmail = match[1]; | |
| } | |
| const subject = emailSubject ? `Re: ${emailSubject}` : 'Re: Email'; | |
| const success = await onSendEmail(toEmail, subject, draftContent); | |
| setIsSending(false); | |
| if (success) { | |
| setSendStatus('success'); | |
| } else { | |
| setErrorMsg('Failed to deliver reply.'); | |
| setSendStatus('error'); | |
| } | |
| }; | |
| const tones = [ | |
| { label: '💼 Professional', value: 'professional' }, | |
| { label: '☕ Casual', value: 'casual' }, | |
| { label: '⚡ Short / Quick', value: 'short' }, | |
| ]; | |
| return ( | |
| <div className="glass-panel border-purple-500/10 rounded-2xl p-6 flex flex-col gap-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2 text-purple-400 font-semibold text-sm"> | |
| <Sparkles size={16} className="fill-purple-400/20" /> | |
| <span>AI Streaming Reply Composer</span> | |
| </div> | |
| <div className="text-xs text-slate-500"> | |
| Powered by GPT-4o-Mini | |
| </div> | |
| </div> | |
| {/* Tone Selection */} | |
| <div className="flex gap-2.5 bg-slate-950/40 border border-slate-900/60 p-1 rounded-xl"> | |
| {tones.map((t) => ( | |
| <button | |
| key={t.value} | |
| disabled={isGenerating || isSending} | |
| onClick={() => setTone(t.value as any)} | |
| className={`flex-1 text-center py-2 text-xs font-medium rounded-lg cursor-pointer transition ${ | |
| tone === t.value | |
| ? 'bg-purple-600/20 text-purple-300 border border-purple-500/20' | |
| : 'text-slate-500 hover:text-slate-300 border border-transparent' | |
| }`} | |
| > | |
| {t.label} | |
| </button> | |
| ))} | |
| </div> | |
| {/* Generator trigger button */} | |
| {draftContent === '' && !isGenerating && ( | |
| <button | |
| onClick={generateDraft} | |
| className="glow-btn text-white py-3 rounded-xl font-medium flex items-center justify-center gap-2 text-sm cursor-pointer" | |
| > | |
| <Sparkles size={14} /> | |
| <span>Draft Response in {tone.toUpperCase()} Tone</span> | |
| </button> | |
| )} | |
| {/* Draft text editor */} | |
| {(draftContent !== '' || isGenerating) && ( | |
| <div className="flex flex-col gap-3"> | |
| <textarea | |
| value={draftContent} | |
| onChange={(e) => setDraftContent(e.target.value)} | |
| disabled={isGenerating || isSending} | |
| rows={8} | |
| className="w-full text-sm font-sans leading-relaxed bg-slate-950/50 p-4 border border-slate-900 rounded-xl focus:outline-none focus:border-purple-600 resize-y text-slate-300" | |
| placeholder={isGenerating ? "AI is typing reply word-by-word..." : "Edit response..."} | |
| /> | |
| <div className="flex items-center justify-between mt-1"> | |
| {/* Status indicators */} | |
| <div> | |
| {isGenerating && ( | |
| <div className="flex items-center gap-2 text-purple-400 text-xs font-medium"> | |
| <div className="h-3 w-3 animate-spin rounded-full border border-purple-400 border-t-transparent" /> | |
| <span>Streaming draft...</span> | |
| </div> | |
| )} | |
| {sendStatus === 'success' && ( | |
| <div className="flex items-center gap-1.5 text-emerald-400 text-xs font-medium"> | |
| <CheckCircle size={14} /> | |
| <span>Reply sent successfully!</span> | |
| </div> | |
| )} | |
| {sendStatus === 'error' && ( | |
| <div className="flex items-center gap-1.5 text-red-400 text-xs font-medium"> | |
| <AlertCircle size={14} /> | |
| <span>{errorMsg}</span> | |
| </div> | |
| )} | |
| </div> | |
| <div className="flex gap-2"> | |
| <button | |
| onClick={generateDraft} | |
| disabled={isGenerating || isSending} | |
| className="bg-slate-950 border border-slate-800 text-slate-300 hover:text-white px-4 py-2 rounded-xl text-xs font-semibold cursor-pointer transition disabled:opacity-50" | |
| > | |
| Regenerate | |
| </button> | |
| <button | |
| onClick={handleSend} | |
| disabled={isGenerating || isSending || !draftContent.trim()} | |
| className="glow-btn text-white px-4.5 py-2 rounded-xl text-xs font-semibold flex items-center gap-1.5 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed" | |
| > | |
| <Send size={12} /> | |
| <span>{isSending ? 'Sending...' : 'Send Reply'}</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |