mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
Extension disabled when no keys set
This commit is contained in:
parent
5e5fe80901
commit
2be5a2d13a
4 changed files with 373 additions and 223 deletions
|
|
@ -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: () => { }}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 && <div className='text-xs my-2'>
|
||||
Include files:
|
||||
{files.map((filename, i) =>
|
||||
<div key={i} className='flex'>
|
||||
{/* X button on a file */}
|
||||
<button type='button' onClick={() => {
|
||||
let file_index = files.indexOf(filename)
|
||||
setFiles([...files.slice(0, file_index), ...files.slice(file_index + 1, Infinity)])
|
||||
}}>
|
||||
-{' '}<span className='text-gray-500'>{getBasename(filename.fsPath)}</span>
|
||||
</button>
|
||||
const FilesSelector = ({
|
||||
files,
|
||||
setFiles,
|
||||
}: {
|
||||
files: vscode.Uri[];
|
||||
setFiles: (files: vscode.Uri[]) => void;
|
||||
}) => {
|
||||
return (
|
||||
files.length !== 0 && (
|
||||
<div className="text-xs my-2">
|
||||
Include files:
|
||||
{files.map((filename, i) => (
|
||||
<div key={i} className="flex">
|
||||
{/* X button on a file */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
let file_index = files.indexOf(filename);
|
||||
setFiles(files.filter((_, index) => index !== file_index));
|
||||
}}
|
||||
>
|
||||
-{" "}
|
||||
<span className="text-gray-500">
|
||||
{getBasename(filename.fsPath)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const IncludedFiles = ({ files }: { files: vscode.Uri[] }) => {
|
||||
return files.length !== 0 && <div className='text-xs my-2'>
|
||||
{files.map((filename, i) =>
|
||||
<div key={i} className='flex'>
|
||||
<button type='button'
|
||||
className='pointer-events-none'
|
||||
onClick={() => {
|
||||
// TODO redirect to the document filename.fsPath, when add this remove pointer-events-none
|
||||
}}>
|
||||
-{' '}<span className='text-gray-100'>{getBasename(filename.fsPath)}</span>
|
||||
</button>
|
||||
return (
|
||||
files.length !== 0 && (
|
||||
<div className="text-xs my-2">
|
||||
{files.map((filename, i) => (
|
||||
<div key={i} className="flex">
|
||||
<button
|
||||
type="button"
|
||||
className="pointer-events-none"
|
||||
onClick={() => {
|
||||
// TODO redirect to the document filename.fsPath, when add this remove pointer-events-none
|
||||
}}
|
||||
>
|
||||
-{" "}
|
||||
<span className="text-gray-100">
|
||||
{getBasename(filename.fsPath)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
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 = <>
|
||||
<IncludedFiles files={chatMessage.files} />
|
||||
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} disableApplyButton={true} />}
|
||||
{children}
|
||||
</>
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
if (role === "user") {
|
||||
chatbubbleContents = (
|
||||
<>
|
||||
<IncludedFiles files={chatMessage.files} />
|
||||
{chatMessage.selection?.selectionStr && (
|
||||
<BlockCode
|
||||
text={chatMessage.selection.selectionStr}
|
||||
disableApplyButton={true}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
} else if (role === "assistant") {
|
||||
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={`mb-4 ${role === 'user' ? 'text-right' : 'text-left'}`}>
|
||||
<div className={`inline-block p-2 rounded-lg ${role === 'user' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-black'} max-w-full`}>
|
||||
{chatbubbleContents}
|
||||
return (
|
||||
<div className={`mb-4 ${role === "user" ? "text-right" : "text-left"}`}>
|
||||
<div
|
||||
className={`inline-block p-2 rounded-lg ${role === "user" ? "bg-blue-500 text-white" : "bg-gray-200 text-black"
|
||||
} max-w-full`}
|
||||
>
|
||||
{chatbubbleContents}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
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 = <T,>(initVal: T) => {
|
||||
const stateRef = useRef<T>(initVal)
|
||||
const [_, setS] = useState<T>(initVal)
|
||||
const stateRef = useRef<T>(initVal);
|
||||
const [_, setS] = useState<T>(initVal);
|
||||
const setState = useCallback((newVal: T) => {
|
||||
setS(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 = () => {
|
||||
|
||||
// state of current message
|
||||
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 [instructions, setInstructions] = useState('') // the user's instructions
|
||||
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 [instructions, setInstructions] = useState(""); // the user's instructions
|
||||
|
||||
// state of chat
|
||||
const [chatMessageHistory, setChatHistory] = useState<ChatMessage[]>([])
|
||||
const [messageStream, setMessageStream] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [chatMessageHistory, setChatHistory] = useState<ChatMessage[]>([]);
|
||||
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<ApiConfig | null>(null)
|
||||
const abortFnRef = useRef<(() => void) | 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
|
||||
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<HTMLFormElement | null>(null)
|
||||
const formRef = useRef<HTMLFormElement | null>(null);
|
||||
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
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 <>
|
||||
<div className="flex flex-col h-full w-full">
|
||||
<div className="flex-grow overflow-y-auto overflow-x-hidden p-4">
|
||||
{/* previous messages */}
|
||||
{chatMessageHistory.map((message, i) =>
|
||||
<ChatBubble key={i} chatMessage={message} />
|
||||
)}
|
||||
{/* message stream */}
|
||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream }} />
|
||||
</div>
|
||||
{/* chatbar */}
|
||||
<div className="py-4 border-t">
|
||||
{/* selection */}
|
||||
<div className="text-left">
|
||||
{/* selected files */}
|
||||
<FilesSelector files={files} setFiles={setFiles} />
|
||||
{/* selected code */}
|
||||
{!selection?.selectionStr ? null
|
||||
: (
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`flex flex-col h-full w-full relative ${isDisabled ? 'no-select' : ''}`}
|
||||
|
||||
>
|
||||
<div className="flex-grow overflow-y-auto overflow-x-hidden p-4">
|
||||
{/* previous messages */}
|
||||
{chatMessageHistory.map((message, i) => (
|
||||
<ChatBubble key={i} chatMessage={message} />
|
||||
))}
|
||||
{/* message stream */}
|
||||
<ChatBubble
|
||||
chatMessage={{
|
||||
role: "assistant",
|
||||
content: messageStream,
|
||||
displayContent: messageStream,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* chatbar */}
|
||||
<div className="py-4 border-t">
|
||||
{/* selection */}
|
||||
<div className="text-left">
|
||||
{/* selected files */}
|
||||
<FilesSelector files={files} setFiles={setFiles} />
|
||||
{/* selected code */}
|
||||
{!selection?.selectionStr ? null : (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
|
|
@ -275,54 +392,81 @@ const Sidebar = () => {
|
|||
>
|
||||
X
|
||||
</button>
|
||||
<BlockCode text={selection.selectionStr} disableApplyButton={true} />
|
||||
<BlockCode
|
||||
text={selection.selectionStr}
|
||||
disableApplyButton={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
<form
|
||||
ref={formRef}
|
||||
className="flex flex-row items-center rounded-md p-2 border border-gray-400 bg-[rgb(20,20,20)]"
|
||||
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 text-gray-100 rounded-md bg-[rgb(20,20,20)]"
|
||||
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="bg-gray-400 text-white p-2 rounded-r-lg max-h-10"
|
||||
type="button"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="cursor-pointer hover:bg-gray-700 bg-gray-600 text-white 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>
|
||||
<form
|
||||
ref={formRef}
|
||||
className="flex flex-row items-center rounded-md p-2 border border-gray-400 bg-[rgb(20,20,20)]"
|
||||
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 text-gray-100 rounded-md bg-[rgb(20,20,20)]"
|
||||
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="bg-gray-400 text-white p-2 rounded-r-lg max-h-10"
|
||||
type='button'
|
||||
>Stop</button>
|
||||
: <button
|
||||
className="cursor-pointer hover:bg-gray-700 bg-gray-600 text-white 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>
|
||||
{/* Red overlay when isDisabled is true */}
|
||||
{isDisabled && (
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-gray-500 opacity-10 pointer-events-none" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
export default Sidebar
|
||||
export default Sidebar;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.no-select {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
filter: blur(3px)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue