commit fc6d00988c8044a8232965ae3062680a8202dfcd Author: drjones Date: Tue Oct 6 23:43:43 2026 -0700 Snapshot: full project state diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24b2937 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +__pycache__/ +*.pyc +node_modules/ +.venv/ +venv/ +.env +*.db +*.sqlite* +*.log +.DS_Store +out/ +work/ +.pio/ +briefs/ +dns-backup/ +archive/ +*.png diff --git a/README.md b/README.md new file mode 100644 index 0000000..7180815 --- /dev/null +++ b/README.md @@ -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. + diff --git a/pettalk-App-new.tsx b/pettalk-App-new.tsx new file mode 100644 index 0000000..8ced5fa --- /dev/null +++ b/pettalk-App-new.tsx @@ -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([INITIAL_PET_LUNA, INITIAL_PET_MILO]); + const [selectedPetId, setSelectedPetId] = useState(INITIAL_PET_LUNA.id); + const [currentView, setCurrentView] = useState<'landing' | 'onboarding' | 'listen' | 'talkback' | 'timeline' | 'insights'>('landing'); + + const [timelineItems, setTimelineItems] = useState(INITIAL_TIMELINE_LUNA); + const [dictionarySignals, setDictionarySignals] = useState(INITIAL_DICTIONARY_LUNA); + + const selectedPet = pets.find(p => p.id === selectedPetId) || pets[0]; + + // Show loading spinner while checking auth + if (isLoading) { + return ( +
+
+
+ ); + } + + // Show auth page if not authenticated + if (!user) { + return ; + } + + // 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 ( +
+ + {/* Global Responsive Navigation Header & Bottom Bar */} + setCurrentView('onboarding')} + /> + + {/* Main Content View Switcher */} +
+ {currentView === 'landing' && ( + setCurrentView('listen')} + onStartOnboarding={() => setCurrentView('onboarding')} + /> + )} + + {currentView === 'onboarding' && ( + setCurrentView('landing')} + /> + )} + + {currentView === 'listen' && ( + + )} + + {currentView === 'talkback' && ( + + )} + + {currentView === 'timeline' && ( + + )} + + {currentView === 'insights' && ( + + )} +
+ + {/* Footer */} +
+
+
+ PetTalk + • Probabilistic AI Pet Behavioral Interpretation +
+
+ + + + +
+
+
+ +
+ ); +} diff --git a/pettalk-App-v2.tsx b/pettalk-App-v2.tsx new file mode 100644 index 0000000..def4913 --- /dev/null +++ b/pettalk-App-v2.tsx @@ -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([INITIAL_PET_LUNA, INITIAL_PET_MILO]); + const [selectedPetId, setSelectedPetId] = useState(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(INITIAL_TIMELINE_LUNA); + const [dictionarySignals, setDictionarySignals] = useState(INITIAL_DICTIONARY_LUNA); + + const selectedPet = pets.find(p => p.id === selectedPetId) || pets[0]; + + // Show loading spinner while checking auth + if (isLoading) { + return ( +
+
+
+ ); + } + + // ── UNAUTHENTICATED FLOW ── + if (!user) { + // User clicked "Get Started" or "Sign In" → show auth form + if (currentView === 'auth') { + return ( + { + setCurrentView('landing'); + setAuthMode('register'); + }} + /> + ); + } + // Default: show public landing page + return ( + { + 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 ( +
+ + {/* Global Responsive Navigation Header & Bottom Bar */} + setCurrentView('onboarding')} + /> + + {/* Main Content View Switcher */} +
+ {currentView === 'landing' && ( + setCurrentView('listen')} + onStartOnboarding={() => setCurrentView('onboarding')} + /> + )} + + {currentView === 'onboarding' && ( + setCurrentView('landing')} + /> + )} + + {currentView === 'listen' && ( + + )} + + {currentView === 'talkback' && ( + + )} + + {currentView === 'timeline' && ( + + )} + + {currentView === 'insights' && ( + + )} +
+ + {/* Footer */} +
+
+
+ PetTalk + • Probabilistic AI Pet Behavioral Interpretation +
+
+ + + + +
+
+
+ +
+ ); +} diff --git a/pettalk-AuthContext.tsx b/pettalk-AuthContext.tsx new file mode 100644 index 0000000..45aaf87 --- /dev/null +++ b/pettalk-AuthContext.tsx @@ -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; + register: (username: string, email: string, password: string) => Promise; + logout: () => void; +} + +const AuthContext = createContext(null); + +const API_BASE = ""; + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null); + const [token, setToken] = useState(() => 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 ( + + {children} + + ); +} + +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 { + const token = localStorage.getItem("pettalk_token"); + if (!token) return {}; + return { Authorization: `Bearer ${token}` }; +} diff --git a/pettalk-AuthPage-v2.tsx b/pettalk-AuthPage-v2.tsx new file mode 100644 index 0000000..8d306d9 --- /dev/null +++ b/pettalk-AuthPage-v2.tsx @@ -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 = ({ 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 ( +
+
+ {/* Back button */} + {onBack && ( + + )} + + {/* Logo */} +
+
+ +
+

+ PetTalk +

+

+ Understanding pet intent softly +

+
+ + {/* Form Card */} +
+
+ +

+ {isRegistering ? "Create Account" : "Welcome Back"} +

+
+ + {error && ( +
+ {error} +
+ )} + +
+ {isRegistering && ( +
+ +
+ + 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" + /> +
+
+ )} + +
+ +
+ + 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" + /> +
+
+ +
+ +
+ + 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" + /> +
+
+ + +
+ +
+ +
+
+ + {/* Footer */} +

+ PetTalk AI · Probabilistic Pet Behavioral Interpretation +

+
+
+ ); +}; diff --git a/pettalk-AuthPage.tsx b/pettalk-AuthPage.tsx new file mode 100644 index 0000000..07d8e40 --- /dev/null +++ b/pettalk-AuthPage.tsx @@ -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 ( +
+
+ {/* Logo */} +
+
+ +
+

+ PetTalk +

+

+ Understanding pet intent softly +

+
+ + {/* Form Card */} +
+
+ +

+ {isRegistering ? "Create Account" : "Welcome Back"} +

+
+ + {error && ( +
+ {error} +
+ )} + +
+ {isRegistering && ( +
+ +
+ + 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" + /> +
+
+ )} + +
+ +
+ + 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" + /> +
+
+ +
+ +
+ + 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" + /> +
+
+ + +
+ +
+ +
+
+ + {/* Footer */} +

+ PetTalk AI · Probabilistic Pet Behavioral Interpretation +

+
+
+ ); +}; diff --git a/pettalk-auth-db.ts b/pettalk-auth-db.ts new file mode 100644 index 0000000..728a96b --- /dev/null +++ b/pettalk-auth-db.ts @@ -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, + }; +} diff --git a/pettalk-auth-middleware.ts b/pettalk-auth-middleware.ts new file mode 100644 index 0000000..265a569 --- /dev/null +++ b/pettalk-auth-middleware.ts @@ -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." }); + } +} diff --git a/pettalk-auth-routes.ts b/pettalk-auth-routes.ts new file mode 100644 index 0000000..211cbd3 --- /dev/null +++ b/pettalk-auth-routes.ts @@ -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; diff --git a/pettalk-live-imports.tsx b/pettalk-live-imports.tsx new file mode 100644 index 0000000..e3e9905 --- /dev/null +++ b/pettalk-live-imports.tsx @@ -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'; diff --git a/pettalk-main-new.tsx b/pettalk-main-new.tsx new file mode 100644 index 0000000..f4bb1c7 --- /dev/null +++ b/pettalk-main-new.tsx @@ -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( + + + + + , +); diff --git a/pettalk-server-new.ts b/pettalk-server-new.ts new file mode 100644 index 0000000..047395c --- /dev/null +++ b/pettalk-server-new.ts @@ -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();