Fix student_question_answers table creation, clean manifest duplicate units, and calibrate Socratic timeline seeking

This commit is contained in:
Hamza-Ayed
2026-09-03 14:04:39 +03:00
parent b41db82537
commit 7e0aae023a
5 changed files with 141 additions and 128 deletions
@@ -97,14 +97,18 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
void updatePosition(int seconds) {
final currentState = state;
if (currentState is VideoPlaybackReady && currentState.activeCheckpoint == null) {
// Check if this second triggers a Socratic checkpoint
// Check if natural forward playback reaches a Socratic checkpoint
for (var cp in currentState.playbackData.checkpoints) {
final crossedCheckpoint = seconds >= cp.timestampSeconds &&
(_lastObservedPosition < cp.timestampSeconds || _lastObservedPosition > seconds);
if (!currentState.passedCheckpointIds.contains(cp.id) && crossedCheckpoint) {
AppLogger.log('🚨 Socratic Checkpoint Triggered! (${cp.questionText})', tag: 'SOCRATIC_ENGINE');
final hitCheckpoint = !currentState.passedCheckpointIds.contains(cp.id) &&
_lastObservedPosition < cp.timestampSeconds &&
seconds >= cp.timestampSeconds &&
(seconds - _lastObservedPosition).abs() <= 3; // Natural sequential playback
if (hitCheckpoint) {
AppLogger.log('🚨 Socratic Checkpoint Triggered at ${cp.timestampSeconds}s! (${cp.questionText})', tag: 'SOCRATIC_ENGINE');
_lastObservedPosition = cp.timestampSeconds;
emit(currentState.copyWith(
currentPositionSeconds: seconds,
currentPositionSeconds: cp.timestampSeconds,
isPlaying: false, // Freeze video playback
activeCheckpoint: cp,
));
@@ -127,7 +131,25 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
void seekTo(int seconds) {
final currentState = state;
if (currentState is VideoPlaybackReady) {
emit(currentState.copyWith(currentPositionSeconds: seconds));
// Find the earliest unpassed checkpoint before or at this target
SocraticCheckpointModel? blockingCheckpoint;
for (var cp in currentState.playbackData.checkpoints) {
if (!currentState.passedCheckpointIds.contains(cp.id) && seconds > cp.timestampSeconds) {
if (blockingCheckpoint == null || cp.timestampSeconds < blockingCheckpoint.timestampSeconds) {
blockingCheckpoint = cp;
}
}
}
// If user tries to skip past an unpassed checkpoint, snap precisely to that checkpoint
final actualSeek = blockingCheckpoint != null ? blockingCheckpoint.timestampSeconds : seconds;
_lastObservedPosition = actualSeek;
emit(currentState.copyWith(
currentPositionSeconds: actualSeek,
activeCheckpoint: blockingCheckpoint,
isPlaying: blockingCheckpoint == null ? currentState.isPlaying : false,
));
}
}
@@ -155,6 +177,7 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
// Correct Answer -> Reward readiness score (+0.5%) & resume video
AppLogger.log('✅ Correct answer! Rewarding +0.5% readiness.', tag: 'SOCRATIC_ENGINE');
final updatedPassed = Set<int>.from(currentState.passedCheckpointIds)..add(cp.id);
_lastObservedPosition = cp.timestampSeconds;
emit(currentState.copyWith(
clearActiveCheckpoint: true,
isPlaying: true,
@@ -169,8 +192,9 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
}
return true;
} else {
// Wrong Answer -> Socratic Productive Struggle: Rewind video by N seconds
final rewindTo = (currentState.currentPositionSeconds - cp.rewindSecondsOnFail).clamp(0, currentState.playbackData.durationSeconds);
// Wrong Answer -> Socratic Productive Struggle: Rewind video by N seconds from checkpoint
final rewindTo = (cp.timestampSeconds - cp.rewindSecondsOnFail).clamp(0, currentState.playbackData.durationSeconds);
_lastObservedPosition = rewindTo;
AppLogger.log('❌ Incorrect answer. Socratic remediation: Rewinding to ${rewindTo}s.', tag: 'SOCRATIC_ENGINE');
emit(currentState.copyWith(
clearActiveCheckpoint: true,