mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 14:07:55 +00:00
adding email requirement for opt in
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
Reference in New Issue
Block a user