Enhance prompt button for openrouter

This commit is contained in:
Matt Rubens
2024-12-23 23:24:49 -08:00
parent 1581ed135b
commit 111abdbb2c
13 changed files with 703 additions and 102 deletions

View File

@@ -13,7 +13,7 @@ import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
import ContextMenu from "./ContextMenu"
import Thumbnails from "../common/Thumbnails"
declare const vscode: any;
import { vscode } from "../../utils/vscode"
interface ChatTextAreaProps {
inputValue: string
@@ -44,8 +44,20 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
},
ref,
) => {
const { filePaths } = useExtensionState()
const { filePaths, apiConfiguration } = useExtensionState()
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
// Handle enhanced prompt response
useEffect(() => {
const messageHandler = (event: MessageEvent) => {
const message = event.data
if (message.type === 'enhancedPrompt' && message.text) {
setInputValue(message.text)
}
}
window.addEventListener('message', messageHandler)
return () => window.removeEventListener('message', messageHandler)
}, [setInputValue])
const [thumbnailsHeight, setThumbnailsHeight] = useState(0)
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<number | undefined>(undefined)
const [showContextMenu, setShowContextMenu] = useState(false)
@@ -60,6 +72,63 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const [intendedCursorPosition, setIntendedCursorPosition] = useState<number | null>(null)
const contextMenuContainerRef = useRef<HTMLDivElement>(null)
const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false)
const handleEnhancePrompt = useCallback(() => {
if (!textAreaDisabled) {
const trimmedInput = inputValue.trim()
if (trimmedInput) {
setIsEnhancingPrompt(true)
const message = {
type: "enhancePrompt" as const,
text: trimmedInput,
}
vscode.postMessage(message)
} else {
const promptDescription = "The 'Enhance Prompt' button helps improve your prompt by providing additional context, clarification, or rephrasing. Try typing a prompt in here and clicking the button again to see how it works."
setInputValue(promptDescription)
}
}
}, [inputValue, textAreaDisabled, setInputValue])
useEffect(() => {
const messageHandler = (event: MessageEvent) => {
const message = event.data
if (message.type === 'enhancedPrompt') {
setInputValue(message.text)
setIsEnhancingPrompt(false)
}
}
window.addEventListener('message', messageHandler)
return () => window.removeEventListener('message', messageHandler)
}, [setInputValue])
// Handle enhanced prompt response
useEffect(() => {
const messageHandler = (event: MessageEvent) => {
const message = event.data
if (message.type === 'enhancedPrompt') {
setInputValue(message.text)
}
}
window.addEventListener('message', messageHandler)
return () => {
window.removeEventListener('message', messageHandler)
}
}, [setInputValue])
// Handle enhanced prompt response
useEffect(() => {
const messageHandler = (event: MessageEvent) => {
const message = event.data
if (message.type === 'enhancedPrompt' && message.text) {
setInputValue(message.text)
}
}
window.addEventListener('message', messageHandler)
return () => window.removeEventListener('message', messageHandler)
}, [setInputValue])
const queryItems = useMemo(() => {
return [
{ type: ContextMenuOptionType.Problems, value: "problems" },
@@ -423,68 +492,64 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
)
return (
<div
style={{
padding: "10px 15px",
opacity: textAreaDisabled ? 0.5 : 1,
position: "relative",
display: "flex",
}}
onDrop={async (e) => {
console.log("onDrop called")
e.preventDefault()
const files = Array.from(e.dataTransfer.files)
const text = e.dataTransfer.getData("text")
if (text) {
const newValue =
inputValue.slice(0, cursorPosition) + text + inputValue.slice(cursorPosition)
setInputValue(newValue)
const newCursorPosition = cursorPosition + text.length
setCursorPosition(newCursorPosition)
setIntendedCursorPosition(newCursorPosition)
return
}
const acceptedTypes = ["png", "jpeg", "webp"]
const imageFiles = files.filter((file) => {
const [type, subtype] = file.type.split("/")
return type === "image" && acceptedTypes.includes(subtype)
})
if (!shouldDisableImages && imageFiles.length > 0) {
const imagePromises = imageFiles.map((file) => {
return new Promise<string | null>((resolve) => {
const reader = new FileReader()
reader.onloadend = () => {
if (reader.error) {
console.error("Error reading file:", reader.error)
resolve(null)
} else {
const result = reader.result
console.log("File read successfully", result)
resolve(typeof result === "string" ? result : null)
}
<div style={{
padding: "10px 15px",
opacity: textAreaDisabled ? 0.5 : 1,
position: "relative",
display: "flex",
}}
onDrop={async (e) => {
e.preventDefault()
const files = Array.from(e.dataTransfer.files)
const text = e.dataTransfer.getData("text")
if (text) {
const newValue =
inputValue.slice(0, cursorPosition) + text + inputValue.slice(cursorPosition)
setInputValue(newValue)
const newCursorPosition = cursorPosition + text.length
setCursorPosition(newCursorPosition)
setIntendedCursorPosition(newCursorPosition)
return
}
const acceptedTypes = ["png", "jpeg", "webp"]
const imageFiles = files.filter((file) => {
const [type, subtype] = file.type.split("/")
return type === "image" && acceptedTypes.includes(subtype)
})
if (!shouldDisableImages && imageFiles.length > 0) {
const imagePromises = imageFiles.map((file) => {
return new Promise<string | null>((resolve) => {
const reader = new FileReader()
reader.onloadend = () => {
if (reader.error) {
console.error("Error reading file:", reader.error)
resolve(null)
} else {
const result = reader.result
resolve(typeof result === "string" ? result : null)
}
reader.readAsDataURL(file)
})
})
const imageDataArray = await Promise.all(imagePromises)
const dataUrls = imageDataArray.filter((dataUrl): dataUrl is string => dataUrl !== null)
if (dataUrls.length > 0) {
setSelectedImages((prevImages) => [...prevImages, ...dataUrls].slice(0, MAX_IMAGES_PER_MESSAGE))
if (typeof vscode !== 'undefined') {
vscode.postMessage({
type: 'draggedImages',
dataUrls: dataUrls
})
}
} else {
console.warn("No valid images were processed")
reader.readAsDataURL(file)
})
})
const imageDataArray = await Promise.all(imagePromises)
const dataUrls = imageDataArray.filter((dataUrl): dataUrl is string => dataUrl !== null)
if (dataUrls.length > 0) {
setSelectedImages((prevImages) => [...prevImages, ...dataUrls].slice(0, MAX_IMAGES_PER_MESSAGE))
if (typeof vscode !== 'undefined') {
vscode.postMessage({
type: 'draggedImages',
dataUrls: dataUrls
})
}
} else {
console.warn("No valid images were processed")
}
}}
onDragOver={(e) => {
e.preventDefault()
}}
>
}
}}
onDragOver={(e) => {
e.preventDefault()
}}>
{showContextMenu && (
<div ref={contextMenuContainerRef}>
<ContextMenu
@@ -533,7 +598,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
borderTop: 0,
borderColor: "transparent",
borderBottom: `${thumbnailsHeight + 6}px solid transparent`,
padding: "9px 49px 3px 9px",
padding: "9px 9px 25px 9px",
}}
/>
<DynamicTextArea
@@ -588,11 +653,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
borderTop: 0,
borderBottom: `${thumbnailsHeight + 6}px solid transparent`,
borderColor: "transparent",
padding: "9px 9px 25px 9px",
// borderRight: "54px solid transparent",
// borderLeft: "9px solid transparent", // NOTE: react-textarea-autosize doesn't calculate correct height when using borderLeft/borderRight so we need to use horizontal padding instead
// Instead of using boxShadow, we use a div with a border to better replicate the behavior when the textarea is focused
// boxShadow: "0px 0px 0px 1px var(--vscode-input-border)",
padding: "9px 49px 3px 9px",
cursor: textAreaDisabled ? "not-allowed" : undefined,
flex: 1,
zIndex: 1,
@@ -609,45 +674,29 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
paddingTop: 4,
bottom: 14,
left: 22,
right: 67, // (54 + 9) + 4 extra padding
right: 67,
zIndex: 2,
}}
/>
)}
<div
style={{
position: "absolute",
right: 28,
display: "flex",
alignItems: "flex-end",
height: textAreaBaseHeight || 31,
bottom: 18,
zIndex: 2,
}}>
<div style={{ display: "flex", flexDirection: "row", alignItems: "center" }}>
<div
className={`input-icon-button ${
shouldDisableImages ? "disabled" : ""
} codicon codicon-device-camera`}
onClick={() => {
if (!shouldDisableImages) {
onSelectImages()
}
}}
style={{
marginRight: 5.5,
fontSize: 16.5,
}}
/>
<div
className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-send`}
onClick={() => {
if (!textAreaDisabled) {
onSend()
}
}}
style={{ fontSize: 15 }}></div>
</div>
<div className="button-row" style={{ position: "absolute", right: 20, display: "flex", alignItems: "center", height: 31, bottom: 8, zIndex: 2, justifyContent: "flex-end" }}>
<span style={{ display: "flex", alignItems: "center", gap: 12 }}>
{apiConfiguration?.apiProvider === "openrouter" && (
<div style={{ display: "flex", alignItems: "center" }}>
{isEnhancingPrompt && <span style={{ marginRight: 10, color: "var(--vscode-input-foreground)", opacity: 0.5 }}>Enhancing prompt...</span>}
<span
role="button"
aria-label="enhance prompt"
data-testid="enhance-prompt-button"
className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-sparkle`}
onClick={() => !textAreaDisabled && handleEnhancePrompt()}
style={{ fontSize: 16.5 }}
/>
</div>
)}
<span className={`input-icon-button ${shouldDisableImages ? "disabled" : ""} codicon codicon-device-camera`} onClick={() => !shouldDisableImages && onSelectImages()} style={{ fontSize: 16.5 }} />
<span className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-send`} onClick={() => !textAreaDisabled && onSend()} style={{ fontSize: 15 }} />
</span>
</div>
</div>
)