Doner1937 commited on
Commit
5d68d7e
·
verified ·
1 Parent(s): f555cad

Upload 27 files

Browse files
App.tsx ADDED
@@ -0,0 +1,926 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useCallback, useEffect } from 'react';
2
+ import { useAuth } from './services/authService';
3
+ import { CsvInputRow, QueueItem, ResultRow, JobHistoryItem, ProcessedResult, BatchError } from './types';
4
+ import FileUpload from './components/FileUpload';
5
+ import ResultsTable from './components/ResultsTable';
6
+ import SignInPage from './components/SignInPage';
7
+ import { runWorkflow } from './services/difyService';
8
+ import { CheckCircleIcon, XCircleIcon, ClockIcon, UploadIcon, RocketLaunchIcon, PlusIcon, HistoryIcon, DocumentTextIcon, TrashIcon } from './components/Icons';
9
+ import Spinner from './components/Spinner';
10
+
11
+ // --- UTILS & HOOKS (in-file for simplicity) ---
12
+
13
+ const generateUUID = (): string => {
14
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
15
+ const r = (Math.random() * 16) | 0;
16
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
17
+ return v.toString(16);
18
+ });
19
+ };
20
+
21
+ const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
22
+
23
+ // Calculate approximate storage size for job results
24
+ const estimateStorageSize = (results: ResultRow[]): number => {
25
+ return JSON.stringify(results).length;
26
+ };
27
+
28
+ // Maximum storage size per job (500KB)
29
+ const MAX_JOB_STORAGE_SIZE = 500 * 1024;
30
+
31
+ // Enhanced localStorage hook with better error handling and data validation
32
+ function useLocalStorage<T>(key: string, initialValue: T): [T, React.Dispatch<React.SetStateAction<T>>] {
33
+ const [storedValue, setStoredValue] = useState<T>(() => {
34
+ try {
35
+ const item = window.localStorage.getItem(key);
36
+ if (item) {
37
+ const parsed = JSON.parse(item);
38
+
39
+ // Enhanced validation for different data types
40
+ if (Array.isArray(parsed)) {
41
+ // Filter out old items that had the 'results' property which caused storage errors.
42
+ let cleanHistory = parsed.filter(p => typeof p === 'object' && p !== null && !p.hasOwnProperty('results'));
43
+
44
+ // Remove duplicates based on ID if it's a history array
45
+ if (key === 'ace-copywriting-history' && cleanHistory.length > 0) {
46
+ const uniqueHistory = cleanHistory.reduce((acc: any[], current: any) => {
47
+ const existingIndex = acc.findIndex(item => item.id === current.id);
48
+ if (existingIndex === -1) {
49
+ acc.push(current);
50
+ }
51
+ return acc;
52
+ }, []);
53
+ cleanHistory = uniqueHistory;
54
+ }
55
+
56
+ return cleanHistory;
57
+ }
58
+ return parsed;
59
+ }
60
+ return initialValue;
61
+ } catch (error) {
62
+ console.error('Error reading from localStorage, resetting to initial value.', error);
63
+ window.localStorage.removeItem(key); // Clear corrupted item
64
+ return initialValue;
65
+ }
66
+ });
67
+
68
+ const setValue: React.Dispatch<React.SetStateAction<T>> = useCallback((value) => {
69
+ try {
70
+ setStoredValue(prev => {
71
+ const valueToStore = value instanceof Function ? value(prev) : value;
72
+
73
+ // Check storage quota before saving
74
+ const serialized = JSON.stringify(valueToStore);
75
+ if (serialized.length > 5 * 1024 * 1024) { // 5MB limit
76
+ console.warn('Data too large for localStorage, truncating...');
77
+ // For history, keep only the most recent items
78
+ if (Array.isArray(valueToStore) && key === 'ace-copywriting-history') {
79
+ const truncated = (valueToStore as any[]).slice(0, 5); // Keep only 5 most recent
80
+ window.localStorage.setItem(key, JSON.stringify(truncated));
81
+ return truncated as T;
82
+ }
83
+ }
84
+
85
+ window.localStorage.setItem(key, serialized);
86
+ return valueToStore;
87
+ });
88
+ } catch (error) {
89
+ console.error('Error writing to localStorage', error);
90
+ // If quota exceeded, try to clear old data and retry
91
+ if (error instanceof Error && error.name === 'QuotaExceededError') {
92
+ try {
93
+ // Clear old history items to make space
94
+ if (key === 'ace-copywriting-history') {
95
+ const currentHistory = window.localStorage.getItem(key);
96
+ if (currentHistory) {
97
+ const parsed = JSON.parse(currentHistory);
98
+ if (Array.isArray(parsed) && parsed.length > 3) {
99
+ const truncated = parsed.slice(0, 3); // Keep only 3 most recent
100
+ window.localStorage.setItem(key, JSON.stringify(truncated));
101
+ setStoredValue(truncated as T);
102
+ }
103
+ }
104
+ }
105
+ } catch (clearError) {
106
+ console.error('Failed to clear storage for new data', clearError);
107
+ }
108
+ }
109
+ }
110
+ }, [key]);
111
+
112
+ return [storedValue, setValue];
113
+ }
114
+
115
+ // Hook for user preferences
116
+ function useUserPreferences() {
117
+ const [preferences, setPreferences] = useLocalStorage('ace-copywriting-preferences', {
118
+ autoSave: true,
119
+ maxHistoryItems: 10,
120
+ showDebugInfo: false,
121
+ defaultExportFormat: 'csv' as 'csv' | 'pdf' | 'docx' | 'json',
122
+ theme: 'dark' as 'dark' | 'light'
123
+ });
124
+
125
+ return [preferences, setPreferences] as const;
126
+ }
127
+
128
+ // Hook for session data (cleared on browser close)
129
+ function useSessionStorage<T>(key: string, initialValue: T): [T, React.Dispatch<React.SetStateAction<T>>] {
130
+ const [storedValue, setStoredValue] = useState<T>(() => {
131
+ try {
132
+ const item = window.sessionStorage.getItem(key);
133
+ return item ? JSON.parse(item) : initialValue;
134
+ } catch (error) {
135
+ console.error('Error reading from sessionStorage', error);
136
+ return initialValue;
137
+ }
138
+ });
139
+
140
+ const setValue: React.Dispatch<React.SetStateAction<T>> = useCallback((value) => {
141
+ try {
142
+ setStoredValue(prev => {
143
+ const valueToStore = value instanceof Function ? value(prev) : value;
144
+ window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
145
+ return valueToStore;
146
+ });
147
+ } catch (error) {
148
+ console.error('Error writing to sessionStorage', error);
149
+ }
150
+ }, [key]);
151
+
152
+ return [storedValue, setValue];
153
+ }
154
+
155
+
156
+ // --- TYPES ---
157
+
158
+ type ProcessingStatus = 'idle' | 'processing' | 'completed' | 'error';
159
+ type ViewState =
160
+ | { type: 'welcome' }
161
+ | { type: 'new_job' }
162
+ | { type: 'view_history'; jobId: string };
163
+
164
+ // --- CONSTANTS ---
165
+ const MAX_HISTORY_ITEMS = 10;
166
+
167
+ // --- MAIN COMPONENT ---
168
+
169
+ const App: React.FC = () => {
170
+ const { user, logout } = useAuth();
171
+ const [history, setHistory] = useLocalStorage<JobHistoryItem[]>('ace-copywriting-history', []);
172
+ const [preferences, setPreferences] = useUserPreferences();
173
+ const [view, setView] = useState<ViewState>(() => history.length > 0 ? { type: 'new_job' } : { type: 'welcome' });
174
+
175
+ // State for the currently active processing job - persist in sessionStorage
176
+ const [jobId, setJobId] = useSessionStorage<string | null>('ace-current-job-id', null);
177
+ const [filename, setFilename] = useSessionStorage<string>('ace-current-filename', '');
178
+ const [queue, setQueue] = useSessionStorage<QueueItem[]>('ace-current-queue', []);
179
+ const [results, setResults] = useSessionStorage<ResultRow[]>('ace-current-results', []);
180
+ const [status, setStatus] = useSessionStorage<ProcessingStatus>('ace-current-status', 'idle');
181
+ const [error, setError] = useSessionStorage<string | null>('ace-current-error', null);
182
+ const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
183
+
184
+ // New: capture all user-facing errors and show a details modal - persist in sessionStorage
185
+ const [errors, setErrors] = useSessionStorage<BatchError[]>('ace-current-errors', []);
186
+ const [showErrorDetails, setShowErrorDetails] = useState(false);
187
+ const [expandedDebugIndex, setExpandedDebugIndex] = useState<number | null>(null);
188
+
189
+ // Auto-save current job state when it changes
190
+ useEffect(() => {
191
+ if (preferences.autoSave && jobId) {
192
+ const jobState = {
193
+ jobId,
194
+ filename,
195
+ queue,
196
+ results,
197
+ status,
198
+ error,
199
+ errors,
200
+ timestamp: new Date().toISOString()
201
+ };
202
+
203
+ try {
204
+ window.sessionStorage.setItem('ace-current-job-state', JSON.stringify(jobState));
205
+ } catch (error) {
206
+ console.warn('Failed to auto-save job state:', error);
207
+ }
208
+ }
209
+ }, [preferences.autoSave, jobId, filename, queue, results, status, error, errors]);
210
+
211
+ // Restore job state on app load
212
+ useEffect(() => {
213
+ try {
214
+ const savedState = window.sessionStorage.getItem('ace-current-job-state');
215
+ if (savedState) {
216
+ const jobState = JSON.parse(savedState);
217
+ const stateAge = Date.now() - new Date(jobState.timestamp).getTime();
218
+ const maxAge = 24 * 60 * 60 * 1000; // 24 hours
219
+
220
+ // Only restore if state is recent
221
+ if (stateAge < maxAge) {
222
+ setJobId(jobState.jobId);
223
+ setFilename(jobState.filename);
224
+ setQueue(jobState.queue || []);
225
+ setResults(jobState.results || []);
226
+ setStatus(jobState.status || 'idle');
227
+ setError(jobState.error);
228
+ setErrors(jobState.errors || []);
229
+
230
+ // Set appropriate view based on restored state
231
+ if (jobState.status === 'completed' && jobState.results?.length > 0) {
232
+ setView({ type: 'new_job' }); // Show results view
233
+ } else if (jobState.status === 'processing') {
234
+ setView({ type: 'new_job' }); // Show processing view
235
+ }
236
+ } else {
237
+ // Clear old state
238
+ window.sessionStorage.removeItem('ace-current-job-state');
239
+ }
240
+ }
241
+ } catch (error) {
242
+ console.warn('Failed to restore job state:', error);
243
+ // Clear corrupted state
244
+ window.sessionStorage.removeItem('ace-current-job-state');
245
+ }
246
+ }, []);
247
+
248
+ const resetNewJobState = useCallback(() => {
249
+ setJobId(generateUUID());
250
+ setFilename('');
251
+ setQueue([]);
252
+ setResults([]);
253
+ setStatus('idle');
254
+ setError(null);
255
+ setErrors([]);
256
+ setShowErrorDetails(false);
257
+
258
+ // Clear session storage for new job
259
+ window.sessionStorage.removeItem('ace-current-job-state');
260
+ }, [setJobId, setFilename, setQueue, setResults, setStatus, setError, setErrors]);
261
+
262
+ const handleStartNewJob = () => {
263
+ resetNewJobState();
264
+ setView({ type: 'new_job' });
265
+ };
266
+
267
+ const handleDeleteJob = (jobId: string) => {
268
+ setHistory(prevHistory => prevHistory.filter(h => h.id !== jobId));
269
+ setDeleteConfirmId(null);
270
+
271
+ // If we're currently viewing the deleted job, navigate back to new job
272
+ if (view.type === 'view_history' && view.jobId === jobId) {
273
+ setView({ type: 'new_job' });
274
+ }
275
+ };
276
+
277
+ // Enhanced file parsing with better error handling
278
+ const handleFileParsed = useCallback((data: CsvInputRow[], file: File) => {
279
+ // Ensure we have a jobId for this processing session
280
+ if (!jobId) {
281
+ setJobId(generateUUID());
282
+ }
283
+ setFilename(file.name);
284
+ const initialQueue = data.map((row, index) => ({
285
+ id: index,
286
+ data: row,
287
+ status: 'pending' as const,
288
+ }));
289
+ setQueue(initialQueue);
290
+ setError(null);
291
+ setErrors([]);
292
+
293
+ // Save file info to history for reference
294
+ const fileInfo = {
295
+ filename: file.name,
296
+ size: file.size,
297
+ lastModified: file.lastModified,
298
+ rowCount: data.length
299
+ };
300
+
301
+ try {
302
+ const fileHistory = JSON.parse(localStorage.getItem('ace-file-history') || '[]');
303
+ const updatedHistory = [fileInfo, ...fileHistory.slice(0, 9)]; // Keep last 10 files
304
+ localStorage.setItem('ace-file-history', JSON.stringify(updatedHistory));
305
+ } catch (error) {
306
+ console.warn('Failed to save file history:', error);
307
+ }
308
+ }, [jobId, setJobId, setFilename, setQueue, setError, setErrors]);
309
+
310
+ const handleStartProcessing = async () => {
311
+ console.log('Start processing clicked. JobId:', jobId, 'Queue length:', queue.length);
312
+ if (!jobId || queue.length === 0) {
313
+ console.log('Processing aborted - missing jobId or empty queue');
314
+ return;
315
+ }
316
+ console.log('Starting processing...');
317
+ setStatus('processing');
318
+ setError(null);
319
+ setErrors([]);
320
+ setResults([]); // Clear previous results
321
+
322
+ const newResults: ResultRow[] = [];
323
+
324
+ for (const [index, item] of queue.entries()) {
325
+ setQueue(prev => prev.map(q => q.id === item.id ? { ...q, status: 'processing' } : q));
326
+ try {
327
+ const processedData: ProcessedResult = await runWorkflow(item.data);
328
+ const resultRow: ResultRow = { ...item.data, ...processedData, id: item.id };
329
+ newResults.push(resultRow);
330
+ setResults(prev => [...prev, resultRow]); // Add one result at a time
331
+ setQueue(prev => prev.map(q => q.id === item.id ? { ...q, status: 'completed' } : q));
332
+ } catch (e) {
333
+ const msg = e instanceof Error ? e.message : 'An unknown error occurred';
334
+ setQueue(prev => prev.map(q => q.id === item.id ? { ...q, status: 'failed', error: msg } : q));
335
+ setError('Some rows failed during processing.');
336
+ setErrors(prev => ([
337
+ ...prev,
338
+ { row: item.id + 1, code: e instanceof Error ? (e as any).code || 'UNKNOWN' : 'UNKNOWN', message: msg, at: (e as any).at || 'unknown', suggestion: 'Retry later or check input for this row.' }
339
+ ]));
340
+ }
341
+
342
+ // Add a delay between requests to avoid overwhelming the API.
343
+ if (index < queue.length - 1) {
344
+ await delay(500); // 0.5 second pause
345
+ }
346
+ }
347
+ setStatus('completed');
348
+ };
349
+
350
+ // Effect to save job summary to history when completed
351
+ useEffect(() => {
352
+ console.log('History save effect triggered:', { status, jobId, queueLength: queue.length, resultsLength: results.length });
353
+ if (status === 'completed' && jobId) {
354
+ console.log('Saving job to history:', { jobId, filename, queueLength: queue.length, resultsLength: results.length });
355
+ const completedCount = queue.filter(q => q.status === 'completed').length;
356
+ const failedCount = queue.filter(q => q.status === 'failed').length;
357
+
358
+ setHistory(prevHistory => {
359
+ // Check if this job is already in history to prevent duplicates
360
+ const existingIndex = prevHistory.findIndex(h => h.id === jobId);
361
+ if (existingIndex !== -1) {
362
+ return prevHistory; // Already exists, don't add again
363
+ }
364
+
365
+ // Check if results are too large for storage
366
+ const storageSize = estimateStorageSize(results);
367
+ const shouldStoreResults = storageSize <= MAX_JOB_STORAGE_SIZE;
368
+
369
+ if (!shouldStoreResults) {
370
+ console.warn(`Job results too large for storage (${Math.round(storageSize / 1024)}KB). Only storing summary.`);
371
+ }
372
+
373
+ const newHistoryItem: JobHistoryItem = {
374
+ id: jobId,
375
+ filename,
376
+ date: new Date().toISOString(),
377
+ totalRows: queue.length,
378
+ completedCount,
379
+ failedCount,
380
+ results: shouldStoreResults ? [...results] : undefined, // Store results if not too large
381
+ };
382
+
383
+ const updatedHistory = [newHistoryItem, ...prevHistory];
384
+
385
+ // Prune history to the maximum allowed size from preferences
386
+ const maxItems = preferences.maxHistoryItems || 10;
387
+ const prunedHistory = updatedHistory.slice(0, maxItems);
388
+
389
+ console.log('Job saved to history successfully:', newHistoryItem);
390
+ return prunedHistory;
391
+ });
392
+
393
+ // Clear session storage after successful save
394
+ window.sessionStorage.removeItem('ace-current-job-state');
395
+ }
396
+ }, [status, jobId, filename, queue.length, results, setHistory, preferences.maxHistoryItems]);
397
+
398
+ // Cleanup old history items periodically
399
+ useEffect(() => {
400
+ const cleanupHistory = () => {
401
+ setHistory(prevHistory => {
402
+ const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
403
+ const now = Date.now();
404
+ const filtered = prevHistory.filter(item => {
405
+ const itemAge = now - new Date(item.date).getTime();
406
+ return itemAge < maxAge;
407
+ });
408
+
409
+ if (filtered.length !== prevHistory.length) {
410
+ console.log(`Cleaned up ${prevHistory.length - filtered.length} old history items`);
411
+ }
412
+
413
+ return filtered;
414
+ });
415
+ };
416
+
417
+ // Clean up on app start
418
+ cleanupHistory();
419
+
420
+ // Set up periodic cleanup (every hour)
421
+ const interval = setInterval(cleanupHistory, 60 * 60 * 1000);
422
+
423
+ return () => clearInterval(interval);
424
+ }, [setHistory]);
425
+
426
+ // --- RENDER ---
427
+
428
+ const renderStatusIcon = (itemStatus: QueueItem['status']) => {
429
+ switch (itemStatus) {
430
+ case 'pending': return <ClockIcon className="w-5 h-5 text-gray-400 flex-shrink-0" />;
431
+ case 'processing': return <Spinner />;
432
+ case 'completed': return <CheckCircleIcon className="w-5 h-5 text-green-400 flex-shrink-0" />;
433
+ case 'failed': return <XCircleIcon className="w-5 h-5 text-red-400 flex-shrink-0" />;
434
+ }
435
+ };
436
+
437
+ const getActiveJobId = () => {
438
+ if (view.type === 'new_job' && jobId) return jobId;
439
+ if (view.type === 'view_history') return view.jobId;
440
+ return null;
441
+ }
442
+
443
+ const ErrorDetailsModal: React.FC<{ onClose: () => void; items: BatchError[] }> = ({ onClose, items }) => (
444
+ <div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center p-4 z-50" onClick={onClose}>
445
+ <div className="bg-gray-800 rounded-lg shadow-xl p-6 max-w-2xl w-full" onClick={e => e.stopPropagation()}>
446
+ <div className="flex items-center justify-between mb-4">
447
+ <h3 className="text-xl font-bold text-white">Processing Errors</h3>
448
+ <div className="flex items-center gap-2">
449
+ <button onClick={() => setErrors([])} className="text-xs px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded text-gray-200">Clear</button>
450
+ <button onClick={onClose} className="text-gray-300 hover:text-white">Close</button>
451
+ </div>
452
+ </div>
453
+ {items.length === 0 ? (
454
+ <p className="text-gray-300">No errors recorded.</p>
455
+ ) : (
456
+ <div className="max-h-80 overflow-y-auto divide-y divide-gray-700">
457
+ {items.map((e, idx) => (
458
+ <div key={idx} className="py-3 text-sm">
459
+ <div className="flex items-start justify-between">
460
+ <div>
461
+ <div className="text-white font-medium">{e.row ? `Row ${e.row}` : 'General'} — {e.code}</div>
462
+ <div className="text-gray-300 mt-1">{e.message}</div>
463
+ {e.suggestion && <div className="text-gray-400 mt-1">Suggestion: {e.suggestion}</div>}
464
+ {e.at && <div className="text-gray-500 mt-1 text-xs">Area: {e.at}</div>}
465
+ </div>
466
+ <button
467
+ className="text-xs text-gray-400 hover:text-white"
468
+ onClick={() => setErrors(prev => prev.filter((_, i) => i !== idx))}
469
+ title="Dismiss this error"
470
+ >Dismiss</button>
471
+ </div>
472
+ {e.debug && (
473
+ <div className="mt-2">
474
+ <button
475
+ className="text-xs text-blue-300 hover:text-blue-200 underline"
476
+ onClick={() => setExpandedDebugIndex(expandedDebugIndex === idx ? null : idx)}
477
+ >
478
+ {expandedDebugIndex === idx ? 'Hide debug' : 'Show debug'}
479
+ </button>
480
+ {expandedDebugIndex === idx && (
481
+ <div className="mt-2 bg-gray-900 rounded border border-gray-700 p-2">
482
+ <pre className="text-xs text-gray-300 whitespace-pre-wrap max-h-48 overflow-auto">{e.debug}</pre>
483
+ <button
484
+ className="mt-2 text-xs text-gray-300 hover:text-white underline"
485
+ onClick={() => {
486
+ navigator.clipboard?.writeText(e.debug || '');
487
+ }}
488
+ >Copy debug</button>
489
+ </div>
490
+ )}
491
+ </div>
492
+ )}
493
+ </div>
494
+ ))}
495
+ </div>
496
+ )}
497
+ </div>
498
+ </div>
499
+ );
500
+
501
+ const renderMainContent = () => {
502
+ if (view.type === 'welcome') {
503
+ return (
504
+ <div className="text-center">
505
+ <h1 className="text-4xl font-bold text-white">Welcome to the ACE Copywriting Pipeline</h1>
506
+ <p className="mt-4 text-lg text-gray-400">Process CSV files, generate optimized copy, and review your job history.</p>
507
+ <button
508
+ onClick={handleStartNewJob}
509
+ className="mt-8 inline-flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg transition-colors duration-200"
510
+ >
511
+ <PlusIcon className="w-6 h-6"/> Start Your First Job
512
+ </button>
513
+ </div>
514
+ );
515
+ }
516
+
517
+ if (view.type === 'view_history' && view.jobId) {
518
+ const job = history.find(h => h.id === view.jobId);
519
+ if (!job) return <div className="text-center text-red-400">Job not found in history.</div>;
520
+
521
+ // If we have stored results, show them; otherwise show summary
522
+ if (job.results && job.results.length > 0) {
523
+ return <ResultsTable results={job.results} />;
524
+ }
525
+
526
+ return (
527
+ <div className="w-full max-w-4xl mx-auto">
528
+ <div className="bg-gray-800 p-8 rounded-xl shadow-2xl">
529
+ <h2 className="text-3xl font-bold text-white truncate" title={job.filename}>{job.filename}</h2>
530
+ <p className="mt-2 text-md text-gray-400 flex items-center gap-2"><HistoryIcon className="w-5 h-5" /><span>Job processed on {new Date(job.date).toLocaleString()}</span></p>
531
+
532
+ <div className="mt-8 grid grid-cols-1 md:grid-cols-3 gap-6 text-center">
533
+ <div className="bg-gray-700/50 p-6 rounded-lg">
534
+ <p className="text-sm text-gray-400 uppercase tracking-wider">Total Rows</p>
535
+ <p className="text-4xl font-semibold text-white mt-1">{job.totalRows}</p>
536
+ </div>
537
+ <div className="bg-green-900/40 p-6 rounded-lg">
538
+ <p className="text-sm text-green-400 uppercase tracking-wider">Succeeded</p>
539
+ <p className="text-4xl font-semibold text-green-300 mt-1">{job.completedCount}</p>
540
+ </div>
541
+ <div className="bg-red-900/40 p-6 rounded-lg">
542
+ <p className="text-sm text-red-400 uppercase tracking-wider">Failed</p>
543
+ <p className="text-4xl font-semibold text-red-300 mt-1">{job.failedCount}</p>
544
+ </div>
545
+ </div>
546
+
547
+ <div className="mt-8 bg-gray-900/50 border border-gray-700 rounded-lg p-4 text-center">
548
+ <p className="text-gray-300">
549
+ Detailed results are not available for this job.
550
+ </p>
551
+ <p className="text-gray-400 text-sm mt-1">
552
+ This may be due to job size limitations or legacy storage format.
553
+ </p>
554
+ </div>
555
+ </div>
556
+ </div>
557
+ )
558
+ }
559
+
560
+ if (view.type === 'new_job') {
561
+ if (status === 'completed') {
562
+ return <ResultsTable results={results} />;
563
+ }
564
+ if (queue.length === 0) {
565
+ return <FileUpload onFileParsed={handleFileParsed} onParseError={(msg) => {
566
+ setError(msg);
567
+ setErrors(prev => ([...prev, { code: 'CSV_PARSE_ERROR', message: msg, at: 'csv', suggestion: 'Check required columns and CSV formatting.' }]));
568
+ }} disabled={false} />;
569
+ }
570
+ return (
571
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-8 w-full">
572
+ <div className="lg:col-span-1 bg-gray-800 p-6 rounded-xl shadow-lg self-start">
573
+ <h2 className="text-2xl font-semibold mb-4 text-white">Controls</h2>
574
+ <div className="space-y-4">
575
+ <div className="p-4 bg-gray-700 rounded-lg">
576
+ <p className="font-medium text-white truncate" title={filename}>{filename}</p>
577
+ <p className="text-sm text-gray-300">{queue.length} rows ready for processing.</p>
578
+ </div>
579
+ <button
580
+ onClick={handleStartProcessing}
581
+ disabled={status === 'processing'}
582
+ className="w-full flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 text-white font-bold py-3 px-4 rounded-lg transition-colors duration-200"
583
+ >
584
+ {status === 'processing' ? <><Spinner /> Processing...</> : <><RocketLaunchIcon className="w-5 h-5"/> Start Processing</>}
585
+ </button>
586
+ <button
587
+ onClick={handleStartNewJob}
588
+ disabled={status === 'processing'}
589
+ className="w-full flex items-center justify-center gap-2 bg-gray-600 hover:bg-gray-500 disabled:bg-gray-500/50 text-white font-bold py-3 px-4 rounded-lg transition-colors duration-200"
590
+ >
591
+ <UploadIcon className="w-5 h-5"/> Upload New File
592
+ </button>
593
+ </div>
594
+ </div>
595
+ <div className="lg:col-span-2 bg-gray-800 p-6 rounded-xl shadow-lg">
596
+ <h2 className="text-2xl font-semibold mb-4 text-white">Processing Queue</h2>
597
+ <div className="space-y-1 max-h-96 overflow-y-auto pr-2">
598
+ {queue
599
+ .filter((item, index, arr) => arr.findIndex(q => q.id === item.id) === index) // Remove any runtime duplicates
600
+ .map(item => (
601
+ <div key={item.id} className="flex items-center justify-between p-3 bg-gray-700/50 rounded-lg" title={item.error}>
602
+ <div className="flex items-center gap-3 overflow-hidden">
603
+ {renderStatusIcon(item.status)}
604
+ <p className="truncate text-gray-300 text-sm">Row {item.id + 1}: <span className="text-gray-400">{item.data.URL}</span></p>
605
+ </div>
606
+ </div>
607
+ ))}
608
+ </div>
609
+ {status === 'processing' &&
610
+ <div className="mt-4 text-blue-300">
611
+ <p className="text-sm">Processed: {results.length}/{queue.length}</p>
612
+ <div className="w-full bg-gray-700 rounded-full h-2.5 mt-2">
613
+ <div className="bg-blue-600 h-2.5 rounded-full" style={{ width: `${(results.length / queue.length) * 100}%` }}></div>
614
+ </div>
615
+ </div>
616
+ }
617
+ </div>
618
+ </div>
619
+ );
620
+ }
621
+
622
+ return null;
623
+ };
624
+
625
+
626
+ const DeleteConfirmationDialog: React.FC<{ jobId: string; filename: string; onConfirm: () => void; onCancel: () => void }> = ({ jobId, filename, onConfirm, onCancel }) => (
627
+ <div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-50" onClick={onCancel}>
628
+ <div className="bg-gray-800 rounded-lg shadow-xl p-6 max-w-md w-full" onClick={e => e.stopPropagation()}>
629
+ <div className="flex items-center gap-3 mb-4">
630
+ <TrashIcon className="w-6 h-6 text-red-400" />
631
+ <h3 className="text-xl font-bold text-white">Delete Job</h3>
632
+ </div>
633
+ <p className="text-gray-300 mb-6">
634
+ Are you sure you want to delete the job for <span className="font-medium text-white">"{filename}"</span>?
635
+ This action cannot be undone.
636
+ </p>
637
+ <div className="flex gap-3 justify-end">
638
+ <button
639
+ onClick={onCancel}
640
+ className="px-4 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded-lg transition-colors"
641
+ >
642
+ Cancel
643
+ </button>
644
+ <button
645
+ onClick={onConfirm}
646
+ className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors"
647
+ >
648
+ Delete
649
+ </button>
650
+ </div>
651
+ </div>
652
+ </div>
653
+ );
654
+
655
+ // Storage management utilities
656
+ const getStorageInfo = () => {
657
+ try {
658
+ const totalSpace = 5 * 1024 * 1024; // 5MB limit
659
+ const usedSpace = new Blob([JSON.stringify(history)]).size;
660
+ const usedPercentage = (usedSpace / totalSpace) * 100;
661
+
662
+ return {
663
+ usedSpace: Math.round(usedSpace / 1024), // KB
664
+ totalSpace: Math.round(totalSpace / 1024), // KB
665
+ usedPercentage: Math.round(usedPercentage),
666
+ historyCount: history.length,
667
+ maxHistoryItems: preferences.maxHistoryItems
668
+ };
669
+ } catch (error) {
670
+ console.error('Error calculating storage info:', error);
671
+ return null;
672
+ }
673
+ };
674
+
675
+ const clearAllData = () => {
676
+ if (window.confirm('Are you sure you want to clear all stored data? This will remove all job history and cannot be undone.')) {
677
+ try {
678
+ // Clear localStorage
679
+ window.localStorage.removeItem('ace-copywriting-history');
680
+ window.localStorage.removeItem('ace-copywriting-preferences');
681
+ window.localStorage.removeItem('ace-file-history');
682
+
683
+ // Clear sessionStorage
684
+ window.sessionStorage.clear();
685
+
686
+ // Reset state
687
+ setHistory([]);
688
+ setPreferences({
689
+ autoSave: true,
690
+ maxHistoryItems: 10,
691
+ showDebugInfo: false,
692
+ defaultExportFormat: 'csv',
693
+ theme: 'dark'
694
+ });
695
+
696
+ // Reset current job state
697
+ resetNewJobState();
698
+ setView({ type: 'welcome' });
699
+
700
+ alert('All data has been cleared successfully.');
701
+ } catch (error) {
702
+ console.error('Error clearing data:', error);
703
+ alert('Error clearing data. Please try refreshing the page.');
704
+ }
705
+ }
706
+ };
707
+
708
+ const StorageInfoComponent: React.FC = () => {
709
+ const storageInfo = getStorageInfo();
710
+
711
+ if (!storageInfo) return null;
712
+
713
+ return (
714
+ <div className="bg-gray-700/80 backdrop-blur-sm rounded-lg p-3 border border-gray-600">
715
+ <div className="flex items-center justify-between mb-2">
716
+ <h3 className="text-xs font-semibold text-gray-200">Storage</h3>
717
+ <button
718
+ onClick={clearAllData}
719
+ className="text-xs px-2 py-1 bg-red-600 hover:bg-red-700 text-white rounded transition-colors"
720
+ title="Clear all stored data"
721
+ >
722
+ Clear
723
+ </button>
724
+ </div>
725
+ <div className="grid grid-cols-2 gap-3 text-xs">
726
+ <div>
727
+ <div className="text-gray-400 text-xs">Used</div>
728
+ <div className="text-white font-medium">{storageInfo.usedSpace}KB</div>
729
+ <div className="w-full bg-gray-600 rounded-full h-1 mt-1">
730
+ <div
731
+ className={`h-1 rounded-full transition-all ${storageInfo.usedPercentage > 80 ? 'bg-red-500' : storageInfo.usedPercentage > 60 ? 'bg-yellow-500' : 'bg-green-500'}`}
732
+ style={{ width: `${Math.min(storageInfo.usedPercentage, 100)}%` }}
733
+ ></div>
734
+ </div>
735
+ </div>
736
+ <div>
737
+ <div className="text-gray-400 text-xs">History</div>
738
+ <div className="text-white font-medium">{storageInfo.historyCount}/{storageInfo.maxHistoryItems}</div>
739
+ </div>
740
+ </div>
741
+ {storageInfo.usedPercentage > 80 && (
742
+ <div className="text-xs text-yellow-400 mt-2 flex items-center gap-1">
743
+ <span>⚠️</span>
744
+ <span>Storage full</span>
745
+ </div>
746
+ )}
747
+ </div>
748
+ );
749
+ };
750
+
751
+ return (
752
+ <div className="flex flex-col h-screen bg-gray-900 text-gray-100">
753
+ {/* --- HEADER --- */}
754
+ <header className="bg-gray-800/95 backdrop-blur-sm border-b border-gray-700 px-6 py-3 flex-shrink-0">
755
+ <div className="flex justify-between items-center">
756
+ <div className="flex items-center gap-4">
757
+ <h1 className="text-xl font-bold text-white">ACE Pipeline</h1>
758
+ <div className="text-sm text-gray-400">
759
+ {user?.name && `Welcome, ${user.name}`}
760
+ </div>
761
+ </div>
762
+ <div className="flex items-center gap-4">
763
+ <StorageInfoComponent />
764
+ <button
765
+ onClick={() => logout()}
766
+ className="bg-red-600 hover:bg-red-700 text-white px-3 py-1.5 rounded text-sm font-medium transition-colors"
767
+ >
768
+ Sign Out
769
+ </button>
770
+ </div>
771
+ </div>
772
+ </header>
773
+
774
+ {/* --- MAIN LAYOUT --- */}
775
+ <div className="flex flex-1 overflow-hidden">
776
+ {/* --- SIDEBAR --- */}
777
+ <aside className="w-80 bg-gray-800/50 p-4 flex flex-col flex-shrink-0 border-r border-gray-700 overflow-hidden">
778
+ <button
779
+ onClick={handleStartNewJob}
780
+ className={`w-full flex items-center gap-3 p-3 rounded-lg text-left font-semibold mb-4 transition-colors min-w-0 ${getActiveJobId() === jobId && view.type === 'new_job' ? 'bg-blue-600 text-white' : 'bg-gray-700 hover:bg-gray-600 text-gray-200'}`}
781
+ >
782
+ <PlusIcon className="w-6 h-6 flex-shrink-0"/>
783
+ <span className="truncate">New Job</span>
784
+ </button>
785
+ <h2 className="px-2 text-sm font-semibold text-gray-400 uppercase tracking-wider mb-2">History</h2>
786
+ <div className="flex-grow overflow-y-auto overflow-x-hidden pr-1">
787
+ {history.length === 0 ? (
788
+ <div className="text-center text-gray-500 p-4">No jobs completed yet.</div>
789
+ ) : (
790
+ <ul className="space-y-1">
791
+ {history
792
+ .filter((h, index, arr) => arr.findIndex(item => item.id === h.id) === index) // Remove any runtime duplicates
793
+ .map(h => (
794
+ <li key={h.id} className="group">
795
+ <div className="flex items-center min-w-0">
796
+ <button
797
+ onClick={() => setView({ type: 'view_history', jobId: h.id })}
798
+ className={`flex-1 min-w-0 text-left p-3 rounded-l-lg transition-colors ${getActiveJobId() === h.id ? 'bg-gray-700' : 'hover:bg-gray-700/50'}`}
799
+ >
800
+ <div className="flex items-start gap-3 min-w-0">
801
+ <DocumentTextIcon className="w-6 h-6 text-gray-400 mt-0.5 flex-shrink-0"/>
802
+ <div className="flex-1 min-w-0">
803
+ <p className="font-medium text-gray-200 truncate" title={h.filename}>{h.filename}</p>
804
+ <p className="text-xs text-gray-400 truncate">{new Date(h.date).toLocaleDateString()}</p>
805
+ <div className="text-xs mt-1">
806
+ <span className="text-green-400">{h.completedCount}</span>
807
+ <span className="text-gray-500"> / </span>
808
+ <span className="text-red-400">{h.failedCount}</span>
809
+ {h.results && h.results.length > 0 && (
810
+ <span className="text-blue-400 ml-2">• Full Results</span>
811
+ )}
812
+ </div>
813
+ </div>
814
+ </div>
815
+ </button>
816
+ <button
817
+ onClick={(e) => {
818
+ e.stopPropagation();
819
+ setDeleteConfirmId(h.id);
820
+ }}
821
+ className="p-3 text-gray-500 hover:text-red-400 hover:bg-red-900/20 rounded-r-lg transition-colors opacity-0 group-hover:opacity-100 flex-shrink-0"
822
+ title="Delete job"
823
+ >
824
+ <TrashIcon className="w-4 h-4" />
825
+ </button>
826
+ </div>
827
+ </li>
828
+ ))}
829
+ </ul>
830
+ )}
831
+ </div>
832
+ </aside>
833
+
834
+ {/* --- MAIN CONTENT --- */}
835
+ <main className="flex-grow p-8 flex items-center justify-center overflow-auto">
836
+ {error && view.type === 'new_job' && (
837
+ <div className="fixed top-20 right-4 max-w-sm text-left text-red-200 bg-red-900/70 p-4 rounded-lg shadow-lg border border-red-700 z-50">
838
+ <div className="flex items-start justify-between gap-3">
839
+ <div>
840
+ <div className="font-semibold">Issues detected during processing</div>
841
+ <div className="text-sm mt-1">{error}</div>
842
+ {errors.length > 0 && (
843
+ <button
844
+ className="mt-3 text-xs underline text-red-200 hover:text-white"
845
+ onClick={() => setShowErrorDetails(true)}
846
+ >
847
+ View details ({errors.length})
848
+ </button>
849
+ )}
850
+ </div>
851
+ <button
852
+ onClick={() => { setError(null); }}
853
+ className="text-xs px-2 py-1 bg-red-800 hover:bg-red-700 rounded text-red-100"
854
+ title="Dismiss"
855
+ >Dismiss</button>
856
+ </div>
857
+ </div>
858
+ )}
859
+ {renderMainContent()}
860
+ </main>
861
+ </div>
862
+
863
+ {/* --- DELETE CONFIRMATION DIALOG --- */}
864
+ {deleteConfirmId && (
865
+ <DeleteConfirmationDialog
866
+ jobId={deleteConfirmId}
867
+ filename={history.find(h => h.id === deleteConfirmId)?.filename || 'Unknown'}
868
+ onConfirm={() => handleDeleteJob(deleteConfirmId)}
869
+ onCancel={() => setDeleteConfirmId(null)}
870
+ />
871
+ )}
872
+
873
+ {/* --- ERROR DETAILS MODAL --- */}
874
+ {showErrorDetails && <ErrorDetailsModal onClose={() => setShowErrorDetails(false)} items={errors} />}
875
+ </div>
876
+ );
877
+ };
878
+
879
+ // Simple router to handle signin vs main app
880
+ const AppRouter: React.FC = () => {
881
+ const [currentPath, setCurrentPath] = useState(window.location.pathname);
882
+
883
+ useEffect(() => {
884
+ const handlePopState = () => {
885
+ setCurrentPath(window.location.pathname);
886
+ };
887
+
888
+ window.addEventListener('popstate', handlePopState);
889
+ return () => window.removeEventListener('popstate', handlePopState);
890
+ }, []);
891
+
892
+ // Route to signin page
893
+ if (currentPath === '/auth/signin' || currentPath.startsWith('/auth/signin')) {
894
+ return <SignInPage />;
895
+ }
896
+
897
+ // Default to main authenticated app
898
+ return <AuthenticatedApp />;
899
+ };
900
+
901
+ // Main App component wrapped with authentication
902
+ const AuthenticatedApp: React.FC = () => {
903
+ const { user, isAuthenticated, isLoading, logout } = useAuth();
904
+
905
+ if (isLoading) {
906
+ return (
907
+ <div className="min-h-screen flex items-center justify-center bg-gray-900">
908
+ <Spinner />
909
+ </div>
910
+ );
911
+ }
912
+
913
+ if (!isAuthenticated || !user) {
914
+ // Show signin page instead of redirecting
915
+ return <SignInPage />;
916
+ }
917
+
918
+ return <App />;
919
+ };
920
+
921
+ // Root component with routing
922
+ const AppWithAuth: React.FC = () => {
923
+ return <AppRouter />;
924
+ };
925
+
926
+ export default AppWithAuth;
Dockerfile ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # ACE Copywriting Pipeline - Optimized for HuggingFace Spaces
3
+
4
+ FROM node:18-alpine
5
+
6
+ # Set working directory
7
+ WORKDIR /app
8
+
9
+ # Create user for HuggingFace Spaces (user ID 1000 as required)
10
+ # Simple approach: create user with any available UID, HF Spaces will handle the mapping
11
+ RUN adduser -D -s /bin/sh user || echo "User already exists"
12
+
13
+ # Install serve globally for serving static files
14
+ RUN npm install -g serve
15
+
16
+ # Copy package files with proper ownership
17
+ COPY --chown=user package*.json ./
18
+
19
+ # Install dependencies (use npm install since we don't have package-lock.json in git)
20
+ RUN npm install
21
+
22
+ # Copy source code with proper ownership
23
+ COPY --chown=user . .
24
+
25
+ # Set authentication environment variables for build
26
+ # These are hashed values so they're safe to include in the build
27
+ ENV VITE_AUTH_USERNAME_HASH=d86c4384ddade642a91c5bd72d1e428879a1e94e707b8c7c7f6242ca5f014dc8 \
28
+ VITE_AUTH_PASSWORD_HASH=a8e69ee9f4a340cd9c6264569efb4ecbe5f2ebdaac13313338008be2b7c945d8 \
29
+ VITE_AUTH_USER_ID=1 \
30
+ VITE_AUTH_USER_NAME="ACE Admin" \
31
+ VITE_AUTH_USER_EMAIL=admin@aceui.com
32
+
33
+ # Build the application
34
+ RUN npm run build
35
+
36
+ # Switch to user (required by HuggingFace Spaces)
37
+ USER user
38
+
39
+ # Set environment variables for HuggingFace Spaces
40
+ ENV HOME=/home/user \
41
+ PATH=/home/user/.local/bin:$PATH
42
+
43
+ # Expose port 7860 (required by HuggingFace Spaces)
44
+ EXPOSE 7860
45
+
46
+ # Start the application
47
+ CMD ["serve", "-s", "dist", "-l", "7860"]
app.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # This file is for HuggingFace Spaces compatibility
2
+ # The actual application runs in the Docker container
3
+
4
+ print("ACE Copywriting Pipeline is running in Docker container on port 7860")
5
+ print("Access the application at: http://localhost:7860")
6
+
7
+ # This file exists for HF Spaces recognition but the app runs via Docker
8
+ # See Dockerfile for the actual application setup
app/.DS_Store ADDED
Binary file (6.15 kB). View file
 
