Spaces:
Sleeping
Sleeping
Download frontend/src/components/EmailQueue.jsx from habib-analyst/email-agent-api: direct link, hf CLI and curl.
- Browser
- Download file 5.42 kB
-
https://huggingface.co/spaces/habib-analyst/email-agent-api/resolve/main/frontend/src/components/EmailQueue.jsx
- Command line
-
hf download hf://spaces/habib-analyst/email-agent-api/frontend/src/components/EmailQueue.jsx
-
curl -L -o EmailQueue.jsx https://huggingface.co/spaces/habib-analyst/email-agent-api/resolve/main/frontend/src/components/EmailQueue.jsx
5.42 kB
| import { | |
| CheckCircle2, XCircle, Clock, Loader2, SkipForward, Send, Zap, Square, Eye, | |
| } from "lucide-react"; | |
| const STATUS_CONFIG = { | |
| queued: { icon: Clock, color: "text-[var(--c-text-muted)]", bg: "bg-[var(--c-bg-subtle)]", border: "border-[var(--c-border)]", label: "Queued" }, | |
| generating: { icon: Loader2, color: "text-indigo-500 dark:text-indigo-400", bg: "bg-indigo-500/5", border: "border-indigo-500/10", label: "Generating", spin: true }, | |
| ready: { icon: Eye, color: "text-amber-600 dark:text-amber-300", bg: "bg-amber-500/10", border: "border-amber-500/20", label: "Ready" }, | |
| sending: { icon: Loader2, color: "text-indigo-600 dark:text-indigo-300", bg: "bg-indigo-500/10", border: "border-indigo-500/20", label: "Sending", spin: true }, | |
| sent: { icon: CheckCircle2, color: "text-emerald-700 dark:text-emerald-300", bg: "bg-emerald-500/10", border: "border-emerald-500/20", label: "Sent" }, | |
| skipped: { icon: SkipForward, color: "text-[var(--c-text-muted)]", bg: "bg-[var(--c-bg-subtle)]", border: "border-[var(--c-border)]", label: "Skipped" }, | |
| failed: { icon: XCircle, color: "text-red-500 dark:text-red-400", bg: "bg-red-500/5", border: "border-red-500/10", label: "Failed" }, | |
| }; | |
| export default function EmailQueue({ queue, currentIndex, stats, batchMode, autoRunning, onSelect, onApprove, onSkip, onAutoRun, onStopAuto }) { | |
| if (!queue || queue.length === 0) return null; | |
| const total = queue.length; | |
| const sent = stats?.sent || 0; | |
| const failed = stats?.failed || 0; | |
| const skipped = stats?.skipped || 0; | |
| const done = sent + failed + skipped; | |
| const progress = total > 0 ? (done / total) * 100 : 0; | |
| return ( | |
| <div className="sticky bottom-0 bg-[var(--c-bg-primary)]/80 backdrop-blur-xl border-t border-[var(--c-border)] px-6 py-3 transition-colors duration-300 z-30"> | |
| <div className="flex items-center justify-between mb-2"> | |
| <div className="flex items-center gap-3 text-[11px]"> | |
| <span className="font-bold text-[var(--c-text-primary)]">{sent}/{total}</span> | |
| <span className="text-[var(--c-text-muted)]">sent</span> | |
| {skipped > 0 && <span className="text-[var(--c-text-muted)]">{skipped} skipped</span>} | |
| {failed > 0 && <span className="text-red-500 dark:text-red-400 font-medium">{failed} failed</span>} | |
| {autoRunning && <span className="flex items-center gap-1 text-indigo-500 dark:text-indigo-400 font-medium"><Loader2 size={10} className="animate-spin" /> Running...</span>} | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| {batchMode === "auto" && !autoRunning && sent < total && ( | |
| <button onClick={onAutoRun} className="flex items-center gap-1.5 px-4 py-1.5 bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 text-white text-[11px] font-bold rounded-lg transition-all shadow-lg shadow-emerald-500/15"> | |
| <Zap size={11} /> Auto-Send All | |
| </button> | |
| )} | |
| {autoRunning && ( | |
| <button onClick={onStopAuto} className="flex items-center gap-1.5 px-4 py-1.5 bg-red-500/20 hover:bg-red-500/30 text-red-500 dark:text-red-400 border border-red-500/20 text-[11px] font-semibold rounded-lg transition-all"> | |
| <Square size={11} /> Stop | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| <div className="w-full bg-gray-200 dark:bg-white/[0.04] rounded-full h-1 mb-3 overflow-hidden"> | |
| <div className="bg-gradient-to-r from-emerald-400 to-teal-400 h-1 rounded-full transition-all duration-700 ease-out" style={{ width: `${progress}%` }} /> | |
| </div> | |
| <div className="flex gap-1.5 overflow-x-auto pb-1"> | |
| {queue.map((item, i) => { | |
| const config = STATUS_CONFIG[item.status] || STATUS_CONFIG.queued; | |
| const Icon = config.icon; | |
| const isActive = i === currentIndex; | |
| return ( | |
| <div key={i} className="shrink-0 flex flex-col items-center gap-0.5"> | |
| <button onClick={() => onSelect?.(i)} | |
| className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-medium transition-all border ${isActive ? "bg-indigo-500/20 text-indigo-600 dark:text-indigo-300 border-indigo-500/30 shadow-lg shadow-indigo-500/10" : `${config.bg} ${config.color} ${config.border} hover:brightness-110` | |
| }`} | |
| title={`${item.email} — ${config.label}${item.error ? ": " + item.error : ""}`}> | |
| <Icon size={11} className={config.spin ? "animate-spin" : ""} /> | |
| <span className="max-w-[90px] truncate">{item.email?.split("@")[0] || item.input}</span> | |
| </button> | |
| {isActive && item.status === "ready" && batchMode === "manual" && ( | |
| <div className="flex gap-1 mt-0.5"> | |
| <button onClick={() => onApprove?.(i)} className="flex items-center gap-0.5 px-2 py-0.5 bg-emerald-500 hover:bg-emerald-400 text-white rounded text-[9px] font-semibold transition-colors" title="Send"><Send size={8} /> Send</button> | |
| <button onClick={() => onSkip?.(i)} className="px-2 py-0.5 bg-gray-100 dark:bg-white/5 text-gray-500 dark:text-[#5a5a72] hover:bg-gray-200 dark:hover:bg-white/10 rounded text-[9px] font-medium transition-colors" title="Skip">Skip</button> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ); | |
| } | |