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

Viewing files at commit 0fc0266ffa14Commit detailsView latest

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