Repository: reearth/reearth-visualizer
Pull request #1487: fix(web): update browser url on preset-plugin selection [VIZ-569]
Author: login: mulengawilfred
Labels: name: web
Created: 2025-03-11T06:39:20Z | Merged: 2025-03-13T04:45:26Z

# Overview
- Added logic to update the browser url with the plugin's plugin-id when a plugin is selected in plugin playground.
-  If the browser url is shared in another browser tab, the previously selected preset-plugin will be selected in the new tab.
- Refactored logic that handles the compression of preset-plugin to base64.

## What I've done

## What I haven't done

## How I tested

## Which point I want you to review particularly

## Memo


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
  - Improved the plugin selection interface with a new visual highlight for the active plugin, making it easier to identify the current category.
  - Enhanced the sharing functionality so that the URL automatically updates to reflect the selected plugin, ensuring a smoother and more consistent user experience.
  - Restored consistent display of shared plugins across the Plugin Playground for better interaction and navigation.
  - Introduced a new constant for shared plugin identification to streamline plugin handling.
  - Added contextual comments to assist with future translations related to shared plugins.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/web/src/beta/features/PluginPlayground/Plugins/constants.ts b/web/src/beta/features/PluginPlayground/Plugins/constants.ts
index 375e03d183..14da03f667 100644
--- a/web/src/beta/features/PluginPlayground/Plugins/constants.ts
+++ b/web/src/beta/features/PluginPlayground/Plugins/constants.ts
@@ -12,3 +12,5 @@ export type FileType = {
 };
 
 export const ALLOWED_FILE_EXTENSIONS = ["js"] as const;
+
+export const SHARED_PLUGIN_ID = "shared-plugin-id";
diff --git a/web/src/beta/features/PluginPlayground/Plugins/index.tsx b/web/src/beta/features/PluginPlayground/Plugins/index.tsx
index 2815803e28..19540e1b52 100644
--- a/web/src/beta/features/PluginPlayground/Plugins/index.tsx
+++ b/web/src/beta/features/PluginPlayground/Plugins/index.tsx
@@ -2,8 +2,9 @@ import { Collapse, Typography, Button } from "@reearth/beta/lib/reearth-ui";
 import { EntryItem } from "@reearth/beta/ui/components";
 import { useT } from "@reearth/services/i18n";
 import { styled } from "@reearth/services/theme";
-import { FC, useState } from "react";
+import { FC, useState, useEffect } from "react";
 
+import { SHARED_PLUGIN_ID } from "./constants";
 import FileListItem from "./FileListItem";
 import useTitles from "./presets/useTitles";
 import usePlugins from "./usePlugins";
@@ -42,6 +43,9 @@ const Plugins: FC<Props> = ({
 }) => {
   const t = useT();
   const [isAddingNewFile, setIsAddingNewFile] = useState(false);
+  const [selectedCategoryId, setSelectedCategoryId] = useState<
+    string | undefined
+  >();
 
   const handlePluginShare = (): void => {
     if (!selectedPlugin) return;
@@ -50,16 +54,26 @@ const Plugins: FC<Props> = ({
 
   const { categoryTitles, pluginTitles } = useTitles();
 
+  useEffect(() => {
+    if (!selectedPlugin) return;
+    const selectedCategory = presetPlugins.find((category) =>
+      category.plugins.find((plugin) => plugin.id === selectedPlugin.id)
+    );
+    if (selectedCategory) {
+      setSelectedCategoryId(selectedCategory.id);
+    }
+  }, [presetPlugins, selectedPlugin]);
+
   const PluginEntryItem: FC<{
+    highlighted: boolean;
+    onSelect: (id: string) => void;
     pluginId: string;
-    selectedPluginId: string;
     title: string;
-    onSelect: (id: string) => void;
-  }> = ({ pluginId, selectedPluginId, onSelect, title }) => {
+  }> = ({ highlighted, pluginId, onSelect, title }) => {
     return (
       <EntryItem
         key={pluginId}
-        highlighted={selectedPluginId === pluginId}
+        highlighted={highlighted}
         onClick={() => onSelect(pluginId)}
         title={title}
         optionsMenuWidth={100}
@@ -101,7 +115,7 @@ const Plugins: FC<Props> = ({
       </Actions>
       <PluginBrowser>
         <PluginList>
-          {sharedPlugin && (
+          {sharedPlugin && sharedPlugin.id === SHARED_PLUGIN_ID && (
             <div>
               <Collapse
                 key={"shared"}
@@ -112,11 +126,11 @@ const Plugins: FC<Props> = ({
               >
                 <PluginSubList>
                   <PluginEntryItem
-                    pluginId={sharedPlugin.id}
+                    highlighted={selectedPlugin.id === sharedPlugin.id}
                     key={sharedPlugin.id}
-                    title={pluginTitles[sharedPlugin.id]}
-                    selectedPluginId={selectedPlugin.id}
                     onSelect={selectPlugin}
+                    pluginId={sharedPlugin.id}
+                    title={pluginTitles[sharedPlugin.id]}
                   />
                 </PluginSubList>
               </Collapse>
@@ -126,7 +140,9 @@ const Plugins: FC<Props> = ({
             <div key={category.id}>
               <Collapse
                 key={category.id}
-                collapsed={category.id !== "custom"}
+                collapsed={
+                  category.id !== "custom" && category.id !== selectedCategoryId
+                }
                 iconPosition="left"
                 size="small"
                 title={categoryTitles[category.id]}
@@ -134,15 +150,17 @@ const Plugins: FC<Props> = ({
               >
                 <PluginSubList>
                   {category.plugins.length > 0 ? (
-                    category.plugins.map((plugin) => (
-                      <PluginEntryItem
-                        pluginId={plugin.id}
-                        key={plugin.id}
-                        title={pluginTitles[plugin.id]}
-                        selectedPluginId={selectedPlugin.id}
-                        onSelect={selectPlugin}
-                      />
-                    ))
+                    category.plugins.map((plugin) => {
+                      return (
+                        <PluginEntryItem
+                          highlighted={selectedPlugin.id === plugin.id}
+                          key={plugin.id}
+                          onSelect={selectPlugin}
+                          pluginId={plugin.id}
+                          title={pluginTitles[plugin.id]}
+                        />
+                      );
+                    })
                   ) : (
                     <EmptyTip>
                       <Typography size="body" color="weak" trait="italic">
diff --git a/web/src/beta/features/PluginPlayground/Plugins/presets/useTitles.ts b/web/src/beta/features/PluginPlayground/Plugins/presets/useTitles.ts
index 550ec8e3f8..5add8726c1 100644
--- a/web/src/beta/features/PluginPlayground/Plugins/presets/useTitles.ts
+++ b/web/src/beta/features/PluginPlayground/Plugins/presets/useTitles.ts
@@ -59,7 +59,7 @@ export default () => {
       "zoom-in-out": t("Zoom In Out"),
       "camera-rotation": t("Camera Rotation"),
       "camera-position": t("Camera Position"),
-      "shared-plugin-id": t("Shared Plugin")
+      "shared-plugin-id": t("Shared Plugin") // NOTE: needed to hardcode this part to help translation
     };
   }, [t]);
 
diff --git a/web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts b/web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts
index a010ad017c..1aca90c581 100644
--- a/web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts
+++ b/web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts
@@ -2,21 +2,24 @@ import { useT } from "@reearth/services/i18n";
 import { useNotification } from "@reearth/services/state";
 import JSZip from "jszip";
 import LZString from "lz-string";
-import { useCallback, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
 import { useSearchParams } from "react-router-dom";
 import useFileInput from "use-file-input";
 import { v4 as uuidv4 } from "uuid";
 
-import { PluginType } from "./constants";
+import { PluginType, SHARED_PLUGIN_ID } from "./constants";
 import { presetPlugins } from "./presets";
 import { validateFileTitle } from "./utils";
 
 export default () => {
-  const [searchParams] = useSearchParams();
   const [, setNotification] = useNotification();
-
+  const [searchParams] = useSearchParams();
   const t = useT();
-  const sharedPluginUrl = searchParams.get("plugin");
+  const pluginIdParam = searchParams.get("plugin-id");
+  const sharedPluginURLParam = searchParams.get("shared-plugin");
+  const presetPluginsArray = presetPlugins
+    .map((category) => category.plugins)
+    .flat();
 
   const decodePluginURL = useCallback((encoded: string) => {
     const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
@@ -26,14 +29,10 @@ export default () => {
     return JSON.parse(decompressed);
   }, []);
 
-  const sharedPlugin = sharedPluginUrl
+  const sharedPlugin: PluginType = sharedPluginURLParam
     ? (() => {
         try {
-          const decoded = decodePluginURL(sharedPluginUrl);
-          return {
-            ...decoded,
-            id: "shared-plugin-id" // NOTE: hardcoded id as ids are used for the plugin title logic
-          };
+          return decodePluginURL(sharedPluginURLParam);
         } catch (_error) {
           setNotification({
             type: "error",
@@ -44,15 +43,19 @@ export default () => {
       })()
     : null;
 
-  const presetPluginsArray = presetPlugins
-    .map((category) => category.plugins)
-    .flat();
-
   const [plugins, setPlugins] = useState<PluginType[]>(
-    sharedPlugin ? [sharedPlugin, ...presetPluginsArray] : presetPluginsArray
+    sharedPlugin && sharedPlugin.id === SHARED_PLUGIN_ID
+      ? [sharedPlugin, ...presetPluginsArray]
+      : presetPluginsArray
+  );
+  const [selectedPluginId, setSelectedPluginId] = useState(
+    sharedPlugin
+      ? sharedPlugin.id
+      : pluginIdParam
+        ? pluginIdParam
+        : plugins[0].id
   );
 
-  const [selectedPluginId, setSelectedPluginId] = useState(plugins[0].id);
   const [selectedFileId, setSelectedFileId] = useState<string>(
     plugins[0]?.files[0]?.id ?? ""
   );
@@ -70,6 +73,10 @@ export default () => {
     [selectedPlugin, selectedFileId]
   );
 
+  useEffect(() => {
+    window.history.replaceState({}, "", `?plugin-id=${selectedPluginId}`);
+  }, [selectedPluginId]);
+
   const selectPlugin = useCallback((pluginId: string) => {
     setSelectedPluginId(pluginId);
     setSelectedFileId("");
@@ -260,31 +267,25 @@ export default () => {
   }, [selectedPlugin, setNotification]);
 
   const encodeAndSharePlugin = useCallback(
-    (pluginId: string): string | undefined => {
-      selectPlugin(pluginId);
-      // Need to "find" the selectedPlugin as it does not get updated immediately
-      const sharedPlugin = plugins.find((plugin) => plugin.id === pluginId);
-
-      // Note: We can't use the same id for a shared plugin
-      const selectedPluginCopy = { ...sharedPlugin };
-
-      // First compress the code
+    (pluginId: string) => {
       try {
+        const pluginToShare = plugins.find((plugin) => plugin.id === pluginId);
+        const selectedPluginCopy = { ...pluginToShare, id: SHARED_PLUGIN_ID };
+
         const compressed = LZString.compressToBase64(
           JSON.stringify(selectedPluginCopy)
         )
-          .replace(/\+/g, "-") // Convert + to -
-          .replace(/\//g, "_") // Convert / to _
-          .replace(/=/g, ""); // Remove padding =
+          .replace(/\+/g, "-")
+          .replace(/\//g, "_")
+          .replace(/=/g, "");
 
-        const shareUrl = `${window.location.origin}${window.location.pathname}?plugin=${compressed}`;
+        const shareUrl = `${window.location.origin}${window.location.pathname}?shared-plugin=${compressed}`;
         navigator.clipboard.writeText(shareUrl);
 
         setNotification({
           type: "success",
           text: t("Plugin link copied to clipboard")
         });
-        return compressed;
       } catch (error) {
         if (error instanceof Error) {
           setNotification({ type: "error", text: error.message });
@@ -292,7 +293,7 @@ export default () => {
         return;
       }
     },
-    [plugins, selectPlugin, setNotification, t]
+    [setNotification, t, plugins]
   );
 
   return {
diff --git a/web/src/beta/features/PluginPlayground/hooks.tsx b/web/src/beta/features/PluginPlayground/hooks.tsx
index 1a8daf8a62..8ddb98dff2 100644
--- a/web/src/beta/features/PluginPlayground/hooks.tsx
+++ b/web/src/beta/features/PluginPlayground/hooks.tsx
@@ -48,6 +48,7 @@ export default () => {
   );
 
   const {
+    sharedPlugin,
     presetPlugins,
     selectPlugin,
     selectedPlugin,
@@ -59,8 +60,7 @@ export default () => {
     deleteFile,
     handlePluginImport,
     handlePluginDownload,
-    encodeAndSharePlugin,
-    sharedPlugin
+    encodeAndSharePlugin
   } = usePlugins();
 
   const { executeCode, infoboxBlocks, story, widgets } = useCode({
@@ -155,6 +155,7 @@ export default () => {
         children: (
           <Plugins
             encodeAndSharePlugin={encodeAndSharePlugin}
+            sharedPlugin={sharedPlugin}
             presetPlugins={presetPlugins}
             selectedPlugin={selectedPlugin}
             selectPlugin={selectPlugin}
@@ -164,7 +165,6 @@ export default () => {
             updateFileTitle={updateFileTitle}
             deleteFile={deleteFile}
             handlePluginImport={handlePluginImport}
-            sharedPlugin={sharedPlugin}
             handlePluginDownload={handlePluginDownload}
           />
         )
@@ -180,9 +180,9 @@ export default () => {
       selectedPlugin,
       selectFile,
       selectPlugin,
+      sharedPlugin,
       t,
       handlePluginImport,
-      sharedPlugin,
       updateFileTitle
     ]
   );


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>web/src/beta/features/PluginPlayground/Plugins/index.tsx (1)</summary><blockquote>

`199-201`: **Consider preserving user-controlled collapse state**

The current implementation automatically expands/collapses categories based on the selected plugin. This might override user preferences if they manually collapsed a category.

Consider adding a state variable to track user-controlled collapse states separately from auto-expansion:

```diff
- collapsed={
-   category.id !== "custom" && category.id !== selectedCategoryId
- }
+ collapsed={
+   category.id !== "custom" && 
+   category.id !== selectedCategoryId && 
+   !userExpandedCategories.includes(category.id)
+ }
```

This would require adding a `userExpandedCategories` state and updating it on user interaction with the collapse control.

</blockquote></details>
<details>
<summary>web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts (2)</summary><blockquote>

`71-92`: **Good extraction of compression logic into a separate function**

Creating the `getCompressedPlugin` function improves code modularity and reusability. However, there's a minor issue with error handling.

Consider returning a specific error value or throwing a custom error instead of returning undefined to make error handling more explicit:

```diff
try {
  // ...
} catch (error) {
  if (error instanceof Error) {
    setNotification({ type: "error", text: error.message });
  }
- return;
+ return null; // Return null explicitly to indicate failure
}
```

---

`291-317`: **Simplified plugin sharing logic**

The encodeAndSharePlugin function has been streamlined to be more focused on its core responsibility.

The return statement in the catch block is unnecessary since the function signature doesn't return a value:

```diff
catch (error) {
  if (error instanceof Error) {
    setNotification({ type: "error", text: error.message });
  }
- return;
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 14f610468f058807390ad65ac10233f1ceacc234 and 2e10299bba6d822e683b9c80bfaa6a9cbba34874.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `web/src/beta/features/PluginPlayground/Plugins/index.tsx` (7 hunks)
* `web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts` (8 hunks)
* `web/src/beta/features/PluginPlayground/hooks.tsx` (4 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: Redirect rules - reearth-web
* GitHub Check: Header rules - reearth-web
* GitHub Check: ci-server / ci-server-lint
* GitHub Check: Pages changed - reearth-web
* GitHub Check: ci-server / ci-server-test
* GitHub Check: ci-web / ci

</details>

<details>
<summary>🔇 Additional comments (14)</summary><blockquote>

<details>
<summary>web/src/beta/features/PluginPlayground/hooks.tsx (3)</summary>

`50-64`: **Clean integration of the pluginFromURL variable**

The change correctly integrates the new `pluginFromURL` variable from usePlugins while removing the outdated `sharedPlugin` reference. This aligns with the PR's objective of handling plugin state from URLs.

---

`158-158`: **Correctly passing pluginFromURL to Plugins component**

The pluginFromURL variable is properly passed as a prop to the Plugins component, which is necessary for the component to determine the initial plugin state based on URL parameters.

---

`183-183`: **Appropriate dependency inclusion**

Adding pluginFromURL to the dependencies array ensures the component will properly re-render when the URL plugin changes, which is critical for the URL sharing functionality to work correctly.

</details>
<details>
<summary>web/src/beta/features/PluginPlayground/Plugins/index.tsx (7)</summary>

`5-5`: **Correctly added useEffect import**

The useEffect hook has been properly imported to support the new state synchronization logic.

---

`22-22`: **Appropriate type definition update**

Renamed `sharedPlugin` to `pluginFromURL` in the type definition, which better describes its purpose - a plugin retrieved from the URL parameters.

---

`44-46`: **Good state variable addition for category tracking**

Adding the `selectedCategoryId` state variable provides a clean way to track and auto-expand the category containing the selected plugin.

---

`114-121`: **Effective state synchronization implementation**

The useEffect hook appropriately updates the selectedCategoryId based on the current plugin selection, ensuring that the correct category is expanded.

---

`123-128`: **Improved component prop design**

Changing from `selectedPluginId` to `highlighted` makes the component's purpose more explicit and simplifies the parent component's responsibility. This is a good practice that enhances code maintainability.

---

`174-174`: **Correct condition for shared plugin display**

The updated condition correctly checks for the existence of pluginFromURL and its ID, ensuring shared plugins are only displayed when actually present in the URL.

---

`209-219`: **Clean implementation of highlighted prop**

The map function now properly passes the highlighted state directly as a boolean prop, making the code more readable and maintainable.

</details>
<details>
<summary>web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts (4)</summary>

`16-21`: **Improved variable naming and data preparation**

Renamed `sharedPluginUrl` to the more descriptive `pluginURLParam`, and prepared the plugins data structure in a cleaner way by flattening the array.

---

`31-43`: **Simplified error handling for plugin URL decoding**

The logic for handling the plugin from URL has been simplified, with cleaner error handling and more descriptive variable naming.

---

`94-100`: **Excellent implementation of URL state update**

This useEffect hook directly addresses the PR objective by updating the browser URL when a plugin is selected, enabling seamless sharing of plugin selections across tabs or browser sessions.

---

`334-334`: **Consistent API exposure**

The function now correctly returns `pluginFromURL` instead of `sharedPlugin`, maintaining consistent naming across the codebase.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T06:42:27Z
user: login: coderabbitai[bot]
body: > [!CAUTION]
> Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.  If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

**Actionable comments posted: 4**

<details>
<summary>🔭 Outside diff range comments (2)</summary><blockquote>

<details>
<summary>server/internal/usecase/interactor/project.go (1)</summary><blockquote>

`642-680`: _🛠️ Refactor suggestion_

**Refine the import logic to preserve metadata**  
1) The function forcibly sets `alias = ""`, `archived = false`, and `coreSupport = true`. Consider extracting these values from the parsed JSON to maintain original project metadata.  
2) Validate `data` to confirm it is neither `nil` nor empty, and handle partial or incorrect JSON.  
3) Add more descriptive errors (e.g., “missing ‘project’ key in JSON”) for faster debugging.  

Do you want me to generate a follow-up patch that retains the original project metadata and improves logging?

</blockquote></details>
<details>
<summary>server/internal/adapter/gql/resolver_mutation_project.go (1)</summary><blockquote>

`268-283`: _🛠️ Refactor suggestion_

**Scene ID string replacement**  
`replaceOldSceneID` uses `bytes.Replace` to swap out IDs. Ensure that replaced IDs cannot appear as partial matches or inside user-generated text, leading to collisions. If collisions are possible, consider a more structured approach (like JSON manipulation with known field paths) to avoid accidental replacements.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (21)</summary><blockquote>

<details>
<summary>server/internal/adapter/gql/resolver_mutation_project.go (1)</summary><blockquote>

`197-227`: **Consider transactional integrity**  
Multiple operations—creating a project, importing assets, creating a scene, etc.—are performed sequentially. If a later step fails, some resources might be left partially imported. Consider wrapping these calls in a transaction or adding rollback logic for greater reliability.

</blockquote></details>
<details>
<summary>server/internal/usecase/interactor/plugin.go (1)</summary><blockquote>

`70-119`: **Exporting plugins while excluding system plugin**  
Excluding the `"reearth"` system plugin ensures only user-installed plugins are exported. This is a sound choice for preventing duplication of built-in assets. Verify this logic meets the user’s needs if they ever rely on the built-in plugin as a separate entity.

</blockquote></details>
<details>
<summary>web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts (2)</summary><blockquote>

`45-49`: **Use the `SHARED_PLUGIN_ID` constant instead of the hardcoded string.**
You already import `SHARED_PLUGIN_ID` on line 10; using a literal `"shared-plugin-id"` introduces maintenance overhead.


```diff
-  pluginFromURL && pluginFromURL.id === "shared-plugin-id"
+  pluginFromURL && pluginFromURL.id === SHARED_PLUGIN_ID
```

---

`71-92`: **Handle missing plugin scenario before compression.**
If `pluginToCompress` is `undefined` (i.e., not found), a direct `JSON.stringify(pluginToCompress)` will raise an error that’s only caught in the try block. As a minor enhancement, you could check and notify more explicitly if the plugin is not found.


```diff
+      if (!pluginToCompress) {
+        setNotification({ type: "error", text: "Plugin not found." });
+        return;
+      }
       const compressedPlugin = LZString.compressToBase64(
         JSON.stringify(pluginToCompress)
       )
```

</blockquote></details>
<details>
<summary>server/pkg/scene/builder/decoder.go (3)</summary><blockquote>

`31-42`: **Pointer usage in `ParseSceneJSONByByte`.**
Consider an explicit check for `nil` or `len(*data) == 0` before unmarshalling to avoid potential panics. Returning a clearer error message if data is empty would improve maintainability.

---

`43-56`: **Minor naming nitpick for `ParserWidgetAlignSystem`.**
Renaming it to `ParseWidgetAlignSystem` might be more consistent with the naming of similar functions like `ParseSceneJSON`.

---

`131-202`: **Cautiously updating properties within transactions.**
The new logic modifies `PropertyUpdate` to save the property after each field update. If partial updates occur frequently, consider buffering changes for performance. For now, it seems correct.

</blockquote></details>
<details>
<summary>server/internal/usecase/interactor/storytelling.go (6)</summary><blockquote>

`6-6`: **Consider using a logger instead of fmt for production logs.**  
You're using `fmt` for printing unsupported plugins (lines 1010, etc.). While this is fine for quick debugging, using a structured logger (or the existing project logger if available) would be more robust and consistent for production environments.  

```diff
- import (
-     "fmt"
-     ...
- )
+ import (
+     ...
+     "github.com/yourorg/logger" // or any other structured logger
+ )
```

---

`1008-1010`: **Reconsider debug prints.**  
You've placed a debug print for “Unsupported blockJSON.Plugins”. If it’s an expected scenario, you may want a structured warning log or remove it once debugging is complete.  

```diff
-for k, v := blockJSON.Plugins {
-    fmt.Println("Unsupported blockJSON.Plugins ", k, v)
+for k, v := range blockJSON.Plugins {
+    log.Warnf("Encountered unsupported plugins: %s = %v", k, v)
}
```

---

`1034-1034`: **Property updates on page level.**  
`builder.PropertyUpdate(...)` is good for reusing existing property logic. Be sure that no leftover data from older page schemas conflicts here.

---

`1036-1040`: **Handle potential parse errors in page swipeable layers.**  
If `id.NLSLayerIDFrom(...).Err != nil`, some malformed layer strings might appear. Currently you skip them. Consider logging or tracking them to avoid silent data loss.  

```diff
-for _, swipeableLayer := range pageJSON.SwipeableLayers {
-  if id, err := id.NLSLayerIDFrom(swipeableLayer); err == nil {
-    swipeableLayers = append(swipeableLayers, id)
-  }
+for _, sl := range pageJSON.SwipeableLayers {
+  parsedID, parseErr := id.NLSLayerIDFrom(sl)
+  if parseErr != nil {
+    log.Warn("Skipping invalid swipeable layer:", sl)
+    continue
+  }
+  swipeableLayers = append(swipeableLayers, parsedID)
}
```

---

`1043-1046`: **Similarly handle invalid layer IDs.**  
Same logic as for swipeable layers. A small check or log would help catch unexpected config data.

---

`1073-1073`: **Same caution with property updates.**  
Again, confirm `storyJSON.Property` does not conflict with existing story fields.

</blockquote></details>
<details>
<summary>server/internal/usecase/interactor/scene.go (8)</summary><blockquote>

`4-5`: **Confirm necessity of new imports.**  
You're importing `"bytes"` and `"fmt"`. Verify `"fmt"` is essential (it’s used for debug prints in `sceneJSON.Plugins` lines 711-712?). If not, consider removing or using a logger.

---

`143-147`: **Optional default extension widget logic.**  
This block is conditional on `defaultExtensionWidget`. Ensure no duplication occurs if the user triggers it multiple times.

---

`163-187`: **Initialize default visualizer tiles property carefully.**  
The function clarifies which visualizer (Cesium vs. Cesium Beta) is used. Make sure new visualizers or variations are accounted for as well. The code is well-structured, but consider a fallback strategy if an unknown `viz` type is encountered.

---

`190-191`: **Filter usage for extension widget.**  
`filter := Filter(sceneID)` ensures plugin property saving is restricted to the scene. Great for scoping.

---

`271-271`: **Adding new property for widgets.**  
In `AddWidget`, you’re reusing the same approach. This is consistent. Possibly unify with the widget creation in other code paths for even more reuse.

---

`709-712`: **Property update with logging placeholders.**  
You log “Unsupported sceneJSON.Plugins”. Consider a more user-focused or structured log message.

---

`736-746`: **Replacing widget IDs within the raw byte data.**  
This is an interesting step to keep references consistent. Be sure no partial replacements occur if IDs partially overlap. A check or regex might be safer for complex scenarios.

---

`754-755`: **Parsing widget alignment data.**  
`alignSystem` ensures each widget is placed properly. Confirm that partial or invalid alignment data is handled gracefully.

</blockquote></details>

</blockquote></details>

<details>
<summary>🛑 Comments failed to post (4)</summary><blockquote>

<details>
<summary>server/pkg/scene/scene.go (1)</summary><blockquote>

73-78: _⚠️ Potential issue_

**Missing nil check for plugins field.**

The method checks if `s` is nil but doesn't verify if `s.plugins` is nil before accessing it. This could cause a panic if `s.plugins` is nil.


```diff
 func (s *Scene) AddPlugin(plugin *Plugin) bool {
 	if s == nil {
 		return false
 	}
+	if s.plugins == nil {
+		return false
+	}
 	return s.plugins.Add(plugin)
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
func (s *Scene) AddPlugin(plugin *Plugin) bool {
	if s == nil {
		return false
	}
	if s.plugins == nil {
		return false
	}
	return s.plugins.Add(plugin)
}
`````

</details>

<!-- suggestion_end -->

</blockquote></details>
<details>
<summary>server/internal/infrastructure/mongo/property_schema.go (1)</summary><blockquote>

140-140: _⚠️ Potential issue_

**Potential panic risk: accessing slice without length check.**

The code directly accesses `c.Result[0]` without verifying that the slice has elements. This could cause a panic if `c.Result` is empty.


```diff
- return c.Result[0], nil
+ if len(c.Result) == 0 {
+   return nil, errors.New("property schema not found")
+ }
+ return c.Result[0], nil
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
	if len(c.Result) == 0 {
	  return nil, errors.New("property schema not found")
	}
	return c.Result[0], nil
`````

</details>

<!-- suggestion_end -->

</blockquote></details>
<details>
<summary>server/internal/usecase/interactor/plugin.go (1)</summary><blockquote>

121-221: _🛠️ Refactor suggestion_

**Robust plugin import**  
1) `ImportPlugins` properly reconstructs plugins and saves property schemas in the filtered repository context.  
2) Consider partial failure handling and rollback. If one plugin fails to save, it might leave the system in an inconsistent state.  
3) If the plugin list is large, watch out for performance. Possibly introduce batching or streaming.

</blockquote></details>
<details>
<summary>web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts (1)</summary><blockquote>

50-51: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Consider edge cases for empty plugin arrays.**
If `presetPluginsArray` is ever empty, accessing `plugins[0]` could throw an error. Ensure there's at least one plugin available or handle empty arrays gracefully.

Would you like to verify this assumption across the codebase to confirm that `presetPluginsArray` is never empty?

---

🏁 Script executed:

```shell
#!/bin/bash
# Searching for preset plugins definitions to ensure they're non-empty.
rg -A 5 'presetPlugins\s*='
```

Length of output: 555

---



</details>

**Action Required: Safeguard Empty Plugin Arrays**

While investigating, we found that the code in `web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts` initializes state by accessing `plugins[0].id` without checking if the `plugins` (or its source, e.g., `presetPlugins`) array might be empty. The search in `web/src/beta/features/PluginPlayground/hooks.tsx` confirms that `presetPlugins` is being used, but there’s no guarantee from the current definitions that it always contains at least one plugin.

- **Ensure the plugin array is non-empty:** Add a conditional check or provide a sensible fallback when the plugins array is empty.
- **Example Adjustment:**
  ```js
  const defaultPluginId = plugins.length > 0 ? plugins[0].id : null;
  const [selectedPluginId, setSelectedPluginId] = useState(
    pluginFromURL ? pluginFromURL.id : defaultPluginId
  );
  ```
- **Review UI behavior:** Verify that downstream code handles a `null` (or fallback) value for `selectedPluginId` appropriately.

Please update this across the codebase as needed to prevent potential runtime errors.

</blockquote></details>

</blockquote></details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T06:55:41Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f48d5994ad888f1bbfeb5bee95f587e392b90452 and 4ec5c87f29550d42672cab27f8e611f9ccce9d1d.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `web/src/beta/features/PluginPlayground/Plugins/index.tsx` (6 hunks)
* `web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts` (5 hunks)
* `web/src/beta/features/PluginPlayground/hooks.tsx` (4 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* web/src/beta/features/PluginPlayground/hooks.tsx
* web/src/beta/features/PluginPlayground/Plugins/index.tsx

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: Redirect rules - reearth-web
* GitHub Check: Header rules - reearth-web
* GitHub Check: Pages changed - reearth-web

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts (8)</summary>

`5-5`: **Good addition of useEffect import**

The addition of useEffect import is necessary for the new history state updating functionality. This follows React best practices.

---

`10-10`: **Good use of constant for shared plugin ID**

Using the `SHARED_PLUGIN_ID` constant provides better consistency and makes the code more maintainable than hardcoded strings.

---

`16-22`: **Cleaner implementation of search params handling**

The refactored code improves readability by using more descriptive variable names (`sharedPluginURLParam` instead of `sharedPluginUrl`) and leveraging modern array methods (`map` and `flat`) for processing preset plugins.

---

`32-36`: **Improved type safety and simplified code**

Adding the type annotation for `sharedPlugin` and simplifying the `decodePluginURL` call improves type safety and code readability.

---

`47-57`: **Better handling of plugin initialization logic**

The improved conditional logic for state initialization properly checks for the existence of a shared plugin with the correct ID. The priority order for selecting the initial plugin ID (shared plugin → URL param → first plugin) is now more explicit and easier to follow.

---

`76-80`: **Great implementation of URL updating**

This new useEffect hook fulfills the PR objective by updating the browser URL on plugin selection. Using `replaceState` instead of `pushState` is a good choice here to avoid cluttering the browser history.

---

`271-299`: **Improved plugin sharing implementation**

The refactored `encodeAndSharePlugin` function is more flexible by taking a plugin ID parameter rather than assuming `selectedPlugin`. This allows sharing any plugin, not just the currently selected one. Also, properly setting the ID to `SHARED_PLUGIN_ID` before compression ensures consistency in handling shared plugins.

---

`314-314`: **Good addition to returned values**

Exposing the `sharedPlugin` in the return value makes this information available to consumer components, which is a good practice for reusability.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T02:55:25Z
user: login: mulengawilfred
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T03:09:11Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T03:09:56Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>web/src/beta/features/PluginPlayground/Plugins/presets/useTitles.ts (1)</summary><blockquote>

`62-62`: **Good use of a consistent ID for shared plugins**

Hardcoding this UUID for the shared plugin is reasonable for translation consistency. However, consider importing the `SHARED_PLUGIN_ID` constant here instead of duplicating the UUID value. This would ensure that if the ID needs to change in the future, you only need to update it in one place.

```diff
- "e99982f9-143a-44db-9869-b2bd90578190": t("Shared Plugin") // NOTE: needed to hardcode this part to help translation
+ import { SHARED_PLUGIN_ID } from "../constants";
+ [SHARED_PLUGIN_ID]: t("Shared Plugin") // NOTE: needed to use constant to help translation
```

</blockquote></details>
<details>
<summary>web/src/beta/features/PluginPlayground/Plugins/index.tsx (1)</summary><blockquote>

`152-162`: **Consider memoizing plugin items for performance**

The plugin items are re-created on each render. Consider memoizing them for better performance, especially if there are many plugins.

```diff
+ import { FC, useState, useEffect, useMemo } from "react";

// Later in the component:
+ const categoryPluginItems = useMemo(() => {
+   return category.plugins.map((plugin) => (
+     <PluginEntryItem
+       highlighted={selectedPlugin.id === plugin.id}
+       key={plugin.id}
+       onSelect={selectPlugin}
+       pluginId={plugin.id}
+       title={pluginTitles[plugin.id]}
+     />
+   ));
+ }, [category.plugins, selectedPlugin.id, selectPlugin, pluginTitles]);

// And in the JSX:
- {category.plugins.map((plugin) => {
-   return (
-     <PluginEntryItem
-       highlighted={selectedPlugin.id === plugin.id}
-       key={plugin.id}
-       onSelect={selectPlugin}
-       pluginId={plugin.id}
-       title={pluginTitles[plugin.id]}
-     />
-   );
- })}
+ {categoryPluginItems}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4ec5c87f29550d42672cab27f8e611f9ccce9d1d and 950965d7603080d4df0a4e8c11e5f4a679c3f699.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `web/src/beta/features/PluginPlayground/Plugins/index.tsx` (6 hunks)
* `web/src/beta/features/PluginPlayground/Plugins/presets/useTitles.ts` (1 hunks)
* `web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts` (5 hunks)
* `web/src/beta/features/PluginPlayground/hooks.tsx` (4 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* web/src/beta/features/PluginPlayground/hooks.tsx
* web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: Redirect rules - reearth-web
* GitHub Check: Header rules - reearth-web
* GitHub Check: Pages changed - reearth-web
* GitHub Check: ci-server / ci-server-lint
* GitHub Check: ci-web / ci
* GitHub Check: ci-server / ci-server-test

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>web/src/beta/features/PluginPlayground/Plugins/index.tsx (6)</summary>

`46-48`: **LGTM: Good addition of state for tracking selected category**

Adding this state variable improves the user experience by tracking which category should be expanded.

---

`66-71`: **Good addition of highlighted prop**

Adding the `highlighted` prop to the PluginEntryItem component improves visual feedback for users.

---

`117-137`: **Using constant for shared plugin ID**

Good use of the `SHARED_PLUGIN_ID` constant for consistency. This change supports the PR objective of improving how shared plugins are handled.

---

`142-144`: **Improved category collapsing logic**

The update to the category collapse logic ensures that the category containing the selected plugin is always expanded, which enhances usability.

---

`5-5`: **LGTM: Imports updated for new hooks**

Added imports for useState and useEffect to support the new functionality.

---

`7-7`: **LGTM: Good use of constants**

Importing SHARED_PLUGIN_ID from a constants file is a good practice for maintaining consistency across the application.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T03:12:17Z
user: login: mulengawilfred
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T03:15:08Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T03:15:35Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>web/src/beta/features/PluginPlayground/Plugins/index.tsx (1)</summary><blockquote>

`178-178`: _⚠️ Potential issue_

**Add null check for selectedPlugin**

Similar to line 129, there's a potential null reference issue here. The component uses `selectedPlugin.files` without checking if `selectedPlugin` is defined, which could cause runtime errors.


Apply this fix:
```diff
- {selectedPlugin.files.map((file) => (
+ {selectedPlugin?.files.map((file) => (
```

Also consider adding a fallback for when selectedPlugin is undefined:
```diff
- {selectedPlugin.files.map((file) => (
+ {selectedPlugin?.files?.map((file) => (
```
Or even better, add a guard clause:
```diff
<FileList>
  <FileSubList>
+   {!selectedPlugin ? (
+     <EmptyTip>
+       <Typography size="body" color="weak" trait="italic">
+         {t("No plugin selected")}
+       </Typography>
+     </EmptyTip>
+   ) : (
    {selectedPlugin.files.map((file) => (
      <FileListItem
        key={file.id}
        file={file}
        selected={selectedFile.id === file.id}
        confirmFileTitle={updateFileTitle}
        deleteFile={deleteFile}
        onClick={() => selectFile(file.id)}
      />
    ))}
+   )}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>web/src/beta/features/PluginPlayground/Plugins/index.tsx (1)</summary><blockquote>

`129-134`: _⚠️ Potential issue_

**Add null check for selectedPlugin**

There's a potential null reference issue here. The component uses `selectedPlugin.id` without checking if `selectedPlugin` is defined first, which could cause runtime errors.


Apply this fix:
```diff
- highlighted={selectedPlugin.id === sharedPlugin.id}
+ highlighted={selectedPlugin?.id === sharedPlugin.id}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>web/src/beta/features/PluginPlayground/Plugins/index.tsx (1)</summary><blockquote>

`50-53`: **Consider providing visual feedback for sharing action**

Since this PR aims to improve the URL sharing functionality, consider providing some visual feedback to the user when `encodeAndSharePlugin` is called, such as a toast message confirming the URL has been updated/copied.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 950965d7603080d4df0a4e8c11e5f4a679c3f699 and aac7994bfde9f95ff12dc7b447e8f9a9cc6bf522.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `web/src/beta/features/PluginPlayground/Plugins/index.tsx` (6 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: Redirect rules - reearth-web
* GitHub Check: Header rules - reearth-web
* GitHub Check: Pages changed - reearth-web
* GitHub Check: ci-server / ci-server-test
* GitHub Check: ci-server / ci-server-lint
* GitHub Check: ci-web / ci

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>web/src/beta/features/PluginPlayground/Plugins/index.tsx (7)</summary>

`5-7`: **Good addition of required imports**

The addition of React hooks and the constant for shared plugin ID improves code maintainability. Extracting the shared plugin ID to a constant file follows best practices by centralizing magic values.

---

`46-48`: **LGTM: New state for tracking selected category**

Adding this state variable helps track the currently selected category, which is essential for maintaining UI state when a plugin is selected.

---

`57-65`: **Good implementation of category auto-selection**

The useEffect correctly sets the selected category based on the plugin selection, which improves user experience by automatically expanding the relevant category. The null check for `selectedPlugin` is a good addition as recommended in previous reviews.

---

`67-72`: **Improved prop naming with `highlighted`**

Changing from a generic "selected" to a more descriptive "highlighted" prop improves code readability and better indicates the visual state being represented.

---

`118-118`: **Good use of constant instead of magic string**

Using the imported `SHARED_PLUGIN_ID` constant instead of a hardcoded string improves maintainability and consistency.

---

`143-145`: **Good category expansion logic**

The updated logic for determining when a category should be collapsed improves the user experience by ensuring relevant categories are automatically expanded when related plugins are selected.

---

`153-163`: **Improved plugin list rendering**

The refactored code for rendering plugins is cleaner and more consistent with the proper use of the `highlighted` prop.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T03:17:44Z
user: login: airslice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T04:17:48Z
user: login: mulengawilfred
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T04:30:14Z
user: login: mulengawilfred
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T04:30:19Z
user: login: airslice
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T04:34:08Z

Review comments:
diff_hunk: @@ -260,39 +269,33 @@ export default () => {
   }, [selectedPlugin, setNotification]);
 
   const encodeAndSharePlugin = useCallback(
-    (pluginId: string): string | undefined => {
-      selectPlugin(pluginId);
-      // Need to "find" the selectedPlugin as it does not get updated immediately
-      const sharedPlugin = plugins.find((plugin) => plugin.id === pluginId);
-
-      // Note: We can't use the same id for a shared plugin
-      const selectedPluginCopy = { ...sharedPlugin };
-
-      // First compress the code
+    (pluginId: string) => {
       try {
+        const pluginToShare = plugins.find((plugin) => plugin.id === pluginId);
+        const selectedPluginCopy = { ...pluginToShare, id: SHARED_PLUGIN_ID };
+
         const compressed = LZString.compressToBase64(
           JSON.stringify(selectedPluginCopy)
         )
-          .replace(/\+/g, "-") // Convert + to -
-          .replace(/\//g, "_") // Convert / to _
-          .replace(/=/g, ""); // Remove padding =
+          .replace(/\+/g, "-")
+          .replace(/\//g, "_")
+          .replace(/=/g, "");
 
-        const shareUrl = `${window.location.origin}${window.location.pathname}?plugin=${compressed}`;
+        const shareUrl = `${window.location.origin}${window.location.pathname}?shared-plugin-url=${compressed}`;
path: web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify URL parameter consistency**

The code is using `?plugin=` in the history state (line 78) but `?shared-plugin-url=` for sharing (line 284). This might cause confusion if both parameters appear in the URL. Consider standardizing the parameter naming pattern.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for other usages of these URL parameters across the codebase

# Check for usages of plugin-id parameter
echo "Checking for plugin-id parameter usages:"
rg "plugin-id" --type ts

# Check for usages of plugin parameter
echo "Checking for plugin parameter usages:"
rg "\?plugin=" --type ts

# Check for usages of shared-plugin-url parameter
echo "Checking for shared-plugin-url parameter usages:"
rg "shared-plugin-url" --type ts
```

Length of output: 923

---



</details>

**Ensure Consistent Naming for Plugin URL Parameters**

In the file `web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts` we’ve identified three different URL parameters in use:
- **`plugin-id`** – used when retrieving the plugin ID (via `searchParams.get("plugin-id")`).
- **`plugin`** – used in the history state update (via `window.history.replaceState`).
- **`shared-plugin-url`** – used for generating and reading the share URL.

The current inconsistency may lead to confusion and potential bugs if multiple parameters appear in the URL. Please consider unifying these parameters into a single, clear naming pattern across the file (or clarifying their distinct roles if they must remain different).

<!-- This is an auto-generated reply by CodeRabbit -->

✅ Fixed in commits c83f666 to 950965d

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-13T02:55:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -260,39 +269,33 @@ export default () => {
   }, [selectedPlugin, setNotification]);
 
   const encodeAndSharePlugin = useCallback(
-    (pluginId: string): string | undefined => {
-      selectPlugin(pluginId);
-      // Need to "find" the selectedPlugin as it does not get updated immediately
-      const sharedPlugin = plugins.find((plugin) => plugin.id === pluginId);
-
-      // Note: We can't use the same id for a shared plugin
-      const selectedPluginCopy = { ...sharedPlugin };
-
-      // First compress the code
+    (pluginId: string) => {
       try {
+        const pluginToShare = plugins.find((plugin) => plugin.id === pluginId);
+        const selectedPluginCopy = { ...pluginToShare, id: SHARED_PLUGIN_ID };
+
         const compressed = LZString.compressToBase64(
           JSON.stringify(selectedPluginCopy)
         )
-          .replace(/\+/g, "-") // Convert + to -
-          .replace(/\//g, "_") // Convert / to _
-          .replace(/=/g, ""); // Remove padding =
+          .replace(/\+/g, "-")
+          .replace(/\//g, "_")
+          .replace(/=/g, "");
 
-        const shareUrl = `${window.location.origin}${window.location.pathname}?plugin=${compressed}`;
+        const shareUrl = `${window.location.origin}${window.location.pathname}?shared-plugin-url=${compressed}`;
path: web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts
user: login: mulengawilfred
body: This has been addressed. Will keep only to URL parameters: `plugin-id` and `shared-plugin-url` 
created_at: 2025-03-13T03:09:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -260,39 +269,33 @@ export default () => {
   }, [selectedPlugin, setNotification]);
 
   const encodeAndSharePlugin = useCallback(
-    (pluginId: string): string | undefined => {
-      selectPlugin(pluginId);
-      // Need to "find" the selectedPlugin as it does not get updated immediately
-      const sharedPlugin = plugins.find((plugin) => plugin.id === pluginId);
-
-      // Note: We can't use the same id for a shared plugin
-      const selectedPluginCopy = { ...sharedPlugin };
-
-      // First compress the code
+    (pluginId: string) => {
       try {
+        const pluginToShare = plugins.find((plugin) => plugin.id === pluginId);
+        const selectedPluginCopy = { ...pluginToShare, id: SHARED_PLUGIN_ID };
+
         const compressed = LZString.compressToBase64(
           JSON.stringify(selectedPluginCopy)
         )
-          .replace(/\+/g, "-") // Convert + to -
-          .replace(/\//g, "_") // Convert / to _
-          .replace(/=/g, ""); // Remove padding =
+          .replace(/\+/g, "-")
+          .replace(/\//g, "_")
+          .replace(/=/g, "");
 
-        const shareUrl = `${window.location.origin}${window.location.pathname}?plugin=${compressed}`;
+        const shareUrl = `${window.location.origin}${window.location.pathname}?shared-plugin-url=${compressed}`;
path: web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

@mulengawilfred Thanks for addressing this issue! I notice in your PR that the sharing URL now consistently uses `shared-plugin-url`. However, I'm still seeing the `window.history.replaceState` call using `?plugin=`. To fully standardize on just the two URL parameters (`plugin-id` and `shared-plugin-url`), could you also update this line to use one of these parameters instead?

---

🏁 Script executed:
```
#!/bin/bash
# Check the current implementation of the window.history.replaceState call
rg -A 2 -B 2 "replaceState" web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts
```

Length of output: 211

---



</details> @mulengawilfred Great! I see that you've already updated the `window.history.replaceState` call to use `plugin-id=${selectedPluginId}` as well. This ensures complete consistency with your stated approach of using only `plugin-id` and `shared-plugin-url` as URL parameters. The changes look good now. Thanks for addressing this so thoroughly!

--- <!-- This is an auto-generated comment: tweet message by coderabbit.ai --> If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI &text =Great%20advice%20from%20%40coderabbitai!%20Standardizing%20URL%20parameters%20can%20prevent%20confusion%20and%20bugs.%20Let's%20keep%20our%20code%20clean%20and%20consistent!%20%F0%9F%9A%80%F0%9F%92%BB &url =https%3A%2F%2Fgithub.com%2Freearth%2Freearth-visualizer%2Fpull%2F1487%23discussion_r1992601009)?
							Thank you for using CodeRabbit! <!-- end of auto-generated comment: tweet message by coderabbit.ai -->



<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

@mulengawilfred Thanks for addressing this issue! I notice in your PR that the sharing URL now consistently uses `shared-plugin-url`. However, I'm still seeing the `window.history.replaceState` call using `?plugin=`. To fully standardize on just the two URL parameters (`plugin-id` and `shared-plugin-url`), could you also update this line to use one of these parameters instead?

---

🏁 Script executed:
```
#!/bin/bash
# Check the current implementation of the window.history.replaceState call
rg -A 2 -B 2 "replaceState" web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts
```

Length of output: 211

---



</details>

@mulengawilfred Great! I see that you've already updated the `window.history.replaceState` call to use `plugin-id=${selectedPluginId}` as well. This ensures complete consistency with your stated approach of using only `plugin-id` and `shared-plugin-url` as URL parameters. The changes look good now. Thanks for addressing this so thoroughly!

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Great%20advice%20from%20%40coderabbitai!%20Standardizing%20URL%20parameters%20can%20prevent%20confusion%20and%20bugs.%20Let's%20keep%20our%20code%20clean%20and%20consistent!%20%F0%9F%9A%80%F0%9F%92%BB&url=https%3A%2F%2Fgithub.com%2Freearth%2Freearth-visualizer%2Fpull%2F1487%23discussion_r1992601009)?
							Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-13T03:09:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,16 +54,25 @@ const Plugins: FC<Props> = ({
 
   const { categoryTitles, pluginTitles } = useTitles();
 
+  useEffect(() => {
+    const selectedCategory = presetPlugins.find((category) =>
+      category.plugins.find((plugin) => plugin.id === selectedPlugin.id)
+    );
+    if (selectedCategory) {
+      setSelectedCategoryId(selectedCategory.id);
+    }
+  }, [presetPlugins, selectedPlugin]);
path: web/src/beta/features/PluginPlayground/Plugins/index.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Add null check for selectedPlugin**

The useEffect doesn't handle the case where `selectedPlugin` might be undefined or null, which could lead to runtime errors. 

```diff
  useEffect(() => {
+   if (!selectedPlugin) return;
    const selectedCategory = presetPlugins.find((category) =>
      category.plugins.find((plugin) => plugin.id === selectedPlugin.id)
    );
    if (selectedCategory) {
      setSelectedCategoryId(selectedCategory.id);
    }
  }, [presetPlugins, selectedPlugin]);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
  useEffect(() => {
    if (!selectedPlugin) return;
    const selectedCategory = presetPlugins.find((category) =>
      category.plugins.find((plugin) => plugin.id === selectedPlugin.id)
    );
    if (selectedCategory) {
      setSelectedCategoryId(selectedCategory.id);
    }
  }, [presetPlugins, selectedPlugin]);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->

✅ Fixed in commit aac7994
created_at: 2025-03-13T03:12:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,16 +54,25 @@ const Plugins: FC<Props> = ({
 
   const { categoryTitles, pluginTitles } = useTitles();
 
+  useEffect(() => {
+    const selectedCategory = presetPlugins.find((category) =>
+      category.plugins.find((plugin) => plugin.id === selectedPlugin.id)
+    );
+    if (selectedCategory) {
+      setSelectedCategoryId(selectedCategory.id);
+    }
+  }, [presetPlugins, selectedPlugin]);
path: web/src/beta/features/PluginPlayground/Plugins/index.tsx
user: login: mulengawilfred
body: Thanks. Added this.
created_at: 2025-03-13T03:15:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,16 +54,25 @@ const Plugins: FC<Props> = ({
 
   const { categoryTitles, pluginTitles } = useTitles();
 
+  useEffect(() => {
+    const selectedCategory = presetPlugins.find((category) =>
+      category.plugins.find((plugin) => plugin.id === selectedPlugin.id)
+    );
+    if (selectedCategory) {
+      setSelectedCategoryId(selectedCategory.id);
+    }
+  }, [presetPlugins, selectedPlugin]);
path: web/src/beta/features/PluginPlayground/Plugins/index.tsx
user: login: coderabbitai[bot]
body: @mulengawilfred Great! Happy to help. The null check will prevent potential runtime errors. Let me know if you need any further assistance with your PR.

--- <!-- This is an auto-generated comment: tweet message by coderabbit.ai --> If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI &text =Just%20implemented%20%40coderabbitai's%20advice%20to%20add%20a%20null%20check%20for%20%60selectedPlugin%60%20in%20useEffect!%20%F0%9F%9A%80%20This%20prevents%20potential%20runtime%20errors%20and%20improves%20code%20stability.%20Check%20it%20out!%20%F0%9F%94%A7 &url =https%3A%2F%2Fgithub.com%2Freearth%2Freearth-visualizer%2Fpull%2F1487%23discussion_r1992612383)?
							Thank you for using CodeRabbit! <!-- end of auto-generated comment: tweet message by coderabbit.ai -->



<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@mulengawilfred Great! Happy to help. The null check will prevent potential runtime errors. Let me know if you need any further assistance with your PR.

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Just%20implemented%20%40coderabbitai's%20advice%20to%20add%20a%20null%20check%20for%20%60selectedPlugin%60%20in%20useEffect!%20%F0%9F%9A%80%20This%20prevents%20potential%20runtime%20errors%20and%20improves%20code%20stability.%20Check%20it%20out!%20%F0%9F%94%A7&url=https%3A%2F%2Fgithub.com%2Freearth%2Freearth-visualizer%2Fpull%2F1487%23discussion_r1992612383)?
							Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-13T03:15:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -260,39 +267,33 @@ export default () => {
   }, [selectedPlugin, setNotification]);
 
   const encodeAndSharePlugin = useCallback(
-    (pluginId: string): string | undefined => {
-      selectPlugin(pluginId);
-      // Need to "find" the selectedPlugin as it does not get updated immediately
-      const sharedPlugin = plugins.find((plugin) => plugin.id === pluginId);
-
-      // Note: We can't use the same id for a shared plugin
-      const selectedPluginCopy = { ...sharedPlugin };
-
-      // First compress the code
+    (pluginId: string) => {
       try {
+        const pluginToShare = plugins.find((plugin) => plugin.id === pluginId);
+        const selectedPluginCopy = { ...pluginToShare, id: SHARED_PLUGIN_ID };
+
         const compressed = LZString.compressToBase64(
           JSON.stringify(selectedPluginCopy)
         )
-          .replace(/\+/g, "-") // Convert + to -
-          .replace(/\//g, "_") // Convert / to _
-          .replace(/=/g, ""); // Remove padding =
+          .replace(/\+/g, "-")
+          .replace(/\//g, "_")
+          .replace(/=/g, "");
 
-        const shareUrl = `${window.location.origin}${window.location.pathname}?plugin=${compressed}`;
+        const shareUrl = `${window.location.origin}${window.location.pathname}?shared-plugin-url=${compressed}`;
path: web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts
user: login: airslice
body: ```suggestion
        const shareUrl = `${window.location.origin}${window.location.pathname}?shared=${compressed}`;
```
Could we just use `shared-plugin` or `shared`, the value is base64 string not url.
created_at: 2025-03-13T04:16:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,7 +59,7 @@ export default () => {
       "zoom-in-out": t("Zoom In Out"),
       "camera-rotation": t("Camera Rotation"),
       "camera-position": t("Camera Position"),
-      "shared-plugin-id": t("Shared Plugin")
+      "e99982f9-143a-44db-9869-b2bd90578190": t("Shared Plugin") // NOTE: needed to hardcode this part to help translation
path: web/src/beta/features/PluginPlayground/Plugins/presets/useTitles.ts
user: login: airslice
body: ```suggestion
      "shared-plugin": t("Shared Plugin") // NOTE: needed to hardcode this part to help translation
```
Could we use this instead of a hardcoded uuid?
created_at: 2025-03-13T04:17:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,7 +59,7 @@ export default () => {
       "zoom-in-out": t("Zoom In Out"),
       "camera-rotation": t("Camera Rotation"),
       "camera-position": t("Camera Position"),
-      "shared-plugin-id": t("Shared Plugin")
+      "e99982f9-143a-44db-9869-b2bd90578190": t("Shared Plugin") // NOTE: needed to hardcode this part to help translation
path: web/src/beta/features/PluginPlayground/Plugins/presets/useTitles.ts
user: login: mulengawilfred
body: done
created_at: 2025-03-13T04:30:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -260,39 +267,33 @@ export default () => {
   }, [selectedPlugin, setNotification]);
 
   const encodeAndSharePlugin = useCallback(
-    (pluginId: string): string | undefined => {
-      selectPlugin(pluginId);
-      // Need to "find" the selectedPlugin as it does not get updated immediately
-      const sharedPlugin = plugins.find((plugin) => plugin.id === pluginId);
-
-      // Note: We can't use the same id for a shared plugin
-      const selectedPluginCopy = { ...sharedPlugin };
-
-      // First compress the code
+    (pluginId: string) => {
       try {
+        const pluginToShare = plugins.find((plugin) => plugin.id === pluginId);
+        const selectedPluginCopy = { ...pluginToShare, id: SHARED_PLUGIN_ID };
+
         const compressed = LZString.compressToBase64(
           JSON.stringify(selectedPluginCopy)
         )
-          .replace(/\+/g, "-") // Convert + to -
-          .replace(/\//g, "_") // Convert / to _
-          .replace(/=/g, ""); // Remove padding =
+          .replace(/\+/g, "-")
+          .replace(/\//g, "_")
+          .replace(/=/g, "");
 
-        const shareUrl = `${window.location.origin}${window.location.pathname}?plugin=${compressed}`;
+        const shareUrl = `${window.location.origin}${window.location.pathname}?shared-plugin-url=${compressed}`;
path: web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts
user: login: mulengawilfred
body: done
created_at: 2025-03-13T04:30:19Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-11T06:39:31Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request updates the PluginPlayground functionality by introducing a new state variable to track the selected category and adjusting component props to include a highlighted indication for plugins. It refines the logic for handling shared plugins by using a dedicated constant (`SHARED_PLUGIN_ID`), updates URL parameter processing and history state management in the hook, and ensures that the shared plugin information is consistently passed across components.

## Changes

| File(s) | Summary |
| --- | --- |
| `web/src/beta/features/PluginPlayground/Plugins/index.tsx`<br>`web/src/beta/features/PluginPlayground/hooks.tsx` | Introduced `selectedCategoryId` in the Plugins component; modified `PluginEntryItem` props to include `highlighted`; updated rendering logic for shared plugins; reintroduced and consistently passed the `sharedPlugin` prop. |
| `web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts` | Refactored shared plugin handling by replacing `sharedPluginUrl` with `sharedPluginURLParam`, simplifying URL decoding, updating plugin state initialization and history state via `useEffect`, and revising `encodeAndSharePlugin` to apply `SHARED_PLUGIN_ID`. |
| `web/src/beta/features/PluginPlayground/Plugins/constants.ts` | Added new constant `SHARED_PLUGIN_ID` with a fixed UUID for shared plugin identification. |
| `web/src/beta/features/PluginPlayground/Plugins/presets/useTitles.ts` | Modified key in `pluginTitles` object from `"shared-plugin-id"` to a hardcoded UUID string while maintaining the associated value for translation. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant U as User
    participant P as Plugins Component
    participant H as usePlugins Hook
    participant B as Browser

    U->>P: Select a plugin
    P->>P: Update selectedCategoryId state
    P->>H: Propagate selection to determine selectedPluginId
    H->>B: Update browser history with current selection
```

```mermaid
sequenceDiagram
    participant U as User
    participant P as Plugins Component
    participant H as usePlugins Hook

    U->>P: Initiate share action
    P->>H: Invoke encodeAndSharePlugin()
    H->>P: Return shared plugin URL with SHARED_PLUGIN_ID
```

## Possibly related PRs
- **reearth/reearth-visualizer#1358**: The changes in the main PR are related to the modifications in the `Plugins` component's props and rendering logic, which directly interact with the handling of the `sharedPlugin` variable that is also addressed in the retrieved PR.
- **reearth/reearth-visualizer#1307**: The changes in the main PR and the retrieved PR are related as both modify the `usePlugins` hook and the `Plugins` component to handle the `sharedPlugin` variable, ensuring consistent state management and prop usage.
- **reearth/reearth-visualizer#1482**: The changes in the main PR are related to the modifications in the `Plugins` component, specifically regarding the handling of the `sharedPlugin` and its integration with the UI, which aligns with the updates made to the title property in the retrieved PR.

## Suggested reviewers
- mkumbobeaty
- airslice
- OpeDada
- ZTongci

## Poem
> I hopped through lines of code so neat,  
> Adding states and tweaks with rabbit feet.  
> Shared plugins shine with a secret ID,  
> Guiding each click where they should be.  
> In this code garden, bugs take flight—  
> A joyful hop into a clearer night!  
> 🐰💻

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡🧪 Multi-step agentic review comment chat (experimental)</summary>
> 
> - We're introducing multi-step agentic chat in review comments. This experimental feature enhances review discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments.
> 						- To enable this feature, set early_access to true under in the settings.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between aac7994bfde9f95ff12dc7b447e8f9a9cc6bf522 and 5277e5e8aedb7e51dbbea0ebe65cad8024f7eda3.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `web/src/beta/features/PluginPlayground/Plugins/constants.ts` (1 hunks)
* `web/src/beta/features/PluginPlayground/Plugins/presets/useTitles.ts` (1 hunks)
* `web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts` (5 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* web/src/beta/features/PluginPlayground/Plugins/constants.ts

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* web/src/beta/features/PluginPlayground/Plugins/presets/useTitles.ts
* web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: ci-server / ci-server-lint
* GitHub Check: ci-web / ci
* GitHub Check: ci-server / ci-server-test
* GitHub Check: Redirect rules - reearth-web
* GitHub Check: Header rules - reearth-web
* GitHub Check: Pages changed - reearth-web

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=reearth/reearth-visualizer&utm_content=1487):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: netlify[bot]
created_at: 2025-03-11T06:39:39Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *reearth-web* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 22f9d469196f828e21330b4c75d7620e1b7d97b5 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/reearth-web/deploys/67d261c55374a00008741fa8 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1487--reearth-web.netlify.app](https://deploy-preview-1487--reearth-web.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTE0ODctLXJlZWFydGgtd2ViLm5ldGxpZnkuYXBwIn0.LT0yTqgbzBfaqsJdKEU3hjw1CaV6Zo_6GNa293yZxH4)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [reearth-web Preview](https://deploy-preview-1487--reearth-web.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/reearth-web/configuration/notifications#deploy-webhooks)._
author_association: NONE