@visns-studio/visns-components 5.14.0 → 5.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1321 @@
1
+ import React, { useState, useRef, useCallback } from 'react';
2
+ import { toast } from 'react-toastify';
3
+ import {
4
+ FileText,
5
+ Upload,
6
+ User,
7
+ DollarSign,
8
+ Calendar,
9
+ Hash,
10
+ CheckCircle,
11
+ Circle,
12
+ Loader,
13
+ X,
14
+ Eye,
15
+ Settings,
16
+ AlertTriangle,
17
+ Info,
18
+ ArrowRight,
19
+ ArrowLeft,
20
+ Play,
21
+ Pause,
22
+ RotateCcw,
23
+ Zap,
24
+ Target,
25
+ MapPin,
26
+ BookOpen,
27
+ Lightbulb,
28
+ Wand2,
29
+ ClipboardList,
30
+ FolderOpen,
31
+ CheckCircle2,
32
+ AlertCircle,
33
+ HelpCircle,
34
+ ChevronRight,
35
+ ChevronDown,
36
+ Sparkles
37
+ } from 'lucide-react';
38
+ import fetchUtil from '../../utils/fetchUtil';
39
+ import styles from './OcrTemplateEnhanced.module.scss';
40
+
41
+ const OcrTemplateEnhanced = ({
42
+ id,
43
+ data,
44
+ setData,
45
+ routeParams,
46
+ urlParam,
47
+ onSave
48
+ }) => {
49
+ const [dragActive, setDragActive] = useState(false);
50
+ const [processing, setProcessing] = useState(false);
51
+ const [processingStage, setProcessingStage] = useState({
52
+ upload: 'pending',
53
+ ocr: 'pending',
54
+ analysis: 'pending'
55
+ });
56
+ const [uploadProgress, setUploadProgress] = useState(0);
57
+ const [currentStep, setCurrentStep] = useState(1);
58
+ const [completedSteps, setCompletedSteps] = useState(new Set());
59
+ const [showStepDetails, setShowStepDetails] = useState({});
60
+ const [hasAutoJumped, setHasAutoJumped] = useState(false);
61
+ const fileInputRef = useRef(null);
62
+
63
+ const ocrTemplateBuilder = data[id] || {};
64
+ const listOfKeys = ocrTemplateBuilder?.keyValuePairs || {};
65
+
66
+ // Define the guided journey steps
67
+ const journeySteps = [
68
+ {
69
+ id: 1,
70
+ title: "Upload Document",
71
+ description: "Upload PDF, JPG, or PNG documents for OCR analysis and template detection",
72
+ icon: Upload,
73
+ component: "upload",
74
+ isCompleted: () => !!ocrTemplateBuilder.uploadedFile,
75
+ canProceed: () => !!ocrTemplateBuilder.uploadedFile
76
+ },
77
+ {
78
+ id: 2,
79
+ title: "Select Data for Template",
80
+ description: "Choose key-value pairs to use in your filename template creation",
81
+ icon: ClipboardList,
82
+ component: "dataReview",
83
+ isCompleted: () => ocrTemplateBuilder?.selectedKey?.length > 0,
84
+ canProceed: () => ocrTemplateBuilder?.selectedKey?.length > 0
85
+ },
86
+ {
87
+ id: 3,
88
+ title: "Define Template Signature",
89
+ description: "Select variables that uniquely identify this document type for template matching",
90
+ icon: BookOpen,
91
+ component: "titleSelection",
92
+ isCompleted: () => ocrTemplateBuilder?.selectedTitle?.length > 0,
93
+ canProceed: () => true // Optional step
94
+ },
95
+ {
96
+ id: 4,
97
+ title: "Craft Filename Template",
98
+ description: "Design the filename pattern using selected variables for automated file renaming",
99
+ icon: Wand2,
100
+ component: "templateBuilder",
101
+ isCompleted: () => !!ocrTemplateBuilder?.dynamicFilename && ocrTemplateBuilder.dynamicFilename.length > 0,
102
+ canProceed: () => !!ocrTemplateBuilder?.dynamicFilename
103
+ },
104
+ {
105
+ id: 5,
106
+ title: "Setup Automation Folders",
107
+ description: "Configure watch folder for new files and destination folder for processed documents",
108
+ icon: FolderOpen,
109
+ component: "folderConfig",
110
+ isCompleted: () => !!(data?.content?.watchFolder && data?.content?.destinationFolder),
111
+ canProceed: () => true
112
+ }
113
+ ];
114
+
115
+ // Update completed steps when data changes
116
+ React.useEffect(() => {
117
+ const newCompletedSteps = new Set();
118
+ journeySteps.forEach(step => {
119
+ if (step.isCompleted()) {
120
+ newCompletedSteps.add(step.id);
121
+ }
122
+ });
123
+ setCompletedSteps(newCompletedSteps);
124
+
125
+ // Auto-advance to next step if current step is completed
126
+ const currentStepData = journeySteps.find(s => s.id === currentStep);
127
+ if (currentStepData?.isCompleted() && currentStep < journeySteps.length) {
128
+ const nextIncompleteStep = journeySteps.find(s => s.id > currentStep && !newCompletedSteps.has(s.id));
129
+ if (nextIncompleteStep) {
130
+ setCurrentStep(nextIncompleteStep.id);
131
+ }
132
+ }
133
+ }, [ocrTemplateBuilder, data, currentStep]);
134
+
135
+ // Initial step detection - jump to latest completed step on load (only once)
136
+ React.useEffect(() => {
137
+ // Only run if we haven't auto-jumped yet and have template data
138
+ const hasTemplateData = ocrTemplateBuilder && Object.keys(ocrTemplateBuilder).length > 0;
139
+
140
+ if (!hasAutoJumped && hasTemplateData && currentStep === 1) {
141
+ // Calculate completed steps
142
+ const completedStepIds = journeySteps
143
+ .filter(step => step.isCompleted())
144
+ .map(step => step.id);
145
+
146
+ if (completedStepIds.length > 0) {
147
+ // Find the furthest step we should go to
148
+ const highestCompletedStep = Math.max(...completedStepIds);
149
+ let targetStep = 1;
150
+
151
+ if (highestCompletedStep === journeySteps.length) {
152
+ // All steps completed - go to final step
153
+ targetStep = journeySteps.length;
154
+ } else {
155
+ // Go to next incomplete step or the furthest completed step
156
+ const nextIncompleteStep = journeySteps.find(s => s.id > highestCompletedStep && !completedStepIds.includes(s.id));
157
+ targetStep = nextIncompleteStep ? nextIncompleteStep.id : highestCompletedStep;
158
+ }
159
+
160
+ // Only change if we're not already there
161
+ if (targetStep !== currentStep) {
162
+ setCurrentStep(targetStep);
163
+ setHasAutoJumped(true); // Mark that we've auto-jumped
164
+ }
165
+ }
166
+ }
167
+ }, [ocrTemplateBuilder, journeySteps, hasAutoJumped, currentStep]);
168
+
169
+ // Manual navigation function to prevent auto-jump interference
170
+ const navigateToStep = (stepId) => {
171
+ setCurrentStep(stepId);
172
+ setHasAutoJumped(true); // Prevent future auto-jumping after manual navigation
173
+ };
174
+
175
+ // Enhanced smart categorization utility
176
+ const categorizeData = useCallback((keyValuePairs) => {
177
+ const categories = {
178
+ contact: [],
179
+ financial: [],
180
+ dates: [],
181
+ identifiers: [],
182
+ other: []
183
+ };
184
+
185
+ Object.entries(keyValuePairs).forEach(([key, value]) => {
186
+ const keyLower = key.toLowerCase();
187
+ const valueStr = String(value).toLowerCase();
188
+ let categorized = false;
189
+
190
+ // Enhanced Contact Detection
191
+ if (/email|e-mail|mail/i.test(key) || /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/.test(valueStr)) {
192
+ categories.contact.push({ key, value, confidence: 0.95 });
193
+ categorized = true;
194
+ } else if (/phone|tel|mobile|cell|fax|contact/i.test(key) || /^[\+]?[\d\s\-\(\)]{7,15}$/.test(valueStr)) {
195
+ categories.contact.push({ key, value, confidence: 0.9 });
196
+ categorized = true;
197
+ } else if (/name|client|customer|person|company|organisation|organization|vendor|supplier/i.test(key)) {
198
+ categories.contact.push({ key, value, confidence: 0.85 });
199
+ categorized = true;
200
+ } else if (/address|street|city|state|country|zip|postal|location/i.test(key)) {
201
+ categories.contact.push({ key, value, confidence: 0.8 });
202
+ categorized = true;
203
+ }
204
+
205
+ // Enhanced Financial Detection
206
+ if (!categorized && (/amount|price|cost|total|sum|value|fee|charge|payment|invoice|bill|tax|vat|gst/i.test(key) ||
207
+ /[$€£¥₹]/.test(valueStr) || /^\d+[.,]\d{2}$/.test(valueStr) ||
208
+ (/^\d+\.?\d*$/.test(valueStr) && parseFloat(valueStr) > 0))) {
209
+ categories.financial.push({ key, value, confidence: 0.85 });
210
+ categorized = true;
211
+ }
212
+
213
+ // Enhanced Date Detection
214
+ if (!categorized && (/date|time|year|month|day|created|modified|due|expiry|start|end|period/i.test(key) ||
215
+ isDateString(valueStr) || /^\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4}$/.test(valueStr) ||
216
+ /^\d{4}[\/\-\.]\d{1,2}[\/\-\.]\d{1,2}$/.test(valueStr))) {
217
+ categories.dates.push({ key, value, confidence: 0.9 });
218
+ categorized = true;
219
+ }
220
+
221
+ // Enhanced Identifier Detection
222
+ if (!categorized && (/id|number|ref|reference|code|invoice|contract|order|ticket|case|job|project|account|transaction/i.test(key) ||
223
+ /^[A-Z]{2,4}\d{3,}$/.test(valueStr) || /^INV[-_]?\d+/i.test(valueStr) ||
224
+ /^[A-Z0-9]{6,}$/.test(valueStr) || /^\d{6,}$/.test(valueStr))) {
225
+ categories.identifiers.push({ key, value, confidence: 0.8 });
226
+ categorized = true;
227
+ }
228
+
229
+ // Everything else goes to other with lower confidence
230
+ if (!categorized) {
231
+ categories.other.push({ key, value, confidence: 0.4 });
232
+ }
233
+ });
234
+
235
+ // Filter out empty categories
236
+ const filteredCategories = {};
237
+ Object.entries(categories).forEach(([categoryName, items]) => {
238
+ if (items.length > 0) {
239
+ filteredCategories[categoryName] = items;
240
+ }
241
+ });
242
+
243
+ return filteredCategories;
244
+ }, []);
245
+
246
+ const isDateString = (str) => {
247
+ const date = new Date(str);
248
+ return !isNaN(date.getTime()) && str.length > 6;
249
+ };
250
+
251
+ // Generate template suggestions based on detected data
252
+ const generateTemplateSuggestions = useCallback((extractedData) => {
253
+ const suggestions = [];
254
+ const keys = Object.keys(extractedData);
255
+
256
+ if (keys.some(k => /invoice/i.test(k)) && keys.some(k => /date/i.test(k))) {
257
+ suggestions.push({
258
+ id: 'invoice',
259
+ name: "Invoice Template",
260
+ filename: "INV_{{invoice_number}}_{{date}}",
261
+ description: "Standard invoice naming convention"
262
+ });
263
+ }
264
+
265
+ if (keys.some(k => /contract/i.test(k)) && keys.some(k => /client|customer/i.test(k))) {
266
+ suggestions.push({
267
+ id: 'contract',
268
+ name: "Contract Template",
269
+ filename: "CONTRACT_{{client_name}}_{{contract_number}}",
270
+ description: "Legal document naming convention"
271
+ });
272
+ }
273
+
274
+ if (keys.some(k => /quote|estimate/i.test(k))) {
275
+ suggestions.push({
276
+ id: 'quote',
277
+ name: "Quote Template",
278
+ filename: "QUOTE_{{quote_number}}_{{company_name}}",
279
+ description: "Quote document naming"
280
+ });
281
+ }
282
+
283
+ return suggestions;
284
+ }, []);
285
+
286
+ // Enhanced file upload handler
287
+ const handleFileUpload = async (files) => {
288
+ if (!files || files.length === 0) return;
289
+
290
+ const file = files[0];
291
+
292
+ // Validate file type and size
293
+ const allowedTypes = ['application/pdf', 'image/jpeg', 'image/png', 'image/jpg'];
294
+ if (!allowedTypes.includes(file.type)) {
295
+ toast.error('Please upload a PDF, JPG, or PNG file');
296
+ return;
297
+ }
298
+
299
+ if (file.size > 10 * 1024 * 1024) { // 10MB limit
300
+ toast.error('File size must be less than 10MB');
301
+ return;
302
+ }
303
+
304
+ setProcessing(true);
305
+ setProcessingStage({ upload: 'processing', ocr: 'pending', analysis: 'pending' });
306
+
307
+ try {
308
+ const formData = new FormData();
309
+ formData.append('file', file);
310
+ formData.append('template_id', routeParams[urlParam]);
311
+
312
+ // Simulate upload progress
313
+ setUploadProgress(0);
314
+ const progressInterval = setInterval(() => {
315
+ setUploadProgress(prev => Math.min(prev + 10, 90));
316
+ }, 200);
317
+
318
+ setProcessingStage({ upload: 'completed', ocr: 'processing', analysis: 'pending' });
319
+
320
+ const response = await fetchUtil({
321
+ url: '/ajax/ocr/analyzeFile',
322
+ method: 'POST',
323
+ data: formData,
324
+ });
325
+
326
+ clearInterval(progressInterval);
327
+ setUploadProgress(100);
328
+
329
+ setProcessingStage({ upload: 'completed', ocr: 'completed', analysis: 'processing' });
330
+
331
+ const result = response.data;
332
+
333
+ setData(prevState => ({
334
+ ...prevState,
335
+ [id]: {
336
+ ...prevState[id],
337
+ ...result,
338
+ uploadedFile: {
339
+ name: file.name,
340
+ size: file.size,
341
+ type: file.type
342
+ }
343
+ }
344
+ }));
345
+
346
+ setProcessingStage({ upload: 'completed', ocr: 'completed', analysis: 'completed' });
347
+ toast.success('File processed successfully!');
348
+
349
+ } catch (error) {
350
+ console.error(error);
351
+ toast.error('Something went wrong. Please try again.');
352
+ setProcessingStage({ upload: 'pending', ocr: 'pending', analysis: 'pending' });
353
+ } finally {
354
+ setProcessing(false);
355
+ setUploadProgress(0);
356
+ }
357
+ };
358
+
359
+ // Drag and drop handlers
360
+ const handleDrag = useCallback((e) => {
361
+ e.preventDefault();
362
+ e.stopPropagation();
363
+ if (e.type === "dragenter" || e.type === "dragover") {
364
+ setDragActive(true);
365
+ } else if (e.type === "dragleave") {
366
+ setDragActive(false);
367
+ }
368
+ }, []);
369
+
370
+ const handleDrop = useCallback((e) => {
371
+ e.preventDefault();
372
+ e.stopPropagation();
373
+ setDragActive(false);
374
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
375
+ handleFileUpload(e.dataTransfer.files);
376
+ }
377
+ }, []);
378
+
379
+ const handleInputChange = (e) => {
380
+ handleFileUpload(e.target.files);
381
+ };
382
+
383
+ const handleKeyToggle = (key) => {
384
+ const selectedKeys = ocrTemplateBuilder?.selectedKey || [];
385
+ const newSelectedKeys = selectedKeys.includes(key)
386
+ ? selectedKeys.filter(k => k !== key)
387
+ : [...selectedKeys, key];
388
+
389
+ setData(prevState => ({
390
+ ...prevState,
391
+ [id]: {
392
+ ...prevState[id],
393
+ selectedKey: newSelectedKeys
394
+ }
395
+ }));
396
+ };
397
+
398
+ const handleTitleToggle = (title) => {
399
+ const selectedTitles = ocrTemplateBuilder?.selectedTitle || [];
400
+ const newSelectedTitles = selectedTitles.includes(title)
401
+ ? selectedTitles.filter(t => t !== title)
402
+ : [...selectedTitles, title];
403
+
404
+ setData(prevState => ({
405
+ ...prevState,
406
+ [id]: {
407
+ ...prevState[id],
408
+ selectedTitle: newSelectedTitles
409
+ }
410
+ }));
411
+ };
412
+
413
+ const applyTemplate = (template) => {
414
+ setData(prevState => ({
415
+ ...prevState,
416
+ [id]: {
417
+ ...prevState[id],
418
+ dynamicFilename: template.filename
419
+ }
420
+ }));
421
+ toast.success(`Applied ${template.name}`);
422
+ };
423
+
424
+ // Generate actual filename preview by replacing variables with real values
425
+ const generatePreviewFilename = (template, keyValuePairs) => {
426
+ if (!template) return '';
427
+
428
+ let preview = template;
429
+
430
+ // Replace variables with actual values, but only if they have values
431
+ Object.entries(keyValuePairs).forEach(([key, value]) => {
432
+ const regex = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'gi');
433
+
434
+ // Only replace if value exists and is not empty/null/undefined
435
+ if (value && String(value).trim() !== '') {
436
+ const cleanValue = String(value).replace(/[^\w\s-_.]/g, '');
437
+ preview = preview.replace(regex, cleanValue);
438
+ } else {
439
+ // Remove the variable and any adjacent separators for empty values
440
+ preview = preview.replace(new RegExp(`[_\\-\\s]*\\{\\{\\s*${key}\\s*\\}\\}[_\\-\\s]*`, 'gi'), '');
441
+ }
442
+ });
443
+
444
+ // Highlight any remaining unreplaced variables (variables not in keyValuePairs)
445
+ preview = preview.replace(/\{\{[^}]+\}\}/g, match => `⚠️${match}`);
446
+
447
+ // Clean up multiple separators and trim
448
+ preview = preview
449
+ .replace(/[_\-\s]+/g, '_') // Replace multiple separators with single underscore
450
+ .replace(/^[_\-\s]+|[_\-\s]+$/g, '') // Remove leading/trailing separators
451
+ .replace(/_{2,}/g, '_'); // Replace multiple underscores with single
452
+
453
+ return preview || 'filename_preview';
454
+ };
455
+
456
+ // Validate template and return warning message if issues found
457
+ const validateTemplate = (template, selectedKeys) => {
458
+ if (!template) return null;
459
+
460
+ // Check for invalid characters
461
+ const invalidChars = /[<>:"/\\|?*]/g;
462
+ if (invalidChars.test(template.replace(/\{\{[^}]+\}\}/g, ''))) {
463
+ return 'Template contains invalid filename characters: < > : " / \\ | ? *';
464
+ }
465
+
466
+ // Check for unreferenced variables
467
+ const templateVars = (template.match(/\{\{([^}]+)\}\}/g) || [])
468
+ .map(match => match.replace(/[{}]/g, '').trim());
469
+ const invalidVars = templateVars.filter(varName => !selectedKeys.includes(varName));
470
+
471
+ if (invalidVars.length > 0) {
472
+ return `Template references undefined variables: ${invalidVars.join(', ')}`;
473
+ }
474
+
475
+ // Check for extremely long templates
476
+ if (template.length > 200) {
477
+ return 'Template is very long and may cause filename issues';
478
+ }
479
+
480
+ // Check if template starts/ends with separators
481
+ if (/^[_\-\s]|[_\-\s]$/.test(template)) {
482
+ return 'Template should not start or end with separators (_ - space)';
483
+ }
484
+
485
+ return null;
486
+ };
487
+
488
+ // Processing Step Component
489
+ const ProcessingStep = ({ status, title, description, progress }) => (
490
+ <div className={`${styles.processingStep} ${styles[status]}`}>
491
+ <div className={styles.stepIcon}>
492
+ {status === 'completed' && <CheckCircle size={20} />}
493
+ {status === 'processing' && <Loader size={20} className={styles.spinning} />}
494
+ {status === 'pending' && <Circle size={20} />}
495
+ </div>
496
+ <div className={styles.stepContent}>
497
+ <h4>{title}</h4>
498
+ <p>{description}</p>
499
+ {progress !== undefined && (
500
+ <div className={styles.progressBar}>
501
+ <div
502
+ className={styles.progressFill}
503
+ style={{ width: `${progress}%` }}
504
+ />
505
+ </div>
506
+ )}
507
+ </div>
508
+ </div>
509
+ );
510
+
511
+ // Data Card Component
512
+ const DataCard = ({ keyName, value, confidence, selected, onToggle, category }) => (
513
+ <div
514
+ className={`${styles.dataCard} ${selected ? styles.selected : ''} ${styles[category]}`}
515
+ onClick={onToggle}
516
+ >
517
+ <div className={styles.cardHeader}>
518
+ <span className={styles.keyName}>{keyName}</span>
519
+ <div className={styles.cardMeta}>
520
+ <span className={`${styles.confidence} ${styles[getConfidenceLevel(confidence)]}`}>
521
+ {Math.round(confidence * 100)}%
522
+ </span>
523
+ <CheckCircle
524
+ size={16}
525
+ className={selected ? styles.selectedIcon : styles.unselectedIcon}
526
+ />
527
+ </div>
528
+ </div>
529
+ <div className={styles.cardValue}>{String(value).slice(0, 100)}</div>
530
+ </div>
531
+ );
532
+
533
+ const getConfidenceLevel = (confidence) => {
534
+ if (confidence >= 0.8) return 'high';
535
+ if (confidence >= 0.6) return 'medium';
536
+ return 'low';
537
+ };
538
+
539
+ // Data Section Component
540
+ const DataSection = ({ title, icon, children, count }) => (
541
+ <div className={styles.dataSection}>
542
+ <div className={styles.sectionHeader}>
543
+ <div className={styles.sectionTitle}>
544
+ {icon}
545
+ <h3>{title}</h3>
546
+ <span className={styles.count}>({count})</span>
547
+ </div>
548
+ </div>
549
+ <div className={styles.sectionContent}>
550
+ {children}
551
+ </div>
552
+ </div>
553
+ );
554
+
555
+ const categorizedData = Object.keys(listOfKeys).length > 0 ? categorizeData(listOfKeys) : null;
556
+ const templateSuggestions = Object.keys(listOfKeys).length > 0 ? generateTemplateSuggestions(listOfKeys) : [];
557
+
558
+ // Step 1: Upload Component
559
+ const renderUploadStep = () => {
560
+ return (
561
+ <div className={styles.uploadSection}>
562
+ <div
563
+ className={`${styles.dropZone} ${dragActive ? styles.dragActive : ''}`}
564
+ onDragEnter={handleDrag}
565
+ onDragLeave={handleDrag}
566
+ onDragOver={handleDrag}
567
+ onDrop={handleDrop}
568
+ onClick={() => fileInputRef.current?.click()}
569
+ >
570
+ <div className={styles.uploadIcon}>
571
+ <Upload size={48} />
572
+ </div>
573
+ <h3>Drop document here or click to browse</h3>
574
+ <p>Supports PDF, JPG, PNG files up to 10MB</p>
575
+
576
+ <input
577
+ ref={fileInputRef}
578
+ type="file"
579
+ accept=".pdf,.jpg,.jpeg,.png"
580
+ onChange={handleInputChange}
581
+ style={{ display: 'none' }}
582
+ />
583
+ </div>
584
+
585
+ {/* Uploaded File Preview */}
586
+ {ocrTemplateBuilder.uploadedFile && (
587
+ <div className={styles.filePreview}>
588
+ <div className={styles.fileInfo}>
589
+ <FileText size={24} />
590
+ <div>
591
+ <span className={styles.fileName}>
592
+ {ocrTemplateBuilder.uploadedFile.name}
593
+ </span>
594
+ <span className={styles.fileSize}>
595
+ {(ocrTemplateBuilder.uploadedFile.size / 1024).toFixed(1)} KB
596
+ </span>
597
+ </div>
598
+ </div>
599
+ </div>
600
+ )}
601
+
602
+ {/* Processing Feedback */}
603
+ {processing && (
604
+ <div className={styles.processingContainer}>
605
+ <h3>Analyzing Document for Templates...</h3>
606
+ <div className={styles.processingSteps}>
607
+ <ProcessingStep
608
+ status={processingStage.upload}
609
+ title="Uploading Document"
610
+ description="Preparing file for analysis..."
611
+ progress={processingStage.upload === 'processing' ? uploadProgress : undefined}
612
+ />
613
+ <ProcessingStep
614
+ status={processingStage.ocr}
615
+ title="OCR Analysis"
616
+ description="Extracting text and detecting templates..."
617
+ />
618
+ </div>
619
+ </div>
620
+ )}
621
+ </div>
622
+ );
623
+ };
624
+
625
+ // Step 2: Data Review Component
626
+ const renderDataReviewStep = () => {
627
+ if (!categorizedData) {
628
+ return (
629
+ <div className={styles.emptyState}>
630
+ <AlertCircle size={48} />
631
+ <h3>No Data Extracted Yet</h3>
632
+ <p>Please complete Step 1 first by uploading a document.</p>
633
+ </div>
634
+ );
635
+ }
636
+
637
+ return (
638
+ <div className={styles.dataOrganizer}>
639
+ <div className={styles.extractedDataSections}>
640
+ {categorizedData.contact && categorizedData.contact.length > 0 && (
641
+ <DataSection
642
+ title="Contact Information"
643
+ icon={<User size={20} />}
644
+ count={categorizedData.contact.length}
645
+ >
646
+ {categorizedData.contact.map(item => (
647
+ <DataCard
648
+ key={item.key}
649
+ keyName={item.key}
650
+ value={item.value}
651
+ confidence={item.confidence}
652
+ category="contact"
653
+ selected={ocrTemplateBuilder?.selectedKey?.includes(item.key)}
654
+ onToggle={() => handleKeyToggle(item.key)}
655
+ />
656
+ ))}
657
+ </DataSection>
658
+ )}
659
+
660
+ {categorizedData.financial && categorizedData.financial.length > 0 && (
661
+ <DataSection
662
+ title="Financial Data"
663
+ icon={<DollarSign size={20} />}
664
+ count={categorizedData.financial.length}
665
+ >
666
+ {categorizedData.financial.map(item => (
667
+ <DataCard
668
+ key={item.key}
669
+ keyName={item.key}
670
+ value={item.value}
671
+ confidence={item.confidence}
672
+ category="financial"
673
+ selected={ocrTemplateBuilder?.selectedKey?.includes(item.key)}
674
+ onToggle={() => handleKeyToggle(item.key)}
675
+ />
676
+ ))}
677
+ </DataSection>
678
+ )}
679
+
680
+ {categorizedData.dates && categorizedData.dates.length > 0 && (
681
+ <DataSection
682
+ title="Dates & Times"
683
+ icon={<Calendar size={20} />}
684
+ count={categorizedData.dates.length}
685
+ >
686
+ {categorizedData.dates.map(item => (
687
+ <DataCard
688
+ key={item.key}
689
+ keyName={item.key}
690
+ value={item.value}
691
+ confidence={item.confidence}
692
+ category="dates"
693
+ selected={ocrTemplateBuilder?.selectedKey?.includes(item.key)}
694
+ onToggle={() => handleKeyToggle(item.key)}
695
+ />
696
+ ))}
697
+ </DataSection>
698
+ )}
699
+
700
+ {categorizedData.identifiers && categorizedData.identifiers.length > 0 && (
701
+ <DataSection
702
+ title="Identifiers"
703
+ icon={<Hash size={20} />}
704
+ count={categorizedData.identifiers.length}
705
+ >
706
+ {categorizedData.identifiers.map(item => (
707
+ <DataCard
708
+ key={item.key}
709
+ keyName={item.key}
710
+ value={item.value}
711
+ confidence={item.confidence}
712
+ category="identifiers"
713
+ selected={ocrTemplateBuilder?.selectedKey?.includes(item.key)}
714
+ onToggle={() => handleKeyToggle(item.key)}
715
+ />
716
+ ))}
717
+ </DataSection>
718
+ )}
719
+
720
+ {categorizedData.other && categorizedData.other.length > 0 && (
721
+ <DataSection
722
+ title="Other Data"
723
+ icon={<FileText size={20} />}
724
+ count={categorizedData.other.length}
725
+ >
726
+ {categorizedData.other.map(item => (
727
+ <DataCard
728
+ key={item.key}
729
+ keyName={item.key}
730
+ value={item.value}
731
+ confidence={item.confidence}
732
+ category="other"
733
+ selected={ocrTemplateBuilder?.selectedKey?.includes(item.key)}
734
+ onToggle={() => handleKeyToggle(item.key)}
735
+ />
736
+ ))}
737
+ </DataSection>
738
+ )}
739
+ </div>
740
+
741
+ {ocrTemplateBuilder?.selectedKey?.length > 0 && (
742
+ <div className={styles.selectionSummary}>
743
+ <CheckCircle2 size={20} />
744
+ <span>Selected {ocrTemplateBuilder.selectedKey.length} variables for template building</span>
745
+ </div>
746
+ )}
747
+ </div>
748
+ );
749
+ };
750
+
751
+ // Step 3: Title Selection Component
752
+ const renderTitleSelectionStep = () => {
753
+ if (!ocrTemplateBuilder.allText) {
754
+ return (
755
+ <div className={styles.emptyState}>
756
+ <AlertCircle size={48} />
757
+ <h3>No Text Extracted</h3>
758
+ <p>Please complete Step 1 first by uploading a document.</p>
759
+ </div>
760
+ );
761
+ }
762
+
763
+ return (
764
+ <div className={styles.documentTextSection}>
765
+ <div className={styles.titleSelectionHeader}>
766
+ <BookOpen size={24} />
767
+ <div>
768
+ <h3>Create Template Signature</h3>
769
+ <p>Select text lines that will help identify this document type in future processing</p>
770
+ </div>
771
+ </div>
772
+
773
+ <div style={{
774
+ background: '#e3f2fd',
775
+ padding: '0.75rem',
776
+ borderRadius: '6px',
777
+ marginBottom: '1rem',
778
+ fontSize: '0.875rem',
779
+ color: '#1565c0'
780
+ }}>
781
+ <strong>💡 Template Matching Tips:</strong> Choose unique text patterns that appear consistently in this document type.
782
+ Good examples: "INVOICE", document headers, company names, or specific field labels.
783
+ Avoid variable content like dates or amounts.
784
+ </div>
785
+
786
+ <div className={styles.textLinesGrid}>
787
+ {ocrTemplateBuilder.allText.map((line, index) => (
788
+ <div
789
+ key={index}
790
+ className={`${styles.textLine} ${
791
+ ocrTemplateBuilder?.selectedTitle?.includes(line) ? styles.selected : ''
792
+ }`}
793
+ onClick={() => handleTitleToggle(line)}
794
+ >
795
+ <CheckCircle
796
+ size={16}
797
+ className={ocrTemplateBuilder?.selectedTitle?.includes(line) ?
798
+ styles.selectedIcon : styles.unselectedIcon
799
+ }
800
+ />
801
+ <span>{line}</span>
802
+ </div>
803
+ ))}
804
+ </div>
805
+
806
+ {ocrTemplateBuilder?.selectedTitle?.length > 0 && (
807
+ <div className={styles.selectionSummary}>
808
+ <CheckCircle2 size={20} />
809
+ <span>Selected {ocrTemplateBuilder.selectedTitle.length} signature elements for template matching</span>
810
+ </div>
811
+ )}
812
+ </div>
813
+ );
814
+ };
815
+
816
+ // Step 4: Template Builder Component
817
+ const renderTemplateBuilderStep = () => {
818
+ if (!ocrTemplateBuilder?.selectedKey?.length) {
819
+ return (
820
+ <div className={styles.emptyState}>
821
+ <AlertCircle size={48} />
822
+ <h3>No Variables Selected</h3>
823
+ <p>Please complete Step 2 first by selecting data variables.</p>
824
+ </div>
825
+ );
826
+ }
827
+
828
+ return (
829
+ <div className={styles.templateBuilder}>
830
+ {/* Template Suggestions */}
831
+ {templateSuggestions.length > 0 && (
832
+ <div className={styles.templateSuggestions}>
833
+ <div className={styles.suggestionHeader}>
834
+ <Sparkles size={20} />
835
+ <h3>Smart Template Suggestions</h3>
836
+ </div>
837
+ <div className={styles.suggestionGrid}>
838
+ {templateSuggestions.map(template => (
839
+ <div
840
+ key={template.id}
841
+ className={styles.templateSuggestion}
842
+ onClick={() => applyTemplate(template)}
843
+ >
844
+ <Wand2 size={16} />
845
+ <h4>{template.name}</h4>
846
+ <p>{template.description}</p>
847
+ <code>{template.filename}</code>
848
+ </div>
849
+ ))}
850
+ </div>
851
+ </div>
852
+ )}
853
+
854
+ {/* Custom Template Editor */}
855
+ <div className={styles.customTemplate}>
856
+ <div className={styles.templateHeader}>
857
+ <Settings size={20} />
858
+ <h3>Filename Template Designer</h3>
859
+ </div>
860
+
861
+ <div className={styles.availableVariables}>
862
+ <div className={styles.variableHeader}>
863
+ <h4>Available Variables</h4>
864
+ <div className={styles.quickActions}>
865
+ <button
866
+ className={styles.quickBtn}
867
+ onClick={() => {
868
+ const allVars = ocrTemplateBuilder.selectedKey.map(key => `{{${key}}}`).join('_');
869
+ setData(prevState => ({
870
+ ...prevState,
871
+ [id]: {
872
+ ...prevState[id],
873
+ dynamicFilename: allVars
874
+ }
875
+ }));
876
+ }}
877
+ title="Use all variables"
878
+ >
879
+ <Zap size={14} />
880
+ Select All
881
+ </button>
882
+ <button
883
+ className={styles.quickBtn}
884
+ onClick={() => {
885
+ setData(prevState => ({
886
+ ...prevState,
887
+ [id]: {
888
+ ...prevState[id],
889
+ dynamicFilename: ''
890
+ }
891
+ }));
892
+ }}
893
+ title="Clear template"
894
+ >
895
+ <RotateCcw size={14} />
896
+ Clear
897
+ </button>
898
+ </div>
899
+ </div>
900
+ <p className={styles.variableHelp}>Click variables to add them to your template, or use quick actions above</p>
901
+ <div className={styles.variableChips}>
902
+ {ocrTemplateBuilder.selectedKey.map(key => (
903
+ <span
904
+ key={key}
905
+ className={styles.variableChip}
906
+ onClick={() => {
907
+ const currentFilename = ocrTemplateBuilder?.dynamicFilename || '';
908
+ const cursorPosition = document.querySelector(`input[value="${currentFilename}"]`)?.selectionStart || currentFilename.length;
909
+ const before = currentFilename.slice(0, cursorPosition);
910
+ const after = currentFilename.slice(cursorPosition);
911
+ const newFilename = `${before}{{${key}}}${after}`;
912
+
913
+ setData(prevState => ({
914
+ ...prevState,
915
+ [id]: {
916
+ ...prevState[id],
917
+ dynamicFilename: newFilename
918
+ }
919
+ }));
920
+ }}
921
+ title={`Insert {{${key}}} - Value: ${listOfKeys[key]}`}
922
+ >
923
+ {key}
924
+ <span className={styles.variableValue}>
925
+ {String(listOfKeys[key]).slice(0, 20)}
926
+ {String(listOfKeys[key]).length > 20 ? '...' : ''}
927
+ </span>
928
+ </span>
929
+ ))}
930
+ </div>
931
+ </div>
932
+
933
+ <div className={styles.templateField}>
934
+ <label>File Name Template</label>
935
+ <p className={styles.fieldDescription}>
936
+ 💡 Variables with no values will be automatically removed from the filename.
937
+ Use underscores (_) or hyphens (-) as separators.
938
+ </p>
939
+ <input
940
+ type="text"
941
+ value={ocrTemplateBuilder?.dynamicFilename || ''}
942
+ onChange={(e) => setData(prevState => ({
943
+ ...prevState,
944
+ [id]: {
945
+ ...prevState[id],
946
+ dynamicFilename: e.target.value
947
+ }
948
+ }))}
949
+ placeholder="Enter filename template (e.g., DOC_{{key1}}_{{key2}})"
950
+ />
951
+ </div>
952
+
953
+ {/* Enhanced Preview & Validation */}
954
+ {ocrTemplateBuilder?.dynamicFilename && (
955
+ <div className={styles.templatePreview}>
956
+ <h4>Real Filename Preview</h4>
957
+ <div className={styles.previewContainer}>
958
+ <div className={styles.previewBox}>
959
+ <span className={styles.previewLabel}>Generated filename:</span>
960
+ <code className={styles.generatedFilename}>
961
+ {generatePreviewFilename(ocrTemplateBuilder.dynamicFilename, listOfKeys)}
962
+ </code>
963
+ </div>
964
+ {validateTemplate(ocrTemplateBuilder.dynamicFilename, ocrTemplateBuilder.selectedKey) && (
965
+ <div className={styles.validationMessage}>
966
+ <AlertTriangle size={16} className={styles.validationIcon} />
967
+ {validateTemplate(ocrTemplateBuilder.dynamicFilename, ocrTemplateBuilder.selectedKey)}
968
+ </div>
969
+ )}
970
+ <div className={styles.templateStats}>
971
+ <span>Variables used: {(ocrTemplateBuilder.dynamicFilename.match(/\{\{[^}]+\}\}/g) || []).length}</span>
972
+ <span>Template length: {ocrTemplateBuilder.dynamicFilename.length} chars</span>
973
+ </div>
974
+ </div>
975
+ </div>
976
+ )}
977
+ </div>
978
+ </div>
979
+ );
980
+ };
981
+
982
+ // Step 5: Folder Configuration Component
983
+ const renderFolderConfigStep = () => {
984
+ return (
985
+ <div className={styles.folderConfigSection}>
986
+ <div className={styles.folderHeader}>
987
+ <FolderOpen size={24} />
988
+ <div>
989
+ <h3>Setup Automated Processing</h3>
990
+ <p>Configure folders for continuous document monitoring and processing</p>
991
+ </div>
992
+ </div>
993
+
994
+ <div style={{
995
+ background: '#f0f8f4',
996
+ padding: '1rem',
997
+ borderRadius: '8px',
998
+ marginBottom: '1.5rem',
999
+ border: '1px solid #d4edda'
1000
+ }}>
1001
+ <h4 style={{ margin: '0 0 0.5rem 0', color: '#155724', fontSize: '0.9rem' }}>
1002
+ 🔄 Automated Workflow
1003
+ </h4>
1004
+ <div style={{ fontSize: '0.8rem', color: '#155724', lineHeight: '1.4' }}>
1005
+ <strong>1.</strong> New documents → Watch Folder<br/>
1006
+ <strong>2.</strong> System detects & processes using this template<br/>
1007
+ <strong>3.</strong> Processed files → Destination Folder (with generated filenames)
1008
+ </div>
1009
+ </div>
1010
+
1011
+ <div className={styles.folderConfiguration}>
1012
+ <div className={styles.templateField}>
1013
+ <label>
1014
+ <Eye size={16} />
1015
+ Watch Folder (Input)
1016
+ </label>
1017
+ <p className={styles.fieldDescription}>
1018
+ 📂 The system continuously monitors this folder for new PDF documents.
1019
+ Any file dropped here will be automatically processed using this template.
1020
+ </p>
1021
+ <input
1022
+ type="text"
1023
+ value={data?.content?.watchFolder || ''}
1024
+ onChange={(e) => setData(prevState => ({
1025
+ ...prevState,
1026
+ content: {
1027
+ ...prevState.content,
1028
+ watchFolder: e.target.value
1029
+ }
1030
+ }))}
1031
+ placeholder="e.g., /documents/incoming/ or C:\Documents\Watch\"
1032
+ />
1033
+ </div>
1034
+
1035
+ <div className={styles.templateField}>
1036
+ <label>
1037
+ <ArrowRight size={16} />
1038
+ Destination Folder (Output)
1039
+ </label>
1040
+ <p className={styles.fieldDescription}>
1041
+ 📁 After processing, files are automatically moved here with the filename generated from your template.
1042
+ Original files are removed from the watch folder.
1043
+ </p>
1044
+ <input
1045
+ type="text"
1046
+ value={data?.content?.destinationFolder || ''}
1047
+ onChange={(e) => setData(prevState => ({
1048
+ ...prevState,
1049
+ content: {
1050
+ ...prevState.content,
1051
+ destinationFolder: e.target.value
1052
+ }
1053
+ }))}
1054
+ placeholder="e.g., /documents/processed/ or C:\Documents\Processed\"
1055
+ />
1056
+ </div>
1057
+ </div>
1058
+
1059
+ {data?.content?.watchFolder && data?.content?.destinationFolder && (
1060
+ <div className={styles.workflowPreview}>
1061
+ <div className={styles.workflowHeader}>
1062
+ <Settings size={20} />
1063
+ <h4>🔄 Automated Processing Workflow</h4>
1064
+ </div>
1065
+ <div className={styles.workflowSteps}>
1066
+ <div className={styles.workflowStep}>
1067
+ <Eye size={16} />
1068
+ <span>Watch: {data.content.watchFolder}</span>
1069
+ </div>
1070
+ <ArrowRight size={16} className={styles.workflowArrow} />
1071
+ <div className={styles.workflowStep}>
1072
+ <Wand2 size={16} />
1073
+ <span>OCR + Template Match</span>
1074
+ </div>
1075
+ <ArrowRight size={16} className={styles.workflowArrow} />
1076
+ <div className={styles.workflowStep}>
1077
+ <FileText size={16} />
1078
+ <span>Rename: {ocrTemplateBuilder?.dynamicFilename || 'No template set'}</span>
1079
+ </div>
1080
+ <ArrowRight size={16} className={styles.workflowArrow} />
1081
+ <div className={styles.workflowStep}>
1082
+ <CheckCircle2 size={16} />
1083
+ <span>Move to: {data.content.destinationFolder}</span>
1084
+ </div>
1085
+ </div>
1086
+ </div>
1087
+ )}
1088
+ </div>
1089
+ );
1090
+ };
1091
+
1092
+ // Journey Navigation Component
1093
+ const JourneyProgress = () => (
1094
+ <div className={styles.journeyProgress}>
1095
+ <div className={styles.progressHeader}>
1096
+ <div className={styles.progressTitle}>
1097
+ <MapPin size={20} />
1098
+ <h2>OCR Template Setup Journey</h2>
1099
+ </div>
1100
+ <div className={styles.progressStats}>
1101
+ <span>{completedSteps.size} of {journeySteps.length} steps completed</span>
1102
+ {hasAutoJumped && completedSteps.size > 0 && (
1103
+ <span style={{
1104
+ marginLeft: '0.5rem',
1105
+ fontSize: '0.7rem',
1106
+ background: '#e8f5e8',
1107
+ color: '#155724',
1108
+ padding: '0.2rem 0.5rem',
1109
+ borderRadius: '8px'
1110
+ }}>
1111
+ ↻ Resumed
1112
+ </span>
1113
+ )}
1114
+ </div>
1115
+ </div>
1116
+
1117
+ <div className={styles.progressSteps}>
1118
+ {journeySteps.map((step, index) => {
1119
+ const isActive = currentStep === step.id;
1120
+ const isCompleted = completedSteps.has(step.id);
1121
+ const canAccess = index === 0 || isCompleted || completedSteps.has(journeySteps[index - 1].id);
1122
+ const IconComponent = step.icon;
1123
+
1124
+ return (
1125
+ <div
1126
+ key={step.id}
1127
+ className={`${styles.progressStep} ${isActive ? styles.active : ''} ${isCompleted ? styles.completed : ''} ${!canAccess ? styles.disabled : ''}`}
1128
+ onClick={() => canAccess && navigateToStep(step.id)}
1129
+ >
1130
+ <div className={styles.stepIcon}>
1131
+ {isCompleted ? (
1132
+ <CheckCircle2 size={20} />
1133
+ ) : (
1134
+ <IconComponent size={20} />
1135
+ )}
1136
+ </div>
1137
+ <div className={styles.stepInfo}>
1138
+ <span className={styles.stepNumber}>Step {step.id}</span>
1139
+ <span className={styles.stepTitle}>{step.title}</span>
1140
+ </div>
1141
+ {index < journeySteps.length - 1 && (
1142
+ <ChevronRight size={16} className={styles.stepArrow} />
1143
+ )}
1144
+ </div>
1145
+ );
1146
+ })}
1147
+ </div>
1148
+ </div>
1149
+ );
1150
+
1151
+ // Step Instructions Component
1152
+ const StepInstructions = ({ step }) => (
1153
+ <div className={styles.stepInstructions}>
1154
+ <div className={styles.instructionHeader}>
1155
+ <div className={styles.instructionIcon}>
1156
+ <step.icon size={24} />
1157
+ </div>
1158
+ <div className={styles.instructionText}>
1159
+ <h3>{step.title}</h3>
1160
+ <p>{step.description}</p>
1161
+ </div>
1162
+ <div className={styles.instructionToggle}>
1163
+ <button
1164
+ className={styles.helpBtn}
1165
+ onClick={() => setShowStepDetails(prev => ({
1166
+ ...prev,
1167
+ [step.id]: !prev[step.id]
1168
+ }))}
1169
+ >
1170
+ <HelpCircle size={16} />
1171
+ {showStepDetails[step.id] ? 'Hide Help' : 'Show Help'}
1172
+ </button>
1173
+ </div>
1174
+ </div>
1175
+
1176
+ {showStepDetails[step.id] && (
1177
+ <div className={styles.detailedInstructions}>
1178
+ {getDetailedInstructions(step)}
1179
+ </div>
1180
+ )}
1181
+ </div>
1182
+ );
1183
+
1184
+ // Detailed instructions for each step
1185
+ const getDetailedInstructions = (step) => {
1186
+ switch (step.id) {
1187
+ case 1:
1188
+ return (
1189
+ <div className={styles.instructionDetails}>
1190
+ <div className={styles.instructionPoint}>
1191
+ <Info size={16} />
1192
+ <span>Supported formats: PDF, JPG, PNG files up to 10MB</span>
1193
+ </div>
1194
+ <div className={styles.instructionPoint}>
1195
+ <Target size={16} />
1196
+ <span>Best results: Clear text, good contrast, minimal skew</span>
1197
+ </div>
1198
+ <div className={styles.instructionPoint}>
1199
+ <Lightbulb size={16} />
1200
+ <span>System will analyze for multiple templates and auto-split if detected</span>
1201
+ </div>
1202
+ </div>
1203
+ );
1204
+ case 2:
1205
+ return (
1206
+ <div className={styles.instructionDetails}>
1207
+ <div className={styles.instructionPoint}>
1208
+ <Eye size={16} />
1209
+ <span>Review extracted data organized by intelligent categories</span>
1210
+ </div>
1211
+ <div className={styles.instructionPoint}>
1212
+ <CheckCircle size={16} />
1213
+ <span>Select key-value pairs that will be useful for filename templates</span>
1214
+ </div>
1215
+ <div className={styles.instructionPoint}>
1216
+ <Sparkles size={16} />
1217
+ <span>Choose identifiers, dates, and reference numbers for unique filenames</span>
1218
+ </div>
1219
+ </div>
1220
+ );
1221
+ case 3:
1222
+ return (
1223
+ <div className={styles.instructionDetails}>
1224
+ <div className={styles.instructionPoint}>
1225
+ <BookOpen size={16} />
1226
+ <span>Choose a combination of variables that uniquely identify this document type</span>
1227
+ </div>
1228
+ <div className={styles.instructionPoint}>
1229
+ <Info size={16} />
1230
+ <span>This signature helps the OCR system automatically match future documents to this template</span>
1231
+ </div>
1232
+ <div className={styles.instructionPoint}>
1233
+ <Target size={16} />
1234
+ <span>Best practice: Use 2-3 distinctive variables like document headers, format patterns, or field combinations</span>
1235
+ </div>
1236
+ </div>
1237
+ );
1238
+ case 4:
1239
+ return (
1240
+ <div className={styles.instructionDetails}>
1241
+ <div className={styles.instructionPoint}>
1242
+ <Wand2 size={16} />
1243
+ <span>Design filename patterns using variables like {{invoice_number}}_{{date}}</span>
1244
+ </div>
1245
+ <div className={styles.instructionPoint}>
1246
+ <Zap size={16} />
1247
+ <span>Variables with no values are automatically skipped in the final filename</span>
1248
+ </div>
1249
+ <div className={styles.instructionPoint}>
1250
+ <Eye size={16} />
1251
+ <span>Preview shows the exact filename that will be generated for this document</span>
1252
+ </div>
1253
+ </div>
1254
+ );
1255
+ case 5:
1256
+ return (
1257
+ <div className={styles.instructionDetails}>
1258
+ <div className={styles.instructionPoint}>
1259
+ <FolderOpen size={16} />
1260
+ <span>Watch folder: System continuously monitors this folder for new documents to process</span>
1261
+ </div>
1262
+ <div className={styles.instructionPoint}>
1263
+ <ArrowRight size={16} />
1264
+ <span>Destination folder: Processed files are automatically moved here with generated filenames</span>
1265
+ </div>
1266
+ <div className={styles.instructionPoint}>
1267
+ <Settings size={16} />
1268
+ <span>Once configured, the system will automatically process new files using this template</span>
1269
+ </div>
1270
+ </div>
1271
+ );
1272
+ default:
1273
+ return null;
1274
+ }
1275
+ };
1276
+
1277
+ return (
1278
+ <div className={styles.ocrTemplateContainer}>
1279
+ {/* Journey Progress Header */}
1280
+ <JourneyProgress />
1281
+
1282
+ {/* Current Step Instructions */}
1283
+ <StepInstructions step={journeySteps.find(s => s.id === currentStep)} />
1284
+ {/* Step Content */}
1285
+ <div className={styles.stepContent}>
1286
+ {currentStep === 1 && renderUploadStep()}
1287
+ {currentStep === 2 && renderDataReviewStep()}
1288
+ {currentStep === 3 && renderTitleSelectionStep()}
1289
+ {currentStep === 4 && renderTemplateBuilderStep()}
1290
+ {currentStep === 5 && renderFolderConfigStep()}
1291
+ </div>
1292
+
1293
+ {/* Navigation Controls */}
1294
+ <div className={styles.journeyNavigation}>
1295
+ <button
1296
+ className={styles.navBtn}
1297
+ onClick={() => navigateToStep(Math.max(1, currentStep - 1))}
1298
+ disabled={currentStep === 1}
1299
+ >
1300
+ <ArrowLeft size={16} />
1301
+ Previous Step
1302
+ </button>
1303
+
1304
+ <div className={styles.stepIndicator}>
1305
+ Step {currentStep} of {journeySteps.length}
1306
+ </div>
1307
+
1308
+ <button
1309
+ className={styles.navBtn}
1310
+ onClick={() => navigateToStep(Math.min(journeySteps.length, currentStep + 1))}
1311
+ disabled={currentStep === journeySteps.length}
1312
+ >
1313
+ Next Step
1314
+ <ArrowRight size={16} />
1315
+ </button>
1316
+ </div>
1317
+ </div>
1318
+ );
1319
+ };
1320
+
1321
+ export default OcrTemplateEnhanced;