Skip to content

Guardrails UI: include in config editor#61

Closed
nishika26 wants to merge 13 commits intomainfrom
feature/guardrails_ui
Closed

Guardrails UI: include in config editor#61
nishika26 wants to merge 13 commits intomainfrom
feature/guardrails_ui

Conversation

@nishika26
Copy link
Copy Markdown
Contributor

@nishika26 nishika26 commented Mar 10, 2026

Summary by CodeRabbit

  • New Features

    • Config search; Guardrails validator management (add/edit/remove) with save/remove flows; Resync action for evaluations; API key verification for guardrails operations.
  • UI Changes

    • Renamed "Vector Store" to "Knowledge Base" and surfaced Knowledge Base IDs across configs, tools, and editor; KB snippets in evaluation header; score tiles restyled into per-item boxes.
  • Bug Fixes

    • Improved score-format detection and clearer messaging when only summary metrics are available.
  • API

    • New proxy endpoints for guardrails (ban lists, validators) and API key verification.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Renames vector-store terminology to knowledge-bases, adds Guardrails/validator UI and proxy routes, refactors score-type detection to summary-based types (BasicScoreObject + hasSummaryScores), adds evaluation resync support (client Resync button + resync_score forwarding), and exposes knowledge_base_ids across configs.

Changes

Cohort / File(s) Summary
Evaluation Resync & API Proxy
app/api/evaluations/[id]/route.ts, app/evaluations/[id]/page.tsx
Propagate resync_score query param to backend; add client-side resync flow (isResyncing, handleResync, Resync button); update score-format detection imports to hasSummaryScores.
Score Types & Display Components
app/components/types.ts, app/components/DetailedResultsTable.tsx, app/components/ScoreDisplay.tsx
Introduce BasicScoreObject and hasSummaryScores; update ScoreObject union and normalizeToIndividualScores; adapt table and score rendering to summary-based flows and handle empty summary_scores.
Knowledge Base Terminology & Config UI
app/components/ConfigCard.tsx, app/components/ConfigDrawer.tsx, app/components/prompt-editor/CurrentConfigTab.tsx, app/components/prompt-editor/DiffView.tsx
Rename UI/state from “Vector Store” to “Knowledge Base” (labels, toggles, displays); add Guardrails Configured badge in ConfigCard.
Config Model & Selector UI
app/components/ConfigModal.tsx, app/components/ConfigSelector.tsx
Add knowledge_base_ids to ConfigVersionInfo; collect/deduplicate KB IDs from params/tools and surface them in modal/tool sections; add live search and improved dropdown behavior in ConfigSelector.
Guardrails UI & Validator System
app/components/prompt-editor/ValidatorListPane.tsx, app/components/prompt-editor/ConfigEditorPane.tsx, app/configurations/prompt-editor/page.tsx
Add Validator model and ValidatorListPane; extend ConfigEditorPane and parent page with guardrails mode, validator CRUD/save/remove flows, ban-list handling, and related UI/props.
Guardrails API Proxy Routes & API Key Verify
app/api/guardrails/ban_lists/route.ts, app/api/guardrails/ban_lists/[ban_list_id]/route.ts, app/api/guardrails/validators/configs/route.ts, app/api/guardrails/validators/configs/[config_id]/route.ts, app/api/apikeys/verify/route.ts
New server-side proxy endpoints for guardrails ban-lists and validator configs and an API key verify endpoint; validate/forward auth headers, propagate backend responses, and surface proxy errors.
Configs Utilities
app/lib/useConfigs.ts
Add hasGuardrails and guardrailsCount to SavedConfig; compute counts from config blobs; tweak timestamp parsing to treat plain strings as UTC.
Keystore UI
app/keystore/page.tsx
Minor UI layout tweaks for stored keys and AddKeyModal spacing; display label for Kaapi API Key block.
Env Example
.env.example
Add example env vars for NEXT_PUBLIC_BACKEND_URL and GUARDRAIL_TOKEN.

Sequence Diagram

sequenceDiagram
    actor User
    participant EvalPage as Evaluation Page
    participant API as /api/evaluations/{id}
    participant Backend as Backend Service
    participant AssistantAPI as Assistant Config API
    participant ConfigAPI as Config Version API

    User->>EvalPage: Click "Resync"
    activate EvalPage
    EvalPage->>EvalPage: set isResyncing = true
    EvalPage->>API: GET /api/evaluations/{id}?resync_score=true&get_trace_info=true
    deactivate EvalPage

    activate API
    API->>Backend: Forward request with resync params
    Backend-->>API: Return resynced score & traces
    API-->>EvalPage: Respond with updated data
    deactivate API

    activate EvalPage
    EvalPage->>AssistantAPI: Fetch assistant config
    EvalPage->>ConfigAPI: Fetch config version info
    AssistantAPI-->>EvalPage: Assistant config
    ConfigAPI-->>EvalPage: Config version (includes knowledge_base_ids)
    EvalPage->>EvalPage: update state, set isResyncing = false
    EvalPage->>User: Render refreshed scores & KB info
    deactivate EvalPage
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • Prajna1999
  • vprashrex
  • AkhileshNegi

Poem

🐰 I hopped through code and changed a name,

Guardrails stand ready, steady and tame.
Knowledge bases lined the config view,
Scores refreshed when resync came through.
Validators clap — a soft code-chew.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly reflects the main objective: integrating guardrails UI into the config editor. The changes include guardrails support in ConfigEditorPane, ValidatorListPane creation, and guardrails state management across multiple components, with extensive guardrails-related additions throughout.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/guardrails_ui
📝 Coding Plan
  • Generate coding plan for human review comments

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

Caution

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

⚠️ Outside diff range comments (2)
app/components/ConfigCard.tsx (1)

167-205: ⚠️ Potential issue | 🟡 Minor

Deduplicate the aggregated knowledge base list before counting it.

If two tools point at the same knowledge base, this section double-counts it and repeats the same ID in the expanded list.

💡 Minimal fix
-                  const allVectorStoreIds = latestVersion.tools
-                    .flatMap(tool => tool.knowledge_base_ids || [])
-                    .filter(id => id);
+                  const allKnowledgeBaseIds = [...new Set(
+                    latestVersion.tools
+                      .flatMap(tool => tool.knowledge_base_ids || [])
+                      .filter((id): id is string => Boolean(id))
+                  )];
 
-                  return allVectorStoreIds.length > 0 && (
+                  return allKnowledgeBaseIds.length > 0 && (
                     <div className="mt-3 pt-2" style={{ borderTop: `1px solid ${colors.border}` }}>
                       <button
                         onClick={() => setShowKnowledgeBases(!showKnowledgeBases)}
@@
                         }}
                       >
                         <span style={{ color: colors.text.secondary, fontSize: '11px' }}>
-                          Knowledge Base IDs ({allVectorStoreIds.length})
+                          Knowledge Base IDs ({allKnowledgeBaseIds.length})
                         </span>
@@
                       {showKnowledgeBases && (
                         <div
                           className="mt-1 p-2 rounded-md space-y-1"
@@
                           }}
                         >
