adding email requirement for opt in

This commit is contained in:
Gabriel Bo
2026-03-13 14:23:41 -07:00
parent 07ec747c8f
commit 898099e771
6 changed files with 92 additions and 13 deletions
+39 -2
View File
@@ -267,6 +267,7 @@ function isProfane(text: string): boolean {
const OPTIN_KEY = 'openjarvis-desktop-optin';
const OPTIN_NAME_KEY = 'openjarvis-desktop-display-name';
const OPTIN_EMAIL_KEY = 'openjarvis-desktop-email';
const OPTIN_ANONID_KEY = 'openjarvis-desktop-anon-id';
function getOrCreateAnonId(): string {
@@ -289,7 +290,9 @@ export function SavingsDashboard({ apiUrl }: { apiUrl: string }) {
const [optInEnabled, setOptInEnabled] = useState(localStorage.getItem(OPTIN_KEY) === 'true');
const [displayName, setDisplayName] = useState(localStorage.getItem(OPTIN_NAME_KEY) || '');
const [emailStored, setEmailStored] = useState(localStorage.getItem(OPTIN_EMAIL_KEY) || '');
const [nameInput, setNameInput] = useState(localStorage.getItem(OPTIN_NAME_KEY) || '');
const [emailInput, setEmailInput] = useState(localStorage.getItem(OPTIN_EMAIL_KEY) || '');
const [nameError, setNameError] = useState('');
const [showOptIn, setShowOptIn] = useState(false);
const anonId = getOrCreateAnonId();
@@ -326,6 +329,7 @@ export function SavingsDashboard({ apiUrl }: { apiUrl: string }) {
payload: {
anon_id: anonId,
display_name: displayName,
email: emailStored,
total_calls: data.total_calls,
total_tokens: data.total_tokens,
dollar_savings: dollarSavings,
@@ -337,6 +341,7 @@ export function SavingsDashboard({ apiUrl }: { apiUrl: string }) {
const handleOptInJoin = () => {
const trimmed = nameInput.trim();
const trimmedEmail = emailInput.trim();
if (!trimmed || trimmed.length < 2 || trimmed.length > 30) {
setNameError('Name must be 2-30 characters');
return;
@@ -345,19 +350,27 @@ export function SavingsDashboard({ apiUrl }: { apiUrl: string }) {
setNameError('Please choose a different name');
return;
}
if (!trimmedEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmedEmail)) {
setNameError('Please enter a valid email address');
return;
}
setNameError('');
localStorage.setItem(OPTIN_KEY, 'true');
localStorage.setItem(OPTIN_NAME_KEY, trimmed);
localStorage.setItem(OPTIN_EMAIL_KEY, trimmedEmail);
setOptInEnabled(true);
setDisplayName(trimmed);
setEmailStored(trimmedEmail);
setShowOptIn(false);
};
const handleOptOut = () => {
localStorage.removeItem(OPTIN_KEY);
localStorage.removeItem(OPTIN_NAME_KEY);
localStorage.removeItem(OPTIN_EMAIL_KEY);
setOptInEnabled(false);
setDisplayName('');
setEmailStored('');
setShowOptIn(false);
};
@@ -411,7 +424,7 @@ export function SavingsDashboard({ apiUrl }: { apiUrl: string }) {
<div style={{ fontSize: 13, color: colors.textMuted, marginBottom: 14, lineHeight: 1.5 }}>
Opt in to privately share your savings for the chance to win a Mac Mini!
</div>
<div style={{ marginBottom: 10 }}>
<div style={{ marginBottom: 8 }}>
<input
type="text"
value={nameInput}
@@ -424,7 +437,31 @@ export function SavingsDashboard({ apiUrl }: { apiUrl: string }) {
padding: '8px 12px',
borderRadius: 8,
background: colors.bg,
border: nameError ? `1px solid ${colors.red}` : `1px solid ${colors.border}`,
border: `1px solid ${colors.border}`,
color: colors.text,
fontSize: 14,
outline: 'none',
boxSizing: 'border-box',
}}
/>
</div>
<div style={{ marginBottom: 10 }}>
<div style={{ fontSize: 12, color: colors.textMuted, marginBottom: 4 }}>
Email <span style={{ color: colors.red }}>*</span>
<span style={{ fontWeight: 400, marginLeft: 4, opacity: 0.7 }}>(never shown publicly)</span>
</div>
<input
type="email"
value={emailInput}
onChange={(e) => { setEmailInput(e.target.value); setNameError(''); }}
onKeyDown={(e) => { if (e.key === 'Enter') handleOptInJoin(); }}
placeholder="your@email.com"
style={{
width: '100%',
padding: '8px 12px',
borderRadius: 8,
background: colors.bg,
border: `1px solid ${colors.border}`,
color: colors.text,
fontSize: 14,
outline: 'none',
+1 -1
View File
@@ -32,7 +32,7 @@
fetch(
SUPABASE_URL +
"/rest/v1/savings_entries?select=*&order=dollar_savings.desc&limit=100",
"/rest/v1/savings_entries?select=display_name,dollar_savings,energy_wh_saved,flops_saved,total_calls,total_tokens&order=dollar_savings.desc&limit=100",
{
headers: {
apikey: SUPABASE_ANON_KEY,
+2
View File
@@ -26,6 +26,7 @@ export default function App() {
const setCommandPaletteOpen = useAppStore((s) => s.setCommandPaletteOpen);
const optInEnabled = useAppStore((s) => s.optInEnabled);
const optInDisplayName = useAppStore((s) => s.optInDisplayName);
const optInEmail = useAppStore((s) => s.optInEmail);
const optInAnonId = useAppStore((s) => s.optInAnonId);
const optInModalSeen = useAppStore((s) => s.optInModalSeen);
const optInModalOpen = useAppStore((s) => s.optInModalOpen);
@@ -79,6 +80,7 @@ export default function App() {
submitSavings({
anon_id: optInAnonId,
display_name: optInDisplayName,
email: optInEmail,
total_calls: data.total_calls,
total_tokens: data.total_tokens,
dollar_savings: dollarSavings,
+42 -7
View File
@@ -12,12 +12,16 @@ export function OptInModal({ onClose }: OptInModalProps) {
const optInDisplayName = useAppStore((s) => s.optInDisplayName);
const optInEnabled = useAppStore((s) => s.optInEnabled);
const optInEmail = useAppStore((s) => s.optInEmail);
const [name, setName] = useState(optInDisplayName);
const [email, setEmail] = useState(optInEmail);
const [error, setError] = useState('');
const [joined, setJoined] = useState(optInEnabled);
const handleJoin = () => {
const trimmed = name.trim();
const trimmedEmail = email.trim();
if (!trimmed) {
setError('Please enter a display name');
return;
@@ -30,19 +34,23 @@ export function OptInModal({ onClose }: OptInModalProps) {
setError('Please choose a different name');
return;
}
if (!trimmedEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmedEmail)) {
setError('Please enter a valid email address');
return;
}
setError('');
setOptIn(true, trimmed);
setOptIn(true, trimmed, trimmedEmail);
setJoined(true);
};
const handleOptOut = () => {
setOptIn(false, '');
setOptIn(false, '', '');
setJoined(false);
onClose();
};
const features = [
{ icon: Lock, label: 'Fully anonymous — no email, no IP, no hardware info' },
{ icon: Lock, label: 'Private — no IP or hardware info shared publicly' },
{ icon: Zap, label: 'Track energy savings from local inference' },
{ icon: DollarSign, label: 'See how much you save vs cloud providers' },
{ icon: Cpu, label: 'Measure FLOPs and request efficiency' },
@@ -160,7 +168,7 @@ export function OptInModal({ onClose }: OptInModalProps) {
) : (
<>
{/* Name input */}
<div className="mb-4">
<div className="mb-3">
<label
className="block text-xs font-medium mb-1.5"
style={{ color: 'var(--color-text-secondary)' }}
@@ -182,13 +190,40 @@ export function OptInModal({ onClose }: OptInModalProps) {
className="w-full px-3 py-2.5 rounded-xl text-sm outline-none transition-colors"
style={{
background: 'var(--color-bg-secondary)',
border: error
? '1.5px solid var(--color-error)'
: '1.5px solid var(--color-border)',
border: '1.5px solid var(--color-border)',
color: 'var(--color-text)',
}}
autoFocus
/>
</div>
{/* Email input */}
<div className="mb-4">
<label
className="block text-xs font-medium mb-1.5"
style={{ color: 'var(--color-text-secondary)' }}
>
Email <span style={{ color: 'var(--color-error)' }}>*</span>
<span className="font-normal ml-1" style={{ color: 'var(--color-text-tertiary)' }}>(never shown publicly)</span>
</label>
<input
type="email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
if (error) setError('');
}}
onKeyDown={(e) => {
if (e.key === 'Enter') handleJoin();
}}
placeholder="your@email.com"
className="w-full px-3 py-2.5 rounded-xl text-sm outline-none transition-colors"
style={{
background: 'var(--color-bg-secondary)',
border: '1.5px solid var(--color-border)',
color: 'var(--color-text)',
}}
/>
{error && (
<p
className="text-xs mt-1"
+1
View File
@@ -465,6 +465,7 @@ export async function fetchAgentTrace(agentId: string, traceId: string): Promise
export interface SavingsSubmission {
anon_id: string;
display_name: string;
email: string;
total_calls: number;
total_tokens: number;
dollar_savings: number;
+7 -3
View File
@@ -23,6 +23,7 @@ const CONVERSATIONS_KEY = 'openjarvis-conversations';
const SETTINGS_KEY = 'openjarvis-settings';
const OPTIN_KEY = 'openjarvis-optin';
const OPTIN_NAME_KEY = 'openjarvis-display-name';
const OPTIN_EMAIL_KEY = 'openjarvis-email';
const OPTIN_ANONID_KEY = 'openjarvis-anon-id';
const OPTIN_SEEN_KEY = 'openjarvis-optin-seen';
@@ -128,6 +129,7 @@ interface AppState {
// Opt-in sharing
optInEnabled: boolean;
optInDisplayName: string;
optInEmail: string;
optInAnonId: string;
optInModalSeen: boolean;
optInModalOpen: boolean;
@@ -181,7 +183,7 @@ interface AppState {
clearAgentEvents: () => void;
// Actions: opt-in sharing
setOptIn: (enabled: boolean, displayName: string) => void;
setOptIn: (enabled: boolean, displayName: string, email: string) => void;
setOptInModalOpen: (open: boolean) => void;
markOptInModalSeen: () => void;
}
@@ -215,6 +217,7 @@ export const useAppStore = create<AppState>((set, get) => {
optInEnabled: localStorage.getItem(OPTIN_KEY) === 'true',
optInDisplayName: localStorage.getItem(OPTIN_NAME_KEY) || '',
optInEmail: localStorage.getItem(OPTIN_EMAIL_KEY) || '',
optInAnonId: localStorage.getItem(OPTIN_ANONID_KEY) || crypto.randomUUID(),
optInModalSeen: localStorage.getItem(OPTIN_SEEN_KEY) === 'true',
optInModalOpen: false,
@@ -386,12 +389,13 @@ export const useAppStore = create<AppState>((set, get) => {
// ── Opt-in sharing ──────────────────────────────────────────────
setOptIn: (enabled: boolean, displayName: string) => {
setOptIn: (enabled: boolean, displayName: string, email: string) => {
const anonId = get().optInAnonId;
localStorage.setItem(OPTIN_KEY, String(enabled));
localStorage.setItem(OPTIN_NAME_KEY, displayName);
localStorage.setItem(OPTIN_EMAIL_KEY, email);
localStorage.setItem(OPTIN_ANONID_KEY, anonId);
set({ optInEnabled: enabled, optInDisplayName: displayName });
set({ optInEnabled: enabled, optInDisplayName: displayName, optInEmail: email });
},
setOptInModalOpen: (open: boolean) => set({ optInModalOpen: open }),
markOptInModalSeen: () => {