This commit is contained in:
Carlos Correia
2026-05-29 11:03:29 +01:00
parent 967584f083
commit fee538eebd
14 changed files with 1349 additions and 1149 deletions

View File

@@ -1,64 +1,79 @@
# 📦 InventoryAI — Project Overview # DayMaker — Project Overview
## Visão Geral ## Visão geral
**InventoryAI** é uma aplicação móvel de gestão de inventário pessoal inteligente. **DayMaker** é uma aplicação móvel Flutter para organizar itens pessoais, planear o que usar/levar durante a semana e pedir sugestões a uma IA com base no inventário real do utilizador.
O utilizador fotografa os seus pertences, a app organiza-os automaticamente por categoria, e sugere o que levar consoante o contexto (viagem, clima, ocasião).
A app permite guardar roupas, eletrónica, calçado, acessórios, documentos e outros itens com categoria, tags e imagem. Depois, esses itens podem ser usados no planeamento semanal ou em sugestões geradas pela IA.
--- ---
## Problema que resolve ## Problema que resolve
As pessoas não sabem o que têm em casa. Perdem tempo a procurar itens, esquecem-se do que possuem, e não conseguem planear eficientemente o que levar para viagens ou eventos. Muitas pessoas esquecem o que têm, perdem tempo a escolher outfits ou não sabem o que levar para viagens, trabalho, praia, piqueniques e outros planos do dia.
O DayMaker centraliza o inventário pessoal e ajuda o utilizador a decidir rapidamente o que precisa.
--- ---
## Proposta de Valor ## Proposta de valor
> "Fotografa uma vez. A app lembra-se para sempre. Diz-te o que levar quando precisas." > "Guarda os teus itens uma vez. Planeia a semana e pede à IA sugestões com o que já tens."
---
## Funcionalidades atuais
- Autenticação com Supabase.
- Home com saudação, resumo do dia, contagem de itens e itens recentes.
- Inventário com pesquisa, filtros por categoria, vista em grid/lista e detalhe do item.
- Adição e edição de itens com categorias e tags.
- Imagens associadas aos itens via `item_images`.
- Planeamento semanal com tabela `plans` e relação `plan_items`.
- Chat com IA usando endpoint Ollama em `https://apichat.epvc.pt/api/chat`.
- Botão "Pedir sugestão à IA" na home, com input de ocasião e sugestões rápidas.
- Sugestões da IA limitadas aos itens existentes no inventário.
- Exibição dos itens sugeridos com imagem, nome e categoria.
- Exportação das sugestões para um dia da semana.
--- ---
## Utilizador-alvo ## Utilizador-alvo
- Pessoas que viajam frequentemente - Pessoas que querem organizar roupa, acessórios e objetos do dia a dia.
- Quem tem muita roupa e não consegue gerir outfits - Utilizadores que planeiam outfits semanais.
- Utilizadores organizados que querem controlo total dos seus bens pessoais - Pessoas que viajam ou preparam atividades com frequência.
- Nómadas digitais e viajantes de longa duração - Estudantes e profissionais que querem saber rapidamente o que levar.
---
## Princípios de Design
1. **Simplicidade primeiro** — o MVP não tem IA avançada. Regras simples parecem inteligentes.
2. **Foto como input principal** — tirar foto é mais rápido do que escrever.
3. **Categorização automática** — o utilizador não quer trabalho manual.
4. **Sugestões contextuais** — a app responde a perguntas reais do utilizador.
5. **Evolução progressiva** — começar simples, adicionar IA com dados reais.
--- ---
## Estado atual do projeto ## Estado atual do projeto
| Fase | Estado | | Área | Estado |
|------|--------| |------|--------|
| Definição do MVP | ✅ Concluído | | App Flutter | Implementada |
| Arquitetura técnica | ✅ Definida | | Autenticação Supabase | Implementada |
| Implementação | 🔲 Por iniciar | | Inventário | Implementado |
| Testes com utilizadores | 🔲 Por iniciar | | Imagens dos itens | Implementado |
| IA avançada | 🔲 Fase futura | | Planeamento semanal | Implementado |
| Chat IA | Implementado |
| Sugestão IA com itens reais | Implementada |
| Exportar sugestão para dia | Implementado |
| Testes automatizados | Pendente |
--- ---
## Ficheiros de contexto deste projeto ## Documentos do projeto
| Ficheiro | Conteúdo | | Ficheiro | Conteúdo |
|----------|----------| |----------|----------|
| `00_PROJECT_OVERVIEW.md` | Este ficheiro — visão geral | | `00_PROJECT_OVERVIEW.md` | Visão geral do projeto |
| `01_MVP_DEFINITION.md` | Funcionalidades mínimas viáveis | | `01_MVP_DEFINITION.md` | Definição do produto atual |
| `02_ARCHITECTURE.md` | Stack técnica e estrutura da app | | `02_ARCHITECTURE.md` | Arquitetura técnica e dados |
| `03_AI_VISION_LAYER.md` | Integração de IA para imagens | | `03_AI_VISION_LAYER.md` | Estado da camada de imagem/visão |
| `04_CATEGORIES_AND_TAGS.md` | Sistema de organização de itens | | `04_CATEGORIES_AND_TAGS.md` | Categorias e tags usadas |
| `05_RECOMMENDATION_ENGINE.md` | Lógica de sugestões (regras + IA futura) | | `05_RECOMMENDATION_ENGINE.md` | Funcionamento das sugestões da IA |
| `06_FUTURE_FEATURES.md` | Roadmap e funcionalidades avançadas | | `06_FUTURE_FEATURES.md` | Roadmap futuro |
| `07_AGENT_BEHAVIOR.md` | Como o agente IA deve comportar-se | | `07_AGENT_BEHAVIOR.md` | Regras para agentes de código |
| `AI_AGENTS_LOG.md` | Histórico técnico das decisões de IA |
| `RULE_ENGINE.md` | Regras atuais do sistema de sugestão |

View File

