ScreenshotTool.tsx

  1import React, { useState } from "react";
  2import { LLMContent } from "../types";
  3
  4interface ScreenshotToolProps {
  5  // For tool_use (pending state)
  6  toolInput?: unknown;
  7  isRunning?: boolean;
  8
  9  // For tool_result (completed state)
 10  toolResult?: LLMContent[];
 11  hasError?: boolean;
 12  executionTime?: string;
 13  display?: unknown; // Display data from the tool_result Content
 14}
 15
 16function ScreenshotTool({
 17  toolInput,
 18  isRunning,
 19  toolResult,
 20  hasError,
 21  executionTime,
 22  display,
 23}: ScreenshotToolProps) {
 24  const [isExpanded, setIsExpanded] = useState(true); // Default to expanded
 25
 26  // Extract display info from toolInput
 27  const getPath = (input: unknown): string | undefined => {
 28    if (
 29      typeof input === "object" &&
 30      input !== null &&
 31      "path" in input &&
 32      typeof input.path === "string"
 33    ) {
 34      return input.path;
 35    }
 36    return undefined;
 37  };
 38
 39  const getId = (input: unknown): string | undefined => {
 40    if (
 41      typeof input === "object" &&
 42      input !== null &&
 43      "id" in input &&
 44      typeof input.id === "string"
 45    ) {
 46      return input.id;
 47    }
 48    return undefined;
 49  };
 50
 51  const getSelector = (input: unknown): string | undefined => {
 52    if (
 53      typeof input === "object" &&
 54      input !== null &&
 55      "selector" in input &&
 56      typeof input.selector === "string"
 57    ) {
 58      return input.selector;
 59    }
 60    return undefined;
 61  };
 62
 63  const filename = getPath(toolInput) || getId(toolInput) || getSelector(toolInput) || "screenshot";
 64
 65  // Use display data passed as prop (from tool_result Content.Display)
 66  const displayData = display;
 67
 68  // Construct image URL
 69  let imageUrl: string | undefined = undefined;
 70  if (displayData && typeof displayData === "object" && displayData !== null) {
 71    const url =
 72      "url" in displayData && typeof displayData.url === "string" ? displayData.url : undefined;
 73    const path =
 74      "path" in displayData && typeof displayData.path === "string" ? displayData.path : undefined;
 75    const id =
 76      "id" in displayData && typeof displayData.id === "string" ? displayData.id : undefined;
 77
 78    imageUrl =
 79      url ||
 80      (path
 81        ? `/api/read?path=${encodeURIComponent(path)}`
 82        : id
 83          ? `/api/read?path=${encodeURIComponent(id)}`
 84          : undefined);
 85  }
 86
 87  const isComplete = !isRunning && toolResult !== undefined;
 88
 89  return (
 90    <div
 91      className="screenshot-tool"
 92      data-testid={isComplete ? "tool-call-completed" : "tool-call-running"}
 93    >
 94      <div className="screenshot-tool-header" onClick={() => setIsExpanded(!isExpanded)}>
 95        <div className="screenshot-tool-summary">
 96          <span className={`screenshot-tool-emoji ${isRunning ? "running" : ""}`}>📷</span>
 97          <span className="screenshot-tool-filename">{filename}</span>
 98          {isComplete && hasError && <span className="screenshot-tool-error"></span>}
 99          {isComplete && !hasError && <span className="screenshot-tool-success"></span>}
100        </div>
101        <button
102          className="screenshot-tool-toggle"
103          aria-label={isExpanded ? "Collapse" : "Expand"}
104          aria-expanded={isExpanded}
105        >
106          <svg
107            width="12"
108            height="12"
109            viewBox="0 0 12 12"
110            fill="none"
111            xmlns="http://www.w3.org/2000/svg"
112            style={{
113              transform: isExpanded ? "rotate(90deg)" : "rotate(0deg)",
114              transition: "transform 0.2s",
115            }}
116          >
117            <path
118              d="M4.5 3L7.5 6L4.5 9"
119              stroke="currentColor"
120              strokeWidth="1.5"
121              strokeLinecap="round"
122              strokeLinejoin="round"
123            />
124          </svg>
125        </button>
126      </div>
127
128      {isExpanded && (
129        <div className="screenshot-tool-details">
130          {isComplete && !hasError && imageUrl && (
131            <div className="screenshot-tool-section">
132              {executionTime && (
133                <div className="screenshot-tool-label">
134                  <span>Screenshot:</span>
135                  <span className="screenshot-tool-time">{executionTime}</span>
136                </div>
137              )}
138              <div className="screenshot-tool-image-container">
139                <a href={imageUrl} target="_blank" rel="noopener noreferrer">
140                  <img
141                    src={imageUrl}
142                    alt={`Screenshot: ${filename}`}
143                    style={{ maxWidth: "100%", height: "auto" }}
144                  />
145                </a>
146              </div>
147            </div>
148          )}
149
150          {isComplete && hasError && (
151            <div className="screenshot-tool-section">
152              <div className="screenshot-tool-label">
153                <span>Error:</span>
154                {executionTime && <span className="screenshot-tool-time">{executionTime}</span>}
155              </div>
156              <pre className="screenshot-tool-error-message">
157                {toolResult && toolResult[0]?.Text
158                  ? toolResult[0].Text
159                  : "Screenshot capture failed"}
160              </pre>
161            </div>
162          )}
163
164          {isRunning && (
165            <div className="screenshot-tool-section">
166              <div className="screenshot-tool-label">Capturing screenshot...</div>
167            </div>
168          )}
169        </div>
170      )}
171    </div>
172  );
173}
174
175export default ScreenshotTool;