Skip to content

Commit 194d67e

Browse files
committed
feat(gpt-runner-web): add popover-menu component
1 parent e84ddec commit 194d67e

File tree

8 files changed

+220
-21
lines changed

8 files changed

+220
-21
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { styled } from 'styled-components'
1+
import { css, styled } from 'styled-components'
22

33
export const ButtonWrapper = styled.div<{ $hoverShowText?: boolean }>`
44
display: flex;
55
66
${({ $hoverShowText }) => ($hoverShowText
7-
? `
7+
? css`
88
& .icon-button-text {
99
opacity: 0;
1010
width: 0px;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// PopoverMenu.tsx
2+
import React, { useState } from 'react'
3+
import { Popover } from 'react-tiny-popover'
4+
import { useHover } from '../../hooks/use-hover.hook'
5+
import { useSize } from '../../hooks/use-size.hook'
6+
import { Children, ChildrenWrapper, Menu } from './popover-menu.styles'
7+
8+
export interface PopoverMenuChildrenState {
9+
isHovering: boolean
10+
}
11+
export interface PopoverMenuProps {
12+
isPopoverOpen?: boolean
13+
onPopoverDisplayChange?: (isPopoverOpen: boolean) => void
14+
buildMenuSlot: () => React.ReactNode
15+
buildChildrenSlot: (state: PopoverMenuChildrenState) => React.ReactNode
16+
}
17+
18+
export const PopoverMenu: React.FC<PopoverMenuProps> = (props) => {
19+
const { isPopoverOpen, onPopoverDisplayChange, buildMenuSlot, buildChildrenSlot } = props
20+
const [privateIsPopoverOpen, setPrivateIsPopoverOpen] = useState(false)
21+
const [childrenHoverRef, isChildrenHovering] = useHover()
22+
const [, { height: childrenHeight }] = useSize({ ref: childrenHoverRef })
23+
const isProvideOpenAndChange = isPopoverOpen !== undefined && onPopoverDisplayChange !== undefined
24+
25+
const getIsPopoverOpen = () => {
26+
return isProvideOpenAndChange ? isPopoverOpen : privateIsPopoverOpen
27+
}
28+
29+
const getOnPopoverDisplayChange = () => {
30+
return isProvideOpenAndChange ? onPopoverDisplayChange : setPrivateIsPopoverOpen
31+
}
32+
33+
const childrenState: PopoverMenuChildrenState = {
34+
isHovering: isChildrenHovering || getIsPopoverOpen(),
35+
}
36+
37+
const handleClose = () => {
38+
getOnPopoverDisplayChange()(false)
39+
}
40+
41+
return (
42+
<Popover
43+
isOpen={getIsPopoverOpen()}
44+
positions={['bottom']}
45+
align='start'
46+
onClickOutside={handleClose}
47+
content={() => (
48+
<div>
49+
<Menu style={{
50+
marginTop: `${-childrenHeight}px`,
51+
}}
52+
onMouseLeave={handleClose}
53+
>
54+
<Children style={{
55+
width: '100%',
56+
flex: 1,
57+
}}>
58+
{buildChildrenSlot(childrenState)}
59+
</Children>
60+
{buildMenuSlot()}
61+
</Menu>
62+
</div>
63+
)}
64+
>
65+
<ChildrenWrapper>
66+
<Children
67+
ref={childrenHoverRef}
68+
style={{
69+
visibility: isPopoverOpen ? 'hidden' : 'visible',
70+
}}
71+
onMouseEnter={() => {
72+
getOnPopoverDisplayChange()(true)
73+
}}
74+
>
75+
{buildChildrenSlot(childrenState)}
76+
</Children>
77+
</ChildrenWrapper>
78+
</Popover>
79+
)
80+
}
81+
82+
PopoverMenu.displayName = 'PopoverMenu'
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import styled from 'styled-components'
2+
3+
export const Menu = styled.div`
4+
display: flex;
5+
flex-direction: column;
6+
background-color: var(--list-hover-background);
7+
border: 1px solid var(--panel-view-border);
8+
border-radius: 0.25rem;
9+
overflow: hidden;
10+
11+
& > .icon-button {
12+
margin-left: 0;
13+
margin-right: 0;
14+
padding-left: 0;
15+
padding-right: 0;
16+
border-top: 1px solid var(--panel-view-border);
17+
}
18+
19+
& > .icon-button:first-child {
20+
border-top: none;
21+
}
22+
23+
& vscode-button {
24+
flex: 1;
25+
border-radius: 0 !important;
26+
27+
&::part(content) {
28+
width: 100%;
29+
}
30+
}
31+
`
32+
export const ChildrenWrapper = styled.div`
33+
overflow: hidden;
34+
35+
& + .icon-button {
36+
padding-left: 0.5rem;
37+
}
38+
`
39+
40+
export const Children = styled.div`
41+
overflow: hidden;
42+
display: flex;
43+
align-items: center;
44+
justify-content: center;
45+
46+
& > .icon-button {
47+
width: 100%;
48+
}
49+
`

packages/gpt-runner-web/client/src/hooks/use-hover.hook.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ export function useHover<Ref extends RefObject<any>>(): [Ref, boolean] {
1414
}
1515

1616
useEffect(() => {
17-
const element = ref.current
18-
17+
const element = ref.current as HTMLElement
1918
if (element) {
2019
element.addEventListener('mouseenter', handleMouseEnter)
2120
element.addEventListener('mouseleave', handleMouseLeave)
@@ -27,7 +26,7 @@ export function useHover<Ref extends RefObject<any>>(): [Ref, boolean] {
2726
element.removeEventListener('mouseleave', handleMouseLeave)
2827
}
2928
}
30-
}, [ref])
29+
}, [ref.current])
3130

3231
return [ref, isHover]
3332
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useEffect, useRef, useState } from 'react'
2+
3+
interface Size {
4+
width: number
5+
height: number
6+
}
7+
8+
interface UseSizeProps {
9+
ref?: React.RefObject<HTMLElement>
10+
width?: number
11+
height?: number
12+
}
13+
14+
export function useSize(props?: UseSizeProps): [ref: React.RefObject<HTMLElement>, size: Size] {
15+
const { width = 0, height = 0 } = props || {}
16+
17+
const ref = props?.ref ?? useRef<HTMLElement>(null)
18+
const [size, setSize] = useState<Size>({ width, height })
19+
20+
useEffect(() => {
21+
if (!ref.current)
22+
return
23+
24+
const updateSize = () => {
25+
if (ref.current) {
26+
console.log('updateSize', ref.current.offsetWidth, ref.current.offsetHeight)
27+
setSize({
28+
width: ref.current.offsetWidth,
29+
height: ref.current.offsetHeight,
30+
})
31+
}
32+
}
33+
34+
updateSize()
35+
36+
ref.current.addEventListener('resize', updateSize)
37+
38+
return () => {
39+
ref.current?.removeEventListener('resize', updateSize)
40+
}
41+
}, [ref.current])
42+
43+
return [ref, size]
44+
}

packages/gpt-runner-web/client/src/pages/chat/chat-panel.tsx

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useGlobalStore } from '../../store/zustand/global'
1313
import type { GptFileTreeItem } from '../../store/zustand/global/sidebar-tree.slice'
1414
import { emitter } from '../../helpers/emitter'
1515
import { getGlobalConfig } from '../../helpers/global-config'
16+
import { PopoverMenu } from '../../components/popover-menu'
1617

1718
export interface ChatPanelProps {
1819
scrollDownRef: RefObject<any>
@@ -229,30 +230,40 @@ export const ChatPanel: FC<ChatPanelProps> = (props) => {
229230

230231
const renderInputToolbar = () => {
231232
return <>
232-
<IconButton
233-
text='Pre Chat'
234-
iconClassName='codicon-chevron-left'
235-
onClick={handleSwitchPreChat}
236-
></IconButton>
233+
<PopoverMenu
234+
buildChildrenSlot={({ isHovering }) => {
235+
return <IconButton
236+
text='New Chat'
237+
iconClassName='codicon-add'
238+
hoverShowText={!isHovering}
239+
onClick={handleNewChat}
240+
></IconButton>
241+
}}
242+
buildMenuSlot={() => {
243+
return <>
244+
<IconButton
245+
text='Pre Chat'
246+
iconClassName='codicon-chevron-left'
247+
hoverShowText={false}
248+
onClick={handleSwitchPreChat}
249+
></IconButton>
237250

238-
<IconButton
239-
text='Next Chat'
240-
iconClassName='codicon-chevron-right'
241-
onClick={handleSwitchNextChat}
242-
></IconButton>
251+
<IconButton
252+
text='Next Chat'
253+
iconClassName='codicon-chevron-right'
254+
hoverShowText={false}
255+
onClick={handleSwitchNextChat}
256+
></IconButton>
257+
</>
258+
}}
259+
/>
243260

244261
<IconButton
245262
text='Clear All'
246263
iconClassName='codicon-trash'
247264
onClick={handleClearAll}
248265
></IconButton>
249266

250-
<IconButton
251-
text='New Chat'
252-
iconClassName='codicon-add'
253-
onClick={handleNewChat}
254-
></IconButton>
255-
256267
{/* right icon */}
257268
<IconButton
258269
style={{

packages/gpt-runner-web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"react-markdown": "^8.0.7",
8585
"react-router-dom": "^6.11.2",
8686
"react-syntax-highlighter": "^15.5.0",
87+
"react-tiny-popover": "^7.2.4",
8788
"react-use": "^17.4.0",
8889
"remark-gfm": "^3.0.1",
8990
"styled-components": "^6.0.0-rc.2-4007",

pnpm-lock.yaml

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)