Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions components/agents/AskAnAgent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { useState } from "react";
import { Box, Text } from "@obolnetwork/obol-ui";
import type { CSS } from "@stitches/react";
import { VbButton } from "@components/ui/VbButton";
import { AGENT_FILES, ASK_AGENT_ID } from "@lib/agents/prompts";
import { copyText } from "@lib/share/clipboard";

const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? "";

type AskAnAgentProps = {
/** The prompt, rendered into the static HTML so agents reading the DOM see it too. */
prompt: string;
title?: string;
lede?: string;
headingAs?: "h2" | "h3";
css?: CSS;
};

/** A visible, copyable prompt for the visitor's own AI assistant, plus links to the agent docs. */
export function AskAnAgent({
prompt,
title = "Ask your AI about your setup",
lede = "Paste this into ChatGPT, Claude, or any assistant that can read a URL. Fill in the brackets first.",
headingAs = "h2",
css,
}: AskAnAgentProps) {
const [copied, setCopied] = useState(false);

const copy = async () => {
if (!(await copyText(prompt))) return;
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};

return (
<Box
as="section"
id={ASK_AGENT_ID}
aria-labelledby={`${ASK_AGENT_ID}-title`}
css={{
backgroundColor: "$bg03",
border: "1px solid $bg05",
borderRadius: 14,
padding: "20px 22px",
textAlign: "left",
minWidth: 0,
"@media (max-width: 760px)": { padding: 16 },
...css,
}}
>
<Text
as={headingAs}
id={`${ASK_AGENT_ID}-title`}
css={{ fontSize: "$4", fontWeight: "$bold", color: "$body", margin: 0 }}
>
{title}
</Text>
<Text as="p" css={{ fontSize: "$2", lineHeight: 1.55, color: "$textMiddle", margin: "6px 0 14px" }}>
{lede}
</Text>
<Box
as="pre"
css={{
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
fontSize: 13,
lineHeight: 1.6,
color: "$body",
backgroundColor: "$bg01",
border: "1px solid $bg05",
borderRadius: 10,
padding: 14,
margin: 0,
whiteSpace: "pre-wrap",
overflowWrap: "anywhere",
maxHeight: "16rem",
overflowY: "auto",
}}
>
{prompt}
</Box>
<Box css={{ display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap", marginTop: 14 }}>
<VbButton onClick={copy} aria-live="polite">
{copied ? "Prompt copied ✓" : "Copy prompt"}
</VbButton>
<Text as="span" css={{ fontSize: "$2", color: "$textMiddle" }}>
For agents:{" "}
{AGENT_FILES.map((f, i) => (
<span key={f.path}>
{i > 0 && " · "}
<Box
as="a"
href={`${BASE_PATH}${f.path}`}
css={{ color: "var(--theme-brand)", textDecoration: "underline" }}
>
{f.path.slice(1)}
</Box>
</span>
))}
</Text>
</Box>
</Box>
);
}
20 changes: 20 additions & 0 deletions components/assessment/AssessmentApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { CompareCard } from "./Compare";
import { Intro } from "./Intro";
import { Question } from "./Question";
import { LevelUp, ResultHero, ShareModal } from "./Results";
import { AskAnAgent } from "@components/agents/AskAnAgent";
import { assessPrompt, resultPrompt } from "@lib/agents/prompts";
import { SiteHeader } from "@components/layout/SiteHeader";
import { SiteFooter } from "@components/layout/SiteFooter";
import {
Expand Down Expand Up @@ -137,6 +139,23 @@ export function AssessmentApp({ initialShareCode }: AssessmentAppProps) {
document.title = `${shareName} is ${STAGE_META[a.stage].name} | ${SITE_NAME}`;
}, [guest, shareName, a.stage]);

// The intro is also the server-rendered state, so a share page's static HTML
// carries its own result prompt for agents reading the DOM.
const promptCode = a.atResults ? code : a.atIntro ? initialShareCode?.toUpperCase() : undefined;
const askAgent = (a.atIntro || a.atResults) && (
<AskAnAgent
headingAs="h3"
css={{ marginTop: 28 }}
prompt={promptCode ? resultPrompt(promptCode) : assessPrompt()}
title={promptCode ? "Ask your AI about this result" : "Or let your AI walk you through it"}
lede={
promptCode
? "Paste this into ChatGPT, Claude, or any assistant that can read a URL. Fill in the brackets first."
: "Paste this into ChatGPT, Claude, or any assistant that can read a URL. It asks the same six questions and hands back your share link."
}
/>
);

const takeItYourself = () => {
router.push(compareAssessPath({ code, name: shareName }));
};
Expand Down Expand Up @@ -224,6 +243,7 @@ export function AssessmentApp({ initialShareCode }: AssessmentAppProps) {
)}
</>
) : null}
{askAgent}
</LeftCard>

{/* On phones the pizza leads, except on a shared link, where the name and stage should. */}
Expand Down
13 changes: 13 additions & 0 deletions components/landing/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
TopNavLink,
risk,
} from "@components/assessment/stitches";
import { AskAnAgent } from "@components/agents/AskAnAgent";
import { assessPrompt } from "@lib/agents/prompts";
import { SiteHeader } from "@components/layout/SiteHeader";
import { SiteFooter } from "@components/layout/SiteFooter";
import {
Expand Down Expand Up @@ -686,6 +688,16 @@ function LClosing() {
);
}

