fix(ai): align frontend with backend ai routes

This commit is contained in:
Ashwin Kumar Sivakumar 2026-06-15 09:39:26 +05:30
parent 5f535cba59
commit 5973e6216e
5 changed files with 138 additions and 61 deletions

View file

@ -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);

View file

@ -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");
}

View file

@ -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,
});

View file

@ -38,16 +38,17 @@ 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`);
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,
};

View file

@ -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, unknown>): 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<string, number> = {
// Fetch credit balance from API
async function fetchCredits(): Promise<CreditBalance> {
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<CreditBalance> {
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<GenerateResponse> {
const response = await fetch(`${API_BASE}/api/ai/generate`, {
const context = stringifyContext(request.context);
let path = '/api/ai/help/ask';
let body: Record<string, unknown> = { 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<PurchaseResponse> {
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