chore: ai, public course and other ui improvements
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { BaseApi, classroomio } from '$lib/utils/services/api';
|
||||
import type {
|
||||
AiUsageData,
|
||||
GetLeaderboardRequest,
|
||||
GetPurchasedRequest,
|
||||
GetUsageRequest,
|
||||
LeaderboardData,
|
||||
PurchasedSummaryData
|
||||
} from '../utils/types';
|
||||
|
||||
class AiCreditsApi extends BaseApi {
|
||||
usage = $state<AiUsageData | null>(null);
|
||||
purchased = $state<PurchasedSummaryData | null>(null);
|
||||
leaderboard = $state<LeaderboardData | null>(null);
|
||||
|
||||
async fetchAll() {
|
||||
await Promise.all([this.fetchUsage(), this.fetchPurchased(), this.fetchLeaderboard()]);
|
||||
}
|
||||
|
||||
async fetchUsage() {
|
||||
await this.execute<GetUsageRequest>({
|
||||
requestFn: () => classroomio.agent.usage.$get(),
|
||||
logContext: 'fetching AI usage',
|
||||
onSuccess: (result) => {
|
||||
this.usage = result.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchPurchased() {
|
||||
await this.execute<GetPurchasedRequest>({
|
||||
requestFn: () => classroomio.agent.usage.purchased.$get(),
|
||||
logContext: 'fetching purchased summary',
|
||||
onSuccess: (result) => {
|
||||
this.purchased = result.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchLeaderboard() {
|
||||
await this.execute<GetLeaderboardRequest>({
|
||||
requestFn: () => classroomio.agent.usage.leaderboard.$get(),
|
||||
logContext: 'fetching AI usage leaderboard',
|
||||
onSuccess: (result) => {
|
||||
this.leaderboard = result.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const aiCreditsApi = new AiCreditsApi();
|
||||
@@ -0,0 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Card from '@cio/ui/base/card';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import { TOKEN_PACK } from '@cio/utils/plans';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
|
||||
interface Props {
|
||||
onBeforeRedirect?: () => void;
|
||||
}
|
||||
|
||||
let { onBeforeRedirect }: Props = $props();
|
||||
|
||||
let quantity = $state(1);
|
||||
let isRedirecting = $state(false);
|
||||
|
||||
const tokens = $derived(quantity * TOKEN_PACK.TOKENS_PER_UNIT);
|
||||
const dollars = $derived((quantity * TOKEN_PACK.PRICE_USD_CENTS) / 100);
|
||||
|
||||
function bump(delta: number) {
|
||||
quantity = Math.min(100, Math.max(1, quantity + delta));
|
||||
}
|
||||
|
||||
function buy() {
|
||||
if (!$profile.id || !$profile.email || !$currentOrg.id || !$currentOrg.siteName) {
|
||||
snackbar.error('snackbar.ai_credits.checkout_failed');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
isRedirecting = true;
|
||||
|
||||
const checkoutURL = new URL('/api/polar/buy-tokens', page.url);
|
||||
checkoutURL.searchParams.set('quantity', String(quantity));
|
||||
checkoutURL.searchParams.set('customerEmail', $profile.email?.replace('@test.com', '[email protected]') ?? '');
|
||||
|
||||
if ($profile.fullname) {
|
||||
checkoutURL.searchParams.set('customerName', $profile.fullname);
|
||||
}
|
||||
|
||||
checkoutURL.searchParams.set(
|
||||
'metadata',
|
||||
JSON.stringify({
|
||||
orgId: $currentOrg.id,
|
||||
orgSlug: $currentOrg.siteName,
|
||||
triggeredBy: $profile.id
|
||||
})
|
||||
);
|
||||
|
||||
onBeforeRedirect?.();
|
||||
window.location.href = checkoutURL.toString();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Root id="buy-tokens">
|
||||
<Card.Header>
|
||||
<Card.Title>{$t('settings.ai_credits.buy_tokens.title')}</Card.Title>
|
||||
<Card.Description>
|
||||
{$t('settings.ai_credits.buy_tokens.description')}
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" onclick={() => bump(-1)} disabled={quantity <= 1}>
|
||||
<MinusIcon class="size-4" />
|
||||
</Button>
|
||||
<span class="ui:text-muted-foreground min-w-[3rem] text-center text-sm font-medium tabular-nums">
|
||||
{quantity}
|
||||
</span>
|
||||
<Button variant="outline" size="icon" onclick={() => bump(1)} disabled={quantity >= 100}>
|
||||
<PlusIcon class="size-4" />
|
||||
</Button>
|
||||
<span class="ui:text-muted-foreground ml-2 text-xs">
|
||||
{$t('settings.ai_credits.buy_tokens.quantity_hint')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="ui:text-muted-foreground text-sm">
|
||||
{$t('settings.ai_credits.buy_tokens.preview', {
|
||||
quantity: String(quantity),
|
||||
tokens: tokens.toLocaleString(),
|
||||
dollars: dollars.toFixed(2)
|
||||
})}
|
||||
</p>
|
||||
|
||||
<Button onclick={buy} loading={isRedirecting} class="w-fit">
|
||||
{$t('settings.ai_credits.buy_tokens.buy_button', { dollars: dollars.toFixed(2) })}
|
||||
</Button>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Table from '@cio/ui/base/table';
|
||||
import * as Avatar from '@cio/ui/base/avatar';
|
||||
import type { LeaderboardData } from '$features/settings/utils/types';
|
||||
|
||||
interface Props {
|
||||
leaderboard: LeaderboardData | null;
|
||||
}
|
||||
|
||||
let { leaderboard }: Props = $props();
|
||||
|
||||
const entries = $derived(leaderboard?.entries ?? []);
|
||||
|
||||
function initials(name: string | null, email: string | null) {
|
||||
const source = (name?.trim() || email || '?').split(/\s+/).filter(Boolean);
|
||||
|
||||
return source
|
||||
.slice(0, 2)
|
||||
.map((part) => part[0]?.toUpperCase() ?? '')
|
||||
.join('');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="mb-3 flex items-baseline justify-between gap-2">
|
||||
<h3 class="text-lg font-semibold tracking-tight">{$t('settings.ai_credits.leaderboard.title')}</h3>
|
||||
<p class="ui:text-muted-foreground text-xs">{$t('settings.ai_credits.leaderboard.this_month')}</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head class="w-10">#</Table.Head>
|
||||
<Table.Head>{$t('settings.ai_credits.leaderboard.user')}</Table.Head>
|
||||
<Table.Head>{$t('settings.ai_credits.leaderboard.favorite_model')}</Table.Head>
|
||||
<Table.Head class="text-right">{$t('settings.ai_credits.leaderboard.tokens')}</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each entries as entry, index (entry.userId)}
|
||||
<Table.Row>
|
||||
<Table.Cell class="ui:text-muted-foreground">{index + 1}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div class="flex items-center gap-3">
|
||||
<Avatar.Root class="size-8">
|
||||
{#if entry.avatarUrl}
|
||||
<Avatar.Image src={entry.avatarUrl} alt={entry.fullname ?? ''} loading="lazy" decoding="async" />
|
||||
{/if}
|
||||
<Avatar.Fallback aria-hidden="true">{initials(entry.fullname, entry.email)}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<span class="truncate text-sm font-medium">{entry.fullname ?? entry.email ?? '—'}</span>
|
||||
<span class="ui:text-muted-foreground truncate text-xs">{entry.email ?? ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="ui:text-muted-foreground max-w-[12rem] truncate text-sm">
|
||||
{entry.favoriteModel ?? '—'}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-right">
|
||||
<div class="inline-flex flex-col items-end gap-1">
|
||||
<span class="text-sm font-medium">{entry.tokens.toLocaleString()}</span>
|
||||
<div class="ui:bg-muted h-1 w-24 rounded-full">
|
||||
<div
|
||||
class="ui:bg-primary h-1 rounded-full"
|
||||
style="width: {Math.min(100, Math.round(entry.percentage * 100))}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell class="ui:text-muted-foreground py-8 text-center text-sm" colspan={4}>
|
||||
{$t('settings.ai_credits.leaderboard.empty')}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import * as Chart from '@cio/ui/base/chart';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import type { AiUsageData } from '$features/settings/utils/types';
|
||||
|
||||
interface Props {
|
||||
usage: AiUsageData | null;
|
||||
}
|
||||
|
||||
let { usage }: Props = $props();
|
||||
|
||||
const data = $derived(usage?.history ?? []);
|
||||
const totalTokens = $derived(data.reduce((sum, day) => sum + day.tokens, 0));
|
||||
const totalRequests = $derived(usage?.requestsThisMonth ?? 0);
|
||||
|
||||
const chartConfig = $derived({
|
||||
tokens: {
|
||||
label: $t('settings.ai_credits.chart.tokens'),
|
||||
color: 'var(--chart-1)'
|
||||
}
|
||||
} satisfies Chart.ChartConfig);
|
||||
|
||||
const series = $derived([
|
||||
{ key: 'tokens', value: 'tokens', label: chartConfig.tokens.label, color: 'var(--color-tokens)' }
|
||||
]);
|
||||
|
||||
const hasData = $derived(data.some((day) => day.tokens > 0));
|
||||
</script>
|
||||
|
||||
<div class="bg-card flex flex-col rounded-xl border p-3 md:p-5 dark:text-white">
|
||||
<div class="mb-4 flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold tracking-tight">{$t('settings.ai_credits.chart.heading')}</h3>
|
||||
<p class="ui:text-muted-foreground mt-1 text-sm">
|
||||
{$t('settings.ai_credits.chart.subheading')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-6 text-right">
|
||||
<div>
|
||||
<p class="ui:text-muted-foreground text-xs">{$t('settings.ai_credits.chart.total_tokens')}</p>
|
||||
<p class="text-xl font-semibold">{totalTokens.toLocaleString()}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="ui:text-muted-foreground text-xs">{$t('settings.ai_credits.chart.total_requests')}</p>
|
||||
<p class="text-xl font-semibold">{totalRequests.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if hasData}
|
||||
<Chart.ChartContainer class="h-[260px] w-full" config={chartConfig}>
|
||||
<Chart.BarChart {data} x="date" axis="x" {series} />
|
||||
</Chart.ChartContainer>
|
||||
{:else}
|
||||
<div class="ui:text-muted-foreground flex h-[260px] items-center justify-center text-sm">
|
||||
{$t('settings.ai_credits.chart.empty')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Card from '@cio/ui/base/card';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import type { AiUsageData, PurchasedSummaryData } from '$features/settings/utils/types';
|
||||
|
||||
interface Props {
|
||||
usage: AiUsageData | null;
|
||||
purchased: PurchasedSummaryData | null;
|
||||
}
|
||||
|
||||
let { usage, purchased }: Props = $props();
|
||||
|
||||
const includedPercent = $derived(
|
||||
usage && usage.allowance > 0 ? Math.min(100, Math.round((usage.used / usage.allowance) * 100)) : 0
|
||||
);
|
||||
|
||||
const resetDate = $derived.by(() => {
|
||||
const nextReset = new SvelteDate();
|
||||
nextReset.setMonth(nextReset.getMonth() + 1);
|
||||
nextReset.setDate(1);
|
||||
|
||||
return nextReset.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
});
|
||||
|
||||
const dollarsSpent = $derived(purchased ? (purchased.totalSpentCents / 100).toFixed(2) : '0.00');
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Description>{$t('settings.ai_credits.included.title')}</Card.Description>
|
||||
<Card.Title class="text-2xl">
|
||||
{#if usage}
|
||||
{usage.used.toLocaleString()} / {usage.allowance.toLocaleString()}
|
||||
{:else}
|
||||
—
|
||||
{/if}
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<p class="ui:text-muted-foreground mb-3 text-xs">
|
||||
{$t('settings.ai_credits.included.subtitle')}
|
||||
</p>
|
||||
<div class="ui:bg-muted h-2 w-full rounded-full">
|
||||
<div
|
||||
class="h-2 rounded-full transition-all {includedPercent > 90
|
||||
? 'bg-red-500'
|
||||
: includedPercent > 70
|
||||
? 'bg-amber-500'
|
||||
: 'ui:bg-primary'}"
|
||||
style="width: {includedPercent}%"
|
||||
></div>
|
||||
</div>
|
||||
<p class="ui:text-muted-foreground mt-2 text-xs">
|
||||
{$t('settings.ai_credits.included.resets_on', { date: resetDate })}
|
||||
</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Description>{$t('settings.ai_credits.purchased.title')}</Card.Description>
|
||||
<Card.Title class="text-2xl">
|
||||
{#if purchased}
|
||||
{purchased.currentBalance.toLocaleString()}
|
||||
{:else}
|
||||
—
|
||||
{/if}
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content class="flex flex-col gap-3">
|
||||
<p class="ui:text-muted-foreground text-xs">
|
||||
{$t('settings.ai_credits.purchased.subtitle')}
|
||||
</p>
|
||||
<p class="ui:text-muted-foreground text-xs">
|
||||
{$t('settings.ai_credits.purchased.lifetime', {
|
||||
tokens: purchased ? purchased.totalPurchasedTokens.toLocaleString() : '0',
|
||||
dollars: dollarsSpent
|
||||
})}
|
||||
</p>
|
||||
<Button variant="outline" size="sm" class="w-fit" href="#buy-tokens">
|
||||
{$t('settings.ai_credits.purchased.buy_more')}
|
||||
</Button>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import BuyTokensCard from '$features/settings/components/ai-credits/buy-tokens-card.svelte';
|
||||
import TeamLeaderboard from '$features/settings/components/ai-credits/team-leaderboard.svelte';
|
||||
import UsageChart from '$features/settings/components/ai-credits/usage-chart.svelte';
|
||||
import UsageSummaryCards from '$features/settings/components/ai-credits/usage-summary-cards.svelte';
|
||||
import { aiCreditsApi } from '$features/settings/api/ai-credits.svelte';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
let lastFetchedOrgId = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
const orgId = $currentOrg.id;
|
||||
|
||||
if (!orgId || orgId === lastFetchedOrgId) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastFetchedOrgId = orgId;
|
||||
void aiCreditsApi.fetchAll();
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (page.url.searchParams.get('tokens') === 'success') {
|
||||
snackbar.success('snackbar.ai_credits.purchase_success');
|
||||
void aiCreditsApi.fetchAll();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="ui:mx-auto flex w-full max-w-5xl flex-col gap-8 px-2 pb-12">
|
||||
<UsageSummaryCards usage={aiCreditsApi.usage} purchased={aiCreditsApi.purchased} />
|
||||
<p class="ui:text-muted-foreground -mt-4 text-sm">{$t('settings.ai_credits.cost_note')}</p>
|
||||
<UsageChart usage={aiCreditsApi.usage} />
|
||||
<TeamLeaderboard leaderboard={aiCreditsApi.leaderboard} />
|
||||
<BuyTokensCard onBeforeRedirect={() => aiCreditsApi.fetchAll()} />
|
||||
</div>
|
||||
@@ -3,53 +3,14 @@
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { currentOrgPlan, isFreePlan, currentOrg } from '$lib/utils/store/org';
|
||||
import HourglassIcon from '@lucide/svelte/icons/hourglass';
|
||||
import SparklesIcon from '@lucide/svelte/icons/sparkles';
|
||||
import { openUpgradeModal } from '$lib/utils/functions/org';
|
||||
import ReceiptIcon from '@lucide/svelte/icons/receipt';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Item from '@cio/ui/base/item';
|
||||
import { ExternalLinkIcon, HoverableItem } from '@cio/ui/custom/moving-icons';
|
||||
|
||||
const PUBLIC_API_URL = import.meta.env.VITE_API_URL || '';
|
||||
|
||||
let isRedirecting = $state(false);
|
||||
|
||||
interface AiUsageData {
|
||||
used: number;
|
||||
allowance: number;
|
||||
creditBalance: number;
|
||||
remaining: number;
|
||||
}
|
||||
|
||||
let aiUsage: AiUsageData | null = $state(null);
|
||||
let aiUsageLoaded = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (!aiUsageLoaded && $currentOrg.id) {
|
||||
aiUsageLoaded = true;
|
||||
fetchAiUsage();
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchAiUsage() {
|
||||
try {
|
||||
const response = await fetch(`${PUBLIC_API_URL}/agent/usage`, {
|
||||
headers: { 'Cio-org-id': $currentOrg.id || '' },
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
aiUsage = result.data;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Silently fail
|
||||
}
|
||||
}
|
||||
|
||||
async function onOpenBilling() {
|
||||
console.log({
|
||||
plan: $currentOrgPlan
|
||||
@@ -119,39 +80,4 @@
|
||||
</Item.Actions>
|
||||
</Item.Root>
|
||||
{/if}
|
||||
|
||||
{#if aiUsage}
|
||||
<div class="mt-6">
|
||||
<p class="text-md mb-3 font-medium">{$t('settings.billing.ai_credits')}</p>
|
||||
|
||||
<Item.Root variant="outline">
|
||||
<Item.Media variant="icon">
|
||||
<SparklesIcon />
|
||||
</Item.Media>
|
||||
<Item.Content>
|
||||
<Item.Title>
|
||||
{aiUsage.used.toLocaleString()} / {aiUsage.allowance.toLocaleString()}
|
||||
{$t('settings.billing.ai_tokens_used')}
|
||||
</Item.Title>
|
||||
<Item.Description>
|
||||
{aiUsage.creditBalance.toLocaleString()}
|
||||
{$t('settings.billing.ai_bonus_credits')}
|
||||
</Item.Description>
|
||||
|
||||
{@const usagePercent =
|
||||
aiUsage.allowance > 0 ? Math.min(100, Math.round((aiUsage.used / aiUsage.allowance) * 100)) : 0}
|
||||
<div class="ui:bg-muted mt-2 h-1.5 w-full rounded-full">
|
||||
<div
|
||||
class="h-1.5 rounded-full transition-all {usagePercent > 90
|
||||
? 'bg-red-500'
|
||||
: usagePercent > 70
|
||||
? 'bg-amber-500'
|
||||
: 'ui:bg-primary'}"
|
||||
style="width: {usagePercent}%"
|
||||
></div>
|
||||
</div>
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ export { default as OrgPage } from './org.svelte';
|
||||
export { default as LandingpagePage } from './landingpage.svelte';
|
||||
export { default as LandingpageEditor } from './landingpage-editor.svelte';
|
||||
export { default as BillingPage } from './billing.svelte';
|
||||
export { default as AiCreditsPage } from './ai-credits.svelte';
|
||||
export { default as IntegrationsPage } from './integrations.svelte';
|
||||
export { default as AuthPage } from './auth.svelte';
|
||||
// Keep old exports for backward compatibility
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { classroomio, type InferResponseType } from '$lib/utils/services/api';
|
||||
|
||||
export type GetUsageRequest = typeof classroomio.agent.usage.$get;
|
||||
export type GetPurchasedRequest = (typeof classroomio.agent.usage)['purchased']['$get'];
|
||||
export type GetLeaderboardRequest = (typeof classroomio.agent.usage)['leaderboard']['$get'];
|
||||
|
||||
type GetUsageSuccess = Extract<InferResponseType<GetUsageRequest>, { success: true }>;
|
||||
type GetPurchasedSuccess = Extract<InferResponseType<GetPurchasedRequest>, { success: true }>;
|
||||
type GetLeaderboardSuccess = Extract<InferResponseType<GetLeaderboardRequest>, { success: true }>;
|
||||
|
||||
export type AiUsageData = GetUsageSuccess['data'];
|
||||
export type PurchasedSummaryData = GetPurchasedSuccess['data'];
|
||||
export type LeaderboardData = GetLeaderboardSuccess['data'];
|
||||
export type LeaderboardEntry = LeaderboardData['entries'][number];
|
||||
Reference in New Issue
Block a user