feat(gpt-runner-web): optimize tab and monaco-editor

This commit is contained in:
JinmingYang
2023-07-05 02:33:10 +08:00
parent acb2aec0ea
commit f556a5d3fc
15 changed files with 415 additions and 312 deletions

3
.gitignore vendored
View File

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

View 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}`)
}
}

View File

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

View File

@@ -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) => {

View File

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

View File

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

View 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_

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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