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:
Michael Chihlas
2026-02-03 21:48:00 -05:00
parent 27624fbe55
commit f49e0b9327
3 changed files with 96 additions and 44 deletions

View File

@@ -37,15 +37,6 @@ export function ContinuationModal({
onSelectNode,
onBuildCustomBranch
}: 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
return (
@@ -58,36 +49,28 @@ export function ContinuationModal({
Select the next step in your troubleshooting path:
</p>
<div className="space-y-4">
{Object.entries(groupedNodes).map(([parentOption, nodes]) => (
<div key={parentOption}>
<p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
From: {parentOption}
</p>
<div className="space-y-2">
{nodes.map(node => (
<button
key={node.id}
onClick={() => onSelectNode(node.id)}
className={cn(
'flex w-full items-center gap-3 rounded-lg border border-border p-3 text-left transition-colors',
'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 className="space-y-2">
{descendantNodes.map(node => (
<button
key={node.id}
onClick={() => onSelectNode(node.id)}
title={`From: ${node.parentOptionLabel}`}
className={cn(
'flex w-full items-center gap-3 rounded-lg border border-border p-3 text-left transition-colors',
'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>
<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>

View File

@@ -8,7 +8,7 @@ import { cn } from '@/lib/utils'
import { MarkdownContent } from '@/components/ui/MarkdownContent'
import { CustomStepModal } from '@/components/step-library/CustomStepModal'
import { PostStepActionModal, ContinuationModal, ForkTreeModal, type DescendantNode } from '@/components/session'
import { Plus, CheckCircle } from 'lucide-react'
import { Plus, CheckCircle, ArrowRight } from 'lucide-react'
interface LocationState {
sessionId?: string
@@ -48,6 +48,7 @@ export function TreeNavigationPage() {
// Continuation flow
const [showContinuationModal, setShowContinuationModal] = useState(false)
const [branchOriginNodeId, setBranchOriginNodeId] = useState<string | null>(null)
const [pendingContinuationNodeId, setPendingContinuationNodeId] = useState<string | null>(null)
// Custom branch mode
const [customBranchMode, setCustomBranchMode] = useState(false)
@@ -409,18 +410,26 @@ export function TreeNavigationPage() {
}
}
// Handle selecting a descendant node from continuation modal
const handleSelectDescendant = async (nodeId: string) => {
// Handle selecting a descendant node from continuation modal.
// Stores the selection so the user can write notes on their custom step first.
const handleSelectDescendant = (nodeId: string) => {
setShowContinuationModal(false)
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)
setCurrentNodeId(nodeId)
setCurrentNodeId(pendingContinuationNodeId)
setNotes('')
setPendingContinuationNodeId(null)
try {
await sessionsApi.update(session!.id, { path_taken: newPath })
await sessionsApi.update(session.id, { path_taken: newPath })
} catch (err) {
console.error('Failed to update session path:', err)
}
@@ -777,6 +786,27 @@ export function TreeNavigationPage() {
</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 */}
{customBranchMode && (
<div className="mt-6 border-t border-purple-200 pt-4 dark:border-purple-700">