Skip to content

feat: new chat playground - #928

Merged
steebchen merged 26 commits into
mainfrom
feat/playground-v2
Sep 25, 2025
Merged

steebchen merged 26 commits into
mainfrom
feat/playground-v2

Conversation

@smakosh

@smakosh smakosh commented Sep 23, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Full Playground app: interactive chat UI, model selector, attachments, suggestions, branching/reasoning UIs, web preview, auth (login/signup), API key manager, credits top-up flow, and many reusable UI components.
  • Behavior

    • New accounts receive a credits-mode default project and an auto-generated playground API key.
    • CORS expanded to allow the Playground origin.
    • Improved user-facing error toasts across the UI.
  • Documentation

    • Dev docs updated with Playground server, ports, and URLs.
  • Chores

    • CI, Docker, infra and tooling updated to include Playground (port 3003).

- 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.
@smakosh
smakosh requested a review from steebchen September 23, 2025 20:35
@smakosh smakosh self-assigned this Sep 23, 2025
@bunnyshell

bunnyshell Bot commented Sep 23, 2025

Copy link
Copy Markdown

❌ Preview Environment deleted from Bunnyshell

Available commands (reply to this comment):

  • 🚀 /bns:deploy to deploy the environment

@coderabbitai

coderabbitai Bot commented Sep 23, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds 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

Cohort / File(s) Summary
API auth & CORS
apps/api/src/auth/config.ts, apps/api/src/index.ts, apps/api/package.json
Signup now creates default org/project with project.mode="credits" and, inside the same transaction, inserts an auto-generated playground API key (env-prefixed token using shortid). CORS fallback now accepts multiple localhost origins. API package version bumped.
Playground app scaffold & config
(root)
apps/playground/{package.json,tsconfig.json,next.config.ts,postcss.config.mjs,eslin t.config.mjs,components.json,.gitignore,.prettierignore,.lintstagedrc.json,README.md,public/favicon/site.webmanifest}
New playground app top-level config, tooling and manifest files, workspace/tsconfig entries, and ignore/lint/format configs.
Playground pages & API
apps/playground/src/app/{layout.tsx,page.tsx,login/page.tsx,signup/page.tsx,api/chat/route.ts,posthog.ts}
App layout, main page, login/signup flows, streaming POST /api/chat route, and PostHog client factory added.
Playground core libs & types
apps/playground/src/lib/* (config-server.ts, config.tsx, fetch-client.ts, server-api.ts, auth-client.ts, getUser.ts, mapmodels.ts, model-utils.ts, types.ts, utils.ts)
New runtime + server config, AppConfig provider, OpenAPI fetch/react-query utilities, server API helpers, auth hooks, getUser helper, model mapping/utilities, types, and cn util.
Playground hooks
apps/playground/src/hooks/* (useApiKey.ts,useAutoApiKey.ts,useCreateApiKey.ts,useDefaultProject.ts,useOrganization.ts,useUser.ts,useChats.ts,use-mobile.ts)
Hooks for local API key storage/sync, auto-detection of server keys, API key creation, default project/org discovery, user/session management, chat CRUD, and mobile detection.
Playground UI — core pages & playground features
apps/playground/src/components/playground/* (chat-page-client.tsx,chat-ui.tsx,chat-sidebar.tsx,chat-header.tsx,auth-dialog.tsx,api-key-manager.tsx)
Client chat page, ChatUI, chat sidebar (CRUD), header (model selector, theme), auth modal, and API key manager (paste/create/validate).
AI UI primitives
apps/playground/src/components/ai-elements/*
Large set of AI-focused UI components (actions, artifact, branch, chain-of-thought, code-block, context, conversation, image, inline-citation carousel, loader, message/avatar, open-in menu, prompt input with attachments, reasoning, response, sources, suggestions, task, tool, web-preview, etc.).
Credits & payments
apps/playground/src/components/credits/* (credits-display.tsx,top-up-credits-dialog.tsx)
Credits display and multi-step Stripe top-up dialog with fee estimation and payment flows.
UI primitives / toolkit
apps/playground/src/components/ui/*
Comprehensive Radix/Embla-based UI primitives (alert, avatar, badge, button, checkbox, command, dialog, dropdown-menu, form, hover-card, input, label, logo, popover, provider-icons, progress, scroll-area, select, separator, sheet, sidebar, skeleton, sonner, tabs, textarea, tooltip, carousel, collapsible, etc.).
Styling & globals
apps/playground/src/app/globals.css
Global theming tokens, light/dark variants, and Tailwind integration.
Providers & server wrappers
apps/playground/src/components/{providers,server-data-wrapper}.tsx, apps/playground/src/components/auth/user-provider.tsx, apps/playground/src/lib/actions/project.ts
App providers (Query, Theme, AppConfig, PostHog, Crisp), server-data cache hydrator, user cache prefill provider, and server action to clear last-used-project cookies on logout.
Model selector & icons
apps/playground/src/components/model-selector.tsx, apps/playground/src/components/provider-icons.tsx
Searchable/filterable model selector with HoverCard quick-view and a large provider SVG icon set plus getProviderIcon helper.
Playground hooks & UX wiring
apps/playground/src/components/providers.tsx, apps/playground/src/components/playground/*
Providers wiring, PostHog/Crisp lazy init, ServerDataWrapper and Playground UI composition.
Workspace / infra / CI / docs
.env.example, .env.unified.example, AGENTS.md, CLAUDE.md, apps/docs/content/self-host.mdx, .github/*, infra/*, .github/start.sh, .github/test-*.sh
Adds PLAYGROUND_URL/PORT entries, docs updates, integrates playground into CI/image workflows and start/test scripts, adds playground service to docker-compose and Dockerfiles, supervisord and deployment configs; exposes port 3003.
Version bumps & misc
packages/*/package.json, apps/ui/package.json, apps/gateway/package.json, apps/worker/package.json, turbo.json
Multiple package.json version fields updated to 0.0.0; minor turbo.json ordering tweak.
UI app small changes
apps/ui/src/app/layout.tsx, apps/ui/src/app/playground/playground-client.tsx, apps/ui/src/components/{Chat.tsx,app-sidebar.tsx}, apps/ui/eslint.config.mjs
Adds Google fonts, replaces some console.error logs with Sonner toasts for user-facing errors, and a small ESLint tweak.

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)
Loading
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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title ‘feat: new chat playground’ clearly captures the main change of this PR, which is the introduction of a new chat playground application. It follows a conventional commit prefix, remains concise, and focuses on the feature without extraneous detail. A teammate reviewing the history can understand the PR’s primary purpose at a glance.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/playground-v2

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0397b6e and 976bac7.

