mirror of
https://github.com/langgenius/dify.git
synced 2026-04-22 19:57:40 +08:00
chore: search ui
This commit is contained in:
@ -1,12 +1,46 @@
|
||||
import type { FC } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import * as React from 'react'
|
||||
import FileTypeIcon from '@/app/components/base/file-uploader/file-type-icon'
|
||||
import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type FileItemProps = {
|
||||
name: string
|
||||
prefix?: ReactNode
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
const getAppearanceType = (name: string) => {
|
||||
const extension = name.split('.').pop()?.toLowerCase() ?? ''
|
||||
|
||||
if (['md', 'markdown', 'mdx'].includes(extension))
|
||||
return FileAppearanceTypeEnum.markdown
|
||||
|
||||
if (['json', 'yaml', 'yml', 'toml', 'js', 'jsx', 'ts', 'tsx', 'py', 'schema'].includes(extension))
|
||||
return FileAppearanceTypeEnum.code
|
||||
|
||||
return FileAppearanceTypeEnum.document
|
||||
}
|
||||
|
||||
const FileItem: FC<FileItemProps> = ({ name, prefix, active = false }) => {
|
||||
const appearanceType = getAppearanceType(name)
|
||||
|
||||
const FileItem: FC = () => {
|
||||
return (
|
||||
<div
|
||||
className="h-6 rounded bg-gray-100"
|
||||
className={cn(
|
||||
'flex h-6 items-center rounded-md pl-2 pr-1.5 text-text-secondary',
|
||||
active && 'bg-state-base-active text-text-primary',
|
||||
)}
|
||||
data-component="file-item"
|
||||
/>
|
||||
>
|
||||
{prefix}
|
||||
<div className="flex items-center gap-2 py-0.5">
|
||||
<FileTypeIcon type={appearanceType} size="sm" className={cn(active && 'text-text-primary')} />
|
||||
<span className={cn('system-sm-regular', active && 'font-medium text-text-primary')}>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,97 @@
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
'use client'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import type { ParentId, ResourceItem, ResourceItemList } from './type'
|
||||
import { RiDragDropLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FileItem from './file-item'
|
||||
import FoldItem from './fold-item'
|
||||
import { ResourceKind, SKILL_ROOT_ID } from './type'
|
||||
|
||||
type FilesProps = PropsWithChildren
|
||||
const TreeIndent = ({ depth }: { depth: number }) => {
|
||||
if (depth <= 0)
|
||||
return null
|
||||
|
||||
const Files: FC<FilesProps> = ({ children }) => {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-1 flex-col gap-2 overflow-auto"
|
||||
data-component="files"
|
||||
>
|
||||
{children}
|
||||
<div className="flex h-full items-stretch">
|
||||
{Array.from({ length: depth }).map((_, index) => (
|
||||
<span key={index} className="relative w-5 shrink-0">
|
||||
<span className="absolute left-1/2 top-0 h-full w-px bg-components-panel-border-subtle" />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type FilesProps = {
|
||||
items: ResourceItemList
|
||||
activeItemId?: string
|
||||
}
|
||||
|
||||
const buildChildrenMap = (items: ResourceItemList) => {
|
||||
const map = new Map<ParentId, ResourceItem[]>()
|
||||
items.forEach((item) => {
|
||||
const parentId = item.parent_id ?? null
|
||||
const existing = map.get(parentId)
|
||||
if (existing)
|
||||
existing.push(item)
|
||||
else
|
||||
map.set(parentId, [item])
|
||||
})
|
||||
return map
|
||||
}
|
||||
|
||||
const Files: FC<FilesProps> = ({ items, activeItemId }) => {
|
||||
const { t } = useTranslation()
|
||||
const childrenMap = useMemo(() => buildChildrenMap(items), [items])
|
||||
|
||||
const renderNodes = (parentId: ParentId, depth: number): ReactNode[] => {
|
||||
const children = childrenMap.get(parentId) || []
|
||||
|
||||
return children.flatMap((item) => {
|
||||
const prefix = <TreeIndent depth={depth} />
|
||||
const isActive = item.id === activeItemId
|
||||
const nodes: ReactNode[] = []
|
||||
|
||||
if (item.kind === ResourceKind.folder) {
|
||||
nodes.push(
|
||||
<FoldItem
|
||||
key={item.id}
|
||||
name={item.name}
|
||||
prefix={prefix}
|
||||
active={isActive}
|
||||
open
|
||||
/>,
|
||||
)
|
||||
nodes.push(...renderNodes(item.id, depth + 1))
|
||||
}
|
||||
else {
|
||||
nodes.push(
|
||||
<FileItem
|
||||
key={item.id}
|
||||
name={item.name}
|
||||
prefix={prefix}
|
||||
active={isActive}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
return nodes
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col" data-component="files">
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-px overflow-auto px-1 pb-0 pt-1">
|
||||
{renderNodes(SKILL_ROOT_ID, 0)}
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2 py-4 text-text-quaternary">
|
||||
<RiDragDropLine className="size-4" />
|
||||
<span className="system-xs-regular">
|
||||
{t('skillSidebar.dropTip', { ns: 'workflow' })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,12 +1,40 @@
|
||||
import type { FC } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { RiFolder6Line, RiFolderOpenLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type FoldItemProps = {
|
||||
name: string
|
||||
prefix?: ReactNode
|
||||
active?: boolean
|
||||
open?: boolean
|
||||
}
|
||||
|
||||
const FoldItem: FC<FoldItemProps> = ({ name, prefix, active = false, open = false }) => {
|
||||
const Icon = open ? RiFolderOpenLine : RiFolder6Line
|
||||
|
||||
const FoldItem: FC = () => {
|
||||
return (
|
||||
<div
|
||||
className="h-6 rounded bg-gray-100"
|
||||
className={cn(
|
||||
'flex h-6 items-center rounded-md pl-2 pr-1.5 text-text-secondary',
|
||||
active && 'bg-state-base-active text-text-primary',
|
||||
)}
|
||||
data-component="fold-item"
|
||||
/>
|
||||
>
|
||||
{prefix}
|
||||
<div className="flex items-center gap-2 py-0.5">
|
||||
<Icon
|
||||
className={cn(
|
||||
'size-4',
|
||||
open ? 'text-primary-600' : 'text-text-secondary',
|
||||
active && 'text-text-primary',
|
||||
)}
|
||||
/>
|
||||
<span className={cn('system-sm-regular', active && 'font-medium text-text-primary')}>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -5,25 +5,22 @@ import EditorArea from './editor-area'
|
||||
import EditorBody from './editor-body'
|
||||
import EditorTabItem from './editor-tab-item'
|
||||
import EditorTabs from './editor-tabs'
|
||||
import FileItem from './file-item'
|
||||
import Files from './files'
|
||||
import FoldItem from './fold-item'
|
||||
import { mockSkillItems } from './mock-data'
|
||||
import Sidebar from './sidebar'
|
||||
import SidebarSearchAdd from './sidebar-search-add'
|
||||
import SkillDocEditor from './skill-doc-editor'
|
||||
import SkillPageLayout from './skill-page-layout'
|
||||
|
||||
const SkillMain: FC = () => {
|
||||
const activeItemId = 'skills/_schemas/email-writer/output-schema'
|
||||
|
||||
return (
|
||||
<div className="h-full bg-workflow-canvas-workflow-top-bar-1 pl-3 pt-[52px]">
|
||||
<SkillPageLayout>
|
||||
<Sidebar>
|
||||
<SidebarSearchAdd />
|
||||
<Files>
|
||||
<FoldItem />
|
||||
<FileItem />
|
||||
<FileItem />
|
||||
</Files>
|
||||
<Files items={mockSkillItems} activeItemId={activeItemId} />
|
||||
</Sidebar>
|
||||
<EditorArea>
|
||||
<EditorTabs>
|
||||
|
||||
@ -1,14 +1,35 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
const SidebarSearchAdd: FC = () => {
|
||||
const [value, setValue] = useState('')
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2"
|
||||
className="flex items-center gap-1 bg-components-panel-bg p-2"
|
||||
data-component="sidebar-search-add"
|
||||
>
|
||||
<div className="h-8 flex-1 rounded-md bg-gray-100" />
|
||||
<div className="h-8 w-8 rounded-md bg-gray-100" />
|
||||
<SearchInput
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
className="h-8 flex-1"
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="medium"
|
||||
className={cn('!h-8 !w-8 !px-0')}
|
||||
aria-label={t('operation.add', { ns: 'common' })}
|
||||
>
|
||||
<RiAddLine className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ type SidebarProps = PropsWithChildren
|
||||
const Sidebar: FC<SidebarProps> = ({ children }) => {
|
||||
return (
|
||||
<aside
|
||||
className="flex w-[260px] shrink-0 flex-col gap-3 rounded-lg bg-white p-3"
|
||||
className="flex w-[320px] shrink-0 flex-col gap-px overflow-hidden rounded-[10px] border border-components-panel-border-subtle bg-components-panel-bg"
|
||||
data-component="sidebar"
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -995,6 +995,7 @@
|
||||
"singleRun.testRun": "Test Run",
|
||||
"singleRun.testRunIteration": "Test Run Iteration",
|
||||
"singleRun.testRunLoop": "Test Run Loop",
|
||||
"skillSidebar.dropTip": "Drop files here to upload",
|
||||
"tabs.-": "Default",
|
||||
"tabs.addAll": "Add all",
|
||||
"tabs.agent": "Agent Strategy",
|
||||
|
||||
Reference in New Issue
Block a user