From c06ac8aa8a7a1e603b9b41455c6b80584bde1eea Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Thu, 16 Apr 2026 21:39:44 -0700 Subject: [PATCH] =?UTF-8?q?fix(canvas):=205=20UX=20polish=20fixes=20?= =?UTF-8?q?=E2=80=94=20error=20handling,=20a11y,=20loading=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. ScheduleTab + ChannelsTab: wrap toggle/delete in try/catch with error feedback (was silently swallowing API failures) 2. MemoryTab: "+Add" button now auto-expands Advanced section 3. SidePanel: keyboard-navigated tabs scroll into view 4. TracesTab: emoji aria-hidden, env-var hint in
5. page.tsx: show Spinner while hydrating instead of flash of EmptyState Co-Authored-By: Claude Opus 4.6 (1M context) --- canvas/src/app/page.tsx | 17 ++++++++++++- canvas/src/components/SidePanel.tsx | 2 +- canvas/src/components/tabs/ChannelsTab.tsx | 28 +++++++++++++++++----- canvas/src/components/tabs/MemoryTab.tsx | 2 +- canvas/src/components/tabs/ScheduleTab.tsx | 20 +++++++++++----- canvas/src/components/tabs/TracesTab.tsx | 11 +++++---- 6 files changed, 61 insertions(+), 19 deletions(-) diff --git a/canvas/src/app/page.tsx b/canvas/src/app/page.tsx index b8976a35..74291409 100644 --- a/canvas/src/app/page.tsx +++ b/canvas/src/app/page.tsx @@ -1,9 +1,10 @@ "use client"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Canvas } from "@/components/Canvas"; import { Legend } from "@/components/Legend"; import { CommunicationOverlay } from "@/components/CommunicationOverlay"; +import { Spinner } from "@/components/Spinner"; import { connectSocket, disconnectSocket } from "@/store/socket"; import { useCanvasStore } from "@/store/canvas"; import { api } from "@/lib/api"; @@ -12,6 +13,7 @@ import type { WorkspaceData } from "@/store/socket"; export default function Home() { const hydrationError = useCanvasStore((s) => s.hydrationError); const setHydrationError = useCanvasStore((s) => s.setHydrationError); + const [hydrating, setHydrating] = useState(true); useEffect(() => { connectSocket(); @@ -31,6 +33,8 @@ export default function Home() { useCanvasStore.getState().setHydrationError( err instanceof Error && err.message ? err.message : "Failed to load canvas" ); + }).finally(() => { + setHydrating(false); }); return () => { @@ -38,6 +42,17 @@ export default function Home() { }; }, []); + if (hydrating) { + return ( +
+
+ + Loading canvas... +
+
+ ); + } + return ( <> diff --git a/canvas/src/components/SidePanel.tsx b/canvas/src/components/SidePanel.tsx index d9bef424..c318b29e 100644 --- a/canvas/src/components/SidePanel.tsx +++ b/canvas/src/components/SidePanel.tsx @@ -173,7 +173,7 @@ export function SidePanel() { else if (e.key === "End") { e.preventDefault(); next = TABS.length - 1; } if (next !== null) { setPanelTab(TABS[next].id); - requestAnimationFrame(() => { document.getElementById(`tab-${TABS[next!].id}`)?.focus(); }); + requestAnimationFrame(() => { const el = document.getElementById(`tab-${TABS[next!].id}`); el?.focus(); el?.scrollIntoView({ block: "nearest", inline: "nearest" }); }); } }} > diff --git a/canvas/src/components/tabs/ChannelsTab.tsx b/canvas/src/components/tabs/ChannelsTab.tsx index 5249dba1..78cb628f 100644 --- a/canvas/src/components/tabs/ChannelsTab.tsx +++ b/canvas/src/components/tabs/ChannelsTab.tsx @@ -141,19 +141,29 @@ export function ChannelsTab({ workspaceId }: Props) { } }; + const [error, setError] = useState(""); + const handleToggle = async (ch: Channel) => { - await api.patch(`/workspaces/${workspaceId}/channels/${ch.id}`, { - enabled: !ch.enabled, - }); - load(); + try { + await api.patch(`/workspaces/${workspaceId}/channels/${ch.id}`, { + enabled: !ch.enabled, + }); + load(); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : "Failed to toggle channel"); + } }; const confirmDelete = async () => { if (!pendingDelete) return; const ch = pendingDelete; setPendingDelete(null); - await api.del(`/workspaces/${workspaceId}/channels/${ch.id}`); - load(); + try { + await api.del(`/workspaces/${workspaceId}/channels/${ch.id}`); + load(); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : "Failed to delete channel"); + } }; const handleTest = async (ch: Channel) => { @@ -188,6 +198,12 @@ export function ChannelsTab({ workspaceId }: Props) { + {error && ( +
+ {error} +
+ )} + {/* Create form */} {showForm && (
diff --git a/canvas/src/components/tabs/MemoryTab.tsx b/canvas/src/components/tabs/MemoryTab.tsx index 4502f982..fa70faa5 100644 --- a/canvas/src/components/tabs/MemoryTab.tsx +++ b/canvas/src/components/tabs/MemoryTab.tsx @@ -219,7 +219,7 @@ export function MemoryTab({ workspaceId }: Props) { Refresh