fix: ContinuationModal UX - hover tooltips and stay-on-step flow
- Replace grouped section headers with hover tooltips (title attr) for a cleaner flat list of descendant options - After selecting a descendant, stay on the custom step so the user can write notes before proceeding via a "Continue to" button - Add pendingContinuationNodeId state to track selected descendant - "Continue to" and custom branch controls are mutually exclusive Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
39
docs/plans/2026-02-03-continuation-modal-ux.md
Normal file
39
docs/plans/2026-02-03-continuation-modal-ux.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Continuation Modal UX Improvements
|
||||||
|
|
||||||
|
**Date:** 2026-02-03
|
||||||
|
**Status:** Approved
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### 1. Parent option labels: hover tooltips instead of static headers
|
||||||
|
|
||||||
|
The ContinuationModal currently groups descendant nodes under uppercase headers like "FROM: PRINTER WON'T PRINT AT ALL -> NOTHING HAPPENS / NO RESPONSE". These clutter the screen.
|
||||||
|
|
||||||
|
**Change:** Move `parentOptionLabel` from static section headers to a `title` attribute on each descendant button. The descendants become a flat list. Hovering shows the path context as a native browser tooltip.
|
||||||
|
|
||||||
|
The "Or" divider and "Build Custom Branch" button at the bottom remain unchanged.
|
||||||
|
|
||||||
|
**Files:** `frontend/src/components/session/ContinuationModal.tsx`
|
||||||
|
|
||||||
|
### 2. Land on custom step before navigating to selected descendant
|
||||||
|
|
||||||
|
Currently, selecting a descendant in the ContinuationModal navigates directly to that node. The user should land on their custom step first to write notes, then proceed.
|
||||||
|
|
||||||
|
**Change:** Add `pendingContinuationNodeId` state to `TreeNavigationPage`. When the user selects a descendant:
|
||||||
|
|
||||||
|
1. Store the selected node ID in `pendingContinuationNodeId`
|
||||||
|
2. Close the ContinuationModal (user is now viewing their custom step)
|
||||||
|
3. Render a "Continue to: [node name]" button on the custom step view
|
||||||
|
4. Clicking it navigates to the descendant and clears `pendingContinuationNodeId`
|
||||||
|
|
||||||
|
The "Continue to" button and custom branch controls are mutually exclusive. If the user chose "Build Custom Branch", they get the existing Add Another Step / This Solves My Issue controls instead.
|
||||||
|
|
||||||
|
**Files:** `frontend/src/pages/TreeNavigationPage.tsx`
|
||||||
|
|
||||||
|
## Summary of state changes
|
||||||
|
|
||||||
|
| State | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `pendingContinuationNodeId` | Stores which descendant the user selected, so the custom step can show a "Continue to" button |
|
||||||
|
|
||||||
|
## No backend changes required
|
||||||
@@ -37,15 +37,6 @@ export function ContinuationModal({
|
|||||||
onSelectNode,
|
onSelectNode,
|
||||||
onBuildCustomBranch
|
onBuildCustomBranch
|
||||||
}: ContinuationModalProps) {
|
}: ContinuationModalProps) {
|
||||||
// Group nodes by parent option
|
|
||||||
const groupedNodes = descendantNodes.reduce((acc, node) => {
|
|
||||||
if (!acc[node.parentOptionLabel]) {
|
|
||||||
acc[node.parentOptionLabel] = []
|
|
||||||
}
|
|
||||||
acc[node.parentOptionLabel].push(node)
|
|
||||||
return acc
|
|
||||||
}, {} as Record<string, DescendantNode[]>)
|
|
||||||
|
|
||||||
const hasDescendants = descendantNodes.length > 0
|
const hasDescendants = descendantNodes.length > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,36 +49,28 @@ export function ContinuationModal({
|
|||||||
Select the next step in your troubleshooting path:
|
Select the next step in your troubleshooting path:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-2">
|
||||||
{Object.entries(groupedNodes).map(([parentOption, nodes]) => (
|
{descendantNodes.map(node => (
|
||||||
<div key={parentOption}>
|
<button
|
||||||
<p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
key={node.id}
|
||||||
From: {parentOption}
|
onClick={() => onSelectNode(node.id)}
|
||||||
</p>
|
title={`From: ${node.parentOptionLabel}`}
|
||||||
<div className="space-y-2">
|
className={cn(
|
||||||
{nodes.map(node => (
|
'flex w-full items-center gap-3 rounded-lg border border-border p-3 text-left transition-colors',
|
||||||
<button
|
'hover:border-primary hover:bg-accent'
|
||||||
key={node.id}
|
)}
|
||||||
onClick={() => onSelectNode(node.id)}
|
>
|
||||||
className={cn(
|
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-muted">
|
||||||
'flex w-full items-center gap-3 rounded-lg border border-border p-3 text-left transition-colors',
|
{nodeTypeIcons[node.type]}
|
||||||
'hover:border-primary hover:bg-accent'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-muted">
|
|
||||||
{nodeTypeIcons[node.type]}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="truncate font-medium">{node.label}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{nodeTypeLabels[node.type]}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<ArrowRight className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate font-medium">{node.label}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{nodeTypeLabels[node.type]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ArrowRight className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { cn } from '@/lib/utils'
|
|||||||
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
||||||
import { CustomStepModal } from '@/components/step-library/CustomStepModal'
|
import { CustomStepModal } from '@/components/step-library/CustomStepModal'
|
||||||
import { PostStepActionModal, ContinuationModal, ForkTreeModal, type DescendantNode } from '@/components/session'
|
import { PostStepActionModal, ContinuationModal, ForkTreeModal, type DescendantNode } from '@/components/session'
|
||||||
import { Plus, CheckCircle } from 'lucide-react'
|
import { Plus, CheckCircle, ArrowRight } from 'lucide-react'
|
||||||
|
|
||||||
interface LocationState {
|
interface LocationState {
|
||||||
sessionId?: string
|
sessionId?: string
|
||||||
@@ -48,6 +48,7 @@ export function TreeNavigationPage() {
|
|||||||
// Continuation flow
|
// Continuation flow
|
||||||
const [showContinuationModal, setShowContinuationModal] = useState(false)
|
const [showContinuationModal, setShowContinuationModal] = useState(false)
|
||||||
const [branchOriginNodeId, setBranchOriginNodeId] = useState<string | null>(null)
|
const [branchOriginNodeId, setBranchOriginNodeId] = useState<string | null>(null)
|
||||||
|
const [pendingContinuationNodeId, setPendingContinuationNodeId] = useState<string | null>(null)
|
||||||
|
|
||||||
// Custom branch mode
|
// Custom branch mode
|
||||||
const [customBranchMode, setCustomBranchMode] = useState(false)
|
const [customBranchMode, setCustomBranchMode] = useState(false)
|
||||||
@@ -409,18 +410,26 @@ export function TreeNavigationPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle selecting a descendant node from continuation modal
|
// Handle selecting a descendant node from continuation modal.
|
||||||
const handleSelectDescendant = async (nodeId: string) => {
|
// Stores the selection so the user can write notes on their custom step first.
|
||||||
|
const handleSelectDescendant = (nodeId: string) => {
|
||||||
setShowContinuationModal(false)
|
setShowContinuationModal(false)
|
||||||
setBranchOriginNodeId(null)
|
setBranchOriginNodeId(null)
|
||||||
|
setPendingContinuationNodeId(nodeId)
|
||||||
|
}
|
||||||
|
|
||||||
const newPath = [...pathTaken, nodeId]
|
// Navigate to the previously-selected descendant node
|
||||||
|
const handleContinueToDescendant = async () => {
|
||||||
|
if (!pendingContinuationNodeId || !session) return
|
||||||
|
|
||||||
|
const newPath = [...pathTaken, pendingContinuationNodeId]
|
||||||
setPathTaken(newPath)
|
setPathTaken(newPath)
|
||||||
setCurrentNodeId(nodeId)
|
setCurrentNodeId(pendingContinuationNodeId)
|
||||||
setNotes('')
|
setNotes('')
|
||||||
|
setPendingContinuationNodeId(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sessionsApi.update(session!.id, { path_taken: newPath })
|
await sessionsApi.update(session.id, { path_taken: newPath })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to update session path:', err)
|
console.error('Failed to update session path:', err)
|
||||||
}
|
}
|
||||||
@@ -777,6 +786,27 @@ export function TreeNavigationPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Continue to selected descendant */}
|
||||||
|
{pendingContinuationNodeId && !customBranchMode && (() => {
|
||||||
|
const targetNode = findNode(pendingContinuationNodeId, tree?.tree_structure)
|
||||||
|
const targetLabel = targetNode?.question || targetNode?.title || 'next step'
|
||||||
|
return (
|
||||||
|
<div className="mt-6 border-t border-purple-200 pt-4 dark:border-purple-700">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleContinueToDescendant}
|
||||||
|
className={cn(
|
||||||
|
'flex w-full items-center justify-between rounded-md bg-primary px-4 py-3 text-sm font-medium text-primary-foreground',
|
||||||
|
'hover:bg-primary/90'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>Continue to: {targetLabel.length > 50 ? `${targetLabel.slice(0, 50)}...` : targetLabel}</span>
|
||||||
|
<ArrowRight className="h-4 w-4 flex-shrink-0" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Custom Branch Controls */}
|
{/* Custom Branch Controls */}
|
||||||
{customBranchMode && (
|
{customBranchMode && (
|
||||||
<div className="mt-6 border-t border-purple-200 pt-4 dark:border-purple-700">
|
<div className="mt-6 border-t border-purple-200 pt-4 dark:border-purple-700">
|
||||||
|
|||||||
Reference in New Issue
Block a user