From e7cc6b1efc5844e517813c3ace72d75935494b08 Mon Sep 17 00:00:00 2001 From: 0fatal <2816813070@qq.com> Date: Tue, 27 Feb 2024 18:32:20 +0800 Subject: [PATCH] feat(web): support ai code completion --- web/package-lock.json | 21 ++ web/package.json | 1 + web/public/locales/en/translation.json | 5 +- web/public/locales/zh-CN/translation.json | 7 +- web/public/locales/zh/translation.json | 7 +- web/src/components/Editor/FunctionEditor.tsx | 19 +- web/src/components/Editor/TSEditor.tsx | 25 ++- .../SysSetting/CommonSetting/index.tsx | 183 +++++++++++------- web/src/pages/customSetting.ts | 2 + web/src/pages/siteSetting.ts | 1 + 10 files changed, 190 insertions(+), 81 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 7623828ef1..c23b5ce894 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -53,6 +53,7 @@ "react-icons": "^4.8.0", "react-image-crop": "^10.1.5", "react-markdown": "^8.0.7", + "react-monaco-copilot": "^1.0.3", "react-router-dom": "^6.11.2", "react-syntax-highlighter": "^15.5.0", "react-window": "^1.8.10", @@ -9607,6 +9608,20 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/react-monaco-copilot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/react-monaco-copilot/-/react-monaco-copilot-1.0.3.tgz", + "integrity": "sha512-KdIWH5ycKi6AdO+2Fl6rYWfxlPmJSWBPAyIW7uyeJ7yi3GiBFid0dILCIJOvKtwi6IcCxkfFiYV9M6fA9aHC8A==", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "axios": ">=1.0.0", + "monaco-editor": ">=0.41.0", + "react": ">=17.0.2", + "react-dom": ">=17.0.2" + } + }, "node_modules/react-onclickoutside": { "version": "6.12.2", "resolved": "https://mirrors.tencent.com/npm/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz", @@ -19240,6 +19255,12 @@ "integrity": "sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==", "dev": true }, + "react-monaco-copilot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/react-monaco-copilot/-/react-monaco-copilot-1.0.3.tgz", + "integrity": "sha512-KdIWH5ycKi6AdO+2Fl6rYWfxlPmJSWBPAyIW7uyeJ7yi3GiBFid0dILCIJOvKtwi6IcCxkfFiYV9M6fA9aHC8A==", + "requires": {} + }, "react-onclickoutside": { "version": "6.12.2", "resolved": "https://mirrors.tencent.com/npm/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz", diff --git a/web/package.json b/web/package.json index 47ee9a0c27..37ad830244 100644 --- a/web/package.json +++ b/web/package.json @@ -58,6 +58,7 @@ "react-icons": "^4.8.0", "react-image-crop": "^10.1.5", "react-markdown": "^8.0.7", + "react-monaco-copilot": "^1.0.3", "react-router-dom": "^6.11.2", "react-syntax-highlighter": "^15.5.0", "react-window": "^1.8.10", diff --git a/web/public/locales/en/translation.json b/web/public/locales/en/translation.json index fb390a7b51..8635c36ae7 100644 --- a/web/public/locales/en/translation.json +++ b/web/public/locales/en/translation.json @@ -239,7 +239,7 @@ "FuncListDisplay": "Function List Display", "ListDisplay": "List Display", "EditorLanguageServer": "Editor Language Server", - "isOpenLanguageServer": "Enable or Close Language server (only effective when the app config is above 0.5C 1G)", + "isOpenLanguageServer": "Enable typescript language server (only effective when the spec of the app is above 0.5C 1G)", "Editor": "Editor", "FuncList": "Function List", "EditorMode": "Editor Mode", @@ -247,7 +247,8 @@ "MonitorSetting": "Resource Monitor", "DatabaseMonitor": "Database monitor", "RuntimeMonitor": "Runtime monitor", - "ClientSetting": "Client settings" + "ClientSetting": "Client settings", + "AICompletion": "Enable code intelligent completion (need refresh)" }, "StoragePanel": { "All": "Total Capacity", diff --git a/web/public/locales/zh-CN/translation.json b/web/public/locales/zh-CN/translation.json index e98a0f5987..b2622015cc 100644 --- a/web/public/locales/zh-CN/translation.json +++ b/web/public/locales/zh-CN/translation.json @@ -239,7 +239,7 @@ "FuncListDisplay": "函数列表显示", "ListDisplay": "列表显示", "EditorLanguageServer": "编辑器语言服务", - "isOpenLanguageServer": "是否开启语言服务(仅在应用配置高于 0.5C 1G 时生效)", + "isOpenLanguageServer": "是否开启 TypeScript 语言服务(体验更好的类型提示,仅在应用配置高于 0.5C 1G 时生效)", "Editor": "编辑器", "FuncList": "函数列表", "EditorMode": "使用编辑器模式", @@ -247,7 +247,8 @@ "MonitorSetting": "资源监控", "DatabaseMonitor": "数据库监控", "RuntimeMonitor": "运行时监控", - "ClientSetting": "客户端设置" + "ClientSetting": "客户端设置", + "AICompletion": "是否开启代码智能补全(需刷新生效)" }, "StoragePanel": { "All": "总容量", @@ -757,4 +758,4 @@ "Title": "Laf 新版本已经准备好了!", "Description": "点击立即更新" } -} +} \ No newline at end of file diff --git a/web/public/locales/zh/translation.json b/web/public/locales/zh/translation.json index 47bcfd413a..1703f8eb80 100644 --- a/web/public/locales/zh/translation.json +++ b/web/public/locales/zh/translation.json @@ -239,7 +239,7 @@ "FuncListDisplay": "函数列表显示", "ListDisplay": "列表显示", "EditorLanguageServer": "编辑器语言服务", - "isOpenLanguageServer": "是否开启语言服务(仅在应用配置高于 0.5C 1G 时生效)", + "isOpenLanguageServer": "是否开启 TypeScript 语言服务(体验更好的类型提示,仅在应用配置高于 0.5C 1G 时生效)", "Editor": "编辑器", "FuncList": "函数列表", "EditorMode": "使用编辑器模式", @@ -247,7 +247,8 @@ "MonitorSetting": "资源监控", "DatabaseMonitor": "数据库监控", "RuntimeMonitor": "运行时监控", - "ClientSetting": "客户端设置" + "ClientSetting": "客户端设置", + "AICompletion": "是否开启代码智能补全(需刷新生效)" }, "StoragePanel": { "All": "总容量", @@ -757,4 +758,4 @@ "Title": "Laf 新版本已经准备好了!", "Description": "点击立即更新" } -} +} \ No newline at end of file diff --git a/web/src/components/Editor/FunctionEditor.tsx b/web/src/components/Editor/FunctionEditor.tsx index 4b3c046a9f..0478b84b78 100644 --- a/web/src/components/Editor/FunctionEditor.tsx +++ b/web/src/components/Editor/FunctionEditor.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useRef, useState } from "react"; +import { useCompletionFeature } from "react-monaco-copilot"; import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; import { RegisteredFileSystemProvider, @@ -15,7 +16,9 @@ import { createUrl, createWebSocketAndStartClient, performInit } from "./Languag import { TFunction } from "@/apis/typing"; import useFunctionCache from "@/hooks/useFunctionCache"; import useFunctionStore from "@/pages/app/functions/store"; +import useCustomSettingStore from "@/pages/customSetting"; import useGlobalStore from "@/pages/globalStore"; +import useSiteSettingStore from "@/pages/siteSetting"; export const fileSystemProvider = new RegisteredFileSystemProvider(false); registerFileSystemOverlay(1, fileSystemProvider); @@ -37,11 +40,14 @@ function FunctionEditor(props: { }) { const { onChange, path, className, colorMode = COLOR_MODE.light, fontSize = 14, value } = props; - const editorRef = useRef(); + const editorRef = useRef(); const subscriptionRef = useRef(undefined); const monacoEl = useRef(null); const globalStore = useGlobalStore((state) => state); const { allFunctionList, setLSPStatus, LSPStatus } = useFunctionStore((state) => state); + const { commonSettings } = useCustomSettingStore(); + const { siteSettings } = useSiteSettingStore(); + const functionCache = useFunctionCache(); const [functionList, setFunctionList] = useState(allFunctionList); const baseUrl = globalStore.currentApp.host; @@ -53,6 +59,13 @@ function FunctionEditor(props: { } }, [baseUrl]); + const aiCompleteUrl = siteSettings.ai_complete_url?.value; + const completionFeature = useCompletionFeature({ + monaco: monaco, + editor: editorRef.current, + apiUrl: aiCompleteUrl || "", + }); + useEffect(() => { const startLSP = () => { const lspWebSocket = createWebSocketAndStartClient(url, globalStore.currentApp.develop_token); @@ -152,6 +165,10 @@ function FunctionEditor(props: { keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyP, command: null, }); + + if (commonSettings.useCopilot && aiCompleteUrl) { + completionFeature.onMounted(); + } }); } diff --git a/web/src/components/Editor/TSEditor.tsx b/web/src/components/Editor/TSEditor.tsx index 5c389636b0..578650b454 100644 --- a/web/src/components/Editor/TSEditor.tsx +++ b/web/src/components/Editor/TSEditor.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef } from "react"; +import { useCompletionFeature } from "react-monaco-copilot"; import { Spinner } from "@chakra-ui/react"; import { Editor, Monaco } from "@monaco-editor/react"; import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; @@ -10,6 +11,8 @@ import "./useWorker"; import useFunctionCache from "@/hooks/useFunctionCache"; import useFunctionStore from "@/pages/app/functions/store"; +import useCustomSettingStore from "@/pages/customSetting"; +import useSiteSettingStore from "@/pages/siteSetting"; const autoImportTypings = new AutoImportTypings(); @@ -24,14 +27,23 @@ export default function TSEditor(props: { const functionCache = useFunctionCache(); const { currentFunction, allFunctionList } = useFunctionStore((state) => state); + const { commonSettings } = useCustomSettingStore(); + const { siteSettings } = useSiteSettingStore(); + + const aiCompleteUrl = siteSettings.ai_complete_url?.value; const monacoRef = useRef(); const editorRef = useRef(); - const loadModalsRef = useRef(loadModals); + const loadModelsRef = useRef(loadModels); + const completionFeature = useCompletionFeature({ + monaco: monacoRef.current, + editor: editorRef.current, + apiUrl: aiCompleteUrl || "", + }); - loadModalsRef.current = loadModals; + loadModelsRef.current = loadModels; - function loadModals(monaco: Monaco) { + function loadModels(monaco: Monaco) { allFunctionList.forEach((item: any) => { const uri = monaco.Uri.file(`${RUNTIMES_PATH}/${item.name}.ts`); if (!monaco.editor.getModel(uri)) { @@ -47,15 +59,18 @@ export default function TSEditor(props: { function handleEditorDidMount(editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) { monacoRef.current = monaco; editorRef.current = editor; + if (commonSettings.useCopilot && aiCompleteUrl) { + completionFeature.onMounted(); + } setTimeout(() => { - loadModalsRef.current(monacoRef.current!); + loadModelsRef.current(monacoRef.current!); autoImportTypings.loadDefaults(monacoRef.current); }, 10); } useEffect(() => { if (monacoRef.current) { - loadModalsRef.current(monacoRef.current!); + loadModelsRef.current(monacoRef.current!); } }, [allFunctionList]); diff --git a/web/src/pages/app/setting/SysSetting/CommonSetting/index.tsx b/web/src/pages/app/setting/SysSetting/CommonSetting/index.tsx index 7a3dff4c20..0cb4498fe4 100644 --- a/web/src/pages/app/setting/SysSetting/CommonSetting/index.tsx +++ b/web/src/pages/app/setting/SysSetting/CommonSetting/index.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Button, Divider, Select, Switch, useColorMode } from "@chakra-ui/react"; +import { Button, Divider, HStack, Select, Switch, useColorMode, VStack } from "@chakra-ui/react"; import clsx from "clsx"; import useCustomSettingStore from "@/pages/customSetting"; @@ -21,75 +21,124 @@ export default function CommonSetting() { return (
-
- {t("SettingPanel.Editor")} - - {t("SettingPanel.isOpenLanguageServer")} - -
- { - setCurrentCommonSettings({ - ...currentCommonSettings, - useLSP: !currentCommonSettings.useLSP, - }); - }} - variant={"primary"} - colorScheme="primary" - /> -
-
-
- - {t("SettingPanel.FontSize")} - -
- -
+
+
{t("SettingPanel.Editor")}
+ + + + {t("SettingPanel.isOpenLanguageServer")} + + { + setCurrentCommonSettings({ + ...currentCommonSettings, + useLSP: !currentCommonSettings.useLSP, + }); + }} + variant={"primary"} + colorScheme="primary" + /> + + + + {t("SettingPanel.AICompletion")} + + { + setCurrentCommonSettings({ + ...currentCommonSettings, + useCopilot: !currentCommonSettings.useCopilot, + }); + }} + variant={"primary"} + colorScheme="primary" + /> + + + + {t("SettingPanel.FontSize")} + + + +
-
- {t("SettingPanel.FuncList")} - - {t("SettingPanel.ListDisplay")} - -
- -
+
+
{t("SettingPanel.FuncList")}
+ + + + {t("SettingPanel.ListDisplay")} + + + +
-
diff --git a/web/src/pages/customSetting.ts b/web/src/pages/customSetting.ts index 9820daa359..4131e688d1 100644 --- a/web/src/pages/customSetting.ts +++ b/web/src/pages/customSetting.ts @@ -27,6 +27,7 @@ type TCommonSettings = { fontSize: number; funcListDisplay: string; useLSP: boolean; + useCopilot: boolean; }; type State = { @@ -177,6 +178,7 @@ const useCustomSettingStore = create()( fontSize: 14, funcListDisplay: "name", useLSP: true, + useCopilot: true, }, setCommonSettings: (settings) => { diff --git a/web/src/pages/siteSetting.ts b/web/src/pages/siteSetting.ts index 56a5f05d20..db13570278 100644 --- a/web/src/pages/siteSetting.ts +++ b/web/src/pages/siteSetting.ts @@ -10,6 +10,7 @@ type SITE_KEY = | "id_verify" | "site_url" | "ai_pilot_url" + | "ai_complete_url" | "laf_forum_url" | "laf_business_url" | "laf_discord_url"