- Replace all rgba(6,182,212,...) cyan focus borders and accents with rgba(249,115,22,...) ember orange across 21+ component files - Remove all var(--glass-border) references (undefined variable) with var(--color-border-default) across 24 files - Remove deprecated blur orbs and glass-morphism effects from SurveyPage, SurveyThankYouPage, and LoginPage - Migrate landing.css from hardcoded hex to CSS custom properties (~97 replacements for single-source theming) - Fix off-palette grays in FlowPilotAnalyticsPage chart styling (#8891a0 → #848b9b, #18191f → var(--color-bg-card)) - Update stale comments: "cyan brand" → "accent brand" in GlowEdge, "gradient cyan square" → "gradient orange square" in BrandLogo - Rename glow-cyan SVG filter ID to glow-accent - Fix category color comment: "cyan" → "deep orange" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
205 lines
6.8 KiB
TypeScript
205 lines
6.8 KiB
TypeScript
import { useState, useEffect } 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 { SaveToLibraryDialog } from '@/components/script-builder/SaveToLibraryDialog'
|
|
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<ScriptBuilderSessionDetail | null>(null)
|
|
const [messages, setMessages] = useState<ScriptBuilderMessage[]>([])
|
|
const [language, setLanguage] = useState('powershell')
|
|
const [isLoading, setIsLoading] = useState(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) {
|
|
currentSession = await scriptBuilderApi.createSession(effectiveLanguage)
|
|
setSession(currentSession)
|
|
}
|
|
|
|
// 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 = () => {
|
|
setShowSaveDialog(false)
|
|
}
|
|
|
|
// Derive default name from session title or filename
|
|
const defaultSaveName = session?.title
|
|
|| session?.latest_script_filename
|
|
|| 'Untitled Script'
|
|
|
|
return (
|
|
<div className="flex flex-col h-full min-h-0">
|
|
{/* Header with language selector */}
|
|
<div className="shrink-0 px-6 py-4 border-b" style={{ borderColor: 'var(--color-border-default)' }}>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<span className="flex items-center justify-center w-9 h-9 rounded-xl bg-[rgba(232,121,249,0.1)]">
|
|
<Terminal size={18} className="text-fuchsia-400" />
|
|
</span>
|
|
<div>
|
|
<h1 className="text-base font-heading font-bold text-foreground">Script Builder</h1>
|
|
<p className="text-xs text-muted-foreground">Describe what you need, AI generates the script</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Language pills */}
|
|
<div className="flex items-center gap-1 p-0.5 rounded-lg bg-[rgba(255,255,255,0.03)] border border-[rgba(255,255,255,0.06)]">
|
|
{LANGUAGES.map((lang) => (
|
|
<button
|
|
key={lang.value}
|
|
onClick={() => !hasMessages && setLanguage(lang.value)}
|
|
disabled={hasMessages}
|
|
className={cn(
|
|
"px-3 py-1.5 rounded-md text-xs font-sans text-xs font-medium transition-all",
|
|
language === lang.value
|
|
? "bg-primary text-white"
|
|
: hasMessages
|
|
? "text-text-muted cursor-not-allowed"
|
|
: "text-muted-foreground hover:text-foreground hover:bg-[rgba(255,255,255,0.04)]"
|
|
)}
|
|
>
|
|
{lang.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Chat area */}
|
|
<ScriptBuilderChat
|
|
messages={messages}
|
|
language={language}
|
|
onViewScript={handleViewScript}
|
|
onSaveScript={handleSaveScript}
|
|
isLoading={isLoading}
|
|
/>
|
|
|
|
{/* Input */}
|
|
<div className="shrink-0">
|
|
<ScriptBuilderInput
|
|
onSend={(content) => handleSend(content)}
|
|
disabled={isLoading}
|
|
/>
|
|
</div>
|
|
|
|
{/* Preview modal */}
|
|
{previewScript && (
|
|
<ScriptPreviewModal
|
|
script={previewScript.script}
|
|
filename={previewScript.filename}
|
|
language={language}
|
|
onClose={() => setPreviewScript(null)}
|
|
onSave={() => {
|
|
setPreviewScript(null)
|
|
setShowSaveDialog(true)
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Save dialog */}
|
|
{showSaveDialog && session && (
|
|
<SaveToLibraryDialog
|
|
sessionId={session.id}
|
|
defaultName={defaultSaveName}
|
|
onClose={() => setShowSaveDialog(false)}
|
|
onSaved={handleSaved}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|