1
1
// src/components/ThinkingBlockPeek.tsx
2
- import {
3
- ChevronDownIcon ,
4
- ChevronRightIcon ,
5
- LightBulbIcon ,
6
- } from "@heroicons/react/24/outline" ;
2
+ import { ChevronDownIcon } from "@heroicons/react/24/outline" ;
3
+ import { ChevronUpIcon } from "@heroicons/react/24/solid" ;
7
4
import { ChatHistoryItem } from "core" ;
8
- import { useState } from "react" ;
5
+ import { useEffect , useState } from "react" ;
9
6
import styled from "styled-components" ;
10
- import { lightGray , vscBackground } from ".." ;
7
+ import { defaultBorderRadius , lightGray , vscBackground } from ".." ;
8
+ import { getFontSize } from "../../util" ;
11
9
import StyledMarkdownPreview from "../markdown/StyledMarkdownPreview" ;
12
10
11
+ const SpoilerButton = styled . div `
12
+ background-color: ${ vscBackground } ;
13
+ width: fit-content;
14
+ margin: 8px 6px 0px 2px;
15
+ font-size: ${ getFontSize ( ) - 2 } px;
16
+ border: 0.5px solid ${ lightGray } ;
17
+ border-radius: ${ defaultBorderRadius } ;
18
+ padding: 4px 8px;
19
+ color: ${ lightGray } ;
20
+ cursor: pointer;
21
+ box-shadow:
22
+ 0 4px 6px rgba(0, 0, 0, 0.1),
23
+ 0 1px 3px rgba(0, 0, 0, 0.08);
24
+ transition: box-shadow 0.3s ease;
25
+
26
+ &:hover {
27
+ box-shadow:
28
+ 0 6px 8px rgba(0, 0, 0, 0.15),
29
+ 0 3px 6px rgba(0, 0, 0, 0.1);
30
+ }
31
+ ` ;
32
+
33
+ const ButtonContent = styled . div `
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 6px;
37
+ ` ;
38
+
39
+ export const ThinkingText = styled . span `
40
+ position: relative;
41
+ padding-right: 12px;
42
+
43
+ &:after {
44
+ content: "...";
45
+ position: absolute;
46
+ animation: ellipsis 1s steps(4, end) infinite;
47
+ width: 0px;
48
+ display: inline-block;
49
+ overflow: hidden;
50
+ }
51
+
52
+ @keyframes ellipsis {
53
+ 0%,
54
+ 100% {
55
+ width: 0px;
56
+ }
57
+ 33% {
58
+ width: 8px;
59
+ }
60
+ 66% {
61
+ width: 16px;
62
+ }
63
+ 90% {
64
+ width: 24px;
65
+ }
66
+ }
67
+ ` ;
68
+
13
69
const MarkdownWrapper = styled . div `
14
70
& > div > *:first-child {
15
71
margin-top: 0 !important;
@@ -21,48 +77,69 @@ interface ThinkingBlockPeekProps {
21
77
redactedThinking ?: string ;
22
78
index : number ;
23
79
prevItem : ChatHistoryItem | null ;
80
+ inProgress ?: boolean ;
24
81
signature ?: string ;
82
+ tokens ?: number ;
25
83
}
26
84
27
85
function ThinkingBlockPeek ( {
28
86
content,
29
87
redactedThinking,
30
88
index,
31
89
prevItem,
90
+ inProgress,
32
91
signature,
92
+ tokens,
33
93
} : ThinkingBlockPeekProps ) {
34
94
const [ open , setOpen ] = useState ( false ) ;
95
+ const [ startTime , setStartTime ] = useState < number | null > ( null ) ;
96
+ const [ elapsedTime , setElapsedTime ] = useState < string > ( "" ) ;
35
97
36
98
const duplicateRedactedThinkingBlock =
37
99
prevItem &&
38
100
prevItem . message . role === "thinking" &&
39
101
redactedThinking &&
40
102
prevItem . message . redactedThinking ;
41
103
104
+ useEffect ( ( ) => {
105
+ if ( inProgress ) {
106
+ setStartTime ( Date . now ( ) ) ;
107
+ setElapsedTime ( "" ) ;
108
+ } else if ( startTime ) {
109
+ const endTime = Date . now ( ) ;
110
+ const diff = endTime - startTime ;
111
+ const diffString = `${ ( diff / 1000 ) . toFixed ( 1 ) } s` ;
112
+ setElapsedTime ( diffString ) ;
113
+ }
114
+ } , [ inProgress ] ) ;
115
+
42
116
return duplicateRedactedThinkingBlock ? null : (
43
117
< div className = "thread-message" >
44
118
< div className = "" style = { { backgroundColor : vscBackground } } >
45
119
< div
46
- className = "flex cursor-pointer items-center justify-start pl-2 text-xs text-gray-300"
47
- onClick = { ( ) => setOpen ( ( prev ) => ! prev ) }
120
+ className = "flex items-center justify-start pl-2 text-xs text-gray-300"
48
121
data-testid = "thinking-block-peek"
49
122
>
50
- < div className = "relative mr-1.5 h-4 w-4" >
51
- < ChevronRightIcon
52
- className = { `absolute h-4 w-4 transition-all duration-200 ease-in-out text-[${ lightGray } ] ${
53
- open ? "rotate-90 opacity-0" : "rotate-0 opacity-100"
54
- } `}
55
- />
56
- < ChevronDownIcon
57
- className = { `absolute h-4 w-4 transition-all duration-200 ease-in-out text-[${ lightGray } ] ${
58
- open ? "rotate-0 opacity-100" : "-rotate-90 opacity-0"
59
- } `}
60
- />
61
- </ div >
62
- < LightBulbIcon className = "mr-0 h-4 w-4 text-gray-400" />
63
- < span className = "ml-1 text-xs text-gray-400 transition-colors duration-200" >
64
- { redactedThinking ? "Redacted Thinking" : "AI Reasoning" }
65
- </ span >
123
+ < SpoilerButton onClick = { ( ) => setOpen ( ( prev ) => ! prev ) } >
124
+ < ButtonContent >
125
+ { inProgress ? (
126
+ < ThinkingText >
127
+ { redactedThinking ? "Redacted Thinking" : "Thinking" }
128
+ </ ThinkingText >
129
+ ) : redactedThinking ? (
130
+ "Redacted Thinking"
131
+ ) : (
132
+ "Thought" +
133
+ ( elapsedTime ? ` for ${ elapsedTime } ` : "" ) +
134
+ ( tokens ? ` (${ tokens } tokens)` : "" )
135
+ ) }
136
+ { open ? (
137
+ < ChevronUpIcon className = "h-3 w-3" />
138
+ ) : (
139
+ < ChevronDownIcon className = "h-3 w-3" />
140
+ ) }
141
+ </ ButtonContent >
142
+ </ SpoilerButton >
66
143
</ div >
67
144
68
145
< div
0 commit comments