feat(gpt-runner-web): add third-party api providers and notification

This commit is contained in:
JinmingYang
2023-07-25 00:29:36 +08:00
parent fcaf7af91e
commit 1ebc996a7d
54 changed files with 2571 additions and 647 deletions

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"jsynowiec.vscode-insertdatestring",
"EditorConfig.EditorConfig",
"dbaeumer.vscode-eslint"
]
}

View File

@@ -34,31 +34,31 @@
"@types/node": "^18.16.19",
"@types/prettier": "^2.7.3",
"@types/react": "^18.2.15",
"@vitejs/plugin-legacy": "^4.1.0",
"@vitejs/plugin-legacy": "^4.1.1",
"@vitest/ui": "^0.33.0",
"bumpp": "^9.1.1",
"eslint": "8.44.0",
"eslint": "8.45.0",
"esno": "^0.17.0",
"execa": "^7.1.1",
"fast-glob": "^3.3.0",
"fast-glob": "^3.3.1",
"fs-extra": "^11.1.1",
"jiti": "^1.19.1",
"jsdom": "^22.1.0",
"lint-staged": "^13.2.3",
"msw": "1.2.2",
"pnpm": "8.6.9",
"msw": "1.2.3",
"pnpm": "8.6.10",
"prettier": "^3.0.0",
"react": "^18.2.0",
"rollup": "^3.26.3",
"semver": "^7.5.4",
"simple-git-hooks": "^2.8.1",
"simple-git-hooks": "^2.9.0",
"taze": "^0.11.2",
"terser": "^5.19.1",
"terser": "^5.19.2",
"tsup": "^7.1.0",
"typescript": "^5.1.6",
"unbuild": "^0.8.11",
"unplugin-auto-import": "^0.16.6",
"vite": "^4.4.4",
"vite": "^4.4.7",
"vite-plugin-inspect": "^0.7.33",
"vite-plugin-pages": "^0.31.0",
"vitest": "^0.33.0"

View File

@@ -48,7 +48,7 @@
"dependencies": {
"@nicepkg/gpt-runner-shared": "workspace:*",
"ignore": "^5.2.4",
"langchain": "^0.0.112",
"langchain": "^0.0.116",
"unconfig": "^0.3.9",
"zod": "^3.21.4"
}

View File

@@ -93,10 +93,10 @@
"zod-to-json-schema": "*"
},
"dependencies": {
"@kvs/node-localstorage": "^2.1.3",
"@kvs/storage": "^2.1.3",
"@kvs/node-localstorage": "^2.1.5",
"@kvs/storage": "^2.1.4",
"axios": "1.3.4",
"cachedir": "^2.3.0",
"cachedir": "^2.4.0",
"debug": "^4.3.4",
"find-free-ports": "^3.1.1",
"http-proxy-agent": "^7.0.0",

View File

@@ -1,5 +1,3 @@
import { tryStringifyJson } from './common'
export function isNumber<T extends number>(value: T | unknown): value is number {
return Object.prototype.toString.call(value) === '[object Number]'
}
@@ -71,11 +69,46 @@ export function isShallowEqual<T>(
return true
}
export function isShallowDeepEqual(
objA: any,
objB: any,
): boolean {
return isShallowEqual(objA, objB, (a, b) => {
return tryStringifyJson(a, true) === tryStringifyJson(b, true)
})
// export function isShallowDeepEqual(
// objA: any,
// objB: any,
// ): boolean {
// return isShallowEqual(objA, objB, (a, b) => {
// return tryStringifyJson(a, true) === tryStringifyJson(b, true)
// })
// }
// export function isDeepEqual<T>(objA: any, objB: any): boolean {
// const compare = (a: any, b: any) => {
// if (typeof a === 'object' && a !== null && typeof b === 'object' && b !== null) {
// // For objects, perform a deep comparison
// return isDeepEqual(a, b)
// }
// // For primitives, perform a shallow comparison
// return Object.is(a, b)
// }
// return isShallowEqual(objA, objB, compare)
// }
export function isDeepEqual(objA: any, objB: any, maxDepth = 20, visited: any[] = [], depth: number = 0): boolean {
if (depth > maxDepth) {
// Limit the maximum recursion depth to prevent "Maximum call stack size exceeded" error
return true
}
if (visited.includes(objA) || visited.includes(objB))
return true
const compare = (a: any, b: any) => {
if (typeof a === 'object' && a !== null && typeof b === 'object' && b !== null) {
// For objects, perform a deep comparison
return isDeepEqual(a, b, maxDepth, [...visited, a, b], depth + 1)
}
// For primitives, perform a shallow comparison
return Object.is(a, b)
}
return isShallowEqual(objA, objB, compare)
}

View File

@@ -0,0 +1,74 @@
import type { GetModelConfigType } from './config'
import type { ChatModelType, LocaleLang, VendorTag } from './enum'
export type MarkdownString = string
export interface BaseConfig {
/**
* create time like 2023-04-23 12:34:56, for diff update
*/
createAt: string
}
export interface ChangeLogConfig {
/**
* like 2023-04-23 12:34:56
*/
releaseDate: string
version: string
changes: MarkdownString
}
export interface ReleaseConfig extends BaseConfig {
changeLogs: ChangeLogConfig[]
}
export interface NotificationConfig extends BaseConfig {
title: string
message: MarkdownString
}
export interface BaseApiVendor {
vendorName: string
vendorShortDescription?: string
vendorOfficialUrl?: string
vendorLogoUrl?: string
vendorDescription?: MarkdownString
vendorTags?: VendorTag[]
}
export type ModelApiVendor<T extends ChatModelType> = BaseApiVendor & {
vendorSecrets?: GetModelConfigType<T, 'secrets'>
}
export type ModelTypeVendorsMap = {
[Key in ChatModelType]?: ModelApiVendor<Key>[]
}
export interface VendorsConfig extends BaseConfig, ModelTypeVendorsMap {
}
export interface CommonAppConfig {
notificationConfig: NotificationConfig
releaseConfig: ReleaseConfig
vendorsConfig: VendorsConfig
}
export type AppConfig = {
common: CommonAppConfig
} & {
[K in LocaleLang]?: Partial<CommonAppConfig>
}
export interface CurrentAppConfig {
showNotificationModal: boolean
showReleaseModal: boolean
currentConfig?: CommonAppConfig
}
export interface LastVisitModalDateRecord {
notificationDate?: string
releaseDate?: string
}
export type MarkedAsVisitedType = keyof LastVisitModalDateRecord

View File

@@ -40,6 +40,7 @@ export enum GptFileTreeItemType {
export enum ServerStorageName {
FrontendState = 'frontend-state',
SecretsConfig = 'secrets-config',
GlobalState = 'global-state',
WebPreset = 'web-preset',
}
@@ -65,3 +66,10 @@ export enum SecretStorageKey {
Openai = 'openai',
Proxy = 'proxy',
}
export enum VendorTag {
Free = 'free',
Official = 'official',
Unofficial = 'unofficial',
Recommended = 'recommended',
}

View File

@@ -1,3 +1,4 @@
export * from './app-config'
export * from './client'
export * from './common-file'
export * from './common'

View File

@@ -1,6 +1,7 @@
import type { CurrentAppConfig, MarkedAsVisitedType } from './app-config'
import type { FileInfoTree } from './common-file'
import type { PartialChatModelTypeMap, SingleChatMessage, SingleFileConfig, UserConfig } from './config'
import type { ChatModelType, ServerStorageName } from './enum'
import type { ChatModelType, LocaleLang, ServerStorageName } from './enum'
import type { GptFileInfo, GptFileInfoTree } from './gpt-file'
export interface BaseResponse<T = any> {
@@ -17,6 +18,10 @@ export interface ProxySecrets {
proxyUrl: string
}
export type ModelTypeVendorNameMap = {
[K in ChatModelType]?: string
}
export interface ChatStreamReqParams {
messages: SingleChatMessage[]
prompt: string
@@ -42,6 +47,12 @@ export interface ChatStreamReqParams {
singleFileConfig?: SingleFileConfig
overrideModelType?: ChatModelType
overrideModelsConfig?: PartialChatModelTypeMap
/**
* models type vendor name map
*/
modelTypeVendorNameMap?: ModelTypeVendorNameMap
contextFilePaths?: string[]
editingFilePath?: string
rootPath?: string
@@ -80,6 +91,18 @@ export interface GetProjectConfigResData {
nodeVersionValidMessage: string
}
export interface GetAppConfigReqParams {
langId?: LocaleLang
}
export type GetAppConfigResData = CurrentAppConfig
export interface MarkAsVisitedAppConfigReqParams {
types: MarkedAsVisitedType[]
}
export type MarkAsVisitedAppConfigResData = null
export interface GetUserConfigReqParams {
rootPath: string
}

View File

@@ -1,5 +1,5 @@
import { z } from 'zod'
import { ChatMessageStatus, ChatModelType, ChatRole, ClientEventName, GptFileTreeItemType, ServerStorageName } from '../types'
import { ChatMessageStatus, ChatModelType, ChatRole, ClientEventName, GptFileTreeItemType, LocaleLang, ServerStorageName } from '../types'
export const ChatModelTypeSchema = z.nativeEnum(ChatModelType)
@@ -12,3 +12,5 @@ export const ClientEventNameSchema = z.nativeEnum(ClientEventName)
export const GptFileTreeItemTypeSchema = z.nativeEnum(GptFileTreeItemType)
export const ServerStorageNameSchema = z.nativeEnum(ServerStorageName)
export const LocaleLangSchema = z.nativeEnum(LocaleLang)

View File

@@ -1,7 +1,7 @@
import { z } from 'zod'
import type { ChatStreamReqParams, CreateFilePathReqParams, DeleteFilePathReqParams, GetCommonFilesReqParams, GetFileInfoReqParams, GetGptFileInfoReqParams, GetGptFilesReqParams, GetUserConfigReqParams, InitGptFilesReqParams, OpenEditorReqParams, RenameFilePathReqParams, SaveFileContentReqParams, StorageClearReqParams, StorageGetItemReqParams, StorageRemoveItemReqParams, StorageSetItemReqParams } from '../types'
import type { ChatStreamReqParams, CreateFilePathReqParams, DeleteFilePathReqParams, GetAppConfigReqParams, GetCommonFilesReqParams, GetFileInfoReqParams, GetGptFileInfoReqParams, GetGptFilesReqParams, GetUserConfigReqParams, InitGptFilesReqParams, MarkAsVisitedAppConfigReqParams, OpenEditorReqParams, RenameFilePathReqParams, SaveFileContentReqParams, StorageClearReqParams, StorageGetItemReqParams, StorageRemoveItemReqParams, StorageSetItemReqParams } from '../types'
import { PartialChatModelTypeMapSchema, SingleChatMessageSchema, SingleFileConfigSchema } from './config'
import { ChatModelTypeSchema, ServerStorageNameSchema } from './enum.zod'
import { ChatModelTypeSchema, LocaleLangSchema, ServerStorageNameSchema } from './enum.zod'
export const ChatStreamReqParamsSchema = z.object({
messages: z.array(SingleChatMessageSchema),
@@ -13,6 +13,7 @@ export const ChatStreamReqParamsSchema = z.object({
singleFileConfig: SingleFileConfigSchema.optional(),
overrideModelType: ChatModelTypeSchema.optional(),
overrideModelsConfig: PartialChatModelTypeMapSchema.optional(),
modelTypeVendorNameMap: z.record(z.string()).optional(),
contextFilePaths: z.array(z.string()).optional(),
editingFilePath: z.string().optional(),
rootPath: z.string().optional(),
@@ -89,3 +90,14 @@ export const SaveFileContentReqParamsSchema = z.object({
fileFullPath: z.string(),
content: z.string(),
}) satisfies z.ZodType<SaveFileContentReqParams>
export const GetAppConfigReqParamsSchema = z.object({
langId: LocaleLangSchema.optional(),
}) satisfies z.ZodType<GetAppConfigReqParams>
export const MarkAsVisitedAppConfigReqParamsSchema = z.object({
types: z.array(z.union([
z.literal('notificationDate'),
z.literal('releaseDate'),
])),
}) satisfies z.ZodType<MarkAsVisitedAppConfigReqParams>

View File

@@ -0,0 +1,44 @@
{
"common": {
"notificationConfig": {
"createAt": "2023-07-24 23:31:22",
"title": "GPT Runner Notification",
"message": "v1.2.0 is release"
},
"releaseConfig": {
"createAt": "2023-07-24 23:41:04",
"changeLogs": [
{
"releaseDate": "2023-07-24 23:40:59",
"version": "1.2.0",
"changes": "fix some bug"
}
]
},
"vendorsConfig": {
"createAt": "2023-07-24 23:40:49",
"openai": [],
"anthropic": []
}
},
"zh_CN": {
"notificationConfig": {
"createAt": "2023-07-24 23:31:26",
"title": "GPT Runner 通知",
"message": "\n### 版本更新到了 v1.2.0\n1. 重启 vscode 即可去扩展处更新\n2. cli 的执行 `npm i -g gptr` 即可更新\n\n### 本次功能更新\n1. 针对语言为简体中文的用户提供 OpenAI API key 供应商,也就是你可以白嫖了。\n2. 点击左上角设置,切换供应商即可。\n3. 本次 API Key 由慷慨大方的 `剑廿三` 提供,让我们把掌声送给他。\n\n### 交流\n1. 想进群交流的加 wechat: `qq2214962083`\n "
},
"vendorsConfig": {
"createAt": "2023-07-24 23:40:49",
"openai": [
{
"vendorName": "xabcai",
"vendorSecrets": {
"basePath": "https://api.xabcai.com/v1",
"apiKey": "c2stWHZQeGJQMVBySFduZDJFZ0xpa0lKTlQzOTNoc3pZdDdmN0NNZUozSE1pdkw2QVdx"
}
}
],
"anthropic": []
}
}
}

View File

@@ -75,6 +75,8 @@
"reward": "Belohnung",
"contributors": "Mitwirkende",
"buy_me_a_coffee": "Kauf mir einen Kaffee",
"third_party_api_providers": "Drittanbieter-API-Anbieter",
"custom": "Benutzerdefiniert",
"anthropic_api_key": "Anthropic API-Schlüssel",
"anthropic_api_key_placeholder": "Bitte geben Sie den Anthropic API-Schlüssel ein",
"anthropic_api_base_path": "Anthropic API-Basispfad",
@@ -99,4 +101,4 @@
"file_editor_forgot_save_tips_title": "Möchten Sie die Änderungen an {{fileName}} speichern?",
"file_editor_forgot_save_tips_content": "Ihre Änderungen gehen verloren, wenn Sie sie nicht speichern."
}
}
}

View File

@@ -75,6 +75,8 @@
"reward": "Reward",
"contributors": "Contributors",
"buy_me_a_coffee": "buy me a coffee",
"third_party_api_providers": "Third-Party API Providers",
"custom": "Custom",
"anthropic_api_key": "Anthropic API Key",
"anthropic_api_key_placeholder": "Please input Anthropic API Key",
"anthropic_api_base_path": "Anthropic API Base Path",
@@ -99,4 +101,4 @@
"file_editor_forgot_save_tips_title": "Do you want to save changes to {{fileName}}?",
"file_editor_forgot_save_tips_content": "Your changes will be lost if you don't save them."
}
}
}

View File

@@ -75,6 +75,8 @@
"reward": "寄付",
"contributors": "貢献者",
"buy_me_a_coffee": "コーヒーを買ってください",
"third_party_api_providers": "サードパーティAPIプロバイダー",
"custom": "カスタム",
"anthropic_api_key": "Anthropic APIキー",
"anthropic_api_key_placeholder": "Anthropic API キーを入力してください",
"anthropic_api_base_path": "Anthropic API ベースパス",
@@ -99,4 +101,4 @@
"file_editor_forgot_save_tips_title": "変更を{{fileName}}に保存しますか?",
"file_editor_forgot_save_tips_content": "保存しない場合、変更は失われます。"
}
}
}

View File

@@ -75,6 +75,8 @@
"reward": "赞赏",
"contributors": "贡献者",
"buy_me_a_coffee": "请我喝杯咖啡",
"third_party_api_providers": "第三方 API 提供商",
"custom": "自定义",
"anthropic_api_key": "Anthropic API Key",
"anthropic_api_key_placeholder": "请输入 Anthropic API Key",
"anthropic_api_base_path": "Anthropic API 基础路径",
@@ -99,4 +101,4 @@
"file_editor_forgot_save_tips_title": "你想要保存对{{fileName}}的更改吗?",
"file_editor_forgot_save_tips_content": "如果你不保存,你的改动将会丢失."
}
}
}

