Browse Source

Add pagination and screenshots to browser

Saoud Rizwan 1 year ago
parent
commit
b05cc0d059
1 changed files with 183 additions and 66 deletions
  1. 183 66
      webview-ui/src/components/chat/BrowserSessionRow.tsx

+ 183 - 66
webview-ui/src/components/chat/BrowserSessionRow.tsx

@@ -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
-						style={{
-							position: "absolute",
-							top: 0,
-							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"
+					{displayState.screenshot ? (
+						<img
+							src={displayState.screenshot}
+							alt="Browser screenshot"
 							style={{
-								fontSize: "80px",
-								color: "var(--vscode-input-background)",
-							}}></span>
-					</div>
-					<BrowserCursor style={{ position: "absolute", bottom: "10%", right: "20%" }} />
-				</div>
-				<div style={{ width: "100%" }}>
-					<div
-						onClick={() => {
-							if (consoleLogs) {
-								setConsoleLogsExpanded(!consoleLogsExpanded)
+								position: "absolute",
+								top: 0,
+								left: 0,
+								width: "100%",
+								height: "100%",
+								objectFit: "contain",
+							}}
+							onClick={() =>
+								vscode.postMessage({
+									type: "openImage",
+									text: displayState.screenshot,
+								})
 							}
-						}}
-						style={{
-							display: "flex",
-							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`,
-						}}>
-						<span className={`codicon codicon-chevron-${consoleLogsExpanded ? "down" : "right"}`}></span>
-						<span style={{ fontSize: "0.8em" }}>Console Logs</span>
+						/>
+					) : (
+						<div
+							style={{
+								position: "absolute",
+								top: "50%",
+								left: "50%",
+								transform: "translate(-50%, -50%)",
+							}}>
+							<span
+								className="codicon codicon-globe"
+								style={{ fontSize: "80px", color: "var(--vscode-descriptionForeground)" }}
+							/>
+						</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>
-					{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) {