import 'dart:convert'; import 'package:dio/dio.dart'; import '../models/conversation.dart'; import '../models/message.dart'; import 'api_client.dart'; class ChatApi { final Dio _dio; const ChatApi(this._dio); Future> getConversations() async { try { final response = await _dio.get('/api/chat/conversations'); final data = response.data as Map; final list = data['conversations'] as List; return list .map((e) => Conversation.fromJson(e as Map)) .toList(); } on DioException catch (e) { throw dioToApp(e); } } Future createConversation(String title) async { try { final response = await _dio.post('/api/chat/conversations', data: { 'title': title, }); return Conversation.fromJson(response.data as Map); } on DioException catch (e) { throw dioToApp(e); } } Future deleteConversation(int id) async { try { await _dio.delete('/api/chat/conversations/$id'); } on DioException catch (e) { throw dioToApp(e); } } // Messages are embedded in the conversation detail response. Future> getMessages(int conversationId) async { try { final response = await _dio.get('/api/chat/conversations/$conversationId'); final conv = response.data as Map; final list = conv['messages'] as List? ?? []; return list .map((e) => Message.fromJson(e as Map)) .where((m) => m.status != 'generating') // skip in-flight placeholders .toList(); } on DioException catch (e) { throw dioToApp(e); } } // Step 1: POST the user message — server starts background generation. Future sendMessage(int conversationId, String content) async { try { await _dio.post( '/api/chat/conversations/$conversationId/messages', data: {'content': content}, ); } on DioException catch (e) { throw dioToApp(e); } } // Step 2: GET the SSE stream and yield text chunks. Stream streamGeneration(int conversationId) async* { try { final response = await _dio.get( '/api/chat/conversations/$conversationId/generation/stream', options: Options( responseType: ResponseType.stream, headers: {'Accept': 'text/event-stream'}, ), ); final stream = (response.data as ResponseBody).stream; final buf = StringBuffer(); String currentEvent = ''; await for (final chunk in stream) { buf.write(utf8.decode(chunk)); final raw = buf.toString(); final lines = raw.split('\n'); // Keep the last (potentially incomplete) line in the buffer. buf.clear(); buf.write(lines.last); for (final line in lines.sublist(0, lines.length - 1)) { if (line.startsWith('event: ')) { currentEvent = line.substring(7).trim(); } else if (line.startsWith('data: ')) { final jsonStr = line.substring(6).trim(); if (currentEvent == 'chunk') { try { final data = json.decode(jsonStr) as Map; final text = data['text'] as String? ?? ''; if (text.isNotEmpty) yield text; } catch (_) {} } else if (currentEvent == 'done' || currentEvent == 'error') { return; } } else if (line.isEmpty) { currentEvent = ''; // blank line = SSE event separator } } } } on DioException catch (e) { throw dioToApp(e); } } }