Merge pull request #20 from runjinz/chat-messages

Chat messages
This commit is contained in:
Rankin Zheng 2023-05-06 00:43:04 +08:00 committed by GitHub
commit 1847e100d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1124 additions and 32 deletions

911
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -174,6 +174,7 @@
"nonce": "^1.0.4", "nonce": "^1.0.4",
"openai": "^3.2.1", "openai": "^3.2.1",
"quote": "^0.4.0", "quote": "^0.4.0",
"react-remark": "^2.1.0",
"shell-escape": "^0.2.0", "shell-escape": "^0.2.0",
"uuid": "^9.0.0" "uuid": "^9.0.0"
} }

45
src/utils/MessageUtil.ts Normal file
View File

@ -0,0 +1,45 @@
// @ts-ignore
const vscodeApi = window.acquireVsCodeApi();
class MessageUtil {
handlers: { [x: string]: any; };
constructor() {
this.handlers = {};
}
// Register a message handler for a specific message type
registerHandler(messageType: string, handler: { (message: { text: string; }): void; (message: { text: string; }): void; }) {
if (!this.handlers[messageType]) {
this.handlers[messageType] = [];
}
this.handlers[messageType].push(handler);
}
// Unregister a message handler for a specific message type
unregisterHandler(messageType: string | number, handler: any) {
if (this.handlers[messageType]) {
this.handlers[messageType] = this.handlers[messageType].filter(
(h: any) => h !== handler
);
}
}
// Handle a received message
handleMessage(message: { command: string | number; }) {
console.log("handleMessage", message);
const handlers = this.handlers[message.command];
if (handlers) {
handlers.forEach((handler: (arg0: { command: string | number; }) => any) => handler(message));
}
}
// Send a message to the VSCode API
sendMessage(message: { command: string; text: string; }) {
console.log("sendMessage", message);
vscodeApi.postMessage(message);
}
}
// Export the MessageUtil class as a module
export default MessageUtil;

