From 0835282b793d2cf23c2c19d945f383e00c94d71e Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 26 Feb 2026 02:59:42 -0500 Subject: [PATCH] fix: surface errors and polish UX across Step Library and Batch pages - StepLibraryPage: replace silent console.error with toast.error on edit/save failures; replace manual setTimeout toast with toast.success helper - BatchStatusPage: add error state with retry button for failed session loads instead of showing ambiguous "No sessions found" - StepDetailModal: guard clipboard copy against browser denial (no false "Copied!" state on permission error); fix dead "See all reviews" button with inline expand/collapse toggle - StepLibraryBrowser: add "Try again" retry button to error state; retry increments a counter that re-triggers both load effects Co-Authored-By: Claude Sonnet 4.6 --- .../step-library/StepDetailModal.tsx | 27 ++++++++++++------- .../step-library/StepLibraryBrowser.tsx | 15 ++++++++--- frontend/src/pages/BatchStatusPage.tsx | 19 ++++++++++++- frontend/src/pages/StepLibraryPage.tsx | 21 +++++---------- 4 files changed, 53 insertions(+), 29 deletions(-) diff --git a/frontend/src/components/step-library/StepDetailModal.tsx b/frontend/src/components/step-library/StepDetailModal.tsx index e7f88449..481e2cc0 100644 --- a/frontend/src/components/step-library/StepDetailModal.tsx +++ b/frontend/src/components/step-library/StepDetailModal.tsx @@ -29,6 +29,7 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [copiedCommandIndex, setCopiedCommandIndex] = useState(null) + const [showAllReviews, setShowAllReviews] = useState(false) useEffect(() => { const loadStepDetails = async () => { @@ -53,9 +54,13 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr }, [stepId]) const handleCopyCommand = async (command: string, index: number) => { - await navigator.clipboard.writeText(command) - setCopiedCommandIndex(index) - setTimeout(() => setCopiedCommandIndex(null), 2000) + try { + await navigator.clipboard.writeText(command) + setCopiedCommandIndex(index) + setTimeout(() => setCopiedCommandIndex(null), 2000) + } catch { + // Clipboard access denied — don't show false "Copied!" state + } } const handleInsert = () => { @@ -66,7 +71,8 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr const Icon = step ? stepTypeIcons[step.step_type as keyof typeof stepTypeIcons] : HelpCircle const hasRating = step && step.rating_count > 0 - const topReviews = reviews.filter(r => r.review_text).slice(0, 3) + const allTextReviews = reviews.filter(r => r.review_text) + const visibleReviews = showAllReviews ? allTextReviews : allTextReviews.slice(0, 3) return (
@@ -228,18 +234,21 @@ export function StepDetailModal({ stepId, onClose, onInsert }: StepDetailModalPr )} {/* Reviews */} - {topReviews.length > 0 && ( + {visibleReviews.length > 0 && (

Reviews

- {reviews.length > 3 && ( - )}
- {topReviews.map(review => ( + {visibleReviews.map(review => (
diff --git a/frontend/src/components/step-library/StepLibraryBrowser.tsx b/frontend/src/components/step-library/StepLibraryBrowser.tsx index 9cd07c7c..146cac20 100644 --- a/frontend/src/components/step-library/StepLibraryBrowser.tsx +++ b/frontend/src/components/step-library/StepLibraryBrowser.tsx @@ -37,6 +37,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f // UI state const [previewStepId, setPreviewStepId] = useState(null) const [collapsedSections, setCollapsedSections] = useState>({}) + const [retryCount, setRetryCount] = useState(0) // Load initial data useEffect(() => { @@ -59,7 +60,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f } loadInitialData() - }, []) + }, [retryCount]) // Load steps when filters change useEffect(() => { @@ -92,7 +93,7 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f } loadSteps() - }, [searchQuery, selectedCategoryId, selectedStepType, minRating, sortBy, selectedTag, refreshKey]) + }, [searchQuery, selectedCategoryId, selectedStepType, minRating, sortBy, selectedTag, refreshKey, retryCount]) // Group steps by visibility const groupedSteps = useMemo(() => { @@ -250,8 +251,14 @@ export function StepLibraryBrowser({ onInsert, onCreateNew, showCreateButton = f
) : error ? ( -
- {error} +
+

{error}

+
) : steps.length === 0 ? (
diff --git a/frontend/src/pages/BatchStatusPage.tsx b/frontend/src/pages/BatchStatusPage.tsx index c6a0de1e..0bf98bc1 100644 --- a/frontend/src/pages/BatchStatusPage.tsx +++ b/frontend/src/pages/BatchStatusPage.tsx @@ -21,6 +21,7 @@ export default function BatchStatusPage() { const [sessions, setSessions] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isRefreshing, setIsRefreshing] = useState(false) + const [loadError, setLoadError] = useState(null) const [batchDate, setBatchDate] = useState(null) const pollRef = useRef | null>(null) @@ -30,9 +31,12 @@ export default function BatchStatusPage() { try { const data = await sessionsApi.list({ batch_id: batchId, size: 100 }) setSessions(Array.isArray(data) ? data : []) + setLoadError(null) if (data.length > 0 && data[0].started_at) { setBatchDate(new Date(data[0].started_at)) } + } catch { + setLoadError('Failed to load batch sessions') } finally { if (showRefreshing) setIsRefreshing(false) } @@ -48,6 +52,8 @@ export default function BatchStatusPage() { loadSessions(), ]) setTree(treeData) + } catch { + setLoadError('Failed to load batch data') } finally { setIsLoading(false) } @@ -178,7 +184,17 @@ export default function BatchStatusPage() { {/* Target cards */}
- {sessions.length === 0 ? ( + {loadError ? ( +
+

{loadError}

+ +
+ ) : sessions.length === 0 ? (

No sessions found for this batch.

@@ -207,3 +223,4 @@ export default function BatchStatusPage() {
) } + diff --git a/frontend/src/pages/StepLibraryPage.tsx b/frontend/src/pages/StepLibraryPage.tsx index 532acd4d..5df23b20 100644 --- a/frontend/src/pages/StepLibraryPage.tsx +++ b/frontend/src/pages/StepLibraryPage.tsx @@ -5,6 +5,7 @@ import { usePermissions } from '@/hooks/usePermissions' import { stepsApi } from '@/api/steps' import { StepLibraryBrowser } from '@/components/step-library/StepLibraryBrowser' import { StepFormModal } from '@/components/step-library/StepFormModal' +import { toast } from '@/lib/toast' import type { Step, StepListItem } from '@/types/step' export default function StepLibraryPage() { @@ -20,9 +21,6 @@ export default function StepLibraryPage() { const [isDeleting, setIsDeleting] = useState(false) const [deleteError, setDeleteError] = useState(null) - // Toast for "Save to My Library" - const [saveToast, setSaveToast] = useState(null) - // Increment to trigger StepLibraryBrowser reload const [refreshKey, setRefreshKey] = useState(0) const refresh = () => setRefreshKey(k => k + 1) @@ -32,8 +30,8 @@ export default function StepLibraryPage() { try { const full = await stepsApi.get(step.id) setEditingStep(full) - } catch (err) { - console.error('Failed to load step for edit:', err) + } catch { + toast.error('Failed to load step for editing') } } @@ -68,11 +66,10 @@ export default function StepLibraryPage() { category_id: full.category_id, tags: full.tags, }) - setSaveToast(`"${full.title}" saved to My Steps`) - setTimeout(() => setSaveToast(null), 3000) + toast.success(`"${full.title}" saved to My Steps`) refresh() - } catch (err) { - console.error('Failed to save step:', err) + } catch { + toast.error('Failed to save step to your library') } } @@ -168,12 +165,6 @@ export default function StepLibraryPage() {
)} - {/* Save Toast */} - {saveToast && ( -
- {saveToast} -
- )}
) }