diff --git a/src/components/dashboard/MyDashboardPage.tsx b/src/components/dashboard/MyDashboardPage.tsx index 5cc6436..4aeb40c 100644 --- a/src/components/dashboard/MyDashboardPage.tsx +++ b/src/components/dashboard/MyDashboardPage.tsx @@ -11,6 +11,7 @@ import ShortlistedWidget from './widgets/ShortlistedWidget'; import PortfolioWidget from './widgets/PortfolioWidget'; import ProfileCompletionWidget from './widgets/ProfileCompletionWidget'; import VerificationWidget from './widgets/VerificationWidget'; +import AiUsageWidget from './widgets/AiUsageWidget'; import VerificationSubmissionGuide from './VerificationSubmissionGuide'; import { fetchProfile } from '~/lib/api'; import { @@ -55,10 +56,10 @@ type Props = { }; const DEFAULT_WIDGETS: Record = { - PROFESSIONAL: ['tracecoins', 'open_leads', 'my_requests', 'portfolio', 'profile_status', 'verification_status'], - COMPANY: ['tracecoins', 'total_jobs', 'applications_received', 'shortlisted_candidates', 'profile_status', 'verification_status'], - CUSTOMER: ['credits', 'total_requirements', 'shortlisted_responses'], - JOB_SEEKER: ['credits', 'available_jobs', 'my_applications', 'shortlisted', 'profile_status', 'verification_status'], + PROFESSIONAL: ['tracecoins', 'open_leads', 'my_requests', 'ai_usage', 'portfolio', 'profile_status', 'verification_status'], + COMPANY: ['tracecoins', 'total_jobs', 'applications_received', 'ai_usage', 'shortlisted_candidates', 'profile_status', 'verification_status'], + CUSTOMER: ['credits', 'total_requirements', 'ai_usage', 'shortlisted_responses'], + JOB_SEEKER: ['credits', 'available_jobs', 'ai_usage', 'my_applications', 'shortlisted', 'profile_status', 'verification_status'], }; type Metric = { @@ -88,6 +89,7 @@ const WIDGET_COMPONENTS: Record any> = portfolio: PortfolioWidget, profile_status: ProfileCompletionWidget, verification_status: VerificationWidget, + ai_usage: AiUsageWidget, }; export default function MyDashboardPage(props: Props) { diff --git a/src/components/dashboard/widgets/AiUsageWidget.tsx b/src/components/dashboard/widgets/AiUsageWidget.tsx new file mode 100644 index 0000000..9fbeb8c --- /dev/null +++ b/src/components/dashboard/widgets/AiUsageWidget.tsx @@ -0,0 +1,156 @@ +import { createResource } from 'solid-js'; +import { Sparkles, Calendar, Zap } from 'lucide-solid'; +import DashboardWidget from './DashboardWidget'; +import type { RoleKey } from '../RoleDashboardShared'; +import { ROLE_PREFIXES } from '../RoleDashboardShared'; + +const API = '/api/gateway'; + +async function apiFetch(path: string, opts?: RequestInit) { + const token = + typeof window !== 'undefined' + ? window.sessionStorage.getItem('nxtgauge_access_token') || '' + : ''; + const cleanPath = path.startsWith('/api/') ? path.slice(4) : path; + return fetch(`${API}${cleanPath}`, { + ...opts, + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...(opts?.headers ?? {}), + }, + }); +} + +export interface AiUsageData { + plan: string; + monthly_limit: number; + monthly_used: number; + monthly_remaining: number; + daily_limit: number; + daily_used: number; + addon_balance: number; + renewal_date: string | null; +} + +async function fetchAiUsage(roleKey: RoleKey): Promise { + const prefix = ROLE_PREFIXES[roleKey]; + if (!prefix) return null; + try { + const res = await apiFetch(`/api/ai/usage/v2`); + if (!res.ok) return null; + const data = await res.json(); + return { + plan: data.plan || 'Free', + monthly_limit: data.monthly_limit || 50, + monthly_used: data.monthly_used || 0, + monthly_remaining: data.monthly_remaining || 50, + daily_limit: data.daily_limit || 10, + daily_used: data.daily_used || 0, + addon_balance: data.addon_balance || 0, + renewal_date: data.renewal_date || null, + }; + } catch { + return null; + } +} + +type Props = { + roleKey: RoleKey; +}; + +export default function AiUsageWidget(props: Props) { + const [usage] = createResource(() => props.roleKey, fetchAiUsage); + + const formatDate = (dateStr: string | null) => { + if (!dateStr) return 'N/A'; + try { + const date = new Date(dateStr); + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); + } catch { + return dateStr; + } + }; + + const monthlyPercent = () => { + const u = usage(); + if (!u || u.monthly_limit === 0) return 0; + return Math.round((u.monthly_used / u.monthly_limit) * 100); + }; + + const dailyPercent = () => { + const u = usage(); + if (!u || u.daily_limit === 0) return 0; + return Math.round((u.daily_used / u.daily_limit) * 100); + }; + + return ( + } + > +
+
+
+ Plan + + {usage()?.plan || 'Free'} + +
+
+ +
+

+ Monthly +

+

+ {usage()?.monthly_remaining ?? '—'} +

+
+
80 ? '#EF4444' : '#10B981' }} /> +
+

+ of {usage()?.monthly_limit ?? 50} +

+
+ +
+

+ Daily +

+

+ {usage()?.daily_limit !== undefined && usage()?.daily_used !== undefined + ? Math.max(0, (usage()?.daily_limit ?? 10) - (usage()?.daily_used ?? 0)) + : '—'} +

+
+
80 ? '#EF4444' : '#3B82F6' }} /> +
+

+ of {usage()?.daily_limit ?? 10} +

+
+ +
+
+ + Add-on + + {usage()?.addon_balance ?? 0} + +
+
+ + Renews + + {formatDate(usage()?.renewal_date ?? null)} + +
+
+
+ + ); +}