add VinEye frontend app + fix hardcoded paths + gitignore

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yanis 2026-04-02 10:30:05 +02:00
parent bb9df73b5f
commit a964cc3836
91 changed files with 11666 additions and 204 deletions

25
.gitignore vendored Normal file
View file

@ -0,0 +1,25 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
*.egg
# Models (too large for git)
*.keras
*.tflite
*.h5
# Environment
.env
.env.*
# OS
.DS_Store
Thumbs.db
# VinEye (handled by VinEye/.gitignore)
VinEye/node_modules/
VinEye/.expo/
VinEye/dist/
VinEye/ios/
VinEye/android/

View file

@ -10,7 +10,7 @@ The data used in this study came from [kaggle](kaggle.com/datasets/rm1000/grape-
![Dataset Overview](./docs/images/dataset_overview.png) ![Dataset Overview](./docs/images/dataset_overview.png)
![Sample](./docs/images/samples_img.png) ![Sample](./docs/images/samples_img.png)
## Model Structure ## Model Structure
Our model is a Convolutional Neural Network (CNN) built using Keras API with TensorFlow backend. It includes several convolutional layers followed by batch normalization, ReLU activation function and max pooling for downsampling. Our model is a Convolutional Neural Network (CNN) built using Keras API with TensorFlow backend. It includes several convolutional layers followed by batch normalization, ReLU activation function and max pooling for downsampling.
Dropout layers are used for regularization to prevent overfitting. The architecture details and parameters are as follows: Dropout layers are used for regularization to prevent overfitting. The architecture details and parameters are as follows:

View file

@ -0,0 +1,23 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "VinEye — Expo (all platforms)",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["start"],
"port": 8081
},
{
"name": "VinEye — Android",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["android"],
"port": 8081
},
{
"name": "VinEye — Web",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["web"],
"port": 8081
}
]
}

View file

@ -0,0 +1,10 @@
# MEMORY — vineye
## Vue d'ensemble
<!-- Remplir au fil des sessions -->
## Décisions techniques importantes
<!-- Patterns, choix d'architecture, gotchas découverts -->
## État actuel
<!-- Ce qui est fait, ce qui reste à faire -->

View file

@ -0,0 +1,29 @@
# Index des fonctionnalités — vineye
> Mis à jour automatiquement. Compléter les README.md de chaque feature après implémentation.
> **Règle** : Avant de travailler sur une feature → lire son README. Après → le mettre à jour.
## Fonctionnalités détectées
| Feature | Documentation | Status | Dernière MAJ |
|---------|--------------|--------|-------------|
## Fichiers critiques globaux
| Fichier | Rôle |
|---------|------|
| `CLAUDE.md` | Contexte projet chargé automatiquement |
| `.claude/notes/_features.md` | Cet index |
| `.claude/rules/` | Rules spécifiques au projet |
## Stack détectée
## Convention de mise à jour
Après chaque feature implémentée :
1. Ouvrir `.claude/notes/<feature>/README.md`
2. Compléter : description, fichiers clés, endpoints, gotchas
3. Mettre à jour le status dans cet index (🟡 → ✅)

41
VinEye/.gitignore vendored Normal file
View file

@ -0,0 +1,41 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
# generated native folders
/ios
/android

15
VinEye/App.tsx Normal file
View file

@ -0,0 +1,15 @@
import './global.css';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { PortalHost } from '@rn-primitives/portal';
import RootNavigator from '@/navigation/RootNavigator';
export default function App() {
return (
<SafeAreaProvider>
<StatusBar style="dark" />
<RootNavigator />
<PortalHost />
</SafeAreaProvider>
);
}

129
VinEye/CLAUDE.md Normal file
View file

@ -0,0 +1,129 @@
# VinEye
Application mobile React Native (Expo) de détection de cépages par IA.
Analyse la vigne en temps réel via la caméra, identifie le cépage, et gamifie la progression.
---
## Stack
| Couche | Technologies |
|--------|-------------|
| Framework | React Native + Expo SDK 54 (bare workflow) |
| Navigation | React Navigation v7 (NativeStack + BottomTabs) — **PAS Expo Router** |
| Langage | TypeScript strict |
| UI | Composants custom (pas de shadcn — RN only) |
| Animations | React Native Reanimated v4 (`useEffect` vient de `react`, **pas** de reanimated) |
| IA | TFLite mock (weighted random : 70% vine / 20% uncertain / 10% not_vine) |
| Persistance | AsyncStorage (`@react-native-async-storage/async-storage`) |
| i18n | i18next + react-i18next (FR + EN) |
| Caméra | expo-camera |
| Haptics | expo-haptics |
| SVG | react-native-svg |
| Lottie | lottie-react-native |
| Package manager | **pnpm** |
---
## Architecture
```
VinEye/
├── App.tsx # Entry point (i18n init + RootNavigator)
├── src/
│ ├── assets/
│ │ ├── images/ # logo.svg, icon.png, splash.png
│ │ └── lottie/ # confetti.json, scan-success.json, vine-leaf.json, level-up.json
│ ├── components/
│ │ ├── ui/ # Button, Card, Badge, ProgressCircle, AnimatedCounter
│ │ ├── scanner/ # DetectionFrame, CameraOverlay, ConfidenceMeter
│ │ ├── gamification/ # XPBar, BadgeCard, LevelIndicator, StreakCounter
│ │ └── history/ # ScanCard, ScanList
│ ├── hooks/ # useDetection, useGameProgress, useHistory
│ ├── i18n/ # fr.json, en.json, index.ts
│ ├── navigation/ # RootNavigator, BottomTabNavigator, linking.ts
│ ├── screens/ # SplashScreen, HomeScreen, ScannerScreen, ResultScreen, HistoryScreen, ProfileScreen
│ ├── services/
│ │ ├── tflite/model.ts # Mock TFLite inference
│ │ ├── storage.ts # AsyncStorage wrapper typé
│ │ └── haptics.ts # hapticSuccess/Warning/Error/Light/Medium/Heavy
│ ├── theme/ # colors.ts, typography.ts, spacing.ts, index.ts
│ ├── types/ # detection.ts, gamification.ts, navigation.ts
│ └── utils/
│ ├── cepages.ts # 15 cépages (origine, couleur, caractéristiques, régions)
│ └── achievements.ts # XP_REWARDS, LEVELS, BADGE_DEFINITIONS, checkNewBadges, getLevelForXP
```
---
## Navigation
```
RootNavigator (Stack)
├── Splash → SplashScreen (auto-navigate vers Main après 2.8s)
├── Main → BottomTabNavigator
│ ├── Home → HomeScreen
│ ├── Scanner → ScannerScreen (bouton FAB central)
│ ├── History → HistoryScreen
│ └── Profile → ProfileScreen
└── Result (modal) → ResultScreen (slide_from_bottom)
```
---
## Design Tokens (colors.ts)
| Token | Hex | Usage |
|-------|-----|-------|
| `primary[700]` | `#2D6A4F` | Tab active, CTA principal |
| `primary[800]` | `#1B4332` | Scanner FAB |
| `primary[900]` | `#0A2318` | Ombres |
| `accent[500]` | `#7C3AED` | Badges, accents violet raisin |
| `surface` | `#FFFFFF` | Fond tab bar, cards |
| `background` | `#F8FBF9` | Fond écrans |
| `neutral[300]` | `#D1D5DB` | Bordures |
| `neutral[400]` | `#9CA3AF` | Tab inactive |
---
## Gamification
- **7 niveaux** : Bourgeon → Apprenti Vigneron → Vigneron → Expert Viticole → Sommelier → Grand Cru → Maître de Chai
- **XP** : +10 (vigne détectée), +5 (incertain), +15 (streak bonus)
- **7 badges** : premier_scan, amateur, expert, streaker_3, streaker_7, collectionneur, marathonien
- **Streak** : scan quotidien consécutif
---
## Fonctionnalités clés
| Feature | Fichier principal | Statut |
|---------|-------------------|--------|
| Splash animée | `screens/SplashScreen.tsx` | ✅ |
| Scanner caméra | `screens/ScannerScreen.tsx` | ✅ |
| Résultat + cépage | `screens/ResultScreen.tsx` | ✅ |
| Historique + search | `screens/HistoryScreen.tsx` | ✅ |
| Profil + badges | `screens/ProfileScreen.tsx` | ✅ |
| Gamification XP | `hooks/useGameProgress.ts` | ✅ |
| Persistance | `services/storage.ts` | ✅ |
| Bilingue FR/EN | `i18n/` | ✅ |
---
## Conventions
- Package manager : **pnpm**
- Path alias : `@/*``src/*`
- `useEffect` toujours depuis `react` (jamais depuis `react-native-reanimated`)
- Navigation : React Navigation v7 uniquement, **jamais Expo Router** (`src/app/` est interdit — renommé en `src/screens/`)
- Max 300 lignes par fichier
---
## Commandes
```bash
pnpm start # Lance Metro bundler
pnpm android # Build Android
pnpm ios # Build iOS
```

41
VinEye/app.json Normal file
View file

@ -0,0 +1,41 @@
{
"expo": {
"name": "VinEye",
"slug": "vineye",
"version": "1.0.0",
"scheme": "vineye",
"orientation": "portrait",
"icon": "./src/assets/images/icon.png",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"splash": {
"image": "./src/assets/images/splash.png",
"resizeMode": "contain",
"backgroundColor": "#2D6A4F"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.vineye.app"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./src/assets/images/adaptive-icon.png",
"backgroundColor": "#2D6A4F"
},
"edgeToEdgeEnabled": true,
"package": "com.vineye.app"
},
"web": {
"favicon": "./src/assets/images/icon.png"
},
"plugins": [
"expo-localization",
[
"expo-camera",
{
"cameraPermission": "VinEye needs camera access to detect grapevines."
}
]
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
VinEye/assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
VinEye/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

9
VinEye/babel.config.js Normal file
View file

@ -0,0 +1,9 @@
module.exports = function (api) {
api.cache(true);
return {
presets: [
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
],
plugins: ['react-native-reanimated/plugin'],
};
};

19
VinEye/components.json Normal file
View file

@ -0,0 +1,19 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "global.css",
"baseColor": "neutral",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

50
VinEye/global.css Normal file
View file

@ -0,0 +1,50 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 98%;
--foreground: 0 0% 10.6%;
--card: 0 0% 100%;
--card-foreground: 0 0% 10.6%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 10.6%;
--primary: 153 42% 30%;
--primary-foreground: 0 0% 100%;
--secondary: 147 46% 89%;
--secondary-foreground: 153 42% 30%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 42%;
--accent: 147 46% 89%;
--accent-foreground: 153 42% 30%;
--destructive: 14 100% 63%;
--destructive-foreground: 0 0% 100%;
--border: 0 0% 87.8%;
--input: 0 0% 87.8%;
--ring: 153 42% 30%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 6.7%;
--foreground: 0 0% 98%;
--card: 0 0% 10.6%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 10.6%;
--popover-foreground: 0 0% 98%;
--primary: 147 46% 89%;
--primary-foreground: 153 42% 30%;
--secondary: 153 42% 30%;
--secondary-foreground: 147 46% 89%;
--muted: 0 0% 17.6%;
--muted-foreground: 0 0% 61%;
--accent: 153 42% 30%;
--accent-foreground: 147 46% 89%;
--destructive: 14 100% 63%;
--destructive-foreground: 0 0% 100%;
--border: 0 0% 17.6%;
--input: 0 0% 17.6%;
--ring: 147 46% 89%;
}
}

8
VinEye/index.ts Normal file
View file

@ -0,0 +1,8 @@
import { registerRootComponent } from 'expo';
import App from './App';
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);

6
VinEye/metro.config.js Normal file
View file

@ -0,0 +1,6 @@
const { getDefaultConfig } = require('expo/metro-config');
const { withNativeWind } = require('nativewind/metro');
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, { input: './global.css', inlineRem: 16 });

1
VinEye/nativewind-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="nativewind/types" />

57
VinEye/package.json Normal file
View file

@ -0,0 +1,57 @@
{
"name": "vineye",
"version": "1.0.0",
"main": "index.ts",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"@expo/vector-icons": "^15.1.1",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^7.15.9",
"@react-navigation/native": "^7.2.2",
"@react-navigation/native-stack": "^7.14.10",
"@rn-primitives/portal": "^1.4.0",
"@rn-primitives/separator": "^1.4.0",
"@rn-primitives/slot": "^1.4.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"expo": "~54.0.33",
"expo-camera": "~17.0.10",
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-linear-gradient": "~15.0.8",
"expo-localization": "~17.0.8",
"expo-status-bar": "~3.0.9",
"i18next": "^26.0.1",
"lottie-react-native": "^7.3.6",
"nativewind": "^4.2.3",
"react": "19.1.0",
"react-i18next": "^17.0.1",
"react-lucid": "^0.0.1",
"react-native": "0.81.5",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "^15.12.1",
"react-native-worklets": "0.5.1",
"tailwind-merge": "^3.5.0",
"tailwindcss": "3.4.17"
},
"devDependencies": {
"@types/react": "~19.1.0",
"tailwindcss-animate": "^1.0.7",
"typescript": "~5.9.2"
},
"private": true,
"pnpm": {
"onlyBuiltDependencies": [
"@expo/config-plugins",
"expo",
"expo-modules-core"
]
}
}

7103
VinEye/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
<defs>
<linearGradient id="leafGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2D6A4F;stop-opacity:1" />
<stop offset="100%" style="stop-color:#6A0572;stop-opacity:1" />
</linearGradient>
<linearGradient id="eyeGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#40916C;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8E24AA;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Feuille de vigne stylisée formant un œil -->
<!-- Corps de la feuille (forme ovale penchée = paupières de l'œil) -->
<ellipse cx="100" cy="95" rx="75" ry="45" fill="none" stroke="url(#leafGrad)" stroke-width="6"
transform="rotate(-15, 100, 95)" />
<!-- Iris/pupille centrale -->
<circle cx="100" cy="95" r="22" fill="url(#eyeGrad)" />
<circle cx="100" cy="95" r="10" fill="#1B4332" />
<circle cx="107" cy="88" r="4" fill="white" opacity="0.8" />
<!-- Lobes de la feuille de vigne (3 lobes comme une vraie feuille) -->
<!-- Lobe haut gauche -->
<path d="M 55 70 Q 40 45 60 35 Q 70 55 55 70 Z" fill="url(#leafGrad)" opacity="0.85" />
<!-- Lobe haut droit -->
<path d="M 145 70 Q 160 45 140 35 Q 130 55 145 70 Z" fill="url(#leafGrad)" opacity="0.85" />
<!-- Lobe haut centre -->
<path d="M 100 50 Q 100 25 100 20 Q 108 35 100 50 Z" fill="url(#leafGrad)" opacity="0.9" />
<!-- Pétiole (queue de la feuille) = pupille / tige centrale -->
<line x1="100" y1="140" x2="100" y2="170" stroke="url(#leafGrad)" stroke-width="5" stroke-linecap="round" />
<!-- Vrilles (spirales décoratives) -->
<path d="M 100 165 Q 115 160 120 150 Q 125 140 115 138" fill="none" stroke="url(#eyeGrad)" stroke-width="2.5" stroke-linecap="round" />
<path d="M 100 165 Q 85 160 80 150 Q 75 140 85 138" fill="none" stroke="url(#eyeGrad)" stroke-width="2.5" stroke-linecap="round" />
<!-- Nervures de la feuille -->
<line x1="100" y1="95" x2="60" y2="70" stroke="url(#leafGrad)" stroke-width="1.5" opacity="0.4" stroke-linecap="round" />
<line x1="100" y1="95" x2="140" y2="70" stroke="url(#leafGrad)" stroke-width="1.5" opacity="0.4" stroke-linecap="round" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,39 @@
{
"v": "5.7.4",
"fr": 30,
"ip": 0,
"op": 60,
"w": 400,
"h": 400,
"nm": "confetti",
"ddd": 0,
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "particle",
"ks": {
"o": { "a": 1, "k": [{ "t": 0, "s": [100] }, { "t": 60, "s": [0] }] },
"r": { "a": 1, "k": [{ "t": 0, "s": [0] }, { "t": 60, "s": [360] }] },
"p": { "a": 1, "k": [{ "t": 0, "s": [200, 100, 0] }, { "t": 60, "s": [200, 380, 0] }] },
"s": { "a": 0, "k": [100, 100, 100] }
},
"shapes": [
{
"ty": "rc",
"nm": "rect",
"s": { "a": 0, "k": [10, 10] },
"r": { "a": 0, "k": 2 }
},
{
"ty": "fl",
"nm": "fill",
"c": { "a": 0, "k": [0.18, 0.42, 0.31, 1] },
"o": { "a": 0, "k": 100 }
}
]
}
]
}

View file

@ -0,0 +1,42 @@
{
"v": "5.7.4",
"fr": 30,
"ip": 0,
"op": 60,
"w": 300,
"h": 300,
"nm": "level-up",
"ddd": 0,
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "star",
"ks": {
"o": { "a": 1, "k": [{ "t": 0, "s": [0] }, { "t": 10, "s": [100] }, { "t": 50, "s": [100] }, { "t": 60, "s": [0] }] },
"p": { "a": 0, "k": [150, 150, 0] },
"s": { "a": 1, "k": [{ "t": 0, "s": [0, 0, 100] }, { "t": 20, "s": [120, 120, 100] }, { "t": 30, "s": [100, 100, 100] }] }
},
"shapes": [
{
"ty": "sr",
"nm": "star",
"pt": { "a": 0, "k": 5 },
"p": { "a": 0, "k": [0, 0] },
"r": { "a": 0, "k": 0 },
"ir": { "a": 0, "k": 30 },
"or": { "a": 0, "k": 60 },
"os": { "a": 0, "k": 0 },
"is": { "a": 0, "k": 0 }
},
{
"ty": "fl",
"c": { "a": 0, "k": [0.416, 0.024, 0.447, 1] },
"o": { "a": 0, "k": 100 }
}
]
}
]
}

View file

@ -0,0 +1,38 @@
{
"v": "5.7.4",
"fr": 30,
"ip": 0,
"op": 45,
"w": 200,
"h": 200,
"nm": "scan-success",
"ddd": 0,
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "checkmark",
"ks": {
"o": { "a": 0, "k": 100 },
"r": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [100, 100, 0] },
"s": { "a": 1, "k": [{ "t": 0, "s": [0, 0, 100] }, { "t": 20, "s": [110, 110, 100] }, { "t": 30, "s": [100, 100, 100] }] }
},
"shapes": [
{
"ty": "el",
"nm": "circle",
"s": { "a": 0, "k": [120, 120] },
"p": { "a": 0, "k": [0, 0] }
},
{
"ty": "fl",
"c": { "a": 0, "k": [0.298, 0.686, 0.314, 1] },
"o": { "a": 0, "k": 100 }
}
]
}
]
}

View file

@ -0,0 +1,38 @@
{
"v": "5.7.4",
"fr": 30,
"ip": 0,
"op": 90,
"w": 300,
"h": 300,
"nm": "vine-leaf",
"ddd": 0,
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "leaf",
"ks": {
"o": { "a": 1, "k": [{ "t": 0, "s": [0] }, { "t": 30, "s": [100] }, { "t": 90, "s": [100] }] },
"r": { "a": 1, "k": [{ "t": 0, "s": [-15] }, { "t": 30, "s": [0] }] },
"p": { "a": 0, "k": [150, 150, 0] },
"s": { "a": 1, "k": [{ "t": 0, "s": [0, 0, 100] }, { "t": 30, "s": [100, 100, 100] }] }
},
"shapes": [
{
"ty": "el",
"nm": "leaf-shape",
"s": { "a": 0, "k": [140, 100] },
"p": { "a": 0, "k": [0, 0] }
},
{
"ty": "fl",
"c": { "a": 0, "k": [0.176, 0.416, 0.31, 1] },
"o": { "a": 0, "k": 100 }
}
]
}
]
}

View file

@ -0,0 +1 @@
# TFLite model placeholder - replace with vine_detector.tflite

View file

