@flamingo-stack/openframe-frontend-core 0.0.722 → 0.0.724
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-XJYAUIOL.cjs → chunk-2A4AGW7Z.cjs} +11 -11
- package/dist/{chunk-65C4WHG4.js → chunk-2HLGX6JF.js} +5 -5
- package/dist/{chunk-5K6YKRP5.cjs → chunk-3MMKIVWZ.cjs} +23 -23
- package/dist/{chunk-P7Y626YJ.cjs → chunk-3NVSC3BU.cjs} +687 -611
- package/dist/{chunk-VSYQJCNR.js → chunk-4N5DLIEC.js} +1 -1
- package/dist/{chunk-GYSTZD6X.cjs → chunk-5CJGNPTT.cjs} +35 -35
- package/dist/{chunk-AWWLEYAU.js → chunk-6BKDXUOQ.js} +6 -6
- package/dist/{chunk-BUDZWXRG.js → chunk-6QO6BIX7.js} +1 -1
- package/dist/{chunk-FDWYBJYD.cjs → chunk-7AXOAA5T.cjs} +3 -3
- package/dist/{chunk-SZF7ENIW.js → chunk-7CNGNPBI.js} +3 -3
- package/dist/{chunk-6HRQLM6T.cjs → chunk-A4WHVIYZ.cjs} +17 -17
- package/dist/{chunk-VV5INIQB.cjs → chunk-BIWG3CYN.cjs} +3 -3
- package/dist/{chunk-I3UUMRO7.js → chunk-BWMEQ7IZ.js} +1 -1
- package/dist/{chunk-7UZ2JZJY.js → chunk-CJENFR5T.js} +29 -3
- package/dist/{chunk-IITZ6JSC.cjs → chunk-DGNGABNG.cjs} +33 -7
- package/dist/{chunk-KWBH63GV.cjs → chunk-FR4MFSH5.cjs} +19 -19
- package/dist/{chunk-DWSBTGKT.cjs → chunk-GRB5HVEV.cjs} +2610 -2125
- package/dist/{chunk-JQ55FFIK.cjs → chunk-IPUDFAU5.cjs} +31 -31
- package/dist/{chunk-F33RMJOL.js → chunk-IUQKIU5A.js} +6 -6
- package/dist/{chunk-LV6UJD2K.cjs → chunk-JKWQWHNV.cjs} +6 -1
- package/dist/{chunk-VC7COEWF.cjs → chunk-JQ7A7PO5.cjs} +76 -76
- package/dist/{chunk-BCU6UUNQ.js → chunk-KKO4GRM3.js} +4 -4
- package/dist/{chunk-RVQV6J5K.cjs → chunk-L5CAET76.cjs} +64 -64
- package/dist/{chunk-WY4SLTMZ.js → chunk-MRBPJ4NB.js} +1 -1
- package/dist/{chunk-IF72N4PE.js → chunk-MU5F5QXJ.js} +6 -6
- package/dist/{chunk-NMGWNUKB.js → chunk-N2PX26BZ.js} +2 -2
- package/dist/{chunk-NFABAOMD.cjs → chunk-N4NGKXUU.cjs} +15 -15
- package/dist/{chunk-ETQVXIYW.js → chunk-NLZZUWR6.js} +4 -4
- package/dist/{chunk-ZMHP4Y6M.js → chunk-OFOBVPDV.js} +2 -2
- package/dist/{chunk-DX3W7DPC.js → chunk-OWFDXYEO.js} +1 -1
- package/dist/{chunk-HA2ASGJM.js → chunk-PF4WAKCS.js} +4 -4
- package/dist/{chunk-GAFK3WNT.cjs → chunk-QJ67GFF3.cjs} +32 -32
- package/dist/{chunk-KBPGSPXQ.js → chunk-QN3OXRTM.js} +4492 -4007
- package/dist/{chunk-ZHC6MDGF.cjs → chunk-QUI7DKBW.cjs} +41 -41
- package/dist/{chunk-E7NIZXDT.js → chunk-S4SVKXHN.js} +88 -12
- package/dist/{chunk-VYHCN5NU.cjs → chunk-SOTXV3T5.cjs} +3 -3
- package/dist/{chunk-MJ4SXOG2.cjs → chunk-SVDXNMBL.cjs} +16 -16
- package/dist/{chunk-UYMTAFHG.js → chunk-VLPGV5WH.js} +1 -1
- package/dist/{chunk-SZ3CBFSH.js → chunk-XDTD6YGG.js} +7 -7
- package/dist/{chunk-VU7RU3LL.cjs → chunk-YKV45BSJ.cjs} +4 -4
- package/dist/{chunk-XR7NVJXS.cjs → chunk-ZKL52ALL.cjs} +3 -3
- package/dist/{chunk-EWED6ISQ.js → chunk-ZQKN2Z6W.js} +5 -0
- package/dist/{cloud-tenants-4Z36KWSX.js → cloud-tenants-5MINXS5J.js} +6 -6
- package/dist/{cloud-tenants-5NIC5VXK.cjs → cloud-tenants-UMZJZTMB.cjs} +7 -7
- package/dist/components/case-studies/index.cjs +20 -20
- package/dist/components/case-studies/index.js +7 -7
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +5 -5
- package/dist/components/chat/index.js +4 -4
- package/dist/components/chat/utils/icon-library.d.ts +2 -0
- package/dist/components/chat/utils/icon-library.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +8 -8
- package/dist/components/contact/index.js +7 -7
- package/dist/components/docs/index.cjs +11 -11
- package/dist/components/docs/index.js +10 -10
- package/dist/components/embeds/index.cjs +8 -8
- package/dist/components/embeds/index.js +7 -7
- package/dist/components/faq/index.cjs +8 -8
- package/dist/components/faq/index.js +7 -7
- package/dist/components/features/index.cjs +23 -7
- package/dist/components/features/index.d.ts +1 -0
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +22 -6
- package/dist/components/features/model-token-rates.d.ts +179 -0
- package/dist/components/features/model-token-rates.d.ts.map +1 -0
- package/dist/components/help-center-pages/index.cjs +130 -126
- package/dist/components/help-center-pages/index.js +47 -43
- package/dist/components/help-center-pages/trust-center-page.d.ts +34 -1
- package/dist/components/help-center-pages/trust-center-page.d.ts.map +1 -1
- package/dist/components/help-center-pages/trust-center-sections.d.ts +1 -1
- package/dist/components/help-center-pages/trust-center-sections.d.ts.map +1 -1
- package/dist/components/index.cjs +73 -55
- package/dist/components/index.js +36 -18
- package/dist/components/meeting-scheduler/index.cjs +32 -32
- package/dist/components/meeting-scheduler/index.js +6 -6
- package/dist/components/navigation/index.cjs +10 -10
- package/dist/components/navigation/index.js +9 -9
- package/dist/components/onboarding-guides/index.cjs +10 -10
- package/dist/components/onboarding-guides/index.js +9 -9
- package/dist/components/product-demo/index.cjs +7 -7
- package/dist/components/product-demo/index.js +7 -7
- package/dist/components/related-content/index.cjs +4 -4
- package/dist/components/related-content/index.js +3 -3
- package/dist/components/tickets/index.cjs +11 -11
- package/dist/components/tickets/index.js +10 -10
- package/dist/components/ui/data-attribution.d.ts +3 -1
- package/dist/components/ui/data-attribution.d.ts.map +1 -1
- package/dist/components/ui/download-button.d.ts +19 -0
- package/dist/components/ui/download-button.d.ts.map +1 -0
- package/dist/components/ui/index.cjs +9 -7
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +8 -6
- package/dist/{devices-2TK3LAKM.js → devices-FSSUAUQN.js} +6 -6
- package/dist/{devices-7IKT72DK.cjs → devices-HOT7YSDN.cjs} +8 -8
- package/dist/index.cjs +53 -11
- package/dist/index.js +52 -10
- package/dist/{knowledge-HYSEV35H.cjs → knowledge-3N6HCFLU.cjs} +8 -8
- package/dist/{knowledge-3GJ4MU3V.js → knowledge-7HRUJX26.js} +6 -6
- package/dist/{logs-ZX3NADQC.cjs → logs-75M5YP6V.cjs} +15 -15
- package/dist/{logs-TATVM62H.js → logs-T25OE2QW.js} +6 -6
- package/dist/{policies-MRRKLF4Q.js → policies-4OK4XTD4.js} +6 -6
- package/dist/{policies-J4FSDKPI.cjs → policies-BTG7UJJC.cjs} +11 -11
- package/dist/{remote-session-Z2YCUPHU.cjs → remote-session-4DTOM3QD.cjs} +5 -5
- package/dist/{remote-session-WCLR47RN.js → remote-session-AZJXG7ZF.js} +3 -3
- package/dist/{software-update-KM46WUVQ.js → software-update-5SOV4FIR.js} +6 -6
- package/dist/{software-update-CXUBWYD2.cjs → software-update-OZURO4VK.cjs} +11 -11
- package/dist/types/index.cjs +27 -2
- package/dist/types/index.js +25 -3
- package/dist/types/trust-center.d.ts +19 -2
- package/dist/types/trust-center.d.ts.map +1 -1
- package/dist/utils/ai-providers.d.ts +29 -0
- package/dist/utils/ai-providers.d.ts.map +1 -0
- package/dist/utils/index.cjs +76 -2
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +68 -3
- package/dist/utils/list-url.d.ts.map +1 -1
- package/dist/utils/model-token-rates.d.ts +55 -0
- package/dist/utils/model-token-rates.d.ts.map +1 -0
- package/dist/utils/source-icons.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/entity-cards/dispatch.tsx +3 -0
- package/src/components/chat/utils/icon-library.tsx +6 -0
- package/src/components/features/index.ts +1 -0
- package/src/components/features/model-token-rates.tsx +696 -0
- package/src/components/help-center-pages/trust-center-page.tsx +50 -29
- package/src/components/help-center-pages/trust-center-sections.tsx +2 -2
- package/src/components/ui/data-attribution.tsx +19 -6
- package/src/components/ui/download-button.tsx +34 -0
- package/src/components/ui/index.ts +1 -0
- package/src/types/trust-center.ts +52 -4
- package/src/utils/ai-providers.ts +46 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/list-url.ts +1 -0
- package/src/utils/model-token-rates.ts +103 -0
- package/src/utils/source-icons.ts +3 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* ONE page, not tabs (2026 trust-center practice: buyers skim and Ctrl-F, and a
|
|
7
7
|
* questionnaire needs a deep link per section): hero with the monitoring status
|
|
8
|
-
* and the
|
|
8
|
+
* (with the page's PDF as a quiet link at its end) and the Request access action, then anchored sections in reading order
|
|
9
9
|
* (`TRUST_CENTER_SECTIONS`: AI & data use → compliance → controls → documents →
|
|
10
10
|
* subprocessors → FAQ → questions) with a sticky section rail on desktop
|
|
11
11
|
* (`StickySectionNav` + `useScrollSpy`, the vendor-page / DocViewer pattern).
|
|
@@ -34,11 +34,12 @@ import { useSelfFetch } from '../../hooks/use-self-fetch';
|
|
|
34
34
|
import {
|
|
35
35
|
TRUST_CENTER_API_PATH,
|
|
36
36
|
TRUST_CENTER_CACHE_SECONDS,
|
|
37
|
-
TRUST_CENTER_SECTIONS,
|
|
38
37
|
TRUST_CENTER_TAGLINE,
|
|
39
38
|
TRUST_CENTER_TITLE,
|
|
40
|
-
|
|
39
|
+
trustCenterMonitoringStatus,
|
|
41
40
|
trustCenterDocumentUrl,
|
|
41
|
+
trustCenterPdfUrl,
|
|
42
|
+
visibleTrustCenterSections,
|
|
42
43
|
type TrustCenterControlsPage,
|
|
43
44
|
type TrustCenterPublic,
|
|
44
45
|
type TrustCenterSectionId,
|
|
@@ -50,6 +51,7 @@ import { PageShell } from '../layout/article-detail-layout';
|
|
|
50
51
|
import { PageLayout } from '../layout/page-layout';
|
|
51
52
|
import { StickySectionNav } from '../navigation/sticky-section-nav';
|
|
52
53
|
import { DataAttribution } from '../ui/data-attribution';
|
|
54
|
+
import { DownloadButton } from '../ui/download-button';
|
|
53
55
|
import { EntityImage } from '../ui/entity-image';
|
|
54
56
|
import { LoadError } from '../ui/error-state';
|
|
55
57
|
import { StatusIndicator } from '../ui/status-indicator';
|
|
@@ -85,6 +87,13 @@ export interface TrustCenterPageProps {
|
|
|
85
87
|
subtitle?: string;
|
|
86
88
|
/** The FAQ section's "ask the assistant" card (`FaqSection`'s `ask`): the host states its topic. Absent: no card. */
|
|
87
89
|
ask?: FaqSectionProps['ask'];
|
|
90
|
+
/**
|
|
91
|
+
* Host content shown after the trust sections and BEFORE the FAQ (the hub's
|
|
92
|
+
* own "why we build in the open" sections). The FAQ is the last thing on a
|
|
93
|
+
* page, so with this the page draws: trust sections, this content at the
|
|
94
|
+
* page's full width, then the FAQ. Absent: the FAQ closes the sections column.
|
|
95
|
+
*/
|
|
96
|
+
beforeFaq?: ReactNode;
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
/** One section's page content. Its `h2` is the `TRUST_CENTER_SECTIONS` label — never a literal here. */
|
|
@@ -93,20 +102,6 @@ interface TrustSectionView {
|
|
|
93
102
|
render: (data: TrustCenterPublic) => ReactNode;
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
/** Sections that have content, in page order. */
|
|
97
|
-
function visibleSections(data: TrustCenterPublic): Array<(typeof TRUST_CENTER_SECTIONS)[number]> {
|
|
98
|
-
const hasContent: Record<TrustCenterSectionId, boolean> = {
|
|
99
|
-
ai: data.aiPractices.length > 0,
|
|
100
|
-
compliance: data.frameworks.length > 0,
|
|
101
|
-
controls: data.controlDomains.length > 0,
|
|
102
|
-
documents: data.documents.length > 0,
|
|
103
|
-
subprocessors: data.subprocessors.length > 0,
|
|
104
|
-
faq: data.faqs.length > 0,
|
|
105
|
-
contact: true,
|
|
106
|
-
};
|
|
107
|
-
return TRUST_CENTER_SECTIONS.filter(section => hasContent[section.id]);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
105
|
/**
|
|
111
106
|
* The Controls section's first answer, from the page's own server data: the
|
|
112
107
|
* first category's controls with no query — exactly what the controls endpoint
|
|
@@ -136,12 +131,9 @@ function monitoringStatus(
|
|
|
136
131
|
hydrated: boolean,
|
|
137
132
|
nowMs: number,
|
|
138
133
|
): { status: 'success' | 'pending' | 'missing'; label: string } {
|
|
139
|
-
if (!data.connected) return { status: 'missing', label: 'Live control monitoring is not enabled yet' };
|
|
140
134
|
// Before mount the clock is the server's, so the text claims nothing either way.
|
|
141
|
-
if (!hydrated) return { status: 'missing', label: 'Checking monitoring status' };
|
|
142
|
-
return
|
|
143
|
-
? { status: 'success', label: 'Controls continuously monitored' }
|
|
144
|
-
: { status: 'pending', label: 'Monitoring paused' };
|
|
135
|
+
if (data.connected && !hydrated) return { status: 'missing', label: 'Checking monitoring status' };
|
|
136
|
+
return trustCenterMonitoringStatus(data, nowMs);
|
|
145
137
|
}
|
|
146
138
|
|
|
147
139
|
export function TrustCenterPage({
|
|
@@ -152,6 +144,7 @@ export function TrustCenterPage({
|
|
|
152
144
|
title = TRUST_CENTER_TITLE,
|
|
153
145
|
subtitle = TRUST_CENTER_TAGLINE,
|
|
154
146
|
ask,
|
|
147
|
+
beforeFaq,
|
|
155
148
|
}: TrustCenterPageProps) {
|
|
156
149
|
const { data, isLoading, error, reload } = useSelfFetch<TrustCenterPublic>(endpoint, {
|
|
157
150
|
initialData,
|
|
@@ -180,7 +173,7 @@ export function TrustCenterPage({
|
|
|
180
173
|
documentTitle: null,
|
|
181
174
|
});
|
|
182
175
|
|
|
183
|
-
const sections = useMemo(() => (data ?
|
|
176
|
+
const sections = useMemo(() => (data ? visibleTrustCenterSections(data) : []), [data]);
|
|
184
177
|
// Anchors. The scroll spy lights the rail from whatever scrolls the sections
|
|
185
178
|
// (the window, or a host shell's `<main overflow-y-auto>` — OpenFrame's
|
|
186
179
|
// `AppLayout`) and keeps the URL's hash on the section in view (`syncHash`;
|
|
@@ -205,9 +198,15 @@ export function TrustCenterPage({
|
|
|
205
198
|
const actions = hasGatedDocuments
|
|
206
199
|
? [{ label: 'Request access', variant: 'accent' as const, onClick: () => openRequest(null) }]
|
|
207
200
|
: undefined;
|
|
201
|
+
// The page as a file (the hub's `pdf` route beside `endpoint`). An occasional
|
|
202
|
+
// utility on a read-only page, so it is NOT a header action (a button there,
|
|
203
|
+
// and a fixed bar on a phone, are for the page's task: Request access). It is
|
|
204
|
+
// the shared small `DownloadButton` at the end of the status line under the
|
|
205
|
+
// title, at every width.
|
|
206
|
+
const download = <DownloadButton label="Download PDF" href={trustCenterPdfUrl(endpoint)} />;
|
|
208
207
|
|
|
209
208
|
// Section id → its content. Titles come from `TRUST_CENTER_SECTIONS`; which
|
|
210
|
-
// sections show comes from `
|
|
209
|
+
// sections show comes from `visibleTrustCenterSections` — this map only says what each renders.
|
|
211
210
|
const views: Record<TrustCenterSectionId, TrustSectionView> = {
|
|
212
211
|
ai: {
|
|
213
212
|
lead: TRUST_SECTION_LEADS.ai,
|
|
@@ -239,6 +238,11 @@ export function TrustCenterPage({
|
|
|
239
238
|
contact: { render: () => <ContactSection onContact={() => openRequest(null)} /> },
|
|
240
239
|
};
|
|
241
240
|
|
|
241
|
+
// With host content before it, the FAQ leaves the sections column and closes the page (below).
|
|
242
|
+
const faqSection = sections.find(section => section.id === 'faq');
|
|
243
|
+
const faqLast = Boolean(beforeFaq) && Boolean(faqSection);
|
|
244
|
+
const columnSections = faqLast ? sections.filter(section => section.id !== 'faq') : sections;
|
|
245
|
+
|
|
242
246
|
let body: ReactNode;
|
|
243
247
|
if (error && !data) {
|
|
244
248
|
body = <LoadError message="Could not load the trust center" onRetry={reload} />;
|
|
@@ -264,13 +268,16 @@ export function TrustCenterPage({
|
|
|
264
268
|
}
|
|
265
269
|
source={data.dataSource.name}
|
|
266
270
|
lastUpdated={data.syncedAt}
|
|
271
|
+
action={download}
|
|
267
272
|
/>
|
|
268
|
-
) :
|
|
273
|
+
) : (
|
|
274
|
+
download
|
|
275
|
+
)}
|
|
269
276
|
</div>
|
|
270
277
|
|
|
271
278
|
<div className={TRUST_BODY_GRID_CLASS}>
|
|
272
279
|
<div className={TRUST_SECTIONS_COLUMN_CLASS}>
|
|
273
|
-
{
|
|
280
|
+
{columnSections.map(section => {
|
|
274
281
|
const view = views[section.id];
|
|
275
282
|
return (
|
|
276
283
|
<TrustSection key={section.id} id={section.id} title={section.label} lead={view.lead}>
|
|
@@ -302,9 +309,23 @@ export function TrustCenterPage({
|
|
|
302
309
|
}
|
|
303
310
|
|
|
304
311
|
return (
|
|
305
|
-
|
|
306
|
-
{
|
|
307
|
-
|
|
312
|
+
<>
|
|
313
|
+
<TrustCenterChrome shell={shell} title={title} subtitle={subtitle} backButton={backButton} actions={actions}>
|
|
314
|
+
{body}
|
|
315
|
+
</TrustCenterChrome>
|
|
316
|
+
{beforeFaq}
|
|
317
|
+
{faqLast && faqSection && data ? (
|
|
318
|
+
// The page's own content box at its FULL width (no section-rail column): a closing FAQ takes the
|
|
319
|
+
// same space the FAQ block takes at the foot of every other page.
|
|
320
|
+
<div className={shell ? 'bg-ods-bg' : undefined}>
|
|
321
|
+
<div className={shell ? 'page-shell-content mx-auto max-w-[1920px]' : 'page-shell-content'}>
|
|
322
|
+
<TrustSection id={faqSection.id} title={faqSection.label} lead={views.faq.lead}>
|
|
323
|
+
{views.faq.render(data)}
|
|
324
|
+
</TrustSection>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
) : null}
|
|
328
|
+
</>
|
|
308
329
|
);
|
|
309
330
|
}
|
|
310
331
|
|
|
@@ -72,7 +72,7 @@ const BODY_TEXT = 'text-h6 text-ods-text-secondary';
|
|
|
72
72
|
* beside the section rail.
|
|
73
73
|
*/
|
|
74
74
|
export const TRUST_STATUS_ROW_CLASS =
|
|
75
|
-
'flex flex-col gap-[var(--spacing-system-
|
|
75
|
+
'flex flex-col gap-[var(--spacing-system-xsf)] content-md:flex-row content-md:items-center content-md:justify-between';
|
|
76
76
|
export const TRUST_BODY_GRID_CLASS =
|
|
77
77
|
'grid grid-cols-1 gap-[var(--spacing-system-xl)] content-lg:grid-cols-[minmax(0,1fr)_12rem]';
|
|
78
78
|
export const TRUST_SECTIONS_COLUMN_CLASS = 'flex min-w-0 flex-col gap-[var(--spacing-system-xxl)]';
|
|
@@ -675,7 +675,7 @@ export function TrustCenterSkeleton() {
|
|
|
675
675
|
<TextSkeleton.Body className="w-44" />
|
|
676
676
|
</div>
|
|
677
677
|
{/* `DataAttribution`'s own layout: two lines below `sm`, one row above it. */}
|
|
678
|
-
<div className="flex shrink-0 flex-col gap-[var(--spacing-system-
|
|
678
|
+
<div className="flex shrink-0 flex-col gap-[var(--spacing-system-xsf)] content-sm:flex-row content-sm:items-center content-sm:gap-[var(--spacing-system-sf)]">
|
|
679
679
|
<div className="flex items-center gap-[var(--spacing-system-xsf)]">
|
|
680
680
|
<UnifiedSkeleton className="size-4 rounded" />
|
|
681
681
|
<TextSkeleton.Body className="w-36" />
|
|
@@ -8,6 +8,20 @@ export interface DataAttributionProps {
|
|
|
8
8
|
source: string;
|
|
9
9
|
/** ISO instant of the last successful sync, or null when it never ran. */
|
|
10
10
|
lastUpdated: string | null;
|
|
11
|
+
/** One control at the end of the line: a way to take this data away (a `DownloadButton` for a file of it). */
|
|
12
|
+
action?: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const LINE_CLASS =
|
|
16
|
+
'flex shrink-0 flex-col gap-[var(--spacing-system-xsf)] text-ods-text-primary text-h6 content-sm:flex-row content-sm:items-center content-sm:gap-[var(--spacing-system-sf)]';
|
|
17
|
+
const ITEM_CLASS = 'flex items-center gap-[var(--spacing-system-xsf)]';
|
|
18
|
+
|
|
19
|
+
function Separator() {
|
|
20
|
+
return (
|
|
21
|
+
<span className="hidden text-ods-text-secondary content-sm:inline" aria-hidden="true">
|
|
22
|
+
·
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
/**
|
|
@@ -16,17 +30,16 @@ export interface DataAttributionProps {
|
|
|
16
30
|
* UTC (`formatDate`), so a server-rendered page and its hydrated copy print the
|
|
17
31
|
* same text.
|
|
18
32
|
*/
|
|
19
|
-
export function DataAttribution({ icon, source, lastUpdated }: DataAttributionProps) {
|
|
33
|
+
export function DataAttribution({ icon, source, lastUpdated, action }: DataAttributionProps) {
|
|
20
34
|
return (
|
|
21
|
-
<div className=
|
|
22
|
-
<div className=
|
|
35
|
+
<div className={LINE_CLASS}>
|
|
36
|
+
<div className={ITEM_CLASS}>
|
|
23
37
|
{icon}
|
|
24
38
|
<span>Data synced from {source}</span>
|
|
25
39
|
</div>
|
|
26
|
-
<
|
|
27
|
-
·
|
|
28
|
-
</span>
|
|
40
|
+
<Separator />
|
|
29
41
|
<span className="text-ods-text-secondary">Last updated: {lastUpdated ? formatDate(lastUpdated) : 'Never'}</span>
|
|
42
|
+
{action ? <div className="flex content-sm:pl-[var(--spacing-system-xs)]">{action}</div> : null}
|
|
30
43
|
</div>
|
|
31
44
|
);
|
|
32
45
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Download01Icon } from '../icons-v2-generated/interface/download-01-icon';
|
|
2
|
+
import { Button, type ButtonProps } from './button/button';
|
|
3
|
+
|
|
4
|
+
export type DownloadButtonProps = Omit<ButtonProps, 'leftIcon' | 'children'> & {
|
|
5
|
+
/** What is downloaded, as the button says it ("Download PDF"). */
|
|
6
|
+
label: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* THE download button: a regular small `Button` in the quiet `link` variant
|
|
11
|
+
* (no surface, secondary text like the facts it sits beside, the accent on
|
|
12
|
+
* hover) with the download glyph, wherever a surface hands the reader a file of
|
|
13
|
+
* what it shows (the Trust Center's PDF, the margin report's PDF). One look and
|
|
14
|
+
* one size, so a download reads the same on every page and never louder than
|
|
15
|
+
* the page's own action.
|
|
16
|
+
*
|
|
17
|
+
* With `href` it is a link that opens in a new tab (an attachment downloads
|
|
18
|
+
* there without leaving the page). With `onClick` the host fetches the file
|
|
19
|
+
* itself and passes `loading` while it does.
|
|
20
|
+
*/
|
|
21
|
+
export function DownloadButton({ label, variant = 'link', size = 'small', href, ...props }: DownloadButtonProps) {
|
|
22
|
+
return (
|
|
23
|
+
<Button
|
|
24
|
+
variant={variant}
|
|
25
|
+
size={size}
|
|
26
|
+
leftIcon={<Download01Icon aria-hidden="true" />}
|
|
27
|
+
href={href}
|
|
28
|
+
{...(href ? { openInNewTab: true, prefetch: false } : {})}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
{label}
|
|
32
|
+
</Button>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -108,6 +108,7 @@ export * from './device-card';
|
|
|
108
108
|
export * from './device-card-compact';
|
|
109
109
|
export * from './entity-image';
|
|
110
110
|
export * from './data-attribution';
|
|
111
|
+
export * from './download-button';
|
|
111
112
|
export * from './compliance-badge';
|
|
112
113
|
export * from './feature-card';
|
|
113
114
|
export * from './feature-list';
|
|
@@ -188,6 +188,21 @@ export function isTrustCenterMonitored(
|
|
|
188
188
|
return Number.isFinite(synced) && nowMs - synced <= data.monitoredWindowMs;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* THE monitoring claim and its wording, judged against a clock: monitored,
|
|
193
|
+
* paused, or not enabled. The page reads it after mount (its own clock) and the
|
|
194
|
+
* PDF when it is generated, so both say the same thing in the same words.
|
|
195
|
+
*/
|
|
196
|
+
export function trustCenterMonitoringStatus(
|
|
197
|
+
data: Pick<TrustCenterPublic, 'connected' | 'syncedAt' | 'monitoredWindowMs'>,
|
|
198
|
+
nowMs: number,
|
|
199
|
+
): { status: 'success' | 'pending' | 'missing'; label: string } {
|
|
200
|
+
if (!data.connected) return { status: 'missing', label: 'Live control monitoring is not enabled yet' };
|
|
201
|
+
return isTrustCenterMonitored(data, nowMs)
|
|
202
|
+
? { status: 'success', label: 'Controls continuously monitored' }
|
|
203
|
+
: { status: 'pending', label: 'Monitoring paused' };
|
|
204
|
+
}
|
|
205
|
+
|
|
191
206
|
/** "SOC 2 Type II: In progress · ISO 27001: Planned" — the ONE framework summary line (chat card, RAG mapper). */
|
|
192
207
|
export function trustFrameworksSummary(
|
|
193
208
|
frameworks: ReadonlyArray<Pick<TrustCenterFramework, 'label' | 'monitoring' | 'percent'>>,
|
|
@@ -195,10 +210,16 @@ export function trustFrameworksSummary(
|
|
|
195
210
|
return frameworks.map(f => `${f.label}: ${trustFrameworkBadge(f)}`).join(' · ');
|
|
196
211
|
}
|
|
197
212
|
|
|
213
|
+
/** A path BESIDE the page's `endpoint`, which may carry a query string (an embed proxy): `endpoint/segment?query`. */
|
|
214
|
+
function besideTrustCenterEndpoint(endpoint: string, segment: string): string {
|
|
215
|
+
const [path, query] = endpoint.split('?', 2);
|
|
216
|
+
return `${path}/${segment}${query ? `?${query}` : ''}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
198
219
|
/**
|
|
199
220
|
* Where a public document opens: its own link, or — for a file — the hub's
|
|
200
|
-
* documents route beside the page's `endpoint`
|
|
201
|
-
*
|
|
221
|
+
* documents route beside the page's `endpoint`. `null` for a document that is
|
|
222
|
+
* only available on request.
|
|
202
223
|
*/
|
|
203
224
|
export function trustCenterDocumentUrl(
|
|
204
225
|
endpoint: string,
|
|
@@ -206,8 +227,35 @@ export function trustCenterDocumentUrl(
|
|
|
206
227
|
): string | null {
|
|
207
228
|
if (document.access !== 'public') return null;
|
|
208
229
|
if (document.externalUrl) return document.externalUrl;
|
|
209
|
-
|
|
210
|
-
|
|
230
|
+
return besideTrustCenterEndpoint(endpoint, `documents/${encodeURIComponent(document.id)}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Where the page downloads as a PDF: the hub's `pdf` route beside the page's `endpoint`. */
|
|
234
|
+
export function trustCenterPdfUrl(endpoint: string): string {
|
|
235
|
+
return besideTrustCenterEndpoint(endpoint, 'pdf');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* THE rule for which sections a trust center shows, in reading order: a section
|
|
240
|
+
* with nothing to list is left out; Contact always shows. The page and its PDF
|
|
241
|
+
* both read it, so the two can never disagree about what is published.
|
|
242
|
+
*/
|
|
243
|
+
export function visibleTrustCenterSections(
|
|
244
|
+
data: Pick<
|
|
245
|
+
TrustCenterPublic,
|
|
246
|
+
'aiPractices' | 'frameworks' | 'controlDomains' | 'documents' | 'subprocessors' | 'faqs'
|
|
247
|
+
>,
|
|
248
|
+
): Array<(typeof TRUST_CENTER_SECTIONS)[number]> {
|
|
249
|
+
const hasContent: Record<TrustCenterSectionId, boolean> = {
|
|
250
|
+
ai: data.aiPractices.length > 0,
|
|
251
|
+
compliance: data.frameworks.length > 0,
|
|
252
|
+
controls: data.controlDomains.length > 0,
|
|
253
|
+
documents: data.documents.length > 0,
|
|
254
|
+
subprocessors: data.subprocessors.length > 0,
|
|
255
|
+
faq: data.faqs.length > 0,
|
|
256
|
+
contact: true,
|
|
257
|
+
};
|
|
258
|
+
return TRUST_CENTER_SECTIONS.filter(section => hasContent[section.id]);
|
|
211
259
|
}
|
|
212
260
|
|
|
213
261
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AI model providers the product names: what each is called and the NAME of
|
|
3
|
+
* its mark in the icon set. THE one owner of both, for every surface that shows
|
|
4
|
+
* a provider (the token rates, a model picker). Server-safe: it holds names
|
|
5
|
+
* only, and the mark is resolved from the icon set by that name where it is
|
|
6
|
+
* drawn (`findIcon`), so no surface imports a provider's icon.
|
|
7
|
+
*
|
|
8
|
+
* A host that is told a provider's name or mark by its server states them on
|
|
9
|
+
* the record and they win. A provider this list has not met is named from its
|
|
10
|
+
* key, and its mark is looked up by the icon set's own naming
|
|
11
|
+
* (`<provider>-logo-grey`), so a logo added to the set shows with no change here.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface AiProvider {
|
|
15
|
+
/** The name people know the provider by. */
|
|
16
|
+
label: string;
|
|
17
|
+
/** The icon set's name of the provider's monochrome mark. */
|
|
18
|
+
icon: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Keyed by the provider key billing sends (`ANTHROPIC`). */
|
|
22
|
+
export const AI_PROVIDERS: Readonly<Record<string, AiProvider>> = {
|
|
23
|
+
ANTHROPIC: { label: 'Anthropic', icon: 'anthropic-logo-grey' },
|
|
24
|
+
OPENAI: { label: 'OpenAI', icon: 'openai-logo-grey' },
|
|
25
|
+
GOOGLE_GEMINI: { label: 'Google Gemini', icon: 'gemini-logo-grey' },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const words = (key: string): string[] =>
|
|
29
|
+
key
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.split(/[^a-z0-9]+/)
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A provider by its key: what a host stated for it, else this list's entry,
|
|
36
|
+
* else its key read as a name and the mark the icon set would hold for it.
|
|
37
|
+
*/
|
|
38
|
+
export function aiProvider(key: string, stated?: { label?: string | null; icon?: string | null } | null): AiProvider {
|
|
39
|
+
const known = AI_PROVIDERS[key];
|
|
40
|
+
const parts = words(key);
|
|
41
|
+
return {
|
|
42
|
+
label:
|
|
43
|
+
stated?.label || known?.label || parts.map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') || key,
|
|
44
|
+
icon: stated?.icon || known?.icon || `${parts.join('-')}-logo-grey`,
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -17,6 +17,8 @@ export {
|
|
|
17
17
|
// source of truth). Re-exported here so existing callers that pull from
|
|
18
18
|
// the barrel keep working without changing imports.
|
|
19
19
|
export { formatDate, formatNumber, formatPrice, formatBytes, formatUrlForDisplay } from './format';
|
|
20
|
+
export * from './ai-providers';
|
|
21
|
+
export * from './model-token-rates';
|
|
20
22
|
export { TRIAL_DAYS, TRIAL_LABELS } from './trial';
|
|
21
23
|
export {
|
|
22
24
|
VENDOR_CLASSIFICATIONS,
|
package/src/utils/list-url.ts
CHANGED
|
@@ -118,6 +118,7 @@ const BUILDERS: Record<string, (ids: string[], base: string) => string> = {
|
|
|
118
118
|
prospect_call: (ids, b) => `${b}/api/prospect-calls?ids=${ids.join(',')}`,
|
|
119
119
|
// The public website's own pages (live source; ChatRef-shaped items like the objects above).
|
|
120
120
|
site_page: (ids, b) => `${b}/api/site-pages?ids=${ids.join(',')}`,
|
|
121
|
+
openframe_price: (ids, b) => `${b}/api/openframe-pricing?ids=${ids.join(',')}`,
|
|
121
122
|
// The OpenMSP vendor directory (indexed source).
|
|
122
123
|
vendor: (ids, b) => `${b}/api/vendors/cards?ids=${ids.join(',')}`,
|
|
123
124
|
// Code intelligence — per-object card hydration, same `handleEntityCardList`
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-model AI token exchange rates: their shape and how a rate is written.
|
|
3
|
+
* Server-safe (no client code), so a host's server (a chat source, a page's
|
|
4
|
+
* metadata) states a rate exactly as the shared table (`ModelTokenRates`) does.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { aiProvider } from './ai-providers';
|
|
8
|
+
|
|
9
|
+
/** One model's rates: OpenFrame tokens charged per ONE token of the model. */
|
|
10
|
+
export interface ModelTokenRate {
|
|
11
|
+
modelName: string;
|
|
12
|
+
/** The name people know the model by; the model's id stands in when it is missing. */
|
|
13
|
+
displayName?: string | null;
|
|
14
|
+
/** The provider's key (`ANTHROPIC`, `OPENAI`, `GOOGLE_GEMINI`); an unknown one is shown by name, without a mark. */
|
|
15
|
+
providerType: string;
|
|
16
|
+
/** The provider's name, when the host's server states one; else the shared provider list names it. */
|
|
17
|
+
providerLabel?: string | null;
|
|
18
|
+
/** The icon set's name of the provider's mark, when the host's server states one; else the shared provider list's. */
|
|
19
|
+
providerIcon?: string | null;
|
|
20
|
+
inputTokenRate: number;
|
|
21
|
+
outputTokenRate: number;
|
|
22
|
+
/** Charged for an input token read back from the prompt cache. Shown only where the table has room for it. */
|
|
23
|
+
cacheReadInputTokenRate?: number | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** What a figure that cannot be stated is written as. */
|
|
27
|
+
export const TOKEN_RATE_EMPTY = '\u2014';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A rate as a multiplier. Two decimals from 0.1 up ("1.33×", "5×"), two
|
|
31
|
+
* significant digits under it ("0.033×"), so a cheap model's rate is never
|
|
32
|
+
* rounded to nothing.
|
|
33
|
+
*/
|
|
34
|
+
export function formatTokenRate(value: number | null | undefined): string {
|
|
35
|
+
if (value == null || !Number.isFinite(value) || value <= 0) return TOKEN_RATE_EMPTY;
|
|
36
|
+
const rounded = value >= 0.1 ? Number(value.toFixed(2)) : Number(value.toPrecision(2));
|
|
37
|
+
return `${rounded}\u00d7`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What OpenFrame tokens cost: `price` USD buys `tokens` of them. */
|
|
41
|
+
export interface TokenPrice {
|
|
42
|
+
tokens: number;
|
|
43
|
+
price: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** What a number of OpenFrame tokens costs in USD at a price. Null without a price. */
|
|
47
|
+
export function tokenCost(tokens: number, tokenPrice: TokenPrice | null | undefined): number | null {
|
|
48
|
+
if (!tokenPrice || !(tokenPrice.tokens > 0) || !(tokenPrice.price > 0) || !(tokens >= 0)) return null;
|
|
49
|
+
return (tokens / tokenPrice.tokens) * tokenPrice.price;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The units a token count is shortened to, largest first. */
|
|
53
|
+
const TOKEN_AMOUNT_UNITS = [
|
|
54
|
+
{ size: 1_000_000_000, suffix: 'B' },
|
|
55
|
+
{ size: 1_000_000, suffix: 'M' },
|
|
56
|
+
{ size: 1_000, suffix: 'K' },
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A token count in short form: "850", "10K", "750K", "1.25M". The unit is chosen
|
|
61
|
+
* from the ROUNDED figure, so a count just under a unit reads as the next one
|
|
62
|
+
* ("1M", never "1000K").
|
|
63
|
+
*/
|
|
64
|
+
export function formatTokenAmount(tokens: number | null | undefined): string {
|
|
65
|
+
if (tokens == null || !Number.isFinite(tokens) || tokens < 0) return TOKEN_RATE_EMPTY;
|
|
66
|
+
const whole = Math.round(tokens);
|
|
67
|
+
if (whole < 1_000) return String(whole);
|
|
68
|
+
for (const [index, unit] of TOKEN_AMOUNT_UNITS.entries()) {
|
|
69
|
+
if (whole < unit.size) continue;
|
|
70
|
+
const figure = Number((whole / unit.size).toPrecision(3));
|
|
71
|
+
// Rounded up to a thousand of this unit: it is one of the unit above.
|
|
72
|
+
const larger = TOKEN_AMOUNT_UNITS[index - 1];
|
|
73
|
+
return figure >= 1_000 && larger ? `1${larger.suffix}` : `${figure}${unit.suffix}`;
|
|
74
|
+
}
|
|
75
|
+
return String(whole);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* How many of a model's tokens a balance of OpenFrame tokens runs at a rate:
|
|
80
|
+
* the balance divided by the rate. Null without a rate.
|
|
81
|
+
*/
|
|
82
|
+
export function tokensForBalance(balance: number, rate: number | null | undefined): number | null {
|
|
83
|
+
if (rate == null || !Number.isFinite(rate) || rate <= 0 || !(balance > 0)) return null;
|
|
84
|
+
return balance / rate;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A provider's name: what the host stated for it, else the shared provider list's (`aiProvider`). */
|
|
88
|
+
export function tokenRateProviderLabel(providerType: string, stated?: string | null): string {
|
|
89
|
+
return aiProvider(providerType, { label: stated }).label;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The rates under their providers, in the order the host gave them (providers by first appearance). */
|
|
93
|
+
export function groupTokenRatesByProvider<T extends ModelTokenRate>(
|
|
94
|
+
rates: readonly T[],
|
|
95
|
+
): { providerType: string; rates: T[] }[] {
|
|
96
|
+
const groups = new Map<string, T[]>();
|
|
97
|
+
for (const rate of rates) {
|
|
98
|
+
const group = groups.get(rate.providerType);
|
|
99
|
+
if (group) group.push(rate);
|
|
100
|
+
else groups.set(rate.providerType, [rate]);
|
|
101
|
+
}
|
|
102
|
+
return [...groups].map(([providerType, grouped]) => ({ providerType, rates: grouped }));
|
|
103
|
+
}
|
|
@@ -58,6 +58,7 @@ export const SOURCE_ICON_NAMES: Record<string, string> = {
|
|
|
58
58
|
'trust-center': 'shield-check',
|
|
59
59
|
// The public website's own pages (live source): the site's own mark, as a glyph in the slot's colour
|
|
60
60
|
'website-pages': 'flamingo-logo-grey',
|
|
61
|
+
'openframe-pricing': 'money-bill-dollar',
|
|
61
62
|
// The OpenMSP vendor directory: the icon the site menu's "Tool directory" entry draws (it reads this one)
|
|
62
63
|
vendors: 'package-search',
|
|
63
64
|
|
|
@@ -150,6 +151,7 @@ export const SOURCE_LABELS_BY_TABLE: Record<string, string> = {
|
|
|
150
151
|
// Trust
|
|
151
152
|
'trust-center': 'Trust Center',
|
|
152
153
|
'website-pages': 'Website',
|
|
154
|
+
'openframe-pricing': 'OpenFrame pricing',
|
|
153
155
|
vendors: 'Vendor Directory',
|
|
154
156
|
|
|
155
157
|
// Financials
|
|
@@ -255,6 +257,7 @@ export const DEFAULT_DOCUMENT_TYPE_TO_TABLE_ID: Record<string, string> = {
|
|
|
255
257
|
// Trust
|
|
256
258
|
trust_center: 'trust-center',
|
|
257
259
|
site_page: 'website-pages',
|
|
260
|
+
openframe_price: 'openframe-pricing',
|
|
258
261
|
vendor: 'vendors',
|
|
259
262
|
|
|
260
263
|
// Financials
|