@hef2024/llmasaservice-ui 0.23.2 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -627,10 +627,15 @@ var ThinkingBlock = ({
627
627
  }) => {
628
628
  const displayTitle = title || getDefaultTitle(type);
629
629
  const icon = getIcon(type);
630
+ const getPreview = (text, maxLength = 60) => {
631
+ const cleaned = text.replace(/\s+/g, " ").trim();
632
+ if (cleaned.length <= maxLength) return cleaned;
633
+ return cleaned.substring(0, maxLength).trim() + "...";
634
+ };
630
635
  return /* @__PURE__ */ React10.createElement(
631
636
  "div",
632
637
  {
633
- className: `thinking-block thinking-block--${type} ${isCollapsed ? "thinking-block--collapsed" : ""}`
638
+ className: `thinking-block thinking-block--${type} ${isCollapsed ? "thinking-block--collapsed" : ""} ${isStreaming ? "thinking-block--streaming" : ""}`
634
639
  },
635
640
  /* @__PURE__ */ React10.createElement(
636
641
  "button",
@@ -640,7 +645,7 @@ var ThinkingBlock = ({
640
645
  type: "button",
641
646
  "aria-expanded": !isCollapsed
642
647
  },
643
- /* @__PURE__ */ React10.createElement("div", { className: "thinking-block__header-left" }, icon, /* @__PURE__ */ React10.createElement("span", { className: "thinking-block__title" }, displayTitle), isStreaming && /* @__PURE__ */ React10.createElement("span", { className: "thinking-block__streaming-indicator" }, /* @__PURE__ */ React10.createElement("span", { className: "thinking-block__streaming-dot" }), /* @__PURE__ */ React10.createElement("span", { className: "thinking-block__streaming-dot" }), /* @__PURE__ */ React10.createElement("span", { className: "thinking-block__streaming-dot" }))),
648
+ /* @__PURE__ */ React10.createElement("div", { className: "thinking-block__header-left" }, /* @__PURE__ */ React10.createElement("span", { className: `thinking-block__icon-wrapper ${isStreaming ? "thinking-block__icon-wrapper--spinning" : ""}` }, icon), /* @__PURE__ */ React10.createElement("span", { className: `thinking-block__title ${isStreaming ? "thinking-block__title--shimmer" : ""}` }, displayTitle), isCollapsed && content && /* @__PURE__ */ React10.createElement("span", { className: "thinking-block__preview" }, getPreview(content))),
644
649
  /* @__PURE__ */ React10.createElement(ChevronIcon, { isCollapsed })
645
650
  ),
646
651
  /* @__PURE__ */ React10.createElement("div", { className: "thinking-block__content-wrapper" }, /* @__PURE__ */ React10.createElement("div", { className: "thinking-block__content" }, content, isStreaming && /* @__PURE__ */ React10.createElement("span", { className: "thinking-block__cursor" }, "|")))
@@ -801,6 +806,11 @@ var ChatPanel = ({
801
806
  }, []);
802
807
  const [iframeUrl, setIframeUrl] = useState5(null);
803
808
  const responseAreaRef = useRef5(null);
809
+ const [userHasScrolled, setUserHasScrolled] = useState5(false);
810
+ const lastScrollTopRef = useRef5(0);
811
+ const prevResponseLengthRef = useRef5(0);
812
+ const userHasScrolledRef = useRef5(false);
813
+ const idleRef = useRef5(true);
804
814
  const THINKING_PATTERNS = useMemo(
805
815
  () => ({
806
816
  thinking: /<thinking>([\s\S]*?)<\/thinking>/gi,
@@ -2133,32 +2143,67 @@ var ChatPanel = ({
2133
2143
  }
2134
2144
  }
2135
2145
  }, [initialPrompt]);
2146
+ useEffect6(() => {
2147
+ userHasScrolledRef.current = userHasScrolled;
2148
+ }, [userHasScrolled]);
2149
+ useEffect6(() => {
2150
+ idleRef.current = idle;
2151
+ }, [idle]);
2136
2152
  useEffect6(() => {
2137
2153
  var _a2;
2138
- if (scrollToEnd) {
2154
+ if (idle) return;
2155
+ const currentResponseLength = response.length;
2156
+ const responseGotLonger = currentResponseLength > prevResponseLengthRef.current;
2157
+ prevResponseLengthRef.current = currentResponseLength;
2158
+ const shouldAutoScroll = scrollToEnd || !userHasScrolledRef.current;
2159
+ if (shouldAutoScroll && response && responseGotLonger) {
2139
2160
  if (window.top !== window.self) {
2140
2161
  const responseArea = responseAreaRef.current;
2141
- responseArea.scrollTo({
2142
- top: responseArea.scrollHeight,
2143
- behavior: "smooth"
2144
- });
2162
+ if (responseArea) {
2163
+ responseArea.scrollTo({
2164
+ top: responseArea.scrollHeight,
2165
+ behavior: "auto"
2166
+ });
2167
+ }
2145
2168
  } else {
2146
- (_a2 = bottomRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth", block: "end" });
2169
+ (_a2 = bottomRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "auto", block: "end" });
2147
2170
  }
2148
- } else {
2149
- const responseArea = responseAreaRef.current;
2150
- if (responseArea) {
2151
- setHasScroll(hasVerticalScrollbar(responseArea));
2152
- const handleScroll = () => {
2153
- const isScrolledToBottom = responseArea.scrollHeight - responseArea.scrollTop === responseArea.clientHeight;
2154
- setIsAtBottom(isScrolledToBottom);
2155
- };
2156
- handleScroll();
2157
- responseArea.addEventListener("scroll", handleScroll);
2158
- return () => responseArea.removeEventListener("scroll", handleScroll);
2171
+ }
2172
+ }, [response, idle, scrollToEnd]);
2173
+ useEffect6(() => {
2174
+ const responseArea = responseAreaRef.current;
2175
+ if (!responseArea) return;
2176
+ const handleWheel = (e) => {
2177
+ if (idleRef.current) return;
2178
+ if (e.deltaY < 0 && !userHasScrolledRef.current) {
2179
+ setUserHasScrolled(true);
2159
2180
  }
2181
+ };
2182
+ const handleScroll = () => {
2183
+ setHasScroll(hasVerticalScrollbar(responseArea));
2184
+ const isScrolledToBottom = responseArea.scrollHeight - responseArea.scrollTop - responseArea.clientHeight < 5;
2185
+ setIsAtBottom(isScrolledToBottom);
2186
+ if (!idleRef.current && userHasScrolledRef.current) {
2187
+ const isNearBottom = responseArea.scrollHeight - responseArea.scrollTop - responseArea.clientHeight < 50;
2188
+ if (isNearBottom) {
2189
+ setUserHasScrolled(false);
2190
+ }
2191
+ }
2192
+ };
2193
+ handleScroll();
2194
+ responseArea.addEventListener("wheel", handleWheel, { passive: true });
2195
+ responseArea.addEventListener("scroll", handleScroll, { passive: true });
2196
+ return () => {
2197
+ responseArea.removeEventListener("wheel", handleWheel);
2198
+ responseArea.removeEventListener("scroll", handleScroll);
2199
+ };
2200
+ }, []);
2201
+ useEffect6(() => {
2202
+ const responseArea = responseAreaRef.current;
2203
+ if (responseArea) {
2204
+ setHasScroll(hasVerticalScrollbar(responseArea));
2160
2205
  }
2161
- }, [response, history]);
2206
+ }, [history]);
2162
2207
  useEffect6(() => {
2163
2208
  if (historyCallbackRef.current) {
2164
2209
  historyCallbackRef.current(history);
@@ -2281,6 +2326,8 @@ var ChatPanel = ({
2281
2326
  hasAutoCollapsedRef.current = false;
2282
2327
  prevBlockCountRef.current = 0;
2283
2328
  setError(null);
2329
+ setUserHasScrolled(false);
2330
+ prevResponseLengthRef.current = 0;
2284
2331
  setResponse("");
2285
2332
  if (emailInput && isEmailAddress(emailInput) && !emailInputSet) {
2286
2333
  const newId = (currentCustomer == null ? void 0 : currentCustomer.customer_id) && currentCustomer.customer_id !== "" && currentCustomer.customer_id !== (currentCustomer == null ? void 0 : currentCustomer.customer_user_email) ? currentCustomer.customer_id : emailInput;
@@ -4620,7 +4667,7 @@ var AIChatPanel = ({
4620
4667
  cancelAnimationFrame(scrollRAFRef.current);
4621
4668
  }
4622
4669
  if (!force && responseAreaRef.current) {
4623
- const scrollViewport = responseAreaRef.current.querySelector("[data-radix-scroll-area-viewport]");
4670
+ const scrollViewport = responseAreaRef.current.querySelector(".ai-scroll-area-viewport");
4624
4671
  const scrollElement = scrollViewport || responseAreaRef.current;
4625
4672
  const scrollTop = scrollElement.scrollTop;
4626
4673
  const scrollHeight = scrollElement.scrollHeight;
@@ -4923,7 +4970,7 @@ var AIChatPanel = ({
4923
4970
  prevResponseLengthRef.current = currentResponseLength;
4924
4971
  const shouldAutoScroll = scrollToEndRef.current || !userHasScrolledRef.current;
4925
4972
  if (shouldAutoScroll && response && responseGotLonger) {
4926
- scrollToBottom(false);
4973
+ scrollToBottom(true);
4927
4974
  }
4928
4975
  }, [response, idle]);
4929
4976
  const idleRef = useRef6(idle);
@@ -4931,21 +4978,30 @@ var AIChatPanel = ({
4931
4978
  useEffect8(() => {
4932
4979
  const scrollArea = responseAreaRef.current;
4933
4980
  if (!scrollArea) return;
4934
- const scrollViewport = scrollArea.querySelector("[data-radix-scroll-area-viewport]");
4981
+ const scrollViewport = scrollArea.querySelector(".ai-scroll-area-viewport");
4935
4982
  const scrollElement = scrollViewport || scrollArea;
4983
+ const handleWheel = (e) => {
4984
+ if (idleRef.current) return;
4985
+ if (e.deltaY < 0 && !userHasScrolledRef.current) {
4986
+ setUserHasScrolled(true);
4987
+ }
4988
+ };
4936
4989
  const handleScroll = () => {
4937
- if (idleRef.current || userHasScrolledRef.current) return;
4938
- const currentScrollTop = scrollElement.scrollTop;
4990
+ if (idleRef.current || !userHasScrolledRef.current) return;
4939
4991
  const scrollHeight = scrollElement.scrollHeight;
4992
+ const currentScrollTop = scrollElement.scrollTop;
4940
4993
  const clientHeight = scrollElement.clientHeight;
4941
- const isNearBottom = scrollHeight - currentScrollTop - clientHeight < 100;
4942
- if (currentScrollTop < lastScrollTopRef.current && !isNearBottom) {
4943
- setUserHasScrolled(true);
4994
+ const isNearBottom = scrollHeight - currentScrollTop - clientHeight < 50;
4995
+ if (isNearBottom) {
4996
+ setUserHasScrolled(false);
4944
4997
  }
4945
- lastScrollTopRef.current = currentScrollTop;
4946
4998
  };
4999
+ scrollElement.addEventListener("wheel", handleWheel, { passive: true });
4947
5000
  scrollElement.addEventListener("scroll", handleScroll, { passive: true });
4948
- return () => scrollElement.removeEventListener("scroll", handleScroll);
5001
+ return () => {
5002
+ scrollElement.removeEventListener("wheel", handleWheel);
5003
+ scrollElement.removeEventListener("scroll", handleScroll);
5004
+ };
4949
5005
  }, []);
4950
5006
  useEffect8(() => {
4951
5007
  setFollowOnQuestionsState(followOnQuestions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hef2024/llmasaservice-ui",
3
- "version": "0.23.2",
3
+ "version": "0.24.1",
4
4
  "description": "Prebuilt UI components for LLMAsAService.io",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -2499,6 +2499,23 @@
2499
2499
  gap: 8px;
2500
2500
  }
2501
2501
 
2502
+ /* Icon Wrapper - for spinning animation */
2503
+ .thinking-block__icon-wrapper {
2504
+ display: flex;
2505
+ align-items: center;
2506
+ justify-content: center;
2507
+ flex-shrink: 0;
2508
+ }
2509
+
2510
+ .thinking-block__icon-wrapper--spinning {
2511
+ animation: thinkingIconSpin 1.5s linear infinite;
2512
+ }
2513
+
2514
+ @keyframes thinkingIconSpin {
2515
+ from { transform: rotate(0deg); }
2516
+ to { transform: rotate(360deg); }
2517
+ }
2518
+
2502
2519
  /* Icon */
2503
2520
  .thinking-block__icon {
2504
2521
  width: 16px;
@@ -2512,6 +2529,33 @@
2512
2529
  font-weight: 600;
2513
2530
  font-size: 13px;
2514
2531
  color: var(--thinking-block-text, var(--ai-chat-thinking-text));
2532
+ position: relative;
2533
+ }
2534
+
2535
+ /* Shimmer effect on title when streaming */
2536
+ .thinking-block__title--shimmer {
2537
+ background: linear-gradient(
2538
+ 90deg,
2539
+ var(--thinking-block-text, var(--ai-chat-thinking-text)) 0%,
2540
+ var(--thinking-block-text, var(--ai-chat-thinking-text)) 40%,
2541
+ var(--thinking-block-accent, var(--ai-chat-thinking-icon)) 50%,
2542
+ var(--thinking-block-text, var(--ai-chat-thinking-text)) 60%,
2543
+ var(--thinking-block-text, var(--ai-chat-thinking-text)) 100%
2544
+ );
2545
+ background-size: 200% 100%;
2546
+ -webkit-background-clip: text;
2547
+ background-clip: text;
2548
+ -webkit-text-fill-color: transparent;
2549
+ animation: titleShimmer 2s ease-in-out infinite;
2550
+ }
2551
+
2552
+ @keyframes titleShimmer {
2553
+ 0% {
2554
+ background-position: 100% 0;
2555
+ }
2556
+ 100% {
2557
+ background-position: -100% 0;
2558
+ }
2515
2559
  }
2516
2560
 
2517
2561
  /* Chevron (collapse indicator) */
@@ -2703,6 +2747,140 @@
2703
2747
  border-left-width: 3px;
2704
2748
  }
2705
2749
 
2750
+ /* ============================================================================
2751
+ Collapsed State - Compact Line Style with Preview
2752
+ ============================================================================ */
2753
+
2754
+ .thinking-block--collapsed {
2755
+ margin-bottom: 4px;
2756
+ border-radius: 6px;
2757
+ border-width: 0;
2758
+ background-color: transparent;
2759
+ }
2760
+
2761
+ .thinking-block--collapsed .thinking-block__header {
2762
+ padding: 4px 8px;
2763
+ }
2764
+
2765
+ .thinking-block--collapsed .thinking-block__header-left {
2766
+ gap: 6px;
2767
+ flex: 1;
2768
+ min-width: 0;
2769
+ }
2770
+
2771
+ .thinking-block--collapsed .thinking-block__icon-wrapper {
2772
+ flex-shrink: 0;
2773
+ }
2774
+
2775
+ .thinking-block--collapsed .thinking-block__icon {
2776
+ width: 14px;
2777
+ height: 14px;
2778
+ opacity: 0.6;
2779
+ }
2780
+
2781
+ .thinking-block--collapsed .thinking-block__title {
2782
+ font-size: 12px;
2783
+ font-weight: 500;
2784
+ opacity: 0.7;
2785
+ flex-shrink: 0;
2786
+ }
2787
+
2788
+ /* Preview text in collapsed state */
2789
+ .thinking-block__preview {
2790
+ font-size: 11px;
2791
+ font-weight: 400;
2792
+ opacity: 0.5;
2793
+ color: var(--thinking-block-text, var(--ai-chat-thinking-text));
2794
+ white-space: nowrap;
2795
+ overflow: hidden;
2796
+ text-overflow: ellipsis;
2797
+ flex: 1;
2798
+ min-width: 0;
2799
+ }
2800
+
2801
+ .thinking-block--collapsed .thinking-block__chevron {
2802
+ width: 12px;
2803
+ height: 12px;
2804
+ opacity: 0.4;
2805
+ flex-shrink: 0;
2806
+ }
2807
+
2808
+ /* Hover state for collapsed - subtle highlight */
2809
+ .thinking-block--collapsed:hover {
2810
+ background-color: rgba(0, 0, 0, 0.04);
2811
+ }
2812
+
2813
+ .thinking-block--collapsed:hover .thinking-block__icon,
2814
+ .thinking-block--collapsed:hover .thinking-block__title {
2815
+ opacity: 0.9;
2816
+ }
2817
+
2818
+ .thinking-block--collapsed:hover .thinking-block__preview {
2819
+ opacity: 0.7;
2820
+ }
2821
+
2822
+ .thinking-block--collapsed:hover .thinking-block__chevron {
2823
+ opacity: 0.6;
2824
+ }
2825
+
2826
+ .dark-theme .thinking-block--collapsed:hover {
2827
+ background-color: rgba(255, 255, 255, 0.06);
2828
+ }
2829
+
2830
+ /* ============================================================================
2831
+ Expanded State - Subtle, Transient Content
2832
+ ============================================================================ */
2833
+
2834
+ .thinking-block:not(.thinking-block--collapsed) {
2835
+ margin-bottom: 8px;
2836
+ border-radius: 6px;
2837
+ border: none;
2838
+ background-color: transparent;
2839
+ opacity: 0.75;
2840
+ }
2841
+
2842
+ .thinking-block:not(.thinking-block--collapsed):hover {
2843
+ opacity: 0.9;
2844
+ }
2845
+
2846
+ .thinking-block:not(.thinking-block--collapsed) .thinking-block__header {
2847
+ padding: 4px 10px;
2848
+ }
2849
+
2850
+ .thinking-block:not(.thinking-block--collapsed) .thinking-block__icon {
2851
+ width: 14px;
2852
+ height: 14px;
2853
+ opacity: 0.7;
2854
+ }
2855
+
2856
+ .thinking-block:not(.thinking-block--collapsed) .thinking-block__title {
2857
+ font-size: 12px;
2858
+ opacity: 0.8;
2859
+ }
2860
+
2861
+ .thinking-block:not(.thinking-block--collapsed) .thinking-block__chevron {
2862
+ opacity: 0.5;
2863
+ }
2864
+
2865
+ .thinking-block:not(.thinking-block--collapsed) .thinking-block__content {
2866
+ padding: 0 10px 6px 10px;
2867
+ font-size: 11px;
2868
+ line-height: 1.5;
2869
+ opacity: 0.7;
2870
+ max-height: 120px;
2871
+ overflow-y: auto;
2872
+ }
2873
+
2874
+ /* Streaming state gets slightly more presence */
2875
+ .thinking-block--streaming:not(.thinking-block--collapsed) {
2876
+ opacity: 0.85;
2877
+ background-color: rgba(0, 0, 0, 0.015);
2878
+ }
2879
+
2880
+ .dark-theme .thinking-block--streaming:not(.thinking-block--collapsed) {
2881
+ background-color: rgba(255, 255, 255, 0.02);
2882
+ }
2883
+
2706
2884
  /* Animation when block first appears */
2707
2885
  .thinking-block {
2708
2886
  animation: thinkingBlockAppear 0.2s ease-out;
@@ -1585,7 +1585,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
1585
1585
 
1586
1586
  // Check if we should scroll - only if user is near bottom or force is true
1587
1587
  if (!force && responseAreaRef.current) {
1588
- const scrollViewport = responseAreaRef.current.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement;
1588
+ const scrollViewport = responseAreaRef.current.querySelector('.ai-scroll-area-viewport') as HTMLElement;
1589
1589
  const scrollElement = scrollViewport || responseAreaRef.current;
1590
1590
 
1591
1591
  const scrollTop = scrollElement.scrollTop;
@@ -2038,8 +2038,9 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
2038
2038
  // Only scroll if response actually grew - use refs to avoid unnecessary effect runs
2039
2039
  const shouldAutoScroll = scrollToEndRef.current || !userHasScrolledRef.current;
2040
2040
  if (shouldAutoScroll && response && responseGotLonger) {
2041
- // Use non-forced scroll - will only scroll if near bottom
2042
- scrollToBottom(false);
2041
+ // Use forced scroll since userHasScrolled is our gatekeeper for user intent
2042
+ // The isNearBottom check in scrollToBottom is for layout changes, not streaming
2043
+ scrollToBottom(true);
2043
2044
  }
2044
2045
  }, [response, idle]); // ONLY response and idle - no other dependencies!
2045
2046
 
@@ -2047,39 +2048,48 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
2047
2048
  const idleRef = useRef(idle);
2048
2049
  idleRef.current = idle;
2049
2050
 
2050
- // Detect user scroll to disable auto-scroll
2051
+ // Detect user scroll intent via wheel event (fires before scroll position changes)
2051
2052
  useEffect(() => {
2052
2053
  const scrollArea = responseAreaRef.current;
2053
2054
  if (!scrollArea) return;
2054
2055
 
2055
2056
  // Get the actual scrollable element (ScrollArea wraps a viewport)
2056
- const scrollViewport = scrollArea.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement;
2057
+ const scrollViewport = scrollArea.querySelector('.ai-scroll-area-viewport') as HTMLElement;
2057
2058
  const scrollElement = scrollViewport || scrollArea;
2058
2059
 
2060
+ // Wheel event detects user intent immediately (before scroll position changes)
2061
+ const handleWheel = (e: WheelEvent) => {
2062
+ // Skip if not streaming
2063
+ if (idleRef.current) return;
2064
+
2065
+ // deltaY < 0 means scrolling UP (toward top of document)
2066
+ if (e.deltaY < 0 && !userHasScrolledRef.current) {
2067
+ setUserHasScrolled(true);
2068
+ }
2069
+ };
2070
+
2071
+ // Scroll event for detecting when user returns to bottom
2059
2072
  const handleScroll = () => {
2060
- // Skip if not streaming or already marked as user-scrolled
2061
- if (idleRef.current || userHasScrolledRef.current) return;
2073
+ // Skip if not streaming or user hasn't scrolled up
2074
+ if (idleRef.current || !userHasScrolledRef.current) return;
2062
2075
 
2063
- const currentScrollTop = scrollElement.scrollTop;
2064
2076
  const scrollHeight = scrollElement.scrollHeight;
2077
+ const currentScrollTop = scrollElement.scrollTop;
2065
2078
  const clientHeight = scrollElement.clientHeight;
2066
2079
 
2067
- // If user scrolled UP (away from bottom), disable auto-scroll
2068
- // We consider "near bottom" as within 100px to account for smooth scrolling
2069
- const isNearBottom = scrollHeight - currentScrollTop - clientHeight < 100;
2070
-
2071
- // User scrolled up if:
2072
- // 1. They scrolled upward (currentScrollTop < lastScrollTopRef.current)
2073
- // 2. AND they're not near the bottom
2074
- if (currentScrollTop < lastScrollTopRef.current && !isNearBottom) {
2075
- setUserHasScrolled(true);
2080
+ // Re-enable auto-scroll when user scrolls back near bottom
2081
+ const isNearBottom = scrollHeight - currentScrollTop - clientHeight < 50;
2082
+ if (isNearBottom) {
2083
+ setUserHasScrolled(false);
2076
2084
  }
2077
-
2078
- lastScrollTopRef.current = currentScrollTop;
2079
2085
  };
2080
2086
 
2087
+ scrollElement.addEventListener('wheel', handleWheel, { passive: true });
2081
2088
  scrollElement.addEventListener('scroll', handleScroll, { passive: true });
2082
- return () => scrollElement.removeEventListener('scroll', handleScroll);
2089
+ return () => {
2090
+ scrollElement.removeEventListener('wheel', handleWheel);
2091
+ scrollElement.removeEventListener('scroll', handleScroll);
2092
+ };
2083
2093
  }, []); // Empty deps - handler uses refs to get current values
2084
2094
 
2085
2095
  // Update follow-on questions from props
package/src/ChatPanel.css CHANGED
@@ -1359,6 +1359,23 @@ button[data-pending="true"]::after {
1359
1359
  gap: 8px;
1360
1360
  }
1361
1361
 
1362
+ /* Icon Wrapper - for spinning animation */
1363
+ .thinking-block__icon-wrapper {
1364
+ display: flex;
1365
+ align-items: center;
1366
+ justify-content: center;
1367
+ flex-shrink: 0;
1368
+ }
1369
+
1370
+ .thinking-block__icon-wrapper--spinning {
1371
+ animation: thinkingIconSpin 1.5s linear infinite;
1372
+ }
1373
+
1374
+ @keyframes thinkingIconSpin {
1375
+ from { transform: rotate(0deg); }
1376
+ to { transform: rotate(360deg); }
1377
+ }
1378
+
1362
1379
  /* Icon */
1363
1380
  .thinking-block__icon {
1364
1381
  width: 16px;
@@ -1372,6 +1389,33 @@ button[data-pending="true"]::after {
1372
1389
  font-weight: 600;
1373
1390
  font-size: 13px;
1374
1391
  color: var(--thinking-block-text, var(--title-text-color));
1392
+ position: relative;
1393
+ }
1394
+
1395
+ /* Shimmer effect on title when streaming */
1396
+ .thinking-block__title--shimmer {
1397
+ background: linear-gradient(
1398
+ 90deg,
1399
+ var(--thinking-block-text, var(--title-text-color)) 0%,
1400
+ var(--thinking-block-text, var(--title-text-color)) 40%,
1401
+ var(--thinking-block-accent, var(--ai-chat-thinking-icon)) 50%,
1402
+ var(--thinking-block-text, var(--title-text-color)) 60%,
1403
+ var(--thinking-block-text, var(--title-text-color)) 100%
1404
+ );
1405
+ background-size: 200% 100%;
1406
+ -webkit-background-clip: text;
1407
+ background-clip: text;
1408
+ -webkit-text-fill-color: transparent;
1409
+ animation: titleShimmer 2s ease-in-out infinite;
1410
+ }
1411
+
1412
+ @keyframes titleShimmer {
1413
+ 0% {
1414
+ background-position: 100% 0;
1415
+ }
1416
+ 100% {
1417
+ background-position: -100% 0;
1418
+ }
1375
1419
  }
1376
1420
 
1377
1421
  /* Chevron (collapse indicator) */
@@ -1563,6 +1607,140 @@ button[data-pending="true"]::after {
1563
1607
  border-left-width: 3px;
1564
1608
  }
1565
1609
 
1610
+ /* ============================================================================
1611
+ Collapsed State - Compact Line Style with Preview
1612
+ ============================================================================ */
1613
+
1614
+ .thinking-block--collapsed {
1615
+ margin-bottom: 4px;
1616
+ border-radius: 6px;
1617
+ border-width: 0;
1618
+ background-color: transparent;
1619
+ }
1620
+
1621
+ .thinking-block--collapsed .thinking-block__header {
1622
+ padding: 4px 8px;
1623
+ }
1624
+
1625
+ .thinking-block--collapsed .thinking-block__header-left {
1626
+ gap: 6px;
1627
+ flex: 1;
1628
+ min-width: 0;
1629
+ }
1630
+
1631
+ .thinking-block--collapsed .thinking-block__icon-wrapper {
1632
+ flex-shrink: 0;
1633
+ }
1634
+
1635
+ .thinking-block--collapsed .thinking-block__icon {
1636
+ width: 14px;
1637
+ height: 14px;
1638
+ opacity: 0.6;
1639
+ }
1640
+
1641
+ .thinking-block--collapsed .thinking-block__title {
1642
+ font-size: 12px;
1643
+ font-weight: 500;
1644
+ opacity: 0.7;
1645
+ flex-shrink: 0;
1646
+ }
1647
+
1648
+ /* Preview text in collapsed state */
1649
+ .thinking-block__preview {
1650
+ font-size: 11px;
1651
+ font-weight: 400;
1652
+ opacity: 0.5;
1653
+ color: var(--thinking-block-text, var(--title-text-color));
1654
+ white-space: nowrap;
1655
+ overflow: hidden;
1656
+ text-overflow: ellipsis;
1657
+ flex: 1;
1658
+ min-width: 0;
1659
+ }
1660
+
1661
+ .thinking-block--collapsed .thinking-block__chevron {
1662
+ width: 12px;
1663
+ height: 12px;
1664
+ opacity: 0.4;
1665
+ flex-shrink: 0;
1666
+ }
1667
+
1668
+ /* Hover state for collapsed - subtle highlight */
1669
+ .thinking-block--collapsed:hover {
1670
+ background-color: rgba(0, 0, 0, 0.04);
1671
+ }
1672
+
1673
+ .thinking-block--collapsed:hover .thinking-block__icon,
1674
+ .thinking-block--collapsed:hover .thinking-block__title {
1675
+ opacity: 0.9;
1676
+ }
1677
+
1678
+ .thinking-block--collapsed:hover .thinking-block__preview {
1679
+ opacity: 0.7;
1680
+ }
1681
+
1682
+ .thinking-block--collapsed:hover .thinking-block__chevron {
1683
+ opacity: 0.6;
1684
+ }
1685
+
1686
+ .dark-theme .thinking-block--collapsed:hover {
1687
+ background-color: rgba(255, 255, 255, 0.06);
1688
+ }
1689
+
1690
+ /* ============================================================================
1691
+ Expanded State - Subtle, Transient Content
1692
+ ============================================================================ */
1693
+
1694
+ .thinking-block:not(.thinking-block--collapsed) {
1695
+ margin-bottom: 8px;
1696
+ border-radius: 6px;
1697
+ border: none;
1698
+ background-color: transparent;
1699
+ opacity: 0.75;
1700
+ }
1701
+
1702
+ .thinking-block:not(.thinking-block--collapsed):hover {
1703
+ opacity: 0.9;
1704
+ }
1705
+
1706
+ .thinking-block:not(.thinking-block--collapsed) .thinking-block__header {
1707
+ padding: 4px 10px;
1708
+ }
1709
+
1710
+ .thinking-block:not(.thinking-block--collapsed) .thinking-block__icon {
1711
+ width: 14px;
1712
+ height: 14px;
1713
+ opacity: 0.7;
1714
+ }
1715
+
1716
+ .thinking-block:not(.thinking-block--collapsed) .thinking-block__title {
1717
+ font-size: 12px;
1718
+ opacity: 0.8;
1719
+ }
1720
+
1721
+ .thinking-block:not(.thinking-block--collapsed) .thinking-block__chevron {
1722
+ opacity: 0.5;
1723
+ }
1724
+
1725
+ .thinking-block:not(.thinking-block--collapsed) .thinking-block__content {
1726
+ padding: 0 10px 6px 10px;
1727
+ font-size: 11px;
1728
+ line-height: 1.5;
1729
+ opacity: 0.7;
1730
+ max-height: 120px;
1731
+ overflow-y: auto;
1732
+ }
1733
+
1734
+ /* Streaming state gets slightly more presence */
1735
+ .thinking-block--streaming:not(.thinking-block--collapsed) {
1736
+ opacity: 0.85;
1737
+ background-color: rgba(0, 0, 0, 0.015);
1738
+ }
1739
+
1740
+ .dark-theme .thinking-block--streaming:not(.thinking-block--collapsed) {
1741
+ background-color: rgba(255, 255, 255, 0.02);
1742
+ }
1743
+
1566
1744
  /* Animation when block first appears */
1567
1745
  .thinking-block {
1568
1746
  animation: thinkingBlockAppear 0.2s ease-out;