@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.
- package/README.md +241 -0
- package/package.json +3 -3
- package/src/components/generic/GenericDashboard.jsx +419 -55
- package/src/components/generic/GenericDetail.jsx +21 -7
- package/src/components/generic/OcrTemplateEnhanced.jsx +1321 -0
- package/src/components/generic/OcrTemplateEnhanced.md +249 -0
- package/src/components/generic/OcrTemplateEnhanced.module.scss +1226 -0
- package/src/components/styles/GenericDashboard.module.scss +236 -0
- package/src/index.js +2 -0
|
@@ -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;
|