failproofai 0.0.11-beta.8 → 0.0.11-beta.9
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +6 -6
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/routes-manifest.json +6 -0
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/audit/invite/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/audit/invite/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +7 -0
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/audit/invite/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/audit/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +3 -3
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +3 -3
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_mqemn._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1r1h8v9._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1uatkiv._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1y6gxxb._.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_audit_invite_route_actions_0-2n5sy.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_0-tu4ot._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_0ttxbz7._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_1bnh1y0._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_1epycqa._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_1wpdcgo._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0808sha._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1cd25c7._.js → [root-of-the-server]__0e4-6d8._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1d4gx_t._.js → [root-of-the-server]__0e446gb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ehe24g._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1-scthx._.js → [root-of-the-server]__0f62vu9._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g253ve._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0l13qf2._.js → [root-of-the-server]__0k65l27._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0vxf0_g._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__15i0juc._.js → [root-of-the-server]__0wprfyc._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12mcauo._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0989_dx._.js → [root-of-the-server]__1e-x7j4._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mt35_w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pcxxwg._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1uvfwgr._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0il3fl1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +21 -21
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_0y4a-0q.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/static/chunks/{1nd1e30h8s_mc.js → 07_d165p5h5ys.js} +1 -1
- package/.next/standalone/.next/static/chunks/0f7d7hnbh4djs.js +1 -0
- package/.next/standalone/.next/static/chunks/0h7auy7hzjyhw.js +1 -0
- package/.next/standalone/.next/static/chunks/168k-8z6k7e8z.css +1 -0
- package/.next/standalone/.next/static/chunks/{24z-bgbisv379.js → 1kvadxkgnapyj.js} +1 -1
- package/.next/standalone/.next/static/chunks/{29gs4efgi3hme.js → 277oc363p56n6.js} +2 -2
- package/.next/standalone/.next/static/chunks/28mkxkl_d91-l.js +1 -0
- package/.next/standalone/.next/static/chunks/28x7jvo3kxd3u.js +41 -0
- package/.next/standalone/.next/static/chunks/29nrs5xs9c4hx.css +2 -0
- package/.next/standalone/.next/static/chunks/{2mni177pnjx6u.js → 29tg7deqmq32l.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3w8d8k_dca5rp.js → 2h0dkzyy0vocp.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0j969hb6nujdf.js → 2z42u62k-8-_q.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1m2yj97j7f_km.js → 3nj6g3xu9uy78.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0d49wc5zca0u1.js → 3ty6dhcuogout.js} +1 -1
- package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +1 -0
- package/.next/standalone/.next/static/chunks/{turbopack-00qy7zfa7m--m.js → turbopack-3lrm4f20fz89b.js} +1 -1
- package/.next/standalone/app/api/audit/invite/route.ts +183 -0
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +30 -62
- package/.next/standalone/app/audit/_components/audit-poster.tsx +322 -0
- package/.next/standalone/app/audit/_components/auth-dialog.tsx +21 -49
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +316 -0
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +187 -0
- package/.next/standalone/app/audit/_components/invite-dialog.tsx +227 -0
- package/.next/standalone/app/audit/_components/quirks-section.tsx +75 -0
- package/.next/standalone/app/audit/_components/share-templates.ts +23 -22
- package/.next/standalone/app/audit/_components/sigil.tsx +9 -66
- package/.next/standalone/app/audit/_components/strengths-section.tsx +20 -32
- package/.next/standalone/app/audit/audit-styles.css +781 -1784
- package/.next/standalone/app/components/sessions-list.tsx +77 -80
- package/.next/standalone/app/globals.css +214 -32
- package/.next/standalone/app/layout.tsx +1 -10
- package/.next/standalone/app/policies/hooks-client.tsx +12 -4
- package/.next/standalone/app/project/[name]/page.tsx +23 -79
- package/.next/standalone/app/projects/page.tsx +14 -23
- package/.next/standalone/assets/audit/poster-styles.css +1 -1
- package/.next/standalone/assets/audit/styles.css +11 -11
- package/.next/standalone/components/navbar.tsx +2 -37
- package/.next/standalone/lib/auth/api-server-client.ts +25 -0
- package/.next/standalone/node_modules/@next/env/package.json +2 -2
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/shared/lib/errors/canary-only-config-error.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
- package/.next/standalone/node_modules/next/package.json +15 -15
- package/.next/standalone/package.json +15 -12
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +2 -2
- package/lib/auth/api-server-client.ts +25 -0
- package/package.json +15 -12
- package/src/audit/social-proof.ts +34 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__07tgnzi._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oeun7z._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__12pit4m._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__13ra2jq._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1b9z5-i._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ixjiy8._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1-1804f._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00jkjmt._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013du6r._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e85wxv._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gfxvb1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1hlrq6y._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ihxdo5._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1vvfde2._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html2canvas_dist_html2canvas_esm_1k58rb_.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html2canvas_dist_html2canvas_esm_1n-0xws.js +0 -3
- package/.next/standalone/.next/static/chunks/09ueq8s1as8xs.css +0 -2
- package/.next/standalone/.next/static/chunks/0qassxjx1ef04.js +0 -1
- package/.next/standalone/.next/static/chunks/0qxb5czqxe-vu.js +0 -1
- package/.next/standalone/.next/static/chunks/1dh06515j265n.js +0 -41
- package/.next/standalone/.next/static/chunks/2so39wg7mjbi7.js +0 -1
- package/.next/standalone/.next/static/chunks/3gti1qdk5epqn.js +0 -1
- package/.next/standalone/.next/static/chunks/3wycox197ouus.css +0 -1
- package/.next/standalone/app/audit/_components/findings-section.tsx +0 -135
- package/.next/standalone/app/audit/_components/identity-section.tsx +0 -126
- package/.next/standalone/app/audit/_components/policies-section.tsx +0 -194
- package/.next/standalone/app/audit/_components/return-section.tsx +0 -416
- package/.next/standalone/app/audit/_components/score-section.tsx +0 -179
- package/.next/standalone/app/audit/_components/share-dock.tsx +0 -265
- package/.next/standalone/app/audit/_components/show-off-cta.tsx +0 -135
- /package/.next/standalone/.next/static/{CVv2A0hMd24t0c0x3V-W_ → NYPiJP6Rv_exQdSFVS8HP}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{CVv2A0hMd24t0c0x3V-W_ → NYPiJP6Rv_exQdSFVS8HP}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{CVv2A0hMd24t0c0x3V-W_ → NYPiJP6Rv_exQdSFVS8HP}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Invite dialog — modal that takes a comma/newline-separated list of friend
|
|
5
|
+
* emails and POSTs them to /api/audit/invite. The api-server composes the
|
|
6
|
+
* actual invite email (Cc'ing the sender) using the same email infrastructure
|
|
7
|
+
* that backs the OTP flow.
|
|
8
|
+
*
|
|
9
|
+
* Anonymous users get bounced through the AuthDialog by the caller; this
|
|
10
|
+
* component assumes the session is already established.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
14
|
+
import { usePostHog } from "@/contexts/PostHogContext";
|
|
15
|
+
import { toast } from "@/app/components/toast";
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
open: boolean;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
/** Source tag for PostHog so we can split "invited from come-back-better"
|
|
21
|
+
* from any future entry points. */
|
|
22
|
+
source: string;
|
|
23
|
+
/** Called when the proxy returns 401 (session expired between probe and
|
|
24
|
+
* submit). The parent should re-open the AuthDialog so the user can
|
|
25
|
+
* re-authenticate; without this, a 401 dead-ends with an inline error. */
|
|
26
|
+
onUnauthorized?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
30
|
+
const MAX_RECIPIENTS = 10;
|
|
31
|
+
|
|
32
|
+
function parseEmails(input: string): { valid: string[]; invalid: string[] } {
|
|
33
|
+
const tokens = input
|
|
34
|
+
.split(/[\s,;]+/)
|
|
35
|
+
.map((t) => t.trim().toLowerCase())
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
const valid: string[] = [];
|
|
38
|
+
const invalid: string[] = [];
|
|
39
|
+
const seen = new Set<string>();
|
|
40
|
+
for (const t of tokens) {
|
|
41
|
+
if (seen.has(t)) continue;
|
|
42
|
+
seen.add(t);
|
|
43
|
+
if (EMAIL_RE.test(t)) valid.push(t);
|
|
44
|
+
else invalid.push(t);
|
|
45
|
+
}
|
|
46
|
+
return { valid, invalid };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function InviteDialog({ open, onClose, source, onUnauthorized }: Props): React.ReactElement | null {
|
|
50
|
+
const { capture } = usePostHog();
|
|
51
|
+
const [value, setValue] = useState("");
|
|
52
|
+
const [busy, setBusy] = useState(false);
|
|
53
|
+
const [error, setError] = useState<string | null>(null);
|
|
54
|
+
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (open) {
|
|
58
|
+
setValue("");
|
|
59
|
+
setBusy(false);
|
|
60
|
+
setError(null);
|
|
61
|
+
capture("audit_invite_dialog_opened", { source });
|
|
62
|
+
}
|
|
63
|
+
}, [capture, open, source]);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!open) return;
|
|
67
|
+
const t = setTimeout(() => inputRef.current?.focus(), 50);
|
|
68
|
+
return () => clearTimeout(t);
|
|
69
|
+
}, [open]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!open) return;
|
|
73
|
+
const onKey = (e: KeyboardEvent): void => {
|
|
74
|
+
if (e.key === "Escape" && !busy) onClose();
|
|
75
|
+
};
|
|
76
|
+
window.addEventListener("keydown", onKey);
|
|
77
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
78
|
+
}, [open, busy, onClose]);
|
|
79
|
+
|
|
80
|
+
const { valid, invalid } = useMemo(() => parseEmails(value), [value]);
|
|
81
|
+
|
|
82
|
+
const submit = useCallback(
|
|
83
|
+
async (e: React.FormEvent<HTMLFormElement>) => {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
if (busy) return;
|
|
86
|
+
if (valid.length === 0) {
|
|
87
|
+
setError("add at least one valid email address.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (valid.length > MAX_RECIPIENTS) {
|
|
91
|
+
setError(`up to ${MAX_RECIPIENTS} emails at a time. send the rest in a follow-up.`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
setBusy(true);
|
|
95
|
+
setError(null);
|
|
96
|
+
capture("audit_invite_submitted", {
|
|
97
|
+
source,
|
|
98
|
+
recipient_count: valid.length,
|
|
99
|
+
had_invalid: invalid.length > 0,
|
|
100
|
+
});
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetch("/api/audit/invite", {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "content-type": "application/json" },
|
|
105
|
+
body: JSON.stringify({ to: valid }),
|
|
106
|
+
});
|
|
107
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
108
|
+
sent?: string[];
|
|
109
|
+
failed?: string[];
|
|
110
|
+
code?: string;
|
|
111
|
+
message?: string;
|
|
112
|
+
};
|
|
113
|
+
if (res.status === 401) {
|
|
114
|
+
// Session expired between probe and submit — route back through auth.
|
|
115
|
+
// Without this, repeated submits would dead-end with the same 401.
|
|
116
|
+
if (onUnauthorized) {
|
|
117
|
+
onClose();
|
|
118
|
+
onUnauthorized();
|
|
119
|
+
} else {
|
|
120
|
+
setError("session expired. please sign in again.");
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
const msg = body.message ?? "couldn't send invites.";
|
|
126
|
+
setError(msg);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const sent = body.sent?.length ?? 0;
|
|
130
|
+
const failed = body.failed?.length ?? 0;
|
|
131
|
+
toast(
|
|
132
|
+
failed > 0
|
|
133
|
+
? `📨 sent ${sent}, ${failed} bounced — copy the bounce and try again.`
|
|
134
|
+
: `📨 sent ${sent} ${sent === 1 ? "invite" : "invites"}. thanks for spreading the word.`,
|
|
135
|
+
);
|
|
136
|
+
onClose();
|
|
137
|
+
} catch (err) {
|
|
138
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
139
|
+
setError(`network error: ${message}`);
|
|
140
|
+
} finally {
|
|
141
|
+
setBusy(false);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
[busy, valid, invalid, capture, source, onClose, onUnauthorized],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (!open) return null;
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
className="auth-dialog-backdrop"
|
|
152
|
+
role="dialog"
|
|
153
|
+
aria-modal="true"
|
|
154
|
+
aria-labelledby="invite-dialog-title"
|
|
155
|
+
onClick={(e) => {
|
|
156
|
+
if (!busy && e.target === e.currentTarget) onClose();
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<div className="auth-dialog">
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
className="auth-close"
|
|
163
|
+
onClick={onClose}
|
|
164
|
+
disabled={busy}
|
|
165
|
+
aria-label="close"
|
|
166
|
+
>
|
|
167
|
+
×
|
|
168
|
+
</button>
|
|
169
|
+
|
|
170
|
+
<h2 id="invite-dialog-title" className="auth-headline">
|
|
171
|
+
invite friends to audit theirs
|
|
172
|
+
</h2>
|
|
173
|
+
<p className="auth-sub">
|
|
174
|
+
paste emails separated by commas, spaces, or newlines. you'll be Cc'd on every invite so they know it's from you.
|
|
175
|
+
</p>
|
|
176
|
+
|
|
177
|
+
<form onSubmit={submit} className="auth-form">
|
|
178
|
+
<textarea
|
|
179
|
+
ref={inputRef}
|
|
180
|
+
name="emails"
|
|
181
|
+
placeholder="alice@x.com, bob@y.com carol@z.com"
|
|
182
|
+
disabled={busy}
|
|
183
|
+
className="auth-input invite-textarea"
|
|
184
|
+
value={value}
|
|
185
|
+
onChange={(e) => setValue(e.target.value)}
|
|
186
|
+
rows={4}
|
|
187
|
+
required
|
|
188
|
+
/>
|
|
189
|
+
<div className="invite-summary">
|
|
190
|
+
{valid.length > 0 && (
|
|
191
|
+
<span className="invite-valid">{valid.length} valid</span>
|
|
192
|
+
)}
|
|
193
|
+
{invalid.length > 0 && (
|
|
194
|
+
<span className="invite-invalid">
|
|
195
|
+
{invalid.length} invalid: {invalid.slice(0, 3).join(", ")}
|
|
196
|
+
{invalid.length > 3 ? `, +${invalid.length - 3} more` : ""}
|
|
197
|
+
</span>
|
|
198
|
+
)}
|
|
199
|
+
{valid.length > MAX_RECIPIENTS && (
|
|
200
|
+
<span className="invite-invalid">
|
|
201
|
+
only {MAX_RECIPIENTS} per send — first {MAX_RECIPIENTS} will go.
|
|
202
|
+
</span>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
{error && <div className="auth-error">{error}</div>}
|
|
206
|
+
<div className="auth-actions">
|
|
207
|
+
<button
|
|
208
|
+
type="submit"
|
|
209
|
+
className="auth-btn primary"
|
|
210
|
+
disabled={busy || valid.length === 0}
|
|
211
|
+
>
|
|
212
|
+
{busy ? "sending…" : `send ${valid.length || ""} invite${valid.length === 1 ? "" : "s"}`.trim()}
|
|
213
|
+
</button>
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
className="auth-btn"
|
|
217
|
+
onClick={onClose}
|
|
218
|
+
disabled={busy}
|
|
219
|
+
>
|
|
220
|
+
cancel
|
|
221
|
+
</button>
|
|
222
|
+
</div>
|
|
223
|
+
</form>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Section 03 — QUIRKS. "what to improve."
|
|
5
|
+
*
|
|
6
|
+
* 4-column table: when · what slipped (+ policy that would've caught it) ·
|
|
7
|
+
* severity pill · recurrence. Each row corresponds to a triggered
|
|
8
|
+
* detector. No per-finding card chrome, no 4-quad body, no corner
|
|
9
|
+
* crosshairs — evidence and fix live in section 04 (How to improve).
|
|
10
|
+
*/
|
|
11
|
+
import React from "react";
|
|
12
|
+
import type { FindingCard } from "@/src/audit/findings";
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
findings: FindingCard[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Severity = "low" | "medium" | "high";
|
|
19
|
+
|
|
20
|
+
/** Heuristic severity from occurrence count — until the audit pipeline
|
|
21
|
+
* carries a real severity field. Tuned so a single occurrence reads as
|
|
22
|
+
* low and >5 reads as high. */
|
|
23
|
+
function severityFromCount(count: number): Severity {
|
|
24
|
+
if (count >= 6) return "high";
|
|
25
|
+
if (count >= 2) return "medium";
|
|
26
|
+
return "low";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function recurrenceLabel(count: number): string {
|
|
30
|
+
if (count <= 1) return "new";
|
|
31
|
+
if (count < 10) return `${count}× seen`;
|
|
32
|
+
return "recurring";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function QuirksSection({ findings }: Props) {
|
|
36
|
+
if (findings.length === 0) return null;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<section className="audit-sec" data-screen-label="03 Quirks">
|
|
40
|
+
<div className="audit-sec-head">
|
|
41
|
+
<span className="audit-sec-eyebrow">
|
|
42
|
+
<span className="ix">03</span>{"// quirks"}
|
|
43
|
+
</span>
|
|
44
|
+
<span className="audit-sec-meta">{findings.length} slipped through</span>
|
|
45
|
+
</div>
|
|
46
|
+
<h2 className="audit-sec-title">what to improve</h2>
|
|
47
|
+
|
|
48
|
+
<div className="quirks-table">
|
|
49
|
+
<div className="quirks-thead">
|
|
50
|
+
<span>when</span>
|
|
51
|
+
<span>what slipped</span>
|
|
52
|
+
<span>severity</span>
|
|
53
|
+
<span>seen</span>
|
|
54
|
+
</div>
|
|
55
|
+
{findings.map((f) => {
|
|
56
|
+
const sev = severityFromCount(f.count);
|
|
57
|
+
return (
|
|
58
|
+
<div key={f.sourceSlug} className="quirks-row">
|
|
59
|
+
<span className="q-when">{f.lastSeen}</span>
|
|
60
|
+
<span className="q-what">
|
|
61
|
+
<span className="q-title">{f.title}</span>
|
|
62
|
+
<span className="q-policy">
|
|
63
|
+
would've been caught by:{" "}
|
|
64
|
+
<code>{f.policy}</code>
|
|
65
|
+
</span>
|
|
66
|
+
</span>
|
|
67
|
+
<span className={`q-pill q-pill-${sev}`}>{sev}</span>
|
|
68
|
+
<span className="q-recur">{recurrenceLabel(f.count)}</span>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</div>
|
|
73
|
+
</section>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -22,32 +22,33 @@ export interface ShareCtx {
|
|
|
22
22
|
|
|
23
23
|
const pol = (n: number) => (n === 1 ? "policy" : "policies");
|
|
24
24
|
|
|
25
|
-
/** Quirky, emoji-forward — for X.
|
|
25
|
+
/** Quirky, emoji-forward — for X. No grade-tier framing (sounds bad at
|
|
26
|
+
* the low end). Lean on the persona name + score number instead. */
|
|
26
27
|
export const X_TEMPLATES: ((c: ShareCtx) => string)[] = [
|
|
27
|
-
({ score, arch,
|
|
28
|
-
`my AI coding agent just got profiled and it's "${arch}" 🤠\n\
|
|
29
|
-
({ score, arch
|
|
30
|
-
`turns out my coding agent has a whole personality and it's ${arch} 💀\n\
|
|
31
|
-
({ score, arch
|
|
32
|
-
|
|
33
|
-
({ score, arch,
|
|
34
|
-
`plot twist: my AI agent is ${arch} 🎭\n\n${score}/100
|
|
35
|
-
({ score, arch
|
|
36
|
-
`i let failproofai audit my coding agent and it called me ${arch} ${
|
|
28
|
+
({ score, arch, missing }) =>
|
|
29
|
+
`my AI coding agent just got profiled and it's "${arch}" 🤠\n\nscored ${score}/100${missing > 0 ? ` · ${missing} ${pol(missing)} between me and a clean run` : ` · every guardrail live`}\n\nwhat's yours? → ${SITE_URL}`,
|
|
30
|
+
({ score, arch }) =>
|
|
31
|
+
`turns out my coding agent has a whole personality and it's ${arch} 💀\n\n${score}/100. the audit does not miss.\n\nrun yours in 30s → ${SITE_URL}`,
|
|
32
|
+
({ score, arch }) =>
|
|
33
|
+
`archetype: ${arch} 👀\n\n${score}/100 on the failproofai audit. it reverse-engineered my agent's entire vibe from its own session logs.\n\n${SITE_URL}`,
|
|
34
|
+
({ score, arch, missing }) =>
|
|
35
|
+
`plot twist: my AI agent is ${arch} 🎭\n\n${score}/100${missing > 0 ? ` · ${missing} ${pol(missing)} away from behaving` : ` · spotless, somehow`}\n\nbefailproof.ai`,
|
|
36
|
+
({ score, arch }) =>
|
|
37
|
+
`i let failproofai audit my coding agent and it called me ${arch} ${score >= 80 ? "😎" : "😬"}\n\n${score}/100. brutally accurate. no notes.\n\n${SITE_URL}`,
|
|
37
38
|
];
|
|
38
39
|
|
|
39
|
-
/** Measured, professional — for LinkedIn. */
|
|
40
|
+
/** Measured, professional — for LinkedIn. No grade-tier framing. */
|
|
40
41
|
export const LI_TEMPLATES: ((c: ShareCtx) => string)[] = [
|
|
41
|
-
({ score, arch,
|
|
42
|
-
`I ran a failproofai audit on our AI coding agents.\n\nResult: ${score}/100
|
|
43
|
-
({ score, arch,
|
|
44
|
-
`Security posture check on our coding-agent stack, via failproofai.\n\nScore: ${score}/100
|
|
45
|
-
({ score, arch
|
|
46
|
-
`Most teams can't answer a simple question: how do our AI agents actually behave when no one is watching?\n\nfailproofai audited ours and profiled it as "${arch}" — ${score}/100
|
|
47
|
-
({ score, arch,
|
|
48
|
-
`Agent security isn't a vibe — it's measurable.\n\nWe scored ${score}/100
|
|
49
|
-
({ score, arch
|
|
50
|
-
`Ran our coding agents through a failproofai behavioural audit.\n\n${score}/100,
|
|
42
|
+
({ score, arch, missing }) =>
|
|
43
|
+
`I ran a failproofai audit on our AI coding agents.\n\nResult: ${score}/100, behavioural archetype "${arch}". ${missing > 0 ? `${missing} prescribed ${pol(missing)} would close the remaining gaps.` : `Every prescribed policy is already live.`}\n\nUnderstanding how agents actually behave across real sessions is the first step to securing them. Free and open-source: ${SITE_URL}`,
|
|
44
|
+
({ score, arch, missing }) =>
|
|
45
|
+
`Security posture check on our coding-agent stack, via failproofai.\n\nScore: ${score}/100. Behavioural profile: "${arch}". ${missing > 0 ? `${missing} ${pol(missing)} flagged as real, addressable attack surface.` : `No open policy gaps.`}\n\nThirty seconds to run your own assessment: ${SITE_URL}`,
|
|
46
|
+
({ score, arch }) =>
|
|
47
|
+
`Most teams can't answer a simple question: how do our AI agents actually behave when no one is watching?\n\nfailproofai audited ours and profiled it as "${arch}" — ${score}/100. Concrete, evidence-backed, and mapped to the exact policies that close each gap.\n\n${SITE_URL}`,
|
|
48
|
+
({ score, arch, missing }) =>
|
|
49
|
+
`Agent security isn't a vibe — it's measurable.\n\nWe scored ${score}/100 on the failproofai audit, archetype "${arch}". ${missing > 0 ? `${missing} prescribed ${pol(missing)} remain to harden the stack.` : `Full coverage on every prescribed policy.`}\n\nWorth 30 seconds for any team shipping with AI agents: ${SITE_URL}`,
|
|
50
|
+
({ score, arch }) =>
|
|
51
|
+
`Ran our coding agents through a failproofai behavioural audit.\n\n${score}/100, archetype "${arch}". The report maps every risky pattern to the policy that prevents it, so remediation becomes a checklist rather than a guess.\n\nOpen-source, free to run: ${SITE_URL}`,
|
|
51
52
|
];
|
|
52
53
|
|
|
53
54
|
/** djb2 hash — stable per seed so the template choice is deterministic. */
|
|
@@ -1,39 +1,22 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Pixel sigil —
|
|
4
|
+
* Pixel sigil — 8×8 grid for the archetype. Bare implementation: no plate,
|
|
5
|
+
* no crosshairs, no reveal animation. The poster owns whatever framing it
|
|
6
|
+
* wants around the sigil.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* p = pink accent g = green accent d = dim
|
|
9
|
-
*
|
|
10
|
-
* Two rendering modes:
|
|
11
|
-
*
|
|
12
|
-
* • Default (used on the audit hero, identity-section): wraps the grid in
|
|
13
|
-
* a brutalist "instrument plate" — register crosshairs at the four
|
|
14
|
-
* corners, a header strip with the archetype index + an 8×8 coordinate
|
|
15
|
-
* label, the pixel grid mounted on a dashed inner frame with 20px cells,
|
|
16
|
-
* a footer strip naming the archetype, and a stacked pink + dim hard-
|
|
17
|
-
* offset shadow for depth. Cells fade in along a diagonal wave on
|
|
18
|
-
* mount (`--cx` / `--cy` custom properties), guarded by
|
|
19
|
-
* `prefers-reduced-motion`.
|
|
20
|
-
*
|
|
21
|
-
* • hideLabel (used in the ShowOff CTA + html2canvas capture): a bare
|
|
22
|
-
* `.sigil` grid with no plate or labels, so the showoff card can scale
|
|
23
|
-
* the sigil down independently and html2canvas doesn't have to capture
|
|
24
|
-
* the new plate chrome.
|
|
8
|
+
* Cell letters: `.` empty · `o` ink · `p` pink accent · `g` green accent ·
|
|
9
|
+
* `d` dim.
|
|
25
10
|
*/
|
|
26
11
|
import React from "react";
|
|
27
|
-
import {
|
|
12
|
+
import { SIGILS, type ArchetypeKey } from "@/src/audit/archetypes";
|
|
28
13
|
|
|
29
14
|
interface Props {
|
|
30
15
|
archetypeKey: ArchetypeKey;
|
|
31
|
-
hideLabel?: boolean;
|
|
32
16
|
}
|
|
33
17
|
|
|
34
|
-
export function Sigil({ archetypeKey
|
|
18
|
+
export function Sigil({ archetypeKey }: Props) {
|
|
35
19
|
const grid = SIGILS[archetypeKey] ?? SIGILS.optimist;
|
|
36
|
-
const archetype = ARCHETYPES[archetypeKey];
|
|
37
20
|
const cells: React.ReactElement[] = [];
|
|
38
21
|
|
|
39
22
|
for (let y = 0; y < 8; y++) {
|
|
@@ -45,49 +28,9 @@ export function Sigil({ archetypeKey, hideLabel }: Props) {
|
|
|
45
28
|
else if (c === "p") cls += " p";
|
|
46
29
|
else if (c === "g") cls += " g";
|
|
47
30
|
else if (c === "d") cls += " d";
|
|
48
|
-
cells.push(
|
|
49
|
-
<div
|
|
50
|
-
key={`${y}-${x}`}
|
|
51
|
-
className={cls}
|
|
52
|
-
style={{
|
|
53
|
-
["--cx" as string]: x,
|
|
54
|
-
["--cy" as string]: y,
|
|
55
|
-
} as React.CSSProperties}
|
|
56
|
-
/>,
|
|
57
|
-
);
|
|
31
|
+
cells.push(<div key={`${y}-${x}`} className={cls} />);
|
|
58
32
|
}
|
|
59
33
|
}
|
|
60
34
|
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<div className="sigil-wrap" data-bare="true">
|
|
64
|
-
<div className="sigil">{cells}</div>
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const indexLabel = String(archetype.index).padStart(2, "0");
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<div className="sigil-wrap">
|
|
73
|
-
<div className="sigil-plate">
|
|
74
|
-
<span className="sigil-mark tl" aria-hidden="true" />
|
|
75
|
-
<span className="sigil-mark tr" aria-hidden="true" />
|
|
76
|
-
<span className="sigil-mark bl" aria-hidden="true" />
|
|
77
|
-
<span className="sigil-mark br" aria-hidden="true" />
|
|
78
|
-
|
|
79
|
-
<div className="sigil-strip sigil-strip--top">
|
|
80
|
-
<span className="sigil-ix">№ {indexLabel}</span>
|
|
81
|
-
<span className="sigil-coord">8×8</span>
|
|
82
|
-
</div>
|
|
83
|
-
|
|
84
|
-
<div className="sigil">{cells}</div>
|
|
85
|
-
|
|
86
|
-
<div className="sigil-strip sigil-strip--bot">
|
|
87
|
-
<span className="sigil-strip-key">sigil</span>
|
|
88
|
-
<span className="sigil-strip-val">{archetypeKey}</span>
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
);
|
|
35
|
+
return <div className="sigil">{cells}</div>;
|
|
93
36
|
}
|
|
@@ -1,57 +1,45 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Section 02 — STRENGTHS. "
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Section 02 — STRENGTHS. "what it's great at." Calm row list:
|
|
5
|
+
* ✓ glyph · headline + sub · right-aligned metric. No card chrome,
|
|
6
|
+
* no hover backgrounds, no checkmark backdrop.
|
|
7
7
|
*/
|
|
8
8
|
import React from "react";
|
|
9
9
|
import type { Strength } from "@/src/audit/strengths";
|
|
10
10
|
|
|
11
11
|
interface Props {
|
|
12
12
|
strengths: Strength[];
|
|
13
|
-
totalDetectorsTriggered: number;
|
|
14
|
-
totalDetectorsAvailable: number;
|
|
15
13
|
}
|
|
16
14
|
|
|
17
|
-
export function StrengthsSection({
|
|
18
|
-
strengths, totalDetectorsTriggered, totalDetectorsAvailable,
|
|
19
|
-
}: Props) {
|
|
15
|
+
export function StrengthsSection({ strengths }: Props) {
|
|
20
16
|
if (strengths.length === 0) return null;
|
|
21
17
|
|
|
22
18
|
return (
|
|
23
|
-
<section className="
|
|
24
|
-
<div className="
|
|
25
|
-
<
|
|
26
|
-
<span className="
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
</div>
|
|
30
|
-
<div className="section-meta">
|
|
31
|
-
<span className="g">●</span>{" "}
|
|
32
|
-
{totalDetectorsAvailable - totalDetectorsTriggered} of {totalDetectorsAvailable} clean
|
|
33
|
-
</div>
|
|
19
|
+
<section className="audit-sec" data-screen-label="02 Strengths">
|
|
20
|
+
<div className="audit-sec-head">
|
|
21
|
+
<span className="audit-sec-eyebrow">
|
|
22
|
+
<span className="ix">02</span>{"// strengths"}
|
|
23
|
+
</span>
|
|
24
|
+
<span className="audit-sec-meta">{strengths.length} standouts</span>
|
|
34
25
|
</div>
|
|
35
|
-
<h2 className="
|
|
26
|
+
<h2 className="audit-sec-title">what it's great at</h2>
|
|
36
27
|
|
|
37
|
-
<div className="
|
|
28
|
+
<div className="strength-list">
|
|
38
29
|
{strengths.map((s, i) => (
|
|
39
30
|
<div key={i} className="strength-row">
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
</
|
|
45
|
-
<
|
|
31
|
+
<span className="strength-check" aria-hidden="true">✓</span>
|
|
32
|
+
<span className="strength-body">
|
|
33
|
+
<span className="strength-headline">{s.headline}</span>
|
|
34
|
+
<span className="strength-detail">{s.detail}</span>
|
|
35
|
+
</span>
|
|
36
|
+
<span className="strength-metric">
|
|
46
37
|
{s.metric}
|
|
47
|
-
<span className="unit">{s.unit}</span>
|
|
48
|
-
</
|
|
38
|
+
{s.unit && <span className="unit">{s.unit}</span>}
|
|
39
|
+
</span>
|
|
49
40
|
</div>
|
|
50
41
|
))}
|
|
51
42
|
</div>
|
|
52
|
-
<div className="strengths-footer">
|
|
53
|
-
— these are your agent's defaults. keep them.
|
|
54
|
-
</div>
|
|
55
43
|
</section>
|
|
56
44
|
);
|
|
57
45
|
}
|