Skip to content

Feature/assesment#122

Open
vprashrex wants to merge 3 commits intomainfrom
feature/assesment
Open

Feature/assesment#122
vprashrex wants to merge 3 commits intomainfrom
feature/assesment

Conversation

@vprashrex
Copy link
Copy Markdown
Collaborator

@vprashrex vprashrex commented Apr 21, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added comprehensive assessment workflow enabling users to create datasets, map columns, configure prompts, select model configurations, and review results in a guided multi-step interface.
    • Added real-time evaluation status tracking with event streaming for assessment runs.
    • Added configuration management system for defining model parameters and output schemas.
    • Added feature flag system for controlled feature rollout.
  • Dependencies

    • Added cockatiel, xlsx, and zustand libraries to support assessment features.

…luation

- Added AssessmentPage component with nested steps for dataset selection, column mapping, prompt editing, configuration, output schema, and review.
- Integrated Zustand for state management of assessment dataset.
- Created utility functions for schema conversion and event handling.
- Enhanced Sidebar component to include a link to the new Assessment page.
- Added necessary types for assessment evaluation and schema properties.
- Implemented polling for evaluation status and indicators for processing, success, and failure.
- Included loading states and error handling for API interactions.
- Updated package dependencies to include zustand and xlsx.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

A comprehensive Assessment feature is introduced, encompassing API proxy routes for backend communication, a multi-step UI workflow for dataset upload/column mapping/prompt configuration, configuration management with provider-agnostic model support, feature-flag infrastructure for conditional access, and evaluation result tracking with event streaming and polling.

Changes

Cohort / File(s) Summary
Assessment API Routes
app/api/assessment/assessments/route.ts, app/api/assessment/assessments/[assessment_id]/(results|retry)/route.ts, app/api/assessment/evaluations/route.ts, app/api/assessment/evaluations/[evaluation_id]/(results|retry)/route.ts, app/api/assessment/datasets/route.ts, app/api/assessment/datasets/[dataset_id]/route.ts, app/api/assessment/events/route.ts
New Next.js proxy handlers for assessment CRUD, dataset operations, evaluation management, and Server-Sent Events streaming; all validate X-API-KEY headers and forward requests to backend with configurable base URL.
Assessment Page & Main Workflow
app/assessment/page.tsx, app/assessment/AssessmentPageClient.tsx
New server-rendered assessment page with feature-flag gating, and client component orchestrating multi-step workflow (Datasets, Config, Results tabs) with state management, API polling, and event-driven status updates.
Assessment UI Components
app/assessment/components/(DatasetStep|ColumnMapperStep|PromptEditorStep|PromptAndConfigStep|MultiConfigStep|OutputSchemaStep|ReviewStep|Stepper|EvaluationsTab|DataViewModal|JsonEditor).tsx
Modular UI components for each assessment workflow step: dataset selection/upload, column role mapping, prompt template editing with mention autocompletion, config selection/creation, output schema visual/JSON editing, results preview/export, and evaluation status display with retry support.
Assessment Config Management
app/assessment/config/(ConfigurationStep|api.ts|constants.ts), app/assessment/config/ConfigurationStep.tsx
Configuration UI and client-side API utilities for fetching/creating model configurations; constants define provider/model options and defaults; ConfigurationStep integrates config selection and output schema editing.
Assessment State & Utilities
app/assessment/(types.ts|store.ts|schemaUtils.ts|useAssessmentEvents.ts)
Type definitions for assessment forms/requests, Zustand store for dataset state persistence, JSON Schema conversion utilities, and SSE hook with circuit-breaker resilience for event streaming.
Feature Flag System
app/lib/FeatureFlagProvider.tsx, app/lib/featureFlags.server.ts, app/lib/constants/featureFlags.ts
New feature-flag infrastructure with provider/context hook for client-side flag management, server-side fetching with localStorage caching, and flag constants.
Type & Config Definitions
app/lib/configTypes.ts, app/lib/types/datasets.ts, app/lib/types/nav.ts
New TypeScript interfaces for config management API payloads, dataset structure, and navigation items with optional feature-flag gating.
Authentication & Navigation
app/lib/context/AuthContext.tsx, app/lib/constants/keystore.ts, app/lib/navConfig.ts, app/components/Sidebar.tsx, app/components/icons/(sidebar/AssessmentIcon.tsx|index.tsx)
Moved STORAGE_KEY constant to centralized location, added kaapi-auth-changed event dispatch, updated navigation config with Assessment item and feature-flag support, enhanced sidebar with feature-flag awareness and new Assessment icon.
Layout & Infrastructure
app/layout.tsx, app/components/FeatureRouteGuard.tsx
Converted RootLayout to async server component fetching initial feature flags for provider initialization; added FeatureRouteGuard for conditional route access.
Config & Styling
app/api/configs/(route|[config_id]/versions/route).ts, app/lib/colors.ts
Minor route handler updates to preserve/forward query parameters in config endpoints, added white text color constant.
Dependencies
package.json
Added cockatiel (3.2.1) for circuit-breaker resilience, xlsx (0.18.5) for spreadsheet parsing, zustand (5.0.12) for state management.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Client as AssessmentPageClient
    participant API as API Routes
    participant Backend as Backend Service
    participant Storage as localStorage/SSE

    User->>Client: 1. Select Dataset & Upload
    Client->>API: POST /api/assessment/datasets
    API->>Backend: POST /api/v1/assessment/datasets
    Backend-->>API: Dataset Created
    API-->>Client: Dataset ID + Metadata
    Client->>Storage: Cache Column Mapping

    User->>Client: 2. Configure Prompt & Select Config
    Client->>API: GET /api/assessment/evaluations
    API->>Backend: GET /api/v1/assessment/evaluations
    Backend-->>API: Evaluation Metadata
    API-->>Client: Config Options

    User->>Client: 3. Review & Submit
    Client->>API: POST /api/assessment/evaluations
    API->>Backend: POST /api/v1/assessment/evaluations
    Backend-->>API: Evaluation ID (processing)
    API-->>Client: Success Response

    Client->>API: GET /api/assessment/assessments?limit=10
    API->>Backend: GET /api/v1/assessment/assessments
    Backend-->>API: Assessment List
    API-->>Client: Polling Data

    Client->>API: EventStream: GET /api/assessment/events
    API->>Backend: GET /api/v1/assessment/events (SSE)
    Backend-->>API: Streaming Events
    API-->>Client: Status Updates (processing→completed)
    
    Client->>API: GET /api/assessment/evaluations/:id/results
    API->>Backend: GET /api/v1/assessment/evaluations/:id/results
    Backend-->>API: Results (JSON/CSV/XLSX)
    API-->>Client: Results for Preview/Export
    
    Client->>User: Display Results & Metrics
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • PR #69 — Implements dataset API proxy routes with signed-URL downloads and file content embedding, directly analogous to the dataset endpoint additions in this PR.
  • PR #4 — Adds/modifies config type definitions (configTypes.ts) and config-related API endpoints that share structural patterns with this PR's configuration management layer.
  • PR #89 — Refactors API-key state management and introduces centralized AuthProvider/AppProvider, which integrates with the feature-flag and auth-event dispatch added here.

Suggested labels

enhancement, ready-for-review

Suggested reviewers

  • Prajna1999
  • AkhileshNegi

Poem

🐰 A hop, a skip, assessments configured with glee,
Datasets uploaded, columns mapped for all to see,
Prompts crafted with placeholders, configs in flight,
Events stream results through the SSE night,
Feature flags guard the sacred Assessment path—
Evaluation awaits! ✨📊

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Feature/assesment' is vague and generic, using a branch name pattern rather than describing the actual changes made in this comprehensive pull request. Replace with a descriptive title summarizing the main feature, such as 'Add assessment workflow with datasets, configurations, and evaluations' or 'Implement multi-step assessment evaluation system'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/assesment

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: 2

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (1)
app/lib/context/AuthContext.tsx (1)

