@visns-studio/visns-components 5.11.1 → 5.11.3

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.
Files changed (60) hide show
  1. package/package.json +2 -2
  2. package/src/components/cms/DataGrid.jsx +8 -8
  3. package/src/components/cms/DropZone.jsx +1 -1
  4. package/src/components/cms/Form.jsx +1 -1
  5. package/src/components/cms/Gallery.jsx +1 -1
  6. package/src/components/cms/sorting/Item.jsx +1 -1
  7. package/src/components/crm/AssociationManager.jsx +431 -0
  8. package/src/components/crm/Autocomplete.jsx +3 -3
  9. package/src/components/crm/Breadcrumb.jsx +4 -4
  10. package/src/components/crm/BusinessCardOcr.jsx +681 -95
  11. package/src/components/crm/ClientAssociationManager.jsx +362 -0
  12. package/src/components/crm/DataGrid.jsx +34 -32
  13. package/src/components/crm/Field.jsx +12 -12
  14. package/src/components/crm/Form.jsx +2 -2
  15. package/src/components/crm/MergeEntity.jsx +1049 -0
  16. package/src/components/crm/MultiSelect.jsx +14 -3
  17. package/src/components/crm/Navigation.jsx +14 -15
  18. package/src/components/crm/Notification.jsx +2 -2
  19. package/src/components/crm/QuickAction.jsx +3 -3
  20. package/src/components/crm/SectionHeader.jsx +1 -1
  21. package/src/components/crm/SwitchAccount.jsx +4 -4
  22. package/src/components/crm/auth/ClientLogin.jsx +2 -2
  23. package/src/components/crm/auth/ClientOTPVerify.jsx +2 -2
  24. package/src/components/crm/auth/Login.jsx +3 -3
  25. package/src/components/crm/auth/Reset.jsx +2 -2
  26. package/src/components/crm/auth/TwoFactorAuth.jsx +2 -2
  27. package/src/components/crm/cells/CellWithTooltip.jsx +35 -28
  28. package/src/components/crm/columns/ColumnRenderers.jsx +320 -259
  29. package/src/components/crm/controls/DataGridSearch.jsx +5 -5
  30. package/src/components/crm/generic/AlternativeActionModal.jsx +100 -0
  31. package/src/components/crm/generic/ContactSelectorModal.jsx +59 -16
  32. package/src/components/crm/generic/DateRangeSelectorModal.jsx +181 -0
  33. package/src/components/crm/generic/GenericClientPortal.jsx +1 -1
  34. package/src/components/crm/generic/GenericDashboard.jsx +4 -4
  35. package/src/components/crm/generic/GenericDetail.jsx +31 -6
  36. package/src/components/crm/generic/GenericDynamic.jsx +3 -3
  37. package/src/components/crm/generic/GenericEditableTable.jsx +4 -4
  38. package/src/components/crm/generic/GenericFormBuilder.jsx +6 -6
  39. package/src/components/crm/generic/GenericGrid.jsx +1622 -268
  40. package/src/components/crm/generic/GenericReport.jsx +966 -646
  41. package/src/components/crm/generic/GenericReportForm.jsx +1 -1
  42. package/src/components/crm/generic/NotificationList.jsx +1 -1
  43. package/src/components/crm/generic/ReasonCollectorModal.jsx +194 -0
  44. package/src/components/crm/generic/SortableQuoteItems.jsx +3 -3
  45. package/src/components/crm/generic/shared/formatters.js +107 -1
  46. package/src/components/crm/generic/styles/AlternativeActionModal.css +127 -0
  47. package/src/components/crm/generic/styles/DateRangeSelectorModal.css +115 -0
  48. package/src/components/crm/generic/styles/GenericIndex.module.scss +206 -0
  49. package/src/components/crm/generic/styles/GenericReport.module.scss +96 -0
  50. package/src/components/crm/generic/styles/ReasonCollectorModal.css +217 -0
  51. package/src/components/crm/modals/GalleryModal.jsx +2 -2
  52. package/src/components/crm/sorting/Item.jsx +3 -3
  53. package/src/components/crm/styles/AssociationManager.module.scss +364 -0
  54. package/src/components/crm/styles/BusinessCardOcr.module.scss +197 -4
  55. package/src/components/crm/styles/ClientAssociationManager.module.scss +290 -0
  56. package/src/components/crm/styles/MergeEntity.module.scss +690 -0
  57. package/src/components/crm/styles/MultiSelect.module.scss +18 -0
  58. package/src/components/crm/styles/Navigation.module.scss +68 -8
  59. package/src/components/crm/styles/global-datagrid.css +9 -0
  60. package/src/index.js +6 -0
@@ -6,16 +6,19 @@ import { toast } from 'react-toastify';
6
6
  import Tesseract from 'tesseract.js';
7
7
  import {
8
8
  Camera,
9
- Image as ImageIcon,
10
- CircleX,
11
- ArrowCycle,
12
- CircleCheck,
13
- ArrowCounterClockwise,
14
- Envelope,
15
- Network,
16
- Edit,
17
- Plus,
18
- } from 'akar-icons';
9
+ ImagePlus,
10
+ X,
11
+ SwitchCamera,
12
+ CheckCircle,
13
+ RotateCcw,
14
+ Mail,
15
+ Globe,
16
+ Building2,
17
+ User,
18
+ Zap,
19
+ ZapOff,
20
+ Focus,
21
+ } from 'lucide-react';
19
22
 
20
23
  import CustomFetch from './Fetch';
21
24
  import ocrStyles from './styles/BusinessCardOcr.module.scss';
