feat: new chat playground - #928
Conversation
- Updated CLAUDE.md with new information. - Modified conductor.json for improved configuration. - Adjusted eslint.config.mjs for linting rules. - Updated package.json and pnpm-lock.yaml for dependency management. - Made changes to README.md for better documentation. - Updated turbo.json for build configurations. - Adjusted GitHub workflows for CI/CD processes. - Refactored API and Gateway components for better performance and reliability. - Enhanced Playground UI components for improved user experience. - Updated shared packages for better integration and functionality.
- Removed obsolete files and directories from the playground app, including various build and cache artifacts. - Updated pnpm-lock.yaml and package.json for dependency management. - Enhanced pre-commit hook to include linting and building the UI. - Made minor adjustments to various components for improved functionality and performance.
…UI components - Added Stripe integration for credit top-up functionality, allowing users to purchase credits directly within the application. - Implemented new components for displaying and managing credits, including and . - Updated existing components to incorporate credit management features, ensuring a seamless user experience. - Enhanced API key management with auto-generated keys for new users, improving onboarding. - Refactored chat components to support new credit features and improve overall performance. This update significantly enhances the playground's functionality by enabling users to manage their credits effectively and provides a more engaging interface for interactions.
❌ Preview Environment deleted from BunnyshellAvailable commands (reply to this comment):
|
WalkthroughAdds a new Next.js "playground" app (UI, components, hooks, libs, infra/CI) and integrates it across builds; updates API signup to create a default project in "credits" mode and atomically insert an auto-generated playground API key; expands local CORS origins and bumps multiple package versions. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant API as API (auth)
participant DB as DB
Note over U,API: Signup creates default project + playground API key atomically
U->>API: POST /signup (user data)
activate API
API->>DB: BEGIN transaction
API->>DB: INSERT org & project (project.mode = "credits")
API->>DB: INSERT api_key (description: "Auto-generated playground key", token: env_prefix + shortid(40))
DB-->>API: COMMIT
deactivate API
API-->>U: 201 Created (user + project + key)
sequenceDiagram
autonumber
actor U as User
participant UI as Playground client
participant Route as /api/chat route
participant GW as LLM Gateway
Note over U,UI: Live chat streaming flow
U->>UI: Submit prompt (+ optional apiKey/model)
UI->>Route: POST messages, model, apiKey
activate Route
Route->>GW: streamText(model, converted messages)
GW-->>Route: stream tokens/events
Route-->>UI: streaming response body
deactivate Route
UI-->>U: render assistant reply progressively
Note over UI: Persist messages via useChats on finish
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Possibly related PRs
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used🪛 Shellcheck (0.11.0).github/test-split-docker.sh[warning] 73-73: Declare and assign separately to avoid masking return values. (SC2155) ⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
🔇 Additional comments (1)
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.
Actionable comments posted: 85
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
apps/playground/src/components/ui/input.tsx (1)
1-22: Make Input a Client Component and forward refsWithout "use client", Client Components can’t import this. Forwarding refs improves integration (e.g., react-hook-form).
+ "use client" + import * as React from "react" import { cn } from "@/lib/utils" -function Input({ className, type, ...props }: React.ComponentProps<"input">) { - return ( - <input - type={type} - data-slot="input" - className={cn( - "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", - "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", - "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - className - )} - {...props} - /> - ) -} +const Input = React.forwardRef< + HTMLInputElement, + React.InputHTMLAttributes<HTMLInputElement> +>(({ className, type, ...props }, ref) => { + return ( + <input + ref={ref} + type={type} + data-slot="input" + className={cn( + "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", + "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + className + )} + {...props} + /> + ) +}) +Input.displayName = "Input" export { Input }apps/playground/src/components/ui/textarea.tsx (1)
1-19: Make Textarea a Client Component and forward refsSame RSC boundary concern as Input; also forward the ref.
+ "use client" + import * as React from "react" import { cn } from "@/lib/utils" -function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { - return ( - <textarea - data-slot="textarea" - className={cn( - "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", - className - )} - {...props} - /> - ) -} +const Textarea = React.forwardRef< + HTMLTextAreaElement, + React.TextareaHTMLAttributes<HTMLTextAreaElement> +>(({ className, ...props }, ref) => { + return ( + <textarea + ref={ref} + data-slot="textarea" + className={cn( + "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + className + )} + {...props} + /> + ) +}) +Textarea.displayName = "Textarea" export { Textarea }apps/playground/src/components/ui/providers-icons.tsx (1)
1-416: Consolidate duplicated provider-icon modules — re-export from the canonical provider-icons moduleDuplicate provider icon definitions found; canonical module: apps/playground/src/components/provider-icons.tsx. Replace the duplicated files with thin re-exports so the API stays consistent and prevents drift.
Files to change:
- apps/playground/src/components/ui/providers-icons.tsx
- apps/ui/src/components/ui/providers-icons.tsx
Suggested replacement (use the import path/alias your project uses, e.g. "@/components/provider-icons" or a repo-relative path to apps/playground/src/components/provider-icons):
import type React from "react"; export { AnthropicIcon, CloudriftIcon, DeepseekIcon, GoogleStudioAIIcon, GroqIcon, InferenceNetIcon, MistralIcon, OpenAIIcon, PerplexityIcon, TogetherAIIcon, XAIIcon, MoonshotIcon, NovitaIcon, AlibabaIcon, NebiusIcon, ZaiIcon, ProviderIcons, type ProviderIconKey, getProviderIcon, } from "@/components/provider-icons";apps/playground/components/ui/dropdown-menu.tsx (1)
1-258: Consolidate duplicate dropdown-menu modules — keep one canonical file and update importsFound three copies: apps/ui/src/lib/components/dropdown-menu.tsx, apps/playground/components/ui/dropdown-menu.tsx, apps/playground/src/components/ui/dropdown-menu.tsx. Pick a single source (recommend apps/ui/src/lib/components/dropdown-menu.tsx), remove/merge the others, and update imports that reference "@/components/ui/dropdown-menu" or "@/lib/components/dropdown-menu" (e.g. apps/playground/src/components/ai-elements/prompt-input.tsx, apps/ui/src/components/provider-keys/provider-keys-list.tsx).
| return api.useMutation("patch", "/user/me", { | ||
| onSuccess: () => { | ||
| void queryClient.invalidateQueries({ queryKey: ["user"] }); | ||
| void queryClient.invalidateQueries({ queryKey: ["session"] }); | ||
| }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
🧩 Analysis chain
Ensure invalidation keys match the client’s query keys for /user/me.
If your client doesn’t use ["user"] as the key for /user/me, this won’t refetch. Safer to invalidate by predicate or use the client’s exported key helper.
Proposed adjustment:
return api.useMutation("patch", "/user/me", {
onSuccess: () => {
- void queryClient.invalidateQueries({ queryKey: ["user"] });
+ // Ensure we hit the exact query for `/user/me`
+ void queryClient.invalidateQueries({
+ predicate: (q) =>
+ Array.isArray(q.queryKey) && q.queryKey.includes("/user/me"),
+ });
void queryClient.invalidateQueries({ queryKey: ["session"] });
},
});Run to inspect query keys:
🏁 Script executed:
#!/bin/bash
# Find createClient and inspect how query keys are formed
rg -nP 'createClient\(' -C3
rg -nP 'useQuery\(' -C3Length of output: 20944
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== apps/playground/src/lib/fetch-client.ts ==="
sed -n '1,240p' apps/playground/src/lib/fetch-client.ts || true
echo
echo "=== apps/ui/src/lib/fetch-client.ts ==="
sed -n '1,240p' apps/ui/src/lib/fetch-client.ts || true
echo
echo "=== apps/playground/src/hooks/useUser.ts ==="
sed -n '1,240p' apps/playground/src/hooks/useUser.ts || true
echo
echo "=== apps/ui/src/hooks/useUser.ts ==="
sed -n '1,240p' apps/ui/src/hooks/useUser.ts || true
echo
echo "=== Searching for useApi implementation/exports ==="
rg -n --hidden "useApi\(" -S || true
rg -n --hidden "export function useApi" -S || true
rg -n --hidden "function useApi" -S || true
echo
echo "=== Searching for queryOptions / useQuery / useMutation implementations ==="
rg -n --hidden "queryOptions\(" -S || true
rg -n --hidden "useQuery\(" -S || true
rg -n --hidden "useMutation\(" -S || true
echo
echo "=== Search for 'createClient' definition/import ==="
rg -n --hidden "createClient" -S || trueLength of output: 28454
Invalidate the exact query key used for GET /user/me (use api.queryOptions).
api.queryOptions("get", "/user/me").queryKey is used across the codebase; invalidating ["user"] won't match. Replace the hardcoded key with the client's queryKey:
-return api.useMutation("patch", "/user/me", {
- onSuccess: () => {
- void queryClient.invalidateQueries({ queryKey: ["user"] });
- void queryClient.invalidateQueries({ queryKey: ["session"] });
- },
-});
+return api.useMutation("patch", "/user/me", {
+ onSuccess: () => {
+ const userQueryKey = api.queryOptions("get", "/user/me", {}).queryKey;
+ void queryClient.invalidateQueries({ queryKey: userQueryKey });
+ void queryClient.invalidateQueries({ queryKey: ["session"] });
+ },
+});📝 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.
| return api.useMutation("patch", "/user/me", { | |
| onSuccess: () => { | |
| void queryClient.invalidateQueries({ queryKey: ["user"] }); | |
| void queryClient.invalidateQueries({ queryKey: ["session"] }); | |
| }, | |
| }); | |
| } | |
| return api.useMutation("patch", "/user/me", { | |
| onSuccess: () => { | |
| const userQueryKey = api.queryOptions("get", "/user/me", {}).queryKey; | |
| void queryClient.invalidateQueries({ queryKey: userQueryKey }); | |
| void queryClient.invalidateQueries({ queryKey: ["session"] }); | |
| }, | |
| }); | |
| } |
🤖 Prompt for AI Agents
In apps/playground/src/hooks/useUser.ts around lines 117 to 123, the mutation
invalidates a hardcoded query key ["user"] (and ["session"]) which doesn't match
the actual queryKey used for GET /user/me across the codebase; replace the
hardcoded keys with the client's canonical queryKey by retrieving
api.queryOptions("get", "/user/me").queryKey and using that for
invalidateQueries (and similarly use the appropriate api.queryOptions for the
session query if needed) so the exact queries are invalidated.
| baseURL: config.apiUrl + "/auth", | ||
| plugins: [passkeyClient()], | ||
| }); |
There was a problem hiding this comment.
🧹 Nitpick
Normalize baseURL to avoid double/missing slashes
String concat can produce //auth or omit the slash depending on config. Normalize once.
- return createAuthClient({
- baseURL: config.apiUrl + "/auth",
- plugins: [passkeyClient()],
- });
+ const base = config.apiUrl.replace(/\/+$/, "");
+ return createAuthClient({
+ baseURL: `${base}/auth`,
+ plugins: [passkeyClient()],
+ });📝 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.
| baseURL: config.apiUrl + "/auth", | |
| plugins: [passkeyClient()], | |
| }); | |
| const base = config.apiUrl.replace(/\/+$/, ""); | |
| return createAuthClient({ | |
| baseURL: `${base}/auth`, | |
| plugins: [passkeyClient()], | |
| }); |
🤖 Prompt for AI Agents
In apps/playground/src/lib/auth-client.ts around lines 13 to 15, the baseURL is
built via string concatenation (config.apiUrl + "/auth") which can produce
double or missing slashes; change it to normalize the URL (e.g., use the URL
constructor or ensure a single slash by trimming trailing slash from
config.apiUrl and prefixing "/auth") so baseURL is always a well-formed single
path, and keep existing plugins array unchanged.
| const key = "better-auth.session_token"; | ||
| // Get session cookie for authentication | ||
| const sessionCookie = cookieStore.get(`${key}`); | ||
| const secureSessionCookie = cookieStore.get(`__Secure-${key}`); | ||
|
|
||
| const data = await fetch(`${config.apiBackendUrl}/user/me`, { | ||
| method: "GET", | ||
| headers: { | ||
| Cookie: secureSessionCookie | ||
| ? `__Secure-${key}=${secureSessionCookie.value}` | ||
| : sessionCookie | ||
| ? `${key}=${sessionCookie.value}` | ||
| : "", | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Don’t send an empty Cookie header; disable caching for user lookup.
Avoid emitting Cookie: "" and opt out of Next fetch caching for per-request user identity.
const key = "better-auth.session_token";
// Get session cookie for authentication
const sessionCookie = cookieStore.get(`${key}`);
const secureSessionCookie = cookieStore.get(`__Secure-${key}`);
- const data = await fetch(`${config.apiBackendUrl}/user/me`, {
- method: "GET",
- headers: {
- Cookie: secureSessionCookie
- ? `__Secure-${key}=${secureSessionCookie.value}`
- : sessionCookie
- ? `${key}=${sessionCookie.value}`
- : "",
- },
- });
+ const cookieHeader =
+ secureSessionCookie
+ ? `__Secure-${key}=${secureSessionCookie.value}`
+ : sessionCookie
+ ? `${key}=${sessionCookie.value}`
+ : undefined;
+
+ const data = await fetch(`${config.apiBackendUrl}/user/me`, {
+ method: "GET",
+ cache: "no-store",
+ headers: cookieHeader ? { Cookie: cookieHeader } : undefined,
+ });📝 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.
| const key = "better-auth.session_token"; | |
| // Get session cookie for authentication | |
| const sessionCookie = cookieStore.get(`${key}`); | |
| const secureSessionCookie = cookieStore.get(`__Secure-${key}`); | |
| const data = await fetch(`${config.apiBackendUrl}/user/me`, { | |
| method: "GET", | |
| headers: { | |
| Cookie: secureSessionCookie | |
| ? `__Secure-${key}=${secureSessionCookie.value}` | |
| : sessionCookie | |
| ? `${key}=${sessionCookie.value}` | |
| : "", | |
| }, | |
| }); | |
| const key = "better-auth.session_token"; | |
| // Get session cookie for authentication | |
| const sessionCookie = cookieStore.get(`${key}`); | |
| const secureSessionCookie = cookieStore.get(`__Secure-${key}`); | |
| const cookieHeader = | |
| secureSessionCookie | |
| ? `__Secure-${key}=${secureSessionCookie.value}` | |
| : sessionCookie | |
| ? `${key}=${sessionCookie.value}` | |
| : undefined; | |
| const data = await fetch(`${config.apiBackendUrl}/user/me`, { | |
| method: "GET", | |
| cache: "no-store", | |
| headers: cookieHeader ? { Cookie: cookieHeader } : undefined, | |
| }); |
🤖 Prompt for AI Agents
In apps/playground/src/lib/getUser.ts around lines 13 to 27, the code currently
always sets a Cookie header (possibly as an empty string) and uses the default
fetch caching; change it to only include the Cookie header when a session cookie
exists (omit the header entirely if neither secureSessionCookie nor
sessionCookie is present) and disable Next.js fetch caching by passing cache:
"no-store" (or next: { cache: "no-store" } if your project convention differs)
in the fetch options; construct headers conditionally (e.g. build a headers
object, add Cookie only when value exists) and pass cache: "no-store" to the
fetch call.
| export function getModelCapabilities(model: ModelDefinition): string[] { | ||
| const capabilities: string[] = []; | ||
| const provider = model.providers[0]; | ||
|
|
||
| if (provider?.streaming) capabilities.push("Streaming"); | ||
| if (provider?.vision) capabilities.push("Vision"); | ||
| if (provider?.tools) capabilities.push("Tools"); | ||
| if (provider?.reasoning) capabilities.push("Reasoning"); | ||
| if (model.jsonOutput) capabilities.push("JSON Output"); | ||
|
|
||
| return capabilities; | ||
| } |
There was a problem hiding this comment.
Incorrect capability source; reading non-existent fields from ProviderModelMapping.
model.providers[0] is a mapping, not a ProviderDefinition. Fields like vision, tools, reasoning are not present on ProviderDefinition either (per providers schema). This will type-check fail and/or produce incorrect results.
Apply this diff to derive capabilities from the actual ProviderDefinition and model flags:
-export function getModelCapabilities(model: ModelDefinition): string[] {
- const capabilities: string[] = [];
- const provider = model.providers[0];
-
- if (provider?.streaming) capabilities.push("Streaming");
- if (provider?.vision) capabilities.push("Vision");
- if (provider?.tools) capabilities.push("Tools");
- if (provider?.reasoning) capabilities.push("Reasoning");
- if (model.jsonOutput) capabilities.push("JSON Output");
-
- return capabilities;
-}
+export function getModelCapabilities(
+ model: ModelDefinition,
+ providers: ProviderDefinition[] = [],
+): string[] {
+ const caps: string[] = [];
+ const providerDef = model.providers?.length
+ ? providers.find((p) => p.id === model.providers[0]?.providerId)
+ : undefined;
+
+ if (providerDef?.streaming) caps.push("Streaming");
+ if (providerDef?.jsonOutput || model.jsonOutput) caps.push("JSON Output");
+ return caps;
+}Optional cleanup: remove unused imports Model and Provider at the top.
📝 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.
| export function getModelCapabilities(model: ModelDefinition): string[] { | |
| const capabilities: string[] = []; | |
| const provider = model.providers[0]; | |
| if (provider?.streaming) capabilities.push("Streaming"); | |
| if (provider?.vision) capabilities.push("Vision"); | |
| if (provider?.tools) capabilities.push("Tools"); | |
| if (provider?.reasoning) capabilities.push("Reasoning"); | |
| if (model.jsonOutput) capabilities.push("JSON Output"); | |
| return capabilities; | |
| } | |
| export function getModelCapabilities( | |
| model: ModelDefinition, | |
| providers: ProviderDefinition[] = [], | |
| ): string[] { | |
| const caps: string[] = []; | |
| const providerDef = model.providers?.length | |
| ? providers.find((p) => p.id === model.providers[0]?.providerId) | |
| : undefined; | |
| if (providerDef?.streaming) caps.push("Streaming"); | |
| if (providerDef?.jsonOutput || model.jsonOutput) caps.push("JSON Output"); | |
| return caps; | |
| } |
| [P in keyof paths]: paths[P] extends { get: any } ? P : never; | ||
| }[keyof paths]; | ||
|
|
||
| type PostPaths = { | ||
| [P in keyof paths]: paths[P] extends { post: any } ? P : never; | ||
| }[keyof paths]; | ||
|
|
||
| type PutPaths = { | ||
| [P in keyof paths]: paths[P] extends { put: any } ? P : never; | ||
| }[keyof paths]; | ||
|
|
||
| type DeletePaths = { | ||
| [P in keyof paths]: paths[P] extends { delete: any } ? P : never; | ||
| }[keyof paths]; | ||
|
|
||
| type PatchPaths = { | ||
| [P in keyof paths]: paths[P] extends { patch: any } ? P : never; | ||
| }[keyof paths]; |
There was a problem hiding this comment.
Replace any with unknown to comply with TS guidelines
Project guideline: never use any. These can safely be unknown.
type GetPaths = {
- [P in keyof paths]: paths[P] extends { get: any } ? P : never;
+ [P in keyof paths]: paths[P] extends { get: unknown } ? P : never;
}[keyof paths];
type PostPaths = {
- [P in keyof paths]: paths[P] extends { post: any } ? P : never;
+ [P in keyof paths]: paths[P] extends { post: unknown } ? P : never;
}[keyof paths];
type PutPaths = {
- [P in keyof paths]: paths[P] extends { put: any } ? P : never;
+ [P in keyof paths]: paths[P] extends { put: unknown } ? P : never;
}[keyof paths];
type DeletePaths = {
- [P in keyof paths]: paths[P] extends { delete: any } ? P : never;
+ [P in keyof paths]: paths[P] extends { delete: unknown } ? P : never;
}[keyof paths];
type PatchPaths = {
- [P in keyof paths]: paths[P] extends { patch: any } ? P : never;
+ [P in keyof paths]: paths[P] extends { patch: unknown } ? P : never;
}[keyof paths];
export async function fetchServerData<T>(
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
path: keyof paths,
- options?: any,
+ options?: unknown,
): Promise<T | null> {
try {
const client = await createServerApiClient();
- let response: { data?: T; error?: any };
+ let response: { data?: T; error?: unknown };Also applies to: 56-56, 61-61
🤖 Prompt for AI Agents
In apps/playground/src/lib/server-api.ts around lines 33 to 50 (and also at
lines 56 and 61), the type mappings use `any` for HTTP verb shapes; replace each
`any` with `unknown` so the mapped conditional types read e.g. `paths[P] extends
{ get: unknown } ? P : never`, doing the same for post/put/delete/patch
occurrences and the two additional locations to follow the project guideline
forbidding `any`.
| const newSize = | ||
| (size ?? Children.count(props.children) > 1) ? "default" : "icon"; | ||
|
|
There was a problem hiding this comment.
Fix size resolution: size prop is ignored due to operator precedence.
The current expression treats any provided size (string) as truthy and always chooses "default". Use nullish coalescing only to pick between an inferred size and the provided prop.
Apply this diff:
- const newSize =
- (size ?? Children.count(props.children) > 1) ? "default" : "icon";
+ const newSize =
+ size ?? (Children.count(props.children) > 1 ? "default" : "icon");📝 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.
| const newSize = | |
| (size ?? Children.count(props.children) > 1) ? "default" : "icon"; | |
| const newSize = | |
| size ?? (Children.count(props.children) > 1 ? "default" : "icon"); |
🤖 Prompt for AI Agents
In apps/playground/src/components/ai-elements/prompt-input.tsx around lines
541-543, the size resolution uses (size ?? Children.count(props.children) > 1) ?
"default" : "icon" so the provided size is effectively ignored due to operator
precedence; replace this with logic that first computes the inferred size from
children (e.g. inferred = Children.count(props.children) > 1 ? "default" :
"icon") and then use nullish coalescing to prefer the provided prop (e.g.
newSize = size ?? inferred), updating the assignment accordingly.
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (12)
apps/playground/.gitignore (2)
34-36: Allow committing env templatesIf you keep example env files in VCS, add negations so they’re not ignored.
# env files (can opt-in for committing if needed) .env* +!.env.example +!.env.local.example
7-11: Optional: consider whitelisting Yarn SDKsIf you use Yarn Berry editor SDKs and want them versioned, also allow .yarn/sdks.
.yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/versions +!.yarn/sdksCan you confirm whether Yarn SDKs are used in this workspace?
apps/playground/src/components/ui/collapsible.tsx (1)
1-31: Forward refs to Radix primitives.Current wrappers drop refs, which can break consumers needing focus/measurement. Forward them and tighten prop types.
Apply this refactor:
"use client"; - -import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; - -function Collapsible({ - ...props -}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) { - return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />; -} - -function CollapsibleTrigger({ - ...props -}: React.ComponentProps<typeof CollapsiblePrimitive.Trigger>) { - return ( - <CollapsiblePrimitive.Trigger - data-slot="collapsible-trigger" - {...props} - /> - ); -} - -function CollapsibleContent({ - ...props -}: React.ComponentProps<typeof CollapsiblePrimitive.Content>) { - return ( - <CollapsiblePrimitive.Content - data-slot="collapsible-content" - {...props} - /> - ); -} +import * as React from "react"; +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; + +type RootProps = React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Root>; +type TriggerProps = React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Trigger>; +type ContentProps = React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Content>; + +const Collapsible = React.forwardRef< + React.ElementRef<typeof CollapsiblePrimitive.Root>, + RootProps +>(({ ...props }, ref) => ( + <CollapsiblePrimitive.Root ref={ref} data-slot="collapsible" {...props} /> +)); +Collapsible.displayName = "Collapsible"; + +const CollapsibleTrigger = React.forwardRef< + React.ElementRef<typeof CollapsiblePrimitive.Trigger>, + TriggerProps +>(({ ...props }, ref) => ( + <CollapsiblePrimitive.Trigger ref={ref} data-slot="collapsible-trigger" {...props} /> +)); +CollapsibleTrigger.displayName = "CollapsibleTrigger"; + +const CollapsibleContent = React.forwardRef< + React.ElementRef<typeof CollapsiblePrimitive.Content>, + ContentProps +>(({ ...props }, ref) => ( + <CollapsiblePrimitive.Content ref={ref} data-slot="collapsible-content" {...props} /> +)); +CollapsibleContent.displayName = "CollapsibleContent";apps/playground/src/components/ui/carousel.tsx (4)
78-89: Keyboard UX: don’t capture Arrow keys from children; support vertical keys; make container focusableCapturing on the container will steal Arrow keys from inputs inside slides. Handle keys only when the container itself is focused, and map Up/Down for vertical orientation. Also add tabIndex to make the region focusable.
-const handleKeyDown = React.useCallback( - (event: React.KeyboardEvent<HTMLDivElement>) => { - if (event.key === "ArrowLeft") { - event.preventDefault(); - scrollPrev(); - } else if (event.key === "ArrowRight") { - event.preventDefault(); - scrollNext(); - } - }, - [scrollPrev, scrollNext], -); +const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent<HTMLDivElement>) => { + // Ignore events from descendants (e.g., inputs inside slides) + if (event.currentTarget !== event.target) return; + if (orientation === "horizontal") { + if (event.key === "ArrowLeft") { + event.preventDefault(); + scrollPrev(); + } else if (event.key === "ArrowRight") { + event.preventDefault(); + scrollNext(); + } + } else { + if (event.key === "ArrowUp") { + event.preventDefault(); + scrollPrev(); + } else if (event.key === "ArrowDown") { + event.preventDefault(); + scrollNext(); + } + } + }, + [orientation, scrollPrev, scrollNext], +);-<div - onKeyDownCapture={handleKeyDown} +<div + tabIndex={0} + onKeyDown={handleKeyDown} className={cn("relative", className)} role="region" aria-roledescription="carousel" data-slot="carousel" {...props} >Also applies to: 121-128
113-115: Simplify orientation in context (fallback is dead code)
orientationis always defined due to the default prop, so theopts?.axisfallback never runs.- orientation: - orientation || (opts?.axis === "y" ? "vertical" : "horizontal"), + orientation,
124-126: A11y: ensure the region has an accessible nameThe region has role and roledescription but no accessible name. Please pass either aria-label or aria-labelledby via props at call sites.
160-170: A11y: consider slide labelling (e.g., “Slide X of Y”)To improve SR context, expose slide count/current index in context and let items set aria-label accordingly, or allow passing an aria-label per item.
apps/playground/src/components/playground/chat-sidebar.tsx (4)
134-143: Only clear the current chat after a successful deletion.Unconditionally clearing on the client can desync UI if the server delete fails. Use per-call onSuccess.
- deleteChat.mutate({ - params: { - path: { id: chatId }, - }, - }); - if (currentChatId === chatId) { - clearMessages(); - onChatSelect?.(""); - } + deleteChat.mutate( + { + params: { + path: { id: chatId }, + }, + }, + { + onSuccess: () => { + if (currentChatId === chatId) { + clearMessages(); + onChatSelect?.(""); + } + }, + }, + );
16-16: Memoize chat grouping/sorting to avoid recomputation on every render.This list can be large; memoize based on chats to reduce work.
-import { useState } from "react"; +import { useMemo, useState } from "react"; @@ - const chatGroups = groupChatsByDate( - [...chats].sort( - (a, b) => - new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(), - ), - ); + const chatGroups = useMemo(() => { + const sorted = [...chats].sort( + (a, b) => + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(), + ); + return groupChatsByDate(sorted); + }, [chats]);Also applies to: 191-196
109-120: Exit edit mode only after a successful rename.Currently the UI exits edit mode even on failure. Defer clearing state to mutation success.
- const saveTitle = (chatId: string) => { - if (editTitle.trim()) { - updateChat.mutate({ - params: { - path: { id: chatId }, - }, - body: { title: editTitle.trim() }, - }); - } - setEditingId(null); - setEditTitle(""); - }; + const saveTitle = (chatId: string) => { + const newTitle = editTitle.trim(); + if (!newTitle) { + setEditingId(null); + setEditTitle(""); + return; + } + updateChat.mutate( + { + params: { path: { id: chatId } }, + body: { title: newTitle }, + }, + { + onSuccess: () => { + setEditingId(null); + setEditTitle(""); + }, + }, + ); + };
3-3: Make “Yesterday” label calendar-aware (optional).Using <48h can mislabel items; use calendar-day difference for better UX.
-import { format } from "date-fns"; +import { differenceInCalendarDays, format } from "date-fns"; @@ - const formatDate = (dateString: string) => { + const formatDate = (dateString: string) => { const date = new Date(dateString); const now = new Date(); - const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60); - - if (diffInHours < 1) { + const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60); + const dayDiff = differenceInCalendarDays(now, date); + + if (diffInHours < 1) { return "Just now"; - } else if (diffInHours < 24) { + } else if (diffInHours < 24) { return `${Math.floor(diffInHours)}h ago`; - } else if (diffInHours < 48) { - return "Yesterday"; + } else if (dayDiff === 1) { + return "Yesterday"; } else { return format(date, "MMM d"); } };Also applies to: 145-159
apps/playground/src/components/playground/chat-ui.tsx (1)
175-231: Wire attachments end‑to‑end (optional).You accept files (globalDrop, add attachments action) but don’t send them or pass to onUserMessage. Consider:
- Appending image parts to the UIMessage when supportsImages is true.
- Passing message.files to onUserMessage so persistence includes attachments.
- Avoid using any for attachment types per project guidelines.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (8)
apps/playground/.gitignore(1 hunks)apps/playground/package.json(1 hunks)apps/playground/src/components/playground/chat-page-client.tsx(1 hunks)apps/playground/src/components/playground/chat-sidebar.tsx(1 hunks)apps/playground/src/components/playground/chat-ui.tsx(1 hunks)apps/playground/src/components/ui/carousel.tsx(1 hunks)apps/playground/src/components/ui/collapsible.tsx(1 hunks)apps/playground/src/components/ui/progress.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/playground/package.json
- apps/playground/src/components/playground/chat-page-client.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never useanyoras anyin this TypeScript project unless absolutely necessary
Always use top-levelimport; never userequireor dynamic imports
Files:
apps/playground/src/components/ui/collapsible.tsxapps/playground/src/components/playground/chat-ui.tsxapps/playground/src/components/ui/progress.tsxapps/playground/src/components/playground/chat-sidebar.tsxapps/playground/src/components/ui/carousel.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-21T20:12:20.016Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-21T20:12:20.016Z
Learning: Applies to **/*.{ts,tsx} : Never use `any` or `as any` in this TypeScript project unless absolutely necessary
Applied to files:
apps/playground/src/components/playground/chat-ui.tsx
🧬 Code graph analysis (2)
apps/playground/src/components/playground/chat-ui.tsx (5)
apps/playground/src/components/ai-elements/conversation.tsx (3)
Conversation(12-20)ConversationContent(26-31)ConversationEmptyState(39-66)apps/playground/src/components/ai-elements/suggestion.tsx (2)
Suggestions(13-24)Suggestion(31-56)apps/playground/src/components/ai-elements/response.tsx (1)
Response(9-20)apps/playground/src/components/ai-elements/actions.tsx (2)
Actions(15-19)Action(26-65)apps/playground/src/components/ai-elements/prompt-input.tsx (11)
PromptInput(219-445)PromptInputBody(449-454)PromptInputTextarea(458-503)PromptInputToolbar(507-515)PromptInputTools(519-531)PromptInputActionMenu(561-563)PromptInputActionMenuTrigger(568-578)PromptInputActionMenuContent(583-588)PromptInputActionAddAttachments(172-189)PromptInputButton(535-558)PromptInputSubmit(607-636)
apps/playground/src/components/playground/chat-sidebar.tsx (5)
apps/playground/src/hooks/useUser.ts (1)
useUser(25-111)apps/playground/src/lib/auth-client.ts (1)
useAuth(20-33)apps/playground/src/hooks/useOrganization.ts (1)
useOrganization(10-30)apps/playground/src/hooks/useChats.ts (4)
useChats(25-29)useDeleteChat(90-109)useUpdateChat(69-88)Chat(6-14)apps/playground/src/components/credits/credits-display.tsx (1)
CreditsDisplay(18-78)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: lint / run
- GitHub Check: test / run
- GitHub Check: generate / run
- GitHub Check: build / run
- GitHub Check: autofix
🔇 Additional comments (5)
apps/playground/.gitignore (1)
16-19: Remove nonstandard .next-dev; keep .next.next-dev isn’t a standard Next.js output. Drop it to avoid confusion; .next already covers dev/build artifacts.
# next.js -/.next-dev/ /.next/ /out/apps/playground/src/components/ui/progress.tsx (1)
1-7: Imports and client directive look good.Top-level imports and no any/as-any usage align with project rules.
apps/playground/src/components/playground/chat-sidebar.tsx (2)
87-102: Await signOut and handle navigation explicitly (remove non‑standard onSuccess).Avoid relying on client-specific callbacks. Await signOut and then clear cache + redirect in a finally block.
const logout = async () => { posthog.reset(); - - await signOut({ - fetchOptions: { - onSuccess: () => { - queryClient.clear(); - router.push( - process.env.NODE_ENV === "development" - ? "http://localhost:3002/login" - : "https://llmgateway.io/login", - ); - }, - }, - }); + try { + await signOut(); + } finally { + queryClient.clear(); + router.push( + process.env.NODE_ENV === "development" + ? "http://localhost:3002/login" + : "https://llmgateway.io/login", + ); + } };
122-133: Don’t gate chat deletion on presence of an API key; let the server authorize.Client-side API key checks are a UX/security mismatch. Rely on session auth and server-side permission checks instead.
- if (!userApiKey) { - toast.error("API Key Required", { - description: "You must provide an API key to delete chats.", - style: { - backgroundColor: "var(--destructive)", - color: "var(--destructive-foreground)", - }, - }); - return; - }apps/playground/src/components/playground/chat-ui.tsx (1)
187-203: Await sendMessage so errors are caught; move clear after success.Current try/catch won’t catch async failures; also preserves input on failure.
- setText(""); // Clear input immediately - - // Call sendMessage which will handle adding the user message and API request - sendMessage( + // Call sendMessage and await to catch errors; clear input on success + await sendMessage( { id: crypto.randomUUID(), role: "user", parts: [{ type: "text", text: textContent }], }, { body: { apiKey: userApiKey, model: selectedModel, }, }, ); + setText("");
| @@ -0,0 +1,33 @@ | |||
| "use client"; | |||
|
|
|||
| import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; | |||
There was a problem hiding this comment.
Import React types (or avoid the React namespace).
Using React.ComponentProps<…> without importing React types will fail TS (“Cannot find namespace 'React'”).
Minimal fix:
"use client";
+import type * as React from "react";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";Alternatively (cleaner), avoid the React namespace:
-import type * as React from "react";
+import type { ComponentProps } from "react";
-function Collapsible({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
+function Collapsible({ ...props }: ComponentProps<typeof CollapsiblePrimitive.Root>) {Also applies to: 5-9, 11-13, 22-24
🤖 Prompt for AI Agents
In apps/playground/src/components/ui/collapsible.tsx around lines 3, 5-9, 11-13
and 22-24, the code uses React.ComponentProps<...> without importing React types
which breaks TypeScript. Fix by either adding an explicit type-only import
(import type React from "react";) at the top, or (preferred) remove the React
namespace and use ComponentProps<typeof X> (e.g., ComponentProps<typeof
CollapsiblePrimitive.Root>) for each occurrence; update all referenced types
accordingly so no React namespace remains or so React is imported as a type.
| function CollapsibleTrigger({ | ||
| ...props | ||
| }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) { | ||
| return ( | ||
| <CollapsiblePrimitive.CollapsibleTrigger | ||
| data-slot="collapsible-trigger" | ||
| {...props} | ||
| /> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Fix invalid Radix component names (use Trigger/Content).
@radix-ui/react-collapsible exports Trigger and Content, not CollapsibleTrigger/CollapsibleContent. As written, the components will be undefined at runtime and crash (“Element type is invalid…”).
Apply this diff:
-function CollapsibleTrigger({
- ...props
-}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
+function CollapsibleTrigger({
+ ...props
+}: React.ComponentProps<typeof CollapsiblePrimitive.Trigger>) {
return (
- <CollapsiblePrimitive.CollapsibleTrigger
+ <CollapsiblePrimitive.Trigger
data-slot="collapsible-trigger"
{...props}
/>
);
}
-function CollapsibleContent({
- ...props
-}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
+function CollapsibleContent({
+ ...props
+}: React.ComponentProps<typeof CollapsiblePrimitive.Content>) {
return (
- <CollapsiblePrimitive.CollapsibleContent
+ <CollapsiblePrimitive.Content
data-slot="collapsible-content"
{...props}
/>
);
}Also applies to: 22-31
🤖 Prompt for AI Agents
In apps/playground/src/components/ui/collapsible.tsx around lines 11-20 (and
similarly at 22-31), the code uses non-existent exports
CollapsibleTrigger/CollapsibleContent from @radix-ui/react-collapsible which
will be undefined at runtime; replace those references with the correct exports
Trigger and Content (e.g., use CollapsiblePrimitive.Trigger and
CollapsiblePrimitive.Content), update the React.ComponentProps type parameters
accordingly (React.ComponentProps<typeof CollapsiblePrimitive.Trigger> / typeof
CollapsiblePrimitive.Content), and keep the existing props and attributes
(data-slot, spread props) unchanged.
| function Progress({ | ||
| className, | ||
| value, | ||
| ...props | ||
| }: React.ComponentProps<typeof ProgressPrimitive.Root>) { | ||
| return ( | ||
| <ProgressPrimitive.Root | ||
| data-slot="progress" | ||
| className={cn( | ||
| "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", | ||
| className, | ||
| )} | ||
| {...props} | ||
| > | ||
| <ProgressPrimitive.Indicator | ||
| data-slot="progress-indicator" | ||
| className="bg-primary h-full w-full flex-1 transition-all" | ||
| style={{ transform: `translateX(-${100 - (value || 0)}%)` }} | ||
| /> | ||
| </ProgressPrimitive.Root> | ||
| ); | ||
| } | ||
|
|
||
| export { Progress }; |
There was a problem hiding this comment.
Pass value to Root (a11y) and support refs; clamp value and honor max.
Currently value is stripped from props and never forwarded to ProgressPrimitive.Root, so aria-valuenow is missing and the component is not fully accessible. Also, the component doesn’t support ref forwarding. Suggest refactor below.
-function Progress({
- className,
- value,
- ...props
-}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
- return (
- <ProgressPrimitive.Root
- data-slot="progress"
- className={cn(
- "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
- className,
- )}
- {...props}
- >
- <ProgressPrimitive.Indicator
- data-slot="progress-indicator"
- className="bg-primary h-full w-full flex-1 transition-all"
- style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
- />
- </ProgressPrimitive.Root>
- );
-}
+const Progress = React.forwardRef<
+ React.ElementRef<typeof ProgressPrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
+>(({ className, value, max = 100, ...props }, ref) => {
+ const clampedValue = Math.max(0, Math.min(value ?? 0, max));
+ const percent = (clampedValue / max) * 100;
+ return (
+ <ProgressPrimitive.Root
+ data-slot="progress"
+ ref={ref}
+ className={cn(
+ "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
+ className,
+ )}
+ value={clampedValue}
+ max={max}
+ {...props}
+ >
+ <ProgressPrimitive.Indicator
+ data-slot="progress-indicator"
+ className="bg-primary h-full w-full flex-1 transition-all"
+ style={{ transform: `translateX(-${100 - percent}%)` }}
+ />
+ </ProgressPrimitive.Root>
+ );
+});
+
+Progress.displayName = "Progress";🤖 Prompt for AI Agents
In apps/playground/src/components/ui/progress.tsx around lines 8 to 31, the
component currently strips value from props, never forwards it to
ProgressPrimitive.Root (so aria-valuenow is missing), and doesn’t support refs
or respect a max prop; refactor to use React.forwardRef to forward the ref to
ProgressPrimitive.Root, accept and forward value and max (default max=100) to
the Root so aria attributes are present, clamp the numeric value between 0 and
max before passing it and using it to compute the indicator transform, and
continue to spread remaining props onto ProgressPrimitive.Root.
8b29867 to
12d6839
Compare
12d6839 to
e406782
Compare
…iles Added support for the "playground" service in Docker build configurations, supervisord, and CI workflows. Adjusted "playground" build script for consistency.
2320e79 to
2063e8a
Compare
There was a problem hiding this comment.
Actionable comments posted: 21
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/ui/src/app/playground/playground-client.tsx (2)
243-255: Non-OK HTTP responses can slip through without throwing — always throwIf the error payload parses but lacks an
errorfield, the code proceeds as if success. Force a throw for all non-OK responses.Apply:
- try { - const errorJson = JSON.parse(errorText); - if (errorJson.error) { - setError(errorJson.error); - throw new Error(errorJson.error); - } - } catch { - setError(`HTTP ${response.status}: ${errorText || "Unknown error"}`); - throw new Error(`HTTP ${response.status}: ${errorText}`); - } + try { + const errorJson = JSON.parse(errorText); + const message = + errorJson.error || + errorJson.message || + `HTTP ${response.status}: ${errorText || "Unknown error"}`; + setError(message); + throw new Error(message); + } catch { + const message = `HTTP ${response.status}: ${errorText || "Unknown error"}`; + setError(message); + throw new Error(message); + }
266-306: Replaceany[]with a concrete image type (project guideline: noany)Strongly type streamed image parts to match
Message['images'].Apply:
- let fullContent = ""; - let finalImages: any[] = []; // Track final images received during streaming + let fullContent = ""; + type ImagePart = NonNullable<Message["images"]>[number]; + let finalImages: ImagePart[] = []; // Track final images received during streaming let hasReceivedImages = false; // Track if we received images during streaming @@ - let imagesToSet: any[] | undefined; + let imagesToSet: ImagePart[] | undefined; if (deltaImages && deltaImages.length > 0) { - imagesToSet = deltaImages; - finalImages = [...deltaImages]; // Track final images + imagesToSet = deltaImages as ImagePart[]; + finalImages = [...(deltaImages as ImagePart[])]; // Track final images hasReceivedImages = true; // Mark that we received images } else if (images && images.length > 0) { - imagesToSet = images; - finalImages = [...images]; // Track final images + imagesToSet = images as ImagePart[]; + finalImages = [...(images as ImagePart[])]; // Track final images hasReceivedImages = true; // Mark that we received images }
🧹 Nitpick comments (36)
apps/ui/src/components/app-sidebar.tsx (1)
6-6: Keep toast imports flowing through our shared wrapper.We already expose
toastvia@/lib/components/use-toastto ensure consistent styling and provider configuration. Pulling directly from"sonner"breaks that contract and risks diverging behavior if we ever swap the underlying implementation.-import { toast } from "sonner"; +import { toast } from "@/lib/components/use-toast";apps/playground/package.json (1)
7-12: Make the build script portable.
Prefixing commands withNODE_OPTIONS="..."only works on POSIX shells; Windows devs will get a'NODE_OPTIONS' is not recognizedfailure. Let's route the flag throughcross-envso the build stays platform-agnostic.Apply this diff to keep the high-memory build working everywhere:
- "build": "NODE_OPTIONS=\"--max-old-space-size=8192\" tsc && NODE_OPTIONS=\"--max-old-space-size=8192\" next build", + "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 tsc && cross-env NODE_OPTIONS=--max-old-space-size=8192 next build", @@ - "@types/react-syntax-highlighter": "15.5.13", + "@types/react-syntax-highlighter": "15.5.13", + "cross-env": "7.0.3",apps/playground/src/components/credits/top-up-credits-dialog.tsx (2)
338-341: Surface an error when Stripe/elements are not ready.Return silently hides the failure state.
- if (!stripe || !elements) { - return; - } + if (!stripe || !elements) { + toast.error("Payment form unavailable. Please try again later."); + return; + }
349-352: Avoid non-null assertions on CardElement; guard and reuse instance.Prevents runtime NPE if the element isn’t mounted yet.
+ const cardElement = elements.getElement(CardElement); + if (!cardElement) { + toast.error("Card element not ready. Please try again."); + setLoading(false); + return; + } const setupResult = await stripe.confirmCardSetup(setupSecret, { payment_method: { - card: elements.getElement(CardElement)!, + card: cardElement, }, }); ... const result = await stripe.confirmCardPayment(clientSecret, { payment_method: { - card: elements.getElement(CardElement)!, + card: cardElement, }, });Also applies to: 370-373
apps/playground/src/app/page.tsx (2)
9-13: Remove or relocate GatewayModel typeIt’s unused here and route files aren’t ideal for shared types. Move to a shared types module or remove.
-export interface GatewayModel { - id: string; - name?: string; - architecture?: { input_modalities?: string[] }; -}
17-26: Handle possible null from fetchServerDatafetchServerData returns T | null. Ensure UserProvider accepts null or guard with a fallback before rendering.
-const initialUserData = await fetchServerData<{ user: User }>( +const initialUserData = await fetchServerData<{ user: User }>( "GET", "/user/me", ); return ( - <UserProvider initialUserData={initialUserData}> + <UserProvider initialUserData={initialUserData ?? undefined}> <ChatPageClient models={models} providers={providers} /> </UserProvider> )apps/playground/src/lib/server-api.ts (4)
12-12: cookies() is synchronous; drop unnecessary awaitSmall cleanup.
-const cookieStore = await cookies(); +const cookieStore = cookies();
19-29: Avoid sending an empty Cookie headerOnly set the Cookie header when present; otherwise omit it.
- return createFetchClient<paths>({ - baseUrl: config.apiBackendUrl, - credentials: "include", - headers: { - Cookie: secureSessionCookie - ? `__Secure-${key}=${secureSessionCookie.value}` - : sessionCookie - ? `${key}=${sessionCookie.value}` - : "", - }, - }); + const cookieHeader = + secureSessionCookie + ? `__Secure-${key}=${secureSessionCookie.value}` + : sessionCookie + ? `${key}=${sessionCookie.value}` + : undefined; + + return createFetchClient<paths>({ + baseUrl: config.apiBackendUrl, + credentials: "include", + headers: cookieHeader ? { Cookie: cookieHeader } : {}, + });
53-83: Strengthen typing: bind path to method to remove castsOverload fetchServerData so path is constrained per verb and remove the casts.
-// Generic server-side data fetcher with proper typing -export async function fetchServerData<T>( - method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", - path: keyof paths, - options?: unknown, -): Promise<T | null> { +// Generic server-side data fetcher with verb-bound paths +export async function fetchServerData<T>(method: "GET", path: GetPaths, options?: unknown): Promise<T | null>; +export async function fetchServerData<T>(method: "POST", path: PostPaths, options?: unknown): Promise<T | null>; +export async function fetchServerData<T>(method: "PUT", path: PutPaths, options?: unknown): Promise<T | null>; +export async function fetchServerData<T>(method: "DELETE", path: DeletePaths, options?: unknown): Promise<T | null>; +export async function fetchServerData<T>(method: "PATCH", path: PatchPaths, options?: unknown): Promise<T | null>; +export async function fetchServerData<T>( + method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", + path: GetPaths | PostPaths | PutPaths | DeletePaths | PatchPaths, + options?: unknown, +): Promise<T | null> { try { const client = await createServerApiClient(); - let response: { data?: T; error?: unknown }; + let response: { data?: T; error?: unknown }; const requestOptions = options || {}; switch (method) { case "GET": - response = await client.GET(path as GetPaths, requestOptions); + response = await client.GET(path, requestOptions as never); break; case "POST": - response = await client.POST(path as PostPaths, requestOptions); + response = await client.POST(path, requestOptions as never); break; case "PUT": - response = await client.PUT(path as PutPaths, requestOptions); + response = await client.PUT(path, requestOptions as never); break; case "DELETE": - response = await client.DELETE(path as DeletePaths, requestOptions); + response = await client.DELETE(path, requestOptions as never); break; case "PATCH": - response = await client.PATCH(path as PatchPaths, requestOptions); + response = await client.PATCH(path, requestOptions as never); break; default: throw new Error(`Unsupported HTTP method: ${method}`); }Note: The
as nevercast keeps types strict without introducingany. If desired, we can further type options per method/path using the openapi-fetch types.
19-29: Consider adding a default timeout via AbortControllerPrevent hanging requests from tying up server resources.
I can add a small helper to inject a signal with a 15s timeout into requestOptions; want me to patch it?
Also applies to: 53-94
apps/playground/src/hooks/useAutoApiKey.ts (1)
16-17: Tidy: remove unused setter and console.log; narrow deps.
setUserApiKeyisn’t used;console.logshould be dropped; keep deps minimal.@@ - const { userApiKey, setUserApiKey, isLoaded } = useApiKey(); + const { userApiKey, isLoaded } = useApiKey(); @@ - console.log("User has auto-generated API key available"); + // user has an auto-generated key available @@ - }, [isLoaded, userApiKey, isLoading, apiKeysData, setUserApiKey]); + }, [isLoaded, userApiKey, isLoading, apiKeysData]);Also applies to: 56-61, 62-62
apps/playground/src/components/model-selector.tsx (1)
95-101: Guard background color when provider.color is undefined.Avoid producing
backgroundColor: "undefined15".- <div - className="p-2 rounded-lg" - style={{ backgroundColor: `${provider?.color}15` }} - > + <div + className="p-2 rounded-lg" + style={provider?.color ? { backgroundColor: `${provider.color}15` } : undefined} + >apps/playground/src/components/playground/api-key-manager.tsx (1)
69-74: Clipboard copy: add error handling fallback.
navigator.clipboard.writeTextcan reject (insecure context/permissions).- navigator.clipboard.writeText(newApiKey); - toast.success("API Key Copied", { - description: "The API key has been copied to your clipboard.", - }); + navigator.clipboard.writeText(newApiKey) + .then(() => + toast.success("API Key Copied", { description: "The API key has been copied to your clipboard." }), + ) + .catch(() => + toast.error("Copy failed", { description: "Select and copy the key manually." }), + )apps/playground/src/app/login/page.tsx (2)
58-63: Guard WebAuthn autofill check.Minor: Add a safe window check to avoid edge cases and make intent explicit.
-useEffect(() => { - if (window.PublicKeyCredential) { +useEffect(() => { + if (typeof window !== "undefined" && "PublicKeyCredential" in window) { void signIn.passkey({ autoFill: true }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Only run once on mount for autofill
41-44: Redirect target inconsistency (useUser → "/" vs post-login → "/dashboard").Authenticated users are redirected to "/" by useUser on mount, but success handlers navigate to "/dashboard". Align targets to prevent flicker/back navigation surprises.
Also applies to: 84-85
apps/playground/src/hooks/useUser.ts (1)
65-69: Use replace and path guard for onboarding redirect to avoid flicker/loops.- if (!data.user.onboardingCompleted) { - router.push("/onboarding"); - } + if (!data.user.onboardingCompleted && pathname !== "/onboarding") { + router.replace("/onboarding"); + }apps/ui/src/app/playground/playground-client.tsx (2)
370-372: Keep error objects for observability; log in dev along with toastUse a caught error binding and log it in development to aid debugging while keeping user-facing toasts.
Apply:
- } catch { - toast.error("Failed to save assistant message"); + } catch (err) { + toast.error("Failed to save assistant message"); + if (process.env.NODE_ENV !== "production") console.error(err); }And:
- } catch { - toast.error("Failed to save assistant message"); + } catch (err) { + toast.error("Failed to save assistant message"); + if (process.env.NODE_ENV !== "production") console.error(err); }Also applies to: 410-412
417-421: Avoid shadowingerrorstate; don’t overwrite specific server errorsRename the catch binding and only set the generic error if no error is already set by upstream code.
Apply:
- } catch (error) { + } catch (err) { toast.error("Error sending message"); - if (error instanceof Error && !error.message.includes("HTTP")) { - setError("Failed to send message. Please try again."); - } + if (err instanceof Error && !err.message.includes("HTTP") && !error) { + setError("Failed to send message. Please try again."); + }apps/playground/src/components/ai-elements/chain-of-thought.tsx (3)
31-39: Optionally export the hook for advanced compositionExporting useChainOfThought enables external toggles and custom controls without prop drilling.
-const useChainOfThought = () => { +export const useChainOfThought = () => {
130-135: Hoist statusStyles to module scopeAvoid re-creating the object on every render; keeps referential stability.
- const statusStyles = { - complete: "text-muted-foreground", - active: "text-foreground", - pending: "text-muted-foreground/50", - }; + const statusStyles = STATUS_STYLES;Add at module top (outside components):
const STATUS_STYLES = { complete: "text-muted-foreground", active: "text-foreground", pending: "text-muted-foreground/50", } as const;
213-221: Prefer semantic figure/figcaption for imagesImproves a11y and semantics for visual content with captions.
- <div className={cn("mt-2 space-y-2", className)} {...props}> - <div className="relative flex max-h-[22rem] items-center justify-center overflow-hidden rounded-lg bg-muted p-3"> - {children} - </div> - {caption && <p className="text-muted-foreground text-xs">{caption}</p>} - </div> + <figure className={cn("mt-2 space-y-2", className)} {...props}> + <div className="relative flex max-h-[22rem] items-center justify-center overflow-hidden rounded-lg bg-muted p-3"> + {children} + </div> + {caption && <figcaption className="text-muted-foreground text-xs">{caption}</figcaption>} + </figure>apps/playground/src/components/playground/chat-sidebar.tsx (2)
109-120: Defer exiting edit mode until rename succeedsCurrently editing closes even if the rename fails. Close on success to preserve user input on error.
- const saveTitle = (chatId: string) => { - if (editTitle.trim()) { - updateChat.mutate({ - params: { - path: { id: chatId }, - }, - body: { title: editTitle.trim() }, - }); - } - setEditingId(null); - setEditTitle(""); - }; + const saveTitle = (chatId: string) => { + const title = editTitle.trim(); + if (!title) { + setEditingId(null); + setEditTitle(""); + return; + } + updateChat.mutate( + { + params: { path: { id: chatId } }, + body: { title }, + }, + { + onSuccess: () => { + setEditingId(null); + setEditTitle(""); + }, + }, + ); + };
145-159: Improve relative time formatting for sub-hour rangesShow minutes for <1h; reserve “Just now” for <1m.
- const formatDate = (dateString: string) => { - const date = new Date(dateString); - const now = new Date(); - const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60); - - if (diffInHours < 1) { - return "Just now"; - } else if (diffInHours < 24) { - return `${Math.floor(diffInHours)}h ago`; - } else if (diffInHours < 48) { - return "Yesterday"; - } else { - return format(date, "MMM d"); - } - }; + const formatDate = (dateString: string) => { + const date = new Date(dateString); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / (1000 * 60)); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + if (diffMins < 1) return "Just now"; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffHours < 48) return "Yesterday"; + return format(date, "MMM d"); + };apps/playground/src/components/ui/sheet.tsx (3)
75-77: Prevent accidental form submits and fix non-standard class.Close button inside forms may submit by default; also Tailwind uses focus:outline-none (not focus:outline-hidden).
Apply this diff:
- <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> + <SheetPrimitive.Close + type="button" + className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none" + >
61-69: Ensure scroll handling for large content.Add overflow and max-height so sheets don’t overflow the viewport.
Apply this diff:
- side === "right" && - "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", + side === "right" && + "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 overflow-y-auto border-l sm:max-w-sm", side === "left" && - "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", + "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 overflow-y-auto border-r sm:max-w-sm", side === "top" && - "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", + "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto max-h-[85vh] overflow-y-auto border-b", side === "bottom" && - "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", + "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto max-h-[85vh] overflow-y-auto border-t",
47-82: Forward ref through SheetContent for composability.Enables consumers to focus/measure the content element (consistent with Radix patterns).
Apply this diff:
-function SheetContent({ - className, - children, - side = "right", - ...props -}: React.ComponentProps<typeof SheetPrimitive.Content> & { - side?: "top" | "right" | "bottom" | "left"; -}) { - return ( - <SheetPortal> - <SheetOverlay /> - <SheetPrimitive.Content - data-slot="sheet-content" - className={cn( - "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500", - side === "right" && - "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", - side === "left" && - "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", - side === "top" && - "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", - side === "bottom" && - "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", - className, - )} - {...props} - > - {children} - <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> - <XIcon className="size-4" /> - <span className="sr-only">Close</span> - </SheetPrimitive.Close> - </SheetPrimitive.Content> - </SheetPortal> - ); -} +type SheetContentProps = React.ComponentProps<typeof SheetPrimitive.Content> & { + side?: "top" | "right" | "bottom" | "left"; +}; + +const SheetContent = React.forwardRef< + React.ElementRef<typeof SheetPrimitive.Content>, + SheetContentProps +>(({ className, children, side = "right", ...props }, ref) => { + return ( + <SheetPortal> + <SheetOverlay /> + <SheetPrimitive.Content + ref={ref} + data-slot="sheet-content" + className={cn( + "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500", + side === "right" && + "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 overflow-y-auto border-l sm:max-w-sm", + side === "left" && + "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 overflow-y-auto border-r sm:max-w-sm", + side === "top" && + "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto max-h-[85vh] overflow-y-auto border-b", + side === "bottom" && + "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto max-h-[85vh] overflow-y-auto border-t", + className, + )} + {...props} + > + {children} + <SheetPrimitive.Close + type="button" + className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none" + > + <X className="size-4" /> + <span className="sr-only">Close</span> + </SheetPrimitive.Close> + </SheetPrimitive.Content> + </SheetPortal> + ); +}); +SheetContent.displayName = "SheetContent";apps/playground/src/components/ui/checkbox.tsx (1)
9-30: Forward ref the Root for focus management and parity with other UI primitivesEnables parent components to focus the checkbox, integrate with forms, and keeps API consistent. Also set a displayName.
-function Checkbox({ - className, - ...props -}: React.ComponentProps<typeof CheckboxPrimitive.Root>) { - return ( - <CheckboxPrimitive.Root +const Checkbox = React.forwardRef< + React.ElementRef<typeof CheckboxPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> +>(({ className, ...props }, ref) => { + return ( + <CheckboxPrimitive.Root + ref={ref} data-slot="checkbox" className={cn( "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", className, )} {...props} > <CheckboxPrimitive.Indicator data-slot="checkbox-indicator" className="flex items-center justify-center text-current transition-none" > <CheckIcon className="size-3.5" /> </CheckboxPrimitive.Indicator> </CheckboxPrimitive.Root> - ); -} + ); +}); + +Checkbox.displayName = "Checkbox";apps/playground/src/components/ui/input.tsx (1)
5-19: Forward ref the input; default type to textForwarding the ref improves form integration and focus control. Defaulting type avoids accidental “no type” cases and keeps SSR/CSR consistent.
-function Input({ className, type, ...props }: React.ComponentProps<"input">) { - return ( - <input - type={type} +const Input = React.forwardRef< + React.ElementRef<"input">, + React.ComponentPropsWithoutRef<"input"> +>(({ className, type = "text", ...props }, ref) => { + return ( + <input + ref={ref} + type={type} data-slot="input" className={cn( "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", className, )} {...props} /> - ); -} + ); +}); + +Input.displayName = "Input";apps/playground/src/components/ui/tooltip.tsx (2)
37-59: Forward ref TooltipContent for composition and measurementForwarding the ref allows parent layers to measure/position or manage focus. Keep API intact.
-function TooltipContent({ - className, - sideOffset = 0, - children, - ...props -}: React.ComponentProps<typeof TooltipPrimitive.Content>) { - return ( - <TooltipPrimitive.Portal> - <TooltipPrimitive.Content - data-slot="tooltip-content" - sideOffset={sideOffset} - className={cn( - "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance", - className, - )} - {...props} - > - {children} - <TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> - </TooltipPrimitive.Content> - </TooltipPrimitive.Portal> - ); -} +const TooltipContent = React.forwardRef< + React.ElementRef<typeof TooltipPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> +>(({ className, sideOffset = 0, children, ...props }, ref) => { + return ( + <TooltipPrimitive.Portal> + <TooltipPrimitive.Content + ref={ref} + data-slot="tooltip-content" + sideOffset={sideOffset} + className={cn( + "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance", + className, + )} + {...props} + > + {children} + <TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> + </TooltipPrimitive.Content> + </TooltipPrimitive.Portal> + ); +}); + +TooltipContent.displayName = "TooltipContent";
21-29: Avoid double providers in consumersTooltip wraps its Root with a Provider. In apps/playground/src/components/ai-elements/actions.tsx, an extra TooltipProvider wraps Tooltip, creating nested providers. Prefer a single provider (either keep it here and remove in Actions, or drop it here and require a top-level provider).
apps/playground/src/components/ui/hover-card.tsx (1)
22-41: Forward ref HoverCardContent for consistency and focus controlBrings it in line with other Radix wrappers and allows parent control when needed.
-function HoverCardContent({ - className, - align = "center", - sideOffset = 4, - ...props -}: React.ComponentProps<typeof HoverCardPrimitive.Content>) { - return ( - <HoverCardPrimitive.Portal data-slot="hover-card-portal"> - <HoverCardPrimitive.Content - data-slot="hover-card-content" - align={align} - sideOffset={sideOffset} - className={cn( - "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden", - className, - )} - {...props} - /> - </HoverCardPrimitive.Portal> - ); -} +const HoverCardContent = React.forwardRef< + React.ElementRef<typeof HoverCardPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => { + return ( + <HoverCardPrimitive.Portal data-slot="hover-card-portal"> + <HoverCardPrimitive.Content + ref={ref} + data-slot="hover-card-content" + align={align} + sideOffset={sideOffset} + className={cn( + "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden", + className, + )} + {...props} + /> + </HoverCardPrimitive.Portal> + ); +}); + +HoverCardContent.displayName = "HoverCardContent";apps/playground/src/components/ai-elements/actions.tsx (1)
37-49: Avoid nested TooltipProvider; improve a11y label renderingDrop the extra Provider (Tooltip already includes one). Also add aria-label and render sr-only text only when present.
-import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; @@ const button = ( <Button className={cn( "relative size-9 p-1.5 text-muted-foreground hover:text-foreground", className, )} + aria-label={label || tooltip} size={size} type="button" variant={variant} {...props} > {children} - <span className="sr-only">{label || tooltip}</span> + {(label || tooltip) && ( + <span className="sr-only">{label || tooltip}</span> + )} </Button> ); @@ if (tooltip) { return ( - <TooltipProvider> - <Tooltip> - <TooltipTrigger asChild>{button}</TooltipTrigger> - <TooltipContent> - <p>{tooltip}</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> + <Tooltip> + <TooltipTrigger asChild>{button}</TooltipTrigger> + <TooltipContent> + <p>{tooltip}</p> + </TooltipContent> + </Tooltip> ); }Also applies to: 52-62
apps/playground/src/components/ui/carousel.tsx (1)
80-91: Honor vertical orientation in keyboard navigationSupport ArrowUp/ArrowDown when vertical.
- const handleKeyDown = React.useCallback( - (event: React.KeyboardEvent<HTMLDivElement>) => { - if (event.key === "ArrowLeft") { - event.preventDefault(); - scrollPrev(); - } else if (event.key === "ArrowRight") { - event.preventDefault(); - scrollNext(); - } - }, - [scrollPrev, scrollNext], - ); + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent<HTMLDivElement>) => { + if (orientation === "horizontal") { + if (event.key === "ArrowLeft") { + event.preventDefault(); + scrollPrev(); + } else if (event.key === "ArrowRight") { + event.preventDefault(); + scrollNext(); + } + } else { + if (event.key === "ArrowUp") { + event.preventDefault(); + scrollPrev(); + } else if (event.key === "ArrowDown") { + event.preventDefault(); + scrollNext(); + } + } + }, + [orientation, scrollPrev, scrollNext], + );apps/playground/src/components/ui/command.tsx (1)
27-34: Avoid disabling ESLint by using a data-attribute for styling hook.Use a standards-compliant data-attribute instead of a custom unknown attribute and drop the ESLint disable.
Apply this diff:
- // eslint-disable-next-line react/no-unknown-property - <div className="flex items-center border-b px-2 w-full" cmdk-input-wrapper=""> + <div className="flex items-center border-b px-2 w-full" data-cmdk-input-wrapper>apps/playground/src/components/ai-elements/conversation.tsx (1)
82-98: Add accessible label and return null instead of false.Improve accessibility and make return type explicit.
- return ( - !isAtBottom && ( + return isAtBottom ? null : ( <Button className={cn( "absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full", className, )} onClick={handleScrollToBottom} + aria-label="Scroll to bottom" size="icon" type="button" variant="outline" {...props} > <ArrowDownIcon className="size-4" /> </Button> - ) - ); + );apps/playground/src/components/ai-elements/web-preview.tsx (1)
185-189: Restrict sandbox defaults (dropallow-popupsby default).Popups are rarely necessary and increase risk; consider removing or gating via a prop.
- sandbox="allow-scripts allow-forms allow-popups allow-presentation" + sandbox="allow-scripts allow-forms allow-presentation"
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (10)
apps/playground/public/favicon/android-chrome-192x192.pngis excluded by!**/*.pngapps/playground/public/favicon/android-chrome-512x512.pngis excluded by!**/*.pngapps/playground/public/favicon/apple-touch-icon.pngis excluded by!**/*.pngapps/playground/public/favicon/favicon-16x16.pngis excluded by!**/*.pngapps/playground/public/favicon/favicon-32x32.pngis excluded by!**/*.pngapps/playground/public/favicon/favicon.icois excluded by!**/*.icoapps/playground/public/opengraph.pngis excluded by!**/*.pngapps/playground/src/lib/api/v1.d.tsis excluded by!**/v1.d.tsapps/ui/public/next.svgis excluded by!**/*.svgapps/ui/public/vercel.svgis excluded by!**/*.svg
📒 Files selected for processing (87)
apps/playground/eslint.config.mjs(1 hunks)apps/playground/package.json(1 hunks)apps/playground/postcss.config.mjs(1 hunks)apps/playground/public/favicon/site.webmanifest(1 hunks)apps/playground/src/app/api/chat/route.ts(1 hunks)apps/playground/src/app/layout.tsx(1 hunks)apps/playground/src/app/login/page.tsx(1 hunks)apps/playground/src/app/page.tsx(1 hunks)apps/playground/src/app/signup/page.tsx(1 hunks)apps/playground/src/components/ai-elements/actions.tsx(1 hunks)apps/playground/src/components/ai-elements/artifact.tsx(1 hunks)apps/playground/src/components/ai-elements/branch.tsx(1 hunks)apps/playground/src/components/ai-elements/chain-of-thought.tsx(1 hunks)apps/playground/src/components/ai-elements/code-block.tsx(1 hunks)apps/playground/src/components/ai-elements/context.tsx(1 hunks)apps/playground/src/components/ai-elements/conversation.tsx(1 hunks)apps/playground/src/components/ai-elements/image.tsx(1 hunks)apps/playground/src/components/ai-elements/inline-citation.tsx(1 hunks)apps/playground/src/components/ai-elements/loader.tsx(1 hunks)apps/playground/src/components/ai-elements/message.tsx(1 hunks)apps/playground/src/components/ai-elements/open-in-chat.tsx(1 hunks)apps/playground/src/components/ai-elements/prompt-input.tsx(1 hunks)apps/playground/src/components/ai-elements/reasoning.tsx(1 hunks)apps/playground/src/components/ai-elements/response.tsx(1 hunks)apps/playground/src/components/ai-elements/sources.tsx(1 hunks)apps/playground/src/components/ai-elements/suggestion.tsx(1 hunks)apps/playground/src/components/ai-elements/task.tsx(1 hunks)apps/playground/src/components/ai-elements/tool.tsx(1 hunks)apps/playground/src/components/ai-elements/web-preview.tsx(1 hunks)apps/playground/src/components/credits/credits-display.tsx(1 hunks)apps/playground/src/components/credits/top-up-credits-dialog.tsx(1 hunks)apps/playground/src/components/landing/theme-toggle.tsx(1 hunks)apps/playground/src/components/model-selector.tsx(1 hunks)apps/playground/src/components/playground/api-key-manager.tsx(1 hunks)apps/playground/src/components/playground/auth-dialog.tsx(1 hunks)apps/playground/src/components/playground/chat-header.tsx(1 hunks)apps/playground/src/components/playground/chat-page-client.tsx(1 hunks)apps/playground/src/components/playground/chat-sidebar.tsx(1 hunks)apps/playground/src/components/playground/chat-ui.tsx(1 hunks)apps/playground/src/components/provider-icons.tsx(1 hunks)apps/playground/src/components/providers.tsx(1 hunks)apps/playground/src/components/ui/alert.tsx(1 hunks)apps/playground/src/components/ui/avatar.tsx(1 hunks)apps/playground/src/components/ui/badge.tsx(1 hunks)apps/playground/src/components/ui/button.tsx(1 hunks)apps/playground/src/components/ui/carousel.tsx(1 hunks)apps/playground/src/components/ui/checkbox.tsx(1 hunks)apps/playground/src/components/ui/command.tsx(1 hunks)apps/playground/src/components/ui/dialog.tsx(1 hunks)apps/playground/src/components/ui/dropdown-menu.tsx(1 hunks)apps/playground/src/components/ui/form.tsx(1 hunks)apps/playground/src/components/ui/hover-card.tsx(1 hunks)apps/playground/src/components/ui/input.tsx(1 hunks)apps/playground/src/components/ui/label.tsx(1 hunks)apps/playground/src/components/ui/popover.tsx(1 hunks)apps/playground/src/components/ui/progress.tsx(1 hunks)apps/playground/src/components/ui/providers-icons.tsx(1 hunks)apps/playground/src/components/ui/scroll-area.tsx(1 hunks)apps/playground/src/components/ui/select.tsx(1 hunks)apps/playground/src/components/ui/separator.tsx(1 hunks)apps/playground/src/components/ui/sheet.tsx(1 hunks)apps/playground/src/components/ui/sidebar.tsx(1 hunks)apps/playground/src/components/ui/skeleton.tsx(1 hunks)apps/playground/src/components/ui/sonner.tsx(1 hunks)apps/playground/src/components/ui/tabs.tsx(1 hunks)apps/playground/src/components/ui/textarea.tsx(1 hunks)apps/playground/src/components/ui/tooltip.tsx(1 hunks)apps/playground/src/hooks/use-mobile.ts(1 hunks)apps/playground/src/hooks/useApiKey.ts(1 hunks)apps/playground/src/hooks/useAutoApiKey.ts(1 hunks)apps/playground/src/hooks/useChats.ts(1 hunks)apps/playground/src/hooks/useCreateApiKey.ts(1 hunks)apps/playground/src/hooks/useUser.ts(1 hunks)apps/playground/src/lib/mapmodels.ts(1 hunks)apps/playground/src/lib/model-utils.ts(1 hunks)apps/playground/src/lib/server-api.ts(1 hunks)apps/playground/src/lib/types.ts(1 hunks)apps/playground/src/lib/utils.ts(1 hunks)apps/playground/tsconfig.json(1 hunks)apps/ui/eslint.config.mjs(0 hunks)apps/ui/src/app/layout.tsx(2 hunks)apps/ui/src/app/playground/playground-client.tsx(3 hunks)apps/ui/src/components/Chat.tsx(2 hunks)apps/ui/src/components/app-sidebar.tsx(2 hunks)infra/split.dockerfile(1 hunks)infra/unified.dockerfile(1 hunks)package.json(1 hunks)
💤 Files with no reviewable changes (1)
- apps/ui/eslint.config.mjs
✅ Files skipped from review due to trivial changes (1)
- apps/playground/public/favicon/site.webmanifest
🚧 Files skipped from review as they are similar to previous changes (34)
- apps/playground/src/components/ai-elements/response.tsx
- apps/playground/src/components/ui/skeleton.tsx
- apps/playground/src/components/ai-elements/suggestion.tsx
- apps/playground/src/hooks/useCreateApiKey.ts
- apps/playground/postcss.config.mjs
- apps/playground/src/components/ui/badge.tsx
- apps/playground/src/components/playground/auth-dialog.tsx
- apps/playground/src/components/ui/sonner.tsx
- apps/playground/src/app/api/chat/route.ts
- apps/playground/src/app/layout.tsx
- apps/playground/src/components/ai-elements/sources.tsx
- apps/playground/src/components/ui/alert.tsx
- apps/playground/src/components/ai-elements/task.tsx
- apps/playground/src/components/playground/chat-header.tsx
- apps/playground/src/components/ui/button.tsx
- apps/playground/src/components/ui/avatar.tsx
- apps/playground/src/components/ui/popover.tsx
- apps/playground/src/components/landing/theme-toggle.tsx
- apps/playground/src/components/ai-elements/reasoning.tsx
- apps/playground/src/components/ui/scroll-area.tsx
- apps/playground/src/components/ai-elements/tool.tsx
- apps/playground/src/lib/model-utils.ts
- apps/playground/src/components/playground/chat-page-client.tsx
- apps/playground/src/components/ui/dropdown-menu.tsx
- apps/playground/src/components/ui/label.tsx
- apps/playground/src/hooks/useChats.ts
- apps/playground/src/components/ui/separator.tsx
- apps/playground/src/components/ui/textarea.tsx
- apps/playground/src/components/credits/credits-display.tsx
- apps/playground/src/components/ai-elements/branch.tsx
- apps/playground/src/components/ai-elements/artifact.tsx
- apps/playground/src/lib/mapmodels.ts
- apps/playground/src/components/ai-elements/prompt-input.tsx
- apps/playground/src/components/playground/chat-ui.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never useanyoras anyin this TypeScript project unless absolutely necessary
Always use top-levelimport; never userequireor dynamic imports
Files:
apps/playground/src/lib/utils.tsapps/playground/src/components/ai-elements/loader.tsxapps/playground/src/components/ui/tooltip.tsxapps/playground/src/components/ai-elements/actions.tsxapps/playground/src/components/credits/top-up-credits-dialog.tsxapps/playground/src/components/ai-elements/code-block.tsxapps/playground/src/app/signup/page.tsxapps/playground/src/components/ui/hover-card.tsxapps/playground/src/lib/types.tsapps/ui/src/components/app-sidebar.tsxapps/playground/src/hooks/useAutoApiKey.tsapps/playground/src/components/ui/command.tsxapps/playground/src/components/ai-elements/conversation.tsxapps/playground/src/app/login/page.tsxapps/playground/src/lib/server-api.tsapps/playground/src/components/ui/carousel.tsxapps/playground/src/app/page.tsxapps/playground/src/components/ai-elements/message.tsxapps/ui/src/app/layout.tsxapps/playground/src/components/ui/dialog.tsxapps/playground/src/hooks/use-mobile.tsapps/playground/src/components/ui/select.tsxapps/playground/src/components/ui/progress.tsxapps/playground/src/components/ui/tabs.tsxapps/playground/src/components/playground/chat-sidebar.tsxapps/playground/src/components/ai-elements/open-in-chat.tsxapps/playground/src/components/ui/providers-icons.tsxapps/playground/src/components/playground/api-key-manager.tsxapps/playground/src/components/provider-icons.tsxapps/ui/src/components/Chat.tsxapps/playground/src/components/providers.tsxapps/playground/src/components/model-selector.tsxapps/playground/src/components/ui/checkbox.tsxapps/playground/src/components/ui/input.tsxapps/playground/src/components/ai-elements/web-preview.tsxapps/playground/src/components/ui/sheet.tsxapps/playground/src/components/ui/sidebar.tsxapps/playground/src/components/ai-elements/image.tsxapps/playground/src/components/ui/form.tsxapps/ui/src/app/playground/playground-client.tsxapps/playground/src/hooks/useUser.tsapps/playground/src/components/ai-elements/chain-of-thought.tsxapps/playground/src/components/ai-elements/inline-citation.tsxapps/playground/src/hooks/useApiKey.tsapps/playground/src/components/ai-elements/context.tsx
{apps/api,apps/gateway,apps/ui,apps/docs,packages}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Always use top-level import; never use require() or dynamic imports (e.g., import(), next/dynamic)
Files:
apps/ui/src/components/app-sidebar.tsxapps/ui/src/app/layout.tsxapps/ui/src/components/Chat.tsxapps/ui/src/app/playground/playground-client.tsx
apps/ui/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use next/link for links and next/navigation’s router for programmatic navigation
apps/ui/**/*.{ts,tsx}: Use next/link for links and next/navigation's router for programmatic navigation in the UI
Use cookies for user settings not saved in the database to ensure SSR works
Files:
apps/ui/src/components/app-sidebar.tsxapps/ui/src/app/layout.tsxapps/ui/src/components/Chat.tsxapps/ui/src/app/playground/playground-client.tsx
🧠 Learnings (3)
📚 Learning: 2025-09-21T20:12:20.016Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-21T20:12:20.016Z
Learning: Applies to **/*.{ts,tsx} : Never use `any` or `as any` in this TypeScript project unless absolutely necessary
Applied to files:
apps/playground/src/components/credits/top-up-credits-dialog.tsx
📚 Learning: 2025-09-15T13:16:05.365Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: AGENTS.md:0-0
Timestamp: 2025-09-15T13:16:05.365Z
Learning: Applies to {apps/api,apps/gateway,apps/ui,apps/docs,packages}/**/*.{ts,tsx} : Always use top-level import; never use require() or dynamic imports (e.g., import(), next/dynamic)
Applied to files:
apps/playground/src/components/providers.tsx
📚 Learning: 2025-09-21T20:12:20.016Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-21T20:12:20.016Z
Learning: Applies to **/*.{ts,tsx} : Always use top-level `import`; never use `require` or dynamic imports
Applied to files:
apps/playground/src/components/providers.tsx
🧬 Code graph analysis (30)
apps/playground/src/lib/utils.ts (2)
apps/docs/lib/cn.ts (1)
twMerge(1-1)apps/ui/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/loader.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/actions.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/credits/top-up-credits-dialog.tsx (3)
apps/playground/src/lib/stripe.ts (1)
useStripe(20-38)apps/playground/src/hooks/useOrganization.ts (1)
useOrganization(10-30)apps/ui/src/components/credits/top-up-credits-dialog.tsx (1)
TopUpCreditsDialog(46-144)
apps/playground/src/components/ai-elements/code-block.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/app/signup/page.tsx (1)
apps/api/src/posthog.ts (1)
posthog(3-6)
apps/playground/src/components/ui/hover-card.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/ui/src/components/app-sidebar.tsx (1)
apps/ui/src/lib/components/use-toast.ts (1)
toast(194-194)
apps/playground/src/hooks/useAutoApiKey.ts (1)
apps/playground/src/hooks/useDefaultProject.ts (1)
useDefaultProject(3-36)
apps/playground/src/components/ui/command.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/conversation.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/app/login/page.tsx (1)
apps/api/src/posthog.ts (1)
posthog(3-6)
apps/playground/src/app/page.tsx (2)
apps/playground/src/components/playground/chat-page-client.tsx (1)
ChatPageClient(31-260)packages/models/src/providers.ts (1)
providers(19-247)
apps/playground/src/components/ai-elements/message.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ui/select.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/playground/chat-sidebar.tsx (5)
apps/playground/src/hooks/useUser.ts (1)
useUser(24-105)apps/playground/src/lib/auth-client.ts (1)
useAuth(20-33)apps/playground/src/hooks/useOrganization.ts (1)
useOrganization(10-30)apps/playground/src/hooks/useChats.ts (4)
useChats(25-29)useDeleteChat(90-109)useUpdateChat(69-88)Chat(6-14)apps/playground/src/components/credits/credits-display.tsx (1)
CreditsDisplay(19-79)
apps/playground/src/components/ai-elements/open-in-chat.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ui/providers-icons.tsx (2)
apps/playground/src/components/provider-icons.tsx (19)
AnthropicIcon(6-30)CloudriftIcon(33-84)DeepseekIcon(87-102)GoogleStudioAIIcon(105-155)GroqIcon(158-173)InferenceNetIcon(176-200)MistralIcon(203-241)OpenAIIcon(244-265)PerplexityIcon(268-285)TogetherAIIcon(288-305)XAIIcon(308-321)MoonshotIcon(324-337)NovitaIcon(340-355)AlibabaIcon(358-370)NebiusIcon(372-384)ZaiIcon(387-400)ProviderIcons(442-461)ProviderIconKey(464-464)getProviderIcon(467-480)apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/playground/api-key-manager.tsx (4)
apps/playground/src/hooks/useApiKey.ts (1)
useApiKey(9-63)apps/playground/src/lib/config.tsx (1)
useAppConfig(25-31)apps/playground/src/hooks/useAutoApiKey.ts (1)
useAutoApiKey(13-77)apps/ui/src/components/playground/api-key-manager.tsx (1)
ApiKeyManager(134-378)
apps/playground/src/components/provider-icons.tsx (2)
apps/playground/src/components/ui/providers-icons.tsx (19)
AnthropicIcon(6-30)CloudriftIcon(33-48)DeepseekIcon(51-66)GoogleStudioAIIcon(69-119)GroqIcon(122-137)InferenceNetIcon(140-164)MistralIcon(167-205)OpenAIIcon(208-229)PerplexityIcon(232-249)TogetherAIIcon(252-269)XAIIcon(272-285)MoonshotIcon(288-301)NovitaIcon(304-319)AlibabaIcon(322-334)NebiusIcon(336-348)ZaiIcon(351-364)ProviderIcons(367-384)ProviderIconKey(386-386)getProviderIcon(403-419)apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/providers.tsx (2)
apps/playground/src/lib/config.tsx (1)
AppConfigProvider(14-23)apps/docs/lib/providers.tsx (1)
PostHogProvider(11-36)
apps/playground/src/components/model-selector.tsx (4)
packages/models/src/models.ts (1)
ModelDefinition(108-161)packages/models/src/providers.ts (2)
ProviderDefinition(1-17)providers(19-247)apps/playground/src/lib/model-utils.ts (4)
getProviderForModel(29-35)getModelCapabilities(37-58)formatPrice(3-14)formatContextSize(16-27)apps/playground/src/components/provider-icons.tsx (1)
getProviderIcon(467-480)
apps/playground/src/components/ui/checkbox.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/web-preview.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/image.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/ui/src/app/playground/playground-client.tsx (2)
apps/ui/src/lib/components/use-toast.ts (1)
toast(194-194)packages/logger/src/index.ts (1)
error(147-154)
apps/playground/src/hooks/useUser.ts (2)
apps/api/src/posthog.ts (1)
posthog(3-6)apps/playground/src/lib/fetch-client.ts (1)
useApi(22-28)
apps/playground/src/components/ai-elements/chain-of-thought.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/inline-citation.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/context.tsx (3)
apps/playground/src/components/ui/hover-card.tsx (3)
HoverCard(44-44)HoverCardTrigger(44-44)HoverCardContent(44-44)apps/playground/src/components/ui/button.tsx (1)
Button(58-58)apps/playground/src/lib/utils.ts (1)
cn(4-6)
🪛 ast-grep (0.39.5)
apps/playground/src/hooks/useApiKey.ts
[warning] 39-39: Sensitive information detected in localStorage. Storing sensitive data like emails, usernames, or personal information in localStorage exposes it to malicious scripts and XSS attacks. Use secure storage alternatives or avoid storing sensitive data client-side.
Context: localStorage.setItem(API_KEY_STORAGE_KEY, key)
Note: [CWE-312] Cleartext Storage of Sensitive Information [REFERENCES]
- https://cwe.mitre.org/data/definitions/312.html
- https://owasp.org/www-community/vulnerabilities/HTML5_Local_Storage_Security
(local-storage-sensitive-information)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: test-split
| async function onSubmit(values: z.infer<typeof formSchema>) { | ||
| setIsLoading(true); | ||
| const { error } = await signIn.email( | ||
| { | ||
| email: values.email, | ||
| password: values.password, | ||
| }, | ||
| { | ||
| onSuccess: (ctx) => { | ||
| queryClient.clear(); | ||
| posthog.identify(ctx.data.user.id, { | ||
| email: ctx.data.user.email, | ||
| name: ctx.data.user.name, | ||
| }); | ||
| posthog.capture("user_logged_in", { | ||
| method: "email", | ||
| email: values.email, | ||
| }); | ||
| toast.success("Login successful"); | ||
| router.push("/dashboard"); | ||
| }, | ||
| onError: (ctx) => { | ||
| toast.error(ctx.error.message || "An unknown error occurred", { | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| if (error) { | ||
| toast.error(error.message || "An unknown error occurred", { | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| setIsLoading(false); | ||
| } |
There was a problem hiding this comment.
Duplicate error toasts in email sign-in.
You toast in onError and again when checking returned error → double notifications.
Apply this diff to rely on onError only:
const { error } = await signIn.email(
{
email: values.email,
password: values.password,
},
{
onSuccess: (ctx) => {
queryClient.clear();
posthog.identify(ctx.data.user.id, {
email: ctx.data.user.email,
name: ctx.data.user.name,
});
posthog.capture("user_logged_in", {
method: "email",
- email: values.email,
});
toast.success("Login successful");
- router.push("/dashboard");
+ router.replace("/dashboard");
},
onError: (ctx) => {
toast.error(ctx.error.message || "An unknown error occurred", {
style: {
backgroundColor: "var(--destructive)",
color: "var(--destructive-foreground)",
},
});
},
},
);
-
- if (error) {
- toast.error(error.message || "An unknown error occurred", {
- style: {
- backgroundColor: "var(--destructive)",
- color: "var(--destructive-foreground)",
- },
- });
- }Also applies to: 97-104
🤖 Prompt for AI Agents
In apps/playground/src/app/login/page.tsx around lines 65-107 (also applies to
lines 97-104), the code shows duplicate error toasts because an onError callback
displays a toast and the returned { error } is handled afterward with another
toast; remove the post-signIn check that shows the toast and rely solely on the
onError callback for error display. Concretely, delete or skip the if (error) {
toast.error(...) } block after awaiting signIn.email so only the onError path
triggers user-facing toasts, and ensure setIsLoading(false) still runs after the
call.
| posthog.capture("user_logged_in", { | ||
| method: "email", | ||
| email: values.email, | ||
| }); | ||
| toast.success("Login successful"); | ||
| router.push("/dashboard"); | ||
| }, |
There was a problem hiding this comment.
❓ Verification inconclusive
Avoid sending PII in analytics event; keep email out or ensure consent.
Event properties include the user’s email. Either remove it or confirm explicit consent/contractual basis for PII in analytics.
Apply this diff to remove the email field:
posthog.capture("user_logged_in", {
method: "email",
- email: values.email,
});Remove email from analytics event
Sending user email without explicit consent violates PII policies; drop the email property from the user_logged_in event.
posthog.capture("user_logged_in", {
method: "email",
- email: values.email,
});📝 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.
| posthog.capture("user_logged_in", { | |
| method: "email", | |
| email: values.email, | |
| }); | |
| toast.success("Login successful"); | |
| router.push("/dashboard"); | |
| }, | |
| posthog.capture("user_logged_in", { | |
| method: "email", | |
| }); | |
| toast.success("Login successful"); | |
| router.push("/dashboard"); | |
| }, |
🤖 Prompt for AI Agents
In apps/playground/src/app/login/page.tsx around lines 79 to 85, the analytics
event currently includes the user's email which is PII; remove the email
property from the posthog.capture call and only send non-identifying metadata
(e.g., method: "email"), and ensure you do not log or send the email anywhere
else in this block (keep toast.success and router.push as-is).
| async function handlePasskeySignIn() { | ||
| setIsLoading(true); | ||
| try { | ||
| const res = await signIn.passkey(); | ||
| if (res?.error) { | ||
| toast.error(res.error.message || "Failed to sign in with passkey", { | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); | ||
| return; | ||
| } | ||
| posthog.capture("user_logged_in", { method: "passkey" }); | ||
| toast.success("Login successful"); | ||
| router.push("/dashboard"); | ||
| } catch (error: unknown) { | ||
| toast.error( | ||
| (error as Error)?.message || "Failed to sign in with passkey", | ||
| { | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }, | ||
| ); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } |
There was a problem hiding this comment.
Passkey sign-in misses cache clear and user identify; align with email flow.
Email flow clears the React Query cache and identifies the user; passkey flow does neither, which can leave stale state and inconsistent analytics.
Apply this diff:
async function handlePasskeySignIn() {
setIsLoading(true);
try {
const res = await signIn.passkey();
if (res?.error) {
toast.error(res.error.message || "Failed to sign in with passkey", {
style: {
backgroundColor: "var(--destructive)",
color: "var(--destructive-foreground)",
},
});
return;
}
- posthog.capture("user_logged_in", { method: "passkey" });
- toast.success("Login successful");
- router.push("/dashboard");
+ // Keep auth cache/analytics consistent with email flow
+ queryClient.clear();
+ if (res?.data?.user) {
+ posthog.identify(res.data.user.id, {
+ email: res.data.user.email,
+ name: res.data.user.name,
+ });
+ }
+ posthog.capture("user_logged_in", { method: "passkey" });
+ toast.success("Login successful");
+ router.replace("/dashboard");
} catch (error: unknown) {
toast.error(
(error as Error)?.message || "Failed to sign in with passkey",
{
style: {
backgroundColor: "var(--destructive)",
color: "var(--destructive-foreground)",
},
},
);
} finally {
setIsLoading(false);
}
}🤖 Prompt for AI Agents
In apps/playground/src/app/login/page.tsx around lines 109 to 137, the passkey
sign-in success path needs to mirror the email flow by clearing React Query
cache and identifying the user in PostHog; after a successful signIn.passkey()
(and before capturing "user_logged_in" and navigating), call the query client
clear/invalidate method used elsewhere (e.g., queryClient.clear() or
queryClient.invalidateQueries()) and call posthog.identify(...) with the
signed-in user's identifier (id or email available on the response) so analytics
and client state are consistent, then proceed to capture and redirect.
| useEffect(() => { | ||
| posthog.capture("page_viewed_signup"); | ||
| }, [posthog]); |
There was a problem hiding this comment.
Guard PostHog usages when the SDK is unavailable
usePostHog() yields undefined whenever the provider isn’t mounted or analytics are disabled (for example in local dev without keys). Calling posthog.capture/identify unconditionally will then throw a TypeError, crashing the signup page and blocking the flow entirely. Please null-check before invoking the SDK so the UI still works when analytics are off.
useEffect(() => {
- posthog.capture("page_viewed_signup");
+ if (!posthog) return;
+ posthog.capture("page_viewed_signup");
}, [posthog]);
@@
onSuccess: (ctx) => {
queryClient.clear();
- posthog.identify(ctx.data.user.id, {
- email: ctx.data.user.email,
- name: ctx.data.user.name,
- });
- posthog.capture("user_signed_up", {
- email: values.email,
- name: values.name,
- });
+ if (posthog) {
+ posthog.identify(ctx.data.user.id, {
+ email: ctx.data.user.email,
+ name: ctx.data.user.name,
+ });
+ posthog.capture("user_signed_up", {
+ email: values.email,
+ name: values.name,
+ });
+ }
toast.success("Account created", {Also applies to: 73-80
🤖 Prompt for AI Agents
In apps/playground/src/app/signup/page.tsx around lines 48-50 (and also apply
same change to lines 73-80), the code calls posthog.capture/identify unguarded,
which will throw when usePostHog() returns undefined; wrap all PostHog calls in
a null-check (e.g., if (posthog) { posthog.capture(...) } or optional chaining
like posthog?.capture(...)) so the analytics calls are skipped when the
SDK/provider is unavailable, preserving the signup UI and flow.
| return ( | ||
| <ChainOfThoughtContext.Provider value={contextValue}> | ||
| <div | ||
| className={cn("not-prose max-w-prose space-y-4", className)} | ||
| {...props} | ||
| > | ||
| {children} | ||
| </div> | ||
| </ChainOfThoughtContext.Provider> | ||
| ); |
There was a problem hiding this comment.
Use a single Collapsible root to bind Trigger and Content for correct a11y
Trigger and Content live under different Collapsible.Roots. This breaks the implicit linkage (aria-controls) and Radix context between them. Hoist a single Root to ChainOfThought and remove the extra Roots in Header/Content.
Apply this diff to ChainOfThought:
return (
- <ChainOfThoughtContext.Provider value={contextValue}>
- <div
- className={cn("not-prose max-w-prose space-y-4", className)}
- {...props}
- >
- {children}
- </div>
- </ChainOfThoughtContext.Provider>
+ <ChainOfThoughtContext.Provider value={contextValue}>
+ <Collapsible open={isOpen} onOpenChange={setIsOpen}>
+ <div
+ className={cn("not-prose max-w-prose space-y-4", className)}
+ {...props}
+ >
+ {children}
+ </div>
+ </Collapsible>
+ </ChainOfThoughtContext.Provider>
);📝 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.
| return ( | |
| <ChainOfThoughtContext.Provider value={contextValue}> | |
| <div | |
| className={cn("not-prose max-w-prose space-y-4", className)} | |
| {...props} | |
| > | |
| {children} | |
| </div> | |
| </ChainOfThoughtContext.Provider> | |
| ); | |
| return ( | |
| <ChainOfThoughtContext.Provider value={contextValue}> | |
| <Collapsible open={isOpen} onOpenChange={setIsOpen}> | |
| <div | |
| className={cn("not-prose max-w-prose space-y-4", className)} | |
| {...props} | |
| > | |
| {children} | |
| </div> | |
| </Collapsible> | |
| </ChainOfThoughtContext.Provider> | |
| ); |
🤖 Prompt for AI Agents
In apps/playground/src/components/ai-elements/chain-of-thought.tsx around lines
67-76, the Collapsible.Trigger and Collapsible.Content are rendered under
different Collapsible.Roots which breaks Radix accessibility linkage; move/hoist
a single Collapsible.Root to wrap the ChainOfThought component (around the
ChainOfThoughtContext.Provider and its inner div) so Trigger and Content share
the same Root, then remove any Collapsible.Root usage from Header and Content
components so they only render Collapsible.Trigger and Collapsible.Content
respectively, ensuring the open state and props are passed down from the single
Root and any context/ids remain intact for correct aria-controls behavior.
| data-slot="sidebar-container" | ||
| className={cn( | ||
| "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex", | ||
| side === "left" | ||
| ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" | ||
| : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", | ||
| // Adjust the padding for floating and inset variants. | ||
| variant === "floating" || variant === "inset" | ||
| ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" | ||
| : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l", |
There was a problem hiding this comment.
Fix Tailwind var() syntax in container width.
- "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
+ "fixed inset-y-0 z-10 hidden h-svh w-[var(--sidebar-width)] transition-[left,right,width] duration-200 ease-linear md:flex",📝 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.
| data-slot="sidebar-container" | |
| className={cn( | |
| "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex", | |
| side === "left" | |
| ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" | |
| : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", | |
| // Adjust the padding for floating and inset variants. | |
| variant === "floating" || variant === "inset" | |
| ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" | |
| : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l", | |
| data-slot="sidebar-container" | |
| className={cn( | |
| "fixed inset-y-0 z-10 hidden h-svh w-[var(--sidebar-width)] transition-[left,right,width] duration-200 ease-linear md:flex", | |
| side === "left" | |
| ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" | |
| : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", | |
| // Adjust the padding for floating and inset variants. | |
| variant === "floating" || variant === "inset" | |
| ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" | |
| : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l", |
🤖 Prompt for AI Agents
In apps/playground/src/components/ui/sidebar.tsx around lines 232–241, the
Tailwind arbitrary width values use incorrect parentheses syntax like
w-(--sidebar-width) and w-(--sidebar-width-icon); replace them with proper
arbitrary values using var(), e.g. w-[var(--sidebar-width)] and
w-[var(--sidebar-width-icon)], and update any calc(...) uses to reference
var(...) consistently (e.g. calc(var(--sidebar-width)*-1) and
calc(var(--sidebar-width-icon)+var(--spacing-4)+2px) or the correct spacing
variable) so Tailwind recognizes the custom properties.
| const onChange = () => { | ||
| setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); | ||
| }; | ||
| mql.addEventListener("change", onChange); | ||
| setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); | ||
| return () => mql.removeEventListener("change", onChange); | ||
| }, []); |
There was a problem hiding this comment.
Handle browsers missing MediaQueryList#addEventListener.
On Safari ≤13 / older WebViews, MediaQueryList only exposes addListener/removeListener. Calling addEventListener throws a TypeError, so this hook crashes as soon as the effect runs, breaking the entire page for those users. Please add a safe fallback that uses the legacy listener APIs when necessary.
- const onChange = () => {
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
- };
- mql.addEventListener("change", onChange);
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
- return () => mql.removeEventListener("change", onChange);
+ const handleChange = (event: MediaQueryListEvent) => {
+ setIsMobile(event.matches);
+ };
+
+ setIsMobile(mql.matches);
+
+ if (typeof mql.addEventListener === "function") {
+ mql.addEventListener("change", handleChange);
+ return () => mql.removeEventListener("change", handleChange);
+ }
+
+ if (typeof mql.addListener === "function") {
+ mql.addListener(handleChange);
+ return () => mql.removeListener(handleChange);
+ }
+
+ return () => {};📝 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.
| const onChange = () => { | |
| setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); | |
| }; | |
| mql.addEventListener("change", onChange); | |
| setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); | |
| return () => mql.removeEventListener("change", onChange); | |
| }, []); | |
| const handleChange = (event: MediaQueryListEvent) => { | |
| setIsMobile(event.matches); | |
| }; | |
| setIsMobile(mql.matches); | |
| if (typeof mql.addEventListener === "function") { | |
| mql.addEventListener("change", handleChange); | |
| return () => mql.removeEventListener("change", handleChange); | |
| } | |
| if (typeof mql.addListener === "function") { | |
| mql.addListener(handleChange); | |
| return () => mql.removeListener(handleChange); | |
| } | |
| return () => {}; | |
| }, []); |
🤖 Prompt for AI Agents
In apps/playground/src/hooks/use-mobile.ts around lines 12 to 18, the effect
currently calls mql.addEventListener which throws on browsers that only support
the legacy MediaQueryList.addListener/removeListener API; update the effect to
feature-detect and use mql.addEventListener("change", onChange) when available
and fall back to mql.addListener(onChange) otherwise, and likewise in the
cleanup use mql.removeEventListener("change", onChange) when available or
mql.removeListener(onChange) as the fallback; keep the existing onChange handler
and initial setIsMobile call unchanged.
| @@ -0,0 +1,94 @@ | |||
| import { cookies } from "next/headers"; | |||
| import createFetchClient from "openapi-fetch"; | |||
| import { toast } from "sonner"; | |||
There was a problem hiding this comment.
Don’t use client-only toasts on the server
Importing/calling sonner on the server can crash (window not defined). Log server-side and surface errors to callers instead.
-import { toast } from "sonner";
@@
- } catch {
- toast.error(`Server API error for ${method} ${path}`);
- return null;
- }
+ } catch (err) {
+ console.error(`Server API error for ${method} ${String(path)}`, err);
+ return null;
+ }Also applies to: 90-94
🤖 Prompt for AI Agents
In apps/playground/src/lib/server-api.ts around lines 3 and 90-94, the file
imports and calls the client-only sonner toast on the server which can crash
(window is undefined). Remove the direct import and any toast calls from this
server module; instead log errors with a server-safe logger (console.error or
your app logger) and surface failures to callers by throwing an Error or
returning an error result. If you need to show toasts in the UI, move toast
calls to client-side code (or call them through a client-only wrapper that
checks typeof window !== "undefined") so the server code never imports or
invokes sonner.
| toast.error("Chat Error"); | ||
|
|
||
| // Show user-friendly error message | ||
| const errorMessage = | ||
| error instanceof Error ? error.message : "An unexpected error occurred"; | ||
| toast({ | ||
| title: "Chat Error", | ||
| toast.error("Chat Error", { | ||
| description: errorMessage, | ||
| variant: "destructive", | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Avoid firing the same error toast twice
Calling toast.error twice in a row emits two identical toasts for a single failure. Consolidate into one call so users aren’t spammed with duplicate notifications.
Apply this diff:
- toast.error("Chat Error");
-
const errorMessage =
error instanceof Error ? error.message : "An unexpected error occurred";
toast.error("Chat Error", {
description: errorMessage,
style: {
backgroundColor: "var(--destructive)",
color: "var(--destructive-foreground)",
},
});📝 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.
| toast.error("Chat Error"); | |
| // Show user-friendly error message | |
| const errorMessage = | |
| error instanceof Error ? error.message : "An unexpected error occurred"; | |
| toast({ | |
| title: "Chat Error", | |
| toast.error("Chat Error", { | |
| description: errorMessage, | |
| variant: "destructive", | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| // Show user-friendly error message | |
| const errorMessage = | |
| error instanceof Error ? error.message : "An unexpected error occurred"; | |
| toast.error("Chat Error", { | |
| description: errorMessage, | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In apps/ui/src/components/Chat.tsx around lines 71 to 82, the code currently
calls toast.error twice which produces duplicate notifications; remove the first
toast.error("Chat Error") and consolidate into a single toast.error call that
uses the computed errorMessage (error instanceof Error ? error.message : "An
unexpected error occurred") and the existing style and description options so
only one user-friendly, styled toast is shown.
left a comment
There was a problem hiding this comment.
Actionable comments posted: 25
🧹 Nitpick comments (24)
apps/ui/src/components/app-sidebar.tsx (1)
61-63: Keep logging the caught error for diagnostics.Dropping the error parameter means we no longer log what actually went wrong, which makes field debugging harder. Please keep the caught error around, log it (or send it to telemetry), and still surface the toast to the user.
- } catch { - toast.error("Failed to clear last used project cookies"); + } catch (error) { + console.error("Failed to clear last used project cookies", error); + toast.error("Failed to clear last used project cookies");apps/playground/src/components/ai-elements/context.tsx (2)
334-339: Optional: Normalize reasoning usage shapeIf tokenlens expects the full usage shape, include zeros for other fields for consistency. Otherwise, this is fine if sparse objects are supported.
Apply this diff (optional):
- const reasoningCost = modelId - ? estimateCost({ - modelId, - usage: { reasoningTokens }, - }).totalUSD + const reasoningCost = modelId + ? estimateCost({ + modelId, + usage: { inputTokens: 0, outputTokens: 0, reasoningTokens }, + }).totalUSD
145-151: Type alias names shadow component namesRename type aliases to *Props for clarity and consistency with others (e.g., ContextContentProps).
Apply this diff:
-export type ContextContentHeader = ComponentProps<"div">; +export type ContextContentHeaderProps = ComponentProps<"div">; @@ -export const ContextContentHeader = ({ +export const ContextContentHeader = ({ children, className, ...props -}: ContextContentHeader) => { +}: ContextContentHeaderProps) => {-export type ContextContentBody = ComponentProps<"div">; +export type ContextContentBodyProps = ComponentProps<"div">; @@ -export const ContextContentBody = ({ +export const ContextContentBody = ({ children, className, ...props -}: ContextContentBody) => ( +}: ContextContentBodyProps) => (-export type ContextContentFooter = ComponentProps<"div">; +export type ContextContentFooterProps = ComponentProps<"div">; @@ -export const ContextContentFooter = ({ +export const ContextContentFooter = ({ children, className, ...props -}: ContextContentFooter) => { +}: ContextContentFooterProps) => {Also applies to: 184-190, 196-202
apps/playground/src/components/ai-elements/branch.tsx (2)
86-97: Stabilize children handling to avoid unnecessary effects and undefined entries.Using a per-render array reference as an effect dep causes frequent re-runs; single child undefined can produce an empty wrapper. Prefer React.Children helpers.
-export const BranchMessages = ({ children, ...props }: BranchMessagesProps) => { +export const BranchMessages = ({ children, ...props }: BranchMessagesProps) => { const { currentBranch, setBranches, branches } = useBranch(); - // eslint-disable-next-line react-hooks/exhaustive-deps - const childrenArray = Array.isArray(children) ? children : [children]; + const childrenArray = (Array.isArray(children) ? children : [children]).filter( + Boolean, + ) as ReactElement[]; // Use useEffect to update branches when they change useEffect(() => { - if (branches.length !== childrenArray.length) { - setBranches(childrenArray); - } - }, [childrenArray, branches, setBranches]); + if (branches.length !== childrenArray.length) { + setBranches(childrenArray); + } + }, [childrenArray.length, branches.length, setBranches]);Optionally, switch to React.Children.toArray(children) and compare counts only.
202-215: Avoid “0 of 0” when empty.Hide the page indicator or show “1 of 1” when totalBranches <= 0.
export const BranchPage = ({ className, ...props }: BranchPageProps) => { const { currentBranch, totalBranches } = useBranch(); return ( <span className={cn( "font-medium text-muted-foreground text-xs tabular-nums", className, )} {...props} > - {currentBranch + 1} of {totalBranches} + {totalBranches > 0 ? currentBranch + 1 : 0} of {Math.max(0, totalBranches)} </span> ); };apps/playground/src/components/ai-elements/code-block.tsx (1)
144-151: Accessibility: add an aria-label to the icon-only copy button.Improves screen reader usability.
return ( <Button className={cn("shrink-0", className)} onClick={copyToClipboard} size="icon" variant="ghost" + aria-label={isCopied ? "Copied" : "Copy code"} {...props} >apps/playground/src/components/ui/carousel.tsx (1)
80-91: Keyboard UX: honor vertical orientation with ArrowUp/ArrowDown.Improve accessibility by switching keys based on orientation.
- const handleKeyDown = React.useCallback( - (event: React.KeyboardEvent<HTMLDivElement>) => { - if (event.key === "ArrowLeft") { - event.preventDefault(); - scrollPrev(); - } else if (event.key === "ArrowRight") { - event.preventDefault(); - scrollNext(); - } - }, - [scrollPrev, scrollNext], - ); + const { orientation: dir } = { orientation }; // capture prop + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent<HTMLDivElement>) => { + if (dir === "vertical") { + if (event.key === "ArrowUp") { + event.preventDefault(); + scrollPrev(); + } else if (event.key === "ArrowDown") { + event.preventDefault(); + scrollNext(); + } + return; + } + if (event.key === "ArrowLeft") { + event.preventDefault(); + scrollPrev(); + } else if (event.key === "ArrowRight") { + event.preventDefault(); + scrollNext(); + } + }, + [dir, scrollPrev, scrollNext], + );apps/playground/src/components/ui/sheet.tsx (1)
47-82: Consider forwardRef for SheetContent for parity with Radix and focus management.Forwarding the ref to
SheetPrimitive.Contentimproves interoperability with consumers needing a ref (focus/measurement).Apply this diff:
-function SheetContent({ +const SheetContent = React.forwardRef< + React.ElementRef<typeof SheetPrimitive.Content>, + React.ComponentProps<typeof SheetPrimitive.Content> & { side?: "top" | "right" | "bottom" | "left" } +>(function SheetContent( className, children, side = "right", ...props -}: React.ComponentProps<typeof SheetPrimitive.Content> & { - side?: "top" | "right" | "bottom" | "left"; -}) { +}, ref) { return ( <SheetPortal> <SheetOverlay /> <SheetPrimitive.Content + ref={ref}apps/playground/src/components/ai-elements/message.tsx (1)
63-77: Make avatar src optional and set meaningful alt text.Improve accessibility and DX by allowing fallback-only avatars and describing images.
Apply this diff:
-export type MessageAvatarProps = ComponentProps<typeof Avatar> & { - src: string; - name?: string; -}; +export type MessageAvatarProps = ComponentProps<typeof Avatar> & { + src?: string; + name?: string; +}; export const MessageAvatar = ({ src, name, className, ...props }: MessageAvatarProps) => ( <Avatar className={cn("size-8 ring-1 ring-border", className)} {...props}> - <AvatarImage alt="" className="mt-0 mb-0" src={src} /> + {src ? <AvatarImage alt={name ?? ""} className="mt-0 mb-0" src={src} /> : null} <AvatarFallback>{name?.slice(0, 2) || "ME"}</AvatarFallback> </Avatar> );apps/playground/src/components/credits/top-up-credits-dialog.tsx (1)
58-68: Surface Stripe load errors in UI.You already have
errorinuseStripe(); consider showing an inline message when present.Apply this diff:
- const { stripe, isLoading: stripeLoading } = useStripe(); + const { stripe, isLoading: stripeLoading, error: stripeError } = useStripe();And in the payment step switch:
- stripeLoading ? ( + stripeLoading ? ( <div className="p-6 text-center">Loading payment form...</div> - ) : !stripe ? ( + ) : stripeError ? ( + <div className="p-6 text-center text-sm text-red-600"> + {stripeError.message || "Failed to load payment form."} + </div> + ) : !stripe ? (apps/playground/src/components/ai-elements/conversation.tsx (1)
82-97: Add accessible name to scroll button.Improve A11y with an aria-label.
Apply this diff:
<Button className={cn( "absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full", className, )} onClick={handleScrollToBottom} size="icon" type="button" variant="outline" + aria-label="Scroll to bottom" {...props}apps/playground/src/components/ui/textarea.tsx (1)
5-16: Forward ref the textarea for focus management and form libsExpose the DOM node to callers (focus, RHF, accessibility). Keeps API stable.
-function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { - return ( - <textarea - data-slot="textarea" - className={cn( - "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", - className, - )} - {...props} - /> - ); -} +const Textarea = React.forwardRef< + HTMLTextAreaElement, + React.ComponentProps<"textarea"> +>(function Textarea({ className, ...props }, ref) { + return ( + <textarea + ref={ref} + data-slot="textarea" + className={cn( + "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + className, + )} + {...props} + /> + ); +})apps/playground/src/hooks/useAutoApiKey.ts (2)
34-61: Remove no-op effect and console.log; clarify hook’s purposeThe effect never sets a key (maskedToken isn’t usable). This adds noise without behavior.
- const { userApiKey, setUserApiKey, isLoaded } = useApiKey(); + const { userApiKey, isLoaded } = useApiKey(); @@ - useEffect(() => { - // Auto-set API key for new users - if ( - isLoaded && // localStorage is loaded - !userApiKey && // no API key in localStorage - !isLoading && // server data is loaded - apiKeysData?.apiKeys && // we have API keys data - apiKeysData.apiKeys.length > 0 // user has at least one API key - ) { - // Find the auto-generated playground key or use the first active key - const autoGeneratedKey = apiKeysData.apiKeys.find( - (key) => - key.status === "active" && - key.description?.includes("Auto-generated playground key"), - ); - - const firstActiveKey = apiKeysData.apiKeys.find( - (key) => key.status === "active", - ); - - const keyToUse = autoGeneratedKey || firstActiveKey; - - if (keyToUse?.maskedToken) { - // We can't use the masked token directly, but we can indicate that the user has a key - // For now, we'll still show the API key manager but with better messaging - console.log("User has auto-generated API key available"); - } - } - }, [isLoaded, userApiKey, isLoading, apiKeysData, setUserApiKey]); + // Intentionally no side-effects: listing keys returns masked tokens. + // Consumers can use the booleans below to drive UI (e.g., prompt to copy key).Also applies to: 16-16, 62-62
13-13: Rename hook to match behavior
The hook returns status flags (hasAutoGeneratedKey,hasAnyKey) rather than auto-setting a key. RenameuseAutoApiKeytouseAutoApiKeyStatus(or clarify via JSDoc) and update imports in:
- apps/playground/src/hooks/useAutoApiKey.ts
- apps/playground/src/components/playground/api-key-manager.tsx:139
apps/playground/src/components/ai-elements/task.tsx (1)
57-59: Maketitleoptional for custom childrenIf
childrenis provided,titleshouldn’t be required.-export type TaskTriggerProps = ComponentProps<typeof CollapsibleTrigger> & { - title: string; -}; +export type TaskTriggerProps = ComponentProps<typeof CollapsibleTrigger> & { + title?: string; +}; @@ - <p className="text-sm">{title}</p> + <p className="text-sm">{title ?? "Task"}</p>Also applies to: 71-71
apps/playground/src/lib/utils.ts (1)
4-6: LGTM; consider de-duplicating cn across workspaceMatches common clsx→twMerge pattern. Optionally export a single cn from a shared package to avoid drift.
apps/playground/src/components/playground/auth-dialog.tsx (1)
12-27: Add dialog semantics (role/aria) and labelImproves accessibility without changing UX.
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> - <div className="w-[420px] rounded-md border bg-background p-4 shadow-md"> - <div className="text-sm font-medium mb-2">Sign in required</div> + <div + className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" + role="dialog" + aria-modal="true" + aria-labelledby="auth-dialog-title" + > + <div className="w-[420px] max-w-[92vw] rounded-md border bg-background p-4 shadow-md"> + <h2 id="auth-dialog-title" className="text-sm font-medium mb-2"> + Sign in required + </h2>apps/playground/src/app/layout.tsx (1)
23-43: Avoid hard-coding metadataBase to production hostUse env (or
generateMetadata) so links are correct in dev/self-hosted deployments.apps/playground/src/lib/mapmodels.ts (1)
8-12: Pre-index providers by id to avoid O(n·m) lookupsLinear search per model-provider mapping is unnecessary and scales poorly. Build a Map once and use O(1) lookups.
- const entries: ComboboxModel[] = []; - for (const m of models) { - for (const p of m.providers) { - const providerInfo = providers.find((pr) => pr.id === p.providerId); + const entries: ComboboxModel[] = []; + const providerById = new Map(providers.map((pr) => [pr.id, pr])); + for (const m of models) { + for (const p of m.providers) { + const providerInfo = providerById.get(p.providerId);apps/playground/src/components/ui/scroll-area.tsx (1)
8-13: Forward refs for Root and Scrollbar (Radix pattern, better interoperability)Forwarding refs aligns with Radix wrappers elsewhere and enables parent components to access the underlying elements.
-function ScrollArea({ - className, - children, - ...props -}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) { - return ( - <ScrollAreaPrimitive.Root +const ScrollArea = React.forwardRef< + React.ElementRef<typeof ScrollAreaPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> +>(({ className, children, ...props }, ref) => { + return ( + <ScrollAreaPrimitive.Root + ref={ref} data-slot="scroll-area" className={cn("relative overflow-hidden", className)} {...props} > @@ - </ScrollAreaPrimitive.Root> - ); -} + </ScrollAreaPrimitive.Root> + ); +}); @@ -function ScrollBar({ - className, - orientation = "vertical", - ...props -}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) { - return ( - <ScrollAreaPrimitive.ScrollAreaScrollbar +const ScrollBar = React.forwardRef< + React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, + React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> +>(({ className, orientation = "vertical", ...props }, ref) => { + return ( + <ScrollAreaPrimitive.ScrollAreaScrollbar + ref={ref} data-slot="scroll-area-scrollbar" orientation={orientation} className={cn( "flex touch-none p-px transition-colors select-none", @@ - </ScrollAreaPrimitive.ScrollAreaScrollbar> - ); -} + </ScrollAreaPrimitive.ScrollAreaScrollbar> + ); +});Also applies to: 31-36
apps/playground/src/components/ui/badge.tsx (1)
28-35: Optionally forward ref for consistency with other UI primitivesThis improves composability and parity with common Radix wrapper patterns.
-function Badge({ - className, - variant, - asChild = false, - ...props -}: React.ComponentProps<"span"> & - VariantProps<typeof badgeVariants> & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span"; +const Badge = React.forwardRef< + React.ElementRef<"span">, + React.ComponentPropsWithoutRef<"span"> & + VariantProps<typeof badgeVariants> & { asChild?: boolean } +>(({ className, variant, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "span"; return ( <Comp data-slot="badge" className={cn(badgeVariants({ variant }), className)} + ref={ref} {...props} /> ); -} +});Also applies to: 37-44
apps/playground/src/components/ui/avatar.tsx (1)
8-12: Forward refs to align with Radix convention and enable parent ref accessImproves ergonomics for consumers that need refs (focus, measurements).
-function Avatar({ - className, - ...props -}: React.ComponentProps<typeof AvatarPrimitive.Root>) { - return ( - <AvatarPrimitive.Root +const Avatar = React.forwardRef< + React.ElementRef<typeof AvatarPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> +>(({ className, ...props }, ref) => { + return ( + <AvatarPrimitive.Root + ref={ref} data-slot="avatar" className={cn( "relative flex size-8 shrink-0 overflow-hidden rounded-full", className, )} {...props} /> ); -} +}); @@ -function AvatarImage({ - className, - ...props -}: React.ComponentProps<typeof AvatarPrimitive.Image>) { +const AvatarImage = React.forwardRef< + React.ElementRef<typeof AvatarPrimitive.Image>, + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> +>(({ className, ...props }, ref) => { return ( <AvatarPrimitive.Image + ref={ref} data-slot="avatar-image" className={cn("aspect-square size-full", className)} {...props} /> ); -} +}); @@ -function AvatarFallback({ - className, - ...props -}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { +const AvatarFallback = React.forwardRef< + React.ElementRef<typeof AvatarPrimitive.Fallback>, + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> +>(({ className, ...props }, ref) => { return ( <AvatarPrimitive.Fallback + ref={ref} data-slot="avatar-fallback" className={cn( "bg-muted flex size-full items-center justify-center rounded-full", className, )} {...props} /> ); -} +});Also applies to: 24-28, 37-41
apps/playground/src/components/ui/button.tsx (1)
37-46: Forward ref to match Radix wrapper patterns and enable ref usageBrings Button in line with other primitives and typical ShadCN/CVA patterns.
-function Button({ - className, - variant, - size, - asChild = false, - ...props -}: React.ComponentProps<"button"> & - VariantProps<typeof buttonVariants> & { - asChild?: boolean; - }) { - const Comp = asChild ? Slot : "button"; +const Button = React.forwardRef< + HTMLButtonElement, + React.ComponentPropsWithoutRef<"button"> & + VariantProps<typeof buttonVariants> & { + asChild?: boolean; + } +>(({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size }), className)} + ref={ref} {...props} /> ); -} +});apps/playground/src/app/login/page.tsx (1)
46-48: Guard PostHog calls if the client is unavailableusePostHog can be undefined; optional-chain to avoid runtime errors in dev/misconfig.
- useEffect(() => { - posthog.capture("page_viewed_login"); - }, [posthog]); + useEffect(() => { + posthog?.capture("page_viewed_login"); + }, [posthog]); @@ - posthog.identify(ctx.data.user.id, { + posthog?.identify(ctx.data.user.id, { email: ctx.data.user.email, name: ctx.data.user.name, }); - posthog.capture("user_logged_in", { + posthog?.capture("user_logged_in", { method: "email", - email: values.email, }); @@ - posthog.capture("user_logged_in", { method: "passkey" }); + posthog?.capture("user_logged_in", { method: "passkey" });Also applies to: 75-78, 79-83, 122-123
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (10)
apps/playground/public/favicon/android-chrome-192x192.pngis excluded by!**/*.pngapps/playground/public/favicon/android-chrome-512x512.pngis excluded by!**/*.pngapps/playground/public/favicon/apple-touch-icon.pngis excluded by!**/*.pngapps/playground/public/favicon/favicon-16x16.pngis excluded by!**/*.pngapps/playground/public/favicon/favicon-32x32.pngis excluded by!**/*.pngapps/playground/public/favicon/favicon.icois excluded by!**/*.icoapps/playground/public/opengraph.pngis excluded by!**/*.pngapps/playground/src/lib/api/v1.d.tsis excluded by!**/v1.d.tsapps/ui/public/next.svgis excluded by!**/*.svgapps/ui/public/vercel.svgis excluded by!**/*.svg
📒 Files selected for processing (87)
apps/playground/eslint.config.mjs(1 hunks)apps/playground/package.json(1 hunks)apps/playground/postcss.config.mjs(1 hunks)apps/playground/public/favicon/site.webmanifest(1 hunks)apps/playground/src/app/api/chat/route.ts(1 hunks)apps/playground/src/app/layout.tsx(1 hunks)apps/playground/src/app/login/page.tsx(1 hunks)apps/playground/src/app/page.tsx(1 hunks)apps/playground/src/app/signup/page.tsx(1 hunks)apps/playground/src/components/ai-elements/actions.tsx(1 hunks)apps/playground/src/components/ai-elements/artifact.tsx(1 hunks)apps/playground/src/components/ai-elements/branch.tsx(1 hunks)apps/playground/src/components/ai-elements/chain-of-thought.tsx(1 hunks)apps/playground/src/components/ai-elements/code-block.tsx(1 hunks)apps/playground/src/components/ai-elements/context.tsx(1 hunks)apps/playground/src/components/ai-elements/conversation.tsx(1 hunks)apps/playground/src/components/ai-elements/image.tsx(1 hunks)apps/playground/src/components/ai-elements/inline-citation.tsx(1 hunks)apps/playground/src/components/ai-elements/loader.tsx(1 hunks)apps/playground/src/components/ai-elements/message.tsx(1 hunks)apps/playground/src/components/ai-elements/open-in-chat.tsx(1 hunks)apps/playground/src/components/ai-elements/prompt-input.tsx(1 hunks)apps/playground/src/components/ai-elements/reasoning.tsx(1 hunks)apps/playground/src/components/ai-elements/response.tsx(1 hunks)apps/playground/src/components/ai-elements/sources.tsx(1 hunks)apps/playground/src/components/ai-elements/suggestion.tsx(1 hunks)apps/playground/src/components/ai-elements/task.tsx(1 hunks)apps/playground/src/components/ai-elements/tool.tsx(1 hunks)apps/playground/src/components/ai-elements/web-preview.tsx(1 hunks)apps/playground/src/components/credits/credits-display.tsx(1 hunks)apps/playground/src/components/credits/top-up-credits-dialog.tsx(1 hunks)apps/playground/src/components/landing/theme-toggle.tsx(1 hunks)apps/playground/src/components/model-selector.tsx(1 hunks)apps/playground/src/components/playground/api-key-manager.tsx(1 hunks)apps/playground/src/components/playground/auth-dialog.tsx(1 hunks)apps/playground/src/components/playground/chat-header.tsx(1 hunks)apps/playground/src/components/playground/chat-page-client.tsx(1 hunks)apps/playground/src/components/playground/chat-sidebar.tsx(1 hunks)apps/playground/src/components/playground/chat-ui.tsx(1 hunks)apps/playground/src/components/provider-icons.tsx(1 hunks)apps/playground/src/components/providers.tsx(1 hunks)apps/playground/src/components/ui/alert.tsx(1 hunks)apps/playground/src/components/ui/avatar.tsx(1 hunks)apps/playground/src/components/ui/badge.tsx(1 hunks)apps/playground/src/components/ui/button.tsx(1 hunks)apps/playground/src/components/ui/carousel.tsx(1 hunks)apps/playground/src/components/ui/checkbox.tsx(1 hunks)apps/playground/src/components/ui/command.tsx(1 hunks)apps/playground/src/components/ui/dialog.tsx(1 hunks)apps/playground/src/components/ui/dropdown-menu.tsx(1 hunks)apps/playground/src/components/ui/form.tsx(1 hunks)apps/playground/src/components/ui/hover-card.tsx(1 hunks)apps/playground/src/components/ui/input.tsx(1 hunks)apps/playground/src/components/ui/label.tsx(1 hunks)apps/playground/src/components/ui/popover.tsx(1 hunks)apps/playground/src/components/ui/progress.tsx(1 hunks)apps/playground/src/components/ui/providers-icons.tsx(1 hunks)apps/playground/src/components/ui/scroll-area.tsx(1 hunks)apps/playground/src/components/ui/select.tsx(1 hunks)apps/playground/src/components/ui/separator.tsx(1 hunks)apps/playground/src/components/ui/sheet.tsx(1 hunks)apps/playground/src/components/ui/sidebar.tsx(1 hunks)apps/playground/src/components/ui/skeleton.tsx(1 hunks)apps/playground/src/components/ui/sonner.tsx(1 hunks)apps/playground/src/components/ui/tabs.tsx(1 hunks)apps/playground/src/components/ui/textarea.tsx(1 hunks)apps/playground/src/components/ui/tooltip.tsx(1 hunks)apps/playground/src/hooks/use-mobile.ts(1 hunks)apps/playground/src/hooks/useApiKey.ts(1 hunks)apps/playground/src/hooks/useAutoApiKey.ts(1 hunks)apps/playground/src/hooks/useChats.ts(1 hunks)apps/playground/src/hooks/useCreateApiKey.ts(1 hunks)apps/playground/src/hooks/useUser.ts(1 hunks)apps/playground/src/lib/mapmodels.ts(1 hunks)apps/playground/src/lib/model-utils.ts(1 hunks)apps/playground/src/lib/server-api.ts(1 hunks)apps/playground/src/lib/types.ts(1 hunks)apps/playground/src/lib/utils.ts(1 hunks)apps/playground/tsconfig.json(1 hunks)apps/ui/eslint.config.mjs(0 hunks)apps/ui/src/app/layout.tsx(2 hunks)apps/ui/src/app/playground/playground-client.tsx(3 hunks)apps/ui/src/components/Chat.tsx(2 hunks)apps/ui/src/components/app-sidebar.tsx(2 hunks)infra/split.dockerfile(1 hunks)infra/unified.dockerfile(1 hunks)package.json(1 hunks)
💤 Files with no reviewable changes (1)
- apps/ui/eslint.config.mjs
✅ Files skipped from review due to trivial changes (2)
- apps/playground/tsconfig.json
- apps/playground/public/favicon/site.webmanifest
🚧 Files skipped from review as they are similar to previous changes (27)
- apps/playground/src/components/ui/sonner.tsx
- apps/playground/src/components/playground/chat-ui.tsx
- apps/playground/src/components/ai-elements/actions.tsx
- apps/playground/src/components/ui/hover-card.tsx
- apps/playground/src/components/ui/label.tsx
- apps/playground/package.json
- apps/playground/src/app/page.tsx
- apps/playground/src/components/ai-elements/artifact.tsx
- apps/playground/src/components/credits/credits-display.tsx
- apps/playground/src/components/ui/separator.tsx
- apps/playground/src/components/ai-elements/sources.tsx
- apps/playground/src/components/ai-elements/web-preview.tsx
- apps/playground/src/components/playground/chat-header.tsx
- apps/playground/src/components/ui/form.tsx
- apps/playground/src/components/ui/providers-icons.tsx
- apps/playground/src/components/ui/select.tsx
- apps/playground/src/components/ai-elements/response.tsx
- apps/playground/src/components/ui/dialog.tsx
- apps/playground/src/hooks/use-mobile.ts
- apps/playground/src/components/ui/input.tsx
- apps/playground/src/components/landing/theme-toggle.tsx
- apps/playground/src/components/provider-icons.tsx
- apps/playground/src/components/model-selector.tsx
- apps/playground/src/components/ui/progress.tsx
- apps/playground/src/components/ui/alert.tsx
- apps/playground/eslint.config.mjs
- apps/playground/src/components/ui/dropdown-menu.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never useanyoras anyin this TypeScript project unless absolutely necessary
Always use top-levelimport; never userequireor dynamic imports
Files:
apps/playground/src/lib/types.tsapps/playground/src/components/ui/avatar.tsxapps/ui/src/app/playground/playground-client.tsxapps/playground/src/components/ui/scroll-area.tsxapps/playground/src/components/ai-elements/task.tsxapps/playground/src/components/playground/chat-sidebar.tsxapps/playground/src/app/layout.tsxapps/playground/src/hooks/useAutoApiKey.tsapps/playground/src/components/ui/textarea.tsxapps/playground/src/components/ai-elements/message.tsxapps/playground/src/lib/server-api.tsapps/playground/src/hooks/useUser.tsapps/playground/src/components/ai-elements/open-in-chat.tsxapps/playground/src/components/ui/checkbox.tsxapps/playground/src/components/ui/tabs.tsxapps/playground/src/components/ai-elements/image.tsxapps/playground/src/app/login/page.tsxapps/playground/src/components/ui/skeleton.tsxapps/ui/src/components/app-sidebar.tsxapps/playground/src/components/ui/badge.tsxapps/playground/src/components/ai-elements/reasoning.tsxapps/playground/src/components/credits/top-up-credits-dialog.tsxapps/playground/src/components/ui/command.tsxapps/playground/src/hooks/useChats.tsapps/playground/src/components/ai-elements/conversation.tsxapps/playground/src/components/ui/button.tsxapps/playground/src/components/ai-elements/context.tsxapps/playground/src/components/ui/popover.tsxapps/ui/src/components/Chat.tsxapps/playground/src/components/ai-elements/chain-of-thought.tsxapps/playground/src/lib/mapmodels.tsapps/playground/src/lib/model-utils.tsapps/playground/src/hooks/useApiKey.tsapps/playground/src/components/ai-elements/code-block.tsxapps/playground/src/hooks/useCreateApiKey.tsapps/playground/src/components/ai-elements/branch.tsxapps/playground/src/components/ui/sidebar.tsxapps/playground/src/components/ai-elements/loader.tsxapps/playground/src/components/ui/tooltip.tsxapps/playground/src/lib/utils.tsapps/playground/src/components/ui/sheet.tsxapps/playground/src/components/providers.tsxapps/playground/src/components/playground/auth-dialog.tsxapps/playground/src/components/ai-elements/inline-citation.tsxapps/playground/src/app/api/chat/route.tsapps/playground/src/app/signup/page.tsxapps/playground/src/components/ai-elements/suggestion.tsxapps/ui/src/app/layout.tsxapps/playground/src/components/playground/chat-page-client.tsxapps/playground/src/components/ai-elements/tool.tsxapps/playground/src/components/playground/api-key-manager.tsxapps/playground/src/components/ui/carousel.tsxapps/playground/src/components/ai-elements/prompt-input.tsx
{apps/api,apps/gateway,apps/ui,apps/docs,packages}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Always use top-level import; never use require() or dynamic imports (e.g., import(), next/dynamic)
Files:
apps/ui/src/app/playground/playground-client.tsxapps/ui/src/components/app-sidebar.tsxapps/ui/src/components/Chat.tsxapps/ui/src/app/layout.tsx
apps/ui/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use next/link for links and next/navigation’s router for programmatic navigation
apps/ui/**/*.{ts,tsx}: Use next/link for links and next/navigation's router for programmatic navigation in the UI
Use cookies for user settings not saved in the database to ensure SSR works
Files:
apps/ui/src/app/playground/playground-client.tsxapps/ui/src/components/app-sidebar.tsxapps/ui/src/components/Chat.tsxapps/ui/src/app/layout.tsx
🧠 Learnings (3)
📚 Learning: 2025-09-21T20:12:20.016Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-21T20:12:20.016Z
Learning: Applies to **/*.{ts,tsx} : Never use `any` or `as any` in this TypeScript project unless absolutely necessary
Applied to files:
apps/playground/src/components/credits/top-up-credits-dialog.tsx
📚 Learning: 2025-09-15T13:16:05.365Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: AGENTS.md:0-0
Timestamp: 2025-09-15T13:16:05.365Z
Learning: Applies to {apps/api,apps/gateway,apps/ui,apps/docs,packages}/**/*.{ts,tsx} : Always use top-level import; never use require() or dynamic imports (e.g., import(), next/dynamic)
Applied to files:
apps/playground/src/components/providers.tsx
📚 Learning: 2025-09-21T20:12:20.016Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-21T20:12:20.016Z
Learning: Applies to **/*.{ts,tsx} : Always use top-level `import`; never use `require` or dynamic imports
Applied to files:
apps/playground/src/components/providers.tsx
🧬 Code graph analysis (42)
apps/playground/src/components/ui/avatar.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/ui/src/app/playground/playground-client.tsx (1)
apps/ui/src/lib/components/use-toast.ts (1)
toast(194-194)
apps/playground/src/components/ai-elements/task.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/playground/chat-sidebar.tsx (6)
apps/api/src/posthog.ts (1)
posthog(3-6)apps/playground/src/hooks/useUser.ts (1)
useUser(24-105)apps/playground/src/lib/auth-client.ts (1)
useAuth(20-33)apps/playground/src/hooks/useOrganization.ts (1)
useOrganization(10-30)apps/playground/src/hooks/useChats.ts (4)
useChats(25-29)useDeleteChat(90-109)useUpdateChat(69-88)Chat(6-14)apps/playground/src/components/credits/credits-display.tsx (1)
CreditsDisplay(19-79)
apps/playground/src/app/layout.tsx (3)
apps/playground/src/app/page.tsx (1)
dynamic(15-15)apps/playground/src/lib/config-server.ts (1)
getConfig(14-29)apps/playground/src/lib/providers.tsx (1)
Providers(21-69)
apps/playground/src/hooks/useAutoApiKey.ts (1)
apps/playground/src/hooks/useDefaultProject.ts (1)
useDefaultProject(3-36)
apps/playground/src/components/ui/textarea.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/message.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/hooks/useUser.ts (1)
apps/playground/src/lib/fetch-client.ts (1)
useApi(22-28)
apps/playground/src/components/ai-elements/open-in-chat.tsx (3)
apps/playground/src/components/ui/dropdown-menu.tsx (6)
DropdownMenu(242-242)DropdownMenuContent(245-245)DropdownMenuItem(248-248)DropdownMenuLabel(247-247)DropdownMenuSeparator(252-252)DropdownMenuTrigger(244-244)apps/playground/src/lib/utils.ts (1)
cn(4-6)apps/playground/src/components/ui/button.tsx (1)
Button(58-58)
apps/playground/src/components/ui/checkbox.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/image.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/app/login/page.tsx (1)
apps/api/src/posthog.ts (1)
posthog(3-6)
apps/ui/src/components/app-sidebar.tsx (1)
apps/ui/src/lib/components/use-toast.ts (1)
toast(194-194)
apps/playground/src/components/ui/badge.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/reasoning.tsx (2)
apps/playground/src/lib/utils.ts (1)
cn(4-6)apps/playground/src/components/ai-elements/response.tsx (1)
Response(10-21)
apps/playground/src/components/credits/top-up-credits-dialog.tsx (2)
apps/playground/src/lib/stripe.ts (1)
useStripe(20-38)apps/playground/src/hooks/useOrganization.ts (1)
useOrganization(10-30)
apps/playground/src/components/ui/command.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/hooks/useChats.ts (1)
apps/playground/src/lib/fetch-client.ts (1)
useApi(22-28)
apps/playground/src/components/ai-elements/conversation.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ui/button.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/context.tsx (3)
apps/playground/src/components/ui/hover-card.tsx (3)
HoverCard(44-44)HoverCardTrigger(44-44)HoverCardContent(44-44)apps/playground/src/components/ui/button.tsx (1)
Button(58-58)apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ui/popover.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/chain-of-thought.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/lib/mapmodels.ts (3)
packages/models/src/models.ts (1)
ModelDefinition(108-161)packages/models/src/providers.ts (2)
providers(19-247)ProviderDefinition(1-17)apps/playground/src/lib/types.ts (1)
ComboboxModel(39-50)
apps/playground/src/lib/model-utils.ts (3)
packages/db/src/schema.ts (2)
model(566-598)provider(531-564)packages/models/src/models.ts (1)
ModelDefinition(108-161)packages/models/src/providers.ts (2)
providers(19-247)ProviderDefinition(1-17)
apps/playground/src/hooks/useApiKey.ts (1)
apps/ui/src/hooks/useApiKey.ts (1)
useApiKey(6-60)
apps/playground/src/components/ai-elements/code-block.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/hooks/useCreateApiKey.ts (2)
apps/api/src/posthog.ts (1)
posthog(3-6)apps/ui/src/hooks/useCreateApiKey.tsx (1)
useCreateApiKey(8-54)
apps/playground/src/components/ai-elements/branch.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/loader.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/lib/utils.ts (2)
apps/docs/lib/cn.ts (1)
twMerge(1-1)apps/ui/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/providers.tsx (2)
apps/playground/src/lib/config.tsx (1)
AppConfigProvider(14-23)apps/docs/lib/providers.tsx (1)
PostHogProvider(11-36)
apps/playground/src/components/playground/auth-dialog.tsx (1)
apps/playground/src/components/ui/button.tsx (1)
Button(58-58)
apps/playground/src/components/ai-elements/inline-citation.tsx (4)
apps/playground/src/lib/utils.ts (1)
cn(4-6)apps/playground/src/components/ui/hover-card.tsx (3)
HoverCard(44-44)HoverCardTrigger(44-44)HoverCardContent(44-44)apps/playground/src/components/ui/badge.tsx (1)
Badge(46-46)apps/playground/src/components/ui/carousel.tsx (4)
CarouselApi(242-242)Carousel(243-243)CarouselContent(244-244)CarouselItem(245-245)
apps/playground/src/app/api/chat/route.ts (2)
apps/playground/src/components/ai-elements/response.tsx (1)
Response(10-21)packages/db/src/schema.ts (2)
apiKey(228-249)model(566-598)
apps/playground/src/app/signup/page.tsx (1)
apps/api/src/posthog.ts (1)
posthog(3-6)
apps/playground/src/components/ai-elements/suggestion.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/playground/chat-page-client.tsx (8)
packages/models/src/models.ts (1)
ModelDefinition(108-161)packages/models/src/providers.ts (2)
ProviderDefinition(1-17)providers(19-247)apps/playground/src/lib/mapmodels.ts (1)
mapModels(4-27)apps/playground/src/lib/types.ts (1)
ComboboxModel(39-50)apps/playground/src/hooks/useChats.ts (1)
useDataChat(31-46)apps/playground/src/components/playground/chat-sidebar.tsx (1)
ChatSidebar(61-417)apps/playground/src/components/playground/chat-ui.tsx (1)
ChatUI(71-251)apps/playground/src/components/playground/api-key-manager.tsx (1)
ApiKeyManager(129-417)
apps/playground/src/components/ai-elements/tool.tsx (2)
apps/playground/src/lib/utils.ts (1)
cn(4-6)apps/playground/src/components/ai-elements/code-block.tsx (1)
CodeBlock(31-106)
apps/playground/src/components/playground/api-key-manager.tsx (3)
apps/playground/src/hooks/useApiKey.ts (1)
useApiKey(9-63)apps/playground/src/lib/config.tsx (1)
useAppConfig(25-31)apps/playground/src/hooks/useAutoApiKey.ts (1)
useAutoApiKey(13-77)
apps/playground/src/components/ai-elements/prompt-input.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
🪛 ast-grep (0.39.5)
apps/playground/src/hooks/useApiKey.ts
[warning] 39-39: Sensitive information detected in localStorage. Storing sensitive data like emails, usernames, or personal information in localStorage exposes it to malicious scripts and XSS attacks. Use secure storage alternatives or avoid storing sensitive data client-side.
Context: localStorage.setItem(API_KEY_STORAGE_KEY, key)
Note: [CWE-312] Cleartext Storage of Sensitive Information [REFERENCES]
- https://cwe.mitre.org/data/definitions/312.html
- https://owasp.org/www-community/vulnerabilities/HTML5_Local_Storage_Security
(local-storage-sensitive-information)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: build-split (gateway, linux/amd64)
- GitHub Check: build-split (ui, linux/amd64)
- GitHub Check: build-split (api, linux/amd64)
- GitHub Check: build-split (docs, linux/amd64)
- GitHub Check: build-unified (linux/amd64)
| const result = streamText({ | ||
| model: llmgateway.chat(selectedModel), | ||
| messages: convertToModelMessages(messages), | ||
| }); | ||
|
|
||
| return result.toUIMessageStreamResponse(); |
There was a problem hiding this comment.
Await streamText before using its response helpers
streamText returns a promise; without awaiting it, result is a Promise lacking toUIMessageStreamResponse, so this path will throw at runtime. Await the call before accessing the helper.
- const result = streamText({
+ const result = await streamText({
model: llmgateway.chat(selectedModel),
messages: convertToModelMessages(messages),
});📝 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.
| const result = streamText({ | |
| model: llmgateway.chat(selectedModel), | |
| messages: convertToModelMessages(messages), | |
| }); | |
| return result.toUIMessageStreamResponse(); | |
| const result = await streamText({ | |
| model: llmgateway.chat(selectedModel), | |
| messages: convertToModelMessages(messages), | |
| }); | |
| return result.toUIMessageStreamResponse(); |
🤖 Prompt for AI Agents
In apps/playground/src/app/api/chat/route.ts around lines 50 to 55, the code
calls streamText(...) but does not await its promise before calling
toUIMessageStreamResponse, causing a runtime error; change the call to await
streamText(...) (ensuring the enclosing function is async if it isn’t), assign
the resolved value to result, then call result.toUIMessageStreamResponse() and
return that response.
| @@ -0,0 +1,57 @@ | |||
| import { Geist, Geist_Mono } from "next/font/google"; | |||
There was a problem hiding this comment.
Build blocker: Wrong font import source for Geist
Geist/Geist_Mono aren’t from next/font/google. Use the Geist font package.
-import { Geist, Geist_Mono } from "next/font/google";
+import { GeistSans } from "geist/font/sans";
+import { GeistMono } from "geist/font/mono";
@@
-const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
+const geistSans = GeistSans({
+ variable: "--font-geist-sans",
+});
@@
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
- subsets: ["latin"],
-});
+const geistMono = GeistMono({
+ variable: "--font-geist-mono",
+});Also applies to: 11-15, 16-20
🤖 Prompt for AI Agents
In apps/playground/src/app/layout.tsx (lines 1 and also around 11-15 and 16-20),
the code imports Geist and Geist_Mono from "next/font/google" which is
incorrect; change the import to the official Geist font package (e.g., import
the fonts from the Geist font package your project uses such as "geist-font" or
the package provided by Geist UI), update any variable names if the package
export names differ, and ensure the package is added to package.json and
installed; also update any subsequent uses of those font objects in the file to
match the new package's API.
| posthog.capture("user_logged_in", { | ||
| method: "email", | ||
| email: values.email, | ||
| }); | ||
| toast.success("Login successful"); |
There was a problem hiding this comment.
Avoid sending email PII in analytics event payloads
Email in event properties can violate privacy/compliance policies. Prefer identify-only or hashed tokens; keep event minimal.
- posthog.capture("user_logged_in", {
- method: "email",
- email: values.email,
- });
+ posthog?.capture("user_logged_in", { method: "email" });📝 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.
| posthog.capture("user_logged_in", { | |
| method: "email", | |
| email: values.email, | |
| }); | |
| toast.success("Login successful"); | |
| posthog?.capture("user_logged_in", { method: "email" }); | |
| toast.success("Login successful"); |
🤖 Prompt for AI Agents
In apps/playground/src/app/login/page.tsx around lines 79 to 83, the
posthog.capture call currently includes the user's email (PII) in the event
payload; remove the raw email from analytics events and instead send a non-PII
identifier (e.g., a stable user id) or a hashed/tokenized version of the email
if you must correlate events. Update the capture to omit the email property, or
replace it with a hashedEmail/anonymousId value computed server-side or via a
secure hash function, or alternatively call posthog.identify separately with
authenticated user metadata while keeping capture properties minimal.
| if (error) { | ||
| toast.error(error.message || "An unknown error occurred", { | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Prevent duplicate error toasts on email sign-in failures
You toast in onError callback and again after. Remove one to avoid double messaging.
- if (error) {
- toast.error(error.message || "An unknown error occurred", {
- style: {
- backgroundColor: "var(--destructive)",
- color: "var(--destructive-foreground)",
- },
- });
- }
+ if (error) {
+ // onError already handled toast; optionally handle non-callback path here if you remove onError above.
+ }📝 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.
| if (error) { | |
| toast.error(error.message || "An unknown error occurred", { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| } | |
| if (error) { | |
| // onError already handled toast; optionally handle non-callback path here if you remove onError above. | |
| } |
🤖 Prompt for AI Agents
In apps/playground/src/app/login/page.tsx around lines 97 to 104, the code shows
an error toast here but the same toast is already triggered in the onError
callback, causing duplicate messages; remove this toast block (or guard it with
a flag so only one of the two paths shows a toast) and ensure only the onError
handler displays the error toast so users see a single message.
| useEffect(() => { | ||
| posthog.capture("page_viewed_signup"); | ||
| }, [posthog]); | ||
|
|
||
| const form = useForm<z.infer<typeof formSchema>>({ | ||
| resolver: zodResolver(formSchema), | ||
| defaultValues: { | ||
| name: "", | ||
| email: "", | ||
| password: "", | ||
| }, | ||
| }); | ||
|
|
||
| async function onSubmit(values: z.infer<typeof formSchema>) { | ||
| setIsLoading(true); | ||
|
|
||
| const { error } = await signUp.email( | ||
| { | ||
| name: values.name, | ||
| email: values.email, | ||
| password: values.password, | ||
| }, | ||
| { | ||
| onSuccess: (ctx) => { | ||
| queryClient.clear(); | ||
| posthog.identify(ctx.data.user.id, { | ||
| email: ctx.data.user.email, | ||
| name: ctx.data.user.name, | ||
| }); | ||
| posthog.capture("user_signed_up", { | ||
| email: values.email, | ||
| name: values.name, | ||
| }); | ||
| toast.success("Account created", { | ||
| description: | ||
| "Please check your email to verify your account before signing in.", | ||
| }); | ||
| router.push("/onboarding"); | ||
| }, | ||
| onError: (ctx) => { | ||
| toast.error(ctx.error.message || "Failed to sign up", { | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| if (error) { | ||
| toast.error(error.message || "Failed to sign up", { | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Guard PostHog calls when analytics is disabled
usePostHog() returns undefined when the provider isn’t mounted (common when no PostHog key is configured). Calling posthog.capture / identify unguarded will throw and crash the signup page. Please nil-check posthog before invoking its methods.
- useEffect(() => {
- posthog.capture("page_viewed_signup");
- }, [posthog]);
+ useEffect(() => {
+ if (posthog) {
+ posthog.capture("page_viewed_signup");
+ }
+ }, [posthog]);
@@
- onSuccess: (ctx) => {
+ onSuccess: (ctx) => {
queryClient.clear();
- posthog.identify(ctx.data.user.id, {
- email: ctx.data.user.email,
- name: ctx.data.user.name,
- });
- posthog.capture("user_signed_up", {
- email: values.email,
- name: values.name,
- });
+ if (posthog) {
+ posthog.identify(ctx.data.user.id, {
+ email: ctx.data.user.email,
+ name: ctx.data.user.name,
+ });
+ posthog.capture("user_signed_up", {
+ email: values.email,
+ name: values.name,
+ });
+ }📝 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.
| useEffect(() => { | |
| posthog.capture("page_viewed_signup"); | |
| }, [posthog]); | |
| const form = useForm<z.infer<typeof formSchema>>({ | |
| resolver: zodResolver(formSchema), | |
| defaultValues: { | |
| name: "", | |
| email: "", | |
| password: "", | |
| }, | |
| }); | |
| async function onSubmit(values: z.infer<typeof formSchema>) { | |
| setIsLoading(true); | |
| const { error } = await signUp.email( | |
| { | |
| name: values.name, | |
| email: values.email, | |
| password: values.password, | |
| }, | |
| { | |
| onSuccess: (ctx) => { | |
| queryClient.clear(); | |
| posthog.identify(ctx.data.user.id, { | |
| email: ctx.data.user.email, | |
| name: ctx.data.user.name, | |
| }); | |
| posthog.capture("user_signed_up", { | |
| email: values.email, | |
| name: values.name, | |
| }); | |
| toast.success("Account created", { | |
| description: | |
| "Please check your email to verify your account before signing in.", | |
| }); | |
| router.push("/onboarding"); | |
| }, | |
| onError: (ctx) => { | |
| toast.error(ctx.error.message || "Failed to sign up", { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| }, | |
| }, | |
| ); | |
| if (error) { | |
| toast.error(error.message || "Failed to sign up", { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| } | |
| useEffect(() => { | |
| if (posthog) { | |
| posthog.capture("page_viewed_signup"); | |
| } | |
| }, [posthog]); | |
| const form = useForm<z.infer<typeof formSchema>>({ | |
| resolver: zodResolver(formSchema), | |
| defaultValues: { | |
| name: "", | |
| email: "", | |
| password: "", | |
| }, | |
| }); | |
| async function onSubmit(values: z.infer<typeof formSchema>) { | |
| setIsLoading(true); | |
| const { error } = await signUp.email( | |
| { | |
| name: values.name, | |
| email: values.email, | |
| password: values.password, | |
| }, | |
| { | |
| onSuccess: (ctx) => { | |
| queryClient.clear(); | |
| if (posthog) { | |
| posthog.identify(ctx.data.user.id, { | |
| email: ctx.data.user.email, | |
| name: ctx.data.user.name, | |
| }); | |
| posthog.capture("user_signed_up", { | |
| email: values.email, | |
| name: values.name, | |
| }); | |
| } | |
| toast.success("Account created", { | |
| description: | |
| "Please check your email to verify your account before signing in.", | |
| }); | |
| router.push("/onboarding"); | |
| }, | |
| onError: (ctx) => { | |
| toast.error(ctx.error.message || "Failed to sign up", { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| }, | |
| }, | |
| ); | |
| if (error) { | |
| toast.error(error.message || "Failed to sign up", { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| } |
🤖 Prompt for AI Agents
In apps/playground/src/app/signup/page.tsx around lines 48 to 105, PostHog
methods are called without checking that usePostHog() returned a client, which
can be undefined and crash the page; guard every posthog usage (posthog.capture,
posthog.identify, etc.) by first checking that posthog is truthy (or using
optional chaining) before calling methods, e.g., if (posthog)
posthog.capture(...), and apply the same guard in the onSuccess callback and the
useEffect so analytics calls are skipped when the provider/key is absent.
| } catch { | ||
| toast.error(`Server API error for ${method} ${path}`); | ||
| return null; |
There was a problem hiding this comment.
Avoid client-only toasts in server utilities
Line 92 runs toast.error(...), but this file lives on the server (imports next/headers), so the toast never renders—and importing sonner from the server bundle risks runtime breakage. Please remove the toast call (and import) and log/propagate the error in a server-safe way instead. For example:
-import { toast } from "sonner";
@@
- } catch {
- toast.error(`Server API error for ${method} ${path}`);
- return null;
- }
+ } catch (error) {
+ console.error(`Server API error for ${method} ${String(path)}`, error);
+ return null;
+ }📝 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.
| } catch { | |
| toast.error(`Server API error for ${method} ${path}`); | |
| return null; | |
| } catch (error) { | |
| console.error(`Server API error for ${method} ${String(path)}`, error); | |
| return null; | |
| } |
🤖 Prompt for AI Agents
In apps/playground/src/lib/server-api.ts around lines 90 to 92, the catch block
calls toast.error(...) which is a client-only UI call and unsafe on the server;
remove the toast import and call, replace it with a server-safe handling such as
logging the error (e.g., console.error or a server logger) and either rethrowing
the error or returning a structured error value (instead of null) so callers can
handle it; ensure no sonner/toast imports remain in this server file.
| import { Geist, Geist_Mono } from "next/font/google"; | ||
|
|
||
| import { Providers } from "@/components/providers"; | ||
| import { getConfig } from "@/lib/config-server"; | ||
|
|
||
| import "./globals.css"; | ||
|
|
||
| import type { Metadata } from "next"; | ||
| import type { ReactNode } from "react"; | ||
|
|
||
| const geistSans = Geist({ | ||
| variable: "--font-geist-sans", | ||
| subsets: ["latin"], | ||
| }); | ||
|
|
||
| const geistMono = Geist_Mono({ | ||
| variable: "--font-geist-mono", | ||
| subsets: ["latin"], | ||
| }); |
There was a problem hiding this comment.
Fix the invalid font imports
next/font/google does not export Geist / Geist_Mono (Geist isn’t a Google font), so this will throw at build time. Import the preconfigured faces from the official geist/font/* entry points instead and drop the manual instantiation. Suggested fix:
-import { Geist, Geist_Mono } from "next/font/google";
+import { GeistSans } from "geist/font/sans";
+import { GeistMono } from "geist/font/mono";
…
-const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
- subsets: ["latin"],
-});Then update the body class to use GeistSans.variable / GeistMono.variable.
📝 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.
| import { Geist, Geist_Mono } from "next/font/google"; | |
| import { Providers } from "@/components/providers"; | |
| import { getConfig } from "@/lib/config-server"; | |
| import "./globals.css"; | |
| import type { Metadata } from "next"; | |
| import type { ReactNode } from "react"; | |
| const geistSans = Geist({ | |
| variable: "--font-geist-sans", | |
| subsets: ["latin"], | |
| }); | |
| const geistMono = Geist_Mono({ | |
| variable: "--font-geist-mono", | |
| subsets: ["latin"], | |
| }); | |
| import { GeistSans } from "geist/font/sans"; | |
| import { GeistMono } from "geist/font/mono"; | |
| import { Providers } from "@/components/providers"; | |
| import { getConfig } from "@/lib/config-server"; | |
| import "./globals.css"; | |
| import type { Metadata } from "next"; | |
| import type { ReactNode } from "react"; | |
| // Removed manual instantiation of Geist and Geist_Mono — use GeistSans.variable and GeistMono.variable in your <body> class instead |
🤖 Prompt for AI Agents
In apps/ui/src/app/layout.tsx around lines 1–19, the code incorrectly imports
Geist and Geist_Mono from next/font/google and manually instantiates them;
replace those imports with the preconfigured exports from the official geist
font entry points (e.g. import { GeistSans, GeistMono } from 'geist/font/...' or
the exact package export provided by the geist package), remove the Geist(...)
and Geist_Mono(...) calls, and update the root/body class usage to reference
GeistSans.variable and GeistMono.variable (adjust constant names to match the
imports).
| } catch { | ||
| toast.error("Failed to save assistant message"); | ||
| } |
There was a problem hiding this comment.
Preserve the underlying error when showing the toast
Swapping the catch (error) for a bare catch drops the original exception, so we no longer see why the persistence failed. Please keep the error object (and either log it or forward it to your telemetry) while still emitting the toast. The same applies to the non-streaming path a few lines below.
- } catch {
- toast.error("Failed to save assistant message");
+ } catch (error) {
+ console.error("Failed to save assistant message", error);
+ toast.error("Failed to save assistant message");📝 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.
| } catch { | |
| toast.error("Failed to save assistant message"); | |
| } | |
| } catch (error) { | |
| console.error("Failed to save assistant message", error); | |
| toast.error("Failed to save assistant message"); | |
| } |
🤖 Prompt for AI Agents
In apps/ui/src/app/playground/playground-client.tsx around lines 370 to 372 (and
the similar non-streaming path a few lines below), the bare `catch` is
discarding the original exception; change it to `catch (error)` and preserve the
error by logging it (e.g., console.error) or sending it to telemetry before
showing the toast, so the toast remains user-facing but the underlying error is
captured for debugging/metrics.
| } catch { | ||
| toast.error("Failed to save assistant message"); | ||
| } |
There was a problem hiding this comment.
Preserve the underlying error when showing the toast
Same concern here: without capturing the error, we lose all debugging signal when the DB write fails. Keep the caught error around (log it / send to telemetry) in addition to the toast so we can diagnose the underlying issue.
- } catch {
- toast.error("Failed to save assistant message");
+ } catch (error) {
+ console.error("Failed to save assistant message", error);
+ toast.error("Failed to save assistant message");📝 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.
| } catch { | |
| toast.error("Failed to save assistant message"); | |
| } | |
| } catch (error) { | |
| console.error("Failed to save assistant message", error); | |
| toast.error("Failed to save assistant message"); | |
| } |
🤖 Prompt for AI Agents
In apps/ui/src/app/playground/playground-client.tsx around lines 410 to 412, the
catch block currently swallows the thrown error and only shows a toast; instead
capture the caught error (e.g. catch (err)) and include it in diagnostics by
logging it via console.error or sending to telemetry before or after calling
toast.error so the user still sees the message but the underlying error details
are preserved for debugging and monitoring.
| } catch (error) { | ||
| console.error("Chat error:", error); | ||
| toast.error("Chat Error"); | ||
|
|
||
| // Show user-friendly error message | ||
| const errorMessage = | ||
| error instanceof Error ? error.message : "An unexpected error occurred"; | ||
| toast({ | ||
| title: "Chat Error", | ||
| toast.error("Chat Error", { | ||
| description: errorMessage, | ||
| variant: "destructive", | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Avoid firing duplicate error toasts on a single failure.
Calling toast.error("Chat Error") immediately followed by another toast.error("Chat Error", …) shows two toasts for one exception. That’s a UX regression and will spam users whenever the request fails. Please consolidate into a single toast invocation that includes the description/styling in one go.
- toast.error("Chat Error");
-
// Show user-friendly error message
const errorMessage =
error instanceof Error ? error.message : "An unexpected error occurred";
- toast.error("Chat Error", {
+ toast.error("Chat Error", {
description: errorMessage,
style: {
backgroundColor: "var(--destructive)",
color: "var(--destructive-foreground)",
},
});📝 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.
| } catch (error) { | |
| console.error("Chat error:", error); | |
| toast.error("Chat Error"); | |
| // Show user-friendly error message | |
| const errorMessage = | |
| error instanceof Error ? error.message : "An unexpected error occurred"; | |
| toast({ | |
| title: "Chat Error", | |
| toast.error("Chat Error", { | |
| description: errorMessage, | |
| variant: "destructive", | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| } catch (error) { | |
| // Show user-friendly error message | |
| const errorMessage = | |
| error instanceof Error ? error.message : "An unexpected error occurred"; | |
| toast.error("Chat Error", { | |
| description: errorMessage, | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In apps/ui/src/components/Chat.tsx around lines 70 to 82, the catch block
currently calls toast.error twice which results in duplicate toasts; consolidate
into a single toast.error call that includes the user-friendly description and
the styling options (use the error instanceof Error ? error.message : fallback
to set description) and remove the redundant initial toast.error so only one
toast with description and style is shown.
Added "playground" service configurations across Dockerfiles, Compose files, environment variables, and related test scripts. Updated documentation to reflect new service endpoints.
2063e8a to
fe89c37
Compare
left a comment
There was a problem hiding this comment.
Actionable comments posted: 26
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/playground/src/components/ui/checkbox.tsx (1)
9-33: Forward the ref through the Checkbox wrapper.Dropping the
refmeans consumers (e.g.,react-hook-form’sregister) can’t focus or control the underlying Radix root, degrading form integrations. Please forward the ref like the rest of the UI primitives.-import * as React from "react"; +import * as React from "react"; @@ -function Checkbox({ - className, - ...props -}: React.ComponentProps<typeof CheckboxPrimitive.Root>) { - return ( - <CheckboxPrimitive.Root +const Checkbox = React.forwardRef< + React.ElementRef<typeof CheckboxPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> +>(({ className, ...props }, ref) => { + return ( + <CheckboxPrimitive.Root data-slot="checkbox" className={cn( "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", className, )} + ref={ref} {...props} > <CheckboxPrimitive.Indicator data-slot="checkbox-indicator" className="flex items-center justify-center text-current transition-none" @@ - ); -} - -export { Checkbox }; + ); +}); + +Checkbox.displayName = + CheckboxPrimitive.Root.displayName ?? "Checkbox"; + +export { Checkbox };
🧹 Nitpick comments (21)
apps/ui/src/app/playground/playground-client.tsx (3)
370-372: Keep logging the save error details before showing the toastWe still need the original error context for troubleshooting; otherwise production issues become opaque. Please keep logging the caught error (even if just to
console.error) before surfacing the toast so we have breadcrumbs in devtools.- } catch { - toast.error("Failed to save assistant message"); + } catch (error) { + console.error("Failed to save assistant message:", error); + toast.error("Failed to save assistant message"); }
410-412: Retain the non-stream save error loggingSame as the streaming branch—dropping the log means we lose the actual exception details. Let’s keep logging before firing the toast.
- } catch { - toast.error("Failed to save assistant message"); + } catch (error) { + console.error("Failed to save assistant message:", error); + toast.error("Failed to save assistant message"); }
418-421: Preserve the send failure log outputWithout logging here we lose the root cause (network error, aborted fetch, etc.). Please keep logging before showing the toast so we can debug regressions more easily.
- } catch (error) { - toast.error("Error sending message"); + } catch (error) { + console.error("Error sending message:", error); + toast.error("Error sending message"); if (error instanceof Error && !error.message.includes("HTTP")) { setError("Failed to send message. Please try again."); }apps/playground/src/components/ui/scroll-area.tsx (2)
8-29: Forward ref on ScrollArea Root.Enables imperative scroll access and consistency with other primitives.
-function ScrollArea({ - className, - children, - ...props -}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) { - return ( - <ScrollAreaPrimitive.Root +const ScrollArea = React.forwardRef< + React.ElementRef<typeof ScrollAreaPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> +>(({ className, children, ...props }, ref) => { + return ( + <ScrollAreaPrimitive.Root data-slot="scroll-area" - className={cn("relative", className)} - {...props} - > + ref={ref} + className={cn("relative", className)} + {...props} + > <ScrollAreaPrimitive.Viewport data-slot="scroll-area-viewport" className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1" > {children} </ScrollAreaPrimitive.Viewport> <ScrollBar /> <ScrollAreaPrimitive.Corner /> - </ScrollAreaPrimitive.Root> - ); -} + </ScrollAreaPrimitive.Root> + ); +}); + +ScrollArea.displayName = "ScrollArea";
31-56: Forward ref on ScrollBar.Keeps parity with Radix parts and allows measuring/controlling scrollbar.
-function ScrollBar({ - className, - orientation = "vertical", - ...props -}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) { - return ( - <ScrollAreaPrimitive.ScrollAreaScrollbar +const ScrollBar = React.forwardRef< + React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, + React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> +>(({ className, orientation = "vertical", ...props }, ref) => { + return ( + <ScrollAreaPrimitive.ScrollAreaScrollbar data-slot="scroll-area-scrollbar" orientation={orientation} + ref={ref} className={cn( "flex touch-none p-px transition-colors select-none", orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent", orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent", className, )} {...props} > <ScrollAreaPrimitive.ScrollAreaThumb data-slot="scroll-area-thumb" className="bg-border relative flex-1 rounded-full" /> - </ScrollAreaPrimitive.ScrollAreaScrollbar> - ); -} + </ScrollAreaPrimitive.ScrollAreaScrollbar> + ); +}); + +ScrollBar.displayName = "ScrollBar";apps/playground/src/components/ui/avatar.tsx (3)
8-22: Forward ref on Avatar Root.Improves composability and parity with Radix primitives.
-function Avatar({ - className, - ...props -}: React.ComponentProps<typeof AvatarPrimitive.Root>) { - return ( - <AvatarPrimitive.Root +const Avatar = React.forwardRef< + React.ElementRef<typeof AvatarPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> +>(({ className, ...props }, ref) => { + return ( + <AvatarPrimitive.Root data-slot="avatar" className={cn( "relative flex size-8 shrink-0 overflow-hidden rounded-full", className, )} + ref={ref} {...props} /> - ); -} + ); +}); + +Avatar.displayName = "Avatar";
24-35: Forward ref on AvatarImage.-function AvatarImage({ - className, - ...props -}: React.ComponentProps<typeof AvatarPrimitive.Image>) { - return ( - <AvatarPrimitive.Image +const AvatarImage = React.forwardRef< + React.ElementRef<typeof AvatarPrimitive.Image>, + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> +>(({ className, ...props }, ref) => { + return ( + <AvatarPrimitive.Image data-slot="avatar-image" className={cn("aspect-square size-full", className)} + ref={ref} {...props} /> - ); -} + ); +}); + +AvatarImage.displayName = "AvatarImage";
37-51: Forward ref on AvatarFallback.-function AvatarFallback({ - className, - ...props -}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { - return ( - <AvatarPrimitive.Fallback +const AvatarFallback = React.forwardRef< + React.ElementRef<typeof AvatarPrimitive.Fallback>, + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> +>(({ className, ...props }, ref) => { + return ( + <AvatarPrimitive.Fallback data-slot="avatar-fallback" className={cn( "bg-muted flex size-full items-center justify-center rounded-full", className, )} + ref={ref} {...props} /> - ); -} + ); +}); + +AvatarFallback.displayName = "AvatarFallback";apps/playground/src/components/ui/button.tsx (1)
37-56: Forward ref on Button (supports asChild) and set displayName.Common pattern for UI libs; enables ref access and interop with Radix Slot.
-function Button({ - className, - variant, - size, - asChild = false, - ...props -}: React.ComponentProps<"button"> & - VariantProps<typeof buttonVariants> & { - asChild?: boolean; - }) { - const Comp = asChild ? Slot : "button"; - - return ( - <Comp - data-slot="button" - className={cn(buttonVariants({ variant, size, className }))} - {...props} - /> - ); -} +type ButtonProps = React.ComponentProps<"button"> & + VariantProps<typeof buttonVariants> & { asChild?: boolean }; + +const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + <Comp + data-slot="button" + className={cn(buttonVariants({ variant, size, className }))} + ref={ref as any} + {...props} + /> + ); + }, +); + +Button.displayName = "Button";apps/playground/src/components/ai-elements/message.tsx (1)
24-37: Drop the invalid Tailwind variant.
is-user:darkisn’t a valid Tailwind modifier and never applies. Remove it to keep the variant clean.-const messageContentVariants = cva( - "is-user:dark flex flex-col gap-2 overflow-hidden rounded-lg text-sm", +const messageContentVariants = cva( + "flex flex-col gap-2 overflow-hidden rounded-lg text-sm",apps/playground/src/components/playground/auth-dialog.tsx (1)
12-26: Add dialog semantics for accessibility.This overlay lacks
role="dialog"/aria-modal, so assistive tech won’t announce it as a blocking dialog. Please tag the dialog container correctly (and wire up labelled/described-by ids) so screen-reader users get the proper context.- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> - <div className="w-[420px] rounded-md border bg-background p-4 shadow-md"> - <div className="text-sm font-medium mb-2">Sign in required</div> - <p className="text-sm text-muted-foreground mb-3"> + <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> + <div + role="dialog" + aria-modal="true" + aria-labelledby="auth-dialog-title" + aria-describedby="auth-dialog-description" + className="w-[420px] rounded-md border bg-background p-4 shadow-md" + > + <h2 id="auth-dialog-title" className="text-sm font-medium mb-2"> + Sign in required + </h2> + <p id="auth-dialog-description" className="text-sm text-muted-foreground mb-3">apps/playground/src/lib/utils.ts (1)
1-6: Consider de-duplicatingcnacross apps.
apps/ui/src/lib/utils.tsdefines the samecn. Re-export from a shared module to avoid drift.apps/playground/src/components/ui/skeleton.tsx (1)
3-11: Optional: forward the ref for better composability.Skeletons are often wrapped and animated; forwarding the ref makes it more flexible.
-function Skeleton({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="skeleton" - className={cn("bg-accent animate-pulse rounded-md", className)} - {...props} - /> - ); -} +const Skeleton = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>( + ({ className, ...props }, ref) => ( + <div + ref={ref} + data-slot="skeleton" + className={cn("bg-accent animate-pulse rounded-md", className)} + {...props} + /> + ), +); +Skeleton.displayName = "Skeleton";apps/playground/src/lib/types.ts (2)
7-37: Model IAM rules as a discriminated union for safer typing.Today
ruleTypeandruleValuecan be mismatched. A discriminated union prevents invalid combinations at compile time.Here’s a sketch you can adapt:
type PricingRuleValue = { pricingType: "free" | "paid"; maxInputPrice?: number; maxOutputPrice?: number; }; type ModelsRuleValue = { models: string[] }; type ProvidersRuleValue = { providers: string[] }; type IAMRule = | { id: string; createdAt: string; updatedAt: string; status: "active" | "inactive"; ruleType: "allow_models" | "deny_models"; ruleValue: ModelsRuleValue } | { id: string; createdAt: string; updatedAt: string; status: "active" | "inactive"; ruleType: "allow_providers" | "deny_providers"; ruleValue: ProvidersRuleValue } | { id: string; createdAt: string; updatedAt: string; status: "active" | "inactive"; ruleType: "allow_pricing" | "deny_pricing"; ruleValue: PricingRuleValue }; export interface ApiKey { // ... iamRules?: IAMRule[]; }
1-5: Nit: preferUser | nullalias for readability.Wrap in
type Nullable<T> = T | null;thenexport type User = Nullable<{ id: string; email: string; name: string | null }>;apps/playground/src/components/ui/badge.tsx (1)
28-44: Forward ref to support focusing/measurement by parents.
Badgecurrently can’t be focused programmatically or measured. Forwarding the ref is standard for UI primitives.-function Badge({ - className, - variant, - asChild = false, - ...props -}: React.ComponentProps<"span"> & - VariantProps<typeof badgeVariants> & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span"; - - return ( - <Comp - data-slot="badge" - className={cn(badgeVariants({ variant }), className)} - {...props} - /> - ); -} +const Badge = React.forwardRef< + React.ElementRef<"span">, + React.ComponentProps<"span"> & + VariantProps<typeof badgeVariants> & { asChild?: boolean } +>(({ className, variant, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "span"; + return ( + <Comp + ref={ref} + data-slot="badge" + className={cn(badgeVariants({ variant }), className)} + {...props} + /> + ); +}); +Badge.displayName = "Badge";apps/playground/src/app/login/page.tsx (2)
58-63: Handle autofill passkey errors to avoid unhandled promise rejections.Wrap the
signIn.passkey({ autoFill: true })call to prevent noisy errors.- if (window.PublicKeyCredential) { - void signIn.passkey({ autoFill: true }); - } + if (window.PublicKeyCredential) { + signIn.passkey({ autoFill: true }).catch(() => { + /* silently ignore autofill failures */ + }); + }
41-44: Align redirect target to avoid post‑login route race.
useUserredirects authenticated users to “/” whileonSuccesspushes “/dashboard”. Pick one to avoid flicker.-useUser({ - redirectTo: "/", - redirectWhen: "authenticated", -}); +useUser({ + redirectTo: "/dashboard", + redirectWhen: "authenticated", +});apps/playground/src/components/provider-icons.tsx (1)
1-461: Optional consistency: standardize icon className handling.Some icons pass
props.classNamedirectly; others wrap withcn+ defaults. Consider a consistent pattern (e.g., default size viacn).apps/playground/src/components/ai-elements/chain-of-thought.tsx (2)
84-110: Minor: unify Collapsible root usage (optional).You render separate
Collapsibleroots for header and content, synchronized via context. Consider a single shared root to inherit ARIA state automatically.
120-160: Step connector line may overflow last item.The vertical connector extends “bottom-0” for all steps. Optionally remove it for the last item to avoid a trailing line.
Example:
- add
isLast?: booleanand conditionally render the connector.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (10)
apps/playground/public/favicon/android-chrome-192x192.pngis excluded by!**/*.pngapps/playground/public/favicon/android-chrome-512x512.pngis excluded by!**/*.pngapps/playground/public/favicon/apple-touch-icon.pngis excluded by!**/*.pngapps/playground/public/favicon/favicon-16x16.pngis excluded by!**/*.pngapps/playground/public/favicon/favicon-32x32.pngis excluded by!**/*.pngapps/playground/public/favicon/favicon.icois excluded by!**/*.icoapps/playground/public/opengraph.pngis excluded by!**/*.pngapps/playground/src/lib/api/v1.d.tsis excluded by!**/v1.d.tsapps/ui/public/next.svgis excluded by!**/*.svgapps/ui/public/vercel.svgis excluded by!**/*.svg
📒 Files selected for processing (87)
apps/playground/eslint.config.mjs(1 hunks)apps/playground/package.json(1 hunks)apps/playground/postcss.config.mjs(1 hunks)apps/playground/public/favicon/site.webmanifest(1 hunks)apps/playground/src/app/api/chat/route.ts(1 hunks)apps/playground/src/app/layout.tsx(1 hunks)apps/playground/src/app/login/page.tsx(1 hunks)apps/playground/src/app/page.tsx(1 hunks)apps/playground/src/app/signup/page.tsx(1 hunks)apps/playground/src/components/ai-elements/actions.tsx(1 hunks)apps/playground/src/components/ai-elements/artifact.tsx(1 hunks)apps/playground/src/components/ai-elements/branch.tsx(1 hunks)apps/playground/src/components/ai-elements/chain-of-thought.tsx(1 hunks)apps/playground/src/components/ai-elements/code-block.tsx(1 hunks)apps/playground/src/components/ai-elements/context.tsx(1 hunks)apps/playground/src/components/ai-elements/conversation.tsx(1 hunks)apps/playground/src/components/ai-elements/image.tsx(1 hunks)apps/playground/src/components/ai-elements/inline-citation.tsx(1 hunks)apps/playground/src/components/ai-elements/loader.tsx(1 hunks)apps/playground/src/components/ai-elements/message.tsx(1 hunks)apps/playground/src/components/ai-elements/open-in-chat.tsx(1 hunks)apps/playground/src/components/ai-elements/prompt-input.tsx(1 hunks)apps/playground/src/components/ai-elements/reasoning.tsx(1 hunks)apps/playground/src/components/ai-elements/response.tsx(1 hunks)apps/playground/src/components/ai-elements/sources.tsx(1 hunks)apps/playground/src/components/ai-elements/suggestion.tsx(1 hunks)apps/playground/src/components/ai-elements/task.tsx(1 hunks)apps/playground/src/components/ai-elements/tool.tsx(1 hunks)apps/playground/src/components/ai-elements/web-preview.tsx(1 hunks)apps/playground/src/components/credits/credits-display.tsx(1 hunks)apps/playground/src/components/credits/top-up-credits-dialog.tsx(1 hunks)apps/playground/src/components/landing/theme-toggle.tsx(1 hunks)apps/playground/src/components/model-selector.tsx(1 hunks)apps/playground/src/components/playground/api-key-manager.tsx(1 hunks)apps/playground/src/components/playground/auth-dialog.tsx(1 hunks)apps/playground/src/components/playground/chat-header.tsx(1 hunks)apps/playground/src/components/playground/chat-page-client.tsx(1 hunks)apps/playground/src/components/playground/chat-sidebar.tsx(1 hunks)apps/playground/src/components/playground/chat-ui.tsx(1 hunks)apps/playground/src/components/provider-icons.tsx(1 hunks)apps/playground/src/components/providers.tsx(1 hunks)apps/playground/src/components/ui/alert.tsx(1 hunks)apps/playground/src/components/ui/avatar.tsx(1 hunks)apps/playground/src/components/ui/badge.tsx(1 hunks)apps/playground/src/components/ui/button.tsx(1 hunks)apps/playground/src/components/ui/carousel.tsx(1 hunks)apps/playground/src/components/ui/checkbox.tsx(1 hunks)apps/playground/src/components/ui/command.tsx(1 hunks)apps/playground/src/components/ui/dialog.tsx(1 hunks)apps/playground/src/components/ui/dropdown-menu.tsx(1 hunks)apps/playground/src/components/ui/form.tsx(1 hunks)apps/playground/src/components/ui/hover-card.tsx(1 hunks)apps/playground/src/components/ui/input.tsx(1 hunks)apps/playground/src/components/ui/label.tsx(1 hunks)apps/playground/src/components/ui/popover.tsx(1 hunks)apps/playground/src/components/ui/progress.tsx(1 hunks)apps/playground/src/components/ui/providers-icons.tsx(1 hunks)apps/playground/src/components/ui/scroll-area.tsx(1 hunks)apps/playground/src/components/ui/select.tsx(1 hunks)apps/playground/src/components/ui/separator.tsx(1 hunks)apps/playground/src/components/ui/sheet.tsx(1 hunks)apps/playground/src/components/ui/sidebar.tsx(1 hunks)apps/playground/src/components/ui/skeleton.tsx(1 hunks)apps/playground/src/components/ui/sonner.tsx(1 hunks)apps/playground/src/components/ui/tabs.tsx(1 hunks)apps/playground/src/components/ui/textarea.tsx(1 hunks)apps/playground/src/components/ui/tooltip.tsx(1 hunks)apps/playground/src/hooks/use-mobile.ts(1 hunks)apps/playground/src/hooks/useApiKey.ts(1 hunks)apps/playground/src/hooks/useAutoApiKey.ts(1 hunks)apps/playground/src/hooks/useChats.ts(1 hunks)apps/playground/src/hooks/useCreateApiKey.ts(1 hunks)apps/playground/src/hooks/useUser.ts(1 hunks)apps/playground/src/lib/mapmodels.ts(1 hunks)apps/playground/src/lib/model-utils.ts(1 hunks)apps/playground/src/lib/server-api.ts(1 hunks)apps/playground/src/lib/types.ts(1 hunks)apps/playground/src/lib/utils.ts(1 hunks)apps/playground/tsconfig.json(1 hunks)apps/ui/eslint.config.mjs(0 hunks)apps/ui/src/app/layout.tsx(2 hunks)apps/ui/src/app/playground/playground-client.tsx(3 hunks)apps/ui/src/components/Chat.tsx(2 hunks)apps/ui/src/components/app-sidebar.tsx(2 hunks)infra/split.dockerfile(1 hunks)infra/unified.dockerfile(1 hunks)package.json(1 hunks)
💤 Files with no reviewable changes (1)
- apps/ui/eslint.config.mjs
✅ Files skipped from review due to trivial changes (2)
- infra/unified.dockerfile
- apps/playground/tsconfig.json
🚧 Files skipped from review as they are similar to previous changes (34)
- apps/playground/src/components/ui/input.tsx
- apps/playground/src/components/ai-elements/response.tsx
- apps/playground/src/app/page.tsx
- apps/playground/src/components/ai-elements/actions.tsx
- apps/playground/src/components/credits/top-up-credits-dialog.tsx
- apps/playground/src/hooks/use-mobile.ts
- apps/playground/src/app/layout.tsx
- apps/playground/src/components/ui/tabs.tsx
- apps/playground/src/lib/model-utils.ts
- apps/playground/src/components/ai-elements/loader.tsx
- apps/playground/src/lib/mapmodels.ts
- apps/playground/src/components/ui/hover-card.tsx
- apps/playground/src/components/ui/sonner.tsx
- apps/playground/package.json
- apps/playground/postcss.config.mjs
- apps/playground/src/components/ui/dialog.tsx
- apps/playground/src/components/ai-elements/conversation.tsx
- apps/playground/src/components/ai-elements/open-in-chat.tsx
- apps/playground/src/app/api/chat/route.ts
- apps/playground/src/components/ai-elements/artifact.tsx
- apps/playground/src/components/ai-elements/context.tsx
- apps/playground/src/components/ui/alert.tsx
- apps/playground/src/components/playground/chat-header.tsx
- apps/playground/src/components/landing/theme-toggle.tsx
- apps/playground/src/components/ui/sidebar.tsx
- apps/playground/src/components/ai-elements/reasoning.tsx
- apps/playground/src/components/ui/textarea.tsx
- apps/playground/src/components/ai-elements/suggestion.tsx
- apps/playground/src/components/ai-elements/inline-citation.tsx
- apps/playground/src/components/ai-elements/image.tsx
- apps/playground/src/components/ai-elements/web-preview.tsx
- apps/playground/src/components/ui/sheet.tsx
- apps/playground/src/components/ui/providers-icons.tsx
- apps/playground/src/components/ai-elements/prompt-input.tsx
🧰 Additional context used
📓 Path-based instructions (3)
{apps/api,apps/gateway,apps/ui,apps/docs,packages}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Always use top-level import; never use require() or dynamic imports (e.g., import(), next/dynamic)
Files:
apps/ui/src/components/app-sidebar.tsxapps/ui/src/app/playground/playground-client.tsxapps/ui/src/app/layout.tsxapps/ui/src/components/Chat.tsx
apps/ui/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use next/link for links and next/navigation’s router for programmatic navigation
apps/ui/**/*.{ts,tsx}: Use next/link for links and next/navigation's router for programmatic navigation in the UI
Use cookies for user settings not saved in the database to ensure SSR works
Files:
apps/ui/src/components/app-sidebar.tsxapps/ui/src/app/playground/playground-client.tsxapps/ui/src/app/layout.tsxapps/ui/src/components/Chat.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never useanyoras anyin this TypeScript project unless absolutely necessary
Always use top-levelimport; never userequireor dynamic imports
Files:
apps/ui/src/components/app-sidebar.tsxapps/playground/src/components/ui/label.tsxapps/playground/src/components/ui/skeleton.tsxapps/playground/src/components/playground/chat-ui.tsxapps/playground/src/components/ui/progress.tsxapps/playground/src/lib/types.tsapps/ui/src/app/playground/playground-client.tsxapps/playground/src/lib/utils.tsapps/playground/src/components/ui/badge.tsxapps/playground/src/components/ui/button.tsxapps/playground/src/components/ui/select.tsxapps/playground/src/hooks/useCreateApiKey.tsapps/playground/src/components/ui/avatar.tsxapps/playground/src/hooks/useAutoApiKey.tsapps/playground/src/components/ui/tooltip.tsxapps/playground/src/components/ui/command.tsxapps/playground/src/components/ai-elements/code-block.tsxapps/playground/src/components/playground/chat-page-client.tsxapps/playground/src/components/playground/auth-dialog.tsxapps/playground/src/components/ai-elements/message.tsxapps/playground/src/components/ui/checkbox.tsxapps/playground/src/components/ui/separator.tsxapps/playground/src/components/providers.tsxapps/playground/src/components/playground/api-key-manager.tsxapps/playground/src/components/credits/credits-display.tsxapps/ui/src/app/layout.tsxapps/playground/src/lib/server-api.tsapps/playground/src/components/ai-elements/tool.tsxapps/playground/src/components/playground/chat-sidebar.tsxapps/playground/src/components/ui/popover.tsxapps/playground/src/app/login/page.tsxapps/playground/src/app/signup/page.tsxapps/playground/src/components/ai-elements/branch.tsxapps/playground/src/components/ui/dropdown-menu.tsxapps/playground/src/components/ai-elements/sources.tsxapps/ui/src/components/Chat.tsxapps/playground/src/components/model-selector.tsxapps/playground/src/components/provider-icons.tsxapps/playground/src/hooks/useChats.tsapps/playground/src/components/ai-elements/task.tsxapps/playground/src/components/ai-elements/chain-of-thought.tsxapps/playground/src/components/ui/carousel.tsxapps/playground/src/components/ui/scroll-area.tsxapps/playground/src/components/ui/form.tsxapps/playground/src/hooks/useUser.tsapps/playground/src/hooks/useApiKey.ts
🧠 Learnings (3)
📚 Learning: 2025-09-21T20:12:20.016Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-21T20:12:20.016Z
Learning: Applies to **/*.{ts,tsx} : Never use `any` or `as any` in this TypeScript project unless absolutely necessary
Applied to files:
apps/playground/src/components/playground/chat-ui.tsx
📚 Learning: 2025-09-15T13:16:05.365Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: AGENTS.md:0-0
Timestamp: 2025-09-15T13:16:05.365Z
Learning: Applies to {apps/api,apps/gateway,apps/ui,apps/docs,packages}/**/*.{ts,tsx} : Always use top-level import; never use require() or dynamic imports (e.g., import(), next/dynamic)
Applied to files:
apps/playground/src/components/providers.tsx
📚 Learning: 2025-09-21T20:12:20.016Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-21T20:12:20.016Z
Learning: Applies to **/*.{ts,tsx} : Always use top-level `import`; never use `require` or dynamic imports
Applied to files:
apps/playground/src/components/providers.tsx
🧬 Code graph analysis (33)
apps/playground/src/components/ui/label.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/playground/chat-ui.tsx (6)
packages/db/src/schema.ts (1)
message(498-518)apps/playground/src/components/ai-elements/conversation.tsx (3)
Conversation(14-22)ConversationContent(28-33)ConversationEmptyState(41-68)apps/playground/src/components/ai-elements/suggestion.tsx (2)
Suggestions(11-22)Suggestion(29-54)apps/playground/src/components/ai-elements/response.tsx (1)
Response(10-21)apps/playground/src/components/ai-elements/actions.tsx (2)
Actions(16-20)Action(27-66)apps/playground/src/components/ai-elements/prompt-input.tsx (11)
PromptInput(221-447)PromptInputBody(451-456)PromptInputTextarea(460-505)PromptInputToolbar(509-517)PromptInputTools(521-533)PromptInputActionMenu(563-565)PromptInputActionMenuTrigger(570-580)PromptInputActionMenuContent(585-590)PromptInputActionAddAttachments(174-191)PromptInputButton(537-560)PromptInputSubmit(609-638)
apps/playground/src/lib/utils.ts (2)
apps/docs/lib/cn.ts (1)
twMerge(1-1)apps/ui/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ui/badge.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ui/button.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ui/select.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/hooks/useCreateApiKey.ts (1)
apps/api/src/posthog.ts (1)
posthog(3-6)
apps/playground/src/components/ui/avatar.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/hooks/useAutoApiKey.ts (1)
apps/playground/src/hooks/useDefaultProject.ts (1)
useDefaultProject(3-36)
apps/playground/src/components/ui/command.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/code-block.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/playground/chat-page-client.tsx (7)
packages/models/src/models.ts (1)
ModelDefinition(108-161)packages/models/src/providers.ts (2)
ProviderDefinition(1-17)providers(19-247)apps/playground/src/lib/mapmodels.ts (1)
mapModels(4-27)apps/playground/src/lib/types.ts (1)
ComboboxModel(39-50)apps/playground/src/hooks/useChats.ts (1)
useDataChat(31-46)apps/playground/src/components/playground/chat-ui.tsx (1)
ChatUI(71-251)apps/playground/src/components/playground/api-key-manager.tsx (1)
ApiKeyManager(129-417)
apps/playground/src/components/playground/auth-dialog.tsx (1)
apps/playground/src/components/ui/button.tsx (1)
Button(58-58)
apps/playground/src/components/ai-elements/message.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ui/checkbox.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ui/separator.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/providers.tsx (2)
apps/playground/src/lib/config.tsx (1)
AppConfigProvider(14-23)apps/docs/lib/providers.tsx (1)
PostHogProvider(11-36)
apps/playground/src/components/playground/api-key-manager.tsx (3)
apps/playground/src/hooks/useApiKey.ts (1)
useApiKey(9-63)apps/playground/src/lib/config.tsx (1)
useAppConfig(25-31)apps/playground/src/hooks/useAutoApiKey.ts (1)
useAutoApiKey(13-77)
apps/playground/src/components/credits/credits-display.tsx (2)
apps/ui/src/lib/types.ts (1)
Organization(1-14)apps/playground/src/components/credits/top-up-credits-dialog.tsx (1)
TopUpCreditsDialog(46-161)
apps/playground/src/components/ai-elements/tool.tsx (2)
apps/playground/src/lib/utils.ts (1)
cn(4-6)apps/playground/src/components/ai-elements/code-block.tsx (1)
CodeBlock(31-106)
apps/playground/src/components/playground/chat-sidebar.tsx (6)
apps/api/src/posthog.ts (1)
posthog(3-6)apps/playground/src/hooks/useUser.ts (1)
useUser(24-105)apps/playground/src/lib/auth-client.ts (1)
useAuth(20-33)apps/playground/src/hooks/useOrganization.ts (1)
useOrganization(10-30)apps/playground/src/hooks/useChats.ts (4)
useChats(25-29)useDeleteChat(90-109)useUpdateChat(69-88)Chat(6-14)apps/playground/src/components/credits/credits-display.tsx (1)
CreditsDisplay(19-79)
apps/playground/src/components/ui/popover.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/app/signup/page.tsx (1)
apps/api/src/posthog.ts (1)
posthog(3-6)
apps/playground/src/components/ai-elements/branch.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ui/dropdown-menu.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/sources.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/model-selector.tsx (5)
packages/models/src/models.ts (1)
ModelDefinition(108-161)packages/models/src/providers.ts (2)
ProviderDefinition(1-17)providers(19-247)apps/playground/src/lib/model-utils.ts (4)
getProviderForModel(29-35)getModelCapabilities(37-58)formatPrice(3-14)formatContextSize(16-27)apps/playground/src/components/provider-icons.tsx (1)
getProviderIcon(467-480)apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/provider-icons.tsx (2)
apps/playground/src/components/ui/providers-icons.tsx (19)
AnthropicIcon(6-30)CloudriftIcon(33-48)DeepseekIcon(51-66)GoogleStudioAIIcon(69-119)GroqIcon(122-137)InferenceNetIcon(140-164)MistralIcon(167-205)OpenAIIcon(208-229)PerplexityIcon(232-249)TogetherAIIcon(252-269)XAIIcon(272-285)MoonshotIcon(288-301)NovitaIcon(304-319)AlibabaIcon(322-334)NebiusIcon(336-348)ZaiIcon(351-364)ProviderIcons(367-384)ProviderIconKey(386-386)getProviderIcon(403-419)apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/hooks/useChats.ts (1)
apps/playground/src/lib/fetch-client.ts (1)
useApi(22-28)
apps/playground/src/components/ai-elements/task.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/chain-of-thought.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/hooks/useUser.ts (2)
apps/api/src/posthog.ts (1)
posthog(3-6)apps/playground/src/lib/fetch-client.ts (1)
useApi(22-28)
apps/playground/src/hooks/useApiKey.ts (1)
apps/ui/src/hooks/useApiKey.ts (1)
useApiKey(6-60)
🪛 ast-grep (0.39.5)
apps/playground/src/hooks/useApiKey.ts
[warning] 39-39: Sensitive information detected in localStorage. Storing sensitive data like emails, usernames, or personal information in localStorage exposes it to malicious scripts and XSS attacks. Use secure storage alternatives or avoid storing sensitive data client-side.
Context: localStorage.setItem(API_KEY_STORAGE_KEY, key)
Note: [CWE-312] Cleartext Storage of Sensitive Information [REFERENCES]
- https://cwe.mitre.org/data/definitions/312.html
- https://owasp.org/www-community/vulnerabilities/HTML5_Local_Storage_Security
(local-storage-sensitive-information)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: test-split
- GitHub Check: build-unified (linux/amd64)
| "name": "", | ||
| "short_name": "", |
There was a problem hiding this comment.
Fill in the manifest name fields
Leaving name and short_name blank makes install surfaces and OS-level shortcuts render without a label, which is a major UX regression for the playground. Please populate them with the product name.
- "name": "",
- "short_name": "",
+ "name": "LLM Gateway Playground",
+ "short_name": "Playground",📝 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.
| "name": "", | |
| "short_name": "", | |
| - "name": "", | |
| "name": "LLM Gateway Playground", | |
| "short_name": "Playground", |
🤖 Prompt for AI Agents
In apps/playground/public/favicon/site.webmanifest around lines 2 to 3, the
"name" and "short_name" fields are empty; populate them with the product name
(e.g., set "name": "Playground" and "short_name": "Playground") so install
surfaces and OS-level shortcuts render a proper label; if you support
localization or a longer display name, use the full product name for "name" and
a concise label for "short_name".
| "src": "/android-chrome-192x192.png", | ||
| "sizes": "192x192", | ||
| "type": "image/png" | ||
| }, | ||
| { | ||
| "src": "/android-chrome-512x512.png", | ||
| "sizes": "512x512", | ||
| "type": "image/png" |
There was a problem hiding this comment.
Point icon URLs at their actual location
Because this manifest lives under /favicon/site.webmanifest, the icon files are served under /favicon/…. Pointing at /android-chrome-*.png will 404 and the install flow loses its icons. Please reference the correct paths.
- "src": "/android-chrome-192x192.png",
+ "src": "/favicon/android-chrome-192x192.png",
@@
- "src": "/android-chrome-512x512.png",
+ "src": "/favicon/android-chrome-512x512.png",📝 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.
| "src": "/android-chrome-192x192.png", | |
| "sizes": "192x192", | |
| "type": "image/png" | |
| }, | |
| { | |
| "src": "/android-chrome-512x512.png", | |
| "sizes": "512x512", | |
| "type": "image/png" | |
| "src": "/favicon/android-chrome-192x192.png", | |
| "sizes": "192x192", | |
| "type": "image/png" | |
| }, | |
| { | |
| "src": "/favicon/android-chrome-512x512.png", | |
| "sizes": "512x512", | |
| "type": "image/png" |
🤖 Prompt for AI Agents
In apps/playground/public/favicon/site.webmanifest around lines 6 to 13, the
manifest's icon "src" values wrongly point to the site root (e.g.
"/android-chrome-192x192.png") but the file is served from the favicon
directory, causing 404s; update each "src" to the correct relative path under
/favicon (e.g. "/favicon/android-chrome-192x192.png" and
"/favicon/android-chrome-512x512.png") so the install flow can find the icons.
| if (error) { | ||
| toast.error(error.message || "An unknown error occurred", { | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Avoid double error toasts on email sign‑in.
You toast in onError and again after awaiting signIn.email. Users will see two errors.
Apply this diff to remove the duplicate toast:
- if (error) {
- toast.error(error.message || "An unknown error occurred", {
- style: {
- backgroundColor: "var(--destructive)",
- color: "var(--destructive-foreground)",
- },
- });
- }📝 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.
| if (error) { | |
| toast.error(error.message || "An unknown error occurred", { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| } |
🤖 Prompt for AI Agents
In apps/playground/src/app/login/page.tsx around lines 97 to 104, the code shows
the same error toast both in the onError handler and again after awaiting
signIn.email which results in duplicate toasts; remove the redundant toast by
deleting the post-signIn toast (or the onError toast) so only one toast is
shown—preferably keep the existing onError toast and remove the duplicate toast
block after await signIn.email, ensuring no other logic depends on that removed
branch.
| async function handlePasskeySignIn() { | ||
| setIsLoading(true); | ||
| try { | ||
| const res = await signIn.passkey(); | ||
| if (res?.error) { | ||
| toast.error(res.error.message || "Failed to sign in with passkey", { | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); | ||
| return; | ||
| } | ||
| posthog.capture("user_logged_in", { method: "passkey" }); | ||
| toast.success("Login successful"); | ||
| router.push("/dashboard"); | ||
| } catch (error: unknown) { | ||
| toast.error( | ||
| (error as Error)?.message || "Failed to sign in with passkey", | ||
| { | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }, | ||
| ); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Clear React Query cache after successful passkey sign‑in.
You clear cache on email login but not on passkey, which may leave stale data.
const res = await signIn.passkey();
if (res?.error) {
toast.error(res.error.message || "Failed to sign in with passkey", {
style: {
backgroundColor: "var(--destructive)",
color: "var(--destructive-foreground)",
},
});
return;
}
+ queryClient.clear();
posthog.capture("user_logged_in", { method: "passkey" });
toast.success("Login successful");
router.push("/dashboard");📝 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.
| async function handlePasskeySignIn() { | |
| setIsLoading(true); | |
| try { | |
| const res = await signIn.passkey(); | |
| if (res?.error) { | |
| toast.error(res.error.message || "Failed to sign in with passkey", { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| return; | |
| } | |
| posthog.capture("user_logged_in", { method: "passkey" }); | |
| toast.success("Login successful"); | |
| router.push("/dashboard"); | |
| } catch (error: unknown) { | |
| toast.error( | |
| (error as Error)?.message || "Failed to sign in with passkey", | |
| { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }, | |
| ); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| async function handlePasskeySignIn() { | |
| setIsLoading(true); | |
| try { | |
| const res = await signIn.passkey(); | |
| if (res?.error) { | |
| toast.error(res.error.message || "Failed to sign in with passkey", { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| return; | |
| } | |
| queryClient.clear(); | |
| posthog.capture("user_logged_in", { method: "passkey" }); | |
| toast.success("Login successful"); | |
| router.push("/dashboard"); | |
| } catch (error: unknown) { | |
| toast.error( | |
| (error as Error)?.message || "Failed to sign in with passkey", | |
| { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }, | |
| ); | |
| } finally { | |
| setIsLoading(false); | |
| } |
🤖 Prompt for AI Agents
In apps/playground/src/app/login/page.tsx around lines 109 to 137, the passkey
sign-in branch doesn't clear the React Query cache like the email sign-in does,
which can leave stale data; after a successful sign-in (before pushing to
/dashboard) obtain the React Query client (e.g., via useQueryClient or the app's
existing queryClient instance) and call queryClient.clear() or
queryClient.invalidateQueries()/resetQueries() to clear or refresh cached data,
then proceed to posthog.capture, show the success toast and router.push; ensure
you import/use the same QueryClient API used elsewhere in the file so cache
clearing is consistent and keep the existing try/catch/finally flow so
setIsLoading(false) still runs in finally.
| const { error } = await signUp.email( | ||
| { | ||
| name: values.name, | ||
| email: values.email, | ||
| password: values.password, | ||
| }, | ||
| { | ||
| onSuccess: (ctx) => { | ||
| queryClient.clear(); | ||
| posthog.identify(ctx.data.user.id, { | ||
| email: ctx.data.user.email, | ||
| name: ctx.data.user.name, | ||
| }); | ||
| posthog.capture("user_signed_up", { | ||
| email: values.email, | ||
| name: values.name, | ||
| }); | ||
| toast.success("Account created", { | ||
| description: | ||
| "Please check your email to verify your account before signing in.", | ||
| }); | ||
| router.push("/onboarding"); | ||
| }, | ||
| onError: (ctx) => { | ||
| toast.error(ctx.error.message || "Failed to sign up", { | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| if (error) { | ||
| toast.error(error.message || "Failed to sign up", { | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Avoid double failure toasts in the signup flow.
signUp.email already triggers the toast inside onError, and the fallback if (error) block fires a second identical toast. Users currently see duplicates on every failure. Drop the outer toast (or guard against duplicate display) to keep the UX clean.
- if (error) {
- toast.error(error.message || "Failed to sign up", {
- style: {
- backgroundColor: "var(--destructive)",
- color: "var(--destructive-foreground)",
- },
- });
- }
+ if (error) {
+ return;
+ }📝 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.
| const { error } = await signUp.email( | |
| { | |
| name: values.name, | |
| email: values.email, | |
| password: values.password, | |
| }, | |
| { | |
| onSuccess: (ctx) => { | |
| queryClient.clear(); | |
| posthog.identify(ctx.data.user.id, { | |
| email: ctx.data.user.email, | |
| name: ctx.data.user.name, | |
| }); | |
| posthog.capture("user_signed_up", { | |
| email: values.email, | |
| name: values.name, | |
| }); | |
| toast.success("Account created", { | |
| description: | |
| "Please check your email to verify your account before signing in.", | |
| }); | |
| router.push("/onboarding"); | |
| }, | |
| onError: (ctx) => { | |
| toast.error(ctx.error.message || "Failed to sign up", { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| }, | |
| }, | |
| ); | |
| if (error) { | |
| toast.error(error.message || "Failed to sign up", { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| } | |
| const { error } = await signUp.email( | |
| { | |
| name: values.name, | |
| email: values.email, | |
| password: values.password, | |
| }, | |
| { | |
| onSuccess: (ctx) => { | |
| queryClient.clear(); | |
| posthog.identify(ctx.data.user.id, { | |
| email: ctx.data.user.email, | |
| name: ctx.data.user.name, | |
| }); | |
| posthog.capture("user_signed_up", { | |
| email: values.email, | |
| name: values.name, | |
| }); | |
| toast.success("Account created", { | |
| description: | |
| "Please check your email to verify your account before signing in.", | |
| }); | |
| router.push("/onboarding"); | |
| }, | |
| onError: (ctx) => { | |
| toast.error(ctx.error.message || "Failed to sign up", { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| }, | |
| }, | |
| ); | |
| if (error) { | |
| - toast.error(error.message || "Failed to sign up", { | |
| - style: { | |
| - backgroundColor: "var(--destructive)", | |
| - color: "var(--destructive-foreground)", | |
| - }, | |
| return; | |
| } |
🤖 Prompt for AI Agents
In apps/playground/src/app/signup/page.tsx around lines 64 to 105, the signup
failure toast is shown twice because signUp.email already displays a toast in
its onError handler and the subsequent if (error) block re-displays the same
error; remove the outer if (error) toast block (or alternatively add a guard so
only one toast is shown) so failures only produce a single user-visible toast
and avoid duplicate messages.
| return api.useMutation("patch", "/chats/{id}", { | ||
| onSuccess: () => { | ||
| const queryKey = api.queryOptions("get", "/chats").queryKey; | ||
| queryClient.invalidateQueries({ queryKey }); | ||
| toast("Chat updated successfully"); | ||
| }, | ||
| onError: (error: { message: string }) => { | ||
| toast(error.message, { | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); | ||
| }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Also invalidate the chat detail query after updates.
Right now we only invalidate the list query. When a chat is renamed or otherwise updated, any active /chats/{id} query keeps serving stale data until the page is refreshed, so the conversation header never reflects the change. Please invalidate the detail query alongside the list (you can derive the id from the mutation variables) so both list and detail stay consistent.
- return api.useMutation("patch", "/chats/{id}", {
- onSuccess: () => {
- const queryKey = api.queryOptions("get", "/chats").queryKey;
- queryClient.invalidateQueries({ queryKey });
+ return api.useMutation("patch", "/chats/{id}", {
+ onSuccess: (_data, variables) => {
+ const listKey = api.queryOptions("get", "/chats").queryKey;
+ queryClient.invalidateQueries({ queryKey: listKey });
+
+ const chatId = variables?.params?.path?.id;
+ if (chatId) {
+ const detailKey = api
+ .queryOptions("get", "/chats/{id}", {
+ params: { path: { id: chatId } },
+ })
+ .queryKey;
+ queryClient.invalidateQueries({ queryKey: detailKey });
+ }
toast("Chat updated successfully");📝 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.
| return api.useMutation("patch", "/chats/{id}", { | |
| onSuccess: () => { | |
| const queryKey = api.queryOptions("get", "/chats").queryKey; | |
| queryClient.invalidateQueries({ queryKey }); | |
| toast("Chat updated successfully"); | |
| }, | |
| onError: (error: { message: string }) => { | |
| toast(error.message, { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| }, | |
| }); | |
| } | |
| return api.useMutation("patch", "/chats/{id}", { | |
| onSuccess: (_data, variables) => { | |
| const listKey = api.queryOptions("get", "/chats").queryKey; | |
| queryClient.invalidateQueries({ queryKey: listKey }); | |
| const chatId = variables?.params?.path?.id; | |
| if (chatId) { | |
| const detailKey = api | |
| .queryOptions("get", "/chats/{id}", { | |
| params: { path: { id: chatId } }, | |
| }) | |
| .queryKey; | |
| queryClient.invalidateQueries({ queryKey: detailKey }); | |
| } | |
| toast("Chat updated successfully"); | |
| }, | |
| onError: (error: { message: string }) => { | |
| toast(error.message, { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| }, | |
| }); | |
| } |
🤖 Prompt for AI Agents
In apps/playground/src/hooks/useChats.ts around lines 73 to 88, the mutation
only invalidates the chats list causing any active /chats/{id} detail query to
remain stale after updates; modify the onSuccess handler to read the updated
chat id from the mutation variables (or from the response if preferred) and call
queryClient.invalidateQueries for the detail query key (e.g.,
api.queryOptions("get", "/chats/{id}", { id }).queryKey) in addition to
invalidating the list, so both list and detail are refreshed after a patch.
| return api.useMutation("delete", "/chats/{id}", { | ||
| onSuccess: () => { | ||
| const queryKey = api.queryOptions("get", "/chats").queryKey; | ||
| queryClient.invalidateQueries({ queryKey }); | ||
| toast("Chat deleted successfully"); | ||
| }, | ||
| onError: (error: { message: string }) => { | ||
| toast(error.message, { | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Handle deletion by cleaning up the chat detail cache too.
After deleting a chat we leave any cached /chats/{id} query behind; reopening that chat id will briefly render a ghost record until revalidate. Please remove or invalidate the detail query with the deleted id so the cache reflects the removal immediately.
- return api.useMutation("delete", "/chats/{id}", {
- onSuccess: () => {
- const queryKey = api.queryOptions("get", "/chats").queryKey;
- queryClient.invalidateQueries({ queryKey });
+ return api.useMutation("delete", "/chats/{id}", {
+ onSuccess: (_data, variables) => {
+ const listKey = api.queryOptions("get", "/chats").queryKey;
+ queryClient.invalidateQueries({ queryKey: listKey });
+
+ const chatId = variables?.params?.path?.id;
+ if (chatId) {
+ const detailKey = api
+ .queryOptions("get", "/chats/{id}", {
+ params: { path: { id: chatId } },
+ })
+ .queryKey;
+ queryClient.removeQueries({ queryKey: detailKey });
+ }
toast("Chat deleted successfully");📝 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.
| return api.useMutation("delete", "/chats/{id}", { | |
| onSuccess: () => { | |
| const queryKey = api.queryOptions("get", "/chats").queryKey; | |
| queryClient.invalidateQueries({ queryKey }); | |
| toast("Chat deleted successfully"); | |
| }, | |
| onError: (error: { message: string }) => { | |
| toast(error.message, { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| }, | |
| }); | |
| return api.useMutation("delete", "/chats/{id}", { | |
| onSuccess: (_data, variables) => { | |
| const listKey = api.queryOptions("get", "/chats").queryKey; | |
| queryClient.invalidateQueries({ queryKey: listKey }); | |
| const chatId = variables?.params?.path?.id; | |
| if (chatId) { | |
| const detailKey = api | |
| .queryOptions("get", "/chats/{id}", { | |
| params: { path: { id: chatId } }, | |
| }) | |
| .queryKey; | |
| queryClient.removeQueries({ queryKey: detailKey }); | |
| } | |
| toast("Chat deleted successfully"); | |
| }, | |
| onError: (error: { message: string }) => { | |
| toast(error.message, { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In apps/playground/src/hooks/useChats.ts around lines 94 to 108, the delete
mutation only invalidates the list query but leaves any cached /chats/{id}
detail behind; update the onSuccess handler to also remove/invalidate the detail
cache for the deleted chat id: obtain the id (from the mutation variables or
response), build the corresponding queryKey for the "get" "/chats/{id}" route
for that id, and call queryClient.invalidateQueries (or removeQueries) for that
key so the per-chat cache is cleared immediately; keep the existing list
invalidation and toast behavior.
| return api.useMutation("post", "/chats/{id}/messages", { | ||
| onSuccess: () => { | ||
| const queryKey = api.queryOptions("get", "/chats").queryKey; | ||
| queryClient.invalidateQueries({ queryKey }); | ||
| }, | ||
| onError: (error: { message: string }) => { | ||
| toast(error.message, { | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Invalidate the active chat detail after adding a message.
When a message is appended we only invalidate the chat list. Any open conversation using /chats/{id} continues to show the old message list until a hard reload, which breaks the live chat experience. Please invalidate the matching detail query on success.
- return api.useMutation("post", "/chats/{id}/messages", {
- onSuccess: () => {
- const queryKey = api.queryOptions("get", "/chats").queryKey;
- queryClient.invalidateQueries({ queryKey });
+ return api.useMutation("post", "/chats/{id}/messages", {
+ onSuccess: (_data, variables) => {
+ const listKey = api.queryOptions("get", "/chats").queryKey;
+ queryClient.invalidateQueries({ queryKey: listKey });
+
+ const chatId = variables?.params?.path?.id;
+ if (chatId) {
+ const detailKey = api
+ .queryOptions("get", "/chats/{id}", {
+ params: { path: { id: chatId } },
+ })
+ .queryKey;
+ queryClient.invalidateQueries({ queryKey: detailKey });
+ }📝 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.
| return api.useMutation("post", "/chats/{id}/messages", { | |
| onSuccess: () => { | |
| const queryKey = api.queryOptions("get", "/chats").queryKey; | |
| queryClient.invalidateQueries({ queryKey }); | |
| }, | |
| onError: (error: { message: string }) => { | |
| toast(error.message, { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| }, | |
| }); | |
| return api.useMutation("post", "/chats/{id}/messages", { | |
| onSuccess: (_data, variables) => { | |
| const listKey = api.queryOptions("get", "/chats").queryKey; | |
| queryClient.invalidateQueries({ queryKey: listKey }); | |
| const chatId = variables?.params?.path?.id; | |
| if (chatId) { | |
| const detailKey = api | |
| .queryOptions("get", "/chats/{id}", { | |
| params: { path: { id: chatId } }, | |
| }) | |
| .queryKey; | |
| queryClient.invalidateQueries({ queryKey: detailKey }); | |
| } | |
| }, | |
| onError: (error: { message: string }) => { | |
| toast(error.message, { | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In apps/playground/src/hooks/useChats.ts around lines 115-128, the success
handler only invalidates the chats list so an open chat detail doesn’t refresh;
update the onSuccess to also invalidate the specific chat detail by extracting
the chat id from the mutation variables (e.g., onSuccess: (_data, variables) =>
{ const id = variables.id; const listKey = api.queryOptions("get",
"/chats").queryKey; const detailKey = api.queryOptions("get",
`/chats/${id}`).queryKey; queryClient.invalidateQueries({ queryKey: listKey });
queryClient.invalidateQueries({ queryKey: detailKey }); }) so both the list and
the active chat detail refresh after posting a message.
| import { cookies } from "next/headers"; | ||
| import createFetchClient from "openapi-fetch"; | ||
| import { toast } from "sonner"; | ||
|
|
There was a problem hiding this comment.
Remove client-only toast usage from server module
This module is strictly server-side (next/headers cookies access), so importing sonner (a "use client" package) forces the file into the client bundle and breaks the build. Even if it compiled, toast.error can’t execute in a server runtime. Replace the toast with server-safe logging or error propagation.
-import { toast } from "sonner";
...
- } catch {
- toast.error(`Server API error for ${method} ${path}`);
- return null;
- }
+ } catch (error) {
+ console.error(`Server API error for ${method} ${String(path)}`, error);
+ return null;
+ }📝 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.
| import { cookies } from "next/headers"; | |
| import createFetchClient from "openapi-fetch"; | |
| import { toast } from "sonner"; | |
| // apps/playground/src/lib/server-api.ts | |
| import { cookies } from "next/headers"; | |
| import createFetchClient from "openapi-fetch"; | |
| export async function fetchServerData<T>( | |
| method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", | |
| path: keyof paths, | |
| options?: unknown, | |
| ): Promise<T | null> { | |
| try { | |
| const client = await createServerApiClient(); | |
| // …existing request logic… | |
| } catch (error) { | |
| console.error( | |
| `Server API error for ${method} ${String(path)}`, | |
| error | |
| ); | |
| return null; | |
| } | |
| } |
🤖 Prompt for AI Agents
In apps/playground/src/lib/server-api.ts around lines 1 to 4, the file imports
and uses the client-only sonner toast which forces this server module into the
client bundle and cannot run in a server context; remove the `toast` import and
any `toast.*` calls, replace them with server-safe behavior such as throwing the
error or logging via console.error or the project's server logger, and ensure
the function surfaces errors to the caller (or returns an error result) instead
of attempting to show a client toast.
| toast.error("Chat Error"); | ||
|
|
||
| // Show user-friendly error message | ||
| const errorMessage = | ||
| error instanceof Error ? error.message : "An unexpected error occurred"; | ||
| toast({ | ||
| title: "Chat Error", | ||
| toast.error("Chat Error", { | ||
| description: errorMessage, | ||
| variant: "destructive", | ||
| style: { | ||
| backgroundColor: "var(--destructive)", | ||
| color: "var(--destructive-foreground)", | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Avoid duplicate error toasts in the catch block.
Calling toast.error twice (Lines 71 and 77) shows two identical notifications for a single failure, which is a noticeable UX regression. Drop the first call and keep the descriptive one.
- toast.error("Chat Error");
-
- // Show user-friendly error message
- const errorMessage =
- error instanceof Error ? error.message : "An unexpected error occurred";
- toast.error("Chat Error", {
+ const errorMessage =
+ error instanceof Error ? error.message : "An unexpected error occurred";
+ toast.error("Chat Error", {📝 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.
| toast.error("Chat Error"); | |
| // Show user-friendly error message | |
| const errorMessage = | |
| error instanceof Error ? error.message : "An unexpected error occurred"; | |
| toast({ | |
| title: "Chat Error", | |
| toast.error("Chat Error", { | |
| description: errorMessage, | |
| variant: "destructive", | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); | |
| const errorMessage = | |
| error instanceof Error ? error.message : "An unexpected error occurred"; | |
| toast.error("Chat Error", { | |
| description: errorMessage, | |
| style: { | |
| backgroundColor: "var(--destructive)", | |
| color: "var(--destructive-foreground)", | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In apps/ui/src/components/Chat.tsx around lines 71 to 82, the catch block is
calling toast.error twice which produces duplicate notifications; remove the
first plain toast.error("Chat Error") call and keep the single descriptive
toast.error invocation that includes the errorMessage and styling so only one
user-friendly toast is shown.
af412eb to
d420814
Compare
left a comment
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
infra/docker-compose.unified.local.yml (1)
23-28: Add PLAYGROUND_URL to the local unified compose environmentThe unified local stack never injects
PLAYGROUND_URL, so any code that readsprocess.env.PLAYGROUND_URLends up withundefinedwhen runningdocker compose -f infra/docker-compose.unified.local.yml …. The production compose already sets it (and.env.unified.exampledocuments it), so we should mirror that here to keep the environments consistent.- UI_URL=${UI_URL:-http://localhost:3002} + - PLAYGROUND_URL=${PLAYGROUND_URL:-http://localhost:3003} - API_URL=${API_URL:-http://localhost:4002}
🧹 Nitpick comments (4)
apps/playground/src/lib/mapmodels.ts (1)
9-24: Build a provider lookup to avoid repeated linear scansWe repeatedly call
providers.findinside the inner loop, which turns this mapper into O(models × providers). Pre-computing a map keeps it linear and scales better as the catalog grows.export function mapModels( models: readonly ModelDefinition[], providers: readonly ProviderDefinition[], ): ComboboxModel[] { - const entries: ComboboxModel[] = []; + const providerMap = new Map(providers.map((provider) => [provider.id, provider])); + const entries: ComboboxModel[] = []; for (const m of models) { for (const p of m.providers) { - const providerInfo = providers.find((pr) => pr.id === p.providerId); + const providerInfo = providerMap.get(p.providerId); entries.push({ id: `${p.providerId}/${p.modelName}`, name: m.name ?? m.id,apps/playground/src/components/playground/auth-dialog.tsx (1)
12-25: Add dialog semantics for accessibility.
Withoutrole="dialog"/aria-modal(and labelled/described refs), assistive tech treats this overlay as plain content, making it harder to understand that interaction is constrained to the modal. Please add the relevant ARIA hooks so screen readers announce this as a dialog.- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> - <div className="w-[420px] rounded-md border bg-background p-4 shadow-md"> - <div className="text-sm font-medium mb-2">Sign in required</div> - <p className="text-sm text-muted-foreground mb-3"> + <div + role="dialog" + aria-modal="true" + aria-labelledby="auth-dialog-title" + aria-describedby="auth-dialog-description" + className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" + > + <div className="w-[420px] rounded-md border bg-background p-4 shadow-md"> + <div id="auth-dialog-title" className="text-sm font-medium mb-2"> + Sign in required + </div> + <p + id="auth-dialog-description" + className="text-sm text-muted-foreground mb-3" + > Please sign in to use the playground and manage your API keys. </p>apps/playground/src/lib/utils.ts (1)
1-6: Consider sharing thecnhelper.
Line 4: We already ship the samecnimplementation inapps/ui. Pulling it from a shared util (e.g.,packages/sharedor re-exporting from the UI lib) would keep future tweaks in one place.apps/playground/src/app/page.tsx (1)
9-13: Drop the unusedGatewayModelexport.The interface isn’t referenced in this module (or elsewhere from what I can see), so it’s just dead code. Let’s remove it to avoid exporting unused types.
Apply this diff to delete the unused interface:
-export interface GatewayModel { - id: string; - name?: string; - architecture?: { input_modalities?: string[] }; -}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (10)
apps/playground/public/favicon/android-chrome-192x192.pngis excluded by!**/*.pngapps/playground/public/favicon/android-chrome-512x512.pngis excluded by!**/*.pngapps/playground/public/favicon/apple-touch-icon.pngis excluded by!**/*.pngapps/playground/public/favicon/favicon-16x16.pngis excluded by!**/*.pngapps/playground/public/favicon/favicon-32x32.pngis excluded by!**/*.pngapps/playground/public/favicon/favicon.icois excluded by!**/*.icoapps/playground/public/opengraph.pngis excluded by!**/*.pngapps/playground/src/lib/api/v1.d.tsis excluded by!**/v1.d.tsapps/ui/public/next.svgis excluded by!**/*.svgapps/ui/public/vercel.svgis excluded by!**/*.svg
📒 Files selected for processing (107)
.env.example(2 hunks).env.unified.example(1 hunks).github/start.sh(1 hunks).github/test-split-docker.sh(5 hunks).github/test-unified-docker.sh(4 hunks).github/workflows/images.yml(3 hunks)AGENTS.md(2 hunks)CLAUDE.md(2 hunks)apps/api/package.json(1 hunks)apps/docs/content/self-host.mdx(1 hunks)apps/gateway/package.json(1 hunks)apps/playground/eslint.config.mjs(1 hunks)apps/playground/next.config.ts(1 hunks)apps/playground/package.json(1 hunks)apps/playground/postcss.config.mjs(1 hunks)apps/playground/public/favicon/site.webmanifest(1 hunks)apps/playground/src/app/api/chat/route.ts(1 hunks)apps/playground/src/app/layout.tsx(1 hunks)apps/playground/src/app/login/page.tsx(1 hunks)apps/playground/src/app/page.tsx(1 hunks)apps/playground/src/app/signup/page.tsx(1 hunks)apps/playground/src/components/ai-elements/actions.tsx(1 hunks)apps/playground/src/components/ai-elements/artifact.tsx(1 hunks)apps/playground/src/components/ai-elements/branch.tsx(1 hunks)apps/playground/src/components/ai-elements/chain-of-thought.tsx(1 hunks)apps/playground/src/components/ai-elements/code-block.tsx(1 hunks)apps/playground/src/components/ai-elements/context.tsx(1 hunks)apps/playground/src/components/ai-elements/conversation.tsx(1 hunks)apps/playground/src/components/ai-elements/image.tsx(1 hunks)apps/playground/src/components/ai-elements/inline-citation.tsx(1 hunks)apps/playground/src/components/ai-elements/loader.tsx(1 hunks)apps/playground/src/components/ai-elements/message.tsx(1 hunks)apps/playground/src/components/ai-elements/open-in-chat.tsx(1 hunks)apps/playground/src/components/ai-elements/prompt-input.tsx(1 hunks)apps/playground/src/components/ai-elements/reasoning.tsx(1 hunks)apps/playground/src/components/ai-elements/response.tsx(1 hunks)apps/playground/src/components/ai-elements/sources.tsx(1 hunks)apps/playground/src/components/ai-elements/suggestion.tsx(1 hunks)apps/playground/src/components/ai-elements/task.tsx(1 hunks)apps/playground/src/components/ai-elements/tool.tsx(1 hunks)apps/playground/src/components/ai-elements/web-preview.tsx(1 hunks)apps/playground/src/components/credits/credits-display.tsx(1 hunks)apps/playground/src/components/credits/top-up-credits-dialog.tsx(1 hunks)apps/playground/src/components/landing/theme-toggle.tsx(1 hunks)apps/playground/src/components/model-selector.tsx(1 hunks)apps/playground/src/components/playground/api-key-manager.tsx(1 hunks)apps/playground/src/components/playground/auth-dialog.tsx(1 hunks)apps/playground/src/components/playground/chat-header.tsx(1 hunks)apps/playground/src/components/playground/chat-page-client.tsx(1 hunks)apps/playground/src/components/playground/chat-sidebar.tsx(1 hunks)apps/playground/src/components/playground/chat-ui.tsx(1 hunks)apps/playground/src/components/provider-icons.tsx(1 hunks)apps/playground/src/components/providers.tsx(1 hunks)apps/playground/src/components/ui/alert.tsx(1 hunks)apps/playground/src/components/ui/avatar.tsx(1 hunks)apps/playground/src/components/ui/badge.tsx(1 hunks)apps/playground/src/components/ui/button.tsx(1 hunks)apps/playground/src/components/ui/carousel.tsx(1 hunks)apps/playground/src/components/ui/checkbox.tsx(1 hunks)apps/playground/src/components/ui/command.tsx(1 hunks)apps/playground/src/components/ui/dialog.tsx(1 hunks)apps/playground/src/components/ui/dropdown-menu.tsx(1 hunks)apps/playground/src/components/ui/form.tsx(1 hunks)apps/playground/src/components/ui/hover-card.tsx(1 hunks)apps/playground/src/components/ui/input.tsx(1 hunks)apps/playground/src/components/ui/label.tsx(1 hunks)apps/playground/src/components/ui/popover.tsx(1 hunks)apps/playground/src/components/ui/progress.tsx(1 hunks)apps/playground/src/components/ui/providers-icons.tsx(1 hunks)apps/playground/src/components/ui/scroll-area.tsx(1 hunks)apps/playground/src/components/ui/select.tsx(1 hunks)apps/playground/src/components/ui/separator.tsx(1 hunks)apps/playground/src/components/ui/sheet.tsx(1 hunks)apps/playground/src/components/ui/sidebar.tsx(1 hunks)apps/playground/src/components/ui/skeleton.tsx(1 hunks)apps/playground/src/components/ui/sonner.tsx(1 hunks)apps/playground/src/components/ui/tabs.tsx(1 hunks)apps/playground/src/components/ui/textarea.tsx(1 hunks)apps/playground/src/components/ui/tooltip.tsx(1 hunks)apps/playground/src/hooks/use-mobile.ts(1 hunks)apps/playground/src/hooks/useApiKey.ts(1 hunks)apps/playground/src/hooks/useAutoApiKey.ts(1 hunks)apps/playground/src/hooks/useChats.ts(1 hunks)apps/playground/src/hooks/useCreateApiKey.ts(1 hunks)apps/playground/src/hooks/useUser.ts(1 hunks)apps/playground/src/lib/mapmodels.ts(1 hunks)apps/playground/src/lib/model-utils.ts(1 hunks)apps/playground/src/lib/server-api.ts(1 hunks)apps/playground/src/lib/types.ts(1 hunks)apps/playground/src/lib/utils.ts(1 hunks)apps/playground/tsconfig.json(1 hunks)apps/ui/eslint.config.mjs(0 hunks)apps/ui/package.json(1 hunks)apps/ui/src/app/layout.tsx(2 hunks)apps/ui/src/app/playground/playground-client.tsx(3 hunks)apps/ui/src/components/Chat.tsx(2 hunks)apps/ui/src/components/app-sidebar.tsx(2 hunks)apps/ui/src/content/blog/2025-05-01-self-host-llm-gateway.md(1 hunks)apps/worker/package.json(1 hunks)infra/bunnyshell.yaml(1 hunks)infra/docker-compose.split.local.yml(1 hunks)infra/docker-compose.split.yml(2 hunks)infra/docker-compose.unified.local.yml(1 hunks)infra/docker-compose.unified.yml(2 hunks)infra/split.dockerfile(2 hunks)infra/supervisord.conf(1 hunks)infra/unified.dockerfile(3 hunks)
⛔ Files not processed due to max files limit (8)
- package.json
- packages/cache/package.json
- packages/db/package.json
- packages/instrumentation/package.json
- packages/logger/package.json
- packages/models/package.json
- packages/shared/package.json
- turbo.json
💤 Files with no reviewable changes (1)
- apps/ui/eslint.config.mjs
✅ Files skipped from review due to trivial changes (8)
- apps/api/package.json
- apps/playground/src/components/landing/theme-toggle.tsx
- apps/docs/content/self-host.mdx
- apps/ui/src/content/blog/2025-05-01-self-host-llm-gateway.md
- AGENTS.md
- apps/gateway/package.json
- apps/playground/public/favicon/site.webmanifest
- apps/worker/package.json
🚧 Files skipped from review as they are similar to previous changes (39)
- apps/playground/src/components/ai-elements/image.tsx
- apps/playground/src/components/ui/separator.tsx
- apps/playground/src/components/ui/button.tsx
- apps/playground/src/components/ai-elements/artifact.tsx
- apps/playground/src/components/ai-elements/response.tsx
- apps/playground/src/app/api/chat/route.ts
- apps/playground/src/components/ui/progress.tsx
- apps/playground/src/components/credits/credits-display.tsx
- apps/playground/src/components/playground/chat-header.tsx
- apps/playground/src/hooks/useApiKey.ts
- apps/playground/src/hooks/useAutoApiKey.ts
- apps/playground/src/components/ui/command.tsx
- apps/playground/src/components/ai-elements/task.tsx
- apps/playground/src/components/ai-elements/sources.tsx
- apps/playground/src/components/ui/select.tsx
- apps/playground/package.json
- apps/playground/src/hooks/useChats.ts
- apps/playground/src/components/ai-elements/suggestion.tsx
- apps/playground/src/components/ai-elements/open-in-chat.tsx
- apps/playground/src/components/ui/alert.tsx
- apps/playground/src/components/ui/sonner.tsx
- apps/playground/src/components/ui/skeleton.tsx
- apps/playground/src/hooks/use-mobile.ts
- apps/playground/src/components/ui/tooltip.tsx
- apps/playground/next.config.ts
- apps/playground/src/components/model-selector.tsx
- apps/playground/src/components/ai-elements/actions.tsx
- apps/playground/src/components/ui/dropdown-menu.tsx
- apps/playground/src/components/ui/textarea.tsx
- apps/playground/src/components/ai-elements/chain-of-thought.tsx
- apps/playground/src/components/ai-elements/inline-citation.tsx
- apps/playground/postcss.config.mjs
- apps/playground/src/components/ui/scroll-area.tsx
- apps/playground/src/components/ui/input.tsx
- apps/playground/src/lib/model-utils.ts
- apps/playground/src/components/ui/form.tsx
- apps/playground/src/components/ai-elements/tool.tsx
- apps/playground/src/components/ui/dialog.tsx
- apps/playground/src/components/ui/hover-card.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never useanyoras anyin this TypeScript project unless absolutely necessary
Always use top-levelimport; never userequireor dynamic imports
Files:
apps/playground/src/lib/types.tsapps/ui/src/app/layout.tsxapps/playground/src/components/playground/chat-ui.tsxapps/playground/src/components/ui/label.tsxapps/playground/src/components/ui/popover.tsxapps/playground/src/components/ui/avatar.tsxapps/playground/src/lib/utils.tsapps/playground/src/components/ai-elements/code-block.tsxapps/playground/src/components/ui/checkbox.tsxapps/playground/src/lib/mapmodels.tsapps/playground/src/components/ai-elements/web-preview.tsxapps/playground/src/components/ui/carousel.tsxapps/playground/src/components/ai-elements/conversation.tsxapps/ui/src/components/app-sidebar.tsxapps/playground/src/app/page.tsxapps/playground/src/components/playground/auth-dialog.tsxapps/playground/src/hooks/useCreateApiKey.tsapps/playground/src/components/ai-elements/branch.tsxapps/playground/src/components/playground/chat-sidebar.tsxapps/ui/src/app/playground/playground-client.tsxapps/playground/src/app/login/page.tsxapps/playground/src/components/ui/badge.tsxapps/playground/src/components/playground/api-key-manager.tsxapps/ui/src/components/Chat.tsxapps/playground/src/components/ai-elements/loader.tsxapps/playground/src/components/ai-elements/message.tsxapps/playground/src/app/signup/page.tsxapps/playground/src/components/ui/sheet.tsxapps/playground/src/components/ai-elements/prompt-input.tsxapps/playground/src/lib/server-api.tsapps/playground/src/components/provider-icons.tsxapps/playground/src/components/ai-elements/reasoning.tsxapps/playground/src/hooks/useUser.tsapps/playground/src/components/ui/sidebar.tsxapps/playground/src/components/ai-elements/context.tsxapps/playground/src/components/credits/top-up-credits-dialog.tsxapps/playground/src/components/playground/chat-page-client.tsxapps/playground/src/app/layout.tsxapps/playground/src/components/ui/providers-icons.tsxapps/playground/src/components/providers.tsxapps/playground/src/components/ui/tabs.tsx
{apps/api,apps/gateway,apps/ui,apps/docs,packages}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Always use top-level import; never use require() or dynamic imports (e.g., import(), next/dynamic)
Files:
apps/ui/src/app/layout.tsxapps/ui/src/components/app-sidebar.tsxapps/ui/src/app/playground/playground-client.tsxapps/ui/src/components/Chat.tsx
apps/ui/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use next/link for links and next/navigation’s router for programmatic navigation
apps/ui/**/*.{ts,tsx}: Use next/link for links and next/navigation's router for programmatic navigation in the UI
Use cookies for user settings not saved in the database to ensure SSR works
Files:
apps/ui/src/app/layout.tsxapps/ui/src/components/app-sidebar.tsxapps/ui/src/app/playground/playground-client.tsxapps/ui/src/components/Chat.tsx
🧠 Learnings (9)
📚 Learning: 2025-09-21T20:12:20.016Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-21T20:12:20.016Z
Learning: Run `pnpm build` to ensure production builds work
Applied to files:
CLAUDE.mdinfra/unified.dockerfile
📚 Learning: 2025-09-15T13:16:05.365Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: AGENTS.md:0-0
Timestamp: 2025-09-15T13:16:05.365Z
Learning: Run pnpm build to ensure production builds work
Applied to files:
CLAUDE.mdinfra/unified.dockerfile
📚 Learning: 2025-09-21T20:12:20.016Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-21T20:12:20.016Z
Learning: Run `pnpm generate` after API route changes to update OpenAPI schemas
Applied to files:
CLAUDE.md
📚 Learning: 2025-09-21T20:12:20.016Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-21T20:12:20.016Z
Learning: Always run `pnpm format` before committing code
Applied to files:
CLAUDE.md
📚 Learning: 2025-09-21T20:12:20.016Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-21T20:12:20.016Z
Learning: Always use pnpm for package management
Applied to files:
CLAUDE.mdinfra/unified.dockerfile
📚 Learning: 2025-09-21T20:12:20.016Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-21T20:12:20.016Z
Learning: Run `pnpm test:unit` and `pnpm test:e2e` after adding features
Applied to files:
CLAUDE.md
📚 Learning: 2025-09-21T20:12:20.016Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-21T20:12:20.016Z
Learning: Applies to **/*.{ts,tsx} : Never use `any` or `as any` in this TypeScript project unless absolutely necessary
Applied to files:
apps/playground/src/components/playground/chat-ui.tsxapps/playground/src/components/credits/top-up-credits-dialog.tsx
📚 Learning: 2025-09-15T13:16:05.365Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: AGENTS.md:0-0
Timestamp: 2025-09-15T13:16:05.365Z
Learning: Applies to {apps/api,apps/gateway,apps/ui,apps/docs,packages}/**/*.{ts,tsx} : Always use top-level import; never use require() or dynamic imports (e.g., import(), next/dynamic)
Applied to files:
apps/playground/src/components/providers.tsx
📚 Learning: 2025-09-21T20:12:20.016Z
Learnt from: CR
PR: theopenco/llmgateway#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-21T20:12:20.016Z
Learning: Applies to **/*.{ts,tsx} : Always use top-level `import`; never use `require` or dynamic imports
Applied to files:
apps/playground/src/components/providers.tsx
🧬 Code graph analysis (31)
apps/playground/src/components/playground/chat-ui.tsx (6)
packages/db/src/schema.ts (1)
message(498-518)apps/playground/src/components/ai-elements/conversation.tsx (3)
Conversation(14-22)ConversationContent(28-33)ConversationEmptyState(41-68)apps/playground/src/components/ai-elements/suggestion.tsx (2)
Suggestions(11-22)Suggestion(29-54)apps/playground/src/components/ai-elements/response.tsx (1)
Response(10-21)apps/playground/src/components/ai-elements/actions.tsx (2)
Actions(16-20)Action(27-66)apps/playground/src/components/ai-elements/prompt-input.tsx (10)
PromptInput(221-447)PromptInputTextarea(460-505)PromptInputToolbar(509-517)PromptInputTools(521-533)PromptInputActionMenu(563-565)PromptInputActionMenuTrigger(570-580)PromptInputActionMenuContent(585-590)PromptInputActionAddAttachments(174-191)PromptInputButton(537-560)PromptInputSubmit(609-638)
apps/playground/src/components/ui/label.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ui/popover.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ui/avatar.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/lib/utils.ts (2)
apps/docs/lib/cn.ts (1)
twMerge(1-1)apps/ui/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/code-block.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ui/checkbox.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/lib/mapmodels.ts (3)
packages/models/src/models.ts (1)
ModelDefinition(108-161)packages/models/src/providers.ts (2)
providers(19-247)ProviderDefinition(1-17)apps/playground/src/lib/types.ts (1)
ComboboxModel(39-50)
apps/playground/src/components/ai-elements/web-preview.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/conversation.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/ui/src/components/app-sidebar.tsx (1)
apps/ui/src/lib/components/use-toast.ts (1)
toast(194-194)
apps/playground/src/app/page.tsx (3)
apps/playground/src/app/layout.tsx (1)
dynamic(21-21)apps/playground/src/components/playground/chat-page-client.tsx (1)
ChatPageClient(31-260)packages/models/src/providers.ts (1)
providers(19-247)
apps/playground/src/hooks/useCreateApiKey.ts (1)
apps/api/src/posthog.ts (1)
posthog(3-6)
apps/playground/src/components/ai-elements/branch.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/playground/chat-sidebar.tsx (5)
apps/api/src/posthog.ts (1)
posthog(3-6)apps/playground/src/hooks/useUser.ts (1)
useUser(24-105)apps/playground/src/lib/auth-client.ts (1)
useAuth(20-33)apps/playground/src/hooks/useOrganization.ts (1)
useOrganization(10-30)apps/playground/src/hooks/useChats.ts (4)
useChats(25-29)useDeleteChat(90-109)useUpdateChat(69-88)Chat(6-14)
apps/playground/src/app/login/page.tsx (1)
apps/api/src/posthog.ts (1)
posthog(3-6)
apps/playground/src/components/ui/badge.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/playground/api-key-manager.tsx (3)
apps/playground/src/hooks/useApiKey.ts (1)
useApiKey(9-63)apps/playground/src/lib/config.tsx (1)
useAppConfig(25-31)apps/playground/src/hooks/useAutoApiKey.ts (1)
useAutoApiKey(13-77)
apps/playground/src/components/ai-elements/loader.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/message.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/app/signup/page.tsx (1)
apps/api/src/posthog.ts (1)
posthog(3-6)
apps/playground/src/components/ai-elements/prompt-input.tsx (5)
apps/playground/src/lib/utils.ts (1)
cn(4-6)apps/playground/src/components/ui/button.tsx (1)
Button(58-58)apps/playground/src/components/ui/dropdown-menu.tsx (4)
DropdownMenuItem(248-248)DropdownMenu(242-242)DropdownMenuTrigger(244-244)DropdownMenuContent(245-245)apps/playground/src/components/ui/textarea.tsx (1)
Textarea(18-18)apps/playground/src/components/ui/select.tsx (5)
Select(175-175)SelectTrigger(183-183)SelectContent(176-176)SelectItem(178-178)SelectValue(184-184)
apps/playground/src/components/provider-icons.tsx (2)
apps/playground/src/components/ui/providers-icons.tsx (19)
AnthropicIcon(6-30)CloudriftIcon(33-48)DeepseekIcon(51-66)GoogleStudioAIIcon(69-119)GroqIcon(122-137)InferenceNetIcon(140-164)MistralIcon(167-205)OpenAIIcon(208-229)PerplexityIcon(232-249)TogetherAIIcon(252-269)XAIIcon(272-285)MoonshotIcon(288-301)NovitaIcon(304-319)AlibabaIcon(322-334)NebiusIcon(336-348)ZaiIcon(351-364)ProviderIcons(367-384)ProviderIconKey(386-386)getProviderIcon(403-419)apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/ai-elements/reasoning.tsx (2)
apps/playground/src/lib/utils.ts (1)
cn(4-6)apps/playground/src/components/ai-elements/response.tsx (1)
Response(10-21)
apps/playground/src/hooks/useUser.ts (1)
apps/playground/src/lib/fetch-client.ts (1)
useApi(22-28)
apps/playground/src/components/ai-elements/context.tsx (1)
apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/credits/top-up-credits-dialog.tsx (3)
apps/playground/src/lib/stripe.ts (1)
useStripe(20-38)apps/playground/src/hooks/useOrganization.ts (1)
useOrganization(10-30)apps/ui/src/components/credits/top-up-credits-dialog.tsx (1)
TopUpCreditsDialog(46-144)
apps/playground/src/components/playground/chat-page-client.tsx (7)
packages/models/src/models.ts (1)
ModelDefinition(108-161)packages/models/src/providers.ts (2)
ProviderDefinition(1-17)providers(19-247)apps/playground/src/lib/mapmodels.ts (1)
mapModels(4-27)apps/playground/src/lib/types.ts (1)
ComboboxModel(39-50)apps/playground/src/hooks/useChats.ts (1)
useDataChat(31-46)apps/playground/src/components/playground/chat-ui.tsx (1)
ChatUI(71-251)apps/playground/src/components/playground/api-key-manager.tsx (1)
ApiKeyManager(129-417)
apps/playground/src/app/layout.tsx (2)
apps/playground/src/lib/config-server.ts (1)
getConfig(14-29)apps/playground/src/lib/providers.tsx (1)
Providers(21-69)
apps/playground/src/components/ui/providers-icons.tsx (2)
apps/playground/src/components/provider-icons.tsx (19)
AnthropicIcon(6-30)CloudriftIcon(33-84)DeepseekIcon(87-102)GoogleStudioAIIcon(105-155)GroqIcon(158-173)InferenceNetIcon(176-200)MistralIcon(203-241)OpenAIIcon(244-265)PerplexityIcon(268-285)TogetherAIIcon(288-305)XAIIcon(308-321)MoonshotIcon(324-337)NovitaIcon(340-355)AlibabaIcon(358-370)NebiusIcon(372-384)ZaiIcon(387-400)ProviderIcons(442-461)ProviderIconKey(464-464)getProviderIcon(467-480)apps/playground/src/lib/utils.ts (1)
cn(4-6)
apps/playground/src/components/providers.tsx (2)
apps/playground/src/lib/config.tsx (1)
AppConfigProvider(14-23)apps/docs/lib/providers.tsx (1)
PostHogProvider(11-36)
| APP_ENDPOINTS["api"]="http://localhost:4002" | ||
| APP_ENDPOINTS["gateway"]="http://localhost:4001" | ||
| APP_ENDPOINTS["ui"]="http://localhost:3002" | ||
| APP_ENDPOINTS["playground"]="http://localhost:3002" | ||
| APP_ENDPOINTS["docs"]="http://localhost:3005" |
There was a problem hiding this comment.
Point the playground smoke test at port 3003
Line 20 still targets http://localhost:3002, so our CI never exercises the actual playground instance on port 3003. Please update the endpoint so the split-image test really covers the new service.
APP_ENDPOINTS["ui"]="http://localhost:3002"
-APP_ENDPOINTS["playground"]="http://localhost:3002"
+APP_ENDPOINTS["playground"]="http://localhost:3003"
APP_ENDPOINTS["docs"]="http://localhost:3005"🤖 Prompt for AI Agents
In .github/test-split-docker.sh around lines 17 to 21, the PLAYGROUND endpoint
is incorrectly set to http://localhost:3002; update APP_ENDPOINTS["playground"]
to point to http://localhost:3003 so the playground smoke test targets the
actual playground service on port 3003 (replace the current line with the new
URL).
| # Array of endpoints for testing | ||
| declare -A APP_ENDPOINTS | ||
| APP_ENDPOINTS["api"]="http://localhost:4002/" | ||
| APP_ENDPOINTS["gateway"]="http://localhost:4001/" | ||
| APP_ENDPOINTS["api"]="http://localhost:4002" | ||
| APP_ENDPOINTS["gateway"]="http://localhost:4001" | ||
| APP_ENDPOINTS["ui"]="http://localhost:3002" | ||
| APP_ENDPOINTS["playground"]="http://localhost:3002" | ||
| APP_ENDPOINTS["docs"]="http://localhost:3005" |
There was a problem hiding this comment.
Point the playground health check at port 3003
The playground runs on 3003, not 3002. Right now the script just re-tests the UI endpoint, so we’ll miss a broken playground entirely.
APP_ENDPOINTS["api"]="http://localhost:4002"
APP_ENDPOINTS["gateway"]="http://localhost:4001"
APP_ENDPOINTS["ui"]="http://localhost:3002"
-APP_ENDPOINTS["playground"]="http://localhost:3002"
+APP_ENDPOINTS["playground"]="http://localhost:3003"📝 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.
| # Array of endpoints for testing | |
| declare -A APP_ENDPOINTS | |
| APP_ENDPOINTS["api"]="http://localhost:4002/" | |
| APP_ENDPOINTS["gateway"]="http://localhost:4001/" | |
| APP_ENDPOINTS["api"]="http://localhost:4002" | |
| APP_ENDPOINTS["gateway"]="http://localhost:4001" | |
| APP_ENDPOINTS["ui"]="http://localhost:3002" | |
| APP_ENDPOINTS["playground"]="http://localhost:3002" | |
| APP_ENDPOINTS["docs"]="http://localhost:3005" | |
| # Array of endpoints for testing | |
| declare -A APP_ENDPOINTS | |
| APP_ENDPOINTS["api"]="http://localhost:4002" | |
| APP_ENDPOINTS["gateway"]="http://localhost:4001" | |
| APP_ENDPOINTS["ui"]="http://localhost:3002" | |
| APP_ENDPOINTS["playground"]="http://localhost:3003" | |
| APP_ENDPOINTS["docs"]="http://localhost:3005" |
🤖 Prompt for AI Agents
.github/test-unified-docker.sh around lines 19 to 25: the PLAYGROUND endpoint is
incorrectly set to port 3002 (duplicating the UI) so the playground health check
targets the UI; update APP_ENDPOINTS["playground"] to use http://localhost:3003
instead of http://localhost:3002 to point the health check at the actual
playground service port.
| export type ConversationEmptyStateProps = ComponentProps<"div"> & { | ||
| title?: string; | ||
| description?: string; | ||
| icon?: React.ReactNode; | ||
| }; | ||
|
|
||
| export const ConversationEmptyState = ({ | ||
| className, | ||
| title = "No messages yet", | ||
| description = "Start a conversation to see messages here", | ||
| icon, | ||
| children, | ||
| ...props | ||
| }: ConversationEmptyStateProps) => ( |
There was a problem hiding this comment.
Import ReactNode instead of referencing React namespace
Line 38 references React.ReactNode, but we never import React. In a module this fails TypeScript compilation (“Cannot find namespace 'React'”). Please import the type directly and drop the namespace reference.
-import type { ComponentProps } from "react";
+import type { ComponentProps, ReactNode } from "react";
…
-export type ConversationEmptyStateProps = ComponentProps<"div"> & {
+export type ConversationEmptyStateProps = ComponentProps<"div"> & {
title?: string;
description?: string;
- icon?: React.ReactNode;
+ icon?: ReactNode;
};📝 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.
| export type ConversationEmptyStateProps = ComponentProps<"div"> & { | |
| title?: string; | |
| description?: string; | |
| icon?: React.ReactNode; | |
| }; | |
| export const ConversationEmptyState = ({ | |
| className, | |
| title = "No messages yet", | |
| description = "Start a conversation to see messages here", | |
| icon, | |
| children, | |
| ...props | |
| }: ConversationEmptyStateProps) => ( | |
| // apps/playground/src/components/ai-elements/conversation.tsx | |
| import type { ComponentProps, ReactNode } from "react"; | |
| export type ConversationEmptyStateProps = ComponentProps<"div"> & { | |
| title?: string; | |
| description?: string; | |
| icon?: ReactNode; | |
| }; | |
| export const ConversationEmptyState = ({ | |
| className, | |
| title = "No messages yet", | |
| description = "Start a conversation to see messages here", | |
| icon, | |
| children, | |
| ...props | |
| }: ConversationEmptyStateProps) => ( | |
| /* ... */ | |
| ); |
🤖 Prompt for AI Agents
In apps/playground/src/components/ai-elements/conversation.tsx around lines 35
to 48, the type references React.ReactNode but the React namespace isn't
imported which breaks TypeScript; replace React.ReactNode with the direct
ReactNode type and add an import for it from 'react' (e.g. import type {
ReactNode } from 'react' or include it in an existing react import), and update
the ConversationEmptyStateProps declaration to use ReactNode.
| function Avatar({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<typeof AvatarPrimitive.Root>) { | ||
| return ( | ||
| <AvatarPrimitive.Root | ||
| data-slot="avatar" | ||
| className={cn( | ||
| "relative flex size-8 shrink-0 overflow-hidden rounded-full", | ||
| className, | ||
| )} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function AvatarImage({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<typeof AvatarPrimitive.Image>) { | ||
| return ( | ||
| <AvatarPrimitive.Image | ||
| data-slot="avatar-image" | ||
| className={cn("aspect-square size-full", className)} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function AvatarFallback({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { | ||
| return ( | ||
| <AvatarPrimitive.Fallback | ||
| data-slot="avatar-fallback" | ||
| className={cn( | ||
| "bg-muted flex size-full items-center justify-center rounded-full", | ||
| className, | ||
| )} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Forward refs so Radix wrappers remain ref-compatible
By wrapping the Radix primitives in plain function components, we drop ref forwarding. Any consumer attempting <Avatar ref={...} /> (which Radix supports and many of our components rely on) now gets “Function components cannot be given refs”, breaking focus management/tooling. Please forward refs for all three wrappers.
-function Avatar({
- className,
- ...props
-}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
- return (
- <AvatarPrimitive.Root
- data-slot="avatar"
- className={cn(
- "relative flex size-8 shrink-0 overflow-hidden rounded-full",
- className,
- )}
- {...props}
- />
- );
-}
+const Avatar = React.forwardRef<
+ React.ElementRef<typeof AvatarPrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
+>(({ className, ...props }, ref) => (
+ <AvatarPrimitive.Root
+ ref={ref}
+ data-slot="avatar"
+ className={cn(
+ "relative flex size-8 shrink-0 overflow-hidden rounded-full",
+ className,
+ )}
+ {...props}
+ />
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
-function AvatarImage({
- className,
- ...props
-}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
- return (
- <AvatarPrimitive.Image
- data-slot="avatar-image"
- className={cn("aspect-square size-full", className)}
- {...props}
- />
- );
-}
+const AvatarImage = React.forwardRef<
+ React.ElementRef<typeof AvatarPrimitive.Image>,
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
+>(({ className, ...props }, ref) => (
+ <AvatarPrimitive.Image
+ ref={ref}
+ data-slot="avatar-image"
+ className={cn("aspect-square size-full", className)}
+ {...props}
+ />
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
-function AvatarFallback({
- className,
- ...props
-}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
- return (
- <AvatarPrimitive.Fallback
- data-slot="avatar-fallback"
- className={cn(
- "bg-muted flex size-full items-center justify-center rounded-full",
- className,
- )}
- {...props}
- />
- );
-}
+const AvatarFallback = React.forwardRef<
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
+>(({ className, ...props }, ref) => (
+ <AvatarPrimitive.Fallback
+ ref={ref}
+ data-slot="avatar-fallback"
+ className={cn(
+ "bg-muted flex size-full items-center justify-center rounded-full",
+ className,
+ )}
+ {...props}
+ />
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;📝 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.
| function Avatar({ | |
| className, | |
| ...props | |
| }: React.ComponentProps<typeof AvatarPrimitive.Root>) { | |
| return ( | |
| <AvatarPrimitive.Root | |
| data-slot="avatar" | |
| className={cn( | |
| "relative flex size-8 shrink-0 overflow-hidden rounded-full", | |
| className, | |
| )} | |
| {...props} | |
| /> | |
| ); | |
| } | |
| function AvatarImage({ | |
| className, | |
| ...props | |
| }: React.ComponentProps<typeof AvatarPrimitive.Image>) { | |
| return ( | |
| <AvatarPrimitive.Image | |
| data-slot="avatar-image" | |
| className={cn("aspect-square size-full", className)} | |
| {...props} | |
| /> | |
| ); | |
| } | |
| function AvatarFallback({ | |
| className, | |
| ...props | |
| }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { | |
| return ( | |
| <AvatarPrimitive.Fallback | |
| data-slot="avatar-fallback" | |
| className={cn( | |
| "bg-muted flex size-full items-center justify-center rounded-full", | |
| className, | |
| )} | |
| {...props} | |
| /> | |
| ); | |
| } | |
| const Avatar = React.forwardRef< | |
| React.ElementRef<typeof AvatarPrimitive.Root>, | |
| React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> | |
| >(({ className, ...props }, ref) => ( | |
| <AvatarPrimitive.Root | |
| ref={ref} | |
| data-slot="avatar" | |
| className={cn( | |
| "relative flex size-8 shrink-0 overflow-hidden rounded-full", | |
| className, | |
| )} | |
| {...props} | |
| /> | |
| )); | |
| Avatar.displayName = AvatarPrimitive.Root.displayName; | |
| const AvatarImage = React.forwardRef< | |
| React.ElementRef<typeof AvatarPrimitive.Image>, | |
| React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> | |
| >(({ className, ...props }, ref) => ( | |
| <AvatarPrimitive.Image | |
| ref={ref} | |
| data-slot="avatar-image" | |
| className={cn("aspect-square size-full", className)} | |
| {...props} | |
| /> | |
| )); | |
| AvatarImage.displayName = AvatarPrimitive.Image.displayName; | |
| const AvatarFallback = React.forwardRef< | |
| React.ElementRef<typeof AvatarPrimitive.Fallback>, | |
| React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> | |
| >(({ className, ...props }, ref) => ( | |
| <AvatarPrimitive.Fallback | |
| ref={ref} | |
| data-slot="avatar-fallback" | |
| className={cn( | |
| "bg-muted flex size-full items-center justify-center rounded-full", | |
| className, | |
| )} | |
| {...props} | |
| /> | |
| )); | |
| AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; |
🤖 Prompt for AI Agents
In apps/playground/src/components/ui/avatar.tsx around lines 8 to 51, the three
wrapper components (Avatar, AvatarImage, AvatarFallback) are plain function
components that drop ref forwarding and break consumers expecting
Radix-compatible refs; convert each to use React.forwardRef so the ref passed by
callers is forwarded to the underlying Radix primitive, preserve existing props
and className merging, and type the forwardRef generically (or with
React.ComponentPropsWithRef for the corresponding AvatarPrimitive.* element) so
the forwarded ref has the correct element type; ensure you keep the data-slot
and className logic and export the resulting forwardRef components in place of
the current functions.
| function Badge({ | ||
| className, | ||
| variant, | ||
| asChild = false, | ||
| ...props | ||
| }: React.ComponentProps<"span"> & | ||
| VariantProps<typeof badgeVariants> & { asChild?: boolean }) { | ||
| const Comp = asChild ? Slot : "span"; | ||
|
|
||
| return ( | ||
| <Comp | ||
| data-slot="badge" | ||
| className={cn(badgeVariants({ variant }), className)} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Mirror Radix’s ref-forwarding on Badge.
asChild implies consumers may need a ref on the underlying element. Forward it to keep parity with Radix primitives and other wrappers in this PR.
-function Badge({
- className,
- variant,
- asChild = false,
- ...props
-}: React.ComponentProps<"span"> &
- VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
- const Comp = asChild ? Slot : "span";
-
- return (
- <Comp
- data-slot="badge"
- className={cn(badgeVariants({ variant }), className)}
- {...props}
- />
- );
-}
+const Badge = React.forwardRef<
+ HTMLElement,
+ React.ComponentProps<"span"> &
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }
+>(({ className, variant, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "span";
+
+ return (
+ <Comp
+ ref={ref}
+ data-slot="badge"
+ className={cn(badgeVariants({ variant }), className)}
+ {...props}
+ />
+ );
+});
+
+Badge.displayName = "Badge";📝 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.
| function Badge({ | |
| className, | |
| variant, | |
| asChild = false, | |
| ...props | |
| }: React.ComponentProps<"span"> & | |
| VariantProps<typeof badgeVariants> & { asChild?: boolean }) { | |
| const Comp = asChild ? Slot : "span"; | |
| return ( | |
| <Comp | |
| data-slot="badge" | |
| className={cn(badgeVariants({ variant }), className)} | |
| {...props} | |
| /> | |
| ); | |
| } | |
| const Badge = React.forwardRef< | |
| HTMLElement, | |
| React.ComponentProps<"span"> & | |
| VariantProps<typeof badgeVariants> & { asChild?: boolean } | |
| >(({ className, variant, asChild = false, ...props }, ref) => { | |
| const Comp = asChild ? Slot : "span"; | |
| return ( | |
| <Comp | |
| ref={ref} | |
| data-slot="badge" | |
| className={cn(badgeVariants({ variant }), className)} | |
| {...props} | |
| /> | |
| ); | |
| }); | |
| Badge.displayName = "Badge"; |
🤖 Prompt for AI Agents
In apps/playground/src/components/ui/badge.tsx around lines 28 to 44, the Badge
component does not forward refs when asChild is used (or otherwise), so
consumers cannot attach refs like Radix primitives; convert Badge to a
React.forwardRef functional component with appropriate generic typing, accept
(props, ref) and pass the ref to the rendered Comp (Slot or "span"), and ensure
the forwarded ref is typed to the underlying HTMLElement type so consumers can
use refs safely.
Updated package versions to `0.0.0` across multiple apps and packages for consistency during development. Enabled `reactStrictMode` in Next.js configuration for the playground app.
d420814 to
c74792e
Compare
left a comment
There was a problem hiding this comment.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
apps/api/package.json(1 hunks)apps/gateway/package.json(1 hunks)apps/playground/next.config.ts(1 hunks)apps/playground/package.json(1 hunks)apps/ui/package.json(1 hunks)apps/worker/package.json(1 hunks)packages/cache/package.json(1 hunks)packages/db/package.json(1 hunks)packages/instrumentation/package.json(1 hunks)packages/logger/package.json(1 hunks)packages/models/package.json(1 hunks)packages/shared/package.json(1 hunks)turbo.json(1 hunks)
✅ Files skipped from review due to trivial changes (5)
- packages/cache/package.json
- turbo.json
- packages/logger/package.json
- packages/models/package.json
- packages/db/package.json
🚧 Files skipped from review as they are similar to previous changes (3)
- apps/ui/package.json
- apps/playground/next.config.ts
- apps/worker/package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
- GitHub Check: build / run
- GitHub Check: lint / run
- GitHub Check: test / run
- GitHub Check: generate / run
- GitHub Check: e2e-shards (3)
- GitHub Check: e2e-shards (1)
- GitHub Check: e2e-shards (2)
- GitHub Check: e2e-shards (5)
- GitHub Check: e2e-shards (4)
- GitHub Check: autofix
🔇 Additional comments (3)
apps/api/package.json (1)
3-3: Version reset aligns with the workspace baseline.No issues spotted with bumping the API package back to
0.0.0, given it remains private and matches the other workspace entries.packages/shared/package.json (1)
3-3: Version reset is fine for private workspace packages.Setting this private package to
0.0.0matches the placeholder strategy used elsewhere in the repo; no action needed.packages/instrumentation/package.json (1)
3-3: Confirm the 0.0.0 version resetDropping from 1.0.0 to 0.0.0 is an unusual move and could confuse release automation or any tooling that expects semantic version monotonicity. Please double-check that nothing in our publishing/release flow or workspace dependency graph depends on this package retaining a ≥1.x version.
| "format": "eslint --fix . && prettier --write .", | ||
| "generate": "openapi-typescript ../api/openapi.json -o ./src/lib/api/v1.d.ts", | ||
| "lint": "eslint . && prettier --check .", | ||
| "start": "next start" |
There was a problem hiding this comment.
Add Prettier to devDependencies.
Both the format and lint scripts invoke prettier, but this package doesn’t declare it anywhere. Running the scripts in isolation (e.g., pnpm --filter playground lint) will fail with “prettier: command not found”. Please add Prettier locally so CI and workspace-filtered runs don’t break.
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@tanstack/react-query-devtools": "5.84.1",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-syntax-highlighter": "15.5.13",
"eslint": "^9",
"eslint-config-next": "15.5.3",
"openapi-typescript": "7.8.0",
+ "prettier": "^3.3.3",
"tailwindcss": "^4",
"tw-animate-css": "1.3.8",
"typescript": "^5",
"zod": "3.25.75"
}Also applies to: 68-83
🤖 Prompt for AI Agents
In apps/playground/package.json around lines 9 to 12 (and similarly for the
other package.json at lines 68-83), the package.json scripts call prettier but
Prettier is not declared as a devDependency; add a devDependency entry for
"prettier" (pin to the monorepo/common prettier version or a specific compatible
version), run install so the workspace/local install provides the binary, and
commit the updated package.json and lockfile so running npm/pnpm/yarn --filter
playground lint or format succeeds in CI and filtered workspace runs.
Replaced UI_PORT with PLAYGROUND_PORT in Docker Compose files to improve clarity and better align with the service naming conventions.
Summary by CodeRabbit
New Features
Behavior
Documentation
Chores