Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { players } from './players'
/**
* Configuration stored in the player_session_preferences JSON config column.
* These are the settings that persist between StartPracticeModal opens.
*/
export interface PlayerSessionPreferencesConfig {
durationMinutes: number
problemLengthPreference: 'shorter' | 'recommended' | 'longer'
partWeights: { abacus: number; visualization: number; linear: number }
purposeWeights: { focus: number; reinforce: number; review: number; challenge: number }
shufflePurposes: boolean
gameBreakEnabled: boolean
gameBreakMinutes: number
gameBreakSelectionMode: string
gameBreakSelectedGame: string | null
gameBreakDifficultyPreset: string | null
/** Whether the selected game should use custom break-game settings instead of a preset. */
gameBreakShowCustomize?: boolean
/** Custom settings for the selected break game. */
gameBreakCustomConfig?: Record<string, unknown>
gameBreakEnabledGames?: string[]
/** Optional narration level for kid-facing explanations. */
kidLanguageStyle?: KidLanguageStyle
/** Whether celebration songs are enabled for this student. Defaults to true when feature flag is on. */
sessionSongEnabled?: boolean
/** Preferred genre for celebration songs. 'shuffle' picks a fresh 2-3 genre mix each song, 'any' rotates single genres. */
sessionSongGenre?: SessionSongGenre
}
export type KidLanguageStyle = 'simple' | 'standard' | 'classical'
/**
* Song genre preference — any string is valid (custom genres supported).
* Preset IDs are convenience values shown in the UI picker.
*/
export type SessionSongGenre = string
export const SESSION_SONG_GENRES: { id: string; label: string }[] = [
{ id: 'shuffle', label: 'Surprise Mix' },
{ id: 'any', label: 'Single Genre (rotate)' },
// Pop / Electronic
{ id: 'pop', label: 'Pop' },
{ id: 'disco', label: 'Disco' },
{ id: 'edm', label: 'EDM' },
{ id: 'chiptune', label: '8-Bit' },
// Soul / Groove
{ id: 'funk', label: 'Funk' },
{ id: 'hip-hop', label: 'Hip-Hop' },
{ id: 'reggae', label: 'Reggae' },
{ id: 'jazz', label: 'Jazz' },
// World / Latin
{ id: 'afrobeat', label: 'Afrobeat' },
{ id: 'salsa', label: 'Salsa' },
{ id: 'bossa-nova', label: 'Bossa Nova' },
{ id: 'bollywood', label: 'Bollywood' },
// Rock / Acoustic
{ id: 'rock', label: 'Rock' },
{ id: 'folk', label: 'Folk' },
{ id: 'country', label: 'Country' },
// Fun / Theatrical
{ id: 'musical-theater', label: 'Broadway' },
{ id: 'marching-band', label: 'Marching Band' },
{ id: 'electro-swing', label: 'Electro Swing' },
]
export const DEFAULT_SESSION_PREFERENCES: PlayerSessionPreferencesConfig = {
durationMinutes: 10,
problemLengthPreference: 'recommended',
partWeights: { abacus: 2, visualization: 1, linear: 0 },
purposeWeights: { focus: 3, reinforce: 1, review: 1, challenge: 1 },
shufflePurposes: true,
gameBreakEnabled: true,
gameBreakMinutes: 5,
gameBreakSelectionMode: 'kid-chooses',
gameBreakSelectedGame: null,
gameBreakDifficultyPreset: 'medium',
gameBreakShowCustomize: false,
gameBreakCustomConfig: {},
gameBreakEnabledGames: [],
}
/**
* Player session preferences table - persists StartPracticeModal settings per student
*
* One row per player. Config is a JSON blob containing all persisted settings.
* Cascade-deletes when the player is deleted.
*/
export const playerSessionPreferences = sqliteTable('player_session_preferences', {
/** Player ID (primary key, FK → players) */
playerId: text('player_id')
.primaryKey()
.references(() => players.id, { onDelete: 'cascade' }),
/** JSON blob containing session preferences */
config: text('config').notNull(),
/** Timestamp of last update (unix ms) */
updatedAt: integer('updated_at').notNull(),
})
export type PlayerSessionPreferences = typeof playerSessionPreferences.$inferSelect
export type NewPlayerSessionPreferences = typeof playerSessionPreferences.$inferInsert
|