feat(gpt-runner-web): add init gpt file suppport when root path not have gpt files

This commit is contained in:
JinmingYang
2023-06-21 16:48:49 +08:00
parent 0992e0e51d
commit 5e92d3ea8a
21 changed files with 264 additions and 45 deletions

View File

@@ -180,5 +180,12 @@ ${content}
tips += fileTips
}
tips += `When you want to create/modify/delete a file or talk about a file, you should always return the full path of the file.
For example, if I provide you with a file path \`src/component/button.ts\`, you should return \`src/component/button.ts\` instead of \`button.ts\ when you talk about it.
Return full path is very important !!!
`
return tips
}

View File

@@ -31,6 +31,13 @@ export interface GetGptFilesTreeResData {
filesInfoTree: GptFileInfoTree
}
export interface InitGptFilesReqParams {
rootPath: string
gptFilesNames: string[]
}
export type InitGptFilesResData = null
export type GetProjectConfigReqParams = null
export interface GetProjectConfigResData {
gptRunnerVersion: string

View File

@@ -1,5 +1,5 @@
import { z } from 'zod'
import type { ChatStreamReqParams, GetCommonFilesReqParams, GetGptFilesReqParams, GetUserConfigReqParams, OpenEditorReqParams, StorageClearReqParams, StorageGetItemReqParams, StorageRemoveItemReqParams, StorageSetItemReqParams } from '../types'
import type { ChatStreamReqParams, GetCommonFilesReqParams, GetGptFilesReqParams, GetUserConfigReqParams, InitGptFilesReqParams, OpenEditorReqParams, StorageClearReqParams, StorageGetItemReqParams, StorageRemoveItemReqParams, StorageSetItemReqParams } from '../types'
import { SingleChatMessageSchema, SingleFileConfigSchema } from './config.zod'
import { ServerStorageNameSchema } from './enum.zod'
@@ -16,6 +16,11 @@ export const GetGptFilesReqParamsSchema = z.object({
rootPath: z.string(),
}) satisfies z.ZodType<GetGptFilesReqParams>
export const InitGptFilesReqParamsSchema = z.object({
rootPath: z.string(),
gptFilesNames: z.array(z.string()),
}) satisfies z.ZodType<InitGptFilesReqParams>
export const GetUserConfigReqParamsSchema = z.object({
rootPath: z.string(),
}) satisfies z.ZodType<GetUserConfigReqParams>

View File

@@ -17,22 +17,27 @@ export function openInBrowser(props: OpenInBrowserProps) {
}
export interface GetPortProps {
defaultPort: number
defaultPort?: number
autoFreePort?: boolean
}
export async function getPort(props: GetPortProps): Promise<number> {
const { defaultPort, autoFreePort } = props
if (!autoFreePort)
return defaultPort
if (defaultPort) {
if (!autoFreePort)
return defaultPort
const canUseDefaultPort = await fp.isFreePort(defaultPort)
const canUseDefaultPort = await fp.isFreePort(defaultPort)
if (canUseDefaultPort)
return defaultPort
if (canUseDefaultPort)
return defaultPort
}
const [freePort] = await fp.findFreePorts(1)
const [freePort] = await fp.findFreePorts(1, {
startPort: 3001,
endPort: 9999,
})
return freePort
}

View File

@@ -29,14 +29,20 @@ export async function registerServer(
const { extensionUri } = ext
const serverUri = vscode.Uri.joinPath(extensionUri, './dist/web/start-server.cjs')
// always get a random free port
const finalPort = await getPort({
defaultPort: 3003,
autoFreePort: true,
})
state.serverPort = finalPort
serverProcess = child_process.spawn('node', [serverUri.fsPath, '--port', String(finalPort)], {
serverProcess = child_process.spawn('node', [
serverUri.fsPath,
'--port',
String(finalPort),
'--client-dist-path',
vscode.Uri.joinPath(extensionUri, './dist/web/browser').fsPath,
], {
env: {
...process.env,
NODE_OPTIONS: '--experimental-fetch',

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,18 +1,25 @@
import { styled } from 'styled-components'
// actual line weight is 8px, because we need to make it bigger to make it easier to grab
const lineWeight = '8px'
export const DragLine = styled.div<{ $dragLineColor: string; $dragLineActiveColor: string; $dragLineWidth: string }>`
position: absolute;
background: ${({ $dragLineColor }) => $dragLineColor};
/* z-index: 2; */
/* background: ${({ $dragLineColor }) => $dragLineColor}; */
border-color: ${({ $dragLineColor }) => $dragLineColor};
border-style: solid;
border-width: 0;
touch-action: none;
&:active {
background: ${({ $dragLineActiveColor }) => $dragLineActiveColor};
/* background: ${({ $dragLineActiveColor }) => $dragLineActiveColor}; */
border-color: ${({ $dragLineActiveColor }) => $dragLineActiveColor};
}
&[data-direction='left'] {
cursor: col-resize;
width: ${({ $dragLineWidth }) => $dragLineWidth};
width: ${lineWeight};
border-left-width: ${({ $dragLineWidth }) => $dragLineWidth};
left: 0;
top: 0;
bottom: 0;
@@ -20,7 +27,8 @@ export const DragLine = styled.div<{ $dragLineColor: string; $dragLineActiveColo
&[data-direction='right'] {
cursor: col-resize;
width: ${({ $dragLineWidth }) => $dragLineWidth};
width: ${lineWeight};
border-right-width: ${({ $dragLineWidth }) => $dragLineWidth};
right: 0;
top: 0;
bottom: 0;
@@ -28,7 +36,8 @@ export const DragLine = styled.div<{ $dragLineColor: string; $dragLineActiveColo
&[data-direction='top'] {
cursor: row-resize;
height: ${({ $dragLineWidth }) => $dragLineWidth};
height: ${lineWeight};
border-top-width: ${({ $dragLineWidth }) => $dragLineWidth};
top: 0;
left: 0;
right: 0;
@@ -36,7 +45,8 @@ export const DragLine = styled.div<{ $dragLineColor: string; $dragLineActiveColo
&[data-direction='bottom'] {
cursor: row-resize;
height: ${({ $dragLineWidth }) => $dragLineWidth};
height: ${lineWeight};
border-bottom-width: ${({ $dragLineWidth }) => $dragLineWidth};
bottom: 0;
left: 0;
right: 0;

View File

@@ -42,6 +42,7 @@ export async function fetchChatgptStream(
contextFilePaths,
rootPath,
} satisfies ChatStreamReqParams),
openWhenHidden: true,
onmessage: onMessage,
onerror: onError,
})

View File

@@ -1,4 +1,5 @@
import { type BaseResponse, type GetGptFilesReqParams, type GetGptFilesTreeResData, objectToQueryString } from '@nicepkg/gpt-runner-shared/common'
import { objectToQueryString } from '@nicepkg/gpt-runner-shared/common'
import type { BaseResponse, GetGptFilesReqParams, GetGptFilesTreeResData, InitGptFilesReqParams, InitGptFilesResData } from '@nicepkg/gpt-runner-shared/common'
import { getGlobalConfig } from '../helpers/global-config'
export async function fetchGptFilesTree(params: GetGptFilesReqParams): Promise<BaseResponse<GetGptFilesTreeResData>> {
@@ -13,3 +14,17 @@ export async function fetchGptFilesTree(params: GetGptFilesReqParams): Promise<B
const data = await res.json()
return data
}
export async function initGptFiles(params: InitGptFilesReqParams): Promise<BaseResponse<InitGptFilesResData>> {
const res = await fetch(`${getGlobalConfig().serverBaseUrl}/api/gpt-files/init-gpt-files?${objectToQueryString({
...params,
})}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
})
const data = await res.json()
return data
}

View File

@@ -31,9 +31,28 @@ export interface ChatPanelProps {
}
export const ChatPanel: FC<ChatPanelProps> = (props) => {
const { scrollDownRef, chatTreeView, fileTreeView, settingsView, chatId, onChatIdChange } = props
const { createChatAndActive, getGptFileTreeItemFromChatId } = useGlobalStore()
const { chatInstance, updateCurrentChatInstance, generateCurrentChatAnswer, regenerateCurrentLastChatAnswer, stopCurrentGeneratingChatAnswer } = useChatInstance({ chatId })
const {
scrollDownRef,
chatTreeView,
fileTreeView,
settingsView,
chatId,
onChatIdChange,
} = props
const {
createChatAndActive,
getGptFileTreeItemFromChatId,
} = useGlobalStore()
const {
chatInstance,
updateCurrentChatInstance,
generateCurrentChatAnswer,
regenerateCurrentLastChatAnswer,
stopCurrentGeneratingChatAnswer,
} = useChatInstance({ chatId })
const status = chatInstance?.status ?? ChatMessageStatus.Success
const [gptFileTreeItem, setGptFileTreeItem] = useState<GptFileTreeItem>()
const [chatPanelRef, { width: chatPanelWidth }] = useElementSizeRealTime<HTMLDivElement>()
@@ -334,8 +353,8 @@ export const ChatPanel: FC<ChatPanelProps> = (props) => {
{/* file tree */}
{fileTreeView && <PopoverMenu
isPopoverOpen={true}
onPopoverDisplayChange={() => { }}
// isPopoverOpen={true}
// onPopoverDisplayChange={() => { }}
childrenInMenuWhenOpen={false}
clickOutSideToClose={false}
menuStyle={{

View File

@@ -21,7 +21,17 @@ export type GptTreeItemOtherInfo = GptFileInfoTreeItem
export const ChatSidebar: FC<ChatSidebarProps> = (props) => {
const { rootPath } = props
const { activeChatId, sidebarTree, expandChatTreeItem, createChatAndActive, updateSidebarTreeItem, updateActiveChatId, updateUserConfigFromRemote, updateSidebarTreeFromRemote } = useGlobalStore()
const {
activeChatId,
sidebarTree,
expandChatTreeItem,
createChatAndActive,
updateSidebarTreeItem,
updateActiveChatId,
updateUserConfigFromRemote,
updateSidebarTreeFromRemote,
} = useGlobalStore()
const [isLoading, setIsLoading] = useState(false)
const { removeChatInstance } = useChatInstance({

View File

@@ -10,9 +10,12 @@ export const FileTreeItemRightWrapper = styled.div`
export const FileTreeSidebarUnderSearchWrapper = styled.div`
font-size: var(--type-ramp-base-font-size);
margin-top: 0.25rem;
color: var(--input-foreground);
padding: 0.5rem 0;
margin-bottom: 1rem;
& ::part(control) {
flex-shrink: 0;
}
`
export const FileTreeSidebarHighlight = styled(VSCodeBadge)`

View File

@@ -363,7 +363,7 @@ const FileTree: FC<FileTreeProps> = (props: FileTreeProps) => {
}
return <FileTreeSidebarUnderSearchWrapper>
<FileTreeSidebarHighlight style={{ paddingLeft: 0 }}>{checkedFilePaths.length}</FileTreeSidebarHighlight>
<FileTreeSidebarHighlight style={{ marginLeft: 0 }}>{checkedFilePaths.length}</FileTreeSidebarHighlight>
Files.
<FileTreeSidebarHighlight>{formatNumWithK(totalTokenNum)}</FileTreeSidebarHighlight>
Tokens.

View File

@@ -0,0 +1,60 @@
import { type FC, useState } from 'react'
import { useMutation } from '@tanstack/react-query'
import { type MaybePromise, sleep } from '@nicepkg/gpt-runner-shared/common'
import { IconButton } from '../../../../components/icon-button'
import { initGptFiles } from '../../../../networks/gpt-files'
import { getGlobalConfig } from '../../../../helpers/global-config'
import { LoadingView } from '../../../../components/loading-view'
import { StyledVSCodeTag, Title, Wrapper } from './init-gpt-files.styles'
export interface InitGptFilesProps {
rootPath: string
onCreated?: () => MaybePromise<void>
}
export const InitGptFiles: FC<InitGptFilesProps> = (props) => {
const { rootPath, onCreated } = props
const [isLoading, setIsLoading] = useState(false)
const { mutate: runInitGptFiles } = useMutation({
mutationKey: ['initGptFiles', rootPath],
mutationFn: () => initGptFiles({
rootPath: getGlobalConfig().rootPath,
gptFilesNames: ['copilot'],
}),
})
const handleCreate = async () => {
setIsLoading(true)
try {
await runInitGptFiles()
await sleep(1000)
await onCreated?.()
}
finally {
setIsLoading(false)
}
}
return <Wrapper>
<Title>
There is no
<StyledVSCodeTag>xxx.gpt.md</StyledVSCodeTag>
file in the current directory.
</Title>
<Title>
Do you need to create a
<StyledVSCodeTag>./gpt-presets/copilot.gpt.md</StyledVSCodeTag>
file?
</Title>
<IconButton
text='Yes, Create'
hoverShowText={false}
iconClassName='codicon-new-file'
onClick={handleCreate}></IconButton>
{isLoading && <LoadingView absolute></LoadingView>}
</Wrapper>
}
InitGptFiles.displayName = 'InitGptFiles'

View File

@@ -0,0 +1,30 @@
import { VSCodeTag } from '@vscode/webview-ui-toolkit/react'
import { styled } from 'styled-components'
export const Wrapper = styled.div`
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
padding: 1rem;
align-items: center;
justify-content: center;
`
export const Title = styled.div`
display: flex;
align-items: center;
font-size: 1.2rem;
margin-bottom: 1rem;
`
export const StyledVSCodeTag = styled(VSCodeTag)`
margin: 0 0.5rem;
&::part(control) {
font-size: 1.1rem;
text-transform: lowercase;
}
`

View File

@@ -18,6 +18,7 @@ import { ChatSidebar } from './components/chat-sidebar'
import { ChatPanel } from './components/chat-panel'
import FileTree from './components/file-tree'
import { Settings } from './components/settings'
import { InitGptFiles } from './components/init-gpt-files'
enum TabId {
Explore = 'explore',
@@ -28,7 +29,7 @@ enum TabId {
const Chat: FC = () => {
const isMobile = useIsMobile()
const { width: windowWidth, height: windowHeight } = useWindowSize()
const { activeChatId, updateActiveChatId } = useGlobalStore()
const { activeChatId, sidebarTree, updateActiveChatId, updateSidebarTreeFromRemote } = useGlobalStore()
const { chatInstance } = useChatInstance({ chatId: activeChatId })
const [scrollDownRef, scrollDown, getScrollBottom] = useScrollDown()
const [tabActiveId, setTabActiveId] = useState(TabId.Explore)
@@ -37,6 +38,7 @@ const Chat: FC = () => {
queryKey: ['fetchProjectInfo'],
queryFn: () => fetchProjectInfo(),
})
const rootPath = getGlobalConfig().rootPath
// when active chat id change, change tab active id
useEffect(() => {
@@ -63,25 +65,25 @@ const Chat: FC = () => {
}, [scrollDownRef.current])
const renderSidebar = useCallback(() => {
if (!getGlobalConfig().rootPath)
if (!rootPath)
return null
return <SidebarWrapper className='sidebar-wrapper'>
<ChatSidebar rootPath={getGlobalConfig().rootPath}></ChatSidebar>
<ChatSidebar rootPath={rootPath}></ChatSidebar>
</SidebarWrapper>
}, [])
const renderFileTree = useCallback(() => {
if (!getGlobalConfig().rootPath)
if (!rootPath)
return null
return <SidebarWrapper className='sidebar-wrapper'>
<FileTree rootPath={getGlobalConfig().rootPath}></FileTree>
<FileTree rootPath={rootPath}></FileTree>
</SidebarWrapper>
}, [])
const renderSettings = useCallback((showSingleFileConfig = false) => {
if (!getGlobalConfig().rootPath)
if (!rootPath)
return null
return <SidebarWrapper className='sidebar-wrapper'>
@@ -108,12 +110,19 @@ const Chat: FC = () => {
updateActiveChatId,
])
if (!getGlobalConfig().rootPath)
if (!rootPath)
return <ErrorView text="Please provide the root path!"></ErrorView>
if (fetchProjectInfoRes?.data?.nodeVersionValidMessage)
return <ErrorView text={fetchProjectInfoRes?.data?.nodeVersionValidMessage}></ErrorView>
if (!sidebarTree?.length) {
return <InitGptFiles
rootPath={rootPath}
onCreated={() => updateSidebarTreeFromRemote(rootPath)}
></InitGptFiles>
}
if (isMobile) {
const viewStyle: CSSProperties = {
height: '100%',

View File

@@ -291,6 +291,7 @@ export const MarkdownStyle = createGlobalStyle`
padding: 0 0.25rem;
white-space: nowrap;
background: var(--button-primary-background);
color: var(--button-primary-foreground);
border-radius: 0.25rem;
margin: 0 0.25rem;
}

View File

@@ -13,15 +13,16 @@ const dirname = PathUtils.getCurrentDirName(import.meta.url, () => __dirname)
const resolvePath = (...paths: string[]) => path.resolve(dirname, ...paths)
export const clientDistPath = resolvePath('../dist/browser')
export const DEFAULT_CLIENT_DIST_PATH = resolvePath('../dist/browser')
export interface StartServerProps {
port?: number
autoFreePort?: boolean
clientDistPath?: string
}
export async function startServer(props: StartServerProps): Promise<Express> {
const { port = 3003, autoFreePort } = props
const { port = 3003, autoFreePort, clientDistPath = DEFAULT_CLIENT_DIST_PATH } = props
const finalPort = await getPort({
defaultPort: port,

View File

@@ -1,7 +1,8 @@
import { getGptFilesInfo, loadUserConfig } from '@nicepkg/gpt-runner-core'
import type { GptInitFileName } from '@nicepkg/gpt-runner-core'
import { getGptFilesInfo, initGptFiles, loadUserConfig } from '@nicepkg/gpt-runner-core'
import { PathUtils, sendFailResponse, sendSuccessResponse, verifyParamsByZod } from '@nicepkg/gpt-runner-shared/node'
import type { GetGptFilesReqParams, GetGptFilesTreeResData } from '@nicepkg/gpt-runner-shared/common'
import { Debug, GetGptFilesReqParamsSchema, resetUserConfigUnsafeKey } from '@nicepkg/gpt-runner-shared/common'
import type { GetGptFilesReqParams, GetGptFilesTreeResData, InitGptFilesReqParams, InitGptFilesResData } from '@nicepkg/gpt-runner-shared/common'
import { Debug, GetGptFilesReqParamsSchema, InitGptFilesReqParamsSchema, resetUserConfigUnsafeKey } from '@nicepkg/gpt-runner-shared/common'
import type { ControllerConfig } from '../types'
const debug = new Debug('gpt-files.controller')
@@ -45,5 +46,34 @@ export const gptFilesControllers: ControllerConfig = {
})
},
},
{
url: '/init-gpt-files',
method: 'post',
handler: async (req, res) => {
const body = req.body as InitGptFilesReqParams
verifyParamsByZod(body, InitGptFilesReqParamsSchema)
const { rootPath, gptFilesNames } = body
const finalPath = PathUtils.resolve(rootPath)
if (!PathUtils.isDirectory(finalPath)) {
sendFailResponse(res, {
message: 'rootPath is not a valid directory',
})
return
}
await initGptFiles({
rootPath: finalPath,
gptFilesNames: gptFilesNames as GptInitFileName[],
})
sendSuccessResponse(res, {
data: null satisfies InitGptFilesResData,
})
},
},
],
}

View File

@@ -1,18 +1,19 @@
import { program } from 'commander'
import pkg from '../package.json'
import type { StartServerProps } from './index'
import { startServer } from './index'
program.option('-p, --port <port>', 'Port number', parseInt)
program.option('--auto-free-port', 'Automatically find a free port')
program.option('--auto-open', 'Automatically open the browser')
program.option('--client-dist-path <clientDistPath>', 'Client dist path')
program.option('-v, --version', 'Version number')
program.parse(process.argv)
interface ProgramOpts {
port?: StartServerProps['port']
port?: number
autoFreePort?: boolean
autoOpen?: boolean
clientDistPath?: string
version?: boolean
}

View File

@@ -11,10 +11,9 @@
#01 你充当前端开发专家。
#02 user 将提供一些关于前端代码问题的具体信息,而你的工作就是想出为 user 解决问题的策略。这可能包括建议代码、代码逻辑思路策略。
#03 你如果想要增删改文件,应该需要告诉 user 文件的完整路径,以及你想要增删改的内容。
#04 你的代码应该遵循 SOLID and KISS and DRY principles
#05 你应该一步步思考完再作答
#06 你应该非常细心,不要遗漏 user 提问的任何信息或需求,回答要完整
#03 你的代码应该遵循 SOLID and KISS and DRY principles
#04 你应该一步步思考完再作答
#05 你应该非常细心,不要遗漏 user 提问的任何信息或需求,回答要完整
# User Prompt