Skip to content

init: Openrouter implementation - #52

Open
mrabbani wants to merge 13 commits into
mainfrom
integrate/openrouter
Open

init: Openrouter implementation#52
mrabbani wants to merge 13 commits into
mainfrom
integrate/openrouter

Conversation

@mrabbani

@mrabbani mrabbani commented Apr 16, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added support for multiple AI providers (Google and OpenRouter) with easy provider selection
    • Introduced video generation capabilities with progress tracking and media library integration
    • Enhanced image optimization for try-on features
  • Improvements

    • Redesigned settings interface for unified credential management across AI providers
    • Improved visual styling and user interface consistency
    • Enhanced dashboard with better analytics and activity tracking
  • Bug Fixes

    • Fixed API error handling across different provider implementations

@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request extends the TryAura plugin with multi-provider AI support (adding OpenRouter alongside existing Google Gemini), introduces video generation with job tracking, refactors admin settings into a schema-driven interface, optimizes user-captured images, migrates stylesheets from SCSS to CSS with Tailwind, and normalizes code formatting across the codebase.

Changes

Cohort / File(s) Summary
Backend Provider & Generation Routing
inc/Rest/GenerateController.php, inc/Admin/Admin.php, inc/Admin/Enhancer.php, inc/WooCommerce/Frontend/TryOn.php
Added provider-aware request routing: OpenRouter generation method using /chat/completions endpoint with image-URL parts, fallback to Gemini, unified error handling via build_remote_error_response, request timeout constant (50s), and tryonNonce localization. Extended REST-exposed settings to include dual API keys and model labels per provider, defaulting provider to 'google'.
Backend Assets & Configuration
inc/Common/Assets.php, inc/WooCommerce/WooCommerce.php
Extended localized configuration with new Google video model (veo-3.1-generate-preview), OpenRouter provider section (Gemini image/video models), and provider-specific defaults. Replaced bundled CSS with inline stylesheet generation for products list toggle component.
Admin Settings UI – Provider Selection
src/admin/dashboard/pages/Settings/Gemini/GeminiSettings.tsx, src/admin/dashboard/pages/Settings/Gemini/index.tsx, src/admin/dashboard/pages/Settings/Gemini/registerModelSelectorField.tsx
Replaced manual Gemini-only settings UI with schema-driven provider-aware form. Added provider switcher, provider-specific API key validation (network checks against respective endpoints), model selector field that fetches from Gemini/OpenRouter APIs, and unified save structure persisting both providers' credentials/models in single google object.
Admin Enhancer – Video Generation Store
src/admin/enhancer/store/types.ts, src/admin/enhancer/store/constants.ts, src/admin/enhancer/store/actions.ts, src/admin/enhancer/store/reducer.ts, src/admin/enhancer/store/selectors.ts
Added video job state management: new VideoJobStatus type, four job-tracking fields (videoJobId, videoJobStatus, videoResultUrl, videoError), corresponding action creators and selectors, and reducer handlers.
Admin Enhancer – Video UI Components
src/admin/enhancer/PreviewSections/VideoProgress.tsx, src/admin/enhancer/PreviewSections/ConfigSettings.tsx, src/admin/enhancer/PreviewSections/GenerateVideoBtn.tsx, src/admin/enhancer/PreviewSections/DummyVideoConfigInputs.tsx
Introduced VideoProgress component for video generation status, preview, and save-to-library flow. Conditional video tab/config rendering based on Pro status (locked: !hasPro()). Video model selection via schema field and UI state management.
Admin Enhancer – Request/Response Handling
src/admin/enhancer/PreviewModal.tsx, src/admin/enhancer/EnhanceButton.tsx
Migrated from client-side @google/genai to REST endpoint (/tryaura/v1/generate/v1/image) using nonce headers. Image input now uses blobToOptimizedInlineData with JPEG preference, response extracts image field. Adjusted validation to require REST URL/nonces instead of API key.
Frontend Image Optimization
src/frontend/tryon/TryOnModal.tsx, src/utils/tryaura.ts
Added image optimization on capture/file upload: optimizeImageDataUrl scales images, optionally converts MIME type (preserving PNG), applies quality settings, and returns optimized data URL or original on guard failures. blobToOptimizedInlineData wraps blob→data URL conversion with optimization. Both functions use WordPress filters for parameter customization.
Admin Dashboard Layout & Shadow DOM
src/admin/dashboard/index.tsx
Dashboard now renders React app into shadow DOM attached to settings root, with stylesheet mirroring logic via MutationObserver to keep linked stylesheets in sync within shadow boundary.
Stylesheet Migration – CSS/Tailwind
src/base-tailwind.css, src/admin/enhancer/style.css, src/admin/enhancer/style.scss, src/admin/woocommerce-products-list/style.css, src/admin/woocommerce-products-list/style.scss, src/admin/dashboard/index.tsx, src/admin/enhancer/index.tsx, src/frontend/tryon/index.tsx, src/admin/product-video-gallery/index.tsx, src/admin/woocommerce-products-list/index.tsx, src/components/index.tsx, src/frontend/product-video/index.tsx
Migrated from SCSS to CSS with Tailwind @layer/@source/@theme directives. Expanded base Tailwind configuration with CSS variable resets, theme token mappings, and component-specific styles (modals, tooltips, toggles). Removed individual SCSS files and centralized theme management.
Template Markup Update
templates/products/tryon-switch.php
Refactored try-on toggle from Tailwind utility classes to semantic BEM-style markup with dedicated input (tryaura-try-on__input) and track (tryaura-try-on__track) elements for CSS-driven styling.
Shared Utilities & Data Store
src/utils/plugin-ui.ts, src/utils/router.ts, src/utils/tryaura.ts, src/data/ai-models/*, src/data/settings/*
Added plugin-UI component re-exports. Introduced image optimization utilities. Refactored AI models and settings Redux store for consistency (no functional changes). Whitespace/formatting normalization across all data selectors, actions, and reducer logic.
Components & Frontend UI – Formatting
src/components/*.tsx, src/admin/dashboard/pages/Dashboard/Components/*, src/admin/dashboard/pages/Settings/components/*, src/frontend/tryon/**/*.tsx, src/admin/enhancer/**/*.tsx, src/admin/product-video-gallery/**/*.tsx, src/admin/dashboard/utils/menu-fix.js
Comprehensive whitespace, spacing, and JSX formatting normalization: removed extraneous spaces in function signatures, type annotations, JSX attributes, method calls, and conditionals. Standardized parameter destructuring, call-site spacing, and arrow-function formatting across ~150+ file updates. No behavioral changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes


Possibly related PRs

  • PR #35: Introduced initial enhancer/generation, settings REST, and product-video infrastructure that this PR extends with OpenRouter provider and video-job flows.
  • PR #20: Established enhancer/try-on codepaths and REST generation logic that this PR refactors for provider routing and request/response handling.
  • PR #48: Modified REST arguments, permissions, and nonce/API key handling for WP.org compliance; overlaps with provider-aware generation and tryonNonce usage in this PR.

Poem

🐰 Hopping through providers with glee,
Google and OpenRouter in harmony,
Videos spin, images optimized bright,
Tailwind's shadow DOM shines in the night,
One schema to rule them all, you'll see!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'init: Openrouter implementation' is specific and directly related to the main changes in the PR, which add comprehensive OpenRouter support throughout the codebase including backend API integration, frontend UI for provider selection, settings management, and video generation features.

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

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

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

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (7)
inc/WooCommerce/Frontend/TryOn.php (1)

49-60: Provider value is passed but not used in the try-on frontend.

The backend enqueues provider via wp_localize_script (line 60), but the frontend window.tryAura interface declares the type on line 19 without ever referencing it in the component logic. If this is intentional for future use, add a clarifying comment in the type definition. Otherwise, consider removing it from the localized data to avoid passing unnecessary values.

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

In `@inc/WooCommerce/Frontend/TryOn.php` around lines 49 - 60, The localized
'provider' value is being added in TryOn.php via wp_localize_script under the
'tryAura' object but the frontend's window.tryAura interface (the try-on
component's type declaration) never uses it; either remove 'provider' from the
localized payload in TryOn.php (remove the 'provider' array key) or, if it's
intended for future use, add a short comment to the frontend's window.tryAura
interface explaining it's intentionally included for future/feature-flag use so
reviewers know it's deliberate; locate wp_localize_script in TryOn.php and the
window.tryAura interface in the frontend code to make the corresponding change.
src/admin/enhancer/PreviewSections/VideoProgress.tsx (1)

61-66: Consider surfacing save errors to the user.

When handleSaveToLibrary fails, the error is only logged to the console. The user sees the spinner stop but receives no feedback about the failure. Consider adding a local error state to display a retry option or error message.

♻️ Proposed fix to show save error to user
 const [ saving, setSaving ] = useState( false );
 const [ savedUrl, setSavedUrl ] = useState< string | null >( null );
+const [ saveError, setSaveError ] = useState< string | null >( null );

 // ... in handleSaveToLibrary:
+setSaveError( null );
 setSaving( true );
 try {
     // ...
     setSavedUrl( response.url );
 } catch ( err: any ) {
-    // Let user know the save failed but don't block the UI.
-    console.error( 'Failed to save video:', err );
+    setSaveError( err?.message || __( 'Failed to save video.', 'tryaura' ) );
 } finally {
     setSaving( false );
 }

Then render the error near the save button when saveError is set.

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

In `@src/admin/enhancer/PreviewSections/VideoProgress.tsx` around lines 61 - 66,
Add a local error state (e.g., saveError via useState) and update the catch
block inside handleSaveToLibrary to setSaveError(err?.message || String(err)) in
addition to the existing console.error, then clear saveError at the start of a
new save (before setSaving(true)). Render the saveError message and a retry
action/button next to the existing save UI so users see the failure and can
retry; ensure the retry invokes handleSaveToLibrary again and that successful
saves clear saveError.
inc/Rest/GenerateController.php (1)

278-292: Consider handling missing image in OpenRouter response.

When choices[0]['message']['images'] is empty or missing, $image remains null and the response returns successfully with 'image' => null. This differs from how a failed Gemini request would be handled (which checks for $data['error']).

Consider adding explicit error handling when no image is returned:

♻️ Proposed fix to handle missing image
 if ( ! empty( $choices[0]['message']['images'] ) ) {
     // Images returned as data URLs (base64).
     $image_url = $choices[0]['message']['images'][0];
     // Strip the data URL prefix to get raw base64.
     if ( preg_match( '/^data:image\/\w+;base64,(.+)$/', $image_url, $matches ) ) {
         $image = $matches[1];
     } else {
         $image = $image_url;
     }
 }