@@ -34,6 +37,9 @@ const BusinessCardOcr = ({
34
37
  const [cameraFacing, setCameraFacing] = useState('environment'); // environment (back), user (front)
35
38
  const [availableCameras, setAvailableCameras] = useState([]);
36
39
  const [currentCameraIndex, setCurrentCameraIndex] = useState(0);
40
+ const [flashEnabled, setFlashEnabled] = useState(false);
41
+ const [isFocusing, setIsFocusing] = useState(false);
42
+ const [cameraReady, setCameraReady] = useState(false);
37
43
  const [isProcessing, setIsProcessing] = useState(false);
38
44
  const [extractedData, setExtractedData] = useState({});
39
45
  const [ocrProgress, setOcrProgress] = useState(0);
@@ -112,9 +118,10 @@ const BusinessCardOcr = ({
112
118
 
113
119
  const initializeCamera = async () => {
114
120
  try {
121
+ setCameraReady(false);
115
122
  let constraints;
116
123
 
117
- // Use specific device if available cameras are detected
124
+ // Enhanced camera constraints with focus and quality
118
125
  if (
119
126
  availableCameras.length > 0 &&
120
127
  availableCameras[currentCameraIndex]
@@ -125,17 +132,25 @@ const BusinessCardOcr = ({
125
132
  exact: availableCameras[currentCameraIndex]
126
133
  .deviceId,
127
134
  },
128
- width: { ideal: 1280 },
129
- height: { ideal: 720 },
135
+ width: { min: 1280, ideal: 1920, max: 3840 },
136
+ height: { min: 720, ideal: 1080, max: 2160 },
137
+ focusMode: ['continuous', 'auto'],
138
+ exposureMode: 'auto',
139
+ whiteBalanceMode: 'auto',
140
+ torch: flashEnabled
130
141
  },
131
142
  };
132
143
  } else {
133
- // Fallback to facing mode
144
+ // Fallback to facing mode with enhanced settings
134
145
  constraints = {
135
146
  video: {
136
147
  facingMode: cameraFacing,
137
- width: { ideal: 1280 },
138
- height: { ideal: 720 },
148
+ width: { min: 1280, ideal: 1920, max: 3840 },
149
+ height: { min: 720, ideal: 1080, max: 2160 },
150
+ focusMode: ['continuous', 'auto'],
151
+ exposureMode: 'auto',
152
+ whiteBalanceMode: 'auto',
153
+ torch: flashEnabled
139
154
  },
140
155
  };
141
156
  }
@@ -147,6 +162,9 @@ const BusinessCardOcr = ({
147
162
 
148
163
  if (videoRef.current) {
149
164
  videoRef.current.srcObject = stream;
165
+ videoRef.current.onloadedmetadata = () => {
166
+ setCameraReady(true);
167
+ };
150
168
  }
151
169
  } catch (error) {
152
170
  console.error('Error accessing camera:', error);
@@ -161,9 +179,80 @@ const BusinessCardOcr = ({
161
179
  }
162
180
  };
163
181
 
164
- const capturePhoto = useCallback(() => {
182
+ // Autofocus trigger before capture
183
+ const triggerAutofocus = async () => {
184
+ if (!cameraStream) return;
185
+
186
+ setIsFocusing(true);
187
+ try {
188
+ const track = cameraStream.getVideoTracks()[0];
189
+ const capabilities = track.getCapabilities();
190
+
191
+ if (capabilities.focusMode && capabilities.focusMode.includes('auto')) {
192
+ await track.applyConstraints({
193
+ advanced: [{ focusMode: 'auto' }]
194
+ });
195
+
196
+ // Wait for focus to settle
197
+ await new Promise(resolve => setTimeout(resolve, 1000));
198
+ }
199
+ } catch (error) {
200
+ console.warn('Autofocus failed:', error);
201
+ } finally {
202
+ setIsFocusing(false);
203
+ }
204
+ };
205
+
206
+ // Image preprocessing pipeline
207
+ const preprocessImage = async (imageBlob) => {
208
+ return new Promise((resolve) => {
209
+ const canvas = document.createElement('canvas');
210
+ const ctx = canvas.getContext('2d');
211
+ const img = new Image();
212
+
213
+ img.onload = () => {
214
+ // Step 1: Optimal sizing (2400px width max for OCR)
215
+ const maxWidth = 2400;
216
+ const scale = Math.min(maxWidth / img.width, maxWidth / img.height, 1);
217
+ canvas.width = img.width * scale;
218
+ canvas.height = img.height * scale;
219
+
220
+ // Step 2: Draw with anti-aliasing disabled for sharp text
221
+ ctx.imageSmoothingEnabled = false;
222
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
223
+
224
+ // Step 3: Enhance contrast and brightness
225
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
226
+ const data = imageData.data;
227
+
228
+ for (let i = 0; i < data.length; i += 4) {
229
+ // Increase contrast (1.4x) and brightness (+20) for better OCR
230
+ data[i] = Math.min(255, Math.max(0, data[i] * 1.4 + 20)); // Red
231
+ data[i + 1] = Math.min(255, Math.max(0, data[i + 1] * 1.4 + 20)); // Green
232
+ data[i + 2] = Math.min(255, Math.max(0, data[i + 2] * 1.4 + 20)); // Blue
233
+ }
234
+
235
+ ctx.putImageData(imageData, 0, 0);
236
+
237
+ // Step 4: Convert to high-quality PNG for OCR
238
+ canvas.toBlob(resolve, 'image/png', 1.0);
239
+ };
240
+
241
+ img.onerror = () => {
242
+ console.warn('Image preprocessing failed, using original');
243
+ resolve(imageBlob);
244
+ };
245
+
246
+ img.src = URL.createObjectURL(imageBlob);
247
+ });
248
+ };
249
+
250
+ const capturePhoto = useCallback(async () => {
165
251
  if (!videoRef.current || !canvasRef.current) return;
166
252
 
253
+ // Trigger autofocus before capture
254
+ await triggerAutofocus();
255
+
167
256
  const video = videoRef.current;
168
257
  const canvas = canvasRef.current;
169
258
  const context = canvas.getContext('2d');
@@ -173,15 +262,17 @@ const BusinessCardOcr = ({
173
262
  context.drawImage(video, 0, 0);
174
263
 
175
264
  canvas.toBlob(
176
- (blob) => {
177
- setCapturedImage(blob);
265
+ async (blob) => {
266
+ // Preprocess image for better OCR
267
+ const processedBlob = await preprocessImage(blob);
268
+ setCapturedImage(processedBlob);
178
269
  setCurrentStep('preview');
179
270
  stopCamera();
180
271
  },
181
272
  'image/jpeg',
182
- 0.9
273
+ 0.95
183
274
  );
184
- }, []);
275
+ }, [cameraStream]);
185
276
 
186
277
  const switchCamera = () => {
187
278
  if (availableCameras.length > 1) {
@@ -196,6 +287,28 @@ const BusinessCardOcr = ({
196
287
  }
197
288
  };
198
289
 
290
+ // Toggle flash/torch
291
+ const toggleFlash = async () => {
292
+ if (!cameraStream) return;
293
+
294
+ try {
295
+ const track = cameraStream.getVideoTracks()[0];
296
+ const capabilities = track.getCapabilities();
297
+
298
+ if (capabilities.torch) {
299
+ await track.applyConstraints({
300
+ advanced: [{ torch: !flashEnabled }]
301
+ });
302
+ setFlashEnabled(!flashEnabled);
303
+ } else {
304
+ toast.warn('Flash not supported on this camera');
305
+ }
306
+ } catch (error) {
307
+ console.warn('Flash toggle failed:', error);
308
+ toast.warn('Unable to control flash');
309
+ }
310
+ };
311
+
199
312
  const retakePhoto = () => {
200
313
  setCapturedImage(null);
201
314
  setCurrentStep('camera');
@@ -254,6 +367,55 @@ const BusinessCardOcr = ({
254
367
  event.target.value = '';
255
368
  };
256
369
 
370
+ // Advanced OCR with optimized configuration
371
+ const performAdvancedOCR = async (imageBlob) => {
372
+ const ocrConfig = {
373
+ logger: (m) => {
374
+ if (m.status === 'recognizing text') {
375
+ const progress = Math.round(m.progress * 100);
376
+ setOcrProgress(progress);
377
+ }
378
+ },
379
+ options: {
380
+ tessedit_pageseg_mode: '6', // Single uniform block
381
+ tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghklmnopqrstuvwxyz0123456789@.-_+()/ ',
382
+ tessedit_ocr_engine_mode: '1', // Neural nets LSTM engine
383
+ preserve_interword_spaces: '1',
384
+ tessjs_create_pdf: '0',
385
+ tessjs_create_hocr: '0'
386
+ }
387
+ };
388
+
389
+ try {
390
+ // Pass 1: Standard recognition (60% of progress)
391
+ setOcrProgress(10);
392
+ const result1 = await Tesseract.recognize(imageBlob, 'eng', ocrConfig);
393
+ setOcrProgress(60);
394
+
395
+ // Pass 2: Alternative segmentation for stubborn text (40% of progress)
396
+ const altConfig = {
397
+ ...ocrConfig,
398
+ options: { ...ocrConfig.options, tessedit_pageseg_mode: '8' } // Single word
399
+ };
400
+ const result2 = await Tesseract.recognize(imageBlob, 'eng', altConfig);
401
+ setOcrProgress(100);
402
+
403
+ // Combine results - prefer the longer, more complete text
404
+ const text1 = result1.data.text.trim();
405
+ const text2 = result2.data.text.trim();
406
+ const finalText = text1.length > text2.length ? text1 : text2;
407
+
408
+ console.log('OCR Result 1:', text1);
409
+ console.log('OCR Result 2:', text2);
410
+ console.log('Final Text:', finalText);
411
+
412
+ return finalText;
413
+ } catch (error) {
414
+ console.error('Advanced OCR failed:', error);
415
+ throw error;
416
+ }
417
+ };
418
+
257
419
  const processBusinessCard = async () => {
258
420
  if (!capturedImage) return;
259
421
 
@@ -262,17 +424,8 @@ const BusinessCardOcr = ({
262
424
  setOcrProgress(0);
263
425
 
264
426
  try {
265
- const result = await Tesseract.recognize(capturedImage, 'eng', {
266
- logger: (m) => {
267
- if (m.status === 'recognizing text') {
268
- const progress = Math.round(m.progress * 100);
269
- setOcrProgress(progress);
270
- }
271
- },
272
- });
273
-
274
- const text = result.data.text;
275
- const parsedData = parseExtractedText(text);
427
+ const text = await performAdvancedOCR(capturedImage);
428
+ const parsedData = intelligentFieldMapping(text);
276
429
  setExtractedData({ ...parsedData, rawText: text });
277
430
  setCurrentStep('results');
278
431
  toast.success('Business card processed successfully!');
@@ -348,10 +501,10 @@ const BusinessCardOcr = ({
348
501
  const spaceRatio = spaces / total;
349
502
 
350
503
  // Skip if mostly symbols
351
- if (symbolRatio > 0.3) return true;
504
+ if (symbolRatio > 0.4) return true;
352
505
 
353
506
  // Skip if too many consecutive spaces or weird spacing
354
- if (spaceRatio > 0.4) return true;
507
+ if (spaceRatio > 0.5) return true;
355
508
 
356
509
  // Skip if it's all numbers or mostly numbers (unless it looks like a phone)
357
510
  if (
@@ -361,10 +514,34 @@ const BusinessCardOcr = ({
361
514
  )
362
515
  return true;
363
516
 
364
- // Skip lines that look like formatting artifacts
365
- const formatArtifacts =
366
- /^[.\-_|]+$|^[^a-zA-Z]*[|]+[^a-zA-Z]*$|^\s*[.]\s*[A-Z]?\s*$/;
367
- if (formatArtifacts.test(trimmed)) return true;
517
+ // Enhanced formatting artifacts detection
518
+ const formatArtifacts = [
519
+ /^[.\-_|>]+$/, // Lines of just punctuation
520
+ /^[^a-zA-Z]*[|>]+[^a-zA-Z]*$/, // Lines with pipes/arrows mixed with symbols
521
+ /^\s*[.]\s*[A-Z]?\s*$/, // Dots with random letters
522
+ /^[A-Z]\s*[>|]\s*[|]*\s*[A-Za-z\.]*\s*[|]*$/, // Pattern like "I | I.egal"
523
+ /^[)>|.\s]{3,}$/, // Lines of mixed symbols
524
+ /^[a-zA-Z]\s*[)>|.\s]+$/, // Single letter followed by symbols
525
+ /^[\s\d§.\-_]+$/, // Lines of numbers and symbols only
526
+ /^[EF]{2,}[.\s]*$/, // Repeated E or F letters (common OCR errors)
527
+ // New patterns for the current example
528
+ /^[A-Z\s]{4,}[R\s]{2,}.*$/, // "HEIR A NR RS RR rn ORRIN"
529
+ /^[A-Z][A-Z\s]{3,}[A-Z].*$/, // All caps with spaces
530
+ /^[—\-]\s*[A-Za-z]{1,3}\s*[§].*$/, // "— Cl § ae"
531
+ /^[&§¢”]+.*$/, // Lines starting with symbols
532
+ /^[,\-'\(¢¢”]+.*$/, // Lines starting with punctuation
533
+ /^\d+\s*[A-Za-z\s]{1,10}\s*[¢”]+.*$/, // "94 Level 3 16 Irwin S ¢"
534
+ /^[a-z]{1,4}\s*[A-Za-z]\s*[b]\s*[A-Za-z]\s*[b][ef]\s*[fs].*$/, // "dose I b E be fs"
535
+ /^\d+\s*[i]\s*[A-Za-z]{1,3}\s*\d+\s*[A-Za-z]{1,3}$/, // "3 i Ee 4 Le"
536
+ /^[x]\s*[A-Za-z]{1,3}\s*[A-Z]{1,3}\s*[&]+.*$/, // "x Ei FY &"
537
+ /^[A-Z]{3,}.*[R]{2,}.*$/, // Multiple caps with repeated R's
538
+ /.*[die]$/, // Lines ending with "die"
539
+ /^[ORRIN]{3,}.*$/, // Lines with repeated caps like ORRIN
540
+ ];
541
+
542
+ for (const pattern of formatArtifacts) {
543
+ if (pattern.test(trimmed)) return true;
544
+ }
368
545
 
369
546
  // Skip if it has random character combinations
370
547
  const randomPattern =
@@ -394,7 +571,7 @@ const BusinessCardOcr = ({
394
571
 
395
572
  // Check for excessive symbol contamination in words
396
573
  const symbolsInWord = (word.match(/[^a-zA-Z0-9]/g) || []).length;
397
- if (symbolsInWord / word.length > 0.3) {
574
+ if (symbolsInWord / word.length > 0.4) {
398
575
  gibberishWords++;
399
576
  continue;
400
577
  }
@@ -427,22 +604,33 @@ const BusinessCardOcr = ({
427
604
  }
428
605
  }
429
606
 
430
- // If more than 60% of words are gibberish or mixed case, consider it gibberish
607
+ // If more than 50% of words are gibberish or mixed case, consider it gibberish
431
608
  const gibberishRatio = gibberishWords / Math.max(totalWords, 1);
432
609
  const mixedCaseRatio = mixedCaseWords / Math.max(totalWords, 1);
433
610
 
434
- if (gibberishRatio > 0.6) return true;
435
- if (mixedCaseRatio > 0.5 && totalWords > 2) return true;
611
+ if (gibberishRatio > 0.5) return true;
612
+ if (mixedCaseRatio > 0.4 && totalWords > 2) return true;
436
613
 
437
- // Check for specific gibberish patterns
614
+ // Check for specific gibberish patterns from your example
438
615
  const gibberishPatterns = [
439
- // Patterns like ": Re : Bei oc oC"
616
+ // Patterns like "> | I.egal Practitioner Director |"
617
+ /^[>|]\s*[|]\s*[I]\.[a-zA-Z]+/,
618
+ // Patterns like "urae (08) 9220 4402" (partial words)
619
+ /^[a-z]{2,4}\s*\(/,
620
+ // Patterns like ") . 3 dh § 8" (symbols with scattered letters/numbers)
621
+ /^[)].+[§].+$/,
622
+ // Patterns like "» 3 "e |" (mixed symbols)
623
+ /^[»].+["e].+[|]/,
624
+ // Patterns like "oy Level" (partial words)
625
+ /^[a-z]{1,2}\s+[A-Z][a-z]+/,
626
+ // Patterns like "EE. Se" (repeated letters)
627
+ /^[A-Z]{2,}[.]\s*[A-Z][a-z]$/,
628
+ // Patterns like "fio) i ) L\" (mixed parentheses and letters)
629
+ /^[a-z]{2,3}[)].*[)].*[L\\]/,
630
+ // Original patterns
440
631
  /^[:;]\s*[A-Za-z]{1,3}\s*[:;]\s*[A-Za-z]{1,4}\s+[a-z]{1,2}\s+[a-zA-Z]{1,2}$/,
441
- // Patterns like "HE — SE TL Beant 2 Es Re Se"
442
632
  /^[A-Z]{1,3}\s*[—\-]\s*[A-Z]{1,3}\s+[A-Z]{1,3}\s+[A-Za-z]{1,6}\s+\d+\s+[A-Za-z]{1,3}\s+[A-Za-z]{1,3}\s+[A-Za-z]{1,3}$/,
443
- // Patterns with excessive brackets, symbols mixed with short words
444
633
  /[>\[\]]{2,}|[A-Za-z]{1,3}\s+[>\[\]<]+\s+[A-Za-z]{1,3}/,
445
- // Excessive punctuation mixed with short letter sequences
446
634
  /^[^a-zA-Z]*[a-zA-Z]{1,2}[^a-zA-Z]+[a-zA-Z]{1,3}[^a-zA-Z]+.*$/,
447
635
  ];
448
636
 
@@ -503,6 +691,8 @@ const BusinessCardOcr = ({
503
691
  switch (fieldType) {
504
692
  case 'name':
505
693
  // For names: only letters, spaces, apostrophes, periods, hyphens
694
+ // First remove pipe symbols and other common artifacts
695
+ cleaned = cleaned.replace(/[|]/g, ' '); // Replace pipes with spaces
506
696
  cleaned = cleaned.replace(/[^a-zA-Z\s'.-]/g, '');
507
697
  // Remove excessive spaces
508
698
  cleaned = cleaned.replace(/\s+/g, ' ');
@@ -512,9 +702,11 @@ const BusinessCardOcr = ({
512
702
 
513
703
  case 'company':
514
704
  // For companies: letters, numbers, spaces, common punctuation
705
+ // Remove pipe characters and other artifacts first
706
+ cleaned = cleaned.replace(/[|]/g, ' '); // Replace pipes with spaces
515
707
  cleaned = cleaned.replace(/[^a-zA-Z0-9\s&.,'-]/g, '');
516
- // Remove pipe characters and equals signs that OCR often mistakes
517
- cleaned = cleaned.replace(/[|="]/g, '');
708
+ // Remove equals signs that OCR often mistakes
709
+ cleaned = cleaned.replace(/[="]/g, '');
518
710
  // Remove excessive spaces
519
711
  cleaned = cleaned.replace(/\s+/g, ' ');
520
712
  // Remove leading/trailing punctuation except periods
@@ -526,9 +718,11 @@ const BusinessCardOcr = ({
526
718
 
527
719
  case 'position':
528
720
  // For positions: letters, spaces, common punctuation
721
+ // Remove pipe characters and other artifacts first
722
+ cleaned = cleaned.replace(/[|]/g, ' '); // Replace pipes with spaces
529
723
  cleaned = cleaned.replace(/[^a-zA-Z\s&.,'-]/g, '');
530
- // Remove pipe characters and equals signs
531
- cleaned = cleaned.replace(/[|="]/g, '');
724
+ // Remove equals signs
725
+ cleaned = cleaned.replace(/[="]/g, '');
532
726
  // Remove excessive spaces
533
727
  cleaned = cleaned.replace(/\s+/g, ' ');
534
728
  // Remove leading/trailing punctuation
@@ -555,7 +749,7 @@ const BusinessCardOcr = ({
555
749
 
556
750
  default:
557
751
  // General cleaning: remove obvious artifacts
558
- cleaned = cleaned.replace(/[|="]/g, '');
752
+ cleaned = cleaned.replace(/[|="]/g, ' '); // Replace pipes with spaces
559
753
  cleaned = cleaned.replace(/\s+/g, ' ');
560
754
  }
561
755
 
@@ -568,7 +762,369 @@ const BusinessCardOcr = ({
568
762
  return cleaned;
569
763
  };
570
764
 
765
+ // Helper function to capitalize first letter of each word (ucfirst)
766
+ const ucFirstWords = (text) => {
767
+ if (!text || typeof text !== 'string') return '';
768
+
769
+ return text
770
+ .toLowerCase()
771
+ .split(' ')
772
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
773
+ .join(' ');
774
+ };
775
+
776
+ // Intelligent field mapping with confidence scoring
777
+ const intelligentFieldMapping = (rawText) => {
778
+ console.log('Raw OCR Text:', rawText);
779
+
780
+ const lines = rawText.split('\n').filter(line => line.trim().length > 0);
781
+ const fieldCandidates = {};
782
+
783
+ // Enhanced email detection
784
+ const emailRegex = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/gi;
785
+ const emails = rawText.match(emailRegex);
786
+ if (emails && emails.length > 0) {
787
+ const cleanEmail = cleanFieldValue(emails[0], 'email');
788
+ if (cleanEmail) {
789
+ fieldCandidates.email = cleanEmail;
790
+ console.log('Found email:', cleanEmail);
791
+ }
792
+ }
793
+
794
+ // Enhanced phone detection with international support
795
+ const phoneRegex = /(\+?[\d\s\-\(\)]{8,20})/g;
796
+ const phoneMatches = rawText.match(phoneRegex);
797
+ if (phoneMatches) {
798
+ const validPhones = phoneMatches.filter(p => {
799
+ const digits = p.replace(/\D/g, '');
800
+ return digits.length >= 8 && digits.length <= 15;
801
+ });
802
+ if (validPhones.length > 0) {
803
+ const cleanPhone = cleanFieldValue(validPhones[0].trim(), 'phone');
804
+ if (cleanPhone) {
805
+ fieldCandidates.mobile = cleanPhone;
806
+ fieldCandidates.phone = cleanPhone; // Backward compatibility
807
+ console.log('Found phone:', cleanPhone);
808
+ }
809
+ }
810
+ }
811
+
812
+ // Website detection
813
+ const websiteRegex = /((?:https?:\/\/)?(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(?:\.[a-zA-Z]{2,})?)/gi;
814
+ const websites = rawText.match(websiteRegex)?.filter(w => !w.includes('@'));
815
+ if (websites && websites.length > 0) {
816
+ const cleanWebsite = cleanFieldValue(websites[0], 'website');
817
+ if (cleanWebsite) {
818
+ fieldCandidates.website = cleanWebsite;
819
+ console.log('Found website:', cleanWebsite);
820
+ }
821
+ }
822
+
823
+ // Advanced name detection
824
+ const namePatterns = detectNames(lines, fieldCandidates);
825
+ console.log('Name detection candidates:', namePatterns);
826
+ console.log('Lines being processed for names:', lines.map((line, i) => `${i}: "${line}"`));
827
+ if (namePatterns.length > 0) {
828
+ const bestName = namePatterns[0];
829
+ fieldCandidates.firstname = bestName.first;
830
+ fieldCandidates.first_name = bestName.first; // Backward compatibility
831
+ if (bestName.last && bestName.last.trim()) {
832
+ fieldCandidates.lastname = bestName.last;
833
+ fieldCandidates.last_name = bestName.last; // Backward compatibility
834
+ console.log('Selected name:', bestName.first, '|', bestName.last, 'from line:', bestName.originalLine, 'confidence:', bestName.confidence);
835
+ } else {
836
+ console.log('Selected name (first only):', bestName.first, 'from line:', bestName.originalLine, 'confidence:', bestName.confidence);
837
+ console.log('Last name was empty or invalid:', `"${bestName.last}"`);
838
+ }
839
+ } else {
840
+ console.log('No name patterns detected. Lines processed:', lines.map((line, i) => `${i}: "${line}"`));
841
+ }
842
+
843
+ // Position detection with context awareness
844
+ const positionCandidates = detectPosition(lines, fieldCandidates);
845
+ console.log('Position detection candidates:', positionCandidates);
846
+ if (positionCandidates.length > 0) {
847
+ fieldCandidates.position = ucFirstWords(positionCandidates[0].text);
848
+ console.log('Selected position:', fieldCandidates.position, 'confidence:', positionCandidates[0].confidence);
849
+ }
850
+
851
+ // Company detection with improved logic
852
+ const companyCandidates = detectCompany(lines, fieldCandidates);
853
+ console.log('Company detection candidates:', companyCandidates);
854
+ if (companyCandidates.length > 0) {
855
+ fieldCandidates.company = ucFirstWords(companyCandidates[0].text);
856
+ console.log('Selected company:', fieldCandidates.company, 'confidence:', companyCandidates[0].confidence);
857
+ }
858
+
859
+ // Collect unused lines
860
+ const unusedLines = [];
861
+ lines.forEach((line, index) => {
862
+ const trimmedLine = line.trim();
863
+ if (!isLikelyOCRArtifact(trimmedLine) &&
864
+ !Object.values(fieldCandidates).some(value =>
865
+ typeof value === 'string' && value.includes(trimmedLine)
866
+ )) {
867
+ const cleanedLine = cleanFieldValue(trimmedLine, 'general');
868
+ if (cleanedLine && cleanedLine.length > 1) {
869
+ unusedLines.push({
870
+ text: cleanedLine,
871
+ index: index,
872
+ });
873
+ }
874
+ }
875
+ });
876
+
877
+ fieldCandidates._unusedData = unusedLines;
878
+ console.log('Final field candidates:', fieldCandidates);
879
+
880
+ return fieldCandidates;
881
+ };
882
+
883
+ // Enhanced name detection with ML-like scoring
884
+ const detectNames = (lines, existingFields) => {
885
+ const namePatterns = [
886
+ /^[A-Z][a-z]+ [A-Z][a-z]+$/, // John Smith
887
+ /^[A-Z]\. [A-Z][a-z]+$/, // J. Smith
888
+ /^[A-Z][a-z]+ [A-Z]\. [A-Z][a-z]+$/, // John A. Smith
889
+ /^[A-Z][a-z]+$/, // John (single name)
890
+ /^[A-Z][a-z]+ [A-Z][a-z]+ [A-Z][a-z]+$/, // John Middle Smith
891
+ ];
892
+
893
+ const candidates = [];
894
+
895
+ lines.forEach((line, index) => {
896
+ const cleanLine = line.trim();
897
+
898
+ // Skip lines with known field data
899
+ if (existingFields.email && cleanLine.includes(existingFields.email)) return;
900
+ if (existingFields.mobile && cleanLine.includes(existingFields.mobile)) return;
901
+
902
+ // Pre-clean the line to handle artifacts like pipes
903
+ const preCleanedLine = cleanLine.replace(/[|]/g, ' ').replace(/\s+/g, ' ').trim();
904
+
905
+ // Skip obvious artifacts (but check both original and pre-cleaned)
906
+ if (isLikelyOCRArtifact(cleanLine) && isLikelyOCRArtifact(preCleanedLine)) return;
907
+
908
+ // Check against name patterns on both original and pre-cleaned text
909
+ const testLines = [cleanLine, preCleanedLine];
910
+
911
+ testLines.forEach(testLine => {
912
+ namePatterns.forEach(pattern => {
913
+ if (pattern.test(testLine)) {
914
+ const nameWords = testLine.split(' ').filter(w => w.length > 0);
915
+ const confidence = calculateNameConfidence(testLine, index, lines.length);
916
+
917
+ // Lower threshold for better name capture
918
+ if (confidence > 0.2) {
919
+ const rawLast = nameWords.slice(1).join(' ');
920
+ const cleanedFirst = cleanFieldValue(nameWords[0], 'name');
921
+ const cleanedLast = cleanFieldValue(rawLast, 'name');
922
+
923
+ console.log(`Processing name: words=[${nameWords.join(', ')}], rawLast="${rawLast}", cleanedFirst="${cleanedFirst}", cleanedLast="${cleanedLast}"`);
924
+
925
+ if (cleanedFirst) {
926
+ console.log(`Name candidate from "${testLine}": first="${cleanedFirst}", last="${cleanedLast}", confidence=${confidence}`);
927
+ candidates.push({
928
+ first: cleanedFirst,
929
+ last: cleanedLast || '', // Ensure last is always a string
930
+ confidence: confidence,
931
+ index: index,
932
+ originalLine: cleanLine
933
+ });
934
+ }
935
+ }
936
+ }
937
+ });
938
+ });
939
+
940
+ // Special case: Check for names that might not match strict patterns
941
+ // but have strong name characteristics (use pre-cleaned text)
942
+ const nameWords = preCleanedLine.split(' ').filter(w => w.length > 0);
943
+ if (nameWords.length === 2 || nameWords.length === 3) {
944
+ const allProperCase = nameWords.every(word =>
945
+ word.length > 0 &&
946
+ word[0] === word[0].toUpperCase() &&
947
+ word.slice(1) === word.slice(1).toLowerCase() &&
948
+ /^[A-Za-z]+$/.test(word) // Only letters, no symbols
949
+ );
950
+
951
+ if (allProperCase) {
952
+ const confidence = calculateNameConfidence(preCleanedLine, index, lines.length);
953
+ if (confidence > 0.15) { // Even lower threshold for proper case names
954
+ const rawLast = nameWords.slice(1).join(' ');
955
+ const cleanedFirst = cleanFieldValue(nameWords[0], 'name');
956
+ const cleanedLast = cleanFieldValue(rawLast, 'name');
957
+
958
+ console.log(`Processing proper case name: words=[${nameWords.join(', ')}], rawLast="${rawLast}", cleanedFirst="${cleanedFirst}", cleanedLast="${cleanedLast}"`);
959
+
960
+ if (cleanedFirst) {
961
+ console.log(`Proper case name candidate from "${preCleanedLine}": first="${cleanedFirst}", last="${cleanedLast}", confidence=${confidence + 0.1}`);
962
+ candidates.push({
963
+ first: cleanedFirst,
964
+ last: cleanedLast || '', // Ensure last is always a string
965
+ confidence: confidence + 0.1, // Bonus for proper case
966
+ index: index,
967
+ originalLine: cleanLine
968
+ });
969
+ }
970
+ }
971
+ }
972
+ }
973
+ });
974
+
975
+ // Sort by confidence and position (names usually appear early)
976
+ return candidates.sort((a, b) =>
977
+ (b.confidence + (1 - b.index / lines.length) * 0.3) -
978
+ (a.confidence + (1 - a.index / lines.length) * 0.3)
979
+ );
980
+ };
981
+
982
+ // Calculate name confidence based on various factors
983
+ const calculateNameConfidence = (text, index, totalLines) => {
984
+ let confidence = 0.5; // Base confidence
985
+
986
+ // Bonus for being early in the text (names usually come first)
987
+ if (index < totalLines * 0.3) confidence += 0.3;
988
+
989
+ // Bonus for proper capitalization
990
+ const words = text.split(' ');
991
+ const properlyCapitalized = words.every(word =>
992
+ word.length > 0 && word[0] === word[0].toUpperCase() &&
993
+ word.slice(1) === word.slice(1).toLowerCase()
994
+ );
995
+ if (properlyCapitalized) confidence += 0.2;
996
+
997
+ // Penalty for being too long (unlikely to be just a name)
998
+ if (text.length > 30) confidence -= 0.2;
999
+
1000
+ // Bonus for common name lengths
1001
+ if (text.length >= 5 && text.length <= 20) confidence += 0.1;
1002
+
1003
+ return Math.min(1, Math.max(0, confidence));
1004
+ };
1005
+
1006
+ // Position detection with business context
1007
+ const detectPosition = (lines, existingFields) => {
1008
+ const positionKeywords = [
1009
+ 'director', 'manager', 'ceo', 'cto', 'cfo', 'president', 'vice president',
1010
+ 'senior', 'lead', 'head', 'chief', 'supervisor', 'coordinator',
1011
+ 'specialist', 'analyst', 'engineer', 'developer', 'designer',
1012
+ 'consultant', 'sales', 'marketing', 'lawyer', 'attorney', 'practitioner',
1013
+ 'partner', 'associate', 'executive', 'officer', 'founder', 'owner',
1014
+ 'legal practitioner' // Added for the specific example
1015
+ ];
1016
+
1017
+ const candidates = [];
1018
+
1019
+ lines.forEach((line, index) => {
1020
+ const cleanLine = line.trim();
1021
+ const lowerLine = cleanLine.toLowerCase();
1022
+
1023
+ // Skip if line contains email/phone
1024
+ if (existingFields.email && cleanLine.includes(existingFields.email)) return;
1025
+ if (existingFields.mobile && cleanLine.includes(existingFields.mobile)) return;
1026
+
1027
+ // Pre-clean the line to handle artifacts like pipes
1028
+ const preCleanedLine = cleanLine.replace(/[|]/g, ' ').replace(/\s+/g, ' ').trim();
1029
+ const preCleanedLower = preCleanedLine.toLowerCase();
1030
+
1031
+ // Calculate keyword score on both original and pre-cleaned text
1032
+ let keywordScore = 0;
1033
+ let matchedKeywords = 0;
1034
+
1035
+ positionKeywords.forEach(keyword => {
1036
+ if (lowerLine.includes(keyword) || preCleanedLower.includes(keyword)) {
1037
+ keywordScore += keyword.length / Math.max(cleanLine.length, preCleanedLine.length);
1038
+ matchedKeywords++;
1039
+ }
1040
+ });
1041
+
1042
+ // Bonus for multiple keyword matches
1043
+ if (matchedKeywords > 1) keywordScore *= 1.5;
1044
+
1045
+ if (keywordScore > 0.1) {
1046
+ const cleanedText = cleanFieldValue(preCleanedLine, 'position');
1047
+ if (cleanedText && cleanedText.length > 2) {
1048
+ candidates.push({
1049
+ text: cleanedText,
1050
+ confidence: Math.min(0.95, keywordScore * 2),
1051
+ index: index
1052
+ });
1053
+ }
1054
+ }
1055
+ });
1056
+
1057
+ return candidates.sort((a, b) => b.confidence - a.confidence);
1058
+ };
1059
+
1060
+ // Company detection with improved logic
1061
+ const detectCompany = (lines, existingFields) => {
1062
+ const companyIndicators = [
1063
+ 'inc', 'corp', 'ltd', 'llc', 'company', 'co.', 'corporation',
1064
+ 'incorporated', 'limited', 'enterprises', 'group', 'solutions',
1065
+ 'services', 'systems', 'technologies', 'tech', 'consulting',
1066
+ 'law', 'legal', 'firm', 'associates', 'partners', 'curae' // Added for the specific example
1067
+ ];
1068
+
1069
+ const candidates = [];
1070
+
1071
+ lines.forEach((line, index) => {
1072
+ const cleanLine = line.trim();
1073
+ const lowerLine = cleanLine.toLowerCase();
1074
+
1075
+ // Skip known fields
1076
+ if (existingFields.email && cleanLine.includes(existingFields.email)) return;
1077
+ if (existingFields.mobile && cleanLine.includes(existingFields.mobile)) return;
1078
+ if (existingFields.firstname && cleanLine.includes(existingFields.firstname)) return;
1079
+
1080
+ // Pre-clean the line to handle artifacts like pipes
1081
+ const preCleanedLine = cleanLine.replace(/[|]/g, ' ').replace(/\s+/g, ' ').trim();
1082
+ const preCleanedLower = preCleanedLine.toLowerCase();
1083
+
1084
+ const hasCompanyIndicator = companyIndicators.some(
1085
+ indicator => lowerLine.includes(indicator) || preCleanedLower.includes(indicator)
1086
+ );
1087
+ const isAllCaps = cleanLine === cleanLine.toUpperCase() && cleanLine.length > 2;
1088
+ const cleanedWords = preCleanedLine.split(/\s+/);
1089
+ const letterRatio = (preCleanedLine.match(/[a-zA-Z]/g) || []).length / preCleanedLine.length;
1090
+
1091
+ let confidence = 0;
1092
+
1093
+ // High confidence for company indicators
1094
+ if (hasCompanyIndicator) confidence += 0.7;
1095
+
1096
+ // Moderate confidence for all caps (common for company names)
1097
+ if (isAllCaps) confidence += 0.4;
1098
+
1099
+ // Bonus for reasonable length and structure
1100
+ if (letterRatio > 0.6 && preCleanedLine.length >= 3 && preCleanedLine.length <= 40) {
1101
+ confidence += 0.2;
1102
+ }
1103
+
1104
+ // Bonus for being positioned after name/position
1105
+ if (index > 0 && index < lines.length * 0.7) confidence += 0.1;
1106
+
1107
+ if (confidence > 0.3) {
1108
+ const cleanedText = cleanFieldValue(preCleanedLine, 'company');
1109
+ if (cleanedText && cleanedText.length > 2) {
1110
+ candidates.push({
1111
+ text: cleanedText,
1112
+ confidence: Math.min(0.95, confidence),
1113
+ index: index
1114
+ });
1115
+ }
1116
+ }
1117
+ });
1118
+
1119
+ return candidates.sort((a, b) => b.confidence - a.confidence);
1120
+ };
1121
+
571
1122
  const parseExtractedText = (text) => {
1123
+ // Use the new intelligent field mapping system
1124
+ return intelligentFieldMapping(text);
1125
+ };
1126
+
1127
+ const parseExtractedTextLegacy = (text) => {
572
1128
  const lines = text.split('\n').filter((line) => line.trim());
573
1129
  const parsed = {};
574
1130
  const unusedLines = [];
@@ -831,6 +1387,8 @@ const BusinessCardOcr = ({
831
1387
  return parsed;
832
1388
  };
833
1389
 
1390
+ // parseExtractedText now uses intelligent mapping above
1391
+
834
1392
  // Helper function to get field input type
835
1393
  const getFieldInputType = (fieldId) => {
836
1394
  const fieldTypeMap = {
@@ -849,14 +1407,14 @@ const BusinessCardOcr = ({
849
1407
  // Helper function to get field icon
850
1408
  const getFieldIcon = (fieldId) => {
851
1409
  const iconMap = {
852
- email: <Envelope size={16} />,
1410
+ email: <Mail size={16} />,
853
1411
  mobile: null,
854
1412
  phone: null,
855
- website: <Network size={16} />,
1413
+ website: <Globe size={16} />,
856
1414
  position: null,
857
- company: <Network size={16} />,
858
- firstname: <Edit size={16} />,
859
- lastname: <Edit size={16} />,
1415
+ company: <Building2 size={16} />,
1416
+ firstname: <User size={16} />,
1417
+ lastname: <User size={16} />,
860
1418
  };
861
1419
  return iconMap[fieldId];
862
1420
  };
@@ -1086,7 +1644,7 @@ const BusinessCardOcr = ({
1086
1644
  onClick={handleClose}
1087
1645
  className={ocrStyles.closeButton}
1088
1646
  >
1089
- <CircleX size={24} />
1647
+ <X size={24} />
1090
1648
  </button>
1091
1649
  </div>
1092
1650
 
@@ -1094,7 +1652,7 @@ const BusinessCardOcr = ({
1094
1652
  {currentStep === 'camera' && (
1095
1653
  <div className={ocrStyles.cameraContainer}>
1096
1654
  <div className={ocrStyles.captureGuide}>
1097
- <p>Position business card within the frame</p>
1655
+ <p>Position business card within the frame and tap to focus</p>
1098
1656
  </div>
1099
1657
  <div className={ocrStyles.videoWrapper}>
1100
1658
  <video
@@ -1103,45 +1661,73 @@ const BusinessCardOcr = ({
1103
1661
  playsInline
1104
1662
  muted
1105
1663
  className={ocrStyles.cameraVideo}
1664
+ onClick={triggerAutofocus}
1106
1665
  />
1107
- <div className={ocrStyles.cameraOverlay}></div>
1666
+ {/* Enhanced camera overlay with focus frame */}
1667
+ <div className={ocrStyles.cameraOverlay}>
1668
+ <div className={ocrStyles.focusFrame}>
1669
+ {isFocusing && (
1670
+ <div className={ocrStyles.focusIndicator}>
1671
+ <Focus className={ocrStyles.focusIcon} />
1672
+ <span>Focusing...</span>
1673
+ </div>
1674
+ )}
1675
+ </div>
1676
+ </div>
1108
1677
  </div>
1109
1678
 
1110
1679
  <div className={ocrStyles.cameraControls}>
1111
- {availableCameras.length > 1 && (
1680
+ <div className={ocrStyles.controlsTop}>
1681
+ {availableCameras.length > 1 && (
1682
+ <button
1683
+ onClick={switchCamera}
1684
+ className={ocrStyles.controlButton}
1685
+ title="Switch Camera"
1686
+ >
1687
+ <SwitchCamera size={20} />
1688
+ </button>
1689
+ )}
1690
+
1691
+ {/* Flash toggle button */}
1692
+ <button
1693
+ onClick={toggleFlash}
1694
+ className={`${ocrStyles.controlButton} ${flashEnabled ? ocrStyles.active : ''}`}
1695
+ title={flashEnabled ? 'Turn off flash' : 'Turn on flash'}
1696
+ >
1697
+ {flashEnabled ? <Zap size={20} /> : <ZapOff size={20} />}
1698
+ </button>
1699
+ </div>
1700
+
1701
+ <div className={ocrStyles.controlsBottom}>
1112
1702
  <button
1113
- onClick={switchCamera}
1703
+ onClick={() =>
1704
+ fileInputRef.current?.click()
1705
+ }
1114
1706
  className={ocrStyles.secondaryButton}
1115
1707
  >
1116
- <ArrowCycle size={20} />
1117
- Switch Camera
1708
+ <ImagePlus size={20} />
1709
+ Upload
1118
1710
  </button>
1119
- )}
1120
1711
 
1121
- <button
1122
- onClick={capturePhoto}
1123
- className={ocrStyles.captureButton}
1124
- >
1125
- <Camera size={24} />
1126
- Capture
1127
- </button>
1712
+ <button
1713
+ onClick={capturePhoto}
1714
+ className={`${ocrStyles.captureButton} ${isFocusing ? ocrStyles.focusing : ''}`}
1715
+ disabled={isFocusing}
1716
+ >
1717
+ <Camera size={28} />
1718
+ {isFocusing ? 'Focusing...' : 'Capture'}
1719
+ </button>
1128
1720
 
1129
- <input
1130
- type="file"
1131
- ref={fileInputRef}
1132
- onChange={handleFileUpload}
1133
- accept="image/jpeg,image/png,image/jpg,image/gif,image/webp"
1134
- style={{ display: 'none' }}
1135
- />
1136
- <button
1137
- onClick={() =>
1138
- fileInputRef.current?.click()
1139
- }
1140
- className={ocrStyles.secondaryButton}
1141
- >
1142
- <ImageIcon size={24} />
1143
- Upload Image
1144
- </button>
1721
+ <input
1722
+ type="file"
1723
+ ref={fileInputRef}
1724
+ onChange={handleFileUpload}
1725
+ accept="image/jpeg,image/png,image/jpg,image/gif,image/webp"
1726
+ style={{ display: 'none' }}
1727
+ />
1728
+
1729
+ <div className={ocrStyles.placeholder}></div>
1730
+ </div>
1145
1731
  </div>
1146
1732
  </div>
1147
1733
  )}
@@ -1161,7 +1747,7 @@ const BusinessCardOcr = ({
1161
1747
  onClick={retakePhoto}
1162
1748
  className={ocrStyles.secondaryButton}
1163
1749
  >
1164
- <ArrowCounterClockwise size={20} />
1750
+ <RotateCcw size={20} />
1165
1751
  Retake
1166
1752
  </button>
1167
1753
 
@@ -1170,7 +1756,7 @@ const BusinessCardOcr = ({
1170
1756
  disabled={isProcessing}
1171
1757
  className={ocrStyles.primaryButton}
1172
1758
  >
1173
- <CircleCheck size={20} />
1759
+ <CheckCircle size={20} />
1174
1760
  Process Card
1175
1761
  </button>
1176
1762
  </div>
@@ -1231,7 +1817,7 @@ const BusinessCardOcr = ({
1231
1817
  <>
1232
1818
  <div className={ocrStyles.fieldGroup}>
1233
1819
  <label>
1234
- <Edit size={16} />
1820
+ <User size={16} />
1235
1821
  First Name
1236
1822
  </label>
1237
1823
  <input
@@ -1252,7 +1838,7 @@ const BusinessCardOcr = ({
1252
1838
  </div>
1253
1839
  <div className={ocrStyles.fieldGroup}>
1254
1840
  <label>
1255
- <Edit size={16} />
1841
+ <User size={16} />
1256
1842
  Last Name
1257
1843
  </label>
1258
1844
  <input
@@ -1273,7 +1859,7 @@ const BusinessCardOcr = ({
1273
1859
  </div>
1274
1860
  <div className={ocrStyles.fieldGroup}>
1275
1861
  <label>
1276
- <Envelope size={16} />
1862
+ <Mail size={16} />
1277
1863
  Email
1278
1864
  </label>
1279
1865
  <input
@@ -1310,7 +1896,7 @@ const BusinessCardOcr = ({
1310
1896
  </div>
1311
1897
  <div className={ocrStyles.fieldGroup}>
1312
1898
  <label>
1313
- <Network size={16} />
1899
+ <Building2 size={16} />
1314
1900
  Company
1315
1901
  </label>
1316
1902
  <input
@@ -1439,7 +2025,7 @@ const BusinessCardOcr = ({
1439
2025
  onClick={retakePhoto}
1440
2026
  className={ocrStyles.secondaryButton}
1441
2027
  >
1442
- <ImageIcon size={20} />
2028
+ <ImagePlus size={20} />
1443
2029
  Scan Another
1444
2030
  </button>
1445
2031