The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit 5ab277316767Commit detailsView latest

apps/dashboard/src/main.tsx 2132 lines · 69.7 KB · tsx
1
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
import { createRoot } from 'react-dom/client';
3
import {
4
  applyPreviewRequest,
5
  createPreviewData,
6
  defaultPreviewSettings,
7
  disablePreview,
8
  enablePreview,
9
  isPreviewActive,
10
  previewSession,
11
} from './preview.js';
12
import './styles.css';
13
14
declare const __AUTH0_DOMAIN__: string;
15
declare const __AUTH0_CLIENT_ID__: string;
16
declare const __AUTH0_AUDIENCE__: string;
17
18
type Auth0User = { sub: string; name?: string; email?: string; picture?: string };
19
type AuthSession = { user: Auth0User };
20
type AuthTransaction = { state: string; nonce: string; verifier: string };
21
type Theme = 'dark' | 'light';
22
23
const authTransactionKey = 'openclasp.auth0.transaction';
24
const themeKey = 'openclasp.theme.v1';
25
26
type DashboardData = {
27
  agents: Record<string, any>[];
28
  projects: Record<string, any>[];
29
  installations: Record<string, any>[];
30
  setupRequests: Record<string, any>[];
31
  publications: Record<string, any>[];
32
  interactions: Record<string, any>[];
33
  federatedInteractions: Record<string, any>[];
34
  liveSessions: Record<string, any>[];
35
  hostedThreads: Record<string, any>[];
36
  events: Record<string, any>[];
37
  conflicts: Record<string, any>[];
38
  receipts: Record<string, any>[];
39
  profiles: Record<string, any>[];
40
  runtimes: Record<string, any>[];
41
};
42
43
type Settings = {
44
  displayName: string;
45
  contributionEnabled: boolean;
46
  retentionDays: number;
47
  evidenceSharing: 'never' | 'ask' | 'contract_only';
48
  rawConversationsStored: false;
49
};
50
51
const emptyData: DashboardData = {
52
  agents: [],
53
  projects: [],
54
  installations: [],
55
  setupRequests: [],
56
  publications: [],
57
  interactions: [],
58
  federatedInteractions: [],
59
  liveSessions: [],
60
  hostedThreads: [],
61
  events: [],
62
  conflicts: [],
63
  receipts: [],
64
  profiles: [],
65
  runtimes: [],
66
};
67
const defaultSettings: Settings = {
68
  displayName: '',
69
  contributionEnabled: false,
70
  retentionDays: 30,
71
  evidenceSharing: 'ask',
72
  rawConversationsStored: false,
73
};
74
const pages = [
75
  'dashboard',
76
  'conversations',
77
  'history',
78
  'agents',
79
  'insights',
80
  'connect',
81
  'settings',
82
] as const;
83
type Page = (typeof pages)[number];
84
const pageMeta: Record<Page, { label: string; title: string; lede: string; eyebrow: string }> = {
85
  dashboard: {
86
    label: 'Overview',
87
    title: 'Overview',
88
    lede: 'Agent readiness, signed outcomes, and anything waiting on you.',
89
    eyebrow: 'Network',
90
  },
91
  history: {
92
    label: 'History',
93
    title: 'History',
94
    lede: 'Structured events, receipts, and shared contracts — never raw messages.',
95
    eyebrow: 'Audit',
96
  },
97
  conversations: {
98
    label: 'Conversations',
99
    title: 'Temporary conversations',
100
    lede: 'Hosted history for temporary chat identities. Direct runtime messages never appear here.',
101
    eyebrow: 'Inbox',
102
  },
103
  agents: {
104
    label: 'Agents',
105
    title: 'Agents',
106
    lede: 'Identities, runtimes, and the automation each agent is allowed to do.',
107
    eyebrow: 'Registry',
108
  },
109
  insights: {
110
    label: 'Insights',
111
    title: 'Insights',
112
    lede: 'Task-specific reliability from signed outcomes. No universal score.',
113
    eyebrow: 'Context',
114
  },
115
  connect: {
116
    label: 'Connect',
117
    title: 'Connect',
118
    lede: 'Add an agent once. Approve its identity, then OpenClasp handles the routine work.',
119
    eyebrow: 'Setup',
120
  },
121
  settings: {
122
    label: 'Settings',
123
    title: 'Settings',
124
    lede: 'Privacy, retention, and what this account may contribute to the network.',
125
    eyebrow: 'Account',
126
  },
127
};
128
129
function base64Url(bytes: Uint8Array) {
130
  let binary = '';
131
  for (const byte of bytes) binary += String.fromCharCode(byte);
132
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
133
}
134
135
function randomValue(size = 32) {
136
  return base64Url(crypto.getRandomValues(new Uint8Array(size)));
137
}
138
139
async function beginAuth(provider: 'google' | 'github') {
140
  const verifier = randomValue(48);
141
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
142
  const transaction: AuthTransaction = { state: randomValue(), nonce: randomValue(), verifier };
143
  sessionStorage.setItem(authTransactionKey, JSON.stringify(transaction));
144
  const parameters = new URLSearchParams({
145
    client_id: __AUTH0_CLIENT_ID__,
146
    redirect_uri: `${location.origin}/sso-callback`,
147
    response_type: 'code',
148
    scope: 'openid profile email',
149
    audience: __AUTH0_AUDIENCE__,
150
    state: transaction.state,
151
    nonce: transaction.nonce,
152
    code_challenge: base64Url(new Uint8Array(digest)),
153
    code_challenge_method: 'S256',
154
    connection: provider === 'google' ? 'google-oauth2' : 'github',
155
  });
156
  location.assign(`https://${__AUTH0_DOMAIN__}/authorize?${parameters}`);
157
}
158
159
async function signOut(preview: boolean) {
160
  if (preview) {
161
    disablePreview();
162
    location.replace('/login');
163
    return;
164
  }
165
  await fetch('/api/session', { method: 'DELETE', credentials: 'same-origin' });
166
  const parameters = new URLSearchParams({
167
    client_id: __AUTH0_CLIENT_ID__,
168
    returnTo: `${location.origin}/login`,
169
  });
170
  location.assign(`https://${__AUTH0_DOMAIN__}/v2/logout?${parameters}`);
171
}
172
173
async function remoteApi(path: string, init?: RequestInit) {
174
  const headers = new Headers(init?.headers);
175
  if (init?.body && !headers.has('content-type')) headers.set('content-type', 'application/json');
176
  return fetch(path, {
177
    ...init,
178
    credentials: 'same-origin',
179
    headers,
180
  }).then(async (response) => {
181
    if (!response.ok)
182
      throw new Error((await response.text()) || `Request failed: ${response.status}`);
183
    return response.json();
184
  });
185
}
186
187
function route(): Page {
188
  const value = location.pathname.slice(1) || 'dashboard';
189
  return pages.includes(value as Page) ? (value as Page) : 'dashboard';
190
}
191
192
function initialTheme(): Theme {
193
  const saved = localStorage.getItem(themeKey);
194
  if (saved === 'dark' || saved === 'light') return saved;
195
  return matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
196
}
197
198
function App() {
199
  const [session, setSession] = useState<AuthSession | null>();
200
  const [preview, setPreview] = useState(false);
201
  const [theme, setTheme] = useState<Theme>(initialTheme);
202
  const [page, setPage] = useState<Page>(route());
203
  const [data, setData] = useState<DashboardData>(emptyData);
204
  const [settings, setSettings] = useState<Settings>(defaultSettings);
205
  const [loading, setLoading] = useState(true);
206
  const [error, setError] = useState('');
207
  const api = useCallback(
208
    async (path: string, init?: RequestInit) => {
209
      if (preview) {
210
        const next = applyPreviewRequest(data, settings, path, init);
211
        setData(next.data);
212
        setSettings(next.settings);
213
        return next.result;
214
      }
215
      return remoteApi(path, init);
216
    },
217
    [data, preview, settings],
218
  );
219
  const refreshDashboard = useCallback(async () => {
220
    if (preview) return;
221
    setData((await remoteApi('/v0.1/dashboard')) as DashboardData);
222
  }, [preview]);
223
224
  useEffect(() => {
225
    document.title = session
226
      ? `${pageMeta[page].label} · OpenClasp`
227
      : session === null
228
        ? 'Sign in · OpenClasp'
229
        : 'OpenClasp';
230
  }, [page, session]);
231
232
  useEffect(() => {
233
    let cancelled = false;
234
    const boot = async () => {
235
      if (import.meta.env.DEV && isPreviewActive()) {
236
        if (cancelled) return;
237
        setPreview(true);
238
        setSession(previewSession);
239
        setData(createPreviewData());
240
        setSettings(defaultPreviewSettings);
241
        setLoading(false);
242
        return;
243
      }
244
      try {
245
        const response = await fetch('/api/session', { credentials: 'same-origin' });
246
        if (!response.ok) {
247
          if (!cancelled && !(import.meta.env.DEV && isPreviewActive())) setSession(null);
248
          return;
249
        }
250
        if (!cancelled && !(import.meta.env.DEV && isPreviewActive())) {
251
          setSession((await response.json()) as AuthSession);
252
        }
253
      } catch {
254
        if (!cancelled && !(import.meta.env.DEV && isPreviewActive())) setSession(null);
255
      }
256
    };
257
    void boot();
258
    return () => {
259
      cancelled = true;
260
    };
261
  }, []);
262
263
  useEffect(() => {
264
    document.documentElement.dataset.theme = theme;
265
    localStorage.setItem(themeKey, theme);
266
    const meta = document.querySelector('meta[name="theme-color"]');
267
    if (meta) meta.setAttribute('content', theme === 'light' ? '#f4f6f1' : '#0b0d0b');
268
  }, [theme]);
269
270
  useEffect(() => {
271
    const onPopState = () => setPage(route());
272
    addEventListener('popstate', onPopState);
273
    return () => removeEventListener('popstate', onPopState);
274
  }, []);
275
276
  useEffect(() => {
277
    if (session === undefined) return;
278
    if (!session && location.pathname !== '/login') history.replaceState({}, '', '/login');
279
    if (!session) return;
280
    if (location.pathname === '/login') history.replaceState({}, '', '/dashboard');
281
    setPage(route());
282
    if (preview) {
283
      setLoading(false);
284
      return;
285
    }
286
    Promise.all([remoteApi('/v0.1/dashboard'), remoteApi('/v0.1/settings')])
287
      .then(([dashboard, accountSettings]) => {
288
        setData(dashboard as DashboardData);
289
        setSettings(accountSettings as Settings);
290
      })
291
      .catch((reason: unknown) =>
292
        setError(reason instanceof Error ? reason.message : 'Load failed'),
293
      )
294
      .finally(() => setLoading(false));
295
  }, [preview, session]);
296
297
  useEffect(() => {
298
    if (!session || preview) return;
299
    const refresh = () => {
300
      if (document.visibilityState === 'visible') void refreshDashboard().catch(() => undefined);
301
    };
302
    const timer = window.setInterval(refresh, 10_000);
303
    document.addEventListener('visibilitychange', refresh);
304
    return () => {
305
      window.clearInterval(timer);
306
      document.removeEventListener('visibilitychange', refresh);
307
    };
308
  }, [preview, refreshDashboard, session]);
309
310
  const navigate = (next: Page) => {
311
    history.pushState({}, '', `/${next}`);
312
    setPage(next);
313
  };
314
  const pendingSetup = data.setupRequests.filter((request) => request.status === 'pending').length;
315
  const pendingInvites = data.federatedInteractions.filter(
316
    (interaction) => interaction.status === 'pending',
317
  ).length;
318
  const unreadThreads = data.hostedThreads.reduce(
319
    (total, thread) => total + Number(thread.unreadCount ?? 0),
320
    0,
321
  );
322
323
  if (session === undefined) return <Loading />;
324
  if (!session)
325
    return import.meta.env.DEV ? (
326
      <Login
327
        onPreview={() => {
328
          enablePreview();
329
          setPreview(true);
330
          setSession(previewSession);
331
          setData(createPreviewData());
332
          setSettings(defaultPreviewSettings);
333
          setLoading(false);
334
          history.replaceState({}, '', '/dashboard');
335
          setPage('dashboard');
336
        }}
337
      />
338
    ) : (
339
      <Login />
340
    );
341
342
  return (
343
    <div className="appShell">
344
      <a className="skipLink" href="#main">
345
        Skip to content
346
      </a>
347
      <aside>
348
        <div className="brand">
349
          <div className="mark">OC</div>
350
          <div>
351
            <strong>OpenClasp</strong>
352
            <small>ASSURANCE NETWORK</small>
353
          </div>
354
        </div>
355
        <nav aria-label="Dashboard">
356
          <Nav
357
            page="dashboard"
358
            active={page}
359
            onClick={navigate}
360
            label="Overview"
361
            icon="home"
362
            badge={pendingInvites || pendingSetup ? pendingInvites + pendingSetup : 0}
363
          />
364
          <Nav page="history" active={page} onClick={navigate} label="History" icon="history" />
365
          <Nav
366
            page="conversations"
367
            active={page}
368
            onClick={navigate}
369
            label="Conversations"
370
            icon="connect"
371
            badge={unreadThreads}
372
          />
373
          <Nav page="agents" active={page} onClick={navigate} label="Agents" icon="agents" />
374
          <Nav page="insights" active={page} onClick={navigate} label="Insights" icon="insights" />
375
          <Nav
376
            page="connect"
377
            active={page}
378
            onClick={navigate}
379
            label="Connect"
380
            icon="connect"
381
            badge={pendingSetup}
382
          />
383
          <Nav page="settings" active={page} onClick={navigate} label="Settings" icon="settings" />
384
        </nav>
385
        <button
386
          className="themeSwitch"
387
          type="button"
388
          onClick={() => setTheme((current) => (current === 'dark' ? 'light' : 'dark'))}
389
          aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
390
        >
391
          <Icon name={theme === 'dark' ? 'sun' : 'moon'} />
392
          {theme === 'dark' ? 'Light theme' : 'Dark theme'}
393
        </button>
394
        <div className="privacyStamp">
395
          <span className="liveDot" />
396
          <div>
397
            <strong>Privacy separated</strong>
398
            <small>Direct messages never stored</small>
399
          </div>
400
        </div>
401
        <button
402
          className="account"
403
          type="button"
404
          onClick={() => void signOut(preview)}
405
          title="Sign out"
406
        >
407
          <span>{initials(session.user.name || session.user.email || 'OC')}</span>
408
          <div>
409
            <strong>{session.user.name || 'OpenClasp user'}</strong>
410
            <small>{session.user.email || 'Signed in'}</small>
411
          </div>
412
          <b>Sign out</b>
413
        </button>
414
      </aside>
415
      <div>
416
        <div className="mobileBar">
417
          <div className="brand">
418
            <div className="mark">OC</div>
419
            <div>
420
              <strong>OpenClasp</strong>
421
              <small>ASSURANCE</small>
422
            </div>
423
          </div>
424
          <div className="mobileActions">
425
            <button
426
              className="iconButton"
427
              type="button"
428
              aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
429
              onClick={() => setTheme((current) => (current === 'dark' ? 'light' : 'dark'))}
430
            >
431
              <Icon name={theme === 'dark' ? 'sun' : 'moon'} />
432
            </button>
433
            <button
434
              className="iconButton"
435
              type="button"
436
              aria-label="Sign out"
437
              onClick={() => void signOut(preview)}
438
            >
439
              <span>{initials(session.user.name || session.user.email || 'OC')}</span>
440
            </button>
441
          </div>
442
        </div>
443
        <main id="main">
444
          {preview && (
445
            <div className="previewBanner">Local preview with sample data. Changes stay here.</div>
446
          )}
447
          {error && (
448
            <div className="errorBar" role="alert">
449
              {error}
450
            </div>
451
          )}
452
          {loading ? (
453
            <Loading compact />
454
          ) : (
455
            <PageContent
456
              page={page}
457
              data={data}
458
              settings={settings}
459
              setSettings={setSettings}
460
              navigate={navigate}
461
              refreshDashboard={refreshDashboard}
462
              api={api}
463
            />
464
          )}
465
        </main>
466
      </div>
467
    </div>
468
  );
469
}
470
471
function Login({ onPreview }: { onPreview?: () => void }) {
472
  const [error, setError] = useState('');
473
  const continueWith = async (provider: 'google' | 'github') => {
474
    setError('');
475
    try {
476
      await beginAuth(provider);
477
    } catch {
478
      setError(`${provider === 'google' ? 'Google' : 'GitHub'} sign-in is not configured yet.`);
479
    }
480
  };
481
  return (
482
    <div className="loginPage">
483
      <section className="loginPitch">
484
        <div className="brand">
485
          <div className="mark">OC</div>
486
          <strong>OpenClasp</strong>
487
        </div>
488
        <div>
489
          <p className="eyebrow">AGENT-TO-AGENT ASSURANCE</p>
490
          <h1>Know who your agent is dealing with.</h1>
491
          <p>
492
            Verified identities, signed outcomes, private warnings, and contextual
493
            reliability—without using message bodies for scoring.
494
          </p>
495
        </div>
496
        <div className="loginProof">
497
          <span>01</span> User-owned history
498
          <span>02</span> No universal trust score
499
          <span>03</span> Explicit network consent
500
        </div>
501
      </section>
502
      <section className="loginCard">
503
        <div>
504
          <p className="eyebrow">SECURE ACCESS</p>
505
          <h2>Sign in to OpenClasp</h2>
506
          <p>
507
            Manage connected agents, review signed history, and control what contributes to the
508
            network.
509
          </p>
510
        </div>
511
        <div className="socialButtons">
512
          <button type="button" onClick={() => void continueWith('google')}>
513
            <GoogleMark /> Continue with Google
514
          </button>
515
          <button type="button" onClick={() => void continueWith('github')}>
516
            <GitHubMark /> Continue with GitHub
517
          </button>
518
          {onPreview && (
519
            <button type="button" onClick={() => void onPreview()}>
520
              Continue with local preview
521
            </button>
522
          )}
523
        </div>
524
        {error && (
525
          <div className="loginError" role="alert">
526
            {error}
527
          </div>
528
        )}
529
        <small>
530
          Google and GitHub handle authentication. OpenClasp never receives your password.
531
        </small>
532
      </section>
533
    </div>
534
  );
535
}
536
537
let callbackStarted = false;
538
function AuthCallback() {
539
  const [error, setError] = useState('');
540
  useEffect(() => {
541
    if (callbackStarted) return;
542
    callbackStarted = true;
543
    const finish = async () => {
544
      const query = new URLSearchParams(location.search);
545
      const oauthError = query.get('error_description') ?? query.get('error');
546
      if (oauthError) throw new Error(oauthError);
547
      const code = query.get('code');
548
      const returnedState = query.get('state');
549
      const rawTransaction = sessionStorage.getItem(authTransactionKey);
550
      if (!code || !returnedState || !rawTransaction) throw new Error('Missing OAuth transaction');
551
      const transaction = JSON.parse(rawTransaction) as AuthTransaction;
552
      sessionStorage.removeItem(authTransactionKey);
553
      if (returnedState !== transaction.state) throw new Error('OAuth state mismatch');
554
      const response = await fetch('/api/session', {
555
        method: 'POST',
556
        credentials: 'same-origin',
557
        headers: { 'content-type': 'application/json' },
558
        body: JSON.stringify({
559
          code,
560
          codeVerifier: transaction.verifier,
561
        }),
562
      });
563
      if (!response.ok) throw new Error('Auth0 token exchange failed');
564
      location.replace('/dashboard');
565
    };
566
    void finish().catch((reason: unknown) =>
567
      setError(reason instanceof Error ? reason.message : 'Authentication failed'),
568
    );
569
  }, []);
570
  if (error)
571
    return (
572
      <div className="loading">
573
        <span className="mark">OC</span>
574
        <p>{error}</p>
575
        <a href="/login">Return to login</a>
576
      </div>
577
    );
578
  return <Loading />;
579
}
580
581
function PageContent({
582
  page,
583
  data,
584
  settings,
585
  setSettings,
586
  navigate,
587
  refreshDashboard,
588
  api,
589
}: {
590
  page: Page;
591
  data: DashboardData;
592
  settings: Settings;
593
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
594
  navigate: (page: Page) => void;
595
  refreshDashboard: () => Promise<void>;
596
  api: (path: string, init?: RequestInit) => Promise<unknown>;
597
}) {
598
  if (page === 'history') return <History data={data} />;
599
  if (page === 'conversations')
600
    return <Conversations data={data} refreshDashboard={refreshDashboard} api={api} />;
601
  if (page === 'agents')
602
    return <Agents data={data} navigate={navigate} refreshDashboard={refreshDashboard} api={api} />;
603
  if (page === 'insights') return <Insights data={data} />;
604
  if (page === 'connect')
605
    return <Connect data={data} refreshDashboard={refreshDashboard} api={api} />;
606
  if (page === 'settings')
607
    return <SettingsPage settings={settings} setSettings={setSettings} api={api} />;
608
  return <Overview data={data} navigate={navigate} refreshDashboard={refreshDashboard} api={api} />;
609
}
610
611
function Overview({
612
  data,
613
  navigate,
614
  refreshDashboard,
615
  api,
616
}: {
617
  data: DashboardData;
618
  navigate: (page: Page) => void;
619
  refreshDashboard: () => Promise<void>;
620
  api: (path: string, init?: RequestInit) => Promise<unknown>;
621
}) {
622
  const completed = data.receipts.filter((item) => item.outcome === 'success').length;
623
  const warnings = data.events.filter((item) =>
624
    ['policy_warning', 'policy_violation', 'objection'].includes(String(item.eventType)),
625
  ).length;
626
  const publishedIds = new Set(
627
    data.publications
628
      .filter((publication) => publication.published)
629
      .map((publication) => String(publication.agentId)),
630
  );
631
  const runtimeIds = new Set(
632
    data.runtimes
633
      .filter((runtime) => runtime.status === 'verified')
634
      .map((runtime) => String(runtime.agentId)),
635
  );
636
  const modeOf = (agent: Record<string, any>) =>
637
    agent.agentMode ?? (agent.a2aEndpoint ? 'persistent_runtime' : 'temporary_chat');
638
  const readyAgents = data.agents.filter(
639
    (agent) =>
640
      publishedIds.has(String(agent.agentId)) &&
641
      (modeOf(agent) === 'temporary_chat' || runtimeIds.has(String(agent.agentId))),
642
  ).length;
643
  const activeTemporaryChats = data.agents.filter(
644
    (agent) => modeOf(agent) === 'temporary_chat' && agent.presence?.status === 'online',
645
  ).length;
646
  const pendingInvitations = data.federatedInteractions.filter(
647
    (interaction) => interaction.status === 'pending',
648
  ).length;
649
  const pendingSetup = data.setupRequests.filter((request) => request.status === 'pending').length;
650
  const reviewCount = warnings + pendingInvitations;
651
  return (
652
    <>
653
      <PageHead page="dashboard" action="Connect agent" onAction={() => navigate('connect')} />
654
      {pendingSetup > 0 && (
655
        <button className="attentionBanner" type="button" onClick={() => navigate('connect')}>
656
          <div>
657
            <span className="statusOrb">{pendingSetup}</span>
658
            <div>
659
              <strong>
660
                {pendingSetup} setup request{pendingSetup === 1 ? '' : 's'} waiting
661
              </strong>
662
              <small>Approve the proposed identity before the agent can be bound.</small>
663
            </div>
664
          </div>
665
        </button>
666
      )}
667
      <section
668
        className={`readiness ${readyAgents === data.agents.length && readyAgents ? 'ready' : ''}`}
669
      >
670
        <div>
671
          <span className="statusOrb">{readyAgents ? '✓' : '!'}</span>
672
          <div>
673
            <strong>
674
              {readyAgents === data.agents.length && readyAgents
675
                ? 'Your agent network is ready'
676
                : `${readyAgents} of ${data.agents.length} agents can receive A2A work`}
677
            </strong>
678
            <small>
679
              Safe matching requests activate automatically. Everything else waits for approval.
680
            </small>
681
          </div>
682
        </div>
683
        <button className="secondary" type="button" onClick={() => navigate('agents')}>
684
          Manage automation
685
        </button>
686
      </section>
687
      <section className="metrics">
688
        <Metric
689
          label="Active temporary chats"
690
          value={activeTemporaryChats}
691
          note={`${runtimeIds.size} persistent runtimes connected`}
692
        />
693
        <Metric
694
          label="Interactions"
695
          value={data.interactions.length + data.federatedInteractions.length}
696
          note="local and shared"
697
        />
698
        <Metric label="Successful outcomes" value={completed} note="receipt-backed" />
699
        <Metric
700
          label="Needs review"
701
          value={reviewCount}
702
          note="warnings or approvals"
703
          warn={reviewCount > 0}
704
        />
705
      </section>
706
      <Invitations data={data} refreshDashboard={refreshDashboard} api={api} />
707
      <section className="contentGrid">
708
        <Panel title="Recent activity" subtitle="Structured events and signed outcomes">
709
          <Timeline events={data.events.slice(-6).reverse()} />
710
          <TextButton onClick={() => navigate('history')}>View complete history</TextButton>
711
        </Panel>
712
        <Panel
713
          title="Reliability context"
714
          subtitle="Task-specific profiles, never a universal score"
715
        >
716
          {data.profiles.length ? (
717
            data.profiles
718
              .slice(0, 4)
719
              .map((profile) => (
720
                <Profile key={`${profile.agentId}-${profile.taskCategory}`} profile={profile} />
721
              ))
722
          ) : (
723
            <Empty
724
              title="No reliability profile yet"
725
              text="Profiles appear after signed receipts and eligible bilateral feedback."
726
            />
727
          )}
728
        </Panel>
729
      </section>
730
    </>
731
  );
732
}
733
734
type HistoryFilter = 'all' | 'interaction' | 'event' | 'receipt' | 'dispute';
735
736
function Conversations({
737
  data,
738
  refreshDashboard,
739
  api,
740
}: {
741
  data: DashboardData;
742
  refreshDashboard: () => Promise<void>;
743
  api: (path: string, init?: RequestInit) => Promise<unknown>;
744
}) {
745
  const [selected, setSelected] = useState<Record<string, any> | null>(null);
746
  const [selectedAgentId, setSelectedAgentId] = useState('');
747
  const [draft, setDraft] = useState('');
748
  const [working, setWorking] = useState(false);
749
  const [error, setError] = useState('');
750
  const temporaryAgentFor = (thread: Record<string, any>) =>
751
    data.agents.find(
752
      (agent) =>
753
        thread.participantAgentIds?.includes(agent.agentId) &&
754
        (agent.agentMode ?? (agent.a2aEndpoint ? 'persistent_runtime' : 'temporary_chat')) ===
755
          'temporary_chat',
756
    );
757
  const openThread = async (thread: Record<string, any>) => {
758
    const agent = temporaryAgentFor(thread);
759
    if (!agent) return;
760
    setWorking(true);
761
    setError('');
762
    try {
763
      const [detail] = (await Promise.all([
764
        api(
765
          `/v0.1/agents/${encodeURIComponent(agent.agentId)}/threads/${encodeURIComponent(thread.threadId)}`,
766
        ),
767
        api(
768
          `/v0.1/agents/${encodeURIComponent(agent.agentId)}/threads/${encodeURIComponent(thread.threadId)}/read`,
769
          { method: 'POST' },
770
        ),
771
      ])) as [Record<string, any>, unknown];
772
      setSelected(detail);
773
      setSelectedAgentId(String(agent.agentId));
774
      await refreshDashboard();
775
    } catch (reason) {
776
      setError(reason instanceof Error ? reason.message : 'Could not open conversation');
777
    } finally {
778
      setWorking(false);
779
    }
780
  };
781
  const reply = async () => {
782
    if (!selected || !draft.trim() || !selectedAgentId) return;
783
    setWorking(true);
784
    setError('');
785
    try {
786
      await api(`/v0.1/agents/${encodeURIComponent(selectedAgentId)}/messages`, {
787
        method: 'POST',
788
        body: JSON.stringify({
789
          interactionId: selected.thread.interactionId,
790
          content: draft.trim(),
791
        }),
792
      });
793
      setDraft('');
794
      await openThread(selected.thread);
795
    } catch (reason) {
796
      setError(reason instanceof Error ? reason.message : 'Could not send reply');
797
      setWorking(false);
798
    }
799
  };
800
  return (
801
    <>
802
      <PageHead page="conversations" />
803
      <div className="notice">
804
        <strong>Hosted temporary mode.</strong> OpenClasp processes these messages and encrypts them
805
        at rest for 30 days. Messages between persistent runtimes remain direct and are never shown
806
        here.
807
      </div>
808
      {error ? (
809
        <div className="errorBar" role="alert">
810
          {error}
811
        </div>
812
      ) : null}
813
      <section className="conversationLayout">
814
        <Panel title="Threads" subtitle="Temporary chat identities only">
815
          <div className="threadList">
816
            {data.hostedThreads.length ? (
817
              data.hostedThreads.map((thread) => {
818
                const agent = temporaryAgentFor(thread);
819
                const peer = thread.participantAgentIds?.find(
820
                  (value: string) => value !== agent?.agentId,
821
                );
822
                return (
823
                  <button
824
                    type="button"
825
                    key={thread.threadId}
826
                    className={selected?.thread?.threadId === thread.threadId ? 'active' : ''}
827
                    onClick={() => void openThread(thread)}
828
                    disabled={working}
829
                  >
830
                    <span>
831
                      <strong>{peer ?? 'Counterparty'}</strong>
832
                      <small>{agent?.name ?? agent?.agentId}</small>
833
                    </span>
834
                    <b>{thread.unreadCount ? `${thread.unreadCount} NEW` : thread.status}</b>
835
                  </button>
836
                );
837
              })
838
            ) : (
839
              <Empty
840
                title="No temporary conversations"
841
                text="Messages appear after a temporary chat identity accepts an interaction with a persistent runtime."
842
              />
843
            )}
844
          </div>
845
        </Panel>
846
        <Panel
847
          title={selected ? 'Conversation' : 'Select a thread'}
848
          subtitle="Private insights are attributable and task-specific"
849
        >
850
          {selected ? (
851
            <div className="conversationDetail">
852
              {!!selected.insights?.length && (
853
                <div className="conversationInsights">
854
                  {selected.insights.map((insight: Record<string, any>) => (
855
                    <p key={insight.code}>
856
                      <b>{String(insight.severity).toUpperCase()}</b> {insight.message}
857
                    </p>
858
                  ))}
859
                </div>
860
              )}
861
              <div className="messageList">
862
                {selected.messages.map((message: Record<string, any>) => (
863
                  <article
864
                    key={message.messageId}
865
                    className={message.senderAgentId === selectedAgentId ? 'sent' : 'received'}
866
                  >
867
                    <small>{message.senderAgentId === selectedAgentId ? 'You' : 'Peer'}</small>
868
                    <p>{message.content}</p>
869
                    <span>{new Date(message.createdAt).toLocaleString()}</span>
870
                  </article>
871
                ))}
872
              </div>
873
              <div className="replyBox">
874
                <textarea
875
                  aria-label="Temporary chat reply"
876
                  value={draft}
877
                  onChange={(event) => setDraft(event.target.value)}
878
                  maxLength={20_000}
879
                  placeholder="Reply through the temporary OpenClasp A2A adapter"
880
                />
881
                <button
882
                  className="primary"
883
                  type="button"
884
                  disabled={working || !draft.trim() || selected.thread.status !== 'open'}
885
                  onClick={() => void reply()}
886
                >
887
                  {working ? 'Sending…' : 'Send reply'}
888
                </button>
889
              </div>
890
            </div>
891
          ) : (
892
            <Empty title="Nothing selected" text="Choose a thread to read its hosted history." />
893
          )}
894
        </Panel>
895
      </section>
896
    </>
897
  );
898
}
899
900
function History({ data }: { data: DashboardData }) {
901
  const [query, setQuery] = useState('');
902
  const [filter, setFilter] = useState<HistoryFilter>('all');
903
  const records = useMemo(
904
    () =>
905
      [
906
        ...data.interactions.map((value) => ({ ...value, _kind: 'interaction' })),
907
        ...data.federatedInteractions.map((value) => ({
908
          ...value,
909
          _kind: 'federated interaction',
910
        })),
911
        ...data.events.map((value) => ({ ...value, _kind: 'event' })),
912
        ...data.receipts.map((value) => ({ ...value, _kind: 'receipt' })),
913
        ...data.conflicts.map((value) => ({ ...value, _kind: 'dispute' })),
914
      ].sort((a, b) => Date.parse(timestamp(b)) - Date.parse(timestamp(a))),
915
    [data],
916
  );
917
  const filtered = useMemo(() => {
918
    const needle = query.trim().toLowerCase();
919
    return records.filter((item) => {
920
      const kind = String(item._kind);
921
      if (filter === 'interaction' && !kind.includes('interaction')) return false;
922
      if (filter === 'event' && kind !== 'event') return false;
923
      if (filter === 'receipt' && kind !== 'receipt') return false;
924
      if (filter === 'dispute' && kind !== 'dispute') return false;
925
      if (!needle) return true;
926
      const record = item as Record<string, any>;
927
      const hay = [
928
        record.eventType,
929
        record._kind,
930
        record.agentId,
931
        record.interactionId,
932
        record.receiptId,
933
        record.outcome,
934
        record.status,
935
        record.visibility,
936
        record.contract?.purpose,
937
      ]
938
        .filter(Boolean)
939
        .join(' ')
940
        .toLowerCase();
941
      return hay.includes(needle);
942
    });
943
  }, [filter, query, records]);
944
  const count = (value: HistoryFilter) =>
945
    value === 'all'
946
      ? records.length
947
      : records.filter((item) =>
948
          value === 'interaction'
949
            ? String(item._kind).includes('interaction')
950
            : item._kind === value,
951
        ).length;
952
  return (
953
    <>
954
      <PageHead page="history" />
955
      <Panel
956
        title="Account history"
957
        subtitle="Only structured metadata, signatures, hashes, and permitted evidence are hosted"
958
      >
959
        <div className="toolbar">
960
          <input
961
            className="searchField"
962
            value={query}
963
            onChange={(event) => setQuery(event.target.value)}
964
            placeholder="Search agents, contracts, or statuses"
965
            aria-label="Search history"
966
          />
967
          <div className="filterRow" role="tablist" aria-label="History filters">
968
            {(
969
              [
970
                ['all', 'All'],
971
                ['interaction', 'Interactions'],
972
                ['event', 'Events'],
973
                ['receipt', 'Receipts'],
974
                ['dispute', 'Disputes'],
975
              ] as const
976
            ).map(([value, label]) => (
977
              <button
978
                key={value}
979
                className={filter === value ? 'filterChip active' : 'filterChip'}
980
                type="button"
981
                aria-pressed={filter === value}
982
                onClick={() => setFilter(value)}
983
              >
984
                {label} {count(value)}
985
              </button>
986
            ))}
987
          </div>
988
        </div>
989
        {filtered.length ? (
990
          filtered.map((item, index) => (
991
            <HistoryRow
992
              key={`${String(item._kind)}-${String(
993
                (item as Record<string, any>).eventId ??
994
                  (item as Record<string, any>).receiptId ??
995
                  (item as Record<string, any>).conflictId ??
996
                  (item as Record<string, any>).interactionId ??
997
                  index,
998
              )}`}
999
              item={item}
1000
            />
1001
          ))
1002
        ) : (
1003
          <Empty
1004
            title={records.length ? 'No matching records' : 'No interactions recorded'}
1005
            text={
1006
              records.length
1007
                ? 'Try another filter or clear the search.'
1008
                : 'Connect an agent and start an assured interaction. Raw message bodies will not appear here.'
1009
            }
1010
          />
1011
        )}
1012
      </Panel>
1013
    </>
1014
  );
1015
}
1016
1017
function Agents({
1018
  data,
1019
  navigate,
1020
  refreshDashboard,
1021
  api,
1022
}: {
1023
  data: DashboardData;
1024
  navigate: (page: Page) => void;
1025
  refreshDashboard: () => Promise<void>;
1026
  api: (path: string, init?: RequestInit) => Promise<unknown>;
1027
}) {
1028
  const [working, setWorking] = useState('');
1029
  const [error, setError] = useState('');
1030
  const saveAutomation = async (
1031
    agentId: string,
1032
    value: {
1033
      autoPublish: boolean;
1034
      autoAcceptPolicy: 'off' | 'safe_matching';
1035
      autoAcceptTaskCategories: string[];
1036
    },
1037
  ) => {
1038
    setWorking(agentId);
1039
    setError('');
1040
    try {
1041
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}/automation`, {
1042
        method: 'PUT',
1043
        body: JSON.stringify(value),
1044
      });
1045
      await refreshDashboard();
1046
      return true;
1047
    } catch (reason) {
1048
      setError(reason instanceof Error ? reason.message : 'Directory update failed');
1049
      return false;
1050
    } finally {
1051
      setWorking('');
1052
    }
1053
  };
1054
  const saveRuntime = async (agentId: string, endpoint: string) => {
1055
    setWorking(`runtime:${agentId}`);
1056
    setError('');
1057
    try {
1058
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}/runtime`, {
1059
        method: 'PUT',
1060
        body: JSON.stringify({ endpoint }),
1061
      });
1062
      await refreshDashboard();
1063
    } catch (reason) {
1064
      setError(reason instanceof Error ? reason.message : 'Runtime verification failed');
1065
    } finally {
1066
      setWorking('');
1067
    }
1068
  };
1069
  const disableRuntime = async (agentId: string) => {
1070
    setWorking(`runtime:${agentId}`);
1071
    setError('');
1072
    try {
1073
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}/runtime`, { method: 'DELETE' });
1074
      await refreshDashboard();
1075
    } catch (reason) {
1076
      setError(reason instanceof Error ? reason.message : 'Runtime update failed');
1077
    } finally {
1078
      setWorking('');
1079
    }
1080
  };
1081
  const deleteAgent = async (agent: Record<string, any>) => {
1082
    const agentId = String(agent.agentId);
1083
    if (
1084
      !window.confirm(
1085
        `Delete “${String(agent.name ?? agentId)}”?\n\nIts runtime, publication, presence, MCP binding and hosted temporary messages will be removed. Signed interaction and receipt history will be retained.`,
1086
      )
1087
    )
1088
      return;
1089
    setWorking(`delete:${agentId}`);
1090
    setError('');
1091
    try {
1092
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}`, { method: 'DELETE' });
1093
      await refreshDashboard();
1094
    } catch (reason) {
1095
      setError(reason instanceof Error ? reason.message : 'Agent deletion failed');
1096
    } finally {
1097
      setWorking('');
1098
    }
1099
  };
1100
  return (
1101
    <>
1102
      <PageHead page="agents" action="Connect agent" onAction={() => navigate('connect')} />
1103
      <div className="notice">
1104
        <strong>Two explicit modes.</strong> Persistent runtimes talk directly over A2A. Temporary
1105
        chats use an OpenClasp-hosted endpoint with encrypted-at-rest history. There is no offline
1106
        relay for persistent runtimes.
1107
      </div>
1108
      {error && (
1109
        <div className="errorBar" role="alert">
1110
          {error}
1111
        </div>
1112
      )}
1113
      <section className="agentGrid">
1114
        {data.agents.length ? (
1115
          data.agents.map((agent) => (
1116
            <AgentCard
1117
              key={agent.agentId}
1118
              agent={agent}
1119
              projectName={
1120
                data.projects.find((project) => project.projectId === agent.projectId)?.name
1121
              }
1122
              published={data.publications.some(
1123
                (publication) => publication.agentId === agent.agentId && publication.published,
1124
              )}
1125
              working={working === agent.agentId}
1126
              onSave={(value) => saveAutomation(agent.agentId, value)}
1127
              runtime={data.runtimes.find((runtime) => runtime.agentId === agent.agentId)}
1128
              runtimeWorking={working === `runtime:${agent.agentId}`}
1129
              deleteWorking={working === `delete:${agent.agentId}`}
1130
              onRuntime={(endpoint) => saveRuntime(agent.agentId, endpoint)}
1131
              onDisableRuntime={() => disableRuntime(agent.agentId)}
1132
              onDelete={() => deleteAgent(agent)}
1133
            />
1134
          ))
1135
        ) : (
1136
          <Empty
1137
            title="No agents connected"
1138
            text="Install the remote MCP endpoint in an agent, sign in, then create and register its identity."
1139
            action="Open connection guide"
1140
            onAction={() => navigate('connect')}
1141
          />
1142
        )}
1143
      </section>
1144
    </>
1145
  );
