@magmamath/students-features 0.9.111-rc-4 → 0.9.111-rc.6

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.
@@ -39,16 +39,16 @@ chatModel.bot.setUpT2S({
39
39
 
40
40
  ## Architecture
41
41
 
42
- ### ChatTextToSpeechModel (ChatTTSModel.ts)
42
+ ### ChatTTSModel (ChatTTSModel.ts)
43
43
 
44
44
  Handles TTS playback and word highlighting. Key design decisions:
45
45
 
46
- - **Unified cache** — single `cache: Record<string, CacheEntry>` stores both `audioPath` and `timepoints`. When highlighting is off, `timepoints` is `[]`.
46
+ - **Map-based cache** — `audioCache: Map<string, AudioCache>` stores both `audioPath` and `timepoints` per message. When highlighting is off, `timepoints` is `[]`. Cache lives for the model's lifetime (cleared when user leaves the problem).
47
47
  - **`fetchAudio()` method** — encapsulates API call + audioContent decoding. Highlight vs non-highlight differences are ternaries (SSML vs plain text, `enableTimePointing`).
48
48
  - **`play()` is linear** — cache lookup → fetch if miss → store → activate highlight if enabled → play audio. No branching between highlight/non-highlight paths.
49
- - **`isWordHighlightEnabled` config flag** — explicitly enables highlighting at the call site. Validated once in `setConfig` against `audioProvider.onTimeUpdate` capability.
50
- - **`wordHighlightEnabled` (private)** — stored result of `!!isWordHighlightEnabled && !!audioProvider.onTimeUpdate`. Set once at setup.
51
- - **`isWordHighlightEnabled` (public getter)** — read-only access for external consumers (e.g. `Chat.tsx`). Returns `wordHighlightEnabled`.
49
+ - **Pause/resume guards** — `wordHighlightEnabled` check before toggling `setIsActive(false)`/`setIsActive(true)` on pause/resume.
50
+ - **`wordHighlightEnabled` (private)** — stored result of `!!isWordHighlightEnabled && !!audioProvider.onTimeUpdate`. Set once at `setConfig`.
51
+ - **`isWordHighlightEnabled` (public getter)** — read-only access for external consumers (e.g. `Chat.tsx`).
52
52
 
53
53
  ### AudioProvider type
54
54
 
@@ -66,17 +66,28 @@ type AudioProvider = {
66
66
 
67
67
  ### ChatTTSHighlightModel (ChatTTSHighlightModel.ts)
68
68
 
69
- Manages highlight state. Receives `OnTimeUpdate` callback directly (not the whole `AudioProvider`).
69
+ Manages highlight state using Effector stores with declarative reset. Receives `OnTimeUpdate` callback directly (not the whole `AudioProvider`).
70
70
 
71
- - `activate(timepoints, onTimeUpdate)` sets timepoints, starts listening to time updates
72
- - `deactivate(onTimeUpdate)`stops listening, resets state
73
- - `$highlightedWordIndex`derived store, computes current word index from timepoints + time
71
+ - **`$isActive`** — gates `$highlightedWordIndex` computation. Toggled on pause/resume without resetting timepoints.
72
+ - **`$highlightedWordIndex`**derived store, returns `-1` when inactive or no match. Parses mark name via `WORD_MARK_REGEX`.
73
+ - **`activate(timepoints, onTimeUpdate)`** sets timepoints, enables highlighting, registers time update callback. Only triggers store updates when the highlighted word actually changes (`lastMark` comparison).
74
+ - **`deactivate(onTimeUpdate)`** — stops listening, resets all state via declarative `.reset()` chain.
75
+ - **All stores use `.reset(this.reset)`** — single `reset` event clears `$time`, `$timepoints`, and `$isActive`.
76
+
77
+ ### Helpers (helpers/ttsHighlight.ts)
78
+
79
+ - **`textToSSML(text)`** — converts plain text to SSML with `<mark>` tags for word timing.
80
+ - **`findCurrentMark(timepoints, time)`** — iterates backwards through timepoints to find the current mark. Efficient for linear playback.
81
+
82
+ ### Constants (constants.ts)
83
+
84
+ - **`WORD_MARK_REGEX`** — `/^w_(\d+)$/` — extracts word index from mark names like `w_0`, `w_1`.
74
85
 
75
86
  ### Naming conventions
76
87
 
77
88
  | Name | Scope | Purpose |
78
89
  |------|-------|---------|
79
- | `wordHighlight` | public instance | `ChatTTSHighlightModel` instance on `ChatTextToSpeechModel` |
90
+ | `wordHighlight` | public instance | `ChatTTSHighlightModel` instance on `ChatTTSModel` |
80
91
  | `wordHighlightEnabled` | private field | Whether highlighting is active (set once at config) |
81
92
  | `isWordHighlightEnabled` | public getter | Read-only access to `wordHighlightEnabled` for external consumers |
82
93
  | `isWordHighlightEnabled` | T2SConfig field | Config flag passed to `setUpT2S()` |
@@ -90,19 +101,20 @@ ChatTTSHighlightModel.activate(timepoints, onTimeUpdate)
90
101
 
91
102
  RAF loop → audio.currentTime → findCurrentMark() → only updates when mark changes
92
103
 
93
- $highlightedWordIndex = parse mark name "w_N" → N
104
+ $highlightedWordIndex = parse mark name "w_N" → N (or -1 if inactive/no match)
94
105
 
95
- Chat.tsx: model.t2s.isWordHighlightEnabled && isCurrentPlaying ? highlightedWordIndex : null
106
+ Chat.tsx: model.t2s.isWordHighlightEnabled && isPlayingMessage ? highlightedWordIndex : -1
96
107
 
97
- ChatMessage → MessageContent → MessageRenderer({ text, highlightedIndex })
108
+ ChatMessage → MessageContent → TTSHighlightRenderer({ text, highlightedIndex })
98
109
  ```
99
110
 
100
111
  ## Key Files
101
112
 
102
113
  | File | Purpose |
103
114
  |------|---------|
104
- | `model/ChatTTSModel.ts` | `ChatTextToSpeechModel` — TTS playback, caching, highlight orchestration |
105
- | `model/ChatTTSHighlightModel.ts` | Highlight state — timepoints, current time, derived word index |
106
- | `helpers/textToSSML.ts` | Converts text to SSML with `<mark>` tags for word timing |
107
- | `types/t2s.types.ts` | `AudioProvider`, `OnTimeUpdate`, `AudioStatus` types |
115
+ | `model/ChatTTSModel.ts` | `ChatTTSModel` — TTS playback, caching, highlight orchestration |
116
+ | `model/ChatTTSHighlightModel.ts` | Highlight state — timepoints, current time, `$isActive`, derived word index |
117
+ | `helpers/ttsHighlight.ts` | `textToSSML` + `findCurrentMark` SSML conversion and playback position lookup |
118
+ | `constants.ts` | `WORD_MARK_REGEX` and audio config defaults |
119
+ | `types/t2s.types.ts` | `AudioProvider`, `OnTimeUpdate`, `AudioStatus`, `AudioCache`, `TTSHighlightRendererProps` types |
108
120
  | `components/Chat/Chat.tsx` | Reads `isWordHighlightEnabled` and `$highlightedWordIndex`, passes to messages |
@@ -62,7 +62,8 @@ const Chat = ({
62
62
  },
63
63
  children: [isOpen && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
64
64
  style: [styles.gap, isInputDisabled && styles.messagesWithoutInput],
65
- children: [messages.map(message => {
65
+ children: [messages.map((message, index) => {
66
+ const key = `${message.id}-${index}`;
66
67
  const isPlayingMessage = message.id === currentPlayingId;
67
68
  const messageHighlightIndex = model.t2s.isWordHighlightEnabled && isPlayingMessage ? highlightedWordIndex : -1;
68
69
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_react.default.Fragment, {
@@ -85,9 +86,9 @@ const Chat = ({
85
86
  status: audioStatus,
86
87
  disabled: isTextToSpeechPending
87
88
  }
88
- }, message.id)
89
+ })
89
90
  })
90
- }, message.id);
91
+ }, key);
91
92
  }), isHelpRequestPending && /*#__PURE__*/(0, _jsxRuntime.jsx)(_MessageLoader.MessageLoader, {
92
93
  colorScheme: colorScheme
93
94
  })]
@@ -1 +1 @@
1
- {"version":3,"names":["_react","_interopRequireWildcard","require","_reactNative","_effectorReact","_ChatMessage","_MessageLoader","_ChatInput","_modelTypes","_styleTypes","_ConditionalWrapper","_jsxRuntime","_getRequireWildcardCache","e","WeakMap","r","t","__esModule","default","has","get","n","__proto__","a","Object","defineProperty","getOwnPropertyDescriptor","u","hasOwnProperty","call","i","set","TOOLBAR_HEIGHT","BOTTOM_OFFSET","Platform","select","web","Chat","model","isHintFeedback","isTextToSpeechEnabled","isInputDisabled","colorScheme","ColorScheme","Blue","MessageWrapper","TTSHighlightRenderer","dimensions","useWindowDimensions","ref","useRef","isOpen","useUnit","$isOpen","isHelpRequestPending","sendHelpRequestFx","pending","isTextToSpeechPending","api","textToSpeechTextFx","audioStatus","t2s","$status","currentPlayingId","$currentPlayingId","highlightedWordIndex","wordHighlight","$highlightedWordIndex","isTranslated","translation","$isTranslated","errorSpottingEnabled","errorSpotting","$isEnabled","messages","useStoreMap","$currentChatData","lastMessageId","at","id","MAX_HEIGHT","height","shouldWrapMessage","length","jsx","View","style","styles","container","maxHeight","children","jsxs","ScrollView","bounces","contentContainerStyle","list","onContentSizeChange","_","current","scrollToEnd","setScrollActive","gap","messagesWithoutInput","map","message","isPlayingMessage","messageHighlightIndex","isWordHighlightEnabled","Fragment","ConditionalWrapper","withWrapper","Wrapper","ChatMessage","withIcon","isOwnMessage","variant","MessageVariant","SENT","RECEIVED","withErrorSpotting","t2sState","isEnabled","isActive","isLoading","status","disabled","MessageLoader","ChatInput","hasMessages","exports","StyleSheet","create","flex","alignItems","width","paddingVertical","paddingRight","marginBottom"],"sourceRoot":"../../../../../../src","sources":["features/chatbot/components/Chat/Chat.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["_react","_interopRequireWildcard","require","_reactNative","_effectorReact","_ChatMessage","_MessageLoader","_ChatInput","_modelTypes","_styleTypes","_ConditionalWrapper","_jsxRuntime","_getRequireWildcardCache","e","WeakMap","r","t","__esModule","default","has","get","n","__proto__","a","Object","defineProperty","getOwnPropertyDescriptor","u","hasOwnProperty","call","i","set","TOOLBAR_HEIGHT","BOTTOM_OFFSET","Platform","select","web","Chat","model","isHintFeedback","isTextToSpeechEnabled","isInputDisabled","colorScheme","ColorScheme","Blue","MessageWrapper","TTSHighlightRenderer","dimensions","useWindowDimensions","ref","useRef","isOpen","useUnit","$isOpen","isHelpRequestPending","sendHelpRequestFx","pending","isTextToSpeechPending","api","textToSpeechTextFx","audioStatus","t2s","$status","currentPlayingId","$currentPlayingId","highlightedWordIndex","wordHighlight","$highlightedWordIndex","isTranslated","translation","$isTranslated","errorSpottingEnabled","errorSpotting","$isEnabled","messages","useStoreMap","$currentChatData","lastMessageId","at","id","MAX_HEIGHT","height","shouldWrapMessage","length","jsx","View","style","styles","container","maxHeight","children","jsxs","ScrollView","bounces","contentContainerStyle","list","onContentSizeChange","_","current","scrollToEnd","setScrollActive","gap","messagesWithoutInput","map","message","index","key","isPlayingMessage","messageHighlightIndex","isWordHighlightEnabled","Fragment","ConditionalWrapper","withWrapper","Wrapper","ChatMessage","withIcon","isOwnMessage","variant","MessageVariant","SENT","RECEIVED","withErrorSpotting","t2sState","isEnabled","isActive","isLoading","status","disabled","MessageLoader","ChatInput","hasMessages","exports","StyleSheet","create","flex","alignItems","width","paddingVertical","paddingRight","marginBottom"],"sourceRoot":"../../../../../../src","sources":["features/chatbot/components/Chat/Chat.tsx"],"mappings":"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","ignoreList":[]}
@@ -11,6 +11,10 @@ var _oeHelpers = require("../oe.helpers.js");
11
11
  var _commonConstants = require("../../../shared/common.constants.js");
12
12
  var _jsxRuntime = require("react/jsx-runtime");
13
13
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
+ const SCORE_CONTAINER_WIDTH = {
15
+ singleDigit: _commonConstants.isWeb ? 20 : 24,
16
+ doubleDigit: _commonConstants.isWeb ? 22 : 26
17
+ };
14
18
  const OpenEndedScoreLabel = ({
15
19
  label,
16
20
  score,
@@ -22,10 +26,13 @@ const OpenEndedScoreLabel = ({
22
26
  variant,
23
27
  disabled
24
28
  });
29
+ const scoreContainerWidth = score !== null && score >= 10 ? SCORE_CONTAINER_WIDTH.doubleDigit : SCORE_CONTAINER_WIDTH.singleDigit;
25
30
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
26
31
  style: styles.container,
27
32
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
28
- style: styles.score,
33
+ style: [styles.score, {
34
+ width: scoreContainerWidth
35
+ }],
29
36
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeUi.Typography, {
30
37
  variant: _reactNativeUi.HeadingVariants.H3,
31
38
  style: {
@@ -52,7 +59,6 @@ const styles = _reactNative.StyleSheet.create({
52
59
  flexDirection: 'row',
53
60
  alignItems: 'center',
54
61
  justifyContent: 'center',
55
- width: _commonConstants.isWeb ? 20 : 24,
56
62
  height: 32,
57
63
  borderWidth: 1,
58
64
  borderRadius: _reactNativeUi.BORDER_RADIUS[200],
@@ -1 +1 @@
1
- {"version":3,"names":["_react","_interopRequireDefault","require","_reactNative","_reactNativeUi","_oeHelpers","_commonConstants","_jsxRuntime","e","__esModule","default","OpenEndedScoreLabel","label","score","maxScore","disabled","variant","colors","getScoreLabelColor","jsxs","View","style","styles","container","children","jsx","Typography","HeadingVariants","H3","color","maxScoreText","exports","StyleSheet","create","alignItems","flexDirection","justifyContent","width","isWeb","height","borderWidth","borderRadius","BORDER_RADIUS","borderColor","COLORS","NEUTRAL_5","paddingVertical","SPACING","paddingHorizontal","backgroundColor","NEUTRAL_1","marginLeft"],"sourceRoot":"../../../../../src","sources":["features/openEnded/components/OpenEndedScoreLabel.tsx"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,sBAAA,CAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AACA,IAAAE,cAAA,GAAAF,OAAA;AAOA,IAAAG,UAAA,GAAAH,OAAA;AAEA,IAAAI,gBAAA,GAAAJ,OAAA;AAAwD,IAAAK,WAAA,GAAAL,OAAA;AAAA,SAAAD,uBAAAO,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;AAUjD,MAAMG,mBAAmB,GAAGA,CAAC;EAClCC,KAAK;EACLC,KAAK;EACLC,QAAQ;EACRC,QAAQ;EACRC;AACwB,CAAC,KAAK;EAC9B,MAAMC,MAAM,GAAG,IAAAC,6BAAkB,EAAC;IAAEF,OAAO;IAAED;EAAS,CAAC,CAAC;EACxD,oBACE,IAAAR,WAAA,CAAAY,IAAA,EAAChB,YAAA,CAAAiB,IAAI;IAACC,KAAK,EAAEC,MAAM,CAACC,SAAU;IAAAC,QAAA,gBAC5B,IAAAjB,WAAA,CAAAkB,GAAA,EAACtB,YAAA,CAAAiB,IAAI;MAACC,KAAK,EAAEC,MAAM,CAACT,KAAM;MAAAW,QAAA,eACxB,IAAAjB,WAAA,CAAAkB,GAAA,EAACrB,cAAA,CAAAsB,UAAU;QAACV,OAAO,EAAEW,8BAAe,CAACC,EAAG;QAACP,KAAK,EAAE;UAAEQ,KAAK,EAAEZ,MAAM,CAACJ;QAAM,CAAE;QAAAW,QAAA,EACrEX;MAAK,CACI;IAAC,CACT,CAAC,eACP,IAAAN,WAAA,CAAAY,IAAA,EAACf,cAAA,CAAAsB,UAAU;MACTV,OAAO,EAAEW,8BAAe,CAACC,EAAG;MAC5BP,KAAK,EAAE,CAACC,MAAM,CAACQ,YAAY,EAAE;QAAED,KAAK,EAAEZ,MAAM,CAACH;MAAS,CAAC,CAAE;MAAAU,QAAA,GAC1D,IACG,EAACV,QAAQ,EAAC,GAAC,EAACF,KAAK;IAAA,CACT,CAAC;EAAA,CACT,CAAC;AAEX,CAAC;AAAAmB,OAAA,CAAApB,mBAAA,GAAAA,mBAAA;AAED,MAAMW,MAAM,GAAGU,uBAAU,CAACC,MAAM,CAAC;EAC/BV,SAAS,EAAE;IACTW,UAAU,EAAE,QAAQ;IACpBC,aAAa,EAAE;EACjB,CAAC;EACDtB,KAAK,EAAE;IACLsB,aAAa,EAAE,KAAK;IACpBD,UAAU,EAAE,QAAQ;IACpBE,cAAc,EAAE,QAAQ;IACxBC,KAAK,EAAEC,sBAAK,GAAG,EAAE,GAAG,EAAE;IACtBC,MAAM,EAAE,EAAE;IACVC,WAAW,EAAE,CAAC;IACdC,YAAY,EAAEC,4BAAa,CAAC,GAAG,CAAC;IAChCC,WAAW,EAAEC,qBAAM,CAACC,SAAS;IAC7BC,eAAe,EAAER,sBAAK,GAAGS,sBAAO,CAAC,EAAE,CAAC,GAAG,CAAC;IACxCC,iBAAiB,EAAED,sBAAO,CAAC,GAAG,CAAC;IAC/BE,eAAe,EAAEL,qBAAM,CAACM;EAC1B,CAAC;EACDpB,YAAY,EAAE;IACZqB,UAAU,EAAEJ,sBAAO,CAAC,GAAG;EACzB;AACF,CAAC,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["_react","_interopRequireDefault","require","_reactNative","_reactNativeUi","_oeHelpers","_commonConstants","_jsxRuntime","e","__esModule","default","SCORE_CONTAINER_WIDTH","singleDigit","isWeb","doubleDigit","OpenEndedScoreLabel","label","score","maxScore","disabled","variant","colors","getScoreLabelColor","scoreContainerWidth","jsxs","View","style","styles","container","children","jsx","width","Typography","HeadingVariants","H3","color","maxScoreText","exports","StyleSheet","create","alignItems","flexDirection","justifyContent","height","borderWidth","borderRadius","BORDER_RADIUS","borderColor","COLORS","NEUTRAL_5","paddingVertical","SPACING","paddingHorizontal","backgroundColor","NEUTRAL_1","marginLeft"],"sourceRoot":"../../../../../src","sources":["features/openEnded/components/OpenEndedScoreLabel.tsx"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,sBAAA,CAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AACA,IAAAE,cAAA,GAAAF,OAAA;AAOA,IAAAG,UAAA,GAAAH,OAAA;AAEA,IAAAI,gBAAA,GAAAJ,OAAA;AAAwD,IAAAK,WAAA,GAAAL,OAAA;AAAA,SAAAD,uBAAAO,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;AAUxD,MAAMG,qBAAqB,GAAG;EAC5BC,WAAW,EAAEC,sBAAK,GAAG,EAAE,GAAG,EAAE;EAC5BC,WAAW,EAAED,sBAAK,GAAG,EAAE,GAAG;AAC5B,CAAU;AAEH,MAAME,mBAAmB,GAAGA,CAAC;EAClCC,KAAK;EACLC,KAAK;EACLC,QAAQ;EACRC,QAAQ;EACRC;AACwB,CAAC,KAAK;EAC9B,MAAMC,MAAM,GAAG,IAAAC,6BAAkB,EAAC;IAAEF,OAAO;IAAED;EAAS,CAAC,CAAC;EACxD,MAAMI,mBAAmB,GACvBN,KAAK,KAAK,IAAI,IAAIA,KAAK,IAAI,EAAE,GACzBN,qBAAqB,CAACG,WAAW,GACjCH,qBAAqB,CAACC,WAAW;EAEvC,oBACE,IAAAL,WAAA,CAAAiB,IAAA,EAACrB,YAAA,CAAAsB,IAAI;IAACC,KAAK,EAAEC,MAAM,CAACC,SAAU;IAAAC,QAAA,gBAC5B,IAAAtB,WAAA,CAAAuB,GAAA,EAAC3B,YAAA,CAAAsB,IAAI;MAACC,KAAK,EAAE,CAACC,MAAM,CAACV,KAAK,EAAE;QAAEc,KAAK,EAAER;MAAoB,CAAC,CAAE;MAAAM,QAAA,eAC1D,IAAAtB,WAAA,CAAAuB,GAAA,EAAC1B,cAAA,CAAA4B,UAAU;QAACZ,OAAO,EAAEa,8BAAe,CAACC,EAAG;QAACR,KAAK,EAAE;UAAES,KAAK,EAAEd,MAAM,CAACJ;QAAM,CAAE;QAAAY,QAAA,EACrEZ;MAAK,CACI;IAAC,CACT,CAAC,eACP,IAAAV,WAAA,CAAAiB,IAAA,EAACpB,cAAA,CAAA4B,UAAU;MACTZ,OAAO,EAAEa,8BAAe,CAACC,EAAG;MAC5BR,KAAK,EAAE,CAACC,MAAM,CAACS,YAAY,EAAE;QAAED,KAAK,EAAEd,MAAM,CAACH;MAAS,CAAC,CAAE;MAAAW,QAAA,GAC1D,IACG,EAACX,QAAQ,EAAC,GAAC,EAACF,KAAK;IAAA,CACT,CAAC;EAAA,CACT,CAAC;AAEX,CAAC;AAAAqB,OAAA,CAAAtB,mBAAA,GAAAA,mBAAA;AAED,MAAMY,MAAM,GAAGW,uBAAU,CAACC,MAAM,CAAC;EAC/BX,SAAS,EAAE;IACTY,UAAU,EAAE,QAAQ;IACpBC,aAAa,EAAE;EACjB,CAAC;EACDxB,KAAK,EAAE;IACLwB,aAAa,EAAE,KAAK;IACpBD,UAAU,EAAE,QAAQ;IACpBE,cAAc,EAAE,QAAQ;IACxBC,MAAM,EAAE,EAAE;IACVC,WAAW,EAAE,CAAC;IACdC,YAAY,EAAEC,4BAAa,CAAC,GAAG,CAAC;IAChCC,WAAW,EAAEC,qBAAM,CAACC,SAAS;IAC7BC,eAAe,EAAErC,sBAAK,GAAGsC,sBAAO,CAAC,EAAE,CAAC,GAAG,CAAC;IACxCC,iBAAiB,EAAED,sBAAO,CAAC,GAAG,CAAC;IAC/BE,eAAe,EAAEL,qBAAM,CAACM;EAC1B,CAAC;EACDlB,YAAY,EAAE;IACZmB,UAAU,EAAEJ,sBAAO,CAAC,GAAG;EACzB;AACF,CAAC,CAAC","ignoreList":[]}
@@ -39,16 +39,16 @@ chatModel.bot.setUpT2S({
39
39
 
40
40
  ## Architecture
41
41
 
42
- ### ChatTextToSpeechModel (ChatTTSModel.ts)
42
+ ### ChatTTSModel (ChatTTSModel.ts)
43
43
 
44
44
  Handles TTS playback and word highlighting. Key design decisions:
45
45
 
46
- - **Unified cache** — single `cache: Record<string, CacheEntry>` stores both `audioPath` and `timepoints`. When highlighting is off, `timepoints` is `[]`.
46
+ - **Map-based cache** — `audioCache: Map<string, AudioCache>` stores both `audioPath` and `timepoints` per message. When highlighting is off, `timepoints` is `[]`. Cache lives for the model's lifetime (cleared when user leaves the problem).
47
47
  - **`fetchAudio()` method** — encapsulates API call + audioContent decoding. Highlight vs non-highlight differences are ternaries (SSML vs plain text, `enableTimePointing`).
48
48
  - **`play()` is linear** — cache lookup → fetch if miss → store → activate highlight if enabled → play audio. No branching between highlight/non-highlight paths.
49
- - **`isWordHighlightEnabled` config flag** — explicitly enables highlighting at the call site. Validated once in `setConfig` against `audioProvider.onTimeUpdate` capability.
50
- - **`wordHighlightEnabled` (private)** — stored result of `!!isWordHighlightEnabled && !!audioProvider.onTimeUpdate`. Set once at setup.
51
- - **`isWordHighlightEnabled` (public getter)** — read-only access for external consumers (e.g. `Chat.tsx`). Returns `wordHighlightEnabled`.
49
+ - **Pause/resume guards** — `wordHighlightEnabled` check before toggling `setIsActive(false)`/`setIsActive(true)` on pause/resume.
50
+ - **`wordHighlightEnabled` (private)** — stored result of `!!isWordHighlightEnabled && !!audioProvider.onTimeUpdate`. Set once at `setConfig`.
51
+ - **`isWordHighlightEnabled` (public getter)** — read-only access for external consumers (e.g. `Chat.tsx`).
52
52
 
53
53
  ### AudioProvider type
54
54
 
@@ -66,17 +66,28 @@ type AudioProvider = {
66
66
 
67
67
  ### ChatTTSHighlightModel (ChatTTSHighlightModel.ts)
68
68
 
69
- Manages highlight state. Receives `OnTimeUpdate` callback directly (not the whole `AudioProvider`).
69
+ Manages highlight state using Effector stores with declarative reset. Receives `OnTimeUpdate` callback directly (not the whole `AudioProvider`).
70
70
 
71
- - `activate(timepoints, onTimeUpdate)` sets timepoints, starts listening to time updates
72
- - `deactivate(onTimeUpdate)`stops listening, resets state
73
- - `$highlightedWordIndex`derived store, computes current word index from timepoints + time
71
+ - **`$isActive`** — gates `$highlightedWordIndex` computation. Toggled on pause/resume without resetting timepoints.
72
+ - **`$highlightedWordIndex`**derived store, returns `-1` when inactive or no match. Parses mark name via `WORD_MARK_REGEX`.
73
+ - **`activate(timepoints, onTimeUpdate)`** sets timepoints, enables highlighting, registers time update callback. Only triggers store updates when the highlighted word actually changes (`lastMark` comparison).
74
+ - **`deactivate(onTimeUpdate)`** — stops listening, resets all state via declarative `.reset()` chain.
75
+ - **All stores use `.reset(this.reset)`** — single `reset` event clears `$time`, `$timepoints`, and `$isActive`.
76
+
77
+ ### Helpers (helpers/ttsHighlight.ts)
78
+
79
+ - **`textToSSML(text)`** — converts plain text to SSML with `<mark>` tags for word timing.
80
+ - **`findCurrentMark(timepoints, time)`** — iterates backwards through timepoints to find the current mark. Efficient for linear playback.
81
+
82
+ ### Constants (constants.ts)
83
+
84
+ - **`WORD_MARK_REGEX`** — `/^w_(\d+)$/` — extracts word index from mark names like `w_0`, `w_1`.
74
85
 
75
86
  ### Naming conventions
76
87
 
77
88
  | Name | Scope | Purpose |
78
89
  |------|-------|---------|
79
- | `wordHighlight` | public instance | `ChatTTSHighlightModel` instance on `ChatTextToSpeechModel` |
90
+ | `wordHighlight` | public instance | `ChatTTSHighlightModel` instance on `ChatTTSModel` |
80
91
  | `wordHighlightEnabled` | private field | Whether highlighting is active (set once at config) |
81
92
  | `isWordHighlightEnabled` | public getter | Read-only access to `wordHighlightEnabled` for external consumers |
82
93
  | `isWordHighlightEnabled` | T2SConfig field | Config flag passed to `setUpT2S()` |
@@ -90,19 +101,20 @@ ChatTTSHighlightModel.activate(timepoints, onTimeUpdate)
90
101
 
91
102
  RAF loop → audio.currentTime → findCurrentMark() → only updates when mark changes
92
103
 
93
- $highlightedWordIndex = parse mark name "w_N" → N
104
+ $highlightedWordIndex = parse mark name "w_N" → N (or -1 if inactive/no match)
94
105
 
95
- Chat.tsx: model.t2s.isWordHighlightEnabled && isCurrentPlaying ? highlightedWordIndex : null
106
+ Chat.tsx: model.t2s.isWordHighlightEnabled && isPlayingMessage ? highlightedWordIndex : -1
96
107
 
97
- ChatMessage → MessageContent → MessageRenderer({ text, highlightedIndex })
108
+ ChatMessage → MessageContent → TTSHighlightRenderer({ text, highlightedIndex })
98
109
  ```
99
110
 
100
111
  ## Key Files
101
112
 
102
113
  | File | Purpose |
103
114
  |------|---------|
104
- | `model/ChatTTSModel.ts` | `ChatTextToSpeechModel` — TTS playback, caching, highlight orchestration |
105
- | `model/ChatTTSHighlightModel.ts` | Highlight state — timepoints, current time, derived word index |
106
- | `helpers/textToSSML.ts` | Converts text to SSML with `<mark>` tags for word timing |
107
- | `types/t2s.types.ts` | `AudioProvider`, `OnTimeUpdate`, `AudioStatus` types |
115
+ | `model/ChatTTSModel.ts` | `ChatTTSModel` — TTS playback, caching, highlight orchestration |
116
+ | `model/ChatTTSHighlightModel.ts` | Highlight state — timepoints, current time, `$isActive`, derived word index |
117
+ | `helpers/ttsHighlight.ts` | `textToSSML` + `findCurrentMark` SSML conversion and playback position lookup |
118
+ | `constants.ts` | `WORD_MARK_REGEX` and audio config defaults |
119
+ | `types/t2s.types.ts` | `AudioProvider`, `OnTimeUpdate`, `AudioStatus`, `AudioCache`, `TTSHighlightRendererProps` types |
108
120
  | `components/Chat/Chat.tsx` | Reads `isWordHighlightEnabled` and `$highlightedWordIndex`, passes to messages |
@@ -56,7 +56,8 @@ export const Chat = ({
56
56
  },
57
57
  children: [isOpen && /*#__PURE__*/_jsxs(View, {
58
58
  style: [styles.gap, isInputDisabled && styles.messagesWithoutInput],
59
- children: [messages.map(message => {
59
+ children: [messages.map((message, index) => {
60
+ const key = `${message.id}-${index}`;
60
61
  const isPlayingMessage = message.id === currentPlayingId;
61
62
  const messageHighlightIndex = model.t2s.isWordHighlightEnabled && isPlayingMessage ? highlightedWordIndex : -1;
62
63
  return /*#__PURE__*/_jsx(React.Fragment, {
@@ -79,9 +80,9 @@ export const Chat = ({
79
80
  status: audioStatus,
80
81
  disabled: isTextToSpeechPending
81
82
  }
82
- }, message.id)
83
+ })
83
84
  })
84
- }, message.id);
85
+ }, key);
85
86
  }), isHelpRequestPending && /*#__PURE__*/_jsx(MessageLoader, {
86
87
  colorScheme: colorScheme
87
88
  })]
@@ -1 +1 @@
1
- {"version":3,"names":["React","useRef","Platform","ScrollView","StyleSheet","useWindowDimensions","View","useStoreMap","useUnit","ChatMessage","MessageLoader","ChatInput","MessageVariant","ColorScheme","ConditionalWrapper","jsx","_jsx","jsxs","_jsxs","TOOLBAR_HEIGHT","BOTTOM_OFFSET","select","web","default","Chat","model","isHintFeedback","isTextToSpeechEnabled","isInputDisabled","colorScheme","Blue","MessageWrapper","TTSHighlightRenderer","dimensions","ref","isOpen","$isOpen","isHelpRequestPending","sendHelpRequestFx","pending","isTextToSpeechPending","api","textToSpeechTextFx","audioStatus","t2s","$status","currentPlayingId","$currentPlayingId","highlightedWordIndex","wordHighlight","$highlightedWordIndex","isTranslated","translation","$isTranslated","errorSpottingEnabled","errorSpotting","$isEnabled","messages","$currentChatData","lastMessageId","at","id","MAX_HEIGHT","height","shouldWrapMessage","length","style","styles","container","maxHeight","children","bounces","contentContainerStyle","list","onContentSizeChange","_","current","scrollToEnd","setScrollActive","gap","messagesWithoutInput","map","message","isPlayingMessage","messageHighlightIndex","isWordHighlightEnabled","Fragment","withWrapper","Wrapper","withIcon","isOwnMessage","variant","SENT","RECEIVED","withErrorSpotting","t2sState","isEnabled","isActive","isLoading","status","disabled","hasMessages","create","flex","alignItems","width","paddingVertical","paddingRight","marginBottom"],"sourceRoot":"../../../../../../src","sources":["features/chatbot/components/Chat/Chat.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["React","useRef","Platform","ScrollView","StyleSheet","useWindowDimensions","View","useStoreMap","useUnit","ChatMessage","MessageLoader","ChatInput","MessageVariant","ColorScheme","ConditionalWrapper","jsx","_jsx","jsxs","_jsxs","TOOLBAR_HEIGHT","BOTTOM_OFFSET","select","web","default","Chat","model","isHintFeedback","isTextToSpeechEnabled","isInputDisabled","colorScheme","Blue","MessageWrapper","TTSHighlightRenderer","dimensions","ref","isOpen","$isOpen","isHelpRequestPending","sendHelpRequestFx","pending","isTextToSpeechPending","api","textToSpeechTextFx","audioStatus","t2s","$status","currentPlayingId","$currentPlayingId","highlightedWordIndex","wordHighlight","$highlightedWordIndex","isTranslated","translation","$isTranslated","errorSpottingEnabled","errorSpotting","$isEnabled","messages","$currentChatData","lastMessageId","at","id","MAX_HEIGHT","height","shouldWrapMessage","length","style","styles","container","maxHeight","children","bounces","contentContainerStyle","list","onContentSizeChange","_","current","scrollToEnd","setScrollActive","gap","messagesWithoutInput","map","message","index","key","isPlayingMessage","messageHighlightIndex","isWordHighlightEnabled","Fragment","withWrapper","Wrapper","withIcon","isOwnMessage","variant","SENT","RECEIVED","withErrorSpotting","t2sState","isEnabled","isActive","isLoading","status","disabled","hasMessages","create","flex","alignItems","width","paddingVertical","paddingRight","marginBottom"],"sourceRoot":"../../../../../../src","sources":["features/chatbot/components/Chat/Chat.tsx"],"mappings":"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","ignoreList":[]}
@@ -6,6 +6,10 @@ import { Typography, HeadingVariants, SPACING, BORDER_RADIUS, COLORS } from '@ma
6
6
  import { getScoreLabelColor } from "../oe.helpers.js";
7
7
  import { isWeb } from "../../../shared/common.constants.js";
8
8
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
+ const SCORE_CONTAINER_WIDTH = {
10
+ singleDigit: isWeb ? 20 : 24,
11
+ doubleDigit: isWeb ? 22 : 26
12
+ };
9
13
  export const OpenEndedScoreLabel = ({
10
14
  label,
11
15
  score,
@@ -17,10 +21,13 @@ export const OpenEndedScoreLabel = ({
17
21
  variant,
18
22
  disabled
19
23
  });
24
+ const scoreContainerWidth = score !== null && score >= 10 ? SCORE_CONTAINER_WIDTH.doubleDigit : SCORE_CONTAINER_WIDTH.singleDigit;
20
25
  return /*#__PURE__*/_jsxs(View, {
21
26
  style: styles.container,
22
27
  children: [/*#__PURE__*/_jsx(View, {
23
- style: styles.score,
28
+ style: [styles.score, {
29
+ width: scoreContainerWidth
30
+ }],
24
31
  children: /*#__PURE__*/_jsx(Typography, {
25
32
  variant: HeadingVariants.H3,
26
33
  style: {
@@ -46,7 +53,6 @@ const styles = StyleSheet.create({
46
53
  flexDirection: 'row',
47
54
  alignItems: 'center',
48
55
  justifyContent: 'center',
49
- width: isWeb ? 20 : 24,
50
56
  height: 32,
51
57
  borderWidth: 1,
52
58
  borderRadius: BORDER_RADIUS[200],
@@ -1 +1 @@
1
- {"version":3,"names":["React","View","StyleSheet","Typography","HeadingVariants","SPACING","BORDER_RADIUS","COLORS","getScoreLabelColor","isWeb","jsx","_jsx","jsxs","_jsxs","OpenEndedScoreLabel","label","score","maxScore","disabled","variant","colors","style","styles","container","children","H3","color","maxScoreText","create","alignItems","flexDirection","justifyContent","width","height","borderWidth","borderRadius","borderColor","NEUTRAL_5","paddingVertical","paddingHorizontal","backgroundColor","NEUTRAL_1","marginLeft"],"sourceRoot":"../../../../../src","sources":["features/openEnded/components/OpenEndedScoreLabel.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,SAASC,IAAI,EAAEC,UAAU,QAAQ,cAAc;AAC/C,SACEC,UAAU,EACVC,eAAe,EACfC,OAAO,EACPC,aAAa,EACbC,MAAM,QACD,4BAA4B;AACnC,SAASC,kBAAkB,QAAQ,kBAAe;AAElD,SAASC,KAAK,QAAQ,qCAAkC;AAAA,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AAUxD,OAAO,MAAMC,mBAAmB,GAAGA,CAAC;EAClCC,KAAK;EACLC,KAAK;EACLC,QAAQ;EACRC,QAAQ;EACRC;AACwB,CAAC,KAAK;EAC9B,MAAMC,MAAM,GAAGZ,kBAAkB,CAAC;IAAEW,OAAO;IAAED;EAAS,CAAC,CAAC;EACxD,oBACEL,KAAA,CAACZ,IAAI;IAACoB,KAAK,EAAEC,MAAM,CAACC,SAAU;IAAAC,QAAA,gBAC5Bb,IAAA,CAACV,IAAI;MAACoB,KAAK,EAAEC,MAAM,CAACN,KAAM;MAAAQ,QAAA,eACxBb,IAAA,CAACR,UAAU;QAACgB,OAAO,EAAEf,eAAe,CAACqB,EAAG;QAACJ,KAAK,EAAE;UAAEK,KAAK,EAAEN,MAAM,CAACJ;QAAM,CAAE;QAAAQ,QAAA,EACrER;MAAK,CACI;IAAC,CACT,CAAC,eACPH,KAAA,CAACV,UAAU;MACTgB,OAAO,EAAEf,eAAe,CAACqB,EAAG;MAC5BJ,KAAK,EAAE,CAACC,MAAM,CAACK,YAAY,EAAE;QAAED,KAAK,EAAEN,MAAM,CAACH;MAAS,CAAC,CAAE;MAAAO,QAAA,GAC1D,IACG,EAACP,QAAQ,EAAC,GAAC,EAACF,KAAK;IAAA,CACT,CAAC;EAAA,CACT,CAAC;AAEX,CAAC;AAED,MAAMO,MAAM,GAAGpB,UAAU,CAAC0B,MAAM,CAAC;EAC/BL,SAAS,EAAE;IACTM,UAAU,EAAE,QAAQ;IACpBC,aAAa,EAAE;EACjB,CAAC;EACDd,KAAK,EAAE;IACLc,aAAa,EAAE,KAAK;IACpBD,UAAU,EAAE,QAAQ;IACpBE,cAAc,EAAE,QAAQ;IACxBC,KAAK,EAAEvB,KAAK,GAAG,EAAE,GAAG,EAAE;IACtBwB,MAAM,EAAE,EAAE;IACVC,WAAW,EAAE,CAAC;IACdC,YAAY,EAAE7B,aAAa,CAAC,GAAG,CAAC;IAChC8B,WAAW,EAAE7B,MAAM,CAAC8B,SAAS;IAC7BC,eAAe,EAAE7B,KAAK,GAAGJ,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC;IACxCkC,iBAAiB,EAAElC,OAAO,CAAC,GAAG,CAAC;IAC/BmC,eAAe,EAAEjC,MAAM,CAACkC;EAC1B,CAAC;EACDd,YAAY,EAAE;IACZe,UAAU,EAAErC,OAAO,CAAC,GAAG;EACzB;AACF,CAAC,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["React","View","StyleSheet","Typography","HeadingVariants","SPACING","BORDER_RADIUS","COLORS","getScoreLabelColor","isWeb","jsx","_jsx","jsxs","_jsxs","SCORE_CONTAINER_WIDTH","singleDigit","doubleDigit","OpenEndedScoreLabel","label","score","maxScore","disabled","variant","colors","scoreContainerWidth","style","styles","container","children","width","H3","color","maxScoreText","create","alignItems","flexDirection","justifyContent","height","borderWidth","borderRadius","borderColor","NEUTRAL_5","paddingVertical","paddingHorizontal","backgroundColor","NEUTRAL_1","marginLeft"],"sourceRoot":"../../../../../src","sources":["features/openEnded/components/OpenEndedScoreLabel.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,SAASC,IAAI,EAAEC,UAAU,QAAQ,cAAc;AAC/C,SACEC,UAAU,EACVC,eAAe,EACfC,OAAO,EACPC,aAAa,EACbC,MAAM,QACD,4BAA4B;AACnC,SAASC,kBAAkB,QAAQ,kBAAe;AAElD,SAASC,KAAK,QAAQ,qCAAkC;AAAA,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AAUxD,MAAMC,qBAAqB,GAAG;EAC5BC,WAAW,EAAEN,KAAK,GAAG,EAAE,GAAG,EAAE;EAC5BO,WAAW,EAAEP,KAAK,GAAG,EAAE,GAAG;AAC5B,CAAU;AAEV,OAAO,MAAMQ,mBAAmB,GAAGA,CAAC;EAClCC,KAAK;EACLC,KAAK;EACLC,QAAQ;EACRC,QAAQ;EACRC;AACwB,CAAC,KAAK;EAC9B,MAAMC,MAAM,GAAGf,kBAAkB,CAAC;IAAEc,OAAO;IAAED;EAAS,CAAC,CAAC;EACxD,MAAMG,mBAAmB,GACvBL,KAAK,KAAK,IAAI,IAAIA,KAAK,IAAI,EAAE,GACzBL,qBAAqB,CAACE,WAAW,GACjCF,qBAAqB,CAACC,WAAW;EAEvC,oBACEF,KAAA,CAACZ,IAAI;IAACwB,KAAK,EAAEC,MAAM,CAACC,SAAU;IAAAC,QAAA,gBAC5BjB,IAAA,CAACV,IAAI;MAACwB,KAAK,EAAE,CAACC,MAAM,CAACP,KAAK,EAAE;QAAEU,KAAK,EAAEL;MAAoB,CAAC,CAAE;MAAAI,QAAA,eAC1DjB,IAAA,CAACR,UAAU;QAACmB,OAAO,EAAElB,eAAe,CAAC0B,EAAG;QAACL,KAAK,EAAE;UAAEM,KAAK,EAAER,MAAM,CAACJ;QAAM,CAAE;QAAAS,QAAA,EACrET;MAAK,CACI;IAAC,CACT,CAAC,eACPN,KAAA,CAACV,UAAU;MACTmB,OAAO,EAAElB,eAAe,CAAC0B,EAAG;MAC5BL,KAAK,EAAE,CAACC,MAAM,CAACM,YAAY,EAAE;QAAED,KAAK,EAAER,MAAM,CAACH;MAAS,CAAC,CAAE;MAAAQ,QAAA,GAC1D,IACG,EAACR,QAAQ,EAAC,GAAC,EAACF,KAAK;IAAA,CACT,CAAC;EAAA,CACT,CAAC;AAEX,CAAC;AAED,MAAMQ,MAAM,GAAGxB,UAAU,CAAC+B,MAAM,CAAC;EAC/BN,SAAS,EAAE;IACTO,UAAU,EAAE,QAAQ;IACpBC,aAAa,EAAE;EACjB,CAAC;EACDhB,KAAK,EAAE;IACLgB,aAAa,EAAE,KAAK;IACpBD,UAAU,EAAE,QAAQ;IACpBE,cAAc,EAAE,QAAQ;IACxBC,MAAM,EAAE,EAAE;IACVC,WAAW,EAAE,CAAC;IACdC,YAAY,EAAEjC,aAAa,CAAC,GAAG,CAAC;IAChCkC,WAAW,EAAEjC,MAAM,CAACkC,SAAS;IAC7BC,eAAe,EAAEjC,KAAK,GAAGJ,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC;IACxCsC,iBAAiB,EAAEtC,OAAO,CAAC,GAAG,CAAC;IAC/BuC,eAAe,EAAErC,MAAM,CAACsC;EAC1B,CAAC;EACDb,YAAY,EAAE;IACZc,UAAU,EAAEzC,OAAO,CAAC,GAAG;EACzB;AACF,CAAC,CAAC","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"OpenEndedScoreLabel.d.ts","sourceRoot":"","sources":["../../../../../../src/features/openEnded/components/OpenEndedScoreLabel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAUzB,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAGxD,KAAK,wBAAwB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,0BAA0B,CAAA;CACpC,CAAA;AAED,eAAO,MAAM,mBAAmB,mDAM7B,wBAAwB,sBAiB1B,CAAA"}
1
+ {"version":3,"file":"OpenEndedScoreLabel.d.ts","sourceRoot":"","sources":["../../../../../../src/features/openEnded/components/OpenEndedScoreLabel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAUzB,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAGxD,KAAK,wBAAwB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,0BAA0B,CAAA;CACpC,CAAA;AAOD,eAAO,MAAM,mBAAmB,mDAM7B,wBAAwB,sBAsB1B,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"OpenEndedScoreLabel.d.ts","sourceRoot":"","sources":["../../../../../../src/features/openEnded/components/OpenEndedScoreLabel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAUzB,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAGxD,KAAK,wBAAwB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,0BAA0B,CAAA;CACpC,CAAA;AAED,eAAO,MAAM,mBAAmB,mDAM7B,wBAAwB,sBAiB1B,CAAA"}
1
+ {"version":3,"file":"OpenEndedScoreLabel.d.ts","sourceRoot":"","sources":["../../../../../../src/features/openEnded/components/OpenEndedScoreLabel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAUzB,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAGxD,KAAK,wBAAwB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,0BAA0B,CAAA;CACpC,CAAA;AAOD,eAAO,MAAM,mBAAmB,mDAM7B,wBAAwB,sBAsB1B,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magmamath/students-features",
3
- "version": "0.9.111-rc-4",
3
+ "version": "0.9.111-rc.6",
4
4
  "description": "Magmamath features library",
5
5
  "source": "src/index.ts",
6
6
  "main": "src/index.ts",
@@ -39,16 +39,16 @@ chatModel.bot.setUpT2S({
39
39
 
40
40
  ## Architecture
41
41
 
42
- ### ChatTextToSpeechModel (ChatTTSModel.ts)
42
+ ### ChatTTSModel (ChatTTSModel.ts)
43
43
 
44
44
  Handles TTS playback and word highlighting. Key design decisions:
45
45
 
46
- - **Unified cache** — single `cache: Record<string, CacheEntry>` stores both `audioPath` and `timepoints`. When highlighting is off, `timepoints` is `[]`.
46
+ - **Map-based cache** — `audioCache: Map<string, AudioCache>` stores both `audioPath` and `timepoints` per message. When highlighting is off, `timepoints` is `[]`. Cache lives for the model's lifetime (cleared when user leaves the problem).
47
47
  - **`fetchAudio()` method** — encapsulates API call + audioContent decoding. Highlight vs non-highlight differences are ternaries (SSML vs plain text, `enableTimePointing`).
48
48
  - **`play()` is linear** — cache lookup → fetch if miss → store → activate highlight if enabled → play audio. No branching between highlight/non-highlight paths.
49
- - **`isWordHighlightEnabled` config flag** — explicitly enables highlighting at the call site. Validated once in `setConfig` against `audioProvider.onTimeUpdate` capability.
50
- - **`wordHighlightEnabled` (private)** — stored result of `!!isWordHighlightEnabled && !!audioProvider.onTimeUpdate`. Set once at setup.
51
- - **`isWordHighlightEnabled` (public getter)** — read-only access for external consumers (e.g. `Chat.tsx`). Returns `wordHighlightEnabled`.
49
+ - **Pause/resume guards** — `wordHighlightEnabled` check before toggling `setIsActive(false)`/`setIsActive(true)` on pause/resume.
50
+ - **`wordHighlightEnabled` (private)** — stored result of `!!isWordHighlightEnabled && !!audioProvider.onTimeUpdate`. Set once at `setConfig`.
51
+ - **`isWordHighlightEnabled` (public getter)** — read-only access for external consumers (e.g. `Chat.tsx`).
52
52
 
53
53
  ### AudioProvider type
54
54
 
@@ -66,17 +66,28 @@ type AudioProvider = {
66
66
 
67
67
  ### ChatTTSHighlightModel (ChatTTSHighlightModel.ts)
68
68
 
69
- Manages highlight state. Receives `OnTimeUpdate` callback directly (not the whole `AudioProvider`).
69
+ Manages highlight state using Effector stores with declarative reset. Receives `OnTimeUpdate` callback directly (not the whole `AudioProvider`).
70
70
 
71
- - `activate(timepoints, onTimeUpdate)` sets timepoints, starts listening to time updates
72
- - `deactivate(onTimeUpdate)`stops listening, resets state
73
- - `$highlightedWordIndex`derived store, computes current word index from timepoints + time
71
+ - **`$isActive`** — gates `$highlightedWordIndex` computation. Toggled on pause/resume without resetting timepoints.
72
+ - **`$highlightedWordIndex`**derived store, returns `-1` when inactive or no match. Parses mark name via `WORD_MARK_REGEX`.
73
+ - **`activate(timepoints, onTimeUpdate)`** sets timepoints, enables highlighting, registers time update callback. Only triggers store updates when the highlighted word actually changes (`lastMark` comparison).
74
+ - **`deactivate(onTimeUpdate)`** — stops listening, resets all state via declarative `.reset()` chain.
75
+ - **All stores use `.reset(this.reset)`** — single `reset` event clears `$time`, `$timepoints`, and `$isActive`.
76
+
77
+ ### Helpers (helpers/ttsHighlight.ts)
78
+
79
+ - **`textToSSML(text)`** — converts plain text to SSML with `<mark>` tags for word timing.
80
+ - **`findCurrentMark(timepoints, time)`** — iterates backwards through timepoints to find the current mark. Efficient for linear playback.
81
+
82
+ ### Constants (constants.ts)
83
+
84
+ - **`WORD_MARK_REGEX`** — `/^w_(\d+)$/` — extracts word index from mark names like `w_0`, `w_1`.
74
85
 
75
86
  ### Naming conventions
76
87
 
77
88
  | Name | Scope | Purpose |
78
89
  |------|-------|---------|
79
- | `wordHighlight` | public instance | `ChatTTSHighlightModel` instance on `ChatTextToSpeechModel` |
90
+ | `wordHighlight` | public instance | `ChatTTSHighlightModel` instance on `ChatTTSModel` |
80
91
  | `wordHighlightEnabled` | private field | Whether highlighting is active (set once at config) |
81
92
  | `isWordHighlightEnabled` | public getter | Read-only access to `wordHighlightEnabled` for external consumers |
82
93
  | `isWordHighlightEnabled` | T2SConfig field | Config flag passed to `setUpT2S()` |
@@ -90,19 +101,20 @@ ChatTTSHighlightModel.activate(timepoints, onTimeUpdate)
90
101
 
91
102
  RAF loop → audio.currentTime → findCurrentMark() → only updates when mark changes
92
103
 
93
- $highlightedWordIndex = parse mark name "w_N" → N
104
+ $highlightedWordIndex = parse mark name "w_N" → N (or -1 if inactive/no match)
94
105
 
95
- Chat.tsx: model.t2s.isWordHighlightEnabled && isCurrentPlaying ? highlightedWordIndex : null
106
+ Chat.tsx: model.t2s.isWordHighlightEnabled && isPlayingMessage ? highlightedWordIndex : -1
96
107
 
97
- ChatMessage → MessageContent → MessageRenderer({ text, highlightedIndex })
108
+ ChatMessage → MessageContent → TTSHighlightRenderer({ text, highlightedIndex })
98
109
  ```
99
110
 
100
111
  ## Key Files
101
112
 
102
113
  | File | Purpose |
103
114
  |------|---------|
104
- | `model/ChatTTSModel.ts` | `ChatTextToSpeechModel` — TTS playback, caching, highlight orchestration |
105
- | `model/ChatTTSHighlightModel.ts` | Highlight state — timepoints, current time, derived word index |
106
- | `helpers/textToSSML.ts` | Converts text to SSML with `<mark>` tags for word timing |
107
- | `types/t2s.types.ts` | `AudioProvider`, `OnTimeUpdate`, `AudioStatus` types |
115
+ | `model/ChatTTSModel.ts` | `ChatTTSModel` — TTS playback, caching, highlight orchestration |
116
+ | `model/ChatTTSHighlightModel.ts` | Highlight state — timepoints, current time, `$isActive`, derived word index |
117
+ | `helpers/ttsHighlight.ts` | `textToSSML` + `findCurrentMark` SSML conversion and playback position lookup |
118
+ | `constants.ts` | `WORD_MARK_REGEX` and audio config defaults |
119
+ | `types/t2s.types.ts` | `AudioProvider`, `OnTimeUpdate`, `AudioStatus`, `AudioCache`, `TTSHighlightRendererProps` types |
108
120
  | `components/Chat/Chat.tsx` | Reads `isWordHighlightEnabled` and `$highlightedWordIndex`, passes to messages |
@@ -72,16 +72,16 @@ export const Chat = ({
72
72
  >
73
73
  {isOpen && (
74
74
  <View style={[styles.gap, isInputDisabled && styles.messagesWithoutInput]}>
75
- {messages.map((message) => {
75
+ {messages.map((message, index) => {
76
+ const key = `${message.id}-${index}`
76
77
  const isPlayingMessage = message.id === currentPlayingId
77
78
  const messageHighlightIndex =
78
79
  model.t2s.isWordHighlightEnabled && isPlayingMessage ? highlightedWordIndex : -1
79
80
 
80
81
  return (
81
- <React.Fragment key={message.id}>
82
+ <React.Fragment key={key}>
82
83
  <ConditionalWrapper withWrapper={shouldWrapMessage} Wrapper={MessageWrapper}>
83
84
  <ChatMessage
84
- key={message.id}
85
85
  isTranslated={isTranslated}
86
86
  withIcon={!message.isOwnMessage && message.id === lastMessageId}
87
87
  message={message}
@@ -19,6 +19,11 @@ type OpenEndedScoreLabelProps = {
19
19
  variant: OpenEndedScoreLabelVariant
20
20
  }
21
21
 
22
+ const SCORE_CONTAINER_WIDTH = {
23
+ singleDigit: isWeb ? 20 : 24,
24
+ doubleDigit: isWeb ? 22 : 26,
25
+ } as const
26
+
22
27
  export const OpenEndedScoreLabel = ({
23
28
  label,
24
29
  score,
@@ -27,9 +32,14 @@ export const OpenEndedScoreLabel = ({
27
32
  variant,
28
33
  }: OpenEndedScoreLabelProps) => {
29
34
  const colors = getScoreLabelColor({ variant, disabled })
35
+ const scoreContainerWidth =
36
+ score !== null && score >= 10
37
+ ? SCORE_CONTAINER_WIDTH.doubleDigit
38
+ : SCORE_CONTAINER_WIDTH.singleDigit
39
+
30
40
  return (
31
41
  <View style={styles.container}>
32
- <View style={styles.score}>
42
+ <View style={[styles.score, { width: scoreContainerWidth }]}>
33
43
  <Typography variant={HeadingVariants.H3} style={{ color: colors.score }}>
34
44
  {score}
35
45
  </Typography>
@@ -53,7 +63,6 @@ const styles = StyleSheet.create({
53
63
  flexDirection: 'row',
54
64
  alignItems: 'center',
55
65
  justifyContent: 'center',
56
- width: isWeb ? 20 : 24,
57
66
  height: 32,
58
67
  borderWidth: 1,
59
68
  borderRadius: BORDER_RADIUS[200],