kioai / artifacts /image-gen /src /pages /ApiKeys.tsx
kinaiok
Initial deployment setup for Hugging Face Spaces
5ef6e9d
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Key, Plus, Trash2, Copy, Check, Code2, Sparkles } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { useLang } from "@/contexts/LanguageContext";
import { useAuth } from "@/contexts/AuthContext";
import { AuthModal } from "@/components/AuthModal";
const BASE = import.meta.env.BASE_URL.replace(/\/$/, "");
const API_BASE = `${BASE}/api`;
const V1_BASE = window.location.origin + `${BASE}/api/v1`;
interface ApiKeyRecord {
id: number;
name: string;
keyPrefix: string;
createdAt: string;
lastUsedAt: string | null;
}
async function fetchKeys(): Promise<ApiKeyRecord[]> {
const res = await fetch(`${API_BASE}/apikeys`, { credentials: "include" });
if (!res.ok) throw new Error("Failed to fetch keys");
const data = await res.json();
return data.keys;
}
async function createKey(name: string): Promise<{ key: string; id: number; name: string; keyPrefix: string; createdAt: string }> {
const res = await fetch(`${API_BASE}/apikeys`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
});
if (!res.ok) throw new Error("Failed to create key");
return res.json();
}
async function revokeKey(id: number): Promise<void> {
const res = await fetch(`${API_BASE}/apikeys/${id}`, { method: "DELETE", credentials: "include" });
if (!res.ok) throw new Error("Failed to revoke key");
}
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const { t } = useLang();
return (
<button
onClick={async () => {
await navigator.clipboard.writeText(text).catch(() => {});
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}}
className="flex items-center gap-1 text-xs px-2 py-1 rounded border border-border/50 hover:bg-secondary transition-colors text-muted-foreground hover:text-foreground"
>
{copied ? <Check className="w-3 h-3 text-green-400" /> : <Copy className="w-3 h-3" />}
{copied ? t.apiKeysCopied : t.apiKeysCopy}
</button>
);
}
function KeyCard({ apiKey, onRevoke, isRevoking }: { apiKey: ApiKeyRecord; onRevoke: (id: number) => void; isRevoking: boolean }) {
const { t } = useLang();
const created = new Date(apiKey.createdAt).toLocaleDateString();
const lastUsed = apiKey.lastUsedAt ? new Date(apiKey.lastUsedAt).toLocaleDateString() : t.apiKeysNeverUsed;
return (
<div className="flex items-center justify-between p-4 rounded-xl border border-border/60 bg-card/50 hover:bg-card/80 transition-colors group">
<div className="flex items-center gap-3 min-w-0">
<div className="w-8 h-8 rounded-lg bg-primary/10 border border-primary/20 flex items-center justify-center flex-shrink-0">
<Key className="w-4 h-4 text-primary" />
</div>
<div className="min-w-0">
<div className="font-medium text-sm text-foreground">{apiKey.name}</div>
<div className="text-xs text-muted-foreground font-mono mt-0.5">{apiKey.keyPrefix}</div>
<div className="flex gap-3 mt-1 text-xs text-muted-foreground/70">
<span>{t.apiKeysCreatedAt}: {created}</span>
<span>{t.apiKeysLastUsed}: {lastUsed}</span>
</div>
</div>
</div>
<button
onClick={() => onRevoke(apiKey.id)}
disabled={isRevoking}
className="flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg border border-destructive/30 text-destructive/70 hover:bg-destructive/10 hover:text-destructive hover:border-destructive/60 transition-colors opacity-0 group-hover:opacity-100 disabled:opacity-50"
>
<Trash2 className="w-3 h-3" />
{isRevoking ? t.apiKeysRevoking : t.apiKeysRevoke}
</button>
</div>
);
}
function NewKeyBanner({ rawKey }: { rawKey: string }) {
const { t } = useLang();
return (
<div className="rounded-xl border border-yellow-500/40 bg-yellow-500/5 p-4 space-y-2">
<div className="flex items-center gap-2 text-yellow-400 text-sm font-medium">
<Check className="w-4 h-4" />
{t.apiKeysCopyHint}
</div>
<div className="flex items-center gap-2">
<code className="flex-1 text-xs font-mono bg-black/40 rounded-lg px-3 py-2 text-green-300 overflow-x-auto whitespace-nowrap">
{rawKey}
</code>
<CopyButton text={rawKey} />
</div>
</div>
);
}
function KeysContent() {
const { t } = useLang();
const qc = useQueryClient();
const [name, setName] = useState("");
const [newRawKey, setNewRawKey] = useState<string | null>(null);
const { data: keys = [], isLoading } = useQuery({ queryKey: ["apikeys"], queryFn: fetchKeys });
const createMutation = useMutation({
mutationFn: createKey,
onSuccess: (data) => {
setNewRawKey(data.key);
setName("");
qc.invalidateQueries({ queryKey: ["apikeys"] });
},
});
const revokeMutation = useMutation({
mutationFn: revokeKey,
onSuccess: () => {
setNewRawKey(null);
qc.invalidateQueries({ queryKey: ["apikeys"] });
},
});
const handleCreate = (e: React.FormEvent) => {
e.preventDefault();
createMutation.mutate(name.trim() || "Default Key");
};
return (
<div className="space-y-6">
{newRawKey && <NewKeyBanner rawKey={newRawKey} />}
<form onSubmit={handleCreate} className="flex gap-2">
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t.apiKeysCreateNamePlaceholder}
className="flex-1 bg-background/50 border-border/60"
maxLength={64}
/>
<Button type="submit" disabled={createMutation.isPending} className="gap-2">
<Plus className="w-4 h-4" />
{createMutation.isPending ? t.apiKeysCreating : t.apiKeysCreate}
</Button>
</form>
{isLoading ? (
<div className="text-center py-8 text-muted-foreground text-sm">Loading...</div>
) : keys.length === 0 ? (
<div className="text-center py-12 space-y-2">
<Key className="w-10 h-10 mx-auto text-muted-foreground/40" />
<div className="text-muted-foreground">{t.apiKeysEmpty}</div>
<div className="text-xs text-muted-foreground/60">{t.apiKeysEmptyHint}</div>
</div>
) : (
<div className="space-y-2">
{keys.map((k) => (
<KeyCard key={k.id} apiKey={k} onRevoke={(id) => revokeMutation.mutate(id)} isRevoking={revokeMutation.isPending} />
))}
</div>
)}
</div>
);
}
function DocsSection() {
const { t } = useLang();
const exampleCode = `from openai import OpenAI
client = OpenAI(
api_key="sk-sf-...",
base_url="${V1_BASE}",
)
response = client.images.generate(
model="grok",
prompt="a glowing neon cityscape at night",
n=1,
size="1024x1024",
)
print(response.data[0].url)`;
return (
<div className="space-y-4 rounded-xl border border-border/60 bg-card/30 p-6">
<h2 className="text-base font-semibold text-foreground flex items-center gap-2">
<Code2 className="w-4 h-4 text-primary" />
{t.apiKeysDocsTitle}
</h2>
<div>
<div className="text-xs text-muted-foreground mb-1">{t.apiKeysBaseUrl}</div>
<code className="block text-xs font-mono bg-black/40 rounded-lg px-3 py-2 text-green-300">{V1_BASE}</code>
</div>
<div>
<div className="text-xs text-muted-foreground mb-2">{t.apiKeysEndpointsTitle}</div>
<div className="space-y-1.5">
{[
{ method: "GET", path: "/models", desc: t.apiKeysModelsDesc },
{ method: "POST", path: "/images/generations", desc: t.apiKeysGenerateDesc },
].map(({ method, path, desc }) => (
<div key={path} className="flex items-center gap-3 text-xs p-2 rounded-lg bg-black/20">
<Badge variant="outline" className="font-mono text-[10px] px-1.5 py-0 border-primary/40 text-primary">{method}</Badge>
<code className="font-mono text-green-300">{path}</code>
<span className="text-muted-foreground ml-auto">{desc}</span>
</div>
))}
</div>
</div>
<div>
<div className="text-xs text-muted-foreground mb-1">{t.apiKeysExampleTitle} (Python)</div>
<pre className="text-xs font-mono bg-black/40 rounded-lg px-4 py-3 text-emerald-300 overflow-x-auto leading-5 whitespace-pre">{exampleCode}</pre>
</div>
</div>
);
}
export function ApiKeys() {
const { t } = useLang();
const { isSignedIn, isLoaded } = useAuth();
const [authOpen, setAuthOpen] = useState(false);
return (
<div className="container mx-auto px-4 py-8 max-w-3xl space-y-8">
<div>
<h1 className="text-2xl font-bold text-foreground flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-primary/15 border border-primary/30 flex items-center justify-center">
<Sparkles className="w-5 h-5 text-primary" />
</div>
{t.apiKeysTitle}
</h1>
<p className="text-muted-foreground text-sm mt-1">{t.apiKeysSubtitle}</p>
</div>
{isLoaded && isSignedIn && <KeysContent />}
{isLoaded && !isSignedIn && (
<div className="text-center py-16 space-y-4">
<Key className="w-12 h-12 mx-auto text-muted-foreground/30" />
<p className="text-muted-foreground">{t.apiKeysSignInRequired}</p>
<Button onClick={() => setAuthOpen(true)}>{t.apiKeysSignIn}</Button>
</div>
)}
<DocsSection />
<AuthModal open={authOpen} onOpenChange={setAuthOpen} />
</div>
);
}