mirror of
https://github.com/willchen96/mike.git
synced 2026-06-26 21:39:39 +02:00
Add local repo contents
This commit is contained in:
parent
65739ef1ce
commit
d9690965b5
176 changed files with 68998 additions and 0 deletions
351
frontend/src/contexts/UserProfileContext.tsx
Normal file
351
frontend/src/contexts/UserProfileContext.tsx
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
"use client";
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
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 (userId: string) => {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("*")
|
||||
.eq("user_id", userId)
|
||||
.single();
|
||||
|
||||
// Define credit limit constant
|
||||
const MONTHLY_CREDIT_LIMIT = 999999; // temporarily unlimited
|
||||
|
||||
// Calculate a default future reset date (30 days from now)
|
||||
const futureResetDate = new Date();
|
||||
futureResetDate.setDate(futureResetDate.getDate() + 30);
|
||||
const defaultResetDateStr = futureResetDate.toISOString();
|
||||
|
||||
if (error) {
|
||||
// Set fallback profile data if profile doesn't exist
|
||||
setProfile({
|
||||
displayName: null,
|
||||
organisation: null,
|
||||
messageCreditsUsed: 0,
|
||||
creditsResetDate: defaultResetDateStr,
|
||||
creditsRemaining: MONTHLY_CREDIT_LIMIT,
|
||||
tier: "Free",
|
||||
tabularModel: "gemini-3-flash-preview",
|
||||
claudeApiKey: null,
|
||||
geminiApiKey: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Use fetched data to update profile state
|
||||
if (data) {
|
||||
let creditsUsed = data.message_credits_used;
|
||||
let resetDate = data.credits_reset_date;
|
||||
let creditsRemaining = MONTHLY_CREDIT_LIMIT - creditsUsed;
|
||||
let shouldUpdateDb = false;
|
||||
|
||||
// Check if credits have expired and need reset
|
||||
if (resetDate && new Date() > new Date(resetDate)) {
|
||||
// Calculate new reset date
|
||||
const newResetDate = new Date();
|
||||
newResetDate.setDate(newResetDate.getDate() + 30);
|
||||
resetDate = newResetDate.toISOString();
|
||||
creditsUsed = 0;
|
||||
creditsRemaining = MONTHLY_CREDIT_LIMIT;
|
||||
shouldUpdateDb = true;
|
||||
}
|
||||
|
||||
// 1. Update local state immediately
|
||||
setProfile({
|
||||
displayName: data.display_name,
|
||||
organisation: data.organisation ?? null,
|
||||
messageCreditsUsed: creditsUsed,
|
||||
creditsResetDate: resetDate,
|
||||
creditsRemaining: creditsRemaining,
|
||||
tier: data.tier || "Free",
|
||||
tabularModel:
|
||||
data.tabular_model || "gemini-3-flash-preview",
|
||||
claudeApiKey: data.claude_api_key ?? null,
|
||||
geminiApiKey: data.gemini_api_key ?? null,
|
||||
});
|
||||
|
||||
// 2. Update database in background if needed
|
||||
if (shouldUpdateDb) {
|
||||
supabase
|
||||
.from("user_profiles")
|
||||
.update({
|
||||
message_credits_used: 0,
|
||||
credits_reset_date: resetDate,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("user_id", userId)
|
||||
.then(({ error }) => {
|
||||
if (error)
|
||||
console.error(
|
||||
"Failed to auto-reset credits",
|
||||
error,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 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(user.id);
|
||||
} else {
|
||||
setProfile(null);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [isAuthenticated, user, loadProfile]);
|
||||
|
||||
const updateDisplayName = useCallback(
|
||||
async (displayName: string): Promise<boolean> => {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from("user_profiles")
|
||||
.update({
|
||||
display_name: displayName,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("user_id", user.id);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
setProfile((prev) => (prev ? { ...prev, displayName } : null));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[user],
|
||||
);
|
||||
|
||||
const updateOrganisation = useCallback(
|
||||
async (organisation: string): Promise<boolean> => {
|
||||
if (!user) return false;
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from("user_profiles")
|
||||
.update({
|
||||
organisation,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("user_id", user.id);
|
||||
if (error) throw error;
|
||||
setProfile((prev) =>
|
||||
prev ? { ...prev, organisation } : null,
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[user],
|
||||
);
|
||||
|
||||
const updateModelPreference = useCallback(
|
||||
async (
|
||||
field: "tabularModel",
|
||||
value: string,
|
||||
): Promise<boolean> => {
|
||||
if (!user) return false;
|
||||
const dbField = field === "tabularModel" ? "tabular_model" : "";
|
||||
if (!dbField) return false;
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from("user_profiles")
|
||||
.update({
|
||||
[dbField]: value,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("user_id", user.id);
|
||||
if (error) throw error;
|
||||
setProfile((prev) =>
|
||||
prev ? { ...prev, [field]: value } : null,
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[user],
|
||||
);
|
||||
|
||||
const updateApiKey = useCallback(
|
||||
async (
|
||||
provider: "claude" | "gemini",
|
||||
value: string | null,
|
||||
): Promise<boolean> => {
|
||||
if (!user) return false;
|
||||
const dbField =
|
||||
provider === "claude" ? "claude_api_key" : "gemini_api_key";
|
||||
const stateField =
|
||||
provider === "claude" ? "claudeApiKey" : "geminiApiKey";
|
||||
const normalized = value?.trim() ? value.trim() : null;
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from("user_profiles")
|
||||
.update({
|
||||
[dbField]: normalized,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("user_id", user.id);
|
||||
if (error) throw error;
|
||||
setProfile((prev) =>
|
||||
prev ? { ...prev, [stateField]: normalized } : null,
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[user],
|
||||
);
|
||||
|
||||
const reloadProfile = useCallback(async () => {
|
||||
if (user) {
|
||||
await loadProfile(user.id);
|
||||
}
|
||||
}, [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;
|
||||
}
|
||||
|
||||
try {
|
||||
const newCreditsUsed = profile.messageCreditsUsed + 1;
|
||||
|
||||
const { error } = await supabase
|
||||
.from("user_profiles")
|
||||
.update({
|
||||
message_credits_used: newCreditsUsed,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("user_id", user.id);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Update local state
|
||||
setProfile((prev) =>
|
||||
prev
|
||||
? {
|
||||
...prev,
|
||||
messageCreditsUsed: newCreditsUsed,
|
||||
creditsRemaining: 999999 - newCreditsUsed, // temporarily unlimited
|
||||
}
|
||||
: null,
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue