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
42 changes: 41 additions & 1 deletion packages/landing/scripts/gen-landing-snippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ type UseCaseSnippets = {
connector: Snippet;
memorySchema: Snippet;
watcher: Snippet;
agentConfig: Snippet;
reaction: Snippet | null;
skill: Snippet | null;
};

type LandingSnippets = {
Expand Down Expand Up @@ -442,6 +445,27 @@ function findConnectorFile(slug: string): { rel: string } {
return { rel: file };
}

function findReactionFile(slug: string): string | null {
const exampleDir = resolve(examplesDir, slug);
const file = readdirSync(exampleDir).find((f) => f.endsWith(".reaction.ts"));
return file ?? null;
}

// First-party skills only — under examples/<slug>/skills/<name>/SKILL.md.
// Workspace-bundled .skills/ and node_modules SKILL.md are noise.
function findSkillFile(slug: string): string | null {
const skillsDir = resolve(examplesDir, slug, "skills");
if (!existsSync(skillsDir)) return null;
const names = readdirSync(skillsDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
for (const name of names) {
const rel = `skills/${name}/SKILL.md`;
if (existsSync(resolve(examplesDir, slug, rel))) return rel;
}
return null;
}

function buildUseCases(): Record<string, UseCaseSnippets> {
const out: Record<string, UseCaseSnippets> = {};
for (const slug of USE_CASE_SLUGS) {
Expand All @@ -454,7 +478,23 @@ function buildUseCases(): Record<string, UseCaseSnippets> {
);
const memorySchema = configSnippet(slug, entitySlice);
const watcher = configSnippet(slug, watcherSlice);
out[slug] = { connector, memorySchema, watcher };
const agentConfig = configSnippet(slug, agentConfigSlice);
const reactionRel = findReactionFile(slug);
const reaction = reactionRel
? fileSnippet(slug, reactionRel, "typescript")
: null;
const skillRel = findSkillFile(slug);
const skill = skillRel
? fileSnippet(slug, skillRel, "markdown", trimSkillMarkdown)
: null;
out[slug] = {
connector,
memorySchema,
watcher,
agentConfig,
reaction,
skill,
};
}
return out;
}
Expand Down
115 changes: 84 additions & 31 deletions packages/landing/src/components/ArchitectureDiagram.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
// biome-ignore-all format: stays compact for the landing-page panel
import { GITHUB_CONNECTORS_TREE_URL } from "../lib/urls";
import {
type ArchitectureEntityRow,
getArchitectureConnectorChips,
getArchitectureEntityRows,
} from "../use-case-showcases";
import { messagingChannels } from "./platforms";

const GENERIC_CONNECTOR_ROWS: ReadonlyArray<{
label: string;
href: string;
}> = [
{ label: "50+ built-in connectors", href: GITHUB_CONNECTORS_TREE_URL },
{ label: "any MCP server", href: "/guides/mcp-proxy/" },
{ label: "custom via connector-sdk", href: "/getting-started/connector-sdk/" },
];

// Fallback entity rows when no use case is active (the homepage). Labelled
// generically so the widget reads as a schema preview, not a snapshot of a
// real customer's data. /for/<slug> pages pass `slug` and get vertical-
// specific rows via getArchitectureEntityRows.
const GENERIC_ROWS: readonly ArchitectureEntityRow[] = [
["Customer A", "company", "2d"],
["Customer B", "person", "5h"],
["Customer C", "meeting", "1h"],
];

/**
* Architecture diagram: layered stream story (NOT cycling pairs).
*
Expand All @@ -22,11 +47,19 @@ import { messagingChannels } from "./platforms";
/* Component */
/* -------------------------------------------------------------------------- */

export function ArchitectureDiagram() {
export function ArchitectureDiagram({ slug }: { slug?: string } = {}) {
const rows = getArchitectureEntityRows(slug) ?? GENERIC_ROWS;
const chips = getArchitectureConnectorChips(slug);
const connectorRows = chips
? chips.map((label) => ({
label,
href: "/getting-started/connector-sdk/",
}))
: GENERIC_CONNECTOR_ROWS;
return (
<div class="flex flex-col gap-6">
<Header />
<DiagramBoard />
<DiagramBoard rows={rows} connectorRows={connectorRows} />
<PulseStyles />
</div>
);
Expand Down Expand Up @@ -60,7 +93,15 @@ function Header() {
);
}

function DiagramBoard() {
type ConnectorRow = { label: string; href: string };

function DiagramBoard({
rows,
connectorRows,
}: {
rows: readonly ArchitectureEntityRow[];
connectorRows: readonly ConnectorRow[];
}) {
return (
<div
class="relative rounded-lg border p-6 sm:p-8"
Expand All @@ -71,10 +112,10 @@ function DiagramBoard() {
}}
>
<div class="hidden lg:block">
<DesktopBoard />
<DesktopBoard rows={rows} connectorRows={connectorRows} />
</div>
<div class="block lg:hidden">
<MobileBoard />
<MobileBoard rows={rows} connectorRows={connectorRows} />
</div>
</div>
);
Expand All @@ -84,24 +125,36 @@ function DiagramBoard() {
/* Desktop board: three column layered story */
/* -------------------------------------------------------------------------- */

function DesktopBoard() {
function DesktopBoard({
rows,
connectorRows,
}: {
rows: readonly ArchitectureEntityRow[];
connectorRows: readonly ConnectorRow[];
}) {
return (
<div class="grid grid-cols-[1fr_auto_1.1fr_auto_1.1fr] items-stretch gap-x-2">
<ConnectorsColumn />
<ConnectorsColumn connectorRows={connectorRows} />
<ColumnArrow label="events" />
<MemoryColumn />
<MemoryColumn rows={rows} />
<ColumnArrow label="chat" sublabel="read" split />
<AgentsColumn />
</div>
);
}

function MobileBoard() {
function MobileBoard({
rows,
connectorRows,
}: {
rows: readonly ArchitectureEntityRow[];
connectorRows: readonly ConnectorRow[];
}) {
return (
<div class="flex flex-col gap-4">
<ConnectorsColumn />
<ConnectorsColumn connectorRows={connectorRows} />
<VerticalArrow label="events" />
<MemoryColumn />
<MemoryColumn rows={rows} />
<VerticalArrow label="chat · read" />
<AgentsColumn />
</div>
Expand All @@ -112,31 +165,33 @@ function MobileBoard() {
/* Columns */
/* -------------------------------------------------------------------------- */

function ConnectorsColumn() {
function ConnectorsColumn({
connectorRows,
}: {
connectorRows: readonly ConnectorRow[];
}) {
return (
<ColumnCard heading="Connectors" footer="stream events from any source">
<div class="flex flex-col gap-2">
<SourceRow
label="50+ built-in connectors"
href="https://github.com/lobu-ai/lobu/tree/main/packages/connectors/src"
/>
<SourceRow label="any MCP server" href="/guides/mcp-proxy/" />
<SourceRow
label="custom via connector-sdk"
href="/getting-started/connector-sdk/"
/>
{connectorRows.map((row) => (
<SourceRow key={row.label} label={row.label} href={row.href} />
))}
</div>
</ColumnCard>
);
}

function MemoryColumn() {
function MemoryColumn({
rows,
}: {
rows: readonly ArchitectureEntityRow[];
}) {
return (
<ColumnCard heading="Memory" footer="append-only knowledge graph">
<div class="flex flex-col">
<StreamBox label="events" />
<DerivationArrow />
<EntitiesTable />
<EntitiesTable rows={rows} />
</div>
</ColumnCard>
);
Expand Down Expand Up @@ -319,15 +374,13 @@ function StreamBox({ label }: { label: string }) {
* monospace dashes/blocks so the table reads as a schema preview rather
* than fake data. Sits in the same box family as StreamBox.
*/
function EntitiesTable() {
function EntitiesTable({
rows,
}: {
rows: readonly ArchitectureEntityRow[];
}) {
const HEADERS = ["name", "type", "updated"] as const;
// Placeholder rows, labelled generically so the widget reads as a
// schema preview, not a snapshot of a real customer's data.
const ROWS: ReadonlyArray<readonly [string, string, string]> = [
["Customer A", "company", "2d"],
["Customer B", "person", "5h"],
["Customer C", "meeting", "1h"],
];
const ROWS = rows;
return (
<div
class="relative overflow-hidden rounded-lg border"
Expand Down
13 changes: 11 additions & 2 deletions packages/landing/src/components/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ type CodeBlockProps = {
collapsible?: boolean;
/** Initial open state when `collapsible`. Defaults to false (collapsed). */
defaultOpen?: boolean;
/**
* Cap the rendered code body's height (CSS value) and make it scrollable.
* Use on long snippets that would otherwise dominate a two-column section.
*/
maxHeight?: string;
};

type Token = { kind: TokenKind; text: string };
Expand Down Expand Up @@ -454,6 +459,7 @@ export function CodeBlock({
footnote,
collapsible = false,
defaultOpen = false,
maxHeight,
}: CodeBlockProps) {
const lines = highlight(snippet.code, snippet.language);
const [open, setOpen] = useState(defaultOpen);
Expand Down Expand Up @@ -521,8 +527,11 @@ export function CodeBlock({
{showBody ? (
<>
<pre
class="overflow-x-auto px-5 py-4 font-mono text-[12.5px] leading-[1.65]"
style={{ color: "var(--color-landing-code-text)" }}
class="overflow-auto px-5 py-4 font-mono text-[12.5px] leading-[1.65]"
style={{
color: "var(--color-landing-code-text)",
...(maxHeight ? { maxHeight } : {}),
}}
>
<code class="block">
{lines.map((toks, idx) => (
Expand Down
2 changes: 1 addition & 1 deletion packages/landing/src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const GITHUB_URL = "https://github.com/lobu-ai/lobu";
import { GITHUB_URL } from "../lib/urls";

type FooterColumn = {
heading: string;
Expand Down
Loading
Loading