feat(flowpilot): add always-visible message bar, remove hidden free text escape hatch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-21 16:49:46 -04:00
parent ad64f26883
commit ec7df50064
3 changed files with 85 additions and 52 deletions

View File

@@ -0,0 +1,75 @@
import { useState, useRef, useCallback } from 'react'
import { Send } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { StepResponseRequest } from '@/types/ai-session'
interface FlowPilotMessageBarProps {
onRespond: (response: StepResponseRequest) => void
disabled?: boolean
isProcessing?: boolean
}
export function FlowPilotMessageBar({ onRespond, disabled = false, isProcessing = false }: FlowPilotMessageBarProps) {
const [message, setMessage] = useState('')
const textareaRef = useRef<HTMLTextAreaElement>(null)
const isDisabled = disabled || isProcessing
const handleSubmit = useCallback(() => {
const trimmed = message.trim()
if (!trimmed || isDisabled) return
onRespond({ free_text_input: trimmed })
setMessage('')
if (textareaRef.current) {
textareaRef.current.style.height = 'auto'
}
}, [message, isDisabled, onRespond])
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmit()
}
}, [handleSubmit])
const handleInput = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setMessage(e.target.value)
const textarea = e.target
textarea.style.height = 'auto'
textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`
}, [])
return (
<div className="px-3 sm:px-4 lg:px-5 pb-3">
<div className={cn(
'flex items-end gap-2 rounded-xl border bg-card p-2 transition-colors',
isDisabled
? 'border-border/50 opacity-50'
: 'border-border focus-within:border-[rgba(6,182,212,0.3)]'
)}>
<textarea
ref={textareaRef}
value={message}
onChange={handleInput}
onKeyDown={handleKeyDown}
placeholder={isProcessing ? 'FlowPilot is thinking...' : 'Type a message...'}
disabled={isDisabled}
rows={1}
className="flex-1 resize-none bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed py-1.5 px-2"
/>
<button
onClick={handleSubmit}
disabled={isDisabled || !message.trim()}
className={cn(
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg transition-all',
message.trim() && !isDisabled
? 'bg-gradient-brand text-[#101114] hover:opacity-90 active:scale-[0.97]'
: 'bg-[rgba(255,255,255,0.04)] text-muted-foreground cursor-not-allowed'
)}
>
<Send size={14} />
</button>
</div>
</div>
)
}

View File

@@ -11,6 +11,7 @@ import type {
import { ConfidenceIndicator } from './ConfidenceIndicator'
import { FlowPilotStepCard } from './FlowPilotStepCard'
import { FlowPilotActionBar } from './FlowPilotActionBar'
import { FlowPilotMessageBar } from './FlowPilotMessageBar'
import { SessionDocView } from './SessionDocView'
import { SessionTicketCard } from './SessionTicketCard'
import { SimilarSessions } from './SimilarSessions'
@@ -300,6 +301,15 @@ export function FlowPilotSession({
</div>
</div>
{/* Message bar */}
{session.status === 'active' && (
<FlowPilotMessageBar
onRespond={onRespond}
isProcessing={isProcessing}
disabled={currentStep?.allow_free_text === false}
/>
)}
{/* Action bar */}
{session.status === 'active' && (
<FlowPilotActionBar

View File

@@ -2,9 +2,7 @@ import { useState } from 'react'
import { MessageSquare, Zap, CheckCircle2, SkipForward, ChevronDown, ChevronUp } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { AISessionStepResponse, StepResponseRequest } from '@/types/ai-session'
import type { FileUploadResponse } from '@/types/upload'
import { MarkdownContent } from '@/components/ui/MarkdownContent'
import { RichTextInput } from '@/components/common/RichTextInput'
import { FlowPilotOptions } from './FlowPilotOptions'
import { InSessionScriptGenerator } from './InSessionScriptGenerator'
@@ -27,10 +25,7 @@ const STEP_TYPE_ICONS = {
} as const
export function FlowPilotStepCard({ step, isCurrentStep, isProcessing, sessionId, onRespond }: FlowPilotStepCardProps) {
const [freeText, setFreeText] = useState('')
const [showFreeText, setShowFreeText] = useState(false)
const [isCollapsed, setIsCollapsed] = useState(!isCurrentStep)
const [_freeTextUploads, setFreeTextUploads] = useState<FileUploadResponse[]>([])
const content = step.content as Record<string, unknown>
const stepText = (content.text as string) || ''
@@ -42,13 +37,6 @@ export function FlowPilotStepCard({ step, isCurrentStep, isProcessing, sessionId
onRespond({ selected_option: value })
}
const handleFreeTextSubmit = () => {
if (!freeText.trim()) return
onRespond({ free_text_input: freeText.trim() })
setFreeText('')
setShowFreeText(false)
}
const handleSkip = () => {
onRespond({ was_skipped: true })
}
@@ -197,46 +185,6 @@ export function FlowPilotStepCard({ step, isCurrentStep, isProcessing, sessionId
</div>
)}
{/* Free text escape hatch */}
{!isResolutionSuggestion && step.allow_free_text && (
<>
{!showFreeText ? (
<button
onClick={() => setShowFreeText(true)}
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
None of these let me describe
</button>
) : (
<div className="space-y-2">
<RichTextInput
value={freeText}
onChange={setFreeText}
onFilesChange={setFreeTextUploads}
sessionId={sessionId}
placeholder="Describe what you're seeing..."
rows={3}
/>
<div className="flex gap-2">
<button
onClick={handleFreeTextSubmit}
disabled={!freeText.trim()}
className="rounded-lg bg-gradient-brand px-4 py-1.5 text-sm font-semibold text-[#101114] hover:opacity-90 disabled:opacity-50 transition-opacity"
>
Submit
</button>
<button
onClick={() => { setShowFreeText(false); setFreeText('') }}
className="rounded-lg px-4 py-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Cancel
</button>
</div>
</div>
)}
</>
)}
{/* Skip option */}
{!isResolutionSuggestion && step.allow_skip && (
<button