Merge pull request #255 from devchat-ai/72-implement-ask-codebase-feature

72 implement ask codebase feature
This commit is contained in:
Rankin Zheng 2023-08-22 17:48:04 +08:00 committed by GitHub
commit 02fba2e811
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 990 additions and 462 deletions

941
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -507,6 +507,7 @@
"quote": "^0.4.0",
"react-markdown": "^8.0.7",
"react-syntax-highlighter": "^15.5.0",
"rehype-raw": "^6.1.1",
"shell-escape": "^0.2.0",
"string-argv": "^0.3.2",
"uuid": "^9.0.0"

View File

@ -40,6 +40,9 @@ function apiKeyMissedMessage(): LogEntry {
request: 'Is OPENAI_API_KEY ready?',
response: `
OPENAI_API_KEY is missing from your environment or settings. Kindly input your OpenAI or DevChat key, and I'll ensure DevChat is all set for you.
<button value="setting_openai_key">Set OpenAI key</button>
<button value="setting_devchat_key">Set DevChat key</button>
`,
context: []
} as LogEntry;
@ -170,7 +173,7 @@ export async function historyMessagesBase(): Promise<LoadHistoryMessages | undef
return {
command: 'loadHistoryMessages',
entries: logEntriesFlat.length>0? logEntriesFlat : [welcomeMessage()],
entries: logEntriesFlat.length > 0 ? logEntriesFlat : [],
} as LoadHistoryMessages;
}
@ -183,5 +186,7 @@ export async function onApiKeyBase(apiKey: string): Promise<{command: string, te
ApiKeyManager.writeApiKeySecret(apiKey);
const welcomeMessageText = welcomeMessage().response;
return { command: 'receiveMessage', text: `Your OPENAI_API_KEY is set. Enjoy DevChat!\n${welcomeMessageText}`, hash: '', user: 'system', date: '', isError: false };
return {
command: 'receiveMessage', text: `Your OPENAI_API_KEY is set. Enjoy DevChat!\n${welcomeMessageText}`, hash: '', user: 'system', date: '', isError: false
};
}

View File

@ -1,70 +0,0 @@
import { Container } from "@mantine/core";
import React from "react";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { okaidia } from "react-syntax-highlighter/dist/esm/styles/prism";
import CodeButtons from "./CodeButtons";
const CodeBlock = (props: any) => {
const { messageText, messageType } = props;
const LanguageCorner = (props: any) => {
const { language } = props;
return (<div style={{ position: 'absolute', top: 0, left: 0 }}>
{language && (
<div style={{
backgroundColor: '#333',
color: '#fff',
padding: '0.2rem 0.5rem',
borderRadius: '0.2rem',
fontSize: '0.8rem',
}}>
{language}
</div>
)}
</div>);
};
return (
messageType === 'bot'
? <ReactMarkdown
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
const value = String(children).replace(/\n$/, '');
return !inline && match ? (
<div style={{ position: 'relative' }}>
<LanguageCorner language={match[1]} />
<CodeButtons language={match[1]} code={value} />
<SyntaxHighlighter {...props} language={match[1]} customStyle={{ padding: '3em 1em 1em 2em', }} style={okaidia} PreTag="div">
{value}
</SyntaxHighlighter>
</div >
) : (
<code {...props} className={className}>
{children}
</code>
);
}
}}
>
{messageText}
</ReactMarkdown >
: <Container
sx={{
margin: 0,
padding: 0,
pre: {
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
},
}}>
<pre>{messageText}</pre>
</Container>
);
};
export default CodeBlock;

View File

@ -2,7 +2,7 @@
import React, { useEffect } from "react";
import { keyframes } from "@emotion/react";
import { Container, Text } from "@mantine/core";
import CodeBlock from "@/views/components/CodeBlock";
import MessageBody from "@/views/components/MessageBody";
import { observer } from "mobx-react-lite";
import { useMst } from "@/views/stores/RootStore";
import { Message } from "@/views/stores/ChatStore";
@ -48,29 +48,36 @@ const getBlocks = (message) => {
const CurrentMessage = observer((props: any) => {
const { width } = props;
const { chat } = useMst();
const { messages, currentMessage, generating, responsed, hasDone } = chat;
// split blocks
const messageBlocks = getBlocks(chat.currentMessage);
const lastMessageBlocks = getBlocks(chat.messages[chat.messages.length - 1]?.message);
const messageBlocks = getBlocks(currentMessage);
const lastMessageBlocks = getBlocks(messages[messages.length - 1]?.message);
const fixedCount = lastMessageBlocks.length;
const receivedCount = messageBlocks.length;
const renderBlocks = messageBlocks.splice(-1);
useEffect(() => {
if (chat.generating) {
if (generating) {
// new a bot message
const messageItem = Message.create({ type: 'bot', message: chat.currentMessage });
const messageItem = Message.create({ type: 'bot', message: currentMessage });
chat.newMessage(messageItem);
}
}, [chat.generating]);
}, [generating]);
useEffect(() => {
if (receivedCount - fixedCount >= 1 || !chat.responsed) {
chat.updateLastMessage(chat.currentMessage);
if (generating && (receivedCount - fixedCount >= 1 || !responsed)) {
chat.updateLastMessage(currentMessage);
}
}, [chat.currentMessage, chat.responsed]);
}, [currentMessage, responsed, generating]);
return chat.generating
useEffect(() => {
if (hasDone) {
chat.updateLastMessage(currentMessage);
}
}, [hasDone]);
return generating
? <Container
sx={{
margin: 0,
@ -80,7 +87,7 @@ const CurrentMessage = observer((props: any) => {
whiteSpace: 'break-spaces'
},
}}>
<CodeBlock messageText={renderBlocks.join('\n\n')} messageType="bot" />
<MessageBody messageText={renderBlocks.join('\n\n')} messageType="bot" />
<MessageBlink />
</Container>
: <></>;

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View File

@ -10,16 +10,27 @@ import { observer } from "mobx-react-lite";
import { useMst } from "@/views/stores/RootStore";
import { IMessage } from "@/views/stores/ChatStore";
import { IChatContext } from "@/views/stores/InputStore";
interface IProps {
item: IMessage,
item?: IMessage,
avatarType?: "user" | "bot" | "system",
copyMessage?: string,
messageContexts?: IChatContext[],
deleteHash?: string,
showEdit?: boolean,
showDelete: boolean
showDelete?: boolean
}
const MessageHeader = observer((props: IProps) => {
const { item, showEdit = false, showDelete = true } = props;
const { contexts, message, type, hash } = item;
const MessageAvatar = observer((props: IProps) => {
const {
messageContexts = [],
copyMessage = "",
deleteHash = undefined,
avatarType = "user",
showEdit = false,
showDelete = false
} = props;
const { input, chat } = useMst();
const [done, setDone] = React.useState(false);
return (<Flex
@ -30,7 +41,7 @@ const MessageHeader = observer((props: IProps) => {
direction="row"
wrap="wrap">
{
type === 'bot'
avatarType === 'bot'
? <Avatar
color="indigo"
size={25}
@ -42,8 +53,8 @@ const MessageHeader = observer((props: IProps) => {
radius="xl"
src={SvgAvatarUser} />
}
<Text weight='bold'>{type === 'bot' ? 'DevChat' : 'User'}</Text>
{type === 'user'
<Text weight='bold'>{avatarType === 'bot' ? 'DevChat' : 'User'}</Text>
{avatarType === 'user'
? <Flex
gap="xs"
justify="flex-end"
@ -54,8 +65,8 @@ const MessageHeader = observer((props: IProps) => {
<Tooltip sx={{ padding: '3px', fontSize: 'var(--vscode-editor-font-size)' }} label={done ? 'Refilled' : 'Refill prompt'} withArrow position="left" color="gray">
<ActionIcon size='sm'
onClick={() => {
input.setValue(message);
input.setContexts(contexts);
input.setValue(copyMessage);
input.setContexts(messageContexts);
setDone(true);
setTimeout(() => { setDone(false); }, 2000);
}}>
@ -70,11 +81,11 @@ const MessageHeader = observer((props: IProps) => {
<IconEdit size="1.125rem" />
</ActionIcon>
</Tooltip >}
{showDelete && hash !== 'message' && <Tooltip sx={{ padding: '3px', fontSize: 'var(--vscode-editor-font-size)' }} label="Delete message" withArrow position="left" color="gray">
{showDelete && deleteHash !== 'message' && <Tooltip sx={{ padding: '3px', fontSize: 'var(--vscode-editor-font-size)' }} label="Delete message" withArrow position="left" color="gray">
<ActionIcon size='sm'
onClick={() => {
if (item.hash) {
chat.deleteMessage(item).then();
if (deleteHash) {
chat.deleteMessage(deleteHash).then();
} else {
chat.popMessage();
chat.popMessage();
@ -84,7 +95,7 @@ const MessageHeader = observer((props: IProps) => {
</ActionIcon>
</Tooltip >}
</Flex >
: <CopyButton value={message} timeout={2000}>
: <CopyButton value={copyMessage} timeout={2000}>
{({ copied, copy }) => (
<Tooltip sx={{ padding: '3px', fontSize: 'var(--vscode-editor-font-size)' }} label={copied ? 'Copied' : 'Copy message'} withArrow position="left" color="gray">
<ActionIcon size='xs' color={copied ? 'teal' : 'gray'} onClick={copy} style={{ marginLeft: 'auto', marginRight: '10px' }}>
@ -97,4 +108,4 @@ const MessageHeader = observer((props: IProps) => {
</Flex >);
});
export default MessageHeader;
export default MessageAvatar;

View File

@ -0,0 +1,30 @@
import { Container } from "@mantine/core";
import React from "react";
import { observer } from "mobx-react-lite";
import MessageMarkdown from "@/views/components/MessageMarkdown";
interface IProps {
messageText: string,
messageType: string
}
const MessageBody = observer((props: IProps) => {
const { messageText, messageType } = props;
return (
messageType === 'bot'
? <MessageMarkdown messageText={messageText} />
: <Container
sx={{
margin: 0,
padding: 0,
pre: {
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
},
}}>
<pre>{messageText}</pre>
</Container>
);
});
export default MessageBody;

View File

@ -1,12 +1,8 @@
import { Center, Text, Accordion, Box, Stack, Container, Divider } from "@mantine/core";
import React from "react";
import CodeBlock from "@/views/components/CodeBlock";
import MessageHeader from "@/views/components/MessageHeader";
import { observer } from "mobx-react-lite";
import { types } from "mobx-state-tree";
import { useMst } from "@/views/stores/RootStore";
import { IInputStore } from "@/views/stores/InputStore";
import { Accordion, Box, Center, Text } from "@mantine/core";
import React from "react";
interface IProps {
contexts?: IInputStore['contexts'];
@ -89,44 +85,4 @@ const MessageContext = ({ contexts }: IProps) => {
</>);
};
const MessageContainer = observer((props: any) => {
const { width } = props;
const { chat } = useMst();
return (<>
{chat.messages.map((item, index: number) => {
const { message: messageText, type: messageType, contexts } = item;
// setMessage(messageText);
return <Stack
spacing={0}
key={`message-${index}`}
sx={{
width: width,
padding: 0,
margin: 0,
}}>
<MessageHeader
key={`message-header-${index}`}
showDelete={index === chat.messages.length - 2}
item={item} />
<Container
key={`message-container-${index}`}
sx={{
margin: 0,
padding: 0,
width: width,
pre: {
whiteSpace: 'break-spaces'
},
}}>
<MessageContext key={`message-context-${index}`} contexts={contexts} />
<CodeBlock key={`message-codeblock-${index}`} messageType={messageType} messageText={messageText} />
</Container >
{index !== chat.messages.length - 1 && <Divider my={3} key={`message-divider-${index}`} />}
</Stack >;
})}
</>);
});
export default MessageContainer;
export default MessageContext;

View File

@ -0,0 +1,54 @@
import { Stack, Container, Divider } from "@mantine/core";
import React, { useEffect } from "react";
import MessageBody from "@/views/components/MessageBody";
import MessageAvatar from "@/views/components/MessageAvatar";
import { observer } from "mobx-react-lite";
import { useMst } from "@/views/stores/RootStore";
import { Message } from "@/views/stores/ChatStore";
import MessageContext from "@/views/components/MessageContext";
const MessageList = observer((props: any) => {
const { width } = props;
const { chat } = useMst();
return (<>
{chat.messages.map((item, index: number) => {
const { message: messageText, type: messageType, hash: messageHash, contexts } = item;
// setMessage(messageText);
return <Stack
spacing={0}
key={`message-${index}`}
sx={{
width: width,
padding: 0,
margin: 0,
}}>
<MessageAvatar
key={`message-header-${index}`}
showDelete={index === chat.messages.length - 2}
deleteHash={messageHash}
avatarType={messageType}
copyMessage={messageText}
messageContexts={contexts} />
<Container
key={`message-container-${index}`}
sx={{
margin: 0,
padding: 0,
width: width,
pre: {
whiteSpace: 'break-spaces'
},
}}>
<MessageContext key={`message-context-${index}`} contexts={contexts} />
<MessageBody key={`message-codeblock-${index}`} messageType={messageType} messageText={messageText} />
</Container >
{index !== chat.messages.length - 1 && <Divider my={3} key={`message-divider-${index}`} />}
</Stack >;
})}
</>);
});
export default MessageList;

View File

@ -0,0 +1,154 @@
import { Button, Anchor } from "@mantine/core";
import React from "react";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { okaidia } from "react-syntax-highlighter/dist/esm/styles/prism";
import CodeButtons from "./CodeButtons";
import { observer } from "mobx-react-lite";
import { useMst } from "@/views/stores/RootStore";
import { Message } from "@/views/stores/ChatStore";
import messageUtil from '@/util/MessageUtil';
interface IProps {
messageText: string
}
const MessageMarkdown = observer((props: IProps) => {
const { messageText } = props;
const { chat } = useMst();
const LanguageCorner = (props: any) => {
const { language } = props;
return (<div style={{ position: 'absolute', top: 0, left: 0 }}>
{language && (
<div style={{
backgroundColor: '#333',
color: '#fff',
padding: '0.2rem 0.5rem',
borderRadius: '0.2rem',
fontSize: '0.8rem',
}}>
{language}
</div>
)}
</div>);
};
const handleExplain = (value: string | undefined) => {
console.log(value);
switch (value) {
case "#ask_code":
chat.addMessages([
Message.create({
type: 'user',
message: 'Explain /ask_code'
}),
Message.create({
type: 'bot',
message: `***/ask_code***
If you would like to ask questions related to your own codebase, you can enable and use the /ask_code feature of DevChat.
While /ask_code is being enabled, DevChat will need to index your codebase before you can use this feature. Indexing usually takes a while, depending on the size of your codebase, your computing power and the network. Once its done, you can ask questions about your codebase by typing the /ask_code command, followed by your question.
Example questions:
(Here we only show example questions from a few popular open-source projects codebases.)
How do I access POST form fields in Express?
How do I pass command line arguments to a Node.js program?
How do I print the value of a tensor object in TensorFlow?
How do I force Kubernetes to re-pull an image in Kubernetes?
How do I set focus on an input field after rendering in React?
How to index your codebase?
\`Please check DevChat.ask_code settings\` before enabling the feature, because once indexing has been started, changing the settings will not affect the process anymore, unless if you terminate it and re-index.
To enable, you can enter \`DevChat:Start AskCode Index\` in the Command Palette or click on the button to start indexing now.
<button value="settings">Settings</button>
<button value="start_indexing">Start Indexing</button>
`
}),
]);
}
chat.goScrollBottom();
};
const handleButton = (value: string | number | readonly string[] | undefined) => {
switch (value) {
case "settings": messageUtil.sendMessage({ command: 'doCommand', content: ['workbench.action.openSettings', 'DevChat'] }); break;
case "start_indexing": messageUtil.sendMessage({ command: 'doCommand', content: ['DevChat.AskCodeIndexStart'] }); break;
case "setting_openai_key": messageUtil.sendMessage({ command: 'doCommand', content: ['workbench.action.openSettings', 'DevChat: Api_key_OpenAI'] }); break;
case "setting_devchat_key": messageUtil.sendMessage({ command: 'doCommand', content: ['workbench.action.openSettings', 'DevChat: Access_key_DevChat'] }); break;
}
};
return <ReactMarkdown
rehypePlugins={[rehypeRaw]}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
const value = String(children).replace(/\n$/, '');
return !inline && match ? (
<div style={{ position: 'relative' }}>
<LanguageCorner language={match[1]} />
<CodeButtons language={match[1]} code={value} />
<SyntaxHighlighter {...props} language={match[1]} customStyle={{ padding: '3em 1em 1em 2em', }} style={okaidia} PreTag="div">
{value}
</SyntaxHighlighter>
</div >
) : (
<code {...props} className={className}>
{children}
</code>
);
},
button({ node, className, children, value, ...props }) {
return (
<Button
size='xs'
sx={{
backgroundColor: 'var(--vscode-button-background)',
}}
styles={{
icon: {
color: 'var(--vscode-button-foreground)'
},
label: {
color: 'var(--vscode-button-foreground)',
fontSize: 'var(--vscode-editor-font-size)',
}
}}
onClick={() => {
handleButton(value);
}}>
{children}
</Button>
);
},
a({ node, className, children, href, ...props }) {
const customAnchors = ["#code",
"#commit_message",
"#release_note",
"#ask_code",
"#extension"].filter((item) => item === href);
return customAnchors.length > 0
? <Anchor href={href} onClick={() => {
handleExplain(href);
}}>
{children}
</Anchor>
: <a {...props} href={href} className={className}>
{children}
</a>;
}
}}>
{messageText}
</ReactMarkdown >;
});
export default MessageMarkdown;

View File

@ -13,7 +13,7 @@ import { Message } from "@/views/stores/ChatStore";
import InputMessage from '@/views/components/InputMessage';
import MessageContainer from '../components/MessageContainer';
import MessageList from '@/views/components/MessageList';
import { IconCircleArrowDown, IconCircleArrowDownFilled } from '@tabler/icons-react';
@ -83,6 +83,10 @@ const chatPanel = observer(() => {
};
}, []);
useEffect(() => {
scrollToBottom();
}, [chat.scrollBottom]);
return (
<Container
ref={chatContainerRef}
@ -109,7 +113,7 @@ const chatPanel = observer(() => {
}}
onScrollPositionChange={onScrollPositionChange}
viewportRef={scrollViewport}>
<MessageContainer
<MessageList
width={chatContainerRect.width} />
<CurrentMessage width={chatContainerRect.width} />
{chat.errorMessage &&

View File

@ -39,11 +39,10 @@ export const fetchHistoryMessages = async (params) => {
});
};
export const deleteMessage = async (item: IMessage) => {
const { hash } = item;
export const deleteMessage = async (messageHash: string) => {
return new Promise<{ hash: string }>((resolve, reject) => {
try {
messageUtil.sendMessage({ command: 'deleteChatMessage', hash: hash });
messageUtil.sendMessage({ command: 'deleteChatMessage', hash: messageHash });
messageUtil.registerHandler('deletedChatMessage', (message) => {
resolve({
hash: message.hash
@ -74,6 +73,7 @@ export const ChatStore = types.model('Chat', {
isLastPage: false,
isBottom: true,
isTop: false,
scrollBottom: 0
})
.actions(self => ({
startGenerating: (text: string, chatContexts) => {
@ -149,6 +149,9 @@ export const ChatStore = types.model('Chat', {
newMessage: (message: IMessage) => {
self.messages.push(message);
},
addMessages: (messages: IMessage[]) => {
self.messages.push(...messages);
},
updateLastMessage: (message: string) => {
if (self.messages.length > 0) {
self.messages[self.messages.length - 1].message = message;
@ -178,6 +181,9 @@ export const ChatStore = types.model('Chat', {
self.isTop = false;
self.isBottom = false;
},
goScrollBottom: () => {
self.scrollBottom++;
},
fetchHistoryMessages: flow(function* (params: { pageIndex: number }) {
const { pageIndex, entries } = yield fetchHistoryMessages(params);
if (entries.length > 0) {
@ -201,10 +207,31 @@ export const ChatStore = types.model('Chat', {
}
} else {
self.isLastPage = true;
if (self.messages.length === 0) {
self.messages.push(
Message.create({
type: 'user',
message: "How do I use DevChat?"
}));
self.messages.push(
Message.create({
type: 'bot',
message: `
Do you want to write some code or have a question about the project? Simply right-click on your chosen files or code snippets and add them to DevChat. Feel free to ask me anything or let me help you with coding.
Don't forget to check out the "+" button on the left of the input to add more context. To see a list of workflows you can run in the context, just type "/". Happy prompting!
To get started, here are the things that DevChat can do:
[/ask_code: ask questions about your own codebase](#ask_code)
<button value="settings">Settings</button>
`}));
}
}
}),
deleteMessage: flow(function* (item: IMessage) {
const { hash } = yield deleteMessage(item);
deleteMessage: flow(function* (messageHash: string) {
const { hash } = yield deleteMessage(messageHash);
const index = self.messages.findIndex((item: any) => item.hash === hash);
if (index > -1) {
self.messages.splice(index);

View File

@ -69,7 +69,7 @@ export const InputStore = types
clearContexts() {
self.contexts.clear();
},
setContexts(contexts: IMessage['contexts']) {
setContexts(contexts: IChatContext[]) {
self.contexts.clear();
contexts?.forEach(context => {
self.contexts.push({ ...context });