Skip to content

Guardrails UI: Configure and list validators#116

Open
nishika26 wants to merge 3 commits intomainfrom
feature/guardrail_ui
Open

Guardrails UI: Configure and list validators#116
nishika26 wants to merge 3 commits intomainfrom
feature/guardrail_ui

Conversation

@nishika26
Copy link
Copy Markdown
Contributor

@nishika26 nishika26 commented Apr 10, 2026

Target Issue is #48

Notes

  • New Features

    • Launched Guardrails feature with full configuration interface for managing validators and safety checks.
    • Added ability to create and manage ban lists for content filtering.
    • Integrated guardrails into prompt configuration with separate input and output rule management.
    • Added new Guardrails navigation menu item.
  • Chores

    • Added guardrails service configuration to environment setup.

Summary by CodeRabbit

  • New Features

    • Guardrails UI: full-config editor with saved validator configs, two-panel layout, and list management.
    • Ban lists: create/manage ban lists and select them in validator configs.
    • Prompt editor: attach input/output guardrails to prompts.
    • New UI: tooltip and multi-select components; new icon and sidebar entry for Guardrails.
  • Bug Fixes

    • Replaced placeholder "Coming Soon" with the live Guardrails page.
  • Chores

    • Added guardrails environment variables and backend proxy support.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

📝 Walkthrough

Walkthrough

Adds a Guardrails feature set: new Guardrails UI and components, guardrails API proxy routes and client helper, prompt-editor integration for guardrails, sidebar nav changes, new types and validators catalog, and small env/example update; removes the prior coming-soon placeholder page.

Changes

Cohort / File(s) Summary
Environment & API client
\.env\.example, app/lib/apiClient.ts
Added GUARDRAILS_URL/GUARDRAILS_TOKEN to example env and introduced guardrailsClient proxy helper; updated BACKEND_URL default.
Top-level Guardrails page
app/(main)/guardrails/page.tsx, app/(main)/coming-soon/guardrails/page.tsx
Removed placeholder coming-soon page; added new two-panel GuardrailsPage with org/project verification, validator catalog loading, saved-config CRUD, and toast notifications.
API proxy routes for Guardrails
app/api/apikeys/verify/route.ts, app/api/guardrails/route.ts, app/api/guardrails/validators/configs/route.ts, app/api/guardrails/validators/configs/[config_id]/route.ts, app/api/guardrails/ban_lists/route.ts, app/api/guardrails/ban_lists/[ban_list_id]/route.ts
Added Next.js API routes that proxy to the Guardrails backend (GET/POST/PATCH/DELETE) and an API key verification route; handlers forward auth, preserve org/project query params, and normalize errors/status.
Guardrails UI & types
app/components/guardrails/ValidatorConfigPanel.tsx, app/components/guardrails/BanListModal.tsx, app/components/guardrails/types.ts, app/components/guardrails/validators.json
New ValidatorConfigPanel (schema-driven fields, ban-list integration), BanListModal, typed validator interfaces, and validators catalog JSON.
Shared UI components
app/components/InfoTooltip.tsx, app/components/MultiSelect.tsx, app/components/icons/sidebar/ShieldCheckIcon.tsx, app/components/icons/index.tsx
Added InfoTooltip and MultiSelect components; added ShieldCheckIcon and re-export in icons barrel.
Prompt editor & integrations
app/components/prompt-editor/ConfigEditorPane.tsx, app/(main)/configurations/prompt-editor/page.tsx, app/lib/types/configs.ts
ConfigEditorPane now accepts apiKey prop and includes a GuardrailsSection (fetches validator configs using verified org/project query); prompt-editor page omits empty input_guardrails/output_guardrails from payloads; extended ConfigBlob types to include guardrail refs and optional prompt_template.
Sidebar/navigation
app/components/Sidebar.tsx
Inserted Guardrails nav item and other nav entries; note: navItems identifier is redeclared in the same scope (potential collision).

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant GuardrailsPage as Guardrails Page (client)
    participant APIVerify as /api/apikeys/verify
    participant APIProxy as /api/guardrails/validators/configs
    participant GuardrailsService as Guardrails Backend

    User->>GuardrailsPage: Open Guardrails page
    GuardrailsPage->>APIVerify: GET /api/apikeys/verify (X-API-KEY)
    APIVerify->>GuardrailsService: Forward verify request
    GuardrailsService-->>APIVerify: Return org/project
    APIVerify-->>GuardrailsPage: Return org/project

    GuardrailsPage->>APIProxy: GET /api/guardrails/validators/configs?organization_id=...&project_id=...
    APIProxy->>GuardrailsService: Forward with Authorization (or X-API-KEY)
    GuardrailsService-->>APIProxy: Return validator configs
    APIProxy-->>GuardrailsPage: Return configs

    User->>GuardrailsPage: Create/Update/Delete config
    GuardrailsPage->>APIProxy: POST/PATCH/DELETE (includes org/project)
    APIProxy->>GuardrailsService: Forward change request
    GuardrailsService-->>APIProxy: Return result
    APIProxy-->>GuardrailsPage: Return saved/deleted response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

ready-for-review

Suggested reviewers

  • AkhileshNegi

Poem

🥕 I hopped in with a shield and a curious nose,
Validators marching in tidy rows.
Ban lists and schemas, saved with a thump,
I nibble the bugs and give bad outputs a bump.
Hooray — safer prompts, one carrot-hop at a time!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.43% 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 clearly and accurately summarizes the main objective: introducing a guardrails UI with validator configuration and listing functionality, matching the substantial changes throughout the changeset.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/guardrail_ui

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

@nishika26 nishika26 self-assigned this Apr 10, 2026
@nishika26 nishika26 added the enhancement New feature or request label Apr 10, 2026
@nishika26 nishika26 linked an issue Apr 10, 2026 that may be closed by this pull request
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: 12

🧹 Nitpick comments (5)
app/api/guardrails/validators/configs/route.ts (1)

4-18: Consider extracting the shared guardrails proxy helper.

getAuthHeader, query forwarding, and the same try/catch -> NextResponse shape now exist in app/api/guardrails/route.ts, this file, and app/api/guardrails/validators/configs/[config_id]/route.ts. Pulling that into one helper will keep auth/error behavior from drifting as more guardrails endpoints land.

Also applies to: 20-61

🤖 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 4 - 18, Extract
the repeated Guardrails proxy logic (auth header creation, query param
forwarding, and the try/catch → NextResponse error/response shape) into a single
helper function (e.g., proxyToGuardrails or forwardGuardrailsRequest) and
replace duplicates in getAuthHeader, buildEndpoint and the route handlers in
app/api/guardrails/route.ts, app/api/guardrails/validators/configs/route.ts, and
app/api/guardrails/validators/configs/[config_id]/route.ts; the helper should
accept the incoming NextRequest and a target path (or accept optional
overrides), build the auth header (or accept the token), forward searchParams,
perform fetch to `/api/v1/guardrails...`, and return a normalized NextResponse
inside a try/catch so all endpoints reuse identical auth/error behavior.
app/(main)/guardrails/page.tsx (2)

134-150: Add confirmation before deleting a configuration.

The delete action immediately sends the DELETE request without asking the user to confirm. Consider adding a confirmation dialog to prevent accidental deletions.

♻️ Add confirmation dialog
 const handleDeleteConfig = async (configId: string) => {
   if (!configsQueryString) return;
+  if (!window.confirm("Are you sure you want to delete this configuration?")) {
+    return;
+  }
   try {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(main)/guardrails/page.tsx around lines 134 - 150, Add a user
confirmation step to handleDeleteConfig so deletion is not sent immediately:
before performing the fetch call (and before checking configsQueryString),
prompt the user (e.g., window.confirm or your app modal) to confirm deletion of
the configId; if the user cancels, return early, otherwise proceed with the
existing fetch, toast handling, handleClearForm() if selectedSavedConfig?.id
matches configId, and fetchSavedConfigs(); keep all existing error handling
intact.

77-77: Missing dependency in useEffect.

The toast object is used inside the effect but not listed in the dependency array. While toast methods are likely stable references, ESLint's exhaustive-deps rule may flag this.

♻️ Add toast to dependency array
-  }, [isHydrated, activeKey?.key]);
+  }, [isHydrated, activeKey?.key, toast]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(main)/guardrails/page.tsx at line 77, The useEffect that depends on
isHydrated and activeKey?.key also uses the toast object but doesn't include it
in the dependency array; update the effect's dependency array to include toast
(or, if you intentionally want to ignore it, explicitly silence exhaustive-deps
with a well-documented // eslint-disable-next-line comment). Locate the
useEffect that references isHydrated, activeKey?.key and toast in page.tsx and
either add toast to the dependencies or add a concise eslint-disable comment and
explanation to prevent false positives.
app/components/guardrails/types.ts (1)

39-44: Minor: formatValidatorName may produce unexpected output for edge cases.

The function works well for snake_case inputs but consider edge cases:

  • Empty string returns empty string (acceptable)
  • Single word like "pii" becomes "Pii" (may want "PII")
  • Already formatted "LLM Critic" becomes "Llm Critic"

This is acceptable if input is always guaranteed to be snake_case validator types from the API.

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

In `@app/components/guardrails/types.ts` around lines 39 - 44, The
formatValidatorName function can mangle already-formatted or acronym words;
update formatValidatorName to handle edge cases by: if the input contains
spaces, return it unchanged (preserve "LLM Critic"); otherwise split on '_' and
for each token preserve existing ALL CAPS tokens, convert short all-lowercase
tokens (length <= 3, e.g., "pii") to UPPERCASE, and title-case other tokens as
currently done; implement these checks inside formatValidatorName to avoid
breaking snake_case handling while preserving acronyms and pre-formatted names.
app/components/prompt-editor/ConfigEditorPane.tsx (1)

48-69: Consider adding error feedback for fetch failures.

The catch block on line 67 silently sets validators to an empty array. While this prevents UI crashes, the user won't know if the fetch failed due to a network issue or misconfiguration. Consider accepting an optional onError callback or logging the error.

♻️ Optional: Add error callback
 function GuardrailsSection({
   label,
   guardrails,
   onChange,
   apiKey,
   queryString,
   stage,
+  onError,
 }: {
   label: string;
   guardrails: GuardrailRef[];
   onChange: (refs: GuardrailRef[]) => void;
   apiKey: string;
   queryString: string | null;
   stage: "input" | "output";
+  onError?: (message: string) => void;
 }) {
   // ...
       .catch(() => {
         setValidators([]);
+        onError?.("Failed to load validators");
       })
🤖 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 48 - 69, The
fetch in the useEffect (which uses queryString and apiKey) currently swallows
errors in the .catch(() => setValidators([])) block; update this to accept an
optional onError callback prop (or at least log the error) and call it with the
caught Error before falling back to setValidators([]) and setLoading(false);
locate the fetch block in ConfigEditorPane.tsx (the useEffect where
setValidators and setLoading are used) and modify the catch to .catch((err) => {
if (onError) onError(err); else console.error('Failed to load guardrail
validators', err); setValidators([]); }) so callers can react to failures and
you still clear loading state.
🤖 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:
- Around line 3-5: Normalize the new environment entries by removing spaces
around the equals sign and trimming trailing whitespace so they conform to
dotenv syntax and pass dotenv-linter; specifically update the GUARDRAILS_URL and
GUARDRAILS_TOKEN lines (remove spaces before/after '=' and delete any trailing
spaces on GUARDRAILS_TOKEN) so the variables are defined as VAR=VALUE with no
extra whitespace.

In `@app/`(main)/configurations/prompt-editor/page.tsx:
- Around line 297-302: applyConfig currently rebuilds currentConfigBlob using
only the completion field, which drops existing input_guardrails and
output_guardrails when saving; update applyConfig to preserve those fields by
either spreading the existing currentConfigBlob or explicitly including
input_guardrails: currentConfigBlob.input_guardrails (when truthy) and
output_guardrails: currentConfigBlob.output_guardrails (when truthy) alongside
completion so loading an existing config doesn't remove guardrails on save.

In `@app/`(main)/guardrails/page.tsx:
- Around line 168-177: The PATCH payload currently strips validator-specific
fields by building body when isUpdate is true; update the body construction in
page.tsx (the isUpdate ? {...} branch) to include the dynamic validator fields
from configValues (e.g., ban_list_id, entity_types, and any other
validator-specific keys you support) so edits persist, or alternatively disable
those dynamic inputs when editing by gating their rendering when isUpdate is
true; adjust the code around the body variable and the form rendering to either
include the dynamic keys in the PATCH payload or hide/disable the dynamic fields
during updates.

In `@app/api/guardrails/ban_lists/`[ban_list_id]/route.ts:
- Line 10: The code interpolates ban_list_id directly into upstream paths (calls
to guardrailsClient) which breaks when IDs contain reserved URL characters;
update every place that builds the path (the guardrailsClient calls in route.ts)
to use an encoded ID by wrapping ban_list_id with encodeURIComponent before
interpolation so all three occurrences (the calls that construct
`/api/v1/guardrails/ban_lists/${ban_list_id}`) use the encoded value.

In `@app/api/guardrails/ban_lists/route.ts`:
- Around line 16-29: The POST handler currently treats JSON parse failures from
request.json() as 500; update the POST function to detect JSON parsing errors
(e.g., catch SyntaxError or check error.message from request.json()) and return
NextResponse.json({ error: "Malformed JSON" }, { status: 400 }) for those cases,
while preserving the existing 500 response for other unexpected errors;
specifically locate the POST function and modify the try/catch so parsing errors
from request.json() result in a 400 response and other errors still return 500.

In `@app/api/guardrails/validators/configs/`[config_id]/route.ts:
- Around line 57-62: The handlers (GET, PATCH, DELETE) call guardrailsClient and
always return NextResponse.json(data, { status }), which serializes null into a
message body for 204 No Content responses; change each handler (the
GET/PATCH/DELETE blocks that call guardrailsClient and buildEndpoint) to detect
a 204 response (e.g., status === 204 || (status && data === null)) and return an
empty response using NextResponse (or new Response) with no body and the same
status instead of NextResponse.json, otherwise continue to return
NextResponse.json(data, { status }).

In `@app/components/guardrails/BanListModal.tsx`:
- Around line 60-85: Wrap the modal container in proper dialog semantics and
programmatic labels: add role="dialog", aria-modal="true", and assign an id to
the title element (e.g., dialogTitle) then set aria-labelledby on the container
and aria-describedby pointing to the short description element; ensure the close
control has an accessible name (e.g., add aria-label="Close" to the close button
or use a visually hidden label) and keyboard focus handling if present; for all
form controls inside BanListModal, give each input a unique id and bind their
labels with htmlFor to those ids so labels are programmatically associated
(locate the title <h2>, the description <p>, the close button using onClose, and
all input/label pairs in BanListModal to apply these changes).

In `@app/components/guardrails/ValidatorConfigPanel.tsx`:
- Around line 104-112: The two useEffect hooks in ValidatorConfigPanel call
fetchBanLists and fetchBannedWords while relying on apiKey but don't list apiKey
in their dependency arrays; update the dependency arrays to include apiKey
(i.e., useEffect(() => { fetchBanLists(); }, [apiKey]) and useEffect(() => { ...
}, [value, apiKey])) and to satisfy exhaustive-deps, wrap fetchBanLists and
fetchBannedWords with useCallback keyed on apiKey (e.g., const fetchBanLists =
useCallback(..., [apiKey]) and const fetchBannedWords = useCallback(...,
[apiKey])) so the effects re-run when apiKey changes.
- Around line 371-391: The useEffect that initializes form state references the
local variable validator (and calls buildDefaultValues(validator.config) and
setFieldValues) but only lists selectedType, existingValues, and existingName in
its dependency array, which can lead to stale initialization when validators
change; update the dependency array to include validator (and if validator is
derived from an external validators array, include that array too) so the effect
reruns when the resolved validator changes, ensuring setFieldValues and other
setters use the correct config.

In `@app/components/InfoTooltip.tsx`:
- Around line 14-24: The InfoTooltip component currently only shows the tooltip
on mouse enter/leave; update the button to also handle keyboard and touch users
by wiring onFocus to setVisible(true) and onBlur to setVisible(false), add an
onClick handler to toggle visibility as a touch fallback, and add an onKeyDown
handler to close on Escape; ensure proper tooltip semantics by adding an id on
the tooltip element and connecting the button via aria-describedby (and
aria-expanded on the button) and marking the tooltip with role="tooltip" so
screen readers can discover it; keep using the existing setVisible state and the
same classnames/visual styling while only adding these handlers and ARIA
attributes in InfoTooltip.

In `@app/components/MultiSelect.tsx`:
- Around line 45-53: The div that toggles the dropdown (using setOpen and open
in MultiSelect) is not keyboard-accessible; replace or augment it with proper
combobox/button semantics: add tabindex={0} (or use a native <button>),
role="combobox" (or role="button") and aria-expanded={open} and
aria-haspopup="listbox", and implement an onKeyDown handler that toggles setOpen
when Enter or Space are pressed and handles Escape to close; ensure the same
changes are applied to the other trigger block (the second clickable region
around lines 104-139) so both mouse and keyboard users can open/close the
dropdown and focus moves appropriately.

In `@app/lib/apiClient.ts`:
- Around line 62-64: The code unconditionally calls JSON.parse on the response
body (variables: response, text, data), which will throw for non-JSON responses
and convert upstream errors into local exceptions; change this by wrapping
JSON.parse(text) in a try/catch and only set data to the parsed object on
success, otherwise set data to null (or keep the raw text) and
preserve/propagate the original response status and body so upstream errors
aren’t masked; update the logic around response.text(), JSON.parse, and the data
variable to safely handle non-JSON responses without throwing.

---

Nitpick comments:
In `@app/`(main)/guardrails/page.tsx:
- Around line 134-150: Add a user confirmation step to handleDeleteConfig so
deletion is not sent immediately: before performing the fetch call (and before
checking configsQueryString), prompt the user (e.g., window.confirm or your app
modal) to confirm deletion of the configId; if the user cancels, return early,
otherwise proceed with the existing fetch, toast handling, handleClearForm() if
selectedSavedConfig?.id matches configId, and fetchSavedConfigs(); keep all
existing error handling intact.
- Line 77: The useEffect that depends on isHydrated and activeKey?.key also uses
the toast object but doesn't include it in the dependency array; update the
effect's dependency array to include toast (or, if you intentionally want to
ignore it, explicitly silence exhaustive-deps with a well-documented //
eslint-disable-next-line comment). Locate the useEffect that references
isHydrated, activeKey?.key and toast in page.tsx and either add toast to the
dependencies or add a concise eslint-disable comment and explanation to prevent
false positives.

In `@app/api/guardrails/validators/configs/route.ts`:
- Around line 4-18: Extract the repeated Guardrails proxy logic (auth header
creation, query param forwarding, and the try/catch → NextResponse
error/response shape) into a single helper function (e.g., proxyToGuardrails or
forwardGuardrailsRequest) and replace duplicates in getAuthHeader, buildEndpoint
and the route handlers in app/api/guardrails/route.ts,
app/api/guardrails/validators/configs/route.ts, and
app/api/guardrails/validators/configs/[config_id]/route.ts; the helper should
accept the incoming NextRequest and a target path (or accept optional
overrides), build the auth header (or accept the token), forward searchParams,
perform fetch to `/api/v1/guardrails...`, and return a normalized NextResponse
inside a try/catch so all endpoints reuse identical auth/error behavior.

In `@app/components/guardrails/types.ts`:
- Around line 39-44: The formatValidatorName function can mangle
already-formatted or acronym words; update formatValidatorName to handle edge
cases by: if the input contains spaces, return it unchanged (preserve "LLM
Critic"); otherwise split on '_' and for each token preserve existing ALL CAPS
tokens, convert short all-lowercase tokens (length <= 3, e.g., "pii") to
UPPERCASE, and title-case other tokens as currently done; implement these checks
inside formatValidatorName to avoid breaking snake_case handling while
preserving acronyms and pre-formatted names.

In `@app/components/prompt-editor/ConfigEditorPane.tsx`:
- Around line 48-69: The fetch in the useEffect (which uses queryString and
apiKey) currently swallows errors in the .catch(() => setValidators([])) block;
update this to accept an optional onError callback prop (or at least log the
error) and call it with the caught Error before falling back to
setValidators([]) and setLoading(false); locate the fetch block in
ConfigEditorPane.tsx (the useEffect where setValidators and setLoading are used)
and modify the catch to .catch((err) => { if (onError) onError(err); else
console.error('Failed to load guardrail validators', err); setValidators([]); })
so callers can react to failures and you still clear loading state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7b2f52ae-9558-4390-b8eb-cac05ae0291a

📥 Commits

Reviewing files that changed from the base of the PR and between bc65a03 and 3bd304e.

📒 Files selected for processing (22)
  • .env.example
  • app/(main)/coming-soon/guardrails/page.tsx
  • app/(main)/configurations/prompt-editor/page.tsx
  • app/(main)/guardrails/page.tsx
  • 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/route.ts
  • app/api/guardrails/validators/configs/[config_id]/route.ts
  • app/api/guardrails/validators/configs/route.ts
  • app/components/InfoTooltip.tsx
  • app/components/MultiSelect.tsx
  • app/components/Sidebar.tsx
  • app/components/guardrails/BanListModal.tsx
  • app/components/guardrails/ValidatorConfigPanel.tsx
  • app/components/guardrails/types.ts
  • app/components/guardrails/validators.json
  • app/components/icons/index.tsx
  • app/components/icons/sidebar/ShieldCheckIcon.tsx
  • app/components/prompt-editor/ConfigEditorPane.tsx
  • app/lib/apiClient.ts
  • app/lib/types/configs.ts
💤 Files with no reviewable changes (1)
  • app/(main)/coming-soon/guardrails/page.tsx

.env.example Outdated
Comment on lines +3 to +5
#for guardrails
GUARDRAILS_URL = http://localhost:8001
GUARDRAILS_TOKEN =
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 new env entries before merging.

This hunk will still trip dotenv-linter because of the spaces around = and the trailing whitespace on GUARDRAILS_TOKEN.

🧹 Suggested cleanup
-#for guardrails
-GUARDRAILS_URL = http://localhost:8001
-GUARDRAILS_TOKEN = 
+# for guardrails
+GUARDRAILS_TOKEN=
+GUARDRAILS_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
#for guardrails
GUARDRAILS_URL = http://localhost:8001
GUARDRAILS_TOKEN =
# for guardrails
GUARDRAILS_TOKEN=
GUARDRAILS_URL=http://localhost:8001
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 4-4: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 5-5: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 5-5: [TrailingWhitespace] Trailing whitespace detected

(TrailingWhitespace)


[warning] 5-5: [UnorderedKey] The GUARDRAILS_TOKEN key should go before the GUARDRAILS_URL key

(UnorderedKey)

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

In @.env.example around lines 3 - 5, Normalize the new environment entries by
removing spaces around the equals sign and trimming trailing whitespace so they
conform to dotenv syntax and pass dotenv-linter; specifically update the
GUARDRAILS_URL and GUARDRAILS_TOKEN lines (remove spaces before/after '=' and
delete any trailing spaces on GUARDRAILS_TOKEN) so the variables are defined as
VAR=VALUE with no extra whitespace.

Comment on lines +297 to +302
...(currentConfigBlob.input_guardrails?.length && {
input_guardrails: currentConfigBlob.input_guardrails,
}),
...(currentConfigBlob.output_guardrails?.length && {
output_guardrails: currentConfigBlob.output_guardrails,
}),
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

Loading an existing config still strips guardrails on the next save.

These fields are written out here now, but applyConfig on Lines 96-111 still rebuilds currentConfigBlob with only completion. Opening a version that already has guardrails and then saving any other change will silently drop them.

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

In `@app/`(main)/configurations/prompt-editor/page.tsx around lines 297 - 302,
applyConfig currently rebuilds currentConfigBlob using only the completion
field, which drops existing input_guardrails and output_guardrails when saving;
update applyConfig to preserve those fields by either spreading the existing
currentConfigBlob or explicitly including input_guardrails:
currentConfigBlob.input_guardrails (when truthy) and output_guardrails:
currentConfigBlob.output_guardrails (when truthy) alongside completion so
loading an existing config doesn't remove guardrails on save.

) {
try {
const { ban_list_id } = await params;
const { status, data } = await guardrailsClient(request, `/api/v1/guardrails/ban_lists/${ban_list_id}`);
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

ban_list_id should be URL-encoded before building upstream paths.

Direct interpolation at Line 10, Line 27, and Line 46 can break routing for IDs containing reserved URL characters.

🔐 Suggested fix
   try {
     const { ban_list_id } = await params;
-    const { status, data } = await guardrailsClient(request, `/api/v1/guardrails/ban_lists/${ban_list_id}`);
+    const encodedId = encodeURIComponent(ban_list_id);
+    const { status, data } = await guardrailsClient(request, `/api/v1/guardrails/ban_lists/${encodedId}`);
   try {
     const { ban_list_id } = await params;
+    const encodedId = encodeURIComponent(ban_list_id);
     const body = await request.json();
-    const { status, data } = await guardrailsClient(request, `/api/v1/guardrails/ban_lists/${ban_list_id}`, {
+    const { status, data } = await guardrailsClient(request, `/api/v1/guardrails/ban_lists/${encodedId}`, {
       method: "PUT",
       body: JSON.stringify(body),
     });
   try {
     const { ban_list_id } = await params;
-    const { status, data } = await guardrailsClient(request, `/api/v1/guardrails/ban_lists/${ban_list_id}`, {
+    const encodedId = encodeURIComponent(ban_list_id);
+    const { status, data } = await guardrailsClient(request, `/api/v1/guardrails/ban_lists/${encodedId}`, {
       method: "DELETE",
     });

Also applies to: 27-27, 46-46

🤖 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 10, The code
interpolates ban_list_id directly into upstream paths (calls to
guardrailsClient) which breaks when IDs contain reserved URL characters; update
every place that builds the path (the guardrailsClient calls in route.ts) to use
an encoded ID by wrapping ban_list_id with encodeURIComponent before
interpolation so all three occurrences (the calls that construct
`/api/v1/guardrails/ban_lists/${ban_list_id}`) use the encoded value.

Comment on lines +16 to +29
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { status, data } = await guardrailsClient(request, "/api/v1/guardrails/ban_lists", {
method: "POST",
body: JSON.stringify(body),
});
return NextResponse.json(data, { status });
} catch (e: unknown) {
return NextResponse.json(
{ error: e instanceof Error ? e.message : String(e) },
{ 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 | 🟡 Minor

Malformed JSON requests are incorrectly reported as server errors.

At Line 18, request.json() parse failures currently fall through to the generic 500 response. This should return a 400 to reflect client input error.

✅ Suggested fix
 export async function POST(request: NextRequest) {
   try {
-    const body = await request.json();
+    let body: unknown;
+    try {
+      body = await request.json();
+    } catch {
+      return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
+    }
     const { status, data } = await guardrailsClient(request, "/api/v1/guardrails/ban_lists", {
       method: "POST",
       body: JSON.stringify(body),
     });
📝 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
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { status, data } = await guardrailsClient(request, "/api/v1/guardrails/ban_lists", {
method: "POST",
body: JSON.stringify(body),
});
return NextResponse.json(data, { status });
} catch (e: unknown) {
return NextResponse.json(
{ error: e instanceof Error ? e.message : String(e) },
{ status: 500 },
);
}
export async function POST(request: NextRequest) {
try {
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
const { status, data } = await guardrailsClient(request, "/api/v1/guardrails/ban_lists", {
method: "POST",
body: JSON.stringify(body),
});
return NextResponse.json(data, { status });
} catch (e: unknown) {
return NextResponse.json(
{ error: e instanceof Error ? e.message : String(e) },
{ status: 500 },
);
}
}
🤖 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` around lines 16 - 29, The POST handler
currently treats JSON parse failures from request.json() as 500; update the POST
function to detect JSON parsing errors (e.g., catch SyntaxError or check
error.message from request.json()) and return NextResponse.json({ error:
"Malformed JSON" }, { status: 400 }) for those cases, while preserving the
existing 500 response for other unexpected errors; specifically locate the POST
function and modify the try/catch so parsing errors from request.json() result
in a 400 response and other errors still return 500.

Comment on lines +104 to +112
useEffect(() => { fetchBanLists(); }, []);

useEffect(() => {
if (value) {
fetchBannedWords(value);
} else {
setBannedWords([]);
}
}, [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

Missing apiKey in useEffect dependency arrays.

Both useEffect hooks use apiKey in their fetch calls but don't list it as a dependency. If apiKey changes after mount, the component won't re-fetch with the new key.

🐛 Add apiKey to dependency arrays
-  useEffect(() => { fetchBanLists(); }, []);
+  useEffect(() => { fetchBanLists(); }, [apiKey]);

   useEffect(() => {
     if (value) {
       fetchBannedWords(value);
     } else {
       setBannedWords([]);
     }
-  }, [value]);
+  }, [value, apiKey]);

Note: You may also need to wrap fetchBanLists and fetchBannedWords in useCallback with apiKey as a dependency to satisfy the exhaustive-deps rule properly.

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

In `@app/components/guardrails/ValidatorConfigPanel.tsx` around lines 104 - 112,
The two useEffect hooks in ValidatorConfigPanel call fetchBanLists and
fetchBannedWords while relying on apiKey but don't list apiKey in their
dependency arrays; update the dependency arrays to include apiKey (i.e.,
useEffect(() => { fetchBanLists(); }, [apiKey]) and useEffect(() => { ... },
[value, apiKey])) and to satisfy exhaustive-deps, wrap fetchBanLists and
fetchBannedWords with useCallback keyed on apiKey (e.g., const fetchBanLists =
useCallback(..., [apiKey]) and const fetchBannedWords = useCallback(...,
[apiKey])) so the effects re-run when apiKey changes.

Comment on lines +371 to +391
useEffect(() => {
if (existingValues != null) {
// Loading a saved config — populate everything
setConfigName(existingName ?? '');
setStage((existingValues.stage as 'input' | 'output') ?? 'output');
setOnFailAction((existingValues.on_fail_action as string) ?? 'fix');
setIsEnabled((existingValues.is_enabled as boolean) ?? true);
const { stage: _s, on_fail_action: _o, is_enabled: _i, ...rest } = existingValues;
setFieldValues(validator ? (Object.keys(rest).length > 0 ? rest : buildDefaultValues(validator.config)) : {});
} else if (validator) {
// Type changed with no saved config — only reset dynamic fields, leave static fields alone
setFieldValues(buildDefaultValues(validator.config));
} else {
// Cleared — reset everything
setConfigName('');
setStage('output');
setOnFailAction('fix');
setIsEnabled(true);
setFieldValues({});
}
}, [selectedType, existingValues, existingName]);
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

Missing validator in useEffect dependency array.

The effect uses validator (line 379, 382) which is derived from selectedType and validators, but only selectedType is in the dependency array. If the validators array changes after mount, the form state could be initialized with stale data.

🐛 Add validator to dependencies
-  }, [selectedType, existingValues, existingName]);
+  }, [selectedType, existingValues, existingName, validator]);
📝 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
useEffect(() => {
if (existingValues != null) {
// Loading a saved config — populate everything
setConfigName(existingName ?? '');
setStage((existingValues.stage as 'input' | 'output') ?? 'output');
setOnFailAction((existingValues.on_fail_action as string) ?? 'fix');
setIsEnabled((existingValues.is_enabled as boolean) ?? true);
const { stage: _s, on_fail_action: _o, is_enabled: _i, ...rest } = existingValues;
setFieldValues(validator ? (Object.keys(rest).length > 0 ? rest : buildDefaultValues(validator.config)) : {});
} else if (validator) {
// Type changed with no saved config — only reset dynamic fields, leave static fields alone
setFieldValues(buildDefaultValues(validator.config));
} else {
// Cleared — reset everything
setConfigName('');
setStage('output');
setOnFailAction('fix');
setIsEnabled(true);
setFieldValues({});
}
}, [selectedType, existingValues, existingName]);
useEffect(() => {
if (existingValues != null) {
// Loading a saved config — populate everything
setConfigName(existingName ?? '');
setStage((existingValues.stage as 'input' | 'output') ?? 'output');
setOnFailAction((existingValues.on_fail_action as string) ?? 'fix');
setIsEnabled((existingValues.is_enabled as boolean) ?? true);
const { stage: _s, on_fail_action: _o, is_enabled: _i, ...rest } = existingValues;
setFieldValues(validator ? (Object.keys(rest).length > 0 ? rest : buildDefaultValues(validator.config)) : {});
} else if (validator) {
// Type changed with no saved config — only reset dynamic fields, leave static fields alone
setFieldValues(buildDefaultValues(validator.config));
} else {
// Cleared — reset everything
setConfigName('');
setStage('output');
setOnFailAction('fix');
setIsEnabled(true);
setFieldValues({});
}
}, [selectedType, existingValues, existingName, validator]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/guardrails/ValidatorConfigPanel.tsx` around lines 371 - 391,
The useEffect that initializes form state references the local variable
validator (and calls buildDefaultValues(validator.config) and setFieldValues)
but only lists selectedType, existingValues, and existingName in its dependency
array, which can lead to stale initialization when validators change; update the
dependency array to include validator (and if validator is derived from an
external validators array, include that array too) so the effect reruns when the
resolved validator changes, ensuring setFieldValues and other setters use the
correct config.

Comment on lines +14 to +24
<button
type="button"
className="w-3.5 h-3.5 rounded-full text-[10px] font-bold flex items-center justify-center leading-none select-none"
style={{
backgroundColor: colors.bg.secondary,
color: colors.text.secondary,
border: `1px solid ${colors.border}`,
}}
onMouseEnter={() => setVisible(true)}
onMouseLeave={() => setVisible(false)}
>
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

Tooltip is mouse-only; keyboard/touch users cannot access helper text.

At Line 22-Line 24, visibility is tied only to hover events. Add focus/blur (and preferably click fallback) plus tooltip semantics so non-mouse users can access the content.

♿ Suggested fix
 export default function InfoTooltip({ text }: InfoTooltipProps) {
   const [visible, setVisible] = useState(false);
   return (
     <span className="relative inline-flex items-center ml-1" style={{ verticalAlign: 'text-bottom' }}>
       <button
         type="button"
+        aria-label="More information"
+        aria-expanded={visible}
+        aria-describedby="info-tooltip"
         className="w-3.5 h-3.5 rounded-full text-[10px] font-bold flex items-center justify-center leading-none select-none"
         style={{
           backgroundColor: colors.bg.secondary,
           color: colors.text.secondary,
           border: `1px solid ${colors.border}`,
         }}
         onMouseEnter={() => setVisible(true)}
         onMouseLeave={() => setVisible(false)}
+        onFocus={() => setVisible(true)}
+        onBlur={() => setVisible(false)}
+        onClick={() => setVisible((v) => !v)}
       >
         i
       </button>
       {visible && (
         <div
+          id="info-tooltip"
+          role="tooltip"
           className="absolute z-50 left-5 top-0 w-64 text-xs rounded-lg p-2.5 shadow-lg"
           style={{
             backgroundColor: colors.bg.primary,
             border: `1px solid ${colors.border}`,
             color: colors.text.secondary,
             lineHeight: "1.5",
           }}
         >
           {text}
         </div>
       )}
     </span>
   );
 }

Also applies to: 27-38

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

In `@app/components/InfoTooltip.tsx` around lines 14 - 24, The InfoTooltip
component currently only shows the tooltip on mouse enter/leave; update the
button to also handle keyboard and touch users by wiring onFocus to
setVisible(true) and onBlur to setVisible(false), add an onClick handler to
toggle visibility as a touch fallback, and add an onKeyDown handler to close on
Escape; ensure proper tooltip semantics by adding an id on the tooltip element
and connecting the button via aria-describedby (and aria-expanded on the button)
and marking the tooltip with role="tooltip" so screen readers can discover it;
keep using the existing setVisible state and the same classnames/visual styling
while only adding these handlers and ARIA attributes in InfoTooltip.

Comment on lines +45 to +53
<div
className="min-h-[36px] w-full flex flex-wrap items-center gap-1.5 rounded-md border px-2.5 py-1.5 cursor-text"
style={{
borderColor: open ? colors.accent.primary : colors.border,
backgroundColor: colors.bg.primary,
outline: open ? `1px solid ${colors.accent.primary}` : "none",
}}
onClick={() => setOpen((v) => !v)}
>
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

Dropdown trigger is not keyboard-accessible.

At Line 45-Line 53, interaction is mouse-only (div + onClick). Add keyboard handlers and combobox semantics so users can open/close via keyboard.

♿ Suggested fix
       <div
         className="min-h-[36px] w-full flex flex-wrap items-center gap-1.5 rounded-md border px-2.5 py-1.5 cursor-text"
+        role="combobox"
+        aria-expanded={open}
+        aria-haspopup="listbox"
+        tabIndex={0}
         style={{
           borderColor: open ? colors.accent.primary : colors.border,
           backgroundColor: colors.bg.primary,
           outline: open ? `1px solid ${colors.accent.primary}` : "none",
         }}
         onClick={() => setOpen((v) => !v)}
+        onKeyDown={(e) => {
+          if (e.key === "Enter" || e.key === " ") {
+            e.preventDefault();
+            setOpen((v) => !v);
+          }
+          if (e.key === "Escape") {
+            setOpen(false);
+          }
+        }}
       >
       {open && (
         <div
+          role="listbox"
           className="absolute z-50 mt-1 w-full rounded-md shadow-md overflow-auto max-h-52"

Also applies to: 104-139

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

In `@app/components/MultiSelect.tsx` around lines 45 - 53, The div that toggles
the dropdown (using setOpen and open in MultiSelect) is not keyboard-accessible;
replace or augment it with proper combobox/button semantics: add tabindex={0}
(or use a native <button>), role="combobox" (or role="button") and
aria-expanded={open} and aria-haspopup="listbox", and implement an onKeyDown
handler that toggles setOpen when Enter or Space are pressed and handles Escape
to close; ensure the same changes are applied to the other trigger block (the
second clickable region around lines 104-139) so both mouse and keyboard users
can open/close the dropdown and focus moves appropriately.

Comment on lines +62 to 64
const text = response.status === 204 ? "" : await response.text();
const data = text ? JSON.parse(text) : null;

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

Unconditional JSON parsing can mask upstream errors as generic 500s.

At Line 62-Line 64, any non-JSON body throws on JSON.parse, which turns a real upstream response into a route-level exception path.

🛠️ Suggested fix
-  const text = response.status === 204 ? "" : await response.text();
-  const data = text ? JSON.parse(text) : null;
+  const text = response.status === 204 ? "" : await response.text();
+  let data: unknown = null;
+  if (text) {
+    try {
+      data = JSON.parse(text);
+    } catch {
+      data = { raw: 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
const text = response.status === 204 ? "" : await response.text();
const data = text ? JSON.parse(text) : null;
const text = response.status === 204 ? "" : await response.text();
let data: unknown = null;
if (text) {
try {
data = JSON.parse(text);
} catch {
data = { raw: text };
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/apiClient.ts` around lines 62 - 64, The code unconditionally calls
JSON.parse on the response body (variables: response, text, data), which will
throw for non-JSON responses and convert upstream errors into local exceptions;
change this by wrapping JSON.parse(text) in a try/catch and only set data to the
parsed object on success, otherwise set data to null (or keep the raw text) and
preserve/propagate the original response status and body so upstream errors
aren’t masked; update the logic around response.text(), JSON.parse, and the data
variable to safely handle non-JSON responses without throwing.

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

♻️ Duplicate comments (2)
.env.example (1)

3-4: ⚠️ Potential issue | 🟡 Minor

Normalize dotenv entries to pass linter.

Line 3 and Line 4 use invalid spacing around = (and Line 4 includes trailing whitespace), and key order currently violates the linter rule.

💡 Proposed fix
-GUARDRAILS_URL = http://localhost:8001
-GUARDRAILS_TOKEN = 
+GUARDRAILS_TOKEN=
+GUARDRAILS_URL=http://localhost:8001
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 3 - 4, Normalize the .env entries by removing
spaces around the equals sign and removing trailing whitespace for the keys
shown: change "GUARDRAILS_URL = http://localhost:8001" to
"GUARDRAILS_URL=http://localhost:8001" and "GUARDRAILS_TOKEN = " to
"GUARDRAILS_TOKEN=" (no trailing space), and reorder the keys if required by the
linter so the key order matches the project's dotenv ordering convention; update
the lines containing GUARDRAILS_URL and GUARDRAILS_TOKEN accordingly.
app/lib/apiClient.ts (1)

186-187: ⚠️ Potential issue | 🟠 Major

Guard JSON parsing for non-JSON upstream responses.

Line 187 can throw on non-JSON bodies and mask the real upstream failure payload/status handling in this helper.

💡 Proposed fix
   const text = response.status === 204 ? "" : await response.text();
-  const data = text ? JSON.parse(text) : null;
+  let data: unknown = null;
+  if (text) {
+    try {
+      data = JSON.parse(text);
+    } catch {
+      data = { raw: text };
+    }
+  }
 
   return { status: response.status, data };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/apiClient.ts` around lines 186 - 187, The JSON.parse call on the
response body can throw for non-JSON upstream responses; update the parsing
logic around response, text and data to first check the Content-Type
(response.headers.get('content-type')) for a JSON media type and only then
JSON.parse, or otherwise wrap JSON.parse in a try/catch and fall back to
returning the raw text (or null) while preserving the original response.status;
change the code that computes text and data to handle parse failures safely so
callers of this helper (the variables response, text, data) won't throw on
non-JSON bodies.
🤖 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/Sidebar.tsx`:
- Around line 80-118: There are two conflicting declarations of the navItems
constant in the Sidebar component; remove the duplicate inline const navItems
(the first declaration containing Guardrails and Settings) and instead add those
entries to the shared NAV_ITEMS configuration in app/lib/navConfig.ts, then
update the iconMap to include icons for "Guardrails" and "Settings" so the
Sidebar JSX uses the single NAV_ITEMS source consistently; ensure you only keep
one navItems reference (the NAV_ITEMS import) used by the Sidebar render logic.

In `@app/lib/apiClient.ts`:
- Around line 172-174: The code currently overwrites any caller-provided
Content-Type when the body is not a FormData; update the logic in apiClient.ts
to only set headers.set("Content-Type", "application/json") if there is a body
and the headers do not already include a Content-Type (i.e., use headers.has or
headers.get to check caller-provided options.headers first). Locate the block
that reads fetchOptions and headers (references: fetchOptions, headers,
options.headers) and add the guard so explicit media types passed by the caller
are preserved.
- Around line 4-6: There are two declarations of the same module-scoped constant
BACKEND_URL causing a TypeScript error; remove the duplicate declaration so only
one const BACKEND_URL exists (keep the preferred default value and environment
fallback), and ensure any code using BACKEND_URL still references that single
symbol; check nearby constants like GUARDRAILS_URL to confirm no other
duplicates were added.

---

Duplicate comments:
In @.env.example:
- Around line 3-4: Normalize the .env entries by removing spaces around the
equals sign and removing trailing whitespace for the keys shown: change
"GUARDRAILS_URL = http://localhost:8001" to
"GUARDRAILS_URL=http://localhost:8001" and "GUARDRAILS_TOKEN = " to
"GUARDRAILS_TOKEN=" (no trailing space), and reorder the keys if required by the
linter so the key order matches the project's dotenv ordering convention; update
the lines containing GUARDRAILS_URL and GUARDRAILS_TOKEN accordingly.

In `@app/lib/apiClient.ts`:
- Around line 186-187: The JSON.parse call on the response body can throw for
non-JSON upstream responses; update the parsing logic around response, text and
data to first check the Content-Type (response.headers.get('content-type')) for
a JSON media type and only then JSON.parse, or otherwise wrap JSON.parse in a
try/catch and fall back to returning the raw text (or null) while preserving the
original response.status; change the code that computes text and data to handle
parse failures safely so callers of this helper (the variables response, text,
data) won't throw on non-JSON bodies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7c59fda0-5933-43e0-a588-0bbc2da2cd82

📥 Commits

Reviewing files that changed from the base of the PR and between 3bd304e and 644cc49.

📒 Files selected for processing (6)
  • .env.example
  • app/(main)/configurations/prompt-editor/page.tsx
  • app/components/Sidebar.tsx
  • app/components/icons/index.tsx
  • app/components/prompt-editor/ConfigEditorPane.tsx
  • app/lib/apiClient.ts
✅ Files skipped from review due to trivial changes (1)
  • app/components/icons/index.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/(main)/configurations/prompt-editor/page.tsx
  • app/components/prompt-editor/ConfigEditorPane.tsx

Comment on lines +80 to +118
const navItems: MenuItem[] = [
{
name: "Evaluations",
icon: <ClipboardIcon />,
submenu: [
{ name: "Text", route: "/evaluations" },
{ name: "Speech-to-Text", route: "/speech-to-text" },
{ name: "Text-to-Speech", route: "/text-to-speech" },
],
},
{
name: "Documents",
route: "/document",
icon: <DocumentFileIcon />,
},
{
name: "Knowledge Base",
route: "/knowledge-base",
icon: <BookOpenIcon />,
},
{
name: "Configurations",
icon: <GearIcon className="w-5 h-5" />,
submenu: [
{ name: "Library", route: "/configurations" },
{ name: "Prompt Editor", route: "/configurations/prompt-editor" },
],
},
{
name: "Guardrails",
route: "/guardrails",
icon: <ShieldCheckIcon />
},
{
name: "Settings",
route: "/settings/credentials",
icon: <SlidersIcon />,
},
];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the duplicate declaration and the render path that consumes it.
rg -n -C2 'const navItems|navItems\.map' app/components/Sidebar.tsx

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 574


Remove the duplicate const navItems declaration at line 80–118.

The component declares const navItems twice in the same scope (lines 80 and 158), which will fail to compile. Additionally, the JSX at line 191 uses the second declaration (line 158), which is derived from the shared NAV_ITEMS config. The Guardrails and Settings entries added in the first array will not render unless they are also added to NAV_ITEMS in app/lib/navConfig.ts and mapped in the iconMap (lines 151–156).

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

In `@app/components/Sidebar.tsx` around lines 80 - 118, There are two conflicting
declarations of the navItems constant in the Sidebar component; remove the
duplicate inline const navItems (the first declaration containing Guardrails and
Settings) and instead add those entries to the shared NAV_ITEMS configuration in
app/lib/navConfig.ts, then update the iconMap to include icons for "Guardrails"
and "Settings" so the Sidebar JSX uses the single NAV_ITEMS source consistently;
ensure you only keep one navItems reference (the NAV_ITEMS import) used by the
Sidebar render logic.

Comment on lines +4 to 6
const BACKEND_URL = process.env.BACKEND_URL || "http://0.0.0.0:8000";
const GUARDRAILS_URL = process.env.GUARDRAILS_URL || "http://localhost:8001";
const BACKEND_URL = process.env.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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify duplicate BACKEND_URL declarations in the reviewed file.
# Expected: exactly one match.
rg -nP '^\s*const\s+BACKEND_URL\b' app/lib/apiClient.ts

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 221


🏁 Script executed:

head -n 10 app/lib/apiClient.ts | cat -n

Repository: ProjectTech4DevAI/kaapi-frontend

Length of output: 648


Remove duplicate BACKEND_URL declaration (build blocker).

Lines 4 and 6 both declare the same module-scoped const, which causes a TypeScript compilation error.

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

In `@app/lib/apiClient.ts` around lines 4 - 6, There are two declarations of the
same module-scoped constant BACKEND_URL causing a TypeScript error; remove the
duplicate declaration so only one const BACKEND_URL exists (keep the preferred
default value and environment fallback), and ensure any code using BACKEND_URL
still references that single symbol; check nearby constants like GUARDRAILS_URL
to confirm no other duplicates were added.

Comment on lines +172 to +174
if (!(fetchOptions.body instanceof FormData)) {
headers.set("Content-Type", "application/json");
}
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

Don’t overwrite caller-provided Content-Type.

Line 173 unconditionally sets Content-Type for all non-FormData requests, which can break explicit media types passed in options.headers.

💡 Proposed fix
-  if (!(fetchOptions.body instanceof FormData)) {
+  if (
+    !(fetchOptions.body instanceof FormData) &&
+    !headers.has("Content-Type")
+  ) {
     headers.set("Content-Type", "application/json");
   }
📝 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
if (!(fetchOptions.body instanceof FormData)) {
headers.set("Content-Type", "application/json");
}
if (
!(fetchOptions.body instanceof FormData) &&
!headers.has("Content-Type")
) {
headers.set("Content-Type", "application/json");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/apiClient.ts` around lines 172 - 174, The code currently overwrites
any caller-provided Content-Type when the body is not a FormData; update the
logic in apiClient.ts to only set headers.set("Content-Type",
"application/json") if there is a body and the headers do not already include a
Content-Type (i.e., use headers.has or headers.get to check caller-provided
options.headers first). Locate the block that reads fetchOptions and headers
(references: fetchOptions, headers, options.headers) and add the guard so
explicit media types passed by the caller are preserved.

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

♻️ Duplicate comments (4)
app/components/Sidebar.tsx (1)

79-117: ⚠️ Potential issue | 🔴 Critical

Remove the duplicate navItems declaration.

Line 79 and Line 157 both declare const navItems in the same scope, so this component will not compile. The render path also consumes the NAV_ITEMS-derived array, so the inline Guardrails/Settings list is not a viable second source of truth anyway. Move those entries into the shared nav config and extend iconMap instead of keeping two local declarations.

Also applies to: 157-165

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

In `@app/components/Sidebar.tsx` around lines 79 - 117, There are two conflicting
nav arrays—remove the duplicate const navItems declaration and consolidate all
menu entries into the single shared NAV_ITEMS (or the original navItems used by
the render path) so the component compiles; add the missing "Guardrails" and
"Settings" entries to that shared nav configuration and extend iconMap to
include ShieldCheckIcon and SlidersIcon (or map the existing names to these
icons) so the UI uses the single source of truth (navItems/NAV_ITEMS) and no
inline duplicate list remains.
app/components/guardrails/ValidatorConfigPanel.tsx (2)

441-472: ⚠️ Potential issue | 🟠 Major

Include validator in the initialization effect dependencies.

Line 455 reads validator.config, but the effect never reruns when validators resolves to a new validator. If the catalog arrives after selectedType or existingValues, the dynamic fields stay blank/defaulted and a save can overwrite the existing config.

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

In `@app/components/guardrails/ValidatorConfigPanel.tsx` around lines 441 - 472,
The useEffect that initializes fields reads validator.config (and calls
buildDefaultValues) but does not list validator in its dependency array, so the
effect won't rerun when the validator variable becomes available; update the
effect dependencies to include validator so that when validator changes (e.g.,
catalog resolves) the initialization branch that uses validator,
buildDefaultValues, and setFieldValues runs again and correctly populates
dynamic fields instead of leaving them blank/defaulted.

74-125: ⚠️ Potential issue | 🟡 Minor

Re-fetch ban-list data when apiKey changes.

Line 77 and Line 100 read apiKey, but the effects at Line 115 and Line 119 never depend on it. If credentials are updated, this field keeps using the stale key and never refreshes until the component remounts.

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

In `@app/components/guardrails/ValidatorConfigPanel.tsx` around lines 74 - 125,
The component reads apiKey inside fetchBanLists and fetchBannedWords but the
useEffect hooks that call them (the one that calls fetchBanLists on mount and
the one that calls fetchBannedWords when value changes) do not include apiKey in
their dependency arrays, causing stale credentials to be used; update the two
useEffect dependency arrays to include apiKey (i.e., change useEffect(() => {
fetchBanLists(); }, [] ) to useEffect(() => { fetchBanLists(); }, [apiKey]) and
change useEffect(() => { if (value) fetchBannedWords(value); else
setBannedWords([]); }, [value]) to include apiKey as well so that when apiKey
changes you re-run the effects and refetch ban lists and banned words (ensuring
fetchBannedWords is called with the current value).
app/api/guardrails/validators/configs/[config_id]/route.ts (1)

33-38: ⚠️ Potential issue | 🟠 Major

Return an empty response for upstream 204s.

These handlers always call NextResponse.json(data, { status }). If the upstream proxy comes back with 204 No Content, serializing null here adds a body to a no-content response and can break DELETE/PATCH flows.

📦 Minimal 204 handling
-    return NextResponse.json(data, { status });
+    return status === 204
+      ? new NextResponse(null, { status })
+      : NextResponse.json(data, { status });
@@
-    return NextResponse.json(data, { status });
+    return status === 204
+      ? new NextResponse(null, { status })
+      : NextResponse.json(data, { status });
@@
-    return NextResponse.json(data, { status });
+    return status === 204
+      ? new NextResponse(null, { status })
+      : NextResponse.json(data, { status });

Run this read-only check to confirm the helper can surface 204s and that these return sites always serialize the proxy result:

#!/bin/bash
set -euo pipefail

ROUTE_FILE="$(fd -a 'route.ts' app/api | rg 'guardrails/validators/configs/\[config_id\]/route\.ts$' | head -n1)"
API_CLIENT_FILE="$(fd -a 'apiClient.ts' app/lib | head -n1)"

sed -n '1,180p' "$ROUTE_FILE"
sed -n '1,260p' "$API_CLIENT_FILE"
rg -n -C2 'NextResponse\.json\(data, \{ status \}\)|\b204\b' "$ROUTE_FILE" "$API_CLIENT_FILE"

Also applies to: 61-70, 92-100

🤖 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 33 -
38, The handler is always calling NextResponse.json(data, { status }) which will
serialize a body even when the upstream returned 204; update the response logic
around the guardrailsClient call (the const { status, data } = await
guardrailsClient(request, buildEndpoint(request, config_id), { authHeader })) to
special-case status === 204 and return an empty NextResponse with that status
(e.g., return new NextResponse(null, { status })) and only call
NextResponse.json(data, { status }) for non-204 statuses.
🤖 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/guardrails/ValidatorConfigPanel.tsx`:
- Around line 74-113: fetchBanLists and fetchBannedWords currently treat non-OK
HTTP responses as successful empty data; update both to check response.ok (e.g.,
after fetch, if (!r.ok) throw new Error(await r.text() || r.statusText)) so
errors flow to .catch, introduce two error state variables (e.g., banListsError
and bannedWordsError with setters setBanListsError and setBannedWordsError),
clear those errors at the start of each fetch, and in .catch set the appropriate
error state instead of calling setBanLists([]) or setBannedWords([]); ensure
setLoading/setWordsLoading are still cleared in .finally and wire the new error
states into the component UI to show an inline alert when non-OK responses occur
rather than rendering the empty-state text.

In `@app/components/prompt-editor/ConfigEditorPane.tsx`:
- Around line 48-71: The effect in ConfigEditorPane's useEffect that fetches
validator configs (using queryString, apiKey, fetch, setValidators, setLoading)
must cancel in-flight requests and clear stale state when the scope changes:
create an AbortController inside the effect, pass controller.signal to fetch,
call controller.abort in the cleanup, and in the case queryString is falsy
ensure you immediately setValidators([]) and setLoading(false) (or bail early)
so old validators aren’t left behind; also handle fetch aborts in the catch to
avoid overwriting state from aborted requests.
- Around line 251-265: When apiKey changes we must clear previous org/project
scope and ignore stale verify responses; in the useEffect that depends on
apiKey, call setGuardrailsQueryString("") immediately when apiKey is falsy or
before starting a new verification, and cancel/ignore prior fetches by using an
AbortController or a local request token so that late-resolving promises from
fetch("/api/apikeys/verify") do not overwrite the cleared state; update the
effect surrounding fetch("/api/apikeys/verify") and setGuardrailsQueryString to
implement this cancellation/ignore logic.

---

Duplicate comments:
In `@app/api/guardrails/validators/configs/`[config_id]/route.ts:
- Around line 33-38: The handler is always calling NextResponse.json(data, {
status }) which will serialize a body even when the upstream returned 204;
update the response logic around the guardrailsClient call (the const { status,
data } = await guardrailsClient(request, buildEndpoint(request, config_id), {
authHeader })) to special-case status === 204 and return an empty NextResponse
with that status (e.g., return new NextResponse(null, { status })) and only call
NextResponse.json(data, { status }) for non-204 statuses.

In `@app/components/guardrails/ValidatorConfigPanel.tsx`:
- Around line 441-472: The useEffect that initializes fields reads
validator.config (and calls buildDefaultValues) but does not list validator in
its dependency array, so the effect won't rerun when the validator variable
becomes available; update the effect dependencies to include validator so that
when validator changes (e.g., catalog resolves) the initialization branch that
uses validator, buildDefaultValues, and setFieldValues runs again and correctly
populates dynamic fields instead of leaving them blank/defaulted.
- Around line 74-125: The component reads apiKey inside fetchBanLists and
fetchBannedWords but the useEffect hooks that call them (the one that calls
fetchBanLists on mount and the one that calls fetchBannedWords when value
changes) do not include apiKey in their dependency arrays, causing stale
credentials to be used; update the two useEffect dependency arrays to include
apiKey (i.e., change useEffect(() => { fetchBanLists(); }, [] ) to useEffect(()
=> { fetchBanLists(); }, [apiKey]) and change useEffect(() => { if (value)
fetchBannedWords(value); else setBannedWords([]); }, [value]) to include apiKey
as well so that when apiKey changes you re-run the effects and refetch ban lists
and banned words (ensuring fetchBannedWords is called with the current value).

In `@app/components/Sidebar.tsx`:
- Around line 79-117: There are two conflicting nav arrays—remove the duplicate
const navItems declaration and consolidate all menu entries into the single
shared NAV_ITEMS (or the original navItems used by the render path) so the
component compiles; add the missing "Guardrails" and "Settings" entries to that
shared nav configuration and extend iconMap to include ShieldCheckIcon and
SlidersIcon (or map the existing names to these icons) so the UI uses the single
source of truth (navItems/NAV_ITEMS) and no inline duplicate list remains.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eb67bafc-7483-4222-925c-bda2a120012f

📥 Commits

Reviewing files that changed from the base of the PR and between 644cc49 and 6498f19.

📒 Files selected for processing (15)
  • app/(main)/guardrails/page.tsx
  • app/api/guardrails/ban_lists/[ban_list_id]/route.ts
  • app/api/guardrails/ban_lists/route.ts
  • app/api/guardrails/route.ts
  • app/api/guardrails/validators/configs/[config_id]/route.ts
  • app/api/guardrails/validators/configs/route.ts
  • app/components/InfoTooltip.tsx
  • app/components/MultiSelect.tsx
  • app/components/Sidebar.tsx
  • app/components/guardrails/BanListModal.tsx
  • app/components/guardrails/ValidatorConfigPanel.tsx
  • app/components/guardrails/types.ts
  • app/components/icons/index.tsx
  • app/components/icons/sidebar/ShieldCheckIcon.tsx
  • app/components/prompt-editor/ConfigEditorPane.tsx
✅ Files skipped from review due to trivial changes (5)
  • app/components/icons/index.tsx
  • app/components/guardrails/types.ts
  • app/components/icons/sidebar/ShieldCheckIcon.tsx
  • app/api/guardrails/ban_lists/route.ts
  • app/(main)/guardrails/page.tsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/api/guardrails/route.ts
  • app/components/InfoTooltip.tsx
  • app/components/MultiSelect.tsx
  • app/api/guardrails/ban_lists/[ban_list_id]/route.ts
  • app/components/guardrails/BanListModal.tsx

Comment on lines +74 to +113
const fetchBanLists = () => {
setLoading(true);
fetch("/api/guardrails/ban_lists", {
headers: { "X-API-KEY": apiKey },
})
.then((r) => r.json())
.then((data) => {
const list: BanList[] = Array.isArray(data?.data?.ban_lists)
? data.data.ban_lists
: Array.isArray(data?.data)
? data.data
: Array.isArray(data?.ban_lists)
? data.ban_lists
: Array.isArray(data)
? data
: [];
setBanLists(list);
})
.catch(() => setBanLists([]))
.finally(() => setLoading(false));
};

const fetchBannedWords = (id: string) => {
setWordsLoading(true);
setBannedWords([]);
fetch(`/api/guardrails/ban_lists/${id}`, {
headers: { "X-API-KEY": apiKey },
})
.then((r) => r.json())
.then((data) => {
const words: string[] = Array.isArray(data?.banned_words)
? data.banned_words
: Array.isArray(data?.data?.banned_words)
? data.data.banned_words
: [];
setBannedWords(words);
})
.catch(() => setBannedWords([]))
.finally(() => setWordsLoading(false));
};
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

Don’t render ban-list fetch failures as empty data.

Line 79 and Line 102 never check r.ok, and Line 92 and Line 111 collapse failures into [], so a 401/500 is shown as “No ban lists yet” or “No words in this ban list.” Track an error state and render an inline alert instead of falling back to the empty-state UI.

Based on learnings: Handle inline validation and error states with alerts instead of implementing a toast library for consistency with the current codebase.

Also applies to: 144-179, 181-209

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

In `@app/components/guardrails/ValidatorConfigPanel.tsx` around lines 74 - 113,
fetchBanLists and fetchBannedWords currently treat non-OK HTTP responses as
successful empty data; update both to check response.ok (e.g., after fetch, if
(!r.ok) throw new Error(await r.text() || r.statusText)) so errors flow to
.catch, introduce two error state variables (e.g., banListsError and
bannedWordsError with setters setBanListsError and setBannedWordsError), clear
those errors at the start of each fetch, and in .catch set the appropriate error
state instead of calling setBanLists([]) or setBannedWords([]); ensure
setLoading/setWordsLoading are still cleared in .finally and wire the new error
states into the component UI to show an inline alert when non-OK responses occur
rather than rendering the empty-state text.

Comment on lines +48 to +71
useEffect(() => {
if (!queryString) return;
setLoading(true);
fetch(`/api/guardrails/validators/configs${queryString}`, {
headers: apiKey ? { "X-API-KEY": apiKey } : {},
})
.then((r) => r.json())
.then((data) => {
const items: ValidatorConfigOption[] = Array.isArray(
data?.data?.configs,
)
? data.data.configs
: Array.isArray(data?.data)
? data.data
: Array.isArray(data?.configs)
? data.configs
: Array.isArray(data)
? data
: [];
setValidators(items);
})
.catch(() => setValidators([]))
.finally(() => setLoading(false));
}, [queryString, apiKey]);
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

Abort/reset validator catalog fetches when the scope changes.

Both GuardrailsSection instances fetch the same catalog, but this effect never cancels stale requests and Line 49 leaves the old validators array intact when queryString disappears. Switching API keys/projects can therefore leave the dropdown populated with validators from the previous scope.

🔧 Minimal hardening inside the section
  useEffect(() => {
-    if (!queryString) return;
+    const controller = new AbortController();
+    if (!queryString) {
+      setValidators([]);
+      setLoading(false);
+      return () => controller.abort();
+    }
     setLoading(true);
     fetch(`/api/guardrails/validators/configs${queryString}`, {
       headers: apiKey ? { "X-API-KEY": apiKey } : {},
+      signal: controller.signal,
     })
       .then((r) => r.json())
       .then((data) => {
         const items: ValidatorConfigOption[] = Array.isArray(
           data?.data?.configs,
@@
-      .catch(() => setValidators([]))
-      .finally(() => setLoading(false));
+      .catch((error) => {
+        if (error.name !== "AbortError") setValidators([]);
+      })
+      .finally(() => {
+        if (!controller.signal.aborted) setLoading(false);
+      });
+    return () => controller.abort();
   }, [queryString, apiKey]);

Longer-term, it would be cleaner to fetch this catalog once in ConfigEditorPane and pass the shared options into both sections.

Also applies to: 1033-1054

🤖 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 48 - 71, The
effect in ConfigEditorPane's useEffect that fetches validator configs (using
queryString, apiKey, fetch, setValidators, setLoading) must cancel in-flight
requests and clear stale state when the scope changes: create an AbortController
inside the effect, pass controller.signal to fetch, call controller.abort in the
cleanup, and in the case queryString is falsy ensure you immediately
setValidators([]) and setLoading(false) (or bail early) so old validators aren’t
left behind; also handle fetch aborts in the catch to avoid overwriting state
from aborted requests.

Comment on lines +251 to +265
useEffect(() => {
if (!apiKey) return;
fetch("/api/apikeys/verify", { headers: { "X-API-KEY": apiKey } })
.then((r) => r.json())
.then((data) => {
const org_id = data?.data?.organization_id;
const proj_id = data?.data?.project_id;
if (org_id != null && proj_id != null) {
setGuardrailsQueryString(
`?organization_id=${parseInt(String(org_id), 10)}&project_id=${parseInt(String(proj_id), 10)}`,
);
}
})
.catch(() => {});
}, [apiKey]);
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

Clear the derived org/project scope before re-verifying the API key.

This effect only updates guardrailsQueryString on success. If apiKey becomes empty/invalid, or an older verify call resolves last, the previous org/project query remains active and later guardrails requests stay scoped to the wrong context.

🧭 Reset and cancel stale verification
  useEffect(() => {
-    if (!apiKey) return;
-    fetch("/api/apikeys/verify", { headers: { "X-API-KEY": apiKey } })
-      .then((r) => r.json())
+    const controller = new AbortController();
+    setGuardrailsQueryString(null);
+    if (!apiKey) return () => controller.abort();
+    fetch("/api/apikeys/verify", {
+      headers: { "X-API-KEY": apiKey },
+      signal: controller.signal,
+    })
+      .then(async (r) => {
+        if (!r.ok) throw new Error("Failed to verify API key");
+        return r.json();
+      })
       .then((data) => {
         const org_id = data?.data?.organization_id;
         const proj_id = data?.data?.project_id;
         if (org_id != null && proj_id != null) {
-          setGuardrailsQueryString(
-            `?organization_id=${parseInt(String(org_id), 10)}&project_id=${parseInt(String(proj_id), 10)}`,
-          );
+          const params = new URLSearchParams({
+            organization_id: String(org_id),
+            project_id: String(proj_id),
+          });
+          setGuardrailsQueryString(`?${params.toString()}`);
+        } else {
+          setGuardrailsQueryString(null);
         }
       })
-      .catch(() => {});
+      .catch((error) => {
+        if (error.name !== "AbortError") {
+          setGuardrailsQueryString(null);
+        }
+      });
+    return () => controller.abort();
   }, [apiKey]);
📝 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
useEffect(() => {
if (!apiKey) return;
fetch("/api/apikeys/verify", { headers: { "X-API-KEY": apiKey } })
.then((r) => r.json())
.then((data) => {
const org_id = data?.data?.organization_id;
const proj_id = data?.data?.project_id;
if (org_id != null && proj_id != null) {
setGuardrailsQueryString(
`?organization_id=${parseInt(String(org_id), 10)}&project_id=${parseInt(String(proj_id), 10)}`,
);
}
})
.catch(() => {});
}, [apiKey]);
useEffect(() => {
const controller = new AbortController();
setGuardrailsQueryString(null);
if (!apiKey) return () => controller.abort();
fetch("/api/apikeys/verify", {
headers: { "X-API-KEY": apiKey },
signal: controller.signal,
})
.then(async (r) => {
if (!r.ok) throw new Error("Failed to verify API key");
return r.json();
})
.then((data) => {
const org_id = data?.data?.organization_id;
const proj_id = data?.data?.project_id;
if (org_id != null && proj_id != null) {
const params = new URLSearchParams({
organization_id: String(org_id),
project_id: String(proj_id),
});
setGuardrailsQueryString(`?${params.toString()}`);
} else {
setGuardrailsQueryString(null);
}
})
.catch((error) => {
if (error.name !== "AbortError") {
setGuardrailsQueryString(null);
}
});
return () => controller.abort();
}, [apiKey]);
🤖 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 251 - 265,
When apiKey changes we must clear previous org/project scope and ignore stale
verify responses; in the useEffect that depends on apiKey, call
setGuardrailsQueryString("") immediately when apiKey is falsy or before starting
a new verification, and cancel/ignore prior fetches by using an AbortController
or a local request token so that late-resolving promises from
fetch("/api/apikeys/verify") do not overwrite the cleared state; update the
effect surrounding fetch("/api/apikeys/verify") and setGuardrailsQueryString to
implement this cancellation/ignore logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UI: include guardrails in config UI

1 participant