@jjlmoya/utils-language 1.9.0 → 1.10.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/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +2 -1
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/language-shadowing-session-planner/bibliography.astro +6 -0
- package/src/tool/language-shadowing-session-planner/bibliography.ts +6 -0
- package/src/tool/language-shadowing-session-planner/component.astro +142 -0
- package/src/tool/language-shadowing-session-planner/controller.ts +152 -0
- package/src/tool/language-shadowing-session-planner/dom-views.ts +82 -0
- package/src/tool/language-shadowing-session-planner/entry.ts +34 -0
- package/src/tool/language-shadowing-session-planner/evaluator.ts +17 -0
- package/src/tool/language-shadowing-session-planner/i18n/de.ts +54 -0
- package/src/tool/language-shadowing-session-planner/i18n/en.ts +128 -0
- package/src/tool/language-shadowing-session-planner/i18n/es.ts +45 -0
- package/src/tool/language-shadowing-session-planner/i18n/fr.ts +45 -0
- package/src/tool/language-shadowing-session-planner/i18n/id.ts +40 -0
- package/src/tool/language-shadowing-session-planner/i18n/it.ts +40 -0
- package/src/tool/language-shadowing-session-planner/i18n/ja.ts +40 -0
- package/src/tool/language-shadowing-session-planner/i18n/ko.ts +40 -0
- package/src/tool/language-shadowing-session-planner/i18n/nl.ts +40 -0
- package/src/tool/language-shadowing-session-planner/i18n/pl.ts +40 -0
- package/src/tool/language-shadowing-session-planner/i18n/pt.ts +40 -0
- package/src/tool/language-shadowing-session-planner/i18n/ru.ts +40 -0
- package/src/tool/language-shadowing-session-planner/i18n/sv.ts +40 -0
- package/src/tool/language-shadowing-session-planner/i18n/tr.ts +40 -0
- package/src/tool/language-shadowing-session-planner/i18n/zh.ts +40 -0
- package/src/tool/language-shadowing-session-planner/index.ts +14 -0
- package/src/tool/language-shadowing-session-planner/language-shadowing-session-planner.css +561 -0
- package/src/tool/language-shadowing-session-planner/logic.test.ts +53 -0
- package/src/tool/language-shadowing-session-planner/logic.ts +94 -0
- package/src/tool/language-shadowing-session-planner/seo.astro +9 -0
- package/src/tool/language-shadowing-session-planner/storage.ts +18 -0
- package/src/tool/language-shadowing-session-planner/timer-view.ts +55 -0
- package/src/tool/language-shadowing-session-planner/timer.test.ts +50 -0
- package/src/tool/language-shadowing-session-planner/timer.ts +158 -0
- package/src/tool/language-shadowing-session-planner/types.ts +32 -0
- package/src/tool/language-shadowing-session-planner/ui.ts +55 -0
- package/src/tools.ts +2 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ShadowingPlanInputs } from './types';
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'jjlmoya-language-shadowing-session-planner';
|
|
4
|
+
|
|
5
|
+
export function readShadowingInputs(): Partial<ShadowingPlanInputs> | null {
|
|
6
|
+
try {
|
|
7
|
+
const stored = window.localStorage.getItem(STORAGE_KEY);
|
|
8
|
+
return stored ? JSON.parse(stored) as Partial<ShadowingPlanInputs> : null;
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function saveShadowingInputs(inputs: ShadowingPlanInputs): void {
|
|
15
|
+
try {
|
|
16
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(inputs));
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ShadowingSessionUI } from './ui';
|
|
2
|
+
import type { ShadowingTimerState } from './timer';
|
|
3
|
+
|
|
4
|
+
export interface TimerViewElements {
|
|
5
|
+
clock: HTMLElement;
|
|
6
|
+
status: HTMLElement;
|
|
7
|
+
block: HTMLElement;
|
|
8
|
+
toggle: HTMLButtonElement;
|
|
9
|
+
reset: HTMLButtonElement;
|
|
10
|
+
sound: HTMLButtonElement;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatClock(seconds: number): string {
|
|
14
|
+
const minutes = Math.floor(seconds / 60).toString().padStart(2, '0');
|
|
15
|
+
const remainder = (seconds % 60).toString().padStart(2, '0');
|
|
16
|
+
return `${minutes}:${remainder}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getBlockLabel(state: ShadowingTimerState, ui: ShadowingSessionUI): string {
|
|
20
|
+
if (state.currentBlockKind === 'shadow') return `${ui.shadowBlock} ${state.currentBlockIndex + 1}`;
|
|
21
|
+
if (state.currentBlockKind === 'pause') return ui.pauseBlock;
|
|
22
|
+
if (state.currentBlockKind === 'buffer') return ui.bufferBlock;
|
|
23
|
+
return ui.timerStartHint;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getStatusLabel(state: ShadowingTimerState, ui: ShadowingSessionUI): string {
|
|
27
|
+
if (state.status === 'running') return ui.timerRunning;
|
|
28
|
+
if (state.status === 'paused') return ui.timerPaused;
|
|
29
|
+
if (state.status === 'complete') return ui.timerComplete;
|
|
30
|
+
return ui.timerIdle;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getToggleLabel(state: ShadowingTimerState, ui: ShadowingSessionUI): string {
|
|
34
|
+
if (state.status === 'running') return ui.pauseTimer;
|
|
35
|
+
if (state.status === 'paused') return ui.resumeTimer;
|
|
36
|
+
return ui.startTimer;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getTimerBlockLabel(state: ShadowingTimerState, ui: ShadowingSessionUI): string {
|
|
40
|
+
if (!state.canStart) return ui.timerNoSchedule;
|
|
41
|
+
if (state.status === 'complete') return ui.timerCompleteDetail;
|
|
42
|
+
return getBlockLabel(state, ui);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function renderTimerState(elements: TimerViewElements, state: ShadowingTimerState, ui: ShadowingSessionUI): void {
|
|
46
|
+
elements.clock.textContent = formatClock(state.remainingSeconds);
|
|
47
|
+
elements.status.textContent = getStatusLabel(state, ui);
|
|
48
|
+
elements.block.textContent = getTimerBlockLabel(state, ui);
|
|
49
|
+
elements.toggle.textContent = getToggleLabel(state, ui);
|
|
50
|
+
elements.toggle.disabled = !state.canStart || state.status === 'complete';
|
|
51
|
+
elements.reset.disabled = state.status === 'idle';
|
|
52
|
+
elements.sound.textContent = state.soundEnabled ? ui.timerSoundOn : ui.timerSoundOff;
|
|
53
|
+
elements.sound.setAttribute('aria-pressed', String(state.soundEnabled));
|
|
54
|
+
elements.status.dataset.state = state.status;
|
|
55
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { calculateShadowingPlan } from './logic';
|
|
3
|
+
import { ShadowingTimer, type ShadowingTimerState } from './timer';
|
|
4
|
+
|
|
5
|
+
describe('ShadowingTimer', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
vi.useRealTimers();
|
|
8
|
+
vi.unstubAllGlobals();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('runs, pauses, resumes, and resets a scheduled plan', () => {
|
|
12
|
+
vi.useFakeTimers();
|
|
13
|
+
vi.setSystemTime(new Date('2026-08-30T20:00:00Z'));
|
|
14
|
+
vi.stubGlobal('window', { setInterval, clearInterval });
|
|
15
|
+
const states: ShadowingTimerState[] = [];
|
|
16
|
+
const timer = new ShadowingTimer({ onChange: (state) => states.push(state) });
|
|
17
|
+
const plan = calculateShadowingPlan({ totalMinutes: 1, clipSeconds: 10, repetitions: 3, pauseSeconds: 2 });
|
|
18
|
+
timer.setPlan(plan);
|
|
19
|
+
|
|
20
|
+
expect(states.at(-1)).toMatchObject({ status: 'idle', remainingSeconds: 34, currentBlockIndex: 0 });
|
|
21
|
+
timer.start();
|
|
22
|
+
vi.advanceTimersByTime(1200);
|
|
23
|
+
expect(states.at(-1)).toMatchObject({ status: 'running', elapsedSeconds: 1, remainingSeconds: 33 });
|
|
24
|
+
|
|
25
|
+
timer.pause();
|
|
26
|
+
expect(states.at(-1)).toMatchObject({ status: 'paused', elapsedSeconds: 1, remainingSeconds: 33 });
|
|
27
|
+
timer.start();
|
|
28
|
+
vi.advanceTimersByTime(1300);
|
|
29
|
+
expect(states.at(-1)).toMatchObject({ status: 'running', elapsedSeconds: 2, remainingSeconds: 32 });
|
|
30
|
+
|
|
31
|
+
timer.reset();
|
|
32
|
+
expect(states.at(-1)).toMatchObject({ status: 'idle', elapsedSeconds: 0, remainingSeconds: 34 });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('completes and toggles sound state', () => {
|
|
36
|
+
vi.useFakeTimers();
|
|
37
|
+
vi.setSystemTime(new Date('2026-08-30T20:00:00Z'));
|
|
38
|
+
vi.stubGlobal('window', { setInterval, clearInterval });
|
|
39
|
+
const states: ShadowingTimerState[] = [];
|
|
40
|
+
const timer = new ShadowingTimer({ onChange: (state) => states.push(state) });
|
|
41
|
+
const plan = calculateShadowingPlan({ totalMinutes: 1, clipSeconds: 1, repetitions: 1, pauseSeconds: 0 });
|
|
42
|
+
timer.setPlan(plan);
|
|
43
|
+
|
|
44
|
+
timer.toggleSound();
|
|
45
|
+
expect(states.at(-1)).toMatchObject({ soundEnabled: false });
|
|
46
|
+
timer.start();
|
|
47
|
+
vi.advanceTimersByTime((plan.scheduledSeconds + 1) * 1000);
|
|
48
|
+
expect(states.at(-1)).toMatchObject({ status: 'complete', remainingSeconds: 0, soundEnabled: false });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { ShadowingPlanResult } from './types';
|
|
2
|
+
|
|
3
|
+
export type ShadowingTimerStatus = 'idle' | 'running' | 'paused' | 'complete';
|
|
4
|
+
|
|
5
|
+
export interface ShadowingTimerState {
|
|
6
|
+
status: ShadowingTimerStatus;
|
|
7
|
+
elapsedSeconds: number;
|
|
8
|
+
remainingSeconds: number;
|
|
9
|
+
currentBlockIndex: number;
|
|
10
|
+
currentBlockKind: 'shadow' | 'pause' | 'buffer' | null;
|
|
11
|
+
soundEnabled: boolean;
|
|
12
|
+
canStart: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface TimerOptions {
|
|
16
|
+
onChange: (state: ShadowingTimerState) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface WindowWithAudioContext extends Window {
|
|
20
|
+
webkitAudioContext?: typeof AudioContext;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ShadowingTimer {
|
|
24
|
+
private plan: ShadowingPlanResult | null = null;
|
|
25
|
+
private status: ShadowingTimerStatus = 'idle';
|
|
26
|
+
private soundEnabled = true;
|
|
27
|
+
private elapsedBeforeRun = 0;
|
|
28
|
+
private startedAt = 0;
|
|
29
|
+
private intervalId: number | null = null;
|
|
30
|
+
private audioContext: AudioContext | null = null;
|
|
31
|
+
|
|
32
|
+
public constructor(private readonly options: TimerOptions) {}
|
|
33
|
+
|
|
34
|
+
public setPlan(plan: ShadowingPlanResult): void {
|
|
35
|
+
this.plan = plan;
|
|
36
|
+
this.reset();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public toggle(): void {
|
|
40
|
+
if (this.status === 'running') {
|
|
41
|
+
this.pause();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.start();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public start(): void {
|
|
48
|
+
if (!this.plan || this.plan.plannedRepetitions === 0 || this.status === 'complete') return;
|
|
49
|
+
this.primeAudio();
|
|
50
|
+
this.status = 'running';
|
|
51
|
+
this.startedAt = Date.now();
|
|
52
|
+
this.intervalId = window.setInterval(() => this.tick(), 250);
|
|
53
|
+
this.emit();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public pause(): void {
|
|
57
|
+
if (this.status !== 'running') return;
|
|
58
|
+
this.elapsedBeforeRun += (Date.now() - this.startedAt) / 1000;
|
|
59
|
+
this.clearInterval();
|
|
60
|
+
this.status = 'paused';
|
|
61
|
+
this.emit();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public reset(): void {
|
|
65
|
+
this.clearInterval();
|
|
66
|
+
this.status = 'idle';
|
|
67
|
+
this.elapsedBeforeRun = 0;
|
|
68
|
+
this.startedAt = 0;
|
|
69
|
+
this.emit();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public toggleSound(): void {
|
|
73
|
+
this.soundEnabled = !this.soundEnabled;
|
|
74
|
+
this.emit();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private tick(): void {
|
|
78
|
+
const elapsed = this.elapsedBeforeRun + (Date.now() - this.startedAt) / 1000;
|
|
79
|
+
if (!this.plan || elapsed >= this.plan.scheduledSeconds) {
|
|
80
|
+
this.elapsedBeforeRun = this.plan?.scheduledSeconds ?? 0;
|
|
81
|
+
this.clearInterval();
|
|
82
|
+
this.status = 'complete';
|
|
83
|
+
this.beep(740, 160);
|
|
84
|
+
this.emit();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const previousBlock = this.getBlockIndex(this.elapsedBeforeRun);
|
|
88
|
+
const currentBlock = this.getBlockIndex(elapsed);
|
|
89
|
+
if (currentBlock !== previousBlock) this.beep(560, 90);
|
|
90
|
+
this.emit(elapsed);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private emit(currentElapsed = this.getElapsedSeconds()): void {
|
|
94
|
+
const currentBlockIndex = this.getBlockIndex(currentElapsed);
|
|
95
|
+
const total = this.plan?.scheduledSeconds ?? 0;
|
|
96
|
+
this.options.onChange({
|
|
97
|
+
status: this.status,
|
|
98
|
+
elapsedSeconds: Math.floor(currentElapsed),
|
|
99
|
+
remainingSeconds: this.getRemainingSeconds(total, currentElapsed),
|
|
100
|
+
currentBlockIndex,
|
|
101
|
+
currentBlockKind: this.getBlockKind(currentBlockIndex),
|
|
102
|
+
soundEnabled: this.soundEnabled,
|
|
103
|
+
canStart: this.canStart(),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private getRemainingSeconds(total: number, elapsed: number): number {
|
|
108
|
+
return Math.max(0, Math.ceil(total - elapsed));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private getBlockKind(index: number): 'shadow' | 'pause' | 'buffer' | null {
|
|
112
|
+
return this.plan?.blocks[index]?.kind ?? null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private canStart(): boolean {
|
|
116
|
+
if (!this.plan) return false;
|
|
117
|
+
return this.plan.status !== 'short' || this.plan.plannedRepetitions > 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private getElapsedSeconds(): number {
|
|
121
|
+
if (this.status !== 'running') return this.elapsedBeforeRun;
|
|
122
|
+
return this.elapsedBeforeRun + (Date.now() - this.startedAt) / 1000;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private getBlockIndex(elapsed: number): number {
|
|
126
|
+
if (!this.plan) return -1;
|
|
127
|
+
const index = this.plan.blocks.findIndex((block) => elapsed < block.endSeconds);
|
|
128
|
+
return index === -1 ? this.plan.blocks.length - 1 : index;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private primeAudio(): void {
|
|
132
|
+
if (!this.soundEnabled) return;
|
|
133
|
+
const audioWindow = window as WindowWithAudioContext;
|
|
134
|
+
const AudioContextConstructor = window.AudioContext ?? audioWindow.webkitAudioContext;
|
|
135
|
+
if (!AudioContextConstructor) return;
|
|
136
|
+
this.audioContext ??= new AudioContextConstructor();
|
|
137
|
+
void this.audioContext.resume();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private beep(frequency: number, duration: number): void {
|
|
141
|
+
if (!this.soundEnabled || !this.audioContext) return;
|
|
142
|
+
const oscillator = this.audioContext.createOscillator();
|
|
143
|
+
const gain = this.audioContext.createGain();
|
|
144
|
+
oscillator.frequency.value = frequency;
|
|
145
|
+
gain.gain.setValueAtTime(0.05, this.audioContext.currentTime);
|
|
146
|
+
gain.gain.exponentialRampToValueAtTime(0.001, this.audioContext.currentTime + duration / 1000);
|
|
147
|
+
oscillator.connect(gain);
|
|
148
|
+
gain.connect(this.audioContext.destination);
|
|
149
|
+
oscillator.start();
|
|
150
|
+
oscillator.stop(this.audioContext.currentTime + duration / 1000);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private clearInterval(): void {
|
|
154
|
+
if (this.intervalId === null) return;
|
|
155
|
+
window.clearInterval(this.intervalId);
|
|
156
|
+
this.intervalId = null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface ShadowingPlanInputs {
|
|
2
|
+
totalMinutes: number;
|
|
3
|
+
clipSeconds: number;
|
|
4
|
+
repetitions: number;
|
|
5
|
+
pauseSeconds: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type ShadowingBlockKind = 'shadow' | 'pause' | 'buffer';
|
|
9
|
+
|
|
10
|
+
export interface ShadowingBlock {
|
|
11
|
+
kind: ShadowingBlockKind;
|
|
12
|
+
index?: number;
|
|
13
|
+
startSeconds: number;
|
|
14
|
+
endSeconds: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ShadowingPlanStatus = 'fits' | 'short' | 'buffer';
|
|
18
|
+
|
|
19
|
+
export interface ShadowingPlanResult {
|
|
20
|
+
inputs: ShadowingPlanInputs;
|
|
21
|
+
budgetSeconds: number;
|
|
22
|
+
requestedSeconds: number;
|
|
23
|
+
activeSeconds: number;
|
|
24
|
+
pauseTotalSeconds: number;
|
|
25
|
+
scheduledSeconds: number;
|
|
26
|
+
remainingSeconds: number;
|
|
27
|
+
requestedRepetitions: number;
|
|
28
|
+
plannedRepetitions: number;
|
|
29
|
+
coveragePercent: number;
|
|
30
|
+
status: ShadowingPlanStatus;
|
|
31
|
+
blocks: ShadowingBlock[];
|
|
32
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface ShadowingSessionUI extends Record<string, string> {
|
|
2
|
+
quickStarts: string;
|
|
3
|
+
quickShort: string;
|
|
4
|
+
quickFocused: string;
|
|
5
|
+
quickLong: string;
|
|
6
|
+
totalMinutes: string;
|
|
7
|
+
clipSeconds: string;
|
|
8
|
+
repetitions: string;
|
|
9
|
+
pauseSeconds: string;
|
|
10
|
+
minutesUnit: string;
|
|
11
|
+
secondsUnit: string;
|
|
12
|
+
passesUnit: string;
|
|
13
|
+
resetLabel: string;
|
|
14
|
+
scheduledShadowing: string;
|
|
15
|
+
passesLabel: string;
|
|
16
|
+
minutesScheduled: string;
|
|
17
|
+
activeSpeaking: string;
|
|
18
|
+
pauseTime: string;
|
|
19
|
+
flexibleBuffer: string;
|
|
20
|
+
timelineLabel: string;
|
|
21
|
+
shadowBlock: string;
|
|
22
|
+
pauseBlock: string;
|
|
23
|
+
bufferBlock: string;
|
|
24
|
+
statusFits: string;
|
|
25
|
+
statusShort: string;
|
|
26
|
+
statusBuffer: string;
|
|
27
|
+
shortDetail: string;
|
|
28
|
+
bufferDetail: string;
|
|
29
|
+
fitsDetail: string;
|
|
30
|
+
budgetNote: string;
|
|
31
|
+
useBuffer: string;
|
|
32
|
+
cueTitle: string;
|
|
33
|
+
cuePlay: string;
|
|
34
|
+
cueSpeak: string;
|
|
35
|
+
cueNotice: string;
|
|
36
|
+
timerTitle: string;
|
|
37
|
+
startTimer: string;
|
|
38
|
+
pauseTimer: string;
|
|
39
|
+
resumeTimer: string;
|
|
40
|
+
resetTimer: string;
|
|
41
|
+
timerSoundOn: string;
|
|
42
|
+
timerSoundOff: string;
|
|
43
|
+
timerIdle: string;
|
|
44
|
+
timerRunning: string;
|
|
45
|
+
timerPaused: string;
|
|
46
|
+
timerComplete: string;
|
|
47
|
+
timerCompleteDetail: string;
|
|
48
|
+
timerStartHint: string;
|
|
49
|
+
timerNoSchedule: string;
|
|
50
|
+
legendShadow: string;
|
|
51
|
+
legendPause: string;
|
|
52
|
+
legendBuffer: string;
|
|
53
|
+
inputHelp: string;
|
|
54
|
+
numberLocale: string;
|
|
55
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ToolDefinition } from './types';
|
|
2
2
|
import { CEFR_LANGUAGE_SKILL_PROFILE_PLANNER_TOOL } from './tool/cefr-language-skill-profile-planner';
|
|
3
|
+
import { LANGUAGE_SHADOWING_SESSION_PLANNER_TOOL } from './tool/language-shadowing-session-planner';
|
|
3
4
|
import { LANGUAGE_LEARNING_STUDY_PLAN_PLANNER_TOOL } from './tool/language-learning-study-plan-planner';
|
|
4
5
|
|
|
5
|
-
export const ALL_TOOLS: ToolDefinition[] = [LANGUAGE_LEARNING_STUDY_PLAN_PLANNER_TOOL, CEFR_LANGUAGE_SKILL_PROFILE_PLANNER_TOOL];
|
|
6
|
+
export const ALL_TOOLS: ToolDefinition[] = [LANGUAGE_LEARNING_STUDY_PLAN_PLANNER_TOOL, CEFR_LANGUAGE_SKILL_PROFILE_PLANNER_TOOL, LANGUAGE_SHADOWING_SESSION_PLANNER_TOOL];
|
|
6
7
|
|
|
7
8
|
export { ALL_ENTRIES } from './entries';
|