Snapshot: full project state

This commit is contained in:
2026-10-06 23:43:43 -07:00
commit fc6d00988c
13 changed files with 1283 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
__pycache__/
*.pyc
node_modules/
.venv/
venv/
.env
*.db
*.sqlite*
*.log
.DS_Store
out/
work/
.pio/
briefs/
dns-backup/
archive/
*.png

7
README.md Normal file
View File

@@ -0,0 +1,7 @@
# PetTalk
React Native / Expo app skeleton: pet-community app with auth (context +
provider), auth pages v1/v2, Express-style server with auth routes and SQLite
DB layer. Screen iterations (`pettalk-App-new.tsx`, `-v2.tsx`) and live-import
shims included.

197
pettalk-App-new.tsx Normal file
View File

@@ -0,0 +1,197 @@
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 { 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'>('landing');
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>
);
}
// Show auth page if not authenticated
if (!user) {
return <AuthPage />;
}
// 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>
);
}

224
pettalk-App-v2.tsx Normal file
View File

@@ -0,0 +1,224 @@
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>
);
}

109
pettalk-AuthContext.tsx Normal file
View File

@@ -0,0 +1,109 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from "react";
interface User {
id: number;
username: string;
email: string;
created_at: string;
}
interface AuthContextType {
user: User | null;
token: string | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
register: (username: string, email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | null>(null);
const API_BASE = "";
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(() => localStorage.getItem("pettalk_token"));
const [isLoading, setIsLoading] = useState(true);
const fetchMe = useCallback(async (t: string) => {
try {
const res = await fetch("/api/me", {
headers: { Authorization: `Bearer ${t}` },
});
if (res.ok) {
const data = await res.json();
setUser(data.user);
} else {
localStorage.removeItem("pettalk_token");
setToken(null);
}
} catch {
// Network error — keep token but mark as loaded
}
}, []);
useEffect(() => {
if (token) {
fetchMe(token).finally(() => setIsLoading(false));
} else {
setIsLoading(false);
}
}, [token, fetchMe]);
const login = async (email: string, password: string) => {
const res = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Login failed");
}
localStorage.setItem("pettalk_token", data.token);
setToken(data.token);
setUser(data.user);
};
const register = async (username: string, email: string, password: string) => {
const res = await fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, email, password }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Registration failed");
}
localStorage.setItem("pettalk_token", data.token);
setToken(data.token);
setUser(data.user);
};
const logout = () => {
localStorage.removeItem("pettalk_token");
setToken(null);
setUser(null);
};
return (
<AuthContext.Provider value={{ user, token, isLoading, login, register, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth(): AuthContextType {
const ctx = useContext(AuthContext);
if (!ctx) {
throw new Error("useAuth must be used within an AuthProvider");
}
return ctx;
}
/** Helper to get auth headers for fetch calls */
export function getAuthHeaders(): Record<string, string> {
const token = localStorage.getItem("pettalk_token");
if (!token) return {};
return { Authorization: `Bearer ${token}` };
}

176
pettalk-AuthPage-v2.tsx Normal file
View File

@@ -0,0 +1,176 @@
import React, { useState, useEffect } from "react";
import { Sparkles, Mail, Lock, User, PawPrint, ArrowLeft } from "lucide-react";
import { useAuth } from "../context/AuthContext";
interface AuthPageProps {
initialMode?: "login" | "register";
onBack?: () => void;
}
export const AuthPage: React.FC<AuthPageProps> = ({ initialMode = "login", onBack }) => {
const { login, register } = useAuth();
const [isRegistering, setIsRegistering] = useState(initialMode === "register");
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
// Sync with initialMode if it changes externally
useEffect(() => {
setIsRegistering(initialMode === "register");
}, [initialMode]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
try {
if (isRegistering) {
await register(username, email, password);
} else {
await login(email, password);
}
} catch (err: any) {
setError(err.message || "Something went wrong");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-[#F9F7F2] flex items-center justify-center px-4">
<div className="w-full max-w-md">
{/* Back button */}
{onBack && (
<button
onClick={onBack}
className="mb-4 flex items-center gap-2 text-sm text-[#5A6E85] hover:text-[#1A2A40] transition-colors cursor-pointer"
>
<ArrowLeft className="w-4 h-4" />
Back to PetTAlk
</button>
)}
{/* Logo */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-[#1A2A40] text-[#F9F7F2] shadow-lg mb-4">
<Sparkles className="w-8 h-8 text-[#F08C7D]" />
</div>
<h1 className="font-serif text-3xl font-bold text-[#1A2A40] tracking-tight">
PetTalk
</h1>
<p className="text-[#5A6E85] text-sm mt-1">
Understanding pet intent softly
</p>
</div>
{/* Form Card */}
<div className="bg-white rounded-3xl border border-[#E5DFD3] shadow-2xs p-8">
<div className="flex items-center justify-center gap-2 mb-6">
<PawPrint className="w-5 h-5 text-[#F08C7D]" />
<h2 className="font-serif text-xl font-bold text-[#1A2A40]">
{isRegistering ? "Create Account" : "Welcome Back"}
</h2>
</div>
{error && (
<div className="mb-4 p-3 rounded-xl bg-red-50 border border-red-200 text-red-700 text-sm">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
{isRegistering && (
<div>
<label className="block text-xs font-semibold text-[#4A5D78] mb-1.5">
Username
</label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#5A6E85]" />
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Your name"
required
className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-[#E5DFD3] bg-[#F9F7F2] text-[#1A2A40] text-sm placeholder:text-[#5A6E85] focus:outline-none focus:ring-2 focus:ring-[#F08C7D]/40 focus:border-[#F08C7D]/50 transition-all"
/>
</div>
</div>
)}
<div>
<label className="block text-xs font-semibold text-[#4A5D78] mb-1.5">
Email
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#5A6E85]" />
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-[#E5DFD3] bg-[#F9F7F2] text-[#1A2A40] text-sm placeholder:text-[#5A6E85] focus:outline-none focus:ring-2 focus:ring-[#F08C7D]/40 focus:border-[#F08C7D]/50 transition-all"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-[#4A5D78] mb-1.5">
Password
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#5A6E85]" />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Min 6 characters"
required
minLength={6}
className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-[#E5DFD3] bg-[#F9F7F2] text-[#1A2A40] text-sm placeholder:text-[#5A6E85] focus:outline-none focus:ring-2 focus:ring-[#F08C7D]/40 focus:border-[#F08C7D]/50 transition-all"
/>
</div>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-3 rounded-2xl bg-[#F08C7D] hover:bg-[#E57B6C] text-white font-semibold text-sm flex items-center justify-center gap-2 transition-all disabled:opacity-50 cursor-pointer"
>
{loading ? (
<span className="animate-pulse">Please wait...</span>
) : isRegistering ? (
"Create Account"
) : (
"Sign In"
)}
</button>
</form>
<div className="mt-6 text-center">
<button
onClick={() => {
setIsRegistering(!isRegistering);
setError("");
}}
className="text-sm text-[#5A6E85] hover:text-[#1A2A40] underline underline-offset-2 cursor-pointer transition-colors"
>
{isRegistering
? "Already have an account? Sign in"
: "Don't have an account? Create one"}
</button>
</div>
</div>
{/* Footer */}
<p className="text-center text-[10px] text-[#5A6E85] mt-6">
PetTalk AI · Probabilistic Pet Behavioral Interpretation
</p>
</div>
</div>
);
};

155
pettalk-AuthPage.tsx Normal file
View File

@@ -0,0 +1,155 @@
import React, { useState } from "react";
import { Sparkles, Mail, Lock, User, PawPrint } from "lucide-react";
import { useAuth } from "../context/AuthContext";
export const AuthPage: React.FC = () => {
const { login, register } = useAuth();
const [isRegistering, setIsRegistering] = useState(false);
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
try {
if (isRegistering) {
await register(username, email, password);
} else {
await login(email, password);
}
} catch (err: any) {
setError(err.message || "Something went wrong");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-[#F9F7F2] flex items-center justify-center px-4">
<div className="w-full max-w-md">
{/* Logo */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-[#1A2A40] text-[#F9F7F2] shadow-lg mb-4">
<Sparkles className="w-8 h-8 text-[#F08C7D]" />
</div>
<h1 className="font-serif text-3xl font-bold text-[#1A2A40] tracking-tight">
PetTalk
</h1>
<p className="text-[#5A6E85] text-sm mt-1">
Understanding pet intent softly
</p>
</div>
{/* Form Card */}
<div className="bg-white rounded-3xl border border-[#E5DFD3] shadow-2xs p-8">
<div className="flex items-center justify-center gap-2 mb-6">
<PawPrint className="w-5 h-5 text-[#F08C7D]" />
<h2 className="font-serif text-xl font-bold text-[#1A2A40]">
{isRegistering ? "Create Account" : "Welcome Back"}
</h2>
</div>
{error && (
<div className="mb-4 p-3 rounded-xl bg-red-50 border border-red-200 text-red-700 text-sm">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
{isRegistering && (
<div>
<label className="block text-xs font-semibold text-[#4A5D78] mb-1.5">
Username
</label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#5A6E85]" />
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Your name"
required
className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-[#E5DFD3] bg-[#F9F7F2] text-[#1A2A40] text-sm placeholder:text-[#5A6E85] focus:outline-none focus:ring-2 focus:ring-[#F08C7D]/40 focus:border-[#F08C7D]/50 transition-all"
/>
</div>
</div>
)}
<div>
<label className="block text-xs font-semibold text-[#4A5D78] mb-1.5">
Email
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#5A6E85]" />
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-[#E5DFD3] bg-[#F9F7F2] text-[#1A2A40] text-sm placeholder:text-[#5A6E85] focus:outline-none focus:ring-2 focus:ring-[#F08C7D]/40 focus:border-[#F08C7D]/50 transition-all"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-[#4A5D78] mb-1.5">
Password
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#5A6E85]" />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Min 6 characters"
required
minLength={6}
className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-[#E5DFD3] bg-[#F9F7F2] text-[#1A2A40] text-sm placeholder:text-[#5A6E85] focus:outline-none focus:ring-2 focus:ring-[#F08C7D]/40 focus:border-[#F08C7D]/50 transition-all"
/>
</div>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-3 rounded-2xl bg-[#F08C7D] hover:bg-[#E57B6C] text-white font-semibold text-sm flex items-center justify-center gap-2 transition-all disabled:opacity-50 cursor-pointer"
>
{loading ? (
<span className="animate-pulse">Please wait...</span>
) : isRegistering ? (
"Create Account"
) : (
"Sign In"
)}
</button>
</form>
<div className="mt-6 text-center">
<button
onClick={() => {
setIsRegistering(!isRegistering);
setError("");
}}
className="text-sm text-[#5A6E85] hover:text-[#1A2A40] underline underline-offset-2 cursor-pointer transition-colors"
>
{isRegistering
? "Already have an account? Sign in"
: "Don't have an account? Create one"}
</button>
</div>
</div>
{/* Footer */}
<p className="text-center text-[10px] text-[#5A6E85] mt-6">
PetTalk AI · Probabilistic Pet Behavioral Interpretation
</p>
</div>
</div>
);
};

113
pettalk-auth-db.ts Normal file
View File

@@ -0,0 +1,113 @@
import fs from "fs";
import path from "path";
import bcrypt from "bcryptjs";
const DATA_DIR = path.join(process.cwd(), "data");
const USERS_FILE = path.join(DATA_DIR, "users.json");
export interface UserRow {
id: number;
username: string;
email: string;
password_hash: string;
created_at: string;
}
export interface UserPublic {
id: number;
username: string;
email: string;
created_at: string;
}
function ensureDataDir() {
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
}
function readUsers(): UserRow[] {
ensureDataDir();
if (!fs.existsSync(USERS_FILE)) {
return [];
}
try {
const raw = fs.readFileSync(USERS_FILE, "utf-8");
return JSON.parse(raw);
} catch {
return [];
}
}
function writeUsers(users: UserRow[]) {
ensureDataDir();
fs.writeFileSync(USERS_FILE, JSON.stringify(users, null, 2), "utf-8");
}
let nextId = 0;
function getNextId(): number {
if (nextId === 0) {
const users = readUsers();
nextId = users.reduce((max, u) => Math.max(max, u.id), 0) + 1;
}
return nextId++;
}
export function createUser(username: string, email: string, password: string): UserPublic {
const users = readUsers();
if (users.find((u) => u.email === email || u.username === username)) {
throw new Error("A user with that email or username already exists.");
}
const password_hash = bcrypt.hashSync(password, 10);
const newUser: UserRow = {
id: getNextId(),
username,
email,
password_hash,
created_at: new Date().toISOString(),
};
users.push(newUser);
writeUsers(users);
return {
id: newUser.id,
username: newUser.username,
email: newUser.email,
created_at: newUser.created_at,
};
}
export function authenticateUser(email: string, password: string): UserPublic {
const users = readUsers();
const user = users.find((u) => u.email === email);
if (!user) {
throw new Error("Invalid email or password.");
}
if (!bcrypt.compareSync(password, user.password_hash)) {
throw new Error("Invalid email or password.");
}
return {
id: user.id,
username: user.username,
email: user.email,
created_at: user.created_at,
};
}
export function getUserById(id: number): UserPublic | null {
const users = readUsers();
const user = users.find((u) => u.id === id);
if (!user) return null;
return {
id: user.id,
username: user.username,
email: user.email,
created_at: user.created_at,
};
}

View File

@@ -0,0 +1,42 @@
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
const JWT_SECRET = process.env.JWT_SECRET || "pettalk-dev-secret-change-in-production";
// Public routes that don't require authentication
const PUBLIC_ROUTES = [
{ method: "POST", path: "/api/register" },
{ method: "POST", path: "/api/login" },
{ method: "GET", path: "/api/health" },
];
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
// Skip auth for public routes
const isPublic = PUBLIC_ROUTES.some(
(r) => r.method === req.method && req.path === r.path
);
if (isPublic) {
return next();
}
// Also skip non-API routes (static files, SPA routes)
if (!req.path.startsWith("/api/")) {
return next();
}
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ error: "Authentication required." });
}
const token = authHeader.split(" ")[1];
try {
const decoded = jwt.verify(token, JWT_SECRET) as { userId: number; email: string };
(req as any).userId = decoded.userId;
(req as any).userEmail = decoded.email;
next();
} catch (err) {
return res.status(401).json({ error: "Invalid or expired token." });
}
}

83
pettalk-auth-routes.ts Normal file
View File

@@ -0,0 +1,83 @@
import { Router, Request, Response } from "express";
import jwt from "jsonwebtoken";
import { createUser, authenticateUser, getUserById, UserPublic } from "./database.js";
const JWT_SECRET = process.env.JWT_SECRET || "pettalk-dev-secret-change-in-production";
const TOKEN_EXPIRY = "7d";
const router = Router();
function generateToken(user: UserPublic): string {
return jwt.sign({ userId: user.id, email: user.email }, JWT_SECRET, {
expiresIn: TOKEN_EXPIRY,
});
}
// POST /api/register
router.post("/register", (req: Request, res: Response) => {
try {
const { username, email, password } = req.body;
if (!username || !email || !password) {
return res.status(400).json({ error: "Username, email, and password are required." });
}
if (password.length < 6) {
return res.status(400).json({ error: "Password must be at least 6 characters." });
}
const user = createUser(username, email, password);
const token = generateToken(user);
return res.status(201).json({ user, token });
} catch (err: any) {
if (err.message?.includes("already exists")) {
return res.status(409).json({ error: err.message });
}
console.error("Register error:", err);
return res.status(500).json({ error: "Registration failed." });
}
});
// POST /api/login
router.post("/login", (req: Request, res: Response) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: "Email and password are required." });
}
const user = authenticateUser(email, password);
const token = generateToken(user);
return res.json({ user, token });
} catch (err: any) {
if (err.message?.includes("Invalid")) {
return res.status(401).json({ error: err.message });
}
console.error("Login error:", err);
return res.status(500).json({ error: "Login failed." });
}
});
// GET /api/me — protected by authMiddleware
router.get("/me", (req: Request, res: Response) => {
try {
const userId = (req as any).userId;
if (!userId) {
return res.status(401).json({ error: "Authentication required." });
}
const user = getUserById(userId);
if (!user) {
return res.status(404).json({ error: "User not found." });
}
return res.json({ user });
} catch (err: any) {
return res.status(500).json({ error: "Failed to fetch user." });
}
});
export default router;

5
pettalk-live-imports.tsx Normal file
View File

@@ -0,0 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
import { Pet, AnalysisResult } from '../types';
import { audioService } from '../lib/audioService';
import { getAuthHeaders } from '../context/AuthContext';
import { Mic, MicOff, Camera, CameraOff, Check, X, MessageSquare, Sparkles, AlertCircle, RefreshCw, MapPin, Volume2 } from 'lucide-react';

13
pettalk-main-new.tsx Normal file
View File

@@ -0,0 +1,13 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import { AuthProvider } from './context/AuthContext';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</StrictMode>,
);

142
pettalk-server-new.ts Normal file
View File

@@ -0,0 +1,142 @@
import express from "express";
import path from "path";
import { createServer as createViteServer } from "vite";
import { GoogleGenAI } from "@google/genai";
import { defaultMLProvider } from "./src/lib/mlProvider";
import { Pet } from "./src/types";
import { authMiddleware } from "./src/auth/middleware";
import authRouter from "./src/auth/routes";
async function startServer() {
const app = express();
const PORT = 3000;
app.use(express.json({ limit: "10mb" }));
// Auth middleware — protects /api/* except register, login, health
app.use(authMiddleware);
// Auth routes (register, login, me)
app.use("/api", authRouter);
// API Routes
app.get("/api/health", (_req, res) => {
res.json({ status: "ok", app: "PetTalk", timestamp: new Date().toISOString() });
});
/**
* Analyze Pet Signal API endpoint.
* Leverages Gemini API when GEMINI_API_KEY is available, or uses local ML Provider.
*/
app.post("/api/analyze", async (req, res) => {
try {
const { pet, selectedZoneId, audio, video, customNote } = req.body as {
pet: Pet;
selectedZoneId?: string;
audio?: any;
video?: any;
customNote?: string;
};
if (!pet) {
return res.status(400).json({ error: "Pet profile is required for analysis." });
}
// Base ML prediction
const baseResult = defaultMLProvider.combineSignals({
pet,
timeOfDay: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
selectedZoneId,
audio,
video,
customNote
});
// If GEMINI_API_KEY is configured, enrich interpretation reasoning with Gemini
if (process.env.GEMINI_API_KEY) {
try {
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const zoneObj = pet.homeZones.find(z => z.id === selectedZoneId) || pet.homeZones[0];
const zoneName = zoneObj ? zoneObj.name : "Home Area";
const prompt = `You are a certified veterinary behaviorist and pet communication expert assisting a pet owner on PetTalk.
Pet Name: ${pet.name}
Species: ${pet.species}
Breed: ${pet.breed}
Age: ${pet.ageYears} years old
Personality: ${pet.personality.join(", ")}
Feeding Schedule: ${pet.feedingSchedule}
Current Home Zone: ${zoneName}
Acoustic Trait Detected: ${audio?.pitchVariation || 'rising pitch'} sound (${audio?.averageVolume ? Math.round(audio.averageVolume * 100) : 50}% volume)
Primary Predicted Intent: ${baseResult.primaryIntent.title} (${baseResult.primaryIntent.confidence}% confidence)
Write a short, empathetic, scientifically grounded "Why" reasoning breakdown (maximum 3 sentences).
Follow this STRICT honesty rule:
- Frame everything as a probabilistic likelihood based on sound, body language, routine, location, and owner feedback.
- NEVER claim pets speak English or that this is a literal translation.
- Maintain a warm, modern, intelligent tone.
- Do NOT make medical diagnoses or claims.`;
const response = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: prompt
});
if (response.text) {
baseResult.reasoning.summary = response.text.trim();
}
} catch (geminiErr) {
console.warn("Gemini API enrichment skipped/failed, using ML provider output:", geminiErr);
}
}
return res.json({ success: true, result: baseResult });
} catch (err: any) {
console.error("Error in /api/analyze:", err);
return res.status(500).json({ error: "Failed to analyze pet signal", details: err?.message });
}
});
/**
* Train Pet Profile Feedback API endpoint
*/
app.post("/api/train", (req, res) => {
try {
const { pet, feedback, correctedCategory } = req.body as {
pet: Pet;
feedback: 'confirmed' | 'corrected';
correctedCategory?: string;
};
if (!pet || !feedback) {
return res.status(400).json({ error: "Pet and feedback are required." });
}
const updatedPet = defaultMLProvider.trainPetProfile(pet, feedback, correctedCategory);
return res.json({ success: true, pet: updatedPet });
} catch (err: any) {
return res.status(500).json({ error: "Failed to process training feedback" });
}
});
// Vite development vs production setup
if (process.env.NODE_ENV !== "production") {
const vite = await createViteServer({
server: { middlewareMode: true },
appType: "spa",
});
app.use(vite.middlewares);
} else {
const distPath = path.join(process.cwd(), "dist");
app.use(express.static(distPath));
app.get("*", (_req, res) => {
res.sendFile(path.join(distPath, "index.html"));
});
}
app.listen(PORT, "0.0.0.0", () => {
console.log(`PetTalk Server running on http://localhost:${PORT}`);
});
}
startServer();