ai-agents-client.tsx145 lines · main
| 1 | 'use client'; |
| 2 | |
| 3 | import { useCallback, useEffect, useState } from 'react'; |
| 4 | |
| 5 | type AgentRow = { |
| 6 | id: string; |
| 7 | agentName: string; |
| 8 | scopes: string[]; |
| 9 | hint: string; |
| 10 | expiresAt: string | null; |
| 11 | revokedAt: string | null; |
| 12 | }; |
| 13 | |
| 14 | export function AuthAiAgentsClient({ projectId }: { projectId: string }) { |
| 15 | const [items, setItems] = useState<AgentRow[]>([]); |
| 16 | const [name, setName] = useState('support-bot'); |
| 17 | const [plaintext, setPlaintext] = useState<string | null>(null); |
| 18 | const [err, setErr] = useState<string | null>(null); |
| 19 | const [pending, setPending] = useState(false); |
| 20 | |
| 21 | const load = useCallback(async () => { |
| 22 | if (!projectId) return; |
| 23 | setErr(null); |
| 24 | const res = await fetch( |
| 25 | `/api/v1/auth-core/projects/${encodeURIComponent(projectId)}/ai/agents`, |
| 26 | { credentials: 'include', cache: 'no-store' }, |
| 27 | ); |
| 28 | if (!res.ok) { |
| 29 | setErr(res.status === 401 ? 'sign in required' : `load failed (${res.status})`); |
| 30 | return; |
| 31 | } |
| 32 | const body = (await res.json()) as { agents?: AgentRow[] }; |
| 33 | setItems(body.agents ?? []); |
| 34 | }, [projectId]); |
| 35 | |
| 36 | useEffect(() => { |
| 37 | void load(); |
| 38 | }, [load]); |
| 39 | |
| 40 | async function create(): Promise<void> { |
| 41 | setPending(true); |
| 42 | setErr(null); |
| 43 | setPlaintext(null); |
| 44 | try { |
| 45 | const res = await fetch( |
| 46 | `/api/v1/auth-core/projects/${encodeURIComponent(projectId)}/ai/agents`, |
| 47 | { |
| 48 | method: 'POST', |
| 49 | credentials: 'include', |
| 50 | headers: { 'content-type': 'application/json' }, |
| 51 | body: JSON.stringify({ agentName: name || 'agent', ttlHours: 24 }), |
| 52 | }, |
| 53 | ); |
| 54 | const body = (await res.json().catch(() => ({}))) as { |
| 55 | plaintext?: string; |
| 56 | message?: string; |
| 57 | }; |
| 58 | if (!res.ok) throw new Error(body.message ?? `http ${res.status}`); |
| 59 | setPlaintext(body.plaintext ?? null); |
| 60 | await load(); |
| 61 | } catch (e) { |
| 62 | setErr(e instanceof Error ? e.message : 'create failed'); |
| 63 | } finally { |
| 64 | setPending(false); |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | async function revoke(id: string): Promise<void> { |
| 69 | setPending(true); |
| 70 | try { |
| 71 | await fetch( |
| 72 | `/api/v1/auth-core/projects/${encodeURIComponent(projectId)}/ai/agents/${encodeURIComponent(id)}`, |
| 73 | { method: 'DELETE', credentials: 'include' }, |
| 74 | ); |
| 75 | await load(); |
| 76 | } finally { |
| 77 | setPending(false); |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | return ( |
| 82 | <div className="flex max-w-xl flex-col gap-4"> |
| 83 | <p className="font-mono text-xs text-[var(--color-text-muted)] leading-relaxed"> |
| 84 | Tokens for AI agents and tools (not humans). Call{' '} |
| 85 | <code className="text-[var(--color-text)]">GET /v1/auth-core/ai/me</code> with{' '} |
| 86 | <code className="text-[var(--color-text)]">Authorization: Bearer brai_…</code>. |
| 87 | </p> |
| 88 | <div className="flex flex-wrap items-end gap-2"> |
| 89 | <label className="flex flex-col gap-1 font-mono text-xs"> |
| 90 | <span className="text-[var(--color-text-muted)]">agent name</span> |
| 91 | <input |
| 92 | value={name} |
| 93 | onChange={(e) => setName(e.target.value)} |
| 94 | className="rounded-md border bg-[var(--color-surface)] px-3 py-2" |
| 95 | style={{ borderColor: 'var(--auth-accent-border)' }} |
| 96 | /> |
| 97 | </label> |
| 98 | <button |
| 99 | type="button" |
| 100 | disabled={pending} |
| 101 | onClick={() => void create()} |
| 102 | className="rounded-md px-3 py-2 font-mono text-xs font-medium text-black disabled:opacity-50" |
| 103 | style={{ background: '#FFFD74' }} |
| 104 | > |
| 105 | {pending ? 'creating…' : 'create agent token'} |
| 106 | </button> |
| 107 | </div> |
| 108 | {plaintext ? ( |
| 109 | <div |
| 110 | className="rounded-md border p-3 font-mono text-xs" |
| 111 | style={{ borderColor: 'var(--auth-accent-border)' }} |
| 112 | > |
| 113 | <p className="text-[var(--color-text-muted)]">copy once:</p> |
| 114 | <code className="mt-1 block break-all text-[var(--color-text)]"> |
| 115 | {plaintext} |
| 116 | </code> |
| 117 | </div> |
| 118 | ) : null} |
| 119 | <ul className="flex flex-col gap-2"> |
| 120 | {items.map((a) => ( |
| 121 | <li |
| 122 | key={a.id} |
| 123 | className="flex justify-between gap-2 rounded-md border px-3 py-2 font-mono text-xs" |
| 124 | style={{ borderColor: 'var(--auth-accent-border)' }} |
| 125 | > |
| 126 | <span className="text-[var(--color-text)]"> |
| 127 | {a.agentName} · {a.hint} |
| 128 | {a.revokedAt ? ' · revoked' : ''} |
| 129 | </span> |
| 130 | {!a.revokedAt ? ( |
| 131 | <button |
| 132 | type="button" |
| 133 | className="underline text-[var(--color-text-muted)]" |
| 134 | onClick={() => void revoke(a.id)} |
| 135 | > |
| 136 | revoke |
| 137 | </button> |
| 138 | ) : null} |
| 139 | </li> |
| 140 | ))} |
| 141 | </ul> |
| 142 | {err ? <p className="font-mono text-xs text-red-400">{err}</p> : null} |
| 143 | </div> |
| 144 | ); |
| 145 | } |