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':