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 React, { memo, useEffect, useRef, useState } from "react"
import React, { memo, useEffect, useMemo, useRef, useState } from "react"
import { useSize } from "react-use"
import { BrowserActionResult, ClineMessage, ClineSayBrowserAction } from "../../../../src/shared/ExtensionMessage"
import { vscode } from "../../utils/vscode"
import CodeAccordian from "../common/CodeAccordian"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { ChatRowContent } from "./ChatRow"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
interface BrowserSessionRowProps {
messages: ClineMessage[]
@@ -25,29 +26,134 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
const { messages, isLast, onHeightChange } = props
const prevHeightRef = useRef(0)
const [consoleLogsExpanded, setConsoleLogsExpanded] = useState(false)
const consoleLogs = "console logs\nhere\n..."
// Organize messages into pages with current state and next action
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(
<div style={{ padding: "10px 6px 10px 15px" }}>
<h3>Browser Session Group</h3>
<div
style={{
borderRadius: 3,
border: "1px solid var(--vscode-editorGroup-border)",
overflow: "hidden",
backgroundColor: CODE_BLOCK_BG_COLOR,
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}>
{/* URL Bar */}
<div
style={{
width: "calc(100% - 10px)",
boxSizing: "border-box", // includes padding in width calculation
margin: "5px auto",
width: "calc(100% - 10px)",
backgroundColor: "var(--vscode-input-background)",
border: "1px solid var(--vscode-input-border)",
borderRadius: "4px",
@@ -57,79 +163,90 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
justifyContent: "center",
color: "var(--vscode-input-foreground)",
fontSize: "12px",
wordBreak: "break-all", // Allow breaks anywhere
whiteSpace: "normal", // Allow wrapping
wordBreak: "break-all",
whiteSpace: "normal",
}}>
{"https://example.com/thisisalongurl/asdfasfdasdf?asdfasdfasf"}
{displayState.url}
</div>
{/* Screenshot Area */}
<div
style={{
width: "100%",
paddingBottom: "75%", // This creates a 4:3 aspect ratio
paddingBottom: "75%",
position: "relative",
backgroundColor: "var(--vscode-input-background)",
}}>
{/* <div
{displayState.screenshot ? (
<img
src={displayState.screenshot}
alt="Browser screenshot"
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "red",
width: "100%",
height: "100%",
objectFit: "contain",
}}
/> */}
onClick={() =>
vscode.postMessage({
type: "openImage",
text: displayState.screenshot,
})
}
/>
) : (
<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={{
fontSize: "80px",
color: "var(--vscode-input-background)",
}}></span>
style={{ fontSize: "80px", color: "var(--vscode-descriptionForeground)" }}
/>
</div>
<BrowserCursor style={{ position: "absolute", bottom: "10%", right: "20%" }} />
)}
</div>
<div style={{ width: "100%" }}>
{/* Pagination */}
<div
onClick={() => {
if (consoleLogs) {
setConsoleLogsExpanded(!consoleLogsExpanded)
}
}}
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "4px",
width: "100%",
justifyContent: "flex-start",
cursor: consoleLogs ? "pointer" : "default",
opacity: consoleLogs ? 1 : 0.5,
padding: `8px 8px ${consoleLogsExpanded ? 0 : 8}px 8px`,
padding: "8px",
borderTop: "1px solid var(--vscode-editorGroup-border)",
}}>
<span className={`codicon codicon-chevron-${consoleLogsExpanded ? "down" : "right"}`}></span>
<span style={{ fontSize: "0.8em" }}>Console Logs</span>
<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>
{consoleLogsExpanded && <CodeBlock source={`${"```"}shell\n${consoleLogs}\n${"```"}`} />}
</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} />
))}
<h3>END Browser Session Group</h3>
</div>
)
// Height change effect
useEffect(() => {
const isInitialRender = prevHeightRef.current === 0
if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) {