@magmamath/students-features 1.7.9 → 1.7.10-rc.2
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/voice/constants.js +42 -3
- package/dist/commonjs/features/voice/constants.js.map +1 -1
- package/dist/commonjs/features/voice/helpers.js +67 -1
- package/dist/commonjs/features/voice/helpers.js.map +1 -1
- package/dist/commonjs/features/voice/index.js +77 -0
- package/dist/commonjs/features/voice/index.js.map +1 -1
- package/dist/commonjs/features/voice/playing/components/VoiceTranscriptContent.js +22 -9
- package/dist/commonjs/features/voice/playing/components/VoiceTranscriptContent.js.map +1 -1
- package/dist/commonjs/features/voice/playing/model/TranscriptionsDownloader.model.js +4 -27
- package/dist/commonjs/features/voice/playing/model/TranscriptionsDownloader.model.js.map +1 -1
- package/dist/commonjs/features/voice/recording/components/VoiceRecordWithTranscript.js +49 -0
- package/dist/commonjs/features/voice/recording/components/VoiceRecordWithTranscript.js.map +1 -0
- package/dist/commonjs/features/voice/recording/components/VoiceTranscriptPanel.js +253 -0
- package/dist/commonjs/features/voice/recording/components/VoiceTranscriptPanel.js.map +1 -0
- package/dist/commonjs/features/voice/recording/components/VoiceWaveform.js +93 -0
- package/dist/commonjs/features/voice/recording/components/VoiceWaveform.js.map +1 -0
- package/dist/commonjs/features/voice/recording/hooks/useTranscriptPanelAnimation.js +42 -0
- package/dist/commonjs/features/voice/recording/hooks/useTranscriptPanelAnimation.js.map +1 -0
- package/dist/commonjs/features/voice/recording/hooks/useVoiceRecorder.js +17 -2
- package/dist/commonjs/features/voice/recording/hooks/useVoiceRecorder.js.map +1 -1
- package/dist/commonjs/features/voice/recording/hooks/useVoiceWaveform.js +43 -0
- package/dist/commonjs/features/voice/recording/hooks/useVoiceWaveform.js.map +1 -0
- package/dist/commonjs/features/voice/recording/model/Recorder.model.js +3 -0
- package/dist/commonjs/features/voice/recording/model/Recorder.model.js.map +1 -1
- package/dist/commonjs/features/voice/recording/model/VoiceRecordWithTranscript.model.js +100 -0
- package/dist/commonjs/features/voice/recording/model/VoiceRecordWithTranscript.model.js.map +1 -0
- package/dist/commonjs/features/voice/recording/model/VoiceTranscriptPanel.model.js +115 -0
- package/dist/commonjs/features/voice/recording/model/VoiceTranscriptPanel.model.js.map +1 -0
- package/dist/commonjs/features/voice/transcript.helpers.js +36 -0
- package/dist/commonjs/features/voice/transcript.helpers.js.map +1 -0
- package/dist/commonjs/features/voice/types.js +11 -7
- package/dist/commonjs/features/voice/types.js.map +1 -1
- package/dist/module/features/voice/constants.js +41 -0
- package/dist/module/features/voice/constants.js.map +1 -1
- package/dist/module/features/voice/helpers.js +65 -1
- package/dist/module/features/voice/helpers.js.map +1 -1
- package/dist/module/features/voice/index.js +7 -0
- package/dist/module/features/voice/index.js.map +1 -1
- package/dist/module/features/voice/playing/components/VoiceTranscriptContent.js +20 -9
- package/dist/module/features/voice/playing/components/VoiceTranscriptContent.js.map +1 -1
- package/dist/module/features/voice/playing/model/TranscriptionsDownloader.model.js +4 -27
- package/dist/module/features/voice/playing/model/TranscriptionsDownloader.model.js.map +1 -1
- package/dist/module/features/voice/recording/components/VoiceRecordWithTranscript.js +43 -0
- package/dist/module/features/voice/recording/components/VoiceRecordWithTranscript.js.map +1 -0
- package/dist/module/features/voice/recording/components/VoiceTranscriptPanel.js +246 -0
- package/dist/module/features/voice/recording/components/VoiceTranscriptPanel.js.map +1 -0
- package/dist/module/features/voice/recording/components/VoiceWaveform.js +86 -0
- package/dist/module/features/voice/recording/components/VoiceWaveform.js.map +1 -0
- package/dist/module/features/voice/recording/hooks/useTranscriptPanelAnimation.js +37 -0
- package/dist/module/features/voice/recording/hooks/useTranscriptPanelAnimation.js.map +1 -0
- package/dist/module/features/voice/recording/hooks/useVoiceRecorder.js +20 -3
- package/dist/module/features/voice/recording/hooks/useVoiceRecorder.js.map +1 -1
- package/dist/module/features/voice/recording/hooks/useVoiceWaveform.js +38 -0
- package/dist/module/features/voice/recording/hooks/useVoiceWaveform.js.map +1 -0
- package/dist/module/features/voice/recording/model/Recorder.model.js +3 -0
- package/dist/module/features/voice/recording/model/Recorder.model.js.map +1 -1
- package/dist/module/features/voice/recording/model/VoiceRecordWithTranscript.model.js +95 -0
- package/dist/module/features/voice/recording/model/VoiceRecordWithTranscript.model.js.map +1 -0
- package/dist/module/features/voice/recording/model/VoiceTranscriptPanel.model.js +110 -0
- package/dist/module/features/voice/recording/model/VoiceTranscriptPanel.model.js.map +1 -0
- package/dist/module/features/voice/transcript.helpers.js +31 -0
- package/dist/module/features/voice/transcript.helpers.js.map +1 -0
- package/dist/module/features/voice/types.js +4 -6
- package/dist/module/features/voice/types.js.map +1 -1
- package/dist/typescript/commonjs/features/voice/__tests__/VoiceRecordWithTranscript.model.test.d.ts +2 -0
- package/dist/typescript/commonjs/features/voice/__tests__/VoiceRecordWithTranscript.model.test.d.ts.map +1 -0
- package/dist/typescript/commonjs/features/voice/__tests__/VoiceTranscriptPanel.model.test.d.ts +2 -0
- package/dist/typescript/commonjs/features/voice/__tests__/VoiceTranscriptPanel.model.test.d.ts.map +1 -0
- package/dist/typescript/commonjs/features/voice/constants.d.ts +34 -0
- package/dist/typescript/commonjs/features/voice/constants.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/voice/helpers.d.ts +3 -0
- package/dist/typescript/commonjs/features/voice/helpers.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/voice/index.d.ts +7 -0
- package/dist/typescript/commonjs/features/voice/index.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/voice/playing/components/VoiceTranscriptContent.d.ts +3 -1
- package/dist/typescript/commonjs/features/voice/playing/components/VoiceTranscriptContent.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/voice/playing/model/TranscriptionsDownloader.model.d.ts +2 -7
- package/dist/typescript/commonjs/features/voice/playing/model/TranscriptionsDownloader.model.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/voice/recording/components/VoiceRecordWithTranscript.d.ts +13 -0
- package/dist/typescript/commonjs/features/voice/recording/components/VoiceRecordWithTranscript.d.ts.map +1 -0
- package/dist/typescript/commonjs/features/voice/recording/components/VoiceTranscriptPanel.d.ts +11 -0
- package/dist/typescript/commonjs/features/voice/recording/components/VoiceTranscriptPanel.d.ts.map +1 -0
- package/dist/typescript/commonjs/features/voice/recording/components/VoiceWaveform.d.ts +8 -0
- package/dist/typescript/commonjs/features/voice/recording/components/VoiceWaveform.d.ts.map +1 -0
- package/dist/typescript/commonjs/features/voice/recording/hooks/useTranscriptPanelAnimation.d.ts +18 -0
- package/dist/typescript/commonjs/features/voice/recording/hooks/useTranscriptPanelAnimation.d.ts.map +1 -0
- package/dist/typescript/commonjs/features/voice/recording/hooks/useVoiceRecorder.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/voice/recording/hooks/useVoiceWaveform.d.ts +7 -0
- package/dist/typescript/commonjs/features/voice/recording/hooks/useVoiceWaveform.d.ts.map +1 -0
- package/dist/typescript/commonjs/features/voice/recording/model/Recorder.model.d.ts +2 -0
- package/dist/typescript/commonjs/features/voice/recording/model/Recorder.model.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/voice/recording/model/VoiceRecordWithTranscript.model.d.ts +16 -0
- package/dist/typescript/commonjs/features/voice/recording/model/VoiceRecordWithTranscript.model.d.ts.map +1 -0
- package/dist/typescript/commonjs/features/voice/recording/model/VoiceTranscriptPanel.model.d.ts +31 -0
- package/dist/typescript/commonjs/features/voice/recording/model/VoiceTranscriptPanel.model.d.ts.map +1 -0
- package/dist/typescript/commonjs/features/voice/transcript.helpers.d.ts +8 -0
- package/dist/typescript/commonjs/features/voice/transcript.helpers.d.ts.map +1 -0
- package/dist/typescript/commonjs/features/voice/types.d.ts +2 -5
- package/dist/typescript/commonjs/features/voice/types.d.ts.map +1 -1
- package/dist/typescript/module/features/voice/__tests__/VoiceRecordWithTranscript.model.test.d.ts +2 -0
- package/dist/typescript/module/features/voice/__tests__/VoiceRecordWithTranscript.model.test.d.ts.map +1 -0
- package/dist/typescript/module/features/voice/__tests__/VoiceTranscriptPanel.model.test.d.ts +2 -0
- package/dist/typescript/module/features/voice/__tests__/VoiceTranscriptPanel.model.test.d.ts.map +1 -0
- package/dist/typescript/module/features/voice/constants.d.ts +34 -0
- package/dist/typescript/module/features/voice/constants.d.ts.map +1 -1
- package/dist/typescript/module/features/voice/helpers.d.ts +3 -0
- package/dist/typescript/module/features/voice/helpers.d.ts.map +1 -1
- package/dist/typescript/module/features/voice/index.d.ts +7 -0
- package/dist/typescript/module/features/voice/index.d.ts.map +1 -1
- package/dist/typescript/module/features/voice/playing/components/VoiceTranscriptContent.d.ts +3 -1
- package/dist/typescript/module/features/voice/playing/components/VoiceTranscriptContent.d.ts.map +1 -1
- package/dist/typescript/module/features/voice/playing/model/TranscriptionsDownloader.model.d.ts +2 -7
- package/dist/typescript/module/features/voice/playing/model/TranscriptionsDownloader.model.d.ts.map +1 -1
- package/dist/typescript/module/features/voice/recording/components/VoiceRecordWithTranscript.d.ts +13 -0
- package/dist/typescript/module/features/voice/recording/components/VoiceRecordWithTranscript.d.ts.map +1 -0
- package/dist/typescript/module/features/voice/recording/components/VoiceTranscriptPanel.d.ts +11 -0
- package/dist/typescript/module/features/voice/recording/components/VoiceTranscriptPanel.d.ts.map +1 -0
- package/dist/typescript/module/features/voice/recording/components/VoiceWaveform.d.ts +8 -0
- package/dist/typescript/module/features/voice/recording/components/VoiceWaveform.d.ts.map +1 -0
- package/dist/typescript/module/features/voice/recording/hooks/useTranscriptPanelAnimation.d.ts +18 -0
- package/dist/typescript/module/features/voice/recording/hooks/useTranscriptPanelAnimation.d.ts.map +1 -0
- package/dist/typescript/module/features/voice/recording/hooks/useVoiceRecorder.d.ts.map +1 -1
- package/dist/typescript/module/features/voice/recording/hooks/useVoiceWaveform.d.ts +7 -0
- package/dist/typescript/module/features/voice/recording/hooks/useVoiceWaveform.d.ts.map +1 -0
- package/dist/typescript/module/features/voice/recording/model/Recorder.model.d.ts +2 -0
- package/dist/typescript/module/features/voice/recording/model/Recorder.model.d.ts.map +1 -1
- package/dist/typescript/module/features/voice/recording/model/VoiceRecordWithTranscript.model.d.ts +16 -0
- package/dist/typescript/module/features/voice/recording/model/VoiceRecordWithTranscript.model.d.ts.map +1 -0
- package/dist/typescript/module/features/voice/recording/model/VoiceTranscriptPanel.model.d.ts +31 -0
- package/dist/typescript/module/features/voice/recording/model/VoiceTranscriptPanel.model.d.ts.map +1 -0
- package/dist/typescript/module/features/voice/transcript.helpers.d.ts +8 -0
- package/dist/typescript/module/features/voice/transcript.helpers.d.ts.map +1 -0
- package/dist/typescript/module/features/voice/types.d.ts +2 -5
- package/dist/typescript/module/features/voice/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/features/voice/__tests__/VoiceRecordWithTranscript.model.test.ts +228 -0
- package/src/features/voice/__tests__/VoiceTranscriptPanel.model.test.ts +237 -0
- package/src/features/voice/constants.ts +43 -0
- package/src/features/voice/helpers.ts +85 -1
- package/src/features/voice/index.ts +7 -0
- package/src/features/voice/playing/components/VoiceTranscriptContent.tsx +27 -8
- package/src/features/voice/playing/model/TranscriptionsDownloader.model.ts +8 -30
- package/src/features/voice/recording/components/VoiceRecordWithTranscript.tsx +52 -0
- package/src/features/voice/recording/components/VoiceTranscriptPanel.tsx +293 -0
- package/src/features/voice/recording/components/VoiceWaveform.tsx +102 -0
- package/src/features/voice/recording/hooks/useTranscriptPanelAnimation.ts +49 -0
- package/src/features/voice/recording/hooks/useVoiceRecorder.ts +26 -3
- package/src/features/voice/recording/hooks/useVoiceWaveform.ts +46 -0
- package/src/features/voice/recording/model/Recorder.model.ts +3 -0
- package/src/features/voice/recording/model/VoiceRecordWithTranscript.model.ts +104 -0
- package/src/features/voice/recording/model/VoiceTranscriptPanel.model.ts +131 -0
- package/src/features/voice/transcript.helpers.ts +37 -0
- package/src/features/voice/types.ts +5 -6
- package/src/i18n/.generated/schema.json +6 -5
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createEffect, Effect, sample } from 'effector'
|
|
2
|
+
|
|
3
|
+
import { NO_AUDIO_BE_MESSAGE, VoiceRecorderState } from '../../constants'
|
|
4
|
+
import { fetchTranscriptWithRetry, GetAudioFileTranscript } from '../../transcript.helpers'
|
|
5
|
+
import { VoiceTranscriptPanelModel } from './VoiceTranscriptPanel.model'
|
|
6
|
+
import type { VoiceRecordModel } from './VoiceRecord.model'
|
|
7
|
+
import type { VoiceRecordCollectionItem } from '../../types'
|
|
8
|
+
|
|
9
|
+
export type VoiceRecordWithTranscriptModelProps = {
|
|
10
|
+
recordModel: VoiceRecordModel
|
|
11
|
+
getTranscript: GetAudioFileTranscript
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type ResultGuardSource = {
|
|
15
|
+
currentRecord: VoiceRecordCollectionItem | null
|
|
16
|
+
isDismissPending: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Wires the existing recording flow to the transcript panel:
|
|
20
|
+
// recording starts → panel opens with the waveform; the recording is confirmed
|
|
21
|
+
// (upload kicks off) → panel shows the loader; the uploaded file's transcript
|
|
22
|
+
// arrives → panel shows the text. Deleting/discarding the record closes the panel.
|
|
23
|
+
export class VoiceRecordWithTranscriptModel {
|
|
24
|
+
public readonly record: VoiceRecordModel
|
|
25
|
+
public readonly panel = new VoiceTranscriptPanelModel()
|
|
26
|
+
|
|
27
|
+
private readonly resolveTranscriptFx: Effect<VoiceRecordCollectionItem, string>
|
|
28
|
+
|
|
29
|
+
constructor({ recordModel, getTranscript }: VoiceRecordWithTranscriptModelProps) {
|
|
30
|
+
this.record = recordModel
|
|
31
|
+
this.resolveTranscriptFx = createEffect(async (record: VoiceRecordCollectionItem) => {
|
|
32
|
+
const upload = await record.audioUploadPromise
|
|
33
|
+
if (!upload || !upload.id) throw new Error('Audio upload did not return a file id')
|
|
34
|
+
|
|
35
|
+
const { text } = await fetchTranscriptWithRetry(getTranscript, upload.id)
|
|
36
|
+
if (text === NO_AUDIO_BE_MESSAGE) throw new Error('Transcript is not available')
|
|
37
|
+
return text
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
this.setupPanelLifecycle()
|
|
41
|
+
this.setupTranscriptPipeline()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private setupPanelLifecycle() {
|
|
45
|
+
sample({
|
|
46
|
+
clock: this.record.recorderModel.$voiceRecordState,
|
|
47
|
+
filter: (state) => state === VoiceRecorderState.RECORDING,
|
|
48
|
+
target: this.panel.open,
|
|
49
|
+
})
|
|
50
|
+
// startAudioUpload fires exactly when a recording was confirmed (the
|
|
51
|
+
// discard path never uploads), so it doubles as the panel's confirm clock.
|
|
52
|
+
sample({ clock: this.record.startAudioUpload, target: this.panel.confirm })
|
|
53
|
+
// The record resets as a side effect of a wrong answer (the attempt key
|
|
54
|
+
// rotates to a take with no recording). While the panel is handling its own
|
|
55
|
+
// dismissal — dimming a shown transcript, or waiting for one that is still
|
|
56
|
+
// loading — that incidental reset must not close it early; the panel owns
|
|
57
|
+
// the close.
|
|
58
|
+
sample({
|
|
59
|
+
clock: [this.record.reset, this.record.stop.done],
|
|
60
|
+
source: this.panel.$isHandlingDismissal,
|
|
61
|
+
filter: (isHandlingDismissal) => !isHandlingDismissal,
|
|
62
|
+
target: this.panel.discard,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private setupTranscriptPipeline() {
|
|
67
|
+
// A fetch result applies when the record it was fetched for is still the
|
|
68
|
+
// current one — late results from a discarded or re-recorded take are
|
|
69
|
+
// dropped. The exception is a pending dismissal: a wrong answer rotates
|
|
70
|
+
// $currentRecord away from the take it was fetched for, but that take's
|
|
71
|
+
// transcript is exactly the one the panel is waiting to show before it
|
|
72
|
+
// closes, so it must still apply.
|
|
73
|
+
const shouldApplyResult = (
|
|
74
|
+
{ currentRecord, isDismissPending }: ResultGuardSource,
|
|
75
|
+
{ params }: { params: VoiceRecordCollectionItem },
|
|
76
|
+
) => isDismissPending || currentRecord === params
|
|
77
|
+
|
|
78
|
+
const resultGuardSource = {
|
|
79
|
+
currentRecord: this.record.$currentRecord,
|
|
80
|
+
isDismissPending: this.panel.$isDismissPending,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
sample({
|
|
84
|
+
clock: this.record.$currentRecord.updates,
|
|
85
|
+
source: this.panel.$flags,
|
|
86
|
+
filter: (flags, record) => flags.isProcessing && Boolean(record?.audioUploadPromise),
|
|
87
|
+
fn: (_, record): VoiceRecordCollectionItem => record,
|
|
88
|
+
target: this.resolveTranscriptFx,
|
|
89
|
+
})
|
|
90
|
+
sample({
|
|
91
|
+
clock: this.resolveTranscriptFx.done,
|
|
92
|
+
source: resultGuardSource,
|
|
93
|
+
filter: shouldApplyResult,
|
|
94
|
+
fn: (_, { result }) => result,
|
|
95
|
+
target: this.panel.complete,
|
|
96
|
+
})
|
|
97
|
+
sample({
|
|
98
|
+
clock: this.resolveTranscriptFx.fail,
|
|
99
|
+
source: resultGuardSource,
|
|
100
|
+
filter: shouldApplyResult,
|
|
101
|
+
target: this.panel.fail,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { combine, createEvent, createStore, Event, sample } from 'effector'
|
|
2
|
+
import { delay } from 'patronum'
|
|
3
|
+
|
|
4
|
+
import { VOICE_TRANSCRIPT_PANEL, VoicePanelPhase } from '../../constants'
|
|
5
|
+
import { DropdownModel } from '../../playing/model/Dropdown.model'
|
|
6
|
+
|
|
7
|
+
type PhaseFlags = {
|
|
8
|
+
isOpen: boolean
|
|
9
|
+
isRecording: boolean
|
|
10
|
+
isProcessing: boolean
|
|
11
|
+
isDone: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Phase machine for the recording-flow panel: open → RECORDING (waveform),
|
|
15
|
+
// confirm → PROCESSING (loader), complete/fail → DONE (transcript text).
|
|
16
|
+
// The transcript shows inside a collapsible dropdown (same UX as the playing
|
|
17
|
+
// side): collapsed to a short preview by default, expandable via the caret.
|
|
18
|
+
// Transcript fetching itself is wired externally
|
|
19
|
+
// (see VoiceRecordWithTranscriptModel).
|
|
20
|
+
export class VoiceTranscriptPanelModel {
|
|
21
|
+
public readonly reset = createEvent()
|
|
22
|
+
|
|
23
|
+
public readonly dropdown = new DropdownModel()
|
|
24
|
+
|
|
25
|
+
public readonly open = createEvent()
|
|
26
|
+
public readonly confirm = createEvent()
|
|
27
|
+
public readonly complete = createEvent<string>()
|
|
28
|
+
public readonly fail = createEvent()
|
|
29
|
+
public readonly discard = createEvent()
|
|
30
|
+
// Ask the panel to dismiss itself gracefully — dim the transcript, linger,
|
|
31
|
+
// then close (vs. discard, which closes immediately). Wired to a wrong answer
|
|
32
|
+
// by the consumer; the panel itself stays domain-agnostic and owns the timing.
|
|
33
|
+
public readonly requestDismiss = createEvent()
|
|
34
|
+
|
|
35
|
+
public readonly $phase = createStore<VoicePanelPhase | null>(null)
|
|
36
|
+
public readonly $transcript = createStore('')
|
|
37
|
+
public readonly $hasError = createStore(false)
|
|
38
|
+
public readonly $isDimmed = createStore(false)
|
|
39
|
+
// A dismissal asked for while the transcript is still loading: there is
|
|
40
|
+
// nothing to dim yet, so we remember it and run the dismissal once the
|
|
41
|
+
// transcript resolves into the DONE phase.
|
|
42
|
+
public readonly $isDismissPending = createStore(false)
|
|
43
|
+
|
|
44
|
+
// True while the panel owns its own dismissal — either already dimming a
|
|
45
|
+
// shown transcript, or waiting for an in-flight transcript before it can.
|
|
46
|
+
// The consumer reads this so an incidental record reset can't close the
|
|
47
|
+
// panel out from under a dismissal in progress.
|
|
48
|
+
public readonly $isHandlingDismissal = combine(
|
|
49
|
+
this.$isDimmed,
|
|
50
|
+
this.$isDismissPending,
|
|
51
|
+
(isDimmed, isDismissPending) => isDimmed || isDismissPending,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
public readonly $flags = this.$phase.map(
|
|
55
|
+
(phase): PhaseFlags => ({
|
|
56
|
+
isOpen: phase !== null,
|
|
57
|
+
isRecording: phase === VoicePanelPhase.RECORDING,
|
|
58
|
+
isProcessing: phase === VoicePanelPhase.PROCESSING,
|
|
59
|
+
isDone: phase === VoicePanelPhase.DONE,
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
constructor() {
|
|
64
|
+
this.setupPhaseTransitions()
|
|
65
|
+
this.setupDelayedDismissal()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The event passes through only while the panel is in the given phase —
|
|
69
|
+
// e.g. a transcript that arrives after a discard must not reopen the panel
|
|
70
|
+
private allowInPhase<T>(clock: Event<T>, phase: VoicePanelPhase): Event<T> {
|
|
71
|
+
return sample({
|
|
72
|
+
clock,
|
|
73
|
+
source: this.$phase,
|
|
74
|
+
filter: (currentPhase) => currentPhase === phase,
|
|
75
|
+
fn: (_, payload: T) => payload,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private setupPhaseTransitions() {
|
|
80
|
+
const confirmed = this.allowInPhase(this.confirm, VoicePanelPhase.RECORDING)
|
|
81
|
+
const completed = this.allowInPhase(this.complete, VoicePanelPhase.PROCESSING)
|
|
82
|
+
const failed = this.allowInPhase(this.fail, VoicePanelPhase.PROCESSING)
|
|
83
|
+
|
|
84
|
+
this.$phase
|
|
85
|
+
.on(this.open, () => VoicePanelPhase.RECORDING)
|
|
86
|
+
.on(confirmed, () => VoicePanelPhase.PROCESSING)
|
|
87
|
+
.on(completed, () => VoicePanelPhase.DONE)
|
|
88
|
+
.on(failed, () => VoicePanelPhase.DONE)
|
|
89
|
+
.reset(this.reset)
|
|
90
|
+
|
|
91
|
+
this.$transcript.on(completed, (_, transcript) => transcript).reset(this.reset)
|
|
92
|
+
this.$hasError.on(failed, () => true).reset(this.reset)
|
|
93
|
+
|
|
94
|
+
sample({ clock: this.discard, target: this.reset })
|
|
95
|
+
sample({ clock: this.reset, target: this.dropdown.reset })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private setupDelayedDismissal() {
|
|
99
|
+
const beginDismissal = createEvent()
|
|
100
|
+
|
|
101
|
+
// Transcript already shown → dim and dismiss right away.
|
|
102
|
+
const dismissWhileDone = this.allowInPhase(this.requestDismiss, VoicePanelPhase.DONE)
|
|
103
|
+
|
|
104
|
+
// Transcript still loading → there is nothing to dim, so defer the request.
|
|
105
|
+
const dismissWhileProcessing = this.allowInPhase(this.requestDismiss, VoicePanelPhase.PROCESSING)
|
|
106
|
+
this.$isDismissPending.on(dismissWhileProcessing, () => true).reset([this.reset, this.open])
|
|
107
|
+
|
|
108
|
+
// A deferred request fires the moment the transcript resolves into DONE.
|
|
109
|
+
const deferredDismiss = sample({
|
|
110
|
+
clock: this.$phase.updates,
|
|
111
|
+
source: this.$isDismissPending,
|
|
112
|
+
filter: (isDismissPending, phase) => isDismissPending && phase === VoicePanelPhase.DONE,
|
|
113
|
+
fn: () => undefined,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
sample({ clock: [dismissWhileDone, deferredDismiss], target: beginDismissal })
|
|
117
|
+
|
|
118
|
+
this.$isDimmed.on(beginDismissal, () => true).reset([this.reset, this.open])
|
|
119
|
+
|
|
120
|
+
const dismissTimeout = delay({
|
|
121
|
+
source: beginDismissal,
|
|
122
|
+
timeout: VOICE_TRANSCRIPT_PANEL.WRONG_ANSWER_DISMISS_DELAY_MS,
|
|
123
|
+
})
|
|
124
|
+
// The phase guard drops the timer if the panel was already discarded or a
|
|
125
|
+
// new recording started during the linger window
|
|
126
|
+
sample({
|
|
127
|
+
clock: this.allowInPhase(dismissTimeout, VoicePanelPhase.DONE),
|
|
128
|
+
target: this.reset,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NO_AUDIO_BE_MESSAGE,
|
|
3
|
+
TRANSCRIPT_MAX_RETRIES,
|
|
4
|
+
TRANSCRIPT_RETRY_INTERVAL_MS,
|
|
5
|
+
TranscriptionStatus,
|
|
6
|
+
} from './constants'
|
|
7
|
+
import type { AudioTranscriptResponse } from './types'
|
|
8
|
+
|
|
9
|
+
export type TranscriptResult = { text: string; language?: string }
|
|
10
|
+
export type GetAudioFileTranscript = (audioFileId: string) => Promise<AudioTranscriptResponse>
|
|
11
|
+
|
|
12
|
+
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
13
|
+
|
|
14
|
+
// Polls the transcript endpoint until the BE finishes processing. FAILED and
|
|
15
|
+
// exhausted retries both resolve to the NO_AUDIO sentinel — same contract the
|
|
16
|
+
// playing side relies on.
|
|
17
|
+
export const fetchTranscriptWithRetry = async (
|
|
18
|
+
getTranscript: GetAudioFileTranscript,
|
|
19
|
+
audioFileId: string,
|
|
20
|
+
): Promise<TranscriptResult> => {
|
|
21
|
+
for (let attempt = 0; attempt < TRANSCRIPT_MAX_RETRIES; attempt++) {
|
|
22
|
+
const response = await getTranscript(audioFileId)
|
|
23
|
+
|
|
24
|
+
if (response.status === TranscriptionStatus.COMPLETED) {
|
|
25
|
+
return { text: response.text, language: response.language }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (response.status === TranscriptionStatus.FAILED) {
|
|
29
|
+
return { text: NO_AUDIO_BE_MESSAGE }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (attempt < TRANSCRIPT_MAX_RETRIES - 1) {
|
|
33
|
+
await delay(TRANSCRIPT_RETRY_INTERVAL_MS)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { text: NO_AUDIO_BE_MESSAGE }
|
|
37
|
+
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { WithAbortSignal } from 'src/types/common.types'
|
|
2
|
+
import { TranscriptionStatus } from './constants'
|
|
2
3
|
import { getAvailableInputs } from './helpers'
|
|
3
4
|
import { VoiceRecordModel } from './recording/model/VoiceRecord.model'
|
|
4
5
|
import { AxiosResponse } from 'axios'
|
|
6
|
+
|
|
7
|
+
// The enum moved to constants.ts (jest-safe import chain for transcript helpers);
|
|
8
|
+
// re-exported here to keep the public surface unchanged.
|
|
9
|
+
export { TranscriptionStatus }
|
|
5
10
|
import type {
|
|
6
11
|
GoogleTranslateProps as GoogleTranslatePayload,
|
|
7
12
|
GoogleTranslateResponse,
|
|
@@ -60,12 +65,6 @@ export type VoiceRecorderApi = {
|
|
|
60
65
|
deleteAudioFile: (audioFileId: string) => Promise<void>
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
export enum TranscriptionStatus {
|
|
64
|
-
PENDING = 'pending',
|
|
65
|
-
COMPLETED = 'completed',
|
|
66
|
-
FAILED = 'failed',
|
|
67
|
-
}
|
|
68
|
-
|
|
69
68
|
export type AudioTranscriptResponse = {
|
|
70
69
|
text: string
|
|
71
70
|
language: string
|
|
@@ -73,9 +73,9 @@
|
|
|
73
73
|
"errorTitle": "Something went wrong",
|
|
74
74
|
"fast": "Fast!",
|
|
75
75
|
"incorrect": "INCORRECT — ANSWER: {{answer}}",
|
|
76
|
-
"masteryGrowing": "
|
|
77
|
-
"masteryLearning": "
|
|
78
|
-
"masteryMastered": "
|
|
76
|
+
"masteryGrowing": "Familiar",
|
|
77
|
+
"masteryLearning": "New",
|
|
78
|
+
"masteryMastered": "Automatic",
|
|
79
79
|
"masteryStrong": "Strong",
|
|
80
80
|
"multiplication": "Multiplication",
|
|
81
81
|
"notStarted": "New",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"teacher": {
|
|
99
99
|
"fluency": {
|
|
100
100
|
"allPupils": "All students",
|
|
101
|
-
"average": "
|
|
101
|
+
"average": "Progress",
|
|
102
102
|
"avgTimeTitle": "Avg Time / Student",
|
|
103
103
|
"factFluency": "Fact Fluency",
|
|
104
104
|
"factsGridLabel": "{{cols}}×{{rows}} facts",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"noFactsPracticed": "No facts practiced yet",
|
|
109
109
|
"noStudents": "This class has no students",
|
|
110
110
|
"noStudentsFound": "No students match your search",
|
|
111
|
-
"overallMastery": "
|
|
111
|
+
"overallMastery": "Automaticity progress",
|
|
112
112
|
"progressDeveloping": "Developing",
|
|
113
113
|
"progressFluent": "Fluent",
|
|
114
114
|
"progressLearning": "Learning",
|
|
@@ -140,6 +140,7 @@
|
|
|
140
140
|
"noMicrophoneAccess": "No microphone access.",
|
|
141
141
|
"noMicrophoneAccessBySystem": "Microphone access denied by system settings.",
|
|
142
142
|
"noMicrophoneFound": "No microphone found.",
|
|
143
|
+
"preparingTranscript": "Preparing transcript",
|
|
143
144
|
"recordingDeleted": "Recording successfully deleted.",
|
|
144
145
|
"redo": "Redo",
|
|
145
146
|
"redoRecording": "Redo recording?",
|