diff --git a/backend/src/intelligence/prompt/followup.ts b/backend/src/intelligence/prompt/followup.ts index b03cdcff..ffd2b276 100644 --- a/backend/src/intelligence/prompt/followup.ts +++ b/backend/src/intelligence/prompt/followup.ts @@ -1,4 +1,4 @@ -import { ChatPromptTemplate, MessagesPlaceholder } from "langchain/prompts"; +import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts"; export const followUpPromptTemplate = ChatPromptTemplate.fromMessages([ [ diff --git a/backend/src/intelligence/prompt/github-issue.ts b/backend/src/intelligence/prompt/github-issue.ts index 76f59827..8d9e9cef 100644 --- a/backend/src/intelligence/prompt/github-issue.ts +++ b/backend/src/intelligence/prompt/github-issue.ts @@ -1,7 +1,7 @@ -import { ChatPromptTemplate, FewShotChatMessagePromptTemplate } from "langchain/prompts"; +import { ChatPromptTemplate, FewShotChatMessagePromptTemplate } from "@langchain/core/prompts"; const examplePrompt = ChatPromptTemplate.fromTemplate( - "## Title\n{title}\n## Issue Type\n{issueType}\n## Content\n{content}" + "[Example]\n## Title\n{title}\n## Issue Type\n{issueType}\n## Content\n{content}" ); const examples = [ @@ -73,8 +73,9 @@ Keep the product simple`, ]; export const githubIssuePromptTemplate = new FewShotChatMessagePromptTemplate({ - prefix: `I want you to act as a GitHub Issue writer. I will provide brief information about the GitHub issue I want to create, and you should write the GitHub issue based on the examples I provide. - The types of issues you can write are bug 🐞 or enhancement 🌟. Please ensure that you follow the template used in each type of issue example provided. Please write your responses in English.`, + prefix: `I want you to act as a GitHub Issue writer. I will provide brief information about the GitHub issue I want to create, and you should write the GitHub issue. +The types of issues you can write are bug 🐞 or enhancement 🌟. Please ensure that you follow the template used in each type of issue example provided. Do not provide the example as it is. Please write your responses in English. +If there is insufficient information to create the issue, request additional information.`, suffix: "Brief information about the GitHub issue: {content}", examplePrompt, examples, diff --git a/backend/src/intelligence/prompt/github-pr.ts b/backend/src/intelligence/prompt/github-pr.ts index 66443aed..9f236347 100644 --- a/backend/src/intelligence/prompt/github-pr.ts +++ b/backend/src/intelligence/prompt/github-pr.ts @@ -1,6 +1,8 @@ -import { ChatPromptTemplate, FewShotChatMessagePromptTemplate } from "langchain/prompts"; +import { ChatPromptTemplate, FewShotChatMessagePromptTemplate } from "@langchain/core/prompts"; -const examplePrompt = ChatPromptTemplate.fromTemplate("## Title\n{title}\n## Content\n{content}"); +const examplePrompt = ChatPromptTemplate.fromTemplate( + "[Example]\n## Title\n{title}\n## Content\n{content}" +); const examples = [ { @@ -100,10 +102,9 @@ Fixes # ]; export const githubPrPromptTemplate = new FewShotChatMessagePromptTemplate({ - prefix: `I want you to act as a GitHub PR Writer for me. I'll provide you with brief notes about GitHub PR, and you just need to write the PR using the examples I've provided. -Make sure to adhere to the template that we commonly follow in Example. -If the information is not provided by the user, please refrain from attaching document links found elsewhere. Please respond in English. -Please refer to the example for guidance, but generate results based on the information provided in the Brief Information section.`, + prefix: `I want you to act as a GitHub PR Writer for me. I'll provide you with brief notes about GitHub PR, and you just need to write the PR. +Please ensure that you follow the template used in example provided. Do not provide the example as it is. Please write your responses in English. +If there is insufficient information to create the PR, request additional information.`, suffix: "Brief information about the GitHub PR: {content}", examplePrompt, examples, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 22771ef8..dad1a08b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -33,6 +33,7 @@ "color": "^4.2.3", "lib0": "^0.2.88", "lodash": "^4.17.21", + "match-sorter": "^6.3.3", "moment": "^2.30.1", "notistack": "^2.0.8", "randomcolor": "^0.6.2", @@ -47,7 +48,7 @@ "react-social-login-buttons": "^3.9.1", "react-use": "^17.5.0", "redux-persist": "^6.0.0", - "yorkie-js-sdk": "^0.4.13" + "yorkie-js-sdk": "^0.4.15-rc" }, "devDependencies": { "@types/color": "^3.0.6", @@ -715,70 +716,6 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", - "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", - "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", - "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", - "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.19.11", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", @@ -795,294 +732,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", - "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", - "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", - "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", - "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", - "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", - "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", - "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", - "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", - "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", - "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", - "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", - "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", - "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", - "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", - "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", - "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", - "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", - "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1813,32 +1462,6 @@ "node": ">=14.0.0" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", - "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", - "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.9.6", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", @@ -1852,136 +1475,6 @@ "darwin" ] }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", - "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", - "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", - "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", - "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", - "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", - "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", - "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", - "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", - "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", - "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@swc/helpers": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", @@ -4634,6 +4127,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/match-sorter": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.3.tgz", + "integrity": "sha512-sgiXxrRijEe0SzHKGX4HouCpfHRPnqteH42UdMEW7BlWy990ZkzcvonJGv4Uu9WE7Y1f8Yocm91+4qFPCbmNww==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", @@ -6445,6 +5947,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + }, "node_modules/reselect": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.0.tgz", @@ -7267,9 +6774,9 @@ } }, "node_modules/yorkie-js-sdk": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/yorkie-js-sdk/-/yorkie-js-sdk-0.4.13.tgz", - "integrity": "sha512-6Im/SRxJoGcUOf5nHIQDDZnKbLBDUgD66xZxcrAXvZS4KblQnRU54/MdloIF9BzNJlJGwWSk9KjgYwLY73nntg==", + "version": "0.4.15-rc", + "resolved": "https://registry.npmjs.org/yorkie-js-sdk/-/yorkie-js-sdk-0.4.15-rc.tgz", + "integrity": "sha512-phH4zcT7qr908dclFtgs57fPT7F5sBdEtc+5VGL6kAEUOsIONtz/cPLsJCE2+RkGgCFQ8iCevhl9rr1dxE9kWA==", "dependencies": { "@types/google-protobuf": "^3.15.5", "@types/long": "^4.0.1", diff --git a/frontend/package.json b/frontend/package.json index 85b002c1..e527458d 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "codemirror-toolbar": "^0.0.3", "color": "^4.2.3", "lib0": "^0.2.88", + "match-sorter": "^6.3.3", "lodash": "^4.17.21", "moment": "^2.30.1", "notistack": "^2.0.8", @@ -51,7 +52,7 @@ "react-social-login-buttons": "^3.9.1", "react-use": "^17.5.0", "redux-persist": "^6.0.0", - "yorkie-js-sdk": "^0.4.13" + "yorkie-js-sdk": "^0.4.15-rc" }, "devDependencies": { "@types/color": "^3.0.6", diff --git a/frontend/public/yorkie.png b/frontend/public/yorkie.png new file mode 100644 index 00000000..6f57b5f0 Binary files /dev/null and b/frontend/public/yorkie.png differ diff --git a/frontend/src/components/editor/DocumentView.tsx b/frontend/src/components/editor/DocumentView.tsx index 8f3c6d01..a9bbd3cf 100644 --- a/frontend/src/components/editor/DocumentView.tsx +++ b/frontend/src/components/editor/DocumentView.tsx @@ -43,7 +43,7 @@ function DocumentView() { width: 8, borderRadius: 0, cursor: "col-resize", - zIndex: 100, + zIndex: 0, }} />
(); const editorStore = useSelector(selectEditor); @@ -42,6 +44,7 @@ function Editor() { }), EditorView.lineWrapping, keymap.of([indentWithTab]), + intelligencePivot, ], }); @@ -49,11 +52,12 @@ function Editor() { state, parent: element, }); + dispatch(setCmView(view)); return () => { view?.destroy(); }; - }, [editorStore.client, editorStore.doc, element, themeMode]); + }, [dispatch, editorStore.client, editorStore.doc, element, themeMode]); return (
{ const editorText = editorStore.doc?.getRoot().content?.toString() || ""; // Add soft line break - setContent( - editorText - .split("\n") - .map((line) => line + " ") - .join("\n") - ); + setContent(addSoftLineBreak(editorText)); }; updatePreviewContent(); @@ -46,9 +42,13 @@ function Preview() { return ( ); diff --git a/frontend/src/components/editor/YorkieIntelligence.tsx b/frontend/src/components/editor/YorkieIntelligence.tsx new file mode 100644 index 00000000..1fcac643 --- /dev/null +++ b/frontend/src/components/editor/YorkieIntelligence.tsx @@ -0,0 +1,85 @@ +import { Card, CardActionArea, Fade, Popper, Stack, Typography, useTheme } from "@mui/material"; +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import { useDebounce } from "react-use"; +import { INTELLIGENCE_FOOTER_ID, INTELLIGENCE_HEADER_ID } from "../../constants/intelligence"; +import YorkieIntelligenceFooter from "./YorkieIntelligenceFooter"; + +function YorkieIntelligence() { + const theme = useTheme(); + const [footerOpen, setFooterOpen] = useState(false); + const [intelligenceHeaderPivot, setIntelligenceHeaderPivot] = useState(null); + const [intelligenceFooterPivot, setIntelligenceFooterPivot] = useState(null); + const [debouncedPivot, setDebouncedPivot] = useState(null); + + useDebounce( + () => { + setDebouncedPivot(intelligenceHeaderPivot); + }, + 500, + [intelligenceHeaderPivot] + ); + + useEffect(() => { + document.addEventListener("selectionchange", function () { + const intelligenceHeaderPivot = document.getElementById(INTELLIGENCE_HEADER_ID); + const intelligenceFooterPivot = document.getElementById(INTELLIGENCE_FOOTER_ID); + setIntelligenceHeaderPivot(intelligenceHeaderPivot); + setIntelligenceFooterPivot(intelligenceFooterPivot); + + if (!intelligenceHeaderPivot) { + setFooterOpen(false); + setDebouncedPivot(null); + } + }); + }, []); + + const handleFooterOpen = () => { + setFooterOpen((prev) => !prev); + }; + + if (!debouncedPivot || !intelligenceFooterPivot) return; + + return ( + <> + + {({ TransitionProps }) => ( + + + + + + Yorkie Intelligence + + + + + )} + + {footerOpen && + createPortal( + , + intelligenceFooterPivot + )} + + ); +} + +export default YorkieIntelligence; diff --git a/frontend/src/components/editor/YorkieIntelligenceFeature.tsx b/frontend/src/components/editor/YorkieIntelligenceFeature.tsx new file mode 100644 index 00000000..c04ee28b --- /dev/null +++ b/frontend/src/components/editor/YorkieIntelligenceFeature.tsx @@ -0,0 +1,215 @@ +import { + Box, + Button, + CircularProgress, + Fade, + FormControl, + IconButton, + InputAdornment, + Stack, + Typography, + useTheme, +} from "@mui/material"; +import { INTELLIGENCE_FOOTER_ID, IntelligenceFeature } from "../../constants/intelligence"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import { FormContainer, TextFieldElement, useForm } from "react-hook-form-mui"; +import SendIcon from "@mui/icons-material/Send"; +import { useIntelligenceFeatureStream, useIntelligenceStream } from "../../hooks/api/intelligence"; +import { useEffect, useMemo, useRef, useState } from "react"; +import clipboard from "clipboardy"; +import { useSnackbar } from "notistack"; +import MarkdownPreview from "@uiw/react-markdown-preview"; +import { useCurrentTheme } from "../../hooks/useCurrentTheme"; +import { addSoftLineBreak } from "../../utils/document"; +import { useSelector } from "react-redux"; +import { selectEditor } from "../../store/editorSlice"; + +interface YorkieIntelligenceFeatureProps { + title: string; + feature: IntelligenceFeature; + onClose: () => void; +} + +function YorkieIntelligenceFeature(props: YorkieIntelligenceFeatureProps) { + const { title, feature, onClose } = props; + const theme = useTheme(); + const currentTheme = useCurrentTheme(); + const editorStore = useSelector(selectEditor); + const { + data: featureData, + memoryKey, + isLoading: isFeatureLoading, + isComplete: isFeatureComplete, + mutateAsync: mutateIntelligenceFeature, + } = useIntelligenceFeatureStream(feature); + const { + data: followUpData, + isLoading: isFollowUpLoading, + isComplete: isFollowUpComplete, + mutateAsync: mutateIntelligence, + } = useIntelligenceStream(memoryKey); + const [content, setContent] = useState(""); + const intelligenceFooterPivot = document.getElementById(INTELLIGENCE_FOOTER_ID); + const isLoading = useMemo( + () => isFeatureLoading || isFollowUpLoading, + [isFeatureLoading, isFollowUpLoading] + ); + const isComplete = useMemo( + () => isFeatureComplete || isFollowUpComplete, + [isFeatureComplete, isFollowUpComplete] + ); + const data = useMemo(() => followUpData || featureData, [featureData, followUpData]); + const { enqueueSnackbar } = useSnackbar(); + const markdownPreviewRef = useRef(null); + const formContext = useForm<{ content: string }>(); + const { reset, formState } = formContext; + + useEffect(() => { + if (formState.isSubmitSuccessful) { + reset({ content: "" }); + } + }, [formState.isSubmitSuccessful, reset]); + + useEffect(() => { + setContent(intelligenceFooterPivot?.getAttribute("content") ?? ""); + }, [intelligenceFooterPivot]); + + useEffect(() => { + if (!content) return; + + mutateIntelligenceFeature(content); + }, [content, mutateIntelligenceFeature]); + + useEffect(() => { + if (data && markdownPreviewRef.current) { + markdownPreviewRef.current.scrollTo({ + behavior: "smooth", + top: markdownPreviewRef.current.scrollHeight, + }); + } + }, [data]); + + const handleCopyContent = async () => { + if (!data) return; + + await clipboard.write(data); + enqueueSnackbar("URL Copied!", { variant: "success" }); + }; + + const handleRetry = async () => { + mutateIntelligence( + "Recreate the last statement with a paraphrase or adjust it slightly to better suit the user's input." + ); + }; + + const handleRequestSubmit = (data: { content: string }) => { + mutateIntelligence(data.content); + }; + + const handleAddContent = (replace: boolean = false) => { + if (!editorStore.cmView) return; + const selection = editorStore.cmView.state.selection.main; + let from = Math.min(selection.to, selection.from); + const to = Math.max(selection.to, selection.from); + let insert = data as string; + + if (!replace) { + from = to; + insert = `\n${insert}`; + } + + const selectionFrom = replace ? from : from + 1; + const selectionTo = from + insert.length; + + editorStore.cmView?.dispatch({ + changes: { from, to, insert }, + selection: { + anchor: selectionFrom, + head: selectionTo, + }, + }); + editorStore.doc?.update((root, presence) => { + root.content.edit(from, to, insert); + presence.set({ + selection: root.content.indexRangeToPosRange([selectionFrom, selectionTo]), + }); + }); + onClose(); + }; + + return ( + + + {title} + + {isLoading && } + + {!isLoading && ( + + )} + + + + + + + + + + + + + + + + + + ), + endAdornment: ( + + + + + + + + ), + }} + /> + + + + + + ); +} + +export default YorkieIntelligenceFeature; diff --git a/frontend/src/components/editor/YorkieIntelligenceFeatureList.tsx b/frontend/src/components/editor/YorkieIntelligenceFeatureList.tsx new file mode 100644 index 00000000..feb9c640 --- /dev/null +++ b/frontend/src/components/editor/YorkieIntelligenceFeatureList.tsx @@ -0,0 +1,63 @@ +import { ListItemIcon, ListItemText, MenuItem, MenuList, Stack, TextField } from "@mui/material"; +import { useMemo, useState } from "react"; +import GitHubIcon from "@mui/icons-material/GitHub"; +import { matchSorter } from "match-sorter"; +import { IntelligenceFeature } from "../../constants/intelligence"; + +const featureInfoList = [ + { + title: "Write GitHub Issue", + icon: , + feature: IntelligenceFeature.GITHUB_ISSUE, + }, + { + title: "Write GitHub Pull Request", + icon: , + feature: IntelligenceFeature.GITHUB_PR, + }, +]; + +interface YorkieIntelligenceFeatureListProps { + onSelectFeature: (feature: IntelligenceFeature, title: string) => void; +} + +function YorkieIntelligenceFeatureList(props: YorkieIntelligenceFeatureListProps) { + const { onSelectFeature } = props; + const [featureText, setFeatureText] = useState(""); + const filteredFeatureInfoList = useMemo(() => { + return matchSorter(featureInfoList, featureText, { keys: ["title", "feature"] }); + }, [featureText]); + + const handleFeatureTextChange: React.ChangeEventHandler< + HTMLInputElement | HTMLTextAreaElement + > = (e) => { + setFeatureText(e.target.value); + }; + + return ( + + + + {filteredFeatureInfoList.map((featureInfo) => ( + onSelectFeature(featureInfo.feature, featureInfo.title)} + > + {featureInfo.icon} + {featureInfo.title} + + ))} + + + ); +} + +export default YorkieIntelligenceFeatureList; diff --git a/frontend/src/components/editor/YorkieIntelligenceFooter.tsx b/frontend/src/components/editor/YorkieIntelligenceFooter.tsx new file mode 100644 index 00000000..3f757c40 --- /dev/null +++ b/frontend/src/components/editor/YorkieIntelligenceFooter.tsx @@ -0,0 +1,104 @@ +import { Box, Card, Popover, useTheme } from "@mui/material"; +import YorkieIntelligenceFeatureList from "./YorkieIntelligenceFeatureList"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { IntelligenceFeature } from "../../constants/intelligence"; +import YorkieIntelligenceFeature from "./YorkieIntelligenceFeature"; +import { useSelector } from "react-redux"; +import { selectEditor } from "../../store/editorSlice"; +import CloseIntelligenceModal from "../modals/CloseIntelligenceModal"; + +interface YorkieIntelligenceFooterProps { + onClose: () => void; +} + +function YorkieIntelligenceFooter(props: YorkieIntelligenceFooterProps) { + const { onClose } = props; + const theme = useTheme(); + const editorStore = useSelector(selectEditor); + const anchorRef = useRef(null); + const [selectedTitle, setSelectedTitle] = useState(null); + const [selectedFeature, setSelectedFeature] = useState(null); + const [anchorEl, setAnchorEl] = useState(); + const [closeModalOpen, setCloseModalOpen] = useState(false); + const cardRef = useRef(null); + + const width = useMemo( + () => editorStore.cmView!.contentDOM.getBoundingClientRect().width - 12, + [editorStore.cmView] + ); + + useEffect(() => { + if (!anchorRef.current) return; + + setAnchorEl(anchorRef.current); + + return () => { + setAnchorEl(undefined); + }; + }, []); + + const handleSelectFeature = (feature: IntelligenceFeature, title: string) => { + setSelectedFeature(feature); + setSelectedTitle(title); + }; + + const handleCloseModalOpen = () => { + setCloseModalOpen((prev) => !prev); + }; + + return ( + + + + + {selectedFeature && selectedTitle ? ( + + ) : ( + + )} + + + + + ); +} + +export default YorkieIntelligenceFooter; diff --git a/frontend/src/components/modals/CloseIntelligenceModal.tsx b/frontend/src/components/modals/CloseIntelligenceModal.tsx new file mode 100644 index 00000000..7301daeb --- /dev/null +++ b/frontend/src/components/modals/CloseIntelligenceModal.tsx @@ -0,0 +1,53 @@ +import { Button, Modal, ModalProps, Paper, Stack, Typography } from "@mui/material"; + +interface CloseIntelligenceModalProps extends Omit { + onCloseIntelligence: () => void; +} + +function CloseIntelligenceModal(props: CloseIntelligenceModalProps) { + const { onCloseIntelligence, ...modalProps } = props; + + const handleCloseModal = () => { + modalProps?.onClose?.(new Event("Close Modal"), "escapeKeyDown"); + }; + + const handleDiscard = () => { + onCloseIntelligence(); + handleCloseModal(); + }; + + return ( + + + + + yorkie + + Do you want to discard +
+ the Yorkie response? +
+
+ + + + +
+
+
+ ); +} + +export default CloseIntelligenceModal; diff --git a/frontend/src/constants/intelligence.ts b/frontend/src/constants/intelligence.ts new file mode 100644 index 00000000..fc278995 --- /dev/null +++ b/frontend/src/constants/intelligence.ts @@ -0,0 +1,7 @@ +export const INTELLIGENCE_HEADER_ID = "yorkie-intelligence-header"; +export const INTELLIGENCE_FOOTER_ID = "yorkie-intelligence-footer"; + +export enum IntelligenceFeature { + GITHUB_ISSUE = "github-issue", + GITHUB_PR = "github-pr", +} diff --git a/frontend/src/hooks/api/intelligence.ts b/frontend/src/hooks/api/intelligence.ts new file mode 100644 index 00000000..57e9e588 --- /dev/null +++ b/frontend/src/hooks/api/intelligence.ts @@ -0,0 +1,127 @@ +import { useCallback, useState } from "react"; +import { useSelector } from "react-redux"; +import { selectAuth } from "../../store/authSlice"; +import { selectDocument } from "../../store/documentSlice"; +import { IntelligenceFeature } from "../../constants/intelligence"; + +export const useIntelligenceFeatureStream = (feature: IntelligenceFeature) => { + const authStore = useSelector(selectAuth); + const documentSotre = useSelector(selectDocument); + const [data, setData] = useState(null); + const [memoryKey, setMemoryKey] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isComplete, setIsComplete] = useState(false); + + const mutateAsync = useCallback( + async (content: string) => { + setIsLoading(true); + setIsComplete(false); + setMemoryKey(null); + setData(null); + const response = await fetch( + `${import.meta.env.VITE_API_ADDR}/intelligence/${feature}`, + { + method: "POST", + headers: { + Authorization: `Bearer ${authStore.accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + documentId: documentSotre.data?.id, + content, + }), + } + ); + const reader = response.body?.getReader(); + let isFirst = true; + let result = ""; + + while (reader) { + const { done, value } = await reader.read(); + setIsLoading(false); + + if (done) { + break; + } + + let text = new TextDecoder().decode(value); + + if (isFirst) { + const splitted = text.split("\n"); + setMemoryKey(splitted[0]); + isFirst = false; + text = splitted.slice(1).join("\n"); + } + + result += text; + setData(result); + } + setIsComplete(true); + }, + [authStore.accessToken, documentSotre.data?.id, feature] + ); + + return { + data, + memoryKey, + isLoading, + isComplete, + mutateAsync, + }; +}; + +export const useIntelligenceStream = (memoryKey: string | null) => { + const authStore = useSelector(selectAuth); + const documentSotre = useSelector(selectDocument); + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isComplete, setIsComplete] = useState(false); + + const mutateAsync = useCallback( + async (content: string) => { + if (!memoryKey) return; + + setIsLoading(true); + setIsComplete(false); + setData(null); + const response = await fetch(`${import.meta.env.VITE_API_ADDR}/intelligence`, { + method: "POST", + headers: { + Authorization: `Bearer ${authStore.accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + documentId: documentSotre.data?.id, + memoryKey, + content, + }), + }); + const reader = response.body?.getReader(); + let result = ""; + + while (reader) { + const { done, value } = await reader.read(); + setIsLoading(false); + + if (done) { + break; + } + + const text = new TextDecoder().decode(value); + + result += text; + setData(result); + } + setIsComplete(true); + }, + [authStore.accessToken, documentSotre.data?.id, memoryKey] + ); + + return { + data, + memoryKey, + isLoading, + isComplete, + mutateAsync, + }; +}; diff --git a/frontend/src/hooks/useYorkieDocument.ts b/frontend/src/hooks/useYorkieDocument.ts index d15ffecb..ae3e80b6 100644 --- a/frontend/src/hooks/useYorkieDocument.ts +++ b/frontend/src/hooks/useYorkieDocument.ts @@ -7,6 +7,8 @@ import { useSearchParams } from "react-router-dom"; import { useSelector } from "react-redux"; import { selectAuth } from "../store/authSlice"; +yorkie.setLogLevel(4); + export const useYorkieDocument = ( yorkieDocuentId?: string | null, presenceName?: string | null diff --git a/frontend/src/index.css b/frontend/src/index.css index 5643c740..99c45664 100755 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -2,3 +2,8 @@ body { margin: 0; min-height: 100vh; } + +#yorkie-intelligence-footer .wmde-markdown { + -webkit-user-modify: read-only; + white-space: initial !important; +} diff --git a/frontend/src/pages/workspace/document/Index.tsx b/frontend/src/pages/workspace/document/Index.tsx index b4aaa2b0..c707744a 100644 --- a/frontend/src/pages/workspace/document/Index.tsx +++ b/frontend/src/pages/workspace/document/Index.tsx @@ -8,6 +8,7 @@ import { useGetDocumentQuery } from "../../../hooks/api/workspaceDocument"; import { useGetWorkspaceQuery } from "../../../hooks/api/workspace"; import DocumentView from "../../../components/editor/DocumentView"; import { useYorkieDocument } from "../../../hooks/useYorkieDocument"; +import YorkieIntelligence from "../../../components/editor/YorkieIntelligence"; function DocumentIndex() { const dispatch = useDispatch(); @@ -36,6 +37,7 @@ function DocumentIndex() { return ( + ); } diff --git a/frontend/src/store/editorSlice.ts b/frontend/src/store/editorSlice.ts index 659120d1..0cd1e7c5 100644 --- a/frontend/src/store/editorSlice.ts +++ b/frontend/src/store/editorSlice.ts @@ -4,6 +4,7 @@ import { RootState } from "./store"; import * as yorkie from "yorkie-js-sdk"; import { YorkieCodeMirrorDocType, YorkieCodeMirrorPresenceType } from "../utils/yorkie/yorkieSync"; import { ShareRole } from "../utils/share"; +import { EditorView } from "codemirror"; export type EditorModeType = "edit" | "both" | "read"; export type CodePairDocType = yorkie.Document< @@ -16,6 +17,7 @@ export interface EditorState { shareRole: ShareRole | null; doc: CodePairDocType | null; client: yorkie.Client | null; + cmView: EditorView | null; } const initialState: EditorState = { @@ -23,6 +25,7 @@ const initialState: EditorState = { shareRole: null, doc: null, client: null, + cmView: null, }; export const editorSlice = createSlice({ @@ -41,10 +44,13 @@ export const editorSlice = createSlice({ setClient: (state, action: PayloadAction) => { state.client = action.payload; }, + setCmView: (state, action: PayloadAction) => { + state.cmView = action.payload; + }, }, }); -export const { setMode, setDoc, setClient, setShareRole } = editorSlice.actions; +export const { setMode, setDoc, setClient, setShareRole, setCmView } = editorSlice.actions; export const selectEditor = (state: RootState) => state.editor; diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts index 9cd5faac..79710107 100644 --- a/frontend/src/store/store.ts +++ b/frontend/src/store/store.ts @@ -36,11 +36,12 @@ export const store = configureStore({ "persist/PERSIST", // redux-persist "editor/setDoc", "editor/setClient", + "editor/setCmView", ], - ignoredPaths: ["editor.doc", "editor.client"], + ignoredPaths: ["editor.doc", "editor.client", "editor.cmView"], }, immutableCheck: { - ignoredPaths: ["editor.doc", "editor.client"], + ignoredPaths: ["editor.doc", "editor.client", "editor.cmView"], }, }), }); diff --git a/frontend/src/utils/document.ts b/frontend/src/utils/document.ts index 152f64aa..0b219944 100644 --- a/frontend/src/utils/document.ts +++ b/frontend/src/utils/document.ts @@ -1,3 +1,10 @@ export function createDocumentKey() { return Math.random().toString(36).substring(7); } + +export function addSoftLineBreak(text: string) { + return text + .split("\n") + .map((line) => line + " ") + .join("\n"); +} diff --git a/frontend/src/utils/intelligence/intelligencePivot.ts b/frontend/src/utils/intelligence/intelligencePivot.ts new file mode 100644 index 00000000..e41bb893 --- /dev/null +++ b/frontend/src/utils/intelligence/intelligencePivot.ts @@ -0,0 +1,98 @@ +import * as cmView from "@codemirror/view"; + +import * as cmState from "@codemirror/state"; +import * as dom from "lib0/dom"; +import * as pair from "lib0/pair"; +import { INTELLIGENCE_FOOTER_ID, INTELLIGENCE_HEADER_ID } from "../../constants/intelligence"; + +class IntelligencePivotWidget extends cmView.WidgetType { + id: string; + content: string; + selectionRange: cmState.SelectionRange | null; + + constructor(id: string, content: string, selectionRange: cmState.SelectionRange | null) { + super(); + this.id = id; + this.content = content; + this.selectionRange = selectionRange; + } + + toDOM() { + return dom.element("span", [ + pair.create("id", this.id), + pair.create("content", this.content), + pair.create("style", `position: relaitve;`), + ]) as HTMLElement; + } + + eq(widget: IntelligencePivotWidget) { + return widget.selectionRange === this.selectionRange; + } + + compare(widget: IntelligencePivotWidget) { + return widget.selectionRange === this.selectionRange; + } + + updateDOM() { + return false; + } + + get estimatedHeight() { + return -1; + } + + ignoreEvent() { + return true; + } +} + +export class IntelligencePivotPluginValue { + decorations: cmView.DecorationSet; + + constructor() { + this.decorations = cmState.RangeSet.of([]); + } + + update(update: cmView.ViewUpdate) { + const decorations: Array> = []; + const selectionRange = update.state.selection.main; + const isDragged = selectionRange?.from !== selectionRange?.to; + + if (isDragged && selectionRange) { + const selectedContent = update.state.sliceDoc(selectionRange.from, selectionRange.to); + decorations.push({ + from: selectionRange.from, + to: selectionRange.from, + value: cmView.Decoration.widget({ + side: 1, + block: false, + widget: new IntelligencePivotWidget( + INTELLIGENCE_HEADER_ID, + selectedContent, + selectionRange + ), + }), + }); + + decorations.push({ + from: selectionRange.to, + to: selectionRange.to, + value: cmView.Decoration.widget({ + side: 1, + block: false, + widget: new IntelligencePivotWidget( + INTELLIGENCE_FOOTER_ID, + selectedContent, + selectionRange + ), + }), + }); + } + + this.decorations = cmView.Decoration.set(decorations, true); + } +} + +export const intelligencePivot = cmView.ViewPlugin.fromClass(IntelligencePivotPluginValue, { + decorations: (v) => v.decorations, +});