131 lines
4.4 KiB
Dart
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 [];
|
|
}
|
|
}
|
|
}
|