Repository navigation
feat: add SDK settings - #2582
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughThis PR adds a complete platform secret key management feature for LLM SDK integration, extends project configuration to support end-user session settings, and renames SDK branding from "Embeddable SDK" to "LLM SDK" throughout the codebase. The backend exposes three new ChangesPlatform Keys and End-User Settings Feature
SDK Branding Updates (Embeddable → LLM)
🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b2ad35be63
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| token, | ||
| projectId, | ||
| description, | ||
| keyType: "platform_secret", |
There was a problem hiding this comment.
Reject platform secrets in gateway auth
Creating this row as an active platform_secret makes the returned sk_... token usable against normal gateway endpoints, because apps/gateway/src/lib/cached-queries.ts currently authenticates any API key whose token matches and whose keyType is not end_user_customer. In practice, a secret advertised here as only for minting end-user sessions can call /v1/chat/completions directly and bypass the end-user session/wallet controls; either the gateway lookup needs to exclude platform_secret keys or these secrets need to be stored so they cannot authenticate regular LLM requests.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Adds a new “Embeddable” project settings area to configure end-user session settings (enablement, markup, allowed origins) and manage platform secret keys used for minting embeddable sessions. This extends both the dashboard UI and API surface to support embeddable SDK configuration, with unit tests covering the new platform key endpoints.
Changes:
- Add Embeddable settings page in the dashboard (new route + sidebar link) with UI to update project end-user session settings and manage platform secret keys.
- Extend API project schemas + update handler to support end-user session settings, including origin normalization and owner/admin-only updates.
- Add platform secret key list/create/revoke routes plus unit tests for platform secret creation/listing/revocation.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/db/src/types.ts | Expands SerializedProject to include embeddable-related project fields for UI consumption. |
| apps/ui/src/components/settings/embeddable-settings.tsx | New client component for editing end-user session settings and managing platform secret keys. |
| apps/ui/src/components/dashboard/dashboard-sidebar.tsx | Adds “Embeddable” link under Project Settings navigation. |
| apps/ui/src/app/dashboard/[orgId]/[projectId]/settings/embeddable/page.tsx | Adds the new Embeddable settings page route and server-side project fetch. |
| apps/api/src/routes/projects.ts | Adds embeddable fields to project schema and restricts updates to owner/admin; normalizes allowed origins. |
| apps/api/src/routes/organization.ts | Adds embeddable fields to org project schema (project list responses). |
| apps/api/src/routes/keys-api.ts | Introduces platform secret key endpoints and access checks for owner/admin only. |
| apps/api/src/routes/keys-api.spec.ts | Adds unit tests for platform secret create/list/delete and partial role access coverage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {platformKeysQuery.error && ( | ||
| <p className="text-destructive text-sm"> | ||
| Only organization owners and admins can manage platform keys. | ||
| </p> | ||
| )} |
| const copyCreatedToken = () => { | ||
| void navigator.clipboard.writeText(createdToken); | ||
| toast({ | ||
| title: "Secret key copied", | ||
| description: "The platform secret key has been copied.", | ||
| }); | ||
| }; |
| test("POST /keys/platform rejects organization developers", async () => { | ||
| await db | ||
| .update(tables.userOrganization) | ||
| .set({ role: "developer" }) | ||
| .where(eq(tables.userOrganization.id, "test-user-org-id")); | ||
|
|
||
| const res = await app.request("/keys/platform", { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| Cookie: token, | ||
| }, | ||
| body: JSON.stringify({ | ||
| projectId: "test-project-id", | ||
| description: "Developer secret", | ||
| }), | ||
| }); | ||
|
|
||
| expect(res.status).toBe(403); | ||
| }); |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
apps/ui/src/components/settings/embeddable-settings.tsx (1)
35-51: 💤 Low valueConsider wrapping URL parsing to provide a user-friendly error message.
When
new URL(origin)throws for malformed input (e.g., "not-a-url"), the user sees a generic "Invalid URL" message. The backend'snormalizeAllowedOriginsprovides clearer feedback:Invalid allowed origin: ${origin}.♻️ Suggested improvement
function normalizeOrigins(value: string) { const origins = value .split(/\r?\n/) .map((origin) => origin.trim()) .filter(Boolean); const normalizedOrigins = new Set<string>(); for (const origin of origins) { - const url = new URL(origin); + let url: URL; + try { + url = new URL(origin); + } catch { + throw new Error(`Invalid allowed origin: ${origin}`); + } if (url.protocol !== "https:" && url.protocol !== "http:") { throw new Error("Origins must use http or https."); } normalizedOrigins.add(url.origin); } return Array.from(normalizedOrigins); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/ui/src/components/settings/embeddable-settings.tsx` around lines 35 - 51, The URL constructor in normalizeOrigins can throw for malformed entries; wrap the new URL(origin) call in a try/catch inside the normalizeOrigins function and, on failure, throw a new Error with a user-friendly message such as `Invalid allowed origin: ${origin}` (preserving the invalid origin value) instead of letting the generic URL error bubble up; keep the existing protocol check and normalizedOrigins.add(url.origin) logic intact.apps/api/src/routes/projects.ts (1)
343-354: 💤 Low valueRedundant call to
normalizeAllowedOrigins.The origins are already normalized at line 281. Reuse that value instead of calling the function again.
♻️ Suggested refactor
+ let normalizedAllowedOrigins: string[] | undefined; if (allowedOrigins !== undefined) { - updateData.allowedOrigins = normalizeAllowedOrigins(allowedOrigins); + normalizedAllowedOrigins = normalizeAllowedOrigins(allowedOrigins); + updateData.allowedOrigins = normalizedAllowedOrigins; } ... if (allowedOrigins !== undefined) { - const normalizedAllowedOrigins = normalizeAllowedOrigins(allowedOrigins); const previousAllowedOrigins = project.allowedOrigins ?? []; if ( - JSON.stringify(normalizedAllowedOrigins) !== + JSON.stringify(normalizedAllowedOrigins!) !== JSON.stringify(previousAllowedOrigins) ) { changes.allowedOrigins = { old: previousAllowedOrigins, - new: normalizedAllowedOrigins, + new: normalizedAllowedOrigins!, }; } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/routes/projects.ts` around lines 343 - 354, You’re calling normalizeAllowedOrigins again even though the origins were normalized earlier; remove the redundant normalizeAllowedOrigins call and reuse the already-normalized value (the variable computed earlier when processing allowedOrigins) when comparing to project.allowedOrigins and when setting changes.allowedOrigins; update the comparison to use that existing normalized value instead of calling normalizeAllowedOrigins(allowedOrigins) and keep the rest of the logic (previousAllowedOrigins/project.allowedOrigins and changes.allowedOrigins) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@apps/api/src/routes/projects.ts`:
- Around line 343-354: You’re calling normalizeAllowedOrigins again even though
the origins were normalized earlier; remove the redundant
normalizeAllowedOrigins call and reuse the already-normalized value (the
variable computed earlier when processing allowedOrigins) when comparing to
project.allowedOrigins and when setting changes.allowedOrigins; update the
comparison to use that existing normalized value instead of calling
normalizeAllowedOrigins(allowedOrigins) and keep the rest of the logic
(previousAllowedOrigins/project.allowedOrigins and changes.allowedOrigins)
unchanged.
In `@apps/ui/src/components/settings/embeddable-settings.tsx`:
- Around line 35-51: The URL constructor in normalizeOrigins can throw for
malformed entries; wrap the new URL(origin) call in a try/catch inside the
normalizeOrigins function and, on failure, throw a new Error with a
user-friendly message such as `Invalid allowed origin: ${origin}` (preserving
the invalid origin value) instead of letting the generic URL error bubble up;
keep the existing protocol check and normalizedOrigins.add(url.origin) logic
intact.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 3ec203eb-5d26-401c-b7f0-5d7a5986d2b7
📒 Files selected for processing (8)
apps/api/src/routes/keys-api.spec.tsapps/api/src/routes/keys-api.tsapps/api/src/routes/organization.tsapps/api/src/routes/projects.tsapps/ui/src/app/dashboard/[orgId]/[projectId]/settings/embeddable/page.tsxapps/ui/src/components/dashboard/dashboard-sidebar.tsxapps/ui/src/components/settings/embeddable-settings.tsxpackages/db/src/types.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/ui/src/components/settings/llm-sdk-settings.tsx (1)
35-51: ⚡ Quick winWrap URL parsing in try-catch for better error messages.
The
new URL(origin)constructor at line 43 can throw if the origin is invalid, but it's not wrapped in a try-catch block. While the outersaveSettingsfunction catches the error, the generic URL constructor error message may be less helpful to users than a specific message. The backend normalizes origins with a try-catch and provides"Invalid allowed origin: ${origin}".Consider matching the backend pattern for consistent error messages:
Suggested improvement
function normalizeOrigins(value: string) { const origins = value .split(/\r?\n/) .map((origin) => origin.trim()) .filter(Boolean); const normalizedOrigins = new Set<string>(); for (const origin of origins) { - const url = new URL(origin); + let url: URL; + try { + url = new URL(origin); + } catch { + throw new Error(`Invalid allowed origin: ${origin}`); + } if (url.protocol !== "https:" && url.protocol !== "http:") { throw new Error("Origins must use http or https."); } normalizedOrigins.add(url.origin); } return Array.from(normalizedOrigins); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/ui/src/components/settings/llm-sdk-settings.tsx` around lines 35 - 51, The URL parsing in normalizeOrigins can throw on invalid input; wrap the new URL(origin) call inside a try-catch in the normalizeOrigins function and rethrow a clearer, consistent message (e.g. "Invalid allowed origin: ${origin}") so the UI matches backend normalization errors; keep the rest of the logic (protocol check for http/https and normalizedOrigins.add(url.origin)) the same and only change the error path to use the new, descriptive message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@apps/ui/src/components/settings/llm-sdk-settings.tsx`:
- Around line 35-51: The URL parsing in normalizeOrigins can throw on invalid
input; wrap the new URL(origin) call inside a try-catch in the normalizeOrigins
function and rethrow a clearer, consistent message (e.g. "Invalid allowed
origin: ${origin}") so the UI matches backend normalization errors; keep the
rest of the logic (protocol check for http/https and
normalizedOrigins.add(url.origin)) the same and only change the error path to
use the new, descriptive message.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: ea0b991f-d0ca-47e7-900b-a62bb458bf91
📒 Files selected for processing (28)
apps/api/src/index.tsapps/api/src/lib/end-user-session-auth.tsapps/api/src/lib/platform-secret-auth.tsapps/api/src/routes/keys-api.spec.tsapps/api/src/routes/keys-api.tsapps/api/src/routes/platform-connect.tsapps/api/src/routes/platform-customers.tsapps/api/src/routes/platform-session-refresh.tsapps/api/src/routes/platform-sessions.tsapps/api/src/routes/platform-wallet.tsapps/api/src/routes/platform-webhooks.tsapps/api/src/routes/projects.tsapps/api/src/stripe.tsapps/docs/content/features/llm-sdk.mdxapps/gateway/src/chat/chat.tsapps/gateway/src/chat/tools/create-log-entry.tsapps/gateway/src/embeddings/embeddings.tsapps/gateway/src/lib/end-user-session.tsapps/gateway/src/moderations/moderations.tsapps/gateway/src/videos/videos.tsapps/ui/src/app/dashboard/[orgId]/[projectId]/settings/llm-sdk/page.tsxapps/ui/src/components/dashboard/dashboard-sidebar.tsxapps/ui/src/components/settings/llm-sdk-settings.tsxapps/ui/src/content/blog/2026-06-07-embeddable-ai-credits.mdapps/worker/src/worker.tspackages/db/src/schema.tspackages/db/src/types.tspackages/db/src/webhook-helpers.ts
✅ Files skipped from review due to trivial changes (20)
- apps/api/src/routes/platform-customers.ts
- apps/api/src/routes/platform-sessions.ts
- apps/api/src/routes/platform-connect.ts
- apps/api/src/index.ts
- apps/gateway/src/embeddings/embeddings.ts
- apps/api/src/routes/platform-webhooks.ts
- apps/gateway/src/lib/end-user-session.ts
- apps/api/src/routes/platform-session-refresh.ts
- apps/api/src/lib/end-user-session-auth.ts
- apps/api/src/lib/platform-secret-auth.ts
- apps/ui/src/content/blog/2026-06-07-embeddable-ai-credits.md
- packages/db/src/webhook-helpers.ts
- apps/gateway/src/moderations/moderations.ts
- apps/gateway/src/videos/videos.ts
- apps/api/src/stripe.ts
- apps/gateway/src/chat/tools/create-log-entry.ts
- apps/gateway/src/chat/chat.ts
- apps/api/src/routes/platform-wallet.ts
- packages/db/src/schema.ts
- apps/worker/src/worker.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/db/src/types.ts
- apps/api/src/routes/projects.ts
- apps/api/src/routes/keys-api.ts
- apps/api/src/routes/keys-api.spec.ts
There was a problem hiding this comment.
🧹 Nitpick comments (4)
apps/ui/src/app/dashboard/[orgId]/[projectId]/settings/sdk/page.tsx (1)
36-40: 💤 Low valueHeading text is inconsistent with card title.
The page heading says "SDK" (line 36) while the CardTitle says "LLM SDK" (line 40). Consider using "LLM SDK" for the heading as well to maintain consistency and clarity.
Suggested consistency fix
- <h2 className="text-3xl font-bold tracking-tight">SDK</h2> + <h2 className="text-3xl font-bold tracking-tight">LLM SDK</h2>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/ui/src/app/dashboard/`[orgId]/[projectId]/settings/sdk/page.tsx around lines 36 - 40, The page heading text in the h2 element (className="text-3xl font-bold tracking-tight") is inconsistent with the CardTitle component (CardTitle>LLM SDK</CardTitle>); update the h2 string from "SDK" to "LLM SDK" so the top-level heading and CardTitle match and clearly indicate "LLM SDK".apps/ui/src/components/settings/sdk-settings.tsx (3)
35-51: ⚡ Quick winAdd explicit error handling in normalizeOrigins.
The
new URL(origin)constructor can throw a TypeError if the origin is invalid. While this is caught by the outer try-catch insaveSettings, adding an explicit try-catch here would make the error handling clearer and ensure consistent error messages.Proposed error handling improvement
function normalizeOrigins(value: string) { const origins = value .split(/\r?\n/) .map((origin) => origin.trim()) .filter(Boolean); const normalizedOrigins = new Set<string>(); for (const origin of origins) { - const url = new URL(origin); + let url: URL; + try { + url = new URL(origin); + } catch { + throw new Error(`Invalid URL format: ${origin}`); + } if (url.protocol !== "https:" && url.protocol !== "http:") { throw new Error("Origins must use http or https."); } normalizedOrigins.add(url.origin); } return Array.from(normalizedOrigins); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/ui/src/components/settings/sdk-settings.tsx` around lines 35 - 51, The normalizeOrigins function can throw an uncaught TypeError when new URL(origin) receives invalid input; wrap the URL construction and protocol check in a try/catch inside normalizeOrigins to catch invalid origins and rethrow a clearer, consistent Error (e.g., "Invalid origin: <origin>") so saveSettings receives a uniform error message. Update normalizeOrigins (referenced by the function name) to validate each origin string with a try/catch around new URL(origin) and throw a descriptive Error for invalid values, preserving the existing deduplication via normalizedOrigins on success.
104-144: 💤 Low valueConsider adding user feedback when origins are normalized.
After successfully saving, the code updates
allowedOriginsTextwith the normalized origins (line 129), which will change the textarea content if the user entered duplicates, extra whitespace, or full URLs. This could surprise users. Consider adding a toast message when normalization occurs to inform them their input was cleaned up.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/ui/src/components/settings/sdk-settings.tsx` around lines 104 - 144, The saveSettings function currently normalizes origins via normalizeOrigins and then silently updates the textarea state with setAllowedOriginsText(allowedOrigins.join("\n")), which can surprise users; modify saveSettings to detect when the normalized origins string differs from the original allowedOriginsText and, if so, call toast (or reuse the existing toast pattern) to show a non-destructive informational message (e.g., "Origins normalized: duplicates/whitespace removed") after successful save but before closing, referencing normalizeOrigins, setAllowedOriginsText, allowedOriginsText, and toast so users are informed when their input was cleaned up.
310-314: 💤 Low valueError message assumes all query errors are authorization failures.
The error message "Only organization owners and admins can manage platform keys" is displayed for any
platformKeysQuery.error, but the error could be a network issue, server error, or other problem unrelated to authorization. Consider checking the error status or using a more generic message with a hint about authorization.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/ui/src/components/settings/sdk-settings.tsx` around lines 310 - 314, The current error UI in sdk-settings.tsx always shows an authorization message when platformKeysQuery.error exists; instead inspect the error object from platformKeysQuery (e.g., check error.status, error.response?.status, or a network flag) and render a specific "permission denied" message only for 401/403, otherwise render a generic "Failed to load platform keys" message (optionally with a retry hint). Update the conditional around platformKeysQuery.error to branch on the error status and use platformKeysQuery.error to surface non-auth errors without assuming authorization is the cause.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@apps/ui/src/app/dashboard/`[orgId]/[projectId]/settings/sdk/page.tsx:
- Around line 36-40: The page heading text in the h2 element
(className="text-3xl font-bold tracking-tight") is inconsistent with the
CardTitle component (CardTitle>LLM SDK</CardTitle>); update the h2 string from
"SDK" to "LLM SDK" so the top-level heading and CardTitle match and clearly
indicate "LLM SDK".
In `@apps/ui/src/components/settings/sdk-settings.tsx`:
- Around line 35-51: The normalizeOrigins function can throw an uncaught
TypeError when new URL(origin) receives invalid input; wrap the URL construction
and protocol check in a try/catch inside normalizeOrigins to catch invalid
origins and rethrow a clearer, consistent Error (e.g., "Invalid origin:
<origin>") so saveSettings receives a uniform error message. Update
normalizeOrigins (referenced by the function name) to validate each origin
string with a try/catch around new URL(origin) and throw a descriptive Error for
invalid values, preserving the existing deduplication via normalizedOrigins on
success.
- Around line 104-144: The saveSettings function currently normalizes origins
via normalizeOrigins and then silently updates the textarea state with
setAllowedOriginsText(allowedOrigins.join("\n")), which can surprise users;
modify saveSettings to detect when the normalized origins string differs from
the original allowedOriginsText and, if so, call toast (or reuse the existing
toast pattern) to show a non-destructive informational message (e.g., "Origins
normalized: duplicates/whitespace removed") after successful save but before
closing, referencing normalizeOrigins, setAllowedOriginsText,
allowedOriginsText, and toast so users are informed when their input was cleaned
up.
- Around line 310-314: The current error UI in sdk-settings.tsx always shows an
authorization message when platformKeysQuery.error exists; instead inspect the
error object from platformKeysQuery (e.g., check error.status,
error.response?.status, or a network flag) and render a specific "permission
denied" message only for 401/403, otherwise render a generic "Failed to load
platform keys" message (optionally with a retry hint). Update the conditional
around platformKeysQuery.error to branch on the error status and use
platformKeysQuery.error to surface non-auth errors without assuming
authorization is the cause.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: cd0f6224-a5a1-42d4-ae6e-352d75c70f30
📒 Files selected for processing (7)
apps/api/src/routes/keys-api.spec.tsapps/api/src/routes/keys-api.tsapps/api/src/routes/projects.tsapps/docs/content/features/llm-sdk.mdxapps/ui/src/app/dashboard/[orgId]/[projectId]/settings/sdk/page.tsxapps/ui/src/components/dashboard/dashboard-sidebar.tsxapps/ui/src/components/settings/sdk-settings.tsx
✅ Files skipped from review due to trivial changes (1)
- apps/docs/content/features/llm-sdk.mdx
🚧 Files skipped from review as they are similar to previous changes (3)
- apps/api/src/routes/keys-api.spec.ts
- apps/api/src/routes/keys-api.ts
- apps/api/src/routes/projects.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5afb6ad07d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| <Switch | ||
| checked={endUserEnabled} |
There was a problem hiding this comment.
Remove duplicate Switch checked prop
With this duplicated checked attribute, the new SDK settings component contains invalid JSX (JSX elements cannot have multiple attributes with the same name), so the UI TypeScript/Next build will fail as soon as this page is compiled. Remove one of the duplicate attributes.
Useful? React with 👍 / 👎.
Summary
Tests
Notes
Summary by CodeRabbit
New Features
Documentation
Tests