diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx index b837aa8d..3b7a02d5 100644 --- a/frontend/src/pages/AssistantChatPage.tsx +++ b/frontend/src/pages/AssistantChatPage.tsx @@ -20,8 +20,10 @@ import type { BannerMode } from '@/components/pilot/ProposalBanner' import { EscalateInterceptDialog } from '@/components/pilot/EscalateInterceptDialog' import type { InterceptChoice } from '@/components/pilot/EscalateInterceptDialog' import { TemplateMatchPanel } from '@/components/pilot/script/TemplateMatchPanel' -import { NoTemplateDialog } from '@/components/pilot/script/NoTemplateDialog' import { TemplatizePrompt } from '@/components/pilot/script/TemplatizePrompt' +import { ChatTabStrip, type ChatTab } from '@/components/pilot/ChatTabStrip' +import { ScriptBuilderTab } from '@/components/pilot/ScriptBuilderTab' +import { InlineNoTemplateDialog } from '@/components/pilot/InlineNoTemplateDialog' import { ShortcutsHelpOverlay } from '@/components/pilot/ShortcutsHelpOverlay' import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts' import { useMediaQuery } from '@/hooks/useMediaQuery' @@ -138,6 +140,9 @@ export default function AssistantChatPage() { const [postApplyMsgCount, setPostApplyMsgCount] = useState(0) const [nudgeSilenced, setNudgeSilenced] = useState(false) const [escalateIntercept, setEscalateIntercept] = useState<{ fixId: string; fixTitle: string } | null>(null) + // Phase 9: ChatTabStrip + ScriptBuilderTab state. + const [chatTab, setChatTab] = useState('chat') + const [scriptBuilderHasProgress, setScriptBuilderHasProgress] = useState(false) // Phase 8: compute the current banner mode from activeFix. // applied_at is now persisted on the server (stamped by POST /apply), // so bannerMode is derived entirely from server state — no client-side flag. @@ -154,6 +159,22 @@ export default function AssistantChatPage() { return 'proposed' })() + // Phase 9: show the tab strip when the fix needs a script drafted (no template, + // no drafted script yet, and still in a live state). + const showTabStrip = + activeFix != null + && activeFix.status !== 'dismissed' + && activeFix.status !== 'applied_success' + && activeFix.status !== 'applied_failed' + && !activeFix.script_template_id + && !activeFix.ai_drafted_script + + // Defensive: if the strip hides (fix resolved/dismissed/script-drafted), + // snap back to the Chat tab so the user doesn't land on a blank panel. + useEffect(() => { + if (!showTabStrip && chatTab === 'script_builder') setChatTab('chat') + }, [showTabStrip, chatTab]) + const toggleSidebarCollapse = () => { const next = !sidebarCollapsed setSidebarCollapsed(next) @@ -343,6 +364,9 @@ export default function AssistantChatPage() { setPostApplyMsgCount(0) setNudgeSilenced(false) setEscalateIntercept(null) + // Phase 9: tab strip reset + setChatTab('chat') + setScriptBuilderHasProgress(false) }, []) // Phase 2 facts — fetch + handlers. `refreshFacts` is called from selectChat @@ -511,23 +535,22 @@ export default function AssistantChatPage() { refreshPreview(activeChatId, kind) }, [activeChatId, previewKind, refreshPreview]) - // Phase 8: handleApplyFix — stamps applied_at on the server so Verifying state - // survives refresh/reselect/multi-tab, then opens the script panel. - const handleApplyFix = useCallback(async () => { - if (!activeFix || !activeChatId) return - setPostApplyMsgCount(0) - setNudgeSilenced(false) - try { - const updated = await sessionSuggestedFixesApi.applyFix(activeChatId, activeFix.id) - setActiveFix(updated) - } catch (err: unknown) { - // Non-fatal: the script panel still opens. The banner will stay in - // 'proposed' mode until the next refreshActiveFix succeeds, which is - // a cosmetic gap only — no data loss. - console.error('[AssistantChat] applyFix failed:', err) + // Phase 9: handleApplyFix — routes to the appropriate surface based on + // fix state. applyFix() call site moves to Task 13 (handleScriptDecision + // and TemplateMatchPanel.onMarkRun). + const handleApplyFix = useCallback(() => { + if (!activeFix) return + if (activeFix.script_template_id) { + setScriptPanelOpen(true) // existing TemplateMatchPanel flow in task lane + return } - setScriptPanelOpen(true) - }, [activeFix, activeChatId]) + if (activeFix.ai_drafted_script) { + setScriptPanelOpen(true) // InlineNoTemplateDialog, now in chat region (Step 5) + return + } + // No draft, no template — route to the Script Builder tab. + setChatTab('script_builder') + }, [activeFix]) // Phase 8: record a terminal outcome for the active fix. Updates local state // on success. For applied_success also opens the Resolve preview. @@ -1372,6 +1395,19 @@ export default function AssistantChatPage() { ) })()} + {/* Phase 9: ChatTabStrip — shown when the fix needs a script drafted */} + {showTabStrip && ( + + )} + + {/* Chat tab content — messages + banner + composer. + Hidden (not unmounted) when Script Builder tab is active so + scroll position and input state are preserved. */} +
{/* Messages */}
{messages.length === 0 && !loading && ( @@ -1426,6 +1462,18 @@ export default function AssistantChatPage() { /> )} + {/* Phase 9: InlineNoTemplateDialog — drafted-script evaluation case, + rendered in the chat region above the composer so all three + option cards fit side-by-side without the TaskLane's narrow width. */} + {scriptPanelOpen && activeFix && activeChatId && !activeFix.script_template_id && activeFix.ai_drafted_script && ( + setScriptPanelOpen(false)} + onDecide={handleScriptDecision} + busy={scriptDecisionBusy} + /> + )} + {/* Rich Input */}
+
{/* end chat-tab content wrapper */} + + {/* Phase 9: Script Builder tab — mounted alongside chat via display:none + so both scroll positions and state are preserved across tab switches. */} + {showTabStrip && activeFix && activeChatId && ( +
+ { + setActiveFix(updated) + setChatTab('chat') + setScriptBuilderHasProgress(false) + }} + /> +
+ )} ) : (
@@ -1626,21 +1692,12 @@ export default function AssistantChatPage() { } bottomSlot={ <> - {scriptPanelOpen && activeFix && activeChatId && ( - activeFix.script_template_id ? ( - setScriptPanelOpen(false)} - /> - ) : ( - setScriptPanelOpen(false)} - onDecide={handleScriptDecision} - busy={scriptDecisionBusy} - /> - ) + {scriptPanelOpen && activeFix && activeChatId && activeFix.script_template_id && ( + setScriptPanelOpen(false)} + /> )}