From 2fc4e69c38c1cf8a09cfba21f07fd792b782a1ef Mon Sep 17 00:00:00 2001 From: chihlasm Date: Fri, 13 Feb 2026 02:19:24 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20UX=20improvements=20=E2=80=94=20copy=20?= =?UTF-8?q?buttons,=20shortcuts=20modal,=20breadcrumb=20rewind,=20create?= =?UTF-8?q?=20tree=20CTA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add copy-to-clipboard buttons on command blocks (action + custom step nodes) - Add keyboard shortcuts modal (?) with option loading spinners and [Esc] hint - Restyle session timer as a pill badge for better visibility - Add prominent "Create Tree" CTA to MyTreesPage header and empty state - Make breadcrumb items clickable to rewind navigation to any previous step Co-Authored-By: Claude Opus 4.6 --- frontend/src/hooks/useKeyboardShortcuts.ts | 9 ++ frontend/src/pages/MyTreesPage.tsx | 55 +++++-- frontend/src/pages/TreeNavigationPage.tsx | 164 +++++++++++++++++---- 3 files changed, 182 insertions(+), 46 deletions(-) diff --git a/frontend/src/hooks/useKeyboardShortcuts.ts b/frontend/src/hooks/useKeyboardShortcuts.ts index c25fd59f..332c1351 100644 --- a/frontend/src/hooks/useKeyboardShortcuts.ts +++ b/frontend/src/hooks/useKeyboardShortcuts.ts @@ -51,6 +51,7 @@ export interface TreeNavigationShortcutsConfig { onSelectOption: (index: number) => void onGoBack: () => void onContinue: () => void + onShowShortcuts?: () => void optionCount: number canGoBack: boolean canContinue: boolean @@ -60,6 +61,7 @@ export function useTreeNavigationShortcuts({ onSelectOption, onGoBack, onContinue, + onShowShortcuts, optionCount, canGoBack, canContinue, @@ -89,6 +91,13 @@ export function useTreeNavigationShortcuts({ document.getElementById('session-notes')?.focus() }, }, + // ? to show shortcuts modal + { + key: '?', + shift: true, + handler: () => onShowShortcuts?.(), + enabled: !!onShowShortcuts, + }, ] useKeyboardShortcuts(shortcuts) diff --git a/frontend/src/pages/MyTreesPage.tsx b/frontend/src/pages/MyTreesPage.tsx index 567524dc..607cea58 100644 --- a/frontend/src/pages/MyTreesPage.tsx +++ b/frontend/src/pages/MyTreesPage.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { useNavigate, Link } from 'react-router-dom' -import { Play, Pencil, Share2, Trash2, GitBranch, Clock, TrendingUp, FolderTree } from 'lucide-react' +import { Play, Pencil, Share2, Trash2, GitBranch, Clock, TrendingUp, FolderTree, Plus } from 'lucide-react' import { treesApi } from '@/api/trees' import { sessionsApi } from '@/api/sessions' import type { TreeListItem } from '@/types' @@ -22,7 +22,7 @@ interface TreeWithStats extends TreeListItem { export function MyTreesPage() { const navigate = useNavigate() const { user } = useAuthStore() - const { canEditTree } = usePermissions() + const { canEditTree, canCreateTrees } = usePermissions() const [trees, setTrees] = useState([]) const [isLoading, setIsLoading] = useState(true) const [treeToDelete, setTreeToDelete] = useState(null) @@ -101,11 +101,22 @@ export function MyTreesPage() { return (
-
-

My Trees

-

- Your forked and custom decision trees -

+
+
+

My Trees

+

+ Your forked and custom decision trees +

+
+ {canCreateTrees && ( + + + Create Tree + + )}
{/* Loading State */} @@ -120,15 +131,29 @@ export function MyTreesPage() {

Fork a tree from the library to customize it for your workflow

- + + Browse Library + + {canCreateTrees && ( + + + Create from Scratch + )} - > - Browse Trees - +
) : (
diff --git a/frontend/src/pages/TreeNavigationPage.tsx b/frontend/src/pages/TreeNavigationPage.tsx index 1ffaff95..18930f0c 100644 --- a/frontend/src/pages/TreeNavigationPage.tsx +++ b/frontend/src/pages/TreeNavigationPage.tsx @@ -10,7 +10,8 @@ import { cn, safeGetItem, safeSetItem } from '@/lib/utils' import { MarkdownContent } from '@/components/ui/MarkdownContent' import { CustomStepModal } from '@/components/step-library/CustomStepModal' import { PostStepActionModal, ContinuationModal, ForkTreeModal, ScratchpadSidebar, SessionOutcomeModal } from '@/components/session' -import { Plus, CheckCircle, ArrowRight, Clock, Terminal } from 'lucide-react' +import { Plus, CheckCircle, ArrowRight, Clock, Terminal, Clipboard, Check, HelpCircle } from 'lucide-react' +import { Modal } from '@/components/common/Modal' interface LocationState { sessionId?: string @@ -41,6 +42,15 @@ export function TreeNavigationPage() { const [showOutcomeModal, setShowOutcomeModal] = useState(false) const [pendingCompletionDecision, setPendingCompletionDecision] = useState(null) const [completionSource, setCompletionSource] = useState('standard') + const [copiedCommand, setCopiedCommand] = useState(null) + const [shortcutsModalOpen, setShortcutsModalOpen] = useState(false) + const [selectingOption, setSelectingOption] = useState(null) + + const handleCopyCommand = (text: string) => { + navigator.clipboard.writeText(text) + setCopiedCommand(text) + setTimeout(() => setCopiedCommand(null), 2000) + } // Session metadata (prefill from Repeat Last Session) const [ticketNumber, setTicketNumber] = useState(locationState?.prefillTicketNumber || '') @@ -220,10 +230,12 @@ export function TreeNavigationPage() { } const handleSelectOption = async (_optionId: string, optionLabel: string, nextNodeId: string) => { - if (!session || !tree) return + if (!session || !tree || selectingOption) return + + setSelectingOption(_optionId) const node = findNode(currentNodeId, tree.tree_structure) - if (!node) return + if (!node) { setSelectingOption(null); return } const exitedAt = new Date().toISOString() const enteredAt = currentStepEnteredAt || session.started_at || exitedAt @@ -259,6 +271,8 @@ export function TreeNavigationPage() { }) } catch (err) { console.error('Failed to update session:', err) + } finally { + setSelectingOption(null) } } @@ -370,6 +384,16 @@ export function TreeNavigationPage() { setCommandOutputOpen(!!prevOutput) } + const handleBreadcrumbJump = (nodeId: string, index: number) => { + setPathTaken(prev => prev.slice(0, index + 1)) + setDecisions(prev => prev.slice(0, index)) + setCurrentNodeId(nodeId) + setCurrentStepEnteredAt(new Date().toISOString()) + setNotes('') + setCommandOutput('') + setCommandOutputOpen(false) + } + // Compute current node for keyboard shortcuts (must be before any returns for hooks rules) const currentNode = tree ? findNode(currentNodeId, tree.tree_structure) : null const currentCustomStep = customStepFlow.findCustomStep(currentNodeId) @@ -379,11 +403,12 @@ export function TreeNavigationPage() { useTreeNavigationShortcuts({ onSelectOption: (index) => { const option = currentOptions[index] - if (option && session && tree) { + if (option && session && tree && !selectingOption) { handleSelectOption(option.id, option.label, option.next_node_id) } }, onGoBack: handleGoBack, + onShowShortcuts: () => setShortcutsModalOpen(true), onContinue: () => { if (currentNode?.type === 'action' && currentNode.next_node_id) { handleContinue() @@ -392,8 +417,8 @@ export function TreeNavigationPage() { } }, optionCount: currentOptions.length, - canGoBack: pathTaken.length > 1 && !showMetadataForm && !isLoading, - canContinue: !showMetadataForm && !isLoading && (currentNode?.type === 'action' || currentNode?.type === 'solution'), + canGoBack: pathTaken.length > 1 && !showMetadataForm && !isLoading && !selectingOption, + canContinue: !showMetadataForm && !isLoading && !selectingOption && (currentNode?.type === 'action' || currentNode?.type === 'solution'), }) if (isLoading) { @@ -502,11 +527,19 @@ export function TreeNavigationPage() {

{tree.name}

{timerDisplay && ( - - + + {timerDisplay} )} +
{(ticketNumber || clientName) && (

@@ -530,18 +563,23 @@ export function TreeNavigationPage() { const node = findNode(nodeId, tree?.tree_structure) const customStep = customStepFlow.findCustomStep(nodeId) const label = node?.question || node?.title || customStep?.step_data.title || nodeId + const truncatedLabel = label.length > 30 ? `${label.slice(0, 30)}...` : label return ( {index > 0 && } - - {label.length > 30 ? `${label.slice(0, 30)}...` : label} - + {index < pathTaken.length - 1 ? ( + + ) : ( + + {truncatedLabel} + + )} ) })} @@ -565,16 +603,24 @@ export function TreeNavigationPage() { @@ -650,9 +696,23 @@ export function TreeNavigationPage() { {currentCustomStep.step_data.content.commands.map((cmd, index) => (

{cmd.label}

- - {cmd.command} - +
+ + {cmd.command} + + +
))}
@@ -761,12 +821,23 @@ export function TreeNavigationPage() {

Commands:

{currentNode.commands.map((cmd, index) => ( - - {cmd} - +
+ + {cmd} + + +
))}
{/* Command Output Capture */} @@ -885,7 +956,7 @@ export function TreeNavigationPage() { onClick={handleGoBack} className="mt-4 text-sm text-white/50 hover:text-white" > - ← Go back + ← Go back [Esc] )} @@ -950,6 +1021,37 @@ export function TreeNavigationPage() { onSubmit={handleSubmitOutcome} isSubmitting={isCompleting} /> + + {/* Keyboard Shortcuts Modal */} + setShortcutsModalOpen(false)} + title="Keyboard Shortcuts" + size="sm" + > +
+
+ Select option + 1-9 +
+
+ Go back + Esc +
+
+ Continue / Complete + Enter +
+
+ Focus notes + Tab +
+
+ Show shortcuts + ? +
+
+