Configuration: UI/UX Revamp#157
Configuration: UI/UX Revamp#157Ayush8923 merged 18 commits intofeat/document-knowledge-base-ui-revampfrom
Conversation
…ub.com/ProjectTech4DevAI/kaapi-frontend into feat/configuration-revamp
…ub.com/ProjectTech4DevAI/kaapi-frontend into feat/configuration-revamp
…ub.com/ProjectTech4DevAI/kaapi-frontend into feat/configuration-revamp
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR performs a comprehensive refactoring of the frontend UI system, transitioning from inline colors-based styling to Tailwind utility classes and reusable shared components. It removes deprecated prompt-editor components, adds new reusable UI components, consolidates imports through barrel exports, and updates dozens of existing components to use a consistent tokenized design-system approach. ChangesUI Consolidation & Component Refactor: Colors → Tailwind + Shared Components
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
app/components/MultiSelect.tsx (1)
145-155:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse display label for remove button announcement
At Line 154, the
aria-labelstill uses raw value IDs. Since chips now displaylabelFor(v), screen reader text should match that user-facing label.Proposed fix
- aria-label={`Remove ${v}`} + aria-label={`Remove ${labelFor(v)}`}🤖 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/MultiSelect.tsx` around lines 145 - 155, The remove button's aria-label currently exposes the raw value ID (aria-label={`Remove ${v}`}) instead of the user-facing chip label; update the aria-label to use the displayed label by calling labelFor(v) (e.g., aria-label={`Remove ${labelFor(v)}`}) inside the MultiSelect component so screen readers announce the same text users see, ensuring labelFor is in scope where the remove handler and element are defined and leaving the remove(v, e) handler unchanged.app/components/settings/onboarding/ProjectList.tsx (1)
86-136: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winFix inconsistent interactive element pattern.
The wrapper
divon line 88 hascursor-pointerandhover:shadoweffects, suggesting the entire card is clickable, but thedivitself is not interactive. Instead, there are two separate<button>elements inside (lines 90–105 and 129–135) that both callonSelectProject(project).Issues:
- Misleading cursor:
cursor-pointeron a non-interactivedivcreates false affordance.- Redundant targets: Two buttons performing identical actions increase complexity without clear UX benefit.
- Inconsistent pattern: Compare to
OrganizationList.tsx(line 35), where the entire card is wrapped in a single<button>.♻️ Recommended fix: wrap the entire card in a button
<div className="space-y-2"> {projects.map((project) => ( - <div + <button key={project.id} - className="flex items-center justify-between gap-3 p-4 rounded-lg bg-bg-primary shadow-[0_2px_6px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] hover:shadow-[0_4px_12px_rgba(0,0,0,0.08),0_1px_2px_rgba(0,0,0,0.04)] transition-shadow cursor-pointer" + onClick={() => onSelectProject(project)} + className="w-full flex items-center justify-between gap-3 p-4 rounded-lg bg-bg-primary text-left shadow-[0_2px_6px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] hover:shadow-[0_4px_12px_rgba(0,0,0,0.08),0_1px_2px_rgba(0,0,0,0.04)] transition-shadow cursor-pointer" > - <button - onClick={() => onSelectProject(project)} - className="flex-1 text-left min-w-0 cursor-pointer" - > + <div className="flex-1 min-w-0"> <p className="text-sm font-medium text-text-primary truncate"> {project.name} </p> {project.description && ( <p className="text-xs text-text-secondary mt-0.5 truncate"> {project.description} </p> )} <p className="text-xs text-text-secondary mt-0.5"> Created {formatRelativeTime(project.inserted_at)} </p> - </button> + </div> <div className="flex items-center gap-2 shrink-0"> <span className={`inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full font-medium border ${ project.is_active ? "bg-status-success-bg text-status-success-text border-status-success-border" : "bg-bg-secondary text-text-secondary border-border" }`} > {project.is_active ? "Active" : "Inactive"} </span> {currentUser?.is_superuser && ( <button onClick={(e) => { e.stopPropagation(); setEditingProject(project); }} className="p-1.5 rounded-md text-text-secondary hover:bg-neutral-100 hover:text-text-primary transition-colors cursor-pointer" title="Edit project" aria-label="Edit project" > <EditIcon className="w-4 h-4" /> </button> )} - <button - onClick={() => onSelectProject(project)} - className="p-1.5 rounded-md text-text-secondary hover:bg-neutral-100 hover:text-text-primary transition-colors cursor-pointer" - aria-label="View project" - > - <ChevronRightIcon className="w-4 h-4" /> - </button> + <ChevronRightIcon className="w-4 h-4 text-text-secondary" /> </div> - </div> + </button> ))} </div>This matches the pattern used in
OrganizationList.tsxand provides a single, clear interaction target.🤖 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/settings/onboarding/ProjectList.tsx` around lines 86 - 136, The card div is non-interactive but styled as clickable and contains two buttons that both call onSelectProject(project); change the wrapper <div> into a single interactive <button> (preserving the existing className, key, and onClick behavior) so the whole card is the click target (use onClick={() => onSelectProject(project)} and set aria-label/title like "View project"), remove the duplicate right-side button that also calls onSelectProject, keep the Edit button (setEditingProject) as a separate <button> and ensure it calls e.stopPropagation() so it doesn't trigger the card button; update any cursor/hover classes to remain on the wrapper button to match OrganizationList.tsx pattern.app/components/prompt-editor/DiffView.tsx (1)
129-134:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGuard async compare fetch with
try/catch/finally.Line 130 can leave
isLoadingComparestucktrueifonFetchVersionDetailrejects, and the handler can fail without a graceful fallback.Suggested fix
if (onFetchVersionDetail) { setIsLoadingCompare(true); - detail = (await onFetchVersionDetail(config_id, version)) ?? undefined; - setIsLoadingCompare(false); + try { + detail = (await onFetchVersionDetail(config_id, version)) ?? undefined; + } catch { + detail = undefined; + } finally { + setIsLoadingCompare(false); + } }🤖 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/prompt-editor/DiffView.tsx` around lines 129 - 134, Wrap the async call to onFetchVersionDetail in a try/catch/finally: setIsLoadingCompare(true) before the try, await detail = await onFetchVersionDetail(config_id, version) inside try, call onCompareChange(detail ?? null) in the success path, catch errors to call onCompareChange(null) (or fallback) and optionally log/propagate the error, and always call setIsLoadingCompare(false) in finally so isLoadingCompare cannot be left true if onFetchVersionDetail rejects.app/(main)/configurations/page.tsx (1)
99-129:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse the client-side fetch wrapper for this request.
This page is a client component, but the new evaluation-count call goes through
apiFetch. That bypasses the shared 401 refresh / auth-expiry flow, so this panel can fail independently once the session token expires.As per coding guidelines, "Use
clientFetch(endpoint, options)on the client-side for API requests, which handles token refresh on 401 errors and dispatchesAUTH_EXPIRED_EVENTwhen refresh fails".🤖 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)/configurations/page.tsx around lines 99 - 129, The client-side fetch in useEffect/fetchEvaluationCounts currently calls apiFetch which bypasses client token-refresh logic; switch to clientFetch for the "/api/evaluations" call (keeping the same response parsing into EvalJob[] / {data: EvalJob[]}), ensure you pass the apiKey via options/headers as needed, keep the cancelled guard and setEvaluationCounts logic intact, and add the clientFetch import at the top of the module so the component uses the client-side wrapper that handles 401 refresh and AUTH_EXPIRED_EVENT.app/components/prompt-editor/HistorySidebar.tsx (1)
124-133:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReset version-loading state in
finallyblocksIf the async detail fetch throws/rejects, the loading flags never clear, leaving actions disabled until a rerender/reset.
Suggested fix
const handleAction = useCallback( async (item: ConfigVersionItems, action: "load" | "compare") => { let detail = savedConfigs.find( (c) => c.config_id === configId && c.version === item.version, ); if (!detail && onFetchVersionDetail) { - setFetchingVersion(item.version); - detail = (await onFetchVersionDetail(item.version)) ?? undefined; - setFetchingVersion(null); + setFetchingVersion(item.version); + try { + detail = (await onFetchVersionDetail(item.version)) ?? undefined; + } finally { + setFetchingVersion(null); + } } if (!detail) return; if (action === "load") onLoadVersion(detail); else onSelectVersion(detail); }, @@ const handleAction = useCallback( async (item: ConfigVersionItems, action: "load" | "compare") => { @@ if (!detail && loadSingleVersionForConfig) { const key = `${meta.id}:${item.version}`; setLoadingVersionKey(key); - detail = await loadSingleVersionForConfig(meta.id, item.version); - setLoadingVersionKey(null); + try { + detail = await loadSingleVersionForConfig(meta.id, item.version); + } finally { + setLoadingVersionKey(null); + } } if (!detail) return; if (action === "load") onLoadVersion(detail); else onSelectVersion(detail); },Also applies to: 227-238
🤖 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/prompt-editor/HistorySidebar.tsx` around lines 124 - 133, The async handler handleAction currently sets fetching state with setFetchingVersion(item.version) but only clears it after a successful await, so if onFetchVersionDetail throws the fetching flag remains set; move the setFetchingVersion(null) into a finally block around the await to guarantee the loading flag is cleared regardless of success/failure and do the same fix for the similar logic at the other handler (lines referenced include the other version-fetching block). Ensure you still assign detail from the awaited onFetchVersionDetail result in the try block and preserve existing onFetchVersionDetail and savedConfigs checks while wrapping the await in try/catch/finally (or try/finally) so errors don’t leave the UI stuck loading.
🧹 Nitpick comments (3)
app/components/auth/LoginModal.tsx (1)
53-55: ⚡ Quick winVerify backend error code stability.
The regex pattern
/no account found/idepends on exact backend error message wording. If the backend changes this message, the custom user-facing message won't display and users will see the raw error instead.Consider coordinating with the backend team to use structured error codes (e.g.,
error.code === 'ACCOUNT_NOT_FOUND') instead of string matching. This would make error handling more robust and less prone to breaking on backend message updates.🤖 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/auth/LoginModal.tsx` around lines 53 - 55, The current logic in LoginModal (the message variable derived from raw) relies on regex matching the backend error string (/no account found/i), which is brittle; update the error handling to prefer a structured error code (e.g., check response.error?.code or error.code for 'ACCOUNT_NOT_FOUND') inside the login error path in LoginModal.tsx and set the user-facing message accordingly, falling back to the existing regex or raw text only if no structured code exists; coordinate with the backend to ensure they emit a stable code like ACCOUNT_NOT_FOUND so the code path (message, raw) becomes robust.app/components/knowledge-base/CollectionsList.tsx (1)
66-66: 💤 Low valueConsider restoring icon background for consistency.
The icon bubble no longer has a background (
bg-accent-primary/10was removed), while the empty state at line 39 still renders a filled circle. If visual hierarchy differs intentionally (list items lighter, empty state emphasized), this is fine; otherwise, consider adding backbg-accent-primary/10to match.🤖 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/CollectionsList.tsx` at line 66, In CollectionsList.tsx the icon span inside the list rows (the <span className="shrink-0 inline-flex items-center justify-center w-6 h-6 rounded-full text-accent-primary"> element) lost its background class; restore consistency by adding the background class (e.g., bg-accent-primary/10) back to that span so list item icons match the filled circle used in the empty state (the empty-state marker around line 39), keeping the same visual hierarchy across both render paths.app/components/keystore/KeysCard.tsx (1)
81-82: ⚡ Quick winFormat the added date with the shared date utilities.
Using
toLocaleDateString()here makes this card's output depend on the runtime locale/timezone instead of the app's standard date path. Please switch this to the repo'sdate-fns/date-fns-tzformatting helper.As per coding guidelines, "Use date-fns 4.1.0 and date-fns-tz 3.2.0 for date/time handling across the application".
🤖 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/keystore/KeysCard.tsx` around lines 81 - 82, Replace the runtime-dependent toLocaleDateString() usage in KeysCard (the expression new Date(apiKey.createdAt).toLocaleDateString()) with the repo's shared date-fns/date-fns-tz formatting helper; import the app's standard formatter (e.g., formatDate or the project's date utility) at the top of KeysCard.tsx and call that helper with apiKey.createdAt to produce the displayed "Added" date so the card follows the app-wide date/time formatting conventions.
🤖 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)/configurations/prompt-editor/page.tsx:
- Around line 403-408: In handleRenameConfig replace the client-side call to
apiFetch with clientFetch so the request benefits from built-in
401/token-refresh handling: change the call that currently invokes
apiFetch(`/api/configs/${configId}`, apiKey, { method: "PATCH", body:
JSON.stringify({ name: trimmed }) }) to use
clientFetch(`/api/configs/${configId}`, { method: "PATCH", body:
JSON.stringify({ name: trimmed }), headers: { Authorization: `Bearer ${apiKey}`
} } ) (or the app's standard way of passing the apiKey) and ensure clientFetch
is imported; keep the same response typing ({ success: boolean; error?: string
}) and existing error/response handling in handleRenameConfig.
In `@app/components/ConfigCard.tsx`:
- Around line 49-56: The handleCopyKbId callback starts a timeout to clear
setCopiedKbId but doesn't cancel any previous timer, so rapid successive copies
can clear a newer success state; add a ref (e.g., copyTimeoutRef) to store the
timeout id, call clearTimeout(copyTimeoutRef.current) before scheduling a new
setTimeout, assign the new id to copyTimeoutRef.current, and clear it when the
timeout runs (or on unmount) to ensure copied state isn't cleared prematurely in
the handleCopyKbId useCallback and associated cleanup.
In `@app/components/ConfigLibrarySkeleton.tsx`:
- Around line 7-15: Clamp and sanitize the incoming columnCount and rows inside
ConfigLibrarySkeleton before computing total and gridTemplateColumns: coerce to
integers (e.g. Math.floor or Math.round), default NaN to 1, and ensure a minimum
of 1 (e.g. const safeCols = Math.max(1, Math.floor(columnCount || 1)); const
safeRows = Math.max(1, Math.floor(rows || 1));), then use safeCols/safeRows for
total and the gridTemplateColumns string so invalid/zero/negative/non-integer
inputs cannot produce broken CSS.
In `@app/components/configurations/SelectedConfigPreview.tsx`:
- Around line 45-56: The PreviewField showing "Max Results" currently renders
String(config.tools[0].max_num_results) which will display "undefined" if
max_num_results is missing; update SelectedConfigPreview to guard this value by
either conditionally rendering the PreviewField only when
config.tools[0].max_num_results != null/undefined or formatting it with a real
fallback (e.g., String(config.tools[0].max_num_results ?? "None" or a default
number)); ensure you reference the same symbol (config.tools[0].max_num_results
and the PreviewField with label "Max Results") when applying the fix.
- Around line 18-23: The effect using useLayoutEffect that sets
setPromptExpanded(false) and computes setIsPromptOverflowing based only on
[config.config_id, config.version] can miss layout changes (window resize or
sidebar collapse); update it to re-measure whenever the prompt element or layout
changes by adding a ResizeObserver on promptRef.current (and a window 'resize'
listener as a fallback) inside the useLayoutEffect so you recalculate
el.scrollHeight > el.clientHeight on observable size changes, clean up the
observer/listener on unmount, and keep the existing behavior of resetting
setPromptExpanded(false) when the config changes.
In `@app/components/prompt-editor/ConfigEditorPane.tsx`:
- Around line 123-137: handleApplyRename can leave the UI stuck loading if
onRenameConfig rejects; change it so setIsApplyingRename(true) is called before
awaiting onRenameConfig and the await is wrapped in a try/finally that always
calls setIsApplyingRename(false). Inside the try block await
onRenameConfig(boundConfigId, trimmed) and handle the successful-ok path
(setIsRenaming(false); setRenameDraft("")) as now; use the finally to always
clear the loading state so failures don't wedge the pane.
In `@app/components/prompt-editor/DiffView.tsx`:
- Around line 137-147: The sameConfig check currently compares compareWith?.name
to selectedCommit.name which is brittle; update the comparison to use the unique
config identifier (compareWith?.config_id === selectedCommit.config_id) when
computing sameConfig (and ensure compareWith and selectedCommit are non-null),
leaving the existing compareLabelLeft and compareLabelRight label logic
unchanged so labels and breadcrumbs are correct even if names are duplicated or
configs renamed.
In `@app/components/prompt-editor/HistorySidebar.tsx`:
- Around line 149-152: The clickable header divs in HistorySidebar.tsx should be
changed to semantic buttons that expose their expanded state; replace the <div
onClick={onToggle} ...> used for the expandable headers with a <button> that
preserves the same className and onClick handler (onToggle) and add
aria-expanded={isExpanded} (or the relevant expanded state variable) so
assistive tech can read the state; make the same change for the second
occurrence noted (lines ~258-261) so both headers are keyboard-accessible and
announce expansion.
In `@app/components/prompt-editor/SaveConfigModal.tsx`:
- Around line 54-65: The label and input in the SaveConfigModal component are
not associated for accessibility; add a unique id (e.g., "commitMessageInput")
to the input that binds to the label via htmlFor on the label so screen readers
and keyboard navigation can correctly focus the commitMessage input; update the
label element and the input element (which uses value commitMessage, onChange
onCommitMessageChange, placeholder isUpdate and disabled isSaving) to include
the matching htmlFor/id pair.
---
Outside diff comments:
In `@app/`(main)/configurations/page.tsx:
- Around line 99-129: The client-side fetch in useEffect/fetchEvaluationCounts
currently calls apiFetch which bypasses client token-refresh logic; switch to
clientFetch for the "/api/evaluations" call (keeping the same response parsing
into EvalJob[] / {data: EvalJob[]}), ensure you pass the apiKey via
options/headers as needed, keep the cancelled guard and setEvaluationCounts
logic intact, and add the clientFetch import at the top of the module so the
component uses the client-side wrapper that handles 401 refresh and
AUTH_EXPIRED_EVENT.
In `@app/components/MultiSelect.tsx`:
- Around line 145-155: The remove button's aria-label currently exposes the raw
value ID (aria-label={`Remove ${v}`}) instead of the user-facing chip label;
update the aria-label to use the displayed label by calling labelFor(v) (e.g.,
aria-label={`Remove ${labelFor(v)}`}) inside the MultiSelect component so screen
readers announce the same text users see, ensuring labelFor is in scope where
the remove handler and element are defined and leaving the remove(v, e) handler
unchanged.
In `@app/components/prompt-editor/DiffView.tsx`:
- Around line 129-134: Wrap the async call to onFetchVersionDetail in a
try/catch/finally: setIsLoadingCompare(true) before the try, await detail =
await onFetchVersionDetail(config_id, version) inside try, call
onCompareChange(detail ?? null) in the success path, catch errors to call
onCompareChange(null) (or fallback) and optionally log/propagate the error, and
always call setIsLoadingCompare(false) in finally so isLoadingCompare cannot be
left true if onFetchVersionDetail rejects.
In `@app/components/prompt-editor/HistorySidebar.tsx`:
- Around line 124-133: The async handler handleAction currently sets fetching
state with setFetchingVersion(item.version) but only clears it after a
successful await, so if onFetchVersionDetail throws the fetching flag remains
set; move the setFetchingVersion(null) into a finally block around the await to
guarantee the loading flag is cleared regardless of success/failure and do the
same fix for the similar logic at the other handler (lines referenced include
the other version-fetching block). Ensure you still assign detail from the
awaited onFetchVersionDetail result in the try block and preserve existing
onFetchVersionDetail and savedConfigs checks while wrapping the await in
try/catch/finally (or try/finally) so errors don’t leave the UI stuck loading.
In `@app/components/settings/onboarding/ProjectList.tsx`:
- Around line 86-136: The card div is non-interactive but styled as clickable
and contains two buttons that both call onSelectProject(project); change the
wrapper <div> into a single interactive <button> (preserving the existing
className, key, and onClick behavior) so the whole card is the click target (use
onClick={() => onSelectProject(project)} and set aria-label/title like "View
project"), remove the duplicate right-side button that also calls
onSelectProject, keep the Edit button (setEditingProject) as a separate <button>
and ensure it calls e.stopPropagation() so it doesn't trigger the card button;
update any cursor/hover classes to remain on the wrapper button to match
OrganizationList.tsx pattern.
---
Nitpick comments:
In `@app/components/auth/LoginModal.tsx`:
- Around line 53-55: The current logic in LoginModal (the message variable
derived from raw) relies on regex matching the backend error string (/no account
found/i), which is brittle; update the error handling to prefer a structured
error code (e.g., check response.error?.code or error.code for
'ACCOUNT_NOT_FOUND') inside the login error path in LoginModal.tsx and set the
user-facing message accordingly, falling back to the existing regex or raw text
only if no structured code exists; coordinate with the backend to ensure they
emit a stable code like ACCOUNT_NOT_FOUND so the code path (message, raw)
becomes robust.
In `@app/components/keystore/KeysCard.tsx`:
- Around line 81-82: Replace the runtime-dependent toLocaleDateString() usage in
KeysCard (the expression new Date(apiKey.createdAt).toLocaleDateString()) with
the repo's shared date-fns/date-fns-tz formatting helper; import the app's
standard formatter (e.g., formatDate or the project's date utility) at the top
of KeysCard.tsx and call that helper with apiKey.createdAt to produce the
displayed "Added" date so the card follows the app-wide date/time formatting
conventions.
In `@app/components/knowledge-base/CollectionsList.tsx`:
- Line 66: In CollectionsList.tsx the icon span inside the list rows (the <span
className="shrink-0 inline-flex items-center justify-center w-6 h-6 rounded-full
text-accent-primary"> element) lost its background class; restore consistency by
adding the background class (e.g., bg-accent-primary/10) back to that span so
list item icons match the filled circle used in the empty state (the empty-state
marker around line 39), keeping the same visual hierarchy across both render
paths.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e98aff75-fe6b-4aad-b047-ab17de8734b4
📒 Files selected for processing (45)
app/(main)/configurations/page.tsxapp/(main)/configurations/prompt-editor/page.tsxapp/(main)/evaluations/[id]/page.tsxapp/(main)/evaluations/page.tsxapp/(main)/keystore/page.tsxapp/(main)/settings/onboarding/page.tsxapp/components/ConfigCard.tsxapp/components/ConfigDrawer.tsxapp/components/ConfigLibrarySkeleton.tsxapp/components/ConfigSelector.tsxapp/components/Field.tsxapp/components/MultiSelect.tsxapp/components/PageHeader.tsxapp/components/SimplifiedConfigEditor.tsxapp/components/auth/LoginModal.tsxapp/components/configurations/SelectedConfigPreview.tsxapp/components/configurations/VersionPill.tsxapp/components/document/DocumentPreview.tsxapp/components/document/DocumentPreviewSkeleton.tsxapp/components/index.tsapp/components/keystore/AddKeyModal.tsxapp/components/keystore/KeysCard.tsxapp/components/knowledge-base/CollectionsList.tsxapp/components/prompt-editor/ABTestTab.tsxapp/components/prompt-editor/BranchModal.tsxapp/components/prompt-editor/CommitNode.tsxapp/components/prompt-editor/ConfigDrawer.tsxapp/components/prompt-editor/ConfigEditorPane.tsxapp/components/prompt-editor/CurrentConfigTab.tsxapp/components/prompt-editor/DiffView.tsxapp/components/prompt-editor/EditorView.tsxapp/components/prompt-editor/GuardrailsSection.tsxapp/components/prompt-editor/Header.tsxapp/components/prompt-editor/HistorySidebar.tsxapp/components/prompt-editor/HistoryTab.tsxapp/components/prompt-editor/MergeModal.tsxapp/components/prompt-editor/PromptEditorPane.tsxapp/components/prompt-editor/SaveConfigModal.tsxapp/components/prompt-editor/UnifiedCommitBar.tsxapp/components/settings/credentials/CredentialForm.tsxapp/components/settings/credentials/CredentialFormPanel.tsxapp/components/settings/onboarding/OrganizationList.tsxapp/components/settings/onboarding/ProjectList.tsxapp/components/settings/onboarding/UserList.tsxapp/components/text-to-speech/DatasetDescription.tsx
💤 Files with no reviewable changes (11)
- app/components/prompt-editor/HistoryTab.tsx
- app/components/prompt-editor/UnifiedCommitBar.tsx
- app/components/prompt-editor/CommitNode.tsx
- app/components/prompt-editor/BranchModal.tsx
- app/components/SimplifiedConfigEditor.tsx
- app/components/prompt-editor/EditorView.tsx
- app/components/prompt-editor/ABTestTab.tsx
- app/components/prompt-editor/CurrentConfigTab.tsx
- app/components/ConfigDrawer.tsx
- app/components/prompt-editor/ConfigDrawer.tsx
- app/components/prompt-editor/MergeModal.tsx
| try { | ||
| const data = await apiFetch<{ success: boolean; error?: string }>( | ||
| `/api/configs/${configId}`, | ||
| apiKey, | ||
| { method: "PATCH", body: JSON.stringify({ name: trimmed }) }, | ||
| ); |
There was a problem hiding this comment.
Use clientFetch in the client rename path
handleRenameConfig is a client-side API call but currently uses apiFetch; this bypasses the standardized 401 refresh/auth-expiry handling required in this app.
Suggested fix
-import { apiFetch } from "@/app/lib/apiClient";
+import { apiFetch, clientFetch } from "@/app/lib/apiClient";
@@
- const apiKey = activeKey?.key ?? "";
if (!isAuthenticated) {
toast.error("Please log in to rename configurations.");
return false;
}
@@
- const data = await apiFetch<{ success: boolean; error?: string }>(
+ const data = await clientFetch<{ success: boolean; error?: string }>(
`/api/configs/${configId}`,
- apiKey,
- { method: "PATCH", body: JSON.stringify({ name: trimmed }) },
+ { method: "PATCH", body: JSON.stringify({ name: trimmed }) },
);As per coding guidelines, "Use clientFetch(endpoint, options) on the client-side for API requests, which handles token refresh on 401 errors and dispatches AUTH_EXPIRED_EVENT when refresh fails".
🤖 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)/configurations/prompt-editor/page.tsx around lines 403 - 408, In
handleRenameConfig replace the client-side call to apiFetch with clientFetch so
the request benefits from built-in 401/token-refresh handling: change the call
that currently invokes apiFetch(`/api/configs/${configId}`, apiKey, { method:
"PATCH", body: JSON.stringify({ name: trimmed }) }) to use
clientFetch(`/api/configs/${configId}`, { method: "PATCH", body:
JSON.stringify({ name: trimmed }), headers: { Authorization: `Bearer ${apiKey}`
} } ) (or the app's standard way of passing the apiKey) and ensure clientFetch
is imported; keep the same response typing ({ success: boolean; error?: string
}) and existing error/response handling in handleRenameConfig.
| const handleCopyKbId = useCallback( | ||
| async (e: React.MouseEvent, kbId: string) => { | ||
| e.stopPropagation(); | ||
| try { | ||
| await navigator.clipboard.writeText(kbId); | ||
| setCopiedKbId(kbId); | ||
| toast.success("Knowledge Base ID copied to clipboard"); | ||
| setTimeout(() => setCopiedKbId(null), 1500); |
There was a problem hiding this comment.
Clear the previous KB-copy timer before starting a new one.
Copying two Knowledge Base IDs back-to-back lets the first setTimeout clear the second item's success state early. Store the timeout id in a ref and cancel it before scheduling the next reset.
Suggested fix
-import { useState, useCallback } from "react";
+import { useState, useCallback, useRef, useEffect } from "react";
@@
const [copiedKbId, setCopiedKbId] = useState<string | null>(null);
+ const copiedKbTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@
try {
await navigator.clipboard.writeText(kbId);
setCopiedKbId(kbId);
toast.success("Knowledge Base ID copied to clipboard");
- setTimeout(() => setCopiedKbId(null), 1500);
+ if (copiedKbTimeoutRef.current) {
+ clearTimeout(copiedKbTimeoutRef.current);
+ }
+ copiedKbTimeoutRef.current = setTimeout(() => {
+ setCopiedKbId(null);
+ }, 1500);
} catch {
toast.error("Failed to copy");
}
@@
+ useEffect(() => {
+ return () => {
+ if (copiedKbTimeoutRef.current) {
+ clearTimeout(copiedKbTimeoutRef.current);
+ }
+ };
+ }, []);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleCopyKbId = useCallback( | |
| async (e: React.MouseEvent, kbId: string) => { | |
| e.stopPropagation(); | |
| try { | |
| await navigator.clipboard.writeText(kbId); | |
| setCopiedKbId(kbId); | |
| toast.success("Knowledge Base ID copied to clipboard"); | |
| setTimeout(() => setCopiedKbId(null), 1500); | |
| import { useState, useCallback, useRef, useEffect } from "react"; | |
| const [copiedKbId, setCopiedKbId] = useState<string | null>(null); | |
| const copiedKbTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); | |
| const handleCopyKbId = useCallback( | |
| async (e: React.MouseEvent, kbId: string) => { | |
| e.stopPropagation(); | |
| try { | |
| await navigator.clipboard.writeText(kbId); | |
| setCopiedKbId(kbId); | |
| toast.success("Knowledge Base ID copied to clipboard"); | |
| if (copiedKbTimeoutRef.current) { | |
| clearTimeout(copiedKbTimeoutRef.current); | |
| } | |
| copiedKbTimeoutRef.current = setTimeout(() => { | |
| setCopiedKbId(null); | |
| }, 1500); | |
| } catch { | |
| toast.error("Failed to copy"); | |
| } | |
| }, | |
| [] | |
| ); | |
| useEffect(() => { | |
| return () => { | |
| if (copiedKbTimeoutRef.current) { | |
| clearTimeout(copiedKbTimeoutRef.current); | |
| } | |
| }; | |
| }, []); |
🤖 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/ConfigCard.tsx` around lines 49 - 56, The handleCopyKbId
callback starts a timeout to clear setCopiedKbId but doesn't cancel any previous
timer, so rapid successive copies can clear a newer success state; add a ref
(e.g., copyTimeoutRef) to store the timeout id, call
clearTimeout(copyTimeoutRef.current) before scheduling a new setTimeout, assign
the new id to copyTimeoutRef.current, and clear it when the timeout runs (or on
unmount) to ensure copied state isn't cleared prematurely in the handleCopyKbId
useCallback and associated cleanup.
| columnCount = 3, | ||
| rows = 2, | ||
| }: ConfigLibrarySkeletonProps) { | ||
| const total = columnCount * rows; | ||
| return ( | ||
| <div | ||
| className="grid gap-4 items-start w-full animate-pulse" | ||
| style={{ gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))` }} | ||
| > |
There was a problem hiding this comment.
Clamp columnCount and rows before computing layout.
Line 14 can emit invalid gridTemplateColumns when these props are zero/negative/non-integer, which can break skeleton layout unexpectedly.
Suggested fix
export default function ConfigLibrarySkeleton({
columnCount = 3,
rows = 2,
}: ConfigLibrarySkeletonProps) {
- const total = columnCount * rows;
+ const safeColumnCount = Math.max(1, Math.floor(columnCount));
+ const safeRows = Math.max(1, Math.floor(rows));
+ const total = safeColumnCount * safeRows;
return (
<div
className="grid gap-4 items-start w-full animate-pulse"
- style={{ gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))` }}
+ style={{
+ gridTemplateColumns: `repeat(${safeColumnCount}, minmax(0, 1fr))`,
+ }}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| columnCount = 3, | |
| rows = 2, | |
| }: ConfigLibrarySkeletonProps) { | |
| const total = columnCount * rows; | |
| return ( | |
| <div | |
| className="grid gap-4 items-start w-full animate-pulse" | |
| style={{ gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))` }} | |
| > | |
| export default function ConfigLibrarySkeleton({ | |
| columnCount = 3, | |
| rows = 2, | |
| }: ConfigLibrarySkeletonProps) { | |
| const safeColumnCount = Math.max(1, Math.floor(columnCount)); | |
| const safeRows = Math.max(1, Math.floor(rows)); | |
| const total = safeColumnCount * safeRows; | |
| return ( | |
| <div | |
| className="grid gap-4 items-start w-full animate-pulse" | |
| style={{ | |
| gridTemplateColumns: `repeat(${safeColumnCount}, minmax(0, 1fr))`, | |
| }} | |
| > |
🤖 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/ConfigLibrarySkeleton.tsx` around lines 7 - 15, Clamp and
sanitize the incoming columnCount and rows inside ConfigLibrarySkeleton before
computing total and gridTemplateColumns: coerce to integers (e.g. Math.floor or
Math.round), default NaN to 1, and ensure a minimum of 1 (e.g. const safeCols =
Math.max(1, Math.floor(columnCount || 1)); const safeRows = Math.max(1,
Math.floor(rows || 1));), then use safeCols/safeRows for total and the
gridTemplateColumns string so invalid/zero/negative/non-integer inputs cannot
produce broken CSS.
| useLayoutEffect(() => { | ||
| setPromptExpanded(false); | ||
| const el = promptRef.current; | ||
| if (!el) return; | ||
| setIsPromptOverflowing(el.scrollHeight > el.clientHeight); | ||
| }, [config.config_id, config.version]); |
There was a problem hiding this comment.
Re-measure prompt overflow when the layout changes.
This effect only reruns when config.config_id or config.version changes. Resizing the window or collapsing the sidebar changes line wrapping too, so the expand/collapse affordance can get stuck in the wrong state for the same config.
🤖 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/configurations/SelectedConfigPreview.tsx` around lines 18 -
23, The effect using useLayoutEffect that sets setPromptExpanded(false) and
computes setIsPromptOverflowing based only on [config.config_id, config.version]
can miss layout changes (window resize or sidebar collapse); update it to
re-measure whenever the prompt element or layout changes by adding a
ResizeObserver on promptRef.current (and a window 'resize' listener as a
fallback) inside the useLayoutEffect so you recalculate el.scrollHeight >
el.clientHeight on observable size changes, clean up the observer/listener on
unmount, and keep the existing behavior of resetting setPromptExpanded(false)
when the config changes.
| {config.tools && config.tools.length > 0 && ( | ||
| <> | ||
| <div className="col-span-2"> | ||
| <PreviewLabel>Knowledge Base IDs</PreviewLabel> | ||
| <div className="text-xs font-mono break-all text-text-primary"> | ||
| {knowledgeBaseIds || "None"} | ||
| </div> | ||
| </div> | ||
| <PreviewField | ||
| label="Max Results" | ||
| value={String(config.tools[0].max_num_results)} | ||
| /> |
There was a problem hiding this comment.
Guard max_num_results before stringifying it.
If the first tool omits max_num_results, this renders "undefined" in the preview. Either skip the field when the value is absent or provide a real fallback.
🤖 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/configurations/SelectedConfigPreview.tsx` around lines 45 -
56, The PreviewField showing "Max Results" currently renders
String(config.tools[0].max_num_results) which will display "undefined" if
max_num_results is missing; update SelectedConfigPreview to guard this value by
either conditionally rendering the PreviewField only when
config.tools[0].max_num_results != null/undefined or formatting it with a real
fallback (e.g., String(config.tools[0].max_num_results ?? "None" or a default
number)); ensure you reference the same symbol (config.tools[0].max_num_results
and the PreviewField with label "Max Results") when applying the fix.
| const handleApplyRename = async () => { | ||
| if (!boundConfigId || !onRenameConfig) return; | ||
| const trimmed = renameDraft.trim(); | ||
| if (!trimmed || trimmed === configName) { | ||
| handleCancelRename(); | ||
| return; | ||
| } | ||
| setIsApplyingRename(true); | ||
| const ok = await onRenameConfig(boundConfigId, trimmed); | ||
| setIsApplyingRename(false); | ||
| if (ok) { | ||
| setIsRenaming(false); | ||
| setRenameDraft(""); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Always clear the rename loading state on failure.
If onRenameConfig rejects, this exits before setIsApplyingRename(false) runs, and the rename UI stays disabled. Wrap the await in try/finally so transient API failures don't wedge the pane.
Suggested fix
const handleApplyRename = async () => {
if (!boundConfigId || !onRenameConfig) return;
const trimmed = renameDraft.trim();
if (!trimmed || trimmed === configName) {
handleCancelRename();
return;
}
- setIsApplyingRename(true);
- const ok = await onRenameConfig(boundConfigId, trimmed);
- setIsApplyingRename(false);
+ setIsApplyingRename(true);
+ let ok = false;
+ try {
+ ok = await onRenameConfig(boundConfigId, trimmed);
+ } finally {
+ setIsApplyingRename(false);
+ }
if (ok) {
setIsRenaming(false);
setRenameDraft("");
}
};🤖 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/prompt-editor/ConfigEditorPane.tsx` around lines 123 - 137,
handleApplyRename can leave the UI stuck loading if onRenameConfig rejects;
change it so setIsApplyingRename(true) is called before awaiting onRenameConfig
and the await is wrapped in a try/finally that always calls
setIsApplyingRename(false). Inside the try block await
onRenameConfig(boundConfigId, trimmed) and handle the successful-ok path
(setIsRenaming(false); setRenameDraft("")) as now; use the finally to always
clear the loading state so failures don't wedge the pane.
| const sameConfig = compareWith?.name === selectedCommit.name; | ||
| const compareLabelLeft = compareWith | ||
| ? sameConfig | ||
| ? `Load v${compareWith.version}` | ||
| : `Load ${compareWith.name} v${compareWith.version}` | ||
| : ""; | ||
| const compareLabelRight = compareWith | ||
| ? sameConfig | ||
| ? `Load v${selectedCommit.version}` | ||
| : `Load ${selectedCommit.name} v${selectedCommit.version}` | ||
| : ""; |
There was a problem hiding this comment.
Use config_id for same-config checks, not name.
Line 137 currently compares names; duplicate names or renamed configs can produce wrong labels and breadcrumbs.
Suggested fix
- const sameConfig = compareWith?.name === selectedCommit.name;
+ const sameConfig = compareWith?.config_id === selectedCommit.config_id;🤖 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/prompt-editor/DiffView.tsx` around lines 137 - 147, The
sameConfig check currently compares compareWith?.name to selectedCommit.name
which is brittle; update the comparison to use the unique config identifier
(compareWith?.config_id === selectedCommit.config_id) when computing sameConfig
(and ensure compareWith and selectedCommit are non-null), leaving the existing
compareLabelLeft and compareLabelRight label logic unchanged so labels and
breadcrumbs are correct even if names are duplicated or configs renamed.
| <div | ||
| onClick={onToggle} | ||
| className="p-3 cursor-pointer" | ||
| style={{ | ||
| backgroundColor: colors.bg.secondary, | ||
| transition: "all 0.15s ease", | ||
| }} | ||
| onMouseEnter={(e) => | ||
| (e.currentTarget.style.backgroundColor = "#f5f5f5") | ||
| } | ||
| onMouseLeave={(e) => | ||
| (e.currentTarget.style.backgroundColor = colors.bg.secondary) | ||
| } | ||
| className="p-3 cursor-pointer bg-bg-primary hover:bg-neutral-50 transition-colors" | ||
| > |
There was a problem hiding this comment.
Use semantic toggle controls for expandable headers
These expandable headers are clickable divs, which are not keyboard-accessible and do not expose expanded state to assistive tech. Use button with aria-expanded.
Suggested fix
- <div
- onClick={onToggle}
- className="p-3 cursor-pointer bg-bg-primary hover:bg-neutral-50 transition-colors"
- >
+ <button
+ type="button"
+ onClick={onToggle}
+ aria-expanded={isExpanded}
+ className="w-full text-left p-3 cursor-pointer bg-bg-primary hover:bg-neutral-50 transition-colors"
+ >
@@
- </div>
+ </button>Also applies to: 258-261
🤖 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/prompt-editor/HistorySidebar.tsx` around lines 149 - 152, The
clickable header divs in HistorySidebar.tsx should be changed to semantic
buttons that expose their expanded state; replace the <div onClick={onToggle}
...> used for the expandable headers with a <button> that preserves the same
className and onClick handler (onToggle) and add aria-expanded={isExpanded} (or
the relevant expanded state variable) so assistive tech can read the state; make
the same change for the second occurrence noted (lines ~258-261) so both headers
are keyboard-accessible and announce expansion.
| <label className="block text-xs font-semibold mb-2 text-text-primary"> | ||
| Commit Message{" "} | ||
| <span className="font-normal text-text-secondary">(Optional)</span> | ||
| </label> | ||
| <input | ||
| type="text" | ||
| value={commitMessage} | ||
| onChange={(e) => onCommitMessageChange(e.target.value)} | ||
| placeholder={isUpdate ? "Describe your changes…" : "Initial version"} | ||
| disabled={isSaving} | ||
| className="w-full px-3 py-2 rounded-md border border-border bg-bg-primary text-text-primary text-sm focus:outline-none focus:ring-2 focus:ring-accent-primary/20 focus:border-accent-primary transition-colors" | ||
| /> |
There was a problem hiding this comment.
Associate the commit-message label with its input.
Line 54 and Line 58 are not explicitly linked, which hurts accessibility in form navigation.
Suggested fix
export default function SaveConfigModal({
@@
}: SaveConfigModalProps) {
+ const commitMessageInputId = "save-config-commit-message";
return (
@@
- <label className="block text-xs font-semibold mb-2 text-text-primary">
+ <label
+ htmlFor={commitMessageInputId}
+ className="block text-xs font-semibold mb-2 text-text-primary"
+ >
Commit Message{" "}
<span className="font-normal text-text-secondary">(Optional)</span>
</label>
<input
+ id={commitMessageInputId}
type="text"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <label className="block text-xs font-semibold mb-2 text-text-primary"> | |
| Commit Message{" "} | |
| <span className="font-normal text-text-secondary">(Optional)</span> | |
| </label> | |
| <input | |
| type="text" | |
| value={commitMessage} | |
| onChange={(e) => onCommitMessageChange(e.target.value)} | |
| placeholder={isUpdate ? "Describe your changes…" : "Initial version"} | |
| disabled={isSaving} | |
| className="w-full px-3 py-2 rounded-md border border-border bg-bg-primary text-text-primary text-sm focus:outline-none focus:ring-2 focus:ring-accent-primary/20 focus:border-accent-primary transition-colors" | |
| /> | |
| export default function SaveConfigModal({ | |
| isUpdate, | |
| isSaving, | |
| commitMessage, | |
| onCommitMessageChange, | |
| }: SaveConfigModalProps) { | |
| const commitMessageInputId = "save-config-commit-message"; | |
| return ( | |
| // ... previous JSX ... | |
| <> | |
| <label | |
| htmlFor={commitMessageInputId} | |
| className="block text-xs font-semibold mb-2 text-text-primary" | |
| > | |
| Commit Message{" "} | |
| <span className="font-normal text-text-secondary">(Optional)</span> | |
| </label> | |
| <input | |
| id={commitMessageInputId} | |
| type="text" | |
| value={commitMessage} | |
| onChange={(e) => onCommitMessageChange(e.target.value)} | |
| placeholder={isUpdate ? "Describe your changes…" : "Initial version"} | |
| disabled={isSaving} | |
| className="w-full px-3 py-2 rounded-md border border-border bg-bg-primary text-text-primary text-sm focus:outline-none focus:ring-2 focus:ring-accent-primary/20 focus:border-accent-primary transition-colors" | |
| /> | |
| </> | |
| // ... remaining JSX ... | |
| ); | |
| } |
🤖 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/prompt-editor/SaveConfigModal.tsx` around lines 54 - 65, The
label and input in the SaveConfigModal component are not associated for
accessibility; add a unique id (e.g., "commitMessageInput") to the input that
binds to the label via htmlFor on the label so screen readers and keyboard
navigation can correctly focus the commitMessage input; update the label element
and the input element (which uses value commitMessage, onChange
onCommitMessageChange, placeholder isUpdate and disabled isSaving) to include
the matching htmlFor/id pair.
c9461f9
into
feat/document-knowledge-base-ui-revamp
Issue: #137
Summary
Update the Configuration surface and adjacent pages,
color/stylecleanup, component extraction, and dead-code removal in service of the configuration revamp.Configuration pages
ConfigLibrarySkeletonso the loading state is reusable across pages.ConfigCard
MetaPillsubcomponent for consistent metadata display.Buttonprimitive instead of bespoke buttons.Prompt editor
handleRenameConfigso configurations can be renamed inline via the API.Loaderfrom the components barrel.Keystore
keystore/page.tsxintoKeysCardandAddKeyModalto keep files under the 500-LOC cap.alert()notifications withuseToastfeedback.Evaluations
Loaderimports onto the centralized barrel export (no behavior change).Onboarding
Cleanup
ConfigDrawer.tsx(~1,240 LOC); its functionality now lives in the dedicated modal + sidebar components.Summary by CodeRabbit
Release Notes
New Features
Refactor