app picker

This commit is contained in:
JzoNg 2024-11-14 14:11:55 +08:00
parent c723bd2c96
commit 7446244147
5 changed files with 172 additions and 53 deletions

View File

@ -0,0 +1,116 @@
'use client'
import type { FC } from 'react'
import React, { useMemo } from 'react'
import { useState } from 'react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import Input from '@/app/components/base/input'
import AppIcon from '@/app/components/base/app-icon'
import {
useAppFullList,
} from '@/service/use-apps'
import type { App } from '@/types/app'
type Props = {
disabled: boolean
trigger: React.ReactNode
placement?: Placement
offset?: OffsetOptions
isShow: boolean
onShowChange: (isShow: boolean) => void
onSelect: (app: App) => void
}
const AppPicker: FC<Props> = ({
disabled,
trigger,
placement = 'right-start',
offset = 0,
isShow,
onShowChange,
onSelect,
}) => {
const [searchText, setSearchText] = useState('')
const { data: appList } = useAppFullList()
const filteredAppList = useMemo(() => {
return (appList || []).filter(app => app.name.toLowerCase().includes(searchText.toLowerCase()))
}, [appList, searchText])
const getAppType = (app: App) => {
switch (app.mode) {
case 'advanced-chat':
return 'chatflow'
case 'agent-chat':
return 'agent'
case 'chat':
return 'chat'
case 'completion':
return 'completion'
case 'workflow':
return 'workflow'
}
}
const handleTriggerClick = () => {
if (disabled) return
onShowChange(true)
}
return (
<PortalToFollowElem
placement={placement}
offset={offset}
open={isShow}
onOpenChange={onShowChange}
>
<PortalToFollowElemTrigger
onClick={handleTriggerClick}
>
{trigger}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className="relative w-[356px] min-h-20 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border shadow-lg">
<div className='p-2 pb-1'>
<Input
showLeftIcon
showClearIcon
// wrapperClassName='w-[200px]'
value={searchText}
onChange={e => setSearchText(e.target.value)}
onClear={() => setSearchText('')}
/>
</div>
<div className='p-1'>
{filteredAppList.map(app => (
<div
key={app.id}
className='flex items-center gap-3 py-1 pl-2 pr-3 rounded-lg hover:bg-state-base-hover cursor-pointer'
onClick={() => onSelect(app)}
>
<AppIcon
className='shrink-0'
size='xs'
iconType={app.icon_type}
icon={app.icon}
background={app.icon_background}
imageUrl={app.icon_url}
/>
<div title={app.name} className='grow system-sm-medium text-components-input-text-filled'>{app.name}</div>
<div className='shrink-0 text-text-tertiary system-2xs-medium-uppercase'>{getAppType(app)}</div>
</div>
))}
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default React.memo(AppPicker)

View File

@ -25,18 +25,16 @@ const AppTrigger = ({
appDetail && 'pl-1.5 py-1.5',
)}>
{appDetail && (
<div className='shrink-0 mr-1 p-px rounded-lg bg-components-panel-bg border border-components-panel-border'>
<AppIcon
size='xs'
iconType={appDetail.icon_type}
icon={appDetail.icon}
background={appDetail.icon_background}
imageUrl={appDetail.icon_url}
/>
</div>
<AppIcon
size='xs'
iconType={appDetail.icon_type}
icon={appDetail.icon}
background={appDetail.icon_background}
imageUrl={appDetail.icon_url}
/>
)}
{appDetail && (
<div className='grow system-sm-regular text-components-input-text-filled'>{appDetail.name}</div>
<div title={appDetail.name} className='grow system-sm-medium text-components-input-text-filled'>{appDetail.name}</div>
)}
{!appDetail && (
<div className='grow text-components-input-text-placeholder system-sm-regular truncate'>{t('app.appSelector.placeholder')}</div>

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useMemo, useState } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
PortalToFollowElem,
@ -8,33 +8,29 @@ import {
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import AppTrigger from '@/app/components/plugins/plugin-detail-panel/app-selector/app-trigger'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import Button from '@/app/components/base/button'
import AppPicker from '@/app/components/plugins/plugin-detail-panel/app-selector/app-picker'
// import Button from '@/app/components/base/button'
import {
useAllBuiltInTools,
useAllCustomTools,
useAllWorkflowTools,
} from '@/service/use-tools'
import { CollectionType } from '@/app/components/tools/types'
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
import { useAppDetail } from '@/service/use-apps'
import type { App } from '@/types/app'
import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import cn from '@/utils/classnames'
type Props = {
value?: {
provider: string
tool_name: string
app_id: string
inputs: Record<string, any>
files?: any[]
}
disabled?: boolean
placement?: Placement
offset?: OffsetOptions
onSelect: (tool: {
provider: string
tool_name: string
onSelect: (app: {
app_id: string
inputs: Record<string, any>
files?: any[]
}) => void
supportAddCustomTool?: boolean
}
@ -52,25 +48,16 @@ const AppSelector: FC<Props> = ({
onShowChange(true)
}
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const currentProvider = useMemo(() => {
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])]
return mergedTools.find((toolWithProvider) => {
return toolWithProvider.id === value?.provider && toolWithProvider.tools.some(tool => tool.name === value?.tool_name)
})
}, [value, buildInTools, customTools, workflowTools])
const { data: currentApp } = useAppDetail(value?.app_id || '')
const [isShowChooseApp, setIsShowChooseApp] = useState(false)
const handleSelectTool = (tool: ToolDefaultValue) => {
const toolValue = {
provider: tool.provider_id,
tool_name: tool.tool_name,
const handleSelectApp = (app: App) => {
const appValue = {
app_id: app.id,
inputs: value?.inputs || {},
files: value?.files || [],
}
onSelect(toolValue)
onSelect(appValue)
setIsShowChooseApp(false)
if (tool.provider_type === CollectionType.builtIn && tool.is_team_authorization)
onShowChange(false)
}
return (
@ -94,7 +81,7 @@ const AppSelector: FC<Props> = ({
<div className="relative w-[389px] min-h-20 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border shadow-lg">
<div className='px-4 py-3 flex flex-col gap-1'>
<div className='h-6 flex items-center system-sm-semibold text-text-secondary'>{t('tools.toolSelector.label')}</div>
<ToolPicker
<AppPicker
placement='bottom'
offset={offset}
trigger={
@ -106,19 +93,11 @@ const AppSelector: FC<Props> = ({
isShow={isShowChooseApp}
onShowChange={setIsShowChooseApp}
disabled={false}
supportAddCustomTool
onSelect={handleSelectTool}
onSelect={handleSelectApp}
/>
</div>
{/* app inputs config panel */}
<div className='px-4 py-3 flex items-center border-t border-divider-subtle'>
<Button
variant='primary'
className={cn('shrink-0 w-full')}
onClick={() => {}}
>
{t('tools.auth.unauthorized')}
</Button>
</div>
</div>
</PortalToFollowElemContent>

View File

@ -40,7 +40,7 @@ const ToolTrigger = ({
</div>
)}
{value && (
<div className='grow system-sm-regular text-components-input-text-filled'>{value.tool_name}</div>
<div className='grow system-sm-medium text-components-input-text-filled'>{value.tool_name}</div>
)}
{!value && (
<div className='grow text-components-input-text-placeholder system-sm-regular'>{t('tools.toolSelector.placeholder')}</div>

26
web/service/use-apps.ts Normal file
View File

@ -0,0 +1,26 @@
import { get } from './base'
import type { App } from '@/types/app'
import { useInvalid } from './use-base'
import { useQuery } from '@tanstack/react-query'
const NAME_SPACE = 'apps'
// TODO paging for list
const useAppFullListKey = [NAME_SPACE, 'full-list']
export const useAppFullList = () => {
return useQuery<App[]>({
queryKey: useAppFullListKey,
queryFn: () => get<App[]>('/apps', { params: { page: 1, limit: 100 } }),
})
}
export const useInvalidateAppFullList = () => {
return useInvalid(useAppFullListKey)
}
export const useAppDetail = (appID: string) => {
return useQuery<App>({
queryKey: [NAME_SPACE, 'detail', appID],
queryFn: () => get<App>(`/apps/${appID}`),
})
}