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:
@@ -0,0 +1,78 @@
|
||||
import { useState, useRef, useCallback, useEffect } from 'react'
|
||||
import { Send } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface ScriptBuilderInputProps {
|
||||
onSend: (content: string) => void
|
||||
disabled: boolean
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export function ScriptBuilderInput({
|
||||
onSend,
|
||||
disabled,
|
||||
placeholder = 'Describe the script you need...',
|
||||
}: ScriptBuilderInputProps) {
|
||||
const [value, setValue] = useState('')
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const adjustHeight = useCallback(() => {
|
||||
const textarea = textareaRef.current
|
||||
if (!textarea) return
|
||||
textarea.style.height = 'auto'
|
||||
textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
adjustHeight()
|
||||
}, [value, adjustHeight])
|
||||
|
||||
const handleSend = () => {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed || disabled) return
|
||||
onSend(trimmed)
|
||||
setValue('')
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
|
||||
const canSend = value.trim().length > 0 && !disabled
|
||||
|
||||
return (
|
||||
<div className="flex items-end gap-2 p-3 border-t" style={{ borderColor: 'var(--glass-border)' }}>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
rows={1}
|
||||
className={cn(
|
||||
"flex-1 resize-none rounded-xl px-4 py-2.5 text-sm",
|
||||
"bg-card border border-border text-foreground placeholder:text-muted-foreground",
|
||||
"focus:outline-none focus:border-[rgba(6,182,212,0.3)] transition-colors",
|
||||
"disabled:opacity-50"
|
||||
)}
|
||||
style={{ maxHeight: 120 }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!canSend}
|
||||
className={cn(
|
||||
"shrink-0 flex items-center justify-center w-10 h-10 rounded-xl transition-all",
|
||||
canSend
|
||||
? "bg-gradient-brand text-[#101114] hover:opacity-90 active:scale-[0.97]"
|
||||
: "bg-[rgba(255,255,255,0.04)] text-[#5a6170] cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<Send size={18} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user