@@ -1,101 +1,101 @@
# 🎯 MVP — Definição do Produto Mínimo Viável # MVP — Produto Atual
## Objetivo do MVP ## Objetivo
Criar a versão mais simples possível da app que já seja útil para um utilizador real. O produto atual do **DayMaker** é uma app móvel funcional para guardar itens pessoais, consultar o inventário, planear dias da semana e pedir sugestões à IA com base nos itens existentes.
**Sem IA avançada. Sem funcionalidades complexas. Sem over-engineering.**
O MVP deve validar a hipótese central: O foco é oferecer uma experiência simples e útil, sem obrigar o utilizador a configurar regras complexas.
> "Os utilizadores estão dispostos a fotografar os seus itens para depois receber sugestões contextuais."
--- ---
## Funcionalidades obrigatórias (must-have) ## Funcionalidades implementadas
### 1. 📸 Upload / Captura de Foto ### 1. Autenticação
- O utilizador tira foto a um item com a câmara do telemóvel
- Alternativa: selecionar foto da galeria
- A foto é guardada no perfil do item
- **Requisito mínimo:** uma foto por item
### 2. 📦 Guardar Item - Login e registo com Supabase Auth.
- Cada item tem: - Dados associados ao utilizador autenticado.
- `nome` (texto livre ou sugerido pela IA de imagem) - Perfil com nome/email do utilizador.
- `foto` (obrigatória)
- `categoria` (selecionada pelo utilizador ou auto-detetada)
- `tags` (opcional, atribuídas manualmente ou automaticamente)
- `data de adição`
- O item é guardado na base de dados do utilizador
### 3. 🏷️ Categorização ### 2. Home
- O utilizador escolhe a categoria principal do item
- Categorias base disponíveis no MVP:
- Roupa
- Eletrónica
- Calçado
- Acessórios
- Documentos
- Outros
- Subcategorias simples dentro de cada categoria (ver `04_CATEGORIES_AND_TAGS.md`)
### 4. 🔍 Pesquisa e Visualização do Inventário - Saudação personalizada.
- Lista de todos os itens (grid de fotos ou lista) - Card com dia atual.
- Filtrar por categoria - Contagem de itens no inventário.
- Pesquisa por nome ou tag - Lista de itens planeados para hoje.
- Detalhe do item ao clicar - Lista de itens recentes.
- Botão de adicionar item.
- Botão "Pedir sugestão à IA".
### 5. 🎯 Sugestões Simples por Contexto (sem IA) ### 3. Inventário
- O utilizador seleciona um contexto pré-definido:
- "Vou viajar (curta duração)" Cada item contém, conforme disponibilidade da base de dados:
- "Vou viajar (longa duração)"
- "Vou ao trabalho" - `id`
- "Fim de semana casual" - `user_id`
- A app filtra e mostra os itens relevantes para esse contexto - `nome`
- As regras são estáticas e definidas manualmente (ver `05_RECOMMENDATION_ENGINE.md`) - `categoria`
- `tags`
- `nota` ou `notes`
- imagens relacionadas em `item_images(image_url)`
Funcionalidades:
- Vista em grid.
- Vista em lista.
- Pesquisa por nome ou tag.
- Filtro por categoria.
- Detalhe do item.
- Edição de nome, categoria e tags.
- Remoção de itens.
### 4. Planeamento semanal
- Separação por dias da semana.
- Tabela `plans` para representar um plano diário.
- Tabela `plan_items` para associar itens ao plano.
- Adição manual de itens a um dia.
- Remoção de itens de um dia.
- Home mostra os itens do dia atual.
### 5. Chat com IA
- Ecrã `AiChatScreen` acessível pela barra inferior.
- Histórico de conversa em memória durante a sessão do serviço.
- Sugestões rápidas no topo do chat.
- Mensagens com design consistente com o tema da app.
- Integração com Ollama via endpoint remoto.
### 6. Sugestão IA na Home
Fluxo atual:
1. Utilizador toca em "Pedir sugestão à IA".
2. App pede a ocasião, por exemplo: `piquenique no parque`.
3. IA recebe o inventário do utilizador como contexto.
4. IA responde apenas com nomes de itens do inventário.
5. App cruza a resposta com os itens reais.
6. App mostra os itens sugeridos com imagem e categoria.
7. Utilizador pode exportar esses itens para um dia da semana.
--- ---
## Funcionalidades excluídas do MVP ## Funcionalidades fora do escopo atual
> Estas funcionalidades são importantes mas ficam para iterações futuras. - Reconhecimento automático de imagem por IA.
- Previsão meteorológica.
- ❌ Sugestões baseadas em clima - Notificações push.
- ❌ Montagem automática de outfits - Sincronização com calendário externo.
- ❌ Integração com calendário - Partilha de inventário entre utilizadores.
- ❌ Assistente conversacional ("O que devo levar amanhã?") - Recomendação visual por cor/compatibilidade.
- ❌ Sistema de preferências pessoais aprendidas - Testes automatizados completos.
- ❌ Multi-utilizador / partilha
- ❌ Exportação de listas
- ❌ Notificações inteligentes
--- ---
## Fluxo principal do utilizador (MVP) ## Critérios de sucesso atuais
``` - O utilizador consegue criar conta e entrar.
1. Utilizador abre a app - O utilizador consegue adicionar e consultar itens.
2. Tira foto de um item - A pesquisa e filtros devolvem itens corretos.
3. App sugere nome e categoria (via Google Vision) - O utilizador consegue planear itens para qualquer dia da semana.
4. Utilizador confirma ou edita - A IA responde usando o inventário do utilizador.
5. Item guardado no inventário - A sugestão da IA pode ser exportada para a semana.
6. Utilizador acede ao inventário → pesquisa / filtra
7. Utilizador seleciona contexto → app mostra lista filtrada
```
---
## Critérios de sucesso do MVP
- [ ] Utilizador consegue adicionar 10 itens em menos de 5 minutos
- [ ] Pesquisa devolve resultados corretos em menos de 1 segundo
- [ ] Sugestão por contexto é considerada "útil" por 70%+ dos utilizadores testados
- [ ] Taxa de retenção ao fim de 7 dias > 40%
---
## Notas para o agente IA
- Quando o utilizador pedir para construir uma funcionalidade, verificar primeiro se está no MVP ou no roadmap futuro
- Se estiver fora do MVP, avisar e perguntar se deve ser priorizada ou adiada
- Nunca adicionar complexidade desnecessária ao MVP
- Preferir sempre a solução mais simples que funcione

View File

@@ -1,143 +1,159 @@
# 🏗️ Arquitetura Técnica # Arquitetura Técnica
## Stack Tecnológica ## Stack atual
### Decisões principais | Camada | Tecnologia |
|--------|------------|
| Camada | Tecnologia escolhida | Alternativa considerada | Motivo da escolha | | App móvel | Flutter / Dart |
|--------|----------------------|-------------------------|-------------------| | Backend/Auth/DB | Supabase |
| App móvel | React Native | Flutter | Ecosistema JS, mais fácil de integrar com APIs web | | Base de dados | PostgreSQL via Supabase |
| Backend / Auth | Firebase | Supabase | Setup rápido, escalável, gratuito no início | | Imagens | URLs guardados em `item_images` |
| Base de dados | Firestore | PostgreSQL (Supabase) | NoSQL flexível, sincronia em tempo real | | IA de texto | Ollama API remota |
| Armazenamento de imagens | Firebase Storage | AWS S3 | Integrado com Firebase, simples | | Tema visual | Design tokens em `lib/theme/app_theme.dart` |
| IA de imagem | Google Vision AI | AWS Rekognition | Melhor deteção de objetos, preço competitivo |
| IA de texto (futuro) | OpenAI API | Anthropic Claude API | Fase futura — não implementar no MVP |
--- ---
## Estrutura da Aplicação ## Estrutura principal
``` ```text
inventoryai/ lib/
├── mobile/ # App React Native ├── constants/
── src/ ── item_categories.dart
│ │ ├── screens/ # Ecrãs principais ├── login/
│ │ ├── HomeScreen.jsx └── login_screen.dart
│ │ │ ├── AddItemScreen.jsx ├── Screens/
│ │ ├── InventoryScreen.jsx ├── home_screen.dart
│ │ │ ├── ItemDetailScreen.jsx │ ├── item_screen.dart
│ │ └── SuggestionsScreen.jsx ├── add_item_screen.dart
│ ├── components/ # Componentes reutilizáveis │ ├── week_screen.dart
│ ├── services/ # Lógica de negócio e APIs │ ├── ai_chat_screen.dart
│ │ ├── firebase.js └── perfil_screen.dart
│ │ │ ├── visionApi.js ├── services/
│ │ └── suggestions.js └── ai_recommendation_service.dart
│ │ ├── hooks/ # Custom hooks React ├── theme/
│ ├── utils/ # Utilitários └── app_theme.dart
│ │ └── constants/ # Categorias, regras, etc. └── main.dart
│ └── package.json
├── functions/ # Firebase Cloud Functions (opcional)
└── docs/ # Ficheiros .md deste projeto
``` ```
--- ---
## Modelo de Dados (Firestore) ## Navegação
A `HomeScreen` contém uma navegação inferior com 5 áreas:
1. Início
2. Itens
3. Semana
4. IA
5. Perfil
O conteúdo é mantido num `IndexedStack`, permitindo alternar tabs sem recriar todos os ecrãs imediatamente.
---
## Modelo de dados Supabase
### `users`
Usada para dados adicionais do perfil.
| Campo | Tipo esperado |
|-------|---------------|
| `id` | UUID do utilizador |
| `nome` | texto |
### `items`
Representa itens do inventário.
| Campo | Tipo esperado |
|-------|---------------|
| `id` | inteiro/identificador |
| `user_id` | UUID |
| `nome` | texto |
| `categoria` | texto |
| `tags` | array/lista |
| `nota` / `notes` | texto opcional |
### `item_images`
Relaciona imagens a itens.
| Campo | Tipo esperado |
|-------|---------------|
| `item_id` | id do item |
| `image_url` | URL da imagem |
### `plans`
Representa um dia planeado.
| Campo | Tipo esperado |
|-------|---------------|
| `id` | inteiro/identificador |
| `user_id` | UUID |
| `data` | data em `YYYY-MM-DD` |
### `plan_items`
Relaciona itens com planos.
| Campo | Tipo esperado |
|-------|---------------|
| `plan_id` | id do plano |
| `item_id` | id do item |
---
## Serviço de IA
Ficheiro: `lib/services/ai_recommendation_service.dart`
Responsabilidades:
- Obter contexto do inventário do utilizador.
- Enviar mensagens para `https://apichat.epvc.pt/api/chat`.
- Usar modelo `llama3.2:3b`.
- Manter histórico simples em memória.
- Suportar modo silencioso para devolver apenas nomes de itens.
- Carregar itens com imagens para o fluxo de sugestão da home.
Formato da chamada Ollama:
### Coleção: `users/{userId}`
```json ```json
{ {
"uid": "string", "model": "llama3.2:3b",
"email": "string", "messages": [
"displayName": "string", {"role": "system", "content": "..."},
"createdAt": "timestamp", {"role": "user", "content": "..."}
"preferences": { ],
"defaultContext": "travel_short" "stream": false
}
}
```
### Coleção: `users/{userId}/items/{itemId}`
```json
{
"id": "string",
"name": "string",
"photoUrl": "string",
"thumbnailUrl": "string",
"category": "clothing | electronics | footwear | accessories | documents | other",
"subcategory": "string",
"tags": ["string"],
"visionLabels": ["string"],
"contextTags": ["travel", "work", "casual"],
"createdAt": "timestamp",
"updatedAt": "timestamp"
} }
``` ```
--- ---
## Fluxo de Adição de Item ## Tema visual
``` O tema é centralizado em `app_theme.dart`:
[Utilizador tira foto]
- `AppColors`
[Upload para Firebase Storage] - `AppRadius`
- `AppSpacing`
[Chamada à Google Vision API] - `AppShadows`
- `AppText`
[Receber labels automáticas] - `AppDecorations`
- `AppButton`
[Mapear labels → categoria + tags sugeridas] - `AppChip`
- `AppSnack`
[Mostrar ao utilizador para confirmar/editar]
Novos ecrãs devem reutilizar estes tokens para manter consistência.
[Guardar item no Firestore]
```
--- ---
## Autenticação ## Segurança
- Firebase Authentication - Todas as queries de dados usam `user_id` do utilizador autenticado quando aplicável.
- Login com Google (obrigatório no MVP) - A app não deve hardcodar chaves privadas.
- Login com email/password (opcional no MVP) - A API de IA atual é endpoint remoto público do projeto, sem chave no cliente.
- Todos os dados são isolados por `userId` - Dados de inventário enviados à IA incluem nomes, categorias, tags e notas dos itens.
---
## Performance e Limites
| Recurso | Limite gratuito Firebase | Estimativa uso MVP |
|---------|--------------------------|---------------------|
| Firestore reads | 50.000/dia | ~5.000/dia (100 users) |
| Firestore writes | 20.000/dia | ~2.000/dia |
| Storage | 5 GB | ~1 GB para 100 users |
| Google Vision API | 1.000 unidades/mês grátis | ~500 unidades/mês |
> Para o MVP com utilizadores de teste, o tier gratuito é suficiente.
---
## Segurança (Firestore Rules — MVP)
```javascript
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId}/{document=**} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
```
---
## Notas para o agente IA
- **Não** sugerir AWS, Azure ou outras clouds — a decisão está tomada: Firebase
- **Não** sugerir Flutter — a decisão está tomada: React Native
- Quando criar código de serviço, usar o padrão `services/` definido acima
- O modelo de dados do Firestore é o definido neste ficheiro — não alterar sem avisar
- Para novas funcionalidades, verificar se precisam de novas coleções no Firestore
- Imagens são sempre guardadas no Firebase Storage antes de qualquer processamento

View File

@@ -1,204 +1,80 @@
# 👁️ Camada de Visão por IA # Camada de Imagem e Visão
## Objetivo ## Estado atual
Usar IA de visão computacional para **identificar automaticamente** o que está na foto tirada pelo utilizador, sugerindo: O DayMaker guarda imagens dos itens, mas **não faz reconhecimento automático de imagem** no produto atual.
- Nome do item
- Categoria
- Tags relevantes
Isto reduz o esforço do utilizador ao mínimo. A imagem de cada item é tratada como dado visual associado ao inventário. A app espera receber ou guardar URLs em `item_images(image_url)` e usa essas imagens para:
- Mostrar cards no inventário.
- Mostrar detalhes do item.
- Mostrar itens planeados na semana.
- Mostrar sugestões da IA com imagem quando há correspondência com itens reais.
--- ---
## Serviço escolhido: Google Vision API ## Fluxo atual de imagem
### Porquê Google Vision? ```text
- Fácil integração com Firebase (mesmo ecosistema Google) Item criado/editado
- Excelente deteção de objetos do quotidiano
- Labels em múltiplos idiomas Imagem associada ao item
- Tier gratuito generoso (1.000 unidades/mês)
- Documentação extensa URL guardado em item_images.image_url
### Funcionalidades usadas no MVP Ecrãs carregam items com item_images(image_url)
| Feature | Uso | Image.network mostra a imagem ou fallback por categoria
|---------|-----|
| `LABEL_DETECTION` | Identificar o objeto (ex: "T-shirt", "Laptop") |
| `OBJECT_LOCALIZATION` | Confirmar que existe um objeto na foto |
> No MVP **não** usamos OCR, Safe Search ou outras features avançadas.
---
## Integração técnica
### Chamada à API (exemplo React Native)
```javascript
// services/visionApi.js
const VISION_API_KEY = process.env.GOOGLE_VISION_API_KEY;
const VISION_API_URL = `https://vision.googleapis.com/v1/images:annotate?key=${VISION_API_KEY}`;
export async function analyzeImage(base64Image) {
const requestBody = {
requests: [
{
image: { content: base64Image },
features: [
{ type: 'LABEL_DETECTION', maxResults: 10 },
{ type: 'OBJECT_LOCALIZATION', maxResults: 5 }
]
}
]
};
const response = await fetch(VISION_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
const data = await response.json();
return parseVisionResponse(data);
}
```
### Parsing da resposta
```javascript
// services/visionApi.js (continuação)
export function parseVisionResponse(data) {
const labels = data.responses[0]?.labelAnnotations || [];
const objects = data.responses[0]?.localizedObjectAnnotations || [];
// Extrair labels com score > 0.7
const highConfidenceLabels = labels
.filter(l => l.score > 0.7)
.map(l => l.description.toLowerCase());
// Mapear para categoria da app
const category = mapLabelsToCategory(highConfidenceLabels);
// Sugerir nome baseado no objeto mais confiante
const suggestedName = objects[0]?.name || labels[0]?.description || 'Item';
return {
suggestedName,
category,
tags: highConfidenceLabels.slice(0, 5),
rawLabels: highConfidenceLabels
};
}
``` ```
--- ---
## Mapeamento de Labels → Categorias ## Ficheiros relevantes
```javascript | Ficheiro | Responsabilidade |
// constants/categoryMapping.js |----------|------------------|
| `lib/Screens/add_item_screen.dart` | Criação de itens e associação de dados |
export const LABEL_TO_CATEGORY = { | `lib/Screens/item_screen.dart` | Inventário, detalhe, edição e imagem do item |
// Roupa | `lib/Screens/week_screen.dart` | Mostra imagens dos itens planeados |
't-shirt': 'clothing', | `lib/Screens/home_screen.dart` | Mostra itens recentes, hoje e sugestões IA |
'shirt': 'clothing',
'dress': 'clothing',
'jacket': 'clothing',
'coat': 'clothing',
'jeans': 'clothing',
'trousers': 'clothing',
'sweater': 'clothing',
'clothing': 'clothing',
'fashion': 'clothing',
// Eletrónica
'laptop': 'electronics',
'computer': 'electronics',
'smartphone': 'electronics',
'tablet': 'electronics',
'headphones': 'electronics',
'camera': 'electronics',
'charger': 'electronics',
'cable': 'electronics',
'electronics': 'electronics',
// Calçado
'shoe': 'footwear',
'boot': 'footwear',
'sneaker': 'footwear',
'sandal': 'footwear',
'footwear': 'footwear',
// Acessórios
'bag': 'accessories',
'backpack': 'accessories',
'watch': 'accessories',
'sunglasses': 'accessories',
'hat': 'accessories',
'belt': 'accessories',
// Documentos
'passport': 'documents',
'document': 'documents',
'book': 'documents',
'notebook': 'documents',
};
export function mapLabelsToCategory(labels) {
for (const label of labels) {
if (LABEL_TO_CATEGORY[label]) {
return LABEL_TO_CATEGORY[label];
}
}
return 'other'; // fallback
}
```
--- ---
## Fluxo de UX ao adicionar item ## Fallback visual
``` Quando um item não tem imagem ou a imagem falha ao carregar:
1. Utilizador tira foto
2. App mostra loading ("A analisar item...")
3. Google Vision responde com labels
4. App mostra:
- Nome sugerido (editável)
- Categoria sugerida (editável)
- Tags sugeridas (checkboxes, editáveis)
5. Utilizador confirma ou ajusta
6. Item guardado
```
> O utilizador tem **sempre** controlo final. A IA apenas sugere. - A UI mostra um fundo com a cor da categoria.
- A UI mostra o ícone da categoria.
Isto evita cards vazios e mantém consistência visual.
--- ---
## Tratamento de erros ## Reconhecimento automático por IA
| Erro | Comportamento | Reconhecimento automático por imagem **não está implementado**.
|------|---------------|
| API indisponível | Mostrar formulário manual, sem sugestões | Pode ser uma funcionalidade futura para sugerir:
| Foto sem objeto reconhecível | Mostrar sugestão "Outro" e pedir nome manual |
| Score muito baixo (< 0.5) | Ignorar label, não sugerir | - Nome do item.
| Timeout | Retry 1x, depois formulário manual | - Categoria.
- Tags.
- Cor dominante.
- Tipo de peça ou objeto.
Possíveis serviços futuros:
- Google Vision API.
- Modelos multimodais via API externa.
- Modelo local/servidor próprio.
--- ---
## Evolução futura desta camada ## Regras para futuras implementações
- Fase 2: Treinar modelo custom com os dados dos utilizadores - Não guardar API keys privadas no cliente Flutter.
- Fase 3: Identificar marcas e modelos específicos - O utilizador deve poder confirmar/editar qualquer sugestão automática.
- Fase 3: Reconhecimento de cor dominante para filtros de outfit - Nunca substituir nome, categoria ou tags sem confirmação.
- Manter fallback manual caso a análise de imagem falhe.
--- - Atualizar este documento antes de implementar visão computacional.
## Notas para o agente IA
- A chave da API (`GOOGLE_VISION_API_KEY`) nunca deve aparecer em código client-side em produção — usar Firebase Cloud Functions como proxy
- No MVP, a chamada pode ser feita diretamente do cliente para simplicidade
- Nunca guardar `rawLabels` como `name` sem confirmação do utilizador
- O mapeamento de labels é iterativo — quando encontrar labels não mapeadas, adicionar ao ficheiro `categoryMapping.js`

View File

@@ -1,146 +1,102 @@
# 🏷️ Sistema de Categorias e Tags # Sistema de Categorias e Tags
## Filosofia ## Filosofia
O sistema de organização deve ser: O sistema de organização deve ser simples, visual e útil para recomendações.
- **Simples o suficiente** para não sobrecarregar o utilizador
- **Flexível o suficiente** para cobrir a maioria dos casos reais Categorias identificam o tipo principal do item. Tags descrevem contexto de uso, como `casual`, `work`, `travel` ou `gaming`.
- **Extensível** para futuras funcionalidades (outfits, clima, etc.)
--- ---
## Categorias Principais (MVP) ## Implementação atual
| ID | Nome | Ícone | Descrição | As categorias e tags vivem em:
|----|------|-------|-----------|
| `clothing` | Roupa | 👕 | Peças de vestuário | ```text
| `electronics` | Eletrónica | 💻 | Dispositivos e acessórios tecnológicos | lib/constants/item_categories.dart
| `footwear` | Calçado | 👟 | Sapatos, botas, sandálias | ```
| `accessories` | Acessórios | 🎒 | Bolsas, relógios, óculos, bijuteria |
| `documents` | Documentos | 📄 | Passaporte, cartões, papéis importantes | A UI usa estas constantes para:
| `other` | Outros | 📦 | Tudo o resto |
- Mostrar filtros horizontais no inventário.
- Mostrar ícones e cores por categoria.
- Permitir seleção de categoria ao adicionar/editar item.
- Permitir seleção de tags de contexto.
- Enviar contexto útil à IA.
--- ---
## Subcategorias por Categoria ## Categorias principais
### 👕 Roupa (`clothing`) | ID esperado | Nome | Uso |
``` |-------------|------|-----|
casual → t-shirts, calças de ganga, hoodies | `roupa` / equivalente | Roupa | Peças de vestuário |
formal → fatos, camisas, vestidos de cerimónia | `eletronica` / equivalente | Eletrónica | Consolas, portáteis, carregadores, gadgets |
sportswear → leggings, tops de treino, shorts | `calcado` / equivalente | Calçado | Botas, sapatilhas, sapatos |
outerwear → casacos, impermeáveis, parkas | `acessorios` / equivalente | Acessórios | Mochilas, bolsas, relógios, óculos |
underwear → roupa interior, meias | `documentos` / equivalente | Documentos | Cartões, papéis, bilhetes, identificação |
sleepwear → pijamas, roupões | `outros` / equivalente | Outros | Itens fora das categorias principais |
```
### 💻 Eletrónica (`electronics`) A nomenclatura exata deve seguir `item_categories.dart`, porque a UI usa esses IDs diretamente.
```
computers → portáteis, tablets
phones → smartphones, earphones
cameras → máquinas fotográficas, action cams
cables → carregadores, cabos USB, adaptadores
gaming → consolas, comandos, jogos
audio → headphones, colunas bluetooth
```
### 👟 Calçado (`footwear`)
```
casual → sapatilhas, loafers
formal → sapatos de salto, mocassins
sport → ténis de corrida, chuteiras
outdoor → botas de caminhada, sandálias
```
### 🎒 Acessórios (`accessories`)
```
bags → mochilas, malas, bolsas
watches → relógios analógicos e digitais
eyewear → óculos de sol, óculos de grau
jewelry → colares, pulseiras, brincos
hats → bonés, chapéus, gorros
belts → cintos
```
### 📄 Documentos (`documents`)
```
identity → passaporte, BI, carta de condução
health → cartão de saúde, receitas
travel → bilhetes, reservas, seguros
financial → cartões de crédito/débito
```
--- ---
## Tags de Contexto ## Tags de contexto
Estas tags determinam em que situações o item é relevante para sugestões: As tags são livres dentro das opções fornecidas pela app. Exemplos úteis:
| Tag | Descrição | Exemplos de itens | | Tag | Contexto |
|-----|-----------|-------------------| |-----|----------|
| `travel` | Qualquer tipo de viagem | Mala, passaporte, adaptador de tomadas | | `casual` | Uso diário e informal |
| `work` | Ambiente de trabalho | Portátil, fato, sapatos formais | | `formal` | Reuniões, eventos e ocasiões formais |
| `casual` | Dia-a-dia informal | Jeans, t-shirt, sapatilhas | | `work` | Trabalho ou estudo |
| `sport` | Atividade física | Leggings, ténis de corrida, garrafa de água | | `travel` | Viagens e deslocações |
| `formal` | Eventos formais | Fato, vestido, sapatos de salto | | `sport` | Treino/desporto |
| `outdoor` | Exterior e natureza | Blusão, botas, impermeável | | `outdoor` | Atividades exteriores |
| `beach` | Praia ou piscina | Biquíni, chinelos, protetor solar | | `beach` | Praia/piscina |
| `cold` | Clima frio | Casaco, cachecol, luvas | | `cold` | Tempo frio |
| `hot` | Clima quente | Roupa leve, t-shirts, sandálias | | `hot` | Tempo quente |
| `gaming` | Consolas e entretenimento |
--- ---
## Atribuição Automática de Tags de Contexto ## Como as tags são usadas pela IA
Baseado na categoria e subcategoria, a app atribui context tags automaticamente: O serviço `AiRecommendationService` envia à IA um contexto com:
```javascript ```text
// constants/contextTagRules.js - nome do item
- categoria
export const AUTO_CONTEXT_TAGS = { - tags
// Por categoria - nota/notes, quando existe
'clothing.formal': ['work', 'formal'],
'clothing.casual': ['casual', 'travel'],
'clothing.sportswear':['sport'],
'clothing.outerwear': ['travel', 'cold', 'outdoor'],
'electronics.computers': ['work', 'travel'],
'electronics.phones': ['travel', 'work', 'casual'],
'electronics.cables': ['travel'],
'electronics.gaming': ['casual', 'travel'],
'footwear.formal': ['work', 'formal'],
'footwear.casual': ['casual', 'travel'],
'footwear.sport': ['sport'],
'footwear.outdoor': ['outdoor', 'travel'],
'accessories.bags': ['travel', 'work', 'casual'],
'documents.identity':['travel'],
'documents.travel': ['travel'],
};
``` ```
--- Exemplo:
## Interface de Tags (UX) ```text
- Switch (categoria: Eletrónica) [tags: gaming, casual]
- Camisa verde (categoria: Roupa) [tags: formal, work]
```
### Ao adicionar um item: Quando o utilizador pede uma sugestão, por exemplo `piquenique no parque`, a IA deve escolher apenas itens compatíveis com o pedido e existentes no inventário.
1. Categoria é sugerida automaticamente (Google Vision)
2. Subcategoria é apresentada como lista (scroll horizontal)
3. Tags de contexto são pré-selecionadas automaticamente
4. Utilizador pode adicionar/remover tags livremente
5. Máximo de **10 tags por item** no MVP
### Pesquisa por tags:
- Filtro por categoria (dropdown ou tabs)
- Filtro por context tag (chips selecionáveis)
- Pesquisa textual (nome ou qualquer tag)
--- ---
## Notas para o agente IA ## Pesquisa e filtros
- As categorias e subcategorias definidas aqui são **fixas** no MVP — não criar novas sem validar No inventário, o utilizador pode:
- As context tags são a base do sistema de sugestões em `05_RECOMMENDATION_ENGINE.md`
- Quando o utilizador edita manualmente as tags, essas preferências devem ser guardadas e têm prioridade sobre as automáticas - Pesquisar por nome.
- O sistema de tags deve ser simples de perceber sem documentação — usar labels em português na UI - Pesquisar por tag.
- Filtrar por categoria.
- Alternar entre vista em grid e lista.
---
## Regras para manutenção
- Não criar novas categorias diretamente na UI sem atualizar `item_categories.dart`.
- Tags devem ser claras e curtas.
- Preferir tags em minúsculas e sem espaços.
- Se uma categoria nova for adicionada, garantir ícone, cor e nome visível.
- A IA depende da qualidade das tags e notas; manter esses campos simples e úteis.

View File

@@ -1,187 +1,133 @@
# 🎯 Motor de Recomendações # Motor de Recomendações e IA
## Filosofia ## Estado atual
> "Não precisas de IA para parecer inteligente. Precisas de regras bem pensadas." O DayMaker usa uma abordagem baseada em **IA de linguagem com contexto do inventário**.
O motor de recomendações evolui em 3 fases: Não existe atualmente um motor de regras local como fonte principal das recomendações. A recomendação é gerada pelo serviço `AiRecommendationService`, que chama uma API Ollama remota.
| Fase | Tecnologia | Estado |
|------|-----------|--------|
| 1 — Regras estáticas | Lógica JS simples | ✅ MVP |
| 2 — IA de linguagem | OpenAI API | 🔲 Fase 2 |
| 3 — Recomendação personalizada | Histórico + ML | 🔲 Fase 3 |
--- ---
## FASE 1 — Regras Estáticas (MVP) ## Serviço principal
### Contextos disponíveis Ficheiro:
O utilizador escolhe um dos seguintes contextos: ```text
lib/services/ai_recommendation_service.dart
```javascript
// constants/contexts.js
export const CONTEXTS = [
{
id: 'travel_short',
label: 'Viagem curta (1-3 dias)',
icon: '✈️',
tags: ['travel'],
priorityCategories: ['documents', 'clothing', 'electronics', 'footwear'],
maxItems: 20,
},
{
id: 'travel_long',
label: 'Viagem longa (4+ dias)',
icon: '🧳',
tags: ['travel'],
priorityCategories: ['documents', 'clothing', 'electronics', 'footwear', 'accessories'],
maxItems: 40,
},
{
id: 'work',
label: 'Dia de trabalho',
icon: '💼',
tags: ['work'],
priorityCategories: ['electronics', 'clothing', 'footwear', 'accessories'],
maxItems: 15,
},
{
id: 'casual',
label: 'Fim de semana casual',
icon: '😎',
tags: ['casual'],
priorityCategories: ['clothing', 'footwear', 'accessories'],
maxItems: 10,
},
{
id: 'sport',
label: 'Treino / Desporto',
icon: '🏃',
tags: ['sport'],
priorityCategories: ['clothing', 'footwear'],
maxItems: 8,
},
{
id: 'outdoor',
label: 'Saída para o exterior',
icon: '🌲',
tags: ['outdoor', 'travel'],
priorityCategories: ['clothing', 'footwear', 'accessories'],
maxItems: 12,
},
];
``` ```
### Lógica de filtragem Responsabilidades:
```javascript - Buscar itens do utilizador no Supabase.
// services/suggestions.js - Construir contexto textual do inventário.
- Enviar mensagens para a API Ollama.
export function getSuggestionsForContext(items, contextId) { - Manter histórico simples de conversa.
const context = CONTEXTS.find(c => c.id === contextId); - Suportar `silent: true` para sugestões estruturadas.
if (!context) return []; - Buscar itens com imagens para a home.
// 1. Filtrar itens com pelo menos uma tag do contexto
const relevantItems = items.filter(item =>
item.contextTags.some(tag => context.tags.includes(tag))
);
// 2. Ordenar por prioridade de categoria
const sorted = relevantItems.sort((a, b) => {
const aIndex = context.priorityCategories.indexOf(a.category);
const bIndex = context.priorityCategories.indexOf(b.category);
const aPriority = aIndex === -1 ? 999 : aIndex;
const bPriority = bIndex === -1 ? 999 : bIndex;
return aPriority - bPriority;
});
// 3. Limitar ao máximo de itens
return sorted.slice(0, context.maxItems);
}
```
### Exemplos de output
**Contexto: "Viagem curta"**
```
✅ Passaporte (documents.identity → travel)
✅ Portátil (electronics.computers → travel, work)
✅ Carregador USB-C (electronics.cables → travel)
✅ T-shirt preta (clothing.casual → casual, travel)
✅ Jeans azul (clothing.casual → casual, travel)
✅ Sapatilhas brancas (footwear.casual → casual, travel)
❌ Fato de treino (clothing.sportswear → sport) ← não aparece
❌ Consola PS5 (electronics.gaming → casual) ← não aparece
```
--- ---
## FASE 2 — IA de Linguagem (OpenAI API) ## Endpoint e modelo
> **Não implementar no MVP. Esta secção é referência para a Fase 2.** | Campo | Valor |
|-------|-------|
| Endpoint | `https://apichat.epvc.pt/api/chat` |
| Modelo | `llama3.2:3b` |
| Stream | `false` |
| Formato | Ollama `/api/chat` |
### Objetivo Payload base:
Permitir ao utilizador escrever em linguagem natural: ```json
- "Vou viajar 4 horas de comboio"
- "Tenho uma reunião importante amanhã de manhã"
- "Fim de semana na praia"
### Arquitetura proposta
```
Utilizador escreve frase
OpenAI API (classificação de contexto)
Retorna JSON estruturado:
{ {
"context": "travel", "model": "llama3.2:3b",
"duration": "short", "messages": [
"environment": "transit", {"role": "system", "content": "..."},
"weather": "unknown", {"role": "user", "content": "..."}
"formality": "casual" ],
"stream": false
} }
Motor de regras Fase 1 (com parâmetros enriquecidos)
Lista de itens sugeridos
```
### Prompt base para OpenAI
```
System: És um assistente de inventário pessoal.
Analisa a frase do utilizador e devolve APENAS um JSON com:
- context: "travel" | "work" | "casual" | "sport" | "outdoor" | "formal"
- duration: "short" | "medium" | "long" | null
- environment: "indoor" | "outdoor" | "transit" | null
- weather: "cold" | "hot" | "rainy" | null
- formality: "casual" | "formal" | null
User: {frase do utilizador}
``` ```
--- ---
## FASE 3 — Recomendação Personalizada ## Prompt de sistema
> **Fase muito futura. Apenas visão.** A IA é instruída a:
- Sistema aprende quais itens o utilizador seleciona/ignora das sugestões - Ajudar a montar outfits e escolher o que levar.
- Pontuação de relevância por item por contexto - Usar linguagem simples e curta.
- Integração com API de clima para filtros automáticos - Não usar emojis.
- Histórico de viagens para sugestões baseadas em padrões - Basear-se nas tags e notas dos itens.
- Responder sempre em português.
--- ---
## Notas para o agente IA ## Contexto enviado à IA
- **No MVP, usar APENAS a Fase 1.** Não implementar OpenAI no MVP. O contexto inclui os itens do utilizador:
- A lógica de `getSuggestionsForContext` deve ser pura (sem side effects)
- Os contextos são fixos no MVP — não adicionar sem validar com o utilizador ```text
- A ordenação por `priorityCategories` é intencional e importante — respeitá-la Itens disponiveis no inventario do utilizador:
- Quando a Fase 2 for implementada, o output do OpenAI deve mapear para os IDs de contexto da Fase 1 - Bota verde (categoria: Roupa) [tags: casual, outdoor]
- Switch (categoria: Eletrónica) [tags: gaming, casual]
```
Esse contexto é anexado à mensagem de sistema.
---
## Chat IA
No ecrã `AiChatScreen`:
- O utilizador escreve livremente.
- Existem sugestões rápidas no topo.
- A resposta é apresentada como conversa.
- O histórico fica em memória no serviço enquanto a instância existir.
---
## Sugestão IA na Home
Fluxo:
1. Utilizador toca em `Pedir sugestão à IA`.
2. App abre um diálogo e pede a ocasião.
3. O utilizador pode escrever algo como `piquenique no parque` ou escolher chip rápido.
4. A app envia a ocasião em modo `silent`.
5. A IA deve devolver apenas nomes exatos dos itens, um por linha.
6. A app cruza esses nomes com os itens reais do Supabase.
7. A app mostra cards com imagem, nome e categoria.
8. O utilizador pode exportar os itens para um dia da semana.
---
## Modo silencioso
Quando `silent: true`, a instrução acrescentada ao pedido é:
```text
responde APENAS com os nomes exatos dos itens do meu inventario que sugeres, um por linha, sem numeracao, sem explicacao, sem comentarios.
```
Isto permite transformar a resposta da IA numa lista de itens reais.
---
## Limitações conhecidas
- A correspondência depende da IA devolver nomes próximos aos nomes reais.
- Se a IA devolver texto extra, a app tenta limpar linhas, mas pode falhar correspondência.
- A IA não vê imagens, apenas nomes, categorias, tags e notas.
- Não há ranking local por clima, cor ou histórico de uso.
---
## Melhorias futuras
- Resposta em JSON em vez de texto simples.
- Validação local mais robusta por ID de item.
- Sugestões com clima e duração.
- Preferências aprendidas por histórico.
- Integração com calendário.

View File

@@ -1,119 +1,90 @@
# 🚀 Roadmap e Funcionalidades Futuras # Roadmap e Funcionalidades Futuras
## Visão a longo prazo ## Visão a longo prazo
> "Uma app que conhece tudo o que possuis e te diz exatamente o que precisas, quando precisas." > "Uma app que conhece o teu inventário e te ajuda a preparar qualquer dia, viagem ou ocasião."
--- ---
## Fases de desenvolvimento ## Estado atual concluído
``` O produto atual já inclui:
FASE 1 — MVP (agora)
├── Upload de fotos
├── Guardar itens
├── Categorização manual + auto (Google Vision)
├── Pesquisa / filtro
└── Sugestões simples por regras
FASE 2 — IA de Linguagem - Autenticação Supabase.
├── Input em linguagem natural - Inventário com imagens, categorias e tags.
├── Integração OpenAI API - Pesquisa e filtros.
└── Contextos dinâmicos - Planeamento semanal.
- Chat IA.
FASE 3 — Personalização - Sugestões IA com base nos itens reais.
├── Aprendizagem por histórico - Exportação de sugestões para dias da semana.
├── Integração com clima
└── Sugestões proativas
FASE 4 — Social e Avançado
├── Outfits automáticos
├── Pack automático para viagens
├── Integração com calendário
└── Assistente conversacional
```
--- ---
## Funcionalidades por fase ## Próximas melhorias recomendadas
### 🔵 Fase 2 — IA de Linguagem ### 1. Melhorar robustez das sugestões IA
**Input em linguagem natural** - Fazer a IA devolver JSON com IDs ou nomes estruturados.
- Utilizador escreve ou fala: "Vou a uma conferência em Londres por 3 dias" - Validar itens sugeridos localmente antes de mostrar.
- App interpreta e gera lista personalizada - Mostrar mensagem clara quando nenhum item corresponde.
- Implementado via OpenAI API (ver `05_RECOMMENDATION_ENGINE.md`) - Permitir editar a lista antes de exportar.
**Contextos dinâmicos** ### 2. Checklist por plano
- Em vez de escolher de uma lista, o utilizador descreve a situação
- App infere contexto, duração, clima esperado, formalidade - Marcar item como preparado/levado.
- Separar itens planeados de itens concluídos.
- Mostrar progresso por dia.
### 3. Notificações
- Lembrete de itens planeados para amanhã.
- Lembrete na manhã do dia.
- Notificação quando uma viagem/plano tem poucos itens.
### 4. Clima e localização
- Utilizador informa destino/data.
- App consulta previsão meteorológica.
- IA recebe clima como contexto adicional.
- Sugestões ajustadas para frio, calor ou chuva.
### 5. Reconhecimento por imagem
- Sugerir categoria e tags a partir da foto.
- Detetar cor dominante.
- Melhorar recomendações de outfit.
### 6. Preferências e histórico
- Guardar sugestões aceites/ignoradas.
- Priorizar itens usados com frequência.
- Evitar sugerir itens que o utilizador remove sempre.
### 7. Calendário externo
- Integração com Google Calendar / Apple Calendar.
- Sugestões proativas baseadas em eventos.
--- ---
### 🟡 Fase 3 — Personalização ## Funcionalidades fora do escopo atual
**Aprendizagem por histórico** | Funcionalidade | Estado |
- App regista quais itens o utilizador aceitou/ignorou nas sugestões |----------------|--------|
- Com 10+ interações, começa a ajustar pesos de relevância | Clima automático | Futuro |
- Exemplo: utilizador nunca leva sapatos formais em viagens curtas → app para de sugerir | Notificações push | Futuro |
| Reconhecimento de imagem | Futuro |
**Integração com API de clima** | Calendário externo | Futuro |
- Utilizador indica destino e datas | Partilha/social | Futuro |
- App consulta previsão meteorológica | Recomendação visual por cor | Futuro |
- Filtra roupa por temperatura e condições | Testes automatizados completos | Pendente |
- APIs candidatas: OpenWeatherMap, WeatherAPI
**Sugestões proativas (push notifications)**
- "Tens uma reunião amanhã — lembra-te do teu fato"
- "Viagem na sexta — queres ver a tua lista?"
--- ---
### 🔴 Fase 4 — Funcionalidades Avançadas ## Prioridade sugerida
**Montagem automática de outfits** 1. Resposta IA estruturada em JSON.
- App combina peças de roupa compatíveis visualmente 2. Checklist por plano.
- Usa cor dominante extraída das fotos 3. Notificações simples.
- Sugere conjuntos completos (peça cima + baixo + calçado + acessório) 4. Clima.
5. Visão por imagem.
**Pack automático para viagens**
- Utilizador diz: "Vou 5 dias ao Porto, clima ameno, reuniões + lazer"
- App gera lista completa, organizada por tipo de item
- Permite marcar o que já está na mala (checklist)
**Integração com Google Calendar / Apple Calendar**
- App lê eventos do calendário
- Antecipa necessidades: "Reunião importante na 5ª feira → fato?"
- Notificações contextuais automáticas
**Assistente conversacional**
- Chat dentro da app
- Perguntas como: "O que devo levar amanhã?"
- Resposta baseada em calendário + clima + inventário
- Implementado via OpenAI Assistants API ou similar
**Funcionalidades sociais (opcional)**
- Partilhar listas com parceiro de viagem
- "O que ele/ela já vai levar?" para evitar duplicações
- Listas de viagem colaborativas
---
## Integrações externas planeadas
| Serviço | Fase | Objetivo |
|---------|------|----------|
| Google Vision AI | 1 (MVP) | Identificar objetos nas fotos |
| OpenAI API | 2 | Processar linguagem natural |
| OpenWeatherMap | 3 | Dados de clima por destino |
| Google Calendar | 4 | Antecipação de necessidades |
| Apple Calendar | 4 | Antecipação de necessidades (iOS) |
---
## Notas para o agente IA
- Quando o utilizador pedir uma funcionalidade das Fases 2-4, não recusar — mas clarificar que está fora do MVP e perguntar se deve ser planeada para uma fase futura
- Não implementar funcionalidades de fases futuras sem confirmação explícita do utilizador
- Ao planear uma funcionalidade futura, criar um documento de especificação separado antes de implementar
- A ordem das fases é intencional — não saltar fases sem validação com utilizadores reais

View File

@@ -1,184 +1,104 @@
# 🤖 Comportamento do Agente IA # Comportamento do Agente IA
## Propósito deste ficheiro ## Propósito deste ficheiro
Este ficheiro define como um agente IA (assistente de código, Cursor, Copilot, Claude, etc.) deve comportar-se quando trabalha neste projeto. Este ficheiro define como um agente IA de código deve comportar-se quando trabalha no projeto **DayMaker**.
**Lê este ficheiro primeiro antes de qualquer tarefa.**
--- ---
## Contexto do projeto ## Contexto do projeto
Estás a trabalhar num projeto chamado **InventoryAI** — uma app móvel de gestão de inventário pessoal com sugestões contextuais. DayMaker é uma app Flutter com Supabase para inventário pessoal, planeamento semanal e sugestões por IA usando Ollama.
Lê os seguintes ficheiros para entender o projeto completo: Antes de alterações grandes, consultar:
1. `00_PROJECT_OVERVIEW.md` — visão geral e princípios
2. `01_MVP_DEFINITION.md` — o que está incluído no MVP 1. `00_PROJECT_OVERVIEW.md`
3. `02_ARCHITECTURE.md` — stack técnica e modelo de dados 2. `01_MVP_DEFINITION.md`
4. `03_AI_VISION_LAYER.md` — integração com Google Vision 3. `02_ARCHITECTURE.md`
5. `04_CATEGORIES_AND_TAGS.md` — sistema de organização 4. `05_RECOMMENDATION_ENGINE.md`
6. `05_RECOMMENDATION_ENGINE.md` — lógica de sugestões
7. `06_FUTURE_FEATURES.md` — roadmap futuro
--- ---
## Regras de comportamento ## Regras de comportamento
### ✅ SEMPRE fazer ### Sempre fazer
- **Verificar o ficheiro MVP** antes de implementar qualquer funcionalidade — só implementar o que está em `01_MVP_DEFINITION.md` - Respeitar a stack atual: Flutter, Dart, Supabase e Ollama API.
- **Respeitar a stack definida** em `02_ARCHITECTURE.md` — Firebase, React Native, Google Vision - Usar os componentes e tokens de `lib/theme/app_theme.dart`.
- **Usar o modelo de dados** exatamente como definido (campos, tipos, nomes) - Manter queries Supabase filtradas por `user_id` quando os dados forem do utilizador.
- **Manter o código simples** — se há duas formas, escolher a mais simples - Verificar o modelo de dados antes de alterar tabelas ou campos.
- **Comentar decisões de arquitetura** no código quando relevante - Manter a UI consistente com a navegação atual: Início, Itens, Semana, IA e Perfil.
- **Avisar** quando uma funcionalidade pedida está fora do MVP - Usar `flutter analyze --no-pub` depois de alterações relevantes.
- **Perguntar** antes de alterar o modelo de dados ou a arquitetura - Explicar ao utilizador quando uma funcionalidade exige alteração de base de dados.
- **Criar ficheiros de serviço** em `src/services/` para lógica de negócio
- **Usar constantes** de `src/constants/` para valores fixos (categorias, contextos, etc.)
### ❌ NUNCA fazer ### Nunca fazer
- Adicionar funcionalidades que não estão no MVP sem confirmação explícita - Reintroduzir Firebase, Firestore ou React Native.
- Mudar a stack tecnológica (ex: sugerir Flutter, Supabase, AWS) - Assumir que Google Vision está implementado.
- Implementar OpenAI no MVP — fica para a Fase 2 - Trocar o endpoint/modelo da IA sem testar ou explicar.
- Guardar a API key do Google Vision em código client-side em produção - Hardcodar API keys privadas.
- Criar lógica de base de dados fora dos ficheiros `services/` - Enviar dados sensíveis desnecessários à IA.
- Usar `console.log` em produção para dados sensíveis do utilizador - Criar categorias fora de `item_categories.dart` sem atualizar a documentação.
- Alterar os nomes dos campos do modelo de dados sem avisar
--- ---
## Linguagem e comunicação ## Padrões de código
- Comunicar em **português** (de Portugal, não do Brasil) ### Flutter/Dart
- Explicações técnicas devem ser **claras e objetivas**
- Quando há uma decisão de arquitetura, **explicar o porquê** - Widgets em `PascalCase`.
- Usar **exemplos de código** sempre que possível - Métodos privados com `_camelCase`.
- Avisos sobre o MVP devem ser **visíveis** (usar emoji ⚠️ ou bloco de nota) - Serviços em `lib/services/`.
- Telas em `lib/Screens/` ou pasta equivalente já existente.
- Imports no topo do ficheiro.
### UI
Usar preferencialmente:
- `AppColors`
- `AppText`
- `AppRadius`
- `AppShadows`
- `AppDecorations`
- `AppButton`
- `AppChip`
- `AppSnack`
### Supabase
- Ler utilizador com `Supabase.instance.client.auth.currentUser`.
- Não executar operações de dados se `user == null`.
- Usar `.eq('user_id', user.id)` em tabelas por utilizador.
--- ---
## Ordem de construção obrigatória ## IA e sugestões
Seguir esta ordem ao implementar. Não avançar para o próximo passo sem o anterior estar funcional: O fluxo atual usa `AiRecommendationService`.
``` - Chat livre: `sendMessage(text)`.
PASSO 1: 📸 Upload de fotos - Sugestão estruturada: `sendMessage(text, silent: true)`.
└── Câmara + seleção de galeria - Itens com imagem: `getItemsWithImages()`.
└── Upload para Firebase Storage
└── URL guardado no estado
PASSO 2: 📦 Guardar itens A IA deve receber contexto de inventário e responder em português.
└── Formulário básico (nome, categoria)
└── Gravar no Firestore
└── Autenticação Firebase
PASSO 3: 🏷️ Categorização
└── Integração Google Vision API
└── Mapeamento labels → categorias
└── UI de confirmação/edição
PASSO 4: 🔍 Pesquisa e visualização
└── Lista de itens (grid)
└── Filtro por categoria
└── Pesquisa por nome
PASSO 5: 🎯 Sugestões simples
└── Seleção de contexto
└── Lógica de filtragem por tags
└── Apresentação da lista sugerida
```
--- ---
## Como responder a pedidos fora do MVP ## Pedidos fora do produto atual
Quando o utilizador pede uma funcionalidade que está fora do MVP: Se o utilizador pedir clima, notificações, visão por imagem ou calendário externo:
``` 1. Explicar que é funcionalidade futura.
⚠️ MVP: Esta funcionalidade não está incluída no MVP atual. 2. Perguntar se deve implementar agora.
3. Avisar se exigir nova dependência, API key ou alteração de base de dados.
Está documentada como [Fase X] em 06_FUTURE_FEATURES.md.
Opções:
1. Adiar para a Fase X (recomendado — manter foco no MVP)
2. Priorizar agora (implica atrasar outras funcionalidades do MVP)
O que preferes?
```
--- ---
## Padrões de código obrigatórios ## Checklist antes de terminar uma tarefa
### Nomenclatura - Código compila com `flutter analyze --no-pub`.
```javascript - Não há imports não usados.
// Componentes React: PascalCase - UI segue tema atual.
ItemCard.jsx - Documentação atualizada quando a funcionalidade muda comportamento.
AddItemScreen.jsx - Resumo final indica ficheiros alterados.
// Serviços e utilitários: camelCase
visionApi.js
suggestions.js
// Constantes: camelCase (ficheiros), UPPER_SNAKE_CASE (valores)
categoryMapping.js export const LABEL_TO_CATEGORY = { ... }
contexts.js export const CONTEXTS = [ ... ]
```
### Estrutura de um serviço
```javascript
// services/exemplo.js
// 1. Imports
import { db } from './firebase';
// 2. Funções puras (sem side effects)
export function processarDados(input) { ... }
// 3. Funções de base de dados
export async function guardarItem(userId, item) { ... }
export async function obterItens(userId) { ... }
```
### Tratamento de erros
```javascript
// Sempre usar try/catch em chamadas assíncronas
try {
const resultado = await guardarItem(userId, novoItem);
// sucesso
} catch (erro) {
console.error('Erro ao guardar item:', erro.message);
// mostrar mensagem ao utilizador (não o erro técnico)
Alert.alert('Erro', 'Não foi possível guardar o item. Tenta novamente.');
}
```
---
## Perguntas frequentes
**P: O utilizador pediu uma funcionalidade — implemento já?**
R: Verificar `01_MVP_DEFINITION.md`. Se estiver no MVP, implementar. Se não, avisar e perguntar.
**P: Posso usar uma biblioteca nova?**
R: Só se resolver um problema que não tem solução simples sem ela. Avisar antes de instalar.
**P: O modelo de dados precisa de um campo novo — adiciono?**
R: Avisar o utilizador, explicar o impacto, aguardar confirmação.
**P: A API do Google Vision não está a funcionar nos testes — o que fazer?**
R: Criar um mock local (`services/visionApiMock.js`) que retorna dados fixos para desenvolvimento.
---
## Ficheiros de referência rápida
| Quero saber... | Ficheiro |
|----------------|----------|
| O que está no MVP? | `01_MVP_DEFINITION.md` |
| Que tecnologias usar? | `02_ARCHITECTURE.md` |
| Como a IA de imagem funciona? | `03_AI_VISION_LAYER.md` |
| Que categorias existem? | `04_CATEGORIES_AND_TAGS.md` |
| Como funcionam as sugestões? | `05_RECOMMENDATION_ENGINE.md` |
| O que vem a seguir? | `06_FUTURE_FEATURES.md` |

View File

@@ -1,70 +1,81 @@
# 📝 AI Agents Log # AI Agents Log
## Propósito ## Propósito
Este ficheiro regista toda a atividade do agente de recomendação. Este ficheiro regista decisões e alterações relevantes relacionadas com a IA e recomendações do DayMaker.
Cada execução gera uma entrada neste log.
--- ---
## Formato de Entrada ## Formato recomendado
### Data: ```text
### Data
YYYY-MM-DD YYYY-MM-DD
### Agente: ### Área
Recommender-MVP Chat IA / Sugestão IA / Inventário / Planeamento / Documentação
### Tipo: ### Descrição
- recomendacao Resumo do que mudou.
- erro
- melhoria
### Descrição: ### Impacto
O que foi feito Como afeta o utilizador ou o código.
```
### Itens afetados:
Lista de itens usados
### Ação tomada:
O que se decidiu
### Notas:
Observações úteis
---
## Regras de Logging
1. **SEMPRE** registar após cada execução
2. Tipo `recomendacao` → quando sugestões são geradas com sucesso
3. Tipo `erro` → quando lista vazia, pedido ambíguo ou dados incompletos
4. Tipo `melhoria` → quando se detetam problemas no sistema
5. Nunca omitir entradas de erro — são tão importantes como sucessos
--- ---
## Histórico ## Histórico
### Data: ### Data
2026-04-24 2026-04-24
### Agente: ### Área
Recommender-MVP Recomendação inicial
### Tipo: ### Descrição
recomendacao Primeiras ideias de motor de recomendações baseado em regras estáticas e contextos.
### Descrição: ### Impacto
Implementação inicial do motor de recomendações MVP. Sistema de regras estáticas com 6 contextos, parser por palavras-chave, logging e deteção de melhorias. Serviu como base conceptual, mas não representa a implementação final.
### Itens afetados: ---
T-shirt preta, Portátil, Nintendo Switch, Fones de ouvido, Passaporte, Carregador USB-C, Jeans azul, Sapatilhas brancas, Mochila, Fato de treino
### Ação tomada: ### Data
Criado APPprojetoLP.dart com modelo de dados alinhado com 02_ARCHITECTURE.md, contextos de 05_RECOMMENDATION_ENGINE.md, e tags de 04_CATEGORIES_AND_TAGS.md 2026-05-21
### Notas: ### Área
- Parser de pedidos é básico (palavras-chave). Fase 2 usará OpenAI API. Chat IA
- Clima e formalidade são detetados mas não filtram itens no MVP.
- Duração "medium" (ex: 4 horas) não tem regras específicas ainda. ### Descrição
Integração do chat com endpoint Ollama remoto `https://apichat.epvc.pt/api/chat` usando o modelo `llama3.2:3b`.
### Impacto
A app passou a ter ecrã de conversa com IA acessível pela barra inferior.
---
### Data
2026-05-22
### Área
Sugestão IA na Home
### Descrição
Criado fluxo de sugestão por ocasião. O utilizador escreve algo como `piquenique no parque`; a IA responde com nomes de itens do inventário; a app cruza com itens reais, mostra imagens e permite exportar para a semana.
### Impacto
A sugestão deixou de ser texto solto e passou a produzir itens acionáveis.
---
### Data
2026-05-28
### Área
Documentação
### Descrição
Documentação reescrita para refletir o projeto final: Flutter, Supabase, inventário, planeamento semanal, chat IA e sugestões Ollama.
### Impacto
Removidas referências antigas a React Native/Firebase/OpenAI como stack atual.

View File

@@ -1,115 +1,93 @@
# ⚙️ Rule Engine — Regras de Recomendação (MVP) # Regras do Sistema de Sugestão
## Propósito ## Estado atual
Define as regras estáticas que o motor de recomendações segue para sugerir itens ao utilizador. O DayMaker não usa atualmente um motor de regras local como fonte principal de recomendações.
A sugestão é gerada pela IA através de `AiRecommendationService`, usando o inventário real do utilizador como contexto.
--- ---
## Regras Ativas (MVP) ## Regras ativas
### R1 — Filtragem por Context Tags ### R1 — Usar apenas inventário do utilizador
```
IF item.contextTags contém pelo menos uma tag do contexto A IA recebe apenas itens do utilizador autenticado.
THEN item é relevante
```text
items.user_id == currentUser.id
``` ```
### R2 — Ordenação por Prioridade de Categoria ### R2 — Contexto textual dos itens
```
Ordenar itens pela posição da categoria em context.priorityCategories
Categorias fora da lista → prioridade baixa (999)
```
### R3 — Limite de Itens Cada item é enviado com:
```
Retornar no máximo context.maxItems itens
```
| Contexto | maxItems | - nome
|----------|----------| - categoria
| travel_short | 20 | - tags
| travel_long | 40 | - nota/notes, quando disponível
| work | 15 |
| casual | 10 |
| sport | 8 |
| outdoor | 12 |
### R4Interpretação de Pedido (Parser) ### R3Modo silencioso para sugestões
```
Palavras-chave → contexto + duração + clima + formalidade
viagem/comboio/voo → travel_short + transit Quando a sugestão vem da Home, o serviço usa `silent: true` e instrui a IA a responder apenas com nomes exatos dos itens.
trabalho/reunião → work + indoor
fim de semana/casual/lazer → casual
treino/desporto/gym → sport
exterior/caminhada → outdoor + outdoor
4+ dias/semana/longa → duration:long + upgrade para travel_long ### R4 — Correspondência com itens reais
4 horas/média → duration:medium
1-2 horas/curta → duration:short
frio/inverno → weather:cold A app cruza as linhas devolvidas pela IA com os nomes dos itens carregados do Supabase.
calor/verão/praia → weather:hot
chuva → weather:rainy
formal/cerimónia → formality:formal Apenas itens correspondidos são mostrados com imagem e podem ser exportados.
casual/informal → formality:casual
``` ### R5 — Exportação para semana
Ao exportar:
1. A app cria ou encontra um registo em `plans` para o dia escolhido.
2. Insere relações em `plan_items`.
3. Evita duplicados já existentes no plano.
--- ---
## Regras Pendentes (a implementar) ## Limitações atuais
### R5 — Filtro por Duração - A IA pode devolver nomes com pequenas diferenças.
``` - O matching é textual e pode falhar se o nome não coincidir.
IF duration == short → apenas essenciais (documents, cables, 1x clothing) - Não existe ranking local por categoria, clima ou duração.
IF duration == medium → essenciais + entretenimento - Não existe checklist de itens preparados.
IF duration == long → essenciais + entretenimento + conforto extra
```
**Estado:** Não implementado no MVP atual. Duração é detetada mas não filtra.
### R6 — Filtro por Clima
```
IF weather == cold → priorizar outerwear, remover beach/hot items
IF weather == hot → priorizar roupa leve, remover cold items
IF weather == rainy → priorizar outerwear + accessories.impermeável
```
**Estado:** Clima é detetado mas não filtra itens no MVP.
### R7 — Filtro por Formalidade
```
IF formality == formal → priorizar clothing.formal, footwear.formal
IF formality == casual → excluir clothing.formal
```
**Estado:** Formalidade é detetada mas não filtra itens no MVP.
--- ---
## Mapeamento de Context Tags (04_CATEGORIES_AND_TAGS.md) ## Regras futuras recomendadas
| Categoria.Subcategoria | Context Tags | ### RF1 — Resposta estruturada
|------------------------|-------------|
| clothing.formal | work, formal | Pedir à IA JSON com identificadores ou nomes normalizados.
| clothing.casual | casual, travel |
| clothing.sportswear | sport | ### RF2 — Score local
| clothing.outerwear | travel, cold, outdoor |
| electronics.computers | work, travel | Calcular pontuação por:
| electronics.phones | travel, work, casual |
| electronics.cables | travel | - categoria
| electronics.gaming | casual, travel | - tags
| footwear.formal | work, formal | - ocasião
| footwear.casual | casual, travel | - histórico do utilizador
| footwear.sport | sport | - clima
| footwear.outdoor | outdoor, travel |
| accessories.bags | travel, work, casual | ### RF3 — Checklist
| documents.identity | travel |
| documents.travel | travel | Guardar estado de cada item planeado:
- planeado
- preparado
- levado
### RF4 — Clima
Ajustar sugestões quando houver dados meteorológicos.
--- ---
## Notas para o Agente ## Notas para manutenção
- **NÃO** adicionar regras novas sem validar com o utilizador - Não mudar o comportamento do modo `silent` sem atualizar `05_RECOMMENDATION_ENGINE.md`.
- As regras R5-R7 estão identificadas como pendentes — implementar quando o MVP base estiver estável - Se a app passar a usar IDs na resposta da IA, atualizar este ficheiro e o serviço.
- O parser de palavras-chave (R4) é temporário — Fase 2 substitui por OpenAI - Manter a exportação compatível com `plans` e `plan_items`.
- Sempre que uma regra pendente for implementada, atualizar este ficheiro e o AI_AGENTS_LOG.md

View File

@@ -566,61 +566,7 @@ class _HomeContentState extends State<_HomeContent> {
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(AppRadius.lg), borderRadius: BorderRadius.circular(AppRadius.lg),
onTap: () async { onTap: () => _requestAiSuggestion(),
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),
),
),
],
),
),
);
},
child: Container( child: Container(
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
decoration: BoxDecoration( 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() { Widget _buildAddCta() {
return Material( return Material(
color: Colors.transparent, 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,
),
),
],
),
),
),
);
}
}

