@ray-js/t-agent-ui-ray 0.2.6-beta-7 → 0.2.6-beta-8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ray-js/t-agent-ui-ray",
3
- "version": "0.2.6-beta-7",
3
+ "version": "0.2.6-beta-8",
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": "d76b6bc9d2d7b69b12cc1b741a8638f2dd671db2"
43
+ "gitHead": "6fc9a8195706d72c2bab10479e8d65a26854bdde"
44
44
  }
@@ -1,18 +0,0 @@
1
- import '../index.less';
2
- import React from 'react';
3
- interface Props {
4
- text: string;
5
- setText: (text: string) => void;
6
- onBack: () => void;
7
- sendDisabled: boolean;
8
- responding: boolean;
9
- onSend: () => Promise<any>;
10
- onError: (error: Error) => void;
11
- onMoreClick: () => void;
12
- closeMore: () => void;
13
- moreOpen: boolean;
14
- hasMore: boolean;
15
- onAbort: () => void;
16
- }
17
- export default function AsrInput(props: Props): React.JSX.Element;
18
- export {};
@@ -1,89 +0,0 @@
1
- import '../index.less';
2
- import { Button, Text, View, Textarea } from '@ray-js/components';
3
- import React from 'react';
4
- import cx from 'clsx';
5
- import { useAsrInput } from './useAsrInput';
6
- export default function AsrInput(props) {
7
- const {
8
- responding,
9
- sendDisabled,
10
- onMoreClick,
11
- moreOpen,
12
- closeMore,
13
- hasMore,
14
- text,
15
- setText,
16
- onAbort
17
- } = props;
18
- const {
19
- state,
20
- press,
21
- release,
22
- clear,
23
- currentText,
24
- incomingText
25
- } = useAsrInput(props);
26
- return /*#__PURE__*/React.createElement(View, {
27
- className: "t-agent-message-input-voice-bar"
28
- }, (state === 'recording' || state === 'pending') && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(View, {
29
- className: "t-agent-message-input-voice-mask"
30
- }), /*#__PURE__*/React.createElement(View, {
31
- className: "t-agent-message-input-voice-bubble",
32
- "data-testid": "t-agent-message-input-asr-bubble"
33
- }, state === 'recording' ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Text, null, currentText.endsWith(' ') ? /*#__PURE__*/React.createElement(React.Fragment, null, currentText, "\xA0") : currentText), /*#__PURE__*/React.createElement(Text, {
34
- className: "t-agent-message-input-voice-bubble-predicted"
35
- }, incomingText)) : /*#__PURE__*/React.createElement(Textarea, {
36
- "data-testid": "t-agent-message-input-asr-bubble-input",
37
- autoHeight: true,
38
- maxLength: 2048,
39
- className: "t-agent-message-input-voice-bubble-input",
40
- value: text,
41
- onInput: event => setText(event.value)
42
- }))), /*#__PURE__*/React.createElement(Button, {
43
- "data-testid": "t-agent-message-input-asr-button-left",
44
- className: cx('t-agent-message-input-button', {
45
- 't-agent-message-input-button-text': state === 'init',
46
- 't-agent-message-input-button-hide': state === 'recording',
47
- 't-agent-message-input-button-clear': state === 'pending'
48
- }),
49
- onClick: () => {
50
- if (state === 'init') {
51
- clear();
52
- props.onBack();
53
- } else if (state === 'pending') {
54
- clear();
55
- }
56
- }
57
- }), /*#__PURE__*/React.createElement(Button, {
58
- "data-testid": "t-agent-message-input-asr-button-middle",
59
- className: "t-agent-message-input-button t-agent-message-input-button-voice-active",
60
- disabled: sendDisabled,
61
- onTouchStart: () => {
62
- closeMore();
63
- press();
64
- },
65
- onClick: release,
66
- onTouchCancel: release,
67
- onTouchEnd: release
68
- }), /*#__PURE__*/React.createElement(Button, {
69
- disabled: sendDisabled,
70
- "data-testid": "t-agent-message-input-asr-button-right",
71
- className: cx('t-agent-message-input-button', {
72
- 't-agent-message-input-button-voice-send': state === 'pending',
73
- 't-agent-message-input-button-hide': (state === 'recording' || !hasMore && state !== 'pending') && !responding,
74
- 't-agent-message-input-button-more': state === 'init' && hasMore,
75
- 't-agent-message-input-button-more-open': state === 'init' && moreOpen,
76
- 't-agent-message-input-button-stop': state === 'init' && responding
77
- }),
78
- onClick: () => {
79
- if (responding) {
80
- onAbort();
81
- } else if (state === 'init' && hasMore) {
82
- onMoreClick();
83
- } else {
84
- props.onSend();
85
- clear();
86
- }
87
- }
88
- }));
89
- }
@@ -1,30 +0,0 @@
1
- import { Emitter } from '@ray-js/t-agent';
2
- import { AsrListenerManager } from '../../utils/ttt';
3
- export interface DetectResult {
4
- /** managerId */
5
- managerId: number;
6
- /** 拾音状态 0. 未开启 1.进行中 2.结束 3.发送错误 */
7
- state: number;
8
- /** 语言转换内容 */
9
- text: string;
10
- /** 错误码 0. 录音时间太短 */
11
- errorCode: number;
12
- }
13
- export declare enum AsrDetectResultState {
14
- WAIT = 0,
15
- MID = 1,
16
- END = 2,
17
- ERROR = 3
18
- }
19
- export declare class Asr {
20
- static manager: AsrListenerManager | null;
21
- static emitter: Emitter;
22
- static authorize(): Promise<boolean>;
23
- static createManager(): Promise<AsrListenerManager>;
24
- static listener: (detail: DetectResult) => void;
25
- static dispose: () => void;
26
- static detect(callback: (params: DetectResult) => void): {
27
- start(): Promise<void>;
28
- stop(): Promise<void>;
29
- };
30
- }
@@ -1,126 +0,0 @@
1
- import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
2
- var _Asr;
3
- import "core-js/modules/es.json.stringify.js";
4
- import { Emitter, EmitterEvent } from '@ray-js/t-agent';
5
- import logger from '../../logger';
6
- import { authorize, getAppInfo, getAsrListenerManager, getCurrentHomeInfo } from '../../utils/ttt';
7
- export let AsrDetectResultState = /*#__PURE__*/function (AsrDetectResultState) {
8
- AsrDetectResultState[AsrDetectResultState["WAIT"] = 0] = "WAIT";
9
- AsrDetectResultState[AsrDetectResultState["MID"] = 1] = "MID";
10
- AsrDetectResultState[AsrDetectResultState["END"] = 2] = "END";
11
- AsrDetectResultState[AsrDetectResultState["ERROR"] = 3] = "ERROR";
12
- return AsrDetectResultState;
13
- }({});
14
- export class Asr {
15
- // 录音权限
16
- static authorize() {
17
- return authorize({
18
- scope: 'scope.record'
19
- });
20
- }
21
- static async createManager() {
22
- if (Asr.manager) {
23
- return Asr.manager;
24
- }
25
- const {
26
- homeId
27
- } = await getCurrentHomeInfo();
28
- const systemInfo = ty.getSystemInfoSync();
29
- const appInfo = await getAppInfo();
30
- const isCnApp = appInfo.regionCode === 'AY';
31
- const lang = systemInfo.language;
32
- const params = {
33
- homeId,
34
- sampleRate: isCnApp ? 16000 : 8000,
35
- channels: 1,
36
- codec: isCnApp ? 0 : 1,
37
- options: JSON.stringify({
38
- format: isCnApp ? 'wav' : 'mulaw',
39
- lang,
40
- channel: '1',
41
- sampleRate: isCnApp ? '16000' : '8000',
42
- realTimeAsr: 'true',
43
- 'asr.only': 'true'
44
- })
45
- };
46
- logger.debug('Asr createManager', params);
47
- Asr.manager = await getAsrListenerManager(params);
48
- Asr.manager.onDetect(Asr.listener);
49
- return Asr.manager;
50
- }
51
- static detect(callback) {
52
- const listener = event => {
53
- var _event$detail;
54
- callback(event.detail);
55
- if (((_event$detail = event.detail) === null || _event$detail === void 0 ? void 0 : _event$detail.state) === AsrDetectResultState.END) {
56
- Asr.emitter.removeEventListener('result', listener);
57
- }
58
- };
59
- const promise = Asr.createManager();
60
- Asr.emitter.addEventListener('result', listener);
61
- return {
62
- start() {
63
- logger.debug('Asr detect start');
64
- return new Promise((resolve, reject) => {
65
- promise.then(manager => {
66
- manager.getAsrActive({
67
- success: _ref => {
68
- let {
69
- isActive
70
- } = _ref;
71
- logger.debug('Asr manager.getAsrActive', isActive);
72
- if (!isActive) {
73
- manager.startDetect({
74
- success: function (res) {
75
- logger.debug('Asr startDetect success', res);
76
- resolve();
77
- },
78
- fail: function (err) {
79
- logger.error('Asr startDetect fail', err);
80
- reject(err);
81
- }
82
- });
83
- } else {
84
- resolve();
85
- }
86
- },
87
- failure: params => {
88
- logger.error('Asr manager.getAsrActive failure', params);
89
- reject(params);
90
- }
91
- });
92
- });
93
- });
94
- },
95
- async stop() {
96
- const manager = await promise;
97
- logger.debug('Asr detect stop');
98
- await new Promise((resolve, reject) => {
99
- manager.stopDetect({
100
- success: resolve,
101
- fail: reject
102
- });
103
- });
104
- }
105
- };
106
- }
107
- }
108
- _Asr = Asr;
109
- _defineProperty(Asr, "manager", null);
110
- _defineProperty(Asr, "emitter", new Emitter());
111
- _defineProperty(Asr, "listener", detail => {
112
- const {
113
- text,
114
- state
115
- } = detail;
116
- logger.debug('Asr listener', detail);
117
- _Asr.emitter.dispatchEvent(new EmitterEvent('result', {
118
- detail
119
- }));
120
- });
121
- _defineProperty(Asr, "dispose", () => {
122
- if (_Asr.manager) {
123
- _Asr.manager.offDetect(_Asr.listener);
124
- _Asr.manager = null;
125
- }
126
- });
@@ -1,11 +0,0 @@
1
- import '../index.less';
2
- import React from 'react';
3
- interface Props {
4
- className?: string;
5
- renderTop?: React.ReactNode;
6
- placeholder?: string;
7
- style?: React.CSSProperties;
8
- multiModal?: boolean;
9
- }
10
- export default function MessageInputAssistant(props: Props): React.JSX.Element;
11
- export {};
@@ -1,325 +0,0 @@
1
- import "core-js/modules/es.string.trim.js";
2
- import "core-js/modules/esnext.iterator.constructor.js";
3
- import "core-js/modules/esnext.iterator.filter.js";
4
- import "core-js/modules/esnext.iterator.map.js";
5
- import "core-js/modules/web.dom-collections.iterator.js";
6
- import '../index.less';
7
- import { Button, View } from '@ray-js/components';
8
- import React, { useRef, useState } from 'react';
9
- import { Image, Input, ScrollView } from '@ray-js/ray';
10
- import cx from 'clsx';
11
- import PrivateImage from '../../PrivateImage';
12
- import imageSvg from '../icons/image.svg';
13
- import videoSvg from '../icons/video.svg';
14
- import loadingSvg from '../icons/loading.svg';
15
- import closeCircleSvg from '../icons/close-circle.svg';
16
- import { AbortController } from '../../utils/abort';
17
- import { useAttachmentInput, useChatAgent, useEmitEvent, useIsUnmounted, useOnEvent, useRenderOptions, useTranslate } from '../../hooks';
18
- import AsrInput from './AsrInput';
19
- import { authorize } from '../../utils/ttt';
20
- export default function MessageInputAssistant(props) {
21
- const [moreOpen, setMoreOpen] = useState(false);
22
- const t = useTranslate();
23
- const [text, setText] = useState('');
24
- const [asrText, setAsrText] = useState('');
25
- const attachmentInput = useAttachmentInput();
26
- const {
27
- uploading,
28
- uploaded,
29
- setUploaded,
30
- upload
31
- } = attachmentInput;
32
- const acRef = useRef(null);
33
- const [responding, setResponding] = useState(false);
34
- const [mode, setMode] = useState('text');
35
- const agent = useChatAgent();
36
- const emitEvent = useEmitEvent();
37
- const isUnmounted = useIsUnmounted();
38
- useOnEvent('networkChange', _ref => {
39
- let {
40
- online
41
- } = _ref;
42
- if (!online && responding) {
43
- setResponding(false);
44
- }
45
- });
46
- const hasMore = !!props.multiModal;
47
- const isMore = !text.trim().length && hasMore;
48
- const send = async inputBlocks => {
49
- if (!(inputBlocks !== null && inputBlocks !== void 0 && inputBlocks.length) || attachmentInput.uploading || responding) {
50
- return;
51
- }
52
- setUploaded([]);
53
- setText('');
54
- setResponding(true);
55
- const ac = new AbortController();
56
- acRef.current = ac;
57
- ac.signal.addEventListener('abort', () => {
58
- if (acRef.current === ac) {
59
- acRef.current = null;
60
- }
61
- setResponding(false);
62
- });
63
- try {
64
- await agent.pushInputBlocks(inputBlocks, ac.signal);
65
- } finally {
66
- if (!isUnmounted()) {
67
- setResponding(false);
68
- }
69
- }
70
- };
71
- useOnEvent('sendMessage', async _ref2 => {
72
- let {
73
- blocks
74
- } = _ref2;
75
- if (uploading || responding) {
76
- return;
77
- }
78
- setText('');
79
- setMoreOpen(false);
80
- setUploaded([]);
81
- setResponding(true);
82
- const ac = new AbortController();
83
- acRef.current = ac;
84
- ac.signal.addEventListener('abort', () => {
85
- if (acRef.current === ac) {
86
- acRef.current = null;
87
- }
88
- setResponding(false);
89
- });
90
- try {
91
- await agent.pushInputBlocks(blocks, ac.signal);
92
- } finally {
93
- if (!isUnmounted()) {
94
- setResponding(false);
95
- }
96
- }
97
- });
98
- useOnEvent('setInputBlocks', async _ref3 => {
99
- let {
100
- blocks
101
- } = _ref3;
102
- if (uploading || responding) {
103
- return;
104
- }
105
- if (mode !== 'text') {
106
- setMode('text');
107
- }
108
- attachmentInput.loadBlocks(blocks);
109
- let t = '';
110
- for (const block of blocks) {
111
- if (block.type === 'text') {
112
- t = block.text;
113
- }
114
- }
115
- if (t) {
116
- setText(t);
117
- }
118
- setMoreOpen(false);
119
- });
120
- const openMoreClick = () => {
121
- setMoreOpen(!moreOpen);
122
- if (!moreOpen) {
123
- emitEvent('scrollToBottom', {
124
- animation: true
125
- });
126
- }
127
- };
128
- const {
129
- getStaticResourceBizType
130
- } = useRenderOptions();
131
- let container;
132
- if (mode === 'text') {
133
- container = /*#__PURE__*/React.createElement(View, {
134
- className: "t-agent-message-input-text-bar"
135
- }, /*#__PURE__*/React.createElement(View, {
136
- className: "t-agent-message-input-text-group"
137
- }, /*#__PURE__*/React.createElement(Input, {
138
- confirmType: "send",
139
- value: text,
140
- onInput: event => setText(event.detail.value),
141
- placeholder: props.placeholder,
142
- "data-testid": "t-agent-message-input-text-inner",
143
- className: "t-agent-message-input-text-inner",
144
- onConfirm: () => {
145
- if (text.trim().length) {
146
- send([...attachmentInput.blocks, {
147
- type: 'text',
148
- text
149
- }]);
150
- }
151
- },
152
- maxLength: 200,
153
- placeholderStyle: "color: var(--app-B1-N4)",
154
- onFocus: () => {
155
- setMoreOpen(false);
156
- emitEvent('scrollToBottom', {
157
- animation: true
158
- });
159
- }
160
- }), /*#__PURE__*/React.createElement(Button, {
161
- "data-testid": "t-agent-message-input-button-asr",
162
- onClick: async () => {
163
- const auth = await authorize({
164
- scope: 'scope.record'
165
- });
166
- if (!auth) {
167
- ty.showToast({
168
- icon: 'none',
169
- title: t('t-agent.input.voice.require-permission')
170
- });
171
- return;
172
- }
173
- setText('');
174
- setMode('voice');
175
- },
176
- className: "t-agent-message-input-button t-agent-message-input-button-voice"
177
- })), /*#__PURE__*/React.createElement(Button, {
178
- disabled: !isMore && uploading,
179
- className: cx('t-agent-message-input-button', {
180
- 't-agent-message-input-button-more': isMore,
181
- 't-agent-message-input-button-more-open': moreOpen && isMore,
182
- 't-agent-message-input-button-send': !isMore && !responding,
183
- 't-agent-message-input-button-stop': responding
184
- }),
185
- "data-testid": "t-agent-message-input-button-main",
186
- onClick: async () => {
187
- if (responding) {
188
- if (acRef.current) {
189
- acRef.current.abort('User abort');
190
- }
191
- } else if (isMore) {
192
- openMoreClick();
193
- } else if (text.trim().length) {
194
- await send([...attachmentInput.blocks, {
195
- type: 'text',
196
- text
197
- }]);
198
- }
199
- }
200
- }));
201
- } else {
202
- container = /*#__PURE__*/React.createElement(AsrInput, {
203
- text: asrText,
204
- setText: setAsrText,
205
- onBack: () => {
206
- setText('');
207
- setMode('text');
208
- },
209
- onAbort: () => {
210
- if (acRef.current) {
211
- acRef.current.abort('User abort');
212
- }
213
- },
214
- moreOpen: moreOpen,
215
- closeMore: () => setMoreOpen(false),
216
- onMoreClick: openMoreClick,
217
- responding: responding,
218
- sendDisabled: uploading,
219
- onSend: async () => {
220
- if (asrText) {
221
- await send([...attachmentInput.blocks, {
222
- type: 'text',
223
- text: asrText
224
- }]);
225
- }
226
- },
227
- hasMore: hasMore,
228
- onError: error => {
229
- ty.showToast({
230
- icon: 'error',
231
- title: error.message
232
- });
233
- }
234
- });
235
- }
236
- return /*#__PURE__*/React.createElement(View, {
237
- className: "".concat(props.className || '', " t-agent-message-input"),
238
- style: props.style
239
- }, /*#__PURE__*/React.createElement(View, {
240
- className: "t-agent-message-input-container"
241
- }, props.renderTop, !!uploaded.length && /*#__PURE__*/React.createElement(ScrollView, {
242
- scrollX: true,
243
- scrollY: false,
244
- enableFlex: true,
245
- className: "t-agent-message-input-uploaded-files",
246
- refresherTriggered: false
247
- }, uploaded.map(file => {
248
- let content;
249
- switch (file.type) {
250
- case 'image':
251
- content = /*#__PURE__*/React.createElement(PrivateImage, {
252
- className: "t-agent-message-input-uploaded-file-image",
253
- mode: "aspectFill",
254
- bizType: getStaticResourceBizType(file.url, 'image:view'),
255
- src: file.url
256
- });
257
- break;
258
- case 'video':
259
- content = /*#__PURE__*/React.createElement(PrivateImage, {
260
- bizType: getStaticResourceBizType(file.thumbUrl, 'videoThumb:view'),
261
- className: "t-agent-message-input-uploaded-file-image",
262
- mode: "aspectFill",
263
- src: file.thumbUrl
264
- });
265
- break;
266
- case 'loading':
267
- content = /*#__PURE__*/React.createElement(View, {
268
- className: "t-agent-message-input-uploaded-file-loading",
269
- key: file.id
270
- }, /*#__PURE__*/React.createElement(Image, {
271
- src: loadingSvg,
272
- className: "t-agent-message-input-uploaded-file-loading-icon"
273
- }));
274
- break;
275
- default:
276
- content = null;
277
- }
278
- return /*#__PURE__*/React.createElement(View, {
279
- key: file.id,
280
- className: "t-agent-message-input-uploaded-file"
281
- }, /*#__PURE__*/React.createElement(Image, {
282
- onClick: () => {
283
- setUploaded(prev => prev.filter(f => f.id !== file.id));
284
- },
285
- className: "t-agent-message-input-uploaded-file-delete",
286
- src: closeCircleSvg
287
- }), content);
288
- })), container), /*#__PURE__*/React.createElement(View, {
289
- className: "t-agent-message-input-panel ".concat(moreOpen ? '' : 't-agent-message-input-panel-close')
290
- }, /*#__PURE__*/React.createElement(View, {
291
- className: "t-agent-message-input-panel-content"
292
- }, /*#__PURE__*/React.createElement(Button, {
293
- className: "t-agent-message-input-panel-button",
294
- onClick: async () => {
295
- try {
296
- await upload('image', 1);
297
- } catch (e) {
298
- ty.showToast({
299
- icon: 'error',
300
- title: t('t-agent.input.upload.failed')
301
- });
302
- }
303
- }
304
- }, /*#__PURE__*/React.createElement(View, {
305
- className: "t-agent-message-input-panel-button-icon"
306
- }, /*#__PURE__*/React.createElement(Image, {
307
- src: imageSvg
308
- }))), /*#__PURE__*/React.createElement(Button, {
309
- className: "t-agent-message-input-panel-button",
310
- onClick: async () => {
311
- try {
312
- await upload('video', 1);
313
- } catch (e) {
314
- ty.showToast({
315
- icon: 'error',
316
- title: t('t-agent.input.upload.failed')
317
- });
318
- }
319
- }
320
- }, /*#__PURE__*/React.createElement(View, {
321
- className: "t-agent-message-input-panel-button-icon"
322
- }, /*#__PURE__*/React.createElement(Image, {
323
- src: videoSvg
324
- }))))));
325
- }
@@ -1,37 +0,0 @@
1
- export declare enum AsrErrorCode {
2
- SHORT_TIME = 0
3
- }
4
- export declare class AsrError extends Error {
5
- readonly errorCode: AsrErrorCode;
6
- constructor(errorCode: AsrErrorCode, message?: string);
7
- }
8
- export interface UseAsrInputOptions {
9
- text: string;
10
- setText: (text: string) => void;
11
- onError: (error: Error) => void;
12
- }
13
- /**
14
- init 初始状态,有返回按钮
15
- recording 按住说话按钮收音,无按钮
16
- pending 已有文本,待发送,有清除、发送按钮
17
-
18
- +-------------------------------------------------------+
19
- | clear/send |
20
- ↓ |
21
- +--------+ press +------------+ release +---------+
22
- | init | ----------> | recording | ----------> | pending |
23
- +--------+ +------------+ +---------+
24
- ^ | ^ |
25
- | | | |
26
- | | | |
27
- | empty release | | press |
28
- +-------------------------+ +--------------------------+
29
- */
30
- export declare function useAsrInput(options: UseAsrInputOptions): {
31
- currentText: string;
32
- incomingText: string;
33
- state: "pending" | "recording" | "init";
34
- clear: () => void;
35
- press: () => Promise<void>;
36
- release: () => Promise<void>;
37
- };
@@ -1,131 +0,0 @@
1
- import "core-js/modules/web.dom-collections.iterator.js";
2
- import { useEffect, useRef, useState } from 'react';
3
- import { Asr, AsrDetectResultState } from './asr';
4
- import { useIsUnmounted } from '../../hooks';
5
- export let AsrErrorCode = /*#__PURE__*/function (AsrErrorCode) {
6
- AsrErrorCode[AsrErrorCode["SHORT_TIME"] = 0] = "SHORT_TIME";
7
- return AsrErrorCode;
8
- }({});
9
- export class AsrError extends Error {
10
- constructor(errorCode, message) {
11
- if (!message) {
12
- switch (errorCode) {
13
- case AsrErrorCode.SHORT_TIME:
14
- message = 'Recording time is too short';
15
- break;
16
- default:
17
- message = 'Unknown error';
18
- break;
19
- }
20
- }
21
- super(message);
22
- this.errorCode = errorCode;
23
- }
24
- }
25
- /**
26
- init 初始状态,有返回按钮
27
- recording 按住说话按钮收音,无按钮
28
- pending 已有文本,待发送,有清除、发送按钮
29
-
30
- +-------------------------------------------------------+
31
- | clear/send |
32
- ↓ |
33
- +--------+ press +------------+ release +---------+
34
- | init | ----------> | recording | ----------> | pending |
35
- +--------+ +------------+ +---------+
36
- ^ | ^ |
37
- | | | |
38
- | | | |
39
- | empty release | | press |
40
- +-------------------------+ +--------------------------+
41
- */
42
- export function useAsrInput(options) {
43
- const {
44
- text,
45
- setText,
46
- onError
47
- } = options;
48
- const asrRef = useRef(null);
49
- const [currentText, setCurrentText] = useState('');
50
- const [incomingText, setIncomingText] = useState('');
51
- const isUnmounted = useIsUnmounted();
52
- const [state, setState] = useState('init');
53
- const startAt = useRef(0);
54
- useEffect(() => {
55
- return () => {
56
- if (asrRef.current) {
57
- asrRef.current.stop();
58
- asrRef.current = null;
59
- Asr.dispose();
60
- }
61
- };
62
- }, []);
63
- return {
64
- currentText,
65
- incomingText,
66
- state,
67
- clear: () => {
68
- setState('init');
69
- setText('');
70
- setCurrentText('');
71
- setIncomingText('');
72
- },
73
- press: async () => {
74
- setState('recording');
75
-
76
- // 保存当前文本,用于恢复
77
- const initial = text;
78
-
79
- // 上次识别的结果
80
- let last = '';
81
- setCurrentText(initial);
82
- setIncomingText('');
83
-
84
- // 开始录音时
85
- const asr = Asr.detect(res => {
86
- if (isUnmounted()) {
87
- return;
88
- }
89
- if (res.state === AsrDetectResultState.MID || res.state === AsrDetectResultState.END) {
90
- if (res.text.startsWith(last)) {
91
- const incoming = res.text.slice(last.length);
92
- setIncomingText(incoming);
93
- setCurrentText(initial + last);
94
- last = res.text;
95
- } else {
96
- setIncomingText(res.text);
97
- setCurrentText(initial);
98
- }
99
- setText(initial + res.text);
100
- }
101
- if (res.state === AsrDetectResultState.ERROR) {
102
- onError(new AsrError(res.errorCode));
103
- }
104
- });
105
- try {
106
- await asr.start();
107
- startAt.current = Date.now();
108
- asrRef.current = asr;
109
- } catch (error) {
110
- onError(error);
111
- }
112
- },
113
- release: async () => {
114
- if (state !== 'recording') {
115
- return;
116
- }
117
- if (!text && startAt.current - Date.now() < 400) {
118
- onError(new AsrError(AsrErrorCode.SHORT_TIME));
119
- }
120
- if (text) {
121
- setState('pending');
122
- } else {
123
- setState('init');
124
- }
125
- if (asrRef.current) {
126
- await asrRef.current.stop();
127
- asrRef.current = null;
128
- }
129
- }
130
- };
131
- }