import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:supabase_flutter/supabase_flutter.dart'; class AiRecommendationService { static const String _apiUrl = 'https://apichat.epvc.pt/api/chat'; static const String _model = 'llama3.2:3b'; static const String _systemPrompt = 'voce é uma agente de ia que tem como objetivo ajudar o utilizador a formar uma especie de outfit e acessorios como consolas e ate documentacao que é preciso para seu dia ou viagem. voce usa uma linguagem descontraida mas sem usar emojis ou afins. para saber oque escolher voce vai usar as tags que estao nos itens ou suas notas. responde sempre em portugues.'; final List> _history = []; Future _itemsContext() async { try { final user = Supabase.instance.client.auth.currentUser; if (user == null) return ''; final rows = await Supabase.instance.client .from('items') .select() .eq('user_id', user.id); if (rows.isEmpty) return ''; final buf = StringBuffer( 'Itens disponiveis no inventario do utilizador:\n', ); for (final it in rows) { final nome = it['nome'] ?? ''; final cat = it['categoria'] ?? ''; final tags = (it['tags'] as List?)?.join(', ') ?? ''; final nota = it['nota'] ?? it['notes'] ?? ''; buf.write('- $nome'); if (cat.toString().isNotEmpty) buf.write(' (categoria: $cat)'); if (tags.isNotEmpty) buf.write(' [tags: $tags]'); if (nota.toString().isNotEmpty) buf.write(' {nota: $nota}'); buf.writeln(); } return buf.toString(); } catch (_) { return ''; } } Future sendMessage(String userMessage, {bool silent = false}) async { final ctx = await _itemsContext(); final systemContent = ctx.isNotEmpty ? '$_systemPrompt\n\n$ctx' : _systemPrompt; final messages = >[ {'role': 'system', 'content': systemContent}, ..._history, ]; final userContent = silent ? '$userMessage\n\n[Instrucao: nao expliques nem comentes. Devolve apenas a lista de itens (do meu inventario quando possivel) que sugeres para esta ocasiao, em formato de lista simples.]' : userMessage; messages.add({'role': 'user', 'content': userContent}); try { final response = await http .post( Uri.parse(_apiUrl), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'model': _model, 'messages': messages, 'stream': false, }), ) .timeout(const Duration(seconds: 60)); if (response.statusCode != 200) { return 'Erro a contactar a IA (${response.statusCode}). Tenta de novo.'; } final data = jsonDecode(utf8.decode(response.bodyBytes)); final aiText = _extract(data); if (aiText.isEmpty) { return 'Nao recebi resposta da IA. Tenta de novo.'; } // guardar no historico (mensagem do user "limpa", sem instrucoes silenciosas) _history.add({'role': 'user', 'content': userMessage}); _history.add({'role': 'assistant', 'content': aiText}); return aiText; } catch (_) { return 'Nao consegui ligar ao servidor. Verifica a tua internet e tenta de novo.'; } } String _extract(dynamic data) { if (data is Map) { // Ollama /api/chat -> { message: { role, content }, done, ... } final msg = data['message']; if (msg is Map && msg['content'] != null) { return msg['content'].toString().trim(); } // Ollama /api/generate -> { response: "..." } if (data['response'] != null) return data['response'].toString().trim(); // OpenAI-style fallback final choices = data['choices']; if (choices is List && choices.isNotEmpty) { final m = choices[0]['message']; if (m is Map && m['content'] != null) { return m['content'].toString().trim(); } } if (data['content'] != null) return data['content'].toString().trim(); } if (data is String) return data.trim(); return ''; } void clearHistory() => _history.clear(); }