Skip to content

Commit 991c443

Browse files
committed
feat(gpt-runner-web): add search for sidebar tree
1 parent 6416606 commit 991c443

File tree

21 files changed

+541
-300
lines changed

21 files changed

+541
-300
lines changed

packages/gpt-runner-core/src/core/config.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,15 @@ export async function getGptFilesInfo(params: GetGptFilesInfoParams): Promise<Ge
7777

7878
const parentTitleParts = titleParts.slice(0, -1)
7979

80-
const fileId = title || filePath
80+
const name = getName(title, filePath)
81+
82+
const fileId = [...titleParts.slice(0, -1), name].join('/') || filePath
8183

8284
const fileInfo: GptFileInfo = {
8385
id: fileId,
8486
parentId: null,
8587
path: filePath,
86-
name: getName(title, filePath),
88+
name,
8789
content,
8890
singleFileConfig,
8991
type: GptFileTreeItemType.File,

packages/gpt-runner-shared/src/common/helpers/common.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,42 @@ export function formatSourceValue<T = any>(value: any): T {
4141
return result as T
4242
}
4343

44-
export function travelTree<T extends TreeItem<Record<string, any>>, R extends TreeItem<Record<string, any>> = TreeItem<Record<string, any>> >(tree: T[], callback: (item: T, parent?: T) => void | R): R[] {
44+
export function travelTree<T extends TreeItem<Record<string, any>>, R extends TreeItem<Record<string, any>> = TreeItem<Record<string, any>> >(tree: T[], callback: (item: T, parent?: T) => void | null | undefined | R): R[] {
4545
const travel = (tree: T[], parent?: T) => {
46-
return tree.map((item) => {
47-
const finalItem = callback(item, parent) || item
48-
if (item.children)
46+
const result: R[] = []
47+
tree.forEach((item) => {
48+
const callbackResult = callback(item, parent)
49+
const finalItem = callbackResult === undefined ? item : callbackResult
50+
51+
if (finalItem && item.children)
4952
finalItem.children = travel(item.children as T[], item)
5053

51-
return finalItem
54+
if (finalItem !== null)
55+
result.push(finalItem as R)
56+
})
57+
58+
return result
59+
}
60+
return travel(tree) as R[]
61+
}
62+
63+
export function travelTreeDeepFirst<T extends TreeItem<Record<string, any>>, R extends TreeItem<Record<string, any>> = TreeItem<Record<string, any>> >(tree: T[], callback: (item: T, parent?: T) => void | null | undefined | R): R[] {
64+
const travel = (tree: T[], parent?: T) => {
65+
const result: R[] = []
66+
tree.forEach((item) => {
67+
let children: R[] | undefined
68+
69+
if (item.children)
70+
children = travel(item.children as T[], item)
71+
72+
const callbackResult = callback({ ...item, children }, parent)
73+
const finalItem = callbackResult === undefined ? item : callbackResult
74+
75+
if (finalItem !== null)
76+
result.push(finalItem as R)
5277
})
78+
79+
return result
5380
}
5481
return travel(tree) as R[]
5582
}

packages/gpt-runner-shared/src/common/types/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export interface GptFolderInfo extends GptPathBaseInfo {
105105

106106
export interface GptChatInfo extends GptPathBaseInfo {
107107
type: GptFileTreeItemType.Chat
108-
singleFileConfig: SingleFileConfig
108+
createAt: number
109109
}
110110

111111
export type GptFileInfoTreeItem = TreeItem<GptFolderInfo | GptFileInfo | GptChatInfo>

packages/gpt-runner-web/client/src/components/icon-button/index.tsx

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'
22
import type { FC } from 'react'
3+
import { useCallback, useEffect, useState } from 'react'
34
import clsx from 'clsx'
5+
import type { AnimationProps, Target, Tween } from 'framer-motion'
6+
import { motion } from 'framer-motion'
7+
import { useDebounce } from 'react-use'
8+
import type { MaybePromise } from '@nicepkg/gpt-runner-shared/common'
49
import { FlexRowCenter } from '../../styles/global.styles'
510
import type { GetComponentProps } from '../../types/common'
611
import { Icon } from '../icon'
@@ -12,14 +17,88 @@ export interface IconButtonProps extends GetComponentProps<InstanceType<typeof V
1217
radius?: string
1318
showText?: boolean
1419
hoverShowText?: boolean
20+
isAnimating?: boolean
21+
animateDuration?: number
22+
animateEase?: Tween['ease']
23+
animateSate?: {
24+
from: Target
25+
to: Target
26+
}
27+
animatingWhenClick?: boolean
28+
onClick?: () => MaybePromise<void>
1529
buttonStyle?: React.CSSProperties
1630
}
1731

1832
export const IconButton: FC<IconButtonProps> = (props) => {
19-
const { text, iconClassName, showText = true, hoverShowText = true, radius = '0.25rem', className, style, buttonStyle, ...otherProps } = props
33+
const {
34+
text,
35+
iconClassName,
36+
showText = true,
37+
hoverShowText = true,
38+
radius = '0.25rem',
39+
className, style,
40+
buttonStyle,
41+
isAnimating: initialIsAnimating = false,
42+
animateDuration = 1000,
43+
animateEase = 'linear',
44+
animateSate = {
45+
from: {
46+
rotate: 0,
47+
},
48+
to: {
49+
rotate: [0, 360],
50+
},
51+
},
52+
animatingWhenClick = false,
53+
onClick,
54+
...otherProps
55+
} = props
56+
const [isAnimating, setIsAnimating] = useState(initialIsAnimating)
57+
const [debouncedIsAnimating, setDebouncedIsAnimating] = useState(isAnimating)
58+
59+
useEffect(() => {
60+
setIsAnimating(initialIsAnimating)
61+
}, [initialIsAnimating])
62+
63+
useDebounce(() => {
64+
setDebouncedIsAnimating(isAnimating)
65+
}, animateDuration, [isAnimating])
66+
67+
useEffect(() => {
68+
isAnimating && setDebouncedIsAnimating(isAnimating)
69+
}, [isAnimating])
70+
71+
const handleClick = useCallback(async () => {
72+
if (animatingWhenClick)
73+
setIsAnimating(true)
74+
75+
await onClick?.()
76+
77+
if (animatingWhenClick)
78+
setIsAnimating(false)
79+
}, [animatingWhenClick, onClick])
80+
81+
const rotateAnimation: AnimationProps = {
82+
initial: animateSate.from,
83+
animate: Object.fromEntries(Object.entries(animateSate.to).map(([key, value]) => {
84+
return [
85+
key,
86+
debouncedIsAnimating
87+
? value
88+
: animateSate.from[key as keyof typeof animateSate.from],
89+
]
90+
})),
91+
transition: {
92+
duration: animateDuration / 1000,
93+
ease: animateEase,
94+
repeat: debouncedIsAnimating ? Infinity : 0,
95+
},
96+
}
97+
2098
return <ButtonWrapper className={clsx('icon-button', className)} style={style} $hoverShowText={hoverShowText}>
2199
<VSCodeButton
22100
{...otherProps}
101+
onClick={handleClick}
23102
appearance="secondary"
24103
ariaLabel={text}
25104
title={text}
@@ -31,7 +110,10 @@ export const IconButton: FC<IconButtonProps> = (props) => {
31110
<FlexRowCenter style={{
32111
fontSize: 'var(--type-ramp-base-font-size)',
33112
}}>
34-
<Icon className={iconClassName}></Icon>
113+
<motion.div style={{ display: 'flex' }} {...rotateAnimation}>
114+
<Icon className={iconClassName}></Icon>
115+
</motion.div>
116+
35117
{showText && <Text className='icon-button-text'>{text}</Text>}
36118
</FlexRowCenter>
37119
</VSCodeButton>
Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,81 @@
1-
import { useCallback, useState } from 'react'
2-
import { VSCodeTextField } from '@vscode/webview-ui-toolkit/react'
1+
import { useEffect, useState } from 'react'
2+
import { travelTreeDeepFirst } from '@nicepkg/gpt-runner-shared/common'
3+
import { useDebounce } from 'react-use'
34
import type { TreeProps } from '../tree'
45
import { Tree } from '../tree'
5-
import type { TopToolbarProps } from '../top-toolbar'
6-
import { TopToolbar } from '../top-toolbar'
76
import type { TreeItemBaseStateOtherInfo, TreeItemProps } from '../tree-item'
8-
import { SidebarWrapper } from './sidebar.styles'
7+
import { SidebarHeader, SidebarSearch, SidebarWrapper } from './sidebar.styles'
98

109
export interface SidebarProps<OtherInfo extends TreeItemBaseStateOtherInfo = TreeItemBaseStateOtherInfo> {
1110
defaultSearchKeyword?: string
1211
placeholder?: string
13-
topToolbar?: TopToolbarProps
14-
tree?: TreeProps<OtherInfo>
15-
renderTreeLeftSlot?: TreeItemProps<OtherInfo>['renderLeftSlot']
16-
renderTreeRightSlot?: TreeItemProps<OtherInfo>['renderRightSlot']
12+
tree?: Omit<TreeProps<OtherInfo>, 'filter'>
13+
buildTreeItem?: (item: TreeItemProps<OtherInfo>) => TreeItemProps<OtherInfo>
14+
sortTreeItems?: (items: TreeItemProps<OtherInfo>[]) => TreeItemProps<OtherInfo>[]
15+
buildTopToolbarSlot?: () => React.ReactNode
1716
}
1817

1918
export function Sidebar<OtherInfo extends TreeItemBaseStateOtherInfo = TreeItemBaseStateOtherInfo>(props: SidebarProps<OtherInfo>) {
2019
const {
2120
defaultSearchKeyword = '',
2221
placeholder,
2322
tree,
24-
topToolbar,
25-
renderTreeLeftSlot,
26-
renderTreeRightSlot,
23+
buildTreeItem,
24+
sortTreeItems,
25+
buildTopToolbarSlot,
2726
} = props
2827

2928
const [searchKeyword, setSearchKeyword] = useState(defaultSearchKeyword)
29+
const [debouncedSearchKeyword, setDebouncedSearchKeyword] = useState(defaultSearchKeyword)
30+
const [finalItems, setFinalItems] = useState<TreeItemProps<OtherInfo>[]>([])
3031

31-
const filterTreeItems = tree?.items.filter(file => searchKeyword ? file.name.includes(searchKeyword) : true)
32+
useDebounce(() => {
33+
setDebouncedSearchKeyword(searchKeyword)
34+
}, 300, [searchKeyword])
3235

33-
const processTreeItem = useCallback((items: TreeItemProps<OtherInfo>[]): TreeItemProps<OtherInfo>[] => {
34-
return items.map((item) => {
35-
return {
36-
...item,
37-
renderLeftSlot: renderTreeLeftSlot,
38-
renderRightSlot: renderTreeRightSlot,
39-
children: item.children ? processTreeItem(item.children) : undefined,
40-
}
36+
useEffect(() => {
37+
let _finalItems: TreeItemProps<OtherInfo>[] = [...(tree?.items || [])]
38+
39+
_finalItems = travelTreeDeepFirst(tree?.items || [], (item) => {
40+
if (buildTreeItem)
41+
item = buildTreeItem(item)
42+
43+
if (debouncedSearchKeyword && !item.name?.match(new RegExp(debouncedSearchKeyword, 'i')) && !item.children?.length)
44+
return null
45+
46+
const sortedChildren
47+
= sortTreeItems && item?.children
48+
? sortTreeItems(item.children)
49+
: item.children?.sort((a, b) => {
50+
// 0-9 a-z A-Z
51+
const aName = a.name?.toLowerCase()
52+
const bName = b.name?.toLowerCase()
53+
54+
return (aName < bName) ? -1 : (aName > bName) ? 1 : 0
55+
})
56+
57+
const finalExpanded = debouncedSearchKeyword ? true : item.isExpanded
58+
59+
return { ...item, isExpanded: finalExpanded, children: sortedChildren }
4160
})
42-
}, [renderTreeLeftSlot])
4361

44-
const finalTreeItems = filterTreeItems ? processTreeItem(filterTreeItems) : undefined
62+
setFinalItems(_finalItems)
63+
}, [buildTreeItem, sortTreeItems, debouncedSearchKeyword, tree?.items])
4564

4665
return <SidebarWrapper>
47-
{topToolbar && <TopToolbar {...topToolbar} />}
48-
<VSCodeTextField placeholder={placeholder}
66+
<SidebarHeader>
67+
{buildTopToolbarSlot?.()}
68+
</SidebarHeader>
69+
<SidebarSearch
70+
placeholder={placeholder}
4971
value={searchKeyword}
50-
onChange={(e: any) => {
72+
onInput={(e: any) => {
5173
setSearchKeyword(e.target?.value)
5274
}}>
53-
Text Field Label
54-
</VSCodeTextField>
55-
{finalTreeItems && <Tree {...tree} items={finalTreeItems} />}
75+
</SidebarSearch>
76+
<Tree
77+
{...tree}
78+
items={finalItems}
79+
/>
5680
</SidebarWrapper>
5781
}

packages/gpt-runner-web/client/src/components/sidebar/sidebar.styles.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { VSCodeTextField } from '@vscode/webview-ui-toolkit/react'
12
import { styled } from 'styled-components'
23

34
export const SidebarWrapper = styled.div`
@@ -7,6 +8,16 @@ export const SidebarWrapper = styled.div`
78
padding: 0 0.5rem;
89
`
910

10-
export const SidebarSearch = styled.div`
11+
export const SidebarHeader = styled.div`
12+
display: flex;
13+
align-items: center;
14+
`
15+
16+
export const SidebarSearch = styled(VSCodeTextField)`
17+
&::part(root) {
18+
border-radius: 0.25rem;
19+
overflow: hidden;
20+
}
1121
22+
margin-bottom: 0.5rem;
1223
`

packages/gpt-runner-web/client/src/components/top-toolbar/index.tsx

Lines changed: 0 additions & 26 deletions
This file was deleted.

packages/gpt-runner-web/client/src/components/top-toolbar/top-toolbar.styles.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

0 commit comments

Comments
 (0)