Skip to content

Commit 86ae868

Browse files
author
Enols
committed
feat(ui): add Python environment status card to DevicePage
- PythonEnvCard component showing python/pip/torch/ultralytics/cuda status - Colored badges (green=ok, red=missing) - Install Environment button when ready_for_training=false - Progress bar during async installation - Listens for python-env-progress and python-env-done events
1 parent 8ab0ef6 commit 86ae868

File tree

1 file changed

+173
-0
lines changed

1 file changed

+173
-0
lines changed

src/modules/yolo/pages/DevicePage.tsx

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,176 @@
11
import { useState, useEffect } from 'react';
2+
import { invoke } from '@tauri-apps/api/core';
3+
import { listen } from '@tauri-apps/api/event';
24
import { useSettingsStore, DeviceInfo } from '../../../core/stores/settingsStore';
35

6+
interface PythonEnvStatus {
7+
pythonAvailable: boolean;
8+
pythonVersion: string | null;
9+
torchAvailable: boolean;
10+
torchVersion: string | null;
11+
ultralyticsAvailable: boolean;
12+
ultralyticsVersion: string | null;
13+
cudaAvailable: boolean;
14+
ready_for_training: boolean;
15+
installing: boolean;
16+
}
17+
18+
interface InstallProgress {
19+
stage: string;
20+
message: string;
21+
progress: number | null;
22+
}
23+
24+
function PythonEnvCard() {
25+
const [envStatus, setEnvStatus] = useState<PythonEnvStatus | null>(null);
26+
const [progress, setProgress] = useState<InstallProgress | null>(null);
27+
const [isLoading, setIsLoading] = useState(false);
28+
29+
useEffect(() => {
30+
checkEnv();
31+
const unlistenProgress = listen<InstallProgress>('python-env-progress', (event) => {
32+
setProgress(event.payload);
33+
});
34+
const unlistenDone = listen<{ success: boolean; message: string }>('python-env-done', () => {
35+
setProgress(null);
36+
checkEnv();
37+
});
38+
return () => {
39+
unlistenProgress.then((fn) => fn());
40+
unlistenDone.then((fn) => fn());
41+
};
42+
}, []);
43+
44+
const checkEnv = async () => {
45+
setIsLoading(true);
46+
try {
47+
const status = await invoke<PythonEnvStatus>('python_env_status');
48+
setEnvStatus(status);
49+
} catch (e) {
50+
console.error('Failed to check Python env:', e);
51+
} finally {
52+
setIsLoading(false);
53+
}
54+
};
55+
56+
const installEnv = async () => {
57+
setIsLoading(true);
58+
try {
59+
await invoke('python_env_install');
60+
} catch (e) {
61+
console.error('Failed to install env:', e);
62+
}
63+
};
64+
65+
return (
66+
<div className="card" style={{ marginTop: 'var(--spacing-lg)' }}>
67+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 'var(--spacing-md)' }}>
68+
<h3 style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' }}>Python 环境</h3>
69+
<button
70+
onClick={checkEnv}
71+
disabled={isLoading}
72+
style={{
73+
padding: '4px 12px',
74+
fontSize: 12,
75+
background: 'var(--accent-primary)',
76+
color: '#fff',
77+
border: 'none',
78+
borderRadius: 'var(--radius-sm)',
79+
cursor: isLoading ? 'not-allowed' : 'pointer',
80+
opacity: isLoading ? 0.6 : 1,
81+
}}
82+
>
83+
{isLoading ? '检查中...' : '检查'}
84+
</button>
85+
</div>
86+
87+
{/* Badges */}
88+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 'var(--spacing-md)' }}>
89+
<span style={{
90+
padding: '2px 8px',
91+
fontSize: 11,
92+
borderRadius: 4,
93+
background: envStatus?.pythonAvailable ? 'rgba(34, 197, 94, 0.2)' : 'rgba(239, 68, 68, 0.2)',
94+
color: envStatus?.pythonAvailable ? '#22c55e' : '#ef4444',
95+
border: `1px solid ${envStatus?.pythonAvailable ? 'rgba(34, 197, 94, 0.3)' : 'rgba(239, 68, 68, 0.3)'}`,
96+
}}>
97+
Python {envStatus?.pythonVersion || 'N/A'}
98+
</span>
99+
<span style={{
100+
padding: '2px 8px',
101+
fontSize: 11,
102+
borderRadius: 4,
103+
background: envStatus?.torchAvailable ? 'rgba(34, 197, 94, 0.2)' : 'rgba(239, 68, 68, 0.2)',
104+
color: envStatus?.torchAvailable ? '#22c55e' : '#ef4444',
105+
border: `1px solid ${envStatus?.torchAvailable ? 'rgba(34, 197, 94, 0.3)' : 'rgba(239, 68, 68, 0.3)'}`,
106+
}}>
107+
PyTorch {envStatus?.torchVersion || 'N/A'}
108+
</span>
109+
<span style={{
110+
padding: '2px 8px',
111+
fontSize: 11,
112+
borderRadius: 4,
113+
background: envStatus?.ultralyticsAvailable ? 'rgba(34, 197, 94, 0.2)' : 'rgba(239, 68, 68, 0.2)',
114+
color: envStatus?.ultralyticsAvailable ? '#22c55e' : '#ef4444',
115+
border: `1px solid ${envStatus?.ultralyticsAvailable ? 'rgba(34, 197, 94, 0.3)' : 'rgba(239, 68, 68, 0.3)'}`,
116+
}}>
117+
Ultralytics {envStatus?.ultralyticsVersion || 'N/A'}
118+
</span>
119+
<span style={{
120+
padding: '2px 8px',
121+
fontSize: 11,
122+
borderRadius: 4,
123+
background: envStatus?.cudaAvailable ? 'rgba(34, 197, 94, 0.2)' : 'rgba(239, 68, 68, 0.2)',
124+
color: envStatus?.cudaAvailable ? '#22c55e' : '#ef4444',
125+
border: `1px solid ${envStatus?.cudaAvailable ? 'rgba(34, 197, 94, 0.3)' : 'rgba(239, 68, 68, 0.3)'}`,
126+
}}>
127+
CUDA {envStatus?.cudaAvailable ? '可用' : '不可用'}
128+
</span>
129+
</div>
130+
131+
{/* Progress bar */}
132+
{progress && (
133+
<div style={{ marginTop: 'var(--spacing-md)' }}>
134+
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 4 }}>
135+
<span style={{ color: 'var(--text-secondary)' }}>{progress.message}</span>
136+
<span style={{ color: 'var(--accent-primary)' }}>
137+
{progress.progress !== null ? `${Math.round(progress.progress * 100)}%` : ''}
138+
</span>
139+
</div>
140+
<div style={{ height: 4, background: 'var(--bg-elevated)', borderRadius: 2 }}>
141+
<div style={{
142+
height: '100%',
143+
width: progress.progress !== null ? `${progress.progress * 100}%` : '0%',
144+
background: 'var(--accent-primary)',
145+
borderRadius: 2,
146+
transition: 'width 0.3s',
147+
}} />
148+
</div>
149+
</div>
150+
)}
151+
152+
{/* Install button */}
153+
{envStatus && !envStatus.ready_for_training && !envStatus.installing && (
154+
<button
155+
onClick={installEnv}
156+
style={{
157+
marginTop: 'var(--spacing-md)',
158+
padding: '8px 16px',
159+
fontSize: 13,
160+
background: 'var(--status-success)',
161+
color: '#fff',
162+
border: 'none',
163+
borderRadius: 'var(--radius-sm)',
164+
cursor: 'pointer',
165+
}}
166+
>
167+
安装环境
168+
</button>
169+
)}
170+
</div>
171+
);
172+
}
173+
4174
export default function DevicePage() {
5175
const { devices, loadDevices } = useSettingsStore();
6176
const [selectedDeviceId, setSelectedDeviceId] = useState<number>(0);
@@ -64,6 +234,7 @@ export default function DevicePage() {
64234
) : (
65235
<CPUDetail device={selected} formatBytes={formatBytes} getMemoryUtilization={getMemoryUtilization} />
66236
)}
237+
<PythonEnvCard />
67238
</div>
68239
)}
69240
</div>
@@ -137,6 +308,8 @@ function GPUDetail({ device, formatBytes, getMemoryUtilization }: GPUDetailProps
137308
<div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 4 }}>可用显存</div>
138309
</div>
139310
</div>
311+
312+
<PythonEnvCard />
140313
</div>
141314
);
142315
}

0 commit comments

Comments
 (0)