- landing.css: hardcode --lp-btn to #60a5fa (lesson 104 — no var(--color-*) in landing.css) - ScriptBuilderInput: suggestion chips now correctly disabled during generation - ChatSidebar: wrapper onClick no longer fires onSelect while in confirming state - SessionHistoryPage: fix loadMoreAiSessions race condition with generation counter; flow session tab auto-activates when URL params target flow session filters Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
108 lines
3.6 KiB
TypeScript
108 lines
3.6 KiB
TypeScript
import { useState, useRef, useCallback, useEffect } from 'react'
|
|
import { Send, Terminal, UserPlus, HardDrive, RotateCcw } from 'lucide-react'
|
|
import type { LucideIcon } from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
const SUGGESTIONS: { icon: LucideIcon; label: string }[] = [
|
|
{ icon: UserPlus, label: 'Create a new AD user' },
|
|
{ icon: HardDrive, label: 'Check disk space on all servers' },
|
|
{ icon: RotateCcw, label: 'Restart a Windows service' },
|
|
{ icon: Terminal, label: 'Reset MFA for a user' },
|
|
]
|
|
|
|
interface ScriptBuilderInputProps {
|
|
onSend: (content: string) => void
|
|
disabled: boolean
|
|
placeholder?: string
|
|
showSuggestions?: boolean
|
|
}
|
|
|
|
export function ScriptBuilderInput({
|
|
onSend,
|
|
disabled,
|
|
placeholder = 'Describe the script you need...',
|
|
showSuggestions = false,
|
|
}: 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="border-t border-border p-3 space-y-2">
|
|
<div className="flex items-end gap-2">
|
|
<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(249,115,22,0.25)] focus:ring-1 focus:ring-[rgba(249,115,22,0.1)] 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-primary text-white hover:brightness-110 active:scale-[0.98]"
|
|
: "bg-[var(--color-bg-elevated)] text-muted-foreground cursor-not-allowed"
|
|
)}
|
|
>
|
|
<Send size={18} />
|
|
</button>
|
|
</div>
|
|
|
|
{showSuggestions && (
|
|
<div className="flex flex-wrap gap-2">
|
|
{SUGGESTIONS.map(({ icon: Icon, label }) => (
|
|
<button
|
|
key={label}
|
|
type="button"
|
|
disabled={disabled}
|
|
onClick={() => { if (!disabled) onSend(label) }}
|
|
className="group flex items-center gap-1.5 rounded-md border border-border bg-card px-3 py-1.5 text-xs text-muted-foreground transition-all hover:border-[var(--color-border-hover)] hover:bg-[var(--color-bg-elevated)] hover:text-foreground active:scale-[0.97] disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none"
|
|
>
|
|
<Icon size={11} className="text-muted shrink-0 group-hover:text-[#f97316] transition-colors" />
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|