import React, { useState, useRef, useEffect } from 'react'; import { Send, Bot, User, Database } from 'lucide-react'; const ManningChatbot = () => { const [messages, setMessages] = useState([ { role: 'assistant', content: 'Hello! I\'m your Manning Schedule Assistant. Send me a manning schedule and ask me questions about who\'s working, their roles, or any other details!\n\nYou can also update the personnel database by sending an updated NRIC list.' } ]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [pendingUpdate, setPendingUpdate] = useState(null); const messagesEndRef = useRef(null); const [personnelData, setPersonnelData] = useState({ // Paramedics 'Christina': { rank: 'WO2', nric: 'S-953J', role: 'Paramedic' }, 'Joseph': { rank: 'WO1', nric: 'S-206Z', role: 'Paramedic' }, 'Rayhan': { rank: 'SGT', nric: 'S-160F', role: 'Paramedic' }, 'Haris': { rank: 'SGT', nric: 'S-776H', role: 'Paramedic' }, 'Elinda': { rank: 'SGT', nric: 'S-544F', role: 'Paramedic' }, 'Pearlyn': { rank: 'SGT', nric: 'T-628C', role: 'Paramedic' }, 'Ridhuan': { rank: 'SGT', nric: 'T-353F', role: 'Paramedic' }, 'Syahidin': { rank: 'SGT', nric: 'S-340J', role: 'Paramedic' }, 'Shafiq': { rank: 'SGT', nric: 'S-837Z', role: 'Paramedic' }, 'Dini Danial': { rank: 'SGT', nric: 'S-832E', role: 'Paramedic' }, 'Emily': { rank: 'SGT', nric: 'S-365A', role: 'Paramedic' }, 'Willie': { rank: 'SGT', nric: 'S-353G', role: 'Paramedic' }, 'Azli': { rank: 'SSG', nric: 'S-941G', role: 'Paramedic' }, 'Ruqniah': { rank: 'SGT', nric: 'S-671A', role: 'Paramedic' }, 'Haikal': { rank: 'SGT', nric: 'S-063F', role: 'Paramedic' }, 'Claris': { rank: 'SGT', nric: 'T-606F', role: 'Paramedic' }, 'Yuxiu': { rank: 'SGT', nric: 'S-679F', role: 'Paramedic' }, 'Patrick': { rank: 'WO2', nric: 'S-138H', role: 'Paramedic' }, 'Haziq': { rank: 'SGT', nric: 'S-108F', role: 'Paramedic' }, 'Nadia': { rank: 'SGT', nric: 'T-259B', role: 'Paramedic' }, 'Indrawan': { rank: 'SGT', nric: 'S-553C', role: 'Paramedic' }, // Drivers (EMT & FRS) 'Shawal': { rank: 'SSG', nric: 'S-834G', role: 'Driver' }, 'Israr': { rank: 'SSG', nric: 'S-409F', role: 'Driver' }, 'Hamizan': { rank: 'SGT', nric: 'S-397I', role: 'Driver' }, 'Syahmi': { rank: 'SGT', nric: 'S-179C', role: 'Driver' }, 'Aizat': { rank: 'WO1', nric: 'S-124I', role: 'Driver' }, 'Solihin': { rank: 'SGT', nric: 'S-694J', role: 'Driver' }, 'Sol': { rank: 'SGT', nric: 'S-694J', role: 'Driver' }, 'Valerie': { rank: 'SGT', nric: 'T-349E', role: 'Driver' }, 'Rashid': { rank: 'SGT', nric: 'T-683B', role: 'Driver' }, 'Zulfiqar': { rank: 'SGT', nric: 'T-928G', role: 'Driver' }, 'Arif': { rank: 'WO1', nric: 'S-242Z', role: 'Driver' }, 'Farhan': { rank: 'SGT', nric: 'S-776H', role: 'Driver' }, 'Hakim': { rank: 'SGT', nric: 'S-617H', role: 'Driver' }, 'Salihin': { rank: 'SGT', nric: 'S-979C', role: 'Driver' }, 'Amanullah': { rank: 'SGT', nric: 'T-069A', role: 'Driver' }, 'James': { rank: 'WO1', nric: 'S-129J', role: 'Driver' }, 'Isaeiyah': { rank: 'SGT', nric: 'S-704F', role: 'Driver' }, 'Adli': { rank: 'SGT', nric: 'S-447F', role: 'Driver' }, 'Amiruddin': { rank: 'SGT', nric: 'S-818C', role: 'Driver' }, 'Hanwen': { rank: 'SGT', nric: 'T-417E', role: 'Driver' }, 'Ahmad Zaki': { rank: 'SSG', nric: 'S-086J', role: 'Driver' }, 'Zaki': { rank: 'SSG', nric: 'S-086J', role: 'Driver' }, 'Syahir': { rank: 'SGT', nric: 'S-597E', role: 'Driver' }, 'Aizat Bas': { rank: 'SGT', nric: 'S-671B', role: 'Driver' }, 'JianHe': { rank: 'SGT', nric: 'S-469J', role: 'Driver' }, 'Jason': { rank: 'SGT', nric: 'S-469J', role: 'Driver' }, 'Fadzly': { rank: 'SGT', nric: 'S-262Z', role: 'Driver' }, 'Khairi': { rank: 'WO1', nric: 'S-908F', role: 'Driver' }, 'Hanif': { rank: 'SGT', nric: 'S-920H', role: 'Driver' }, 'Muzammil': { rank: 'WO1', nric: 'S-075G', role: 'Driver' }, 'Roshdie': { rank: 'SGT', nric: 'S-841C', role: 'Driver' }, 'Basheer': { rank: 'SGT', nric: 'S-859D', role: 'Driver' }, 'Aniq': { rank: 'SGT', nric: 'S-652J', role: 'Driver' }, 'Aziz': { rank: 'SGT', nric: 'S-583J', role: 'Driver' }, 'Afiq': { rank: 'WO2', nric: 'S-155B', role: 'Driver' }, 'Imran': { rank: 'WO1', nric: 'S-211H', role: 'Driver' }, 'Zulqarnaen': { rank: 'WO1', nric: 'S-323J', role: 'Driver' }, 'Nicholas Yeo': { rank: 'SGT', nric: 'S-225G', role: 'Driver' }, 'Yusali': { rank: 'SGT', nric: 'S-421A', role: 'Driver' }, 'Nick Tan': { rank: 'SGT', nric: 'S-816A', role: 'Driver' }, 'Damien': { rank: 'WO2', nric: 'S-616G', role: 'Driver' }, 'Kuang': { rank: 'WO2', nric: 'S-616G', role: 'Driver' }, 'Hassan': { rank: 'WO2', nric: 'S-544C', role: 'Driver' }, 'Ridhwan': { rank: 'WO2', nric: 'S-236G', role: 'Driver' }, 'Sallihin': { rank: 'WO2', nric: 'S-895F', role: 'Driver' }, 'Khairul': { rank: 'WO1', nric: 'S-182C', role: 'Driver' }, // EMT NSF 'Haiman': { rank: 'CPL', nric: 'T-816F', role: 'EMT NSF' }, 'Izz²': { rank: 'CPL', nric: 'T-199A', role: 'EMT NSF' }, 'Zubair': { rank: 'LCP', nric: 'T-969D', role: 'EMT NSF' }, 'Eddy': { rank: 'PTE', nric: 'T-708D', role: 'EMT NSF' }, 'Bilal': { rank: 'PTE', nric: 'T-955A', role: 'EMT NSF' }, 'Lutfil': { rank: 'PTE', nric: 'T-405A', role: 'EMT NSF' }, "Mu'az": { rank: 'CPL', nric: 'T-273H', role: 'EMT NSF' }, 'Rafiq': { rank: 'CPL', nric: 'T-607D', role: 'EMT NSF' }, 'Silmi': { rank: 'LCP', nric: 'T-318I', role: 'EMT NSF' }, 'Hazim': { rank: 'LCP', nric: 'T-761C', role: 'EMT NSF' }, 'Zheng Hao': { rank: 'LCP', nric: 'T-008E', role: 'EMT NSF' }, 'Russell': { rank: 'PTE', nric: 'T-691I', role: 'EMT NSF' }, 'Afiq': { rank: 'PTE', nric: 'T-397H', role: 'EMT NSF' }, 'Fawaris': { rank: 'CPL', nric: 'T-547F', role: 'EMT NSF' }, 'Faw': { rank: 'CPL', nric: 'T-547F', role: 'EMT NSF' }, 'Shabbeer': { rank: 'CPL', nric: 'T-192G', role: 'EMT NSF' }, 'Shab': { rank: 'CPL', nric: 'T-192G', role: 'EMT NSF' }, 'Farid': { rank: 'CPL', nric: 'T-193A', role: 'EMT NSF' }, 'Mufaddal': { rank: 'LCP', nric: 'T-289B', role: 'EMT NSF' }, 'Mufa': { rank: 'LCP', nric: 'T-289B', role: 'EMT NSF' }, 'Suhayl': { rank: 'PTE', nric: 'T-360D', role: 'EMT NSF' }, 'Romeo': { rank: 'PTE', nric: 'T-053F', role: 'EMT NSF' }, 'Jaris': { rank: 'CPL', nric: 'T-048G', role: 'EMT NSF' }, 'Fazly': { rank: 'LCP', nric: 'S-373H', role: 'EMT NSF' }, 'Elijah': { rank: 'LCP', nric: 'T-482A', role: 'EMT NSF' }, 'Hadi': { rank: 'PTE', nric: 'T-623F', role: 'EMT NSF' }, 'Wayne': { rank: 'PTE', nric: 'T-330J', role: 'EMT NSF' }, 'Mithran': { rank: 'PTE', nric: 'T-820D', role: 'EMT NSF' }, // PMT 'Khalid': { rank: 'PMT', nric: 'T-622E', role: 'PMT' }, 'Syarafana': { rank: 'PMT', nric: 'T-610G', role: 'PMT' }, 'Syamim': { rank: 'PMT', nric: 'T-194B', role: 'PMT' }, 'Fadilah': { rank: 'PMT', nric: 'T-067A', role: 'PMT' }, 'Meng Leong': { rank: 'PMT', nric: 'T-323I', role: 'PMT' }, 'Wafiy': { rank: 'PMT', nric: 'T-854H', role: 'PMT' }, 'Jia Hao': { rank: 'PMT', nric: 'T-979A', role: 'PMT' }, 'Asyiqin': { rank: 'PMT', nric: 'S-070H', role: 'PMT' }, 'Kevin': { rank: 'PMT', nric: 'S-076H', role: 'PMT' }, 'Syukri': { rank: 'PMT', nric: 'T-251Z', role: 'PMT' }, 'Mirza': { rank: 'PMT', nric: 'S-691Z', role: 'PMT' }, 'Melody': { rank: 'PMT', nric: 'T-695B', role: 'PMT' }, 'Sau Kuan': { rank: 'OCT', nric: 'T-985B', role: 'PMT' }, 'Jia Mao': { rank: 'OCT', nric: 'S-392E', role: 'PMT' }, }); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [messages]); const parseNRICList = (text) => { // Check if this looks like an NRIC list update if (!text.includes('PARAMEDIC') && !text.includes('DRIVER') && !text.includes('EMT NSF') && !text.includes('PMT')) { return null; } const newData = {}; const lines = text.split('\n'); let currentRole = null; for (let line of lines) { line = line.trim(); // Detect role headers if (line.includes('PARAMEDIC')) currentRole = 'Paramedic'; else if (line.includes('DRIVER') || line.includes('FRS')) currentRole = 'Driver'; else if (line.includes('EMT NSF')) currentRole = 'EMT NSF'; else if (line.includes('PMT')) currentRole = 'PMT'; // Parse personnel lines const match = line.match(/^(WO\d|SSG|SGT|CPL|LCP|PTE|PMT|OCT)\s+(.+?)\s+(S-\w{3,4}|T-\w{3,4})$/i); if (match && currentRole) { const [, rank, name, nric] = match; const cleanName = name.trim(); newData[cleanName] = { rank: rank.toUpperCase(), nric: nric.toUpperCase(), role: currentRole }; // Add common nicknames if (cleanName === 'Solihin') newData['Sol'] = newData[cleanName]; if (cleanName === 'Fawaris') newData['Faw'] = newData[cleanName]; if (cleanName === 'Mufaddal') newData['Mufa'] = newData[cleanName]; if (cleanName === 'Shabbeer') newData['Shab'] = newData[cleanName]; if (cleanName === 'Ahmad Zaki') newData['Zaki'] = newData[cleanName]; if (cleanName.includes('JianHe') || cleanName.includes('Jason')) { newData['JianHe'] = newData[cleanName]; newData['Jason'] = newData[cleanName]; } if (cleanName.includes('Damien') || cleanName.includes('Kuang')) { newData['Damien'] = newData[cleanName]; newData['Kuang'] = newData[cleanName]; } } } return Object.keys(newData).length > 0 ? newData : null; }; const handleSend = async () => { if (!input.trim() || isLoading) return; const userMessage = input.trim(); setInput(''); // Check if user is confirming an update if (pendingUpdate && (userMessage.toLowerCase() === 'yes' || userMessage.toLowerCase() === 'y')) { setMessages(prev => [...prev, { role: 'user', content: userMessage }]); setPersonnelData(pendingUpdate); setPendingUpdate(null); setMessages(prev => [...prev, { role: 'assistant', content: '✅ Database updated successfully! The new personnel list is now active.' }]); return; } if (pendingUpdate && (userMessage.toLowerCase() === 'no' || userMessage.toLowerCase() === 'n')) { setMessages(prev => [...prev, { role: 'user', content: userMessage }]); setPendingUpdate(null); setMessages(prev => [...prev, { role: 'assistant', content: 'Update cancelled. The database remains unchanged.' }]); return; } // Check if this is an NRIC list update const parsedData = parseNRICList(userMessage); if (parsedData) { setMessages(prev => [...prev, { role: 'user', content: userMessage }]); const count = Object.keys(parsedData).filter(key => !['Sol', 'Faw', 'Mufa', 'Shab', 'Zaki', 'JianHe', 'Jason', 'Damien', 'Kuang'].includes(key)).length; setMessages(prev => [...prev, { role: 'assistant', content: `📋 I've detected an NRIC list update with ${count} personnel entries.\n\nWould you like me to update the database? Reply "yes" to confirm or "no" to cancel.` }]); setPendingUpdate(parsedData); return; } setMessages(prev => [...prev, { role: 'user', content: userMessage }]); setIsLoading(true); try { const systemPrompt = `You are a Manning Schedule Assistant for emergency medical services. You have access to a complete personnel database with their ranks, NRIC numbers, and roles (Paramedic, Driver, EMT NSF, PMT). Personnel Database: ${JSON.stringify(personnelData, null, 2)} CRITICAL - ROLE IDENTIFICATION: - ALWAYS verify roles by checking the NRIC AND RANK in the database - DO NOT trust labels in the schedule - **EMT NSF** = Anyone with rank PTE, LCP, or CPL in the database (check NRIC to confirm) - **PMT** = Anyone with rank PMT or OCT in the database (check NRIC to confirm) - If someone is labeled "PMT" in the schedule but their NRIC shows they have PTE/CPL/LCP rank, they are EMT NSF NOT PMT - If someone has PTE/LCP/CPL rank, they are EMT NSF - period. No exceptions. - Cross-reference EVERY person's NRIC with the database to determine their TRUE role based on their rank IMPORTANT - Name Handling: - When a SHORT NAME or NICKNAME is given in the schedule (e.g., "Mufa", "Faw", "Sol", "Shab", "Zaki", "Jason", "Kuang"), ALWAYS use their FULL NAME in your response - Examples: "Mufa" → "Mufaddal", "Faw" → "Fawaris", "Sol" → "Solihin", "Shab" → "Shabbeer", "Zaki" → "Ahmad Zaki" - The database contains both short names and full names - use the NRIC to identify the correct person and always output their full name When users send manning schedules, parse them and answer questions about: - Who is working on specific dates - What role each person has (Paramedic, Driver, EMT NSF, PMT) - NRIC numbers and ranks - Crew compositions for each ambulance/vehicle Manning schedule format example: "5/1(R3 ND) A231 - Willie, Sol, Mufa, Asyiqin A232 - Nadia, Hakim, Faw, Suhayl" For each crew, identify based on DATABASE lookup (not schedule labels): - **Paramedic**: (first person, usually SGT/WO rank from Paramedic list) - **Driver**: (second person, from Driver list - includes both EMT and FRS drivers) - **EMT NSF**: (persons with CPL/LCP/PTE rank from EMT NSF list) - If there is ONE EMT NSF: format as "CPL Fawaris (T-547F)" or "PTE Suhayl (T-360D)" - If there are TWO OR MORE EMT NSFs: format as "CPL Fawaris//PTE Suhayl (T-547F//T-360D)" - Always use "//" to separate multiple EMT NSFs with their respective ranks and NRICs - IMPORTANT: Check NRIC - if someone is labeled "PMT" but has PTE/CPL/LCP rank in database, they are EMT NSF - **PMT**: (person with PMT/OCT rank in database - check NRIC to confirm) - PMT should ALWAYS be listed on a SEPARATE line/row from EMT NSF - Even if there are multiple EMT NSFs, PMT stays separate - Do NOT combine PMT with EMT NSF using "//" CRITICAL FORMATTING RULES: - EMT NSF and PMT are SEPARATE categories - NEVER combine them together - When there is ONLY ONE EMT NSF: show just that person with their rank and NRIC on EMT NSF line - When there are TWO OR MORE EMT NSFs: combine them with "//" on EMT NSF line - PMT is ALWAYS on its own separate line, regardless of how many EMT NSFs there are - Example correct format: EMT NSF: CPL Fawaris (T-547F) PMT: PMT Asyiqin (S-070H) - Example WRONG format (do not do this): EMT NSF: CPL Fawaris//PMT Asyiqin (combined - INCORRECT!) - VERIFY every person's role against the database using their NRIC Be concise and clear. Format responses with proper line breaks and structure. Always include the full rank, FULL NAME (not nickname), and NRIC (last 4 digits) when identifying personnel.`; const response = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'claude-sonnet-4-20250514', max_tokens: 1000, system: systemPrompt, messages: [ ...messages.map(m => ({ role: m.role, content: m.content })), { role: 'user', content: userMessage } ] }) }); const data = await response.json(); const assistantMessage = data.content .filter(block => block.type === 'text') .map(block => block.text) .join('\n'); setMessages(prev => [...prev, { role: 'assistant', content: assistantMessage }]); } catch (error) { setMessages(prev => [...prev, { role: 'assistant', content: 'Sorry, I encountered an error processing your request. Please try again.' }]); } finally { setIsLoading(false); } }; return (

Manning Schedule Assistant

Ask me about manning schedules or update the database

{messages.map((message, index) => (
{message.role === 'assistant' && (
)}
{message.content}
{message.role === 'user' && (
)}
))} {isLoading && (
)}