Seth commited on
Commit
952e292
·
1 Parent(s): 7a74d14
backend/app/main.py CHANGED
@@ -103,25 +103,70 @@ async def save_prompts(request: PromptSaveRequest, db: Session = Depends(get_db)
103
  raise HTTPException(status_code=500, detail=f"Error saving prompts: {str(e)}")
104
 
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  @app.get("/api/generate-sequences")
107
  async def generate_sequences(
108
  file_id: str = Query(...),
109
- db: Session = Depends(get_db)
 
110
  ):
111
- """Generate email sequences using GPT with Server-Sent Events streaming"""
 
112
 
113
  async def event_generator():
114
  try:
115
- # Get uploaded file
116
  db_file = db.query(UploadedFile).filter(UploadedFile.file_id == file_id).first()
117
  if not db_file:
118
  yield f"data: {json.dumps({'type': 'error', 'error': 'File not found'})}\n\n"
119
  return
120
 
121
- # Read CSV
122
  df = pd.read_csv(db_file.file_path)
123
-
124
- # Get prompts for this file
125
  prompts = db.query(Prompt).filter(Prompt.file_id == file_id).all()
126
  prompt_dict = {p.product_name: p.prompt_template for p in prompts}
127
 
@@ -129,37 +174,59 @@ async def generate_sequences(
129
  yield f"data: {json.dumps({'type': 'error', 'error': 'No prompts found'})}\n\n"
130
  return
131
 
132
- # Get products from prompts
133
  products = list(prompt_dict.keys())
 
 
 
134
 
135
- # Clear existing sequences
136
- db.query(GeneratedSequence).filter(GeneratedSequence.file_id == file_id).delete()
137
- db.commit()
138
-
139
- sequence_id = 1
140
  total_contacts = len(df)
 
141
 
142
- # Process each contact
143
  for idx, row in df.iterrows():
144
- # Convert row to dict
145
- contact = row.to_dict()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
- # Select product (round-robin or random)
148
  product_name = products[sequence_id % len(products)]
149
  prompt_template = prompt_dict[product_name]
150
 
151
- # Generate sequence using GPT (run in executor to avoid blocking)
152
  loop = asyncio.get_event_loop()
153
  with concurrent.futures.ThreadPoolExecutor() as executor:
154
  sequence_data_list = await loop.run_in_executor(
155
- executor,
156
- generate_email_sequence,
157
- contact,
158
- prompt_template,
159
- product_name
160
  )
161
 
162
- # Save all emails for this contact to database
163
  for seq_data in sequence_data_list:
164
  db_sequence = GeneratedSequence(
165
  file_id=file_id,
@@ -172,13 +239,12 @@ async def generate_sequences(
172
  title=seq_data.get("title", ""),
173
  product=seq_data["product"],
174
  subject=seq_data["subject"],
175
- email_content=seq_data["email_content"]
176
  )
177
  db.add(db_sequence)
178
 
179
  db.commit()
180
 
181
- # Stream all emails for this contact
182
  for seq_data in sequence_data_list:
183
  sequence_response = {
184
  "id": sequence_id,
@@ -190,21 +256,15 @@ async def generate_sequences(
190
  "title": seq_data.get("title", ""),
191
  "product": seq_data["product"],
192
  "subject": seq_data["subject"],
193
- "emailContent": seq_data["email_content"]
194
  }
195
-
196
  yield f"data: {json.dumps({'type': 'sequence', 'sequence': sequence_response})}\n\n"
197
 
198
- # Send progress update
199
  progress = min(100, max(0, (sequence_id / total_contacts) * 100)) if total_contacts > 0 else 0
200
  yield f"data: {json.dumps({'type': 'progress', 'progress': float(progress)})}\n\n"
201
-
202
  sequence_id += 1
203
-
204
- # Small delay to prevent overwhelming the API
205
  await asyncio.sleep(0.1)
206
 
207
- # Send completion
208
  yield f"data: {json.dumps({'type': 'complete'})}\n\n"
209
 
210
  except Exception as e:
@@ -252,10 +312,10 @@ async def download_sequences(file_id: str = Query(...), db: Session = Depends(ge
252
  output = io.StringIO()
253
  writer = csv.writer(output, quoting=csv.QUOTE_MINIMAL)
254
 
255
- # Write header - only include columns up to the maximum email number found
256
  header = ['First Name', 'Last Name', 'Email', 'Company', 'Title', 'Product']
257
  for i in range(1, max_email_number + 1):
258
- header.extend([f'Subject {i}', f'Body {i}'])
259
  writer.writerow(header)
260
 
261
  # Write rows (one row per contact with subjects/bodies up to max_email_number)
@@ -287,12 +347,12 @@ async def download_sequences(file_id: str = Query(...), db: Session = Depends(ge
287
 
288
  output.seek(0)
289
 
290
- # Return as downloadable file
291
  return StreamingResponse(
292
  iter([output.getvalue()]),
293
  media_type="text/csv",
294
  headers={
295
- "Content-Disposition": f"attachment; filename=email_sequences_{file_id}.csv"
296
  }
297
  )
298
 
 
103
  raise HTTPException(status_code=500, detail=f"Error saving prompts: {str(e)}")
104
 
105
 
106
+ @app.get("/api/generation-status")
107
+ async def generation_status(file_id: str = Query(...), db: Session = Depends(get_db)):
108
+ """Return progress for sequence generation (so frontend can resume after sleep/reconnect)."""
109
+ db_file = db.query(UploadedFile).filter(UploadedFile.file_id == file_id).first()
110
+ if not db_file:
111
+ raise HTTPException(status_code=404, detail="File not found")
112
+ total_contacts = db_file.contact_count or 0
113
+ completed = (
114
+ db.query(GeneratedSequence.sequence_id)
115
+ .filter(GeneratedSequence.file_id == file_id)
116
+ .distinct()
117
+ .count()
118
+ )
119
+ return {
120
+ "file_id": file_id,
121
+ "total_contacts": total_contacts,
122
+ "completed_count": completed,
123
+ "is_complete": total_contacts > 0 and completed >= total_contacts,
124
+ }
125
+
126
+
127
+ @app.get("/api/sequences")
128
+ async def get_sequences(file_id: str = Query(...), db: Session = Depends(get_db)):
129
+ """Return all generated sequences for a file (for catch-up after reconnect)."""
130
+ sequences = (
131
+ db.query(GeneratedSequence)
132
+ .filter(GeneratedSequence.file_id == file_id)
133
+ .order_by(GeneratedSequence.sequence_id, GeneratedSequence.email_number)
134
+ .all()
135
+ )
136
+ out = []
137
+ for seq in sequences:
138
+ out.append({
139
+ "id": seq.sequence_id,
140
+ "emailNumber": seq.email_number,
141
+ "firstName": seq.first_name,
142
+ "lastName": seq.last_name,
143
+ "email": seq.email,
144
+ "company": seq.company,
145
+ "title": seq.title or "",
146
+ "product": seq.product,
147
+ "subject": seq.subject,
148
+ "emailContent": seq.email_content,
149
+ })
150
+ return {"sequences": out}
151
+
152
+
153
  @app.get("/api/generate-sequences")
154
  async def generate_sequences(
155
  file_id: str = Query(...),
156
+ reset: bool = Query(True),
157
+ db: Session = Depends(get_db),
158
  ):
159
+ """Generate email sequences using GPT with Server-Sent Events streaming.
160
+ Use reset=1 for a fresh run (clears existing). Use reset=0 to resume after disconnect/sleep."""
161
 
162
  async def event_generator():
163
  try:
 
164
  db_file = db.query(UploadedFile).filter(UploadedFile.file_id == file_id).first()
165
  if not db_file:
166
  yield f"data: {json.dumps({'type': 'error', 'error': 'File not found'})}\n\n"
167
  return
168
 
 
169
  df = pd.read_csv(db_file.file_path)
 
 
170
  prompts = db.query(Prompt).filter(Prompt.file_id == file_id).all()
171
  prompt_dict = {p.product_name: p.prompt_template for p in prompts}
172
 
 
174
  yield f"data: {json.dumps({'type': 'error', 'error': 'No prompts found'})}\n\n"
175
  return
176
 
 
177
  products = list(prompt_dict.keys())
178
+ if reset:
179
+ db.query(GeneratedSequence).filter(GeneratedSequence.file_id == file_id).delete()
180
+ db.commit()
181
 
 
 
 
 
 
182
  total_contacts = len(df)
183
+ sequence_id = 1
184
 
 
185
  for idx, row in df.iterrows():
186
+ existing = (
187
+ db.query(GeneratedSequence)
188
+ .filter(
189
+ GeneratedSequence.file_id == file_id,
190
+ GeneratedSequence.sequence_id == sequence_id,
191
+ )
192
+ .order_by(GeneratedSequence.email_number)
193
+ .all()
194
+ )
195
+ if existing:
196
+ for seq in existing:
197
+ sequence_response = {
198
+ "id": seq.sequence_id,
199
+ "emailNumber": seq.email_number,
200
+ "firstName": seq.first_name,
201
+ "lastName": seq.last_name,
202
+ "email": seq.email,
203
+ "company": seq.company,
204
+ "title": seq.title or "",
205
+ "product": seq.product,
206
+ "subject": seq.subject,
207
+ "emailContent": seq.email_content,
208
+ }
209
+ yield f"data: {json.dumps({'type': 'sequence', 'sequence': sequence_response})}\n\n"
210
+ progress = min(100, max(0, (sequence_id / total_contacts) * 100)) if total_contacts > 0 else 0
211
+ yield f"data: {json.dumps({'type': 'progress', 'progress': float(progress)})}\n\n"
212
+ sequence_id += 1
213
+ await asyncio.sleep(0.05)
214
+ continue
215
 
216
+ contact = row.to_dict()
217
  product_name = products[sequence_id % len(products)]
218
  prompt_template = prompt_dict[product_name]
219
 
 
220
  loop = asyncio.get_event_loop()
221
  with concurrent.futures.ThreadPoolExecutor() as executor:
222
  sequence_data_list = await loop.run_in_executor(
223
+ executor,
224
+ generate_email_sequence,
225
+ contact,
226
+ prompt_template,
227
+ product_name,
228
  )
229
 
 
230
  for seq_data in sequence_data_list:
231
  db_sequence = GeneratedSequence(
232
  file_id=file_id,
 
239
  title=seq_data.get("title", ""),
240
  product=seq_data["product"],
241
  subject=seq_data["subject"],
242
+ email_content=seq_data["email_content"],
243
  )
244
  db.add(db_sequence)
245
 
246
  db.commit()
247
 
 
248
  for seq_data in sequence_data_list:
249
  sequence_response = {
250
  "id": sequence_id,
 
256
  "title": seq_data.get("title", ""),
257
  "product": seq_data["product"],
258
  "subject": seq_data["subject"],
259
+ "emailContent": seq_data["email_content"],
260
  }
 
261
  yield f"data: {json.dumps({'type': 'sequence', 'sequence': sequence_response})}\n\n"
262
 
 
263
  progress = min(100, max(0, (sequence_id / total_contacts) * 100)) if total_contacts > 0 else 0
264
  yield f"data: {json.dumps({'type': 'progress', 'progress': float(progress)})}\n\n"
 
265
  sequence_id += 1
 
 
266
  await asyncio.sleep(0.1)
267
 
 
268
  yield f"data: {json.dumps({'type': 'complete'})}\n\n"
269
 
270
  except Exception as e:
 
312
  output = io.StringIO()
313
  writer = csv.writer(output, quoting=csv.QUOTE_MINIMAL)
314
 
315
+ # Write header fixed format: subject1, body1, subject2, body2, ... (separate columns, not merged)
316
  header = ['First Name', 'Last Name', 'Email', 'Company', 'Title', 'Product']
317
  for i in range(1, max_email_number + 1):
318
+ header.extend([f'subject{i}', f'body{i}'])
319
  writer.writerow(header)
320
 
321
  # Write rows (one row per contact with subjects/bodies up to max_email_number)
 
347
 
348
  output.seek(0)
349
 
350
+ # Return as downloadable file (fixed format: one row per contact, separate subject/body columns)
351
  return StreamingResponse(
352
  iter([output.getvalue()]),
353
  media_type="text/csv",
354
  headers={
355
+ "Content-Disposition": "attachment; filename=email_sequences_fixed.csv"
356
  }
357
  )
358
 
frontend/src/components/sequences/SequenceViewer.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react';
2
  import { Download, Mail, Loader2, CheckCircle2, Search, Filter } from 'lucide-react';
3
  import { Button } from "@/components/ui/button";
4
  import { Input } from "@/components/ui/input";
@@ -7,107 +7,170 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
7
  import { motion, AnimatePresence } from 'framer-motion';
8
  import SequenceCard from './SequenceCard';
9
 
10
- export default function SequenceViewer({ isGenerating, contactCount, selectedProducts, uploadedFile, prompts, onComplete }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  const [sequences, setSequences] = useState([]);
12
  const [contacts, setContacts] = useState([]);
13
  const [progress, setProgress] = useState(0);
14
  const [searchQuery, setSearchQuery] = useState('');
15
  const [filterProduct, setFilterProduct] = useState('all');
16
  const [isComplete, setIsComplete] = useState(false);
17
- const [displayedCount, setDisplayedCount] = useState(50); // Pagination: show 50 at a time
 
 
18
 
19
  useEffect(() => {
20
- if (isGenerating && uploadedFile?.fileId) {
 
 
 
 
21
  setSequences([]);
22
  setContacts([]);
23
  setProgress(0);
24
  setIsComplete(false);
25
-
26
- // Start streaming sequences from API
27
- const eventSource = new EventSource(`/api/generate-sequences?file_id=${uploadedFile.fileId}`, {
28
- withCredentials: false
29
- });
30
-
31
- eventSource.onmessage = (event) => {
32
- try {
33
- const data = JSON.parse(event.data);
34
-
35
- if (data.type === 'sequence') {
36
- const sequence = data.sequence;
37
- setSequences(prev => [...prev, sequence]);
38
-
39
- // Group sequences by contact (sequence.id)
40
- setContacts(prev => {
41
- const existingContact = prev.find(c =>
42
- c.firstName === sequence.firstName &&
43
- c.lastName === sequence.lastName &&
44
- c.email === sequence.email
45
- );
46
-
47
- let updatedContacts;
48
- if (existingContact) {
49
- // Add email to existing contact
50
- existingContact.emails.push({
51
- emailNumber: sequence.emailNumber || existingContact.emails.length + 1,
52
- subject: sequence.subject,
53
- emailContent: sequence.emailContent
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  });
55
- updatedContacts = [...prev];
56
- } else {
57
- // Create new contact
58
- updatedContacts = [...prev, {
59
- id: sequence.id,
60
- firstName: sequence.firstName,
61
- lastName: sequence.lastName,
62
- email: sequence.email,
63
- company: sequence.company,
64
- title: sequence.title,
65
- product: sequence.product,
66
- emails: [{
67
- emailNumber: sequence.emailNumber || 1,
68
- subject: sequence.subject,
69
- emailContent: sequence.emailContent
70
- }]
71
- }];
72
  }
73
-
74
- // Update progress based on unique contacts
75
- const progressValue = contactCount > 0
76
- ? Math.min(100, Math.max(0, (updatedContacts.length / contactCount) * 100))
77
- : 0;
78
- setProgress(progressValue);
79
-
80
- return updatedContacts;
81
  });
82
- } else if (data.type === 'progress') {
83
- setProgress(data.progress);
84
- } else if (data.type === 'complete') {
85
- setIsComplete(true);
86
- onComplete?.();
87
- eventSource.close();
88
- } else if (data.type === 'error') {
89
- console.error('Generation error:', data.error);
90
- alert('Error generating sequences: ' + data.error);
91
- eventSource.close();
92
  }
93
- } catch (error) {
94
- console.error('Error parsing SSE data:', error);
95
  }
96
- };
97
-
98
- eventSource.onerror = (error) => {
99
- console.error('SSE error:', error);
100
- eventSource.close();
101
- if (!isComplete) {
102
- alert('Connection error. Please try again.');
103
- }
104
- };
105
-
106
- return () => {
107
- eventSource.close();
108
- };
109
- }
110
- }, [isGenerating, uploadedFile, contactCount, selectedProducts, prompts, onComplete, isComplete]);
111
 
112
  const handleDownload = async () => {
113
  try {
@@ -117,7 +180,7 @@ export default function SequenceViewer({ isGenerating, contactCount, selectedPro
117
  const url = URL.createObjectURL(blob);
118
  const a = document.createElement('a');
119
  a.href = url;
120
- a.download = 'email_sequences.csv';
121
  a.click();
122
  URL.revokeObjectURL(url);
123
  } else {
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
  import { Download, Mail, Loader2, CheckCircle2, Search, Filter } from 'lucide-react';
3
  import { Button } from "@/components/ui/button";
4
  import { Input } from "@/components/ui/input";
 
7
  import { motion, AnimatePresence } from 'framer-motion';
8
  import SequenceCard from './SequenceCard';
9
 
10
+ function applySequenceToContacts(prev, sequence, contactCount, setProgress) {
11
+ const existingContact = prev.find(c =>
12
+ c.firstName === sequence.firstName &&
13
+ c.lastName === sequence.lastName &&
14
+ c.email === sequence.email
15
+ );
16
+ let updatedContacts;
17
+ if (existingContact) {
18
+ existingContact.emails.push({
19
+ emailNumber: sequence.emailNumber || existingContact.emails.length + 1,
20
+ subject: sequence.subject,
21
+ emailContent: sequence.emailContent
22
+ });
23
+ updatedContacts = [...prev];
24
+ } else {
25
+ updatedContacts = [...prev, {
26
+ id: sequence.id,
27
+ firstName: sequence.firstName,
28
+ lastName: sequence.lastName,
29
+ email: sequence.email,
30
+ company: sequence.company,
31
+ title: sequence.title,
32
+ product: sequence.product,
33
+ emails: [{
34
+ emailNumber: sequence.emailNumber || 1,
35
+ subject: sequence.subject,
36
+ emailContent: sequence.emailContent
37
+ }]
38
+ }];
39
+ }
40
+ const progressValue = contactCount > 0
41
+ ? Math.min(100, Math.max(0, (updatedContacts.length / contactCount) * 100))
42
+ : 0;
43
+ setProgress(progressValue);
44
+ return updatedContacts;
45
+ }
46
+
47
+ export default function SequenceViewer({ isGenerating, generationRunId, contactCount, selectedProducts, uploadedFile, prompts, onComplete }) {
48
  const [sequences, setSequences] = useState([]);
49
  const [contacts, setContacts] = useState([]);
50
  const [progress, setProgress] = useState(0);
51
  const [searchQuery, setSearchQuery] = useState('');
52
  const [filterProduct, setFilterProduct] = useState('all');
53
  const [isComplete, setIsComplete] = useState(false);
54
+ const [displayedCount, setDisplayedCount] = useState(50);
55
+ const [reconnectKey, setReconnectKey] = useState(0);
56
+ const prevRunIdRef = useRef(null);
57
 
58
  useEffect(() => {
59
+ if (!isGenerating || !uploadedFile?.fileId) return;
60
+
61
+ const isNewRun = prevRunIdRef.current !== generationRunId;
62
+ if (isNewRun) {
63
+ prevRunIdRef.current = generationRunId;
64
  setSequences([]);
65
  setContacts([]);
66
  setProgress(0);
67
  setIsComplete(false);
68
+ }
69
+
70
+ const reset = isNewRun ? 1 : 0;
71
+ const url = `/api/generate-sequences?file_id=${encodeURIComponent(uploadedFile.fileId)}&reset=${reset}`;
72
+ const eventSource = new EventSource(url, { withCredentials: false });
73
+
74
+ eventSource.onmessage = (event) => {
75
+ try {
76
+ const data = JSON.parse(event.data);
77
+ if (data.type === 'sequence') {
78
+ const seq = data.sequence;
79
+ setSequences(prev => {
80
+ if (prev.some(s => s.id === seq.id && s.emailNumber === seq.emailNumber)) return prev;
81
+ return [...prev, seq];
82
+ });
83
+ setContacts(prev => {
84
+ const existing = prev.find(c => c.email === seq.email);
85
+ if (existing?.emails.some(e => e.emailNumber === seq.emailNumber)) return prev;
86
+ return applySequenceToContacts(prev, seq, contactCount, setProgress);
87
+ });
88
+ } else if (data.type === 'progress') {
89
+ setProgress(data.progress);
90
+ } else if (data.type === 'complete') {
91
+ setIsComplete(true);
92
+ onComplete?.();
93
+ eventSource.close();
94
+ } else if (data.type === 'error') {
95
+ console.error('Generation error:', data.error);
96
+ alert('Error generating sequences: ' + data.error);
97
+ eventSource.close();
98
+ }
99
+ } catch (err) {
100
+ console.error('Error parsing SSE data:', err);
101
+ }
102
+ };
103
+
104
+ eventSource.onerror = () => {
105
+ eventSource.close();
106
+ if (!isComplete) setReconnectKey(k => k + 1);
107
+ };
108
+
109
+ return () => eventSource.close();
110
+ }, [isGenerating, uploadedFile?.fileId, generationRunId, contactCount, reconnectKey, onComplete, isComplete]);
111
+
112
+ useEffect(() => {
113
+ if (!isGenerating || !uploadedFile?.fileId || reconnectKey === 0) return;
114
+ let cancelled = false;
115
+ (async () => {
116
+ try {
117
+ const [statusRes, seqRes] = await Promise.all([
118
+ fetch(`/api/generation-status?file_id=${encodeURIComponent(uploadedFile.fileId)}`),
119
+ fetch(`/api/sequences?file_id=${encodeURIComponent(uploadedFile.fileId)}`)
120
+ ]);
121
+ if (cancelled) return;
122
+ if (statusRes.ok && seqRes.ok) {
123
+ const status = await statusRes.json();
124
+ const { sequences: list } = await seqRes.json();
125
+ if (status.is_complete) {
126
+ setIsComplete(true);
127
+ onComplete?.();
128
+ }
129
+ if (list?.length > 0) {
130
+ const byContact = new Map();
131
+ list.forEach(seq => {
132
+ const key = seq.email;
133
+ if (!byContact.has(key)) {
134
+ byContact.set(key, {
135
+ id: seq.id,
136
+ firstName: seq.firstName,
137
+ lastName: seq.lastName,
138
+ email: seq.email,
139
+ company: seq.company,
140
+ title: seq.title,
141
+ product: seq.product,
142
+ emails: []
143
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  }
145
+ byContact.get(key).emails.push({
146
+ emailNumber: seq.emailNumber,
147
+ subject: seq.subject,
148
+ emailContent: seq.emailContent
149
+ });
 
 
 
150
  });
151
+ const arr = [...byContact.values()];
152
+ arr.sort((a, b) => (a.id || 0) - (b.id || 0));
153
+ setSequences(list);
154
+ setContacts(arr);
155
+ const p = status.total_contacts > 0 ? Math.min(100, (arr.length / status.total_contacts) * 100) : 0;
156
+ setProgress(p);
 
 
 
 
157
  }
 
 
158
  }
159
+ } catch (e) {
160
+ if (!cancelled) console.error('Reconnect fetch error:', e);
161
+ }
162
+ })();
163
+ return () => { cancelled = true; };
164
+ }, [reconnectKey, isGenerating, uploadedFile?.fileId, contactCount, onComplete]);
165
+
166
+ useEffect(() => {
167
+ if (!isGenerating || !uploadedFile?.fileId) return;
168
+ const onVisible = () => {
169
+ if (document.visibilityState === 'visible') setReconnectKey(k => k + 1);
170
+ };
171
+ document.addEventListener('visibilitychange', onVisible);
172
+ return () => document.removeEventListener('visibilitychange', onVisible);
173
+ }, [isGenerating, uploadedFile?.fileId]);
174
 
175
  const handleDownload = async () => {
176
  try {
 
180
  const url = URL.createObjectURL(blob);
181
  const a = document.createElement('a');
182
  a.href = url;
183
+ a.download = 'email_sequences_fixed.csv';
184
  a.click();
185
  URL.revokeObjectURL(url);
186
  } else {
frontend/src/components/smartlead/SmartleadPanel.jsx CHANGED
@@ -85,7 +85,7 @@ export default function SmartleadPanel({ uploadedFile, sequencesCount, onPushCom
85
  const url = URL.createObjectURL(blob);
86
  const a = document.createElement('a');
87
  a.href = url;
88
- a.download = 'email_sequences.csv';
89
  a.click();
90
  URL.revokeObjectURL(url);
91
  } else {
 
85
  const url = URL.createObjectURL(blob);
86
  const a = document.createElement('a');
87
  a.href = url;
88
+ a.download = 'email_sequences_fixed.csv';
89
  a.click();
90
  URL.revokeObjectURL(url);
91
  } else {
frontend/src/pages/EmailSequenceGenerator.jsx CHANGED
@@ -15,6 +15,7 @@ export default function EmailSequenceGenerator() {
15
  const [prompts, setPrompts] = useState({});
16
  const [isGenerating, setIsGenerating] = useState(false);
17
  const [generationComplete, setGenerationComplete] = useState(false);
 
18
  const generateButtonRef = useRef(null);
19
 
20
  const canProceedToStep2 = uploadedFile && selectedProducts.length > 0;
@@ -56,6 +57,7 @@ export default function EmailSequenceGenerator() {
56
  return;
57
  }
58
 
 
59
  setStep(3);
60
  setIsGenerating(true);
61
  };
@@ -265,6 +267,7 @@ export default function EmailSequenceGenerator() {
265
 
266
  <SequenceViewer
267
  isGenerating={isGenerating}
 
268
  contactCount={uploadedFile?.contactCount || 50}
269
  selectedProducts={selectedProducts}
270
  uploadedFile={uploadedFile}
 
15
  const [prompts, setPrompts] = useState({});
16
  const [isGenerating, setIsGenerating] = useState(false);
17
  const [generationComplete, setGenerationComplete] = useState(false);
18
+ const [generationRunId, setGenerationRunId] = useState(0);
19
  const generateButtonRef = useRef(null);
20
 
21
  const canProceedToStep2 = uploadedFile && selectedProducts.length > 0;
 
57
  return;
58
  }
59
 
60
+ setGenerationRunId((r) => r + 1);
61
  setStep(3);
62
  setIsGenerating(true);
63
  };
 
267
 
268
  <SequenceViewer
269
  isGenerating={isGenerating}
270
+ generationRunId={generationRunId}
271
  contactCount={uploadedFile?.contactCount || 50}
272
  selectedProducts={selectedProducts}
273
  uploadedFile={uploadedFile}