import { useCallback, useEffect, useState } from "react"; import { extractErrorMessage } from "../utils/errors"; import { createToolType, deleteToolType, listToolTypes, updateToolType, type CreateToolTypeRequest, type ReadinessProbe, type ToolType, type UpdateToolTypeRequest, } from "../api/tool-types"; import { createToolDefinition, getToolDefinition, listToolDefinitions, updateToolDefinition, } from "../api/tool-definitions"; import type { ToolTypeFormState } from "../components/features/tool-workshop/ToolTypeEditorPanel"; type Status = "loading" | "ready" | "error"; const defaultForm: ToolTypeFormState = { name: "", display_name: "", description: "", category: "", interface_type: "web", requires_port: true, default_port: "", definition_type: "compose", compose_template: "", dockerfile_template: "", readiness_command: "", readiness_timeout: "30", readiness_interval: "2", required_variables: "", startup_command: "", }; export const useToolWorkshop = () => { const [status, setStatus] = useState("loading"); const [toolTypes, setToolTypes] = useState([]); const [baseDefinitions, setBaseDefinitions] = useState< Awaited> >([]); const [manifestData, setManifestData] = useState | null>(null); const [manifestDefinitionId, setManifestDefinitionId] = useState< string | null >(null); const [selectedToolTypeId, setSelectedToolTypeId] = useState( null, ); const [isCreating, setIsCreating] = useState(false); const [toolTypeForm, setToolTypeForm] = useState(defaultForm); const [toolTypeError, setToolTypeError] = useState(null); const [toolTypeDirty, setToolTypeDirty] = useState(false); const selectedToolType = (toolTypes || []).find((t) => t.id === selectedToolTypeId) || null; const loadData = useCallback(async () => { setStatus("loading"); try { const [types, defs] = await Promise.all([ listToolTypes(), listToolDefinitions(), ]); setToolTypes(types || []); setBaseDefinitions((defs || []).filter((d) => d.is_base)); setStatus("ready"); } catch { setStatus("error"); } }, []); useEffect(() => { void loadData(); }, [loadData]); const resetToolTypeForm = () => { setToolTypeForm(defaultForm); setToolTypeError(null); setToolTypeDirty(false); setManifestData(null); setManifestDefinitionId(null); }; const populateToolTypeForm = async (toolType: ToolType) => { setToolTypeForm({ name: toolType.name, display_name: toolType.display_name, description: toolType.description || "", category: toolType.category || "", interface_type: (toolType.interface_type as "web" | "terminal") || "web", requires_port: toolType.requires_port ?? true, default_port: toolType.default_port?.toString() || "", definition_type: (toolType.definition_type as "compose" | "dockerfile" | "manifest") || "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(", ") || "", startup_command: toolType.startup_command || "", }); setToolTypeError(null); setToolTypeDirty(false); setManifestDefinitionId(toolType.manifest_id || null); if (toolType.definition_type === "manifest" && toolType.manifest_id) { try { const defn = await getToolDefinition(toolType.manifest_id); setManifestData(defn.manifest); } catch { setManifestData(null); } } else { setManifestData(null); } }; const handleSelectToolType = (toolType: ToolType | null) => { if (toolTypeDirty) { if (!window.confirm("You have unsaved changes. Discard them?")) { return; } } if (toolType) { setSelectedToolTypeId(toolType.id); setIsCreating(false); void populateToolTypeForm(toolType); } else { setSelectedToolTypeId(null); } }; const handleCreateNew = () => { if (toolTypeDirty) { if (!window.confirm("You have unsaved changes. Discard them?")) { return; } } setSelectedToolTypeId(null); setIsCreating(true); resetToolTypeForm(); }; const handleToolTypeSubmit = async (e?: React.FormEvent): Promise => { e?.preventDefault(); setToolTypeError(null); if (!toolTypeForm.name.trim() || !toolTypeForm.display_name.trim()) { setToolTypeError("Name and display name are required"); return false; } if ( toolTypeForm.requires_port && (!toolTypeForm.default_port.trim() || isNaN(Number(toolTypeForm.default_port))) ) { setToolTypeError("Default port is required and must be a number"); return false; } if (toolTypeForm.definition_type !== "manifest") { 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 false; } } else if (!manifestData) { setToolTypeError( "Manifest data is required for manifest definition type", ); return false; } 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; const template = toolTypeForm.definition_type === "compose" ? toolTypeForm.compose_template : toolTypeForm.dockerfile_template; try { if (isCreating) { let manifestId: string | undefined; if (toolTypeForm.definition_type === "manifest" && manifestData) { const manifestPayload = { name: toolTypeForm.name.trim(), display_name: toolTypeForm.display_name.trim(), description: toolTypeForm.description.trim() || undefined, category: toolTypeForm.category.trim() || undefined, interface_type: toolTypeForm.interface_type, base_image: (manifestData.base_image as string) || undefined, base_definition_id: (manifestData.base_definition_id as string) || undefined, manifest: manifestData, }; const newManifest = await createToolDefinition(manifestPayload); manifestId = newManifest.id; } const input: CreateToolTypeRequest = { name: toolTypeForm.name.trim(), display_name: toolTypeForm.display_name.trim(), description: toolTypeForm.description.trim() || undefined, category: toolTypeForm.category.trim() || undefined, interface_type: toolTypeForm.interface_type, requires_port: toolTypeForm.requires_port, default_port: toolTypeForm.requires_port ? Number(toolTypeForm.default_port) : 0, definition_type: toolTypeForm.definition_type, manifest_id: manifestId, compose_template: toolTypeForm.definition_type === "compose" ? template : undefined, dockerfile_template: toolTypeForm.definition_type === "dockerfile" ? template : undefined, readiness_probe: readinessProbe, required_variables: variables, startup_command: toolTypeForm.startup_command.trim() || undefined, }; const newTool = await createToolType(input); setIsCreating(false); setSelectedToolTypeId(newTool.id); setToolTypeDirty(false); } else if (selectedToolType) { let manifestId = selectedToolType.manifest_id || undefined; if (toolTypeForm.definition_type === "manifest" && manifestData) { if (manifestId) { await updateToolDefinition(manifestId, { display_name: toolTypeForm.display_name.trim(), description: toolTypeForm.description.trim() || undefined, category: toolTypeForm.category.trim() || undefined, manifest: manifestData, }); } else { const manifestPayload = { name: toolTypeForm.name.trim(), display_name: toolTypeForm.display_name.trim(), description: toolTypeForm.description.trim() || undefined, category: toolTypeForm.category.trim() || undefined, interface_type: toolTypeForm.interface_type, base_image: (manifestData.base_image as string) || undefined, base_definition_id: (manifestData.base_definition_id as string) || undefined, manifest: manifestData, }; const newManifest = await createToolDefinition(manifestPayload); manifestId = newManifest.id; } } const input: UpdateToolTypeRequest = { display_name: toolTypeForm.display_name.trim(), description: toolTypeForm.description.trim() || undefined, category: toolTypeForm.category.trim() || undefined, interface_type: toolTypeForm.interface_type, requires_port: toolTypeForm.requires_port, default_port: toolTypeForm.requires_port ? Number(toolTypeForm.default_port) : 0, definition_type: toolTypeForm.definition_type, manifest_id: toolTypeForm.definition_type === "manifest" ? manifestId : undefined, compose_template: toolTypeForm.definition_type === "compose" ? template : undefined, dockerfile_template: toolTypeForm.definition_type === "dockerfile" ? template : undefined, readiness_probe: readinessProbe, required_variables: variables, startup_command: toolTypeForm.startup_command.trim() || undefined, }; await updateToolType(selectedToolType.id, input); setToolTypeDirty(false); } await loadData(); return true; } catch (err) { setToolTypeError(extractErrorMessage(err)); return false; } }; const handleDeleteToolType = async (id: string) => { if ( !window.confirm( "Delete this tool type? All associated configs will be removed.", ) ) return; try { await deleteToolType(id); if (selectedToolTypeId === id) { setSelectedToolTypeId(null); setIsCreating(false); resetToolTypeForm(); } await loadData(); } catch { alert("Failed to delete tool type"); } }; const handleFormChange = (changes: Partial) => { setToolTypeForm((prev) => ({ ...prev, ...changes })); setToolTypeDirty(true); }; const handleReset = () => { if (isCreating) { resetToolTypeForm(); } else if (selectedToolType) { void populateToolTypeForm(selectedToolType); } }; return { status, toolTypes, baseDefinitions, selectedToolType, selectedToolTypeId, isCreating, toolTypeForm, manifestData, manifestDefinitionId, toolTypeError, toolTypeDirty, loadData, handleSelectToolType, handleCreateNew, handleToolTypeSubmit, handleDeleteToolType, handleFormChange, handleReset, setManifestData, setToolTypeDirty, }; };