Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
reviews:
auto_review:
enabled: true
labels:
- "ready-for-review"
base_branches:
- ".*" # allows all branches
11 changes: 11 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,17 @@ import { useApp } from '@/app/lib/context/AppContext';
const { sidebarCollapsed, setSidebarCollapsed, toggleSidebar } = useApp();
```

## Code Quality Guidelines

Follow these rules when writing or modifying code in this repository:

- **File size limit**: Do not let any file exceed **500 LOC**. If a file is approaching or has crossed this limit, split it into smaller modules (extract sub-components, hooks, utilities, or types into their own files).
- **Single Responsibility Principle (SRP)**: Each component, hook, function, or module should do one thing and have one reason to change. If a component handles data fetching, business logic, and UI rendering all together, split it — extract data fetching into a hook, business logic into a utility, and keep the component focused on presentation.
- **Don't Repeat Yourself (DRY)**: Before writing new logic, search the codebase for existing implementations. Reuse and extend rather than duplicate. If you spot the same pattern emerging in 2+ places, extract it into a shared helper, hook, or component in `app/lib/` or `app/components/`.
- **Reuse existing components and icons**: Always check `app/components/` and `app/components/icons/` before creating a new component or icon. Prefer composing or extending existing primitives over authoring new ones. New icons go in `app/components/icons/` as hand-authored React components — do not inline SVGs in feature code.
- **Reuse existing utilities and hooks**: Check `app/lib/utils/`, `app/lib/utils.ts`, and `app/hooks/` before adding new helpers. Domain-specific utilities belong under `app/lib/utils/<domain>/`.
- **Reuse existing types**: Shared types live in `app/lib/types/` and `app/lib/models.ts` — import from there instead of redefining shapes locally.

## API Client & Error Handling

The BFF layer uses [apiClient.ts](app/lib/apiClient.ts) which forwards requests from Next.js route handlers to the backend at `BACKEND_URL` (defaults to `http://localhost:8000`). Key patterns:
Expand Down
156 changes: 51 additions & 105 deletions app/(main)/configurations/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { useState, useEffect, useCallback, useMemo } from "react";
import { useRouter } from "next/navigation";
import Sidebar from "@/app/components/Sidebar";
import PageHeader from "@/app/components/PageHeader";
import { colors } from "@/app/lib/colors";
import { usePaginatedList, useInfiniteScroll } from "@/app/hooks";
import { Button, Loader } from "@/app/components";
import ConfigCard from "@/app/components/ConfigCard";
import Loader, { LoaderBox } from "@/app/components/Loader";
import ConfigLibrarySkeleton from "@/app/components/ConfigLibrarySkeleton";
import { usePaginatedList, useInfiniteScroll } from "@/app/hooks";
import { EvalJob } from "@/app/lib/types/evaluation";
import {
ConfigPublic,
Expand Down Expand Up @@ -68,7 +68,6 @@ export default function ConfigLibraryPage() {
isLoading: isLoading || isLoadingMore,
});

