Performance optimizations with memoization and useCallbacks

This commit is contained in:
Saoud Rizwan
2024-09-06 14:35:02 -04:00
parent 76868f21d1
commit 428d3c39b5
12 changed files with 578 additions and 562 deletions

View File

@@ -215,7 +215,7 @@ const ChatView = ({
}
}, [messages.length])
const handleSendMessage = () => {
const handleSendMessage = useCallback(() => {
const text = inputValue.trim()
if (text || selectedImages.length > 0) {
if (messages.length === 0) {
@@ -248,26 +248,33 @@ const ChatView = ({
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
}
}
}, [inputValue, selectedImages, messages.length, claudeAsk])
const handleSendStdin = (text: string) => {
if (claudeAsk === "command_output") {
vscode.postMessage({
type: "askResponse",
askResponse: "messageResponse",
text: COMMAND_STDIN_STRING + text,
})
setClaudeAsk(undefined)
// don't need to disable since extension relinquishes control back immediately
// setTextAreaDisabled(true)
// setEnableButtons(false)
}
}
const handleSendStdin = useCallback(
(text: string) => {
if (claudeAsk === "command_output") {
vscode.postMessage({
type: "askResponse",
askResponse: "messageResponse",
text: COMMAND_STDIN_STRING + text,
})
setClaudeAsk(undefined)
// don't need to disable since extension relinquishes control back immediately
// setTextAreaDisabled(true)
// setEnableButtons(false)
}
},
[claudeAsk]
)
const startNewTask = useCallback(() => {
vscode.postMessage({ type: "clearTask" })
}, [])
/*
This logic depends on the useEffect[messages] above to set claudeAsk, after which buttons are shown and we then send an askResponse to the extension.
*/
const handlePrimaryButtonClick = () => {
const handlePrimaryButtonClick = useCallback(() => {
switch (claudeAsk) {
case "api_req_failed":
case "command":
@@ -288,9 +295,9 @@ const ChatView = ({
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
}
}, [claudeAsk, startNewTask])
const handleSecondaryButtonClick = () => {
const handleSecondaryButtonClick = useCallback(() => {
switch (claudeAsk) {
case "api_req_failed":
case "mistake_limit_reached":
@@ -307,67 +314,72 @@ const ChatView = ({
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
}
}, [claudeAsk, startNewTask])
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
const isComposing = event.nativeEvent?.isComposing ?? false
if (event.key === "Enter" && !event.shiftKey && !isComposing) {
event.preventDefault()
handleSendMessage()
}
}
const handleTaskCloseButtonClick = () => {
startNewTask()
}
const startNewTask = () => {
vscode.postMessage({ type: "clearTask" })
}
const selectImages = () => {
vscode.postMessage({ type: "selectImages" })
}
const handlePaste = async (e: React.ClipboardEvent) => {
const items = e.clipboardData.items
const acceptedTypes = ["png", "jpeg", "webp"] // supported by anthropic and openrouter (jpg is just a file extension but the image will be recognized as jpeg)
const imageItems = Array.from(items).filter((item) => {
const [type, subtype] = item.type.split("/")
return type === "image" && acceptedTypes.includes(subtype)
})
if (!shouldDisableImages && imageItems.length > 0) {
e.preventDefault()
const imagePromises = imageItems.map((item) => {
return new Promise<string | null>((resolve) => {
const blob = item.getAsFile()
if (!blob) {
resolve(null)
return
}
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(blob)
})
})
const imageDataArray = await Promise.all(imagePromises)
const dataUrls = imageDataArray.filter((dataUrl): dataUrl is string => dataUrl !== null)
//.map((dataUrl) => dataUrl.split(",")[1]) // strip the mime type prefix, sharp doesn't need it
if (dataUrls.length > 0) {
setSelectedImages((prevImages) => [...prevImages, ...dataUrls].slice(0, MAX_IMAGES_PER_MESSAGE))
} else {
console.warn("No valid images were processed")
const handleKeyDown = useCallback(
(event: KeyboardEvent<HTMLTextAreaElement>) => {
const isComposing = event.nativeEvent?.isComposing ?? false
if (event.key === "Enter" && !event.shiftKey && !isComposing) {
event.preventDefault()
handleSendMessage()
}
}
}
},
[handleSendMessage]
)
const handleTaskCloseButtonClick = useCallback(() => {
startNewTask()
}, [startNewTask])
const selectImages = useCallback(() => {
vscode.postMessage({ type: "selectImages" })
}, [])
const shouldDisableImages =
!selectedModelSupportsImages || textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
const handlePaste = useCallback(
async (e: React.ClipboardEvent) => {
const items = e.clipboardData.items
const acceptedTypes = ["png", "jpeg", "webp"] // supported by anthropic and openrouter (jpg is just a file extension but the image will be recognized as jpeg)
const imageItems = Array.from(items).filter((item) => {
const [type, subtype] = item.type.split("/")
return type === "image" && acceptedTypes.includes(subtype)
})
if (!shouldDisableImages && imageItems.length > 0) {
e.preventDefault()
const imagePromises = imageItems.map((item) => {
return new Promise<string | null>((resolve) => {
const blob = item.getAsFile()
if (!blob) {
resolve(null)
return
}
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(blob)
})
})
const imageDataArray = await Promise.all(imagePromises)
const dataUrls = imageDataArray.filter((dataUrl): dataUrl is string => dataUrl !== null)
//.map((dataUrl) => dataUrl.split(",")[1]) // strip the mime type prefix, sharp doesn't need it
if (dataUrls.length > 0) {
setSelectedImages((prevImages) => [...prevImages, ...dataUrls].slice(0, MAX_IMAGES_PER_MESSAGE))
} else {
console.warn("No valid images were processed")
}
}
},
[shouldDisableImages, setSelectedImages]
)
useEffect(() => {
if (selectedImages.length === 0) {
@@ -469,8 +481,21 @@ const ChatView = ({
return text
}, [task])
const shouldDisableImages =
!selectedModelSupportsImages || textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
const itemContent = useCallback(
(index: number, message: any) => (
<ChatRow
key={message.ts}
message={message}
syntaxHighlighterStyle={syntaxHighlighterStyle}
isExpanded={expandedRows[message.ts] || false}
onToggleExpand={() => toggleRowExpansion(message.ts)}
lastModifiedMessage={modifiedMessages.at(-1)}
isLast={index === visibleMessages.length - 1}
handleSendStdin={handleSendStdin}
/>
),
[expandedRows, syntaxHighlighterStyle, modifiedMessages, visibleMessages.length, handleSendStdin]
)
return (
<div
@@ -540,18 +565,7 @@ const ChatView = ({
// }}
increaseViewportBy={{ top: 0, bottom: Number.MAX_SAFE_INTEGER }} // hack to make sure the last message is always rendered to get truly perfect scroll to bottom animation when new messages are added (Number.MAX_SAFE_INTEGER is safe for arithmetic operations, which is all virtuoso uses this value for in src/sizeRangeSystem.ts)
data={visibleMessages} // messages is the raw format returned by extension, modifiedMessages is the manipulated structure that combines certain messages of related type, and visibleMessages is the filtered structure that removes messages that should not be rendered
itemContent={(index, message) => (
<ChatRow
key={message.ts}
message={message}
syntaxHighlighterStyle={syntaxHighlighterStyle}
isExpanded={expandedRows[message.ts] || false}
onToggleExpand={() => toggleRowExpansion(message.ts)}
lastModifiedMessage={modifiedMessages.at(-1)}
isLast={index === visibleMessages.length - 1}
handleSendStdin={handleSendStdin}
/>
)}
itemContent={itemContent}
/>
<div
style={{