import React, { useState, useCallback, useRef, useMemo } from 'react';
// --- Global Constants and Utilities ---
const API_KEY = "";
const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${API_KEY}`;
const coralColors = {
light: '#FEECE7',
DEFAULT: '#E76E55', // The primary coral color
dark: '#D45A4A',
};
// Utility to convert File to Base64
const fileToBase64 = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
// Always read the file as an ArrayBuffer/DataURL for robust handling of binary files (like PDF)
// This is necessary because PDFs cannot be reliably read as text.
reader.readAsDataURL(file);
reader.onload = () => {
// Extract the Base64 data part (after the comma in the data URL)
// Example: "data:application/pdf;base64,JVBERi0xLjc..." -> "JVBERi0xLjc..."
resolve(reader.result.split(',')[1]);
};
reader.onerror = error => reject(error);
});
};
// Utility for API call with exponential backoff
const fetchWithBackoff = async (url, options, maxRetries = 5, delay = 1000) => {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.status === 429 && i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2;
continue;
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2;
}
}
};
// --- API Schemas and Prompts ---
const responseSchemaOCR = {
type: "OBJECT",
properties: {
physicalId: {
type: "STRING",
// UPDATED: Removed hyphen restriction. Retained length minimum.
description: "The unique Physical ID extracted from the image. MUST be more than 4 characters long."
},
taskId: {
type: "STRING",
description: "A simulated task ID that this reel is being associated with (e.g., TSK-1093)."
},
confidence: {
type: "NUMBER",
description: "Confidence level of the OCR extraction, ranging from 0.0 to 1.0."
}
},
required: ["physicalId", "taskId", "confidence"]
};
const responseSchemaInvoice = {
type: "OBJECT",
properties: {
extractedItems: {
type: "ARRAY",
description: "An array of extracted fiber asset items, each containing a Physical ID, Type, Quantity, and Status.",
items: {
type: "OBJECT",
properties: {
physicalId: { type: "STRING", description: "The unique Physical ID." },
type: { type: "STRING", description: "The material type, e.g., '12F', '24F', '48F'." },
quantity: { type: "STRING", description: "The length or quantity as a string (e.g., '100', '123', '10000')." },
status: { type: "STRING", description: "The status, e.g., 'live' or 'expended'." }
},
required: ["physicalId", "type", "quantity", "status"]
}
}
},
required: ["extractedItems"]
};
// --- Placeholder Component for Task Form Fields (Mobile optimized) ---
const TaskField = ({ label, type = 'text', options, fieldNumber, value, onChange }) => {
const isDropdown = type === 'dropdown' && options;
const isInput = type === 'text';
// Determine props for dropdown: if value/onChange are provided, use them.
// Otherwise, let the element be uncontrolled (or default to empty string value)
const dropdownProps = isDropdown && value !== undefined && onChange
? { value, onChange }
: { defaultValue: value || '' }; // Use defaultValue for non-controlled inputs
return (
{isInput && (
)}
{isDropdown && (
)}
);
};
// --- Component Definitions ---
export default function App() {
// Set 'invoice' as the new default active tab
const [activeTab, setActiveTab] = useState('invoice');
// State to manage which fields are visible in the Task Form
const [selectedWorkActivity, setSelectedWorkActivity] = useState('AERIAL Cable Installation');
// NEW state for manual entry toggle
const [isManualEntry, setIsManualEntry] = useState(false);
// NEW state to hold the manually or extracted Physical ID value
const [physicalIdValue, setPhysicalIdValue] = useState('');
// NEW state for copy button feedback
const [copyStatus, setCopyStatus] = useState(null);
// State for Function 1: OCR/Scan (used in taskForm)
const [ocrState, setOcrState] = useState({
file: null,
preview: 'https://placehold.co/400x300/f3f4f6/333333?text=No+Image+Selected',
result: null,
loading: false,
error: null
});
const ocrInputRef = useRef(null);
// NEW State for the File Upload Field in Task Form (Field 23)
const [formFileState, setFormFileState] = useState({
file: null,
fileName: 'No file attached',
dropzoneStyle: {
borderColor: coralColors.light,
backgroundColor: 'white',
}
});
const formFileInputRef = useRef(null);
const formFileDropzoneRef = useRef(null);
// State for Function 2: Invoice Parsing (used in invoice tab)
const [invoiceState, setInvoiceState] = useState({
file: null,
fileName: 'No file selected',
items: [],
loading: false,
error: null
});
const csvInputRef = useRef(null);
const dropzoneRef = useRef(null);
// --- Handlers for Function 1: OCR / Scan ---
const handleOcrFileChange = useCallback((e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
setOcrState(prev => ({
...prev,
file: file,
preview: e.target.result,
error: null,
result: null
}));
// Reset manual entry on new file selection
setIsManualEntry(false);
};
reader.readAsDataURL(file);
}
e.target.value = null;
}, []);
const processReelImage = useCallback(async () => {
if (!ocrState.file) {
setOcrState(prev => ({ ...prev, error: `Please capture or upload an image file.` }));
return;
}
// Reset manual mode if processing starts
setIsManualEntry(false);
setOcrState(prev => ({ ...prev, loading: true, error: null, result: null }));
try {
// fileToBase64 handles reading file content for both image and text.
const fileContent = await fileToBase64(ocrState.file);
const mimeType = ocrState.file.type;
// Use the base64 content for image analysis
const base64Image = fileContent;
// UPDATED SYSTEM PROMPT: Added instruction to prioritize largest text/barcode
const systemPrompt = "Act as a high-precision OCR and asset management system. Analyze the provided image of a fiber optic reel, extract a unique Physical ID (or Reel Identification Number) from the image, and associate it with a simulated task ID. If the sticker contains multiple identifiers, **prioritize the largest and most prominent text/barcode, typically found near the bottom of the sticker**, as the CABLE BATCH NUMBER. The extracted Physical ID MUST be more than 4 characters long. Generate the output as a JSON object based on the required schema.";
const userQuery = "This image was either uploaded or captured in the field. Extract the Physical ID (CABLE BATCH NUMBER) and associated Task ID, focusing on identifying barcode data.";
const payload = {
contents: [
{
role: "user",
parts: [
{ text: userQuery },
{ inlineData: { mimeType: mimeType, data: base64Image } }
]
}
],
systemInstruction: { parts: [{ text: systemPrompt }] },
generationConfig: {
responseMimeType: "application/json",
responseSchema: responseSchemaOCR
}
};
const response = await fetchWithBackoff(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
const jsonText = result?.candidates?.[0]?.content?.parts?.[0]?.text;
if (!jsonText) {
throw new Error("API response was missing expected JSON content.");
}
const data = JSON.parse(jsonText);
setOcrState(prev => ({ ...prev, result: data, loading: false }));
setPhysicalIdValue(data.physicalId); // Update the primary value state
} catch (error) {
console.error("OCR API Error:", error);
setOcrState(prev => ({ ...prev, error: `Processing failed: ${error.message}`, loading: false }));
// Offer manual entry on failure
setIsManualEntry(true);
setPhysicalIdValue('');
}
}, [ocrState.file]);
// Handler to copy the physical ID to the clipboard
const copyPhysicalId = useCallback(() => {
// Use document.execCommand('copy') for guaranteed function in sandbox environments
const tempElement = document.createElement('textarea');
tempElement.value = physicalIdValue;
document.body.appendChild(tempElement);
tempElement.select();
document.execCommand('copy');
document.body.removeChild(tempElement);
setCopyStatus('Copied!');
setTimeout(() => setCopyStatus(null), 1500);
}, [physicalIdValue]);
// --- Handlers for Function 2: Invoice Parsing (CSV/PDF) ---
const handleInvoiceFileSelected = useCallback((file) => {
if (file) {
if (!['text/csv', 'application/pdf'].includes(file.type) && !file.name.endsWith('.csv') && !file.name.endsWith('.pdf')) {
setInvoiceState(prev => ({ ...prev, error: 'Unsupported file type. Please upload a CSV or PDF file.', file: null, fileName: 'No file selected', items: [] }));
return;
}
setInvoiceState(prev => ({ ...prev, file: file, fileName: file.name, error: null, items: [] }));
} else {
setInvoiceState(prev => ({ ...prev, file: null, fileName: 'No file selected', items: [] }));
}
}, []);
const handleDrop = useCallback((e) => {
e.preventDefault();
dropzoneRef.current.style.borderColor = coralColors.light;
dropzoneRef.current.style.backgroundColor = 'white';
const file = e.dataTransfer.files[0];
handleInvoiceFileSelected(file);
}, [handleInvoiceFileSelected]);
const handleDragOver = useCallback((e) => {
e.preventDefault();
dropzoneRef.current.style.borderColor = coralColors.DEFAULT;
dropzoneRef.current.style.backgroundColor = coralColors.light;
}, []);
const handleDragLeave = useCallback((e) => {
e.preventDefault();
dropzoneRef.current.style.borderColor = coralColors.light;
dropzoneRef.current.style.backgroundColor = 'white';
}, []);
const processInvoiceFile = useCallback(async () => {
if (!invoiceState.file) {
setInvoiceState(prev => ({ ...prev, error: `Please upload a CSV or PDF file.` }));
return;
}
setInvoiceState(prev => ({ ...prev, loading: true, error: null, items: [] }));
try {
const file = invoiceState.file;
const mimeType = file.type;
const fileContentBase64 = await fileToBase64(file);
const systemPrompt = `Act as an invoice parsing system. Analyze the provided document and extract all line items related to fiber reels. The output MUST strictly conform to the provided JSON schema. Specifically extract 'Physical ID', 'Type', 'Quantity', and 'Status' (assume 'live' if not specified). The input might be raw CSV text or the content of a PDF invoice.`;
// For CSV, we can still use the Base64 content as the text part since it often decodes cleanly.
// For PDF, we rely purely on inlineData.
const userQuery = `Process this document (${file.name}) to extract the Physical ID, Type, Quantity, and Status for all fiber assets.`;
const contents = [
{ role: "user", parts: [{ text: userQuery }] }
];
// If it's CSV, try including the text content in the text part as well (optional, but good for clarity).
// For the API to handle PDF, we must use inlineData with the base64 content.
contents[0].parts.push({ inlineData: { mimeType: mimeType, data: fileContentBase64 } });
const payload = {
contents: contents,
systemInstruction: { parts: [{ text: systemPrompt }] },
generationConfig: {
responseMimeType: "application/json",
responseSchema: responseSchemaInvoice
}
};
const response = await fetchWithBackoff(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
const jsonText = result?.candidates?.[0]?.content?.parts?.[0]?.text;
if (!jsonText) {
throw new Error("API response was missing expected JSON content. Check the input data format or content.");
}
const data = JSON.parse(jsonText);
setInvoiceState(prev => ({ ...prev, items: data.extractedItems, loading: false }));
} catch (error) {
console.error("Invoice API Error:", error);
setInvoiceState(prev => ({ ...prev, error: `Parsing failed. Please check file format and content. Error: ${error.message}`, loading: false }));
}
}, [invoiceState.file]);
// --- Handlers for Mobile File Upload (Field 23) ---
const handleFormFileSelected = useCallback((file) => {
if (file) {
setFormFileState(prev => ({ ...prev, file: file, fileName: file.name }));
} else {
setFormFileState(prev => ({ ...prev, file: null, fileName: 'No file attached' }));
}
}, []);
const handleFormDrop = useCallback((e) => {
e.preventDefault();
// Reset dropzone style after drop
setFormFileState(prev => ({ ...prev, dropzoneStyle: { borderColor: coralColors.light, backgroundColor: 'white' } }));
const file = e.dataTransfer.files[0];
handleFormFileSelected(file);
}, [handleFormFileSelected]);
const handleFormDragOver = useCallback((e) => {
e.preventDefault();
// Apply hover style
setFormFileState(prev => ({ ...prev, dropzoneStyle: { borderColor: coralColors.DEFAULT, backgroundColor: coralColors.light } }));
}, []);
const handleFormDragLeave = useCallback((e) => {
e.preventDefault();
// Reset style
setFormFileState(prev => ({ ...prev, dropzoneStyle: { borderColor: coralColors.light, backgroundColor: 'white' } }));
}, []);
// --- Derived Display Logic (OCR) ---
const OcrResultDisplay = useMemo(() => {
const { result, error, loading } = ocrState;
if (loading) {
return
Processing image data...
;
}
if (error) {
// FIX: Ensure this returns a single component structure
return (
Processing failed. Try manual entry below.
);
}
if (result) {
// Apply the extracted Physical ID directly to the simulated form field
const confidenceColor = result.confidence >= 0.8 ? 'text-green-600' : result.confidence >= 0.5 ? 'text-yellow-600' : 'text-red-600';
// This is the simulated result display, which is integrated directly into the form
return (
Physical ID
{result.physicalId}
Associated Task
{result.taskId}
Confidence Score
{/* FIX: Correct syntax for injecting confidenceColor class */}
);
// AI Component replacing the Cable BATCH NUMBER dropdown
const renderCableBatchNumberField = (fieldNumber) => {
// Retain the actual field name "Cable BATCH NUMBER" as the title
const title = 'Cable BATCH NUMBER';
const description = 'AI Automation: Tap the button to use your camera for **barcode scanning** or select an image from your gallery.';
const placeholderText = 'Tap to Scan Barcode or Upload Photo';
// This input accepts both file selection and triggers camera capture on mobile devices
const inputAccept = 'image/*;capture=camera';
const inputID = 'mergedInput';
// Custom Capture Button component to replace the standard file input
const CaptureButton = (
);
return (
// Note: This component acts as the visual replacement for the "Cable BATCH NUMBER" input field
{description}
{/* --- Primary Capture/Scan Interface --- */}
{/* Hidden File Input for Triggering Camera/Gallery */}
{/* Manual Input Toggle: Show Capture button/interface only if not in manual mode */}
{!isManualEntry && (
<>
{CaptureButton}
>
)}
{/* --- Manual Entry or AI Result Display --- */}
{isManualEntry || ocrState.result || ocrState.error ? (
);
}
// NEW Component for mobile file upload/dropzone (Field 23)
const renderMobileFileUploadField = (fieldNumber) => {
const title = 'Drag file here or [SEARCH FILE]';
return (
{allFields.map((field, index) => {
const isAlwaysVisible = field.isAlwaysVisible;
// Dynamic visibility: show if always visible OR if work activity is selected
const isVisible = isAlwaysVisible || (selectedWorkActivity);
if (!isVisible) return null;
if (field.type === 'header') {
// Headers are unnumbered and separate visual sections
return ;
}
// Handle WORK ACTIVITY field specially (always visible, handles dynamic visibility logic)
if (field.isWorkActivity) {
const fieldNum = currentFieldNumber++;
return (
setSelectedWorkActivity(e.target.value)}
/>
);
}
if (field.type === 'ai-scan') {
// This is the AI automation field, passing the current number
const fieldNum = currentFieldNumber++;
return
{renderCableBatchNumberField(fieldNum)}
;
}
if (field.type === 'file-upload') {
// This is the file Upload field
const fieldNum = currentFieldNumber++;
return
{renderMobileFileUploadField(fieldNum)}
;
}
// Render standard field and increment the counter.
// These fields do NOT need value/onChange as they are just placeholders.
return (
);
})}