1146
}
1147
1148
function Invitations({
1149
  data,
1150
  refreshDashboard,
1151
  api,
1152
}: {
1153
  data: DashboardData;
1154
  refreshDashboard: () => Promise<void>;
1155
  api: (path: string, init?: RequestInit) => Promise<unknown>;
1156
}) {
1157
  const [working, setWorking] = useState('');
1158
  const [error, setError] = useState('');
1159
  const ownedAgentIds = useMemo(
1160
    () => new Set(data.agents.map((agent) => String(agent.agentId))),
1161
    [data.agents],
1162
  );
1163
  const incoming = data.federatedInteractions.filter(
1164
    (interaction) =>
1165
      interaction.status === 'pending' && ownedAgentIds.has(String(interaction.responderAgentId)),
1166
  );
1167
  const respond = async (interactionId: string, agentId: string, decision: 'accept' | 'reject') => {
1168
    setWorking(interactionId);
1169
    setError('');
1170
    try {
1171
      await api(`/v0.1/federated-interactions/${encodeURIComponent(interactionId)}/respond`, {
1172
        method: 'POST',
1173
        body: JSON.stringify({ agentId, decision }),
1174
      });
1175
      await refreshDashboard();
1176
    } catch (reason) {
1177
      setError(reason instanceof Error ? reason.message : 'Could not respond to invitation');
1178
    } finally {
1179
      setWorking('');
1180
    }
1181
  };
1182
  if (!incoming.length && !data.federatedInteractions.length) return null;
1183
  return (
1184
    <Panel
1185
      title={incoming.length ? `Agent invitations (${incoming.length})` : 'Shared interactions'}
1186
      subtitle="Both accounts see the same immutable contract and acceptance state"
1187
    >
1188
      {error ? (
1189
        <div className="errorBar" role="alert">
1190
          {error}
1191
        </div>
1192
      ) : null}
1193
      {(incoming.length ? incoming : data.federatedInteractions.slice(0, 5)).map((interaction) => (
1194
        <article className="setupRequest" key={interaction.interactionId}>
1195
          <div>
1196
            <strong>{interaction.contract?.purpose ?? interaction.interactionId}</strong>
1197
            <small>
1198
              {interaction.initiatorAgentId} → {interaction.responderAgentId} · {interaction.status}
1199
            </small>
1200
            <small>Contract: {String(interaction.termsHash).slice(0, 16)}…</small>
1201
            {data.liveSessions.find(
1202
              (session) => session.interactionId === interaction.interactionId,
1203
            ) ? (
1204
              <small className="autoAccepted">
1205
                Live session:{' '}
1206
                {
1207
                  data.liveSessions.find(
1208
                    (session) => session.interactionId === interaction.interactionId,
1209
                  )?.status
1210
                }
1211
              </small>
1212
            ) : null}
1213
            {interaction.status === 'active' &&
1214
            interaction.acceptances?.[interaction.responderAgentId]?.method ===
1215
              'policy_auto_accept' ? (
1216
              <small className="autoAccepted">✓ Auto-approved by the responder's safe policy</small>
1217
            ) : null}
1218
          </div>
1219
          {interaction.status === 'pending' &&
1220
          ownedAgentIds.has(String(interaction.responderAgentId)) ? (
1221
            <div className="decisionButtons">
1222
              <button
1223
                className="secondary"
1224
                type="button"
1225
                disabled={working === interaction.interactionId}
1226
                onClick={() =>
1227
                  void respond(interaction.interactionId, interaction.responderAgentId, 'reject')
1228
                }
1229
              >
1230
                Reject
1231
              </button>
1232
              <button
1233
                className="primary"
1234
                type="button"
1235
                disabled={working === interaction.interactionId}
1236
                onClick={() =>
1237
                  void respond(interaction.interactionId, interaction.responderAgentId, 'accept')
1238
                }
1239
              >
1240
                {working === interaction.interactionId ? 'Working…' : 'Accept contract'}
1241
              </button>
1242
            </div>
1243
          ) : null}
1244
        </article>
1245
      ))}
1246
    </Panel>
1247
  );
1248
}
1249
1250
function Insights({ data }: { data: DashboardData }) {
1251
  return (
1252
    <>
1253
      <PageHead page="insights" />
1254
      <div className="notice">
1255
        <strong>Context matters.</strong> OpenClasp profiles an agent by task category and version.
1256
        It does not produce a single universal trust score.
1257
      </div>
1258
      <section className="agentGrid">
1259
        {data.profiles.length ? (
1260
          data.profiles.map((profile) => (
1261
            <Panel
1262
              key={`${profile.agentId}-${profile.taskCategory}`}
1263
              title={profile.agentId}
1264
              subtitle={`${profile.taskCategory} · v${profile.agentVersion}`}
1265
            >
1266
              <Profile profile={profile} expanded />
1267
            </Panel>
1268
          ))
1269
        ) : (
1270
          <Empty
1271
            title="Not enough verified evidence"
1272
            text="Insights grow only from signed outcomes, eligible feedback, corrections, disputes, and version-aware history."
1273
          />
1274
        )}
1275
      </section>
1276
    </>
1277
  );
1278
}
1279
1280
function Connect({
1281
  data,
1282
  refreshDashboard,
1283
  api,
1284
}: {
1285
  data: DashboardData;
1286
  refreshDashboard: () => Promise<void>;
1287
  api: (path: string, init?: RequestInit) => Promise<unknown>;
1288
}) {
1289
  const endpoint = 'https://openclasp.vercel.app/mcp';
1290
  const [copied, setCopied] = useState(false);
1291
  const [working, setWorking] = useState('');
1292
  const [decisionError, setDecisionError] = useState('');
1293
  const pending = data.setupRequests.filter((request) => request.status === 'pending');
1294
  useEffect(() => {
1295
    const poll = () => void refreshDashboard().catch(() => undefined);
1296
    poll();
1297
    const timer = window.setInterval(poll, 5000);
1298
    return () => window.clearInterval(timer);
1299
  }, [refreshDashboard]);
1300
  const decide = async (requestId: string, decision: 'approve' | 'reject') => {
1301
    setWorking(requestId);
1302
    setDecisionError('');
1303
    try {
1304
      await api(`/v0.1/onboarding/${encodeURIComponent(requestId)}/${decision}`, {
1305
        method: 'POST',
1306
      });
1307
      await refreshDashboard();
1308
    } catch (reason) {
1309
      setDecisionError(reason instanceof Error ? reason.message : 'Could not update setup request');
1310
    } finally {
1311
      setWorking('');
1312
    }
1313
  };
1314
  return (
1315
    <>
1316
      <PageHead page="connect" />
1317
      {pending.length > 0 && (
1318
        <section className="setupRequests">
1319
          <div>
1320
            <p className="eyebrow">CONFIRMATION REQUIRED</p>
1321
            <h2>Approve agent setup</h2>
1322
            <p>
1323
              An agent proposed this identity. Confirm it before OpenClasp binds the installation.
1324
            </p>
1325
          </div>
1326
          {pending.map((request) => (
1327
            <article className="setupRequest" key={request.requestId}>
1328
              <div>
1329
                <strong>{request.agentName ?? 'Switch installation'}</strong>
1330
                <small>
1331
                  {request.projectName ?? request.existingAgentId} · {request.framework}
1332
                </small>
1333
                {!!request.capabilities?.length && (
1334
                  <div className="tags">
1335
                    {request.capabilities.slice(0, 5).map((capability: string) => (
1336
                      <span key={capability}>{capability}</span>
1337
                    ))}
1338
                  </div>
1339
                )}
1340
                {!!request.limitations?.length && (
1341
                  <small>Limitations: {request.limitations.join(' · ')}</small>
1342
                )}
1343
                <div className="automationPreview">
1344
                  <span>{request.autoPublish ? 'Public after approval' : 'Private'}</span>
1345
                  <span>
1346
                    {(request.agentMode ?? 'temporary_chat') === 'temporary_chat'
1347
                      ? 'Hosted temporary A2A · encrypted history'
1348
                      : 'Persistent runtime · direct A2A'}
1349
                  </span>
1350
                  <span>
1351
                    {request.autoAcceptPolicy === 'safe_matching'
1352
                      ? 'Auto-accept safe matches'
1353
                      : 'Review every request'}
1354
                  </span>
1355
                </div>
1356
              </div>
1357
              <div className="decisionButtons">
1358
                <button
1359
                  className="secondary"
1360
                  type="button"
1361
                  disabled={working === request.requestId}
1362
                  onClick={() => void decide(request.requestId, 'reject')}
1363
                >
1364
                  Reject
1365
                </button>
1366
                <button
1367
                  className="primary"
1368
                  type="button"
1369
                  disabled={working === request.requestId}
1370
                  onClick={() => void decide(request.requestId, 'approve')}
1371
                >
1372
                  {working === request.requestId ? 'Working…' : 'Approve & automate'}
1373
                </button>
1374
              </div>
1375
            </article>
1376
          ))}
1377
          {decisionError && (
1378
            <div className="loginError" role="alert">
1379
              {decisionError}
1380
            </div>
1381
          )}
1382
        </section>
1383
      )}
1384
      <section className="connectLayout">
1385
        <Panel title="Connect once" subtitle="OpenClasp handles the routine steps afterward">
1386
          <div className="endpoint">
1387
            <code>{endpoint}</code>
1388
            <button
1389
              type="button"
1390
              onClick={() => {
1391
                void navigator.clipboard.writeText(endpoint);
1392
                setCopied(true);
1393
                window.setTimeout(() => setCopied(false), 2000);
1394
              }}
1395
            >
1396
              {copied ? 'Copied' : 'Copy'}
1397
            </button>
1398
          </div>
1399
          <div className="connectSteps">
1400
            <div className="connectStep">
1401
              <b>1</b>
1402
              <span>Add this MCP URL and complete the OAuth sign-in.</span>
1403
            </div>
1404
            <div className="connectStep">
1405
              <b>2</b>
1406
              <span>
1407
                Tell the agent: <code>Set yourself up on OpenClasp</code>.
1408
              </span>
1409
            </div>
1410
            <div className="connectStep">
1411
              <b>3</b>
1412
              <span>
1413
                Approve its identity, temporary/direct mode, privacy and automation policy here.
1414
              </span>
1415
            </div>
1416
          </div>
1417
        </Panel>
1418
        <Panel
1419
          title="What becomes automatic"
1420
          subtitle="Safe defaults with deterministic approval boundaries"
1421
        >
1422
          <ul className="checkList">
1423
            <li>Agent Card publication and discovery</li>
1424
            <li>Contract defaults inferred from the task</li>
1425
            <li>Safe matching invitations accepted immediately</li>
1426
            <li>Ready-to-send A2A endpoint, headers, and metadata</li>
1427
            <li>Hosted encrypted history only for temporary chat identities</li>
1428
            <li>Risky requests routed to human review</li>
1429
          </ul>
1430
        </Panel>
1431
      </section>
1432
    </>
1433
  );
1434
}
1435
1436
function SettingsPage({
1437
  settings,
1438
  setSettings,
1439
  api,
1440
}: {
1441
  settings: Settings;
1442
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
1443
  api: (path: string, init?: RequestInit) => Promise<unknown>;
1444
}) {
1445
  const [saving, setSaving] = useState(false);
1446
  const [saved, setSaved] = useState(false);
1447
  const [error, setError] = useState('');
1448
  const save = useCallback(async () => {
1449
    setSaving(true);
1450
    setSaved(false);
1451
    setError('');
1452
    try {
1453
      const updated = (await api('/v0.1/settings', {
1454
        method: 'PUT',
1455
        body: JSON.stringify(settings),
1456
      })) as Settings;
1457
      setSettings(updated);
1458
      setSaved(true);
1459
    } catch (reason) {
1460
      setError(reason instanceof Error ? reason.message : 'Could not save settings');
1461
    } finally {
1462
      setSaving(false);
1463
    }
1464
  }, [api, settings, setSettings]);
1465
  return (
1466
    <>
1467
      <PageHead page="settings" />
1468
      <section className="settingsCard">
1469
        <Setting label="Display name" description="Shown only inside your OpenClasp account.">
1470
          <input
1471
            value={settings.displayName}
1472
            onChange={(event) =>
1473
              setSettings((value) => ({ ...value, displayName: event.target.value }))
1474
            }
1475
            placeholder="Team or operator name"
1476
          />
1477
        </Setting>
1478
        <Setting
1479
          label="Network contribution"
1480
          description="Allow eligible structured aggregates to improve contextual intelligence. Raw conversations are never included."
1481
        >
1482
          <Toggle
1483
            checked={settings.contributionEnabled}
1484
            label="Network contribution"
1485
            onChange={(checked) =>
1486
              setSettings((value) => ({ ...value, contributionEnabled: checked }))
1487
            }
1488
          />
1489
        </Setting>
1490
        <Setting
1491
          label="Evidence sharing"
1492
          description="Controls when permitted evidence references may be shared with interaction participants."
1493
        >
1494
          <select
1495
            value={settings.evidenceSharing}
1496
            onChange={(event) =>
1497
              setSettings((value) => ({
1498
                ...value,
1499
                evidenceSharing: event.target.value as Settings['evidenceSharing'],
1500
              }))
1501
            }
1502
          >
1503
            <option value="never">Never</option>
1504
            <option value="ask">Ask every time</option>
1505
            <option value="contract_only">When contract permits</option>
1506
          </select>
1507
        </Setting>
1508
        <Setting
1509
          label="Structured record retention"
1510
          description="Retention for hosted account records. Signed protocol records may require separate revocation markers."
1511
        >
1512
          <select
1513
            value={settings.retentionDays}
1514
            onChange={(event) =>
1515
              setSettings((value) => ({ ...value, retentionDays: Number(event.target.value) }))
1516
            }
1517
          >
1518
            <option value={7}>7 days</option>
1519
            <option value={30}>30 days</option>
1520
            <option value={90}>90 days</option>
1521
            <option value={365}>1 year</option>
1522
            <option value={0}>No automatic deletion</option>
1523
          </select>
1524
        </Setting>
1525
        <Setting
1526
          label="Raw agent messages"
1527
          description="Persistent runtimes remain direct and are not stored. Temporary chat messages pass through OpenClasp and are encrypted at rest for 30 days."
1528
        >
1529
          <span className="locked">MODE DEPENDENT</span>
1530
        </Setting>
1531
        <div className="saveRow">
1532
          <button className="primary" type="button" onClick={() => void save()} disabled={saving}>
1533
            {saving ? 'Saving…' : 'Save settings'}
1534
          </button>
1535
          {saved && <span role="status">Saved</span>}
1536
          {error && (
1537
            <span className="loginError" role="alert">
1538
              {error}
1539
            </span>
1540
          )}
1541
        </div>
1542
      </section>
1543
    </>
1544
  );
1545
}
1546
1547
function PageHead({
1548
  page,
1549
  action,
1550
  onAction,
1551
}: {
1552
  page: Page;
1553
  action?: string;
1554
  onAction?: () => void;
1555
}) {
1556
  const meta = pageMeta[page];
1557
  return (
1558
    <header className="pageHead">
1559
      <div>
1560
        <p className="eyebrow">{meta.eyebrow}</p>
1561
        <h1>{meta.title}</h1>
1562
        <p className="lede">{meta.lede}</p>
1563
      </div>
1564
      {action && (
1565
        <button className="primary" type="button" onClick={onAction}>
1566
          {action}
1567
        </button>
1568
      )}
1569
    </header>
1570
  );
1571
}
1572
function Nav({
1573
  page,
1574
  active,
1575
  onClick,
1576
  label,
1577
  icon,
1578
  badge = 0,
1579
}: {
1580
  page: Page;
1581
  active: Page;
1582
  onClick: (page: Page) => void;
1583
  label: string;
1584
  icon: IconName;
1585
  badge?: number;
1586
}) {
1587
  return (
1588
    <button
1589
      className={active === page ? 'active' : ''}
1590
      type="button"
1591
      aria-current={active === page ? 'page' : undefined}
1592
      onClick={() => onClick(page)}
1593
    >
1594
      <Icon name={icon} />
1595
      {label}
1596
      {badge > 0 ? <span className="navBadge">{badge}</span> : null}
1597
    </button>
1598
  );
1599
}
1600
function Metric({
1601
  label,
1602
  value,
1603
  note,
1604
  warn,
1605
}: {
1606
  label: string;
1607
  value: number;
1608
  note: string;
1609
  warn?: boolean;
1610
}) {
1611
  return (
1612
    <article className={warn ? 'metric warn' : 'metric'}>
1613
      <span>{label}</span>
1614
      <strong>{value}</strong>
1615
      <small>{note}</small>
1616
    </article>
1617
  );
1618
}
1619
function Panel({
1620
  title,
1621
  subtitle,
1622
  children,
1623
}: React.PropsWithChildren<{ title: string; subtitle: string }>) {
1624
  return (
1625
    <section className="panel">
1626
      <div className="panelHead">
1627
        <div>
1628
          <h2>{title}</h2>
1629
          <p>{subtitle}</p>
1630
        </div>
1631
      </div>
1632
      {children}
1633
    </section>
1634
  );
1635
}
1636
function Timeline({ events }: { events: Record<string, any>[] }) {
1637
  return events.length ? (
1638
    <div className="timeline">
1639
      {events.map((event) => (
1640
        <HistoryRow key={event.eventId} item={{ ...event, _kind: 'event' }} />
1641
      ))}
1642
    </div>
1643
  ) : (
1644
    <Empty
1645
      title="Nothing recorded yet"
1646
      text="Activity appears after your connected agents submit signed structured events."
1647
    />
1648
  );
1649
}
1650
function HistoryRow({ item }: { item: Record<string, any> }) {
1651
  const kind = String(item.eventType ?? item._kind ?? 'record').replaceAll('_', ' ');
1652
  const id = String(item.agentId ?? item.interactionId ?? item.receiptId ?? 'OpenClasp');
1653
  return (
1654
    <article className="historyRow">
1655
      <span className={`eventDot ${item.eventType ?? item._kind}`} />
1656
      <div>
1657
        <strong>{kind}</strong>
1658
        <small>
1659
          {id} · {new Date(timestamp(item)).toLocaleString()}
1660
        </small>
1661
      </div>
1662
      <b>{item.visibility ?? item.outcome ?? item.status ?? 'SIGNED'}</b>
1663
    </article>
1664
  );
1665
}
1666
function AgentCard({
1667
  agent,
1668
  projectName,
1669
  published,
1670
  working,
1671
  onSave,
1672
  runtime,
1673
  runtimeWorking,
1674
  deleteWorking,
1675
  onRuntime,
1676
  onDisableRuntime,
1677
  onDelete,
1678
}: {
1679
  agent: Record<string, any>;
1680
  projectName?: string;
1681
  published: boolean;
1682
  working: boolean;
1683
  onSave: (value: {
1684
    autoPublish: boolean;
1685
    autoAcceptPolicy: 'off' | 'safe_matching';
1686
    autoAcceptTaskCategories: string[];
1687
  }) => Promise<boolean>;
1688
  runtime: Record<string, any> | undefined;
1689
  runtimeWorking: boolean;
1690
  deleteWorking: boolean;
1691
  onRuntime: (endpoint: string) => void;
1692
  onDisableRuntime: () => void;
1693
  onDelete: () => void;
1694
}) {
1695
  const [editing, setEditing] = useState(false);
1696
  const [autoPublish, setAutoPublish] = useState(Boolean(agent.autoPublish ?? published));
1697
  const [autoAcceptPolicy, setAutoAcceptPolicy] = useState<'off' | 'safe_matching'>(
1698
    agent.autoAcceptPolicy === 'safe_matching' ? 'safe_matching' : 'off',
1699
  );
1700
  const [categories, setCategories] = useState<string>(
1701
    (agent.autoAcceptTaskCategories?.length
1702
      ? agent.autoAcceptTaskCategories
1703
      : (agent.capabilities ?? [])
1704
    ).join(', '),
1705
  );
1706
  const [runtimeEndpoint, setRuntimeEndpoint] = useState(String(runtime?.endpoint ?? ''));
1707
  const mode = agent.agentMode ?? (agent.a2aEndpoint ? 'persistent_runtime' : 'temporary_chat');
1708
  const temporary = mode === 'temporary_chat';
1709
  const ready = published && (temporary || runtime?.status === 'verified');
1710
  const online = agent.presence?.status === 'online';
1711
  const endpoint = String(
1712
    temporary
1713
      ? `/a2a/temporary/${encodeURIComponent(agent.agentId)}`
1714
      : (runtime?.a2aEndpoint ?? agent.a2aEndpoint ?? 'Connect a runtime first'),
1715
  );
1716
  const identityLabel = agent.revoked
1717
    ? 'REVOKED'
1718
    : agent.identityMode === 'oauth_installation'
1719
      ? 'AUTHENTICATED'
1720
      : 'VERIFIED';
1721
  return (
1722
    <article className="agentCard">
1723
      <div className="agentTop">
1724
        <span className="agentGlyph">
1725
          <Icon name="agents" />
1726
        </span>
1727
        <div className="agentBadges">
1728
          <b className={online && temporary ? 'onlineBadge' : 'offlineBadge'}>
1729
            {temporary
1730
              ? online
1731
                ? 'CHAT ACTIVE'
1732
                : 'CHAT IDLE'
1733
              : runtime?.status === 'verified'
1734
                ? 'ENDPOINT VERIFIED'
1735
                : 'ENDPOINT MISSING'}
1736
          </b>
1737
          <b className={agent.revoked ? 'bad' : ''}>{identityLabel}</b>
1738
          <b className={ready ? 'readyBadge' : 'needsBadge'}>{ready ? 'READY' : 'SETUP NEEDED'}</b>
1739
        </div>
1740
      </div>
1741
      <h2>{agent.name ?? agent.agentId}</h2>
1742
      <p>{projectName ?? `Version ${agent.agentVersion ?? '1.0.0'}`}</p>
1743
      <div className="tags">
1744
        {(agent.capabilities ?? []).slice(0, 4).map((capability: string) => (
1745
          <span key={capability}>{capability}</span>
1746
        ))}
1747
      </div>
1748
      <small>
1749
        {agent.framework ? `${agent.framework} · ` : ''}
1750
        {agent.identityMode === 'oauth_installation' ? 'OAuth-bound · ' : 'Ed25519 · '}Created{' '}
1751
        {new Date(agent.createdAt).toLocaleDateString()}
1752
      </small>
1753
      <small>
1754
        {agent.presence?.lastSeenAt
1755
          ? `Last agent activity ${new Date(agent.presence.lastSeenAt).toLocaleString()}`
1756
          : 'No agent activity recorded yet'}
1757
      </small>
1758
      {published ? (
1759
        <a
1760
          href={`/agents/${encodeURIComponent(agent.agentId)}/card.json`}
1761
          target="_blank"
1762
          rel="noreferrer"
1763
        >
1764
          Public Agent Card
1765
        </a>
1766
      ) : null}
1767
      <div className="automationSummary">
1768
        <span>{published ? 'Public discovery' : 'Private'}</span>
1769
        <span>{temporary ? 'Hosted temporary A2A' : 'Direct agent-owned A2A'}</span>
1770
        <span>
1771
          {agent.autoAcceptPolicy === 'safe_matching' ? 'Safe tasks automatic' : 'Manual approval'}
1772
        </span>
1773
      </div>
1774
      <div className="runtimeBox">
1775
        <div>
1776
          <strong>{temporary ? 'Temporary chat endpoint' : 'Autonomous runtime'}</strong>
1777
          <span
1778
            className={
1779
              temporary || runtime?.status === 'verified' ? 'runtimeLive' : 'runtimeMissing'
1780
            }
1781
          >
1782
            {temporary
1783
              ? 'HOSTED BY OPENCLASP'
1784
              : runtime?.status === 'verified'
1785
                ? 'VERIFIED'
1786
                : 'NOT CONNECTED'}
1787
          </span>
1788
        </div>
1789
        <p>
1790
          {temporary
1791
            ? 'OpenClasp receives A2A for this temporary identity and encrypts its history at rest. Connect a runtime below to switch to direct A2A.'
1792
            : 'Connect the worker running on your cloud. OpenClasp prepares the live session, then messages travel directly between agents.'}
1793
        </p>
1794
        <input
1795
          type="url"
1796
          value={runtimeEndpoint}
1797
          onChange={(event) => setRuntimeEndpoint(event.target.value)}
1798
          placeholder="https://agent.example.com/openclasp"
1799
        />
1800
        <div className="agentActions">
1801
          {runtime?.status === 'verified' ? (
1802
            <button
1803
              className="secondary"
1804
              type="button"
1805
              disabled={runtimeWorking}
1806
              onClick={onDisableRuntime}
1807
            >
1808
              Disable runtime
1809
            </button>
1810
          ) : null}
1811
          <button
1812
            className="primary"
1813
            type="button"
1814
            disabled={runtimeWorking || !runtimeEndpoint.trim()}
1815
            onClick={() => onRuntime(runtimeEndpoint.trim())}
1816
          >
1817
            {runtimeWorking ? 'Verifying…' : runtime ? 'Rotate connection' : 'Verify & connect'}
1818
          </button>
1819
        </div>
1820
        {runtime?.lastError ? <small>Last session error: {runtime.lastError}</small> : null}
1821
      </div>
1822
      {editing ? (
1823
        <div className="automationForm">
1824
          <label>
1825
            <span>
1826
              {temporary ? 'OpenClasp temporary A2A endpoint' : 'Agent-owned A2A endpoint'}
1827
            </span>
1828
            <input type="url" value={endpoint} readOnly />
1829
          </label>
1830
          <label>
1831
            <span>Invitation policy</span>
1832
            <select
1833
              value={autoAcceptPolicy}
1834
              onChange={(event) =>
1835
                setAutoAcceptPolicy(event.target.value as 'off' | 'safe_matching')
1836
              }
1837
            >
1838
              <option value="safe_matching">Auto-accept safe matches</option>
1839
              <option value="off">Review every request</option>
1840
            </select>
1841
          </label>
1842
          <label>
1843
            <span>Safe task categories</span>
1844
            <input
1845
              value={categories}
1846
              onChange={(event) => setCategories(event.target.value)}
1847
              placeholder="research, planning, coding"
1848
            />
1849
          </label>
1850
          <label className="checkControl">
1851
            <input
1852
              type="checkbox"
1853
              checked={autoPublish}
1854
              onChange={(event) => setAutoPublish(event.target.checked)}
1855
            />
1856
            <span>Publish and keep Agent Card updated</span>
1857
          </label>
1858
          <div className="agentActions">
1859
            <button className="secondary" type="button" onClick={() => setEditing(false)}>
1860
              Cancel
1861
            </button>
1862
            <button
1863
              className="primary"
1864
              type="button"
1865
              disabled={working || agent.status === 'revoked'}
1866
              onClick={() =>
1867
                void onSave({
1868
                  autoPublish,
1869
                  autoAcceptPolicy,
1870
                  autoAcceptTaskCategories: categories
1871
                    .split(',')
1872
                    .map((value) => value.trim())
1873
                    .filter(Boolean),
1874
                }).then((ok) => {
1875
                  if (ok) setEditing(false);
1876
                })
1877
              }
1878
            >
1879
              {working ? 'Saving…' : 'Save automation'}
1880
            </button>
1881
          </div>
1882
        </div>
1883
      ) : (
1884
        <div className="agentActions agentManagementActions">
1885
          <button
1886
            className="secondary dangerButton"
1887
            type="button"
1888
            disabled={working || runtimeWorking || deleteWorking}
1889
            onClick={onDelete}
1890
          >
1891
            {deleteWorking ? 'Deleting…' : 'Delete agent'}
1892
          </button>
1893
          <button
1894
            className="secondary"
1895
            type="button"
1896
            disabled={working || runtimeWorking || deleteWorking || agent.status === 'revoked'}
1897
            onClick={() => setEditing(true)}
1898
          >
1899
            Configure automation
1900
          </button>
1901
        </div>
1902
      )}
1903
    </article>
1904
  );
1905
}
1906
function Profile({ profile, expanded }: { profile: Record<string, any>; expanded?: boolean }) {
1907
  const dimensions = [
1908
    ['Completion', profile.completion],
1909
    ['Scope', profile.scope],
1910
    ['Evidence', profile.evidence],
1911
    ...(expanded
1912
      ? [
1913
          ['Communication', profile.communication],
1914
          ['Deadline', profile.deadline],
1915
        ]
1916
      : []),
1917
  ] as [string, number][];
1918
  return (
1919
    <article className="profile">
1920
      <div>
1921
        <strong>{profile.agentId}</strong>
1922
        <small>
1923
          {profile.taskCategory} · {profile.sampleSize} outcome(s)
1924
        </small>
1925
      </div>
1926
      {dimensions.map(([label, value]) => (
1927
        <Meter key={label} label={label} value={value ?? 0} />
1928
      ))}
1929
    </article>
1930
  );
1931
}
1932
function Meter({ label, value }: { label: string; value: number }) {
1933
  return (
1934
    <div className="meter">
1935
      <span>{label}</span>
1936
      <i>
1937
        <b style={{ width: `${Math.round(value * 100)}%` }} />
1938
      </i>
1939
      <strong>{Math.round(value * 100)}%</strong>
1940
    </div>
1941
  );
1942
}
1943
function Empty({
1944
  title,
1945
  text,
1946
  action,
1947
  onAction,
1948
}: {
1949
  title: string;
1950
  text: string;
1951
  action?: string;
1952
  onAction?: () => void;
1953
}) {
1954
  return (
1955
    <div className="empty">
1956
      <Icon name="agents" />
1957
      <strong>{title}</strong>
1958
      <p>{text}</p>
1959
      {action && (
1960
        <button className="secondary" type="button" onClick={onAction}>
1961
          {action}
1962
        </button>
1963
      )}
1964
    </div>
1965
  );
1966
}
1967
function TextButton({ children, onClick }: React.PropsWithChildren<{ onClick: () => void }>) {
1968
  return (
1969
    <button className="textButton" type="button" onClick={onClick}>
1970
      {children}
1971
    </button>
1972
  );
1973
}
1974
function Setting({
1975
  label,
1976
  description,
1977
  children,
1978
}: React.PropsWithChildren<{ label: string; description: string }>) {
1979
  return (
1980
    <div className="setting">
1981
      <div>
1982
        <strong>{label}</strong>
1983
        <p>{description}</p>
1984
      </div>
1985
      <div>{children}</div>
1986
    </div>
1987
  );
1988
}
1989
function Toggle({
1990
  checked,
1991
  onChange,
1992
  label,
1993
}: {
1994
  checked: boolean;
1995
  onChange: (checked: boolean) => void;
1996
  label?: string;
1997
}) {
1998
  return (
1999
    <button
2000
      className={checked ? 'toggle on' : 'toggle'}
2001
      type="button"
2002
      role="switch"
2003
      aria-checked={checked}
2004
      aria-label={label}
2005
      onClick={() => onChange(!checked)}
2006
    >
2007
      <span />
2008
    </button>
2009
  );
2010
}
2011
function Loading({ compact }: { compact?: boolean }) {
2012
  return (
2013
    <div className={compact ? 'loading compact' : 'loading'}>
2014
      <span className="spinner" aria-hidden="true" />
2015
      <p>Verifying session…</p>
2016
    </div>
2017
  );
2018
}
2019
2020
type IconName =
2021
  'home' | 'history' | 'agents' | 'insights' | 'connect' | 'settings' | 'sun' | 'moon';
