@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.
Files changed (31) hide show
  1. package/components/AgentScopeRuntimeWebUI/core/AgentScopeRuntime/Response/Message.tsx +7 -3
  2. package/components/AgentScopeRuntimeWebUI/core/types/IChatAnywhere.ts +4 -4
  3. package/components/Markdown/Markdown.tsx +1 -1
  4. package/components/Markdown/core/animation/AnimatedText.tsx +181 -0
  5. package/components/Markdown/core/animation/timeline.ts +67 -0
  6. package/components/Markdown/core/hooks/useTyping.tsx +17 -22
  7. package/components/Markdown/core/index.tsx +76 -17
  8. package/components/Markdown/index.en-US.md +2 -2
  9. package/components/Markdown/index.tsx +2 -1
  10. package/components/Markdown/index.zh-CN.md +2 -2
  11. package/components/Markdown/styles.ts +7 -0
  12. package/components/Markdown/types.tsx +10 -8
  13. package/components/index.ts +1 -0
  14. package/lib/AgentScopeRuntimeWebUI/core/AgentScopeRuntime/Response/Message.js +16 -3
  15. package/lib/AgentScopeRuntimeWebUI/core/types/IChatAnywhere.d.ts +4 -4
  16. package/lib/Markdown/Markdown.js +1 -1
  17. package/lib/Markdown/core/animation/AnimatedText.d.ts +36 -0
  18. package/lib/Markdown/core/animation/AnimatedText.js +159 -0
  19. package/lib/Markdown/core/animation/timeline.d.ts +15 -0
  20. package/lib/Markdown/core/animation/timeline.js +56 -0
  21. package/lib/Markdown/core/hooks/useTyping.d.ts +4 -4
  22. package/lib/Markdown/core/hooks/useTyping.js +17 -15
  23. package/lib/Markdown/core/index.js +76 -9
  24. package/lib/Markdown/index.d.ts +1 -1
  25. package/lib/Markdown/styles.js +1 -1
  26. package/lib/Markdown/types.d.ts +8 -4
  27. package/lib/index.d.ts +1 -1
  28. package/package.json +3 -2
  29. package/components/Markdown/core/hooks/typingController.ts +0 -49
  30. package/lib/Markdown/core/hooks/typingController.d.ts +0 -5
  31. 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
- /** Fade incoming live text without delaying it in a character queue. */
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 实时正文按新增文本块渐显,默认关闭。复用 Markdown 的 animation,不逐字排队。历史消息及终态不播放动画。自定义 render 需调用 fallback() 或复用 SDK Message 才生效。
921
- * @descriptionEn Fade in incoming live text chunks, off by default. Reuses Markdown animation without a character queue. History and terminal states do not animate. Custom renderers must use fallback() or the SDK Message component.
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 正文淡入动画配置,仅 animation 为 true 时生效。fadeDuration 为毫秒(默认 200),easing 为 CSS 缓动函数(默认 ease-in-out)。不控制逐字输出速度。
926
- * @descriptionEn Live text fade-in options, effective only when animation is true. fadeDuration is in milliseconds (default 200); easing is a CSS easing function (default ease-in-out). Does not control typing speed.
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.typing && !props.animation });
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
- content,
6
- typing,
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
- controller.current!.update(content || '', typing);
18
- }, [content, typing]);
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
- useEffect(() => () => controller.current!.stop(), []);
16
+ return () => clearInterval(timer.current);
17
+ }, [typing]);
21
18
 
22
- const enabled =
23
- typing === true ||
24
- (typeof typing === 'number' && Number.isFinite(typing) && typing > 0);
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
- export default useTyping;
24
+
25
+ export default useTyping;
@@ -1,37 +1,96 @@
1
1
  import XMarkdown from '@ant-design/x-markdown';
2
- import { InnerMarkdownXProps } from '../types';
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 { content: originalContent, cursor, animation, animationConfig, ...rest } = props;
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: animation && cursor,
21
- animationConfig,
22
- }
23
- }, [cursor, animation, animationConfig]);
24
-
73
+ enableAnimation: false,
74
+ };
75
+ }, [cursor, animation]);
25
76
 
26
- return <>
27
- <Styles />
77
+ const markdown = (
28
78
  <XMarkdown
29
79
  {...rest}
80
+ components={animation ? resolvedComponents : components}
30
81
  content={content}
31
- streaming={streaming as {
32
- hasNextChunk: boolean;
33
- enableAnimation: boolean;
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={{ fadeDuration: 120, easing: 'ease-out' }}
35
+ animationConfig={{ characterInterval: 12 }}
36
36
  />
37
37
  ```
38
- `fadeDuration` is in milliseconds (default `200`); `easing` defaults to `ease-in-out`. Effective only with animation enabled during streaming; this does not control typing speed. ChatAnywhere Text cards accept the same options in `data.animationConfig`.
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.
@@ -3,4 +3,5 @@ export { default } from './Markdown';
3
3
 
4
4
  export type {
5
5
  MarkdownProps,
6
- } from './types';
6
+ MarkdownAnimationConfig,
7
+ } from './types';
@@ -33,7 +33,7 @@ import { Markdown } from '@agentscope-ai/chat';
33
33
  content={content}
34
34
  cursor={isStreaming}
35
35
  animation
36
- animationConfig={{ fadeDuration: 120, easing: 'ease-out' }}
36
+ animationConfig={{ characterInterval: 12 }}
37
37
  />
38
38
  ```
39
- `fadeDuration` 单位为毫秒,默认 `200`;`easing` 默认 `ease-in-out`。仅启用 `animation` 且正在流式输出时生效,不控制逐字输出速度。ChatAnywhere 的 Text 卡片可在 `data.animationConfig` 中传入相同配置。
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 淡入动画配置,仅 animation 开启时生效;fadeDuration 单位为毫秒(默认 200),easing 默认 ease-in-out。
59
- * @descriptionEn Fade-in options, effective only when animation is enabled; fadeDuration is in milliseconds (default 200), easing defaults to ease-in-out.
61
+ * @description 逐字渐显速度配置,仅 animation 开启时生效。
62
+ * @descriptionEn Character reveal speed, effective only with animation enabled.
60
63
  */
61
- animationConfig?: NonNullable<XMarkdownProps['streaming']>['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
+ }
@@ -95,6 +95,7 @@ export {
95
95
  default as Markdown,
96
96
  type MarkdownProps as IMarkdownProps,
97
97
  type MarkdownProps,
98
+ type MarkdownAnimationConfig,
98
99
  } from './Markdown';
99
100
 
100
101
  export { default as AIGC } from './AIGC';
@@ -1,4 +1,10 @@
1
- import React from 'react';
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
- /** Fade incoming live text without delaying it in a character queue. */
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 实时正文按新增文本块渐显,默认关闭。复用 Markdown 的 animation,不逐字排队。历史消息及终态不播放动画。自定义 render 需调用 fallback() 或复用 SDK Message 才生效。
830
- * @descriptionEn Fade in incoming live text chunks, off by default. Reuses Markdown animation without a character queue. History and terminal states do not animate. Custom renderers must use fallback() or the SDK Message component.
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 正文淡入动画配置,仅 animation 为 true 时生效。fadeDuration 为毫秒(默认 200),easing 为 CSS 缓动函数(默认 ease-in-out)。不控制逐字输出速度。
835
- * @descriptionEn Live text fade-in options, effective only when animation is true. fadeDuration is in milliseconds (default 200); easing is a CSS easing function (default ease-in-out). Does not control typing speed.
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
  /**
@@ -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.typing && !props.animation
56
+ typing: props.animation ? false : props.typing
57
57
  });
58
58
  var prefixCls = useProviderContext().getPrefixCls('markdown');
59
59
  var _props$raw = props.raw,