feat: add Script Builder page with chat UI, code blocks, preview modal, and save dialog
- ScriptCodeBlock: collapsed code preview with syntax highlighting (first 5 lines) - ScriptBuilderInput: auto-resize chat input with Enter-to-send - ScriptBuilderChat: message list with markdown rendering and code blocks - ScriptPreviewModal: fullscreen script viewer with line numbers - SaveToLibraryDialog: save script with name, description, category, team sharing - ScriptBuilderPage: language selector, session management, FlowPilot handoff - Added route, sidebar nav item (fuchsia), and mobile nav entry Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
145
frontend/src/components/script-builder/ScriptPreviewModal.tsx
Normal file
145
frontend/src/components/script-builder/ScriptPreviewModal.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import atomOneDark from 'react-syntax-highlighter/dist/esm/styles/hljs/atom-one-dark'
|
||||
import { X, Copy, Check, BookmarkPlus } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const LANGUAGE_MAP: Record<string, string> = {
|
||||
powershell: 'powershell',
|
||||
bash: 'bash',
|
||||
python: 'python',
|
||||
}
|
||||
|
||||
const LANGUAGE_LABELS: Record<string, string> = {
|
||||
powershell: 'PowerShell',
|
||||
bash: 'Bash',
|
||||
python: 'Python',
|
||||
}
|
||||
|
||||
interface ScriptPreviewModalProps {
|
||||
script: string
|
||||
filename: string | null
|
||||
language: string
|
||||
onClose: () => void
|
||||
onSave: () => void
|
||||
}
|
||||
|
||||
export function ScriptPreviewModal({
|
||||
script,
|
||||
filename,
|
||||
language,
|
||||
onClose,
|
||||
onSave,
|
||||
}: ScriptPreviewModalProps) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const hlLanguage = LANGUAGE_MAP[language] || 'powershell'
|
||||
const lineCount = script.split('\n').length
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [onClose])
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(script)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch {
|
||||
// clipboard not available
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center"
|
||||
onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
|
||||
>
|
||||
<div className="bg-[#18191f] rounded-xl border border-[rgba(255,255,255,0.08)] max-w-[900px] w-full mx-4 max-h-[85vh] flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-5 py-3.5 border-b border-[rgba(255,255,255,0.06)]">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<span className="font-label text-sm text-cyan-400 truncate">
|
||||
{filename || 'script'}
|
||||
</span>
|
||||
<span className="shrink-0 font-label text-[0.625rem] uppercase tracking-wider px-2 py-0.5 rounded-full bg-[rgba(255,255,255,0.06)] text-muted-foreground">
|
||||
{LANGUAGE_LABELS[language] || language}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-1.5 rounded-[10px] text-xs font-medium transition-colors",
|
||||
"bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-foreground hover:border-[rgba(255,255,255,0.12)]"
|
||||
)}
|
||||
>
|
||||
{copied ? <Check size={14} className="text-emerald-400" /> : <Copy size={14} />}
|
||||
{copied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onSave}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-1.5 rounded-[10px] text-xs font-medium transition-colors",
|
||||
"bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 hover:bg-emerald-500/15"
|
||||
)}
|
||||
>
|
||||
<BookmarkPlus size={14} />
|
||||
Save to Library
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-[rgba(255,255,255,0.06)] transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Code body */}
|
||||
<div className="flex-1 overflow-auto min-h-0">
|
||||
<SyntaxHighlighter
|
||||
language={hlLanguage}
|
||||
style={atomOneDark}
|
||||
showLineNumbers
|
||||
customStyle={{
|
||||
background: 'transparent',
|
||||
padding: '16px',
|
||||
margin: 0,
|
||||
fontSize: '0.8125rem',
|
||||
lineHeight: '1.6',
|
||||
}}
|
||||
lineNumberStyle={{
|
||||
color: '#5a6170',
|
||||
minWidth: '2.5em',
|
||||
paddingRight: '1em',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
wrapLongLines
|
||||
>
|
||||
{script}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between px-5 py-3 border-t border-[rgba(255,255,255,0.06)]">
|
||||
<span className="font-label text-[0.625rem] text-muted-foreground">
|
||||
{lineCount} line{lineCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
"px-4 py-1.5 rounded-[10px] text-xs font-medium transition-colors",
|
||||
"bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-foreground hover:border-[rgba(255,255,255,0.12)]"
|
||||
)}
|
||||
>
|
||||
Close & Return to Chat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user