2022
2023
function Icon({ name }: { name: IconName }) {
2024
  return (
2025
    <svg
2026
      viewBox="0 0 24 24"
2027
      fill="none"
2028
      stroke="currentColor"
2029
      strokeWidth="1.8"
2030
      strokeLinecap="round"
2031
      strokeLinejoin="round"
2032
      aria-hidden="true"
2033
    >
2034
      {name === 'home' && <path d="M4 11 12 4l8 7v9a1 1 0 0 1-1 1h-5v-6H10v6H5a1 1 0 0 1-1-1z" />}
2035
      {name === 'history' && (
2036
        <>
2037
          <path d="M4 12a8 8 0 1 0 2.2-5.5" />
2038
          <path d="M4 4v5h5" />
2039
          <path d="M12 8v5l3 2" />
2040
        </>
2041
      )}
2042
      {name === 'agents' && (
2043
        <>
2044
          <path d="M8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6" />
2045
          <path d="M16 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6" />
2046
          <path d="M4 20v-1a4 4 0 0 1 4-4h1" />
2047
          <path d="M20 20v-1a4 4 0 0 0-4-4h-1" />
2048
        </>
2049
      )}
2050
      {name === 'insights' && (
2051
        <>
2052
          <path d="M4 19h16" />
2053
          <path d="M7 16V9" />
2054
          <path d="M12 16V5" />
2055
          <path d="M17 16v-6" />
2056
        </>
2057
      )}
2058
      {name === 'connect' && (
2059
        <>
2060
          <path d="M12 5v14" />
2061
          <path d="M5 12h14" />
2062
        </>
2063
      )}
2064
      {name === 'settings' && (
2065
        <>
2066
          <circle cx="12" cy="12" r="3" />
2067
          <path d="M12 3v2M12 19v2M3 12h2M19 12h2M5.6 5.6l1.4 1.4M16.9 16.9l1.5 1.5M5.6 18.4l1.4-1.4M16.9 7.1l1.5-1.5" />
2068
        </>
2069
      )}
2070
      {name === 'sun' && (
2071
        <>
2072
          <circle cx="12" cy="12" r="4" />
2073
          <path d="M12 3v2M12 19v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M3 12h2M19 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" />
2074
        </>
2075
      )}
2076
      {name === 'moon' && <path d="M16 13a6 6 0 0 1-7.8-7.8A7 7 0 1 0 16 13" />}
2077
    </svg>
2078
  );
2079
}
2080
2081
function GoogleMark() {
2082
  return (
2083
    <svg viewBox="0 0 24 24" aria-hidden="true">
2084
      <path
2085
        fill="#4285F4"
2086
        d="M22.6 12.2c0-.8-.1-1.6-.2-2.3H12v4.4h5.9c-.3 1.4-1 2.6-2.2 3.4v2.8h3.6c2.1-1.9 3.3-4.8 3.3-8.3"
2087
      />
2088
      <path
2089
        fill="#34A853"
2090
        d="M12 23c3 0 5.5-1 7.3-2.7l-3.6-2.8c-1 .7-2.3 1.1-3.7 1.1-2.8 0-5.2-1.9-6.1-4.4H2.2v2.9C4 20.5 7.7 23 12 23"
2091
      />
2092
      <path
2093
        fill="#FBBC05"
2094
        d="M5.9 14.2c-.2-.7-.4-1.4-.4-2.2s.1-1.5.4-2.2V7H2.2C1.4 8.6 1 10.3 1 12s.4 3.4 1.2 5z"
2095
      />
2096
      <path
2097
        fill="#EA4335"
2098
        d="M12 4.8c1.6 0 3.1.6 4.2 1.7l3.2-3.2C17.5 1.5 15 0.5 12 .5 7.7.5 4 3 2.2 7l3.7 2.9C6.8 6.7 9.2 4.8 12 4.8"
2099
      />
2100
    </svg>
2101
  );
2102
}
2103
2104
function GitHubMark() {
2105
  return (
2106
    <svg viewBox="0 0 24 24" aria-hidden="true">
2107
      <path
2108
        fill="currentColor"
2109
        d="M12 2a10 10 0 0 0-3.2 19.5c.5.1.7-.2.7-.5v-1.7c-2.8.6-3.4-1.2-3.4-1.2-.5-1.1-1.1-1.4-1.1-1.4-.9-.6.1-.6.1-.6 1 .1 1.5 1 1.5 1 .9 1.5 2.4 1.1 3 .8.1-.6.4-1.1.6-1.3-2.2-.3-4.6-1.1-4.6-5a3.9 3.9 0 0 1 1-2.7 3.6 3.6 0 0 1 .1-2.6s.8-.3 2.7 1a9.3 9.3 0 0 1 5 0c1.9-1.3 2.7-1 2.7-1a3.6 3.6 0 0 1 .1 2.6 3.9 3.9 0 0 1 1 2.7c0 3.9-2.3 4.7-4.6 5 .4.3.7.9.7 1.9v2.8c0 .3.2.6.7.5A10 10 0 0 0 12 2"
2110
      />
2111
    </svg>
2112
  );
2113
}
2114
2115
function initials(value: string) {
2116
  return value
2117
    .split(/\s+|@/)
2118
    .slice(0, 2)
2119
    .map((part) => part[0]?.toUpperCase())
2120
    .join('');
2121
}
2122
function timestamp(item: Record<string, any>) {
2123
  return String(item.timestamp ?? item.completedAt ?? item.createdAt ?? new Date(0).toISOString());
2124
}
2125
2126
if (!__AUTH0_DOMAIN__ || !__AUTH0_CLIENT_ID__ || !__AUTH0_AUDIENCE__)
2127
  throw new Error('Auth0 public configuration is incomplete');
2128
createRoot(document.getElementById('root')!).render(
2129
  <React.StrictMode>
2130
    {location.pathname === '/sso-callback' ? <AuthCallback /> : <App />}
2131
  </React.StrictMode>,
2132
);