feat: can add tree view

This commit is contained in:
Joel 2024-10-31 11:37:47 +08:00
parent 06729f6d9d
commit 074e660a67
13 changed files with 343 additions and 180 deletions

View File

@ -22,7 +22,8 @@ const ToolsPicker = () => {
setCustomTools(customTools)
setWorkflowTools(workflowTools)
})()
})
}, [])
return (
<div className="relative mt-5 mx-auto w-[320px] bg-white">
<AllTools

View File

@ -1,4 +1,5 @@
import type { TypeWithI18N } from '../header/account-setting/model-provider-page/declarations'
export enum LOC {
tools = 'tools',
app = 'app',
@ -16,10 +17,10 @@ export enum AuthHeaderPrefix {
}
export type Credential = {
'auth_type': AuthType
'api_key_header'?: string
'api_key_value'?: string
'api_key_header_prefix'?: AuthHeaderPrefix
auth_type: AuthType
api_key_header?: string
api_key_value?: string
api_key_header_prefix?: AuthHeaderPrefix
}
export enum CollectionType {
@ -66,6 +67,7 @@ export type ToolParameter = {
max?: number
}
// Action
export type Tool = {
name: string
author: string

View File

@ -33,7 +33,7 @@ const AllTools = ({
const language = useGetLanguage()
const tabs = useToolTabs()
const [activeTab, setActiveTab] = useState(ToolTypeEnum.All)
const [activeView, setActiveView] = useState<ViewType>(ViewType.list)
const [activeView, setActiveView] = useState<ViewType>(ViewType.flat)
const tools = useMemo(() => {
let mergedTools: ToolWithProvider[] = []

View File

@ -1,8 +1,25 @@
import { pinyin } from 'pinyin-pro'
import type { FC, RefObject } from 'react'
import type { ToolWithProvider } from '../types'
import { CollectionType } from '../../tools/types'
export const groupItems = (items: Array<any>, getFirstChar: (item: string) => string) => {
const groups = items.reduce((acc, item) => {
/*
{
A: {
'google': [ // plugin organize name
...tools
],
'custom': [ // custom tools
...tools
],
'workflow': [ // workflow as tools
...tools
]
}
}
*/
export const groupItems = (items: ToolWithProvider[], getFirstChar: (item: ToolWithProvider) => string) => {
const groups = items.reduce((acc: Record<string, Record<string, ToolWithProvider[]>>, item) => {
const firstChar = getFirstChar(item)
if (!firstChar || firstChar.length === 0)
return acc
@ -19,9 +36,21 @@ export const groupItems = (items: Array<any>, getFirstChar: (item: string) => st
letter = '#'
if (!acc[letter])
acc[letter] = []
acc[letter] = {}
let groupName: string = ''
if (item.type === CollectionType.builtIn)
groupName = item.author
else if (item.type === CollectionType.custom)
groupName = 'custom'
else
groupName = 'workflow'
if (!acc[letter][groupName])
acc[letter][groupName] = []
acc[letter][groupName].push(item)
acc[letter].push(item)
return acc
}, {})

View File

@ -54,7 +54,7 @@ const List = ({
window.open(urlWithSearchText, '_blank')
}
if (!hasSearchText) {
if (hasSearchText) {
return (
<Link
className='sticky bottom-0 z-10 flex h-8 px-4 py-1 system-sm-medium items-center rounded-b-xl border-t border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg text-text-accent-light-mode-only cursor-pointer'

View File

@ -1,117 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import BlockIcon from '../block-icon'
import type { ToolWithProvider } from '../types'
import { BlockEnum } from '../types'
import type { ToolDefaultValue } from './types'
import Tooltip from '@/app/components/base/tooltip'
import type { Tool } from '@/app/components/tools/types'
import { useGetLanguage } from '@/context/i18n'
import cn from '@/utils/classnames'
type Props = {
className?: string
isToolPlugin: boolean // Tool plugin should choose action
provider: ToolWithProvider
payload: Tool
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
}
const ToolItem: FC<Props> = ({
className,
isToolPlugin,
provider,
payload,
onSelect,
}) => {
const language = useGetLanguage()
const [isFold, {
toggle: toggleFold,
}] = useBoolean(false)
const FoldIcon = isFold ? RiArrowDownSLine : RiArrowRightSLine
const actions = [
'DuckDuckGo AI Search',
'DuckDuckGo Connect',
]
return (
<Tooltip
key={payload.name}
position='right'
popupClassName='!p-0 !px-3 !py-2.5 !w-[200px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg'
popupContent={(
<div>
<BlockIcon
size='md'
className='mb-2'
type={BlockEnum.Tool}
toolIcon={provider.icon}
/>
<div className='mb-1 text-sm leading-5 text-gray-900'>{payload.label[language]}</div>
<div className='text-xs text-gray-700 leading-[18px]'>{payload.description[language]}</div>
</div>
)}
>
<div className={cn(className)}>
<div
className='flex items-center justify-between pl-3 pr-1 w-full rounded-lg hover:bg-gray-50 cursor-pointer'
onClick={() => {
if (isToolPlugin) {
toggleFold()
return
}
onSelect(BlockEnum.Tool, {
provider_id: provider.id,
provider_type: provider.type,
provider_name: provider.name,
tool_name: payload.name,
tool_label: payload.label[language],
title: payload.label[language],
})
}}
>
<div className='flex grow items-center h-8'>
<BlockIcon
className='shrink-0'
type={BlockEnum.Tool}
toolIcon={provider.icon}
/>
<div className='ml-2 text-sm text-gray-900 flex-1 w-0 grow truncate'>{payload.label[language]}</div>
</div>
{isToolPlugin && (
<FoldIcon className={cn('w-4 h-4 text-text-quaternary shrink-0', isFold && 'text-text-tertiary')} />
)}
</div>
{(!isFold && isToolPlugin) && (
<div>
{actions.map(action => (
<div
key={action}
className='rounded-lg pl-[21px] hover:bg-state-base-hover cursor-pointer'
onClick={() => {
onSelect(BlockEnum.Tool, {
provider_id: provider.id,
provider_type: provider.type,
provider_name: provider.name,
tool_name: payload.name,
tool_label: payload.label[language],
title: payload.label[language],
})
}}
>
<div className='h-8 leading-8 border-l-2 border-divider-subtle pl-4 truncate text-text-secondary system-sm-medium'>{action}</div>
</div>
))}
</div>
)}
</div>
</Tooltip>
)
}
export default React.memo(ToolItem)

View File

@ -0,0 +1,64 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { ToolWithProvider } from '../../types'
import { BlockEnum } from '../../types'
import type { ToolDefaultValue } from '../types'
import Tooltip from '@/app/components/base/tooltip'
import type { Tool } from '@/app/components/tools/types'
import { useGetLanguage } from '@/context/i18n'
import BlockIcon from '../../block-icon'
type Props = {
className?: string
provider: ToolWithProvider
payload: Tool
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
}
const ToolItem: FC<Props> = ({
className,
provider,
payload,
onSelect,
}) => {
const language = useGetLanguage()
return (
<Tooltip
key={payload.name}
position='right'
popupClassName='!p-0 !px-3 !py-2.5 !w-[200px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg'
popupContent={(
<div>
<BlockIcon
size='md'
className='mb-2'
type={BlockEnum.Tool}
toolIcon={provider.icon}
/>
<div className='mb-1 text-sm leading-5 text-gray-900'>{payload.label[language]}</div>
<div className='text-xs text-gray-700 leading-[18px]'>{payload.description[language]}</div>
</div>
)}
>
<div
key={payload.name}
className='rounded-lg pl-[21px] hover:bg-state-base-hover cursor-pointer'
onClick={() => {
onSelect(BlockEnum.Tool, {
provider_id: provider.id,
provider_type: provider.type,
provider_name: provider.name,
tool_name: payload.name,
tool_label: payload.label[language],
title: payload.label[language],
})
}}
>
<div className='h-8 leading-8 border-l-2 border-divider-subtle pl-4 truncate text-text-secondary system-sm-medium'>{payload.name}</div>
</div>
</Tooltip >
)
}
export default React.memo(ToolItem)

View File

@ -0,0 +1,16 @@
'use client'
import type { FC } from 'react'
import React from 'react'
type Props = {
}
const ToolViewFlatView: FC<Props> = () => {
return (
<div>
list...
</div>
)
}
export default React.memo(ToolViewFlatView)

View File

@ -0,0 +1,41 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { ToolWithProvider } from '../../../types'
import Tool from '../tool'
import type { BlockEnum } from '../../../types'
import { ViewType } from '../../view-type-select'
import type { ToolDefaultValue } from '../../types'
type Props = {
groupName: string
toolList: ToolWithProvider[]
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
}
const Item: FC<Props> = ({
groupName,
toolList,
onSelect,
}) => {
return (
<div>
<div className='flex items-start px-3 h-[22px] text-xs font-medium text-gray-500'>
{groupName}
</div>
<div>
{toolList.map((tool: ToolWithProvider) => (
<Tool
key={tool.id}
payload={tool}
viewType={ViewType.tree}
isShowLetterIndex
onSelect={onSelect}
/>
))}
</div>
</div>
)
}
export default React.memo(Item)

View File

@ -0,0 +1,33 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { ToolWithProvider } from '../../../types'
import type { BlockEnum } from '../../../types'
import type { ToolDefaultValue } from '../../types'
import Item from './item'
type Props = {
payload: Record<string, ToolWithProvider[]>
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
}
const OrgTools: FC<Props> = ({
payload,
onSelect,
}) => {
if (!payload) return null
return (
<div>
{Object.keys(payload).map(groupName => (
<Item
key={groupName}
groupName={groupName}
toolList={payload[groupName]}
onSelect={onSelect}
/>
))}
</div>
)
}
export default React.memo(OrgTools)

View File

@ -0,0 +1,95 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import { useGetLanguage } from '@/context/i18n'
import { CollectionType } from '../../../tools/types'
import type { ToolWithProvider } from '../../types'
import { BlockEnum } from '../../types'
import type { ToolDefaultValue } from '../types'
import { ViewType } from '../view-type-select'
import ActonItem from './action-item'
import BlockIcon from '../../block-icon'
import { useBoolean } from 'ahooks'
type Props = {
className?: string
payload: ToolWithProvider
viewType: ViewType
isShowLetterIndex: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
}
const Tool: FC<Props> = ({
className,
payload,
viewType,
isShowLetterIndex,
onSelect,
}) => {
const language = useGetLanguage()
const isTreeView = viewType === ViewType.tree
const actions = payload.tools
const isToolPlugin = payload.type === CollectionType.builtIn
const [isFold, {
toggle: toggleFold,
}] = useBoolean(false)
const FoldIcon = isFold ? RiArrowDownSLine : RiArrowRightSLine
const {
label,
} = payload
return (
<div
key={payload.id}
className='mb-1 last-of-type:mb-0'
>
<div className={cn(className)}>
<div
className='flex items-center justify-between pl-3 pr-1 w-full rounded-lg hover:bg-gray-50 cursor-pointer'
// onClick={() => {
// if (isToolPlugin) {
// toggleFold()
// return
// }
// onSelect(BlockEnum.Tool, {
// provider_id: provider.id,
// provider_type: provider.type,
// provider_name: provider.name,
// tool_name: payload.name,
// tool_label: payload.label[language],
// title: payload.label[language],
// })
// }}
>
<div className='flex grow items-center h-8'>
<BlockIcon
className='shrink-0'
type={BlockEnum.Tool}
toolIcon={payload.icon}
/>
<div className='ml-2 text-sm text-gray-900 flex-1 w-0 grow truncate'>{payload.label[language]}</div>
</div>
{isToolPlugin && (
<FoldIcon className={cn('w-4 h-4 text-text-quaternary shrink-0', isFold && 'text-text-tertiary')} />
)}
</div>
{isToolPlugin && (
actions.map(action => (
<ActonItem
key={action.name}
className={cn(isShowLetterIndex && 'mr-6')}
provider={payload}
payload={action}
onSelect={onSelect}
/>
))
)}
</div>
</div>
)
}
export default React.memo(Tool)

View File

@ -1,18 +1,17 @@
import {
memo,
useCallback,
useMemo,
useRef,
} from 'react'
import { useTranslation } from 'react-i18next'
import type { BlockEnum, ToolWithProvider } from '../types'
import { CollectionType } from '../../tools/types'
import IndexBar, { groupItems } from './index-bar'
import type { ToolDefaultValue } from './types'
import ToolItem from './tool-item'
import { ViewType } from './view-type-select'
import Empty from '@/app/components/tools/add-tool-modal/empty'
import { useGetLanguage } from '@/context/i18n'
import cn from '@/utils/classnames'
import ToolListTreeView from './tool/tool-list-tree-view/list'
import ToolListFlatView from './tool/tool-list-flat-view/list'
type ToolsProps = {
showWorkflowEmpty: boolean
@ -28,52 +27,41 @@ const Blocks = ({
}: ToolsProps) => {
const { t } = useTranslation()
const language = useGetLanguage()
const isListView = viewType === ViewType.list
const isFlatView = viewType === ViewType.flat
const isTreeView = viewType === ViewType.tree
const isShowLetterIndex = isFlatView && tools.length > 10
const { letters, groups: groupedTools } = groupItems(tools, tool => (tool as any).label[language][0])
const toolRefs = useRef({})
const renderGroup = useCallback((toolWithProvider: ToolWithProvider) => {
const list = toolWithProvider.tools
return (
<div
key={toolWithProvider.id}
className='mb-1 last-of-type:mb-0'
>
{isTreeView && (
<div className='flex items-start px-3 h-[22px] text-xs font-medium text-gray-500'>
{toolWithProvider.label[language]}
</div>
)}
{
list.map(tool => (
<ToolItem
key={tool.name}
className={cn(isListView && 'mr-6')}
isToolPlugin={toolWithProvider.type === CollectionType.builtIn}
provider={toolWithProvider}
payload={tool}
onSelect={onSelect}
/>
))
}
</div>
)
}, [onSelect, language])
const renderLetterGroup = (letter: string) => {
const tools = groupedTools[letter]
return (
<div
key={letter}
ref={el => ((toolRefs as any).current[letter] = el) as any}
>
{tools.map(renderGroup)}
</div>
)
/*
treeViewToolsData:
{
A: {
'google': [ // plugin organize name
...tools
],
'custom': [ // custom tools
...tools
],
'workflow': [ // workflow as tools
...tools
]
}
}
*/
const { letters, groups: withLetterAndGroupViewToolsData } = groupItems(tools, tool => (tool as any).label[language][0])
const treeViewToolsData = useMemo(() => {
const result: Record<string, ToolWithProvider[]> = {}
Object.keys(withLetterAndGroupViewToolsData).forEach((letter) => {
Object.keys(withLetterAndGroupViewToolsData[letter]).forEach((groupName) => {
if (!result[groupName])
result[groupName] = []
result[groupName].push(...withLetterAndGroupViewToolsData[letter][groupName])
})
})
return result
}, [withLetterAndGroupViewToolsData])
const toolRefs = useRef({})
return (
<div className='p-1 max-w-[320px]'>
@ -87,8 +75,19 @@ const Blocks = ({
<Empty />
</div>
)}
{!!tools.length && letters.map(renderLetterGroup)}
{isListView && tools.length > 10 && <IndexBar letters={letters} itemRefs={toolRefs} />}
{!!tools.length && (
isFlatView ? (
<ToolListFlatView
/>
) : (
<ToolListTreeView
payload={treeViewToolsData}
onSelect={onSelect}
/>
)
)}
{isShowLetterIndex && <IndexBar letters={letters} itemRefs={toolRefs} />}
</div>
)
}

View File

@ -5,7 +5,7 @@ import { RiNodeTree, RiSortAlphabetAsc } from '@remixicon/react'
import cn from '@/utils/classnames'
export enum ViewType {
list = 'list',
flat = 'flat',
tree = 'tree',
}
@ -31,12 +31,12 @@ const ViewTypeSelect: FC<Props> = ({
<div
className={
cn('p-[3px] rounded-lg',
viewType === ViewType.list
viewType === ViewType.flat
? 'bg-components-segmented-control-item-active-bg shadow-xs text-text-accent-light-mode-only'
: 'text-text-tertiary cursor-pointer',
)
}
onClick={handleChange(ViewType.list)}
onClick={handleChange(ViewType.flat)}
>
<RiSortAlphabetAsc className='w-4 h-4' />
</div>