"use client"; import { useState, useEffect } from "react"; import { AppShell } from "@/components/AppShell"; import { useAuth } from "@/components/AuthProvider"; import { useServiceWorker } from "@/components/ServiceWorkerProvider"; import { APP_NAME } from "@/lib/constants"; import { LogOut, Users, Bell, Sparkles, Loader2, Check, Key, ChevronDown } from "lucide-react"; import Link from "next/link"; interface UserSettings { reminderEnabled: boolean; reminderTime: string; reminderAlways: boolean; llmProvider: string | null; hasLlmKey: boolean; llmModel: string | null; } interface ModelInfo { id: string; name: string; description?: string; } const PROVIDERS = [ { id: "openrouter", name: "OpenRouter", description: "Access many models, often cheapest" }, { id: "openai", name: "OpenAI", description: "GPT-4o, GPT-4o-mini" }, { id: "anthropic", name: "Anthropic", description: "Claude models" }, ]; export default function SettingsPage() { const { user, logout } = useAuth(); const { isSupported: swSupported, isRegistered: swRegistered, showNotification, subscribeToPush } = useServiceWorker(); const [settings, setSettings] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [showLlmSetup, setShowLlmSetup] = useState(false); const [notificationPermission, setNotificationPermission] = useState("default"); const [isPWA, setIsPWA] = useState(false); // LLM setup state const [selectedProvider, setSelectedProvider] = useState(""); const [apiKey, setApiKey] = useState(""); const [models, setModels] = useState([]); const [selectedModel, setSelectedModel] = useState(""); const [isLoadingModels, setIsLoadingModels] = useState(false); const [modelError, setModelError] = useState(null); useEffect(() => { async function loadSettings() { try { const res = await fetch("/api/settings"); if (res.ok) { const data = await res.json(); setSettings(data); if (data.llmProvider) { setSelectedProvider(data.llmProvider); } if (data.llmModel) { setSelectedModel(data.llmModel); } } } catch (error) { console.error("Failed to load settings:", error); } finally { setIsLoading(false); } } loadSettings(); if ("Notification" in window) { setNotificationPermission(Notification.permission); } // Detect if running as PWA (standalone mode) const isStandalone = window.matchMedia("(display-mode: standalone)").matches || (window.navigator as Navigator & { standalone?: boolean }).standalone === true; setIsPWA(isStandalone); }, []); // Ensure push subscription is active if permissions are granted useEffect(() => { if (notificationPermission === "granted" && swRegistered) { subscribeToPush().catch(console.error); } }, [notificationPermission, swRegistered, subscribeToPush]); const updateSetting = async (key: string, value: unknown) => { setIsSaving(true); try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ [key]: value }), }); if (res.ok) { setSettings((prev) => prev ? { ...prev, [key]: value } : null); } } catch (error) { console.error("Failed to update setting:", error); } finally { setIsSaving(false); } }; const requestNotificationPermission = async () => { if ("Notification" in window) { const permission = await Notification.requestPermission(); setNotificationPermission(permission); if (permission === "granted") { updateSetting("reminderEnabled", true); const subscribed = await subscribeToPush(); if (subscribed) { showNotification(APP_NAME, { body: "Notifications enabled! You will receive daily reminders.", icon: "/icons/icon.svg", tag: "welcome-notification", }); } } } }; const toggleReminder = async () => { if (!settings) return; if (!settings.reminderEnabled) { if (notificationPermission !== "granted") { await requestNotificationPermission(); } else { updateSetting("reminderEnabled", true); } } else { updateSetting("reminderEnabled", false); } }; const fetchModels = async () => { if (!selectedProvider || !apiKey) return; setIsLoadingModels(true); setModelError(null); setModels([]); try { const res = await fetch("/api/llm/models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider: selectedProvider, apiKey }), }); const data = await res.json(); if (!res.ok) { setModelError(data.error || "Failed to fetch models"); return; } setModels(data.models); if (data.models.length > 0) { setSelectedModel(data.models[0].id); } } catch { setModelError("Failed to connect to API"); } finally { setIsLoadingModels(false); } }; const saveLlmSettings = async () => { if (!selectedProvider || !apiKey || !selectedModel) return; setIsSaving(true); try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ llmProvider: selectedProvider, llmApiKey: apiKey, llmModel: selectedModel, }), }); if (res.ok) { setSettings((prev) => prev ? { ...prev, llmProvider: selectedProvider, hasLlmKey: true, llmModel: selectedModel, } : null); setShowLlmSetup(false); setApiKey(""); } } catch (error) { console.error("Failed to save LLM settings:", error); } finally { setIsSaving(false); } }; const clearLlmSettings = async () => { setIsSaving(true); try { await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ llmProvider: "", llmApiKey: "", llmModel: "", }), }); setSettings((prev) => prev ? { ...prev, llmProvider: null, hasLlmKey: false, llmModel: null, } : null); setSelectedProvider(""); setSelectedModel(""); setModels([]); } catch (error) { console.error("Failed to clear LLM settings:", error); } finally { setIsSaving(false); } }; // Reminder check interval - uses service worker for iOS compatibility useEffect(() => { if (!settings?.reminderEnabled || !settings?.reminderTime) return; // Track if we've shown the notification this minute let lastShownMinute = -1; const checkReminder = async () => { const now = new Date(); const [hours, minutes] = settings.reminderTime.split(":").map(Number); const currentMinute = now.getHours() * 60 + now.getMinutes(); if (now.getHours() === hours && now.getMinutes() === minutes && currentMinute !== lastShownMinute) { lastShownMinute = currentMinute; if (Notification.permission === "granted") { await showNotification(APP_NAME, { body: "Take a moment to reflect on what you're grateful for today.", icon: "/icons/icon.svg", tag: "daily-reminder", }); } } }; // Check immediately and then every minute checkReminder(); const interval = setInterval(checkReminder, 60000); return () => clearInterval(interval); }, [settings?.reminderEnabled, settings?.reminderTime, showNotification]); const getProviderName = (id: string) => PROVIDERS.find(p => p.id === id)?.name || id; return (

{APP_NAME}

Settings

{/* Account info */} {user && (

Account

{user.email}

{user.name &&

{user.name}

}
)} {/* Admin */} {user?.isAdmin && (

User Management

Add users, reset passwords

)} {/* Daily Reminder */} {!isLoading && settings && (

Daily reminder

Get a gentle nudge to check in

{settings.reminderEnabled && (
updateSetting("reminderTime", e.target.value)} className="px-3 py-1 bg-background border border-border rounded-lg text-sm focus:outline-none focus:border-muted" />
updateSetting("reminderAlways", e.target.checked)} className="w-4 h-4 rounded border-border text-accent focus:ring-accent" />
)} {settings.reminderEnabled && notificationPermission === "granted" && swRegistered && (
Notifications ready{isPWA ? " (PWA mode)" : ""}
)} {settings.reminderEnabled && notificationPermission === "granted" && !swRegistered && (

Service worker loading... Notifications may be limited.

)} {notificationPermission === "denied" && (

Notifications are blocked. Please enable them in your browser/device settings.

)} {settings.reminderEnabled && notificationPermission === "default" && (

Notification permission not yet granted. Toggle reminders off and on to request permission.

)}
)} {/* AI Reflections */} {!isLoading && settings && (

AI reflections

Generate insights from your entries

{settings.hasLlmKey ? ( ) : ( )}
{settings.hasLlmKey && !showLlmSetup && (

{getProviderName(settings.llmProvider || "")} configured {settings.llmModel && ( • {settings.llmModel} )}

View reflections
)} {showLlmSetup && (
{/* Provider selection */}
{PROVIDERS.map((provider) => ( ))}
{/* API Key */} {selectedProvider && (
setApiKey(e.target.value)} placeholder={ selectedProvider === "anthropic" ? "sk-ant-..." : selectedProvider === "openai" ? "sk-..." : "sk-or-..." } className="w-full pl-9 pr-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:border-muted" />

{selectedProvider === "anthropic" && ( <>Get your key from console.anthropic.com )} {selectedProvider === "openai" && ( <>Get your key from platform.openai.com )} {selectedProvider === "openrouter" && ( <>Get your key from openrouter.ai )}

)} {/* Error */} {modelError && (

{modelError}

)} {/* Model selection */} {models.length > 0 && (

{selectedProvider === "openrouter" && "Sorted by price (cheapest first)"} {selectedProvider === "openai" && "Mini models are cheapest"} {selectedProvider === "anthropic" && "Haiku is cheapest, Opus is most capable"}

)} {/* Actions */}
{models.length > 0 && ( )}
)}
)}

{APP_NAME} v0.1.0

A calm, private gratitude log.

); }