From 5973e6216e685f8779920d19a377efe657f8bd0b Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Sivakumar Date: Mon, 15 Jun 2026 09:39:26 +0530 Subject: [PATCH] fix(ai): align frontend with backend ai routes --- src/components/AiChatWidget.tsx | 18 ++-- src/components/dashboard/CompanyJobsPage.tsx | 45 ++++++--- src/components/dashboard/CreditsPage.tsx | 22 ++--- .../dashboard/widgets/AiUsageWidget.tsx | 15 +-- src/hooks/useAiCredits.ts | 99 +++++++++++++++---- 5 files changed, 138 insertions(+), 61 deletions(-) diff --git a/src/components/AiChatWidget.tsx b/src/components/AiChatWidget.tsx index 37fe9a4..b3ad0a4 100644 --- a/src/components/AiChatWidget.tsx +++ b/src/components/AiChatWidget.tsx @@ -50,18 +50,18 @@ export function AiChatWidget() { const fetchUsage = async () => { try { - const res = await fetch(`${API}/api/ai/usage`); + const res = await fetch(`${API}/api/ai/usage/summary`); if (!res.ok) return; const data = await res.json(); - const plan = data.plan || {}; + const plan = data.plan_details || data.plan || {}; setUsage({ - remaining_credits: plan.remaining_credits ?? 0, - remaining_daily_actions: plan.remaining_daily_actions ?? 0, - daily_action_limit: plan.daily_action_limit ?? 0, - plan_code: plan.plan_code ?? "free", - plan_name: plan.plan_name ?? "Free", - monthly_credits_total: plan.monthly_credits_total ?? 0, - monthly_credits_used: plan.monthly_credits_used ?? 0, + remaining_credits: plan.remaining_credits ?? data.monthly_remaining ?? 0, + remaining_daily_actions: plan.remaining_daily_actions ?? data.daily_remaining ?? 0, + daily_action_limit: plan.daily_action_limit ?? data.daily_limit ?? 0, + plan_code: plan.plan_code ?? data.plan_code ?? "free", + plan_name: plan.plan_name ?? data.plan ?? "Free", + monthly_credits_total: plan.monthly_credits_total ?? data.monthly_limit ?? 0, + monthly_credits_used: plan.monthly_credits_used ?? data.monthly_used ?? 0, }); } catch (err) { console.error("Failed to fetch AI usage", err); diff --git a/src/components/dashboard/CompanyJobsPage.tsx b/src/components/dashboard/CompanyJobsPage.tsx index 7c8dbf4..32139ec 100644 --- a/src/components/dashboard/CompanyJobsPage.tsx +++ b/src/components/dashboard/CompanyJobsPage.tsx @@ -5,8 +5,9 @@ * POST /api/companies/jobs - Create new job * PATCH /api/companies/jobs/:id - Update job * DELETE /api/companies/jobs/:id - Delete job - * POST /api/ai/generate-job-field - AI job field generation - * GET /api/ai/usage - AI usage status + * POST /api/ai/company/jobs/generate-description - AI job description generation + * POST /api/ai/company/jobs/extract-skills - AI job skill extraction + * GET /api/ai/usage/summary - AI usage status */ import { For, Show, createMemo, createSignal, onMount } from "solid-js"; import { Sparkles, Loader } from "lucide-solid"; @@ -162,14 +163,17 @@ export default function CompanyJobsPage() { const loadAiUsage = async () => { const token = window.sessionStorage.getItem("nxtgauge_access_token") || ""; - const res = await fetch(`${API}/api/ai/usage`, { + const res = await fetch(`${API}/api/ai/usage/summary`, { headers: { Authorization: `Bearer ${token}` }, }); if (res.ok) { const data = await res.json(); - setAiRemaining(data.remaining_today ?? 0); - setAiLimit(data.daily_limit ?? 5); - setHasAiPack(data.has_ai_pack ?? false); + const plan = data.plan_details || data.plan || {}; + const remainingDaily = plan.remaining_daily_actions ?? data.daily_remaining ?? 0; + const dailyLimit = plan.daily_action_limit ?? data.daily_limit ?? 5; + setAiRemaining(remainingDaily); + setAiLimit(dailyLimit); + setHasAiPack((data.addon_balance ?? 0) > 0 || (plan.remaining_credits ?? data.monthly_remaining ?? 0) > 0); } }; @@ -188,14 +192,24 @@ export default function CompanyJobsPage() { const context = form().title || form().description || "job posting"; const token = window.sessionStorage.getItem("nxtgauge_access_token") || ""; + const endpoint = + field === "description" + ? "/api/ai/company/jobs/generate-description" + : field === "skills" + ? "/api/ai/company/jobs/extract-skills" + : "/api/ai/generate-job-field"; + const body = + endpoint === "/api/ai/generate-job-field" + ? { field, context } + : { context }; try { - const res = await fetch(`${API}/api/ai/generate-job-field`, { + const res = await fetch(`${API}${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - body: JSON.stringify({ field, context }), + body: JSON.stringify(body), }); if (res.status === 429) { @@ -204,14 +218,15 @@ export default function CompanyJobsPage() { } const data = await res.json(); - if (res.ok && data.generated_text) { - if (field === "title") setField("title", data.generated_text.substring(0, 100)); - else if (field === "description") setField("description", data.generated_text); - else if (field === "skills") setField("skills", data.generated_text); - else if (field === "category") setField("category", data.generated_text.substring(0, 60)); - setAiRemaining(data.remaining_today ?? aiRemaining() - 1); + const generatedText = data.generated_text || data.skills; + if (res.ok && generatedText) { + if (field === "title") setField("title", String(generatedText).substring(0, 100)); + else if (field === "description") setField("description", String(generatedText)); + else if (field === "skills") setField("skills", String(generatedText)); + else if (field === "category") setField("category", String(generatedText).substring(0, 60)); + setAiRemaining(data.remaining_today ?? data.remaining_daily_actions ?? Math.max(0, aiRemaining() - 1)); setAiLimit(data.daily_limit ?? aiLimit()); - setHasAiPack(data.has_ai_pack ?? hasAiPack()); + setHasAiPack(hasAiPack()); } else { setError(data.error || "Generation failed"); } diff --git a/src/components/dashboard/CreditsPage.tsx b/src/components/dashboard/CreditsPage.tsx index 00f3ada..9467d76 100644 --- a/src/components/dashboard/CreditsPage.tsx +++ b/src/components/dashboard/CreditsPage.tsx @@ -216,21 +216,21 @@ export default function CreditsPage(props: Props) { const loadAiUsage = async () => { try { const [summaryRes, logsRes] = await Promise.all([ - apiFetch("/api/ai/usage"), - apiFetch("/api/admin/ai/users/me/usage"), + apiFetch("/api/ai/usage/summary"), + apiFetch("/api/ai/usage/logs"), ]); const summaryData = await summaryRes.json().catch(() => ({})); const logsData = await logsRes.json().catch(() => ({})); - if (summaryRes.ok && summaryData.plan) { - const p = summaryData.plan; + if (summaryRes.ok) { + const p = summaryData.plan_details || summaryData.plan || {}; setAiSummary({ - remaining_credits: p.remaining_credits ?? 0, - remaining_daily_actions: p.remaining_daily_actions ?? 0, - daily_action_limit: p.daily_action_limit ?? 0, - plan_code: p.plan_code ?? "free", - plan_name: p.plan_name ?? "Free", - monthly_credits_total: p.monthly_credits_total ?? 0, - monthly_credits_used: p.monthly_credits_used ?? 0, + remaining_credits: p.remaining_credits ?? summaryData.monthly_remaining ?? 0, + remaining_daily_actions: p.remaining_daily_actions ?? summaryData.daily_remaining ?? 0, + daily_action_limit: p.daily_action_limit ?? summaryData.daily_limit ?? 0, + plan_code: p.plan_code ?? summaryData.plan_code ?? "free", + plan_name: p.plan_name ?? summaryData.plan ?? "Free", + monthly_credits_total: p.monthly_credits_total ?? summaryData.monthly_limit ?? 0, + monthly_credits_used: p.monthly_credits_used ?? summaryData.monthly_used ?? 0, purchased_credits_total: p.purchased_credits_total ?? 0, purchased_credits_used: p.purchased_credits_used ?? 0, }); diff --git a/src/components/dashboard/widgets/AiUsageWidget.tsx b/src/components/dashboard/widgets/AiUsageWidget.tsx index 9fbeb8c..b85b6f2 100644 --- a/src/components/dashboard/widgets/AiUsageWidget.tsx +++ b/src/components/dashboard/widgets/AiUsageWidget.tsx @@ -38,16 +38,17 @@ async function fetchAiUsage(roleKey: RoleKey): Promise { const prefix = ROLE_PREFIXES[roleKey]; if (!prefix) return null; try { - const res = await apiFetch(`/api/ai/usage/v2`); + const res = await apiFetch(`/api/ai/usage/summary`); if (!res.ok) return null; const data = await res.json(); + const plan = data.plan_details || data.plan || {}; 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, + plan: plan.plan_name || data.plan || 'Free', + monthly_limit: plan.monthly_credits_total || data.monthly_limit || 50, + monthly_used: plan.monthly_credits_used || data.monthly_used || 0, + monthly_remaining: plan.remaining_credits || data.monthly_remaining || 50, + daily_limit: plan.daily_action_limit || data.daily_limit || 10, + daily_used: data.daily_used || ((plan.daily_action_limit || data.daily_limit || 0) - (plan.remaining_daily_actions || data.daily_remaining || 0)), addon_balance: data.addon_balance || 0, renewal_date: data.renewal_date || null, }; diff --git a/src/hooks/useAiCredits.ts b/src/hooks/useAiCredits.ts index f84811d..56be39b 100644 --- a/src/hooks/useAiCredits.ts +++ b/src/hooks/useAiCredits.ts @@ -7,6 +7,10 @@ const API_BASE = import.meta.env.VITE_API_BASE_URL || ''; export interface CreditBalance { credits: number; tracecoin_balance: number; + plan_code?: string; + plan_name?: string; + daily_action_limit?: number; + remaining_daily_actions?: number; } export interface GenerateRequest { @@ -18,6 +22,7 @@ export interface GenerateResponse { content: string; credits_used: number; remaining_credits: number; + remaining_daily_actions?: number; } export interface PurchaseRequest { @@ -32,6 +37,30 @@ export interface PurchaseResponse { transaction_id: string; } +function authHeaders(): HeadersInit { + const token = typeof window !== 'undefined' + ? window.sessionStorage.getItem('nxtgauge_access_token') || '' + : ''; + return { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; +} + +function mapPurchaseAmountToAddonCode(amount: number): string { + if (amount <= 50) return 'STARTER'; + if (amount <= 150) return 'GROWTH'; + if (amount <= 500) return 'POWER'; + return 'ENTERPRISE'; +} + +function stringifyContext(context: Record): string { + return Object.entries(context) + .filter(([, value]) => value !== null && value !== undefined && String(value).trim() !== '') + .map(([key, value]) => `${key}: ${String(value)}`) + .join('\n'); +} + export interface ApiError { code: string; message: string; @@ -47,11 +76,9 @@ export const CREDIT_COSTS: Record = { // Fetch credit balance from API async function fetchCredits(): Promise { - const response = await fetch(`${API_BASE}/api/ai/credits`, { + const response = await fetch(`${API_BASE}/api/ai/credits/balance`, { method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, + headers: authHeaders(), credentials: 'include', }); @@ -63,20 +90,44 @@ async function fetchCredits(): Promise { throw new Error(error.message); } - return response.json(); + const data = await response.json(); + return { + credits: data.remaining_credits ?? 0, + tracecoin_balance: 0, + plan_code: data.plan_code, + plan_name: data.plan_name, + daily_action_limit: data.daily_limit, + remaining_daily_actions: Math.max(0, (data.daily_limit ?? 0) - (data.daily_used ?? 0)), + }; } // Generate AI content export async function generateContent( request: GenerateRequest ): Promise { - const response = await fetch(`${API_BASE}/api/ai/generate`, { + const context = stringifyContext(request.context); + + let path = '/api/ai/help/ask'; + let body: Record = { message: context || request.type }; + + if (request.type === 'job_description') { + path = '/api/ai/company/jobs/generate-description'; + body = { context }; + } else if (request.type === 'resume_review') { + path = '/api/ai/help/ask'; + body = { message: `Review and improve this resume content: +${context}` }; + } else if (request.type === 'interview_question') { + path = '/api/ai/help/ask'; + body = { message: `Generate interview guidance or questions for: +${context}` }; + } + + const response = await fetch(`${API_BASE}${path}`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: authHeaders(), credentials: 'include', - body: JSON.stringify(request), + body: JSON.stringify(body), }); if (!response.ok) { @@ -84,28 +135,32 @@ export async function generateContent( code: 'GENERATE_ERROR', message: `Failed to generate content: ${response.statusText}`, })); - + if (response.status === 402) { throw new Error('Insufficient credits. Please purchase more credits to continue.'); } - + throw new Error(error.message); } - return response.json(); + const data = await response.json(); + return { + content: data.generated_text || data.message || data.reply || '', + credits_used: data.credits_charged || 0, + remaining_credits: data.remaining_credits || 0, + remaining_daily_actions: data.remaining_daily_actions || data.remaining_today, + }; } // Purchase credits export async function purchaseCredits( request: PurchaseRequest ): Promise { - const response = await fetch(`${API_BASE}/api/ai/credits/purchase`, { + const response = await fetch(`${API_BASE}/api/ai/credits/buy`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: authHeaders(), credentials: 'include', - body: JSON.stringify(request), + body: JSON.stringify({ addon_code: mapPurchaseAmountToAddonCode(request.amount) }), }); if (!response.ok) { @@ -116,7 +171,13 @@ export async function purchaseCredits( throw new Error(error.message); } - return response.json(); + const data = await response.json(); + return { + success: !!data.success, + credits_added: request.amount, + new_balance: data.addon_balance || 0, + transaction_id: data.transaction_id || `addon:${mapPurchaseAmountToAddonCode(request.amount)}`, + }; } // Main hook for credit management