126-139: ⚠️ Potential issue | 🟠 Major

Notify feature-flag listeners during logout too.

Line 138 clears apiKeys directly, bypassing persist, so the new kaapi-auth-changed event from Line 98 is not emitted on logout. That can leave same-tab feature-gated UI using stale flags after auth is cleared.

🐛 Proposed fix
     setSession(null);
     setCurrentUser(null);
     clearAllStorage();
-    setApiKeys([]);
+    persist([]);
   }, [persist]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/context/AuthContext.tsx` around lines 126 - 139, The logout function
currently clears apiKeys by calling setApiKeys([]) which bypasses the persist
hook and prevents the "kaapi-auth-changed" feature-flag event from firing;
change the logout flow in the logout callback to clear api keys via the existing
persist mechanism (use the same persist call used elsewhere to update/clear
"apiKeys" so it emits the kaapi-auth-changed event) instead of calling
setApiKeys directly, keeping the rest of the cleanup (setSession(null),
setCurrentUser(null), clearAllStorage()) intact and ensuring persist remains in
the dependency array.
🟠 Major comments (26)
app/components/speech-to-text/ModelComparisonCard.tsx-149-172 (1)

149-172: ⚠️ Potential issue | 🟠 Major

Add type="button" to non-submitting buttons and ensure the expand toggle is accessible.

Both buttons lack explicit type="button" attributes, causing them to default to type="submit" per HTML spec, which can accidentally trigger form submission if the component is used within a form. Additionally, the icon-only expand button needs an accessible name (aria-label) and expanded state indicator (aria-expanded), and both SVG icons should use aria-hidden="true".

Suggested changes
             {hasExpandedContent && (
               <button
+                type="button"
                 onClick={handleExpandToggle}
+                aria-expanded={isExpanded}
+                aria-label={
+                  isExpanded ? "Collapse model details" : "Expand model details"
+                }
                 className="p-1 rounded hover:bg-gray-100"
                 style={{ color: colors.text.secondary }}
               >
                 <svg
                   className="w-4 h-4"
+                  aria-hidden="true"
                   fill="none"
                   viewBox="0 0 24 24"
                   stroke="currentColor"
           {onClick && (
             <button
+              type="button"
               onClick={onClick}
               className="w-full py-1.5 rounded text-xs font-medium flex items-center justify-center gap-1"
               <svg
                 className="w-3 h-3"
+                aria-hidden="true"
                 fill="none"
                 viewBox="0 0 24 24"
                 stroke="currentColor"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/speech-to-text/ModelComparisonCard.tsx` around lines 149 -
172, The expand toggle currently defined inside the hasExpandedContent
conditional should be made non-submitting and accessible: add type="button" to
the button element that calls handleExpandToggle, add an accessible name (e.g.,
aria-label="Toggle details" or similar) and an aria-expanded attribute bound to
isExpanded, and mark the SVG icon as presentational with aria-hidden="true";
update any other nearby buttons lacking type attributes the same way to prevent
accidental form submission.
app/api/assessment/evaluations/route.ts-8-88 (1)

8-88: ⚠️ Potential issue | 🟠 Major

Use apiClient so auth relay and response status stay consistent.

These handlers manually enforce X-API-KEY, bypassing the shared cookie/header forwarding path, and they return 200 for every successful backend response. For evaluation submission, preserving backend statuses like 201 or 202 is important for clients tracking async work.

♻️ Proposed direction
 import { NextRequest, NextResponse } from 'next/server';