View File

@@ -325,7 +325,7 @@ class _ItemScreenState extends State<ItemScreen> {
crossAxisCount: 2, crossAxisCount: 2,
mainAxisSpacing: 12, mainAxisSpacing: 12,
crossAxisSpacing: 12, crossAxisSpacing: 12,
childAspectRatio: 0.50, childAspectRatio: 0.70,
), ),
itemCount: _filteredItems.length, itemCount: _filteredItems.length,
itemBuilder: (_, i) => _buildGridCard(_filteredItems[i]), itemBuilder: (_, i) => _buildGridCard(_filteredItems[i]),
@@ -356,8 +356,7 @@ class _ItemScreenState extends State<ItemScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AspectRatio( Expanded(
aspectRatio: 1,
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
@@ -377,7 +376,7 @@ class _ItemScreenState extends State<ItemScreen> {
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(10, 10, 10, 12), padding: const EdgeInsets.fromLTRB(10, 8, 10, 10),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

@@ -37,7 +37,8 @@ class _LoginScreenState extends State<LoginScreen>
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height - minHeight:
MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top - MediaQuery.of(context).padding.top -
MediaQuery.of(context).padding.bottom, MediaQuery.of(context).padding.bottom,
), ),
@@ -55,10 +56,7 @@ class _LoginScreenState extends State<LoginScreen>
const Spacer(), const Spacer(),
Padding( Padding(
padding: const EdgeInsets.only(top: 24, bottom: 8), padding: const EdgeInsets.only(top: 24, bottom: 8),
child: Text( child: Text('Versão 1.0.0', style: AppText.caption),
'Versão 1.0.0',
style: AppText.caption,
),
), ),
], ],
), ),
@@ -75,16 +73,9 @@ class _LoginScreenState extends State<LoginScreen>
Container( Container(
width: 84, width: 84,
height: 84, height: 84,
decoration: BoxDecoration( decoration: BoxDecoration(borderRadius: BorderRadius.circular(24)),
gradient: AppColors.brandGradient, clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular(24), child: Image.asset('assets/logoDayMaker.png', fit: BoxFit.cover),
boxShadow: AppShadows.brand,
),
child: const Icon(
Icons.auto_awesome_rounded,
color: Colors.white,
size: 42,
),
), ),
const SizedBox(height: 18), const SizedBox(height: 18),
const Text('DayMaker', style: AppText.h1), const Text('DayMaker', style: AppText.h1),
@@ -245,8 +236,7 @@ class _LoginScreenState extends State<LoginScreen>
} }
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
final response = final response = await Supabase.instance.client.auth.signInWithPassword(
await Supabase.instance.client.auth.signInWithPassword(
email: email, email: email,
password: password, password: password,
); );

View File

@@ -7,7 +7,7 @@ class AiRecommendationService {
static const String _model = 'llama3.2:3b'; static const String _model = 'llama3.2:3b';
static const String _systemPrompt = 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 = []; final List<Map<String, String>> _history = [];
@@ -52,7 +52,7 @@ class AiRecommendationService {
]; ];
final userContent = silent 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; : userMessage;
messages.add({'role': 'user', 'content': userContent}); messages.add({'role': 'user', 'content': userContent});
@@ -113,4 +113,18 @@ class AiRecommendationService {
} }
void clearHistory() => _history.clear(); 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 [];
}
}
} }