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