Focus textarea when webview becomes visible; add getApiMetrics

This commit is contained in:
Saoud Rizwan
2024-07-09 12:35:46 -04:00
parent 037c6eb226
commit 771c612d8a
5 changed files with 179 additions and 7 deletions

View File

@@ -41,6 +41,17 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
// and executes code based on the message that is recieved
this.setWebviewMessageListener(webviewView.webview)
// Listen for when the panel becomes visible
// https://github.com/microsoft/vscode-discussions/discussions/840
webviewView.onDidChangeVisibility((e: any) => {
if (e.visible) {
// Your view is visible
this.postMessageToWebview({ type: "action", action: "didBecomeVisible"})
} else {
// Your view is hidden
}
})
// if the extension is starting a new session, clear previous task state
this.resetTask()
}

View File

@@ -4,7 +4,7 @@
export interface ExtensionMessage {
type: "action" | "state"
text?: string
action?: "plusButtonTapped" | "settingsButtonTapped"
action?: "plusButtonTapped" | "settingsButtonTapped" | "didBecomeVisible"
state?: { didOpenOnce: boolean, apiKey?: string, maxRequestsPerTask?: number, claudeMessages: ClaudeMessage[] }
}

View File

@@ -24,7 +24,8 @@ const App: React.FC = () => {
useEffect(() => {
vscode.postMessage({ type: "webviewDidLaunch" })
window.addEventListener("message", (e: MessageEvent) => {
const handleMessage = (e: MessageEvent) => {
const message: ExtensionMessage = e.data
// switch message.type
switch (message.type) {
@@ -50,7 +51,13 @@ const App: React.FC = () => {
}
break
}
})
}
window.addEventListener("message", handleMessage)
return () => {
window.removeEventListener("message", handleMessage)
}
}, [])
// dummy data for messages
@@ -136,6 +143,78 @@ const App: React.FC = () => {
ts: generateRandomTimestamp(baseDate, 29),
},
{ type: "say", say: "text", text: "Final message", ts: generateRandomTimestamp(baseDate, 30) },
{
type: "say",
say: "text",
text: "Starting API requests",
ts: Date.now(),
},
{
type: "say",
say: "api_req_started",
text: JSON.stringify({
request: "GET /api/data1",
}),
ts: Date.now() + 1000,
},
{
type: "say",
say: "api_req_finished",
text: JSON.stringify({
tokensIn: 10,
tokensOut: 20,
cost: 0.002,
}),
ts: Date.now() + 2000,
},
{
type: "say",
say: "text",
text: "Processing data...",
ts: Date.now() + 3000,
},
{
type: "say",
say: "api_req_started",
text: JSON.stringify({
request: "POST /api/update1",
}),
ts: Date.now() + 4000,
},
{
type: "say",
say: "api_req_finished",
text: JSON.stringify({
tokensIn: 15,
tokensOut: 25,
cost: 0.003,
}),
ts: Date.now() + 5000,
},
{
type: "say",
say: "text",
text: "More processing...",
ts: Date.now() + 6000,
},
{
type: "say",
say: "api_req_started",
text: JSON.stringify({
request: "GET /api/data2",
}),
ts: Date.now() + 7000,
},
{
type: "say",
say: "api_req_finished",
text: JSON.stringify({
tokensIn: 5,
tokensOut: 15,
cost: 0.001,
}),
ts: Date.now() + 8000,
},
]
return (

View File

@@ -8,6 +8,7 @@ import ChatRow from "./ChatRow"
import { combineCommandSequences } from "../utilities/combineCommandSequences"
import { combineApiRequests } from "../utilities/combineApiRequests"
import TaskHeader from "./TaskHeader"
import { getApiMetrics } from "../utilities/getApiMetrics"
interface ChatViewProps {
messages: ClaudeMessage[]
@@ -16,6 +17,8 @@ interface ChatViewProps {
const ChatView = ({ messages }: ChatViewProps) => {
const task = messages.shift()
const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages)), [messages])
// has to be after api_req_finished are all reduced into api_req_started messages
const apiMetrics = useMemo(() => getApiMetrics(modifiedMessages), [modifiedMessages])
const [inputValue, setInputValue] = useState("")
const messagesEndRef = useRef<HTMLDivElement>(null)
@@ -110,7 +113,30 @@ const ChatView = ({ messages }: ChatViewProps) => {
useEffect(() => {
if (textAreaRef.current && !textAreaHeight) {
setTextAreaHeight(textAreaRef.current.offsetHeight)
textAreaRef.current.focus()
//textAreaRef.current.focus()
}
const handleMessage = (e: MessageEvent) => {
const message: ExtensionMessage = e.data
switch (message.type) {
case "action":
switch (message.action!) {
case "didBecomeVisible":
textAreaRef.current?.focus()
break
}
break
}
}
window.addEventListener("message", handleMessage)
const timer = setTimeout(() => {
textAreaRef.current?.focus()
}, 20)
return () => {
clearTimeout(timer)
window.removeEventListener("message", handleMessage)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@@ -129,9 +155,9 @@ const ChatView = ({ messages }: ChatViewProps) => {
}}>
<TaskHeader
taskText={task?.text || ""}
tokensIn={1000}
tokensOut={1500}
totalCost={0.0025}
tokensIn={apiMetrics.totalTokensIn}
tokensOut={apiMetrics.totalTokensOut}
totalCost={apiMetrics.totalCost}
/>
<div
className="scrollable"
@@ -177,6 +203,7 @@ const ChatView = ({ messages }: ChatViewProps) => {
onHeightChange={() => scrollToBottom(true)}
placeholder="Type a message..."
maxRows={10}
autoFocus={true}
style={{
width: "100%",
boxSizing: "border-box",

View File

@@ -0,0 +1,55 @@
import { ClaudeMessage } from "@shared/ExtensionMessage"
interface ApiMetrics {
totalTokensIn: number
totalTokensOut: number
totalCost: number
}
/**
* Calculates API metrics from an array of ClaudeMessages.
*
* This function processes 'api_req_started' messages that have been combined with their
* corresponding 'api_req_finished' messages by the combineApiRequests function.
* It extracts and sums up the tokensIn, tokensOut, and cost from these messages.
*
* @param messages - An array of ClaudeMessage objects to process.
* @returns An ApiMetrics object containing totalTokensIn, totalTokensOut, and totalCost.
*
* @example
* const messages = [
* { type: "say", say: "api_req_started", text: '{"request":"GET /api/data","tokensIn":10,"tokensOut":20,"cost":0.005}', ts: 1000 }
* ];
* const { totalTokensIn, totalTokensOut, totalCost } = getApiMetrics(messages);
* // Result: { totalTokensIn: 10, totalTokensOut: 20, totalCost: 0.005 }
*/
export function getApiMetrics(messages: ClaudeMessage[]): ApiMetrics {
const result: ApiMetrics = {
totalTokensIn: 0,
totalTokensOut: 0,
totalCost: 0,
}
messages.forEach((message) => {
if (message.type === "say" && message.say === "api_req_started" && message.text) {
try {
const parsedData = JSON.parse(message.text)
const { tokensIn, tokensOut, cost } = parsedData
if (typeof tokensIn === "number") {
result.totalTokensIn += tokensIn
}
if (typeof tokensOut === "number") {
result.totalTokensOut += tokensOut
}
if (typeof cost === "number") {
result.totalCost += cost
}
} catch (error) {
console.error("Error parsing JSON:", error)
}
}
})
return result
}