feat: add AI usage widget to user dashboard

- Add AiUsageWidget showing:
  - Current plan
  - Monthly usage vs limit
  - Daily usage vs limit
  - Addon balance
  - Renewal date
- Widget added to all role dashboards
This commit is contained in:
Tracewebstudio Dev 2026-06-14 20:28:02 +02:00
parent cbaff36f88
commit 3b8f75d836
2 changed files with 162 additions and 4 deletions

View file

@ -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<string, string[]> = {
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<string, (props: { roleKey: RoleKey }) => any> =
portfolio: PortfolioWidget,
profile_status: ProfileCompletionWidget,
verification_status: VerificationWidget,
ai_usage: AiUsageWidget,
};
export default function MyDashboardPage(props: Props) {

View file

@ -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<AiUsageData | null> {
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 (
<DashboardWidget
title="AI Usage"
loading={usage.loading}
error={usage.error ? 'Failed to load' : undefined}
icon={<Sparkles size={16} />}
>
<div style={{ display: 'grid', 'grid-template-columns': '1fr 1fr', gap: '16px' }}>
<div style={{ 'grid-column': '1 / -1' }}>
<div style={{ display: 'flex', 'align-items': 'center', 'justify-content': 'space-between' }}>
<span style={{ 'font-size': '12px', color: '#6B7280' }}>Plan</span>
<span style={{ 'font-size': '14px', 'font-weight': '600', color: '#111827' }}>
{usage()?.plan || 'Free'}
</span>
</div>
</div>
<div>
<p style={{ margin: '0', 'font-size': '10px', 'text-transform': 'uppercase', 'letter-spacing': '0.05em', color: '#6B7280' }}>
Monthly
</p>
<p style={{ margin: '4px 0 2px', 'font-size': '18px', 'font-weight': '700', color: '#111827' }}>
{usage()?.monthly_remaining ?? '—'}
</p>
<div style={{ height: '4px', background: '#E5E7EB', 'border-radius': '2px', overflow: 'hidden' }}>
<div style={{ width: `${monthlyPercent()}%`, height: '100%', background: monthlyPercent() > 80 ? '#EF4444' : '#10B981' }} />
</div>
<p style={{ margin: '2px 0 0', 'font-size': '10px', color: '#9CA3AF' }}>
of {usage()?.monthly_limit ?? 50}
</p>
</div>
<div>
<p style={{ margin: '0', 'font-size': '10px', 'text-transform': 'uppercase', 'letter-spacing': '0.05em', color: '#6B7280' }}>
Daily
</p>
<p style={{ margin: '4px 0 2px', 'font-size': '18px', 'font-weight': '700', color: '#111827' }}>
{usage()?.daily_limit !== undefined && usage()?.daily_used !== undefined
? Math.max(0, (usage()?.daily_limit ?? 10) - (usage()?.daily_used ?? 0))
: '—'}
</p>
<div style={{ height: '4px', background: '#E5E7EB', 'border-radius': '2px', overflow: 'hidden' }}>
<div style={{ width: `${dailyPercent()}%`, height: '100%', background: dailyPercent() > 80 ? '#EF4444' : '#3B82F6' }} />
</div>
<p style={{ margin: '2px 0 0', 'font-size': '10px', color: '#9CA3AF' }}>
of {usage()?.daily_limit ?? 10}
</p>
</div>
<div style={{ 'grid-column': '1 / -1', display: 'flex', 'align-items': 'center', gap: '16px', 'margin-top': '4px' }}>
<div style={{ display: 'flex', 'align-items': 'center', gap: '6px' }}>
<Zap size={12} style={{ color: '#F59E0B' }} />
<span style={{ 'font-size': '11px', color: '#6B7280' }}>Add-on</span>
<span style={{ 'font-size': '13px', 'font-weight': '600', color: '#111827' }}>
{usage()?.addon_balance ?? 0}
</span>
</div>
<div style={{ display: 'flex', 'align-items': 'center', gap: '6px' }}>
<Calendar size={12} style={{ color: '#6B7280' }} />
<span style={{ 'font-size': '11px', color: '#6B7280' }}>Renews</span>
<span style={{ 'font-size': '13px', 'font-weight': '600', color: '#111827' }}>
{formatDate(usage()?.renewal_date ?? null)}
</span>
</div>
</div>
</div>
</DashboardWidget>
);
}