View File

@@ -75,6 +75,8 @@
"reward": "贊賞",
"contributors": "貢獻者",
"buy_me_a_coffee": "請我喝杯咖啡",
"third_party_api_providers": "第三方 API 提供者",
"custom": "自定義",
"anthropic_api_key": "Anthropic API Key",
"anthropic_api_key_placeholder": "請輸入 Anthropic API Key",
"anthropic_api_base_path": "Anthropic API 基礎路徑",
@@ -99,4 +101,4 @@
"file_editor_forgot_save_tips_title": "你想要保存對{{fileName}}的更改嗎?",
"file_editor_forgot_save_tips_content": "如果你不保存,你的改動將會丟失."
}
}
}

View File

@@ -1,7 +1,7 @@
import { type FC, memo } from 'react'
import clsx from 'clsx'
import type { SingleChatMessage } from '@nicepkg/gpt-runner-shared/common'
import { ChatMessageStatus, ChatRole, isShallowDeepEqual } from '@nicepkg/gpt-runner-shared/common'
import { ChatMessageStatus, ChatRole } from '@nicepkg/gpt-runner-shared/common'
import type { MessageTextViewProps } from '../chat-message-text-view'
import { MessageTextView } from '../chat-message-text-view'
import { Icon } from '../icon'
@@ -87,6 +87,6 @@ export const MessageItem: FC<MessageItemProps> = memo((props) => {
</MsgContentWrapper>
</MsgWrapper>
)
}, isShallowDeepEqual)
})
MessageItem.displayName = 'MessageItem'

View File

@@ -2,7 +2,6 @@ import ReactMarkdown from 'react-markdown'
import { type FC, memo } from 'react'
import remarkGfm from 'remark-gfm'
import type { PluginOptions } from 'react-markdown/lib/react-markdown'
import { isShallowDeepEqual } from '@nicepkg/gpt-runner-shared/common'
import type { MessageCodeBlockProps } from '../chat-message-code-block'
import { MessageCodeBlock } from '../chat-message-code-block'
@@ -49,6 +48,6 @@ export const MessageTextView: FC<MessageTextViewProps> = memo((props) => {
{contents}
</ReactMarkdown>
)
}, isShallowDeepEqual)
})
MessageTextView.displayName = 'MessageTextView'

View File

