GenericTool.tsx

  1import React, { useState } from "react";
  2import { LLMContent } from "../types";
  3
  4interface GenericToolProps {
  5  toolName: string;
  6
  7  // For tool_use (pending state)
  8  toolInput?: unknown;
  9  isRunning?: boolean;
 10
 11  // For tool_result (completed state)
 12  toolResult?: LLMContent[];
 13  hasError?: boolean;
 14  executionTime?: string;
 15}
 16
 17function GenericTool({
 18  toolName,
 19  toolInput,
 20  isRunning,
 21  toolResult,
 22  hasError,
 23  executionTime,
 24}: GenericToolProps) {
 25  const [isExpanded, setIsExpanded] = useState(false);
 26
 27  // Format data for display
 28  const formatData = (data: unknown): string => {
 29    if (data === undefined || data === null) return "";
 30    if (typeof data === "string") return data;
 31    try {
 32      return JSON.stringify(data, null, 2);
 33    } catch {
 34      return String(data);
 35    }
 36  };
 37
 38  // Extract output from toolResult
 39  const output =
 40    toolResult && toolResult.length > 0
 41      ? toolResult.map((result) => result.Text || formatData(result)).join("\n")
 42      : "";
 43
 44  const isComplete = !isRunning && toolResult !== undefined;
 45
 46  return (
 47    <div className="tool" data-testid={isComplete ? "tool-call-completed" : "tool-call-running"}>
 48      <div className="tool-header" onClick={() => setIsExpanded(!isExpanded)}>
 49        <div className="tool-summary">
 50          <span className={`tool-emoji ${isRunning ? "running" : ""}`}></span>
 51          <span className="tool-command">{toolName}</span>
 52          {isComplete && hasError && <span className="tool-error"></span>}
 53          {isComplete && !hasError && <span className="tool-success"></span>}
 54        </div>
 55        <button
 56          className="tool-toggle"
 57          aria-label={isExpanded ? "Collapse" : "Expand"}
 58          aria-expanded={isExpanded}
 59        >
 60          <svg
 61            width="12"
 62            height="12"
 63            viewBox="0 0 12 12"
 64            fill="none"
 65            xmlns="http://www.w3.org/2000/svg"
 66            style={{
 67              transform: isExpanded ? "rotate(90deg)" : "rotate(0deg)",
 68              transition: "transform 0.2s",
 69            }}
 70          >
 71            <path
 72              d="M4.5 3L7.5 6L4.5 9"
 73              stroke="currentColor"
 74              strokeWidth="1.5"
 75              strokeLinecap="round"
 76              strokeLinejoin="round"
 77            />
 78          </svg>
 79        </button>
 80      </div>
 81
 82      {isExpanded && (
 83        <div className="tool-details">
 84          {toolInput !== undefined && (
 85            <div className="tool-section">
 86              <div className="tool-label">Input:</div>
 87              <pre className="tool-code">{formatData(toolInput)}</pre>
 88            </div>
 89          )}
 90
 91          {isRunning && (
 92            <div className="tool-section">
 93              <div className="tool-label">Status:</div>
 94              <div className="tool-running-text">running...</div>
 95            </div>
 96          )}
 97
 98          {isComplete && (
 99            <div className="tool-section">
100              <div className="tool-label">
101                Output{hasError ? " (Error)" : ""}:
102                {executionTime && <span className="tool-time">{executionTime}</span>}
103              </div>
104              <pre className={`tool-code ${hasError ? "error" : ""}`}>
105                {output || "(no output)"}
106              </pre>
107            </div>
108          )}
109        </div>
110      )}
111    </div>
112  );
113}
114
115export default GenericTool;