mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 20:28:23 +00:00
make format required & fix ui formating
This commit is contained in:
@@ -62,14 +62,18 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: "18"
|
||||||
cache: 'npm'
|
cache: "npm"
|
||||||
cache-dependency-path: ui/package-lock.json
|
cache-dependency-path: ui/package-lock.json
|
||||||
|
|
||||||
- name: Install UI dependencies
|
- name: Install UI dependencies
|
||||||
working-directory: ./ui
|
working-directory: ./ui
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run UI format check
|
||||||
|
working-directory: ./ui
|
||||||
|
run: npm run format-check
|
||||||
|
|
||||||
- name: Run UI linting
|
- name: Run UI linting
|
||||||
working-directory: ./ui
|
working-directory: ./ui
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ export default function BalancesPage() {
|
|||||||
<div className='container max-w-6xl px-4 py-8 md:px-6 lg:px-8'>
|
<div className='container max-w-6xl px-4 py-8 md:px-6 lg:px-8'>
|
||||||
<div className='mb-8 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between'>
|
<div className='mb-8 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between'>
|
||||||
<div>
|
<div>
|
||||||
<h1 className='text-3xl font-bold tracking-tight'>
|
<h1 className='text-3xl font-bold tracking-tight'>Balances</h1>
|
||||||
Balances
|
|
||||||
</h1>
|
|
||||||
<p className='text-muted-foreground mt-2'>
|
<p className='text-muted-foreground mt-2'>
|
||||||
Monitor and manage wallet balances
|
Monitor and manage wallet balances
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ export default function AdminLoginPage(): ReactElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-screen items-center justify-center bg-background px-4 py-12 text-foreground'>
|
<div className='bg-background text-foreground flex min-h-screen items-center justify-center px-4 py-12'>
|
||||||
<Card className='w-full max-w-md border border-border/60 bg-card/90 shadow-2xl shadow-black/30 backdrop-blur'>
|
<Card className='border-border/60 bg-card/90 w-full max-w-md border shadow-2xl shadow-black/30 backdrop-blur'>
|
||||||
<CardHeader className='space-y-1'>
|
<CardHeader className='space-y-1'>
|
||||||
<CardTitle className='text-center text-2xl font-bold'>
|
<CardTitle className='text-center text-2xl font-bold'>
|
||||||
Admin Login
|
Admin Login
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export function LogDetailsDialog({
|
|||||||
<div>
|
<div>
|
||||||
<h4 className='mb-2 text-sm font-medium'>Message</h4>
|
<h4 className='mb-2 text-sm font-medium'>Message</h4>
|
||||||
<div className='bg-muted max-h-48 overflow-auto rounded-md p-3'>
|
<div className='bg-muted max-h-48 overflow-auto rounded-md p-3'>
|
||||||
<pre className='font-mono text-sm whitespace-pre break-all'>
|
<pre className='font-mono text-sm break-all whitespace-pre'>
|
||||||
{log.message}
|
{log.message}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,7 +116,12 @@ export function LogDetailsDialog({
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() => copyToClipboard(String(log[field as keyof LogEntry] || ''), field)}
|
onClick={() =>
|
||||||
|
copyToClipboard(
|
||||||
|
String(log[field as keyof LogEntry] || ''),
|
||||||
|
field
|
||||||
|
)
|
||||||
|
}
|
||||||
className='h-6 flex-shrink-0 px-2'
|
className='h-6 flex-shrink-0 px-2'
|
||||||
>
|
>
|
||||||
{copiedField === field ? (
|
{copiedField === field ? (
|
||||||
@@ -175,7 +180,9 @@ export function LogDetailsDialog({
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() => copyToClipboard(JSON.stringify(log, null, 2), 'json')}
|
onClick={() =>
|
||||||
|
copyToClipboard(JSON.stringify(log, null, 2), 'json')
|
||||||
|
}
|
||||||
className='h-6 px-2'
|
className='h-6 px-2'
|
||||||
>
|
>
|
||||||
{copiedField === 'json' ? (
|
{copiedField === 'json' ? (
|
||||||
|
|||||||
+23
-10
@@ -205,32 +205,45 @@ export default function ModelsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{totalModels === 0 && (
|
{totalModels === 0 && (
|
||||||
<Alert className="mb-4">
|
<Alert className='mb-4'>
|
||||||
<AlertCircle className='h-4 w-4' />
|
<AlertCircle className='h-4 w-4' />
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<p className='font-medium'>
|
<p className='font-medium'>
|
||||||
No models found for this provider
|
No models found for this provider
|
||||||
</p>
|
</p>
|
||||||
<div className='text-sm space-y-1'>
|
<div className='space-y-1 text-sm'>
|
||||||
<p className='font-medium'>Common issues:</p>
|
<p className='font-medium'>
|
||||||
<ul className='list-disc list-inside space-y-1 ml-2'>
|
Common issues:
|
||||||
|
</p>
|
||||||
|
<ul className='ml-2 list-inside list-disc space-y-1'>
|
||||||
<li>
|
<li>
|
||||||
<strong>API credentials:</strong> Check if the API key is correct and has the right permissions
|
<strong>API credentials:</strong>{' '}
|
||||||
|
Check if the API key is correct
|
||||||
|
and has the right permissions
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Base URL:</strong> Verify the base URL is correct for your provider
|
<strong>Base URL:</strong> Verify
|
||||||
|
the base URL is correct for your
|
||||||
|
provider
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Network access:</strong> Ensure the server can reach the provider's API endpoint
|
<strong>Network access:</strong>{' '}
|
||||||
|
Ensure the server can reach the
|
||||||
|
provider's API endpoint
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Provider status:</strong> The upstream provider might be temporarily unavailable
|
<strong>Provider status:</strong>{' '}
|
||||||
|
The upstream provider might be
|
||||||
|
temporarily unavailable
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{groupData?.group_url && (
|
{groupData?.group_url && (
|
||||||
<p className='mt-2 text-xs text-muted-foreground'>
|
<p className='text-muted-foreground mt-2 text-xs'>
|
||||||
Current endpoint: <code className='bg-muted px-1 rounded'>{groupData.group_url}</code>
|
Current endpoint:{' '}
|
||||||
|
<code className='bg-muted rounded px-1'>
|
||||||
|
{groupData.group_url}
|
||||||
|
</code>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+49
-23
@@ -53,7 +53,7 @@ export default function DashboardPage() {
|
|||||||
window.removeEventListener('storage', syncAuthState);
|
window.removeEventListener('storage', syncAuthState);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { data: btcUsdPrice } = useQuery({
|
const { data: btcUsdPrice } = useQuery({
|
||||||
queryKey: ['btc-usd-price'],
|
queryKey: ['btc-usd-price'],
|
||||||
queryFn: fetchBtcUsdPrice,
|
queryFn: fetchBtcUsdPrice,
|
||||||
@@ -131,8 +131,13 @@ export default function DashboardPage() {
|
|||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
<div className='container max-w-7xl px-4 py-8 md:px-6 lg:px-8'>
|
<div className='container max-w-7xl px-4 py-8 md:px-6 lg:px-8'>
|
||||||
<div className='mb-8'>
|
<div className='mb-8'>
|
||||||
<h1 className='text-3xl font-bold tracking-tight mb-6'>Dashboard</h1>
|
<h1 className='mb-6 text-3xl font-bold tracking-tight'>
|
||||||
<DashboardBalanceSummary displayUnit={displayUnit} usdPerSat={usdPerSat} />
|
Dashboard
|
||||||
|
</h1>
|
||||||
|
<DashboardBalanceSummary
|
||||||
|
displayUnit={displayUnit}
|
||||||
|
usdPerSat={usdPerSat}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mb-6 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between'>
|
<div className='mb-6 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between'>
|
||||||
@@ -140,8 +145,9 @@ export default function DashboardPage() {
|
|||||||
<h2 className='text-xl font-semibold tracking-tight'>
|
<h2 className='text-xl font-semibold tracking-tight'>
|
||||||
Usage Analytics
|
Usage Analytics
|
||||||
</h2>
|
</h2>
|
||||||
<p className='text-muted-foreground text-sm mt-1'>
|
<p className='text-muted-foreground mt-1 text-sm'>
|
||||||
Monitor requests, errors, and revenue over the last {timeRange} hours
|
Monitor requests, errors, and revenue over the last {timeRange}{' '}
|
||||||
|
hours
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-4'>
|
<div className='flex items-center gap-4'>
|
||||||
@@ -176,27 +182,31 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
<div className='space-y-6'>
|
<div className='space-y-6'>
|
||||||
{summaryLoading ? (
|
{summaryLoading ? (
|
||||||
<div className='text-center py-8'>Loading summary...</div>
|
<div className='py-8 text-center'>Loading summary...</div>
|
||||||
) : summaryData ? (
|
) : summaryData ? (
|
||||||
<UsageSummaryCards summary={summaryData} />
|
<UsageSummaryCards summary={summaryData} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className='grid gap-6 lg:grid-cols-2'>
|
<div className='grid gap-6 lg:grid-cols-2'>
|
||||||
{metricsLoading ? (
|
{metricsLoading ? (
|
||||||
<div className='text-center py-8 col-span-2'>
|
<div className='col-span-2 py-8 text-center'>
|
||||||
Loading metrics...
|
Loading metrics...
|
||||||
</div>
|
</div>
|
||||||
) : metricsData && metricsData.metrics.length > 0 ? (
|
) : metricsData && metricsData.metrics.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className="col-span-full">
|
<div className='col-span-full'>
|
||||||
<UsageMetricsChart
|
<UsageMetricsChart
|
||||||
data={metricsData.metrics.map((m) => ({
|
data={
|
||||||
...m,
|
metricsData.metrics.map((m) => ({
|
||||||
revenue_sats: m.revenue_msats / 1000,
|
...m,
|
||||||
refunds_sats: m.refunds_msats / 1000,
|
revenue_sats: m.revenue_msats / 1000,
|
||||||
net_revenue_sats:
|
refunds_sats: m.refunds_msats / 1000,
|
||||||
(m.revenue_msats - m.refunds_msats) / 1000,
|
net_revenue_sats:
|
||||||
})) as Array<Record<string, unknown> & { timestamp: string }>}
|
(m.revenue_msats - m.refunds_msats) / 1000,
|
||||||
|
})) as Array<
|
||||||
|
Record<string, unknown> & { timestamp: string }
|
||||||
|
>
|
||||||
|
}
|
||||||
title='Revenue Over Time (sats)'
|
title='Revenue Over Time (sats)'
|
||||||
dataKeys={[
|
dataKeys={[
|
||||||
{
|
{
|
||||||
@@ -218,7 +228,11 @@ export default function DashboardPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<UsageMetricsChart
|
<UsageMetricsChart
|
||||||
data={metricsData.metrics as Array<Record<string, unknown> & { timestamp: string }>}
|
data={
|
||||||
|
metricsData.metrics as Array<
|
||||||
|
Record<string, unknown> & { timestamp: string }
|
||||||
|
>
|
||||||
|
}
|
||||||
title='Request Volume'
|
title='Request Volume'
|
||||||
dataKeys={[
|
dataKeys={[
|
||||||
{
|
{
|
||||||
@@ -239,7 +253,11 @@ export default function DashboardPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<UsageMetricsChart
|
<UsageMetricsChart
|
||||||
data={metricsData.metrics as Array<Record<string, unknown> & { timestamp: string }>}
|
data={
|
||||||
|
metricsData.metrics as Array<
|
||||||
|
Record<string, unknown> & { timestamp: string }
|
||||||
|
>
|
||||||
|
}
|
||||||
title='Error Tracking'
|
title='Error Tracking'
|
||||||
dataKeys={[
|
dataKeys={[
|
||||||
{
|
{
|
||||||
@@ -260,7 +278,11 @@ export default function DashboardPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<UsageMetricsChart
|
<UsageMetricsChart
|
||||||
data={metricsData.metrics as Array<Record<string, unknown> & { timestamp: string }>}
|
data={
|
||||||
|
metricsData.metrics as Array<
|
||||||
|
Record<string, unknown> & { timestamp: string }
|
||||||
|
>
|
||||||
|
}
|
||||||
title='Payment Activity'
|
title='Payment Activity'
|
||||||
dataKeys={[
|
dataKeys={[
|
||||||
{
|
{
|
||||||
@@ -270,7 +292,7 @@ export default function DashboardPage() {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<div className="space-y-6">
|
<div className='space-y-6'>
|
||||||
{summaryData && summaryData.unique_models.length > 0 && (
|
{summaryData && summaryData.unique_models.length > 0 && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -306,7 +328,9 @@ export default function DashboardPage() {
|
|||||||
key={type}
|
key={type}
|
||||||
className='flex items-center justify-between'
|
className='flex items-center justify-between'
|
||||||
>
|
>
|
||||||
<span className='text-sm font-medium'>{type}</span>
|
<span className='text-sm font-medium'>
|
||||||
|
{type}
|
||||||
|
</span>
|
||||||
<span className='text-muted-foreground text-sm'>
|
<span className='text-muted-foreground text-sm'>
|
||||||
{count}
|
{count}
|
||||||
</span>
|
</span>
|
||||||
@@ -335,16 +359,18 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{revenueByModelLoading ? (
|
{revenueByModelLoading ? (
|
||||||
<div className='text-center py-8'>Loading revenue by model...</div>
|
<div className='py-8 text-center'>
|
||||||
|
Loading revenue by model...
|
||||||
|
</div>
|
||||||
) : revenueByModelData && revenueByModelData.models.length > 0 ? (
|
) : revenueByModelData && revenueByModelData.models.length > 0 ? (
|
||||||
<RevenueByModelTable
|
<RevenueByModelTable
|
||||||
models={revenueByModelData.models}
|
models={revenueByModelData.models}
|
||||||
totalRevenue={revenueByModelData.total_revenue_sats}
|
totalRevenue={revenueByModelData.total_revenue_sats}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{errorLoading ? (
|
{errorLoading ? (
|
||||||
<div className='text-center py-8'>Loading errors...</div>
|
<div className='py-8 text-center'>Loading errors...</div>
|
||||||
) : errorData ? (
|
) : errorData ? (
|
||||||
<ErrorDetailsTable errors={errorData.errors} />
|
<ErrorDetailsTable errors={errorData.errors} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
+73
-34
@@ -76,7 +76,11 @@ function ProviderBalance({
|
|||||||
);
|
);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { data: balanceData, isLoading, error } = useQuery({
|
const {
|
||||||
|
data: balanceData,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
} = useQuery({
|
||||||
queryKey: ['provider-balance', providerId],
|
queryKey: ['provider-balance', providerId],
|
||||||
queryFn: () => AdminService.getProviderBalance(providerId),
|
queryFn: () => AdminService.getProviderBalance(providerId),
|
||||||
refetchInterval: 30000,
|
refetchInterval: 30000,
|
||||||
@@ -108,7 +112,10 @@ function ProviderBalance({
|
|||||||
mutationFn: async (amount: number) => {
|
mutationFn: async (amount: number) => {
|
||||||
console.log('Calling top-up API with:', { providerId, amount });
|
console.log('Calling top-up API with:', { providerId, amount });
|
||||||
try {
|
try {
|
||||||
const result = await AdminService.initiateProviderTopup(providerId, amount);
|
const result = await AdminService.initiateProviderTopup(
|
||||||
|
providerId,
|
||||||
|
amount
|
||||||
|
);
|
||||||
console.log('API returned:', result);
|
console.log('API returned:', result);
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -120,11 +127,8 @@ function ProviderBalance({
|
|||||||
console.log('Top-up response:', data);
|
console.log('Top-up response:', data);
|
||||||
console.log('Type of data:', typeof data);
|
console.log('Type of data:', typeof data);
|
||||||
console.log('Keys in data:', Object.keys(data || {}));
|
console.log('Keys in data:', Object.keys(data || {}));
|
||||||
|
|
||||||
if (
|
if (data?.topup_data?.payment_request && data?.topup_data?.invoice_id) {
|
||||||
data?.topup_data?.payment_request &&
|
|
||||||
data?.topup_data?.invoice_id
|
|
||||||
) {
|
|
||||||
setInvoiceData({
|
setInvoiceData({
|
||||||
payment_request: data.topup_data.payment_request as string,
|
payment_request: data.topup_data.payment_request as string,
|
||||||
invoice_id: data.topup_data.invoice_id as string,
|
invoice_id: data.topup_data.invoice_id as string,
|
||||||
@@ -172,19 +176,23 @@ function ProviderBalance({
|
|||||||
// The backend throws 500/400 if not implemented.
|
// The backend throws 500/400 if not implemented.
|
||||||
// A better approach is to check if we have a platform URL and maybe redirect there
|
// A better approach is to check if we have a platform URL and maybe redirect there
|
||||||
// if we know it's not supported.
|
// if we know it's not supported.
|
||||||
|
|
||||||
// BUT, we don't know for sure if it's supported without checking metadata or trying.
|
// BUT, we don't know for sure if it's supported without checking metadata or trying.
|
||||||
// Let's rely on the "can_topup" metadata if available, but currently we only have "can_show_balance".
|
// Let's rely on the "can_topup" metadata if available, but currently we only have "can_show_balance".
|
||||||
|
|
||||||
// Simple heuristic: If platformUrl exists and we suspect no direct topup, redirect?
|
// Simple heuristic: If platformUrl exists and we suspect no direct topup, redirect?
|
||||||
// Actually, let's try to open the dialog, but if it's OpenRouter/OpenAI, maybe we just redirect?
|
// Actually, let's try to open the dialog, but if it's OpenRouter/OpenAI, maybe we just redirect?
|
||||||
// The user specifically mentioned "like in openrouter".
|
// The user specifically mentioned "like in openrouter".
|
||||||
|
|
||||||
if (platformUrl && (platformUrl.includes('openrouter.ai') || platformUrl.includes('openai.com'))) {
|
if (
|
||||||
window.open(platformUrl, '_blank');
|
platformUrl &&
|
||||||
return;
|
(platformUrl.includes('openrouter.ai') ||
|
||||||
|
platformUrl.includes('openai.com'))
|
||||||
|
) {
|
||||||
|
window.open(platformUrl, '_blank');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsTopupDialogOpen(true);
|
setIsTopupDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -266,9 +274,7 @@ function ProviderBalance({
|
|||||||
<path d='M5 13l4 4L19 7'></path>
|
<path d='M5 13l4 4L19 7'></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p className='text-center font-semibold'>
|
<p className='text-center font-semibold'>Top-up successful!</p>
|
||||||
Top-up successful!
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : invoiceData ? (
|
) : invoiceData ? (
|
||||||
<div className='flex flex-col items-center gap-4 py-4'>
|
<div className='flex flex-col items-center gap-4 py-4'>
|
||||||
@@ -482,7 +488,9 @@ export default function ProvidersPage() {
|
|||||||
...formData,
|
...formData,
|
||||||
api_key: String(response.account_data.api_key),
|
api_key: String(response.account_data.api_key),
|
||||||
});
|
});
|
||||||
toast.success('Account created successfully! API key has been filled in.');
|
toast.success(
|
||||||
|
'Account created successfully! API key has been filled in.'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.success('Account created, but no API key returned.');
|
toast.success('Account created, but no API key returned.');
|
||||||
}
|
}
|
||||||
@@ -778,8 +786,7 @@ export default function ProvidersPage() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<p className='text-muted-foreground text-xs'>
|
<p className='text-muted-foreground text-xs'>
|
||||||
1.01 means +1% e.g. currency exchange, card
|
1.01 means +1% e.g. currency exchange, card fees, etc.
|
||||||
fees, etc.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -856,9 +863,11 @@ export default function ProvidersPage() {
|
|||||||
<div className='flex flex-wrap items-center gap-2'>
|
<div className='flex flex-wrap items-center gap-2'>
|
||||||
{canShowBalance(provider.provider_type) &&
|
{canShowBalance(provider.provider_type) &&
|
||||||
provider.api_key && (
|
provider.api_key && (
|
||||||
<ProviderBalance
|
<ProviderBalance
|
||||||
providerId={provider.id}
|
providerId={provider.id}
|
||||||
platformUrl={getPlatformUrl(provider.provider_type)}
|
platformUrl={getPlatformUrl(
|
||||||
|
provider.provider_type
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
@@ -925,9 +934,16 @@ export default function ProvidersPage() {
|
|||||||
{providerModels.db_models.length === 0 ? (
|
{providerModels.db_models.length === 0 ? (
|
||||||
<div className='flex flex-col items-center justify-center gap-2 py-4'>
|
<div className='flex flex-col items-center justify-center gap-2 py-4'>
|
||||||
<div className='text-muted-foreground text-sm'>
|
<div className='text-muted-foreground text-sm'>
|
||||||
No models configured. Add custom models to use this provider.
|
No models configured. Add custom models
|
||||||
|
to use this provider.
|
||||||
</div>
|
</div>
|
||||||
<Button variant='outline' size='sm' onClick={() => handleAddModel(provider.id)}>
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='sm'
|
||||||
|
onClick={() =>
|
||||||
|
handleAddModel(provider.id)
|
||||||
|
}
|
||||||
|
>
|
||||||
<Plus className='mr-2 h-4 w-4' />
|
<Plus className='mr-2 h-4 w-4' />
|
||||||
Add Custom Model
|
Add Custom Model
|
||||||
</Button>
|
</Button>
|
||||||
@@ -970,7 +986,12 @@ export default function ProvidersPage() {
|
|||||||
variant='ghost'
|
variant='ghost'
|
||||||
size='icon'
|
size='icon'
|
||||||
className='h-8 w-8'
|
className='h-8 w-8'
|
||||||
onClick={() => handleEditModel(provider.id, model)}
|
onClick={() =>
|
||||||
|
handleEditModel(
|
||||||
|
provider.id,
|
||||||
|
model
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Pencil className='h-4 w-4' />
|
<Pencil className='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1027,10 +1048,17 @@ export default function ProvidersPage() {
|
|||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
{providerModels.db_models.length > 0 && (
|
{providerModels.db_models.length > 0 && (
|
||||||
<div className='text-muted-foreground text-sm'>
|
<div className='text-muted-foreground text-sm'>
|
||||||
Custom models override or extend the provider's catalog.
|
Custom models override or extend the
|
||||||
|
provider's catalog.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button variant='outline' size='sm' onClick={() => handleAddModel(provider.id)}>
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='sm'
|
||||||
|
onClick={() =>
|
||||||
|
handleAddModel(provider.id)
|
||||||
|
}
|
||||||
|
>
|
||||||
<Plus className='mr-2 h-4 w-4' />
|
<Plus className='mr-2 h-4 w-4' />
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1079,7 +1107,12 @@ export default function ProvidersPage() {
|
|||||||
variant='ghost'
|
variant='ghost'
|
||||||
size='icon'
|
size='icon'
|
||||||
className='h-8 w-8'
|
className='h-8 w-8'
|
||||||
onClick={() => handleEditModel(provider.id, model)}
|
onClick={() =>
|
||||||
|
handleEditModel(
|
||||||
|
provider.id,
|
||||||
|
model
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Pencil className='h-4 w-4' />
|
<Pencil className='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1126,7 +1159,12 @@ export default function ProvidersPage() {
|
|||||||
variant='outline'
|
variant='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
className='h-7 text-xs'
|
className='h-7 text-xs'
|
||||||
onClick={() => handleOverrideModel(provider.id, model)}
|
onClick={() =>
|
||||||
|
handleOverrideModel(
|
||||||
|
provider.id,
|
||||||
|
model
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Plus className='mr-1 h-3 w-3' />
|
<Plus className='mr-1 h-3 w-3' />
|
||||||
Override
|
Override
|
||||||
@@ -1245,7 +1283,7 @@ export default function ProvidersPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='flex items-center space-x-2'>
|
<div className='flex items-center space-x-2'>
|
||||||
<Switch
|
<Switch
|
||||||
id='edit_enabled'
|
id='edit_enabled'
|
||||||
checked={formData.enabled}
|
checked={formData.enabled}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
@@ -1277,8 +1315,7 @@ export default function ProvidersPage() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<p className='text-muted-foreground text-xs'>
|
<p className='text-muted-foreground text-xs'>
|
||||||
1.01 means +1% e.g. currency exchange, card
|
1.01 means +1% e.g. currency exchange, card fees, etc.
|
||||||
fees, etc.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1303,7 +1340,9 @@ export default function ProvidersPage() {
|
|||||||
<AddProviderModelDialog
|
<AddProviderModelDialog
|
||||||
providerId={modelDialogState.providerId}
|
providerId={modelDialogState.providerId}
|
||||||
isOpen={modelDialogState.isOpen}
|
isOpen={modelDialogState.isOpen}
|
||||||
onClose={() => setModelDialogState((prev) => ({ ...prev, isOpen: false }))}
|
onClose={() =>
|
||||||
|
setModelDialogState((prev) => ({ ...prev, isOpen: false }))
|
||||||
|
}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['provider-models', modelDialogState.providerId],
|
queryKey: ['provider-models', modelDialogState.providerId],
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ export function AddProviderModelDialog({
|
|||||||
}: AddProviderModelDialogProps) {
|
}: AddProviderModelDialogProps) {
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [isPresetOpen, setIsPresetOpen] = useState(false);
|
const [isPresetOpen, setIsPresetOpen] = useState(false);
|
||||||
const [selectedPresetLabel, setSelectedPresetLabel] = useState('Select a preset');
|
const [selectedPresetLabel, setSelectedPresetLabel] =
|
||||||
|
useState('Select a preset');
|
||||||
|
|
||||||
const form = useForm<FormData>({
|
const form = useForm<FormData>({
|
||||||
resolver: zodResolver(FormSchema) as never,
|
resolver: zodResolver(FormSchema) as never,
|
||||||
@@ -149,7 +150,10 @@ export function AddProviderModelDialog({
|
|||||||
if (initialData) {
|
if (initialData) {
|
||||||
const architecture = initialData.architecture as Record<string, unknown>;
|
const architecture = initialData.architecture as Record<string, unknown>;
|
||||||
const pricing = initialData.pricing as Record<string, number>;
|
const pricing = initialData.pricing as Record<string, number>;
|
||||||
const topProvider = initialData.top_provider as Record<string, unknown> | null;
|
const topProvider = initialData.top_provider as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
> | null;
|
||||||
|
|
||||||
form.reset({
|
form.reset({
|
||||||
id: initialData.id,
|
id: initialData.id,
|
||||||
@@ -250,7 +254,9 @@ export function AddProviderModelDialog({
|
|||||||
form.setValue('context_length', model.context_length);
|
form.setValue('context_length', model.context_length);
|
||||||
form.setValue(
|
form.setValue(
|
||||||
'modality',
|
'modality',
|
||||||
typeof architecture?.modality === 'string' ? architecture.modality : 'text'
|
typeof architecture?.modality === 'string'
|
||||||
|
? architecture.modality
|
||||||
|
: 'text'
|
||||||
);
|
);
|
||||||
form.setValue(
|
form.setValue(
|
||||||
'input_modalities_raw',
|
'input_modalities_raw',
|
||||||
@@ -318,7 +324,10 @@ export function AddProviderModelDialog({
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
let perRequestLimits: Record<string, unknown> | null = null;
|
let perRequestLimits: Record<string, unknown> | null = null;
|
||||||
if (data.per_request_limits_raw && data.per_request_limits_raw.trim().length) {
|
if (
|
||||||
|
data.per_request_limits_raw &&
|
||||||
|
data.per_request_limits_raw.trim().length
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
perRequestLimits = JSON.parse(data.per_request_limits_raw);
|
perRequestLimits = JSON.parse(data.per_request_limits_raw);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -336,7 +345,9 @@ export function AddProviderModelDialog({
|
|||||||
context_length: data.context_length,
|
context_length: data.context_length,
|
||||||
architecture: {
|
architecture: {
|
||||||
modality: data.modality,
|
modality: data.modality,
|
||||||
input_modalities: listFromString(data.input_modalities_raw || data.modality),
|
input_modalities: listFromString(
|
||||||
|
data.input_modalities_raw || data.modality
|
||||||
|
),
|
||||||
output_modalities: listFromString(
|
output_modalities: listFromString(
|
||||||
data.output_modalities_raw || data.modality
|
data.output_modalities_raw || data.modality
|
||||||
),
|
),
|
||||||
@@ -366,10 +377,9 @@ export function AddProviderModelDialog({
|
|||||||
is_moderated: data.top_provider_is_moderated,
|
is_moderated: data.top_provider_is_moderated,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
upstream_provider_id:
|
upstream_provider_id: data.upstream_provider_id?.trim().length
|
||||||
data.upstream_provider_id?.trim().length
|
? data.upstream_provider_id.trim()
|
||||||
? data.upstream_provider_id.trim()
|
: providerId,
|
||||||
: providerId,
|
|
||||||
canonical_slug: data.canonical_slug?.trim() || null,
|
canonical_slug: data.canonical_slug?.trim() || null,
|
||||||
alias_ids: listFromString(data.alias_ids_raw || ''),
|
alias_ids: listFromString(data.alias_ids_raw || ''),
|
||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
@@ -416,7 +426,7 @@ export function AddProviderModelDialog({
|
|||||||
<DialogDescription>{description}</DialogDescription>
|
<DialogDescription>{description}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{!isEdit && !isOverride && (
|
{!isEdit && !isOverride && (
|
||||||
<div className='rounded-md border bg-muted/30 p-3'>
|
<div className='bg-muted/30 rounded-md border p-3'>
|
||||||
<div className='mb-2 text-sm font-medium'>Presets</div>
|
<div className='mb-2 text-sm font-medium'>Presets</div>
|
||||||
<div className='grid gap-2 sm:grid-cols-3 sm:items-start'>
|
<div className='grid gap-2 sm:grid-cols-3 sm:items-start'>
|
||||||
<Popover open={isPresetOpen} onOpenChange={setIsPresetOpen}>
|
<Popover open={isPresetOpen} onOpenChange={setIsPresetOpen}>
|
||||||
@@ -425,23 +435,25 @@ export function AddProviderModelDialog({
|
|||||||
variant='outline'
|
variant='outline'
|
||||||
role='combobox'
|
role='combobox'
|
||||||
aria-expanded={isPresetOpen}
|
aria-expanded={isPresetOpen}
|
||||||
className='w-full justify-between text-left text-sm overflow-hidden'
|
className='w-full justify-between overflow-hidden text-left text-sm'
|
||||||
>
|
>
|
||||||
<span className='truncate'>
|
<span className='truncate'>
|
||||||
{isLoadingPresets ? 'Loading presets...' : selectedPresetLabel}
|
{isLoadingPresets
|
||||||
|
? 'Loading presets...'
|
||||||
|
: selectedPresetLabel}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className='w-80 max-w-sm p-0'
|
className='w-80 max-w-sm p-0'
|
||||||
align='start'
|
align='start'
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
collisionPadding={12}
|
collisionPadding={12}
|
||||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<Command shouldFilter={true}>
|
<Command shouldFilter={true}>
|
||||||
<CommandInput placeholder='Search presets...' />
|
<CommandInput placeholder='Search presets...' />
|
||||||
<CommandList
|
<CommandList
|
||||||
className='max-h-64 overflow-y-auto overscroll-contain'
|
className='max-h-64 overflow-y-auto overscroll-contain'
|
||||||
onWheel={(e) => e.stopPropagation()}
|
onWheel={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
@@ -475,8 +487,9 @@ export function AddProviderModelDialog({
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground text-xs mt-1'>
|
<div className='text-muted-foreground mt-1 text-xs'>
|
||||||
Prefill fields from a preset model definition, then adjust as needed.
|
Prefill fields from a preset model definition, then adjust as
|
||||||
|
needed.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -496,7 +509,9 @@ export function AddProviderModelDialog({
|
|||||||
disabled={isOverride || isEdit}
|
disabled={isOverride || isEdit}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Unique identifier for the model</FormDescription>
|
<FormDescription>
|
||||||
|
Unique identifier for the model
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -524,7 +539,11 @@ export function AddProviderModelDialog({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>Description</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea placeholder='Brief description...' {...field} rows={2} />
|
<Textarea
|
||||||
|
placeholder='Brief description...'
|
||||||
|
{...field}
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -575,10 +594,7 @@ export function AddProviderModelDialog({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Input Modalities</FormLabel>
|
<FormLabel>Input Modalities</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder='text, image, file' {...field} />
|
||||||
placeholder='text, image, file'
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Comma-separated list</FormDescription>
|
<FormDescription>Comma-separated list</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -593,10 +609,7 @@ export function AddProviderModelDialog({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Output Modalities</FormLabel>
|
<FormLabel>Output Modalities</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder='text, image' {...field} />
|
||||||
placeholder='text, image'
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Comma-separated list</FormDescription>
|
<FormDescription>Comma-separated list</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -643,7 +656,10 @@ export function AddProviderModelDialog({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Canonical Slug</FormLabel>
|
<FormLabel>Canonical Slug</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder='google/gemini-3-pro-preview-20251117' {...field} />
|
<Input
|
||||||
|
placeholder='google/gemini-3-pro-preview-20251117'
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -697,15 +713,19 @@ export function AddProviderModelDialog({
|
|||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>JSON object; leave empty for none</FormDescription>
|
<FormDescription>
|
||||||
|
JSON object; leave empty for none
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='space-y-4 rounded-md border bg-muted/20 p-4'>
|
<div className='bg-muted/20 space-y-4 rounded-md border p-4'>
|
||||||
<h4 className='text-sm font-medium'>Pricing (USD per 1M tokens)</h4>
|
<h4 className='text-sm font-medium'>
|
||||||
|
Pricing (USD per 1M tokens)
|
||||||
|
</h4>
|
||||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>
|
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -827,7 +847,7 @@ export function AddProviderModelDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='space-y-4 rounded-md border bg-muted/20 p-4'>
|
<div className='bg-muted/20 space-y-4 rounded-md border p-4'>
|
||||||
<h4 className='text-sm font-medium'>Top Provider (optional)</h4>
|
<h4 className='text-sm font-medium'>Top Provider (optional)</h4>
|
||||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-3'>
|
<div className='grid grid-cols-1 gap-4 sm:grid-cols-3'>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -870,11 +890,18 @@ export function AddProviderModelDialog({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
|
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
|
||||||
<div className='space-y-0.5'>
|
<div className='space-y-0.5'>
|
||||||
<FormLabel className='text-base'>Is Moderated</FormLabel>
|
<FormLabel className='text-base'>
|
||||||
<FormDescription>Whether provider enforces moderation</FormDescription>
|
Is Moderated
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Whether provider enforces moderation
|
||||||
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -892,7 +919,10 @@ export function AddProviderModelDialog({
|
|||||||
<FormDescription>Enable this model for use</FormDescription>
|
<FormDescription>Enable this model for use</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -908,8 +938,14 @@ export function AddProviderModelDialog({
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type='submit' disabled={isSubmitting}>
|
<Button type='submit' disabled={isSubmitting}>
|
||||||
{isSubmitting && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
{isSubmitting && (
|
||||||
{isEdit ? 'Save Changes' : isOverride ? 'Create Override' : 'Create Model'}
|
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||||
|
)}
|
||||||
|
{isEdit
|
||||||
|
? 'Save Changes'
|
||||||
|
: isOverride
|
||||||
|
? 'Create Override'
|
||||||
|
: 'Create Model'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
@@ -918,4 +954,3 @@ export function AddProviderModelDialog({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { type Model, type GroupSettings } from '@/lib/api/schemas/models';
|
||||||
import {
|
import {
|
||||||
type Model,
|
AdminService,
|
||||||
type GroupSettings,
|
type AdminModelGroup,
|
||||||
} from '@/lib/api/schemas/models';
|
type AdminModel,
|
||||||
import { AdminService, type AdminModelGroup, type AdminModel } from '@/lib/api/services/admin';
|
} from '@/lib/api/services/admin';
|
||||||
type ModelGroup = AdminModelGroup;
|
type ModelGroup = AdminModelGroup;
|
||||||
import { AddProviderModelDialog } from '@/components/AddProviderModelDialog';
|
import { AddProviderModelDialog } from '@/components/AddProviderModelDialog';
|
||||||
import { EditGroupForm } from '@/components/EditGroupForm';
|
import { EditGroupForm } from '@/components/EditGroupForm';
|
||||||
@@ -514,7 +515,7 @@ export function ModelSelector({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const providerId = parseInt(model.provider_id);
|
const providerId = parseInt(model.provider_id);
|
||||||
|
|
||||||
// Construct AdminModel from Model
|
// Construct AdminModel from Model
|
||||||
const adminModel: AdminModel = {
|
const adminModel: AdminModel = {
|
||||||
id: model.id,
|
id: model.id,
|
||||||
@@ -557,7 +558,7 @@ export function ModelSelector({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const providerId = parseInt(model.provider_id);
|
const providerId = parseInt(model.provider_id);
|
||||||
|
|
||||||
// Construct AdminModel from Model
|
// Construct AdminModel from Model
|
||||||
const adminModel: AdminModel = {
|
const adminModel: AdminModel = {
|
||||||
id: model.id,
|
id: model.id,
|
||||||
@@ -901,10 +902,10 @@ export function ModelSelector({
|
|||||||
)}
|
)}
|
||||||
{/* Model Management Actions */}
|
{/* Model Management Actions */}
|
||||||
{groupData && (
|
{groupData && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleAddModelClick(parseInt(groupData.id))}
|
onClick={() => handleAddModelClick(parseInt(groupData.id))}
|
||||||
className='gap-2'
|
className='gap-2'
|
||||||
variant="outline"
|
variant='outline'
|
||||||
>
|
>
|
||||||
<CheckSquare className='h-4 w-4' />
|
<CheckSquare className='h-4 w-4' />
|
||||||
Add Custom Model
|
Add Custom Model
|
||||||
@@ -1306,7 +1307,9 @@ export function ModelSelector({
|
|||||||
<AddProviderModelDialog
|
<AddProviderModelDialog
|
||||||
providerId={modelDialogState.providerId}
|
providerId={modelDialogState.providerId}
|
||||||
isOpen={modelDialogState.isOpen}
|
isOpen={modelDialogState.isOpen}
|
||||||
onClose={() => setModelDialogState((prev) => ({ ...prev, isOpen: false }))}
|
onClose={() =>
|
||||||
|
setModelDialogState((prev) => ({ ...prev, isOpen: false }))
|
||||||
|
}
|
||||||
onSuccess={handleModelUpdate}
|
onSuccess={handleModelUpdate}
|
||||||
initialData={modelDialogState.initialData}
|
initialData={modelDialogState.initialData}
|
||||||
mode={modelDialogState.mode}
|
mode={modelDialogState.mode}
|
||||||
|
|||||||
@@ -48,20 +48,26 @@ export function CurrencyToggle() {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="sm" className="h-9 w-9 px-0 gap-2 w-auto px-3 font-normal">
|
<Button
|
||||||
<Coins className="h-4 w-4" />
|
variant='ghost'
|
||||||
<span className="hidden sm:inline-block">{getLabel(displayUnit)}</span>
|
size='sm'
|
||||||
<span className="sm:hidden uppercase">{displayUnit}</span>
|
className='h-9 w-9 w-auto gap-2 px-0 px-3 font-normal'
|
||||||
|
>
|
||||||
|
<Coins className='h-4 w-4' />
|
||||||
|
<span className='hidden sm:inline-block'>
|
||||||
|
{getLabel(displayUnit)}
|
||||||
|
</span>
|
||||||
|
<span className='uppercase sm:hidden'>{displayUnit}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align='end'>
|
||||||
<DropdownMenuItem onClick={() => setDisplayUnit('msat')}>
|
<DropdownMenuItem onClick={() => setDisplayUnit('msat')}>
|
||||||
Millisatoshis (mSAT)
|
Millisatoshis (mSAT)
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setDisplayUnit('sat')}>
|
<DropdownMenuItem onClick={() => setDisplayUnit('sat')}>
|
||||||
Satoshis (sat)
|
Satoshis (sat)
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => setDisplayUnit('usd')}
|
onClick={() => setDisplayUnit('usd')}
|
||||||
disabled={!usdPerSat}
|
disabled={!usdPerSat}
|
||||||
>
|
>
|
||||||
@@ -71,4 +77,3 @@ export function CurrencyToggle() {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,4 +96,3 @@ export function DashboardBalanceSummary({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function ErrorDetailsTable({ errors }: ErrorDetailsTableProps) {
|
|||||||
<CardTitle>Recent Errors</CardTitle>
|
<CardTitle>Recent Errors</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className='text-muted-foreground text-center py-8'>
|
<p className='text-muted-foreground py-8 text-center'>
|
||||||
No errors found in the selected time period
|
No errors found in the selected time period
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -116,7 +116,9 @@ export function CheatSheet(): JSX.Element {
|
|||||||
const [topupToken, setTopupToken] = useState('');
|
const [topupToken, setTopupToken] = useState('');
|
||||||
const [apiKeyInput, setApiKeyInput] = useState('');
|
const [apiKeyInput, setApiKeyInput] = useState('');
|
||||||
const [walletInfo, setWalletInfo] = useState<WalletSnapshot | null>(null);
|
const [walletInfo, setWalletInfo] = useState<WalletSnapshot | null>(null);
|
||||||
const [refundReceipt, setRefundReceipt] = useState<RefundReceipt | null>(null);
|
const [refundReceipt, setRefundReceipt] = useState<RefundReceipt | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const [isCreatingKey, setIsCreatingKey] = useState(false);
|
const [isCreatingKey, setIsCreatingKey] = useState(false);
|
||||||
const [isTopupLoading, setIsTopupLoading] = useState(false);
|
const [isTopupLoading, setIsTopupLoading] = useState(false);
|
||||||
const [isRefunding, setIsRefunding] = useState(false);
|
const [isRefunding, setIsRefunding] = useState(false);
|
||||||
@@ -332,22 +334,21 @@ export function CheatSheet(): JSX.Element {
|
|||||||
const showCreateDetails =
|
const showCreateDetails =
|
||||||
hasInteractedCreate || initialToken.trim().length > 0;
|
hasInteractedCreate || initialToken.trim().length > 0;
|
||||||
const showManageDetails = hasInteractedManage || Boolean(walletInfo);
|
const showManageDetails = hasInteractedManage || Boolean(walletInfo);
|
||||||
const showTopupDetails =
|
const showTopupDetails = hasInteractedTopup || topupToken.trim().length > 0;
|
||||||
hasInteractedTopup || topupToken.trim().length > 0;
|
|
||||||
const refundToken = refundReceipt?.token ?? null;
|
const refundToken = refundReceipt?.token ?? null;
|
||||||
const canTopup = Boolean(activeApiKey);
|
const canTopup = Boolean(activeApiKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='min-h-screen bg-gradient-to-b from-background via-background to-muted'>
|
<div className='from-background via-background to-muted min-h-screen bg-gradient-to-b'>
|
||||||
<main className='mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-10 sm:px-6 lg:px-8'>
|
<main className='mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-10 sm:px-6 lg:px-8'>
|
||||||
<section className='relative space-y-3 text-center md:text-left'>
|
<section className='relative space-y-3 text-center md:text-left'>
|
||||||
<div className='absolute right-0 top-0 hidden md:block'>
|
<div className='absolute top-0 right-0 hidden md:block'>
|
||||||
<Button asChild size='sm' className='px-3 text-xs'>
|
<Button asChild size='sm' className='px-3 text-xs'>
|
||||||
<a href='/login'>Admin</a>
|
<a href='/login'>Admin</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className='inline-flex items-center gap-2 rounded-full border px-3 py-1 text-[0.65rem] uppercase tracking-wider text-muted-foreground'>
|
<div className='text-muted-foreground inline-flex items-center gap-2 rounded-full border px-3 py-1 text-[0.65rem] tracking-wider uppercase'>
|
||||||
<Bolt className='h-4 w-4 text-primary' />
|
<Bolt className='text-primary h-4 w-4' />
|
||||||
Routstr cheat sheet
|
Routstr cheat sheet
|
||||||
</div>
|
</div>
|
||||||
<h1 className='text-3xl font-semibold tracking-tight sm:text-4xl'>
|
<h1 className='text-3xl font-semibold tracking-tight sm:text-4xl'>
|
||||||
@@ -365,10 +366,10 @@ export function CheatSheet(): JSX.Element {
|
|||||||
<CardHeader className='flex flex-row items-start justify-between gap-4'>
|
<CardHeader className='flex flex-row items-start justify-between gap-4'>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className='flex items-center gap-2 text-lg'>
|
<CardTitle className='flex items-center gap-2 text-lg'>
|
||||||
<ShieldCheck className='h-4 w-4 text-primary' />
|
<ShieldCheck className='text-primary h-4 w-4' />
|
||||||
Node identity
|
Node identity
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<p className='text-xs uppercase tracking-wide text-muted-foreground'>
|
<p className='text-muted-foreground text-xs tracking-wide uppercase'>
|
||||||
/v1/info snapshot
|
/v1/info snapshot
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -404,7 +405,7 @@ export function CheatSheet(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
<dl className='grid gap-4 sm:grid-cols-2'>
|
<dl className='grid gap-4 sm:grid-cols-2'>
|
||||||
<div>
|
<div>
|
||||||
<dt className='text-muted-foreground text-xs uppercase tracking-wide'>
|
<dt className='text-muted-foreground text-xs tracking-wide uppercase'>
|
||||||
Version
|
Version
|
||||||
</dt>
|
</dt>
|
||||||
<dd className='text-base font-medium'>
|
<dd className='text-base font-medium'>
|
||||||
@@ -412,7 +413,7 @@ export function CheatSheet(): JSX.Element {
|
|||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className='text-muted-foreground text-xs uppercase tracking-wide'>
|
<dt className='text-muted-foreground text-xs tracking-wide uppercase'>
|
||||||
HTTP
|
HTTP
|
||||||
</dt>
|
</dt>
|
||||||
<dd className='text-base font-medium break-all'>
|
<dd className='text-base font-medium break-all'>
|
||||||
@@ -421,7 +422,7 @@ export function CheatSheet(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
{nodeInfo.onion_url && (
|
{nodeInfo.onion_url && (
|
||||||
<div className='sm:col-span-2'>
|
<div className='sm:col-span-2'>
|
||||||
<dt className='text-muted-foreground text-xs uppercase tracking-wide'>
|
<dt className='text-muted-foreground text-xs tracking-wide uppercase'>
|
||||||
Onion
|
Onion
|
||||||
</dt>
|
</dt>
|
||||||
<dd className='text-base font-medium break-all'>
|
<dd className='text-base font-medium break-all'>
|
||||||
@@ -431,10 +432,10 @@ export function CheatSheet(): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
{nodeInfo.npub && (
|
{nodeInfo.npub && (
|
||||||
<div className='sm:col-span-2'>
|
<div className='sm:col-span-2'>
|
||||||
<dt className='text-muted-foreground text-xs uppercase tracking-wide'>
|
<dt className='text-muted-foreground text-xs tracking-wide uppercase'>
|
||||||
npub
|
npub
|
||||||
</dt>
|
</dt>
|
||||||
<dd className='flex items-center gap-2 break-all text-sm font-mono'>
|
<dd className='flex items-center gap-2 font-mono text-sm break-all'>
|
||||||
{nodeInfo.npub}
|
{nodeInfo.npub}
|
||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -449,7 +450,7 @@ export function CheatSheet(): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
</dl>
|
</dl>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<p className='text-xs uppercase tracking-wide text-muted-foreground'>
|
<p className='text-muted-foreground text-xs tracking-wide uppercase'>
|
||||||
Cashu mints
|
Cashu mints
|
||||||
</p>
|
</p>
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
@@ -478,10 +479,10 @@ export function CheatSheet(): JSX.Element {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className='flex flex-row items-center justify-between'>
|
<CardHeader className='flex flex-row items-center justify-between'>
|
||||||
<CardTitle className='flex items-center gap-2 text-lg'>
|
<CardTitle className='flex items-center gap-2 text-lg'>
|
||||||
<Terminal className='h-4 w-4 text-primary' />
|
<Terminal className='text-primary h-4 w-4' />
|
||||||
Quick docs
|
Quick docs
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<span className='text-xs uppercase tracking-wide text-muted-foreground'>
|
<span className='text-muted-foreground text-xs tracking-wide uppercase'>
|
||||||
curl-ready
|
curl-ready
|
||||||
</span>
|
</span>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -502,8 +503,10 @@ export function CheatSheet(): JSX.Element {
|
|||||||
<Copy className='h-4 w-4' />
|
<Copy className='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className='rounded-lg bg-muted p-4 font-mono text-sm leading-6'>
|
<div className='bg-muted rounded-lg p-4 font-mono text-sm leading-6'>
|
||||||
<pre className='whitespace-pre-wrap break-all'>{curlSnippet}</pre>
|
<pre className='break-all whitespace-pre-wrap'>
|
||||||
|
{curlSnippet}
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Button
|
<Button
|
||||||
@@ -532,16 +535,16 @@ export function CheatSheet(): JSX.Element {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className='space-y-1'>
|
<CardHeader className='space-y-1'>
|
||||||
<CardTitle className='flex items-center gap-2 text-xl'>
|
<CardTitle className='flex items-center gap-2 text-xl'>
|
||||||
<KeyRound className='h-5 w-5 text-primary' />
|
<KeyRound className='text-primary h-5 w-5' />
|
||||||
API key workflow
|
API key workflow
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<p className='text-xs uppercase tracking-wide text-muted-foreground'>
|
<p className='text-muted-foreground text-xs tracking-wide uppercase'>
|
||||||
Sections expand as soon as you interact
|
Sections expand as soon as you interact
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-6'>
|
<CardContent className='space-y-6'>
|
||||||
<section className='space-y-2'>
|
<section className='space-y-2'>
|
||||||
<header className='flex items-center justify-between text-[0.7rem] uppercase tracking-wider text-muted-foreground'>
|
<header className='text-muted-foreground flex items-center justify-between text-[0.7rem] tracking-wider uppercase'>
|
||||||
<span>1 · Create key</span>
|
<span>1 · Create key</span>
|
||||||
{showCreateDetails && (
|
{showCreateDetails && (
|
||||||
<span className='text-primary'>Cashu token detected</span>
|
<span className='text-primary'>Cashu token detected</span>
|
||||||
@@ -564,7 +567,7 @@ export function CheatSheet(): JSX.Element {
|
|||||||
>
|
>
|
||||||
{isCreatingKey ? 'Creating…' : 'Create API key'}
|
{isCreatingKey ? 'Creating…' : 'Create API key'}
|
||||||
</Button>
|
</Button>
|
||||||
<span className='text-xs text-muted-foreground'>
|
<span className='text-muted-foreground text-xs'>
|
||||||
Redeems instantly and returns <code>sk-</code> key.
|
Redeems instantly and returns <code>sk-</code> key.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -574,7 +577,7 @@ export function CheatSheet(): JSX.Element {
|
|||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<section className='space-y-2'>
|
<section className='space-y-2'>
|
||||||
<header className='flex items-center justify-between text-[0.7rem] uppercase tracking-wider text-muted-foreground'>
|
<header className='text-muted-foreground flex items-center justify-between text-[0.7rem] tracking-wider uppercase'>
|
||||||
<span>2 · Manage key</span>
|
<span>2 · Manage key</span>
|
||||||
{walletInfo && (
|
{walletInfo && (
|
||||||
<span className='text-primary'>
|
<span className='text-primary'>
|
||||||
@@ -618,7 +621,7 @@ export function CheatSheet(): JSX.Element {
|
|||||||
{showManageDetails && (
|
{showManageDetails && (
|
||||||
<div className='grid gap-3 sm:grid-cols-2'>
|
<div className='grid gap-3 sm:grid-cols-2'>
|
||||||
<div className='rounded-lg border p-3'>
|
<div className='rounded-lg border p-3'>
|
||||||
<p className='text-[0.65rem] uppercase tracking-wide text-muted-foreground'>
|
<p className='text-muted-foreground text-[0.65rem] tracking-wide uppercase'>
|
||||||
Spendable
|
Spendable
|
||||||
</p>
|
</p>
|
||||||
<p className='text-xl font-semibold'>
|
<p className='text-xl font-semibold'>
|
||||||
@@ -633,7 +636,7 @@ export function CheatSheet(): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='rounded-lg border p-3'>
|
<div className='rounded-lg border p-3'>
|
||||||
<p className='text-[0.65rem] uppercase tracking-wide text-muted-foreground'>
|
<p className='text-muted-foreground text-[0.65rem] tracking-wide uppercase'>
|
||||||
Reserved
|
Reserved
|
||||||
</p>
|
</p>
|
||||||
<p className='text-xl font-semibold'>
|
<p className='text-xl font-semibold'>
|
||||||
@@ -654,10 +657,12 @@ export function CheatSheet(): JSX.Element {
|
|||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<section className='space-y-2'>
|
<section className='space-y-2'>
|
||||||
<header className='flex items-center justify-between text-[0.7rem] uppercase tracking-wider text-muted-foreground'>
|
<header className='text-muted-foreground flex items-center justify-between text-[0.7rem] tracking-wider uppercase'>
|
||||||
<span>3 · Top up</span>
|
<span>3 · Top up</span>
|
||||||
{showTopupDetails && (
|
{showTopupDetails && (
|
||||||
<span className={canTopup ? 'text-primary' : 'text-destructive'}>
|
<span
|
||||||
|
className={canTopup ? 'text-primary' : 'text-destructive'}
|
||||||
|
>
|
||||||
{canTopup ? 'Ready to redeem' : 'Paste API key first'}
|
{canTopup ? 'Ready to redeem' : 'Paste API key first'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -681,14 +686,14 @@ export function CheatSheet(): JSX.Element {
|
|||||||
>
|
>
|
||||||
{isTopupLoading ? 'Topping up…' : 'Top up this key'}
|
{isTopupLoading ? 'Topping up…' : 'Top up this key'}
|
||||||
</Button>
|
</Button>
|
||||||
<span className='text-xs text-muted-foreground'>
|
<span className='text-muted-foreground text-xs'>
|
||||||
{canTopup
|
{canTopup ? (
|
||||||
? (
|
<>
|
||||||
<>
|
Adds balance to the same <code>sk-</code> token.
|
||||||
Adds balance to the same <code>sk-</code> token.
|
</>
|
||||||
</>
|
) : (
|
||||||
)
|
'Enter your sk- key above to unlock top ups.'
|
||||||
: 'Enter your sk- key above to unlock top ups.'}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -697,7 +702,7 @@ export function CheatSheet(): JSX.Element {
|
|||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<section className='space-y-2'>
|
<section className='space-y-2'>
|
||||||
<header className='flex items-center justify-between text-[0.7rem] uppercase tracking-wider text-muted-foreground'>
|
<header className='text-muted-foreground flex items-center justify-between text-[0.7rem] tracking-wider uppercase'>
|
||||||
<span>4 · Refund</span>
|
<span>4 · Refund</span>
|
||||||
{refundReceipt && <span className='text-primary'>Done</span>}
|
{refundReceipt && <span className='text-primary'>Done</span>}
|
||||||
</header>
|
</header>
|
||||||
@@ -710,13 +715,13 @@ export function CheatSheet(): JSX.Element {
|
|||||||
>
|
>
|
||||||
{isRefunding ? 'Processing…' : 'Refund remaining balance'}
|
{isRefunding ? 'Processing…' : 'Refund remaining balance'}
|
||||||
</Button>
|
</Button>
|
||||||
<span className='text-xs text-muted-foreground'>
|
<span className='text-muted-foreground text-xs'>
|
||||||
Burns the key and returns a fresh Cashu token.
|
Burns the key and returns a fresh Cashu token.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{refundToken && (
|
{refundToken && (
|
||||||
<div className='space-y-2 rounded-lg border bg-muted/30 p-4'>
|
<div className='bg-muted/30 space-y-2 rounded-lg border p-4'>
|
||||||
<div className='flex items-center justify-between text-[0.7rem] uppercase tracking-wider text-muted-foreground'>
|
<div className='text-muted-foreground flex items-center justify-between text-[0.7rem] tracking-wider uppercase'>
|
||||||
<span>Cashu refund token</span>
|
<span>Cashu refund token</span>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -743,4 +748,3 @@ export function CheatSheet(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,11 @@ export function RevenueByModelTable({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Revenue by Model</CardTitle>
|
<CardTitle>Revenue by Model</CardTitle>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className='text-muted-foreground text-sm'>
|
||||||
Total Revenue: <span className="font-mono font-medium text-foreground">{formatAmount(totalRevenue)}</span>
|
Total Revenue:{' '}
|
||||||
|
<span className='text-foreground font-mono font-medium'>
|
||||||
|
{formatAmount(totalRevenue)}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -50,48 +53,62 @@ export function RevenueByModelTable({
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Model</TableHead>
|
<TableHead>Model</TableHead>
|
||||||
<TableHead className="text-right">Requests</TableHead>
|
<TableHead className='text-right'>Requests</TableHead>
|
||||||
<TableHead className="text-right">Successful</TableHead>
|
<TableHead className='text-right'>Successful</TableHead>
|
||||||
<TableHead className="text-right">Failed</TableHead>
|
<TableHead className='text-right'>Failed</TableHead>
|
||||||
<TableHead className="text-right">Revenue</TableHead>
|
<TableHead className='text-right'>Revenue</TableHead>
|
||||||
<TableHead className="w-[100px]">Share</TableHead>
|
<TableHead className='w-[100px]'>Share</TableHead>
|
||||||
<TableHead className="text-right">Refunds</TableHead>
|
<TableHead className='text-right'>Refunds</TableHead>
|
||||||
<TableHead className="text-right">Net Revenue</TableHead>
|
<TableHead className='text-right'>Net Revenue</TableHead>
|
||||||
<TableHead className="text-right">Avg/Request</TableHead>
|
<TableHead className='text-right'>Avg/Request</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{models.length === 0 ? (
|
{models.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={9} className="text-center text-muted-foreground">
|
<TableCell
|
||||||
|
colSpan={9}
|
||||||
|
className='text-muted-foreground text-center'
|
||||||
|
>
|
||||||
No model data available
|
No model data available
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
models.map((model) => {
|
models.map((model) => {
|
||||||
const share = totalRevenue > 0 ? (model.revenue_sats / totalRevenue) * 100 : 0;
|
const share =
|
||||||
|
totalRevenue > 0
|
||||||
|
? (model.revenue_sats / totalRevenue) * 100
|
||||||
|
: 0;
|
||||||
return (
|
return (
|
||||||
<TableRow key={model.model}>
|
<TableRow key={model.model}>
|
||||||
<TableCell className="font-medium">{model.model}</TableCell>
|
<TableCell className='font-medium'>{model.model}</TableCell>
|
||||||
<TableCell className="text-right font-mono">{model.requests}</TableCell>
|
<TableCell className='text-right font-mono'>
|
||||||
<TableCell className="text-right text-green-600 font-mono">{model.successful}</TableCell>
|
{model.requests}
|
||||||
<TableCell className="text-right text-red-600 font-mono">{model.failed}</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right font-mono">
|
<TableCell className='text-right font-mono text-green-600'>
|
||||||
|
{model.successful}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right font-mono text-red-600'>
|
||||||
|
{model.failed}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right font-mono'>
|
||||||
{formatAmount(model.revenue_sats)}
|
{formatAmount(model.revenue_sats)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Progress value={share} className="h-2" />
|
<Progress value={share} className='h-2' />
|
||||||
<span className="text-xs text-muted-foreground w-8 text-right">{share.toFixed(0)}%</span>
|
<span className='text-muted-foreground w-8 text-right text-xs'>
|
||||||
|
{share.toFixed(0)}%
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right text-red-500 font-mono">
|
<TableCell className='text-right font-mono text-red-500'>
|
||||||
{formatAmount(model.refunds_sats)}
|
{formatAmount(model.refunds_sats)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right font-semibold font-mono">
|
<TableCell className='text-right font-mono font-semibold'>
|
||||||
{formatAmount(model.net_revenue_sats)}
|
{formatAmount(model.net_revenue_sats)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right text-muted-foreground font-mono">
|
<TableCell className='text-muted-foreground text-right font-mono'>
|
||||||
{formatAmount(model.avg_revenue_per_request)}
|
{formatAmount(model.avg_revenue_per_request)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -90,7 +90,11 @@ export function UsageMetricsChart({
|
|||||||
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
||||||
}}
|
}}
|
||||||
itemStyle={{ fontSize: '12px' }}
|
itemStyle={{ fontSize: '12px' }}
|
||||||
labelStyle={{ fontSize: '12px', color: 'hsl(var(--muted-foreground))', marginBottom: '8px' }}
|
labelStyle={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'hsl(var(--muted-foreground))',
|
||||||
|
marginBottom: '8px',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Legend wrapperStyle={{ fontSize: '12px', paddingTop: '16px' }} />
|
<Legend wrapperStyle={{ fontSize: '12px', paddingTop: '16px' }} />
|
||||||
{dataKeys.map((dataKey) => (
|
{dataKeys.map((dataKey) => (
|
||||||
|
|||||||
@@ -114,15 +114,19 @@ export function UsageSummaryCards({ summary }: UsageSummaryCardsProps) {
|
|||||||
return (
|
return (
|
||||||
<div className='grid gap-6 md:grid-cols-2 lg:grid-cols-4'>
|
<div className='grid gap-6 md:grid-cols-2 lg:grid-cols-4'>
|
||||||
{cards.map((card) => (
|
{cards.map((card) => (
|
||||||
<Card key={card.title} className="hover:bg-muted/50 transition-colors">
|
<Card key={card.title} className='hover:bg-muted/50 transition-colors'>
|
||||||
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
|
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
|
||||||
<CardTitle className='text-sm font-medium text-muted-foreground'>{card.title}</CardTitle>
|
<CardTitle className='text-muted-foreground text-sm font-medium'>
|
||||||
<div className={`p-2 rounded-full bg-secondary`}>
|
{card.title}
|
||||||
<card.icon className={`h-4 w-4 ${card.color}`} />
|
</CardTitle>
|
||||||
|
<div className={`bg-secondary rounded-full p-2`}>
|
||||||
|
<card.icon className={`h-4 w-4 ${card.color}`} />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className='text-2xl font-bold tracking-tight'>{card.value}</div>
|
<div className='text-2xl font-bold tracking-tight'>
|
||||||
|
{card.value}
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -816,7 +816,9 @@ export class AdminService {
|
|||||||
if (search) params.append('search', search);
|
if (search) params.append('search', search);
|
||||||
params.append('limit', limit.toString());
|
params.append('limit', limit.toString());
|
||||||
|
|
||||||
return await apiClient.get<LogResponse>(`/admin/api/logs?${params.toString()}`);
|
return await apiClient.get<LogResponse>(
|
||||||
|
`/admin/api/logs?${params.toString()}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getLogDates(): Promise<{ dates: string[] }> {
|
static async getLogDates(): Promise<{ dates: string[] }> {
|
||||||
@@ -862,9 +864,7 @@ export class AdminService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async createProviderAccountByType(
|
static async createProviderAccountByType(providerType: string): Promise<{
|
||||||
providerType: string
|
|
||||||
): Promise<{
|
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
account_data: Record<string, unknown>;
|
account_data: Record<string, unknown>;
|
||||||
message: string;
|
message: string;
|
||||||
@@ -881,7 +881,11 @@ export class AdminService {
|
|||||||
static async initiateProviderTopup(
|
static async initiateProviderTopup(
|
||||||
providerId: number,
|
providerId: number,
|
||||||
amount: number
|
amount: number
|
||||||
): Promise<{ ok: boolean; topup_data: Record<string, unknown>; message: string }> {
|
): Promise<{
|
||||||
|
ok: boolean;
|
||||||
|
topup_data: Record<string, unknown>;
|
||||||
|
message: string;
|
||||||
|
}> {
|
||||||
return await apiClient.post<{
|
return await apiClient.post<{
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
topup_data: Record<string, unknown>;
|
topup_data: Record<string, unknown>;
|
||||||
@@ -901,9 +905,10 @@ export class AdminService {
|
|||||||
}>(`/admin/api/upstream-providers/${providerId}/topup/${invoiceId}/status`);
|
}>(`/admin/api/upstream-providers/${providerId}/topup/${invoiceId}/status`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getProviderBalance(
|
static async getProviderBalance(providerId: number): Promise<{
|
||||||
providerId: number
|
ok: boolean;
|
||||||
): Promise<{ ok: boolean; balance_data: number | null | Record<string, unknown> }> {
|
balance_data: number | null | Record<string, unknown>;
|
||||||
|
}> {
|
||||||
return await apiClient.get<{
|
return await apiClient.get<{
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
balance_data: number | null | Record<string, unknown>;
|
balance_data: number | null | Record<string, unknown>;
|
||||||
|
|||||||
@@ -18,4 +18,3 @@ export const useCurrencyStore = create<CurrencyState>()(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user