@ -0,0 +1,76 @@
import { useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withSequence,
withTiming,
} from 'react-native-reanimated';
import { useTranslation } from 'react-i18next';
import { colors } from '@/theme/colors';
import { typography } from '@/theme/typography';
import { spacing } from '@/theme/spacing';
import type { Badge } from '@/types/gamification';
interface BadgeCardProps {
badge: Badge;
showNewAnimation?: boolean;
}
export function BadgeCard({ badge, showNewAnimation = false }: BadgeCardProps) {
const { t } = useTranslation();
const scale = useSharedValue(1);
const glowOpacity = useSharedValue(0);
useEffect(() => {
if (showNewAnimation && badge.unlocked) {
scale.value = withSequence(
withSpring(1.2, { damping: 8 }),
withSpring(1, { damping: 12 })
);
glowOpacity.value = withSequence(
withTiming(1, { duration: 200 }),
withTiming(0.3, { duration: 600 }),
withTiming(1, { duration: 300 }),
withTiming(0, { duration: 800 })
);
}
}, [showNewAnimation, badge.unlocked]);
const animStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const glowStyle = useAnimatedStyle(() => ({
opacity: glowOpacity.value,
}));
return (
<Animated.View style={[styles.container, !badge.unlocked && styles.locked, animStyle]}>
{badge.unlocked && (
<Animated.View style={[styles.glow, glowStyle]} />
)}
<Text style={[styles.icon, !badge.unlocked && styles.lockedIcon]}>
{badge.icon}
</Text>
<Text style={[styles.name, !badge.unlocked && styles.lockedText]} numberOfLines={1}>
{t(badge.nameKey)}
</Text>
</Animated.View>
);
}
const styles = StyleSheet.create({
container: {
width: 88, alignItems: 'center', gap: spacing.xs, padding: spacing.sm,
borderRadius: 12, backgroundColor: colors.primary[100],
position: 'relative', overflow: 'hidden',
},
locked: { backgroundColor: colors.neutral[200] },
glow: { ...StyleSheet.absoluteFillObject, backgroundColor: colors.accent[400], borderRadius: 12 },
icon: { fontSize: 32 },
lockedIcon: { opacity: 0.3 },
name: { fontSize: typography.fontSizes.xs, fontWeight: typography.fontWeights.medium, color: colors.primary[800], textAlign: 'center' },
lockedText: { color: colors.neutral[500] },
});

View file

@ -0,0 +1,54 @@
import { View, Text, StyleSheet } from 'react-native';
import { useTranslation } from 'react-i18next';
import { colors } from '@/theme/colors';
import { typography } from '@/theme/typography';
import { spacing } from '@/theme/spacing';
import { getLevelForXP, getLevelNumber } from '@/utils/achievements';
interface LevelIndicatorProps {
xp: number;
showLabel?: boolean;
}
export function LevelIndicator({ xp, showLabel = true }: LevelIndicatorProps) {
const { t } = useTranslation();
const level = getLevelForXP(xp);
const levelNumber = getLevelNumber(xp);
return (
<View style={styles.container}>
<View style={styles.badge}>
<Text style={styles.number}>{levelNumber}</Text>
</View>
{showLabel && (
<Text style={styles.label}>{t(level.labelKey)}</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
},
badge: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: colors.accent[700],
alignItems: 'center',
justifyContent: 'center',
},
number: {
fontSize: typography.fontSizes.sm,
fontWeight: typography.fontWeights.bold,
color: colors.surface,
},
label: {
fontSize: typography.fontSizes.sm,
fontWeight: typography.fontWeights.semibold,
color: colors.primary[800],
},
});

View file

@ -0,0 +1,74 @@
import { useEffect } from 'react';
import { View } from 'react-native';
import Svg, { Circle } from 'react-native-svg';
import Animated, {
useSharedValue,
useAnimatedProps,
withTiming,
} from 'react-native-reanimated';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
interface ProgressRingProps {
progress: number;
size?: number;
strokeWidth?: number;
color?: string;
trackColor?: string;
children?: React.ReactNode;
}
export function ProgressRing({
progress,
size = 100,
strokeWidth = 8,
color = '#40916C',
trackColor = '#E0E0E0',
children,
}: ProgressRingProps) {
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const animatedProgress = useSharedValue(0);
useEffect(() => {
animatedProgress.value = withTiming(
Math.min(Math.max(progress, 0), 1),
{ duration: 1200 },
);
}, [progress]);
const animatedProps = useAnimatedProps(() => ({
strokeDashoffset: circumference * (1 - animatedProgress.value),
}));
return (
<View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
<Svg
width={size}
height={size}
style={{ position: 'absolute', transform: [{ rotate: '-90deg' }] }}
>
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={trackColor}
strokeWidth={strokeWidth}
fill="none"
/>
<AnimatedCircle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={color}
strokeWidth={strokeWidth}
fill="none"
strokeDasharray={`${circumference} ${circumference}`}
animatedProps={animatedProps}
strokeLinecap="round"
/>
</Svg>
{children}
</View>
);
}

View file

@ -0,0 +1,58 @@
import { View, Text, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { colors } from '@/theme/colors';
import { typography } from '@/theme/typography';
import { spacing } from '@/theme/spacing';
import { useTranslation } from 'react-i18next';
interface StreakCounterProps {
streak: number;
compact?: boolean;
}
export function StreakCounter({ streak, compact = false }: StreakCounterProps) {
const { t } = useTranslation();
if (compact) {
return (
<View style={styles.compactRow}>
<Ionicons name="flame" size={16} color={colors.accent[700]} />
<Text style={styles.compactValue}>{streak}</Text>
</View>
);
}
return (
<View style={styles.container}>
<Ionicons name="flame" size={24} color={colors.accent[700]} />
<Text style={styles.value}>{streak}</Text>
<Text style={styles.label}>{t('profile.days')}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
gap: 2,
},
compactRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
},
value: {
fontSize: typography.fontSizes['2xl'],
fontWeight: typography.fontWeights.bold,
color: colors.accent[700],
},
label: {
fontSize: typography.fontSizes.xs,
color: colors.neutral[600],
},
compactValue: {
fontSize: typography.fontSizes.md,
fontWeight: typography.fontWeights.bold,
color: colors.accent[700],
},
});

View file

@ -0,0 +1,78 @@
import { useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
} from 'react-native-reanimated';
import { useTranslation } from 'react-i18next';
import { colors } from '@/theme/colors';
import { typography } from '@/theme/typography';
import { spacing } from '@/theme/spacing';
import { getXPProgress, getNextLevel, getLevelForXP } from '@/utils/achievements';
interface XPBarProps {
xp: number;
compact?: boolean;
}
export function XPBar({ xp, compact = false }: XPBarProps) {
const { t } = useTranslation();
const { current, total, ratio } = getXPProgress(xp);
const nextLevel = getNextLevel(xp);
const currentLevel = getLevelForXP(xp);
const animatedWidth = useSharedValue(0);
useEffect(() => {
animatedWidth.value = withTiming(ratio, { duration: 800 });
}, [ratio]);
const barStyle = useAnimatedStyle(() => ({
width: `${animatedWidth.value * 100}%`,
}));
if (compact) {
return (
<View style={styles.compactContainer}>
<Text style={styles.compactLevel}>{t(currentLevel.labelKey)}</Text>
<View style={styles.trackCompact}>
<Animated.View style={[styles.bar, barStyle]} />
</View>
<Text style={styles.compactXP}>{xp} XP</Text>
</View>
);
}
return (
<View style={styles.container}>
<View style={styles.labelRow}>
<Text style={styles.levelLabel}>{t(currentLevel.labelKey)}</Text>
{nextLevel && (
<Text style={styles.xpLabel}>{current}/{total} XP</Text>
)}
</View>
<View style={styles.track}>
<Animated.View style={[styles.bar, barStyle]} />
</View>
{nextLevel && (
<Text style={styles.nextLevelLabel}>
{t('levels.xpToNext', { xp: total - current })}
</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
container: { gap: spacing.xs },
compactContainer: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm },
labelRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
levelLabel: { fontSize: typography.fontSizes.sm, fontWeight: typography.fontWeights.semibold, color: colors.primary[800] },
xpLabel: { fontSize: typography.fontSizes.xs, color: colors.neutral[600] },
track: { height: 8, backgroundColor: colors.neutral[200], borderRadius: 4, overflow: 'hidden' },
trackCompact: { flex: 1, height: 5, backgroundColor: colors.neutral[200], borderRadius: 3, overflow: 'hidden' },
bar: { height: '100%', backgroundColor: colors.primary[700], borderRadius: 4 },
nextLevelLabel: { fontSize: typography.fontSizes.xs, color: colors.neutral[500], textAlign: 'right' },
compactLevel: { fontSize: typography.fontSizes.xs, color: colors.primary[700], fontWeight: typography.fontWeights.medium, minWidth: 70 },
compactXP: { fontSize: typography.fontSizes.xs, color: colors.neutral[600], minWidth: 50, textAlign: 'right' },
});

View file

@ -0,0 +1,126 @@
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { useTranslation } from 'react-i18next';
import { colors } from '@/theme/colors';
import { typography } from '@/theme/typography';
import { spacing } from '@/theme/spacing';
import { Badge } from '@/components/ui/Badge';
import { getCepageById } from '@/utils/cepages';
import type { ScanRecord } from '@/types/detection';
interface ScanCardProps {
record: ScanRecord;
onDelete?: () => void;
onPress?: () => void;
}
function formatDate(iso: string): string {
const d = new Date(iso);
return d.toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' });
}
export function ScanCard({ record, onDelete, onPress }: ScanCardProps) {
const { t } = useTranslation();
const { detection } = record;
const cepage = detection.cepageId ? getCepageById(detection.cepageId) : undefined;
const resultColor =
detection.result === 'vine'
? 'success'
: detection.result === 'uncertain'
? 'warning'
: 'danger';
const resultLabel =
detection.result === 'vine'
? t('result.vineDetected')
: detection.result === 'uncertain'
? t('result.uncertain')
: t('result.notVine');
return (
<TouchableOpacity style={styles.container} onPress={onPress} activeOpacity={0.75}>
{/* Colored strip */}
<View style={[styles.strip, { backgroundColor: colors[detection.result === 'vine' ? 'success' : detection.result === 'uncertain' ? 'warning' : 'danger'] }]} />
<View style={styles.content}>
<View style={styles.top}>
<Text style={styles.date}>{formatDate(record.createdAt)}</Text>
<Badge label={`+${record.xpEarned} XP`} color="primary" size="sm" />
</View>
<View style={styles.middle}>
<Badge label={resultLabel} color={resultColor} />
{cepage && (
<Text style={styles.ceepage}>{cepage.name.fr}</Text>
)}
</View>
<View style={styles.bottom}>
<Text style={styles.confidence}>
{t('scanner.confidence')} : <Text style={{ fontWeight: typography.fontWeights.bold }}>{detection.confidence}%</Text>
</Text>
{onDelete && (
<TouchableOpacity onPress={onDelete} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
<Text style={styles.delete}>🗑</Text>
</TouchableOpacity>
)}
</View>
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
backgroundColor: colors.card,
borderRadius: 12,
overflow: 'hidden',
shadowColor: colors.neutral[900],
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 8,
elevation: 2,
},
strip: {
width: 5,
},
content: {
flex: 1,
padding: spacing.md,
gap: spacing.xs,
},
top: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
middle: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
flexWrap: 'wrap',
},
bottom: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
date: {
fontSize: typography.fontSizes.xs,
color: colors.neutral[600],
},
ceepage: {
fontSize: typography.fontSizes.sm,
fontWeight: typography.fontWeights.medium,
color: colors.primary[800],
},
confidence: {
fontSize: typography.fontSizes.xs,
color: colors.neutral[600],
},
delete: {
fontSize: 16,
opacity: 0.6,
},
});

View file

@ -0,0 +1,68 @@
import { FlatList, View, Text, StyleSheet } from 'react-native';
import { useTranslation } from 'react-i18next';
import { ScanCard } from './ScanCard';
import { colors } from '@/theme/colors';
import { typography } from '@/theme/typography';
import { spacing } from '@/theme/spacing';
import type { ScanRecord } from '@/types/detection';
interface ScanListProps {
records: ScanRecord[];
onDelete?: (id: string) => void;
onPressItem?: (record: ScanRecord) => void;
}
export function ScanList({ records, onDelete, onPressItem }: ScanListProps) {
const { t } = useTranslation();
if (records.length === 0) {
return (
<View style={styles.empty}>
<Text style={styles.emptyEmoji}>🍇</Text>
<Text style={styles.emptyText}>{t('history.empty')}</Text>
</View>
);
}
return (
<FlatList
data={records}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<ScanCard
record={item}
onDelete={onDelete ? () => onDelete(item.id) : undefined}
onPress={onPressItem ? () => onPressItem(item) : undefined}
/>
)}
contentContainerStyle={styles.list}
ItemSeparatorComponent={() => <View style={styles.separator} />}
showsVerticalScrollIndicator={false}
/>
);
}
const styles = StyleSheet.create({
list: {
padding: spacing.base,
},
separator: {
height: spacing.sm,
},
empty: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: spacing.base,
paddingVertical: spacing['5xl'],
},
emptyEmoji: {
fontSize: 48,
opacity: 0.5,
},
emptyText: {
fontSize: typography.fontSizes.base,
color: colors.neutral[500],
textAlign: 'center',
},
});

View file

@ -0,0 +1,56 @@
import { useEffect } from "react";
import { View, ScrollView, TouchableOpacity, TextInput } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigation } from "@react-navigation/native";
import type { BottomTabNavigationProp } from "@react-navigation/bottom-tabs";
import { useTranslation } from "react-i18next";
import { Ionicons } from "@expo/vector-icons";
import Animated, {
useSharedValue,
useAnimatedStyle,
withRepeat,
withSequence,
withTiming,
} from "react-native-reanimated";
import { Text } from "@/components/ui/text";
import { ProgressRing } from "@/components/gamification/ProgressRing";
import { ScanCard } from "@/components/history/ScanCard";
import { useGameProgress } from "@/hooks/useGameProgress";
import { useHistory } from "@/hooks/useHistory";
import { colors } from "@/theme/colors";
import {
getLevelForXP,
getLevelNumber,
getXPProgress,
} from "@/utils/achievements";
import type { BottomTabParamList } from "@/types/navigation";
import StatCard from "@/components/home/gamificationstat";
import StatisticsSection from "@/components/home/statssection";
export default function SectionHeader({
title,
onViewAll,
}: {
title: string;
onViewAll?: () => void;
}) {
const { t } = useTranslation();
return (
<View className="flex-row items-center justify-between">
<Text className="text-[17px] font-semibold text-neutral-900">
{title}
</Text>
{onViewAll && (
<TouchableOpacity onPress={onViewAll}>
<Text
className="text-[13px] font-medium"
style={{ color: colors.primary[700] }}
>
{t("common.viewAll") ?? "View all"}
</Text>
</TouchableOpacity>
)}
</View>
);
}

View file

@ -0,0 +1,116 @@
import React from "react";
import { View, Text, Dimensions } from "react-native";
import { Svg, Circle } from "react-native-svg";
import { Ionicons } from "@expo/vector-icons";
interface StatCardProps {
title: string;
value: number; // Ex: 82 pour 82%
trend: string; // Ex: "+5.1%"
isUp?: boolean;
color?: string;
}
const StatCard = ({
title,
value,
trend,
isUp = true,
color = "#3B82F6",
}: StatCardProps) => {
// Config de l'arc
const size = 180;
const strokeWidth = 15;
const radius = (size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI;
// On veut un arc de 180 degrés (le haut du cercle)
const arcTotalLength = circumference / 2;
const progress = (value / 100) * arcTotalLength;
return (
<View className="bg-white p-6 rounded-[32px] shadow-sm overflow-hidden relative border border-gray-50">
{/* Header : Titre + Icône flèche */}
<View className="flex-row justify-between items-start mb-10">
<Text className="text-gray-400 font-medium text-[15px]">{title}</Text>
<View className="bg-gray-50 p-2 rounded-xl">
<Ionicons
name="arrow-up-outline"
size={18}
color="#9CA3AF"
style={{ transform: [{ rotate: "45deg" }] }}
/>
</View>
</View>
<View className="flex-row items-end justify-between">
{/* Section Chiffres */}
<View className="z-10">
<View className="flex-row items-center mb-1">
<Text
className={`text-xs font-bold ${isUp ? "text-blue-500" : "text-red-500"}`}
>
{trend}
</Text>
<Ionicons
name={isUp ? "caret-up" : "caret-down"}
size={12}
color={isUp ? "#3B82F6" : "#EF4444"}
className="ml-1"
/>
</View>
<Text className="text-5xl font-bold text-gray-900 tracking-tighter">
{value}%
</Text>
</View>
{/* L'arc de cercle (Gauge) */}
{/* On tourne de -180 deg pour que l'arc soit en haut/droite comme sur la photo */}
<View
className="absolute -right-6 -bottom-[92px]"
style={{ transform: [{ rotate: "-190deg" }] }}
>
<Svg width={size} height={size}>
{/* Rail gris */}
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="#F3F4F6"
strokeWidth={strokeWidth}
strokeDasharray={`${arcTotalLength} ${circumference}`}
strokeLinecap="round"
fill="none"
/>
{/* Progression colorée */}
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={color}
strokeWidth={strokeWidth}
strokeDasharray={`${progress} ${circumference}`}
strokeLinecap="round"
fill="none"
/>
</Svg>
{/* Le petit curseur blanc au bout du remplissage */}
{/* Note: Le calcul de position exacte du point demande de la trigo,
ici on le place de façon fixe pour le style, ou on le retire pour plus de sobriété */}
<View
className="absolute bg-white rounded-full border-[3px] border-blue-500 shadow-sm"
style={{
width: 12,
height: 12,
top: "47%",
left: 1,
}}
/>
</View>
</View>
</View>
);
};
export default StatCard;

View file

@ -0,0 +1,97 @@
import React from "react";
import { View, Text } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useTranslation } from "react-i18next";
import StatCard from "./gamificationstat";
import SectionHeader from "./components/homeheader";
// import SectionHeader from '@/components/SectionHeader';
export default function StatisticsSection({ progress }: { progress: any }) {
const { t } = useTranslation();
// On ne garde que les deux stats essentielles
const STATS = [
{
label: t("home.daily_streak"),
value: `${progress.streak}`,
unit: t("profile.days"),
icon: "flame",
color: "#F59E0B", // Amber 500
bgColor: "#FFFBEB", // Amber 50
},
{
label: t("home.total_xp"),
value: `${progress.xpTotal}`,
unit: "XP",
icon: "flash",
color: "#3B82F6", // Blue 500
bgColor: "#EFF6FF", // Blue 50
},
];
return (
<View className="mx-5 mb-2">
<View>
<SectionHeader title={t("home.statistics") ?? "Statistics"} />
</View>
{/* Progression Section */}
<View className="my-2">
<StatCard
title="Task Execution"
value={82}
trend="+5.1%"
color="#3B82F6"
/>
</View>
{/* Container avec un gap réduit pour coller les cartes */}
<View className="flex-row gap-3">
{STATS.map((stat, i) => (
<View
key={i}
className="flex-1 bg-white p-4 rounded-[32px] shadow-sm overflow-hidden relative border border-gray-50"
style={{
elevation: 2,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.04,
shadowRadius: 8,
}}
>
{/* Top Row: Icon + Unit */}
<View className="flex-row justify-between items-start mb-2">
<View
className="w-8 h-8 items-center justify-center rounded-xl"
style={{ backgroundColor: stat.bgColor }}
>
<Ionicons
name={stat.icon as any}
size={16}
color={stat.color}
/>
</View>
<Text className="text-[10px] font-bold uppercase tracking-widest text-gray-400">
{stat.unit}
</Text>
</View>
{/* Value Section */}
<View>
<Text className="text-2xl font-bold text-gray-900 leading-tight">
{stat.value}
</Text>
<Text
className="text-[11px] font-medium mt-0.5"
style={{ color: stat.color }}
numberOfLines={1}
>
{stat.label}
</Text>
</View>
</View>
))}
</View>
</View>
);
}

View file

