feat(gpt-runner-web): optimize tab and monaco-editor
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -26,9 +26,8 @@ website/netlifyDeployPreview/*
|
||||
website/changelog
|
||||
!website/netlifyDeployPreview/index.html
|
||||
!website/netlifyDeployPreview/_redirects
|
||||
|
||||
website/_dogfooding/_swizzle_theme_tests
|
||||
|
||||
website/i18n/**/*
|
||||
|
||||
.gpt-runner
|
||||
packages/gpt-runner-web/client/public/monaco-editor-vs
|
||||
|
||||
22
packages/gpt-runner-web/client/scripts/copy-monaco-editor.ts
Normal file
22
packages/gpt-runner-web/client/scripts/copy-monaco-editor.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as path from 'node:path'
|
||||
import * as fs from 'fs-extra'
|
||||
|
||||
const resolvePath = (...paths: string[]) => path.resolve(__dirname, ...paths)
|
||||
const sourceDir = resolvePath('../../node_modules/monaco-editor/min/vs')
|
||||
const targetDir = resolvePath('../public', 'monaco-editor-vs')
|
||||
|
||||
// copy source directory to target directory
|
||||
export async function copyMonacoEditor() {
|
||||
try {
|
||||
// if target directory exists, do nothing
|
||||
if (await fs.pathExists(targetDir))
|
||||
return
|
||||
|
||||
await fs.ensureDir(targetDir)
|
||||
await fs.copy(sourceDir, targetDir)
|
||||
console.log('Copied \'monaco-editor/min/vs\' to \'public/monaco-editor-vs\'')
|
||||
}
|
||||
catch (err: any) {
|
||||
console.error(`Failed to copy 'monaco-editor/min/vs': ${err.message}`)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { VSCodeTextArea } from '@vscode/webview-ui-toolkit/react'
|
||||
import { styled } from 'styled-components'
|
||||
import { Logo } from '../logo'
|
||||
|
||||
@@ -38,16 +37,6 @@ export const TextAreaWrapper = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export const StyledVSCodeTextArea = styled(VSCodeTextArea)`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&::part(control) {
|
||||
border-radius: 0.25rem;
|
||||
height: 100%;
|
||||
}
|
||||
`
|
||||
|
||||
export const LogoWrapper = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@@ -24,15 +24,6 @@ export const ChatMessageInput: FC<ChatMessageInputProps> = memo((props) => {
|
||||
</ToolbarWrapper>
|
||||
|
||||
<TextAreaWrapper>
|
||||
{/* <StyledVSCodeTextArea
|
||||
rows={10}
|
||||
value={value}
|
||||
onInput={(e: any) => {
|
||||
onChange(e.target?.value)
|
||||
}}
|
||||
>
|
||||
</StyledVSCodeTextArea> */}
|
||||
|
||||
<Editor
|
||||
className='chat-input-editor'
|
||||
language='markdown' value={value} onChange={(value) => {
|
||||
|
||||
@@ -1,39 +1,32 @@
|
||||
import type { CSSProperties, FC } from 'react'
|
||||
import type { CSSProperties } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { TabList } from '../tab-list'
|
||||
|
||||
import type { TabProps } from '../tab'
|
||||
import { Tab } from '../tab'
|
||||
import {
|
||||
PanelTabContainer,
|
||||
PanelTabContent,
|
||||
} from './panel-tab.styles'
|
||||
|
||||
export interface PanelProps {
|
||||
defaultActiveIndex?: number
|
||||
export interface PanelTabProps<T extends string = string> extends Pick<TabProps<T>, 'defaultActiveId' | 'items' | 'onChange' | 'activeId'> {
|
||||
style?: CSSProperties
|
||||
tabStyle?: CSSProperties
|
||||
items?: any[]
|
||||
onChange?: (activeIndex: number) => void
|
||||
}
|
||||
|
||||
export const PanelTab: FC<PanelProps> = memo(({
|
||||
defaultActiveIndex,
|
||||
style,
|
||||
tabStyle,
|
||||
items = [],
|
||||
onChange,
|
||||
}) => {
|
||||
export function PanelTab_<T extends string = string>(props: PanelTabProps<T>) {
|
||||
const { style, tabStyle, ...otherProps } = props
|
||||
|
||||
return (
|
||||
<PanelTabContainer style={style}>
|
||||
<PanelTabContent>
|
||||
<TabList
|
||||
tabList={items}
|
||||
defaultActiveIndex={defaultActiveIndex}
|
||||
<Tab
|
||||
style={tabStyle}
|
||||
onChange={onChange}
|
||||
{...otherProps}
|
||||
/>
|
||||
</PanelTabContent>
|
||||
</PanelTabContainer>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
PanelTab.displayName = 'PanelTab'
|
||||
PanelTab_.displayName = 'PanelTab'
|
||||
|
||||
export const PanelTab = memo(PanelTab_) as typeof PanelTab_
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
import type { CSSProperties, FC } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useElementSizeRealTime } from '../../hooks/use-element-size-real-time.hook'
|
||||
import { Icon } from '../icon'
|
||||
import {
|
||||
ActivedTabIndicator,
|
||||
MoreIcon,
|
||||
MoreList,
|
||||
MoreListItem,
|
||||
MoreWrapper,
|
||||
TabContainer,
|
||||
TabItem,
|
||||
TabItemWrapper,
|
||||
TabListHeader,
|
||||
TabListWrapper,
|
||||
TabView,
|
||||
} from './tab-list.styles'
|
||||
|
||||
export interface TabItemData {
|
||||
label: string
|
||||
key: string | number
|
||||
children?: React.ReactNode
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
export interface TabProps {
|
||||
defaultActiveIndex?: number
|
||||
tabList: TabItemData[]
|
||||
style?: CSSProperties
|
||||
onChange?: (activeIndex: number) => void
|
||||
}
|
||||
|
||||
export const TabList: FC<TabProps> = ({
|
||||
defaultActiveIndex,
|
||||
tabList = [],
|
||||
onChange,
|
||||
}) => {
|
||||
const [activeIndex, setActiveIndex] = useState(defaultActiveIndex || 0)
|
||||
const [indicatorWidth, setIndicatorWidth] = useState(0)
|
||||
const [indicatorLeft, setIndicatorLeft] = useState(0)
|
||||
const [showMore, setShowMore] = useState(false)
|
||||
const [moreList, setMoreList] = useState<TabItemData[]>([])
|
||||
const [showMoreList, setShowMoreList] = useState(false)
|
||||
const [tabRef, tabSize] = useElementSizeRealTime<HTMLDivElement>()
|
||||
|
||||
const handleTabItemClick = useCallback((index: number) => {
|
||||
setActiveIndex(index)
|
||||
setShowMoreList(false)
|
||||
}, [setActiveIndex, setShowMoreList])
|
||||
|
||||
const calcTabsWidth = useCallback(() => {
|
||||
if (!tabRef.current)
|
||||
return 0
|
||||
|
||||
const labelElements = tabRef.current
|
||||
.children as HTMLCollectionOf<HTMLElement>
|
||||
const tabsWidth = Array.from(labelElements).reduce((acc, cur) => {
|
||||
return acc + cur.offsetWidth
|
||||
}, 0)
|
||||
return tabsWidth
|
||||
}, [tabRef.current])
|
||||
|
||||
const updateIndicatorPosition = useCallback(() => {
|
||||
if (tabRef.current) {
|
||||
const labelElements = tabRef.current
|
||||
.children as HTMLCollectionOf<HTMLElement>
|
||||
setIndicatorWidth(labelElements[activeIndex].offsetWidth)
|
||||
setIndicatorLeft(labelElements[activeIndex].offsetLeft)
|
||||
labelElements[activeIndex].scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'center',
|
||||
})
|
||||
}
|
||||
}, [activeIndex, tabRef.current, setIndicatorWidth, setIndicatorLeft])
|
||||
|
||||
const createIntersectionObserver = (root: HTMLDivElement, observerNode: HTMLDivElement) => {
|
||||
const options = {
|
||||
root,
|
||||
rootMargin: '0px',
|
||||
threshold: 0.3,
|
||||
}
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries?.forEach((entry) => {
|
||||
entry.target.setAttribute('data-visible', entry.intersectionRatio > 0.6 ? 'true' : 'false')
|
||||
})
|
||||
}, options)
|
||||
|
||||
observerNode && observer.observe(observerNode)
|
||||
}
|
||||
|
||||
const handleVisibleTabItem = useCallback(() => {
|
||||
if (!tabRef.current)
|
||||
return
|
||||
const tabListItems = Array.from(tabRef.current?.children as HTMLCollectionOf<HTMLElement>)
|
||||
tabListItems?.forEach((item) => {
|
||||
createIntersectionObserver(tabRef.current as HTMLDivElement, item as HTMLDivElement)
|
||||
})
|
||||
const _moreList: TabItemData[] = []
|
||||
Array.from(tabRef.current?.children).forEach((item, index) => {
|
||||
const isVisible = item.getAttribute('data-visible') === 'true'
|
||||
const { label = '', key = '' } = tabList?.[index] || {}
|
||||
_moreList.push({
|
||||
label,
|
||||
key,
|
||||
visible: isVisible,
|
||||
})
|
||||
})
|
||||
setMoreList(_moreList)
|
||||
}, [tabRef.current, tabList])
|
||||
|
||||
useEffect(() => {
|
||||
setActiveIndex(defaultActiveIndex || 0)
|
||||
|
||||
window.addEventListener('resize', updateIndicatorPosition)
|
||||
tabRef.current?.addEventListener('scroll', handleVisibleTabItem)
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateIndicatorPosition)
|
||||
tabRef.current?.removeEventListener('scroll', handleVisibleTabItem)
|
||||
}
|
||||
}, [defaultActiveIndex])
|
||||
|
||||
useEffect(() => {
|
||||
updateIndicatorPosition()
|
||||
}, [defaultActiveIndex, updateIndicatorPosition, tabRef.current, setActiveIndex])
|
||||
|
||||
useEffect(() => {
|
||||
onChange?.(activeIndex)
|
||||
}, [activeIndex, onChange])
|
||||
|
||||
useEffect(() => {
|
||||
const tabsTotalWidth = calcTabsWidth()
|
||||
setShowMore(tabsTotalWidth > tabSize.width)
|
||||
handleVisibleTabItem()
|
||||
}, [calcTabsWidth, setShowMore, handleVisibleTabItem, tabSize.width])
|
||||
|
||||
return (
|
||||
<TabContainer>
|
||||
<TabListHeader $showMore={showMore}>
|
||||
<TabListWrapper ref={tabRef} $showMore={showMore}>
|
||||
{tabList.map((item, index) => (
|
||||
<div key={index} data-index={index}>
|
||||
<TabItemWrapper onClick={() => handleTabItemClick(index)}>
|
||||
<TabItem
|
||||
className={activeIndex === index ? 'active' : ''}
|
||||
tabIndex={activeIndex === index ? 0 : -1}
|
||||
>
|
||||
{item.label}
|
||||
</TabItem>
|
||||
</TabItemWrapper>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<ActivedTabIndicator
|
||||
left={indicatorLeft}
|
||||
width={indicatorWidth}
|
||||
/>
|
||||
</TabListWrapper>
|
||||
|
||||
{showMore && (
|
||||
<MoreWrapper>
|
||||
<MoreIcon onClick={() => setShowMoreList(!showMoreList)}>
|
||||
<Icon className="codicon-more" />
|
||||
</MoreIcon>
|
||||
{showMoreList && (
|
||||
<MoreList>
|
||||
{moreList.map((item, index) => (
|
||||
!item.visible && <MoreListItem key={index} onClick={() => handleTabItemClick(index)}>
|
||||
{item.label}
|
||||
</MoreListItem>
|
||||
))}
|
||||
</MoreList>
|
||||
)}
|
||||
</MoreWrapper>
|
||||
)}
|
||||
</TabListHeader>
|
||||
|
||||
<TabView>
|
||||
{tabList[activeIndex]?.children}
|
||||
</TabView>
|
||||
</TabContainer>
|
||||
)
|
||||
}
|
||||
|
||||
TabList.displayName = 'TabList'
|
||||
225
packages/gpt-runner-web/client/src/components/tab/index.tsx
Normal file
225
packages/gpt-runner-web/client/src/components/tab/index.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import type { CSSProperties, ReactNode } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useMotionValue } from 'framer-motion'
|
||||
import { useElementSizeRealTime } from '../../hooks/use-element-size-real-time.hook'
|
||||
import { Icon } from '../icon'
|
||||
import { useDebounceFn } from '../../hooks/use-debounce-fn.hook'
|
||||
import { isElementVisible } from '../../helpers/utils'
|
||||
import {
|
||||
ActiveTabIndicator,
|
||||
MoreIcon,
|
||||
MoreList,
|
||||
MoreListItem,
|
||||
MoreWrapper,
|
||||
TabContainer,
|
||||
TabItemLabel,
|
||||
TabItemWrapper,
|
||||
TabListHeader,
|
||||
TabListWrapper,
|
||||
TabView,
|
||||
} from './tab.styles'
|
||||
|
||||
export interface TabItem<T extends string = string> {
|
||||
label: ReactNode
|
||||
id: T
|
||||
children?: ReactNode
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
export interface TabProps<T extends string = string> {
|
||||
defaultActiveId?: T
|
||||
activeId?: T
|
||||
items: TabItem<T>[]
|
||||
style?: CSSProperties
|
||||
onChange?: (activeTabId: T) => void
|
||||
}
|
||||
|
||||
export function Tab_<T extends string = string>(props: TabProps<T>) {
|
||||
const {
|
||||
defaultActiveId,
|
||||
activeId: activeIdFromProp,
|
||||
items = [],
|
||||
onChange: onChangeFromProp,
|
||||
} = props
|
||||
|
||||
const DEFAULT_ACTIVE_ID = items[0].id
|
||||
const TAB_ID_ATTR = 'data-tab-id'
|
||||
const [activeIdFromPrivate, setActiveIdFromPrivate] = useState<T>()
|
||||
const [showMore, setShowMore] = useState(false)
|
||||
const [moreList, setMoreList] = useState<TabItem<T>[]>([])
|
||||
const [moreListVisible, setMoreListVisible] = useState(false)
|
||||
const [tabRef, tabSize] = useElementSizeRealTime<HTMLDivElement>()
|
||||
|
||||
// motion
|
||||
const indicatorWidth = useMotionValue(0)
|
||||
const indicatorLeft = useMotionValue(0)
|
||||
|
||||
const activeId = useMemo(() => {
|
||||
return activeIdFromProp ?? activeIdFromPrivate ?? defaultActiveId ?? DEFAULT_ACTIVE_ID
|
||||
}, [activeIdFromProp, activeIdFromPrivate, defaultActiveId])
|
||||
|
||||
const setActiveId = useCallback((id: T) => {
|
||||
onChangeFromProp ? onChangeFromProp(id) : setActiveIdFromPrivate(id)
|
||||
}, [onChangeFromProp])
|
||||
|
||||
const tabIdTabItemMap = useMemo(() => {
|
||||
const map = {} as Record<T, TabItem<T>>
|
||||
|
||||
items.forEach((item) => {
|
||||
map[item.id] = item
|
||||
})
|
||||
return map
|
||||
}, [items])
|
||||
|
||||
const getTabItemById = useCallback((id: T): TabItem<T> | undefined => {
|
||||
return tabIdTabItemMap[id]
|
||||
}, [tabIdTabItemMap])
|
||||
|
||||
const getLabelDoms = useCallback(() => {
|
||||
return Array.from(tabRef.current?.querySelectorAll(`[${TAB_ID_ATTR}]`) || []) as HTMLElement[]
|
||||
}, [tabRef.current, items])
|
||||
|
||||
const getActiveLabelDom = useCallback(() => {
|
||||
return tabRef.current?.querySelector<HTMLElement>(`[${TAB_ID_ATTR}="${activeId}"]`) ?? null
|
||||
}, [tabRef.current, activeId])
|
||||
|
||||
const handleTabItemClick = useCallback((item: TabItem<T>) => {
|
||||
setActiveId(item.id)
|
||||
setMoreListVisible(false)
|
||||
}, [setActiveId, setMoreListVisible])
|
||||
|
||||
const calcTabChildrenWidth = useCallback(() => {
|
||||
const labelDoms = getLabelDoms()
|
||||
|
||||
if (!labelDoms.length)
|
||||
return 0
|
||||
|
||||
const tabsWidth = labelDoms.reduce((acc, cur) => {
|
||||
return acc + cur.offsetWidth
|
||||
}, 0)
|
||||
|
||||
return tabsWidth
|
||||
}, [getLabelDoms])
|
||||
|
||||
useEffect(() => {
|
||||
const activeLabelDom = getActiveLabelDom()
|
||||
|
||||
if (!activeLabelDom)
|
||||
return
|
||||
|
||||
activeLabelDom.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'center',
|
||||
})
|
||||
}, [getActiveLabelDom, tabSize.width])
|
||||
|
||||
const updateMoreList = useCallback(() => {
|
||||
const labelDoms = getLabelDoms()
|
||||
const _moreList: TabItem<T>[] = []
|
||||
|
||||
Array.from(labelDoms).forEach((item) => {
|
||||
const tabId = item.getAttribute(TAB_ID_ATTR) as T
|
||||
const tabItem = getTabItemById(tabId)
|
||||
const isVisible = isElementVisible(item, item.parentElement!, 0.6)
|
||||
|
||||
if (!tabItem)
|
||||
return
|
||||
|
||||
_moreList.push({
|
||||
...tabItem,
|
||||
visible: isVisible,
|
||||
})
|
||||
})
|
||||
|
||||
setMoreList(_moreList)
|
||||
}, [getLabelDoms, setMoreList, getTabItemById])
|
||||
|
||||
const debounceUpdateMoreList = useDebounceFn(updateMoreList)
|
||||
|
||||
const updateIndicatorPosition = useCallback(() => {
|
||||
const activeLabelDom = getActiveLabelDom()
|
||||
|
||||
if (!activeLabelDom)
|
||||
return
|
||||
|
||||
const { offsetLeft, offsetWidth } = activeLabelDom
|
||||
|
||||
indicatorWidth.set(offsetWidth)
|
||||
indicatorLeft.set(offsetLeft)
|
||||
debounceUpdateMoreList()
|
||||
}, [getActiveLabelDom, debounceUpdateMoreList])
|
||||
|
||||
useEffect(() => {
|
||||
updateIndicatorPosition()
|
||||
}, [updateIndicatorPosition, tabSize.width])
|
||||
|
||||
useEffect(() => {
|
||||
const tabsTotalWidth = calcTabChildrenWidth()
|
||||
setShowMore(tabsTotalWidth > tabSize.width)
|
||||
updateMoreList()
|
||||
}, [calcTabChildrenWidth, setShowMore, updateMoreList, tabSize.width])
|
||||
|
||||
return (
|
||||
<TabContainer>
|
||||
<TabListHeader data-show-more={showMore}>
|
||||
<TabListWrapper ref={tabRef} data-show-more={showMore}>
|
||||
{items.map(item => (
|
||||
<div {...{
|
||||
key: item.id,
|
||||
[TAB_ID_ATTR]: item.id,
|
||||
}}>
|
||||
<TabItemWrapper onClick={() => handleTabItemClick(item)}>
|
||||
<TabItemLabel
|
||||
className={activeId === item.id ? 'tab-item-active' : ''}
|
||||
tabIndex={activeId === item.id ? 0 : -1}
|
||||
>
|
||||
{item.label}
|
||||
</TabItemLabel>
|
||||
</TabItemWrapper>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<ActiveTabIndicator
|
||||
style={{
|
||||
width: indicatorWidth,
|
||||
x: indicatorLeft,
|
||||
}}
|
||||
/>
|
||||
</TabListWrapper>
|
||||
|
||||
{showMore && (
|
||||
<MoreWrapper>
|
||||
<MoreIcon onClick={() => setMoreListVisible(!moreListVisible)}>
|
||||
<Icon className="codicon-more" />
|
||||
</MoreIcon>
|
||||
{moreListVisible && (
|
||||
<MoreList>
|
||||
{moreList.map(item => (
|
||||
!item.visible && <MoreListItem key={item.id} onClick={() => handleTabItemClick(item)}>
|
||||
{item.label}
|
||||
</MoreListItem>
|
||||
))}
|
||||
</MoreList>
|
||||
)}
|
||||
</MoreWrapper>
|
||||
)}
|
||||
</TabListHeader>
|
||||
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<TabView key={item.id} style={{
|
||||
display: activeId === item.id ? 'flex' : 'none',
|
||||
}}>
|
||||
{item.children}
|
||||
</TabView>
|
||||
)
|
||||
})}
|
||||
|
||||
</TabContainer>
|
||||
)
|
||||
}
|
||||
|
||||
Tab_.displayName = 'Tab'
|
||||
|
||||
export const Tab = memo(Tab_) as typeof Tab_
|
||||
@@ -1,3 +1,4 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const TabContainer = styled.div`
|
||||
@@ -6,20 +7,25 @@ export const TabContainer = styled.div`
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
export const TabListHeader = styled.div<{ $showMore: boolean }>`
|
||||
export const TabListHeader = styled.div`
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
padding: calc(var(--border-width) * 3px) 1rem;
|
||||
|
||||
${({ $showMore }) => $showMore ? 'padding-right: calc(var(--type-ramp-minus1-font-size) * 2 + 1rem)' : ''}
|
||||
&[data-show-more=true] {
|
||||
padding-right: calc(var(--type-ramp-minus1-font-size) * 2 + 1rem);
|
||||
}
|
||||
`
|
||||
|
||||
export const TabListWrapper = styled.div<{ $showMore: boolean }>`
|
||||
export const TabListWrapper = styled.div`
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
justify-content: space-evenly;
|
||||
|
||||
justify-content: ${({ $showMore }) => $showMore ? 'flex-start' : 'space-evenly'};
|
||||
&[data-show-more=true] {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* hide scroll bar style */
|
||||
&::-webkit-scrollbar {
|
||||
@@ -34,7 +40,7 @@ export const TabItemWrapper = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
export const TabItem = styled.div`
|
||||
export const TabItemLabel = styled.div`
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
padding: 0 var(--type-ramp-base-font-size);
|
||||
@@ -45,23 +51,17 @@ export const TabItem = styled.div`
|
||||
font-size: var(--type-ramp-base-font-size);
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
&.tab-item-active {
|
||||
color: var(--panel-tab-active-foreground);
|
||||
}
|
||||
`
|
||||
|
||||
export const ActivedTabIndicator = styled.div<{
|
||||
width: number
|
||||
left: number
|
||||
}>`
|
||||
width: ${props => props.width}px;
|
||||
export const ActiveTabIndicator = styled(motion.div)`
|
||||
height: 1px;
|
||||
background-color: var(--panel-tab-active-foreground);
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
transform: translateX(${props => props.left}px);
|
||||
transition: transform 0.2s ease-in-out;
|
||||
`
|
||||
|
||||
@@ -88,7 +88,8 @@ export const MoreList = styled.div`
|
||||
padding-bottom: 0;
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 33px;
|
||||
/* top: 33px; */
|
||||
top: calc(var(--design-unit) * 7px + var(--border-width) * 3px);
|
||||
right: 2px;
|
||||
background-color: var(--panel-view-background);
|
||||
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, .08), 0 3px 6px -4px rgba(0, 0, 0, .12), 0 9px 28px 8px rgba(0, 0, 0, .05);
|
||||
@@ -52,3 +52,36 @@ export function countTokenQuick(text: string) {
|
||||
export function isDomHidden(el: HTMLElement) {
|
||||
return el.offsetParent === null
|
||||
}
|
||||
|
||||
export function isElementVisible<T extends HTMLElement = HTMLElement, P extends HTMLElement = HTMLElement>(
|
||||
element: T,
|
||||
parentElement: P,
|
||||
intersectionRatio = 1,
|
||||
): boolean {
|
||||
// Get the bounding information of the element and parent element
|
||||
const rect = element.getBoundingClientRect()
|
||||
const parentRect = parentElement.getBoundingClientRect()
|
||||
|
||||
// Check if the element is within the visible area of the parent element
|
||||
const isVisibleHorizontally = rect.left >= parentRect.left && rect.right <= parentRect.right
|
||||
const isVisibleVertically = rect.top >= parentRect.top && rect.bottom <= parentRect.bottom
|
||||
|
||||
// If the element is fully within the visible area of the parent element, return true directly
|
||||
if (isVisibleHorizontally && isVisibleVertically)
|
||||
return true
|
||||
|
||||
// If an intersection ratio is specified, calculate the visible percentage of the element in the parent element
|
||||
if (intersectionRatio > 0 && intersectionRatio <= 1) {
|
||||
const visibleWidth = Math.min(rect.right, parentRect.right) - Math.max(rect.left, parentRect.left)
|
||||
const visibleHeight = Math.min(rect.bottom, parentRect.bottom) - Math.max(rect.top, parentRect.top)
|
||||
const visibleArea = visibleWidth * visibleHeight
|
||||
const elementArea = rect.width * rect.height
|
||||
const visiblePercentage = visibleArea / elementArea
|
||||
|
||||
// Check if the visible percentage meets the requirement
|
||||
return visiblePercentage >= intersectionRatio
|
||||
}
|
||||
|
||||
// By default, if at least a part of the element is visible within the parent element, return true
|
||||
return isVisibleHorizontally || isVisibleVertically
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import type { EditorProps as MonacoEditorProps } from '@monaco-editor/react'
|
||||
import MonacoEditor from '@monaco-editor/react'
|
||||
import type { Monaco, EditorProps as MonacoEditorProps } from '@monaco-editor/react'
|
||||
import MonacoEditor, { loader } from '@monaco-editor/react'
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { memo, useCallback, useMemo, useRef } from 'react'
|
||||
import { isDarkTheme } from '../../../../styles/themes'
|
||||
import { useGlobalStore } from '../../../../store/zustand/global'
|
||||
import { BASE_URL } from '../../../../helpers/constant'
|
||||
import type { MonacoEditorInstance } from '../../../../types/monaco-editor'
|
||||
|
||||
loader.config({
|
||||
paths: {
|
||||
vs: `${BASE_URL}/monaco-editor-vs`,
|
||||
},
|
||||
})
|
||||
|
||||
export interface EditorProps extends MonacoEditorProps {
|
||||
filePath?: string
|
||||
@@ -11,30 +19,41 @@ export interface EditorProps extends MonacoEditorProps {
|
||||
|
||||
export const Editor: FC<EditorProps> = memo((props) => {
|
||||
const { filePath, ...otherProps } = props
|
||||
|
||||
const monacoRef = useRef<Monaco>()
|
||||
const fileExt = filePath?.split('.')?.pop()
|
||||
const extLanguageMap: Record<string, string> = {
|
||||
js: 'javascript',
|
||||
cjs: 'javascript',
|
||||
mjs: 'javascript',
|
||||
ts: 'typescript',
|
||||
mts: 'typescript',
|
||||
jsx: 'javascriptreact',
|
||||
tsx: 'typescriptreact',
|
||||
py: 'python',
|
||||
md: 'markdown',
|
||||
html: 'html',
|
||||
css: 'css',
|
||||
json: 'json',
|
||||
yaml: 'yaml',
|
||||
yml: 'yaml',
|
||||
}
|
||||
const language = extLanguageMap[fileExt ?? ''] || otherProps?.defaultLanguage || 'javascript'
|
||||
const DEFAULT_LANGUAGE = 'markdown'
|
||||
|
||||
const monacoLanguages = useMemo(() => {
|
||||
const languages = monacoRef.current?.languages.getLanguages() || []
|
||||
return languages
|
||||
}, [monacoRef.current])
|
||||
|
||||
// current ext lang
|
||||
const currentExtLanguage = useMemo(() => {
|
||||
const extLanguage = monacoLanguages.find(lang => lang.extensions?.includes(`.${fileExt}`))
|
||||
return extLanguage?.id
|
||||
}, [monacoLanguages, fileExt])
|
||||
|
||||
const defaultLanguage = otherProps?.defaultLanguage || DEFAULT_LANGUAGE
|
||||
const language = currentExtLanguage || defaultLanguage
|
||||
|
||||
const {
|
||||
themeName,
|
||||
} = useGlobalStore()
|
||||
const isDark = isDarkTheme(themeName)
|
||||
|
||||
const handleEditorWillMount = useCallback((monaco: Monaco) => {
|
||||
// here is the monaco instance
|
||||
// do something before editor is mounted
|
||||
monacoRef.current = monaco
|
||||
}, [])
|
||||
|
||||
const handleEditorDidMount = useCallback((editor: MonacoEditorInstance, monaco: Monaco) => {
|
||||
// here is another way to get monaco instance
|
||||
// you can also store it in `useRef` for further usage
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<MonacoEditor
|
||||
height="100%"
|
||||
@@ -42,6 +61,9 @@ export const Editor: FC<EditorProps> = memo((props) => {
|
||||
language={language}
|
||||
theme={isDark ? 'vs-dark' : 'light'}
|
||||
{...otherProps}
|
||||
defaultLanguage={defaultLanguage}
|
||||
beforeMount={handleEditorWillMount}
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -12,12 +12,12 @@ import { useGlobalStore } from '../../store/zustand/global'
|
||||
import { getGlobalConfig } from '../../helpers/global-config'
|
||||
import { ErrorView } from '../../components/error-view'
|
||||
import { DragResizeView } from '../../components/drag-resize-view'
|
||||
|
||||
import { PanelTab } from '../../components/panel-tab'
|
||||
import { fetchProjectInfo } from '../../networks/config'
|
||||
import { useEmitBind } from '../../hooks/use-emit-bind.hook'
|
||||
import { useSize } from '../../hooks/use-size.hook'
|
||||
import { useGetCommonFilesTree } from '../../hooks/use-get-common-files-tree.hook'
|
||||
import type { TabItem } from '../../components/tab'
|
||||
import { ContentWrapper } from './chat.styles'
|
||||
import { ChatSidebar } from './components/chat-sidebar'
|
||||
import { ChatPanel } from './components/chat-panel'
|
||||
@@ -139,34 +139,25 @@ const Chat: FC = memo(() => {
|
||||
|
||||
const renderChat = () => {
|
||||
if (isMobile) {
|
||||
const tabIdViewMap: Partial<Record<TabId, { title: React.ReactNode; view: React.ReactNode }>> = {
|
||||
[TabId.Presets]: {
|
||||
title: t('chat_page.tab_presets'),
|
||||
view: renderSidebar(),
|
||||
const tabIdViewMap: TabItem<TabId>[] = [
|
||||
{
|
||||
id: TabId.Presets,
|
||||
label: t('chat_page.tab_presets'),
|
||||
children: renderSidebar(),
|
||||
},
|
||||
[TabId.Chat]: {
|
||||
title: t('chat_page.tab_chat'),
|
||||
view: renderChatPanel(),
|
||||
{
|
||||
id: TabId.Chat,
|
||||
label: t('chat_page.tab_chat'),
|
||||
children: renderChatPanel(),
|
||||
},
|
||||
[TabId.Files]: {
|
||||
title: t('chat_page.tab_files'),
|
||||
view: renderFileTree(),
|
||||
{
|
||||
id: TabId.Files,
|
||||
label: t('chat_page.tab_files'),
|
||||
children: renderFileTree(),
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
return <PanelTab
|
||||
items={
|
||||
Object.keys(tabIdViewMap).map((tabId) => {
|
||||
const { title } = tabIdViewMap[tabId as TabId]!
|
||||
return {
|
||||
label: title,
|
||||
key: tabId,
|
||||
children: tabIdViewMap[tabId as TabId]!.view,
|
||||
}
|
||||
})
|
||||
}
|
||||
defaultActiveIndex={0}
|
||||
/>
|
||||
return <PanelTab items={tabIdViewMap} activeId={tabActiveId} onChange={setTabActiveId} />
|
||||
}
|
||||
|
||||
return <FlexRow style={{ height: '100%', overflow: 'hidden' }}>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'
|
||||
import type { editor } from 'monaco-editor'
|
||||
|
||||
export type Monaco = typeof monaco
|
||||
|
||||
export type MonacoEditorInstance = editor.IStandaloneCodeEditor
|
||||
|
||||
export type MonacoLanguage = monaco.languages.ILanguageExtensionPoint
|
||||
@@ -1,42 +1,48 @@
|
||||
import path from 'node:path'
|
||||
import type { UserConfig } from 'vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import React from '@vitejs/plugin-react'
|
||||
import Svgr from 'vite-plugin-svgr'
|
||||
import { EnvConfig } from '@nicepkg/gpt-runner-shared/common'
|
||||
import { PathUtils } from '@nicepkg/gpt-runner-shared/node'
|
||||
import { alias } from './../../../alias'
|
||||
import { copyMonacoEditor } from './scripts/copy-monaco-editor'
|
||||
|
||||
const dirname = PathUtils.getCurrentDirName(import.meta.url, () => __dirname)
|
||||
|
||||
const resolvePath = (...paths: string[]) => path.resolve(dirname, ...paths)
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
root: resolvePath('./'),
|
||||
publicDir: resolvePath('./public'),
|
||||
optimizeDeps: {
|
||||
include: ['@nicepkg/gpt-runner-shared'],
|
||||
},
|
||||
plugins: [
|
||||
React(),
|
||||
Svgr(),
|
||||
],
|
||||
build: {
|
||||
outDir: resolvePath('../dist/browser'),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
...alias,
|
||||
export default defineConfig(async () => {
|
||||
await copyMonacoEditor()
|
||||
|
||||
return {
|
||||
root: resolvePath('./'),
|
||||
publicDir: resolvePath('./public'),
|
||||
optimizeDeps: {
|
||||
include: ['@nicepkg/gpt-runner-shared'],
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3006,
|
||||
host: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: EnvConfig.get('GPTR_BASE_SERVER_URL'),
|
||||
changeOrigin: true,
|
||||
plugins: [
|
||||
React(),
|
||||
Svgr(),
|
||||
],
|
||||
build: {
|
||||
outDir: resolvePath('../dist/browser'),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
...alias,
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3006,
|
||||
host: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: EnvConfig.get('GPTR_BASE_SERVER_URL'),
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies UserConfig
|
||||
})
|
||||
|
||||
@@ -97,12 +97,14 @@
|
||||
"eventemitter": "^0.3.3",
|
||||
"express": "^4.18.2",
|
||||
"framer-motion": "^10.12.18",
|
||||
"fs-extra": "^11.1.1",
|
||||
"global-agent": "^3.0.0",
|
||||
"i18next": "^23.2.6",
|
||||
"i18next-browser-languagedetector": "^7.1.0",
|
||||
"i18next-http-backend": "^2.2.1",
|
||||
"keyboardjs": "^2.7.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.39.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.10",
|
||||
@@ -123,4 +125,4 @@
|
||||
"vite-plugin-svgr": "^3.2.0",
|
||||
"zustand": "^4.3.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -357,6 +357,9 @@ importers:
|
||||
framer-motion:
|
||||
specifier: ^10.12.18
|
||||
version: 10.12.18(react-dom@18.2.0)(react@18.2.0)
|
||||
fs-extra:
|
||||
specifier: ^11.1.1
|
||||
version: 11.1.1
|
||||
global-agent:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
@@ -375,6 +378,9 @@ importers:
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
monaco-editor:
|
||||
specifier: ^0.39.0
|
||||
version: 0.39.0
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
|
||||
Reference in New Issue
Block a user