function LAgent() {
return (
<Box as="section" css={s.section}>
<Box css={s.wrap}>
<AskAnAgent prompt={assessPrompt()} title="Prefer to ask your AI?" css={{ maxWidth: 760 }} />
</Box>
</Box>
);
}

export function Landing() {
return (
<Box css={{ minHeight: "100vh", backgroundColor: "$bg01", color: "$textMiddle" }}>
Expand All @@ -699,6 +711,7 @@ export function Landing() {
<LValos />
<LNeutral />
<LClosing />
<LAgent />
</Box>
<SiteFooter />
</Box>
Expand Down
28 changes: 28 additions & 0 deletions components/layout/SiteFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
METHODOLOGY_PATH,
VALOS_URL,
} from "@constants/index";
import { AGENT_FILES, ASK_AGENT_ID } from "@lib/agents/prompts";

const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? "";

const navLink: CSS = {
fontSize: "$2",
Expand Down Expand Up @@ -70,6 +73,31 @@ export function SiteFooter({ contentWidth = 1140 }: SiteFooterProps) {
</Box>
<TopNavLink href={ASSESS_PATH}>Assess</TopNavLink>
</Box>
<Box
as="nav"
aria-label="For AI agents"
css={{
maxWidth: contentWidth,
margin: "10px auto 0",
padding: "0 28px",
display: "flex",
alignItems: "center",
gap: "6px 16px",
flexWrap: "wrap",
}}
>
<Text css={{ fontSize: "$1", fontWeight: "$semibold", color: "$textMiddle" }}>
For AI agents
</Text>
{AGENT_FILES.map((f) => (
<Box key={f.path} as="a" href={`${BASE_PATH}${f.path}`} css={{ ...navLink, fontSize: "$1" }}>
{f.path.slice(1)}
</Box>
))}
<TopNavLink href={`/#${ASK_AGENT_ID}`} css={{ fontSize: "$1" }}>
Ask your AI
</TopNavLink>
</Box>
</Box>
);
}
12 changes: 12 additions & 0 deletions components/methodology/Inline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Fragment } from "react";

/** Render a copy string's `**bold**` spans as <strong> (see lib/methodology/content.ts). */
export function Inline({ text }: { text: string }) {
return (
<>
{text.split(/\*\*(.+?)\*\*/g).map((part, i) =>
i % 2 ? <strong key={i}>{part}</strong> : <Fragment key={i}>{part}</Fragment>,
)}
</>
);
}
58 changes: 58 additions & 0 deletions lib/agents/agents.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { QUESTIONS } from "@lib/assessment/questions";
import { NUANCE_CASES, UNSCORED } from "@lib/methodology/content";
import { SLICES, STAGE_META, TIPS, allShareCodes } from "@lib/rubric";
import { llmsFullTxt, llmsTxt, sitemapXml, skillMd } from "./files";
import { AGENT_FILES, assessPrompt, resultPrompt } from "./prompts";

describe("llms-full.txt", () => {
const doc = llmsFullTxt();

it("contains every slice, question, band, and tip from the rubric", () => {
for (const s of SLICES) {
const q = QUESTIONS[s.id];
expect(doc).toContain(s.label);
expect(doc).toContain(s.why);
expect(doc).toContain(q.q);
expect(doc).toContain(q.helper);
for (const o of q.options) {
expect(doc).toContain(o.label);
expect(doc).toContain(o.sub);
}
for (const tip of Object.values(TIPS[s.id])) expect(doc).toContain(tip);
}
});

it("lists slices in share-code order", () => {
const positions = SLICES.map((s) => doc.indexOf(`(\`${s.id}\`, share-code position`));
expect(positions.every((p) => p >= 0)).toBe(true);
expect([...positions].sort((a, b) => a - b)).toEqual(positions);
});

it("contains every stage and the methodology nuances", () => {
for (const m of Object.values(STAGE_META)) expect(doc).toContain(m.tagline);
for (const n of NUANCE_CASES) expect(doc).toContain(n.body);
for (const u of UNSCORED) expect(doc).toContain(u.body);
expect(doc).toContain(`${allShareCodes().length} valid codes`);
});
});

describe("agent file links", () => {
it("llms.txt, skill.md and the sitemap point at llms-full.txt", () => {
expect(llmsTxt()).toContain("/llms-full.txt");
expect(skillMd()).toContain("/llms-full.txt");
for (const f of AGENT_FILES) expect(sitemapXml()).toContain(`${f.path}</loc>`);
});
});

describe("prompts", () => {
it("embeds the share code and its stage", () => {
const p = resultPrompt("GYYGGY");
expect(p).toContain("/GYYGGY/");
expect(p).toContain("which is Stage 1");
expect(p).toContain("reach Stage 2");
});

it("points the assess prompt at skill.md", () => {
expect(assessPrompt()).toContain("/skill.md");
});
});
Loading
Loading