fix(ai): align frontend with backend ai routes
This commit is contained in:
parent
5f535cba59
commit
5973e6216e
5 changed files with 138 additions and 61 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue