897 lines
28 KiB
TypeScript
897 lines
28 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
|
import {
|
|
ActivityIndicator,
|
|
Animated,
|
|
Easing,
|
|
FlatList,
|
|
Image,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableOpacity,
|
|
View,
|
|
useWindowDimensions,
|
|
} from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { useRouter } from 'expo-router';
|
|
import { useApp } from '../../context/AppContext';
|
|
import { useColors } from '../../constants/Colors';
|
|
import { useSafeAnalytics } from '../../services/analytics';
|
|
import { OnboardingProgressService } from '../../services/onboardingProgressService';
|
|
import { PreAuthOnboardingService } from '../../services/preAuthOnboardingService';
|
|
import { Language } from '../../types';
|
|
|
|
// Avatar path matching copied file
|
|
const BOT_AVATAR = require('../../assets/robot_monstera_avatar.jpg');
|
|
|
|
interface Message {
|
|
id: string;
|
|
sender: 'bot' | 'user';
|
|
text: string;
|
|
subtext?: string;
|
|
timestamp: string;
|
|
}
|
|
|
|
const getChatCopy = (language: Language) => {
|
|
if (language === 'de') {
|
|
return {
|
|
title: 'Monstera',
|
|
skip: 'Überspringen',
|
|
continue: 'Weiter',
|
|
today: 'Heute, 20:42 Uhr',
|
|
typing: 'schreibt...',
|
|
botGreeting: "Hi! Ich bin deine neue Monstera. 🌿 Ich freue mich riesig darauf, mit dir zu wachsen!",
|
|
botSource: "Als erstes: Wie hast du von GreenLens gehört?",
|
|
botGoal: "Verstanden! Und was ist dein Hauptziel mit deinen Pflanzen?",
|
|
botExperience: "Schön! Wie erfahren bist du in der Pflanzenpflege?",
|
|
botLight: "Damit ich glücklich bleibe und diese großen, schönen Blätter wachsen, muss ich ein bisschen über mein neues Zuhause wissen. Wie viel Licht bekomme ich hier?",
|
|
botComplete: "Perfekt! Ich bin bereit einzuziehen. Lass uns deinen Pflegeplan erstellen!",
|
|
sources: {
|
|
app_store: 'App Store / Play Store',
|
|
instagram: 'Instagram',
|
|
tiktok: 'TikTok',
|
|
friend: 'Freunde oder Familie',
|
|
search: 'Google oder Suche',
|
|
other: 'Etwas anderes',
|
|
},
|
|
goals: {
|
|
identify: 'Pflanzen erkennen',
|
|
care: 'Pflegepläne',
|
|
collection: 'Sammlung vergrößern',
|
|
learn: 'Über Pflanzen lernen',
|
|
},
|
|
experiences: {
|
|
beginner: 'Anfänger (Ich bin neu)',
|
|
intermediate: 'Fortgeschritten (Ich habe Pflanzen)',
|
|
advanced: 'Experte (Ich weiß Bescheid)',
|
|
},
|
|
lights: {
|
|
bright_indirect: {
|
|
title: 'Helles, indirektes Licht',
|
|
sub: 'In der Nähe eines sonnigen Fensters, ohne direkte Strahlen',
|
|
},
|
|
low_light: {
|
|
title: 'Wenig Licht',
|
|
sub: 'Weit weg von Fenstern, meistens Schatten',
|
|
},
|
|
direct_sunlight: {
|
|
title: 'Direktes Sonnenlicht',
|
|
sub: 'Den ganzen Tag direkt an einem sonnigen Fenster',
|
|
},
|
|
},
|
|
};
|
|
}
|
|
if (language === 'es') {
|
|
return {
|
|
title: 'Monstera',
|
|
skip: 'Omitir',
|
|
continue: 'Continuar',
|
|
today: 'Hoy, 8:42 PM',
|
|
typing: 'escribiendo...',
|
|
botGreeting: "¡Hola! Soy tu nueva Monstera. 🌿 ¡Estoy muy emocionada de crecer contigo!",
|
|
botSource: "Primero, ¿cómo te enteraste de GreenLens?",
|
|
botGoal: "¡Entendido! ¿Y cuál es tu objetivo principal con tus plantas?",
|
|
botExperience: "¡Genial! ¿Qué experiencia tienes en el cuidado de plantas?",
|
|
botLight: "Para estar feliz y que crezcan esas hojas grandes y hermosas, necesito saber un poco sobre mi nuevo hogar. ¿Cuánta luz recibiré aquí?",
|
|
botComplete: "¡Perfecto! Estoy lista para mudarme. ¡Creemos tu plan de cuidados!",
|
|
sources: {
|
|
app_store: 'App Store / Play Store',
|
|
instagram: 'Instagram',
|
|
tiktok: 'TikTok',
|
|
friend: 'Amigos o familia',
|
|
search: 'Google o búsqueda',
|
|
other: 'Otra cosa',
|
|
},
|
|
goals: {
|
|
identify: 'Identificar plantas',
|
|
care: 'Horarios de cuidado',
|
|
collection: 'Crecer mi colección',
|
|
learn: 'Aprender sobre plantas',
|
|
},
|
|
experiences: {
|
|
beginner: 'Principiante (Soy nuevo)',
|
|
intermediate: 'Intermedio (Tengo plantas)',
|
|
advanced: 'Experto (Sé bastante)',
|
|
},
|
|
lights: {
|
|
bright_indirect: {
|
|
title: 'Luz brillante e indirecta',
|
|
sub: 'Cerca de una ventana soleada, sin sol directo',
|
|
},
|
|
low_light: {
|
|
title: 'Poca luz',
|
|
sub: 'Lejos de las ventanas, principalmente sombra',
|
|
},
|
|
direct_sunlight: {
|
|
title: 'Luz solar directa',
|
|
sub: 'Directamente en una ventana soleada todo el día',
|
|
},
|
|
},
|
|
};
|
|
}
|
|
return {
|
|
title: 'Monstera',
|
|
skip: 'Skip',
|
|
continue: 'Continue',
|
|
today: 'Today, 8:42 PM',
|
|
typing: 'typing...',
|
|
botGreeting: "Hi! I'm your new Monstera. 🌿 I'm so excited to grow with you!",
|
|
botSource: "First, how did you hear about GreenLens?",
|
|
botGoal: "Got it! And what is your main goal with your plants?",
|
|
botExperience: "Nice! How experienced are you with plant care?",
|
|
botLight: "To stay happy and grow those big, beautiful leaves, I need to know a bit about my new home. How much light do I get here?",
|
|
botComplete: "Perfect! I'm ready to move in. Let's build your care plan!",
|
|
sources: {
|
|
app_store: 'App Store / Play Store',
|
|
instagram: 'Instagram',
|
|
tiktok: 'TikTok',
|
|
friend: 'Friends or family',
|
|
search: 'Google or search',
|
|
other: 'Something else',
|
|
},
|
|
goals: {
|
|
identify: 'Identify plants',
|
|
care: 'Care schedules',
|
|
collection: 'Grow collection',
|
|
learn: 'Learn about plants',
|
|
},
|
|
experiences: {
|
|
beginner: 'Beginner (I\'m a new plant parent)',
|
|
intermediate: 'Intermediate (I have some plants)',
|
|
advanced: 'Advanced (I\'m a plant expert)',
|
|
},
|
|
lights: {
|
|
bright_indirect: {
|
|
title: 'Bright, indirect light',
|
|
sub: 'Near a sunny window, but no harsh rays',
|
|
},
|
|
low_light: {
|
|
title: 'Low light',
|
|
sub: 'Far from windows, mostly shadow',
|
|
},
|
|
direct_sunlight: {
|
|
title: 'Direct sunlight',
|
|
sub: 'Directly in a sunny window all day',
|
|
},
|
|
},
|
|
};
|
|
};
|
|
|
|
const TypingIndicator = ({ colors }: { colors: any }) => {
|
|
const dot1 = useRef(new Animated.Value(0)).current;
|
|
const dot2 = useRef(new Animated.Value(0)).current;
|
|
const dot3 = useRef(new Animated.Value(0)).current;
|
|
|
|
useEffect(() => {
|
|
const animateDot = (value: Animated.Value, delay: number) => {
|
|
return Animated.loop(
|
|
Animated.sequence([
|
|
Animated.delay(delay),
|
|
Animated.timing(value, {
|
|
toValue: -6,
|
|
duration: 250,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(value, {
|
|
toValue: 0,
|
|
duration: 250,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.delay(350),
|
|
])
|
|
);
|
|
};
|
|
|
|
const anim1 = animateDot(dot1, 0);
|
|
const anim2 = animateDot(dot2, 120);
|
|
const anim3 = animateDot(dot3, 240);
|
|
|
|
anim1.start();
|
|
anim2.start();
|
|
anim3.start();
|
|
|
|
return () => {
|
|
anim1.stop();
|
|
anim2.stop();
|
|
anim3.stop();
|
|
};
|
|
}, [dot1, dot2, dot3]);
|
|
|
|
return (
|
|
<View style={styles.typingContainer}>
|
|
<Animated.View style={[styles.typingDot, { backgroundColor: colors.primary, transform: [{ translateY: dot1 }] }]} />
|
|
<Animated.View style={[styles.typingDot, { backgroundColor: colors.primary, transform: [{ translateY: dot2 }] }]} />
|
|
<Animated.View style={[styles.typingDot, { backgroundColor: colors.primary, transform: [{ translateY: dot3 }] }]} />
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default function OnboardingChatScreen() {
|
|
const router = useRouter();
|
|
const posthog = useSafeAnalytics();
|
|
const { session, colorPalette, language } = useApp();
|
|
// This screen is hardcoded dark (#131e14 backgrounds), so always use the
|
|
// dark color set — theme-dependent text would be unreadable in light mode.
|
|
const colors = useColors(true, colorPalette);
|
|
const copy = getChatCopy(language);
|
|
const { height } = useWindowDimensions();
|
|
const compact = height < 700;
|
|
|
|
const [currentStep, setCurrentStep] = useState<number>(0);
|
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
const [isTyping, setIsTyping] = useState<boolean>(false);
|
|
const [showOptions, setShowOptions] = useState<boolean>(false);
|
|
const [answers, setAnswers] = useState({
|
|
acquisitionSource: null as string | null,
|
|
primaryGoal: null as string | null,
|
|
experienceLevel: null as string | null,
|
|
lightLevel: null as string | null,
|
|
});
|
|
|
|
const flatListRef = useRef<FlatList>(null);
|
|
const floatAnim = useRef(new Animated.Value(0)).current;
|
|
|
|
// Float animation loop for the background monstera pot
|
|
useEffect(() => {
|
|
const float = () => {
|
|
Animated.sequence([
|
|
Animated.timing(floatAnim, {
|
|
toValue: -12,
|
|
duration: 3000,
|
|
easing: Easing.inOut(Easing.sin),
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(floatAnim, {
|
|
toValue: 0,
|
|
duration: 3000,
|
|
easing: Easing.inOut(Easing.sin),
|
|
useNativeDriver: true,
|
|
}),
|
|
]).start(() => float());
|
|
};
|
|
float();
|
|
}, [floatAnim]);
|
|
|
|
// Initial Bot Greeting
|
|
useEffect(() => {
|
|
setIsTyping(true);
|
|
setShowOptions(false);
|
|
const timer1 = setTimeout(() => {
|
|
setMessages([
|
|
{
|
|
id: 'greeting',
|
|
sender: 'bot',
|
|
text: copy.botGreeting,
|
|
timestamp: copy.today,
|
|
},
|
|
]);
|
|
setIsTyping(false);
|
|
|
|
// Delay before asking first question
|
|
const timer2 = setTimeout(() => {
|
|
setIsTyping(true);
|
|
const timer3 = setTimeout(() => {
|
|
setMessages((prev) => [
|
|
...prev,
|
|
{
|
|
id: 'question-source',
|
|
sender: 'bot',
|
|
text: copy.botSource,
|
|
timestamp: copy.today,
|
|
},
|
|
]);
|
|
setIsTyping(false);
|
|
setShowOptions(true); // Only show options after question is fully posted!
|
|
}, 800);
|
|
return () => clearTimeout(timer3);
|
|
}, 500);
|
|
|
|
return () => clearTimeout(timer2);
|
|
}, 1200);
|
|
|
|
return () => clearTimeout(timer1);
|
|
}, []);
|
|
|
|
// Scroll to bottom on messages update
|
|
useEffect(() => {
|
|
setTimeout(() => {
|
|
flatListRef.current?.scrollToEnd({ animated: true });
|
|
}, 100);
|
|
}, [messages, isTyping]);
|
|
|
|
const selectOption = (id: string, label: string) => {
|
|
// Add user bubble
|
|
const userMsg: Message = {
|
|
id: `user-${currentStep}-${Date.now()}`,
|
|
sender: 'user',
|
|
text: label,
|
|
timestamp: copy.today,
|
|
};
|
|
setMessages((prev) => [...prev, userMsg]);
|
|
|
|
const nextStep = currentStep + 1;
|
|
setCurrentStep(nextStep);
|
|
|
|
// Save answer
|
|
const nextAnswers = { ...answers };
|
|
if (currentStep === 0) nextAnswers.acquisitionSource = id;
|
|
else if (currentStep === 1) nextAnswers.primaryGoal = id;
|
|
else if (currentStep === 2) nextAnswers.experienceLevel = id;
|
|
else if (currentStep === 3) nextAnswers.lightLevel = id;
|
|
setAnswers(nextAnswers);
|
|
|
|
// Trigger bot response typing
|
|
setIsTyping(true);
|
|
setShowOptions(false); // Hide immediately when user answers
|
|
const delay = setTimeout(() => {
|
|
let botText = '';
|
|
let botSubtext = undefined;
|
|
|
|
if (nextStep === 1) {
|
|
botText = copy.botGoal;
|
|
} else if (nextStep === 2) {
|
|
botText = copy.botExperience;
|
|
} else if (nextStep === 3) {
|
|
botText = copy.botLight;
|
|
} else if (nextStep === 4) {
|
|
botText = copy.botComplete;
|
|
}
|
|
|
|
setMessages((prev) => [
|
|
...prev,
|
|
{
|
|
id: `bot-${nextStep}-${Date.now()}`,
|
|
sender: 'bot',
|
|
text: botText,
|
|
subtext: botSubtext,
|
|
timestamp: copy.today,
|
|
},
|
|
]);
|
|
setIsTyping(false);
|
|
setShowOptions(true); // Re-enable options after bot finishes typing
|
|
}, 850);
|
|
|
|
return () => clearTimeout(delay);
|
|
};
|
|
|
|
const handleBack = () => {
|
|
if (currentStep === 0) {
|
|
router.back();
|
|
return;
|
|
}
|
|
|
|
const prevStep = currentStep - 1;
|
|
setCurrentStep(prevStep);
|
|
|
|
// Revert last answers
|
|
const nextAnswers = { ...answers };
|
|
if (prevStep === 0) nextAnswers.acquisitionSource = null;
|
|
else if (prevStep === 1) nextAnswers.primaryGoal = null;
|
|
else if (prevStep === 2) nextAnswers.experienceLevel = null;
|
|
else if (prevStep === 3) nextAnswers.lightLevel = null;
|
|
setAnswers(nextAnswers);
|
|
|
|
// Remove user bubble and bot bubble from messages history
|
|
setMessages((prev) => {
|
|
const copyMsgs = [...prev];
|
|
// Pop bot question bubble and user response bubble
|
|
copyMsgs.pop();
|
|
copyMsgs.pop();
|
|
return copyMsgs;
|
|
});
|
|
};
|
|
|
|
const onSkip = () => {
|
|
posthog.capture('onboarding_chat_skipped', { step: currentStep });
|
|
router.replace('/onboarding/health-check');
|
|
};
|
|
|
|
const onFinish = () => {
|
|
if (session?.userId) {
|
|
if (answers.acquisitionSource) OnboardingProgressService.setAcquisitionSource(session.userId, answers.acquisitionSource);
|
|
if (answers.primaryGoal) OnboardingProgressService.setPrimaryGoal(session.userId, answers.primaryGoal);
|
|
if (answers.experienceLevel) OnboardingProgressService.setExperienceLevel(session.userId, answers.experienceLevel);
|
|
}
|
|
|
|
if (answers.acquisitionSource) void PreAuthOnboardingService.setAnswer('acquisitionSource', answers.acquisitionSource);
|
|
if (answers.primaryGoal) void PreAuthOnboardingService.setAnswer('primaryGoal', answers.primaryGoal);
|
|
if (answers.experienceLevel) void PreAuthOnboardingService.setAnswer('experienceLevel', answers.experienceLevel);
|
|
if (answers.lightLevel) void PreAuthOnboardingService.setAnswer('lightLevel', answers.lightLevel);
|
|
|
|
posthog.capture('onboarding_chat_completed', {
|
|
source: answers.acquisitionSource,
|
|
goal: answers.primaryGoal,
|
|
experience: answers.experienceLevel,
|
|
light: answers.lightLevel,
|
|
});
|
|
|
|
router.replace('/onboarding/health-check');
|
|
};
|
|
|
|
// Build options list based on current question step
|
|
const renderOptions = () => {
|
|
if (!showOptions || isTyping) return null;
|
|
|
|
if (currentStep === 0) {
|
|
return (
|
|
<View style={styles.optionsWrap}>
|
|
<Text style={styles.stepTitle}>How did you find GreenLens?</Text>
|
|
<View style={styles.optionsGrid}>
|
|
{([
|
|
{ id: 'app_store', label: copy.sources.app_store, icon: 'logo-apple' as const },
|
|
{ id: 'instagram', label: copy.sources.instagram, icon: 'logo-instagram' as const },
|
|
{ id: 'tiktok', label: copy.sources.tiktok, icon: 'logo-youtube' as const },
|
|
{ id: 'friend', label: copy.sources.friend, icon: 'people-outline' as const },
|
|
{ id: 'search', label: copy.sources.search, icon: 'search-outline' as const },
|
|
{ id: 'other', label: copy.sources.other, icon: 'sparkles-outline' as const },
|
|
]).map((opt) => (
|
|
<TouchableOpacity key={opt.id} style={[styles.pillBtn, { borderColor: colors.border }]} onPress={() => selectOption(opt.id, opt.label)}>
|
|
<Ionicons name={opt.icon} size={15} color={colors.primary} style={{ marginRight: 4 }} />
|
|
<Text style={[styles.pillLabel, { color: colors.text }]}>{opt.label}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (currentStep === 1) {
|
|
return (
|
|
<View style={styles.optionsWrap}>
|
|
<Text style={styles.stepTitle}>What is your main goal?</Text>
|
|
<View style={styles.optionsVertical}>
|
|
{([
|
|
{ id: 'identify', label: copy.goals.identify, icon: 'search-outline' as const },
|
|
{ id: 'care', label: copy.goals.care, icon: 'water-outline' as const },
|
|
{ id: 'collection', label: copy.goals.collection, icon: 'leaf-outline' as const },
|
|
{ id: 'learn', label: copy.goals.learn, icon: 'book-outline' as const },
|
|
]).map((opt) => (
|
|
<TouchableOpacity key={opt.id} style={[styles.optionRow, { borderColor: colors.border }]} onPress={() => selectOption(opt.id, opt.label)}>
|
|
<View style={styles.optionRowLeft}>
|
|
<Ionicons name={opt.icon} size={18} color={colors.primary} style={{ marginRight: 10 }} />
|
|
<Text style={[styles.rowText, { color: colors.text }]}>{opt.label}</Text>
|
|
</View>
|
|
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (currentStep === 2) {
|
|
return (
|
|
<View style={styles.optionsWrap}>
|
|
<Text style={styles.stepTitle}>What is your experience level?</Text>
|
|
<View style={styles.optionsVertical}>
|
|
{([
|
|
{ id: 'beginner', label: copy.experiences.beginner, icon: 'leaf-outline' as const },
|
|
{ id: 'intermediate', label: copy.experiences.intermediate, icon: 'sunny-outline' as const },
|
|
{ id: 'advanced', label: copy.experiences.advanced, icon: 'flask-outline' as const },
|
|
]).map((opt) => (
|
|
<TouchableOpacity key={opt.id} style={[styles.optionRow, { borderColor: colors.border }]} onPress={() => selectOption(opt.id, opt.label)}>
|
|
<View style={styles.optionRowLeft}>
|
|
<Ionicons name={opt.icon} size={18} color={colors.primary} style={{ marginRight: 10 }} />
|
|
<Text style={[styles.rowText, { color: colors.text }]}>{opt.label}</Text>
|
|
</View>
|
|
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (currentStep === 3) {
|
|
return (
|
|
<View style={styles.optionsWrap}>
|
|
<Text style={styles.stepTitle}>How much light do I get here?</Text>
|
|
<View style={styles.optionsVertical}>
|
|
{([
|
|
{ id: 'bright_indirect', label: copy.lights.bright_indirect.title, sub: copy.lights.bright_indirect.sub, icon: 'wb-twilight' },
|
|
{ id: 'low_light', label: copy.lights.low_light.title, sub: copy.lights.low_light.sub, icon: 'brightness-4' },
|
|
{ id: 'direct_sunlight', label: copy.lights.direct_sunlight.title, sub: copy.lights.direct_sunlight.sub, icon: 'sunny' },
|
|
]).map((opt) => (
|
|
<TouchableOpacity key={opt.id} style={[styles.bentoOption, { borderColor: colors.border }]} onPress={() => selectOption(opt.id, opt.label)}>
|
|
<View style={styles.bentoCopy}>
|
|
<Text style={[styles.bentoTitle, { color: colors.text }]}>{opt.label}</Text>
|
|
<Text style={[styles.bentoSub, { color: colors.textSecondary }]}>{opt.sub}</Text>
|
|
</View>
|
|
<Ionicons name={opt.icon === 'sunny' ? 'sunny-outline' : opt.icon === 'brightness-4' ? 'contrast' : 'partly-sunny-outline'} size={20} color={colors.primary} />
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (currentStep === 4) {
|
|
return (
|
|
<View style={styles.footerWrap}>
|
|
<TouchableOpacity style={[styles.continueCta, { backgroundColor: colors.primary }]} onPress={onFinish} activeOpacity={0.86}>
|
|
<Text style={[styles.continueCtaText, { color: colors.onPrimary }]}>{copy.continue}</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const renderMessageItem = ({ item }: { item: Message }) => {
|
|
const isBot = item.sender === 'bot';
|
|
return (
|
|
<View style={[styles.messageRow, isBot ? styles.messageRowLeft : styles.messageRowRight]}>
|
|
{isBot && (
|
|
<Image source={BOT_AVATAR} style={styles.chatAvatar} resizeMode="cover" />
|
|
)}
|
|
<View style={[
|
|
styles.bubble,
|
|
isBot
|
|
? [styles.bubbleLeft, { backgroundColor: '#142215' }]
|
|
: [styles.bubbleRight, { backgroundColor: '#2e4f00' }]
|
|
]}>
|
|
<Text style={[styles.bubbleText, { color: isBot ? '#dae6d6' : '#ffffff' }]}>{item.text}</Text>
|
|
{item.subtext ? (
|
|
<Text style={styles.bubbleSub}>{item.subtext}</Text>
|
|
) : null}
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const progressPercent = `${Math.min(currentStep * 25, 100)}%`;
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
style={styles.container}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
>
|
|
<Animated.Image
|
|
source={{ uri: 'https://lh3.googleusercontent.com/aida/AP1WRLvNv80S62DEz_zBQ-9HwoEIRjqH2RBaY0knbsg2dBsQySI1kJC23xC8q_rkQ_100IiLnW4uwtzj0t7koHQvFUc6-4kyGCdyYdE7JY_nMeC628hnY7KYw52GNy1ErcjFii-YOISxq1GRLNpaFN_IklHttmJ30NC85GX4pCt7Q23hrYSQ_Mfx4_T1lqXum2KdLfiyyOZoRKuLCP0jTBbAcEi8DCLZtsAUSYcttqmUtuaTMnt5W07_sIn0dAw' }}
|
|
style={[styles.robotBackground, { transform: [{ translateY: floatAnim }] }]}
|
|
resizeMode="cover"
|
|
/>
|
|
{/* Soft stepwise gradient mask to blend bottom image edge with background */}
|
|
<View style={styles.maskContainer}>
|
|
<View style={[styles.maskLine, { opacity: 0.1, backgroundColor: '#131e14' }]} />
|
|
<View style={[styles.maskLine, { opacity: 0.25, backgroundColor: '#131e14' }]} />
|
|
<View style={[styles.maskLine, { opacity: 0.45, backgroundColor: '#131e14' }]} />
|
|
<View style={[styles.maskLine, { opacity: 0.65, backgroundColor: '#131e14' }]} />
|
|
<View style={[styles.maskLine, { opacity: 0.85, backgroundColor: '#131e14' }]} />
|
|
<View style={[styles.maskLine, { opacity: 1.0, backgroundColor: '#131e14' }]} />
|
|
</View>
|
|
|
|
<SafeAreaView style={styles.safe} edges={['top', 'bottom']}>
|
|
{/* Header Bar */}
|
|
<View style={styles.header}>
|
|
<TouchableOpacity onPress={handleBack} style={styles.headerBack}>
|
|
<Ionicons name="arrow-back" size={24} color="#dae6d6" />
|
|
</TouchableOpacity>
|
|
|
|
<View style={styles.progressContainer}>
|
|
<View style={styles.progressBarTrack}>
|
|
<View style={[styles.progressBarFill, { width: progressPercent as any }]} />
|
|
</View>
|
|
</View>
|
|
|
|
<TouchableOpacity onPress={onSkip} style={styles.headerSkip}>
|
|
<Text style={styles.skipText}>{copy.skip}</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Chat History Panel */}
|
|
<View style={styles.chatPanel}>
|
|
<FlatList
|
|
ref={flatListRef}
|
|
data={messages}
|
|
keyExtractor={(item) => item.id}
|
|
renderItem={renderMessageItem}
|
|
contentContainerStyle={styles.chatListContent}
|
|
showsVerticalScrollIndicator={false}
|
|
ListHeaderComponent={
|
|
<View style={styles.timeDivider}>
|
|
<Text style={styles.timeDividerText}>{copy.today}</Text>
|
|
</View>
|
|
}
|
|
ListFooterComponent={
|
|
isTyping ? (
|
|
<View style={[styles.messageRow, styles.messageRowLeft]}>
|
|
<Image source={BOT_AVATAR} style={styles.chatAvatar} resizeMode="cover" />
|
|
<View style={[styles.bubble, styles.bubbleLeft, { backgroundColor: '#142215' }]}>
|
|
<TypingIndicator colors={colors} />
|
|
</View>
|
|
</View>
|
|
) : null
|
|
}
|
|
/>
|
|
</View>
|
|
|
|
{/* Input/Selection Panel */}
|
|
<View style={styles.inputPanel}>
|
|
{renderOptions()}
|
|
</View>
|
|
</SafeAreaView>
|
|
</KeyboardAvoidingView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#131e14',
|
|
},
|
|
robotBackground: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
width: '100%',
|
|
height: 380,
|
|
opacity: 0.28,
|
|
},
|
|
maskContainer: {
|
|
position: 'absolute',
|
|
top: 260,
|
|
left: 0,
|
|
right: 0,
|
|
height: 120,
|
|
},
|
|
maskLine: {
|
|
height: 20,
|
|
width: '100%',
|
|
},
|
|
|
|
optionRowLeft: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
safe: {
|
|
flex: 1,
|
|
justifyContent: 'space-between',
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 12,
|
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
borderBottomColor: 'rgba(255,255,255,0.08)',
|
|
},
|
|
headerBack: {
|
|
width: 38,
|
|
height: 38,
|
|
borderRadius: 19,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: 'rgba(255,255,255,0.04)',
|
|
},
|
|
progressContainer: {
|
|
flex: 1,
|
|
marginHorizontal: 16,
|
|
},
|
|
progressBarTrack: {
|
|
height: 6,
|
|
borderRadius: 3,
|
|
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
overflow: 'hidden',
|
|
},
|
|
progressBarFill: {
|
|
height: 6,
|
|
borderRadius: 3,
|
|
backgroundColor: '#a5d56e',
|
|
},
|
|
headerSkip: {
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 6,
|
|
borderRadius: 14,
|
|
backgroundColor: 'rgba(255,255,255,0.04)',
|
|
},
|
|
skipText: {
|
|
color: '#dae6d6',
|
|
fontSize: 13,
|
|
fontWeight: '700',
|
|
},
|
|
chatPanel: {
|
|
flex: 1,
|
|
paddingHorizontal: 16,
|
|
},
|
|
chatListContent: {
|
|
paddingVertical: 16,
|
|
gap: 16,
|
|
},
|
|
timeDivider: {
|
|
alignSelf: 'center',
|
|
backgroundColor: 'rgba(255,255,255,0.05)',
|
|
borderRadius: 999,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 4,
|
|
marginBottom: 16,
|
|
},
|
|
timeDividerText: {
|
|
color: '#8d9382',
|
|
fontSize: 11,
|
|
fontWeight: '700',
|
|
},
|
|
messageRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-end',
|
|
gap: 10,
|
|
marginVertical: 4,
|
|
},
|
|
messageRowLeft: {
|
|
alignSelf: 'flex-start',
|
|
maxWidth: '85%',
|
|
},
|
|
messageRowRight: {
|
|
alignSelf: 'flex-end',
|
|
flexDirection: 'row-reverse',
|
|
maxWidth: '85%',
|
|
},
|
|
chatAvatar: {
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 16,
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(255,255,255,0.1)',
|
|
},
|
|
bubble: {
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 12,
|
|
borderRadius: 18,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 1 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 2,
|
|
elevation: 1,
|
|
},
|
|
bubbleLeft: {
|
|
borderBottomLeftRadius: 3,
|
|
},
|
|
bubbleRight: {
|
|
borderBottomRightRadius: 3,
|
|
},
|
|
bubbleText: {
|
|
fontSize: 15,
|
|
lineHeight: 21,
|
|
fontWeight: '600',
|
|
},
|
|
bubbleSub: {
|
|
fontSize: 12,
|
|
marginTop: 4,
|
|
color: '#8d9382',
|
|
},
|
|
typingContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 4,
|
|
width: 36,
|
|
height: 18,
|
|
justifyContent: 'center',
|
|
},
|
|
typingDot: {
|
|
width: 6,
|
|
height: 6,
|
|
borderRadius: 3,
|
|
},
|
|
inputPanel: {
|
|
borderTopWidth: StyleSheet.hairlineWidth,
|
|
borderTopColor: 'rgba(255,255,255,0.08)',
|
|
paddingTop: 12,
|
|
paddingBottom: 24,
|
|
paddingHorizontal: 16,
|
|
backgroundColor: '#131e14',
|
|
},
|
|
optionsWrap: {
|
|
gap: 10,
|
|
},
|
|
stepTitle: {
|
|
color: '#8d9382',
|
|
fontSize: 12,
|
|
fontWeight: '800',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.8,
|
|
marginBottom: 4,
|
|
textAlign: 'center',
|
|
},
|
|
optionsGrid: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
gap: 8,
|
|
justifyContent: 'center',
|
|
},
|
|
pillBtn: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
paddingHorizontal: 14,
|
|
paddingVertical: 10,
|
|
borderRadius: 999,
|
|
borderWidth: 1.5,
|
|
backgroundColor: '#101e11',
|
|
},
|
|
pillEmoji: {
|
|
fontSize: 14,
|
|
},
|
|
pillLabel: {
|
|
fontSize: 13.5,
|
|
fontWeight: '700',
|
|
},
|
|
optionsVertical: {
|
|
gap: 8,
|
|
},
|
|
optionRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 14,
|
|
borderRadius: 14,
|
|
borderWidth: 1,
|
|
backgroundColor: '#101e11',
|
|
},
|
|
rowText: {
|
|
fontSize: 14.5,
|
|
fontWeight: '700',
|
|
},
|
|
bentoOption: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 12,
|
|
borderRadius: 14,
|
|
borderWidth: 1,
|
|
backgroundColor: '#101e11',
|
|
},
|
|
bentoCopy: {
|
|
flex: 1,
|
|
gap: 2,
|
|
marginRight: 12,
|
|
},
|
|
bentoTitle: {
|
|
fontSize: 15,
|
|
fontWeight: '700',
|
|
},
|
|
bentoSub: {
|
|
fontSize: 11.5,
|
|
fontWeight: '600',
|
|
lineHeight: 15,
|
|
},
|
|
footerWrap: {
|
|
paddingVertical: 8,
|
|
},
|
|
continueCta: {
|
|
height: 56,
|
|
borderRadius: 14,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
continueCtaText: {
|
|
fontSize: 16.5,
|
|
fontWeight: '900',
|
|
},
|
|
});
|