feat: cockpit UX polish — conversation log, interactive steps, onboarding
- Redesign conversation log: proper role labels, left-border accents, larger text, CSS variable background, min-height guarantee - Interactive steps panel: click-to-complete, click-to-select, progress bar with counter, hover-reveal descriptions, smooth transitions - Replace noop overflow button with real dropdown menu (Pause, Copy Link, Close Case) with keyboard/click-outside dismiss - Evidence cycling: right-click to reverse-cycle status, tooltips on icons - First-run onboarding overlay labeling the three cockpit zones, auto- dismisses on first message or manual dismiss, persisted via localStorage - Drag handle: taller, visible hover state, title tooltip - Simplify input placeholder (remove redundant paste-log hint) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Pencil, X, Check, ExternalLink } from 'lucide-react'
|
||||
import { Pencil, X, Check, ExternalLink, Pause, XCircle, Link2 } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from '@/lib/toast'
|
||||
import type { TriageMeta } from '@/types/ai-session'
|
||||
|
||||
interface IncidentHeaderProps {
|
||||
@@ -9,7 +10,8 @@ interface IncidentHeaderProps {
|
||||
sessionId: string
|
||||
onFieldSave: (field: keyof TriageMeta, value: string) => void
|
||||
onResolve: () => void
|
||||
onOverflow: () => void
|
||||
onPause?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
interface EditPopoverProps {
|
||||
@@ -95,12 +97,81 @@ function HeaderField({ label, value, placeholder, onSave, isHypothesis }: Header
|
||||
)
|
||||
}
|
||||
|
||||
function OverflowMenu({ onPause, onClose, sessionId }: { onPause?: () => void; onClose?: () => void; sessionId: string }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) setOpen(false)
|
||||
}
|
||||
const handleEsc = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
document.addEventListener('keydown', handleEsc)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
document.removeEventListener('keydown', handleEsc)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const handleCopyLink = () => {
|
||||
navigator.clipboard.writeText(`${window.location.origin}/assistant/${sessionId}`)
|
||||
toast.success('Session link copied')
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="bg-elevated border border-default rounded px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
⋯
|
||||
</button>
|
||||
{open && (
|
||||
<div className="absolute right-0 top-full mt-1 z-50 w-44 bg-elevated border border-hover rounded-md shadow-lg py-1">
|
||||
{onPause && (
|
||||
<button
|
||||
onClick={() => { onPause(); setOpen(false) }}
|
||||
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-card transition-colors text-left"
|
||||
>
|
||||
<Pause size={12} />
|
||||
Pause Case
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleCopyLink}
|
||||
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-card transition-colors text-left"
|
||||
>
|
||||
<Link2 size={12} />
|
||||
Copy Link
|
||||
</button>
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={() => { onClose(); setOpen(false) }}
|
||||
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-danger hover:text-danger hover:bg-danger-dim transition-colors text-left"
|
||||
>
|
||||
<XCircle size={12} />
|
||||
Close Case
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function IncidentHeader({
|
||||
triageMeta,
|
||||
psaTicketId,
|
||||
sessionId,
|
||||
onFieldSave,
|
||||
onResolve,
|
||||
onOverflow,
|
||||
onPause,
|
||||
onClose,
|
||||
}: IncidentHeaderProps) {
|
||||
return (
|
||||
<div className="bg-card border-b border-default px-4 py-2 flex items-center gap-4 flex-wrap">
|
||||
@@ -146,12 +217,7 @@ export function IncidentHeader({
|
||||
>
|
||||
Resolve
|
||||
</button>
|
||||
<button
|
||||
onClick={onOverflow}
|
||||
className="bg-elevated border border-default rounded px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
⋯
|
||||
</button>
|
||||
<OverflowMenu onPause={onPause} onClose={onClose} sessionId={sessionId} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import { Check, ArrowRight, Circle, Terminal } from 'lucide-react'
|
||||
import { Check, ArrowRight, Circle, Terminal, ChevronRight } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { ActionItem } from '@/types/ai-session'
|
||||
|
||||
interface StepsPanelProps {
|
||||
actions: ActionItem[]
|
||||
activeIndex: number
|
||||
completedSteps?: Set<number>
|
||||
onStepComplete?: (index: number) => void
|
||||
onStepSelect?: (index: number) => void
|
||||
onGenerateScript?: () => void
|
||||
}
|
||||
|
||||
export function StepsPanel({ actions, activeIndex, onGenerateScript }: StepsPanelProps) {
|
||||
export function StepsPanel({
|
||||
actions,
|
||||
activeIndex,
|
||||
completedSteps = new Set(),
|
||||
onStepComplete,
|
||||
onStepSelect,
|
||||
onGenerateScript,
|
||||
}: StepsPanelProps) {
|
||||
if (actions.length === 0) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center text-muted-foreground text-sm">
|
||||
@@ -17,45 +27,100 @@ export function StepsPanel({ actions, activeIndex, onGenerateScript }: StepsPane
|
||||
)
|
||||
}
|
||||
|
||||
const completedCount = completedSteps.size
|
||||
const totalCount = actions.length
|
||||
const progressPct = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0
|
||||
|
||||
const showScriptCta = actions[activeIndex]?.command?.toLowerCase().includes('script') ||
|
||||
actions[activeIndex]?.description?.toLowerCase().includes('script')
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="text-[10px] uppercase tracking-wider text-accent font-semibold mb-2">
|
||||
Steps
|
||||
{/* Header with progress */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-[10px] uppercase tracking-wider text-accent font-semibold">
|
||||
Steps
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground tabular-nums">
|
||||
{completedCount}/{totalCount} complete
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="h-1 bg-elevated rounded-full mb-3 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-accent rounded-full transition-all duration-500 ease-out"
|
||||
style={{ width: `${progressPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Steps list */}
|
||||
<div className="flex-1 overflow-y-auto space-y-1.5 min-h-0">
|
||||
{actions.map((action, idx) => {
|
||||
const isCompleted = idx < activeIndex
|
||||
const isCompleted = completedSteps.has(idx)
|
||||
const isActive = idx === activeIndex
|
||||
const isPending = idx > activeIndex
|
||||
const isPending = !isCompleted && !isActive
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
onClick={() => onStepSelect?.(idx)}
|
||||
className={cn(
|
||||
'rounded px-3 py-2 text-sm flex items-start gap-2.5 transition-colors',
|
||||
isCompleted && 'bg-elevated/50 text-muted-foreground',
|
||||
'rounded-md px-3 py-2 text-sm flex items-start gap-2.5 transition-all duration-200 cursor-pointer group',
|
||||
isCompleted && 'bg-success/[0.06] text-muted-foreground hover:bg-success/[0.1]',
|
||||
isActive && 'bg-elevated border border-accent/30 text-primary',
|
||||
isPending && 'bg-card text-muted-foreground/60',
|
||||
isPending && 'bg-card text-muted-foreground/60 hover:bg-elevated/50',
|
||||
)}
|
||||
>
|
||||
<span className="mt-0.5 flex-shrink-0">
|
||||
{isCompleted && <Check size={14} className="text-success" />}
|
||||
{isActive && <ArrowRight size={14} className="text-accent" />}
|
||||
{isPending && <Circle size={14} className="text-muted" />}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<span className={cn(isCompleted && 'line-through')}>{action.label}</span>
|
||||
{isActive && action.description && (
|
||||
<p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">{action.description}</p>
|
||||
{/* Status icon / complete button */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (!isCompleted) onStepComplete?.(idx)
|
||||
}}
|
||||
className={cn(
|
||||
'mt-0.5 flex-shrink-0 rounded-full transition-all duration-200',
|
||||
isCompleted && 'text-success',
|
||||
isActive && 'text-accent hover:text-success hover:scale-110',
|
||||
isPending && 'text-muted hover:text-muted-foreground',
|
||||
)}
|
||||
title={isCompleted ? 'Completed' : 'Mark as done'}
|
||||
>
|
||||
{isCompleted && <Check size={14} />}
|
||||
{isActive && (
|
||||
<div className="relative">
|
||||
<ArrowRight size={14} className="group-hover:hidden" />
|
||||
<Check size={14} className="hidden group-hover:block" />
|
||||
</div>
|
||||
)}
|
||||
{isPending && <Circle size={14} />}
|
||||
</button>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className={cn(
|
||||
'transition-colors duration-200',
|
||||
isCompleted && 'line-through opacity-70',
|
||||
)}>{action.label}</span>
|
||||
{/* Show description for active step and on hover for others */}
|
||||
{action.description && (
|
||||
<p className={cn(
|
||||
'text-xs text-muted-foreground mt-0.5 leading-relaxed transition-all duration-200',
|
||||
isActive ? 'opacity-100 max-h-20' : 'opacity-0 max-h-0 group-hover:opacity-70 group-hover:max-h-20 overflow-hidden',
|
||||
)}>
|
||||
{action.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Active indicator */}
|
||||
{isActive && !isCompleted && (
|
||||
<ChevronRight size={12} className="text-accent/50 flex-shrink-0 mt-1" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{showScriptCta && onGenerateScript && (
|
||||
<button
|
||||
onClick={onGenerateScript}
|
||||
|
||||
@@ -10,9 +10,9 @@ interface WhatWeKnowProps {
|
||||
}
|
||||
|
||||
const STATUS_CONFIG = {
|
||||
confirmed: { icon: Check, color: 'text-success', label: '✓' },
|
||||
ruled_out: { icon: X, color: 'text-danger', label: '✗' },
|
||||
pending: { icon: HelpCircle, color: 'text-muted-foreground', label: '?' },
|
||||
confirmed: { icon: Check, color: 'text-success', label: 'Confirmed', tooltip: 'Confirmed — click to cycle, right-click to reverse' },
|
||||
ruled_out: { icon: X, color: 'text-danger', label: 'Ruled out', tooltip: 'Ruled out — click to cycle, right-click to reverse' },
|
||||
pending: { icon: HelpCircle, color: 'text-muted-foreground', label: 'Pending', tooltip: 'Pending — click to cycle, right-click to reverse' },
|
||||
} as const
|
||||
|
||||
const STATUS_CYCLE: EvidenceItem['status'][] = ['confirmed', 'ruled_out', 'pending']
|
||||
@@ -30,10 +30,11 @@ export function WhatWeKnow({ items, onAdd, onEdit }: WhatWeKnowProps) {
|
||||
setShowAddInput(false)
|
||||
}
|
||||
|
||||
const handleStatusToggle = (idx: number) => {
|
||||
const handleStatusCycle = (idx: number, reverse = false) => {
|
||||
const item = items[idx]
|
||||
const currentIdx = STATUS_CYCLE.indexOf(item.status)
|
||||
const nextStatus = STATUS_CYCLE[(currentIdx + 1) % STATUS_CYCLE.length]
|
||||
const offset = reverse ? STATUS_CYCLE.length - 1 : 1
|
||||
const nextStatus = STATUS_CYCLE[(currentIdx + offset) % STATUS_CYCLE.length]
|
||||
onEdit(idx, item.text, nextStatus)
|
||||
}
|
||||
|
||||
@@ -65,9 +66,13 @@ export function WhatWeKnow({ items, onAdd, onEdit }: WhatWeKnowProps) {
|
||||
return (
|
||||
<div key={idx} className="flex items-start gap-2 text-sm group">
|
||||
<button
|
||||
onClick={() => handleStatusToggle(idx)}
|
||||
onClick={() => handleStatusCycle(idx)}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault()
|
||||
handleStatusCycle(idx, true)
|
||||
}}
|
||||
className={cn('mt-0.5 flex-shrink-0 transition-colors hover:opacity-70', config.color)}
|
||||
title="Click to cycle status"
|
||||
title={config.tooltip}
|
||||
>
|
||||
<Icon size={13} />
|
||||
</button>
|
||||
|
||||
@@ -84,7 +84,15 @@ export default function AssistantChatPage() {
|
||||
const saved = localStorage.getItem('rf-assistant-work-zone-height')
|
||||
return saved ? parseFloat(saved) : 55
|
||||
})
|
||||
const [activeStepIndex] = useState(0)
|
||||
const [activeStepIndex, setActiveStepIndex] = useState(0)
|
||||
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set())
|
||||
const [showOnboarding, setShowOnboarding] = useState(() =>
|
||||
!localStorage.getItem('rf-cockpit-onboarded')
|
||||
)
|
||||
const dismissOnboarding = () => {
|
||||
setShowOnboarding(false)
|
||||
localStorage.setItem('rf-cockpit-onboarded', '1')
|
||||
}
|
||||
const splitContainerRef = useRef<HTMLDivElement>(null)
|
||||
const toggleSidebarCollapse = () => {
|
||||
const next = !sidebarCollapsed
|
||||
@@ -336,6 +344,7 @@ export default function AssistantChatPage() {
|
||||
.map((u) => u.result!.id)
|
||||
setInput('')
|
||||
setPendingUploads([])
|
||||
if (showOnboarding) dismissOnboarding()
|
||||
setMessages(prev => [...prev, { role: 'user', content: userMessage }])
|
||||
setLoading(true)
|
||||
|
||||
@@ -480,7 +489,24 @@ export default function AssistantChatPage() {
|
||||
}
|
||||
}, [activeChatId, triageMeta.evidence_items])
|
||||
|
||||
const handleStepComplete = useCallback((index: number) => {
|
||||
setCompletedSteps(prev => {
|
||||
const next = new Set(prev)
|
||||
next.add(index)
|
||||
return next
|
||||
})
|
||||
// Auto-advance to the next incomplete step
|
||||
const nextIncomplete = activeActions.findIndex((_, i) => i > index && !completedSteps.has(i))
|
||||
if (nextIncomplete !== -1) {
|
||||
setActiveStepIndex(nextIncomplete)
|
||||
} else if (index + 1 < activeActions.length) {
|
||||
setActiveStepIndex(index + 1)
|
||||
}
|
||||
}, [activeActions, completedSteps])
|
||||
|
||||
const handleStepSelect = useCallback((index: number) => {
|
||||
setActiveStepIndex(index)
|
||||
}, [])
|
||||
|
||||
// Merge triage_update from AI response into local state
|
||||
const mergeTriageUpdate = useCallback((update: TriageUpdate) => {
|
||||
@@ -674,11 +700,43 @@ export default function AssistantChatPage() {
|
||||
sessionId={activeChatId}
|
||||
onFieldSave={handleTriageFieldSave}
|
||||
onResolve={() => setShowConclude(true)}
|
||||
onOverflow={() => {}}
|
||||
onClose={() => setShowConclude(true)}
|
||||
/>
|
||||
|
||||
{/* Resizable work zone + conversation log split */}
|
||||
<div ref={splitContainerRef} className="flex-1 flex flex-col min-h-0">
|
||||
<div ref={splitContainerRef} className="flex-1 flex flex-col min-h-0 relative">
|
||||
{/* First-run onboarding overlay */}
|
||||
{showOnboarding && messages.length === 0 && (
|
||||
<div className="absolute inset-0 z-30 pointer-events-none">
|
||||
{/* Steps zone label */}
|
||||
<div className="absolute top-3 left-3 pointer-events-auto">
|
||||
<div className="bg-elevated border border-accent/30 rounded-lg px-3 py-2 shadow-lg max-w-[200px]">
|
||||
<p className="text-xs font-medium text-accent mb-0.5">Steps Panel</p>
|
||||
<p className="text-[11px] text-muted-foreground leading-snug">Troubleshooting steps appear here as FlowPilot identifies them. Click to mark done.</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* FlowPilot Asks zone label */}
|
||||
<div className="absolute top-3 right-3 pointer-events-auto">
|
||||
<div className="bg-elevated border border-warning/30 rounded-lg px-3 py-2 shadow-lg max-w-[200px]">
|
||||
<p className="text-xs font-medium text-warning mb-0.5">AI Questions & Evidence</p>
|
||||
<p className="text-[11px] text-muted-foreground leading-snug">FlowPilot asks clarifying questions here. Evidence you confirm or rule out is tracked below.</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Conversation log label */}
|
||||
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 pointer-events-auto">
|
||||
<div className="bg-elevated border border-default rounded-lg px-3 py-2 shadow-lg max-w-[280px] text-center">
|
||||
<p className="text-xs font-medium text-muted-foreground mb-0.5">Conversation Log</p>
|
||||
<p className="text-[11px] text-muted-foreground leading-snug">Full chat history lives here. Drag the handle above to resize.</p>
|
||||
<button
|
||||
onClick={dismissOnboarding}
|
||||
className="mt-2 text-[11px] text-accent hover:text-accent/80 transition-colors pointer-events-auto"
|
||||
>
|
||||
Got it, dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Work zone */}
|
||||
<div className="flex min-h-0 overflow-hidden" style={{ height: `${workZonePct}%` }}>
|
||||
{/* Left: Steps panel */}
|
||||
@@ -686,6 +744,9 @@ export default function AssistantChatPage() {
|
||||
<StepsPanel
|
||||
actions={activeActions}
|
||||
activeIndex={activeStepIndex}
|
||||
completedSteps={completedSteps}
|
||||
onStepComplete={handleStepComplete}
|
||||
onStepSelect={handleStepSelect}
|
||||
/>
|
||||
</div>
|
||||
{/* Right: FlowPilot Asks + What We Know */}
|
||||
@@ -709,13 +770,14 @@ export default function AssistantChatPage() {
|
||||
{/* Drag handle */}
|
||||
<div
|
||||
onMouseDown={handleDragStart}
|
||||
className="h-2 flex items-center justify-center cursor-row-resize hover:bg-elevated/50 transition-colors flex-shrink-0 border-y border-default/50"
|
||||
className="h-4 flex items-center justify-center cursor-row-resize hover:bg-elevated transition-colors flex-shrink-0 border-y border-default/50 group"
|
||||
title="Drag to resize"
|
||||
>
|
||||
<GripHorizontal size={14} className="text-muted" />
|
||||
<GripHorizontal size={16} className="text-muted group-hover:text-muted-foreground transition-colors" />
|
||||
</div>
|
||||
|
||||
{/* Conversation log */}
|
||||
<div className="flex-1 min-h-0 overflow-y-auto" style={{ background: '#13151c' }}>
|
||||
<div className="flex-1 min-h-[180px] overflow-y-auto bg-sidebar">
|
||||
<div className="px-4 pt-2 pb-1">
|
||||
<span className="text-[10px] uppercase tracking-wider text-muted font-semibold">Conversation Log</span>
|
||||
</div>
|
||||
@@ -727,26 +789,34 @@ export default function AssistantChatPage() {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-4 pb-2 space-y-1">
|
||||
<div className="px-4 pb-2 space-y-1.5">
|
||||
{messages.map((msg, i) => (
|
||||
<div key={i} className="flex gap-2 text-xs leading-relaxed">
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
'flex gap-3 text-sm leading-relaxed rounded-md px-3 py-2 border-l-2',
|
||||
msg.role === 'user'
|
||||
? 'border-l-muted-foreground/30 bg-white/[0.02]'
|
||||
: 'border-l-accent/40 bg-accent/[0.03]'
|
||||
)}
|
||||
>
|
||||
<span className={cn(
|
||||
'font-mono flex-shrink-0 mt-px',
|
||||
msg.role === 'user' ? 'text-muted' : 'text-accent/50'
|
||||
'text-xs font-medium flex-shrink-0 mt-0.5 min-w-[58px]',
|
||||
msg.role === 'user' ? 'text-muted-foreground' : 'text-accent/70'
|
||||
)}>
|
||||
{msg.role === 'user' ? 'you:' : 'fp:'}
|
||||
{msg.role === 'user' ? 'You' : 'FlowPilot'}
|
||||
</span>
|
||||
<span className={cn(
|
||||
msg.role === 'user' ? 'text-muted-foreground/70' : 'text-muted-foreground'
|
||||
msg.role === 'user' ? 'text-muted-foreground/80' : 'text-primary/80'
|
||||
)}>
|
||||
{msg.content.length > 300 ? msg.content.slice(0, 300) + '…' : msg.content}
|
||||
{msg.content}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{loading && (
|
||||
<div className="flex gap-2 text-xs">
|
||||
<span className="font-mono text-accent/50">fp:</span>
|
||||
<Loader2 size={12} className="animate-spin text-muted-foreground" />
|
||||
<div className="flex gap-3 text-sm px-3 py-2 border-l-2 border-l-accent/40 bg-accent/[0.03] rounded-md">
|
||||
<span className="text-xs font-medium text-accent/70 min-w-[58px]">FlowPilot</span>
|
||||
<Loader2 size={14} className="animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
@@ -786,7 +856,7 @@ export default function AssistantChatPage() {
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
placeholder={loading ? 'FlowPilot is working...' : 'Describe finding, paste log output, or ask FlowPilot...'}
|
||||
placeholder={loading ? 'FlowPilot is working...' : 'Describe the issue or ask FlowPilot...'}
|
||||
disabled={loading}
|
||||
rows={1}
|
||||
className="w-full resize-none bg-transparent px-4 pt-3 pb-1 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed"
|
||||
|
||||
Reference in New Issue
Block a user