Skip to content

Document & Knowledge Base: UI Revamp#154

Open
Ayush8923 wants to merge 5 commits intomainfrom
feat/document-knowledge-base-ui-revamp
Open

Document & Knowledge Base: UI Revamp#154
Ayush8923 wants to merge 5 commits intomainfrom
feat/document-knowledge-base-ui-revamp

Conversation

@Ayush8923
Copy link
Copy Markdown
Collaborator

@Ayush8923 Ayush8923 commented May 10, 2026

Summary by CodeRabbit

  • New Features

    • Multi-file document uploads (up to 5 files per batch)
    • File extension badges on documents
    • Document deletion confirmation modal
    • Mobile-responsive preview modal
    • Download button for documents
  • Improvements

    • Enhanced knowledge base page layout and organization
    • Better mobile and tablet experience with responsive design

Review Change Stack

@Ayush8923 Ayush8923 self-assigned this May 10, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

Warning

Rate limit exceeded

@Ayush8923 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 20 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e849e9d5-5aea-4e50-9655-599bb3648e3c

📥 Commits

Reviewing files that changed from the base of the PR and between de58825 and 85056ae.

📒 Files selected for processing (7)
  • app/components/PageHeader.tsx
  • app/components/Sidebar.tsx
  • app/components/knowledge-base/CollectionDetail.tsx
  • app/components/knowledge-base/DocumentPickerModal.tsx
  • app/components/user-menu/Branding.tsx
  • app/hooks/useCollections.ts
  • eslint.config.mjs
📝 Walkthrough

Walkthrough

This PR refactors the knowledge-base feature from a monolithic page component into a modular, hook-driven architecture. It adds multi-file document upload batching, two-step modal-based deletion, job-status polling with LocalStorage caching, and introduces reusable UI components (skeletons, file badges, icons) across document and collection management flows.

Changes

Knowledge Base & Document Management Refactor

Layer / File(s) Summary
Cache and Job Enrichment Utilities
app/lib/utils/knowledgeBaseCache.ts, app/lib/utils/collectionEnrichment.ts
New LocalStorage-backed cache for collection metadata keyed by job ID, and utilities to fetch job statuses concurrently, enrich collections with cached data, and sync job-to-collection mappings.
Shared UI Components & Icons
app/components/FileExtBadge.tsx, app/components/icons/common/DownloadIcon.tsx, app/components/icons/index.tsx, app/components/Loader.tsx, app/components/document/DocumentListingSkeleton.tsx, app/components/document/DocumentPreviewSkeleton.tsx, app/components/knowledge-base/CollectionsListSkeleton.tsx
File extension badge component with color mapping, download icon, updated Loader with size-class mapping and ARIA semantics, and animated skeleton loaders for lists and previews.
Document Management Components
app/components/document/DeleteDocumentModal.tsx, app/components/document/DocumentListing.tsx, app/components/document/DocumentPreview.tsx, app/components/document/UploadDocumentModal.tsx
Multi-file upload modal with batch limiting, document listing with skeleton/error states, document preview with download handling, and delete confirmation modal for two-step deletion flow.
Knowledge Base Collection Components
app/components/knowledge-base/CollectionsList.tsx, app/components/knowledge-base/CollectionsListSkeleton.tsx, app/components/knowledge-base/CollectionDetail.tsx, app/components/knowledge-base/CreateCollectionForm.tsx, app/components/knowledge-base/DeleteCollectionModal.tsx, app/components/knowledge-base/DocumentChip.tsx, app/components/knowledge-base/DocumentPickerModal.tsx, app/components/knowledge-base/DocumentPreviewModal.tsx
Collection sidebar with creation button, collection detail view with metadata/document list, create form with document picker modal, delete confirmation modal, and document preview modal with sidebar.
useCollections Hook & State Management
app/hooks/useCollections.ts
React hook managing collections, documents, selection, loading/creating states; handles fetching, optimistic creation, polling job status, cache enrichment, and deletion with rollback.
Page Layer Integration
app/(main)/document/page.tsx, app/(main)/knowledge-base/page.tsx
Document page refactored for multi-file upload batch support and modal-based deletion; Knowledge Base page simplified to use useCollections hook and componentized UI, delegating collection/document/modal logic to subcomponents.
Constants and Configuration
app/lib/constants.ts, eslint.config.mjs, package.json
New MAX_DOCUMENT_UPLOAD_BATCH constant, ESLint SonarJS plugin for duplication/complexity rules, and supporting dependency.
CSS & Styling
app/globals.css, app/(main)/evaluations/[id]/page.tsx
Removed unused .loader-spinner CSS class; updated segmented control styling in evaluation page with per-button rounding and border overlap.
Code Quality Guidelines
CLAUDE.md
Added "Code Quality Guidelines" section specifying file size limits, SRP/DRY principles, and component/utility reuse patterns.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

enhancement, ready-for-review

Suggested reviewers

  • Prajna1999
  • vprashrex
  • nishika26

Poem

🐰 A rabbit hops through documents with glee,
Batching uploads in harmony—five at a time!
Collections bloom with jobs that poll,
Skeletons dance while caches stroll,
Knowledge bases grow, refactored and sublime! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Document & Knowledge Base: UI Revamp' directly reflects the main changes: a comprehensive UI redesign affecting the document upload/management flow (multi-file uploads, deletion modals, responsive layouts) and the knowledge base page (refactored from monolithic implementation to component-driven architecture with hooks).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/document-knowledge-base-ui-revamp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
eslint.config.mjs (1)

