import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:get/get.dart'; import 'package:sen_pt/app/data/apiVar.dart'; import 'package:sen_pt/app/data/models/analysis_models.dart'; import 'package:sen_pt/app/data/models/inputLink_models.dart'; class AnalysisProvider extends GetConnect { // Buat job baru Future createJob(Map payload) async { final response = await post(analyzeUrl, payload); if (response.status.hasError) { print('Error: ${response.body}'); return null; } return AnalysisJob.fromJson(response.body); } Future createJobVader(Map payload) async { final response = await post(analyzeUrlVader, payload); if (response.status.hasError) { print('Error: ${response.body}'); return null; } return AnalysisJob.fromJson(response.body); } // Dapatkan progress Stream getProgress(String jobId) { final controller = StreamController.broadcast(); bool canceled = false; String? lastKey; void emitOnce(AnalysisProgress p) { // Include result signature in the dedupe key to differentiate // between an earlier terminal tick without result and a later // terminal tick that includes the final result payload. final resultSig = (p.result == null) ? 'nores' : 'res:${p.result!.count}:${p.result!.categoryEncoded}:${p.result!.category}'; final resumeSig = 'resume:${p.resume.positif}:${p.resume.negatif}:${p.resume.topPhrasesPositif.length}:${p.resume.topPhrasesNegatif.length}'; final key = '${p.ts.toIso8601String()}|${p.percent}|${p.message}|$resultSig|$resumeSig'; if (key == lastKey) return; lastKey = key; controller.add(p); } controller.onCancel = () { canceled = true; }; Future connect({int attempt = 0}) async { if (canceled) return; final client = HttpClient(); try { final request = await client.getUrl(Uri.parse('$progressUrl/$jobId')); request.headers.set(HttpHeaders.acceptHeader, 'text/event-stream'); final response = await request.close(); final buffer = StringBuffer(); final dataEvent = StringBuffer(); void processBuffer() { final text = buffer.toString(); final lines = text.split('\n'); final lastComplete = text.endsWith('\n'); buffer.clear(); for (var i = 0; i < lines.length; i++) { final line = lines[i].trimRight(); final isLast = i == lines.length - 1; if (isLast && !lastComplete) { buffer.write(line); break; } if (line.isEmpty) { if (dataEvent.isNotEmpty) { final payload = dataEvent.toString(); dataEvent.clear(); try { print('SSE payload: $payload'); final map = jsonDecode(payload) as Map; final progress = AnalysisProgress.fromJson(map); print( 'Parsed progress: ${progress.percent} - ${progress.message}', ); emitOnce(progress); // Stop reconnecting on terminal states final st = progress.status.toLowerCase(); if (progress.percent >= 100 || st == 'completed' || st == 'failed') { canceled = true; } } catch (e) { print('SSE parse error: $e'); } } continue; } if (line.startsWith('data:')) { final content = line.substring(5).trimLeft(); if (dataEvent.isNotEmpty) dataEvent.write('\n'); dataEvent.write(content); } else { if (dataEvent.isNotEmpty) dataEvent.write('\n'); dataEvent.write(line); } } } late StreamSubscription> sub; sub = response.listen( (chunk) { buffer.write(utf8.decode(chunk, allowMalformed: true)); processBuffer(); }, onError: (e, st) async { print('SSE error: $e'); await sub.cancel(); if (!canceled) { final delay = Duration( milliseconds: (1000 * (1 << (attempt > 5 ? 5 : attempt))), ); print( 'Reconnecting SSE in ${delay.inMilliseconds} ms (attempt ${attempt + 1})', ); await Future.delayed(delay); connect(attempt: attempt + 1); } }, onDone: () async { buffer.write('\n'); processBuffer(); // Flush remaining event if (dataEvent.isNotEmpty) { final payload = dataEvent.toString(); dataEvent.clear(); try { print('Final SSE payload: $payload'); final map = jsonDecode(payload) as Map; final progress = AnalysisProgress.fromJson(map); print( 'Final parsed progress: ${progress.percent} - ${progress.message}', ); emitOnce(progress); } catch (e) { print('Final SSE parse error: $e'); } } if (!canceled) { final delay = Duration( milliseconds: (1000 * (1 << (attempt > 5 ? 5 : attempt))), ); print( 'SSE done, reconnecting in ${delay.inMilliseconds} ms (attempt ${attempt + 1})', ); await Future.delayed(delay); connect(attempt: attempt + 1); } }, cancelOnError: true, ); controller.onCancel = () async { canceled = true; await sub.cancel(); }; } catch (e) { print('SSE connect error: $e'); if (!canceled) { final delay = Duration( milliseconds: (1000 * (1 << (attempt > 5 ? 5 : attempt))), ); print( 'Reconnect after error in ${delay.inMilliseconds} ms (attempt ${attempt + 1})', ); await Future.delayed(delay); connect(attempt: attempt + 1); } } } // kick off first connection connect(); return controller.stream; } }