MVP
This commit is contained in:
@@ -566,61 +566,7 @@ class _HomeContentState extends State<_HomeContent> {
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
onTap: () async {
|
||||
final service = AiRecommendationService();
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
final response = await service.sendMessage(
|
||||
'vou fazer uma viagem de 4 horas de onibus',
|
||||
silent: true,
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.6,
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.auto_awesome, color: AppColors.primary),
|
||||
const SizedBox(width: 10),
|
||||
const Expanded(
|
||||
child: Text('Sugestao da IA', style: AppText.h3),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 20),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Text(response, style: AppText.body),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () => _requestAiSuggestion(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
@@ -682,6 +628,179 @@ class _HomeContentState extends State<_HomeContent> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> _askOccasion() async {
|
||||
final controller = TextEditingController();
|
||||
final suggestions = [
|
||||
'Piquenique no parque',
|
||||
'Viagem de 4h de onibus',
|
||||
'Dia de praia',
|
||||
'Reuniao de trabalho',
|
||||
'Jantar fora',
|
||||
];
|
||||
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
),
|
||||
backgroundColor: AppColors.surface,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.brandGradient,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.auto_awesome,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Expanded(
|
||||
child: Text('Pedir sugestao', style: AppText.h3),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'Diz a ocasiao para a IA sugerir o que levares.',
|
||||
style: AppText.caption,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
decoration: AppDecorations.outlined(),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (v) => Navigator.pop(ctx, v.trim()),
|
||||
style: AppText.body,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Ex: piquenique no parque',
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Sugestoes rapidas', style: AppText.label),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: suggestions
|
||||
.map(
|
||||
(s) =>
|
||||
AppChip(label: s, onTap: () => Navigator.pop(ctx, s)),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: AppButton(
|
||||
label: 'Pedir',
|
||||
icon: Icons.send_rounded,
|
||||
onPressed: () =>
|
||||
Navigator.pop(ctx, controller.text.trim()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _requestAiSuggestion() async {
|
||||
final occasion = await _askOccasion();
|
||||
if (occasion == null || occasion.trim().isEmpty) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final service = AiRecommendationService();
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(28),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
),
|
||||
child: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(color: AppColors.primary),
|
||||
SizedBox(height: 16),
|
||||
Text('A pedir sugestao...', style: AppText.caption),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final allItems = await service.getItemsWithImages();
|
||||
final response = await service.sendMessage(occasion.trim(), silent: true);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
|
||||
final lines = response
|
||||
.split('\n')
|
||||
.map((l) => l.replaceAll(RegExp(r'^[-•*\d.)\s]+'), '').trim())
|
||||
.where((l) => l.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
final matched = <Map<String, dynamic>>[];
|
||||
for (final item in allItems) {
|
||||
final nome = (item['nome'] ?? '').toString().toLowerCase();
|
||||
for (final line in lines) {
|
||||
if (nome.isNotEmpty &&
|
||||
(line.toLowerCase().contains(nome) ||
|
||||
nome.contains(line.toLowerCase()))) {
|
||||
matched.add(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => _AiSuggestionSheet(
|
||||
matchedItems: matched,
|
||||
rawResponse: response,
|
||||
allItems: allItems,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddCta() {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
@@ -744,3 +863,392 @@ class _HomeContentState extends State<_HomeContent> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AiSuggestionSheet extends StatelessWidget {
|
||||
final List<Map<String, dynamic>> matchedItems;
|
||||
final String rawResponse;
|
||||
final List<Map<String, dynamic>> allItems;
|
||||
|
||||
const _AiSuggestionSheet({
|
||||
required this.matchedItems,
|
||||
required this.rawResponse,
|
||||
required this.allItems,
|
||||
});
|
||||
|
||||
String? _imageUrl(Map<String, dynamic> item) {
|
||||
final images = item['item_images'] as List?;
|
||||
if (images != null && images.isNotEmpty) {
|
||||
return images.first['image_url'] as String?;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.75,
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 12, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.brandGradient,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.auto_awesome,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Expanded(
|
||||
child: Text('Sugestao da IA', style: AppText.h3),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 16),
|
||||
if (matchedItems.isNotEmpty)
|
||||
Flexible(
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: matchedItems.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||
itemBuilder: (_, i) {
|
||||
final item = matchedItems[i];
|
||||
final cat = categoryById(item['categoria'] as String?);
|
||||
final imgUrl = _imageUrl(item);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceAlt,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
child: Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
color: cat.color.withValues(alpha: 0.15),
|
||||
child: imgUrl != null
|
||||
? Image.network(
|
||||
imgUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Icon(
|
||||
cat.icon,
|
||||
color: cat.color,
|
||||
size: 24,
|
||||
),
|
||||
)
|
||||
: Icon(cat.icon, color: cat.color, size: 24),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item['nome'] ?? '',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
Icon(cat.icon, size: 12, color: cat.color),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
cat.name,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: cat.color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(rawResponse, style: AppText.body),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _ActionBtn(
|
||||
icon: Icons.calendar_month_rounded,
|
||||
label: 'Exportar para dia',
|
||||
onTap: () => _exportToDay(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _exportToDay(BuildContext context) {
|
||||
if (matchedItems.isEmpty) {
|
||||
Navigator.pop(context);
|
||||
return;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
final startOfWeek = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
).subtract(Duration(days: now.weekday - 1));
|
||||
final days = List.generate(7, (i) => startOfWeek.add(Duration(days: i)));
|
||||
const dayNames = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sab', 'Dom'];
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (ctx) => Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(ctx).size.height * 0.75,
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Escolher dia', style: AppText.h3),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'Exportar itens sugeridos para qual dia?',
|
||||
style: AppText.caption,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(7, (i) {
|
||||
final d = days[i];
|
||||
final isToday =
|
||||
d.day == now.day &&
|
||||
d.month == now.month &&
|
||||
d.year == now.year;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
onTap: () async {
|
||||
Navigator.pop(ctx);
|
||||
Navigator.pop(context);
|
||||
await _saveToDay(d, context);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: isToday
|
||||
? AppColors.primary.withValues(
|
||||
alpha: 0.12,
|
||||
)
|
||||
: AppColors.surfaceAlt,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppRadius.md,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${d.day}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: isToday
|
||||
? AppColors.primary
|
||||
: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${dayNames[i]}${isToday ? ' (hoje)' : ''}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isToday
|
||||
? AppColors.primary
|
||||
: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 14,
|
||||
color: AppColors.textTertiary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveToDay(DateTime day, BuildContext context) async {
|
||||
try {
|
||||
final user = Supabase.instance.client.auth.currentUser;
|
||||
if (user == null) return;
|
||||
final dateStr =
|
||||
'${day.year.toString().padLeft(4, '0')}-${day.month.toString().padLeft(2, '0')}-${day.day.toString().padLeft(2, '0')}';
|
||||
|
||||
final existing = await Supabase.instance.client
|
||||
.from('plans')
|
||||
.select('id')
|
||||
.eq('user_id', user.id)
|
||||
.eq('data', dateStr)
|
||||
.maybeSingle();
|
||||
|
||||
final int planId;
|
||||
if (existing != null) {
|
||||
planId = existing['id'] as int;
|
||||
} else {
|
||||
final created = await Supabase.instance.client
|
||||
.from('plans')
|
||||
.insert({'user_id': user.id, 'data': dateStr})
|
||||
.select()
|
||||
.single();
|
||||
planId = created['id'] as int;
|
||||
}
|
||||
|
||||
final existingItems = await Supabase.instance.client
|
||||
.from('plan_items')
|
||||
.select('item_id')
|
||||
.eq('plan_id', planId);
|
||||
final existingIds = (existingItems as List)
|
||||
.map((e) => e['item_id'])
|
||||
.toSet();
|
||||
|
||||
final toInsert = matchedItems
|
||||
.where((item) => !existingIds.contains(item['id']))
|
||||
.map((item) => {'plan_id': planId, 'item_id': item['id']})
|
||||
.toList();
|
||||
|
||||
if (toInsert.isNotEmpty) {
|
||||
await Supabase.instance.client.from('plan_items').insert(toInsert);
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
AppSnack.success(
|
||||
context,
|
||||
'${matchedItems.length} itens exportados para ${day.day}/${day.month}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
AppSnack.error(context, 'Erro ao exportar: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionBtn extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ActionBtn({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.brandGradient,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
boxShadow: AppShadows.brand,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: Colors.white, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 0.50,
|
||||
childAspectRatio: 0.70,
|
||||
),
|
||||
itemCount: _filteredItems.length,
|
||||
itemBuilder: (_, i) => _buildGridCard(_filteredItems[i]),
|
||||
@@ -356,8 +356,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
Expanded(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
@@ -377,7 +376,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 10, 12),
|
||||
padding: const EdgeInsets.fromLTRB(10, 8, 10, 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
@@ -37,7 +37,8 @@ class _LoginScreenState extends State<LoginScreen>
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: MediaQuery.of(context).size.height -
|
||||
minHeight:
|
||||
MediaQuery.of(context).size.height -
|
||||
MediaQuery.of(context).padding.top -
|
||||
MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
@@ -55,10 +56,7 @@ class _LoginScreenState extends State<LoginScreen>
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24, bottom: 8),
|
||||
child: Text(
|
||||
'Versão 1.0.0',
|
||||
style: AppText.caption,
|
||||
),
|
||||
child: Text('Versão 1.0.0', style: AppText.caption),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -75,16 +73,9 @@ class _LoginScreenState extends State<LoginScreen>
|
||||
Container(
|
||||
width: 84,
|
||||
height: 84,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.brandGradient,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: AppShadows.brand,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.auto_awesome_rounded,
|
||||
color: Colors.white,
|
||||
size: 42,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(24)),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Image.asset('assets/logoDayMaker.png', fit: BoxFit.cover),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
const Text('DayMaker', style: AppText.h1),
|
||||
@@ -245,8 +236,7 @@ class _LoginScreenState extends State<LoginScreen>
|
||||
}
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final response =
|
||||
await Supabase.instance.client.auth.signInWithPassword(
|
||||
final response = await Supabase.instance.client.auth.signInWithPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ class AiRecommendationService {
|
||||
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.';
|
||||
'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 = [];
|
||||
|
||||
@@ -52,7 +52,7 @@ class AiRecommendationService {
|
||||
];
|
||||
|
||||
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\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});
|
||||
@@ -113,4 +113,18 @@ class AiRecommendationService {
|
||||
}
|
||||
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user