-                          {allVectorStoreIds.map((id, idx) => (
+                          {allKnowledgeBaseIds.map((id) => (
                             <div
-                              key={idx}
+                              key={id}
                               className="break-all"
                               style={{
                                 fontFamily: 'monospace',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/ConfigCard.tsx` around lines 167 - 205, The aggregated list
allVectorStoreIds in ConfigCard.tsx (built from
latestVersion.tools.flatMap(...).filter(...)) should be deduplicated before
counting and rendering so duplicate knowledge_base_ids aren't double-counted or
repeated; replace the current allVectorStoreIds with a deduplicated version
(e.g., use a Set or Array.from(new Set(allVectorStoreIds))) and use that unique
array for the count in the button label and for the expanded
allVectorStoreIds.map rendering.
app/components/ScoreDisplay.tsx (1)

60-108: ⚠️ Potential issue | 🟠 Major

Categorical-only summaries currently render nothing.

This branch only returns a fallback when both arrays are empty. If summary_scores contains only CATEGORICAL items, the check falls through and numericScores.map(...) produces an empty wrapper instead of any message or categorical rendering.

💡 Minimal fix
-    // If no numeric scores at all, show a message
-    if (numericScores.length === 0 && categoricalScores.length === 0) {
+    // If no numeric scores at all, show a message
+    if (numericScores.length === 0) {
       return (
         <div
           className="inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-sm"
@@
         >
           <span className="font-medium">Score:</span>
-          <span>No numeric scores available</span>
+          <span>
+            {categoricalScores.length > 0 ? 'Only categorical scores are available' : 'No numeric scores available'}
+          </span>
         </div>
       );
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/ScoreDisplay.tsx` around lines 60 - 108, The component
currently only handles the case where both numericScores and categoricalScores
are empty and otherwise always renders numericScores, which produces an empty
wrapper when only categoricalScores exist; update ScoreDisplay so that after
computing numericScores and categoricalScores it checks for three cases: (1) if
both are empty keep the existing "No numeric scores available" fallback, (2) if
numericScores is empty but categoricalScores has items render a categorical view
(e.g., map over categoricalScores to show name/value badges or a brief summary)
instead of the empty wrapper, and (3) otherwise render the existing
numericScores.map output; locate symbols summaryScores, numericScores,
categoricalScores and the return blocks to implement this conditional branching.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/components/ConfigModal.tsx`:
- Around line 95-100: The code assumes params.tools is an array when extracting
knowledge_base_ids causing runtime errors for non-array payloads; update the
guard to use Array.isArray(params.tools) before calling filter/flatMap so only
true arrays are processed (leave existing logic that filters for
Array.isArray(tool.knowledge_base_ids) intact), i.e., change the conditional
around the block that computes toolKbIds and pushes into knowledgeBaseIds to
check Array.isArray(params.tools) and skip extraction otherwise; locate this in
ConfigModal.tsx around the params.tools / knowledgeBaseIds logic.

In `@app/components/ConfigSelector.tsx`:
- Around line 224-252: The search input wrapper in ConfigSelector (the div
containing the input with className "w-full px-4 py-3 rounded-md flex
items-center justify-between" and the input element using searchQuery) sits
below the outside-click backdrop (z-40), causing clicks to hit the backdrop and
close the dropdown; raise the wrapper (or the input) above the backdrop by
adding a higher z-index (e.g. z-50 or style { zIndex: 50 }) to that wrapper or
the input so the field and caret receive pointer events before the backdrop.
- Around line 231-239: The search input in ConfigSelector (the input bound to
searchQuery and updated via setSearchQuery) lacks a programmatic label; add an
accessible name by adding an aria-label (for example aria-label="Search
configurations") to the same input element so screen readers can announce its
purpose while keeping existing props (value, onChange, placeholder, autoFocus,
style, className) intact.
- Around line 41-45: filteredConfigGroups currently trims searchQuery only to
decide whether to filter but uses the untrimmed value in includes(), causing
mismatches for queries with trailing/leading spaces; normalize the query first
by creating a normalizedSearchQuery = searchQuery.trim().toLowerCase() (or
similar) and use that in the filter: compare
group.name.toLowerCase().includes(normalizedSearchQuery), while still falling
back to configGroups when normalizedSearchQuery is empty; update references in
the filteredConfigGroups computation to use normalizedSearchQuery.

In `@app/components/prompt-editor/ConfigEditorPane.tsx`:
- Around line 812-821: The threshold fallback uses the || operator which treats
0 as falsy, preventing saving a zero value; update all places that render or
read validatorConfig.threshold (e.g., the label showing Threshold, the range
input value, and any other uses such as where validatorConfig.threshold is
defaulted around setValidatorConfig) to use the nullish coalescing operator (??
0.8 or ?? 0) instead of || so a legitimate 0 is preserved (e.g., change
expressions like validatorConfig.threshold || 0.8 to validatorConfig.threshold
?? 0.8).

In `@app/components/prompt-editor/DiffView.tsx`:
- Around line 216-219: The current check uses tool.knowledge_base_ids which is
truthy for an empty array and the UI only renders the first ID via
tool.knowledge_base_ids[0]; change the guard to check
tool.knowledge_base_ids?.length > 0 and render the full array instead of only
the first element (e.g., map over tool.knowledge_base_ids or join them into a
single string) so empty arrays don’t produce a blank row and all IDs are
displayed; update the block in DiffView.tsx where tool.knowledge_base_ids is
referenced.

In `@app/components/prompt-editor/ValidatorListPane.tsx`:
- Around line 74-117: The button only uses color to indicate selection; add
semantic state attributes so assistive tech can announce the active validator:
on the button element (where selectedValidator is compared to validator.id and
onSelectValidator is called) add aria-pressed={selectedValidator ===
validator.id} and aria-current={selectedValidator === validator.id} (or
aria-selected if you prefer list semantics) and ensure the button remains
focusable and uses the existing validator.id to compute the boolean. This keeps
the selection logic in the same place (button onClick ->
onSelectValidator(validator.id)) while exposing selected state via aria
attributes for accessibility.

In `@app/components/types.ts`:
- Around line 178-205: normalizeToIndividualScores currently maps
GroupedTraceItem to bogus placeholders; change it to detect grouped traces and
return a safe empty array instead of producing fake rows. Specifically, inside
normalizeToIndividualScores (and before mapping), if isNewScoreObjectV2(score)
then check score.traces for any item that is a GroupedTraceItem (i.e., missing
'llm_answer' or using whatever discriminator your codebase uses such as
isGroupedFormat) and if any grouped item is present return []; otherwise
continue mapping TraceItem entries (the existing branch that checks 'llm_answer'
and builds IndividualScore objects).

In `@app/configurations/prompt-editor/page.tsx`:
- Around line 87-90: The savedValidators state is not included in the persisted
config blob so guardrails are only UI‑level and get lost or leaked across config
switches; update the save/load/new-config paths to include and restore
savedValidators: when building currentConfigBlob in handleSaveConfig include the
savedValidators array, update handleLoadConfigById to read savedValidators from
the loaded blob and call setSavedValidators, and ensure the new-config and load
handlers explicitly clear setSavedValidators (or set from blob) to avoid bleed
between configs (references: savedValidators, setSavedValidators,
currentConfigBlob, handleSaveConfig, handleLoadConfigById, isGuardrailsMode).

In `@app/evaluations/`[id]/page.tsx:
- Around line 460-465: The UI allows the "new normalized" path by detecting
summary_scores via hasSummaryScores(scoreObject) but handleExportCSV() still
gatekeeps export using isNewScoreObjectV2(scoreObject), causing inconsistent
"Export not available" behavior; update handleExportCSV (or its
export-eligibility check) to accept the same condition used for rendering—use
hasSummaryScores(scoreObject) (and the existing summary_scores fallback) or
create a shared helper (e.g., isExportableScoreObject) and replace calls to
isNewScoreObjectV2(scoreObject) so both rendering and exporting use the same
predicate.
- Around line 384-403: The code is currently treating the response envelope as a
successful job object (setting foundJob and showing toast) even when the API
returned { success: false, error }, so add a guard to check the response
envelope before mutating state: after obtaining data from response.json() (in
the fetchJobDetails / page.tsx flow), verify that data.success is not false and
that data.data exists; if data.success === false or data.data is missing, throw
or handle the error (do not call setJob, fetchAssistantConfig, fetchConfigInfo,
or toast.success). Update the block around
foundJob/setJob/fetchAssistantConfig/fetchConfigInfo/toast.success to only run
when the envelope indicates success (e.g., data.success !== false && data.data).

---

Outside diff comments:
In `@app/components/ConfigCard.tsx`:
- Around line 167-205: The aggregated list allVectorStoreIds in ConfigCard.tsx
(built from latestVersion.tools.flatMap(...).filter(...)) should be deduplicated
before counting and rendering so duplicate knowledge_base_ids aren't
double-counted or repeated; replace the current allVectorStoreIds with a
deduplicated version (e.g., use a Set or Array.from(new Set(allVectorStoreIds)))
and use that unique array for the count in the button label and for the expanded
allVectorStoreIds.map rendering.

In `@app/components/ScoreDisplay.tsx`:
- Around line 60-108: The component currently only handles the case where both
numericScores and categoricalScores are empty and otherwise always renders
numericScores, which produces an empty wrapper when only categoricalScores
exist; update ScoreDisplay so that after computing numericScores and
categoricalScores it checks for three cases: (1) if both are empty keep the
existing "No numeric scores available" fallback, (2) if numericScores is empty
but categoricalScores has items render a categorical view (e.g., map over
categoricalScores to show name/value badges or a brief summary) instead of the
empty wrapper, and (3) otherwise render the existing numericScores.map output;
locate symbols summaryScores, numericScores, categoricalScores and the return
blocks to implement this conditional branching.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f6786b66-24aa-4b5f-9e79-6410a316e6c9

📥 Commits

Reviewing files that changed from the base of the PR and between a0ada14 and 855281a.

📒 Files selected for processing (17)
  • app/api/evaluations/[id]/route.ts
  • app/api/guardrails/validators/configs/[config_id]/route.ts
  • app/api/guardrails/validators/configs/route.ts
  • app/components/ConfigCard.tsx
  • app/components/ConfigDrawer.tsx
  • app/components/ConfigModal.tsx
  • app/components/ConfigSelector.tsx
  • app/components/DetailedResultsTable.tsx
  • app/components/ScoreDisplay.tsx
  • app/components/prompt-editor/ConfigEditorPane.tsx
  • app/components/prompt-editor/CurrentConfigTab.tsx
  • app/components/prompt-editor/DiffView.tsx
  • app/components/prompt-editor/ValidatorListPane.tsx
  • app/components/types.ts
  • app/configurations/prompt-editor/page.tsx
  • app/evaluations/[id]/page.tsx
  • app/lib/useConfigs.ts

Comment on lines +95 to +100
// 2. Check tools array for knowledge_base_ids
if (params.tools) {
const toolKbIds = params.tools
.filter((tool: any) => Array.isArray(tool.knowledge_base_ids) && tool.knowledge_base_ids.length > 0)
.flatMap((tool: any) => tool.knowledge_base_ids);
knowledgeBaseIds.push(...toolKbIds);
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

Guard params.tools with Array.isArray here too.

The render path already treats tools as optional/unknown, but this branch assumes any truthy params.tools has filter and flatMap. A non-array payload drops into the catch block and skips KB extraction entirely.

💡 Minimal fix
-          if (params.tools) {
+          if (Array.isArray(params.tools)) {
             const toolKbIds = params.tools
-              .filter((tool: any) => Array.isArray(tool.knowledge_base_ids) && tool.knowledge_base_ids.length > 0)
+              .filter((tool: any) => tool && Array.isArray(tool.knowledge_base_ids) && tool.knowledge_base_ids.length > 0)
               .flatMap((tool: any) => tool.knowledge_base_ids);
             knowledgeBaseIds.push(...toolKbIds);
           }
📝 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
// 2. Check tools array for knowledge_base_ids
if (params.tools) {
const toolKbIds = params.tools
.filter((tool: any) => Array.isArray(tool.knowledge_base_ids) && tool.knowledge_base_ids.length > 0)
.flatMap((tool: any) => tool.knowledge_base_ids);
knowledgeBaseIds.push(...toolKbIds);
// 2. Check tools array for knowledge_base_ids
if (Array.isArray(params.tools)) {
const toolKbIds = params.tools
.filter((tool: any) => tool && Array.isArray(tool.knowledge_base_ids) && tool.knowledge_base_ids.length > 0)
.flatMap((tool: any) => tool.knowledge_base_ids);
knowledgeBaseIds.push(...toolKbIds);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/ConfigModal.tsx` around lines 95 - 100, The code assumes
params.tools is an array when extracting knowledge_base_ids causing runtime
errors for non-array payloads; update the guard to use
Array.isArray(params.tools) before calling filter/flatMap so only true arrays
are processed (leave existing logic that filters for
Array.isArray(tool.knowledge_base_ids) intact), i.e., change the conditional
around the block that computes toolKbIds and pushes into knowledgeBaseIds to
check Array.isArray(params.tools) and skip extraction otherwise; locate this in
ConfigModal.tsx around the params.tools / knowledgeBaseIds logic.

Comment on lines +41 to +45
const filteredConfigGroups = searchQuery.trim()
? configGroups.filter(group =>
group.name.toLowerCase().includes(searchQuery.toLowerCase())
)
: configGroups;
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

Normalize the query before matching.

trim() is only used to decide whether to filter, not for the actual includes() call. A value like "foo " will enter the filtered branch and then miss a config named "foo".

💡 Suggested fix
-  const filteredConfigGroups = searchQuery.trim()
+  const normalizedQuery = searchQuery.trim().toLowerCase();
+  const filteredConfigGroups = normalizedQuery
     ? configGroups.filter(group =>
-        group.name.toLowerCase().includes(searchQuery.toLowerCase())
+        group.name.toLowerCase().includes(normalizedQuery)
       )
     : configGroups;
📝 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
const filteredConfigGroups = searchQuery.trim()
? configGroups.filter(group =>
group.name.toLowerCase().includes(searchQuery.toLowerCase())
)
: configGroups;
const normalizedQuery = searchQuery.trim().toLowerCase();
const filteredConfigGroups = normalizedQuery
? configGroups.filter(group =>
group.name.toLowerCase().includes(normalizedQuery)
)
: configGroups;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/ConfigSelector.tsx` around lines 41 - 45, filteredConfigGroups
currently trims searchQuery only to decide whether to filter but uses the
untrimmed value in includes(), causing mismatches for queries with
trailing/leading spaces; normalize the query first by creating a
normalizedSearchQuery = searchQuery.trim().toLowerCase() (or similar) and use
that in the filter: compare
group.name.toLowerCase().includes(normalizedSearchQuery), while still falling
back to configGroups when normalizedSearchQuery is empty; update references in
the filteredConfigGroups computation to use normalizedSearchQuery.

Comment on lines +224 to +252
<div
className="w-full px-4 py-3 rounded-md flex items-center justify-between"
style={{
backgroundColor: colors.bg.primary,
border: `1px solid ${colors.accent.primary}`,
}}
>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search configurations..."
className="flex-1 bg-transparent text-sm focus:outline-none"
style={{ color: colors.text.primary }}
autoFocus
/>
<svg
className="w-5 h-5 flex-shrink-0 ml-2 transition-transform"
style={{
color: colors.text.secondary,
transform: 'rotate(180deg)'
}}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
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 | 🟠 Major

Keep the open search field above the outside-click backdrop.

The backdrop at Line 437 is z-40, but this wrapper never gets lifted above it. A mouse click into the search field or caret will hit the backdrop and close the dropdown instead of interacting with the input.

💡 Suggested fix
-              <div
-                className="w-full px-4 py-3 rounded-md flex items-center justify-between"
+              <div
+                className="relative z-50 w-full px-4 py-3 rounded-md flex items-center justify-between"
                 style={{
                   backgroundColor: colors.bg.primary,
                   border: `1px solid ${colors.accent.primary}`,
                 }}
               >

Also applies to: 435-439

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

In `@app/components/ConfigSelector.tsx` around lines 224 - 252, The search input
wrapper in ConfigSelector (the div containing the input with className "w-full
px-4 py-3 rounded-md flex items-center justify-between" and the input element
using searchQuery) sits below the outside-click backdrop (z-40), causing clicks
to hit the backdrop and close the dropdown; raise the wrapper (or the input)
above the backdrop by adding a higher z-index (e.g. z-50 or style { zIndex: 50
}) to that wrapper or the input so the field and caret receive pointer events
before the backdrop.

Comment on lines +231 to +239
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search configurations..."
className="flex-1 bg-transparent text-sm focus:outline-none"
style={{ color: colors.text.primary }}
autoFocus
/>
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

Give the search input an accessible name.

The placeholder does not provide a programmatic label, so assistive tech will treat this as an unlabeled text field. Adding an aria-label here is enough.

💡 Suggested fix
                 <input
                   type="text"
                   value={searchQuery}
                   onChange={(e) => setSearchQuery(e.target.value)}
+                  aria-label="Search configurations"
                   placeholder="Search configurations..."
                   className="flex-1 bg-transparent text-sm focus:outline-none"
                   style={{ color: colors.text.primary }}
                   autoFocus
                 />
📝 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={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search configurations..."
className="flex-1 bg-transparent text-sm focus:outline-none"
style={{ color: colors.text.primary }}
autoFocus
/>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
aria-label="Search configurations"
placeholder="Search configurations..."
className="flex-1 bg-transparent text-sm focus:outline-none"
style={{ color: colors.text.primary }}
autoFocus
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/ConfigSelector.tsx` around lines 231 - 239, The search input
in ConfigSelector (the input bound to searchQuery and updated via
setSearchQuery) lacks a programmatic label; add an accessible name by adding an
aria-label (for example aria-label="Search configurations") to the same input
element so screen readers can announce its purpose while keeping existing props
(value, onChange, placeholder, autoFocus, style, className) intact.

Comment on lines +812 to +821
<label className="block text-xs font-semibold mb-2" style={{ color: colors.text.primary }}>
Threshold: {validatorConfig.threshold || 0.8}
</label>
<input
type="range"
min="0"
max="1"
step="0.1"
value={validatorConfig.threshold || 0.8}
onChange={(e) => setValidatorConfig({ ...validatorConfig, threshold: parseFloat(e.target.value) })}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
node - <<'NODE'
console.log('0 || 0.8 =', 0 || 0.8);
console.log('0 ?? 0.8 =', 0 ?? 0.8);
NODE

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 103


🏁 Script executed:

sed -n '812,821p' app/components/prompt-editor/ConfigEditorPane.tsx

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 624


🏁 Script executed:

sed -n '870,872p' app/components/prompt-editor/ConfigEditorPane.tsx

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 343


🏁 Script executed:

rg "threshold \|\| 0\.8" app/components/prompt-editor/ConfigEditorPane.tsx -n

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 351


0 is currently impossible to save as a threshold.

The slider allows min="0", but the || 0.8 fallback treats it as falsy and reverts to 0.8. Use ?? instead.

🛠️ Suggested fix
- Threshold: {validatorConfig.threshold || 0.8}
+ Threshold: {validatorConfig.threshold ?? 0.8}
- value={validatorConfig.threshold || 0.8}
+ value={validatorConfig.threshold ?? 0.8}
- Threshold: {validator.config?.threshold || 0.8} • {validator.config?.enabled ? 'Enabled' : 'Disabled'}
+ Threshold: {validator.config?.threshold ?? 0.8} • {validator.config?.enabled ? 'Enabled' : 'Disabled'}

Applies to lines 813, 820, and 871.

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

In `@app/components/prompt-editor/ConfigEditorPane.tsx` around lines 812 - 821,
The threshold fallback uses the || operator which treats 0 as falsy, preventing
saving a zero value; update all places that render or read
validatorConfig.threshold (e.g., the label showing Threshold, the range input
value, and any other uses such as where validatorConfig.threshold is defaulted
around setValidatorConfig) to use the nullish coalescing operator (?? 0.8 or ??
0) instead of || so a legitimate 0 is preserved (e.g., change expressions like
validatorConfig.threshold || 0.8 to validatorConfig.threshold ?? 0.8).

Comment on lines 216 to 219
{tool.knowledge_base_ids && (
<div className="text-xs mt-1" style={{ color: colors.text.secondary }}>
Vector Store: {tool.knowledge_base_ids[0]}
Knowledge Base: {tool.knowledge_base_ids[0]}
</div>
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

Render the full knowledge base list here.

tool.knowledge_base_ids && is truthy for [], so this can render a blank row, and it still hides every ID after the first one. Guard on length > 0 and render the full array.

💡 Minimal fix
-                              {tool.knowledge_base_ids && (
+                              {Array.isArray(tool.knowledge_base_ids) && tool.knowledge_base_ids.length > 0 && (
                                 <div className="text-xs mt-1" style={{ color: colors.text.secondary }}>
-                                  Knowledge Base: {tool.knowledge_base_ids[0]}
+                                  Knowledge Base{tool.knowledge_base_ids.length > 1 ? 's' : ''}: {tool.knowledge_base_ids.join(', ')}
                                 </div>
                               )}
📝 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
{tool.knowledge_base_ids && (
<div className="text-xs mt-1" style={{ color: colors.text.secondary }}>
Vector Store: {tool.knowledge_base_ids[0]}
Knowledge Base: {tool.knowledge_base_ids[0]}
</div>
{Array.isArray(tool.knowledge_base_ids) && tool.knowledge_base_ids.length > 0 && (
<div className="text-xs mt-1" style={{ color: colors.text.secondary }}>
Knowledge Base{tool.knowledge_base_ids.length > 1 ? 's' : ''}: {tool.knowledge_base_ids.join(', ')}
</div>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/prompt-editor/DiffView.tsx` around lines 216 - 219, The
current check uses tool.knowledge_base_ids which is truthy for an empty array
and the UI only renders the first ID via tool.knowledge_base_ids[0]; change the
guard to check tool.knowledge_base_ids?.length > 0 and render the full array
instead of only the first element (e.g., map over tool.knowledge_base_ids or
join them into a single string) so empty arrays don’t produce a blank row and
all IDs are displayed; update the block in DiffView.tsx where
tool.knowledge_base_ids is referenced.

Comment on lines +74 to +117
<button
key={validator.id}
onClick={() => onSelectValidator(validator.id)}
className="w-full text-left p-3 rounded-lg transition-colors"
style={{
backgroundColor: selectedValidator === validator.id ? colors.bg.secondary : colors.bg.primary,
border: `1px solid ${colors.border}`,
}}
onMouseEnter={(e) => {
if (selectedValidator !== validator.id) {
e.currentTarget.style.backgroundColor = colors.bg.secondary;
}
}}
onMouseLeave={(e) => {
if (selectedValidator !== validator.id) {
e.currentTarget.style.backgroundColor = colors.bg.primary;
}
}}
>
<div className="flex items-start gap-2">
<div className="flex-1">
<div className="font-semibold text-sm" style={{ color: colors.text.primary }}>
{validator.name}
</div>
<div className="text-xs mt-1" style={{ color: colors.text.secondary }}>
{validator.description}
</div>
<div className="flex gap-1 mt-2">
{validator.tags.map((tag) => (
<span
key={tag}
className="text-xs px-2 py-0.5 rounded"
style={{
backgroundColor: colors.bg.secondary,
color: colors.text.secondary,
}}
>
{tag}
</span>
))}
</div>
</div>
</div>
</button>
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

Expose the selected validator state semantically.

Right now the active validator is only distinguishable by color. Add a selected state that assistive tech can announce.

💡 Minimal fix
             <button
               key={validator.id}
+              type="button"
+              aria-pressed={selectedValidator === validator.id}
               onClick={() => onSelectValidator(validator.id)}
               className="w-full text-left p-3 rounded-lg transition-colors"
               style={{
📝 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
<button
key={validator.id}
onClick={() => onSelectValidator(validator.id)}
className="w-full text-left p-3 rounded-lg transition-colors"
style={{
backgroundColor: selectedValidator === validator.id ? colors.bg.secondary : colors.bg.primary,
border: `1px solid ${colors.border}`,
}}
onMouseEnter={(e) => {
if (selectedValidator !== validator.id) {
e.currentTarget.style.backgroundColor = colors.bg.secondary;
}
}}
onMouseLeave={(e) => {
if (selectedValidator !== validator.id) {
e.currentTarget.style.backgroundColor = colors.bg.primary;
}
}}
>
<div className="flex items-start gap-2">
<div className="flex-1">
<div className="font-semibold text-sm" style={{ color: colors.text.primary }}>
{validator.name}
</div>
<div className="text-xs mt-1" style={{ color: colors.text.secondary }}>
{validator.description}
</div>
<div className="flex gap-1 mt-2">
{validator.tags.map((tag) => (
<span
key={tag}
className="text-xs px-2 py-0.5 rounded"
style={{
backgroundColor: colors.bg.secondary,
color: colors.text.secondary,
}}
>
{tag}
</span>
))}
</div>
</div>
</div>
</button>
<button
key={validator.id}
type="button"
aria-pressed={selectedValidator === validator.id}
onClick={() => onSelectValidator(validator.id)}
className="w-full text-left p-3 rounded-lg transition-colors"
style={{
backgroundColor: selectedValidator === validator.id ? colors.bg.secondary : colors.bg.primary,
border: `1px solid ${colors.border}`,
}}
onMouseEnter={(e) => {
if (selectedValidator !== validator.id) {
e.currentTarget.style.backgroundColor = colors.bg.secondary;
}
}}
onMouseLeave={(e) => {
if (selectedValidator !== validator.id) {
e.currentTarget.style.backgroundColor = colors.bg.primary;
}
}}
>
<div className="flex items-start gap-2">
<div className="flex-1">
<div className="font-semibold text-sm" style={{ color: colors.text.primary }}>
{validator.name}
</div>
<div className="text-xs mt-1" style={{ color: colors.text.secondary }}>
{validator.description}
</div>
<div className="flex gap-1 mt-2">
{validator.tags.map((tag) => (
<span
key={tag}
className="text-xs px-2 py-0.5 rounded"
style={{
backgroundColor: colors.bg.secondary,
color: colors.text.secondary,
}}
>
{tag}
</span>
))}
</div>
</div>
</div>
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/prompt-editor/ValidatorListPane.tsx` around lines 74 - 117,
The button only uses color to indicate selection; add semantic state attributes
so assistive tech can announce the active validator: on the button element
(where selectedValidator is compared to validator.id and onSelectValidator is
called) add aria-pressed={selectedValidator === validator.id} and
aria-current={selectedValidator === validator.id} (or aria-selected if you
prefer list semantics) and ensure the button remains focusable and uses the
existing validator.id to compute the boolean. This keeps the selection logic in
the same place (button onClick -> onSelectValidator(validator.id)) while
exposing selected state via aria attributes for accessibility.

Comment on lines +178 to +205
// Normalize traces to IndividualScore format for table display
export function normalizeToIndividualScores(score: ScoreObject | null | undefined): IndividualScore[] {
if (!score) return [];

if (isNewScoreObjectV2(score)) {
// Convert TraceItem[] to IndividualScore[]
return score.traces.map(trace => ({
trace_id: trace.trace_id,
input: { question: trace.question },
output: { answer: trace.llm_answer },
metadata: { ground_truth: trace.ground_truth_answer },
trace_scores: trace.scores
}));
}

if (isNewScoreObject(score)) {
return score.individual_scores;
// Convert TraceItem[] to IndividualScore[] for table display
// Note: Grouped traces should be detected earlier and handled separately
return score.traces.map((trace: TraceItem | GroupedTraceItem) => {
// Handle regular TraceItem format
if ('llm_answer' in trace) {
return {
trace_id: trace.trace_id,
input: { question: trace.question },
output: { answer: trace.llm_answer },
metadata: { ground_truth: trace.ground_truth_answer },
trace_scores: trace.scores
};
}
// Should not reach here if grouped format is handled properly
return {
trace_id: '',
trace_scores: []
};
});
}

return [];
}
} No newline at end of file
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

Return a safe empty result for grouped traces.

NewScoreObjectV2 legitimately allows GroupedTraceItem[], but this helper currently turns each grouped item into { trace_id: '', trace_scores: [] }. Any caller that forgets to pre-check isGroupedFormat() will get bogus rows instead of a safe empty result.

💡 Minimal fix
 export function normalizeToIndividualScores(score: ScoreObject | null | undefined): IndividualScore[] {
   if (!score) return [];
 
   if (isNewScoreObjectV2(score)) {
-    // Convert TraceItem[] to IndividualScore[] for table display
-    // Note: Grouped traces should be detected earlier and handled separately
-    return score.traces.map((trace: TraceItem | GroupedTraceItem) => {
-      // Handle regular TraceItem format
-      if ('llm_answer' in trace) {
-        return {
-          trace_id: trace.trace_id,
-          input: { question: trace.question },
-          output: { answer: trace.llm_answer },
-          metadata: { ground_truth: trace.ground_truth_answer },
-          trace_scores: trace.scores
-        };
-      }
-      // Should not reach here if grouped format is handled properly
-      return {
-        trace_id: '',
-        trace_scores: []
-      };
-    });
+    if (isGroupedFormat(score.traces)) {
+      return [];
+    }
+
+    return score.traces
+      .filter((trace): trace is TraceItem => 'llm_answer' in trace)
+      .map((trace) => ({
+        trace_id: trace.trace_id,
+        input: { question: trace.question },
+        output: { answer: trace.llm_answer },
+        metadata: { ground_truth: trace.ground_truth_answer },
+        trace_scores: trace.scores,
+      }));
   }
 
   return [];
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/types.ts` around lines 178 - 205, normalizeToIndividualScores
currently maps GroupedTraceItem to bogus placeholders; change it to detect
grouped traces and return a safe empty array instead of producing fake rows.
Specifically, inside normalizeToIndividualScores (and before mapping), if
isNewScoreObjectV2(score) then check score.traces for any item that is a
GroupedTraceItem (i.e., missing 'llm_answer' or using whatever discriminator
your codebase uses such as isGroupedFormat) and if any grouped item is present
return []; otherwise continue mapping TraceItem entries (the existing branch
that checks 'llm_answer' and builds IndividualScore objects).

Comment on lines +384 to +403
const data = await response.json();
const foundJob = data.data || data;

if (!foundJob) {
throw new Error('Evaluation job not found');
}

setJob(foundJob);

// Fetch assistant config if assistant_id exists
if (foundJob.assistant_id) {
fetchAssistantConfig(foundJob.assistant_id, selectedKey.key);
}

// Fetch config info if config_id exists
if (foundJob.config_id && foundJob.config_version) {
fetchConfigInfo(foundJob.config_id, foundJob.config_version, selectedKey.key);
}

toast.success('Metrics resynced successfully');
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 | 🟠 Major

Handle success: false before mutating the page state.

fetchJobDetails() already treats { success: false, error } as a special case. Here that same payload falls through, foundJob becomes the error envelope, and the page stores something that is not an EvalJob before showing a success toast.

🛠️ Suggested guard
       const data = await response.json();
+      if (data.success === false) {
+        if (data.error && exportFormat === 'grouped') {
+          setExportFormat('row');
+        }
+        throw new Error(data.error || 'Failed to resync evaluation');
+      }
       const foundJob = data.data || data;
📝 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
const data = await response.json();
const foundJob = data.data || data;
if (!foundJob) {
throw new Error('Evaluation job not found');
}
setJob(foundJob);
// Fetch assistant config if assistant_id exists
if (foundJob.assistant_id) {
fetchAssistantConfig(foundJob.assistant_id, selectedKey.key);
}
// Fetch config info if config_id exists
if (foundJob.config_id && foundJob.config_version) {
fetchConfigInfo(foundJob.config_id, foundJob.config_version, selectedKey.key);
}
toast.success('Metrics resynced successfully');
const data = await response.json();
if (data.success === false) {
if (data.error && exportFormat === 'grouped') {
setExportFormat('row');
}
throw new Error(data.error || 'Failed to resync evaluation');
}
const foundJob = data.data || data;
if (!foundJob) {
throw new Error('Evaluation job not found');
}
setJob(foundJob);
// Fetch assistant config if assistant_id exists
if (foundJob.assistant_id) {
fetchAssistantConfig(foundJob.assistant_id, selectedKey.key);
}
// Fetch config info if config_id exists
if (foundJob.config_id && foundJob.config_version) {
fetchConfigInfo(foundJob.config_id, foundJob.config_version, selectedKey.key);
}
toast.success('Metrics resynced successfully');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/evaluations/`[id]/page.tsx around lines 384 - 403, The code is currently
treating the response envelope as a successful job object (setting foundJob and
showing toast) even when the API returned { success: false, error }, so add a
guard to check the response envelope before mutating state: after obtaining data
from response.json() (in the fetchJobDetails / page.tsx flow), verify that
data.success is not false and that data.data exists; if data.success === false
or data.data is missing, throw or handle the error (do not call setJob,
fetchAssistantConfig, fetchConfigInfo, or toast.success). Update the block
around foundJob/setJob/fetchAssistantConfig/fetchConfigInfo/toast.success to
only run when the envelope indicates success (e.g., data.success !== false &&
data.data).

Comment on lines +460 to 465
// Check if we have new score structure (V1, V2, or Basic)
const isNewFormat = hasSummaryScores(scoreObject);

// Safe access to summary scores
const summaryScores = (isNewFormat && scoreObject && 'summary_scores' in scoreObject)
const summaryScores = (isNewFormat && scoreObject)
? scoreObject.summary_scores || []
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 | 🟠 Major

Keep render and export support checks in sync.

These lines broaden the “supported score” path to anything with summary_scores, but handleExportCSV() still hard-requires isNewScoreObjectV2(scoreObject). On the new normalized path, users can see metrics and detailed results yet still hit “Export not available”.

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

In `@app/evaluations/`[id]/page.tsx around lines 460 - 465, The UI allows the "new
normalized" path by detecting summary_scores via hasSummaryScores(scoreObject)
but handleExportCSV() still gatekeeps export using
isNewScoreObjectV2(scoreObject), causing inconsistent "Export not available"
behavior; update handleExportCSV (or its export-eligibility check) to accept the
same condition used for rendering—use hasSummaryScores(scoreObject) (and the
existing summary_scores fallback) or create a shared helper (e.g.,
isExportableScoreObject) and replace calls to isNewScoreObjectV2(scoreObject) so
both rendering and exporting use the same predicate.

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

♻️ Duplicate comments (2)
app/components/prompt-editor/ConfigEditorPane.tsx (1)

1338-1346: ⚠️ Potential issue | 🟡 Minor

0 is still impossible to save for generic validator thresholds.

These || 0.8 fallbacks treat 0 as falsy, so the minimum value snaps back to 0.8 in both the UI and the saved config. Use ?? here as well.

🛠️ Preserve legitimate zero values
-                          Threshold: {validatorConfig.threshold || 0.8}
+                          Threshold: {validatorConfig.threshold ?? 0.8}
@@
-                          value={validatorConfig.threshold || 0.8}
+                          value={validatorConfig.threshold ?? 0.8}
@@
-                            threshold: validatorConfig.threshold || 0.8,
+                            threshold: validatorConfig.threshold ?? 0.8,

Also applies to: 1397-1400

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

In `@app/components/prompt-editor/ConfigEditorPane.tsx` around lines 1338 - 1346,
The UI and save logic treat a threshold of 0 as falsy because the code uses ||
fallbacks, causing values of 0 to be overwritten by 0.8; update the places that
use "validatorConfig.threshold || 0.8" to use the nullish coalescing operator
"validatorConfig.threshold ?? 0.8" (both where rendering "Threshold: ..." and
where setting the input value, and likewise change the same pattern at the other
occurrence around the validatorConfig usage at the later block referenced) so
legitimate zero thresholds are preserved; keep using parseFloat(e.target.value)
when calling setValidatorConfig to set the threshold.
app/components/prompt-editor/ValidatorListPane.tsx (1)

76-94: ⚠️ Potential issue | 🟡 Minor

Expose the selected validator semantically.

The active validator is still only distinguishable by color. Add button semantics so assistive tech can announce which item is currently selected.

♿ Minimal fix
             <button
               key={validator.id}
+              type="button"
+              aria-pressed={selectedValidator === validator.id}
               onClick={() => onSelectValidator(validator.id)}
               className="w-full text-left p-3 rounded-lg transition-colors"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/prompt-editor/ValidatorListPane.tsx` around lines 76 - 94, The
button that renders each validator in ValidatorListPane.tsx should expose
selection to assistive tech; update the button (where validator.id,
onSelectValidator, and selectedValidator are used) to include an accessibility
attribute such as aria-pressed={selectedValidator === validator.id} or
aria-current="true" when selected (and/or aria-selected if the container is a
listbox/option) so screen readers announce which item is active; keep the
existing onClick/onMouse handlers and styling but add the appropriate aria
attribute(s) and ensure the container has the correct list semantics
(role="listbox"/role="option") if using aria-selected.
🧹 Nitpick comments (9)
app/lib/useConfigs.ts (2)

469-483: Simplify timezone detection logic for robustness.

The current condition on lines 477-479 is fragile and hard to read. It may also miss edge cases like lowercase 'z' or space-separated timestamps.

♻️ Proposed simplification using regex
   let date: number;
   if (typeof timestamp === 'string') {
     // If timestamp doesn't include timezone info, assume it's UTC
     // and append 'Z' to ensure it's interpreted as UTC
-    const utcTimestamp = timestamp.endsWith('Z') || timestamp.includes('+') || timestamp.includes('T') && timestamp.split('T')[1].includes('-')
-      ? timestamp
-      : timestamp + 'Z';
+    // Check for timezone indicators: Z, +HH:MM, -HH:MM at the end
+    const hasTimezone = /([Zz]|[+-]\d{2}:\d{2})$/.test(timestamp);
+    const utcTimestamp = hasTimezone ? timestamp : timestamp + 'Z';
     date = new Date(utcTimestamp).getTime();
   } else {
     date = timestamp;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/useConfigs.ts` around lines 469 - 483, The timezone-detection branch
in formatRelativeTime is fragile; replace the current conditional (checking
.endsWith('Z'), includes('+'), and parsing 'T' parts) with a single robust regex
test that detects any explicit timezone indicator (trailing Z or z, or an offset
like +HH[:MM] or -HH[:MM], or a space followed by an offset), and if the regex
does not match append 'Z' before constructing new Date; update the logic inside
formatRelativeTime to use this regex-based check so it correctly handles
lowercase 'z', space-separated timestamps, and common offset formats.

115-121: Remove debug logging before merging.

The console.log on line 120 appears to be debug code that will pollute the browser console in production. Consider removing it or using a conditional debug flag.

Additionally, the (blob as any) type assertion bypasses TypeScript's type checking. If input_guardrails and output_guardrails are now expected fields, consider extending the ConfigVersionPublic type definition to include them.

♻️ Proposed fix
   // Check for guardrails
-  const inputGuardrails = (blob as any).input_guardrails || [];
-  const outputGuardrails = (blob as any).output_guardrails || [];
+  const inputGuardrails = blob.input_guardrails ?? [];
+  const outputGuardrails = blob.output_guardrails ?? [];
   const guardrailsCount = inputGuardrails.length + outputGuardrails.length;
-
-  console.log('[DEBUG flattenConfigVersion] Config:', config.name, 'Input guardrails:', inputGuardrails.length, 'Output guardrails:', outputGuardrails.length, 'Total:', guardrailsCount);

And update the type definition for config_blob in configTypes.ts to include:

input_guardrails?: unknown[];
output_guardrails?: unknown[];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/useConfigs.ts` around lines 115 - 121, Remove the debug console.log
line ('[DEBUG flattenConfigVersion] ...') from flattenConfigVersion and avoid
leaving ad-hoc debug prints in production; if you need runtime debugging gate it
behind a debug flag. Replace the (blob as any) assertions by updating the config
blob type: add optional properties input_guardrails?: unknown[] and
output_guardrails?: unknown[] to the ConfigVersionPublic/config_blob type in
configTypes.ts, then cast/annotate blob with that type and access
blob.input_guardrails and blob.output_guardrails directly.
app/api/guardrails/ban_lists/[ban_list_id]/route.ts (2)

1-1: Consider using NextRequest for consistency with sibling route.

This file uses the native Request type while the sibling ban_lists/route.ts uses NextRequest. While both work, using NextRequest consistently provides access to additional Next.js-specific utilities if needed later.

🔧 Suggested fix
-import { NextResponse } from 'next/server';
+import { NextRequest, NextResponse } from 'next/server';

 export async function GET(
-  request: Request,
+  request: NextRequest,
   { params }: { params: Promise<{ ban_list_id: string }> }
 ) {

Apply similarly to PUT and DELETE handlers.

Also applies to: 5-7, 54-56, 109-111

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

In `@app/api/guardrails/ban_lists/`[ban_list_id]/route.ts at line 1, Replace the
native Request type with NextRequest for consistency: update the import to bring
in NextRequest alongside NextResponse and change the handler signatures named
GET, PUT, and DELETE to accept NextRequest instead of Request; ensure any places
that construct or type-check the request (e.g., parameter names referenced in
GET/PUT/DELETE in this file) are updated to use NextRequest so you retain
Next.js-specific utilities and keep parity with the sibling ban_lists/route.ts.

22-22: Inconsistent log message prefixes across handlers.

The log messages have inconsistent path naming:

  • Line 22: [GET /api/guardrails/ban_lists/[ban_list_id]] (plural, correct)
  • Line 31: [GET /api/guardrails/ban_list/[ban_list_id]] (singular, incorrect)
  • Line 126: [DELETE /api/guardrails/ban_lists/[ban_list_id]] (plural, correct)
  • Line 136: [DELETE /api/guardrails/ban_list/[ban_list_id]] (singular, incorrect)

This inconsistency makes log filtering and debugging harder.

🔧 Suggested fix

Standardize all log prefixes to use the plural "ban_lists" to match the actual route path.

Also applies to: 31-31, 37-37, 40-40, 74-75, 86-86, 92-92, 95-95, 126-126, 136-136, 142-142, 145-145

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

In `@app/api/guardrails/ban_lists/`[ban_list_id]/route.ts at line 22, Log prefixes
in route.ts are inconsistent: replace all occurrences of the singular prefix
strings like "[GET /api/guardrails/ban_list/[ban_list_id]]" and "[DELETE
/api/guardrails/ban_list/[ban_list_id]]" with the plural form "[GET
/api/guardrails/ban_lists/[ban_list_id]]" and "[DELETE
/api/guardrails/ban_lists/[ban_list_id]]" respectively so all console.log calls
in the GET and DELETE handlers (and any other console.log lines referenced) use
the standardized "ban_lists" path; search for the exact log text snippets to
locate and update them within the GET handler, DELETE handler and any helper
logs in route.ts.
app/api/guardrails/ban_lists/route.ts (2)

18-18: Inconsistent log message naming: "ban_list" vs "ban_lists".

The log prefixes use singular "ban_list" (e.g., [GET /api/guardrails/ban_list]) but the actual route path is plural "ban_lists". This inconsistency could cause confusion when debugging.

🔧 Suggested fix
-    console.log('[GET /api/guardrails/ban_list] Forwarding to:', url);
+    console.log('[GET /api/guardrails/ban_lists] Forwarding to:', url);

Apply similar fixes to all log statements in both GET and POST handlers.

Also applies to: 29-29, 35-35, 39-39, 69-70, 82-82, 88-88, 92-92

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

In `@app/api/guardrails/ban_lists/route.ts` at line 18, Update all console.log
prefixes in the GET and POST handlers in route.ts to use the plural "ban_lists"
to match the actual route path; find the log statements inside the GET function
(e.g., the console.log that currently prints "[GET /api/guardrails/ban_list]
Forwarding to:") and the POST function loggers and change their prefix to "[GET
/api/guardrails/ban_lists]" or "[POST /api/guardrails/ban_lists]" respectively —
apply the same rename to every occurrence (the statements referenced around the
existing console.log calls in the GET/POST handlers).

35-35: Consider reducing verbose logging in production.

Logging full request bodies and response data (Lines 35, 70, 88) may expose sensitive information in production logs. Consider either:

  • Using a debug flag to conditionally enable verbose logging
  • Omitting or redacting potentially sensitive fields
🔧 Suggested approach
+const DEBUG = process.env.NODE_ENV === 'development';

-    console.log('[GET /api/guardrails/ban_list] Backend response data:', JSON.stringify(data, null, 2));
+    if (DEBUG) {
+      console.log('[GET /api/guardrails/ban_lists] Backend response data:', JSON.stringify(data, null, 2));
+    }

Also applies to: 70-70, 88-88

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

In `@app/api/guardrails/ban_lists/route.ts` at line 35, The console.log statements
in route.ts that print full response/request payloads (e.g., the '[GET
/api/guardrails/ban_list] Backend response data:' log) are too verbose for
production; update these to either guard them behind a debug flag
(process.env.NODE_ENV !== 'production' or a dedicated DEBUG flag) or redact
sensitive fields before logging (omit or mask request/response body properties)
and replace raw JSON.stringify calls with the guarded/redacting logger so the
same unique console.log instances in this file no longer emit full payloads in
production.
app/keystore/page.tsx (1)

370-386: Consider adding copy functionality for the guardrails token.

The copy button only copies the API key. If users need to copy the guardrails token, they currently have no way to do so from this UI. Consider either adding a separate copy button for the guardrails token or extending the existing button to handle both.

💡 Example: Add a copy button next to the guardrails token
                         {visibleKeys.has(apiKey.id)
                           ? apiKey.guardrails_token
                           : '•'.repeat(32)}
                       </code>
+                      <button
+                        onClick={() => onCopy(apiKey.guardrails_token!)}
+                        className="ml-2 p-1 rounded-md transition-colors"
+                        style={{
+                          borderWidth: '1px',
+                          borderColor: 'hsl(0, 0%, 85%)',
+                          backgroundColor: 'hsl(0, 0%, 100%)',
+                          color: 'hsl(330, 3%, 19%)'
+                        }}
+                        title="Copy Guardrails Token"
+                      >
+                        <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
+                        </svg>
+                      </button>
                     </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/keystore/page.tsx` around lines 370 - 386, The copy button currently
calls onCopy(apiKey.key) and only copies the API key; add UI and wiring to also
copy the guardrails token by either rendering a second button next to the token
display that calls onCopy(guardrailsToken) (or apiKey.guardrailsToken if that is
the field) or extend onCopy to accept a second optional value and render two
buttons that call onCopy(apiKey.key) and onCopy(guardrailsToken) respectively;
update any tooltip/title text to distinguish "Copy API key" vs "Copy guardrails
token" so users can copy either value from the same row.
app/components/ConfigCard.tsx (1)

115-144: Remove the render-time debug side effect.

This console.log runs every time the card renders. Keep render pure here and inline the badge condition without the IIFE/logging wrapper.

♻️ Minimal cleanup
-          {(() => {
-            console.log('[DEBUG ConfigCard] Config:', configGroup.name, 'hasGuardrails:', latestVersion.hasGuardrails, 'count:', latestVersion.guardrailsCount);
-            return latestVersion.hasGuardrails && (
+          {latestVersion.hasGuardrails && (
               <div
                 className="px-2.5 py-1 rounded-md text-xs flex items-center gap-1"
                 style={{
@@
                 <span style={{ color: '#16a34a', fontWeight: 500 }}>
                   Guardrails Configured
                 </span>
               </div>
-            );
-          })()}
+            )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/ConfigCard.tsx` around lines 115 - 144, In ConfigCard remove
the render-time side effect by deleting the console.log and the IIFE wrapper
around the badge render; instead directly inline the conditional JSX that checks
latestVersion.hasGuardrails (and optionally latestVersion.guardrailsCount) to
render the badge element, keeping the same classes/styles and SVG; this makes
the component pure—look for the anonymous IIFE around
latestVersion.hasGuardrails and the debug log and replace with a simple
conditional expression inside the JSX returned by ConfigCard.
app/api/guardrails/validators/configs/route.ts (1)

35-36: Gate the proxy debug logs behind development.

These logs dump full validator payloads and backend bodies on every request. Keep status-level logging only, or guard the verbose output so it does not ship by default.

Also applies to: 48-54

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

In `@app/api/guardrails/validators/configs/route.ts` around lines 35 - 36, The two
console.log calls that print full validator payloads in the route handler (the
logs at the start of the POST /api/guardrails/validators/configs handler and the
similar ones around lines 48-54) must be gated behind a development-only check
or replaced with a debug-level logger; update the route code (the POST handler
function) to only emit the verbose JSON body and forwarding URL when running in
development (e.g., process.env.NODE_ENV === 'development') or call
logger.debug/trace instead of console.log, and keep only status-level logs
(info/warn/error) in production.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/apikeys/verify/route.ts`:
- Line 3: The fallback backend URL in route.ts uses the constant backendUrl set
to 'http://localhost:8000', which is inconsistent with sibling guardrails
routes; update the backendUrl default to 'http://localhost:8001' so local
verification uses the same proxy port when NEXT_PUBLIC_BACKEND_URL is unset, and
scan other proxy/guardrails routes to ensure they all use the 8001 fallback for
consistency.
- Around line 39-41: The response currently returns raw error.message to clients
from the NextResponse.json call in the verify route; remove exposing internal
error details by returning a generic client-safe payload (e.g. { error: "Failed
to forward request to backend" }) and keep the full error in server logs instead
(use the existing logger or console.error to log error and any request context).
Ensure the status remains 500, do not include error.message in the JSON, and
optionally generate/log an errorId if you want to correlate client errors with
server logs.
- Around line 27-30: The current code calls JSON.parse(text) unguarded which
will throw on non-JSON backend bodies and turn valid backend errors into 500s;
update the parsing in the route handler to only parse when the response is JSON
(check response.headers.get('content-type') for 'application/json' or
'application/hal+json') and fallback to returning the raw text (or an empty
object) when not JSON, or wrap JSON.parse in a try/catch and use the caught
error to fall back to the raw text; adjust the variables `text` and `data` in
route.ts (the response handling block) so downstream logic consumes a safe
`data` value without letting parse errors propagate.

In `@app/api/guardrails/validators/configs/`[config_id]/route.ts:
- Around line 40-42: The GET proxy currently does "const text = await
response.text(); const data = text ? JSON.parse(text) : {}" which throws on
non-JSON upstream bodies; wrap the parsing in a try/catch so non-JSON (plain
text/HTML) falls back to returning the raw text body and original response
status/headers instead of throwing. Concretely: after reading response.text()
(variable text), attempt JSON.parse(text) inside try to produce data, but on
parse error return a Response(text, { status: response.status, headers:
response.headers }) (or set content-type to text/plain/html as appropriate) and
only use parsed JSON for downstream logic; also preserve the empty-response
handling (204 -> {}).
- Around line 88-97: The current DELETE handling calls response.json() which
throws on empty 204 bodies; update the handler around the fetch/response logic
so that you only call response.json() for non-empty responses (e.g., check
response.status === 204 or check Content-Length/Content-Type/response.headers
for an empty body) and return an appropriate empty NextResponse (for example
NextResponse.json(null, { status: response.status }) or NextResponse.json({}, {
status: response.status })) when the body is empty; keep using the existing
symbols (response from fetch and NextResponse.json) and ensure you still pass
through response.status.

In `@app/api/guardrails/validators/configs/route.ts`:
- Around line 50-52: The current parsing assumes JSON and calls JSON.parse(text)
directly which throws on plain-text/HTML errors; change the parsing around the
response handling near the response/text/data variables so it attempts to parse
JSON inside a try/catch (or use response.headers.get('content-type') to check
for application/json) and on failure falls back to returning the raw text (or an
object like { success: true, raw: text }) while preserving non-JSON status codes
and headers; ensure empty responses (204) still resolve to a sensible default
(e.g., { success: true }) and update any downstream uses of data accordingly.

In `@app/components/prompt-editor/ConfigEditorPane.tsx`:
- Around line 168-173: The POST payload construction for the ban list omits
newBanList.description and the cancel/reset logic clears the description key,
causing loss of user input and potential uncontrolled-to-controlled React input
errors; update the payload object (where payload is created) to include
description: newBanList.description, and adjust the reset/cancel handler that
resets newBanList (e.g., setNewBanList or resetNewBanList) so it preserves or
explicitly restores the description field (or resets it to an empty string) to
keep the input controlled and ensure the description is round-tripped.
- Around line 30-31: The remove/save callbacks cause a race because they are not
awaited; update the prop signatures to return Promise<void> (change
onSaveValidator?: (validator: Validator) => Promise<void>; and
onRemoveValidator?: (index: number) => Promise<void>), update callers to return
promises, and then in the edit flow inside ConfigEditorPane (where
onRemoveValidator(...) and onSaveValidator(...) are invoked) call and await them
sequentially (e.g., await onRemoveValidator(index) then await
onSaveValidator(newValidator)) so the network handlers execute in order.

In `@app/configurations/prompt-editor/page.tsx`:
- Around line 92-99: The new VALIDATOR_TYPE_TO_ID mapping adds 'topic-relevance'
but there is no corresponding metadata in AVAILABLE_VALIDATORS, so
loadGuardrailsFromConfig() will skip those validators; add an entry for the
'topic-relevance' validator to AVAILABLE_VALIDATORS in
app/components/prompt-editor/ValidatorListPane.tsx (or update
loadGuardrailsFromConfig() to accept the new ID) with the same shape/fields as
other validators (id 'topic-relevance' and required display/label/controls
metadata) so that validators mapped from VALIDATOR_TYPE_TO_ID are found and
retained during load/save.
- Around line 87-90: The editor's dirty-check currently ignores guardrail
validator changes: include savedValidators in the unsaved-change calculation so
adding/removing validators flips hasUnsavedChanges; update the function or
effect that computes hasUnsavedChanges (referencing hasUnsavedChanges,
setHasUnsavedChanges or its computing effect) to compare the current
savedValidators array (and selectedValidator where relevant) against the
persisted blob/initial state, and ensure any mutations to savedValidators
(add/remove/edit) call the same comparer or trigger the effect so the header
correctly shows dirty state; mirror this change for the other dirty-check logic
referenced around the second block (the identical logic covering the 266-297
region).

---

Duplicate comments:
In `@app/components/prompt-editor/ConfigEditorPane.tsx`:
- Around line 1338-1346: The UI and save logic treat a threshold of 0 as falsy
because the code uses || fallbacks, causing values of 0 to be overwritten by
0.8; update the places that use "validatorConfig.threshold || 0.8" to use the
nullish coalescing operator "validatorConfig.threshold ?? 0.8" (both where
rendering "Threshold: ..." and where setting the input value, and likewise
change the same pattern at the other occurrence around the validatorConfig usage
at the later block referenced) so legitimate zero thresholds are preserved; keep
using parseFloat(e.target.value) when calling setValidatorConfig to set the
threshold.

In `@app/components/prompt-editor/ValidatorListPane.tsx`:
- Around line 76-94: The button that renders each validator in
ValidatorListPane.tsx should expose selection to assistive tech; update the
button (where validator.id, onSelectValidator, and selectedValidator are used)
to include an accessibility attribute such as aria-pressed={selectedValidator
=== validator.id} or aria-current="true" when selected (and/or aria-selected if
the container is a listbox/option) so screen readers announce which item is
active; keep the existing onClick/onMouse handlers and styling but add the
appropriate aria attribute(s) and ensure the container has the correct list
semantics (role="listbox"/role="option") if using aria-selected.

---

Nitpick comments:
In `@app/api/guardrails/ban_lists/`[ban_list_id]/route.ts:
- Line 1: Replace the native Request type with NextRequest for consistency:
update the import to bring in NextRequest alongside NextResponse and change the
handler signatures named GET, PUT, and DELETE to accept NextRequest instead of
Request; ensure any places that construct or type-check the request (e.g.,
parameter names referenced in GET/PUT/DELETE in this file) are updated to use
NextRequest so you retain Next.js-specific utilities and keep parity with the
sibling ban_lists/route.ts.
- Line 22: Log prefixes in route.ts are inconsistent: replace all occurrences of
the singular prefix strings like "[GET /api/guardrails/ban_list/[ban_list_id]]"
and "[DELETE /api/guardrails/ban_list/[ban_list_id]]" with the plural form "[GET
/api/guardrails/ban_lists/[ban_list_id]]" and "[DELETE
/api/guardrails/ban_lists/[ban_list_id]]" respectively so all console.log calls
in the GET and DELETE handlers (and any other console.log lines referenced) use
the standardized "ban_lists" path; search for the exact log text snippets to
locate and update them within the GET handler, DELETE handler and any helper
logs in route.ts.

In `@app/api/guardrails/ban_lists/route.ts`:
- Line 18: Update all console.log prefixes in the GET and POST handlers in
route.ts to use the plural "ban_lists" to match the actual route path; find the
log statements inside the GET function (e.g., the console.log that currently
prints "[GET /api/guardrails/ban_list] Forwarding to:") and the POST function
loggers and change their prefix to "[GET /api/guardrails/ban_lists]" or "[POST
/api/guardrails/ban_lists]" respectively — apply the same rename to every
occurrence (the statements referenced around the existing console.log calls in
the GET/POST handlers).
- Line 35: The console.log statements in route.ts that print full
response/request payloads (e.g., the '[GET /api/guardrails/ban_list] Backend
response data:' log) are too verbose for production; update these to either
guard them behind a debug flag (process.env.NODE_ENV !== 'production' or a
dedicated DEBUG flag) or redact sensitive fields before logging (omit or mask
request/response body properties) and replace raw JSON.stringify calls with the
guarded/redacting logger so the same unique console.log instances in this file
no longer emit full payloads in production.

In `@app/api/guardrails/validators/configs/route.ts`:
- Around line 35-36: The two console.log calls that print full validator
payloads in the route handler (the logs at the start of the POST
/api/guardrails/validators/configs handler and the similar ones around lines
48-54) must be gated behind a development-only check or replaced with a
debug-level logger; update the route code (the POST handler function) to only
emit the verbose JSON body and forwarding URL when running in development (e.g.,
process.env.NODE_ENV === 'development') or call logger.debug/trace instead of
console.log, and keep only status-level logs (info/warn/error) in production.

In `@app/components/ConfigCard.tsx`:
- Around line 115-144: In ConfigCard remove the render-time side effect by
deleting the console.log and the IIFE wrapper around the badge render; instead
directly inline the conditional JSX that checks latestVersion.hasGuardrails (and
optionally latestVersion.guardrailsCount) to render the badge element, keeping
the same classes/styles and SVG; this makes the component pure—look for the
anonymous IIFE around latestVersion.hasGuardrails and the debug log and replace
with a simple conditional expression inside the JSX returned by ConfigCard.

In `@app/keystore/page.tsx`:
- Around line 370-386: The copy button currently calls onCopy(apiKey.key) and
only copies the API key; add UI and wiring to also copy the guardrails token by
either rendering a second button next to the token display that calls
onCopy(guardrailsToken) (or apiKey.guardrailsToken if that is the field) or
extend onCopy to accept a second optional value and render two buttons that call
onCopy(apiKey.key) and onCopy(guardrailsToken) respectively; update any
tooltip/title text to distinguish "Copy API key" vs "Copy guardrails token" so
users can copy either value from the same row.

In `@app/lib/useConfigs.ts`:
- Around line 469-483: The timezone-detection branch in formatRelativeTime is
fragile; replace the current conditional (checking .endsWith('Z'),
includes('+'), and parsing 'T' parts) with a single robust regex test that
detects any explicit timezone indicator (trailing Z or z, or an offset like
+HH[:MM] or -HH[:MM], or a space followed by an offset), and if the regex does
not match append 'Z' before constructing new Date; update the logic inside
formatRelativeTime to use this regex-based check so it correctly handles
lowercase 'z', space-separated timestamps, and common offset formats.
- Around line 115-121: Remove the debug console.log line ('[DEBUG
flattenConfigVersion] ...') from flattenConfigVersion and avoid leaving ad-hoc
debug prints in production; if you need runtime debugging gate it behind a debug
flag. Replace the (blob as any) assertions by updating the config blob type: add
optional properties input_guardrails?: unknown[] and output_guardrails?:
unknown[] to the ConfigVersionPublic/config_blob type in configTypes.ts, then
cast/annotate blob with that type and access blob.input_guardrails and
blob.output_guardrails directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 16975b96-1f8c-4a89-89da-70e36d5baac9

📥 Commits

Reviewing files that changed from the base of the PR and between 855281a and 2455ed6.

📒 Files selected for processing (11)
  • app/api/apikeys/verify/route.ts
  • app/api/guardrails/ban_lists/[ban_list_id]/route.ts
  • app/api/guardrails/ban_lists/route.ts
  • app/api/guardrails/validators/configs/[config_id]/route.ts
  • app/api/guardrails/validators/configs/route.ts
  • app/components/ConfigCard.tsx
  • app/components/prompt-editor/ConfigEditorPane.tsx
  • app/components/prompt-editor/ValidatorListPane.tsx
  • app/configurations/prompt-editor/page.tsx
  • app/keystore/page.tsx
  • app/lib/useConfigs.ts

@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from 'next/server';

const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000';
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 | 🟠 Major

Default backend port is inconsistent with sibling guardrails routes.

Line 3 falls back to http://localhost:8000, while other proxy routes use http://localhost:8001. This can silently break local verification when NEXT_PUBLIC_BACKEND_URL is unset.

🔧 Proposed fix
-const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000';
+const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8001';
📝 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
const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000';
const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8001';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/apikeys/verify/route.ts` at line 3, The fallback backend URL in
route.ts uses the constant backendUrl set to 'http://localhost:8000', which is
inconsistent with sibling guardrails routes; update the backendUrl default to
'http://localhost:8001' so local verification uses the same proxy port when
NEXT_PUBLIC_BACKEND_URL is unset, and scan other proxy/guardrails routes to
ensure they all use the 8001 fallback for consistency.

Comment on lines +27 to +30
// Handle empty responses (204 No Content, etc.)
const text = await response.text();
const data = text ? JSON.parse(text) : {};

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 | 🟠 Major

Guard JSON parsing to avoid false 500s on non-JSON backend bodies.

At Line 29, JSON.parse(text) can throw for plain-text/error HTML responses, causing this route to return a proxy failure instead of the backend status.

🔧 Proposed fix
-    const text = await response.text();
-    const data = text ? JSON.parse(text) : {};
+    const text = await response.text();
+    let data: unknown = {};
+    if (text) {
+      try {
+        data = JSON.parse(text);
+      } catch {
+        data = { message: text };
+      }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle empty responses (204 No Content, etc.)
const text = await response.text();
const data = text ? JSON.parse(text) : {};
// Handle empty responses (204 No Content, etc.)
const text = await response.text();
let data: unknown = {};
if (text) {
try {
data = JSON.parse(text);
} catch {
data = { message: text };
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/apikeys/verify/route.ts` around lines 27 - 30, The current code calls
JSON.parse(text) unguarded which will throw on non-JSON backend bodies and turn
valid backend errors into 500s; update the parsing in the route handler to only
parse when the response is JSON (check response.headers.get('content-type') for
'application/json' or 'application/hal+json') and fallback to returning the raw
text (or an empty object) when not JSON, or wrap JSON.parse in a try/catch and
use the caught error to fall back to the raw text; adjust the variables `text`
and `data` in route.ts (the response handling block) so downstream logic
consumes a safe `data` value without letting parse errors propagate.

Comment on lines +39 to +41
return NextResponse.json(
{ error: 'Failed to forward request to backend', details: error.message },
{ status: 500 }
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 | 🟠 Major

Do not expose raw internal error details to clients.

Line 40 returns error.message directly, which can leak internal service/network details. Keep details in server logs and return a generic client-safe error payload.

🔧 Proposed fix
     return NextResponse.json(
-      { error: 'Failed to forward request to backend', details: error.message },
+      { error: 'Failed to forward request to backend' },
       { status: 500 }
     );
📝 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
return NextResponse.json(
{ error: 'Failed to forward request to backend', details: error.message },
{ status: 500 }
return NextResponse.json(
{ error: 'Failed to forward request to backend' },
{ status: 500 }
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/apikeys/verify/route.ts` around lines 39 - 41, The response currently
returns raw error.message to clients from the NextResponse.json call in the
verify route; remove exposing internal error details by returning a generic
client-safe payload (e.g. { error: "Failed to forward request to backend" }) and
keep the full error in server logs instead (use the existing logger or
console.error to log error and any request context). Ensure the status remains
500, do not include error.message in the JSON, and optionally generate/log an
errorId if you want to correlate client errors with server logs.

Comment on lines +40 to +42
// Handle empty responses (204 No Content, etc.)
const text = await response.text();
const data = text ? JSON.parse(text) : {};
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 | 🟠 Major

Handle non-JSON backend responses in the GET proxy.

This path has the same failure mode as the POST proxy: a plain-text/HTML upstream response will throw during JSON.parse and get rewritten as a local 500 instead of preserving the backend status.

🛠️ Safer response parsing
-    const text = await response.text();
-    const data = text ? JSON.parse(text) : {};
+    const text = await response.text();
+    let data: any = {};
+
+    if (text) {
+      try {
+        data = JSON.parse(text);
+      } catch {
+        data = { error: text };
+      }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle empty responses (204 No Content, etc.)
const text = await response.text();
const data = text ? JSON.parse(text) : {};
// Handle empty responses (204 No Content, etc.)
const text = await response.text();
let data: any = {};
if (text) {
try {
data = JSON.parse(text);
} catch {
data = { error: text };
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/guardrails/validators/configs/`[config_id]/route.ts around lines 40 -
42, The GET proxy currently does "const text = await response.text(); const data
= text ? JSON.parse(text) : {}" which throws on non-JSON upstream bodies; wrap
the parsing in a try/catch so non-JSON (plain text/HTML) falls back to returning
the raw text body and original response status/headers instead of throwing.
Concretely: after reading response.text() (variable text), attempt
JSON.parse(text) inside try to produce data, but on parse error return a
Response(text, { status: response.status, headers: response.headers }) (or set
content-type to text/plain/html as appropriate) and only use parsed JSON for
downstream logic; also preserve the empty-response handling (204 -> {}).

Comment on lines +88 to +97
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Authorization': authHeader,
'Content-Type': 'application/json',
},
});

const data = await response.json();
return NextResponse.json(data, { status: response.status });
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 | 🟠 Major

Successful 204 deletes will currently be turned into failures.

response.json() throws on empty bodies, which is a common DELETE success response. That sends a successful backend delete down the catch path as a 500.

🛠️ Preserve empty-body DELETE responses
-    const data = await response.json();
-    return NextResponse.json(data, { status: response.status });
+    const text = await response.text();
+    let data: any = {};
+
+    if (text) {
+      try {
+        data = JSON.parse(text);
+      } catch {
+        data = { error: text };
+      }
+    }
+
+    if (response.status === 204) {
+      return new NextResponse(null, { status: 204 });
+    }
+
+    return NextResponse.json(data, { status: response.status });
📝 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
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Authorization': authHeader,
'Content-Type': 'application/json',
},
});
const data = await response.json();
return NextResponse.json(data, { status: response.status });
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Authorization': authHeader,
'Content-Type': 'application/json',
},
});
const text = await response.text();
let data: any = {};
if (text) {
try {
data = JSON.parse(text);
} catch {
data = { error: text };
}
}
if (response.status === 204) {
return new NextResponse(null, { status: 204 });
}
return NextResponse.json(data, { status: response.status });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/guardrails/validators/configs/`[config_id]/route.ts around lines 88 -
97, The current DELETE handling calls response.json() which throws on empty 204
bodies; update the handler around the fetch/response logic so that you only call
response.json() for non-empty responses (e.g., check response.status === 204 or
check Content-Length/Content-Type/response.headers for an empty body) and return
an appropriate empty NextResponse (for example NextResponse.json(null, { status:
response.status }) or NextResponse.json({}, { status: response.status })) when
the body is empty; keep using the existing symbols (response from fetch and
NextResponse.json) and ensure you still pass through response.status.

Comment on lines +30 to +31
onSaveValidator?: (validator: Validator) => void;
onRemoveValidator?: (index: number) => void;
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "ConfigEditorPane.tsx" -type f

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 127


🏁 Script executed:

wc -l ./app/components/prompt-editor/ConfigEditorPane.tsx

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 132


🏁 Script executed:

sed -n '25,35p' ./app/components/prompt-editor/ConfigEditorPane.tsx

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 432


🏁 Script executed:

sed -n '1400,1420p' ./app/components/prompt-editor/ConfigEditorPane.tsx

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 1005


🏁 Script executed:

rg "ConfigEditorPane" --type tsx --type ts -l

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 103


🏁 Script executed:

rg "ConfigEditorPane" -l

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 167


🏁 Script executed:

cat -n app/configurations/prompt-editor/page.tsx

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 40221


🏁 Script executed:

sed -n '1398,1425p' ./app/components/prompt-editor/ConfigEditorPane.tsx

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 1364


🏁 Script executed:

sed -n '1370,1430p' ./app/components/prompt-editor/ConfigEditorPane.tsx

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 3450


🏁 Script executed:

sed -n '1375,1395p' ./app/components/prompt-editor/ConfigEditorPane.tsx

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 1584


🏁 Script executed:

sed -n '1280,1310p' ./app/components/prompt-editor/ConfigEditorPane.tsx

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 1706


🏁 Script executed:

sed -n '1340,1365p' ./app/components/prompt-editor/ConfigEditorPane.tsx

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 1367


Race condition: ensure validator remove and save execute sequentially.

When editing a validator, onRemoveValidator(...) and onSaveValidator(...) are called back-to-back without awaiting. The parent handlers are async network calls that execute concurrently, so the final state depends on promise resolution order—this can reintroduce the old validator or drop the new one.

🛠️ Make callbacks awaitable and await them in sequence
-  onSaveValidator?: (validator: Validator) => void;
-  onRemoveValidator?: (index: number) => void;
+  onSaveValidator?: (validator: Validator) => Promise<void> | void;
+  onRemoveValidator?: (index: number) => Promise<void> | void;
-                  <button
-                    onClick={() => {
+                  <button
+                    onClick={async () => {
@@
-                        if (editingValidatorIndex !== null && onRemoveValidator) {
-                          onRemoveValidator(editingValidatorIndex);
+                        if (editingValidatorIndex !== null && onRemoveValidator) {
+                          await onRemoveValidator(editingValidatorIndex);
                         }
@@
-                        onSaveValidator({
+                        await onSaveValidator?.({
                           id: validator.id,
                           name: validator.name,
                           description: validator.description,
                           tags: validator.tags,
                           config: finalConfig
-                        });
+                        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/prompt-editor/ConfigEditorPane.tsx` around lines 30 - 31, The
remove/save callbacks cause a race because they are not awaited; update the prop
signatures to return Promise<void> (change onSaveValidator?: (validator:
Validator) => Promise<void>; and onRemoveValidator?: (index: number) =>
Promise<void>), update callers to return promises, and then in the edit flow
inside ConfigEditorPane (where onRemoveValidator(...) and onSaveValidator(...)
are invoked) call and await them sequentially (e.g., await
onRemoveValidator(index) then await onSaveValidator(newValidator)) so the
network handlers execute in order.

Comment on lines +1477 to +1495
{!isEditMode && (
<button
onClick={() => setShowVersionWarningModal(true)}
className="px-3 py-1.5 rounded-md text-xs font-medium flex items-center gap-1.5"
style={{
backgroundColor: colors.bg.primary,
color: colors.text.primary,
border: `1px solid ${colors.border}`,
cursor: 'pointer',
transition: 'all 0.15s ease',
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = colors.bg.secondary}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = colors.bg.primary}
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Edit
</button>
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 | 🟠 Major

Edit mode never loads an existing validator.

The global Edit button only flips isEditMode. None of the saved-validator rows ever set editingValidatorIndex, editingValidatorId, or validatorConfig, so the form above stays in create mode and existing validators cannot actually be edited.

Also applies to: 1508-1624

Comment on lines +87 to +90
// Guardrails state
const [isGuardrailsMode, setIsGuardrailsMode] = useState<boolean>(false);
const [selectedValidator, setSelectedValidator] = useState<string | null>(null);
const [savedValidators, setSavedValidators] = useState<Validator[]>([]);
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 | 🟠 Major

Guardrail-only edits do not mark the editor dirty.

savedValidators now changes the persisted blob, but hasUnsavedChanges still ignores that state. Adding/removing validators can leave the header showing a clean editor even though Save would create a new version.

Also applies to: 266-297

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

In `@app/configurations/prompt-editor/page.tsx` around lines 87 - 90, The editor's
dirty-check currently ignores guardrail validator changes: include
savedValidators in the unsaved-change calculation so adding/removing validators
flips hasUnsavedChanges; update the function or effect that computes
hasUnsavedChanges (referencing hasUnsavedChanges, setHasUnsavedChanges or its
computing effect) to compare the current savedValidators array (and
selectedValidator where relevant) against the persisted blob/initial state, and
ensure any mutations to savedValidators (add/remove/edit) call the same comparer
or trigger the effect so the header correctly shows dirty state; mirror this
change for the other dirty-check logic referenced around the second block (the
identical logic covering the 266-297 region).

Comment on lines +92 to +99
// Mapping from backend validator type to frontend validator id
const VALIDATOR_TYPE_TO_ID: Record<string, string> = {
'pii_remover': 'detect-pii',
'uli_slur_match': 'lexical-slur-match',
'gender_assumption_bias': 'gender-assumption-bias',
'ban_list': 'ban-list',
'topic_relevance': 'topic-relevance',
};
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 | 🟠 Major

topic_relevance validators will be silently dropped on load.

This mapping introduces topic-relevance, but AVAILABLE_VALIDATORS in app/components/prompt-editor/ValidatorListPane.tsx has no metadata for that ID. loadGuardrailsFromConfig() then skips it at the lookup here, so those backend validators never reach savedValidators and will disappear on the next save.

Also applies to: 744-769

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

In `@app/configurations/prompt-editor/page.tsx` around lines 92 - 99, The new
VALIDATOR_TYPE_TO_ID mapping adds 'topic-relevance' but there is no
corresponding metadata in AVAILABLE_VALIDATORS, so loadGuardrailsFromConfig()
will skip those validators; add an entry for the 'topic-relevance' validator to
AVAILABLE_VALIDATORS in app/components/prompt-editor/ValidatorListPane.tsx (or
update loadGuardrailsFromConfig() to accept the new ID) with the same
shape/fields as other validators (id 'topic-relevance' and required
display/label/controls metadata) so that validators mapped from
VALIDATOR_TYPE_TO_ID are found and retained during load/save.

@nishika26 nishika26 changed the title Feature/guardrails UI Guardrails UI: include in config editor Mar 16, 2026
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: 1

♻️ Duplicate comments (4)
app/components/prompt-editor/ConfigEditorPane.tsx (3)

1322-1329: ⚠️ Potential issue | 🟡 Minor

Use nullish coalescing for threshold defaults.

Line [1322], Line [1329], and Line [1384] still use ||, so 0 is treated as falsy and replaced by 0.8. Switch to ?? to preserve valid zero thresholds.

💡 Minimal fix
- Threshold: {validatorConfig.threshold || 0.8}
+ Threshold: {validatorConfig.threshold ?? 0.8}

- value={validatorConfig.threshold || 0.8}
+ value={validatorConfig.threshold ?? 0.8}

- threshold: validatorConfig.threshold || 0.8,
+ threshold: validatorConfig.threshold ?? 0.8,

Also applies to: 1384-1385

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

In `@app/components/prompt-editor/ConfigEditorPane.tsx` around lines 1322 - 1329,
Replace uses of the logical OR fallback with the nullish coalescing operator so
valid zero thresholds are preserved: change occurrences where the component
accesses validatorConfig.threshold (in ConfigEditorPane, e.g., the Threshold
label text, the range input value, and any other uses like the one near lines
referenced) from using "validatorConfig.threshold || 0.8" to
"validatorConfig.threshold ?? 0.8"; ensure all similar fallbacks for threshold
use "??" rather than "||" so 0 is not treated as falsy.

1462-1480: ⚠️ Potential issue | 🟠 Major

Edit mode is enabled, but no saved validator can be loaded into the form.

Line [1464] only toggles isEditMode. In the saved validator rows (Line [1493]+ and Line [1556]+), there is no action that sets editingValidatorIndex, editingValidatorId, and validatorConfig, so the form never enters a real edit-populated state.

Also applies to: 1493-1541, 1556-1604

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

In `@app/components/prompt-editor/ConfigEditorPane.tsx` around lines 1462 - 1480,
The Edit button currently only toggles isEditMode; update its onClick handler
(the button that calls setIsEditMode(true)) so it also sets
editingValidatorIndex, editingValidatorId, and validatorConfig to the selected
saved validator's values before enabling edit mode; locate the saved validator
rows (the render blocks around the saved validator items referenced by
editingValidatorIndex/editingValidatorId/validatorConfig) and ensure their Edit
action passes the correct validator object/index into that handler so the form
is populated with the saved validator data when isEditMode is true.

30-31: ⚠️ Potential issue | 🟠 Major

Validator update is non-atomic and can overwrite state.

At Line [1389] and Line [1394], edit flow performs remove and save as two separate mutations. With parent handlers using separate setSavedValidators(...) calls (app/configurations/prompt-editor/page.tsx Line [568] and Line [649]), this can reintroduce old entries or drop replacement depending on update order. Use a single replace callback (or a single functional state update path) for edit operations.

Also applies to: 1388-1401

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

In `@app/components/prompt-editor/ConfigEditorPane.tsx` around lines 30 - 31, The
edit flow is doing remove + save as two separate mutations via
onRemoveValidator/onSaveValidator which can race and overwrite state; change to
an atomic replace/update callback so the parent updates savedValidators with a
single functional update. Specifically, replace the separate onRemoveValidator +
onSaveValidator calls with a single onUpdateValidator (or a replaceValidator
signature) that accepts (index: number, validator: Validator) and ensure the
parent uses setSavedValidators(prev => { const copy = [...prev]; copy[index] =
validator; return copy; }) so the replacement happens in one functional state
update; alternatively have the parent expose a single callback that performs the
replace internally rather than two separate handlers.
app/components/prompt-editor/ValidatorListPane.tsx (1)

76-79: ⚠️ Potential issue | 🟡 Minor

Expose selected state semantically on validator buttons.

Line [76] still relies on visual styling to show selection. Please add semantic state (e.g., aria-pressed/aria-current) so assistive tech can announce the active validator.

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

In `@app/components/prompt-editor/ValidatorListPane.tsx` around lines 76 - 79, The
validator buttons in ValidatorListPane.tsx rely only on visual styling for
selection; update the button rendering (the element using onSelectValidator and
key={validator.id}) to expose semantic selected state by adding an appropriate
ARIA attribute (e.g., aria-pressed={selectedValidatorId === validator.id} or
aria-current={selectedValidatorId === validator.id}), using the component's
selected state/prop (selectedValidatorId or equivalent) to compute the boolean
so assistive tech can announce the active validator.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/components/prompt-editor/ConfigEditorPane.tsx`:
- Around line 1083-1087: The "On Fail Action" value from validatorConfig isn't
being persisted because finalConfig built in ConfigEditorPane.tsx (where
finalConfig is constructed around lines ~1358-1363) omits
validatorConfig.on_fail; update the finalConfig construction to include the
on_fail property (e.g., include validator: { ...validatorConfig, on_fail:
validatorConfig.on_fail } or add on_fail directly) so that the value set via the
input (which updates validatorConfig via setValidatorConfig) is saved with the
rest of the config.

---

Duplicate comments:
In `@app/components/prompt-editor/ConfigEditorPane.tsx`:
- Around line 1322-1329: Replace uses of the logical OR fallback with the
nullish coalescing operator so valid zero thresholds are preserved: change
occurrences where the component accesses validatorConfig.threshold (in
ConfigEditorPane, e.g., the Threshold label text, the range input value, and any
other uses like the one near lines referenced) from using
"validatorConfig.threshold || 0.8" to "validatorConfig.threshold ?? 0.8"; ensure
all similar fallbacks for threshold use "??" rather than "||" so 0 is not
treated as falsy.
- Around line 1462-1480: The Edit button currently only toggles isEditMode;
update its onClick handler (the button that calls setIsEditMode(true)) so it
also sets editingValidatorIndex, editingValidatorId, and validatorConfig to the
selected saved validator's values before enabling edit mode; locate the saved
validator rows (the render blocks around the saved validator items referenced by
editingValidatorIndex/editingValidatorId/validatorConfig) and ensure their Edit
action passes the correct validator object/index into that handler so the form
is populated with the saved validator data when isEditMode is true.
- Around line 30-31: The edit flow is doing remove + save as two separate
mutations via onRemoveValidator/onSaveValidator which can race and overwrite
state; change to an atomic replace/update callback so the parent updates
savedValidators with a single functional update. Specifically, replace the
separate onRemoveValidator + onSaveValidator calls with a single
onUpdateValidator (or a replaceValidator signature) that accepts (index: number,
validator: Validator) and ensure the parent uses setSavedValidators(prev => {
const copy = [...prev]; copy[index] = validator; return copy; }) so the
replacement happens in one functional state update; alternatively have the
parent expose a single callback that performs the replace internally rather than
two separate handlers.

In `@app/components/prompt-editor/ValidatorListPane.tsx`:
- Around line 76-79: The validator buttons in ValidatorListPane.tsx rely only on
visual styling for selection; update the button rendering (the element using
onSelectValidator and key={validator.id}) to expose semantic selected state by
adding an appropriate ARIA attribute (e.g., aria-pressed={selectedValidatorId
=== validator.id} or aria-current={selectedValidatorId === validator.id}), using
the component's selected state/prop (selectedValidatorId or equivalent) to
compute the boolean so assistive tech can announce the active validator.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 73b6f9d4-8a40-4d9e-8df6-306ef240a804

📥 Commits

Reviewing files that changed from the base of the PR and between 2455ed6 and 9690443.

📒 Files selected for processing (2)
  • app/components/prompt-editor/ConfigEditorPane.tsx
  • app/components/prompt-editor/ValidatorListPane.tsx

Comment on lines +1083 to +1087
<input
type="text"
value={validatorConfig.on_fail || 'Fix'}
onChange={(e) => setValidatorConfig({ ...validatorConfig, on_fail: e.target.value })}
className="w-full px-3 py-2 rounded-md text-sm focus:outline-none"
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 | 🟠 Major

Lexical Slur Match “On Fail Action” input is not persisted.

Line [1085] writes validatorConfig.on_fail, but Line [1358]-[1363] builds finalConfig without including any on-fail field. User input is dropped on save.

💡 Minimal fix
- value={validatorConfig.on_fail || 'Fix'}
- onChange={(e) => setValidatorConfig({ ...validatorConfig, on_fail: e.target.value })}
+ value={validatorConfig.on_fail_action ?? 'fix'}
+ onChange={(e) => setValidatorConfig({ ...validatorConfig, on_fail_action: e.target.value })}

  finalConfig = {
    type: validatorConfig.type || 'uli_slur_match',
    stage: validatorConfig.stage || 'input',
+   on_fail_action: validatorConfig.on_fail_action ?? 'fix',
    languages: validatorConfig.languages || ['en', 'hi'],
    severity: validatorConfig.severity || 'all',
  };

Also applies to: 1358-1363

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

In `@app/components/prompt-editor/ConfigEditorPane.tsx` around lines 1083 - 1087,
The "On Fail Action" value from validatorConfig isn't being persisted because
finalConfig built in ConfigEditorPane.tsx (where finalConfig is constructed
around lines ~1358-1363) omits validatorConfig.on_fail; update the finalConfig
construction to include the on_fail property (e.g., include validator: {
...validatorConfig, on_fail: validatorConfig.on_fail } or add on_fail directly)
so that the value set via the input (which updates validatorConfig via
setValidatorConfig) is saved with the rest of the config.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Line 3: Add a trailing blank newline at the end of the .env.example file so it
ends with an empty line (i.e., after the existing GUARDRAIL_TOKEN= entry);
update the file to ensure the final character is a newline to satisfy
dotenv-linter.
- Line 3: Update the environment variable in the example to match the code:
replace GUARDRAIL_TOKEN with GUARDRAILS_TOKEN (the name the code reads) so the
runtime finds the expected variable; look for occurrences of GUARDRAIL_TOKEN and
change them to GUARDRAILS_TOKEN in the .env example (or alternatively change the
code references to GUARDRAIL_TOKEN if you prefer to standardize on the singular
name), ensuring the symbol GUARDRAILS_TOKEN used by the guardrails
validator/config routes is present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dd237dee-6865-45bf-97c6-494388174455

📥 Commits

Reviewing files that changed from the base of the PR and between 9690443 and 6a8493c.

📒 Files selected for processing (4)
  • .env.example
  • app/api/guardrails/validators/configs/[config_id]/route.ts
  • app/api/guardrails/validators/configs/route.ts
  • app/keystore/page.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/api/guardrails/validators/configs/[config_id]/route.ts
  • app/api/guardrails/validators/configs/route.ts

@@ -0,0 +1,3 @@
NEXT_PUBLIC_BACKEND_URL=

GUARDRAIL_TOKEN= No newline at end of file
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

Add blank line at the end of the file.

As per dotenv-linter, environment files should end with a blank line.

📝 Proposed fix
 GUARDRAILS_TOKEN=
+
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 3-3: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

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

In @.env.example at line 3, Add a trailing blank newline at the end of the
.env.example file so it ends with an empty line (i.e., after the existing
GUARDRAIL_TOKEN= entry); update the file to ensure the final character is a
newline to satisfy dotenv-linter.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check which form of the guardrails token variable is used in the codebase

echo "=== Searching for GUARDRAILS_TOKEN (plural) ==="
rg -n 'GUARDRAILS_TOKEN' --type=ts --type=js -C2

echo ""
echo "=== Searching for GUARDRAIL_TOKEN (singular) ==="
rg -n 'GUARDRAIL_TOKEN' --type=ts --type=js -C2

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 2381


🏁 Script executed:

cat -n .env.example | head -10

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 139


Environment variable name mismatch will cause runtime errors.

The .env.example file defines GUARDRAIL_TOKEN (singular), but the code reads GUARDRAILS_TOKEN (plural). This mismatch will cause guardrails validator/config routes to fail at runtime with "Missing GUARDRAILS_TOKEN environment variable" errors.

🔧 Proposed fix
-GUARDRAIL_TOKEN=
+GUARDRAILS_TOKEN=
📝 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
GUARDRAIL_TOKEN=
GUARDRAILS_TOKEN=
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 3-3: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

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

In @.env.example at line 3, Update the environment variable in the example to
match the code: replace GUARDRAIL_TOKEN with GUARDRAILS_TOKEN (the name the code
reads) so the runtime finds the expected variable; look for occurrences of
GUARDRAIL_TOKEN and change them to GUARDRAILS_TOKEN in the .env example (or
alternatively change the code references to GUARDRAIL_TOKEN if you prefer to
standardize on the singular name), ensuring the symbol GUARDRAILS_TOKEN used by
the guardrails validator/config routes is present.

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.

🧹 Nitpick comments (2)
app/keystore/page.tsx (2)

544-544: Minor: Extraneous blank line.

An extra blank line was added after the closing </div> of the API key input. Consider removing for consistency.

🧹 Suggested removal
             />
           </div>
-
         </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/keystore/page.tsx` at line 544, Remove the extraneous blank line inserted
after the closing </div> of the API key input in app/keystore/page.tsx (the
block that renders the API key input field); delete that empty line so the
markup is consistent and there are no unintended gaps in the rendered JSX.

7-7: Minor: Extraneous blank line.

An extra blank line was added before the "use client" directive. This doesn't affect functionality but is inconsistent with typical file formatting where the directive immediately follows the file-level comment block.

🧹 Suggested removal
  */
-
 "use client"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/keystore/page.tsx` at line 7, Remove the extraneous blank line that was
inserted before the "use client" directive so the directive immediately follows
the file-level comment block; locate the top of the file where the "use client"
directive appears and delete the stray empty line to restore the expected
formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/keystore/page.tsx`:
- Line 544: Remove the extraneous blank line inserted after the closing </div>
of the API key input in app/keystore/page.tsx (the block that renders the API
key input field); delete that empty line so the markup is consistent and there
are no unintended gaps in the rendered JSX.
- Line 7: Remove the extraneous blank line that was inserted before the "use
client" directive so the directive immediately follows the file-level comment
block; locate the top of the file where the "use client" directive appears and
delete the stray empty line to restore the expected formatting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d5fb5273-0f4a-4ffd-8bd4-4f8a3dc097d5

📥 Commits

Reviewing files that changed from the base of the PR and between 6a8493c and 4871f2a.

📒 Files selected for processing (3)
  • app/api/guardrails/validators/configs/[config_id]/route.ts
  • app/api/guardrails/validators/configs/route.ts
  • app/keystore/page.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/api/guardrails/validators/configs/[config_id]/route.ts
  • app/api/guardrails/validators/configs/route.ts

@nishika26
Copy link
Copy Markdown
Contributor Author

closing this PR since i made this branch over evals UI changes branch and not main which caused this PR to become a mess

@nishika26 nishika26 closed this Mar 17, 2026
@Ayush8923 Ayush8923 deleted the feature/guardrails_ui branch April 1, 2026 08:57
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