feat(gpt-runner-web): add third-party api providers and notification
This commit is contained in:
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"jsynowiec.vscode-insertdatestring",
|
||||
"EditorConfig.EditorConfig",
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
16
package.json
16
package.json
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
74
packages/gpt-runner-shared/src/common/types/app-config.ts
Normal file
74
packages/gpt-runner-shared/src/common/types/app-config.ts
Normal 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
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './app-config'
|
||||
export * from './client'
|
||||
export * from './common-file'
|
||||
export * from './common'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
44
packages/gpt-runner-web/assets/app-config.json
Normal file
44
packages/gpt-runner-web/assets/app-config.json
Normal 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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "保存しない場合、変更は失われます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "如果你不保存,你的改动将会丢失."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "如果你不保存,你的改動將會丟失."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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'
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
102
packages/gpt-runner-web/client/src/views/layout/index.tsx
Normal file
102
packages/gpt-runner-web/client/src/views/layout/index.tsx
Normal 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'
|
||||
@@ -1 +1 @@
|
||||
export * from './common'
|
||||
export * from './dist/common'
|
||||
|
||||
@@ -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"
|
||||
|
||||
23
packages/gpt-runner-web/scripts/build-app-config-json.ts
Normal file
23
packages/gpt-runner-web/scripts/build-app-config-json.ts
Normal 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)
|
||||
})
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 })
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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\`
|
||||
`,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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]: [],
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
127
packages/gpt-runner-web/server/src/services/llm.service.ts
Normal file
127
packages/gpt-runner-web/server/src/services/llm.service.ts
Normal 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
1726
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user