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
3 changes: 3 additions & 0 deletions docs-site/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ const config = {
},
items: [
{ type: "doc", docId: "user/intro", label: "Docs", position: "left" },
// Agent Skills is a peer of Docs, not a subsection of it. Targets the
// standalone `agents` sidebar so the entry follows the version dropdown.
{ type: "docSidebar", sidebarId: "agents", label: "Agent Skills", position: "left" },
{ type: "search", position: "left" },
{ type: "docsVersionDropdown", position: "right" },
{
Expand Down
10 changes: 8 additions & 2 deletions docs-site/scripts/prepare-versioned-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const fs = require("fs");
const os = require("os");
const path = require("path");

const { buildDocsSidebar } = require("../sidebarsConfig");
const { buildDocsSidebar, buildAgentsSidebar } = require("../sidebarsConfig");
const { mirrorSkillsToAgents } = require("./mirror-skills");

const DOCS_SITE_DIR = path.resolve(__dirname, "..");
Expand Down Expand Up @@ -281,9 +281,15 @@ function prepareVersionedDocs(plan, options = {}) {
handwrittenDir: paths.agentsSrcDir,
destDir: path.join(versionDir, "agents"),
});
// Versions that predate the skills feature get no `agents` sidebar at all --
// see buildAgentsSidebar. The navbar entry is guarded to match.
const agentsSidebar = buildAgentsSidebar(versionDir);
writeJson(
path.join(paths.versionedSidebarsDir, `${safeVersionDirName(version.label)}-sidebars.json`),
{ docs: buildDocsSidebar(versionDir) },
{
docs: buildDocsSidebar(versionDir),
...(agentsSidebar ? { agents: agentsSidebar } : {}),
},
);
}

Expand Down
10 changes: 8 additions & 2 deletions docs-site/sidebars.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const path = require("path");
const fs = require("fs");
const { buildDocsSidebar } = require("./sidebarsConfig");
const { buildDocsSidebar, buildAgentsSidebar } = require("./sidebarsConfig");

// Mirror docusaurus.config.js: prefer the generated develop snapshot, otherwise
// fall back to the repo-level docs/ directory for local dev.
Expand All @@ -10,8 +10,14 @@ function currentDocsPath() {
}

/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const currentDocs = currentDocsPath();
const agents = buildAgentsSidebar(currentDocs);

const sidebars = {
docs: buildDocsSidebar(currentDocsPath()),
docs: buildDocsSidebar(currentDocs),
// Spread rather than assign: Docusaurus rejects an empty/undefined sidebar, so a
// snapshot without skills must omit the key entirely.
...(agents ? { agents } : {}),
};

module.exports = sidebars;
28 changes: 18 additions & 10 deletions docs-site/sidebarsConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,9 @@ function buildDocsSidebar(docsDir) {
if (categories.length) {
sidebar.push({ type: "category", label: "Sflow User Guide", collapsed: false, items: categories });
}
// Agent Skills: a labeled category wrapping the autogenerated agents/ tree,
// present only when this snapshot ships the skills mirror.
if (dirHasDocs(docsDir, "agents")) {
sidebar.push({
type: "category",
label: "Agent Skills",
collapsed: false,
items: [{ type: "autogenerated", dirName: "agents" }],
});
}
// Agent Skills deliberately does NOT appear here -- it is its own top-level
// sidebar (see buildAgentsSidebar) reached from its own navbar entry, so it is
// a peer of the user guide rather than a subsection buried inside it.
for (const dir of AUTOGEN_SECTIONS) {
if (!EXCLUDED_DIRS.has(dir) && dirHasDocs(docsDir, dir)) {
sidebar.push({ type: "autogenerated", dirName: dir });
Expand All @@ -120,11 +113,26 @@ function buildDocsSidebar(docsDir) {
return sidebar;
}

// Build the standalone `agents` sidebar for the Agent Skills navbar entry.
// The mirrored agents/ tree already carries sidebar_position / _category_.json
// ordering, so plain autogeneration is enough -- no wrapper category, which would
// only repeat the navbar label back at the reader.
//
// Returns null when this snapshot ships no skills (e.g. v0.1.0, which predates the
// feature). Callers must omit the sidebar entirely in that case: Docusaurus rejects
// an empty sidebar array, and a version that never had agent skills should not
// advertise them.
function buildAgentsSidebar(docsDir) {
if (!dirHasDocs(docsDir, "agents")) return null;
return [{ type: "autogenerated", dirName: "agents" }];
}

module.exports = {
USER_GUIDE_CATEGORIES,
AUTOGEN_SECTIONS,
EXCLUDED_DIRS,
listUserDocIds,
dirHasDocs,
buildDocsSidebar,
buildAgentsSidebar,
};
24 changes: 17 additions & 7 deletions docs-site/sidebarsConfig.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const fs = require("fs");
const os = require("os");
const path = require("path");

const { buildDocsSidebar } = require("./sidebarsConfig");
const { buildDocsSidebar, buildAgentsSidebar } = require("./sidebarsConfig");

function makeDocs(files) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sflow-sidebar-"));
Expand Down Expand Up @@ -43,16 +43,26 @@ test("groups user docs, keeps only existing ones, and buckets the rest under Mor
{ type: "category", label: "More", collapsed: false, items: ["user/zzz-extra"] },
],
},
{
type: "category",
label: "Agent Skills",
collapsed: false,
items: [{ type: "autogenerated", dirName: "agents" }],
},
{ type: "autogenerated", dirName: "release_notes" },
]);
});

