fix(vision): use --clear flag for venv creation and add preflight check

- Use Python's --clear flag to handle existing venv directories atomically
- Disable "Start Training" button until hardware setup succeeds
- Show "Setup Failed" state on button when hardware detection fails
- Add "Retry Setup" button when setup fails

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2026-01-06 07:22:41 -06:00
parent e8d338b5e5
commit 17289c8a1c
2 changed files with 59 additions and 32 deletions

View File

@@ -117,14 +117,8 @@ async function createVenv(): Promise<SetupResult> {
console.log(`[vision-training] Creating venv with ${basePython}...`)
try {
// Remove incomplete venv if it exists
if (fs.existsSync(VENV_DIR)) {
console.log('[vision-training] Removing incomplete venv...')
fs.rmSync(VENV_DIR, { recursive: true, force: true })
}
// Create venv
await execAsync(`"${basePython}" -m venv "${VENV_DIR}"`, { timeout: 60000 })
// Create venv (--clear removes existing incomplete venv)
await execAsync(`"${basePython}" -m venv --clear "${VENV_DIR}"`, { timeout: 60000 })
// Upgrade pip
await execAsync(`"${TRAINING_PYTHON}" -m pip install --upgrade pip`, {

View File

@@ -74,28 +74,32 @@ export default function TrainModelPage() {
const eventSourceRef = useRef<EventSource | null>(null)
const logsEndRef = useRef<HTMLDivElement>(null)
// Fetch hardware info (also used for retry)
const fetchHardware = useCallback(async () => {
setHardwareLoading(true)
setHardwareInfo(null)
try {
const response = await fetch('/api/vision-training/hardware')
const data = await response.json()
setHardwareInfo(data)
} catch {
setHardwareInfo({
available: false,
device: 'unknown',
deviceName: 'Failed to detect',
deviceType: 'unknown',
details: {},
error: 'Failed to fetch hardware info',
})
} finally {
setHardwareLoading(false)
}
}, [])
// Fetch hardware info on mount
useEffect(() => {
async function fetchHardware() {
try {
const response = await fetch('/api/vision-training/hardware')
const data = await response.json()
setHardwareInfo(data)
} catch {
setHardwareInfo({
available: false,
device: 'unknown',
deviceName: 'Failed to detect',
deviceType: 'unknown',
details: {},
error: 'Failed to fetch hardware info',
})
} finally {
setHardwareLoading(false)
}
}
fetchHardware()
}, [])
}, [fetchHardware])
// Auto-scroll logs
useEffect(() => {
@@ -507,13 +511,36 @@ export default function TrainModelPage() {
{/* Error details */}
{hardwareInfo?.error && (
<div className={css({ mt: 2, fontSize: 'sm', color: 'red.300' })}>
{hardwareInfo.error}
<div className={css({ mt: 2 })}>
<p className={css({ fontSize: 'sm', color: 'red.300' })}>
{hardwareInfo.error}
</p>
{hardwareInfo.hint && (
<span className={css({ display: 'block', color: 'gray.400', mt: 1 })}>
<p className={css({ fontSize: 'sm', color: 'gray.400', mt: 1 })}>
Hint: {hardwareInfo.hint}
</span>
</p>
)}
<button
type="button"
onClick={fetchHardware}
disabled={hardwareLoading}
className={css({
mt: 2,
px: 3,
py: 1,
bg: 'blue.600',
color: 'white',
fontSize: 'sm',
fontWeight: 'medium',
borderRadius: 'md',
border: 'none',
cursor: 'pointer',
_hover: { bg: 'blue.700' },
_disabled: { opacity: 0.6, cursor: 'not-allowed' },
})}
>
{hardwareLoading ? 'Retrying...' : 'Retry Setup'}
</button>
</div>
)}
@@ -536,6 +563,7 @@ export default function TrainModelPage() {
<button
type="button"
onClick={startTraining}
disabled={hardwareLoading || !!hardwareInfo?.error}
className={css({
flex: 1,
px: 6,
@@ -548,9 +576,14 @@ export default function TrainModelPage() {
cursor: 'pointer',
fontSize: 'lg',
_hover: { bg: 'green.700' },
_disabled: {
bg: 'gray.600',
cursor: 'not-allowed',
opacity: 0.6,
},
})}
>
Start Training
{hardwareLoading ? 'Setting up...' : hardwareInfo?.error ? 'Setup Failed' : 'Start Training'}
</button>
)}