mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 02:11:48 +00:00
chore: more components
This commit is contained in:
parent
e116957a63
commit
a9369fc193
3 changed files with 56 additions and 105 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { ExternalLink, Copy, X, Check, Loader2 } from "lucide-react";
|
||||
import { Button, H2 } from "@nous-research/ui";
|
||||
import { ExternalLink, X, Check, Loader2 } from "lucide-react";
|
||||
import { Button, CopyButton, H2 } from "@nous-research/ui";
|
||||
import { api, type OAuthProvider, type OAuthStartResponse } from "@/lib/api";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useI18n } from "@/i18n";
|
||||
|
|
@ -25,14 +25,12 @@ export function OAuthLoginModal({
|
|||
provider,
|
||||
onClose,
|
||||
onSuccess,
|
||||
onError,
|
||||
}: Props) {
|
||||
const [phase, setPhase] = useState<Phase>("starting");
|
||||
const [start, setStart] = useState<OAuthStartResponse | null>(null);
|
||||
const [pkceCode, setPkceCode] = useState("");
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
const [secondsLeft, setSecondsLeft] = useState<number | null>(null);
|
||||
const [codeCopied, setCodeCopied] = useState(false);
|
||||
const isMounted = useRef(true);
|
||||
const pollTimer = useRef<number | null>(null);
|
||||
const { t } = useI18n();
|
||||
|
|
@ -153,16 +151,6 @@ export function OAuthLoginModal({
|
|||
onClose();
|
||||
};
|
||||
|
||||
const handleCopyUserCode = async (code: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCodeCopied(true);
|
||||
window.setTimeout(() => isMounted.current && setCodeCopied(false), 1500);
|
||||
} catch {
|
||||
onError("Clipboard write failed");
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackdrop = (e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) handleClose();
|
||||
};
|
||||
|
|
@ -286,26 +274,16 @@ export function OAuthLoginModal({
|
|||
).user_code
|
||||
}
|
||||
</code>
|
||||
<Button
|
||||
outlined
|
||||
onClick={() =>
|
||||
handleCopyUserCode(
|
||||
(
|
||||
start as Extract<
|
||||
OAuthStartResponse,
|
||||
{ flow: "device_code" }
|
||||
>
|
||||
).user_code,
|
||||
)
|
||||
<CopyButton
|
||||
text={
|
||||
(
|
||||
start as Extract<
|
||||
OAuthStartResponse,
|
||||
{ flow: "device_code" }
|
||||
>
|
||||
).user_code
|
||||
}
|
||||
className="!p-2 aspect-square"
|
||||
>
|
||||
{codeCopied ? (
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
href={
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { ShieldCheck, ShieldOff, Copy, ExternalLink, RefreshCw, LogOut, Terminal, LogIn } from "lucide-react";
|
||||
import { ShieldCheck, ShieldOff, ExternalLink, RefreshCw, LogOut, Terminal, LogIn } from "lucide-react";
|
||||
import { api, type OAuthProvider } from "@/lib/api";
|
||||
import { Button } from "@nous-research/ui";
|
||||
import { Button, CopyButton } from "@nous-research/ui";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { OAuthLoginModal } from "@/components/OAuthLoginModal";
|
||||
|
|
@ -35,7 +35,6 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) {
|
|||
const [providers, setProviders] = useState<OAuthProvider[] | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const [loginFor, setLoginFor] = useState<OAuthProvider | null>(null);
|
||||
const { t } = useI18n();
|
||||
|
||||
|
|
@ -55,17 +54,6 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) {
|
|||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const handleCopy = async (provider: OAuthProvider) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(provider.cli_command);
|
||||
setCopiedId(provider.id);
|
||||
onSuccess?.(`Copied: ${provider.cli_command}`);
|
||||
setTimeout(() => setCopiedId((v) => (v === provider.id ? null : v)), 1500);
|
||||
} catch {
|
||||
onError?.("Clipboard write failed — copy the command manually");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = async (provider: OAuthProvider) => {
|
||||
if (!confirm(`${t.oauth.disconnect} ${provider.name}?`)) {
|
||||
return;
|
||||
|
|
@ -206,14 +194,11 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) {
|
|||
</Button>
|
||||
)}
|
||||
{!p.status.logged_in && (
|
||||
<Button
|
||||
outlined
|
||||
onClick={() => handleCopy(p)}
|
||||
title={t.oauth.copyCliCommand}
|
||||
prefix={copiedId === p.id ? undefined : <Copy />}
|
||||
>
|
||||
{copiedId === p.id ? t.oauth.copied : t.oauth.cli}
|
||||
</Button>
|
||||
<CopyButton
|
||||
text={p.cli_command}
|
||||
label={t.oauth.cli}
|
||||
copiedLabel={t.oauth.copied}
|
||||
/>
|
||||
)}
|
||||
{p.status.logged_in && p.flow !== "external" && (
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@ import {
|
|||
BarChart3,
|
||||
Brain,
|
||||
Cpu,
|
||||
Hash,
|
||||
RefreshCw,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import { api } from "@/lib/api";
|
||||
import type { AnalyticsResponse, AnalyticsDailyEntry, AnalyticsModelEntry, AnalyticsSkillEntry } from "@/lib/api";
|
||||
import { timeAgo } from "@/lib/utils";
|
||||
import { Button } from "@nous-research/ui";
|
||||
import { Button, Stats } from "@nous-research/ui";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { usePageHeader } from "@/contexts/usePageHeader";
|
||||
|
|
@ -40,31 +39,6 @@ function formatDate(day: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
function SummaryCard({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
}: {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
value: string;
|
||||
sub?: string;
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">{label}</CardTitle>
|
||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
{sub && <p className="text-xs text-muted-foreground mt-1">{sub}</p>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function TokenBarChart({ daily }: { daily: AnalyticsDailyEntry[] }) {
|
||||
const { t } = useI18n();
|
||||
if (daily.length === 0) return null;
|
||||
|
|
@ -364,30 +338,44 @@ export default function AnalyticsPage() {
|
|||
|
||||
{data && (
|
||||
<>
|
||||
{/* Summary cards */}
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<SummaryCard
|
||||
icon={Hash}
|
||||
label={t.analytics.totalTokens}
|
||||
value={formatTokens(data.totals.total_input + data.totals.total_output)}
|
||||
sub={t.analytics.inOut.replace("{input}", formatTokens(data.totals.total_input)).replace("{output}", formatTokens(data.totals.total_output))}
|
||||
/>
|
||||
<SummaryCard
|
||||
icon={BarChart3}
|
||||
label={t.analytics.totalSessions}
|
||||
value={String(data.totals.total_sessions)}
|
||||
sub={`~${(data.totals.total_sessions / days).toFixed(1)}${t.analytics.perDayAvg}`}
|
||||
/>
|
||||
<SummaryCard
|
||||
icon={TrendingUp}
|
||||
label={t.analytics.apiCalls}
|
||||
value={String(data.totals.total_api_calls ?? data.daily.reduce((sum, d) => sum + d.sessions, 0))}
|
||||
sub={t.analytics.acrossModels.replace("{count}", String(data.by_model.length))}
|
||||
/>
|
||||
</div>
|
||||
{/* Summary stats + bar chart side-by-side on lg+ */}
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardContent className="py-6">
|
||||
<Stats
|
||||
items={[
|
||||
{
|
||||
label: t.analytics.totalTokens,
|
||||
value: formatTokens(
|
||||
data.totals.total_input + data.totals.total_output,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: t.analytics.input,
|
||||
value: formatTokens(data.totals.total_input),
|
||||
},
|
||||
{
|
||||
label: t.analytics.output,
|
||||
value: formatTokens(data.totals.total_output),
|
||||
},
|
||||
{
|
||||
label: t.analytics.totalSessions,
|
||||
value: `${data.totals.total_sessions} (~${(data.totals.total_sessions / days).toFixed(1)}${t.analytics.perDayAvg})`,
|
||||
},
|
||||
{
|
||||
label: t.analytics.apiCalls,
|
||||
value: String(
|
||||
data.totals.total_api_calls ??
|
||||
data.daily.reduce((sum, d) => sum + d.sessions, 0),
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Bar chart */}
|
||||
<TokenBarChart daily={data.daily} />
|
||||
<TokenBarChart daily={data.daily} />
|
||||
</div>
|
||||
|
||||
{/* Tables */}
|
||||
<DailyTable daily={data.daily} />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue