| @@ -5,7 +5,7 @@ import { | |||
| tagKeys, | |||
| } from './constants' | |||
| type Tag = { | |||
| export type Tag = { | |||
| name: string | |||
| label: string | |||
| } | |||
| @@ -4,6 +4,7 @@ import TagsFilter from './tags-filter' | |||
| import ActionButton from '@/app/components/base/action-button' | |||
| import cn from '@/utils/classnames' | |||
| import { RiAddLine } from '@remixicon/react' | |||
| import Divider from '@/app/components/base/divider' | |||
| type SearchBoxProps = { | |||
| search: string | |||
| @@ -12,10 +13,10 @@ type SearchBoxProps = { | |||
| inputClassName?: string | |||
| tags: string[] | |||
| onTagsChange: (tags: string[]) => void | |||
| size?: 'small' | 'large' | |||
| placeholder?: string | |||
| locale?: string | |||
| supportAddCustomTool?: boolean | |||
| usedInMarketplace?: boolean | |||
| onShowAddCustomCollectionModal?: () => void | |||
| onAddedCustomTool?: () => void | |||
| } | |||
| @@ -26,9 +27,9 @@ const SearchBox = ({ | |||
| inputClassName, | |||
| tags, | |||
| onTagsChange, | |||
| size = 'small', | |||
| placeholder = '', | |||
| locale, | |||
| usedInMarketplace = false, | |||
| supportAddCustomTool, | |||
| onShowAddCustomCollectionModal, | |||
| }: SearchBoxProps) => { | |||
| @@ -38,42 +39,82 @@ const SearchBox = ({ | |||
| > | |||
| <div className={ | |||
| cn('flex items-center', | |||
| size === 'large' && 'rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-1.5 shadow-md', | |||
| size === 'small' && 'rounded-lg bg-components-input-bg-normal p-0.5', | |||
| usedInMarketplace && 'rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-1.5 shadow-md', | |||
| !usedInMarketplace && 'rounded-lg bg-components-input-bg-normal p-0.5', | |||
| inputClassName, | |||
| ) | |||
| }> | |||
| <div className='relative flex grow items-center p-1 pl-2'> | |||
| <div className='mr-2 flex w-full items-center pr-7'> | |||
| <RiSearchLine className='mr-1.5 size-4 text-text-placeholder' /> | |||
| <input | |||
| className={cn( | |||
| 'body-md-medium block grow appearance-none bg-transparent text-text-secondary outline-none', | |||
| )} | |||
| value={search} | |||
| onChange={(e) => { | |||
| onSearchChange(e.target.value) | |||
| }} | |||
| placeholder={placeholder} | |||
| /> | |||
| { | |||
| search && ( | |||
| <div className='absolute right-2 top-1/2 -translate-y-1/2'> | |||
| <ActionButton onClick={() => onSearchChange('')}> | |||
| <RiCloseLine className='h-4 w-4' /> | |||
| </ActionButton> | |||
| </div> | |||
| ) | |||
| } | |||
| </div> | |||
| </div> | |||
| <div className='mx-1 h-3.5 w-[1px] bg-divider-regular'></div> | |||
| <TagsFilter | |||
| tags={tags} | |||
| onTagsChange={onTagsChange} | |||
| size={size} | |||
| locale={locale} | |||
| /> | |||
| { | |||
| usedInMarketplace && ( | |||
| <> | |||
| <TagsFilter | |||
| tags={tags} | |||
| onTagsChange={onTagsChange} | |||
| usedInMarketplace | |||
| locale={locale} | |||
| /> | |||
| <Divider type='vertical' className='mx-1 h-3.5' /> | |||
| <div className='flex grow items-center gap-x-2 p-1'> | |||
| <input | |||
| className={cn( | |||
| 'body-md-medium inline-block grow appearance-none bg-transparent text-text-secondary outline-none', | |||
| )} | |||
| value={search} | |||
| onChange={(e) => { | |||
| onSearchChange(e.target.value) | |||
| }} | |||
| placeholder={placeholder} | |||
| /> | |||
| { | |||
| search && ( | |||
| <ActionButton | |||
| onClick={() => onSearchChange('')} | |||
| className='shrink-0' | |||
| > | |||
| <RiCloseLine className='size-4' /> | |||
| </ActionButton> | |||
| ) | |||
| } | |||
| </div> | |||
| </> | |||
| ) | |||
| } | |||
| { | |||
| !usedInMarketplace && ( | |||
| <> | |||
| <div className='flex grow items-center p-2'> | |||
| <RiSearchLine className='size-4 text-components-input-text-placeholder' /> | |||
| <input | |||
| className={cn( | |||
| 'body-md-medium ml-1.5 mr-1 inline-block grow appearance-none bg-transparent text-text-secondary outline-none', | |||
| search && 'mr-2', | |||
| )} | |||
| value={search} | |||
| onChange={(e) => { | |||
| onSearchChange(e.target.value) | |||
| }} | |||
| placeholder={placeholder} | |||
| /> | |||
| { | |||
| search && ( | |||
| <ActionButton | |||
| onClick={() => onSearchChange('')} | |||
| className='shrink-0' | |||
| > | |||
| <RiCloseLine className='size-4' /> | |||
| </ActionButton> | |||
| ) | |||
| } | |||
| </div> | |||
| <Divider type='vertical' className='mx-0 mr-0.5 h-3.5' /> | |||
| <TagsFilter | |||
| tags={tags} | |||
| onTagsChange={onTagsChange} | |||
| locale={locale} | |||
| /> | |||
| </> | |||
| ) | |||
| } | |||
| </div> | |||
| {supportAddCustomTool && ( | |||
| <div className='flex shrink-0 items-center'> | |||
| @@ -36,9 +36,9 @@ const SearchBoxWrapper = ({ | |||
| onSearchChange={handleSearchPluginTextChange} | |||
| tags={filterPluginTags} | |||
| onTagsChange={handleFilterPluginTagsChange} | |||
| size='large' | |||
| locale={locale} | |||
| placeholder={t('plugin.searchPlugins')} | |||
| usedInMarketplace | |||
| /> | |||
| ) | |||
| } | |||
| @@ -1,40 +1,29 @@ | |||
| 'use client' | |||
| import { useState } from 'react' | |||
| import type { ReactElement } from 'react' | |||
| import { | |||
| RiCloseCircleFill, | |||
| RiFilter3Line, | |||
| RiPriceTag3Line, | |||
| } from '@remixicon/react' | |||
| import { | |||
| PortalToFollowElem, | |||
| PortalToFollowElemContent, | |||
| PortalToFollowElemTrigger, | |||
| } from '@/app/components/base/portal-to-follow-elem' | |||
| import Checkbox from '@/app/components/base/checkbox' | |||
| import cn from '@/utils/classnames' | |||
| import Input from '@/app/components/base/input' | |||
| import { useTags } from '@/app/components/plugins/hooks' | |||
| import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks' | |||
| import MarketplaceTrigger from './trigger/marketplace' | |||
| import ToolSelectorTrigger from './trigger/tool-selector' | |||
| type TagsFilterProps = { | |||
| tags: string[] | |||
| onTagsChange: (tags: string[]) => void | |||
| size: 'small' | 'large' | |||
| usedInMarketplace?: boolean | |||
| locale?: string | |||
| emptyTrigger?: ReactElement | |||
| className?: string | |||
| triggerClassName?: string | |||
| } | |||
| const TagsFilter = ({ | |||
| tags, | |||
| onTagsChange, | |||
| size, | |||
| usedInMarketplace = false, | |||
| locale, | |||
| emptyTrigger, | |||
| className, | |||
| triggerClassName, | |||
| }: TagsFilterProps) => { | |||
| const { t } = useMixedTranslation(locale) | |||
| const [open, setOpen] = useState(false) | |||
| @@ -63,55 +52,29 @@ const TagsFilter = ({ | |||
| className='shrink-0' | |||
| onClick={() => setOpen(v => !v)} | |||
| > | |||
| <div className={cn( | |||
| 'ml-0.5 mr-1.5 flex select-none items-center text-text-tertiary', | |||
| size === 'large' && 'h-8 py-1', | |||
| size === 'small' && 'h-7 py-0.5 ', | |||
| className, | |||
| )}> | |||
| { | |||
| !emptyTrigger && ( | |||
| <div className='p-0.5'> | |||
| <RiFilter3Line className='h-4 w-4' /> | |||
| </div> | |||
| ) | |||
| } | |||
| <div className={cn( | |||
| 'system-sm-medium flex items-center p-1', | |||
| size === 'large' && 'p-1', | |||
| size === 'small' && 'px-0.5 py-1', | |||
| triggerClassName, | |||
| )}> | |||
| { | |||
| !selectedTagsLength && (emptyTrigger || t('pluginTags.allTags')) | |||
| } | |||
| { | |||
| !!selectedTagsLength && tags.map(tag => tagsMap[tag].label).slice(0, 2).join(',') | |||
| } | |||
| { | |||
| selectedTagsLength > 2 && ( | |||
| <div className='system-xs-medium ml-1 text-text-tertiary'> | |||
| +{selectedTagsLength - 2} | |||
| </div> | |||
| ) | |||
| } | |||
| </div> | |||
| { | |||
| !!selectedTagsLength && ( | |||
| <RiCloseCircleFill | |||
| className='h-4 w-4 cursor-pointer text-text-quaternary' | |||
| onClick={() => onTagsChange([])} | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| !selectedTagsLength && !emptyTrigger && ( | |||
| <div className='cursor-pointer rounded-md p-0.5 hover:bg-state-base-hover'> | |||
| <RiPriceTag3Line className='h-4 w-4 text-text-tertiary' /> | |||
| </div> | |||
| ) | |||
| } | |||
| </div> | |||
| { | |||
| usedInMarketplace && ( | |||
| <MarketplaceTrigger | |||
| selectedTagsLength={selectedTagsLength} | |||
| open={open} | |||
| tags={tags} | |||
| tagsMap={tagsMap} | |||
| locale={locale} | |||
| onTagsChange={onTagsChange} | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| !usedInMarketplace && ( | |||
| <ToolSelectorTrigger | |||
| selectedTagsLength={selectedTagsLength} | |||
| open={open} | |||
| tags={tags} | |||
| tagsMap={tagsMap} | |||
| onTagsChange={onTagsChange} | |||
| /> | |||
| ) | |||
| } | |||
| </PortalToFollowElemTrigger> | |||
| <PortalToFollowElemContent className='z-[1000]'> | |||
| <div className='w-[240px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm'> | |||
| @@ -0,0 +1,75 @@ | |||
| import React from 'react' | |||
| import { RiArrowDownSLine, RiCloseCircleFill, RiFilter3Line } from '@remixicon/react' | |||
| import type { Tag } from '../../../hooks' | |||
| import cn from '@/utils/classnames' | |||
| import { useMixedTranslation } from '../../hooks' | |||
| type MarketplaceTriggerProps = { | |||
| selectedTagsLength: number | |||
| open: boolean | |||
| tags: string[] | |||
| tagsMap: Record<string, Tag> | |||
| locale?: string | |||
| onTagsChange: (tags: string[]) => void | |||
| } | |||
| const MarketplaceTrigger = ({ | |||
| selectedTagsLength, | |||
| open, | |||
| tags, | |||
| tagsMap, | |||
| locale, | |||
| onTagsChange, | |||
| }: MarketplaceTriggerProps) => { | |||
| const { t } = useMixedTranslation(locale) | |||
| return ( | |||
| <div | |||
| className={cn( | |||
| 'flex h-8 cursor-pointer select-none items-center rounded-lg px-2 py-1 text-text-tertiary', | |||
| !!selectedTagsLength && 'border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs shadow-shadow-shadow-3', | |||
| open && !selectedTagsLength && 'bg-state-base-hover', | |||
| )} | |||
| > | |||
| <div className='p-0.5'> | |||
| <RiFilter3Line className={cn('size-4', !!selectedTagsLength && 'text-text-secondary')} /> | |||
| </div> | |||
| <div className='system-sm-medium flex items-center gap-x-1 p-1'> | |||
| { | |||
| !selectedTagsLength && <span>{t('pluginTags.allTags')}</span> | |||
| } | |||
| { | |||
| !!selectedTagsLength && ( | |||
| <span className='text-text-secondary'> | |||
| {tags.map(tag => tagsMap[tag].label).slice(0, 2).join(',')} | |||
| </span> | |||
| ) | |||
| } | |||
| { | |||
| selectedTagsLength > 2 && ( | |||
| <div className='system-xs-medium text-text-tertiary'> | |||
| +{selectedTagsLength - 2} | |||
| </div> | |||
| ) | |||
| } | |||
| </div> | |||
| { | |||
| !!selectedTagsLength && ( | |||
| <RiCloseCircleFill | |||
| className='size-4 text-text-quaternary' | |||
| onClick={() => onTagsChange([])} | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| !selectedTagsLength && ( | |||
| <div className='p-0.5'> | |||
| <RiArrowDownSLine className='size-4 text-text-tertiary' /> | |||
| </div> | |||
| ) | |||
| } | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(MarketplaceTrigger) | |||
| @@ -0,0 +1,60 @@ | |||
| import React from 'react' | |||
| import type { Tag } from '../../../hooks' | |||
| import cn from '@/utils/classnames' | |||
| import { RiCloseCircleFill, RiPriceTag3Line } from '@remixicon/react' | |||
| type ToolSelectorTriggerProps = { | |||
| selectedTagsLength: number | |||
| open: boolean | |||
| tags: string[] | |||
| tagsMap: Record<string, Tag> | |||
| onTagsChange: (tags: string[]) => void | |||
| } | |||
| const ToolSelectorTrigger = ({ | |||
| selectedTagsLength, | |||
| open, | |||
| tags, | |||
| tagsMap, | |||
| onTagsChange, | |||
| }: ToolSelectorTriggerProps) => { | |||
| return ( | |||
| <div className={cn( | |||
| 'flex h-7 cursor-pointer select-none items-center rounded-md p-0.5 text-text-tertiary', | |||
| !selectedTagsLength && 'py-1 pl-1.5 pr-2', | |||
| !!selectedTagsLength && 'border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg py-0.5 pl-1 pr-1.5 shadow-xs shadow-shadow-shadow-3', | |||
| open && !selectedTagsLength && 'bg-state-base-hover', | |||
| )} | |||
| > | |||
| <div className='p-0.5'> | |||
| <RiPriceTag3Line className={cn('size-4', !!selectedTagsLength && 'text-text-secondary')} /> | |||
| </div> | |||
| { | |||
| !!selectedTagsLength && ( | |||
| <div className='system-sm-medium flex items-center gap-x-0.5 px-0.5 py-1'> | |||
| <span className='text-text-secondary'> | |||
| {tags.map(tag => tagsMap[tag].label).slice(0, 2).join(',')} | |||
| </span> | |||
| { | |||
| selectedTagsLength > 2 && ( | |||
| <div className='system-xs-medium text-text-tertiary'> | |||
| +{selectedTagsLength - 2} | |||
| </div> | |||
| ) | |||
| } | |||
| </div> | |||
| ) | |||
| } | |||
| { | |||
| !!selectedTagsLength && ( | |||
| <RiCloseCircleFill | |||
| className='size-4 text-text-quaternary' | |||
| onClick={() => onTagsChange([])} | |||
| /> | |||
| ) | |||
| } | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(ToolSelectorTrigger) | |||
| @@ -131,7 +131,6 @@ const ToolPicker: FC<Props> = ({ | |||
| onSearchChange={setQuery} | |||
| tags={tags} | |||
| onTagsChange={setTags} | |||
| size='small' | |||
| placeholder={t('plugin.searchTools')!} | |||
| inputClassName='w-full' | |||
| /> | |||
| @@ -184,7 +184,6 @@ const NodeSelector: FC<NodeSelectorProps> = ({ | |||
| onSearchChange={setSearchText} | |||
| tags={tags} | |||
| onTagsChange={setTags} | |||
| size='small' | |||
| placeholder={t('plugin.searchTools')!} | |||
| inputClassName='grow' | |||
| /> | |||
| @@ -13,7 +13,7 @@ import type { | |||
| } from '@floating-ui/react' | |||
| import AllTools from '@/app/components/workflow/block-selector/all-tools' | |||
| import type { ToolDefaultValue, ToolValue } from './types' | |||
| import type { BlockEnum } from '@/app/components/workflow/types' | |||
| import type { BlockEnum, OnSelectBlock } from '@/app/components/workflow/types' | |||
| import SearchBox from '@/app/components/plugins/marketplace/search-box' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useBoolean } from 'ahooks' | |||
| @@ -158,13 +158,11 @@ const ToolPicker: FC<Props> = ({ | |||
| onSearchChange={setSearchText} | |||
| tags={tags} | |||
| onTagsChange={setTags} | |||
| size='small' | |||
| placeholder={t('plugin.searchTools')!} | |||
| supportAddCustomTool={supportAddCustomTool} | |||
| onAddedCustomTool={handleAddedCustomTool} | |||
| onShowAddCustomCollectionModal={showEditCustomCollectionModal} | |||
| inputClassName='grow' | |||
| /> | |||
| </div> | |||
| <AllTools | |||
| @@ -172,7 +170,7 @@ const ToolPicker: FC<Props> = ({ | |||
| toolContentClassName='max-w-[100%]' | |||
| tags={tags} | |||
| searchText={searchText} | |||
| onSelect={handleSelect} | |||
| onSelect={handleSelect as OnSelectBlock} | |||
| onSelectMultiple={handleSelectMultiple} | |||
| buildInTools={builtinToolList || []} | |||
| customTools={customToolList || []} | |||
| @@ -1,36 +0,0 @@ | |||
| import { memo } from 'react' | |||
| import { RiPriceTag3Line } from '@remixicon/react' | |||
| import TagsFilter from '@/app/components/plugins/marketplace/search-box/tags-filter' | |||
| import cn from '@/utils/classnames' | |||
| type ToolSearchInputTagProps = { | |||
| tags: string[] | |||
| onTagsChange: (tags: string[]) => void | |||
| } | |||
| const ToolSearchInputTag = ({ | |||
| tags, | |||
| onTagsChange, | |||
| }: ToolSearchInputTagProps) => { | |||
| return ( | |||
| <TagsFilter | |||
| tags={tags} | |||
| onTagsChange={onTagsChange} | |||
| size='large' | |||
| className={cn( | |||
| 'p-0', | |||
| tags.length && 'px-0.5', | |||
| )} | |||
| triggerClassName={cn( | |||
| 'p-0', | |||
| tags.length && 'px-0.5', | |||
| )} | |||
| emptyTrigger={ | |||
| <div className='flex h-7 w-[34px] items-center justify-center'> | |||
| <RiPriceTag3Line className='h-4 w-4 text-text-tertiary' /> | |||
| </div> | |||
| } | |||
| /> | |||
| ) | |||
| } | |||
| export default memo(ToolSearchInputTag) | |||