import { useCallback, useEffect, useState } from "react"; import { Icon } from "../components/icon"; import { createToolType, deleteToolType, listToolTypes, updateToolType, validateToolType, type CreateToolTypeRequest, type ReadinessProbe, type ToolType, type UpdateToolTypeRequest, } from "../api/tool_types"; import { createToolConfig, deleteToolConfig, getToolConfigDefaults, listToolConfigs, updateToolConfig, type CreateToolConfigRequest, type ToolConfig, } from "../api/tool_configs"; import { createConfigFolder, deleteConfigFolder, listConfigFolders, updateConfigFolder, type ConfigFolder, type CreateConfigFolderRequest, type UpdateConfigFolderRequest, } from "../api/config_folders"; type Tab = "types" | "configs" | "folders"; type Status = "loading" | "ready" | "error"; export const ToolWorkshopPage = () => { const [activeTab, setActiveTab] = useState("types"); const [status, setStatus] = useState("loading"); const [toolTypes, setToolTypes] = useState([]); const [configs, setConfigs] = useState([]); const [folders, setFolders] = useState([]); // Tool Type state const [selectedToolType, setSelectedToolType] = useState(null); const [toolTypeForm, setToolTypeForm] = useState({ name: "", display_name: "", description: "", category: "", interfaces: [] as string[], default_port: "", definition_type: "compose" as "compose" | "dockerfile", compose_template: "", dockerfile_template: "", readiness_command: "", readiness_timeout: "30", readiness_interval: "2", required_variables: "", }); const [toolTypeError, setToolTypeError] = useState(null); const [showToolTypeForm, setShowToolTypeForm] = useState(false); // Config state const [selectedConfig, setSelectedConfig] = useState(null); const [configForm, setConfigForm] = useState({ tool_type_id: "", key: "", value: "", config_type: "env", file_path: "", port_override: "", start_command: "", working_directory: "", env_vars_json: "{}", volumes_json: "[]", }); const [configError, setConfigError] = useState(null); const [showConfigForm, setShowConfigForm] = useState(false); // Folder state const [selectedFolder, setSelectedFolder] = useState(null); const [folderForm, setFolderForm] = useState({ name: "", description: "", mount_path: "/home/user", files_json: "{}", is_active: true, }); const [folderError, setFolderError] = useState(null); const [showFolderForm, setShowFolderForm] = useState(false); const loadData = useCallback(async () => { setStatus("loading"); try { const [types, cfgs, fldrs] = await Promise.all([ listToolTypes(), listToolConfigs(), listConfigFolders(), ]); setToolTypes(types); setConfigs(cfgs); setFolders(fldrs); setStatus("ready"); } catch { setStatus("error"); } }, []); useEffect(() => { void loadData(); }, [loadData]); // Tool Type handlers const openCreateToolType = () => { setToolTypeForm({ name: "", display_name: "", description: "", category: "", interfaces: [], default_port: "", definition_type: "compose", compose_template: "", dockerfile_template: "", readiness_command: "", readiness_timeout: "30", readiness_interval: "2", required_variables: "", }); setToolTypeError(null); setShowToolTypeForm(true); setSelectedToolType(null); }; const openEditToolType = (toolType: ToolType) => { setToolTypeForm({ name: toolType.name, display_name: toolType.display_name, description: toolType.description || "", category: toolType.category || "", interfaces: toolType.interfaces || [], default_port: toolType.default_port?.toString() || "", definition_type: toolType.definition_type || "compose", compose_template: toolType.compose_template || "", dockerfile_template: toolType.dockerfile_template || "", readiness_command: toolType.readiness_probe?.command || "", readiness_timeout: toolType.readiness_probe?.timeout?.toString() || "30", readiness_interval: toolType.readiness_probe?.interval?.toString() || "2", required_variables: toolType.required_variables?.join(", ") || "", }); setToolTypeError(null); setShowToolTypeForm(true); setSelectedToolType(toolType); }; const handleToolTypeSubmit = async (e: React.FormEvent) => { e.preventDefault(); setToolTypeError(null); if (!toolTypeForm.name.trim() || !toolTypeForm.display_name.trim()) { setToolTypeError("Name and display name are required"); return; } if (!toolTypeForm.default_port.trim() || isNaN(Number(toolTypeForm.default_port))) { setToolTypeError("Default port is required and must be a number"); return; } const template = toolTypeForm.definition_type === "compose" ? toolTypeForm.compose_template : toolTypeForm.dockerfile_template; if (!template.trim()) { setToolTypeError(`${toolTypeForm.definition_type === "compose" ? "Compose" : "Dockerfile"} template is required`); return; } const variables = toolTypeForm.required_variables .split(",") .map((v) => v.trim()) .filter((v) => v.length > 0); const readinessProbe: ReadinessProbe | undefined = toolTypeForm.readiness_command.trim() ? { command: toolTypeForm.readiness_command.trim(), timeout: parseInt(toolTypeForm.readiness_timeout) || 30, interval: parseInt(toolTypeForm.readiness_interval) || 2, } : undefined; try { if (selectedToolType) { const input: UpdateToolTypeRequest = { display_name: toolTypeForm.display_name.trim(), description: toolTypeForm.description.trim() || undefined, category: toolTypeForm.category.trim() || undefined, interfaces: toolTypeForm.interfaces.length > 0 ? toolTypeForm.interfaces : undefined, default_port: Number(toolTypeForm.default_port), definition_type: toolTypeForm.definition_type, compose_template: toolTypeForm.definition_type === "compose" ? template : undefined, dockerfile_template: toolTypeForm.definition_type === "dockerfile" ? template : undefined, readiness_probe: readinessProbe, required_variables: variables, }; await updateToolType(selectedToolType.id, input); } else { const input: CreateToolTypeRequest = { name: toolTypeForm.name.trim(), display_name: toolTypeForm.display_name.trim(), description: toolTypeForm.description.trim() || undefined, category: toolTypeForm.category.trim() || undefined, interfaces: toolTypeForm.interfaces.length > 0 ? toolTypeForm.interfaces : undefined, default_port: Number(toolTypeForm.default_port), definition_type: toolTypeForm.definition_type, compose_template: toolTypeForm.definition_type === "compose" ? template : undefined, dockerfile_template: toolTypeForm.definition_type === "dockerfile" ? template : undefined, readiness_probe: readinessProbe, required_variables: variables, }; await createToolType(input); } setShowToolTypeForm(false); setSelectedToolType(null); await loadData(); } catch (err) { const axiosError = err as { response?: { data?: { detail?: string } } }; setToolTypeError(axiosError?.response?.data?.detail || "Failed to save tool type"); } }; const handleDeleteToolType = async (id: string) => { if (!window.confirm("Delete this tool type? All associated configs will be removed.")) return; try { await deleteToolType(id); await loadData(); } catch { alert("Failed to delete tool type"); } }; // Config handlers const openCreateConfig = () => { setConfigForm({ tool_type_id: toolTypes[0]?.id || "", key: "", value: "", config_type: "env", file_path: "", port_override: "", start_command: "", working_directory: "", env_vars_json: "{}", volumes_json: "[]", }); setConfigError(null); setShowConfigForm(true); setSelectedConfig(null); }; const openEditConfig = (config: ToolConfig) => { setConfigForm({ tool_type_id: config.tool_type_id, key: config.key, value: config.value, config_type: config.config_type, file_path: config.file_path || "", port_override: config.port_override?.toString() || "", start_command: config.start_command || "", working_directory: config.working_directory || "", env_vars_json: config.environment_variables ? JSON.stringify(config.environment_variables, null, 2) : "{}", volumes_json: config.volumes ? JSON.stringify(config.volumes, null, 2) : "[]", }); setConfigError(null); setShowConfigForm(true); setSelectedConfig(config); }; const handleConfigSubmit = async (e: React.FormEvent) => { e.preventDefault(); setConfigError(null); if (!configForm.tool_type_id || !configForm.key.trim()) { setConfigError("Tool type and key are required"); return; } // Validate JSON fields let envVars: Record | undefined; let volumes: Array<{ source: string; target: string; type?: string }> | undefined; try { if (configForm.env_vars_json.trim() && configForm.env_vars_json.trim() !== "{}") { envVars = JSON.parse(configForm.env_vars_json); } } catch { setConfigError("Environment variables must be valid JSON"); return; } try { if (configForm.volumes_json.trim() && configForm.volumes_json.trim() !== "[]") { volumes = JSON.parse(configForm.volumes_json); } } catch { setConfigError("Volumes must be valid JSON array"); return; } const data: CreateToolConfigRequest = { tool_type_id: configForm.tool_type_id, key: configForm.key.trim(), value: configForm.value, config_type: configForm.config_type, file_path: configForm.config_type === "file" ? configForm.file_path : undefined, port_override: configForm.port_override ? Number(configForm.port_override) : undefined, start_command: configForm.start_command.trim() || undefined, working_directory: configForm.working_directory.trim() || undefined, environment_variables: envVars, volumes, }; try { if (selectedConfig) { await updateToolConfig(selectedConfig.id, data); } else { await createToolConfig(data); } setShowConfigForm(false); setSelectedConfig(null); await loadData(); } catch (err) { const axiosError = err as { response?: { data?: { detail?: string } } }; setConfigError(axiosError?.response?.data?.detail || "Failed to save config"); } }; const handleDeleteConfig = async (id: string) => { if (!window.confirm("Delete this config?")) return; try { await deleteToolConfig(id); await loadData(); } catch { alert("Failed to delete config"); } }; // Folder handlers const openCreateFolder = () => { setFolderForm({ name: "", description: "", mount_path: "/home/user", files_json: "{}", is_active: true, }); setFolderError(null); setShowFolderForm(true); setSelectedFolder(null); }; const openEditFolder = (folder: ConfigFolder) => { setFolderForm({ name: folder.name, description: folder.description || "", mount_path: folder.mount_path, files_json: JSON.stringify(folder.files, null, 2), is_active: folder.is_active, }); setFolderError(null); setShowFolderForm(true); setSelectedFolder(folder); }; const handleFolderSubmit = async (e: React.FormEvent) => { e.preventDefault(); setFolderError(null); if (!folderForm.name.trim() || !folderForm.mount_path.trim()) { setFolderError("Name and mount path are required"); return; } let files: Record | undefined; try { if (folderForm.files_json.trim() && folderForm.files_json.trim() !== "{}") { files = JSON.parse(folderForm.files_json); } } catch { setFolderError("Files must be valid JSON object"); return; } const data: CreateConfigFolderRequest | UpdateConfigFolderRequest = { name: folderForm.name.trim(), description: folderForm.description.trim() || undefined, mount_path: folderForm.mount_path.trim(), files, is_active: folderForm.is_active, }; try { if (selectedFolder) { await updateConfigFolder(selectedFolder.id, data); } else { await createConfigFolder(data as CreateConfigFolderRequest); } setShowFolderForm(false); setSelectedFolder(null); await loadData(); } catch (err) { const axiosError = err as { response?: { data?: { detail?: string } } }; setFolderError(axiosError?.response?.data?.detail || "Failed to save folder"); } }; const handleDeleteFolder = async (id: string) => { if (!window.confirm("Delete this config folder?")) return; try { await deleteConfigFolder(id); await loadData(); } catch { alert("Failed to delete folder"); } }; if (status === "loading") { return (

Loading Tool Workshop...

); } if (status === "error") { return (

Failed to load Tool Workshop.

); } return (

Tool Workshop

{/* Tabs */}
{(["types", "configs", "folders"] as Tab[]).map((tab) => ( ))}
{/* Tool Types Tab */} {activeTab === "types" && (

Tool Types

{showToolTypeForm && (

{selectedToolType ? "Edit" : "Create"} Tool Type

setToolTypeForm({ ...toolTypeForm, name: e.target.value })} disabled={!!selectedToolType} placeholder="e.g., code-server" className="form-input" required />
setToolTypeForm({ ...toolTypeForm, display_name: e.target.value })} placeholder="e.g., VS Code Server" className="form-input" required />
setToolTypeForm({ ...toolTypeForm, description: e.target.value })} placeholder="Optional description" className="form-input" />
setToolTypeForm({ ...toolTypeForm, category: e.target.value })} placeholder="e.g., editor, notebook, ai-assistant" className="form-input" />
{["web", "terminal"].map((iface) => ( ))}
setToolTypeForm({ ...toolTypeForm, default_port: e.target.value })} placeholder="e.g., 8443" className="form-input" required />