diff --git a/frontend/src/components/script-editor/ParameterDetectorStepper.tsx b/frontend/src/components/script-editor/ParameterDetectorStepper.tsx new file mode 100644 index 00000000..68e54cef --- /dev/null +++ b/frontend/src/components/script-editor/ParameterDetectorStepper.tsx @@ -0,0 +1,245 @@ +import { useState } from 'react' +import { ChevronRight, SkipForward, Info, Check } from 'lucide-react' +import { cn } from '@/lib/utils' +import { Input } from '@/components/ui/Input' +import type { ParameterCandidate, ScriptParameter } from '@/types' + +const PARAM_TYPES: { value: ScriptParameter['type']; label: string }[] = [ + { value: 'text', label: 'Text' }, + { value: 'password', label: 'Password' }, + { value: 'textarea', label: 'Textarea' }, + { value: 'number', label: 'Number' }, + { value: 'boolean', label: 'Boolean' }, + { value: 'select', label: 'Select' }, + { value: 'multi_text', label: 'Multi-text' }, +] + +interface Props { + candidates: ParameterCandidate[] + existingKeys: string[] + onAccept: (candidate: ParameterCandidate, overrides: { + key: string + label: string + type: ScriptParameter['type'] + sensitive: boolean + required: boolean + defaultValue: string | boolean | number | null + }) => void + onSkip: (candidate: ParameterCandidate) => void + onFinish: (acceptedCount: number, totalCount: number) => void +} + +export function ParameterDetectorStepper({ + candidates, + existingKeys, + onAccept, + onSkip, + onFinish, +}: Props) { + const [currentIndex, setCurrentIndex] = useState(0) + const [acceptedCount, setAcceptedCount] = useState(0) + const [showInferenceInfo, setShowInferenceInfo] = useState(false) + + const current = candidates[currentIndex] + const [key, setKey] = useState(current.suggestedKey) + const [label, setLabel] = useState(current.suggestedLabel) + const [type, setType] = useState(current.suggestedType) + const [sensitive, setSensitive] = useState(current.sensitive) + const [required, setRequired] = useState(true) + const [defaultValue, setDefaultValue] = useState( + current.defaultValue !== null ? String(current.defaultValue) : '' + ) + + const isLast = currentIndex === candidates.length - 1 + + const resetFieldsForIndex = (index: number) => { + const c = candidates[index] + setKey(c.suggestedKey) + setLabel(c.suggestedLabel) + setType(c.suggestedType) + setSensitive(c.sensitive) + setRequired(true) + setDefaultValue(c.defaultValue !== null ? String(c.defaultValue) : '') + setShowInferenceInfo(false) + } + + const handleAccept = () => { + const parsedDefault = type === 'boolean' + ? defaultValue === 'true' + : type === 'number' + ? (defaultValue ? Number(defaultValue) : null) + : (defaultValue || null) + + onAccept(current, { + key, + label, + type, + sensitive, + required, + defaultValue: parsedDefault, + }) + + const newAccepted = acceptedCount + 1 + setAcceptedCount(newAccepted) + + if (isLast) { + onFinish(newAccepted, candidates.length) + } else { + const nextIndex = currentIndex + 1 + setCurrentIndex(nextIndex) + resetFieldsForIndex(nextIndex) + } + } + + const handleSkip = () => { + onSkip(current) + + if (isLast) { + onFinish(acceptedCount, candidates.length) + } else { + const nextIndex = currentIndex + 1 + setCurrentIndex(nextIndex) + resetFieldsForIndex(nextIndex) + } + } + + return ( +
+ {/* Progress */} +
+

+ Candidate {currentIndex + 1} of {candidates.length} +

+
+ {candidates.map((_, i) => ( +
+ ))} +
+
+ + {/* Matched line */} +
+

+ {current.matchedLine} +

+

+ Line {current.lineNumber} · {current.source === 'param_block' ? 'param() block' : 'variable assignment'} +

+
+ + {/* Editable fields */} +
+
+ + setKey(e.target.value.replace(/[^a-zA-Z0-9_]/g, ''))} + placeholder="param_key" + /> + {existingKeys.includes(key) && ( +

Key already exists — consider a different name

+ )} +
+
+ + setLabel(e.target.value)} + placeholder="Display Label" + /> +
+
+ +
+
+ + + {showInferenceInfo && ( +

+ {current.inferenceReason} +

+ )} +
+
+ + setDefaultValue(e.target.value)} + placeholder="Original value preserved" + /> +
+
+ +
+ + +
+ + {/* Actions */} +
+ + +
+
+ ) +}