Seth commited on
Commit
ab420b2
·
1 Parent(s): 323544d
backend/app/__pycache__/database.cpython-314.pyc ADDED
Binary file (4.89 kB). View file
 
backend/app/__pycache__/main.cpython-314.pyc ADDED
Binary file (37.4 kB). View file
 
backend/app/database.py CHANGED
@@ -54,6 +54,22 @@ class GeneratedSequence(Base):
54
  created_at = Column(DateTime, default=datetime.utcnow)
55
 
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  class SmartleadRun(Base):
58
  __tablename__ = "smartlead_runs"
59
 
 
54
  created_at = Column(DateTime, default=datetime.utcnow)
55
 
56
 
57
+ class Contact(Base):
58
+ __tablename__ = "contacts"
59
+
60
+ id = Column(Integer, primary_key=True, index=True)
61
+ file_id = Column(String, index=True)
62
+ row_index = Column(Integer)
63
+ first_name = Column(String)
64
+ last_name = Column(String)
65
+ email = Column(String, index=True)
66
+ company = Column(String)
67
+ title = Column(String)
68
+ source = Column(String, default="apollo_csv")
69
+ raw_data = Column(JSON)
70
+ created_at = Column(DateTime, default=datetime.utcnow)
71
+
72
+
73
  class SmartleadRun(Base):
74
  __tablename__ = "smartlead_runs"
75
 
backend/app/main.py CHANGED
@@ -16,7 +16,7 @@ import asyncio
16
  import math
17
  from datetime import datetime
18
 
19
- from .database import get_db, UploadedFile, Prompt, GeneratedSequence, SmartleadRun
20
  from .models import UploadResponse, PromptSaveRequest, SequenceResponse, SmartleadPushRequest, SmartleadRunResponse
21
  from .gpt_service import generate_email_sequence
22
  from .smartlead_client import SmartleadClient
@@ -36,6 +36,24 @@ UPLOAD_DIR = Path("/data/uploads")
36
  UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
37
 
38
  # ---- API ----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  @app.get("/api/health")
40
  def health():
41
  return {"status": "ok"}