@@ -1,7 +1,8 @@
import type { ReactNode } from 'react'
import { memo, useEffect, useState } from 'react'
import type { CSSProperties, ReactNode } from 'react'
import { memo, useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import { useTranslation } from 'react-i18next'
import { AnimatePresence, type Variants } from 'framer-motion'
import { FlexColumnCenter } from '../../styles/global.styles'
import { CloseButton, ModalContent, ModalContentFooter, ModalContentHeader, ModalContentWrapper, ModalTitle, ModalWrapper, StyledFooterButton } from './modal.styles'
@@ -18,7 +19,7 @@ export interface ModalProps {
text: string
onClick: () => void
}[]
contentWidth?: string
contentStyle?: CSSProperties
children?: ReactNode
onCancel?: () => void
onOk?: () => void
@@ -30,11 +31,12 @@ export const Modal = memo(({
zIndex = 10,
cancelText,
okText,
showCancelBtn = true,
showOkBtn = true,
showCloseIcon = true,
footerCenterButtons = [],
contentWidth,
contentStyle,
children,
onCancel,
onOk,
@@ -49,57 +51,77 @@ export const Modal = memo(({
setIsOpen(open)
}, [open])
const modalAnimation: Variants = useMemo(() => ({
hidden: {
opacity: 0,
y: 50,
display: 'none',
},
visible: { opacity: 1, y: 0, display: 'flex' },
exit: {
opacity: 0,
y: 50,
transitionEnd: {
display: 'none',
},
},
}), [])
const showFooter = showCancelBtn || showOkBtn || footerCenterButtons.length > 0
return createPortal(
<ModalWrapper
style={{
zIndex,
display: isOpen ? 'flex' : 'none',
}}
>
<ModalContentWrapper
<AnimatePresence>
<ModalWrapper
style={{
width: contentWidth,
zIndex,
}}
initial={'hidden'}
animate={isOpen ? 'visible' : 'hidden'}
exit={'exit'}
variants={modalAnimation}
>
<ModalContentHeader>
<ModalTitle>{title}</ModalTitle>
{showCloseIcon && <FlexColumnCenter style={{ fontSize: '1.2rem' }}>
<CloseButton className='codicon-close' onClick={onCancel}></CloseButton>
</FlexColumnCenter>}
</ModalContentHeader>
<ModalContentWrapper style={contentStyle}>
<ModalContentHeader>
<ModalTitle>{title}</ModalTitle>
{showCloseIcon && <FlexColumnCenter style={{ fontSize: '1.2rem' }}>
<CloseButton className='codicon-close' onClick={onCancel}></CloseButton>
</FlexColumnCenter>}
</ModalContentHeader>
<ModalContent>
{children}
</ModalContent>
<ModalContent style={{
paddingBottom: showFooter ? '0' : '1rem',
}}>
{children}
</ModalContent>
<ModalContentFooter>
{showCancelBtn && <StyledFooterButton
onClick={onCancel}>
{finalCancelText}
</StyledFooterButton>}
{showFooter && <ModalContentFooter>
{showCancelBtn && <StyledFooterButton
onClick={onCancel}>
{finalCancelText}
</StyledFooterButton>}
{footerCenterButtons.map((btn, index) => (
<StyledFooterButton
key={index}
{footerCenterButtons.map((btn, index) => (
<StyledFooterButton
key={index}
style={{
marginLeft: '1rem',
}}
onClick={btn.onClick}>
{btn.text}
</StyledFooterButton>
))}
{showOkBtn && <StyledFooterButton
style={{
marginLeft: '1rem',
}}
onClick={btn.onClick}>
{btn.text}
</StyledFooterButton>
))}
{showOkBtn && <StyledFooterButton
style={{
marginLeft: '1rem',
}}
onClick={onOk}>
{finalOkText}
</StyledFooterButton>}
</ModalContentFooter>
</ModalContentWrapper>
</ModalWrapper>,
onClick={onOk}>
{finalOkText}
</StyledFooterButton>}
</ModalContentFooter>}
</ModalContentWrapper>
</ModalWrapper>
</AnimatePresence>,
document.body,
)
})

View File

@@ -1,8 +1,9 @@
import { styled } from 'styled-components'
import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'
import { motion } from 'framer-motion'
import { Icon } from '../icon'
export const ModalWrapper = styled.div`
export const ModalWrapper = styled(motion.div)`
position: fixed;
top: 0;
left: 0;
@@ -21,7 +22,7 @@ export const ModalContentWrapper = styled.div`
flex-direction: column;
max-width: 100%;
max-height: 80vh;
width: min(500px, calc(100vw -1rem));
width: min(500px, calc(100vw - 1rem));
overflow: hidden;
background: var(--panel-view-background);
border-radius: 0.5rem;

View File

@@ -1,4 +1,5 @@
import { type BaseResponse, type GetProjectConfigResData, type GetUserConfigReqParams, type GetUserConfigResData, objectToQueryString } from '@nicepkg/gpt-runner-shared/common'
import { objectToQueryString } from '@nicepkg/gpt-runner-shared/common'
import type { BaseResponse, GetAppConfigReqParams, GetAppConfigResData, GetProjectConfigResData, GetUserConfigReqParams, GetUserConfigResData, MarkAsVisitedAppConfigReqParams, MarkAsVisitedAppConfigResData } from '@nicepkg/gpt-runner-shared/common'
import { getGlobalConfig } from '../helpers/global-config'
import { myFetch } from '../helpers/fetch'
@@ -21,3 +22,24 @@ export async function fetchProjectInfo(): Promise<BaseResponse<GetProjectConfigR
},
})
}
export async function fetchAppConfig(params: GetAppConfigReqParams): Promise<BaseResponse<GetAppConfigResData>> {
return await myFetch(`${getGlobalConfig().serverBaseUrl}/api/config/app-config?${objectToQueryString({
...params,
})}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
}
export async function markAsVisitedAppConfig(params: MarkAsVisitedAppConfigReqParams): Promise<BaseResponse<MarkAsVisitedAppConfigResData>> {
return await myFetch(`${getGlobalConfig().serverBaseUrl}/api/config/mark-as-visited-app-config`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
})
}

View File

@@ -27,6 +27,7 @@ export async function fetchLlmStream(
editingFilePath,
overrideModelType,
overrideModelsConfig,
modelTypeVendorNameMap,
rootPath,
namespace,
onMessage = () => {},
@@ -63,6 +64,7 @@ export async function fetchLlmStream(
editingFilePath,
overrideModelType,
overrideModelsConfig: finalOverrideModelsConfig,
modelTypeVendorNameMap,
rootPath,
} satisfies ChatStreamReqParams),
openWhenHidden: true,

View File

@@ -115,6 +115,8 @@ export const ChatPanel: FC<ChatPanelProps> = memo((props) => {
// return finalPathInfos
}, [filesRelativePaths])
const remarkPlugins = useMemo(() => [createRemarkOpenEditorPlugin(filesPathsAllPartsInfo)], [filesPathsAllPartsInfo])
useEffect(() => {
const gptFileTreeItem = getGptFileTreeItemFromChatId(chatId)
setGptFileTreeItem(gptFileTreeItem)
@@ -251,80 +253,96 @@ export const ChatPanel: FC<ChatPanelProps> = memo((props) => {
const codeBlockTheme: MessageCodeBlockTheme = isDarkTheme(themeName) ? 'dark' : 'light'
const messagePanelProps: ChatMessagePanelProps = {
messageItems: chatInstance?.messages.map((message, i) => {
const isLast = i === chatInstance.messages.length - 1
const isLastTwo = i >= chatInstance.messages.length - 2
const isAi = message.name === ChatRole.Assistant
const messagePanelProps: ChatMessagePanelProps = useMemo(() => {
return {
messageItems: chatInstance?.messages.map((message, i) => {
const isLast = i === chatInstance.messages.length - 1
const isLastTwo = i >= chatInstance.messages.length - 2
const isAi = message.name === ChatRole.Assistant
const handleRegenerateMessage = () => {
if (!isLast)
return
const handleRegenerateMessage = () => {
if (!isLast)
return
if (status === ChatMessageStatus.Pending) {
// is generating, stop first
stopCurrentGeneratingChatAnswer()
if (status === ChatMessageStatus.Pending) {
// is generating, stop first
stopCurrentGeneratingChatAnswer()
}
regenerateCurrentLastChatAnswer()
}
regenerateCurrentLastChatAnswer()
}
const handleDeleteMessage = () => {
updateCurrentChatInstance({
messages: chatInstance.messages.filter((_, index) => index !== i),
}, false)
}
const handleDeleteMessage = () => {
updateCurrentChatInstance({
messages: chatInstance.messages.filter((_, index) => index !== i),
}, false)
}
const buildMessageToolbar: MessageItemProps['buildMessageToolbar'] = ({ text }) => {
return <>
<IconButton
text={t('chat_page.copy_btn')}
iconClassName='codicon-copy'
onClick={() => handleCopy(text)}
>
</IconButton>
<IconButton
text={t('chat_page.edit_btn')}
iconClassName='codicon-edit'
onClick={() => handleEditMessage(text)}
>
</IconButton>
{isAi && isLast && <IconButton
text={status === ChatMessageStatus.Error ? t('chat_page.retry_btn') : t('chat_page.regenerate_btn')}
iconClassName='codicon-sync'
onClick={handleRegenerateMessage}
></IconButton>}
{status === ChatMessageStatus.Pending && isLast
? <IconButton
text={t('chat_page.stop_btn')}
iconClassName='codicon-chrome-maximize'
hoverShowText={false}
onClick={handleStopGenerateAnswer}
></IconButton>
: <IconButton
text={t('chat_page.delete_btn')}
iconClassName='codicon-trash'
onClick={handleDeleteMessage}
const buildMessageToolbar: MessageItemProps['buildMessageToolbar'] = ({ text }) => {
return <>
<IconButton
text={t('chat_page.copy_btn')}
iconClassName='codicon-copy'
onClick={() => handleCopy(text)}
>
</IconButton>}
</>
}
</IconButton>
return {
...message,
remarkPlugins: [createRemarkOpenEditorPlugin(filesPathsAllPartsInfo)],
status: isLast ? status : ChatMessageStatus.Success,
showToolbar: isLastTwo ? 'always' : 'hover',
showAvatar: chatPanelWidth > 600,
theme: codeBlockTheme,
buildCodeToolbar: status === ChatMessageStatus.Pending ? undefined : buildCodeToolbar,
buildMessageToolbar,
} satisfies MessageItemProps
}) ?? [],
}
<IconButton
text={t('chat_page.edit_btn')}
iconClassName='codicon-edit'
onClick={() => handleEditMessage(text)}
>
</IconButton>
{isAi && isLast && <IconButton
text={status === ChatMessageStatus.Error ? t('chat_page.retry_btn') : t('chat_page.regenerate_btn')}
iconClassName='codicon-sync'
onClick={handleRegenerateMessage}
></IconButton>}
{status === ChatMessageStatus.Pending && isLast
? <IconButton
text={t('chat_page.stop_btn')}
iconClassName='codicon-chrome-maximize'
hoverShowText={false}
onClick={handleStopGenerateAnswer}
></IconButton>
: <IconButton
text={t('chat_page.delete_btn')}
iconClassName='codicon-trash'
onClick={handleDeleteMessage}
>
</IconButton>}
</>
}
return {
...message,
remarkPlugins,
status: isLast ? status : ChatMessageStatus.Success,
showToolbar: isLastTwo ? 'always' : 'hover',
showAvatar: chatPanelWidth > 600,
theme: codeBlockTheme,
buildCodeToolbar: status === ChatMessageStatus.Pending ? undefined : buildCodeToolbar,
buildMessageToolbar,
} satisfies MessageItemProps
}) ?? [],
}
}, [
chatInstance?.messages,
status,
chatPanelWidth,
remarkPlugins,
codeBlockTheme,
buildCodeToolbar,
handleCopy,
handleEditMessage,
handleStopGenerateAnswer,
regenerateCurrentLastChatAnswer,
stopCurrentGeneratingChatAnswer,
updateCurrentChatInstance,
t,
])
const renderInputToolbar = () => {
return <>

View File

@@ -3,9 +3,10 @@ import type { AnthropicSecrets } from '@nicepkg/gpt-runner-shared/common'
import { type FC, memo } from 'react'
import { useTranslation } from 'react-i18next'
import { HookFormInput } from '../../../../../../../components/hook-form/hook-form-input'
import { BaseSecretsSettings, type BaseSecretsSettingsFormItemConfig } from '../base-secrets-settings'
import { BaseSecretsSettings } from '../base-secrets-settings'
import type { BaseSecretsFormData, BaseSecretsSettingsFormItemConfig } from '../base-secrets-settings'
interface FormData extends Pick<AnthropicSecrets, 'apiKey' | 'basePath'> {
interface FormData extends Pick<AnthropicSecrets, 'apiKey' | 'basePath'>, BaseSecretsFormData {
}
export interface AnthropicSecretsSettingsProps {
@@ -17,12 +18,13 @@ export const AnthropicSecretsSettings: FC<AnthropicSecretsSettingsProps> = memo(
const formConfig: BaseSecretsSettingsFormItemConfig<FormData>[] = [
{
name: 'apiKey',
buildView: ({ useFormReturns: { control, formState } }) => {
buildView: ({ useFormReturns: { control, formState }, currentVendorConfig }) => {
return <>
<HookFormInput
label={t('chat_page.anthropic_api_key')}
placeholder={t('chat_page.anthropic_api_key_placeholder')}
name="apiKey"
disabled={Boolean(currentVendorConfig?.vendorSecrets)}
errors={formState.errors}
control={control}
type="password"
@@ -32,12 +34,13 @@ export const AnthropicSecretsSettings: FC<AnthropicSecretsSettingsProps> = memo(
},
{
name: 'basePath',
buildView: ({ useFormReturns: { control, formState } }) => {
buildView: ({ useFormReturns: { control, formState }, currentVendorConfig }) => {
return <>
<HookFormInput
label={t('chat_page.anthropic_api_base_path')}
placeholder={DEFAULT_ANTHROPIC_API_BASE_PATH}
name="basePath"
disabled={Boolean(currentVendorConfig?.vendorSecrets)}
errors={formState.errors}
control={control}
/>

View File

@@ -1,40 +1,82 @@
import type { ModelApiVendor } from '@nicepkg/gpt-runner-shared/common'
import { ChatModelType, ServerStorageName, getModelConfigTypeSchema } from '@nicepkg/gpt-runner-shared/common'
import { memo, useEffect } from 'react'
import { memo, useEffect, useMemo } from 'react'
import type { ReactNode } from 'react'
import { useForm } from 'react-hook-form'
import type { Path, UseFormReturn } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { useMutation, useQuery } from '@tanstack/react-query'
import { zodResolver } from '@hookform/resolvers/zod'
import { toast } from 'react-hot-toast'
import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'
import { zodResolver } from '@hookform/resolvers/zod'
import { StyledForm, StyledFormItem } from '../../settings.styles'
import { getServerStorage, saveServerStorage } from '../../../../../../networks/server-storage'
import { IS_SAFE } from '../../../../../../helpers/constant'
import { useLoading } from '../../../../../../hooks/use-loading.hook'
import { HookFormSelect } from '../../../../../../components/hook-form/hook-form-select'
import { useTempStore } from '../../../../../../store/zustand/temp'
import { useGlobalStore } from '../../../../../../store/zustand/global'
export interface BaseSecretsSettingsFormItemBuildViewState<FormData extends Record<string, any>> {
useFormReturns: UseFormReturn<FormData, any, undefined>
export interface BaseSecretsFormData {
vendorName: string
}
export interface BaseSecretsSettingsFormItemConfig<FormData extends Record<string, any>> {
export interface BaseSecretsSettingsFormItemBuildViewState<FormData extends BaseSecretsFormData, M extends ChatModelType = ChatModelType.Openai> {
useFormReturns: UseFormReturn<FormData, any, undefined>
currentVendorConfig: ModelApiVendor<M> | null
}
export interface BaseSecretsSettingsFormItemConfig<FormData extends BaseSecretsFormData> {
name: keyof FormData
buildView: (state: BaseSecretsSettingsFormItemBuildViewState<FormData>) => ReactNode
}
export interface BaseSecretsSettingsProps<FormData extends Record<string, any>> {
modelType?: ChatModelType
export interface BaseSecretsSettingsProps<FormData extends BaseSecretsFormData, M extends ChatModelType = ChatModelType.Openai> {
modelType?: M
formConfig: BaseSecretsSettingsFormItemConfig<FormData>[]
}
function BaseSecretsSettings_<FormData extends Record<string, any>>(props: BaseSecretsSettingsProps<FormData>) {
function BaseSecretsSettings_<FormData extends BaseSecretsFormData, M extends ChatModelType = ChatModelType.Openai>(props: BaseSecretsSettingsProps<FormData, M>) {
const { modelType, formConfig } = props
const { t } = useTranslation()
const { setLoading } = useLoading()
const { currentAppConfig } = useTempStore()
const { modelTypeVendorNameMap, updateModelTypeVendorName } = useGlobalStore()
const currentModelType = modelType || ChatModelType.Openai
const { data: querySecretsRes } = useQuery({
const vendorsConfig = useMemo(() => {
return currentAppConfig?.currentConfig?.vendorsConfig?.[currentModelType]
}, [currentAppConfig, currentModelType])
const vendorNameConfigMap = useMemo(() => {
const map: Record<string, ModelApiVendor<typeof currentModelType>> = {}
vendorsConfig?.forEach((item) => {
map[item.vendorName] = item
})
return map
}, [vendorsConfig])
const vendorSelectOptions = useMemo(() => {
const options = vendorsConfig?.map((item) => {
return {
label: item.vendorName,
value: item.vendorName,
}
}) ?? []
options.unshift({
label: t('chat_page.custom'),
value: '',
})
return options
}, [vendorsConfig, t])
const { data: querySecretsRes, refetch: refetchSecretes } = useQuery({
queryKey: ['secrets', currentModelType],
enabled: !!currentModelType,
queryFn: () => getServerStorage({
@@ -56,23 +98,40 @@ function BaseSecretsSettings_<FormData extends Record<string, any>>(props: BaseS
const useFormReturns = useForm<FormData>({
mode: 'onBlur',
resolver: zodResolver(getModelConfigTypeSchema(currentModelType, 'secrets')),
defaultValues: {
vendorName: modelTypeVendorNameMap[currentModelType] || '',
} as any,
})
const { handleSubmit, setValue } = useFormReturns
const { handleSubmit, setValue, watch, formState, control } = useFormReturns
const currentVendorName = watch('vendorName' as any)
const currentVendorConfig = useMemo(() => {
return vendorNameConfigMap[currentVendorName] || null
}, [currentVendorName]) as ModelApiVendor<M> | null
useEffect(() => {
if (remoteSecrets) {
Object.keys(remoteSecrets).forEach((key) => {
setValue(key as Path<FormData>, remoteSecrets[key as keyof FormData])
const finalSecrets = currentVendorConfig?.vendorSecrets || remoteSecrets
if (finalSecrets) {
Object.keys(finalSecrets).forEach((key) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
setValue(key as Path<FormData>, finalSecrets[key])
})
}
}, [remoteSecrets])
}, [remoteSecrets, currentVendorConfig])
useEffect(() => {
updateModelTypeVendorName(currentModelType, currentVendorName)
}, [currentModelType, currentVendorName])
const onSubmit = async (data: FormData) => {
setLoading(true)
try {
await saveSecrets(data)
await refetchSecretes()
toast.success(t('chat_page.toast_save_success'))
}
finally {
@@ -81,9 +140,23 @@ function BaseSecretsSettings_<FormData extends Record<string, any>>(props: BaseS
}
return <StyledForm onSubmit={handleSubmit(onSubmit)}>
{IS_SAFE && <StyledFormItem key={-1}>
<HookFormSelect
label={t('chat_page.third_party_api_providers')}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
name="vendorName"
errors={formState.errors}
options={vendorSelectOptions}
control={control}
/>
</StyledFormItem>}
{formConfig.map((formItemConfig, index) => {
const buildViewState: BaseSecretsSettingsFormItemBuildViewState<FormData> = {
useFormReturns,
currentVendorConfig,
}
return <StyledFormItem key={index}>
{formItemConfig.buildView(buildViewState)}
@@ -91,7 +164,7 @@ function BaseSecretsSettings_<FormData extends Record<string, any>>(props: BaseS
})}
<VSCodeButton
disabled={!IS_SAFE}
disabled={!IS_SAFE || Boolean(currentVendorConfig?.vendorSecrets)}
appearance='primary'
type='submit'
>

View File

@@ -5,9 +5,10 @@ import { VSCodeLink } from '@vscode/webview-ui-toolkit/react'
import { useTranslation } from 'react-i18next'
import { HookFormInput } from '../../../../../../../components/hook-form/hook-form-input'
import { HookFormTextarea } from '../../../../../../../components/hook-form/hook-form-textarea'
import { BaseSecretsSettings, type BaseSecretsSettingsFormItemConfig } from '../base-secrets-settings'
import { BaseSecretsSettings } from '../base-secrets-settings'
import type { BaseSecretsFormData, BaseSecretsSettingsFormItemConfig } from '../base-secrets-settings'
interface FormData extends Pick<OpenaiSecrets, 'apiKey' | 'accessToken' | 'basePath'> {
interface FormData extends Pick<OpenaiSecrets, 'apiKey' | 'accessToken' | 'basePath'>, BaseSecretsFormData {
}
export interface OpenaiSecretsSettingsProps {
@@ -19,12 +20,13 @@ export const OpenaiSecretsSettings: FC<OpenaiSecretsSettingsProps> = memo((props
const formConfig: BaseSecretsSettingsFormItemConfig<FormData>[] = [
{
name: 'apiKey',
buildView: ({ useFormReturns: { control, formState } }) => {
buildView: ({ useFormReturns: { control, formState }, currentVendorConfig }) => {
return <>
<HookFormInput
label={t('chat_page.openai_api_key')}
placeholder={t('chat_page.openai_api_key_placeholder')}
name="apiKey"
disabled={Boolean(currentVendorConfig?.vendorSecrets)}
errors={formState.errors}
control={control}
type="password"
@@ -34,12 +36,13 @@ export const OpenaiSecretsSettings: FC<OpenaiSecretsSettingsProps> = memo((props
},
{
name: 'basePath',
buildView: ({ useFormReturns: { control, formState } }) => {
buildView: ({ useFormReturns: { control, formState }, currentVendorConfig }) => {
return <>
<HookFormInput
label={t('chat_page.openai_api_base_path')}
placeholder={DEFAULT_OPENAI_API_BASE_PATH}
name="basePath"
disabled={Boolean(currentVendorConfig?.vendorSecrets)}
errors={formState.errors}
control={control}
/>
@@ -47,11 +50,12 @@ export const OpenaiSecretsSettings: FC<OpenaiSecretsSettingsProps> = memo((props
},
}, {
name: 'accessToken',
buildView: ({ useFormReturns: { control, formState } }) => {
buildView: ({ useFormReturns: { control, formState }, currentVendorConfig }) => {
return <>
<HookFormTextarea
label={t('chat_page.openai_access_token')}
name="accessToken"
disabled={Boolean(currentVendorConfig?.vendorSecrets)}
placeholder={t('chat_page.openai_access_token_placeholder')}
errors={formState.errors}
control={control}

View File

@@ -91,7 +91,7 @@ const Chat: FC = memo(() => {
// any status will scroll down
useEffect(() => {
scrollDown()
}, [chatInstance?.status, scrollDownRef.current, scrollDown])
}, [chatInstance?.status, scrollDownRef.current, scrollDown, chatInstance?.id])
// if is pending and scroll bottom is less than 40, scroll down
// when you scroll by yourself, scrollDown will stop auto scrollDown

View File

@@ -8,6 +8,7 @@ import Error404 from './pages/error/404'
import { useLoading } from './hooks/use-loading.hook'
import Chat from './pages/chat'
import { getGlobalConfig } from './helpers/global-config'
import { Layout } from './views/layout'
const HackRouter: FC = () => {
const navigate = useNavigate()
@@ -46,14 +47,16 @@ export const AppRouter: FC = () => {
{loading && <VSCodeProgressRing />}
<Suspense fallback={<VSCodeProgressRing />}>
<Router>
<HackRouter></HackRouter>
<Routes>
<Route index element={<Home />} />
<Route path="chat" element={<Chat />} />
<Route path="*" element={<Error404 />} />
</Routes>
</Router>
<Layout>
<Router>
<HackRouter></HackRouter>
<Routes>
<Route index element={<Home />} />
<Route path="chat" element={<Chat />} />
<Route path="*" element={<Error404 />} />
</Routes>
</Router>
</Layout>
</Suspense>
</>
)

View File

@@ -8,6 +8,7 @@ import { getGlobalConfig } from '../../../helpers/global-config'
import { useTempStore } from '../temp'
import type { SidebarTreeItem, SidebarTreeSlice } from './sidebar-tree.slice'
import type { FileTreeSlice } from './file-tree.slice'
import type { GeneralSlice } from './general.slice'
export enum GenerateAnswerType {
Generate = 'generate',
@@ -67,7 +68,7 @@ const chatIdChatInstanceMap = new Map<string, SingleChat>()
const singleFilePathChatInstancesMap = new Map<string, SingleChat[]>()
export const createChatSlice: StateCreator<
ChatSlice & SidebarTreeSlice & FileTreeSlice,
ChatSlice & SidebarTreeSlice & FileTreeSlice & GeneralSlice,
[],
[],
ChatSlice
@@ -312,6 +313,7 @@ export const createChatSlice: StateCreator<
editingFilePath: shouldProvideEditingPath ? tempState.ideActiveFilePath : undefined,
overrideModelType: state.overrideModelType || undefined,
overrideModelsConfig: state.overrideModelsConfig,
modelTypeVendorNameMap: state.modelTypeVendorNameMap,
rootPath: getGlobalConfig().rootPath,
onError(e) {
console.error('fetchLlmStream error:', e)

View File

@@ -1,5 +1,5 @@
import type { StateCreator } from 'zustand'
import type { LocaleLang } from '@nicepkg/gpt-runner-shared/common'
import type { ChatModelType, LocaleLang, ModelTypeVendorNameMap } from '@nicepkg/gpt-runner-shared/common'
import type { GetState } from '../types'
import type { ThemeName } from '../../../styles/themes'
import { getGlobalConfig } from '../../../helpers/global-config'
@@ -7,8 +7,10 @@ import { getGlobalConfig } from '../../../helpers/global-config'
export interface GeneralSlice {
langId: LocaleLang
themeName: ThemeName
modelTypeVendorNameMap: ModelTypeVendorNameMap
updateLangId: (langId: LocaleLang) => void
updateThemeName: (themeName: ThemeName) => void
updateModelTypeVendorName: (modelType: ChatModelType, vendorName: string) => void
}
export type GeneralState = GetState<GeneralSlice>
@@ -17,6 +19,8 @@ function getInitialState() {
return {
langId: getGlobalConfig().defaultLangId,
themeName: getGlobalConfig().defaultTheme,
modelTypeVendorNameMap: {
},
} satisfies GeneralState
}
@@ -33,4 +37,13 @@ export const createGeneralSlice: StateCreator<
updateThemeName(themeName) {
set({ themeName })
},
updateModelTypeVendorName(modelType, vendorName) {
const state = get()
set({
modelTypeVendorNameMap: {
...state.modelTypeVendorNameMap,
[modelType]: vendorName,
},
})
},
})

View File

@@ -2,16 +2,18 @@ import type { StateCreator } from 'zustand'
import type { GetState } from '../types'
import { createStore } from '../utils'
import { FileSidebarTreeItem } from '../global/file-tree.slice'
import { BaseResponse, GetCommonFilesResData, travelTree } from '@nicepkg/gpt-runner-shared/common'
import { BaseResponse, CurrentAppConfig, GetCommonFilesResData, travelTree } from '@nicepkg/gpt-runner-shared/common'
import { useGlobalStore } from '../global'
export interface TempSlice {
currentAppConfig: CurrentAppConfig | null
userSelectedText: string
ideActiveFilePath: string
ideOpeningFilePaths: string[]
filesTree: FileSidebarTreeItem[]
fullPathFileMap: Record<string, FileSidebarTreeItem>
filesRelativePaths: string[]
updateCurrentAppConfig: (currentAppConfig: Partial<CurrentAppConfig> | null) => void
updateIdeSelectedText: (userSelectedText: string) => void
updateIdeActiveFilePath: (ideActiveFilePath: string) => void
updateIdeOpeningFilePaths: (ideOpeningFilePaths: string[] | ((oldIdeOpeningFilePaths: string[]) => string[])) => void
@@ -26,6 +28,7 @@ export type TempState = GetState<TempSlice>
function getInitialState() {
return {
currentAppConfig: null,
userSelectedText: '',
ideActiveFilePath: '',
ideOpeningFilePaths: [],
@@ -42,6 +45,15 @@ export const createTempSlice: StateCreator<
TempSlice
> = (set, get) => ({
...getInitialState(),
updateCurrentAppConfig(currentAppConfig) {
const state = get()
set({
currentAppConfig: {
...state.currentAppConfig!,
...currentAppConfig
},
})
},
updateIdeSelectedText(userSelectedText) {
set({
userSelectedText,

View File

@@ -0,0 +1,102 @@
import { useMutation, useQuery } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import { memo, useMemo } from 'react'
import type { MarkAsVisitedAppConfigReqParams } from '@nicepkg/gpt-runner-shared/common'
import { useTempStore } from '../../store/zustand/temp'
import { useGlobalStore } from '../../store/zustand/global'
import { fetchAppConfig, markAsVisitedAppConfig } from '../../networks/config'
import { Modal } from '../../components/modal'
import { MessageTextView } from '../../components/chat-message-text-view'
export interface LayoutProps {
children?: ReactNode
}
export const Layout = memo((props: LayoutProps) => {
const { children } = props
const { currentAppConfig, updateCurrentAppConfig } = useTempStore()
const { langId } = useGlobalStore()
// update app config
useQuery({
queryKey: ['app-config', langId],
enabled: !!langId,
queryFn: () => fetchAppConfig({
langId,
}),
onSuccess(data) {
const currentAppConfig = data.data
if (currentAppConfig)
updateCurrentAppConfig(currentAppConfig)
},
})
const { mutate: markVisitedModal } = useMutation({
mutationFn: (params: MarkAsVisitedAppConfigReqParams) => markAsVisitedAppConfig(params),
})
const notificationConfig = currentAppConfig?.currentConfig?.notificationConfig
const releaseConfig = currentAppConfig?.currentConfig?.releaseConfig
const releaseLog = useMemo(() => {
let content = ''
releaseConfig?.changeLogs.forEach((log) => {
content += `## ${log.version}\n`
content += `${log.changes}\n\n`
})
return content
}, [releaseConfig?.changeLogs])
return <>
{/* notification modal */}
<Modal
zIndex={99}
open={Boolean(currentAppConfig?.showNotificationModal)}
title={notificationConfig?.title || ''}
showCancelBtn={false}
showOkBtn={false}
contentStyle={{
minHeight: '80vh',
}}
onCancel={() => {
markVisitedModal({
types: ['notificationDate'],
})
updateCurrentAppConfig({
showNotificationModal: false,
})
}}
>
<MessageTextView
contents={notificationConfig?.message || ''} />
</Modal>
{/* release log modal */}
<Modal
zIndex={100}
open={Boolean(currentAppConfig?.showReleaseModal)}
title={'Release Log'}
showCancelBtn={false}
showOkBtn={false}
contentStyle={{
minHeight: '80vh',
}}
onCancel={() => {
markVisitedModal({
types: ['releaseDate'],
})
updateCurrentAppConfig({
showReleaseModal: false,
})
}}
>
<MessageTextView
contents={releaseLog} />
</Modal>
{children}
</>
})
Layout.displayName = 'Layout'

View File

@@ -1 +1 @@
export * from './common'
export * from './dist/common'

View File

@@ -63,6 +63,7 @@
],
"scripts": {
"build": "pnpm build:server & pnpm build:client",
"build:app-config": "esno ./scripts/build-app-config-json.ts",
"build:client": "vite build --config ./client/vite.config.ts",
"build:client:watch": "vite build --config ./client/vite.config.ts --watch",
"build:server": "unbuild && pnpm tsc --build tsconfig.dts.json",
@@ -73,13 +74,13 @@
},
"devDependencies": {
"@hookform/resolvers": "^3.1.1",
"@kvs/node-localstorage": "^2.1.3",
"@kvs/storage": "^2.1.3",
"@kvs/node-localstorage": "^2.1.5",
"@kvs/storage": "^2.1.4",
"@microsoft/fetch-event-source": "^2.0.1",
"@monaco-editor/react": "^4.5.1",
"@nicepkg/gpt-runner-core": "workspace:*",
"@nicepkg/gpt-runner-shared": "workspace:*",
"@tanstack/react-query": "^4.29.25",
"@tanstack/react-query": "^4.32.0",
"@types/connect-history-api-fallback": "^1.5.0",
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
@@ -100,12 +101,13 @@
"cross-env": "^7.0.3",
"eventemitter": "^0.3.3",
"express": "^4.18.2",
"framer-motion": "^10.13.0",
"framer-motion": "^10.13.1",
"fs-extra": "^11.1.1",
"global-agent": "^3.0.0",
"i18next": "^23.2.11",
"i18next-browser-languagedetector": "^7.1.0",
"i18next-http-backend": "^2.2.1",
"js-base64": "^3.7.5",
"keyboardjs": "^2.7.0",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.40.0",
@@ -121,11 +123,11 @@
"react-tiny-popover": "^7.2.4",
"react-use": "^17.4.0",
"remark-gfm": "^3.0.1",
"styled-components": "^6.0.4",
"styled-components": "^6.0.5",
"undici": "^5.22.1",
"unist-util-visit": "^5.0.0",
"uuid": "^9.0.0",
"vite": "^4.4.4",
"vite": "^4.4.7",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^3.2.0",
"zustand": "^4.3.9"

View File

@@ -0,0 +1,23 @@
import { FileUtils, PathUtils } from '@nicepkg/gpt-runner-shared/node'
import { appConfig } from '../server/index'
async function buildAppConfigJson() {
try {
await FileUtils.writeFile({
filePath: PathUtils.resolve(__dirname, '../assets/app-config.json'),
content: JSON.stringify(appConfig, null, 2),
valid: false,
})
console.log('buildAppConfigJson successfully')
}
catch (e) {
console.error(`buildAppConfigJson fail, could not be written: ${e}`)
}
}
buildAppConfigJson().then(() => {
console.log('buildAppConfigJson done')
}).catch((e) => {
console.error('buildAppConfigJson error:', e)
})

View File

@@ -1,24 +0,0 @@
#01 You are a Senior Frontend developer.
#02 Users will describe a project details you will code project with this tools:
react/typescript/styled-components/react-hook-form/react-use-query/zustand/@vscode/webview-ui-toolkit/react-use.
#03 You can always write high-quality code,
such as high cohesion, low coupling, follow SOLID principles, etc.
#04 Your responses should be informative and logical.
#05 First think step-by-step - describe your plan for what to build in pseudocode, written out in great detail.
#06 Then output the code in a single code block.
#07 Minimize any other prose.
#08 Keep your answers short and impersonal.
#09 Use Markdown formatting in your answers.
#10 Make sure to include the programming language name at the start of the Markdown code blocks.
#11 Avoid wrapping the whole response in triple backticks.

View File

@@ -1,38 +0,0 @@
SystemPrompt:
#01 You are a Senior Frontend developer.
#02 Users will describe a project details you will code project with this tools:
react/typescript/styled-components/react-hook-form/react-use-query/zustand/@vscode/webview-ui-toolkit/react-use.
#03 You can always write high-quality code,
such as high cohesion, low coupling, follow SOLID principles, etc.
#04 Your responses should be informative and logical.
#05 First think step-by-step - describe your plan for what to build in pseudocode, written out in great detail.
#06 Then output the code in a single code block.
#07 Minimize any other prose.
#08 Keep your answers short and impersonal.
#09 Use Markdown formatting in your answers.
#10 Make sure to include the programming language name at the start of the Markdown code blocks.
#11 Avoid wrapping the whole response in triple backticks.
Users:
please help me to write a compnent with react + ts, i will give you a component interface props,
please write a component with this props with my stack tools,
BTW, here is my css vars:
var(--background), var(--border-width), var(--contrast-active-border), var(--contrast-border), var(--corner-radius), var(--design-unit), var(--disabled-opacity), var(--focus-border), var(--font-family), var(--font-weight), var(--foreground), var(--input-height), var(--input-min-width), var(--type-ramp-base-font-size), var(--type-ramp-base-line-height), var(--type-ramp-minus1-font-size), var(--type-ramp-minus1-line-height), var(--type-ramp-minus2-font-size), var(--type-ramp-minus2-line-height), var(--type-ramp-plus1-font-size), var(--type-ramp-plus1-line-height), var(--scrollbarWidth), var(--scrollbarHeight), var(--scrollbar-slider-background), var(--scrollbar-slider-hover-background), var(--scrollbar-slider-active-background), var(--badge-background), var(--badge-foreground), var(--button-border), var(--button-icon-background), var(--button-icon-corner-radius), var(--button-icon-outline-offset), var(--button-icon-hover-background), var(--button-icon-padding), var(--button-primary-background), var(--button-primary-foreground), var(--button-primary-hover-background), var(--button-secondary-background), var(--button-secondary-foreground), var(--button-secondary-hover-background), var(--button-padding-horizontal), var(--button-padding-vertical), var(--checkbox-background), var(--checkbox-border), var(--checkbox-corner-radius), var(--checkbox-foreground), var(--list-active-selection-background), var(--list-active-selection-foreground), var(--list-hover-background), var(--divider-background), var(--dropdown-background), var(--dropdown-border), var(--dropdown-foreground), var(--dropdown-list-max-height), var(--input-background), var(--input-foreground), var(--input-placeholder-foreground), var(--link-active-foreground), var(--link-foreground), var(--progress-background), var(--panel-tab-active-border), var(--panel-tab-active-foreground), var(--panel-tab-foreground), var(--panel-view-background), var(--panel-view-border), var(--tag-corner-radius),
my props is:

View File

@@ -8,6 +8,8 @@ import { setProxyUrl } from './src/proxy'
import { processControllers } from './src/controllers'
import { errorHandlerMiddleware, safeCheckMiddleware } from './src/middleware'
export * from './src/helpers/app-config'
const dirname = PathUtils.getCurrentDirName(import.meta.url, () => __dirname)
const resolvePath = (...paths: string[]) => PathUtils.resolve(dirname, ...paths)

View File

@@ -2,7 +2,7 @@ import { PathUtils, sendSuccessResponse, verifyParamsByZod } from '@nicepkg/gpt-
import { DEFAULT_EXCLUDE_FILE_EXTS, type GetCommonFilesReqParams, GetCommonFilesReqParamsSchema, type GetCommonFilesResData } from '@nicepkg/gpt-runner-shared/common'
import { getCommonFileTree, loadUserConfig } from '@nicepkg/gpt-runner-core'
import type { ControllerConfig } from '../types'
import { getValidFinalPath } from '../services/valid-path'
import { getValidFinalPath } from '../helpers/valid-path'
export const commonFilesControllers: ControllerConfig = {
namespacePath: '/common-files',

View File

@@ -1,10 +1,11 @@
import { checkNodeVersion, sendSuccessResponse, verifyParamsByZod } from '@nicepkg/gpt-runner-shared/node'
import type { GetProjectConfigResData, GetUserConfigReqParams, GetUserConfigResData } from '@nicepkg/gpt-runner-shared/common'
import { EnvConfig, GetUserConfigReqParamsSchema, removeUserConfigUnsafeKey } from '@nicepkg/gpt-runner-shared/common'
import type { GetAppConfigReqParams, GetAppConfigResData, GetProjectConfigResData, GetUserConfigReqParams, GetUserConfigResData, MarkAsVisitedAppConfigReqParams, MarkAsVisitedAppConfigResData } from '@nicepkg/gpt-runner-shared/common'
import { EnvConfig, GetAppConfigReqParamsSchema, GetUserConfigReqParamsSchema, MarkAsVisitedAppConfigReqParamsSchema, removeUserConfigUnsafeKey } from '@nicepkg/gpt-runner-shared/common'
import { loadUserConfig } from '@nicepkg/gpt-runner-core'
import pkg from '../../../package.json'
import type { ControllerConfig } from '../types'
import { getValidFinalPath } from '../services/valid-path'
import { getValidFinalPath } from '../helpers/valid-path'
import { AppConfigService } from '../services/app-config.service'
export const configControllers: ControllerConfig = {
namespacePath: '/config',
@@ -61,5 +62,42 @@ export const configControllers: ControllerConfig = {
})
},
},
{
url: '/app-config',
method: 'get',
handler: async (req, res) => {
const query = req.query as GetAppConfigReqParams
verifyParamsByZod(query, GetAppConfigReqParamsSchema)
const { langId } = query
langId && AppConfigService.instance.updateLangId(langId)
const currentAppConfig = await AppConfigService.instance.getCurrentAppConfig(true)
sendSuccessResponse(res, {
data: {
...currentAppConfig,
} satisfies GetAppConfigResData,
})
},
},
{
url: '/mark-as-visited-app-config',
method: 'post',
handler: async (req, res) => {
const body = req.body as MarkAsVisitedAppConfigReqParams
verifyParamsByZod(body, MarkAsVisitedAppConfigReqParamsSchema)
const { types } = body
await AppConfigService.instance.markedAsVisited(types)
sendSuccessResponse(res, {
data: null satisfies MarkAsVisitedAppConfigResData,
})
},
},
],
}

View File

@@ -2,7 +2,7 @@ import { FileUtils, PathUtils, launchEditorByPathAndContent, sendSuccessResponse
import type { CreateFilePathReqParams, CreateFilePathResData, DeleteFilePathReqParams, DeleteFilePathResData, GetFileInfoReqParams, GetFileInfoResData, OpenEditorReqParams, OpenEditorResData, RenameFilePathReqParams, RenameFilePathResData, SaveFileContentReqParams, SaveFileContentResData } from '@nicepkg/gpt-runner-shared/common'
import { CreateFilePathReqParamsSchema, DeleteFilePathReqParamsSchema, GetFileInfoReqParamsSchema, OpenEditorReqParamsSchema, RenameFilePathReqParamsSchema, SaveFileContentReqParamsSchema } from '@nicepkg/gpt-runner-shared/common'
import type { ControllerConfig } from '../types'
import { getValidFinalPath } from '../services/valid-path'
import { getValidFinalPath } from '../helpers/valid-path'
export const editorControllers: ControllerConfig = {
namespacePath: '/editor',

View File

@@ -4,7 +4,7 @@ import { sendSuccessResponse, verifyParamsByZod } from '@nicepkg/gpt-runner-shar
import type { GetGptFileInfoReqParams, GetGptFileInfoResData, GetGptFilesReqParams, GetGptFilesResData, InitGptFilesReqParams, InitGptFilesResData } from '@nicepkg/gpt-runner-shared/common'
import { Debug, GetGptFileInfoReqParamsSchema, GetGptFilesReqParamsSchema, InitGptFilesReqParamsSchema, removeUserConfigUnsafeKey } from '@nicepkg/gpt-runner-shared/common'
import type { ControllerConfig } from '../types'
import { getValidFinalPath } from '../services/valid-path'
import { getValidFinalPath } from '../helpers/valid-path'
export const gptFilesControllers: ControllerConfig = {
namespacePath: '/gpt-files',

View File

@@ -1,10 +1,10 @@
import type { Request, Response } from 'express'
import type { ChatModelType, ChatStreamReqParams, FailResponse, SingleFileConfig, SuccessResponse } from '@nicepkg/gpt-runner-shared/common'
import { ChatStreamReqParamsSchema, Debug, STREAM_DONE_FLAG, buildFailResponse, buildSuccessResponse, toUnixPath } from '@nicepkg/gpt-runner-shared/common'
import { PathUtils, verifyParamsByZod } from '@nicepkg/gpt-runner-shared/node'
import { createFileContext, getLLMChain, getSecrets, loadUserConfig, parseGptFile } from '@nicepkg/gpt-runner-core'
import { getValidFinalPath } from '../services/valid-path'
import type { ChatStreamReqParams, FailResponse, SuccessResponse } from '@nicepkg/gpt-runner-shared/common'
import { ChatStreamReqParamsSchema, Debug, STREAM_DONE_FLAG, buildFailResponse, buildSuccessResponse } from '@nicepkg/gpt-runner-shared/common'
import { verifyParamsByZod } from '@nicepkg/gpt-runner-shared/node'
import { getLLMChain } from '@nicepkg/gpt-runner-core'
import type { ControllerConfig } from '../types'
import { LLMService } from '../services/llm.service'
export const llmControllers: ControllerConfig = {
namespacePath: '/chatgpt',
@@ -26,60 +26,10 @@ export const llmControllers: ControllerConfig = {
verifyParamsByZod(body, ChatStreamReqParamsSchema)
const {
messages = [],
prompt = '',
systemPrompt: systemPromptFromParams = '',
singleFilePath,
singleFileConfig: singleFileConfigFromParams,
appendSystemPrompt = '',
systemPromptAsUserPrompt = false,
contextFilePaths,
editingFilePath,
overrideModelType,
overrideModelsConfig,
rootPath,
} = body
const finalPath = getValidFinalPath({
path: rootPath,
assertType: 'directory',
fieldName: 'rootPath',
})
const { config: userConfig } = await loadUserConfig(finalPath)
let singleFileConfig: SingleFileConfig | undefined = singleFileConfigFromParams
if (singleFilePath && PathUtils.isFile(singleFilePath)) {
// keep realtime config
singleFileConfig = await parseGptFile({
filePath: singleFilePath,
userConfig,
})
}
if (overrideModelType && overrideModelType !== singleFileConfig?.model?.type) {
singleFileConfig = {
model: {
type: overrideModelType,
},
}
}
const model = {
...singleFileConfig?.model,
...overrideModelsConfig?.[singleFileConfig?.model?.type as ChatModelType || ''],
} as SingleFileConfig['model']
const secretFromUserConfig = userConfig.model?.type === model?.type ? userConfig.model?.secrets : undefined
let secretsFromStorage = await getSecrets(model?.type as ChatModelType || null)
// if some secret value is '' or null or undefined, should remove
secretsFromStorage = Object.fromEntries(Object.entries(secretsFromStorage || {}).filter(([_, value]) => value != null && value !== '' && value !== undefined))
const finalSecrets = {
...secretFromUserConfig,
...secretsFromStorage,
}
const llmChainParams = await LLMService.getLLMChainParams(body)
const sendSuccessData = (options: Omit<SuccessResponse, 'type'>) => {
return res.write(`data: ${JSON.stringify(buildSuccessResponse(options))}\n\n`)
@@ -90,32 +40,11 @@ export const llmControllers: ControllerConfig = {
return res.write(`data: ${JSON.stringify(buildFailResponse(options))}\n\n`)
}
debug.log('model config', model)
debug.log('model config', llmChainParams.model)
try {
let finalSystemPrompt = systemPromptFromParams || singleFileConfig?.systemPrompt || ''
// provide file context
if (contextFilePaths && finalPath) {
const fileContext = await createFileContext({
rootPath: finalPath,
filePaths: contextFilePaths?.map(toUnixPath),
editingFilePath: toUnixPath(editingFilePath),
})
finalSystemPrompt += `\n${fileContext}\n`
}
finalSystemPrompt += appendSystemPrompt
const llmChain = await getLLMChain({
messages,
systemPrompt: finalSystemPrompt,
systemPromptAsUserPrompt,
model: {
...model!,
secrets: finalSecrets,
},
...llmChainParams,
onTokenStream: (token: string) => {
sendSuccessData({ data: token })
},

View File

@@ -2,7 +2,7 @@ import { getStorage, sendSuccessResponse, verifyParamsByZod } from '@nicepkg/gpt
import type { StorageClearReqParams, StorageClearResData, StorageGetItemReqParams, StorageGetItemResData, StorageRemoveItemReqParams, StorageRemoveItemResData, StorageSetItemReqParams, StorageSetItemResData } from '@nicepkg/gpt-runner-shared/common'
import { ServerStorageName, StorageClearReqParamsSchema, StorageGetItemReqParamsSchema, StorageRemoveItemReqParamsSchema, StorageSetItemReqParamsSchema } from '@nicepkg/gpt-runner-shared/common'
import type { ControllerConfig } from '../types'
import { handleStorageKeySet } from '../services/handle-storage-key-set'
import { handleStorageKeySet } from '../helpers/handle-storage-key-set'
export const storageControllers: ControllerConfig = {
namespacePath: '/storage',

View File

@@ -0,0 +1,17 @@
import type { AppConfig } from '@nicepkg/gpt-runner-shared/common'
import { LocaleLang } from '@nicepkg/gpt-runner-shared/common'
import { cnNotificationConfig, notificationConfig } from './notification.config'
import { releaseConfig } from './release.config'
import { cnVendorsConfig, vendorsConfig } from './vendors.config'
export const appConfig: AppConfig = {
common: {
notificationConfig,
releaseConfig,
vendorsConfig,
},
[LocaleLang.ChineseSimplified]: {
notificationConfig: cnNotificationConfig,
vendorsConfig: cnVendorsConfig,
},
}

View File

@@ -0,0 +1,25 @@
import type { NotificationConfig } from '@nicepkg/gpt-runner-shared/common'
export const notificationConfig: NotificationConfig = {
createAt: '2023-07-24 23:31:22',
title: 'GPT Runner Notification',
message: 'v1.2.0 is release',
}
export const cnNotificationConfig: NotificationConfig = {
createAt: '2023-07-24 23:31:26',
title: 'GPT Runner 通知',
message: `
### 版本更新到了 v1.2.0
1. 重启 vscode 即可去扩展处更新
2. cli 的执行 \`npm i -g gptr\` 即可更新
### 本次功能更新
1. 针对语言为简体中文的用户提供 OpenAI API key 供应商,也就是你可以白嫖了。
2. 点击左上角设置,切换供应商即可。
3. 本次 API Key 由慷慨大方的 \`剑廿三\` 提供,让我们把掌声送给他。
### 交流
1. 想进群交流的加 wechat: \`qq2214962083\`
`,
}

View File

@@ -0,0 +1,14 @@
import type { ChangeLogConfig, ReleaseConfig } from '@nicepkg/gpt-runner-shared/common'
const changeLogs: ChangeLogConfig[] = [
{
releaseDate: '2023-07-24 23:40:59',
version: '1.2.0',
changes: 'fix some bug',
},
]
export const releaseConfig: ReleaseConfig = {
createAt: '2023-07-24 23:41:04',
changeLogs,
}

View File

@@ -0,0 +1,21 @@
import type { VendorsConfig } from '@nicepkg/gpt-runner-shared/common'
import { ChatModelType } from '@nicepkg/gpt-runner-shared/common'
export const vendorsConfig: VendorsConfig = {
createAt: '2023-07-24 23:40:49',
[ChatModelType.Openai]: [],
[ChatModelType.Anthropic]: [],
}
export const cnVendorsConfig: VendorsConfig = {
createAt: '2023-07-24 23:40:49',
[ChatModelType.Openai]: [{
vendorName: 'xabcai',
vendorSecrets: {
basePath: 'https://api.xabcai.com/v1',
// don't forgot it should be base64
apiKey: 'c2stWHZQeGJQMVBySFduZDJFZ0xpa0lKTlQzOTNoc3pZdDdmN0NNZUozSE1pdkw2QVdx',
},
}],
[ChatModelType.Anthropic]: [],
}

View File

@@ -0,0 +1,186 @@
import type { AppConfig, ChatModelType, CommonAppConfig, CurrentAppConfig, GetModelConfigType, LastVisitModalDateRecord, MarkedAsVisitedType } from '@nicepkg/gpt-runner-shared/common'
import { EnvConfig, LocaleLang, ServerStorageName } from '@nicepkg/gpt-runner-shared/common'
import merge from 'lodash-es/merge'
import cloneDeep from 'lodash-es/cloneDeep'
import { getStorage } from '@nicepkg/gpt-runner-shared/node'
import { decode } from 'js-base64'
export class AppConfigService implements CurrentAppConfig {
static _instance: AppConfigService
static REMOTE_CONFIG_LINK = 'https://raw.githubusercontent.com/nicepkg/gpt-runner/main/packages/gpt-runner-web/assets/app-config.json'
static UNSAFE_SECRETS_KEY = ['apiKey', 'accessToken']
static LAST_VISIT_MODAL_DATE_KEY = 'last-visit-modal-date'
private loadAppConfigPromise?: Promise<AppConfig>
private langId: LocaleLang
public showNotificationModal: boolean
public showReleaseModal: boolean
public appConfig?: AppConfig | undefined
public get currentConfig(): CommonAppConfig | undefined {
if (!this.appConfig)
return undefined
return AppConfigService.mergeAndGetCurrentAppConfig(this.appConfig, this.langId)
}
static get instance(): AppConfigService {
if (!AppConfigService._instance)
AppConfigService._instance = new AppConfigService()
return AppConfigService._instance
}
constructor() {
this.langId = LocaleLang.English
this.showNotificationModal = false
this.showReleaseModal = false
}
static async getAppConfig(): Promise<AppConfig> {
return EnvConfig.get('NODE_ENV') === 'development' ? (await import('../../../assets/app-config.json')).default : await fetch(`${AppConfigService.REMOTE_CONFIG_LINK}?timestamp=${Date.now()}`).then(res => res.json())
}
static mergeAndGetCurrentAppConfig(appConfig: AppConfig, currentLang: LocaleLang): CommonAppConfig {
const commonConfig = appConfig.common
const currentLangConfig = appConfig[currentLang]
if (!currentLangConfig)
return commonConfig
const finalAppConfig: CommonAppConfig = merge({}, commonConfig, currentLangConfig)
return finalAppConfig
}
updateLangId(langId: LocaleLang) {
this.langId = langId
}
async getStorage() {
const { storage } = await getStorage(ServerStorageName.GlobalState)
return storage
}
async getCurrentAppConfig(safe = true): Promise<CurrentAppConfig> {
if (!this.appConfig)
await this.loadAppConfig()
await this.updateShouldShowModal()
const result = {
showNotificationModal: this.showNotificationModal,
showReleaseModal: this.showReleaseModal,
currentConfig: this.currentConfig,
}
const unsafeSecretKey = AppConfigService.UNSAFE_SECRETS_KEY
if (result.currentConfig) {
const oldVendorsConfig = result.currentConfig.vendorsConfig
const newVendorsConfig = cloneDeep(oldVendorsConfig)
Object.entries(oldVendorsConfig).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((modelVendorConfig, modelVendorConfigIndex) => {
Object.entries(modelVendorConfig?.vendorSecrets || {}).forEach(([secretKey, secretValue]) => {
if (unsafeSecretKey.includes(secretKey)) {
const decodedSecretValue = decode(String(secretValue || ''))
;(newVendorsConfig as any)[key][modelVendorConfigIndex].vendorSecrets[secretKey] = safe ? '*'.repeat(decodedSecretValue.length) : decodedSecretValue
}
})
})
}
})
result.currentConfig.vendorsConfig = newVendorsConfig
}
return result
}
async getSecretsConfig<T extends ChatModelType>(props: {
modelType: T
vendorName: string
}): Promise<GetModelConfigType<T, 'secrets'> | undefined> {
if (!this.appConfig)
await this.loadAppConfig()
const { modelType, vendorName } = props
const { currentConfig } = await this.getCurrentAppConfig(false)
let result: GetModelConfigType<T, 'secrets'> | undefined
if (currentConfig) {
const modelVendors = currentConfig.vendorsConfig[modelType]
result = modelVendors?.find(vendor => vendor.vendorName === vendorName)?.vendorSecrets
}
return result
}
async loadAppConfig(): Promise<void> {
if (this.appConfig)
return
try {
if (!this.loadAppConfigPromise)
this.loadAppConfigPromise = AppConfigService.getAppConfig()
const appConfig = await this.loadAppConfigPromise
this.appConfig = appConfig || null
}
finally {
this.loadAppConfigPromise = undefined
}
}
async updateShouldShowModal(): Promise<void> {
if (!this.appConfig)
await this.loadAppConfig()
const currentConfig = this.currentConfig
const storage = await this.getStorage()
const lastVisitModalDateRecord: LastVisitModalDateRecord | undefined | null = await storage.get(AppConfigService.LAST_VISIT_MODAL_DATE_KEY)
const notificationDateFromConfig = currentConfig?.notificationConfig?.createAt
const notificationDateFromStorage = lastVisitModalDateRecord?.notificationDate
const releaseDateFromConfig = currentConfig?.releaseConfig?.createAt
const releaseDateFromStorage = lastVisitModalDateRecord?.releaseDate
/**
* date like 2021-02-03 12:30:21
* return a is after b
*/
const dateIsAfter = (a: string | undefined | null, b: string | undefined | null, defaultValue = false) => {
if (!a || !b)
return defaultValue
const aDate = new Date(a).getTime()
const bDate = new Date(b).getTime()
return aDate > bDate
}
this.showNotificationModal = dateIsAfter(notificationDateFromConfig, notificationDateFromStorage, true)
this.showReleaseModal = dateIsAfter(releaseDateFromConfig, releaseDateFromStorage, true)
}
async markedAsVisited(types: MarkedAsVisitedType[]): Promise<void> {
const storage = await this.getStorage()
const currentConfig = this.currentConfig
const notificationDate = currentConfig?.notificationConfig?.createAt
const releaseDate = currentConfig?.releaseConfig?.createAt
const oldValue = await storage.get(AppConfigService.LAST_VISIT_MODAL_DATE_KEY) || {}
const newValue = {
...oldValue,
}
if (types.includes('notificationDate'))
newValue.notificationDate = notificationDate
if (types.includes('releaseDate'))
newValue.releaseDate = releaseDate
await storage.set(AppConfigService.LAST_VISIT_MODAL_DATE_KEY, newValue)
}
}

View File

@@ -0,0 +1,127 @@
import { ChatModelType, type ChatStreamReqParams, type SingleFileConfig, toUnixPath } from '@nicepkg/gpt-runner-shared/common'
import type { LLMChainParams } from '@nicepkg/gpt-runner-core'
import { createFileContext, getSecrets, loadUserConfig, parseGptFile } from '@nicepkg/gpt-runner-core'
import { PathUtils } from '@nicepkg/gpt-runner-shared/node'
import { getValidFinalPath } from '../helpers/valid-path'
import { AppConfigService } from './app-config.service'
export class LLMService {
static async getLLMChainParams(params: ChatStreamReqParams): Promise<LLMChainParams> {
const {
messages = [],
systemPrompt: systemPromptFromParams = '',
singleFilePath,
singleFileConfig: singleFileConfigFromParams,
appendSystemPrompt = '',
systemPromptAsUserPrompt = false,
contextFilePaths,
editingFilePath,
overrideModelType,
modelTypeVendorNameMap,
overrideModelsConfig,
rootPath,
} = params
const finalPath = getValidFinalPath({
path: rootPath,
assertType: 'directory',
fieldName: 'rootPath',
})
const { config: userConfig } = await loadUserConfig(finalPath)
let singleFileConfig: SingleFileConfig | undefined = singleFileConfigFromParams
if (singleFilePath && PathUtils.isFile(singleFilePath)) {
// keep realtime config
singleFileConfig = await parseGptFile({
filePath: singleFilePath,
userConfig,
})
}
if (overrideModelType && overrideModelType !== singleFileConfig?.model?.type) {
singleFileConfig = {
model: {
type: overrideModelType,
},
}
}
const model = {
type: ChatModelType.Openai,
...singleFileConfig?.model,
...overrideModelsConfig?.[singleFileConfig?.model?.type as ChatModelType || ''],
} as SingleFileConfig['model']
const secretFromUserConfig = userConfig.model?.type === model?.type ? userConfig.model?.secrets : undefined
let secretsFromStorage = await getSecrets(model?.type as ChatModelType || null)
// if some secret value is '' or null or undefined, should remove
secretsFromStorage = Object.fromEntries(Object.entries(secretsFromStorage || {}).filter(([_, value]) => value != null && value !== '' && value !== undefined))
// if user use vendor secrets
const currentVendorName = modelTypeVendorNameMap?.[model!.type] || ''
const vendorSecrets = await AppConfigService.instance.getSecretsConfig({
modelType: model!.type,
vendorName: currentVendorName,
})
const finalSecrets = vendorSecrets || {
...secretFromUserConfig,
...secretsFromStorage,
}
const finalSystemPrompt = await LLMService.getFinalSystemPrompt({
finalPath,
systemPrompt: systemPromptFromParams,
singleFileConfig,
appendSystemPrompt,
contextFilePaths,
editingFilePath,
})
return {
messages,
systemPrompt: finalSystemPrompt,
systemPromptAsUserPrompt,
model: {
...model!,
secrets: finalSecrets,
},
}
}
static async getFinalSystemPrompt(params: {
finalPath: string
systemPrompt: string
singleFileConfig?: SingleFileConfig
appendSystemPrompt: string
contextFilePaths?: string[]
editingFilePath?: string
}): Promise<string> {
const {
finalPath,
systemPrompt: systemPromptFromParams = '',
singleFileConfig,
appendSystemPrompt = '',
contextFilePaths,
editingFilePath,
} = params
let finalSystemPrompt = systemPromptFromParams || singleFileConfig?.systemPrompt || ''
// provide file context
if (contextFilePaths && finalPath) {
const fileContext = await createFileContext({
rootPath: finalPath,
filePaths: contextFilePaths?.map(toUnixPath),
editingFilePath: toUnixPath(editingFilePath),
})
finalSystemPrompt += `\n${fileContext}\n`
}
finalSystemPrompt += appendSystemPrompt
return finalSystemPrompt
}
}

1726
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff