This commit is contained in:
2026-07-27 16:42:04 +02:00
parent 45a7704ea6
commit 018b751723
23 changed files with 2327 additions and 635 deletions

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { ImageBackground, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
@@ -16,57 +16,84 @@ const ONBOARDING_BACKGROUND = {
const getHealthOnboardingCopy = (language: 'de' | 'en' | 'es') => {
if (language === 'de') {
return {
title: 'Wo ist der Health-Scan?',
subtitle: 'Du findest ihn auf jeder gespeicherten Pflanze, direkt unter der Beschreibung.',
buttonPreview: 'Health-Scan starten',
cta: 'Weiter',
skip: 'Spaeter',
flow: ['Pflanze scannen', 'Speichern', 'Detailseite oeffnen', 'Health-Scan starten'],
outputTitle: 'Was du danach bekommst',
outputs: [
'Gesundheits-Score mit Status: stabil, beobachten oder kritisch.',
'Ausfuehrliche Analyse mit sichtbaren Hinweisen und Unsicherheit.',
'Wahrscheinlichste Ursachen mit Confidence-Werten.',
'Sofortmassnahmen plus konkreter 7-Tage-Pflegeplan.',
title: 'So sieht eine Antwort aus',
subtitle: 'Ein echtes Beispiel - damit du weißt, was du bekommst, bevor du etwas entscheidest.',
exampleSymptomLabel: 'Symptom',
exampleSymptom: 'Untere Blätter gelb, Erde feucht',
causeLabel: 'Wahrscheinlichste Ursache',
// Bewusst ein Fall MITTLERER Sicherheit, kein Vorzeige-Treffer - das ist
// der Beweis fuer Belief 3. Die Werte summieren sich absichtlich NICHT auf
// 100 %: der Backend-Prompt (server/lib/openai.js, likelyIssues) liefert je
// Ursache eine unabhaengige Sicherheit, keine Wahrscheinlichkeitsverteilung.
// Ein Beispiel, das sich auf 100 addiert, wuerde ein Verhalten zeigen, das
// das Produkt nicht hat.
causes: [
{ name: 'Überwässerung', level: '64 %', tone: 'high' as const },
{ name: 'Nährstoffmangel', level: '41 %', tone: 'mid' as const },
{ name: 'Lichtmangel', level: '22 %', tone: 'low' as const },
],
guidanceNote: 'Tipp: Fotografiere die ganze Pflanze, die Blattunterseiten und die Erde. Je klarer das Foto, desto genauer wird der Plan.',
// Schwelle 65 % entspricht der Kalibrierung im Backend-Prompt:
// 0.65-0.84 "sehr wahrscheinlich", 0.40-0.64 "mehrdeutig".
confidenceNote: 'Unter 65 % sagen wir dir das deutlich - dann sind die Prüfschritte wichtiger als die Ursache.',
checkLabel: 'Prüfe zuerst',
checks: ['Erde 3 cm tief anfassen', 'Hat der Topf Abzugslöcher?', 'Blattunterseiten ansehen'],
actionLabel: 'Tu jetzt',
action: 'Nicht gießen. In 5 Tagen erneut prüfen.',
followUpLabel: 'In 7 Tagen',
followUp: 'Wir fragen nach, ob es besser wird - und passen den Plan an, wenn nicht.',
limitNote: 'Ein Foto zeigt keine Wurzeln, keine Erdfeuchte und keine Vorgeschichte. Deshalb bekommst du Wahrscheinlichkeiten und Prüfschritte - keine Diagnose.',
guidanceNote: 'Tipp: Fotografiere die ganze Pflanze, die Blattunterseiten und die Erde. Je klarer das Foto, desto präziser der Plan.',
cta: 'Weiter',
skip: 'Später',
};
}
if (language === 'es') {
return {
title: 'Donde esta el health-scan?',
subtitle: 'Lo encuentras en cada planta guardada, justo debajo de la descripcion.',
buttonPreview: 'Iniciar health-scan',
cta: 'Continuar',
skip: 'Mas tarde',
flow: ['Escanear planta', 'Guardar', 'Abrir detalle', 'Iniciar health-scan'],
outputTitle: 'Que recibes despues',
outputs: [
'Puntaje de salud con estado: estable, observar o critico.',
'Analisis detallado con senales visibles e incertidumbre.',
'Causas probables con valores de confianza.',
'Acciones inmediatas y plan concreto de 7 dias.',
title: 'Así se ve una respuesta',
subtitle: 'Un ejemplo real - para que sepas qué recibes antes de decidir nada.',
exampleSymptomLabel: 'Síntoma',
exampleSymptom: 'Hojas inferiores amarillas, sustrato húmedo',
causeLabel: 'Causa más probable',
causes: [
{ name: 'Exceso de riego', level: '64 %', tone: 'high' as const },
{ name: 'Falta de nutrientes', level: '41 %', tone: 'mid' as const },
{ name: 'Falta de luz', level: '22 %', tone: 'low' as const },
],
guidanceNote: 'Consejo: fotografia la planta completa, el reverso de las hojas y el sustrato. Cuanto mas clara sea la foto, mas preciso sera el plan.',
confidenceNote: 'Por debajo del 65 % te lo decimos claramente - entonces los pasos de revisión importan más que la causa.',
checkLabel: 'Revisa primero',
checks: ['Tocar el sustrato a 3 cm', '¿La maceta tiene drenaje?', 'Mirar el reverso de las hojas'],
actionLabel: 'Haz ahora',
action: 'No regar. Volver a revisar en 5 días.',
followUpLabel: 'En 7 días',
followUp: 'Preguntamos si va mejor - y ajustamos el plan si no.',
limitNote: 'Una foto no muestra las raíces, la humedad del sustrato ni el historial. Por eso recibes probabilidades y pasos de revisión - no un diagnóstico.',
guidanceNote: 'Consejo: fotografía la planta completa, el reverso de las hojas y el sustrato. Cuanto más clara sea la foto, más preciso será el plan.',
cta: 'Continuar',
skip: 'Más tarde',
};
}
return {
title: 'Where is the health scan?',
subtitle: 'It lives on every saved plant, directly below the plant description.',
buttonPreview: 'Start health scan',
title: 'This is what an answer looks like',
subtitle: 'A real example - so you know what you get before you decide anything.',
exampleSymptomLabel: 'Symptom',
exampleSymptom: 'Lower leaves yellow, soil damp',
causeLabel: 'Most likely cause',
causes: [
{ name: 'Overwatering', level: '64%', tone: 'high' as const },
{ name: 'Nutrient deficiency', level: '41%', tone: 'mid' as const },
{ name: 'Too little light', level: '22%', tone: 'low' as const },
],
confidenceNote: 'Below 65% we say so plainly - then the checks matter more than the cause.',
checkLabel: 'Check first',
checks: ['Feel the soil 3 cm down', 'Does the pot have drainage holes?', 'Look at the leaf undersides'],
actionLabel: 'Do now',
action: 'Do not water. Check again in 5 days.',
followUpLabel: 'In 7 days',
followUp: 'We ask whether it is improving - and adjust the plan if it is not.',
limitNote: 'A photo cannot show roots, soil moisture or history. So you get probabilities and things to check - not a diagnosis.',
guidanceNote: 'Tip: photograph the full plant, leaf undersides, and the soil. The clearer the photo, the more precise the plan.',
cta: 'Continue',
skip: 'Later',
flow: ['Scan plant', 'Save', 'Open detail', 'Start health scan'],
outputTitle: 'What you get after',
outputs: [
'Health score with stable, watch, or critical status.',
'Detailed analysis with visible signals and uncertainty.',
'Most likely causes with confidence values.',
'Immediate actions plus a concrete 7-day care plan.',
],
guidanceNote: 'Tip: photograph the full plant, leaf undersides, and the soil. The clearer the photo, the more precise the plan.',
};
};
@@ -105,36 +132,73 @@ export default function HealthCheckOnboardingScreen() {
</View>
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
<ImageBackground
source={require('../../assets/onboarding_health_scan_mockup.png')}
style={[styles.illustration, { borderColor: colors.border, backgroundColor: isDarkMode ? '#0a110b' : '#fbfaf3' }]}
imageStyle={styles.illustrationImage}
resizeMode="cover"
>
<View style={[styles.illustrationOverlay, { backgroundColor: isDarkMode ? 'rgba(8, 14, 9, 0.08)' : 'rgba(251, 250, 243, 0.04)' }]} />
</ImageBackground>
{/* Worked Example statt Wegbeschreibung: ein echtes Ergebnis schlaegt
jede Behauptung ueber die Qualitaet der Ergebnisse. */}
<View style={[styles.exampleCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Text style={[styles.exampleEyebrow, { color: colors.textMuted }]}>{copy.exampleSymptomLabel}</Text>
<Text style={[styles.exampleSymptom, { color: colors.text }]}>{copy.exampleSymptom}</Text>
<View style={[styles.flowCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
{copy.flow.map((item, index) => (
<View key={item} style={styles.flowRow}>
<View style={[styles.flowIndex, { backgroundColor: index === 3 ? colors.primary : colors.surfaceMuted }]}>
<Text style={[styles.flowIndexText, { color: index === 3 ? colors.onPrimary : colors.textMuted }]}>
{index + 1}
</Text>
</View>
<Text style={[styles.flowText, { color: colors.text }]}>{item}</Text>
<View style={[styles.divider, { backgroundColor: colors.border }]} />
<Text style={[styles.exampleEyebrow, { color: colors.textMuted }]}>{copy.causeLabel}</Text>
{copy.causes.map((cause) => (
<View key={cause.name} style={styles.causeRow}>
<View
style={[
styles.causeDot,
{
backgroundColor:
cause.tone === 'high' ? colors.danger : cause.tone === 'mid' ? colors.warning : colors.textMuted,
},
]}
/>
<Text
style={[
styles.causeName,
{ color: cause.tone === 'high' ? colors.text : colors.textSecondary },
cause.tone === 'high' && styles.causeNamePrimary,
]}
>
{cause.name}
</Text>
<Text style={[styles.causeLevel, { color: colors.textMuted }]}>{cause.level}</Text>
</View>
))}
<View style={[styles.confidenceNote, { backgroundColor: colors.surfaceMuted }]}>
<Ionicons name="alert-circle-outline" size={14} color={colors.textMuted} />
<Text style={[styles.confidenceNoteText, { color: colors.textSecondary }]}>{copy.confidenceNote}</Text>
</View>
<View style={[styles.divider, { backgroundColor: colors.border }]} />
<Text style={[styles.exampleEyebrow, { color: colors.textMuted }]}>{copy.checkLabel}</Text>
{copy.checks.map((check) => (
<View key={check} style={styles.outputRow}>
<Ionicons name="ellipse-outline" size={15} color={colors.primary} />
<Text style={[styles.outputText, { color: colors.textSecondary }]}>{check}</Text>
</View>
))}
<View style={[styles.actionBox, { backgroundColor: colors.primarySoft }]}>
<Text style={[styles.actionLabel, { color: colors.primaryDark }]}>{copy.actionLabel}</Text>
<Text style={[styles.actionText, { color: colors.primaryDark }]}>{copy.action}</Text>
</View>
<View style={styles.followUpRow}>
<Ionicons name="chatbubble-ellipses-outline" size={15} color={colors.textMuted} />
<View style={styles.followUpCopy}>
<Text style={[styles.followUpLabel, { color: colors.text }]}>{copy.followUpLabel}</Text>
<Text style={[styles.followUpText, { color: colors.textSecondary }]}>{copy.followUp}</Text>
</View>
</View>
</View>
<View style={[styles.outputCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Text style={[styles.outputTitle, { color: colors.text }]}>{copy.outputTitle}</Text>
{copy.outputs.map((item) => (
<View key={item} style={styles.outputRow}>
<Ionicons name="checkmark-circle" size={16} color={colors.success} />
<Text style={[styles.outputText, { color: colors.textSecondary }]}>{item}</Text>
</View>
))}
{/* Die Grenze des Verfahrens steht bewusst direkt unter dem Beispiel,
nicht im Kleingedruckten. Sie ist Teil des Verkaufsarguments. */}
<View style={[styles.limitCard, { backgroundColor: colors.surfaceMuted, borderColor: colors.border }]}>
<Ionicons name="information-circle-outline" size={17} color={colors.textMuted} />
<Text style={[styles.limitText, { color: colors.textSecondary }]}>{copy.limitNote}</Text>
</View>
<View style={[styles.guidanceCard, { backgroundColor: colors.primarySoft, borderColor: colors.border }]}>
@@ -170,30 +234,36 @@ const styles = StyleSheet.create({
title: { fontSize: 30, lineHeight: 34, fontWeight: '900' },
subtitle: { fontSize: 14, lineHeight: 20 },
content: { gap: 14, paddingBottom: 12 },
illustration: { height: 230, borderRadius: 28, borderWidth: 1, justifyContent: 'center', overflow: 'hidden' },
illustrationImage: { borderRadius: 28 },
illustrationOverlay: { ...StyleSheet.absoluteFillObject },
phone: { width: 178, minHeight: 156, borderRadius: 26, borderWidth: 1, padding: 12, gap: 10, marginLeft: 16 },
phoneHeader: { height: 58, borderRadius: 18, justifyContent: 'flex-end', padding: 10 },
phoneTitle: { fontSize: 13, fontWeight: '800' },
phoneRows: { gap: 8 },
phoneRowLong: { height: 8, borderRadius: 999 },
phoneRowShort: { width: '66%', height: 8, borderRadius: 999 },
healthButtonPreview: { height: 34, borderRadius: 14, borderWidth: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 5 },
healthButtonText: { fontSize: 10, fontWeight: '800' },
scanCard: { position: 'absolute', right: 16, bottom: 20, width: 136, borderRadius: 20, borderWidth: 1, padding: 14, gap: 7 },
scanScore: { fontSize: 25, lineHeight: 29, fontWeight: '900' },
scanLabel: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase' },
scanLine: { height: 8, borderRadius: 999 },
scanLineShort: { width: '68%', height: 8, borderRadius: 999 },
flowCard: { borderRadius: 18, borderWidth: 1, padding: 14, gap: 10 },
flowRow: { flexDirection: 'row', alignItems: 'center', gap: 10 },
flowIndex: { width: 26, height: 26, borderRadius: 13, alignItems: 'center', justifyContent: 'center' },
flowIndexText: { fontSize: 12, fontWeight: '900' },
flowText: { flex: 1, fontSize: 14, fontWeight: '700' },
outputCard: { borderRadius: 18, borderWidth: 1, padding: 16, gap: 11 },
outputTitle: { fontSize: 15, fontWeight: '800' },
outputRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 9 },
outputRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 9, marginBottom: 6 },
exampleCard: { borderRadius: 20, borderWidth: 1, padding: 18, gap: 4 },
exampleEyebrow: { fontSize: 10.5, fontWeight: '900', letterSpacing: 0.9, textTransform: 'uppercase', marginBottom: 6 },
exampleSymptom: { fontSize: 16, fontWeight: '700', lineHeight: 22 },
divider: { height: 1, marginVertical: 14 },
causeRow: { flexDirection: 'row', alignItems: 'center', gap: 9, marginBottom: 8 },
causeDot: { width: 8, height: 8, borderRadius: 4 },
causeName: { flex: 1, fontSize: 14.5, fontWeight: '600' },
causeNamePrimary: { fontWeight: '800' },
causeLevel: { fontSize: 12.5, fontWeight: '800', fontVariant: ['tabular-nums'] },
confidenceNote: { flexDirection: 'row', alignItems: 'flex-start', gap: 7, borderRadius: 10, padding: 10, marginTop: 4 },
confidenceNoteText: { flex: 1, fontSize: 12, lineHeight: 16.5, fontWeight: '500' },
actionBox: { borderRadius: 14, padding: 13, marginTop: 12, gap: 3 },
actionLabel: { fontSize: 10.5, fontWeight: '900', letterSpacing: 0.9, textTransform: 'uppercase' },
actionText: { fontSize: 14.5, fontWeight: '700', lineHeight: 20 },
followUpRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 9, marginTop: 14 },
followUpCopy: { flex: 1, gap: 2 },
followUpLabel: { fontSize: 13, fontWeight: '800' },
followUpText: { fontSize: 13, lineHeight: 18, fontWeight: '500' },
limitCard: { borderRadius: 16, borderWidth: 1, padding: 14, flexDirection: 'row', alignItems: 'flex-start', gap: 10 },
limitText: { flex: 1, fontSize: 12.5, lineHeight: 18, fontWeight: '500' },
outputText: { flex: 1, fontSize: 13, lineHeight: 18 },
guidanceCard: { borderRadius: 18, borderWidth: 1, padding: 14, flexDirection: 'row', alignItems: 'flex-start', gap: 10 },
guidanceText: { flex: 1, fontSize: 12, lineHeight: 18, fontWeight: '600' },