📒 Files selected for processing (1)
  • .github/test-split-docker.sh (5 hunks)
🧰 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)
  • GitHub Check: build-split (api, linux/amd64)
  • GitHub Check: build-split (ui, linux/amd64)
  • GitHub Check: build-unified (linux/amd64)
  • GitHub Check: build-split (worker, linux/amd64)
  • GitHub Check: build-split (docs, linux/amd64)
  • GitHub Check: build-split (playground, linux/amd64)
  • GitHub Check: build-split (gateway, linux/amd64)
  • GitHub Check: autofix
  • GitHub Check: build / run
  • GitHub Check: test / run
  • GitHub Check: generate / run
  • GitHub Check: lint / run
  • GitHub Check: e2e-shards (5)
  • GitHub Check: e2e-shards (4)
  • GitHub Check: e2e-shards (2)
  • GitHub Check: e2e-shards (1)
  • GitHub Check: e2e-shards (3)
🔇 Additional comments (1)
.github/test-split-docker.sh (1)

20-20: Point the playground smoke test to port 3003

The playground container in split Docker runs on 3003, so this still probes the UI service on 3002 and leaves the real playground path untested.

-APP_ENDPOINTS["playground"]="http://localhost:3002"
+APP_ENDPOINTS["playground"]="http://localhost:3003"

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 refs

Without "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 refs

Same 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 module

Duplicate 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 imports

Found 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).

Comment thread apps/api/src/auth/config.ts
Comment thread apps/playground/.gitignore
Comment thread apps/playground/components/ui/badge.tsx Outdated
Comment thread apps/playground/components/ui/collapsible.tsx Outdated
Comment thread apps/playground/components/ui/hover-card.tsx Outdated
Comment on lines +117 to +123
return api.useMutation("patch", "/user/me", {
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["user"] });
void queryClient.invalidateQueries({ queryKey: ["session"] });
},
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ 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\(' -C3

Length 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 || true

Length 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.

Suggested change
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.

Comment on lines +13 to +15
baseURL: config.apiUrl + "/auth",
plugins: [passkeyClient()],
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 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.

Suggested change
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.

Comment on lines +13 to +27
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}`
: "",
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +30 to +41
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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;
}

Comment on lines +33 to +50
[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];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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`.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review continued from previous batch...

