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:
129
frontend/src/components/script-builder/ScriptCodeBlock.tsx
Normal file
129
frontend/src/components/script-builder/ScriptCodeBlock.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { useState } from 'react'
|
||||
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import powershell from 'react-syntax-highlighter/dist/esm/languages/hljs/powershell'
|
||||
import bash from 'react-syntax-highlighter/dist/esm/languages/hljs/bash'
|
||||
import python from 'react-syntax-highlighter/dist/esm/languages/hljs/python'
|
||||
import atomOneDark from 'react-syntax-highlighter/dist/esm/styles/hljs/atom-one-dark'
|
||||
import { Eye, Copy, Check, BookmarkPlus } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
SyntaxHighlighter.registerLanguage('powershell', powershell)
|
||||
SyntaxHighlighter.registerLanguage('bash', bash)
|
||||
SyntaxHighlighter.registerLanguage('python', python)
|
||||
|
||||
const LANGUAGE_MAP: Record<string, string> = {
|
||||
powershell: 'powershell',
|
||||
bash: 'bash',
|
||||
python: 'python',
|
||||
}
|
||||
|
||||
interface ScriptCodeBlockProps {
|
||||
script: string
|
||||
filename: string | null
|
||||
lineCount: number | null
|
||||
language: string
|
||||
onViewFull: () => void
|
||||
onSave: () => void
|
||||
}
|
||||
|
||||
export function ScriptCodeBlock({
|
||||
script,
|
||||
filename,
|
||||
lineCount,
|
||||
language,
|
||||
onViewFull,
|
||||
onSave,
|
||||
}: ScriptCodeBlockProps) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const lines = script.split('\n')
|
||||
const previewLines = lines.slice(0, 5).join('\n')
|
||||
const remainingLines = lines.length - 5
|
||||
const hlLanguage = LANGUAGE_MAP[language] || 'powershell'
|
||||
|
||||
const handleCopy = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
try {
|
||||
await navigator.clipboard.writeText(script)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch {
|
||||
// clipboard not available
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 rounded-lg border bg-[rgba(0,0,0,0.3)] border-[rgba(255,255,255,0.06)] overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b border-[rgba(255,255,255,0.06)]">
|
||||
<span className="font-label text-xs text-cyan-400 truncate">
|
||||
{filename || 'script'}
|
||||
</span>
|
||||
{lineCount != null && (
|
||||
<span className="font-label text-[0.625rem] text-muted-foreground ml-2 shrink-0">
|
||||
{lineCount} lines
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Code preview — clickable */}
|
||||
<button
|
||||
onClick={onViewFull}
|
||||
className="block w-full text-left cursor-pointer hover:bg-[rgba(255,255,255,0.02)] transition-colors"
|
||||
>
|
||||
<SyntaxHighlighter
|
||||
language={hlLanguage}
|
||||
style={atomOneDark}
|
||||
customStyle={{
|
||||
background: 'transparent',
|
||||
padding: '12px',
|
||||
margin: 0,
|
||||
fontSize: '0.8125rem',
|
||||
lineHeight: '1.5',
|
||||
}}
|
||||
wrapLongLines
|
||||
>
|
||||
{previewLines}
|
||||
</SyntaxHighlighter>
|
||||
{remainingLines > 0 && (
|
||||
<div className="px-3 pb-2 font-label text-[0.625rem] text-[#5a6170]">
|
||||
{"··· "}{remainingLines} more line{remainingLines !== 1 ? 's' : ''}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-t border-[rgba(255,255,255,0.06)]">
|
||||
<button
|
||||
onClick={onViewFull}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-1.5 rounded-[10px] text-xs font-semibold transition-all",
|
||||
"bg-gradient-brand text-[#101114] hover:opacity-90 active:scale-[0.97]"
|
||||
)}
|
||||
>
|
||||
<Eye size={14} />
|
||||
View Full Script
|
||||
</button>
|
||||
<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={(e) => { e.stopPropagation(); 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>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user