mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 12:28:22 +00:00
Refine admin UI and models routing
This commit is contained in:
@@ -72,14 +72,16 @@ def _normalize_legacy_prompt_completion_pricing(
|
||||
|
||||
for field in ("prompt", "completion"):
|
||||
raw_value = normalized.get(field)
|
||||
if not isinstance(raw_value, (str, int, float)):
|
||||
continue
|
||||
try:
|
||||
value = float(raw_value) if raw_value is not None else None
|
||||
value = float(raw_value)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
# Older admin overrides were occasionally saved in "per 1M tokens"
|
||||
# units instead of the backend's expected per-token unit.
|
||||
if value is not None and value > 0.1:
|
||||
if value > 0.1:
|
||||
normalized[field] = value / 1_000_000
|
||||
|
||||
return normalized
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for pricing unit normalization across model sources."""
|
||||
|
||||
import json
|
||||
from types import TracebackType
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -74,7 +75,13 @@ async def test_generic_provider_missing_pricing_defaults_to_zero(
|
||||
async def __aenter__(self) -> "FakeClient":
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb) -> None:
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc: BaseException | None,
|
||||
tb: TracebackType | None,
|
||||
) -> None:
|
||||
del exc_type, exc, tb
|
||||
return None
|
||||
|
||||
async def get(
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type KeyboardEvent,
|
||||
} from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { CalendarIcon, Filter, X } from 'lucide-react';
|
||||
import { CalendarIcon, X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
@@ -219,10 +219,7 @@ export function LogFilters({
|
||||
return (
|
||||
<Card className='mb-6'>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex items-center gap-2'>
|
||||
<Filter className='h-5 w-5' />
|
||||
Filters
|
||||
</CardTitle>
|
||||
<CardTitle>Filters</CardTitle>
|
||||
<CardDescription>
|
||||
Filter logs by date, level, request ID, text search, status code,
|
||||
method, endpoint and limit
|
||||
|
||||
+7
-12
@@ -174,12 +174,7 @@ export default function LogsPage() {
|
||||
title='System Logs'
|
||||
description='View and filter application logs.'
|
||||
actions={
|
||||
<Button
|
||||
onClick={() => refetchLogs()}
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='w-full sm:w-auto'
|
||||
>
|
||||
<Button onClick={() => refetchLogs()} variant='outline' size='sm'>
|
||||
<RefreshCw className='mr-2 h-4 w-4' />
|
||||
Refresh
|
||||
</Button>
|
||||
@@ -208,21 +203,21 @@ export default function LogsPage() {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex flex-col items-start gap-2 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<span className='text-lg sm:text-xl'>Log Entries</span>
|
||||
<div className='flex flex-col items-start gap-2 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<CardTitle>Log Entries</CardTitle>
|
||||
{logsData && (
|
||||
<Badge variant='secondary' className='text-xs sm:text-sm'>
|
||||
<Badge variant='secondary'>
|
||||
{logsData.logs.length} entries
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</div>
|
||||
{hasActiveFilters && (
|
||||
<CardDescription className='text-xs sm:text-sm'>
|
||||
<CardDescription>
|
||||
Showing logs filtered by {activeFilterDescription}
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className='overflow-hidden p-3 sm:p-6'>
|
||||
<CardContent className='overflow-hidden'>
|
||||
{isLoading ? (
|
||||
<div className='space-y-2'>
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
|
||||
@@ -648,6 +648,20 @@ export default function DashboardPage() {
|
||||
}
|
||||
}, [chartConfigs, activeChartId]);
|
||||
|
||||
if (!isAuthResolved) {
|
||||
return (
|
||||
<AppPageShell contentClassName='mx-auto w-full max-w-5xl'>
|
||||
<div className='space-y-6'>
|
||||
<div className='space-y-2'>
|
||||
<Skeleton className='h-8 w-56 max-w-full sm:h-9' />
|
||||
<Skeleton className='h-4 w-80 max-w-full' />
|
||||
</div>
|
||||
<SectionLoading label='summary' />
|
||||
<SectionLoading label='metrics' />
|
||||
</div>
|
||||
</AppPageShell>
|
||||
);
|
||||
}
|
||||
if (!isAuthenticated) {
|
||||
return <CheatSheet />;
|
||||
}
|
||||
|
||||
+24
-13
@@ -20,6 +20,13 @@ import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { AlertCircle, Plus, Server } from 'lucide-react';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from '@/components/ui/empty';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -333,7 +340,7 @@ export default function ProvidersPage() {
|
||||
description='Manage your AI provider connections and credentials.'
|
||||
actions={
|
||||
<DialogTrigger asChild>
|
||||
<Button className='flex items-center gap-2'>
|
||||
<Button>
|
||||
<Plus className='h-4 w-4' />
|
||||
Add Provider
|
||||
</Button>
|
||||
@@ -376,18 +383,22 @@ export default function ProvidersPage() {
|
||||
</Alert>
|
||||
) : providers.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className='flex flex-col items-center justify-center py-12'>
|
||||
<Server className='text-muted-foreground mb-4 h-12 w-12' />
|
||||
<h3 className='mb-2 text-lg font-semibold'>
|
||||
No providers configured
|
||||
</h3>
|
||||
<p className='text-muted-foreground mb-4 text-sm'>
|
||||
Get started by adding your first upstream provider
|
||||
</p>
|
||||
<Button onClick={() => setIsCreateDialogOpen(true)}>
|
||||
<Plus className='mr-2 h-4 w-4' />
|
||||
Add Provider
|
||||
</Button>
|
||||
<CardContent className='py-12'>
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant='icon'>
|
||||
<Server className='h-4 w-4' />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No providers configured</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Get started by adding your first upstream provider.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<Button onClick={() => setIsCreateDialogOpen(true)}>
|
||||
<Plus className='mr-2 h-4 w-4' />
|
||||
Add Provider
|
||||
</Button>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
|
||||
@@ -483,10 +483,7 @@ export function ApiEndpointTester({ models }: ApiEndpointTesterProps) {
|
||||
return (
|
||||
<Card className='w-full'>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex items-center gap-2'>
|
||||
<Send className='h-5 w-5' />
|
||||
API Endpoint Tester
|
||||
</CardTitle>
|
||||
<CardTitle>API Endpoint Tester</CardTitle>
|
||||
<CardDescription>
|
||||
Comprehensive testing of OpenAI-compatible API endpoints through the
|
||||
secure proxy (resolves CORS and network issues)
|
||||
|
||||
@@ -46,7 +46,7 @@ export const API_ENDPOINTS = {
|
||||
},
|
||||
models: {
|
||||
name: 'List Models',
|
||||
path: '/model',
|
||||
path: '/models',
|
||||
icon: List,
|
||||
description: 'List all available models from the provider',
|
||||
},
|
||||
|
||||
@@ -40,7 +40,7 @@ const NAV_ITEMS = [
|
||||
{ title: 'Dashboard', url: '/', icon: LayoutDashboardIcon },
|
||||
{ title: 'Balances', url: '/balances', icon: WalletIcon },
|
||||
{ title: 'Logs', url: '/logs', icon: FileTextIcon },
|
||||
{ title: 'Models', url: '/model', icon: DatabaseIcon },
|
||||
{ title: 'Models', url: '/models', icon: DatabaseIcon },
|
||||
{ title: 'Providers', url: '/providers', icon: ServerIcon },
|
||||
{ title: 'Settings', url: '/settings', icon: SettingsIcon },
|
||||
] as const;
|
||||
|
||||
@@ -52,7 +52,7 @@ const data = {
|
||||
},
|
||||
{
|
||||
title: 'Models',
|
||||
url: '/model',
|
||||
url: '/models',
|
||||
icon: DatabaseIcon,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -78,23 +78,17 @@ export function DashboardBalanceSummary({
|
||||
return (
|
||||
<div className='grid grid-cols-2 gap-2.5 max-[359px]:grid-cols-1 sm:gap-3 lg:grid-cols-3'>
|
||||
{cards.map((card) => (
|
||||
<Card
|
||||
key={card.title}
|
||||
size='sm'
|
||||
className='min-h-[6.25rem] sm:min-h-[7rem]'
|
||||
>
|
||||
<Card key={card.title} size='sm'>
|
||||
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-1'>
|
||||
<CardTitle className='text-muted-foreground text-[11px] font-medium sm:text-sm'>
|
||||
{card.title}
|
||||
</CardTitle>
|
||||
<div className='flex size-6 items-center justify-center sm:size-7'>
|
||||
<card.icon
|
||||
className={`h-3.5 w-3.5 sm:h-4 sm:w-4 ${card.color}`}
|
||||
/>
|
||||
</div>
|
||||
<span className='inline-flex size-6 items-center justify-center sm:size-7'>
|
||||
<card.icon className={`size-3.5 sm:size-4 ${card.color}`} />
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent className='pt-0'>
|
||||
<div className='text-base font-semibold break-words sm:text-xl'>
|
||||
<div className='text-base font-semibold break-words tabular-nums sm:text-xl'>
|
||||
{card.value}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -107,10 +107,15 @@ export function DetailedWalletBalance({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className='h-full w-full shadow-sm'>
|
||||
<Card>
|
||||
<CardHeader className='pb-4'>
|
||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<CardTitle className='text-xl'>Cashu Wallet Balance</CardTitle>
|
||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
|
||||
<div className='space-y-1.5'>
|
||||
<CardTitle>Cashu Wallet Balance</CardTitle>
|
||||
<CardDescription>
|
||||
Detailed balance breakdown by mint and currency
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className='flex w-full gap-2 sm:w-auto'>
|
||||
<Button
|
||||
variant='default'
|
||||
@@ -127,7 +132,6 @@ export function DetailedWalletBalance({
|
||||
size='icon'
|
||||
onClick={() => refetch()}
|
||||
disabled={isLoading || isFetching}
|
||||
className='h-8 w-8'
|
||||
>
|
||||
<RefreshCw
|
||||
className={cn(
|
||||
@@ -139,19 +143,13 @@ export function DetailedWalletBalance({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Detailed balance breakdown by mint and currency
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className='space-y-4'>
|
||||
<div className='grid gap-3 md:grid-cols-3'>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Card
|
||||
key={`wallet-stat-skeleton-${index}`}
|
||||
className='shadow-none'
|
||||
>
|
||||
<Card key={`wallet-stat-skeleton-${index}`}>
|
||||
<CardHeader className='space-y-2 pb-1'>
|
||||
<Skeleton className='h-3.5 w-28' />
|
||||
<Skeleton className='h-3 w-8' />
|
||||
@@ -182,47 +180,47 @@ export function DetailedWalletBalance({
|
||||
) : (
|
||||
<div className='space-y-6'>
|
||||
<div className='grid gap-3 md:grid-cols-3'>
|
||||
<Card className='shadow-none'>
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
|
||||
<CardTitle className='text-sm font-medium'>
|
||||
<CardTitle className='text-muted-foreground text-sm font-medium'>
|
||||
Your Balance (Total)
|
||||
</CardTitle>
|
||||
<span className='inline-flex size-8 items-center justify-center text-green-600 dark:text-green-300'>
|
||||
<Coins className='h-4 w-4' />
|
||||
<span className='inline-flex size-8 items-center justify-center'>
|
||||
<Coins className='size-4 text-green-600 dark:text-green-300' />
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className='text-primary text-2xl font-bold tracking-tight'>
|
||||
<CardContent className='pt-0'>
|
||||
<p className='text-primary text-2xl font-semibold tracking-tight tabular-nums'>
|
||||
{formatAmount(totals.totalOwner)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className='shadow-none'>
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
|
||||
<CardTitle className='text-sm font-medium'>
|
||||
<CardTitle className='text-muted-foreground text-sm font-medium'>
|
||||
Total Wallet
|
||||
</CardTitle>
|
||||
<span className='inline-flex size-8 items-center justify-center text-blue-600 dark:text-blue-300'>
|
||||
<Wallet className='h-4 w-4' />
|
||||
<span className='inline-flex size-8 items-center justify-center'>
|
||||
<Wallet className='size-4 text-blue-600 dark:text-blue-300' />
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className='text-2xl font-bold tracking-tight'>
|
||||
<CardContent className='pt-0'>
|
||||
<p className='text-2xl font-semibold tracking-tight tabular-nums'>
|
||||
{formatAmount(totals.totalWallet)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className='shadow-none'>
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
|
||||
<CardTitle className='text-sm font-medium'>
|
||||
<CardTitle className='text-muted-foreground text-sm font-medium'>
|
||||
User Balance
|
||||
</CardTitle>
|
||||
<span className='inline-flex size-8 items-center justify-center text-purple-600 dark:text-purple-300'>
|
||||
<User className='h-4 w-4' />
|
||||
<span className='inline-flex size-8 items-center justify-center'>
|
||||
<User className='size-4 text-purple-600 dark:text-purple-300' />
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className='text-2xl font-bold tracking-tight'>
|
||||
<CardContent className='pt-0'>
|
||||
<p className='text-2xl font-semibold tracking-tight tabular-nums'>
|
||||
{formatAmount(totals.totalUser)}
|
||||
</p>
|
||||
</CardContent>
|
||||
@@ -294,7 +292,6 @@ export function DetailedWalletBalance({
|
||||
<Card
|
||||
key={`${key}-mobile`}
|
||||
className={cn(
|
||||
'shadow-none',
|
||||
detail.error &&
|
||||
'border-destructive/40 bg-destructive/5'
|
||||
)}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Loader2, Copy, Check, SendIcon } from 'lucide-react';
|
||||
import { Loader2, Copy, Check } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
@@ -101,10 +101,7 @@ export function EcashRedeem() {
|
||||
return (
|
||||
<Card className='h-full w-full shadow-sm'>
|
||||
<CardHeader>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<SendIcon className='text-primary h-5 w-5' />
|
||||
<CardTitle>Send eCash</CardTitle>
|
||||
</div>
|
||||
<CardTitle>Send eCash</CardTitle>
|
||||
<CardDescription>
|
||||
Generate a token to send eCash to someone
|
||||
</CardDescription>
|
||||
|
||||
@@ -172,10 +172,7 @@ export function ApiKeyManager({
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className='space-y-1'>
|
||||
<CardTitle className='flex items-center gap-2 text-xl'>
|
||||
<RefreshCcw className='text-primary h-5 w-5' />
|
||||
API Key Management
|
||||
</CardTitle>
|
||||
<CardTitle className='text-xl'>API Key Management</CardTitle>
|
||||
<p className='text-muted-foreground text-xs tracking-wide'>
|
||||
Manage your existing API keys and balances
|
||||
</p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { type JSX, useCallback, useState } from 'react';
|
||||
import { Copy, KeyRound, RefreshCcw, Trash2 } from 'lucide-react';
|
||||
import { Copy, RefreshCcw, Trash2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -305,10 +305,7 @@ export function CashuPaymentWorkflow({
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className='space-y-1'>
|
||||
<CardTitle className='flex items-center gap-2 text-xl'>
|
||||
<KeyRound className='text-primary h-5 w-5' />
|
||||
API key workflow
|
||||
</CardTitle>
|
||||
<CardTitle className='text-xl'>API key workflow</CardTitle>
|
||||
<p className='text-muted-foreground text-xs tracking-wide'>
|
||||
Sections expand as soon as you interact
|
||||
</p>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { type JSX, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Bolt, Copy, RefreshCcw, ShieldCheck, Terminal } from 'lucide-react';
|
||||
import { Bolt, Copy, RefreshCcw, Terminal } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -193,10 +193,7 @@ export function CheatSheet(): JSX.Element {
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-start justify-between gap-4'>
|
||||
<div>
|
||||
<CardTitle className='flex items-center gap-2 text-lg'>
|
||||
<ShieldCheck className='text-primary h-4 w-4' />
|
||||
Node identity
|
||||
</CardTitle>
|
||||
<CardTitle className='text-lg'>Node identity</CardTitle>
|
||||
<p className='text-muted-foreground text-xs tracking-wide'>
|
||||
/v1/info snapshot
|
||||
</p>
|
||||
@@ -341,10 +338,7 @@ export function CheatSheet(): JSX.Element {
|
||||
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between'>
|
||||
<CardTitle className='flex items-center gap-2 text-lg'>
|
||||
<Terminal className='text-primary h-4 w-4' />
|
||||
Quick docs
|
||||
</CardTitle>
|
||||
<CardTitle className='text-lg'>Quick docs</CardTitle>
|
||||
<span className='text-muted-foreground text-xs tracking-wide'>
|
||||
curl-ready
|
||||
</span>
|
||||
|
||||
@@ -4,11 +4,7 @@ import { type JSX, useState, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
Copy,
|
||||
RefreshCcw,
|
||||
ShieldCheck,
|
||||
History,
|
||||
Users,
|
||||
RotateCcw,
|
||||
KeyRound,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
@@ -154,10 +150,7 @@ export function KeyInfoDetails({
|
||||
<div className='space-y-6'>
|
||||
<Card>
|
||||
<CardHeader className='space-y-1'>
|
||||
<CardTitle className='flex items-center gap-2 text-xl'>
|
||||
<KeyRound className='text-primary h-5 w-5' />
|
||||
Key Information
|
||||
</CardTitle>
|
||||
<CardTitle className='text-xl'>Key Information</CardTitle>
|
||||
<CardDescription>
|
||||
Enter an API key to view its balance, consumption, and child keys.
|
||||
</CardDescription>
|
||||
@@ -202,10 +195,7 @@ export function KeyInfoDetails({
|
||||
<div className='grid gap-4 md:grid-cols-2'>
|
||||
<Card>
|
||||
<CardHeader className='pb-2'>
|
||||
<CardTitle className='flex items-center gap-2 text-lg'>
|
||||
<ShieldCheck className='text-primary h-5 w-5' />
|
||||
Status & Identity
|
||||
</CardTitle>
|
||||
<CardTitle className='text-lg'>Status & Identity</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
@@ -255,10 +245,7 @@ export function KeyInfoDetails({
|
||||
|
||||
<Card>
|
||||
<CardHeader className='pb-2'>
|
||||
<CardTitle className='flex items-center gap-2 text-lg'>
|
||||
<History className='text-primary h-5 w-5' />
|
||||
Consumption
|
||||
</CardTitle>
|
||||
<CardTitle className='text-lg'>Consumption</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
@@ -313,8 +300,7 @@ export function KeyInfoDetails({
|
||||
walletInfo.childKeys.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex items-center gap-2 text-lg'>
|
||||
<Users className='text-primary h-5 w-5' />
|
||||
<CardTitle className='text-lg'>
|
||||
Child Keys ({walletInfo.childKeys.length})
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { type JSX, useCallback, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { Copy, Zap, CheckCircle, Loader2 } from 'lucide-react';
|
||||
import { Copy, CheckCircle, Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import QRCode from 'qrcode';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@@ -514,10 +514,7 @@ export function LightningPaymentWorkflow({
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className='space-y-1'>
|
||||
<CardTitle className='flex items-center gap-2 text-xl'>
|
||||
<Zap className='text-primary h-5 w-5' />
|
||||
Lightning Payment Workflow
|
||||
</CardTitle>
|
||||
<CardTitle className='text-xl'>Lightning Payment Workflow</CardTitle>
|
||||
<p className='text-muted-foreground text-xs tracking-wide'>
|
||||
Create and manage API keys using Lightning Network payments
|
||||
</p>
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
Key,
|
||||
MoreVertical,
|
||||
RefreshCw,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface ModelProviderSectionProps {
|
||||
@@ -133,8 +132,7 @@ export function ModelProviderSection({
|
||||
aria-label={`Select models for provider ${provider}`}
|
||||
/>
|
||||
<div className='min-w-0'>
|
||||
<CardTitle className='flex items-center gap-1.5 text-base sm:gap-2 sm:text-lg'>
|
||||
<Users className='h-5 w-5' />
|
||||
<CardTitle className='flex items-baseline gap-1.5 text-base sm:text-lg'>
|
||||
<span className='truncate'>{provider}</span>
|
||||
<span className='text-muted-foreground text-sm font-normal'>
|
||||
({providerModels.length} models)
|
||||
|
||||
@@ -203,10 +203,7 @@ export function ModelTester({ models }: ModelTesterProps) {
|
||||
return (
|
||||
<Card className='w-full'>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex items-center gap-2'>
|
||||
<Send className='h-5 w-5' />
|
||||
Model Credential Tester
|
||||
</CardTitle>
|
||||
<CardTitle>Model Credential Tester</CardTitle>
|
||||
<CardDescription>
|
||||
Test model functionality by sending chat completion requests through
|
||||
the secure proxy (resolves CORS and network issues)
|
||||
|
||||
@@ -173,9 +173,11 @@ export function ProviderBalance({
|
||||
onClick={handleTopUpClick}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className='w-full font-mono sm:w-auto'
|
||||
className='w-full sm:w-auto'
|
||||
>
|
||||
{isHovered ? 'Top Up' : displayValue}
|
||||
<span className={isHovered ? undefined : 'font-mono'}>
|
||||
{isHovered ? 'Top Up' : displayValue}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
<Dialog open={isTopupDialogOpen} onOpenChange={handleCloseDialog}>
|
||||
|
||||
@@ -54,22 +54,20 @@ export function ProviderCard({
|
||||
const hasDetails = Boolean(provider.api_version) || isExpanded;
|
||||
|
||||
return (
|
||||
<Card size='sm' className='border-border/70 bg-card/55'>
|
||||
<CardHeader className='pb-2'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='flex flex-col gap-2 sm:flex-row sm:items-center'>
|
||||
<CardTitle className='truncate text-base sm:text-lg'>
|
||||
{provider.provider_type}
|
||||
</CardTitle>
|
||||
<CardTitle className='truncate'>{provider.provider_type}</CardTitle>
|
||||
<Badge
|
||||
variant={provider.enabled ? 'default' : 'secondary'}
|
||||
className='w-fit sm:ml-2'
|
||||
className='w-fit'
|
||||
>
|
||||
{provider.enabled ? 'Enabled' : 'Disabled'}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription className='mt-1 text-xs break-all sm:text-sm'>
|
||||
<CardDescription className='break-all'>
|
||||
{provider.base_url}
|
||||
</CardDescription>
|
||||
</div>
|
||||
@@ -88,7 +86,7 @@ export function ProviderCard({
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={onToggleExpansion}
|
||||
className='col-span-2 h-9 justify-between px-3 sm:col-auto sm:h-8 sm:justify-center'
|
||||
className='col-span-2 justify-between sm:col-auto sm:justify-center'
|
||||
>
|
||||
<span className='inline-flex items-center gap-1.5'>
|
||||
<Database className='h-4 w-4' />
|
||||
@@ -105,7 +103,7 @@ export function ProviderCard({
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={onEditProvider}
|
||||
className='h-9 justify-center gap-1.5 px-3 sm:h-8'
|
||||
className='justify-center gap-1.5'
|
||||
>
|
||||
<Pencil className='h-4 w-4' />
|
||||
<span>Edit</span>
|
||||
@@ -115,7 +113,7 @@ export function ProviderCard({
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={onDeleteProvider}
|
||||
className='text-destructive hover:text-destructive h-9 justify-center gap-1.5 px-3 sm:h-8'
|
||||
className='text-destructive hover:text-destructive justify-center gap-1.5'
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
<span>Delete</span>
|
||||
@@ -125,7 +123,7 @@ export function ProviderCard({
|
||||
</CardHeader>
|
||||
|
||||
{hasDetails ? (
|
||||
<CardContent className='pt-1'>
|
||||
<CardContent>
|
||||
<div className='space-y-3'>
|
||||
{provider.api_version && (
|
||||
<div className='flex flex-col gap-1 text-sm sm:flex-row sm:items-center sm:justify-between'>
|
||||
|
||||
@@ -23,7 +23,7 @@ const PAGE_META: Record<string, { title: string; description: string }> = {
|
||||
title: 'System Logs',
|
||||
description: 'Inspect request and application logs.',
|
||||
},
|
||||
'/model': {
|
||||
'/models': {
|
||||
title: 'Models',
|
||||
description: 'Manage model catalog and provider mappings.',
|
||||
},
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Clock,
|
||||
DollarSign,
|
||||
Activity,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { AdminService, TemporaryBalance } from '@/lib/api/services/admin';
|
||||
import {
|
||||
@@ -43,39 +42,6 @@ import { cn } from '@/lib/utils';
|
||||
import type { DisplayUnit } from '@/lib/types/units';
|
||||
import { formatFromMsat } from '@/lib/currency';
|
||||
|
||||
function TemporaryBalanceStat({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
iconClassName,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
value: string;
|
||||
iconClassName: string;
|
||||
}) {
|
||||
return (
|
||||
<Card className='shadow-none'>
|
||||
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
|
||||
<CardTitle className='text-sm font-medium'>{label}</CardTitle>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex size-8 items-center justify-center rounded-full',
|
||||
iconClassName
|
||||
)}
|
||||
>
|
||||
<Icon className='h-4 w-4' />
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className='text-2xl font-bold tracking-tight tabular-nums'>
|
||||
{value}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function getTotals(balances: TemporaryBalance[]) {
|
||||
let totalBalance = 0;
|
||||
let totalSpent = 0;
|
||||
@@ -157,32 +123,32 @@ export function TemporaryBalances({
|
||||
const rows = data ? buildHierarchicalData(data, filteredData) : [];
|
||||
|
||||
return (
|
||||
<Card className='h-full w-full shadow-sm'>
|
||||
<Card>
|
||||
<CardHeader className='pb-4'>
|
||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<CardTitle className='flex items-center gap-2 text-xl'>
|
||||
<Key className='h-5 w-5' />
|
||||
Temporary Balances
|
||||
</CardTitle>
|
||||
<div className='flex w-full flex-col gap-2 sm:w-auto sm:flex-row'>
|
||||
<div className='relative flex-1 sm:flex-initial'>
|
||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
|
||||
<div className='space-y-1.5'>
|
||||
<CardTitle>Temporary Balances</CardTitle>
|
||||
<CardDescription className='max-w-2xl'>
|
||||
API keys with their current balances and usage statistics
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<div className='flex w-full gap-2 sm:w-auto sm:pt-0.5'>
|
||||
<div className='min-w-0 flex-1 sm:w-72 sm:flex-none'>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder='Search by key or address...'
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className='pr-3 pl-8 sm:w-64'
|
||||
name='temporary_balance_search'
|
||||
autoComplete='off'
|
||||
/>
|
||||
<Key className='text-muted-foreground absolute top-2.5 left-2 h-4 w-4' />
|
||||
</div>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => refetch()}
|
||||
disabled={isLoading || isFetching}
|
||||
className='h-8 w-full sm:w-8'
|
||||
>
|
||||
<RefreshCw
|
||||
className={cn(
|
||||
@@ -194,19 +160,13 @@ export function TemporaryBalances({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<CardDescription>
|
||||
API keys with their current balances and usage statistics
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className='space-y-4'>
|
||||
<div className='grid gap-3 md:grid-cols-3'>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Card
|
||||
key={`temp-stat-skeleton-${index}`}
|
||||
className='shadow-none'
|
||||
>
|
||||
<Card key={`temp-stat-skeleton-${index}`}>
|
||||
<CardHeader className='space-y-2 pb-1'>
|
||||
<Skeleton className='h-3.5 w-24' />
|
||||
<Skeleton className='h-3 w-8' />
|
||||
@@ -236,24 +196,51 @@ export function TemporaryBalances({
|
||||
) : (
|
||||
<div className='space-y-6'>
|
||||
<div className='grid gap-3 md:grid-cols-3'>
|
||||
<TemporaryBalanceStat
|
||||
icon={DollarSign}
|
||||
label='Total Balance'
|
||||
value={formatBalance(totals.totalBalance)}
|
||||
iconClassName='text-green-600 dark:text-green-300'
|
||||
/>
|
||||
<TemporaryBalanceStat
|
||||
icon={Activity}
|
||||
label='Total Spent'
|
||||
value={formatBalance(totals.totalSpent)}
|
||||
iconClassName='text-blue-600 dark:text-blue-300'
|
||||
/>
|
||||
<TemporaryBalanceStat
|
||||
icon={Key}
|
||||
label='Total Requests'
|
||||
value={totals.totalRequests.toLocaleString()}
|
||||
iconClassName='text-purple-600 dark:text-purple-300'
|
||||
/>
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
|
||||
<CardTitle className='text-muted-foreground text-sm font-medium'>
|
||||
Total Balance
|
||||
</CardTitle>
|
||||
<span className='inline-flex size-8 items-center justify-center'>
|
||||
<DollarSign className='size-4 text-green-600 dark:text-green-300' />
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent className='pt-0'>
|
||||
<p className='text-2xl font-semibold tracking-tight tabular-nums'>
|
||||
{formatBalance(totals.totalBalance)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
|
||||
<CardTitle className='text-muted-foreground text-sm font-medium'>
|
||||
Total Spent
|
||||
</CardTitle>
|
||||
<span className='inline-flex size-8 items-center justify-center'>
|
||||
<Activity className='size-4 text-blue-600 dark:text-blue-300' />
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent className='pt-0'>
|
||||
<p className='text-2xl font-semibold tracking-tight tabular-nums'>
|
||||
{formatBalance(totals.totalSpent)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
|
||||
<CardTitle className='text-muted-foreground text-sm font-medium'>
|
||||
Total Requests
|
||||
</CardTitle>
|
||||
<span className='inline-flex size-8 items-center justify-center'>
|
||||
<Key className='size-4 text-purple-600 dark:text-purple-300' />
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent className='pt-0'>
|
||||
<p className='text-2xl font-semibold tracking-tight tabular-nums'>
|
||||
{totals.totalRequests.toLocaleString()}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{rows.length > 0 ? (
|
||||
@@ -343,7 +330,6 @@ export function TemporaryBalances({
|
||||
<Card
|
||||
key={`${balance.hashed_key}-${balance.parent_key_hash ?? 'root'}-mobile-${index}`}
|
||||
className={cn(
|
||||
'shadow-none',
|
||||
balance.balance === 0 &&
|
||||
!balance.isChild &&
|
||||
'opacity-80',
|
||||
|
||||
@@ -114,19 +114,17 @@ export function UsageSummaryCards({ summary }: UsageSummaryCardsProps) {
|
||||
return (
|
||||
<div className='grid grid-cols-1 gap-2.5 px-1 min-[380px]:grid-cols-2 sm:gap-4 sm:px-0 xl:grid-cols-4'>
|
||||
{cards.map((card) => (
|
||||
<Card key={card.title} className='hover:bg-muted/40 transition-colors'>
|
||||
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-1 sm:pb-2'>
|
||||
<CardTitle className='text-muted-foreground text-xs font-medium sm:text-sm'>
|
||||
<Card key={card.title} size='sm'>
|
||||
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-1'>
|
||||
<CardTitle className='text-muted-foreground text-[11px] font-medium sm:text-sm'>
|
||||
{card.title}
|
||||
</CardTitle>
|
||||
<span
|
||||
className={`inline-flex size-6 items-center justify-center sm:size-8 ${card.iconClassName}`}
|
||||
>
|
||||
<card.icon className='h-3.5 w-3.5 sm:h-4 sm:w-4' />
|
||||
<span className='inline-flex size-6 items-center justify-center sm:size-7'>
|
||||
<card.icon className={`size-3.5 sm:size-4 ${card.iconClassName}`} />
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardContent className='pt-0'>
|
||||
<div className='text-lg font-semibold tracking-tight break-words sm:text-2xl'>
|
||||
<div className='text-base font-semibold break-words tabular-nums sm:text-2xl'>
|
||||
{card.value}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user