Comment thread apps/playground/components/ui/carousel.tsx Outdated
Comment thread apps/playground/components/ui/tooltip.tsx Outdated
Comment thread apps/playground/components/ui/tooltip.tsx Outdated
Comment thread apps/playground/src/components/ai-elements/artifact.tsx Outdated
Comment thread apps/playground/src/components/ai-elements/artifact.tsx Outdated
Comment thread apps/playground/src/components/ui/form.tsx Outdated
Comment thread apps/playground/src/components/ui/select.tsx Outdated
Comment thread apps/playground/src/components/ui/select.tsx Outdated
Comment thread apps/playground/src/components/ui/select.tsx Outdated
Comment thread apps/playground/src/components/ui/sidebar.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review continued from previous batch...

Comment thread apps/playground/src/components/ai-elements/inline-citation.tsx Outdated
Comment thread apps/playground/src/components/ai-elements/inline-citation.tsx Outdated
Comment thread apps/playground/src/components/ai-elements/inline-citation.tsx Outdated
Comment thread apps/playground/src/components/ai-elements/prompt-input.tsx Outdated
Comment on lines +541 to +543
const newSize =
(size ?? Children.count(props.children) > 1) ? "default" : "icon";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (12)
apps/playground/.gitignore (2)

34-36: Allow committing env templates

If 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 SDKs

If you use Yarn Berry editor SDKs and want them versioned, also allow .yarn/sdks.

 .yarn/*
 !.yarn/patches
 !.yarn/plugins
 !.yarn/releases
 !.yarn/versions
+!.yarn/sdks

Can 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 focusable

Capturing 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)

orientation is always defined due to the default prop, so the opts?.axis fallback never runs.

-        orientation:
-          orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
+        orientation,

124-126: A11y: ensure the region has an accessible name

The 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

📥 Commits

Reviewing files that changed from the base of the PR and between b6974f0 and 947575d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is 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 use any or as any in this TypeScript project unless absolutely necessary
Always use top-level import; never use require or dynamic imports

Files:

  • apps/playground/src/components/ui/collapsible.tsx
  • apps/playground/src/components/playground/chat-ui.tsx
  • apps/playground/src/components/ui/progress.tsx
  • apps/playground/src/components/playground/chat-sidebar.tsx
  • apps/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("");

Comment thread apps/playground/.gitignore
Comment thread apps/playground/src/components/playground/chat-sidebar.tsx
Comment thread apps/playground/src/components/ui/carousel.tsx
@@ -0,0 +1,33 @@
"use client";

import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Comment on lines +11 to +20
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Comment on lines +8 to +31
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 };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

@steebchen
steebchen enabled auto-merge September 24, 2025 22:13
@steebchen
steebchen added this pull request to the merge queue Sep 24, 2025
@steebchen
steebchen removed this pull request from the merge queue due to a manual request Sep 24, 2025
…iles

Added support for the "playground" service in Docker build configurations, supervisord, and CI workflows. Adjusted "playground" build script for consistency.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 throw

If the error payload parses but lacks an error field, 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: Replace any[] with a concrete image type (project guideline: no any)

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 toast via @/lib/components/use-toast to 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 with NODE_OPTIONS="..." only works on POSIX shells; Windows devs will get a 'NODE_OPTIONS' is not recognized failure. Let's route the flag through cross-env so 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 type

It’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 fetchServerData

fetchServerData 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 await

Small cleanup.

-const cookieStore = await cookies();
+const cookieStore = cookies();

19-29: Avoid sending an empty Cookie header

Only 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 casts

Overload 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 never cast keeps types strict without introducing any. If desired, we can further type options per method/path using the openapi-fetch types.


19-29: Consider adding a default timeout via AbortController

Prevent 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.

setUserApiKey isn’t used; console.log should 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.writeText can 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 toast

Use 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 shadowing error state; don’t overwrite specific server errors

Rename 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 composition

Exporting useChainOfThought enables external toggles and custom controls without prop drilling.

-const useChainOfThought = () => {
+export const useChainOfThought = () => {

130-135: Hoist statusStyles to module scope

Avoid 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 images

Improves 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 succeeds

Currently 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 ranges

Show 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 primitives

Enables 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 text

Forwarding 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 measurement

Forwarding 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 consumers

Tooltip 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 control

Brings 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 rendering

Drop 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 navigation

Support 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 (drop allow-popups by 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

📥 Commits

Reviewing files that changed from the base of the PR and between 947575d and e406782.

⛔ Files ignored due to path filters (10)
  • apps/playground/public/favicon/android-chrome-192x192.png is excluded by !**/*.png
  • apps/playground/public/favicon/android-chrome-512x512.png is excluded by !**/*.png
  • apps/playground/public/favicon/apple-touch-icon.png is excluded by !**/*.png
  • apps/playground/public/favicon/favicon-16x16.png is excluded by !**/*.png
  • apps/playground/public/favicon/favicon-32x32.png is excluded by !**/*.png
  • apps/playground/public/favicon/favicon.ico is excluded by !**/*.ico
  • apps/playground/public/opengraph.png is excluded by !**/*.png
  • apps/playground/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/ui/public/next.svg is excluded by !**/*.svg
  • apps/ui/public/vercel.svg is 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 use any or as any in this TypeScript project unless absolutely necessary
Always use top-level import; never use require or dynamic imports

Files:

  • apps/playground/src/lib/utils.ts
  • apps/playground/src/components/ai-elements/loader.tsx
  • apps/playground/src/components/ui/tooltip.tsx
  • apps/playground/src/components/ai-elements/actions.tsx
  • apps/playground/src/components/credits/top-up-credits-dialog.tsx
  • apps/playground/src/components/ai-elements/code-block.tsx
  • apps/playground/src/app/signup/page.tsx
  • apps/playground/src/components/ui/hover-card.tsx
  • apps/playground/src/lib/types.ts
  • apps/ui/src/components/app-sidebar.tsx
  • apps/playground/src/hooks/useAutoApiKey.ts
  • apps/playground/src/components/ui/command.tsx
  • apps/playground/src/components/ai-elements/conversation.tsx
  • apps/playground/src/app/login/page.tsx
  • apps/playground/src/lib/server-api.ts
  • apps/playground/src/components/ui/carousel.tsx
  • apps/playground/src/app/page.tsx
  • apps/playground/src/components/ai-elements/message.tsx
  • apps/ui/src/app/layout.tsx
  • apps/playground/src/components/ui/dialog.tsx
  • apps/playground/src/hooks/use-mobile.ts
  • apps/playground/src/components/ui/select.tsx
  • apps/playground/src/components/ui/progress.tsx
  • apps/playground/src/components/ui/tabs.tsx
  • apps/playground/src/components/playground/chat-sidebar.tsx
  • apps/playground/src/components/ai-elements/open-in-chat.tsx
  • apps/playground/src/components/ui/providers-icons.tsx
  • apps/playground/src/components/playground/api-key-manager.tsx
  • apps/playground/src/components/provider-icons.tsx
  • apps/ui/src/components/Chat.tsx
  • apps/playground/src/components/providers.tsx
  • apps/playground/src/components/model-selector.tsx
  • apps/playground/src/components/ui/checkbox.tsx
  • apps/playground/src/components/ui/input.tsx
  • apps/playground/src/components/ai-elements/web-preview.tsx
  • apps/playground/src/components/ui/sheet.tsx
  • apps/playground/src/components/ui/sidebar.tsx
  • apps/playground/src/components/ai-elements/image.tsx
  • apps/playground/src/components/ui/form.tsx
  • apps/ui/src/app/playground/playground-client.tsx
  • apps/playground/src/hooks/useUser.ts
  • apps/playground/src/components/ai-elements/chain-of-thought.tsx
  • apps/playground/src/components/ai-elements/inline-citation.tsx
  • apps/playground/src/hooks/useApiKey.ts
  • apps/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.tsx
  • apps/ui/src/app/layout.tsx
  • apps/ui/src/components/Chat.tsx
  • apps/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.tsx
  • apps/ui/src/app/layout.tsx
  • apps/ui/src/components/Chat.tsx
  • apps/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

Comment on lines +65 to +107
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Comment on lines +79 to +85
posthog.capture("user_logged_in", {
method: "email",
email: values.email,
});
toast.success("Login successful");
router.push("/dashboard");
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

❓ 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.

Suggested change
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).

Comment on lines +109 to +137
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Comment on lines +48 to +50
useEffect(() => {
posthog.capture("page_viewed_signup");
}, [posthog]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Comment on lines +67 to +76
return (
<ChainOfThoughtContext.Provider value={contextValue}>
<div
className={cn("not-prose max-w-prose space-y-4", className)}
{...props}
>
{children}
</div>
</ChainOfThoughtContext.Provider>
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +232 to +241
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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment thread apps/playground/src/components/ui/tabs.tsx
Comment on lines +12 to +18
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Comment on lines +71 to 82
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)",
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 shape

If 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 names

Rename 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.Content improves 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 error in useStripe(); 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 libs

Expose 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 purpose

The 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. Rename useAutoApiKey to useAutoApiKeyStatus (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: Make title optional for custom children

If children is provided, title shouldn’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 workspace

Matches 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 label

Improves 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 host

Use 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) lookups

Linear 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 primitives

This 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 access

Improves 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 usage

Brings 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 unavailable

usePostHog 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

📥 Commits

Reviewing files that changed from the base of the PR and between 947575d and e406782.

⛔ Files ignored due to path filters (10)
  • apps/playground/public/favicon/android-chrome-192x192.png is excluded by !**/*.png
  • apps/playground/public/favicon/android-chrome-512x512.png is excluded by !**/*.png
  • apps/playground/public/favicon/apple-touch-icon.png is excluded by !**/*.png
  • apps/playground/public/favicon/favicon-16x16.png is excluded by !**/*.png
  • apps/playground/public/favicon/favicon-32x32.png is excluded by !**/*.png
  • apps/playground/public/favicon/favicon.ico is excluded by !**/*.ico
  • apps/playground/public/opengraph.png is excluded by !**/*.png
  • apps/playground/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/ui/public/next.svg is excluded by !**/*.svg
  • apps/ui/public/vercel.svg is 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 use any or as any in this TypeScript project unless absolutely necessary
Always use top-level import; never use require or dynamic imports

Files:

  • apps/playground/src/lib/types.ts
  • apps/playground/src/components/ui/avatar.tsx
  • apps/ui/src/app/playground/playground-client.tsx
  • apps/playground/src/components/ui/scroll-area.tsx
  • apps/playground/src/components/ai-elements/task.tsx
  • apps/playground/src/components/playground/chat-sidebar.tsx
  • apps/playground/src/app/layout.tsx
  • apps/playground/src/hooks/useAutoApiKey.ts
  • apps/playground/src/components/ui/textarea.tsx
  • apps/playground/src/components/ai-elements/message.tsx
  • apps/playground/src/lib/server-api.ts
  • apps/playground/src/hooks/useUser.ts
  • apps/playground/src/components/ai-elements/open-in-chat.tsx
  • apps/playground/src/components/ui/checkbox.tsx
  • apps/playground/src/components/ui/tabs.tsx
  • apps/playground/src/components/ai-elements/image.tsx
  • apps/playground/src/app/login/page.tsx
  • apps/playground/src/components/ui/skeleton.tsx
  • apps/ui/src/components/app-sidebar.tsx
  • apps/playground/src/components/ui/badge.tsx
  • apps/playground/src/components/ai-elements/reasoning.tsx
  • apps/playground/src/components/credits/top-up-credits-dialog.tsx
  • apps/playground/src/components/ui/command.tsx
  • apps/playground/src/hooks/useChats.ts
  • apps/playground/src/components/ai-elements/conversation.tsx
  • apps/playground/src/components/ui/button.tsx
  • apps/playground/src/components/ai-elements/context.tsx
  • apps/playground/src/components/ui/popover.tsx
  • apps/ui/src/components/Chat.tsx
  • apps/playground/src/components/ai-elements/chain-of-thought.tsx
  • apps/playground/src/lib/mapmodels.ts
  • apps/playground/src/lib/model-utils.ts
  • apps/playground/src/hooks/useApiKey.ts
  • apps/playground/src/components/ai-elements/code-block.tsx
  • apps/playground/src/hooks/useCreateApiKey.ts
  • apps/playground/src/components/ai-elements/branch.tsx
  • apps/playground/src/components/ui/sidebar.tsx
  • apps/playground/src/components/ai-elements/loader.tsx
  • apps/playground/src/components/ui/tooltip.tsx
  • apps/playground/src/lib/utils.ts
  • apps/playground/src/components/ui/sheet.tsx
  • apps/playground/src/components/providers.tsx
  • apps/playground/src/components/playground/auth-dialog.tsx
  • apps/playground/src/components/ai-elements/inline-citation.tsx
  • apps/playground/src/app/api/chat/route.ts
  • apps/playground/src/app/signup/page.tsx
  • apps/playground/src/components/ai-elements/suggestion.tsx
  • apps/ui/src/app/layout.tsx
  • apps/playground/src/components/playground/chat-page-client.tsx
  • apps/playground/src/components/ai-elements/tool.tsx
  • apps/playground/src/components/playground/api-key-manager.tsx
  • apps/playground/src/components/ui/carousel.tsx
  • apps/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.tsx
  • apps/ui/src/components/app-sidebar.tsx
  • apps/ui/src/components/Chat.tsx
  • apps/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.tsx
  • apps/ui/src/components/app-sidebar.tsx
  • apps/ui/src/components/Chat.tsx
  • apps/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)

Comment on lines +50 to +55
const result = streamText({
model: llmgateway.chat(selectedModel),
messages: convertToModelMessages(messages),
});

return result.toUIMessageStreamResponse();

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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";

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Comment on lines +79 to +83
posthog.capture("user_logged_in", {
method: "email",
email: values.email,
});
toast.success("Login successful");

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +97 to +104
if (error) {
toast.error(error.message || "An unknown error occurred", {
style: {
backgroundColor: "var(--destructive)",
color: "var(--destructive-foreground)",
},
});
}

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +48 to +105
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)",
},
});
}

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +90 to +92
} catch {
toast.error(`Server API error for ${method} ${path}`);
return null;

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
} 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.

Comment on lines +1 to +19
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"],
});

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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).

Comment on lines +370 to 372
} catch {
toast.error("Failed to save assistant message");
}

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
} 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.

Comment on lines +410 to 412
} catch {
toast.error("Failed to save assistant message");
}

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
} 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.

Comment on lines 70 to 82
} 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)",
},
});

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
} 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.

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 ref means consumers (e.g., react-hook-form’s register) 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 toast

We 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 logging

Same 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 output

Without 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:dark isn’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-duplicating cn across apps.

apps/ui/src/lib/utils.ts defines the same cn. 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 ruleType and ruleValue can 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: prefer User | null alias for readability.

Wrap in type Nullable<T> = T | null; then export 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.

Badge currently 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.

useUser redirects authenticated users to “/” while onSuccess pushes “/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.className directly; others wrap with cn + defaults. Consider a consistent pattern (e.g., default size via cn).

apps/playground/src/components/ai-elements/chain-of-thought.tsx (2)

84-110: Minor: unify Collapsible root usage (optional).

You render separate Collapsible roots 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?: boolean and conditionally render the connector.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 947575d and e406782.

⛔ Files ignored due to path filters (10)
  • apps/playground/public/favicon/android-chrome-192x192.png is excluded by !**/*.png
  • apps/playground/public/favicon/android-chrome-512x512.png is excluded by !**/*.png
  • apps/playground/public/favicon/apple-touch-icon.png is excluded by !**/*.png
  • apps/playground/public/favicon/favicon-16x16.png is excluded by !**/*.png
  • apps/playground/public/favicon/favicon-32x32.png is excluded by !**/*.png
  • apps/playground/public/favicon/favicon.ico is excluded by !**/*.ico
  • apps/playground/public/opengraph.png is excluded by !**/*.png
  • apps/playground/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/ui/public/next.svg is excluded by !**/*.svg
  • apps/ui/public/vercel.svg is 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.tsx
  • apps/ui/src/app/playground/playground-client.tsx
  • apps/ui/src/app/layout.tsx
  • apps/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.tsx
  • apps/ui/src/app/playground/playground-client.tsx
  • apps/ui/src/app/layout.tsx
  • apps/ui/src/components/Chat.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any in this TypeScript project unless absolutely necessary
Always use top-level import; never use require or dynamic imports

Files:

  • apps/ui/src/components/app-sidebar.tsx
  • apps/playground/src/components/ui/label.tsx
  • apps/playground/src/components/ui/skeleton.tsx
  • apps/playground/src/components/playground/chat-ui.tsx
  • apps/playground/src/components/ui/progress.tsx
  • apps/playground/src/lib/types.ts
  • apps/ui/src/app/playground/playground-client.tsx
  • apps/playground/src/lib/utils.ts
  • apps/playground/src/components/ui/badge.tsx
  • apps/playground/src/components/ui/button.tsx
  • apps/playground/src/components/ui/select.tsx
  • apps/playground/src/hooks/useCreateApiKey.ts
  • apps/playground/src/components/ui/avatar.tsx
  • apps/playground/src/hooks/useAutoApiKey.ts
  • apps/playground/src/components/ui/tooltip.tsx
  • apps/playground/src/components/ui/command.tsx
  • apps/playground/src/components/ai-elements/code-block.tsx
  • apps/playground/src/components/playground/chat-page-client.tsx
  • apps/playground/src/components/playground/auth-dialog.tsx
  • apps/playground/src/components/ai-elements/message.tsx
  • apps/playground/src/components/ui/checkbox.tsx
  • apps/playground/src/components/ui/separator.tsx
  • apps/playground/src/components/providers.tsx
  • apps/playground/src/components/playground/api-key-manager.tsx
  • apps/playground/src/components/credits/credits-display.tsx
  • apps/ui/src/app/layout.tsx
  • apps/playground/src/lib/server-api.ts
  • apps/playground/src/components/ai-elements/tool.tsx
  • apps/playground/src/components/playground/chat-sidebar.tsx
  • apps/playground/src/components/ui/popover.tsx
  • apps/playground/src/app/login/page.tsx
  • apps/playground/src/app/signup/page.tsx
  • apps/playground/src/components/ai-elements/branch.tsx
  • apps/playground/src/components/ui/dropdown-menu.tsx
  • apps/playground/src/components/ai-elements/sources.tsx
  • apps/ui/src/components/Chat.tsx
  • apps/playground/src/components/model-selector.tsx
  • apps/playground/src/components/provider-icons.tsx
  • apps/playground/src/hooks/useChats.ts
  • apps/playground/src/components/ai-elements/task.tsx
  • apps/playground/src/components/ai-elements/chain-of-thought.tsx
  • apps/playground/src/components/ui/carousel.tsx
  • apps/playground/src/components/ui/scroll-area.tsx
  • apps/playground/src/components/ui/form.tsx
  • apps/playground/src/hooks/useUser.ts
  • apps/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)

Comment on lines +2 to +3
"name": "",
"short_name": "",

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
"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".

Comment on lines +6 to +13
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
"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.

Comment on lines +97 to +104
if (error) {
toast.error(error.message || "An unknown error occurred", {
style: {
backgroundColor: "var(--destructive)",
color: "var(--destructive-foreground)",
},
});
}

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +109 to +137
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);
}

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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.

Comment on lines +64 to +105
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)",
},
});
}

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +73 to +88
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)",
},
});
},
});
}

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +94 to +108
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)",
},
});
},
});

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +115 to +128
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)",
},
});
},
});

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +1 to +4
import { cookies } from "next/headers";
import createFetchClient from "openapi-fetch";
import { toast } from "sonner";

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +71 to 82
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)",
},
});

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 environment

The unified local stack never injects PLAYGROUND_URL, so any code that reads process.env.PLAYGROUND_URL ends up with undefined when running docker compose -f infra/docker-compose.unified.local.yml …. The production compose already sets it (and .env.unified.example documents 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 scans

We repeatedly call providers.find inside 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.
Without role="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 the cn helper.
Line 4: We already ship the same cn implementation in apps/ui. Pulling it from a shared util (e.g., packages/shared or re-exporting from the UI lib) would keep future tweaks in one place.

apps/playground/src/app/page.tsx (1)

9-13: Drop the unused GatewayModel export.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 947575d and d420814.

⛔ Files ignored due to path filters (10)
  • apps/playground/public/favicon/android-chrome-192x192.png is excluded by !**/*.png
  • apps/playground/public/favicon/android-chrome-512x512.png is excluded by !**/*.png
  • apps/playground/public/favicon/apple-touch-icon.png is excluded by !**/*.png
  • apps/playground/public/favicon/favicon-16x16.png is excluded by !**/*.png
  • apps/playground/public/favicon/favicon-32x32.png is excluded by !**/*.png
  • apps/playground/public/favicon/favicon.ico is excluded by !**/*.ico
  • apps/playground/public/opengraph.png is excluded by !**/*.png
  • apps/playground/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/ui/public/next.svg is excluded by !**/*.svg
  • apps/ui/public/vercel.svg is 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 use any or as any in this TypeScript project unless absolutely necessary
Always use top-level import; never use require or dynamic imports

Files:

  • apps/playground/src/lib/types.ts
  • apps/ui/src/app/layout.tsx
  • apps/playground/src/components/playground/chat-ui.tsx
  • apps/playground/src/components/ui/label.tsx
  • apps/playground/src/components/ui/popover.tsx
  • apps/playground/src/components/ui/avatar.tsx
  • apps/playground/src/lib/utils.ts
  • apps/playground/src/components/ai-elements/code-block.tsx
  • apps/playground/src/components/ui/checkbox.tsx
  • apps/playground/src/lib/mapmodels.ts
  • apps/playground/src/components/ai-elements/web-preview.tsx
  • apps/playground/src/components/ui/carousel.tsx
  • apps/playground/src/components/ai-elements/conversation.tsx
  • apps/ui/src/components/app-sidebar.tsx
  • apps/playground/src/app/page.tsx
  • apps/playground/src/components/playground/auth-dialog.tsx
  • apps/playground/src/hooks/useCreateApiKey.ts
  • apps/playground/src/components/ai-elements/branch.tsx
  • apps/playground/src/components/playground/chat-sidebar.tsx
  • apps/ui/src/app/playground/playground-client.tsx
  • apps/playground/src/app/login/page.tsx
  • apps/playground/src/components/ui/badge.tsx
  • apps/playground/src/components/playground/api-key-manager.tsx
  • apps/ui/src/components/Chat.tsx
  • apps/playground/src/components/ai-elements/loader.tsx
  • apps/playground/src/components/ai-elements/message.tsx
  • apps/playground/src/app/signup/page.tsx
  • apps/playground/src/components/ui/sheet.tsx
  • apps/playground/src/components/ai-elements/prompt-input.tsx
  • apps/playground/src/lib/server-api.ts
  • apps/playground/src/components/provider-icons.tsx
  • apps/playground/src/components/ai-elements/reasoning.tsx
  • apps/playground/src/hooks/useUser.ts
  • apps/playground/src/components/ui/sidebar.tsx
  • apps/playground/src/components/ai-elements/context.tsx
  • apps/playground/src/components/credits/top-up-credits-dialog.tsx
  • apps/playground/src/components/playground/chat-page-client.tsx
  • apps/playground/src/app/layout.tsx
  • apps/playground/src/components/ui/providers-icons.tsx
  • apps/playground/src/components/providers.tsx
  • apps/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.tsx
  • apps/ui/src/components/app-sidebar.tsx
  • apps/ui/src/app/playground/playground-client.tsx
  • apps/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.tsx
  • apps/ui/src/components/app-sidebar.tsx
  • apps/ui/src/app/playground/playground-client.tsx
  • apps/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.md
  • infra/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.md
  • infra/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.md
  • infra/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.tsx
  • 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 (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)

Comment on lines +17 to 21
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"

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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).

Comment thread .github/test-split-docker.sh Outdated
Comment on lines 19 to 25
# 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"

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
# 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.

Comment on lines +35 to +48
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) => (

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +8 to +51
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}
/>
);
}

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +28 to +44
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}
/>
);
}

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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.

Comment thread infra/docker-compose.split.local.yml Outdated
Comment thread infra/docker-compose.split.yml
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.

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d420814 and c74792e.

📒 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.0 matches the placeholder strategy used elsewhere in the repo; no action needed.

packages/instrumentation/package.json (1)

3-3: Confirm the 0.0.0 version reset

Dropping 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.

Comment thread apps/gateway/package.json
Comment on lines +9 to +12
"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"

ghost Sep 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants