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

Viewing files at commit bd8b77ebf65aCommit detailsView latest

apps/dashboard/src/main.tsx 781 lines · 24.0 KB · tsx
1
import {
2
  AuthenticateWithRedirectCallback,
3
  ClerkProvider,
4
  useAuth,
5
  useClerk,
6
  useUser,
7
} from '@clerk/react';
8
import { useSignIn } from '@clerk/react/legacy';
9
import React, { useCallback, useEffect, useMemo, useState } from 'react';
10
import { createRoot } from 'react-dom/client';
11
import './styles.css';
12
13
declare const __CLERK_PUBLISHABLE_KEY__: string;
14
15
type DashboardData = {
16
  agents: Record<string, any>[];
17
  interactions: Record<string, any>[];
18
  events: Record<string, any>[];
19
  conflicts: Record<string, any>[];
20
  receipts: Record<string, any>[];
21
  profiles: Record<string, any>[];
22
};
23
24
type Settings = {
25
  displayName: string;
26
  contributionEnabled: boolean;
27
  retentionDays: number;
28
  evidenceSharing: 'never' | 'ask' | 'contract_only';
29
  rawConversationsStored: false;
30
};
31
32
const emptyData: DashboardData = {
33
  agents: [],
34
  interactions: [],
35
  events: [],
36
  conflicts: [],
37
  receipts: [],
38
  profiles: [],
39
};
40
const defaultSettings: Settings = {
41
  displayName: '',
42
  contributionEnabled: false,
43
  retentionDays: 30,
44
  evidenceSharing: 'ask',
45
  rawConversationsStored: false,
46
};
47
const pages = ['dashboard', 'history', 'agents', 'insights', 'connect', 'settings'] as const;
48
type Page = (typeof pages)[number];
49
50
let getAuthToken: () => Promise<string | null> = async () => null;
51
52
async function api(path: string, init?: RequestInit) {
53
  const token = await getAuthToken();
54
  return fetch(path, {
55
    ...init,
56
    headers: {
57
      'content-type': 'application/json',
58
      ...(init?.headers ?? {}),
59
      ...(token ? { authorization: `Bearer ${token}` } : {}),
60
    },
61
  }).then(async (response) => {
62
    if (!response.ok)
63
      throw new Error((await response.text()) || `Request failed: ${response.status}`);
64
    return response.json();
65
  });
66
}
67
68
function route(): Page {
69
  const value = location.pathname.slice(1) || 'dashboard';
70
  return pages.includes(value as Page) ? (value as Page) : 'dashboard';
71
}
72
73
function App() {
74
  const { isLoaded: isAuthLoaded, isSignedIn, getToken } = useAuth();
75
  const { isLoaded: isUserLoaded, user } = useUser();
76
  const { signOut } = useClerk();
77
  const [page, setPage] = useState<Page>(route());
78
  const [data, setData] = useState<DashboardData>(emptyData);
79
  const [settings, setSettings] = useState<Settings>(defaultSettings);
80
  const [loading, setLoading] = useState(true);
81
  const [error, setError] = useState('');
82
83
  useEffect(() => {
84
    const onPopState = () => setPage(route());
85
    addEventListener('popstate', onPopState);
86
    return () => removeEventListener('popstate', onPopState);
87
  }, []);
88
89
  useEffect(() => {
90
    getAuthToken = getToken;
91
    if (isAuthLoaded && !isSignedIn && location.pathname !== '/login')
92
      history.replaceState({}, '', '/login');
93
    if (!isSignedIn) return;
94
    if (location.pathname === '/login') history.replaceState({}, '', '/dashboard');
95
    setPage(route());
96
    Promise.all([api('/v0.1/dashboard'), api('/v0.1/settings')])
97
      .then(([dashboard, accountSettings]) => {
98
        setData(dashboard as DashboardData);
99
        setSettings(accountSettings as Settings);
100
      })
101
      .catch((reason: unknown) =>
102
        setError(reason instanceof Error ? reason.message : 'Load failed'),
103
      )
104
      .finally(() => setLoading(false));
105
  }, [getToken, isAuthLoaded, isSignedIn]);
106
107
  const navigate = (next: Page) => {
108
    history.pushState({}, '', `/${next}`);
109
    setPage(next);
110
  };
111
112
  if (!isAuthLoaded || !isUserLoaded) return <Loading />;
113
  if (!isSignedIn) return <Login />;
114
115
  return (
116
    <div className="appShell">
117
      <aside>
118
        <div className="brand">
119
          <div className="mark">OC</div>
120
          <div>
121
            <strong>OpenClasp</strong>
122
            <small>ASSURANCE NETWORK</small>
123
          </div>
124
        </div>
125
        <nav>
126
          <Nav page="dashboard" active={page} onClick={navigate} label="Overview" glyph="⌂" />
127
          <Nav page="history" active={page} onClick={navigate} label="History" glyph="≡" />
128
          <Nav page="agents" active={page} onClick={navigate} label="Agents" glyph="◇" />
129
          <Nav page="insights" active={page} onClick={navigate} label="Insights" glyph="⌁" />
130
          <Nav page="connect" active={page} onClick={navigate} label="Connect" glyph="+" />
131
          <Nav page="settings" active={page} onClick={navigate} label="Settings" glyph="⚙" />
132
        </nav>
133
        <div className="privacyStamp">
134
          <span className="liveDot" />
135
          <div>
136
            <strong>Structured-only</strong>
137
            <small>Raw conversations stay local</small>
138
          </div>
139
        </div>
140
        <button className="account" onClick={() => void signOut({ redirectUrl: '/login' })}>
141
          <span>{initials(user?.fullName || user?.primaryEmailAddress?.emailAddress || 'OC')}</span>
142
          <div>
143
            <strong>{user?.fullName || 'OpenClasp user'}</strong>
144
            <small>{user?.primaryEmailAddress?.emailAddress || 'Sign out'}</small>
145
          </div>
146
          <b>↗</b>
147
        </button>
148
      </aside>
149
      <main>
150
        {error && <div className="errorBar">{error}</div>}
151
        {loading ? (
152
          <Loading compact />
153
        ) : (
154
          <PageContent
155
            page={page}
156
            data={data}
157
            settings={settings}
158
            setSettings={setSettings}
159
            navigate={navigate}
160
          />
161
        )}
162
      </main>
163
    </div>
164
  );
165
}
166
167
function Login() {
168
  const { isLoaded, signIn } = useSignIn();
169
  const [error, setError] = useState('');
170
  const continueWith = async (provider: 'google' | 'github') => {
171
    setError('');
172
    try {
173
      if (!isLoaded) return;
174
      await signIn.authenticateWithRedirect({
175
        strategy: provider === 'google' ? 'oauth_google' : 'oauth_github',
176
        redirectUrl: '/sso-callback',
177
        redirectUrlComplete: '/dashboard',
178
      });
179
    } catch {
180
      setError(`${provider === 'google' ? 'Google' : 'GitHub'} sign-in is not configured yet.`);
181
    }
182
  };
183
  return (
184
    <div className="loginPage">
185
      <section className="loginPitch">
186
        <div className="brand">
187
          <div className="mark">OC</div>
188
          <strong>OpenClasp</strong>
189
        </div>
190
        <div>
191
          <p className="eyebrow">AGENT-TO-AGENT ASSURANCE</p>
192
          <h1>Know who your agent is dealing with.</h1>
193
          <p>
194
            Verified identities, signed outcomes, private warnings, and contextual
195
            reliability—without collecting raw conversations.
196
          </p>
197
        </div>
198
        <div className="loginProof">
199
          <span>01</span> User-owned history<span>02</span> No universal trust score<span>03</span>{' '}
200
          Explicit network consent
201
        </div>
202
      </section>
203
      <section className="loginCard">
204
        <div>
205
          <p className="eyebrow">SECURE ACCESS</p>
206
          <h2>Sign in to OpenClasp</h2>
207
          <p>
208
            Manage connected agents, review signed history, and control what contributes to the
209
            network.
210
          </p>
211
        </div>
212
        <div className="socialButtons">
213
          <button onClick={() => void continueWith('google')}>
214
            <span className="google">G</span> Continue with Google
215
          </button>
216
          <button onClick={() => void continueWith('github')}>
217
            <span>◉</span> Continue with GitHub
218
          </button>
219
        </div>
220
        {error && <div className="loginError">{error}</div>}
221
        <small>
222
          Google and GitHub handle authentication. OpenClasp never receives your password.
223
        </small>
224
      </section>
225
    </div>
226
  );
227
}
228
229
function PageContent({
230
  page,
231
  data,
232
  settings,
233
  setSettings,
234
  navigate,
235
}: {
236
  page: Page;
237
  data: DashboardData;
238
  settings: Settings;
239
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
240
  navigate: (page: Page) => void;
241
}) {
242
  if (page === 'history') return <History data={data} />;
243
  if (page === 'agents') return <Agents data={data} navigate={navigate} />;
244
  if (page === 'insights') return <Insights data={data} />;
245
  if (page === 'connect') return <Connect />;
246
  if (page === 'settings') return <SettingsPage settings={settings} setSettings={setSettings} />;
247
  return <Overview data={data} navigate={navigate} />;
248
}
249
250
function Overview({ data, navigate }: { data: DashboardData; navigate: (page: Page) => void }) {
251
  const completed = data.receipts.filter((item) => item.outcome === 'success').length;
252
  const warnings = data.events.filter((item) =>
253
    ['policy_warning', 'policy_violation', 'objection'].includes(String(item.eventType)),
254
  ).length;
255
  return (
256
    <>
257
      <PageHead
258
        eyebrow="NETWORK OVERVIEW"
259
        title="Assurance, at a glance."
260
        action="Connect agent"
261
        onAction={() => navigate('connect')}
262
      />
263
      <section className="metrics">
264
        <Metric label="Connected agents" value={data.agents.length} note="verified identities" />
265
        <Metric label="Interactions" value={data.interactions.length} note="signed or active" />
266
        <Metric label="Successful outcomes" value={completed} note="receipt-backed" />
267
        <Metric label="Open warnings" value={warnings} note="needs attention" warn={warnings > 0} />
268
      </section>
269
      <section className="contentGrid">
270
        <Panel title="Recent activity" subtitle="Structured events and signed outcomes">
271
          <Timeline events={data.events.slice(-6).reverse()} />
272
          <TextButton onClick={() => navigate('history')}>View complete history →</TextButton>
273
        </Panel>
274
        <Panel
275
          title="Reliability context"
276
          subtitle="Task-specific profiles, never a universal score"
277
        >
278
          {data.profiles.length ? (
279
            data.profiles
280
              .slice(0, 4)
281
              .map((profile) => (
282
                <Profile key={`${profile.agentId}-${profile.taskCategory}`} profile={profile} />
283
              ))
284
          ) : (
285
            <Empty
286
              title="No reliability profile yet"
287
              text="Profiles appear after signed receipts and eligible bilateral feedback."
288
            />
289
          )}
290
        </Panel>
291
      </section>
292
    </>
293
  );
294
}
295
296
function History({ data }: { data: DashboardData }) {
297
  const records = useMemo(
298
    () =>
299
      [
300
        ...data.interactions.map((value) => ({ ...value, _kind: 'interaction' })),
301
        ...data.events.map((value) => ({ ...value, _kind: 'event' })),
302
        ...data.receipts.map((value) => ({ ...value, _kind: 'receipt' })),
303
        ...data.conflicts.map((value) => ({ ...value, _kind: 'dispute' })),
304
      ].sort((a, b) => Date.parse(timestamp(b)) - Date.parse(timestamp(a))),
305
    [data],
306
  );
307
  return (
308
    <>
309
      <PageHead eyebrow="AUDIT HISTORY" title="What happened, and what proves it." />
310
      <Panel
311
        title="Account history"
312
        subtitle="Only structured metadata, signatures, hashes, and permitted evidence are hosted"
313
      >
314
        {records.length ? (
315
          records.map((item, index) => (
316
            <HistoryRow
317
              key={String(
318
                (item as Record<string, any>).eventId ??
319
                  (item as Record<string, any>).receiptId ??
320
                  (item as Record<string, any>).interactionId ??
321
                  index,
322
              )}
323
              item={item}
324
            />
325
          ))
326
        ) : (
327
          <Empty
328
            title="No interactions recorded"
329
            text="Connect an agent and start an assured interaction. Raw message bodies will not appear here."
330
          />
331
        )}
332
      </Panel>
333
    </>
334
  );
335
}
336
337
function Agents({ data, navigate }: { data: DashboardData; navigate: (page: Page) => void }) {
338
  return (
339
    <>
340
      <PageHead
341
        eyebrow="IDENTITY REGISTRY"
342
        title="Your connected agents."
343
        action="Connect agent"
344
        onAction={() => navigate('connect')}
345
      />
346
      <section className="agentGrid">
347
        {data.agents.length ? (
348
          data.agents.map((agent) => <AgentCard key={agent.agentId} agent={agent} />)
349
        ) : (
350
          <Empty
351
            title="No agents connected"
352
            text="Install the remote MCP endpoint in an agent, sign in, then create and register its identity."
353
            action="Open connection guide"
354
            onAction={() => navigate('connect')}
355
          />
356
        )}
357
      </section>
358
    </>
359
  );
360
}
361
362
function Insights({ data }: { data: DashboardData }) {
363
  return (
364
    <>
365
      <PageHead eyebrow="BEHAVIOURAL CONTEXT" title="Evidence, not reputation theatre." />
366
      <div className="notice">
367
        <strong>Context matters.</strong> OpenClasp profiles an agent by task category and version.
368
        It does not produce a single universal trust score.
369
      </div>
370
      <section className="agentGrid">
371
        {data.profiles.length ? (
372
          data.profiles.map((profile) => (
373
            <Panel
374
              key={`${profile.agentId}-${profile.taskCategory}`}
375
              title={profile.agentId}
376
              subtitle={`${profile.taskCategory} · v${profile.agentVersion}`}
377
            >
378
              <Profile profile={profile} expanded />
379
            </Panel>
380
          ))
381
        ) : (
382
          <Empty
383
            title="Not enough verified evidence"
384
            text="Insights grow only from signed outcomes, eligible feedback, corrections, disputes, and version-aware history."
385
          />
386
        )}
387
      </section>
388
    </>
389
  );
390
}
391
392
function Connect() {
393
  const endpoint = 'https://openclasp.vercel.app/mcp';
394
  const [copied, setCopied] = useState(false);
395
  return (
396
    <>
397
      <PageHead eyebrow="AGENT CONNECTION" title="Add OpenClasp to an agent." />
398
      <section className="connectLayout">
399
        <Panel title="Remote MCP endpoint" subtitle="For OAuth-capable MCP clients">
400
          <div className="endpoint">
401
            <code>{endpoint}</code>
402
            <button
403
              onClick={() => {
404
                void navigator.clipboard.writeText(endpoint);
405
                setCopied(true);
406
              }}
407
            >
408
              {copied ? 'Copied' : 'Copy'}
409
            </button>
410
          </div>
411
          <ol>
412
            <li>Add the URL as a remote MCP server in your agent or framework.</li>
413
            <li>The client discovers OAuth and opens the Descope login page.</li>
414
            <li>Sign in with this account and approve access.</li>
415
            <li>Ask the agent to create and register its OpenClasp identity.</li>
416
          </ol>
417
        </Panel>
418
        <Panel
419
          title="What the agent receives"
420
          subtitle="Assurance tools, not a replacement transport"
421
        >
422
          <ul className="checkList">
423
            <li>Identity and delegation verification</li>
424
            <li>Counterparty-specific reliability clues</li>
425
            <li>Signed contracts, events, feedback, and receipts</li>
426
            <li>Policy challenges and consented mediation</li>
427
            <li>No hidden rewriting or raw-message upload</li>
428
          </ul>
429
        </Panel>
430
      </section>
431
    </>
432
  );
433
}
434
435
function SettingsPage({
436
  settings,
437
  setSettings,
438
}: {
439
  settings: Settings;
440
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
441
}) {
442
  const [saving, setSaving] = useState(false);
443
  const [saved, setSaved] = useState(false);
444
  const save = useCallback(async () => {
445
    setSaving(true);
446
    setSaved(false);
447
    const updated = (await api('/v0.1/settings', {
448
      method: 'PUT',
449
      body: JSON.stringify(settings),
450
    })) as Settings;
451
    setSettings(updated);
452
    setSaving(false);
453
    setSaved(true);
454
  }, [settings, setSettings]);
455
  return (
456
    <>
457
      <PageHead eyebrow="ACCOUNT CONTROLS" title="Privacy and network settings." />
458
      <section className="settingsCard">
459
        <Setting label="Display name" description="Shown only inside your OpenClasp account.">
460
          <input
461
            value={settings.displayName}
462
            onChange={(event) =>
463
              setSettings((value) => ({ ...value, displayName: event.target.value }))
464
            }
465
            placeholder="Team or operator name"
466
          />
467
        </Setting>
468
        <Setting
469
          label="Network contribution"
470
          description="Allow eligible structured aggregates to improve contextual intelligence. Raw conversations are never included."
471
        >
472
          <Toggle
473
            checked={settings.contributionEnabled}
474
            onChange={(checked) =>
475
              setSettings((value) => ({ ...value, contributionEnabled: checked }))
476
            }
477
          />
478
        </Setting>
479
        <Setting
480
          label="Evidence sharing"
481
          description="Controls when permitted evidence references may be shared with interaction participants."
482
        >
483
          <select
484
            value={settings.evidenceSharing}
485
            onChange={(event) =>
486
              setSettings((value) => ({
487
                ...value,
488
                evidenceSharing: event.target.value as Settings['evidenceSharing'],
489
              }))
490
            }
491
          >
492
            <option value="never">Never</option>
493
            <option value="ask">Ask every time</option>
494
            <option value="contract_only">When contract permits</option>
495
          </select>
496
        </Setting>
497
        <Setting
498
          label="Structured record retention"
499
          description="Retention for hosted account records. Signed protocol records may require separate revocation markers."
500
        >
501
          <select
502
            value={settings.retentionDays}
503
            onChange={(event) =>
504
              setSettings((value) => ({ ...value, retentionDays: Number(event.target.value) }))
505
            }
506
          >
507
            <option value={7}>7 days</option>
508
            <option value={30}>30 days</option>
509
            <option value={90}>90 days</option>
510
            <option value={365}>1 year</option>
511
            <option value={0}>No automatic deletion</option>
512
          </select>
513
        </Setting>
514
        <Setting
515
          label="Raw conversations"
516
          description="Message bodies remain local and user-owned. This cannot be enabled on the hosted service."
517
        >
518
          <span className="locked">ALWAYS OFF</span>
519
        </Setting>
520
        <div className="saveRow">
521
          <button className="primary" onClick={() => void save()} disabled={saving}>
522
            {saving ? 'Saving…' : 'Save settings'}
523
          </button>
524
          {saved && <span>Saved</span>}
525
        </div>
526
      </section>
527
    </>
528
  );
529
}
530
531
function PageHead({
532
  eyebrow,
533
  title,
534
  action,
535
  onAction,
536
}: {
537
  eyebrow: string;
538
  title: string;
539
  action?: string;
540
  onAction?: () => void;
541
}) {
542
  return (
543
    <header className="pageHead">
544
      <div>
545
        <p className="eyebrow">{eyebrow}</p>
546
        <h1>{title}</h1>
547
      </div>
548
      {action && (
549
        <button className="primary" onClick={onAction}>
550
          {action}
551
        </button>
552
      )}
553
    </header>
554
  );
555
}
556
function Nav({
557
  page,
558
  active,
559
  onClick,
560
  label,
561
  glyph,
562
}: {
563
  page: Page;
564
  active: Page;
565
  onClick: (page: Page) => void;
566
  label: string;
567
  glyph: string;
568
}) {
569
  return (
570
    <button className={active === page ? 'active' : ''} onClick={() => onClick(page)}>
571
      <span>{glyph}</span>
572
      {label}
573
    </button>
574
  );
575
}
576
function Metric({
577
  label,
578
  value,
579
  note,
580
  warn,
581
}: {
582
  label: string;
583
  value: number;
584
  note: string;
585
  warn?: boolean;
586
}) {
587
  return (
588
    <article className={warn ? 'metric warn' : 'metric'}>
589
      <span>{label}</span>
590
      <strong>{String(value).padStart(2, '0')}</strong>
591
      <small>{note}</small>
592
    </article>
593
  );
594
}
595
function Panel({
596
  title,
597
  subtitle,
598
  children,
599
}: React.PropsWithChildren<{ title: string; subtitle: string }>) {
600
  return (
601
    <section className="panel">
602
      <div className="panelHead">
603
        <div>
604
          <h2>{title}</h2>
605
          <p>{subtitle}</p>
606
        </div>
607
      </div>
608
      {children}
609
    </section>
610
  );
611
}
612
function Timeline({ events }: { events: Record<string, any>[] }) {
613
  return events.length ? (
614
    <div className="timeline">
615
      {events.map((event) => (
616
        <HistoryRow key={event.eventId} item={{ ...event, _kind: 'event' }} />
617
      ))}
618
    </div>
619
  ) : (
620
    <Empty
621
      title="Nothing recorded yet"
622
      text="Activity appears after your connected agents submit signed structured events."
623
    />
624
  );
625
}
626
function HistoryRow({ item }: { item: Record<string, any> }) {
627
  const kind = String(item.eventType ?? item._kind ?? 'record').replaceAll('_', ' ');
628
  const id = String(item.agentId ?? item.interactionId ?? item.receiptId ?? 'OpenClasp');
629
  return (
630
    <article className="historyRow">
631
      <span className={`eventDot ${item.eventType ?? item._kind}`} />
632
      <div>
633
        <strong>{kind}</strong>
634
        <small>
635
          {id} · {new Date(timestamp(item)).toLocaleString()}
636
        </small>
637
      </div>
638
      <b>{item.visibility ?? item.outcome ?? item.status ?? 'SIGNED'}</b>
639
    </article>
640
  );
641
}
642
function AgentCard({ agent }: { agent: Record<string, any> }) {
643
  return (
644
    <article className="agentCard">
645
      <div className="agentTop">
646
        <span className="agentGlyph">◇</span>
647
        <b className={agent.revoked ? 'bad' : ''}>{agent.revoked ? 'REVOKED' : 'VERIFIED'}</b>
648
      </div>
649
      <h2>{agent.agentId}</h2>
650
      <p>Version {agent.agentVersion}</p>
651
      <div className="tags">
652
        {(agent.capabilities ?? []).slice(0, 4).map((capability: string) => (
653
          <span key={capability}>{capability}</span>
654
        ))}
655
      </div>
656
      <small>Created {new Date(agent.createdAt).toLocaleDateString()}</small>
657
    </article>
658
  );
659
}
660
function Profile({ profile, expanded }: { profile: Record<string, any>; expanded?: boolean }) {
661
  const dimensions = [
662
    ['Completion', profile.completion],
663
    ['Scope', profile.scope],
664
    ['Evidence', profile.evidence],
665
    ...(expanded
666
      ? [
667
          ['Communication', profile.communication],
668
          ['Deadline', profile.deadline],
669
        ]
670
      : []),
671
  ] as [string, number][];
672
  return (
673
    <article className="profile">
674
      <div>
675
        <strong>{profile.agentId}</strong>
676
        <small>
677
          {profile.taskCategory} · {profile.sampleSize} outcome(s)
678
        </small>
679
      </div>
680
      {dimensions.map(([label, value]) => (
681
        <Meter key={label} label={label} value={value ?? 0} />
682
      ))}
683
    </article>
684
  );
685
}
686
function Meter({ label, value }: { label: string; value: number }) {
687
  return (
688
    <div className="meter">
689
      <span>{label}</span>
690
      <i>
691
        <b style={{ width: `${Math.round(value * 100)}%` }} />
692
      </i>
693
      <strong>{Math.round(value * 100)}%</strong>
694
    </div>
695
  );
696
}
697
function Empty({
698
  title,
699
  text,
700
  action,
701
  onAction,
702
}: {
703
  title: string;
704
  text: string;
705
  action?: string;
706
  onAction?: () => void;
707
}) {
708
  return (
709
    <div className="empty">
710
      <span>◇</span>
711
      <strong>{title}</strong>
712
      <p>{text}</p>
713
      {action && (
714
        <button className="secondary" onClick={onAction}>
715
          {action}
716
        </button>
717
      )}
718
    </div>
719
  );
720
}
721
function TextButton({ children, onClick }: React.PropsWithChildren<{ onClick: () => void }>) {
722
  return (
723
    <button className="textButton" onClick={onClick}>
724
      {children}
725
    </button>
726
  );
727
}
728
function Setting({
729
  label,
730
  description,
731
  children,
732
}: React.PropsWithChildren<{ label: string; description: string }>) {
733
  return (
734
    <div className="setting">
735
      <div>
736
        <strong>{label}</strong>
737
        <p>{description}</p>
738
      </div>
739
      <div>{children}</div>
740
    </div>
741
  );
742
}
743
function Toggle({ checked, onChange }: { checked: boolean; onChange: (checked: boolean) => void }) {
744
  return (
745
    <button
746
      className={checked ? 'toggle on' : 'toggle'}
747
      role="switch"
748
      aria-checked={checked}
749
      onClick={() => onChange(!checked)}
750
    >
751
      <span />
752
    </button>
753
  );
754
}
755
function Loading({ compact }: { compact?: boolean }) {
756
  return (
757
    <div className={compact ? 'loading compact' : 'loading'}>
758
      <span className="mark">OC</span>
759
      <p>Verifying session…</p>
760
    </div>
761
  );
762
}
763
function initials(value: string) {
764
  return value
765
    .split(/\s+|@/)
766
    .slice(0, 2)
767
    .map((part) => part[0]?.toUpperCase())
768
    .join('');
769
}
770
function timestamp(item: Record<string, any>) {
771
  return String(item.timestamp ?? item.completedAt ?? item.createdAt ?? new Date(0).toISOString());
772
}
773
774
if (!__CLERK_PUBLISHABLE_KEY__) throw new Error('Clerk publishable key is not configured');
775
createRoot(document.getElementById('root')!).render(
776
  <React.StrictMode>
777
    <ClerkProvider publishableKey={__CLERK_PUBLISHABLE_KEY__} afterSignOutUrl="/login">
778
      {location.pathname === '/sso-callback' ? <AuthenticateWithRedirectCallback /> : <App />}
779
    </ClerkProvider>
780
  </React.StrictMode>,
781
);