@ -0,0 +1,50 @@
import { View, Text, StyleSheet } from 'react-native';
import { useTranslation } from 'react-i18next';
import { DetectionFrame } from './DetectionFrame';
import { ConfidenceMeter } from './ConfidenceMeter';
import { colors } from '@/theme/colors';
import { typography } from '@/theme/typography';
import { spacing } from '@/theme/spacing';
interface CameraOverlayProps {
isScanning: boolean;
confidence: number;
}
export function CameraOverlay({ isScanning, confidence }: CameraOverlayProps) {
const { t } = useTranslation();
return (
<View style={styles.overlay} pointerEvents="none">
{/* Center frame — dashed rectangle Figma style */}
<View style={styles.centerSection}>
<DetectionFrame size={260} active={isScanning} />
</View>
{/* Bottom — subtle confidence if scanning */}
{isScanning && confidence > 0 && (
<View style={styles.bottomSection}>
<ConfidenceMeter confidence={confidence} />
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
justifyContent: 'center',
alignItems: 'center',
},
centerSection: {
alignItems: 'center',
justifyContent: 'center',
},
bottomSection: {
position: 'absolute',
bottom: spacing['5xl'],
left: spacing.xl,
right: spacing.xl,
},
});

View file

@ -0,0 +1,58 @@
import { useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
} from 'react-native-reanimated';
import { colors } from '@/theme/colors';
import { typography } from '@/theme/typography';
import { spacing } from '@/theme/spacing';
import { useTranslation } from 'react-i18next';
interface ConfidenceMeterProps {
confidence: number; // 0100
}
function getConfidenceColor(confidence: number): string {
if (confidence >= 70) return colors.success;
if (confidence >= 40) return colors.warning;
return colors.danger;
}
export function ConfidenceMeter({ confidence }: ConfidenceMeterProps) {
const { t } = useTranslation();
const animatedWidth = useSharedValue(0);
useEffect(() => {
animatedWidth.value = withTiming(confidence / 100, { duration: 500 });
}, [confidence]);
const barStyle = useAnimatedStyle(() => ({
width: `${animatedWidth.value * 100}%`,
backgroundColor: getConfidenceColor(confidence),
}));
return (
<View style={styles.container}>
<View style={styles.labelRow}>
<Text style={styles.label}>{t('scanner.confidence')}</Text>
<Text style={[styles.value, { color: getConfidenceColor(confidence) }]}>
{confidence}%
</Text>
</View>
<View style={styles.track}>
<Animated.View style={[styles.bar, barStyle]} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { width: '100%', paddingHorizontal: spacing.base, gap: spacing.xs },
labelRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
label: { fontSize: typography.fontSizes.sm, color: colors.surface, fontWeight: typography.fontWeights.medium, opacity: 0.9 },
value: { fontSize: typography.fontSizes.md, fontWeight: typography.fontWeights.bold },
track: { height: 6, backgroundColor: 'rgba(255,255,255,0.2)', borderRadius: 3, overflow: 'hidden' },
bar: { height: 6, borderRadius: 3 },
});

View file

@ -0,0 +1,56 @@
import { useEffect } from 'react';
import { StyleSheet, type ViewStyle } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withRepeat,
withSequence,
withTiming,
} from 'react-native-reanimated';
interface DetectionFrameProps {
size?: number;
active?: boolean;
style?: ViewStyle;
}
export function DetectionFrame({ size = 240, active = true, style }: DetectionFrameProps) {
const scale = useSharedValue(1);
const opacity = useSharedValue(0.7);
useEffect(() => {
if (active) {
scale.value = withRepeat(
withSequence(withTiming(1.02, { duration: 1000 }), withTiming(1, { duration: 1000 })),
-1,
false
);
opacity.value = withRepeat(
withSequence(withTiming(1, { duration: 1000 }), withTiming(0.5, { duration: 1000 })),
-1,
false
);
} else {
scale.value = withTiming(1);
opacity.value = withTiming(0.7);
}
}, [active]);
const animStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: opacity.value,
}));
return (
<Animated.View style={[styles.frame, { width: size, height: size }, animStyle, style]} />
);
}
const styles = StyleSheet.create({
frame: {
borderWidth: 2,
borderColor: 'rgba(255,255,255,0.8)',
borderStyle: 'dashed',
borderRadius: 12,
},
});

View file

@ -0,0 +1,45 @@
import { useEffect } from 'react';
import { Text, type TextStyle } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedProps,
withTiming,
} from 'react-native-reanimated';
import { typography } from '@/theme/typography';
import { colors } from '@/theme/colors';
const AnimatedText = Animated.createAnimatedComponent(Text);
interface AnimatedCounterProps {
value: number;
prefix?: string;
suffix?: string;
style?: TextStyle;
duration?: number;
}
export function AnimatedCounter({
value,
prefix = '',
suffix = '',
style,
duration = 600,
}: AnimatedCounterProps) {
const animatedValue = useSharedValue(0);
useEffect(() => {
animatedValue.value = withTiming(value, { duration });
}, [value, duration]);
const animatedProps = useAnimatedProps(() => ({
text: `${prefix}${Math.floor(animatedValue.value)}${suffix}`,
}));
return (
<AnimatedText
style={[{ fontSize: typography.fontSizes.lg, fontWeight: typography.fontWeights.bold, color: colors.primary[800] }, style]}
// @ts-expect-error animatedProps text property
animatedProps={animatedProps}
/>
);
}

View file

@ -0,0 +1,62 @@
import { View, Text, StyleSheet, type ViewStyle } from 'react-native';
import { colors } from '@/theme/colors';
import { typography } from '@/theme/typography';
import { spacing } from '@/theme/spacing';
type BadgeColor = 'success' | 'warning' | 'danger' | 'primary' | 'accent' | 'neutral';
interface BadgeProps {
label: string;
color?: BadgeColor;
size?: 'sm' | 'md';
style?: ViewStyle;
}
export function Badge({ label, color = 'primary', size = 'md', style }: BadgeProps) {
return (
<View style={[styles.base, COLOR_STYLES[color], size === 'sm' && styles.small, style]}>
<Text style={[styles.text, COLOR_TEXT[color], size === 'sm' && styles.smallText]}>
{label}
</Text>
</View>
);
}
const styles = StyleSheet.create({
base: {
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
borderRadius: 20,
alignSelf: 'flex-start',
},
small: {
paddingHorizontal: spacing.xs + 2,
paddingVertical: 2,
},
text: {
fontSize: typography.fontSizes.xs,
fontWeight: typography.fontWeights.semibold,
letterSpacing: 0.3,
},
smallText: {
fontSize: 10,
},
});
const COLOR_STYLES: Record<BadgeColor, ViewStyle> = {
success: { backgroundColor: '#E8F5E9' },
warning: { backgroundColor: '#FFF3E0' },
danger: { backgroundColor: '#FBE9E7' },
primary: { backgroundColor: colors.primary[200] },
accent: { backgroundColor: colors.accent[300] },
neutral: { backgroundColor: colors.neutral[200] },
};
const COLOR_TEXT: Record<BadgeColor, { color: string }> = {
success: { color: '#2E7D32' },
warning: { color: '#E65100' },
danger: { color: '#BF360C' },
primary: { color: colors.primary[800] },
accent: { color: colors.accent[800] },
neutral: { color: colors.neutral[700] },
};

View file

