import { useState, useEffect, useRef } from 'react' import { useSearchParams } from 'react-router-dom' import { Terminal } from 'lucide-react' import { cn } from '@/lib/utils' import { scriptBuilderApi } from '@/api' import { ScriptBuilderChat } from '@/components/script-builder/ScriptBuilderChat' import { ScriptBuilderInput } from '@/components/script-builder/ScriptBuilderInput' import { ScriptPreviewModal } from '@/components/script-builder/ScriptPreviewModal' import { ParameterizeAndSavePanel } from '@/components/scripts/ParameterizeAndSavePanel' import type { ScriptBuilderSessionDetail, ScriptBuilderMessage } from '@/types' const LANGUAGES = [ { value: 'powershell', label: 'PowerShell' }, { value: 'bash', label: 'Bash' }, { value: 'python', label: 'Python' }, ] as const export default function ScriptBuilderPage() { const [searchParams] = useSearchParams() const [session, setSession] = useState(null) const [messages, setMessages] = useState([]) const [language, setLanguage] = useState('powershell') const [isLoading, setIsLoading] = useState(false) // Ref-based lock: prevents two concurrent handleSend calls (e.g. FlowPilot // handoff useEffect + user keystroke) from each calling createSession() and // creating two orphaned sessions. React state updates are async so isLoading // alone can't guard across two calls in the same render cycle. const creatingSessionRef = useRef(false) const [previewScript, setPreviewScript] = useState<{ script: string; filename: string | null } | null>(null) const [showSaveDialog, setShowSaveDialog] = useState(false) const [handoffProcessed, setHandoffProcessed] = useState(false) const hasMessages = messages.length > 0 // Handle FlowPilot handoff on mount useEffect(() => { if (handoffProcessed) return setHandoffProcessed(true) const contextRaw = sessionStorage.getItem('scriptBuilderContext') if (!contextRaw) return try { const context = JSON.parse(contextRaw) as { from_session?: string prompt?: string language?: string } sessionStorage.removeItem('scriptBuilderContext') if (context.language) { setLanguage(context.language) } if (context.prompt) { // Auto-send the prompt handleSend(context.prompt, context.language || 'powershell') } } catch { sessionStorage.removeItem('scriptBuilderContext') } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // Suppress unused searchParams warning — used to detect ?from=flowpilot context void searchParams const handleSend = async (content: string, langOverride?: string) => { const effectiveLanguage = langOverride || language // Optimistically add user message const userMessage: ScriptBuilderMessage = { role: 'user', content, created_at: new Date().toISOString(), } setMessages((prev) => [...prev, userMessage]) setIsLoading(true) try { // Create session if needed let currentSession = session if (!currentSession) { if (creatingSessionRef.current) { // Another concurrent call is already creating the session; drop this send. setIsLoading(false) setMessages((prev) => prev.slice(0, -1)) return } creatingSessionRef.current = true try { currentSession = await scriptBuilderApi.createSession(effectiveLanguage) setSession(currentSession) } finally { creatingSessionRef.current = false } } // Send message const response = await scriptBuilderApi.sendMessage(currentSession.id, content) const assistantMessage: ScriptBuilderMessage = { role: 'assistant', content: response.content, script: response.script, script_filename: response.script_filename, line_count: response.line_count, created_at: response.timestamp, } setMessages((prev) => [...prev, assistantMessage]) } catch (err) { // Add error message const errorMessage: ScriptBuilderMessage = { role: 'assistant', content: `An error occurred: ${err instanceof Error ? err.message : 'Failed to generate response. Please try again.'}`, created_at: new Date().toISOString(), } setMessages((prev) => [...prev, errorMessage]) } finally { setIsLoading(false) } } const handleViewScript = (script: string, filename: string | null) => { setPreviewScript({ script, filename }) } const handleSaveScript = () => { setShowSaveDialog(true) } const handleSaved = async (payload: { name: string description: string | undefined category_id: string | undefined share_with_team: boolean script_body: string parameters_schema: { parameters: import('@/types').ScriptParameter[] } }) => { if (!session) return await scriptBuilderApi.saveToLibrary(session.id, { name: payload.name, description: payload.description, category_id: payload.category_id, share_with_team: payload.share_with_team, script_body: payload.script_body, parameters_schema: payload.parameters_schema, }) setShowSaveDialog(false) } // Derive default name from session title or filename const defaultSaveName = session?.title || session?.latest_script_filename || 'Untitled Script' return (
{/* Header with language selector */}

Script Builder

Describe what you need, AI generates the script

{/* Language pills */}
{LANGUAGES.map((lang) => ( ))}
{/* Chat area */} {/* Input */}
handleSend(content)} disabled={isLoading} showSuggestions={messages.length === 0} />
{/* Preview modal */} {previewScript && ( setPreviewScript(null)} onSave={() => { setPreviewScript(null) setShowSaveDialog(true) }} /> )} {/* Save panel */} {showSaveDialog && session && session.latest_script && ( setShowSaveDialog(false)} /> )}
) }