@magmamath/students-features 1.5.1 → 1.5.3-rc.1
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/index.js +7 -0
- package/dist/commonjs/features/chatbot/index.js.map +1 -1
- package/dist/commonjs/features/chatbot/model/VoiceTranscriptSnapshotModel.js +99 -0
- package/dist/commonjs/features/chatbot/model/VoiceTranscriptSnapshotModel.js.map +1 -0
- package/dist/commonjs/features/pmProgress/model/PmProgressModel.js +7 -1
- package/dist/commonjs/features/pmProgress/model/PmProgressModel.js.map +1 -1
- package/dist/commonjs/features/pmProgress/model/PmRecommendationsModel.js +20 -4
- package/dist/commonjs/features/pmProgress/model/PmRecommendationsModel.js.map +1 -1
- package/dist/commonjs/features/pmProgress/shared/pmProgress.helpers.js +22 -1
- package/dist/commonjs/features/pmProgress/shared/pmProgress.helpers.js.map +1 -1
- package/dist/module/features/chatbot/index.js +1 -0
- package/dist/module/features/chatbot/index.js.map +1 -1
- package/dist/module/features/chatbot/model/VoiceTranscriptSnapshotModel.js +94 -0
- package/dist/module/features/chatbot/model/VoiceTranscriptSnapshotModel.js.map +1 -0
- package/dist/module/features/pmProgress/model/PmProgressModel.js +7 -1
- package/dist/module/features/pmProgress/model/PmProgressModel.js.map +1 -1
- package/dist/module/features/pmProgress/model/PmRecommendationsModel.js +20 -4
- package/dist/module/features/pmProgress/model/PmRecommendationsModel.js.map +1 -1
- package/dist/module/features/pmProgress/shared/pmProgress.helpers.js +20 -0
- package/dist/module/features/pmProgress/shared/pmProgress.helpers.js.map +1 -1
- package/dist/typescript/commonjs/features/chatbot/index.d.ts +1 -0
- package/dist/typescript/commonjs/features/chatbot/index.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/chatbot/model/VoiceTranscriptSnapshotModel.d.ts +27 -0
- package/dist/typescript/commonjs/features/chatbot/model/VoiceTranscriptSnapshotModel.d.ts.map +1 -0
- package/dist/typescript/commonjs/features/chatbot/types/api.types.d.ts +1 -0
- package/dist/typescript/commonjs/features/chatbot/types/api.types.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/pmProgress/model/PmProgressModel.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/pmProgress/model/PmRecommendationsModel.d.ts +9 -38
- package/dist/typescript/commonjs/features/pmProgress/model/PmRecommendationsModel.d.ts.map +1 -1
- package/dist/typescript/commonjs/features/pmProgress/shared/pmProgress.constants.d.ts +1 -1
- package/dist/typescript/commonjs/features/pmProgress/shared/pmProgress.helpers.d.ts +1 -0
- package/dist/typescript/commonjs/features/pmProgress/shared/pmProgress.helpers.d.ts.map +1 -1
- package/dist/typescript/module/features/chatbot/index.d.ts +1 -0
- package/dist/typescript/module/features/chatbot/index.d.ts.map +1 -1
- package/dist/typescript/module/features/chatbot/model/VoiceTranscriptSnapshotModel.d.ts +27 -0
- package/dist/typescript/module/features/chatbot/model/VoiceTranscriptSnapshotModel.d.ts.map +1 -0
- package/dist/typescript/module/features/chatbot/types/api.types.d.ts +1 -0
- package/dist/typescript/module/features/chatbot/types/api.types.d.ts.map +1 -1
- package/dist/typescript/module/features/pmProgress/model/PmProgressModel.d.ts.map +1 -1
- package/dist/typescript/module/features/pmProgress/model/PmRecommendationsModel.d.ts +9 -38
- package/dist/typescript/module/features/pmProgress/model/PmRecommendationsModel.d.ts.map +1 -1
- package/dist/typescript/module/features/pmProgress/shared/pmProgress.constants.d.ts +1 -1
- package/dist/typescript/module/features/pmProgress/shared/pmProgress.helpers.d.ts +1 -0
- package/dist/typescript/module/features/pmProgress/shared/pmProgress.helpers.d.ts.map +1 -1
- package/package.json +2 -3
- package/src/features/chatbot/index.ts +1 -0
- package/src/features/chatbot/model/VoiceTranscriptSnapshotModel.ts +128 -0
- package/src/features/chatbot/types/api.types.ts +1 -0
- package/src/features/pmProgress/model/PmProgressModel.ts +4 -1
- package/src/features/pmProgress/model/PmRecommendationsModel.ts +22 -6
- package/src/features/pmProgress/shared/pmProgress.helpers.ts +33 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createEffect, sample } from 'effector'
|
|
2
|
+
import {
|
|
3
|
+
AudioTranscriptResponse,
|
|
4
|
+
TranscriptionStatus,
|
|
5
|
+
VoiceRecordCollectionItem,
|
|
6
|
+
} from '../../voice/types'
|
|
7
|
+
import { VoiceRecordModel } from '../../voice/recording/model/VoiceRecord.model'
|
|
8
|
+
import { RequestOptionalConfig } from '../../../lib/effector/createControllerEffect'
|
|
9
|
+
|
|
10
|
+
const TRANSCRIPT_RETRY_INTERVAL_MS = 3000
|
|
11
|
+
const TRANSCRIPT_MAX_RETRIES = 20
|
|
12
|
+
|
|
13
|
+
type GetTranscriptApi = {
|
|
14
|
+
getAudioFileTranscript: (audioFileId: string) => Promise<AudioTranscriptResponse>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type CaptureParams = {
|
|
18
|
+
voiceKey: string
|
|
19
|
+
record: VoiceRecordCollectionItem
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type VoiceTranscriptSnapshotModelProps = {
|
|
23
|
+
voiceRecord: VoiceRecordModel
|
|
24
|
+
api: GetTranscriptApi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Voice key: `${assignmentId}-${problemId}-${attemptsCount}`
|
|
28
|
+
// Chat key: `${assignmentId}-${problemId}` — survives attempt increments and voice deletion.
|
|
29
|
+
const toChatKey = (voiceKey: string): string | null => {
|
|
30
|
+
const lastDashIndex = voiceKey.lastIndexOf('-')
|
|
31
|
+
if (lastDashIndex <= 0) return null
|
|
32
|
+
return voiceKey.slice(0, lastDashIndex)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class VoiceTranscriptSnapshotModel {
|
|
36
|
+
public readonly fetchTranscript = (audioFileId: string): Promise<string | undefined> => {
|
|
37
|
+
const cached = this.transcriptByAudioFileId.get(audioFileId)
|
|
38
|
+
if (cached) return cached
|
|
39
|
+
|
|
40
|
+
const promise = this.pollTranscript(audioFileId).catch(() => undefined)
|
|
41
|
+
this.transcriptByAudioFileId.set(audioFileId, promise)
|
|
42
|
+
return promise
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public readonly getTranscriptForCurrent = async (): Promise<string | undefined> => {
|
|
46
|
+
const voiceKey = this.voiceRecord.$currentKey.getState()
|
|
47
|
+
if (!voiceKey) return undefined
|
|
48
|
+
|
|
49
|
+
const chatKey = toChatKey(voiceKey)
|
|
50
|
+
if (chatKey) {
|
|
51
|
+
const snapshot = this.snapshotByChatKey.get(chatKey)
|
|
52
|
+
if (snapshot) return snapshot
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const record = this.voiceRecord.collection.get(voiceKey)
|
|
56
|
+
if (!record) return undefined
|
|
57
|
+
|
|
58
|
+
const audioFileId = await this.resolveAudioFileId(record)
|
|
59
|
+
if (!audioFileId) return undefined
|
|
60
|
+
|
|
61
|
+
return this.fetchTranscript(audioFileId)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public readonly wrapRequestHint = <P extends object, R>(
|
|
65
|
+
apiFn: (
|
|
66
|
+
params: P & { voiceTranscript?: string },
|
|
67
|
+
requestConfig?: RequestOptionalConfig,
|
|
68
|
+
) => Promise<R>,
|
|
69
|
+
) => {
|
|
70
|
+
return async (params: P, requestConfig?: RequestOptionalConfig): Promise<R> => {
|
|
71
|
+
const voiceTranscript = await this.getTranscriptForCurrent()
|
|
72
|
+
return apiFn(voiceTranscript ? { ...params, voiceTranscript } : params, requestConfig)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
constructor({ voiceRecord, api }: VoiceTranscriptSnapshotModelProps) {
|
|
77
|
+
this.voiceRecord = voiceRecord
|
|
78
|
+
this.api = api
|
|
79
|
+
|
|
80
|
+
sample({
|
|
81
|
+
clock: voiceRecord.setCurrentRecord,
|
|
82
|
+
source: voiceRecord.$currentKey,
|
|
83
|
+
filter: (_, record): record is VoiceRecordCollectionItem => !!record,
|
|
84
|
+
fn: (voiceKey, record) => ({ voiceKey, record }),
|
|
85
|
+
target: this.captureSnapshotFx,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private readonly voiceRecord: VoiceRecordModel
|
|
90
|
+
private readonly api: GetTranscriptApi
|
|
91
|
+
private readonly transcriptByAudioFileId = new Map<string, Promise<string | undefined>>()
|
|
92
|
+
private readonly snapshotByChatKey = new Map<string, Promise<string | undefined>>()
|
|
93
|
+
|
|
94
|
+
private readonly captureSnapshotFx = createEffect(async ({ voiceKey, record }: CaptureParams) => {
|
|
95
|
+
const chatKey = toChatKey(voiceKey)
|
|
96
|
+
if (!chatKey) return
|
|
97
|
+
|
|
98
|
+
const audioFileId = await this.resolveAudioFileId(record)
|
|
99
|
+
if (!audioFileId) return
|
|
100
|
+
|
|
101
|
+
this.snapshotByChatKey.set(chatKey, this.fetchTranscript(audioFileId))
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
private async pollTranscript(audioFileId: string): Promise<string | undefined> {
|
|
105
|
+
for (let attempt = 0; attempt < TRANSCRIPT_MAX_RETRIES; attempt++) {
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, TRANSCRIPT_RETRY_INTERVAL_MS))
|
|
107
|
+
|
|
108
|
+
const response = await this.api.getAudioFileTranscript(audioFileId)
|
|
109
|
+
|
|
110
|
+
if (response.status === TranscriptionStatus.COMPLETED) return response.text
|
|
111
|
+
if (response.status === TranscriptionStatus.FAILED) return undefined
|
|
112
|
+
}
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async resolveAudioFileId(
|
|
117
|
+
record: VoiceRecordCollectionItem,
|
|
118
|
+
): Promise<string | undefined> {
|
|
119
|
+
if (record.id) return record.id
|
|
120
|
+
if (!record.audioUploadPromise) return undefined
|
|
121
|
+
try {
|
|
122
|
+
const result = await record.audioUploadPromise
|
|
123
|
+
return result?.id
|
|
124
|
+
} catch {
|
|
125
|
+
return undefined
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -43,7 +43,10 @@ export class PmProgressModel {
|
|
|
43
43
|
|
|
44
44
|
sample({
|
|
45
45
|
clock: this.tree.initTreeFx.done,
|
|
46
|
-
fn: ({ result: [tree] }) =>
|
|
46
|
+
fn: ({ params: { user }, result: [tree] }) => ({
|
|
47
|
+
tree,
|
|
48
|
+
lockedGrades: user.setting.practiceModeLockedGrades,
|
|
49
|
+
}),
|
|
47
50
|
target: this.recommendations.initRecommendationsFx,
|
|
48
51
|
})
|
|
49
52
|
}
|
|
@@ -2,6 +2,12 @@ import { PmProgressApi } from './PmProgressApi'
|
|
|
2
2
|
import { createEffect, createEvent, createStore, sample } from 'effector'
|
|
3
3
|
import { Recommendations, RecsResponse, TreeMappingResponse } from '../shared/pmProgress.types'
|
|
4
4
|
import { RECOMMENDATIONS_LIMIT_PARAM, SolvingFlow } from '../shared/pmProgress.constants'
|
|
5
|
+
import { filterLockedGradeRecs } from '../shared/pmProgress.helpers'
|
|
6
|
+
|
|
7
|
+
type FetchMagmaRecsParams = {
|
|
8
|
+
tree: TreeMappingResponse
|
|
9
|
+
lockedGrades: number[]
|
|
10
|
+
}
|
|
5
11
|
|
|
6
12
|
const EMPTY_RECS: Recommendations = { items: [], offset: 0, limit: 0, total: 0 }
|
|
7
13
|
|
|
@@ -22,14 +28,24 @@ export class PmRecommendationsModel {
|
|
|
22
28
|
),
|
|
23
29
|
)
|
|
24
30
|
|
|
25
|
-
public readonly fetchMagmaRecsFx = createEffect(
|
|
26
|
-
|
|
27
|
-
this.
|
|
28
|
-
|
|
31
|
+
public readonly fetchMagmaRecsFx = createEffect(
|
|
32
|
+
async ({ tree, lockedGrades }: FetchMagmaRecsParams) => {
|
|
33
|
+
const recs = await this.fetchRecs(tree, SolvingFlow.MAGMA, () =>
|
|
34
|
+
this.api.getMagmaRecsFx({ limit: RECOMMENDATIONS_LIMIT_PARAM }),
|
|
35
|
+
)
|
|
36
|
+
return {
|
|
37
|
+
...recs,
|
|
38
|
+
items: filterLockedGradeRecs(recs.items, tree.treeMapping, lockedGrades),
|
|
39
|
+
}
|
|
40
|
+
},
|
|
29
41
|
)
|
|
30
42
|
|
|
31
|
-
public readonly initRecommendationsFx = createEffect(
|
|
32
|
-
|
|
43
|
+
public readonly initRecommendationsFx = createEffect(
|
|
44
|
+
({ tree, lockedGrades }: FetchMagmaRecsParams) =>
|
|
45
|
+
Promise.all([
|
|
46
|
+
this.fetchTeacherRecsFx(tree),
|
|
47
|
+
this.fetchMagmaRecsFx({ tree, lockedGrades }),
|
|
48
|
+
]),
|
|
33
49
|
)
|
|
34
50
|
|
|
35
51
|
private fetchRecs = async (
|
|
@@ -298,6 +298,39 @@ export const formatGradeLabel = (grade: number, customLabelsMap?: GradeLabelsMap
|
|
|
298
298
|
return `${getText('pmProgress.grade')} ${label.name}`
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
const buildSkillGradeMap = (
|
|
302
|
+
treeMapping: TreeMappingResponse['treeMapping'],
|
|
303
|
+
): Map<string, number> => {
|
|
304
|
+
const map = new Map<string, number>()
|
|
305
|
+
|
|
306
|
+
const collectSkills = (nodeId: string, gradeValue: number) => {
|
|
307
|
+
const node = treeMapping[nodeId]
|
|
308
|
+
if (!node) return
|
|
309
|
+
node.skills.forEach((skillId) => map.set(skillId, gradeValue))
|
|
310
|
+
node.children.forEach((childId) => collectSkills(childId, gradeValue))
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
Object.values(treeMapping)
|
|
314
|
+
.filter((node) => node.type === 'grade')
|
|
315
|
+
.forEach((gradeNode) => collectSkills(gradeNode._id, gradeNode.attributes.grade))
|
|
316
|
+
|
|
317
|
+
return map
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export const filterLockedGradeRecs = (
|
|
321
|
+
skills: Skill[],
|
|
322
|
+
treeMapping: TreeMappingResponse['treeMapping'],
|
|
323
|
+
lockedGrades: number[],
|
|
324
|
+
): Skill[] => {
|
|
325
|
+
if (lockedGrades.length === 0) return skills
|
|
326
|
+
const locked = new Set(lockedGrades)
|
|
327
|
+
const skillGradeMap = buildSkillGradeMap(treeMapping)
|
|
328
|
+
return skills.filter((skill) => {
|
|
329
|
+
const grade = skillGradeMap.get(skill._id)
|
|
330
|
+
return grade === undefined || !locked.has(grade)
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
301
334
|
export const getTreeGrades = (tree: TreeMappingResponse, user: User): Grade[] => {
|
|
302
335
|
const lockedGrades = new Set(user.setting.practiceModeLockedGrades)
|
|
303
336
|
|