@ -0,0 +1,106 @@
import { TextClassContext } from '@/components/ui/text';
import { cn } from '@/lib/utils';
import { cva, type VariantProps } from 'class-variance-authority';
import { Platform, Pressable } from 'react-native';
const buttonVariants = cva(
cn(
'group shrink-0 flex-row items-center justify-center gap-2 rounded-md shadow-none',
Platform.select({
web: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
})
),
{
variants: {
variant: {
default: cn(
'bg-primary active:bg-primary/90 shadow-sm shadow-black/5',
Platform.select({ web: 'hover:bg-primary/90' })
),
destructive: cn(
'bg-destructive active:bg-destructive/90 dark:bg-destructive/60 shadow-sm shadow-black/5',
Platform.select({
web: 'hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
})
),
outline: cn(
'border-border bg-background active:bg-accent dark:bg-input/30 dark:border-input dark:active:bg-input/50 border shadow-sm shadow-black/5',
Platform.select({
web: 'hover:bg-accent dark:hover:bg-input/50',
})
),
secondary: cn(
'bg-secondary active:bg-secondary/80 shadow-sm shadow-black/5',
Platform.select({ web: 'hover:bg-secondary/80' })
),
ghost: cn(
'active:bg-accent dark:active:bg-accent/50',
Platform.select({ web: 'hover:bg-accent dark:hover:bg-accent/50' })
),
link: '',
},
size: {
default: cn('h-10 px-4 py-2 sm:h-9', Platform.select({ web: 'has-[>svg]:px-3' })),
sm: cn('h-9 gap-1.5 rounded-md px-3 sm:h-8', Platform.select({ web: 'has-[>svg]:px-2.5' })),
lg: cn('h-11 rounded-md px-6 sm:h-10', Platform.select({ web: 'has-[>svg]:px-4' })),
icon: 'h-10 w-10 sm:h-9 sm:w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
const buttonTextVariants = cva(
cn(
'text-foreground text-sm font-medium',
Platform.select({ web: 'pointer-events-none transition-colors' })
),
{
variants: {
variant: {
default: 'text-primary-foreground',
destructive: 'text-white',
outline: cn(
'group-active:text-accent-foreground',
Platform.select({ web: 'group-hover:text-accent-foreground' })
),
secondary: 'text-secondary-foreground',
ghost: 'group-active:text-accent-foreground',
link: cn(
'text-primary group-active:underline',
Platform.select({ web: 'underline-offset-4 hover:underline group-hover:underline' })
),
},
size: {
default: '',
sm: '',
lg: '',
icon: '',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
type ButtonProps = React.ComponentProps<typeof Pressable> & VariantProps<typeof buttonVariants>;
function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<TextClassContext.Provider value={buttonTextVariants({ variant, size })}>
<Pressable
className={cn(props.disabled && 'opacity-50', buttonVariants({ variant, size }), className)}
role="button"
{...props}
/>
</TextClassContext.Provider>
);
}
export { Button, buttonTextVariants, buttonVariants };
export type { ButtonProps };

View file

@ -0,0 +1,48 @@
import { View, StyleSheet, type ViewStyle } from 'react-native';
import { colors } from '@/theme/colors';
import { spacing } from '@/theme/spacing';
interface CardProps {
children: React.ReactNode;
style?: ViewStyle;
variant?: 'default' | 'elevated' | 'outlined';
padding?: keyof typeof spacing;
}
export function Card({ children, style, variant = 'default', padding = 'base' }: CardProps) {
return (
<View style={[styles.base, VARIANT_STYLES[variant], { padding: spacing[padding] }, style]}>
{children}
</View>
);
}
const styles = StyleSheet.create({
base: {
borderRadius: 20,
backgroundColor: colors.card,
},
});
const VARIANT_STYLES: Record<'default' | 'elevated' | 'outlined', ViewStyle> = {
default: {
shadowColor: colors.neutral[900],
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 10,
elevation: 2,
},
elevated: {
shadowColor: colors.neutral[900],
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08,
shadowRadius: 16,
elevation: 4,
},
outlined: {
borderWidth: 1,
borderColor: colors.neutral[300],
shadowOpacity: 0,
elevation: 0,
},
};

View file

@ -0,0 +1,81 @@
import { useEffect } from 'react';
import { View, StyleSheet, type ViewStyle } from 'react-native';
import Svg, { Circle } from 'react-native-svg';
import Animated, {
useAnimatedProps,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { colors } from '@/theme/colors';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
interface ProgressCircleProps {
size?: number;
strokeWidth?: number;
progress: number; // 01
color?: string;
trackColor?: string;
children?: React.ReactNode;
style?: ViewStyle;
}
export function ProgressCircle({
size = 80,
strokeWidth = 8,
progress,
color = colors.primary[700],
trackColor = colors.neutral[300],
children,
style,
}: ProgressCircleProps) {
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const animatedProgress = useSharedValue(0);
useEffect(() => {
animatedProgress.value = withTiming(progress, { duration: 700 });
}, [progress]);
const animatedProps = useAnimatedProps(() => ({
strokeDashoffset: circumference * (1 - animatedProgress.value),
}));
return (
<View style={[{ width: size, height: size }, style]}>
<Svg width={size} height={size} style={StyleSheet.absoluteFill}>
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={trackColor}
strokeWidth={strokeWidth}
fill="none"
/>
<AnimatedCircle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={color}
strokeWidth={strokeWidth}
fill="none"
strokeLinecap="round"
strokeDasharray={circumference}
animatedProps={animatedProps}
transform={`rotate(-90 ${size / 2} ${size / 2})`}
/>
</Svg>
{children && (
<View style={styles.content}>{children}</View>
)}
</View>
);
}
const styles = StyleSheet.create({
content: {
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center',
},
});

View file

@ -0,0 +1,24 @@
import { cn } from '@/lib/utils';
import * as SeparatorPrimitive from '@rn-primitives/separator';
function Separator({
className,
orientation = 'horizontal',
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
decorative={decorative}
orientation={orientation}
className={cn(
'bg-border shrink-0',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className
)}
{...props}
/>
);
}
export { Separator };

View file

@ -0,0 +1,88 @@
import { cn } from '@/lib/utils';
import * as Slot from '@rn-primitives/slot';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { Platform, Text as RNText, type Role } from 'react-native';
const textVariants = cva(
cn(
'text-foreground text-base',
Platform.select({
web: 'select-text',
})
),
{
variants: {
variant: {
default: '',
h1: cn(
'text-center text-4xl font-extrabold tracking-tight',
Platform.select({ web: 'scroll-m-20 text-balance' })
),
h2: cn(
'border-border border-b pb-2 text-3xl font-semibold tracking-tight',
Platform.select({ web: 'scroll-m-20 first:mt-0' })
),
h3: cn('text-2xl font-semibold tracking-tight', Platform.select({ web: 'scroll-m-20' })),
h4: cn('text-xl font-semibold tracking-tight', Platform.select({ web: 'scroll-m-20' })),
p: 'mt-3 leading-7 sm:mt-6',
blockquote: 'mt-4 border-l-2 pl-3 italic sm:mt-6 sm:pl-6',
code: cn(
'bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold'
),
lead: 'text-muted-foreground text-xl',
large: 'text-lg font-semibold',
small: 'text-sm font-medium leading-none',
muted: 'text-muted-foreground text-sm',
},
},
defaultVariants: {
variant: 'default',
},
}
);
type TextVariantProps = VariantProps<typeof textVariants>;
type TextVariant = NonNullable<TextVariantProps['variant']>;
const ROLE: Partial<Record<TextVariant, Role>> = {
h1: 'heading',
h2: 'heading',
h3: 'heading',
h4: 'heading',
blockquote: Platform.select({ web: 'blockquote' as Role }),
code: Platform.select({ web: 'code' as Role }),
};
const ARIA_LEVEL: Partial<Record<TextVariant, string>> = {
h1: '1',
h2: '2',
h3: '3',
h4: '4',
};
const TextClassContext = React.createContext<string | undefined>(undefined);
function Text({
className,
asChild = false,
variant = 'default',
...props
}: React.ComponentProps<typeof RNText> &
TextVariantProps & {
asChild?: boolean;
}) {
const textClass = React.useContext(TextClassContext);
const Component = asChild ? Slot.Text : RNText;
return (
<Component
className={cn(textVariants({ variant }), textClass, className)}
role={variant ? ROLE[variant] : undefined}
aria-level={variant ? ARIA_LEVEL[variant] : undefined}
{...props}
/>
);
}
export { Text, TextClassContext };

View file

@ -0,0 +1,33 @@
import { useState, useCallback } from 'react';
import { runInference } from '@/services/tflite/model';
import type { Detection } from '@/types/detection';
// TODO: Remplacer par le vrai hook TFLite avec react-native-fast-tflite
export function useDetection() {
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [lastDetection, setLastDetection] = useState<Detection | null>(null);
const [error, setError] = useState<string | null>(null);
const analyze = useCallback(async (imageUri?: string): Promise<Detection | null> => {
setIsAnalyzing(true);
setError(null);
try {
const detection = await runInference(imageUri);
setLastDetection(detection);
return detection;
} catch (err) {
setError('Erreur lors de l\'analyse. Veuillez réessayer.');
return null;
} finally {
setIsAnalyzing(false);
}
}, []);
const reset = useCallback(() => {
setLastDetection(null);
setError(null);
}, []);
return { analyze, isAnalyzing, lastDetection, error, reset };
}

View file

@ -0,0 +1,151 @@
import { useState, useEffect, useCallback } from 'react';
import { storage } from '@/services/storage';
import type { GameProgress, BadgeId } from '@/types/gamification';
import type { Detection } from '@/types/detection';
import {
createInitialBadges,
checkNewBadges,
getLevelNumber,
XP_REWARDS,
} from '@/utils/achievements';
const INITIAL_PROGRESS: GameProgress = {
xp: 0,
level: 1,
badges: createInitialBadges(),
streak: 0,
lastScanDate: null,
totalScans: 0,
uniqueGrapes: [],
bestStreak: 0,
highConfidenceScans: 0,
};
function isSameDay(a: Date, b: Date): boolean {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
}
function isYesterday(date: Date, reference: Date): boolean {
const yesterday = new Date(reference);
yesterday.setDate(yesterday.getDate() - 1);
return isSameDay(date, yesterday);
}
export function useGameProgress() {
const [progress, setProgress] = useState<GameProgress>(INITIAL_PROGRESS);
const [newlyUnlockedBadges, setNewlyUnlockedBadges] = useState<BadgeId[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
loadProgress();
}, []);
async function loadProgress() {
setIsLoading(true);
const saved = await storage.get<GameProgress>(storage.KEYS.GAME_PROGRESS);
setProgress(saved ?? INITIAL_PROGRESS);
setIsLoading(false);
}
const processDetection = useCallback(async (detection: Detection): Promise<number> => {
let xpEarned = 0;
setProgress((prev) => {
const now = new Date();
const lastDate = prev.lastScanDate ? new Date(prev.lastScanDate) : null;
// Update streak
let newStreak = prev.streak;
if (detection.result === 'vine') {
if (!lastDate || isYesterday(lastDate, now)) {
newStreak = prev.streak + 1;
} else if (!lastDate || !isSameDay(lastDate, now)) {
newStreak = 1;
}
// Same day — no streak change
}
// Calculate XP
if (detection.result === 'vine') {
xpEarned += XP_REWARDS.SCAN_SUCCESS;
if (detection.cepageId && !prev.uniqueGrapes.includes(detection.cepageId)) {
xpEarned += XP_REWARDS.NEW_CEEPAGE;
}
if (newStreak > prev.streak) {
xpEarned += XP_REWARDS.DAILY_STREAK_BONUS;
}
if (detection.confidence > 90) {
xpEarned += XP_REWARDS.HIGH_CONFIDENCE_BONUS;
}
}
const newXP = prev.xp + xpEarned;
const newLevel = getLevelNumber(newXP);
const updatedUniqueGrapes =
detection.cepageId && !prev.uniqueGrapes.includes(detection.cepageId)
? [...prev.uniqueGrapes, detection.cepageId]
: prev.uniqueGrapes;
const updatedTotalScans = prev.totalScans + 1;
const updatedHighConf =
detection.result === 'vine' && detection.confidence > 95
? prev.highConfidenceScans + 1
: prev.highConfidenceScans;
const nextProgress: GameProgress = {
...prev,
xp: newXP,
level: newLevel,
streak: newStreak,
bestStreak: Math.max(prev.bestStreak, newStreak),
lastScanDate: new Date().toISOString(),
totalScans: updatedTotalScans,
uniqueGrapes: updatedUniqueGrapes,
highConfidenceScans: updatedHighConf,
badges: prev.badges,
};
// Check badges
const { badges, newlyUnlocked } = checkNewBadges(nextProgress, prev.badges);
nextProgress.badges = badges;
if (newlyUnlocked.length > 0) {
setNewlyUnlockedBadges(newlyUnlocked);
}
// Persist
storage.set(storage.KEYS.GAME_PROGRESS, nextProgress);
return nextProgress;
});
return xpEarned;
}, []);
const clearNewlyUnlocked = useCallback(() => {
setNewlyUnlockedBadges([]);
}, []);
const resetProgress = useCallback(async () => {
await storage.remove(storage.KEYS.GAME_PROGRESS);
setProgress(INITIAL_PROGRESS);
}, []);
return {
progress,
isLoading,
processDetection,
newlyUnlockedBadges,
clearNewlyUnlocked,
resetProgress,
reload: loadProgress,
};
}

View file

@ -0,0 +1,42 @@
import { useState, useEffect, useCallback } from 'react';
import { storage } from '@/services/storage';
import type { ScanRecord } from '@/types/detection';
export function useHistory() {
const [history, setHistory] = useState<ScanRecord[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
loadHistory();
}, []);
async function loadHistory() {
setIsLoading(true);
const saved = await storage.get<ScanRecord[]>(storage.KEYS.SCAN_HISTORY);
setHistory(saved ?? []);
setIsLoading(false);
}
const addScan = useCallback(async (record: ScanRecord) => {
setHistory((prev) => {
const updated = [record, ...prev];
storage.set(storage.KEYS.SCAN_HISTORY, updated);
return updated;
});
}, []);
const deleteScan = useCallback(async (id: string) => {
setHistory((prev) => {
const updated = prev.filter((r) => r.id !== id);
storage.set(storage.KEYS.SCAN_HISTORY, updated);
return updated;
});
}, []);
const clearHistory = useCallback(async () => {
await storage.remove(storage.KEYS.SCAN_HISTORY);
setHistory([]);
}, []);
return { history, isLoading, addScan, deleteScan, clearHistory, reload: loadHistory };
}

114
VinEye/src/i18n/en.json Normal file
View file

@ -0,0 +1,114 @@
{
"common": {
"scan": "Scan",
"history": "History",
"profile": "Profile",
"home": "Home",
"viewAll": "View all",
"cancel": "Cancel",
"confirm": "Confirm",
"loading": "Loading...",
"error": "Error",
"retry": "Retry"
},
"home": {
"greeting": "Hello, Winemaker!",
"scanButton": "Scan a vine",
"totalScans": "Total scans",
"uniqueGrapes": "Grapes found",
"currentStreak": "Current streak",
"progression": "Progression",
"statistics": "Statistics",
"bannerTitle": "Start your collection",
"bannerSubtitle": "Discover grape varieties and grow your wine knowledge",
"bannerButton": "Get started",
"lastScan": "Last scan",
"noScansYet": "No scans yet",
"startScanning": "Start scanning!"
},
"scanner": {
"scanning": "Analyzing...",
"pointCamera": "Point the camera at a vine",
"confidence": "Confidence",
"capture": "Capture",
"analyzing": "Analyzing...",
"permissionRequired": "Camera permission required",
"permissionMessage": "VinEye needs camera access to detect grapevines.",
"grantPermission": "Grant camera access",
"identify": "Identify the plant"
},
"result": {
"vineDetected": "Vine detected!",
"notVine": "This is not a vine",
"uncertain": "Uncertain result",
"grape": "Probable variety",
"origin": "Origin",
"characteristics": "Characteristics",
"regions": "Typical regions",
"xpEarned": "XP earned",
"scanAgain": "Scan again",
"viewHistory": "View history",
"color": "Color",
"red": "Red",
"white": "White",
"rose": "Rosé"
},
"history": {
"title": "History",
"empty": "No scans in history",
"filter": {
"all": "All",
"vine": "Vine",
"notVine": "Not vine"
},
"search": "Search a variety...",
"sortBy": "Sort by",
"date": "Date",
"confidence": "Confidence",
"deleteConfirm": "Delete this scan?"
},
"profile": {
"title": "Profile",
"stats": "Statistics",
"totalScans": "Total scans",
"successRate": "Success rate",
"bestStreak": "Best streak",
"uniqueGrapes": "Unique grapes",
"badges": "Badges",
"nextLevel": "Next level",
"language": "Language",
"resetData": "Reset data",
"resetConfirm": "Are you sure you want to reset all data?",
"days": "days"
},
"achievements": {
"firstScan": "First Scan",
"firstScanDesc": "First scan completed",
"connoisseur": "Connoisseur",
"connoisseurDesc": "10 different grapes identified",
"onFire": "On Fire",
"onFireDesc": "7-day consecutive streak",
"sharpEye": "Sharp Eye",
"sharpEyeDesc": "5 scans with confidence > 95%",
"explorer": "Explorer",
"explorerDesc": "Scans in 3 different regions",
"perfectionist": "Perfectionist",
"perfectionistDesc": "50 successful scans",
"master": "Master Ampelographer",
"masterDesc": "All badges unlocked",
"unlocked": "Badge unlocked!",
"locked": "Locked",
"xpEarned": "+{{xp}} XP"
},
"levels": {
"bud": "Bud",
"leaf": "Leaf",
"shoot": "Shoot",
"cluster": "Cluster",
"harvester": "Harvester",
"winemaker": "Winemaker",
"cellarMaster": "Cellar Master",
"level": "Level {{level}}",
"xpToNext": "{{xp}} XP to next level"
}
}

114
VinEye/src/i18n/fr.json Normal file
View file

@ -0,0 +1,114 @@
{
"common": {
"scan": "Scanner",
"history": "Historique",
"profile": "Profil",
"home": "Accueil",
"viewAll": "Voir tout",
"cancel": "Annuler",
"confirm": "Confirmer",
"loading": "Chargement...",
"error": "Erreur",
"retry": "Réessayer"
},
"home": {
"greeting": "Bonjour, Vigneron !",
"scanButton": "Scanner une vigne",
"totalScans": "Scans totaux",
"uniqueGrapes": "Cépages trouvés",
"currentStreak": "Streak actuel",
"progression": "Progression",
"statistics": "Statistiques",
"bannerTitle": "Commencez votre collection",
"bannerSubtitle": "Découvrez les cépages et enrichissez vos connaissances viticoles",
"bannerButton": "Commencer",
"lastScan": "Dernier scan",
"noScansYet": "Aucun scan pour l'instant",
"startScanning": "Commencez à scanner !"
},
"scanner": {
"scanning": "Analyse en cours...",
"pointCamera": "Pointez la caméra vers une vigne",
"confidence": "Confiance",
"capture": "Capturer",
"analyzing": "Analyse...",
"permissionRequired": "Permission caméra requise",
"permissionMessage": "VinEye nécessite l'accès à votre caméra pour détecter les vignes.",
"grantPermission": "Autoriser la caméra",
"identify": "Identifier la plante"
},
"result": {
"vineDetected": "Vigne détectée !",
"notVine": "Ce n'est pas une vigne",
"uncertain": "Résultat incertain",
"grape": "Cépage probable",
"origin": "Origine",
"characteristics": "Caractéristiques",
"regions": "Régions typiques",
"xpEarned": "XP gagnés",
"scanAgain": "Scanner encore",
"viewHistory": "Voir l'historique",
"color": "Couleur",
"red": "Rouge",
"white": "Blanc",
"rose": "Rosé"
},
"history": {
"title": "Historique",
"empty": "Aucun scan dans l'historique",
"filter": {
"all": "Tous",
"vine": "Vigne",
"notVine": "Pas vigne"
},
"search": "Rechercher un cépage...",
"sortBy": "Trier par",
"date": "Date",
"confidence": "Confiance",
"deleteConfirm": "Supprimer ce scan ?"
},
"profile": {
"title": "Profil",
"stats": "Statistiques",
"totalScans": "Total scans",
"successRate": "Taux de réussite",
"bestStreak": "Meilleur streak",
"uniqueGrapes": "Cépages uniques",
"badges": "Badges",
"nextLevel": "Prochain niveau",
"language": "Langue",
"resetData": "Réinitialiser les données",
"resetConfirm": "Êtes-vous sûr de vouloir réinitialiser toutes les données ?",
"days": "jours"
},
"achievements": {
"firstScan": "Premier Scan",
"firstScanDesc": "Premier scan effectué",
"connoisseur": "Connaisseur",
"connoisseurDesc": "10 cépages différents identifiés",
"onFire": "En Feu",
"onFireDesc": "Streak de 7 jours consécutifs",
"sharpEye": "Œil de Lynx",
"sharpEyeDesc": "5 scans avec confiance > 95%",
"explorer": "Explorateur",
"explorerDesc": "Scans dans 3 régions différentes",
"perfectionist": "Perfectionniste",
"perfectionistDesc": "50 scans réussis",
"master": "Maître Ampélographe",
"masterDesc": "Tous les badges débloqués",
"unlocked": "Badge débloqué !",
"locked": "Verrouillé",
"xpEarned": "+{{xp}} XP"
},
"levels": {
"bud": "Bourgeon",
"leaf": "Feuille",
"shoot": "Sarment",
"cluster": "Grappe",
"harvester": "Vendangeur",
"winemaker": "Vigneron",
"cellarMaster": "Maître de Chai",
"level": "Niveau {{level}}",
"xpToNext": "{{xp}} XP pour le prochain niveau"
}
}

25
VinEye/src/i18n/index.ts Normal file
View file

@ -0,0 +1,25 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { getLocales } from 'expo-localization';
import fr from './fr.json';
import en from './en.json';
const deviceLocale = getLocales()[0]?.languageCode ?? 'fr';
i18n
.use(initReactI18next)
.init({
resources: {
fr: { translation: fr },
en: { translation: en },
},
lng: deviceLocale === 'fr' ? 'fr' : 'en',
fallbackLng: 'fr',
interpolation: {
escapeValue: false,
},
compatibilityJSON: 'v4',
});
export default i18n;

81
VinEye/src/lib/theme.ts Normal file
View file

@ -0,0 +1,81 @@
import { DarkTheme, DefaultTheme, type Theme } from '@react-navigation/native';
export const THEME = {
light: {
background: 'hsl(0 0% 98%)',
foreground: 'hsl(0 0% 10.6%)',
card: 'hsl(0 0% 100%)',
cardForeground: 'hsl(0 0% 10.6%)',
popover: 'hsl(0 0% 100%)',
popoverForeground: 'hsl(0 0% 10.6%)',
primary: 'hsl(153 42% 30%)',
primaryForeground: 'hsl(0 0% 100%)',
secondary: 'hsl(147 46% 89%)',
secondaryForeground: 'hsl(153 42% 30%)',
muted: 'hsl(0 0% 96.1%)',
mutedForeground: 'hsl(0 0% 42%)',
accent: 'hsl(147 46% 89%)',
accentForeground: 'hsl(153 42% 30%)',
destructive: 'hsl(14 100% 63%)',
border: 'hsl(0 0% 87.8%)',
input: 'hsl(0 0% 87.8%)',
ring: 'hsl(153 42% 30%)',
radius: '0.625rem',
chart1: 'hsl(153 42% 30%)',
chart2: 'hsl(147 46% 89%)',
chart3: 'hsl(282 100% 22%)',
chart4: 'hsl(37 100% 65%)',
chart5: 'hsl(14 100% 63%)',
},
dark: {
background: 'hsl(0 0% 6.7%)',
foreground: 'hsl(0 0% 98%)',
card: 'hsl(0 0% 10.6%)',
cardForeground: 'hsl(0 0% 98%)',
popover: 'hsl(0 0% 10.6%)',
popoverForeground: 'hsl(0 0% 98%)',
primary: 'hsl(147 46% 89%)',
primaryForeground: 'hsl(153 42% 30%)',
secondary: 'hsl(153 42% 30%)',
secondaryForeground: 'hsl(147 46% 89%)',
muted: 'hsl(0 0% 17.6%)',
mutedForeground: 'hsl(0 0% 61%)',
accent: 'hsl(153 42% 30%)',
accentForeground: 'hsl(147 46% 89%)',
destructive: 'hsl(14 100% 63%)',
border: 'hsl(0 0% 17.6%)',
input: 'hsl(0 0% 17.6%)',
ring: 'hsl(147 46% 89%)',
radius: '0.625rem',
chart1: 'hsl(147 46% 89%)',
chart2: 'hsl(153 42% 30%)',
chart3: 'hsl(282 100% 50%)',
chart4: 'hsl(37 100% 65%)',
chart5: 'hsl(14 100% 63%)',
},
};
export const NAV_THEME: Record<'light' | 'dark', Theme> = {
light: {
...DefaultTheme,
colors: {
background: THEME.light.background,
border: THEME.light.border,
card: THEME.light.card,
notification: THEME.light.destructive,
primary: THEME.light.primary,
text: THEME.light.foreground,
},
},
dark: {
...DarkTheme,
colors: {
background: THEME.dark.background,
border: THEME.dark.border,
card: THEME.dark.card,
notification: THEME.dark.destructive,
primary: THEME.dark.primary,
text: THEME.dark.foreground,
},
},
};

6
VinEye/src/lib/utils.ts Normal file
View file

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View file

@ -0,0 +1,148 @@
import React from 'react';
import { View, Text, TouchableOpacity, Platform, LayoutAnimation, UIManager } from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTranslation } from 'react-i18next';
import * as Haptics from 'expo-haptics';
// Imports de tes écrans
import HomeScreen from '@/screens/HomeScreen';
import ScannerScreen from '@/screens/ScannerScreen';
import HistoryScreen from '@/screens/HistoryScreen';
import ProfileScreen from '@/screens/ProfileScreen';
// Activation de LayoutAnimation pour Android
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
const Tab = createBottomTabNavigator();
function MyCustomTabBar({ state, descriptors, navigation }: any) {
const insets = useSafeAreaInsets();
const { t } = useTranslation();
// Gestion de la marge basse pour éviter la superposition avec la barre système
const safeBottom = Platform.OS === 'android' ? Math.max(insets.bottom, 24) : insets.bottom;
return (
<View
className="absolute bg-white flex-row items-center justify-between px-2"
style={{
bottom: safeBottom + 10,
left: 20,
right: 20,
height: 70,
borderRadius: 35,
elevation: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.15,
shadowRadius: 20,
}}
>
{state.routes.map((route: any, index: number) => {
const { options } = descriptors[route.key];
const isFocused = state.index === index;
const onPress = () => {
// 1. Retour Haptique (Vibration légère "Impact")
if (Platform.OS !== 'web') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
// 2. Animation de la transition (Pill expansion)
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name);
}
};
// Choix de l'icône (Outline vs Solid)
const getIcon = (name: string, focused: boolean) => {
switch (name) {
case 'Home': return focused ? 'home' : 'home-outline';
case 'History': return focused ? 'receipt' : 'receipt-outline';
case 'Scanner': return focused ? 'scan' : 'scan-outline';
case 'Profile': return focused ? 'person' : 'person-outline';
default: return 'help-outline';
}
};
const label = options.tabBarLabel || route.name;
return (
<TouchableOpacity
key={index}
onPress={onPress}
activeOpacity={0.7}
style={{ flex: isFocused ? 2 : 1 }}
className="items-center justify-center h-full"
>
<View
className={`flex-row items-center justify-center py-2.5 ${
isFocused ? 'bg-gray-900 px-5' : 'bg-transparent px-0'
}`}
style={{ borderRadius: 999 }}
>
<Ionicons
name={getIcon(route.name, isFocused) as any}
size={22}
color={isFocused ? '#FFFFFF' : '#9CA3AF'}
/>
{isFocused && (
<Text
numberOfLines={1}
className="ml-2 text-white font-bold text-[13px]"
>
{label}
</Text>
)}
</View>
</TouchableOpacity>
);
})}
</View>
);
}
export default function BottomTabNavigator() {
const { t } = useTranslation();
return (
<Tab.Navigator
tabBar={(props) => <MyCustomTabBar {...props} />}
screenOptions={{ headerShown: false }}
>
<Tab.Screen
name="Home"
component={HomeScreen}
options={{ tabBarLabel: t('common.home') }}
/>
<Tab.Screen
name="History"
component={HistoryScreen}
options={{ tabBarLabel: t('common.history') }}
/>
<Tab.Screen
name="Scanner"
component={ScannerScreen}
options={{ tabBarLabel: 'Scan' }}
/>
<Tab.Screen
name="Profile"
component={ProfileScreen}
options={{ tabBarLabel: t('common.profile') }}
/>
</Tab.Navigator>
);
}

View file

@ -0,0 +1,29 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { NavigationContainer } from '@react-navigation/native';
import SplashScreen from '@/screens/SplashScreen';
import ResultScreen from '@/screens/ResultScreen';
import BottomTabNavigator from './BottomTabNavigator';
import linking from './linking';
import type { RootStackParamList } from '@/types/navigation';
const Stack = createNativeStackNavigator<RootStackParamList>();
export default function RootNavigator() {
return (
<NavigationContainer linking={linking}>
<Stack.Navigator
initialRouteName="Splash"
screenOptions={{ headerShown: false, animation: 'fade' }}
>
<Stack.Screen name="Splash" component={SplashScreen} />
<Stack.Screen name="Main" component={BottomTabNavigator} />
<Stack.Screen
name="Result"
component={ResultScreen}
options={{ animation: 'slide_from_bottom', presentation: 'modal' }}
/>
</Stack.Navigator>
</NavigationContainer>
);
}

View file

@ -0,0 +1,22 @@
import type { LinkingOptions } from '@react-navigation/native';
import type { RootStackParamList } from '@/types/navigation';
const linking: LinkingOptions<RootStackParamList> = {
prefixes: ['vineye://'],
config: {
screens: {
Splash: 'splash',
Main: {
screens: {
Home: 'home',
Scanner: 'scan',
History: 'history',
Profile: 'profile',
},
},
Result: 'result',
},
},
};
export default linking;

View file

@ -0,0 +1,217 @@
import { useState, useMemo } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useTranslation } from "react-i18next";
import { Ionicons } from '@expo/vector-icons';
import { ScanList } from "@/components/history/ScanList";
import { useHistory } from "@/hooks/useHistory";
import { getCepageById } from "@/utils/cepages";
import { colors } from "@/theme/colors";
import { typography } from "@/theme/typography";
import { spacing } from "@/theme/spacing";
import type { DetectionResult, ScanRecord } from "@/types/detection";
type Filter = "all" | "vine" | "not_vine";
type SortBy = "date" | "confidence";
export default function HistoryScreen() {
const { t } = useTranslation();
const { history, deleteScan } = useHistory();
const [filter, setFilter] = useState<Filter>("all");
const [sortBy, setSortBy] = useState<SortBy>("date");
const [search, setSearch] = useState("");
const filteredHistory = useMemo(() => {
let result: ScanRecord[] = [...history];
// Filter by result type
if (filter === "vine") {
result = result.filter((r) => r.detection.result === "vine");
} else if (filter === "not_vine") {
result = result.filter((r) => r.detection.result !== "vine");
}
// Filter by ceepage search
if (search.trim()) {
const q = search.toLowerCase().trim();
result = result.filter((r) => {
if (!r.detection.cepageId) return false;
const c = getCepageById(r.detection.cepageId);
return (
c?.name.fr.toLowerCase().includes(q) ||
c?.name.en.toLowerCase().includes(q)
);
});
}
// Sort
result.sort((a, b) => {
if (sortBy === "date") {
return (
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
}
return b.detection.confidence - a.detection.confidence;
});
return result;
}, [history, filter, sortBy, search]);
function handleDelete(id: string) {
Alert.alert(t("history.deleteConfirm"), undefined, [
{ text: t("common.cancel"), style: "cancel" },
{
text: "Supprimer",
style: "destructive",
onPress: () => deleteScan(id),
},
]);
}
return (
<SafeAreaView style={styles.safe} edges={["top"]}>
<View style={styles.header}>
<Text style={styles.title}>{t("history.title")}</Text>
<View style={styles.header}>
<Ionicons name="search-outline" size={22} color={colors.neutral[800]} />
<TextInput
style={styles.search}
placeholder={t("history.search")}
placeholderTextColor={colors.neutral[500]}
value={search}
onChangeText={setSearch}
/>
</View>
{/* Search */}
{/* <TextInput
style={styles.search}
placeholder={t("history.search")}
placeholderTextColor={colors.neutral[500]}
value={search}
onChangeText={setSearch}
/> */}
{/* Filters */}
<View style={styles.filters}>
{(["all", "vine", "not_vine"] as Filter[]).map((f) => (
<TouchableOpacity
key={f}
style={[styles.filterBtn, filter === f && styles.filterBtnActive]}
onPress={() => setFilter(f)}
>
<Text
style={[
styles.filterText,
filter === f && styles.filterTextActive,
]}
>
{t(`history.filter.${f === "not_vine" ? "notVine" : f}`)}
</Text>
</TouchableOpacity>
))}
</View>
{/* Sort */}
<View style={styles.sortRow}>
<Text style={styles.sortLabel}>{t("history.sortBy")} :</Text>
{(["date", "confidence"] as SortBy[]).map((s) => (
<TouchableOpacity
key={s}
onPress={() => setSortBy(s)}
style={[styles.sortBtn, sortBy === s && styles.sortBtnActive]}
>
<Text
style={[styles.sortText, sortBy === s && styles.sortTextActive]}
>
{t(`history.${s}`)}
</Text>
</TouchableOpacity>
))}
</View>
</View>
<ScanList records={filteredHistory} onDelete={handleDelete} />
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, backgroundColor: colors.background },
header: {
paddingHorizontal: spacing.base,
paddingTop: spacing.base,
paddingBottom: spacing.sm,
gap: spacing.sm,
borderBottomWidth: 1,
borderBottomColor: colors.neutral[300],
backgroundColor: colors.surface,
},
title: {
fontSize: typography.fontSizes["2xl"],
fontWeight: typography.fontWeights.bold,
color: colors.neutral[900],
},
search: {
backgroundColor: colors.neutral[200],
borderRadius: 10,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
fontSize: typography.fontSizes.sm,
color: colors.neutral[900],
},
filters: {
flexDirection: "row",
gap: spacing.sm,
},
filterBtn: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
borderRadius: 20,
backgroundColor: colors.neutral[200],
},
filterBtnActive: {
backgroundColor: colors.primary[700],
},
filterText: {
fontSize: typography.fontSizes.sm,
color: colors.neutral[700],
fontWeight: typography.fontWeights.medium,
},
filterTextActive: {
color: colors.surface,
},
sortRow: {
flexDirection: "row",
alignItems: "center",
gap: spacing.sm,
},
sortLabel: {
fontSize: typography.fontSizes.xs,
color: colors.neutral[600],
},
sortBtn: {
paddingHorizontal: spacing.sm,
paddingVertical: 4,
borderRadius: 8,
},
sortBtnActive: {
backgroundColor: colors.primary[200],
},
sortText: {
fontSize: typography.fontSizes.xs,
color: colors.neutral[600],
},
sortTextActive: {
color: colors.primary[800],
fontWeight: typography.fontWeights.semibold,
},
});

