SPB Git forge

spb/llm-api

Public
0commits 0branches 0releases
0 Bsize
maindefault branch
—last push
4.4 KB · 90 lines tsx
Raw Blame History
1"use client";23import { useEffect, useState } from "react";4import { api, ApiError } from "@/lib/api";5import { fmtAgo, fmtDate, fmtNum } from "@/lib/format";6import type { ApiKey } from "@/lib/types";7import { Code, Modal, PageHeader, Pill, useToast } from "@/components/ui";89export default function KeysPage() {10  const toast = useToast();11  const [keys, setKeys] = useState<ApiKey[]>([]);12  const [name, setName] = useState("");13  const [admin, setAdmin] = useState(false);14  const [created, setCreated] = useState<string | null>(null);15  const [busy, setBusy] = useState(false);1617  const load = () => api.get<{ keys: ApiKey[] }>("/api/keys").then((r) => setKeys(r.keys)).catch(() => {});18  useEffect(() => { load(); }, []);1920  const create = async (e: React.FormEvent) => {21    e.preventDefault();22    setBusy(true);23    try {24      const r = await api.post<{ key: string }>("/api/keys", { name: name.trim() || "key", scopes: admin ? ["inference", "admin"] : ["inference"] });25      setCreated(r.key);26      setName("");27      setAdmin(false);28      load();29    } catch (err) {30      toast.push(err instanceof ApiError ? err.message : "Failed", "bad");31    } finally {32      setBusy(false);33    }34  };35  const revoke = async (k: ApiKey) => {36    if (!confirm(`Revoke "${k.name}"? Clients using it will stop working.`)) return;37    await api.del(`/api/keys/${k.id}`);38    load();39  };40  const rename = async (k: ApiKey) => {41    const n = prompt("New name", k.name);42    if (!n) return;43    await api.patch(`/api/keys/${k.id}`, { name: n });44    load();45  };46  const origin = typeof location !== "undefined" ? location.origin : "https://www.llm-api.io";4748  return (49    <div>50      {toast.view}51      <PageHeader title="API keys" sub="Keys are shown once at creation and stored hashed. Use them as Bearer tokens with any OpenAI SDK." />52      <form onSubmit={create} className="card p-4 mb-5 flex flex-col sm:flex-row gap-2 sm:items-center">53        <input className="input sm:max-w-xs" placeholder="Key name (e.g. laptop, cursor, n8n)" value={name} onChange={(e) => setName(e.target.value)} />54        <label className="text-sm flex items-center gap-2 text-ink-2"><input type="checkbox" checked={admin} onChange={(e) => setAdmin(e.target.checked)} /> admin scope (management API)</label>55        <button className="btn btn-primary sm:ml-auto" disabled={busy}>Create key</button>56      </form>57      <div className="card overflow-x-auto">58        <table className="tbl">59          <thead><tr><th>Name</th><th>Prefix</th><th>Scopes</th><th>Created</th><th>Last used</th><th className="text-right">Requests</th><th>Status</th><th></th></tr></thead>60          <tbody>61            {keys.map((k) => (62              <tr key={k.id} className={k.revoked_at ? "opacity-50" : ""}>63                <td className="font-medium">{k.name}</td>64                <td className="mono text-xs">{k.prefix}…</td>65                <td className="flex gap-1">{k.scopes.map((s) => <Pill key={s} tone={s === "admin" ? "violet" : "neutral"}>{s}</Pill>)}</td>66                <td className="text-ink-2">{fmtDate(k.created_at)}</td>67                <td className="text-ink-2">{fmtAgo(k.last_used_at)}</td>68                <td className="text-right num">{fmtNum(k.request_count)}</td>69                <td>{k.revoked_at ? <Pill tone="bad">revoked</Pill> : <Pill tone="good">active</Pill>}</td>70                <td className="text-right whitespace-nowrap">{!k.revoked_at && <><button className="btn btn-sm btn-ghost" onClick={() => rename(k)}>Rename</button> <button className="btn btn-sm btn-danger" onClick={() => revoke(k)}>Revoke</button></>}</td>71              </tr>72            ))}73            {!keys.length && <tr><td colSpan={8} className="text-center text-ink-3 py-8">No keys yet.</td></tr>}74          </tbody>75        </table>76      </div>77      <Modal open={!!created} onClose={() => setCreated(null)} title="Your new API key" width={640}>78        <p className="text-sm mb-3">Copy it now — it will not be shown again.</p>79        <Code>{created || ""}</Code>80        <p className="text-sm mt-4 mb-2 text-ink-2">Quick start:</p>81        <Code>{`from openai import OpenAI82client = OpenAI(base_url="${origin}/v1", api_key="${created}")83r = client.chat.completions.create(model="default", messages=[{"role": "user", "content": "Hello"}])84print(r.choices[0].message.content)`}</Code>85        <div className="flex justify-end mt-4"><button className="btn btn-primary" onClick={() => setCreated(null)}>Done</button></div>86      </Modal>87    </div>88  );89}90