@hef2024/llmasaservice-ui 0.22.11 → 0.23.0

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.
@@ -0,0 +1,150 @@
1
+ import React from 'react';
2
+
3
+ export type ThinkingBlockType = 'thinking' | 'reasoning' | 'searching';
4
+
5
+ export interface ThinkingBlockProps {
6
+ type: ThinkingBlockType;
7
+ content: string;
8
+ isStreaming: boolean;
9
+ isCollapsed: boolean;
10
+ onToggleCollapse: () => void;
11
+ /** Optional custom title override */
12
+ title?: string;
13
+ }
14
+
15
+ // Icons for each thinking type
16
+ const ThinkingIcon = () => (
17
+ <svg
18
+ xmlns="http://www.w3.org/2000/svg"
19
+ viewBox="0 0 24 24"
20
+ fill="none"
21
+ stroke="currentColor"
22
+ strokeWidth="2"
23
+ strokeLinecap="round"
24
+ strokeLinejoin="round"
25
+ className="thinking-block__icon"
26
+ >
27
+ <circle cx="12" cy="12" r="10" />
28
+ <path d="M12 16v-4" />
29
+ <path d="M12 8h.01" />
30
+ </svg>
31
+ );
32
+
33
+ const ReasoningIcon = () => (
34
+ <svg
35
+ xmlns="http://www.w3.org/2000/svg"
36
+ viewBox="0 0 24 24"
37
+ fill="none"
38
+ stroke="currentColor"
39
+ strokeWidth="2"
40
+ strokeLinecap="round"
41
+ strokeLinejoin="round"
42
+ className="thinking-block__icon"
43
+ >
44
+ <path d="M12 2a8 8 0 0 0-8 8c0 3.4 2.1 6.3 5 7.5V20a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-2.5c2.9-1.2 5-4.1 5-7.5a8 8 0 0 0-8-8z" />
45
+ <path d="M9 22h6" />
46
+ </svg>
47
+ );
48
+
49
+ const SearchingIcon = () => (
50
+ <svg
51
+ xmlns="http://www.w3.org/2000/svg"
52
+ viewBox="0 0 24 24"
53
+ fill="none"
54
+ stroke="currentColor"
55
+ strokeWidth="2"
56
+ strokeLinecap="round"
57
+ strokeLinejoin="round"
58
+ className="thinking-block__icon"
59
+ >
60
+ <circle cx="11" cy="11" r="8" />
61
+ <path d="m21 21-4.3-4.3" />
62
+ </svg>
63
+ );
64
+
65
+ const ChevronIcon = ({ isCollapsed }: { isCollapsed: boolean }) => (
66
+ <svg
67
+ xmlns="http://www.w3.org/2000/svg"
68
+ viewBox="0 0 24 24"
69
+ fill="none"
70
+ stroke="currentColor"
71
+ strokeWidth="2"
72
+ strokeLinecap="round"
73
+ strokeLinejoin="round"
74
+ className={`thinking-block__chevron ${isCollapsed ? 'thinking-block__chevron--collapsed' : ''}`}
75
+ >
76
+ <path d="m6 9 6 6 6-6" />
77
+ </svg>
78
+ );
79
+
80
+ const getIcon = (type: ThinkingBlockType) => {
81
+ switch (type) {
82
+ case 'thinking':
83
+ return <ThinkingIcon />;
84
+ case 'reasoning':
85
+ return <ReasoningIcon />;
86
+ case 'searching':
87
+ return <SearchingIcon />;
88
+ }
89
+ };
90
+
91
+ const getDefaultTitle = (type: ThinkingBlockType): string => {
92
+ switch (type) {
93
+ case 'thinking':
94
+ return 'Thinking';
95
+ case 'reasoning':
96
+ return 'Reasoning';
97
+ case 'searching':
98
+ return 'Searching';
99
+ }
100
+ };
101
+
102
+ /**
103
+ * ThinkingBlock - A collapsible block for displaying thinking/reasoning/searching content
104
+ * with streaming support. Content streams in naturally as it arrives.
105
+ */
106
+ export const ThinkingBlock: React.FC<ThinkingBlockProps> = ({
107
+ type,
108
+ content,
109
+ isStreaming,
110
+ isCollapsed,
111
+ onToggleCollapse,
112
+ title,
113
+ }) => {
114
+ const displayTitle = title || getDefaultTitle(type);
115
+ const icon = getIcon(type);
116
+
117
+ return (
118
+ <div
119
+ className={`thinking-block thinking-block--${type} ${isCollapsed ? 'thinking-block--collapsed' : ''}`}
120
+ >
121
+ <button
122
+ className="thinking-block__header"
123
+ onClick={onToggleCollapse}
124
+ type="button"
125
+ aria-expanded={!isCollapsed}
126
+ >
127
+ <div className="thinking-block__header-left">
128
+ {icon}
129
+ <span className="thinking-block__title">{displayTitle}</span>
130
+ {isStreaming && (
131
+ <span className="thinking-block__streaming-indicator">
132
+ <span className="thinking-block__streaming-dot" />
133
+ <span className="thinking-block__streaming-dot" />
134
+ <span className="thinking-block__streaming-dot" />
135
+ </span>
136
+ )}
137
+ </div>
138
+ <ChevronIcon isCollapsed={isCollapsed} />
139
+ </button>
140
+ <div className="thinking-block__content-wrapper">
141
+ <div className="thinking-block__content">
142
+ {content}
143
+ {isStreaming && <span className="thinking-block__cursor">|</span>}
144
+ </div>
145
+ </div>
146
+ </div>
147
+ );
148
+ };
149
+
150
+ export default ThinkingBlock;
@@ -0,0 +1,101 @@
1
+ import React, { useRef, useEffect, useState } from 'react';
2
+
3
+ export interface WordFadeInProps {
4
+ content: string;
5
+ className?: string;
6
+ /** Animation duration in ms */
7
+ animationDuration?: number;
8
+ }
9
+
10
+ /**
11
+ * WordFadeIn - A component that applies fade-in animation to new content chunks
12
+ * as they arrive during streaming. Each new chunk fades in smoothly.
13
+ */
14
+ export const WordFadeIn: React.FC<WordFadeInProps> = ({
15
+ content,
16
+ className = '',
17
+ animationDuration = 300,
18
+ }) => {
19
+ // Track segments: each segment is content that arrived together
20
+ const [segments, setSegments] = useState<Array<{ text: string; key: number }>>([]);
21
+ const prevContentRef = useRef<string>('');
22
+ const keyCounterRef = useRef(0);
23
+ const animationTimeoutsRef = useRef<Set<NodeJS.Timeout>>(new Set());
24
+
25
+ useEffect(() => {
26
+ const prevContent = prevContentRef.current;
27
+
28
+ // If content is shorter or same, reset (new response started)
29
+ if (content.length <= prevContent.length && content !== prevContent) {
30
+ setSegments([{ text: content, key: keyCounterRef.current++ }]);
31
+ prevContentRef.current = content;
32
+ return;
33
+ }
34
+
35
+ if (content === prevContent) {
36
+ return;
37
+ }
38
+
39
+ // Find the new content (what was added since last update)
40
+ const newContent = content.slice(prevContent.length);
41
+
42
+ if (newContent) {
43
+ const newKey = keyCounterRef.current++;
44
+ setSegments(prev => [...prev, { text: newContent, key: newKey }]);
45
+ prevContentRef.current = content;
46
+ }
47
+ }, [content]);
48
+
49
+ // Consolidate segments periodically to prevent too many DOM elements
50
+ useEffect(() => {
51
+ if (segments.length > 20) {
52
+ // Merge older segments, keep recent ones for animation
53
+ const timer = setTimeout(() => {
54
+ setSegments(prev => {
55
+ if (prev.length <= 5) return prev;
56
+ const olderText = prev.slice(0, -5).map(s => s.text).join('');
57
+ const recentSegments = prev.slice(-5);
58
+ return [{ text: olderText, key: -1 }, ...recentSegments];
59
+ });
60
+ }, animationDuration + 100);
61
+ animationTimeoutsRef.current.add(timer);
62
+ return () => {
63
+ animationTimeoutsRef.current.delete(timer);
64
+ clearTimeout(timer);
65
+ };
66
+ }
67
+ }, [segments.length, animationDuration]);
68
+
69
+ // Cleanup on unmount
70
+ useEffect(() => {
71
+ return () => {
72
+ animationTimeoutsRef.current.forEach(timer => clearTimeout(timer));
73
+ };
74
+ }, []);
75
+
76
+ // Reset when content is cleared
77
+ useEffect(() => {
78
+ if (!content && segments.length > 0) {
79
+ setSegments([]);
80
+ prevContentRef.current = '';
81
+ }
82
+ }, [content, segments.length]);
83
+
84
+ return (
85
+ <span className={className}>
86
+ {segments.map((segment, index) => (
87
+ <span
88
+ key={segment.key}
89
+ className={segment.key >= 0 ? 'streaming-text-chunk' : ''}
90
+ style={{
91
+ '--fade-duration': `${animationDuration}ms`,
92
+ } as React.CSSProperties}
93
+ >
94
+ {segment.text}
95
+ </span>
96
+ ))}
97
+ </span>
98
+ );
99
+ };
100
+
101
+ export default WordFadeIn;
@@ -16,6 +16,12 @@ export type { TooltipProps } from './Tooltip';
16
16
  export { Dialog, DialogFooter } from './Dialog';
17
17
  export type { DialogProps, DialogFooterProps } from './Dialog';
18
18
 
19
+ export { WordFadeIn } from './WordFadeIn';
20
+ export type { WordFadeInProps } from './WordFadeIn';
21
+
22
+ export { ThinkingBlock } from './ThinkingBlock';
23
+ export type { ThinkingBlockProps, ThinkingBlockType } from './ThinkingBlock';
24
+
19
25
 
20
26
 
21
27