easyai-ai-gateway/apps/web/src/App.tsx
wangbo 6323e70e49 Initial project scaffold
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 14:36:35 +08:00

228 lines
7.9 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import type {
BaseModelCatalogItem,
CatalogProvider,
IntegrationPlatform,
PlatformModel,
PricingRule,
RateLimitWindow,
} from '@easyai-ai-gateway/contracts';
import {
getHealth,
listBaseModels,
listCatalogProviders,
listModels,
listPlatforms,
listPricingRules,
listRateLimitWindows,
type HealthResponse,
} from './api';
type LoadState = 'idle' | 'loading' | 'ready' | 'error';
export function App() {
const [token, setToken] = useState('');
const [health, setHealth] = useState<HealthResponse | null>(null);
const [platforms, setPlatforms] = useState<IntegrationPlatform[]>([]);
const [models, setModels] = useState<PlatformModel[]>([]);
const [providers, setProviders] = useState<CatalogProvider[]>([]);
const [baseModels, setBaseModels] = useState<BaseModelCatalogItem[]>([]);
const [pricingRules, setPricingRules] = useState<PricingRule[]>([]);
const [rateLimitWindows, setRateLimitWindows] = useState<RateLimitWindow[]>([]);
const [state, setState] = useState<LoadState>('idle');
const [error, setError] = useState('');
useEffect(() => {
getHealth()
.then(setHealth)
.catch((err: Error) => setError(err.message));
}, []);
const stats = useMemo(() => {
const enabledPlatforms = platforms.filter((item) => item.status === 'enabled').length;
const enabledModels = models.filter((item) => item.enabled).length;
const activeProviders = providers.filter((item) => item.status === 'active').length;
const activeRateWindows = rateLimitWindows.filter((item) => item.resetAt >= new Date().toISOString()).length;
return [
{ label: '平台', value: platforms.length, tone: 'blue' },
{ label: '启用平台', value: enabledPlatforms, tone: 'green' },
{ label: '基准模型', value: baseModels.length, tone: 'violet' },
{ label: 'Provider', value: activeProviders || providers.length || enabledModels, tone: 'amber' },
{ label: '定价规则', value: pricingRules.length, tone: 'cyan' },
{ label: '限流窗口', value: activeRateWindows, tone: 'rose' },
];
}, [baseModels.length, models, platforms, pricingRules.length, providers, rateLimitWindows]);
async function refresh() {
setState('loading');
setError('');
try {
const [
platformResponse,
modelResponse,
providerResponse,
baseModelResponse,
pricingRuleResponse,
rateLimitWindowResponse,
] = await Promise.all([
listPlatforms(token),
listModels(token),
listCatalogProviders(token),
listBaseModels(token),
listPricingRules(token),
listRateLimitWindows(token),
]);
setPlatforms(platformResponse.items);
setModels(modelResponse.items);
setProviders(providerResponse.items);
setBaseModels(baseModelResponse.items);
setPricingRules(pricingRuleResponse.items);
setRateLimitWindows(rateLimitWindowResponse.items);
setState('ready');
} catch (err) {
setState('error');
setError(err instanceof Error ? err.message : '加载失败');
}
}
return (
<main className="page">
<header className="topbar">
<div>
<p className="eyebrow">EasyAI</p>
<h1>AI Gateway Console</h1>
</div>
<div className="health" data-ok={health?.ok === true}>
<span />
{health?.service ?? 'API 未连接'}
</div>
</header>
<section className="toolbar" aria-label="授权与刷新">
<label className="tokenField">
<span>Server Main JWT</span>
<input
value={token}
onChange={(event) => setToken(event.target.value)}
placeholder="粘贴 server-main access_token"
/>
</label>
<button type="button" onClick={refresh} disabled={!token || state === 'loading'}>
{state === 'loading' ? '加载中' : '刷新'}
</button>
</section>
{error && <div className="notice">{error}</div>}
<section className="metrics" aria-label="概览">
{stats.map((item) => (
<div className="metric" data-tone={item.tone} key={item.label}>
<span>{item.label}</span>
<strong>{item.value}</strong>
</div>
))}
</section>
<section className="split">
<div className="panel">
<div className="panelHeader">
<h2></h2>
<span>{platforms.length}</span>
</div>
<div className="table" role="table">
<div className="row head" role="row">
<span>Provider</span>
<span></span>
<span></span>
<span></span>
</div>
{platforms.map((item) => (
<div className="row" role="row" key={item.id}>
<span>{item.provider}</span>
<span>{item.name}</span>
<span>{item.status}</span>
<span>{item.priority}</span>
</div>
))}
{!platforms.length && <p className="empty"></p>}
</div>
</div>
<div className="panel">
<div className="panelHeader">
<h2></h2>
<span>{models.length}</span>
</div>
<div className="table" role="table">
<div className="row head" role="row">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
{models.map((item) => (
<div className="row" role="row" key={item.id}>
<span>{item.modelName}</span>
<span>{item.modelType}</span>
<span>{item.provider ?? item.platformName}</span>
<span>{item.enabled ? '是' : '否'}</span>
</div>
))}
{!models.length && <p className="empty"></p>}
</div>
</div>
</section>
<section className="split secondary">
<div className="panel">
<div className="panelHeader">
<h2></h2>
<span>{baseModels.length}</span>
</div>
<div className="table catalogTable" role="table">
<div className="row head" role="row">
<span>Provider</span>
<span></span>
<span></span>
<span></span>
</div>
{baseModels.map((item) => (
<div className="row" role="row" key={item.id}>
<span>{item.providerKey}</span>
<span>{item.canonicalModelKey}</span>
<span>{item.modelType}</span>
<span>{item.pricingVersion}</span>
</div>
))}
{!baseModels.length && <p className="empty"></p>}
</div>
</div>
<div className="panel">
<div className="panelHeader">
<h2>TPM/RPM </h2>
<span>{rateLimitWindows.length}</span>
</div>
<div className="table rateTable" role="table">
<div className="row head" role="row">
<span>Scope</span>
<span></span>
<span>使</span>
<span></span>
</div>
{rateLimitWindows.map((item) => (
<div className="row" role="row" key={`${item.scopeType}:${item.scopeKey}:${item.metric}:${item.windowStart}`}>
<span>{item.scopeKey}</span>
<span>{item.metric}</span>
<span>{item.usedValue}/{item.limitValue}</span>
<span>{item.reservedValue}</span>
</div>
))}
{!rateLimitWindows.length && <p className="empty"></p>}
</div>
</div>
</section>
</main>
);
}