TaskHeader.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
  2. import React, { useEffect, useRef, useState } from "react"
  3. import TextTruncate from "react-text-truncate"
  4. import { useWindowSize } from "react-use"
  5. import { vscode } from "../utilities/vscode"
  6. interface TaskHeaderProps {
  7. taskText: string
  8. tokensIn: number
  9. tokensOut: number
  10. totalCost: number
  11. onClose: () => void
  12. isHidden: boolean
  13. }
  14. const TaskHeader: React.FC<TaskHeaderProps> = ({ taskText, tokensIn, tokensOut, totalCost, onClose, isHidden }) => {
  15. const [isExpanded, setIsExpanded] = useState(false)
  16. const [textTruncateKey, setTextTruncateKey] = useState(0)
  17. const textContainerRef = useRef<HTMLDivElement>(null)
  18. /*
  19. 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.
  20. Sources
  21. - https://usehooks-ts.com/react-hook/use-event-listener
  22. - https://streamich.github.io/react-use/?path=/story/sensors-useevent--docs
  23. - https://github.com/streamich/react-use/blob/master/src/useEvent.ts
  24. - https://stackoverflow.com/questions/55565444/how-to-register-event-with-useeffect-hooks
  25. Before:
  26. const updateMaxHeight = useCallback(() => {
  27. if (isExpanded && textContainerRef.current) {
  28. const maxHeight = window.innerHeight * (3 / 5)
  29. textContainerRef.current.style.maxHeight = `${maxHeight}px`
  30. }
  31. }, [isExpanded])
  32. useEffect(() => {
  33. updateMaxHeight()
  34. }, [isExpanded, updateMaxHeight])
  35. useEffect(() => {
  36. window.removeEventListener("resize", updateMaxHeight)
  37. window.addEventListener("resize", updateMaxHeight)
  38. return () => {
  39. window.removeEventListener("resize", updateMaxHeight)
  40. }
  41. }, [updateMaxHeight])
  42. After:
  43. */
  44. const { height: windowHeight } = useWindowSize()
  45. useEffect(() => {
  46. if (isExpanded && textContainerRef.current) {
  47. const maxHeight = windowHeight * (3 / 5)
  48. textContainerRef.current.style.maxHeight = `${maxHeight}px`
  49. }
  50. }, [isExpanded, windowHeight])
  51. useEffect(() => {
  52. if (!isHidden) {
  53. /*
  54. There's an issue with TextTruncate where it is hidden when removed from the screen. It uses canvas to measure text width and adjusts the content accordingly. However, when the component is hidden, the canvas is not rendered and the text is not measured.
  55. We can fix this by forcing re-render after navigation by using a key prop that changes when you navigate back to the page.
  56. - https://github.com/ShinyChang/React-Text-Truncate?tab=readme-ov-file#faq
  57. */
  58. setTextTruncateKey((prev) => prev + 1)
  59. }
  60. }, [isHidden])
  61. const toggleExpand = () => setIsExpanded(!isExpanded)
  62. const handleDownload = () => {
  63. vscode.postMessage({ type: "downloadTask" })
  64. }
  65. return (
  66. <div style={{ padding: "15px 15px 10px 15px" }}>
  67. <div
  68. style={{
  69. backgroundColor: "var(--vscode-badge-background)",
  70. color: "var(--vscode-badge-foreground)",
  71. borderRadius: "3px",
  72. padding: "12px",
  73. display: "flex",
  74. flexDirection: "column",
  75. gap: "8px",
  76. position: "relative",
  77. }}>
  78. <div
  79. style={{
  80. display: "flex",
  81. justifyContent: "space-between",
  82. alignItems: "center",
  83. }}>
  84. <span style={{ fontWeight: "bold", fontSize: "16px" }}>Task</span>
  85. <VSCodeButton
  86. appearance="icon"
  87. onClick={onClose}
  88. style={{ marginTop: "-5px", marginRight: "-5px" }}>
  89. <span className="codicon codicon-close"></span>
  90. </VSCodeButton>
  91. </div>
  92. <div
  93. ref={textContainerRef}
  94. style={{
  95. fontSize: "var(--vscode-font-size)",
  96. overflowY: isExpanded ? "auto" : "hidden",
  97. wordBreak: "break-word",
  98. }}>
  99. <TextTruncate
  100. key={textTruncateKey}
  101. line={isExpanded ? 0 : 3}
  102. element="span"
  103. truncateText="…"
  104. text={taskText}
  105. textTruncateChild={
  106. <span
  107. style={{
  108. cursor: "pointer",
  109. color: "var(--vscode-textLink-foreground)",
  110. marginLeft: "5px",
  111. }}
  112. onClick={toggleExpand}>
  113. See more
  114. </span>
  115. }
  116. />
  117. {isExpanded && (
  118. <span
  119. style={{
  120. cursor: "pointer",
  121. color: "var(--vscode-textLink-foreground)",
  122. marginLeft: "5px",
  123. }}
  124. onClick={toggleExpand}>
  125. See less
  126. </span>
  127. )}
  128. </div>
  129. <div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
  130. <div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
  131. <span style={{ fontWeight: "bold" }}>Tokens:</span>
  132. <span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
  133. <i
  134. className="codicon codicon-arrow-down"
  135. style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-2px" }}
  136. />
  137. {tokensOut.toLocaleString()}
  138. </span>
  139. <span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
  140. <i
  141. className="codicon codicon-arrow-up"
  142. style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-1.5px" }}
  143. />
  144. {tokensIn.toLocaleString()}
  145. </span>
  146. </div>
  147. <div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
  148. <span style={{ fontWeight: "bold" }}>API Cost:</span>
  149. <span>${totalCost.toFixed(4)}</span>
  150. </div>
  151. </div>
  152. <VSCodeButton
  153. appearance="icon"
  154. onClick={handleDownload}
  155. style={{
  156. position: "absolute",
  157. bottom: "9.5px",
  158. right: "9px",
  159. }}>
  160. <div style={{ fontSize: "10.5px", fontWeight: "bold", opacity: 0.6 }}>EXPORT .MD</div>
  161. </VSCodeButton>
  162. </div>
  163. </div>
  164. )
  165. }
  166. export default TaskHeader