diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..b8e32cf
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,144 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Commands
+
+```bash
+npm run dev # Start dev server at http://localhost:3000
+npm run lint # Run ESLint
+npm run build # Production build
+npm run format # Format TS/tsx with Prettier
+```
+
+## Architecture Overview
+
+### Tech Stack
+
+- **Framework**: Next.js 16.0.7 (App Router)
+- **React 19.2.0** + **TypeScript** (strict mode)
+- **Routing**: Next.js App Router (react-router-dom is listed as a dependency but currently unused)
+- **Styling**: Tailwind CSS 4.x + custom color system and styles defined in `/app/globals.css` for cases not supported by Tailwind
+- **Data Fetching**: Native Fetch API + SWR 2.3.6 (used selectively where required)
+- **Date/Time**: date-fns 4.1.0, date-fns-tz 3.2.0
+- **ESLint**: Used for maintaining code quality, enforcing consistent coding standards, and catching potential issues during development and build time
+
+### Directory Structure
+
+| Path | Purpose |
+| ------------------------------ | --------------------------------------------------------------------------------------------- |
+| `app/(auth)/` | Authentication-related routes (e.g., invite, verify flows) |
+| `app/(main)/` | Main application routes (dashboard-level features like datasets, evaluations, settings, etc.) |
+| `app/api/` | Backend API route handlers (Next.js route handlers acting as BFF layer) |
+| `app/components/` | App-scoped components used within routes/Pages |
+| `app/components/icons/` | Hand-authored React icon components |
+| `app/hooks/` | Custom React hooks specific to app features |
+| `app/lib/` | Core shared logic and utilities across the application |
+| `app/lib/context/` | React context providers (global state handling) |
+| `app/lib/store/` | State management logic (custom/global store) |
+| `app/lib/types/` | TypeScript type definitions (shared across modules) |
+| `app/lib/utils/` | Domain-specific utility modules (e.g., evaluation, guardrails) |
+| `app/lib/data/` | Static data and validators (e.g., guardrails validators) |
+| `app/lib/apiClient.ts` | Centralized API client for forwarding requests to the backend |
+| `app/lib/authCookie.ts` | Authentication cookie utilities (get/set/remove tokens) |
+| `app/lib/configFetchers.ts` | API fetchers related to configuration modules |
+| `app/lib/constants.ts` | Global constants used across the app |
+| `app/lib/guardrailsClient.ts` | Client-side API helpers for guardrails features |
+| `app/lib/models.ts` | Data models/interfaces for structured data handling |
+| `app/lib/navConfig.ts` | Navigation configuration (sidebar/menu structure) |
+| `app/lib/promptEditorUtils.ts` | Utility functions for prompt editor logic |
+| `app/lib/utils.ts` | General utility/helper functions |
+| `public/favicon.ico` | Application favicon |
+
+## Import Aliases
+
+[tsconfig.json](./tsconfig.json) sets paths: `{ "@/*": ["./*"] }`, so imports are resolved from the project root using the `@/` prefix. Use:
+
+```
+import { apiClient } from '@/app/lib/apiClient';
+import { Providers } from '@/app/components/providers';
+import { APP_NAME } from '@/app/lib/constants';
+```
+
+SVGs follow Next.js defaults (imported as static assets via next/image or referenced from /public).
+
+## Routing & Role-Based Access
+
+Routing uses the **Next.js App Router** exclusively. Routes are organized via route groups:
+
+- `app/(auth)/` - unauthenticated flows (`/invite`, `/verify`)
+- `app/(main)/` — authenticated app surface (`/evaluations`, `/datasets`, `/configurations`, `/guardrails`, `/knowledge-base`, `/settings`, etc.)
+
+Role gating lives in middleware.ts and reads a kaapi_role cookie with two values:
+
+- `user` - standard authenticated user
+- `superuser` - admin; required for `/settings/*`
+
+The cookie is issued server-side by [authCookie.ts](app/lib/authCookie.ts) after login/verify based on user.is_superuser. Middleware classifies each request into one of:
+
+- `PUBLIC_ROUTES` — open to everyone (`/evaluations`, `/invite`, `/verify`, `/coming-soon/*`)
+- `GUEST_ONLY_ROUTES` — unauthenticated only (`/keystore`); authenticated users are redirected to `/evaluations`
+- `/settings/*` — superuser only
+- Everything else — any authenticated user
+
+There is no dynamic/custom role system; only the two static roles above.
+
+## Toast Notifications
+
+Toasts are managed via a React Context provider ([Toast.tsx](app/components/Toast.tsx)), mounted once in [Providers.tsx](app/components/providers/Providers.tsx). Consume them from any client component:
+
+```
+import { useToast } from '@/app/components/Toast';
+// or the re-export: import { useToast } from '@/app/hooks/useToast';
+
+function MyComponent() {
+ const toast = useToast();
+
+ toast.success('Saved successfully'); // success toast
+ toast.error('Something went wrong'); // error toast
+ toast.warning('Heads up'); // warning toast
+ toast.info('FYI'); // info toast
+
+ // Optional: override the default 5000ms auto-dismiss
+ toast.success('Saved', 3000);
+
+ // Low-level API (type + duration)
+ toast.addToast('Custom message', 'success', 4000);
+}
+```
+
+## Authentication [AuthContext.tsx](app/lib/context/AuthContext.tsx)
+
+There is no `AuthService` class. Auth state is owned by a React Context provider (`AuthProvider`) mounted in [Providers.tsx](app/components/providers/Providers.tsx), and consumed via the `useAuth()` hook:
+
+```
+import { useAuth } from '@/app/lib/context/AuthContext';
+
+function MyComponent() {
+ const {
+ isAuthenticated, isHydrated,
+ session, currentUser, googleProfile,
+ apiKeys, activeKey, addKey, removeKey, setKeys,
+ loginWithToken, logout,
+ } = useAuth();
+}
+```
+
+## App Context [AppContext.tsx](app/lib/context/AppContext.tsx)
+
+Sidebar state is managed via `AppProvider`, consumed with `useApp()`:
+
+```
+import { useApp } from '@/app/lib/context/AppContext';
+
+const { sidebarCollapsed, setSidebarCollapsed, toggleSidebar } = useApp();
+```
+
+## 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:
+
+- **Server-side (route handlers)**: Use `apiClient(request, endpoint, options)` — it relays `X-API-KEY` and `Cookie` headers automatically and returns `{ status, data, headers }`.
+- **Client-side**: Use `clientFetch(endpoint, options)` — handles token refresh on 401, dispatches `AUTH_EXPIRED_EVENT` when refresh fails, and throws with a message extracted from `error`, `message`, or `detail` fields in the response body.
+- **Error extraction**: `extractErrorMessage(body, fallback)` reads `body.error || body.message || body.detail` — follow this pattern when adding new API routes.
+- **Auth expiry**: On 401 with failed refresh, a `CustomEvent(AUTH_EXPIRED_EVENT)` is dispatched on `window`, which `AuthContext` listens to for automatic logout.
diff --git a/app/(auth)/invite/page.tsx b/app/(auth)/invite/page.tsx
new file mode 100644
index 0000000..762eaef
--- /dev/null
+++ b/app/(auth)/invite/page.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { Suspense } from "react";
+import { useSearchParams } from "next/navigation";
+import { SpinnerIcon } from "@/app/components/icons";
+import TokenVerifyPage from "@/app/components/auth/TokenVerifyPage";
+
+function InviteContent() {
+ const searchParams = useSearchParams();
+
+ return (
+
- Manage provider credentials -
-+ {selectedOrg.name} +
+{latestVersion.instructions || "No instructions set"} diff --git a/app/components/ConfigModal.tsx b/app/components/ConfigModal.tsx index 817b24f..0f3f412 100644 --- a/app/components/ConfigModal.tsx +++ b/app/components/ConfigModal.tsx @@ -7,7 +7,11 @@ import React, { useState, useEffect } from "react"; import { colors } from "@/app/lib/colors"; -import { EvalJob, AssistantConfig } from "./types"; +import CopyableCodeBlock from "@/app/components/CopyableCodeBlock"; +import CodeBlock from "@/app/components/CodeBlock"; +import Tag from "@/app/components/Tag"; +import { CloseIcon } from "@/app/components/icons"; +import { EvalJob, AssistantConfig } from "@/app/lib/types/evaluation"; import { useAuth } from "@/app/lib/context/AuthContext"; import { apiFetch } from "@/app/lib/apiClient"; import { @@ -35,6 +39,24 @@ interface ConfigVersionInfo { knowledge_base_ids?: string[]; } +const ConfigField = ({ + label, + children, +}: { + label: string; + children: React.ReactNode; +}) => ( +
- No detailed results available or using legacy format -
-- No individual scores available. Only summary metrics are available for - this evaluation. -
-| - | - Question - | -- Ground Truth - | -- Answer - | - {scoreNames.map((scoreName) => ( -- {scoreName} - | - ))} -
|---|---|---|---|---|
| - {index + 1} - | - - {/* Question */} -
-
- {question}
-
- |
-
- {/* Ground Truth */}
-
-
- {groundTruth}
-
- |
-
- {/* Answer */}
-
-
- {answer}
-
- |
-
- {/* Score Columns */}
- {scoreNames.map((scoreName) => {
- const score = getScoreByName(item.trace_scores, scoreName);
- const { value, color, bg } = formatScoreValue(score);
-
- return (
-
-
-
-
- {value}
-
- {score?.comment && (
- <>
- {
- const rect =
- e.currentTarget.getBoundingClientRect();
- const tooltipWidth = 300;
- const centerX = rect.left + rect.width / 2;
- const clampedLeft = Math.min(
- Math.max(centerX - tooltipWidth / 2, 8),
- window.innerWidth - tooltipWidth - 8,
- );
- setCommentPos({
- top: rect.top - 8,
- left: clampedLeft,
- });
- setOpenCommentId(`${index}-${scoreName}`);
- }}
- onMouseLeave={() => setOpenCommentId(null)}
- >
- i
-
- {openCommentId === `${index}-${scoreName}` && (
-
- {score.comment}
-
- )}
- >
- )}
- |
- );
- })}
-
- No grouped results available -
-| - Q.ID - | -- Question - | -- Ground Truth - | - {Array.from({ length: maxAnswers }, (_, i) => ( -- Answer {i + 1} - | - ))} -
|---|---|---|---|
| - {group.question_id} - | - - {/* Question */} -
-
- {group.question}
-
- |
-
- {/* Ground Truth */}
-
-
- {group.ground_truth_answer}
-
- |
-
- {/* Answer text only */}
- {Array.from({ length: maxAnswers }, (_, answerIndex) => {
- const answer = group.llm_answers[answerIndex];
- return (
-
- {answer ? (
-
- {answer}
-
- ) : (
- -
- )}
- |
- );
- })}
-
| - | - | - - {/* Score cells */} - {Array.from({ length: maxAnswers }, (_, answerIndex) => { - const answerScores: TraceScore[] = - group.scores?.[answerIndex] || []; - const answer = group.llm_answers[answerIndex]; - - return ( - |
- {answer && answerScores.length > 0 ? (
-
- {answerScores.map(
- (score: TraceScore, scoreIdx: number) => {
- if (!score) return null;
- const { value, color, bg } =
- formatScoreValue(score);
- return (
-
- ) : null}
-
-
- {score.name}:
-
-
- );
- },
- )}
-
-
-
- {value}
-
- {score?.comment &&
- (() => {
- const commentId = `g${index}-a${answerIndex}-s${scoreIdx}`;
- return (
- <>
- {
- const rect =
- e.currentTarget.getBoundingClientRect();
- const tooltipWidth = 300;
- const centerX =
- rect.left + rect.width / 2;
- const clampedLeft = Math.min(
- Math.max(
- centerX -
- tooltipWidth / 2,
- 8,
- ),
- window.innerWidth -
- tooltipWidth -
- 8,
- );
- setCommentPos({
- top: rect.top - 8,
- left: clampedLeft,
- });
- setOpenCommentId(commentId);
- }}
- onMouseLeave={() =>
- setOpenCommentId(null)
- }
- >
- i
-
- {openCommentId === commentId && (
-
- {score.comment}
-
- )}
- >
- );
- })()}
- |
- );
- })}
-