@agentscope-ai/chat 1.2.0-beta.1789618496646 → 1.2.0-beta.1789634075831
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/components/AgentScopeRuntimeWebUI/core/AgentScopeRuntime/Response/Message.tsx +7 -3
- package/components/AgentScopeRuntimeWebUI/core/types/IChatAnywhere.ts +4 -4
- package/components/Markdown/Markdown.tsx +1 -1
- package/components/Markdown/core/animation/AnimatedText.tsx +181 -0
- package/components/Markdown/core/animation/timeline.ts +67 -0
- package/components/Markdown/core/hooks/useTyping.tsx +17 -22
- package/components/Markdown/core/index.tsx +76 -17
- package/components/Markdown/index.en-US.md +2 -2
- package/components/Markdown/index.tsx +2 -1
- package/components/Markdown/index.zh-CN.md +2 -2
- package/components/Markdown/styles.ts +7 -0
- package/components/Markdown/types.tsx +10 -8
- package/components/index.ts +1 -0
- package/lib/AgentScopeRuntimeWebUI/core/AgentScopeRuntime/Response/Message.js +16 -3
- package/lib/AgentScopeRuntimeWebUI/core/types/IChatAnywhere.d.ts +4 -4
- package/lib/Markdown/Markdown.js +1 -1
- package/lib/Markdown/core/animation/AnimatedText.d.ts +36 -0
- package/lib/Markdown/core/animation/AnimatedText.js +159 -0
- package/lib/Markdown/core/animation/timeline.d.ts +15 -0
- package/lib/Markdown/core/animation/timeline.js +56 -0
- package/lib/Markdown/core/hooks/useTyping.d.ts +4 -4
- package/lib/Markdown/core/hooks/useTyping.js +17 -15
- package/lib/Markdown/core/index.js +76 -9
- package/lib/Markdown/index.d.ts +1 -1
- package/lib/Markdown/styles.js +1 -1
- package/lib/Markdown/types.d.ts +8 -4
- package/lib/index.d.ts +1 -1
- package/package.json +3 -2
- package/components/Markdown/core/hooks/typingController.ts +0 -49
- package/lib/Markdown/core/hooks/typingController.d.ts +0 -5
- package/lib/Markdown/core/hooks/typingController.js +0 -40
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import Audios from '../../../../DefaultCards/Audios';
|
|
3
3
|
import Files from '../../../../DefaultCards/Files';
|
|
4
4
|
import Images from '../../../../DefaultCards/Images';
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
IAgentScopeRuntimeMessage,
|
|
12
12
|
} from '../types';
|
|
13
13
|
|
|
14
|
-
/**
|
|
14
|
+
/** Animate live text and let the last characters settle after normal completion. */
|
|
15
15
|
function StreamingText({
|
|
16
16
|
text,
|
|
17
17
|
status,
|
|
@@ -24,6 +24,10 @@ function StreamingText({
|
|
|
24
24
|
const animation = useChatAnywhereOptions((v) => v.response?.animation);
|
|
25
25
|
const animationConfig = useChatAnywhereOptions((v) => v.response?.animationConfig);
|
|
26
26
|
const streaming = status === AgentScopeRuntimeRunStatus.InProgress;
|
|
27
|
+
const [wasStreaming, setWasStreaming] = useState(streaming);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (streaming) setWasStreaming(true);
|
|
30
|
+
}, [streaming]);
|
|
27
31
|
const interrupted = [status, messageStatus].some(
|
|
28
32
|
(value) =>
|
|
29
33
|
value === AgentScopeRuntimeRunStatus.Canceled ||
|
|
@@ -33,7 +37,7 @@ function StreamingText({
|
|
|
33
37
|
return (
|
|
34
38
|
<Markdown
|
|
35
39
|
content={text}
|
|
36
|
-
animation={Boolean(animation && streaming && !interrupted)}
|
|
40
|
+
animation={Boolean(animation && (streaming || wasStreaming) && !interrupted)}
|
|
37
41
|
animationConfig={animationConfig}
|
|
38
42
|
cursor={!interrupted && streaming}
|
|
39
43
|
/>
|
|
@@ -917,13 +917,13 @@ export interface IAgentScopeRuntimeWebUIResponseSlot {
|
|
|
917
917
|
*/
|
|
918
918
|
export interface IAgentScopeRuntimeWebUIResponseOptions {
|
|
919
919
|
/**
|
|
920
|
-
* @description
|
|
921
|
-
* @descriptionEn
|
|
920
|
+
* @description 实时正文从左到右按字渐显,默认关闭。SSE 正常接收,CSS 独立播放;历史消息直接展示,正常结束等待动画收尾,中断立即补齐。自定义 render 需调用 fallback() 或复用 SDK Message 才生效。
|
|
921
|
+
* @descriptionEn Reveal incoming text from left to right with configurable speed, off by default. CSS animates independently of SSE reception; history displays immediately, completion drains animations, interruption flushes them. Custom renderers must use fallback() or the SDK Message component.
|
|
922
922
|
*/
|
|
923
923
|
animation?: boolean;
|
|
924
924
|
/**
|
|
925
|
-
* @description
|
|
926
|
-
* @descriptionEn Live text
|
|
925
|
+
* @description 正文动画配置,仅 animation 为 true 时生效。characterInterval 为逐字渐显间隔(毫秒,默认 5),值越大越慢;0 为新增文本同时淡入。
|
|
926
|
+
* @descriptionEn Live text animation options, effective only with animation: true. characterInterval sets the stagger in milliseconds (default 5); larger values are slower, zero fades incoming text simultaneously.
|
|
927
927
|
*/
|
|
928
928
|
animationConfig?: MarkdownProps['animationConfig'];
|
|
929
929
|
|
|
@@ -43,7 +43,7 @@ const isSupportsLookbehindAssertions = supportsLookbehindAssertions();
|
|
|
43
43
|
export default memo(function (props: MarkdownProps) {
|
|
44
44
|
const baseFontSize = props.baseFontSize || 14;
|
|
45
45
|
const baseLineHeight = props.baseLineHeight || 1.7;
|
|
46
|
-
const content = useTyping({ content: props.content, typing: props.
|
|
46
|
+
const content = useTyping({ content: props.content, typing: props.animation ? false : props.typing });
|
|
47
47
|
const prefixCls = useProviderContext().getPrefixCls('markdown');
|
|
48
48
|
const {
|
|
49
49
|
raw = false,
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { attributesToProps } from 'html-react-parser';
|
|
2
|
+
import React, {
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { createAnimationTimeline, resolveAnimation } from './timeline';
|
|
10
|
+
|
|
11
|
+
export const AnimationContext = createContext({
|
|
12
|
+
active: false,
|
|
13
|
+
config: resolveAnimation(),
|
|
14
|
+
timeline: null as ReturnType<typeof createAnimationTimeline> | null,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
type Chunk = {
|
|
18
|
+
id: number;
|
|
19
|
+
text: string;
|
|
20
|
+
plan?: ReturnType<ReturnType<typeof createAnimationTimeline>['reserve']>;
|
|
21
|
+
config?: ReturnType<typeof resolveAnimation>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const AnimatedText = React.memo(function AnimatedText({
|
|
25
|
+
text,
|
|
26
|
+
}: {
|
|
27
|
+
text: string;
|
|
28
|
+
}) {
|
|
29
|
+
const { active, config, timeline } = useContext(AnimationContext);
|
|
30
|
+
const [chunks, setChunks] = useState<Chunk[]>(() =>
|
|
31
|
+
active ? [] : [{ id: -1, text }],
|
|
32
|
+
);
|
|
33
|
+
const previous = useRef(active ? '' : text);
|
|
34
|
+
const resetOnSetup = useRef(false);
|
|
35
|
+
const nextId = useRef(0);
|
|
36
|
+
const pending = useRef(
|
|
37
|
+
new Map<
|
|
38
|
+
number,
|
|
39
|
+
{ timer: ReturnType<typeof setTimeout>; release: () => void }
|
|
40
|
+
>(),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const clear = () => {
|
|
45
|
+
pending.current.forEach(({ timer, release }) => {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
release();
|
|
48
|
+
});
|
|
49
|
+
pending.current.clear();
|
|
50
|
+
};
|
|
51
|
+
if (!active) {
|
|
52
|
+
clear();
|
|
53
|
+
previous.current = text;
|
|
54
|
+
setChunks([{ id: nextId.current++, text }]);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (previous.current === text && !resetOnSetup.current) return;
|
|
58
|
+
const append = !resetOnSetup.current && text.startsWith(previous.current);
|
|
59
|
+
resetOnSetup.current = false;
|
|
60
|
+
if (!append) clear();
|
|
61
|
+
const suffix = append ? text.slice(previous.current.length) : text;
|
|
62
|
+
previous.current = text;
|
|
63
|
+
const id = nextId.current++;
|
|
64
|
+
const plan = timeline?.reserve(suffix, performance.now(), config);
|
|
65
|
+
setChunks((old) => [
|
|
66
|
+
...(append ? old : []),
|
|
67
|
+
{ id, text: suffix, plan, config },
|
|
68
|
+
]);
|
|
69
|
+
if (plan) {
|
|
70
|
+
// One cleanup per incoming chunk, never a React update per character/frame.
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
plan.release();
|
|
73
|
+
pending.current.delete(id);
|
|
74
|
+
setChunks((old) => {
|
|
75
|
+
const settled = old.map((chunk) =>
|
|
76
|
+
chunk.id === id ? { id, text: chunk.text } : chunk,
|
|
77
|
+
);
|
|
78
|
+
const firstPending = settled.findIndex((chunk) => chunk.plan);
|
|
79
|
+
const count = firstPending < 0 ? settled.length : firstPending;
|
|
80
|
+
return count > 1
|
|
81
|
+
? [
|
|
82
|
+
{
|
|
83
|
+
id: settled[0].id,
|
|
84
|
+
text: settled
|
|
85
|
+
.slice(0, count)
|
|
86
|
+
.map((chunk) => chunk.text)
|
|
87
|
+
.join(''),
|
|
88
|
+
},
|
|
89
|
+
...settled.slice(count),
|
|
90
|
+
]
|
|
91
|
+
: settled;
|
|
92
|
+
});
|
|
93
|
+
}, plan.duration + 32);
|
|
94
|
+
pending.current.set(id, { timer, release: plan.release });
|
|
95
|
+
}
|
|
96
|
+
}, [text, active, config, timeline]);
|
|
97
|
+
|
|
98
|
+
useEffect(
|
|
99
|
+
() => () => {
|
|
100
|
+
pending.current.forEach(({ timer, release }) => {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
release();
|
|
103
|
+
});
|
|
104
|
+
pending.current.clear();
|
|
105
|
+
resetOnSetup.current = true;
|
|
106
|
+
},
|
|
107
|
+
[],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (!active) return <>{text}</>;
|
|
111
|
+
return (
|
|
112
|
+
<>
|
|
113
|
+
{chunks.map((chunk) => (
|
|
114
|
+
<React.Fragment key={chunk.id}>
|
|
115
|
+
{chunk.plan
|
|
116
|
+
? chunk.plan.units.map((unit, index) => (
|
|
117
|
+
<span
|
|
118
|
+
key={index}
|
|
119
|
+
className="spark-stream-character"
|
|
120
|
+
style={{
|
|
121
|
+
animation: `spark-stream-reveal ${
|
|
122
|
+
chunk.config!.fadeDuration
|
|
123
|
+
}ms ${chunk.config!.easing} ${
|
|
124
|
+
chunk.plan!.delays[index]
|
|
125
|
+
}ms both`,
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
{unit}
|
|
129
|
+
</span>
|
|
130
|
+
))
|
|
131
|
+
: chunk.text}
|
|
132
|
+
</React.Fragment>
|
|
133
|
+
))}
|
|
134
|
+
</>
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export default AnimatedText;
|
|
139
|
+
|
|
140
|
+
// Transform React text children, without touching the renderer's DOM or its
|
|
141
|
+
// sanitization. Code, math, media and user-provided components keep their renderer.
|
|
142
|
+
function animateChildren(children: React.ReactNode): React.ReactNode {
|
|
143
|
+
return React.Children.map(children, (child) => {
|
|
144
|
+
if (typeof child === 'string')
|
|
145
|
+
return /\S/.test(child) ? <AnimatedText text={child} /> : child;
|
|
146
|
+
if (!React.isValidElement<{ children?: React.ReactNode }>(child))
|
|
147
|
+
return child;
|
|
148
|
+
const props = child.props as {
|
|
149
|
+
children?: React.ReactNode;
|
|
150
|
+
className?: string;
|
|
151
|
+
};
|
|
152
|
+
if (
|
|
153
|
+
typeof child.type !== 'string' ||
|
|
154
|
+
['code', 'pre', 'svg', 'math'].includes(child.type) ||
|
|
155
|
+
/katex/.test(props.className || '')
|
|
156
|
+
)
|
|
157
|
+
return child;
|
|
158
|
+
return React.cloneElement(
|
|
159
|
+
child,
|
|
160
|
+
undefined,
|
|
161
|
+
animateChildren(child.props.children),
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const animatedComponents = Object.fromEntries(
|
|
167
|
+
['p', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'th', 'dt', 'dd'].map(
|
|
168
|
+
(tag) => [
|
|
169
|
+
tag,
|
|
170
|
+
function AnimatedBlock({ children, domNode }: any) {
|
|
171
|
+
// Match the HTML renderer's native attribute conversion, including style
|
|
172
|
+
// strings and boolean attributes when allowHtml is enabled.
|
|
173
|
+
return React.createElement(
|
|
174
|
+
tag,
|
|
175
|
+
attributesToProps(domNode?.attribs || {}),
|
|
176
|
+
animateChildren(children),
|
|
177
|
+
);
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
),
|
|
181
|
+
);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { MarkdownAnimationConfig } from '../../types';
|
|
2
|
+
|
|
3
|
+
const finite = (value: number | undefined, fallback: number, max: number) =>
|
|
4
|
+
Number.isFinite(value) ? Math.min(max, Math.max(0, value!)) : fallback;
|
|
5
|
+
|
|
6
|
+
export function resolveAnimation(config?: MarkdownAnimationConfig) {
|
|
7
|
+
const characterInterval = finite(config?.characterInterval, 5, 100);
|
|
8
|
+
return {
|
|
9
|
+
characterInterval,
|
|
10
|
+
fadeDuration: 160,
|
|
11
|
+
easing: 'ease-out',
|
|
12
|
+
// Slower speeds need more room to reveal a normal chunk, while retaining
|
|
13
|
+
// a hard backlog limit for bursts and restored/background content.
|
|
14
|
+
maxDelay: Math.min(2000, Math.max(240, characterInterval * 48)),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// One timeline per Markdown, so new paragraphs do not restart at delay zero.
|
|
19
|
+
// Cap both pending time and live spans. Large bursts coalesce into small groups.
|
|
20
|
+
export function createAnimationTimeline() {
|
|
21
|
+
let tail = 0;
|
|
22
|
+
let activeUnits = 0;
|
|
23
|
+
const Segmenter = (Intl as any).Segmenter;
|
|
24
|
+
const segmenter = Segmenter
|
|
25
|
+
? new Segmenter(undefined, { granularity: 'grapheme' })
|
|
26
|
+
: null;
|
|
27
|
+
return {
|
|
28
|
+
reserve(
|
|
29
|
+
text: string,
|
|
30
|
+
now: number,
|
|
31
|
+
config: ReturnType<typeof resolveAnimation>,
|
|
32
|
+
) {
|
|
33
|
+
if (!text || activeUnits >= 384) return undefined;
|
|
34
|
+
const glyphs: string[] = segmenter
|
|
35
|
+
? Array.from(segmenter.segment(text), (item: any) => item.segment)
|
|
36
|
+
: Array.from(text);
|
|
37
|
+
const limit = Math.max(1, Math.min(192, 384 - activeUnits));
|
|
38
|
+
const size = Math.max(1, Math.ceil(glyphs.length / limit));
|
|
39
|
+
const units: string[] = [];
|
|
40
|
+
for (let i = 0; i < glyphs.length; i += size)
|
|
41
|
+
units.push(glyphs.slice(i, i + size).join(''));
|
|
42
|
+
const start = Math.min(Math.max(now, tail), now + config.maxDelay);
|
|
43
|
+
const step =
|
|
44
|
+
units.length > 1
|
|
45
|
+
? Math.min(
|
|
46
|
+
config.characterInterval,
|
|
47
|
+
(now + config.maxDelay - start) / (units.length - 1),
|
|
48
|
+
)
|
|
49
|
+
: 0;
|
|
50
|
+
const delays = units.map((_, index) => start - now + index * step);
|
|
51
|
+
tail = start + units.length * step;
|
|
52
|
+
activeUnits += units.length;
|
|
53
|
+
let released = false;
|
|
54
|
+
return {
|
|
55
|
+
units,
|
|
56
|
+
delays,
|
|
57
|
+
duration: (delays.at(-1) || 0) + config.fadeDuration,
|
|
58
|
+
release() {
|
|
59
|
+
if (released) return;
|
|
60
|
+
released = true;
|
|
61
|
+
activeUnits -= units.length;
|
|
62
|
+
if (!activeUnits) tail = 0;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -1,30 +1,25 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { createTypingController } from './typingController';
|
|
3
2
|
|
|
4
|
-
const useTyping = ({
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}: {
|
|
8
|
-
content?: string;
|
|
9
|
-
typing?: boolean | number;
|
|
10
|
-
}) => {
|
|
11
|
-
const [visible, setVisible] = useState('');
|
|
12
|
-
const controller = useRef<ReturnType<typeof createTypingController>>();
|
|
13
|
-
if (!controller.current)
|
|
14
|
-
controller.current = createTypingController(setVisible);
|
|
3
|
+
const useTyping = ({ content, typing }) => {
|
|
4
|
+
const [index, setIndex] = useState(0);
|
|
5
|
+
const timer = useRef<NodeJS.Timeout>();
|
|
15
6
|
|
|
16
7
|
useEffect(() => {
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
if (typing) {
|
|
9
|
+
timer.current = setInterval(() => {
|
|
10
|
+
setIndex((v) => v + 1);
|
|
11
|
+
}, typeof typing === 'number' ? typing : 5);
|
|
12
|
+
} else {
|
|
13
|
+
timer.current && clearInterval(timer.current);
|
|
14
|
+
}
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
return () => clearInterval(timer.current);
|
|
17
|
+
}, [typing]);
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!enabled) return content;
|
|
26
|
-
// Never render stale text for a replacement while its effect is pending.
|
|
27
|
-
return (content || '').startsWith(visible) ? visible : '';
|
|
19
|
+
if (!typing) return content;
|
|
20
|
+
|
|
21
|
+
return content.slice(0, index);
|
|
28
22
|
};
|
|
29
23
|
|
|
30
|
-
|
|
24
|
+
|
|
25
|
+
export default useTyping;
|
|
@@ -1,37 +1,96 @@
|
|
|
1
1
|
import XMarkdown from '@ant-design/x-markdown';
|
|
2
|
-
import {
|
|
2
|
+
import { memo, useEffect, useMemo, useState } from 'react';
|
|
3
3
|
import Styles from '../styles';
|
|
4
|
+
import { InnerMarkdownXProps } from '../types';
|
|
5
|
+
import { AnimationContext, animatedComponents } from './animation/AnimatedText';
|
|
6
|
+
import {
|
|
7
|
+
createAnimationTimeline,
|
|
8
|
+
resolveAnimation,
|
|
9
|
+
} from './animation/timeline';
|
|
4
10
|
import useCursorContent from './hooks/useCursorContent';
|
|
5
|
-
import { memo, useMemo } from 'react';
|
|
6
11
|
|
|
12
|
+
// Global styles depend on theme context, not on each typed text frame.
|
|
13
|
+
const MarkdownStyles = memo(Styles);
|
|
7
14
|
|
|
8
15
|
export default memo(function InnerMarkdownX(props: InnerMarkdownXProps) {
|
|
9
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
content: originalContent,
|
|
18
|
+
cursor,
|
|
19
|
+
animation,
|
|
20
|
+
animationConfig,
|
|
21
|
+
components,
|
|
22
|
+
...rest
|
|
23
|
+
} = props;
|
|
24
|
+
const config = useMemo(
|
|
25
|
+
() => resolveAnimation(animationConfig),
|
|
26
|
+
[animationConfig?.characterInterval],
|
|
27
|
+
);
|
|
28
|
+
const timeline = useMemo(() => createAnimationTimeline(), []);
|
|
29
|
+
const [reducedMotion, setReducedMotion] = useState(
|
|
30
|
+
() =>
|
|
31
|
+
typeof window !== 'undefined' &&
|
|
32
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
|
33
|
+
);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const query = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
36
|
+
const update = () => setReducedMotion(query.matches);
|
|
37
|
+
query.addEventListener('change', update);
|
|
38
|
+
return () => query.removeEventListener('change', update);
|
|
39
|
+
}, []);
|
|
40
|
+
const live = Boolean(animation && cursor && !reducedMotion);
|
|
41
|
+
const [draining, setDraining] = useState(live);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (live) {
|
|
44
|
+
setDraining(true);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Keep final characters in the same tree until their animations settle.
|
|
48
|
+
const timer = setTimeout(
|
|
49
|
+
() => setDraining(false),
|
|
50
|
+
config.maxDelay + config.fadeDuration + 64,
|
|
51
|
+
);
|
|
52
|
+
return () => clearTimeout(timer);
|
|
53
|
+
}, [live, config.maxDelay, config.fadeDuration]);
|
|
54
|
+
const active = Boolean(animation && !reducedMotion && (live || draining));
|
|
55
|
+
const animationContext = useMemo(
|
|
56
|
+
() => ({ active, config, timeline }),
|
|
57
|
+
[active, config, timeline],
|
|
58
|
+
);
|
|
59
|
+
const resolvedComponents = useMemo(
|
|
60
|
+
() => ({ ...animatedComponents, ...components }),
|
|
61
|
+
[components],
|
|
62
|
+
);
|
|
10
63
|
const content = useCursorContent({
|
|
11
64
|
cursor: cursor,
|
|
12
65
|
content: originalContent,
|
|
13
|
-
animation: animation
|
|
66
|
+
animation: animation,
|
|
14
67
|
});
|
|
15
68
|
|
|
16
69
|
const streaming = useMemo(() => {
|
|
17
70
|
if (!animation) return undefined;
|
|
18
71
|
return {
|
|
19
72
|
hasNextChunk: animation && cursor,
|
|
20
|
-
enableAnimation:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}, [cursor, animation, animationConfig]);
|
|
24
|
-
|
|
73
|
+
enableAnimation: false,
|
|
74
|
+
};
|
|
75
|
+
}, [cursor, animation]);
|
|
25
76
|
|
|
26
|
-
|
|
27
|
-
<Styles />
|
|
77
|
+
const markdown = (
|
|
28
78
|
<XMarkdown
|
|
29
79
|
{...rest}
|
|
80
|
+
components={animation ? resolvedComponents : components}
|
|
30
81
|
content={content}
|
|
31
|
-
streaming={
|
|
32
|
-
hasNextChunk: boolean;
|
|
33
|
-
|
|
34
|
-
}}
|
|
82
|
+
streaming={
|
|
83
|
+
streaming as { hasNextChunk: boolean; enableAnimation: boolean }
|
|
84
|
+
}
|
|
35
85
|
/>
|
|
36
|
-
|
|
37
|
-
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<>
|
|
90
|
+
<MarkdownStyles />
|
|
91
|
+
<AnimationContext.Provider value={animationContext}>
|
|
92
|
+
{markdown}
|
|
93
|
+
</AnimationContext.Provider>
|
|
94
|
+
</>
|
|
95
|
+
);
|
|
96
|
+
});
|
|
@@ -32,7 +32,7 @@ import { Markdown } from '@agentscope-ai/chat';
|
|
|
32
32
|
content={content}
|
|
33
33
|
cursor={isStreaming}
|
|
34
34
|
animation
|
|
35
|
-
animationConfig={{
|
|
35
|
+
animationConfig={{ characterInterval: 12 }}
|
|
36
36
|
/>
|
|
37
37
|
```
|
|
38
|
-
`
|
|
38
|
+
`animationConfig.characterInterval` controls the stagger in milliseconds (default `5`). Larger values are slower: try `12` or `20`; `0` fades incoming text simultaneously. Large bursts accelerate to bound backlog; actual paints follow screen refresh rate. Container height remains natural with no height animation. Reduced motion is respected. ChatAnywhere Text cards accept the same configuration.
|
|
@@ -33,7 +33,7 @@ import { Markdown } from '@agentscope-ai/chat';
|
|
|
33
33
|
content={content}
|
|
34
34
|
cursor={isStreaming}
|
|
35
35
|
animation
|
|
36
|
-
animationConfig={{
|
|
36
|
+
animationConfig={{ characterInterval: 12 }}
|
|
37
37
|
/>
|
|
38
38
|
```
|
|
39
|
-
`
|
|
39
|
+
`animationConfig.characterInterval` 控制相邻文字开始渐显的间隔(毫秒,默认 `5`)。值越大越慢,可设为 `12` 或 `20`;`0` 同时淡入新增内容。大段突发内容会自动加快以限制积压,实际画面按屏幕刷新率绘制。容器使用自然高度,不播放高度动画;尊重减少动态效果设置。ChatAnywhere Text 卡片可传入相同配置。
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { createGlobalStyle } from 'antd-style';
|
|
2
2
|
|
|
3
3
|
export default createGlobalStyle`
|
|
4
|
+
@keyframes spark-stream-reveal {
|
|
5
|
+
from { opacity: 0; }
|
|
6
|
+
to { opacity: 1; }
|
|
7
|
+
}
|
|
8
|
+
@media (prefers-reduced-motion: reduce) {
|
|
9
|
+
.spark-stream-character { animation: none !important; opacity: 1 !important; }
|
|
10
|
+
}
|
|
4
11
|
.${(p) => p.theme.prefixCls}-markdown {
|
|
5
12
|
color: inherit;
|
|
6
13
|
max-width: 100%;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { XMarkdownProps } from '@ant-design/x-markdown';
|
|
2
2
|
|
|
3
|
+
export interface MarkdownAnimationConfig {
|
|
4
|
+
/** Character reveal interval in ms. Default 5; larger values reveal more slowly. Zero fades each incoming chunk together. */
|
|
5
|
+
characterInterval?: number;
|
|
6
|
+
}
|
|
3
7
|
|
|
4
8
|
export interface MarkdownProps {
|
|
5
9
|
/**
|
|
@@ -32,7 +36,7 @@ export interface MarkdownProps {
|
|
|
32
36
|
/**
|
|
33
37
|
* @description 是否禁用图片渲染
|
|
34
38
|
* @descriptionEn Whether to disable image rendering
|
|
35
|
-
|
|
39
|
+
*/
|
|
36
40
|
disableImage?: boolean;
|
|
37
41
|
|
|
38
42
|
/**
|
|
@@ -47,7 +51,6 @@ export interface MarkdownProps {
|
|
|
47
51
|
*/
|
|
48
52
|
typing?: boolean | number;
|
|
49
53
|
|
|
50
|
-
|
|
51
54
|
/**
|
|
52
55
|
* @description 组件的CSS类名
|
|
53
56
|
* @descriptionEn CSS class name for the component
|
|
@@ -55,11 +58,11 @@ export interface MarkdownProps {
|
|
|
55
58
|
className?: string;
|
|
56
59
|
animation?: boolean;
|
|
57
60
|
/**
|
|
58
|
-
* @description
|
|
59
|
-
* @descriptionEn
|
|
61
|
+
* @description 逐字渐显速度配置,仅 animation 开启时生效。
|
|
62
|
+
* @descriptionEn Character reveal speed, effective only with animation enabled.
|
|
60
63
|
*/
|
|
61
|
-
animationConfig?:
|
|
62
|
-
|
|
64
|
+
animationConfig?: MarkdownAnimationConfig;
|
|
65
|
+
|
|
63
66
|
components?: XMarkdownProps['components'];
|
|
64
67
|
citations?: {
|
|
65
68
|
title?: string;
|
|
@@ -88,7 +91,6 @@ export interface MarkdownProps {
|
|
|
88
91
|
>;
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
|
|
92
94
|
export interface InnerMarkdownXProps extends XMarkdownProps {
|
|
93
95
|
/**
|
|
94
96
|
* @description 光标样式类型,支持点状、下划线或布尔值控制
|
|
@@ -97,4 +99,4 @@ export interface InnerMarkdownXProps extends XMarkdownProps {
|
|
|
97
99
|
cursor?: MarkdownProps['cursor'];
|
|
98
100
|
animation?: MarkdownProps['animation'];
|
|
99
101
|
animationConfig?: MarkdownProps['animationConfig'];
|
|
100
|
-
}
|
|
102
|
+
}
|
package/components/index.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
|
|
2
|
+
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
3
|
+
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
4
|
+
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
|
|
5
|
+
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
6
|
+
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
|
7
|
+
import React, { useEffect, useState } from 'react';
|
|
2
8
|
import Audios from "../../../../DefaultCards/Audios";
|
|
3
9
|
import Files from "../../../../DefaultCards/Files";
|
|
4
10
|
import Images from "../../../../DefaultCards/Images";
|
|
@@ -7,7 +13,7 @@ import Markdown from "../../../../Markdown";
|
|
|
7
13
|
import { useChatAnywhereOptions } from "../../Context/ChatAnywhereOptionsContext";
|
|
8
14
|
import { AgentScopeRuntimeContentType, AgentScopeRuntimeRunStatus } from "../types";
|
|
9
15
|
|
|
10
|
-
/**
|
|
16
|
+
/** Animate live text and let the last characters settle after normal completion. */
|
|
11
17
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
12
18
|
import { Fragment as _Fragment } from "react/jsx-runtime";
|
|
13
19
|
function StreamingText(_ref) {
|
|
@@ -23,12 +29,19 @@ function StreamingText(_ref) {
|
|
|
23
29
|
return (_v$response2 = v.response) === null || _v$response2 === void 0 ? void 0 : _v$response2.animationConfig;
|
|
24
30
|
});
|
|
25
31
|
var streaming = status === AgentScopeRuntimeRunStatus.InProgress;
|
|
32
|
+
var _useState = useState(streaming),
|
|
33
|
+
_useState2 = _slicedToArray(_useState, 2),
|
|
34
|
+
wasStreaming = _useState2[0],
|
|
35
|
+
setWasStreaming = _useState2[1];
|
|
36
|
+
useEffect(function () {
|
|
37
|
+
if (streaming) setWasStreaming(true);
|
|
38
|
+
}, [streaming]);
|
|
26
39
|
var interrupted = [status, messageStatus].some(function (value) {
|
|
27
40
|
return value === AgentScopeRuntimeRunStatus.Canceled || value === AgentScopeRuntimeRunStatus.Failed || value === AgentScopeRuntimeRunStatus.Rejected;
|
|
28
41
|
});
|
|
29
42
|
return /*#__PURE__*/_jsx(Markdown, {
|
|
30
43
|
content: text,
|
|
31
|
-
animation: Boolean(animation && streaming && !interrupted),
|
|
44
|
+
animation: Boolean(animation && (streaming || wasStreaming) && !interrupted),
|
|
32
45
|
animationConfig: animationConfig,
|
|
33
46
|
cursor: !interrupted && streaming
|
|
34
47
|
});
|
|
@@ -826,13 +826,13 @@ export interface IAgentScopeRuntimeWebUIResponseSlot {
|
|
|
826
826
|
*/
|
|
827
827
|
export interface IAgentScopeRuntimeWebUIResponseOptions {
|
|
828
828
|
/**
|
|
829
|
-
* @description
|
|
830
|
-
* @descriptionEn
|
|
829
|
+
* @description 实时正文从左到右按字渐显,默认关闭。SSE 正常接收,CSS 独立播放;历史消息直接展示,正常结束等待动画收尾,中断立即补齐。自定义 render 需调用 fallback() 或复用 SDK Message 才生效。
|
|
830
|
+
* @descriptionEn Reveal incoming text from left to right with configurable speed, off by default. CSS animates independently of SSE reception; history displays immediately, completion drains animations, interruption flushes them. Custom renderers must use fallback() or the SDK Message component.
|
|
831
831
|
*/
|
|
832
832
|
animation?: boolean;
|
|
833
833
|
/**
|
|
834
|
-
* @description
|
|
835
|
-
* @descriptionEn Live text
|
|
834
|
+
* @description 正文动画配置,仅 animation 为 true 时生效。characterInterval 为逐字渐显间隔(毫秒,默认 5),值越大越慢;0 为新增文本同时淡入。
|
|
835
|
+
* @descriptionEn Live text animation options, effective only with animation: true. characterInterval sets the stagger in milliseconds (default 5); larger values are slower, zero fades incoming text simultaneously.
|
|
836
836
|
*/
|
|
837
837
|
animationConfig?: MarkdownProps['animationConfig'];
|
|
838
838
|
/**
|
package/lib/Markdown/Markdown.js
CHANGED
|
@@ -53,7 +53,7 @@ export default /*#__PURE__*/memo(function (props) {
|
|
|
53
53
|
var baseLineHeight = props.baseLineHeight || 1.7;
|
|
54
54
|
var content = useTyping({
|
|
55
55
|
content: props.content,
|
|
56
|
-
typing: props.
|
|
56
|
+
typing: props.animation ? false : props.typing
|
|
57
57
|
});
|
|
58
58
|
var prefixCls = useProviderContext().getPrefixCls('markdown');
|
|
59
59
|
var _props$raw = props.raw,
|