import { useState, useEffect, useRef, useCallback } from "react"; import { api } from "../services/api"; import { useWebSocket } from "../hooks/useWebSocket"; import { Hammer, Package, Cpu, Play, Archive, Upload, ChevronDown, ChevronUp, AlertTriangle, } from "lucide-react"; type BuildStatus = "idle" | "building" | "success" | "failed"; interface BuildSectionState { status: BuildStatus; output: string[]; collapsed: boolean; } function StatusBadge({ status }: { status: BuildStatus }) { const styles: Record = { idle: { backgroundColor: "var(--forge-surface-raised)", color: "var(--forge-text-secondary)", }, building: { backgroundColor: "var(--forge-warning-bg)", color: "var(--forge-warning)", }, success: { backgroundColor: "var(--forge-success-bg)", color: "var(--forge-success)", }, failed: { backgroundColor: "var(--forge-danger-bg)", color: "var(--forge-danger)", }, }; return ( {status} ); } function BuildOutput({ lines, collapsed, onToggle, }: { lines: string[]; collapsed: boolean; onToggle: () => void; }) { const scrollRef = useRef(null); useEffect(() => { if (!collapsed && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [lines, collapsed]); return (
{!collapsed && (
{lines.length === 0 ? ( No output yet ) : ( lines.map((line, i) => (
{line}
)) )}
)}
); } function ActionButton({ label, onClick, disabled, variant = "default", icon, }: { label: string; onClick: () => void; disabled?: boolean; variant?: "default" | "primary" | "warning"; icon?: React.ReactNode; }) { const variantStyles: Record = { default: { backgroundColor: "var(--forge-surface-raised)", border: "1px solid var(--forge-border)", color: "var(--forge-text)", }, primary: { backgroundColor: "var(--forge-accent)", border: "none", color: "var(--forge-accent-text)", }, warning: { backgroundColor: "var(--forge-warning-bg)", border: "1px solid var(--forge-warning-border)", color: "var(--forge-warning)", }, }; return ( ); } export function Build() { const { subscribe } = useWebSocket(); const [module, setModule] = useState({ status: "idle", output: [], collapsed: true, }); const [haks, setHaks] = useState({ status: "idle", output: [], collapsed: true, }); const [nwnx, setNwnx] = useState({ status: "idle", output: [], collapsed: true, }); const [nwnxTarget, setNwnxTarget] = useState(""); useEffect(() => { const unsubs = [ subscribe("build:start", (event) => { const data = event.data as Record; const section = data?.section; const setter = section === "haks" ? setHaks : section === "nwnx" ? setNwnx : setModule; setter((prev) => ({ ...prev, status: "building", output: [], collapsed: false })); }), subscribe("build:complete", (event) => { const data = event.data as Record; const section = data?.section; const setter = section === "haks" ? setHaks : section === "nwnx" ? setNwnx : setModule; setter((prev) => ({ ...prev, status: "success" })); }), subscribe("build:failed", (event) => { const data = event.data as Record; const section = data?.section; const setter = section === "haks" ? setHaks : section === "nwnx" ? setNwnx : setModule; setter((prev) => ({ ...prev, status: "failed" })); }), subscribe("build:output", (event) => { const data = event.data as { text?: string; section?: string }; if (!data?.text) return; const section = data.section; const setter = section === "haks" ? setHaks : section === "nwnx" ? setNwnx : setModule; setter((prev) => ({ ...prev, output: [...prev.output, ...data.text!.split("\n").filter(Boolean)], })); }), ]; return () => unsubs.forEach((u) => u()); }, [subscribe]); const handleAction = useCallback( async (action: () => Promise, section: "module" | "haks" | "nwnx") => { const setter = section === "haks" ? setHaks : section === "nwnx" ? setNwnx : setModule; setter((prev) => ({ ...prev, status: "building", output: [], collapsed: false })); try { await action(); } catch { setter((prev) => ({ ...prev, status: "failed" })); } }, [], ); const isBuilding = module.status === "building" || haks.status === "building" || nwnx.status === "building"; const cardStyle: React.CSSProperties = { backgroundColor: "var(--forge-surface)", border: "1px solid var(--forge-border)", borderRadius: "0.75rem", padding: "1.25rem", marginBottom: "1rem", }; const sectionHeaderStyle: React.CSSProperties = { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "1rem", }; const sectionTitleStyle: React.CSSProperties = { display: "flex", alignItems: "center", gap: "0.5rem", textTransform: "uppercase", fontSize: "var(--text-xs)", fontWeight: 600, letterSpacing: "0.05em", color: "var(--forge-text-secondary)", fontFamily: "var(--font-heading)", }; const buttonRowStyle: React.CSSProperties = { display: "flex", flexWrap: "wrap", gap: "0.5rem", alignItems: "center", }; return (

Build Pipeline

Compile, pack, and deploy module resources

{/* Module Section */}
Module
} disabled={isBuilding} onClick={() => handleAction(() => api.build.compileModule(), "module")} /> } disabled={isBuilding} onClick={() => handleAction(() => api.build.packModule(), "module")} /> } disabled={isBuilding} onClick={() => handleAction(() => api.build.deploy(), "module")} />
setModule((prev) => ({ ...prev, collapsed: !prev.collapsed }))} />
{/* Haks Section */}
Haks
} disabled={isBuilding} onClick={() => handleAction(() => api.build.buildHaks(), "haks")} />
setHaks((prev) => ({ ...prev, collapsed: !prev.collapsed }))} />
{/* NWNX Section */}
NWNX (Advanced)
} disabled={isBuilding} onClick={() => handleAction(() => api.build.buildNwnx(), "nwnx")} />
setNwnxTarget(e.target.value)} placeholder="Target (e.g. Item, Creature)" style={{ backgroundColor: "var(--forge-bg)", border: "1px solid var(--forge-border)", borderRadius: "0.375rem", padding: "0.5rem 0.75rem", fontSize: "var(--text-sm)", color: "var(--forge-text)", fontFamily: "var(--font-sans)", outline: "none", flex: "0 1 16rem", }} /> handleAction(() => api.build.buildNwnx(nwnxTarget.trim()), "nwnx") } />

Requires server restart to pick up changes

setNwnx((prev) => ({ ...prev, collapsed: !prev.collapsed }))} />
); }