mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
merge ollama
This commit is contained in:
parent
74c610632e
commit
631f724138
8 changed files with 288 additions and 439 deletions
|
|
@ -14,15 +14,15 @@ function getNonce() {
|
||||||
export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
|
export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
|
||||||
public static readonly viewId = 'void.viewnumberone';
|
public static readonly viewId = 'void.viewnumberone';
|
||||||
|
|
||||||
public webview: Promise<vscode.Webview> // used to send messages to the webview
|
public webview: Promise<vscode.Webview> // used to send messages to the webview, resolved by _res in resolveWebviewView
|
||||||
|
private _res: (c: vscode.Webview) => void // used to resolve the webview
|
||||||
|
|
||||||
private readonly _extensionUri: vscode.Uri
|
private readonly _extensionUri: vscode.Uri
|
||||||
private _res: (c: vscode.Webview) => void // used to resolve the webview
|
|
||||||
private allowed_urls: string[] = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com', 'http://localhost:11434'];
|
private _webviewView?: vscode.WebviewView; // only used inside onDidChangeConfiguration
|
||||||
private _webviewView?: vscode.WebviewView;
|
|
||||||
|
|
||||||
constructor(context: vscode.ExtensionContext) {
|
constructor(context: vscode.ExtensionContext) {
|
||||||
// const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later, not sure for what though... was included in webviewProvider code
|
// const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later... was included in webviewProvider code
|
||||||
this._extensionUri = context.extensionUri
|
this._extensionUri = context.extensionUri
|
||||||
|
|
||||||
let temp_res: typeof this._res | undefined = undefined
|
let temp_res: typeof this._res | undefined = undefined
|
||||||
|
|
@ -32,23 +32,25 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
|
||||||
|
|
||||||
vscode.workspace.onDidChangeConfiguration(event => {
|
vscode.workspace.onDidChangeConfiguration(event => {
|
||||||
if (event.affectsConfiguration('void.ollamaSettings.endpoint')) {
|
if (event.affectsConfiguration('void.ollamaSettings.endpoint')) {
|
||||||
this.updateAllowedUrls();
|
|
||||||
// Regenerate the webview's HTML content
|
|
||||||
if (this._webviewView) {
|
if (this._webviewView) {
|
||||||
this._webviewView.webview.html = this.getWebviewContent(this._webviewView.webview);
|
this.updateWebviewHTML(this._webviewView.webview);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getWebviewContent(webview: vscode.Webview): string {
|
private updateWebviewHTML(webview: vscode.Webview) {
|
||||||
|
const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com'];
|
||||||
|
const ollamaEndpoint: string | undefined = vscode.workspace.getConfiguration('void').get('ollamaSettings.endpoint');
|
||||||
|
if (ollamaEndpoint)
|
||||||
|
allowed_urls.push(ollamaEndpoint);
|
||||||
|
|
||||||
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/index.js'));
|
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/index.js'));
|
||||||
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/styles.css'));
|
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/styles.css'));
|
||||||
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri));
|
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri));
|
||||||
const nonce = getNonce();
|
const nonce = getNonce();
|
||||||
|
|
||||||
const allowed_urls = this.allowed_urls;
|
const webviewHTML = `<!DOCTYPE html>
|
||||||
return `<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|
@ -63,12 +65,10 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
|
||||||
<script nonce="${nonce}" src="${scriptUri}"></script>
|
<script nonce="${nonce}" src="${scriptUri}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
|
|
||||||
|
webview.html = webviewHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateAllowedUrls() {
|
|
||||||
const ollamaEndpoint: string = vscode.workspace.getConfiguration('void').get('ollamaSettings.endpoint') || 'http://localhost:11434';
|
|
||||||
this.allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com', ollamaEndpoint];
|
|
||||||
}
|
|
||||||
|
|
||||||
// called internally by vscode
|
// called internally by vscode
|
||||||
resolveWebviewView(
|
resolveWebviewView(
|
||||||
|
|
@ -76,31 +76,18 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
|
||||||
context: vscode.WebviewViewResolveContext,
|
context: vscode.WebviewViewResolveContext,
|
||||||
token: vscode.CancellationToken,
|
token: vscode.CancellationToken,
|
||||||
) {
|
) {
|
||||||
this._webviewView = webviewView;
|
|
||||||
|
|
||||||
const webview = webviewView.webview
|
const webview = webviewView.webview;
|
||||||
|
|
||||||
webview.options = {
|
webview.options = {
|
||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
localResourceRoots: [this._extensionUri]
|
localResourceRoots: [this._extensionUri]
|
||||||
};
|
};
|
||||||
|
|
||||||
// This allows us to use React in vscode
|
this.updateWebviewHTML(webview);
|
||||||
// when you run `npm run build`, we take the React code in the `sidebar` folder
|
|
||||||
// and compile it into `dist/sidebar/index.js` and `dist/sidebar/styles.css`
|
|
||||||
// we render that code here
|
|
||||||
const rootPath = this._extensionUri;
|
|
||||||
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath, 'dist/sidebar/index.js'));
|
|
||||||
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath, 'dist/sidebar/styles.css'));
|
|
||||||
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath));
|
|
||||||
|
|
||||||
const nonce = getNonce(); // only scripts with the nonce are allowed to run, this is a recommended security measure
|
|
||||||
|
|
||||||
// Regenerate the Sidebar html whenever the allowed_urls changes
|
|
||||||
this.updateAllowedUrls();
|
|
||||||
const allowed_urls = this.allowed_urls;
|
|
||||||
webview.html = this.getWebviewContent(webview);
|
|
||||||
|
|
||||||
|
// resolve webview and _webviewView
|
||||||
this._res(webview);
|
this._res(webview);
|
||||||
|
this._webviewView = webviewView;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,11 +105,13 @@ const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
|
||||||
// OpenAI
|
// OpenAI
|
||||||
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
||||||
|
|
||||||
let did_abort = false
|
let didAbort = false
|
||||||
let fullText = ''
|
let fullText = ''
|
||||||
|
|
||||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||||
let abort: () => void = () => { did_abort = true }
|
let abort: () => void = () => {
|
||||||
|
didAbort = true;
|
||||||
|
};
|
||||||
|
|
||||||
const openai = new OpenAI({ apiKey: apiConfig.openai.apikey, dangerouslyAllowBrowser: true });
|
const openai = new OpenAI({ apiKey: apiConfig.openai.apikey, dangerouslyAllowBrowser: true });
|
||||||
|
|
||||||
|
|
@ -120,13 +122,13 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
|
||||||
})
|
})
|
||||||
.then(async response => {
|
.then(async response => {
|
||||||
abort = () => {
|
abort = () => {
|
||||||
// response.controller.abort() // this isn't needed now, to keep consistency with claude will leave it commented
|
// response.controller.abort()
|
||||||
did_abort = true;
|
didAbort = true;
|
||||||
}
|
}
|
||||||
// when receive text
|
// when receive text
|
||||||
try {
|
try {
|
||||||
for await (const chunk of response) {
|
for await (const chunk of response) {
|
||||||
if (did_abort) return;
|
if (didAbort) return;
|
||||||
const newText = chunk.choices[0]?.delta?.content || '';
|
const newText = chunk.choices[0]?.delta?.content || '';
|
||||||
fullText += newText;
|
fullText += newText;
|
||||||
onText(newText, fullText);
|
onText(newText, fullText);
|
||||||
|
|
@ -138,8 +140,50 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
|
||||||
console.error('Error in OpenAI stream:', error);
|
console.error('Error in OpenAI stream:', error);
|
||||||
onFinalMessage(fullText);
|
onFinalMessage(fullText);
|
||||||
}
|
}
|
||||||
// when we get the final message on this stream
|
})
|
||||||
onFinalMessage(fullText)
|
return { abort };
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Ollama
|
||||||
|
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
||||||
|
|
||||||
|
let didAbort = false
|
||||||
|
let fullText = ""
|
||||||
|
|
||||||
|
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||||
|
let abort = () => {
|
||||||
|
didAbort = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ollama = new Ollama({ host: apiConfig.ollama.endpoint })
|
||||||
|
|
||||||
|
ollama.chat({
|
||||||
|
model: apiConfig.ollama.model,
|
||||||
|
messages: messages,
|
||||||
|
stream: true,
|
||||||
|
})
|
||||||
|
.then(async stream => {
|
||||||
|
abort = () => {
|
||||||
|
// ollama.abort()
|
||||||
|
didAbort = true
|
||||||
|
}
|
||||||
|
// iterate through the stream
|
||||||
|
try {
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
if (didAbort) return;
|
||||||
|
const newText = chunk.message.content;
|
||||||
|
fullText += newText;
|
||||||
|
onText(newText, fullText);
|
||||||
|
}
|
||||||
|
onFinalMessage(fullText);
|
||||||
|
}
|
||||||
|
// when error/fail
|
||||||
|
catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
onFinalMessage(fullText);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return { abort };
|
return { abort };
|
||||||
};
|
};
|
||||||
|
|
@ -152,11 +196,11 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
|
||||||
|
|
||||||
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
||||||
|
|
||||||
let did_abort = false
|
let didAbort = false
|
||||||
let fullText = ''
|
let fullText = ''
|
||||||
|
|
||||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||||
let abort: () => void = () => { did_abort = true }
|
let abort: () => void = () => { didAbort = true }
|
||||||
|
|
||||||
|
|
||||||
fetch('https://api.greptile.com/v2/query', {
|
fetch('https://api.greptile.com/v2/query', {
|
||||||
|
|
@ -180,7 +224,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
|
||||||
})
|
})
|
||||||
// TODO make this actually stream, right now it just sends one message at the end
|
// TODO make this actually stream, right now it just sends one message at the end
|
||||||
.then(async responseArr => {
|
.then(async responseArr => {
|
||||||
if (did_abort)
|
if (didAbort)
|
||||||
return
|
return
|
||||||
|
|
||||||
for (let response of responseArr) {
|
for (let response of responseArr) {
|
||||||
|
|
@ -215,28 +259,13 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
|
||||||
|
|
||||||
return { abort }
|
return { abort }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
||||||
if (!apiConfig) return { abort: () => { } }
|
if (!apiConfig) return { abort: () => { } }
|
||||||
|
|
||||||
if (
|
|
||||||
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.' })
|
|
||||||
return { abort: () => { }}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
switch (apiConfig.whichApi) {
|
switch (apiConfig.whichApi) {
|
||||||
case 'anthropic':
|
case 'anthropic':
|
||||||
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig });
|
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig });
|
||||||
|
|
@ -249,41 +278,7 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText,
|
||||||
default:
|
default:
|
||||||
console.error(`Error: whichApi was ${apiConfig.whichApi}, which is not recognized!`);
|
console.error(`Error: whichApi was ${apiConfig.whichApi}, which is not recognized!`);
|
||||||
return { abort: () => { } }
|
return { abort: () => { } }
|
||||||
//return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); // TODO
|
//return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); // TODO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Ollama
|
|
||||||
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
|
||||||
const ollamaClient = new Ollama({ host: apiConfig.ollama.endpoint })
|
|
||||||
|
|
||||||
let didAbort = false;
|
|
||||||
let fullText = "";
|
|
||||||
|
|
||||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
|
||||||
const abort = () => {
|
|
||||||
didAbort = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
ollamaClient.chat({
|
|
||||||
model: apiConfig.ollama.model,
|
|
||||||
messages: messages,
|
|
||||||
stream: true,
|
|
||||||
})
|
|
||||||
.then(async (stream) => {
|
|
||||||
for await (const chunk of stream) {
|
|
||||||
if (didAbort) return;
|
|
||||||
const newText = chunk.message.content;
|
|
||||||
fullText += newText;
|
|
||||||
onText(newText, fullText);
|
|
||||||
}
|
|
||||||
onFinalMessage(fullText);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
onFinalMessage(fullText);
|
|
||||||
});
|
|
||||||
|
|
||||||
return { abort };
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,8 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
// send contents to webview
|
// send contents to webview
|
||||||
webview.postMessage({ type: 'files', files, } satisfies WebviewMessage)
|
webview.postMessage({ type: 'files', files, } satisfies WebviewMessage)
|
||||||
|
|
||||||
} else if (m.type === 'applyCode') {
|
}
|
||||||
|
else if (m.type === 'applyCode') {
|
||||||
|
|
||||||
const editor = vscode.window.activeTextEditor
|
const editor = vscode.window.activeTextEditor
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
|
|
@ -124,19 +125,16 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
const oldContents = await readFileContentOfUri(editor.document.uri)
|
const oldContents = await readFileContentOfUri(editor.document.uri)
|
||||||
const suggestedEdits = getDiffedLines(oldContents, m.code)
|
const suggestedEdits = getDiffedLines(oldContents, m.code)
|
||||||
await approvalCodeLensProvider.addNewApprovals(editor, suggestedEdits)
|
await approvalCodeLensProvider.addNewApprovals(editor, suggestedEdits)
|
||||||
} else if (m.type === 'getApiConfig') {
|
}
|
||||||
|
else if (m.type === 'getApiConfig') {
|
||||||
|
|
||||||
const apiConfig = getApiConfig()
|
const apiConfig = getApiConfig()
|
||||||
console.log('Api config:', apiConfig)
|
console.log('Api config:', apiConfig)
|
||||||
|
|
||||||
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
|
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
|
||||||
|
|
||||||
} else if (m.type === 'displayError') {
|
|
||||||
|
|
||||||
vscode.window.showErrorMessage(m.message, { modal: true });
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
||||||
console.error('unrecognized command', m.type, m)
|
console.error('unrecognized command', m.type, m)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,6 @@ type WebviewMessage = (
|
||||||
// editor -> sidebar
|
// editor -> sidebar
|
||||||
| { type: 'apiConfig', apiConfig: ApiConfig }
|
| { type: 'apiConfig', apiConfig: ApiConfig }
|
||||||
|
|
||||||
// Display native vscode error
|
|
||||||
| { type: 'displayError', message: string } //
|
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Command = WebviewMessage['type']
|
type Command = WebviewMessage['type']
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,31 @@
|
||||||
/* eslint-disable no-mixed-spaces-and-tabs */
|
import React, { useState, ChangeEvent, useEffect, useRef, useCallback, FormEvent } from "react"
|
||||||
import React, {
|
import { ApiConfig, LLMMessage, sendLLMMessage } from "../common/sendLLMMessage"
|
||||||
useState,
|
import { Command, File, Selection, WebviewMessage } from "../shared_types"
|
||||||
ChangeEvent,
|
import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi"
|
||||||
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 MarkdownRender, { BlockCode } from "./MarkdownRender";
|
||||||
|
|
||||||
import * as vscode from "vscode";
|
import * as vscode from 'vscode'
|
||||||
|
|
||||||
|
|
||||||
const filesStr = (fullFiles: File[]) => {
|
const filesStr = (fullFiles: File[]) => {
|
||||||
return fullFiles
|
return fullFiles.map(({ filepath, content }) =>
|
||||||
.map(
|
`
|
||||||
({ filepath, content }) =>
|
|
||||||
`
|
|
||||||
${filepath.fsPath}
|
${filepath.fsPath}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
${content}
|
${content}
|
||||||
\`\`\``
|
\`\`\``).join('\n')
|
||||||
)
|
}
|
||||||
.join("\n");
|
|
||||||
};
|
|
||||||
|
|
||||||
const userInstructionsStr = (
|
const userInstructionsStr = (instructions: string, files: File[], selection: Selection | null) => {
|
||||||
instructions: string,
|
|
||||||
files: File[],
|
|
||||||
selection: Selection | null
|
|
||||||
) => {
|
|
||||||
return `
|
return `
|
||||||
${filesStr(files)}
|
${filesStr(files)}
|
||||||
|
|
||||||
${!selection
|
${!selection ? '' : `
|
||||||
? ""
|
|
||||||
: `
|
|
||||||
I am currently selecting this code:
|
I am currently selecting this code:
|
||||||
\`\`\`${selection.selectionStr}\`\`\`
|
\`\`\`${selection.selectionStr}\`\`\`
|
||||||
`
|
`}
|
||||||
}
|
|
||||||
|
|
||||||
Please edit the code following these instructions:
|
Please edit the code following these instructions:
|
||||||
${instructions}
|
${instructions}
|
||||||
|
|
@ -62,303 +36,235 @@ If you make a change, rewrite the entire file.
|
||||||
|
|
||||||
|
|
||||||
const FilesSelector = ({ files, setFiles }: { files: vscode.Uri[], setFiles: (files: vscode.Uri[]) => void }) => {
|
const FilesSelector = ({ files, setFiles }: { files: vscode.Uri[], setFiles: (files: vscode.Uri[]) => void }) => {
|
||||||
return files.length !== 0 && (
|
return files.length !== 0 && <div className='my-2'>
|
||||||
<div className='my-2'>
|
Include files:
|
||||||
Include files:
|
{files.map((filename, i) =>
|
||||||
{files.map((filename, i) =>
|
<div key={i} className='flex'>
|
||||||
<div key={i} className='flex'>
|
{/* X button on a file */}
|
||||||
{/* X button on a file */}
|
<button type='button' onClick={() => {
|
||||||
<button type='button' onClick={() => {
|
let file_index = files.indexOf(filename)
|
||||||
let file_index = files.indexOf(filename)
|
setFiles([...files.slice(0, file_index), ...files.slice(file_index + 1, Infinity)])
|
||||||
setFiles([...files.slice(0, file_index), ...files.slice(file_index + 1, Infinity)])
|
}}>
|
||||||
}}>
|
-{' '}<span className='text-gray-500'>{getBasename(filename.fsPath)}</span>
|
||||||
-{' '}<span className='text-gray-500'>{getBasename(filename.fsPath)}</span>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const IncludedFiles = ({ files }: { files: vscode.Uri[] }) => {
|
const IncludedFiles = ({ files }: { files: vscode.Uri[] }) => {
|
||||||
return files.length !== 0 && (
|
return files.length !== 0 && <div className='text-xs my-2'>
|
||||||
<div className='text-xs my-2'>
|
{files.map((filename, i) =>
|
||||||
{files.map((filename, i) =>
|
<div key={i} className='flex'>
|
||||||
<div key={i} className='flex'>
|
<button type='button'
|
||||||
<button type='button'
|
className='btn btn-secondary pointer-events-none'
|
||||||
className='btn btn-secondary pointer-events-none'
|
onClick={() => {
|
||||||
onClick={() => {
|
// TODO redirect to the document filename.fsPath, when add this remove pointer-events-none
|
||||||
// TODO redirect to the document filename.fsPath, when add this remove pointer-events-none
|
|
||||||
}}>
|
}}>
|
||||||
-{' '}<span className='text-gray-100'>{getBasename(filename.fsPath)}</span>
|
-{' '}<span className='text-gray-100'>{getBasename(filename.fsPath)}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
|
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
|
||||||
const role = chatMessage.role;
|
|
||||||
const children = chatMessage.displayContent;
|
|
||||||
|
|
||||||
if (!children) return null;
|
const role = chatMessage.role
|
||||||
|
const children = chatMessage.displayContent
|
||||||
|
|
||||||
let chatbubbleContents: React.ReactNode;
|
if (!children)
|
||||||
|
return null
|
||||||
|
|
||||||
if (role === "user") {
|
let chatbubbleContents: React.ReactNode
|
||||||
chatbubbleContents = (
|
|
||||||
<>
|
if (role === 'user') {
|
||||||
<IncludedFiles files={chatMessage.files} />
|
chatbubbleContents = <>
|
||||||
{chatMessage.selection?.selectionStr && (
|
<IncludedFiles files={chatMessage.files} />
|
||||||
<BlockCode
|
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} disableApplyButton={true} />}
|
||||||
text={chatMessage.selection.selectionStr}
|
{children}
|
||||||
disableApplyButton={true}
|
</>
|
||||||
/>
|
}
|
||||||
)}
|
else if (role === 'assistant') {
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if (role === "assistant") {
|
|
||||||
const tokens = marked.lexer(children); // https://marked.js.org/using_pro#renderer
|
const tokens = marked.lexer(children); // https://marked.js.org/using_pro#renderer
|
||||||
chatbubbleContents = <MarkdownRender tokens={tokens} />; // sectionsHTML
|
chatbubbleContents = <MarkdownRender tokens={tokens} /> // sectionsHTML
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
|
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
|
||||||
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
|
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
|
||||||
{chatbubbleContents}
|
{chatbubbleContents}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
};
|
}
|
||||||
|
|
||||||
const getBasename = (pathStr: string) => {
|
const getBasename = (pathStr: string) => {
|
||||||
// "unixify" path
|
// "unixify" path
|
||||||
pathStr = pathStr.replace(/[/\\]+/g, "/"); // replace any / or \ or \\ with /
|
pathStr = pathStr.replace(/[/\\]+/g, '/'); // replace any / or \ or \\ with /
|
||||||
const parts = pathStr.split("/"); // split on /
|
const parts = pathStr.split('/') // split on /
|
||||||
return parts[parts.length - 1];
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
// const [stateRef, setState] = useInstantState(initVal)
|
||||||
// setState instantly changes the value of stateRef instead of having to wait until the next render
|
// setState instantly changes the value of stateRef instead of having to wait until the next render
|
||||||
|
|
||||||
const useInstantState = <T,>(initVal: T) => {
|
const useInstantState = <T,>(initVal: T) => {
|
||||||
const stateRef = useRef<T>(initVal);
|
const stateRef = useRef<T>(initVal)
|
||||||
const [_, setS] = useState<T>(initVal);
|
const [_, setS] = useState<T>(initVal)
|
||||||
const setState = useCallback((newVal: T) => {
|
const setState = useCallback((newVal: T) => {
|
||||||
setS(newVal);
|
setS(newVal);
|
||||||
stateRef.current = newVal;
|
stateRef.current = newVal;
|
||||||
}, []);
|
}, [])
|
||||||
return [stateRef as React.RefObject<T>, setState] as const; // make s.current readonly - setState handles all changes
|
return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
|
|
||||||
// state of current message
|
// state of current message
|
||||||
const [selection, setSelection] = useState<Selection | null>(null); // the code the user is selecting
|
const [selection, setSelection] = useState<Selection | null>(null) // the code the user is selecting
|
||||||
const [files, setFiles] = useState<vscode.Uri[]>([]); // the names of the files in the chat
|
const [files, setFiles] = useState<vscode.Uri[]>([]) // the names of the files in the chat
|
||||||
const [instructions, setInstructions] = useState(""); // the user's instructions
|
const [instructions, setInstructions] = useState('') // the user's instructions
|
||||||
|
|
||||||
// state of chat
|
// state of chat
|
||||||
const [chatMessageHistory, setChatHistory] = useState<ChatMessage[]>([]);
|
const [chatMessageHistory, setChatHistory] = useState<ChatMessage[]>([])
|
||||||
const [messageStream, setMessageStream] = useState("");
|
const [messageStream, setMessageStream] = useState('')
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [isDisabled, setIsDisabled] = useState(false);
|
|
||||||
const [errorShown, setErrorShown] = useState(false);
|
|
||||||
|
|
||||||
const abortFnRef = useRef<(() => void) | null>(null);
|
const abortFnRef = useRef<(() => void) | null>(null)
|
||||||
|
|
||||||
const [apiConfig, setApiConfig] = useState<ApiConfig | null>(null);
|
const [apiConfig, setApiConfig] = useState<ApiConfig | null>(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
|
// get Api Config on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getVSCodeAPI().postMessage({ type: "getApiConfig" });
|
getVSCodeAPI().postMessage({ type: 'getApiConfig' })
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
// Receive messages from the extension
|
// Receive messages from the extension
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = (event: MessageEvent) => {
|
const listener = (event: MessageEvent) => {
|
||||||
|
|
||||||
const m = event.data as WebviewMessage;
|
const m = event.data as WebviewMessage;
|
||||||
// resolve any awaiting promises
|
// resolve any awaiting promises
|
||||||
// eg. it will resolve the promise below for `await VSCodeResponse('files')`
|
// 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 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);
|
|
||||||
|
|
||||||
const filepath = m.selection.filePath;
|
setSelection(m.selection)
|
||||||
|
|
||||||
|
const filepath = m.selection.filePath
|
||||||
|
|
||||||
// add file if it's not a duplicate
|
// add file if it's not a duplicate
|
||||||
if (!files.find((f) => f.fsPath === filepath.fsPath))
|
if (!files.find(f => f.fsPath === filepath.fsPath)) setFiles(files => [...files, filepath])
|
||||||
setFiles((files) => [...files, filepath]);
|
|
||||||
}
|
}
|
||||||
// when get apiConfig, set
|
// when get apiConfig, set
|
||||||
else if (m.type === "apiConfig") {
|
else if (m.type === 'apiConfig') {
|
||||||
setApiConfig(m.apiConfig);
|
setApiConfig(m.apiConfig)
|
||||||
checkApiConfig(m.apiConfig);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
window.addEventListener("message", listener);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("message", listener);
|
|
||||||
};
|
|
||||||
}, [files, selection, isDisabled]);
|
|
||||||
|
|
||||||
const formRef = useRef<HTMLFormElement | null>(null);
|
}
|
||||||
|
window.addEventListener('message', listener);
|
||||||
|
return () => { window.removeEventListener('message', listener) }
|
||||||
|
}, [files, selection])
|
||||||
|
|
||||||
|
|
||||||
|
const formRef = useRef<HTMLFormElement | null>(null)
|
||||||
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
|
||||||
if (isLoading || isDisabled) return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
e.preventDefault()
|
||||||
setInstructions("");
|
if (isLoading) return
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setInstructions('');
|
||||||
formRef.current?.reset(); // reset the form's text
|
formRef.current?.reset(); // reset the form's text
|
||||||
setSelection(null);
|
setSelection(null)
|
||||||
setFiles([]);
|
setFiles([])
|
||||||
|
|
||||||
// request file content from vscode and await response
|
// request file content from vscode and await response
|
||||||
getVSCodeAPI().postMessage({ type: "requestFiles", filepaths: files });
|
getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
|
||||||
const relevantFiles = await awaitVSCodeResponse("files");
|
const relevantFiles = await awaitVSCodeResponse('files')
|
||||||
|
|
||||||
// add message to chat history
|
// add message to chat history
|
||||||
const content = userInstructionsStr(
|
const content = userInstructionsStr(instructions, relevantFiles.files, selection)
|
||||||
instructions,
|
|
||||||
relevantFiles.files,
|
|
||||||
selection
|
|
||||||
);
|
|
||||||
// console.log('prompt:\n', content)
|
// console.log('prompt:\n', content)
|
||||||
const newHistoryElt: ChatMessage = {
|
const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files }
|
||||||
role: "user",
|
setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
|
||||||
content,
|
|
||||||
displayContent: instructions,
|
|
||||||
selection,
|
|
||||||
files,
|
|
||||||
};
|
|
||||||
setChatHistory((chatMessageHistory) => [
|
|
||||||
...chatMessageHistory,
|
|
||||||
newHistoryElt,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// send message to claude
|
// send message to claude
|
||||||
let { abort } = sendLLMMessage({
|
let { abort } = sendLLMMessage({
|
||||||
messages: [
|
messages: [...chatMessageHistory.map(m => ({ role: m.role, content: m.content })), { role: 'user', content }],
|
||||||
...chatMessageHistory.map((m) => ({
|
|
||||||
role: m.role,
|
|
||||||
content: m.content,
|
|
||||||
})),
|
|
||||||
{ role: "user", content },
|
|
||||||
],
|
|
||||||
onText: (newText, fullText) => setMessageStream(fullText),
|
onText: (newText, fullText) => setMessageStream(fullText),
|
||||||
onFinalMessage: (content) => {
|
onFinalMessage: (content) => {
|
||||||
|
|
||||||
// add assistant's message to chat history
|
// add assistant's message to chat history
|
||||||
const newHistoryElt: ChatMessage = {
|
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
|
||||||
role: "assistant",
|
setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
|
||||||
content,
|
|
||||||
displayContent: content,
|
|
||||||
};
|
|
||||||
setChatHistory((chatMessageHistory) => [
|
|
||||||
...chatMessageHistory,
|
|
||||||
newHistoryElt,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// clear selection
|
// clear selection
|
||||||
setMessageStream("");
|
setMessageStream('')
|
||||||
setIsLoading(false);
|
setIsLoading(false)
|
||||||
},
|
},
|
||||||
apiConfig: apiConfig,
|
apiConfig: apiConfig
|
||||||
});
|
})
|
||||||
|
abortFnRef.current = abort
|
||||||
|
|
||||||
abortFnRef.current = abort;
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const onStop = useCallback(() => {
|
const onStop = useCallback(() => {
|
||||||
// abort claude
|
// abort claude
|
||||||
abortFnRef.current?.();
|
abortFnRef.current?.()
|
||||||
|
|
||||||
// if messageStream was not empty, add it to the history
|
// if messageStream was not empty, add it to the history
|
||||||
const llmContent = messageStream || "(canceled)";
|
const llmContent = messageStream || '(canceled)'
|
||||||
const newHistoryElt: ChatMessage = {
|
const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent }
|
||||||
role: "assistant",
|
setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
|
||||||
displayContent: messageStream,
|
|
||||||
content: llmContent,
|
|
||||||
};
|
|
||||||
setChatHistory((chatMessageHistory) => [
|
|
||||||
...chatMessageHistory,
|
|
||||||
newHistoryElt,
|
|
||||||
]);
|
|
||||||
|
|
||||||
setMessageStream("");
|
setMessageStream('')
|
||||||
setIsLoading(false);
|
setIsLoading(false)
|
||||||
}, [messageStream]);
|
|
||||||
|
}, [messageStream])
|
||||||
|
|
||||||
//Clear code selection
|
//Clear code selection
|
||||||
const clearSelection = () => {
|
const clearSelection = () => {
|
||||||
setSelection(null);
|
setSelection(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <>
|
||||||
<>
|
<div className="flex flex-col h-screen w-full">
|
||||||
<div
|
<div className="overflow-y-auto overflow-x-hidden space-y-4">
|
||||||
className={`flex flex-col h-screen w-full ${isDisabled ? 'no-select' : ''}`}
|
{/* previous messages */}
|
||||||
>
|
{chatMessageHistory.map((message, i) =>
|
||||||
<div className="overflow-y-auto overflow-x-hidden space-y-4">
|
<ChatBubble key={i} chatMessage={message} />
|
||||||
{/* previous messages */}
|
)}
|
||||||
{chatMessageHistory.map((message, i) => (
|
{/* message stream */}
|
||||||
<ChatBubble key={i} chatMessage={message} />
|
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream }} />
|
||||||
))}
|
</div>
|
||||||
{/* message stream */}
|
{/* chatbar */}
|
||||||
<ChatBubble
|
<div className="shrink-0 py-4">
|
||||||
chatMessage={{
|
{/* selection */}
|
||||||
role: "assistant",
|
<div className="text-left">
|
||||||
content: messageStream,
|
{/* selected files */}
|
||||||
displayContent: messageStream,
|
<FilesSelector files={files} setFiles={setFiles} />
|
||||||
}}
|
{/* selected code */}
|
||||||
/>
|
{!selection?.selectionStr ? null
|
||||||
</div>
|
: (
|
||||||
{/* chatbar */}
|
|
||||||
<div className="shrink-0 py-4">
|
|
||||||
{/* selection */}
|
|
||||||
<div className="text-left">
|
|
||||||
{/* selected files */}
|
|
||||||
<FilesSelector files={files} setFiles={setFiles} />
|
|
||||||
{/* selected code */}
|
|
||||||
{!selection?.selectionStr ? null : (
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={clearSelection}
|
onClick={clearSelection}
|
||||||
|
|
@ -366,79 +272,53 @@ const Sidebar = () => {
|
||||||
>
|
>
|
||||||
X
|
X
|
||||||
</button>
|
</button>
|
||||||
<BlockCode
|
<BlockCode text={selection.selectionStr} disableApplyButton={true} />
|
||||||
text={selection.selectionStr}
|
|
||||||
disableApplyButton={true}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
<form
|
|
||||||
ref={formRef}
|
|
||||||
className="flex flex-row items-center rounded-md p-2 input"
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" && !e.shiftKey) onSubmit(e);
|
|
||||||
}}
|
|
||||||
onSubmit={(e) => {
|
|
||||||
console.log("submit!");
|
|
||||||
e.preventDefault();
|
|
||||||
onSubmit(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* input */}
|
|
||||||
<textarea
|
|
||||||
onChange={(e) => {
|
|
||||||
setInstructions(e.target.value);
|
|
||||||
}}
|
|
||||||
className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
|
|
||||||
style={{ outline: "0px solid" }}
|
|
||||||
placeholder="Ctrl+L to select"
|
|
||||||
rows={1}
|
|
||||||
onInput={(e) => {
|
|
||||||
e.currentTarget.style.height = "auto";
|
|
||||||
e.currentTarget.style.height =
|
|
||||||
e.currentTarget.scrollHeight + "px";
|
|
||||||
}} // Adjust height dynamically
|
|
||||||
/>
|
|
||||||
{/* submit button */}
|
|
||||||
{isLoading ? (
|
|
||||||
<button
|
|
||||||
onClick={onStop}
|
|
||||||
className="btn btn-primary rounded-r-lg max-h-10 p-2"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Stop
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
|
|
||||||
disabled={!instructions}
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<line x1="12" y1="19" x2="12" y2="5"></line>
|
|
||||||
<polyline points="5 12 12 5 19 12"></polyline>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
{isDisabled && (
|
<form
|
||||||
<div className="absolute top-0 left-0 w-full h-full bg-gray-500 opacity-10 pointer-events-none" />
|
ref={formRef}
|
||||||
)}
|
className="flex flex-row items-center rounded-md p-2 input"
|
||||||
</div>
|
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Sidebar;
|
onSubmit={(e) => {
|
||||||
|
console.log('submit!')
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit(e)
|
||||||
|
}}>
|
||||||
|
{/* input */}
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
onChange={(e) => { setInstructions(e.target.value) }}
|
||||||
|
className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
|
||||||
|
placeholder="Ctrl+L to select"
|
||||||
|
rows={1}
|
||||||
|
onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
|
||||||
|
/>
|
||||||
|
{/* submit button */}
|
||||||
|
{isLoading ?
|
||||||
|
<button
|
||||||
|
onClick={onStop}
|
||||||
|
className="btn btn-primary rounded-r-lg max-h-10 p-2"
|
||||||
|
type='button'
|
||||||
|
>Stop</button>
|
||||||
|
: <button
|
||||||
|
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
|
||||||
|
disabled={!instructions}
|
||||||
|
type='submit'
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<line x1="12" y1="19" x2="12" y2="5"></line>
|
||||||
|
<polyline points="5 12 12 5 19 12"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sidebar
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ const awaiting: { [c in Command]: ((res: any) => void)[] } = {
|
||||||
"files": [],
|
"files": [],
|
||||||
"apiConfig": [],
|
"apiConfig": [],
|
||||||
"getApiConfig": [],
|
"getApiConfig": [],
|
||||||
"displayError": []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// use this function to await responses
|
// use this function to await responses
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,3 @@ html {
|
||||||
.input {
|
.input {
|
||||||
@apply bg-vscode-input-bg text-vscode-input-fg border-vscode-input-border;
|
@apply bg-vscode-input-bg text-vscode-input-fg border-vscode-input-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-select {
|
|
||||||
user-select: none;
|
|
||||||
pointer-events: none;
|
|
||||||
filter: blur(3px)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -23,5 +23,4 @@ module.exports = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue