From 31f6e2840f2b6d9f017ccbfd3985e4bb890d58f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:21:23 +0000 Subject: [PATCH 1/5] Initial plan From 6bce36188e123b0f39f9a3a6e3a58ab90c0c8efe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:36:06 +0000 Subject: [PATCH 2/5] Add preview panel feature to model gallery page Co-authored-by: tjyyy3 <97741063+tjyyy3@users.noreply.github.com> --- app/components/gallery/ModelPreviewPanel.tsx | 192 +++++++++++++++++++ app/components/ui/sheet.tsx | 114 +++++++++++ app/gallery/page.tsx | 87 ++++++++- messages/en.json | 8 +- package.json | 1 + yarn.lock | 147 ++++++++++---- 6 files changed, 512 insertions(+), 37 deletions(-) create mode 100644 app/components/gallery/ModelPreviewPanel.tsx create mode 100644 app/components/ui/sheet.tsx diff --git a/app/components/gallery/ModelPreviewPanel.tsx b/app/components/gallery/ModelPreviewPanel.tsx new file mode 100644 index 00000000..4dca96e7 --- /dev/null +++ b/app/components/gallery/ModelPreviewPanel.tsx @@ -0,0 +1,192 @@ +// Copyright 2024 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client'; +import React from 'react'; +import { useRouter } from 'next/navigation'; +import { clsx } from 'clsx'; +import { ExternalLink } from 'lucide-react'; +import { useLang } from '@/app/context/LangContext'; +import { example } from '@/app/components/editor/casbin-mode/example'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from '@/app/components/ui/sheet'; + +interface ModelPreviewPanelProps { + modelKey: string | null; + modelName: string; + modelDescription: string; + modelCategory: string; + isOpen: boolean; + onClose: () => void; +} + +export const ModelPreviewPanel: React.FC = ({ + modelKey, + modelName, + modelDescription, + modelCategory, + isOpen, + onClose, +}) => { + const { theme, t } = useLang(); + const router = useRouter(); + + const modelData = modelKey ? example[modelKey] : null; + + const handleOpenInEditor = () => { + if (modelKey) { + router.push(`/?model=${modelKey}`); + onClose(); + } + }; + + const textClass = clsx(theme === 'dark' ? 'text-gray-200' : 'text-gray-800'); + const bgClass = clsx(theme === 'dark' ? 'bg-slate-900' : 'bg-white'); + const sectionBgClass = clsx(theme === 'dark' ? 'bg-slate-800' : 'bg-slate-50'); + const borderClass = clsx(theme === 'dark' ? 'border-slate-700' : 'border-slate-200'); + + return ( + { + if (!open) { + onClose(); + } + }}> + + +
+ {t(modelName)} + + {t(modelCategory)} + +
+ + {t(modelDescription)} + +
+ + {modelData && ( +
+ {/* Model Configuration */} +
+

+ {t('Model Configuration')} +

+
+
+                  {modelData.model}
+                
+
+
+ + {/* Example Policies */} + {modelData.policy && ( +
+

+ {t('Example Policies')} +

+
+
+                    {modelData.policy}
+                  
+
+
+ )} + + {/* Example Request */} + {modelData.request && ( +
+

+ {t('Example Request')} +

+
+
+                    {modelData.request}
+                  
+
+
+ )} + + {/* Custom Configuration */} + {modelData.customConfig && ( +
+

+ {t('Custom Configuration')} +

+
+
+                    {modelData.customConfig}
+                  
+
+
+ )} +
+ )} + + {/* Footer with Open in Editor button */} + + + +
+
+ ); +}; diff --git a/app/components/ui/sheet.tsx b/app/components/ui/sheet.tsx new file mode 100644 index 00000000..2340a489 --- /dev/null +++ b/app/components/ui/sheet.tsx @@ -0,0 +1,114 @@ +'use client'; + +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; + +import { cn } from '@/app/utils/lib/utils'; + +const Sheet = DialogPrimitive.Root; + +const SheetTrigger = DialogPrimitive.Trigger; + +const SheetClose = DialogPrimitive.Close; + +const SheetPortal = DialogPrimitive.Portal; + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); +SheetOverlay.displayName = DialogPrimitive.Overlay.displayName; + +interface SheetContentProps extends React.ComponentPropsWithoutRef { + side?: 'top' | 'bottom' | 'left' | 'right'; +} + +const SheetContent = React.forwardRef, SheetContentProps>( + ({ side = 'right', className, children, ...props }, ref) => { + const sideVariants = { + // eslint-disable-next-line max-len + top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', + bottom: + // eslint-disable-next-line max-len + 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', + // eslint-disable-next-line max-len + left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm', + right: + // eslint-disable-next-line max-len + 'inset-y-0 right-0 h-full w-full sm:w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-xl', + }; + + return ( + + + + {children} + + + Close + + + + ); + }, +); +SheetContent.displayName = DialogPrimitive.Content.displayName; + +const SheetHeader = ({ className, ...props }: React.HTMLAttributes) => { + return
; +}; +SheetHeader.displayName = 'SheetHeader'; + +const SheetFooter = ({ className, ...props }: React.HTMLAttributes) => { + return
; +}; +SheetFooter.displayName = 'SheetFooter'; + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); +SheetTitle.displayName = DialogPrimitive.Title.displayName; + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ; +}); +SheetDescription.displayName = DialogPrimitive.Description.displayName; + +export { Sheet, SheetPortal, SheetOverlay, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription }; diff --git a/app/gallery/page.tsx b/app/gallery/page.tsx index 35419592..699bfacf 100644 --- a/app/gallery/page.tsx +++ b/app/gallery/page.tsx @@ -16,7 +16,7 @@ import React, { useState } from 'react'; import { useRouter } from 'next/navigation'; import { clsx } from 'clsx'; -import { ArrowLeft, ExternalLink, Search } from 'lucide-react'; +import { ArrowLeft, ExternalLink, Search, Eye } from 'lucide-react'; import { useLang } from '@/app/context/LangContext'; import { modelMetadata, categories } from '@/app/config/modelMetadata'; import { @@ -25,18 +25,45 @@ import { TooltipProvider, TooltipTrigger, } from '@/app/components/ui/tooltip'; +import { ModelPreviewPanel } from '@/app/components/gallery/ModelPreviewPanel'; export default function GalleryPage() { const { theme, t } = useLang(); const router = useRouter(); const [selectedCategory, setSelectedCategory] = useState('All'); const [searchQuery, setSearchQuery] = useState(''); + const [previewModel, setPreviewModel] = useState<{ + key: string; + name: string; + description: string; + category: string; + } | null>(null); const handleModelClick = (modelKey: string) => { // Navigate to home page with model selection router.push(`/?model=${modelKey}`); }; + const handlePreviewClick = ( + modelKey: string, + modelName: string, + modelDescription: string, + modelCategory: string, + e: React.MouseEvent, + ) => { + e.stopPropagation(); + setPreviewModel({ + key: modelKey, + name: modelName, + description: modelDescription, + category: modelCategory, + }); + }; + + const handleClosePreview = () => { + setPreviewModel(null); + }; + const filteredModels = modelMetadata.filter((model) => { // Filter by category const categoryMatch = @@ -163,8 +190,16 @@ export default function GalleryPage() { return (
{ + setPreviewModel({ + key: model.key, + name: model.name, + description: model.description, + category: model.category, + }); + }} className={clsx( - 'rounded-lg border border-border shadow-sm transition-all duration-200', + 'rounded-lg border border-border shadow-sm transition-all duration-200 cursor-pointer', cardBgClass, 'hover:shadow-lg', )} @@ -186,11 +221,45 @@ export default function GalleryPage() {

{t(model.description)}

-
+
+ + + + + +

{t('Preview')}

+
+
+ {/* Preview Panel */} + + {/* Footer */}
= 16.8.0 || 17.x.x || ^18.0.0-0", react@>=16, react@>=16.8.0: +"react@^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", react@^18, react@^18.2.0, react@^18.3.1, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0", react@>=16, react@>=16.8.0: version "18.3.1" resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz" integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== @@ -6723,17 +6806,17 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-callback-ref@^1.3.0: - version "1.3.2" - resolved "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz" - integrity sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA== +use-callback-ref@^1.3.0, use-callback-ref@^1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz" + integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg== dependencies: tslib "^2.0.0" -use-sidecar@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz" - integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== +use-sidecar@^1.1.2, use-sidecar@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz" + integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ== dependencies: detect-node-es "^1.1.0" tslib "^2.0.0" From 44f10f6f0359165860fe4b4aa7822478c335b9ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:41:59 +0000 Subject: [PATCH 3/5] Address code review feedback and improve code quality Co-authored-by: tjyyy3 <97741063+tjyyy3@users.noreply.github.com> --- app/components/gallery/ModelPreviewPanel.tsx | 13 ++++++++----- app/gallery/page.tsx | 16 ++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/app/components/gallery/ModelPreviewPanel.tsx b/app/components/gallery/ModelPreviewPanel.tsx index 4dca96e7..940e8aad 100644 --- a/app/components/gallery/ModelPreviewPanel.tsx +++ b/app/components/gallery/ModelPreviewPanel.tsx @@ -63,11 +63,14 @@ export const ModelPreviewPanel: React.FC = ({ const borderClass = clsx(theme === 'dark' ? 'border-slate-700' : 'border-slate-200'); return ( - { - if (!open) { - onClose(); - } - }}> + { + if (!open) { + onClose(); + } + }} + >
diff --git a/app/gallery/page.tsx b/app/gallery/page.tsx index 699bfacf..21f6aa5b 100644 --- a/app/gallery/page.tsx +++ b/app/gallery/page.tsx @@ -49,9 +49,9 @@ export default function GalleryPage() { modelName: string, modelDescription: string, modelCategory: string, - e: React.MouseEvent, + e?: React.MouseEvent, ) => { - e.stopPropagation(); + e?.stopPropagation(); setPreviewModel({ key: modelKey, name: modelName, @@ -191,12 +191,12 @@ export default function GalleryPage() {
{ - setPreviewModel({ - key: model.key, - name: model.name, - description: model.description, - category: model.category, - }); + handlePreviewClick( + model.key, + model.name, + model.description, + model.category, + ); }} className={clsx( 'rounded-lg border border-border shadow-sm transition-all duration-200 cursor-pointer', From 9950d82cdafbf177abfa8a2e63ce81aceb687684 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:08:48 +0000 Subject: [PATCH 4/5] Fix overflow issue, add Enforcement Result section, and keep yarn.lock unchanged Co-authored-by: tjyyy3 <97741063+tjyyy3@users.noreply.github.com> --- app/components/gallery/ModelPreviewPanel.tsx | 25 +++++++++++++++----- messages/en.json | 3 ++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/app/components/gallery/ModelPreviewPanel.tsx b/app/components/gallery/ModelPreviewPanel.tsx index 940e8aad..6fc3eb71 100644 --- a/app/components/gallery/ModelPreviewPanel.tsx +++ b/app/components/gallery/ModelPreviewPanel.tsx @@ -23,7 +23,6 @@ import { Sheet, SheetContent, SheetDescription, - SheetFooter, SheetHeader, SheetTitle, } from '@/app/components/ui/sheet'; @@ -71,7 +70,7 @@ export const ModelPreviewPanel: React.FC = ({ } }} > - +
{t(modelName)} @@ -90,7 +89,7 @@ export const ModelPreviewPanel: React.FC = ({ {modelData && ( -
+
{/* Model Configuration */}

@@ -146,6 +145,20 @@ export const ModelPreviewPanel: React.FC = ({

)} + {/* Enforcement Result Section */} + {modelData.policy && modelData.request && ( +
+

+ {t('Enforcement Result')} +

+
+

+ {t('Open in editor to see enforcement results for the example request')} +

+
+
+ )} + {/* Custom Configuration */} {modelData.customConfig && (
@@ -168,9 +181,9 @@ export const ModelPreviewPanel: React.FC = ({ )} {/* Footer with Open in Editor button */} - = ({ {t('Open in Editor')} - +
); diff --git a/messages/en.json b/messages/en.json index b9e9d40d..08ce632f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -137,5 +137,6 @@ "Model Configuration": "Model Configuration", "Example Policies": "Example Policies", "Example Request": "Example Request", - "Custom Configuration": "Custom Configuration" + "Custom Configuration": "Custom Configuration", + "Open in editor to see enforcement results for the example request": "Open in editor to see enforcement results for the example request" } From 36173ca6ba90f5a5b703fba0accc29d97abcda3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 07:16:59 +0000 Subject: [PATCH 5/5] Fix footer positioning and improve header spacing in preview panel Co-authored-by: tjyyy3 <97741063+tjyyy3@users.noreply.github.com> --- app/components/gallery/ModelPreviewPanel.tsx | 175 ++++++++++--------- 1 file changed, 90 insertions(+), 85 deletions(-) diff --git a/app/components/gallery/ModelPreviewPanel.tsx b/app/components/gallery/ModelPreviewPanel.tsx index 6fc3eb71..7d2478a2 100644 --- a/app/components/gallery/ModelPreviewPanel.tsx +++ b/app/components/gallery/ModelPreviewPanel.tsx @@ -70,48 +70,34 @@ export const ModelPreviewPanel: React.FC = ({ } }} > - - -
- {t(modelName)} + + +
+
+ {t(modelName)} + + {t(modelDescription)} + +
{t(modelCategory)}
- - {t(modelDescription)} -
- {modelData && ( -
- {/* Model Configuration */} -
-

- {t('Model Configuration')} -

-
-
-                  {modelData.model}
-                
-
-
- - {/* Example Policies */} - {modelData.policy && ( + {/* Scrollable content area */} +
+ {modelData && ( +
+ {/* Model Configuration */}

- {t('Example Policies')} + {t('Model Configuration')}

 = ({
                       textClass,
                     )}
                   >
-                    {modelData.policy}
+                    {modelData.model}
                   
- )} - {/* Example Request */} - {modelData.request && ( -
-

- {t('Example Request')} -

-
-
-                    {modelData.request}
-                  
-
-
- )} + {/* Example Policies */} + {modelData.policy && ( +
+

+ {t('Example Policies')} +

+
+
+                      {modelData.policy}
+                    
+
+
+ )} - {/* Enforcement Result Section */} - {modelData.policy && modelData.request && ( -
-

- {t('Enforcement Result')} -

-
-

- {t('Open in editor to see enforcement results for the example request')} -

-
-
- )} + {/* Example Request */} + {modelData.request && ( +
+

+ {t('Example Request')} +

+
+
+                      {modelData.request}
+                    
+
+
+ )} - {/* Custom Configuration */} - {modelData.customConfig && ( -
-

- {t('Custom Configuration')} -

-
-
-                    {modelData.customConfig}
-                  
-
-
- )} -
- )} + {/* Enforcement Result Section */} + {modelData.policy && modelData.request && ( +
+

+ {t('Enforcement Result')} +

+
+

+ {t('Open in editor to see enforcement results for the example request')} +

+
+
+ )} + + {/* Custom Configuration */} + {modelData.customConfig && ( +
+

+ {t('Custom Configuration')} +

+
+
+                      {modelData.customConfig}
+                    
+
+
+ )} +
+ )} +
- {/* Footer with Open in Editor button */} + {/* Fixed footer with Open in Editor button */}