@magmamath/students-features 0.9.110 → 0.9.111-rc-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/features/chatbot/TTS_HIGHLIGHTING.md +109 -0
- package/dist/commonjs/features/chatbot/components/Chat/Chat.js +25 -22
- package/dist/commonjs/features/chatbot/components/Chat/Chat.js.map +1 -1
- package/dist/commonjs/features/chatbot/components/ChatMessage/ChatMessage.js.map +1 -1
- package/dist/commonjs/features/chatbot/components/ChatMessage/MessageContent.js +20 -4
- package/dist/commonjs/features/chatbot/components/ChatMessage/MessageContent.js.map +1 -1
- package/dist/commonjs/features/chatbot/components/ChatMessage/ReceivedMessage.js +6 -2
- package/dist/commonjs/features/chatbot/components/ChatMessage/ReceivedMessage.js.map +1 -1
- package/dist/commonjs/features/chatbot/components/Chatbot.js +6 -2
- package/dist/commonjs/features/chatbot/components/Chatbot.js.map +1 -1
- package/dist/commonjs/features/chatbot/helpers/textToSSML.js +22 -0
- package/dist/commonjs/features/chatbot/helpers/textToSSML.js.map +1 -0
- package/dist/commonjs/features/chatbot/model/ChatBotModel.js +4 -6
- package/dist/commonjs/features/chatbot/model/ChatBotModel.js.map +1 -1
- package/dist/commonjs/features/chatbot/model/t2s.js +125 -32
- package/dist/commonjs/features/chatbot/model/t2s.js.map +1 -1
- package/dist/commonjs/features/chatbot/types/t2s.types.js.map +1 -1
- package/dist/commonjs/index.js +1 -13
- package/dist/commonjs/index.js.map +1 -1
- package/dist/commonjs/types/common.types.js +0 -4
- package/dist/module/features/chatbot/TTS_HIGHLIGHTING.md +109 -0
- package/dist/module/features/chatbot/components/Chat/Chat.js +25 -22
- package/dist/module/features/chatbot/components/Chat/Chat.js.map +1 -1
- package/dist/module/features/chatbot/components/ChatMessage/ChatMessage.js.map +1 -1
- package/dist/module/features/chatbot/components/ChatMessage/MessageContent.js +20 -4
- package/dist/module/features/chatbot/components/ChatMessage/MessageContent.js.map +1 -1
- package/dist/module/features/chatbot/components/ChatMessage/ReceivedMessage.js +6 -2
- package/dist/module/features/chatbot/components/ChatMessage/ReceivedMessage.js.map +1 -1
- package/dist/module/features/chatbot/components/Chatbot.js +6 -2
- package/dist/module/features/chatbot/components/Chatbot.js.map +1 -1
- package/dist/module/features/chatbot/helpers/textToSSML.js +16 -0
- package/dist/module/features/chatbot/helpers/textToSSML.js.map +1 -0
- package/dist/module/features/chatbot/index.js.map +1 -1
- package/dist/module/features/chatbot/model/ChatBotModel.js +4 -6
- package/dist/module/features/chatbot/model/ChatBotModel.js.map +1 -1
- package/dist/module/features/chatbot/model/t2s.js +126 -33
- package/dist/module/features/chatbot/model/t2s.js.map +1 -1
- package/dist/module/features/chatbot/types/t2s.types.js.map +1 -1
- package/dist/module/index.js +0 -1
- package/dist/module/index.js.map +1 -1
- package/dist/module/types/common.types.js +0 -2
- package/dist/typescript/commonjs/features/chatbot/components/Chat/Chat.d.ts +4 -1
- package/dist/typescript/commonjs/features/chatbot/components/Chat/Chat.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/chatbot/components/ChatMessage/ChatMessage.d.ts +3 -1
- package/dist/typescript/commonjs/features/chatbot/components/ChatMessage/ChatMessage.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/chatbot/components/ChatMessage/MessageContent.d.ts +4 -2
- package/dist/typescript/commonjs/features/chatbot/components/ChatMessage/MessageContent.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/chatbot/components/ChatMessage/ReceivedMessage.d.ts +1 -1
- package/dist/typescript/commonjs/features/chatbot/components/ChatMessage/ReceivedMessage.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/chatbot/components/Chatbot.d.ts +4 -1
- package/dist/typescript/commonjs/features/chatbot/components/Chatbot.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/chatbot/helpers/textToSSML.d.ts +7 -0
- package/dist/typescript/commonjs/features/chatbot/helpers/textToSSML.d.ts.map +1 -0
- package/dist/typescript/commonjs/features/chatbot/index.d.ts +1 -1
- package/dist/typescript/commonjs/features/chatbot/index.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/chatbot/model/ChatBotModel.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/chatbot/model/t2s.d.ts +20 -9
- package/dist/typescript/commonjs/features/chatbot/model/t2s.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/chatbot/types/api.types.d.ts +2 -1
- package/dist/typescript/commonjs/features/chatbot/types/api.types.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/chatbot/types/t2s.types.d.ts +10 -0
- package/dist/typescript/commonjs/features/chatbot/types/t2s.types.d.ts.map +1 -1
- package/dist/typescript/commonjs/index.d.ts +0 -1
- package/dist/typescript/commonjs/index.d.ts.map +1 -1
- package/dist/typescript/commonjs/types/common.types.d.ts +0 -2
- package/dist/typescript/commonjs/types/common.types.d.ts.map +1 -1
- package/dist/typescript/module/features/chatbot/components/Chat/Chat.d.ts +4 -1
- package/dist/typescript/module/features/chatbot/components/Chat/Chat.d.ts.map +1 -1
- package/dist/typescript/module/features/chatbot/components/ChatMessage/ChatMessage.d.ts +3 -1
- package/dist/typescript/module/features/chatbot/components/ChatMessage/ChatMessage.d.ts.map +1 -1
- package/dist/typescript/module/features/chatbot/components/ChatMessage/MessageContent.d.ts +4 -2
- package/dist/typescript/module/features/chatbot/components/ChatMessage/MessageContent.d.ts.map +1 -1
- package/dist/typescript/module/features/chatbot/components/ChatMessage/ReceivedMessage.d.ts +1 -1
- package/dist/typescript/module/features/chatbot/components/ChatMessage/ReceivedMessage.d.ts.map +1 -1
- package/dist/typescript/module/features/chatbot/components/Chatbot.d.ts +4 -1
- package/dist/typescript/module/features/chatbot/components/Chatbot.d.ts.map +1 -1
- package/dist/typescript/module/features/chatbot/helpers/textToSSML.d.ts +7 -0
- package/dist/typescript/module/features/chatbot/helpers/textToSSML.d.ts.map +1 -0
- package/dist/typescript/module/features/chatbot/index.d.ts +1 -1
- package/dist/typescript/module/features/chatbot/index.d.ts.map +1 -1
- package/dist/typescript/module/features/chatbot/model/ChatBotModel.d.ts.map +1 -1
- package/dist/typescript/module/features/chatbot/model/t2s.d.ts +20 -9
- package/dist/typescript/module/features/chatbot/model/t2s.d.ts.map +1 -1
- package/dist/typescript/module/features/chatbot/types/api.types.d.ts +2 -1
- package/dist/typescript/module/features/chatbot/types/api.types.d.ts.map +1 -1
- package/dist/typescript/module/features/chatbot/types/t2s.types.d.ts +10 -0
- package/dist/typescript/module/features/chatbot/types/t2s.types.d.ts.map +1 -1
- package/dist/typescript/module/index.d.ts +0 -1
- package/dist/typescript/module/index.d.ts.map +1 -1
- package/dist/typescript/module/types/common.types.d.ts +0 -2
- package/dist/typescript/module/types/common.types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/features/chatbot/TTS_HIGHLIGHTING.md +109 -0
- package/src/features/chatbot/components/Chat/Chat.tsx +28 -19
- package/src/features/chatbot/components/ChatMessage/ChatMessage.tsx +3 -1
- package/src/features/chatbot/components/ChatMessage/MessageContent.tsx +30 -3
- package/src/features/chatbot/components/ChatMessage/ReceivedMessage.tsx +4 -0
- package/src/features/chatbot/components/Chatbot.tsx +7 -0
- package/src/features/chatbot/helpers/textToSSML.ts +15 -0
- package/src/features/chatbot/index.ts +1 -1
- package/src/features/chatbot/model/ChatBotModel.ts +3 -7
- package/src/features/chatbot/model/t2s.ts +147 -32
- package/src/features/chatbot/types/api.types.ts +2 -1
- package/src/features/chatbot/types/t2s.types.ts +7 -0
- package/src/index.ts +0 -1
- package/src/types/common.types.ts +0 -4
- package/dist/commonjs/features/openEnded/components/OpenEndedAttemptsLimit.js +0 -34
- package/dist/commonjs/features/openEnded/components/OpenEndedAttemptsLimit.js.map +0 -1
- package/dist/commonjs/features/openEnded/components/OpenEndedScoreLabel.js +0 -68
- package/dist/commonjs/features/openEnded/components/OpenEndedScoreLabel.js.map +0 -1
- package/dist/commonjs/features/openEnded/index.js +0 -48
- package/dist/commonjs/features/openEnded/index.js.map +0 -1
- package/dist/commonjs/features/openEnded/oe.helpers.js +0 -26
- package/dist/commonjs/features/openEnded/oe.helpers.js.map +0 -1
- package/dist/commonjs/features/openEnded/oe.types.js +0 -14
- package/dist/commonjs/features/openEnded/oe.types.js.map +0 -1
- package/dist/commonjs/shared/common.constants.js +0 -9
- package/dist/commonjs/shared/common.constants.js.map +0 -1
- package/dist/module/features/openEnded/components/OpenEndedAttemptsLimit.js +0 -28
- package/dist/module/features/openEnded/components/OpenEndedAttemptsLimit.js.map +0 -1
- package/dist/module/features/openEnded/components/OpenEndedScoreLabel.js +0 -62
- package/dist/module/features/openEnded/components/OpenEndedScoreLabel.js.map +0 -1
- package/dist/module/features/openEnded/index.js +0 -7
- package/dist/module/features/openEnded/index.js.map +0 -1
- package/dist/module/features/openEnded/oe.helpers.js +0 -21
- package/dist/module/features/openEnded/oe.helpers.js.map +0 -1
- package/dist/module/features/openEnded/oe.types.js +0 -10
- package/dist/module/features/openEnded/oe.types.js.map +0 -1
- package/dist/module/shared/common.constants.js +0 -5
- package/dist/module/shared/common.constants.js.map +0 -1
- package/dist/typescript/commonjs/features/openEnded/components/OpenEndedAttemptsLimit.d.ts +0 -7
- package/dist/typescript/commonjs/features/openEnded/components/OpenEndedAttemptsLimit.d.ts.map +0 -1
- package/dist/typescript/commonjs/features/openEnded/components/OpenEndedScoreLabel.d.ts +0 -12
- package/dist/typescript/commonjs/features/openEnded/components/OpenEndedScoreLabel.d.ts.map +0 -1
- package/dist/typescript/commonjs/features/openEnded/index.d.ts +0 -5
- package/dist/typescript/commonjs/features/openEnded/index.d.ts.map +0 -1
- package/dist/typescript/commonjs/features/openEnded/oe.helpers.d.ts +0 -13
- package/dist/typescript/commonjs/features/openEnded/oe.helpers.d.ts.map +0 -1
- package/dist/typescript/commonjs/features/openEnded/oe.types.d.ts +0 -7
- package/dist/typescript/commonjs/features/openEnded/oe.types.d.ts.map +0 -1
- package/dist/typescript/commonjs/shared/common.constants.d.ts +0 -2
- package/dist/typescript/commonjs/shared/common.constants.d.ts.map +0 -1
- package/dist/typescript/module/features/openEnded/components/OpenEndedAttemptsLimit.d.ts +0 -7
- package/dist/typescript/module/features/openEnded/components/OpenEndedAttemptsLimit.d.ts.map +0 -1
- package/dist/typescript/module/features/openEnded/components/OpenEndedScoreLabel.d.ts +0 -12
- package/dist/typescript/module/features/openEnded/components/OpenEndedScoreLabel.d.ts.map +0 -1
- package/dist/typescript/module/features/openEnded/index.d.ts +0 -5
- package/dist/typescript/module/features/openEnded/index.d.ts.map +0 -1
- package/dist/typescript/module/features/openEnded/oe.helpers.d.ts +0 -13
- package/dist/typescript/module/features/openEnded/oe.helpers.d.ts.map +0 -1
- package/dist/typescript/module/features/openEnded/oe.types.d.ts +0 -7
- package/dist/typescript/module/features/openEnded/oe.types.d.ts.map +0 -1
- package/dist/typescript/module/shared/common.constants.d.ts +0 -2
- package/dist/typescript/module/shared/common.constants.d.ts.map +0 -1
- package/src/features/openEnded/components/OpenEndedAttemptsLimit.tsx +0 -26
- package/src/features/openEnded/components/OpenEndedScoreLabel.tsx +0 -68
- package/src/features/openEnded/index.ts +0 -4
- package/src/features/openEnded/oe.helpers.ts +0 -31
- package/src/features/openEnded/oe.types.ts +0 -6
- package/src/shared/common.constants.ts +0 -3
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# TTS Word-by-Word Highlighting
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Word-by-word text highlighting synchronized with text-to-speech playback in the chatbot. The feature is optional and enabled via props.
|
|
6
|
+
|
|
7
|
+
## How It Works
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
11
|
+
│ 1. User clicks TTS button on a message │
|
|
12
|
+
│ ↓ │
|
|
13
|
+
│ 2. Text is converted to SSML with word marks │
|
|
14
|
+
│ "Hello world" → <speak><mark name="w_0"/>Hello │
|
|
15
|
+
│ <mark name="w_1"/>world</speak> │
|
|
16
|
+
│ ↓ │
|
|
17
|
+
│ 3. Google TTS API returns audio + timepoints │
|
|
18
|
+
│ [{ markName: "w_0", timeSeconds: 0.0 }, │
|
|
19
|
+
│ { markName: "w_1", timeSeconds: 0.5 }] │
|
|
20
|
+
│ ↓ │
|
|
21
|
+
│ 4. Audio plays, ontimeupdate fires (~4x/sec) │
|
|
22
|
+
│ ↓ │
|
|
23
|
+
│ 5. Current time → find matching timepoint → word index │
|
|
24
|
+
│ ↓ │
|
|
25
|
+
│ 6. $highlightedWordIndex updates → UI re-renders highlighted │
|
|
26
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
// In students-web model setup
|
|
33
|
+
chatModel.bot.setUpT2S({
|
|
34
|
+
audioProvider: createT2SAudioProvider(),
|
|
35
|
+
textSanitizer: removeKatexPartFromTextToSpeech,
|
|
36
|
+
isHighlightEnabled: true, // Enable highlighting
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// In component
|
|
40
|
+
<Chatbot
|
|
41
|
+
model={chatModel.bot}
|
|
42
|
+
isHighlightEnabled={true}
|
|
43
|
+
highlightConfig={{
|
|
44
|
+
MessageRenderer: ChatbotHighlightedMessage,
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Files Modified
|
|
50
|
+
|
|
51
|
+
### students-features
|
|
52
|
+
|
|
53
|
+
| File | Changes |
|
|
54
|
+
|------|---------|
|
|
55
|
+
| `types/t2s.types.ts` | Added `onTimeUpdate`, `getCurrentTime` to AudioProvider; added `HighlightConfig` type |
|
|
56
|
+
| `types/api.types.ts` | Added `ssml` option to GoogleText2SpeechPayload input |
|
|
57
|
+
| `helpers/textToSSML.ts` | NEW - Converts text to SSML with word marks |
|
|
58
|
+
| `model/t2s.ts` | Added `$highlightedWordIndex`, `$timepoints`, `$time` stores; SSML/timepoint handling |
|
|
59
|
+
| `components/Chatbot.tsx` | Added `isHighlightEnabled`, `highlightConfig` props |
|
|
60
|
+
| `components/Chat/Chat.tsx` | Passes highlight state to ChatMessage |
|
|
61
|
+
| `components/ChatMessage/*` | Passes highlight props through component chain |
|
|
62
|
+
| `components/ChatMessage/MessageContent.tsx` | Renders custom MessageRenderer when provided |
|
|
63
|
+
|
|
64
|
+
### students-web
|
|
65
|
+
|
|
66
|
+
| File | Changes |
|
|
67
|
+
|------|---------|
|
|
68
|
+
| `chatbot/helpers.ts` | Added `onTimeUpdate`, `getCurrentTime` to AudioProvider |
|
|
69
|
+
| `chatbot/components/ChatbotHighlightedMessage.tsx` | NEW - Renders words with highlight styling |
|
|
70
|
+
| `chatbot/components/ChatbotHighlightedMessage.module.scss` | NEW - Yellow highlight styles |
|
|
71
|
+
| `chatbot/model/model.ts` | Enabled `isHighlightEnabled: true` |
|
|
72
|
+
| `PracticeMode/chatbot/PracticeChatbot.tsx` | Passes highlightConfig with MessageRenderer |
|
|
73
|
+
|
|
74
|
+
## Key Components
|
|
75
|
+
|
|
76
|
+
### textToSSML (students-features)
|
|
77
|
+
```ts
|
|
78
|
+
textToSSML("Hello world")
|
|
79
|
+
// → '<speak><mark name="w_0"/>Hello <mark name="w_1"/>world</speak>'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### $highlightedWordIndex (students-features)
|
|
83
|
+
Effector store that computes current word index from timepoints + playback time.
|
|
84
|
+
|
|
85
|
+
### ChatbotHighlightedMessage (students-web)
|
|
86
|
+
```tsx
|
|
87
|
+
// Splits text, highlights word at highlightedIndex
|
|
88
|
+
<span className={isHighlighted ? styles.Highlighted : undefined}>
|
|
89
|
+
{word}
|
|
90
|
+
</span>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Data Flow
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
AudioProvider.onTimeUpdate(time)
|
|
97
|
+
↓
|
|
98
|
+
t2s.setTime(time)
|
|
99
|
+
↓
|
|
100
|
+
$highlightedWordIndex = findCurrentMark(timepoints, time) → parse index
|
|
101
|
+
↓
|
|
102
|
+
Chat.tsx: useUnit(model.t2s.$highlightedWordIndex)
|
|
103
|
+
↓
|
|
104
|
+
ChatMessage → ReceivedMessage → MessageContent
|
|
105
|
+
↓
|
|
106
|
+
MessageRenderer({ text, highlightedIndex })
|
|
107
|
+
↓
|
|
108
|
+
ChatbotHighlightedMessage renders highlighted word
|
|
109
|
+
```
|
|
@@ -9,6 +9,7 @@ import { ChatInput } from '../ChatInput/ChatInput'
|
|
|
9
9
|
import { MessageVariant } from '../../types/model.types'
|
|
10
10
|
import { ColorScheme } from '../../types/style.types'
|
|
11
11
|
import { ConditionalWrapper } from '../../../../lib/components/ConditionalWrapper'
|
|
12
|
+
import { HighlightConfig } from '../../types/t2s.types'
|
|
12
13
|
|
|
13
14
|
const TOOLBAR_HEIGHT = 140
|
|
14
15
|
const BOTTOM_OFFSET = Platform.select({
|
|
@@ -23,6 +24,8 @@ type ChatProps = {
|
|
|
23
24
|
isInputDisabled?: boolean
|
|
24
25
|
colorScheme?: ColorScheme
|
|
25
26
|
MessageWrapper?: ComponentType<PropsWithChildren>
|
|
27
|
+
isHighlightEnabled?: boolean
|
|
28
|
+
highlightConfig?: HighlightConfig
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
export const Chat = ({
|
|
@@ -32,6 +35,8 @@ export const Chat = ({
|
|
|
32
35
|
isInputDisabled,
|
|
33
36
|
colorScheme = ColorScheme.Blue,
|
|
34
37
|
MessageWrapper,
|
|
38
|
+
isHighlightEnabled = false,
|
|
39
|
+
highlightConfig,
|
|
35
40
|
}: ChatProps) => {
|
|
36
41
|
const dimensions = useWindowDimensions()
|
|
37
42
|
const ref = useRef<ScrollView>(null)
|
|
@@ -44,6 +49,7 @@ export const Chat = ({
|
|
|
44
49
|
const isTranslated = useUnit(model.translation.$isTranslated)
|
|
45
50
|
const audioStatus = useUnit(model.t2s.$status)
|
|
46
51
|
const errorSpottingEnabled = useUnit(model.errorSpotting.$isEnabled)
|
|
52
|
+
const highlightedWordIndex = useUnit(model.t2s.$highlightedWordIndex)
|
|
47
53
|
|
|
48
54
|
const messages = useStoreMap(model.$currentChatData, ({ messages }) => messages)
|
|
49
55
|
const lastMessageId = messages.at(-1)?.id
|
|
@@ -68,25 +74,28 @@ export const Chat = ({
|
|
|
68
74
|
{isOpen && (
|
|
69
75
|
<View style={[styles.gap, isInputDisabled && styles.messagesWithoutInput]}>
|
|
70
76
|
{messages.map((message) => (
|
|
71
|
-
<
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
77
|
+
<ConditionalWrapper withWrapper={shouldWrapMessage} Wrapper={MessageWrapper}>
|
|
78
|
+
<ChatMessage
|
|
79
|
+
key={message.id}
|
|
80
|
+
isTranslated={isTranslated}
|
|
81
|
+
withIcon={!message.isOwnMessage && message.id === lastMessageId}
|
|
82
|
+
message={message}
|
|
83
|
+
variant={message.isOwnMessage ? MessageVariant.SENT : MessageVariant.RECEIVED}
|
|
84
|
+
colorScheme={colorScheme}
|
|
85
|
+
withErrorSpotting={errorSpottingEnabled && !message.isOwnMessage}
|
|
86
|
+
t2sState={{
|
|
87
|
+
isEnabled: isTextToSpeechEnabled && !message.isOwnMessage,
|
|
88
|
+
isActive: message.id === currentPlayingId,
|
|
89
|
+
isLoading: isTextToSpeechPending,
|
|
90
|
+
status: audioStatus,
|
|
91
|
+
disabled: isTextToSpeechPending,
|
|
92
|
+
}}
|
|
93
|
+
highlightedWordIndex={
|
|
94
|
+
isHighlightEnabled && message.id === currentPlayingId ? highlightedWordIndex : null
|
|
95
|
+
}
|
|
96
|
+
highlightConfig={highlightConfig}
|
|
97
|
+
/>
|
|
98
|
+
</ConditionalWrapper>
|
|
90
99
|
))}
|
|
91
100
|
|
|
92
101
|
{isHelpRequestPending && <MessageLoader colorScheme={colorScheme} />}
|
|
@@ -2,7 +2,7 @@ import React, { ComponentType } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { ReceivedMessage } from './ReceivedMessage'
|
|
4
4
|
import { SentMessage } from './SentMessage'
|
|
5
|
-
import { T2SState } from '../../types/t2s.types'
|
|
5
|
+
import { HighlightConfig, T2SState } from '../../types/t2s.types'
|
|
6
6
|
import { MessageVariant } from '../../types/model.types'
|
|
7
7
|
import { ConversationMessage } from '../../types/units.types'
|
|
8
8
|
import { ColorScheme } from '../../types/style.types'
|
|
@@ -15,6 +15,8 @@ type ChatMessageProps = {
|
|
|
15
15
|
withErrorSpotting?: boolean
|
|
16
16
|
withIcon?: boolean
|
|
17
17
|
colorScheme?: ColorScheme
|
|
18
|
+
highlightedWordIndex?: number | null
|
|
19
|
+
highlightConfig?: HighlightConfig
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export type VariantProps = Omit<ChatMessageProps, 'variant'>
|
|
@@ -2,7 +2,7 @@ import React from 'react'
|
|
|
2
2
|
import { Platform, StyleSheet, Text, View } from 'react-native'
|
|
3
3
|
|
|
4
4
|
import { ConversationMessage } from '../../types/units.types'
|
|
5
|
-
import { T2SState } from '../../types/t2s.types'
|
|
5
|
+
import { HighlightConfig, T2SState } from '../../types/t2s.types'
|
|
6
6
|
import { ColorScheme } from '../../types/style.types'
|
|
7
7
|
|
|
8
8
|
import { MessageButtonsBlock } from './MessageButtonsBlock'
|
|
@@ -16,6 +16,8 @@ type MessageContentProps = {
|
|
|
16
16
|
variant: MessageVariant
|
|
17
17
|
colorScheme?: ColorScheme
|
|
18
18
|
withErrorSpotting?: boolean
|
|
19
|
+
highlightedWordIndex?: number | null
|
|
20
|
+
highlightConfig?: HighlightConfig
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
const MessageContentWeb = ({
|
|
@@ -25,10 +27,23 @@ const MessageContentWeb = ({
|
|
|
25
27
|
variant,
|
|
26
28
|
colorScheme = ColorScheme.Blue,
|
|
27
29
|
withErrorSpotting,
|
|
30
|
+
highlightedWordIndex,
|
|
31
|
+
highlightConfig,
|
|
28
32
|
}: MessageContentProps) => {
|
|
33
|
+
const MessageRenderer = highlightConfig?.MessageRenderer
|
|
34
|
+
// console.log('[TTS Debug] MessageContent - hasRenderer:', !!MessageRenderer, 'highlightedWordIndex:', highlightedWordIndex)
|
|
35
|
+
|
|
29
36
|
return (
|
|
30
37
|
<Text style={styles.messageText}>
|
|
31
|
-
{
|
|
38
|
+
{MessageRenderer ? (
|
|
39
|
+
<MessageRenderer
|
|
40
|
+
text={text}
|
|
41
|
+
highlightedIndex={highlightedWordIndex ?? null}
|
|
42
|
+
colorScheme={colorScheme}
|
|
43
|
+
/>
|
|
44
|
+
) : (
|
|
45
|
+
text
|
|
46
|
+
)}
|
|
32
47
|
<MessageButtonsBlock
|
|
33
48
|
message={message}
|
|
34
49
|
t2sState={t2sState}
|
|
@@ -47,11 +62,23 @@ const MessageContentMobile = ({
|
|
|
47
62
|
variant,
|
|
48
63
|
colorScheme = ColorScheme.Blue,
|
|
49
64
|
withErrorSpotting,
|
|
65
|
+
highlightedWordIndex,
|
|
66
|
+
highlightConfig,
|
|
50
67
|
}: MessageContentProps) => {
|
|
68
|
+
const MessageRenderer = highlightConfig?.MessageRenderer
|
|
69
|
+
|
|
51
70
|
return (
|
|
52
71
|
<>
|
|
53
72
|
<Text style={[styles.messageText, styles.mobileMessageText]}>
|
|
54
|
-
{
|
|
73
|
+
{MessageRenderer ? (
|
|
74
|
+
<MessageRenderer
|
|
75
|
+
text={text}
|
|
76
|
+
highlightedIndex={highlightedWordIndex ?? null}
|
|
77
|
+
colorScheme={colorScheme}
|
|
78
|
+
/>
|
|
79
|
+
) : (
|
|
80
|
+
text
|
|
81
|
+
)}
|
|
55
82
|
<View style={{ width: getSpacerSize(message, t2sState) }} />
|
|
56
83
|
</Text>
|
|
57
84
|
<MessageButtonsBlock
|
|
@@ -14,6 +14,8 @@ export const ReceivedMessage = ({
|
|
|
14
14
|
withIcon,
|
|
15
15
|
colorScheme = ColorScheme.Blue,
|
|
16
16
|
withErrorSpotting,
|
|
17
|
+
highlightedWordIndex,
|
|
18
|
+
highlightConfig,
|
|
17
19
|
}: VariantProps) => {
|
|
18
20
|
const text = isTranslated ? message.translatedMessage : message.message
|
|
19
21
|
|
|
@@ -33,6 +35,8 @@ export const ReceivedMessage = ({
|
|
|
33
35
|
text={text ?? ''}
|
|
34
36
|
colorScheme={colorScheme}
|
|
35
37
|
withErrorSpotting={withErrorSpotting}
|
|
38
|
+
highlightedWordIndex={highlightedWordIndex}
|
|
39
|
+
highlightConfig={highlightConfig}
|
|
36
40
|
/>
|
|
37
41
|
</View>
|
|
38
42
|
</View>
|
|
@@ -7,6 +7,7 @@ import { ChatControls } from './ChatControls/ChatControls'
|
|
|
7
7
|
import { AUDIO_CONFIG, DEFAULT_VOICE } from '../constants'
|
|
8
8
|
import { ChatBotModelContext } from '../context/ChatBotModelContext'
|
|
9
9
|
import { ColorScheme } from '../types/style.types'
|
|
10
|
+
import { HighlightConfig } from '../types/t2s.types'
|
|
10
11
|
|
|
11
12
|
type ChatbotProps = {
|
|
12
13
|
model: ChatbotModel
|
|
@@ -20,6 +21,8 @@ type ChatbotProps = {
|
|
|
20
21
|
iconSize?: { width: number; height: number } // TODO: use number instead
|
|
21
22
|
isInputDisabled?: boolean
|
|
22
23
|
colorScheme?: ColorScheme
|
|
24
|
+
isHighlightEnabled?: boolean
|
|
25
|
+
highlightConfig?: HighlightConfig
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
export const Chatbot = ({
|
|
@@ -31,6 +34,8 @@ export const Chatbot = ({
|
|
|
31
34
|
ButtonWrapper,
|
|
32
35
|
iconSize,
|
|
33
36
|
colorScheme = ColorScheme.Blue,
|
|
37
|
+
isHighlightEnabled = false,
|
|
38
|
+
highlightConfig,
|
|
34
39
|
}: ChatbotProps) => {
|
|
35
40
|
const isChatAllowed = useUnit(model.$chatAccess)
|
|
36
41
|
const isAvailable = useStoreMap(model.$currentChatData, ({ isAvailable }) => isAvailable)
|
|
@@ -60,6 +65,8 @@ export const Chatbot = ({
|
|
|
60
65
|
isTextToSpeechEnabled={isTextToSpeechEnabled}
|
|
61
66
|
isInputDisabled={isInputDisabled}
|
|
62
67
|
MessageWrapper={ButtonWrapper}
|
|
68
|
+
isHighlightEnabled={isHighlightEnabled}
|
|
69
|
+
highlightConfig={highlightConfig}
|
|
63
70
|
/>
|
|
64
71
|
|
|
65
72
|
<ChatControls
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts plain text to SSML with word marks for TTS highlighting.
|
|
3
|
+
* "Hello world" -> '<speak><mark name="w_0"/>Hello <mark name="w_1"/>world</speak>'
|
|
4
|
+
*/
|
|
5
|
+
export const textToSSML = (text: string): string => {
|
|
6
|
+
const words = text.split(/\s+/).filter(Boolean)
|
|
7
|
+
if (words.length === 0) return '<speak></speak>'
|
|
8
|
+
|
|
9
|
+
const ssmlParts = words.map((word, i) => `<mark name="w_${i}"/>${word}`)
|
|
10
|
+
return `<speak>${ssmlParts.join(' ')}</speak>`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const getWordCount = (text: string): number => {
|
|
14
|
+
return text.split(/\s+/).filter(Boolean).length
|
|
15
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Chatbot } from './components/Chatbot'
|
|
2
2
|
export { ChatbotModel } from './model/ChatBotModel'
|
|
3
|
-
export { AudioStatus, type AudioProvider } from './types/t2s.types'
|
|
3
|
+
export { AudioStatus, type AudioProvider, type HighlightConfig } from './types/t2s.types'
|
|
4
4
|
export {
|
|
5
5
|
MessageVariant,
|
|
6
6
|
type ChatbotModelProps,
|
|
@@ -86,7 +86,6 @@ export class ChatbotModel {
|
|
|
86
86
|
this.api = new ChatbotAPI({ ...api, postMessage: this.threadManager.postMessage })
|
|
87
87
|
|
|
88
88
|
this.$chatAccess = chatAccess
|
|
89
|
-
if ($key) this.bindKey($key)
|
|
90
89
|
|
|
91
90
|
this.threadManager.setApi({ getHint: api.requestHint, sendMessage: api.sendMessage })
|
|
92
91
|
if (threadMessagesLimit) {
|
|
@@ -98,6 +97,7 @@ export class ChatbotModel {
|
|
|
98
97
|
.on(this.setAlternatives, (_, payload) => transformAlternatives(payload))
|
|
99
98
|
.reset(this.resetAlternatives)
|
|
100
99
|
|
|
100
|
+
this.t2s = new ChatTextToSpeechModel({ api: this.api })
|
|
101
101
|
this.translation = new ChatbotTranslation({
|
|
102
102
|
$context: this.$context,
|
|
103
103
|
defaultMessages: this.defaultMessages,
|
|
@@ -110,16 +110,12 @@ export class ChatbotModel {
|
|
|
110
110
|
chats: this.cache,
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
-
this.t2s = new ChatTextToSpeechModel({
|
|
114
|
-
api: this.api,
|
|
115
|
-
$currentChatData: this.$currentChatData,
|
|
116
|
-
$isTranslated: this.translation.$isTranslated,
|
|
117
|
-
})
|
|
118
|
-
|
|
119
113
|
this.errorSpotting = new ErrorSpottingModel({
|
|
120
114
|
$currentChatData: this.$currentChatData,
|
|
121
115
|
})
|
|
122
116
|
|
|
117
|
+
if ($key) this.bindKey($key)
|
|
118
|
+
|
|
123
119
|
sample({
|
|
124
120
|
source: this.$isOpen.updates,
|
|
125
121
|
filter: (isOpen) => !isOpen,
|
|
@@ -1,18 +1,29 @@
|
|
|
1
|
-
import { restore, createEvent, createEffect,
|
|
1
|
+
import { restore, createEvent, createEffect, combine } from 'effector'
|
|
2
2
|
import { AUDIO_CONFIG, DEFAULT_VOICE } from '../constants'
|
|
3
3
|
import { AudioProvider, AudioStatus, PlayTextToSpeechProps } from '../types/t2s.types'
|
|
4
4
|
import { ChatbotAPI } from './ChatbotApi'
|
|
5
|
-
import {
|
|
5
|
+
import { Timepoint } from '../types/api.types'
|
|
6
|
+
import { textToSSML } from '../helpers/textToSSML'
|
|
7
|
+
|
|
8
|
+
const WORD_MARK_REGEX = /^w_(\d+)$/
|
|
9
|
+
|
|
10
|
+
const findCurrentMark = (timepoints: Timepoint[], time: number): string | undefined => {
|
|
11
|
+
for (let i = timepoints.length - 1; i >= 0; i--) {
|
|
12
|
+
if (timepoints[i].timeSeconds <= time) {
|
|
13
|
+
return timepoints[i].markName
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
6
18
|
|
|
7
19
|
type ChatTextToSpeechModelProps = {
|
|
8
20
|
api: ChatbotAPI
|
|
9
|
-
$currentChatData: Store<ChatItem>
|
|
10
|
-
$isTranslated: Store<boolean>
|
|
11
21
|
}
|
|
12
22
|
|
|
13
23
|
export type T2SConfig = {
|
|
14
24
|
audioProvider?: AudioProvider
|
|
15
25
|
textSanitizer?: (speech: string) => string
|
|
26
|
+
isHighlightEnabled?: boolean
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
type AudioConfig = {
|
|
@@ -20,34 +31,70 @@ type AudioConfig = {
|
|
|
20
31
|
audioConfig?: typeof AUDIO_CONFIG
|
|
21
32
|
}
|
|
22
33
|
|
|
34
|
+
type HighlightCacheEntry = {
|
|
35
|
+
audioPath: string
|
|
36
|
+
timepoints: Timepoint[]
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
export class ChatTextToSpeechModel {
|
|
24
40
|
private readonly api: ChatbotAPI
|
|
25
41
|
private readonly audioCache: Record<string, string> = {}
|
|
26
|
-
private readonly
|
|
27
|
-
private readonly $isTranslated
|
|
42
|
+
private readonly highlightCache: Record<string, HighlightCacheEntry> = {}
|
|
28
43
|
private audioProvider: AudioProvider | null = null
|
|
44
|
+
private isHighlightEnabled = false
|
|
45
|
+
private lastHighlightedIndex: number | null = null
|
|
29
46
|
|
|
30
47
|
readonly setCurrentPlayingId = createEvent<string | null>()
|
|
31
48
|
readonly $currentPlayingId = restore(this.setCurrentPlayingId, null)
|
|
32
49
|
readonly setStatus = createEvent<AudioStatus>()
|
|
33
50
|
readonly $status = restore(this.setStatus, AudioStatus.STOPPED)
|
|
34
51
|
|
|
52
|
+
readonly setTimepoints = createEvent<Timepoint[]>()
|
|
53
|
+
readonly $timepoints = restore(this.setTimepoints, [])
|
|
54
|
+
|
|
55
|
+
readonly setTime = createEvent<number>()
|
|
56
|
+
readonly $time = restore(this.setTime, 0)
|
|
57
|
+
|
|
58
|
+
readonly $highlightedWordIndex = combine(
|
|
59
|
+
this.$timepoints,
|
|
60
|
+
this.$time,
|
|
61
|
+
this.$status,
|
|
62
|
+
(timepoints, time, status) => {
|
|
63
|
+
if (!timepoints.length || status !== AudioStatus.PLAYING) {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const currentMark = findCurrentMark(timepoints, time)
|
|
68
|
+
if (!currentMark) return null
|
|
69
|
+
|
|
70
|
+
const match = currentMark.match(WORD_MARK_REGEX)
|
|
71
|
+
const index = match ? parseInt(match[1], 10) : null
|
|
72
|
+
// console.log('[TTS Debug] Computed highlightedWordIndex:', index, '(time:', time, ', mark:', currentMark, ')')
|
|
73
|
+
return index
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
readonly $highlightState = combine({
|
|
78
|
+
highlightedWordIndex: this.$highlightedWordIndex,
|
|
79
|
+
playingId: this.$currentPlayingId,
|
|
80
|
+
status: this.$status,
|
|
81
|
+
})
|
|
82
|
+
|
|
35
83
|
private audioConfig: AudioConfig | null = null
|
|
36
84
|
private textSanitizer = (value: string) => value
|
|
37
85
|
|
|
38
|
-
constructor({ api
|
|
86
|
+
constructor({ api }: ChatTextToSpeechModelProps) {
|
|
39
87
|
this.api = api
|
|
40
|
-
this.$currentChatData = $currentChatData
|
|
41
|
-
this.$isTranslated = $isTranslated
|
|
42
88
|
}
|
|
43
89
|
|
|
44
90
|
public setAudioConfig(config: AudioConfig | null) {
|
|
45
91
|
this.audioConfig = config
|
|
46
92
|
}
|
|
47
93
|
|
|
48
|
-
public setConfig({ audioProvider, textSanitizer }: T2SConfig) {
|
|
94
|
+
public setConfig({ audioProvider, textSanitizer, isHighlightEnabled }: T2SConfig) {
|
|
49
95
|
if (audioProvider) this.audioProvider = audioProvider
|
|
50
96
|
if (textSanitizer) this.textSanitizer = textSanitizer
|
|
97
|
+
if (isHighlightEnabled !== undefined) this.isHighlightEnabled = isHighlightEnabled
|
|
51
98
|
}
|
|
52
99
|
|
|
53
100
|
private getCacheKey = (messageId: string, isTranslated: boolean) => {
|
|
@@ -81,35 +128,70 @@ export class ChatTextToSpeechModel {
|
|
|
81
128
|
|
|
82
129
|
if (messageId && !isCurrentMessage) {
|
|
83
130
|
this.audioProvider.stop()
|
|
131
|
+
this.resetHighlight()
|
|
84
132
|
}
|
|
85
133
|
|
|
86
134
|
const cacheKey = this.getCacheKey(messageId, isTranslated)
|
|
87
|
-
|
|
135
|
+
const useHighlighting = this.isHighlightEnabled && !!this.audioProvider.onTimeUpdate
|
|
136
|
+
// console.log('[TTS Debug] useHighlighting:', useHighlighting, '(isHighlightEnabled:', this.isHighlightEnabled, ', hasOnTimeUpdate:', !!this.audioProvider.onTimeUpdate, ')')
|
|
137
|
+
|
|
138
|
+
let audioPath: string
|
|
139
|
+
let timepoints: Timepoint[] = []
|
|
140
|
+
|
|
88
141
|
this.setCurrentPlayingId(messageId)
|
|
89
142
|
|
|
90
|
-
if (
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
audioConfig: config?.audioConfig ?? AUDIO_CONFIG,
|
|
96
|
-
enableTimePointing: [],
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
// Response might be different type based on provided AUDIO_CONFIG
|
|
100
|
-
if (typeof response.audioContent === 'string') {
|
|
101
|
-
audioPath = `data:audio/mpeg;base64,${response.audioContent}`
|
|
143
|
+
if (useHighlighting) {
|
|
144
|
+
const cached = this.highlightCache[cacheKey]
|
|
145
|
+
if (cached) {
|
|
146
|
+
audioPath = cached.audioPath
|
|
147
|
+
timepoints = cached.timepoints
|
|
102
148
|
} else {
|
|
103
|
-
|
|
149
|
+
const config = this.audioConfig
|
|
150
|
+
const ssml = textToSSML(text)
|
|
151
|
+
const response = await this.api.textToSpeechTextFx({
|
|
152
|
+
input: { ssml },
|
|
153
|
+
voice: config?.voice ?? DEFAULT_VOICE,
|
|
154
|
+
audioConfig: config?.audioConfig ?? AUDIO_CONFIG,
|
|
155
|
+
enableTimePointing: ['SSML_MARK'],
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
if (typeof response.audioContent === 'string') {
|
|
159
|
+
audioPath = `data:audio/mpeg;base64,${response.audioContent}`
|
|
160
|
+
} else {
|
|
161
|
+
audioPath = response?.audioContent ?? ''
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
timepoints = response.timepoints ?? []
|
|
165
|
+
this.highlightCache[cacheKey] = { audioPath, timepoints }
|
|
104
166
|
}
|
|
105
167
|
|
|
106
|
-
this.
|
|
168
|
+
this.setTimepoints(timepoints)
|
|
169
|
+
// console.log('[TTS Debug] Timepoints set:', timepoints.length, timepoints)
|
|
170
|
+
this.setupTimeTracking()
|
|
171
|
+
} else {
|
|
172
|
+
audioPath = this.audioCache[cacheKey]
|
|
173
|
+
if (!audioPath) {
|
|
174
|
+
const config = this.audioConfig
|
|
175
|
+
const response = await this.api.textToSpeechTextFx({
|
|
176
|
+
input: { text },
|
|
177
|
+
voice: config?.voice ?? DEFAULT_VOICE,
|
|
178
|
+
audioConfig: config?.audioConfig ?? AUDIO_CONFIG,
|
|
179
|
+
enableTimePointing: [],
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
if (typeof response.audioContent === 'string') {
|
|
183
|
+
audioPath = `data:audio/mpeg;base64,${response.audioContent}`
|
|
184
|
+
} else {
|
|
185
|
+
audioPath = response?.audioContent ?? ''
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.audioCache[cacheKey] = audioPath
|
|
189
|
+
}
|
|
107
190
|
}
|
|
108
191
|
|
|
109
192
|
this.setStatus(AudioStatus.PLAYING)
|
|
110
193
|
await this.audioProvider.play(audioPath, () => {
|
|
111
|
-
this.
|
|
112
|
-
this.setCurrentPlayingId(null)
|
|
194
|
+
this.onPlaybackComplete()
|
|
113
195
|
})
|
|
114
196
|
} catch (error) {
|
|
115
197
|
this.reset()
|
|
@@ -117,12 +199,43 @@ export class ChatTextToSpeechModel {
|
|
|
117
199
|
}
|
|
118
200
|
}
|
|
119
201
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
202
|
+
private setupTimeTracking() {
|
|
203
|
+
if (!this.audioProvider?.onTimeUpdate) {
|
|
204
|
+
// console.log('[TTS Debug] No onTimeUpdate on audioProvider!')
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// console.log('[TTS Debug] Setting up time tracking')
|
|
209
|
+
this.lastHighlightedIndex = null
|
|
210
|
+
|
|
211
|
+
this.audioProvider.onTimeUpdate((time: number) => {
|
|
212
|
+
const timepoints = this.$timepoints.getState()
|
|
213
|
+
const currentMark = findCurrentMark(timepoints, time)
|
|
214
|
+
if (!currentMark) return
|
|
215
|
+
|
|
216
|
+
const match = currentMark.match(WORD_MARK_REGEX)
|
|
217
|
+
const newIndex = match ? parseInt(match[1], 10) : null
|
|
218
|
+
|
|
219
|
+
// Only update if word index changed
|
|
220
|
+
if (newIndex !== this.lastHighlightedIndex) {
|
|
221
|
+
// console.log('[TTS Debug] Word changed:', this.lastHighlightedIndex, '→', newIndex)
|
|
222
|
+
this.lastHighlightedIndex = newIndex
|
|
223
|
+
this.setTime(time)
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private onPlaybackComplete = () => {
|
|
229
|
+
this.setStatus(AudioStatus.STOPPED)
|
|
230
|
+
this.setCurrentPlayingId(null)
|
|
231
|
+
this.resetHighlight()
|
|
232
|
+
this.audioProvider?.onTimeUpdate?.(null)
|
|
233
|
+
}
|
|
123
234
|
|
|
124
|
-
|
|
125
|
-
|
|
235
|
+
private resetHighlight() {
|
|
236
|
+
this.setTimepoints([])
|
|
237
|
+
this.setTime(0)
|
|
238
|
+
this.lastHighlightedIndex = null
|
|
126
239
|
}
|
|
127
240
|
|
|
128
241
|
public stop() {
|
|
@@ -134,5 +247,7 @@ export class ChatTextToSpeechModel {
|
|
|
134
247
|
this.stop()
|
|
135
248
|
this.setStatus(AudioStatus.STOPPED)
|
|
136
249
|
this.setCurrentPlayingId(null)
|
|
250
|
+
this.resetHighlight()
|
|
251
|
+
this.audioProvider?.onTimeUpdate?.(null)
|
|
137
252
|
})
|
|
138
253
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from 'react'
|
|
1
2
|
import { ConversationMessage } from './units.types'
|
|
2
3
|
|
|
3
4
|
export enum AudioStatus {
|
|
@@ -11,6 +12,12 @@ export type AudioProvider = {
|
|
|
11
12
|
pause: () => void
|
|
12
13
|
resume: () => void
|
|
13
14
|
stop: () => void
|
|
15
|
+
onTimeUpdate?: (callback: ((time: number) => void) | null) => void
|
|
16
|
+
getCurrentTime?: () => number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type HighlightConfig = {
|
|
20
|
+
MessageRenderer?: React.ComponentType<{ text: string; highlightedIndex: number | null; colorScheme?: string }>
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
export type PlayTextToSpeechProps = {
|
package/src/index.ts
CHANGED