@@ -62,7 +80,7 @@ async def upload_csv(file: UploadFile = File(...), db: Session = Depends(get_db)
62
  df = pd.read_csv(file_path)
63
  contact_count = len(df)
64
 
65
- # Save to database
66
  db_file = UploadedFile(
67
  file_id=file_id,
68
  filename=file.filename,
@@ -70,6 +88,24 @@ async def upload_csv(file: UploadFile = File(...), db: Session = Depends(get_db)
70
  file_path=str(file_path)
71
  )
72
  db.add(db_file)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  db.commit()
74
 
75
  return {
@@ -81,6 +117,107 @@ async def upload_csv(file: UploadFile = File(...), db: Session = Depends(get_db)
81
  raise HTTPException(status_code=500, detail=f"Error uploading file: {str(e)}")
82
 
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  @app.post("/api/save-prompts")
85
  async def save_prompts(request: PromptSaveRequest, db: Session = Depends(get_db)):
86
  """Save prompt templates for products"""
 
16
  import math
17
  from datetime import datetime
18
 
19
+ from .database import get_db, UploadedFile, Prompt, GeneratedSequence, SmartleadRun, Contact
20
  from .models import UploadResponse, PromptSaveRequest, SequenceResponse, SmartleadPushRequest, SmartleadRunResponse
21
  from .gpt_service import generate_email_sequence
22
  from .smartlead_client import SmartleadClient
 
36
  UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
37
 
38
  # ---- API ----
39
+ def _safe_str(value):
40
+ if value is None:
41
+ return ""
42
+ if isinstance(value, float):
43
+ if math.isnan(value):
44
+ return ""
45
+ return str(value).strip()
46
+ return str(value).strip()
47
+
48
+
49
+ def _pick_field(row_dict: Dict, aliases: List[str]) -> str:
50
+ lowered = {str(k).strip().lower(): v for k, v in row_dict.items()}
51
+ for alias in aliases:
52
+ if alias in lowered:
53
+ return _safe_str(lowered[alias])
54
+ return ""
55
+
56
+
57
  @app.get("/api/health")
58
  def health():
59
  return {"status": "ok"}
 
80
  df = pd.read_csv(file_path)
81
  contact_count = len(df)
82
 
83
+ # Save upload metadata
84
  db_file = UploadedFile(
85
  file_id=file_id,
86
  filename=file.filename,
 
88
  file_path=str(file_path)
89
  )
90
  db.add(db_file)
91
+
92
+ # Persist uploaded contacts for CRM/contacts views
93
+ for idx, row in df.iterrows():
94
+ row_dict = row.to_dict()
95
+ sanitized_raw_data = {str(k): _safe_str(v) for k, v in row_dict.items()}
96
+ contact = Contact(
97
+ file_id=file_id,
98
+ row_index=idx + 1,
99
+ first_name=_pick_field(row_dict, ["first name", "firstname", "first_name"]),
100
+ last_name=_pick_field(row_dict, ["last name", "lastname", "last_name"]),
101
+ email=_pick_field(row_dict, ["email", "work email", "email address"]),
102
+ company=_pick_field(row_dict, ["company", "organization name", "account name"]),
103
+ title=_pick_field(row_dict, ["title", "job title"]),
104
+ source="apollo_csv",
105
+ raw_data=sanitized_raw_data,
106
+ )
107
+ db.add(contact)
108
+
109
  db.commit()
110
 
111
  return {
 
117
  raise HTTPException(status_code=500, detail=f"Error uploading file: {str(e)}")
118
 
119
 
120
+ @app.get("/api/contacts")
121
+ async def list_contacts(
122
+ search: str = Query("", description="Search by name/email/company/title"),
123
+ limit: int = Query(200, ge=1, le=1000),
124
+ offset: int = Query(0, ge=0),
125
+ db: Session = Depends(get_db)
126
+ ):
127
+ query = db.query(Contact)
128
+ if search:
129
+ pattern = f"%{search}%"
130
+ query = query.filter(
131
+ (Contact.first_name.ilike(pattern)) |
132
+ (Contact.last_name.ilike(pattern)) |
133
+ (Contact.email.ilike(pattern)) |
134
+ (Contact.company.ilike(pattern)) |
135
+ (Contact.title.ilike(pattern))
136
+ )
137
+ total = query.count()
138
+ contacts = (
139
+ query.order_by(Contact.created_at.desc(), Contact.id.desc())
140
+ .offset(offset)
141
+ .limit(limit)
142
+ .all()
143
+ )
144
+ return {
145
+ "total": total,
146
+ "contacts": [
147
+ {
148
+ "id": c.id,
149
+ "file_id": c.file_id,
150
+ "row_index": c.row_index,
151
+ "first_name": c.first_name or "",
152
+ "last_name": c.last_name or "",
153
+ "email": c.email or "",
154
+ "company": c.company or "",
155
+ "title": c.title or "",
156
+ "source": c.source or "apollo_csv",
157
+ "created_at": c.created_at.isoformat() if c.created_at else None,
158
+ }
159
+ for c in contacts
160
+ ],
161
+ }
162
+
163
+
164
+ @app.get("/api/contacts/{contact_id}")
165
+ async def get_contact(contact_id: int, db: Session = Depends(get_db)):
166
+ contact = db.query(Contact).filter(Contact.id == contact_id).first()
167
+ if not contact:
168
+ raise HTTPException(status_code=404, detail="Contact not found")
169
+ return {
170
+ "id": contact.id,
171
+ "file_id": contact.file_id,
172
+ "row_index": contact.row_index,
173
+ "first_name": contact.first_name or "",
174
+ "last_name": contact.last_name or "",
175
+ "email": contact.email or "",
176
+ "company": contact.company or "",
177
+ "title": contact.title or "",
178
+ "source": contact.source or "apollo_csv",
179
+ "created_at": contact.created_at.isoformat() if contact.created_at else None,
180
+ "raw_data": contact.raw_data or {},
181
+ }
182
+
183
+
184
+ @app.get("/api/contacts/{contact_id}/sequences")
185
+ async def get_contact_sequences(contact_id: int, db: Session = Depends(get_db)):
186
+ contact = db.query(Contact).filter(Contact.id == contact_id).first()
187
+ if not contact:
188
+ raise HTTPException(status_code=404, detail="Contact not found")
189
+
190
+ if not contact.email:
191
+ return {"contact_id": contact_id, "sequences": []}
192
+
193
+ sequences = (
194
+ db.query(GeneratedSequence)
195
+ .filter(
196
+ GeneratedSequence.file_id == contact.file_id,
197
+ GeneratedSequence.email == contact.email
198
+ )
199
+ .order_by(GeneratedSequence.sequence_id, GeneratedSequence.email_number)
200
+ .all()
201
+ )
202
+
203
+ return {
204
+ "contact_id": contact_id,
205
+ "sequences": [
206
+ {
207
+ "id": s.id,
208
+ "file_id": s.file_id,
209
+ "sequence_id": s.sequence_id,
210
+ "email_number": s.email_number,
211
+ "subject": s.subject or "",
212
+ "email_content": s.email_content or "",
213
+ "product": s.product or "",
214
+ "created_at": s.created_at.isoformat() if s.created_at else None,
215
+ }
216
+ for s in sequences
217
+ ],
218
+ }
219
+
220
+
221
  @app.post("/api/save-prompts")
222
  async def save_prompts(request: PromptSaveRequest, db: Session = Depends(get_db)):
223
  """Save prompt templates for products"""
frontend/src/App.jsx CHANGED
@@ -2,6 +2,7 @@ import React from "react";
2
  import { BrowserRouter, Routes, Route } from "react-router-dom";
3
  import EmailSequenceGenerator from "./pages/EmailSequenceGenerator";
4
  import RunHistory from "./pages/RunHistory";
 
5
  import "./index.css";
6
 
7
  export default function App() {
@@ -9,6 +10,7 @@ export default function App() {
9
  <BrowserRouter>
10
  <Routes>
11
  <Route path="/" element={<EmailSequenceGenerator />} />
 
12
  <Route path="/history" element={<RunHistory />} />
13
  </Routes>
14
  </BrowserRouter>
 
2
  import { BrowserRouter, Routes, Route } from "react-router-dom";
3
  import EmailSequenceGenerator from "./pages/EmailSequenceGenerator";
4
  import RunHistory from "./pages/RunHistory";
5
+ import Contacts from "./pages/Contacts";
6
  import "./index.css";
7
 
8
  export default function App() {
 
10
  <BrowserRouter>
11
  <Routes>
12
  <Route path="/" element={<EmailSequenceGenerator />} />
13
+ <Route path="/contacts" element={<Contacts />} />
14
  <Route path="/history" element={<RunHistory />} />
15
  </Routes>
16
  </BrowserRouter>
frontend/src/components/layout/AppHeader.jsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Link, useLocation } from 'react-router-dom';
3
+ import { Zap } from 'lucide-react';
4
+ import { Button } from "@/components/ui/button";
5
+
6
+ const MENU_ITEMS = [
7
+ { label: 'Generator', href: '/' },
8
+ { label: 'Contacts', href: '/contacts' },
9
+ { label: 'Run History', href: '/history' },
10
+ ];
11
+
12
+ export default function AppHeader({ rightContent }) {
13
+ const location = useLocation();
14
+
15
+ return (
16
+ <header className="border-b border-slate-100 bg-white/80 backdrop-blur-sm sticky top-0 z-50">
17
+ <div className="max-w-6xl mx-auto px-6 py-4">
18
+ <div className="flex items-center justify-between gap-4">
19
+ <div className="flex items-center gap-6">
20
+ <div className="flex items-center gap-3">
21
+ <div className="h-10 w-10 rounded-xl bg-gradient-to-br from-violet-600 to-purple-600 flex items-center justify-center shadow-lg shadow-violet-200">
22
+ <Zap className="h-5 w-5 text-white" />
23
+ </div>
24
+ <div>
25
+ <h1 className="font-bold text-slate-800 text-lg">SequenceAI</h1>
26
+ <p className="text-xs text-slate-500">Personalized Email Outreach</p>
27
+ </div>
28
+ </div>
29
+ <nav className="hidden md:flex items-center gap-2">
30
+ {MENU_ITEMS.map((item) => {
31
+ const isActive = location.pathname === item.href;
32
+ return (
33
+ <Button
34
+ key={item.href}
35
+ asChild
36
+ variant={isActive ? "default" : "ghost"}
37
+ size="sm"
38
+ className={isActive ? "bg-violet-600 hover:bg-violet-700" : "text-slate-600"}
39
+ >
40
+ <Link to={item.href}>{item.label}</Link>
41
+ </Button>
42
+ );
43
+ })}
44
+ </nav>
45
+ </div>
46
+ <div className="flex items-center gap-2">
47
+ {rightContent}
48
+ </div>
49
+ </div>
50
+ <nav className="md:hidden flex items-center gap-2 mt-3">
51
+ {MENU_ITEMS.map((item) => {
52
+ const isActive = location.pathname === item.href;
53
+ return (
54
+ <Button
55
+ key={item.href}
56
+ asChild
57
+ variant={isActive ? "default" : "outline"}
58
+ size="sm"
59
+ className={isActive ? "bg-violet-600 hover:bg-violet-700" : ""}
60
+ >
61
+ <Link to={item.href}>{item.label}</Link>
62
+ </Button>
63
+ );
64
+ })}
65
+ </nav>
66
+ </div>
67
+ </header>
68
+ );
69
+ }
frontend/src/pages/Contacts.jsx ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { Users, Search, Mail, Building2, Briefcase, FileText } from 'lucide-react';
3
+ import { Input } from "@/components/ui/input";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import AppHeader from '@/components/layout/AppHeader';
7
+
8
+ export default function Contacts() {
9
+ const [contacts, setContacts] = useState([]);
10
+ const [selectedContact, setSelectedContact] = useState(null);
11
+ const [sequences, setSequences] = useState([]);
12
+ const [loading, setLoading] = useState(true);
13
+ const [seqLoading, setSeqLoading] = useState(false);
14
+ const [searchQuery, setSearchQuery] = useState('');
15
+
16
+ useEffect(() => {
17
+ fetchContacts();
18
+ }, []);
19
+
20
+ const fetchContacts = async () => {
21
+ setLoading(true);
22
+ try {
23
+ const res = await fetch('/api/contacts?limit=1000');
24
+ if (res.ok) {
25
+ const data = await res.json();
26
+ setContacts(data.contacts || []);
27
+ }
28
+ } catch (e) {
29
+ console.error('Failed to fetch contacts:', e);
30
+ } finally {
31
+ setLoading(false);
32
+ }
33
+ };
34
+
35
+ const openContact = async (contact) => {
36
+ setSelectedContact(contact);
37
+ setSeqLoading(true);
38
+ try {
39
+ const res = await fetch(`/api/contacts/${contact.id}/sequences`);
40
+ if (res.ok) {
41
+ const data = await res.json();
42
+ setSequences(data.sequences || []);
43
+ } else {
44
+ setSequences([]);
45
+ }
46
+ } catch (e) {
47
+ console.error('Failed to fetch contact sequences:', e);
48
+ setSequences([]);
49
+ } finally {
50
+ setSeqLoading(false);
51
+ }
52
+ };
53
+
54
+ const filteredContacts = useMemo(() => {
55
+ const q = searchQuery.trim().toLowerCase();
56
+ if (!q) return contacts;
57
+ return contacts.filter((c) =>
58
+ `${c.first_name} ${c.last_name}`.toLowerCase().includes(q) ||
59
+ (c.email || '').toLowerCase().includes(q) ||
60
+ (c.company || '').toLowerCase().includes(q) ||
61
+ (c.title || '').toLowerCase().includes(q)
62
+ );
63
+ }, [contacts, searchQuery]);
64
+
65
+ return (
66
+ <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-violet-50">
67
+ <AppHeader />
68
+ <main className="max-w-6xl mx-auto px-6 py-8">
69
+ <div className="mb-8">
70
+ <h2 className="text-2xl font-bold text-slate-800 mb-2">Contacts</h2>
71
+ <p className="text-slate-500">Apollo CSV contacts stored in your local database.</p>
72
+ </div>
73
+
74
+ <div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
75
+ <section className="lg:col-span-2 rounded-2xl border border-slate-200 bg-white p-4">
76
+ <div className="relative mb-4">
77
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
78
+ <Input
79
+ placeholder="Search contacts..."
80
+ value={searchQuery}
81
+ onChange={(e) => setSearchQuery(e.target.value)}
82
+ className="pl-10"
83
+ />
84
+ </div>
85
+ <div className="max-h-[70vh] overflow-y-auto space-y-2 pr-1">
86
+ {loading ? (
87
+ <p className="text-sm text-slate-500 p-3">Loading contacts...</p>
88
+ ) : filteredContacts.length === 0 ? (
89
+ <div className="text-center py-12 text-slate-400">
90
+ <Users className="h-8 w-8 mx-auto mb-2" />
91
+ <p>No contacts found</p>
92
+ </div>
93
+ ) : (
94
+ filteredContacts.map((contact) => {
95
+ const active = selectedContact?.id === contact.id;
96
+ return (
97
+ <button
98
+ key={contact.id}
99
+ onClick={() => openContact(contact)}
100
+ className={`w-full text-left rounded-xl border p-3 transition ${
101
+ active
102
+ ? 'border-violet-300 bg-violet-50'
103
+ : 'border-slate-200 bg-white hover:bg-slate-50'
104
+ }`}
105
+ >
106
+ <div className="font-medium text-slate-800">
107
+ {contact.first_name} {contact.last_name}
108
+ </div>
109
+ <div className="text-xs text-slate-500 truncate">{contact.email || 'No email'}</div>
110
+ <div className="text-xs text-slate-400 truncate">{contact.company || 'No company'}</div>
111
+ </button>
112
+ );
113
+ })
114
+ )}
115
+ </div>
116
+ </section>
117
+
118
+ <section className="lg:col-span-3 rounded-2xl border border-slate-200 bg-white p-6">
119
+ {!selectedContact ? (
120
+ <div className="h-full min-h-[300px] flex items-center justify-center text-slate-400">
121
+ <div className="text-center">
122
+ <FileText className="h-10 w-10 mx-auto mb-2" />
123
+ <p>Select a contact to view generated sequences</p>
124
+ </div>
125
+ </div>
126
+ ) : (
127
+ <div>
128
+ <div className="mb-6">
129
+ <h3 className="text-xl font-semibold text-slate-800">
130
+ {selectedContact.first_name} {selectedContact.last_name}
131
+ </h3>
132
+ <div className="mt-2 flex flex-wrap gap-2 text-xs text-slate-600">
133
+ <Badge variant="outline" className="gap-1"><Mail className="h-3 w-3" /> {selectedContact.email || 'N/A'}</Badge>
134
+ <Badge variant="outline" className="gap-1"><Building2 className="h-3 w-3" /> {selectedContact.company || 'N/A'}</Badge>
135
+ <Badge variant="outline" className="gap-1"><Briefcase className="h-3 w-3" /> {selectedContact.title || 'N/A'}</Badge>
136
+ </div>
137
+ </div>
138
+
139
+ {seqLoading ? (
140
+ <p className="text-sm text-slate-500">Loading sequences...</p>
141
+ ) : sequences.length === 0 ? (
142
+ <p className="text-sm text-slate-500">No generated sequences found for this contact yet.</p>
143
+ ) : (
144
+ <div className="space-y-3 max-h-[60vh] overflow-y-auto pr-1">
145
+ {sequences.map((seq) => (
146
+ <div key={seq.id} className="rounded-xl border border-slate-200 p-4 bg-slate-50/50">
147
+ <div className="flex items-center justify-between mb-2">
148
+ <div className="font-medium text-slate-800">
149
+ Email {seq.email_number} {seq.product ? `• ${seq.product}` : ''}
150
+ </div>
151
+ </div>
152
+ <div className="text-sm mb-2">
153
+ <span className="font-semibold text-slate-700">Subject: </span>
154
+ <span className="text-slate-700">{seq.subject}</span>
155
+ </div>
156
+ <div className="rounded-lg border border-slate-200 bg-white p-3 text-sm text-slate-700 whitespace-pre-wrap">
157
+ {seq.email_content}
158
+ </div>
159
+ </div>
160
+ ))}
161
+ </div>
162
+ )}
163
+ </div>
164
+ )}
165
+ </section>
166
+ </div>
167
+ </main>
168
+ </div>
169
+ );
170
+ }
frontend/src/pages/EmailSequenceGenerator.jsx CHANGED
@@ -1,5 +1,5 @@
1
  import React, { useState, useRef } from 'react';
2
- import { Sparkles, ArrowRight, ArrowLeft, Mail, Zap } from 'lucide-react';
3
  import { Button } from "@/components/ui/button";
4
  import { motion, AnimatePresence } from 'framer-motion';
5
 
@@ -7,6 +7,7 @@ import UploadStep from '@/components/upload/UploadStep';
7
  import ProductSelector from '@/components/products/ProductSelector';
8
  import PromptEditor from '@/components/prompts/PromptEditor';
9
  import SequenceViewer from '@/components/sequences/SequenceViewer';
 
10
 
11
  export default function EmailSequenceGenerator() {
12
  const [step, setStep] = useState(1);
@@ -78,32 +79,19 @@ export default function EmailSequenceGenerator() {
78
 
79
  return (
80
  <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-violet-50">
81
- {/* Header */}
82
- <header className="border-b border-slate-100 bg-white/80 backdrop-blur-sm sticky top-0 z-50">
83
- <div className="max-w-6xl mx-auto px-6 py-4">
84
- <div className="flex items-center justify-between">
85
- <div className="flex items-center gap-3">
86
- <div className="h-10 w-10 rounded-xl bg-gradient-to-br from-violet-600 to-purple-600
87
- flex items-center justify-center shadow-lg shadow-violet-200">
88
- <Zap className="h-5 w-5 text-white" />
89
- </div>
90
- <div>
91
- <h1 className="font-bold text-slate-800 text-lg">SequenceAI</h1>
92
- <p className="text-xs text-slate-500">Personalized Email Outreach</p>
93
- </div>
94
- </div>
95
- {step > 1 && (
96
- <Button
97
- variant="ghost"
98
- onClick={handleReset}
99
- className="text-slate-500 hover:text-slate-700"
100
- >
101
- Start Over
102
- </Button>
103
- )}
104
- </div>
105
- </div>
106
- </header>
107
 
108
  <main className="max-w-6xl mx-auto px-6 py-8">
109
  {/* Progress Steps */}
 
1
  import React, { useState, useRef } from 'react';
2
+ import { Sparkles, ArrowRight, ArrowLeft } from 'lucide-react';
3
  import { Button } from "@/components/ui/button";
4
  import { motion, AnimatePresence } from 'framer-motion';
5
 
 
7
  import ProductSelector from '@/components/products/ProductSelector';
8
  import PromptEditor from '@/components/prompts/PromptEditor';
9
  import SequenceViewer from '@/components/sequences/SequenceViewer';
10
+ import AppHeader from '@/components/layout/AppHeader';
11
 
12
  export default function EmailSequenceGenerator() {
13
  const [step, setStep] = useState(1);
 
79
 
80
  return (
81
  <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-violet-50">
82
+ <AppHeader
83
+ rightContent={
84
+ step > 1 ? (
85
+ <Button
86
+ variant="ghost"
87
+ onClick={handleReset}
88
+ className="text-slate-500 hover:text-slate-700"
89
+ >
90
+ Start Over
91
+ </Button>
92
+ ) : null
93
+ }
94
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  <main className="max-w-6xl mx-auto px-6 py-8">
97
  {/* Progress Steps */}
frontend/src/pages/RunHistory.jsx CHANGED
@@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input";
5
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
6
  import { Badge } from "@/components/ui/badge";
7
  import { motion } from 'framer-motion';
 
8
 
9
  export default function RunHistory() {
10
  const [runs, setRuns] = useState([]);
@@ -68,30 +69,7 @@ export default function RunHistory() {
68
 
69
  return (
70
  <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-violet-50">
71
- {/* Header */}
72
- <header className="border-b border-slate-100 bg-white/80 backdrop-blur-sm sticky top-0 z-50">
73
- <div className="max-w-6xl mx-auto px-6 py-4">
74
- <div className="flex items-center justify-between">
75
- <div className="flex items-center gap-3">
76
- <div className="h-10 w-10 rounded-xl bg-gradient-to-br from-violet-600 to-purple-600
77
- flex items-center justify-center shadow-lg shadow-violet-200">
78
- <History className="h-5 w-5 text-white" />
79
- </div>
80
- <div>
81
- <h1 className="font-bold text-slate-800 text-lg">Run History</h1>
82
- <p className="text-xs text-slate-500">Smartlead campaign push history</p>
83
- </div>
84
- </div>
85
- <Button
86
- variant="outline"
87
- onClick={() => window.location.href = '/'}
88
- className="text-slate-500 hover:text-slate-700"
89
- >
90
- Back to Generator
91
- </Button>
92
- </div>
93
- </div>
94
- </header>
95
 
96
  <main className="max-w-6xl mx-auto px-6 py-8">
97
  {/* Filters */}
 
5
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
6
  import { Badge } from "@/components/ui/badge";
7
  import { motion } from 'framer-motion';
8
+ import AppHeader from '@/components/layout/AppHeader';
9
 
10
  export default function RunHistory() {
11
  const [runs, setRuns] = useState([]);
 
69
 
70
  return (
71
  <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-violet-50">
72
+ <AppHeader />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  <main className="max-w-6xl mx-auto px-6 py-8">
75
  {/* Filters */}