225 lines
8.1 KiB
TypeScript
225 lines
8.1 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Pet, AnalysisResult, PetTimelineItem, DictionarySignal, TalkBackLog } from './types';
|
|
import { INITIAL_PET_LUNA, INITIAL_PET_MILO, INITIAL_TIMELINE_LUNA, INITIAL_DICTIONARY_LUNA } from './data/seedData';
|
|
import { Navigation } from './components/Navigation';
|
|
import { LandingPage } from './components/LandingPage';
|
|
import { PublicLandingPage } from './components/PublicLandingPage';
|
|
import { OnboardingFlow } from './components/OnboardingFlow';
|
|
import { LiveListenScreen } from './components/LiveListenScreen';
|
|
import { TalkBackScreen } from './components/TalkBackScreen';
|
|
import { PetTimeline } from './components/PetTimeline';
|
|
import { InsightsScreen } from './components/InsightsScreen';
|
|
import { AuthPage } from './components/AuthPage';
|
|
import { useAuth } from './context/AuthContext';
|
|
|
|
export default function App() {
|
|
const { user, isLoading, logout } = useAuth();
|
|
const [pets, setPets] = useState<Pet[]>([INITIAL_PET_LUNA, INITIAL_PET_MILO]);
|
|
const [selectedPetId, setSelectedPetId] = useState<string>(INITIAL_PET_LUNA.id);
|
|
const [currentView, setCurrentView] = useState<'landing' | 'onboarding' | 'listen' | 'talkback' | 'timeline' | 'insights' | 'auth'>('landing');
|
|
const [authMode, setAuthMode] = useState<'login' | 'register'>('register');
|
|
|
|
const [timelineItems, setTimelineItems] = useState<PetTimelineItem[]>(INITIAL_TIMELINE_LUNA);
|
|
const [dictionarySignals, setDictionarySignals] = useState<DictionarySignal[]>(INITIAL_DICTIONARY_LUNA);
|
|
|
|
const selectedPet = pets.find(p => p.id === selectedPetId) || pets[0];
|
|
|
|
// Show loading spinner while checking auth
|
|
if (isLoading) {
|
|
return (
|
|
<div className="min-h-screen bg-[#F9F7F2] flex items-center justify-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#F08C7D]"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── UNAUTHENTICATED FLOW ──
|
|
if (!user) {
|
|
// User clicked "Get Started" or "Sign In" → show auth form
|
|
if (currentView === 'auth') {
|
|
return (
|
|
<AuthPage
|
|
initialMode={authMode}
|
|
onBack={() => {
|
|
setCurrentView('landing');
|
|
setAuthMode('register');
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
// Default: show public landing page
|
|
return (
|
|
<PublicLandingPage
|
|
onGetStarted={() => {
|
|
setAuthMode('register');
|
|
setCurrentView('auth');
|
|
}}
|
|
onSignIn={() => {
|
|
setAuthMode('login');
|
|
setCurrentView('auth');
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// ── AUTHENTICATED FLOW ──
|
|
// Callback when analysis completes from LiveListenScreen
|
|
const handleAnalysisComplete = (result: AnalysisResult) => {
|
|
const newTimelineItem: PetTimelineItem = {
|
|
id: `tl_${Date.now()}`,
|
|
petId: result.petId,
|
|
type: 'interpretation',
|
|
title: result.primaryIntent.title,
|
|
description: result.reasoning.summary,
|
|
timestamp: result.timestamp,
|
|
intentCategory: result.primaryIntent.category,
|
|
confidence: result.primaryIntent.confidence,
|
|
feedbackStatus: 'pending',
|
|
zoneName: result.zoneName
|
|
};
|
|
|
|
setTimelineItems(prev => [newTimelineItem, ...prev]);
|
|
};
|
|
|
|
// Callback when pet model updates from training
|
|
const handleUpdatePet = (updatedPet: Pet) => {
|
|
setPets(prev => prev.map(p => p.id === updatedPet.id ? updatedPet : p));
|
|
};
|
|
|
|
// Callback when Talk Back reaction is logged
|
|
const handleLogTalkBackReaction = (log: TalkBackLog) => {
|
|
const newTimelineItem: PetTimelineItem = {
|
|
id: `tl_tb_${Date.now()}`,
|
|
petId: log.petId,
|
|
type: 'talk_back',
|
|
title: `Played "${log.messageLabel}" Tone`,
|
|
description: `Pet reaction observed: ${log.petReaction ? log.petReaction.replace('_', ' ') : 'played'}`,
|
|
timestamp: log.timestamp
|
|
};
|
|
|
|
setTimelineItems(prev => [newTimelineItem, ...prev]);
|
|
};
|
|
|
|
// Callback when manual event is added
|
|
const handleAddManualEvent = (event: PetTimelineItem) => {
|
|
setTimelineItems(prev => [event, ...prev]);
|
|
};
|
|
|
|
// Callback when onboarding completes
|
|
const handleCompleteOnboarding = (newPet: Pet) => {
|
|
setPets(prev => [...prev, newPet]);
|
|
setSelectedPetId(newPet.id);
|
|
setCurrentView('listen');
|
|
};
|
|
|
|
// Callback to reset pet ML model
|
|
const handleResetPetModel = () => {
|
|
setPets(prev => prev.map(p => {
|
|
if (p.id === selectedPetId) {
|
|
return { ...p, confirmedSampleCount: 0, overallAccuracy: 75 };
|
|
}
|
|
return p;
|
|
}));
|
|
alert(`${selectedPet.name}'s profile memory has been reset.`);
|
|
};
|
|
|
|
// Callback to delete pet account
|
|
const handleDeletePetAccount = () => {
|
|
const remainingPets = pets.filter(p => p.id !== selectedPetId);
|
|
if (remainingPets.length > 0) {
|
|
setPets(remainingPets);
|
|
setSelectedPetId(remainingPets[0].id);
|
|
setCurrentView('landing');
|
|
} else {
|
|
// Re-seed Luna if all pets deleted
|
|
setPets([INITIAL_PET_LUNA]);
|
|
setSelectedPetId(INITIAL_PET_LUNA.id);
|
|
setCurrentView('landing');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#F9F7F2] text-[#1A2A40] font-sans antialiased selection:bg-[#F08C7D]/30 selection:text-[#1A2A40]">
|
|
|
|
{/* Global Responsive Navigation Header & Bottom Bar */}
|
|
<Navigation
|
|
currentView={currentView}
|
|
setCurrentView={setCurrentView}
|
|
pets={pets}
|
|
selectedPet={selectedPet}
|
|
setSelectedPetId={setSelectedPetId}
|
|
onOpenAddPet={() => setCurrentView('onboarding')}
|
|
/>
|
|
|
|
{/* Main Content View Switcher */}
|
|
<main className="max-w-5xl mx-auto px-4 py-6 sm:py-8">
|
|
{currentView === 'landing' && (
|
|
<LandingPage
|
|
selectedPet={selectedPet}
|
|
onStartListen={() => setCurrentView('listen')}
|
|
onStartOnboarding={() => setCurrentView('onboarding')}
|
|
/>
|
|
)}
|
|
|
|
{currentView === 'onboarding' && (
|
|
<OnboardingFlow
|
|
onComplete={handleCompleteOnboarding}
|
|
onCancel={() => setCurrentView('landing')}
|
|
/>
|
|
)}
|
|
|
|
{currentView === 'listen' && (
|
|
<LiveListenScreen
|
|
selectedPet={selectedPet}
|
|
onAnalysisComplete={handleAnalysisComplete}
|
|
onUpdatePet={handleUpdatePet}
|
|
/>
|
|
)}
|
|
|
|
{currentView === 'talkback' && (
|
|
<TalkBackScreen
|
|
selectedPet={selectedPet}
|
|
onLogTalkBackReaction={handleLogTalkBackReaction}
|
|
/>
|
|
)}
|
|
|
|
{currentView === 'timeline' && (
|
|
<PetTimeline
|
|
selectedPet={selectedPet}
|
|
timelineItems={timelineItems}
|
|
onAddManualEvent={handleAddManualEvent}
|
|
/>
|
|
)}
|
|
|
|
{currentView === 'insights' && (
|
|
<InsightsScreen
|
|
selectedPet={selectedPet}
|
|
dictionarySignals={dictionarySignals}
|
|
onResetPetModel={handleResetPetModel}
|
|
onDeletePetAccount={handleDeletePetAccount}
|
|
/>
|
|
)}
|
|
</main>
|
|
|
|
{/* Footer */}
|
|
<footer className="border-t border-[#E5DFD3] py-8 bg-[#F4EFEC]/60 text-center text-xs text-[#5A6E85] space-y-2">
|
|
<div className="max-w-4xl mx-auto px-4 flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-serif font-bold text-[#1A2A40] text-sm tracking-tight">PetTalk</span>
|
|
<span>• Probabilistic AI Pet Behavioral Interpretation</span>
|
|
</div>
|
|
<div className="flex items-center gap-4 text-[#5A6E85]">
|
|
<button onClick={() => setCurrentView('landing')} className="hover:text-[#1A2A40] transition-colors cursor-pointer">About</button>
|
|
<button onClick={() => setCurrentView('insights')} className="hover:text-[#1A2A40] transition-colors cursor-pointer">Privacy & Data</button>
|
|
<button onClick={() => setCurrentView('onboarding')} className="hover:text-[#1A2A40] transition-colors cursor-pointer">Add Pet</button>
|
|
<button onClick={logout} className="hover:text-[#F08C7D] transition-colors cursor-pointer font-semibold">
|
|
Sign Out ({user?.username})
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
</div>
|
|
);
|
|
}
|