ktx/docs-site/components/semantic-layer-flow.tsx
Andrey Avtomonov 620d6adbe6
docs: rewrite Semantic Querying concept with imperative-vs-declarative diagram (#156)
* docs: rewrite Semantic Querying concept with imperative-vs-declarative diagram

Reframe semantic-layer-internals.mdx around the contract the semantic
layer offers an agent: declare what you want (a Semantic Query), KTX
figures out how to compute it. Replaces the old "Context-Aware SQL"
framing with a clear imperative-vs-declarative narrative.

Adds a React Flow component (semantic-layer-flow.tsx) that contrasts a
buggy 4-table agent-authored SQL (chasm trap, LEFT-JOIN-in-WHERE,
hardcoded DATE_TRUNC) against the chasm-safe per-fact CTE SQL the
planner actually emits, including the outer GROUP BY over the requested
dimensions. Both lanes converge into a shared warehouse node and each
SQL card now has parallel bullet notes (failures on the left, KTX
behavior on the right).

Side fixes bundled in:
- include the /ktx basePath in the favicon metadata so the icon resolves
  under the production prefix
- migrate docs-site/middleware.ts to docs-site/proxy.ts (Next 16 rename)
- redirect / to /ktx/docs/getting-started/introduction so the apex docs
  URL works
- add tests covering the apex redirect, the favicon basePath, and the
  middleware-to-proxy rename
- propagate the Semantic Query terminology across the ktx-sl CLI
  reference, the context-layer concept page, and the agent-clients /
  primary-sources integration pages

* Fix CI dead-code failures

* docs-site: polish semantic-layer-internals code blocks and flow diagram

- Make CodeBlock a server component so children traverse synchronously
  under React 19 RSC streaming; previously extractText returned "" in
  dev SSR, leaving code blocks empty.
- Add custom JSON/YAML/SQL/code-like tokenizers with theme-aware token
  classes; drop the colored file-glyph dot and gradient tab-head.
- Tighten tab-head: subtle grey background, smaller monospace filename
  in muted grey, smaller rectangular language pill placed to the left
  of the filename.
- Polish the React Flow semantic-layer diagram (controls, fit-view
  padding, edge types).

* docs-site: annotate imperative SQL, add section anchor, drop ClickHouse

- Wire numbered red badges to each problematic span in the "Without KTX"
  SQL with hover sync between SQL gutter, lines, and the notes list.
- Add #imperative-vs-declarative anchor on the flow section header so
  the eyebrow link is shareable; reveals a # glyph on hover/focus.
- Align the compiled-SQL note dots to the first-line midpoint
  (mt-[6px] instead of mt-1) so 4px dots sit at y=8 in a 16px line.
- Remove all ClickHouse references from docs-site (primary-sources,
  quickstart, ktx-setup, contributing, agents-setup, mechanics test,
  warehouse drivers in the flow diagram).

* test: drop ClickHouse contributing-docs assertion

Align the workspace-package mirror test with the ClickHouse removal
from docs-site (75907eb). The connector-clickhouse package still
exists in packages/, but contributing.mdx no longer lists it, so the
test that mirrored docs against the workspace was failing.
2026-05-19 23:41:29 +02:00

1258 lines
36 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useCallback, useState } from "react";
import {
Background,
BackgroundVariant,
Controls,
Handle,
MarkerType,
type Node,
type NodeProps,
type OnInit,
Position,
ReactFlow,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
type LaneVariant = "manual" | "ktx";
type AgentNodeData = {
variant: "single";
title: string;
subtitle: string;
};
type IssueNote = {
id: number;
label: string;
};
type ManualSqlNodeData = {
variant: "manual";
badge: string;
title: string;
caption: string;
code: string;
notes: IssueNote[];
lineIssues: Record<number, number[]>;
};
type SlQueryNodeData = {
variant: "slQuery";
badge: string;
title: string;
caption: string;
code: string;
};
type EngineNodeData = {
variant: "engine";
badge: string;
title: string;
stages: Array<{ index: number; title: string; detail: string }>;
};
type CompiledSqlNodeData = {
variant: "compiled";
badge: string;
title: string;
caption: string;
code: string;
notes: string[];
};
type WarehouseNodeData = {
variant: "warehouse";
title: string;
drivers: string[];
};
type AgentNode = Node<AgentNodeData, "agent">;
type ManualSqlNode = Node<ManualSqlNodeData, "manualSql">;
type SlQueryNode = Node<SlQueryNodeData, "slQuery">;
type EngineNode = Node<EngineNodeData, "engine">;
type CompiledSqlNode = Node<CompiledSqlNodeData, "compiledSql">;
type WarehouseNode = Node<WarehouseNodeData, "warehouse">;
type FlowNode =
| AgentNode
| ManualSqlNode
| SlQueryNode
| EngineNode
| CompiledSqlNode
| WarehouseNode;
const CANVAS_W = 1120;
const AGENT_W = 380;
const AGENT_H = 104;
const AGENT_X = (CANVAS_W - AGENT_W) / 2;
const AGENT_Y = 16;
const LANE_W = 488;
const LEFT_LANE_X = 32;
const RIGHT_LANE_X = CANVAS_W - LEFT_LANE_X - LANE_W;
const LANE_TOP_Y = 248;
const SL_QUERY_H = 510;
const ENGINE_H = 380;
const COMPILED_H = 1380;
const RIGHT_GAP = 24;
const RIGHT_LANE_TOTAL = SL_QUERY_H + RIGHT_GAP + ENGINE_H + RIGHT_GAP + COMPILED_H;
const MANUAL_SQL_H = 840;
const LANES_BOTTOM_Y =
LANE_TOP_Y + Math.max(MANUAL_SQL_H, RIGHT_LANE_TOTAL);
const SL_QUERY_Y = LANE_TOP_Y;
const ENGINE_Y = SL_QUERY_Y + SL_QUERY_H + RIGHT_GAP;
const COMPILED_Y = ENGINE_Y + ENGINE_H + RIGHT_GAP;
const WAREHOUSE_W = 304;
const WAREHOUSE_H = 92;
const WAREHOUSE_X = (CANVAS_W - WAREHOUSE_W) / 2;
const WAREHOUSE_Y = LANES_BOTTOM_Y + 56;
const MANUAL_STROKE = "#94a3b8";
const KTX_STROKE = "#0891b2";
const FIT_VIEW_OPTIONS = { padding: 0.05 };
const agent: AgentNode = {
id: "agent",
type: "agent",
position: { x: AGENT_X, y: AGENT_Y },
data: {
variant: "single",
title: "Analytics agent",
subtitle:
"Asks: monthly net revenue and open tickets per segment, high-value orders only, no test customers",
},
draggable: false,
selectable: false,
};
const manualSql: ManualSqlNode = {
id: "manual-sql",
type: "manualSql",
position: { x: LEFT_LANE_X, y: LANE_TOP_Y },
data: {
variant: "manual",
badge: "Without KTX",
title: "Agent writes the SQL",
caption:
"Stitches four tables, mixes grains, and ships numbers that won't match the dashboard.",
code: `-- agent stitches four tables, mixes facts,
-- and ships numbers that won't match the dashboard
SELECT
c.segment,
DATE_TRUNC('month', o.created_at) AS month,
SUM(o.amount) - SUM(r.amount) AS net_revenue,
COUNT(t.id) AS open_tickets
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.id
LEFT JOIN refunds r
ON r.order_id = o.id
LEFT JOIN tickets t
ON t.customer_id = c.id
WHERE
c.is_test = false
AND o.amount >= 100
AND t.status = 'open' -- turns LEFT JOIN into INNER
GROUP BY
c.segment,
DATE_TRUNC('month', o.created_at)
ORDER BY
month,
c.segment
LIMIT 1000;
-- chasm trap: orders rows multiply by tickets and refunds
-- net_revenue and open_tickets are both inflated
-- DATE_TRUNC syntax breaks on BigQuery`,
notes: [
{ id: 1, label: "Re-stitches a 4-way join on every question" },
{ id: 2, label: "Reinvents net_revenue and the high-value rule" },
{ id: 3, label: "Hides a chasm trap across three facts" },
{ id: 4, label: "Filters a LEFT JOIN target in WHERE" },
{ id: 5, label: "Hardcodes one warehouse's date functions" },
],
lineIssues: {
5: [5],
6: [2, 3],
7: [3],
8: [1],
9: [1],
10: [1],
11: [1],
12: [1],
13: [1],
14: [1],
17: [2],
18: [4],
21: [5],
27: [3],
28: [3],
29: [5],
},
},
draggable: false,
selectable: false,
};
const slQuery: SlQueryNode = {
id: "sl-query",
type: "slQuery",
position: { x: RIGHT_LANE_X, y: SL_QUERY_Y },
data: {
variant: "slQuery",
badge: "With KTX",
title: "Agent sends a Semantic Query",
caption:
"Names the measures, dimensions, segments, and filters it wants. No SQL, no joins.",
code: `{
"measures": [
"orders.revenue",
"refunds.amount",
"tickets.open_count",
{
"name": "net_revenue",
"expr": "orders.revenue - refunds.amount"
}
],
"dimensions": [
"customers.segment",
{ "field": "orders.created_at", "granularity": "month" }
],
"segments": ["orders.high_value"],
"filters": ["customers.is_test = false"],
"limit": 1000
}`,
},
draggable: false,
selectable: false,
};
const engine: EngineNode = {
id: "engine",
type: "engine",
position: { x: RIGHT_LANE_X, y: ENGINE_Y },
data: {
variant: "engine",
badge: "Semantic-layer engine",
title: "Plans the query against the reviewed graph",
stages: [
{
index: 1,
title: "Resolve refs",
detail: "qualify columns, look up measure formulas",
},
{
index: 2,
title: "Build join tree",
detail: "Dijkstra over typed edges from an anchor source",
},
{
index: 3,
title: "Detect fan-out",
detail: "group measures by source, flag chasm traps",
},
{
index: 4,
title: "Localize aggregation",
detail: "pre-aggregate each fact as its own CTE",
},
{
index: 5,
title: "Transpile dialect",
detail: "emit Postgres-shaped SQL, then target dialect",
},
],
},
draggable: false,
selectable: false,
};
const compiledSql: CompiledSqlNode = {
id: "compiled-sql",
type: "compiledSql",
position: { x: RIGHT_LANE_X, y: COMPILED_Y },
data: {
variant: "compiled",
badge: "Generated SQL",
title: "KTX returns dialect-correct SQL",
caption:
"Pre-aggregates each fact at its own grain, then joins back on the shared dimension.",
code: `WITH orders_agg AS (
SELECT
customer_id,
DATE_TRUNC('month', created_at) AS month,
SUM(amount) AS revenue
FROM public.orders
WHERE amount >= 100
GROUP BY
customer_id,
DATE_TRUNC('month', created_at)
),
refunds_agg AS (
SELECT
o.customer_id,
DATE_TRUNC('month', o.created_at) AS month,
SUM(r.amount) AS refund_amount
FROM public.refunds r
JOIN public.orders o
ON o.id = r.order_id
WHERE o.amount >= 100
GROUP BY
o.customer_id,
DATE_TRUNC('month', o.created_at)
),
tickets_agg AS (
SELECT
customer_id,
DATE_TRUNC('month', opened_at) AS month,
COUNT(*) AS open_count
FROM public.tickets
WHERE status = 'open'
GROUP BY
customer_id,
DATE_TRUNC('month', opened_at)
)
SELECT
c.segment,
o.month,
SUM(o.revenue - COALESCE(r.refund_amount, 0)) AS net_revenue,
SUM(o.revenue) AS revenue,
SUM(r.refund_amount) AS refund_amount,
SUM(COALESCE(t.open_count, 0)) AS open_tickets
FROM public.customers c
JOIN orders_agg o
ON o.customer_id = c.id
LEFT JOIN refunds_agg r
ON r.customer_id = c.id
AND r.month = o.month
LEFT JOIN tickets_agg t
ON t.customer_id = c.id
AND t.month = o.month
WHERE c.is_test = false
GROUP BY
c.segment,
o.month
ORDER BY
o.month,
c.segment
LIMIT 1000;`,
notes: [
"Walks the reviewed join graph automatically",
"Uses the canonical net_revenue formula",
"Pre-aggregates each fact to avoid the chasm trap",
"Keeps LEFT JOIN filters on the dimension source",
"Transpiles DATE_TRUNC to the target dialect",
],
},
draggable: false,
selectable: false,
};
const warehouse: WarehouseNode = {
id: "warehouse",
type: "warehouse",
position: { x: WAREHOUSE_X, y: WAREHOUSE_Y },
data: {
variant: "warehouse",
title: "Warehouse",
drivers: ["PostgreSQL", "Snowflake", "BigQuery"],
},
draggable: false,
selectable: false,
};
const nodes: FlowNode[] = [
agent,
manualSql,
slQuery,
engine,
compiledSql,
warehouse,
];
const arrowMarker = (color: string) => ({
type: MarkerType.ArrowClosed,
color,
width: 16,
height: 16,
});
const edges = [
{
id: "agent-manual",
source: "agent",
target: "manual-sql",
type: "default" as const,
label: "writes raw SQL",
labelBgPadding: [6, 3] as [number, number],
labelBgBorderRadius: 4,
labelStyle: {
fontSize: 12,
fontWeight: 500,
fill: "var(--color-fd-muted-foreground)",
},
labelBgStyle: {
fill: "var(--color-fd-background)",
stroke: "var(--color-fd-border)",
strokeWidth: 1,
},
style: {
stroke: MANUAL_STROKE,
strokeWidth: 1.5,
strokeDasharray: "5 4",
},
markerEnd: arrowMarker(MANUAL_STROKE),
},
{
id: "manual-warehouse",
source: "manual-sql",
target: "warehouse",
targetHandle: "warehouse-manual",
type: "default" as const,
style: {
stroke: MANUAL_STROKE,
strokeWidth: 1.5,
strokeDasharray: "5 4",
},
markerEnd: arrowMarker(MANUAL_STROKE),
},
{
id: "agent-slquery",
source: "agent",
target: "sl-query",
type: "default" as const,
label: "sends Semantic Query",
labelBgPadding: [6, 3] as [number, number],
labelBgBorderRadius: 4,
labelStyle: {
fontSize: 12,
fontWeight: 600,
fill: KTX_STROKE,
},
labelBgStyle: {
fill: "var(--color-fd-background)",
stroke: "var(--color-fd-border)",
strokeWidth: 1,
},
style: { stroke: KTX_STROKE, strokeWidth: 1.75 },
markerEnd: arrowMarker(KTX_STROKE),
},
{
id: "slquery-engine",
source: "sl-query",
target: "engine",
type: "straight" as const,
style: { stroke: KTX_STROKE, strokeWidth: 1.75 },
markerEnd: arrowMarker(KTX_STROKE),
},
{
id: "engine-compiled",
source: "engine",
target: "compiled-sql",
type: "straight" as const,
style: { stroke: KTX_STROKE, strokeWidth: 1.75 },
markerEnd: arrowMarker(KTX_STROKE),
},
{
id: "compiled-warehouse",
source: "compiled-sql",
target: "warehouse",
targetHandle: "warehouse-compiled",
type: "straight" as const,
style: { stroke: KTX_STROKE, strokeWidth: 1.75 },
markerEnd: arrowMarker(KTX_STROKE),
},
];
type FlowEdge = (typeof edges)[number];
function AgentNodeView({ data }: NodeProps<AgentNode>) {
return (
<div
style={{ width: AGENT_W, height: AGENT_H }}
className="flex items-center gap-3 rounded-md border border-fd-border bg-fd-card px-4 py-3 shadow-sm"
>
<Handle type="source" position={Position.Bottom} className="!opacity-0" />
<div className="flex h-10 w-10 flex-none items-center justify-center rounded-full bg-fd-primary/15 text-fd-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<rect x="3" y="6" width="18" height="12" rx="3" />
<circle cx="9" cy="12" r="1.25" fill="currentColor" stroke="none" />
<circle cx="15" cy="12" r="1.25" fill="currentColor" stroke="none" />
<path d="M12 3v3" />
</svg>
</div>
<div className="min-w-0">
<p className="text-[15px] font-semibold leading-5 text-fd-foreground">
{data.title}
</p>
<p className="mt-0.5 text-[12px] leading-4 text-fd-muted-foreground">
{data.subtitle}
</p>
</div>
</div>
);
}
function LaneBadge({
variant,
children,
}: {
variant: LaneVariant;
children: React.ReactNode;
}) {
const cls =
variant === "manual"
? "border-slate-300 bg-slate-100 text-slate-700 dark:border-slate-600/60 dark:bg-slate-700/40 dark:text-slate-200"
: "border-cyan-300/70 bg-cyan-50 text-cyan-800 dark:border-cyan-400/40 dark:bg-cyan-400/15 dark:text-cyan-100";
return (
<span
className={`inline-flex items-center gap-1.5 rounded-sm border px-2 py-0.5 text-[10.5px] font-semibold uppercase tracking-[0.08em] ${cls}`}
>
<span
className="h-1.5 w-1.5 rounded-full"
style={{
background: variant === "manual" ? MANUAL_STROKE : KTX_STROKE,
}}
/>
{children}
</span>
);
}
const JSON_TOKEN_PATTERN =
/"(?:\\.|[^"\\])*"|-?\b\d+(?:\.\d+)?\b|\b(?:true|false|null)\b|[{}[\],:]/g;
const SQL_TOKEN_PATTERN =
/--[^\n]*|'(?:''|[^'])*'|\b\d+(?:\.\d+)?\b|\b(?:select|from|join|left|right|inner|outer|on|where|group|by|order|limit|as|sum|count|coalesce|date_trunc|case|when|then|else|end|and|or|is|not|null|false|true|with|having|over|partition)\b|[(),.;=*<>+-]/gi;
const SQL_FUNCTIONS = new Set(["sum", "count", "coalesce", "date_trunc"]);
function highlightJson(code: string) {
const parts = [];
let lastIndex = 0;
let tokenIndex = 0;
for (const match of code.matchAll(JSON_TOKEN_PATTERN)) {
const token = match[0];
const index = match.index ?? 0;
if (index > lastIndex) parts.push(code.slice(lastIndex, index));
const nextText = code.slice(index + token.length);
const className = token.startsWith('"')
? /^\s*:/.test(nextText)
? "syntax-json-key"
: "syntax-string"
: /^-?\d/.test(token)
? "syntax-number"
: /^(true|false|null)$/.test(token)
? "syntax-constant"
: "syntax-punctuation";
parts.push(
<span key={`json-${tokenIndex}`} className={className}>
{token}
</span>,
);
lastIndex = index + token.length;
tokenIndex += 1;
}
if (lastIndex < code.length) parts.push(code.slice(lastIndex));
return parts;
}
function highlightSql(code: string) {
const parts = [];
let lastIndex = 0;
let tokenIndex = 0;
for (const match of code.matchAll(SQL_TOKEN_PATTERN)) {
const token = match[0];
const index = match.index ?? 0;
if (index > lastIndex) parts.push(code.slice(lastIndex, index));
const lowerToken = token.toLowerCase();
const className = token.startsWith("--")
? "syntax-comment"
: token.startsWith("'")
? "syntax-string"
: /^\d/.test(token)
? "syntax-number"
: SQL_FUNCTIONS.has(lowerToken)
? "syntax-function"
: /^[a-z_]+$/i.test(token)
? "syntax-keyword"
: "syntax-punctuation";
parts.push(
<span key={`sql-${tokenIndex}`} className={className}>
{token}
</span>,
);
lastIndex = index + token.length;
tokenIndex += 1;
}
if (lastIndex < code.length) parts.push(code.slice(lastIndex));
return parts;
}
function highlightCode(language: string, code: string) {
if (language === "json") return highlightJson(code);
if (language === "sql") return highlightSql(code);
return code;
}
function CodeBlock({
language,
code,
tone,
}: {
language: string;
code: string;
tone: "manual" | "slQuery" | "compiled";
}) {
const toneClass =
tone === "manual"
? "text-slate-600 dark:text-slate-300"
: tone === "slQuery"
? "text-fd-primary"
: "text-fd-primary/90";
const highlightedCode = highlightCode(language, code);
return (
<div className="flex h-full flex-col overflow-hidden rounded-md border border-fd-border bg-[#fbfaf6] dark:bg-[#0c1417]">
<div className="flex flex-none items-center justify-between border-b border-fd-border bg-fd-muted/40 px-3 py-1.5">
<span
className={`font-mono font-medium tracking-wide ${toneClass}`}
style={{ fontSize: "11px", lineHeight: "16px" }}
>
{language}
</span>
<span
className="font-mono uppercase tracking-[0.08em] text-fd-muted-foreground"
style={{ fontSize: "10.5px", lineHeight: "16px" }}
>
{tone === "compiled" ? "ktx-compiled" : "agent-authored"}
</span>
</div>
<pre
className="m-0 flex-1 overflow-auto px-3 py-2 font-mono text-fd-foreground"
style={{ fontSize: "11.5px", lineHeight: "17.5px" }}
>
{highlightedCode}
</pre>
</div>
);
}
function AnnotatedSqlBlock({
code,
lineIssues,
activeIssue,
onIssueEnter,
onIssueLeave,
}: {
code: string;
lineIssues: Record<number, number[]>;
activeIssue: number | null;
onIssueEnter: (n: number) => void;
onIssueLeave: () => void;
}) {
const lines = code.split("\n");
return (
<div className="flex h-full flex-col overflow-hidden rounded-md border border-fd-border bg-[#fbfaf6] dark:bg-[#0c1417]">
<div className="flex flex-none items-center justify-between border-b border-fd-border bg-fd-muted/40 px-3 py-1.5">
<span
className="font-mono font-medium tracking-wide text-slate-600 dark:text-slate-300"
style={{ fontSize: "11px", lineHeight: "16px" }}
>
sql
</span>
<span
className="font-mono uppercase tracking-[0.08em] text-fd-muted-foreground"
style={{ fontSize: "10.5px", lineHeight: "16px" }}
>
agent-authored
</span>
</div>
<pre
className="m-0 flex-1 overflow-auto px-2 py-2 font-mono text-fd-foreground"
style={{ fontSize: "11.5px", lineHeight: "17.5px" }}
>
{lines.map((line, idx) => {
const issues = lineIssues[idx] ?? [];
const hasIssue = issues.length > 0;
const dim =
activeIssue !== null && !issues.includes(activeIssue);
const active =
activeIssue !== null && issues.includes(activeIssue);
const classes = [
"sl-sql-line",
hasIssue ? "is-issue" : "",
active ? "is-active" : "",
dim ? "is-dim" : "",
]
.filter(Boolean)
.join(" ");
return (
<div key={idx} className={classes}>
<span className="sl-sql-gutter">
{issues.map((n) => (
<button
key={n}
type="button"
className={`sl-issue-pill sl-issue-pill-sql ${
activeIssue === n ? "is-active" : ""
}`}
onMouseEnter={() => onIssueEnter(n)}
onMouseLeave={onIssueLeave}
onFocus={() => onIssueEnter(n)}
onBlur={onIssueLeave}
aria-label={`Issue ${n}`}
>
{n}
</button>
))}
</span>
<span className="sl-sql-content">
{line.length ? highlightSql(line) : " "}
</span>
</div>
);
})}
</pre>
</div>
);
}
function ManualSqlNodeView({ data }: NodeProps<ManualSqlNode>) {
const [activeIssue, setActiveIssue] = useState<number | null>(null);
const clearActive = useCallback(() => setActiveIssue(null), []);
return (
<div
style={{ width: LANE_W, height: MANUAL_SQL_H }}
className="flex flex-col rounded-lg border border-fd-border bg-fd-card p-3.5 shadow-sm"
>
<Handle type="target" position={Position.Top} className="!opacity-0" />
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<LaneBadge variant="manual">{data.badge}</LaneBadge>
<p className="mt-2 text-[15px] font-semibold leading-5 text-fd-foreground">
{data.title}
</p>
<p className="mt-1 text-[12px] leading-5 text-fd-muted-foreground">
{data.caption}
</p>
</div>
</div>
<div className="mt-3 min-h-0 flex-1">
<AnnotatedSqlBlock
code={data.code}
lineIssues={data.lineIssues}
activeIssue={activeIssue}
onIssueEnter={setActiveIssue}
onIssueLeave={clearActive}
/>
</div>
<ul className="mt-3 grid gap-1.5 sm:grid-cols-2">
{data.notes.map((note) => {
const dim = activeIssue !== null && activeIssue !== note.id;
const active = activeIssue === note.id;
return (
<li
key={note.id}
className={`flex items-start gap-2 text-[11.5px] leading-4 transition-opacity duration-150 ${
dim ? "opacity-35" : ""
} ${active ? "text-fd-foreground" : "text-fd-muted-foreground"}`}
onMouseEnter={() => setActiveIssue(note.id)}
onMouseLeave={clearActive}
onFocus={() => setActiveIssue(note.id)}
onBlur={clearActive}
tabIndex={0}
>
<span
className={`sl-issue-pill sl-issue-pill-note ${
active ? "is-active" : ""
}`}
aria-hidden="true"
>
{note.id}
</span>
<span>{note.label}</span>
</li>
);
})}
</ul>
<Handle type="source" position={Position.Bottom} className="!opacity-0" />
</div>
);
}
function SlQueryNodeView({ data }: NodeProps<SlQueryNode>) {
return (
<div
style={{ width: LANE_W, height: SL_QUERY_H }}
className="flex flex-col rounded-lg border border-fd-primary/40 bg-fd-card p-3.5 shadow-sm"
>
<Handle type="target" position={Position.Top} className="!opacity-0" />
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<LaneBadge variant="ktx">{data.badge}</LaneBadge>
<p className="mt-2 text-[15px] font-semibold leading-5 text-fd-foreground">
{data.title}
</p>
<p className="mt-0.5 text-[12px] leading-4 text-fd-muted-foreground">
{data.caption}
</p>
</div>
</div>
<div className="mt-2 min-h-0 flex-1 overflow-hidden">
<CodeBlock language="json" code={data.code} tone="slQuery" />
</div>
<Handle type="source" position={Position.Bottom} className="!opacity-0" />
</div>
);
}
function EngineNodeView({ data }: NodeProps<EngineNode>) {
return (
<div
style={{ width: LANE_W, height: ENGINE_H }}
className="relative flex flex-col rounded-lg border border-cyan-200/30 bg-[#0f1f23] p-3.5 text-white shadow-sm dark:bg-[#0b181b]"
>
<span
className="absolute inset-y-0 left-0 w-[3px] rounded-l-lg"
style={{ background: KTX_STROKE }}
aria-hidden="true"
/>
<Handle type="target" position={Position.Top} className="!opacity-0" />
<div className="flex items-center justify-between">
<p className="text-[10.5px] font-semibold uppercase tracking-[0.08em] text-cyan-300">
{data.badge}
</p>
</div>
<p className="mt-1.5 text-[15px] font-semibold leading-5 text-white">
{data.title}
</p>
<ol className="mt-3 flex flex-1 flex-col gap-1.5">
{data.stages.map((stage) => (
<li
key={stage.index}
className="flex items-start gap-3 rounded-md border border-cyan-100/15 bg-white/[0.04] px-3 py-2"
>
<span className="mt-0.5 flex h-6 w-6 flex-none items-center justify-center rounded-full bg-cyan-300/95 font-mono text-[11px] font-semibold text-[#0b1c20]">
{stage.index}
</span>
<div className="min-w-0">
<p className="text-[13px] font-semibold leading-[18px] text-white">
{stage.title}
</p>
<p className="mt-0.5 text-[11.5px] leading-[16px] text-cyan-50/80">
{stage.detail}
</p>
</div>
</li>
))}
</ol>
<Handle type="source" position={Position.Bottom} className="!opacity-0" />
</div>
);
}
function CompiledSqlNodeView({ data }: NodeProps<CompiledSqlNode>) {
return (
<div
style={{ width: LANE_W, height: COMPILED_H }}
className="flex flex-col rounded-lg border border-fd-primary/40 bg-fd-card p-3.5 shadow-sm"
>
<Handle type="target" position={Position.Top} className="!opacity-0" />
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<LaneBadge variant="ktx">{data.badge}</LaneBadge>
<p className="mt-2 text-[15px] font-semibold leading-5 text-fd-foreground">
{data.title}
</p>
<p className="mt-1 text-[12px] leading-5 text-fd-muted-foreground">
{data.caption}
</p>
</div>
</div>
<div className="mt-3 min-h-0 flex-1">
<CodeBlock language="sql" code={data.code} tone="compiled" />
</div>
<ul className="mt-3 grid gap-1.5 sm:grid-cols-2">
{data.notes.map((note) => (
<li
key={note}
className="flex items-start gap-1.5 text-[11.5px] leading-4 text-fd-muted-foreground"
>
<span
className="mt-[6px] h-1 w-1 flex-none rounded-full"
style={{ background: KTX_STROKE }}
aria-hidden="true"
/>
<span>{note}</span>
</li>
))}
</ul>
<Handle type="source" position={Position.Bottom} className="!opacity-0" />
</div>
);
}
function WarehouseNodeView({ data }: NodeProps<WarehouseNode>) {
return (
<div
style={{ width: WAREHOUSE_W, height: WAREHOUSE_H }}
className="flex items-center gap-3 rounded-md border border-fd-border bg-fd-card px-4 py-3 shadow-sm"
>
<Handle
id="warehouse-manual"
type="target"
position={Position.Top}
className="!opacity-0"
style={{ left: "42%" }}
/>
<Handle
id="warehouse-compiled"
type="target"
position={Position.Top}
className="!opacity-0"
style={{ left: "58%" }}
/>
<div className="flex h-10 w-10 flex-none items-center justify-center rounded-md bg-fd-primary/12 text-fd-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<ellipse cx="12" cy="5.5" rx="8" ry="2.6" />
<path d="M4 5.5v6.2c0 1.43 3.58 2.6 8 2.6s8-1.17 8-2.6V5.5" />
<path d="M4 11.7v6.2c0 1.43 3.58 2.6 8 2.6s8-1.17 8-2.6v-6.2" />
</svg>
</div>
<div className="min-w-0">
<p className="text-[15px] font-semibold leading-5 text-fd-foreground">
{data.title}
</p>
<p className="mt-0.5 text-[11.5px] leading-4 text-fd-muted-foreground">
{data.drivers.join(" • ")}
</p>
</div>
</div>
);
}
const nodeTypes = {
agent: AgentNodeView,
manualSql: ManualSqlNodeView,
slQuery: SlQueryNodeView,
engine: EngineNodeView,
compiledSql: CompiledSqlNodeView,
warehouse: WarehouseNodeView,
};
export function SemanticLayerFlow() {
const [minZoom, setMinZoom] = useState(0.2);
const handleFlowInit = useCallback<OnInit<FlowNode, FlowEdge>>((instance) => {
requestAnimationFrame(() => {
void instance.fitView(FIT_VIEW_OPTIONS).then(() => {
setMinZoom(instance.getZoom());
});
});
}, []);
return (
<section
id="imperative-vs-declarative"
className="not-prose my-10 w-full max-w-full min-w-0 space-y-4 scroll-mt-24"
aria-labelledby="sl-flow-title"
>
<article
className="max-w-full min-w-0 overflow-hidden rounded-lg border border-fd-border bg-fd-card shadow-sm"
aria-label="From Semantic Query to executed SQL: contrast between agent-authored SQL and KTX-compiled SQL"
>
<div className="border-b border-fd-border bg-fd-muted/35 px-5 py-4">
<a
href="#imperative-vs-declarative"
className="group/anchor inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-[0.08em] text-fd-primary transition-colors hover:text-fd-primary/80"
>
Imperative vs declarative
<span
aria-hidden="true"
className="opacity-0 transition-opacity duration-150 group-hover/anchor:opacity-100 group-focus-visible/anchor:opacity-100"
>
#
</span>
</a>
<h3
id="sl-flow-title"
className="mt-1 text-base font-semibold tracking-normal text-fd-foreground sm:text-lg"
style={{ fontFamily: "var(--font-display)" }}
>
Same answer, two contracts
</h3>
<p className="mt-2 max-w-3xl text-xs leading-5 text-fd-muted-foreground">
On the left, the agent works imperatively: chooses tables, writes
joins, picks the grain, and remembers each warehouse's dialect. On
the right, the agent only declares what it wants. KTX handles
every how.
</p>
</div>
<div
className="sl-flow-canvas relative bg-fd-background"
style={{
height: "min(2340px, 290vw)",
minHeight: 1780,
}}
>
<div className="pointer-events-none absolute right-2.5 top-2.5 z-10 rounded border border-fd-border/50 bg-white/30 px-1.5 py-px font-mono text-[9.5px] font-medium uppercase tracking-[0.06em] text-fd-muted-foreground shadow-sm backdrop-blur-sm dark:bg-white/10">
Pan / zoom
</div>
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
onInit={handleFlowInit}
nodesDraggable={false}
nodesConnectable={false}
nodesFocusable={false}
edgesFocusable={false}
elementsSelectable={false}
panOnDrag
panOnScroll={false}
zoomOnScroll
zoomOnPinch
zoomOnDoubleClick
preventScrolling
minZoom={minZoom}
maxZoom={1.5}
proOptions={{ hideAttribution: true }}
>
<Controls position="bottom-right" showInteractive={false} />
<Background
variant={BackgroundVariant.Dots}
gap={18}
size={1}
color="var(--color-fd-border)"
/>
</ReactFlow>
</div>
</article>
<style>{`
.sl-flow-canvas .react-flow__node {
background: transparent;
border: 0;
box-shadow: none;
padding: 0;
border-radius: 0;
width: auto;
text-align: left;
user-select: text;
-webkit-user-select: text;
cursor: auto;
pointer-events: all !important;
}
.sl-flow-canvas .react-flow__node > * {
pointer-events: auto;
user-select: text;
-webkit-user-select: text;
}
.sl-flow-canvas .react-flow__node.selected,
.sl-flow-canvas .react-flow__node:focus,
.sl-flow-canvas .react-flow__node:focus-visible {
outline: none;
box-shadow: none;
}
.sl-flow-canvas .react-flow__pane {
cursor: grab;
}
.sl-flow-canvas .react-flow__pane:active {
cursor: grabbing;
}
.sl-flow-canvas .react-flow__handle {
width: 1px;
height: 1px;
min-width: 0;
min-height: 0;
background: transparent;
border: 0;
pointer-events: none;
}
.sl-flow-canvas pre {
font-size: 11.5px !important;
line-height: 17.5px !important;
background: transparent !important;
padding: 8px 12px !important;
border: 0 !important;
margin: 0 !important;
box-shadow: none !important;
}
.sl-flow-canvas .react-flow__node pre code,
.sl-flow-canvas .react-flow__node pre span {
font-size: inherit !important;
line-height: inherit !important;
}
.sl-flow-canvas .syntax-json-key {
color: #0f766e;
}
.sl-flow-canvas .syntax-keyword {
color: #0e7490;
font-weight: 650;
}
.sl-flow-canvas .syntax-function {
color: #7c3aed;
font-weight: 650;
}
.sl-flow-canvas .syntax-string {
color: #b45309;
}
.sl-flow-canvas .syntax-number,
.sl-flow-canvas .syntax-constant {
color: #be123c;
}
.sl-flow-canvas .syntax-comment {
color: #64748b;
font-style: italic;
}
.sl-flow-canvas .syntax-punctuation {
color: #64748b;
}
.sl-flow-canvas .sl-sql-line {
display: flex;
align-items: center;
min-height: 17.5px;
padding: 0 6px;
border-radius: 4px;
transition: background-color 140ms ease, opacity 140ms ease;
}
.sl-flow-canvas .sl-sql-line.is-issue {
background-color: rgba(239, 68, 68, 0.05);
}
.sl-flow-canvas .sl-sql-line.is-active {
background-color: rgba(239, 68, 68, 0.15);
box-shadow: inset 2px 0 0 0 #ef4444;
}
.sl-flow-canvas .sl-sql-line.is-dim {
opacity: 0.34;
}
.sl-flow-canvas .sl-sql-gutter {
display: inline-flex;
align-items: center;
gap: 3px;
flex: none;
width: 38px;
padding-right: 8px;
}
.sl-flow-canvas .sl-sql-content {
flex: 1;
min-width: 0;
white-space: pre;
}
.sl-flow-canvas .sl-issue-pill {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
background: #ef4444;
color: white;
font-weight: 700;
line-height: 1;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
border: 0;
padding: 0;
cursor: pointer;
transition: transform 140ms ease, box-shadow 140ms ease,
background-color 140ms ease;
}
.sl-flow-canvas .sl-issue-pill:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.35);
}
.sl-flow-canvas .sl-issue-pill.is-active {
background: #dc2626;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.28);
transform: scale(1.08);
}
.sl-flow-canvas .sl-issue-pill-sql {
width: 14px;
height: 14px;
font-size: 9.5px;
}
.sl-flow-canvas .sl-issue-pill-note {
width: 16px;
height: 16px;
font-size: 10.5px;
flex: none;
margin-top: 0;
}
.dark .sl-flow-canvas .sl-sql-line.is-issue {
background-color: rgba(248, 113, 113, 0.08);
}
.dark .sl-flow-canvas .sl-sql-line.is-active {
background-color: rgba(248, 113, 113, 0.2);
box-shadow: inset 2px 0 0 0 #f87171;
}
.dark .sl-flow-canvas .sl-issue-pill {
background: #f87171;
color: #1b0408;
}
.dark .sl-flow-canvas .sl-issue-pill.is-active {
background: #fca5a5;
box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.32);
}
.dark .sl-flow-canvas .syntax-json-key {
color: #5eead4;
}
.dark .sl-flow-canvas .syntax-keyword {
color: #67e8f9;
}
.dark .sl-flow-canvas .syntax-function {
color: #c4b5fd;
}
.dark .sl-flow-canvas .syntax-string {
color: #fbbf24;
}
.dark .sl-flow-canvas .syntax-number,
.dark .sl-flow-canvas .syntax-constant {
color: #fb7185;
}
.dark .sl-flow-canvas .syntax-comment {
color: #94a3b8;
}
.dark .sl-flow-canvas .syntax-punctuation {
color: #94a3b8;
}
`}</style>
</section>
);
}