82 lines
4.3 KiB
JavaScript
82 lines
4.3 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { Trash2, Plus } from 'lucide-react';
|
|
import { Image } from '@/components/ui/image';
|
|
|
|
const PRESET_TAGS = [
|
|
'Raw meat next to ready-to-eat food',
|
|
'Same board for raw + cooked',
|
|
'Soiled towel on food surface',
|
|
'Food in the temperature danger zone',
|
|
'Unwashed hands',
|
|
'Improper storage',
|
|
'Cross-contact allergen',
|
|
];
|
|
|
|
// Interactive hazard identification — tap a kitchen photo to mark hazards.
|
|
export default function HazardIdentifier({ course, onSubmit, submitting }) {
|
|
const imageUrl = course?.challenge?.image_url;
|
|
const [markers, setMarkers] = useState([]);
|
|
const [pending, setPending] = useState(null);
|
|
const [customLabel, setCustomLabel] = useState('');
|
|
|
|
const addMarker = (e) => {
|
|
if (submitting || pending) return;
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
const x = (e.clientX - rect.left) / rect.width;
|
|
const y = (e.clientY - rect.top) / rect.height;
|
|
setPending({ x, y });
|
|
};
|
|
|
|
const confirm = (label) => {
|
|
if (!pending) return;
|
|
setMarkers([...markers, { ...pending, label }]);
|
|
setPending(null);
|
|
setCustomLabel('');
|
|
};
|
|
|
|
const remove = (i) => setMarkers(markers.filter((_, j) => j !== i));
|
|
|
|
return (
|
|
<div>
|
|
<p className="text-[13px] text-[#64748B] mb-3">Tap anywhere on the kitchen photo to mark a hazard. Label each one, then submit for AI evaluation.</p>
|
|
<div className="relative rounded-2xl overflow-hidden border border-[#E5E7EB] mb-3 select-none cursor-crosshair" onClick={addMarker}>
|
|
{imageUrl ? (
|
|
<Image src={imageUrl} alt="Kitchen scene" className="w-full max-h-80 object-cover" fittingType="fill" />
|
|
) : (
|
|
<div className="w-full h-64 bg-[#F1F5F9] flex items-center justify-center text-[13px] text-[#94A3B8]">No scene image configured</div>
|
|
)}
|
|
{markers.map((m, i) => (
|
|
<div key={i} className="absolute -translate-x-1/2 -translate-y-1/2 group" style={{ left: `${m.x * 100}%`, top: `${m.y * 100}%` }} onClick={(e) => e.stopPropagation()}>
|
|
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-red-500 text-white text-[11px] font-bold shadow ring-2 ring-white">{i + 1}</span>
|
|
<span className="absolute left-1/2 -translate-x-1/2 top-7 whitespace-nowrap text-[10px] bg-white/95 rounded px-1.5 py-0.5 shadow text-[#0F172A]">{m.label}</span>
|
|
<button onClick={() => remove(i)} className="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-white text-red-500 shadow flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"><Trash2 className="w-2.5 h-2.5" /></button>
|
|
</div>
|
|
))}
|
|
{pending && (
|
|
<div className="absolute -translate-x-1/2 -translate-y-1/2 pointer-events-none" style={{ left: `${pending.x * 100}%`, top: `${pending.y * 100}%` }}>
|
|
<span className="block w-6 h-6 rounded-full bg-[#0A39DF] ring-4 ring-white animate-pulse" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{pending && (
|
|
<div className="rounded-xl border border-[#0A39DF] bg-[#EFF6FF] p-3 mb-3">
|
|
<p className="text-[12px] font-medium text-[#1E40AF] mb-2">Label this hazard:</p>
|
|
<div className="flex flex-wrap gap-1.5 mb-2">
|
|
{PRESET_TAGS.map((t) => (
|
|
<button key={t} onClick={() => confirm(t)} className="text-[11px] px-2.5 py-1 rounded-full bg-white border border-[#E5E7EB] text-[#374151] hover:border-[#0A39DF]">{t}</button>
|
|
))}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<input value={customLabel} onChange={(e) => setCustomLabel(e.target.value)} placeholder="Or type your own…" className="flex-1 h-8 px-2.5 text-[12px] rounded-lg border border-[#E5E7EB] focus:border-[#0A39DF] focus:outline-none" />
|
|
<button onClick={() => customLabel.trim() && confirm(customLabel.trim())} className="h-8 px-3 rounded-lg bg-[#0A39DF] text-white text-[12px] font-medium flex items-center gap-1"><Plus className="w-3.5 h-3.5" /> Add</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<button onClick={() => onSubmit(markers)} disabled={markers.length === 0 || submitting || !!pending} className="w-full h-10 rounded-xl bg-[#0F172A] text-white text-[13px] font-semibold disabled:opacity-40">
|
|
Submit {markers.length} hazard{markers.length !== 1 ? 's' : ''} for AI evaluation
|
|
</button>
|
|
</div>
|
|
);
|
|
} |