mike/frontend/src/contexts/UserProfileContext.tsx
2026-05-08 20:45:16 +08:00

232 lines
6.6 KiB
TypeScript

"use client";
import React, {
createContext,
useContext,
useEffect,
useState,
ReactNode,
useCallback,
} from "react";
import { useAuth } from "@/contexts/AuthContext";
import {
type UserProfile as ApiUserProfile,
getUserProfile,
saveApiKey,
updateUserProfile,
} from "@/app/lib/mikeApi";
interface UserProfile {
displayName: string | null;
organisation: string | null;
messageCreditsUsed: number;
creditsResetDate: string;
creditsRemaining: number;
tier: string;
tabularModel: string;
claudeApiKey: string | null;
geminiApiKey: string | null;
}
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: "claude" | "gemini",
value: string | null,
) => Promise<boolean>;
reloadProfile: () => Promise<void>;
incrementMessageCredits: () => Promise<boolean>;
}
const UserProfileContext = createContext<UserProfileContextType | undefined>(
undefined,
);
const CONFIGURED_KEY_MARKER = "configured";
function toProfile(data: ApiUserProfile): UserProfile {
const { apiKeyStatus, ...profile } = data;
return {
...profile,
claudeApiKey: apiKeyStatus.claude ? CONFIGURED_KEY_MARKER : null,
geminiApiKey: apiKeyStatus.gemini ? CONFIGURED_KEY_MARKER : null,
};
}
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 () => {
try {
const profileData = await getUserProfile();
setProfile(toProfile(profileData));
} catch {
// 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",
claudeApiKey: null,
geminiApiKey: null,
});
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (isAuthenticated && user) {
setLoading(true);
loadProfile();
} 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,
);
return true;
} catch {
return false;
}
},
[user],
);
const updateOrganisation = useCallback(
async (organisation: string): Promise<boolean> => {
if (!user) return false;
try {
const updated = await updateUserProfile({ organisation });
setProfile((prev) =>
prev ? { ...prev, ...toProfile(updated) } : null,
);
return true;
} catch {
return false;
}
},
[user],
);
const updateModelPreference = useCallback(
async (field: "tabularModel", value: string): Promise<boolean> => {
if (!user) return false;
if (field !== "tabularModel") return false;
try {
const updated = await updateUserProfile({
tabularModel: value,
});
setProfile((prev) =>
prev ? { ...prev, ...toProfile(updated) } : null,
);
return true;
} catch {
return false;
}
},
[user],
);
const updateApiKey = useCallback(
async (
provider: "claude" | "gemini",
value: string | null,
): Promise<boolean> => {
if (!user) return false;
const stateField =
provider === "claude" ? "claudeApiKey" : "geminiApiKey";
const normalized = value?.trim() ? value.trim() : null;
try {
await saveApiKey(provider, normalized);
setProfile((prev) =>
prev
? {
...prev,
[stateField]: normalized
? CONFIGURED_KEY_MARKER
: null,
}
: null,
);
return true;
} catch {
return false;
}
},
[user],
);
const reloadProfile = useCallback(async () => {
if (user) {
await loadProfile();
}
}, [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;
}, [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;
}