Skip to content

chore(playground): improve canvas generation UX - #2313

Merged
smakosh merged 8 commits into
theopenco:mainfrom
amelitoalcuitas:chore/canvas-enhancements
May 18, 2026
Merged

smakosh merged 8 commits into
theopenco:mainfrom
amelitoalcuitas:chore/canvas-enhancements

Conversation

@amelitoalcuitas

@amelitoalcuitas amelitoalcuitas commented May 17, 2026 •

Copy link
Copy Markdown
Contributor
image

Summary by CodeRabbit

  • New Features

    • Model preferences persist across sessions for chat, image, video, and canvas and are initialized from stored preferences (including server-side).
    • Canvas displays a skeleton while streamed generation establishes and better handles streaming lifecycle.
  • Bug Fixes

    • More robust error parsing for clearer generation failure messages.
    • API key handling accepts keys from request body, headers, or cookies and validates presence, returning a clear error when missing.
  • UI/UX Improvements

    • Reasoning sections in chat now default to collapsed.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 17, 2026 •

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9628c11e-ceae-4d6f-9126-7df53df58b41

📥 Commits

Reviewing files that changed from the base of the PR and between 637b90b and e665538.

📒 Files selected for processing (1)
  • apps/playground/src/app/api/canvas/generate/route.ts

Walkthrough

The PR adds cookie-backed model preference helpers and SSR wiring, refactors error-message parsing, makes the canvas generate API resolve API keys from body/header/cookies, migrates clients to cookie-based model persistence, and updates the canvas client for streaming-aware lifecycle, auth/project side-effects, and skeleton UIs. A chat UI small change collapses reasoning by default.

Changes

Canvas Playground Generation and Streaming

Layer / File(s) Summary
Error message parsing utilities
apps/playground/src/lib/utils.ts
getErrorMessage now accepts unknown, adds record-detection and string-normalization helpers, parses nested/JSON error payloads and Zod-like issues to extract messages.
Model preference cookie helpers
apps/playground/src/lib/model-preferences.ts
New helpers/constants: cookie names, decodeModelPreference, getModelPreferenceCookie, and setModelPreferenceCookie with a shared max-age.
SSR pages: read and pass initialModelPreference
apps/playground/src/app/canvas/page.tsx, apps/playground/src/app/image/page.tsx, apps/playground/src/app/playground-shell.tsx, apps/playground/src/app/video/page.tsx
Server pages now decode the relevant model preference cookie and pass initialModelPreference into their client components; cookie store reuse for last-used-project lookup.
Clients: cookie-backed model persistence
apps/playground/src/components/playground/chat-page-client.tsx, apps/playground/src/components/playground/image-page-client.tsx, apps/playground/src/components/playground/video-page-client.tsx, apps/playground/src/components/playground/canvas-page-client.tsx
Clients read/write model preferences via cookie helpers, accept optional initialModelPreference prop as fallback, remove localStorage usage, and adjust ensure-playground-key effects to scope by project.
API key resolution in canvas generation
apps/playground/src/app/api/canvas/generate/route.ts
POST derives prompt/model from JSON body, resolves finalApiKey via x-llmgateway-key header then playground key cookies, returns 400 if missing, and passes resolved key to createLLMGateway.
Canvas client: streaming lifecycle, auth, and skeleton UIs
apps/playground/src/components/playground/canvas-page-client.tsx
Introduces hasStreamingSpec/showGenerationLoading, skeleton components, getResponseErrorMessage helper, auth/project-side-effect to ensure playground key, clears generation state at start, one-time spec reset on first stream chunk, incremental editor updates, finalization sets streaming-established, and conditional skeleton rendering for editor/preview.
Chat reasoning default state
apps/playground/src/components/playground/chat-ui.tsx
Sets Reasoning component defaultOpen={false} so reasoning content is collapsed by default.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • smakosh
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is partially related to the changeset, referring to canvas generation improvements, but is overly broad and doesn't capture the primary technical changes (API key resolution, cookie-based model preferences, error handling improvements). Consider a more specific title like 'refactor(playground): migrate model preferences to cookies and improve canvas API key handling' to better reflect the scope of changes across multiple components and the core architectural improvements.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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/app/api/canvas/generate/route.ts (1)

34-55: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize and validate prompt/apiKey before use.

" " prompt and whitespace apiKey currently bypass checks and can trigger avoidable upstream failures. Trim first, then validate.

Suggested patch
-	const { prompt, model, apiKey } = body;
+	const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
+	const model = body.model;
+	const apiKey = typeof body.apiKey === "string" ? body.apiKey.trim() : undefined;
@@
-	if (!prompt) {
+	if (!prompt) {
 		return new Response(JSON.stringify({ error: "Missing prompt" }), {
 			status: 400,
 		});
 	}
@@
-	const finalApiKey = apiKey ?? headerApiKey ?? cookieApiKey;
+	const finalApiKey = apiKey || headerApiKey || cookieApiKey;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/app/api/canvas/generate/route.ts` around lines 34 - 55,
Trim and validate the incoming prompt and API key values before use: call
.trim() on the incoming prompt (the local variable prompt) and on
apiKey/headerApiKey/cookieApiKey before computing finalApiKey, then reject if
the trimmed prompt is empty or the trimmed finalApiKey is empty; update the
checks that currently return 400 to use the trimmedPrompt and trimmedFinalApiKey
and ensure you pass the trimmedPrompt (not the original) onward (references:
prompt, apiKey, headerApiKey, cookieApiKey, finalApiKey, trimmedPrompt).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/playground/src/components/playground/canvas-page-client.tsx`:
- Around line 241-251: The state initialization and persistence for
selectedModel currently read/write localStorage (variables: selectedModel,
setSelectedModel, CANVAS_MODEL_KEY, DEFAULT_CANVAS_MODEL); replace that logic
with cookie-backed storage so SSR can read the preference: on init, read the
cookie named by CANVAS_MODEL_KEY (fall back to DEFAULT_CANVAS_MODEL) and set
selectedModel; when changing model via setSelectedModel, write the value to the
same cookie (handle cookie read/write errors similar to the existing try/catch).
Also update the equivalent logic referenced around the other occurrence (the
code at the later snippet lines) to use the same cookie-based read/write
approach. Ensure cookie lifecycle (expires/path) follows repo standards for
user-settings cookies.
- Around line 288-313: Replace the inline fetch inside the useEffect with a
TanStack Query mutation: create a mutation using your api provider (e.g., const
ensureKeyMutation = api.playground.ensurePlaygroundKey.useMutation()) and call
ensureKeyMutation.mutate({ projectId: selectedProject.id }) instead of fetch
inside ensureKey; onSuccess set ensuredProjectRef.current = selectedProject.id
and handle errors via onError (instead of the empty catch). Keep the useEffect
to gate invocation by isAuthenticated/selectedOrganization/selectedProject but
remove raw fetch and the ensureKey async implementation, using the mutation's
mutate/mutateAsync and callbacks to manage side effects for the symbols
ensureKey, ensuredProjectRef, and the "/api/ensure-playground-key" behavior.

---

Outside diff comments:
In `@apps/playground/src/app/api/canvas/generate/route.ts`:
- Around line 34-55: Trim and validate the incoming prompt and API key values
before use: call .trim() on the incoming prompt (the local variable prompt) and
on apiKey/headerApiKey/cookieApiKey before computing finalApiKey, then reject if
the trimmed prompt is empty or the trimmed finalApiKey is empty; update the
checks that currently return 400 to use the trimmedPrompt and trimmedFinalApiKey
and ensure you pass the trimmedPrompt (not the original) onward (references:
prompt, apiKey, headerApiKey, cookieApiKey, finalApiKey, trimmedPrompt).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 136eedb1-4ae8-4171-b481-49b318ce928b

📥 Commits

Reviewing files that changed from the base of the PR and between 1e9e40a and 8e50734.

📒 Files selected for processing (4)
  • apps/playground/src/app/api/canvas/generate/route.ts
  • apps/playground/src/components/playground/canvas-page-client.tsx
  • apps/playground/src/components/playground/chat-ui.tsx
  • apps/playground/src/lib/utils.ts

Comment thread apps/playground/src/components/playground/canvas-page-client.tsx
Comment thread apps/playground/src/components/playground/canvas-page-client.tsx

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/playground/src/components/playground/canvas-page-client.tsx`:
- Around line 293-306: The ensureKey function currently treats any resolved
fetch as success and sets ensuredProjectRef.current regardless of status and
request ordering; change it to check the fetch response (response.ok) before
marking ensuredProjectRef.current = selectedProject.id, and add an ordering
guard (e.g., a local requestId or AbortController tied to ensureKey) so that
only the latest request's successful response can update ensuredProjectRef;
ensure you reference ensureKey, ensuredProjectRef, selectedProject.id and the
POST to "/api/ensure-playground-key" when implementing these checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 179b9efb-c36a-4d72-bc81-35f7a70976be

📥 Commits

Reviewing files that changed from the base of the PR and between 8e50734 and 2cee95f.

📒 Files selected for processing (10)
  • apps/playground/src/app/api/canvas/generate/route.ts
  • apps/playground/src/app/canvas/page.tsx
  • apps/playground/src/app/image/page.tsx
  • apps/playground/src/app/playground-shell.tsx
  • apps/playground/src/app/video/page.tsx
  • apps/playground/src/components/playground/canvas-page-client.tsx
  • apps/playground/src/components/playground/chat-page-client.tsx
  • apps/playground/src/components/playground/image-page-client.tsx
  • apps/playground/src/components/playground/video-page-client.tsx
  • apps/playground/src/lib/model-preferences.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/playground/src/app/api/canvas/generate/route.ts

Comment thread apps/playground/src/components/playground/canvas-page-client.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.

Caution

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

⚠️ Outside diff range comments (3)
apps/playground/src/components/playground/video-page-client.tsx (1)

242-269: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace the stale selectedProject check with a cancellation flag.

The comparison selectedProject.id === projectId doesn't prevent stale updates because projectId is captured before the async operation. A response from a previous project selection can still arrive after the user changes projects, causing ensuredProjectRef.current to be overwritten with an outdated project ID. Use a cancellation flag cleared in the effect's cleanup function instead.

Suggested fix
 useEffect(() => {
 	if (!isAuthenticated || !selectedProject) {
 		ensuredProjectRef.current = null;
 		return;
 	}
+	let cancelled = false;
 	const ensureKey = async () => {
 		if (!selectedOrganization) {
 			return;
 		}
 		const projectId = selectedProject.id;
 		if (ensuredProjectRef.current === projectId) {
 			return;
 		}
 		try {
 			const response = await fetch("/api/ensure-playground-key", {
 				method: "POST",
 				headers: { "Content-Type": "application/json" },
 				body: JSON.stringify({ projectId }),
 			});
-			if (response.ok && selectedProject.id === projectId) {
+			if (!cancelled && response.ok) {
 				ensuredProjectRef.current = projectId;
 			}
 		} catch {
 			// ignore
 		}
 	};
 	void ensureKey();
+	return () => {
+		cancelled = true;
+	};
 }, [isAuthenticated, selectedOrganization, selectedProject]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/components/playground/video-page-client.tsx` around lines
242 - 269, The effect's async ensureKey captures projectId and can race with
later selections, so replace the stale selectedProject.id check with a local
cancellation flag: inside the useEffect create let cancelled = false and set
cancelled = true in the cleanup, then in the async function ensureKey (and
before setting ensuredProjectRef.current) check if cancelled and return early;
keep existing guards (isAuthenticated, selectedOrganization) and refer to
ensuredProjectRef, ensureKey, selectedProject, selectedOrganization, and
isAuthenticated to locate the logic to change.
apps/playground/src/components/playground/image-page-client.tsx (1)

170-197: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use a cleanup flag to prevent stale async requests from updating the ref after project switches.

The response.ok && selectedProject.id === projectId check doesn't prevent updates from stale responses because both selectedProject and projectId are captured in the ensureKey closure when the effect runs. When a user switches projects mid-request, the old async operation completes with its captured values and still updates ensuredProjectRef.current with the outdated project ID.

Introduce a cancelled flag that the cleanup function sets to true, preventing stale requests from updating the ref:

Suggested fix
 useEffect(() => {
 	if (!isAuthenticated || !selectedProject) {
 		ensuredProjectRef.current = null;
 		return;
 	}
+	let cancelled = false;
 	const ensureKey = async () => {
 		if (!selectedOrganization) {
 			return;
 		}
 		const projectId = selectedProject.id;
 		if (ensuredProjectRef.current === projectId) {
 			return;
 		}
 		try {
 			const response = await fetch("/api/ensure-playground-key", {
 				method: "POST",
 				headers: { "Content-Type": "application/json" },
 				body: JSON.stringify({ projectId }),
 			});
-			if (response.ok && selectedProject.id === projectId) {
+			if (!cancelled && response.ok) {
 				ensuredProjectRef.current = projectId;
 			}
 		} catch {
 			// ignore
 		}
 	};
 	void ensureKey();
+	return () => {
+		cancelled = true;
+	};
 }, [isAuthenticated, selectedOrganization, selectedProject]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/components/playground/image-page-client.tsx` around lines
170 - 197, The effect in useEffect that runs ensureKey can let stale async
responses overwrite ensuredProjectRef.current; add a local cancelled flag in the
useEffect scope, set cancelled = true in the returned cleanup function, and
inside ensureKey check the flag before writing ensuredProjectRef.current (and
after awaiting fetch/response.ok). Update references in the closure (ensureKey,
ensuredProjectRef.current, selectedProject, selectedOrganization) so the async
callback returns early when cancelled to avoid updating the ref for a switched
project.
apps/playground/src/components/playground/chat-page-client.tsx (1)

916-946: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use AbortController or cleanup cancellation to prevent stale requests from overwriting state.

When selectedProject changes while the async request is pending, the older request can still complete and update ensuredProjectRef.current even after switching projects. The comparison selectedProject.id === projectId doesn't fully protect against this because both references exist in the same closure.

The suggested fix correctly prevents stale responses by cancelling pending operations in the cleanup function:

Suggested fix
 useEffect(() => {
 	// Reset ref when user logs out or project is unset
 	if (!isAuthenticated || !selectedProject) {
 		ensuredProjectRef.current = null;
 		return;
 	}
 
+	let cancelled = false;
 	const ensureKey = async () => {
 		if (!selectedOrganization) {
 			return;
 		}
 		const projectId = selectedProject.id;
 		// Skip if we've already ensured the key for this project
 		if (ensuredProjectRef.current === projectId) {
 			return;
 		}
 		try {
 			const response = await fetch("/api/ensure-playground-key", {
 				method: "POST",
 				headers: { "Content-Type": "application/json" },
 				body: JSON.stringify({ projectId }),
 			});
-			if (response.ok && selectedProject.id === projectId) {
+			if (!cancelled && response.ok) {
 				ensuredProjectRef.current = projectId;
 			}
 		} catch {
 			// ignore for now
 		}
 	};
 	void ensureKey();
+	return () => {
+		cancelled = true;
+	};
 }, [isAuthenticated, selectedOrganization, selectedProject]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/components/playground/chat-page-client.tsx` around lines
916 - 946, The ensureKey effect can let a stale fetch resolve and overwrite
ensuredProjectRef.current; update the useEffect that defines ensureKey to use an
AbortController (or a cancellation flag) and pass its signal to fetch, and in
the cleanup return call controller.abort() (or flip the flag) so any late
response is ignored; also ensure you check controller.signal.aborted (or the
flag) before writing ensuredProjectRef.current and keep references to
selectedProject.id via the captured projectId inside ensureKey to avoid
cross-request races (functions: useEffect, ensureKey, ensuredProjectRef,
selectedProject, selectedOrganization, isAuthenticated).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/playground/src/components/playground/chat-page-client.tsx`:
- Around line 916-946: The ensureKey effect can let a stale fetch resolve and
overwrite ensuredProjectRef.current; update the useEffect that defines ensureKey
to use an AbortController (or a cancellation flag) and pass its signal to fetch,
and in the cleanup return call controller.abort() (or flip the flag) so any late
response is ignored; also ensure you check controller.signal.aborted (or the
flag) before writing ensuredProjectRef.current and keep references to
selectedProject.id via the captured projectId inside ensureKey to avoid
cross-request races (functions: useEffect, ensureKey, ensuredProjectRef,
selectedProject, selectedOrganization, isAuthenticated).

In `@apps/playground/src/components/playground/image-page-client.tsx`:
- Around line 170-197: The effect in useEffect that runs ensureKey can let stale
async responses overwrite ensuredProjectRef.current; add a local cancelled flag
in the useEffect scope, set cancelled = true in the returned cleanup function,
and inside ensureKey check the flag before writing ensuredProjectRef.current
(and after awaiting fetch/response.ok). Update references in the closure
(ensureKey, ensuredProjectRef.current, selectedProject, selectedOrganization) so
the async callback returns early when cancelled to avoid updating the ref for a
switched project.

In `@apps/playground/src/components/playground/video-page-client.tsx`:
- Around line 242-269: The effect's async ensureKey captures projectId and can
race with later selections, so replace the stale selectedProject.id check with a
local cancellation flag: inside the useEffect create let cancelled = false and
set cancelled = true in the cleanup, then in the async function ensureKey (and
before setting ensuredProjectRef.current) check if cancelled and return early;
keep existing guards (isAuthenticated, selectedOrganization) and refer to
ensuredProjectRef, ensureKey, selectedProject, selectedOrganization, and
isAuthenticated to locate the logic to change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a091c957-183a-4688-8c11-da725f327378

📥 Commits

Reviewing files that changed from the base of the PR and between 2cee95f and 637b90b.

📒 Files selected for processing (5)
  • apps/playground/src/components/playground/canvas-page-client.tsx
  • apps/playground/src/components/playground/chat-page-client.tsx
  • apps/playground/src/components/playground/group-chat-client.tsx
  • apps/playground/src/components/playground/image-page-client.tsx
  • apps/playground/src/components/playground/video-page-client.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/playground/src/components/playground/canvas-page-client.tsx

@smakosh
smakosh added this pull request to the merge queue May 18, 2026
Merged via the queue into theopenco:main with commit 78290b2 May 18, 2026
9 checks passed
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