Khushal-kreeda commited on
Commit
56bf851
·
1 Parent(s): 7748079
.env.example ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Syncora Environment Configuration
2
+ # Copy this file to .env.local and fill in your actual values
3
+
4
+ # AWS Configuration (Required for client-side S3 upload)
5
+ REACT_APP_AWS_ACCESS_KEY_ID=your_aws_access_key_id_here
6
+ REACT_APP_AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key_here
7
+ REACT_APP_AWS_REGION=aws_region_here
8
+ REACT_APP_S3_BUCKET_NAME=your_s3_bucket_name_here
9
+
10
+ # API Configuration
11
+ REACT_APP_API_BASE_URL=api.example.com
12
+ REACT_APP_API_TIMEOUT=30
13
+ REACT_APP_MAX_RETRIES=3
14
+
15
+ # Application Settings
16
+ REACT_APP_DEFAULT_NUM_RECORDS=100
17
+ REACT_APP_MAX_FILE_SIZE_MB=10
18
+
19
+ # Debug Mode (set to 'true' for development)
20
+ REACT_APP_DEBUG=false
21
+
22
+ # Note: API key encryption/decryption keys are hardcoded in config.js for security
23
+ # In production, these should be environment variables on the server side
24
+ DEBUG=false
25
+ LOG_LEVEL=INFO
26
+ MAX_FILE_SIZE_MB=10
27
+ DEFAULT_NUM_RECORDS=25
28
+
29
+ # Optional: Custom API timeout settings (Server-side)
30
+ API_TIMEOUT=1800
31
+ MAX_RETRIES=3
.gitignore CHANGED
@@ -17,6 +17,7 @@
17
  .env.development.local
18
  .env.test.local
19
  .env.production.local
 
20
 
21
  npm-debug.log*
22
  yarn-debug.log*
 
17
  .env.development.local
18
  .env.test.local
19
  .env.production.local
20
+ .env
21
 
22
  npm-debug.log*
23
  yarn-debug.log*
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -1,12 +1,13 @@
1
  {
2
- "name": "react-template",
3
  "version": "0.1.0",
4
  "private": true,
5
  "dependencies": {
6
- "@testing-library/dom": "^10.4.0",
7
- "@testing-library/jest-dom": "^6.6.3",
8
- "@testing-library/react": "^16.3.0",
9
- "@testing-library/user-event": "^13.5.0",
 
10
  "react": "^19.1.0",
11
  "react-dom": "^19.1.0",
12
  "react-scripts": "5.0.1",
 
1
  {
2
+ "name": "syncora-synthetic-generation",
3
  "version": "0.1.0",
4
  "private": true,
5
  "dependencies": {
6
+ "@aws-sdk/client-s3": "^3.837.0",
7
+ "@aws-sdk/s3-request-presigner": "^3.837.0",
8
+ "crypto-js": "^4.2.0",
9
+ "dotenv": "^17.0.0",
10
+ "papaparse": "^5.5.3",
11
  "react": "^19.1.0",
12
  "react-dom": "^19.1.0",
13
  "react-scripts": "5.0.1",
public/index.html CHANGED
@@ -24,7 +24,7 @@
24
  work correctly both with client-side routing and a non-root public URL.
25
  Learn how to configure a non-root public URL by running `npm run build`.
26
  -->
27
- <title>React App</title>
28
  </head>
29
  <body>
30
  <noscript>You need to enable JavaScript to run this app.</noscript>
 
24
  work correctly both with client-side routing and a non-root public URL.
25
  Learn how to configure a non-root public URL by running `npm run build`.
26
  -->
27
+ <title>Syncora Data Generation</title>
28
  </head>
29
  <body>
30
  <noscript>You need to enable JavaScript to run this app.</noscript>
src/App.css CHANGED
@@ -1,34 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  .App {
2
- text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  }
4
 
5
- .App-logo {
6
- height: 40vmin;
7
- pointer-events: none;
 
 
 
 
 
8
  }
9
 
10
- @media (prefers-reduced-motion: no-preference) {
11
- .App-logo {
12
- animation: App-logo-spin infinite 20s linear;
13
- }
 
 
 
 
14
  }
15
 
16
- .App-header {
17
- background-color: #282c34;
18
- min-height: 100vh;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  display: flex;
20
- flex-direction: column;
21
  align-items: center;
22
  justify-content: center;
23
- font-size: calc(10px + 2vmin);
24
- color: white;
25
  }
26
 
27
- .App-link {
28
- color: #61dafb;
 
 
29
  }
30
 
31
- @keyframes App-logo-spin {
 
 
 
 
 
 
32
  from {
33
  transform: rotate(0deg);
34
  }
@@ -36,3 +184,1005 @@
36
  transform: rotate(360deg);
37
  }
38
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Import Google Fonts */
2
+ @import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap");
3
+
4
+ * {
5
+ margin: 0;
6
+ padding: 0;
7
+ box-sizing: border-box;
8
+ }
9
+
10
+ :root {
11
+ --primary-color: #3b82f6;
12
+ --primary-hover: #2563eb;
13
+ --primary-light: rgba(59, 130, 246, 0.1);
14
+ --secondary-color: #6b7280;
15
+ --success-color: #10b981;
16
+ --error-color: #ef4444;
17
+ --warning-color: #f59e0b;
18
+ --bg-primary: #0f172a;
19
+ --bg-secondary: #1e293b;
20
+ --bg-tertiary: #334155;
21
+ --bg-card: #1e293b;
22
+ --border-color: #475569;
23
+ --border-light: #64748b;
24
+ --text-primary: #f8fafc;
25
+ --text-secondary: #cbd5e1;
26
+ --text-muted: #94a3b8;
27
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
28
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
29
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
30
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
31
+ --gradient-primary: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
32
+ --gradient-card: linear-gradient(145deg, #1e293b 0%, #334155 100%);
33
+ }
34
+
35
  .App {
36
+ min-height: 100vh;
37
+ background: var(--bg-primary);
38
+ background-image: radial-gradient(
39
+ circle at 20% 20%,
40
+ rgba(59, 130, 246, 0.08) 0%,
41
+ transparent 50%
42
+ ),
43
+ radial-gradient(
44
+ circle at 80% 80%,
45
+ rgba(16, 185, 129, 0.05) 0%,
46
+ transparent 50%
47
+ );
48
+ font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
49
+ "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
50
+ sans-serif;
51
+ -webkit-font-smoothing: antialiased;
52
+ -moz-osx-font-smoothing: grayscale;
53
+ color: var(--text-primary);
54
  }
55
 
56
+ .app-header {
57
+ text-align: left;
58
+ padding: 2rem 2.5rem;
59
+ border-bottom: 1px solid var(--border-color);
60
+ background: var(--bg-card);
61
+ backdrop-filter: blur(10px);
62
+ box-shadow: var(--shadow-lg);
63
+ position: relative;
64
  }
65
 
66
+ .app-header::before {
67
+ content: "";
68
+ position: absolute;
69
+ top: 0;
70
+ left: 0;
71
+ right: 0;
72
+ height: 3px;
73
+ background: var(--gradient-primary);
74
  }
75
 
76
+ .header-content {
77
+ display: flex;
78
+ justify-content: space-between;
79
+ align-items: center;
80
+ max-width: 1400px;
81
+ margin: 0 auto;
82
+ }
83
+
84
+ .app-header h1 {
85
+ font-size: 2.25rem;
86
+ font-weight: 700;
87
+ margin-bottom: 0.5rem;
88
+ color: var(--text-primary);
89
+ background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
90
+ -webkit-background-clip: text;
91
+ -webkit-text-fill-color: transparent;
92
+ background-clip: text;
93
+ }
94
+
95
+ .app-header p {
96
+ font-size: 1rem;
97
+ color: var(--text-secondary);
98
+ font-weight: 500;
99
+ }
100
+
101
+ .status-indicator {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 0.75rem;
105
+ background: rgba(16, 185, 129, 0.15);
106
+ padding: 0.75rem 1.25rem;
107
+ border-radius: 12px;
108
+ border: 1px solid rgba(16, 185, 129, 0.3);
109
+ backdrop-filter: blur(10px);
110
+ box-shadow: var(--shadow-md);
111
+ transition: all 0.3s ease;
112
+ }
113
+
114
+ .status-indicator:hover {
115
+ background: rgba(16, 185, 129, 0.2);
116
+ transform: translateY(-1px);
117
+ box-shadow: var(--shadow-lg);
118
+ }
119
+
120
+ .status-dot {
121
+ width: 10px;
122
+ height: 10px;
123
+ border-radius: 50%;
124
+ background: var(--success-color);
125
+ box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
126
+ animation: pulse 2s infinite;
127
+ }
128
+
129
+ .status-dot.success {
130
+ background: var(--success-color);
131
+ box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
132
+ }
133
+
134
+ .status-dot.error {
135
+ background: var(--error-color);
136
+ box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);
137
+ }
138
+
139
+ .status-dot.checking {
140
+ background: var(--warning-color);
141
+ box-shadow: 0 0 10px rgba(245, 158, 11, 0.5);
142
+ animation: pulse-checking 1s infinite;
143
+ }
144
+
145
+ .status-indicator {
146
+ display: flex;
147
+ align-items: center;
148
+ gap: 8px;
149
+ font-size: 0.875rem;
150
+ color: var(--text-secondary);
151
+ }
152
+
153
+ .status-refresh-btn {
154
+ background: none;
155
+ border: none;
156
+ color: var(--text-muted);
157
+ cursor: pointer;
158
+ padding: 4px;
159
+ border-radius: 4px;
160
+ font-size: 0.875rem;
161
+ transition: all 0.2s ease;
162
  display: flex;
 
163
  align-items: center;
164
  justify-content: center;
 
 
165
  }
166
 
167
+ .status-refresh-btn:hover:not(:disabled) {
168
+ color: var(--text-secondary);
169
+ background: rgba(255, 255, 255, 0.1);
170
+ transform: scale(1.1);
171
  }
172
 
173
+ .status-refresh-btn:disabled {
174
+ opacity: 0.5;
175
+ cursor: not-allowed;
176
+ animation: spin 1s linear infinite;
177
+ }
178
+
179
+ @keyframes spin {
180
  from {
181
  transform: rotate(0deg);
182
  }
 
184
  transform: rotate(360deg);
185
  }
186
  }
187
+
188
+ .status-details {
189
+ font-size: 0.75rem;
190
+ color: var(--text-muted);
191
+ margin-top: 2px;
192
+ max-width: 200px;
193
+ white-space: nowrap;
194
+ overflow: hidden;
195
+ text-overflow: ellipsis;
196
+ }
197
+
198
+ @keyframes pulse-checking {
199
+ 0%,
200
+ 100% {
201
+ opacity: 0.6;
202
+ transform: scale(1);
203
+ }
204
+ 50% {
205
+ opacity: 1;
206
+ transform: scale(1.2);
207
+ }
208
+ }
209
+
210
+ @keyframes pulse {
211
+ 0%,
212
+ 100% {
213
+ opacity: 1;
214
+ transform: scale(1);
215
+ }
216
+ 50% {
217
+ opacity: 0.8;
218
+ transform: scale(1.1);
219
+ }
220
+ }
221
+
222
+ .status-indicator span {
223
+ color: var(--success-color);
224
+ font-size: 0.875rem;
225
+ font-weight: 600;
226
+ }
227
+
228
+ .main-container {
229
+ display: grid;
230
+ grid-template-columns: 1fr 1fr;
231
+ min-height: calc(100vh - 140px);
232
+ gap: 1.5rem;
233
+ padding: 1.5rem;
234
+ max-width: 1400px;
235
+ margin: 0 auto;
236
+ align-items: start;
237
+ }
238
+
239
+ /* Steps Columns */
240
+ .steps-column {
241
+ display: flex;
242
+ flex-direction: column;
243
+ gap: 1.5rem;
244
+ }
245
+
246
+ /* Step Content Areas */
247
+ .step-content {
248
+ background: var(--bg-card);
249
+ background-image: var(--gradient-card);
250
+ padding: 2rem;
251
+ border-radius: 16px;
252
+ border: 1px solid var(--border-color);
253
+ box-shadow: var(--shadow-lg);
254
+ opacity: 1;
255
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
256
+ position: relative;
257
+ backdrop-filter: blur(10px);
258
+ border-color: var(--primary-color);
259
+ box-shadow: var(--shadow-xl), 0 0 0 1px var(--primary-color),
260
+ 0 0 40px rgba(99, 102, 241, 0.15);
261
+ transform: translateY(-2px);
262
+ min-height: fit-content;
263
+ }
264
+
265
+ .step-content::before {
266
+ content: "";
267
+ position: absolute;
268
+ top: 0;
269
+ left: 0;
270
+ right: 0;
271
+ bottom: 0;
272
+ border-radius: 16px;
273
+ padding: 1px;
274
+ background: linear-gradient(
275
+ 135deg,
276
+ transparent,
277
+ rgba(99, 102, 241, 0.2),
278
+ transparent
279
+ );
280
+ mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
281
+ mask-composite: exclude;
282
+ opacity: 1;
283
+ transition: opacity 0.4s ease;
284
+ }
285
+
286
+ /* Step Components Styling */
287
+ .step-container {
288
+ display: flex;
289
+ flex-direction: column;
290
+ min-height: fit-content;
291
+ }
292
+
293
+ .step-header {
294
+ margin-bottom: 2rem;
295
+ padding-bottom: 1.5rem;
296
+ border-bottom: 1px solid var(--border-color);
297
+ position: relative;
298
+ }
299
+
300
+ .step-header::after {
301
+ content: "";
302
+ position: absolute;
303
+ bottom: -1px;
304
+ left: 0;
305
+ width: 60px;
306
+ height: 2px;
307
+ background: var(--gradient-primary);
308
+ border-radius: 1px;
309
+ }
310
+
311
+ .step-header h2 {
312
+ color: var(--text-primary);
313
+ font-size: 1.5rem;
314
+ margin-bottom: 0.75rem;
315
+ font-weight: 700;
316
+ display: flex;
317
+ align-items: center;
318
+ gap: 0.75rem;
319
+ letter-spacing: -0.025em;
320
+ }
321
+
322
+ .step-number {
323
+ display: inline-flex;
324
+ align-items: center;
325
+ justify-content: center;
326
+ width: 32px;
327
+ height: 32px;
328
+ background: var(--gradient-primary);
329
+ border-radius: 8px;
330
+ font-size: 0.875rem;
331
+ font-weight: 700;
332
+ color: white;
333
+ box-shadow: var(--shadow-md);
334
+ }
335
+
336
+ .step-header p {
337
+ color: var(--text-secondary);
338
+ font-size: 1rem;
339
+ line-height: 1.6;
340
+ }
341
+
342
+ .step-body {
343
+ flex: 1;
344
+ display: flex;
345
+ flex-direction: column;
346
+ gap: 1.5rem;
347
+ }
348
+
349
+ .step-footer {
350
+ margin-top: 2rem;
351
+ padding-top: 1.5rem;
352
+ border-top: 1px solid var(--border-color);
353
+ display: flex;
354
+ justify-content: space-between;
355
+ align-items: center;
356
+ }
357
+
358
+ /* Form Elements */
359
+ .form-group {
360
+ display: flex;
361
+ flex-direction: column;
362
+ gap: 0.75rem;
363
+ margin-bottom: 2rem;
364
+ }
365
+
366
+ .form-group:last-child {
367
+ margin-bottom: 0;
368
+ }
369
+
370
+ .form-group label {
371
+ font-weight: 600;
372
+ color: var(--text-primary);
373
+ font-size: 0.9rem;
374
+ letter-spacing: 0.025em;
375
+ margin-bottom: 0.5rem;
376
+ display: flex;
377
+ align-items: center;
378
+ gap: 0.5rem;
379
+ }
380
+
381
+ .form-input {
382
+ padding: 1rem 1.25rem;
383
+ border: 1px solid var(--border-color);
384
+ border-radius: 12px;
385
+ font-size: 0.875rem;
386
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
387
+ background: rgba(15, 23, 42, 0.8);
388
+ color: var(--text-primary);
389
+ backdrop-filter: blur(10px);
390
+ box-shadow: var(--shadow-sm);
391
+ }
392
+
393
+ .form-input:focus {
394
+ outline: none;
395
+ border-color: var(--primary-color);
396
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1), var(--shadow-md);
397
+ transform: translateY(-1px);
398
+ }
399
+
400
+ .form-input::placeholder {
401
+ color: var(--text-muted);
402
+ }
403
+
404
+ .btn {
405
+ padding: 1rem 1.5rem;
406
+ border: none;
407
+ border-radius: 12px;
408
+ font-size: 0.875rem;
409
+ font-weight: 600;
410
+ cursor: pointer;
411
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
412
+ text-decoration: none;
413
+ display: inline-flex;
414
+ align-items: center;
415
+ justify-content: center;
416
+ gap: 0.5rem;
417
+ position: relative;
418
+ overflow: hidden;
419
+ letter-spacing: -0.025em;
420
+ box-shadow: var(--shadow-md);
421
+ }
422
+
423
+ .btn::before {
424
+ content: "";
425
+ position: absolute;
426
+ top: 0;
427
+ left: -100%;
428
+ width: 100%;
429
+ height: 100%;
430
+ background: linear-gradient(
431
+ 90deg,
432
+ transparent,
433
+ rgba(255, 255, 255, 0.1),
434
+ transparent
435
+ );
436
+ transition: left 0.5s ease;
437
+ }
438
+
439
+ .btn:hover::before {
440
+ left: 100%;
441
+ }
442
+
443
+ .btn-primary {
444
+ background: var(--gradient-primary);
445
+ color: white;
446
+ }
447
+
448
+ .btn-primary:hover {
449
+ transform: translateY(-2px);
450
+ box-shadow: var(--shadow-lg);
451
+ }
452
+
453
+ .btn-secondary {
454
+ background: var(--bg-tertiary);
455
+ color: var(--text-primary);
456
+ border: 1px solid var(--border-color);
457
+ }
458
+
459
+ .btn-secondary:hover {
460
+ background: var(--border-light);
461
+ transform: translateY(-1px);
462
+ }
463
+
464
+ .btn-success {
465
+ background: linear-gradient(135deg, var(--success-color) 0%, #059669 100%);
466
+ color: white;
467
+ }
468
+
469
+ .btn-success:hover {
470
+ transform: translateY(-2px);
471
+ box-shadow: var(--shadow-lg);
472
+ }
473
+
474
+ .btn:disabled {
475
+ opacity: 0.6;
476
+ cursor: not-allowed;
477
+ transform: none;
478
+ }
479
+
480
+ .btn:disabled:hover {
481
+ transform: none;
482
+ }
483
+
484
+ /* File Upload */
485
+ .file-upload,
486
+ .file-upload-area {
487
+ border: 2px dashed var(--border-color);
488
+ border-radius: 16px;
489
+ padding: 3rem 2rem;
490
+ text-align: center;
491
+ cursor: pointer;
492
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
493
+ background: rgba(15, 23, 42, 0.5);
494
+ color: var(--text-secondary);
495
+ position: relative;
496
+ overflow: hidden;
497
+ margin-top: 1rem;
498
+ }
499
+
500
+ .file-upload::before,
501
+ .file-upload-area::before {
502
+ content: "";
503
+ position: absolute;
504
+ top: 0;
505
+ left: 0;
506
+ right: 0;
507
+ bottom: 0;
508
+ background: radial-gradient(
509
+ circle at center,
510
+ rgba(99, 102, 241, 0.05) 0%,
511
+ transparent 70%
512
+ );
513
+ opacity: 0;
514
+ transition: opacity 0.3s ease;
515
+ }
516
+
517
+ .file-upload:hover,
518
+ .file-upload-area:hover {
519
+ border-color: var(--primary-color);
520
+ background: rgba(99, 102, 241, 0.05);
521
+ transform: translateY(-2px);
522
+ box-shadow: var(--shadow-lg);
523
+ }
524
+
525
+ .file-upload:hover::before,
526
+ .file-upload-area:hover::before {
527
+ opacity: 1;
528
+ }
529
+
530
+ .file-upload.drag-over,
531
+ .file-upload-area.drag-over {
532
+ border-color: var(--primary-color);
533
+ background: rgba(99, 102, 241, 0.1);
534
+ box-shadow: var(--shadow-lg), 0 0 0 1px var(--primary-color);
535
+ }
536
+
537
+ .file-upload input[type="file"],
538
+ .file-upload-area input[type="file"] {
539
+ display: none;
540
+ }
541
+
542
+ .file-upload-icon {
543
+ font-size: 3rem;
544
+ margin-bottom: 1rem;
545
+ color: var(--primary-color);
546
+ }
547
+
548
+ .file-upload-text {
549
+ font-size: 1.125rem;
550
+ font-weight: 600;
551
+ margin-bottom: 0.5rem;
552
+ color: var(--text-primary);
553
+ letter-spacing: -0.025em;
554
+ }
555
+
556
+ .file-upload-subtext {
557
+ font-size: 0.875rem;
558
+ color: var(--text-muted);
559
+ }
560
+
561
+ .file-uploaded {
562
+ background: var(--bg-tertiary);
563
+ border: 2px solid var(--success-color);
564
+ border-radius: 12px;
565
+ padding: 1.5rem;
566
+ box-shadow: var(--shadow-md), 0 0 20px rgba(16, 185, 129, 0.1);
567
+ margin-top: 1rem;
568
+ position: relative;
569
+ overflow: hidden;
570
+ }
571
+
572
+ .file-uploaded::before {
573
+ content: "";
574
+ position: absolute;
575
+ top: 0;
576
+ left: 0;
577
+ right: 0;
578
+ height: 3px;
579
+ background: linear-gradient(90deg, var(--success-color) 0%, #34d399 100%);
580
+ }
581
+
582
+ .file-info {
583
+ display: flex;
584
+ align-items: center;
585
+ gap: 1rem;
586
+ color: var(--success-color);
587
+ }
588
+
589
+ .file-icon {
590
+ font-size: 2rem;
591
+ background: rgba(16, 185, 129, 0.1);
592
+ padding: 0.75rem;
593
+ border-radius: 8px;
594
+ border: 1px solid rgba(16, 185, 129, 0.3);
595
+ }
596
+
597
+ .file-details {
598
+ flex: 1;
599
+ }
600
+
601
+ .file-details h4 {
602
+ font-weight: 600;
603
+ margin-bottom: 0.5rem;
604
+ color: var(--text-primary);
605
+ font-size: 1.1rem;
606
+ }
607
+
608
+ .file-details p {
609
+ font-size: 0.875rem;
610
+ opacity: 0.8;
611
+ color: var(--text-secondary);
612
+ margin-bottom: 0.25rem;
613
+ }
614
+
615
+ .file-details p:last-child {
616
+ margin-bottom: 0;
617
+ }
618
+
619
+ /* Output Options */
620
+ .output-options {
621
+ display: flex;
622
+ gap: 0.75rem;
623
+ margin-bottom: 1.5rem;
624
+ }
625
+
626
+ .output-option {
627
+ padding: 0.75rem 1rem;
628
+ border: 1px solid var(--border-color);
629
+ border-radius: 8px;
630
+ text-align: left;
631
+ cursor: pointer;
632
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
633
+ background: var(--bg-tertiary);
634
+ color: var(--text-primary);
635
+ position: relative;
636
+ overflow: hidden;
637
+ flex: 1;
638
+ min-width: 0;
639
+ }
640
+
641
+ .output-option::before {
642
+ content: "";
643
+ position: absolute;
644
+ top: 0;
645
+ left: 0;
646
+ right: 0;
647
+ bottom: 0;
648
+ background: var(--gradient-primary);
649
+ opacity: 0;
650
+ transition: opacity 0.3s ease;
651
+ }
652
+
653
+ .output-option:hover {
654
+ border-color: var(--primary-color);
655
+ transform: translateY(-2px);
656
+ box-shadow: var(--shadow-lg);
657
+ }
658
+
659
+ .output-option.selected {
660
+ border-color: var(--primary-color);
661
+ background: rgba(99, 102, 241, 0.1);
662
+ box-shadow: var(--shadow-md), 0 0 0 1px var(--primary-color);
663
+ }
664
+
665
+ .output-option.selected::before {
666
+ opacity: 0.05;
667
+ }
668
+
669
+ .output-option.disabled {
670
+ opacity: 0.5;
671
+ cursor: not-allowed;
672
+ }
673
+
674
+ .output-option.disabled:hover {
675
+ transform: none;
676
+ box-shadow: none;
677
+ }
678
+
679
+ .output-option h4 {
680
+ margin-bottom: 0.25rem;
681
+ color: var(--text-primary);
682
+ font-weight: 600;
683
+ position: relative;
684
+ z-index: 1;
685
+ font-size: 0.875rem;
686
+ }
687
+
688
+ .output-option p {
689
+ color: var(--text-secondary);
690
+ font-size: 0.75rem;
691
+ line-height: 1.4;
692
+ position: relative;
693
+ z-index: 1;
694
+ opacity: 0.8;
695
+ }
696
+
697
+ /* Data Preview */
698
+ .data-preview {
699
+ margin-top: 1.5rem;
700
+ background: var(--bg-card);
701
+ border-radius: 12px;
702
+ padding: 1.5rem;
703
+ border: 1px solid var(--border-color);
704
+ box-shadow: var(--shadow-md);
705
+ }
706
+
707
+ .data-preview h4 {
708
+ color: var(--text-primary);
709
+ margin-bottom: 1rem;
710
+ font-size: 1rem;
711
+ font-weight: 600;
712
+ }
713
+
714
+ .data-table-container {
715
+ width: 100%;
716
+ max-height: 400px;
717
+ overflow: auto;
718
+ border: 1px solid var(--border-color);
719
+ border-radius: 8px;
720
+ box-shadow: var(--shadow-md);
721
+ background: var(--bg-secondary);
722
+ }
723
+
724
+ .data-table-scrollable {
725
+ width: 100%;
726
+ border-collapse: collapse;
727
+ font-size: 0.8rem;
728
+ min-width: 600px; /* Ensure minimum width for horizontal scrolling */
729
+ }
730
+
731
+ .data-table-scrollable th,
732
+ .data-table-scrollable td {
733
+ padding: 0.75rem 1rem;
734
+ text-align: left;
735
+ border-bottom: 1px solid var(--border-color);
736
+ color: var(--text-primary);
737
+ white-space: nowrap;
738
+ overflow: hidden;
739
+ text-overflow: ellipsis;
740
+ max-width: 200px; /* Limit column width */
741
+ min-width: 100px; /* Minimum column width */
742
+ }
743
+
744
+ .data-table-scrollable th {
745
+ background: var(--bg-primary);
746
+ font-weight: 600;
747
+ color: var(--text-primary);
748
+ position: sticky;
749
+ top: 0;
750
+ z-index: 2;
751
+ border-right: 1px solid var(--border-color);
752
+ }
753
+
754
+ .data-table-scrollable td {
755
+ border-right: 1px solid var(--border-color);
756
+ }
757
+
758
+ .data-table-scrollable tr:hover {
759
+ background: rgba(59, 130, 246, 0.08);
760
+ }
761
+
762
+ .data-table-scrollable tr:nth-child(even) {
763
+ background: rgba(255, 255, 255, 0.02);
764
+ }
765
+
766
+ .preview-note {
767
+ margin-top: 0.75rem;
768
+ font-size: 0.85rem;
769
+ color: var(--text-muted);
770
+ text-align: center;
771
+ font-style: italic;
772
+ }
773
+
774
+ /* Custom scrollbar for the data table container */
775
+ .data-table-container::-webkit-scrollbar {
776
+ width: 8px;
777
+ height: 8px;
778
+ }
779
+
780
+ .data-table-container::-webkit-scrollbar-track {
781
+ background: var(--bg-tertiary);
782
+ border-radius: 4px;
783
+ }
784
+
785
+ .data-table-container::-webkit-scrollbar-thumb {
786
+ background: var(--border-light);
787
+ border-radius: 4px;
788
+ }
789
+
790
+ .data-table-container::-webkit-scrollbar-thumb:hover {
791
+ background: var(--primary-color);
792
+ }
793
+
794
+ /* Legacy data-table styles for backward compatibility */
795
+ .data-table {
796
+ width: 100%;
797
+ border-collapse: collapse;
798
+ font-size: 0.8rem;
799
+ border-radius: 8px;
800
+ overflow: hidden;
801
+ }
802
+
803
+ .data-table th,
804
+ .data-table td {
805
+ padding: 0.75rem 0.5rem;
806
+ text-align: left;
807
+ border-bottom: 1px solid var(--border-color);
808
+ color: var(--text-primary);
809
+ }
810
+
811
+ .data-table th {
812
+ background: var(--bg-primary);
813
+ font-weight: 600;
814
+ color: var(--text-primary);
815
+ position: sticky;
816
+ top: 0;
817
+ z-index: 1;
818
+ }
819
+
820
+ .data-table tr:hover {
821
+ background: rgba(99, 102, 241, 0.05);
822
+ }
823
+
824
+ /* Slider */
825
+ .slider-container {
826
+ display: flex;
827
+ flex-direction: column;
828
+ gap: 0.75rem;
829
+ }
830
+
831
+ .slider {
832
+ width: 100%;
833
+ height: 8px;
834
+ border-radius: 4px;
835
+ background: var(--bg-tertiary);
836
+ outline: none;
837
+ -webkit-appearance: none;
838
+ appearance: none;
839
+ position: relative;
840
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
841
+ }
842
+
843
+ .slider::-webkit-slider-thumb {
844
+ -webkit-appearance: none;
845
+ appearance: none;
846
+ width: 24px;
847
+ height: 24px;
848
+ border-radius: 50%;
849
+ background: var(--gradient-primary);
850
+ cursor: pointer;
851
+ box-shadow: var(--shadow-md);
852
+ transition: all 0.3s ease;
853
+ }
854
+
855
+ .slider::-webkit-slider-thumb:hover {
856
+ transform: scale(1.1);
857
+ box-shadow: var(--shadow-lg);
858
+ }
859
+
860
+ .slider::-moz-range-thumb {
861
+ width: 24px;
862
+ height: 24px;
863
+ border-radius: 50%;
864
+ background: var(--gradient-primary);
865
+ cursor: pointer;
866
+ border: none;
867
+ box-shadow: var(--shadow-md);
868
+ transition: all 0.3s ease;
869
+ }
870
+
871
+ slider::-moz-range-thumb:hover {
872
+ transform: scale(1.1);
873
+ box-shadow: var(--shadow-lg);
874
+ }
875
+
876
+ .slider-value {
877
+ text-align: center;
878
+ font-weight: 600;
879
+ color: var(--primary-color);
880
+ font-size: 1rem;
881
+ background: rgba(99, 102, 241, 0.1);
882
+ padding: 0.5rem 1rem;
883
+ border-radius: 8px;
884
+ border: 1px solid rgba(99, 102, 241, 0.2);
885
+ }
886
+
887
+ /* Status Messages */
888
+ .status-message {
889
+ padding: 1rem 1.25rem;
890
+ border-radius: 12px;
891
+ font-weight: 500;
892
+ font-size: 0.875rem;
893
+ display: flex;
894
+ align-items: center;
895
+ gap: 0.75rem;
896
+ backdrop-filter: blur(10px);
897
+ box-shadow: var(--shadow-md);
898
+ font-family: "Inter", sans-serif;
899
+ }
900
+
901
+ .status-message.success {
902
+ background: rgba(16, 185, 129, 0.15);
903
+ color: var(--success-color);
904
+ border: 1px solid rgba(16, 185, 129, 0.3);
905
+ }
906
+
907
+ .status-message.error {
908
+ background: rgba(248, 113, 113, 0.15);
909
+ color: var(--error-color);
910
+ border: 1px solid rgba(248, 113, 113, 0.3);
911
+ }
912
+
913
+ .status-message.info {
914
+ background: rgba(99, 102, 241, 0.15);
915
+ color: var(--primary-color);
916
+ border: 1px solid rgba(99, 102, 241, 0.3);
917
+ }
918
+
919
+ .status-message.warning {
920
+ background: rgba(245, 158, 11, 0.15);
921
+ color: var(--warning-color);
922
+ border: 1px solid rgba(245, 158, 11, 0.3);
923
+ }
924
+
925
+ .status-message-icon {
926
+ font-size: 1.25rem;
927
+ display: flex;
928
+ align-items: center;
929
+ flex-shrink: 0;
930
+ }
931
+
932
+ .status-message-content h4 {
933
+ margin-bottom: 0.5rem;
934
+ font-size: 0.875rem;
935
+ font-weight: 600;
936
+ }
937
+
938
+ .status-message-content ul {
939
+ margin: 0;
940
+ color: inherit;
941
+ opacity: 0.9;
942
+ }
943
+
944
+ /* Status Summary List */
945
+ .status-summary {
946
+ margin-top: 0.75rem;
947
+ background: rgba(15, 23, 42, 0.3);
948
+ border: 1px solid var(--border-color);
949
+ border-radius: 6px;
950
+ padding: 0.75rem;
951
+ backdrop-filter: blur(5px);
952
+ }
953
+
954
+ .status-row {
955
+ display: flex;
956
+ justify-content: space-between;
957
+ align-items: center;
958
+ padding: 0.4rem 0;
959
+ border-bottom: 1px solid rgba(71, 85, 105, 0.2);
960
+ font-size: 0.8rem;
961
+ }
962
+
963
+ .status-row:last-child {
964
+ border-bottom: none;
965
+ margin-bottom: 0;
966
+ padding-bottom: 0;
967
+ }
968
+
969
+ .status-row:first-child {
970
+ padding-top: 0;
971
+ }
972
+
973
+ .status-label {
974
+ font-weight: 500;
975
+ color: var(--text-secondary);
976
+ min-width: 80px;
977
+ font-size: 0.8rem;
978
+ }
979
+
980
+ .status-badge {
981
+ padding: 0.15rem 0.5rem;
982
+ border-radius: 12px;
983
+ font-size: 0.65rem;
984
+ font-weight: 600;
985
+ text-transform: uppercase;
986
+ letter-spacing: 0.025em;
987
+ border: 1px solid;
988
+ backdrop-filter: blur(10px);
989
+ transition: all 0.2s ease;
990
+ }
991
+
992
+ .status-badge.success {
993
+ background: rgba(16, 185, 129, 0.15);
994
+ color: var(--success-color);
995
+ border-color: rgba(16, 185, 129, 0.3);
996
+ }
997
+
998
+ .status-badge.warning {
999
+ background: rgba(245, 158, 11, 0.15);
1000
+ color: var(--warning-color);
1001
+ border-color: rgba(245, 158, 11, 0.3);
1002
+ }
1003
+
1004
+ .status-badge.info {
1005
+ background: rgba(99, 102, 241, 0.15);
1006
+ color: var(--primary-color);
1007
+ border-color: rgba(99, 102, 241, 0.3);
1008
+ }
1009
+
1010
+ /* Step wrapper states */
1011
+ .step-wrapper.disabled {
1012
+ opacity: 0.5;
1013
+ pointer-events: none;
1014
+ position: relative;
1015
+ }
1016
+
1017
+ .step-wrapper.disabled::before {
1018
+ content: "";
1019
+ position: absolute;
1020
+ top: 0;
1021
+ left: 0;
1022
+ right: 0;
1023
+ bottom: 0;
1024
+ background-color: rgba(0, 0, 0, 0.1);
1025
+ z-index: 1;
1026
+ border-radius: 12px;
1027
+ }
1028
+
1029
+ .step-wrapper.enabled {
1030
+ opacity: 1;
1031
+ pointer-events: auto;
1032
+ }
1033
+
1034
+ /* Disabled step content */
1035
+ .step-wrapper.disabled .btn {
1036
+ pointer-events: none;
1037
+ opacity: 0.6;
1038
+ }
1039
+
1040
+ .step-wrapper.disabled input,
1041
+ .step-wrapper.disabled select,
1042
+ .step-wrapper.disabled textarea {
1043
+ pointer-events: none;
1044
+ opacity: 0.6;
1045
+ }
1046
+
1047
+ .step-wrapper.disabled .file-upload-zone {
1048
+ pointer-events: none;
1049
+ opacity: 0.6;
1050
+ }
1051
+
1052
+ /* Results Section Styles */
1053
+ .results-section {
1054
+ margin-top: 2rem;
1055
+ }
1056
+
1057
+ .generate-new-section {
1058
+ border-top: 1px solid var(--border-color);
1059
+ padding-top: 1.5rem;
1060
+ margin-top: 2rem;
1061
+ }
1062
+
1063
+ .success-details {
1064
+ display: flex;
1065
+ gap: 1rem;
1066
+ flex-wrap: wrap;
1067
+ margin-top: 0.75rem;
1068
+ }
1069
+
1070
+ .success-stat {
1071
+ display: flex;
1072
+ align-items: center;
1073
+ gap: 0.5rem;
1074
+ font-size: 0.875rem;
1075
+ }
1076
+
1077
+ .success-label {
1078
+ color: var(--text-secondary);
1079
+ font-weight: 500;
1080
+ }
1081
+
1082
+ .success-value {
1083
+ color: var(--text-primary);
1084
+ font-weight: 600;
1085
+ padding: 0.25rem 0.5rem;
1086
+ background: rgba(16, 185, 129, 0.1);
1087
+ border-radius: 4px;
1088
+ border: 1px solid rgba(16, 185, 129, 0.2);
1089
+ }
1090
+
1091
+ .download-actions {
1092
+ display: flex;
1093
+ justify-content: center;
1094
+ gap: 1rem;
1095
+ flex-wrap: wrap;
1096
+ }
1097
+
1098
+ .download-btn-primary {
1099
+ font-size: 1rem;
1100
+ padding: 0.875rem 2rem;
1101
+ font-weight: 600;
1102
+ background: linear-gradient(135deg, var(--success-color) 0%, #059669 100%);
1103
+ border: none;
1104
+ color: white;
1105
+ }
1106
+
1107
+ .download-btn-primary:hover {
1108
+ transform: translateY(-2px);
1109
+ box-shadow: var(--shadow-lg);
1110
+ }
1111
+
1112
+ .btn-outline {
1113
+ background: transparent;
1114
+ border: 2px solid var(--border-color);
1115
+ color: var(--text-secondary);
1116
+ padding: 0.75rem 1.5rem;
1117
+ border-radius: 8px;
1118
+ font-weight: 500;
1119
+ cursor: pointer;
1120
+ transition: all 0.2s ease;
1121
+ text-decoration: none;
1122
+ }
1123
+
1124
+ .btn-outline:hover {
1125
+ border-color: var(--primary-color);
1126
+ color: var(--primary-color);
1127
+ background: rgba(59, 130, 246, 0.05);
1128
+ }
1129
+
1130
+ /* Error Section Improvements */
1131
+ .error-section {
1132
+ margin-top: 2rem;
1133
+ }
1134
+
1135
+ .error-details {
1136
+ margin-top: 0.75rem;
1137
+ padding: 0.75rem;
1138
+ background: rgba(239, 68, 68, 0.1);
1139
+ border: 1px solid rgba(239, 68, 68, 0.2);
1140
+ border-radius: 8px;
1141
+ font-size: 0.875rem;
1142
+ }
1143
+
1144
+ .error-help {
1145
+ color: var(--text-secondary);
1146
+ }
1147
+
1148
+ .error-help ul {
1149
+ color: var(--text-secondary);
1150
+ opacity: 0.9;
1151
+ }
1152
+
1153
+ .error-help li {
1154
+ margin-bottom: 0.25rem;
1155
+ }
1156
+
1157
+ /* Generation section improvements */
1158
+ .generation-progress {
1159
+ text-align: center;
1160
+ padding: 2rem;
1161
+ background: var(--bg-card);
1162
+ border-radius: 12px;
1163
+ border: 1px solid var(--border-color);
1164
+ }
1165
+
1166
+ .generation-progress .spinner {
1167
+ margin: 0 auto 1rem;
1168
+ }
1169
+
1170
+ .progress-bar {
1171
+ width: 100%;
1172
+ height: 8px;
1173
+ background: var(--bg-tertiary);
1174
+ border-radius: 4px;
1175
+ overflow: hidden;
1176
+ margin: 1rem 0;
1177
+ }
1178
+
1179
+ .progress-fill {
1180
+ height: 100%;
1181
+ background: linear-gradient(
1182
+ 90deg,
1183
+ var(--primary-color),
1184
+ var(--success-color)
1185
+ );
1186
+ border-radius: 4px;
1187
+ transition: width 0.3s ease;
1188
+ }
src/App.js CHANGED
@@ -1,23 +1,192 @@
1
- import logo from './logo.svg';
2
- import './App.css';
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  function App() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  return (
6
  <div className="App">
7
- <header className="App-header">
8
- <img src={logo} className="App-logo" alt="logo" />
9
- <p>
10
- Edit <code>src/App.js</code> and save to reload.
11
- </p>
12
- <a
13
- className="App-link"
14
- href="https://reactjs.org"
15
- target="_blank"
16
- rel="noopener noreferrer"
17
- >
18
- Learn React
19
- </a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  </header>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  </div>
22
  );
23
  }
 
1
+ import React, { useState, useEffect } from "react";
2
+ import "./App.css";
3
+ import Step1 from "./components/Step1";
4
+ import Step2 from "./components/Step2";
5
+ import Step3 from "./components/Step3";
6
+ import Step4 from "./components/Step4";
7
+ import { config as appConfig, debugLog } from "./utils/config";
8
+ import { ApiService } from "./utils/apiService";
9
+
10
+ // Make ApiService available globally for debugging
11
+ if (process.env.NODE_ENV === "development") {
12
+ window.ApiService = ApiService;
13
+ console.log("[Debug] ApiService is available globally as window.ApiService");
14
+ }
15
 
16
  function App() {
17
+ const [apiKey, setApiKey] = useState("");
18
+ const [isApiKeyValid, setIsApiKeyValid] = useState(false); // Explicitly initialized to false
19
+
20
+ // Debug log when API key validity changes
21
+ useEffect(() => {
22
+ console.log("[App] API key validity changed:", isApiKeyValid);
23
+ }, [isApiKeyValid]);
24
+ const [uploadedFile, setUploadedFile] = useState(null);
25
+ const [s3Link, setS3Link] = useState("");
26
+ const [fileMetadata, setFileMetadata] = useState({
27
+ fileSizeBytes: 0,
28
+ sourceFileRows: 0,
29
+ });
30
+ const [config, setConfig] = useState({
31
+ numRows: appConfig.defaultNumRecords,
32
+ targetColumn: "",
33
+ });
34
+ const [generatedDataLink, setGeneratedDataLink] = useState("");
35
+
36
+ // Check for previously stored and valid API key on app load
37
+ useEffect(() => {
38
+ const checkStoredApiKey = async () => {
39
+ try {
40
+ debugLog("Checking for stored API key on app initialization");
41
+ const storedKeyStatus = await ApiService.verifyStoredApiKey();
42
+ if (storedKeyStatus.valid) {
43
+ setIsApiKeyValid(true);
44
+ debugLog("Valid stored API key found", storedKeyStatus);
45
+ } else {
46
+ setIsApiKeyValid(false);
47
+ debugLog("No valid stored API key", storedKeyStatus);
48
+ }
49
+ } catch (error) {
50
+ debugLog("Error checking stored API key", error);
51
+ setIsApiKeyValid(false);
52
+ }
53
+ };
54
+
55
+ checkStoredApiKey();
56
+ }, []);
57
+
58
+ // Log app initialization in debug mode
59
+ useEffect(() => {
60
+ debugLog("Syncora app initialized", {
61
+ defaultNumRecords: appConfig.defaultNumRecords,
62
+ maxFileSizeMB: appConfig.maxFileSizeMB,
63
+ apiBaseUrl: appConfig.apiBaseUrl,
64
+ });
65
+ }, []);
66
+
67
+ const steps = [
68
+ { number: 1, title: "API Key Validation", component: Step1, icon: "🔑" },
69
+ { number: 2, title: "Upload Files", component: Step2, icon: "📁" },
70
+ { number: 3, title: "Configuration", component: Step3, icon: "⚙️" },
71
+ { number: 4, title: "Generate Data", component: Step4, icon: "🚀" },
72
+ ];
73
+
74
+ // Helper function to check if a step should be enabled
75
+ const isStepEnabled = (stepNumber) => {
76
+ switch (stepNumber) {
77
+ case 1:
78
+ return true; // Step 1 is always enabled
79
+ case 2:
80
+ const step2Enabled = isApiKeyValid;
81
+ debugLog(`Step 2 enabled check: ${step2Enabled}`, {
82
+ isApiKeyValid,
83
+ apiKeyLength: apiKey.length,
84
+ hasApiKey: !!apiKey,
85
+ });
86
+ return step2Enabled; // Step 2 enabled when API key is valid
87
+ case 3:
88
+ return isApiKeyValid && uploadedFile && s3Link; // Step 3 enabled when file is uploaded
89
+ case 4:
90
+ return isApiKeyValid && uploadedFile && s3Link && config.targetColumn; // Step 4 enabled when config is complete
91
+ default:
92
+ return false;
93
+ }
94
+ };
95
+
96
+ const renderStep = (stepNumber) => {
97
+ const step = steps[stepNumber - 1];
98
+ const StepComponent = step.component;
99
+ const enabled = isStepEnabled(stepNumber);
100
+
101
+ return (
102
+ <div className={`step-wrapper ${enabled ? "enabled" : "disabled"}`}>
103
+ <StepComponent
104
+ apiKey={apiKey}
105
+ setApiKey={setApiKey}
106
+ isApiKeyValid={isApiKeyValid}
107
+ setIsApiKeyValid={setIsApiKeyValid}
108
+ uploadedFile={uploadedFile}
109
+ setUploadedFile={setUploadedFile}
110
+ s3Link={s3Link}
111
+ setS3Link={setS3Link}
112
+ fileMetadata={fileMetadata}
113
+ setFileMetadata={setFileMetadata}
114
+ config={config}
115
+ setConfig={setConfig}
116
+ generatedDataLink={generatedDataLink}
117
+ setGeneratedDataLink={setGeneratedDataLink}
118
+ stepNumber={stepNumber}
119
+ stepTitle={step.title}
120
+ stepIcon={step.icon}
121
+ enabled={enabled}
122
+ />
123
+ </div>
124
+ );
125
+ };
126
+
127
  return (
128
  <div className="App">
129
+ <header className="app-header">
130
+ <div className="header-content">
131
+ <div>
132
+ <h1>Syncora</h1>
133
+ <p>AI-Powered Data Generation Platform</p>
134
+ </div>
135
+ <div className="progress-indicator">
136
+ {steps.map((step, index) => {
137
+ const stepNumber = step.number;
138
+ const isEnabled = isStepEnabled(stepNumber);
139
+ const isCompleted =
140
+ stepNumber < steps.length
141
+ ? isStepEnabled(stepNumber + 1)
142
+ : stepNumber === 4 && generatedDataLink;
143
+
144
+ return (
145
+ <div
146
+ key={step.number}
147
+ className={`progress-step ${
148
+ isCompleted
149
+ ? "completed"
150
+ : isEnabled
151
+ ? "active"
152
+ : "disabled"
153
+ }`}
154
+ title={`Step ${step.number}: ${step.title}`}
155
+ />
156
+ );
157
+ })}
158
+ </div>
159
+ </div>
160
  </header>
161
+
162
+ <div className="main-container">
163
+ {/* Left Column */}
164
+ <div className="steps-column">
165
+ {/* Step 1 */}
166
+ <div className="step-content">{renderStep(1)}</div>
167
+
168
+ {/* Step 2 */}
169
+ <div className="step-content">{renderStep(2)}</div>
170
+ </div>
171
+
172
+ {/* Right Column */}
173
+ <div className="steps-column">
174
+ {/* Step 3 */}
175
+ <div className="step-content">{renderStep(3)}</div>
176
+
177
+ {/* Step 4 */}
178
+ <div className="step-content">{renderStep(4)}</div>
179
+ </div>
180
+ </div>
181
+
182
+ {/* Floating Help Button */}
183
+ <button
184
+ className="floating-help"
185
+ onClick={() => window.open("https://docs.syncora.ai/help", "_blank")}
186
+ title="Get Help"
187
+ >
188
+ ?
189
+ </button>
190
  </div>
191
  );
192
  }
src/App.test.js DELETED
@@ -1,8 +0,0 @@
1
- import { render, screen } from '@testing-library/react';
2
- import App from './App';
3
-
4
- test('renders learn react link', () => {
5
- render(<App />);
6
- const linkElement = screen.getByText(/learn react/i);
7
- expect(linkElement).toBeInTheDocument();
8
- });
 
 
 
 
 
 
 
 
 
src/components/DataViewer.css ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Data Viewer Styles */
2
+ .data-viewer {
3
+ width: 100%;
4
+ }
5
+
6
+ .data-viewer.loading {
7
+ display: flex;
8
+ flex-direction: column;
9
+ align-items: center;
10
+ justify-content: center;
11
+ padding: 2rem;
12
+ min-height: 200px;
13
+ }
14
+
15
+ .data-viewer.loading p {
16
+ margin-top: 1rem;
17
+ color: var(--text-secondary);
18
+ }
19
+
20
+ .data-viewer-header {
21
+ display: flex;
22
+ justify-content: space-between;
23
+ align-items: flex-start;
24
+ margin-bottom: 1.5rem;
25
+ gap: 1rem;
26
+ }
27
+
28
+ .data-viewer-header .data-info h4 {
29
+ color: var(--text-primary);
30
+ font-size: 1.1rem;
31
+ font-weight: 600;
32
+ margin-bottom: 0.25rem;
33
+ }
34
+
35
+ .data-viewer-header .data-info p {
36
+ color: var(--text-secondary);
37
+ font-size: 0.875rem;
38
+ }
39
+
40
+ .download-btn {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 0.5rem;
44
+ padding: 0.75rem 1.5rem;
45
+ background: var(--success-color);
46
+ color: white;
47
+ border: none;
48
+ border-radius: 8px;
49
+ font-weight: 500;
50
+ cursor: pointer;
51
+ transition: all 0.2s ease;
52
+ text-decoration: none;
53
+ white-space: nowrap;
54
+ }
55
+
56
+ .download-btn:hover {
57
+ background: #059669;
58
+ transform: translateY(-1px);
59
+ box-shadow: var(--shadow-md);
60
+ }
61
+
62
+ .data-table-container {
63
+ background: var(--bg-card);
64
+ border: 1px solid var(--border-color);
65
+ border-radius: 12px;
66
+ overflow: hidden;
67
+ box-shadow: var(--shadow-md);
68
+ }
69
+
70
+ .data-table {
71
+ width: 100%;
72
+ border-collapse: collapse;
73
+ font-size: 0.875rem;
74
+ }
75
+
76
+ .data-table thead {
77
+ background: var(--bg-tertiary);
78
+ }
79
+
80
+ .data-table th {
81
+ padding: 1rem 0.75rem;
82
+ text-align: left;
83
+ font-weight: 600;
84
+ color: var(--text-primary);
85
+ border-bottom: 1px solid var(--border-color);
86
+ white-space: nowrap;
87
+ overflow: hidden;
88
+ text-overflow: ellipsis;
89
+ max-width: 200px;
90
+ }
91
+
92
+ .data-table td {
93
+ padding: 0.75rem;
94
+ color: var(--text-secondary);
95
+ border-bottom: 1px solid rgba(71, 85, 105, 0.3);
96
+ white-space: nowrap;
97
+ overflow: hidden;
98
+ text-overflow: ellipsis;
99
+ max-width: 200px;
100
+ }
101
+
102
+ .data-table tbody tr:hover {
103
+ background: rgba(59, 130, 246, 0.05);
104
+ }
105
+
106
+ .data-table tbody tr:last-child td {
107
+ border-bottom: none;
108
+ }
109
+
110
+ .pagination {
111
+ display: flex;
112
+ justify-content: center;
113
+ align-items: center;
114
+ gap: 1rem;
115
+ margin-top: 1.5rem;
116
+ padding: 1rem;
117
+ }
118
+
119
+ .pagination-info {
120
+ color: var(--text-secondary);
121
+ font-size: 0.875rem;
122
+ font-weight: 500;
123
+ }
124
+
125
+ .pagination .btn {
126
+ padding: 0.5rem 1rem;
127
+ min-width: auto;
128
+ }
129
+
130
+ .data-viewer.error,
131
+ .data-viewer.empty {
132
+ text-align: center;
133
+ }
134
+
135
+ .download-section {
136
+ text-align: center;
137
+ }
138
+
139
+ /* Additional styles for improved UI */
140
+ .error-actions {
141
+ display: flex;
142
+ justify-content: center;
143
+ gap: 0.75rem;
144
+ flex-wrap: wrap;
145
+ }
146
+
147
+ .error-help ul {
148
+ color: var(--text-secondary);
149
+ opacity: 0.9;
150
+ }
151
+
152
+ .error-help li {
153
+ margin-bottom: 0.25rem;
154
+ }
155
+
156
+ .success-details {
157
+ display: flex;
158
+ gap: 1rem;
159
+ flex-wrap: wrap;
160
+ margin-top: 0.75rem;
161
+ }
162
+
163
+ .success-stat {
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 0.5rem;
167
+ font-size: 0.875rem;
168
+ }
169
+
170
+ .success-label {
171
+ color: var(--text-secondary);
172
+ font-weight: 500;
173
+ }
174
+
175
+ .success-value {
176
+ color: var(--text-primary);
177
+ font-weight: 600;
178
+ padding: 0.25rem 0.5rem;
179
+ background: rgba(16, 185, 129, 0.1);
180
+ border-radius: 4px;
181
+ }
182
+
183
+ .download-actions {
184
+ display: flex;
185
+ justify-content: center;
186
+ gap: 1rem;
187
+ flex-wrap: wrap;
188
+ }
189
+
190
+ .download-btn-primary {
191
+ font-size: 1rem;
192
+ padding: 0.875rem 2rem;
193
+ font-weight: 600;
194
+ }
195
+
196
+ .btn-outline {
197
+ background: transparent;
198
+ border: 2px solid var(--border-color);
199
+ color: var(--text-secondary);
200
+ padding: 0.75rem 1.5rem;
201
+ border-radius: 8px;
202
+ font-weight: 500;
203
+ cursor: pointer;
204
+ transition: all 0.2s ease;
205
+ text-decoration: none;
206
+ }
207
+
208
+ .btn-outline:hover {
209
+ border-color: var(--primary-color);
210
+ color: var(--primary-color);
211
+ background: rgba(59, 130, 246, 0.05);
212
+ }
213
+
214
+ .preview-note {
215
+ border: 1px solid var(--border-color);
216
+ }
217
+
218
+ /* Enhanced error display styles */
219
+ .btn-large {
220
+ padding: 12px 24px !important;
221
+ font-size: 1rem !important;
222
+ font-weight: 600 !important;
223
+ }
224
+
225
+ .success-note {
226
+ animation: fadeIn 0.3s ease-in-out;
227
+ }
228
+
229
+ @keyframes fadeIn {
230
+ from {
231
+ opacity: 0;
232
+ transform: translateY(10px);
233
+ }
234
+ to {
235
+ opacity: 1;
236
+ transform: translateY(0);
237
+ }
238
+ }
239
+
240
+ .error-actions .btn {
241
+ min-width: 180px;
242
+ }
243
+
244
+ .status-message.error .status-message-content h4 {
245
+ color: var(--error-color, #dc3545);
246
+ }
247
+
248
+ .status-message.error .status-message-content p {
249
+ margin-bottom: 0.75rem;
250
+ }
251
+
252
+ /* Responsive data table */
253
+ @media (max-width: 768px) {
254
+ .data-viewer-header {
255
+ flex-direction: column;
256
+ align-items: stretch;
257
+ gap: 1rem;
258
+ }
259
+
260
+ .data-table-container {
261
+ overflow-x: auto;
262
+ }
263
+
264
+ .data-table th,
265
+ .data-table td {
266
+ padding: 0.5rem;
267
+ min-width: 120px;
268
+ }
269
+
270
+ .pagination {
271
+ flex-direction: column;
272
+ gap: 0.5rem;
273
+ }
274
+ }
275
+
276
+ /* Responsive improvements */
277
+ @media (max-width: 568px) {
278
+ .error-actions {
279
+ flex-direction: column;
280
+ align-items: center;
281
+ }
282
+
283
+ .download-actions {
284
+ flex-direction: column;
285
+ align-items: center;
286
+ }
287
+
288
+ .success-details {
289
+ flex-direction: column;
290
+ gap: 0.5rem;
291
+ }
292
+ }
src/components/DataViewer.js ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useCallback } from "react";
2
+ import { debugLog } from "../utils/config";
3
+ import TroubleshootingGuide from "./TroubleshootingGuide";
4
+ import "./DataViewer.css";
5
+
6
+ const DataViewer = ({ s3Url, onDownload, showPreviewOnly = false }) => {
7
+ const [data, setData] = useState([]);
8
+ const [columns, setColumns] = useState([]);
9
+ const [loading, setLoading] = useState(true);
10
+ const [error, setError] = useState(null);
11
+ const [currentPage, setCurrentPage] = useState(1);
12
+ const [rowsPerPage] = useState(10);
13
+
14
+ const fetchData = useCallback(async () => {
15
+ try {
16
+ setLoading(true);
17
+ setError(null);
18
+
19
+ debugLog("Fetching data from S3 URL:", s3Url);
20
+
21
+ // Try multiple approaches to fetch the data
22
+ let response;
23
+
24
+ // Method 1: Direct fetch with CORS
25
+ try {
26
+ response = await fetch(s3Url, {
27
+ method: "GET",
28
+ headers: {
29
+ Accept: "text/csv,text/plain,application/octet-stream,*/*",
30
+ },
31
+ mode: "cors",
32
+ });
33
+ } catch (corsError) {
34
+ debugLog("CORS fetch failed, trying no-cors:", corsError);
35
+
36
+ // Method 2: Try with no-cors mode (limited but might work)
37
+ try {
38
+ response = await fetch(s3Url, {
39
+ method: "GET",
40
+ mode: "no-cors",
41
+ });
42
+ } catch (noCorsError) {
43
+ debugLog("No-cors fetch also failed:", noCorsError);
44
+ throw new Error(
45
+ "Unable to preview data due to CORS restrictions. You can still download the file directly."
46
+ );
47
+ }
48
+ }
49
+
50
+ if (!response.ok && response.status !== 0) {
51
+ // If direct fetch fails, provide helpful error messages
52
+ if (response.status === 403 || response.status === 401) {
53
+ throw new Error(
54
+ "Access denied. The file may require authentication or have expired."
55
+ );
56
+ } else if (response.status === 404) {
57
+ throw new Error(
58
+ "File not found. The download link may have expired."
59
+ );
60
+ } else {
61
+ throw new Error(
62
+ `Unable to fetch data (${response.status}). You can still download the file directly.`
63
+ );
64
+ }
65
+ }
66
+
67
+ // For no-cors responses, we can't read the content
68
+ if (response.type === "opaque") {
69
+ throw new Error(
70
+ "Preview not available due to CORS restrictions. Please download the file to view the data."
71
+ );
72
+ }
73
+
74
+ const csvText = await response.text();
75
+
76
+ if (!csvText || csvText.trim().length === 0) {
77
+ throw new Error("The downloaded file appears to be empty");
78
+ }
79
+
80
+ const parsedData = parseCSV(csvText);
81
+
82
+ if (parsedData.length > 0) {
83
+ setColumns(Object.keys(parsedData[0]));
84
+ setData(parsedData);
85
+ debugLog("Data parsed successfully:", {
86
+ rows: parsedData.length,
87
+ columns: Object.keys(parsedData[0]).length,
88
+ sampleData: parsedData.slice(0, 2),
89
+ });
90
+ } else {
91
+ throw new Error("No valid data rows found in the file");
92
+ }
93
+ } catch (err) {
94
+ console.error("Error fetching data:", err);
95
+ setError(err.message);
96
+ debugLog("Error fetching data:", err);
97
+ } finally {
98
+ setLoading(false);
99
+ }
100
+ }, [s3Url]);
101
+
102
+ useEffect(() => {
103
+ if (s3Url) {
104
+ fetchData();
105
+ }
106
+ }, [s3Url, fetchData]);
107
+
108
+ const parseCSV = (csvText) => {
109
+ try {
110
+ const lines = csvText.trim().split("\n");
111
+ if (lines.length < 2) return [];
112
+
113
+ // Handle different CSV formats and potential quotes
114
+ const parseCSVLine = (line) => {
115
+ const result = [];
116
+ let current = "";
117
+ let inQuotes = false;
118
+
119
+ for (let i = 0; i < line.length; i++) {
120
+ const char = line[i];
121
+
122
+ if (char === '"') {
123
+ inQuotes = !inQuotes;
124
+ } else if (char === "," && !inQuotes) {
125
+ result.push(current.trim());
126
+ current = "";
127
+ } else {
128
+ current += char;
129
+ }
130
+ }
131
+
132
+ result.push(current.trim());
133
+ return result.map((value) => value.replace(/^"(.*)"$/, "$1")); // Remove outer quotes
134
+ };
135
+
136
+ const headers = parseCSVLine(lines[0]);
137
+ const rows = [];
138
+
139
+ for (let i = 1; i < lines.length; i++) {
140
+ if (lines[i].trim() === "") continue; // Skip empty lines
141
+
142
+ const values = parseCSVLine(lines[i]);
143
+ if (values.length > 0 && values.some((val) => val.trim() !== "")) {
144
+ const row = {};
145
+ headers.forEach((header, index) => {
146
+ row[header] = values[index] || "";
147
+ });
148
+ rows.push(row);
149
+ }
150
+ }
151
+
152
+ return rows;
153
+ } catch (err) {
154
+ debugLog("Error parsing CSV:", err);
155
+ throw new Error(
156
+ "Failed to parse CSV data. The file format may be invalid."
157
+ );
158
+ }
159
+ };
160
+
161
+ const getPaginatedData = () => {
162
+ const startIndex = (currentPage - 1) * rowsPerPage;
163
+ const endIndex = startIndex + rowsPerPage;
164
+ return data.slice(startIndex, endIndex);
165
+ };
166
+
167
+ const totalPages = Math.ceil(data.length / rowsPerPage);
168
+
169
+ const handleDownload = () => {
170
+ if (onDownload) {
171
+ onDownload();
172
+ } else {
173
+ window.open(s3Url, "_blank");
174
+ }
175
+ };
176
+
177
+ if (loading) {
178
+ return (
179
+ <div className="data-viewer loading">
180
+ <div className="spinner"></div>
181
+ <p>Loading data preview...</p>
182
+ </div>
183
+ );
184
+ }
185
+
186
+ if (error) {
187
+ return (
188
+ <div className="data-viewer error">
189
+ <div className="status-message error">
190
+ <div className="status-message-icon">❌</div>
191
+ <div className="status-message-content">
192
+ <h4>Unable to Preview Data</h4>
193
+ <p>{error}</p>
194
+ <div
195
+ className="error-help"
196
+ style={{ marginTop: "0.75rem", fontSize: "0.875rem" }}
197
+ >
198
+ <strong>
199
+ Don't worry! Your data has been generated successfully.
200
+ </strong>
201
+ <br />
202
+ <strong>Possible solutions:</strong>
203
+ <ul
204
+ style={{
205
+ marginTop: "0.5rem",
206
+ paddingLeft: "1.5rem",
207
+ textAlign: "left",
208
+ }}
209
+ >
210
+ <li>
211
+ <strong>Download the file directly</strong> using the button
212
+ below
213
+ </li>
214
+ <li>
215
+ The preview may fail due to browser security restrictions
216
+ </li>
217
+ <li>
218
+ Your generated data is still available and ready to download
219
+ </li>
220
+ </ul>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ <div
225
+ className="error-actions"
226
+ style={{ marginTop: "1.5rem", textAlign: "center" }}
227
+ >
228
+ <button
229
+ className="btn btn-primary btn-large"
230
+ onClick={handleDownload}
231
+ style={{
232
+ marginRight: "0.75rem",
233
+ padding: "12px 24px",
234
+ fontSize: "1rem",
235
+ }}
236
+ >
237
+ 📥 Download Generated Data
238
+ </button>
239
+ <button
240
+ className="btn btn-secondary"
241
+ onClick={() => fetchData()}
242
+ style={{ marginLeft: "0.75rem" }}
243
+ >
244
+ 🔄 Try Preview Again
245
+ </button>
246
+ </div>
247
+
248
+ <div
249
+ className="success-note"
250
+ style={{
251
+ marginTop: "1.5rem",
252
+ padding: "1rem",
253
+ background: "var(--success-light, #e8f5e8)",
254
+ borderRadius: "8px",
255
+ border: "1px solid var(--success, #28a745)",
256
+ textAlign: "center",
257
+ }}
258
+ >
259
+ <div
260
+ style={{
261
+ color: "var(--success, #28a745)",
262
+ fontWeight: "bold",
263
+ marginBottom: "0.5rem",
264
+ }}
265
+ >
266
+ ✅ Data Generation Completed Successfully!
267
+ </div>
268
+ <div style={{ fontSize: "0.875rem", color: "var(--text-secondary)" }}>
269
+ The preview failed, but your synthetic data has been generated and
270
+ is ready for download.
271
+ </div>
272
+ </div>
273
+
274
+ <TroubleshootingGuide
275
+ generatedDataLink={s3Url}
276
+ onDownload={handleDownload}
277
+ />
278
+ </div>
279
+ );
280
+ }
281
+
282
+ if (data.length === 0) {
283
+ return (
284
+ <div className="data-viewer empty">
285
+ <div className="status-message warning">
286
+ <div className="status-message-icon">⚠️</div>
287
+ <div className="status-message-content">
288
+ <h4>No Data Available</h4>
289
+ <p>The generated file appears to be empty.</p>
290
+ </div>
291
+ </div>
292
+ <div className="download-section" style={{ marginTop: "1rem" }}>
293
+ <button className="btn btn-primary" onClick={handleDownload}>
294
+ 📥 Download File
295
+ </button>
296
+ </div>
297
+ </div>
298
+ );
299
+ }
300
+
301
+ return (
302
+ <div className="data-viewer">
303
+ <div className="data-viewer-header">
304
+ <div className="data-info">
305
+ <h4>📊 Generated Data {showPreviewOnly ? "Preview" : ""}</h4>
306
+ <p>
307
+ Showing {getPaginatedData().length} of {data.length} rows •{" "}
308
+ {columns.length} columns
309
+ </p>
310
+ </div>
311
+ {!showPreviewOnly && (
312
+ <button
313
+ className="btn btn-success download-btn"
314
+ onClick={handleDownload}
315
+ >
316
+ 📥 Download Complete File
317
+ </button>
318
+ )}
319
+ </div>
320
+
321
+ <div className="data-table-container">
322
+ <table className="data-table">
323
+ <thead>
324
+ <tr>
325
+ {columns.map((column, index) => (
326
+ <th key={index} title={column}>
327
+ {column}
328
+ </th>
329
+ ))}
330
+ </tr>
331
+ </thead>
332
+ <tbody>
333
+ {getPaginatedData().map((row, index) => (
334
+ <tr key={index}>
335
+ {columns.map((column, colIndex) => (
336
+ <td key={colIndex} title={row[column]}>
337
+ {row[column]}
338
+ </td>
339
+ ))}
340
+ </tr>
341
+ ))}
342
+ </tbody>
343
+ </table>
344
+ </div>
345
+
346
+ {totalPages > 1 && (
347
+ <div className="pagination">
348
+ <button
349
+ className="btn btn-secondary"
350
+ onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
351
+ disabled={currentPage === 1}
352
+ >
353
+ ← Previous
354
+ </button>
355
+
356
+ <span className="pagination-info">
357
+ Page {currentPage} of {totalPages}
358
+ </span>
359
+
360
+ <button
361
+ className="btn btn-secondary"
362
+ onClick={() =>
363
+ setCurrentPage((prev) => Math.min(prev + 1, totalPages))
364
+ }
365
+ disabled={currentPage === totalPages}
366
+ >
367
+ Next →
368
+ </button>
369
+ </div>
370
+ )}
371
+
372
+ {showPreviewOnly && data.length > 0 && (
373
+ <div
374
+ className="preview-note"
375
+ style={{
376
+ marginTop: "1rem",
377
+ padding: "0.75rem",
378
+ background: "var(--bg-tertiary)",
379
+ borderRadius: "8px",
380
+ fontSize: "0.875rem",
381
+ color: "var(--text-secondary)",
382
+ textAlign: "center",
383
+ }}
384
+ >
385
+ 💡 Showing first {Math.min(data.length, rowsPerPage * totalPages)}{" "}
386
+ rows. Download the complete file to view all {data.length} rows.
387
+ </div>
388
+ )}
389
+ </div>
390
+ );
391
+ };
392
+
393
+ export default DataViewer;
src/components/Step1.js ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+ import { ApiService } from "../utils/apiService";
3
+ import { debugLog } from "../utils/config";
4
+
5
+ const Step1 = ({
6
+ apiKey,
7
+ setApiKey,
8
+ isApiKeyValid,
9
+ setIsApiKeyValid,
10
+ stepNumber,
11
+ stepTitle,
12
+ stepIcon,
13
+ enabled = true,
14
+ }) => {
15
+ const [isValidating, setIsValidating] = useState(false);
16
+ const [validationMessage, setValidationMessage] = useState("");
17
+
18
+ // Helper function to validate API key format
19
+ const isValidApiKeyFormat = (key) => {
20
+ return key.startsWith("sync_");
21
+ };
22
+
23
+ const validateApiKey = async () => {
24
+ if (!isValidApiKeyFormat(apiKey)) {
25
+ setValidationMessage(
26
+ 'API key must start with "sync_" and be at least 37 characters long'
27
+ );
28
+ setIsApiKeyValid(false);
29
+ return;
30
+ }
31
+
32
+ setIsValidating(true);
33
+ setValidationMessage("");
34
+
35
+ try {
36
+ debugLog("Starting API key validation", {
37
+ apiKey: apiKey.substring(0, 8) + "...",
38
+ encrypted: "Key will be encrypted before transmission",
39
+ });
40
+
41
+ const result = await ApiService.retryRequest(async () => {
42
+ return await ApiService.validateApiKey(apiKey);
43
+ });
44
+
45
+ const isValid = result.success && result.data && result.data.isValid;
46
+ console.log("[Step1] Validation result details:", {
47
+ result,
48
+ isValid,
49
+ success: result.success,
50
+ hasData: !!result.data,
51
+ dataIsValid: result.data?.isValid,
52
+ });
53
+
54
+ setIsApiKeyValid(isValid);
55
+ setValidationMessage(
56
+ isValid
57
+ ? result.message || "API key validated successfully!"
58
+ : result.message || "Invalid API key"
59
+ );
60
+
61
+ debugLog("API key validation completed", {
62
+ valid: isValid,
63
+ response: result,
64
+ });
65
+ } catch (error) {
66
+ debugLog("API key validation failed", error);
67
+ setValidationMessage(
68
+ error.message || "Validation failed. Please try again."
69
+ );
70
+ setIsApiKeyValid(false);
71
+ } finally {
72
+ setIsValidating(false);
73
+ }
74
+ };
75
+
76
+ return (
77
+ <div className="step-container fade-in">
78
+ <div className="step-header">
79
+ <h2>
80
+ <span className="step-number">{stepNumber}</span>
81
+ {stepIcon} {stepTitle}
82
+ </h2>
83
+ <p>
84
+ Enter your Hugging Face API key to get started with synthetic data
85
+ generation. Your key will be securely encrypted before validation.
86
+ </p>
87
+ </div>
88
+
89
+ <div className="step-body">
90
+ <div className="form-group">
91
+ <label htmlFor="apiKey">API Key</label>
92
+ <input
93
+ id="apiKey"
94
+ type="password"
95
+ className="form-input"
96
+ placeholder="sync_xxxxxxxxxxxxxxxxxxxxxxx"
97
+ value={apiKey}
98
+ onChange={(e) => setApiKey(e.target.value)}
99
+ disabled={isValidating}
100
+ />
101
+ <small style={{ color: "var(--text-muted)", fontSize: "0.8rem" }}>
102
+ 💡 Your API key should start with "sync_" and will be encrypted
103
+ </small>
104
+ {apiKey && !isValidApiKeyFormat(apiKey) && (
105
+ <div className="status-message error">
106
+ <div className="status-message-icon">⚠️</div>
107
+ <div className="status-message-content">
108
+ Invalid format. API key must start with "sync_".
109
+ </div>
110
+ </div>
111
+ )}
112
+ </div>
113
+
114
+ <button
115
+ className="btn btn-primary"
116
+ onClick={validateApiKey}
117
+ disabled={!apiKey || isValidating}
118
+ >
119
+ {isValidating ? (
120
+ <>
121
+ <div className="spinner spinner-small"></div>
122
+ Validating API Key...
123
+ </>
124
+ ) : (
125
+ <>🔍 Validate API Key</>
126
+ )}
127
+ </button>
128
+
129
+ {validationMessage && (
130
+ <div
131
+ className={`status-message ${isApiKeyValid ? "success" : "error"}`}
132
+ >
133
+ <div className="status-message-icon">
134
+ {isApiKeyValid ? "✅" : "❌"}
135
+ </div>
136
+ <div className="status-message-content">{validationMessage}</div>
137
+ </div>
138
+ )}
139
+ </div>
140
+ </div>
141
+ );
142
+ };
143
+
144
+ export default Step1;
src/components/Step2.js ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef } from "react";
2
+ import Papa from "papaparse";
3
+ import { ApiService } from "../utils/apiService";
4
+ import { config, debugLog } from "../utils/config";
5
+
6
+ const Step2 = ({
7
+ uploadedFile,
8
+ setUploadedFile,
9
+ s3Link,
10
+ setS3Link,
11
+ fileMetadata,
12
+ setFileMetadata,
13
+ stepNumber,
14
+ stepTitle,
15
+ stepIcon,
16
+ enabled = true,
17
+ // Add API key props for validation
18
+ apiKey,
19
+ isApiKeyValid,
20
+ }) => {
21
+ const [outputFormat, setOutputFormat] = useState("tabular");
22
+ const [isUploading, setIsUploading] = useState(false);
23
+ const [csvData, setCsvData] = useState(null);
24
+ const [dragOver, setDragOver] = useState(false);
25
+ const fileInputRef = useRef(null);
26
+
27
+ const handleFileUpload = async (file) => {
28
+ // Prevent action if step is disabled
29
+ if (!enabled) {
30
+ debugLog("File upload attempted but step is disabled");
31
+ return;
32
+ }
33
+
34
+ if (!file) {
35
+ alert("No file selected");
36
+ return;
37
+ }
38
+
39
+ if (!file.name.endsWith(".csv")) {
40
+ alert("Please upload a CSV file. Only CSV files are supported.");
41
+ return;
42
+ }
43
+
44
+ // Check file size against configured limit
45
+ const maxSizeBytes = config.maxFileSizeMB * 1024 * 1024;
46
+ if (file.size > maxSizeBytes) {
47
+ alert(
48
+ `File size (${(file.size / 1024 / 1024).toFixed(
49
+ 2
50
+ )}MB) exceeds maximum allowed size of ${config.maxFileSizeMB}MB`
51
+ );
52
+ return;
53
+ }
54
+
55
+ // Check if file is empty
56
+ if (file.size === 0) {
57
+ alert(
58
+ "The selected file is empty. Please choose a valid CSV file with data."
59
+ );
60
+ return;
61
+ }
62
+
63
+ setUploadedFile(file);
64
+ debugLog("File selected for upload", {
65
+ name: file.name,
66
+ size: file.size,
67
+ type: file.type,
68
+ });
69
+
70
+ // Initialize file metadata with file size
71
+ setFileMetadata({
72
+ fileSizeBytes: file.size,
73
+ sourceFileRows: 0, // Will be updated after CSV parsing
74
+ });
75
+
76
+ // Parse CSV for preview
77
+ Papa.parse(file, {
78
+ complete: (results) => {
79
+ if (results.errors && results.errors.length > 0) {
80
+ debugLog("CSV parsing warnings", results.errors);
81
+ }
82
+
83
+ if (!results.data || results.data.length === 0) {
84
+ alert(
85
+ "The CSV file appears to be empty or invalid. Please check your file and try again."
86
+ );
87
+ setUploadedFile(null);
88
+ setFileMetadata({ fileSizeBytes: 0, sourceFileRows: 0 });
89
+ return;
90
+ }
91
+
92
+ setCsvData(results.data);
93
+
94
+ // Update file metadata with row count
95
+ setFileMetadata({
96
+ fileSizeBytes: file.size,
97
+ sourceFileRows: results.data.length,
98
+ });
99
+
100
+ debugLog("CSV parsed for preview", {
101
+ rows: results.data.length,
102
+ columns: results.data[0] ? Object.keys(results.data[0]).length : 0,
103
+ });
104
+ },
105
+ header: true,
106
+ skipEmptyLines: true,
107
+ error: (error) => {
108
+ debugLog("CSV parsing error", error);
109
+ alert("Error parsing CSV file. Please ensure it's a valid CSV format.");
110
+ setUploadedFile(null);
111
+ },
112
+ });
113
+
114
+ // Upload to S3 using ApiService
115
+ setIsUploading(true);
116
+ try {
117
+ debugLog("Starting file upload to S3");
118
+
119
+ // Use the ApiService with retry logic
120
+ const result = await ApiService.retryRequest(async () => {
121
+ return await ApiService.uploadFileToS3(file);
122
+ });
123
+
124
+ // Handle different response structures
125
+ const s3Url =
126
+ result.s3_link || result.link || result.publicUrl || result.url;
127
+ setS3Link(s3Url);
128
+ debugLog("File uploaded successfully", {
129
+ s3Link: s3Url,
130
+ result: result,
131
+ });
132
+ } catch (error) {
133
+ debugLog("File upload failed", error);
134
+ console.error("File upload error details:", {
135
+ error: error,
136
+ message: error.message,
137
+ stack: error.stack,
138
+ });
139
+
140
+ // More specific error messages
141
+ let errorMessage = "Failed to upload file. Please try again.";
142
+ if (error.message.includes("credentials")) {
143
+ errorMessage =
144
+ "AWS credentials are not configured properly. Please check your .env.local file and restart the application.";
145
+ } else if (error.message.includes("bucket")) {
146
+ errorMessage =
147
+ "Storage bucket configuration issue. Please check your S3 bucket name in .env.local file.";
148
+ } else if (error.message.includes("size")) {
149
+ errorMessage = `File size exceeds the maximum limit of ${config.maxFileSizeMB}MB.`;
150
+ } else if (
151
+ error.message.includes("network") ||
152
+ error.message.includes("fetch") ||
153
+ error.message.includes("CORS")
154
+ ) {
155
+ errorMessage =
156
+ "Network/CORS error. This might be due to S3 bucket CORS configuration or network connectivity issues.";
157
+ } else if (error.message.includes("AccessDenied")) {
158
+ errorMessage =
159
+ "Access denied to S3 bucket. Please check your AWS permissions.";
160
+ }
161
+
162
+ alert(errorMessage);
163
+ setUploadedFile(null);
164
+ setCsvData(null);
165
+ } finally {
166
+ setIsUploading(false);
167
+ }
168
+ };
169
+
170
+ const handleDrop = (e) => {
171
+ // Prevent action if step is disabled
172
+ if (!enabled) return;
173
+
174
+ e.preventDefault();
175
+ setDragOver(false);
176
+ const files = e.dataTransfer.files;
177
+ if (files.length > 0) {
178
+ handleFileUpload(files[0]);
179
+ }
180
+ };
181
+
182
+ const handleDragOver = (e) => {
183
+ e.preventDefault();
184
+ setDragOver(true);
185
+ };
186
+
187
+ const handleDragLeave = (e) => {
188
+ e.preventDefault();
189
+ setDragOver(false);
190
+ };
191
+
192
+ const renderDataPreview = () => {
193
+ if (!csvData || csvData.length === 0) return null;
194
+
195
+ // Show more rows for better preview (up to 20 rows)
196
+ const previewData = csvData.slice(0, 20);
197
+ const columns = Object.keys(previewData[0]);
198
+
199
+ return (
200
+ <div className="data-preview">
201
+ <h4>
202
+ 📊 Data Preview ({csvData.length} rows total, showing first{" "}
203
+ {previewData.length})
204
+ </h4>
205
+ <div className="data-table-container">
206
+ <table className="data-table-scrollable">
207
+ <thead>
208
+ <tr>
209
+ {columns.map((col, index) => (
210
+ <th key={index}>{col}</th>
211
+ ))}
212
+ </tr>
213
+ </thead>
214
+ <tbody>
215
+ {previewData.map((row, index) => (
216
+ <tr key={index}>
217
+ {columns.map((col, colIndex) => (
218
+ <td key={colIndex} title={row[col]}>
219
+ {row[col]}
220
+ </td>
221
+ ))}
222
+ </tr>
223
+ ))}
224
+ </tbody>
225
+ </table>
226
+ </div>
227
+ {csvData.length > 20 && (
228
+ <p className="preview-note">
229
+ Showing first 20 rows of {csvData.length} total rows
230
+ </p>
231
+ )}
232
+ </div>
233
+ );
234
+ };
235
+
236
+ return (
237
+ <div className="step-container fade-in">
238
+ <div className="step-header">
239
+ <h2>
240
+ <span className="step-number">{stepNumber}</span>
241
+ {stepIcon} {stepTitle}
242
+ </h2>
243
+ <p>
244
+ Choose your output format and upload a CSV file for analysis and
245
+ synthetic data generation.
246
+ </p>
247
+ </div>
248
+
249
+ <div className="step-body">
250
+ {!enabled && (
251
+ <div
252
+ className="status-message warning"
253
+ style={{ marginBottom: "1.5rem" }}
254
+ >
255
+ <div className="status-message-icon">⚠️</div>
256
+ <div className="status-message-content">
257
+ <h4>API Key Required</h4>
258
+ <p>
259
+ Please complete Step 1 (API Key Validation) before uploading
260
+ files.
261
+ </p>
262
+ </div>
263
+ </div>
264
+ )}
265
+
266
+ <div className="form-group output-format-section">
267
+ <label>Output Format</label>
268
+ <div className="output-options">
269
+ <div
270
+ className={`output-option ${
271
+ outputFormat === "tabular" ? "selected" : ""
272
+ }`}
273
+ onClick={() => setOutputFormat("tabular")}
274
+ >
275
+ <h4>📊 Tabular</h4>
276
+ <p>CSV format with structured rows and columns</p>
277
+ </div>
278
+ <div className="output-option disabled" title="Coming Soon">
279
+ <h4>📄 JSONL (Coming Soon)</h4>
280
+ <p>JSON Lines format for advanced use cases</p>
281
+ </div>
282
+ </div>
283
+ </div>
284
+
285
+ <div className="form-group file-upload-section">
286
+ <label>📤 Upload Source File</label>
287
+ {uploadedFile && s3Link ? (
288
+ <div className="file-uploaded">
289
+ <div className="file-info">
290
+ <div className="file-icon">📊</div>
291
+ <div className="file-details">
292
+ <h4>{uploadedFile.name}</h4>
293
+ <p>Successfully uploaded and ready for processing</p>
294
+ <p
295
+ style={{
296
+ fontSize: "0.75rem",
297
+ opacity: 0.8,
298
+ marginTop: "0.25rem",
299
+ }}
300
+ >
301
+ Size: {(uploadedFile.size / 1024 / 1024).toFixed(2)} MB
302
+ </p>
303
+ </div>
304
+ <button
305
+ className="btn btn-secondary"
306
+ onClick={() => {
307
+ setUploadedFile(null);
308
+ setS3Link("");
309
+ setCsvData(null);
310
+ setFileMetadata({ fileSizeBytes: 0, sourceFileRows: 0 });
311
+ }}
312
+ style={{ marginLeft: "auto" }}
313
+ >
314
+ 🔄 Change File
315
+ </button>
316
+ </div>
317
+ </div>
318
+ ) : (
319
+ <div
320
+ className={`file-upload-area ${dragOver ? "drag-over" : ""}`}
321
+ onDrop={handleDrop}
322
+ onDragOver={handleDragOver}
323
+ onDragLeave={handleDragLeave}
324
+ onClick={() => fileInputRef.current?.click()}
325
+ >
326
+ <input
327
+ ref={fileInputRef}
328
+ type="file"
329
+ accept=".csv"
330
+ onChange={(e) => handleFileUpload(e.target.files[0])}
331
+ />
332
+ {isUploading ? (
333
+ <div>
334
+ <div className="spinner"></div>
335
+ <div className="file-upload-text">Uploading your file...</div>
336
+ <div className="file-upload-subtext">
337
+ Please wait while we process your data
338
+ </div>
339
+ </div>
340
+ ) : (
341
+ <div>
342
+ <div className="file-upload-icon">📊</div>
343
+ <div className="file-upload-text">
344
+ Drop your CSV file here
345
+ </div>
346
+ <div className="file-upload-subtext">
347
+ or click to browse • Max {config.maxFileSizeMB}MB • CSV
348
+ files only
349
+ </div>
350
+ </div>
351
+ )}
352
+ </div>
353
+ )}
354
+ </div>
355
+
356
+ {renderDataPreview()}
357
+ </div>
358
+ </div>
359
+ );
360
+ };
361
+
362
+ export default Step2;
src/components/Step3.js ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from "react";
2
+ import Papa from "papaparse";
3
+ import { config as envConfig, debugLog } from "../utils/config";
4
+
5
+ const Step3 = ({
6
+ uploadedFile,
7
+ fileMetadata,
8
+ config,
9
+ setConfig,
10
+ stepNumber,
11
+ stepTitle,
12
+ stepIcon,
13
+ enabled = true,
14
+ // Add validation props
15
+ apiKey,
16
+ isApiKeyValid,
17
+ s3Link,
18
+ }) => {
19
+ const [columns, setColumns] = useState([]);
20
+
21
+ useEffect(() => {
22
+ if (uploadedFile) {
23
+ debugLog("Parsing uploaded file for columns", {
24
+ fileName: uploadedFile.name,
25
+ });
26
+
27
+ // Parse the uploaded file to get column names
28
+ Papa.parse(uploadedFile, {
29
+ complete: (results) => {
30
+ if (results.data.length > 0) {
31
+ const headers = Object.keys(results.data[0]);
32
+ setColumns(headers);
33
+ debugLog("Columns extracted from file", { columns: headers });
34
+
35
+ // Set first column as default target if not already set
36
+ if (!config.targetColumn && headers.length > 0) {
37
+ setConfig((prev) => ({
38
+ ...prev,
39
+ targetColumn: headers[0],
40
+ fileSizeBytes: fileMetadata?.fileSizeBytes || 0,
41
+ sourceFileRows: fileMetadata?.sourceFileRows || 0,
42
+ }));
43
+ }
44
+
45
+ // Set default number of rows from environment config if not already set
46
+ if (!config.numRows || config.numRows === 100) {
47
+ setConfig((prev) => ({
48
+ ...prev,
49
+ numRows: envConfig.defaultNumRecords,
50
+ fileSizeBytes: fileMetadata?.fileSizeBytes || 0,
51
+ sourceFileRows: fileMetadata?.sourceFileRows || 0,
52
+ }));
53
+ debugLog("Set default number of rows", {
54
+ numRows: envConfig.defaultNumRecords,
55
+ });
56
+ }
57
+ }
58
+ },
59
+ header: true,
60
+ skipEmptyLines: true,
61
+ });
62
+ }
63
+ }, [
64
+ uploadedFile,
65
+ config.targetColumn,
66
+ config.numRows,
67
+ setConfig,
68
+ fileMetadata?.fileSizeBytes,
69
+ fileMetadata?.sourceFileRows,
70
+ ]);
71
+
72
+ const handleNumRowsChange = (e) => {
73
+ // Prevent action if step is disabled
74
+ if (!enabled) return;
75
+
76
+ const numRows = parseInt(e.target.value, 10);
77
+ debugLog("Number of rows changed", { numRows });
78
+ setConfig((prev) => ({
79
+ ...prev,
80
+ numRows: numRows,
81
+ fileSizeBytes: fileMetadata?.fileSizeBytes || 0,
82
+ sourceFileRows: fileMetadata?.sourceFileRows || 0,
83
+ }));
84
+ };
85
+
86
+ const handleTargetColumnChange = (e) => {
87
+ // Prevent action if step is disabled
88
+ if (!enabled) return;
89
+
90
+ const targetColumn = e.target.value;
91
+ debugLog("Target column changed", { targetColumn });
92
+ setConfig((prev) => ({
93
+ ...prev,
94
+ targetColumn: targetColumn,
95
+ fileSizeBytes: fileMetadata?.fileSizeBytes || 0,
96
+ sourceFileRows: fileMetadata?.sourceFileRows || 0,
97
+ }));
98
+ };
99
+
100
+ return (
101
+ <div className="step-container fade-in">
102
+ <div className="step-header">
103
+ <h2>
104
+ <span className="step-number">{stepNumber}</span>
105
+ {stepIcon} {stepTitle}
106
+ </h2>
107
+ <p>
108
+ Configure the parameters for your synthetic data generation process
109
+ </p>
110
+ </div>
111
+
112
+ <div className="step-body">
113
+ {!enabled && (
114
+ <div
115
+ className="status-message warning"
116
+ style={{ marginBottom: "1.5rem" }}
117
+ >
118
+ <div className="status-message-icon">⚠️</div>
119
+ <div className="status-message-content">
120
+ <h4>Prerequisites Required</h4>
121
+ <p>
122
+ Please complete the previous steps before configuring data
123
+ generation settings.
124
+ </p>
125
+ <div style={{ marginTop: "0.5rem", fontSize: "0.875rem" }}>
126
+ <div>• {isApiKeyValid ? "✅" : "❌"} API Key Validation</div>
127
+ <div>• {uploadedFile && s3Link ? "✅" : "❌"} File Upload</div>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ )}
132
+
133
+ <div className="config-section">
134
+ <h3>
135
+ <span className="config-icon">📊</span>
136
+ Data Generation Settings
137
+ </h3>
138
+
139
+ <div className="form-group">
140
+ <label htmlFor="numRows">Number of Rows to Generate</label>
141
+ <div className="slider-container">
142
+ <input
143
+ id="numRows"
144
+ type="range"
145
+ min="10"
146
+ max="1000"
147
+ step="10"
148
+ value={config.numRows}
149
+ onChange={handleNumRowsChange}
150
+ className="slider"
151
+ disabled={false}
152
+ />
153
+ <div className="slider-value">{config.numRows} rows</div>
154
+ </div>
155
+ </div>
156
+
157
+ <div className="form-group">
158
+ <label htmlFor="targetColumn">Target Column</label>
159
+ <select
160
+ id="targetColumn"
161
+ className="form-input"
162
+ value={config.targetColumn}
163
+ onChange={handleTargetColumnChange}
164
+ disabled={columns.length === 0}
165
+ >
166
+ <option value="">Select a target column...</option>
167
+ {columns.map((column, index) => (
168
+ <option key={index} value={column}>
169
+ {column}
170
+ </option>
171
+ ))}
172
+ </select>
173
+ </div>
174
+ </div>
175
+
176
+ {columns.length > 0 && (
177
+ <div className="config-section">
178
+ <h3>
179
+ <span className="config-icon">📋</span>
180
+ Available Columns ({columns.length})
181
+ </h3>
182
+ <div
183
+ style={{
184
+ display: "flex",
185
+ flexWrap: "wrap",
186
+ gap: "0.5rem",
187
+ marginTop: "1rem",
188
+ }}
189
+ >
190
+ {columns.map((column, index) => (
191
+ <span
192
+ key={index}
193
+ style={{
194
+ background:
195
+ config.targetColumn === column
196
+ ? "var(--primary-color)"
197
+ : "var(--bg-tertiary)",
198
+ color:
199
+ config.targetColumn === column
200
+ ? "white"
201
+ : "var(--text-secondary)",
202
+ padding: "0.5rem 0.75rem",
203
+ borderRadius: "6px",
204
+ fontSize: "0.8rem",
205
+ fontWeight: "500",
206
+ border:
207
+ config.targetColumn === column
208
+ ? "none"
209
+ : "1px solid var(--border-color)",
210
+ }}
211
+ >
212
+ {column}
213
+ </span>
214
+ ))}
215
+ </div>
216
+ </div>
217
+ )}
218
+ </div>
219
+ </div>
220
+ );
221
+ };
222
+
223
+ export default Step3;
src/components/Step4.js ADDED
@@ -0,0 +1,509 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+ import { ApiService } from "../utils/apiService";
3
+ import { debugLog } from "../utils/config";
4
+ import DataViewer from "./DataViewer";
5
+
6
+ const Step4 = ({
7
+ apiKey,
8
+ s3Link,
9
+ config: generationConfig,
10
+ fileMetadata,
11
+ generatedDataLink,
12
+ setGeneratedDataLink,
13
+ stepNumber,
14
+ stepTitle,
15
+ stepIcon,
16
+ enabled = true,
17
+ }) => {
18
+ const [isGenerating, setIsGenerating] = useState(false);
19
+ const [generationStatus, setGenerationStatus] = useState("");
20
+ const [generationProgress, setGenerationProgress] = useState(0);
21
+ const [hasError, setHasError] = useState(false);
22
+ const [errorMessage, setErrorMessage] = useState("");
23
+
24
+ const generateSyntheticData = async () => {
25
+ // Prevent action if step is disabled
26
+ if (!enabled) {
27
+ debugLog("Generation attempted but step is disabled");
28
+ return;
29
+ }
30
+
31
+ setIsGenerating(true);
32
+ setGenerationStatus("Initializing generation...");
33
+ setGenerationProgress(0);
34
+ setHasError(false);
35
+ setErrorMessage("");
36
+
37
+ try {
38
+ debugLog("Starting synthetic data generation", {
39
+ s3Link,
40
+ config: generationConfig,
41
+ fileMetadata,
42
+ apiKeyPrefix: apiKey.substring(0, 8) + "...",
43
+ });
44
+
45
+ // Use the ApiService with retry logic
46
+ const result = await ApiService.retryRequest(async () => {
47
+ // Update progress during API call
48
+ setGenerationStatus("Sending request to AI model...");
49
+ setGenerationProgress(10);
50
+
51
+ return await ApiService.generateSyntheticData(apiKey, s3Link, {
52
+ ...generationConfig,
53
+ fileSizeBytes: fileMetadata?.fileSizeBytes || 0,
54
+ sourceFileRows: fileMetadata?.sourceFileRows || 0,
55
+ });
56
+ });
57
+
58
+ // If the API returns progress updates, handle them
59
+ if (result.jobId) {
60
+ // For now, simulate progress since polling is not implemented
61
+ await simulateProgress();
62
+ // In a real implementation, you would poll for job status here
63
+ setGeneratedDataLink(
64
+ result.data?.fileUrl ||
65
+ result.fileUrl ||
66
+ result.download_link ||
67
+ result.link ||
68
+ result.s3_url
69
+ );
70
+ } else {
71
+ // Simulate progress for immediate results
72
+ await simulateProgress();
73
+ // Handle the specific response format: { "status": "success", "data": { "fileUrl": "..." } }
74
+ const dataLink =
75
+ result.data?.fileUrl ||
76
+ result.fileUrl ||
77
+ result.s3_url ||
78
+ result.download_link ||
79
+ result.link;
80
+
81
+ if (!dataLink) {
82
+ throw new Error(
83
+ "No download link received from the API. The generation may have failed on the server side."
84
+ );
85
+ }
86
+
87
+ setGeneratedDataLink(dataLink);
88
+ }
89
+
90
+ setGenerationStatus("Generation completed successfully!");
91
+ setGenerationProgress(100);
92
+
93
+ debugLog("Synthetic data generation completed", {
94
+ downloadLink:
95
+ result.data?.fileUrl ||
96
+ result.fileUrl ||
97
+ result.s3_url ||
98
+ result.download_link ||
99
+ result.link,
100
+ status: result.status,
101
+ message: result.message,
102
+ fullResult: result,
103
+ });
104
+ } catch (error) {
105
+ debugLog("Synthetic data generation failed", error);
106
+ setHasError(true);
107
+ setGenerationProgress(0);
108
+
109
+ // Create a user-friendly error message
110
+ let friendlyErrorMessage = "An error occurred during generation.";
111
+
112
+ if (error.message.includes("403") || error.message.includes("401")) {
113
+ friendlyErrorMessage =
114
+ "Authentication failed. Please check your API key and try again.";
115
+ } else if (error.message.includes("404")) {
116
+ friendlyErrorMessage =
117
+ "Generation service not found. Please try again later.";
118
+ } else if (error.message.includes("500")) {
119
+ friendlyErrorMessage =
120
+ "Server error occurred. The service may be temporarily unavailable.";
121
+ } else if (
122
+ error.message.includes("timeout") ||
123
+ error.message.includes("TimeoutError")
124
+ ) {
125
+ friendlyErrorMessage =
126
+ "Request timed out. The generation process may take longer than expected. Please try again.";
127
+ } else if (
128
+ error.message.includes("Network") ||
129
+ error.message.includes("fetch")
130
+ ) {
131
+ friendlyErrorMessage =
132
+ "Network error. Please check your internet connection and try again.";
133
+ } else if (error.message.includes("No download link")) {
134
+ friendlyErrorMessage =
135
+ "Generation completed but no download link was provided. Please try generating again.";
136
+ } else if (error.message) {
137
+ friendlyErrorMessage = error.message;
138
+ }
139
+
140
+ setErrorMessage(friendlyErrorMessage);
141
+ setGenerationStatus(`❌ Generation failed: ${friendlyErrorMessage}`);
142
+ } finally {
143
+ setIsGenerating(false);
144
+ }
145
+ };
146
+
147
+ const simulateProgress = async () => {
148
+ const steps = [
149
+ { progress: 25, message: "Analyzing data structure..." },
150
+ { progress: 50, message: "Training AI model..." },
151
+ { progress: 75, message: "Generating synthetic data..." },
152
+ { progress: 90, message: "Finalizing output..." },
153
+ ];
154
+
155
+ for (const step of steps) {
156
+ setGenerationProgress(step.progress);
157
+ setGenerationStatus(step.message);
158
+ await new Promise((resolve) => setTimeout(resolve, 1500));
159
+ }
160
+ };
161
+
162
+ const handleDownload = () => {
163
+ if (generatedDataLink) {
164
+ debugLog("Downloading generated data", { link: generatedDataLink });
165
+ // Open in new tab so user can see if download works
166
+ const newWindow = window.open(generatedDataLink, "_blank");
167
+
168
+ // If popup blocked, provide fallback
169
+ if (!newWindow) {
170
+ // Fallback: create a temporary download link
171
+ const link = document.createElement("a");
172
+ link.href = generatedDataLink;
173
+ link.download = `synthetic_data_${Date.now()}.csv`;
174
+ document.body.appendChild(link);
175
+ link.click();
176
+ document.body.removeChild(link);
177
+ }
178
+ }
179
+ };
180
+
181
+ const handleCopyLink = async () => {
182
+ if (generatedDataLink) {
183
+ try {
184
+ await navigator.clipboard.writeText(generatedDataLink);
185
+ // You could add a toast notification here
186
+ debugLog("Link copied to clipboard");
187
+ } catch (error) {
188
+ debugLog("Failed to copy link:", error);
189
+ // Fallback for older browsers
190
+ const textArea = document.createElement("textarea");
191
+ textArea.value = generatedDataLink;
192
+ document.body.appendChild(textArea);
193
+ textArea.select();
194
+ document.execCommand("copy");
195
+ document.body.removeChild(textArea);
196
+ }
197
+ }
198
+ };
199
+
200
+ const isReadyForGeneration =
201
+ apiKey &&
202
+ s3Link &&
203
+ generationConfig.targetColumn &&
204
+ generationConfig.numRows;
205
+
206
+ return (
207
+ <div className="step-container fade-in">
208
+ <div className="step-header">
209
+ <h2>
210
+ <span className="step-number">{stepNumber}</span>
211
+ {stepIcon} {stepTitle}
212
+ </h2>
213
+ <p>Generate high-quality synthetic data based on your configuration</p>
214
+ </div>
215
+
216
+ <div className="step-body">
217
+ {!generatedDataLink && !isGenerating && !hasError && (
218
+ <div className="generate-section">
219
+ {!enabled && (
220
+ <div
221
+ className="status-message warning"
222
+ style={{ marginBottom: "1.5rem" }}
223
+ >
224
+ <div className="status-message-icon">⚠️</div>
225
+ <div className="status-message-content">
226
+ <h4>Prerequisites Required</h4>
227
+ <p>
228
+ Please complete all previous steps before generating
229
+ synthetic data.
230
+ </p>
231
+ <div style={{ marginTop: "0.5rem", fontSize: "0.875rem" }}>
232
+ <div>• {apiKey ? "✅" : "❌"} API Key Validation</div>
233
+ <div>• {s3Link ? "✅" : "❌"} File Upload</div>
234
+ <div>
235
+ • {generationConfig.targetColumn ? "✅" : "❌"}{" "}
236
+ Configuration
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </div>
241
+ )}
242
+
243
+ <div
244
+ className={`status-message ${
245
+ isReadyForGeneration ? "info" : "warning"
246
+ }`}
247
+ >
248
+ <div className="status-message-icon">
249
+ {isReadyForGeneration ? "✅" : "⚠️"}
250
+ </div>
251
+ <div className="status-message-content">
252
+ <h4>
253
+ {isReadyForGeneration
254
+ ? "Ready for Generation"
255
+ : "Configuration Required"}
256
+ </h4>
257
+ {!isReadyForGeneration && (
258
+ <p
259
+ style={{
260
+ marginTop: "0.5rem",
261
+ fontSize: "0.875rem",
262
+ opacity: 0.9,
263
+ }}
264
+ >
265
+ Please complete all previous steps before generating data.
266
+ </p>
267
+ )}
268
+ <div className="status-summary">
269
+ <div className="status-row">
270
+ <span className="status-label">🔑 API Key:</span>
271
+ <span
272
+ className={`status-badge ${
273
+ apiKey ? "success" : "warning"
274
+ }`}
275
+ >
276
+ {apiKey ? "✓ Valid" : "⚠ Required"}
277
+ </span>
278
+ </div>
279
+ <div className="status-row">
280
+ <span className="status-label">📊 Data:</span>
281
+ <span
282
+ className={`status-badge ${
283
+ s3Link ? "success" : "warning"
284
+ }`}
285
+ >
286
+ {s3Link ? "✓ Uploaded" : "⚠ Required"}
287
+ </span>
288
+ </div>
289
+ <div className="status-row">
290
+ <span className="status-label">🎯 Target:</span>
291
+ <span
292
+ className={`status-badge ${
293
+ generationConfig.targetColumn ? "success" : "warning"
294
+ }`}
295
+ >
296
+ {generationConfig.targetColumn
297
+ ? `✓ ${generationConfig.targetColumn}`
298
+ : "⚠ Not set"}
299
+ </span>
300
+ </div>
301
+ <div className="status-row">
302
+ <span className="status-label">📥 Source:</span>
303
+ <span className="status-badge info">
304
+ {fileMetadata?.sourceFileRows || 0} rows
305
+ </span>
306
+ </div>
307
+ <div className="status-row">
308
+ <span className="status-label">📤 Generate:</span>
309
+ <span className="status-badge info">
310
+ {generationConfig.numRows} rows
311
+ </span>
312
+ </div>
313
+ </div>
314
+ </div>
315
+ </div>
316
+
317
+ <button
318
+ className="btn btn-primary generate-btn"
319
+ onClick={generateSyntheticData}
320
+ disabled={!isReadyForGeneration || !enabled}
321
+ style={{ marginTop: "1.5rem" }}
322
+ >
323
+ 🎯 Generate Synthetic Data
324
+ </button>
325
+ </div>
326
+ )}
327
+
328
+ {isGenerating && (
329
+ <div className="generation-progress">
330
+ <div className="spinner"></div>
331
+ <div className="file-upload-text" style={{ marginTop: "1rem" }}>
332
+ {generationStatus}
333
+ </div>
334
+ <div className="progress-bar">
335
+ <div
336
+ className="progress-fill"
337
+ style={{ width: `${generationProgress}%` }}
338
+ ></div>
339
+ </div>
340
+ <div
341
+ className="file-upload-subtext"
342
+ style={{ marginTop: "0.75rem" }}
343
+ >
344
+ {generationProgress}% Complete • This may take a few minutes
345
+ </div>
346
+ </div>
347
+ )}
348
+
349
+ {hasError && !isGenerating && (
350
+ <div className="error-section">
351
+ <div className="status-message error">
352
+ <div className="status-message-icon">❌</div>
353
+ <div className="status-message-content">
354
+ <h4>Generation Failed</h4>
355
+ <p>There was an error generating your synthetic data.</p>
356
+ <div className="error-details">
357
+ <strong>Error:</strong> {errorMessage}
358
+ </div>
359
+ <div
360
+ className="error-help"
361
+ style={{ marginTop: "0.75rem", fontSize: "0.875rem" }}
362
+ >
363
+ <strong>What you can try:</strong>
364
+ <ul
365
+ style={{
366
+ marginTop: "0.5rem",
367
+ paddingLeft: "1.5rem",
368
+ textAlign: "left",
369
+ }}
370
+ >
371
+ <li>Check your internet connection and try again</li>
372
+ <li>
373
+ Verify your API key is valid and has sufficient credits
374
+ </li>
375
+ <li>Try reducing the number of rows to generate</li>
376
+ <li>Contact support if the problem persists</li>
377
+ </ul>
378
+ </div>
379
+ </div>
380
+ </div>
381
+
382
+ <div style={{ marginTop: "1.5rem", textAlign: "center" }}>
383
+ <button
384
+ className="btn btn-primary"
385
+ onClick={() => {
386
+ setHasError(false);
387
+ setErrorMessage("");
388
+ setGenerationStatus("");
389
+ setGenerationProgress(0);
390
+ }}
391
+ style={{ marginRight: "1rem" }}
392
+ >
393
+ 🔄 Try Again
394
+ </button>
395
+ <button
396
+ className="btn btn-secondary"
397
+ onClick={() => {
398
+ // Reset to initial state
399
+ setHasError(false);
400
+ setErrorMessage("");
401
+ setGenerationStatus("");
402
+ setGenerationProgress(0);
403
+ setGeneratedDataLink("");
404
+ }}
405
+ >
406
+ 🔙 Start Over
407
+ </button>
408
+ </div>
409
+ </div>
410
+ )}
411
+
412
+ {generatedDataLink && !isGenerating && (
413
+ <div className="results-section">
414
+ <div className="status-message success">
415
+ <div className="status-message-icon">🎉</div>
416
+ <div className="status-message-content">
417
+ <h4>Generation Complete!</h4>
418
+ <p>Your synthetic data has been generated successfully.</p>
419
+ <div
420
+ className="success-details"
421
+ style={{ marginTop: "0.5rem" }}
422
+ >
423
+ <div className="success-stat">
424
+ <span className="success-label">📊 Target Column:</span>
425
+ <span className="success-value">
426
+ {generationConfig.targetColumn}
427
+ </span>
428
+ </div>
429
+ <div className="success-stat">
430
+ <span className="success-label">📈 Rows Generated:</span>
431
+ <span className="success-value">
432
+ {generationConfig.numRows}
433
+ </span>
434
+ </div>
435
+ </div>
436
+ </div>
437
+ </div>
438
+
439
+ <div
440
+ className="download-actions"
441
+ style={{ marginTop: "1.5rem", textAlign: "center" }}
442
+ >
443
+ <button
444
+ className="btn btn-success download-btn-primary"
445
+ onClick={handleDownload}
446
+ style={{ marginRight: "1rem" }}
447
+ >
448
+ 📥 Download Generated Data
449
+ </button>
450
+ <button className="btn btn-outline" onClick={handleCopyLink}>
451
+ 📋 Copy Link
452
+ </button>
453
+ </div>
454
+
455
+ <div className="data-preview-section" style={{ marginTop: "2rem" }}>
456
+ <h4
457
+ style={{ marginBottom: "1rem", color: "var(--text-primary)" }}
458
+ >
459
+ 📊 Data Preview
460
+ </h4>
461
+ <DataViewer
462
+ s3Url={generatedDataLink}
463
+ onDownload={handleDownload}
464
+ showPreviewOnly={true}
465
+ />
466
+
467
+ <div
468
+ className="preview-info"
469
+ style={{
470
+ marginTop: "1rem",
471
+ padding: "0.75rem",
472
+ background: "var(--bg-tertiary)",
473
+ borderRadius: "8px",
474
+ fontSize: "0.875rem",
475
+ color: "var(--text-secondary)",
476
+ textAlign: "center",
477
+ }}
478
+ >
479
+ 💡 If the preview doesn't load, don't worry! Your data has been
480
+ generated successfully. Click the download button above to get
481
+ your file.
482
+ </div>
483
+ </div>
484
+
485
+ <div
486
+ className="generate-new-section"
487
+ style={{ marginTop: "2rem", textAlign: "center" }}
488
+ >
489
+ <button
490
+ className="btn btn-secondary"
491
+ onClick={() => {
492
+ setGeneratedDataLink("");
493
+ setGenerationProgress(0);
494
+ setGenerationStatus("");
495
+ setHasError(false);
496
+ setErrorMessage("");
497
+ }}
498
+ >
499
+ 🔄 Generate New Data
500
+ </button>
501
+ </div>
502
+ </div>
503
+ )}
504
+ </div>
505
+ </div>
506
+ );
507
+ };
508
+
509
+ export default Step4;
src/components/TroubleshootingGuide.js ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+
3
+ const TroubleshootingGuide = ({ generatedDataLink, onDownload }) => {
4
+ const [isExpanded, setIsExpanded] = useState(false);
5
+
6
+ return (
7
+ <div className="troubleshooting-guide" style={{ marginTop: "1rem" }}>
8
+ <button
9
+ className="btn btn-outline btn-small"
10
+ onClick={() => setIsExpanded(!isExpanded)}
11
+ style={{
12
+ display: "flex",
13
+ alignItems: "center",
14
+ gap: "0.5rem",
15
+ fontSize: "0.875rem",
16
+ padding: "0.5rem 1rem",
17
+ }}
18
+ >
19
+ {isExpanded ? "🔽" : "🔍"} Troubleshooting Guide
20
+ </button>
21
+
22
+ {isExpanded && (
23
+ <div
24
+ className="troubleshooting-content"
25
+ style={{
26
+ marginTop: "1rem",
27
+ padding: "1rem",
28
+ background: "var(--bg-tertiary)",
29
+ borderRadius: "8px",
30
+ border: "1px solid var(--border-color)",
31
+ fontSize: "0.875rem",
32
+ lineHeight: "1.6",
33
+ }}
34
+ >
35
+ <h5 style={{ marginBottom: "1rem", color: "var(--text-primary)" }}>
36
+ 🛠️ Common Issues and Solutions
37
+ </h5>
38
+
39
+ <div
40
+ className="troubleshooting-section"
41
+ style={{ marginBottom: "1rem" }}
42
+ >
43
+ <strong>📱 Preview not working?</strong>
44
+ <ul style={{ marginTop: "0.5rem", paddingLeft: "1.5rem" }}>
45
+ <li>
46
+ This is normal! Browser security prevents previewing some files
47
+ </li>
48
+ <li>Your data has been generated successfully</li>
49
+ <li>Click the download button to get your file</li>
50
+ </ul>
51
+ </div>
52
+
53
+ <div
54
+ className="troubleshooting-section"
55
+ style={{ marginBottom: "1rem" }}
56
+ >
57
+ <strong>💾 Download not working?</strong>
58
+ <ul style={{ marginTop: "0.5rem", paddingLeft: "1.5rem" }}>
59
+ <li>Check if your browser blocked popups</li>
60
+ <li>
61
+ Try right-clicking the download button and select "Open in new
62
+ tab"
63
+ </li>
64
+ <li>Copy the link and paste it in a new browser tab</li>
65
+ </ul>
66
+ </div>
67
+
68
+ <div
69
+ className="troubleshooting-section"
70
+ style={{ marginBottom: "1rem" }}
71
+ >
72
+ <strong>🔗 Direct download link:</strong>
73
+ <div
74
+ style={{
75
+ marginTop: "0.5rem",
76
+ padding: "0.5rem",
77
+ background: "var(--bg-secondary)",
78
+ borderRadius: "4px",
79
+ fontFamily: "monospace",
80
+ fontSize: "0.75rem",
81
+ wordBreak: "break-all",
82
+ border: "1px solid var(--border-color)",
83
+ }}
84
+ >
85
+ {generatedDataLink}
86
+ </div>
87
+ <div style={{ marginTop: "0.5rem", textAlign: "center" }}>
88
+ <button
89
+ className="btn btn-small"
90
+ onClick={() => navigator.clipboard.writeText(generatedDataLink)}
91
+ style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}
92
+ >
93
+ 📋 Copy Link
94
+ </button>
95
+ </div>
96
+ </div>
97
+
98
+ <div className="troubleshooting-section">
99
+ <strong>❓ Still having issues?</strong>
100
+ <ul style={{ marginTop: "0.5rem", paddingLeft: "1.5rem" }}>
101
+ <li>Try using a different browser (Chrome, Firefox, Safari)</li>
102
+ <li>Disable browser extensions temporarily</li>
103
+ <li>Check if your antivirus is blocking downloads</li>
104
+ <li>Contact support if problems persist</li>
105
+ </ul>
106
+ </div>
107
+ </div>
108
+ )}
109
+ </div>
110
+ );
111
+ };
112
+
113
+ export default TroubleshootingGuide;
src/index.css CHANGED
@@ -1,13 +1,13 @@
1
  body {
2
  margin: 0;
3
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5
  sans-serif;
6
  -webkit-font-smoothing: antialiased;
7
  -moz-osx-font-smoothing: grayscale;
8
  }
9
 
10
  code {
11
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12
  monospace;
13
  }
 
1
  body {
2
  margin: 0;
3
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4
+ "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5
  sans-serif;
6
  -webkit-font-smoothing: antialiased;
7
  -moz-osx-font-smoothing: grayscale;
8
  }
9
 
10
  code {
11
+ font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12
  monospace;
13
  }
src/index.js CHANGED
@@ -1,17 +1,11 @@
1
- import React from 'react';
2
- import ReactDOM from 'react-dom/client';
3
- import './index.css';
4
- import App from './App';
5
- import reportWebVitals from './reportWebVitals';
6
 
7
- const root = ReactDOM.createRoot(document.getElementById('root'));
8
  root.render(
9
  <React.StrictMode>
10
  <App />
11
  </React.StrictMode>
12
  );
13
-
14
- // If you want to start measuring performance in your app, pass a function
15
- // to log results (for example: reportWebVitals(console.log))
16
- // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17
- reportWebVitals();
 
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import "./index.css";
4
+ import App from "./App";
 
5
 
6
+ const root = ReactDOM.createRoot(document.getElementById("root"));
7
  root.render(
8
  <React.StrictMode>
9
  <App />
10
  </React.StrictMode>
11
  );
 
 
 
 
 
src/logo.svg DELETED
src/reportWebVitals.js DELETED
@@ -1,13 +0,0 @@
1
- const reportWebVitals = onPerfEntry => {
2
- if (onPerfEntry && onPerfEntry instanceof Function) {
3
- import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4
- getCLS(onPerfEntry);
5
- getFID(onPerfEntry);
6
- getFCP(onPerfEntry);
7
- getLCP(onPerfEntry);
8
- getTTFB(onPerfEntry);
9
- });
10
- }
11
- };
12
-
13
- export default reportWebVitals;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/setupTests.js DELETED
@@ -1,5 +0,0 @@
1
- // jest-dom adds custom jest matchers for asserting on DOM nodes.
2
- // allows you to do things like:
3
- // expect(element).toHaveTextContent(/react/i)
4
- // learn more: https://github.com/testing-library/jest-dom
5
- import '@testing-library/jest-dom';
 
 
 
 
 
 
src/utils/apiService.js ADDED
@@ -0,0 +1,739 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ S3Client,
3
+ PutObjectCommand,
4
+ HeadBucketCommand,
5
+ } from "@aws-sdk/client-s3";
6
+ import {
7
+ config,
8
+ getValidationUrl,
9
+ getGenerationUrl,
10
+ debugLog,
11
+ encryptApiKey,
12
+ } from "./config";
13
+
14
+ // Initialize S3 client
15
+ const getS3Client = () => {
16
+ debugLog("Getting S3 client with config:", {
17
+ region: config.awsRegion,
18
+ bucket: config.s3BucketName,
19
+ hasAccessKey: !!config.awsAccessKeyId,
20
+ hasSecretKey: !!config.awsSecretAccessKey,
21
+ });
22
+
23
+ if (!config.awsAccessKeyId || !config.awsSecretAccessKey) {
24
+ const error = new Error(
25
+ "AWS credentials not configured. Please set REACT_APP_AWS_ACCESS_KEY_ID and REACT_APP_AWS_SECRET_ACCESS_KEY"
26
+ );
27
+ console.error("[AWS Error] Missing credentials:", error);
28
+ throw error;
29
+ }
30
+
31
+ try {
32
+ const client = new S3Client({
33
+ region: config.awsRegion,
34
+ credentials: {
35
+ accessKeyId: config.awsAccessKeyId,
36
+ secretAccessKey: config.awsSecretAccessKey,
37
+ },
38
+ });
39
+
40
+ debugLog("S3 client created successfully");
41
+ return client;
42
+ } catch (error) {
43
+ console.error("[AWS Error] Error creating S3 client:", error);
44
+ debugLog("Error creating S3 client:", error);
45
+ throw error;
46
+ }
47
+ };
48
+
49
+ // API utility functions
50
+ export class ApiService {
51
+ // Check AWS S3 connection status
52
+ static async checkAwsConnection() {
53
+ debugLog("Checking AWS S3 connection status");
54
+ debugLog("AWS Configuration:", {
55
+ region: config.awsRegion,
56
+ bucket: config.s3BucketName,
57
+ hasAccessKey: !!config.awsAccessKeyId,
58
+ hasSecretKey: !!config.awsSecretAccessKey,
59
+ });
60
+
61
+ try {
62
+ // Check if AWS credentials are configured
63
+ if (!config.awsAccessKeyId || !config.awsSecretAccessKey) {
64
+ // In development mode, allow bypassing AWS connection requirement
65
+ if (process.env.NODE_ENV === "development") {
66
+ debugLog(
67
+ "Development mode: AWS credentials not configured, but allowing bypass"
68
+ );
69
+ return {
70
+ connected: true, // Allow development without AWS
71
+ status: "warning",
72
+ message: "Development mode: AWS configuration bypassed",
73
+ details: "AWS credentials not configured - using development mode",
74
+ development: true,
75
+ debug: {
76
+ hasAccessKey: !!config.awsAccessKeyId,
77
+ hasSecretKey: !!config.awsSecretAccessKey,
78
+ region: config.awsRegion,
79
+ bucket: config.s3BucketName,
80
+ },
81
+ };
82
+ }
83
+
84
+ return {
85
+ connected: false,
86
+ status: "error",
87
+ message: "AWS credentials not configured",
88
+ details: "Missing AWS Access Key ID or Secret Access Key",
89
+ debug: {
90
+ hasAccessKey: !!config.awsAccessKeyId,
91
+ hasSecretKey: !!config.awsSecretAccessKey,
92
+ region: config.awsRegion,
93
+ bucket: config.s3BucketName,
94
+ },
95
+ };
96
+ }
97
+
98
+ // Validate AWS Access Key format
99
+ if (!config.awsAccessKeyId.startsWith("AKIA")) {
100
+ console.warn(
101
+ "[AWS Connection] Access Key does not start with AKIA - this might be invalid"
102
+ );
103
+ }
104
+
105
+ // Validate Secret Key length (should be 40 characters)
106
+ if (config.awsSecretAccessKey.length !== 40) {
107
+ console.warn(
108
+ "[AWS Connection] Secret Key length is not 40 characters - this might be invalid"
109
+ );
110
+ }
111
+
112
+ if (!config.s3BucketName) {
113
+ return {
114
+ connected: false,
115
+ status: "error",
116
+ message: "S3 bucket not configured",
117
+ details: "Missing S3 bucket name configuration",
118
+ debug: {
119
+ bucket: config.s3BucketName,
120
+ region: config.awsRegion,
121
+ },
122
+ };
123
+ }
124
+
125
+ debugLog("Initializing S3 client with credentials...");
126
+
127
+ // Initialize S3 client and test connection
128
+ const s3Client = getS3Client();
129
+
130
+ debugLog("Testing S3 connection with HeadBucket operation...");
131
+
132
+ // Use HeadBucket operation to test connectivity and permissions
133
+ const headBucketCommand = new HeadBucketCommand({
134
+ Bucket: config.s3BucketName,
135
+ });
136
+
137
+ const startTime = Date.now();
138
+
139
+ try {
140
+ await s3Client.send(headBucketCommand);
141
+ const endTime = Date.now();
142
+
143
+ debugLog("AWS S3 connection successful", {
144
+ bucket: config.s3BucketName,
145
+ region: config.awsRegion,
146
+ responseTime: `${endTime - startTime}ms`,
147
+ });
148
+ } catch (headBucketError) {
149
+ // If HeadBucket fails due to CORS, it might still work for file uploads
150
+ // Let's check if it's a CORS error specifically
151
+ if (
152
+ headBucketError.message &&
153
+ headBucketError.message.includes("CORS")
154
+ ) {
155
+ debugLog(
156
+ "CORS error detected - this is common for browser S3 access"
157
+ );
158
+
159
+ return {
160
+ connected: true, // We'll mark as connected but with a warning
161
+ status: "warning",
162
+ message: "AWS S3 accessible with CORS limitations",
163
+ details:
164
+ "HeadBucket operation blocked by CORS, but file uploads should work",
165
+ bucket: config.s3BucketName,
166
+ region: config.awsRegion,
167
+ corsWarning: true,
168
+ };
169
+ }
170
+
171
+ // Re-throw if it's not a CORS issue
172
+ throw headBucketError;
173
+ }
174
+
175
+ return {
176
+ connected: true,
177
+ status: "success",
178
+ message: "AWS S3 connected successfully",
179
+ details: `Connected to bucket: ${config.s3BucketName} in ${config.awsRegion}`,
180
+ bucket: config.s3BucketName,
181
+ region: config.awsRegion,
182
+ };
183
+ } catch (error) {
184
+ debugLog("AWS S3 connection failed", error);
185
+ debugLog("Error details:", {
186
+ name: error.name,
187
+ message: error.message,
188
+ code: error.code,
189
+ statusCode: error.$metadata?.httpStatusCode,
190
+ requestId: error.$metadata?.requestId,
191
+ });
192
+
193
+ let message = "AWS S3 connection failed";
194
+ let details = error.message;
195
+
196
+ // Provide more specific error messages based on error type
197
+ if (error.name === "CredentialsProviderError") {
198
+ message = "Invalid AWS credentials";
199
+ details = "Check your AWS Access Key ID and Secret Access Key";
200
+ } else if (error.name === "NoSuchBucket") {
201
+ message = "S3 bucket not found";
202
+ details = `Bucket '${config.s3BucketName}' does not exist or is not accessible`;
203
+ } else if (error.name === "AccessDenied" || error.name === "Forbidden") {
204
+ message = "Access denied to S3 bucket";
205
+ details = "Check your AWS permissions for S3 operations";
206
+ } else if (
207
+ error.name === "NetworkingError" ||
208
+ error.message.includes("fetch") ||
209
+ error.name === "TypeError" ||
210
+ error.message.includes("CORS") ||
211
+ error.code === "NetworkingError"
212
+ ) {
213
+ message = "Network/CORS connection failed";
214
+ details =
215
+ "This is likely a CORS issue. The bucket exists but browser access is restricted. File uploads might still work.";
216
+ } else if (error.name === "TimeoutError") {
217
+ message = "Connection timeout";
218
+ details = "AWS S3 connection timed out";
219
+ } else if (error.code === "InvalidAccessKeyId") {
220
+ message = "Invalid AWS Access Key ID";
221
+ details = "The AWS Access Key ID you provided does not exist";
222
+ } else if (error.code === "SignatureDoesNotMatch") {
223
+ message = "Invalid AWS Secret Access Key";
224
+ details = "The AWS Secret Access Key you provided is incorrect";
225
+ }
226
+
227
+ return {
228
+ connected: false,
229
+ status: "error",
230
+ message,
231
+ details,
232
+ error: error.name || "Unknown error",
233
+ debug: {
234
+ errorCode: error.code,
235
+ errorName: error.name,
236
+ httpStatusCode: error.$metadata?.httpStatusCode,
237
+ requestId: error.$metadata?.requestId,
238
+ bucket: config.s3BucketName,
239
+ region: config.awsRegion,
240
+ },
241
+ };
242
+ }
243
+ }
244
+
245
+ // Retry mechanism for API requests
246
+ static async retryRequest(requestFunction, maxRetries = config.maxRetries) {
247
+ let lastError = null;
248
+
249
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
250
+ try {
251
+ debugLog(`API request attempt ${attempt}/${maxRetries}`);
252
+ const result = await requestFunction();
253
+ return result;
254
+ } catch (error) {
255
+ lastError = error;
256
+ debugLog(`API request attempt ${attempt} failed`, error);
257
+
258
+ if (attempt < maxRetries) {
259
+ // Wait before retrying (exponential backoff)
260
+ const waitTime = Math.pow(2, attempt - 1) * 1000;
261
+ debugLog(`Retrying in ${waitTime}ms...`);
262
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
263
+ }
264
+ }
265
+ }
266
+
267
+ throw lastError;
268
+ }
269
+
270
+ static async validateApiKey(apiKey) {
271
+ debugLog("Validating API key", {
272
+ keyPrefix: apiKey.substring(0, 8) + "...",
273
+ });
274
+
275
+ try {
276
+ // Encrypt the API key before sending for validation
277
+ const encryptedApiKey = encryptApiKey(apiKey);
278
+
279
+ const response = await fetch(getValidationUrl(), {
280
+ method: "POST",
281
+ headers: {
282
+ "Content-Type": "application/json",
283
+ },
284
+ body: JSON.stringify({ apiKey: encryptedApiKey }),
285
+ signal: AbortSignal.timeout(config.apiTimeout * 1000), // Convert to milliseconds
286
+ });
287
+
288
+ const result = await response.json();
289
+ debugLog("API key validation result", result);
290
+
291
+ // Handle the new API response format
292
+ if (response.ok && result.status === "success") {
293
+ // If validation is successful, store the encrypted key for future use
294
+ if (result.data && result.data.isValid) {
295
+ sessionStorage.setItem("encryptedApiKey", encryptedApiKey);
296
+ return {
297
+ success: true,
298
+ valid: true,
299
+ isValid: true,
300
+ message: result.message || "Api Credentials Validated Successfully",
301
+ data: result.data,
302
+ };
303
+ }
304
+ }
305
+
306
+ // Handle error responses or invalid API keys
307
+ if (result.status === "error") {
308
+ // Remove any stored invalid key
309
+ sessionStorage.removeItem("encryptedApiKey");
310
+ return {
311
+ success: false,
312
+ valid: false,
313
+ isValid: false,
314
+ message: result.message || "Invalid or revoked API key",
315
+ error: true,
316
+ };
317
+ }
318
+
319
+ // Fallback for unexpected response format
320
+ throw new Error(
321
+ result.message ||
322
+ `Validation failed: ${response.status} ${response.statusText}`
323
+ );
324
+ } catch (error) {
325
+ debugLog("API key validation error", error);
326
+
327
+ // Handle network errors - for development, allow bypass with proper format
328
+ if (error.name === "TypeError" && error.message.includes("fetch")) {
329
+ debugLog(
330
+ "Network error detected, using fallback validation for development"
331
+ );
332
+
333
+ // Simple validation for development - check if it's a valid sync_ token format
334
+ if (apiKey.startsWith("sync_") && apiKey.length > 20) {
335
+ const encryptedApiKey = encryptApiKey(apiKey);
336
+ sessionStorage.setItem("encryptedApiKey", encryptedApiKey);
337
+
338
+ return {
339
+ success: true,
340
+ valid: true,
341
+ isValid: true,
342
+ message: "API key format valid (offline validation)",
343
+ data: { isValid: true },
344
+ };
345
+ } else {
346
+ // Remove any stored invalid key
347
+ sessionStorage.removeItem("encryptedApiKey");
348
+ return {
349
+ success: false,
350
+ valid: false,
351
+ isValid: false,
352
+ message:
353
+ "Invalid API key format. Key must start with 'sync_' and be at least 20 characters long.",
354
+ error: true,
355
+ };
356
+ }
357
+ }
358
+
359
+ // Handle other network errors for development
360
+ if (
361
+ error.message.includes("Failed to fetch") ||
362
+ error.name === "TypeError"
363
+ ) {
364
+ debugLog(
365
+ "Network connection error, using fallback validation for development"
366
+ );
367
+
368
+ // Simple validation for development - check if it's a valid sync_ token format
369
+ if (apiKey.startsWith("sync_") && apiKey.length > 20) {
370
+ const encryptedApiKey = encryptApiKey(apiKey);
371
+ sessionStorage.setItem("encryptedApiKey", encryptedApiKey);
372
+
373
+ return {
374
+ success: true,
375
+ valid: true,
376
+ isValid: true,
377
+ message:
378
+ "API key format valid (offline validation - server not available)",
379
+ data: { isValid: true },
380
+ };
381
+ } else {
382
+ // Remove any stored invalid key
383
+ sessionStorage.removeItem("encryptedApiKey");
384
+ return {
385
+ success: false,
386
+ valid: false,
387
+ isValid: false,
388
+ message:
389
+ "Invalid API key format. Key must start with 'sync_' and be at least 20 characters long.",
390
+ error: true,
391
+ };
392
+ }
393
+ }
394
+
395
+ // Network or other errors
396
+ if (error.name === "AbortError") {
397
+ throw new Error("Request timeout: API validation took too long");
398
+ }
399
+
400
+ throw error;
401
+ }
402
+ }
403
+
404
+ static async uploadFileToS3(file) {
405
+ debugLog("Uploading file to S3", { fileName: file.name, size: file.size });
406
+
407
+ // Check file size
408
+ const maxSizeBytes = config.maxFileSizeMB * 1024 * 1024;
409
+ if (file.size > maxSizeBytes) {
410
+ throw new Error(
411
+ `File size exceeds maximum allowed size of ${config.maxFileSizeMB}MB`
412
+ );
413
+ }
414
+
415
+ // In development mode, if AWS credentials are not configured, simulate upload
416
+ if (
417
+ process.env.NODE_ENV === "development" &&
418
+ (!config.awsAccessKeyId || !config.awsSecretAccessKey)
419
+ ) {
420
+ debugLog(
421
+ "Development mode: Simulating S3 upload without actual AWS credentials"
422
+ );
423
+
424
+ // Generate a mock S3 URL for development
425
+ const timestamp = Date.now();
426
+ const fileName = `uploads/${timestamp}-${file.name}`;
427
+ const mockUrl = `https://mock-bucket.s3.mock-region.amazonaws.com/${fileName}`;
428
+
429
+ // Simulate upload delay
430
+ await new Promise((resolve) => setTimeout(resolve, 1000));
431
+
432
+ return {
433
+ success: true,
434
+ s3_link: mockUrl,
435
+ link: mockUrl,
436
+ publicUrl: mockUrl,
437
+ url: mockUrl,
438
+ s3Key: fileName,
439
+ etag: `"mock-etag-${timestamp}"`,
440
+ bucket: "mock-bucket",
441
+ region: "mock-region",
442
+ development: true,
443
+ message: "Development mode: Upload simulated successfully",
444
+ };
445
+ }
446
+
447
+ try {
448
+ // Initialize S3 client
449
+ const s3Client = getS3Client();
450
+
451
+ // Generate unique filename
452
+ const timestamp = Date.now();
453
+ const fileName = `uploads/${timestamp}-${file.name}`;
454
+
455
+ // Convert file to ArrayBuffer for compatibility with AWS SDK
456
+ const fileBuffer = await file.arrayBuffer();
457
+
458
+ // Create upload command
459
+ const uploadCommand = new PutObjectCommand({
460
+ Bucket: config.s3BucketName,
461
+ Key: fileName,
462
+ Body: fileBuffer,
463
+ ContentType: file.type,
464
+ ACL: "public-read", // Make the uploaded file publicly accessible
465
+ Metadata: {
466
+ "original-name": file.name,
467
+ "upload-timestamp": timestamp.toString(),
468
+ },
469
+ });
470
+
471
+ debugLog("Starting S3 upload", {
472
+ bucket: config.s3BucketName,
473
+ key: fileName,
474
+ contentType: file.type,
475
+ fileSize: file.size,
476
+ bufferSize: fileBuffer.byteLength,
477
+ });
478
+
479
+ // Upload to S3
480
+ const result = await s3Client.send(uploadCommand);
481
+
482
+ // Construct public URL
483
+ const publicUrl = `https://${config.s3BucketName}.s3.${config.awsRegion}.amazonaws.com/${fileName}`;
484
+
485
+ debugLog("File upload result", {
486
+ etag: result.ETag,
487
+ publicUrl: publicUrl,
488
+ });
489
+
490
+ return {
491
+ success: true,
492
+ s3_link: publicUrl,
493
+ link: publicUrl,
494
+ publicUrl: publicUrl,
495
+ url: publicUrl,
496
+ s3Key: fileName,
497
+ etag: result.ETag,
498
+ bucket: config.s3BucketName,
499
+ region: config.awsRegion,
500
+ };
501
+ } catch (error) {
502
+ debugLog("File upload error", error);
503
+
504
+ // Provide more specific error messages
505
+ if (error.name === "CredentialsProviderError") {
506
+ throw new Error(
507
+ "AWS credentials are invalid or not configured properly"
508
+ );
509
+ } else if (error.name === "NoSuchBucket") {
510
+ throw new Error(
511
+ `S3 bucket '${config.s3BucketName}' does not exist or is not accessible`
512
+ );
513
+ } else if (error.name === "AccessDenied") {
514
+ throw new Error(
515
+ "Access denied. Check your AWS permissions for S3 operations"
516
+ );
517
+ } else {
518
+ throw new Error(`Upload failed: ${error.message || "Unknown error"}`);
519
+ }
520
+ }
521
+ }
522
+
523
+ static async verifyStoredApiKey() {
524
+ try {
525
+ const encryptedApiKey = sessionStorage.getItem("encryptedApiKey");
526
+ if (!encryptedApiKey) {
527
+ return {
528
+ valid: false,
529
+ message: "No stored API key found",
530
+ };
531
+ }
532
+
533
+ // Actually verify the stored API key by making a validation call
534
+ debugLog("Verifying stored API key");
535
+
536
+ try {
537
+ const validationResponse = await fetch(getValidationUrl(), {
538
+ method: "POST",
539
+ headers: {
540
+ "Content-Type": "application/json",
541
+ },
542
+ body: JSON.stringify({
543
+ encryptedApiKey: encryptedApiKey,
544
+ }),
545
+ });
546
+
547
+ const result = await validationResponse.json();
548
+
549
+ if (result.success && result.data && result.data.isValid) {
550
+ return {
551
+ valid: true,
552
+ message: "Stored API key is valid",
553
+ encryptedKey: encryptedApiKey,
554
+ };
555
+ } else {
556
+ // Remove invalid stored key
557
+ sessionStorage.removeItem("encryptedApiKey");
558
+ return {
559
+ valid: false,
560
+ message: "Stored API key is invalid",
561
+ };
562
+ }
563
+ } catch (validationError) {
564
+ debugLog("Error validating stored API key", validationError);
565
+ // On validation error, assume key might be invalid and remove it
566
+ sessionStorage.removeItem("encryptedApiKey");
567
+ return {
568
+ valid: false,
569
+ message: "Could not validate stored API key",
570
+ error: validationError.message,
571
+ };
572
+ }
573
+ } catch (error) {
574
+ debugLog("Error verifying stored API key", error);
575
+ return {
576
+ valid: false,
577
+ message: "Error verifying stored API key",
578
+ error: error.message,
579
+ };
580
+ }
581
+ }
582
+
583
+ static async generateSyntheticData(apiKey, s3Link, generationConfig) {
584
+ debugLog("Generating synthetic data", { s3Link, config: generationConfig });
585
+
586
+ try {
587
+ // Get encrypted API key from session storage or encrypt the provided key
588
+ let encryptedApiKey = sessionStorage.getItem("encryptedApiKey");
589
+ if (!encryptedApiKey) {
590
+ encryptedApiKey = encryptApiKey(apiKey);
591
+ }
592
+
593
+ const response = await fetch(getGenerationUrl(), {
594
+ method: "POST",
595
+ headers: {
596
+ "Content-Type": "application/json",
597
+ "x-api-key": encryptedApiKey,
598
+ },
599
+ body: JSON.stringify({
600
+ fileUrl: s3Link,
601
+ type: "Tabular",
602
+ numberOfRows: generationConfig.numRows || config.defaultNumRecords,
603
+ targetColumn: generationConfig.targetColumn,
604
+ fileSizeBytes: generationConfig.fileSizeBytes || 0,
605
+ sourceFileRows: generationConfig.sourceFileRows || 0,
606
+ }),
607
+ signal: AbortSignal.timeout(config.apiTimeout * 1000),
608
+ });
609
+
610
+ if (!response.ok) {
611
+ throw new Error(
612
+ `Generation failed: ${response.status} ${response.statusText}`
613
+ );
614
+ }
615
+
616
+ const result = await response.json();
617
+ debugLog("Data generation result", result);
618
+ return result;
619
+ } catch (error) {
620
+ debugLog("Data generation error", error);
621
+ throw error;
622
+ }
623
+ }
624
+
625
+ // Check AWS credentials - equivalent to Python check_aws_credentials function
626
+ static async checkAwsCredentials() {
627
+ /**
628
+ * Check if AWS credentials are valid
629
+ *
630
+ * Returns:
631
+ * Object: Status dictionary with 'valid' boolean and 'message' string
632
+ */
633
+ debugLog("Checking AWS credentials validity");
634
+
635
+ // Check if credentials are configured
636
+ if (!config.awsAccessKeyId || !config.awsSecretAccessKey) {
637
+ // In development mode, allow bypassing AWS credentials requirement
638
+ if (process.env.NODE_ENV === "development") {
639
+ debugLog(
640
+ "Development mode: AWS credentials not configured, but allowing bypass"
641
+ );
642
+ return {
643
+ valid: true,
644
+ connected: true,
645
+ message: "Development mode: AWS configuration bypassed",
646
+ development: true,
647
+ };
648
+ }
649
+
650
+ return {
651
+ valid: false,
652
+ connected: false,
653
+ message: "Cloud storage credentials not configured.",
654
+ };
655
+ }
656
+
657
+ // Check if bucket is configured
658
+ if (!config.s3BucketName) {
659
+ return {
660
+ valid: false,
661
+ connected: false,
662
+ message: "Cloud storage not configured.",
663
+ };
664
+ }
665
+
666
+ // Try to get S3 client
667
+ let s3Client;
668
+ try {
669
+ s3Client = getS3Client();
670
+ } catch (error) {
671
+ return {
672
+ valid: false,
673
+ connected: false,
674
+ message: "Cloud storage connection unavailable.",
675
+ };
676
+ }
677
+
678
+ // Check if bucket exists and is accessible
679
+ try {
680
+ const headBucketCommand = new HeadBucketCommand({
681
+ Bucket: config.s3BucketName,
682
+ });
683
+
684
+ await s3Client.send(headBucketCommand);
685
+
686
+ return {
687
+ valid: true,
688
+ connected: true,
689
+ message: "Cloud storage connected",
690
+ };
691
+ } catch (error) {
692
+ debugLog("HeadBucket operation failed:", error);
693
+
694
+ // Handle different error types similar to Python ClientError handling
695
+ if (
696
+ error.name === "NoSuchBucket" ||
697
+ error.$metadata?.httpStatusCode === 404
698
+ ) {
699
+ return {
700
+ valid: false,
701
+ connected: false,
702
+ message: "Storage location not found",
703
+ error: "Storage not found",
704
+ };
705
+ } else if (
706
+ error.name === "Forbidden" ||
707
+ error.$metadata?.httpStatusCode === 403
708
+ ) {
709
+ return {
710
+ valid: false,
711
+ connected: false,
712
+ message: "Storage access denied",
713
+ error: "Access denied",
714
+ };
715
+ } else if (
716
+ error.message &&
717
+ error.message.toLowerCase().includes("cors")
718
+ ) {
719
+ // Handle CORS errors specially - this is common in browser environments
720
+ debugLog("CORS error detected, but credentials may still be valid");
721
+ return {
722
+ valid: true,
723
+ connected: true,
724
+ message: "Cloud storage connected (CORS limitations)",
725
+ warning: "CORS restrictions apply in browser environment",
726
+ };
727
+ } else {
728
+ return {
729
+ valid: false,
730
+ connected: false,
731
+ message: "Storage connection error",
732
+ error: "Connection error",
733
+ };
734
+ }
735
+ }
736
+ }
737
+ }
738
+
739
+ export default ApiService;
src/utils/config.js ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import CryptoJS from "crypto-js";
2
+
3
+ // Fallback encryption keys for development (should be environment variables in production)
4
+ const AES_SECRET_KEY =
5
+ process.env.REACT_APP_AES_SECRET_KEY ||
6
+ "2b7e151628aed2a6abf7158809cf4f3c2b7e151628aed2a6abf7158809cf4f3c";
7
+ const AES_IV =
8
+ process.env.REACT_APP_AES_IV || "000102030405060708090a0b0c0d0e0f";
9
+
10
+ // Encryption utilities
11
+ export function encryptApiKey(rawKey) {
12
+ const key = CryptoJS.enc.Hex.parse(AES_SECRET_KEY);
13
+ const iv = CryptoJS.enc.Hex.parse(AES_IV);
14
+ const encrypted = CryptoJS.AES.encrypt(rawKey, key, {
15
+ iv,
16
+ mode: CryptoJS.mode.CBC,
17
+ padding: CryptoJS.pad.Pkcs7,
18
+ });
19
+ return encrypted.toString(); // base64
20
+ }
21
+
22
+ export function decryptApiKey(encryptedKey) {
23
+ const key = CryptoJS.enc.Hex.parse(AES_SECRET_KEY);
24
+ const iv = CryptoJS.enc.Hex.parse(AES_IV);
25
+ const decrypted = CryptoJS.AES.decrypt(encryptedKey, key, {
26
+ iv,
27
+ mode: CryptoJS.mode.CBC,
28
+ padding: CryptoJS.pad.Pkcs7,
29
+ });
30
+ return decrypted.toString(CryptoJS.enc.Utf8);
31
+ }
32
+
33
+ // Application configuration
34
+ export const config = {
35
+ apiBaseUrl:
36
+ process.env.REACT_APP_API_BASE_URL || "http://localhost:8000/api/v1",
37
+ apiTimeout: parseInt(process.env.REACT_APP_API_TIMEOUT) || 30,
38
+ maxRetries: parseInt(process.env.REACT_APP_MAX_RETRIES) || 3,
39
+
40
+ // AWS S3 Configuration
41
+ awsRegion: process.env.REACT_APP_AWS_REGION || "ap-south-1",
42
+ s3BucketName: process.env.REACT_APP_S3_BUCKET_NAME || "test-bucket-name",
43
+ awsAccessKeyId:
44
+ process.env.REACT_APP_AWS_ACCESS_KEY_ID ||
45
+ process.env.REACT_APP_AWS_ACCESS_KEY,
46
+ awsSecretAccessKey:
47
+ process.env.REACT_APP_AWS_SECRET_ACCESS_KEY ||
48
+ process.env.REACT_APP_AWS_SECRET_KEY,
49
+
50
+ // Application Settings
51
+ defaultNumRecords: parseInt(process.env.REACT_APP_DEFAULT_NUM_RECORDS) || 100,
52
+ maxFileSizeMB: parseInt(process.env.REACT_APP_MAX_FILE_SIZE_MB) || 100,
53
+
54
+ // Debug mode
55
+ debugMode: process.env.REACT_APP_DEBUG === "true",
56
+ };
57
+
58
+ // API endpoints
59
+ export const getValidationUrl = () =>
60
+ `${config.apiBaseUrl}/credentials/api/validate`;
61
+ export const getGenerationUrl = () => `${config.apiBaseUrl}/ai/execute`;
62
+
63
+ // Debug logging utility
64
+ export const debugLog = (message, data = null) => {
65
+ if (config.debugMode || process.env.NODE_ENV === "development") {
66
+ console.log(`[DEBUG] ${message}`, data || "");
67
+ }
68
+ };
69
+
70
+ // Validate configuration on load
71
+ const validateConfig = () => {
72
+ const requiredEnvVars = [
73
+ "REACT_APP_AWS_ACCESS_KEY_ID",
74
+ "REACT_APP_AWS_SECRET_ACCESS_KEY",
75
+ ];
76
+
77
+ const missing = requiredEnvVars.filter(
78
+ (envVar) =>
79
+ !process.env[envVar] &&
80
+ !process.env[envVar.replace("_ID", "").replace("_KEY", "")]
81
+ );
82
+
83
+ if (missing.length > 0) {
84
+ console.warn("Missing environment variables:", missing);
85
+ }
86
+ };
87
+
88
+ validateConfig();
89
+
90
+ // Enhanced credential validation utilities for browser environment
91
+ export const credentialValidation = {
92
+ // Validate AWS credential format without making API calls
93
+ validateCredentialFormat: () => {
94
+ const errors = [];
95
+
96
+ if (!config.awsAccessKeyId) {
97
+ errors.push("AWS Access Key ID is not configured");
98
+ } else if (!config.awsAccessKeyId.startsWith("AKIA")) {
99
+ errors.push(
100
+ "AWS Access Key ID format appears invalid (should start with AKIA)"
101
+ );
102
+ }
103
+
104
+ if (!config.awsSecretAccessKey) {
105
+ errors.push("AWS Secret Access Key is not configured");
106
+ } else if (config.awsSecretAccessKey.length !== 40) {
107
+ errors.push(
108
+ "AWS Secret Access Key format appears invalid (should be 40 characters)"
109
+ );
110
+ }
111
+
112
+ if (!config.s3BucketName) {
113
+ errors.push("S3 Bucket name is not configured");
114
+ }
115
+
116
+ if (!config.awsRegion) {
117
+ errors.push("AWS Region is not configured");
118
+ }
119
+
120
+ return {
121
+ valid: errors.length === 0,
122
+ errors: errors,
123
+ formatValid: errors.length === 0,
124
+ };
125
+ },
126
+
127
+ // Check if we're in a CORS-restricted environment
128
+ isBrowserEnvironment: () => {
129
+ return typeof window !== "undefined" && typeof document !== "undefined";
130
+ },
131
+ };
132
+
133
+ export default config;