mike/frontend/src/contexts/UserProfileContext.tsx

252 lines
7.1 KiB
TypeScript
Raw Normal View History

2026-04-29 19:49:06 +02:00
"use client";
import React, {
createContext,
useContext,
useEffect,
useState,
ReactNode,
useCallback,
} from "react";
import { useAuth } from "@/contexts/AuthContext";
import {
type ApiKeyState,
type ApiKeyProvider,
type UserProfile as ApiUserProfile,
getUserProfile,
saveApiKey,
updateUserProfile,
} from "@/app/lib/mikeApi";
2026-04-29 19:49:06 +02:00
interface UserProfile {
displayName: string | null;
organisation: string | null;
messageCreditsUsed: number;
creditsResetDate: string;
creditsRemaining: number;
tier: string;
tabularModel: string;
apiKeys: ApiKeyState;
2026-04-29 19:49:06 +02:00
}
interface UserProfileContextType {
profile: UserProfile | null;
loading: boolean;
updateDisplayName: (name: string) => Promise<boolean>;
updateOrganisation: (organisation: string) => Promise<boolean>;
updateModelPreference: (
field: "tabularModel",
value: string,
) => Promise<boolean>;
updateApiKey: (
provider: ApiKeyProvider,
2026-04-29 19:49:06 +02:00
value: string | null,
) => Promise<boolean>;
reloadProfile: () => Promise<void>;
incrementMessageCredits: () => Promise<boolean>;
}
const UserProfileContext = createContext<UserProfileContextType | undefined>(
undefined,
);
const API_KEY_PROVIDERS: ApiKeyProvider[] = ["claude", "gemini", "openai"];
function emptyApiKeys(): ApiKeyState {
return {
claude: { configured: false, source: null },
gemini: { configured: false, source: null },
openai: { configured: false, source: null },
};
}
function toProfile(data: ApiUserProfile): UserProfile {
const { apiKeyStatus, ...profile } = data;
const apiKeys = emptyApiKeys();
for (const provider of API_KEY_PROVIDERS) {
apiKeys[provider] = {
configured: !!apiKeyStatus[provider],
source:
apiKeyStatus.sources?.[provider] ??
(apiKeyStatus[provider] ? "user" : null),
};
}
return {
...profile,
apiKeys,
};
}
2026-04-29 19:49:06 +02:00
export function UserProfileProvider({ children }: { children: ReactNode }) {
const { user, isAuthenticated } = useAuth();
const [profile, setProfile] = useState<UserProfile | null>(null);
const [loading, setLoading] = useState(true);
const loadProfile = useCallback(async () => {
2026-04-29 19:49:06 +02:00
try {
const profileData = await getUserProfile();
setProfile(toProfile(profileData));
} catch {
2026-04-29 19:49:06 +02:00
// Calculate a default future reset date for fallback
const futureResetDate = new Date();
futureResetDate.setDate(futureResetDate.getDate() + 30);
// Set fallback profile data on exception
setProfile({
displayName: null,
organisation: null,
messageCreditsUsed: 0,
creditsResetDate: futureResetDate.toISOString(),
creditsRemaining: 999999, // temporarily unlimited
tier: "Free",
tabularModel: "gemini-3-flash-preview",
apiKeys: emptyApiKeys(),
2026-04-29 19:49:06 +02:00
});
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (isAuthenticated && user) {
setLoading(true);
loadProfile();
2026-04-29 19:49:06 +02:00
} else {
setProfile(null);
setLoading(false);
}
}, [isAuthenticated, user, loadProfile]);
const updateDisplayName = useCallback(
async (displayName: string): Promise<boolean> => {
if (!user) {
return false;
}
try {
const updated = await updateUserProfile({ displayName });
setProfile((prev) =>
prev ? { ...prev, ...toProfile(updated) } : null,
);
2026-04-29 19:49:06 +02:00
return true;
} catch {
return false;
}
},
[user],
);
const updateOrganisation = useCallback(
async (organisation: string): Promise<boolean> => {
if (!user) return false;
try {
const updated = await updateUserProfile({ organisation });
2026-04-29 19:49:06 +02:00
setProfile((prev) =>
prev ? { ...prev, ...toProfile(updated) } : null,
2026-04-29 19:49:06 +02:00
);
return true;
} catch {
return false;
}
},
[user],
);
const updateModelPreference = useCallback(
async (field: "tabularModel", value: string): Promise<boolean> => {
2026-04-29 19:49:06 +02:00
if (!user) return false;
if (field !== "tabularModel") return false;
2026-04-29 19:49:06 +02:00
try {
const updated = await updateUserProfile({
tabularModel: value,
});
2026-04-29 19:49:06 +02:00
setProfile((prev) =>
prev ? { ...prev, ...toProfile(updated) } : null,
2026-04-29 19:49:06 +02:00
);
return true;
} catch {
return false;
}
},
[user],
);
const updateApiKey = useCallback(
async (
provider: ApiKeyProvider,
2026-04-29 19:49:06 +02:00
value: string | null,
): Promise<boolean> => {
if (!user) return false;
const normalized = value?.trim() ? value.trim() : null;
try {
await saveApiKey(provider, normalized);
2026-04-29 19:49:06 +02:00
setProfile((prev) =>
prev
? {
...prev,
apiKeys: {
...prev.apiKeys,
[provider]: {
configured: !!normalized,
source: normalized ? "user" : null,
},
},
}
: null,
2026-04-29 19:49:06 +02:00
);
return true;
} catch {
return false;
}
},
[user],
);
const reloadProfile = useCallback(async () => {
if (user) {
await loadProfile();
2026-04-29 19:49:06 +02:00
}
}, [user, loadProfile]);
const incrementMessageCredits = useCallback(async (): Promise<boolean> => {
if (!user || !profile) {
return false;
}
// Check if user has credits remaining
if (profile.creditsRemaining <= 0) {
return false;
}
return false;
2026-04-29 19:49:06 +02:00
}, [user, profile]);
return (
<UserProfileContext.Provider
value={{
profile,
loading,
updateDisplayName,
updateOrganisation,
updateModelPreference,
updateApiKey,
reloadProfile,
incrementMessageCredits,
}}
>
{children}
</UserProfileContext.Provider>
);
}
export function useUserProfile() {
const context = useContext(UserProfileContext);
if (context === undefined) {
throw new Error(
"useUserProfile must be used within a UserProfileProvider",
);
}
return context;
}