Update Saqel Platform: 2026-09-09 00:24:41
This commit is contained in:
@@ -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!,
|
||||
|
||||
Reference in New Issue
Block a user