agenticworkflowsspace commited on
Commit
2002023
Β·
verified Β·
1 Parent(s): 8c85fd3

Upload frontend/src/App.jsx with huggingface_hub

Browse files
Files changed (1) hide show
  1. frontend/src/App.jsx +261 -0
frontend/src/App.jsx ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import gsap from 'gsap';
3
+
4
+ const API_BASE = '/api';
5
+
6
+ export default function App() {
7
+ const [niche, setNiche] = useState('');
8
+ const [location, setLocation] = useState('');
9
+ const [limit, setLimit] = useState(10);
10
+ const [jobStatus, setJobStatus] = useState({ status: 'idle', message: 'Awaiting transmission...', progress: 0 });
11
+ const [results, setResults] = useState([]);
12
+ const [activeTab, setActiveTab] = useState('search');
13
+
14
+ const heroRef = useRef(null);
15
+ const panelRef = useRef(null);
16
+ const gridRef = useRef(null);
17
+ const particlesRef = useRef([]);
18
+
19
+ const isRunning = ['scraping','enriching','saving'].includes(jobStatus.status);
20
+
21
+ /* ── Entrance GSAP animation ── */
22
+ useEffect(() => {
23
+ const ctx = gsap.context(() => {
24
+ gsap.fromTo(heroRef.current,
25
+ { opacity: 0, y: -40 },
26
+ { opacity: 1, y: 0, duration: 1.1, ease: 'power4.out' }
27
+ );
28
+ gsap.fromTo(panelRef.current,
29
+ { opacity: 0, y: 30, scale: 0.97 },
30
+ { opacity: 1, y: 0, scale: 1, duration: 0.9, delay: 0.3, ease: 'back.out(1.4)' }
31
+ );
32
+ });
33
+ return () => ctx.revert();
34
+ }, []);
35
+
36
+ /* ── Animate particles ── */
37
+ useEffect(() => {
38
+ particlesRef.current.forEach((el, i) => {
39
+ if (!el) return;
40
+ gsap.to(el, {
41
+ y: `random(-30, 30)`,
42
+ x: `random(-20, 20)`,
43
+ opacity: `random(0.2, 0.8)`,
44
+ duration: `random(3, 6)`,
45
+ repeat: -1,
46
+ yoyo: true,
47
+ ease: 'sine.inOut',
48
+ delay: i * 0.3,
49
+ });
50
+ });
51
+ }, []);
52
+
53
+ /* ── Poll status while running ── */
54
+ useEffect(() => {
55
+ if (!isRunning) return;
56
+ const id = setInterval(async () => {
57
+ try {
58
+ const res = await fetch(`${API_BASE}/status`);
59
+ const data = await res.json();
60
+ setJobStatus(data);
61
+ if (data.status === 'complete') {
62
+ clearInterval(id);
63
+ loadResults();
64
+ } else if (data.status === 'error') {
65
+ clearInterval(id);
66
+ }
67
+ } catch (_) {}
68
+ }, 1000);
69
+ return () => clearInterval(id);
70
+ }, [isRunning]);
71
+
72
+ /* ── Animate result rows in ── */
73
+ useEffect(() => {
74
+ if (results.length > 0 && gridRef.current) {
75
+ const rows = gridRef.current.querySelectorAll('tbody tr');
76
+ gsap.fromTo(rows, { opacity: 0, x: -15 }, {
77
+ opacity: 1, x: 0, stagger: 0.06, duration: 0.4, ease: 'power2.out'
78
+ });
79
+ }
80
+ }, [results]);
81
+
82
+ const loadResults = async () => {
83
+ try {
84
+ const r = await fetch(`${API_BASE}/results`);
85
+ setResults(await r.json());
86
+ setActiveTab('results');
87
+ } catch (_) {}
88
+ };
89
+
90
+ const handleSubmit = async (e) => {
91
+ e.preventDefault();
92
+ if (!niche.trim() || !location.trim()) return;
93
+ setResults([]);
94
+ setJobStatus({ status: 'scraping', message: 'Initiating agent...', progress: 3 });
95
+ try {
96
+ await fetch(`${API_BASE}/scrape`, {
97
+ method: 'POST',
98
+ headers: { 'Content-Type': 'application/json' },
99
+ body: JSON.stringify({ niche, location, limit }),
100
+ });
101
+ } catch (_) {
102
+ setJobStatus({ status: 'error', message: 'Cannot reach backend on :8000', progress: 0 });
103
+ }
104
+ };
105
+
106
+ const exportCSV = () => {
107
+ const cols = ['name','website','phone','email','rating','facebook','instagram','linkedin','status'];
108
+ const lines = [cols.join(','), ...results.map(r => cols.map(k => `"${(r[k]||'').replace(/"/g,'""')}"`).join(','))];
109
+ const a = document.createElement('a');
110
+ a.href = URL.createObjectURL(new Blob([lines.join('\n')], { type: 'text/csv' }));
111
+ a.download = `leads_${niche}_${location}.csv`;
112
+ a.click();
113
+ };
114
+
115
+ const safeHost = url => { try { return new URL(url).hostname; } catch { return url; } };
116
+
117
+ const statusColor = { idle:'#4a5568', scraping:'#00f2ff', enriching:'#8b5cf6', saving:'#10b981', complete:'#22c55e', error:'#ef4444' };
118
+
119
+ /* ── Grid layout ── */
120
+ return (
121
+ <div id="root-app">
122
+ {/* Particles */}
123
+ <div className="particles">
124
+ {[...Array(18)].map((_, i) => (
125
+ <div key={i} className="particle" ref={el => particlesRef.current[i] = el}
126
+ style={{ left: `${Math.random()*100}%`, top: `${Math.random()*100}%`, width: `${2+Math.random()*3}px`, height: `${2+Math.random()*3}px`, opacity: 0.3 + Math.random() * 0.5 }} />
127
+ ))}
128
+ </div>
129
+
130
+ <div className="layout">
131
+
132
+ {/* ── HERO ── */}
133
+ <header className="hero" ref={heroRef}>
134
+ <div className="hero-icon">
135
+ <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#00f2ff" strokeWidth="1.5">
136
+ <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
137
+ </svg>
138
+ </div>
139
+ <div>
140
+ <h1>LEAD HUNTER <span className="dim">AI</span></h1>
141
+ <p className="subtitle">AGENTIC BUSINESS INTELLIGENCE ENGINE v2.0</p>
142
+ </div>
143
+ </header>
144
+
145
+ {/* ── STATUS BAR ── */}
146
+ {jobStatus.status !== 'idle' && (
147
+ <div className="status-bar">
148
+ <div className="status-dot" style={{ background: statusColor[jobStatus.status] || '#4a5568' }} />
149
+ <span className="status-msg">{jobStatus.message}</span>
150
+ <span className="status-pct">{jobStatus.progress}%</span>
151
+ </div>
152
+ )}
153
+ {jobStatus.status !== 'idle' && (
154
+ <div className="progress-track">
155
+ <div className="progress-fill" style={{ width: `${jobStatus.progress}%`, background: statusColor[jobStatus.status] || '#00f2ff' }} />
156
+ </div>
157
+ )}
158
+
159
+ {/* ── TABS ── */}
160
+ <div className="tabs">
161
+ <button className={`tab ${activeTab==='search'?'active':''}`} onClick={() => setActiveTab('search')}>⚑ Search</button>
162
+ <button className={`tab ${activeTab==='results'?'active':''}`} onClick={() => setActiveTab('results')}>
163
+ πŸ“‘ Results {results.length > 0 && <span className="badge">{results.length}</span>}
164
+ </button>
165
+ </div>
166
+
167
+ {/* ── SEARCH PANEL ── */}
168
+ {activeTab === 'search' && (
169
+ <div className="panel" ref={panelRef}>
170
+ <form onSubmit={handleSubmit} className="search-form">
171
+ <div className="field-group">
172
+ <label className="field-label">TARGET NICHE</label>
173
+ <div className="input-wrap">
174
+ <span className="input-icon">πŸ”</span>
175
+ <input className="input" placeholder="e.g. Roofers, Pool Cleaners..." value={niche} onChange={e => setNiche(e.target.value)} disabled={isRunning} required />
176
+ </div>
177
+ </div>
178
+ <div className="field-group">
179
+ <label className="field-label">TARGET LOCATION</label>
180
+ <div className="input-wrap">
181
+ <span className="input-icon">πŸ“</span>
182
+ <input className="input" placeholder="e.g. Miami, New York..." value={location} onChange={e => setLocation(e.target.value)} disabled={isRunning} required />
183
+ </div>
184
+ </div>
185
+ <div className="field-group">
186
+ <label className="field-label">LEAD COUNT</label>
187
+ <div className="input-wrap">
188
+ <span className="input-icon">#</span>
189
+ <input className="input" type="number" min="1" max="50" value={limit} onChange={e => setLimit(Number(e.target.value))} disabled={isRunning} />
190
+ </div>
191
+ </div>
192
+ <button className="btn-execute" type="submit" disabled={isRunning}>
193
+ {isRunning ? <span className="spinner">⟳</span> : '⚑'} {isRunning ? 'AGENT ACTIVE...' : 'INITIALIZE HUNT'}
194
+ </button>
195
+ </form>
196
+
197
+ <div className="info-cards">
198
+ <div className="info-card"><div className="info-num">3</div><div className="info-label">AI Agents</div></div>
199
+ <div className="info-card"><div className="info-num">∞</div><div className="info-label">Niches</div></div>
200
+ <div className="info-card"><div className="info-num">Free</div><div className="info-label">Cost</div></div>
201
+ </div>
202
+ </div>
203
+ )}
204
+
205
+ {/* ── RESULTS PANEL ── */}
206
+ {activeTab === 'results' && (
207
+ <div className="panel results-panel">
208
+ {results.length === 0 ? (
209
+ <div className="empty-state">
210
+ <p>πŸ“‘ No data yet. Run a search first.</p>
211
+ </div>
212
+ ) : (
213
+ <>
214
+ <div className="results-header">
215
+ <span className="results-count">{results.length} leads discovered</span>
216
+ <button className="btn-export" onClick={exportCSV}>⬇ Export CSV</button>
217
+ </div>
218
+ <div className="table-wrap">
219
+ <table className="results-table" ref={gridRef}>
220
+ <thead>
221
+ <tr>
222
+ <th>BUSINESS</th>
223
+ <th>CONTACT</th>
224
+ <th>RATING</th>
225
+ <th>CHANNELS</th>
226
+ <th>STATUS</th>
227
+ </tr>
228
+ </thead>
229
+ <tbody>
230
+ {results.map((row, i) => (
231
+ <tr key={i}>
232
+ <td>
233
+ <div className="biz-name">{row.name}</div>
234
+ {row.website && <a className="biz-url" href={row.website} target="_blank" rel="noreferrer">{safeHost(row.website)}</a>}
235
+ {row.phone && <div className="biz-phone">{row.phone}</div>}
236
+ </td>
237
+ <td><div className="email-cell">{row.email || <span className="dim-text">β€”</span>}</div></td>
238
+ <td><span className="rating-badge">β˜… {row.rating || 'β€”'}</span></td>
239
+ <td>
240
+ <div className="channels">
241
+ {row.facebook && <a href={row.facebook} target="_blank" rel="noreferrer" className="ch-btn">fb</a>}
242
+ {row.instagram && <a href={row.instagram} target="_blank" rel="noreferrer" className="ch-btn">ig</a>}
243
+ {row.linkedin && <a href={row.linkedin} target="_blank" rel="noreferrer" className="ch-btn">in</a>}
244
+ </div>
245
+ </td>
246
+ <td><span className={`status-chip ${row.status === 'Success' ? 'chip-ok' : 'chip-warn'}`}>{row.status || 'β€”'}</span></td>
247
+ </tr>
248
+ ))}
249
+ </tbody>
250
+ </table>
251
+ </div>
252
+ </>
253
+ )}
254
+ </div>
255
+ )}
256
+
257
+ <footer className="footer">LEAD HUNTER AI Β· Running on localhost Β· $0 Budget</footer>
258
+ </div>
259
+ </div>
260
+ );
261
+ }