test("agent skills live in their own sidebar, not inside the docs sidebar", () => {
const dir = makeDocs(["user/intro.md", "agents/intro.md", "agents/writing-sflow-yaml/index.md"]);

// The navbar entry owns the "Agent Skills" label, so the sidebar itself is just
// the autogenerated tree -- and it must not also appear under the docs sidebar.
assert.deepEqual(buildAgentsSidebar(dir), [{ type: "autogenerated", dirName: "agents" }]);
assert.equal(JSON.stringify(buildDocsSidebar(dir)).includes("agents"), false);
});

test("omits the agents sidebar entirely for snapshots that predate agent skills", () => {
// Docusaurus rejects an empty sidebar, and a version that never shipped skills
// should not advertise them -- so the key has to be absent, not empty.
assert.equal(buildAgentsSidebar(makeDocs(["user/intro.md"])), null);
assert.equal(buildAgentsSidebar(makeDocs(["agents/_category_.json"])), null);
});

test("never includes the excluded plc or developer directories and falls back when nothing matches", () => {
const dir = makeDocs(["plc/sflow_srd.md", "developer/note.md"]);
assert.deepEqual(buildDocsSidebar(dir), [{ type: "autogenerated", dirName: "." }]);
Expand Down
114 changes: 96 additions & 18 deletions docs-site/src/pages/index.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,114 @@
import React from "react";
import Head from "@docusaurus/Head";
import useBaseUrl from "@docusaurus/useBaseUrl";
import useIsBrowser from "@docusaurus/useIsBrowser";
import { useLocation } from "@docusaurus/router";

// The homepage is a full-viewport frame around the static intro deck. Language is
// carried in the query string (`/?lang=zh`) rather than by swapping the iframe in
// place, so the Chinese deck is shareable, bookmarkable and survives a reload --
// an in-place swap leaves the address bar on "/" and silently reverts on refresh.
//
// The deck itself drives this: when it detects it is framed, its language toggle
// rewrites the TOP url instead of navigating its own document. See the
// `langToggle` handler in static/sflow_intro*.html.
const DECKS = {
en: {
file: "/sflow_intro.html",
title: "NV-sflow — Declarative Workflow Descriptor",
description:
"Declarative workflow descriptor with swappable backends. Describe once, run anywhere.",
frameTitle: "NV-sflow Introduction",
htmlLang: "en",
},
zh: {
file: "/sflow_intro_zh.html",
title: "NV-sflow — 面向大规模 GPU 集群的声明式工作流描述器",
description: "声明式工作流描述器,后端可自由切换。一次描述,随处运行。",
frameTitle: "NV-sflow 介绍",
htmlLang: "zh-CN",
},
};

const DECK_BG = "#060a10"; // matches the deck's own background, so the pre-load frame is invisible

export default function Home() {
const introUrl = useBaseUrl("/sflow_intro.html");
const { search, hash } = useLocation();
const isBrowser = useIsBrowser();

const lang = new URLSearchParams(search).get("lang") === "zh" ? "zh" : "en";
const deck = DECKS[lang];

// Both are resolved unconditionally: useBaseUrl is a hook and cannot be called
// behind a branch without breaking the rules of hooks.
const enUrl = useBaseUrl(DECKS.en.file);
const zhUrl = useBaseUrl(DECKS.zh.file);

// Forward the slide anchor (#s7) so switching language deep in the deck lands
// on the same slide instead of resetting to the title.
const slide = /^#s[0-9a-z]+$/i.test(hash) ? hash : "";
const src = (lang === "zh" ? zhUrl : enUrl) + slide;

return (
<>
<Head>
<title>NV-sflow — Declarative Workflow Descriptor</title>
<meta
name="description"
content="Declarative workflow descriptor with swappable backends. Describe once, run anywhere."
/>
<html lang={deck.htmlLang} />
<title>{deck.title}</title>
<meta name="description" content={deck.description} />
<link rel="alternate" hrefLang="en" href={enUrl} />
<link rel="alternate" hrefLang="zh-Hans" href={zhUrl} />
<style>{`
.navbar, .footer, .main-wrapper > nav { display: none !important; }
#__docusaurus { height: 100vh; overflow: hidden; }
.main-wrapper { height: 100vh; padding: 0 !important; margin: 0 !important; }
`}</style>
</Head>
<main style={{ width: "100vw", height: "100vh", margin: 0, padding: 0 }}>
<iframe
src={introUrl}
title="NV-sflow Introduction"
style={{
width: "100%",
height: "100%",
border: "none",
display: "block",
}}
allowFullScreen
/>
<main
style={{
width: "100vw",
height: "100vh",
margin: 0,
padding: 0,
background: DECK_BG,
}}
>
{/* Rendered only in the browser, and never during the hydration pass.
The query string is not knowable at build time, so a server-rendered
frame would always be the English deck -- and React does not patch an
iframe `src` while hydrating, which stranded `?lang=zh` on the English
deck. Waiting one tick costs nothing visible: `main` already paints the
deck's own background colour. */}
{isBrowser && (
<iframe
// Keyed by src so a language switch remounts the frame instead of
// mutating an already-loaded one.
key={src}
src={src}
title={deck.frameTitle}
style={{
width: "100%",
height: "100%",
border: "none",
display: "block",
}}
allowFullScreen
/>
)}
{/* Without JS the frame above never renders, which would leave the
homepage blank. `?lang=zh` is unreadable here, so this falls back to
the English deck -- both decks link to each other anyway. */}
<noscript>
<iframe
src={enUrl}
title={DECKS.en.frameTitle}
style={{
width: "100%",
height: "100%",
border: "none",
display: "block",
}}
/>
</noscript>
</main>
</>
);
Expand Down
Loading
Loading