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 */}

{(result.confidence * 100).toFixed(1)}%

Physical ID successfully recorded!

); } return ( <>

Scan to auto-populate Physical ID field.

); }, [ocrState, setIsManualEntry]); // --- Render Functions --- // Custom Component for Header Fields (bold text, no input) const FormHeader = ({ label }) => (

{label}


); // 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 ? (

Physical ID Entry

setPhysicalIdValue(e.target.value)} className="w-full p-3 pr-16 border border-gray-400 rounded-lg focus:ring-1 focus:ring-opacity-50 font-bold text-lg" style={{ borderColor: coralColors.light, focusRingColor: coralColors.DEFAULT, borderWidth: '2px' }} /> {/* Copy Button functionality */} {physicalIdValue && ( )}
{isManualEntry && !ocrState.loading && ( )}
) : (

Physical ID Entry

Awaiting scan or manual entry...

)} {/* --- Capture Preview and Results --- */}

Capture Preview

{ocrState.file ? ( Captured or Uploaded Image Preview { e.target.onerror = null; e.target.src = 'https://placehold.co/400x300/f3f4f6/333333?text=Error+Loading+Image'; }} /> ) : (

No Image Selected

)}

Extraction Status

{OcrResultDisplay}
{ocrState.error && !isManualEntry &&

AI failed. Check result status above.

}
); } // NEW Component for mobile file upload/dropzone (Field 23) const renderMobileFileUploadField = (fieldNumber) => { const title = 'Drag file here or [SEARCH FILE]'; return (
{/* File Upload/Drag & Drop Area */}
formFileInputRef.current.click()} style={{ ...formFileState.dropzoneStyle, transition: 'all 0.2s', borderColor: formFileState.dropzoneStyle.borderColor }} className="border-2 border-dashed rounded-xl p-6 text-center cursor-pointer" > handleFormFileSelected(e.target.files[0])} />

Tap or Drag File Here

Attached File: {formFileState.fileName}

{/* Note: In a real app, this file would be attached to the form submission */}
); }; // Function to map and render all task fields const renderTaskForm = () => { // Define all form fields based on the latest table provided const allFields = [ // Always Visible Fields { label: 'WORK ACTIVITY', type: 'dropdown', options: ['AERIAL Cable Installation', 'UNDERGROUND Cable Installation', 'Other'], isAlwaysVisible: true, isWorkActivity: true }, { label: 'CONSTRUCTION NOTES', type: 'text', isAlwaysVisible: true }, // Dynamically Visible Fields (Show when WORK ACTIVITY is selected) { label: 'Size', type: 'text' }, { label: 'CABLE SIZE', type: 'dropdown', options: ['12F', '24F', '48F', '96F'] }, { label: 'MATERIALS', type: 'header' }, { label: 'CABLE BATCH NUMBER', type: 'ai-scan' }, // Special AI component (Field 4) { label: 'START Footage Mark', type: 'text' }, { label: 'END Footage Mark', type: 'text' }, { label: 'LENGTH OF AERIAL Cable Placed (ft)', type: 'text' }, { label: 'SINGLE DEAD ENDS Installed', type: 'text' }, { label: 'DOUBLE DEAD ENDS Installed', type: 'text' }, { label: 'SINGLE TANGENTS Installed', type: 'text' }, { label: 'DOUBLE TANGENTS Installed', type: 'text' }, { label: 'EXTENSION ARMS Installed', type: 'text' }, { label: 'VIBRATION DAMPERS Installed', type: 'dropdown', options: ['Yes', 'No'] }, { label: 'Installed Transition', type: 'dropdown', options: ['Yes', 'No'] }, { label: 'SLACKLOOPS INSTALLED (MID ROUTE)', type: 'dropdown', options: ['0', '1', '2', '3'] }, { label: 'Number of Snow Shoe Pairs Installed', type: 'dropdown', options: ['0', '1', '2'] }, { label: 'End of Run (Cable Cut)', type: 'dropdown', options: ['Yes', 'No'] }, { label: 'COMPLETION', type: 'header' }, { label: 'Labels Installed', type: 'dropdown', options: ['Yes', 'No'] }, { label: 'Road Crossings', type: 'dropdown', options: ['Yes', 'No'] }, { label: 'Is this haul on a State Highway?', type: 'dropdown', options: ['Yes', 'No'] }, { label: 'Is REARRANGEMENT Required', type: 'dropdown', options: ['Yes', 'No'] }, // Always Visible Fields (Photos section) { label: 'Photos', type: 'header', isAlwaysVisible: true }, { label: 'Other', type: 'dropdown', options: ['Yes', 'No'], isAlwaysVisible: true }, { label: 'Drag file here or [SEARCH FILE]', type: 'file-upload', isAlwaysVisible: true }, // Field 23: File Upload Component ]; let currentFieldNumber = 1; return (

Mobile Field Task Simulation

{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 ( ); })}
); } const renderFunction2 = () => (

Function 2: Invoice Data Parsing (Web App)

1. Upload Invoice File (CSV or PDF)

{/* File Upload/Drag & Drop Area */}
csvInputRef.current.click()} style={{ borderColor: coralColors.light, transition: 'all 0.2s', backgroundColor: 'white' }} className="border-2 border-dashed rounded-xl p-8 text-center cursor-pointer mb-6" > handleInvoiceFileSelected(e.target.files[0])} />

Drag & Drop CSV or PDF Invoice here, or click to browse

Current File: {invoiceState.fileName}

{invoiceState.error &&

{invoiceState.error}

} {/* Extracted Results Table */} {invoiceState.items.length > 0 && (

2. Extracted Line Items

{['Physical ID', 'Type', 'Quantity', 'Status', 'Retire'].map(header => ( ))} {invoiceState.items.map((item, index) => ( ))}
{header}
{item.physicalId || 'N/A'} {item.type || 'N/A'} {item.quantity || 'N/A'} {item.status || 'N/A'}
)}
); return ( <>
{/* Main Application Header */}

Fiber Asset Automation Engine

Unified Proof of Concept for Field & Office Workflows

{/* Tab Navigation - ORDER SWITCHED */}
{/* 1. Office Web App (Invoice Data) */} {/* 2. Field App (Task Form) */}
{/* Content Rendering - Order matched to tabs */} {activeTab === 'invoice' && renderFunction2()} {activeTab === 'taskForm' && renderTaskForm()}

Note: AI processing simulation is powered by Gemini.

); }