Merge pull request #359 from devchat-ai/feat/idea

feat: move views into the gui project and modify the packaging method
This commit is contained in:
boob.yang 2023-12-14 09:09:45 +08:00 committed by GitHub
commit ba9da6d8ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 9819 additions and 185 deletions

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "tools"]
path = tools
url = git@github.com:devchat-ai/devchat-vscode-tools.git
[submodule "gui"]
path = gui
url = https://github.com/devchat-ai/devchat-gui.git

1
gui Submodule

@ -0,0 +1 @@
Subproject commit 204a8b434948f124444e07f8153ea32194587d76

View File

@ -797,6 +797,7 @@
}
},
"scripts": {
"build:gui":"cd ./gui && yarn && yarn vscode",
"vscode:uninstall": "node ./dist/uninstall",
"vscode:prepublish": "npm run package",
"compile": "webpack",
@ -807,8 +808,9 @@
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"lint": "eslint src --ext ts",
"test": "mocha",
"build": "webpack --config webpack.config.js",
"dev": "webpack serve --config webpack.config.js --open"
"build": "webpack --config webpack.config.js && cd ./gui && yarn && yarn vscode",
"dev": "webpack serve --config webpack.config.js --open",
"idea": "webpack --config webpack.idea.config.js && mv dist/main.js dist/main.html ../devchat-intellij/src/main/resources/static && echo '🎆done'"
},
"devDependencies": {
"@babel/core": "^7.21.8",
@ -872,6 +874,8 @@
"@tiptap/react": "^2.0.3",
"@tiptap/starter-kit": "^2.0.3",
"axios": "^1.3.6",
"clean-webpack-plugin": "^4.0.0",
"dayjs": "^1.11.10",
"dotenv": "^16.0.3",
"js-yaml": "^4.1.0",
"mdast": "^3.0.0",

19
src/main.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>DevChat</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="app"></div>
<script></script>
</body>
</html>

6
src/types/globle.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
declare global {
interface Window {
[key: string]: any;
}
}
export {};

View File

@ -1,26 +1,27 @@
// @ts-ignore
const vscodeApi = window.acquireVsCodeApi();
import IdeaBridge from "./ideaBridge";
class MessageUtil {
private static instance: MessageUtil;
handlers: { [x: string]: any; };
handlers: { [x: string]: any };
vscodeApi: any;
messageListener: any;
constructor() {
this.handlers = {};
this.messageListener = null;
if (process.env.platform === "vscode") {
this.vscodeApi = window.acquireVsCodeApi();
}
if (!this.messageListener) {
this.messageListener = (event: { data: any; }) => {
this.messageListener = (event: { data: any }) => {
const message = event.data;
this.handleMessage(message);
};
window.addEventListener('message', this.messageListener);
window.addEventListener("message", this.messageListener);
} else {
console.log('Message listener has already been bound.');
console.log("Message listener has already been bound.");
}
}
@ -33,10 +34,14 @@ class MessageUtil {
// Register a message handler for a specific message type
registerHandler(messageType: string, handler: any) {
if (!this.handlers[messageType]) {
this.handlers[messageType] = [];
if (process.env.platform === "idea") {
IdeaBridge.registerHandler(messageType, handler);
} else {
if (!this.handlers[messageType]) {
this.handlers[messageType] = [];
}
this.handlers[messageType].push(handler);
}
this.handlers[messageType].push(handler);
}
// Unregister a message handler for a specific message type
@ -49,17 +54,22 @@ class MessageUtil {
}
// Handle a received message
handleMessage(message: { command: string | number; }) {
handleMessage(message: { command: string | number }) {
const handlers = this.handlers[message.command];
if (handlers) {
handlers.forEach((handler: (arg0: { command: string | number; }) => any) => handler(message));
handlers.forEach((handler: (arg0: { command: string | number }) => any) =>
handler(message)
);
}
}
// Send a message to the VSCode API
sendMessage(message: any) {
// console.log(`${JSON.stringify(message)}`);
vscodeApi.postMessage(message);
if (process.env.platform === "idea") {
IdeaBridge.sendMessage(message);
} else {
this.vscodeApi.postMessage(message);
}
}
}

70
src/util/bridge.md Normal file
View File

@ -0,0 +1,70 @@
## sendMessage
- getUserAccessKey // 获取 access key
- doCommit // 提交代码
- updateSetting // 更新设置(目前只有更换模型)
- getSetting // 获取默认模型
- deleteChatMessage // 删除最近一条消息
- show_diff // 调用 editor 代码对比
-- later --
- stopDevChat // 停止生成
- doCommand //
-- content
// 1. 打开设置
// 2. 启动 ask code 安装
// 3. 设置 access key
- featureToggles
- isDevChatInstalled // 判断 ask code 是否安装
- historyMessages // 页面的历史消息
- contextDetail // 获取 appendContext 响应之后,发送次请求获取文件的内容
- addContext // 点击 context 菜单(比如 git diff之后发送到消息
- code_file_apply // 代码应用到 editor替换 current file
- code_apply // 代码应用到 editor 光标位置
- sendMessage // 发送消息
- regeneration // 错误时重新生成
- regContextList // git diff 之类的列表
- regModelList // model 列表
- regCommandList // 输入 / 之后出现的列表
## registerHandler
- getUserAccessKey // 获取 access key
- regCommandList // 获取 / 之后出现的列表
- appendContext // 右键添加到 context 或者 context 菜单点击的响应
- contextDetailResponse // 获取到的文件内容
- loadHistoryMessages // 与 historyMessages 对应
- isDevChatInstalled // 与 isDevChatInstalled 对应
- deletedChatMessage // 与 deleteChatMessage 对应
- regContextList // 与 regContextList 对应
- regModelList // 与 regModelList
- receiveMessagePartial // 部分对话
- receiveMessage // 对话
- systemMessage // 没用了
# css
--vscode-editor-font-familyy
--vscode-editor-font-size
--vscode-dropdown-background
--vscode-dropdown-border
--vscode-foreground
--vscode-sideBar-background
--vscode-dropdown-foreground
--vscode-menu-foreground
--vscode-commandCenter-activeForeground
--vscode-commandCenter-activeBackground
--vscode-menu-border
--vscode-menu-background
--vscode-commandCenter-border
--vscode-editor-foreground
--vscode-input-background
--vscode-input-border
--vscode-input-foreground
--vscode-disabledForeground
--vscode-toolbar-activeBackground
1. 请求 loadHistoryMessages 根据全局 id 返回空/对应的消息
1.1. 参数只有分页
2. 请求 loadConversations,用于切换全局的 id
2.1 loadConversations 的响应里,请求 1

487
src/util/ideaBridge.ts Normal file
View File

@ -0,0 +1,487 @@
const JStoIdea = {
sendMessage: (message: string, context: any = [], parent: string = "") => {
const paramsContext: any = [];
if (Array.isArray(context) && context.length > 0) {
context.forEach((item) => {
paramsContext.push({
type: "code",
...item.context,
});
});
}
const params = {
action: "sendMessage/request",
metadata: {
callback: "IdeaToJSMessage",
parent: parent,
},
payload: {
contexts: paramsContext,
message: message,
},
};
console.log("ready to send message: ", params);
window.JSJavaBridge.callJava(JSON.stringify(params));
},
getModel: () => {
const params = {
action: "listModels/request",
metadata: {
callback: "IdeaToJSMessage",
},
payload: {},
};
window.JSJavaBridge.callJava(JSON.stringify(params));
},
getContextList: () => {
const params = {
action: "listContexts/request",
metadata: {
callback: "IdeaToJSMessage",
},
payload: {},
};
window.JSJavaBridge.callJava(JSON.stringify(params));
},
getCommandList: () => {
const params = {
action: "listCommands/request",
metadata: {
callback: "IdeaToJSMessage",
},
payload: {},
};
window.JSJavaBridge.callJava(JSON.stringify(params));
},
insertCode: (code) => {
const params = {
action: "insertCode/request",
metadata: {
callback: "IdeaToJSMessage",
},
payload: {
content: code,
},
};
window.JSJavaBridge.callJava(JSON.stringify(params));
},
replaceFileContent: (code) => {
const params = {
action: "replaceFileContent/request",
metadata: {
callback: "IdeaToJSMessage",
},
payload: {
content: code,
},
};
window.JSJavaBridge.callJava(JSON.stringify(params));
},
viewDiff: (code) => {
const params = {
action: "viewDiff/request",
metadata: {
callback: "IdeaToJSMessage",
},
payload: {
content: code,
},
};
window.JSJavaBridge.callJava(JSON.stringify(params));
},
getUserAccessKey: () => {
// 这里需要发送一个请求,获取完整的用户设置
const params = {
action: "getSetting/request",
metadata: {
callback: "IdeaToJSMessage",
},
payload: {},
};
window.JSJavaBridge.callJava(JSON.stringify(params));
},
etcCommand: (command: any) => {
/**
*
* 1. workbench.action.openSettings
* 2. AskCodeIndexStart
* 3. AccessKey.OpenAI
* 4. AccessKey.DevChat
*/
const content = Array.isArray(command.content) ? command.content[0] : "";
switch (content) {
case "workbench.action.openSettings":
// 打开设置
const params = {
action: "showSettingDialog/request",
metadata: {
callback: "IdeaToJSMessage",
},
payload: {},
};
window.JSJavaBridge.callJava(JSON.stringify(params));
break;
case "AccessKey.DevChat":
// 设置key
break;
default:
break;
}
},
getTopicList: () => {
// 获取 topic 列表
const params = {
action: "listTopics/request",
metadata: {
callback: "IdeaToJSMessage",
},
payload: {},
};
window.JSJavaBridge.callJava(JSON.stringify(params));
},
updateSetting: (value: string) => {
// 因为现在只有更换模型,所以直接取 value
const params = {
action: "updateSetting/request",
metadata: {
callback: "IdeaToJSMessage",
},
payload: {
setting: {
currentModel: value,
},
},
};
window.JSJavaBridge.callJava(JSON.stringify(params));
},
commit: (code: string) => {
const params = {
action: "commitCode/request",
metadata: {
callback: "IdeaToJSMessage",
},
payload: {
message: code,
},
};
window.JSJavaBridge.callJava(JSON.stringify(params));
},
getTopicDetail: (topicHash: string) => {
const params = {
action: "loadConversations/request",
metadata: {
callback: "IdeaToJSMessage",
topicHash: topicHash,
},
payload: {},
};
window.JSJavaBridge.callJava(JSON.stringify(params));
},
historyMessages: (message) => {
const params = {
action: "loadHistoryMessages/request",
metadata: {
callback: "IdeaToJSMessage",
},
payload: {
pageIndex: message?.page || 0,
},
};
window.JSJavaBridge.callJava(JSON.stringify(params));
},
deleteChatMessage: (message) => {
const params = {
action: "deleteLastConversation/request",
metadata: {
callback: "IdeaToJSMessage",
},
payload: {
promptHash: message?.hash || "",
},
};
window.JSJavaBridge.callJava(JSON.stringify(params));
},
openLink: (message) => {
if (!message?.url) {
return false;
}
const params = {
action: "openLink/request",
metadata: {
callback: "IdeaToJSMessage",
},
payload: {
url: message?.url || "",
},
};
window.JSJavaBridge.callJava(JSON.stringify(params));
},
};
class IdeaBridge {
private static instance: IdeaBridge;
handle: any = {};
constructor() {
this.handle = {};
// 注册全局的回调函数用于接收来自IDEA的消息
window.IdeaToJSMessage = (res: any) => {
console.log("IdeaToJSMessage: ", res);
switch (res.action) {
case "deleteLastConversation/response":
this.resviceDeleteMessage(res);
break;
case "loadHistoryMessages/response":
this.resviceHistoryMessages(res);
break;
case "sendMessage/response":
this.resviceMessage(res);
break;
case "listModels/response":
this.resviceModelList(res);
break;
case "listContexts/response":
this.resviceContextList(res);
break;
case "listCommands/response":
this.resviceCommandList(res);
break;
// 这里暂时不用,因为获取到的只有 key信息不全
// 所以用 resviceSettings 来获取
// case "getKey/response":
// this.resviceAccessKey(res.payload.key);
// break;
case "addContext/notify":
this.resviesContext(res);
break;
case "getSetting/response":
this.resviceSettings(res);
break;
case "listTopics/response":
this.resviceTopicList(res);
break;
case "loadConversations/response":
this.resviceTopicDetail(res);
break;
default:
break;
}
};
}
resviceDeleteMessage(res) {
const hash = res?.payload?.promptHash || "";
this.handle.deletedChatMessage({
hash,
});
}
resviceHistoryMessages(res) {
const list: any = [];
if (res?.payload?.messages?.length > 0) {
res?.payload?.messages.forEach((item) => {
list.push({
...item,
response: item.responses?.join("\n"),
});
});
}
this.handle.reloadMessage({
entries: list.reverse(),
pageIndex: 0,
});
}
resviceTopicDetail(res) {
// 用于重置后端全局的 topic id
if (res?.payload?.reset) {
// 重置后请求历史消息
JStoIdea.historyMessages({ page: 0 });
}
}
resviceTopicList(res) {
const list = res.payload.topics;
this.handle.listTopics(list);
}
resviesContext(res) {
const params = {
file: res.payload.path,
result: "",
};
const contextObj = {
path: res.payload.path,
content: res.payload.content,
command: "",
};
params.result = JSON.stringify(contextObj);
this.handle.contextDetailResponse(params);
}
resviceSettings(res) {
// 用户设置的回调
const setting = res.payload.setting;
// 当前的默认模型
this.handle.getSetting({
value: setting.currentModel,
});
this.handle.getUserAccessKey({
endPoint: setting.apiBase,
accessKey: setting.apiKey,
keyType: setting.apiKey.startsWith("DC") ? "DevChat" : "OpenAi",
});
}
resviceAccessKey(res: string = "") {
const params = {
endPoint: "",
accessKey: res,
keyType: res.startsWith("DC") ? "DevChat" : "OpenAi",
};
this.handle.getUserAccessKey(params);
}
resviceCommandList(res) {
const result = res.payload.commands.map((item) => ({
name: item.name,
pattern: item.name,
description: item.description,
}));
this.handle.regCommandList({
result,
});
}
resviceContextList(res) {
// 接受到的上下文列表
const result = res.payload.contexts.map((item) => ({
name: item.command,
pattern: item.command,
description: item.description,
}));
this.handle.regContextList({ result });
}
resviceModelList(response: any) {
// 接受到模型列表
this.handle["regModelList"]({
result: response.payload.models,
});
}
resviceMessage(response: any) {
console.log(
"response.metadata.isFinalChunk: ",
response.metadata.isFinalChunk
);
// 接受到消息
if (response.metadata?.isFinalChunk) {
// 结束对话
this.handle["receiveMessage"]({
text: response.payload?.message || response.metadata?.error || "",
isError: response.metadata?.error.length > 0,
hash: response.payload?.promptHash || "",
});
} else {
this.handle["receiveMessagePartial"]({
text: response?.payload?.message || "",
});
}
}
public static getInstance(): IdeaBridge {
if (!IdeaBridge.instance) {
IdeaBridge.instance = new IdeaBridge();
}
return IdeaBridge.instance;
}
registerHandler(messageType: string, handler: any) {
// 注册回调函数
this.handle[messageType] = handler;
}
sendMessage(message: any) {
// 根据 command 分发到不同的方法·
switch (message.command) {
// 发送消息
case "sendMessage":
JStoIdea.sendMessage(
message.text,
message.contextInfo,
message.parent_hash
);
break;
// 重新生成消息,用于发送失败时再次发送
case "regeneration":
JStoIdea.sendMessage(message.text, message.context, message.parent);
break;
// 请求 model 列表
case "regModelList":
JStoIdea.getModel();
break;
case "regContextList":
JStoIdea.getContextList();
break;
case "regCommandList":
JStoIdea.getCommandList();
break;
case "code_apply":
JStoIdea.insertCode(message.content);
break;
case "code_file_apply":
JStoIdea.replaceFileContent(message.content);
break;
case "getUserAccessKey":
JStoIdea.getUserAccessKey();
break;
case "doCommand":
JStoIdea.etcCommand(message);
break;
case "show_diff":
JStoIdea.viewDiff(message.content);
break;
case "updateSetting":
JStoIdea.updateSetting(message.value);
break;
case "doCommit":
JStoIdea.commit(message.content);
break;
case "listTopics":
JStoIdea.getTopicList();
break;
case "getTopicDetail":
JStoIdea.getTopicDetail(message.topicHash);
break;
case "historyMessages":
JStoIdea.historyMessages(message);
break;
case "deleteChatMessage":
JStoIdea.deleteChatMessage(message);
break;
case "openLink":
JStoIdea.openLink(message);
break;
default:
break;
}
}
}
export default IdeaBridge.getInstance();

12
src/views/App.css Normal file
View File

@ -0,0 +1,12 @@
::-webkit-scrollbar {
background-color: transparent;
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-thumb {
background-color: grey;
border-radius: 6px;
}

View File

@ -5,6 +5,7 @@ import {
} from '@mantine/core';
import ChatPanel from '@/views/pages/ChatPanel';
import Head from '@/views/components/Header';
import './App.css';
export default function App() {
const theme = useMantineTheme();

View File

@ -42,6 +42,7 @@ export default function WechatTip() {
const [accessKey, setAccessKey] = useState("");
const [env, setEnv] = useState("prod");
const [loading, setLoading] = useState(false);
const platform = process.env.platform;
const getSettings = () => {
messageUtil.sendMessage({
@ -95,6 +96,15 @@ export default function WechatTip() {
);
}, []);
const openLink = (e) => {
e.preventDefault();
e.stopPropagation();
messageUtil.sendMessage({
command: "openLink",
url: envMap[env].link,
});
};
if (balance === null || balance === undefined) {
return null;
}
@ -105,6 +115,11 @@ export default function WechatTip() {
position="left"
width="200"
withArrow={true}
styles={{
arrow: {
borderColor: "var(--vscode-menu-border)",
},
}}
zIndex={999}
>
<HoverCard.Target>
@ -116,16 +131,25 @@ export default function WechatTip() {
</HoverCard.Target>
<HoverCard.Dropdown
sx={{
background: "var(--vscode-dropdown-background)",
borderColor: "var(--vscode-dropdown-border)",
color: "var(--vscode-foreground)",
borderColor: "var(--vscode-menu-border)",
backgroundColor: "var(--vscode-menu-background)",
}}
>
<Group style={{ width: "90%" }}>
<Text size="sm">
Your remaining credit is {formatCurrency(balance, currency)}. Sign
in to <a href={envMap[env].link}>web.devchat.ai </a>to{" "}
{bindWechat ? "purchase more tokens." : "earn additional ¥8"}
in to{" "}
{platform === "idea" ? (
<Text td="underline" c="blue" onClick={(e) => openLink(e)}>
web.devchat.ai{" "}
</Text>
) : (
<a href={envMap[env].link} target="_blank">
web.devchat.ai{" "}
</a>
)}
to {bindWechat ? "purchase more tokens." : "earn additional ¥8"}
</Text>
<LoadingOverlay visible={loading} />
</Group>

View File

@ -0,0 +1,111 @@
import React, { useEffect, useState } from "react";
import { ActionIcon, Drawer, Text, Box, Flex, Divider } from "@mantine/core";
import { IconClock, IconChevronDown } from "@tabler/icons-react";
import { useDisclosure } from "@mantine/hooks";
import messageUtil from "@/util/MessageUtil";
import dayjs from "dayjs";
export default function Topic({ styleName }) {
const [drawerOpened, { open: openDrawer, close: closeDrawer }] =
useDisclosure(false);
const [topicList, setTopicList] = useState([]);
useEffect(() => {
messageUtil.sendMessage({
command: "listTopics",
});
messageUtil.registerHandler("listTopics", (data) => {
setTopicList(data);
});
}, []);
const showTopic = (root_prompt: any) => {
closeDrawer();
messageUtil.sendMessage({
command: "getTopicDetail",
topicHash: root_prompt.hash,
});
};
return (
<>
<Drawer
opened={drawerOpened}
position="bottom"
title="Devchat Topic"
onClose={closeDrawer}
overlayProps={{ opacity: 0.5, blur: 4 }}
closeButtonProps={{ children: <IconChevronDown size="1rem" /> }}
styles={{
content: {
background: "var(--vscode-sideBar-background)",
color: "var(--vscode-editor-foreground)",
overflowY: "auto",
},
header: {
background: "var(--vscode-sideBar-background)",
color: "var(--vscode-editor-foreground)",
},
}}
>
{topicList.map((item: any, index) => (
<Box
sx={{
cursor: "pointer",
}}
onClick={() => showTopic(item?.root_prompt)}
>
<Flex justify="space-between" gap="sm">
<Text
fz="sm"
fw={700}
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
flex: 1,
}}
>
{item?.root_prompt.title}
</Text>
<Text
fz="sm"
c="dimmed"
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{dayjs(item?.latest_time * 1000).format("MM-DD HH:mm:ss")}
</Text>
</Flex>
<Text
c="dimmed"
fz="sm"
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{item?.root_prompt.responses?.[0]}
</Text>
{index !== topicList.length - 1 && (
<Divider variant="solid" my={6} opacity="0.5" />
)}
</Box>
))}
</Drawer>
<ActionIcon
className={styleName}
radius="xl"
variant="default"
onClick={openDrawer}
>
<IconClock size="1rem" />
</ActionIcon>
</>
);
}

View File

@ -5,6 +5,7 @@ import React, { useState, useEffect } from "react";
import { IconGitBranchChecked, IconShellCommand } from "@/views/components/ChatIcons";
import messageUtil from '@/util/MessageUtil';
import InputContexts from './InputContexts';
import Topic from './Topic';
import { observer } from "mobx-react-lite";
import { useMst } from "@/views/stores/RootStore";
import { ChatContext } from "@/views/stores/InputStore";
@ -36,6 +37,7 @@ const InputMessage = observer((props: any) => {
const { input, chat } = useMst();
const { contexts, menuOpend, menuType, currentMenuIndex, contextMenus, commandMenus,modelMenus } = input;
const { generating } = chat;
const showTopic = process.env.platform === 'idea';
const [drawerOpened, { open: openDrawer, close: closeDrawer }] = useDisclosure(false);
@ -388,6 +390,9 @@ const InputMessage = observer((props: any) => {
})}
</Menu.Dropdown>
</Menu>
{
showTopic && <Topic styleName={classes.actionIcon}/>
}
</Group>
{contexts && contexts.length > 0 &&
<Drawer

View File

@ -1,5 +1,13 @@
import { Stack, Container, Divider, Box, Group,Text, Button, createStyles } from "@mantine/core";
import {
Stack,
Container,
Divider,
Box,
Group,
Text,
Button,
createStyles,
} from "@mantine/core";
import React, { useEffect } from "react";
import MessageBody from "@/views/components/MessageBody";
import MessageAvatar from "@/views/components/MessageAvatar";
@ -8,96 +16,142 @@ import { useMst } from "@/views/stores/RootStore";
import { Message } from "@/views/stores/ChatStore";
import MessageContext from "@/views/components/MessageContext";
import CurrentMessage from "@/views/components/CurrentMessage";
import { Card } from '@mantine/core';
import { Card } from "@mantine/core";
import { IconInfoSquareRounded } from "@tabler/icons-react";
const useStyles = createStyles((theme) => ({
card:{
backgroundColor: 'var(--vscode-menu-background)',
fontFamily: 'var(--vscode-editor-font-familyy)',
fontSize: 'var(--vscode-editor-font-size)',
color: 'var(--vscode-menu-foreground)',
borderColor: 'var(--vscode-menu-border)',
card: {
backgroundColor: "var(--vscode-menu-background)",
fontFamily: "var(--vscode-editor-font-familyy)",
fontSize: "var(--vscode-editor-font-size)",
color: "var(--vscode-menu-foreground)",
borderColor: "var(--vscode-menu-border)",
},
cardDescription: {
marginTop: 10,
marginBottom: 10,
},
button: {
backgroundColor: "#ED6A45",
fontFamily: "var(--vscode-editor-font-familyy)",
fontSize: "var(--vscode-editor-font-size)",
color: "#fff",
"&:hover": {
backgroundColor: "#ED6A45",
opacity: 0.8,
},
cardDescription:{
marginTop: 10,
marginBottom: 10,
"&:focus": {
backgroundColor: "#ED6A45",
opacity: 0.8,
},
button:{
backgroundColor:"#ED6A45",
fontFamily: 'var(--vscode-editor-font-familyy)',
fontSize: 'var(--vscode-editor-font-size)',
color:"#fff",
"&:hover":{
backgroundColor:"#ED6A45",
opacity: 0.8,
},
"&:focus":{
backgroundColor:"#ED6A45",
opacity: 0.8,
}
}
}));
},
}));
const MessageList = observer((props: any) => {
const { chat } = useMst();
const {classes} = useStyles();
const { chat } = useMst();
const { classes } = useStyles();
return (<Stack spacing={0} sx={{margin:'0 10px 10px 10px'}}>
{chat.messages.map((item, index: number) => {
const { message: messageText, type: messageType, hash: messageHash, contexts, confirm } = item;
// setMessage(messageText);
return <Stack
spacing={0}
key={`message-${index}`}
sx={{
padding: 0,
margin: 0
}}>
<MessageAvatar
key={`message-header-${index}`}
showDelete={index === chat.messages.length - 2}
deleteHash={messageHash}
avatarType={messageType}
copyMessage={messageText}
messageContexts={contexts} />
<Box
key={`message-container-${index}`}
sx={{
margin: 0,
padding: 0,
pre: {
whiteSpace: 'break-spaces'
},
}}>
{ messageType === 'bot' && confirm && <Card shadow="sm" padding="xs" radius="md" withBorder className={classes.card}>
<Card.Section withBorder inheritPadding py="xs">
<Group position="left">
<IconInfoSquareRounded size={20} />
<Text fw={500}>GPT-4 Usage Required</Text>
</Group>
</Card.Section>
<Text className={classes.cardDescription}>
DevChat will make GPT-4 API calls to analyze up to ten source files, costing <strong>approximately $0.4 USD per question</strong>.
<br/>
<br/>
Would you like to proceed?
</Text>
<Group position="right" >
<Button size="compact-xs" className={classes.button} onClick={()=> chat.sendLastUserMessage() }>Yes</Button>
<Button size="compact-xs" className={classes.button} onClick={()=> chat.cancelDevchatAsk()}>No</Button>
</Group>
</Card>}
<MessageContext key={`message-context-${index}`} contexts={contexts} />
<MessageBody key={`message-codeblock-${index}`} messageType={messageType} messageDone={!(index === chat.messages.length -1 && chat.generating)}>
{messageText}
</MessageBody>
</Box >
{index !== chat.messages.length - 1 && <Divider my={3} key={`message-divider-${index}`} />}
</Stack >;
})}
<CurrentMessage />
</Stack>);
return (
<Stack spacing={0} sx={{ margin: "0 10px 10px 10px" }}>
{chat.messages.map((item, index: number) => {
const {
message: messageText,
type: messageType,
hash: messageHash,
contexts,
confirm,
} = item;
// setMessage(messageText);
return (
<Stack
spacing={0}
key={`message-${index}`}
sx={{
padding: 0,
margin: 0,
}}
>
<MessageAvatar
key={`message-header-${index}`}
showDelete={index === chat.messages.length - 2}
deleteHash={messageHash}
avatarType={messageType}
copyMessage={messageText}
messageContexts={contexts}
/>
<Box
key={`message-container-${index}`}
sx={{
margin: 0,
padding: 0,
pre: {
whiteSpace: "break-spaces",
},
}}
>
{messageType === "bot" && confirm && (
<Card
shadow="sm"
padding="xs"
radius="md"
withBorder
className={classes.card}
>
<Card.Section withBorder inheritPadding py="xs">
<Group position="left">
<IconInfoSquareRounded size={20} />
<Text fw={500}>GPT-4 Usage Required</Text>
</Group>
</Card.Section>
<Text className={classes.cardDescription}>
DevChat will make GPT-4 API calls to analyze up to ten
source files, costing{" "}
<strong>approximately $0.4 USD per question</strong>.
<br />
<br />
Would you like to proceed?
</Text>
<Group position="right">
<Button
size="compact-xs"
className={classes.button}
onClick={() => chat.sendLastUserMessage()}
>
Yes
</Button>
<Button
size="compact-xs"
className={classes.button}
onClick={() => chat.cancelDevchatAsk()}
>
No
</Button>
</Group>
</Card>
)}
<MessageContext
key={`message-context-${index}`}
contexts={contexts}
/>
<MessageBody
key={`message-codeblock-${index}`}
messageType={messageType}
messageDone={
!(index === chat.messages.length - 1 && chat.generating)
}
>
{messageText}
</MessageBody>
</Box>
{index !== chat.messages.length - 1 && (
<Divider my={3} key={`message-divider-${index}`} />
)}
</Stack>
);
})}
<CurrentMessage />
</Stack>
);
});
export default MessageList;
export default MessageList;

View File

@ -85,6 +85,9 @@ const chatPanel = observer(() => {
getSettings();
getFeatureToggles();
chat.fetchHistoryMessages({ pageIndex: 0 }).then();
messageUtil.registerHandler('reloadMessage',(message:any)=>{
chat.reloadMessage(message);
});
messageUtil.registerHandler(
"receiveMessagePartial",
(message: { text: string }) => {

View File

@ -332,6 +332,33 @@ ${yaml.dump(values)}
self.isTop = false;
self.isBottom = false;
},
reloadMessage:({entries,pageIndex})=>{
if (entries.length > 0) {
self.pageIndex = pageIndex;
const messages = entries
.map((entry, index) => {
const { hash, user, date, request, response, context } = entry;
const chatContexts = context?.map(({ content }) => {
return JSON.parse(content);
});
return [
{ type: 'user', message: request, contexts: chatContexts, date: date, hash: hash },
{ type: 'bot', message: response, date: date, hash: hash },
];
})
.flat();
if (self.pageIndex === 0) {
self.messages = messages;
} else if (self.pageIndex > 0) {
self.messages.concat(...messages);
}
} else {
self.isLastPage = true;
if (self.messages.length === 0) {
helpMessage(true);
}
}
},
fetchHistoryMessages: flow(function* (params: { pageIndex: number }) {
const { pageIndex, entries } = yield fetchHistoryMessages(params);
if (entries.length > 0) {

View File

@ -21,6 +21,6 @@
}
},
"exclude": [
"test"
"test","gui"
]
}

View File

@ -1,36 +1,37 @@
//@ts-check
'use strict';
"use strict";
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { DefinePlugin } = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { DefinePlugin } = require("webpack");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
//@ts-check
/** @typedef {import('webpack').Configuration} WebpackConfig **/
/** @type WebpackConfig */
const extensionConfig = {
name: 'vscode extension',
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')
name: "vscode extension",
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')
entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
entry: "./src/extension.ts", // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
output: {
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
path: path.resolve(__dirname, 'dist'),
filename: 'extension.js',
libraryTarget: 'commonjs2'
path: path.resolve(__dirname, "dist"),
filename: "extension.js",
libraryTarget: "commonjs2",
},
externals: {
vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
vscode: "commonjs vscode", // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
// modules added here also need to be added in the .vscodeignore file
},
resolve: {
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
extensions: ['.ts', '.json']
extensions: [".ts", ".json"],
},
module: {
rules: [
@ -39,45 +40,47 @@ const extensionConfig = {
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
loader: "babel-loader",
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript'
]
}
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript",
],
},
},
{
loader: 'ts-loader'
}
]
loader: "ts-loader",
},
],
},
]
],
},
devtool: 'nosources-source-map',
devtool: "nosources-source-map",
infrastructureLogging: {
level: "log", // enables logging required for problem matchers
},
plugins: []
plugins: [
new CleanWebpackPlugin()
],
};
/** @type WebpackConfig */
const webviewConfig = {
name: 'webview',
target: 'web',
mode: 'development',
name: "webview",
target: "web",
mode: "development",
entry: './src/index.tsx',
entry: "./src/index.tsx",
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
path: path.resolve(__dirname, "dist"),
filename: "index.js",
publicPath: "/",
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
extensions: [".ts", ".tsx", ".js", ".json"],
alias: {
'@': path.resolve(__dirname, 'src/')
"@": path.resolve(__dirname, "src/"),
},
},
module: {
@ -87,89 +90,88 @@ const webviewConfig = {
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
loader: "babel-loader",
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript'
]
}
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript",
],
},
},
{
loader: 'ts-loader'
}
]
loader: "ts-loader",
},
],
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
'@babel/preset-react',
],
]
use: [
{
loader: "babel-loader",
options: {
presets: [["@babel/preset-env", "@babel/preset-react"]],
},
},
}]
],
},
{
test: /\.css$/i,
use: [
{
loader: 'style-loader',
loader: "style-loader",
},
{
loader: 'css-loader',
loader: "css-loader",
options: {
modules: {
localIdentName: '[name]__[local]___[hash:base64:5]'
}
}
}
localIdentName: "[name]__[local]___[hash:base64:5]",
},
},
},
],
include: /views/
include: /views/,
},
{
test: /\.json$/i,
use: 'json-loader',
type: 'asset/source'
use: "json-loader",
type: "asset/source",
},
{
test: /\.(png|jpg|jpeg|gif|svg)$/, // 匹配文件类型
use: [
{
loader: 'file-loader', // 使用file-loader
loader: "file-loader", // 使用file-loader
options: {
name: '[name].[ext]', // 输出文件的名称和扩展名
outputPath: 'assets/', // 输出文件的路径
name: "[name].[ext]", // 输出文件的名称和扩展名
outputPath: "assets/", // 输出文件的路径
},
},
],
}
]
},
],
},
devtool: 'source-map',
devtool: "source-map",
infrastructureLogging: {
level: "log",
},
plugins: [
// generate an HTML file that includes the extension's JavaScript file
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src', 'index.html'),
filename: 'index.html',
chunks: ['index']
template: path.resolve(__dirname, "src", "index.html"),
filename: "index.html",
chunks: ["index"],
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src', 'welcome.html'),
filename: 'welcome.html',
chunks: ['welcome']
template: path.resolve(__dirname, "src", "welcome.html"),
filename: "welcome.html",
chunks: ["welcome"],
}),
]
new DefinePlugin({
"process.env.platform": JSON.stringify("vscode"),
}),
],
};
module.exports = [extensionConfig, webviewConfig];
module.exports = extensionConfig;

