@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.
- package/dist/index.css +632 -1
- package/dist/index.d.mts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +3829 -3465
- package/dist/index.mjs +3760 -3398
- package/package.json +1 -1
- package/src/AIChatPanel.css +365 -0
- package/src/AIChatPanel.tsx +251 -88
- package/src/ChatPanel.css +379 -3
- package/src/ChatPanel.tsx +264 -190
- package/src/components/ui/ThinkingBlock.tsx +150 -0
- package/src/components/ui/WordFadeIn.tsx +101 -0
- package/src/components/ui/index.ts +6 -0
|
@@ -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
|
|