+import { apiClient } from "@/app/lib/apiClient";
 
 export async function GET(request: NextRequest) {
   try {
-    const apiKey = request.headers.get('X-API-KEY');
-
-    if (!apiKey) {
-      return NextResponse.json(
-        { error: 'Missing X-API-KEY header' },
-        { status: 401 }
-      );
-    }
-
-    const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000';
     const searchParams = request.nextUrl.searchParams.toString();
     const queryString = searchParams ? `?${searchParams}` : '';
 
-    const response = await fetch(`${backendUrl}/api/v1/assessment/evaluations${queryString}`, {
-      method: 'GET',
-      headers: {
-        'X-API-KEY': apiKey,
-      },
+    const { status, data } = await apiClient(
+      request,
+      `/api/v1/assessment/evaluations${queryString}`,
+      {
+        method: "GET",
+      },
+    );
-    });
-
-    const data = await response.json();
-
-    if (!response.ok) {
-      return NextResponse.json(data, { status: response.status });
-    }
-
-    return NextResponse.json(data, { status: 200 });
+    return NextResponse.json(data, { status });

Apply the same pattern in POST, passing the JSON body through apiClient and returning its status.

As per coding guidelines, app/api/**/*.{ts,tsx}: Use apiClient(request, endpoint, options) in server-side Next.js route handlers to forward requests to the backend, which automatically relays X-API-KEY and Cookie headers and returns { status, data, headers }.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/assessment/evaluations/route.ts` around lines 8 - 88, The GET and
POST handlers in route.ts manually enforce X-API-KEY and call fetch directly,
bypassing the shared header/cookie relay and always returning 200; replace those
fetch flows with apiClient(request, endpoint, options) so auth/cookie forwarding
is preserved and actual backend statuses are returned: for GET call
apiClient(request,
`/api/v1/assessment/evaluations${request.nextUrl.searchParams.toString() ?
`?${request.nextUrl.searchParams.toString()}` : ''}`, { method: 'GET' }) and
return NextResponse.json(response.data, { status: response.status, headers:
response.headers }), and for POST call apiClient(request,
'/api/v1/assessment/evaluations', { method: 'POST', body: await request.json()
}) and return NextResponse.json(response.data, { status: response.status,
headers: response.headers }); remove the manual X-API-KEY checks and
console.error proxy messages in GET and POST so the handlers rely on apiClient
behavior.
app/api/features/route.ts-6-32 (1)

6-32: 🛠️ Refactor suggestion | 🟠 Major

Use the shared server proxy client here.

This route manually reconstructs auth forwarding instead of using the project’s apiClient, so it can drift from the shared X-API-KEY/Cookie relay and error-handling behavior used by other server routes.

♻️ Proposed direction
-import { cookies } from "next/headers";
 import { NextRequest, NextResponse } from "next/server";
+import { apiClient } from "@/app/lib/apiClient";
 
 export const dynamic = "force-dynamic";
 
 export async function GET(request: NextRequest) {
   try {
-    const cookieStore = await cookies();
-    const apiKey =
-      request.headers.get("X-API-KEY") ??
-      cookieStore.get("kaapi_api_key")?.value;
-    if (!apiKey) {
-      return NextResponse.json(
-        { error: "Missing X-API-KEY header" },
-        { status: 401 },
-      );
-    }
-
-    const backendUrl = process.env.BACKEND_URL || "http://localhost:8000";
-    const response = await fetch(`${backendUrl}/api/v1/features`, {
+    const { status, data } = await apiClient(request, "/api/v1/features", {
       method: "GET",
-      headers: { "X-API-KEY": decodeURIComponent(apiKey) },
       cache: "no-store",
     });
 
-    const data = await response.json();
     return NextResponse.json(data, {
-      status: response.status,
+      status,
       headers: {
         "Cache-Control": "no-store, no-cache, must-revalidate",
       },

As per coding guidelines, app/api/**/*.{ts,tsx}: Use apiClient(request, endpoint, options) in server-side Next.js route handlers to forward requests to the backend, which automatically relays X-API-KEY and Cookie headers and returns { status, data, headers }.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/features/route.ts` around lines 6 - 32, The GET handler currently
does its own cookie/header extraction and fetch; replace that logic to call the
shared apiClient(request, "/api/v1/features", { method: "GET", cache: "no-store"
}) so auth (X-API-KEY and Cookie) and error handling are forwarded consistently;
use the returned { status, data, headers } from apiClient to construct the
NextResponse (preserving Cache-Control from response.headers or setting
"no-store, no-cache, must-revalidate" as before) and remove manual cookies() and
decodeURIComponent(apiKey) handling in the GET function.
app/api/assessment/datasets/route.ts-8-87 (1)

8-87: ⚠️ Potential issue | 🟠 Major

Route through apiClient and preserve backend statuses.

Both handlers manually require X-API-KEY, so cookie-authenticated requests are rejected before the shared proxy layer can relay cookies. They also collapse every successful backend response to 200, which can hide 201 Created/202 Accepted from dataset uploads.

♻️ Proposed direction
 import { NextRequest, NextResponse } from 'next/server';
+import { apiClient } from "@/app/lib/apiClient";
 
 export async function GET(request: NextRequest) {
   try {
-    const apiKey = request.headers.get('X-API-KEY');
-
-    if (!apiKey) {
-      return NextResponse.json(
-        { error: 'Missing X-API-KEY header' },
-        { status: 401 }
-      );
-    }
-
-    const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000';
-
-    const response = await fetch(`${backendUrl}/api/v1/assessment/datasets`, {
-      method: 'GET',
-      headers: {
-        'X-API-KEY': apiKey,
-      },
+    const { status, data } = await apiClient(request, "/api/v1/assessment/datasets", {
+      method: "GET",
     });
-
-    const data = await response.json();
-
-    if (!response.ok) {
-      return NextResponse.json(data, { status: response.status });
-    }
-
-    return NextResponse.json(data, { status: 200 });
+    return NextResponse.json(data, { status });

Apply the same pattern in POST, passing the parsed formData as the request body and returning the status from apiClient.

As per coding guidelines, app/api/**/*.{ts,tsx}: Use apiClient(request, endpoint, options) in server-side Next.js route handlers to forward requests to the backend, which automatically relays X-API-KEY and Cookie headers and returns { status, data, headers }.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/assessment/datasets/route.ts` around lines 8 - 87, The GET and POST
handlers in route.ts should use the shared apiClient(request, endpoint, options)
so cookies and X-API-KEY are relayed and backend status/headers are preserved;
remove manual request.headers.get('X-API-KEY') checks, call apiClient(request,
'/api/v1/assessment/datasets', { method: 'GET' }) for GET and apiClient(request,
'/api/v1/assessment/datasets', { method: 'POST', body: formData }) for POST (use
await request.formData() before calling), then return NextResponse.json(data, {
status, headers }) using the status and headers returned by apiClient instead of
always mapping successes to 200.
app/assessment/components/DataViewModal.tsx-24-51 (1)

24-51: ⚠️ Potential issue | 🟠 Major

Add dialog semantics and keyboard-close support.

This modal has no role="dialog", aria-modal, labelled title, Escape handler, or accessible name on the icon-only close button. That makes the data preview hard to operate with assistive tech/keyboard-only flows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/assessment/components/DataViewModal.tsx` around lines 24 - 51, The modal
markup in DataViewModal.tsx is missing dialog semantics and keyboard support;
update the outer modal container element (the fixed inset-0 wrapper or the inner
dialog container used for rendering) to include role="dialog" and
aria-modal="true", give the title element (the h3 that renders {title}) a stable
id (e.g., dataViewModalTitle) and add aria-labelledby referencing that id on the
dialog container, add an accessible label to the icon-only close button (e.g.,
aria-label="Close preview") instead of relying on color-only styling, and wire
an Escape key handler (onKeyDown on the container or a useEffect keyboard
listener) that calls the existing onClose function so keyboard users can close
with Esc. Ensure the stopPropagation click handler remains on the inner
container so clicks inside do not close the dialog.
app/api/assessment/assessments/[assessment_id]/results/route.ts-13-29 (1)

13-29: ⚠️ Potential issue | 🟠 Major

Use the shared proxy path for this backend call.

This route bypasses apiClient and forwards only X-API-KEY, so backend calls won’t receive cookies that the shared route-handler proxy normally relays. If binary passthrough is the blocker, centralize that support in apiClient instead of duplicating proxy logic.

As per coding guidelines, app/api/**/*.{ts,tsx} routes should use apiClient(request, endpoint, options) to forward backend requests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/assessment/assessments/`[assessment_id]/results/route.ts around lines
13 - 29, Replace the direct fetch in route.ts with the central apiClient call:
instead of building backendUrl and calling fetch with only 'X-API-KEY', call
apiClient(request,
`/api/v1/assessment/assessments/${assessment_id}/results${queryString}`, {
method: 'GET', headers: { 'X-API-KEY': apiKey } }) so the shared proxy will
forward cookies and handle binary passthrough; if apiClient doesn't yet support
binary passthrough, add that support there and remove duplicate proxy logic from
this route, keeping references to request, assessment_id, apiClient, and
queryString to locate where to change.
app/api/assessment/evaluations/[evaluation_id]/results/route.ts-13-29 (1)

13-29: ⚠️ Potential issue | 🟠 Major

Keep backend forwarding behind apiClient.

This direct fetch path only forwards X-API-KEY and drops cookies that the shared proxy helper is supposed to relay. If downloads need raw-response handling, consider extending apiClient for passthrough responses rather than bypassing it per route.

As per coding guidelines, app/api/**/*.{ts,tsx} routes should use apiClient(request, endpoint, options) to forward backend requests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/assessment/evaluations/`[evaluation_id]/results/route.ts around lines
13 - 29, This route is directly calling fetch and only forwards the X-API-KEY
header, dropping cookies and bypassing the shared proxy; replace the direct
fetch with the shared apiClient(request, endpoint, options) call so cookies and
other proxy logic are preserved (use apiClient(request,
`/api/v1/assessment/evaluations/${evaluation_id}/results${queryString}`, {
method: 'GET', headers: { 'X-API-KEY': apiKey } })). If you need raw-response
handling for downloads, extend apiClient to support a passthrough/raw response
mode and call that new option from this handler instead of bypassing apiClient.
app/api/assessment/assessments/[assessment_id]/retry/route.ts-7-32 (1)

7-32: ⚠️ Potential issue | 🟠 Major

Use the shared apiClient proxy here.

This route bypasses the server-side proxy helper and only forwards X-API-KEY, so any backend auth/session behavior that depends on forwarded cookies will be lost. Prefer the shared helper so header forwarding and response handling stay consistent.

Proposed direction
 import { NextRequest, NextResponse } from 'next/server';
+import { apiClient } from '@/app/lib/apiClient';

@@
-    const apiKey = request.headers.get('X-API-KEY');
-
-    if (!apiKey) {
-      return NextResponse.json(
-        { error: 'Missing X-API-KEY header' },
-        { status: 401 }
-      );
-    }
-
     const { assessment_id } = await context.params;
-    const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000';
-
-    const response = await fetch(
-      `${backendUrl}/api/v1/assessment/assessments/${assessment_id}/retry`,
-      {
-        method: 'POST',
-        headers: {
-          'X-API-KEY': apiKey,
-        },
-      }
-    );
-
-    const data = await response.json();
-    return NextResponse.json(data, { status: response.status });
+    const { status, data } = await apiClient(
+      request,
+      `/api/v1/assessment/assessments/${assessment_id}/retry`,
+      { method: 'POST' },
+    );
+
+    return NextResponse.json(data, { status });

As per coding guidelines, app/api/**/*.{ts,tsx} routes should use apiClient(request, endpoint, options) to forward backend requests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/assessment/assessments/`[assessment_id]/retry/route.ts around lines 7
- 32, The POST handler is calling fetch directly and bypasses the shared proxy;
update the POST function to call the existing apiClient(request, endpoint,
options) instead of fetch so cookies and forwarded headers are preserved. Build
the endpoint as `/api/v1/assessment/assessments/${assessment_id}/retry`, call
apiClient(request, endpoint, { method: 'POST', headers: { 'X-API-KEY': apiKey }
}) (or omit explicit headers if apiClient already forwards them), await the
response JSON and return it with NextResponse.json(data, { status:
response.status }); ensure you reference the POST function and the assessment_id
param when making the change.
app/assessment/schemaUtils.ts-19-20 (1)

19-20: ⚠️ Potential issue | 🟠 Major

Do not emit empty enum schemas.

If all enum values are blank, this produces enum: [], which makes the field impossible to satisfy. Trim/filter values first and skip or downgrade invalid enum properties instead.

Proposed fix
     } else if (p.type === 'enum') {
-      def = { type: 'string', enum: p.enumValues.filter(v => v.trim()) };
+      const enumValues = p.enumValues.map((v) => v.trim()).filter(Boolean);
+      if (enumValues.length === 0) return;
+      def = { type: 'string', enum: enumValues };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/assessment/schemaUtils.ts` around lines 19 - 20, When handling p.type ===
'enum' in schemaUtils.ts, trim and filter p.enumValues first (e.g., const vals =
p.enumValues.map(v => v.trim()).filter(Boolean)); if vals is non-empty set def =
{ type: 'string', enum: vals } otherwise do not emit an empty enum—fallback to a
plain string type (def = { type: 'string' }) or omit the enum constraint for
that property so you avoid producing enum: [] for property p.
package.json-25-25 (1)

25-25: ⚠️ Potential issue | 🟠 Major

Avoid adding vulnerable xlsx@0.18.5 for parsing user-uploaded files.

Version 0.18.5 is affected by two high-severity advisories: GHSA-4r6h-8v6p-xvw6 (Prototype Pollution) and GHSA-5pgg-2g8v-p4x9 (Regular Expression Denial of Service). Since DatasetStep parses user-provided spreadsheet content with this package, this introduces unnecessary supply-chain and input-processing risk. Use a maintained parser, implement server-side constraints on parsing, or choose a non-vulnerable distribution strategy before accepting arbitrary uploads.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 25, Your package currently depends on a vulnerable xlsx
version (xlsx@0.18.5) which is used by DatasetStep to parse user uploads; remove
or replace this dependency with a non-vulnerable, actively maintained parser (or
upgrade to a patched release) and update usages in DatasetStep to the new API.
Additionally, enforce server-side constraints when parsing untrusted
spreadsheets: validate MIME/type and extension before processing, enforce strict
file size and row/column limits, parse inside an isolated worker/process with
timeouts, and add input sanitization and error handling to DatasetStep to avoid
prototype pollution or ReDoS risks. Ensure tests cover the new parser and the
defensive checks.
app/api/assessment/assessments/[assessment_id]/results/route.ts-31-39 (1)

31-39: ⚠️ Potential issue | 🟠 Major

Stream file exports instead of materializing a Blob.

The misleading comment "stream the response through" contradicts the implementation: await response.blob() buffers the entire CSV/XLSX/ZIP payload in memory before responding. Use response.body directly instead to stream the response without buffering.

Same buffering issue exists in app/api/assessment/evaluations/[evaluation_id]/results/route.ts at line 33—both routes should be fixed.

Proposed streaming change
-      const blob = await response.blob();
       const headers = new Headers();
       headers.set('Content-Type', contentType);
       const disposition = response.headers.get('content-disposition');
       if (disposition) headers.set('Content-Disposition', disposition);
-      return new NextResponse(blob, { status: response.status, headers });
+      return new NextResponse(response.body, { status: response.status, headers });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/assessment/assessments/`[assessment_id]/results/route.ts around lines
31 - 39, The code currently buffers file exports by calling response.blob() in
the route handler (see the block that reads response.headers and calls await
response.blob()), which contradicts the "stream the response through" intent;
change the implementation to pipe the upstream response.body stream directly
into the NextResponse without materializing a Blob, e.g. use response.body as
the body for new NextResponse and copy relevant headers (Content-Type and
Content-Disposition) and status; apply the same change to the analogous handler
in app/api/assessment/evaluations/[evaluation_id]/results/route.ts where
response.blob() is used.
app/assessment/page.tsx-1-19 (1)

1-19: ⚠️ Potential issue | 🟠 Major

Move the Assessment route into the (main) route group.

This authenticated application route is currently at app/assessment/page.tsx; place it at app/(main)/assessment/page.tsx so it follows the repo's App Router grouping without changing the public /assessment URL.

As per coding guidelines, app/{(auth),(main)}/**/*.{ts,tsx} routes should be organized using Next.js App Router route groups: app/(auth)/ for unauthenticated flows and app/(main)/ for authenticated application routes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/assessment/page.tsx` around lines 1 - 19, The Assessment page route is in
the wrong route group; move the file that exports default async function
AssessmentPage (the component importing AssessmentPageClient, FeatureRouteGuard,
FeatureFlag, and getServerFeatureFlags) into the (main) route group so it lives
under the app/(main)/assessment route group while keeping the public /assessment
URL; after moving, verify and update any relative imports (e.g.,
AssessmentPageClient, FeatureRouteGuard, getServerFeatureFlags, FeatureFlag) so
they resolve from the new location and ensure the exported function signature
and behavior (checking initialFlags[FeatureFlag.ASSESSMENT] and calling
notFound()) remain unchanged.
app/api/assessment/evaluations/[evaluation_id]/retry/route.ts-7-42 (1)

7-42: 🛠️ Refactor suggestion | 🟠 Major

Use apiClient for this proxy route.

Same issue as app/api/assessment/assessments/route.ts: the handler reimplements header forwarding and JSON parsing that apiClient handles. Also address the Prettier formatting CI warning.

As per coding guidelines: "Use apiClient(request, endpoint, options) in server-side Next.js route handlers to forward requests to the backend, which automatically relays X-API-KEY and Cookie headers and returns { status, data, headers }".

♻️ Proposed refactor
-import { NextRequest, NextResponse } from 'next/server';
+import { NextRequest, NextResponse } from 'next/server';
+import { apiClient } from '@/app/lib/apiClient';
 
 interface RouteContext {
   params: Promise<{ evaluation_id: string }>;
 }
 
 export async function POST(request: NextRequest, context: RouteContext) {
   try {
-    const apiKey = request.headers.get('X-API-KEY');
-
-    if (!apiKey) {
-      return NextResponse.json(
-        { error: 'Missing X-API-KEY header' },
-        { status: 401 }
-      );
-    }
-
     const { evaluation_id } = await context.params;
-    const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000';
-
-    const response = await fetch(
-      `${backendUrl}/api/v1/assessment/evaluations/${evaluation_id}/retry`,
-      {
-        method: 'POST',
-        headers: {
-          'X-API-KEY': apiKey,
-        },
-      }
-    );
-
-    const data = await response.json();
-    return NextResponse.json(data, { status: response.status });
+    const { status, data } = await apiClient(
+      request,
+      `/api/v1/assessment/evaluations/${evaluation_id}/retry`,
+      { method: 'POST' }
+    );
+    return NextResponse.json(data, { status });
   } catch (error: unknown) {
     return NextResponse.json(
       {
         error: 'Failed to forward evaluation retry request',
         details: error instanceof Error ? error.message : String(error),
       },
       { status: 500 }
     );
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/assessment/evaluations/`[evaluation_id]/retry/route.ts around lines 7
- 42, The POST handler in route.ts reimplements header forwarding and JSON
parsing; replace the manual fetch logic with the shared apiClient utility: call
apiClient(request, `/api/v1/assessment/evaluations/${evaluation_id}/retry`, {
method: 'POST' }), then return NextResponse.json(response.data, { status:
response.status }) (or forward headers if needed) instead of manually extracting
X-API-KEY and parsing JSON; update the function POST and remove the custom
headers block and try/catch parsing duplication, and ensure the file formatting
matches Prettier (run formatter) to resolve the CI warning.
app/api/assessment/assessments/route.ts-3-32 (1)

3-32: 🛠️ Refactor suggestion | 🟠 Major

Use apiClient instead of raw fetch for backend forwarding.

Route handlers under app/api/** must use apiClient(request, endpoint, options) which automatically relays X-API-KEY and Cookie headers and returns { status, data, headers }. This eliminates manual key validation, query reconstruction, and JSON parsing boilerplate. Also run npx prettier --write on this file to resolve CI warnings.

♻️ Proposed refactor
-import { NextRequest, NextResponse } from 'next/server';
-
-export async function GET(request: NextRequest) {
-  try {
-    const apiKey = request.headers.get('X-API-KEY');
-
-    if (!apiKey) {
-      return NextResponse.json(
-        { error: 'Missing X-API-KEY header' },
-        { status: 401 }
-      );
-    }
-
-    const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000';
-    const searchParams = request.nextUrl.searchParams.toString();
-    const queryString = searchParams ? `?${searchParams}` : '';
-
-    const response = await fetch(`${backendUrl}/api/v1/assessment/assessments${queryString}`, {
-      method: 'GET',
-      headers: {
-        'X-API-KEY': apiKey,
-      },
-    });
-
-    const data = await response.json();
-    return NextResponse.json(data, { status: response.status });
-  } catch (error: unknown) {
-    return NextResponse.json(
-      { error: 'Failed to forward request to backend', details: error instanceof Error ? error.message : String(error) },
-      { status: 500 }
-    );
-  }
-}
+import { NextRequest, NextResponse } from 'next/server';
+import { apiClient } from '@/app/lib/apiClient';
+
+export async function GET(request: NextRequest) {
+  try {
+    const qs = request.nextUrl.searchParams.toString();
+    const endpoint = `/api/v1/assessment/assessments${qs ? `?${qs}` : ''}`;
+    const { status, data } = await apiClient(request, endpoint, { method: 'GET' });
+    return NextResponse.json(data, { status });
+  } catch (error: unknown) {
+    return NextResponse.json(
+      {
+        error: 'Failed to forward request to backend',
+        details: error instanceof Error ? error.message : String(error),
+      },
+      { status: 500 }
+    );
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/assessment/assessments/route.ts` around lines 3 - 32, Replace the
manual fetch flow inside the exported GET function: remove the manual X-API-KEY
header check, backendUrl/queryString construction, and raw fetch/response.json
logic, and instead call apiClient(request, '/api/v1/assessment/assessments' +
request.nextUrl.search, { method: 'GET' }) (or appropriate options), then return
NextResponse.json(result.data, { status: result.status, headers: result.headers
}) using the returned { status, data, headers } from apiClient; also run npx
prettier --write on this file to fix CI formatting warnings.
app/lib/FeatureFlagProvider.tsx-38-84 (1)

38-84: ⚠️ Potential issue | 🟠 Major

SSR-provided flags can be wiped on first client render.

getServerFeatureFlags() resolves flags from the kaapi_api_key cookie, while fetchFlags here gates on the STORAGE_KEY localStorage entry. These two auth sources can disagree:

  • A signed-in user whose API key only lives in the HttpOnly cookie (or whose localStorage hasn’t populated yet at first render) will hit lines 57–61, setting flags to {} and overwriting the server snapshot that initialFlags just seeded on line 39.
  • Consumers like FeatureRouteGuard / Sidebar that gate on isEnabled will briefly (or persistently) show “feature disabled” even though the server already determined it was enabled.

Consider either (a) skipping the localStorage gate when hasServerSnapshot is true and trusting the server until a real fetch returns a new answer, or (b) attempting the /api/features call unconditionally (the route already returns 401 when unauthenticated, which you can treat as “no change”). This preserves the SSR→CSR continuity the initialFlags prop was designed for.

app/api/assessment/datasets/[dataset_id]/route.ts-42-54 (1)

42-54: ⚠️ Potential issue | 🟠 Major

S3 download has no timeout and buffers the full file in memory.

Two reliability/scalability concerns in the fetch_content=true branch:

  1. No timeout on line 47. If the signed URL hangs (slow S3 region, network partition, dead DNS), this fetch has no AbortSignal.timeout(...) and will tie up the serverless/Node handler indefinitely. On platforms with concurrency-per-instance caps, a handful of such requests can exhaust the pool.
  2. Whole-file base64 in memory on line 51–52. arrayBuffer() + Buffer.from(...).toString('base64') allocates the full file plus ~1.33× for the base64 copy. For even moderately large assessment datasets (tens of MB), this is a large transient allocation on every call and the response body is then serialized through NextResponse.json. Given DatasetStep.tsx just base64-decodes it back to parse with XLSX, a much better contract would be to return the signed_url to the client and let the browser download + parse directly, or to stream the bytes through as application/octet-stream.
🛡️ Minimal fix (timeout), plus streaming suggestion
-      const fileResponse = await fetch(signedUrl);
+      const fileResponse = await fetch(signedUrl, {
+        signal: AbortSignal.timeout(30_000),
+      });

Longer term, consider returning the signed URL directly (client-side fetch already works for presigned S3 URLs without CORS issues if the bucket is configured) or streaming via new Response(fileResponse.body, { headers: { 'content-type': ... } }) instead of buffering + base64.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/assessment/datasets/`[dataset_id]/route.ts around lines 42 - 54, The
fetch_content=true branch currently does an untimeouted fetch(signedUrl) and
buffers the entire file (arrayBuffer + Buffer.from(...).toString('base64')),
which can hang handlers and OOM; change the logic in the fetchContent block to
(1) use an AbortController with a reasonable timeout (e.g., 10s) and pass
controller.signal to fetch(signedUrl) to avoid hanging requests, and (2) stop
base64-buffering large files on the server — instead return the signedUrl back
to the client (or stream the fileResponse.body with new Response(...) and
appropriate content-type) via NextResponse.json({ ...data, signed_url: signedUrl
}) so DatasetStep.tsx can fetch/parse in the browser rather than using
arrayBuffer/Buffer.from on the server. Ensure you update references to
signedUrl, fileResponse, and remove the arrayBuffer/Buffer.from usage in that
branch.
app/api/assessment/datasets/[dataset_id]/route.ts-10-64 (1)

10-64: ⚠️ Potential issue | 🟠 Major

Use apiClient to forward the backend request per coding guidelines.

This route uses raw fetch() to call the backend instead of the required apiClient(request, endpoint, options). The coding guideline for app/api/** routes states:

"Use apiClient(request, endpoint, options) in server-side Next.js route handlers to forward requests to the backend, which automatically relays X-API-KEY and Cookie headers and returns { status, data, headers }"

Currently this route:

  • Only reads X-API-KEY from the request headers (line 15), missing the cookie fallback that apiClient provides — callers using cookie-based auth will 401
  • Mirrors the entire upstream response body on error (line 38) instead of extracting a normalized message using the pattern body.error || body.message || body.detail
  • Re-implements query-string passthrough manually

Additionally:

  • The S3 signed URL fetch (line 47) has no timeout, risking indefinite thread pins if the connection hangs
  • Loading the entire file into memory as an ArrayBuffer and base64-encoding it (lines 51–53) causes memory spikes and ~33% size overhead, which can exhaust memory on larger datasets or constrained runtimes

Refactor to use apiClient() and consider returning the signed URL to the client instead of fetching the file server-side.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/assessment/datasets/`[dataset_id]/route.ts around lines 10 - 64, The
GET route handler currently calls fetch() directly inside the exported GET
function and manually parses query params, reads only X-API-KEY, mirrors
upstream bodies on error, and downloads the signed S3 file into memory; replace
that call with apiClient(request, `/assessment/datasets/${dataset_id}`, {
method: 'GET', params: <forwarded searchParams with include_signed_url when
fetch_content> }) so headers/cookies are relayed automatically, stop manual
URL/query-string handling, and use the apiClient response shape ({ status, data,
headers }) to return normalized errors (use data.error || data.message ||
data.detail) instead of mirroring the backend body; do not fetch the signed URL
server-side in GET (remove the fileResponse/arrayBuffer/base64 logic) — return
the signed_url to the client or, if you must fetch server-side, implement a
bounded timeout and stream-to-client approach instead of Buffer.from to avoid
memory spikes.
app/assessment/components/DatasetStep.tsx-188-204 (1)

188-204: ⚠️ Potential issue | 🟠 Major

Clear stale columns when dataset parsing fails.

setDatasetId(id) runs before fetchAndParseFile() succeeds. If parsing fails or returns no headers, the selected dataset ID can remain paired with columns/sample data from the previous dataset, and Line 285 can enable “Next” with invalid mapping context.

Track the parsed dataset ID, reset columns on failure, and gate Next on successful parsing.

🐛 Suggested direction
+const [parsedDatasetId, setParsedDatasetId] = useState("");

 const handleDatasetSelect = async (id: string) => {
   setDatasetId(id);
+  setParsedDatasetId("");
   if (!id) return;

   setIsLoadingColumns(true);
   try {
     const parsed = await fetchAndParseFile(id);
-    if (parsed?.headers) {
+    if (parsed?.headers?.length) {
       const firstRow = parsed.rows[0] || [];
       const sampleRow = Object.fromEntries(
         parsed.headers.map((header, index) => [
           header,
           String(firstRow[index] ?? ""),
         ]),
       );
       onColumnsLoaded(parsed.headers, sampleRow);
+      setParsedDatasetId(id);
+    } else {
+      onColumnsLoaded([], {});
+      toast.error("Could not read columns from this dataset");
     }
   } catch (e) {
     console.error("Failed to fetch dataset columns:", e);
+    onColumnsLoaded([], {});
   } finally {
     setIsLoadingColumns(false);
   }
 };

-const canProceed = datasetId && !isLoadingColumns;
+const canProceed = Boolean(datasetId) && parsedDatasetId === datasetId && !isLoadingColumns;

Also applies to: 285-285

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/assessment/components/DatasetStep.tsx` around lines 188 - 204,
handleDatasetSelect currently sets setDatasetId(id) before fetchAndParseFile
succeeds, so if parsing fails or returns no headers the UI can keep stale
columns; modify handleDatasetSelect to clear/reset columns/sample data (call
onColumnsLoaded with empty arrays/object or a dedicated reset handler) and clear
any parsedDatasetId state before calling fetchAndParseFile, then only call
setDatasetId and onColumnsLoaded with real headers/sampleRow after a successful
parse; ensure setIsLoadingColumns is used around the async call and gate the
"Next" button (the logic that checks parsedDatasetId or columns) to require a
successful parse (use a parsedDatasetId or a boolean like isColumnsLoaded) so
Next is disabled unless parsing produced headers.
app/assessment/AssessmentPageClient.tsx-95-144 (1)

95-144: 🛠️ Refactor suggestion | 🟠 Major

Use clientFetch() for assessment API requests.

The polling and submit requests are made with raw fetch(), bypassing centralized 401 refresh handling and auth-expiry dispatch.

♻️ Suggested pattern
-const response = await fetch("/api/assessment/assessments?limit=10", {
+const response = await clientFetch("/api/assessment/assessments?limit=10", {
   headers: { "X-API-KEY": selectedKey.key },
 });

As per coding guidelines, **/*.{ts,tsx} should 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.

Also applies to: 190-224

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/assessment/AssessmentPageClient.tsx` around lines 95 - 144, Replace raw
fetch calls in the client-side assessment API logic with the centralized helper
clientFetch to ensure token refresh/401 handling; specifically update the
pollEvalStatus function (and the related submit function referenced around lines
190-224) to call clientFetch("/api/assessment/assessments?limit=10", { headers:
{ "X-API-KEY": selectedKey.key }, /* include same options as before */ })
instead of fetch, then preserve the existing response.ok check, JSON parsing,
and subsequent logic (runs detection, hasProcessing check, setEvalIndicator
branches, and dismissedRef usage). Ensure you import/usage-reference clientFetch
where pollEvalStatus and the submit handler are defined and keep the same error
handling behavior (catch block) semantics.
app/assessment/components/PromptAndConfigStep.tsx-512-537 (1)

512-537: ⚠️ Potential issue | 🟠 Major

Check MAX_CONFIGS before saving a new behavior.

When the selection is already full, this still persists a new config, addSelection() rejects it, and the UI then resets + shows “saved and added”. Add the cap check before saveAssessmentConfig() and only show success after the selection is actually added.

🐛 Suggested guard
 const handleCreateAndAdd = async () => {
   if (!configName.trim()) {
     toast.error("Configuration name is required");
     return;
   }
+  if (configs.length >= MAX_CONFIGS) {
+    toast.error(`You can select up to ${MAX_CONFIGS} configurations`);
+    return;
+  }
   setIsSaving(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/assessment/components/PromptAndConfigStep.tsx` around lines 512 - 537,
Before calling saveAssessmentConfig in handleCreateAndAdd, check the current
selection count against MAX_CONFIGS (use whatever selection state or selector
the component uses) and abort with toast.error if full; only proceed to save
after confirming there is room. Move the persistence and UI-reset logic
(saveAssessmentConfig, invalidateAssessmentConfigCache,
setDraft(buildInitialDraft()), setConfigName(""), setCommitMessage(""),
setConfigMode("existing"), and toast.success) to occur after addSelection
succeeds, and handle addSelection failure by showing an error and not resetting
the form. Ensure you still call loadConfigs(0, true) after successful
addSelection and keep references to addSelection, saveAssessmentConfig,
handleCreateAndAdd, and MAX_CONFIGS when updating the flow.
app/assessment/config/ConfigurationStep.tsx-217-438 (1)

217-438: ⚠️ Potential issue | 🟠 Major

Avoid stale selection state after async config loads.

toggleVersionSelection() awaits fetchConfigSelection(), then addSelection() writes using the configs array captured before the request. Parallel selections can overwrite each other or apply max/duplicate checks against stale state.

Use a functional setter type for setConfigs, or a latest-configs ref, so async completions merge with the current selection list.

🐛 Suggested direction
 interface ConfigurationStepProps {
   configs: ConfigSelection[];
-  setConfigs: (configs: ConfigSelection[]) => void;
+  setConfigs: React.Dispatch<React.SetStateAction<ConfigSelection[]>>;
 }

 const removeSelection = useCallback(
   (configId: string, version: number) => {
-    setConfigs(
-      configs.filter(
+    setConfigs((prev) =>
+      prev.filter(
         (item) =>
           !(item.config_id === configId && item.config_version === version),
       ),
     );
   },
-  [configs, setConfigs],
+  [setConfigs],
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/assessment/config/ConfigurationStep.tsx` around lines 217 - 438, The
async path in toggleVersionSelection leads to stale writes because addSelection
uses the captured configs array; change addSelection to use the functional state
updater form (call setConfigs(prev => { perform duplicate/max checks against
prev and return [...prev, selection] })) so duplicate detection and MAX_CONFIGS
enforcement operate on the latest list; ensure any other places that call
setConfigs from async callbacks (e.g., toggleVersionSelection) follow the same
pattern or read from a latest-configs ref.
app/assessment/components/EvaluationsTab.tsx-186-431 (1)

186-431: 🛠️ Refactor suggestion | 🟠 Major

Use clientFetch() for client-side API calls.

These direct fetch() calls bypass the project’s token-refresh and auth-expiry handling. Keep the X-API-KEY header, but route the request through clientFetch(endpoint, options).

♻️ Suggested pattern
-const response = await fetch('/api/assessment/assessments', {
+const response = await clientFetch('/api/assessment/assessments', {
   headers: { 'X-API-KEY': apiKey },
 });

As per coding guidelines, **/*.{ts,tsx} should 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 the current code and only fix it if needed.

In `@app/assessment/components/EvaluationsTab.tsx` around lines 186 - 431,
Multiple client-side fetch calls (loadAssessments, loadChildRuns,
loadConfigDetail, triggerDownload, handleRerun, handleRetryAssessment,
handlePreview) bypass the project's token-refresh/auth-expiry logic; replace
each direct fetch(...) with clientFetch(endpoint, options) while preserving the
existing headers (including 'X-API-KEY'), HTTP methods, query strings, response
checks, JSON/blob parsing, and error handling. Ensure you import/ensure
clientFetch is available in this module and use it in the same places and shapes
as the original fetch calls so behavior (response.ok checks, parsing, thrown
errors, and finally blocks) remains identical but benefits from token refresh
and AUTH_EXPIRED_EVENT handling.
app/assessment/components/DatasetStep.tsx-58-255 (1)

58-255: 🛠️ Refactor suggestion | 🟠 Major

Use clientFetch() for client-side API calls.

Dataset list/load/upload/delete requests currently use raw fetch(), so 401 refresh and AUTH_EXPIRED_EVENT behavior is skipped.

♻️ Suggested pattern
-const response = await fetch("/api/assessment/datasets", {
+const response = await clientFetch("/api/assessment/datasets", {
   headers: { "X-API-KEY": apiKey },
 });

As per coding guidelines, **/*.{ts,tsx} should 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 the current code and only fix it if needed.

In `@app/assessment/components/DatasetStep.tsx` around lines 58 - 255, Several
client-side API calls use raw fetch (notably in fetchAndParseFile, loadDatasets,
handleCreateDataset, handleDatasetSelect indirectly, handleViewDataset, and
handleDeleteDataset), which bypasses token refresh and AUTH_EXPIRED_EVENT;
replace those fetch calls with clientFetch(endpoint, options) and preserve all
existing options (method, body/formData, headers including "X-API-KEY": apiKey)
and existing response/error handling logic, import clientFetch at the top of the
file, and ensure you still call await response.json() and check response.ok
exactly as before so behavior remains the same except for automatic
refresh/AUTH_EXPIRED_EVENT handling.
app/assessment/components/PromptAndConfigStep.tsx-345-394 (1)

345-394: ⚠️ Potential issue | 🟠 Major

Avoid stale configs writes after async selection fetches.

addSelection() closes over the configs array from the render that started the request. If users select multiple versions quickly, later async completions can call setConfigs([...configs, selection]) with stale state and drop an earlier selection.

Prefer a functional state update by widening the prop type to React.Dispatch<React.SetStateAction<ConfigSelection[]>>, or keep a synchronized ref for the latest configs before applying async results.

🐛 Suggested direction
 interface PromptAndConfigStepProps {
-  setConfigs: (configs: ConfigSelection[]) => void;
+  setConfigs: React.Dispatch<React.SetStateAction<ConfigSelection[]>>;
 }

 const addSelection = useCallback(
   (selection: ConfigSelection) => {
-    if (
-      configs.some(
+    setConfigs((prev) => {
+      if (
+        prev.some(
           (c) =>
             c.config_id === selection.config_id &&
             c.config_version === selection.config_version,
-        )
-      ) {
-        toast.error("This configuration version is already selected");
-        return;
+        )
+      ) {
+        return prev;
       }
-      if (configs.length >= MAX_CONFIGS) {
-        toast.error(`You can select up to ${MAX_CONFIGS} configurations`);
-        return;
+
+      if (prev.length >= MAX_CONFIGS) {
+        return prev;
       }
-      setConfigs([...configs, selection]);
+
+      return [...prev, selection];
+    });
   },
-  [configs, setConfigs, toast],
+  [setConfigs],
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/assessment/components/PromptAndConfigStep.tsx` around lines 345 - 394,
addSelection currently closes over the stale configs array and uses
setConfigs([...configs, selection]) which can drop selections when async fetches
(in toggleVersionSelection -> fetchConfigSelection) resolve out of order; change
addSelection to use the functional state updater form (call setConfigs(prev =>
[...prev, selection])) or change the configs prop type to
React.Dispatch<React.SetStateAction<ConfigSelection[]>> and update all callers
accordingly so asynchronous completions in toggleVersionSelection/addSelection
safely append without relying on a captured configs variable.
app/assessment/config/api.ts-95-100 (1)

95-100: 🛠️ Refactor suggestion | 🟠 Major

Use the exported CACHE_KEY constant instead of hardcoding the string.

constants.ts already exports CACHE_KEY = 'kaapi_configs_cache' (line 276), but here the same literal is duplicated. If the key is ever renamed in constants.ts, cache invalidation will silently stop working. Import and use CACHE_KEY to stay in sync with the rest of the module.

♻️ Proposed fix
-import { CACHE_INVALIDATED_EVENT } from "./constants";
+import { CACHE_INVALIDATED_EVENT, CACHE_KEY } from "./constants";
@@
 export function invalidateAssessmentConfigCache(): void {
   if (typeof window === "undefined") return;

-  localStorage.removeItem("kaapi_configs_cache");
+  localStorage.removeItem(CACHE_KEY);
   window.dispatchEvent(new CustomEvent(CACHE_INVALIDATED_EVENT));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/assessment/config/api.ts` around lines 95 - 100, The function
invalidateAssessmentConfigCache currently hardcodes the cache key string; import
and use the exported CACHE_KEY constant (from constants.ts) instead of the
literal "kaapi_configs_cache" inside invalidateAssessmentConfigCache and in any
localStorage.removeItem calls so the key stays in sync; update the top-of-file
imports to include CACHE_KEY and replace the hardcoded string in
localStorage.removeItem with CACHE_KEY.
app/assessment/config/api.ts-178-201 (1)

178-201: ⚠️ Potential issue | 🟠 Major

Client-side duplicate-name check is O(N) pagination with unhandled TOCTOU race condition.

findConfigByExactName paginates through all configs sequentially (100 per page) to verify a name doesn't exist before creating, yet another client/tab can still create that same name between the check (line 215) and the POST request (line 256). The backend proxy at app/api/configs/route.ts passes query parameters through to the backend, so if the backend /api/v1/configs endpoint supports a name filter parameter, use it to fetch directly instead of client-side pagination. If the backend returns a 409 on duplicate creates, requestJson will throw on non-2xx status, but currently there is no retry logic or special handling for duplicate-name errors—add explicit handling for the duplicate case as a retry that performs a new version POST instead of failing outright.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/assessment/config/api.ts` around lines 178 - 201, The client-side
pagination in findConfigByExactName is inefficient and racey; change it to call
the backend configs endpoint with a name filter (use the same query param
pass-through that app/api/configs/route.ts supports) so fetchConfigPage is not
needed — i.e., query /api/v1/configs?name=<normalizedName> and return the single
match or null. Also add explicit handling around the POST request that creates a
config (the requestJson call used for creates): if the POST fails with a 409
duplicate-name response, catch that error and implement a retry path that
performs a “create new version” POST (instead of surfacing the error) so
concurrent creates result in a versioned config rather than an unhandled
failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f50ed920-8ac2-4b8e-9512-2e83818393bc

📥 Commits

Reviewing files that changed from the base of the PR and between a52ee8e and 5d8d9bc.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (51)
  • app/(main)/datasets/page.tsx
  • app/(main)/keystore/page.tsx
  • app/api/assessment/assessments/[assessment_id]/results/route.ts
  • app/api/assessment/assessments/[assessment_id]/retry/route.ts
  • app/api/assessment/assessments/route.ts
  • app/api/assessment/datasets/[dataset_id]/route.ts
  • app/api/assessment/datasets/route.ts
  • app/api/assessment/evaluations/[evaluation_id]/results/route.ts
  • app/api/assessment/evaluations/[evaluation_id]/retry/route.ts
  • app/api/assessment/evaluations/route.ts
  • app/api/assessment/events/route.ts
  • app/api/configs/[config_id]/versions/route.ts
  • app/api/configs/route.ts
  • app/api/features/route.ts
  • app/assessment/AssessmentPageClient.tsx
  • app/assessment/components/ColumnMapperStep.tsx
  • app/assessment/components/DataViewModal.tsx
  • app/assessment/components/DatasetStep.tsx
  • app/assessment/components/EvaluationsTab.tsx
  • app/assessment/components/JsonEditor.tsx
  • app/assessment/components/MultiConfigStep.tsx
  • app/assessment/components/OutputSchemaStep.tsx
  • app/assessment/components/PromptAndConfigStep.tsx
  • app/assessment/components/PromptEditorStep.tsx
  • app/assessment/components/ReviewStep.tsx
  • app/assessment/components/Stepper.tsx
  • app/assessment/config/ConfigurationStep.tsx
  • app/assessment/config/api.ts
  • app/assessment/config/constants.ts
  • app/assessment/page.tsx
  • app/assessment/schemaUtils.ts
  • app/assessment/store.ts
  • app/assessment/types.ts
  • app/assessment/useAssessmentEvents.ts
  • app/components/FeatureRouteGuard.tsx
  • app/components/Sidebar.tsx
  • app/components/icons/index.tsx
  • app/components/icons/sidebar/AssessmentIcon.tsx
  • app/components/speech-to-text/ModelComparisonCard.tsx
  • app/layout.tsx
  • app/lib/FeatureFlagProvider.tsx
  • app/lib/colors.ts
  • app/lib/configTypes.ts
  • app/lib/constants/featureFlags.ts
  • app/lib/constants/keystore.ts
  • app/lib/context/AuthContext.tsx
  • app/lib/featureFlags.server.ts
  • app/lib/navConfig.ts
  • app/lib/types/datasets.ts
  • app/lib/types/nav.ts
  • package.json
💤 Files with no reviewable changes (1)
  • app/(main)/keystore/page.tsx

Comment on lines +1 to +10
/**
* ModelComparisonCard Component
*
* Minimal card design showing just the essentials:
* - Model name + WER score
* - Expandable for full details
*/

import React, { useState, useEffect } from "react";
import { colors } from "@/app/lib/colors";
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm this hook-using App Router component has a Client Component directive.

python - <<'PY'
from pathlib import Path

p = Path("app/components/speech-to-text/ModelComparisonCard.tsx")
text = p.read_text()
stripped = text.lstrip()

has_directive = stripped.startswith('"use client"') or stripped.startswith("'use client'")
uses_hooks = "useState" in text or "useEffect" in text

print(f"{p}: has_use_client={has_directive}, uses_hooks={uses_hooks}")
PY

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 168


🏁 Script executed:

cat -n app/components/speech-to-text/ModelComparisonCard.tsx

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 15098


Add "use client" directive to this component.

This component uses useState and useEffect hooks but lacks the Client Component directive. In Next.js App Router, components are Server Components by default; you must mark Client Components that use hooks with "use client" at the top of the file.

Proposed fix
+"use client";
+
 /**
  * ModelComparisonCard Component
📝 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
/**
* ModelComparisonCard Component
*
* Minimal card design showing just the essentials:
* - Model name + WER score
* - Expandable for full details
*/
import React, { useState, useEffect } from "react";
import { colors } from "@/app/lib/colors";
"use client";
/**
* ModelComparisonCard Component
*
* Minimal card design showing just the essentials:
* - Model name + WER score
* - Expandable for full details
*/
import React, { useState, useEffect } from "react";
import { colors } from "@/app/lib/colors";
🧰 Tools
🪛 GitHub Actions: Kaapi CI

[warning] 1-1: Prettier formatting check reported style issues in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/speech-to-text/ModelComparisonCard.tsx` around lines 1 - 10,
This component uses React hooks (useState, useEffect) but is missing the Next.js
Client Component directive; add the exact line "use client" as the very first
line of ModelComparisonCard.tsx (before any imports) so the component and its
hooks (used in the ModelComparisonCard component) run on the client; no other
code changes required.

Comment thread app/layout.tsx
Comment on lines +41 to 48
<FeatureFlagProvider initialFlags={initialFlags}>
<ToastProvider>
<AuthProvider>
<AppProvider>{children}</AppProvider>
</AuthProvider>
</ToastProvider>
</FeatureFlagProvider>
<Providers>{children}</Providers>
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 | 🔴 Critical

Render children only once.

The layout now mounts the entire app twice: once inside FeatureFlagProvider/ToastProvider/AuthProvider/AppProvider, then again inside <Providers>. This can duplicate pages, effects, API calls, and UI.

Proposed fix
-        <FeatureFlagProvider initialFlags={initialFlags}>
-          <ToastProvider>
-            <AuthProvider>
-              <AppProvider>{children}</AppProvider>
-            </AuthProvider>
-          </ToastProvider>
-        </FeatureFlagProvider>
-        <Providers>{children}</Providers>
+        <FeatureFlagProvider initialFlags={initialFlags}>
+          <Providers>{children}</Providers>
+        </FeatureFlagProvider>

Based on learnings, React Context providers such as AuthProvider, AppProvider, and Toast provider should be mounted in Providers.tsx.

📝 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
<FeatureFlagProvider initialFlags={initialFlags}>
<ToastProvider>
<AuthProvider>
<AppProvider>{children}</AppProvider>
</AuthProvider>
</ToastProvider>
</FeatureFlagProvider>
<Providers>{children}</Providers>
<FeatureFlagProvider initialFlags={initialFlags}>
<Providers>{children}</Providers>
</FeatureFlagProvider>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/layout.tsx` around lines 41 - 48, The layout currently mounts children
twice: once wrapped by FeatureFlagProvider, ToastProvider, AuthProvider,
AppProvider (with initialFlags) and again inside Providers; remove the duplicate
by rendering children only once — keep a single
<Providers>{children}</Providers> in app/layout.tsx and move
FeatureFlagProvider, ToastProvider, AuthProvider, and AppProvider (and use of
initialFlags) into the Providers component (Providers.tsx) so those contexts are
provided from one place; ensure you remove the nested <FeatureFlagProvider
...>...<AppProvider> block and verify Providers uses the same initialFlags prop
or reads it internally so children are not duplicated.

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