Spaces:
Sleeping
Sleeping
Download frontend/src/components/RecipientInput.jsx from habib-analyst/email-agent-api: direct link, hf CLI and curl.
- Browser
- Download file 14.1 kB
-
https://huggingface.co/spaces/habib-analyst/email-agent-api/resolve/main/frontend/src/components/RecipientInput.jsx
- Command line
-
hf download hf://spaces/habib-analyst/email-agent-api/frontend/src/components/RecipientInput.jsx
-
curl -L -o RecipientInput.jsx https://huggingface.co/spaces/habib-analyst/email-agent-api/resolve/main/frontend/src/components/RecipientInput.jsx
14.1 kB
| import { useState, useRef } from "react"; | |
| import { Search, Users, Loader2, FileSpreadsheet, Zap, Eye, Sparkles, Globe, Link2 } from "lucide-react"; | |
| import { batchAPI } from "../api/client"; | |
| import toast from "react-hot-toast"; | |
| export default function RecipientInput({ onResearch, onBatchStart, loading, disabled }) { | |
| const [mode, setMode] = useState("single"); | |
| const [singleInput, setSingleInput] = useState(""); | |
| const [batchInput, setBatchInput] = useState(""); | |
| const [extractedEmails, setExtractedEmails] = useState([]); | |
| const [batchMode, setBatchMode] = useState("manual"); | |
| const [delayMin, setDelayMin] = useState(15); | |
| const [delayMax, setDelayMax] = useState(45); | |
| const [extracting, setExtracting] = useState(false); | |
| const [directoryUrl, setDirectoryUrl] = useState(""); | |
| const [scraping, setScraping] = useState(false); | |
| const fileRef = useRef(null); | |
| const handleSingleSubmit = (e) => { e.preventDefault(); if (singleInput.trim() && onResearch) onResearch(singleInput.trim()); }; | |
| const handleCSVUpload = async (e) => { | |
| const file = e.target.files?.[0]; if (!file) return; | |
| setExtracting(true); | |
| try { const res = await batchAPI.uploadCSV(file); setExtractedEmails(res.data.emails); setBatchInput(res.data.emails.join("\n")); toast.success(`Found ${res.data.count} emails from ${file.name}`); } | |
| catch (err) { toast.error(err.response?.data?.detail || "Failed to parse file"); } | |
| setExtracting(false); if (fileRef.current) fileRef.current.value = ""; | |
| }; | |
| const handleExtractFromText = async () => { | |
| if (!batchInput.trim()) return; setExtracting(true); | |
| try { const res = await batchAPI.extractEmails(batchInput); setExtractedEmails(res.data.emails); setBatchInput(res.data.emails.join("\n")); toast.success(`Extracted ${res.data.count} emails`); } | |
| catch { toast.error("No emails found in the text"); } | |
| setExtracting(false); | |
| }; | |
| const handleUrlScrape = async () => { | |
| if (!directoryUrl.trim()) return; setScraping(true); | |
| try { const res = await batchAPI.scrapeFaculty(directoryUrl.trim()); const emails = res.data.emails || []; setBatchInput(emails.join("\n")); setExtractedEmails(emails); setMode("batch"); toast.success(`Found ${res.data.count} email addresses!`); } | |
| catch (err) { toast.error(err.response?.data?.detail || "Could not scrape page"); } | |
| setScraping(false); | |
| }; | |
| const handleBatchSubmit = () => { | |
| const list = batchInput.split("\n").map((l) => l.trim()).filter(Boolean); | |
| if (list.length > 0 && onBatchStart) onBatchStart(list, batchMode, delayMin, delayMax); | |
| }; | |
| const emailCount = batchInput.split("\n").filter((l) => l.trim()).length; | |
| const tabClass = (active) => | |
| `flex-1 text-[10px] py-1.5 px-1 rounded-lg font-semibold transition-all ${active | |
| ? "bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border border-indigo-500/20" | |
| : "text-[var(--c-text-muted)] hover:text-[var(--c-text-secondary)] border border-transparent" | |
| }`; | |
| return ( | |
| <div className="card-dark p-3.5"> | |
| <div className="flex items-center gap-2 mb-3"> | |
| <Sparkles size={14} className="text-indigo-500 dark:text-indigo-400" /> | |
| <h3 className="text-xs font-semibold text-[var(--c-text-secondary)] uppercase tracking-wider">Compose & Send</h3> | |
| </div> | |
| <div className="flex gap-1 mb-3 bg-[var(--c-bg-subtle)] p-1 rounded-xl border border-[var(--c-border)]"> | |
| <button onClick={() => setMode("single")} className={tabClass(mode === "single")}>Single</button> | |
| <button onClick={() => setMode("batch")} className={tabClass(mode === "batch")}>Batch</button> | |
| <button onClick={() => setMode("scrape")} className={tabClass(mode === "scrape")}>Scrape URL</button> | |
| </div> | |
| {mode === "single" && ( | |
| <form onSubmit={handleSingleSubmit}> | |
| <input type="text" value={singleInput} onChange={(e) => setSingleInput(e.target.value)} placeholder="recipient@example.com" disabled={disabled || loading} className="w-full input-dark text-sm mb-2.5" /> | |
| <button type="submit" disabled={!singleInput.trim() || loading || disabled} className="w-full btn-primary text-xs py-2.5"> | |
| {loading ? <><Loader2 size={13} className="animate-spin" /> Generating...</> : <><Search size={13} /> Generate Email</>} | |
| </button> | |
| </form> | |
| )} | |
| {mode === "scrape" && ( | |
| <div className="space-y-2.5"> | |
| <div className="rounded-xl border border-[var(--c-border)] bg-[var(--c-bg-subtle)] overflow-hidden focus-within:border-indigo-500/30 transition-all"> | |
| <div className="flex items-center gap-2 px-3 pt-2.5 pb-1"> | |
| <Link2 size={13} className="text-[var(--c-text-muted)] shrink-0" /> | |
| <input type="text" value={directoryUrl} onChange={(e) => setDirectoryUrl(e.target.value)} placeholder="https://example.com/team" disabled={disabled || scraping} | |
| className="w-full text-[12px] outline-none bg-transparent text-[var(--c-text-primary)] placeholder:text-[var(--c-text-muted)]" /> | |
| </div> | |
| <div className="px-3 py-1.5 bg-[var(--c-bg-subtle)] border-t border-[var(--c-border)]"> | |
| <span className="text-[9px] text-[var(--c-text-muted)]">Paste any page URL to extract email addresses</span> | |
| </div> | |
| </div> | |
| <button onClick={handleUrlScrape} disabled={!directoryUrl.trim() || scraping || disabled} | |
| className="w-full flex items-center justify-center gap-2 bg-gradient-to-r from-violet-500 to-purple-600 hover:from-violet-400 hover:to-purple-500 disabled:opacity-40 text-white text-xs font-semibold py-2.5 rounded-xl transition-all shadow-lg shadow-violet-500/15"> | |
| {scraping ? <><Loader2 size={13} className="animate-spin" /> Scraping page...</> : <><Globe size={13} /> Extract All Emails</>} | |
| </button> | |
| {extractedEmails.length > 0 && ( | |
| <div className="bg-emerald-500/5 border border-emerald-500/20 rounded-xl p-3"> | |
| <p className="text-[11px] font-semibold text-emerald-600 dark:text-emerald-400 mb-0.5">Found {extractedEmails.length} emails</p> | |
| <p className="text-[10px] text-emerald-600/60 dark:text-emerald-500/60">Switched to Batch tab with all emails loaded</p> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {mode === "batch" && ( | |
| <div className="space-y-2.5"> | |
| <input ref={fileRef} type="file" accept=".csv,.txt,.xlsx" onChange={handleCSVUpload} className="hidden" /> | |
| <button onClick={() => fileRef.current?.click()} disabled={disabled || loading} | |
| className="w-full flex items-center justify-center gap-2 border border-dashed border-gray-300 dark:border-white/10 hover:border-indigo-500/30 text-[var(--c-text-muted)] hover:text-indigo-500 dark:hover:text-indigo-400 text-[11px] font-medium py-2 rounded-xl transition-all hover:bg-indigo-500/5"> | |
| {extracting ? <><Loader2 size={12} className="animate-spin" /> Extracting...</> : <><FileSpreadsheet size={12} /> Upload CSV / Text File</>} | |
| </button> | |
| <div className="rounded-xl border border-[var(--c-border)] bg-[var(--c-bg-subtle)] overflow-hidden focus-within:border-indigo-500/30 transition-all"> | |
| <textarea value={batchInput} onChange={(e) => setBatchInput(e.target.value)} placeholder={"contact1@example.com\ncontact2@company.org\ncontact3@domain.net"} disabled={disabled || loading} rows={4} | |
| className="w-full text-[12px] leading-relaxed px-3 py-2.5 outline-none resize-none bg-transparent text-[var(--c-text-primary)] placeholder:text-[var(--c-text-muted)] disabled:opacity-50" /> | |
| <div className="flex items-center justify-between px-3 py-1.5 bg-[var(--c-bg-subtle)] border-t border-[var(--c-border)]"> | |
| <span className="text-[10px] font-medium text-[var(--c-text-muted)]">{emailCount} {emailCount === 1 ? "recipient" : "recipients"}</span> | |
| <span className="text-[9px] text-[var(--c-text-muted)]">One per line or paste any text</span> | |
| </div> | |
| </div> | |
| {batchInput.trim() && !batchInput.includes("@") && ( | |
| <button onClick={handleExtractFromText} disabled={extracting} className="w-full text-[10px] text-indigo-500 dark:text-indigo-400 hover:text-indigo-600 dark:hover:text-indigo-300 font-semibold"> | |
| Extract emails from text | |
| </button> | |
| )} | |
| <div className="bg-[var(--c-bg-subtle)] rounded-xl p-3 space-y-2 border border-[var(--c-border)]"> | |
| <div className="text-[10px] font-semibold text-[var(--c-text-muted)] uppercase tracking-wider">Send Mode</div> | |
| <div className="flex gap-1 bg-[var(--c-bg-subtle)] p-0.5 rounded-lg border border-[var(--c-border)]"> | |
| <button onClick={() => setBatchMode("manual")} | |
| className={`flex-1 flex items-center justify-center gap-1.5 text-[11px] py-2 rounded-lg font-semibold transition-all ${batchMode === "manual" ? "bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20" : "text-[var(--c-text-muted)] hover:text-[var(--c-text-secondary)] border border-transparent"}`}> | |
| <Eye size={11} /> Review | |
| </button> | |
| <button onClick={() => setBatchMode("auto")} | |
| className={`flex-1 flex items-center justify-center gap-1.5 text-[11px] py-2 rounded-lg font-semibold transition-all ${batchMode === "auto" ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20" : "text-[var(--c-text-muted)] hover:text-[var(--c-text-secondary)] border border-transparent"}`}> | |
| <Zap size={11} /> Auto | |
| </button> | |
| </div> | |
| <p className="text-[9px] text-[var(--c-text-muted)] leading-relaxed"> | |
| {batchMode === "manual" ? "Review & approve each email before sending" : "Generate & send all automatically"} | |
| </p> | |
| {batchMode === "auto" && ( | |
| <div className="pt-2.5 border-t border-[var(--c-border)]"> | |
| <div className="text-[10px] font-semibold text-[var(--c-text-muted)] uppercase tracking-wider mb-2">Delay between emails</div> | |
| <div className="flex items-center gap-2"> | |
| <div className="flex-1 min-w-[65px] relative group"> | |
| <input type="number" value={delayMin} onChange={(e) => setDelayMin(Number(e.target.value))} min={5} max={120} | |
| className="w-full text-sm font-bold input-dark px-3 py-2.5 text-center tabular-nums [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none focus:ring-2 focus:ring-indigo-500/10 transition-all" /> | |
| <div className="absolute inset-0 rounded-xl border border-transparent group-focus-within:border-indigo-500/30 pointer-events-none transition-all" /> | |
| </div> | |
| <span className="text-[10px] font-bold text-[var(--c-text-muted)] uppercase shrink-0">to</span> | |
| <div className="flex-1 min-w-[65px] relative group"> | |
| <input type="number" value={delayMax} onChange={(e) => setDelayMax(Number(e.target.value))} min={10} max={300} | |
| className="w-full text-sm font-bold input-dark px-3 py-2.5 text-center tabular-nums [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none focus:ring-2 focus:ring-indigo-500/10 transition-all" /> | |
| <div className="absolute inset-0 rounded-xl border border-transparent group-focus-within:border-indigo-500/30 pointer-events-none transition-all" /> | |
| </div> | |
| <div className="shrink-0 flex items-center justify-center bg-[var(--c-bg-subtle)] border border-[var(--c-border)] rounded-xl px-2.5 py-2.5"> | |
| <span className="text-[10px] font-bold text-[var(--c-text-muted)] uppercase">sec</span> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <button onClick={handleBatchSubmit} disabled={emailCount === 0 || loading || disabled} | |
| className="w-full flex items-center justify-center gap-2 bg-gradient-to-r from-indigo-500 to-violet-600 hover:from-indigo-400 hover:to-violet-500 disabled:opacity-40 text-white text-xs font-semibold py-2.5 rounded-xl transition-all shadow-lg shadow-indigo-500/15"> | |
| {loading ? <><Loader2 size={13} className="animate-spin" /> Processing...</> : <><Users size={13} /> Start Batch ({emailCount})</>} | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |