@ray-js/t-agent-ui-ray 0.2.0-beta.19 → 0.2.0-beta.20

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.
@@ -1,11 +1,12 @@
1
1
  import './index.less';
2
2
  import React, { PropsWithChildren } from 'react';
3
3
  import { ChatAgent } from '@ray-js/t-agent';
4
+ import { UIChatAgent } from '../contexts';
4
5
  import { RenderOptions } from '../types';
5
6
  export default function ChatContainer<T extends ChatAgent<any>>(props: PropsWithChildren<{
6
7
  createAgent: () => T;
7
8
  renderOptions?: RenderOptions;
8
9
  className?: string;
9
10
  style?: React.CSSProperties;
10
- agentRef?: React.MutableRefObject<T>;
11
+ agentRef?: React.MutableRefObject<UIChatAgent | null>;
11
12
  }>): React.JSX.Element;
@@ -4,10 +4,10 @@ import "core-js/modules/esnext.iterator.filter.js";
4
4
  import "core-js/modules/esnext.iterator.map.js";
5
5
  import "core-js/modules/web.dom-collections.iterator.js";
6
6
  import './index.less';
7
- import React, { useEffect, useMemo, useState } from 'react';
7
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
8
8
  import { View } from '@ray-js/components';
9
9
  import cx from 'clsx';
10
- import { ChatAgentContext, MessageContext, RenderContext } from '../contexts';
10
+ import { SharedProvider } from '../contexts';
11
11
  import { defaultRenderOptions } from '../renderOption';
12
12
  import logger from '../logger';
13
13
  import { systemInfo } from '../utils';
@@ -27,6 +27,8 @@ export default function ChatContainer(props) {
27
27
  style
28
28
  } = props;
29
29
  const [messages, setMessages] = useState([]);
30
+ const [tickValue, setRenderTick] = useState(0);
31
+ const notifyHeightChanged = useCallback(() => setRenderTick(t => t + 1), []);
30
32
  const [agent] = useState(() => {
31
33
  const agent = createAgent();
32
34
  if (!agent.plugins.ui) {
@@ -41,9 +43,6 @@ export default function ChatContainer(props) {
41
43
  });
42
44
  return agent;
43
45
  });
44
- if (props.agentRef) {
45
- props.agentRef.current = agent;
46
- }
47
46
  const uiAgent = useMemo(() => {
48
47
  const session = {
49
48
  get: agent.session.get,
@@ -58,63 +57,72 @@ export default function ChatContainer(props) {
58
57
  });
59
58
  return {
60
59
  session,
61
- plugins: agent.plugins,
60
+ plugins: {
61
+ ui: agent.plugins.ui
62
+ },
62
63
  pushInputBlocks: agent.pushInputBlocks,
63
64
  emitTileEvent: agent.emitTileEvent,
64
65
  removeMessage: agent.removeMessage
65
66
  };
66
67
  }, [agent]);
68
+ if (props.agentRef) {
69
+ props.agentRef.current = uiAgent;
70
+ }
67
71
  useEffect(() => {
68
72
  const {
69
73
  onEvent,
70
74
  emitEvent
71
75
  } = agent.plugins.ui;
72
- const offMessageListInit = onEvent('messageListInit', _ref => {
76
+ const offMessageListInit = onEvent('messageListInit', async _ref => {
73
77
  let {
74
78
  messages
75
79
  } = _ref;
76
80
  setMessages(messages);
81
+ notifyHeightChanged();
77
82
  emitEvent('scrollToBottom', {
78
83
  animation: false
79
84
  });
80
85
  });
81
- const offMessageChange = onEvent('messageChange', _ref2 => {
86
+ const offMessageChange = onEvent('messageChange', async _ref2 => {
82
87
  let {
83
88
  type,
84
89
  message
85
90
  } = _ref2;
86
91
  switch (type) {
87
92
  case 'show':
88
- setMessages(prev => [...prev, message]);
89
- emitEvent('scrollToBottom', {
90
- animation: false
91
- });
92
- break;
93
+ {
94
+ setMessages(prev => [...prev, message]);
95
+ notifyHeightChanged();
96
+ emitEvent('scrollToBottom', {
97
+ animation: false
98
+ });
99
+ break;
100
+ }
93
101
  case 'update':
94
- setMessages(prev => {
95
- if (systemInfo.platform === 'android') {
96
- var _prev;
97
- // Android 上有兼容问题,需要手动滚动到底部
98
- if (((_prev = prev[prev.length - 1]) === null || _prev === void 0 ? void 0 : _prev.id) === message.id) {
99
- // 是最后一条消息,滚动到最底部
100
- emitEvent('scrollToBottom', {
101
- animation: false,
102
- follow: true
103
- });
104
- }
105
- }
106
- return prev.map(item => {
107
- if (item.id === message.id) {
108
- return message;
109
- }
110
- return item;
102
+ {
103
+ setMessages(prev => {
104
+ return prev.map(item => {
105
+ if (item.id === message.id) {
106
+ notifyHeightChanged();
107
+ return message;
108
+ }
109
+ return item;
110
+ });
111
111
  });
112
- });
113
- break;
112
+ break;
113
+ }
114
114
  case 'remove':
115
- setMessages(prev => {
116
- return prev.filter(item => item.id !== message.id);
117
- });
115
+ {
116
+ setMessages(prev => {
117
+ return prev.filter(item => item.id !== message.id);
118
+ });
119
+ notifyHeightChanged();
120
+ emitEvent('scrollToBottom', {
121
+ animation: false,
122
+ follow: true
123
+ });
124
+ break;
125
+ }
118
126
  }
119
127
  });
120
128
  return () => {
@@ -122,12 +130,6 @@ export default function ChatContainer(props) {
122
130
  offMessageChange();
123
131
  };
124
132
  }, [agent]);
125
- const messageValue = useMemo(() => {
126
- return {
127
- messages,
128
- keyboardHeight
129
- };
130
- }, [messages, keyboardHeight]);
131
133
  useEffect(() => {
132
134
  logger.debug('ChatProvider agent.start');
133
135
  agent.start().then(() => {
@@ -181,11 +183,14 @@ export default function ChatContainer(props) {
181
183
  className: cx('t-agent-chat-container', className, {
182
184
  't-agent-chat-container-keyboard-show': keyboardHeight > 0
183
185
  })
184
- }, /*#__PURE__*/React.createElement(ChatAgentContext.Provider, {
185
- value: uiAgent
186
- }, /*#__PURE__*/React.createElement(RenderContext.Provider, {
187
- value: renderOptions
188
- }, /*#__PURE__*/React.createElement(MessageContext.Provider, {
189
- value: messageValue
190
- }, children))));
186
+ }, /*#__PURE__*/React.createElement(SharedProvider, {
187
+ value: {
188
+ agent: uiAgent,
189
+ messages,
190
+ notifyHeightChanged,
191
+ keyboardHeight,
192
+ tickValue,
193
+ renderOptions
194
+ }
195
+ }, children));
191
196
  }
@@ -1,6 +1,5 @@
1
1
  import Render from './index.rjs';
2
2
  import logger from '../logger';
3
- import { systemInfo } from '../utils';
4
3
 
5
4
  // eslint-disable-next-line no-undef
6
5
  Component({
@@ -47,9 +46,12 @@ Component({
47
46
  });
48
47
  const data = JSON.parse(content);
49
48
  if (data.option) {
50
- // eslint-disable-next-line no-undef
51
- const pixelRatio = systemInfo.pixelRatio;
52
- this.rjs.update(this.data.canvasId, pixelRatio, data.option);
49
+ if (!this.isInit) {
50
+ this.rjs.init(this.data.canvasId, data.option);
51
+ this.isInit = true;
52
+ } else {
53
+ this.rjs.update(data.option);
54
+ }
53
55
  this.setData({
54
56
  loading: false,
55
57
  error: false
@@ -1,15 +1,41 @@
1
1
  export default Render({
2
2
  myChart: null,
3
- async update(canvasId, devicePixelRatio, option) {
4
- if (!this.myChart) {
3
+ option: null,
4
+ async init(canvasId, option) {
5
+ const { pixelRatio } = this.instance.getSystemInfo()
6
+ this.option = option
7
+ try {
5
8
  const canvas = await this.instance.getCanvasById(canvasId)
6
- const echarts = await requirePlugin('rjs://echarts')
7
-
8
- this.myChart = echarts.init(canvas, undefined, {
9
- devicePixelRatio,
10
- })
9
+ if (canvas) {
10
+ const echarts = await requirePlugin('rjs://echarts')
11
+ const myChart = echarts.init(canvas, undefined, {
12
+ devicePixelRatio: pixelRatio,
13
+ })
14
+ myChart.setOption(this.option, true)
15
+ this.myChart = myChart;
16
+ }
17
+ } catch (err) {
18
+ // ignore
11
19
  }
12
- // 使用刚指定的配置项和数据显示图表。
13
- this.myChart.setOption(option, true)
20
+ },
21
+ async update(option) {
22
+ this.option = option
23
+ if (this.myChart) {
24
+ this.myChart.setOption(this.option, true)
25
+ }
26
+ // const { pixelRatio } = this.instance.getSystemInfo()
27
+ // if (!this.myChart) {
28
+ // const canvas = await this.instance.getCanvasById(canvasId)
29
+ // console.log(`EchartsBlockRender init render, ${canvasId}`, Object.keys(canvas))
30
+ // if (canvas) {
31
+ // const echarts = await requirePlugin('rjs://echarts')
32
+ // this.myChart = echarts.init(canvas, undefined, {
33
+ // devicePixelRatio: pixelRatio,
34
+ // })
35
+ // }
36
+ // }
37
+ // console.log(`EchartsBlockRender set option, ${canvasId}`)
38
+ // // 使用刚指定的配置项和数据显示图表。
39
+ // this.myChart.setOption(option, true)
14
40
  },
15
41
  })
@@ -1,9 +1,8 @@
1
1
  import "core-js/modules/esnext.iterator.constructor.js";
2
2
  import "core-js/modules/esnext.iterator.map.js";
3
3
  import "core-js/modules/web.dom-collections.iterator.js";
4
- import React, { useState } from 'react';
4
+ import React, { useEffect, useState } from 'react';
5
5
  import { View } from '@ray-js/ray';
6
- import { useOnEvent } from '../../hooks';
7
6
  export const WaveformVisualizer = _ref => {
8
7
  let {
9
8
  amplitudeCount
@@ -21,26 +20,34 @@ export const WaveformVisualizer = _ref => {
21
20
  animationDelay: "".concat(index * 10, "ms")
22
21
  }
23
22
  })));
24
- useOnEvent('amplitudes', val => {
25
- const waveBase = val.body.amplitudes || [];
26
- const bars = waveBase.map((item, index) => {
27
- const value = item > 1 ? 1 : item * 100;
28
- // 将数值映射到高度范围(0-100 → 2px-20px)
29
- // const height = 2 + (value / 100) * 18;
30
- return /*#__PURE__*/React.createElement(View
31
- // eslint-disable-next-line react/no-array-index-key
32
- , {
33
- key: "asr_wave_".concat(index),
34
- className: "t-agent-message-input-waveform-bar",
35
- style: {
36
- height: "".concat(value, "%"),
37
- // 添加渐变色偏移效果
38
- animationDelay: "".concat(index * 10, "ms")
39
- }
23
+ useEffect(() => {
24
+ const handle = _ref2 => {
25
+ let {
26
+ amplitudes
27
+ } = _ref2;
28
+ const bars = (amplitudes || []).map((item, index) => {
29
+ const value = item > 1 ? 1 : item * 100;
30
+ // 将数值映射到高度范围(0-100 → 2px-20px)
31
+ // const height = 2 + (value / 100) * 18;
32
+ return /*#__PURE__*/React.createElement(View
33
+ // eslint-disable-next-line react/no-array-index-key
34
+ , {
35
+ key: "asr_wave_".concat(index),
36
+ className: "t-agent-message-input-waveform-bar",
37
+ style: {
38
+ height: "".concat(value, "%"),
39
+ // 添加渐变色偏移效果
40
+ animationDelay: "".concat(index * 10, "ms")
41
+ }
42
+ });
40
43
  });
41
- });
42
- setBars(bars);
43
- });
44
+ setBars(bars);
45
+ };
46
+ ty.aistream.onRecordAmplitudes(handle);
47
+ return () => {
48
+ ty.aistream.offRecordAmplitudes(handle);
49
+ };
50
+ }, []);
44
51
  return /*#__PURE__*/React.createElement(View, {
45
52
  className: "t-agent-message-input-waveform-container"
46
53
  }, bars);
@@ -17,6 +17,7 @@ import { useAttachmentInput, useChatAgent, useEmitEvent, useIsUnmounted, useOnEv
17
17
  import AsrInput from './AsrInput';
18
18
  import logger from '../../logger';
19
19
  import { systemInfo } from '../../utils';
20
+ import { useSleep } from '../../hooks/useSleep';
20
21
  const AMPLITUDE_COUNT = 60;
21
22
  var InputState = /*#__PURE__*/function (InputState) {
22
23
  InputState["PENDING"] = "pending";
@@ -196,6 +197,7 @@ export default function MessageInputAIStream(props) {
196
197
  const canSend = text.trim().length && state.current === InputState.PENDING && !attachmentInput.uploading;
197
198
  const canAbort = state.current === InputState.RESPONDING || state.current === InputState.ASR;
198
199
  const recordingFlagRef = useRef(false);
200
+ const sleep = useSleep();
199
201
  if (mode === 'text') {
200
202
  container = /*#__PURE__*/React.createElement(View, {
201
203
  className: "t-agent-message-input-text-bar"
@@ -223,6 +225,13 @@ export default function MessageInputAIStream(props) {
223
225
  emitEvent('scrollToBottom', {
224
226
  animation: true
225
227
  });
228
+ },
229
+ onBlur: () => {
230
+ sleep(200).then(() => {
231
+ emitEvent('scrollToBottom', {
232
+ follow: true
233
+ });
234
+ });
226
235
  }
227
236
  }), /*#__PURE__*/React.createElement(Button, {
228
237
  "data-testid": "t-agent-message-input-button-asr",
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ interface Props {
3
+ id: string;
4
+ className?: string;
5
+ children?: React.ReactNode;
6
+ containerId: string;
7
+ }
8
+ export default function LazyView(props: Props): React.JSX.Element;
9
+ export {};
@@ -0,0 +1,57 @@
1
+ import "core-js/modules/es.regexp.exec.js";
2
+ import "core-js/modules/web.dom-collections.iterator.js";
3
+ import { View } from '@ray-js/components';
4
+ import React, { useEffect, useRef, useState } from 'react';
5
+ import { usePageInstance } from '@ray-js/ray';
6
+ const queryHeight = id => {
7
+ return new Promise(resolve => {
8
+ // @ts-ignore
9
+ const query = ty.createSelectorQuery();
10
+ query.select("#".concat(id)).boundingClientRect(res => {
11
+ resolve(res.height);
12
+ }).exec();
13
+ });
14
+ };
15
+ export default function LazyView(props) {
16
+ const {
17
+ id,
18
+ className,
19
+ children,
20
+ containerId
21
+ } = props;
22
+ const [visible, setVisible] = useState(true);
23
+ const heightRef = useRef(0);
24
+ const valid = heightRef.current > 0;
25
+ console.log();
26
+ const page = usePageInstance();
27
+ useEffect(() => {
28
+ // @ts-ignore
29
+ const observer = ty.createIntersectionObserver(page, {
30
+ thresholds: [0, 1],
31
+ initialRatio: 0
32
+ });
33
+ observer.relativeTo("#".concat(containerId), {
34
+ top: 50,
35
+ bottom: 50
36
+ }).observe("#".concat(id), res => {
37
+ setVisible(res.intersectionRatio > 0);
38
+ console.log(id, res);
39
+ });
40
+ return () => {
41
+ observer.disconnect();
42
+ };
43
+ }, [id, containerId]);
44
+ useEffect(() => {
45
+ queryHeight(id).then(height => {
46
+ console.log('queryHeight', id, height);
47
+ heightRef.current = height;
48
+ });
49
+ }, [id]);
50
+ console.log('rerender', id);
51
+ return /*#__PURE__*/React.createElement(View, {
52
+ className: className,
53
+ "data-hide": !visible && valid,
54
+ id: id
55
+ // style={hide ? { visibility: 'hidden' } : null}
56
+ }, children);
57
+ }
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ interface ScrollBottomViewParams {
3
+ onScroll?: (event: any) => void;
4
+ }
5
+ export declare function useScrollBottomView(params: ScrollBottomViewParams): {
6
+ scrollToBottom: ({ animation, follow }: {
7
+ animation?: boolean | undefined;
8
+ follow?: boolean | undefined;
9
+ }) => void;
10
+ scrollViewLoading: boolean;
11
+ scrollIntoViewId: string;
12
+ contentViewProps: {
13
+ contentViewId: string;
14
+ };
15
+ scrollViewProps: {
16
+ 'data-render-tick': number;
17
+ id: string;
18
+ scrollY: boolean;
19
+ onScroll: (event: {
20
+ detail: {
21
+ scrollTop: number;
22
+ scrollHeight: number;
23
+ deltaY: number;
24
+ };
25
+ currentTarget: any;
26
+ timeStamp: number;
27
+ }) => void;
28
+ scrollWithAnimation: boolean;
29
+ scrollIntoView: string;
30
+ };
31
+ };
32
+ interface Props {
33
+ className?: string;
34
+ contentClassName?: string;
35
+ children: React.ReactNode;
36
+ style?: React.CSSProperties;
37
+ onScroll?: (event: any) => void;
38
+ }
39
+ interface Ref {
40
+ scrollToBottom: (options?: {
41
+ animation?: boolean;
42
+ follow?: boolean;
43
+ }) => void;
44
+ }
45
+ declare const _default: React.MemoExoticComponent<React.ForwardRefExoticComponent<Props & React.RefAttributes<Ref>>>;
46
+ export default _default;
@@ -0,0 +1,202 @@
1
+ import _extends from "@babel/runtime/helpers/esm/extends";
2
+ import "core-js/modules/es.regexp.exec.js";
3
+ import "core-js/modules/web.dom-collections.iterator.js";
4
+ import React, { useCallback, useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
5
+ import { useSleep } from '../hooks/useSleep';
6
+ import { generateId } from '@ray-js/t-agent';
7
+ import { useOnEvent, useTick } from '../hooks';
8
+ import { ScrollView } from '@ray-js/ray';
9
+ import { View } from '@ray-js/components';
10
+ export function useScrollBottomView(params) {
11
+ const [scrollViewId] = useState(() => "ScrollView-".concat(generateId()));
12
+ const contentViewId = "".concat(scrollViewId, "-content");
13
+ const tickValue = useTick();
14
+ const [ready, setReady] = useState(false);
15
+ const [scrollIntoViewFlag, setScrollIntoViewIdFlag] = useState(() => "".concat(Date.now()));
16
+ const [scrollWithAnimation, setScrollWithAnimation] = useState(false);
17
+ const ref = useRef({
18
+ clientHeight: 0,
19
+ needFollow: true,
20
+ following: false,
21
+ lastScrollEvent: null,
22
+ startTimestamp: 0
23
+ });
24
+ const sleep = useSleep();
25
+ const makeScrollBottomEvent = useCallback(() => {
26
+ if (ref.current.lastScrollEvent) {
27
+ const {
28
+ detail,
29
+ currentTarget,
30
+ target
31
+ } = ref.current.lastScrollEvent;
32
+ const {
33
+ scrollTop,
34
+ scrollHeight
35
+ } = detail;
36
+ const newDetail = {
37
+ deltaX: 0,
38
+ scrollLeft: 0,
39
+ scrollTop: scrollHeight - ref.current.clientHeight,
40
+ scrollWidth: detail.scrollWidth,
41
+ scrollHeight,
42
+ deltaY: scrollTop - (scrollHeight - ref.current.clientHeight)
43
+ };
44
+ if (scrollTop === newDetail.scrollTop) {
45
+ // 如果滚动位置没有变化,则不触发事件
46
+ return null;
47
+ }
48
+ return {
49
+ type: 'scroll',
50
+ timeStamp: Date.now() - ref.current.startTimestamp,
51
+ currentTarget,
52
+ target,
53
+ stopPropagation: () => {},
54
+ detail: newDetail
55
+ };
56
+ }
57
+ return null;
58
+ }, []);
59
+ const scrollToBottom = useCallback(_ref => {
60
+ let {
61
+ animation,
62
+ follow
63
+ } = _ref;
64
+ if (follow) {
65
+ if (ref.current.needFollow) {
66
+ setScrollIntoViewIdFlag("".concat(Date.now()));
67
+ ref.current.needFollow = true;
68
+ const event = makeScrollBottomEvent();
69
+ if (event) {
70
+ sleep(10).then(() => {
71
+ var _params$onScroll;
72
+ params === null || params === void 0 || (_params$onScroll = params.onScroll) === null || _params$onScroll === void 0 || _params$onScroll.call(params, event);
73
+ });
74
+ }
75
+ }
76
+ } else {
77
+ if (animation) {
78
+ setScrollWithAnimation(true);
79
+ }
80
+ ref.current.needFollow = true;
81
+ setScrollIntoViewIdFlag("".concat(Date.now()));
82
+ const event = makeScrollBottomEvent();
83
+ if (event) {
84
+ sleep(100).then(() => {
85
+ var _params$onScroll2;
86
+ params === null || params === void 0 || (_params$onScroll2 = params.onScroll) === null || _params$onScroll2 === void 0 || _params$onScroll2.call(params, event);
87
+ });
88
+ }
89
+ if (animation) {
90
+ sleep(10).then(() => {
91
+ setScrollWithAnimation(false);
92
+ });
93
+ }
94
+ }
95
+ }, []);
96
+ useEffect(() => {
97
+ const getDomTick = () => {
98
+ return new Promise(resolve => {
99
+ // @ts-ignore
100
+ const query = ty.createSelectorQuery();
101
+ query.select("#".concat(scrollViewId)).fields({
102
+ dataset: true
103
+ }, res => {
104
+ resolve(res.dataset.renderTick);
105
+ }).exec();
106
+ });
107
+ };
108
+ const getClientHeight = () => {
109
+ return new Promise(resolve => {
110
+ // @ts-ignore
111
+ const query = ty.createSelectorQuery();
112
+ query.select("#".concat(scrollViewId)).fields({
113
+ size: true
114
+ }, res => {
115
+ resolve(res.height);
116
+ }).exec();
117
+ });
118
+ };
119
+ const scrollViewInterval = setInterval(() => {
120
+ // 外部容器高度变化不频繁,1 秒查一次
121
+ getClientHeight().then(height => {
122
+ ref.current.clientHeight = height;
123
+ });
124
+ }, 1000);
125
+
126
+ // 初始化好 clientHeight
127
+ (async () => {
128
+ let tick = await getDomTick();
129
+ while (tick <= 0) {
130
+ await sleep(1);
131
+ tick = await getDomTick();
132
+ }
133
+ return tick;
134
+ })().then(() => getClientHeight()).then(height => {
135
+ console.log('scrollView clientHeight', height);
136
+ ref.current.clientHeight = height;
137
+ })
138
+ // 渲染完毕后,开始滚动到最底部
139
+ .then(() => scrollToBottom({}))
140
+ // 展示列表
141
+ .then(() => setReady(true));
142
+ return () => {
143
+ clearInterval(scrollViewInterval);
144
+ };
145
+ }, [scrollViewId]);
146
+ let scrollIntoViewId = "".concat(scrollViewId, "-toView-").concat(tickValue, "-").concat(scrollIntoViewFlag);
147
+ if (!ref.current.needFollow) {
148
+ scrollIntoViewId = null;
149
+ }
150
+ return {
151
+ scrollToBottom,
152
+ scrollViewLoading: !ready,
153
+ scrollIntoViewId,
154
+ contentViewProps: {
155
+ contentViewId
156
+ },
157
+ scrollViewProps: {
158
+ 'data-render-tick': tickValue,
159
+ id: scrollViewId,
160
+ scrollY: true,
161
+ onScroll: event => {
162
+ var _params$onScroll3;
163
+ ref.current.lastScrollEvent = event;
164
+ const {
165
+ scrollTop,
166
+ scrollHeight
167
+ } = event.detail;
168
+ ref.current.needFollow = scrollHeight - scrollTop <= ref.current.clientHeight + 25;
169
+ params === null || params === void 0 || (_params$onScroll3 = params.onScroll) === null || _params$onScroll3 === void 0 || _params$onScroll3.call(params, event);
170
+ if (!ref.current.startTimestamp) {
171
+ ref.current.startTimestamp = Date.now() - event.timeStamp;
172
+ }
173
+ },
174
+ scrollWithAnimation,
175
+ scrollIntoView: scrollIntoViewId
176
+ }
177
+ };
178
+ }
179
+ const ScrollBottomView = /*#__PURE__*/forwardRef((props, ref) => {
180
+ const {
181
+ scrollToBottom,
182
+ scrollViewProps,
183
+ scrollIntoViewId,
184
+ contentViewProps,
185
+ scrollViewLoading
186
+ } = useScrollBottomView({
187
+ onScroll: props.onScroll
188
+ });
189
+ useOnEvent('scrollToBottom', scrollToBottom);
190
+ useImperativeHandle(ref, () => ({
191
+ scrollToBottom
192
+ }), [scrollToBottom]);
193
+ return /*#__PURE__*/React.createElement(ScrollView, _extends({
194
+ className: "".concat(props.className || '', " ").concat(scrollViewLoading ? 't-agent-message-list-hide' : ''),
195
+ style: props.style
196
+ }, scrollViewProps), /*#__PURE__*/React.createElement(View, _extends({}, contentViewProps, {
197
+ className: "".concat(props.contentClassName || '')
198
+ }), props.children, /*#__PURE__*/React.createElement(View, {
199
+ id: scrollIntoViewId
200
+ })));
201
+ });
202
+ export default /*#__PURE__*/React.memo(ScrollBottomView);
@@ -6,6 +6,8 @@ interface Props {
6
6
  wrapperClassName?: string;
7
7
  wrapperStyle?: React.CSSProperties;
8
8
  renderTop?: React.ReactNode;
9
+ renderBottom?: React.ReactNode;
10
+ onScroll?: (event: any) => void;
9
11
  roleSide?: {
10
12
  user?: 'start' | 'end' | string;
11
13
  assistant?: 'start' | 'end' | string;
@@ -18,5 +20,5 @@ interface Props {
18
20
  tipText: string;
19
21
  };
20
22
  }
21
- export default function MessageList(props: Props): React.JSX.Element;
22
- export {};
23
+ declare const MessageList: (props: Props) => React.JSX.Element;
24
+ export default MessageList;
@@ -1,19 +1,14 @@
1
- import _extends from "@babel/runtime/helpers/esm/extends";
2
- import "core-js/modules/es.array.reverse.js";
3
1
  import "core-js/modules/esnext.iterator.constructor.js";
4
2
  import "core-js/modules/esnext.iterator.map.js";
5
3
  import "core-js/modules/web.dom-collections.iterator.js";
6
4
  import './index.less';
7
5
  import { View } from '@ray-js/components';
8
- import React, { useMemo, useRef, useState } from 'react';
9
- import { ScrollView } from '@ray-js/ray';
10
- import { generateId } from '@ray-js/t-agent';
6
+ import React from 'react';
11
7
  import MessageRender from '../MessageRender';
12
- import { useAgentMessage, useAgentSessionValue, useOnEvent } from '../hooks';
13
- import { useDebouncedFn } from '../hooks/useDebouncedFn';
14
- import { useSleep } from '../hooks/useSleep';
15
- import { systemInfo } from '../utils';
16
- export default function MessageList(props) {
8
+ import { useAgentMessage, useAgentSessionValue } from '../hooks';
9
+ import ScrollBottomView from './ScrollBottomView';
10
+ import { useStableCallback } from '../hooks/useStableCallback';
11
+ const MessageList = props => {
17
12
  const {
18
13
  className,
19
14
  roleSide,
@@ -22,97 +17,38 @@ export default function MessageList(props) {
22
17
  wrapperStyle,
23
18
  historyLimit
24
19
  } = props;
25
- const {
26
- messages
27
- } = useAgentMessage();
28
- const [id] = useState(() => "ScrollView-".concat(generateId()));
29
- const [scrollAnimation, setScrollAnimation] = useState(false);
30
- // 最大整数位数,用于强制滚动到底部,收到连续 scrollToBottom 时,每 10 毫秒更新一次
31
- const [scrollTop, setScrollTop] = useState(() => 1000000000000000 + Math.floor(Date.now() / 10));
32
- const followNewMessageRef = useRef(true);
33
- const sleep = useSleep();
20
+ const messages = useAgentMessage();
21
+ const onScroll = useStableCallback(props.onScroll);
34
22
  const [showSelect] = useAgentSessionValue('UIRay.multiSelect.show');
35
- const canIUse = useMemo(() => {
36
- // @ts-ignore
37
- const {
38
- containerVersion
39
- } = systemInfo;
40
- return {
41
- // 在 2.27.2 版本之前,pageScrollTo 在某些场景不生效
42
- scroll: false
43
- };
44
- }, []);
45
- const scroll = useDebouncedFn(animation => {
46
- // @ts-ignore
47
- ty.pageScrollTo({
48
- scrollTop: 0,
49
- duration: animation ? 100 : 1,
50
- selector: "#".concat(id)
51
- });
52
- }, 100);
53
-
54
- // 强制滚动到底部
55
- useOnEvent('scrollToBottom', _ref => {
56
- let {
57
- animation,
58
- follow
59
- } = _ref;
60
- // 如果发送的跟随新消息的滚动,判断当前是不是滚动到了底部,如果没有滚动到底部,不执行滚动
61
- if (follow && !followNewMessageRef.current) {
62
- return;
63
- }
64
- if (canIUse.scroll) {
65
- scroll(animation);
66
- } else {
67
- sleep(100).then(() => {
68
- setScrollAnimation(!!animation);
69
- const top = 1000000000000000 + Math.floor(Date.now() / 10);
70
- if (top !== scrollTop) {
71
- setScrollTop(top);
72
- }
73
- });
74
- }
75
- });
76
-
77
- // 使用翻转列表,让滚动和新消息出现在第一条,这样可以避免滚动到底部时的闪烁
78
- const reversed = useMemo(() => [...messages].reverse(), [messages]);
79
- const scrollProps = canIUse.scroll ? {} : {
80
- scrollWithAnimation: scrollAnimation,
81
- scrollTop
82
- };
83
23
  return /*#__PURE__*/React.createElement(View, {
84
24
  className: "t-agent-message-list ".concat(wrapperClassName || ''),
85
25
  "data-testid": "t-agent-message-list",
86
- style: wrapperStyle
87
- }, /*#__PURE__*/React.createElement(ScrollView, _extends({
26
+ style: wrapperStyle,
27
+ id: "message-list"
28
+ }, /*#__PURE__*/React.createElement(ScrollBottomView, {
88
29
  className: "".concat(className || '', " t-agent-message-list-scroll"),
30
+ contentClassName: "t-agent-message-list-scroll-content",
89
31
  style: style,
90
- id: id
91
- }, scrollProps, {
92
- refresherTriggered: false,
93
- enableFlex: true,
94
- scrollY: true,
95
- onScroll: event => {
96
- // 使用了 flex-direction: column-reverse,所以滚动到顶部时,scrollTop = 0
97
- followNewMessageRef.current = event.detail.scrollTop > -10;
98
- }
99
- }), /*#__PURE__*/React.createElement(View, {
100
- className: "t-agent-message-list-padding"
101
- }), reversed.map((msg, index) => {
32
+ onScroll: onScroll
33
+ }, historyLimit && historyLimit.count < messages.length && /*#__PURE__*/React.createElement(View, {
34
+ className: "t-agent-message-list-history-tip"
35
+ }, historyLimit.tipText), /*#__PURE__*/React.createElement(View, {
36
+ className: "t-agent-message-list-padding-start"
37
+ }), props.renderTop, messages.map((msg, index) => {
102
38
  let side = roleSide === null || roleSide === void 0 ? void 0 : roleSide[msg.role];
103
39
  if (!side) {
104
40
  side = msg.role === 'user' ? 'end' : 'start';
105
41
  }
42
+ const isLatestMessage = index === messages.length - 1;
106
43
  return /*#__PURE__*/React.createElement(MessageRender, {
107
44
  showSelect: showSelect,
108
45
  key: msg.id,
109
46
  message: msg,
110
- isLatestMessage: index === 0,
47
+ isLatestMessage: isLatestMessage,
111
48
  side: side
112
49
  });
113
- }), historyLimit && historyLimit.count < reversed.length && /*#__PURE__*/React.createElement(View, {
114
- className: "t-agent-message-list-history-tip"
115
- }, historyLimit.tipText), props.renderTop, /*#__PURE__*/React.createElement(View, {
116
- className: "t-agent-message-list-padding-start"
50
+ }), props.renderBottom, /*#__PURE__*/React.createElement(View, {
51
+ className: "t-agent-message-list-padding"
117
52
  })));
118
- }
53
+ };
54
+ export default MessageList;
@@ -4,21 +4,32 @@
4
4
  overflow-x: hidden;
5
5
  position: relative;
6
6
  background: var(--app-B1);
7
- padding: 16rpx 32rpx 0;
8
7
  }
9
8
 
10
9
  .t-agent-message-list-scroll {
11
- display: flex;
12
10
  width: 100%;
13
11
  height: 100%;
14
- flex-direction: column-reverse;
12
+ }
13
+
14
+ .t-agent-message-list-scroll-content {
15
+ padding: 0 32rpx;
16
+ }
17
+
18
+ .t-agent-message-list-block {
19
+ border: 2rpx solid green;
20
+ }
21
+
22
+ .t-agent-message-list-padding-start {
23
+ height: 16rpx;
15
24
  }
16
25
 
17
26
  .t-agent-message-list-padding {
18
27
  flex: 1
19
28
  }
20
29
 
21
- .t-agent-message-list-padding-start {}
30
+ .t-agent-message-list-hide {
31
+ opacity: 0;
32
+ }
22
33
 
23
34
  .t-agent-message-list-history-tip {
24
35
  width: 100%;
@@ -1,6 +1,6 @@
1
1
  import './index.less';
2
2
  import React, { useCallback, useMemo } from 'react';
3
- import { useChatAgent, useRenderOptions } from '../hooks';
3
+ import { useChatAgent, useNotifyHeightChanged, useRenderOptions } from '../hooks';
4
4
  import { TilePropsContext } from '../contexts';
5
5
  const TileRender = _ref => {
6
6
  let {
@@ -11,6 +11,7 @@ const TileRender = _ref => {
11
11
  } = _ref;
12
12
  const agent = useChatAgent();
13
13
  const emitTileEvent = useCallback(async payload => agent.emitTileEvent(tile.id, payload), [agent, tile.id]);
14
+ const notifyHeightChanged = useNotifyHeightChanged();
14
15
  const {
15
16
  renderTileAs
16
17
  } = useRenderOptions();
@@ -22,9 +23,10 @@ const TileRender = _ref => {
22
23
  side,
23
24
  isLatestMessage,
24
25
  emitEvent: emitTileEvent,
25
- emitTileEvent
26
+ emitTileEvent,
27
+ notifyHeightChanged
26
28
  };
27
- }, [agent, tile, message, side, isLatestMessage, emitTileEvent]);
29
+ }, [agent, tile, message, side, isLatestMessage, emitTileEvent, notifyHeightChanged]);
28
30
  const node = renderTileAs(props);
29
31
  return /*#__PURE__*/React.createElement(TilePropsContext.Provider, {
30
32
  value: props
@@ -2,14 +2,26 @@
2
2
  import { ChatAgent, ChatMessageObject, UIPlugin } from '@ray-js/t-agent';
3
3
  import { RenderOptions, TileProps } from './types';
4
4
  import { ChatSession } from '@ray-js/t-agent';
5
- export declare const MessageContext: import("react").Context<{
6
- messages: ChatMessageObject[];
7
- keyboardHeight: number;
8
- }>;
9
- export declare const RenderContext: import("react").Context<RenderOptions>;
10
5
  export type UIChatSession = Pick<ChatSession, 'get' | 'getData' | 'set' | 'sessionId'>;
11
6
  export type UIChatAgent = Pick<ChatAgent<UIPlugin>, 'pushInputBlocks' | 'plugins' | 'emitTileEvent' | 'removeMessage'> & {
12
7
  session: UIChatSession;
13
8
  };
14
- export declare const ChatAgentContext: import("react").Context<UIChatAgent>;
15
9
  export declare const TilePropsContext: import("react").Context<TileProps<any, any>>;
10
+ declare const SharedProvider: ({ value, children }: import("react").PropsWithChildren<{
11
+ value: {
12
+ agent: UIChatAgent;
13
+ messages: ChatMessageObject[];
14
+ keyboardHeight: number;
15
+ renderOptions: RenderOptions;
16
+ notifyHeightChanged: () => void;
17
+ tickValue: number;
18
+ };
19
+ }>) => import("react").JSX.Element, useSharedState: <K extends "agent" | "messages" | "keyboardHeight" | "renderOptions" | "notifyHeightChanged" | "tickValue">(key: K) => {
20
+ agent: UIChatAgent;
21
+ messages: ChatMessageObject[];
22
+ keyboardHeight: number;
23
+ renderOptions: RenderOptions;
24
+ notifyHeightChanged: () => void;
25
+ tickValue: number;
26
+ }[K];
27
+ export { SharedProvider, useSharedState };
package/dist/contexts.js CHANGED
@@ -1,18 +1,6 @@
1
+ import "core-js/modules/web.dom-collections.iterator.js";
1
2
  import { createContext } from 'react';
2
- export const MessageContext = /*#__PURE__*/createContext({
3
- messages: [],
4
- keyboardHeight: 0
5
- });
6
- export const RenderContext = /*#__PURE__*/createContext({
7
- formatErrorMessageAs: message => message,
8
- renderTileAs: () => null,
9
- renderCustomBlockAs: () => null,
10
- renderCardAs: () => null,
11
- renderLongPressAs: () => null,
12
- customBlockTypes: [],
13
- customCardMap: {},
14
- getStaticResourceBizType: () => '',
15
- i18nTranslate: key => key
16
- });
17
- export const ChatAgentContext = /*#__PURE__*/createContext({});
18
- export const TilePropsContext = /*#__PURE__*/createContext({});
3
+ import { createSharedStore } from './utils/createSharedStore';
4
+ export const TilePropsContext = /*#__PURE__*/createContext({});
5
+ const [SharedProvider, useSharedState] = createSharedStore();
6
+ export { SharedProvider, useSharedState };
@@ -1,11 +1,11 @@
1
1
  import { Dispatch, SetStateAction } from 'react';
2
2
  import { TTTAction } from '../types';
3
3
  export declare const useChatAgent: () => import("../contexts").UIChatAgent;
4
- export declare const useAgentMessage: () => {
5
- messages: import("@ray-js/t-agent").ChatMessageObject[];
6
- keyboardHeight: number;
7
- };
4
+ export declare const useTick: () => number;
5
+ export declare const useNotifyHeightChanged: () => () => void;
6
+ export declare const useAgentMessage: () => import("@ray-js/t-agent").ChatMessageObject[];
8
7
  export declare const useRenderOptions: () => import("../types").RenderOptions;
8
+ export declare const useKeyboardHeight: () => number;
9
9
  export declare const useOnEvent: (eventName: string, callback: (details: any) => void) => void;
10
10
  export declare const useEmitEvent: () => <T extends keyof import("@ray-js/t-agent").UIEventMap>(eventName: T, detail: import("@ray-js/t-agent").UIEventMap[T]) => void;
11
11
  export declare const useTileProps: () => import("../types").TileProps<any, any>;
@@ -1,14 +1,23 @@
1
1
  import "core-js/modules/web.dom-collections.iterator.js";
2
2
  import { useEffect, useContext, useCallback, useRef, useState } from 'react';
3
- import { TilePropsContext, ChatAgentContext, MessageContext, RenderContext } from '../contexts';
3
+ import { TilePropsContext, useSharedState } from '../contexts';
4
4
  export const useChatAgent = () => {
5
- return useContext(ChatAgentContext);
5
+ return useSharedState('agent');
6
+ };
7
+ export const useTick = () => {
8
+ return useSharedState('tickValue');
9
+ };
10
+ export const useNotifyHeightChanged = () => {
11
+ return useSharedState('notifyHeightChanged');
6
12
  };
7
13
  export const useAgentMessage = () => {
8
- return useContext(MessageContext);
14
+ return useSharedState('messages');
9
15
  };
10
16
  export const useRenderOptions = () => {
11
- return useContext(RenderContext);
17
+ return useSharedState('renderOptions');
18
+ };
19
+ export const useKeyboardHeight = () => {
20
+ return useSharedState('keyboardHeight');
12
21
  };
13
22
  export const useOnEvent = (eventName, callback) => {
14
23
  const callbackRef = useRef(callback);
@@ -0,0 +1 @@
1
+ export declare const useStableCallback: <T extends (...args: any[]) => any>(fn: T) => T;
@@ -0,0 +1,13 @@
1
+ import "core-js/modules/web.dom-collections.iterator.js";
2
+ import { useCallback, useRef } from 'react';
3
+ export const useStableCallback = fn => {
4
+ const ref = useRef(fn);
5
+ ref.current = fn;
6
+ const cb = useCallback(function () {
7
+ return ref.current(...arguments);
8
+ }, []);
9
+ if (!fn) {
10
+ return fn;
11
+ }
12
+ return cb;
13
+ };
@@ -108,13 +108,13 @@ const BubbleTile = props => {
108
108
  }), /*#__PURE__*/React.createElement(View, {
109
109
  className: "t-agent-bubble-tile-bubble ".concat(loading ? 't-agent-bubble-tile-bubble-loading' : '')
110
110
  }, (() => {
111
- return /*#__PURE__*/React.createElement(React.Fragment, null, children.map(t => /*#__PURE__*/React.createElement(TileRender, {
111
+ return children.map(t => /*#__PURE__*/React.createElement(TileRender, {
112
112
  side: side,
113
113
  tile: t,
114
114
  message: message,
115
115
  key: t.id,
116
116
  isLatestMessage: isLatestMessage
117
- })));
117
+ }));
118
118
  })(), /*#__PURE__*/React.createElement(LoadingIndicator, {
119
119
  status: status
120
120
  }), showAbortedMessage && /*#__PURE__*/React.createElement(View, {
@@ -126,7 +126,6 @@ const BubbleTile = props => {
126
126
  nodeId: workflowNode
127
127
  }));
128
128
  };
129
- export default /*#__PURE__*/React.memo(BubbleTile);
130
129
  const ErrorNotice = /*#__PURE__*/memo(_ref => {
131
130
  let {
132
131
  bubbleStatus,
@@ -172,4 +171,5 @@ const LoadingIndicator = /*#__PURE__*/memo(_ref2 => {
172
171
  }));
173
172
  }
174
173
  return null;
175
- });
174
+ });
175
+ export default /*#__PURE__*/React.memo(BubbleTile);
@@ -7,7 +7,7 @@ import React, { useMemo, useState } from 'react';
7
7
  import './index.less';
8
8
  // import { ChangeInfoItem, DeviceInfoItem, SceneInfoItem } from '@ray-js/t-agent-plugin-assistant';
9
9
 
10
- import { useTranslate } from '../../hooks';
10
+ import { useTileProps, useTranslate } from '../../hooks';
11
11
  import Expand from './Expand';
12
12
  import { formatMessage } from '../../utils';
13
13
  const handleDesc = (detail, t) => {
@@ -197,6 +197,9 @@ export default function OperateCardTile(props) {
197
197
  };
198
198
  }, [deviceInfo, sceneInfo, changeInfo, t]);
199
199
  const desc = useMemo(() => handleDesc(operations, t), [operations, t]);
200
+ const {
201
+ notifyHeightChanged
202
+ } = useTileProps();
200
203
  if (!operations.device.length && !operations.scene.length && !operations.moreDevice.length && !operations.moreScene.length) {
201
204
  return null;
202
205
  }
@@ -208,6 +211,7 @@ export default function OperateCardTile(props) {
208
211
  className: "t-agent-scene-control-tile-summary-detail",
209
212
  onClick: () => {
210
213
  setShowDetail(!showDetail);
214
+ notifyHeightChanged();
211
215
  }
212
216
  }, showDetail ? t('t-agent.operate-card-tile.hide.details') : t('t-agent.operate-card-tile.view.details'))), renderDetail && renderDetail({
213
217
  operations,
package/dist/types.d.ts CHANGED
@@ -52,6 +52,7 @@ export interface TileProps<T = any, P = any> {
52
52
  emitTileEvent: (payload: P) => void;
53
53
  isLatestMessage?: boolean;
54
54
  agent: UIChatAgent;
55
+ notifyHeightChanged: () => void;
55
56
  }
56
57
  export interface MarkdownBlock {
57
58
  id: string;
@@ -0,0 +1,4 @@
1
+ import React, { PropsWithChildren } from 'react';
2
+ export declare function createSharedStore<T>(): readonly [({ value, children }: React.PropsWithChildren<{
3
+ value: T;
4
+ }>) => React.JSX.Element, <K extends keyof T>(key: K) => T[K], <K_1 extends keyof T>(key: K_1) => [T[K_1], (v: T[K_1]) => void]];
@@ -0,0 +1,64 @@
1
+ import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
2
+ import "core-js/modules/esnext.iterator.constructor.js";
3
+ import "core-js/modules/esnext.iterator.for-each.js";
4
+ import "core-js/modules/web.dom-collections.iterator.js";
5
+ import React, { createContext, useContext, useRef, useState, useEffect, useLayoutEffect } from 'react';
6
+ export function createSharedStore() {
7
+ const Context = /*#__PURE__*/createContext(null);
8
+ return [function (_ref) {
9
+ let {
10
+ value,
11
+ children
12
+ } = _ref;
13
+ const storeRef = useRef({
14
+ value,
15
+ listeners: new Map(),
16
+ setValue(newValue) {
17
+ const oldValue = storeRef.current.value;
18
+ storeRef.current.value = newValue;
19
+ for (const key of Object.keys(newValue)) {
20
+ if (oldValue[key] !== newValue[key]) {
21
+ var _storeRef$current$lis;
22
+ (_storeRef$current$lis = storeRef.current.listeners.get(key)) === null || _storeRef$current$lis === void 0 || _storeRef$current$lis.forEach(fn => fn());
23
+ }
24
+ }
25
+ },
26
+ updateKey(key, val) {
27
+ const newValue = _objectSpread(_objectSpread({}, storeRef.current.value), {}, {
28
+ [key]: val
29
+ });
30
+ storeRef.current.setValue(newValue);
31
+ },
32
+ subscribe(key, fn) {
33
+ if (!storeRef.current.listeners.has(key)) {
34
+ storeRef.current.listeners.set(key, new Set());
35
+ }
36
+ storeRef.current.listeners.get(key).add(fn);
37
+ return () => {
38
+ storeRef.current.listeners.get(key).delete(fn);
39
+ };
40
+ }
41
+ });
42
+ useLayoutEffect(() => {
43
+ // ✅ 在 render 阶段同步设置最新 value
44
+ storeRef.current.setValue(value);
45
+ }, [value]);
46
+ return /*#__PURE__*/React.createElement(Context.Provider, {
47
+ value: storeRef.current
48
+ }, children);
49
+ }, function (key) {
50
+ const store = useContext(Context);
51
+ if (!store) throw new Error('useSharedValue must be used within Provider');
52
+ const [, forceUpdate] = useState(0);
53
+ useEffect(() => store.subscribe(key, () => forceUpdate(n => n + 1)), [key, store]);
54
+ return store.value[key];
55
+ }, function (key) {
56
+ const store = useContext(Context);
57
+ if (!store) throw new Error('useSharedState must be used within Provider');
58
+ const [, forceUpdate] = useState(0);
59
+ useEffect(() => store.subscribe(key, () => forceUpdate(n => n + 1)), [key, store]);
60
+ return [store.value[key], val => {
61
+ store.updateKey(key, val);
62
+ }];
63
+ }];
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ray-js/t-agent-ui-ray",
3
- "version": "0.2.0-beta.19",
3
+ "version": "0.2.0-beta.20",
4
4
  "author": "Tuya.inc",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -40,5 +40,5 @@
40
40
  "@types/echarts": "^4.9.22",
41
41
  "@types/markdown-it": "^14.1.1"
42
42
  },
43
- "gitHead": "0db50eef90b217b739bab1d824e4c6b2d495a169"
43
+ "gitHead": "517ce9d6aec8ac6a1ade692df773750d0c7738cf"
44
44
  }