// Responsive column count (matches Tailwind lg/xl breakpoints)
useEffect(() => {
const update = () => {
if (window.innerWidth >= 1280) setColumnCount(3);
Expand All @@ -80,7 +79,6 @@ export default function ConfigLibraryPage() {
return () => window.removeEventListener("resize", update);
}, []);

// Distribute configs into fixed columns so items never shift between columns
const columns = useMemo(() => {
const cols: ConfigPublic[][] = Array.from(
{ length: columnCount },
Expand All @@ -99,13 +97,16 @@ export default function ConfigLibraryPage() {
}, [searchInput]);

useEffect(() => {
if (!isAuthenticated || !apiKey) return;

let cancelled = false;
const fetchEvaluationCounts = async () => {
if (!isAuthenticated) return;
try {
const data = await apiFetch<EvalJob[] | { data: EvalJob[] }>(
"/api/evaluations",
apiKey,
);
if (cancelled) return;
const jobs: EvalJob[] = Array.isArray(data) ? data : data.data || [];
const counts: Record<string, number> = {};
jobs.forEach((job) => {
Expand All @@ -115,11 +116,17 @@ export default function ConfigLibraryPage() {
});
setEvaluationCounts(counts);
} catch (e) {
console.error("Failed to fetch evaluation counts:", e);
if (!cancelled) {
console.warn("Could not fetch evaluation counts:", e);
}
}
};
fetchEvaluationCounts();
}, [activeKey]);

return () => {
cancelled = true;
};
}, [apiKey, isAuthenticated]);

const loadVersionsForConfig = useCallback(
async (configId: string) => {
Expand Down Expand Up @@ -187,157 +194,96 @@ export default function ConfigLibraryPage() {
};

return (
<div
className="w-full h-screen flex flex-col"
style={{ backgroundColor: colors.bg.secondary }}
>
<div className="w-full h-screen flex flex-col bg-bg-secondary">
<div className="flex flex-1 overflow-hidden">
<Sidebar collapsed={sidebarCollapsed} activeRoute="/configurations" />

<div className="flex-1 flex flex-col overflow-hidden">
<PageHeader
title="Configuration Library"
subtitle="Manage your prompts and model configurations"
subtitle="Browse, version, and edit your prompts and model setups"
/>

{/* Toolbar */}
<div
className="px-6 py-4 flex items-center gap-4"
style={{
borderBottom: `1px solid ${colors.border}`,
backgroundColor: colors.bg.primary,
}}
>
<div className="px-6 py-4 flex items-center gap-2 bg-bg-primary">
<div className="flex-1 relative">
<SearchIcon
className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4"
style={{ color: colors.text.secondary }}
/>
<SearchIcon className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-text-secondary pointer-events-none" />
<input
type="text"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
placeholder="Search configs..."
className="w-full pl-10 pr-4 py-2 rounded-md text-sm focus:outline-none transition-colors"
style={{
backgroundColor: colors.bg.secondary,
border: `1px solid ${colors.border}`,
color: colors.text.primary,
}}
className="w-full pl-11 pr-4 py-3 rounded-full bg-bg-secondary text-text-primary text-sm placeholder:text-neutral focus:outline-none focus:ring-1 focus:ring-accent-primary focus:bg-bg-primary transition-colors"
/>
Comment on lines 210 to 216
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add an explicit accessible label for the search input.

The input currently relies on placeholder text only (Line 214). Add a <label> (visually hidden is fine) or an aria-label so the control has a reliable accessible name.

Suggested patch
+              <label htmlFor="config-search" className="sr-only">
+                Search configurations
+              </label>
               <input
+                id="config-search"
                 type="text"
                 value={searchInput}
                 onChange={(e) => setSearchInput(e.target.value)}
                 placeholder="Search configs..."
                 className="w-full pl-11 pr-4 py-3 rounded-full bg-bg-secondary text-text-primary text-sm placeholder:text-neutral focus:outline-none focus:ring-1 focus:ring-accent-primary focus:bg-bg-primary transition-colors"
               />
📝 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.

Suggested change
<input
type="text"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
placeholder="Search configs..."
className="w-full pl-10 pr-4 py-2 rounded-md text-sm focus:outline-none transition-colors"
style={{
backgroundColor: colors.bg.secondary,
border: `1px solid ${colors.border}`,
color: colors.text.primary,
}}
className="w-full pl-11 pr-4 py-3 rounded-full bg-bg-secondary text-text-primary text-sm placeholder:text-neutral focus:outline-none focus:ring-1 focus:ring-accent-primary focus:bg-bg-primary transition-colors"
/>
<label htmlFor="config-search" className="sr-only">
Search configurations
</label>
<input
id="config-search"
type="text"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
placeholder="Search configs..."
className="w-full pl-11 pr-4 py-3 rounded-full bg-bg-secondary text-text-primary text-sm placeholder:text-neutral focus:outline-none focus:ring-1 focus:ring-accent-primary focus:bg-bg-primary transition-colors"
/>
🤖 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 210 - 216, The search input
using the state variables searchInput and setSearchInput lacks an accessible
name; add an explicit label by either: (A) adding a <label> tied to the input
via an id (e.g., give the input id="config-search" and render a visually-hidden
<label htmlFor="config-search">Search configurations</label>), or (B) adding an
aria-label attribute (e.g., aria-label="Search configurations") directly on the
input; ensure the label text is descriptive like "Search configurations" and
that the change is applied to the same input element that uses
value={searchInput} and onChange={(e) => setSearchInput(e.target.value)}.

</div>

<button
onClick={refetch}
disabled={isLoading}
className="p-2 rounded-md transition-colors flex items-center gap-1"
style={{
backgroundColor: colors.bg.primary,
border: `1px solid ${colors.border}`,
color: colors.text.secondary,
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = colors.bg.secondary;
e.currentTarget.style.color = colors.text.primary;
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = colors.bg.primary;
e.currentTarget.style.color = colors.text.secondary;
}}
title="Force refresh from server"
className="p-2 rounded-full text-text-secondary hover:bg-neutral-100 hover:text-text-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
title="Refresh from server"
aria-label="Refresh"
>
<RefreshIcon
className={`w-4 h-4 ${isLoading ? "animate-spin" : ""}`}
/>
</button>

<button
onClick={handleCreateNew}
className="px-4 py-2 rounded-md text-sm font-medium flex items-center gap-2 transition-colors"
style={{
backgroundColor: colors.accent.primary,
color: colors.bg.primary,
border: "none",
}}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = colors.accent.hover)
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = colors.accent.primary)
}
>
<Button variant="primary" size="md" onClick={handleCreateNew}>
<PlusIcon className="w-4 h-4" />
New Config
</button>
</Button>
</div>

<div
ref={scrollRef}
className="flex-1 overflow-y-auto overflow-x-hidden p-6"
>
{isLoading ? (
<LoaderBox message="Loading configurations..." size="md" />
<ConfigLibrarySkeleton columnCount={columnCount} />
) : error ? (
<div className="rounded-lg p-6 text-center bg-[#fef2f2] border border-[#fecaca]">
<WarningTriangleIcon className="w-12 h-12 mx-auto mb-3 text-[#dc2626]" />
<p className="text-sm font-medium text-status-error">{error}</p>
<div className="rounded-lg p-8 text-center bg-status-error-bg border border-status-error-border">
<WarningTriangleIcon className="w-12 h-12 mx-auto mb-3 text-status-error" />
<p className="text-sm font-medium text-status-error-text">
{error}
</p>
</div>
) : configs.length === 0 ? (
<div
className="rounded-lg p-8 text-center"
style={{
backgroundColor: colors.bg.primary,
border: `2px dashed ${colors.border}`,
}}
>
<div className="rounded-lg p-12 text-center bg-bg-primary border-2 border-dashed border-border">
{debouncedQuery ? (
<>
<SearchIcon
className="w-12 h-12 mx-auto mb-3"
style={{ color: colors.text.secondary }}
/>
<p
className="text-sm font-medium"
style={{ color: colors.text.primary }}
>
No configs match &quot;{debouncedQuery}&quot;
<div className="mx-auto mb-4 inline-flex items-center justify-center w-14 h-14 rounded-full bg-accent-primary/10">
<SearchIcon className="w-7 h-7 text-accent-primary" />
</div>
<p className="text-base font-semibold text-text-primary mb-1">
No configs match &ldquo;{debouncedQuery}&rdquo;
</p>
<button
onClick={() => setSearchInput("")}
className="mt-2 text-sm underline"
style={{ color: colors.text.secondary }}
className="mt-2 text-sm text-accent-primary hover:underline cursor-pointer"
>
Clear search
</button>
</>
) : (
<>
<GearIcon
className="w-12 h-12 mx-auto mb-3"
style={{ color: colors.text.secondary }}
/>
<p
className="text-sm font-medium"
style={{ color: colors.text.primary }}
>
<div className="mx-auto mb-4 inline-flex items-center justify-center w-14 h-14 rounded-full bg-accent-primary/10">
<GearIcon className="w-7 h-7 text-accent-primary" />
</div>
<p className="text-base font-semibold text-text-primary mb-1">
No configurations yet
</p>
<p
className="text-sm mt-1"
style={{ color: colors.text.secondary }}
>
Create your first configuration to get started
<p className="text-sm text-text-secondary mb-5">
Create your first configuration to start building prompts
and model setups.
</p>
<button
<Button
variant="primary"
size="md"
onClick={handleCreateNew}
className="mt-4 px-4 py-2 rounded-md text-sm font-medium transition-colors"
style={{
backgroundColor: colors.accent.primary,
color: colors.bg.primary,
}}
>
Create Config
</button>
<PlusIcon className="w-4 h-4" />
Create Configuration
</Button>
</>
)}
</div>
Expand Down
Loading
Loading