Merge pull request #255 from devchat-ai/72-implement-ask-codebase-feature
72 implement ask codebase feature
This commit is contained in:
commit
02fba2e811
941
package-lock.json
generated
941
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -157,10 +157,10 @@
|
|||||||
"when": "DevChat.llmModel == 'OpenAI'"
|
"when": "DevChat.llmModel == 'OpenAI'"
|
||||||
},
|
},
|
||||||
"DevChat.PythonVirtualEnv": {
|
"DevChat.PythonVirtualEnv": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "",
|
"default": "",
|
||||||
"description": "Path to the Python virtual environment for AskCode."
|
"description": "Path to the Python virtual environment for AskCode."
|
||||||
},
|
},
|
||||||
"DevChat.askcode.supportedFileTypes": {
|
"DevChat.askcode.supportedFileTypes": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": ".+\\.js$, .+\\.ts$, .+\\.jsx$, .+\\.tsx$, .+\\.java$, .+\\.py$, .+\\.go$, .+\\.rb$, .+\\.php$, .+\\.cpp$, .+\\.c$, .+\\.cs$, .+\\.swift$, .+\\.rs$, .+\\.sh$, .+\\.bash$, .+\\.zsh$, .+\\.m$, .+\\.mm$, .+\\.h$, .+\\.hpp$, .+\\.hh$, .+\\.html$, .+\\.htm$, .+\\.xhtml$, .+\\.xml$, .+\\.css$, .+\\.scss$, .+\\.sass$, .+\\.less$, .+\\.json$, .+\\.yaml$, .+\\.yml$, .+\\.toml$, .+\\.ini$, .+\\.md$, .+\\.markdown$, .+\\.txt$, .+\\.csv$, .+\\.sql$, .+\\.sqlite$, .+\\.db$, .+\\.hql$, .+\\.psql$, .+\\.pgsql$, .+\\.plpgsql$",
|
"default": ".+\\.js$, .+\\.ts$, .+\\.jsx$, .+\\.tsx$, .+\\.java$, .+\\.py$, .+\\.go$, .+\\.rb$, .+\\.php$, .+\\.cpp$, .+\\.c$, .+\\.cs$, .+\\.swift$, .+\\.rs$, .+\\.sh$, .+\\.bash$, .+\\.zsh$, .+\\.m$, .+\\.mm$, .+\\.h$, .+\\.hpp$, .+\\.hh$, .+\\.html$, .+\\.htm$, .+\\.xhtml$, .+\\.xml$, .+\\.css$, .+\\.scss$, .+\\.sass$, .+\\.less$, .+\\.json$, .+\\.yaml$, .+\\.yml$, .+\\.toml$, .+\\.ini$, .+\\.md$, .+\\.markdown$, .+\\.txt$, .+\\.csv$, .+\\.sql$, .+\\.sqlite$, .+\\.db$, .+\\.hql$, .+\\.psql$, .+\\.pgsql$, .+\\.plpgsql$",
|
||||||
@ -507,6 +507,7 @@
|
|||||||
"quote": "^0.4.0",
|
"quote": "^0.4.0",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
|
"rehype-raw": "^6.1.1",
|
||||||
"shell-escape": "^0.2.0",
|
"shell-escape": "^0.2.0",
|
||||||
"string-argv": "^0.3.2",
|
"string-argv": "^0.3.2",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
|
@ -40,6 +40,9 @@ function apiKeyMissedMessage(): LogEntry {
|
|||||||
request: 'Is OPENAI_API_KEY ready?',
|
request: 'Is OPENAI_API_KEY ready?',
|
||||||
response: `
|
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.
|
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: []
|
context: []
|
||||||
} as LogEntry;
|
} as LogEntry;
|
||||||
@ -74,7 +77,7 @@ export async function isWaitForApiKey() {
|
|||||||
return !isApiSet;
|
return !isApiSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadTopicHistoryLogs(topicId: string | undefined) : Promise<Array<LogEntry> | undefined> {
|
export async function loadTopicHistoryLogs(topicId: string | undefined): Promise<Array<LogEntry> | undefined> {
|
||||||
if (!topicId) {
|
if (!topicId) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -135,13 +138,13 @@ export function loadTopicHistoryFromCurrentMessageHistory(skip: number, count: n
|
|||||||
} as LoadHistoryMessages;
|
} as LoadHistoryMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiKeyInvalidMessage(): Promise<LoadHistoryMessages|undefined> {
|
export async function apiKeyInvalidMessage(): Promise<LoadHistoryMessages | undefined> {
|
||||||
const apiKey = await ApiKeyManager.getApiKey();
|
const apiKey = await ApiKeyManager.getApiKey();
|
||||||
isApiSet = true;
|
isApiSet = true;
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
const startMessage = [ apiKeyMissedMessage() ];
|
const startMessage = [apiKeyMissedMessage()];
|
||||||
isApiSet = false;
|
isApiSet = false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
command: 'loadHistoryMessages',
|
command: 'loadHistoryMessages',
|
||||||
entries: startMessage,
|
entries: startMessage,
|
||||||
@ -162,19 +165,19 @@ export async function historyMessagesBase(): Promise<LoadHistoryMessages | undef
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
updateCurrentMessageHistory(topicId!, logEntriesFlat);
|
updateCurrentMessageHistory(topicId!, logEntriesFlat);
|
||||||
|
|
||||||
const apiKeyMessage = await apiKeyInvalidMessage();
|
const apiKeyMessage = await apiKeyInvalidMessage();
|
||||||
if (apiKeyMessage !== undefined) {
|
if (apiKeyMessage !== undefined) {
|
||||||
return apiKeyMessage;
|
return apiKeyMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
command: 'loadHistoryMessages',
|
command: 'loadHistoryMessages',
|
||||||
entries: logEntriesFlat.length>0? logEntriesFlat : [welcomeMessage()],
|
entries: logEntriesFlat.length > 0 ? logEntriesFlat : [],
|
||||||
} as LoadHistoryMessages;
|
} as LoadHistoryMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function onApiKeyBase(apiKey: string): Promise<{command: string, text: string, hash: string, user: string, date: string, isError: boolean}> {
|
export async function onApiKeyBase(apiKey: string): Promise<{ command: string, text: string, hash: string, user: string, date: string, isError: boolean }> {
|
||||||
if (!isValidApiKey(apiKey)) {
|
if (!isValidApiKey(apiKey)) {
|
||||||
return { command: 'receiveMessage', text: 'Your API key is invalid. We support OpenAI and DevChat keys. Please reset the key.', hash: '', user: 'system', date: '', isError: false };
|
return { command: 'receiveMessage', text: 'Your API key is invalid. We support OpenAI and DevChat keys. Please reset the key.', hash: '', user: 'system', date: '', isError: false };
|
||||||
}
|
}
|
||||||
@ -182,6 +185,8 @@ export async function onApiKeyBase(apiKey: string): Promise<{command: string, te
|
|||||||
isApiSet = true;
|
isApiSet = true;
|
||||||
ApiKeyManager.writeApiKeySecret(apiKey);
|
ApiKeyManager.writeApiKeySecret(apiKey);
|
||||||
|
|
||||||
const welcomeMessageText = welcomeMessage().response;
|
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
|
||||||
|
};
|
||||||
}
|
}
|
@ -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;
|
|
@ -2,7 +2,7 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { keyframes } from "@emotion/react";
|
import { keyframes } from "@emotion/react";
|
||||||
import { Container, Text } from "@mantine/core";
|
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 { observer } from "mobx-react-lite";
|
||||||
import { useMst } from "@/views/stores/RootStore";
|
import { useMst } from "@/views/stores/RootStore";
|
||||||
import { Message } from "@/views/stores/ChatStore";
|
import { Message } from "@/views/stores/ChatStore";
|
||||||
@ -48,29 +48,36 @@ const getBlocks = (message) => {
|
|||||||
const CurrentMessage = observer((props: any) => {
|
const CurrentMessage = observer((props: any) => {
|
||||||
const { width } = props;
|
const { width } = props;
|
||||||
const { chat } = useMst();
|
const { chat } = useMst();
|
||||||
|
const { messages, currentMessage, generating, responsed, hasDone } = chat;
|
||||||
|
|
||||||
// split blocks
|
// split blocks
|
||||||
const messageBlocks = getBlocks(chat.currentMessage);
|
const messageBlocks = getBlocks(currentMessage);
|
||||||
const lastMessageBlocks = getBlocks(chat.messages[chat.messages.length - 1]?.message);
|
const lastMessageBlocks = getBlocks(messages[messages.length - 1]?.message);
|
||||||
const fixedCount = lastMessageBlocks.length;
|
const fixedCount = lastMessageBlocks.length;
|
||||||
const receivedCount = messageBlocks.length;
|
const receivedCount = messageBlocks.length;
|
||||||
const renderBlocks = messageBlocks.splice(-1);
|
const renderBlocks = messageBlocks.splice(-1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chat.generating) {
|
if (generating) {
|
||||||
// new a bot message
|
// 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.newMessage(messageItem);
|
||||||
}
|
}
|
||||||
}, [chat.generating]);
|
}, [generating]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (receivedCount - fixedCount >= 1 || !chat.responsed) {
|
if (generating && (receivedCount - fixedCount >= 1 || !responsed)) {
|
||||||
chat.updateLastMessage(chat.currentMessage);
|
chat.updateLastMessage(currentMessage);
|
||||||
}
|
}
|
||||||
}, [chat.currentMessage, chat.responsed]);
|
}, [currentMessage, responsed, generating]);
|
||||||
|
|
||||||
return chat.generating
|
useEffect(() => {
|
||||||
|
if (hasDone) {
|
||||||
|
chat.updateLastMessage(currentMessage);
|
||||||
|
}
|
||||||
|
}, [hasDone]);
|
||||||
|
|
||||||
|
return generating
|
||||||
? <Container
|
? <Container
|
||||||
sx={{
|
sx={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
@ -80,7 +87,7 @@ const CurrentMessage = observer((props: any) => {
|
|||||||
whiteSpace: 'break-spaces'
|
whiteSpace: 'break-spaces'
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
<CodeBlock messageText={renderBlocks.join('\n\n')} messageType="bot" />
|
<MessageBody messageText={renderBlocks.join('\n\n')} messageType="bot" />
|
||||||
<MessageBlink />
|
<MessageBlink />
|
||||||
</Container>
|
</Container>
|
||||||
: <></>;
|
: <></>;
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
@ -10,16 +10,27 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { useMst } from "@/views/stores/RootStore";
|
import { useMst } from "@/views/stores/RootStore";
|
||||||
|
|
||||||
import { IMessage } from "@/views/stores/ChatStore";
|
import { IMessage } from "@/views/stores/ChatStore";
|
||||||
|
import { IChatContext } from "@/views/stores/InputStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
item: IMessage,
|
item?: IMessage,
|
||||||
|
avatarType?: "user" | "bot" | "system",
|
||||||
|
copyMessage?: string,
|
||||||
|
messageContexts?: IChatContext[],
|
||||||
|
deleteHash?: string,
|
||||||
showEdit?: boolean,
|
showEdit?: boolean,
|
||||||
showDelete: boolean
|
showDelete?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageHeader = observer((props: IProps) => {
|
const MessageAvatar = observer((props: IProps) => {
|
||||||
const { item, showEdit = false, showDelete = true } = props;
|
const {
|
||||||
const { contexts, message, type, hash } = item;
|
messageContexts = [],
|
||||||
|
copyMessage = "",
|
||||||
|
deleteHash = undefined,
|
||||||
|
avatarType = "user",
|
||||||
|
showEdit = false,
|
||||||
|
showDelete = false
|
||||||
|
} = props;
|
||||||
const { input, chat } = useMst();
|
const { input, chat } = useMst();
|
||||||
const [done, setDone] = React.useState(false);
|
const [done, setDone] = React.useState(false);
|
||||||
return (<Flex
|
return (<Flex
|
||||||
@ -30,7 +41,7 @@ const MessageHeader = observer((props: IProps) => {
|
|||||||
direction="row"
|
direction="row"
|
||||||
wrap="wrap">
|
wrap="wrap">
|
||||||
{
|
{
|
||||||
type === 'bot'
|
avatarType === 'bot'
|
||||||
? <Avatar
|
? <Avatar
|
||||||
color="indigo"
|
color="indigo"
|
||||||
size={25}
|
size={25}
|
||||||
@ -42,8 +53,8 @@ const MessageHeader = observer((props: IProps) => {
|
|||||||
radius="xl"
|
radius="xl"
|
||||||
src={SvgAvatarUser} />
|
src={SvgAvatarUser} />
|
||||||
}
|
}
|
||||||
<Text weight='bold'>{type === 'bot' ? 'DevChat' : 'User'}</Text>
|
<Text weight='bold'>{avatarType === 'bot' ? 'DevChat' : 'User'}</Text>
|
||||||
{type === 'user'
|
{avatarType === 'user'
|
||||||
? <Flex
|
? <Flex
|
||||||
gap="xs"
|
gap="xs"
|
||||||
justify="flex-end"
|
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">
|
<Tooltip sx={{ padding: '3px', fontSize: 'var(--vscode-editor-font-size)' }} label={done ? 'Refilled' : 'Refill prompt'} withArrow position="left" color="gray">
|
||||||
<ActionIcon size='sm'
|
<ActionIcon size='sm'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
input.setValue(message);
|
input.setValue(copyMessage);
|
||||||
input.setContexts(contexts);
|
input.setContexts(messageContexts);
|
||||||
setDone(true);
|
setDone(true);
|
||||||
setTimeout(() => { setDone(false); }, 2000);
|
setTimeout(() => { setDone(false); }, 2000);
|
||||||
}}>
|
}}>
|
||||||
@ -70,11 +81,11 @@ const MessageHeader = observer((props: IProps) => {
|
|||||||
<IconEdit size="1.125rem" />
|
<IconEdit size="1.125rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip >}
|
</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'
|
<ActionIcon size='sm'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (item.hash) {
|
if (deleteHash) {
|
||||||
chat.deleteMessage(item).then();
|
chat.deleteMessage(deleteHash).then();
|
||||||
} else {
|
} else {
|
||||||
chat.popMessage();
|
chat.popMessage();
|
||||||
chat.popMessage();
|
chat.popMessage();
|
||||||
@ -84,7 +95,7 @@ const MessageHeader = observer((props: IProps) => {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip >}
|
</Tooltip >}
|
||||||
</Flex >
|
</Flex >
|
||||||
: <CopyButton value={message} timeout={2000}>
|
: <CopyButton value={copyMessage} timeout={2000}>
|
||||||
{({ copied, copy }) => (
|
{({ copied, copy }) => (
|
||||||
<Tooltip sx={{ padding: '3px', fontSize: 'var(--vscode-editor-font-size)' }} label={copied ? 'Copied' : 'Copy message'} withArrow position="left" color="gray">
|
<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' }}>
|
<ActionIcon size='xs' color={copied ? 'teal' : 'gray'} onClick={copy} style={{ marginLeft: 'auto', marginRight: '10px' }}>
|
||||||
@ -97,4 +108,4 @@ const MessageHeader = observer((props: IProps) => {
|
|||||||
</Flex >);
|
</Flex >);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default MessageHeader;
|
export default MessageAvatar;
|
30
src/views/components/MessageBody/index.tsx
Normal file
30
src/views/components/MessageBody/index.tsx
Normal 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;
|
@ -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 { IInputStore } from "@/views/stores/InputStore";
|
||||||
|
import { Accordion, Box, Center, Text } from "@mantine/core";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
contexts?: IInputStore['contexts'];
|
contexts?: IInputStore['contexts'];
|
||||||
@ -89,44 +85,4 @@ const MessageContext = ({ contexts }: IProps) => {
|
|||||||
</>);
|
</>);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default MessageContext;
|
||||||
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;
|
|
54
src/views/components/MessageList/index.tsx
Normal file
54
src/views/components/MessageList/index.tsx
Normal 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;
|
154
src/views/components/MessageMarkdown/index.tsx
Normal file
154
src/views/components/MessageMarkdown/index.tsx
Normal 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 it’s 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;
|
@ -13,7 +13,7 @@ import { Message } from "@/views/stores/ChatStore";
|
|||||||
|
|
||||||
|
|
||||||
import InputMessage from '@/views/components/InputMessage';
|
import InputMessage from '@/views/components/InputMessage';
|
||||||
import MessageContainer from '../components/MessageContainer';
|
import MessageList from '@/views/components/MessageList';
|
||||||
import { IconCircleArrowDown, IconCircleArrowDownFilled } from '@tabler/icons-react';
|
import { IconCircleArrowDown, IconCircleArrowDownFilled } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
|
||||||
@ -83,6 +83,10 @@ const chatPanel = observer(() => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [chat.scrollBottom]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
ref={chatContainerRef}
|
ref={chatContainerRef}
|
||||||
@ -109,7 +113,7 @@ const chatPanel = observer(() => {
|
|||||||
}}
|
}}
|
||||||
onScrollPositionChange={onScrollPositionChange}
|
onScrollPositionChange={onScrollPositionChange}
|
||||||
viewportRef={scrollViewport}>
|
viewportRef={scrollViewport}>
|
||||||
<MessageContainer
|
<MessageList
|
||||||
width={chatContainerRect.width} />
|
width={chatContainerRect.width} />
|
||||||
<CurrentMessage width={chatContainerRect.width} />
|
<CurrentMessage width={chatContainerRect.width} />
|
||||||
{chat.errorMessage &&
|
{chat.errorMessage &&
|
||||||
|
@ -39,11 +39,10 @@ export const fetchHistoryMessages = async (params) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteMessage = async (item: IMessage) => {
|
export const deleteMessage = async (messageHash: string) => {
|
||||||
const { hash } = item;
|
|
||||||
return new Promise<{ hash: string }>((resolve, reject) => {
|
return new Promise<{ hash: string }>((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
messageUtil.sendMessage({ command: 'deleteChatMessage', hash: hash });
|
messageUtil.sendMessage({ command: 'deleteChatMessage', hash: messageHash });
|
||||||
messageUtil.registerHandler('deletedChatMessage', (message) => {
|
messageUtil.registerHandler('deletedChatMessage', (message) => {
|
||||||
resolve({
|
resolve({
|
||||||
hash: message.hash
|
hash: message.hash
|
||||||
@ -74,6 +73,7 @@ export const ChatStore = types.model('Chat', {
|
|||||||
isLastPage: false,
|
isLastPage: false,
|
||||||
isBottom: true,
|
isBottom: true,
|
||||||
isTop: false,
|
isTop: false,
|
||||||
|
scrollBottom: 0
|
||||||
})
|
})
|
||||||
.actions(self => ({
|
.actions(self => ({
|
||||||
startGenerating: (text: string, chatContexts) => {
|
startGenerating: (text: string, chatContexts) => {
|
||||||
@ -149,6 +149,9 @@ export const ChatStore = types.model('Chat', {
|
|||||||
newMessage: (message: IMessage) => {
|
newMessage: (message: IMessage) => {
|
||||||
self.messages.push(message);
|
self.messages.push(message);
|
||||||
},
|
},
|
||||||
|
addMessages: (messages: IMessage[]) => {
|
||||||
|
self.messages.push(...messages);
|
||||||
|
},
|
||||||
updateLastMessage: (message: string) => {
|
updateLastMessage: (message: string) => {
|
||||||
if (self.messages.length > 0) {
|
if (self.messages.length > 0) {
|
||||||
self.messages[self.messages.length - 1].message = message;
|
self.messages[self.messages.length - 1].message = message;
|
||||||
@ -178,6 +181,9 @@ export const ChatStore = types.model('Chat', {
|
|||||||
self.isTop = false;
|
self.isTop = false;
|
||||||
self.isBottom = false;
|
self.isBottom = false;
|
||||||
},
|
},
|
||||||
|
goScrollBottom: () => {
|
||||||
|
self.scrollBottom++;
|
||||||
|
},
|
||||||
fetchHistoryMessages: flow(function* (params: { pageIndex: number }) {
|
fetchHistoryMessages: flow(function* (params: { pageIndex: number }) {
|
||||||
const { pageIndex, entries } = yield fetchHistoryMessages(params);
|
const { pageIndex, entries } = yield fetchHistoryMessages(params);
|
||||||
if (entries.length > 0) {
|
if (entries.length > 0) {
|
||||||
@ -201,10 +207,31 @@ export const ChatStore = types.model('Chat', {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.isLastPage = true;
|
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) {
|
deleteMessage: flow(function* (messageHash: string) {
|
||||||
const { hash } = yield deleteMessage(item);
|
const { hash } = yield deleteMessage(messageHash);
|
||||||
const index = self.messages.findIndex((item: any) => item.hash === hash);
|
const index = self.messages.findIndex((item: any) => item.hash === hash);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
self.messages.splice(index);
|
self.messages.splice(index);
|
||||||
|
@ -69,7 +69,7 @@ export const InputStore = types
|
|||||||
clearContexts() {
|
clearContexts() {
|
||||||
self.contexts.clear();
|
self.contexts.clear();
|
||||||
},
|
},
|
||||||
setContexts(contexts: IMessage['contexts']) {
|
setContexts(contexts: IChatContext[]) {
|
||||||
self.contexts.clear();
|
self.contexts.clear();
|
||||||
contexts?.forEach(context => {
|
contexts?.forEach(context => {
|
||||||
self.contexts.push({ ...context });
|
self.contexts.push({ ...context });
|
||||||
|
Loading…
x
Reference in New Issue
Block a user