feat: panel ui

This commit is contained in:
Joel
2026-01-16 18:39:00 +08:00
parent 16078a9df6
commit d542a74733
2 changed files with 121 additions and 6 deletions

View File

@ -5,7 +5,6 @@ import type { ToolWithProvider } from '@/app/components/workflow/types'
import * as React from 'react'
import { useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import { useSelectOrDelete } from '@/app/components/base/prompt-editor/hooks'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
@ -28,6 +27,7 @@ import { canFindTool } from '@/utils'
import { cn } from '@/utils/classnames'
import { basePath } from '@/utils/var'
import { DELETE_TOOL_BLOCK_COMMAND } from './index'
import ToolHeader from './tool-header'
type ToolBlockComponentProps = {
nodeKey: string
@ -85,7 +85,6 @@ const ToolBlockComponent: FC<ToolBlockComponentProps> = ({
}) => {
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_TOOL_BLOCK_COMMAND)
const language = useGetLanguage()
const { t } = useTranslation()
const { theme } = useTheme()
const [isSettingOpen, setIsSettingOpen] = useState(false)
const [toolValue, setToolValue] = useState<ToolValue | null>(null)
@ -127,6 +126,17 @@ const ToolBlockComponent: FC<ToolBlockComponentProps> = ({
}
}, [currentProvider, currentTool, language, tool])
const toolDescriptionText = useMemo(() => {
if (toolValue?.tool_description)
return toolValue.tool_description
if (currentTool?.description) {
return typeof currentTool.description === 'object'
? (currentTool.description?.[language] || '')
: (currentTool.description || '')
}
return ''
}, [currentTool?.description, language, toolValue?.tool_description])
const toolConfigFromMetadata = useMemo(() => {
if (!activeTabId)
return undefined
@ -345,14 +355,19 @@ const ToolBlockComponent: FC<ToolBlockComponentProps> = ({
</span>
{portalContainer && isSettingOpen && createPortal(
<div
className="absolute right-4 top-4 z-[999]"
className="absolute bottom-4 right-4 top-4 z-[999]"
data-tool-setting-panel="true"
>
<div className={cn('relative max-h-[642px] min-h-20 w-[361px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-4 shadow-lg backdrop-blur-sm', 'overflow-y-auto pb-2')}>
<div className="system-xl-semibold px-4 pb-1 pt-3.5 text-text-primary">{t('detailPanel.toolSelector.toolSetting', { ns: 'plugin' })}</div>
<div className={cn('relative h-full min-h-20 w-[361px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-4 shadow-lg backdrop-blur-sm', 'overflow-y-auto pb-2')}>
{currentProvider && currentTool && toolValue && (
<>
<div className="px-4 pb-2 text-xs text-text-tertiary">{displayLabel}</div>
<ToolHeader
icon={resolvedIcon}
providerLabel={currentProvider.label?.[language] || currentProvider.name || provider}
toolLabel={toolValue.tool_label || displayLabel}
description={toolDescriptionText}
onClose={() => setIsSettingOpen(false)}
/>
<ToolAuthorizationSection
currentProvider={currentProvider}
credentialId={toolValue.credential_id}

View File

@ -0,0 +1,100 @@
'use client'
import type { FC } from 'react'
import type { Emoji } from '@/app/components/tools/types'
import { RiBookOpenLine, RiCloseLine } from '@remixicon/react'
import AppIcon from '@/app/components/base/app-icon'
type ToolHeaderProps = {
icon: string | Emoji | undefined
providerLabel: string
toolLabel: string
description: string
onClose: () => void
}
const ToolHeader: FC<ToolHeaderProps> = ({
icon,
providerLabel,
toolLabel,
description,
onClose,
}) => {
const renderHeaderIcon = () => {
if (!icon)
return null
if (typeof icon === 'string') {
if (icon.startsWith('http') || icon.startsWith('/')) {
return (
<span className="flex h-5 w-5 shrink-0 items-center justify-center overflow-hidden rounded-[6px] border border-divider-subtle bg-background-default-dodge">
<span
className="h-full w-full bg-cover bg-center"
style={{ backgroundImage: `url(${icon})` }}
/>
</span>
)
}
return (
<AppIcon
size="xs"
icon={icon}
className="!h-5 !w-5 shrink-0 !rounded-[6px] !border border-divider-subtle bg-background-default-dodge"
/>
)
}
return (
<AppIcon
size="xs"
icon={icon.content}
background={icon.background}
className="!h-5 !w-5 shrink-0 !rounded-[6px] !border border-divider-subtle bg-background-default-dodge"
/>
)
}
return (
<>
<div className="flex items-start gap-1 px-3 pb-2 pt-3">
<div className="flex flex-1 flex-col items-start">
<div className="flex items-center gap-1 rounded-md px-1 py-1">
{renderHeaderIcon()}
<span className="system-xs-medium text-text-tertiary">
{providerLabel}
</span>
</div>
</div>
<div className="flex items-center gap-1 pt-1">
<button
type="button"
className="flex h-6 w-6 items-center justify-center rounded-[6px] text-text-tertiary hover:bg-state-base-hover"
onClick={(event) => {
event.stopPropagation()
}}
>
<RiBookOpenLine className="h-4 w-4" />
</button>
<button
type="button"
className="flex h-6 w-6 items-center justify-center rounded-[6px] text-text-tertiary hover:bg-state-base-hover"
onClick={(event) => {
event.stopPropagation()
onClose()
}}
>
<RiCloseLine className="h-4 w-4" />
</button>
</div>
</div>
<div className="mt-1.5 px-3 pb-2">
<div className="system-md-semibold text-text-primary">
{toolLabel}
</div>
<div className="system-sm-regular mt-2.5 text-text-secondary">
{description}
</div>
</div>
</>
)
}
export default ToolHeader