chore: search ui

This commit is contained in:
Joel
2026-01-15 10:58:37 +08:00
parent 657739d48b
commit 74d8bdd3a7
7 changed files with 190 additions and 27 deletions

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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}

View File

@ -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",