View File

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { Container } from '@mantine/core'; import { Avatar, Container, Divider, Flex, Grid, Stack, TypographyStylesProvider } from '@mantine/core';
import { Input, Tooltip } from '@mantine/core'; import { Input, Tooltip } from '@mantine/core';
import { List } from '@mantine/core'; import { List } from '@mantine/core';
import { ScrollArea } from '@mantine/core'; import { ScrollArea } from '@mantine/core';
@ -8,8 +8,11 @@ import { createStyles } from '@mantine/core';
import { ActionIcon } from '@mantine/core'; import { ActionIcon } from '@mantine/core';
import { Menu, Button, Text } from '@mantine/core'; import { Menu, Button, Text } from '@mantine/core';
import { useViewportSize } from '@mantine/hooks'; import { useViewportSize } from '@mantine/hooks';
import { IconSend, IconSquareRoundedPlus } from '@tabler/icons-react'; import { IconEdit, IconRobot, IconSend, IconSquareRoundedPlus, IconUser } from '@tabler/icons-react';
import { IconSettings, IconSearch, IconPhoto, IconMessageCircle, IconTrash, IconArrowsLeftRight } from '@tabler/icons-react'; import { IconSettings, IconSearch, IconPhoto, IconMessageCircle, IconTrash, IconArrowsLeftRight } from '@tabler/icons-react';
import { Prism } from '@mantine/prism';
import { useRemark } from 'react-remark';
import MessageUtil from '../utils/MessageUtil';
const useStyles = createStyles((theme, _params, classNames) => ({ const useStyles = createStyles((theme, _params, classNames) => ({
panel: { panel: {
@ -34,17 +37,40 @@ const useStyles = createStyles((theme, _params, classNames) => ({
fontSize: '0.8rem', fontSize: '0.8rem',
color: theme.colors.gray[6], color: theme.colors.gray[6],
}, },
responseContent: {
marginTop: 8,
marginLeft: 0,
marginRight: 0,
},
icon: { icon: {
pointerEvents: 'all', pointerEvents: 'all',
}, },
avatar: {
marginTop: 8,
marginLeft: 8,
},
messageBody: {
},
})); }));
const chatPanel = () => { const chatPanel = () => {
const [reactContent, setMarkdownSource] = useRemark();
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const [input, setInput] = useState('');
const [commandOpened, setCommandOpened] = useState(false); const [commandOpened, setCommandOpened] = useState(false);
const { classes } = useStyles(); const { classes } = useStyles();
const { height, width } = useViewportSize(); const { height, width } = useViewportSize();
const messageUtil = new MessageUtil();
const demoCode = `import { Button } from '@mantine/core';
function Demo() {
return <Button>Hello</Button>
}`;
setMarkdownSource(`# code block
print '3 backticks or'
print 'indent 4 spaces'`);
const handlePlusBottonClick = (event: React.MouseEvent<HTMLButtonElement>) => { const handlePlusBottonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setOpened(!opened); setOpened(!opened);
@ -53,26 +79,89 @@ const chatPanel = () => {
const handleContainerClick = (event: React.MouseEvent<HTMLDivElement>) => { const handleContainerClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (opened) { setOpened(false); } if (opened) { setOpened(false); }
}; };
const handleSendClick = (event: React.MouseEvent<HTMLButtonElement>) => {
const message = input;
if (message) {
// Add the user's message to the chat UI
// addMessageToUI('user', message);
// Clear the input field
event.currentTarget.value = '';
// Process and send the message to the extension
messageUtil.sendMessage({
command: 'sendMessage',
text: message
});
}
};
// Register message handlers for receiving messages from the extension
messageUtil.registerHandler('receiveMessage', (message: { text: string; }) => {
console.log(`receiveMessage: ${message.text}`);
// Add the received message to the chat UI as a bot message
setMarkdownSource(message.text);
});
messageUtil.registerHandler('receiveMessagePartial', (message: { text: string; }) => {
console.log(`receiveMessagePartial: ${message.text}`);
// Add the received message to the chat UI as a bot message
setMarkdownSource(message.text);
});
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value const value = event.target.value;
// if value start with '/' command show menu // if value start with '/' command show menu
if (value.startsWith('/')) { if (value.startsWith('/')) {
setCommandOpened(true); setCommandOpened(true);
} else { } else {
setCommandOpened(false); setCommandOpened(false);
} }
} setInput(value);
};
return ( return (
<Container className={classes.panel} onClick={handleContainerClick}> <Container className={classes.panel} onClick={handleContainerClick}>
<ScrollArea h={height - 70} type="never"> <ScrollArea h={height - 70} type="never">
<List> <Flex
<List.Item>Clone or download repository from GitHub</List.Item> mih={50}
<List.Item>Install dependencies with yarn</List.Item> gap="md"
<List.Item>To start development server run npm start command</List.Item> justify="flex-start"
<List.Item>Run tests to make sure your changes do not break the build</List.Item> align="flex-start"
<List.Item>Submit a pull request once you are done</List.Item> direction="row"
</List> wrap="wrap"
className={classes.messageBody}
>
<Avatar color="indigo" size='md' radius="xl" className={classes.avatar}>
<IconUser size="1.5rem" />
</Avatar>
<Container className={classes.responseContent}>
<Text>
Write a hello world, and explain it.
</Text>
</Container>
{/* <ActionIcon>
<IconEdit size="1.5rem" />
</ActionIcon> */}
</Flex>
<Divider my="sm" label="Mar 4, 2023" labelPosition="center" />
<Flex
mih={50}
gap="md"
justify="flex-start"
align="flex-start"
direction="row"
wrap="wrap"
className={classes.messageBody}
>
<Avatar color="blue" size='md' radius="xl" className={classes.avatar}>
<IconRobot size="1.5rem" />
</Avatar>
<Container className={classes.responseContent}>
{reactContent}
</Container>
</Flex>
</ScrollArea> </ScrollArea>
<Menu id='plusMenu' shadow="md" width={200} opened={opened} onChange={setOpened} > <Menu id='plusMenu' shadow="md" width={200} opened={opened} onChange={setOpened} >
<Menu.Dropdown className={classes.plusMenu}> <Menu.Dropdown className={classes.plusMenu}>
@ -154,7 +243,7 @@ const chatPanel = () => {
</ActionIcon> </ActionIcon>
} }
rightSection={ rightSection={
<ActionIcon> <ActionIcon onClick={handleSendClick}>
<IconSend size="1rem" /> <IconSend size="1rem" />
</ActionIcon> </ActionIcon>
} }

View File

@ -1,10 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import { MantineProvider } from '@mantine/core'; import { MantineProvider } from '@mantine/core';
import App from './App'; import App from './App';
ReactDOM.render( const container = document.getElementById('app')!;
const root = createRoot(container); // createRoot(container!) if you use TypeScript
root.render(
<MantineProvider withGlobalStyles withNormalizeCSS> <MantineProvider withGlobalStyles withNormalizeCSS>
<App /> <App />
</MantineProvider>, </MantineProvider>
document.getElementById('app')); );

View File

@ -12,17 +12,16 @@ const CopyWebpackPlugin = require('copy-webpack-plugin');
/** @type WebpackConfig */ /** @type WebpackConfig */
const extensionConfig = { const extensionConfig = {
name: 'vscode extension',
target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
entry: { entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
extension: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
index: './src/views/index.tsx'
},
output: { output: {
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
filename: '[name].js', filename: 'extension.js',
libraryTarget: 'commonjs2' libraryTarget: 'commonjs2'
}, },
externals: { externals: {
@ -31,6 +30,51 @@ const extensionConfig = {
}, },
resolve: { resolve: {
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
extensions: ['.ts', '.json']
},
module: {
rules: [
{
test: /\.ts?$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript'
]
}
},
{
loader: 'ts-loader'
}
]
},
]
},
devtool: 'nosources-source-map',
infrastructureLogging: {
level: "log", // enables logging required for problem matchers
},
plugins: []
};
/** @type WebpackConfig */
const webviewConfig = {
name: 'webview',
target: 'web',
mode: 'development',
entry: './src/views/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'] extensions: ['.ts', '.tsx', '.js', '.json']
}, },
module: { module: {
@ -56,8 +100,18 @@ const extensionConfig = {
}, },
{ {
test: /\.jsx?$/, test: /\.jsx?$/,
use: 'babel-loader',
exclude: /node_modules/, exclude: /node_modules/,
use: [{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
'@babel/preset-react',
],
]
},
}]
}, },
{ {
test: /\.css$/i, test: /\.css$/i,
@ -94,9 +148,9 @@ const extensionConfig = {
} }
] ]
}, },
devtool: 'nosources-source-map', devtool: 'source-map',
infrastructureLogging: { infrastructureLogging: {
level: "log", // enables logging required for problem matchers level: "log",
}, },
plugins: [ plugins: [
// generate an HTML file that includes the extension's JavaScript file // generate an HTML file that includes the extension's JavaScript file
@ -109,12 +163,9 @@ const extensionConfig = {
patterns: [ patterns: [
{ from: 'assets', to: 'assets' }, { from: 'assets', to: 'assets' },
], ],
}),
// define global variables
new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development')
}) })
] ]
}; };
module.exports = [extensionConfig];
module.exports = [extensionConfig, webviewConfig];