1-58: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Resolve formatting drift to clear CI.

CI reports prettier --check failures on this file. Please run Prettier on eslint.config.mjs so lint-and-build is clean.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@eslint.config.mjs` around lines 1 - 58, Run Prettier to fix formatting drift
in eslint.config.mjs; specifically format the file that defines the eslintConfig
constant (created via defineConfig and using globalIgnores) so it passes
`prettier --check` in CI — you can run your project's configured Prettier
command or an editor/IDE formatter to save the file with Prettier rules and
commit the formatted eslintConfig export.
🧹 Nitpick comments (13)
app/(main)/evaluations/[id]/page.tsx (1)

254-255: ⚡ Quick win

Consider refactoring the long class string for better readability.

The segmentedClass string is over 350 characters long and difficult to read/maintain. Consider using a utility function with clsx or breaking it into logical groups:

♻️ Suggested refactor using template literals
  const segmentedClass =
-    "inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-all cursor-pointer border border-border bg-bg-primary text-text-primary shadow-[0_1px_2px_rgba(0,0,0,0.04)] hover:bg-neutral-50 hover:shadow-[0_1px_2px_rgba(0,0,0,0.08)] data-[selected=true]:bg-accent-primary data-[selected=true]:border-accent-primary data-[selected=true]:text-white data-[selected=true]:shadow-[0_1px_2px_rgba(0,0,0,0.12)] data-[selected=true]:hover:bg-accent-hover data-[selected=true]:hover:shadow-[0_1px_2px_rgba(0,0,0,0.12)]";
+    `inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-all cursor-pointer
+     border border-border bg-bg-primary text-text-primary shadow-[0_1px_2px_rgba(0,0,0,0.04)]
+     hover:bg-neutral-50 hover:shadow-[0_1px_2px_rgba(0,0,0,0.08)]
+     data-[selected=true]:bg-accent-primary data-[selected=true]:border-accent-primary 
+     data-[selected=true]:text-white data-[selected=true]:shadow-[0_1px_2px_rgba(0,0,0,0.12)]
+     data-[selected=true]:hover:bg-accent-hover data-[selected=true]:hover:shadow-[0_1px_2px_rgba(0,0,0,0.12)]`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(main)/evaluations/[id]/page.tsx around lines 254 - 255, The long inline
string assigned to segmentedClass is hard to read; refactor by splitting it into
named logical groups (e.g., base, border, stateSelected, hover) and compose them
with a class-combiner like clsx (import clsx) or join an array/template literal
to build segmentedClass; locate the segmentedClass constant and replace the
single 350+ char string with grouped constants (e.g., const base = "...", const
selected = "...") and then set segmentedClass = clsx(base, selectedCondition &&
selected) or segmentedClass = [base, selectedCondition ? selected :
null].filter(Boolean).join(" ") to improve readability and maintainability.
app/hooks/useCollections.ts (2)

126-137: 💤 Low value

Avoid using a state setter as a synchronous read.

Lines 126–133 invoke setSelectedCollection((prev) => …) solely to read prev and write to the local replacementId, then return prev unchanged. This is fragile: in React Strict Mode the updater can be called twice, which would cause replacementId to be assigned twice (still correct here, but easy to break in the future).

Mirror selectedCollection to a ref (similar to apiKeyRef) and read from it directly, or restructure so the replacement decision happens after setCollections resolves via an effect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/hooks/useCollections.ts` around lines 126 - 137, The code uses
setSelectedCollection(prev => ...) purely to read prev and set replacementId,
which is unsafe in Strict Mode; instead read selectedCollection directly (or
mirror it to a ref like apiKeyRef) and compute replacementId without using the
state setter, then call fetchCollectionDetails(replacementId) if present;
specifically update the logic around setSelectedCollection / selectedCollection
/ replacementId so you do not rely on the updater to synchronously read state
(or move this replacement logic into an effect that runs after setCollections
completes).

45-88: ⚡ Quick win

fetchCollectionDetails and deleteCollection close over collections, causing identity churn.

Both callbacks list collections in their dep arrays so they're rebuilt on every collections update. Because fetchCollections depends on fetchCollectionDetails, every refresh recreates the entire chain and any effect that depends on fetchCollections re-runs unnecessarily.

Inside fetchCollectionDetails the only use of collections is the lookup at Line 51 — this can be replaced with a functional setSelectedCollection that captures prev from the page state, or by reading from a collectionsRef. deleteCollection's use of collections.find(...) for originalCollection can be similarly relocated inside a functional update.

Refactoring to drop collections from the dep arrays prevents needless re-renders/re-fetches in downstream effects.

Also applies to: 318-392

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/hooks/useCollections.ts` around lines 45 - 88, fetchCollectionDetails and
deleteCollection are closing over the collections array causing identity churn;
remove collections from their dependency arrays and move any direct reads of
collections into functional state updaters or a stable ref. Specifically, in
fetchCollectionDetails (function fetchCollectionDetails) replace the
collections.find(...) lookup by using setSelectedCollection(prev => (prev if
matching) || prev) or read from a collectionsRef.current that you initialize and
keep in sync, then remove collections from the useCallback deps; do the same in
deleteCollection by moving originalCollection = collections.find(...) into a
functional updater or reading from collectionsRef.current before removing
collections from its deps. Ensure you update or create a collectionsRef and keep
it updated whenever collections changes so linting passes and behavior remains
unchanged.
app/lib/utils/knowledgeBaseCache.ts (1)

75-100: ⚡ Quick win

Cache entries without collection_id are never pruned.

pruneStaleCache only removes entries that have a collection_id but no longer match a live collection. Entries that never resolved (e.g., a create job that failed silently or whose job status never returned a collectionId) will sit in localStorage forever, and getEntriesWithoutCollectionId will keep re-scanning them on every enrichment pass.

Consider adding a created_at timestamp to CacheEntry and pruning entries older than some TTL (e.g., 24h) inside pruneStaleCache, or pruning unmatched orphans after preFetchJobStatuses completes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/utils/knowledgeBaseCache.ts` around lines 75 - 100, pruneStaleCache
currently only removes entries whose collection_id no longer exists leaving
orphaned entries without collection_id to accumulate; update pruneStaleCache
(which calls readCache/writeCache) to also inspect entries missing
data.collection_id and delete them if their created_at timestamp is older than a
TTL (e.g., 24h), adding created_at to your CacheEntry when entries are created;
alternatively, after preFetchJobStatuses completes remove unmatched orphans by
jobId — ensure you reference CacheEntry, pruneStaleCache,
getEntriesWithoutCollectionId, readCache and writeCache when implementing the
TTL-based deletion so orphaned entries are pruned automatically.
app/lib/utils/collectionEnrichment.ts (2)

65-79: ⚡ Quick win

Redundant nested loop in preFetchJobStatuses.

The inner for (const entry of getEntriesWithoutCollectionId()) does not depend on the outer collection, so it re-reads LocalStorage and re-adds the same set of jobIds for every collection that lacks a cached job_id. The Set deduplicates the writes, but the work is still O(N·M) and reads localStorage once per collection.

Hoist the orphan lookup out of the loop:

♻️ Proposed fix
-  const jobIdsToFetch = new Set<string>();
-  collections.forEach((collection) => {
-    const cached = getCollectionDataByCollectionId(collection.id);
-    if (!cached.job_id) {
-      for (const entry of getEntriesWithoutCollectionId()) {
-        jobIdsToFetch.add(entry.jobId);
-      }
-    }
-  });
+  const hasUncachedCollection = collections.some(
+    (collection) => !getCollectionDataByCollectionId(collection.id).job_id,
+  );
+  const jobIdsToFetch = new Set<string>(
+    hasUncachedCollection
+      ? getEntriesWithoutCollectionId().map((e) => e.jobId)
+      : [],
+  );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/utils/collectionEnrichment.ts` around lines 65 - 79,
preFetchJobStatuses currently calls getEntriesWithoutCollectionId() inside the
collections.forEach loop causing repeated localStorage reads and O(N·M) work;
move the orphan lookup outside the loop so you first call
getEntriesWithoutCollectionId() once, iterate its results to populate
jobIdsToFetch (using the Set), then iterate collections to check
getCollectionDataByCollectionId(collection.id) only to decide whether to skip or
not; update references to jobIdsToFetch, getEntriesWithoutCollectionId, and
getCollectionDataByCollectionId in preFetchJobStatuses accordingly.

81-108: 💤 Low value

Add a typed shape for the intermediate result and consolidate with fetchJobStatus.

The mapper in preFetchJobStatuses and the body of fetchJobStatus both call apiFetch against /api/collections/jobs/${jobId} with the same { data?: JobStatusData } & JobStatusData shape and the same jobData.collection?.id || jobData.collection_id || null normalization. Extract a small private helper (e.g., normalizeJobStatus(jobId, apiKey)) and reuse it from both call sites to keep response handling in one place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/utils/collectionEnrichment.ts` around lines 81 - 108,
preFetchJobStatuses and fetchJobStatus duplicate the same apiFetch call and
response normalization; extract a small helper (e.g., normalizeJobStatus(jobId,
apiKey)) that performs the apiFetch<{ data?: JobStatusData } &
JobStatusData>(`/api/collections/jobs/${jobId}`, apiKey), unwraps result.data ||
result, normalizes collection id via jobData.collection?.id ||
jobData.collection_id || null, and returns a typed intermediate shape { jobId,
status: string | null, collectionId: string | null }; replace the mapper in
preFetchJobStatuses and the body of fetchJobStatus to call normalizeJobStatus
and use its returned shape to build the JobStatusMap or single JobStatus
respectively, preserving existing error handling.
app/components/knowledge-base/CollectionDetail.tsx (1)

113-120: ⚡ Quick win

Preview button is hardcoded to first document.

The "Preview" header button always opens documents[0] regardless of which document the user might want to preview. If users have many documents in a collection, they cannot preview anything other than the first one from this entry point. Consider letting users click any row in the documents list to preview, or remove the misleading single "Preview" button.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/knowledge-base/CollectionDetail.tsx` around lines 113 - 120,
The Preview button currently always calls onPreviewDocument(documents[0]) which
wrongly previews only the first document; change the behavior to preview the
currently selected document (or remove the misleading button). Implement a
selection state (e.g., selectedDocumentId or selectedDocument) inside
CollectionDetail and update document rows to set that selection on click, then
update the header button to call onPreviewDocument(selectedDocument) (or
disable/hide the button when no selection). Update references to documents and
onPreviewDocument in the CollectionDetail component to use the new selection
state so the preview opens the intended document.
app/(main)/document/page.tsx (4)

153-157: ⚡ Quick win

Sequential upload loop prevents concurrent uploads.

The current implementation uploads files one at a time in sequence. While this makes progress tracking simpler, it increases total upload time for multiple files. If the sequential approach is intentional (e.g., to avoid overwhelming the server or to simplify progress UI), consider adding a comment explaining the choice.

If parallelization is acceptable, you could use Promise.allSettled to upload concurrently while still tracking individual results.

⚡ Alternative: Parallel uploads with result tracking
-let successCount = 0;
-for (let i = 0; i < selectedFiles.length; i += 1) {
-  setCurrentUploadIndex(i);
-  const ok = await uploadOneFile(selectedFiles[i]);
-  if (ok) successCount += 1;
-}
+const results = await Promise.allSettled(
+  selectedFiles.map((file, i) => {
+    setCurrentUploadIndex(i);
+    return uploadOneFile(file);
+  })
+);
+const successCount = results.filter(r => r.status === 'fulfilled' && r.value).length;

Note: This would require rethinking progress tracking since multiple uploads would run concurrently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(main)/document/page.tsx around lines 153 - 157, Current sequential loop
over selectedFiles (using setCurrentUploadIndex, uploadOneFile, successCount)
serializes uploads and slows overall throughput; either add a clarifying comment
explaining that sequential uploads are intentional, or convert to concurrent
uploads by mapping selectedFiles to promises that call uploadOneFile and using
Promise.allSettled to await them, then compute successCount from fulfilled
results; if you switch to concurrency, remove or adjust setCurrentUploadIndex
usage (it no longer represents a single active index) and instead track per-file
progress/state so UI updates remain correct.

183-205: ⚡ Quick win

Consider adding loading state for delete operation.

The delete operation is asynchronous but doesn't show a loading indicator. If the network is slow, users may click multiple times or perceive the UI as unresponsive. Consider adding a isDeletingDocument state and disabling the confirm button while deleting.

⏳ Suggested loading state pattern
+const [isDeletingDocument, setIsDeletingDocument] = useState(false);

 const handleConfirmDelete = async () => {
   if (!documentToDelete) return;
   const documentId = documentToDelete.id;
   setDocumentToDelete(null);
+  setIsDeletingDocument(true);

   try {
     await apiFetch(`/api/document/${documentId}`, apiKey?.key ?? "", {
       method: "DELETE",
     });
     // ... rest of logic
   } catch (error) {
     // ... error handling
+  } finally {
+    setIsDeletingDocument(false);
   }
 };

Then pass isDeletingDocument to DeleteDocumentModal to disable the confirm button.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(main)/document/page.tsx around lines 183 - 205, Add a deletion loading
state and disable the confirm button while the async delete runs: introduce a
new boolean state (e.g., isDeletingDocument with setIsDeletingDocument) in the
component, set it true at the start of handleConfirmDelete and false in both
success and catch blocks, pass isDeletingDocument down to DeleteDocumentModal
(or use it to disable the confirm button where that modal is rendered), and
ensure early returns or UI updates (like setDocumentToDelete(null),
setSelectedDocument(null), refetch(), toast.*) still occur while preventing
duplicate requests.

66-98: ⚖️ Poor tradeoff

Consider extracting file selection logic to a custom hook.

This file selection handler includes batch limit enforcement, size validation, and toast notifications—multiple responsibilities that make the page component harder to test and maintain. Consider extracting this logic into a custom hook like useDocumentFileSelection in app/hooks/.

🎣 Example extraction pattern
// app/hooks/useDocumentFileSelection.ts
export function useDocumentFileSelection(
  selectedFiles: File[],
  setSelectedFiles: (files: File[]) => void
) {
  const toast = useToast();
  
  const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
    // ... existing logic ...
  };
  
  return { handleFileSelect };
}

Based on learnings: Apply Single Responsibility Principle (SRP): Each component, hook, function, or module should do one thing and have one reason to change. Extract data fetching into hooks, business logic into utilities, and keep components focused on presentation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(main)/document/page.tsx around lines 66 - 98, Extract the file
selection logic in handleFileSelect into a new custom hook (e.g.,
useDocumentFileSelection) and move validation and toast side-effects there:
create useDocumentFileSelection(selectedFiles, setSelectedFiles) that
encapsulates batch limit enforcement using MAX_DOCUMENT_UPLOAD_BATCH, size
checks against MAX_DOCUMENT_SIZE_BYTES / MAX_DOCUMENT_SIZE_MB, oversized
counting, and toast.error/toast.warning notifications, and return a
handleFileSelect function to be used by the page component so the page keeps
only presentation/state hookup (calling setSelectedFiles via the hook) while all
business logic and notifications live in the hook.

273-289: 💤 Low value

Mobile preview modal has hardcoded height.

The preview modal uses h-[80vh] which is an arbitrary CSS value. This might cause layout issues on very small screens or interfere with the modal's maxHeight="max-h-[90vh]" prop.

Consider either:

  1. Removing the inner height and letting the modal manage it
  2. Using a Tailwind utility class like h-full to fill available space
📱 Suggested adjustment
-<div className="h-[80vh]">
+<div className="h-full">
   <DocumentPreview
     document={selectedDocument}
     isLoading={isLoadingDocument}
   />
 </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(main)/document/page.tsx around lines 273 - 289, The mobile preview
modal currently forces a fixed inner height via the h-[80vh] wrapper which can
conflict with the Modal's maxHeight prop and break on small screens; update the
mobile block that renders <Modal> (where open uses
selectedDocument/isLoadingDocument, onClose calls setSelectedDocument, and it
renders <DocumentPreview document={selectedDocument}
isLoading={isLoadingDocument}>) to remove the hardcoded h-[80vh] wrapper and
instead allow the Modal to size the content (e.g., remove the div or replace its
class with a flexible utility such as h-full), ensuring the preview fills
available modal space without fixed viewport height.
app/components/icons/common/DownloadIcon.tsx (1)

7-15: ⚡ Quick win

Add explicit width and height to the SVG.

The SVG element relies entirely on the className prop for sizing. If a consumer forgets to pass dimensions via className, the icon may not render at a visible size or may have unexpected layout behavior.

📐 Proposed fix to add default dimensions
 <svg
   className={className}
+  width="24"
+  height="24"
   fill="none"
   viewBox="0 0 24 24"
   stroke="currentColor"
   strokeWidth={2}
   strokeLinecap="round"
   strokeLinejoin="round"
 >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/icons/common/DownloadIcon.tsx` around lines 7 - 15, The SVG in
