mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
feat: README architecture diagrams + React Flow diagram studio (#245)
Replace the tall portrait README ingestion SVG with two landscape diagrams — "1 · Ingestion" (build the context layer) and "2 · Serving" (agents query it through MCP) — wired in as transparent 2x PNGs that read on GitHub light and dark. Add docs-site/diagram-studio: a static React Flow page with custom themed nodes and the inlined ktx mascot that renders both diagrams and exports them to PNG via html-to-image (the diagrams' reproducible source). Remove the superseded ingestion-flow SVGs.
This commit is contained in:
parent
d320d54ab2
commit
ba5bb92ab7
11 changed files with 1147 additions and 211 deletions
|
|
@ -34,9 +34,14 @@ business knowledge it builds and maintains for you.
|
||||||
> No extra usage billing from **ktx**.
|
> No extra usage billing from **ktx**.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs-site/public/images/ingestion-flow-transparent.svg" alt="ktx ingestion flow from source systems through validation to wiki and semantic-layer outputs" width="900" />
|
<img src="docs-site/public/images/ingestion-flow.png" alt="Ingestion: ktx ingests databases, BI tools, modeling code, and docs through its context engine (source connectors, context builder, reconciliation, validation) into wiki Markdown and semantic-layer YAML" width="900" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs-site/public/images/mcp-runtime-flow.png" alt="Serving: an agent queries ktx through MCP, which searches the wiki and semantic layer, returns approved metrics, and compiles them into read-only SQL run against the warehouse" width="900" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
## Why ktx
|
## Why ktx
|
||||||
|
|
||||||
General-purpose agents struggle on data tasks. They re-explore your warehouse
|
General-purpose agents struggle on data tasks. They re-explore your warehouse
|
||||||
|
|
|
||||||
12
docs-site/app/diagram-studio/page.tsx
Normal file
12
docs-site/app/diagram-studio/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
import { DiagramStudio } from "@/components/diagram-studio/studio";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Diagram studio",
|
||||||
|
robots: { index: false, follow: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DiagramStudioPage() {
|
||||||
|
return <DiagramStudio />;
|
||||||
|
}
|
||||||
328
docs-site/components/diagram-studio/flows.ts
Normal file
328
docs-site/components/diagram-studio/flows.ts
Normal file
|
|
@ -0,0 +1,328 @@
|
||||||
|
import { type Edge, MarkerType, type Node } from "@xyflow/react";
|
||||||
|
|
||||||
|
import { C } from "./nodes";
|
||||||
|
|
||||||
|
const EDGE_COLOR = "#b3bcc4";
|
||||||
|
const MARKER_COLOR = "#9aa6ad";
|
||||||
|
|
||||||
|
const labelStyle = {
|
||||||
|
fontFamily: "var(--font-inter), system-ui, sans-serif",
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 600,
|
||||||
|
fill: C.inkMuted,
|
||||||
|
};
|
||||||
|
const labelBgStyle = { fill: "#ffffff", stroke: C.chipBorder, strokeWidth: 1 };
|
||||||
|
const labelBg = {
|
||||||
|
labelBgPadding: [8, 4] as [number, number],
|
||||||
|
labelBgBorderRadius: 6,
|
||||||
|
labelStyle,
|
||||||
|
labelBgStyle,
|
||||||
|
};
|
||||||
|
|
||||||
|
const marker = { type: MarkerType.ArrowClosed, color: MARKER_COLOR, width: 16, height: 16 };
|
||||||
|
const edgeStyle = { stroke: EDGE_COLOR, strokeWidth: 2 };
|
||||||
|
|
||||||
|
/* ============================== INGESTION =============================== */
|
||||||
|
|
||||||
|
const SRC_W = 300;
|
||||||
|
const SRC_H = 138;
|
||||||
|
const SRC_GAP = 24;
|
||||||
|
const srcY = (i: number) => i * (SRC_H + SRC_GAP);
|
||||||
|
|
||||||
|
export const ingestionNodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: "title",
|
||||||
|
type: "title",
|
||||||
|
position: { x: 0, y: -96 },
|
||||||
|
data: {
|
||||||
|
width: 560,
|
||||||
|
eyebrow: "1 · Ingestion",
|
||||||
|
title: "ktx builds your context layer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "db",
|
||||||
|
type: "card",
|
||||||
|
position: { x: 0, y: srcY(0) },
|
||||||
|
data: {
|
||||||
|
width: SRC_W,
|
||||||
|
height: SRC_H,
|
||||||
|
accent: C.teal,
|
||||||
|
rows: [
|
||||||
|
{ kind: "title", text: "Databases" },
|
||||||
|
{ kind: "desc", text: "Schemas, keys, query history" },
|
||||||
|
{ kind: "muted", text: "Postgres · Snowflake · BigQuery · …" },
|
||||||
|
],
|
||||||
|
handles: [{ side: "right", type: "source", id: "out" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bi",
|
||||||
|
type: "card",
|
||||||
|
position: { x: 0, y: srcY(1) },
|
||||||
|
data: {
|
||||||
|
width: SRC_W,
|
||||||
|
height: SRC_H,
|
||||||
|
accent: C.orange,
|
||||||
|
rows: [
|
||||||
|
{ kind: "title", text: "BI tools" },
|
||||||
|
{ kind: "desc", text: "Dashboards, explores, usage" },
|
||||||
|
{ kind: "muted", text: "Metabase · Looker · …" },
|
||||||
|
],
|
||||||
|
handles: [{ side: "right", type: "source", id: "out" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "model",
|
||||||
|
type: "card",
|
||||||
|
position: { x: 0, y: srcY(2) },
|
||||||
|
data: {
|
||||||
|
width: SRC_W,
|
||||||
|
height: SRC_H,
|
||||||
|
accent: C.amber,
|
||||||
|
rows: [
|
||||||
|
{ kind: "title", text: "Modeling code" },
|
||||||
|
{ kind: "desc", text: "Metrics, models, joins, entities" },
|
||||||
|
{ kind: "muted", text: "dbt · LookML · MetricFlow · …" },
|
||||||
|
],
|
||||||
|
handles: [{ side: "right", type: "source", id: "out" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "docs",
|
||||||
|
type: "card",
|
||||||
|
position: { x: 0, y: srcY(3) },
|
||||||
|
data: {
|
||||||
|
width: SRC_W,
|
||||||
|
height: SRC_H,
|
||||||
|
accent: C.emerald,
|
||||||
|
rows: [
|
||||||
|
{ kind: "title", text: "Docs & notes" },
|
||||||
|
{ kind: "desc", text: "Policies, definitions, notes" },
|
||||||
|
{ kind: "muted", text: "Notion · any text · …" },
|
||||||
|
],
|
||||||
|
handles: [{ side: "right", type: "source", id: "out" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "engine",
|
||||||
|
type: "engine",
|
||||||
|
position: { x: 420, y: 52 },
|
||||||
|
data: {
|
||||||
|
width: 380,
|
||||||
|
height: 520,
|
||||||
|
steps: [
|
||||||
|
{ n: 1, title: "Source connectors", desc: "Read each source in its shape" },
|
||||||
|
{ n: 2, title: "Context builder", desc: "Evidence into proposed updates" },
|
||||||
|
{ n: 3, title: "Reconciliation", desc: "Merge with existing context" },
|
||||||
|
{ n: 4, title: "Validation", desc: "Check references & semantics" },
|
||||||
|
],
|
||||||
|
handles: [
|
||||||
|
{ side: "left", type: "target", id: "in" },
|
||||||
|
{ side: "right", type: "source", id: "out" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "wiki",
|
||||||
|
type: "card",
|
||||||
|
position: { x: 900, y: 66 },
|
||||||
|
data: {
|
||||||
|
width: 320,
|
||||||
|
height: 220,
|
||||||
|
accent: C.emerald,
|
||||||
|
rows: [
|
||||||
|
{ kind: "mono", text: "wiki/*.md", color: C.emerald },
|
||||||
|
{ kind: "title", text: "Wiki" },
|
||||||
|
{ kind: "chips", items: ["free-form", "auto-maintained"] },
|
||||||
|
{ kind: "desc", text: "Definitions, caveats, policies," },
|
||||||
|
{ kind: "desc", text: "and notes agents can search." },
|
||||||
|
],
|
||||||
|
handles: [{ side: "left", type: "target", id: "in" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sl",
|
||||||
|
type: "card",
|
||||||
|
position: { x: 900, y: 338 },
|
||||||
|
data: {
|
||||||
|
width: 320,
|
||||||
|
height: 220,
|
||||||
|
accent: C.teal,
|
||||||
|
rows: [
|
||||||
|
{ kind: "mono", text: "semantic-layer/*.yaml", color: C.teal },
|
||||||
|
{ kind: "title", text: "Semantic layer" },
|
||||||
|
{ kind: "chips", items: ["executable", "auto-maintained"] },
|
||||||
|
{ kind: "desc", text: "Metrics, joins, dimensions, and" },
|
||||||
|
{ kind: "desc", text: "filters ktx compiles into SQL." },
|
||||||
|
],
|
||||||
|
handles: [{ side: "left", type: "target", id: "in" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ingestEdge = (source: string, target: string): Edge => ({
|
||||||
|
id: `${source}-${target}`,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
sourceHandle: "out",
|
||||||
|
targetHandle: "in",
|
||||||
|
type: "default",
|
||||||
|
style: edgeStyle,
|
||||||
|
markerEnd: marker,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ingestionEdges: Edge[] = [
|
||||||
|
ingestEdge("db", "engine"),
|
||||||
|
ingestEdge("bi", "engine"),
|
||||||
|
ingestEdge("model", "engine"),
|
||||||
|
ingestEdge("docs", "engine"),
|
||||||
|
ingestEdge("engine", "wiki"),
|
||||||
|
ingestEdge("engine", "sl"),
|
||||||
|
];
|
||||||
|
|
||||||
|
/* =============================== RUNTIME ================================ */
|
||||||
|
|
||||||
|
export const runtimeNodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: "title",
|
||||||
|
type: "title",
|
||||||
|
position: { x: 0, y: -84 },
|
||||||
|
data: {
|
||||||
|
width: 560,
|
||||||
|
eyebrow: "2 · Serving",
|
||||||
|
title: "agents query it through MCP",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "agent",
|
||||||
|
type: "card",
|
||||||
|
position: { x: 0, y: 115 },
|
||||||
|
data: {
|
||||||
|
width: 280,
|
||||||
|
height: 190,
|
||||||
|
accent: C.neutral,
|
||||||
|
align: "center",
|
||||||
|
rows: [
|
||||||
|
{ kind: "title", text: "Your agent" },
|
||||||
|
{ kind: "muted", text: "Claude Code · Cursor" },
|
||||||
|
{ kind: "muted", text: "Codex · OpenCode" },
|
||||||
|
],
|
||||||
|
handles: [
|
||||||
|
{ side: "right", type: "source", id: "ask", top: "42%" },
|
||||||
|
{ side: "right", type: "target", id: "answer", top: "62%" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "hub",
|
||||||
|
type: "hub",
|
||||||
|
position: { x: 420, y: 85 },
|
||||||
|
data: {
|
||||||
|
width: 360,
|
||||||
|
height: 250,
|
||||||
|
rows: [
|
||||||
|
"Search wiki + semantic layer",
|
||||||
|
"Return approved metrics",
|
||||||
|
"Compile metrics → SQL",
|
||||||
|
],
|
||||||
|
handles: [
|
||||||
|
{ side: "left", type: "target", id: "ask", top: "42%" },
|
||||||
|
{ side: "left", type: "source", id: "answer", top: "62%" },
|
||||||
|
{ side: "right", type: "source", id: "to-context", top: "30%" },
|
||||||
|
{ side: "right", type: "source", id: "to-warehouse", top: "72%" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "context",
|
||||||
|
type: "card",
|
||||||
|
position: { x: 920, y: 15 },
|
||||||
|
data: {
|
||||||
|
width: 300,
|
||||||
|
height: 150,
|
||||||
|
accent: C.teal,
|
||||||
|
rows: [
|
||||||
|
{ kind: "title", text: "Context layer" },
|
||||||
|
{ kind: "mono", text: "wiki/*.md", color: C.emerald },
|
||||||
|
{ kind: "mono", text: "semantic-layer/*.yaml", color: C.teal },
|
||||||
|
],
|
||||||
|
handles: [{ side: "left", type: "target", id: "in" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "warehouse",
|
||||||
|
type: "card",
|
||||||
|
position: { x: 920, y: 255 },
|
||||||
|
data: {
|
||||||
|
width: 300,
|
||||||
|
height: 150,
|
||||||
|
accent: C.slate,
|
||||||
|
rows: [
|
||||||
|
{ kind: "title", text: "Warehouse" },
|
||||||
|
{
|
||||||
|
kind: "badge",
|
||||||
|
text: "read-only",
|
||||||
|
bg: "#ecf6f8",
|
||||||
|
border: "#bfe3ea",
|
||||||
|
color: C.teal,
|
||||||
|
},
|
||||||
|
{ kind: "desc", text: "Runs the compiled SQL" },
|
||||||
|
],
|
||||||
|
handles: [{ side: "left", type: "target", id: "in" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const runtimeEdges: Edge[] = [
|
||||||
|
{
|
||||||
|
id: "ask",
|
||||||
|
source: "agent",
|
||||||
|
sourceHandle: "ask",
|
||||||
|
target: "hub",
|
||||||
|
targetHandle: "ask",
|
||||||
|
type: "default",
|
||||||
|
label: "ask",
|
||||||
|
...labelBg,
|
||||||
|
style: edgeStyle,
|
||||||
|
markerEnd: marker,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "answer",
|
||||||
|
source: "hub",
|
||||||
|
sourceHandle: "answer",
|
||||||
|
target: "agent",
|
||||||
|
targetHandle: "answer",
|
||||||
|
type: "default",
|
||||||
|
label: "answer",
|
||||||
|
...labelBg,
|
||||||
|
style: edgeStyle,
|
||||||
|
markerEnd: marker,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "search",
|
||||||
|
source: "hub",
|
||||||
|
sourceHandle: "to-context",
|
||||||
|
target: "context",
|
||||||
|
targetHandle: "in",
|
||||||
|
type: "default",
|
||||||
|
label: "search",
|
||||||
|
...labelBg,
|
||||||
|
style: edgeStyle,
|
||||||
|
markerStart: marker,
|
||||||
|
markerEnd: marker,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "readonly",
|
||||||
|
source: "hub",
|
||||||
|
sourceHandle: "to-warehouse",
|
||||||
|
target: "warehouse",
|
||||||
|
targetHandle: "in",
|
||||||
|
type: "default",
|
||||||
|
label: "read-only",
|
||||||
|
...labelBg,
|
||||||
|
style: edgeStyle,
|
||||||
|
markerStart: marker,
|
||||||
|
markerEnd: marker,
|
||||||
|
},
|
||||||
|
];
|
||||||
57
docs-site/components/diagram-studio/mascot.tsx
Normal file
57
docs-site/components/diagram-studio/mascot.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* Inlined ktx mascot, ported from assets/ktx-mascot.svg.
|
||||||
|
*
|
||||||
|
* - `light` renders the dark-bodied mascot for light surfaces.
|
||||||
|
* - `dark` renders the cream-bodied mascot for dark surfaces (e.g. the ktx
|
||||||
|
* hub panel), mirroring brand/ktx-mascot-dark.svg.
|
||||||
|
*/
|
||||||
|
export function KtxMascot({
|
||||||
|
variant = "light",
|
||||||
|
size = 56,
|
||||||
|
}: {
|
||||||
|
variant?: "light" | "dark";
|
||||||
|
size?: number;
|
||||||
|
}) {
|
||||||
|
const body = variant === "dark" ? "#F5F1EA" : "#1B3139";
|
||||||
|
const eye = variant === "dark" ? "#1B3139" : "#F5F1EA";
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 200 200"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
role="img"
|
||||||
|
aria-label="ktx mascot"
|
||||||
|
>
|
||||||
|
<g fill="none" stroke={body} strokeWidth="16" strokeLinecap="round">
|
||||||
|
<path d="M 62 110 Q 32 130 44 152" />
|
||||||
|
<path d="M 88 116 Q 80 152 70 174" />
|
||||||
|
<path d="M 112 116 Q 120 152 130 174" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
d="M 134 108 C 162 116, 172 96, 162 78 C 154 64, 168 56, 178 60"
|
||||||
|
fill="none"
|
||||||
|
stroke="#FF8A4C"
|
||||||
|
strokeWidth="16"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 48 102 C 48 56, 78 30, 100 30 C 122 30, 152 56, 152 102 C 152 116, 132 120, 100 120 C 68 120, 48 116, 48 102 Z"
|
||||||
|
fill={body}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 80 84 Q 86 77 92 84"
|
||||||
|
fill="none"
|
||||||
|
stroke={eye}
|
||||||
|
strokeWidth="3.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 108 84 Q 114 77 120 84"
|
||||||
|
fill="none"
|
||||||
|
stroke={eye}
|
||||||
|
strokeWidth="3.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
493
docs-site/components/diagram-studio/nodes.tsx
Normal file
493
docs-site/components/diagram-studio/nodes.tsx
Normal file
|
|
@ -0,0 +1,493 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
||||||
|
|
||||||
|
import { KtxMascot } from "./mascot";
|
||||||
|
|
||||||
|
/** Fixed palette mirrored from the approved SVG diagrams so the exported PNG
|
||||||
|
* is theme-independent (one image that reads on light and dark GitHub). */
|
||||||
|
export const C = {
|
||||||
|
ink: "#1b1b18",
|
||||||
|
inkSoft: "#57534e",
|
||||||
|
inkMuted: "#8c857f",
|
||||||
|
cardBorder: "#e2dfd9",
|
||||||
|
engineBg: "#15323a",
|
||||||
|
engineBorder: "#23474f",
|
||||||
|
cyan: "#55dced",
|
||||||
|
stepNum: "#06262c",
|
||||||
|
stepTitle: "#f3f1ec",
|
||||||
|
stepDesc: "#9fb6bc",
|
||||||
|
hubRow: "#eef4f5",
|
||||||
|
chipBg: "#faf9f6",
|
||||||
|
chipBorder: "#e7e5e4",
|
||||||
|
teal: "#0e7490",
|
||||||
|
emerald: "#059669",
|
||||||
|
orange: "#f97316",
|
||||||
|
amber: "#d97706",
|
||||||
|
slate: "#334155",
|
||||||
|
neutral: "#94a3b8",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const DISPLAY = "var(--font-display), system-ui, sans-serif";
|
||||||
|
const BODY = "var(--font-inter), system-ui, sans-serif";
|
||||||
|
const MONO = "var(--font-mono), ui-monospace, monospace";
|
||||||
|
|
||||||
|
const CARD_SHADOW = "0 3px 12px rgba(27, 49, 57, 0.10)";
|
||||||
|
const ENGINE_SHADOW = "0 6px 22px rgba(2, 12, 15, 0.30)";
|
||||||
|
|
||||||
|
/** ktx logo mascot size, shared by the engine and hub headers. */
|
||||||
|
const LOGO_SIZE = 56;
|
||||||
|
|
||||||
|
type HandleSpec = {
|
||||||
|
side: "left" | "right";
|
||||||
|
type: "source" | "target";
|
||||||
|
id: string;
|
||||||
|
top?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Handles({ specs }: { specs?: HandleSpec[] }) {
|
||||||
|
if (!specs) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{specs.map((h) => (
|
||||||
|
<Handle
|
||||||
|
key={`${h.type}-${h.id}`}
|
||||||
|
id={h.id}
|
||||||
|
type={h.type}
|
||||||
|
position={h.side === "left" ? Position.Left : Position.Right}
|
||||||
|
isConnectable={false}
|
||||||
|
style={{
|
||||||
|
opacity: 0,
|
||||||
|
border: 0,
|
||||||
|
background: "transparent",
|
||||||
|
...(h.top ? { top: h.top } : {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------- Card node ------------------------------- */
|
||||||
|
|
||||||
|
type CardRow =
|
||||||
|
| { kind: "title"; text: string }
|
||||||
|
| { kind: "mono"; text: string; color: string }
|
||||||
|
| { kind: "desc"; text: string }
|
||||||
|
| { kind: "muted"; text: string }
|
||||||
|
| { kind: "chips"; items: string[] }
|
||||||
|
| { kind: "badge"; text: string; bg: string; border: string; color: string };
|
||||||
|
|
||||||
|
type CardData = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
accent: string;
|
||||||
|
align?: "center";
|
||||||
|
rows: CardRow[];
|
||||||
|
handles?: HandleSpec[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function gapFor(kind: CardRow["kind"], prev?: CardRow["kind"]): number {
|
||||||
|
if (!prev) return 0;
|
||||||
|
if (kind === "desc" && prev === "desc") return 3;
|
||||||
|
if (kind === "mono" && prev === "mono") return 2;
|
||||||
|
if (kind === "title") return 6;
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardRowView({ row }: { row: CardRow }) {
|
||||||
|
switch (row.kind) {
|
||||||
|
case "title":
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: DISPLAY,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 26,
|
||||||
|
lineHeight: 1.15,
|
||||||
|
color: C.ink,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.text}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case "mono":
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: MONO,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 18,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
color: row.color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.text}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case "desc":
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: BODY,
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: 17,
|
||||||
|
lineHeight: 1.45,
|
||||||
|
color: C.inkSoft,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.text}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case "muted":
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: BODY,
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
color: C.inkMuted,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.text}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case "chips":
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||||
|
{row.items.map((c) => (
|
||||||
|
<span
|
||||||
|
key={c}
|
||||||
|
style={{
|
||||||
|
fontFamily: BODY,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 14,
|
||||||
|
color: C.inkSoft,
|
||||||
|
background: C.chipBg,
|
||||||
|
border: `1px solid ${C.chipBorder}`,
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: "4px 10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{c}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "badge":
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: 14,
|
||||||
|
padding: "3px 12px",
|
||||||
|
fontFamily: BODY,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 14,
|
||||||
|
background: row.bg,
|
||||||
|
border: `1px solid ${row.border}`,
|
||||||
|
color: row.color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.text}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardNode({ data }: NodeProps<Node<CardData>>) {
|
||||||
|
const center = data.align === "center";
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: data.width,
|
||||||
|
height: data.height,
|
||||||
|
position: "relative",
|
||||||
|
background: "#ffffff",
|
||||||
|
border: `1px solid ${C.cardBorder}`,
|
||||||
|
borderRadius: 10,
|
||||||
|
boxShadow: CARD_SHADOW,
|
||||||
|
padding: "18px 20px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: center ? "center" : "flex-start",
|
||||||
|
justifyContent: center ? "center" : "flex-start",
|
||||||
|
textAlign: center ? "center" : "left",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 2,
|
||||||
|
right: 2,
|
||||||
|
height: 4,
|
||||||
|
borderRadius: 2,
|
||||||
|
background: data.accent,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Handles specs={data.handles} />
|
||||||
|
{data.rows.map((row, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{ marginTop: gapFor(row.kind, data.rows[i - 1]?.kind) }}
|
||||||
|
>
|
||||||
|
<CardRowView row={row} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------ Engine node ------------------------------ */
|
||||||
|
|
||||||
|
type EngineStep = { n: number; title: string; desc: string };
|
||||||
|
|
||||||
|
type EngineData = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
steps: EngineStep[];
|
||||||
|
handles?: HandleSpec[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function EngineNode({ data }: NodeProps<Node<EngineData>>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: data.width,
|
||||||
|
height: data.height,
|
||||||
|
position: "relative",
|
||||||
|
background: C.engineBg,
|
||||||
|
border: `1px solid ${C.engineBorder}`,
|
||||||
|
borderRadius: 14,
|
||||||
|
boxShadow: ENGINE_SHADOW,
|
||||||
|
padding: "24px 24px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 2,
|
||||||
|
right: 2,
|
||||||
|
height: 4,
|
||||||
|
borderRadius: 2,
|
||||||
|
background: C.cyan,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Handles specs={data.handles} />
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 14 }}>
|
||||||
|
<KtxMascot variant="dark" size={LOGO_SIZE} />
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: DISPLAY,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 30,
|
||||||
|
color: C.stepTitle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ktx
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-around",
|
||||||
|
marginTop: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data.steps.map((s) => (
|
||||||
|
<div
|
||||||
|
key={s.n}
|
||||||
|
style={{ display: "flex", alignItems: "center", gap: 18 }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
flex: "none",
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: C.cyan,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontFamily: DISPLAY,
|
||||||
|
fontWeight: 800,
|
||||||
|
fontSize: 22,
|
||||||
|
color: C.stepNum,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s.n}
|
||||||
|
</span>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: DISPLAY,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 24,
|
||||||
|
lineHeight: 1.1,
|
||||||
|
color: C.stepTitle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s.title}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: BODY,
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: 16,
|
||||||
|
lineHeight: 1.3,
|
||||||
|
color: C.stepDesc,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s.desc}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------- Hub node ------------------------------- */
|
||||||
|
|
||||||
|
type HubData = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
rows: string[];
|
||||||
|
handles?: HandleSpec[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function HubNode({ data }: NodeProps<Node<HubData>>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: data.width,
|
||||||
|
height: data.height,
|
||||||
|
position: "relative",
|
||||||
|
background: C.engineBg,
|
||||||
|
border: `1px solid ${C.engineBorder}`,
|
||||||
|
borderRadius: 14,
|
||||||
|
boxShadow: ENGINE_SHADOW,
|
||||||
|
padding: "24px 24px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 2,
|
||||||
|
right: 2,
|
||||||
|
height: 4,
|
||||||
|
borderRadius: 2,
|
||||||
|
background: C.cyan,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Handles specs={data.handles} />
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 14 }}>
|
||||||
|
<KtxMascot variant="dark" size={LOGO_SIZE} />
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: DISPLAY,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 30,
|
||||||
|
color: C.stepTitle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ktx
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 22,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 18,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data.rows.map((r) => (
|
||||||
|
<div key={r} style={{ display: "flex", alignItems: "center", gap: 14 }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
flex: "none",
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: C.cyan,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: BODY,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 19,
|
||||||
|
color: C.hubRow,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{r}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------- Title node ------------------------------ */
|
||||||
|
|
||||||
|
type TitleData = { width: number; eyebrow: string; title: string };
|
||||||
|
|
||||||
|
function TitleNode({ data }: NodeProps<Node<TitleData>>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: data.width,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: BODY,
|
||||||
|
fontSize: 19,
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: 2,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
color: C.teal,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data.eyebrow}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: DISPLAY,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: C.inkMuted,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const nodeTypes = {
|
||||||
|
card: CardNode,
|
||||||
|
engine: EngineNode,
|
||||||
|
hub: HubNode,
|
||||||
|
title: TitleNode,
|
||||||
|
};
|
||||||
242
docs-site/components/diagram-studio/studio.tsx
Normal file
242
docs-site/components/diagram-studio/studio.tsx
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import "@xyflow/react/dist/style.css";
|
||||||
|
|
||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Background,
|
||||||
|
BackgroundVariant,
|
||||||
|
type Edge,
|
||||||
|
getNodesBounds,
|
||||||
|
type Node,
|
||||||
|
ReactFlow,
|
||||||
|
ReactFlowProvider,
|
||||||
|
useEdgesState,
|
||||||
|
useNodesState,
|
||||||
|
useReactFlow,
|
||||||
|
} from "@xyflow/react";
|
||||||
|
import { toPng } from "html-to-image";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ingestionEdges,
|
||||||
|
ingestionNodes,
|
||||||
|
runtimeEdges,
|
||||||
|
runtimeNodes,
|
||||||
|
} from "./flows";
|
||||||
|
import { nodeTypes } from "./nodes";
|
||||||
|
|
||||||
|
const EXPORT_PADDING = 48;
|
||||||
|
const EXPORT_PIXEL_RATIO = 2;
|
||||||
|
|
||||||
|
function DiagramCanvasInner({
|
||||||
|
initialNodes,
|
||||||
|
initialEdges,
|
||||||
|
fileName,
|
||||||
|
height,
|
||||||
|
dark,
|
||||||
|
}: {
|
||||||
|
initialNodes: Node[];
|
||||||
|
initialEdges: Edge[];
|
||||||
|
fileName: string;
|
||||||
|
height: number;
|
||||||
|
dark: boolean;
|
||||||
|
}) {
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||||
|
const [edges, , onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
const { getNodes } = useReactFlow();
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
|
const download = useCallback(async () => {
|
||||||
|
const viewport = wrapperRef.current?.querySelector<HTMLElement>(
|
||||||
|
".react-flow__viewport",
|
||||||
|
);
|
||||||
|
if (!viewport) return;
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await document.fonts.ready;
|
||||||
|
const bounds = getNodesBounds(getNodes());
|
||||||
|
const outW = Math.ceil(bounds.width + EXPORT_PADDING * 2);
|
||||||
|
const outH = Math.ceil(bounds.height + EXPORT_PADDING * 2);
|
||||||
|
const tx = EXPORT_PADDING - bounds.x;
|
||||||
|
const ty = EXPORT_PADDING - bounds.y;
|
||||||
|
const dataUrl = await toPng(viewport, {
|
||||||
|
width: outW,
|
||||||
|
height: outH,
|
||||||
|
pixelRatio: EXPORT_PIXEL_RATIO,
|
||||||
|
// transparent background so one PNG works on light and dark GitHub
|
||||||
|
style: {
|
||||||
|
width: `${outW}px`,
|
||||||
|
height: `${outH}px`,
|
||||||
|
transform: `translate(${tx}px, ${ty}px) scale(1)`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = fileName;
|
||||||
|
link.href = dataUrl;
|
||||||
|
link.click();
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}, [fileName, getNodes]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: "flex", gap: 8, marginBottom: 10 }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={download}
|
||||||
|
disabled={busy}
|
||||||
|
style={btnStyle(busy)}
|
||||||
|
>
|
||||||
|
{busy ? "Exporting…" : "Download PNG"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={wrapperRef}
|
||||||
|
style={{
|
||||||
|
height,
|
||||||
|
borderRadius: 12,
|
||||||
|
border: "1px solid rgba(127,127,127,0.2)",
|
||||||
|
background: dark ? "#0d1117" : "#ffffff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
fitView
|
||||||
|
fitViewOptions={{ padding: 0.08 }}
|
||||||
|
nodesDraggable={false}
|
||||||
|
nodesConnectable={false}
|
||||||
|
nodesFocusable={false}
|
||||||
|
edgesFocusable={false}
|
||||||
|
elementsSelectable={false}
|
||||||
|
panOnDrag={false}
|
||||||
|
panOnScroll={false}
|
||||||
|
zoomOnScroll={false}
|
||||||
|
zoomOnPinch={false}
|
||||||
|
zoomOnDoubleClick={false}
|
||||||
|
preventScrolling={false}
|
||||||
|
proOptions={{ hideAttribution: true }}
|
||||||
|
>
|
||||||
|
<Background
|
||||||
|
variant={BackgroundVariant.Dots}
|
||||||
|
gap={18}
|
||||||
|
size={1}
|
||||||
|
color={dark ? "#1f2a30" : "#e6e2db"}
|
||||||
|
/>
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function btnStyle(disabled: boolean): React.CSSProperties {
|
||||||
|
return {
|
||||||
|
fontFamily: "var(--font-inter), system-ui, sans-serif",
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 600,
|
||||||
|
padding: "7px 14px",
|
||||||
|
borderRadius: 8,
|
||||||
|
border: "1px solid #0e7490",
|
||||||
|
background: disabled ? "#9bbdc6" : "#0e7490",
|
||||||
|
color: "#ffffff",
|
||||||
|
cursor: disabled ? "default" : "pointer",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function DiagramCanvas(props: {
|
||||||
|
initialNodes: Node[];
|
||||||
|
initialEdges: Edge[];
|
||||||
|
fileName: string;
|
||||||
|
height: number;
|
||||||
|
dark: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<DiagramCanvasInner {...props} />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DiagramStudio() {
|
||||||
|
const [dark, setDark] = useState(false);
|
||||||
|
return (
|
||||||
|
<main
|
||||||
|
style={{
|
||||||
|
maxWidth: 1320,
|
||||||
|
margin: "0 auto",
|
||||||
|
padding: "32px 24px 80px",
|
||||||
|
fontFamily: "var(--font-inter), system-ui, sans-serif",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<header style={{ marginBottom: 24 }}>
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
fontFamily: "var(--font-display), system-ui, sans-serif",
|
||||||
|
fontSize: 30,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "#1b1b18",
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ktx diagram studio
|
||||||
|
</h1>
|
||||||
|
<p style={{ color: "#6b6560", marginTop: 6, fontSize: 15 }}>
|
||||||
|
Static diagrams. Export is a transparent 2× PNG framed to the node
|
||||||
|
bounds — the dark-background toggle is only for previewing.
|
||||||
|
</p>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
marginTop: 12,
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#57534e",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={dark}
|
||||||
|
onChange={(e) => setDark(e.target.checked)}
|
||||||
|
/>
|
||||||
|
Preview on dark background
|
||||||
|
</label>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section style={{ marginBottom: 40 }}>
|
||||||
|
<h2 style={sectionTitle}>1 · Ingestion — building the context layer</h2>
|
||||||
|
<DiagramCanvas
|
||||||
|
initialNodes={ingestionNodes}
|
||||||
|
initialEdges={ingestionEdges}
|
||||||
|
fileName="ingestion-flow.png"
|
||||||
|
height={560}
|
||||||
|
dark={dark}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 style={sectionTitle}>2 · Serving — answering agents at runtime</h2>
|
||||||
|
<DiagramCanvas
|
||||||
|
initialNodes={runtimeNodes}
|
||||||
|
initialEdges={runtimeEdges}
|
||||||
|
fileName="mcp-runtime-flow.png"
|
||||||
|
height={480}
|
||||||
|
dark={dark}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionTitle: React.CSSProperties = {
|
||||||
|
fontFamily: "var(--font-display), system-ui, sans-serif",
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#1b1b18",
|
||||||
|
marginBottom: 12,
|
||||||
|
};
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"fumadocs-core": "16.8.10",
|
"fumadocs-core": "16.8.10",
|
||||||
"fumadocs-mdx": "15.0.7",
|
"fumadocs-mdx": "15.0.7",
|
||||||
"fumadocs-ui": "16.8.10",
|
"fumadocs-ui": "16.8.10",
|
||||||
|
"html-to-image": "1.11.11",
|
||||||
"next": "^16",
|
"next": "^16",
|
||||||
"react": "19.2.6",
|
"react": "19.2.6",
|
||||||
"react-dom": "19.2.6"
|
"react-dom": "19.2.6"
|
||||||
|
|
|
||||||
|
|
@ -1,210 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1346" height="1710" viewBox="0 0 1346 1710" role="img" aria-labelledby="title desc">
|
|
||||||
<title id="title">ktx ingestion flow</title>
|
|
||||||
<desc id="desc">Source systems flow through source connectors, context builder, reconciliation, and validation to create wiki Markdown and semantic-layer YAML outputs.</desc>
|
|
||||||
<defs>
|
|
||||||
<filter id="card-shadow" x="-12%" y="-12%" width="124%" height="124%" color-interpolation-filters="sRGB">
|
|
||||||
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#0f172a" flood-opacity="0.14"/>
|
|
||||||
</filter>
|
|
||||||
<filter id="dark-shadow" x="-12%" y="-12%" width="124%" height="124%" color-interpolation-filters="sRGB">
|
|
||||||
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#020617" flood-opacity="0.22"/>
|
|
||||||
</filter>
|
|
||||||
<filter id="glow-blue" x="-160%" y="-160%" width="420%" height="420%">
|
|
||||||
<feGaussianBlur stdDeviation="7" result="blur"/>
|
|
||||||
<feMerge>
|
|
||||||
<feMergeNode in="blur"/>
|
|
||||||
<feMergeNode in="SourceGraphic"/>
|
|
||||||
</feMerge>
|
|
||||||
</filter>
|
|
||||||
<marker id="arrow" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="9" markerHeight="9" orient="auto-start-reverse">
|
|
||||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#94a3b8"/>
|
|
||||||
</marker>
|
|
||||||
<style>
|
|
||||||
.card { fill: #ffffff; stroke: #e2e8f0; stroke-width: 1.4; filter: url(#card-shadow); }
|
|
||||||
.stage { fill: #0b1f23; stroke: #17343a; stroke-width: 1.2; filter: url(#dark-shadow); }
|
|
||||||
.title { fill: #24272d; font: 700 28px Inter, Arial, sans-serif; }
|
|
||||||
.body { fill: #666b73; font: 500 18px Inter, Arial, sans-serif; }
|
|
||||||
.tag { fill: #6b7280; font: 500 16px Inter, Arial, sans-serif; }
|
|
||||||
.mono { font: 700 20px "SFMono-Regular", Consolas, monospace; }
|
|
||||||
.stage-title { fill: #f8fafc; font: 700 28px Inter, Arial, sans-serif; }
|
|
||||||
.stage-body { fill: #b8c6ca; font: 500 20px Inter, Arial, sans-serif; }
|
|
||||||
.index { fill: #07313a; font: 700 22px Inter, Arial, sans-serif; text-anchor: middle; dominant-baseline: middle; }
|
|
||||||
.edge { fill: none; stroke: #94a3b8; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
|
||||||
.dash { fill: none; stroke: #64748b; stroke-width: 1.8; stroke-dasharray: 5 8; stroke-linecap: round; }
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<g id="source-cards">
|
|
||||||
<g transform="translate(24 39)">
|
|
||||||
<rect class="card" x="0" y="0" width="298" height="285" rx="4"/>
|
|
||||||
<rect x="0" y="0" width="298" height="4" rx="2" fill="#3b82f6"/>
|
|
||||||
<text class="title" x="22" y="52">Databases</text>
|
|
||||||
<text class="body" x="22" y="92">Schemas, columns, keys,</text>
|
|
||||||
<text class="body" x="22" y="120">row counts, and query</text>
|
|
||||||
<text class="body" x="22" y="148">history.</text>
|
|
||||||
<g transform="translate(22 180)">
|
|
||||||
<rect x="0" y="0" width="112" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="12" y="24">PostgreSQL</text>
|
|
||||||
<rect x="120" y="0" width="100" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="132" y="24">Snowflake</text>
|
|
||||||
<rect x="0" y="46" width="92" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="12" y="70">BigQuery</text>
|
|
||||||
<rect x="100" y="46" width="74" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="112" y="70">SQLite</text>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g transform="translate(358 39)">
|
|
||||||
<rect class="card" x="0" y="0" width="298" height="285" rx="4"/>
|
|
||||||
<rect x="0" y="0" width="298" height="4" rx="2" fill="#f97316"/>
|
|
||||||
<text class="title" x="22" y="52">BI tools</text>
|
|
||||||
<text class="body" x="22" y="92">Dashboards, questions,</text>
|
|
||||||
<text class="body" x="22" y="120">explores, usage, and trusted</text>
|
|
||||||
<text class="body" x="22" y="148">examples.</text>
|
|
||||||
<g transform="translate(22 180)">
|
|
||||||
<rect x="0" y="0" width="96" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="12" y="24">Metabase</text>
|
|
||||||
<rect x="104" y="0" width="74" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="116" y="24">Looker</text>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g transform="translate(692 39)">
|
|
||||||
<rect class="card" x="0" y="0" width="298" height="285" rx="4"/>
|
|
||||||
<rect x="0" y="0" width="298" height="4" rx="2" fill="#f59e0b"/>
|
|
||||||
<text class="title" x="22" y="52">Modeling code</text>
|
|
||||||
<text class="body" x="22" y="92">Existing metrics, dimensions,</text>
|
|
||||||
<text class="body" x="22" y="120">models, joins, and entities.</text>
|
|
||||||
<g transform="translate(22 152)">
|
|
||||||
<rect x="0" y="0" width="48" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="12" y="24">dbt</text>
|
|
||||||
<rect x="56" y="0" width="82" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="68" y="24">LookML</text>
|
|
||||||
<rect x="0" y="46" width="102" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="12" y="70">MetricFlow</text>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g transform="translate(1026 39)">
|
|
||||||
<rect class="card" x="0" y="0" width="298" height="285" rx="4"/>
|
|
||||||
<rect x="0" y="0" width="298" height="4" rx="2" fill="#10b981"/>
|
|
||||||
<text class="title" x="22" y="52">Docs and notes</text>
|
|
||||||
<text class="body" x="22" y="92">Policies, caveats, team</text>
|
|
||||||
<text class="body" x="22" y="120">definitions, and analyst</text>
|
|
||||||
<text class="body" x="22" y="148">context.</text>
|
|
||||||
<g transform="translate(22 180)">
|
|
||||||
<rect x="0" y="0" width="72" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="12" y="24">Notion</text>
|
|
||||||
<rect x="80" y="0" width="84" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="92" y="24">Any text</text>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g id="edges">
|
|
||||||
<path class="edge" d="M172 324 V380 Q172 394 186 394 H507 Q507 394 507 380 V324"/>
|
|
||||||
<path class="edge" d="M841 324 V380 Q841 394 827 394 H507"/>
|
|
||||||
<path class="edge" d="M1175 324 V380 Q1175 394 1161 394 H673 Q673 394 673 408 V433" marker-end="url(#arrow)"/>
|
|
||||||
<path class="edge" d="M507 394 H673"/>
|
|
||||||
<path class="edge" d="M673 618 V651" marker-end="url(#arrow)"/>
|
|
||||||
<path class="edge" d="M673 833 V866" marker-end="url(#arrow)"/>
|
|
||||||
<path class="edge" d="M673 1048 V1081" marker-end="url(#arrow)"/>
|
|
||||||
<path class="edge" d="M673 1262 V1310 Q673 1325 656 1325 H305 Q291 1325 291 1339 V1364" marker-end="url(#arrow)"/>
|
|
||||||
<path class="edge" d="M673 1262 V1310 Q673 1325 690 1325 H1043 Q1057 1325 1057 1339 V1364" marker-end="url(#arrow)"/>
|
|
||||||
<path class="dash" d="M546 1523 H800"/>
|
|
||||||
<path d="M546 1523 l9 -6 v12 z" fill="#64748b"/>
|
|
||||||
<path d="M800 1523 l-9 -6 v12 z" fill="#64748b"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g id="particles">
|
|
||||||
<circle cx="256" cy="394" r="18" fill="#3b82f6" opacity="0.18" filter="url(#glow-blue)"/>
|
|
||||||
<circle cx="256" cy="394" r="6" fill="#3b82f6" opacity="0.9"/>
|
|
||||||
<circle cx="632" cy="394" r="18" fill="#f97316" opacity="0.18" filter="url(#glow-blue)"/>
|
|
||||||
<circle cx="632" cy="394" r="6" fill="#f97316" opacity="0.9"/>
|
|
||||||
<circle cx="830" cy="394" r="18" fill="#10b981" opacity="0.18" filter="url(#glow-blue)"/>
|
|
||||||
<circle cx="830" cy="394" r="6" fill="#10b981" opacity="0.9"/>
|
|
||||||
<circle cx="673" cy="635" r="17" fill="#10b981" opacity="0.18" filter="url(#glow-blue)"/>
|
|
||||||
<circle cx="673" cy="635" r="6" fill="#10b981" opacity="0.9"/>
|
|
||||||
<circle cx="673" cy="1065" r="17" fill="#f59e0b" opacity="0.18" filter="url(#glow-blue)"/>
|
|
||||||
<circle cx="673" cy="1065" r="6" fill="#f59e0b" opacity="0.9"/>
|
|
||||||
<circle cx="573" cy="1322" r="17" fill="#3b82f6" opacity="0.18" filter="url(#glow-blue)"/>
|
|
||||||
<circle cx="573" cy="1322" r="6" fill="#3b82f6" opacity="0.9"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g id="stages">
|
|
||||||
<g transform="translate(464 438)">
|
|
||||||
<rect class="stage" x="0" y="0" width="420" height="180" rx="4"/>
|
|
||||||
<circle cx="52" cy="90" r="26" fill="#55dced"/>
|
|
||||||
<text class="index" x="52" y="90">1</text>
|
|
||||||
<text class="stage-title" x="98" y="72">Source connectors</text>
|
|
||||||
<text class="stage-body" x="98" y="110">Read each configured system in</text>
|
|
||||||
<text class="stage-body" x="98" y="140">its native shape.</text>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g transform="translate(464 653)">
|
|
||||||
<rect class="stage" x="0" y="0" width="420" height="180" rx="4"/>
|
|
||||||
<circle cx="52" cy="90" r="26" fill="#55dced"/>
|
|
||||||
<text class="index" x="52" y="90">2</text>
|
|
||||||
<text class="stage-title" x="98" y="72">Context builder</text>
|
|
||||||
<text class="stage-body" x="98" y="110">Turn source evidence into</text>
|
|
||||||
<text class="stage-body" x="98" y="140">proposed context updates.</text>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g transform="translate(464 868)">
|
|
||||||
<rect class="stage" x="0" y="0" width="420" height="180" rx="4"/>
|
|
||||||
<circle cx="52" cy="90" r="26" fill="#55dced"/>
|
|
||||||
<text class="index" x="52" y="90">3</text>
|
|
||||||
<text class="stage-title" x="98" y="72">Reconciliation</text>
|
|
||||||
<text class="stage-body" x="98" y="110">Merge new evidence with the</text>
|
|
||||||
<text class="stage-body" x="98" y="140">context that already exists.</text>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g transform="translate(464 1082)">
|
|
||||||
<rect class="stage" x="0" y="0" width="420" height="180" rx="4"/>
|
|
||||||
<circle cx="52" cy="90" r="26" fill="#55dced"/>
|
|
||||||
<text class="index" x="52" y="90">4</text>
|
|
||||||
<text class="stage-title" x="98" y="72">Validation</text>
|
|
||||||
<text class="stage-body" x="98" y="110">Check references and semantics</text>
|
|
||||||
<text class="stage-body" x="98" y="140">before agents rely on them.</text>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g id="outputs">
|
|
||||||
<g transform="translate(60 1373)">
|
|
||||||
<rect class="card" x="0" y="0" width="485" height="329" rx="4"/>
|
|
||||||
<rect x="0" y="0" width="485" height="4" rx="2" fill="#10b981"/>
|
|
||||||
<text class="mono" x="24" y="52" fill="#10b981">wiki/*.md</text>
|
|
||||||
<text class="title" x="24" y="100">Wiki</text>
|
|
||||||
<g transform="translate(24 122)">
|
|
||||||
<rect x="0" y="0" width="90" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="12" y="24">free-form</text>
|
|
||||||
<rect x="98" y="0" width="140" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="110" y="24">auto-maintained</text>
|
|
||||||
</g>
|
|
||||||
<text class="body" x="24" y="194">Definitions, caveats, policies, analyst notes, and</text>
|
|
||||||
<text class="body" x="24" y="222">business language that agents can search.</text>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g transform="translate(803 1373)">
|
|
||||||
<rect class="card" x="0" y="0" width="485" height="329" rx="4"/>
|
|
||||||
<rect x="0" y="0" width="485" height="4" rx="2" fill="#3b82f6"/>
|
|
||||||
<text class="mono" x="24" y="52" fill="#3b82f6">semantic-layer/*.yaml</text>
|
|
||||||
<text class="title" x="24" y="100">Semantic layer</text>
|
|
||||||
<g transform="translate(24 122)">
|
|
||||||
<rect x="0" y="0" width="96" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="12" y="24">structured</text>
|
|
||||||
<rect x="104" y="0" width="104" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="116" y="24">executable</text>
|
|
||||||
<rect x="216" y="0" width="140" height="36" rx="4" fill="#fbfaf8" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="228" y="24">auto-maintained</text>
|
|
||||||
</g>
|
|
||||||
<text class="body" x="24" y="194">Metrics, joins, tables, dimensions, filters, and</text>
|
|
||||||
<text class="body" x="24" y="222">segments that ktx can validate and compile into</text>
|
|
||||||
<text class="body" x="24" y="250">SQL.</text>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g transform="translate(622 1505)">
|
|
||||||
<rect x="0" y="0" width="102" height="36" rx="4" fill="#ffffff" stroke="#e5e1dc"/>
|
|
||||||
<text class="tag" x="13" y="24">references</text>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 346 KiB |
BIN
docs-site/public/images/mcp-runtime-flow.png
Normal file
BIN
docs-site/public/images/mcp-runtime-flow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
|
|
@ -85,6 +85,9 @@ importers:
|
||||||
fumadocs-ui:
|
fumadocs-ui:
|
||||||
specifier: 16.8.10
|
specifier: 16.8.10
|
||||||
version: 16.8.10(@tailwindcss/oxide@4.3.0)(@types/mdx@2.0.13)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(fumadocs-core@16.8.10(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.15)(lucide-react@1.16.0(react@19.2.6))(next@16.2.6(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(tailwindcss@4.3.0)
|
version: 16.8.10(@tailwindcss/oxide@4.3.0)(@types/mdx@2.0.13)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(fumadocs-core@16.8.10(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.15)(lucide-react@1.16.0(react@19.2.6))(next@16.2.6(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(tailwindcss@4.3.0)
|
||||||
|
html-to-image:
|
||||||
|
specifier: 1.11.11
|
||||||
|
version: 1.11.11
|
||||||
next:
|
next:
|
||||||
specifier: ^16
|
specifier: ^16
|
||||||
version: 16.2.6(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
version: 16.2.6(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
|
|
@ -3722,6 +3725,9 @@ packages:
|
||||||
html-escaper@2.0.2:
|
html-escaper@2.0.2:
|
||||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||||
|
|
||||||
|
html-to-image@1.11.11:
|
||||||
|
resolution: {integrity: sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==}
|
||||||
|
|
||||||
html-void-elements@3.0.0:
|
html-void-elements@3.0.0:
|
||||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||||
|
|
||||||
|
|
@ -9623,6 +9629,8 @@ snapshots:
|
||||||
|
|
||||||
html-escaper@2.0.2: {}
|
html-escaper@2.0.2: {}
|
||||||
|
|
||||||
|
html-to-image@1.11.11: {}
|
||||||
|
|
||||||
html-void-elements@3.0.0: {}
|
html-void-elements@3.0.0: {}
|
||||||
|
|
||||||
http-errors@2.0.1:
|
http-errors@2.0.1:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue