Update Saqel Platform: 2026-09-09 00:24:41

This commit is contained in:
Hamza-Ayed
2026-09-09 00:24:41 +03:00
parent 956bb4a674
commit 0c239c431d
7 changed files with 275 additions and 27 deletions
@@ -44,9 +44,11 @@ class TeacherLessonAuditModel {
final decisionValue = json['decision']?.toString() ?? 'pending';
final ai = json['ai_analysis'] is Map ? Map<String, dynamic>.from(json['ai_analysis'] as Map) : null;
final qa = ai != null && ai['quality_assessment'] is Map ? Map<String, dynamic>.from(ai['quality_assessment'] as Map) : null;
final cpCount = (ai?['checkpoints_count'] as num?)?.toInt() ?? (json['socratic_stops_count'] as num?)?.toInt() ?? 0;
final alignScore = (qa?['alignment_score'] as num?)?.toInt();
final estimatedCp = (durationSeconds / 600).clamp(1, 4).toInt();
final cpCount = (ai?['checkpoints_count'] as num?)?.toInt() ??
(json['socratic_stops_count'] as num?)?.toInt() ??
(durationSeconds > 0 ? estimatedCp : 0);
final alignScore = (qa?['alignment_score'] as num?)?.toInt() ?? readiness;
return TeacherLessonAuditModel(
lessonTitle: json['lesson_title'] ?? '',
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:http/http.dart' as http;
@@ -6,6 +7,32 @@ import '../../core/services/storage_service.dart';
import '../../core/utils/app_logger.dart';
import '../models/teacher_models.dart';
class MultipartRequestWithProgress extends http.MultipartRequest {
final void Function(int bytes, int totalBytes)? onProgress;
MultipartRequestWithProgress(String method, Uri url, {this.onProgress})
: super(method, url);
@override
http.ByteStream finalize() {
final byteStream = super.finalize();
if (onProgress == null) return byteStream;
final total = contentLength;
int bytes = 0;
final transformer = StreamTransformer<List<int>, List<int>>.fromHandlers(
handleData: (data, sink) {
bytes += data.length;
onProgress!(bytes, total);
sink.add(data);
},
);
return http.ByteStream(byteStream.transform(transformer));
}
}
/**
* ==============================================================================
* SAQEL TEACHER STUDIO - ENTERPRISE REPOSITORY (ZERO-MOCK PRODUCTION RAIL)
@@ -364,14 +391,16 @@ class TeacherRepository {
double? fileSizeMb,
String? filePath,
Uint8List? fileBytes,
void Function(int bytes, int totalBytes)? onProgress,
}) async {
final stopwatch = Stopwatch()..start();
final uploadUri = Uri.parse('$baseUrl${AppConfig.uploadVideoEndpoint}');
try {
final headers = await _authHeaders();
final request = http.MultipartRequest(
final request = MultipartRequestWithProgress(
'POST',
uploadUri,
onProgress: onProgress,
);
request.headers.addAll(headers..remove('Content-Type'));
request.fields.addAll({
@@ -35,6 +35,8 @@ class TeacherStudioState {
final bool isUploading;
final bool isUploaded;
final String? uploadSuccessMessage;
final double uploadProgress;
final String uploadPhase;
const TeacherStudioState({
required this.lessonTitle,
@@ -57,6 +59,8 @@ class TeacherStudioState {
this.isUploading = false,
this.isUploaded = false,
this.uploadSuccessMessage,
this.uploadProgress = 0.0,
this.uploadPhase = 'idle',
});
TeacherStudioState copyWith({
@@ -81,6 +85,8 @@ class TeacherStudioState {
bool? isUploading,
bool? isUploaded,
String? uploadSuccessMessage,
double? uploadProgress,
String? uploadPhase,
}) {
return TeacherStudioState(
lessonTitle: lessonTitle ?? this.lessonTitle,
@@ -103,6 +109,8 @@ class TeacherStudioState {
isUploading: isUploading ?? this.isUploading,
isUploaded: isUploaded ?? this.isUploaded,
uploadSuccessMessage: uploadSuccessMessage ?? this.uploadSuccessMessage,
uploadProgress: uploadProgress ?? this.uploadProgress,
uploadPhase: uploadPhase ?? this.uploadPhase,
);
}
}
@@ -300,7 +308,13 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
(state.selectedFilePath == null && state.selectedFileBytes == null)) {
return false;
}
emit(state.copyWith(isUploading: true, isAuditing: true, clearError: true));
emit(state.copyWith(
isUploading: true,
isAuditing: true,
uploadProgress: 0.0,
uploadPhase: 'uploading',
clearError: true,
));
try {
final res = await repository.uploadLesson(
title: state.lessonTitle,
@@ -312,6 +326,15 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
fileSizeMb: state.selectedFileSizeMb,
filePath: state.selectedFilePath,
fileBytes: state.selectedFileBytes,
onProgress: (sent, total) {
if (total > 0) {
final p = (sent / total).clamp(0.0, 1.0);
emit(state.copyWith(
uploadProgress: p,
uploadPhase: p >= 1.0 ? 'auditing' : 'uploading',
));
}
},
);
final status = res['status']?.toString();
@@ -337,6 +360,8 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
isAuditing: false,
isUploading: false,
isUploaded: false,
uploadProgress: 0.0,
uploadPhase: 'idle',
auditResult: result,
errorMessage: reason,
));
@@ -348,6 +373,8 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
isAuditing: false,
isUploading: false,
isUploaded: true,
uploadProgress: 1.0,
uploadPhase: 'completed',
auditResult: result,
uploadSuccessMessage: msg,
));
@@ -356,6 +383,8 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
emit(state.copyWith(
isAuditing: false,
isUploading: false,
uploadProgress: 0.0,
uploadPhase: 'idle',
errorMessage: e.toString().replaceFirst('Bad state: ', '')));
return false;
}
@@ -491,6 +491,72 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
),
),
),
if (state.isUploading || state.isAuditing) ...[
const SizedBox(height: 14),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF0E1626),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF1E293B)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
color: TeacherTheme.emeraldPrimary,
),
),
const SizedBox(width: 8),
Text(
state.uploadPhase == 'uploading'
? 'جاري نقل ملف الفيديو إلى الخادم...'
: 'جاري فحص الجودة وتجهيز الدرس...',
style: const TextStyle(
color: Colors.white,
fontSize: 12.5,
fontWeight: FontWeight.bold,
),
),
],
),
Text(
state.uploadPhase == 'uploading'
? '${(state.uploadProgress * 100).toInt()}%'
: 'فحص فوري...',
style: const TextStyle(
color: TeacherTheme.emeraldLight,
fontSize: 13,
fontWeight: FontWeight.w900,
),
),
],
),
const SizedBox(height: 10),
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: state.uploadPhase == 'uploading' && state.uploadProgress > 0
? state.uploadProgress
: null,
backgroundColor: const Color(0xFF1E293B),
valueColor: const AlwaysStoppedAnimation<Color>(TeacherTheme.emeraldPrimary),
minHeight: 8,
),
),
],
),
),
],
if (state.errorMessage != null) ...[
const SizedBox(height: 10),
Text(state.errorMessage!,