import { useState, useEffect, useRef, useCallback } from 'react' import { X, Loader2, FileCode, AlertCircle } from 'lucide-react' import { cn } from '@/lib/utils' import { scriptsApi } from '@/api' import { detectParameterCandidates } from '@/lib/scriptParameterDetector' import { ParameterDetectorStepper } from '@/components/script-editor/ParameterDetectorStepper' import type { ScriptCategoryResponse, ScriptParameter, ScriptParametersSchema, ParameterCandidate, } from '@/types' interface ParameterizeAndSavePanelProps { /** Pre-populated script body (script mode). Undefined triggers paste mode. */ scriptBody?: string /** Script language. Undefined shows language picker in paste mode. */ language?: string /** Default name for the template. */ defaultName?: string /** Default description for the template. */ defaultDescription?: string /** Called with the final enriched payload when user saves. */ onSave: (payload: { name: string description: string | undefined category_id: string | undefined share_with_team: boolean script_body: string parameters_schema: ScriptParametersSchema }) => Promise /** Called when the panel is closed without saving. */ onClose: () => void } const LANGUAGES = [ { value: 'powershell', label: 'PowerShell' }, { value: 'bash', label: 'Bash' }, { value: 'python', label: 'Python' }, ] export function ParameterizeAndSavePanel({ scriptBody, language: initialLanguage, defaultName = '', defaultDescription = '', onSave, onClose, }: ParameterizeAndSavePanelProps) { // Mode: script (body provided) vs paste (user pastes) const isPasteMode = scriptBody === undefined // Paste mode state const [pastedScript, setPastedScript] = useState('') const [selectedLanguage, setSelectedLanguage] = useState(initialLanguage || 'powershell') const [scriptConfirmed, setScriptConfirmed] = useState(false) // Working state — the script body being rewritten as params are accepted const effectiveScript = isPasteMode ? pastedScript : scriptBody! const [workingScript, setWorkingScript] = useState(effectiveScript) const [parameters, setParameters] = useState([]) // Detection state const [candidates, setCandidates] = useState([]) const [detectionRan, setDetectionRan] = useState(false) const [showStepper, setShowStepper] = useState(false) const [detectionSummary, setDetectionSummary] = useState(null) // Metadata state const [name, setName] = useState(defaultName) const [description, setDescription] = useState(defaultDescription) const [categoryId, setCategoryId] = useState('') const [shareWithTeam, setShareWithTeam] = useState(false) const [categories, setCategories] = useState([]) // Save state const [isSaving, setIsSaving] = useState(false) const [error, setError] = useState(null) const panelRef = useRef(null) // Load categories on mount useEffect(() => { scriptsApi.getCategories().then(setCategories).catch(() => {}) }, []) // Auto-run detection when script is ready (script mode: on mount, paste mode: after confirm) const runDetection = useCallback((script: string) => { const detected = detectParameterCandidates(script) setCandidates(detected) setDetectionRan(true) if (detected.length > 0) { setShowStepper(true) } else { setDetectionSummary('No configurable values found — the script will be saved as-is. Variable detection currently supports PowerShell only.') } }, []) // Script mode: run detection on mount useEffect(() => { if (!isPasteMode && effectiveScript) { setWorkingScript(effectiveScript) runDetection(effectiveScript) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // Paste mode: run detection after script is confirmed useEffect(() => { if (isPasteMode && scriptConfirmed && pastedScript) { setWorkingScript(pastedScript) runDetection(pastedScript) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [scriptConfirmed]) // Escape key closes panel useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [onClose]) const handleConfirmPaste = () => { if (!pastedScript.trim()) return setScriptConfirmed(true) } const handleAcceptCandidate = ( candidate: ParameterCandidate, overrides: { key: string label: string type: ScriptParameter['type'] sensitive: boolean required: boolean defaultValue: string | boolean | number | null } ) => { // Rewrite the script body — replace the default value with {{ key }} placeholder let updatedScript = workingScript if (candidate.source === 'param_block') { const defaultMatch = candidate.matchedLine.match(/=\s*(.+?)(?:\s*,?\s*$)/) if (defaultMatch) { updatedScript = updatedScript.replace( candidate.matchedLine, candidate.matchedLine.replace(defaultMatch[1], `'{{${overrides.key}}}'`) ) } } else { const assignMatch = candidate.matchedLine.match(/=\s*(.+)$/) if (assignMatch) { updatedScript = updatedScript.replace( candidate.matchedLine, candidate.matchedLine.replace(assignMatch[1], `'{{${overrides.key}}}'`) ) } } setWorkingScript(updatedScript) // Add parameter to accumulated schema const newParam: ScriptParameter = { key: overrides.key, label: overrides.label, type: overrides.type, required: overrides.required, placeholder: null, group: null, order: parameters.length + 1, help_text: null, options: null, default: overrides.defaultValue, validation: null, sensitive: overrides.sensitive, } setParameters(prev => [...prev, newParam]) } const handleSkipCandidate = () => { // Stepper advances internally — nothing to do here } const handleDetectionFinish = (acceptedCount: number, totalCount: number) => { setShowStepper(false) setCandidates([]) setDetectionSummary( acceptedCount === 0 ? 'No parameters were added. Script will be saved as-is.' : `Added ${acceptedCount} of ${totalCount} detected parameter${totalCount !== 1 ? 's' : ''}.` ) } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!name.trim()) return setIsSaving(true) setError(null) try { await onSave({ name: name.trim(), description: description.trim() || undefined, category_id: categoryId || undefined, share_with_team: shareWithTeam, script_body: workingScript, parameters_schema: { parameters }, }) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save script. Please try again.') } finally { setIsSaving(false) } } // Determine if save button should be enabled const canSave = name.trim().length > 0 && !isSaving && !showStepper && (isPasteMode ? scriptConfirmed : true) return ( <> {/* Scrim */}
{/* Panel */}
{/* Header */}

{isPasteMode ? 'Import Script to Library' : 'Save to Library'}

{/* Scrollable content */}
{/* Paste mode: script input area (before confirmation) */} {isPasteMode && !scriptConfirmed && (

Paste Your Script

{LANGUAGES.map((lang) => ( ))}