.agents/skills/ux-audit/references/example/resource.md
A real run of this skill against the Resource module, 2026-07 (LOBE-11149) — a
knowledge-base / file-library manager: resource home (all resources) → library (a single
knowledge base) → library-slug (a folder inside a library). Three surfaces under
src/routes/(main)/resource/**, all delegating to one shared feature,
src/features/ResourceManager/**, backed by src/store/{file/slices/resource,library,tree}.
Use it as a template for the output shape, not current-state truth (re-verify before citing).
Surface class: knowledge-base / file-library / resource manager — benchmark against Notion, Google Drive / Dropbox, NotebookLM, Mem, Obsidian. Class norms checked: upload-with-progress, search/filter, sort, bulk-select+bulk-ops, single-item lifecycle, file preview, item metadata (size/type/date/embedding status), empty-as-onboarding vs no-match, pagination/infinite scroll. Layers run: L1 (static / code) ✅. L2 / L3 ⏳ not run.
Headline: the write / ingestion side is genuinely strong — drag-drop upload with a live
progress dock, per-folder tree loading, virtualized infinite scroll, three-mode bulk-select,
batch delete/chunk with confirm, optimistic create/rename. But the read side has no error
path anywhere: every one of the four data fetches (resource list, sidebar KB list, search,
folder tree) reads only isLoading/data and never error, so a failed fetch renders as
the onboarding empty state — telling the user "create your first resource" when the load
actually broke. Two more failure-swallowing traps compound it: a library-detail fetch failure
renders as 404 Not Found (deleted vs failed-to-load conflation), and the upload dock
auto-dismisses failed uploads after 3s with no retry.
Same "resolves only on success" root cause as the Eval module — see eval.md.
| Pattern (family) | Where | Rating | Note |
|---|---|---|---|
| Overview + Detail (nav) | home → library → folder(slug); sidebar tree drill | ✅ | clean multi-level drill |
| Deep-linking (nav) | /resource/library/:id/:slug restores folder; tree expands ancestors | ✅ | breadcrumb-driven ancestor expand |
| Empty-as-onboarding (growth) | Explorer EmptyPlaceholder (Create KB / Upload File / Folder cards) | ✅ | real page + CTAs — highlight |
| Loading Skeleton (feedback) | list/masonry skeletons reuse row/card chrome; sidebar SkeletonList; tree TreeSkeleton | ✅ | textbook §4.1 |
| Failure + Retry (feedback) | every fetch (resource list, KB list, search, tree) | — abs. | systemic root cause (gap A) |
| Progress Indicator (feedback) | UploadDock per-file + overall progress bar | ✅ | but auto-dismisses failures (gap C) |
| List at scale (data) | Virtuoso / VList virtual scroll + infinite endReached + footer skel | ✅ | main list solid |
| Search over paginated set (data) | SearchResultsOverlay server query capped limit: 50, offset: 0 | ⚠️ | no pagination past 50 matches (gap D) |
| Entity lifecycle — delete/chunk (act) | batch delete + batch chunk: confirm + async | ✅ | confirm present |
| Entity lifecycle — create/rename (act) | KB create modal, inline rename, optimistic resource ops | ✅/⚠️ | optimistic; rename draft in-memory (gap E) |
| Upload (input/act) | drag-drop zone + UploadDock | ✅/⚠️ | strong, but no retry on failure (gap C) |
| Draft safety (edit) | search query → URL+store (survives); rename input → local useState | ⚠️ | rename draft not persisted (gap E, minor) |
Read: ingestion, tree, virtualization, bulk-select and delete/chunk are solid. The weakness
clusters entirely in the read side — list/detail/search/tree error + retry — plus the
upload dock silently discarding failures.
The write /ingestion side is strong where it counts — these are the ✅ half of the 回灌 loop and the "don't regress" list for the next refactor. Keep, don't "fix":
UploadDock, which surfaces both per-file and overall progress bars while ingestion runs — the
ingestion side never goes silent. (The one caveat is that it auto-dismisses failed uploads;
that's gap C, not a knock on the happy-path dock.)Virtuoso /
VList) with infinite endReached paging plus footer skeletons, so the main list stays smooth
at scale and the load-more affordance is honest rather than a hard cap./resource/library/:id/:slug breadcrumb-driven
ancestor expand.mergeServerResourcesWithOptimistic — the list updates instantly without diverging from
server truth.EmptyPlaceholder + chrome-reusing skeletons. The Explorer EmptyPlaceholder is
a real onboarding page (Create-KB / Upload-File / Upload-Folder cards), and the list / masonry /
sidebar / tree skeletons all reuse row/card chrome for an in-place load→content swap — no antd
Spin anywhere. A quiet feedback-hygiene win worth keeping.🔴 A — No error/retry on any read; every fetch resolves only on success → a failed load
renders as the onboarding empty (or a blank / a false 404). Systemic: all four consumers
read { data, isLoading } (never error) and coerce failure into empty:
useFetchResources returns the full swr incl. error
(store/file/slices/resource/hooks.ts:104), but the consumer destructures only
{ isLoading, isValidating } (Explorer/index.tsx:57) and gates
showEmptyStatus = !isLoading && !isValidating && data?.length === 0 (:89) → a failed
fetch → empty list → the "create your first resource" onboarding EmptyPlaceholder
(EmptyPlaceholder.tsx:65).const { data, isLoading } = useFetchKnowledgeBaseList()
((home)/_layout/Body/LibraryList/index.tsx:22); no error, so a failure renders either the
"create new library" empty CTA or a blank list (:38-48).const { data, isLoading } = useClientDataSWR(...)
(SearchResultsOverlay.tsx:43); a failed search → !data → "No results found" (:113),
asserting "nothing matches" when the query actually errored.isLoading = status[''] === 'loading'; empty gated on
!isLoading && … visibleNodes.length === 0 (LibraryHierarchy/index.tsx:60,83) with no
'error' branch → a failed tree load renders the "add folder" empty.
→ Read §1.1 (error before empty) + Feedback §4.2. The error signal already exists on the swr;
it's simply never read — a cheap fix. The module's flagship ❌ example.🟠 B — library detail: a fetch failure renders as 404 "Not Found", conflating deleted with
failed-to-load, no retry. useKnowledgeBaseItem is read as { data, isLoading } and
if (!isLoading && !data) return <NotFound /> (resource/library/index.tsx:21,37). A transient
network/500 → data undefined → the permanent "this doesn't exist" 404, so the user thinks
their library was deleted and has no Reload. → Read §1.1 (a failed load is not "not found";
distinguish deleted vs errored, offer retry).
🟠 C — the upload dock auto-dismisses failed uploads after 3s, with no retry. The
auto-dismiss effect only guards isUploading and 'pending'; the 'error' state falls
through, so after 3s it hides the dock and removeFiles (UploadDock/index.tsx:106-124). A
failed upload therefore vanishes silently — no error kept, no Retry affordance anywhere. Auto-
dismiss should apply to success only; a failure must persist with retry. → Feedback §4.2
(failed state stays available + offers retry), Act §3.1 (async op ends in done/error, not
fire-and-forget).
🟡 D — search results are capped at 50 with no pagination. The search fetch hard-codes
limit: 50, offset: 0 (SearchResultsOverlay.tsx:52-57) and renders a flat virtualized list
with no load-more; matches #51+ are unreachable. Server-side query is correct (not a
partial-page false-empty), but the surface silently truncates. → Read §1.2 (search over a large
set must page through all matches, not cap silently).
🟡 E — inline rename draft is in-memory useState, lost if the row/popover closes
mid-edit. Minor (per Edit §2.1 the bar is lighter for transient inline edits), but a rename
interrupted by a click-away discards the typed name with no restore
((home)/_layout/Body/LibraryList/Item/Editing.tsx). → Edit §2.1.
🟡 F — search overlay keeps its own selectedFileIds local state, separate from the main
explorer selection. Selecting rows in search results doesn't carry into the normal view (and
vice versa) (SearchResultsOverlay.tsx:34). Consistency-of-selection gap; verify the intended
model on L2. → Act §3.1 (bulk parity), Read §1.6-ish (state continuity across views).
UploadDock (gap C) is the ❌.library/index.tsx (gap B) is the ❌.error-never-read pattern (gap A), sibling to the Eval
onSuccess-only systemic root cause.