View file

@ -0,0 +1,217 @@
import { useEffect } from "react";
import { View, ScrollView, TouchableOpacity, TextInput } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigation } from "@react-navigation/native";
import type { BottomTabNavigationProp } from "@react-navigation/bottom-tabs";
import { useTranslation } from "react-i18next";
import { Ionicons } from "@expo/vector-icons";
import Animated, {
useSharedValue,
useAnimatedStyle,
withRepeat,
withSequence,
withTiming,
} from "react-native-reanimated";
import { Text } from "@/components/ui/text";
import { ProgressRing } from "@/components/gamification/ProgressRing";
import { ScanCard } from "@/components/history/ScanCard";
import { useGameProgress } from "@/hooks/useGameProgress";
import { useHistory } from "@/hooks/useHistory";
import { colors } from "@/theme/colors";
import {
getLevelForXP,
getLevelNumber,
getXPProgress,
} from "@/utils/achievements";
import type { BottomTabParamList } from "@/types/navigation";
import StatCard from "@/components/home/gamificationstat";
import StatisticsSection from "@/components/home/statssection";
import SectionHeader from "@/components/home/components/homeheader";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
type HomeNav = BottomTabNavigationProp<BottomTabParamList, "Home">;
interface GameProgress {
totalScans: number;
uniqueGrapes: string[];
streak: number;
}
const userProgress = {
streak: 12, // La série de jours
xpTotal: 2450, // Le total de points XP
// ... tes autres données
};
const STAT_CARDS: {
labelKey: string;
icon: keyof typeof Ionicons.glyphMap;
bg: string;
accent: string;
dark: string;
getValue: (p: GameProgress) => number;
}[] = [
{
labelKey: "home.totalScans",
icon: "scan-outline",
bg: "#E8F0EA",
accent: "#2D6A4F",
dark: "#1B4332",
getValue: (p) => p.totalScans,
},
{
labelKey: "home.uniqueGrapes",
icon: "leaf-outline",
bg: "#EBE5F6",
accent: "#7B5EA7",
dark: "#3E0047",
getValue: (p) => p.uniqueGrapes.length,
},
{
labelKey: "home.currentStreak",
icon: "flame-outline",
bg: "#F0EBE3",
accent: "#8B7355",
dark: "#4A3F30",
getValue: (p) => p.streak,
},
];
export default function HomeScreen() {
const { t } = useTranslation();
const navigation = useNavigation<HomeNav>();
const { progress } = useGameProgress();
const { history } = useHistory();
const pulse = useSharedValue(1);
useEffect(() => {
pulse.value = withRepeat(
withSequence(
withTiming(1.04, { duration: 1200 }),
withTiming(1, { duration: 1200 }),
),
-1,
false,
);
}, []);
const pulseStyle = useAnimatedStyle(() => ({
transform: [{ scale: pulse.value }],
}));
const lastScan = history[0];
const currentLevel = getLevelForXP(progress.xp);
const levelNumber = getLevelNumber(progress.xp);
const {
current: xpInLevel,
total: xpTotal,
ratio: xpRatio,
} = getXPProgress(progress.xp);
return (
<SafeAreaView className="flex-1 bg-[#FAFAFA]" edges={["top"]}>
<ScrollView
className="flex-1"
showsVerticalScrollIndicator={false}
contentContainerStyle={{
paddingBottom: 130, // La hauteur de ta barre (70) + le margin bas (~34) + de l'espace pour respirer (26)
}}
>
{/* Header — title left, icons right */}
<View className="flex-row items-center justify-between px-5 pt-3 pb-4">
<View className="flex-1 flex-row items-center gap-2">
<View className="flex-1 flex-row items-center rounded-full bg-neutral-200 px-3 py-2">
<Ionicons
name="search-outline"
size={18}
color={colors.neutral[500]}
/>
<TextInput
className="ml-2 flex-1 text-[14px]"
placeholder={t("history.search")}
placeholderTextColor={colors.neutral[500]}
style={{ color: colors.neutral[900], paddingVertical: 0 }}
/>
</View>
<TouchableOpacity
className="h-9 w-9 items-center justify-center rounded-full bg-neutral-200"
activeOpacity={0.7}
>
<Ionicons
name="add-outline"
size={22}
color={colors.neutral[800]}
/>
</TouchableOpacity>
</View>
</View>
<StatisticsSection progress={userProgress} />
{/* Scan banner */}
<View
className="mx-5 mb-6 rounded-2xl px-5 pt-5 pb-4 shadow-sm overflow-hidden relative border border-gray-50"
style={{ backgroundColor: colors.primary[100] }}
>
{/* Decorative leaf top-right */}
<View className="absolute -top-1 -right-1 opacity-30">
<Ionicons name="leaf" size={80} color={colors.primary[600]} />
</View>
<Text
className="mb-1 text-[18px] font-bold"
style={{ color: colors.primary[900] }}
>
{t("home.bannerTitle")}
</Text>
<Text
className="mb-5 max-w-[220px] text-[13px] leading-[18px]"
style={{ color: colors.primary[700] }}
>
{t("home.bannerSubtitle")}
</Text>
{/* Scan icon centered */}
<View className="mb-5 items-center">
<Animated.View style={pulseStyle}>
<View
className="h-20 w-20 items-center justify-center rounded-full"
style={{ backgroundColor: colors.primary[200] }}
>
<Ionicons name="scan" size={36} color={colors.primary[800]} />
</View>
</Animated.View>
</View>
{/* Full-width scan button */}
<TouchableOpacity
activeOpacity={0.8}
className="flex-row items-center justify-center gap-2 rounded-full py-3"
style={{ backgroundColor: colors.primary[800] }}
onPress={() => navigation.navigate("Scanner")}
>
<Text className="text-[15px] font-semibold text-white">
{t("home.scanButton")}
</Text>
<MaterialIcons name="arrow-forward-ios" size={16} color="white" />
</TouchableOpacity>
</View>
{/* Last scan section */}
{lastScan && (
<View className="mx-5 mb-6 gap-2">
<SectionHeader
title={t("home.lastScan")}
onViewAll={() => navigation.navigate("History")}
/>
<ScanCard record={lastScan} />
</View>
)}
<View className="h-8" />
</ScrollView>
</SafeAreaView>
);
}

View file

@ -0,0 +1,208 @@
import { View, Text, ScrollView, StyleSheet, TouchableOpacity, Alert } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useTranslation } from 'react-i18next';
import i18n from '@/i18n';
import { Card } from '@/components/ui/Card';
import { XPBar } from '@/components/gamification/XPBar';
import { BadgeCard } from '@/components/gamification/BadgeCard';
import { useGameProgress } from '@/hooks/useGameProgress';
import { useHistory } from '@/hooks/useHistory';
import { colors } from '@/theme/colors';
import { typography } from '@/theme/typography';
import { spacing } from '@/theme/spacing';
export default function ProfileScreen() {
const { t } = useTranslation();
const { progress, resetProgress } = useGameProgress();
const { clearHistory } = useHistory();
const successRate =
progress.totalScans > 0
? Math.round(
(progress.totalScans -
// we don't store not_vine count separately, so approximate
0) /
progress.totalScans *
100
)
: 0;
function handleLanguageToggle() {
const newLang = i18n.language === 'fr' ? 'en' : 'fr';
i18n.changeLanguage(newLang);
}
function handleReset() {
Alert.alert(t('common.confirm'), t('profile.resetConfirm'), [
{ text: t('common.cancel'), style: 'cancel' },
{
text: t('profile.resetData'),
style: 'destructive',
onPress: async () => {
await resetProgress();
await clearHistory();
},
},
]);
}
return (
<SafeAreaView style={styles.safe} edges={['top']}>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Header */}
<View style={styles.header}>
<View style={styles.avatar}>
<Text style={styles.avatarText}>🧑🌾</Text>
</View>
<Text style={styles.username}>Vigneron</Text>
<Text style={styles.xpTotal}>{progress.xp} XP</Text>
</View>
{/* XP Bar */}
<Card style={styles.section} variant="elevated">
<XPBar xp={progress.xp} />
</Card>
{/* Stats */}
<Card style={styles.section} variant="elevated">
<Text style={styles.sectionTitle}>{t('profile.stats')}</Text>
<View style={styles.statsGrid}>
<View style={styles.statItem}>
<Text style={styles.statValue}>{progress.totalScans}</Text>
<Text style={styles.statLabel}>{t('profile.totalScans')}</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statValue}>{progress.uniqueGrapes.length}</Text>
<Text style={styles.statLabel}>{t('profile.uniqueGrapes')}</Text>
</View>
<View style={styles.statItem}>
<Text style={[styles.statValue, { color: colors.warning }]}>{progress.bestStreak}</Text>
<Text style={styles.statLabel}>{t('profile.bestStreak')}</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statValue}>{progress.streak}</Text>
<Text style={styles.statLabel}>{t('home.currentStreak')}</Text>
</View>
</View>
</Card>
{/* Badges */}
<Card style={styles.section} variant="elevated">
<Text style={styles.sectionTitle}>{t('profile.badges')}</Text>
<View style={styles.badgesGrid}>
{progress.badges.map((badge) => (
<BadgeCard key={badge.id} badge={badge} />
))}
</View>
</Card>
{/* Settings */}
<Card style={styles.section} variant="elevated">
<TouchableOpacity style={styles.settingRow} onPress={handleLanguageToggle}>
<Text style={styles.settingLabel}>{t('profile.language')}</Text>
<Text style={styles.settingValue}>
{i18n.language === 'fr' ? '🇫🇷 Français' : '🇬🇧 English'}
</Text>
</TouchableOpacity>
<View style={styles.divider} />
<TouchableOpacity style={styles.settingRow} onPress={handleReset}>
<Text style={[styles.settingLabel, { color: colors.danger }]}>
{t('profile.resetData')}
</Text>
<Text style={styles.settingValue}></Text>
</TouchableOpacity>
</Card>
<View style={{ height: spacing['2xl'] }} />
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, backgroundColor: colors.background },
header: {
alignItems: 'center',
paddingVertical: spacing['2xl'],
gap: spacing.sm,
},
avatar: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: colors.primary[200],
alignItems: 'center',
justifyContent: 'center',
},
avatarText: { fontSize: 40 },
username: {
fontSize: typography.fontSizes.xl,
fontWeight: typography.fontWeights.bold,
color: colors.neutral[900],
},
xpTotal: {
fontSize: typography.fontSizes.sm,
color: colors.primary[700],
fontWeight: typography.fontWeights.semibold,
},
section: {
marginHorizontal: spacing.base,
marginBottom: spacing.md,
gap: spacing.md,
},
sectionTitle: {
fontSize: typography.fontSizes.md,
fontWeight: typography.fontWeights.semibold,
color: colors.neutral[800],
marginBottom: spacing.xs,
},
statsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: spacing.sm,
},
statItem: {
width: '47%',
alignItems: 'center',
backgroundColor: colors.neutral[100],
borderRadius: 12,
paddingVertical: spacing.md,
gap: spacing.xs,
},
statValue: {
fontSize: typography.fontSizes.xl,
fontWeight: typography.fontWeights.bold,
color: colors.primary[800],
},
statLabel: {
fontSize: typography.fontSizes.xs,
color: colors.neutral[600],
textAlign: 'center',
},
badgesGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: spacing.sm,
},
settingRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: spacing.sm,
},
settingLabel: {
fontSize: typography.fontSizes.base,
color: colors.neutral[800],
},
settingValue: {
fontSize: typography.fontSizes.sm,
color: colors.neutral[600],
},
divider: {
height: 1,
backgroundColor: colors.neutral[200],
},
});

View file

@ -0,0 +1,190 @@
import { useEffect } from 'react';
import { View, ScrollView, TouchableOpacity } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation, useRoute } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RouteProp } from '@react-navigation/native';
import { useTranslation } from 'react-i18next';
import { Ionicons } from '@expo/vector-icons';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withDelay,
} from 'react-native-reanimated';
import { ProgressCircle } from '@/components/ui/ProgressCircle';
import { Button } from '@/components/ui/Button';
import { Text } from '@/components/ui/text';
import { Badge } from '@/components/ui/Badge';
import { getCepageById } from '@/utils/cepages';
import { colors } from '@/theme/colors';
import type { RootStackParamList } from '@/types/navigation';
import type { DetectionResult } from '@/types/detection';
type ResultNav = NativeStackNavigationProp<RootStackParamList, 'Result'>;
type ResultRoute = RouteProp<RootStackParamList, 'Result'>;
function getResultColor(result: DetectionResult): string {
if (result === 'vine') return colors.success;
if (result === 'uncertain') return colors.warning;
return colors.danger;
}
function InfoCard({ icon, iconColor, label, value }: {
icon: keyof typeof Ionicons.glyphMap;
iconColor: string;
label: string;
value: string;
}) {
return (
<View className="w-[48%] gap-1 rounded-[14px] bg-white p-[14px] shadow-sm" style={{ elevation: 1 }}>
<View
className="h-8 w-8 items-center justify-center rounded-lg"
style={{ backgroundColor: iconColor + '18' }}
>
<Ionicons name={icon} size={20} color={iconColor} />
</View>
<Text className="text-[11px] text-neutral-500">{label}</Text>
<Text className="text-[15px] font-semibold text-neutral-900">{value}</Text>
</View>
);
}
export default function ResultScreen() {
const { t } = useTranslation();
const navigation = useNavigation<ResultNav>();
const route = useRoute<ResultRoute>();
const { detection } = route.params;
const cepage = detection.cepageId ? getCepageById(detection.cepageId) : undefined;
const resultColor = getResultColor(detection.result);
const headerOpacity = useSharedValue(0);
const headerScale = useSharedValue(0.8);
const cardTranslateY = useSharedValue(30);
const cardOpacity = useSharedValue(0);
useEffect(() => {
headerOpacity.value = withTiming(1, { duration: 400 });
headerScale.value = withTiming(1, { duration: 500 });
cardTranslateY.value = withDelay(250, withTiming(0, { duration: 400 }));
cardOpacity.value = withDelay(250, withTiming(1, { duration: 400 }));
}, []);
const headerStyle = useAnimatedStyle(() => ({
opacity: headerOpacity.value,
transform: [{ scale: headerScale.value }],
}));
const cardStyle = useAnimatedStyle(() => ({
opacity: cardOpacity.value,
transform: [{ translateY: cardTranslateY.value }],
}));
const resultLabel =
detection.result === 'vine'
? t('result.vineDetected')
: detection.result === 'uncertain'
? t('result.uncertain')
: t('result.notVine');
return (
<SafeAreaView className="flex-1 bg-[#FAFAFA]">
<ScrollView showsVerticalScrollIndicator={false} contentContainerClassName="gap-5 p-4 pb-12">
{/* Close button */}
<TouchableOpacity
className="h-8 w-8 items-center justify-center self-end rounded-full bg-neutral-200"
onPress={() => navigation.goBack()}
>
<Ionicons name="close" size={20} color={colors.neutral[700]} />
</TouchableOpacity>
{/* Confidence circle */}
<Animated.View className="items-center gap-3 py-4" style={headerStyle}>
<ProgressCircle
size={100}
strokeWidth={8}
progress={detection.confidence / 100}
color={resultColor}
trackColor={resultColor + '25'}
>
<Text className="text-[20px] font-extrabold" style={{ color: resultColor }}>
{detection.confidence}%
</Text>
</ProgressCircle>
{/* Success message with checkmark */}
<View className="flex-row items-center gap-1.5">
<Ionicons
name={detection.result === 'vine' ? 'checkmark-circle' : detection.result === 'uncertain' ? 'help-circle' : 'close-circle'}
size={20}
color={resultColor}
/>
<Text className="text-[13px] font-medium" style={{ color: resultColor }}>
{resultLabel}
</Text>
</View>
</Animated.View>
{/* Plant name + tags + description + info grid */}
{cepage && detection.result === 'vine' && (
<Animated.View style={cardStyle}>
<Text className="mb-1 text-[24px] font-bold text-neutral-900">{cepage.name.fr}</Text>
{/* Tags */}
<View className="mb-5 flex-row flex-wrap gap-2">
<Badge
label={cepage.color === 'rouge' ? '🍷 Rouge' : cepage.color === 'blanc' ? '🥂 Blanc' : '🌸 Rosé'}
color="neutral"
size="sm"
/>
{cepage.regions.slice(0, 2).map((r) => (
<Badge key={r} label={r} color="neutral" size="sm" />
))}
</View>
{/* Description */}
<View className="mb-4 gap-1">
<Text className="text-[17px] font-semibold text-neutral-900">
{t('result.characteristics')}
</Text>
<Text className="text-[13px] leading-[22px] text-neutral-600">
{cepage.characteristics.fr}
</Text>
</View>
{/* 2x2 info grid */}
<View className="flex-row flex-wrap gap-[10px]">
<InfoCard icon="leaf" iconColor={colors.primary[700]} label={t('result.origin')} value={cepage.origin.fr} />
<InfoCard icon="water" iconColor="#2196F3" label={t('scanner.confidence')} value={`${detection.confidence}%`} />
<InfoCard icon="sunny" iconColor="#FF9800" label={t('result.regions')} value={cepage.regions[0] ?? '—'} />
<InfoCard icon="wine" iconColor="#E91E63" label="Type" value={cepage.color === 'rouge' ? 'Rouge' : cepage.color === 'blanc' ? 'Blanc' : 'Rosé'} />
</View>
</Animated.View>
)}
{/* Action buttons */}
<Animated.View className="mt-2 gap-2" style={cardStyle}>
<Button
variant="default"
size="lg"
className="w-full rounded-[14px]"
onPress={() => navigation.goBack()}
>
<Ionicons name="scan" size={18} color={colors.surface} />
<Text className="text-white">{t('result.scanAgain')}</Text>
</Button>
<Button
variant="ghost"
size="lg"
className="w-full rounded-[14px]"
onPress={() => navigation.goBack()}
>
<Text style={{ color: colors.primary[700] }}>{t('result.viewHistory')}</Text>
</Button>
</Animated.View>
</ScrollView>
</SafeAreaView>
);
}

View file

