// ─── Create instance screen ───
function CreateScreen({ setScreen, toast, instance, setInstance }) {
const [loading, setLoading] = React.useState(false);
async function go() {
setLoading(true);
try {
const data = await apiRequest('/api/instance', { method: 'POST' });
setInstance(data);
toast('云端电脑创建中...');
setScreen('booting');
} catch (ex) {
if (ex.code === 'err_instance_exists') {
try {
const data = await apiRequest('/api/instance');
setInstance(data);
setScreen(data.availability === 'ready' ? 'dashboard' : 'booting');
} catch { /* ignore */ }
} else {
toast(errText(ex.code || ex.message), 'error');
}
} finally {
setLoading(false);
}
}
return (
创建你的 OpenClaw 云端电脑
{'>'} 点击按钮,我们将启动一个专属容器
{'>'} 预计耗时 1~2 分钟
独立容器
专属工作空间
{'// 使用邀请制 · 无需信用卡'}
);
}
window.CreateScreen = CreateScreen;
// ─── Booting screen ─── Poll /api/instance until availability === 'ready'
function BootingScreen({ setScreen, instance, setInstance, toast }) {
// Progress is approximate — derived from elapsed time (real provisioning takes 1-2 min).
const [progress, setProgress] = React.useState(12);
const [step, setStep] = React.useState(1);
const [logs, setLogs] = React.useState([
{ t: '00:00', msg: 'requesting container image', ok: true },
]);
React.useEffect(() => {
let pollToken = 0;
let progressIv = null;
const myToken = ++pollToken;
// Fake progress bar (real boot status is binary: loading vs ready)
progressIv = setInterval(() => {
setProgress(p => Math.min(95, p + 2 + Math.random() * 3));
}, 700);
async function poll() {
if (myToken !== pollToken) return;
try {
const data = await apiRequest('/api/instance');
if (myToken !== pollToken) return;
setInstance(data);
// Step progression based on status
if (data.status === 'running' && step < 2) setStep(2);
if (data.health_status === 'healthy' && step < 3) setStep(3);
// Add log lines
setLogs(l => {
const last = l[l.length - 1];
const msg = `status=${data.status}${data.health_status ? ' health=' + data.health_status : ''}`;
if (last && last.msg === msg) return l;
const t = String(l.length * 3).padStart(2, '0');
return [...l, { t: `00:${t}`, msg, ok: data.status !== 'error' }];
});
if (data.availability === 'ready') {
setProgress(100);
clearInterval(progressIv);
toast('云端电脑已就绪');
setTimeout(() => setScreen('dashboard'), 400);
return;
}
if (data.status === 'error') {
clearInterval(progressIv);
toast('启动失败', 'error');
return;
}
} catch (ex) {
if (ex.code === 'err_instance_not_found') {
clearInterval(progressIv);
setScreen('create');
return;
}
}
setTimeout(poll, 2400);
}
setTimeout(poll, 800);
return () => {
pollToken++;
clearInterval(progressIv);
};
}, []); // eslint-disable-line
const availability = instance?.availability || 'loading';
return (
bootstrap.log
BOOTING · {Math.floor(progress)}%
= 1 ? 'done' : '')}>
✓
容器已创建
= 2 ? 'done' : '')}/>
= 2 ? (step >= 3 ? 'done' : 'active') : '')}>
{step >= 3 ? '✓' : '·'}
系统初始化
= 3 ? 'done' : '')}/>
{availability === 'ready' ? '✓' : '·'}
健康检查
STATUS: {instance?.status || 'provisioning'}
{Math.floor(progress)}% · ETA {Math.max(5, 90 - Math.floor(progress * 0.9))}s
{logs.map((l, i) => (
[{l.t}] {l.ok ? '✓' : '✗'} {l.msg}
))}
[00:{logs.length*3}] ▸ {'waiting for ready...'}
);
}
window.BootingScreen = BootingScreen;