diff --git a/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/ConfigCursor.tsx b/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/ConfigCursor.tsx new file mode 100644 index 00000000..a641a7ac --- /dev/null +++ b/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/ConfigCursor.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { useDelayedUnmount } from '../../hooks/useDelayedUnmount'; + +interface ConfigCursorProps { + pos: { x: number; y: number }; + visible: boolean; + isClicking: boolean; + transition: string; +} + +export default function ConfigCursor({ pos, visible, isClicking, transition }: ConfigCursorProps) { + const shouldRender = useDelayedUnmount(visible, 300); + + if (!shouldRender) return null; + + return ( +
+ + + +
+ ); +} diff --git a/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/InputTypeStep.tsx b/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/InputTypeStep.tsx new file mode 100644 index 00000000..63826834 --- /dev/null +++ b/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/InputTypeStep.tsx @@ -0,0 +1,66 @@ +import React from 'react'; + +interface InputTypeStepProps { + isVisible: boolean; + selected: string | null; +} + +const INPUT_TYPES = [ + { id: 'image', label: 'Image' }, + { id: 'comparison', label: 'Comparison' }, + { id: 'video', label: 'Video' }, + { id: 'depth', label: 'Depth' }, +]; + +export default function InputTypeStep({ isVisible, selected }: InputTypeStepProps) { + return ( +
+
+ INPUT TYPE +
+
+
+ {INPUT_TYPES.map(({ id, label }) => ( +
+ + {label} + +
+
+ ))} +
+
+ + + +
+
+
+ ); +} diff --git a/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/PreviewPanel.tsx b/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/PreviewPanel.tsx new file mode 100644 index 00000000..e15515b0 --- /dev/null +++ b/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/PreviewPanel.tsx @@ -0,0 +1,308 @@ +import React, { useRef, useEffect, useState, useCallback } from 'react'; + +interface PreviewPanelProps { + resolution: number; + inputType: string | null; + taskType: string | null; + isActive: boolean; +} + +const SVG_MAP: Record = { + image: '/img/ai/landing/inspection-image.svg', + comparison: '/img/ai/landing/inspection-comparison.svg', + depth: '/img/ai/landing/inspection-depth.svg', + video: '/img/ai/landing/inspection-video.svg', +}; + +const BOUNDING_BOXES: Record = { + image: [ + { x: 8.5, y: 4, w: 13, h: 16, label: 'Apple', color: '#00ffd1' }, + { x: 59.75, y: 3.5, w: 10.5, h: 13, label: 'Apple', color: '#00ffd1' }, + { x: 38, y: 31.7, w: 13.75, h: 16.7, label: 'Bruised', color: '#ff6b6b' }, + { x: 10.75, y: 46.7, w: 8.5, h: 10.7, label: 'Apple', color: '#00ffd1' }, + { x: 74, y: 42, w: 8, h: 10, label: 'Apple', color: '#00ffd1' }, + { x: 30, y: 68.8, w: 10, h: 12.3, label: 'Apple', color: '#00ffd1' }, + { x: 62, y: 68.8, w: 10, h: 12.3, label: 'Apple', color: '#00ffd1' }, + ], + depth: [ + { x: 9, y: 18.3, w: 9.5, h: 11.7, label: 'Apple', color: '#00ffd1' }, + { x: 23.5, y: 16.7, w: 8, h: 10, label: 'Apple', color: '#00ffd1' }, + { x: 32, y: 21.5, w: 7, h: 8.7, label: 'Apple', color: '#6b94ff' }, + { x: 56.9, y: 41.3, w: 11.25, h: 14, label: 'Apple', color: '#00ffd1' }, + { x: 71.75, y: 40.3, w: 9, h: 11, label: 'Apple', color: '#00ffd1' }, + { x: 83.75, y: 44.5, w: 7.5, h: 9.3, label: 'Apple', color: '#6b94ff' }, + { x: 17.25, y: 66.7, w: 10.5, h: 13.3, label: 'Apple', color: '#ff6b6b' }, + { x: 64.4, y: 73, w: 8.75, h: 10.7, label: 'Apple', color: '#ff6b6b' }, + ], + video: [ + { x: 5.25, y: 32.2, w: 9.5, h: 11.7, label: 'Apple', color: '#00ffd1' }, + { x: 27.75, y: 27.5, w: 10.5, h: 13, label: 'Apple', color: '#00ffd1' }, + { x: 55, y: 30.8, w: 10, h: 12.3, label: 'Defect', color: '#ff6b6b' }, + { x: 78.5, y: 29.5, w: 9, h: 11, label: 'Apple', color: '#00ffd1' }, + { x: 17.5, y: 54.5, w: 9, h: 11, label: 'Apple', color: '#00ffd1' }, + { x: 48, y: 52.8, w: 10, h: 12.3, label: 'Apple', color: '#00ffd1' }, + ], +}; + +const SEGMENTATION_GROUPS: Record = { + image: [ + { d: 'M8.5,12 A6.5,8 0 1,0 21.5,12 A6.5,8 0 1,0 8.5,12 Z M59.75,10 A5.25,6.5 0 1,0 70.25,10 A5.25,6.5 0 1,0 59.75,10 Z M10.75,52 A4.25,5.33 0 1,0 19.25,52 A4.25,5.33 0 1,0 10.75,52 Z M74,47 A4,5 0 1,0 82,47 A4,5 0 1,0 74,47 Z', color: 'rgba(0,255,209,0.2)', stroke: '#00ffd1', label: 'Good' }, + { d: 'M38.1,40 A6.9,8.33 0 1,0 51.9,40 A6.9,8.33 0 1,0 38.1,40 Z', color: 'rgba(255,107,107,0.2)', stroke: '#ff6b6b', label: 'Bruise' }, + { d: 'M30,75 A5,6.17 0 1,0 40,75 A5,6.17 0 1,0 30,75 Z M62,75 A5,6.17 0 1,0 72,75 A5,6.17 0 1,0 62,75 Z', color: 'rgba(107,148,255,0.2)', stroke: '#6b94ff', label: 'Good' }, + ], + depth: [ + { d: 'M9,24.2 A4.75,5.83 0 1,0 18.5,24.2 A4.75,5.83 0 1,0 9,24.2 Z M23.5,21.7 A4,5 0 1,0 31.5,21.7 A4,5 0 1,0 23.5,21.7 Z M32.1,25.8 A3.5,4.33 0 1,0 39.1,25.8 A3.5,4.33 0 1,0 32.1,25.8 Z', color: 'rgba(0,255,209,0.2)', stroke: '#00ffd1', label: 'Good' }, + { d: 'M56.9,48.3 A5.63,7 0 1,0 68.1,48.3 A5.63,7 0 1,0 56.9,48.3 Z M71.75,45.8 A4.5,5.5 0 1,0 80.75,45.8 A4.5,5.5 0 1,0 71.75,45.8 Z M83.75,49.2 A3.75,4.67 0 1,0 91.25,49.2 A3.75,4.67 0 1,0 83.75,49.2 Z', color: 'rgba(107,148,255,0.2)', stroke: '#6b94ff', label: 'Good' }, + { d: 'M17.25,73.3 A5.25,6.67 0 1,0 27.75,73.3 A5.25,6.67 0 1,0 17.25,73.3 Z M64.4,78.3 A4.38,5.33 0 1,0 73.1,78.3 A4.38,5.33 0 1,0 64.4,78.3 Z', color: 'rgba(255,107,107,0.2)', stroke: '#ff6b6b', label: 'Rotten' }, + ], + video: [ + { d: 'M5.25,38 A4.75,5.83 0 1,0 14.75,38 A4.75,5.83 0 1,0 5.25,38 Z M27.75,34 A5.25,6.5 0 1,0 38.25,34 A5.25,6.5 0 1,0 27.75,34 Z M78.5,35 A4.5,5.5 0 1,0 87.5,35 A4.5,5.5 0 1,0 78.5,35 Z M17.5,60 A4.5,5.5 0 1,0 26.5,60 A4.5,5.5 0 1,0 17.5,60 Z M48,59 A5,6.17 0 1,0 58,59 A5,6.17 0 1,0 48,59 Z', color: 'rgba(0,255,209,0.2)', stroke: '#00ffd1', label: 'Good' }, + { d: 'M55,37 A5,6.17 0 1,0 65,37 A5,6.17 0 1,0 55,37 Z', color: 'rgba(255,107,107,0.2)', stroke: '#ff6b6b', label: 'Defect' }, + ], +}; + +const RESOLUTION_STOPS = [ + { value: 0, label: '120p' }, + { value: 15, label: '240p' }, + { value: 30, label: '480p' }, + { value: 50, label: '720p' }, + { value: 70, label: '1080p' }, + { value: 85, label: '1440p' }, + { value: 100, label: '4K' }, +]; + +function getResolutionLabel(value: number): string { + for (let i = RESOLUTION_STOPS.length - 1; i >= 0; i--) { + if (value >= RESOLUTION_STOPS[i].value) return RESOLUTION_STOPS[i].label; + } + return RESOLUTION_STOPS[0].label; +} + +export default function PreviewPanel({ resolution, inputType, taskType, isActive }: PreviewPanelProps) { + const canvasRef = useRef(null); + const imagesRef = useRef>({}); + const [loadedImages, setLoadedImages] = useState>(new Set()); + + useEffect(() => { + Object.entries(SVG_MAP).forEach(([key, src]) => { + const img = new Image(); + img.src = src; + img.onload = () => { + imagesRef.current[key] = img; + setLoadedImages(prev => new Set(prev).add(key)); + }; + }); + }, []); + + const drawPixelated = useCallback((img: HTMLImageElement, scale: number) => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const displayW = canvas.width; + const displayH = canvas.height; + + if (scale >= 0.95) { + ctx.imageSmoothingEnabled = true; + ctx.clearRect(0, 0, displayW, displayH); + ctx.drawImage(img, 0, 0, displayW, displayH); + return; + } + + const smallW = Math.max(4, Math.floor(displayW * scale)); + const smallH = Math.max(4, Math.floor(displayH * scale)); + + const offscreen = document.createElement('canvas'); + offscreen.width = smallW; + offscreen.height = smallH; + const offCtx = offscreen.getContext('2d'); + if (!offCtx) return; + + offCtx.drawImage(img, 0, 0, smallW, smallH); + ctx.imageSmoothingEnabled = false; + ctx.clearRect(0, 0, displayW, displayH); + ctx.drawImage(offscreen, 0, 0, smallW, smallH, 0, 0, displayW, displayH); + }, []); + + const resolutionScale = Math.max(0.03, resolution / 100); + const currentKey = inputType || 'image'; + const currentImg = imagesRef.current[currentKey]; + + useEffect(() => { + if (!currentImg) return; + const canvas = canvasRef.current; + if (!canvas) return; + + const rect = canvas.parentElement?.getBoundingClientRect(); + if (rect) { + canvas.width = Math.floor(rect.width * (window.devicePixelRatio || 1)); + canvas.height = Math.floor(rect.height * (window.devicePixelRatio || 1)); + } + + drawPixelated(currentImg, resolutionScale); + }, [currentImg, resolutionScale, drawPixelated, loadedImages]); + + useEffect(() => { + if (!currentImg) return; + const canvas = canvasRef.current; + if (!canvas) return; + + const observer = new ResizeObserver(() => { + const rect = canvas.parentElement?.getBoundingClientRect(); + if (rect) { + canvas.width = Math.floor(rect.width * (window.devicePixelRatio || 1)); + canvas.height = Math.floor(rect.height * (window.devicePixelRatio || 1)); + } + drawPixelated(currentImg, resolutionScale); + }); + + if (canvas.parentElement) observer.observe(canvas.parentElement); + return () => observer.disconnect(); + }, [currentImg, resolutionScale, drawPixelated]); + + const isComparison = inputType === 'comparison'; + const isDepth = inputType === 'depth'; + const isVideo = inputType === 'video'; + + const boxes = BOUNDING_BOXES[currentKey] || BOUNDING_BOXES.image; + const segGroups = SEGMENTATION_GROUPS[currentKey] || SEGMENTATION_GROUPS.image; + + return ( +
+
+ + + + {isDepth && ( +
+
3D DEPTH
+
+ 0mm +
+ 8mm +
+
+ )} + + {isVideo && ( +
+
+ +
+
+
+
+
+ )} + + {taskType && } + +
+
+ ); +} + +interface TaskOverlayProps { + taskType: string; + boxes: typeof BOUNDING_BOXES.image; + segGroups: typeof SEGMENTATION_GROUPS.image; + inputType: string | null; +} + +function TaskOverlay({ taskType, boxes, segGroups, inputType }: TaskOverlayProps) { + if (taskType === 'classification') { + return null; + } + + if (taskType === 'detection') { + return ( + + {boxes.map((box, i) => ( + + + + ))} + + ); + } + + if (taskType === 'segmentation') { + return ( + + {segGroups.map((group, i) => ( + + + + + ))} + + ); + } + + return null; +} diff --git a/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/ResolutionStep.tsx b/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/ResolutionStep.tsx new file mode 100644 index 00000000..d1c97117 --- /dev/null +++ b/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/ResolutionStep.tsx @@ -0,0 +1,68 @@ +import React from 'react'; + +const RESOLUTION_STOPS = [ + { value: 0, label: '120p' }, + { value: 15, label: '240p' }, + { value: 30, label: '480p' }, + { value: 50, label: '720p' }, + { value: 70, label: '1080p' }, + { value: 85, label: '1440p' }, + { value: 100, label: '4K' }, +]; + +function getResolutionLabel(value: number): string { + for (let i = RESOLUTION_STOPS.length - 1; i >= 0; i--) { + if (value >= RESOLUTION_STOPS[i].value) return RESOLUTION_STOPS[i].label; + } + return RESOLUTION_STOPS[0].label; +} + +interface ResolutionStepProps { + isVisible: boolean; + value: number; +} + +export default function ResolutionStep({ isVisible, value }: ResolutionStepProps) { + return ( +
+
+ RESOLUTION +
+
+
+ + {getResolutionLabel(value)} + +
+
+
+
+
+
+
+
+
+ ); +} diff --git a/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/TaskTypeStep.tsx b/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/TaskTypeStep.tsx new file mode 100644 index 00000000..178687fe --- /dev/null +++ b/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/TaskTypeStep.tsx @@ -0,0 +1,90 @@ +import React from 'react'; + +interface TaskTypeStepProps { + isVisible: boolean; + selected: string | null; +} + +const TASK_TYPES = [ + { + id: 'classification', + label: 'Classification', + icon: ( + + + + + ), + }, + { + id: 'detection', + label: 'Detection', + icon: ( + + + + + ), + }, + { + id: 'segmentation', + label: 'Segmentation', + icon: ( + + + + + + + + + ), + }, +]; + +export default function TaskTypeStep({ isVisible, selected }: TaskTypeStepProps) { + return ( +
+
+ TASK TYPE +
+
+ {TASK_TYPES.map(({ id, label, icon }) => ( +
+ {icon} + + {label} + +
+
+ ))} +
+
+ ); +} diff --git a/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/index.tsx b/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/index.tsx new file mode 100644 index 00000000..c50e1c48 --- /dev/null +++ b/src/components/HomeHero/SoftwareWindow/ConfigurationSteps/index.tsx @@ -0,0 +1,254 @@ +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import ConfigCursor from './ConfigCursor'; +import ResolutionStep from './ResolutionStep'; +import InputTypeStep from './InputTypeStep'; +import TaskTypeStep from './TaskTypeStep'; +import PreviewPanel from './PreviewPanel'; + +interface ConfigurationStepsProps { + isActive: boolean; + onComplete: () => void; + isCompact: boolean; + isSmallScreen: boolean; +} + +const STEP_PAUSE = 500; + +const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export default function ConfigurationSteps({ + isActive, + onComplete, + isCompact, + isSmallScreen, +}: ConfigurationStepsProps) { + const [currentStep, setCurrentStep] = useState(0); + const [resolutionValue, setResolutionValue] = useState(10); + const [selectedInputType, setSelectedInputType] = useState('image'); + const [selectedTaskType, setSelectedTaskType] = useState('classification'); + + const [cursorPos, setCursorPos] = useState({ x: 95, y: 90 }); + const [cursorVisible, setCursorVisible] = useState(false); + const [isClicking, setIsClicking] = useState(false); + const [cursorTransition, setCursorTransition] = useState('all 0.8s cubic-bezier(0.2, 0.8, 0.2, 1)'); + + const [showPreview, setShowPreview] = useState(false); + const [isFadingOut, setIsFadingOut] = useState(false); + + const containerRef = useRef(null); + const sequenceRunning = useRef(false); + const abortRef = useRef(false); + const sliderIntervalRef = useRef(null); + + const getTargetPos = useCallback((id: string, anchor: 'center' | 'left' = 'center') => { + if (!containerRef.current) return { x: 50, y: 50, width: 0, contWidth: 1 }; + const el = document.getElementById(id); + if (!el) return { x: 50, y: 50, width: 0, contWidth: 1 }; + + const rect = el.getBoundingClientRect(); + const contRect = containerRef.current.getBoundingClientRect(); + + let targetX = rect.left; + if (anchor === 'center') targetX += rect.width / 2; + const targetY = rect.top + rect.height / 2; + + const xPercent = ((targetX - contRect.left) / contRect.width) * 100; + const yPercent = ((targetY - contRect.top) / contRect.height) * 100; + + return { x: xPercent, y: yPercent, width: rect.width, contWidth: contRect.width }; + }, []); + + const clickButton = useCallback(async (id: string, setter: (val: string) => void, val: string) => { + if (abortRef.current) return; + const target = getTargetPos(id); + setCursorPos(target); + + await delay(500); + if (abortRef.current) return; + + setIsClicking(true); + await delay(200); + setter(val); + await delay(200); + setIsClicking(false); + }, [getTargetPos]); + + useEffect(() => { + if (!isActive || sequenceRunning.current) return; + sequenceRunning.current = true; + abortRef.current = false; + + const sequence = async () => { + await delay(300); + if (abortRef.current) return; + + setCurrentStep(1); + setShowPreview(true); + + await delay(300); + if (abortRef.current) return; + setCurrentStep(2); + + await delay(300); + if (abortRef.current) return; + setCurrentStep(3); + + await delay(500); + if (abortRef.current) return; + + setCursorTransition('all 0.8s cubic-bezier(0.2, 0.8, 0.2, 1)'); + setCursorVisible(true); + setCursorPos({ x: 95, y: 90 }); + + await delay(100); + if (abortRef.current) return; + + const sliderInfo = getTargetPos('config-res-slider', 'left'); + const sliderWidthPercent = (sliderInfo.width / sliderInfo.contWidth) * 100; + const startPosX = sliderInfo.x + (10 / 100) * sliderWidthPercent; + setCursorPos({ x: startPosX, y: sliderInfo.y }); + + await delay(500); + if (abortRef.current) return; + + setIsClicking(true); + setCursorTransition('left 0s linear, top 0s linear'); + await delay(50); + + const dragDuration = 800; + const startValue = 10; + const targetValue = 100; + const startTime = Date.now(); + + await new Promise((resolve) => { + sliderIntervalRef.current = setInterval(() => { + if (abortRef.current) { + if (sliderIntervalRef.current) clearInterval(sliderIntervalRef.current); + resolve(); + return; + } + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / dragDuration, 1); + const val = Math.round(startValue + (targetValue - startValue) * progress); + const cursorX = sliderInfo.x + (val / 100) * sliderWidthPercent; + + setResolutionValue(val); + setCursorPos({ x: cursorX, y: sliderInfo.y }); + + if (progress >= 1) { + if (sliderIntervalRef.current) clearInterval(sliderIntervalRef.current); + sliderIntervalRef.current = null; + resolve(); + } + }, 32); + }); + + if (abortRef.current) return; + setIsClicking(false); + setCursorTransition('all 0.8s cubic-bezier(0.2, 0.8, 0.2, 1)'); + + await delay(STEP_PAUSE); + if (abortRef.current) return; + + await clickButton('config-input-comparison', (v) => setSelectedInputType(v), 'comparison'); + await delay(STEP_PAUSE); + if (abortRef.current) return; + + await clickButton('config-input-video', (v) => setSelectedInputType(v), 'video'); + await delay(STEP_PAUSE); + if (abortRef.current) return; + + await clickButton('config-input-depth', (v) => setSelectedInputType(v), 'depth'); + await delay(STEP_PAUSE); + if (abortRef.current) return; + + await clickButton('config-task-detection', (v) => setSelectedTaskType(v), 'detection'); + await delay(STEP_PAUSE); + if (abortRef.current) return; + + await clickButton('config-task-segmentation', (v) => setSelectedTaskType(v), 'segmentation'); + setCursorVisible(false); + + await delay(3000); + if (abortRef.current) return; + + setIsFadingOut(true); + await delay(500); + + if (!abortRef.current) { + onComplete(); + } + }; + + sequence(); + + return () => { + abortRef.current = true; + if (sliderIntervalRef.current) { + clearInterval(sliderIntervalRef.current); + sliderIntervalRef.current = null; + } + }; + }, [isActive, onComplete, getTargetPos, clickButton]); + + return ( +
+ + +
+ = 1} value={resolutionValue} /> + = 2} selected={selectedInputType} /> + = 3} selected={selectedTaskType} /> +
+ +
+ +
+ + +
+ ); +} diff --git a/src/components/HomeHero/SoftwareWindow/index.tsx b/src/components/HomeHero/SoftwareWindow/index.tsx index cd92eae0..cc1e3a11 100644 --- a/src/components/HomeHero/SoftwareWindow/index.tsx +++ b/src/components/HomeHero/SoftwareWindow/index.tsx @@ -6,6 +6,7 @@ import CameraStation from "./CameraStation"; import TopMetrics from "./TopMetrics"; import TrainingProgressOverlay from "./TrainingProgressOverlay"; import DeployView from "./DeployView"; +import ConfigurationSteps from "./ConfigurationSteps"; import { usePerformance } from "../index"; import { useDelayedUnmount } from '../hooks/useDelayedUnmount'; @@ -137,6 +138,8 @@ export default function SoftwareWindow({ const [isCollapsingToCore, setIsCollapsingToCore] = useState(false); const [isRetrainingScan, setIsRetrainingScan] = useState(false); + const [isConfigPhase, setIsConfigPhase] = useState(false); + const [configComplete, setConfigComplete] = useState(false); const [animateHardwareSwitch, setAnimateHardwareSwitch] = useState(false); const [isRebuildingBase, setIsRebuildingBase] = useState(false); const [isRetrainingPurple, setIsRetrainingPurple] = useState(false); @@ -189,7 +192,7 @@ export default function SoftwareWindow({ const cameraStationVisible = isFinalLayout && !(isRetraining || isResettingModel || isRetrainingAnalysis || isCollapsingToCore || isRebuildingBase || isRetrainingPurple); const shouldRenderCameraStation = useDelayedUnmount(cameraStationVisible, 300); - const trainingPanelVisible = !((isFinalLayout && !isRetrainingAnalysis) || isResettingModel || isCollapsingToCore); + const trainingPanelVisible = !((isFinalLayout && !isRetrainingAnalysis) || isResettingModel || isCollapsingToCore || (hasTrainingData && !configComplete && !hasRetrained)); const shouldRenderTrainingPanel = useDelayedUnmount(trainingPanelVisible, 800); const handleHeaderMouseDown = (e: React.MouseEvent) => { @@ -464,6 +467,8 @@ export default function SoftwareWindow({ setIsRetrainingPurple(false); setRetrainedPrecision(false); setTrainingColor('orange'); + setIsConfigPhase(false); + setConfigComplete(false); setScanCompleted(false); setCollapseCompleted(false); if (pendingRetrainTimer) { @@ -476,7 +481,21 @@ export default function SoftwareWindow({ }, [hasTrainingData]); useEffect(() => { - if (hasTrainingData && !hasRetrained && !isRetrainingAnalysis) { + if (hasTrainingData && !hasRetrained && !isRetrainingAnalysis && !configComplete) { + const configTimer = setTimeout(() => { + setIsConfigPhase(true); + }, 300); + return () => clearTimeout(configTimer); + } + }, [hasTrainingData, hasRetrained, isRetrainingAnalysis, configComplete]); + + const handleConfigComplete = useCallback(() => { + setIsConfigPhase(false); + setConfigComplete(true); + }, []); + + useEffect(() => { + if (configComplete && !hasRetrained && !isRetrainingAnalysis) { const setupTimer = setTimeout(() => { setAnimateSetup(true); }, 500); @@ -488,7 +507,7 @@ export default function SoftwareWindow({ clearTimeout(analysisTimer); }; } - }, [hasTrainingData, hasRetrained, isRetrainingAnalysis]); + }, [configComplete, hasRetrained, isRetrainingAnalysis]); useEffect(() => { if (startAnalysis && !hasProgressBarStarted && !isRetrainingAnalysis) { @@ -597,6 +616,23 @@ export default function SoftwareWindow({ transition: "all 1.2s cubic-bezier(0.2, 0.8, 0.2, 1)" }} > + {isConfigPhase && ( +
+ +
+ )} + {shouldRenderCameraStation && (
)} - {shouldRenderTrainingPanel && ( + {shouldRenderTrainingPanel && !(hasTrainingData && !configComplete && !hasRetrained) && (
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GRADE: A — PASS + GRADE: F — REJECT + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/img/ai/landing/inspection-depth.svg b/static/img/ai/landing/inspection-depth.svg new file mode 100644 index 00000000..feb5b97d --- /dev/null +++ b/static/img/ai/landing/inspection-depth.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Z: 120mm + Z: 45mm + Z: 0mm + + + + + + \ No newline at end of file diff --git a/static/img/ai/landing/inspection-image.svg b/static/img/ai/landing/inspection-image.svg new file mode 100644 index 00000000..98486ec6 --- /dev/null +++ b/static/img/ai/landing/inspection-image.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/img/ai/landing/inspection-video.svg b/static/img/ai/landing/inspection-video.svg new file mode 100644 index 00000000..041f0f37 --- /dev/null +++ b/static/img/ai/landing/inspection-video.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FLOW + + + + + + + APPLE-01 + + + + + + + APPLE-02 + + + + + + + + APPLE-03 + + + + + + + + APPLE-04 + + + + + + + APPLE-05 + + + + + + + APPLE-06 + + + + \ No newline at end of file