@ -0,0 +1,177 @@
import { useState } from 'react';
import { View, TouchableOpacity } from 'react-native';
import { CameraView, useCameraPermissions } from 'expo-camera';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSequence,
withTiming,
} from 'react-native-reanimated';
import { useTranslation } from 'react-i18next';
import { CameraOverlay } from '@/components/scanner/CameraOverlay';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/Button';
import { useDetection } from '@/hooks/useDetection';
import { useGameProgress } from '@/hooks/useGameProgress';
import { useHistory } from '@/hooks/useHistory';
import { hapticSuccess, hapticLight } from '@/services/haptics';
import { colors } from '@/theme/colors';
import type { RootStackParamList } from '@/types/navigation';
import type { ScanRecord } from '@/types/detection';
type ScannerNav = NativeStackNavigationProp<RootStackParamList>;
function generateId(): string {
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}
export default function ScannerScreen() {
const { t } = useTranslation();
const navigation = useNavigation<ScannerNav>();
const [permission, requestPermission] = useCameraPermissions();
const { analyze, isAnalyzing } = useDetection();
const { processDetection } = useGameProgress();
const { addScan } = useHistory();
const [liveConfidence, setLiveConfidence] = useState(0);
const shutterScale = useSharedValue(1);
const shutterStyle = useAnimatedStyle(() => ({
transform: [{ scale: shutterScale.value }],
}));
async function handleCapture() {
if (isAnalyzing) return;
await hapticLight();
shutterScale.value = withSequence(
withTiming(0.88, { duration: 100 }),
withTiming(1, { duration: 150 })
);
const interval = setInterval(() => {
setLiveConfidence((prev) => Math.min(prev + Math.floor(Math.random() * 12), 85));
}, 150);
const detection = await analyze();
clearInterval(interval);
if (!detection) return;
setLiveConfidence(detection.confidence);
if (detection.result === 'vine') {
await hapticSuccess();
}
const xpEarned = await processDetection(detection);
const record: ScanRecord = {
id: generateId(),
detection,
xpEarned: typeof xpEarned === 'number' ? xpEarned : 10,
createdAt: new Date().toISOString(),
};
await addScan(record);
navigation.navigate('Result', { detection });
setTimeout(() => setLiveConfidence(0), 500);
}
if (!permission) {
return (
<View className="flex-1 items-center justify-center">
<Text>Chargement...</Text>
</View>
);
}
if (!permission.granted) {
return (
<View className="flex-1 items-center justify-center gap-4 bg-[#FAFAFA] p-8">
<Text className="text-[64px]">📷</Text>
<Text className="text-center text-[20px] font-bold text-neutral-900">
{t('scanner.permissionRequired')}
</Text>
<Text className="mb-4 text-center text-[15px] leading-6 text-neutral-600">
{t('scanner.permissionMessage')}
</Text>
<Button onPress={requestPermission} size="lg" variant="default" className="w-full">
<Text className="text-white">{t('scanner.grantPermission')}</Text>
</Button>
</View>
);
}
return (
<View className="flex-1 bg-neutral-900">
<CameraView className="flex-1" facing="back">
{/* Header overlay */}
<SafeAreaView edges={['top']} className="absolute top-0 left-0 right-0 z-10">
<View className="flex-row items-center justify-between px-5 py-2">
<View className="flex-row items-center gap-2">
<Ionicons name="leaf" size={18} color={colors.surface} />
<Text className="text-[15px] font-semibold text-white">
{t('scanner.identify') ?? 'Identify the plant'}
</Text>
</View>
<TouchableOpacity
className="h-8 w-8 items-center justify-center rounded-full"
style={{ backgroundColor: 'rgba(0,0,0,0.3)' }}
onPress={() => navigation.goBack()}
>
<Ionicons name="close" size={22} color={colors.surface} />
</TouchableOpacity>
</View>
</SafeAreaView>
<CameraOverlay isScanning={isAnalyzing} confidence={liveConfidence} />
{/* Bottom toolbar */}
<View className="absolute bottom-0 left-0 right-0 flex-row items-center justify-between px-8 pb-12 pt-5">
{/* Thumbnail */}
<View
className="h-11 w-11 items-center justify-center rounded-lg"
style={{ backgroundColor: 'rgba(255,255,255,0.15)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.3)' }}
>
<Ionicons name="image-outline" size={20} color="rgba(255,255,255,0.5)" />
</View>
{/* Shutter */}
<Animated.View
className="h-[72px] w-[72px] items-center justify-center rounded-full border-[3px] border-white"
style={shutterStyle}
>
<TouchableOpacity
className="h-[60px] w-[60px] items-center justify-center rounded-full bg-white"
onPress={handleCapture}
disabled={isAnalyzing}
activeOpacity={0.8}
>
{isAnalyzing ? (
<Text className="text-center text-[11px] font-semibold" style={{ color: colors.primary[800] }}>
{t('scanner.analyzing')}
</Text>
) : (
<View className="h-[52px] w-[52px] rounded-full bg-white" />
)}
</TouchableOpacity>
</Animated.View>
{/* Flip camera */}
<TouchableOpacity
className="h-11 w-11 items-center justify-center rounded-full"
style={{ backgroundColor: 'rgba(0,0,0,0.3)' }}
>
<Ionicons name="camera-reverse-outline" size={24} color={colors.surface} />
</TouchableOpacity>
</View>
</CameraView>
</View>
);
}

View file

@ -0,0 +1,51 @@
import { useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { colors } from '@/theme/colors';
import { typography } from '@/theme/typography';
import type { RootStackParamList } from '@/types/navigation';
type SplashNav = NativeStackNavigationProp<RootStackParamList, 'Splash'>;
export default function SplashScreen() {
const navigation = useNavigation<SplashNav>();
useEffect(() => {
const timer = setTimeout(() => {
navigation.replace('Main');
}, 2800);
return () => clearTimeout(timer);
}, []);
return (
<View style={styles.container}>
<Text style={styles.leafEmoji}>🍃</Text>
<Text style={styles.logo}>VinEye</Text>
<Text style={styles.subtitle}>Détection de vignes par IA</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.primary[900],
alignItems: 'center',
justifyContent: 'center',
gap: 24,
},
leafEmoji: { fontSize: 80 },
logo: {
fontSize: typography.fontSizes['4xl'],
fontWeight: typography.fontWeights.extrabold,
color: colors.primary[200],
letterSpacing: -1,
},
subtitle: {
fontSize: typography.fontSizes.base,
color: colors.primary[400],
fontWeight: typography.fontWeights.medium,
},
});

View file

@ -0,0 +1,25 @@
import * as Haptics from 'expo-haptics';
export async function hapticSuccess(): Promise<void> {
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
export async function hapticWarning(): Promise<void> {
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
}
export async function hapticError(): Promise<void> {
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
}
export async function hapticLight(): Promise<void> {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
export async function hapticMedium(): Promise<void> {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
}
export async function hapticHeavy(): Promise<void> {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
}

View file

@ -0,0 +1,43 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
const KEYS = {
GAME_PROGRESS: '@vineye:game_progress',
SCAN_HISTORY: '@vineye:scan_history',
LANGUAGE: '@vineye:language',
} as const;
async function get<T>(key: string): Promise<T | null> {
try {
const raw = await AsyncStorage.getItem(key);
if (raw === null) return null;
return JSON.parse(raw) as T;
} catch {
return null;
}
}
async function set<T>(key: string, value: T): Promise<void> {
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
} catch {
// Storage errors are non-critical — fail silently
}
}
async function remove(key: string): Promise<void> {
try {
await AsyncStorage.removeItem(key);
} catch {
// Ignore removal errors
}
}
async function clearAll(): Promise<void> {
try {
await AsyncStorage.multiRemove(Object.values(KEYS));
} catch {
// Ignore errors
}
}
export const storage = { get, set, remove, clearAll, KEYS };

View file

@ -0,0 +1,9 @@
// Labels for the TFLite vine detection model
// TODO: Remplacer par les vrais labels du modèle TFLite MobileNetV2
export const VINE_LABELS = [
'vine', // 0 — vigne détectée
'uncertain', // 1 — incertain
'not_vine', // 2 — pas une vigne
] as const;
export type VineLabel = (typeof VINE_LABELS)[number];

View file

@ -0,0 +1,52 @@
// TODO: Remplacer par le vrai modèle TFLite (MobileNetV2 fine-tuné sur dataset vignes)
import type { Detection, DetectionResult } from '@/types/detection';
import { cepages } from '@/utils/cepages';
const WEIGHTED_RESULTS: { result: DetectionResult; weight: number }[] = [
{ result: 'vine', weight: 70 },
{ result: 'uncertain', weight: 20 },
{ result: 'not_vine', weight: 10 },
];
function weightedRandom(): DetectionResult {
const total = WEIGHTED_RESULTS.reduce((sum, r) => sum + r.weight, 0);
let rand = Math.random() * total;
for (const r of WEIGHTED_RESULTS) {
rand -= r.weight;
if (rand <= 0) return r.result;
}
return 'vine';
}
// TODO: Remplacer par le vrai modèle TFLite
export async function loadModel(): Promise<boolean> {
// Simule le chargement du modèle (1-2 secondes)
await new Promise((resolve) => setTimeout(resolve, 1200 + Math.random() * 800));
return true;
}
// TODO: Remplacer par le vrai modèle TFLite
export async function runInference(imageUri?: string): Promise<Detection> {
// Simule l'inférence (200-600ms)
await new Promise((resolve) => setTimeout(resolve, 200 + Math.random() * 400));
const result = weightedRandom();
const confidence = result === 'vine'
? Math.floor(70 + Math.random() * 30) // 70100%
: result === 'uncertain'
? Math.floor(40 + Math.random() * 30) // 4070%
: Math.floor(10 + Math.random() * 30); // 1040%
const cepageId =
result === 'vine'
? cepages[Math.floor(Math.random() * cepages.length)].id
: undefined;
return {
result,
confidence,
cepageId,
timestamp: Date.now(),
imageUri,
};
}

View file

@ -0,0 +1,46 @@
export const colors = {
// Primary — Vert Vigne
primary: {
900: '#1B4332',
800: '#2D6A4F',
700: '#40916C',
600: '#52B788',
500: '#74C69D',
400: '#95D5B2',
300: '#B7E4C7',
200: '#D8F3DC',
100: '#E9F5EC',
},
// Accent — Violet Raisin
accent: {
900: '#3E0047',
800: '#6A0572',
700: '#8E24AA',
600: '#AB47BC',
500: '#CE93D8',
400: '#E1BEE7',
300: '#F3E5F5',
200: '#F8EDF9',
100: '#FDF5FF',
},
// Neutrals
neutral: {
900: '#1B1B1B',
800: '#2D2D2D',
700: '#4A4A4A',
600: '#6B6B6B',
500: '#9E9E9E',
400: '#BDBDBD',
300: '#E0E0E0',
200: '#F5F5F5',
100: '#FAFAFA',
},
// Semantic — Résultats de détection
success: '#4CAF50',
warning: '#FFB74D',
danger: '#FF7043',
// Backgrounds
background: '#FAFAFA',
surface: '#FFFFFF',
card: '#FFFFFF',
} as const;

View file

@ -0,0 +1,3 @@
export { colors } from './colors';
export { typography } from './typography';
export { spacing } from './spacing';

View file

@ -0,0 +1,12 @@
export const spacing = {
xs: 4,
sm: 8,
md: 12,
base: 16,
lg: 20,
xl: 24,
'2xl': 32,
'3xl': 40,
'4xl': 48,
'5xl': 64,
} as const;

View file

@ -0,0 +1,25 @@
export const typography = {
fontSizes: {
xs: 11,
sm: 13,
base: 15,
md: 17,
lg: 20,
xl: 24,
'2xl': 28,
'3xl': 34,
'4xl': 40,
},
fontWeights: {
regular: '400' as const,
medium: '500' as const,
semibold: '600' as const,
bold: '700' as const,
extrabold: '800' as const,
},
lineHeights: {
tight: 1.2,
normal: 1.5,
relaxed: 1.75,
},
};

View file

@ -0,0 +1,16 @@
export type DetectionResult = 'vine' | 'uncertain' | 'not_vine';
export interface Detection {
result: DetectionResult;
confidence: number; // 0100
cepageId?: string;
timestamp: number;
imageUri?: string;
}
export interface ScanRecord {
id: string;
detection: Detection;
xpEarned: number;
createdAt: string; // ISO date
}

View file

@ -0,0 +1,46 @@
export type BadgeId =
| 'first_scan'
| 'connoisseur'
| 'on_fire'
| 'sharp_eye'
| 'explorer'
| 'perfectionist'
| 'master';
export interface Badge {
id: BadgeId;
nameKey: string;
descKey: string;
icon: string;
unlocked: boolean;
unlockedAt?: string;
}
export type LevelId =
| 'bud'
| 'leaf'
| 'shoot'
| 'cluster'
| 'harvester'
| 'winemaker'
| 'cellar_master';
export interface Level {
id: LevelId;
labelKey: string;
minXP: number;
maxXP: number;
number: number;
}
export interface GameProgress {
xp: number;
level: number;
badges: Badge[];
streak: number;
lastScanDate: string | null;
totalScans: number;
uniqueGrapes: string[];
bestStreak: number;
highConfidenceScans: number;
}

View file

@ -0,0 +1,14 @@
import type { Detection } from './detection';
export type RootStackParamList = {
Splash: undefined;
Main: undefined;
Result: { detection: Detection };
};
export type BottomTabParamList = {
Home: undefined;
Scanner: undefined;
History: undefined;
Profile: undefined;
};

View file

@ -0,0 +1,112 @@
import type { Badge, BadgeId, Level, LevelId, GameProgress } from '@/types/gamification';
// XP rewards
export const XP_REWARDS = {
SCAN_SUCCESS: 10,
NEW_CEEPAGE: 25,
DAILY_STREAK_BONUS: 5,
HIGH_CONFIDENCE_BONUS: 5, // confidence > 90%
} as const;
// Level definitions
export const LEVELS: Level[] = [
{ id: 'bud', labelKey: 'levels.bud', minXP: 0, maxXP: 50, number: 1 },
{ id: 'leaf', labelKey: 'levels.leaf', minXP: 51, maxXP: 150, number: 2 },
{ id: 'shoot', labelKey: 'levels.shoot', minXP: 151, maxXP: 300, number: 3 },
{ id: 'cluster', labelKey: 'levels.cluster', minXP: 301, maxXP: 500, number: 4 },
{ id: 'harvester', labelKey: 'levels.harvester', minXP: 501, maxXP: 800, number: 5 },
{ id: 'winemaker', labelKey: 'levels.winemaker', minXP: 801, maxXP: 1200, number: 6 },
{ id: 'cellar_master', labelKey: 'levels.cellarMaster', minXP: 1201, maxXP: 9999, number: 7 },
];
export const BADGE_DEFINITIONS: Omit<Badge, 'unlocked' | 'unlockedAt'>[] = [
{ id: 'first_scan', nameKey: 'achievements.firstScan', descKey: 'achievements.firstScanDesc', icon: '🌱' },
{ id: 'connoisseur', nameKey: 'achievements.connoisseur', descKey: 'achievements.connoisseurDesc', icon: '🍇' },
{ id: 'on_fire', nameKey: 'achievements.onFire', descKey: 'achievements.onFireDesc', icon: '🔥' },
{ id: 'sharp_eye', nameKey: 'achievements.sharpEye', descKey: 'achievements.sharpEyeDesc', icon: '🎯' },
{ id: 'explorer', nameKey: 'achievements.explorer', descKey: 'achievements.explorerDesc', icon: '🌍' },
{ id: 'perfectionist', nameKey: 'achievements.perfectionist', descKey: 'achievements.perfectionistDesc', icon: '⭐' },
{ id: 'master', nameKey: 'achievements.master', descKey: 'achievements.masterDesc', icon: '🏆' },
];
export function createInitialBadges(): Badge[] {
return BADGE_DEFINITIONS.map((def) => ({
...def,
unlocked: false,
}));
}
export function getLevelForXP(xp: number): Level {
for (let i = LEVELS.length - 1; i >= 0; i--) {
if (xp >= LEVELS[i].minXP) return LEVELS[i];
}
return LEVELS[0];
}
export function getLevelNumber(xp: number): number {
return getLevelForXP(xp).number;
}
export function getNextLevel(xp: number): Level | null {
const current = getLevelForXP(xp);
const next = LEVELS.find((l) => l.number === current.number + 1);
return next ?? null;
}
export function getXPProgress(xp: number): { current: number; total: number; ratio: number } {
const current = getLevelForXP(xp);
const xpInLevel = xp - current.minXP;
const total = current.maxXP - current.minXP;
return {
current: xpInLevel,
total,
ratio: Math.min(xpInLevel / total, 1),
};
}
export function checkNewBadges(
progress: GameProgress,
newBadges: Badge[]
): { badges: Badge[]; newlyUnlocked: BadgeId[] } {
const newlyUnlocked: BadgeId[] = [];
const updatedBadges = newBadges.map((badge) => {
if (badge.unlocked) return badge;
let shouldUnlock = false;
switch (badge.id) {
case 'first_scan':
shouldUnlock = progress.totalScans >= 1;
break;
case 'connoisseur':
shouldUnlock = progress.uniqueGrapes.length >= 10;
break;
case 'on_fire':
shouldUnlock = progress.streak >= 7;
break;
case 'sharp_eye':
shouldUnlock = progress.highConfidenceScans >= 5;
break;
case 'explorer':
// Simplified: 10+ scans spread across regions (mock check)
shouldUnlock = progress.totalScans >= 10;
break;
case 'perfectionist':
shouldUnlock = progress.totalScans >= 50;
break;
case 'master':
// Check all others unlocked
shouldUnlock = newBadges.filter((b) => b.id !== 'master').every((b) => b.unlocked);
break;
}
if (shouldUnlock) {
newlyUnlocked.push(badge.id);
return { ...badge, unlocked: true, unlockedAt: new Date().toISOString() };
}
return badge;
});
return { badges: updatedBadges, newlyUnlocked };
}

185
VinEye/src/utils/cepages.ts Normal file
View file

@ -0,0 +1,185 @@
export interface Ceepage {
id: string;
name: { fr: string; en: string };
origin: { fr: string; en: string };
color: 'rouge' | 'blanc' | 'rosé';
characteristics: { fr: string; en: string };
regions: string[];
imageUrl?: string;
}
export const cepages: Ceepage[] = [
{
id: 'cabernet_sauvignon',
name: { fr: 'Cabernet Sauvignon', en: 'Cabernet Sauvignon' },
origin: { fr: 'Bordeaux, France', en: 'Bordeaux, France' },
color: 'rouge',
characteristics: {
fr: 'Tanins élevés, arômes de cassis, cèdre et poivron vert. Vieillissement excellent.',
en: 'High tannins, aromas of blackcurrant, cedar and green pepper. Excellent ageing potential.',
},
regions: ['Bordeaux', 'Napa Valley', 'Coonawarra', 'Maipo Valley'],
},
{
id: 'merlot',
name: { fr: 'Merlot', en: 'Merlot' },
origin: { fr: 'Bordeaux, France', en: 'Bordeaux, France' },
color: 'rouge',
characteristics: {
fr: 'Souple et fruité, arômes de prune, chocolat et épices douces. Tanins soyeux.',
en: 'Soft and fruity, aromas of plum, chocolate and soft spices. Silky tannins.',
},
regions: ['Saint-Émilion', 'Pomerol', 'California', 'Washington State'],
},
{
id: 'pinot_noir',
name: { fr: 'Pinot Noir', en: 'Pinot Noir' },
origin: { fr: 'Bourgogne, France', en: 'Burgundy, France' },
color: 'rouge',
characteristics: {
fr: 'Délicat et élégant, arômes de cerise, framboise et sous-bois. Tanins fins.',
en: 'Delicate and elegant, aromas of cherry, raspberry and forest floor. Fine tannins.',
},
regions: ['Bourgogne', 'Oregon', 'Nouvelle-Zélande', 'Alsace'],
},
{
id: 'syrah',
name: { fr: 'Syrah / Shiraz', en: 'Syrah / Shiraz' },
origin: { fr: 'Vallée du Rhône, France', en: 'Rhône Valley, France' },
color: 'rouge',
characteristics: {
fr: 'Puissant et épicé, arômes de mûre, poivre noir et olive. Couleur intense.',
en: 'Powerful and spicy, aromas of blackberry, black pepper and olive. Intense color.',
},
regions: ['Côtes du Rhône', 'Barossa Valley', 'McLaren Vale', 'Colchagua'],
},
{
id: 'chardonnay',
name: { fr: 'Chardonnay', en: 'Chardonnay' },
origin: { fr: 'Bourgogne, France', en: 'Burgundy, France' },
color: 'blanc',
characteristics: {
fr: "Polyvalent, arômes de pomme, vanille et beurre selon l'élevage. Très expressif.",
en: 'Versatile, aromas of apple, vanilla and butter depending on ageing. Very expressive.',
},
regions: ['Chablis', 'Meursault', 'Napa Valley', 'Mâconnais'],
},
{
id: 'sauvignon_blanc',
name: { fr: 'Sauvignon Blanc', en: 'Sauvignon Blanc' },
origin: { fr: 'Loire, France', en: 'Loire Valley, France' },
color: 'blanc',
characteristics: {
fr: 'Vif et aromatique, arômes de citron vert, groseille et herbe coupée. Acidité marquée.',
en: 'Crisp and aromatic, aromas of lime, gooseberry and cut grass. Marked acidity.',
},
regions: ['Sancerre', 'Pouilly-Fumé', 'Marlborough', 'Bordeaux'],
},
{
id: 'riesling',
name: { fr: 'Riesling', en: 'Riesling' },
origin: { fr: 'Alsace / Rhénanie, France-Allemagne', en: 'Alsace / Rhine, France-Germany' },
color: 'blanc',
characteristics: {
fr: 'Aromatique et minéral, arômes de pêche, citron et pétrole. Acidité élevée, vieillissement exceptionnel.',
en: 'Aromatic and mineral, aromas of peach, citrus and petrol. High acidity, exceptional ageing.',
},
regions: ['Alsace', 'Mosel', 'Clare Valley', 'Rheingau'],
},
{
id: 'grenache',
name: { fr: 'Grenache', en: 'Grenache' },
origin: { fr: 'Aragon, Espagne', en: 'Aragon, Spain' },
color: 'rouge',
characteristics: {
fr: 'Chaleureux et fruité, arômes de fraise, cerise et garrigue. Fort en alcool.',
en: 'Warm and fruity, aromas of strawberry, cherry and scrubland. High alcohol.',
},
regions: ['Châteauneuf-du-Pape', 'Priorat', 'Gigondas', 'Barossa Valley'],
},
{
id: 'malbec',
name: { fr: 'Malbec', en: 'Malbec' },
origin: { fr: 'Sud-Ouest, France', en: 'South-West France' },
color: 'rouge',
characteristics: {
fr: 'Intense et velouté, arômes de prune, violette et cacao. Tanins doux et ronds.',
en: 'Intense and velvety, aromas of plum, violet and cocoa. Soft, rounded tannins.',
},
regions: ['Mendoza', 'Cahors', 'Salta', 'Patagonie'],
},
{
id: 'tempranillo',
name: { fr: 'Tempranillo', en: 'Tempranillo' },
origin: { fr: 'Rioja, Espagne', en: 'Rioja, Spain' },
color: 'rouge',
characteristics: {
fr: 'Fruité et terreux, arômes de cerise, cuir et tabac. Vieillissement en barrique apprécié.',
en: 'Fruity and earthy, aromas of cherry, leather and tobacco. Barrel ageing appreciated.',
},
regions: ['Rioja', 'Ribera del Duero', 'Toro', 'Penedès'],
},
{
id: 'sangiovese',
name: { fr: 'Sangiovese', en: 'Sangiovese' },
origin: { fr: 'Toscane, Italie', en: 'Tuscany, Italy' },
color: 'rouge',
characteristics: {
fr: 'Acide et tannique, arômes de cerise acidulée, herbes et terre. Grande acidité.',
en: 'Acidic and tannic, aromas of tart cherry, herbs and earth. High acidity.',
},
regions: ['Chianti', 'Brunello di Montalcino', 'Montepulciano', 'Morellino'],
},
{
id: 'nebbiolo',
name: { fr: 'Nebbiolo', en: 'Nebbiolo' },
origin: { fr: 'Piémont, Italie', en: 'Piedmont, Italy' },
color: 'rouge',
characteristics: {
fr: 'Très tannique et austère jeune, arômes de rose, goudron et cerise séchée. Vieillissement impératif.',
en: 'Very tannic and austere when young, aromas of rose, tar and dried cherry. Ageing essential.',
},
regions: ['Barolo', 'Barbaresco', 'Gattinara', 'Carema'],
},
{
id: 'gamay',
name: { fr: 'Gamay', en: 'Gamay' },
origin: { fr: 'Beaujolais, France', en: 'Beaujolais, France' },
color: 'rouge',
characteristics: {
fr: 'Léger et fruité, arômes de framboise, banane et bonbon. Peu tannique, à boire jeune.',
en: 'Light and fruity, aromas of raspberry, banana and candy. Low tannins, best drunk young.',
},
regions: ['Beaujolais', 'Moulin-à-Vent', 'Morgon', 'Touraine'],
},
{
id: 'chenin_blanc',
name: { fr: 'Chenin Blanc', en: 'Chenin Blanc' },
origin: { fr: 'Loire, France', en: 'Loire Valley, France' },
color: 'blanc',
characteristics: {
fr: 'Polyvalent, arômes de coing, miel et fleurs blanches. Acidité élevée. Sec à liquoreux.',
en: 'Versatile, aromas of quince, honey and white flowers. High acidity. Dry to sweet.',
},
regions: ['Vouvray', 'Anjou', 'Savennières', 'Stellenbosch'],
},
{
id: 'semillon',
name: { fr: 'Sémillon', en: 'Sémillon' },
origin: { fr: 'Bordeaux, France', en: 'Bordeaux, France' },
color: 'blanc',
characteristics: {
fr: "Riche et onctueux, arômes de citron, cire d'abeille et miel. Vieillissement remarquable.",
en: 'Rich and unctuous, aromas of lemon, beeswax and honey. Remarkable ageing.',
},
regions: ['Sauternes', 'Barsac', 'Hunter Valley', 'White Bordeaux'],
},
];
export function getCepageById(id: string): Ceepage | undefined {
return cepages.find((c) => c.id === id);
}
export function getRandomCeepage(): Ceepage {
return cepages[Math.floor(Math.random() * cepages.length)];
}

54
VinEye/tailwind.config.js Normal file
View file

@ -0,0 +1,54 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./App.{js,jsx,ts,tsx}',
'./src/**/*.{js,jsx,ts,tsx}',
'./components/**/*.{js,jsx,ts,tsx}',
],
presets: [require('nativewind/preset')],
theme: {
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
},
},
plugins: [require('tailwindcss-animate')],
};