components/FileUpload.tsx ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useState } from 'react';
2
+ import { CsvInputRow, REQUIRED_CSV_HEADERS } from '../types';
3
+ import { UploadIcon } from './Icons';
4
+
5
+ // This tells TypeScript that `Papa` is available on the global window object.
6
+ declare const Papa: any;
7
+
8
+ interface FileUploadProps {
9
+ onFileParsed: (data: CsvInputRow[], file: File) => void;
10
+ onParseError: (message: string) => void;
11
+ disabled: boolean;
12
+ }
13
+
14
+ const FileUpload: React.FC<FileUploadProps> = ({ onFileParsed, onParseError, disabled }) => {
15
+ const [isDragging, setIsDragging] = useState(false);
16
+
17
+ const handleFile = useCallback((file: File) => {
18
+ if (!file) {
19
+ onParseError('No file selected.');
20
+ return;
21
+ }
22
+ if (file.type !== 'text/csv') {
23
+ onParseError('Invalid file type. Please upload a CSV file.');
24
+ return;
25
+ }
26
+
27
+ Papa.parse(file, {
28
+ header: true,
29
+ skipEmptyLines: true,
30
+ complete: (results: any) => {
31
+ if (results.errors.length > 0) {
32
+ console.error('CSV Parsing Errors:', results.errors);
33
+ onParseError(`Error parsing CSV: ${results.errors[0].message}`);
34
+ return;
35
+ }
36
+
37
+ const headers = results.meta.fields;
38
+ const missingHeaders = REQUIRED_CSV_HEADERS.filter(
39
+ (requiredHeader) => !headers.includes(requiredHeader)
40
+ );
41
+
42
+ if (missingHeaders.length > 0) {
43
+ onParseError(`Missing required CSV columns: ${missingHeaders.join(', ')}`);
44
+ return;
45
+ }
46
+
47
+ onFileParsed(results.data as CsvInputRow[], file);
48
+ },
49
+ error: (error: any) => {
50
+ console.error('PapaParse Error:', error);
51
+ onParseError('An unexpected error occurred while parsing the file.');
52
+ },
53
+ });
54
+ }, [onFileParsed, onParseError]);
55
+
56
+ const handleDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
57
+ e.preventDefault();
58
+ e.stopPropagation();
59
+ if (!disabled) setIsDragging(true);
60
+ };
61
+
62
+ const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
63
+ e.preventDefault();
64
+ e.stopPropagation();
65
+ setIsDragging(false);
66
+ };
67
+
68
+ const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
69
+ e.preventDefault();
70
+ e.stopPropagation();
71
+ };
72
+
73
+ const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
74
+ e.preventDefault();
75
+ e.stopPropagation();
76
+ setIsDragging(false);
77
+ if (disabled) return;
78
+
79
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
80
+ handleFile(e.dataTransfer.files[0]);
81
+ e.dataTransfer.clearData();
82
+ }
83
+ };
84
+
85
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
86
+ if (e.target.files && e.target.files.length > 0) {
87
+ handleFile(e.target.files[0]);
88
+ }
89
+ };
90
+
91
+ const borderColor = isDragging ? 'border-blue-500' : 'border-gray-600';
92
+ const bgColor = isDragging ? 'bg-gray-700' : 'bg-gray-800';
93
+
94
+ return (
95
+ <div
96
+ className={`relative p-8 border-2 ${borderColor} border-dashed rounded-xl text-center transition-all duration-300 ${bgColor} ${disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
97
+ onDragEnter={handleDragEnter}
98
+ onDragLeave={handleDragLeave}
99
+ onDragOver={handleDragOver}
100
+ onDrop={handleDrop}
101
+ >
102
+ <input
103
+ type="file"
104
+ id="file-upload"
105
+ className="hidden"
106
+ accept=".csv"
107
+ onChange={handleInputChange}
108
+ disabled={disabled}
109
+ />
110
+ <label htmlFor="file-upload" className={`${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`}>
111
+ <div className="flex flex-col items-center">
112
+ <UploadIcon className="w-12 h-12 text-gray-400 mb-4" />
113
+ <p className="text-lg font-semibold text-white">
114
+ Drag & drop your CSV file here
115
+ </p>
116
+ <p className="text-gray-400">or click to browse</p>
117
+ </div>
118
+ </label>
119
+ </div>
120
+ );
121
+ };
122
+
123
+ export default FileUpload;
components/Icons.tsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ type IconProps = {
4
+ className?: string;
5
+ };
6
+
7
+ export const UploadIcon: React.FC<IconProps> = ({ className }) => (
8
+ <svg className={className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
9
+ <path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
10
+ </svg>
11
+ );
12
+
13
+ export const DownloadIcon: React.FC<IconProps> = ({ className }) => (
14
+ <svg className={className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
15
+ <path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
16
+ </svg>
17
+ );
18
+
19
+ export const CheckCircleIcon: React.FC<IconProps> = ({ className }) => (
20
+ <svg className={className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
21
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
22
+ </svg>
23
+ );
24
+
25
+ export const XCircleIcon: React.FC<IconProps> = ({ className }) => (
26
+ <svg className={className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
27
+ <path strokeLinecap="round" strokeLinejoin="round" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
28
+ </svg>
29
+ );
30
+
31
+ export const ClockIcon: React.FC<IconProps> = ({ className }) => (
32
+ <svg className={className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
33
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
34
+ </svg>
35
+ );
36
+
37
+ export const RocketLaunchIcon: React.FC<IconProps> = ({ className }) => (
38
+ <svg className={className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
39
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15.59 14.37a6 6 0 01-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 006.16-12.12A14.98 14.98 0 009.63 2.18a14.98 14.98 0 00-2.58 5.84m2.58 9.24v-4.8m0 0a6 6 0 01-7.38-5.84m0 0a14.98 14.98 0 00-12.12 6.16A14.98 14.98 0 002.18 14.37a14.98 14.98 0 005.84 2.58m8.34-12.73a6 6 0 00-7.38 5.84l-2.58 5.84m0 0a14.98 14.98 0 016.16 12.12 14.98 14.98 0 0110.5-10.5 14.98 14.98 0 01-2.58-5.84m-7.76 11.94a6 6 0 01-5.84-7.38 6 6 0 017.38-5.84m0 0a6 6 0 015.84 7.38 6 6 0 01-7.38 5.84z" />
40
+ </svg>
41
+ );
42
+
43
+ export const PlusIcon: React.FC<IconProps> = ({ className }) => (
44
+ <svg className={className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
45
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
46
+ </svg>
47
+ );
48
+
49
+ export const HistoryIcon: React.FC<IconProps> = ({ className }) => (
50
+ <svg className={className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
51
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
52
+ </svg>
53
+ );
54
+
55
+ export const DocumentTextIcon: React.FC<IconProps> = ({ className }) => (
56
+ <svg className={className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
57
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
58
+ </svg>
59
+ );
60
+
61
+ export const EyeIcon: React.FC<IconProps> = ({ className }) => (
62
+ <svg className={className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
63
+ <path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
64
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
65
+ </svg>
66
+ );
67
+
68
+ export const TrashIcon: React.FC<IconProps> = ({ className }) => (
69
+ <svg className={className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
70
+ <path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
71
+ </svg>
72
+ );
components/ResultsTable.tsx ADDED
@@ -0,0 +1,1082 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useState, useMemo, useEffect, useRef } from 'react';
3
+ import ReactMarkdown from 'react-markdown';
4
+ import { ResultRow, DetailedQaReport, QaSectionResult } from '../types';
5
+ import { DownloadIcon, CheckCircleIcon, XCircleIcon, EyeIcon } from './Icons';
6
+ import jsPDF from 'jspdf';
7
+ import { Document, Packer, Paragraph, TextRun, HeadingLevel, Table, TableRow, TableCell, WidthType, AlignmentType, BorderStyle } from 'docx';
8
+
9
+ // This tells TypeScript that `Papa` is available on the global window object.
10
+ declare const Papa: any;
11
+
12
+ interface ResultsTableProps {
13
+ results: ResultRow[];
14
+ }
15
+
16
+ type SortConfig = {
17
+ key: keyof ResultRow;
18
+ direction: 'ascending' | 'descending';
19
+ } | null;
20
+
21
+ const QAReportModal: React.FC<{ report: DetailedQaReport; onClose: () => void }> = ({ report, onClose }) => {
22
+
23
+ const Section: React.FC<{ title: string; data: QaSectionResult }> = ({ title, data }) => (
24
+ <div className="bg-gray-900/70 p-4 rounded-lg mb-4 border border-gray-700">
25
+ <h4 className="text-lg font-semibold text-white mb-3 flex items-center gap-3">
26
+ {data.pass
27
+ ? <CheckCircleIcon className="w-6 h-6 text-green-400 flex-shrink-0" />
28
+ : <XCircleIcon className="w-6 h-6 text-red-400 flex-shrink-0" />
29
+ }
30
+ {title}
31
+ </h4>
32
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-2 text-sm pl-9">
33
+ <p><span className="font-semibold text-gray-400">Grade:</span> <span className="text-gray-200">{data.grade}</span></p>
34
+ <p><span className="font-semibold text-gray-400">Pass:</span> <span className={`font-medium ${data.pass ? 'text-green-400' : 'text-red-400'}`}>{data.pass ? 'Yes' : 'No'}</span></p>
35
+ </div>
36
+
37
+ {/* Enhanced Content Display */}
38
+ {data.detailedAssessment && (
39
+ <div className="pl-9 mt-3 text-sm">
40
+ <p className="font-semibold text-gray-400">Detailed Assessment:</p>
41
+ <div className="text-gray-300 text-xs mt-1 bg-gray-950/50 p-3 rounded-md border border-gray-600/50">
42
+ <ReactMarkdown
43
+ components={{
44
+ p: ({ children }) => <p className="mb-2 last:mb-0 text-gray-300">{children}</p>,
45
+ ul: ({ children }) => <ul className="list-disc ml-4 mb-2 text-gray-300">{children}</ul>,
46
+ ol: ({ children }) => <ol className="list-decimal ml-4 mb-2 text-gray-300">{children}</ol>,
47
+ li: ({ children }) => <li className="mb-1 text-gray-300">{children}</li>,
48
+ code: ({ children }) => <code className="bg-gray-800 px-1 py-0.5 rounded text-blue-300 text-xs">{children}</code>,
49
+ strong: ({ children }) => <strong className="font-semibold text-white">{children}</strong>,
50
+ em: ({ children }) => <em className="italic text-gray-200">{children}</em>,
51
+ }}
52
+ >
53
+ {data.detailedAssessment}
54
+ </ReactMarkdown>
55
+ </div>
56
+ </div>
57
+ )}
58
+
59
+ {data.keyStrengths && data.keyStrengths.length > 0 && (
60
+ <div className="pl-9 mt-3 text-sm">
61
+ <p className="font-semibold text-gray-400">Key Strengths:</p>
62
+ <ul className="list-disc list-inside text-gray-300 pl-2 mt-1 space-y-1">
63
+ {data.keyStrengths.map((strength, i) => <li key={i}>{strength}</li>)}
64
+ </ul>
65
+ </div>
66
+ )}
67
+
68
+ {data.recommendations && data.recommendations.length > 0 && (
69
+ <div className="pl-9 mt-3 text-sm">
70
+ <p className="font-semibold text-gray-400">Recommendations:</p>
71
+ <ul className="list-disc list-inside text-gray-300 pl-2 mt-1 space-y-1">
72
+ {data.recommendations.map((rec, i) => <li key={i}>{rec}</li>)}
73
+ </ul>
74
+ </div>
75
+ )}
76
+
77
+ {data.explanations && (
78
+ <div className="pl-9 mt-3 text-sm">
79
+ <p className="font-semibold text-gray-400">Explanations:</p>
80
+ <div className="text-gray-300 text-xs mt-1 bg-gray-950/50 p-3 rounded-md border border-gray-600/50">
81
+ <ReactMarkdown
82
+ components={{
83
+ p: ({ children }) => <p className="mb-2 last:mb-0 text-gray-300">{children}</p>,
84
+ ul: ({ children }) => <ul className="list-disc ml-4 mb-2 text-gray-300">{children}</ul>,
85
+ ol: ({ children }) => <ol className="list-decimal ml-4 mb-2 text-gray-300">{children}</ol>,
86
+ li: ({ children }) => <li className="mb-1 text-gray-300">{children}</li>,
87
+ code: ({ children }) => <code className="bg-gray-800 px-1 py-0.5 rounded text-blue-300 text-xs">{children}</code>,
88
+ strong: ({ children }) => <strong className="font-semibold text-white">{children}</strong>,
89
+ em: ({ children }) => <em className="italic text-gray-200">{children}</em>,
90
+ }}
91
+ >
92
+ {data.explanations}
93
+ </ReactMarkdown>
94
+ </div>
95
+ </div>
96
+ )}
97
+
98
+ <div className="pl-9 mt-3 text-sm">
99
+ <p className="font-semibold text-gray-400">Errors:</p>
100
+ <ul className="list-disc list-inside text-gray-300 pl-2 mt-1 space-y-1">
101
+ {data.errors.map((err, i) => <li key={i}>{err}</li>)}
102
+ </ul>
103
+ </div>
104
+
105
+ {data.corrected && (
106
+ <div className="pl-9 mt-3 text-sm">
107
+ <p className="font-semibold text-gray-400">Corrected Content:</p>
108
+ <div className="text-gray-300 text-xs mt-1 bg-gray-950/50 p-3 rounded-md border border-gray-600/50 overflow-auto max-h-48">
109
+ <div className="prose prose-sm prose-invert max-w-none">
110
+ <ReactMarkdown
111
+ components={{
112
+ p: ({ children }) => <p className="mb-2 last:mb-0 text-gray-300">{children}</p>,
113
+ ul: ({ children }) => <ul className="list-disc ml-4 mb-2 text-gray-300">{children}</ul>,
114
+ ol: ({ children }) => <ol className="list-decimal ml-4 mb-2 text-gray-300">{children}</ol>,
115
+ li: ({ children }) => <li className="mb-1 text-gray-300">{children}</li>,
116
+ code: ({ children }) => <code className="bg-gray-800 px-1 py-0.5 rounded text-blue-300 text-xs">{children}</code>,
117
+ strong: ({ children }) => <strong className="font-semibold text-white">{children}</strong>,
118
+ em: ({ children }) => <em className="italic text-gray-200">{children}</em>,
119
+ h1: ({ children }) => <h1 className="text-lg font-bold mb-2 text-white">{children}</h1>,
120
+ h2: ({ children }) => <h2 className="text-base font-semibold mb-2 text-white">{children}</h2>,
121
+ h3: ({ children }) => <h3 className="text-sm font-semibold mb-1 text-white">{children}</h3>,
122
+ }}
123
+ >
124
+ {data.corrected}
125
+ </ReactMarkdown>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ )}
130
+
131
+ {/* Raw Content for complete transparency */}
132
+ {data.rawContent && (
133
+ <div className="pl-9 mt-3 text-sm">
134
+ <p className="font-semibold text-gray-400">Raw QA Content:</p>
135
+ <div className="text-gray-300 text-xs mt-1 bg-gray-950/50 p-3 rounded-md border border-gray-600/50 overflow-auto max-h-32">
136
+ <pre className="whitespace-pre-wrap text-gray-400">{data.rawContent}</pre>
137
+ </div>
138
+ </div>
139
+ )}
140
+ </div>
141
+ );
142
+
143
+ const OverallSection: React.FC<{ data: DetailedQaReport['overall'] }> = ({ data }) => (
144
+ <div className="bg-gray-900/70 p-4 rounded-lg mb-4 border border-gray-700">
145
+ <h4 className="text-lg font-semibold text-white mb-3 flex items-center gap-3">
146
+ {data.pass
147
+ ? <CheckCircleIcon className="w-6 h-6 text-green-400 flex-shrink-0" />
148
+ : <XCircleIcon className="w-6 h-6 text-red-400 flex-shrink-0" />
149
+ }
150
+ Overall Assessment
151
+ </h4>
152
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-2 text-sm pl-9">
153
+ <p><span className="font-semibold text-gray-400">Grade:</span> <span className="text-gray-200">{data.grade}</span></p>
154
+ <p><span className="font-semibold text-gray-400">Pass:</span> <span className={`font-medium ${data.pass ? 'text-green-400' : 'text-red-400'}`}>{data.pass ? 'Yes' : 'No'}</span></p>
155
+ </div>
156
+
157
+ <div className="pl-9 mt-3 text-sm">
158
+ <p className="font-semibold text-gray-400">Primary Issue:</p>
159
+ <p className="text-gray-300 mt-1">{data.primaryIssue}</p>
160
+ </div>
161
+
162
+ {/* Enhanced Overall Content Display */}
163
+ {data.detailedAssessment && (
164
+ <div className="pl-9 mt-3 text-sm">
165
+ <p className="font-semibold text-gray-400">Detailed Assessment:</p>
166
+ <div className="text-gray-300 text-xs mt-1 bg-gray-950/50 p-3 rounded-md border border-gray-600/50">
167
+ <ReactMarkdown
168
+ components={{
169
+ p: ({ children }) => <p className="mb-2 last:mb-0 text-gray-300">{children}</p>,
170
+ ul: ({ children }) => <ul className="list-disc ml-4 mb-2 text-gray-300">{children}</ul>,
171
+ ol: ({ children }) => <ol className="list-decimal ml-4 mb-2 text-gray-300">{children}</ol>,
172
+ li: ({ children }) => <li className="mb-1 text-gray-300">{children}</li>,
173
+ code: ({ children }) => <code className="bg-gray-800 px-1 py-0.5 rounded text-blue-300 text-xs">{children}</code>,
174
+ strong: ({ children }) => <strong className="font-semibold text-white">{children}</strong>,
175
+ em: ({ children }) => <em className="italic text-gray-200">{children}</em>,
176
+ }}
177
+ >
178
+ {data.detailedAssessment}
179
+ </ReactMarkdown>
180
+ </div>
181
+ </div>
182
+ )}
183
+
184
+ {data.keyStrengths && data.keyStrengths.length > 0 && (
185
+ <div className="pl-9 mt-3 text-sm">
186
+ <p className="font-semibold text-gray-400">Key Strengths:</p>
187
+ <ul className="list-disc list-inside text-gray-300 pl-2 mt-1 space-y-1">
188
+ {data.keyStrengths.map((strength, i) => <li key={i}>{strength}</li>)}
189
+ </ul>
190
+ </div>
191
+ )}
192
+
193
+ {data.recommendations && data.recommendations.length > 0 && (
194
+ <div className="pl-9 mt-3 text-sm">
195
+ <p className="font-semibold text-gray-400">Recommendations:</p>
196
+ <ul className="list-disc list-inside text-gray-300 pl-2 mt-1 space-y-1">
197
+ {data.recommendations.map((rec, i) => <li key={i}>{rec}</li>)}
198
+ </ul>
199
+ </div>
200
+ )}
201
+
202
+ {data.explanations && (
203
+ <div className="pl-9 mt-3 text-sm">
204
+ <p className="font-semibold text-gray-400">Explanations:</p>
205
+ <div className="text-gray-300 text-xs mt-1 bg-gray-950/50 p-3 rounded-md border border-gray-600/50">
206
+ <ReactMarkdown
207
+ components={{
208
+ p: ({ children }) => <p className="mb-2 last:mb-0 text-gray-300">{children}</p>,
209
+ ul: ({ children }) => <ul className="list-disc ml-4 mb-2 text-gray-300">{children}</ul>,
210
+ ol: ({ children }) => <ol className="list-decimal ml-4 mb-2 text-gray-300">{children}</ol>,
211
+ li: ({ children }) => <li className="mb-1 text-gray-300">{children}</li>,
212
+ code: ({ children }) => <code className="bg-gray-800 px-1 py-0.5 rounded text-blue-300 text-xs">{children}</code>,
213
+ strong: ({ children }) => <strong className="font-semibold text-white">{children}</strong>,
214
+ em: ({ children }) => <em className="italic text-gray-200">{children}</em>,
215
+ }}
216
+ >
217
+ {data.explanations}
218
+ </ReactMarkdown>
219
+ </div>
220
+ </div>
221
+ )}
222
+
223
+ {/* Raw Content for complete transparency */}
224
+ {data.rawContent && (
225
+ <div className="pl-9 mt-3 text-sm">
226
+ <p className="font-semibold text-gray-400">Raw Overall Content:</p>
227
+ <div className="text-gray-300 text-xs mt-1 bg-gray-950/50 p-3 rounded-md border border-gray-600/50 overflow-auto max-h-32">
228
+ <pre className="whitespace-pre-wrap text-gray-400">{data.rawContent}</pre>
229
+ </div>
230
+ </div>
231
+ )}
232
+ </div>
233
+ );
234
+
235
+ const AdditionalSections: React.FC<{ sections: DetailedQaReport['additionalSections'] }> = ({ sections }) => {
236
+ if (!sections) return null;
237
+
238
+ return (
239
+ <div className="bg-gray-900/70 p-4 rounded-lg mb-4 border border-gray-700">
240
+ <h4 className="text-lg font-semibold text-white mb-3">Additional QA Sections</h4>
241
+ <div className="space-y-3">
242
+ {Object.entries(sections).map(([sectionName, sectionData]) => (
243
+ <div key={sectionName} className="border-l-2 border-gray-600 pl-3">
244
+ <h5 className="text-sm font-semibold text-gray-300 mb-2 capitalize">{sectionName}</h5>
245
+ <div className="text-gray-300 text-xs bg-gray-950/50 p-3 rounded-md border border-gray-600/50 overflow-auto max-h-32">
246
+ <ReactMarkdown
247
+ components={{
248
+ p: ({ children }) => <p className="mb-2 last:mb-0 text-gray-300">{children}</p>,
249
+ ul: ({ children }) => <ul className="list-disc ml-4 mb-2 text-gray-300">{children}</ul>,
250
+ ol: ({ children }) => <ol className="list-decimal ml-4 mb-2 text-gray-300">{children}</ol>,
251
+ li: ({ children }) => <li className="mb-1 text-gray-300">{children}</li>,
252
+ code: ({ children }) => <code className="bg-gray-800 px-1 py-0.5 rounded text-blue-300 text-xs">{children}</code>,
253
+ strong: ({ children }) => <strong className="font-semibold text-white">{children}</strong>,
254
+ em: ({ children }) => <em className="italic text-gray-200">{children}</em>,
255
+ }}
256
+ >
257
+ {sectionData.content}
258
+ </ReactMarkdown>
259
+ </div>
260
+ </div>
261
+ ))}
262
+ </div>
263
+ </div>
264
+ );
265
+ };
266
+
267
+ return (
268
+ <div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-50" onClick={onClose}>
269
+ <div className="bg-gray-800 rounded-lg shadow-xl p-6 max-w-6xl w-full max-h-[90vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
270
+ <div className="flex justify-between items-center mb-4 pb-4 border-b border-gray-700">
271
+ <h3 className="text-2xl font-bold text-white">Complete QA Report Details</h3>
272
+ <button onClick={onClose} className="text-gray-400 hover:text-white text-3xl font-bold">&times;</button>
273
+ </div>
274
+ <div className="space-y-2">
275
+ <OverallSection data={report.overall} />
276
+ <AdditionalSections sections={report.additionalSections} />
277
+
278
+ {/* Complete Raw Report */}
279
+ {report.completeRawReport && (
280
+ <div className="bg-gray-900/70 p-4 rounded-lg mb-4 border border-gray-700">
281
+ <h4 className="text-lg font-semibold text-white mb-3">Complete Raw QA Report</h4>
282
+ <div className="text-gray-300 text-xs bg-gray-950/50 p-3 rounded-md border border-gray-600/50 overflow-auto max-h-64">
283
+ <pre className="whitespace-pre-wrap text-gray-400">{report.completeRawReport}</pre>
284
+ </div>
285
+ </div>
286
+ )}
287
+ </div>
288
+ </div>
289
+ </div>
290
+ );
291
+ };
292
+
293
+
294
+ const ResultsTable: React.FC<ResultsTableProps> = ({ results }) => {
295
+ const [sortConfig, setSortConfig] = useState<SortConfig>(null);
296
+ const [selectedReport, setSelectedReport] = useState<DetailedQaReport | null>(null);
297
+ const [showDownloadMenu, setShowDownloadMenu] = useState(false);
298
+ const downloadMenuRef = useRef<HTMLDivElement>(null);
299
+
300
+ // Close dropdown when clicking outside
301
+ useEffect(() => {
302
+ const handleClickOutside = (event: MouseEvent) => {
303
+ if (downloadMenuRef.current && !downloadMenuRef.current.contains(event.target as Node)) {
304
+ setShowDownloadMenu(false);
305
+ }
306
+ };
307
+
308
+ document.addEventListener('mousedown', handleClickOutside);
309
+ return () => {
310
+ document.removeEventListener('mousedown', handleClickOutside);
311
+ };
312
+ }, []);
313
+
314
+ const handleDownloadCSV = () => {
315
+ if (results.length === 0) return;
316
+
317
+ const csvData = results.map(row => {
318
+ const report = row.detailedQaReport;
319
+ return {
320
+ 'URL': row.URL,
321
+ 'Page': row.Page,
322
+ 'Keywords': row.Keywords,
323
+ 'Original Title': row.Recommended_Title,
324
+ 'Original H1': row.Recommended_H1,
325
+ 'Original Copy': row.Copy,
326
+ 'Internal Links': row.Internal_Links,
327
+ 'Generated Title': row.generatedTitle,
328
+ 'Generated H1': row.generatedH1,
329
+ 'Generated Meta': row.generatedMeta,
330
+ 'Generated Copy': row.generatedCopy,
331
+ 'Overall Pass': report?.overall.pass,
332
+ 'Overall Grade': report?.overall.grade,
333
+ 'Overall Primary Issue': report?.overall.primaryIssue,
334
+ 'Overall Detailed Assessment': report?.overall.detailedAssessment,
335
+ 'Overall Key Strengths': report?.overall.keyStrengths?.join('; '),
336
+ 'Overall Recommendations': report?.overall.recommendations?.join('; '),
337
+ 'Overall Explanations': report?.overall.explanations,
338
+ 'Additional QA Sections': report?.additionalSections ?
339
+ Object.entries(report.additionalSections)
340
+ .map(([name, data]) => `${name}: ${data.content.replace(/\r?\n|\r/g, ' ')}`) // Sanitize newlines
341
+ .join(' | ') : '',
342
+ 'Complete Raw QA Report': report?.completeRawReport?.replace(/\r?\n|\r/g, ' ') || row.qaReport.replace(/\r?\n|\r/g, ' '), // Sanitize newlines
343
+ };
344
+ });
345
+
346
+ const csv = Papa.unparse(csvData);
347
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
348
+ const link = document.createElement('a');
349
+ const url = URL.createObjectURL(blob);
350
+ link.setAttribute('href', url);
351
+ link.setAttribute('download', 'ace_copywriting_results.csv');
352
+ document.body.appendChild(link);
353
+ link.click();
354
+ document.body.removeChild(link);
355
+ setShowDownloadMenu(false);
356
+ };
357
+
358
+ const handleDownloadPDF = () => {
359
+ if (results.length === 0) return;
360
+
361
+ const pdf = new jsPDF();
362
+ const pageWidth = pdf.internal.pageSize.getWidth();
363
+ const pageHeight = pdf.internal.pageSize.getHeight();
364
+ const margin = 20;
365
+ const maxLineWidth = pageWidth - (margin * 2);
366
+
367
+ let currentY = margin;
368
+ const lineHeight = 6;
369
+ const sectionSpacing = 10;
370
+
371
+ // Helper function to add text with word wrapping and automatic page breaks
372
+ const addWrappedText = (text: string, x: number, y: number, maxWidth: number, fontSize: number = 12): number => {
373
+ pdf.setFontSize(fontSize);
374
+ const lines = pdf.splitTextToSize(text, maxWidth);
375
+ let currentLineY = y;
376
+
377
+ for (let i = 0; i < lines.length; i++) {
378
+ // Check if we need a new page for this line
379
+ if (currentLineY + lineHeight > pageHeight - margin) {
380
+ pdf.addPage();
381
+ currentLineY = margin;
382
+ }
383
+
384
+ pdf.text(lines[i], x, currentLineY);
385
+ currentLineY += lineHeight;
386
+ }
387
+
388
+ return currentLineY;
389
+ };
390
+
391
+ // Helper function to ensure minimum space on page for section headers
392
+ const ensureMinSpace = (minHeight: number = 40): number => {
393
+ if (currentY + minHeight > pageHeight - margin) {
394
+ pdf.addPage();
395
+ return margin;
396
+ }
397
+ return currentY;
398
+ };
399
+
400
+ // Title
401
+ pdf.setFontSize(20);
402
+ pdf.setFont('helvetica', 'bold');
403
+ pdf.text('ACE Copywriting Pipeline Results', margin, currentY);
404
+ currentY += 15;
405
+
406
+ // Summary
407
+ pdf.setFontSize(12);
408
+ pdf.setFont('helvetica', 'normal');
409
+ const totalResults = results.length;
410
+ const passedResults = results.filter(r => r.overallPass).length;
411
+ const summaryText = `Generated ${totalResults} results | ${passedResults} passed QA | ${totalResults - passedResults} failed QA`;
412
+ currentY = addWrappedText(summaryText, margin, currentY, maxLineWidth);
413
+ currentY += sectionSpacing;
414
+
415
+ // Results
416
+ results.forEach((row, index) => {
417
+ // Estimate space needed for this entry based on content length
418
+ const estimatedHeight = 50 + // Base height for headers and metadata
419
+ Math.ceil((row.generatedTitle?.length || 0) / 80) * 6 + // Title
420
+ Math.ceil((row.generatedH1?.length || 0) / 80) * 6 + // H1
421
+ Math.ceil((row.generatedMeta?.length || 0) / 80) * 6 + // Meta
422
+ Math.ceil((row.generatedCopy?.length || 0) / 100) * 5; // Copy (smaller font)
423
+
424
+ // Ensure minimum space for section header
425
+ currentY = ensureMinSpace(40);
426
+
427
+ // Page/URL Header
428
+ pdf.setFontSize(14);
429
+ pdf.setFont('helvetica', 'bold');
430
+ currentY = addWrappedText(`${index + 1}. ${row.Page || 'Page'} (${row.URL})`, margin, currentY, maxLineWidth, 14);
431
+ currentY += 5;
432
+
433
+ // Keywords
434
+ pdf.setFontSize(10);
435
+ pdf.setFont('helvetica', 'normal');
436
+ currentY = addWrappedText(`Keywords: ${row.Keywords}`, margin, currentY, maxLineWidth, 10);
437
+ currentY += 3;
438
+
439
+ // Overall QA Status
440
+ pdf.setFont('helvetica', 'bold');
441
+ if (row.overallPass) {
442
+ pdf.setTextColor(0, 128, 0);
443
+ } else {
444
+ pdf.setTextColor(255, 0, 0);
445
+ }
446
+ currentY = addWrappedText(`Overall QA: ${row.overallPass ? 'PASS' : 'FAIL'} (${row.overallGrade})`, margin, currentY, maxLineWidth, 10);
447
+ pdf.setTextColor(0, 0, 0);
448
+ currentY += 5;
449
+
450
+ // Generated Content
451
+ pdf.setFont('helvetica', 'bold');
452
+ currentY = addWrappedText('Generated Title:', margin, currentY, maxLineWidth, 10);
453
+ pdf.setFont('helvetica', 'normal');
454
+ currentY = addWrappedText(row.generatedTitle || '', margin, currentY, maxLineWidth, 9);
455
+ currentY += 3;
456
+
457
+ pdf.setFont('helvetica', 'bold');
458
+ currentY = addWrappedText('Generated H1:', margin, currentY, maxLineWidth, 10);
459
+ pdf.setFont('helvetica', 'normal');
460
+ currentY = addWrappedText(row.generatedH1 || '', margin, currentY, maxLineWidth, 9);
461
+ currentY += 3;
462
+
463
+ pdf.setFont('helvetica', 'bold');
464
+ currentY = addWrappedText('Generated Meta:', margin, currentY, maxLineWidth, 10);
465
+ pdf.setFont('helvetica', 'normal');
466
+ currentY = addWrappedText(row.generatedMeta || '', margin, currentY, maxLineWidth, 9);
467
+ currentY += 3;
468
+
469
+ // Generated Copy (full content)
470
+ pdf.setFont('helvetica', 'bold');
471
+ currentY = addWrappedText('Generated Copy:', margin, currentY, maxLineWidth, 10);
472
+ pdf.setFont('helvetica', 'normal');
473
+
474
+ // Add the full copy content with proper spacing
475
+ if (row.generatedCopy) {
476
+ currentY = addWrappedText(row.generatedCopy, margin, currentY, maxLineWidth, 9);
477
+ }
478
+
479
+ // Add QA Details if available
480
+ if (row.detailedQaReport) {
481
+ const report = row.detailedQaReport;
482
+ currentY = ensureMinSpace(60); // Ensure space for QA header + at least one section
483
+ currentY += 5;
484
+ pdf.setFontSize(12);
485
+ pdf.setFont('helvetica', 'bold');
486
+ currentY = addWrappedText('QA Report Details:', margin + 5, currentY, maxLineWidth - 5, 12);
487
+ currentY += 2;
488
+
489
+ const addDetailedSection = (title: string, section: QaSectionResult | DetailedQaReport['overall']) => {
490
+ const isOverall = 'primaryIssue' in section;
491
+
492
+ currentY = ensureMinSpace(30);
493
+ pdf.setFontSize(10);
494
+ pdf.setFont('helvetica', 'bold');
495
+
496
+ if (section.pass) {
497
+ pdf.setTextColor(0, 128, 0); // Green for PASS
498
+ } else {
499
+ pdf.setTextColor(255, 0, 0); // Red for FAIL
500
+ }
501
+ currentY = addWrappedText(`${title}: ${section.pass ? 'PASS' : 'FAIL'} (Grade: ${section.grade})`, margin + 5, currentY, maxLineWidth - 5, 10);
502
+ pdf.setTextColor(0, 0, 0); // Reset color
503
+
504
+ pdf.setFont('helvetica', 'normal');
505
+
506
+ if (isOverall && section.primaryIssue) {
507
+ currentY = addWrappedText(`Primary Issue: ${section.primaryIssue}`, margin + 10, currentY, maxLineWidth - 10, 8);
508
+ }
509
+
510
+ if (!isOverall && section.errors.length > 0) {
511
+ currentY = addWrappedText(`Errors: ${section.errors.join(', ')}`, margin + 10, currentY, maxLineWidth - 10, 8);
512
+ }
513
+
514
+ if (section.detailedAssessment) {
515
+ currentY = ensureMinSpace(20);
516
+ pdf.setFont('helvetica', 'bold');
517
+ currentY = addWrappedText('Detailed Assessment:', margin + 10, currentY, maxLineWidth - 10, 8);
518
+ pdf.setFont('helvetica', 'normal');
519
+ currentY = addWrappedText(section.detailedAssessment, margin + 15, currentY, maxLineWidth - 15, 7);
520
+ }
521
+
522
+ if (section.keyStrengths && section.keyStrengths.length > 0) {
523
+ currentY = ensureMinSpace(20);
524
+ pdf.setFont('helvetica', 'bold');
525
+ currentY = addWrappedText('Key Strengths:', margin + 10, currentY, maxLineWidth - 10, 8);
526
+ pdf.setFont('helvetica', 'normal');
527
+ section.keyStrengths.forEach(strength => {
528
+ currentY = addWrappedText(`• ${strength}`, margin + 15, currentY, maxLineWidth - 15, 7);
529
+ });
530
+ }
531
+
532
+ if (section.recommendations && section.recommendations.length > 0) {
533
+ currentY = ensureMinSpace(20);
534
+ pdf.setFont('helvetica', 'bold');
535
+ currentY = addWrappedText('Recommendations:', margin + 10, currentY, maxLineWidth - 10, 8);
536
+ pdf.setFont('helvetica', 'normal');
537
+ section.recommendations.forEach(rec => {
538
+ currentY = addWrappedText(`• ${rec}`, margin + 15, currentY, maxLineWidth - 15, 7);
539
+ });
540
+ }
541
+
542
+ if (section.explanations) {
543
+ currentY = ensureMinSpace(20);
544
+ pdf.setFont('helvetica', 'bold');
545
+ currentY = addWrappedText('Explanations:', margin + 10, currentY, maxLineWidth - 10, 8);
546
+ pdf.setFont('helvetica', 'normal');
547
+ currentY = addWrappedText(section.explanations, margin + 15, currentY, maxLineWidth - 15, 7);
548
+ }
549
+
550
+ if (!isOverall && section.corrected) {
551
+ currentY = ensureMinSpace(20);
552
+ pdf.setFont('helvetica', 'italic');
553
+ currentY = addWrappedText(`Correction/Analysis: ${section.corrected}`, margin + 10, currentY, maxLineWidth - 10, 8);
554
+ pdf.setFont('helvetica', 'normal');
555
+ }
556
+ currentY += 4;
557
+ };
558
+
559
+ addDetailedSection('Overall Assessment', report.overall);
560
+
561
+ // Add Additional Sections if available
562
+ if (report.additionalSections) {
563
+ currentY = ensureMinSpace(30);
564
+ pdf.setFontSize(10);
565
+ pdf.setFont('helvetica', 'bold');
566
+ currentY = addWrappedText('Additional QA Sections:', margin + 5, currentY, maxLineWidth - 5, 10);
567
+ currentY += 2;
568
+
569
+ Object.entries(report.additionalSections).forEach(([sectionName, sectionData]) => {
570
+ currentY = ensureMinSpace(25);
571
+ pdf.setFontSize(9);
572
+ pdf.setFont('helvetica', 'bold');
573
+ currentY = addWrappedText(`${sectionName}:`, margin + 10, currentY, maxLineWidth - 10, 9);
574
+ pdf.setFont('helvetica', 'normal');
575
+ currentY = addWrappedText(sectionData.content, margin + 15, currentY, maxLineWidth - 15, 7);
576
+ currentY += 2;
577
+ });
578
+ }
579
+
580
+ // Add Complete Raw Report
581
+ if (report.completeRawReport) {
582
+ currentY = ensureMinSpace(30);
583
+ pdf.setFontSize(10);
584
+ pdf.setFont('helvetica', 'bold');
585
+ currentY = addWrappedText('Complete Raw QA Report:', margin + 5, currentY, maxLineWidth - 5, 10);
586
+ currentY += 2;
587
+ pdf.setFontSize(7);
588
+ pdf.setFont('helvetica', 'normal');
589
+ pdf.setTextColor(100, 100, 100); // Lighter text for raw content
590
+ currentY = addWrappedText(report.completeRawReport, margin + 10, currentY, maxLineWidth - 10, 7);
591
+ pdf.setTextColor(0, 0, 0); // Reset color
592
+ }
593
+ }
594
+
595
+ currentY += sectionSpacing * 2; // Extra spacing between entries
596
+ });
597
+
598
+ // Footer on last page
599
+ pdf.setFontSize(8);
600
+ pdf.setTextColor(128, 128, 128);
601
+ pdf.text('Generated by ACE Copywriting Pipeline', margin, pageHeight - 10);
602
+
603
+ // Save the PDF
604
+ pdf.save('ace_copywriting_results.pdf');
605
+ setShowDownloadMenu(false);
606
+ };
607
+
608
+ const handleDownloadJSON = () => {
609
+ if (results.length === 0) return;
610
+
611
+ const jsonData = {
612
+ metadata: {
613
+ generatedAt: new Date().toISOString(),
614
+ totalResults: results.length,
615
+ passedResults: results.filter(r => r.overallPass).length,
616
+ failedResults: results.filter(r => !r.overallPass).length
617
+ },
618
+ results: results.map(row => ({
619
+ url: row.URL,
620
+ page: row.Page,
621
+ keywords: row.Keywords,
622
+ original: {
623
+ title: row.Recommended_Title,
624
+ h1: row.Recommended_H1,
625
+ copy: row.Copy,
626
+ internalLinks: row.Internal_Links
627
+ },
628
+ generated: {
629
+ title: row.generatedTitle,
630
+ h1: row.generatedH1,
631
+ meta: row.generatedMeta,
632
+ copy: row.generatedCopy
633
+ },
634
+ qa: {
635
+ overallPass: row.overallPass,
636
+ overallGrade: row.overallGrade,
637
+ sections: {
638
+ title: {
639
+ pass: row.detailedQaReport?.title.pass,
640
+ grade: row.detailedQaReport?.title.grade,
641
+ errors: row.detailedQaReport?.title.errors
642
+ },
643
+ meta: {
644
+ pass: row.detailedQaReport?.meta.pass,
645
+ grade: row.detailedQaReport?.meta.grade,
646
+ errors: row.detailedQaReport?.meta.errors
647
+ },
648
+ h1: {
649
+ pass: row.detailedQaReport?.h1.pass,
650
+ grade: row.detailedQaReport?.h1.grade,
651
+ errors: row.detailedQaReport?.h1.errors
652
+ },
653
+ copy: {
654
+ pass: row.detailedQaReport?.copy.pass,
655
+ grade: row.detailedQaReport?.copy.grade,
656
+ errors: row.detailedQaReport?.copy.errors
657
+ }
658
+ },
659
+ fullReport: row.qaReport
660
+ }
661
+ }))
662
+ };
663
+
664
+ const jsonString = JSON.stringify(jsonData, null, 2);
665
+ const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8;' });
666
+ const link = document.createElement('a');
667
+ const url = URL.createObjectURL(blob);
668
+ link.setAttribute('href', url);
669
+ link.setAttribute('download', 'ace_copywriting_results.json');
670
+ document.body.appendChild(link);
671
+ link.click();
672
+ document.body.removeChild(link);
673
+ setShowDownloadMenu(false);
674
+ };
675
+
676
+ const handleDownloadDOCX = async () => {
677
+ if (results.length === 0) return;
678
+
679
+ // Helper function to clean HTML tags and format text
680
+ const cleanText = (text: string): string => {
681
+ if (!text) return '';
682
+ return text
683
+ .replace(/<[^>]*>/g, '') // Remove HTML tags
684
+ .replace(/\*\*([^*]+)\*\*/g, '$1') // Remove markdown bold
685
+ .replace(/\*([^*]+)\*/g, '$1') // Remove markdown italic
686
+ .replace(/---/g, '') // Remove markdown separators
687
+ .replace(/\n\s*\n/g, '\n') // Remove extra line breaks
688
+ .trim();
689
+ };
690
+
691
+ // Helper function to split text into paragraphs
692
+ const splitIntoParagraphs = (text: string): string[] => {
693
+ const cleaned = cleanText(text);
694
+ return cleaned
695
+ .split(/\n+/)
696
+ .map(p => p.trim())
697
+ .filter(p => p.length > 0);
698
+ };
699
+
700
+ const children: any[] = [];
701
+
702
+ // Title
703
+ children.push(
704
+ new Paragraph({
705
+ text: "ACE Copywriting Pipeline Results",
706
+ heading: HeadingLevel.HEADING_1,
707
+ alignment: AlignmentType.CENTER,
708
+ spacing: { after: 400 }
709
+ })
710
+ );
711
+
712
+ // Summary
713
+ const totalResults = results.length;
714
+ const passedResults = results.filter(r => r.overallPass).length;
715
+ const summaryText = `Generated ${totalResults} results | ${passedResults} passed QA | ${totalResults - passedResults} failed QA`;
716
+
717
+ children.push(
718
+ new Paragraph({
719
+ text: summaryText,
720
+ spacing: { after: 400 }
721
+ })
722
+ );
723
+
724
+ // Results
725
+ results.forEach((row, index) => {
726
+ // Page/URL Header
727
+ children.push(
728
+ new Paragraph({
729
+ text: `${index + 1}. ${row.Page || 'Page'} (${row.URL})`,
730
+ heading: HeadingLevel.HEADING_2,
731
+ spacing: { before: 400, after: 200 }
732
+ })
733
+ );
734
+
735
+ // Keywords
736
+ children.push(
737
+ new Paragraph({
738
+ text: `Keywords: ${row.Keywords}`,
739
+ spacing: { after: 200 }
740
+ })
741
+ );
742
+
743
+ // Overall QA Status
744
+ children.push(
745
+ new Paragraph({
746
+ children: [
747
+ new TextRun({
748
+ text: `Overall QA: ${row.overallPass ? 'PASS' : 'FAIL'} (${row.overallGrade})`,
749
+ color: row.overallPass ? '008000' : 'FF0000',
750
+ bold: true
751
+ })
752
+ ],
753
+ spacing: { after: 200 }
754
+ })
755
+ );
756
+
757
+ // Generated Content
758
+ children.push(
759
+ new Paragraph({
760
+ text: "Generated Title:",
761
+ heading: HeadingLevel.HEADING_3,
762
+ spacing: { before: 300, after: 100 }
763
+ }),
764
+ new Paragraph({
765
+ text: cleanText(row.generatedTitle || ''),
766
+ spacing: { after: 200 }
767
+ }),
768
+ new Paragraph({
769
+ text: "Generated H1:",
770
+ heading: HeadingLevel.HEADING_3,
771
+ spacing: { before: 300, after: 100 }
772
+ }),
773
+ new Paragraph({
774
+ text: cleanText(row.generatedH1 || ''),
775
+ spacing: { after: 200 }
776
+ }),
777
+ new Paragraph({
778
+ text: "Generated Meta:",
779
+ heading: HeadingLevel.HEADING_3,
780
+ spacing: { before: 300, after: 100 }
781
+ }),
782
+ new Paragraph({
783
+ text: cleanText(row.generatedMeta || ''),
784
+ spacing: { after: 200 }
785
+ }),
786
+ new Paragraph({
787
+ text: "Generated Copy:",
788
+ heading: HeadingLevel.HEADING_3,
789
+ spacing: { before: 300, after: 100 }
790
+ })
791
+ );
792
+
793
+ // Handle generated copy with proper paragraph breaks
794
+ if (row.generatedCopy) {
795
+ const copyParagraphs = splitIntoParagraphs(row.generatedCopy);
796
+ copyParagraphs.forEach(paragraph => {
797
+ children.push(
798
+ new Paragraph({
799
+ text: paragraph,
800
+ spacing: { after: 150 }
801
+ })
802
+ );
803
+ });
804
+ }
805
+
806
+ // QA Details if available
807
+ if (row.detailedQaReport) {
808
+ const report = row.detailedQaReport;
809
+ children.push(
810
+ new Paragraph({
811
+ text: "QA Report Details:",
812
+ heading: HeadingLevel.HEADING_3,
813
+ spacing: { before: 400, after: 200 }
814
+ })
815
+ );
816
+
817
+ const addDetailedSection = (title: string, section: QaSectionResult | DetailedQaReport['overall']) => {
818
+ const isOverall = 'primaryIssue' in section;
819
+
820
+ children.push(
821
+ new Paragraph({
822
+ children: [
823
+ new TextRun({
824
+ text: `${title}: ${section.pass ? 'PASS' : 'FAIL'} (Grade: ${section.grade})`,
825
+ color: section.pass ? '008000' : 'FF0000',
826
+ bold: true
827
+ })
828
+ ],
829
+ heading: HeadingLevel.HEADING_4,
830
+ spacing: { before: 300, after: 100 }
831
+ })
832
+ );
833
+
834
+ if (isOverall && section.primaryIssue) {
835
+ children.push(new Paragraph({ text: `Primary Issue: ${cleanText(section.primaryIssue)}`, spacing: { after: 100 } }));
836
+ }
837
+
838
+ if (!isOverall && section.errors && section.errors.length > 0) {
839
+ children.push(new Paragraph({ text: `Errors: ${section.errors.join(', ')}`, spacing: { after: 100 } }));
840
+ }
841
+
842
+ if (section.detailedAssessment) {
843
+ children.push(new Paragraph({ children: [new TextRun({ text: "Detailed Assessment:", bold: true })], spacing: { after: 50 } }));
844
+ splitIntoParagraphs(section.detailedAssessment).forEach(p => children.push(new Paragraph({ text: p, spacing: { after: 100 } })));
845
+ }
846
+
847
+ if (section.keyStrengths && section.keyStrengths.length > 0) {
848
+ children.push(new Paragraph({ children: [new TextRun({ text: "Key Strengths:", bold: true })], spacing: { after: 50 } }));
849
+ section.keyStrengths.forEach(s => children.push(new Paragraph({ text: `• ${cleanText(s)}`, spacing: { after: 50 }, indent: { left: 400 } })));
850
+ }
851
+
852
+ if (section.recommendations && section.recommendations.length > 0) {
853
+ children.push(new Paragraph({ children: [new TextRun({ text: "Recommendations:", bold: true })], spacing: { after: 50 } }));
854
+ section.recommendations.forEach(r => children.push(new Paragraph({ text: `• ${cleanText(r)}`, spacing: { after: 50 }, indent: { left: 400 } })));
855
+ }
856
+
857
+ if (section.explanations) {
858
+ children.push(new Paragraph({ children: [new TextRun({ text: "Explanations:", bold: true })], spacing: { after: 50 } }));
859
+ splitIntoParagraphs(section.explanations).forEach(p => children.push(new Paragraph({ text: p, spacing: { after: 100 } })));
860
+ }
861
+
862
+ if (!isOverall && section.corrected) {
863
+ children.push(new Paragraph({ children: [new TextRun({ text: "Corrected Content:", bold: true })], spacing: { after: 50 } }));
864
+ splitIntoParagraphs(section.corrected).forEach(p => children.push(new Paragraph({ text: p, spacing: { after: 100 } })));
865
+ }
866
+ };
867
+
868
+ addDetailedSection('Overall Assessment', report.overall);
869
+
870
+ // Add Additional Sections
871
+ if (report.additionalSections && Object.keys(report.additionalSections).length > 0) {
872
+ children.push(new Paragraph({ text: "Additional QA Sections", heading: HeadingLevel.HEADING_4, spacing: { before: 300, after: 100 } }));
873
+ Object.entries(report.additionalSections).forEach(([name, data]) => {
874
+ children.push(new Paragraph({ children: [new TextRun({ text: `${name}:`, bold: true })], spacing: { after: 50 } }));
875
+ splitIntoParagraphs(data.content).forEach(p => children.push(new Paragraph({ text: p, spacing: { after: 100 }, indent: { left: 400 } })));
876
+ });
877
+ }
878
+
879
+ // Add Complete Raw Report
880
+ if (report.completeRawReport) {
881
+ children.push(new Paragraph({ text: "Complete Raw QA Report", heading: HeadingLevel.HEADING_4, spacing: { before: 300, after: 100 } }));
882
+ splitIntoParagraphs(report.completeRawReport).forEach(p => {
883
+ children.push(new Paragraph({
884
+ children: [ new TextRun({ text: p, size: 16 }) ], // smaller font
885
+ spacing: { after: 100 }
886
+ }));
887
+ });
888
+ }
889
+ }
890
+
891
+ // Add spacing between entries
892
+ children.push(
893
+ new Paragraph({
894
+ text: "",
895
+ spacing: { after: 400 }
896
+ })
897
+ );
898
+ });
899
+
900
+ // Footer
901
+ children.push(
902
+ new Paragraph({
903
+ children: [
904
+ new TextRun({
905
+ text: "Generated by ACE Copywriting Pipeline",
906
+ color: '808080',
907
+ size: 16
908
+ })
909
+ ],
910
+ alignment: AlignmentType.CENTER,
911
+ spacing: { before: 400 }
912
+ })
913
+ );
914
+
915
+ const doc = new Document({
916
+ sections: [{
917
+ properties: {},
918
+ children: children
919
+ }]
920
+ });
921
+
922
+ const blob = await Packer.toBlob(doc);
923
+ const url = URL.createObjectURL(blob);
924
+ const link = document.createElement('a');
925
+ link.setAttribute('href', url);
926
+ link.setAttribute('download', 'ace_copywriting_results.docx');
927
+ document.body.appendChild(link);
928
+ link.click();
929
+ document.body.removeChild(link);
930
+ URL.revokeObjectURL(url);
931
+ setShowDownloadMenu(false);
932
+ };
933
+
934
+ const sortedResults = useMemo(() => {
935
+ let sortableItems = [...results];
936
+ if (sortConfig !== null) {
937
+ sortableItems.sort((a, b) => {
938
+ const key = sortConfig.key;
939
+ const valA = a[key as keyof typeof a];
940
+ const valB = b[key as keyof typeof b];
941
+
942
+ if (typeof valA === 'boolean' && typeof valB === 'boolean') {
943
+ if (valA === valB) return 0;
944
+ return sortConfig.direction === 'ascending' ? (valA ? -1 : 1) : (valA ? 1 : -1);
945
+ }
946
+
947
+ if (valA < valB) {
948
+ return sortConfig.direction === 'ascending' ? -1 : 1;
949
+ }
950
+ if (valA > valB) {
951
+ return sortConfig.direction === 'ascending' ? 1 : -1;
952
+ }
953
+ return 0;
954
+ });
955
+ }
956
+ return sortableItems;
957
+ }, [results, sortConfig]);
958
+
959
+ const requestSort = (key: keyof ResultRow) => {
960
+ let direction: 'ascending' | 'descending' = 'ascending';
961
+ if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') {
962
+ direction = 'descending';
963
+ }
964
+ setSortConfig({ key, direction });
965
+ };
966
+
967
+ const getSortIndicator = (key: keyof ResultRow) => {
968
+ if (!sortConfig || sortConfig.key !== key) {
969
+ return ' ↕';
970
+ }
971
+ return sortConfig.direction === 'ascending' ? ' ▲' : ' ▼';
972
+ };
973
+
974
+ if (results.length === 0) {
975
+ return (
976
+ <div className="bg-gray-800 rounded-xl shadow-lg p-6 text-center">
977
+ <h2 className="text-2xl font-semibold text-white">Generated Content</h2>
978
+ <p className="mt-4 text-gray-400">No results to display yet. Process a file to see the output here.</p>
979
+ </div>
980
+ )
981
+ }
982
+
983
+ return (
984
+ <>
985
+ <div className="bg-gray-800 rounded-xl shadow-lg p-6 w-full">
986
+ <div className="flex justify-between items-center mb-4">
987
+ <h2 className="text-2xl font-semibold text-white">Generated Content</h2>
988
+ <div className="relative" ref={downloadMenuRef}>
989
+ <button
990
+ onClick={() => setShowDownloadMenu(!showDownloadMenu)}
991
+ className="flex items-center gap-2 bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg transition-colors duration-200"
992
+ >
993
+ <DownloadIcon className="w-5 h-5"/>
994
+ Download
995
+ <svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
996
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
997
+ </svg>
998
+ </button>
999
+
1000
+ {showDownloadMenu && (
1001
+ <div className="absolute right-0 mt-2 w-48 bg-gray-700 rounded-lg shadow-lg z-10 border border-gray-600">
1002
+ <div className="py-1">
1003
+ <button
1004
+ onClick={handleDownloadCSV}
1005
+ className="w-full text-left px-4 py-2 text-white hover:bg-gray-600 flex items-center gap-2"
1006
+ >
1007
+ 📊 Download CSV
1008
+ </button>
1009
+ <button
1010
+ onClick={handleDownloadPDF}
1011
+ className="w-full text-left px-4 py-2 text-white hover:bg-gray-600 flex items-center gap-2"
1012
+ >
1013
+ 📄 Download PDF
1014
+ </button>
1015
+ <button
1016
+ onClick={handleDownloadJSON}
1017
+ className="w-full text-left px-4 py-2 text-white hover:bg-gray-600 flex items-center gap-2"
1018
+ >
1019
+ 💾 Download JSON
1020
+ </button>
1021
+ <button
1022
+ onClick={handleDownloadDOCX}
1023
+ className="w-full text-left px-4 py-2 text-white hover:bg-gray-600 flex items-center gap-2"
1024
+ >
1025
+ 📝 Download DOCX
1026
+ </button>
1027
+ </div>
1028
+ </div>
1029
+ )}
1030
+ </div>
1031
+ </div>
1032
+ <div className="overflow-x-auto">
1033
+ <table className="w-full text-sm text-left text-gray-300">
1034
+ <thead className="text-xs text-gray-400 uppercase bg-gray-700">
1035
+ <tr>
1036
+ <th scope="col" className="px-6 py-3 cursor-pointer" onClick={() => requestSort('URL')}>URL{getSortIndicator('URL')}</th>
1037
+ <th scope="col" className="px-6 py-3">Generated Title</th>
1038
+ <th scope="col" className="px-6 py-3">Generated H1</th>
1039
+ <th scope="col" className="px-6 py-3 cursor-pointer" onClick={() => requestSort('overallPass')}>Overall Pass{getSortIndicator('overallPass')}</th>
1040
+ <th scope="col" className="px-6 py-3 text-center">Details</th>
1041
+ </tr>
1042
+ </thead>
1043
+ <tbody>
1044
+ {sortedResults
1045
+ .filter((row, index, arr) => arr.findIndex(r => r.id === row.id) === index) // Remove any runtime duplicates
1046
+ .map((row) => (
1047
+ <tr key={row.id} className="bg-gray-800 border-b border-gray-700 hover:bg-gray-700/50">
1048
+ <td className="px-6 py-4 font-medium text-white max-w-xs truncate" title={row.URL}>{row.URL}</td>
1049
+ <td className="px-6 py-4 max-w-xs truncate" title={row.generatedTitle}>{row.generatedTitle}</td>
1050
+ <td className="px-6 py-4 max-w-xs truncate" title={row.generatedH1}>{row.generatedH1}</td>
1051
+ <td className="px-6 py-4">
1052
+ <div className="flex justify-center">
1053
+ {row.overallPass
1054
+ ? <CheckCircleIcon className="w-6 h-6 text-green-400" />
1055
+ : <XCircleIcon className="w-6 h-6 text-red-400" />
1056
+ }
1057
+ </div>
1058
+ </td>
1059
+ <td className="px-6 py-4 text-center">
1060
+ <button
1061
+ onClick={() => {
1062
+ console.log('Opening QA modal with data:', row.detailedQaReport);
1063
+ setSelectedReport(row.detailedQaReport || null);
1064
+ }}
1065
+ disabled={!row.detailedQaReport}
1066
+ className="flex items-center justify-center mx-auto gap-1 text-blue-400 hover:text-blue-300 disabled:text-gray-500 disabled:cursor-not-allowed transition-colors"
1067
+ >
1068
+ <EyeIcon className="w-5 h-5" /> View
1069
+ </button>
1070
+ </td>
1071
+ </tr>
1072
+ ))}
1073
+ </tbody>
1074
+ </table>
1075
+ </div>
1076
+ </div>
1077
+ {selectedReport && <QAReportModal report={selectedReport} onClose={() => setSelectedReport(null)} />}
1078
+ </>
1079
+ );
1080
+ };
1081
+
1082
+ export default ResultsTable;
components/SignInPage.tsx ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from "react"
2
+ import { useAuth } from "../services/authService"
3
+
4
+ export default function SignInPage() {
5
+ const [username, setUsername] = useState("")
6
+ const [password, setPassword] = useState("")
7
+ const [error, setError] = useState("")
8
+ const { login, isAuthenticated, isLoading } = useAuth()
9
+
10
+ useEffect(() => {
11
+ // Redirect if already authenticated
12
+ if (isAuthenticated) {
13
+ window.location.href = "/"
14
+ }
15
+ }, [isAuthenticated])
16
+
17
+ const handleSubmit = async (e: React.FormEvent) => {
18
+ e.preventDefault()
19
+ setError("")
20
+
21
+ const result = await login({ username, password })
22
+
23
+ if (!result.success) {
24
+ setError(result.error || "Invalid credentials")
25
+ } else {
26
+ // Redirect to main app
27
+ window.location.href = "/"
28
+ }
29
+ }
30
+
31
+ // Show loading spinner if checking auth state
32
+ if (isLoading) {
33
+ return (
34
+ <div className="min-h-screen flex items-center justify-center bg-gray-900">
35
+ <div className="text-white">Loading...</div>
36
+ </div>
37
+ )
38
+ }
39
+
40
+ return (
41
+ <div className="min-h-screen flex items-center justify-center bg-gray-900">
42
+ <div className="max-w-md w-full space-y-8 p-8">
43
+ <div>
44
+ <h2 className="mt-6 text-center text-3xl font-extrabold text-white">
45
+ ACE UI Login
46
+ </h2>
47
+ <p className="mt-2 text-center text-sm text-gray-400">
48
+ Enter your credentials to access the dashboard
49
+ </p>
50
+ </div>
51
+ <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
52
+ <div className="space-y-4">
53
+ <div>
54
+ <label htmlFor="username" className="sr-only">
55
+ Username
56
+ </label>
57
+ <input
58
+ id="username"
59
+ name="username"
60
+ type="text"
61
+ required
62
+ className="relative block w-full px-3 py-2 border border-gray-600 rounded-md placeholder-gray-400 text-white bg-gray-800 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
63
+ placeholder="Username"
64
+ value={username}
65
+ onChange={(e) => setUsername(e.target.value)}
66
+ />
67
+ </div>
68
+ <div>
69
+ <label htmlFor="password" className="sr-only">
70
+ Password
71
+ </label>
72
+ <input
73
+ id="password"
74
+ name="password"
75
+ type="password"
76
+ required
77
+ className="relative block w-full px-3 py-2 border border-gray-600 rounded-md placeholder-gray-400 text-white bg-gray-800 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
78
+ placeholder="Password"
79
+ value={password}
80
+ onChange={(e) => setPassword(e.target.value)}
81
+ />
82
+ </div>
83
+ </div>
84
+
85
+ {error && (
86
+ <div className="text-red-400 text-sm text-center">{error}</div>
87
+ )}
88
+
89
+ <div>
90
+ <button
91
+ type="submit"
92
+ disabled={isLoading}
93
+ className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
94
+ >
95
+ {isLoading ? "Signing in..." : "Sign in"}
96
+ </button>
97
+ </div>
98
+ </form>
99
+ </div>
100
+ </div>
101
+ )
102
+ }
components/Spinner.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+
4
+ const Spinner: React.FC = () => {
5
+ return (
6
+ <div className="w-5 h-5 border-2 border-t-transparent border-white rounded-full animate-spin"></div>
7
+ );
8
+ };
9
+
10
+ export default Spinner;
constants.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+
2
+ export const API_URL = 'https://api.dify.ai/v1/workflows/run';
3
+ export const API_TOKEN = 'app-yufQq1vEs336l4r2R5zx4fwo';
4
+ export const API_USER = 'ace-copywriting-frontend-user';
dist/assets/html2canvas.esm-CBrSDip1.js ADDED
The diff for this file is too large to render. See raw diff
 
dist/assets/index-D2A424vW.js ADDED
The diff for this file is too large to render. See raw diff
 
dist/assets/index.es-BN5vYZkJ.js ADDED
The diff for this file is too large to render. See raw diff
 
dist/assets/purify.es-C_uT9hQ1.js ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ /*! @license DOMPurify 2.5.8 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.5.8/LICENSE */function I(a){"@babel/helpers - typeof";return I=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(n){return typeof n}:function(n){return n&&typeof Symbol=="function"&&n.constructor===Symbol&&n!==Symbol.prototype?"symbol":typeof n},I(a)}function Fe(a,n){return Fe=Object.setPrototypeOf||function(s,c){return s.__proto__=c,s},Fe(a,n)}function zt(){if(typeof Reflect>"u"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy=="function")return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch{return!1}}function le(a,n,o){return zt()?le=Reflect.construct:le=function(c,g,y){var D=[null];D.push.apply(D,g);var x=Function.bind.apply(c,D),j=new x;return y&&Fe(j,y.prototype),j},le.apply(null,arguments)}function M(a){return Gt(a)||Wt(a)||Bt(a)||$t()}function Gt(a){if(Array.isArray(a))return Ue(a)}function Wt(a){if(typeof Symbol<"u"&&a[Symbol.iterator]!=null||a["@@iterator"]!=null)return Array.from(a)}function Bt(a,n){if(a){if(typeof a=="string")return Ue(a,n);var o=Object.prototype.toString.call(a).slice(8,-1);if(o==="Object"&&a.constructor&&(o=a.constructor.name),o==="Map"||o==="Set")return Array.from(a);if(o==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(o))return Ue(a,n)}}function Ue(a,n){(n==null||n>a.length)&&(n=a.length);for(var o=0,s=new Array(n);o<n;o++)s[o]=a[o];return s}function $t(){throw new TypeError(`Invalid attempt to spread non-iterable instance.
2
+ In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}var jt=Object.hasOwnProperty,ut=Object.setPrototypeOf,Yt=Object.isFrozen,Xt=Object.getPrototypeOf,Vt=Object.getOwnPropertyDescriptor,E=Object.freeze,b=Object.seal,qt=Object.create,vt=typeof Reflect<"u"&&Reflect,ue=vt.apply,He=vt.construct;ue||(ue=function(n,o,s){return n.apply(o,s)});E||(E=function(n){return n});b||(b=function(n){return n});He||(He=function(n,o){return le(n,M(o))});var Kt=O(Array.prototype.forEach),ft=O(Array.prototype.pop),q=O(Array.prototype.push),se=O(String.prototype.toLowerCase),we=O(String.prototype.toString),ct=O(String.prototype.match),L=O(String.prototype.replace),Zt=O(String.prototype.indexOf),Jt=O(String.prototype.trim),_=O(RegExp.prototype.test),Ce=Qt(TypeError);function O(a){return function(n){for(var o=arguments.length,s=new Array(o>1?o-1:0),c=1;c<o;c++)s[c-1]=arguments[c];return ue(a,n,s)}}function Qt(a){return function(){for(var n=arguments.length,o=new Array(n),s=0;s<n;s++)o[s]=arguments[s];return He(a,o)}}function l(a,n,o){var s;o=(s=o)!==null&&s!==void 0?s:se,ut&&ut(a,null);for(var c=n.length;c--;){var g=n[c];if(typeof g=="string"){var y=o(g);y!==g&&(Yt(n)||(n[c]=y),g=y)}a[g]=!0}return a}function U(a){var n=qt(null),o;for(o in a)ue(jt,a,[o])===!0&&(n[o]=a[o]);return n}function ie(a,n){for(;a!==null;){var o=Vt(a,n);if(o){if(o.get)return O(o.get);if(typeof o.value=="function")return O(o.value)}a=Xt(a)}function s(c){return console.warn("fallback value for",c),null}return s}var mt=E(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),Ie=E(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),xe=E(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),er=E(["animate","color-profile","cursor","discard","fedropshadow","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),ke=E(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover"]),tr=E(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),pt=E(["#text"]),dt=E(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","face","for","headers","height","hidden","high","href","hreflang","id","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","playsinline","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","xmlns","slot"]),Pe=E(["accent-height","accumulate","additive","alignment-baseline","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),Tt=E(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),oe=E(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),rr=b(/\{\{[\w\W]*|[\w\W]*\}\}/gm),ar=b(/<%[\w\W]*|[\w\W]*%>/gm),nr=b(/\${[\w\W]*}/gm),ir=b(/^data-[\-\w.\u00B7-\uFFFF]+$/),or=b(/^aria-[\-\w]+$/),lr=b(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),sr=b(/^(?:\w+script|data):/i),ur=b(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),fr=b(/^html$/i),cr=b(/^[a-z][.\w]*(-[.\w]+)+$/i),mr=function(){return typeof window>"u"?null:window},pr=function(n,o){if(I(n)!=="object"||typeof n.createPolicy!="function")return null;var s=null,c="data-tt-policy-suffix";o.currentScript&&o.currentScript.hasAttribute(c)&&(s=o.currentScript.getAttribute(c));var g="dompurify"+(s?"#"+s:"");try{return n.createPolicy(g,{createHTML:function(D){return D},createScriptURL:function(D){return D}})}catch{return console.warn("TrustedTypes policy "+g+" could not be created."),null}};function _t(){var a=arguments.length>0&&arguments[0]!==void 0?arguments[0]:mr(),n=function(e){return _t(e)};if(n.version="2.5.8",n.removed=[],!a||!a.document||a.document.nodeType!==9)return n.isSupported=!1,n;var o=a.document,s=a.document,c=a.DocumentFragment,g=a.HTMLTemplateElement,y=a.Node,D=a.Element,x=a.NodeFilter,j=a.NamedNodeMap,ht=j===void 0?a.NamedNodeMap||a.MozNamedAttrMap:j,Et=a.HTMLFormElement,At=a.DOMParser,K=a.trustedTypes,Z=D.prototype,yt=ie(Z,"cloneNode"),gt=ie(Z,"nextSibling"),St=ie(Z,"childNodes"),fe=ie(Z,"parentNode");if(typeof g=="function"){var ce=s.createElement("template");ce.content&&ce.content.ownerDocument&&(s=ce.content.ownerDocument)}var R=pr(K,o),me=R?R.createHTML(""):"",J=s,pe=J.implementation,bt=J.createNodeIterator,Ot=J.createDocumentFragment,Rt=J.getElementsByTagName,Lt=o.importNode,ze={};try{ze=U(s).documentMode?s.documentMode:{}}catch{}var N={};n.isSupported=typeof fe=="function"&&pe&&pe.createHTMLDocument!==void 0&&ze!==9;var de=rr,Te=ar,ve=nr,Mt=ir,Dt=or,Nt=sr,Ge=ur,wt=cr,_e=lr,p=null,We=l({},[].concat(M(mt),M(Ie),M(xe),M(ke),M(pt))),d=null,Be=l({},[].concat(M(dt),M(Pe),M(Tt),M(oe))),f=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Y=null,he=null,$e=!0,Ee=!0,je=!1,Ye=!0,H=!1,Ae=!0,k=!1,ye=!1,ge=!1,z=!1,Q=!1,ee=!1,Xe=!0,Ve=!1,Ct="user-content-",Se=!0,X=!1,G={},W=null,qe=l({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),Ke=null,Ze=l({},["audio","video","img","source","image","track"]),be=null,Je=l({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),te="http://www.w3.org/1998/Math/MathML",re="http://www.w3.org/2000/svg",w="http://www.w3.org/1999/xhtml",B=w,Oe=!1,Re=null,It=l({},[te,re,w],we),P,xt=["application/xhtml+xml","text/html"],kt="text/html",T,$=null,Pt=s.createElement("form"),Qe=function(e){return e instanceof RegExp||e instanceof Function},Le=function(e){$&&$===e||((!e||I(e)!=="object")&&(e={}),e=U(e),P=xt.indexOf(e.PARSER_MEDIA_TYPE)===-1?P=kt:P=e.PARSER_MEDIA_TYPE,T=P==="application/xhtml+xml"?we:se,p="ALLOWED_TAGS"in e?l({},e.ALLOWED_TAGS,T):We,d="ALLOWED_ATTR"in e?l({},e.ALLOWED_ATTR,T):Be,Re="ALLOWED_NAMESPACES"in e?l({},e.ALLOWED_NAMESPACES,we):It,be="ADD_URI_SAFE_ATTR"in e?l(U(Je),e.ADD_URI_SAFE_ATTR,T):Je,Ke="ADD_DATA_URI_TAGS"in e?l(U(Ze),e.ADD_DATA_URI_TAGS,T):Ze,W="FORBID_CONTENTS"in e?l({},e.FORBID_CONTENTS,T):qe,Y="FORBID_TAGS"in e?l({},e.FORBID_TAGS,T):{},he="FORBID_ATTR"in e?l({},e.FORBID_ATTR,T):{},G="USE_PROFILES"in e?e.USE_PROFILES:!1,$e=e.ALLOW_ARIA_ATTR!==!1,Ee=e.ALLOW_DATA_ATTR!==!1,je=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Ye=e.ALLOW_SELF_CLOSE_IN_ATTR!==!1,H=e.SAFE_FOR_TEMPLATES||!1,Ae=e.SAFE_FOR_XML!==!1,k=e.WHOLE_DOCUMENT||!1,z=e.RETURN_DOM||!1,Q=e.RETURN_DOM_FRAGMENT||!1,ee=e.RETURN_TRUSTED_TYPE||!1,ge=e.FORCE_BODY||!1,Xe=e.SANITIZE_DOM!==!1,Ve=e.SANITIZE_NAMED_PROPS||!1,Se=e.KEEP_CONTENT!==!1,X=e.IN_PLACE||!1,_e=e.ALLOWED_URI_REGEXP||_e,B=e.NAMESPACE||w,f=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&Qe(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(f.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&Qe(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(f.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements=="boolean"&&(f.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),H&&(Ee=!1),Q&&(z=!0),G&&(p=l({},M(pt)),d=[],G.html===!0&&(l(p,mt),l(d,dt)),G.svg===!0&&(l(p,Ie),l(d,Pe),l(d,oe)),G.svgFilters===!0&&(l(p,xe),l(d,Pe),l(d,oe)),G.mathMl===!0&&(l(p,ke),l(d,Tt),l(d,oe))),e.ADD_TAGS&&(p===We&&(p=U(p)),l(p,e.ADD_TAGS,T)),e.ADD_ATTR&&(d===Be&&(d=U(d)),l(d,e.ADD_ATTR,T)),e.ADD_URI_SAFE_ATTR&&l(be,e.ADD_URI_SAFE_ATTR,T),e.FORBID_CONTENTS&&(W===qe&&(W=U(W)),l(W,e.FORBID_CONTENTS,T)),Se&&(p["#text"]=!0),k&&l(p,["html","head","body"]),p.table&&(l(p,["tbody"]),delete Y.tbody),E&&E(e),$=e)},et=l({},["mi","mo","mn","ms","mtext"]),tt=l({},["annotation-xml"]),Ft=l({},["title","style","font","a","script"]),ae=l({},Ie);l(ae,xe),l(ae,er);var Me=l({},ke);l(Me,tr);var Ut=function(e){var t=fe(e);(!t||!t.tagName)&&(t={namespaceURI:B,tagName:"template"});var r=se(e.tagName),u=se(t.tagName);return Re[e.namespaceURI]?e.namespaceURI===re?t.namespaceURI===w?r==="svg":t.namespaceURI===te?r==="svg"&&(u==="annotation-xml"||et[u]):!!ae[r]:e.namespaceURI===te?t.namespaceURI===w?r==="math":t.namespaceURI===re?r==="math"&&tt[u]:!!Me[r]:e.namespaceURI===w?t.namespaceURI===re&&!tt[u]||t.namespaceURI===te&&!et[u]?!1:!Me[r]&&(Ft[r]||!ae[r]):!!(P==="application/xhtml+xml"&&Re[e.namespaceURI]):!1},S=function(e){q(n.removed,{element:e});try{e.parentNode.removeChild(e)}catch{try{e.outerHTML=me}catch{e.remove()}}},ne=function(e,t){try{q(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch{q(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),e==="is"&&!d[e])if(z||Q)try{S(t)}catch{}else try{t.setAttribute(e,"")}catch{}},rt=function(e){var t,r;if(ge)e="<remove></remove>"+e;else{var u=ct(e,/^[\r\n\t ]+/);r=u&&u[0]}P==="application/xhtml+xml"&&B===w&&(e='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+e+"</body></html>");var A=R?R.createHTML(e):e;if(B===w)try{t=new At().parseFromString(A,P)}catch{}if(!t||!t.documentElement){t=pe.createDocument(B,"template",null);try{t.documentElement.innerHTML=Oe?me:A}catch{}}var h=t.body||t.documentElement;return e&&r&&h.insertBefore(s.createTextNode(r),h.childNodes[0]||null),B===w?Rt.call(t,k?"html":"body")[0]:k?t.documentElement:h},at=function(e){return bt.call(e.ownerDocument||e,e,x.SHOW_ELEMENT|x.SHOW_COMMENT|x.SHOW_TEXT|x.SHOW_PROCESSING_INSTRUCTION|x.SHOW_CDATA_SECTION,null,!1)},De=function(e){return e instanceof Et&&(typeof e.nodeName!="string"||typeof e.textContent!="string"||typeof e.removeChild!="function"||!(e.attributes instanceof ht)||typeof e.removeAttribute!="function"||typeof e.setAttribute!="function"||typeof e.namespaceURI!="string"||typeof e.insertBefore!="function"||typeof e.hasChildNodes!="function")},V=function(e){return I(y)==="object"?e instanceof y:e&&I(e)==="object"&&typeof e.nodeType=="number"&&typeof e.nodeName=="string"},C=function(e,t,r){N[e]&&Kt(N[e],function(u){u.call(n,t,r,$)})},nt=function(e){var t;if(C("beforeSanitizeElements",e,null),De(e)||_(/[\u0080-\uFFFF]/,e.nodeName))return S(e),!0;var r=T(e.nodeName);if(C("uponSanitizeElement",e,{tagName:r,allowedTags:p}),e.hasChildNodes()&&!V(e.firstElementChild)&&(!V(e.content)||!V(e.content.firstElementChild))&&_(/<[/\w]/g,e.innerHTML)&&_(/<[/\w]/g,e.textContent)||r==="select"&&_(/<template/i,e.innerHTML)||e.nodeType===7||Ae&&e.nodeType===8&&_(/<[/\w]/g,e.data))return S(e),!0;if(!p[r]||Y[r]){if(!Y[r]&&ot(r)&&(f.tagNameCheck instanceof RegExp&&_(f.tagNameCheck,r)||f.tagNameCheck instanceof Function&&f.tagNameCheck(r)))return!1;if(Se&&!W[r]){var u=fe(e)||e.parentNode,A=St(e)||e.childNodes;if(A&&u)for(var h=A.length,v=h-1;v>=0;--v){var F=yt(A[v],!0);F.__removalCount=(e.__removalCount||0)+1,u.insertBefore(F,gt(e))}}return S(e),!0}return e instanceof D&&!Ut(e)||(r==="noscript"||r==="noembed"||r==="noframes")&&_(/<\/no(script|embed|frames)/i,e.innerHTML)?(S(e),!0):(H&&e.nodeType===3&&(t=e.textContent,t=L(t,de," "),t=L(t,Te," "),t=L(t,ve," "),e.textContent!==t&&(q(n.removed,{element:e.cloneNode()}),e.textContent=t)),C("afterSanitizeElements",e,null),!1)},it=function(e,t,r){if(Xe&&(t==="id"||t==="name")&&(r in s||r in Pt))return!1;if(!(Ee&&!he[t]&&_(Mt,t))){if(!($e&&_(Dt,t))){if(!d[t]||he[t]){if(!(ot(e)&&(f.tagNameCheck instanceof RegExp&&_(f.tagNameCheck,e)||f.tagNameCheck instanceof Function&&f.tagNameCheck(e))&&(f.attributeNameCheck instanceof RegExp&&_(f.attributeNameCheck,t)||f.attributeNameCheck instanceof Function&&f.attributeNameCheck(t))||t==="is"&&f.allowCustomizedBuiltInElements&&(f.tagNameCheck instanceof RegExp&&_(f.tagNameCheck,r)||f.tagNameCheck instanceof Function&&f.tagNameCheck(r))))return!1}else if(!be[t]){if(!_(_e,L(r,Ge,""))){if(!((t==="src"||t==="xlink:href"||t==="href")&&e!=="script"&&Zt(r,"data:")===0&&Ke[e])){if(!(je&&!_(Nt,L(r,Ge,"")))){if(r)return!1}}}}}}return!0},ot=function(e){return e!=="annotation-xml"&&ct(e,wt)},lt=function(e){var t,r,u,A;C("beforeSanitizeAttributes",e,null);var h=e.attributes;if(!(!h||De(e))){var v={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:d};for(A=h.length;A--;){t=h[A];var F=t,m=F.name,Ne=F.namespaceURI;if(r=m==="value"?t.value:Jt(t.value),u=T(m),v.attrName=u,v.attrValue=r,v.keepAttr=!0,v.forceKeepAttr=void 0,C("uponSanitizeAttribute",e,v),r=v.attrValue,!v.forceKeepAttr&&(ne(m,e),!!v.keepAttr)){if(!Ye&&_(/\/>/i,r)){ne(m,e);continue}H&&(r=L(r,de," "),r=L(r,Te," "),r=L(r,ve," "));var st=T(e.nodeName);if(it(st,u,r)){if(Ve&&(u==="id"||u==="name")&&(ne(m,e),r=Ct+r),Ae&&_(/((--!?|])>)|<\/(style|title)/i,r)){ne(m,e);continue}if(R&&I(K)==="object"&&typeof K.getAttributeType=="function"&&!Ne)switch(K.getAttributeType(st,u)){case"TrustedHTML":{r=R.createHTML(r);break}case"TrustedScriptURL":{r=R.createScriptURL(r);break}}try{Ne?e.setAttributeNS(Ne,m,r):e.setAttribute(m,r),De(e)?S(e):ft(n.removed)}catch{}}}}C("afterSanitizeAttributes",e,null)}},Ht=function i(e){var t,r=at(e);for(C("beforeSanitizeShadowDOM",e,null);t=r.nextNode();)C("uponSanitizeShadowNode",t,null),nt(t),lt(t),t.content instanceof c&&i(t.content);C("afterSanitizeShadowDOM",e,null)};return n.sanitize=function(i){var e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},t,r,u,A,h;if(Oe=!i,Oe&&(i="<!-->"),typeof i!="string"&&!V(i))if(typeof i.toString=="function"){if(i=i.toString(),typeof i!="string")throw Ce("dirty is not a string, aborting")}else throw Ce("toString is not a function");if(!n.isSupported){if(I(a.toStaticHTML)==="object"||typeof a.toStaticHTML=="function"){if(typeof i=="string")return a.toStaticHTML(i);if(V(i))return a.toStaticHTML(i.outerHTML)}return i}if(ye||Le(e),n.removed=[],typeof i=="string"&&(X=!1),X){if(i.nodeName){var v=T(i.nodeName);if(!p[v]||Y[v])throw Ce("root node is forbidden and cannot be sanitized in-place")}}else if(i instanceof y)t=rt("<!---->"),r=t.ownerDocument.importNode(i,!0),r.nodeType===1&&r.nodeName==="BODY"||r.nodeName==="HTML"?t=r:t.appendChild(r);else{if(!z&&!H&&!k&&i.indexOf("<")===-1)return R&&ee?R.createHTML(i):i;if(t=rt(i),!t)return z?null:ee?me:""}t&&ge&&S(t.firstChild);for(var F=at(X?i:t);u=F.nextNode();)u.nodeType===3&&u===A||(nt(u),lt(u),u.content instanceof c&&Ht(u.content),A=u);if(A=null,X)return i;if(z){if(Q)for(h=Ot.call(t.ownerDocument);t.firstChild;)h.appendChild(t.firstChild);else h=t;return(d.shadowroot||d.shadowrootmod)&&(h=Lt.call(o,h,!0)),h}var m=k?t.outerHTML:t.innerHTML;return k&&p["!doctype"]&&t.ownerDocument&&t.ownerDocument.doctype&&t.ownerDocument.doctype.name&&_(fr,t.ownerDocument.doctype.name)&&(m="<!DOCTYPE "+t.ownerDocument.doctype.name+`>
3
+ `+m),H&&(m=L(m,de," "),m=L(m,Te," "),m=L(m,ve," ")),R&&ee?R.createHTML(m):m},n.setConfig=function(i){Le(i),ye=!0},n.clearConfig=function(){$=null,ye=!1},n.isValidAttribute=function(i,e,t){$||Le({});var r=T(i),u=T(e);return it(r,u,t)},n.addHook=function(i,e){typeof e=="function"&&(N[i]=N[i]||[],q(N[i],e))},n.removeHook=function(i){if(N[i])return ft(N[i])},n.removeHooks=function(i){N[i]&&(N[i]=[])},n.removeAllHooks=function(){N={}},n}var Tr=_t();export{Tr as default};
dist/index.html ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>ACE Copywriting Pipeline</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
10
+
11
+ <link rel="stylesheet" href="/index.css">
12
+ <script type="importmap">
13
+ {
14
+ "imports": {
15
+ "react": "https://esm.sh/react@^19.1.1",
16
+ "react-dom/": "https://esm.sh/react-dom@^19.1.1/",
17
+ "react/": "https://esm.sh/react@^19.1.1/"
18
+ }
19
+ }
20
+ </script>
21
+ <script type="module" crossorigin src="/assets/index-D2A424vW.js"></script>
22
+ </head>
23
+ <body class="bg-gray-900 text-gray-100 font-sans antialiased">
24
+ <div id="root" class="h-screen"></div>
25
+ </body>
26
+
27
+ <script
28
+ src="https://udify.app/embed.min.js"
29
+ id="U5KHVj67e4Gum8hd"
30
+ defer>
31
+ </script>
32
+ <style>
33
+ #dify-chatbot-bubble-button {
34
+ background-color: #1C64F2 !important;
35
+ }
36
+ #dify-chatbot-bubble-window {
37
+ width: 24rem !important;
38
+ height: 40rem !important;
39
+ }
40
+ </style>
41
+
42
+ </html>
index.html ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>ACE Copywriting Pipeline</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
10
+ <script type="importmap">
11
+ {
12
+ "imports": {
13
+ "react": "https://esm.sh/react@^19.1.1",
14
+ "react-dom/": "https://esm.sh/react-dom@^19.1.1/",
15
+ "react/": "https://esm.sh/react@^19.1.1/"
16
+ }
17
+ }
18
+ </script>
19
+ <link rel="stylesheet" href="/index.css">
20
+ </head>
21
+ <body class="bg-gray-900 text-gray-100 font-sans antialiased">
22
+ <div id="root" class="h-screen"></div>
23
+ <script type="module" src="/index.tsx"></script>
24
+ </body>
25
+
26
+ <script type="module" src="/src/config/difyChatbotConfig.ts"></script>
27
+ <script
28
+ src="https://udify.app/embed.min.js"
29
+ id="U5KHVj67e4Gum8hd"
30
+ defer>
31
+ </script>
32
+ <style>
33
+ #dify-chatbot-bubble-button {
34
+ background-color: #1C64F2 !important;
35
+ }
36
+ #dify-chatbot-bubble-window {
37
+ width: 24rem !important;
38
+ height: 40rem !important;
39
+ }
40
+ </style>
41
+
42
+ </html>
index.tsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import ReactDOM from 'react-dom/client';
4
+ import App from './App';
5
+
6
+ const rootElement = document.getElementById('root');
7
+ if (!rootElement) {
8
+ throw new Error("Could not find root element to mount to");
9
+ }
10
+
11
+ const root = ReactDOM.createRoot(rootElement);
12
+ root.render(
13
+ <React.StrictMode>
14
+ <App />
15
+ </React.StrictMode>
16
+ );
metadata.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "ACE Copywriting Pipeline",
3
+ "emoji": "✍️",
4
+ "colorFrom": "blue",
5
+ "colorTo": "green",
6
+ "sdk": "docker",
7
+ "app_port": 7860,
8
+ "pinned": false,
9
+ "license": "mit",
10
+ "description": "AI-powered copywriting pipeline for generating and quality-assuring content for ACE Hardware pages. Upload CSV files to generate optimized titles, meta descriptions, H1 tags, and full copy content with built-in QA validation.",
11
+ "tags": [
12
+ "copywriting",
13
+ "ai",
14
+ "content-generation",
15
+ "seo",
16
+ "quality-assurance",
17
+ "react",
18
+ "typescript",
19
+ "csv-processing"
20
+ ]
21
+ }
package.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ace-copywriting-pipeline",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "docx": "^9.5.1",
13
+ "jspdf": "^2.5.2",
14
+ "react": "^19.1.1",
15
+ "react-dom": "^19.1.1",
16
+ "react-markdown": "^10.1.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.14.0",
20
+ "typescript": "~5.8.2",
21
+ "vite": "^6.2.0"
22
+ }
23
+ }
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # This file is for HuggingFace Spaces compatibility
2
+ # The actual application runs in a Docker container with Node.js dependencies
3
+ # defined in package.json
4
+
5
+ # No Python dependencies required - this is a React/Node.js application
services/authService.ts ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Types
2
+ export interface User {
3
+ id: string
4
+ name: string
5
+ email: string
6
+ }
7
+
8
+ export interface AuthState {
9
+ user: User | null
10
+ isAuthenticated: boolean
11
+ isLoading: boolean
12
+ }
13
+
14
+ export interface LoginCredentials {
15
+ username: string
16
+ password: string
17
+ }
18
+
19
+ // Environment-based configuration with fallbacks
20
+ const getAuthConfig = () => {
21
+ // Get credentials from environment variables using type assertion
22
+ const env = (import.meta as any).env
23
+ const usernameHash = env.VITE_AUTH_USERNAME_HASH
24
+ const passwordHash = env.VITE_AUTH_PASSWORD_HASH
25
+
26
+ // Validate that required environment variables are present
27
+ if (!usernameHash || !passwordHash) {
28
+ console.error('Missing required authentication environment variables!')
29
+ console.error('Please set VITE_AUTH_USERNAME_HASH and VITE_AUTH_PASSWORD_HASH in your .env file')
30
+ throw new Error('Authentication configuration missing. Check environment variables.')
31
+ }
32
+
33
+ return {
34
+ username: usernameHash,
35
+ password: passwordHash
36
+ }
37
+ }
38
+
39
+ const getUserConfig = (): User => {
40
+ const env = (import.meta as any).env
41
+ return {
42
+ id: env.VITE_AUTH_USER_ID || "1",
43
+ name: env.VITE_AUTH_USER_NAME || "ACE Admin",
44
+ email: env.VITE_AUTH_USER_EMAIL || "admin@aceui.com"
45
+ }
46
+ }
47
+
48
+ // Helper function to hash input with SHA-256 using Web Crypto API (browser-compatible)
49
+ async function hashInput(input: string): Promise<string> {
50
+ const encoder = new TextEncoder()
51
+ const data = encoder.encode(input)
52
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data)
53
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
54
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
55
+ return hashHex
56
+ }
57
+
58
+ // JWT-like token management (simplified for client-side)
59
+ const TOKEN_KEY = 'ace_auth_token'
60
+ const USER_KEY = 'ace_auth_user'
61
+ const TOKEN_EXPIRY_HOURS = 24
62
+
63
+ interface TokenData {
64
+ user: User
65
+ timestamp: number
66
+ expiresAt: number
67
+ }
68
+
69
+ class AuthService {
70
+ private listeners: Set<(authState: AuthState) => void> = new Set()
71
+ private currentState: AuthState = {
72
+ user: null,
73
+ isAuthenticated: false,
74
+ isLoading: true
75
+ }
76
+
77
+ constructor() {
78
+ this.checkExistingSession()
79
+ }
80
+
81
+ // Check for existing valid session on initialization
82
+ private checkExistingSession() {
83
+ try {
84
+ const token = localStorage.getItem(TOKEN_KEY)
85
+ const userData = localStorage.getItem(USER_KEY)
86
+
87
+ if (token && userData) {
88
+ const tokenData: TokenData = JSON.parse(token)
89
+ const user: User = JSON.parse(userData)
90
+
91
+ // Check if token is still valid
92
+ if (Date.now() < tokenData.expiresAt) {
93
+ this.updateState({
94
+ user,
95
+ isAuthenticated: true,
96
+ isLoading: false
97
+ })
98
+ return
99
+ } else {
100
+ // Token expired, clear storage
101
+ this.clearSession()
102
+ }
103
+ }
104
+ } catch (error) {
105
+ console.error('Error checking existing session:', error)
106
+ this.clearSession()
107
+ }
108
+
109
+ this.updateState({
110
+ user: null,
111
+ isAuthenticated: false,
112
+ isLoading: false
113
+ })
114
+ }
115
+
116
+ // Subscribe to auth state changes
117
+ subscribe(callback: (authState: AuthState) => void) {
118
+ this.listeners.add(callback)
119
+ // Immediately call with current state
120
+ callback(this.currentState)
121
+
122
+ // Return unsubscribe function
123
+ return () => {
124
+ this.listeners.delete(callback)
125
+ }
126
+ }
127
+
128
+ // Update state and notify listeners
129
+ private updateState(newState: AuthState) {
130
+ this.currentState = newState
131
+ this.listeners.forEach(callback => callback(newState))
132
+ }
133
+
134
+ // Get current auth state
135
+ getState(): AuthState {
136
+ return this.currentState
137
+ }
138
+
139
+ // Login with credentials
140
+ async login(credentials: LoginCredentials): Promise<{ success: boolean; error?: string }> {
141
+ try {
142
+ this.updateState({ ...this.currentState, isLoading: true })
143
+
144
+ // Simulate network delay for UX
145
+ await new Promise(resolve => setTimeout(resolve, 500))
146
+
147
+ const hashedUsername = await hashInput(credentials.username)
148
+ const hashedPassword = await hashInput(credentials.password)
149
+
150
+ // Get auth configuration
151
+ const validCredentials = getAuthConfig()
152
+ const validUser = getUserConfig()
153
+
154
+ // Validate credentials
155
+ if (hashedUsername === validCredentials.username &&
156
+ hashedPassword === validCredentials.password) {
157
+
158
+ // Create session token
159
+ const now = Date.now()
160
+ const expiresAt = now + (TOKEN_EXPIRY_HOURS * 60 * 60 * 1000)
161
+
162
+ const tokenData: TokenData = {
163
+ user: validUser,
164
+ timestamp: now,
165
+ expiresAt
166
+ }
167
+
168
+ // Store in localStorage
169
+ localStorage.setItem(TOKEN_KEY, JSON.stringify(tokenData))
170
+ localStorage.setItem(USER_KEY, JSON.stringify(validUser))
171
+
172
+ this.updateState({
173
+ user: validUser,
174
+ isAuthenticated: true,
175
+ isLoading: false
176
+ })
177
+
178
+ return { success: true }
179
+ } else {
180
+ this.updateState({
181
+ user: null,
182
+ isAuthenticated: false,
183
+ isLoading: false
184
+ })
185
+ return { success: false, error: 'Invalid username or password' }
186
+ }
187
+ } catch (error) {
188
+ console.error('Login error:', error)
189
+ this.updateState({
190
+ user: null,
191
+ isAuthenticated: false,
192
+ isLoading: false
193
+ })
194
+ return { success: false, error: 'An error occurred during login' }
195
+ }
196
+ }
197
+
198
+ // Logout
199
+ async logout(): Promise<void> {
200
+ this.clearSession()
201
+ this.updateState({
202
+ user: null,
203
+ isAuthenticated: false,
204
+ isLoading: false
205
+ })
206
+ }
207
+
208
+ // Clear session data
209
+ private clearSession() {
210
+ localStorage.removeItem(TOKEN_KEY)
211
+ localStorage.removeItem(USER_KEY)
212
+ }
213
+
214
+ // Check if session is valid
215
+ isSessionValid(): boolean {
216
+ try {
217
+ const token = localStorage.getItem(TOKEN_KEY)
218
+ if (!token) return false
219
+
220
+ const tokenData: TokenData = JSON.parse(token)
221
+ return Date.now() < tokenData.expiresAt
222
+ } catch {
223
+ return false
224
+ }
225
+ }
226
+ }
227
+
228
+ // Create singleton instance
229
+ export const authService = new AuthService()
230
+
231
+ // React hook for using auth service
232
+ export function useAuth(): AuthState & {
233
+ login: (credentials: LoginCredentials) => Promise<{ success: boolean; error?: string }>
234
+ logout: () => Promise<void>
235
+ } {
236
+ const [authState, setAuthState] = React.useState<AuthState>(authService.getState())
237
+
238
+ React.useEffect(() => {
239
+ const unsubscribe = authService.subscribe(setAuthState)
240
+ return unsubscribe
241
+ }, [])
242
+
243
+ return {
244
+ ...authState,
245
+ login: authService.login.bind(authService),
246
+ logout: authService.logout.bind(authService)
247
+ }
248
+ }
249
+
250
+ // React import for the hook
251
+ import React from 'react'
services/difyService.ts ADDED
@@ -0,0 +1,1468 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { API_URL, API_TOKEN, API_USER } from '../constants';
2
+ import { ApiInput, ApiResponseOutput, ProcessedResult, QaSectionResult, DetailedQaReport } from '../types';
3
+
4
+ const MAX_RETRIES = 3;
5
+ const INITIAL_RETRY_DELAY_MS = 1000;
6
+ const RETRYABLE_STATUS_CODES = [429, 502, 503, 504]; // 429: Too Many Requests, 5xx: Server Errors
7
+
8
+ // Add jitter to delay to prevent thundering herd problem
9
+ const delay = (ms: number) => new Promise(res => setTimeout(res, ms + Math.random() * 500));
10
+
11
+ // Structured error for workflow failures
12
+ class WorkflowError extends Error {
13
+ code: string;
14
+ at: 'network' | 'api' | 'stream' | 'parse' | 'unknown';
15
+ debug?: string;
16
+ constructor(code: string, message: string, at: 'network' | 'api' | 'stream' | 'parse' | 'unknown' = 'unknown', debug?: string) {
17
+ super(message);
18
+ this.name = 'WorkflowError';
19
+ this.code = code;
20
+ this.at = at;
21
+ this.debug = debug;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Removes <think>...</think> blocks from a string.
27
+ */
28
+ const cleanResponseText = (text: string): string => {
29
+ if (typeof text !== 'string') return '';
30
+ return text.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
31
+ };
32
+
33
+ /**
34
+ * Parses a single section of the QA report (e.g., TITLE, H1).
35
+ * This uses regular expressions to be robust against multiline content and format variations.
36
+ * @param sectionText The text content of a single QA section.
37
+ * @returns A structured object with the section's results.
38
+ */
39
+ const parseSection = (sectionText: string): QaSectionResult => {
40
+ console.log('Parsing section text:', sectionText.substring(0, 200));
41
+
42
+ // ROBUST GRADE EXTRACTION - handles multiple formats
43
+ let grade = 'N/A';
44
+ let gradeMatch = null;
45
+
46
+ // Try various grade patterns in order of specificity
47
+ const gradePatterns = [
48
+ /-\s*\*\*Grade:\*\*\s*(.*)/, // - **Grade:** 100/100
49
+ /•\s*\*\*Grade:\*\*\s*(.*)/, // • **Grade:** 100/100
50
+ /\*\s*\*\*Grade:\*\*\s*(.*)/, // * **Grade:** 100/100
51
+ /-\s*\*\*Grade\*\*:\s*(.*)/, // - **Grade**: 100/100 (colon without space)
52
+ /•\s*\*\*Grade\*\*:\s*(.*)/, // • **Grade**: 100/100 (colon without space)
53
+ /\*\s*\*\*Grade\*\*:\s*(.*)/, // * **Grade**: 100/100
54
+ /(?:•|-|\*)\s*\*\*Grade\*\*:?:\s*(.*)/, // •/**/- **Grade**: 100/100 or - **Grade** 100/100
55
+ /(?:•|-|\*)\s*Grade:?:\s*(.*)/, // • Grade: 100/100 or - Grade 100/100
56
+ /Grade:?:\s*(\d+\/\d+|\d+)/m, // Grade: 100/100 (anywhere in text)
57
+ /(\d+\/\d+)\s*(?:grade|Grade)/ // 100/100 grade (reverse order)
58
+ ];
59
+
60
+ for (const pattern of gradePatterns) {
61
+ gradeMatch = sectionText.match(pattern);
62
+ if (gradeMatch) {
63
+ grade = gradeMatch[1].trim();
64
+ break;
65
+ }
66
+ }
67
+ console.log('Grade match result:', gradeMatch, 'Final grade:', grade);
68
+
69
+ // ROBUST PASS EXTRACTION - handles multiple formats including the actual QA Guard format
70
+ let pass = false;
71
+ let passMatch = null;
72
+
73
+ // Try various pass patterns in order of specificity - FIXED TO HANDLE ACTUAL QA GUARD FORMAT
74
+ const passPatterns = [
75
+ // First check for the actual QA Guard format: "### **TITLE: PASS** ✅"
76
+ /###\s*\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*\s*(✅|❌)/i,
77
+ /###\s*\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*/i,
78
+ /\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*\s*(✅|❌)/i,
79
+ /\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*/i,
80
+
81
+ // Then check for traditional patterns
82
+ /-\s*\*\*Pass:\*\*\s*(.*)/i, // - **Pass:** true
83
+ /•\s*\*\*Pass:\*\*\s*(.*)/i, // • **Pass:** true
84
+ /\*\s*\*\*Pass:\*\*\s*(.*)/i, // * **Pass:** true
85
+ /-\s*\*\*Pass\*\*:\s*(.*)/i, // - **Pass**: true (colon without space)
86
+ /•\s*\*\*Pass\*\*:\s*(.*)/i, // • **Pass**: true (colon without space)
87
+ /\*\s*\*\*Pass\*\*:\s*(.*)/i, // * **Pass**: true (colon without space)
88
+ /(?:•|-|\*)\s*\*\*Pass\*\*:?:\s*(.*)/i, // •/**/- **Pass**: true
89
+ /(?:•|-|\*)\s*Pass:?:\s*(.*)/i, // • Pass: true
90
+ /Pass:?:\s*(true|false|✅|❌|TRUE|FALSE)/im, // Pass: true (anywhere)
91
+ /(true|false|✅|❌|TRUE|FALSE)\s*pass/im // true pass (reverse)
92
+ ];
93
+
94
+ for (const pattern of passPatterns) {
95
+ passMatch = sectionText.match(pattern);
96
+ if (passMatch) {
97
+ const passValue = passMatch[1].toLowerCase().trim();
98
+ pass = passValue.includes('pass') ||
99
+ passValue.includes('true') ||
100
+ passValue.includes('✅') ||
101
+ passValue === 'yes' ||
102
+ passValue === 'passed';
103
+ break;
104
+ }
105
+ }
106
+ console.log('Pass match result:', passMatch, 'Final pass:', pass);
107
+
108
+ // ROBUST ERRORS EXTRACTION - handles multiple formats
109
+ let errors: string[] = ['No errors reported.'];
110
+ let errorsMatch = null;
111
+
112
+ // Try various error patterns
113
+ const errorPatterns = [
114
+ /-\s*\*\*Errors:\*\*\s*([\s\S]*?)(?=\n-\s*\*\*|$)/, // - **Errors:** []
115
+ /•\s*\*\*Errors:\*\*\s*([\s\S]*?)(?=\n•\s*\*\*|$)/, // • **Errors:** []
116
+ /\*\s*\*\*Errors:\*\*\s*([\s\S]*?)(?=\n\*\s*\*\*|$)/, // * **Errors:** []
117
+ /(?:•|-|\*)\s*\*\*Errors?\*\*:?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, // Generic bullet/dash/star + bold
118
+ /(?:•|-|\*)\s*Errors:?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, // Generic bullet/dash/star + no bold
119
+ /Errors:?:\s*([\s\S]*?)(?=\n(?:•|-|\*|\*\*)|$)/m // Errors: anywhere in text
120
+ ];
121
+
122
+ for (const pattern of errorPatterns) {
123
+ errorsMatch = sectionText.match(pattern);
124
+ if (errorsMatch) break;
125
+ }
126
+
127
+ if (errorsMatch) {
128
+ const errorsBlock = errorsMatch[1].trim();
129
+
130
+ if (errorsBlock === '[]' || !errorsBlock || errorsBlock.toLowerCase() === 'none') {
131
+ errors = ['No errors reported.'];
132
+ } else if (errorsBlock.startsWith('[') && errorsBlock.includes(']')) {
133
+ // Handle array format: []
134
+ try {
135
+ const parsed = JSON.parse(errorsBlock);
136
+ errors = Array.isArray(parsed) && parsed.length > 0 ? parsed : ['No errors reported.'];
137
+ } catch {
138
+ // If JSON parsing fails, treat as plain text
139
+ errors = [errorsBlock.replace(/[\[\]]/g, '').trim()];
140
+ }
141
+ } else {
142
+ // Handle multi-line bullet format or plain text
143
+ const lines = errorsBlock.split('\n').map(e => e.trim().replace(/^[-•\*]\s*/, '')).filter(Boolean);
144
+ errors = lines.length > 0 ? lines : ['No errors reported.'];
145
+ }
146
+ }
147
+
148
+ // ENHANCED LOGIC: If we have a grade of 100/100 and no errors, but pass is still false,
149
+ // we should override the pass status based on the grade and errors
150
+ if (grade === '100/100' && (!errors || errors.length === 0 || errors[0] === 'No errors reported.')) {
151
+ console.log('Overriding pass status: Grade is 100/100 and no errors, setting pass to true');
152
+ pass = true;
153
+ }
154
+
155
+ // Additional logic: If grade is high (80+) and no errors, likely a pass
156
+ if (grade !== 'N/A' && grade !== '0/100') {
157
+ const gradeNum = parseInt(grade.split('/')[0]);
158
+ if (gradeNum >= 80 && (!errors || errors.length === 0 || errors[0] === 'No errors reported.')) {
159
+ console.log(`Overriding pass status: Grade is ${grade} (${gradeNum}/100) and no errors, setting pass to true`);
160
+ pass = true;
161
+ }
162
+ }
163
+
164
+ // ROBUST ANALYSIS/CORRECTED CONTENT EXTRACTION - handles multiple formats
165
+ let corrected = 'Content analysis not available.';
166
+ let contentMatch = null;
167
+
168
+ // Try various content patterns - Analysis, Corrected, or any descriptive text
169
+ const contentPatterns = [
170
+ /-\s*\*\*Analysis:\*\*\s*([\s\S]*?)(?=\n-\s*\*\*|$)/, // - **Analysis:** text
171
+ /•\s*\*\*Analysis:\*\*\s*([\s\S]*?)(?=\n•\s*\*\*|$)/, // • **Analysis:** text
172
+ /\*\s*\*\*Analysis:\*\*\s*([\s\S]*?)(?=\n\*\s*\*\*|$)/, // * **Analysis:** text
173
+ /-\s*\*\*Corrected:\*\*\s*([\s\S]*?)(?=\n-\s*\*\*|$)/, // - **Corrected:** text
174
+ /•\s*\*\*Corrected:\*\*\s*([\s\S]*?)(?=\n•\s*\*\*|$)/, // • **Corrected:** text
175
+ /\*\s*\*\*Corrected:\*\*\s*([\s\S]*?)(?=\n\*\s*\*\*|$)/, // * **Corrected:** text
176
+ /(?:•|-|\*)\s*\*\*(?:Analysis|Corrected)\*\*:?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, // Generic
177
+ /(?:•|-|\*)\s*(?:Analysis|Corrected):?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, // No bold
178
+ /Analysis:?:\s*([\s\S]*?)(?=\n(?:•|-|\*|\*\*)|$)/m, // Analysis: anywhere
179
+ /Corrected:?:\s*([\s\S]*?)(?=\n(?:•|-|\*|\*\*)|$)/m // Corrected: anywhere
180
+ ];
181
+
182
+ for (const pattern of contentPatterns) {
183
+ contentMatch = sectionText.match(pattern);
184
+ if (contentMatch) {
185
+ corrected = contentMatch[1].trim();
186
+ break;
187
+ }
188
+ }
189
+
190
+ // If no Analysis/Corrected found, extract the section title/content as fallback
191
+ if (!contentMatch || corrected.length < 10) {
192
+ // Extract title or first meaningful content line
193
+ const lines = sectionText.split('\n').map(l => l.trim()).filter(Boolean);
194
+ const titleLine = lines.find(line => !line.startsWith('•') && !line.startsWith('-') && !line.startsWith('*') && !line.includes('**') && line.length > 10);
195
+ if (titleLine) {
196
+ corrected = titleLine;
197
+ }
198
+ }
199
+
200
+ // Clean up any extra formatting
201
+ corrected = corrected.replace(/^#\s*/, '').replace(/###\s*\*\*[^*]+\*\*/, '').trim();
202
+ console.log('Content match result:', contentMatch, 'Final corrected:', corrected.substring(0, 50));
203
+
204
+ console.log('Final section result - Grade:', grade, 'Pass:', pass, 'Errors:', errors);
205
+ return { grade, pass, errors, corrected };
206
+ };
207
+
208
+
209
+ /**
210
+ * Parses the structured QA report format that comes as plain text with sections.
211
+ * @param qaText The raw structured QA text from the API.
212
+ * @returns An object containing the detailed parsed report and top-level pass/grade info.
213
+ */
214
+ const parseStructuredQaReport = (qaText: string): { detailedQaReport: DetailedQaReport, overallPass: boolean, overallGrade: string } => {
215
+ const defaultSection: QaSectionResult = { grade: 'N/A', pass: false, errors: ['Parsing failed'], corrected: '' };
216
+ const defaultReport: DetailedQaReport = {
217
+ title: { ...defaultSection },
218
+ meta: { ...defaultSection },
219
+ h1: { ...defaultSection },
220
+ copy: { ...defaultSection },
221
+ overall: { grade: 'N/A', pass: false, primaryIssue: 'Parsing failed' }
222
+ };
223
+
224
+ try {
225
+ // Split the text into sections by looking for section headers
226
+ const sections = qaText.split(/(?=^## [A-Z]+)/gm).filter(Boolean);
227
+
228
+ const parsedData: Partial<DetailedQaReport> = {};
229
+
230
+ sections.forEach(sectionText => {
231
+ const lines = sectionText.trim().split('\n');
232
+ const header = lines[0]?.replace('## ', '').trim().toLowerCase() || '';
233
+
234
+ // Extract grade
235
+ const gradeLine = lines.find(line => line.includes('- Grade:'))?.trim() || '';
236
+ const gradeMatch = gradeLine.match(/- Grade:\s*(\d+)\/100/) || gradeLine.match(/- Grade:\s*([^\n]+)/);
237
+ const grade = gradeMatch ? gradeMatch[1].trim() : 'N/A';
238
+
239
+ // Extract pass status
240
+ const passLine = lines.find(line => line.includes('- Pass:'))?.trim() || '';
241
+ const passMatch = passLine.match(/- Pass:\s*(true|false)/i);
242
+ const pass = passMatch ? passMatch[1].toLowerCase() === 'true' : false;
243
+
244
+ // Extract errors
245
+ let errors: string[] = [];
246
+ const errorsLineIndex = lines.findIndex(line => line.includes('- Errors:'));
247
+ if (errorsLineIndex !== -1) {
248
+ const errorsContent = lines[errorsLineIndex].replace('- Errors:', '').trim();
249
+ if (errorsContent === '[]' || errorsContent === '') {
250
+ errors = ['No errors reported.'];
251
+ } else {
252
+ // Look for multi-line errors
253
+ let errorText = errorsContent;
254
+ for (let i = errorsLineIndex + 1; i < lines.length; i++) {
255
+ if (lines[i].startsWith('- ') && !lines[i].startsWith(' ')) break;
256
+ errorText += '\n' + lines[i].trim();
257
+ }
258
+
259
+ // Parse error list
260
+ if (errorText.startsWith('[') && errorText.includes(']')) {
261
+ // Handle array format
262
+ try {
263
+ const parsedErrors = JSON.parse(errorText);
264
+ errors = Array.isArray(parsedErrors) ? parsedErrors : [errorText];
265
+ } catch {
266
+ errors = [errorText];
267
+ }
268
+ } else {
269
+ // Handle plain text or bullet list
270
+ errors = errorText.split('\n')
271
+ .map(e => e.trim().replace(/^- /, ''))
272
+ .filter(Boolean);
273
+ }
274
+
275
+ if (errors.length === 0) {
276
+ errors = ['No errors reported.'];
277
+ }
278
+ }
279
+ } else {
280
+ errors = ['Errors not found.'];
281
+ }
282
+
283
+ // Extract corrected content
284
+ let corrected = '';
285
+ const correctedLineIndex = lines.findIndex(line => line.includes('- Corrected:'));
286
+ if (correctedLineIndex !== -1) {
287
+ corrected = lines.slice(correctedLineIndex)
288
+ .join('\n')
289
+ .replace('- Corrected:', '')
290
+ .trim();
291
+ } else {
292
+ corrected = 'Correction not found.';
293
+ }
294
+
295
+ const sectionResult: QaSectionResult = { grade, pass, errors, corrected };
296
+
297
+ if (header.includes('title')) {
298
+ parsedData.title = sectionResult;
299
+ } else if (header.includes('meta')) {
300
+ parsedData.meta = sectionResult;
301
+ } else if (header.includes('h1')) {
302
+ parsedData.h1 = sectionResult;
303
+ } else if (header.includes('copy')) {
304
+ parsedData.copy = sectionResult;
305
+ } else if (header.includes('overall')) {
306
+ // Extract primary issue for overall section
307
+ const primaryIssueLine = lines.find(line => line.includes('- Primary Issue:'))?.trim() || '';
308
+ const primaryIssue = primaryIssueLine.replace('- Primary Issue:', '').trim() || 'Not specified.';
309
+ parsedData.overall = { grade, pass, primaryIssue };
310
+ }
311
+ });
312
+
313
+ const finalReport: DetailedQaReport = {
314
+ title: parsedData.title || { ...defaultSection, errors: ['Title section not found'] },
315
+ meta: parsedData.meta || { ...defaultSection, errors: ['Meta section not found'] },
316
+ h1: parsedData.h1 || { ...defaultSection, errors: ['H1 section not found'] },
317
+ copy: parsedData.copy || { ...defaultSection, errors: ['Copy section not found'] },
318
+ overall: parsedData.overall || { grade: 'N/A', pass: false, primaryIssue: 'Overall section not found' }
319
+ };
320
+
321
+ return {
322
+ detailedQaReport: finalReport,
323
+ overallPass: finalReport.overall.pass,
324
+ overallGrade: finalReport.overall.grade
325
+ };
326
+
327
+ } catch (error) {
328
+ console.error('Error parsing structured QA report:', error);
329
+ return { detailedQaReport: defaultReport, overallPass: false, overallGrade: 'N/A' };
330
+ }
331
+ };
332
+
333
+ /**
334
+ * Parses single-section format where all content is in one block.
335
+ * @param sectionText The section containing all embedded QA data.
336
+ * @param defaultReport Default report structure.
337
+ * @returns Parsed QA report data.
338
+ */
339
+ const parseSingleSectionFormat = (sectionText: string, defaultReport: DetailedQaReport): { detailedQaReport: DetailedQaReport, overallPass: boolean, overallGrade: string } => {
340
+ console.log('Parsing single-section format');
341
+
342
+ // Extract embedded sections by looking for section patterns like "**TITLE:", "**META:", etc.
343
+ const titleMatch = sectionText.match(/\*\*TITLE[^*]*\*\*([\s\S]*?)(?=\*\*[A-Z]+|$)/i);
344
+ const metaMatch = sectionText.match(/\*\*META[^*]*\*\*([\s\S]*?)(?=\*\*[A-Z]+|$)/i);
345
+ const h1Match = sectionText.match(/\*\*H1[^*]*\*\*([\s\S]*?)(?=\*\*[A-Z]+|$)/i);
346
+ const copyMatch = sectionText.match(/\*\*COPY[^*]*\*\*([\s\S]*?)(?=\*\*[A-Z]+|$)/i);
347
+ const overallMatch = sectionText.match(/\*\*(?:OVERALL|ASSESSMENT)[^*]*\*\*([\s\S]*?)$/i);
348
+
349
+ const finalReport: DetailedQaReport = {
350
+ title: titleMatch ? parseSection(titleMatch[1]) : { ...defaultReport.title, errors: ['Title section not found'] },
351
+ meta: metaMatch ? parseSection(metaMatch[1]) : { ...defaultReport.meta, errors: ['Meta section not found'] },
352
+ h1: h1Match ? parseSection(h1Match[1]) : { ...defaultReport.h1, errors: ['H1 section not found'] },
353
+ copy: copyMatch ? parseSection(copyMatch[1]) : { ...defaultReport.copy, errors: ['Copy section not found'] },
354
+ overall: overallMatch ? {
355
+ grade: extractOverallGrade(overallMatch[1]),
356
+ pass: extractOverallPass(overallMatch[1]),
357
+ primaryIssue: 'Single-section format parsed'
358
+ } : { ...defaultReport.overall }
359
+ };
360
+
361
+ return {
362
+ detailedQaReport: finalReport,
363
+ overallPass: finalReport.overall.pass,
364
+ overallGrade: finalReport.overall.grade
365
+ };
366
+ };
367
+
368
+ /**
369
+ * Helper function to extract overall grade from text.
370
+ */
371
+ const extractOverallGrade = (text: string): string => {
372
+ const gradeMatch = text.match(/Grade[^:]*:?\s*(\d+(?:\.\d+)?\/?\d*)/i) || text.match(/(\d+(?:\.\d+)?\/\d+)/);
373
+ return gradeMatch ? gradeMatch[1].trim() : 'N/A';
374
+ };
375
+
376
+ /**
377
+ * Helper function to extract overall pass from text.
378
+ */
379
+ const extractOverallPass = (text: string): boolean => {
380
+ const passMatch = text.match(/Pass[^:]*:?\s*(true|false|✅|❌|TRUE|FALSE)/i);
381
+ if (passMatch) {
382
+ const passValue = passMatch[1].toLowerCase().trim();
383
+ return passValue.includes('true') || passValue.includes('✅');
384
+ }
385
+ return false;
386
+ };
387
+
388
+ /**
389
+ * Enhanced section parsing that captures ALL QA Guard content
390
+ */
391
+ const parseEnhancedSection = (sectionBlock: string): QaSectionResult => {
392
+ const baseSection = parseSection(sectionBlock);
393
+
394
+ // Extract detailed assessment content
395
+ const detailedAssessmentMatch = sectionBlock.match(/(?:•|-|\*)\s*\*\*(?:Detailed\s+)?Assessment\*\*?:?\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/i);
396
+ const explanationsMatch = sectionBlock.match(/(?:•|-|\*)\s*\*\*(?:Explanation|Reasoning)\*\*?:?\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/i);
397
+
398
+ // Extract key strengths
399
+ const keyStrengthsMatch = sectionBlock.match(/(?:•|-|\*)\s*\*\*Key\s+Strengths\*\*?:?\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/i);
400
+ const strengthsList = keyStrengthsMatch ?
401
+ keyStrengthsMatch[1].split('\n')
402
+ .map(line => line.replace(/^[-•*]\s*/, '').trim())
403
+ .filter(line => line.length > 0) : undefined;
404
+
405
+ // Extract recommendations
406
+ const recommendationsMatch = sectionBlock.match(/(?:•|-|\*)\s*\*\*(?:Recommendations?|Suggestions?)\*\*?:?\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/i);
407
+ const recommendationsList = recommendationsMatch ?
408
+ recommendationsMatch[1].split('\n')
409
+ .map(line => line.replace(/^[-•*]\s*/, '').trim())
410
+ .filter(line => line.length > 0) : undefined;
411
+
412
+ return {
413
+ ...baseSection,
414
+ detailedAssessment: detailedAssessmentMatch ? detailedAssessmentMatch[1].trim() : undefined,
415
+ explanations: explanationsMatch ? explanationsMatch[1].trim() : undefined,
416
+ keyStrengths: strengthsList,
417
+ recommendations: recommendationsList,
418
+ rawContent: sectionBlock
419
+ };
420
+ };
421
+
422
+ /**
423
+ * Enhanced overall section parsing that captures ALL QA Guard content
424
+ */
425
+ const parseEnhancedOverallSection = (sectionBlock: string): { grade: string; pass: boolean; primaryIssue: string; detailedAssessment?: string; keyStrengths?: string[]; recommendations?: string[]; explanations?: string; rawContent?: string } => {
426
+ console.log('Parsing enhanced overall section with actual QA Guard format');
427
+ console.log('Overall section preview:', sectionBlock.substring(0, 300));
428
+
429
+ let grade = 'N/A';
430
+ let pass = false;
431
+ let primaryIssue = 'Overall assessment not available.';
432
+ let detailedAssessment = '';
433
+ let keyStrengths: string[] = [];
434
+ let recommendations: string[] = [];
435
+ let explanations = '';
436
+
437
+ // COMPREHENSIVE OVERALL GRADE PATTERN MATCHING
438
+ // Look for the actual QA Guard overall format: "Final Grade: 98.75/100"
439
+ const overallGradePatterns = [
440
+ /Final\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Final Grade: 98.75/100
441
+ /Overall\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Overall Grade: 98.75/100
442
+ /Total\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Total Grade: 98.75/100
443
+ /Combined\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Combined Grade: 98.75/100
444
+ /Average\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Average Grade: 98.75/100
445
+ /Mean\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Mean Grade: 98.75/100
446
+ /Composite\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Composite Grade: 98.75/100
447
+ /Final\s+Score:\s*(\d+(?:\.\d+)?)\/100/i, // Final Score: 98.75/100
448
+ /Overall\s+Score:\s*(\d+(?:\.\d+)?)\/100/i, // Overall Score: 98.75/100
449
+ /Total\s+Score:\s*(\d+(?:\.\d+)?)\/100/i, // Total Score: 98.75/100
450
+ /Final\s+Rating:\s*(\d+(?:\.\d+)?)\/100/i, // Final Rating: 98.75/100
451
+ /Overall\s+Rating:\s*(\d+(?:\.\d+)?)\/100/i, // Overall Rating: 98.75/100
452
+ /Final\s+Mark:\s*(\d+(?:\.\d+)?)\/100/i, // Final Mark: 98.75/100
453
+ /Overall\s+Mark:\s*(\d+(?:\.\d+)?)\/100/i, // Overall Mark: 98.75/100
454
+ /Final\s+Points:\s*(\d+(?:\.\d+)?)\/100/i, // Final Points: 98.75/100
455
+ /Overall\s+Points:\s*(\d+(?:\.\d+)?)\/100/i, // Overall Points: 98.75/100
456
+ /-?\s*\*\*Final\s+Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Final Grade:** 98.75/100
457
+ /-?\s*\*\*Overall\s+Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Overall Grade:** 98.75/100
458
+ /-?\s*\*\*Total\s+Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Total Grade:** 98.75/100
459
+ /-?\s*\*\*Final\s+Score:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Final Score:** 98.75/100
460
+ /-?\s*\*\*Overall\s+Score:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Overall Score:** 98.75/100
461
+ /-?\s*\*\*Final\s+Rating:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Final Rating:** 98.75/100
462
+ /-?\s*\*\*Overall\s+Rating:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Overall Rating:** 98.75/100
463
+ /Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Grade: 98.75/100
464
+ /Score:\s*(\d+(?:\.\d+)?)\/100/i, // Score: 98.75/100
465
+ /Rating:\s*(\d+(?:\.\d+)?)\/100/i, // Rating: 98.75/100
466
+ /Mark:\s*(\d+(?:\.\d+)?)\/100/i, // Mark: 98.75/100
467
+ /Points:\s*(\d+(?:\.\d+)?)\/100/i, // Points: 98.75/100
468
+ ];
469
+
470
+ let finalGradeMatch = null;
471
+ for (const pattern of overallGradePatterns) {
472
+ finalGradeMatch = sectionBlock.match(pattern);
473
+ if (finalGradeMatch) {
474
+ grade = `${finalGradeMatch[1]}/100`;
475
+ console.log('Found final grade:', grade);
476
+ break;
477
+ }
478
+ }
479
+
480
+ // COMPREHENSIVE OVERALL PASS STATUS PATTERN MATCHING
481
+ // Look for overall pass status: "Overall Pass: FALSE (due to META violation)"
482
+ const overallPassPatterns = [
483
+ /Overall\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Overall Pass: FALSE (due to META violation)
484
+ /Final\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Final Pass: FALSE (due to META violation)
485
+ /Total\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Total Pass: FALSE (due to META violation)
486
+ /Combined\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Combined Pass: FALSE (due to META violation)
487
+ /Average\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Average Pass: FALSE (due to META violation)
488
+ /Overall\s+Status:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Overall Status: FALSE (due to META violation)
489
+ /Final\s+Status:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Final Status: FALSE (due to META violation)
490
+ /Overall\s+Result:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Overall Result: FALSE (due to META violation)
491
+ /Final\s+Result:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Final Result: FALSE (due to META violation)
492
+ /Overall\s+Assessment:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Overall Assessment: FALSE (due to META violation)
493
+ /Final\s+Assessment:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Final Assessment: FALSE (due to META violation)
494
+ /-?\s*\*\*Overall\s+Pass:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // - **Overall Pass:** FALSE (due to META violation)
495
+ /-?\s*\*\*Final\s+Pass:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // - **Final Pass:** FALSE (due to META violation)
496
+ /-?\s*\*\*Total\s+Pass:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // - **Total Pass:** FALSE (due to META violation)
497
+ /-?\s*\*\*Overall\s+Status:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // - **Overall Status:** FALSE (due to META violation)
498
+ /-?\s*\*\*Final\s+Status:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // - **Final Status:** FALSE (due to META violation)
499
+ /Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Pass: FALSE (due to META violation)
500
+ /Status:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Status: FALSE (due to META violation)
501
+ /Result:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Result: FALSE (due to META violation)
502
+ /Assessment:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Assessment: FALSE (due to META violation)
503
+ ];
504
+
505
+ let overallPassMatch = null;
506
+ for (const pattern of overallPassPatterns) {
507
+ overallPassMatch = sectionBlock.match(pattern);
508
+ if (overallPassMatch) {
509
+ pass = overallPassMatch[1].toUpperCase() === 'TRUE' || overallPassMatch[1].toUpperCase() === 'PASS';
510
+ console.log('Found overall pass status:', pass);
511
+ break;
512
+ }
513
+ }
514
+
515
+ // COMPREHENSIVE PRIMARY ISSUE PATTERN MATCHING
516
+ // Look for primary issue in the pass status explanation
517
+ const issuePatterns = [
518
+ /Overall\s+Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Overall Pass: FALSE (due to META violation)
519
+ /Final\s+Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Final Pass: FALSE (due to META violation)
520
+ /Total\s+Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Total Pass: FALSE (due to META violation)
521
+ /Overall\s+Status:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Overall Status: FALSE (due to META violation)
522
+ /Final\s+Status:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Final Status: FALSE (due to META violation)
523
+ /Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Pass: FALSE (due to META violation)
524
+ /Status:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Status: FALSE (due to META violation)
525
+ /Result:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Result: FALSE (due to META violation)
526
+ /Assessment:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Assessment: FALSE (due to META violation)
527
+ /-?\s*\*\*Overall\s+Pass:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // - **Overall Pass:** FALSE (due to META violation)
528
+ /-?\s*\*\*Final\s+Pass:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // - **Final Pass:** FALSE (due to META violation)
529
+ /-?\s*\*\*Total\s+Pass:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // - **Total Pass:** FALSE (due to META violation)
530
+ /-?\s*\*\*Overall\s+Status:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // - **Overall Status:** FALSE (due to META violation)
531
+ /-?\s*\*\*Final\s+Status:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // - **Final Status:** FALSE (due to META violation)
532
+ /-?\s*\*\*Primary\s+Issue:\*\*\s*([^\n]+)/i, // - **Primary Issue:** Some sections have violations
533
+ /-?\s*\*\*Main\s+Issue:\*\*\s*([^\n]+)/i, // - **Main Issue:** Some sections have violations
534
+ /-?\s*\*\*Key\s+Issue:\*\*\s*([^\n]+)/i, // - **Key Issue:** Some sections have violations
535
+ /-?\s*\*\*Issue:\*\*\s*([^\n]+)/i, // - **Issue:** Some sections have violations
536
+ /-?\s*\*\*Problem:\*\*\s*([^\n]+)/i, // - **Problem:** Some sections have violations
537
+ /-?\s*\*\*Concern:\*\*\s*([^\n]+)/i, // - **Concern:** Some sections have violations
538
+ /Primary\s+Issue:\s*([^\n]+)/i, // Primary Issue: Some sections have violations
539
+ /Main\s+Issue:\s*([^\n]+)/i, // Main Issue: Some sections have violations
540
+ /Key\s+Issue:\s*([^\n]+)/i, // Key Issue: Some sections have violations
541
+ /Issue:\s*([^\n]+)/i, // Issue: Some sections have violations
542
+ /Problem:\s*([^\n]+)/i, // Problem: Some sections have violations
543
+ /Concern:\s*([^\n]+)/i, // Concern: Some sections have violations
544
+ ];
545
+
546
+ let issueMatch = null;
547
+ for (const pattern of issuePatterns) {
548
+ issueMatch = sectionBlock.match(pattern);
549
+ if (issueMatch) {
550
+ primaryIssue = issueMatch[1].trim();
551
+ console.log('Found primary issue:', primaryIssue);
552
+ break;
553
+ }
554
+ }
555
+
556
+ // COMPREHENSIVE DETAILED BREAKDOWN PATTERN MATCHING
557
+ // Look for detailed breakdown sections
558
+ const breakdownPatterns = [
559
+ /##\s+DETAILED\s+BREAKDOWN[^#]*/i, // ## DETAILED BREAKDOWN
560
+ /##\s+BREAKDOWN[^#]*/i, // ## BREAKDOWN
561
+ /##\s+ANALYSIS[^#]*/i, // ## ANALYSIS
562
+ /##\s+ASSESSMENT[^#]*/i, // ## ASSESSMENT
563
+ /##\s+EVALUATION[^#]*/i, // ## EVALUATION
564
+ /##\s+REVIEW[^#]*/i, // ## REVIEW
565
+ /##\s+SUMMARY[^#]*/i, // ## SUMMARY
566
+ /##\s+DETAILS[^#]*/i, // ## DETAILS
567
+ /##\s+EXPLANATION[^#]*/i, // ## EXPLANATION
568
+ /##\s+COMMENTS[^#]*/i, // ## COMMENTS
569
+ /##\s+NOTES[^#]*/i, // ## NOTES
570
+ /###\s+DETAILED\s+BREAKDOWN[^#]*/i, // ### DETAILED BREAKDOWN
571
+ /###\s+BREAKDOWN[^#]*/i, // ### BREAKDOWN
572
+ /###\s+ANALYSIS[^#]*/i, // ### ANALYSIS
573
+ /###\s+ASSESSMENT[^#]*/i, // ### ASSESSMENT
574
+ /###\s+EVALUATION[^#]*/i, // ### EVALUATION
575
+ /###\s+REVIEW[^#]*/i, // ### REVIEW
576
+ /###\s+SUMMARY[^#]*/i, // ### SUMMARY
577
+ /###\s+DETAILS[^#]*/i, // ### DETAILS
578
+ /###\s+EXPLANATION[^#]*/i, // ### EXPLANATION
579
+ /###\s+COMMENTS[^#]*/i, // ### COMMENTS
580
+ /###\s+NOTES[^#]*/i, // ### NOTES
581
+ ];
582
+
583
+ let breakdownMatch = null;
584
+ for (const pattern of breakdownPatterns) {
585
+ breakdownMatch = sectionBlock.match(pattern);
586
+ if (breakdownMatch) {
587
+ detailedAssessment = breakdownMatch[0];
588
+ console.log('Found detailed breakdown');
589
+ break;
590
+ }
591
+ }
592
+
593
+ // ENHANCED KEY STRENGTHS AND RECOMMENDATIONS EXTRACTION
594
+ // Extract key strengths and recommendations from the overall assessment
595
+ const lowerSection = sectionBlock.toLowerCase();
596
+
597
+ // Extract key strengths
598
+ if (lowerSection.includes('compliance') && lowerSection.includes('requirements')) {
599
+ keyStrengths.push('Overall compliance with requirements');
600
+ }
601
+ if (grade !== 'N/A' && parseFloat(grade.split('/')[0]) >= 80) {
602
+ keyStrengths.push('High overall grade achieved');
603
+ }
604
+ if (lowerSection.includes('successful') || lowerSection.includes('approved')) {
605
+ keyStrengths.push('Overall assessment successful');
606
+ }
607
+ if (lowerSection.includes('meets') && lowerSection.includes('standards')) {
608
+ keyStrengths.push('Meets overall standards');
609
+ }
610
+ if (lowerSection.includes('satisfies') && lowerSection.includes('criteria')) {
611
+ keyStrengths.push('Satisfies overall criteria');
612
+ }
613
+ if (lowerSection.includes('valid') || lowerSection.includes('correct')) {
614
+ keyStrengths.push('Overall content validation passed');
615
+ }
616
+
617
+ // Extract recommendations
618
+ if (lowerSection.includes('violation') || lowerSection.includes('fail')) {
619
+ recommendations.push('Address identified violations');
620
+ }
621
+ if (lowerSection.includes('correction') || lowerSection.includes('fix')) {
622
+ recommendations.push('Implement suggested corrections');
623
+ }
624
+ if (lowerSection.includes('improve') || lowerSection.includes('enhance')) {
625
+ recommendations.push('Improve overall content quality');
626
+ }
627
+ if (lowerSection.includes('adjust') || lowerSection.includes('modify')) {
628
+ recommendations.push('Adjust content to meet requirements');
629
+ }
630
+ if (lowerSection.includes('review') && lowerSection.includes('carefully')) {
631
+ recommendations.push('Review content carefully');
632
+ }
633
+ if (lowerSection.includes('consider') && lowerSection.includes('changes')) {
634
+ recommendations.push('Consider suggested changes');
635
+ }
636
+
637
+ // FALLBACK PATTERN MATCHING
638
+ // If no grade found, try alternative patterns
639
+ if (grade === 'N/A') {
640
+ const fallbackGradePatterns = [
641
+ /Grade:\s*(\d+(?:\.\d+)?)\/100/i,
642
+ /Score:\s*(\d+(?:\.\d+)?)\/100/i,
643
+ /Rating:\s*(\d+(?:\.\d+)?)\/100/i,
644
+ /Mark:\s*(\d+(?:\.\d+)?)\/100/i,
645
+ /Points:\s*(\d+(?:\.\d+)?)\/100/i,
646
+ /(\d+(?:\.\d+)?)\/100/i,
647
+ ];
648
+
649
+ for (const pattern of fallbackGradePatterns) {
650
+ const match = sectionBlock.match(pattern);
651
+ if (match) {
652
+ grade = `${match[1]}/100`;
653
+ console.log('Found fallback grade:', grade);
654
+ break;
655
+ }
656
+ }
657
+ }
658
+
659
+ // INFER PASS STATUS FROM GRADE IF NOT DETERMINED
660
+ if (grade !== 'N/A' && !overallPassMatch) {
661
+ const gradeNum = parseFloat(grade.split('/')[0]);
662
+ pass = gradeNum >= 80;
663
+ console.log('Inferred overall pass status from grade:', pass);
664
+ }
665
+
666
+ // FINAL PRIMARY ISSUE DETERMINATION
667
+ // If still no primary issue, generate one based on pass status
668
+ if (primaryIssue === 'Overall assessment not available.') {
669
+ if (pass) {
670
+ primaryIssue = 'All sections meet requirements';
671
+ } else {
672
+ primaryIssue = 'Some sections have violations';
673
+ }
674
+ }
675
+
676
+ console.log('Final overall result - Grade:', grade, 'Pass:', pass, 'Issue:', primaryIssue);
677
+
678
+ return {
679
+ grade,
680
+ pass,
681
+ primaryIssue,
682
+ detailedAssessment: detailedAssessment || undefined,
683
+ keyStrengths: keyStrengths.length > 0 ? keyStrengths : undefined,
684
+ recommendations: recommendations.length > 0 ? recommendations : undefined,
685
+ explanations: explanations || undefined,
686
+ rawContent: sectionBlock
687
+ };
688
+ };
689
+
690
+ /**
691
+ * Determine the type of an additional section based on its content
692
+ */
693
+ const determineSectionType = (sectionBlock: string): 'assessment' | 'strengths' | 'recommendations' | 'explanations' | 'other' => {
694
+ const lowerContent = sectionBlock.toLowerCase();
695
+
696
+ if (lowerContent.includes('strength') || lowerContent.includes('positive') || lowerContent.includes('excellent')) {
697
+ return 'strengths';
698
+ } else if (lowerContent.includes('recommend') || lowerContent.includes('suggest') || lowerContent.includes('improve')) {
699
+ return 'recommendations';
700
+ } else if (lowerContent.includes('explain') || lowerContent.includes('reason') || lowerContent.includes('why')) {
701
+ return 'explanations';
702
+ } else if (lowerContent.includes('assess') || lowerContent.includes('evaluate') || lowerContent.includes('analysis')) {
703
+ return 'assessment';
704
+ } else {
705
+ return 'other';
706
+ }
707
+ };
708
+
709
+ /**
710
+ * Parses the new, structured QA report format.
711
+ * @param qaText The raw `qa_gaurd` string from the API.
712
+ * @returns An object containing the detailed parsed report and top-level pass/grade info.
713
+ */
714
+ const parseNewQaReport = (qaText: string): { detailedQaReport: DetailedQaReport, overallPass: boolean, overallGrade: string } => {
715
+ // Default structure in case of parsing failure
716
+ const defaultSection: QaSectionResult = { grade: 'N/A', pass: false, errors: ['Parsing failed'], corrected: '' };
717
+ const defaultReport: DetailedQaReport = {
718
+ title: { ...defaultSection },
719
+ meta: { ...defaultSection },
720
+ h1: { ...defaultSection },
721
+ copy: { ...defaultSection },
722
+ overall: { grade: 'N/A', pass: false, primaryIssue: 'Parsing failed' },
723
+ completeRawReport: qaText // Always preserve the complete raw report
724
+ };
725
+
726
+ const cleanedQaText = cleanResponseText(qaText);
727
+ if (!cleanedQaText || typeof cleanedQaText !== 'string') {
728
+ return { detailedQaReport: defaultReport, overallPass: false, overallGrade: 'N/A' };
729
+ }
730
+
731
+ console.log('Enhanced QA parsing - input text preview:', cleanedQaText.substring(0, 500));
732
+
733
+ // COMPLETELY REWRITTEN TO HANDLE ACTUAL QA GUARD FORMAT
734
+ // The QA Guard uses formats like: "## **TITLE GRADE: 100/100 ✅ PASS**"
735
+
736
+ const parsedData: Partial<DetailedQaReport> = {};
737
+ const additionalSections: { [sectionName: string]: { content: string; type: 'assessment' | 'strengths' | 'recommendations' | 'explanations' | 'other'; } } = {};
738
+
739
+ // Enhanced section splitting to handle the actual QA Guard format
740
+ let sections: string[] = [];
741
+
742
+ // First, try to remove any leading overall evaluation header that might interfere with section splitting
743
+ const contentAfterOverallHeader = cleanedQaText.replace(/^(#\s*FINAL\s+QUALITY\s+ASSURANCE\s+EVALUATION|##\s*Section\s+Grades)\s*/i, '').trim();
744
+
745
+ // Try multiple splitting strategies for robustness
746
+ // Order matters: more specific patterns should come first
747
+ const sectionHeaderPatterns = [
748
+ // Highly specific: ### **SECTION NAME** - GRADE: X/100 ✅ PASS
749
+ /(?=(?:##|###)\s*\*\*([^*]+)\*\*\s*-\s*GRADE:)/g,
750
+ // Highly specific: ### **SECTION NAME: PASS** ✅
751
+ /(?=(?:##|###)\s*\*\*([^*]+):\s*(?:PASS|FAIL)\*\*\s*(?:✅|❌))/g,
752
+ // Specific: ## **SECTION NAME GRADE:**
753
+ /(?=(?:##|###)\s*\*\*([^*]+)\s+GRADE:)/g,
754
+ // Specific: ## **SECTION NAME** (with optional pass/fail indicator)
755
+ /(?=(?:##|###)\s*\*\*([^*]+)\*\*(?:\s*(?:✅|❌|PASS|FAIL))?)/g,
756
+ // Generic: ## Section Name
757
+ /(?=(?:##|###)\s*[^#\n]*)/g,
758
+ ];
759
+
760
+ for (const pattern of sectionHeaderPatterns) {
761
+ sections = contentAfterOverallHeader.split(pattern).filter(Boolean);
762
+ if (sections.length > 1) { // If we found more than one section, this split was successful
763
+ console.log(`Using successful split pattern: ${pattern}`);
764
+ break;
765
+ }
766
+ }
767
+
768
+ // Fallback to paragraph splitting if no suitable header pattern was found
769
+ if (sections.length <= 1) {
770
+ sections = contentAfterOverallHeader.split(/\n\n+/).filter(section => section.trim().length > 20);
771
+ console.log('Using paragraph-based parsing as fallback (no header pattern matched)');
772
+ }
773
+
774
+ // Ensure sections are not empty after splitting
775
+ sections = sections.filter(s => s.trim().length > 5); // Minimum length to be considered a valid section
776
+
777
+ console.log(`Found ${sections.length} sections to parse`);
778
+ sections.forEach((section, index) => {
779
+ console.log(`Section ${index} preview:`, section.substring(0, 150));
780
+ });
781
+
782
+ // Parse each section with enhanced logic
783
+ sections.forEach((sectionBlock, index) => {
784
+ const lines = sectionBlock.trim().split('\n');
785
+ const headerRaw = lines[0]?.trim() || '';
786
+ const header = headerRaw.toLowerCase();
787
+
788
+ console.log(`Processing section ${index} with header:`, headerRaw);
789
+
790
+ let sectionType = ''; // Must be reset for each block
791
+ let sectionData: QaSectionResult | null = null;
792
+
793
+ // New, more robust section identification logic
794
+ const headerForTypeCheck = header
795
+ .replace(/^(#+\s*|\*\*)/, '')
796
+ .replace(/[:*]/g, '')
797
+ .replace(/\s*-\s*grade:.*/, '') // also strip grade info
798
+ .replace(/\s*(✅|❌|pass|fail).*/, '') // and status info
799
+ .trim();
800
+
801
+ const words = headerForTypeCheck.split(/\s+/).filter(Boolean);
802
+
803
+ // If the normalized header is just ONE clean word, it's a primary section
804
+ if (words.length === 1) {
805
+ const typeWord = words[0];
806
+ if (typeWord === 'title') sectionType = 'title';
807
+ else if (typeWord === 'meta') sectionType = 'meta';
808
+ else if (typeWord === 'h1') sectionType = 'h1';
809
+ else if (typeWord === 'copy') sectionType = 'copy';
810
+ else if (['overall', 'final', 'assessment'].includes(typeWord)) sectionType = 'overall';
811
+ }
812
+
813
+ // If not identified, check for specific multi-word headers for 'overall'
814
+ if (!sectionType) {
815
+ if (headerForTypeCheck.startsWith('overall assessment') || headerForTypeCheck.startsWith('final assessment')) {
816
+ sectionType = 'overall';
817
+ }
818
+ }
819
+
820
+ // Route to the correct parser or handle as an additional section
821
+ if (sectionType === 'title') {
822
+ console.log('Identified as TITLE section');
823
+ sectionData = parseActualQAGuardSection(sectionBlock, sectionType);
824
+ if (sectionData) parsedData.title = sectionData;
825
+ } else if (sectionType === 'meta') {
826
+ console.log('Identified as META section');
827
+ sectionData = parseActualQAGuardSection(sectionBlock, sectionType);
828
+ if (sectionData) parsedData.meta = sectionData;
829
+ } else if (sectionType === 'h1') {
830
+ console.log('Identified as H1 section');
831
+ sectionData = parseActualQAGuardSection(sectionBlock, sectionType);
832
+ if (sectionData) parsedData.h1 = sectionData;
833
+ } else if (sectionType === 'copy') {
834
+ console.log('Identified as COPY section');
835
+ sectionData = parseActualQAGuardSection(sectionBlock, sectionType);
836
+ if (sectionData) parsedData.copy = sectionData;
837
+ } else if (sectionType === 'overall') {
838
+ console.log('Identified as OVERALL section');
839
+ const enhancedOverall = parseEnhancedOverallSection(sectionBlock);
840
+ parsedData.overall = enhancedOverall;
841
+ } else if (header.includes('grades by section') || header.includes('section grades')) {
842
+ console.log('Identified as introductory section, skipping.');
843
+ return;
844
+ } else {
845
+ // Additional sections logic remains here
846
+ console.log('Identified as additional section');
847
+ let displayName = headerRaw.replace(/^[#*\s-]+/g, '').trim();
848
+ // ... (existing additional section mapping logic) ...
849
+ additionalSections[displayName] = {
850
+ content: sectionBlock,
851
+ type: determineSectionType(sectionBlock)
852
+ };
853
+ }
854
+ });
855
+
856
+ // If no sections were found, try to extract from the complete text
857
+ if (Object.keys(parsedData).length === 0) {
858
+ console.log('No sections found, attempting full-text extraction');
859
+
860
+ // Try to extract grades and pass status from the raw text
861
+ const titleMatch = cleanedQaText.match(/TITLE[^:]*:\s*(\d+)\/100[^📋]*?(✅|❌|PASS|FAIL)/i);
862
+ const metaMatch = cleanedQaText.match(/META[^:]*:\s*(\d+)\/100[^📋]*?(✅|❌|PASS|FAIL)/i);
863
+ const h1Match = cleanedQaText.match(/H1[^:]*:\s*(\d+)\/100[^📋]*?(✅|❌|PASS|FAIL)/i);
864
+ const copyMatch = cleanedQaText.match(/COPY[^:]*:\s*(\d+)\/100[^📋]*?(✅|❌|PASS|FAIL)/i);
865
+
866
+ if (titleMatch) {
867
+ parsedData.title = {
868
+ grade: `${titleMatch[1]}/100`,
869
+ pass: titleMatch[2] === '✅' || titleMatch[2].toUpperCase() === 'PASS',
870
+ errors: titleMatch[2] === '✅' || titleMatch[2].toUpperCase() === 'PASS' ? ['No errors reported.'] : ['Violations detected'],
871
+ corrected: 'Content analysis extracted from full report',
872
+ rawContent: cleanedQaText
873
+ };
874
+ console.log('Extracted TITLE data:', parsedData.title);
875
+ }
876
+
877
+ if (metaMatch) {
878
+ parsedData.meta = {
879
+ grade: `${metaMatch[1]}/100`,
880
+ pass: metaMatch[2] === '✅' || metaMatch[2].toUpperCase() === 'PASS',
881
+ errors: metaMatch[2] === '✅' || metaMatch[2].toUpperCase() === 'PASS' ? ['No errors reported.'] : ['Violations detected'],
882
+ corrected: 'Content analysis extracted from full report',
883
+ rawContent: cleanedQaText
884
+ };
885
+ console.log('Extracted META data:', parsedData.meta);
886
+ }
887
+
888
+ if (h1Match) {
889
+ parsedData.h1 = {
890
+ grade: `${h1Match[1]}/100`,
891
+ pass: h1Match[2] === '✅' || h1Match[2].toUpperCase() === 'PASS',
892
+ errors: h1Match[2] === '✅' || h1Match[2].toUpperCase() === 'PASS' ? ['No errors reported.'] : ['Violations detected'],
893
+ corrected: 'Content analysis extracted from full report',
894
+ rawContent: cleanedQaText
895
+ };
896
+ console.log('Extracted H1 data:', parsedData.h1);
897
+ }
898
+
899
+ if (copyMatch) {
900
+ parsedData.copy = {
901
+ grade: `${copyMatch[1]}/100`,
902
+ pass: copyMatch[2] === '✅' || copyMatch[2].toUpperCase() === 'PASS',
903
+ errors: copyMatch[2] === '✅' || copyMatch[2].toUpperCase() === 'PASS' ? ['No errors reported.'] : ['Violations detected'],
904
+ corrected: 'Content analysis extracted from full report',
905
+ rawContent: cleanedQaText
906
+ };
907
+ console.log('Extracted COPY data:', parsedData.copy);
908
+ }
909
+
910
+ // Extract overall assessment
911
+ const overallMatch = cleanedQaText.match(/(?:OVERALL|Final).*?(\d+)\/100/i);
912
+ if (overallMatch) {
913
+ const overallGrade = parseInt(overallMatch[1]);
914
+ parsedData.overall = {
915
+ grade: `${overallGrade}/100`,
916
+ pass: overallGrade >= 80,
917
+ primaryIssue: overallGrade >= 80 ? 'All sections meet requirements' : 'Some violations detected',
918
+ rawContent: cleanedQaText
919
+ };
920
+ console.log('Extracted OVERALL data:', parsedData.overall);
921
+ }
922
+ }
923
+
924
+ const finalReport: DetailedQaReport = {
925
+ title: parsedData.title || { ...defaultSection, errors: ['Title section not found in QA response'] },
926
+ meta: parsedData.meta || { ...defaultSection, errors: ['Meta section not found in QA response'] },
927
+ h1: parsedData.h1 || { ...defaultSection, errors: ['H1 section not found in QA response'] },
928
+ copy: parsedData.copy || { ...defaultSection, errors: ['Copy section not found in QA response'] },
929
+ overall: parsedData.overall || { grade: 'N/A', pass: false, primaryIssue: 'Overall assessment not found in QA response' },
930
+ additionalSections: Object.keys(additionalSections).length > 0 ? additionalSections : undefined,
931
+ completeRawReport: qaText
932
+ };
933
+
934
+ // Calculate overall pass/grade from individual sections if overall not found
935
+ if (!parsedData.overall && (parsedData.title || parsedData.meta || parsedData.h1 || parsedData.copy)) {
936
+ const validSections = [parsedData.title, parsedData.meta, parsedData.h1, parsedData.copy].filter(Boolean);
937
+ const allPass = validSections.every(section => section?.pass);
938
+ const grades = validSections.map(section => {
939
+ if (section?.grade && section.grade !== 'N/A') {
940
+ const match = section.grade.match(/(\d+)/);
941
+ return match ? parseInt(match[1]) : 0;
942
+ }
943
+ return 0;
944
+ }).filter(g => g > 0);
945
+
946
+ const avgGrade = grades.length > 0 ? Math.round(grades.reduce((a, b) => a + b, 0) / grades.length) : 0;
947
+
948
+ finalReport.overall = {
949
+ grade: avgGrade > 0 ? `${avgGrade}/100` : 'N/A',
950
+ pass: allPass && avgGrade >= 80,
951
+ primaryIssue: allPass ? 'All sections passed' : 'Some sections have violations',
952
+ rawContent: cleanedQaText
953
+ };
954
+
955
+ console.log('Calculated overall from sections:', finalReport.overall);
956
+ }
957
+
958
+ console.log('Final QA parsing result:', {
959
+ title: finalReport.title?.grade,
960
+ meta: finalReport.meta?.grade,
961
+ h1: finalReport.h1?.grade,
962
+ copy: finalReport.copy?.grade,
963
+ overall: finalReport.overall?.grade,
964
+ overallPass: finalReport.overall?.pass
965
+ });
966
+
967
+ return {
968
+ detailedQaReport: finalReport,
969
+ overallPass: finalReport.overall.pass,
970
+ overallGrade: finalReport.overall.grade
971
+ };
972
+ };
973
+
974
+ /**
975
+ * Parses a single section of the QA report (e.g., TITLE, H1) based on the actual QA Guard format.
976
+ * This is a more robust parser that handles variations in header format and content.
977
+ */
978
+ const parseActualQAGuardSection = (sectionBlock: string, sectionType: string): QaSectionResult | null => {
979
+ console.log(`Parsing ${sectionType} section with actual QA Guard format`);
980
+ console.log('Section block preview:', sectionBlock.substring(0, 200));
981
+
982
+ // Extract grade and pass status from the actual QA Guard format
983
+ let grade = 'N/A';
984
+ let pass = false;
985
+ let errors: string[] = ['No errors reported.'];
986
+ let corrected = 'Content analysis not available.';
987
+ let detailedAssessment = '';
988
+ let keyStrengths: string[] = [];
989
+ let recommendations: string[] = [];
990
+ let explanations = '';
991
+
992
+ // COMPREHENSIVE HEADER PATTERN MATCHING
993
+ // Look for the actual QA Guard format: "### **TITLE** ✅ PASS" or "### **TITLE** ❌ FAIL"
994
+ // Also handle variations like "## **TITLE: PASS** ✅" or "## **TITLE: FAIL** ❌"
995
+ const headerPatterns = [
996
+ /###\s*\*\*([^*]+)\*\*\s*(✅|❌)\s*(PASS|FAIL)/i, // ### **TITLE** ✅ PASS
997
+ /##\s*\*\*([^*]+):\s*(PASS|FAIL)\*\*\s*(✅|❌)/i, // ## **TITLE: PASS** ✅
998
+ /##\s*\*\*([^*]+)\*\*\s*(PASS|FAIL)\s*(✅|❌)/i, // ## **TITLE** PASS ✅
999
+ /###\s*\*\*([^*]+):\s*(PASS|FAIL)\*\*\s*(✅|❌)/i, // ### **TITLE: PASS** ✅
1000
+ /##\s*\*\*([^*]+)\*\*\s*(✅|❌)\s*(PASS|FAIL)/i, // ## **TITLE** ✅ PASS
1001
+ /###\s*\*\*([^*]+)\*\*\s*(PASS|FAIL)/i, // ### **TITLE** PASS
1002
+ /##\s*\*\*([^*]+)\*\*\s*(PASS|FAIL)/i, // ## **TITLE** PASS
1003
+ /###\s*\*\*([^*]+):\s*(PASS|FAIL)/i, // ### **TITLE: PASS**
1004
+ /##\s*\*\*([^*]+):\s*(PASS|FAIL)/i, // ## **TITLE: PASS**
1005
+ /###\s*([^*]+)\s*(✅|❌)\s*(PASS|FAIL)/i, // ### TITLE ✅ PASS
1006
+ /##\s*([^*]+)\s*(✅|❌)\s*(PASS|FAIL)/i, // ## TITLE ✅ PASS
1007
+ /###\s*([^*]+):\s*(PASS|FAIL)\s*(✅|❌)/i, // ### TITLE: PASS ✅
1008
+ /##\s*([^*]+):\s*(PASS|FAIL)\s*(✅|❌)/i, // ## TITLE: PASS ✅
1009
+ /###\s*([^*]+)\s*(PASS|FAIL)/i, // ### TITLE PASS
1010
+ /##\s*([^*]+)\s*(PASS|FAIL)/i, // ## TITLE PASS
1011
+ /###\s*([^*]+):\s*(PASS|FAIL)/i, // ### TITLE: PASS
1012
+ /##\s*([^*]+):\s*(PASS|FAIL)/i, // ## TITLE: PASS
1013
+ ];
1014
+
1015
+ let headerMatch = null;
1016
+ for (const pattern of headerPatterns) {
1017
+ headerMatch = sectionBlock.match(pattern);
1018
+ if (headerMatch) {
1019
+ console.log('Found QA Guard header format:', headerMatch[0]);
1020
+ // Determine pass status from various indicators in the match
1021
+ const passIndicators = [headerMatch[2], headerMatch[3]].filter(Boolean);
1022
+ pass = passIndicators.some(indicator =>
1023
+ indicator === '✅' || indicator.toUpperCase() === 'PASS'
1024
+ );
1025
+ break;
1026
+ }
1027
+ }
1028
+
1029
+ // COMPREHENSIVE GRADE PATTERN MATCHING
1030
+ // Look for grade in various formats: "- **Grade:** 100/100", "Grade: 100/100", etc.
1031
+ const gradePatterns = [
1032
+ /-?\s*\*\*Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Grade:** 100/100
1033
+ /-?\s*\*\*Grade\*\*:\s*(\d+(?:\.\d+)?)\/100/i, // - **Grade**: 100/100
1034
+ /-?\s*Grade:\s*(\d+(?:\.\d+)?)\/100/i, // - Grade: 100/100
1035
+ /-?\s*Grade\s*:\s*(\d+(?:\.\d+)?)\/100/i, // - Grade : 100/100
1036
+ /\*\*Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // **Grade:** 100/100
1037
+ /\*\*Grade\*\*:\s*(\d+(?:\.\d+)?)\/100/i, // **Grade**: 100/100
1038
+ /Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Grade: 100/100
1039
+ /Grade\s*:\s*(\d+(?:\.\d+)?)\/100/i, // Grade : 100/100
1040
+ /-?\s*\*\*Score:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Score:** 100/100
1041
+ /-?\s*\*\*Rating:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Rating:** 100/100
1042
+ /-?\s*\*\*Mark:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Mark:** 100/100
1043
+ /-?\s*\*\*Points:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Points:** 100/100
1044
+ /-?\s*\*\*(\d+(?:\.\d+)?)\/100\s*points?\*\*/i, // - **100/100 points**
1045
+ /-?\s*(\d+(?:\.\d+)?)\/100\s*points?/i, // - 100/100 points
1046
+ /-?\s*\*\*(\d+(?:\.\d+)?)\/100\*\*/i, // - **100/100**
1047
+ /-?\s*(\d+(?:\.\d+)?)\/100/i, // - 100/100
1048
+ /\*\*(\d+(?:\.\d+)?)\/100\*\*/i, // **100/100**
1049
+ /(\d+(?:\.\d+)?)\/100/i, // 100/100
1050
+ ];
1051
+
1052
+ let gradeMatch = null;
1053
+ for (const pattern of gradePatterns) {
1054
+ gradeMatch = sectionBlock.match(pattern);
1055
+ if (gradeMatch) {
1056
+ grade = `${gradeMatch[1]}/100`;
1057
+ console.log('Found grade:', grade);
1058
+ break;
1059
+ }
1060
+ }
1061
+
1062
+ // COMPREHENSIVE COMPLIANCE PATTERN MATCHING
1063
+ // Look for compliance status in various formats
1064
+ const compliancePatterns = [
1065
+ /-?\s*\*\*Compliance:\*\*\s*([^\n]+)/i, // - **Compliance:** Full compliance
1066
+ /-?\s*\*\*Status:\*\*\s*([^\n]+)/i, // - **Status:** Passed
1067
+ /-?\s*\*\*Result:\*\*\s*([^\n]+)/i, // - **Result:** Successful
1068
+ /-?\s*\*\*Assessment:\*\*\s*([^\n]+)/i, // - **Assessment:** Compliant
1069
+ /-?\s*\*\*Evaluation:\*\*\s*([^\n]+)/i, // - **Evaluation:** Pass
1070
+ /-?\s*\*\*Check:\*\*\s*([^\n]+)/i, // - **Check:** OK
1071
+ /-?\s*\*\*Verification:\*\*\s*([^\n]+)/i, // - **Verification:** Valid
1072
+ /-?\s*\*\*Review:\*\*\s*([^\n]+)/i, // - **Review:** Approved
1073
+ /-?\s*\*\*Analysis:\*\*\s*([^\n]+)/i, // - **Analysis:** Compliant
1074
+ /-?\s*\*\*Summary:\*\*\s*([^\n]+)/i, // - **Summary:** Pass
1075
+ ];
1076
+
1077
+ let complianceMatch = null;
1078
+ for (const pattern of compliancePatterns) {
1079
+ complianceMatch = sectionBlock.match(pattern);
1080
+ if (complianceMatch) {
1081
+ const compliance = complianceMatch[1].trim();
1082
+ console.log('Found compliance:', compliance);
1083
+
1084
+ // Determine pass status from compliance text
1085
+ const failIndicators = ['violation', 'fail', 'error', 'non-compliant', 'rejected', 'invalid', 'incorrect', 'missing', 'below', 'above', 'out of range'];
1086
+ const passIndicators = ['compliance', 'pass', 'success', 'valid', 'correct', 'approved', 'compliant', 'within range', 'meets', 'satisfies'];
1087
+
1088
+ const lowerCompliance = compliance.toLowerCase();
1089
+ if (failIndicators.some(indicator => lowerCompliance.includes(indicator))) {
1090
+ pass = false;
1091
+ } else if (passIndicators.some(indicator => lowerCompliance.includes(indicator))) {
1092
+ pass = true;
1093
+ }
1094
+ break;
1095
+ }
1096
+ }
1097
+
1098
+ // COMPREHENSIVE ANALYSIS PATTERN MATCHING
1099
+ // Look for analysis content in various formats
1100
+ const analysisPatterns = [
1101
+ /-?\s*\*\*Analysis:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Analysis:** Detailed text
1102
+ /-?\s*\*\*Review:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Review:** Detailed text
1103
+ /-?\s*\*\*Assessment:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Assessment:** Detailed text
1104
+ /-?\s*\*\*Evaluation:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Evaluation:** Detailed text
1105
+ /-?\s*\*\*Summary:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Summary:** Detailed text
1106
+ /-?\s*\*\*Details:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Details:** Detailed text
1107
+ /-?\s*\*\*Breakdown:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Breakdown:** Detailed text
1108
+ /-?\s*\*\*Explanation:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Explanation:** Detailed text
1109
+ /-?\s*\*\*Comments:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Comments:** Detailed text
1110
+ /-?\s*\*\*Notes:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Notes:** Detailed text
1111
+ ];
1112
+
1113
+ let analysisMatch = null;
1114
+ for (const pattern of analysisPatterns) {
1115
+ analysisMatch = sectionBlock.match(pattern);
1116
+ if (analysisMatch) {
1117
+ detailedAssessment = analysisMatch[1].trim();
1118
+ console.log('Found analysis:', detailedAssessment.substring(0, 100));
1119
+ break;
1120
+ }
1121
+ }
1122
+
1123
+ // COMPREHENSIVE ERROR PATTERN MATCHING
1124
+ // Look for error information in various formats
1125
+ const errorPatterns = [
1126
+ /-?\s*\*\*Error:\*\*\s*([^\n]+)/i, // - **Error:** Error message
1127
+ /-?\s*\*\*Errors:\*\*\s*([^\n]+)/i, // - **Errors:** Error messages
1128
+ /-?\s*\*\*Issue:\*\*\s*([^\n]+)/i, // - **Issue:** Issue description
1129
+ /-?\s*\*\*Issues:\*\*\s*([^\n]+)/i, // - **Issues:** Issue descriptions
1130
+ /-?\s*\*\*Problem:\*\*\s*([^\n]+)/i, // - **Problem:** Problem description
1131
+ /-?\s*\*\*Problems:\*\*\s*([^\n]+)/i, // - **Problems:** Problem descriptions
1132
+ /-?\s*\*\*Violation:\*\*\s*([^\n]+)/i, // - **Violation:** Violation details
1133
+ /-?\s*\*\*Violations:\*\*\s*([^\n]+)/i, // - **Violations:** Violation details
1134
+ /-?\s*\*\*Warning:\*\*\s*([^\n]+)/i, // - **Warning:** Warning message
1135
+ /-?\s*\*\*Warnings:\*\*\s*([^\n]+)/i, // - **Warnings:** Warning messages
1136
+ /-?\s*\*\*Concern:\*\*\s*([^\n]+)/i, // - **Concern:** Concern details
1137
+ /-?\s*\*\*Concerns:\*\*\s*([^\n]+)/i, // - **Concerns:** Concern details
1138
+ ];
1139
+
1140
+ let errorMatch = null;
1141
+ for (const pattern of errorPatterns) {
1142
+ errorMatch = sectionBlock.match(pattern);
1143
+ if (errorMatch) {
1144
+ errors = [errorMatch[1].trim()];
1145
+ console.log('Found error:', errors[0]);
1146
+ break;
1147
+ }
1148
+ }
1149
+
1150
+ // ENHANCED: Look for multi-line violation lists
1151
+ const violationsMatch = sectionBlock.match(/-?\s*\*\*(?:Structure|Major|Minor)\s+violations?\*\*:\s*([\s\S]*?)(?=\n-?\s*\*\*|\n\n|---|$)/i);
1152
+ if (violationsMatch) {
1153
+ const violationText = violationsMatch[1].trim();
1154
+ // Split by lines that start with the violation marker
1155
+ const violationErrors = violationText.split(/\n\s*(?:-|\d+\.)\s*(?:❌|\⚠️)?\s*\*\*/g)
1156
+ .map(line => {
1157
+ // Clean up the line to get the core violation text
1158
+ return line.replace(/MAJOR VIOLATION \([^)]+\):/, '')
1159
+ .replace(/MINOR VIOLATION \([^)]+\):/, '')
1160
+ .replace(/\*\*$/, '')
1161
+ .trim();
1162
+ })
1163
+ .filter(line => line.length > 10); // Filter out empty or trivial lines
1164
+
1165
+ if (violationErrors.length > 0) {
1166
+ if (errors[0] === 'No errors reported.') {
1167
+ errors = violationErrors;
1168
+ } else {
1169
+ // Prepend violation details to any existing errors
1170
+ errors = [...violationErrors, ...errors];
1171
+ }
1172
+ console.log('Found detailed violation errors:', violationErrors);
1173
+ }
1174
+ }
1175
+
1176
+ // COMPREHENSIVE CORRECTED CONTENT PATTERN MATCHING
1177
+ // Look for corrected content in various formats
1178
+ const correctedPatterns = [
1179
+ /\*\*CORRECTED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **CORRECTED META:**\n```\ncontent\n```
1180
+ /\*\*CORRECTED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **CORRECTED META:**\ncontent
1181
+ /\*\*FIXED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **FIXED META:**\n```\ncontent\n```
1182
+ /\*\*FIXED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **FIXED META:**\ncontent
1183
+ /\*\*REVISED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **REVISED META:**\n```\ncontent\n```
1184
+ /\*\*REVISED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **REVISED META:**\ncontent
1185
+ /\*\*UPDATED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **UPDATED META:**\n```\ncontent\n```
1186
+ /\*\*UPDATED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **UPDATED META:**\ncontent
1187
+ /\*\*SUGGESTED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **SUGGESTED META:**\n```\ncontent\n```
1188
+ /\*\*SUGGESTED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **SUGGESTED META:**\ncontent
1189
+ /\*\*RECOMMENDED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **RECOMMENDED META:**\n```\ncontent\n```
1190
+ /\*\*RECOMMENDED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **RECOMMENDED META:**\ncontent
1191
+ /-?\s*\*\*Corrected:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Corrected:** content
1192
+ /-?\s*\*\*Fixed:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Fixed:** content
1193
+ /-?\s*\*\*Revised:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Revised:** content
1194
+ /-?\s*\*\*Updated:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Updated:** content
1195
+ /-?\s*\*\*Suggested:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Suggested:** content
1196
+ /-?\s*\*\*Recommended:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Recommended:** content
1197
+ ];
1198
+
1199
+ let correctedMatch = null;
1200
+ for (const pattern of correctedPatterns) {
1201
+ correctedMatch = sectionBlock.match(pattern);
1202
+ if (correctedMatch) {
1203
+ corrected = correctedMatch[1].trim();
1204
+ console.log('Found corrected content:', corrected.substring(0, 100));
1205
+ break;
1206
+ }
1207
+ }
1208
+
1209
+ // ENHANCED KEY STRENGTHS AND RECOMMENDATIONS EXTRACTION
1210
+ // Extract key strengths and recommendations from the analysis and compliance text
1211
+ if (detailedAssessment) {
1212
+ // Look for positive indicators in analysis
1213
+ const positiveIndicators = [
1214
+ 'compliance', 'compliant', 'proper', '✓', 'check', 'valid', 'correct', 'appropriate',
1215
+ 'meets', 'satisfies', 'within range', 'successful', 'approved', 'pass', 'good', 'excellent',
1216
+ 'optimal', 'ideal', 'perfect', 'complete', 'thorough', 'comprehensive', 'effective'
1217
+ ];
1218
+
1219
+ const negativeIndicators = [
1220
+ 'violation', 'error', 'fail', 'problem', 'issue', 'concern', 'warning', 'missing',
1221
+ 'below', 'above', 'out of range', 'incorrect', 'invalid', 'non-compliant', 'rejected',
1222
+ 'insufficient', 'inadequate', 'poor', 'weak', 'deficient'
1223
+ ];
1224
+
1225
+ const lowerAnalysis = detailedAssessment.toLowerCase();
1226
+
1227
+ // Extract key strengths
1228
+ if (positiveIndicators.some(indicator => lowerAnalysis.includes(indicator))) {
1229
+ keyStrengths.push('Meets compliance requirements');
1230
+ }
1231
+ if (lowerAnalysis.includes('keyword') && (lowerAnalysis.includes('included') || lowerAnalysis.includes('present'))) {
1232
+ keyStrengths.push('Proper keyword integration');
1233
+ }
1234
+ if (lowerAnalysis.includes('tone') && lowerAnalysis.includes('appropriate')) {
1235
+ keyStrengths.push('Appropriate tone maintained');
1236
+ }
1237
+ if (lowerAnalysis.includes('character') && lowerAnalysis.includes('within')) {
1238
+ keyStrengths.push('Character count within requirements');
1239
+ }
1240
+ if (lowerAnalysis.includes('length') && lowerAnalysis.includes('✓')) {
1241
+ keyStrengths.push('Length requirements met');
1242
+ }
1243
+ if (lowerAnalysis.includes('structure') && lowerAnalysis.includes('proper')) {
1244
+ keyStrengths.push('Proper structure maintained');
1245
+ }
1246
+
1247
+ // Extract recommendations
1248
+ if (negativeIndicators.some(indicator => lowerAnalysis.includes(indicator))) {
1249
+ recommendations.push('Address identified violations');
1250
+ }
1251
+ if (lowerAnalysis.includes('character') && (lowerAnalysis.includes('count') || lowerAnalysis.includes('length'))) {
1252
+ recommendations.push('Adjust character count to meet requirements');
1253
+ }
1254
+ if (lowerAnalysis.includes('word') && lowerAnalysis.includes('count')) {
1255
+ recommendations.push('Adjust word count to meet requirements');
1256
+ }
1257
+ if (lowerAnalysis.includes('keyword') && lowerAnalysis.includes('missing')) {
1258
+ recommendations.push('Include required keywords');
1259
+ }
1260
+ if (lowerAnalysis.includes('tone') && lowerAnalysis.includes('inappropriate')) {
1261
+ recommendations.push('Adjust tone to meet requirements');
1262
+ }
1263
+ if (lowerAnalysis.includes('structure') && lowerAnalysis.includes('improve')) {
1264
+ recommendations.push('Improve content structure');
1265
+ }
1266
+ }
1267
+
1268
+ // FALLBACK PATTERN MATCHING
1269
+ // If no grade found, try alternative patterns
1270
+ if (grade === 'N/A') {
1271
+ const fallbackGradePatterns = [
1272
+ /Grade:\s*(\d+(?:\.\d+)?)\/100/i,
1273
+ /Score:\s*(\d+(?:\.\d+)?)\/100/i,
1274
+ /Rating:\s*(\d+(?:\.\d+)?)\/100/i,
1275
+ /Mark:\s*(\d+(?:\.\d+)?)\/100/i,
1276
+ /Points:\s*(\d+(?:\.\d+)?)\/100/i,
1277
+ /(\d+(?:\.\d+)?)\/100/i,
1278
+ ];
1279
+
1280
+ for (const pattern of fallbackGradePatterns) {
1281
+ const match = sectionBlock.match(pattern);
1282
+ if (match) {
1283
+ grade = `${match[1]}/100`;
1284
+ console.log('Found fallback grade:', grade);
1285
+ break;
1286
+ }
1287
+ }
1288
+ }
1289
+
1290
+ // INFER PASS STATUS FROM GRADE IF NOT DETERMINED
1291
+ if (grade !== 'N/A' && !headerMatch && !complianceMatch) {
1292
+ const gradeNum = parseFloat(grade.split('/')[0]);
1293
+ pass = gradeNum >= 80;
1294
+ console.log('Inferred pass status from grade:', pass);
1295
+ }
1296
+
1297
+ // FINAL PASS STATUS DETERMINATION
1298
+ // If still no pass status, check for pass/fail indicators in text
1299
+ if (!headerMatch && !complianceMatch && grade === 'N/A') {
1300
+ const lowerSection = sectionBlock.toLowerCase();
1301
+ if (lowerSection.includes('pass') && !lowerSection.includes('fail')) {
1302
+ pass = true;
1303
+ } else if (lowerSection.includes('fail')) {
1304
+ pass = false;
1305
+ } else if (lowerSection.includes('✅') && !lowerSection.includes('❌')) {
1306
+ pass = true;
1307
+ } else if (lowerSection.includes('❌')) {
1308
+ pass = false;
1309
+ }
1310
+ }
1311
+
1312
+ // UPDATE ERRORS BASED ON PASS STATUS
1313
+ if (pass && errors[0] === 'No errors reported.') {
1314
+ // Keep as is
1315
+ } else if (!pass && errors[0] === 'No errors reported.') {
1316
+ errors = ['Violations detected'];
1317
+ }
1318
+
1319
+ console.log(`Final ${sectionType} result - Grade: ${grade}, Pass: ${pass}, Errors: ${errors.length}`);
1320
+
1321
+ return {
1322
+ grade,
1323
+ pass,
1324
+ errors,
1325
+ corrected,
1326
+ detailedAssessment: detailedAssessment || undefined,
1327
+ keyStrengths: keyStrengths.length > 0 ? keyStrengths : undefined,
1328
+ recommendations: recommendations.length > 0 ? recommendations : undefined,
1329
+ explanations: explanations || undefined,
1330
+ rawContent: sectionBlock
1331
+ };
1332
+ };
1333
+
1334
+
1335
+ /**
1336
+ * Runs the Dify workflow for a given input row, with retries for transient errors.
1337
+ * @param inputs - The data from a CSV row.
1338
+ * @returns A promise that resolves to the processed and cleaned results.
1339
+ */
1340
+ export const runWorkflow = async (inputs: ApiInput): Promise<ProcessedResult> => {
1341
+ let lastError: Error = new Error('Workflow failed after all retries.');
1342
+
1343
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
1344
+ let responseText = '';
1345
+ try {
1346
+ const payload = {
1347
+ inputs,
1348
+ response_mode: 'streaming',
1349
+ user: API_USER,
1350
+ };
1351
+
1352
+ const response = await fetch(API_URL, {
1353
+ method: 'POST',
1354
+ headers: {
1355
+ 'Authorization': `Bearer ${API_TOKEN}`,
1356
+ 'Content-Type': 'application/json',
1357
+ },
1358
+ body: JSON.stringify(payload),
1359
+ });
1360
+
1361
+ if (!response.ok) {
1362
+ responseText = await response.text();
1363
+ // Check for retryable HTTP status codes.
1364
+ if (RETRYABLE_STATUS_CODES.includes(response.status)) {
1365
+ throw new WorkflowError(`RETRYABLE_HTTP_${response.status}`, `Temporary service issue (HTTP ${response.status}).`, 'network', responseText);
1366
+ }
1367
+ // For other HTTP errors, fail immediately.
1368
+ throw new WorkflowError(`HTTP_${response.status}`, `API request failed (HTTP ${response.status}).`, 'api', responseText);
1369
+ }
1370
+
1371
+ if (!response.body) {
1372
+ throw new WorkflowError('EMPTY_RESPONSE', 'Empty response from API.', 'network');
1373
+ }
1374
+
1375
+ const reader = response.body.getReader();
1376
+ const decoder = new TextDecoder();
1377
+ let streamContent = '';
1378
+ while (true) {
1379
+ const { done, value } = await reader.read();
1380
+ if (done) break;
1381
+ streamContent += decoder.decode(value, { stream: true });
1382
+ }
1383
+
1384
+ // The full stream content becomes our responseText for error logging
1385
+ responseText = streamContent;
1386
+
1387
+ const lines = streamContent.trim().split('\n');
1388
+ const finishedLine = lines.find(line => line.includes('"event": "workflow_finished"')) || '';
1389
+
1390
+ if (!finishedLine) {
1391
+ // The gateway might have returned an HTML error page instead of a stream
1392
+ if (streamContent.trim().toLowerCase().startsWith('<html')) {
1393
+ throw new WorkflowError('RETRYABLE_HTML_RESPONSE', 'Service returned an HTML error page.', 'stream', streamContent.slice(0, 1000));
1394
+ }
1395
+ console.error('Full stream content:', streamContent);
1396
+ throw new WorkflowError('FINISH_EVENT_MISSING', 'Workflow did not finish successfully.', 'stream', streamContent.slice(0, 1000));
1397
+ }
1398
+
1399
+ const jsonString = finishedLine.replace(/^data: /, '');
1400
+ const finishedEventData = JSON.parse(jsonString);
1401
+
1402
+ if (finishedEventData.data.status !== 'succeeded') {
1403
+ const apiError = finishedEventData.data.error || 'Unknown';
1404
+ const isOverloaded = typeof apiError === 'string' && (apiError.toLowerCase().includes('overloaded') || apiError.toLowerCase().includes('gateway time-out'));
1405
+
1406
+ // If it's a known transient error, mark it as retryable.
1407
+ if (isOverloaded) {
1408
+ throw new WorkflowError('RETRYABLE_API_ERROR', 'Service overloaded. Retrying...', 'api', String(apiError));
1409
+ }
1410
+ // Otherwise, it's a permanent failure for this row.
1411
+ throw new WorkflowError('WORKFLOW_FAILED', `Workflow failed: ${apiError}`, 'api', String(apiError));
1412
+ }
1413
+
1414
+ const outputs: ApiResponseOutput = finishedEventData.data.outputs;
1415
+
1416
+ if (!outputs || Object.keys(outputs).length === 0) {
1417
+ throw new WorkflowError('EMPTY_OUTPUTS', 'Workflow succeeded but returned empty outputs.', 'parse');
1418
+ }
1419
+
1420
+ const rawQaReport = outputs.qa_gaurd || 'QA report not available.';
1421
+ console.log('QA Report length:', rawQaReport.length);
1422
+
1423
+ const { detailedQaReport, overallPass, overallGrade } = parseNewQaReport(rawQaReport);
1424
+ console.log('Final Parsed QA - Pass:', overallPass, 'Grade:', overallGrade);
1425
+
1426
+ // Success, return the result and exit the loop.
1427
+ return {
1428
+ generatedTitle: cleanResponseText(outputs.title),
1429
+ generatedH1: cleanResponseText(outputs.h1),
1430
+ generatedCopy: cleanResponseText(outputs.copy),
1431
+ generatedMeta: cleanResponseText(outputs.meta),
1432
+ qaReport: rawQaReport,
1433
+ detailedQaReport,
1434
+ overallPass,
1435
+ overallGrade,
1436
+ };
1437
+
1438
+ } catch (error) {
1439
+ lastError = error instanceof Error ? error : new Error(String(error));
1440
+
1441
+ const isRetryable = lastError instanceof WorkflowError && lastError.code.startsWith('RETRYABLE_');
1442
+
1443
+ if (isRetryable && attempt < MAX_RETRIES) {
1444
+ // Exponential backoff with jitter: 1s, 2s, 4s, ... + random
1445
+ const delayMs = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
1446
+ console.warn(`Attempt ${attempt}/${MAX_RETRIES} failed due to a transient error. Retrying in ~${Math.round(delayMs / 1000)}s...`, { error: lastError.message });
1447
+ await delay(delayMs);
1448
+ continue; // Move to the next attempt
1449
+ }
1450
+
1451
+ // For non-retryable errors, or if we've exhausted retries, break the loop to throw the error.
1452
+ console.error(`Failed to process row. Attempt ${attempt}/${MAX_RETRIES}. Error: ${lastError.message}`);
1453
+ if (responseText) {
1454
+ // Log the problematic response that caused the failure
1455
+ console.error('Problematic Response:', responseText);
1456
+ }
1457
+ break;
1458
+ }
1459
+ }
1460
+
1461
+ // If the loop finished without returning, it means all attempts failed.
1462
+ // We re-throw the last captured error, making it more user-friendly if it was a transient one.
1463
+ if (lastError instanceof WorkflowError && lastError.code.startsWith('RETRYABLE_')) {
1464
+ throw new WorkflowError('SERVICE_UNAVAILABLE', `API service is temporarily unavailable. Tried ${MAX_RETRIES} times. Please try again later.`, 'api', lastError.debug || lastError.stack);
1465
+ }
1466
+
1467
+ throw lastError;
1468
+ };
src/.DS_Store ADDED
Binary file (6.15 kB). View file
 
src/config/difyChatbotConfig.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ declare global {
2
+ interface Window {
3
+ difyChatbotConfig: {
4
+ token: string;
5
+ inputs: Record<string, string>;
6
+ systemVariables: Record<string, string>;
7
+ userVariables: Record<string, string>;
8
+ };
9
+ }
10
+ }
11
+
12
+ window.difyChatbotConfig = {
13
+ token: 'U5KHVj67e4Gum8hd',
14
+ inputs: {
15
+ // You can define the inputs from the Start node here
16
+ // key is the variable name
17
+ // e.g.
18
+ // name: "NAME"
19
+ },
20
+ systemVariables: {
21
+ // user_id: 'YOU CAN DEFINE USER ID HERE',
22
+ // conversation_id: 'YOU CAN DEFINE CONVERSATION ID HERE, IT MUST BE A VALID UUID',
23
+ },
24
+ userVariables: {
25
+ // avatar_url: 'YOU CAN DEFINE USER AVATAR URL HERE',
26
+ // name: 'YOU CAN DEFINE USER NAME HERE',
27
+ },
28
+ };
tsconfig.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "experimentalDecorators": true,
5
+ "useDefineForClassFields": false,
6
+ "module": "ESNext",
7
+ "lib": [
8
+ "ES2022",
9
+ "DOM",
10
+ "DOM.Iterable"
11
+ ],
12
+ "skipLibCheck": true,
13
+ "moduleResolution": "bundler",
14
+ "isolatedModules": true,
15
+ "moduleDetection": "force",
16
+ "allowJs": true,
17
+ "jsx": "react-jsx",
18
+ "paths": {
19
+ "@/*": [
20
+ "./*"
21
+ ]
22
+ },
23
+ "allowImportingTsExtensions": true,
24
+ "noEmit": true
25
+ }
26
+ }
types.ts ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // The expected structure of a row from the input CSV.
2
+ export interface CsvInputRow {
3
+ URL: string;
4
+ Page: string;
5
+ Keywords: string;
6
+ Recommended_Title: string;
7
+ Recommended_H1: string;
8
+ Internal_Links: string;
9
+ Copy: string;
10
+ }
11
+
12
+ // The structure of the 'inputs' object for the Dify API call.
13
+ export type ApiInput = CsvInputRow;
14
+
15
+
16
+ // The structure of the `outputs` object from the Dify API response.
17
+ export interface ApiResponseOutput {
18
+ title: string;
19
+ h1: string;
20
+ copy: string;
21
+ meta: string;
22
+ qa_gaurd: string;
23
+ }
24
+
25
+ // Represents an item in the processing queue.
26
+ export interface QueueItem {
27
+ id: number;
28
+ data: CsvInputRow;
29
+ status: 'pending' | 'processing' | 'completed' | 'failed';
30
+ error?: string;
31
+ }
32
+
33
+ // Represents the parsed QA results for a single section (e.g., Title, H1).
34
+ export interface QaSectionResult {
35
+ grade: string;
36
+ pass: boolean;
37
+ errors: string[];
38
+ corrected: string;
39
+ // Enhanced to capture ALL QA Guard content
40
+ detailedAssessment?: string; // Full detailed assessment text
41
+ keyStrengths?: string[]; // List of key strengths identified
42
+ recommendations?: string[]; // List of recommendations
43
+ explanations?: string; // Detailed explanations
44
+ rawContent?: string; // Raw section content for complete preservation
45
+ }
46
+
47
+ // Represents the full, structured QA report.
48
+ export interface DetailedQaReport {
49
+ title: QaSectionResult;
50
+ meta: QaSectionResult;
51
+ h1: QaSectionResult;
52
+ copy: QaSectionResult;
53
+ overall: {
54
+ grade: string;
55
+ pass: boolean;
56
+ primaryIssue: string;
57
+ // Enhanced to capture ALL overall assessment content
58
+ detailedAssessment?: string; // Full detailed overall assessment
59
+ keyStrengths?: string[]; // Overall key strengths
60
+ recommendations?: string[]; // Overall recommendations
61
+ explanations?: string; // Detailed overall explanations
62
+ rawContent?: string; // Raw overall section content
63
+ };
64
+ // Capture any additional sections that QA Guard might generate
65
+ additionalSections?: {
66
+ [sectionName: string]: {
67
+ content: string;
68
+ type: 'assessment' | 'strengths' | 'recommendations' | 'explanations' | 'other';
69
+ };
70
+ };
71
+ // Preserve the complete raw QA report
72
+ completeRawReport?: string;
73
+ }
74
+
75
+
76
+ // Extracted and cleaned data from the API response.
77
+ export interface ProcessedResult {
78
+ generatedTitle: string;
79
+ generatedH1: string;
80
+ generatedCopy: string;
81
+ generatedMeta: string;
82
+ qaReport: string; // The raw, original QA report string
83
+ detailedQaReport?: DetailedQaReport; // The parsed, structured QA report
84
+ overallPass: boolean; // For quick access in the main table view
85
+ overallGrade: string; // For quick access in the main table view
86
+ }
87
+
88
+ // A complete row for the final results table and CSV export.
89
+ export type ResultRow = CsvInputRow & ProcessedResult & { id: number };
90
+
91
+ // Represents a completed job in the history.
92
+ export interface JobHistoryItem {
93
+ id: string;
94
+ filename: string;
95
+ date: string;
96
+ totalRows: number;
97
+ completedCount: number;
98
+ failedCount: number;
99
+ results?: ResultRow[]; // Store the full results for review and re-download
100
+ }
101
+
102
+
103
+ // Expected headers for the input CSV, used for validation.
104
+ export const REQUIRED_CSV_HEADERS: (keyof CsvInputRow)[] = [
105
+ 'URL',
106
+ 'Page',
107
+ 'Keywords',
108
+ 'Recommended_Title',
109
+ 'Recommended_H1',
110
+ 'Internal_Links',
111
+ 'Copy'
112
+ ];
113
+
114
+ // Structured, user-facing error captured during batching
115
+ export interface BatchError {
116
+ row?: number; // 1-based row index for per-row errors
117
+ code: string; // machine-friendly code e.g. CSV_PARSE_ERROR, HTTP_ERROR, SERVICE_UNAVAILABLE
118
+ message: string; // user-friendly message
119
+ suggestion?: string; // optional remediation hint
120
+ at?: 'csv' | 'network' | 'api' | 'stream' | 'parse' | 'unknown';
121
+ debug?: string; // optional raw/console error text to help diagnose
122
+ }
vite.config.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+ import { defineConfig, loadEnv } from 'vite';
3
+
4
+ export default defineConfig(({ mode }) => {
5
+ const env = loadEnv(mode, '.', '');
6
+ return {
7
+ define: {
8
+ 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
9
+ 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
10
+ },
11
+ resolve: {
12
+ alias: {
13
+ '@': path.resolve(__dirname, '.'),
14
+ }
15
+ }
16
+ };
17
+ });