feat(web): add import skills menu item with tooltip to skill file tree

Add "Import skills(.zip)" option to root-level context menu and sidebar
add menu with a question mark tooltip showing usage hint. Update menu
item labels and icons for consistency with design.
This commit is contained in:
yyh
2026-02-05 16:53:27 +08:00
parent 7dcb0897c4
commit 9893bf267e
5 changed files with 73 additions and 34 deletions

View File

@ -1,8 +1,10 @@
'use client'
import type { VariantProps } from 'class-variance-authority'
import { RiQuestionLine } from '@remixicon/react'
import { cva } from 'class-variance-authority'
import * as React from 'react'
import Tooltip from '@/app/components/base/tooltip'
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
import { cn } from '@/utils/classnames'
@ -55,9 +57,10 @@ export type MenuItemProps = {
kbd?: readonly string[]
onClick: React.MouseEventHandler<HTMLButtonElement>
disabled?: boolean
tooltip?: string
} & VariantProps<typeof menuItemVariants>
const MenuItem = ({ icon: Icon, label, kbd, onClick, disabled, variant }: MenuItemProps) => {
const MenuItem = ({ icon: Icon, label, kbd, onClick, disabled, variant, tooltip }: MenuItemProps) => {
const handleClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation()
onClick(event)
@ -73,6 +76,20 @@ const MenuItem = ({ icon: Icon, label, kbd, onClick, disabled, variant }: MenuIt
<Icon className={cn(iconVariants({ variant }))} aria-hidden="true" />
<span className={cn(labelVariants({ variant }), 'flex-1 text-left')}>{label}</span>
{kbd && kbd.length > 0 && <ShortcutsName keys={kbd} textColor="secondary" />}
{tooltip && (
<Tooltip
popupContent={tooltip}
position="right"
>
<button
type="button"
className="flex shrink-0 items-center justify-center"
onClick={e => e.stopPropagation()}
>
<RiQuestionLine className="size-4 text-text-quaternary hover:text-text-tertiary" />
</button>
</Tooltip>
)}
</button>
)
}

View File

@ -13,10 +13,12 @@ import {
RiScissorsLine,
RiUploadLine,
} from '@remixicon/react'
import dynamic from 'next/dynamic'
import * as React from 'react'
import { useCallback } from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Confirm from '@/app/components/base/confirm'
import { UploadCloud02 } from '@/app/components/base/icons/src/vender/line/general'
import { Download02 } from '@/app/components/base/icons/src/vender/solid/general'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { cn } from '@/utils/classnames'
@ -24,6 +26,10 @@ import { NODE_MENU_TYPE } from '../constants'
import { useFileOperations } from '../hooks/use-file-operations'
import MenuItem from './menu-item'
const ImportSkillModal = dynamic(() => import('../start-tab/import-skill-modal'), {
ssr: false,
})
export const MENU_CONTAINER_STYLES = [
'min-w-[180px] rounded-xl border-[0.5px] border-components-panel-border',
'bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]',
@ -55,6 +61,7 @@ const NodeMenu = ({
const hasClipboard = useStore(s => s.hasClipboard())
const isRoot = type === NODE_MENU_TYPE.ROOT
const isFolder = type === NODE_MENU_TYPE.FOLDER || isRoot
const [isImportModalOpen, setIsImportModalOpen] = useState(false)
const {
fileInputRef,
@ -134,7 +141,7 @@ const NodeMenu = ({
<div className="my-1 h-px bg-divider-subtle" />
<MenuItem
icon={RiUploadLine}
icon={UploadCloud02}
label={t('skillSidebar.menu.uploadFile')}
onClick={() => fileInputRef.current?.click()}
disabled={isLoading}
@ -146,6 +153,19 @@ const NodeMenu = ({
disabled={isLoading}
/>
{isRoot && (
<>
<div className="my-1 h-px bg-divider-subtle" />
<MenuItem
icon={RiUploadLine}
label={t('skillSidebar.menu.importSkills')}
onClick={() => setIsImportModalOpen(true)}
disabled={isLoading}
tooltip={t('skill.startTab.importSkillDesc')}
/>
</>
)}
{(showRenameDelete || hasClipboard) && <div className="my-1 h-px bg-divider-subtle" />}
</>
)}
@ -212,6 +232,10 @@ const NodeMenu = ({
onCancel={handleDeleteCancel}
isLoading={isDeleting}
/>
<ImportSkillModal
isOpen={isImportModalOpen}
onClose={() => setIsImportModalOpen(false)}
/>
</div>
)
}

View File

@ -7,10 +7,12 @@ import {
RiFolderUploadLine,
RiUploadLine,
} from '@remixicon/react'
import dynamic from 'next/dynamic'
import * as React from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { UploadCloud02 } from '@/app/components/base/icons/src/vender/line/general'
import {
PortalToFollowElem,
PortalToFollowElemContent,
@ -18,42 +20,22 @@ import {
} from '@/app/components/base/portal-to-follow-elem'
import SearchInput from '@/app/components/base/search-input'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { cn } from '@/utils/classnames'
import { ROOT_ID } from './constants'
import MenuItem from './file-tree/menu-item'
import { useFileOperations } from './hooks/use-file-operations'
import { useSkillAssetTreeData } from './hooks/use-skill-asset-tree'
import { getTargetFolderIdFromSelection } from './utils/tree-utils'
type MenuItemProps = {
icon: React.ElementType
label: string
onClick: () => void
disabled?: boolean
}
const MenuItem = ({ icon: Icon, label, onClick, disabled }: MenuItemProps) => (
<button
type="button"
onClick={onClick}
disabled={disabled}
className={cn(
'flex w-full items-center gap-2 rounded-lg px-3 py-2',
'hover:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-components-input-border-active',
)}
>
<Icon className="size-4 text-text-tertiary" aria-hidden="true" />
<span className="system-sm-regular text-text-secondary">
{label}
</span>
</button>
)
const ImportSkillModal = dynamic(() => import('./start-tab/import-skill-modal'), {
ssr: false,
})
const SidebarSearchAdd = () => {
const { t } = useTranslation('workflow')
const searchValue = useStore(s => s.fileTreeSearchTerm)
const storeApi = useWorkflowStore()
const [showMenu, setShowMenu] = useState(false)
const [isImportModalOpen, setIsImportModalOpen] = useState(false)
const { data: treeData } = useSkillAssetTreeData()
const selectedTreeNodeId = useStore(s => s.selectedTreeNodeId)
@ -137,7 +119,7 @@ const SidebarSearchAdd = () => {
<div className="my-1 h-px bg-divider-subtle" />
<MenuItem
icon={RiUploadLine}
icon={UploadCloud02}
label={t('skillSidebar.menu.uploadFile')}
onClick={() => fileInputRef.current?.click()}
disabled={isLoading}
@ -148,9 +130,23 @@ const SidebarSearchAdd = () => {
onClick={() => folderInputRef.current?.click()}
disabled={isLoading}
/>
<div className="my-1 h-px bg-divider-subtle" />
<MenuItem
icon={RiUploadLine}
label={t('skillSidebar.menu.importSkills')}
onClick={() => setIsImportModalOpen(true)}
disabled={isLoading}
tooltip={t('skill.startTab.importSkillDesc')}
/>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
<ImportSkillModal
isOpen={isImportModalOpen}
onClose={() => setIsImportModalOpen(false)}
/>
</div>
)
}

View File

@ -1147,19 +1147,20 @@
"skillSidebar.menu.folderCreated": "Folder created successfully",
"skillSidebar.menu.folderDropNotSupported": "Folder upload via drag-drop is not supported yet. Please use the upload folder option.",
"skillSidebar.menu.folderUploaded": "Folder uploaded successfully",
"skillSidebar.menu.importSkills": "Import skills(.zip)",
"skillSidebar.menu.moreActions": "More actions",
"skillSidebar.menu.moveError": "Failed to move",
"skillSidebar.menu.moved": "Moved successfully",
"skillSidebar.menu.newFile": "New File",
"skillSidebar.menu.newFile": "New file",
"skillSidebar.menu.newFilePrompt": "Enter file name (with extension, e.g., script.py):",
"skillSidebar.menu.newFolder": "New Folder",
"skillSidebar.menu.newFolder": "New folder...",
"skillSidebar.menu.newFolderPrompt": "Enter folder name:",
"skillSidebar.menu.paste": "Paste",
"skillSidebar.menu.rename": "Rename",
"skillSidebar.menu.renameError": "Failed to rename",
"skillSidebar.menu.renamed": "Renamed successfully",
"skillSidebar.menu.uploadError": "Failed to upload",
"skillSidebar.menu.uploadFile": "Upload File",
"skillSidebar.menu.uploadFile": "Upload files...",
"skillSidebar.menu.uploadFolder": "Upload Folder",
"skillSidebar.newFolder": "New folder",
"skillSidebar.renameFileInput": "Rename file",

View File

@ -1139,18 +1139,19 @@
"skillSidebar.menu.folderCreated": "文件夹创建成功",
"skillSidebar.menu.folderDropNotSupported": "暂不支持拖拽上传文件夹,请使用上传文件夹选项。",
"skillSidebar.menu.folderUploaded": "文件夹上传成功",
"skillSidebar.menu.importSkills": "导入技能(.zip)",
"skillSidebar.menu.moveError": "移动失败",
"skillSidebar.menu.moved": "移动成功",
"skillSidebar.menu.newFile": "新建文件",
"skillSidebar.menu.newFilePrompt": "请输入文件名(包含扩展名,如 script.py",
"skillSidebar.menu.newFolder": "新建文件夹",
"skillSidebar.menu.newFolder": "新建文件夹...",
"skillSidebar.menu.newFolderPrompt": "请输入文件夹名称:",
"skillSidebar.menu.paste": "粘贴",
"skillSidebar.menu.rename": "重命名",
"skillSidebar.menu.renameError": "重命名失败",
"skillSidebar.menu.renamed": "重命名成功",
"skillSidebar.menu.uploadError": "上传失败",
"skillSidebar.menu.uploadFile": "上传文件",
"skillSidebar.menu.uploadFile": "上传文件...",
"skillSidebar.menu.uploadFolder": "上传文件夹",
"skillSidebar.newFolder": "新建文件夹",
"skillSidebar.renameFileInput": "重命名文件",