diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index de39feaa..3bcfa6db 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -223,14 +223,14 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, if (!apiConfig) return { abort: () => { } } if ( - apiConfig.anthropic.apikey === "" && - apiConfig.greptile.apikey === "" && - apiConfig.openai.apikey === "" && - apiConfig.ollama.endpoint === "" && - apiConfig.ollama.model === "" + apiConfig.anthropic.apikey === '' && + apiConfig.greptile.apikey === '' && + apiConfig.openai.apikey === '' && + apiConfig.ollama.endpoint === '' && + apiConfig.ollama.model === '' && + apiConfig.whichApi === '' ) { getVSCodeAPI().postMessage({ type: 'displayError', message: 'Required API keys are not set.' }) - onFinalMessage("Required API keys are not set."); return { abort: () => { }} } diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index 835dcade..3eedae28 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -126,7 +126,7 @@ export function activate(context: vscode.ExtensionContext) { } else if (m.type === 'displayError') { - vscode.window.showWarningMessage(m.message, { modal: true }); + vscode.window.showErrorMessage(m.message, { modal: true }); } else { diff --git a/extensions/void/src/sidebar/Sidebar.tsx b/extensions/void/src/sidebar/Sidebar.tsx index 61426869..d4c35d1b 100644 --- a/extensions/void/src/sidebar/Sidebar.tsx +++ b/extensions/void/src/sidebar/Sidebar.tsx @@ -1,273 +1,390 @@ -import React, { useState, ChangeEvent, useEffect, useRef, useCallback, FormEvent } from "react" -import { ApiConfig, LLMMessage, sendLLMMessage } from "../common/sendLLMMessage" -import { Command, File, Selection, WebviewMessage } from "../shared_types" -import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi" +/* eslint-disable no-mixed-spaces-and-tabs */ +import React, { + useState, + ChangeEvent, + useEffect, + useRef, + useCallback, + FormEvent, +} from "react"; +import { + ApiConfig, + LLMMessage, + sendLLMMessage, +} from "../common/sendLLMMessage"; +import { Command, File, Selection, WebviewMessage } from "../shared_types"; +import { + awaitVSCodeResponse, + getVSCodeAPI, + resolveAwaitingVSCodeResponse, +} from "./getVscodeApi"; -import { marked } from 'marked'; +import { marked } from "marked"; import MarkdownRender, { BlockCode } from "./MarkdownRender"; -import * as vscode from 'vscode' - +import * as vscode from "vscode"; const filesStr = (fullFiles: File[]) => { - return fullFiles.map(({ filepath, content }) => - ` + return fullFiles + .map( + ({ filepath, content }) => + ` ${filepath.fsPath} \`\`\` ${content} -\`\`\``).join('\n') -} +\`\`\`` + ) + .join("\n"); +}; -const userInstructionsStr = (instructions: string, files: File[], selection: Selection | null) => { +const userInstructionsStr = ( + instructions: string, + files: File[], + selection: Selection | null +) => { return ` ${filesStr(files)} -${!selection ? '' : ` +${!selection + ? "" + : ` I am currently selecting this code: \`\`\`${selection.selectionStr}\`\`\` -`} +` + } Please edit the code following these instructions: ${instructions} If you make a change, rewrite the entire file. `; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply -} +}; - -const FilesSelector = ({ files, setFiles }: { files: vscode.Uri[], setFiles: (files: vscode.Uri[]) => void }) => { - return files.length !== 0 &&
- Include files: - {files.map((filename, i) => -
- {/* X button on a file */} - +const FilesSelector = ({ + files, + setFiles, +}: { + files: vscode.Uri[]; + setFiles: (files: vscode.Uri[]) => void; +}) => { + return ( + files.length !== 0 && ( +
+ Include files: + {files.map((filename, i) => ( +
+ {/* X button on a file */} + +
+ ))}
- )} -
-} + ) + ); +}; const IncludedFiles = ({ files }: { files: vscode.Uri[] }) => { - return files.length !== 0 &&
- {files.map((filename, i) => -
- + return ( + files.length !== 0 && ( +
+ {files.map((filename, i) => ( +
+ +
+ ))}
- )} -
-} - + ) + ); +}; const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => { + const role = chatMessage.role; + const children = chatMessage.displayContent; - const role = chatMessage.role - const children = chatMessage.displayContent + if (!children) return null; - if (!children) - return null + let chatbubbleContents: React.ReactNode; - let chatbubbleContents: React.ReactNode - - if (role === 'user') { - chatbubbleContents = <> - - {chatMessage.selection?.selectionStr && } - {children} - - } - else if (role === 'assistant') { + if (role === "user") { + chatbubbleContents = ( + <> + + {chatMessage.selection?.selectionStr && ( + + )} + {children} + + ); + } else if (role === "assistant") { const tokens = marked.lexer(children); // https://marked.js.org/using_pro#renderer - chatbubbleContents = // sectionsHTML + chatbubbleContents = ; // sectionsHTML } - - return
-
- {chatbubbleContents} + return ( +
+
+ {chatbubbleContents} +
-
-} + ); +}; const getBasename = (pathStr: string) => { // "unixify" path - pathStr = pathStr.replace(/[/\\]+/g, '/'); // replace any / or \ or \\ with / - const parts = pathStr.split('/') // split on / - return parts[parts.length - 1] -} - -type ChatMessage = { - role: 'user' - content: string, // content sent to the llm - displayContent: string, // content displayed to user - selection: Selection | null, // the user's selection - files: vscode.Uri[], // the files sent in the message -} | { - role: 'assistant', - content: string, // content received from LLM - displayContent: string // content displayed to user (this is the same as content for now) -} + pathStr = pathStr.replace(/[/\\]+/g, "/"); // replace any / or \ or \\ with / + const parts = pathStr.split("/"); // split on / + return parts[parts.length - 1]; +}; +type ChatMessage = + | { + role: "user"; + content: string; // content sent to the llm + displayContent: string; // content displayed to user + selection: Selection | null; // the user's selection + files: vscode.Uri[]; // the files sent in the message + } + | { + role: "assistant"; + content: string; // content received from LLM + displayContent: string; // content displayed to user (this is the same as content for now) + }; // const [stateRef, setState] = useInstantState(initVal) // setState instantly changes the value of stateRef instead of having to wait until the next render + const useInstantState = (initVal: T) => { - const stateRef = useRef(initVal) - const [_, setS] = useState(initVal) + const stateRef = useRef(initVal); + const [_, setS] = useState(initVal); const setState = useCallback((newVal: T) => { setS(newVal); stateRef.current = newVal; - }, []) - return [stateRef as React.RefObject, setState] as const // make s.current readonly - setState handles all changes -} - - + }, []); + return [stateRef as React.RefObject, setState] as const; // make s.current readonly - setState handles all changes +}; const Sidebar = () => { - // state of current message - const [selection, setSelection] = useState(null) // the code the user is selecting - const [files, setFiles] = useState([]) // the names of the files in the chat - const [instructions, setInstructions] = useState('') // the user's instructions + const [selection, setSelection] = useState(null); // the code the user is selecting + const [files, setFiles] = useState([]); // the names of the files in the chat + const [instructions, setInstructions] = useState(""); // the user's instructions // state of chat - const [chatMessageHistory, setChatHistory] = useState([]) - const [messageStream, setMessageStream] = useState('') - const [isLoading, setIsLoading] = useState(false) + const [chatMessageHistory, setChatHistory] = useState([]); + const [messageStream, setMessageStream] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [isDisabled, setIsDisabled] = useState(false); + const [errorShown, setErrorShown] = useState(false); - const abortFnRef = useRef<(() => void) | null>(null) - const [apiConfig, setApiConfig] = useState(null) + const abortFnRef = useRef<(() => void) | null>(null); + + const [apiConfig, setApiConfig] = useState(null); + + const checkApiConfig = (apiConfig: ApiConfig) => { + if ( + (apiConfig.anthropic.apikey === "" && + apiConfig.greptile.apikey === "" && + apiConfig.openai.apikey === "" && + (apiConfig.ollama.endpoint === "" || + apiConfig.ollama.model === "")) || + apiConfig.whichApi === "" + ) { + setIsDisabled(true); + } else { + setIsDisabled(false); + } + + } // get Api Config on mount useEffect(() => { - getVSCodeAPI().postMessage({ type: 'getApiConfig' }) - }, []) + getVSCodeAPI().postMessage({ type: "getApiConfig" }); + }, []); // Receive messages from the extension useEffect(() => { const listener = (event: MessageEvent) => { - const m = event.data as WebviewMessage; // resolve any awaiting promises // eg. it will resolve the promise below for `await VSCodeResponse('files')` - resolveAwaitingVSCodeResponse(m) + resolveAwaitingVSCodeResponse(m); // if user pressed ctrl+l, add their selection to the sidebar - if (m.type === 'ctrl+l') { + if (m.type === "ctrl+l") { + if (isDisabled) { + getVSCodeAPI().postMessage({ + type: "displayError", + message: "Required API keys are not set.", + }); + return; + } + setSelection(m.selection); - - setSelection(m.selection) - - const filepath = m.selection.filePath + const filepath = m.selection.filePath; // add file if it's not a duplicate - if (!files.find(f => f.fsPath === filepath.fsPath)) setFiles(files => [...files, filepath]) - + if (!files.find((f) => f.fsPath === filepath.fsPath)) + setFiles((files) => [...files, filepath]); } // when get apiConfig, set - else if (m.type === 'apiConfig') { - setApiConfig(m.apiConfig) + else if (m.type === "apiConfig") { + setApiConfig(m.apiConfig); + checkApiConfig(m.apiConfig); } + }; + window.addEventListener("message", listener); + return () => { + window.removeEventListener("message", listener); + }; + }, [files, selection, isDisabled]); - } - window.addEventListener('message', listener); - return () => { window.removeEventListener('message', listener) } - }, [files, selection]) - - - const formRef = useRef(null) + const formRef = useRef(null); const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (isLoading || isDisabled) return; - e.preventDefault() - if (isLoading) return - - setIsLoading(true) - setInstructions(''); + setIsLoading(true); + setInstructions(""); formRef.current?.reset(); // reset the form's text - setSelection(null) - setFiles([]) + setSelection(null); + setFiles([]); // request file content from vscode and await response - getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files }) - const relevantFiles = await awaitVSCodeResponse('files') + getVSCodeAPI().postMessage({ type: "requestFiles", filepaths: files }); + const relevantFiles = await awaitVSCodeResponse("files"); // add message to chat history - const content = userInstructionsStr(instructions, relevantFiles.files, selection) + const content = userInstructionsStr( + instructions, + relevantFiles.files, + selection + ); // console.log('prompt:\n', content) - const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files } - setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt]) + const newHistoryElt: ChatMessage = { + role: "user", + content, + displayContent: instructions, + selection, + files, + }; + setChatHistory((chatMessageHistory) => [ + ...chatMessageHistory, + newHistoryElt, + ]); // send message to claude let { abort } = sendLLMMessage({ - messages: [...chatMessageHistory.map(m => ({ role: m.role, content: m.content })), { role: 'user', content }], + messages: [ + ...chatMessageHistory.map((m) => ({ + role: m.role, + content: m.content, + })), + { role: "user", content }, + ], onText: (newText, fullText) => setMessageStream(fullText), onFinalMessage: (content) => { - // add assistant's message to chat history - const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, } - setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt]) + const newHistoryElt: ChatMessage = { + role: "assistant", + content, + displayContent: content, + }; + setChatHistory((chatMessageHistory) => [ + ...chatMessageHistory, + newHistoryElt, + ]); // clear selection - setMessageStream('') - setIsLoading(false) + setMessageStream(""); + setIsLoading(false); }, - apiConfig: apiConfig - }) - - - abortFnRef.current = abort - } + apiConfig: apiConfig, + }); + abortFnRef.current = abort; + }; const onStop = useCallback(() => { // abort claude - abortFnRef.current?.() + abortFnRef.current?.(); // if messageStream was not empty, add it to the history - const llmContent = messageStream || '(canceled)' - const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent } - setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt]) + const llmContent = messageStream || "(canceled)"; + const newHistoryElt: ChatMessage = { + role: "assistant", + displayContent: messageStream, + content: llmContent, + }; + setChatHistory((chatMessageHistory) => [ + ...chatMessageHistory, + newHistoryElt, + ]); - setMessageStream('') - setIsLoading(false) - - }, [messageStream]) + setMessageStream(""); + setIsLoading(false); + }, [messageStream]); //Clear code selection const clearSelection = () => { setSelection(null); }; - return <> -
-
- {/* previous messages */} - {chatMessageHistory.map((message, i) => - - )} - {/* message stream */} - -
- {/* chatbar */} -
- {/* selection */} -
- {/* selected files */} - - {/* selected code */} - {!selection?.selectionStr ? null - : ( + return ( + <> +
+
+ {/* previous messages */} + {chatMessageHistory.map((message, i) => ( + + ))} + {/* message stream */} + +
+ {/* chatbar */} +
+ {/* selection */} +
+ {/* selected files */} + + {/* selected code */} + {!selection?.selectionStr ? null : (
- +
- )} + )} +
+
{ + if (e.key === "Enter" && !e.shiftKey) onSubmit(e); + }} + onSubmit={(e) => { + console.log("submit!"); + e.preventDefault(); + onSubmit(e); + }} + > + {/* input */} + +