Show a loading skeleton in the sidebar until the tool registry arrives
Browse filesBefore the /api/tools response lands, the sidebar rendered its empty
state: a "0 of 0 on" count and no rows, which flashed for a moment on
every load (longer on the deployed Space). Distinguish loading from
empty: hide the count (screen readers get a loading note) and render
placeholder rows mirroring the loaded layout. The skeleton appears only
on the true first load; model-switch refetches keep the old list and
fetch failures still show the error message.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
frontend/components/chat-shell.tsx
CHANGED
|
@@ -498,6 +498,7 @@ export function ChatShell() {
|
|
| 498 |
{!sidebarCollapsed ? (
|
| 499 |
<div className="hidden lg:block lg:h-full lg:min-h-0">
|
| 500 |
<SourceSidebar
|
|
|
|
| 501 |
onNewChat={handleNewChat}
|
| 502 |
onToggleSource={toggleSource}
|
| 503 |
onToggleTool={toggleTool}
|
|
@@ -519,6 +520,7 @@ export function ChatShell() {
|
|
| 519 |
/>
|
| 520 |
<div className="absolute inset-y-0 left-0 w-[300px] max-w-[85vw] p-2">
|
| 521 |
<SourceSidebar
|
|
|
|
| 522 |
onNewChat={() => {
|
| 523 |
handleNewChat();
|
| 524 |
setSidebarOpen(false);
|
|
|
|
| 498 |
{!sidebarCollapsed ? (
|
| 499 |
<div className="hidden lg:block lg:h-full lg:min-h-0">
|
| 500 |
<SourceSidebar
|
| 501 |
+
loading={tools.length === 0 && !sourceError}
|
| 502 |
onNewChat={handleNewChat}
|
| 503 |
onToggleSource={toggleSource}
|
| 504 |
onToggleTool={toggleTool}
|
|
|
|
| 520 |
/>
|
| 521 |
<div className="absolute inset-y-0 left-0 w-[300px] max-w-[85vw] p-2">
|
| 522 |
<SourceSidebar
|
| 523 |
+
loading={tools.length === 0 && !sourceError}
|
| 524 |
onNewChat={() => {
|
| 525 |
handleNewChat();
|
| 526 |
setSidebarOpen(false);
|
frontend/components/source-sidebar.tsx
CHANGED
|
@@ -36,6 +36,7 @@ type SourceSidebarProps = {
|
|
| 36 |
tools: TutorTool[];
|
| 37 |
onClose?: () => void;
|
| 38 |
onCollapse?: () => void;
|
|
|
|
| 39 |
};
|
| 40 |
|
| 41 |
type ToggleToolMeta = {
|
|
@@ -73,6 +74,7 @@ export function SourceSidebar({
|
|
| 73 |
tools,
|
| 74 |
onClose,
|
| 75 |
onCollapse,
|
|
|
|
| 76 |
}: SourceSidebarProps) {
|
| 77 |
const retrievalTool = tools.find(
|
| 78 |
(tool): tool is Extract<TutorTool, { kind: "configurable" }> =>
|
|
@@ -170,12 +172,16 @@ export function SourceSidebar({
|
|
| 170 |
Tools
|
| 171 |
</span>
|
| 172 |
</div>
|
| 173 |
-
|
| 174 |
-
className="
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
</div>
|
| 180 |
<p className="text-[11px] leading-[1.4] text-[var(--muted)]">
|
| 181 |
Toggle tools, pick sources.
|
|
@@ -183,6 +189,7 @@ export function SourceSidebar({
|
|
| 183 |
</div>
|
| 184 |
|
| 185 |
<div className="scrollbar-thin min-h-0 flex-1 space-y-2 overflow-y-auto pr-0.5">
|
|
|
|
| 186 |
{retrievalTool ? (
|
| 187 |
<RetrievalTool
|
| 188 |
tool={retrievalTool}
|
|
@@ -235,6 +242,23 @@ export function SourceSidebar({
|
|
| 235 |
);
|
| 236 |
}
|
| 237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
function RetrievalTool({
|
| 239 |
isOpen,
|
| 240 |
onOpenChange,
|
|
|
|
| 36 |
tools: TutorTool[];
|
| 37 |
onClose?: () => void;
|
| 38 |
onCollapse?: () => void;
|
| 39 |
+
loading?: boolean;
|
| 40 |
};
|
| 41 |
|
| 42 |
type ToggleToolMeta = {
|
|
|
|
| 74 |
tools,
|
| 75 |
onClose,
|
| 76 |
onCollapse,
|
| 77 |
+
loading = false,
|
| 78 |
}: SourceSidebarProps) {
|
| 79 |
const retrievalTool = tools.find(
|
| 80 |
(tool): tool is Extract<TutorTool, { kind: "configurable" }> =>
|
|
|
|
| 172 |
Tools
|
| 173 |
</span>
|
| 174 |
</div>
|
| 175 |
+
{loading ? (
|
| 176 |
+
<span className="sr-only">Loading tools and sources</span>
|
| 177 |
+
) : (
|
| 178 |
+
<span
|
| 179 |
+
className="eyebrow text-[10.5px] text-[var(--muted)]"
|
| 180 |
+
title={`${activeCount} of ${totalCount} tools on`}
|
| 181 |
+
>
|
| 182 |
+
{activeCount} of {totalCount} on
|
| 183 |
+
</span>
|
| 184 |
+
)}
|
| 185 |
</div>
|
| 186 |
<p className="text-[11px] leading-[1.4] text-[var(--muted)]">
|
| 187 |
Toggle tools, pick sources.
|
|
|
|
| 189 |
</div>
|
| 190 |
|
| 191 |
<div className="scrollbar-thin min-h-0 flex-1 space-y-2 overflow-y-auto pr-0.5">
|
| 192 |
+
{loading ? <SidebarSkeleton /> : null}
|
| 193 |
{retrievalTool ? (
|
| 194 |
<RetrievalTool
|
| 195 |
tool={retrievalTool}
|
|
|
|
| 242 |
);
|
| 243 |
}
|
| 244 |
|
| 245 |
+
function SidebarSkeleton() {
|
| 246 |
+
return (
|
| 247 |
+
<div aria-hidden className="space-y-2">
|
| 248 |
+
<div className="h-[38px] animate-pulse rounded-lg border border-[var(--line)] bg-[var(--surface-subtle)]" />
|
| 249 |
+
<div className="ml-[11px] space-y-1.5 border-l border-[var(--line)] pl-3">
|
| 250 |
+
{Array.from({ length: 7 }).map((_, index) => (
|
| 251 |
+
<div
|
| 252 |
+
key={index}
|
| 253 |
+
className="h-[31px] animate-pulse rounded-lg border border-[var(--line)] bg-[var(--surface-soft)]"
|
| 254 |
+
style={{ animationDelay: `${index * 90}ms` }}
|
| 255 |
+
/>
|
| 256 |
+
))}
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
);
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
function RetrievalTool({
|
| 263 |
isOpen,
|
| 264 |
onOpenChange,
|