11
VinEye/tsconfig.json Normal file
View file

@ -0,0 +1,11 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["**/*.ts", "**/*.tsx", "nativewind-env.d.ts"]
}

View file

@ -1,101 +0,0 @@
epoch,accuracy,val_accuracy,loss,val_loss
1,0.7641927003860474,0.15000000596046448,0.6556282639503479,2.786958932876587
2,0.875781238079071,0.24583333730697632,0.3367018401622772,145.1642608642578
3,0.9261718988418579,0.24583333730697632,0.21222706139087677,542.9489135742188
4,0.9468749761581421,0.3499999940395355,0.15502126514911652,436.6821594238281
5,0.95703125,0.30000001192092896,0.12713894248008728,1734.4005126953125
6,0.9653645753860474,0.22083333134651184,0.10625138133764267,2078.35595703125
7,0.9670572876930237,0.22083333134651184,0.10060538351535797,4190.84716796875
8,0.9708333611488342,0.22083333134651184,0.08701884001493454,2175.69384765625
9,0.9759114384651184,0.22083333134651184,0.07224109768867493,1431.79736328125
10,0.9756510257720947,0.22499999403953552,0.0758061408996582,1257.38818359375
11,0.9799479246139526,0.3291666805744171,0.06562892347574234,679.346923828125
12,0.9819010496139526,0.22083333134651184,0.05864816904067993,1250.117431640625
13,0.9837239384651184,0.22083333134651184,0.05603867396712303,949.5216064453125
14,0.9856770634651184,0.22083333134651184,0.04392676800489426,2813.852783203125
15,0.9815104007720947,0.22083333134651184,0.05770495906472206,992.2079467773438
16,0.983593761920929,0.22083333134651184,0.04698636755347252,2555.2177734375
17,0.983203113079071,0.3166666626930237,0.05349167808890343,721.4380493164062
18,0.9864583611488342,0.22499999403953552,0.046624429523944855,1216.3863525390625
19,0.9897135496139526,0.22083333134651184,0.033429499715566635,2209.611572265625
20,0.9885416626930237,0.22083333134651184,0.037510477006435394,1644.256591796875
21,0.9889323115348816,0.2874999940395355,0.032061509788036346,599.4248657226562
22,0.9888020753860474,0.22083333134651184,0.03519482910633087,2405.77587890625
23,0.9846354126930237,0.4833333194255829,0.048758625984191895,374.9194030761719
24,0.9895833134651184,0.23333333432674408,0.031843990087509155,946.0235595703125
25,0.9877604246139526,0.24166665971279144,0.0385715514421463,1195.2344970703125
26,0.9901041388511658,0.22083333134651184,0.032194558531045914,806.0076904296875
27,0.9880208373069763,0.24583333730697632,0.03730996325612068,688.756103515625
28,0.9912760257720947,0.22083333134651184,0.03252696618437767,1622.8123779296875
29,0.9947916865348816,0.22083333134651184,0.01784966140985489,1913.7918701171875
30,0.9924479126930237,0.22083333134651184,0.026797903701663017,279.2188720703125
31,0.9907552003860474,0.22083333134651184,0.033388420939445496,1134.2767333984375
32,0.9908854365348816,0.3375000059604645,0.029145648702979088,95.03201293945312
33,0.9891927242279053,0.42500001192092896,0.03102271445095539,464.6019592285156
34,0.9932291507720947,0.22083333134651184,0.021412037312984467,986.3841552734375
35,0.9923177361488342,0.22083333134651184,0.02759469673037529,760.4578857421875
36,0.991406261920929,0.24583333730697632,0.028778191655874252,593.8187255859375
37,0.9945312738418579,0.32083332538604736,0.018624553456902504,663.8523559570312
38,0.9908854365348816,0.22499999403953552,0.02799573726952076,767.1515502929688
39,0.9962239861488342,0.4375,0.013362539932131767,313.8023986816406
40,0.9946614503860474,0.22083333134651184,0.01853085868060589,1301.3148193359375
41,0.986328125,0.32083332538604736,0.04215172678232193,640.0283813476562
42,0.9925781488418579,0.23333333432674408,0.02235046960413456,218.9736328125
43,0.9944010376930237,0.4208333194255829,0.017718089744448662,202.29286193847656
44,0.9915364384651184,0.22499999403953552,0.025970684364438057,1598.172119140625
45,0.99609375,0.44999998807907104,0.014352566562592983,487.48809814453125
46,0.9934895634651184,0.30000001192092896,0.01648867316544056,996.599365234375
47,0.9924479126930237,0.24583333730697632,0.02568558044731617,1811.96630859375
48,0.9936197996139526,0.22083333134651184,0.019128015264868736,896.1229858398438
49,0.9970052242279053,0.24583333730697632,0.009085672907531261,1030.3626708984375
50,0.9944010376930237,0.24583333730697632,0.017151959240436554,1934.4542236328125
51,0.9916666746139526,0.24583333730697632,0.028664644807577133,983.9193725585938
52,0.9962239861488342,0.3791666626930237,0.01063383650034666,484.26690673828125
53,0.995312511920929,0.42500001192092896,0.016774259507656097,1236.3868408203125
54,0.9947916865348816,0.4333333373069763,0.015777425840497017,207.80308532714844
55,0.9962239861488342,0.3333333432674408,0.012974864803254604,494.0450439453125
56,0.9957031011581421,0.32083332538604736,0.012591647915542126,430.7264709472656
57,0.9966145753860474,0.5083333253860474,0.012390038929879665,194.0229034423828
58,0.9868489503860474,0.2291666716337204,0.05284683033823967,711.9586181640625
59,0.996874988079071,0.3291666805744171,0.008380413986742496,296.1285705566406
60,0.9959635138511658,0.3541666567325592,0.013547107577323914,247.80809020996094
61,0.997265636920929,0.2541666626930237,0.008858543820679188,664.0957641601562
62,0.9977864623069763,0.4541666805744171,0.007046550512313843,381.4072265625
63,0.9976562261581421,0.24166665971279144,0.009287163615226746,667.2242431640625
64,0.9951822757720947,0.4166666567325592,0.015782717615365982,180.54684448242188
65,0.998046875,0.24583333730697632,0.005884839221835136,1132.2967529296875
66,0.996874988079071,0.24583333730697632,0.008365819230675697,423.9919128417969
67,0.9944010376930237,0.4375,0.0157401654869318,591.289794921875
68,0.996874988079071,0.42500001192092896,0.009277629666030407,368.8995666503906
69,0.996874988079071,0.25833332538604736,0.009817498736083508,268.4747619628906
70,0.9981771111488342,0.32083332538604736,0.006176070775836706,848.0325927734375
71,0.9954426884651184,0.32083332538604736,0.017365239560604095,921.9395751953125
72,0.9976562261581421,0.19166666269302368,0.006243720185011625,846.9988403320312
73,0.9979166388511658,0.32083332538604736,0.007621175609529018,770.1184692382812
74,0.998046875,0.3333333432674408,0.007831843569874763,1054.477783203125
75,0.9934895634651184,0.32499998807907104,0.02367531694471836,1973.513671875
76,0.994140625,0.32083332538604736,0.02045782469213009,1825.21875
77,0.9985677003860474,0.1458333283662796,0.006224052980542183,2563.483154296875
78,0.9990885257720947,0.2916666567325592,0.0029171621426939964,1612.9859619140625
79,0.9996093511581421,0.3083333373069763,0.0023937856312841177,1210.00048828125
80,0.9973958134651184,0.32083332538604736,0.00871509313583374,2131.439453125
81,0.996874988079071,0.32083332538604736,0.009170063771307468,1381.1668701171875
82,0.9984375238418579,0.3499999940395355,0.006568868178874254,1278.370361328125
83,0.9977864623069763,0.32083332538604736,0.006044364999979734,1442.549072265625
84,0.9977864623069763,0.36666667461395264,0.006215202622115612,1152.5875244140625
85,0.9954426884651184,0.3541666567325592,0.015190036036074162,1440.297607421875
86,0.9959635138511658,0.32083332538604736,0.013142507523298264,1981.55908203125
87,0.9973958134651184,0.32499998807907104,0.008220557123422623,753.7999267578125
88,0.998046875,0.3708333373069763,0.007407734636217356,1276.0592041015625
89,0.996874988079071,0.32083332538604736,0.011465544812381268,2005.687255859375
90,0.9979166388511658,0.3125,0.009250237606465816,1785.6741943359375
91,0.9977864623069763,0.3583333194255829,0.0066549344919621944,2299.27294921875
92,0.998046875,0.3291666805744171,0.007205411791801453,1235.2969970703125
93,0.9946614503860474,0.32083332538604736,0.017886042594909668,1514.164306640625
94,0.9993489384651184,0.3958333432674408,0.0025855659041553736,1014.2952880859375
95,0.9989583492279053,0.3375000059604645,0.004219961352646351,860.9890747070312
96,0.9975260496139526,0.375,0.009455768391489983,1195.864990234375
97,0.9984375238418579,0.32083332538604736,0.005296614952385426,1493.249267578125
98,0.9958333373069763,0.3708333373069763,0.01711571030318737,1151.3814697265625
99,0.9979166388511658,0.32083332538604736,0.007663541007786989,1383.2186279296875
100,0.9990885257720947,0.32083332538604736,0.0027740655932575464,1476.4110107421875
1 epoch accuracy val_accuracy loss val_loss
2 1 0.7641927003860474 0.15000000596046448 0.6556282639503479 2.786958932876587
3 2 0.875781238079071 0.24583333730697632 0.3367018401622772 145.1642608642578
4 3 0.9261718988418579 0.24583333730697632 0.21222706139087677 542.9489135742188
5 4 0.9468749761581421 0.3499999940395355 0.15502126514911652 436.6821594238281
6 5 0.95703125 0.30000001192092896 0.12713894248008728 1734.4005126953125
7 6 0.9653645753860474 0.22083333134651184 0.10625138133764267 2078.35595703125
8 7 0.9670572876930237 0.22083333134651184 0.10060538351535797 4190.84716796875
9 8 0.9708333611488342 0.22083333134651184 0.08701884001493454 2175.69384765625
10 9 0.9759114384651184 0.22083333134651184 0.07224109768867493 1431.79736328125
11 10 0.9756510257720947 0.22499999403953552 0.0758061408996582 1257.38818359375
12 11 0.9799479246139526 0.3291666805744171 0.06562892347574234 679.346923828125
13 12 0.9819010496139526 0.22083333134651184 0.05864816904067993 1250.117431640625
14 13 0.9837239384651184 0.22083333134651184 0.05603867396712303 949.5216064453125
15 14 0.9856770634651184 0.22083333134651184 0.04392676800489426 2813.852783203125
16 15 0.9815104007720947 0.22083333134651184 0.05770495906472206 992.2079467773438
17 16 0.983593761920929 0.22083333134651184 0.04698636755347252 2555.2177734375
18 17 0.983203113079071 0.3166666626930237 0.05349167808890343 721.4380493164062
19 18 0.9864583611488342 0.22499999403953552 0.046624429523944855 1216.3863525390625
20 19 0.9897135496139526 0.22083333134651184 0.033429499715566635 2209.611572265625
21 20 0.9885416626930237 0.22083333134651184 0.037510477006435394 1644.256591796875
22 21 0.9889323115348816 0.2874999940395355 0.032061509788036346 599.4248657226562
23 22 0.9888020753860474 0.22083333134651184 0.03519482910633087 2405.77587890625
24 23 0.9846354126930237 0.4833333194255829 0.048758625984191895 374.9194030761719
25 24 0.9895833134651184 0.23333333432674408 0.031843990087509155 946.0235595703125
26 25 0.9877604246139526 0.24166665971279144 0.0385715514421463 1195.2344970703125
27 26 0.9901041388511658 0.22083333134651184 0.032194558531045914 806.0076904296875
28 27 0.9880208373069763 0.24583333730697632 0.03730996325612068 688.756103515625
29 28 0.9912760257720947 0.22083333134651184 0.03252696618437767 1622.8123779296875
30 29 0.9947916865348816 0.22083333134651184 0.01784966140985489 1913.7918701171875
31 30 0.9924479126930237 0.22083333134651184 0.026797903701663017 279.2188720703125
32 31 0.9907552003860474 0.22083333134651184 0.033388420939445496 1134.2767333984375
33 32 0.9908854365348816 0.3375000059604645 0.029145648702979088 95.03201293945312
34 33 0.9891927242279053 0.42500001192092896 0.03102271445095539 464.6019592285156
35 34 0.9932291507720947 0.22083333134651184 0.021412037312984467 986.3841552734375
36 35 0.9923177361488342 0.22083333134651184 0.02759469673037529 760.4578857421875
37 36 0.991406261920929 0.24583333730697632 0.028778191655874252 593.8187255859375
38 37 0.9945312738418579 0.32083332538604736 0.018624553456902504 663.8523559570312
39 38 0.9908854365348816 0.22499999403953552 0.02799573726952076 767.1515502929688
40 39 0.9962239861488342 0.4375 0.013362539932131767 313.8023986816406
41 40 0.9946614503860474 0.22083333134651184 0.01853085868060589 1301.3148193359375
42 41 0.986328125 0.32083332538604736 0.04215172678232193 640.0283813476562
43 42 0.9925781488418579 0.23333333432674408 0.02235046960413456 218.9736328125
44 43 0.9944010376930237 0.4208333194255829 0.017718089744448662 202.29286193847656
45 44 0.9915364384651184 0.22499999403953552 0.025970684364438057 1598.172119140625
46 45 0.99609375 0.44999998807907104 0.014352566562592983 487.48809814453125
47 46 0.9934895634651184 0.30000001192092896 0.01648867316544056 996.599365234375
48 47 0.9924479126930237 0.24583333730697632 0.02568558044731617 1811.96630859375
49 48 0.9936197996139526 0.22083333134651184 0.019128015264868736 896.1229858398438
50 49 0.9970052242279053 0.24583333730697632 0.009085672907531261 1030.3626708984375
51 50 0.9944010376930237 0.24583333730697632 0.017151959240436554 1934.4542236328125
52 51 0.9916666746139526 0.24583333730697632 0.028664644807577133 983.9193725585938
53 52 0.9962239861488342 0.3791666626930237 0.01063383650034666 484.26690673828125
54 53 0.995312511920929 0.42500001192092896 0.016774259507656097 1236.3868408203125
55 54 0.9947916865348816 0.4333333373069763 0.015777425840497017 207.80308532714844
56 55 0.9962239861488342 0.3333333432674408 0.012974864803254604 494.0450439453125
57 56 0.9957031011581421 0.32083332538604736 0.012591647915542126 430.7264709472656
58 57 0.9966145753860474 0.5083333253860474 0.012390038929879665 194.0229034423828
59 58 0.9868489503860474 0.2291666716337204 0.05284683033823967 711.9586181640625
60 59 0.996874988079071 0.3291666805744171 0.008380413986742496 296.1285705566406
61 60 0.9959635138511658 0.3541666567325592 0.013547107577323914 247.80809020996094
62 61 0.997265636920929 0.2541666626930237 0.008858543820679188 664.0957641601562
63 62 0.9977864623069763 0.4541666805744171 0.007046550512313843 381.4072265625
64 63 0.9976562261581421 0.24166665971279144 0.009287163615226746 667.2242431640625
65 64 0.9951822757720947 0.4166666567325592 0.015782717615365982 180.54684448242188
66 65 0.998046875 0.24583333730697632 0.005884839221835136 1132.2967529296875
67 66 0.996874988079071 0.24583333730697632 0.008365819230675697 423.9919128417969
68 67 0.9944010376930237 0.4375 0.0157401654869318 591.289794921875
69 68 0.996874988079071 0.42500001192092896 0.009277629666030407 368.8995666503906
70 69 0.996874988079071 0.25833332538604736 0.009817498736083508 268.4747619628906
71 70 0.9981771111488342 0.32083332538604736 0.006176070775836706 848.0325927734375
72 71 0.9954426884651184 0.32083332538604736 0.017365239560604095 921.9395751953125
73 72 0.9976562261581421 0.19166666269302368 0.006243720185011625 846.9988403320312
74 73 0.9979166388511658 0.32083332538604736 0.007621175609529018 770.1184692382812
75 74 0.998046875 0.3333333432674408 0.007831843569874763 1054.477783203125
76 75 0.9934895634651184 0.32499998807907104 0.02367531694471836 1973.513671875
77 76 0.994140625 0.32083332538604736 0.02045782469213009 1825.21875
78 77 0.9985677003860474 0.1458333283662796 0.006224052980542183 2563.483154296875
79 78 0.9990885257720947 0.2916666567325592 0.0029171621426939964 1612.9859619140625
80 79 0.9996093511581421 0.3083333373069763 0.0023937856312841177 1210.00048828125
81 80 0.9973958134651184 0.32083332538604736 0.00871509313583374 2131.439453125
82 81 0.996874988079071 0.32083332538604736 0.009170063771307468 1381.1668701171875
83 82 0.9984375238418579 0.3499999940395355 0.006568868178874254 1278.370361328125
84 83 0.9977864623069763 0.32083332538604736 0.006044364999979734 1442.549072265625
85 84 0.9977864623069763 0.36666667461395264 0.006215202622115612 1152.5875244140625
86 85 0.9954426884651184 0.3541666567325592 0.015190036036074162 1440.297607421875
87 86 0.9959635138511658 0.32083332538604736 0.013142507523298264 1981.55908203125
88 87 0.9973958134651184 0.32499998807907104 0.008220557123422623 753.7999267578125
89 88 0.998046875 0.3708333373069763 0.007407734636217356 1276.0592041015625
90 89 0.996874988079071 0.32083332538604736 0.011465544812381268 2005.687255859375
91 90 0.9979166388511658 0.3125 0.009250237606465816 1785.6741943359375
92 91 0.9977864623069763 0.3583333194255829 0.0066549344919621944 2299.27294921875
93 92 0.998046875 0.3291666805744171 0.007205411791801453 1235.2969970703125
94 93 0.9946614503860474 0.32083332538604736 0.017886042594909668 1514.164306640625
95 94 0.9993489384651184 0.3958333432674408 0.0025855659041553736 1014.2952880859375
96 95 0.9989583492279053 0.3375000059604645 0.004219961352646351 860.9890747070312
97 96 0.9975260496139526 0.375 0.009455768391489983 1195.864990234375
98 97 0.9984375238418579 0.32083332538604736 0.005296614952385426 1493.249267578125
99 98 0.9958333373069763 0.3708333373069763 0.01711571030318737 1151.3814697265625
100 99 0.9979166388511658 0.32083332538604736 0.007663541007786989 1383.2186279296875
101 100 0.9990885257720947 0.32083332538604736 0.0027740655932575464 1476.4110107421875