+if ( ! $image ) {
+    return new WP_REST_Response( array( 'error' => __( 'No image returned from OpenRouter.', 'tryaura' ) ), 400 );
+}
+
 if ( $image ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inc/Rest/GenerateController.php` around lines 278 - 292, The code currently
leaves $image as null when choices[0]['message']['images'] is absent, causing a
successful response with 'image' => null; modify the extraction logic around
$choices/$image (in GenerateController.php) to explicitly detect the missing
image case and return a failure consistent with the Gemini path: if no image is
found and $data['error'] is not present, set an appropriate error response (or
throw/return a WP_Error) with a clear message like "no image returned from
OpenRouter" instead of returning 'image' => null so callers receive an explicit
error state.
inc/Common/Assets.php (1)

130-183: Add duration parameter to google veo-3.1-generate-preview model for consistency.

The veo-3.1-generate-preview model under the google provider lacks the duration parameter, while the equivalent google/veo-3.1 model under openrouter includes it. The Gemini API supports duration configuration (specifying video lengths of 4, 6, or 8 seconds), so consider adding this parameter to maintain feature parity across providers.

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

In `@inc/Common/Assets.php` around lines 130 - 183, The google model entry
'veo-3.1-generate-preview' is missing a 'duration' parameter for configuring
video length; add a 'duration' key under its 'parameters' array (matching the
structure used for 'aspectRatio' and 'resolution') with 'supported' => true,
'locked' => false, a sensible 'default' (e.g., '4s' or numeric 4), and a
'values' array listing the allowed durations (4, 6, 8 seconds) as items with
'label', 'value', and 'locked' keys so it mirrors the 'duration' parameter used
by the openrouter 'google/veo-3.1' model and preserves consistency across
providers.
src/admin/enhancer/PreviewSections/VideoConfigInputs.tsx (2)

213-218: Consider adding loading state to the generate button.

While the UI switches to VideoProgress once generation starts, there's a brief moment where the button could be double-clicked. Consider adding disabled={isGenerating} or a loading indicator.

♻️ Proposed enhancement
 			<Button
 				onClick={ handleGenerate }
-				disabled={ ! videoModel }
+				disabled={ ! videoModel || isGenerating }
 			>
 				{ __( 'Generate Video', 'tryaura' ) }
 			</Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/enhancer/PreviewSections/VideoConfigInputs.tsx` around lines 213 -
218, The Generate Video button can be double-clicked before the UI switches to
VideoProgress; update the Button in VideoConfigInputs.tsx to use the generation
state (e.g., add disabled={ isGenerating || !videoModel } and/or show a loading
indicator) and ensure handleGenerate sets isGenerating true at start and false
on completion/error so VideoProgress and the button state stay synchronized;
reference the Button component, handleGenerate, videoModel, isGenerating and
VideoProgress to locate and wire up the state correctly.

42-43: aura in dependency array won't trigger re-renders on window changes.

The aura variable is read from window.tryAura outside the callback but included in dependencies. Since it's a window property, React won't detect changes. Either read it inside the callback or remove from deps and add an eslint disable comment.

♻️ Proposed fix - read inside callback
 	const handleGenerate = useCallback( async () => {
+		const aura = ( window as any ).tryAura;
+		const videoModel = aura?.videoModel || '';
+
 		setVideoError( null );
 		setVideoResultUrl( null );
 		setIsVideoBusy( true );

And remove lines 42-43 and aura from the dependency array.

Also applies to: 97-111

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

In `@src/admin/enhancer/PreviewSections/VideoConfigInputs.tsx` around lines 42 -
43, The code reads window.tryAura into a top-level const aura and then includes
that variable in hook dependency arrays, which won't update when window.tryAura
changes; move the read inside the relevant callbacks/hooks (e.g., inside the
useEffect/useCallback that currently references aura/videoModel) so you call
(window as any).tryAura (and derive videoModel) at runtime, then remove the
outer const aura and videoModel and drop them from the dependency arrays;
alternatively, if you intentionally depend on a static value, explicitly remove
them from deps with an eslint-disable-next-line comment. Ensure you update every
place the pattern appears (including the other block around lines 97–111) so
hooks read window.tryAura inside their bodies rather than relying on the outer
variables.
inc/Rest/VideoGenerateController.php (1)

594-596: Permission level edit_posts may be too permissive for video generation.

Video generation involves API costs. Consider whether upload_files (requires Author role) or edit_others_posts would be more appropriate to limit who can generate videos.

♻️ Suggested change
 	public function permissions_check() {
-		return current_user_can( 'edit_posts' );
+		return current_user_can( 'upload_files' );
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inc/Rest/VideoGenerateController.php` around lines 594 - 596,
permissions_check currently returns current_user_can('edit_posts'), which is
likely too permissive for API-costly video generation; update the function
(permissions_check) to check a more restrictive capability such as
'upload_files' or 'edit_others_posts' depending on your intent (limit to Authors
or users who can edit others' posts), i.e., replace the capability string passed
to current_user_can accordingly and ensure any related documentation or tests
reflect the new required capability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@inc/Admin/Enhancer.php`:
- Around line 49-57: PreviewModal.tsx's doGenerate always uses client-side
GoogleGenAI and thus fails when Enhancer.php sets apiKey to '' for provider ===
'openrouter'; update doGenerate to be provider-aware: if provider ===
'openrouter' call the server REST image-generation endpoint (same pattern as
generateVideo.ts) instead of instantiating GoogleGenAI, otherwise continue with
existing GoogleGenAI path, or alternatively explicitly disallow 'openrouter' for
image generation and surface a clear error; locate logic in
doGenerate/PreviewModal.tsx and reference the provider/apiKey values injected by
Enhancer.php.

In `@inc/Rest/GenerateController.php`:
- Around line 108-116: The code sets a single default image model
('gemini-2.5-flash-image') into $model regardless of provider, which will pass
an invalid Gemini slug to OpenRouter; change the logic so provider-specific
defaults are used: determine $provider first (from
$settings['google']['provider']), then set $model based on the provider (e.g.,
if $provider === 'openrouter' use an OpenRouter slug like
'google/gemini-2.5-flash-preview-05-20:generateImage', otherwise use the Gemini
default), and keep calling generate_via_openrouter($api_key, $model, ...) or
generate_via_gemini($api_key, $model, ...) as before so each backend always
receives an appropriate model identifier.

In `@inc/Rest/VideoGenerateController.php`:
- Around line 425-427: In VideoGenerateController.php update the block that
assigns $video_url so it verifies unsigned_urls is an array before indexing;
replace the condition in the method that contains the lines with a guard like:
check is_array($data['unsigned_urls']) (and keep !empty) before using [0], or
defensively use reset()/current() on the array; ensure the change references the
existing variables $status and $data['unsigned_urls'] and leaves behavior
unchanged for non-array values (e.g., skip assignment if not an array).
- Around line 532-547: The code builds $download_args with Authorization headers
but still calls download_url($video_url) which ignores headers; change the flow
so that when 'openrouter' === $provider you call wp_remote_get($video_url,
$download_args), check for is_wp_error() and HTTP response code via
wp_remote_retrieve_response_code(), retrieve the body with
wp_remote_retrieve_body(), write it to a temp file created with wp_tempnam() (or
tmpfile pattern) and set $tmp_file to that path, and only fall back to
download_url($video_url, 300) for non-authenticated providers; ensure errors
return new WP_Error consistent with the existing error handling.

In `@src/admin/dashboard/pages/Settings/Gemini/GeminiSettings.tsx`:
- Around line 227-229: The helpText constant in GeminiSettings.tsx currently
uses the placeholder 'API key ?' for both branches; update helpText (in the
GeminiSettings component) to either a single shared descriptive string (e.g.,
"Enter your API key") or provide distinct, meaningful messages for each provider
by using the isOpenRouter condition to set provider-specific guidance (e.g., one
message referencing OpenRouter instructions and another for Google/other Gemini
providers), and ensure the chosen text is passed through __() for localization.
- Around line 201-208: The translation call uses a template literal which breaks
i18n extraction; replace the template literal in the toast.success call and use
sprintf with a translatable string instead: call toast.success(sprintf(__(' %s
API settings saved successfully!', 'tryaura'), providerLabel)) (keeping
providerLabel and isOpenRouter logic unchanged) so the string passed to __() is
a static literal and providerLabel is injected via sprintf.

In `@src/admin/enhancer/PreviewSections/VideoConfigInputs.tsx`:
- Line 197: The Tailwind arbitrary width class on the span in VideoConfigInputs
(className containing "w-[500]") is invalid because it lacks units; update the
className on the span in VideoConfigInputs.tsx to use a valid Tailwind arbitrary
value (e.g., replace "w-[500]" with "w-[500px]") or swap to a standard Tailwind
width utility (like "w-64"/"w-96") depending on the intended width so the style
is applied correctly.

---

Nitpick comments:
In `@inc/Common/Assets.php`:
- Around line 130-183: The google model entry 'veo-3.1-generate-preview' is
missing a 'duration' parameter for configuring video length; add a 'duration'
key under its 'parameters' array (matching the structure used for 'aspectRatio'
and 'resolution') with 'supported' => true, 'locked' => false, a sensible
'default' (e.g., '4s' or numeric 4), and a 'values' array listing the allowed
durations (4, 6, 8 seconds) as items with 'label', 'value', and 'locked' keys so
it mirrors the 'duration' parameter used by the openrouter 'google/veo-3.1'
model and preserves consistency across providers.

In `@inc/Rest/GenerateController.php`:
- Around line 278-292: The code currently leaves $image as null when
choices[0]['message']['images'] is absent, causing a successful response with
'image' => null; modify the extraction logic around $choices/$image (in
GenerateController.php) to explicitly detect the missing image case and return a
failure consistent with the Gemini path: if no image is found and $data['error']
is not present, set an appropriate error response (or throw/return a WP_Error)
with a clear message like "no image returned from OpenRouter" instead of
returning 'image' => null so callers receive an explicit error state.

In `@inc/Rest/VideoGenerateController.php`:
- Around line 594-596: permissions_check currently returns
current_user_can('edit_posts'), which is likely too permissive for API-costly
video generation; update the function (permissions_check) to check a more
restrictive capability such as 'upload_files' or 'edit_others_posts' depending
on your intent (limit to Authors or users who can edit others' posts), i.e.,
replace the capability string passed to current_user_can accordingly and ensure
any related documentation or tests reflect the new required capability.

In `@inc/WooCommerce/Frontend/TryOn.php`:
- Around line 49-60: The localized 'provider' value is being added in TryOn.php
via wp_localize_script under the 'tryAura' object but the frontend's
window.tryAura interface (the try-on component's type declaration) never uses
it; either remove 'provider' from the localized payload in TryOn.php (remove the
'provider' array key) or, if it's intended for future use, add a short comment
to the frontend's window.tryAura interface explaining it's intentionally
included for future/feature-flag use so reviewers know it's deliberate; locate
wp_localize_script in TryOn.php and the window.tryAura interface in the frontend
code to make the corresponding change.

In `@src/admin/enhancer/PreviewSections/VideoConfigInputs.tsx`:
- Around line 213-218: The Generate Video button can be double-clicked before
the UI switches to VideoProgress; update the Button in VideoConfigInputs.tsx to
use the generation state (e.g., add disabled={ isGenerating || !videoModel }
and/or show a loading indicator) and ensure handleGenerate sets isGenerating
true at start and false on completion/error so VideoProgress and the button
state stay synchronized; reference the Button component, handleGenerate,
videoModel, isGenerating and VideoProgress to locate and wire up the state
correctly.
- Around line 42-43: The code reads window.tryAura into a top-level const aura
and then includes that variable in hook dependency arrays, which won't update
when window.tryAura changes; move the read inside the relevant callbacks/hooks
(e.g., inside the useEffect/useCallback that currently references
aura/videoModel) so you call (window as any).tryAura (and derive videoModel) at
runtime, then remove the outer const aura and videoModel and drop them from the
dependency arrays; alternatively, if you intentionally depend on a static value,
explicitly remove them from deps with an eslint-disable-next-line comment.
Ensure you update every place the pattern appears (including the other block
around lines 97–111) so hooks read window.tryAura inside their bodies rather
than relying on the outer variables.

In `@src/admin/enhancer/PreviewSections/VideoProgress.tsx`:
- Around line 61-66: Add a local error state (e.g., saveError via useState) and
update the catch block inside handleSaveToLibrary to setSaveError(err?.message
|| String(err)) in addition to the existing console.error, then clear saveError
at the start of a new save (before setSaving(true)). Render the saveError
message and a retry action/button next to the existing save UI so users see the
failure and can retry; ensure the retry invokes handleSaveToLibrary again and
that successful saves clear saveError.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 63f290b0-78a2-4023-8d74-ac44dde13530

📥 Commits

Reviewing files that changed from the base of the PR and between 99faea3 and 86d033b.

⛔ Files ignored due to path filters (1)
  • src/admin/dashboard/pages/Settings/assets/openrouterLogo.svg is excluded by !**/*.svg
📒 Files selected for processing (19)
  • inc/Admin/Enhancer.php
  • inc/Common/Assets.php
  • inc/DependencyManagement/Providers/RestServiceProvider.php
  • inc/Rest/GenerateController.php
  • inc/Rest/VideoGenerateController.php
  • inc/WooCommerce/Frontend/TryOn.php
  • src/admin/dashboard/pages/Settings/Gemini/GeminiSettings.tsx
  • src/admin/dashboard/pages/Settings/Gemini/index.tsx
  • src/admin/enhancer/PreviewSections/ConfigSettings.tsx
  • src/admin/enhancer/PreviewSections/VideoConfigInputs.tsx
  • src/admin/enhancer/PreviewSections/VideoProgress.tsx
  • src/admin/enhancer/index.tsx
  • src/admin/enhancer/store/actions.ts
  • src/admin/enhancer/store/constants.ts
  • src/admin/enhancer/store/reducer.ts
  • src/admin/enhancer/store/selectors.ts
  • src/admin/enhancer/store/types.ts
  • src/admin/enhancer/utils/generateVideo.ts
  • src/frontend/tryon/index.tsx

Comment thread inc/Admin/Enhancer.php
Comment on lines +108 to +116
$provider = isset( $settings['google']['provider'] ) ? $settings['google']['provider'] : 'google';
$api_key = isset( $settings['google']['apiKey'] ) ? $settings['google']['apiKey'] : '';
$model = isset( $settings['google']['imageModel'] ) && ! empty( $settings['google']['imageModel'] ) ? $settings['google']['imageModel'] : 'gemini-2.5-flash-image';

$model = isset( $settings['google']['imageModel'] ) && ! empty( $settings['google']['imageModel'] ) ? $settings['google']['imageModel'] : 'gemini-2.5-flash-image';
if ( 'openrouter' === $provider ) {
return $this->generate_via_openrouter( $api_key, $model, $prompt, $ref_images, $request );
}

return $this->generate_via_gemini( $api_key, $model, $prompt, $ref_images, $request );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Model fallback may be incorrect for OpenRouter provider.

The $model variable defaults to 'gemini-2.5-flash-image' (line 110), which is a Google/Gemini model identifier. If the provider is 'openrouter' but no imageModel is configured, this fallback will be passed to OpenRouter, which expects a different model slug (e.g., 'google/gemini-2.5-flash-preview-05-20:generateImage').

Consider using provider-specific defaults:

🐛 Proposed fix for provider-specific model defaults
 $settings = get_option( 'tryaura_settings', array() );
 $provider = isset( $settings['google']['provider'] ) ? $settings['google']['provider'] : 'google';
 $api_key  = isset( $settings['google']['apiKey'] ) ? $settings['google']['apiKey'] : '';
-$model    = isset( $settings['google']['imageModel'] ) && ! empty( $settings['google']['imageModel'] ) ? $settings['google']['imageModel'] : 'gemini-2.5-flash-image';
+
+$default_model = 'openrouter' === $provider
+    ? 'google/gemini-2.5-flash-preview-05-20:generateImage'
+    : 'gemini-2.5-flash-image';
+$model = isset( $settings['google']['imageModel'] ) && ! empty( $settings['google']['imageModel'] )
+    ? $settings['google']['imageModel']
+    : $default_model;

 if ( 'openrouter' === $provider ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inc/Rest/GenerateController.php` around lines 108 - 116, The code sets a
single default image model ('gemini-2.5-flash-image') into $model regardless of
provider, which will pass an invalid Gemini slug to OpenRouter; change the logic
so provider-specific defaults are used: determine $provider first (from
$settings['google']['provider']), then set $model based on the provider (e.g.,
if $provider === 'openrouter' use an OpenRouter slug like
'google/gemini-2.5-flash-preview-05-20:generateImage', otherwise use the Gemini
default), and keep calling generate_via_openrouter($api_key, $model, ...) or
generate_via_gemini($api_key, $model, ...) as before so each backend always
receives an appropriate model identifier.

Comment thread inc/Rest/VideoGenerateController.php Outdated
Comment on lines +425 to +427
if ( 'completed' === $status && ! empty( $data['unsigned_urls'] ) ) {
$video_url = $data['unsigned_urls'][0];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add array type check before accessing unsigned_urls[0].

The code checks !empty($data['unsigned_urls']) but doesn't verify it's an array before accessing index [0].

🛠️ Proposed fix
-		if ( 'completed' === $status && ! empty( $data['unsigned_urls'] ) ) {
+		if ( 'completed' === $status && ! empty( $data['unsigned_urls'] ) && is_array( $data['unsigned_urls'] ) ) {
 			$video_url = $data['unsigned_urls'][0];
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inc/Rest/VideoGenerateController.php` around lines 425 - 427, In
VideoGenerateController.php update the block that assigns $video_url so it
verifies unsigned_urls is an array before indexing; replace the condition in the
method that contains the lines with a guard like: check
is_array($data['unsigned_urls']) (and keep !empty) before using [0], or
defensively use reset()/current() on the array; ensure the change references the
existing variables $status and $data['unsigned_urls'] and leaves behavior
unchanged for non-array values (e.g., skip assignment if not an array).

Comment thread inc/Rest/VideoGenerateController.php Outdated
Comment on lines +532 to +547
$download_args = array( 'timeout' => 300 );

if ( 'openrouter' === $provider ) {
$download_args['headers'] = array(
'Authorization' => 'Bearer ' . $api_key,
);
} elseif ( strpos( $video_url, 'googleapis.com' ) !== false && ! empty( $api_key ) ) {
// Append API key for Gemini video URIs.
$separator = ( strpos( $video_url, '?' ) !== false ) ? '&' : '?';
$video_url = $video_url . $separator . 'key=' . $api_key;
}

$tmp_file = download_url( $video_url, 300 );

if ( is_wp_error( $tmp_file ) ) {
return new WP_Error( 'download_failed', $tmp_file->get_error_message(), array( 'status' => 500 ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

$download_args with authorization headers is defined but never used.

The $download_args array containing the Authorization header for OpenRouter is prepared but never passed to download_url(). This means authenticated video downloads for OpenRouter will fail.

🐛 Proposed fix - use wp_remote_get instead

download_url() doesn't support custom headers. Use wp_remote_get() to fetch with headers, then save to temp file:

-		$download_args = array( 'timeout' => 300 );
-
-		if ( 'openrouter' === $provider ) {
-			$download_args['headers'] = array(
-				'Authorization' => 'Bearer ' . $api_key,
-			);
-		} elseif ( strpos( $video_url, 'googleapis.com' ) !== false && ! empty( $api_key ) ) {
+		if ( strpos( $video_url, 'googleapis.com' ) !== false && ! empty( $api_key ) ) {
 			// Append API key for Gemini video URIs.
 			$separator = ( strpos( $video_url, '?' ) !== false ) ? '&' : '?';
 			$video_url = $video_url . $separator . 'key=' . $api_key;
 		}
 
-		$tmp_file = download_url( $video_url, 300 );
+		if ( 'openrouter' === $provider ) {
+			// OpenRouter requires auth header - download manually.
+			$response = wp_remote_get(
+				$video_url,
+				array(
+					'timeout' => 300,
+					'headers' => array( 'Authorization' => 'Bearer ' . $api_key ),
+				)
+			);
+
+			if ( is_wp_error( $response ) ) {
+				return new WP_Error( 'download_failed', $response->get_error_message(), array( 'status' => 500 ) );
+			}
+
+			$tmp_file = wp_tempnam( 'tryaura-video' );
+			file_put_contents( $tmp_file, wp_remote_retrieve_body( $response ) );
+		} else {
+			$tmp_file = download_url( $video_url, 300 );
+		}
📝 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
$download_args = array( 'timeout' => 300 );
if ( 'openrouter' === $provider ) {
$download_args['headers'] = array(
'Authorization' => 'Bearer ' . $api_key,
);
} elseif ( strpos( $video_url, 'googleapis.com' ) !== false && ! empty( $api_key ) ) {
// Append API key for Gemini video URIs.
$separator = ( strpos( $video_url, '?' ) !== false ) ? '&' : '?';
$video_url = $video_url . $separator . 'key=' . $api_key;
}
$tmp_file = download_url( $video_url, 300 );
if ( is_wp_error( $tmp_file ) ) {
return new WP_Error( 'download_failed', $tmp_file->get_error_message(), array( 'status' => 500 ) );
if ( strpos( $video_url, 'googleapis.com' ) !== false && ! empty( $api_key ) ) {
// Append API key for Gemini video URIs.
$separator = ( strpos( $video_url, '?' ) !== false ) ? '&' : '?';
$video_url = $video_url . $separator . 'key=' . $api_key;
}
if ( 'openrouter' === $provider ) {
// OpenRouter requires auth header - download manually.
$response = wp_remote_get(
$video_url,
array(
'timeout' => 300,
'headers' => array( 'Authorization' => 'Bearer ' . $api_key ),
)
);
if ( is_wp_error( $response ) ) {
return new WP_Error( 'download_failed', $response->get_error_message(), array( 'status' => 500 ) );
}
$tmp_file = wp_tempnam( 'tryaura-video' );
file_put_contents( $tmp_file, wp_remote_retrieve_body( $response ) );
} else {
$tmp_file = download_url( $video_url, 300 );
}
if ( is_wp_error( $tmp_file ) ) {
return new WP_Error( 'download_failed', $tmp_file->get_error_message(), array( 'status' => 500 ) );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inc/Rest/VideoGenerateController.php` around lines 532 - 547, The code builds
$download_args with Authorization headers but still calls
download_url($video_url) which ignores headers; change the flow so that when
'openrouter' === $provider you call wp_remote_get($video_url, $download_args),
check for is_wp_error() and HTTP response code via
wp_remote_retrieve_response_code(), retrieve the body with
wp_remote_retrieve_body(), write it to a temp file created with wp_tempnam() (or
tmpfile pattern) and set $tmp_file to that path, and only fall back to
download_url($video_url, 300) for non-authenticated providers; ensure errors
return new WP_Error consistent with the existing error handling.

Comment thread src/admin/dashboard/pages/Settings/Gemini/GeminiSettings.tsx Outdated
Comment thread src/admin/dashboard/pages/Settings/Gemini/GeminiSettings.tsx Outdated
Comment thread src/admin/enhancer/PreviewSections/VideoConfigInputs.tsx Outdated
Aunshon and others added 12 commits April 16, 2026 17:44
- Removed SCSS files and replaced them with CSS files across various components for consistency.
- Updated base-tailwind.css to include new styles and Shadow DOM workarounds.
- Added OpenRouter as a selectable AI provider for image and video generation, enhancing the existing Gemini integration.
- Implemented server-side video generation with unified REST endpoints for both providers.
- Updated settings UI to use @wedevs/plugin-ui for a more streamlined user experience.
- Added new utility functions and state management for video job tracking and orchestration.
- Ensured backward compatibility for existing Gemini users while introducing new provider options.
… components

- Standardized spacing and formatting in ImagePreview, UploadImage, UseCamera, and Index components.
- Updated function parameter formatting for better readability.
- Improved JSX syntax by removing unnecessary spaces and aligning props.
- Enhanced user experience by ensuring consistent button behavior and error handling.
- Cleaned up type definitions and imports in utility files for better maintainability.
feat: Refactor styles and integrate OpenRouter as an AI provider

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (5)
src/base-tailwind.css (1)

63-63: @import placed after other rules relies on PostCSS inlining.

Per CSS spec, @import must precede all other at-rules except @charset and @layer; browsers ignore later ones. It works here only because Tailwind/PostCSS inlines imports at build time. Move this import near the top (with the tailwindcss/theme.css import) so the stylesheet remains valid if ever processed without the Tailwind build step.

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

In `@src/base-tailwind.css` at line 63, Move the `@import`
"../../plugin-ui/dist/styles.css"; statement so it appears near the top of
src/base-tailwind.css alongside the existing `@import` "tailwindcss/theme.css";
(i.e., before any other rules or at-rules except `@charset/`@layer) to ensure it
doesn't rely on PostCSS inlining and remains valid in browsers or other
processors that enforce the CSS `@import` ordering.
src/admin/enhancer/PreviewSections/ConfigSettings.tsx (1)

13-66: Minor: cache hasPro() and type the component props.

hasPro() is called three times (Lines 35, 55, 59); hoist it to a single const isPro = hasPro(); at the top of the component so the renders read consistently and you avoid repeated global lookups. Also, since this is a .tsx file, consider giving the component explicit prop types instead of implicit-any:

♻️ Suggested change
-function ConfigSettings({ doGenerate, className = '' }) {
+type ConfigSettingsProps = {
+	doGenerate: () => void;
+	className?: string;
+};
+
+function ConfigSettings({ doGenerate, className = '' }: ConfigSettingsProps) {
+	const isPro = hasPro();
 	const { activeTab, isBusy, isThumbnailMode } = useSelect((select) => {
 		...
 	}, []);
@@
 		{
 			label: __('Generate Video', 'tryaura'),
 			value: 'video',
 			disabled: isBusy,
-			locked: !hasPro(),
+			locked: !isPro,
 		},
@@
-				{activeTab === 'video' && !hasPro() && (
+				{activeTab === 'video' && !isPro && (
 					<DummyVideoConfigInputs />
 				)}
 
-				{activeTab === 'video' && hasPro() && (
+				{activeTab === 'video' && isPro && (
 					<Slot
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/enhancer/PreviewSections/ConfigSettings.tsx` around lines 13 - 66,
Cache the hasPro() result and add explicit prop typing for the ConfigSettings
component: hoist a const isPro = hasPro() near the top of ConfigSettings and
replace all hasPro() calls (used in the tabs locked flag and the conditional
renders for video: DummyVideoConfigInputs vs Slot) with isPro to avoid repeated
global lookups; also declare an explicit TS type or interface for the component
props (including doGenerate and optional className) and type the ConfigSettings
function signature accordingly so the component is strongly typed in this .tsx
file.
src/utils/plugin-ui.ts (1)

1-15: Prefer @ts-expect-error + real type for plugin-ui re-exports.

@ts-ignore on Line 1 silently swallows all current and future errors from ../../../plugin-ui/dist/index.js, and SettingsElement = Record<string, any> gives up type safety for every consumer of the settings UI. If plugin-ui/dist doesn't ship types today, consider a local .d.ts shim and @ts-expect-error so the suppression disappears automatically once types land.

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

In `@src/utils/plugin-ui.ts` around lines 1 - 15, Replace the blanket suppression
and weak type: remove the leading // `@ts-ignore` and instead use //
`@ts-expect-error` when re-exporting from '../../../plugin-ui/dist/index.js' so
TypeScript will surface future typings, and add a local type shim (.d.ts) that
declares the actual exported types (Settings, Button, Notice, NoticeTitle,
Select, SelectContent, SelectItem, SelectTrigger, SelectValue, useSettings) if
the package lacks types today; then replace SettingsElement = Record<string,
any> with a more precise interface/alias that matches the real Settings type you
declare in the shim (e.g., export type SettingsElement = Settings or a narrowed
shape) so consumers get proper type safety.
src/admin/woocommerce-products-list/style.css (1)

16-26: Consider adding clip-path alongside deprecated clip.

The clip property is deprecated (stylelint property-no-deprecated). For a more robust visually-hidden pattern, pair it with clip-path: inset(50%) so modern browsers use the non-deprecated property:

♻️ Proposed refactor
 .tryaura-try-on__input {
 	position: absolute;
 	width: 1px;
 	height: 1px;
 	padding: 0;
 	margin: -1px;
 	overflow: hidden;
 	clip: rect(0, 0, 0, 0);
+	clip-path: inset(50%);
 	white-space: nowrap;
 	border: 0;
 }

The Stylelint @source warnings on lines 1–2 are false positives for Tailwind v4 at-rules and can be ignored (or suppressed via stylelint-config-tailwindcss/scss/at-rule-no-unknown ignore entries).

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

In `@src/admin/woocommerce-products-list/style.css` around lines 16 - 26, The
visually-hidden utility .tryaura-try-on__input currently uses the deprecated
clip property; update the rule for modern browsers by adding a non-deprecated
clip-path (e.g., clip-path: inset(50%)) alongside the existing clip fallback so
older browsers still work—modify the CSS rule for .tryaura-try-on__input to
include clip-path: inset(50%) while retaining the current clip: rect(...) line.
src/admin/enhancer/PreviewModal.tsx (1)

164-175: Dead code: google is never consumed.

Line 166 derives google from settings, but nothing in doGenerate references it afterwards (provider/model routing happens server-side now). Safe to drop.

♻️ Proposed cleanup
 			const aura = (window as any)?.tryAura;
-			const google = settings?.[aura?.optionKey]?.google;
 			const restUrl = aura?.restUrl;

If you still need settings/optionKey for validation (e.g. to early-fail when no provider/key is configured), do the check explicitly instead of leaving an unused binding. Also consider dropping settings/defaultImageModel from the useSelect return since neither is used in this component anymore.

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

In `@src/admin/enhancer/PreviewModal.tsx` around lines 164 - 175, Remove the dead
local binding `google` (derived as const google =
settings?.[aura?.optionKey]?.google) in PreviewModal.tsx and also drop unused
`settings`/`defaultImageModel` from the `useSelect` return that feed this
component; if you still need to validate provider/keys, replace the unused
binding with an explicit check against the provider/key (e.g. inspect settings
or aura.optionKey and assert the required provider value) inside `doGenerate` or
the component init instead of keeping an unused `google` variable so the linter
no longer flags dead code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@inc/Admin/Admin.php`:
- Around line 82-96: The REST schema and sanitize_settings path accept any
string for the provider, allowing typos/whitespace to bypass checks; update the
Admin.php provider handling and the sanitize_settings function to normalize and
validate provider values: trim() and strtolower() the incoming provider, then
check against a canonical allowlist (e.g., 'openrouter', 'gemini', 'openai',
etc.) and default or reject unknown values; also add an enum of allowed
providers to the REST schema for the provider field so the API layer enforces
valid values, and ensure GenerateController (where 'openrouter' === $provider is
used) and Enhancer logic consume the normalized provider value rather than raw
input.

In `@inc/Common/Assets.php`:
- Around line 267-349: The defaultOpenRouterVideoModel value
('bytedance/seedance-2.0-fast') has no matching entry in the
aiProviders['openrouter'] map (only 'google/veo-3.1' exists), causing lookups in
getDefaultModelsForProvider to return undefined; fix by either registering a
corresponding bytedance/seedance-2.0-fast entry under aiProviders['openrouter']
with appropriate capabilities/parameters (matching the structure used for
'google/veo-3.1') or change defaultOpenRouterVideoModel to an existing key such
as 'google/veo-3.1' so the settings UI can resolve parameters and locks
correctly.

In `@inc/WooCommerce/WooCommerce.php`:
- Around line 194-196: The checked-state rule moves the knob always to the right
via transform: translateX(14px) which breaks RTL; update the CSS for
.tryaura-try-on__input:checked + .tryaura-try-on__track::after to use a
logical/RTL-aware approach — e.g. replace the fixed translateX with a CSS
variable (--try-on-translate: 14px) and set that variable to -14px for RTL via a
[dir="rtl"] .tryaura-try-on__input:checked + .tryaura-try-on__track::after
override, or add a dedicated RTL rule that uses transform: translateX(-14px) so
the knob animates in the correct direction.

In `@src/admin/dashboard/pages/Settings/Gemini/GeminiSettings.tsx`:
- Around line 111-137: validateApiKey currently treats the OpenRouter models GET
as authoritative but that endpoint is public; update validateApiKey (and the
provider === 'openrouter' branch) to call an authenticated OpenRouter endpoint
that actually requires a valid bearer token (for example POST to
/api/v1/chat/completions or an authenticated POST to models) using the
Authorization: Bearer header and a minimal valid request payload, then treat the
response status (e.g., 200/201 ok vs 401/403) as the validation result; ensure
you await the POST response and return res.ok (or false on catch) so invalid
keys fail validation.
- Around line 531-547: The code builds nextGoogleSettings and unconditionally
assigns apiKey which causes OpenRouter keys to be stored in the generic apiKey
field; change the assignment so apiKey is only set when provider === 'gemini'
(or whatever Gemini provider constant you use) and otherwise set apiKey to an
empty string/undefined, leaving openrouterApiKey populated only when provider is
OpenRouter; update the object construction in GeminiSettings.tsx
(nextGoogleSettings, provider, apiKey, openrouterApiKey) to follow the
conditional persistence pattern used in Enhancer.php so the generic apiKey is
never populated with OpenRouter credentials.

In `@src/admin/dashboard/pages/Settings/Gemini/registerModelSelectorField.tsx`:
- Around line 248-325: The effect currently calls runFetch on every apiKey
change causing partial-key requests and stale responses; debounce the automatic
fetch trigger (use a short timeout or a debounced apiKey value) in the useEffect
that watches apiKey/provider/modelType and only call runFetch after the debounce
delay, and also harden runFetch by capturing the computed signature (const
signature = `${provider}:${modelType}:${trimmedApiKey}`) into a local variable
at the start of the try block and verify that lastFetchSignature.current still
equals that signature before applying results (setOptions, updateValue,
setErrorMessage) so out-of-order responses are ignored; keep the existing force
path for manual fetches.

In `@src/admin/enhancer/PreviewModal.tsx`:
- Around line 277-289: The frontend is hardcoding data:image/png when creating
the generatedUrl which can mislabel non-PNG bytes; update the backend
(generate_via_gemini, generate_via_openrouter, and
extract_openrouter_image_data) to return the original mimeType (e.g., mime_type
or mimeType) alongside image, then in the PreviewModal use that mime to build
the data URL (use data:${mime};base64,${image}) before calling setGeneratedUrl
so fetch(...).blob() and wp/v2/media receive the correct Content-Type and file
extension; ensure any places referencing data64 or setGeneratedUrl accept the
new payload shape.

In `@src/admin/enhancer/PreviewSections/VideoProgress.tsx`:
- Around line 42-70: The save flow in handleSaveToLibrary currently only logs
errors to console so users get no feedback; add a saveError state (e.g.,
saveError and setSaveError) and clear it before the request, set a descriptive
message in the catch block (instead of only console.error) and retain existing
setSaving/setSavedUrl behavior; ensure saveError is cleared on success (after
setSavedUrl) and render the saveError message in the component UI near the Save
button so users see the failure and can retry.
- Around line 49-61: Create a new VideoController and register a POST REST route
for '/tryaura/v1/video/save' using register_rest_route() (matching the pattern
used by existing controllers in inc/Rest/); implement a handler method (e.g.,
handle_video_save) that validates the request, parses fields from the request
body (nonce, video_url, object_id, object_type), performs any necessary
permission checks, and returns the same JSON shape used by the frontend
(attachment_id and url). Follow the pattern in
GenerateController::handle_generation for response/error structure and, if you
want explicit nonce verification beyond the X-WP-Nonce header sent by apiFetch,
validate the provided tryon_nonce against wp_verify_nonce inside your handler
before proceeding.

In `@src/frontend/tryon/TryOnModal.tsx`:
- Around line 327-338: The code only uses the first uploaded user photo
(primaryUserImage / userImages[0]) when building the images array, which
discards additional user images; update the flow to include all userImages by
mapping each through toInlineData (similar to selectedProductImages) and append
them into the images array (e.g., replace the single
primaryUserImage/toInlineData call with
Promise.all(userImages.map(toInlineData)) and include their data URIs), and
ensure any prompt or request-building logic that references a single user image
is updated to reflect multiple user photos.
- Around line 298-306: The code currently converts any fetch response into a
blob and passes it to blobToOptimizedInlineData, which can hide 404/HTML errors;
update the logic after apiFetch (resp) to validate the response by checking
resp.ok and the Content-Type header (ensure it starts with "image/") before
converting to blob; if the checks fail, throw or return a clear error (e.g.,
"Failed to fetch image: status X" or "Invalid content-type: Y") so callers of
the TryOnModal image flow can surface a meaningful image-fetch error instead of
an optimization/generation failure.

---

Nitpick comments:
In `@src/admin/enhancer/PreviewModal.tsx`:
- Around line 164-175: Remove the dead local binding `google` (derived as const
google = settings?.[aura?.optionKey]?.google) in PreviewModal.tsx and also drop
unused `settings`/`defaultImageModel` from the `useSelect` return that feed this
component; if you still need to validate provider/keys, replace the unused
binding with an explicit check against the provider/key (e.g. inspect settings
or aura.optionKey and assert the required provider value) inside `doGenerate` or
the component init instead of keeping an unused `google` variable so the linter
no longer flags dead code.

In `@src/admin/enhancer/PreviewSections/ConfigSettings.tsx`:
- Around line 13-66: Cache the hasPro() result and add explicit prop typing for
the ConfigSettings component: hoist a const isPro = hasPro() near the top of
ConfigSettings and replace all hasPro() calls (used in the tabs locked flag and
the conditional renders for video: DummyVideoConfigInputs vs Slot) with isPro to
avoid repeated global lookups; also declare an explicit TS type or interface for
the component props (including doGenerate and optional className) and type the
ConfigSettings function signature accordingly so the component is strongly typed
in this .tsx file.

In `@src/admin/woocommerce-products-list/style.css`:
- Around line 16-26: The visually-hidden utility .tryaura-try-on__input
currently uses the deprecated clip property; update the rule for modern browsers
by adding a non-deprecated clip-path (e.g., clip-path: inset(50%)) alongside the
existing clip fallback so older browsers still work—modify the CSS rule for
.tryaura-try-on__input to include clip-path: inset(50%) while retaining the
current clip: rect(...) line.

In `@src/base-tailwind.css`:
- Line 63: Move the `@import` "../../plugin-ui/dist/styles.css"; statement so it
appears near the top of src/base-tailwind.css alongside the existing `@import`
"tailwindcss/theme.css"; (i.e., before any other rules or at-rules except
`@charset/`@layer) to ensure it doesn't rely on PostCSS inlining and remains valid
in browsers or other processors that enforce the CSS `@import` ordering.

In `@src/utils/plugin-ui.ts`:
- Around line 1-15: Replace the blanket suppression and weak type: remove the
leading // `@ts-ignore` and instead use // `@ts-expect-error` when re-exporting from
'../../../plugin-ui/dist/index.js' so TypeScript will surface future typings,
and add a local type shim (.d.ts) that declares the actual exported types
(Settings, Button, Notice, NoticeTitle, Select, SelectContent, SelectItem,
SelectTrigger, SelectValue, useSettings) if the package lacks types today; then
replace SettingsElement = Record<string, any> with a more precise
interface/alias that matches the real Settings type you declare in the shim
(e.g., export type SettingsElement = Settings or a narrowed shape) so consumers
get proper type safety.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e20bb5db-04f5-4495-8960-a794f2c41d93

📥 Commits

Reviewing files that changed from the base of the PR and between 86d033b and aec5ffa.

⛔ Files ignored due to path filters (1)
  • src/admin/dashboard/pages/Settings/assets/openrouterLogo.svg is excluded by !**/*.svg
📒 Files selected for processing (85)
  • inc/Admin/Admin.php
  • inc/Admin/Enhancer.php
  • inc/Common/Assets.php
  • inc/Rest/GenerateController.php
  • inc/WooCommerce/WooCommerce.php
  • src/admin/dashboard/index.tsx
  • src/admin/dashboard/pages/Dashboard/Components/RecentActivity.tsx
  • src/admin/dashboard/pages/Dashboard/Components/StateCardItem.tsx
  • src/admin/dashboard/pages/Dashboard/Components/TryAuraConfiguration.tsx
  • src/admin/dashboard/pages/Dashboard/Components/UsageChart.tsx
  • src/admin/dashboard/pages/Settings/Gemini/GeminiSettings.tsx
  • src/admin/dashboard/pages/Settings/Gemini/index.tsx
  • src/admin/dashboard/pages/Settings/Gemini/registerModelSelectorField.tsx
  • src/admin/dashboard/pages/Settings/Woocommerce/TryOnControl/ScanFace.tsx
  • src/admin/dashboard/pages/Settings/Woocommerce/TryOnControl/TryOnControlSettings.tsx
  • src/admin/dashboard/pages/Settings/Woocommerce/TryOnControl/index.tsx
  • src/admin/dashboard/pages/Settings/components/ApiKeyInput.tsx
  • src/admin/dashboard/pages/Settings/components/SettingDetailsContainer.tsx
  • src/admin/dashboard/pages/Settings/components/SettingItemCard.tsx
  • src/admin/dashboard/pages/Settings/index.tsx
  • src/admin/dashboard/style.css
  • src/admin/dashboard/utils/menu-fix.js
  • src/admin/enhancer/EnhanceButton.tsx
  • src/admin/enhancer/PreviewModal.tsx
  • src/admin/enhancer/PreviewSections/ConfigFooter.tsx
  • src/admin/enhancer/PreviewSections/ConfigSettings.tsx
  • src/admin/enhancer/PreviewSections/DummyVideoConfigInputs.tsx
  • src/admin/enhancer/PreviewSections/GenerateVideoBtn.tsx
  • src/admin/enhancer/PreviewSections/ImageConfigInputs.tsx
  • src/admin/enhancer/PreviewSections/OriginalImage.tsx
  • src/admin/enhancer/PreviewSections/Output.tsx
  • src/admin/enhancer/PreviewSections/VideoProgress.tsx
  • src/admin/enhancer/filters.tsx
  • src/admin/enhancer/index.tsx
  • src/admin/enhancer/store/actions.ts
  • src/admin/enhancer/store/index.ts
  • src/admin/enhancer/store/reducer.ts
  • src/admin/enhancer/store/selectors.ts
  • src/admin/enhancer/style.css
  • src/admin/enhancer/style.scss
  • src/admin/product-video-gallery/components/ProductVideoGallery.tsx
  • src/admin/product-video-gallery/components/VideoDetailsModal.tsx
  • src/admin/product-video-gallery/index.tsx
  • src/admin/product-video-gallery/style.css
  • src/admin/woocommerce-products-list/index.tsx
  • src/admin/woocommerce-products-list/style.css
  • src/admin/woocommerce-products-list/style.scss
  • src/base-tailwind.css
  • src/components/Button.tsx
  • src/components/Checkbox.tsx
  • src/components/CrownIcon.tsx
  • src/components/DateRangePicker.tsx
  • src/components/GroupButton.tsx
  • src/components/ModernSelect.tsx
  • src/components/Toggle.tsx
  • src/components/TryauraLogoWithText.tsx
  • src/components/WpBtn.tsx
  • src/components/index.tsx
  • src/components/style.css
  • src/data/ai-models/actions.ts
  • src/data/ai-models/index.ts
  • src/data/ai-models/reducer.ts
  • src/data/ai-models/selectors.ts
  • src/data/ai-models/types.ts
  • src/data/settings/actions.ts
  • src/data/settings/controls.ts
  • src/data/settings/index.ts
  • src/data/settings/reducer.ts
  • src/data/settings/resolvers.ts
  • src/data/settings/selectors.ts
  • src/frontend/product-video/index.tsx
  • src/frontend/product-video/style.css
  • src/frontend/tryon/Output/index.tsx
  • src/frontend/tryon/ProductImagesSection/index.tsx
  • src/frontend/tryon/TryOnModal.tsx
  • src/frontend/tryon/UserImageSection/ImagePreview.tsx
  • src/frontend/tryon/UserImageSection/UploadImage.tsx
  • src/frontend/tryon/UserImageSection/UseCamera.tsx
  • src/frontend/tryon/UserImageSection/index.tsx
  • src/frontend/tryon/index.tsx
  • src/frontend/tryon/style.css
  • src/utils/plugin-ui.ts
  • src/utils/router.ts
  • src/utils/tryaura.ts
  • templates/products/tryon-switch.php
💤 Files with no reviewable changes (2)
  • src/admin/woocommerce-products-list/style.scss
  • src/admin/enhancer/style.scss
✅ Files skipped from review due to trivial changes (53)
  • src/admin/dashboard/pages/Dashboard/Components/StateCardItem.tsx
  • src/admin/enhancer/filters.tsx
  • src/data/settings/resolvers.ts
  • src/data/ai-models/index.ts
  • src/admin/dashboard/pages/Settings/index.tsx
  • src/admin/enhancer/store/index.ts
  • src/data/settings/controls.ts
  • src/admin/dashboard/pages/Settings/Woocommerce/TryOnControl/ScanFace.tsx
  • src/admin/product-video-gallery/components/VideoDetailsModal.tsx
  • src/data/settings/index.ts
  • src/components/index.tsx
  • src/components/TryauraLogoWithText.tsx
  • src/admin/enhancer/PreviewSections/OriginalImage.tsx
  • src/admin/enhancer/PreviewSections/DummyVideoConfigInputs.tsx
  • src/components/CrownIcon.tsx
  • src/components/Checkbox.tsx
  • src/components/WpBtn.tsx
  • src/admin/dashboard/pages/Settings/components/SettingItemCard.tsx
  • src/admin/dashboard/pages/Settings/Woocommerce/TryOnControl/index.tsx
  • src/frontend/tryon/UserImageSection/index.tsx
  • src/data/settings/actions.ts
  • src/data/settings/selectors.ts
  • src/admin/dashboard/pages/Settings/Woocommerce/TryOnControl/TryOnControlSettings.tsx
  • src/admin/dashboard/pages/Dashboard/Components/UsageChart.tsx
  • src/components/Toggle.tsx
  • src/data/ai-models/reducer.ts
  • src/admin/enhancer/PreviewSections/ImageConfigInputs.tsx
  • src/admin/dashboard/utils/menu-fix.js
  • src/admin/enhancer/PreviewSections/ConfigFooter.tsx
  • src/admin/dashboard/pages/Dashboard/Components/TryAuraConfiguration.tsx
  • src/components/DateRangePicker.tsx
  • src/components/Button.tsx
  • src/frontend/product-video/index.tsx
  • src/admin/dashboard/pages/Settings/components/SettingDetailsContainer.tsx
  • src/frontend/tryon/Output/index.tsx
  • src/frontend/tryon/UserImageSection/UploadImage.tsx
  • src/admin/enhancer/EnhanceButton.tsx
  • src/components/GroupButton.tsx
  • src/admin/dashboard/pages/Settings/components/ApiKeyInput.tsx
  • src/admin/product-video-gallery/components/ProductVideoGallery.tsx
  • src/admin/dashboard/pages/Dashboard/Components/RecentActivity.tsx
  • src/data/ai-models/selectors.ts
  • src/frontend/tryon/UserImageSection/UseCamera.tsx
  • src/admin/enhancer/PreviewSections/GenerateVideoBtn.tsx
  • src/admin/product-video-gallery/index.tsx
  • src/frontend/tryon/ProductImagesSection/index.tsx
  • src/utils/router.ts
  • src/data/settings/reducer.ts
  • src/frontend/tryon/UserImageSection/ImagePreview.tsx
  • src/data/ai-models/types.ts
  • src/components/ModernSelect.tsx
  • src/data/ai-models/actions.ts
  • src/admin/enhancer/PreviewSections/Output.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • inc/Admin/Enhancer.php
  • src/admin/enhancer/store/selectors.ts
  • src/frontend/tryon/index.tsx
  • src/admin/enhancer/index.tsx
  • src/admin/dashboard/pages/Settings/Gemini/index.tsx
  • src/admin/enhancer/store/reducer.ts

Comment thread inc/Admin/Admin.php
Comment on lines +82 to 96
'provider' => array( 'type' => 'string' ),
'apiKey' => array( 'type' => 'string' ),
'geminiApiKey' => array( 'type' => 'string' ),
'openrouterApiKey' => array( 'type' => 'string' ),
'imageModel' => array( 'type' => 'string' ),
'videoModel' => array( 'type' => 'string' ),
'geminiImageModel' => array( 'type' => 'string' ),
'geminiImageModelLabel' => array( 'type' => 'string' ),
'geminiVideoModel' => array( 'type' => 'string' ),
'geminiVideoModelLabel' => array( 'type' => 'string' ),
'openrouterImageModel' => array( 'type' => 'string' ),
'openrouterImageModelLabel' => array( 'type' => 'string' ),
'openrouterVideoModel' => array( 'type' => 'string' ),
'openrouterVideoModelLabel' => array( 'type' => 'string' ),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate provider against known values.

provider is accepted as any string both in the REST schema (Line 82) and in sanitize_settings (Line 133). A typo or stray whitespace (e.g. "OpenRouter", "openrouter ") will silently slip through and then fail the strict 'openrouter' === $provider checks in inc/Rest/GenerateController.php:267 and inc/Admin/Enhancer.php:41, quietly routing to Gemini with the OpenRouter key/model — a confusing failure mode for users.

🛡️ Proposed fix
-									'provider'       => array( 'type' => 'string' ),
+									'provider'       => array(
+										'type' => 'string',
+										'enum' => array( 'google', 'openrouter' ),
+									),
-				'provider'       => isset( $value['google']['provider'] ) ? sanitize_text_field( $value['google']['provider'] ) : 'google',
+				'provider'       => isset( $value['google']['provider'] ) && in_array( $value['google']['provider'], array( 'google', 'openrouter' ), true )
+					? $value['google']['provider']
+					: 'google',

Also applies to: 131-147

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

In `@inc/Admin/Admin.php` around lines 82 - 96, The REST schema and
sanitize_settings path accept any string for the provider, allowing
typos/whitespace to bypass checks; update the Admin.php provider handling and
the sanitize_settings function to normalize and validate provider values: trim()
and strtolower() the incoming provider, then check against a canonical allowlist
(e.g., 'openrouter', 'gemini', 'openai', etc.) and default or reject unknown
values; also add an enum of allowed providers to the REST schema for the
provider field so the API layer enforces valid values, and ensure
GenerateController (where 'openrouter' === $provider is used) and Enhancer logic
consume the normalized provider value rather than raw input.

Comment thread inc/Common/Assets.php
Comment on lines +267 to +349
'google/veo-3.1' => array(
'label' => __( 'veo-3.1 (OpenRouter)', 'tryaura' ),
'identity' => 'video',
'inputTypes' => array( 'text', 'image' ),
'outputTypes' => array( 'video' ),
'supported' => true,
'locked' => false,
'capabilities' => array(
'video' => array(
'supported' => true,
'locked' => false,
),
'prompt' => array(
'supported' => true,
'locked' => false,
),
),
'parameters' => array(
'aspectRatio' => array(
'supported' => true,
'locked' => false,
'default' => '16:9',
'values' => array(
array(
'label' => __( '16:9', 'tryaura' ),
'value' => '16:9',
'locked' => false,
),
array(
'label' => __( '9:16', 'tryaura' ),
'value' => '9:16',
'locked' => false,
),
array(
'label' => __( '1:1', 'tryaura' ),
'value' => '1:1',
'locked' => false,
),
),
),
'duration' => array(
'supported' => true,
'locked' => false,
'default' => '5',
'values' => array(
array(
'label' => __( '5 seconds', 'tryaura' ),
'value' => '5',
'locked' => false,
),
array(
'label' => __( '10 seconds', 'tryaura' ),
'value' => '10',
'locked' => false,
),
),
),
'resolution' => array(
'supported' => true,
'locked' => false,
'default' => '720p',
'values' => array(
array(
'label' => __( '720p', 'tryaura' ),
'value' => '720p',
'locked' => false,
),
array(
'label' => __( '1080p', 'tryaura' ),
'value' => '1080p',
'locked' => false,
),
),
),
),
),
),
),
'defaultProvider' => 'google',
'defaultImageModel' => 'gemini-2.5-flash-image',
'defaultProvider' => 'google',
'defaultImageModel' => 'gemini-2.5-flash-image',
'defaultVideoModel' => 'veo-3.1-generate-preview',
'defaultOpenRouterImageModel' => 'google/gemini-2.5-flash-preview-05-20:generateImage',
'defaultOpenRouterVideoModel' => 'bytedance/seedance-2.0-fast',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

defaultOpenRouterVideoModel has no matching entry in aiProviders['openrouter'].

defaultOpenRouterVideoModel is set to 'bytedance/seedance-2.0-fast' (Line 349), but the only video model registered under aiProviders['openrouter'] is 'google/veo-3.1' (Line 267). Any frontend code that resolves parameters/capabilities/labels by looking up the default key in aiProviders will get undefined for the OpenRouter default video model — breaking parameter rendering and locking in the Settings UI (see getDefaultModelsForProvider in src/admin/dashboard/pages/Settings/Gemini/GeminiSettings.tsx:51-71, which happily returns the key without validating it exists in the providers map).

Either register a bytedance/seedance-2.0-fast entry under aiProviders['openrouter'] (with appropriate capabilities/parameters), or change the default to match a declared key such as 'google/veo-3.1'.

🐛 Quick-fix option: align the default to a declared model
-			'defaultOpenRouterVideoModel'   => 'bytedance/seedance-2.0-fast',
+			'defaultOpenRouterVideoModel'   => 'google/veo-3.1',
📝 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
'google/veo-3.1' => array(
'label' => __( 'veo-3.1 (OpenRouter)', 'tryaura' ),
'identity' => 'video',
'inputTypes' => array( 'text', 'image' ),
'outputTypes' => array( 'video' ),
'supported' => true,
'locked' => false,
'capabilities' => array(
'video' => array(
'supported' => true,
'locked' => false,
),
'prompt' => array(
'supported' => true,
'locked' => false,
),
),
'parameters' => array(
'aspectRatio' => array(
'supported' => true,
'locked' => false,
'default' => '16:9',
'values' => array(
array(
'label' => __( '16:9', 'tryaura' ),
'value' => '16:9',
'locked' => false,
),
array(
'label' => __( '9:16', 'tryaura' ),
'value' => '9:16',
'locked' => false,
),
array(
'label' => __( '1:1', 'tryaura' ),
'value' => '1:1',
'locked' => false,
),
),
),
'duration' => array(
'supported' => true,
'locked' => false,
'default' => '5',
'values' => array(
array(
'label' => __( '5 seconds', 'tryaura' ),
'value' => '5',
'locked' => false,
),
array(
'label' => __( '10 seconds', 'tryaura' ),
'value' => '10',
'locked' => false,
),
),
),
'resolution' => array(
'supported' => true,
'locked' => false,
'default' => '720p',
'values' => array(
array(
'label' => __( '720p', 'tryaura' ),
'value' => '720p',
'locked' => false,
),
array(
'label' => __( '1080p', 'tryaura' ),
'value' => '1080p',
'locked' => false,
),
),
),
),
),
),
),
'defaultProvider' => 'google',
'defaultImageModel' => 'gemini-2.5-flash-image',
'defaultProvider' => 'google',
'defaultImageModel' => 'gemini-2.5-flash-image',
'defaultVideoModel' => 'veo-3.1-generate-preview',
'defaultOpenRouterImageModel' => 'google/gemini-2.5-flash-preview-05-20:generateImage',
'defaultOpenRouterVideoModel' => 'bytedance/seedance-2.0-fast',
'google/veo-3.1' => array(
'label' => __( 'veo-3.1 (OpenRouter)', 'tryaura' ),
'identity' => 'video',
'inputTypes' => array( 'text', 'image' ),
'outputTypes' => array( 'video' ),
'supported' => true,
'locked' => false,
'capabilities' => array(
'video' => array(
'supported' => true,
'locked' => false,
),
'prompt' => array(
'supported' => true,
'locked' => false,
),
),
'parameters' => array(
'aspectRatio' => array(
'supported' => true,
'locked' => false,
'default' => '16:9',
'values' => array(
array(
'label' => __( '16:9', 'tryaura' ),
'value' => '16:9',
'locked' => false,
),
array(
'label' => __( '9:16', 'tryaura' ),
'value' => '9:16',
'locked' => false,
),
array(
'label' => __( '1:1', 'tryaura' ),
'value' => '1:1',
'locked' => false,
),
),
),
'duration' => array(
'supported' => true,
'locked' => false,
'default' => '5',
'values' => array(
array(
'label' => __( '5 seconds', 'tryaura' ),
'value' => '5',
'locked' => false,
),
array(
'label' => __( '10 seconds', 'tryaura' ),
'value' => '10',
'locked' => false,
),
),
),
'resolution' => array(
'supported' => true,
'locked' => false,
'default' => '720p',
'values' => array(
array(
'label' => __( '720p', 'tryaura' ),
'value' => '720p',
'locked' => false,
),
array(
'label' => __( '1080p', 'tryaura' ),
'value' => '1080p',
'locked' => false,
),
),
),
),
),
),
),
'defaultProvider' => 'google',
'defaultImageModel' => 'gemini-2.5-flash-image',
'defaultVideoModel' => 'veo-3.1-generate-preview',
'defaultOpenRouterImageModel' => 'google/gemini-2.5-flash-preview-05-20:generateImage',
'defaultOpenRouterVideoModel' => 'google/veo-3.1',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inc/Common/Assets.php` around lines 267 - 349, The
defaultOpenRouterVideoModel value ('bytedance/seedance-2.0-fast') has no
matching entry in the aiProviders['openrouter'] map (only 'google/veo-3.1'
exists), causing lookups in getDefaultModelsForProvider to return undefined; fix
by either registering a corresponding bytedance/seedance-2.0-fast entry under
aiProviders['openrouter'] with appropriate capabilities/parameters (matching the
structure used for 'google/veo-3.1') or change defaultOpenRouterVideoModel to an
existing key such as 'google/veo-3.1' so the settings UI can resolve parameters
and locks correctly.

Comment on lines +194 to +196
.tryaura-try-on__input:checked + .tryaura-try-on__track::after {
transform: translateX(14px);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

RTL: toggle knob animates in the wrong direction.

transform: translateX(14px) moves the knob right regardless of the admin's dir. On RTL WordPress admin the track will still render with left: 2px anchoring the knob on the left, and the checked state will push it further off the visible track. Consider flipping via [dir="rtl"] override or using logical inset properties:

♻️ Suggested tweak
+			[dir="rtl"] .tryaura-try-on__input:checked + .tryaura-try-on__track::after {
+				transform: translateX(-14px);
+			}
📝 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
.tryaura-try-on__input:checked + .tryaura-try-on__track::after {
transform: translateX(14px);
}
.tryaura-try-on__input:checked + .tryaura-try-on__track::after {
transform: translateX(14px);
}
[dir="rtl"] .tryaura-try-on__input:checked + .tryaura-try-on__track::after {
transform: translateX(-14px);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inc/WooCommerce/WooCommerce.php` around lines 194 - 196, The checked-state
rule moves the knob always to the right via transform: translateX(14px) which
breaks RTL; update the CSS for .tryaura-try-on__input:checked +
.tryaura-try-on__track::after to use a logical/RTL-aware approach — e.g. replace
the fixed translateX with a CSS variable (--try-on-translate: 14px) and set that
variable to -14px for RTL via a [dir="rtl"] .tryaura-try-on__input:checked +
.tryaura-try-on__track::after override, or add a dedicated RTL rule that uses
transform: translateX(-14px) so the knob animates in the correct direction.

Comment on lines +9 to +58
const STYLE_SELECTORS = [
'link#tryaura-admin-css',
'link#tryaura-components-css',
'link#wp-components-css',
'link[href*="/build/admin/dashboard/index.css"]',
'link[href*="/build/admin/dashboard/style-index.css"]',
'link[href*="/build/style-components.css"]',
];

function mirrorStylesIntoShadow( shadowRoot: ShadowRoot ) {
const styleHost = document.createElement( 'div' );
styleHost.setAttribute( 'data-tryaura-shadow-styles', 'true' );
shadowRoot.prepend( styleHost );

const seen = new Set< string >();

const syncStyles = () => {
const styleSources = document.querySelectorAll< HTMLLinkElement >(
STYLE_SELECTORS.join( ', ' )
);

styleSources.forEach( ( source ) => {
const href = source.href;

if ( ! href || seen.has( href ) ) {
return;
}

const clone = document.createElement( 'link' );
clone.rel = 'stylesheet';
clone.href = href;
clone.media = source.media || 'all';
styleHost.appendChild( clone );
seen.add( href );
} );
};

syncStyles();

const observer = new MutationObserver( () => {
syncStyles();
} );

observer.observe( document.head, {
childList: true,
subtree: true,
} );

return () => observer.disconnect();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm `@wordpress/components` is used within the dashboard subtree,
# which would be affected by the shadow-DOM style-mirroring gap.
rg -nP "from\s+'@wordpress/components'" -g 'src/admin/dashboard/**'
# Also see what admin styles use wp_add_inline_style (not mirrored).
rg -nP '\bwp_add_inline_style\s*\(' -g 'inc/**/*.php'

Repository: getdokan/tryAura

Length of output: 628


Shadow DOM mirror misses inline <style> tags — @wordpress/components styling will break inside the dashboard.

STYLE_SELECTORS only matches <link rel="stylesheet"> elements, so the following are never cloned into the shadow root:

  • @wordpress/components styles — injected by Emotion at runtime as <style data-emotion="..."> tags in document.head, not link elements. The dashboard uses Modal, Tooltip, SlotFillProvider, and Slot from this library.
  • Styles registered via wp_add_inline_style (rendered as <style id="handle-inline-css">), e.g. the tryaura-woo-products stylesheet in inc/WooCommerce/WooCommerce.php:131.
  • Any styles not in the hand-maintained allowlist (future WP admin stylesheets, third-party plugins).

Result: Components from @wordpress/components rendered under tryaura-dashboard-shadow-root will appear unstyled, and CSS rules added in src/base-tailwind.css (e.g. .components-modal__screen-overlay, .ai-enhancer-preview-modal) will not apply to portals inside the shadow root.

Also: the MutationObserver disconnect() returned from mirrorStylesIntoShadow is discarded, leaving no way to clean up for HMR or tests.

🛠️ Suggested direction

Either (a) drop Shadow DOM and rely on the .tryaura class scoping already in use, or (b) clone <style> tags in addition to <link>:

const syncStyles = () => {
-	const styleSources = document.querySelectorAll< HTMLLinkElement >(
-		STYLE_SELECTORS.join( ', ' )
-	);
-
-	styleSources.forEach( ( source ) => {
-		const href = source.href;
-
-		if ( ! href || seen.has( href ) ) {
-			return;
-		}
-
-		const clone = document.createElement( 'link' );
-		clone.rel = 'stylesheet';
-		clone.href = href;
-		clone.media = source.media || 'all';
-		styleHost.appendChild( clone );
-		seen.add( href );
-	} );
+	document
+		.querySelectorAll< HTMLLinkElement >(
+			STYLE_SELECTORS.join( ', ' )
+		)
+		.forEach( ( source ) => {
+			const href = source.href;
+			if ( ! href || seen.has( 'link:' + href ) ) return;
+			const clone = document.createElement( 'link' );
+			clone.rel = 'stylesheet';
+			clone.href = href;
+			clone.media = source.media || 'all';
+			styleHost.appendChild( clone );
+			seen.add( 'link:' + href );
+		} );
+
+	document
+		.querySelectorAll< HTMLStyleElement >( 'head style' )
+		.forEach( ( node ) => {
+			const key = 'style:' + ( node.id || '' ) + ':' + node.textContent?.length;
+			if ( seen.has( key ) ) return;
+			styleHost.appendChild( node.cloneNode( true ) );
+			seen.add( key );
+		} );
};

Note: Emotion injects rules via the CSSOM (not textContent), so a plain cloneNode(true) will be empty. Full parity requires iterating document.styleSheets and re-serializing rules—at which point reconsidering Shadow DOM is usually simpler.

Comment on lines +111 to +137
async function validateApiKey(
provider: ProviderId,
key: string
): Promise<boolean> {
try {
if (provider === 'openrouter') {
const res = await fetch('https://openrouter.ai/api/v1/models', {
method: 'GET',
headers: {
Authorization: `Bearer ${key}`,
},
});

return res.ok;
}

const res = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(
key
)}`
);

return res.ok;
} catch {
return false;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'No auth: '
curl -sS -o /dev/null -w '%{http_code}\n' https://openrouter.ai/api/v1/models

printf 'Invalid bearer: '
curl -sS -o /dev/null -w '%{http_code}\n' \
  -H 'Authorization: Bearer invalid-openrouter-key' \
  https://openrouter.ai/api/v1/models

Repository: getdokan/tryAura

Length of output: 92


OpenRouter API key validation is broken — any key passes validation.

The /api/v1/models endpoint is public and does not authenticate bearer tokens. Both unauthenticated requests and requests with invalid bearer tokens return 200, meaning validateApiKey() cannot validate OpenRouter keys and will approve invalid credentials. Users can save non-functional API keys thinking validation succeeded.

Replace the OpenRouter validation with an authenticated endpoint (e.g., /api/v1/chat/completions or the models endpoint with a POST request that requires authentication) or implement a test API call that actually requires a valid key.

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

In `@src/admin/dashboard/pages/Settings/Gemini/GeminiSettings.tsx` around lines
111 - 137, validateApiKey currently treats the OpenRouter models GET as
authoritative but that endpoint is public; update validateApiKey (and the
provider === 'openrouter' branch) to call an authenticated OpenRouter endpoint
that actually requires a valid bearer token (for example POST to
/api/v1/chat/completions or an authenticated POST to models) using the
Authorization: Bearer header and a minimal valid request payload, then treat the
response status (e.g., 200/201 ok vs 401/403) as the validation result; ensure
you await the POST response and return res.ok (or false on catch) so invalid
keys fail validation.

Comment on lines +277 to +289

setStatus('parsing');
setMessage(__('Processing results…', 'tryaura'));
const data64 = response?.image || null;

if (!data64) {
throw new Error(
__('Model did not return an image.', 'tryaura')
);
}

const dataUrl = `data:image/png;base64,${data64}`;
setGeneratedUrl(dataUrl);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded data:image/png can misrepresent the returned image's MIME type.

The backend returns only a raw base64 image field without its MIME type:

  • generate_via_gemini reads inlineData.data (Gemini can return non-PNG inline data depending on model) but drops inlineData.mimeType.
  • generate_via_openrouter's extract_openrouter_image_data strips the original data:image/<type>;base64, prefix and returns just the payload.

Assembling data:image/png;base64,${data64} here means setInMediaSelection's fetch(generatedUrl).then(r => r.blob()) will produce a Blob with blob.type === 'image/png' even when the bytes are JPEG/WebP, and the subsequent wp/v2/media upload uses that MIME as Content-Type and derives the extension from it — producing .png files with non-PNG content.

Recommend plumbing the real MIME type through the REST response and using it here:

🐛 Sketch of the fix (frontend + backend coordination)

Backend (generate_via_gemini / generate_via_openrouter): also return mime_type (or mimeType) alongside image. For OpenRouter, preserve it in extract_openrouter_image_data (e.g. return a struct), and for Gemini, use $data['candidates'][0]['content']['parts'][0]['inlineData']['mimeType'].

Frontend:

-			const data64 = response?.image || null;
+			const data64   = response?.image || null;
+			const mimeType = response?.mimeType || response?.mime_type || 'image/png';
...
-			const dataUrl = `data:image/png;base64,${data64}`;
+			const dataUrl = `data:${mimeType};base64,${data64}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/enhancer/PreviewModal.tsx` around lines 277 - 289, The frontend is
hardcoding data:image/png when creating the generatedUrl which can mislabel
non-PNG bytes; update the backend (generate_via_gemini, generate_via_openrouter,
and extract_openrouter_image_data) to return the original mimeType (e.g.,
mime_type or mimeType) alongside image, then in the PreviewModal use that mime
to build the data URL (use data:${mime};base64,${image}) before calling
setGeneratedUrl so fetch(...).blob() and wp/v2/media receive the correct
Content-Type and file extension; ensure any places referencing data64 or
setGeneratedUrl accept the new payload shape.

Comment on lines +42 to +70
const handleSaveToLibrary = async () => {
if (!videoUrl) {
return;
}

setSaving(true);
try {
const response = await apiFetch<{
attachment_id: number;
url: string;
}>({
path: '/tryaura/v1/video/save',
method: 'POST',
data: {
nonce: aura?.nonce || '',
video_url: videoUrl,
object_id: aura?.postId?.toString() || '',
object_type: aura?.postType || '',
},
});

setSavedUrl(response.url);
} catch (err: any) {
// Let user know the save failed but don't block the UI.
console.error('Failed to save video:', err);
} finally {
setSaving(false);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Silent save failure — user gets no feedback when saving fails.

On an error, the catch only console.errors and finally clears saving. The UI state reverts to the pre-click state with no indication anything went wrong; the user may click again or assume the request succeeded. Also, when save succeeds you still display the original videoUrl via savedUrl (so preview doesn't regress), but on failure there's no way for the user to know.

🛡️ Proposed fix
 	const [saving, setSaving] = useState(false);
 	const [savedUrl, setSavedUrl] = useState<string | null>(null);
+	const [saveError, setSaveError] = useState<string | null>(null);
 
 	...
 
 	const handleSaveToLibrary = async () => {
 		if (!videoUrl) {
 			return;
 		}
 
 		setSaving(true);
+		setSaveError(null);
 		try {
 			...
 			setSavedUrl(response.url);
 		} catch (err: any) {
-			// Let user know the save failed but don't block the UI.
-			console.error('Failed to save video:', err);
+			console.error('Failed to save video:', err);
+			setSaveError(
+				err?.message ||
+					__('Failed to save video to Media Library.', 'tryaura')
+			);
 		} finally {
 			setSaving(false);
 		}
 	};

Then render saveError near the Save button so the user gets feedback.

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

In `@src/admin/enhancer/PreviewSections/VideoProgress.tsx` around lines 42 - 70,
The save flow in handleSaveToLibrary currently only logs errors to console so
users get no feedback; add a saveError state (e.g., saveError and setSaveError)
and clear it before the request, set a descriptive message in the catch block
(instead of only console.error) and retain existing setSaving/setSavedUrl
behavior; ensure saveError is cleared on success (after setSavedUrl) and render
the saveError message in the component UI near the Save button so users see the
failure and can retry.

Comment on lines +49 to +61
const response = await apiFetch<{
attachment_id: number;
url: string;
}>({
path: '/tryaura/v1/video/save',
method: 'POST',
data: {
nonce: aura?.nonce || '',
video_url: videoUrl,
object_id: aura?.postId?.toString() || '',
object_type: aura?.postType || '',
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the video/save REST endpoint is registered and check how it consumes the `nonce` body param.
rg -nP --type=php -C5 "register_rest_route\([^\)]*['\"]tryaura/v1['\"]"
rg -nP --type=php -C5 "['\"]/?video/save['\"]"
rg -nP --type=php -C5 "wp_verify_nonce\s*\(\s*\\\$nonce"

Repository: getdokan/tryAura

Length of output: 1917


🏁 Script executed:

# Search comprehensively for REST route registrations and video endpoints
rg -nP --type=php "register_rest_route.*tryaura" -A 10
rg -nP --type=php "video.*save" 
rg -nP --type=php "class.*VideoController|class.*Video.*"
rg -nP --type=php "\bvideo\b" --type=php -l
fd -e php | xargs grep -l "video/save" 2>/dev/null || echo "No matches for video/save in php files"

Repository: getdokan/tryAura

Length of output: 897


🏁 Script executed:

# Check VideoThumbnailController to see what endpoints it registers
cat -n inc/Rest/VideoThumbnailController.php

# Search for all REST route registrations
rg -nP --type=php "register_rest_route" inc/Rest/ -A 5

Repository: getdokan/tryAura

Length of output: 10997


Implement the /tryaura/v1/video/save REST endpoint in the backend.

The endpoint called in the code does not exist in the codebase. Registered endpoints in inc/Rest/ are: /generate-thumbnail, /image, /settings, /product/{id}/images, /stats, /chart-data, /log-usage, and /activities. You must create the VideoController with register_rest_route() for POST /tryaura/v1/video/save before this frontend code can function. Additionally, follow the pattern from GenerateController::handle_generation if backend nonce verification is required (note that apiFetch already auto-attaches X-WP-Nonce header, so a second body nonce check is only necessary if you implement explicit backend verification against tryon_nonce).

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

In `@src/admin/enhancer/PreviewSections/VideoProgress.tsx` around lines 49 - 61,
Create a new VideoController and register a POST REST route for
'/tryaura/v1/video/save' using register_rest_route() (matching the pattern used
by existing controllers in inc/Rest/); implement a handler method (e.g.,
handle_video_save) that validates the request, parses fields from the request
body (nonce, video_url, object_id, object_type), performs any necessary
permission checks, and returns the same JSON shape used by the frontend
(attachment_id and url). Follow the pattern in
GenerateController::handle_generation for response/error structure and, if you
want explicit nonce verification beyond the X-WP-Nonce header sent by apiFetch,
validate the provided tryon_nonce against wp_verify_nonce inside your handler
before proceeding.

Comment on lines +298 to +306
const resp = (await apiFetch({
url: img,
parse: false,
} ) ) as Response;
})) as Response;
const blob = await resp.blob();
const mimeType = blob.type || 'image/png';
const base64 = await new Promise< string >( ( resolve, reject ) => {
const reader = new FileReader();
reader.onloadend = () => {
const result = reader.result as string;
const comma = result.indexOf( ',' );
resolve( comma >= 0 ? result.substring( comma + 1 ) : result );
};
reader.onerror = reject;
reader.readAsDataURL( blob );
} );
return { mimeType, data: base64 };
const optimized = await blobToOptimizedInlineData(blob, {
preferredMimeType: 'image/jpeg',
});
return { mimeType: optimized.mimeType, data: optimized.data };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate fetched image responses before optimizing.

A 404/HTML response can be converted to a blob and passed into image optimization, leading to a vague generation failure instead of a clear image-fetch error.

Proposed guard
 		const resp = (await apiFetch({
 			url: img,
 			parse: false,
 		})) as Response;
+
+		if (!resp.ok) {
+			throw new Error(__('Unable to fetch the selected image.', 'tryaura'));
+		}
+
 		const blob = await resp.blob();
+
+		if (blob.type && !blob.type.startsWith('image/')) {
+			throw new Error(__('The selected URL did not return an image.', 'tryaura'));
+		}
+
 		const optimized = await blobToOptimizedInlineData(blob, {
 			preferredMimeType: 'image/jpeg',
 		});
📝 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 resp = (await apiFetch({
url: img,
parse: false,
} ) ) as Response;
})) as Response;
const blob = await resp.blob();
const mimeType = blob.type || 'image/png';
const base64 = await new Promise< string >( ( resolve, reject ) => {
const reader = new FileReader();
reader.onloadend = () => {
const result = reader.result as string;
const comma = result.indexOf( ',' );
resolve( comma >= 0 ? result.substring( comma + 1 ) : result );
};
reader.onerror = reject;
reader.readAsDataURL( blob );
} );
return { mimeType, data: base64 };
const optimized = await blobToOptimizedInlineData(blob, {
preferredMimeType: 'image/jpeg',
});
return { mimeType: optimized.mimeType, data: optimized.data };
const resp = (await apiFetch({
url: img,
parse: false,
})) as Response;
if (!resp.ok) {
throw new Error(__('Unable to fetch the selected image.', 'tryaura'));
}
const blob = await resp.blob();
if (blob.type && !blob.type.startsWith('image/')) {
throw new Error(__('The selected URL did not return an image.', 'tryaura'));
}
const optimized = await blobToOptimizedInlineData(blob, {
preferredMimeType: 'image/jpeg',
});
return { mimeType: optimized.mimeType, data: optimized.data };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/tryon/TryOnModal.tsx` around lines 298 - 306, The code currently
converts any fetch response into a blob and passes it to
blobToOptimizedInlineData, which can hide 404/HTML errors; update the logic
after apiFetch (resp) to validate the response by checking resp.ok and the
Content-Type header (ensure it starts with "image/") before converting to blob;
if the checks fail, throw or return a clear error (e.g., "Failed to fetch image:
status X" or "Invalid content-type: Y") so callers of the TryOnModal image flow
can surface a meaningful image-fetch error instead of an optimization/generation
failure.

Comment on lines +327 to 338
const primaryUserImage = userImages[0];
const userInline = await toInlineData(primaryUserImage);
const productInlineList = await Promise.all(
selectedProductImages.map( toInlineData )
selectedProductImages.map(toInlineData)
);

const images = [
`data:${ userInline.mimeType };base64,${ userInline.data }`,
`data:${userInline.mimeType};base64,${userInline.data}`,
...productInlineList.map(
( p ) => `data:${ p.mimeType };base64,${ p.data }`
(p) => `data:${p.mimeType};base64,${p.data}`
),
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Don’t silently ignore additional uploaded user photos.

The upload flow accepts multiple photos and tells users “Photo(s) selected”, but generation only sends userImages[0]. Either restrict the selector/message to one user photo or include all selected user photos in the request with an updated prompt.

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

In `@src/frontend/tryon/TryOnModal.tsx` around lines 327 - 338, The code only uses
the first uploaded user photo (primaryUserImage / userImages[0]) when building
the images array, which discards additional user images; update the flow to
include all userImages by mapping each through toInlineData (similar to
selectedProductImages) and append them into the images array (e.g., replace the
single primaryUserImage/toInlineData call with
Promise.all(userImages.map(toInlineData)) and include their data URIs), and
ensure any prompt or request-building logic that references a single user image
is updated to reflect multiple user photos.

@Aunshon Aunshon self-assigned this Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants