nxtgauge-admin-solid/src/routes/admin/ai-management.tsx
2026-06-15 06:19:17 +05:30

312 lines
13 KiB
TypeScript

import { createSignal, Show, For, createResource } from 'solid-js';
const API = '/api/admin/ai';
function getToken(): string {
return typeof sessionStorage !== 'undefined'
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
: '';
}
function authHeaders(): Record<string, string> {
return {
Accept: 'application/json',
'Content-Type': 'application/json',
...(getToken() ? { Authorization: `Bearer ${getToken()}` } : {}),
};
}
async function fetchJson(path: string, opts?: RequestInit) {
const res = await fetch(`${API}${path}`, {
...opts,
credentials: 'include',
headers: authHeaders(),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || 'Request failed');
return data;
}
export default function AiManagementPage() {
const [activeTab, setActiveTab] = createSignal<'plans' | 'features' | 'user' | 'usage'>('plans');
const [userId, setUserId] = createSignal('');
const [planCode, setPlanCode] = createSignal('');
const [creditAmount, setCreditAmount] = createSignal(10);
const [loading, setLoading] = createSignal(false);
const [msg, setMsg] = createSignal('');
const [err, setErr] = createSignal('');
const [plans] = createResource(() => activeTab() === 'plans', () => fetchJson('/plans'));
const [features] = createResource(() => activeTab() === 'features', () => fetchJson('/features'));
const handleUpdatePlan = async (e: Event) => {
e.preventDefault();
setLoading(true);
setMsg('');
setErr('');
try {
await fetchJson(`/users/${userId().trim()}/plan`, {
method: 'POST',
body: JSON.stringify({ plan_code: planCode() }),
});
setMsg(`Plan updated to ${planCode()} for user ${userId()}`);
} catch (e: any) {
setErr(e.message);
} finally {
setLoading(false);
}
};
const handleAddCredits = async (e: Event) => {
e.preventDefault();
setLoading(true);
setMsg('');
setErr('');
try {
await fetchJson(`/users/${userId().trim()}/credits`, {
method: 'POST',
body: JSON.stringify({ credits: creditAmount(), source: 'admin', description: 'Manual admin credit grant' }),
});
setMsg(`${creditAmount()} AI credits added to user ${userId()}`);
} catch (e: any) {
setErr(e.message);
} finally {
setLoading(false);
}
};
const tabs: { key: 'plans' | 'features' | 'user' | 'usage'; label: string }[] = [
{ key: 'plans', label: 'Plans' },
{ key: 'features', label: 'Features' },
{ key: 'user', label: 'User Management' },
{ key: 'usage', label: 'Usage Lookup' },
];
return (
<div class="w-full space-y-6 pb-8">
<div style="margin-bottom:1.5rem">
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">AI Management</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">Manage AI plans, feature costs, user credits, and usage.</p>
</div>
<div class="bg-white border-b border-gray-200 px-6 flex items-center gap-8 sticky top-0 z-10">
<For each={tabs}>
{(tab) => (
<button
type="button"
class={activeTab() === tab.key ? 'py-3 border-b-2 border-orange-500 text-orange-600 text-sm font-medium' : 'py-3 border-b-2 border-transparent text-gray-500 hover:text-gray-700 text-sm font-medium transition-colors'}
onClick={() => setActiveTab(tab.key)}
>
{tab.label}
</button>
)}
</For>
</div>
<Show when={msg()}>
<div style="background:#dcfce7;border:1px solid #86efac;border-radius:6px;padding:10px 14px;font-size:14px;color:#15803d;font-weight:600">{msg()}</div>
</Show>
<Show when={err()}>
<div style="background:#fee2e2;border:1px solid #fecaca;border-radius:6px;padding:10px 14px;font-size:14px;color:#b91c1c;font-weight:600">{err()}</div>
</Show>
<Show when={activeTab() === 'plans'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700;color:#1e293b">AI Plans</h2>
<Show when={plans.loading}><p class="text-sm text-gray-500">Loading plans...</p></Show>
<Show when={plans.error}><p class="text-sm text-red-600">Failed to load plans</p></Show>
<Show when={plans()}>
{(data) => (
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Monthly Credits</th>
<th>Daily Actions</th>
<th>Models</th>
</tr>
</thead>
<tbody>
<For each={data().plans || []}>
{(plan: any) => (
<tr>
<td class="font-semibold">{plan.code}</td>
<td>{plan.name}</td>
<td>{plan.monthly_credits}</td>
<td>{plan.daily_action_limit}</td>
<td class="text-xs text-gray-500">{(plan.allowed_models || []).join(', ')}</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
)}
</Show>
</section>
</Show>
<Show when={activeTab() === 'features'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700;color:#1e293b">AI Feature Costs</h2>
<Show when={features.loading}><p class="text-sm text-gray-500">Loading features...</p></Show>
<Show when={features.error}><p class="text-sm text-red-600">Failed to load features</p></Show>
<Show when={features()}>
{(data) => (
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Feature Code</th>
<th>Display Name</th>
<th>Default Model</th>
<th>Credit Cost</th>
</tr>
</thead>
<tbody>
<For each={data().features || []}>
{(feat: any) => (
<tr>
<td class="font-semibold">{feat.feature_code}</td>
<td>{feat.display_name}</td>
<td>{feat.default_model}</td>
<td>{feat.credit_cost}</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
)}
</Show>
</section>
</Show>
<Show when={activeTab() === 'user'}>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700;color:#1e293b">Update User Plan</h2>
<form onSubmit={handleUpdatePlan} style="display:flex;flex-direction:column;gap:14px">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">User ID</label>
<input type="text" required value={userId()} onInput={(e) => setUserId(e.currentTarget.value)} style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box" />
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Plan Code</label>
<select value={planCode()} onChange={(e) => setPlanCode(e.currentTarget.value)} style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box">
<option value="">Select plan</option>
<option value="free">Free</option>
<option value="pro">Pro</option>
<option value="business">Business</option>
<option value="enterprise">Enterprise</option>
</select>
</div>
<button class="btn-primary" type="submit" disabled={loading()}>{loading() ? 'Updating...' : 'Update Plan'}</button>
</form>
</section>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700;color:#1e293b">Add AI Credits</h2>
<form onSubmit={handleAddCredits} style="display:flex;flex-direction:column;gap:14px">
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">User ID</label>
<input type="text" required value={userId()} onInput={(e) => setUserId(e.currentTarget.value)} style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box" />
</div>
<div>
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Credits</label>
<input type="number" min="1" required value={creditAmount()} onInput={(e) => setCreditAmount(Number(e.currentTarget.value))} style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box" />
</div>
<button class="btn-primary" type="submit" disabled={loading()}>{loading() ? 'Adding...' : 'Add Credits'}</button>
</form>
</section>
</div>
</Show>
<Show when={activeTab() === 'usage'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700;color:#1e293b">User Usage Lookup</h2>
<UsageLookup />
</section>
</Show>
</div>
);
}
function UsageLookup() {
const [uid, setUid] = createSignal('');
const [usage, setUsage] = createSignal<any>(null);
const [logs, setLogs] = createSignal<any[]>([]);
const [loading, setLoading] = createSignal(false);
const [err, setErr] = createSignal('');
const handleSearch = async () => {
const id = uid().trim();
if (!id) return;
setLoading(true);
setErr('');
try {
const [usageData, logsData] = await Promise.all([
fetchJson(`/users/${id}/usage`),
fetchJson(`/users/${id}/transactions`),
]);
setUsage(usageData);
setLogs(logsData.transactions || []);
} catch (e: any) {
setErr(e.message);
setUsage(null);
setLogs([]);
} finally {
setLoading(false);
}
};
return (
<div style="display:flex;flex-direction:column;gap:16px">
<div style="display:flex;gap:10px">
<input
type="text"
placeholder="Enter User ID..."
value={uid()}
onInput={(e) => setUid(e.currentTarget.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
style="flex:1;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
<button class="btn-primary" onClick={handleSearch} disabled={loading()}>{loading() ? 'Searching...' : 'Search'}</button>
</div>
<Show when={err()}><p class="text-sm text-red-600">{err()}</p></Show>
<Show when={usage()}>
{(u) => (
<div>
<p class="text-sm text-gray-600">Remaining credits: <span class="font-semibold">{u().remaining_credits}</span></p>
<p class="text-sm text-gray-600">Daily actions: <span class="font-semibold">{u().remaining_daily_actions}/{u().daily_action_limit}</span></p>
<p class="text-sm text-gray-600">Plan: <span class="font-semibold">{u().plan_name}</span></p>
</div>
)}
</Show>
<Show when={logs().length > 0}>
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr><th>Type</th><th>Credits</th><th>Balance After</th><th>Source</th><th>Date</th></tr>
</thead>
<tbody>
<For each={logs()}>
{(tx: any) => (
<tr>
<td>{tx.transaction_type}</td>
<td>{tx.credits}</td>
<td>{tx.balance_after}</td>
<td>{tx.source}</td>
<td>{new Date(tx.created_at).toLocaleString()}</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</Show>
</div>
);
}