MVP
This commit is contained in:
@@ -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.
|
||||
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).
|
||||
**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.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
- Pessoas que viajam frequentemente
|
||||
- Quem tem muita roupa e não consegue gerir outfits
|
||||
- Utilizadores organizados que querem controlo total dos seus bens pessoais
|
||||
- Nómadas digitais e viajantes de longa duração
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
- Pessoas que querem organizar roupa, acessórios e objetos do dia a dia.
|
||||
- Utilizadores que planeiam outfits semanais.
|
||||
- Pessoas que viajam ou preparam atividades com frequência.
|
||||
- Estudantes e profissionais que querem saber rapidamente o que levar.
|
||||
|
||||
---
|
||||
|
||||
## Estado atual do projeto
|
||||
|
||||
| Fase | Estado |
|
||||
| Área | Estado |
|
||||
|------|--------|
|
||||
| Definição do MVP | ✅ Concluído |
|
||||
| Arquitetura técnica | ✅ Definida |
|
||||
| Implementação | 🔲 Por iniciar |
|
||||
| Testes com utilizadores | 🔲 Por iniciar |
|
||||
| IA avançada | 🔲 Fase futura |
|
||||
| App Flutter | Implementada |
|
||||
| Autenticação Supabase | Implementada |
|
||||
| Inventário | Implementado |
|
||||
| Imagens dos itens | Implementado |
|
||||
| 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 |
|
||||
|----------|----------|
|
||||
| `00_PROJECT_OVERVIEW.md` | Este ficheiro — visão geral |
|
||||
| `01_MVP_DEFINITION.md` | Funcionalidades mínimas viáveis |
|
||||
| `02_ARCHITECTURE.md` | Stack técnica e estrutura da app |
|
||||
| `03_AI_VISION_LAYER.md` | Integração de IA para imagens |
|
||||
| `04_CATEGORIES_AND_TAGS.md` | Sistema de organização de itens |
|
||||
| `05_RECOMMENDATION_ENGINE.md` | Lógica de sugestões (regras + IA futura) |
|
||||
| `06_FUTURE_FEATURES.md` | Roadmap e funcionalidades avançadas |
|
||||
| `07_AGENT_BEHAVIOR.md` | Como o agente IA deve comportar-se |
|
||||
| `00_PROJECT_OVERVIEW.md` | Visão geral do projeto |
|
||||
| `01_MVP_DEFINITION.md` | Definição do produto atual |
|
||||
| `02_ARCHITECTURE.md` | Arquitetura técnica e dados |
|
||||
| `03_AI_VISION_LAYER.md` | Estado da camada de imagem/visão |
|
||||
| `04_CATEGORIES_AND_TAGS.md` | Categorias e tags usadas |
|
||||
| `05_RECOMMENDATION_ENGINE.md` | Funcionamento das sugestões da IA |
|
||||
| `06_FUTURE_FEATURES.md` | Roadmap futuro |
|
||||
| `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 |
|
||||
|
||||
@@ -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.
|
||||
**Sem IA avançada. Sem funcionalidades complexas. Sem over-engineering.**
|
||||
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.
|
||||
|
||||
O MVP deve validar a hipótese central:
|
||||
> "Os utilizadores estão dispostos a fotografar os seus itens para depois receber sugestões contextuais."
|
||||
O foco é oferecer uma experiência simples e útil, sem obrigar o utilizador a configurar regras complexas.
|
||||
|
||||
---
|
||||
|
||||
## Funcionalidades obrigatórias (must-have)
|
||||
## Funcionalidades implementadas
|
||||
|
||||
### 1. 📸 Upload / Captura de Foto
|
||||
- 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
|
||||
### 1. Autenticação
|
||||
|
||||
### 2. 📦 Guardar Item
|
||||
- Cada item tem:
|
||||
- `nome` (texto livre ou sugerido pela IA de imagem)
|
||||
- `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
|
||||
- Login e registo com Supabase Auth.
|
||||
- Dados associados ao utilizador autenticado.
|
||||
- Perfil com nome/email do utilizador.
|
||||
|
||||
### 3. 🏷️ Categorização
|
||||
- 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`)
|
||||
### 2. Home
|
||||
|
||||
### 4. 🔍 Pesquisa e Visualização do Inventário
|
||||
- Lista de todos os itens (grid de fotos ou lista)
|
||||
- Filtrar por categoria
|
||||
- Pesquisa por nome ou tag
|
||||
- Detalhe do item ao clicar
|
||||
- Saudação personalizada.
|
||||
- Card com dia atual.
|
||||
- Contagem de itens no inventário.
|
||||
- Lista de itens planeados para hoje.
|
||||
- Lista de itens recentes.
|
||||
- Botão de adicionar item.
|
||||
- Botão "Pedir sugestão à IA".
|
||||
|
||||
### 5. 🎯 Sugestões Simples por Contexto (sem IA)
|
||||
- O utilizador seleciona um contexto pré-definido:
|
||||
- "Vou viajar (curta duração)"
|
||||
- "Vou viajar (longa duração)"
|
||||
- "Vou ao trabalho"
|
||||
- "Fim de semana casual"
|
||||
- A app filtra e mostra os itens relevantes para esse contexto
|
||||
- As regras são estáticas e definidas manualmente (ver `05_RECOMMENDATION_ENGINE.md`)
|
||||
### 3. Inventário
|
||||
|
||||
Cada item contém, conforme disponibilidade da base de dados:
|
||||
|
||||
- `id`
|
||||
- `user_id`
|
||||
- `nome`
|
||||
- `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.
|
||||
|
||||
- ❌ Sugestões baseadas em clima
|
||||
- ❌ Montagem automática de outfits
|
||||
- ❌ Integração com calendário
|
||||
- ❌ Assistente conversacional ("O que devo levar amanhã?")
|
||||
- ❌ Sistema de preferências pessoais aprendidas
|
||||
- ❌ Multi-utilizador / partilha
|
||||
- ❌ Exportação de listas
|
||||
- ❌ Notificações inteligentes
|
||||
- Reconhecimento automático de imagem por IA.
|
||||
- Previsão meteorológica.
|
||||
- Notificações push.
|
||||
- Sincronização com calendário externo.
|
||||
- Partilha de inventário entre utilizadores.
|
||||
- Recomendação visual por cor/compatibilidade.
|
||||
- Testes automatizados completos.
|
||||
|
||||
---
|
||||
|
||||
## Fluxo principal do utilizador (MVP)
|
||||
## Critérios de sucesso atuais
|
||||
|
||||
```
|
||||
1. Utilizador abre a app
|
||||
2. Tira foto de um item
|
||||
3. App sugere nome e categoria (via Google Vision)
|
||||
4. Utilizador confirma ou edita
|
||||
5. Item guardado no inventário
|
||||
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
|
||||
- O utilizador consegue criar conta e entrar.
|
||||
- O utilizador consegue adicionar e consultar itens.
|
||||
- A pesquisa e filtros devolvem itens corretos.
|
||||
- O utilizador consegue planear itens para qualquer dia da semana.
|
||||
- A IA responde usando o inventário do utilizador.
|
||||
- A sugestão da IA pode ser exportada para a semana.
|
||||
|
||||
@@ -1,143 +1,159 @@
|
||||
# 🏗️ Arquitetura Técnica
|
||||
# Arquitetura Técnica
|
||||
|
||||
## Stack Tecnológica
|
||||
## Stack atual
|
||||
|
||||
### Decisões principais
|
||||
|
||||
| Camada | Tecnologia escolhida | Alternativa considerada | Motivo da escolha |
|
||||
|--------|----------------------|-------------------------|-------------------|
|
||||
| App móvel | React Native | Flutter | Ecosistema JS, mais fácil de integrar com APIs web |
|
||||
| Backend / Auth | Firebase | Supabase | Setup rápido, escalável, gratuito no início |
|
||||
| Base de dados | Firestore | PostgreSQL (Supabase) | NoSQL flexível, sincronia em tempo real |
|
||||
| Armazenamento de imagens | Firebase Storage | AWS S3 | Integrado com Firebase, simples |
|
||||
| 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 |
|
||||
| Camada | Tecnologia |
|
||||
|--------|------------|
|
||||
| App móvel | Flutter / Dart |
|
||||
| Backend/Auth/DB | Supabase |
|
||||
| Base de dados | PostgreSQL via Supabase |
|
||||
| Imagens | URLs guardados em `item_images` |
|
||||
| IA de texto | Ollama API remota |
|
||||
| Tema visual | Design tokens em `lib/theme/app_theme.dart` |
|
||||
|
||||
---
|
||||
|
||||
## Estrutura da Aplicação
|
||||
## Estrutura principal
|
||||
|
||||
```
|
||||
inventoryai/
|
||||
├── mobile/ # App React Native
|
||||
│ ├── src/
|
||||
│ │ ├── screens/ # Ecrãs principais
|
||||
│ │ │ ├── HomeScreen.jsx
|
||||
│ │ │ ├── AddItemScreen.jsx
|
||||
│ │ │ ├── InventoryScreen.jsx
|
||||
│ │ │ ├── ItemDetailScreen.jsx
|
||||
│ │ │ └── SuggestionsScreen.jsx
|
||||
│ │ ├── components/ # Componentes reutilizáveis
|
||||
│ │ ├── services/ # Lógica de negócio e APIs
|
||||
│ │ │ ├── firebase.js
|
||||
│ │ │ ├── visionApi.js
|
||||
│ │ │ └── suggestions.js
|
||||
│ │ ├── hooks/ # Custom hooks React
|
||||
│ │ ├── utils/ # Utilitários
|
||||
│ │ └── constants/ # Categorias, regras, etc.
|
||||
│ └── package.json
|
||||
├── functions/ # Firebase Cloud Functions (opcional)
|
||||
└── docs/ # Ficheiros .md deste projeto
|
||||
```text
|
||||
lib/
|
||||
├── constants/
|
||||
│ └── item_categories.dart
|
||||
├── login/
|
||||
│ └── login_screen.dart
|
||||
├── Screens/
|
||||
│ ├── home_screen.dart
|
||||
│ ├── item_screen.dart
|
||||
│ ├── add_item_screen.dart
|
||||
│ ├── week_screen.dart
|
||||
│ ├── ai_chat_screen.dart
|
||||
│ └── perfil_screen.dart
|
||||
├── services/
|
||||
│ └── ai_recommendation_service.dart
|
||||
├── theme/
|
||||
│ └── app_theme.dart
|
||||
└── main.dart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
{
|
||||
"uid": "string",
|
||||
"email": "string",
|
||||
"displayName": "string",
|
||||
"createdAt": "timestamp",
|
||||
"preferences": {
|
||||
"defaultContext": "travel_short"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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"
|
||||
"model": "llama3.2:3b",
|
||||
"messages": [
|
||||
{"role": "system", "content": "..."},
|
||||
{"role": "user", "content": "..."}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fluxo de Adição de Item
|
||||
## Tema visual
|
||||
|
||||
```
|
||||
[Utilizador tira foto]
|
||||
↓
|
||||
[Upload para Firebase Storage]
|
||||
↓
|
||||
[Chamada à Google Vision API]
|
||||
↓
|
||||
[Receber labels automáticas]
|
||||
↓
|
||||
[Mapear labels → categoria + tags sugeridas]
|
||||
↓
|
||||
[Mostrar ao utilizador para confirmar/editar]
|
||||
↓
|
||||
[Guardar item no Firestore]
|
||||
```
|
||||
O tema é centralizado em `app_theme.dart`:
|
||||
|
||||
- `AppColors`
|
||||
- `AppRadius`
|
||||
- `AppSpacing`
|
||||
- `AppShadows`
|
||||
- `AppText`
|
||||
- `AppDecorations`
|
||||
- `AppButton`
|
||||
- `AppChip`
|
||||
- `AppSnack`
|
||||
|
||||
Novos ecrãs devem reutilizar estes tokens para manter consistência.
|
||||
|
||||
---
|
||||
|
||||
## Autenticação
|
||||
## Segurança
|
||||
|
||||
- Firebase Authentication
|
||||
- Login com Google (obrigatório no MVP)
|
||||
- Login com email/password (opcional no MVP)
|
||||
- Todos os dados são isolados por `userId`
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
- Todas as queries de dados usam `user_id` do utilizador autenticado quando aplicável.
|
||||
- A app não deve hardcodar chaves privadas.
|
||||
- A API de IA atual é endpoint remoto público do projeto, sem chave no cliente.
|
||||
- Dados de inventário enviados à IA incluem nomes, categorias, tags e notas dos itens.
|
||||
|
||||
@@ -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:
|
||||
- Nome do item
|
||||
- Categoria
|
||||
- Tags relevantes
|
||||
O DayMaker guarda imagens dos itens, mas **não faz reconhecimento automático de imagem** no produto atual.
|
||||
|
||||
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?
|
||||
- Fácil integração com Firebase (mesmo ecosistema Google)
|
||||
- Excelente deteção de objetos do quotidiano
|
||||
- Labels em múltiplos idiomas
|
||||
- Tier gratuito generoso (1.000 unidades/mês)
|
||||
- Documentação extensa
|
||||
|
||||
### Funcionalidades usadas no MVP
|
||||
|
||||
| Feature | Uso |
|
||||
|---------|-----|
|
||||
| `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
|
||||
};
|
||||
}
|
||||
```text
|
||||
Item criado/editado
|
||||
↓
|
||||
Imagem associada ao item
|
||||
↓
|
||||
URL guardado em item_images.image_url
|
||||
↓
|
||||
Ecrãs carregam items com item_images(image_url)
|
||||
↓
|
||||
Image.network mostra a imagem ou fallback por categoria
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mapeamento de Labels → Categorias
|
||||
## Ficheiros relevantes
|
||||
|
||||
```javascript
|
||||
// constants/categoryMapping.js
|
||||
|
||||
export const LABEL_TO_CATEGORY = {
|
||||
// Roupa
|
||||
't-shirt': 'clothing',
|
||||
'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
|
||||
}
|
||||
```
|
||||
| Ficheiro | Responsabilidade |
|
||||
|----------|------------------|
|
||||
| `lib/Screens/add_item_screen.dart` | Criação de itens e associação de dados |
|
||||
| `lib/Screens/item_screen.dart` | Inventário, detalhe, edição e imagem do item |
|
||||
| `lib/Screens/week_screen.dart` | Mostra imagens dos itens planeados |
|
||||
| `lib/Screens/home_screen.dart` | Mostra itens recentes, hoje e sugestões IA |
|
||||
|
||||
---
|
||||
|
||||
## Fluxo de UX ao adicionar item
|
||||
## Fallback visual
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
Quando um item não tem imagem ou a imagem falha ao carregar:
|
||||
|
||||
> 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 |
|
||||
|------|---------------|
|
||||
| API indisponível | Mostrar formulário manual, sem sugestões |
|
||||
| Foto sem objeto reconhecível | Mostrar sugestão "Outro" e pedir nome manual |
|
||||
| Score muito baixo (< 0.5) | Ignorar label, não sugerir |
|
||||
| Timeout | Retry 1x, depois formulário manual |
|
||||
Reconhecimento automático por imagem **não está implementado**.
|
||||
|
||||
Pode ser uma funcionalidade futura para sugerir:
|
||||
|
||||
- Nome do item.
|
||||
- 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
|
||||
- Fase 3: Identificar marcas e modelos específicos
|
||||
- Fase 3: Reconhecimento de cor dominante para filtros de outfit
|
||||
|
||||
---
|
||||
|
||||
## 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`
|
||||
- Não guardar API keys privadas no cliente Flutter.
|
||||
- O utilizador deve poder confirmar/editar qualquer sugestão automática.
|
||||
- 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.
|
||||
|
||||
@@ -1,146 +1,102 @@
|
||||
# 🏷️ Sistema de Categorias e Tags
|
||||
# Sistema de Categorias e Tags
|
||||
|
||||
## Filosofia
|
||||
|
||||
O sistema de organização deve ser:
|
||||
- **Simples o suficiente** para não sobrecarregar o utilizador
|
||||
- **Flexível o suficiente** para cobrir a maioria dos casos reais
|
||||
- **Extensível** para futuras funcionalidades (outfits, clima, etc.)
|
||||
O sistema de organização deve ser simples, visual e útil para recomendações.
|
||||
|
||||
Categorias identificam o tipo principal do item. Tags descrevem contexto de uso, como `casual`, `work`, `travel` ou `gaming`.
|
||||
|
||||
---
|
||||
|
||||
## Categorias Principais (MVP)
|
||||
## Implementação atual
|
||||
|
||||
| ID | Nome | Ícone | Descrição |
|
||||
|----|------|-------|-----------|
|
||||
| `clothing` | Roupa | 👕 | Peças de vestuário |
|
||||
| `electronics` | Eletrónica | 💻 | Dispositivos e acessórios tecnológicos |
|
||||
| `footwear` | Calçado | 👟 | Sapatos, botas, sandálias |
|
||||
| `accessories` | Acessórios | 🎒 | Bolsas, relógios, óculos, bijuteria |
|
||||
| `documents` | Documentos | 📄 | Passaporte, cartões, papéis importantes |
|
||||
| `other` | Outros | 📦 | Tudo o resto |
|
||||
As categorias e tags vivem em:
|
||||
|
||||
```text
|
||||
lib/constants/item_categories.dart
|
||||
```
|
||||
|
||||
A UI usa estas constantes para:
|
||||
|
||||
- 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`)
|
||||
```
|
||||
casual → t-shirts, calças de ganga, hoodies
|
||||
formal → fatos, camisas, vestidos de cerimónia
|
||||
sportswear → leggings, tops de treino, shorts
|
||||
outerwear → casacos, impermeáveis, parkas
|
||||
underwear → roupa interior, meias
|
||||
sleepwear → pijamas, roupões
|
||||
```
|
||||
| ID esperado | Nome | Uso |
|
||||
|-------------|------|-----|
|
||||
| `roupa` / equivalente | Roupa | Peças de vestuário |
|
||||
| `eletronica` / equivalente | Eletrónica | Consolas, portáteis, carregadores, gadgets |
|
||||
| `calcado` / equivalente | Calçado | Botas, sapatilhas, sapatos |
|
||||
| `acessorios` / equivalente | Acessórios | Mochilas, bolsas, relógios, óculos |
|
||||
| `documentos` / equivalente | Documentos | Cartões, papéis, bilhetes, identificação |
|
||||
| `outros` / equivalente | Outros | Itens fora das categorias principais |
|
||||
|
||||
### 💻 Eletrónica (`electronics`)
|
||||
```
|
||||
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
|
||||
```
|
||||
A nomenclatura exata deve seguir `item_categories.dart`, porque a UI usa esses IDs diretamente.
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
|-----|-----------|-------------------|
|
||||
| `travel` | Qualquer tipo de viagem | Mala, passaporte, adaptador de tomadas |
|
||||
| `work` | Ambiente de trabalho | Portátil, fato, sapatos formais |
|
||||
| `casual` | Dia-a-dia informal | Jeans, t-shirt, sapatilhas |
|
||||
| `sport` | Atividade física | Leggings, ténis de corrida, garrafa de água |
|
||||
| `formal` | Eventos formais | Fato, vestido, sapatos de salto |
|
||||
| `outdoor` | Exterior e natureza | Blusão, botas, impermeável |
|
||||
| `beach` | Praia ou piscina | Biquíni, chinelos, protetor solar |
|
||||
| `cold` | Clima frio | Casaco, cachecol, luvas |
|
||||
| `hot` | Clima quente | Roupa leve, t-shirts, sandálias |
|
||||
| Tag | Contexto |
|
||||
|-----|----------|
|
||||
| `casual` | Uso diário e informal |
|
||||
| `formal` | Reuniões, eventos e ocasiões formais |
|
||||
| `work` | Trabalho ou estudo |
|
||||
| `travel` | Viagens e deslocações |
|
||||
| `sport` | Treino/desporto |
|
||||
| `outdoor` | Atividades exteriores |
|
||||
| `beach` | Praia/piscina |
|
||||
| `cold` | Tempo frio |
|
||||
| `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
|
||||
// constants/contextTagRules.js
|
||||
|
||||
export const AUTO_CONTEXT_TAGS = {
|
||||
// Por categoria
|
||||
'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'],
|
||||
};
|
||||
```text
|
||||
- nome do item
|
||||
- categoria
|
||||
- tags
|
||||
- nota/notes, quando existe
|
||||
```
|
||||
|
||||
---
|
||||
Exemplo:
|
||||
|
||||
## Interface de Tags (UX)
|
||||
```text
|
||||
- Switch (categoria: Eletrónica) [tags: gaming, casual]
|
||||
- Camisa verde (categoria: Roupa) [tags: formal, work]
|
||||
```
|
||||
|
||||
### Ao adicionar um item:
|
||||
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)
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
- 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
|
||||
- O sistema de tags deve ser simples de perceber sem documentação — usar labels em português na UI
|
||||
No inventário, o utilizador pode:
|
||||
|
||||
- Pesquisar por nome.
|
||||
- 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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
| 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 |
|
||||
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 1 — Regras Estáticas (MVP)
|
||||
## Serviço principal
|
||||
|
||||
### Contextos disponíveis
|
||||
Ficheiro:
|
||||
|
||||
O utilizador escolhe um dos seguintes contextos:
|
||||
|
||||
```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,
|
||||
},
|
||||
];
|
||||
```text
|
||||
lib/services/ai_recommendation_service.dart
|
||||
```
|
||||
|
||||
### Lógica de filtragem
|
||||
Responsabilidades:
|
||||
|
||||
```javascript
|
||||
// services/suggestions.js
|
||||
|
||||
export function getSuggestionsForContext(items, contextId) {
|
||||
const context = CONTEXTS.find(c => c.id === contextId);
|
||||
if (!context) return [];
|
||||
|
||||
// 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
|
||||
```
|
||||
- Buscar itens do utilizador no Supabase.
|
||||
- Construir contexto textual do inventário.
|
||||
- Enviar mensagens para a API Ollama.
|
||||
- Manter histórico simples de conversa.
|
||||
- Suportar `silent: true` para sugestões estruturadas.
|
||||
- Buscar itens com imagens para a home.
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
- "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:
|
||||
```json
|
||||
{
|
||||
"context": "travel",
|
||||
"duration": "short",
|
||||
"environment": "transit",
|
||||
"weather": "unknown",
|
||||
"formality": "casual"
|
||||
"model": "llama3.2:3b",
|
||||
"messages": [
|
||||
{"role": "system", "content": "..."},
|
||||
{"role": "user", "content": "..."}
|
||||
],
|
||||
"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
|
||||
- Pontuação de relevância por item por contexto
|
||||
- Integração com API de clima para filtros automáticos
|
||||
- Histórico de viagens para sugestões baseadas em padrões
|
||||
- Ajudar a montar outfits e escolher o que levar.
|
||||
- Usar linguagem simples e curta.
|
||||
- Não usar emojis.
|
||||
- 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.
|
||||
- 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
|
||||
- A ordenação por `priorityCategories` é intencional e importante — respeitá-la
|
||||
- Quando a Fase 2 for implementada, o output do OpenAI deve mapear para os IDs de contexto da Fase 1
|
||||
O contexto inclui os itens do utilizador:
|
||||
|
||||
```text
|
||||
Itens disponiveis no inventario do utilizador:
|
||||
- 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.
|
||||
|
||||
@@ -1,119 +1,90 @@
|
||||
# 🚀 Roadmap e Funcionalidades Futuras
|
||||
# Roadmap e Funcionalidades Futuras
|
||||
|
||||
## 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
|
||||
|
||||
```
|
||||
FASE 1 — MVP (agora)
|
||||
├── Upload de fotos
|
||||
├── Guardar itens
|
||||
├── Categorização manual + auto (Google Vision)
|
||||
├── Pesquisa / filtro
|
||||
└── Sugestões simples por regras
|
||||
O produto atual já inclui:
|
||||
|
||||
FASE 2 — IA de Linguagem
|
||||
├── Input em linguagem natural
|
||||
├── Integração OpenAI API
|
||||
└── Contextos dinâmicos
|
||||
|
||||
FASE 3 — Personalização
|
||||
├── Aprendizagem por histórico
|
||||
├── 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
|
||||
```
|
||||
- Autenticação Supabase.
|
||||
- Inventário com imagens, categorias e tags.
|
||||
- Pesquisa e filtros.
|
||||
- Planeamento semanal.
|
||||
- Chat IA.
|
||||
- Sugestões IA com base nos itens reais.
|
||||
- Exportação de sugestões para dias da semana.
|
||||
|
||||
---
|
||||
|
||||
## Funcionalidades por fase
|
||||
## Próximas melhorias recomendadas
|
||||
|
||||
### 🔵 Fase 2 — IA de Linguagem
|
||||
### 1. Melhorar robustez das sugestões IA
|
||||
|
||||
**Input em linguagem natural**
|
||||
- Utilizador escreve ou fala: "Vou a uma conferência em Londres por 3 dias"
|
||||
- App interpreta e gera lista personalizada
|
||||
- Implementado via OpenAI API (ver `05_RECOMMENDATION_ENGINE.md`)
|
||||
- Fazer a IA devolver JSON com IDs ou nomes estruturados.
|
||||
- Validar itens sugeridos localmente antes de mostrar.
|
||||
- Mostrar mensagem clara quando nenhum item corresponde.
|
||||
- Permitir editar a lista antes de exportar.
|
||||
|
||||
**Contextos dinâmicos**
|
||||
- Em vez de escolher de uma lista, o utilizador descreve a situação
|
||||
- App infere contexto, duração, clima esperado, formalidade
|
||||
### 2. Checklist por plano
|
||||
|
||||
- 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**
|
||||
- App regista quais itens o utilizador aceitou/ignorou nas sugestões
|
||||
- Com 10+ interações, começa a ajustar pesos de relevância
|
||||
- Exemplo: utilizador nunca leva sapatos formais em viagens curtas → app para de sugerir
|
||||
|
||||
**Integração com API de clima**
|
||||
- Utilizador indica destino e datas
|
||||
- App consulta previsão meteorológica
|
||||
- Filtra roupa por temperatura e condições
|
||||
- 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?"
|
||||
| Funcionalidade | Estado |
|
||||
|----------------|--------|
|
||||
| Clima automático | Futuro |
|
||||
| Notificações push | Futuro |
|
||||
| Reconhecimento de imagem | Futuro |
|
||||
| Calendário externo | Futuro |
|
||||
| Partilha/social | Futuro |
|
||||
| Recomendação visual por cor | Futuro |
|
||||
| Testes automatizados completos | Pendente |
|
||||
|
||||
---
|
||||
|
||||
### 🔴 Fase 4 — Funcionalidades Avançadas
|
||||
## Prioridade sugerida
|
||||
|
||||
**Montagem automática de outfits**
|
||||
- App combina peças de roupa compatíveis visualmente
|
||||
- Usa cor dominante extraída das fotos
|
||||
- Sugere conjuntos completos (peça cima + baixo + calçado + acessório)
|
||||
|
||||
**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
|
||||
1. Resposta IA estruturada em JSON.
|
||||
2. Checklist por plano.
|
||||
3. Notificações simples.
|
||||
4. Clima.
|
||||
5. Visão por imagem.
|
||||
|
||||
@@ -1,184 +1,104 @@
|
||||
# 🤖 Comportamento do Agente IA
|
||||
# Comportamento do Agente IA
|
||||
|
||||
## 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.
|
||||
**Lê este ficheiro primeiro antes de qualquer tarefa.**
|
||||
Este ficheiro define como um agente IA de código deve comportar-se quando trabalha no projeto **DayMaker**.
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
1. `00_PROJECT_OVERVIEW.md` — visão geral e princípios
|
||||
2. `01_MVP_DEFINITION.md` — o que está incluído no MVP
|
||||
3. `02_ARCHITECTURE.md` — stack técnica e modelo de dados
|
||||
4. `03_AI_VISION_LAYER.md` — integração com Google Vision
|
||||
5. `04_CATEGORIES_AND_TAGS.md` — sistema de organização
|
||||
6. `05_RECOMMENDATION_ENGINE.md` — lógica de sugestões
|
||||
7. `06_FUTURE_FEATURES.md` — roadmap futuro
|
||||
Antes de alterações grandes, consultar:
|
||||
|
||||
1. `00_PROJECT_OVERVIEW.md`
|
||||
2. `01_MVP_DEFINITION.md`
|
||||
3. `02_ARCHITECTURE.md`
|
||||
4. `05_RECOMMENDATION_ENGINE.md`
|
||||
|
||||
---
|
||||
|
||||
## 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 definida** em `02_ARCHITECTURE.md` — Firebase, React Native, Google Vision
|
||||
- **Usar o modelo de dados** exatamente como definido (campos, tipos, nomes)
|
||||
- **Manter o código simples** — se há duas formas, escolher a mais simples
|
||||
- **Comentar decisões de arquitetura** no código quando relevante
|
||||
- **Avisar** quando uma funcionalidade pedida está fora do MVP
|
||||
- **Perguntar** antes de alterar o modelo de dados ou a arquitetura
|
||||
- **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.)
|
||||
- Respeitar a stack atual: Flutter, Dart, Supabase e Ollama API.
|
||||
- Usar os componentes e tokens de `lib/theme/app_theme.dart`.
|
||||
- Manter queries Supabase filtradas por `user_id` quando os dados forem do utilizador.
|
||||
- Verificar o modelo de dados antes de alterar tabelas ou campos.
|
||||
- Manter a UI consistente com a navegação atual: Início, Itens, Semana, IA e Perfil.
|
||||
- Usar `flutter analyze --no-pub` depois de alterações relevantes.
|
||||
- Explicar ao utilizador quando uma funcionalidade exige alteração de base de dados.
|
||||
|
||||
### ❌ NUNCA fazer
|
||||
### Nunca fazer
|
||||
|
||||
- Adicionar funcionalidades que não estão no MVP sem confirmação explícita
|
||||
- Mudar a stack tecnológica (ex: sugerir Flutter, Supabase, AWS)
|
||||
- Implementar OpenAI no MVP — fica para a Fase 2
|
||||
- Guardar a API key do Google Vision em código client-side em produção
|
||||
- Criar lógica de base de dados fora dos ficheiros `services/`
|
||||
- Usar `console.log` em produção para dados sensíveis do utilizador
|
||||
- Alterar os nomes dos campos do modelo de dados sem avisar
|
||||
- Reintroduzir Firebase, Firestore ou React Native.
|
||||
- Assumir que Google Vision está implementado.
|
||||
- Trocar o endpoint/modelo da IA sem testar ou explicar.
|
||||
- Hardcodar API keys privadas.
|
||||
- Enviar dados sensíveis desnecessários à IA.
|
||||
- Criar categorias fora de `item_categories.dart` sem atualizar a documentação.
|
||||
|
||||
---
|
||||
|
||||
## Linguagem e comunicação
|
||||
## Padrões de código
|
||||
|
||||
- Comunicar em **português** (de Portugal, não do Brasil)
|
||||
- Explicações técnicas devem ser **claras e objetivas**
|
||||
- Quando há uma decisão de arquitetura, **explicar o porquê**
|
||||
- Usar **exemplos de código** sempre que possível
|
||||
- Avisos sobre o MVP devem ser **visíveis** (usar emoji ⚠️ ou bloco de nota)
|
||||
### Flutter/Dart
|
||||
|
||||
- Widgets em `PascalCase`.
|
||||
- Métodos privados com `_camelCase`.
|
||||
- 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`.
|
||||
|
||||
```
|
||||
PASSO 1: 📸 Upload de fotos
|
||||
└── Câmara + seleção de galeria
|
||||
└── Upload para Firebase Storage
|
||||
└── URL guardado no estado
|
||||
- Chat livre: `sendMessage(text)`.
|
||||
- Sugestão estruturada: `sendMessage(text, silent: true)`.
|
||||
- Itens com imagem: `getItemsWithImages()`.
|
||||
|
||||
PASSO 2: 📦 Guardar itens
|
||||
└── 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
|
||||
```
|
||||
A IA deve receber contexto de inventário e responder em português.
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
|
||||
```
|
||||
⚠️ MVP: Esta funcionalidade não está incluída no MVP atual.
|
||||
|
||||
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?
|
||||
```
|
||||
1. Explicar que é funcionalidade futura.
|
||||
2. Perguntar se deve implementar agora.
|
||||
3. Avisar se exigir nova dependência, API key ou alteração de base de dados.
|
||||
|
||||
---
|
||||
|
||||
## Padrões de código obrigatórios
|
||||
## Checklist antes de terminar uma tarefa
|
||||
|
||||
### Nomenclatura
|
||||
```javascript
|
||||
// Componentes React: PascalCase
|
||||
ItemCard.jsx
|
||||
AddItemScreen.jsx
|
||||
|
||||
// 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` |
|
||||
- Código compila com `flutter analyze --no-pub`.
|
||||
- Não há imports não usados.
|
||||
- UI segue tema atual.
|
||||
- Documentação atualizada quando a funcionalidade muda comportamento.
|
||||
- Resumo final indica ficheiros alterados.
|
||||
|
||||
@@ -1,70 +1,81 @@
|
||||
# 📝 AI Agents Log
|
||||
# AI Agents Log
|
||||
|
||||
## Propósito
|
||||
|
||||
Este ficheiro regista toda a atividade do agente de recomendação.
|
||||
Cada execução gera uma entrada neste log.
|
||||
Este ficheiro regista decisões e alterações relevantes relacionadas com a IA e recomendações do DayMaker.
|
||||
|
||||
---
|
||||
|
||||
## Formato de Entrada
|
||||
## Formato recomendado
|
||||
|
||||
### Data:
|
||||
```text
|
||||
### Data
|
||||
YYYY-MM-DD
|
||||
|
||||
### Agente:
|
||||
Recommender-MVP
|
||||
### Área
|
||||
Chat IA / Sugestão IA / Inventário / Planeamento / Documentação
|
||||
|
||||
### Tipo:
|
||||
- recomendacao
|
||||
- erro
|
||||
- melhoria
|
||||
### Descrição
|
||||
Resumo do que mudou.
|
||||
|
||||
### Descrição:
|
||||
O que foi feito
|
||||
|
||||
### 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
|
||||
### Impacto
|
||||
Como afeta o utilizador ou o código.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Histórico
|
||||
|
||||
### Data:
|
||||
### Data
|
||||
2026-04-24
|
||||
|
||||
### Agente:
|
||||
Recommender-MVP
|
||||
### Área
|
||||
Recomendação inicial
|
||||
|
||||
### Tipo:
|
||||
recomendacao
|
||||
### Descrição
|
||||
Primeiras ideias de motor de recomendações baseado em regras estáticas e contextos.
|
||||
|
||||
### Descrição:
|
||||
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.
|
||||
### Impacto
|
||||
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:
|
||||
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
|
||||
### Data
|
||||
2026-05-21
|
||||
|
||||
### Notas:
|
||||
- Parser de pedidos é básico (palavras-chave). Fase 2 usará OpenAI API.
|
||||
- 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.
|
||||
### Área
|
||||
Chat IA
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
IF item.contextTags contém pelo menos uma tag do contexto
|
||||
THEN item é relevante
|
||||
### R1 — Usar apenas inventário do utilizador
|
||||
|
||||
A IA recebe apenas itens do utilizador autenticado.
|
||||
|
||||
```text
|
||||
items.user_id == currentUser.id
|
||||
```
|
||||
|
||||
### R2 — Ordenação por Prioridade de Categoria
|
||||
```
|
||||
Ordenar itens pela posição da categoria em context.priorityCategories
|
||||
Categorias fora da lista → prioridade baixa (999)
|
||||
```
|
||||
### R2 — Contexto textual dos itens
|
||||
|
||||
### R3 — Limite de Itens
|
||||
```
|
||||
Retornar no máximo context.maxItems itens
|
||||
```
|
||||
Cada item é enviado com:
|
||||
|
||||
| Contexto | maxItems |
|
||||
|----------|----------|
|
||||
| travel_short | 20 |
|
||||
| travel_long | 40 |
|
||||
| work | 15 |
|
||||
| casual | 10 |
|
||||
| sport | 8 |
|
||||
| outdoor | 12 |
|
||||
- nome
|
||||
- categoria
|
||||
- tags
|
||||
- nota/notes, quando disponível
|
||||
|
||||
### R4 — Interpretação de Pedido (Parser)
|
||||
```
|
||||
Palavras-chave → contexto + duração + clima + formalidade
|
||||
### R3 — Modo silencioso para sugestões
|
||||
|
||||
viagem/comboio/voo → travel_short + transit
|
||||
trabalho/reunião → work + indoor
|
||||
fim de semana/casual/lazer → casual
|
||||
treino/desporto/gym → sport
|
||||
exterior/caminhada → outdoor + outdoor
|
||||
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.
|
||||
|
||||
4+ dias/semana/longa → duration:long + upgrade para travel_long
|
||||
4 horas/média → duration:medium
|
||||
1-2 horas/curta → duration:short
|
||||
### R4 — Correspondência com itens reais
|
||||
|
||||
frio/inverno → weather:cold
|
||||
calor/verão/praia → weather:hot
|
||||
chuva → weather:rainy
|
||||
A app cruza as linhas devolvidas pela IA com os nomes dos itens carregados do Supabase.
|
||||
|
||||
formal/cerimónia → formality:formal
|
||||
casual/informal → formality:casual
|
||||
```
|
||||
Apenas itens correspondidos são mostrados com imagem e podem ser exportados.
|
||||
|
||||
### 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
|
||||
```
|
||||
IF duration == short → apenas essenciais (documents, cables, 1x clothing)
|
||||
IF duration == medium → essenciais + entretenimento
|
||||
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.
|
||||
- A IA pode devolver nomes com pequenas diferenças.
|
||||
- O matching é textual e pode falhar se o nome não coincidir.
|
||||
- Não existe ranking local por categoria, clima ou duração.
|
||||
- Não existe checklist de itens preparados.
|
||||
|
||||
---
|
||||
|
||||
## Mapeamento de Context Tags (04_CATEGORIES_AND_TAGS.md)
|
||||
## Regras futuras recomendadas
|
||||
|
||||
| Categoria.Subcategoria | Context Tags |
|
||||
|------------------------|-------------|
|
||||
| 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 |
|
||||
### RF1 — Resposta estruturada
|
||||
|
||||
Pedir à IA JSON com identificadores ou nomes normalizados.
|
||||
|
||||
### RF2 — Score local
|
||||
|
||||
Calcular pontuação por:
|
||||
|
||||
- categoria
|
||||
- tags
|
||||
- ocasião
|
||||
- histórico do utilizador
|
||||
- clima
|
||||
|
||||
### RF3 — Checklist
|
||||
|
||||
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
|
||||
- As regras R5-R7 estão identificadas como pendentes — implementar quando o MVP base estiver estável
|
||||
- O parser de palavras-chave (R4) é temporário — Fase 2 substitui por OpenAI
|
||||
- Sempre que uma regra pendente for implementada, atualizar este ficheiro e o AI_AGENTS_LOG.md
|
||||
- Não mudar o comportamento do modo `silent` sem atualizar `05_RECOMMENDATION_ENGINE.md`.
|
||||
- Se a app passar a usar IDs na resposta da IA, atualizar este ficheiro e o serviço.
|
||||
- Manter a exportação compatível com `plans` e `plan_items`.
|
||||
|
||||
@@ -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