TaskHeader.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
  2. import React, { useEffect, useRef, useState } from "react"
  3. import { useWindowSize } from "react-use"
  4. import { ClaudeMessage } from "../../../src/shared/ExtensionMessage"
  5. import { vscode } from "../utils/vscode"
  6. import Thumbnails from "./Thumbnails"
  7. interface TaskHeaderProps {
  8. task: ClaudeMessage
  9. tokensIn: number
  10. tokensOut: number
  11. doesModelSupportPromptCache: boolean
  12. cacheWrites?: number
  13. cacheReads?: number
  14. totalCost: number
  15. onClose: () => void
  16. isHidden: boolean
  17. }
  18. const TaskHeader: React.FC<TaskHeaderProps> = ({
  19. task,
  20. tokensIn,
  21. tokensOut,
  22. doesModelSupportPromptCache,
  23. cacheWrites,
  24. cacheReads,
  25. totalCost,
  26. onClose,
  27. isHidden,
  28. }) => {
  29. const [isExpanded, setIsExpanded] = useState(false)
  30. const [showSeeMore, setShowSeeMore] = useState(false)
  31. const textContainerRef = useRef<HTMLDivElement>(null)
  32. const textRef = useRef<HTMLDivElement>(null)
  33. /*
  34. When dealing with event listeners in React components that depend on state variables, we face a challenge. We want our listener to always use the most up-to-date version of a callback function that relies on current state, but we don't want to constantly add and remove event listeners as that function updates. This scenario often arises with resize listeners or other window events. Simply adding the listener in a useEffect with an empty dependency array risks using stale state, while including the callback in the dependencies can lead to unnecessary re-registrations of the listener. There are react hook libraries that provide a elegant solution to this problem by utilizing the useRef hook to maintain a reference to the latest callback function without triggering re-renders or effect re-runs. This approach ensures that our event listener always has access to the most current state while minimizing performance overhead and potential memory leaks from multiple listener registrations.
  35. Sources
  36. - https://usehooks-ts.com/react-hook/use-event-listener
  37. - https://streamich.github.io/react-use/?path=/story/sensors-useevent--docs
  38. - https://github.com/streamich/react-use/blob/master/src/useEvent.ts
  39. - https://stackoverflow.com/questions/55565444/how-to-register-event-with-useeffect-hooks
  40. Before:
  41. const updateMaxHeight = useCallback(() => {
  42. if (isExpanded && textContainerRef.current) {
  43. const maxHeight = window.innerHeight * (3 / 5)
  44. textContainerRef.current.style.maxHeight = `${maxHeight}px`
  45. }
  46. }, [isExpanded])
  47. useEffect(() => {
  48. updateMaxHeight()
  49. }, [isExpanded, updateMaxHeight])
  50. useEffect(() => {
  51. window.removeEventListener("resize", updateMaxHeight)
  52. window.addEventListener("resize", updateMaxHeight)
  53. return () => {
  54. window.removeEventListener("resize", updateMaxHeight)
  55. }
  56. }, [updateMaxHeight])
  57. After:
  58. */
  59. const { height: windowHeight, width: windowWidth } = useWindowSize()
  60. useEffect(() => {
  61. if (isExpanded && textContainerRef.current) {
  62. const maxHeight = windowHeight * (1 / 2)
  63. textContainerRef.current.style.maxHeight = `${maxHeight}px`
  64. }
  65. }, [isExpanded, windowHeight])
  66. useEffect(() => {
  67. if (textRef.current && textContainerRef.current) {
  68. let textContainerHeight = textContainerRef.current.clientHeight
  69. if (!textContainerHeight) {
  70. textContainerHeight = textContainerRef.current.getBoundingClientRect().height
  71. }
  72. const isOverflowing = textRef.current.scrollHeight > textContainerHeight
  73. // necessary to show see more button again if user resizes window to expand and then back to collapse
  74. if (!isOverflowing) {
  75. setIsExpanded(false)
  76. }
  77. setShowSeeMore(isOverflowing)
  78. }
  79. }, [task.text, windowWidth])
  80. const toggleExpand = () => setIsExpanded(!isExpanded)
  81. const handleDownload = () => {
  82. vscode.postMessage({ type: "downloadTask" })
  83. }
  84. return (
  85. <div style={{ padding: "15px 15px 10px 15px" }}>
  86. <div
  87. style={{
  88. backgroundColor: "var(--vscode-badge-background)",
  89. color: "var(--vscode-badge-foreground)",
  90. borderRadius: "3px",
  91. padding: "12px",
  92. display: "flex",
  93. flexDirection: "column",
  94. gap: "8px",
  95. position: "relative",
  96. }}>
  97. <div
  98. style={{
  99. display: "flex",
  100. justifyContent: "space-between",
  101. alignItems: "center",
  102. }}>
  103. <span style={{ fontWeight: "bold", fontSize: "16px" }}>Task</span>
  104. <VSCodeButton
  105. appearance="icon"
  106. onClick={onClose}
  107. style={{ marginTop: "-5px", marginRight: "-5px" }}>
  108. <span className="codicon codicon-close"></span>
  109. </VSCodeButton>
  110. </div>
  111. <div
  112. ref={textContainerRef}
  113. style={{
  114. fontSize: "var(--vscode-font-size)",
  115. overflowY: isExpanded ? "auto" : "hidden",
  116. wordBreak: "break-word",
  117. overflowWrap: "anywhere",
  118. position: "relative",
  119. }}>
  120. <div
  121. ref={textRef}
  122. style={{
  123. display: "-webkit-box",
  124. WebkitLineClamp: isExpanded ? "unset" : 3,
  125. WebkitBoxOrient: "vertical",
  126. overflow: "hidden",
  127. whiteSpace: "pre-wrap",
  128. wordBreak: "break-word",
  129. overflowWrap: "anywhere",
  130. }}>
  131. {task.text}
  132. </div>
  133. {!isExpanded && showSeeMore && (
  134. <div
  135. style={{
  136. position: "absolute",
  137. right: 0,
  138. bottom: 0,
  139. display: "flex",
  140. alignItems: "center",
  141. }}>
  142. <div
  143. style={{
  144. width: 30,
  145. height: "1.2em",
  146. background:
  147. "linear-gradient(to right, transparent, var(--vscode-badge-background))",
  148. }}
  149. />
  150. <div
  151. style={{
  152. cursor: "pointer",
  153. color: "var(--vscode-textLink-foreground)",
  154. paddingRight: 0,
  155. paddingLeft: 3,
  156. backgroundColor: "var(--vscode-badge-background)",
  157. }}
  158. onClick={toggleExpand}>
  159. See more
  160. </div>
  161. </div>
  162. )}
  163. </div>
  164. {isExpanded && showSeeMore && (
  165. <div
  166. style={{
  167. cursor: "pointer",
  168. color: "var(--vscode-textLink-foreground)",
  169. marginLeft: "auto",
  170. textAlign: "right",
  171. paddingRight: 0,
  172. }}
  173. onClick={toggleExpand}>
  174. See less
  175. </div>
  176. )}
  177. {task.images && task.images.length > 0 && <Thumbnails images={task.images} />}
  178. <div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
  179. <div style={{ display: "flex", alignItems: "center", gap: "4px", flexWrap: "wrap" }}>
  180. <span style={{ fontWeight: "bold" }}>Tokens:</span>
  181. <span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
  182. <i
  183. className="codicon codicon-arrow-up"
  184. style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-1.5px" }}
  185. />
  186. {tokensIn.toLocaleString()}
  187. </span>
  188. <span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
  189. <i
  190. className="codicon codicon-arrow-down"
  191. style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-2px" }}
  192. />
  193. {tokensOut.toLocaleString()}
  194. </span>
  195. </div>
  196. {(doesModelSupportPromptCache || cacheReads !== undefined || cacheWrites !== undefined) && (
  197. <div style={{ display: "flex", alignItems: "center", gap: "4px", flexWrap: "wrap" }}>
  198. <span style={{ fontWeight: "bold" }}>Prompt Cache:</span>
  199. <span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
  200. <i
  201. className="codicon codicon-database"
  202. style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-1px" }}
  203. />
  204. +{(cacheWrites || 0).toLocaleString()}
  205. </span>
  206. <span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
  207. <i
  208. className="codicon codicon-arrow-right"
  209. style={{ fontSize: "12px", fontWeight: "bold", marginBottom: 0 }}
  210. />
  211. {(cacheReads || 0).toLocaleString()}
  212. </span>
  213. </div>
  214. )}
  215. <div
  216. style={{
  217. display: "flex",
  218. justifyContent: "space-between",
  219. alignItems: "center",
  220. }}>
  221. <div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
  222. <span style={{ fontWeight: "bold" }}>API Cost:</span>
  223. <span>${totalCost.toFixed(4)}</span>
  224. </div>
  225. <VSCodeButton
  226. appearance="icon"
  227. onClick={handleDownload}
  228. style={{
  229. marginBottom: "-2px",
  230. marginRight: "-2.5px",
  231. }}>
  232. <div style={{ fontSize: "10.5px", fontWeight: "bold", opacity: 0.6 }}>EXPORT .MD</div>
  233. </VSCodeButton>
  234. </div>
  235. </div>
  236. </div>
  237. </div>
  238. )
  239. }
  240. export default TaskHeader