the DownloadIcon component relies solely on the className prop for sizing; add
explicit width and height attributes to the <svg> element (e.g., width="24" and
height="24") so the icon has sensible default dimensions when className is
omitted, and keep className applied so consumers can still override sizing via
CSS or a size prop if present; update the <svg> tag (the element using
className, fill, viewBox, stroke, strokeWidth, strokeLinecap, strokeLinejoin) to
include these default width/height attributes.
app/components/document/DocumentPreview.tsx (1)

102-115: ⚡ Quick win

The conditional download attribute logic is correct but subtle.

The spread operator usage {...(condition ? {} : { download: document.fname })} conditionally adds the download attribute only for non-previewable files. This means:

  • Previewable files (images, PDFs) open in a new tab (no download attribute)
  • Non-previewable files trigger a download prompt

This is good UX, but the pattern might be unclear to future maintainers. Consider adding a brief comment explaining the intent.

💬 Suggested clarification comment
 {(document.signed_url || document.object_store_url) && (
   <a
     href={document.signed_url || document.object_store_url}
+    // For previewable files: open in new tab; for others: trigger download
     {...(isPreviewable(document.fname)
       ? {}
       : { download: document.fname })}
     target="_blank"
     rel="noopener noreferrer"
     className="shrink-0 inline-flex items-center gap-2 px-4 py-1.5 text-sm font-medium transition-colors bg-accent-primary text-white hover:bg-accent-hover rounded-full"
   >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/document/DocumentPreview.tsx` around lines 102 - 115, Add a
short inline comment above the anchor rendering that explains the conditional
spread: when isPreviewable(document.fname) is true we intentionally omit the
download attribute so previewable files (images/PDFs) open in a new tab, and
when false we add download: document.fname to force a download for
non-previewable files; reference the anchor that uses document.signed_url ||
document.object_store_url, the isPreviewable(document.fname) check, and the
conditional spread `{...(isPreviewable(document.fname) ? {} : { download:
document.fname })}` so future maintainers understand the UX intent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/`(main)/document/page.tsx:
- Around line 125-132: Current code uses localStorage to persist file sizes
(fileSizeMap, document_file_sizes, data.data.id, file.size), which is fragile;
instead, remove the localStorage workaround and either surface "N/A" for missing
file_size or raise a backend task to return file_size. Update the logic in
page.tsx where fileSizeMap is read/written: stop reading/writing
document_file_sizes, read file_size from the document response (e.g.,
data.data.file_size) and if it is null/undefined display "N/A" in the UI (or add
a user-visible indicator), and create a follow-up issue requesting the backend
to include file_size in document responses so syncing and persistence are
handled server-side.

In `@app/`(main)/knowledge-base/page.tsx:
- Around line 240-248: The open prop currently uses
!!selectedCollection?.documents which is true for empty arrays; change the
condition to check the array length (e.g., showDocPreviewModal &&
(selectedCollection?.documents?.length ?? 0) > 0 or
!!selectedCollection?.documents?.length) so the modal only opens when documents
exist; update the JSX open expression and ensure any places that call
setShowDocPreviewModal(false) (used when closing) remain unchanged so empty
collections do not trigger the preview.

In `@app/components/document/DocumentListing.tsx`:
- Around line 79-113: The delete <span> is nested inside a <button>, blocking
keyboard access; refactor so selection and delete are two separate native
<button>s (e.g., wrap each document row in a container div keyed by doc.id,
render a primary selection <button> that calls onSelect(doc) and applies the
existing selection styling/heading/formatDate/FileExtBadge, and render a sibling
delete <button> that calls e.stopPropagation() then onDelete(doc.id) and keeps
the TrashIcon, title/aria-label and visual classes), ensure the delete button is
focusable and keyboard-activatable, remove the nested interactive element inside
the selection button, and preserve existing class names and conditional styles
used in this component (references: onSelect, onDelete, FileExtBadge,
formatDate, TrashIcon, doc.fname, doc.id).

In `@app/components/document/UploadDocumentModal.tsx`:
- Around line 101-110: The file input currently keeps its value so re-selecting
the same file won't re-trigger onChange; update the handler to clear the input
after processing by either resetting the DOM input's value (use
fileInputRef.current && (fileInputRef.current.value = '')) or resetting
event.target.value = '' at the end of onFileSelect, ensuring you null-check
fileInputRef and do this only after successful handling so the same file can be
picked again; references: fileInputRef, onFileSelect, id="document-file-upload",
ACCEPTED_DOCUMENT_TYPES, isUploading, limitReached.

In `@app/components/FileExtBadge.tsx`:
- Around line 25-29: getFileExtension currently returns an empty string for
filenames that end with a dot (e.g., "name."), causing a blank badge; update the
function (getFileExtension) to treat a trailing dot as no extension by checking
if dot === -1 OR dot === fileName.length - 1 OR the sliced extension is an empty
string, and in those cases return "FILE" instead of the empty string so the
badge falls back to "FILE".

In `@app/components/knowledge-base/CollectionsList.tsx`:
- Around line 56-94: The delete control is currently a non-interactive <span>
nested inside the row <button>, which breaks semantics and keyboard access;
extract the delete control out of the row button and render it as its own native
<button type="button"> (respecting the isOptimistic guard) rather than a <span>,
keep the existing onClick handler that calls onRequestDelete(collection.id) and
call e.stopPropagation() at the start of that handler so the row
onSelect(collection.id) doesn't fire, preserve the current className,
title/aria-label for accessibility, and ensure the row still uses onSelect for
the main button while the new delete button is a sibling element so keyboard
users can tab to and activate it.

In `@app/components/knowledge-base/DocumentPickerModal.tsx`:
- Around line 27-33: The search state (search/setSearch) persists between modal
opens causing stale filteredDocuments results; add a useEffect that watches the
modal visibility prop (e.g., open or isOpen) and call setSearch("") on close (or
on every open) to clear the query and reset filteredDocuments; update
DocumentPickerModal.tsx to reset search in response to the visibility prop
change so availableDocuments filtering is fresh each time the modal opens.

In `@app/components/knowledge-base/DocumentPreviewModal.tsx`:
- Around line 54-59: The iframe rendering previewDoc.signed_url in
DocumentPreviewModal is unsandboxed and risky; update the iframe in the
component to harden it by (1) adding a restrictive sandbox attribute (e.g., no
allow-scripts, only add specific allowances like allow-popups or allow-forms if
absolutely needed), (2) add referrerPolicy="no-referrer" and loading="lazy" to
reduce information leakage and resource impact, and (3) validate/sanitize
previewDoc.signed_url before use (ensure it's an HTTPS URL and optionally check
allowed host/origin) so only trusted signed URLs are rendered.

In `@app/hooks/useCollections.ts`:
- Around line 236-240: In the useCollections hook replace all blocking
alert(...) calls with the project's toast API: call const toast = useToast() at
the top of the hook (inside useCollections) and change the alert("Please log in
to continue") to toast.warning(...) or toast.error(...), change alert("Please
provide a name and select at least one document") to toast.warning(...), and
likewise replace alerts for create-failure, delete-status-failure, and
delete-failure with toast.error(...) (and use toast.success(...) where success
messages exist). Update every occurrence referenced (around the check in the
submit/create handler near the name/documentIds validation, and the other
reported spots at lines ~299-304, ~366-368, ~377-378, ~386-388) so the hook uses
toast methods instead of alert.
- Around line 411-420: The first useEffect's dependency array is wrong: it uses
isAuthenticated, fetchCollections, and fetchDocuments but only lists apiKey,
which causes stale closures and lint failures; change its deps to include
isAuthenticated, fetchCollections, and fetchDocuments (or include apiKey as well
if the effect needs to re-run on key change). Also fix the second effect to only
depend on apiKey (remove isAuthenticated) since it only sets apiKeyRef.current;
keep references to the functions fetchCollections and fetchDocuments (and ensure
they are properly memoized) so the effect re-runs with the latest callbacks.
- Around line 360-389: The polling function pollDeleteStatus schedules itself
with setTimeout and can continue after the component unmounts; fix this by
storing the timeout id in a ref (e.g., deletePollTimeoutRef) whenever you call
setTimeout in pollDeleteStatus and clearing that timeout in the existing cleanup
effect (and also when the job finishes or fails). Additionally guard state
updates inside pollDeleteStatus (setCollections, setSelectedCollection) by
checking a mounted flag or that apiKeyRef.current still exists before calling
them, and ensure any pending timeout is cleared on success/failure paths as well
as on unmount.

In `@app/lib/utils/knowledgeBaseCache.ts`:
- Around line 37-46: saveCollectionData currently overwrites an existing cache
entry's collection_id with undefined when called without collectionId; update
saveCollectionData to read the existing entry via readCache(), preserve its
collection_id when the optional collectionId parameter is undefined (i.e., only
set collection_id when collectionId is provided), and then write the merged
entry back with writeCache(); refer to saveCollectionData, readCache, writeCache
and the collection_id field when making the change.

---

Outside diff comments:
In `@eslint.config.mjs`:
- Around line 1-58: Run Prettier to fix formatting drift in eslint.config.mjs;
specifically format the file that defines the eslintConfig constant (created via
defineConfig and using globalIgnores) so it passes `prettier --check` in CI —
you can run your project's configured Prettier command or an editor/IDE
formatter to save the file with Prettier rules and commit the formatted
eslintConfig export.

---

Nitpick comments:
In `@app/`(main)/document/page.tsx:
- Around line 153-157: Current sequential loop over selectedFiles (using
setCurrentUploadIndex, uploadOneFile, successCount) serializes uploads and slows
overall throughput; either add a clarifying comment explaining that sequential
uploads are intentional, or convert to concurrent uploads by mapping
selectedFiles to promises that call uploadOneFile and using Promise.allSettled
to await them, then compute successCount from fulfilled results; if you switch
to concurrency, remove or adjust setCurrentUploadIndex usage (it no longer
represents a single active index) and instead track per-file progress/state so
UI updates remain correct.
- Around line 183-205: Add a deletion loading state and disable the confirm
button while the async delete runs: introduce a new boolean state (e.g.,
isDeletingDocument with setIsDeletingDocument) in the component, set it true at
the start of handleConfirmDelete and false in both success and catch blocks,
pass isDeletingDocument down to DeleteDocumentModal (or use it to disable the
confirm button where that modal is rendered), and ensure early returns or UI
updates (like setDocumentToDelete(null), setSelectedDocument(null), refetch(),
toast.*) still occur while preventing duplicate requests.
- Around line 66-98: Extract the file selection logic in handleFileSelect into a
new custom hook (e.g., useDocumentFileSelection) and move validation and toast
side-effects there: create useDocumentFileSelection(selectedFiles,
setSelectedFiles) that encapsulates batch limit enforcement using
MAX_DOCUMENT_UPLOAD_BATCH, size checks against MAX_DOCUMENT_SIZE_BYTES /
MAX_DOCUMENT_SIZE_MB, oversized counting, and toast.error/toast.warning
notifications, and return a handleFileSelect function to be used by the page
component so the page keeps only presentation/state hookup (calling
setSelectedFiles via the hook) while all business logic and notifications live
in the hook.
- Around line 273-289: The mobile preview modal currently forces a fixed inner
height via the h-[80vh] wrapper which can conflict with the Modal's maxHeight
prop and break on small screens; update the mobile block that renders <Modal>
(where open uses selectedDocument/isLoadingDocument, onClose calls
setSelectedDocument, and it renders <DocumentPreview document={selectedDocument}
isLoading={isLoadingDocument}>) to remove the hardcoded h-[80vh] wrapper and
instead allow the Modal to size the content (e.g., remove the div or replace its
class with a flexible utility such as h-full), ensuring the preview fills
available modal space without fixed viewport height.

In `@app/`(main)/evaluations/[id]/page.tsx:
- Around line 254-255: The long inline string assigned to segmentedClass is hard
to read; refactor by splitting it into named logical groups (e.g., base, border,
stateSelected, hover) and compose them with a class-combiner like clsx (import
clsx) or join an array/template literal to build segmentedClass; locate the
segmentedClass constant and replace the single 350+ char string with grouped
constants (e.g., const base = "...", const selected = "...") and then set
segmentedClass = clsx(base, selectedCondition && selected) or segmentedClass =
[base, selectedCondition ? selected : null].filter(Boolean).join(" ") to improve
readability and maintainability.

In `@app/components/document/DocumentPreview.tsx`:
- Around line 102-115: Add a short inline comment above the anchor rendering
that explains the conditional spread: when isPreviewable(document.fname) is true
we intentionally omit the download attribute so previewable files (images/PDFs)
open in a new tab, and when false we add download: document.fname to force a
download for non-previewable files; reference the anchor that uses
document.signed_url || document.object_store_url, the
isPreviewable(document.fname) check, and the conditional spread
`{...(isPreviewable(document.fname) ? {} : { download: document.fname })}` so
future maintainers understand the UX intent.

In `@app/components/icons/common/DownloadIcon.tsx`:
- Around line 7-15: The SVG in the DownloadIcon component relies solely on the
className prop for sizing; add explicit width and height attributes to the <svg>
element (e.g., width="24" and height="24") so the icon has sensible default
dimensions when className is omitted, and keep className applied so consumers
can still override sizing via CSS or a size prop if present; update the <svg>
tag (the element using className, fill, viewBox, stroke, strokeWidth,
strokeLinecap, strokeLinejoin) to include these default width/height attributes.

In `@app/components/knowledge-base/CollectionDetail.tsx`:
- Around line 113-120: The Preview button currently always calls
onPreviewDocument(documents[0]) which wrongly previews only the first document;
change the behavior to preview the currently selected document (or remove the
misleading button). Implement a selection state (e.g., selectedDocumentId or
selectedDocument) inside CollectionDetail and update document rows to set that
selection on click, then update the header button to call
onPreviewDocument(selectedDocument) (or disable/hide the button when no
selection). Update references to documents and onPreviewDocument in the
CollectionDetail component to use the new selection state so the preview opens
the intended document.

In `@app/hooks/useCollections.ts`:
- Around line 126-137: The code uses setSelectedCollection(prev => ...) purely
to read prev and set replacementId, which is unsafe in Strict Mode; instead read
selectedCollection directly (or mirror it to a ref like apiKeyRef) and compute
replacementId without using the state setter, then call
fetchCollectionDetails(replacementId) if present; specifically update the logic
around setSelectedCollection / selectedCollection / replacementId so you do not
rely on the updater to synchronously read state (or move this replacement logic
into an effect that runs after setCollections completes).
- Around line 45-88: fetchCollectionDetails and deleteCollection are closing
over the collections array causing identity churn; remove collections from their
dependency arrays and move any direct reads of collections into functional state
updaters or a stable ref. Specifically, in fetchCollectionDetails (function
fetchCollectionDetails) replace the collections.find(...) lookup by using
setSelectedCollection(prev => (prev if matching) || prev) or read from a
collectionsRef.current that you initialize and keep in sync, then remove
collections from the useCallback deps; do the same in deleteCollection by moving
originalCollection = collections.find(...) into a functional updater or reading
from collectionsRef.current before removing collections from its deps. Ensure
you update or create a collectionsRef and keep it updated whenever collections
changes so linting passes and behavior remains unchanged.

In `@app/lib/utils/collectionEnrichment.ts`:
- Around line 65-79: preFetchJobStatuses currently calls
getEntriesWithoutCollectionId() inside the collections.forEach loop causing
repeated localStorage reads and O(N·M) work; move the orphan lookup outside the
loop so you first call getEntriesWithoutCollectionId() once, iterate its results
to populate jobIdsToFetch (using the Set), then iterate collections to check
getCollectionDataByCollectionId(collection.id) only to decide whether to skip or
not; update references to jobIdsToFetch, getEntriesWithoutCollectionId, and
getCollectionDataByCollectionId in preFetchJobStatuses accordingly.
- Around line 81-108: preFetchJobStatuses and fetchJobStatus duplicate the same
apiFetch call and response normalization; extract a small helper (e.g.,
normalizeJobStatus(jobId, apiKey)) that performs the apiFetch<{ data?:
JobStatusData } & JobStatusData>(`/api/collections/jobs/${jobId}`, apiKey),
unwraps result.data || result, normalizes collection id via
jobData.collection?.id || jobData.collection_id || null, and returns a typed
intermediate shape { jobId, status: string | null, collectionId: string | null
}; replace the mapper in preFetchJobStatuses and the body of fetchJobStatus to
call normalizeJobStatus and use its returned shape to build the JobStatusMap or
single JobStatus respectively, preserving existing error handling.

In `@app/lib/utils/knowledgeBaseCache.ts`:
- Around line 75-100: pruneStaleCache currently only removes entries whose
collection_id no longer exists leaving orphaned entries without collection_id to
accumulate; update pruneStaleCache (which calls readCache/writeCache) to also
inspect entries missing data.collection_id and delete them if their created_at
timestamp is older than a TTL (e.g., 24h), adding created_at to your CacheEntry
when entries are created; alternatively, after preFetchJobStatuses completes
remove unmatched orphans by jobId — ensure you reference CacheEntry,
pruneStaleCache, getEntriesWithoutCollectionId, readCache and writeCache when
implementing the TTL-based deletion so orphaned entries are pruned
automatically.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f6d902f7-d8ea-449b-ac64-fcfae333d61b

📥 Commits

Reviewing files that changed from the base of the PR and between a868c27 and de58825.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (29)
  • CLAUDE.md
  • app/(main)/document/page.tsx
  • app/(main)/evaluations/[id]/page.tsx
  • app/(main)/knowledge-base/page.tsx
  • app/components/FileExtBadge.tsx
  • app/components/Loader.tsx
  • app/components/document/DeleteDocumentModal.tsx
  • app/components/document/DocumentListing.tsx
  • app/components/document/DocumentListingSkeleton.tsx
  • app/components/document/DocumentPreview.tsx
  • app/components/document/DocumentPreviewSkeleton.tsx
  • app/components/document/UploadDocumentModal.tsx
  • app/components/icons/common/DownloadIcon.tsx
  • app/components/icons/index.tsx
  • app/components/knowledge-base/CollectionDetail.tsx
  • app/components/knowledge-base/CollectionsList.tsx
  • app/components/knowledge-base/CollectionsListSkeleton.tsx
  • app/components/knowledge-base/CreateCollectionForm.tsx
  • app/components/knowledge-base/DeleteCollectionModal.tsx
  • app/components/knowledge-base/DocumentChip.tsx
  • app/components/knowledge-base/DocumentPickerModal.tsx
  • app/components/knowledge-base/DocumentPreviewModal.tsx
  • app/globals.css
  • app/hooks/useCollections.ts
  • app/lib/constants.ts
  • app/lib/utils/collectionEnrichment.ts
  • app/lib/utils/knowledgeBaseCache.ts
  • eslint.config.mjs
  • package.json
💤 Files with no reviewable changes (1)
  • app/globals.css

Comment thread app/(main)/document/page.tsx
Comment thread app/(main)/knowledge-base/page.tsx
Comment thread app/components/document/DocumentListing.tsx
Comment thread app/components/document/UploadDocumentModal.tsx
Comment thread app/components/FileExtBadge.tsx
Comment thread app/components/knowledge-base/DocumentPreviewModal.tsx
Comment thread app/hooks/useCollections.ts Outdated
Comment thread app/hooks/useCollections.ts
Comment thread app/hooks/useCollections.ts
Comment thread app/lib/utils/knowledgeBaseCache.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant