Fix student_question_answers table creation, clean manifest duplicate units, and calibrate Socratic timeline seeking
This commit is contained in:
@@ -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,
|
||||
|
||||
+57
-46
@@ -435,52 +435,63 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Understanding checkpoints on the timeline
|
||||
Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: 5,
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 7),
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
|
||||
activeTrackColor: AppColors.saqelCyan,
|
||||
inactiveTrackColor: Colors.white24,
|
||||
thumbColor: AppColors.saqelCyan,
|
||||
),
|
||||
child: Slider(
|
||||
value: progress,
|
||||
onChanged: (val) {
|
||||
final targetSec = (val * duration).toInt();
|
||||
context.read<VideoPlaybackCubit>().seekTo(targetSec);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Checkpoint markers on the progress track
|
||||
...state.playbackData.checkpoints.map((cp) {
|
||||
final posFraction = (cp.timestampSeconds / duration).clamp(0.0, 1.0);
|
||||
final isPassed = state.passedCheckpointIds.contains(cp.id);
|
||||
return Positioned(
|
||||
left: (MediaQuery.of(context).size.width * 0.85 * posFraction).clamp(20.0, 700.0),
|
||||
child: Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: isPassed ? AppColors.emeraldGreen : AppColors.guardianAmber,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 1.5),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (isPassed ? AppColors.emeraldGreen : AppColors.guardianAmber).withAlpha(120),
|
||||
blurRadius: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const double horizontalPadding = 24.0;
|
||||
final double trackWidth = (constraints.maxWidth - (horizontalPadding * 2)).clamp(10.0, 4000.0);
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: 5,
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 7),
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
|
||||
activeTrackColor: AppColors.saqelCyan,
|
||||
inactiveTrackColor: Colors.white24,
|
||||
thumbColor: AppColors.saqelCyan,
|
||||
),
|
||||
child: Slider(
|
||||
value: progress,
|
||||
onChanged: (val) {
|
||||
final targetSec = (val * duration).toInt();
|
||||
context.read<VideoPlaybackCubit>().seekTo(targetSec);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Checkpoint markers on the progress track
|
||||
if (duration > 0)
|
||||
...state.playbackData.checkpoints.map((cp) {
|
||||
final posFraction = (cp.timestampSeconds / duration).clamp(0.0, 1.0);
|
||||
final isPassed = state.passedCheckpointIds.contains(cp.id);
|
||||
final dotLeft = horizontalPadding + (trackWidth * posFraction) - 5.0;
|
||||
|
||||
return Positioned(
|
||||
left: dotLeft,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: isPassed ? AppColors.emeraldGreen : AppColors.guardianAmber,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 1.5),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (isPassed ? AppColors.emeraldGreen : AppColors.guardianAmber).withAlpha(120),
|
||||
blurRadius: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
|
||||
@@ -332,10 +332,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.18"
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -348,10 +348,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.18.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -577,10 +577,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.9"
|
||||
version: "0.7.11"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user