From e513ad2f16aa5c84b2e69111e6b4489d64532852 Mon Sep 17 00:00:00 2001 From: balibabu Date: Wed, 18 Dec 2024 14:51:24 +0800 Subject: [PATCH] Feat: Add MultiSelect #3221 (#4090) ### What problem does this PR solve? Feat: Add MultiSelect #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/package-lock.json | 261 +++++++++++- web/package.json | 2 + web/src/components/ui/command.tsx | 153 +++++++ web/src/components/ui/dialog.tsx | 122 ++++++ web/src/components/ui/multi-select.tsx | 381 ++++++++++++++++++ .../dataset/setting/advanced-setting-form.tsx | 20 +- .../dataset/setting/basic-setting-form.tsx | 125 +++--- .../dataset/setting/chunk-method-card.tsx | 124 ++++++ web/src/pages/dataset/setting/index.less | 45 +++ web/src/pages/dataset/setting/index.tsx | 4 +- web/src/pages/dataset/setting/utils.ts | 19 + .../pages/profile-setting/sidebar/index.tsx | 9 +- 12 files changed, 1171 insertions(+), 94 deletions(-) create mode 100644 web/src/components/ui/command.tsx create mode 100644 web/src/components/ui/dialog.tsx create mode 100644 web/src/components/ui/multi-select.tsx create mode 100644 web/src/pages/dataset/setting/chunk-method-card.tsx create mode 100644 web/src/pages/dataset/setting/index.less create mode 100644 web/src/pages/dataset/setting/utils.ts diff --git a/web/package-lock.json b/web/package-lock.json index 55f450db3..c027b24ec 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -16,6 +16,7 @@ "@radix-ui/react-aspect-ratio": "^1.1.0", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.1", "@radix-ui/react-label": "^2.1.0", @@ -40,6 +41,7 @@ "class-variance-authority": "^0.7.0", "classnames": "^2.5.1", "clsx": "^2.1.1", + "cmdk": "^1.0.4", "dayjs": "^1.11.10", "dompurify": "^3.1.6", "eventsource-parser": "^1.1.2", @@ -4240,6 +4242,219 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz", + "integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==" + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz", + "integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz", + "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", + "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll": { + "version": "2.6.1", + "resolved": "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.6.1.tgz", + "integrity": "sha512-jWEvWQidZ/C/FnFlUIB1mDLpY3r7uEb22WZ3uVeKj520caKDiaBsNDEB9J1gHJgpiLo+eTdPl2MVi0JitFTiFg==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", @@ -10672,6 +10887,29 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/cmdk/-/cmdk-1.0.4.tgz", + "integrity": "sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg==", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.0", + "use-sync-external-store": "^1.2.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/cmdk/node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmmirror.com/co/-/co-4.6.0.tgz", @@ -25069,19 +25307,19 @@ } }, "node_modules/react-remove-scroll-bar": { - "version": "2.3.6", - "resolved": "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", - "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", "dependencies": { - "react-style-singleton": "^2.2.1", + "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "engines": { "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -25229,20 +25467,19 @@ } }, "node_modules/react-style-singleton": { - "version": "2.2.1", - "resolved": "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz", - "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", "dependencies": { "get-nonce": "^1.0.0", - "invariant": "^2.2.4", "tslib": "^2.0.0" }, "engines": { "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { diff --git a/web/package.json b/web/package.json index 6bcea8f02..6e82a12a4 100644 --- a/web/package.json +++ b/web/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-aspect-ratio": "^1.1.0", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.1", "@radix-ui/react-label": "^2.1.0", @@ -51,6 +52,7 @@ "class-variance-authority": "^0.7.0", "classnames": "^2.5.1", "clsx": "^2.1.1", + "cmdk": "^1.0.4", "dayjs": "^1.11.10", "dompurify": "^3.1.6", "eventsource-parser": "^1.1.2", diff --git a/web/src/components/ui/command.tsx b/web/src/components/ui/command.tsx new file mode 100644 index 000000000..f92a07a86 --- /dev/null +++ b/web/src/components/ui/command.tsx @@ -0,0 +1,153 @@ +'use client'; + +import { type DialogProps } from '@radix-ui/react-dialog'; +import { Command as CommandPrimitive } from 'cmdk'; +import { Search } from 'lucide-react'; +import * as React from 'react'; + +import { Dialog, DialogContent } from '@/components/ui/dialog'; +import { cn } from '@/lib/utils'; + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +const CommandDialog = ({ children, ...props }: DialogProps) => { + return ( + + + + {children} + + + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +CommandShortcut.displayName = 'CommandShortcut'; + +export { + Command, + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, + CommandShortcut, +}; diff --git a/web/src/components/ui/dialog.tsx b/web/src/components/ui/dialog.tsx new file mode 100644 index 000000000..36d18f8a1 --- /dev/null +++ b/web/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +'use client'; + +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = 'DialogHeader'; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = 'DialogFooter'; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/web/src/components/ui/multi-select.tsx b/web/src/components/ui/multi-select.tsx new file mode 100644 index 000000000..0c4fb9d68 --- /dev/null +++ b/web/src/components/ui/multi-select.tsx @@ -0,0 +1,381 @@ +// src/components/multi-select.tsx + +import { cva, type VariantProps } from 'class-variance-authority'; +import { + CheckIcon, + ChevronDown, + WandSparkles, + XCircle, + XIcon, +} from 'lucide-react'; +import * as React from 'react'; + +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from '@/components/ui/command'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { Separator } from '@/components/ui/separator'; +import { cn } from '@/lib/utils'; + +/** + * Variants for the multi-select component to handle different styles. + * Uses class-variance-authority (cva) to define different styles based on "variant" prop. + */ +const multiSelectVariants = cva( + 'm-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300', + { + variants: { + variant: { + default: + 'border-foreground/10 text-foreground bg-card hover:bg-card/80', + secondary: + 'border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80', + destructive: + 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', + inverted: 'inverted', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +/** + * Props for MultiSelect component + */ +interface MultiSelectProps + extends React.ButtonHTMLAttributes, + VariantProps { + /** + * An array of option objects to be displayed in the multi-select component. + * Each option object has a label, value, and an optional icon. + */ + options: { + /** The text to display for the option. */ + label: string; + /** The unique value associated with the option. */ + value: string; + /** Optional icon component to display alongside the option. */ + icon?: React.ComponentType<{ className?: string }>; + }[]; + + /** + * Callback function triggered when the selected values change. + * Receives an array of the new selected values. + */ + onValueChange: (value: string[]) => void; + + /** The default selected values when the component mounts. */ + defaultValue?: string[]; + + /** + * Placeholder text to be displayed when no values are selected. + * Optional, defaults to "Select options". + */ + placeholder?: string; + + /** + * Animation duration in seconds for the visual effects (e.g., bouncing badges). + * Optional, defaults to 0 (no animation). + */ + animation?: number; + + /** + * Maximum number of items to display. Extra selected items will be summarized. + * Optional, defaults to 3. + */ + maxCount?: number; + + /** + * The modality of the popover. When set to true, interaction with outside elements + * will be disabled and only popover content will be visible to screen readers. + * Optional, defaults to false. + */ + modalPopover?: boolean; + + /** + * If true, renders the multi-select component as a child of another component. + * Optional, defaults to false. + */ + asChild?: boolean; + + /** + * Additional class names to apply custom styles to the multi-select component. + * Optional, can be used to add custom styles. + */ + className?: string; +} + +export const MultiSelect = React.forwardRef< + HTMLButtonElement, + MultiSelectProps +>( + ( + { + options, + onValueChange, + variant, + defaultValue = [], + placeholder = 'Select options', + animation = 0, + maxCount = 3, + modalPopover = false, + asChild = false, + className, + ...props + }, + ref, + ) => { + const [selectedValues, setSelectedValues] = + React.useState(defaultValue); + const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); + const [isAnimating, setIsAnimating] = React.useState(false); + + const handleInputKeyDown = ( + event: React.KeyboardEvent, + ) => { + if (event.key === 'Enter') { + setIsPopoverOpen(true); + } else if (event.key === 'Backspace' && !event.currentTarget.value) { + const newSelectedValues = [...selectedValues]; + newSelectedValues.pop(); + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); + } + }; + + const toggleOption = (option: string) => { + const newSelectedValues = selectedValues.includes(option) + ? selectedValues.filter((value) => value !== option) + : [...selectedValues, option]; + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); + }; + + const handleClear = () => { + setSelectedValues([]); + onValueChange([]); + }; + + const handleTogglePopover = () => { + setIsPopoverOpen((prev) => !prev); + }; + + const clearExtraOptions = () => { + const newSelectedValues = selectedValues.slice(0, maxCount); + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); + }; + + const toggleAll = () => { + if (selectedValues.length === options.length) { + handleClear(); + } else { + const allValues = options.map((option) => option.value); + setSelectedValues(allValues); + onValueChange(allValues); + } + }; + + return ( + + + + + setIsPopoverOpen(false)} + > + + + + No results found. + + +
+ +
+ (Select All) +
+ {options.map((option) => { + const isSelected = selectedValues.includes(option.value); + return ( + toggleOption(option.value)} + className="cursor-pointer" + > +
+ +
+ {option.icon && ( + + )} + {option.label} +
+ ); + })} +
+ + +
+ {selectedValues.length > 0 && ( + <> + + Clear + + + + )} + setIsPopoverOpen(false)} + className="flex-1 justify-center cursor-pointer max-w-full" + > + Close + +
+
+
+
+
+ {animation > 0 && selectedValues.length > 0 && ( + setIsAnimating(!isAnimating)} + /> + )} +
+ ); + }, +); + +MultiSelect.displayName = 'MultiSelect'; diff --git a/web/src/pages/dataset/setting/advanced-setting-form.tsx b/web/src/pages/dataset/setting/advanced-setting-form.tsx index e373cb06b..312bf047f 100644 --- a/web/src/pages/dataset/setting/advanced-setting-form.tsx +++ b/web/src/pages/dataset/setting/advanced-setting-form.tsx @@ -23,9 +23,10 @@ import { } from '@/components/ui/select'; import { FormSlider } from '@/components/ui/slider'; import { Textarea } from '@/components/ui/textarea'; +import ChunkMethodCard from './chunk-method-card'; const formSchema = z.object({ - username: z.number().min(2, { + parser_id: z.string().min(1, { message: 'Username must be at least 2 characters.', }), a: z.number().min(2, { @@ -46,7 +47,7 @@ export default function AdvancedSettingForm() { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - username: 0, + parser_id: '', }, }); @@ -59,9 +60,9 @@ export default function AdvancedSettingForm() {
( - + Username @@ -73,11 +74,12 @@ export default function AdvancedSettingForm() { )} /> + ( - + Username @@ -93,7 +95,7 @@ export default function AdvancedSettingForm() { control={form.control} name="b" render={({ field }) => ( - + Username + /> - - This is your public display name. - )} /> - ); diff --git a/web/src/pages/dataset/setting/chunk-method-card.tsx b/web/src/pages/dataset/setting/chunk-method-card.tsx new file mode 100644 index 000000000..559933213 --- /dev/null +++ b/web/src/pages/dataset/setting/chunk-method-card.tsx @@ -0,0 +1,124 @@ +import SvgIcon from '@/components/svg-icon'; +import { Card } from '@/components/ui/card'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { useTranslate } from '@/hooks/common-hooks'; +import { useSelectParserList } from '@/hooks/user-setting-hooks'; +import { Col, Divider, Empty, Row, Typography } from 'antd'; +import DOMPurify from 'dompurify'; +import camelCase from 'lodash/camelCase'; +import { useMemo } from 'react'; +import { useFormContext } from 'react-hook-form'; +import styles from './index.less'; +import { ImageMap } from './utils'; + +const { Title, Text } = Typography; + +const CategoryPanel = ({ chunkMethod }: { chunkMethod: string }) => { + const parserList = useSelectParserList(); + const { t } = useTranslate('knowledgeConfiguration'); + + const item = useMemo(() => { + const item = parserList.find((x) => x.value === chunkMethod); + if (item) { + return { + title: item.label, + description: t(camelCase(item.value)), + }; + } + return { title: '', description: '' }; + }, [parserList, chunkMethod, t]); + + const imageList = useMemo(() => { + if (chunkMethod in ImageMap) { + return ImageMap[chunkMethod as keyof typeof ImageMap]; + } + return []; + }, [chunkMethod]); + + return ( +
+ {imageList.length > 0 ? ( + <> + + {`"${item.title}" ${t('methodTitle')}`} + +

+ {`"${item.title}" ${t('methodExamples')}`} + {t('methodExamplesDescription')} + + {imageList.map((x) => ( + + + + ))} + + + {item.title} {t('dialogueExamplesTitle')} + + + + ) : ( + +

{t('methodEmpty')}

+ +
+ )} +
+ ); +}; + +export default function ChunkMethodCard() { + const { t } = useTranslate('knowledgeConfiguration'); + const form = useFormContext(); + + return ( + +
+ ( + + {t('chunkMethod')} + + + + )} + /> +
+ +
+ ); +} diff --git a/web/src/pages/dataset/setting/index.less b/web/src/pages/dataset/setting/index.less new file mode 100644 index 000000000..4889e3776 --- /dev/null +++ b/web/src/pages/dataset/setting/index.less @@ -0,0 +1,45 @@ +.tags { + margin-bottom: 24px; +} + +.preset { + display: flex; + height: 80px; + background-color: rgba(0, 0, 0, 0.1); + border-radius: 5px; + padding: 5px; + margin-bottom: 24px; + + .left { + flex: 1; + } + + .right { + width: 100px; + border-left: 1px solid rgba(0, 0, 0, 0.4); + margin: 10px 0px; + padding: 5px; + } +} + +.configurationWrapper { + padding: 0 52px; + .buttonWrapper { + text-align: right; + } + .variableSlider { + width: 100%; + } +} + +.categoryPanelWrapper { + .topTitle { + margin-top: 0; + } + .imageRow { + margin-top: 16px; + } + .image { + width: 100%; + } +} diff --git a/web/src/pages/dataset/setting/index.tsx b/web/src/pages/dataset/setting/index.tsx index f4ddb998b..15f82ab93 100644 --- a/web/src/pages/dataset/setting/index.tsx +++ b/web/src/pages/dataset/setting/index.tsx @@ -14,9 +14,7 @@ export default function DatasetSettings() {
Advanced settings
-
- -
+
); diff --git a/web/src/pages/dataset/setting/utils.ts b/web/src/pages/dataset/setting/utils.ts new file mode 100644 index 000000000..ceb9cc97e --- /dev/null +++ b/web/src/pages/dataset/setting/utils.ts @@ -0,0 +1,19 @@ +const getImageName = (prefix: string, length: number) => + new Array(length) + .fill(0) + .map((x, idx) => `chunk-method/${prefix}-0${idx + 1}`); + +export const ImageMap = { + book: getImageName('book', 4), + laws: getImageName('law', 2), + manual: getImageName('manual', 4), + picture: getImageName('media', 2), + naive: getImageName('naive', 2), + paper: getImageName('paper', 2), + presentation: getImageName('presentation', 2), + qa: getImageName('qa', 2), + resume: getImageName('resume', 2), + table: getImageName('table', 2), + one: getImageName('one', 2), + knowledge_graph: getImageName('knowledge-graph', 2), +}; diff --git a/web/src/pages/profile-setting/sidebar/index.tsx b/web/src/pages/profile-setting/sidebar/index.tsx index a697d24cd..b2c82030d 100644 --- a/web/src/pages/profile-setting/sidebar/index.tsx +++ b/web/src/pages/profile-setting/sidebar/index.tsx @@ -1,4 +1,4 @@ -import { useTheme } from '@/components/theme-provider'; +import { useIsDarkTheme, useTheme } from '@/components/theme-provider'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; @@ -52,6 +52,7 @@ export function SideBar() { const pathName = useSecondPathName(); const { handleMenuClick } = useHandleMenuClick(); const { setTheme } = useTheme(); + const isDarkTheme = useIsDarkTheme(); const handleThemeChange = useCallback( (checked: boolean) => { @@ -89,7 +90,11 @@ export function SideBar() {
- +