Add pagination and screenshots to browser

This commit is contained in:
Saoud Rizwan
2024-10-27 08:27:34 -04:00
parent 04b483a67f
commit b05cc0d059

View File

@@ -1,11 +1,12 @@
import deepEqual from "fast-deep-equal" import deepEqual from "fast-deep-equal"
import React, { memo, useEffect, useRef, useState } from "react" import React, { memo, useEffect, useMemo, useRef, useState } from "react"
import { useSize } from "react-use" import { useSize } from "react-use"
import { BrowserActionResult, ClineMessage, ClineSayBrowserAction } from "../../../../src/shared/ExtensionMessage" import { BrowserActionResult, ClineMessage, ClineSayBrowserAction } from "../../../../src/shared/ExtensionMessage"
import { vscode } from "../../utils/vscode" import { vscode } from "../../utils/vscode"
import CodeAccordian from "../common/CodeAccordian" import CodeAccordian from "../common/CodeAccordian"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock" import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { ChatRowContent } from "./ChatRow" import { ChatRowContent } from "./ChatRow"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
interface BrowserSessionRowProps { interface BrowserSessionRowProps {
messages: ClineMessage[] messages: ClineMessage[]
@@ -25,29 +26,134 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
const { messages, isLast, onHeightChange } = props const { messages, isLast, onHeightChange } = props
const prevHeightRef = useRef(0) const prevHeightRef = useRef(0)
const [consoleLogsExpanded, setConsoleLogsExpanded] = useState(false) // Organize messages into pages with current state and next action
const consoleLogs = "console logs\nhere\n..." const pages = useMemo(() => {
const result: {
currentState: {
url?: string
screenshot?: string
messages: ClineMessage[] // messages up to and including the result
}
nextAction?: {
messages: ClineMessage[] // messages leading to next result
}
}[] = []
let currentStateMessages: ClineMessage[] = []
let nextActionMessages: ClineMessage[] = []
messages.forEach((message) => {
if (message.ask === "browser_action_launch") {
// Start first page
currentStateMessages = [message]
} else if (message.say === "browser_action_result") {
// Complete current state
currentStateMessages.push(message)
const resultData = JSON.parse(message.text || "{}") as BrowserActionResult
// Add page with current state and previous next actions
result.push({
currentState: {
url: resultData.currentUrl,
screenshot: resultData.screenshot,
messages: [...currentStateMessages],
},
nextAction:
nextActionMessages.length > 0
? {
messages: [...nextActionMessages],
}
: undefined,
})
// Reset for next page
currentStateMessages = []
nextActionMessages = []
} else if (
message.say === "api_req_started" ||
message.say === "text" ||
message.say === "browser_action"
) {
// These messages lead to the next result, so they should always go in nextActionMessages
nextActionMessages.push(message)
} else {
// Any other message types
currentStateMessages.push(message)
}
})
// Add incomplete page if exists
if (currentStateMessages.length > 0 || nextActionMessages.length > 0) {
result.push({
currentState: {
messages: [...currentStateMessages],
},
nextAction:
nextActionMessages.length > 0
? {
messages: [...nextActionMessages],
}
: undefined,
})
}
return result
}, [messages])
// Auto-advance to latest page
const [currentPageIndex, setCurrentPageIndex] = useState(0)
useEffect(() => {
setCurrentPageIndex(pages.length - 1)
}, [pages.length])
// Get initial URL from launch message
const initialUrl = useMemo(() => {
const launchMessage = messages.find((m) => m.ask === "browser_action_launch")
return launchMessage?.text || ""
}, [messages])
// Find the latest available URL and screenshot
const latestState = useMemo(() => {
for (let i = pages.length - 1; i >= 0; i--) {
const page = pages[i]
if (page.currentState.url || page.currentState.screenshot) {
return {
url: page.currentState.url,
screenshot: page.currentState.screenshot,
}
}
}
return { url: undefined, screenshot: undefined }
}, [pages])
const currentPage = pages[currentPageIndex]
const isLastPage = currentPageIndex === pages.length - 1
// Use latest state if we're on the last page and don't have a state yet
const displayState = isLastPage
? {
url: currentPage?.currentState.url || latestState.url || initialUrl,
screenshot: currentPage?.currentState.screenshot || latestState.screenshot,
}
: {
url: currentPage?.currentState.url || initialUrl,
screenshot: currentPage?.currentState.screenshot,
}
const [browserSessionRow, { height }] = useSize( const [browserSessionRow, { height }] = useSize(
<div style={{ padding: "10px 6px 10px 15px" }}> <div style={{ padding: "10px 6px 10px 15px" }}>
<h3>Browser Session Group</h3>
<div <div
style={{ style={{
borderRadius: 3, borderRadius: 3,
border: "1px solid var(--vscode-editorGroup-border)", border: "1px solid var(--vscode-editorGroup-border)",
overflow: "hidden", overflow: "hidden",
backgroundColor: CODE_BLOCK_BG_COLOR, backgroundColor: CODE_BLOCK_BG_COLOR,
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}> }}>
{/* URL Bar */}
<div <div
style={{ style={{
width: "calc(100% - 10px)",
boxSizing: "border-box", // includes padding in width calculation
margin: "5px auto", margin: "5px auto",
width: "calc(100% - 10px)",
backgroundColor: "var(--vscode-input-background)", backgroundColor: "var(--vscode-input-background)",
border: "1px solid var(--vscode-input-border)", border: "1px solid var(--vscode-input-border)",
borderRadius: "4px", borderRadius: "4px",
@@ -57,79 +163,90 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
justifyContent: "center", justifyContent: "center",
color: "var(--vscode-input-foreground)", color: "var(--vscode-input-foreground)",
fontSize: "12px", fontSize: "12px",
wordBreak: "break-all", // Allow breaks anywhere wordBreak: "break-all",
whiteSpace: "normal", // Allow wrapping whiteSpace: "normal",
}}> }}>
{"https://example.com/thisisalongurl/asdfasfdasdf?asdfasdfasf"} {displayState.url}
</div> </div>
{/* Screenshot Area */}
<div <div
style={{ style={{
width: "100%", width: "100%",
paddingBottom: "75%", // This creates a 4:3 aspect ratio paddingBottom: "75%",
position: "relative", position: "relative",
backgroundColor: "var(--vscode-input-background)",
}}> }}>
{/* <div {displayState.screenshot ? (
style={{ <img
position: "absolute", src={displayState.screenshot}
top: 0, alt="Browser screenshot"
left: 0,
right: 0,
bottom: 0,
backgroundColor: "red",
}}
/> */}
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "100%",
height: "100%",
}}>
<span
className="codicon codicon-globe"
style={{ style={{
fontSize: "80px", position: "absolute",
color: "var(--vscode-input-background)", top: 0,
}}></span> left: 0,
</div> width: "100%",
<BrowserCursor style={{ position: "absolute", bottom: "10%", right: "20%" }} /> height: "100%",
</div> objectFit: "contain",
<div style={{ width: "100%" }}> }}
<div onClick={() =>
onClick={() => { vscode.postMessage({
if (consoleLogs) { type: "openImage",
setConsoleLogsExpanded(!consoleLogsExpanded) text: displayState.screenshot,
})
} }
}} />
style={{ ) : (
display: "flex", <div
alignItems: "center", style={{
gap: "4px", position: "absolute",
width: "100%", top: "50%",
justifyContent: "flex-start", left: "50%",
cursor: consoleLogs ? "pointer" : "default", transform: "translate(-50%, -50%)",
opacity: consoleLogs ? 1 : 0.5, }}>
padding: `8px 8px ${consoleLogsExpanded ? 0 : 8}px 8px`, <span
}}> className="codicon codicon-globe"
<span className={`codicon codicon-chevron-${consoleLogsExpanded ? "down" : "right"}`}></span> style={{ fontSize: "80px", color: "var(--vscode-descriptionForeground)" }}
<span style={{ fontSize: "0.8em" }}>Console Logs</span> />
</div>
)}
</div>
{/* Pagination */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "8px",
borderTop: "1px solid var(--vscode-editorGroup-border)",
}}>
<div>
Step {currentPageIndex + 1} of {pages.length}
</div>
<div style={{ display: "flex", gap: "4px" }}>
<VSCodeButton
disabled={currentPageIndex === 0}
onClick={() => setCurrentPageIndex((i) => i - 1)}>
Previous
</VSCodeButton>
<VSCodeButton
disabled={currentPageIndex === pages.length - 1}
onClick={() => setCurrentPageIndex((i) => i + 1)}>
Next
</VSCodeButton>
</div> </div>
{consoleLogsExpanded && <CodeBlock source={`${"```"}shell\n${consoleLogs}\n${"```"}`} />}
</div> </div>
</div> </div>
{messages.map((message, index) => ( {/* Show next action messages if they exist */}
{currentPage?.nextAction?.messages.map((message) => (
<BrowserSessionRowContent key={message.ts} {...props} message={message} /> <BrowserSessionRowContent key={message.ts} {...props} message={message} />
))} ))}
<h3>END Browser Session Group</h3>
</div> </div>
) )
// Height change effect
useEffect(() => { useEffect(() => {
const isInitialRender = prevHeightRef.current === 0 const isInitialRender = prevHeightRef.current === 0
if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) { if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) {