Files
dayMaker_lp/lib/services/ai_recommendation_service.dart
Carlos Correia fee538eebd MVP
2026-05-29 11:03:29 +01:00

131 lines
4.4 KiB
Dart

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 =
'es um assistente que ajuda a montar outfits e escolher o que levar para o dia ou viagem. usa linguagem simples e curta, sem emojis. baseia-te nas tags e notas dos itens do utilizador. responde sempre em portugues e se breve.';
final List<Map<String, String>> _history = [];
Future<String> _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<String> sendMessage(String userMessage, {bool silent = false}) async {
final ctx = await _itemsContext();
final systemContent = ctx.isNotEmpty
? '$_systemPrompt\n\n$ctx'
: _systemPrompt;
final messages = <Map<String, String>>[
{'role': 'system', 'content': systemContent},
..._history,
];
final userContent = silent
? '$userMessage\n\n[Instrucao: responde APENAS com os nomes exatos dos itens do meu inventario que sugeres, um por linha, sem numeracao, sem explicacao, sem comentarios.]'
: 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();
Future<List<Map<String, dynamic>>> getItemsWithImages() async {
try {
final user = Supabase.instance.client.auth.currentUser;
if (user == null) return [];
final rows = await Supabase.instance.client
.from('items')
.select('*, item_images(image_url)')
.eq('user_id', user.id);
return List<Map<String, dynamic>>.from(rows);
} catch (_) {
return [];
}
}
}