105
webpack.idea.config.js Normal file
View File

@ -0,0 +1,105 @@
//@ts-check
"use strict";
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const webpack = require("webpack");
const webviewConfig = {
name: "webview",
target: "web",
mode: "production",
entry: "./src/index.tsx",
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.js",
publicPath: "/",
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"],
alias: {
"@": path.resolve(__dirname, "src/"),
},
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript",
],
},
},
{
loader: "ts-loader",
},
],
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
presets: [["@babel/preset-env", "@babel/preset-react"]],
},
},
],
},
{
test: /\.css$/i,
use: [
{
loader: "style-loader",
},
{
loader: "css-loader",
options: {
modules: {
localIdentName: "[name]__[local]___[hash:base64:5]",
},
},
},
],
include: /views/,
},
{
test: /\.json$/i,
use: "json-loader",
type: "asset/source",
},
{
test: /\.(png|jpg|jpeg|gif|svg)$/, // 匹配文件类型
type: "asset/inline",
},
],
},
devtool: false,
infrastructureLogging: {
level: "log",
},
plugins: [
// generate an HTML file that includes the extension's JavaScript file
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "src", "main.html"),
filename: "main.html",
chunks: ["main"],
}),
new CleanWebpackPlugin(),
new webpack.ProgressPlugin(),
new webpack.DefinePlugin({
"process.env.platform": JSON.stringify("idea"),
}),
],
};
module.exports = webviewConfig;

8690
yarn.lock Normal file

File diff suppressed because it is too large Load Diff