feat(pilot): Phase 7 — polish (loading/empty states, shortcuts, responsive drawer)
All checks were successful
Mirror to GitHub / mirror (push) Successful in 4s
All checks were successful
Mirror to GitHub / mirror (push) Successful in 4s
- WhatWeKnow shows a "synthesizing" indicator + skeleton pulse while the chat cycle is in-flight; task-lane header mirrors the signal with a "thinking" pip so engineers know the AI is still working. - Quiet-state hint when the lane is open (facts exist) but no open questions, checks, or active fix — keeps the surface from looking "finished" when the AI is about to follow up. - Keyboard shortcuts: ⌘↵/Ctrl+↵ send in the composer (plain Enter still sends), ⌘G toggles the Script Generator panel for the active fix, `?` opens a new ShortcutsHelpOverlay listing all bindings. ⌘K palette was already wired in TopBar. - Responsive: below 1200px the task lane collapses to a bottom drawer with a backdrop + a floating "Tasks ●" toggle button. TaskLane now takes a `variant: 'side' | 'drawer'` prop; drawer variant drops the resize handle and uses the shared slide-in-bottom animation. - Build hygiene: fixed a pre-existing TS error in confirm-post error handling (duplicate `response` type keys) and an unused-import warning in TemplatizePrompt. Verified: `npx tsc -b` and `npm run build` both clean against the dev stack; Vite HMR applied each change without errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,9 @@ import { ResolutionNotePreview as ResolutionNotePreviewPopover } from '@/compone
|
||||
import { TemplateMatchPanel } from '@/components/pilot/script/TemplateMatchPanel'
|
||||
import { NoTemplateDialog } from '@/components/pilot/script/NoTemplateDialog'
|
||||
import { TemplatizePrompt } from '@/components/pilot/script/TemplatizePrompt'
|
||||
import { ShortcutsHelpOverlay } from '@/components/pilot/ShortcutsHelpOverlay'
|
||||
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery'
|
||||
import {
|
||||
draftTemplatesApi,
|
||||
accountPreferencesApi,
|
||||
@@ -121,6 +124,11 @@ export default function AssistantChatPage() {
|
||||
// advance to the next pending draft.
|
||||
const [templatizeQueue, setTemplatizeQueue] = useState<DraftTemplate[]>([])
|
||||
const [showOverflow, setShowOverflow] = useState(false)
|
||||
// Phase 7: keyboard-shortcut help overlay.
|
||||
const [shortcutsHelpOpen, setShortcutsHelpOpen] = useState(false)
|
||||
// Phase 7: below 1200px the task lane collapses to a bottom drawer per the
|
||||
// migration spec. Above, it's the standard right-side panel.
|
||||
const isNarrow = useMediaQuery('(max-width: 1199px)')
|
||||
const toggleSidebarCollapse = () => {
|
||||
const next = !sidebarCollapsed
|
||||
setSidebarCollapsed(next)
|
||||
@@ -539,8 +547,9 @@ export default function AssistantChatPage() {
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.error('[AssistantChat] confirm post failed:', err)
|
||||
const status = (err as { response?: { status?: number }; response?: { data?: { detail?: string } } })?.response?.status
|
||||
const detail = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail
|
||||
const errResp = (err as { response?: { status?: number; data?: { detail?: string } } })?.response
|
||||
const status = errResp?.status
|
||||
const detail = errResp?.data?.detail
|
||||
if (status === 502) {
|
||||
toast.error(detail || 'PSA posted partially — see server logs.')
|
||||
} else if (status === 409) {
|
||||
@@ -864,6 +873,14 @@ export default function AssistantChatPage() {
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
// ⌘↵ / Ctrl+↵ is the FlowPilot-wide "send" shortcut (Phase 7 spec).
|
||||
// Plain Enter (without modifiers, without shift) also sends to preserve
|
||||
// existing composer ergonomics. Shift+Enter keeps the newline behavior.
|
||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
return
|
||||
}
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
@@ -878,6 +895,25 @@ export default function AssistantChatPage() {
|
||||
el.style.height = `${Math.min(el.scrollHeight, 150)}px`
|
||||
}, [input])
|
||||
|
||||
// Phase 7: global keyboard shortcuts. The hook auto-skips keypresses that
|
||||
// originate inside inputs/textareas, so `?` and ⌘G won't fight the composer.
|
||||
// ⌘K is handled by the TopBar (opens the global command palette).
|
||||
useKeyboardShortcuts([
|
||||
{
|
||||
key: '?',
|
||||
shift: true,
|
||||
handler: () => setShortcutsHelpOpen((v) => !v),
|
||||
},
|
||||
{
|
||||
key: 'g',
|
||||
ctrl: true,
|
||||
handler: () => {
|
||||
if (activeFix) setScriptPanelOpen((v) => !v)
|
||||
},
|
||||
enabled: activeFix !== null,
|
||||
},
|
||||
])
|
||||
|
||||
// ── File handling ──────────────────────────────
|
||||
const ACCEPTED_FILE_TYPES = 'image/png,image/jpeg,image/gif,image/webp,.txt,.log,.csv,.pdf,.docx'
|
||||
|
||||
@@ -1344,11 +1380,128 @@ export default function AssistantChatPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Phase 7: on narrow viewports (<1200px) the lane is a bottom drawer
|
||||
that's hidden by default; a floating "Tasks" button toggles it.
|
||||
Shows a count pill when new items are present while closed. */}
|
||||
{isNarrow
|
||||
&& !showTaskLane
|
||||
&& (activeQuestions.length > 0 || activeActions.length > 0 || facts.length > 0 || activeFix !== null) && (
|
||||
<button
|
||||
onClick={() => setShowTaskLane(true)}
|
||||
className="fixed bottom-4 right-4 z-40 flex items-center gap-2 rounded-full bg-accent text-white px-4 py-2.5 shadow-lg hover:bg-accent-hover transition-colors"
|
||||
title="Open tasks drawer"
|
||||
>
|
||||
<ListChecks size={14} />
|
||||
<span className="text-[0.8125rem] font-semibold">Tasks</span>
|
||||
{(activeQuestions.length + activeActions.length) > 0 && (
|
||||
<span className="rounded-full bg-white/20 px-1.5 py-0.5 text-[0.625rem] font-bold tabular-nums">
|
||||
{activeQuestions.length + activeActions.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Task lane — slides in when AI sends questions/actions OR when the
|
||||
session has any "What we know" facts OR an active suggested fix.
|
||||
Phase 2/3 make the lane the structural home of session diagnostic
|
||||
state, not a transient questions panel. */}
|
||||
state, not a transient questions panel.
|
||||
Narrow viewport: the lane renders as a bottom drawer with backdrop. */}
|
||||
{showTaskLane && (activeQuestions.length > 0 || activeActions.length > 0 || facts.length > 0 || activeFix !== null) && (
|
||||
isNarrow ? (
|
||||
<div className="fixed inset-0 z-50 flex flex-col" role="dialog" aria-modal="true">
|
||||
<div
|
||||
className="flex-1 bg-black/50"
|
||||
onClick={() => setShowTaskLane(false)}
|
||||
/>
|
||||
<div className="h-[80vh] bg-bg-page">
|
||||
<TaskLane
|
||||
variant="drawer"
|
||||
questions={activeQuestions}
|
||||
actions={activeActions}
|
||||
sessionId={activeChatId}
|
||||
onSubmit={handleTaskSubmit}
|
||||
onClose={() => setShowTaskLane(false)}
|
||||
loading={loading}
|
||||
whatWeKnowSlot={
|
||||
<WhatWeKnow
|
||||
facts={facts}
|
||||
onAddNote={handleAddNote}
|
||||
onUpdateFact={handleUpdateFact}
|
||||
onDeleteFact={handleDeleteFact}
|
||||
loading={loading}
|
||||
/>
|
||||
}
|
||||
suggestedFixSlot={
|
||||
activeFix && (
|
||||
<SuggestedFix
|
||||
fix={activeFix}
|
||||
onDismiss={handleDismissFix}
|
||||
onActivate={() => setScriptPanelOpen((prev) => !prev)}
|
||||
panelOpen={scriptPanelOpen}
|
||||
/>
|
||||
)
|
||||
}
|
||||
bottomSlot={
|
||||
<>
|
||||
{scriptPanelOpen && activeFix && activeChatId && (
|
||||
activeFix.script_template_id ? (
|
||||
<TemplateMatchPanel
|
||||
fix={activeFix}
|
||||
sessionId={activeChatId}
|
||||
onClose={() => setScriptPanelOpen(false)}
|
||||
/>
|
||||
) : (
|
||||
<NoTemplateDialog
|
||||
fix={activeFix}
|
||||
onClose={() => setScriptPanelOpen(false)}
|
||||
onDecide={handleScriptDecision}
|
||||
busy={scriptDecisionBusy}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div className="flex items-center gap-3 px-3 mt-1">
|
||||
<button
|
||||
onClick={() => handleOpenPreview('resolve')}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 text-[0.75rem] font-medium transition-colors',
|
||||
previewKind === 'resolve'
|
||||
? 'text-success'
|
||||
: 'text-accent-text hover:text-heading',
|
||||
)}
|
||||
>
|
||||
<FileText size={12} />
|
||||
{previewKind === 'resolve' ? 'Showing' : 'Preview'} Resolve note
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleOpenPreview('escalate')}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 text-[0.75rem] font-medium transition-colors',
|
||||
previewKind === 'escalate'
|
||||
? 'text-warning'
|
||||
: 'text-muted-foreground hover:text-heading',
|
||||
)}
|
||||
>
|
||||
<ArrowUpRight size={12} />
|
||||
{previewKind === 'escalate' ? 'Showing' : 'Escalate instead'}
|
||||
</button>
|
||||
</div>
|
||||
<ResolutionNotePreviewPopover
|
||||
kind={previewKind ?? 'resolve'}
|
||||
open={previewOpen}
|
||||
loading={previewLoading}
|
||||
preview={previewData}
|
||||
error={previewError}
|
||||
onClose={handleClosePreview}
|
||||
onRefresh={() => { if (activeChatId) return refreshPreview(activeChatId) }}
|
||||
onConfirm={handleConfirmPost}
|
||||
posting={previewPosting}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<TaskLane
|
||||
questions={activeQuestions}
|
||||
actions={activeActions}
|
||||
@@ -1364,6 +1517,7 @@ export default function AssistantChatPage() {
|
||||
onAddNote={handleAddNote}
|
||||
onUpdateFact={handleUpdateFact}
|
||||
onDeleteFact={handleDeleteFact}
|
||||
loading={loading}
|
||||
/>
|
||||
}
|
||||
suggestedFixSlot={
|
||||
@@ -1427,13 +1581,14 @@ export default function AssistantChatPage() {
|
||||
preview={previewData}
|
||||
error={previewError}
|
||||
onClose={handleClosePreview}
|
||||
onRefresh={() => activeChatId && refreshPreview(activeChatId)}
|
||||
onRefresh={() => { if (activeChatId) return refreshPreview(activeChatId) }}
|
||||
onConfirm={handleConfirmPost}
|
||||
posting={previewPosting}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Branch map hidden — branching is now silent/background only.
|
||||
@@ -1473,6 +1628,12 @@ export default function AssistantChatPage() {
|
||||
onResolved={() => setTemplatizeQueue((q) => q.slice(1))}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Phase 7: keyboard-shortcut help. `?` from anywhere inside /pilot. */}
|
||||
<ShortcutsHelpOverlay
|
||||
open={shortcutsHelpOpen}
|
||||
onClose={() => setShortcutsHelpOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user