fix: UX deep dive — 28 fixes across authoring, navigation, consistency, and cleanup (#86)

* fix: tree editor authoring blockers - scroll trap, form density, branching hint

- Replace fixed viewport height with flex layout in NodeEditorPanel
- Make footer sticky so Save/Cancel always reachable
- Compact root node banner to single-line with InfoTip tooltip
- Reduce resolution note from callout box to inline text
- Add answer-first branching hint below options label

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: broken functionality - auth errors, toast logic, role update, routing, step library

- Extract backend error detail in auth store login/register
- Fix inverted 4xx toast logic and add 429 rate limit handling
- Send account_role field to match backend schema in role update
- Use type-aware routing for Repeat Last Session button
- Add step library placeholder page and route, remove dot badge

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: navigation correctness - back buttons, exit dialog, dedup nav, redirects

- Standardize all procedural back/exit paths to /trees (not /my-trees)
- Add exit button with ConfirmDialog to procedural session top bar
- Consolidate duplicate account links in sidebar and topbar
- Auto-redirect non-owners to personal analytics
- Add toast feedback before silent permission redirects in tree editor
- Delete orphaned AdminCategoriesPage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: shared components, ConfirmDialog migration, pinned flow fixes

- Create shared Spinner component with sm/md/lg sizes
- Migrate 13 page-level spinners to shared Spinner
- Promote EmptyState to shared component, adopt in MyShares and SessionHistory
- Replace window.confirm with ConfirmDialog in 3 files
- Fix PinnedFlow.tree_type to include maintenance, update emoji display
- Verify sidebar unpin handler already correct (no-op)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: visual consistency - toasts, typography, focus rings, container padding

- Remove richColors from Sonner toasts, limit stacking to 3
- Add font-heading to all page H1s (7 files)
- Add font-label (Outfit) to TagBadges component
- Fix focus ring tokens on analytics pages
- Replace deprecated glass-stat with design system tokens
- Standardize container padding on analytics pages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: backend alignment - remove drafts toggle, clean dead code, truncation indicator

- Remove non-functional drafts toggle and clean TreeFilters type
- Fix AccountInvite type to match backend schema
- Remove dead API methods: pinnedFlows.pin/reorder, trees.getSharedTree
- Remove unused types: SessionListResponse, RatingCreate.is_verified_use
- Add session list truncation indicator with size=51 lookahead

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove bg-black from PageLoader and RouteError, fix PageLoader height

PageLoader used h-screen inside a grid cell, causing it to overflow.
Changed to h-full so it fits within the main-content area. Removed
bg-black from both PageLoader and RouteError in favor of theme-aware
bg-background to prevent black flash during lazy loading.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: guard against Pydantic validation error objects in toast/error messages

FastAPI returns `detail` as an array of objects for 422 validation errors,
not a string. Passing these objects to toast.error() or rendering them in
JSX crashes React with Error #31 ("Objects are not valid as a React child").
Now checks typeof detail === 'string' before using it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: toast styling, node editor first-click, action node placeholder pattern

1. Toast fixes: Add theme="dark" to Sonner, use !important CSS overrides
   instead of zero-specificity :where() selectors, suppress noisy 4xx
   global toasts (pages handle their own errors)

2. Node editor first-click: Add node.type to draft initialization
   useEffect deps so draft resets when answer stub converts to real type

3. Action node redesign: Remove NodePicker dropdown, auto-create answer
   placeholder on save (matching decision node pattern). Users click the
   placeholder on canvas to choose type and fill in details.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: auto-seed test users when release command fails on PR envs

The background seeder now creates users directly via DB if login fails,
instead of silently aborting. This handles Railway PR environments where
the releaseCommand may not execute properly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove categories/tags from sidebar to prevent footer clipping

Categories and Tags sections were pushing Feedback, Account, and
Collapse off-screen when All Flows expanded its children. These
filters already exist on the TreeLibraryPage, so the sidebar
duplicates were removed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #86.
This commit is contained in:
chihlasm
2026-02-19 22:10:47 -05:00
committed by GitHub
parent 9462d8b15a
commit aef40078d0
47 changed files with 864 additions and 626 deletions

View File

@@ -1,25 +1,2 @@
import type { ReactNode } from 'react'
import { cn } from '@/lib/utils'
interface EmptyStateProps {
icon?: ReactNode
title: string
description?: string
action?: ReactNode
className?: string
}
export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
return (
<div className={cn('flex flex-col items-center justify-center py-12 text-center', className)}>
{icon && <div className="mb-4 text-muted-foreground">{icon}</div>}
<h3 className="text-lg font-semibold text-foreground">{title}</h3>
{description && (
<p className="mt-1 max-w-sm text-sm text-muted-foreground">{description}</p>
)}
{action && <div className="mt-4">{action}</div>}
</div>
)
}
export default EmptyState
export { EmptyState } from '@/components/common/EmptyState'
export { default } from '@/components/common/EmptyState'

View File

@@ -12,7 +12,7 @@ export function PageHeader({ title, description, action, className }: PageHeader
return (
<div className={cn('flex items-start justify-between gap-4', className)}>
<div>
<h1 className="text-2xl font-bold text-foreground">{title}</h1>
<h1 className="text-2xl font-bold font-heading text-foreground">{title}</h1>
{description && (
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
)}

View File

@@ -0,0 +1,25 @@
import type { ReactNode } from 'react'
import { cn } from '@/lib/utils'
interface EmptyStateProps {
icon?: ReactNode
title: string
description?: string
action?: ReactNode
className?: string
}
export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
return (
<div className={cn('flex flex-col items-center justify-center py-12 text-center', className)}>
{icon && <div className="mb-4 text-muted-foreground">{icon}</div>}
<h3 className="text-lg font-semibold text-foreground">{title}</h3>
{description && (
<p className="mt-1 max-w-sm text-sm text-muted-foreground">{description}</p>
)}
{action && <div className="mt-4">{action}</div>}
</div>
)
}
export default EmptyState

View File

@@ -1,8 +1,10 @@
import { Spinner } from '@/components/common/Spinner'
export function PageLoader() {
return (
<div className="flex h-screen items-center justify-center bg-black">
<div className="flex h-full items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="h-12 w-12 animate-spin rounded-full border-4 border-border border-t-foreground" />
<Spinner size="lg" />
<p className="text-sm text-muted-foreground">Loading...</p>
</div>
</div>

View File

@@ -17,7 +17,7 @@ export function RouteError() {
}
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-black p-8">
<div className="flex min-h-screen flex-col items-center justify-center bg-background p-8">
<div className="max-w-md text-center">
<h1 className="mb-2 text-4xl font-bold text-foreground">Oops!</h1>
<h2 className="mb-2 text-xl font-semibold text-red-400">{errorMessage}</h2>

View File

@@ -0,0 +1,26 @@
import { cn } from '@/lib/utils'
const SIZES = {
sm: 'h-4 w-4 border-2',
md: 'h-8 w-8 border-4',
lg: 'h-12 w-12 border-4',
} as const
interface SpinnerProps {
size?: keyof typeof SIZES
className?: string
}
export function Spinner({ size = 'md', className }: SpinnerProps) {
return (
<div
className={cn(
'animate-spin rounded-full border-border border-t-primary',
SIZES[size],
className
)}
/>
)
}
export default Spinner

View File

@@ -34,7 +34,7 @@ export function TagBadges({
}}
disabled={!onTagClick}
className={cn(
'rounded-full transition-colors',
'rounded-full font-label transition-colors',
size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-2.5 py-1 text-sm',
variant === 'default'
? 'bg-accent text-muted-foreground hover:bg-accent'
@@ -48,7 +48,7 @@ export function TagBadges({
{hiddenCount > 0 && (
<span
className={cn(
'rounded-full',
'rounded-full font-label',
size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-2.5 py-1 text-sm',
'bg-accent/50 text-muted-foreground'
)}

View File

@@ -1,32 +1,18 @@
import { useEffect, useState } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, BarChart3, Users, Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText } from 'lucide-react'
import { LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, BarChart3, Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
import { CategoryList } from '@/components/sidebar/CategoryList'
import { TagCloud } from '@/components/sidebar/TagCloud'
import { PinnedFlowsSection } from '@/components/sidebar/PinnedFlowsSection'
import { NavItem } from './NavItem'
import { categoriesApi, tagsApi, sessionsApi, treesApi } from '@/api'
import { sessionsApi, treesApi } from '@/api'
import { pinnedFlowsApi } from '@/api/pinnedFlows'
import type { PinnedFlow } from '@/api/pinnedFlows'
import { toast } from '@/lib/toast'
interface CategoryItem {
id: string
name: string
color: string
count: number
}
export function Sidebar() {
const sidebarCollapsed = useUserPreferencesStore(s => s.sidebarCollapsed)
const toggleSidebar = useUserPreferencesStore(s => s.toggleSidebar)
const [categories, setCategories] = useState<CategoryItem[]>([])
const [tags, setTags] = useState<string[]>([])
const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null)
const [activeTags, setActiveTags] = useState<string[]>([])
const [activeSessionCount, setActiveSessionCount] = useState(0)
const [pinnedFlows, setPinnedFlows] = useState<PinnedFlow[]>([])
const [treeCounts, setTreeCounts] = useState({ total: 0, troubleshooting: 0, procedural: 0, maintenance: 0 })
@@ -35,20 +21,11 @@ export function Sidebar() {
useEffect(() => {
const fetchData = async () => {
try {
const [cats, tagList, activeSessions, allTrees, pinnedData] = await Promise.all([
categoriesApi.list(),
tagsApi.list().catch(() => []),
const [activeSessions, allTrees, pinnedData] = await Promise.all([
sessionsApi.list({ completed: false, size: 50 }).catch(() => []),
treesApi.list({ sort_by: 'name' }).catch(() => []),
pinnedFlowsApi.list().catch(() => ({ items: [], count: 0 })),
])
setCategories(cats.map(c => ({
id: c.id,
name: c.name,
color: c.color || '#3b82f6',
count: c.tree_count || 0,
})))
setTags(tagList.map((t: { name: string }) => t.name).slice(0, 15))
setActiveSessionCount(activeSessions.length)
setPinnedFlows(pinnedData.items)
@@ -64,44 +41,6 @@ export function Sidebar() {
fetchData()
}, [])
const navigate = useNavigate()
const location = useLocation()
// Sync active filters from URL when on /trees page
useEffect(() => {
if (location.pathname === '/trees') {
const params = new URLSearchParams(location.search)
setActiveCategoryId(params.get('category') || null)
const tagsParam = params.get('tags')
setActiveTags(tagsParam ? tagsParam.split(',') : [])
}
}, [location.pathname, location.search])
const handleCategorySelect = (id: string | null) => {
setActiveCategoryId(id)
const params = new URLSearchParams(location.search)
if (id) {
params.set('category', id)
} else {
params.delete('category')
}
navigate(`/trees?${params.toString()}`)
}
const handleTagClick = (tag: string) => {
const next = activeTags.includes(tag)
? activeTags.filter(t => t !== tag)
: [...activeTags, tag]
setActiveTags(next)
const params = new URLSearchParams(location.search)
if (next.length > 0) {
params.set('tags', next.join(','))
} else {
params.delete('tags')
}
navigate(`/trees?${params.toString()}`)
}
const handleUnpin = async (treeId: string) => {
try {
await pinnedFlowsApi.unpin(treeId)
@@ -172,27 +111,13 @@ export function Sidebar() {
<NavItem href="/my-trees" icon={PenLine} label="Flow Editor" />
<NavItem href="/sessions" icon={Clock} label="Sessions" badge={activeSessionCount || undefined} />
<NavItem href="/shares" icon={FileText} label="Exports" />
<NavItem href="/step-library" icon={Bookmark} label="Step Library" badge="dot" />
<NavItem href="/step-library" icon={Bookmark} label="Step Library" />
<NavItem href="/analytics" icon={BarChart3} label="Analytics" />
</div>
<div className="border-b border-[hsl(var(--border-subtle))]" />
{/* Categories */}
<CategoryList
categories={categories}
activeId={activeCategoryId}
onSelect={handleCategorySelect}
/>
<div className="border-b border-[hsl(var(--border-subtle))]" />
{/* Tags */}
<TagCloud tags={tags} activeTags={activeTags} onTagClick={handleTagClick} />
</>
)}
{/* Spacer */}
{/* Spacer — pushes footer to bottom */}
<div className="flex-1" />
{/* Footer */}
@@ -203,8 +128,7 @@ export function Sidebar() {
{!sidebarCollapsed && (
<>
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" />
<NavItem href="/account" icon={Users} label="Team" />
<NavItem href="/account" icon={Settings} label="Settings" />
<NavItem href="/account" icon={Settings} label="Account" />
</>
)}
<button

View File

@@ -1,6 +1,6 @@
import { useState, useRef, useEffect, useCallback } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Search, Zap, LogOut, User, Shield, Settings } from 'lucide-react'
import { Search, Zap, LogOut, Shield, Settings } from 'lucide-react'
import { useAuthStore } from '@/store/authStore'
import { usePermissions } from '@/hooks/usePermissions'
import { BrandLogo } from '@/components/common/BrandLogo'
@@ -122,21 +122,13 @@ export function TopBar() {
</span>
)}
</div>
<Link
to="/account"
onClick={() => setUserMenuOpen(false)}
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
<User size={14} />
Account
</Link>
<Link
to="/account"
onClick={() => setUserMenuOpen(false)}
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
<Settings size={14} />
Settings
Account
</Link>
{isSuperAdmin && (
<Link

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react'
import { Folder, ChevronDown, ChevronRight, Plus, MoreVertical, Pencil, Trash2, FolderPlus, X } from 'lucide-react'
import { foldersApi } from '@/api/folders'
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
import type { FolderListItem, FolderTreeItem } from '@/types'
import { cn } from '@/lib/utils'
@@ -245,6 +246,7 @@ export function FolderSidebar({
const [menuOpenId, setMenuOpenId] = useState<string | null>(null)
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null)
const [pendingDelete, setPendingDelete] = useState<{ id: string; message: string } | null>(null)
const loadFolders = useCallback(async () => {
setIsLoading(true)
@@ -277,15 +279,19 @@ export function FolderSidebar({
})
}
const handleDeleteFolder = async (folderId: string, folderHasChildren: boolean) => {
const handleDeleteFolder = (folderId: string, folderHasChildren: boolean) => {
const descendantCount = getDescendantIds(folders, folderId).length
const message = folderHasChildren
? `Are you sure you want to delete this folder and its ${descendantCount} subfolder(s)? The trees in them will not be deleted.`
: 'Are you sure you want to delete this folder? The trees in it will not be deleted.'
if (!confirm(message)) {
return
}
setPendingDelete({ id: folderId, message })
}
const confirmDeleteFolder = async () => {
if (!pendingDelete) return
const folderId = pendingDelete.id
setPendingDelete(null)
try {
await foldersApi.delete(folderId)
// Remove folder and all descendants from local state
@@ -494,6 +500,15 @@ export function FolderSidebar({
</button>
</div>
)}
<ConfirmDialog
isOpen={!!pendingDelete}
onClose={() => setPendingDelete(null)}
onConfirm={confirmDeleteFolder}
title="Delete Folder"
message={pendingDelete?.message || ''}
confirmLabel="Delete"
/>
</>
)
}

View File

@@ -46,7 +46,7 @@ export function PinnedFlowsSection({ flows, onUnpin }: PinnedFlowsSectionProps)
title={`${flow.tree_name} (right-click to unpin)`}
>
<span className="text-sm shrink-0">
{flow.tree_type === 'procedural' ? '📋' : '🔧'}
{flow.tree_type === 'procedural' ? '📋' : flow.tree_type === 'maintenance' ? '🛠️' : '🔧'}
</span>
<span className="truncate flex-1 text-left">{flow.tree_name}</span>
<Pin size={12} className="shrink-0 opacity-0 group-hover:opacity-40 transition-opacity" />

View File

@@ -4,6 +4,7 @@ import { useTreeEditorStore, findNodeInTree } from '@/store/treeEditorStore'
import { NodeFormDecision } from './NodeFormDecision'
import { NodeFormAction } from './NodeFormAction'
import { NodeFormResolution } from './NodeFormResolution'
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
import { cn } from '@/lib/utils'
import type { TreeStructure, NodeType } from '@/types'
@@ -36,16 +37,18 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan
const [draft, setDraft] = useState<TreeStructure | null>(null)
const [isDirty, setIsDirty] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [showDiscardConfirm, setShowDiscardConfirm] = useState(false)
const panelRef = useRef<HTMLDivElement>(null)
// Initialize/reset draft when nodeId changes
// Initialize/reset draft when nodeId changes or when node type changes
// (e.g., answer stub → decision/action/solution via type picker)
useEffect(() => {
if (node) {
setDraft(cloneWithoutChildren(node))
setIsDirty(false)
setShowDeleteConfirm(false)
}
}, [nodeId]) // eslint-disable-line react-hooks/exhaustive-deps
}, [nodeId, node?.type]) // eslint-disable-line react-hooks/exhaustive-deps
const handleDraftUpdate = useCallback((updates: Partial<TreeStructure>) => {
setDraft(prev => prev ? { ...prev, ...updates } : prev)
@@ -58,7 +61,7 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan
updateNode(nodeId, draftWithoutChildren)
// Auto-create answer stubs for new decision options without next_node_id
if (draft.options) {
if (draft.type === 'decision' && draft.options) {
const options = draft.options.filter(o => o.label.trim())
const stubsCreated: Array<{ optId: string; stubId: string }> = []
@@ -79,12 +82,20 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan
}
}
// Auto-create answer stub for action node without next_node_id
if (draft.type === 'action' && !draft.next_node_id) {
const stubId = addNode(nodeId, 'answer')
updateNode(stubId, { title: 'Next Step' })
updateNode(nodeId, { next_node_id: stubId })
}
setIsDirty(false)
}, [draft, node, nodeId, updateNode, addNode])
const handleClose = useCallback(() => {
if (isDirty) {
if (!window.confirm('You have unsaved changes. Discard them?')) return
setShowDiscardConfirm(true)
return
}
onClose()
}, [isDirty, onClose])
@@ -162,7 +173,7 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan
const isRoot = treeStructure?.id === nodeId
return (
<div ref={panelRef} className="flex h-[calc(100vh-105px)] w-[400px] shrink-0 flex-col border-l border-border bg-card">
<div ref={panelRef} className="flex h-full min-h-0 w-[400px] shrink-0 flex-col border-l border-border bg-card">
{/* Header */}
<div className="flex items-center gap-2 border-b border-border px-4 py-3 shrink-0">
<span className={cn('flex h-5 w-5 shrink-0 items-center justify-center rounded', config.badgeClass)}>
@@ -175,14 +186,14 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan
</div>
{/* Body — scrollable form area */}
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-3">
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-3 scroll-pb-24">
{draft.type === 'decision' && <NodeFormDecision node={draft} onUpdate={handleDraftUpdate} />}
{draft.type === 'action' && <NodeFormAction node={draft} onUpdate={handleDraftUpdate} />}
{draft.type === 'solution' && <NodeFormResolution node={draft} onUpdate={handleDraftUpdate} />}
</div>
{/* Footer */}
<div className="flex items-center gap-2 border-t border-border px-4 py-3 shrink-0">
<div className="sticky bottom-0 flex items-center gap-2 border-t border-border bg-card px-4 py-3 shrink-0">
<button
onClick={handleSave}
disabled={!isDirty}
@@ -236,6 +247,18 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan
</>
)}
</div>
<ConfirmDialog
isOpen={showDiscardConfirm}
onClose={() => setShowDiscardConfirm(false)}
onConfirm={() => {
setShowDiscardConfirm(false)
onClose()
}}
title="Discard Changes"
message="You have unsaved changes. Discard them?"
confirmLabel="Discard"
/>
</div>
)
}

View File

@@ -1,6 +1,5 @@
import { useState } from 'react'
import { DynamicArrayField } from './DynamicArrayField'
import { NodePicker } from './NodePicker'
import { useTreeEditorStore } from '@/store/treeEditorStore'
import { MarkdownContent } from '@/components/ui/MarkdownContent'
import { InfoTip } from '@/components/common/InfoTip'
@@ -20,9 +19,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
e => e.nodeId === node.id && e.field === 'title'
)
const nextNodeError = validationErrors.find(
e => e.nodeId === node.id && e.field === 'next_node_id'
)
const hasNextNode = !!node.next_node_id
const handleAddCommand = () => {
onUpdate({
@@ -161,16 +158,16 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
/>
</div>
{/* Next Node */}
<NodePicker
value={node.next_node_id || ''}
onChange={(nodeId) => onUpdate({ next_node_id: nodeId })}
parentNodeId={node.id}
excludeNodeId={node.id}
label="Next Node (after action)"
placeholder="Select or create next node..."
error={nextNodeError?.message}
/>
{/* Next step hint */}
{hasNextNode ? (
<p className="text-xs text-muted-foreground">
Next step is linked click it on the canvas to edit.
</p>
) : (
<p className="text-xs text-yellow-400/70">
Save to create a placeholder for the next step.
</p>
)}
</div>
)
}

View File

@@ -82,21 +82,10 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
<div className="space-y-4">
{/* Root node banner */}
{isRootNode && (
<div className="rounded-lg border-2 border-blue-500/30 bg-blue-500/10 p-4">
<div className="flex items-start gap-3">
<div className="rounded-full bg-blue-500/20 p-2">
<Play className="h-5 w-5 text-blue-500" />
</div>
<div>
<h3 className="font-semibold text-blue-400">
Starting Question
</h3>
<p className="mt-1 text-sm text-muted-foreground">
This is the first question users will see when they start this troubleshooting tree.
Each option below creates a different troubleshooting path.
</p>
</div>
</div>
<div className="flex items-center gap-2 rounded-lg border border-blue-500/30 bg-blue-500/10 px-3 py-2">
<Play className="h-4 w-4 text-blue-500 shrink-0" />
<span className="text-sm font-medium text-blue-400">Starting Question</span>
<InfoTip text="This is the first question users will see. Each option creates a different troubleshooting path." />
</div>
)}
@@ -150,6 +139,7 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
? "Add as many options as needed (A, B, C, D...). Each option leads to a different troubleshooting path."
: "Each option can branch to a different next step."} />
</label>
<p className="text-xs text-muted-foreground mt-1">Options become answer placeholders you can fill in later.</p>
{optionsError && (
<p className="mt-1 text-xs text-red-400">{optionsError.message}</p>
)}

View File

@@ -143,10 +143,9 @@ Document what was done and the outcome.
</div>
{/* Note about terminal node */}
<div className="rounded-md bg-emerald-400/10 p-3 text-sm text-emerald-400">
<strong>Note:</strong> Solution nodes are terminal - they end the troubleshooting flow.
The session will be marked complete when the user reaches this node.
</div>
<p className="text-xs text-emerald-400/70">
Solution nodes are terminal the session completes when users reach this node.
</p>
</div>
)
}