From 060d07ae8ee36dcf088ad253a18e6cabcd80f4d7 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Tue, 2 May 2023 04:26:56 +0800 Subject: [PATCH] feat: add AI Assistant plugin for Tailchat with ChatGPT --- .../com.msgbyte.ai-assistant/manifest.json | 9 ++ .../com.msgbyte.ai-assistant/package.json | 16 +++ .../com.msgbyte.ai-assistant/src/index.tsx | 22 ++++ .../com.msgbyte.ai-assistant/src/popover.tsx | 116 ++++++++++++++++++ .../com.msgbyte.ai-assistant/src/prompt.ts | 8 ++ .../com.msgbyte.ai-assistant/src/translate.ts | 44 +++++++ .../com.msgbyte.ai-assistant/tsconfig.json | 7 ++ .../types/tailchat.d.ts | 2 + .../ChatBox/ChatInputBox/context.tsx | 2 + .../components/ChatBox/ChatInputBox/index.tsx | 13 +- client/web/src/plugin/common/reg.ts | 6 + client/web/src/plugin/component/index.tsx | 3 + pnpm-lock.yaml | 12 ++ 13 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 client/web/plugins/com.msgbyte.ai-assistant/manifest.json create mode 100644 client/web/plugins/com.msgbyte.ai-assistant/package.json create mode 100644 client/web/plugins/com.msgbyte.ai-assistant/src/index.tsx create mode 100644 client/web/plugins/com.msgbyte.ai-assistant/src/popover.tsx create mode 100644 client/web/plugins/com.msgbyte.ai-assistant/src/prompt.ts create mode 100644 client/web/plugins/com.msgbyte.ai-assistant/src/translate.ts create mode 100644 client/web/plugins/com.msgbyte.ai-assistant/tsconfig.json create mode 100644 client/web/plugins/com.msgbyte.ai-assistant/types/tailchat.d.ts diff --git a/client/web/plugins/com.msgbyte.ai-assistant/manifest.json b/client/web/plugins/com.msgbyte.ai-assistant/manifest.json new file mode 100644 index 00000000..3b1b6b09 --- /dev/null +++ b/client/web/plugins/com.msgbyte.ai-assistant/manifest.json @@ -0,0 +1,9 @@ +{ + "label": "AI Assistant", + "name": "com.msgbyte.ai-assistant", + "url": "/plugins/com.msgbyte.ai-assistant/index.js", + "version": "0.0.0", + "author": "moonrailgun", + "description": "Add chatgpt into Tailchat", + "requireRestart": true +} diff --git a/client/web/plugins/com.msgbyte.ai-assistant/package.json b/client/web/plugins/com.msgbyte.ai-assistant/package.json new file mode 100644 index 00000000..d689420b --- /dev/null +++ b/client/web/plugins/com.msgbyte.ai-assistant/package.json @@ -0,0 +1,16 @@ +{ + "name": "@plugins/com.msgbyte.ai-assistant", + "main": "src/index.tsx", + "version": "0.0.0", + "description": "Add chatgpt into Tailchat", + "private": true, + "scripts": { + "sync:declaration": "tailchat declaration github" + }, + "dependencies": {}, + "devDependencies": { + "@types/styled-components": "^5.1.26", + "react": "18.2.0", + "styled-components": "^5.3.6" + } +} diff --git a/client/web/plugins/com.msgbyte.ai-assistant/src/index.tsx b/client/web/plugins/com.msgbyte.ai-assistant/src/index.tsx new file mode 100644 index 00000000..306193e3 --- /dev/null +++ b/client/web/plugins/com.msgbyte.ai-assistant/src/index.tsx @@ -0,0 +1,22 @@ +import { regChatInputButton } from '@capital/common'; +import { BaseChatInputButton } from '@capital/component'; +import React from 'react'; +import { AssistantPopover } from './popover'; + +const PLUGIN_ID = 'com.msgbyte.ai-assistant'; +const PLUGIN_NAME = 'AI Assistant'; + +console.log(`Plugin ${PLUGIN_NAME}(${PLUGIN_ID}) is loaded`); + +regChatInputButton({ + render: () => { + return ( + ( + + )} + /> + ); + }, +}); diff --git a/client/web/plugins/com.msgbyte.ai-assistant/src/popover.tsx b/client/web/plugins/com.msgbyte.ai-assistant/src/popover.tsx new file mode 100644 index 00000000..eab5ca86 --- /dev/null +++ b/client/web/plugins/com.msgbyte.ai-assistant/src/popover.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { Translate } from './translate'; +import { useAsyncRequest } from '@capital/common'; +import { + LoadingSpinner, + useChatInputActionContext, + Tag, +} from '@capital/component'; +import axios from 'axios'; +import styled from 'styled-components'; +import { + improveTextPrompt, + longerTextPrompt, + shorterTextPrompt, + translateTextPrompt, +} from './prompt'; + +const Root = styled.div` + padding: 0.5rem; + max-width: 300px; +`; + +const Tip = styled.div` + margin-bottom: 4px; +`; + +const ActionButton = styled.div` + min-width: 180px; + padding: 4px 6px; + border-radius: 3px; + background-color: rgba(0, 0, 0, 0.1); + cursor: pointer; + margin-bottom: 4px; + + &:hover { + background-color: rgba(0, 0, 0, 0.2); + } +`; + +export const AssistantPopover: React.FC<{ + onCompleted: () => void; +}> = React.memo((props) => { + const { message } = useChatInputActionContext(); + const [{ loading, value }, handleCallAI] = useAsyncRequest( + async (question: string) => { + // TODO: wait for replace + const { data } = await axios.post('https://uui1ik.laf.dev/chatgpt', { + question, + }); + + return data; + }, + [] + ); + + if (loading) { + return ( + + + + ); + } + + return ( + + {Translate.helpMeTo} + +
+ {typeof value === 'object' && + (value.result ? ( +
+
{value.answer}
+ + {Translate.usage}: {value.usage}ms + +
+ ) : ( +
+
{Translate.serviceBusy}
+ {Translate.callError} +
+ ))} +
+ + {typeof message === 'string' && message.length > 0 && ( + <> + handleCallAI(improveTextPrompt + message)} + > + {Translate.improveText} + + handleCallAI(shorterTextPrompt + message)} + > + {Translate.makeShorter} + + handleCallAI(longerTextPrompt + message)} + > + {Translate.makeLonger} + + handleCallAI(translateTextPrompt + message)} + > + {Translate.translateInputText} + + + )} + + {/* handleCallAI('')}> + {Translate.summaryMessages} + */} +
+ ); +}); +AssistantPopover.displayName = 'AssistantPopover'; diff --git a/client/web/plugins/com.msgbyte.ai-assistant/src/prompt.ts b/client/web/plugins/com.msgbyte.ai-assistant/src/prompt.ts new file mode 100644 index 00000000..e27fc331 --- /dev/null +++ b/client/web/plugins/com.msgbyte.ai-assistant/src/prompt.ts @@ -0,0 +1,8 @@ +export const improveTextPrompt = + "You are a text embellisher, you can only embellish the text, don't interpret it. Now i need you embellish it and keep my origin language:"; +export const shorterTextPrompt = + "You are a text embellisher, you can only shorter the text, don't interpret it. Now i need you shorter it and keep my origin language:"; +export const longerTextPrompt = + "You are a text embellisher, you can only longer the text, don't interpret it. Now i need you longer it and keep my origin language:"; +export const translateTextPrompt = + 'You are a program responsible for translating text. Your task is to output the specified target language based on the input text. Please do not output any text other than the translation. Target language is english, and if you receive text is english, please translate to chinese(no need pinyin), then its my text:'; diff --git a/client/web/plugins/com.msgbyte.ai-assistant/src/translate.ts b/client/web/plugins/com.msgbyte.ai-assistant/src/translate.ts new file mode 100644 index 00000000..5f0afaec --- /dev/null +++ b/client/web/plugins/com.msgbyte.ai-assistant/src/translate.ts @@ -0,0 +1,44 @@ +import { localTrans } from '@capital/common'; + +export const Translate = { + name: localTrans({ + 'zh-CN': 'AI Assistant', + 'en-US': 'AI Assistant', + }), + helpMeTo: localTrans({ + 'zh-CN': '帮我:', + 'en-US': 'Help me to:', + }), + improveText: localTrans({ + 'zh-CN': '改进文本', + 'en-US': 'Improve Text', + }), + makeShorter: localTrans({ + 'zh-CN': '精简内容', + 'en-US': 'Make Shorter', + }), + makeLonger: localTrans({ + 'zh-CN': '扩写内容', + 'en-US': 'Make Longer', + }), + summaryMessages: localTrans({ + 'zh-CN': '总结内容', + 'en-US': 'Summary Messages', + }), + translateInputText: localTrans({ + 'zh-CN': '翻译输入内容', + 'en-US': 'Translate Input', + }), + usage: localTrans({ + 'zh-CN': '用时', + 'en-US': 'Usage', + }), + serviceBusy: localTrans({ + 'zh-CN': '服务器忙,请稍后再试', + 'en-US': 'Server is busy, please try again later', + }), + callError: localTrans({ + 'zh-CN': '调用失败', + 'en-US': 'Call Error', + }), +}; diff --git a/client/web/plugins/com.msgbyte.ai-assistant/tsconfig.json b/client/web/plugins/com.msgbyte.ai-assistant/tsconfig.json new file mode 100644 index 00000000..d9b47ed0 --- /dev/null +++ b/client/web/plugins/com.msgbyte.ai-assistant/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "jsx": "react", + "importsNotUsedAsValues": "error" + } +} diff --git a/client/web/plugins/com.msgbyte.ai-assistant/types/tailchat.d.ts b/client/web/plugins/com.msgbyte.ai-assistant/types/tailchat.d.ts new file mode 100644 index 00000000..49f524ae --- /dev/null +++ b/client/web/plugins/com.msgbyte.ai-assistant/types/tailchat.d.ts @@ -0,0 +1,2 @@ +declare module '@capital/common'; +declare module '@capital/component'; diff --git a/client/web/src/components/ChatBox/ChatInputBox/context.tsx b/client/web/src/components/ChatBox/ChatInputBox/context.tsx index 16a64d8e..19892df9 100644 --- a/client/web/src/components/ChatBox/ChatInputBox/context.tsx +++ b/client/web/src/components/ChatBox/ChatInputBox/context.tsx @@ -6,6 +6,8 @@ import { useShallowObject } from 'tailchat-shared'; * Input Actions */ export interface ChatInputActionContextProps { + message: string; + setMessage: (msg: string) => void; sendMsg: (message: string) => void; appendMsg: (message: string) => void; } diff --git a/client/web/src/components/ChatBox/ChatInputBox/index.tsx b/client/web/src/components/ChatBox/ChatInputBox/index.tsx index a1c4b86f..27dd6770 100644 --- a/client/web/src/components/ChatBox/ChatInputBox/index.tsx +++ b/client/web/src/components/ChatBox/ChatInputBox/index.tsx @@ -1,4 +1,7 @@ -import { getMessageTextDecorators } from '@/plugin/common'; +import { + getMessageTextDecorators, + pluginChatInputButtons, +} from '@/plugin/common'; import { isEnterHotkey } from '@/utils/hot-key'; import React, { useCallback, useRef, useState } from 'react'; import { ChatInputAddon } from './Addon'; @@ -89,6 +92,8 @@ export const ChatInputBox: React.FC = React.memo((props) => { return ( = React.memo((props) => {
+ {pluginChatInputButtons.map((item, i) => + React.cloneElement(item.render(), { + key: `plugin-chatinput-btn#${i}`, + }) + )} + {message ? ( diff --git a/client/web/src/plugin/common/reg.ts b/client/web/src/plugin/common/reg.ts index a72b90e6..43e6f4d6 100644 --- a/client/web/src/plugin/common/reg.ts +++ b/client/web/src/plugin/common/reg.ts @@ -157,6 +157,12 @@ export type { ChatInputActionContextProps }; export const [pluginChatInputActions, regChatInputAction] = buildRegList(); +interface ChatInputButton { + render: () => React.ReactElement; +} +export const [pluginChatInputButtons, regChatInputButton] = + buildRegList(); + export { regSocketEventListener }; /** diff --git a/client/web/src/plugin/component/index.tsx b/client/web/src/plugin/component/index.tsx index 92e7a1e6..5b51d2db 100644 --- a/client/web/src/plugin/component/index.tsx +++ b/client/web/src/plugin/component/index.tsx @@ -13,6 +13,7 @@ export { notification, Empty, Popover, + Tag, } from 'antd'; export const TextArea = Input.TextArea; export { @@ -31,6 +32,8 @@ export { export { Link } from 'react-router-dom'; export { MessageAckContainer } from '@/components/ChatBox/ChatMessageList/MessageAckContainer'; +export { BaseChatInputButton } from '@/components/ChatBox/ChatInputBox/BaseChatInputButton'; +export { useChatInputActionContext } from '@/components/ChatBox/ChatInputBox/context'; export { GroupExtraDataPanel } from '@/components/Panel/group/GroupExtraDataPanel'; export { Image } from '@/components/Image'; export { IconBtn } from '@/components/IconBtn'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d1b1b9d..1232acea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -951,6 +951,18 @@ importers: specifier: ^6.5.4 version: 6.5.4(webpack@5.75.0) + client/web/plugins/com.msgbyte.ai-assistant: + devDependencies: + '@types/styled-components': + specifier: ^5.1.26 + version: 5.1.26 + react: + specifier: 18.2.0 + version: 18.2.0 + styled-components: + specifier: ^5.3.6 + version: 5.3.6(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) + client/web/plugins/com.msgbyte.bbcode: dependencies: '@bbob/parser':