feat(ai): integrate AI usage/credits into dashboard and chat widget

This commit is contained in:
Ashwin Kumar Sivakumar 2026-06-15 06:19:05 +05:30
parent 82700a68c9
commit 491ade57c0
3 changed files with 514 additions and 10 deletions

View file

@ -13,6 +13,9 @@ concurrency:
jobs:
build:
runs-on: docker-ready
env:
DOCKER_HOST: tcp://127.0.0.1:2375
DOCKER_BUILDKIT: "1"
steps:
- name: Checkout
uses: actions/checkout@v4
@ -22,6 +25,12 @@ jobs:
- name: Set up Docker Buildx
run: |
set -euo pipefail
for attempt in $(seq 1 30); do
if docker version >/dev/null 2>&1; then
break
fi
sleep 2
done
docker version
docker buildx create --use --name nxtgauge-builder || docker buildx use nxtgauge-builder
docker buildx inspect --bootstrap

View file

@ -14,6 +14,19 @@ interface ChatResponse {
conversation_id: string;
intent: string;
confidence: number;
remaining_credits?: number;
remaining_daily_actions?: number;
credits_charged?: number;
}
interface UsageStatus {
remaining_credits: number;
remaining_daily_actions: number;
daily_action_limit: number;
plan_code: string;
plan_name: string;
monthly_credits_total: number;
monthly_credits_used: number;
}
export function AiChatWidget() {
@ -28,6 +41,31 @@ export function AiChatWidget() {
const [input, setInput] = createSignal("");
const [isLoading, setIsLoading] = createSignal(false);
const [conversationId, setConversationId] = createSignal("");
const [usage, setUsage] = createSignal<UsageStatus | null>(null);
onMount(() => {
fetchUsage();
});
const fetchUsage = async () => {
try {
const res = await fetch(`${API}/api/ai/usage`);
if (!res.ok) return;
const data = await res.json();
const plan = 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,
});
} catch (err) {
console.error("Failed to fetch AI usage", err);
}
};
const toggleChat = () => setIsOpen((v) => !v);
@ -57,6 +95,19 @@ export function AiChatWidget() {
setConversationId(data.conversation_id);
}
// Update usage state from response if available
if (typeof data.remaining_credits === "number") {
setUsage((prev) =>
prev
? {
...prev,
remaining_credits: data.remaining_credits!,
remaining_daily_actions: data.remaining_daily_actions ?? prev.remaining_daily_actions,
}
: null
);
}
const assistantMessage: ChatMessage = {
role: "assistant",
content: data.message,
@ -154,6 +205,13 @@ export function AiChatWidget() {
<p style={{ margin: 0, color: "#fff", "font-weight": "700", "font-size": "15px" }}>
AI Assistant
</p>
<Show when={usage()}>
{(u) => (
<p style={{ margin: "2px 0 0", color: "rgba(255,255,255,0.85)", "font-size": "11px" }}>
{u().plan_name} · {u().remaining_credits} credits · {u().remaining_daily_actions}/{u().daily_action_limit} today
</p>
)}
</Show>
</div>
</div>
<button

View file

@ -26,8 +26,38 @@ type Payment = {
package_name?: string;
};
type AiCreditPackage = {
id: string;
name: string;
description?: string;
credits: number;
price_inr: number;
};
type AiUsageLog = {
id: string;
feature_code: string;
model_alias: string;
credits_charged: number;
status: string;
created_at: string;
};
type AiUsageSummary = {
remaining_credits: number;
remaining_daily_actions: number;
daily_action_limit: number;
plan_code: string;
plan_name: string;
monthly_credits_total: number;
monthly_credits_used: number;
purchased_credits_total: number;
purchased_credits_used: number;
};
type CheckoutState = {
package: Package | null;
aiCreditPackage: AiCreditPackage | null;
orderId: string | null;
step: "form" | "processing" | "success" | "error";
error: string;
@ -55,15 +85,19 @@ export default function CreditsPage(props: Props) {
const [ledger, setLedger] = createSignal<any[]>([]);
const [packages, setPackages] = createSignal<Package[]>([]);
const [payments, setPayments] = createSignal<Payment[]>([]);
const [aiPackages, setAiPackages] = createSignal<AiCreditPackage[]>([]);
const [aiUsage, setAiUsage] = createSignal<AiUsageLog[]>([]);
const [aiSummary, setAiSummary] = createSignal<AiUsageSummary | null>(null);
const [loading, setLoading] = createSignal(true);
const [loadingPayments, setLoadingPayments] = createSignal(false);
const [err, setErr] = createSignal("");
const [msg, setMsg] = createSignal("");
const [activeTab, setActiveTab] = createSignal<"overview" | "buy_credits" | "transactions" | "usage_history">("overview");
const [activeTab, setActiveTab] = createSignal<"overview" | "buy_credits" | "buy_ai_credits" | "transactions" | "usage_history" | "ai_usage">("overview");
// Checkout modal state
const [checkout, setCheckout] = createSignal<CheckoutState>({
package: null,
aiCreditPackage: null,
orderId: null,
step: "form",
error: "",
@ -157,10 +191,68 @@ export default function CreditsPage(props: Props) {
}
};
const loadAiPackages = async () => {
try {
const res = await apiFetch("/api/ai-credits");
const data = await res.json().catch(() => ({}));
if (res.ok) {
setAiPackages(
Array.isArray(data?.packages)
? data.packages.map((p: any) => ({
id: p.id,
name: p.name,
description: p.description,
credits: p.credits,
price_inr: p.price_inr,
}))
: []
);
}
} catch {
setAiPackages([]);
}
};
const loadAiUsage = async () => {
try {
const [summaryRes, logsRes] = await Promise.all([
apiFetch("/api/ai/usage"),
apiFetch("/api/admin/ai/users/me/usage"),
]);
const summaryData = await summaryRes.json().catch(() => ({}));
const logsData = await logsRes.json().catch(() => ({}));
if (summaryRes.ok && summaryData.plan) {
const p = 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,
purchased_credits_total: p.purchased_credits_total ?? 0,
purchased_credits_used: p.purchased_credits_used ?? 0,
});
}
if (logsRes.ok) {
setAiUsage(Array.isArray(logsData?.logs) ? logsData.logs : []);
}
} catch {
setAiUsage([]);
}
};
const loadAllData = async () => {
setLoading(true);
setErr("");
await Promise.all([loadWalletData(), loadPackages(), loadPayments()]);
await Promise.all([
loadWalletData(),
loadPackages(),
loadPayments(),
loadAiPackages(),
loadAiUsage(),
]);
setLoading(false);
};
@ -171,15 +263,97 @@ export default function CreditsPage(props: Props) {
setCardExpiry("");
setCardCvv("");
setCardName("");
setCheckout({ package: pkg, orderId: null, step: "form", error: "" });
setCheckout({ package: pkg, aiCreditPackage: null, orderId: null, step: "form", error: "" });
};
const openAiCreditCheckout = (pkg: AiCreditPackage) => {
setCardNumber("");
setCardExpiry("");
setCardCvv("");
setCardName("");
setCheckout({ package: null, aiCreditPackage: pkg, orderId: null, step: "form", error: "" });
};
const closeCheckout = () => {
setCheckout({ package: null, orderId: null, step: "form", error: "" });
setCheckout({ package: null, aiCreditPackage: null, orderId: null, step: "form", error: "" });
};
const processAiCreditPayment = async (aiPkg: AiCreditPackage) => {
// Validate card fields
if (cardNumber().replace(/\s/g, "").length < 16) {
setCheckout((c) => ({ ...c, error: "Please enter a valid card number" }));
return;
}
if (!cardExpiry() || cardExpiry().length < 5) {
setCheckout((c) => ({ ...c, error: "Please enter expiry date (MM/YY)" }));
return;
}
if (cardCvv().length < 3) {
setCheckout((c) => ({ ...c, error: "Please enter valid CVV" }));
return;
}
setCheckout((c) => ({ ...c, step: "processing", error: "" }));
try {
const orderRes = await apiFetch("/api/ai-credits/order", {
method: "POST",
body: JSON.stringify({ package_id: aiPkg.id }),
});
const orderData = await orderRes.json().catch(() => ({}));
if (!orderRes.ok) {
setCheckout((c) => ({
...c,
step: "error",
error: orderData.error || orderData.message || "Failed to create order",
}));
return;
}
const orderId = orderData.order_id;
await new Promise((resolve) => setTimeout(resolve, 2000));
const verifyRes = await apiFetch("/api/ai-credits/verify", {
method: "POST",
body: JSON.stringify({
order_id: orderId,
payment_id: "pay_mock_" + Date.now(),
signature: "mock_signature",
}),
});
const verifyData = await verifyRes.json().catch(() => ({}));
if (verifyRes.ok && verifyData.verified) {
setCheckout((c) => ({ ...c, step: "success", orderId }));
setMsg(`Payment successful! ${verifyData.credits_added} AI credits have been added.`);
loadAllData();
} else {
setCheckout((c) => ({
...c,
step: "error",
error: verifyData.error || "Payment verification failed",
}));
}
} catch (e: any) {
setCheckout((c) => ({
...c,
step: "error",
error: e.message || "Payment failed. Please try again.",
}));
}
};
const processPayment = async () => {
const pkg = checkout().package;
const aiPkg = checkout().aiCreditPackage;
if (!pkg && !aiPkg) return;
if (aiPkg) {
await processAiCreditPayment(aiPkg);
return;
}
if (!pkg) return;
// Validate card fields
@ -291,7 +465,11 @@ export default function CreditsPage(props: Props) {
const CheckoutModal = () => {
const state = checkout();
const pkg = state.package;
if (!pkg) return null;
const aiPkg = state.aiCreditPackage;
if (!pkg && !aiPkg) return null;
const title = pkg ? `${pkg.tracecoins_amount} Tracecoins` : `${aiPkg!.credits} AI Credits`;
const price = pkg ? pkg.price : aiPkg!.price_inr;
return (
<Portal>
@ -373,7 +551,7 @@ export default function CreditsPage(props: Props) {
color: "#111827",
}}
>
{pkg.name}
{pkg ? pkg.name : aiPkg?.name}
</p>
<p
style={{
@ -383,10 +561,10 @@ export default function CreditsPage(props: Props) {
color: "#FF5E13",
}}
>
{formatCurrency(pkg.price)}
{formatCurrency(price)}
</p>
<p style={{ margin: "8px 0 0", "font-size": "14px", color: "#15803D" }}>
+{pkg.tracecoins_amount} Tracecoins
+{title}
</p>
</div>
</Show>
@ -399,7 +577,7 @@ export default function CreditsPage(props: Props) {
Payment successful!
</p>
<p style={{ margin: "0 0 16px", "font-size": "14px", color: "#6B7280" }}>
{pkg.tracecoins_amount} Tracecoins have been credited to your wallet.
{title} have been credited to your account.
</p>
<p style={{ margin: "0", "font-size": "12px", color: "#9CA3AF" }}>
Order ID: {state.orderId}
@ -623,7 +801,7 @@ export default function CreditsPage(props: Props) {
"font-weight": "700",
}}
>
Pay {formatCurrency(pkg.price)}
Pay {formatCurrency(price)}
</button>
<p
@ -756,6 +934,38 @@ export default function CreditsPage(props: Props) {
>
Buy Credits
</button>
<button
type="button"
onClick={() => setActiveTab("buy_ai_credits")}
style={{
padding: "10px 16px",
"border-bottom": activeTab() === "buy_ai_credits" ? "2px solid #FF5E13" : "2px solid transparent",
background: "transparent",
color: activeTab() === "buy_ai_credits" ? "#FF5E13" : "#6B7280",
"font-size": "14px",
"font-weight": activeTab() === "buy_ai_credits" ? "700" : "500",
cursor: "pointer",
"margin-bottom": "-1px",
}}
>
AI Credits
</button>
<button
type="button"
onClick={() => setActiveTab("ai_usage")}
style={{
padding: "10px 16px",
"border-bottom": activeTab() === "ai_usage" ? "2px solid #FF5E13" : "2px solid transparent",
background: "transparent",
color: activeTab() === "ai_usage" ? "#FF5E13" : "#6B7280",
"font-size": "14px",
"font-weight": activeTab() === "ai_usage" ? "700" : "500",
cursor: "pointer",
"margin-bottom": "-1px",
}}
>
AI Usage
</button>
<button
type="button"
onClick={() => setActiveTab("transactions")}
@ -843,6 +1053,33 @@ export default function CreditsPage(props: Props) {
</p>
</div>
<div style={CARD}>
<p
style={{
margin: "0",
"font-size": "12px",
"letter-spacing": "0.06em",
"text-transform": "uppercase",
color: "#6B7280",
}}
>
AI Plan
</p>
<p
style={{
margin: "6px 0 0",
"font-size": "24px",
"font-weight": "800",
color: NAVY,
}}
>
{aiSummary()?.plan_name ?? "Free"}
</p>
<p style={{ margin: "6px 0 0", "font-size": "13px", color: "#6B7280" }}>
{aiSummary()?.remaining_credits ?? 0} credits remaining · {aiSummary()?.remaining_daily_actions ?? 0}/{aiSummary()?.daily_action_limit ?? 0} daily actions
</p>
</div>
<div style={CARD}>
<p
style={{
@ -1149,6 +1386,206 @@ export default function CreditsPage(props: Props) {
</div>
</Show>
{/* Buy AI Credits Tab */}
<Show when={!loading() && activeTab() === "buy_ai_credits"}>
<div style={CARD}>
<p
style={{
margin: "0 0 10px",
"font-size": "16px",
"font-weight": "700",
color: "#111827",
}}
>
Purchase AI Credits
</p>
<p style={{ margin: "0 0 16px", "font-size": "13px", color: "#6B7280" }}>
Buy additional AI credits to power Ask Ash, cover letters, auto-apply, and more.
</p>
<Show when={aiPackages().length === 0}>
<p style={{ margin: "0", "font-size": "13px", color: "#6B7280" }}>
No AI credit packages available.
</p>
</Show>
<Show when={aiPackages().length > 0}>
<div
style={{
display: "grid",
"grid-template-columns": "repeat(auto-fill, minmax(260px, 1fr))",
gap: "16px",
}}
>
<For each={aiPackages()}>
{(pkg) => (
<div
style={{
border: "1px solid #E5E7EB",
"border-radius": "16px",
padding: "20px",
background: "#fff",
display: "flex",
"flex-direction": "column",
gap: "12px",
}}
>
<p
style={{
margin: "0",
"font-size": "18px",
"font-weight": "800",
color: "#111827",
}}
>
{pkg.name}
</p>
<p
style={{
margin: "0",
"font-size": "28px",
"font-weight": "800",
color: "#FF5E13",
}}
>
{formatCurrency(pkg.price_inr)}
</p>
<p style={{ margin: "0", "font-size": "14px", color: "#15803D", "font-weight": "700" }}>
+{pkg.credits} AI Credits
</p>
<Show when={pkg.description}>
<p style={{ margin: "0", "font-size": "13px", color: "#6B7280", flex: "1" }}>
{pkg.description}
</p>
</Show>
<button
type="button"
onClick={() => openAiCreditCheckout(pkg)}
style={{
...BTN_PRIMARY,
width: "100%",
"margin-top": "auto",
padding: "12px 16px",
"font-size": "14px",
"border-radius": "10px",
}}
>
Buy Now
</button>
</div>
)}
</For>
</div>
</Show>
</div>
</Show>
{/* AI Usage Tab */}
<Show when={!loading() && activeTab() === "ai_usage"}>
<div style={CARD}>
<p
style={{
margin: "0 0 10px",
"font-size": "16px",
"font-weight": "700",
color: "#111827",
}}
>
AI Usage History
</p>
<Show when={aiSummary()}>
{(s) => (
<div
style={{
display: "grid",
"grid-template-columns": "repeat(auto-fit, minmax(140px, 1fr))",
gap: "12px",
margin: "0 0 16px",
}}
>
<div style={{ background: "#F9FAFB", "border-radius": "10px", padding: "12px" }}>
<p style={{ margin: "0", "font-size": "12px", color: "#6B7280" }}>Plan</p>
<p style={{ margin: "4px 0 0", "font-size": "16px", "font-weight": "700", color: "#111827" }}>
{s().plan_name}
</p>
</div>
<div style={{ background: "#F9FAFB", "border-radius": "10px", padding: "12px" }}>
<p style={{ margin: "0", "font-size": "12px", color: "#6B7280" }}>Credits Remaining</p>
<p style={{ margin: "4px 0 0", "font-size": "16px", "font-weight": "700", color: "#111827" }}>
{s().remaining_credits}
</p>
</div>
<div style={{ background: "#F9FAFB", "border-radius": "10px", padding: "12px" }}>
<p style={{ margin: "0", "font-size": "12px", color: "#6B7280" }}>Daily Actions</p>
<p style={{ margin: "4px 0 0", "font-size": "16px", "font-weight": "700", color: "#111827" }}>
{s().remaining_daily_actions}/{s().daily_action_limit}
</p>
</div>
<div style={{ background: "#F9FAFB", "border-radius": "10px", padding: "12px" }}>
<p style={{ margin: "0", "font-size": "12px", color: "#6B7280" }}>Monthly Used</p>
<p style={{ margin: "4px 0 0", "font-size": "16px", "font-weight": "700", color: "#111827" }}>
{s().monthly_credits_used}/{s().monthly_credits_total}
</p>
</div>
</div>
)}
</Show>
<Show when={aiUsage().length === 0}>
<p style={{ margin: "0", "font-size": "13px", color: "#6B7280" }}>
No AI usage recorded yet.
</p>
</Show>
<Show when={aiUsage().length > 0}>
<div style={{ display: "grid", gap: "8px" }}>
<For each={aiUsage()}>
{(item) => (
<div
style={{
border: "1px solid #E5E7EB",
"border-radius": "10px",
padding: "12px",
background: "#FCFCFD",
display: "flex",
"justify-content": "space-between",
"align-items": "center",
gap: "10px",
}}
>
<div>
<p
style={{
margin: "0",
"font-size": "13px",
"font-weight": "700",
color: "#111827",
}}
>
{item.feature_code}
</p>
<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}>
{item.model_alias} · {formatDate(item.created_at)}
</p>
</div>
<p
style={{
margin: "0",
"font-size": "14px",
"font-weight": "800",
color: item.status === "success" ? "#B91C1C" : "#6B7280",
}}
>
-{item.credits_charged} credits
</p>
</div>
)}
</For>
</div>
</Show>
</div>
</Show>
{/* Transactions Tab */}
<Show when={!loading() && activeTab() === "transactions"}>
<div style={CARD}>