WitNote / dashboard /src /components /molecules /StartInstanceModal.tsx
AUXteam's picture
Upload folder using huggingface_hub
6a7089a verified
import { useEffect, useMemo, useState } from "react";
import { Button, Input, Modal } from "../atoms";
import { useAppStore } from "../../stores/useAppStore";
import * as api from "../../services/api";
import type { LaunchInstanceRequest, Profile } from "../../generated/types";
interface Props {
open: boolean;
profile: Profile | null;
onClose: () => void;
}
export default function StartInstanceModal({ open, profile, onClose }: Props) {
const { setInstances } = useAppStore();
const [port, setPort] = useState("");
const [headless, setHeadless] = useState(false);
const [launchError, setLaunchError] = useState("");
const [launchLoading, setLaunchLoading] = useState(false);
const [copyFeedback, setCopyFeedback] = useState("");
useEffect(() => {
if (open) {
setLaunchError("");
setCopyFeedback("");
return;
}
setPort("");
setHeadless(false);
setLaunchError("");
setLaunchLoading(false);
setCopyFeedback("");
}, [open, profile?.id, profile?.name]);
const launchCommand = useMemo(() => {
if (!profile) return "";
const payload: LaunchInstanceRequest = {
profileId: profile.id || profile.name,
mode: headless ? undefined : "headed",
port: port.trim() || undefined,
};
return `curl -X POST http://localhost:9867/instances/start -H "Content-Type: application/json" -d '${JSON.stringify(payload)}'`;
}, [headless, port, profile]);
const handleLaunch = async () => {
if (!profile || launchLoading) return;
setLaunchError("");
setLaunchLoading(true);
try {
const payload: LaunchInstanceRequest = {
name: profile.name,
port: port.trim() || undefined,
mode: headless ? undefined : "headed",
};
await api.launchInstance(payload);
const updated = await api.fetchInstances();
setInstances(updated);
onClose();
} catch (e) {
console.error("Launch failed:", e);
const msg = e instanceof Error ? e.message : "Failed to launch instance";
setLaunchError(msg);
} finally {
setLaunchLoading(false);
}
};
const handleCopyCommand = async () => {
try {
await navigator.clipboard.writeText(launchCommand);
setCopyFeedback("Copied!");
setTimeout(() => setCopyFeedback(""), 2000);
} catch {
setCopyFeedback("Failed to copy");
setTimeout(() => setCopyFeedback(""), 2000);
}
};
return (
<Modal
open={open}
onClose={onClose}
title="🖥️ Start Profile"
actions={
<>
<Button
variant="secondary"
disabled={launchLoading}
onClick={onClose}
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleLaunch}
loading={launchLoading}
>
Start
</Button>
</>
}
>
<div className="flex flex-col gap-4">
{launchError && (
<div className="rounded border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{launchError}
</div>
)}
<Input
label="Port"
placeholder="Auto-select from configured range"
value={port}
onChange={(e) => setPort(e.target.value)}
/>
<p className="-mt-2 text-xs text-text-muted">
Leave blank to auto-select a free port from the configured instance
port range.
</p>
<label className="flex items-center gap-2 text-sm text-text-secondary">
<input
type="checkbox"
checked={headless}
onChange={(e) => setHeadless(e.target.checked)}
className="h-4 w-4"
/>
Headless (best for Docker/VPS)
</label>
<div>
<label className="mb-1 block text-xs text-text-muted">
Direct launch command (backup)
</label>
<textarea
readOnly
value={launchCommand}
className="h-20 w-full resize-none rounded border border-border-subtle bg-bg-elevated px-3 py-2 font-mono text-xs text-text-secondary"
/>
<div className="mt-2 flex items-center gap-2">
<Button size="sm" variant="secondary" onClick={handleCopyCommand}>
Copy Command
</Button>
{copyFeedback && (
<span className="text-xs text-success">{copyFeedback}</span>
)}
</div>
</div>
</div>
</Modal>
);
}