View file

@ -1,101 +0,0 @@
epoch,accuracy,val_accuracy,loss,val_loss
1,0.7516441941261292,0.1502770036458969,0.6869990229606628,9.279614448547363
2,0.8496019244194031,0.27770084142684937,0.41523098945617676,111.58092498779297
3,0.8849082589149475,0.23268698155879974,0.30662405490875244,1214.7138671875
4,0.906022846698761,0.23268698155879974,0.25916892290115356,2999.732666015625
5,0.9245413541793823,0.23268698155879974,0.2080700546503067,5503.68408203125
6,0.9383869767189026,0.23268698155879974,0.1752740442752838,4690.720703125
7,0.9484250545501709,0.23268698155879974,0.1510019749403,5465.9267578125
8,0.9586362242698669,0.23268698155879974,0.12660910189151764,1849.1502685546875
9,0.961405336856842,0.23268698155879974,0.11609478294849396,3567.95849609375
10,0.9634822010993958,0.23268698155879974,0.11276258528232574,4574.11767578125
11,0.9655590057373047,0.23268698155879974,0.10540036112070084,3753.94873046875
12,0.9697126746177673,0.23268698155879974,0.0942334458231926,3380.722900390625
13,0.9714434146881104,0.23268698155879974,0.08314096182584763,6275.935546875
14,0.9762893915176392,0.23268698155879974,0.0717514380812645,2442.714599609375
15,0.9733471870422363,0.23268698155879974,0.08071456849575043,3416.07861328125
16,0.9787123799324036,0.23268698155879974,0.07309815287590027,3656.95751953125
17,0.9771547317504883,0.23268698155879974,0.07524916529655457,4760.67138671875
18,0.9790585041046143,0.23268698155879974,0.06330344825983047,2295.76513671875
19,0.9790585041046143,0.23268698155879974,0.06550607830286026,5322.1533203125
20,0.9783661961555481,0.23268698155879974,0.06409229338169098,2194.40283203125
21,0.9795777201652527,0.23268698155879974,0.05746037885546684,1612.3697509765625
22,0.9806161522865295,0.23268698155879974,0.060518983751535416,4674.85400390625
23,0.9833852648735046,0.27770084142684937,0.05250076577067375,1632.55615234375
24,0.987712025642395,0.27770084142684937,0.04003383219242096,1444.8175048828125
25,0.9828660488128662,0.27770084142684937,0.05212881788611412,3077.065185546875
26,0.9816545248031616,0.23268698155879974,0.05729270353913307,987.83740234375
27,0.9845967292785645,0.27770084142684937,0.05151180922985077,1980.85009765625
28,0.9870197176933289,0.2957063615322113,0.045257747173309326,1160.206298828125
29,0.9804430603981018,0.24376730620861053,0.06315220147371292,657.8983764648438
30,0.9890965819358826,0.4058171808719635,0.03781181946396828,797.4231567382812
31,0.9844236969947815,0.23268698155879974,0.05010179430246353,1343.955810546875
32,0.9851159453392029,0.27770084142684937,0.04631909728050232,1621.481689453125
33,0.9868466854095459,0.27770084142684937,0.04147016257047653,1539.1033935546875
34,0.987885057926178,0.3150969445705414,0.03842846676707268,557.3466796875
35,0.9903080463409424,0.23268698155879974,0.029467027634382248,2310.66357421875
36,0.9863274693489075,0.23268698155879974,0.044710543006658554,1721.2637939453125
37,0.9847698211669922,0.2583102583885193,0.04918225109577179,1349.9884033203125
38,0.9906542301177979,0.3067867159843445,0.03000747598707676,957.5072631835938
39,0.9923849105834961,0.29916897416114807,0.02561911940574646,1543.9759521484375
40,0.9865005016326904,0.23268698155879974,0.03470372408628464,2082.9208984375
41,0.9892696142196655,0.27770084142684937,0.03406791016459465,1992.923095703125
42,0.9937694668769836,0.23753462731838226,0.020795246586203575,1350.656982421875
43,0.9865005016326904,0.25692519545555115,0.050108153373003006,1347.6395263671875
44,0.9901350140571594,0.23268698155879974,0.028937410563230515,2191.127197265625
45,0.9941155910491943,0.23268698155879974,0.02078220620751381,1551.0986328125
46,0.9904811382293701,0.23268698155879974,0.028728974983096123,2349.158447265625
47,0.9903080463409424,0.23268698155879974,0.033932607620954514,1677.892822265625
48,0.9868466854095459,0.27770084142684937,0.03687359392642975,1879.01904296875
49,0.9942886829376221,0.27770084142684937,0.021784603595733643,1672.6260986328125
50,0.9911734461784363,0.23268698155879974,0.026831354945898056,2027.2027587890625
51,0.9894427061080933,0.27770084142684937,0.0279900673776865,1085.9053955078125
52,0.989615797996521,0.23337949812412262,0.03173811733722687,1212.1956787109375
53,0.993423342704773,0.23268698155879974,0.022051407024264336,1802.6983642578125
54,0.9955001473426819,0.23268698155879974,0.01723671332001686,1909.1605224609375
55,0.9904811382293701,0.23268698155879974,0.03664610907435417,1560.034912109375
56,0.9918656945228577,0.27770084142684937,0.029300954192876816,983.4076538085938
57,0.9948078989982605,0.2576177418231964,0.01762007363140583,908.1100463867188
58,0.9956732392311096,0.2340720295906067,0.013033601455390453,1055.4168701171875
59,0.993423342704773,0.23268698155879974,0.022611256688833237,2751.419921875
60,0.9892696142196655,0.27839335799217224,0.03379980847239494,1141.1070556640625
61,0.9944617748260498,0.41828253865242004,0.01806570030748844,992.7396850585938
62,0.9951540231704712,0.39058172702789307,0.015332863666117191,964.7886352539062
63,0.9958463311195374,0.4861495792865753,0.013660548254847527,1077.28125
64,0.9963655471801758,0.2340720295906067,0.010616755113005638,1765.6953125
65,0.9939425587654114,0.27770084142684937,0.019288523122668266,3227.864501953125
66,0.9913464784622192,0.23268698155879974,0.030606787651777267,3323.407958984375
67,0.9929041266441345,0.23268698155879974,0.022766467183828354,3642.7861328125
68,0.9955001473426819,0.23268698155879974,0.019210971891880035,1868.906982421875
69,0.996192455291748,0.23268698155879974,0.011477937921881676,2611.31640625
70,0.9949809908866882,0.23268698155879974,0.018107961863279343,3013.491943359375
71,0.9951540231704712,0.23268698155879974,0.015328730456531048,3909.685302734375
72,0.9927310347557068,0.23337949812412262,0.023373156785964966,2733.7919921875
73,0.9956732392311096,0.23268698155879974,0.013462813571095467,2812.021728515625
74,0.9955001473426819,0.23268698155879974,0.01717122085392475,1892.8349609375
75,0.9975770115852356,0.23268698155879974,0.008702440187335014,4670.3193359375
76,0.9972308874130249,0.23268698155879974,0.007852787151932716,4487.03515625
77,0.9965385794639587,0.23268698155879974,0.009604893624782562,6443.4267578125
78,0.9920387864112854,0.23268698155879974,0.020789368078112602,6024.58740234375
79,0.9935963749885559,0.23268698155879974,0.018593357875943184,2821.33935546875
80,0.9956732392311096,0.23268698155879974,0.015648460015654564,2576.421142578125
81,0.9949809908866882,0.23268698155879974,0.017194343730807304,3764.894775390625
82,0.9929041266441345,0.27770084142684937,0.021781075745821,2807.73291015625
83,0.9968847632408142,0.23268698155879974,0.012610912322998047,4471.623046875
84,0.9967116713523865,0.23268698155879974,0.01276914868503809,6788.912109375
85,0.9958463311195374,0.23268698155879974,0.0117557467892766,7592.3515625
86,0.9956732392311096,0.23268698155879974,0.010352320037782192,6271.54931640625
87,0.996192455291748,0.23268698155879974,0.009747961536049843,3658.080322265625
88,0.9974039196968079,0.23268698155879974,0.007929908111691475,8354.2626953125
89,0.9944617748260498,0.23268698155879974,0.01685083471238613,5968.7373046875
90,0.9968847632408142,0.23268698155879974,0.011445037089288235,3262.92578125
91,0.9949809908866882,0.23268698155879974,0.016830891370773315,4658.84912109375
92,0.9987885355949402,0.23268698155879974,0.006762914825230837,8752.833984375
93,0.9970577955245972,0.23268698155879974,0.01124420017004013,7593.404296875
94,0.9979231357574463,0.23268698155879974,0.009695205837488174,1501.1002197265625
95,0.9970577955245972,0.23268698155879974,0.010869201272726059,4020.974609375
96,0.9942886829376221,0.23268698155879974,0.01823830045759678,2655.122802734375
97,0.9982693195343018,0.23545706272125244,0.006866878364235163,886.0972900390625
98,0.9852890372276306,0.2527700960636139,0.06450172513723373,2126.42578125
99,0.9955001473426819,0.42659279704093933,0.015554066747426987,1247.246826171875
100,0.9975770115852356,0.33240997791290283,0.008071373216807842,1635.5611572265625
1 epoch accuracy val_accuracy loss val_loss
2 1 0.7516441941261292 0.1502770036458969 0.6869990229606628 9.279614448547363
3 2 0.8496019244194031 0.27770084142684937 0.41523098945617676 111.58092498779297
4 3 0.8849082589149475 0.23268698155879974 0.30662405490875244 1214.7138671875
5 4 0.906022846698761 0.23268698155879974 0.25916892290115356 2999.732666015625
6 5 0.9245413541793823 0.23268698155879974 0.2080700546503067 5503.68408203125
7 6 0.9383869767189026 0.23268698155879974 0.1752740442752838 4690.720703125
8 7 0.9484250545501709 0.23268698155879974 0.1510019749403 5465.9267578125
9 8 0.9586362242698669 0.23268698155879974 0.12660910189151764 1849.1502685546875
10 9 0.961405336856842 0.23268698155879974 0.11609478294849396 3567.95849609375
11 10 0.9634822010993958 0.23268698155879974 0.11276258528232574 4574.11767578125
12 11 0.9655590057373047 0.23268698155879974 0.10540036112070084 3753.94873046875
13 12 0.9697126746177673 0.23268698155879974 0.0942334458231926 3380.722900390625
14 13 0.9714434146881104 0.23268698155879974 0.08314096182584763 6275.935546875
15 14 0.9762893915176392 0.23268698155879974 0.0717514380812645 2442.714599609375
16 15 0.9733471870422363 0.23268698155879974 0.08071456849575043 3416.07861328125
17 16 0.9787123799324036 0.23268698155879974 0.07309815287590027 3656.95751953125
18 17 0.9771547317504883 0.23268698155879974 0.07524916529655457 4760.67138671875
19 18 0.9790585041046143 0.23268698155879974 0.06330344825983047 2295.76513671875
20 19 0.9790585041046143 0.23268698155879974 0.06550607830286026 5322.1533203125
21 20 0.9783661961555481 0.23268698155879974 0.06409229338169098 2194.40283203125
22 21 0.9795777201652527 0.23268698155879974 0.05746037885546684 1612.3697509765625
23 22 0.9806161522865295 0.23268698155879974 0.060518983751535416 4674.85400390625
24 23 0.9833852648735046 0.27770084142684937 0.05250076577067375 1632.55615234375
25 24 0.987712025642395 0.27770084142684937 0.04003383219242096 1444.8175048828125
26 25 0.9828660488128662 0.27770084142684937 0.05212881788611412 3077.065185546875
27 26 0.9816545248031616 0.23268698155879974 0.05729270353913307 987.83740234375
28 27 0.9845967292785645 0.27770084142684937 0.05151180922985077 1980.85009765625
29 28 0.9870197176933289 0.2957063615322113 0.045257747173309326 1160.206298828125
30 29 0.9804430603981018 0.24376730620861053 0.06315220147371292 657.8983764648438
31 30 0.9890965819358826 0.4058171808719635 0.03781181946396828 797.4231567382812
32 31 0.9844236969947815 0.23268698155879974 0.05010179430246353 1343.955810546875
33 32 0.9851159453392029 0.27770084142684937 0.04631909728050232 1621.481689453125
34 33 0.9868466854095459 0.27770084142684937 0.04147016257047653 1539.1033935546875
35 34 0.987885057926178 0.3150969445705414 0.03842846676707268 557.3466796875
36 35 0.9903080463409424 0.23268698155879974 0.029467027634382248 2310.66357421875
37 36 0.9863274693489075 0.23268698155879974 0.044710543006658554 1721.2637939453125
38 37 0.9847698211669922 0.2583102583885193 0.04918225109577179 1349.9884033203125
39 38 0.9906542301177979 0.3067867159843445 0.03000747598707676 957.5072631835938
40 39 0.9923849105834961 0.29916897416114807 0.02561911940574646 1543.9759521484375
41 40 0.9865005016326904 0.23268698155879974 0.03470372408628464 2082.9208984375
42 41 0.9892696142196655 0.27770084142684937 0.03406791016459465 1992.923095703125
43 42 0.9937694668769836 0.23753462731838226 0.020795246586203575 1350.656982421875
44 43 0.9865005016326904 0.25692519545555115 0.050108153373003006 1347.6395263671875
45 44 0.9901350140571594 0.23268698155879974 0.028937410563230515 2191.127197265625
46 45 0.9941155910491943 0.23268698155879974 0.02078220620751381 1551.0986328125
47 46 0.9904811382293701 0.23268698155879974 0.028728974983096123 2349.158447265625
48 47 0.9903080463409424 0.23268698155879974 0.033932607620954514 1677.892822265625
49 48 0.9868466854095459 0.27770084142684937 0.03687359392642975 1879.01904296875
50 49 0.9942886829376221 0.27770084142684937 0.021784603595733643 1672.6260986328125
51 50 0.9911734461784363 0.23268698155879974 0.026831354945898056 2027.2027587890625
52 51 0.9894427061080933 0.27770084142684937 0.0279900673776865 1085.9053955078125
53 52 0.989615797996521 0.23337949812412262 0.03173811733722687 1212.1956787109375
54 53 0.993423342704773 0.23268698155879974 0.022051407024264336 1802.6983642578125
55 54 0.9955001473426819 0.23268698155879974 0.01723671332001686 1909.1605224609375
56 55 0.9904811382293701 0.23268698155879974 0.03664610907435417 1560.034912109375
57 56 0.9918656945228577 0.27770084142684937 0.029300954192876816 983.4076538085938
58 57 0.9948078989982605 0.2576177418231964 0.01762007363140583 908.1100463867188
59 58 0.9956732392311096 0.2340720295906067 0.013033601455390453 1055.4168701171875
60 59 0.993423342704773 0.23268698155879974 0.022611256688833237 2751.419921875
61 60 0.9892696142196655 0.27839335799217224 0.03379980847239494 1141.1070556640625
62 61 0.9944617748260498 0.41828253865242004 0.01806570030748844 992.7396850585938
63 62 0.9951540231704712 0.39058172702789307 0.015332863666117191 964.7886352539062
64 63 0.9958463311195374 0.4861495792865753 0.013660548254847527 1077.28125
65 64 0.9963655471801758 0.2340720295906067 0.010616755113005638 1765.6953125
66 65 0.9939425587654114 0.27770084142684937 0.019288523122668266 3227.864501953125
67 66 0.9913464784622192 0.23268698155879974 0.030606787651777267 3323.407958984375
68 67 0.9929041266441345 0.23268698155879974 0.022766467183828354 3642.7861328125
69 68 0.9955001473426819 0.23268698155879974 0.019210971891880035 1868.906982421875
70 69 0.996192455291748 0.23268698155879974 0.011477937921881676 2611.31640625
71 70 0.9949809908866882 0.23268698155879974 0.018107961863279343 3013.491943359375
72 71 0.9951540231704712 0.23268698155879974 0.015328730456531048 3909.685302734375
73 72 0.9927310347557068 0.23337949812412262 0.023373156785964966 2733.7919921875
74 73 0.9956732392311096 0.23268698155879974 0.013462813571095467 2812.021728515625
75 74 0.9955001473426819 0.23268698155879974 0.01717122085392475 1892.8349609375
76 75 0.9975770115852356 0.23268698155879974 0.008702440187335014 4670.3193359375
77 76 0.9972308874130249 0.23268698155879974 0.007852787151932716 4487.03515625
78 77 0.9965385794639587 0.23268698155879974 0.009604893624782562 6443.4267578125
79 78 0.9920387864112854 0.23268698155879974 0.020789368078112602 6024.58740234375
80 79 0.9935963749885559 0.23268698155879974 0.018593357875943184 2821.33935546875
81 80 0.9956732392311096 0.23268698155879974 0.015648460015654564 2576.421142578125
82 81 0.9949809908866882 0.23268698155879974 0.017194343730807304 3764.894775390625
83 82 0.9929041266441345 0.27770084142684937 0.021781075745821 2807.73291015625
84 83 0.9968847632408142 0.23268698155879974 0.012610912322998047 4471.623046875
85 84 0.9967116713523865 0.23268698155879974 0.01276914868503809 6788.912109375
86 85 0.9958463311195374 0.23268698155879974 0.0117557467892766 7592.3515625
87 86 0.9956732392311096 0.23268698155879974 0.010352320037782192 6271.54931640625
88 87 0.996192455291748 0.23268698155879974 0.009747961536049843 3658.080322265625
89 88 0.9974039196968079 0.23268698155879974 0.007929908111691475 8354.2626953125
90 89 0.9944617748260498 0.23268698155879974 0.01685083471238613 5968.7373046875
91 90 0.9968847632408142 0.23268698155879974 0.011445037089288235 3262.92578125
92 91 0.9949809908866882 0.23268698155879974 0.016830891370773315 4658.84912109375
93 92 0.9987885355949402 0.23268698155879974 0.006762914825230837 8752.833984375
94 93 0.9970577955245972 0.23268698155879974 0.01124420017004013 7593.404296875
95 94 0.9979231357574463 0.23268698155879974 0.009695205837488174 1501.1002197265625
96 95 0.9970577955245972 0.23268698155879974 0.010869201272726059 4020.974609375
97 96 0.9942886829376221 0.23268698155879974 0.01823830045759678 2655.122802734375
98 97 0.9982693195343018 0.23545706272125244 0.006866878364235163 886.0972900390625
99 98 0.9852890372276306 0.2527700960636139 0.06450172513723373 2126.42578125
100 99 0.9955001473426819 0.42659279704093933 0.015554066747426987 1247.246826171875
101 100 0.9975770115852356 0.33240997791290283 0.008071373216807842 1635.5611572265625

View file

@ -13,7 +13,7 @@ def select_model():
while True: while True:
try: try:
# all_model_dir = input("Model dir : ") # all_model_dir = input("Model dir : ")
all_model_dir = "/home/jhodi/bit/Python/Grapevine_Pathology_Detection/venv/models" all_model_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'models')
model_found = 0 model_found = 0
for foldername, subfolders, filenames in os.walk(all_model_dir): for foldername, subfolders, filenames in os.walk(all_model_dir):
for filename in filenames: for filename in filenames: