mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
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).
This commit is contained in:
parent
c3dc488934
commit
8f6a2a686f
3 changed files with 586 additions and 67 deletions
|
|
@ -221,6 +221,72 @@ pre code,
|
|||
padding-inline: 0 !important;
|
||||
}
|
||||
|
||||
.ktx-code .ktx-token-key {
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.ktx-code .ktx-token-keyword {
|
||||
color: #0e7490;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.ktx-code .ktx-token-function {
|
||||
color: #7c3aed;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.ktx-code .ktx-token-flag {
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
.ktx-code .ktx-token-string {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.ktx-code .ktx-token-number,
|
||||
.ktx-code .ktx-token-constant {
|
||||
color: #be123c;
|
||||
}
|
||||
|
||||
.ktx-code .ktx-token-comment {
|
||||
color: #64748b;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.ktx-code .ktx-token-punctuation {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.dark .ktx-code .ktx-token-key {
|
||||
color: #5eead4;
|
||||
}
|
||||
|
||||
.dark .ktx-code .ktx-token-keyword {
|
||||
color: #67e8f9;
|
||||
}
|
||||
|
||||
.dark .ktx-code .ktx-token-function {
|
||||
color: #c4b5fd;
|
||||
}
|
||||
|
||||
.dark .ktx-code .ktx-token-flag {
|
||||
color: #7dd3fc;
|
||||
}
|
||||
|
||||
.dark .ktx-code .ktx-token-string {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.dark .ktx-code .ktx-token-number,
|
||||
.dark .ktx-code .ktx-token-constant {
|
||||
color: #fb7185;
|
||||
}
|
||||
|
||||
.dark .ktx-code .ktx-token-comment,
|
||||
.dark .ktx-code .ktx-token-punctuation {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Neutralize the outer figure styling that our wrapper now owns */
|
||||
figure:has(> .ktx-code),
|
||||
figure[data-rehype-pretty-code-figure]:has(.ktx-code) {
|
||||
|
|
@ -327,55 +393,32 @@ figure[data-rehype-pretty-code-figure]:has(.ktx-code) {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px 8px 14px;
|
||||
padding: 5px 8px 5px 12px;
|
||||
border-bottom: 1px solid var(--color-fd-border);
|
||||
background: linear-gradient(180deg, var(--color-fd-muted), transparent);
|
||||
background: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
|
||||
.dark .ktx-code-tab-head {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.05);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.ktx-file-glyph {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-fd-muted-foreground);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ktx-file-glyph[data-lang="yaml"],
|
||||
.ktx-file-glyph[data-lang="yml"] { background: #fbbf24; }
|
||||
.ktx-file-glyph[data-lang="ts"],
|
||||
.ktx-file-glyph[data-lang="tsx"],
|
||||
.ktx-file-glyph[data-lang="typescript"] { background: #3b82f6; }
|
||||
.ktx-file-glyph[data-lang="js"],
|
||||
.ktx-file-glyph[data-lang="jsx"],
|
||||
.ktx-file-glyph[data-lang="javascript"] { background: #facc15; }
|
||||
.ktx-file-glyph[data-lang="json"] { background: #84cc16; }
|
||||
.ktx-file-glyph[data-lang="md"],
|
||||
.ktx-file-glyph[data-lang="mdx"] { background: #a3a3a3; }
|
||||
.ktx-file-glyph[data-lang="sql"] { background: #f97316; }
|
||||
.ktx-file-glyph[data-lang="py"],
|
||||
.ktx-file-glyph[data-lang="python"] { background: #22d3ee; }
|
||||
|
||||
.ktx-code-tab-filename {
|
||||
font-family: var(--font-mono), ui-monospace, monospace;
|
||||
font-size: 12.5px;
|
||||
color: var(--color-fd-foreground);
|
||||
font-size: 11.5px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.ktx-lang-pill {
|
||||
margin-left: 4px;
|
||||
padding: 1px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
padding: 0 7px;
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--color-fd-muted-foreground);
|
||||
letter-spacing: 0.06em;
|
||||
color: #9ca3af;
|
||||
border: 1px solid var(--color-fd-border);
|
||||
border-radius: 4px;
|
||||
border-radius: 3px;
|
||||
background: var(--color-fd-card);
|
||||
font-family: var(--font-display), var(--font-sans), sans-serif;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
type ComponentPropsWithoutRef,
|
||||
type ReactNode,
|
||||
|
|
@ -15,6 +13,55 @@ type Props = ComponentPropsWithoutRef<"pre"> & {
|
|||
|
||||
const OUTPUT_LANGS = new Set(["text", "plain", "plaintext", "console", "output"]);
|
||||
const WIZARD_GLYPHS = /^\s*[◆◇◯◐○●]/;
|
||||
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|avg|min|max|count|coalesce|date_trunc|case|when|then|else|end|and|or|is|not|null|false|true|with|having|over|partition|insert|update|delete|create|alter|drop|table|view)\b|[(),.;=*<>+-]/gi;
|
||||
const CODE_LIKE_TOKEN_PATTERN =
|
||||
/\/\/[^\n]*|\/\*[\s\S]*?\*\/|#(?![{\w-]+:)[^\n]*|`(?:\\.|[^`\\])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|-?\b\d+(?:\.\d+)?\b|\b(?:const|let|var|function|return|import|export|from|type|interface|extends|async|await|if|else|for|while|switch|case|break|continue|try|catch|throw|new|class|public|private|protected|readonly|true|false|null|undefined|pnpm|uv|ktx|node|npx|curl|git)\b|--?[\w-]+|[{}[\](),.;:=*<>|&+-]/g;
|
||||
const SQL_FUNCTIONS = new Set([
|
||||
"sum",
|
||||
"avg",
|
||||
"min",
|
||||
"max",
|
||||
"count",
|
||||
"coalesce",
|
||||
"date_trunc",
|
||||
]);
|
||||
const CODE_KEYWORDS = new Set([
|
||||
"const",
|
||||
"let",
|
||||
"var",
|
||||
"function",
|
||||
"return",
|
||||
"import",
|
||||
"export",
|
||||
"from",
|
||||
"type",
|
||||
"interface",
|
||||
"extends",
|
||||
"async",
|
||||
"await",
|
||||
"if",
|
||||
"else",
|
||||
"for",
|
||||
"while",
|
||||
"switch",
|
||||
"case",
|
||||
"break",
|
||||
"continue",
|
||||
"try",
|
||||
"catch",
|
||||
"throw",
|
||||
"new",
|
||||
"class",
|
||||
"public",
|
||||
"private",
|
||||
"protected",
|
||||
"readonly",
|
||||
]);
|
||||
const COMMAND_KEYWORDS = new Set(["pnpm", "uv", "ktx", "node", "npx", "curl", "git"]);
|
||||
const CODE_CONSTANTS = new Set(["true", "false", "null", "undefined"]);
|
||||
|
||||
function extractText(node: ReactNode): string {
|
||||
if (typeof node === "string") return node;
|
||||
|
|
@ -65,15 +112,277 @@ function detectLanguage(props: Props, children: ReactNode): string | null {
|
|||
return findLanguageInNode(children);
|
||||
}
|
||||
|
||||
function stripOneLeadingBlankLine(text: string) {
|
||||
return text.startsWith("\n") ? text.slice(1) : text;
|
||||
}
|
||||
|
||||
function extractCodeHeader(language: string | null, code: string) {
|
||||
const normalized = normalizeLanguage(language);
|
||||
const firstLineEnd = code.indexOf("\n");
|
||||
const firstLine = firstLineEnd === -1 ? code : code.slice(0, firstLineEnd);
|
||||
const rest = firstLineEnd === -1 ? "" : code.slice(firstLineEnd + 1);
|
||||
const commentPrefix =
|
||||
normalized === "sql"
|
||||
? "--"
|
||||
: normalized === "javascript" ||
|
||||
normalized === "js" ||
|
||||
normalized === "jsx" ||
|
||||
normalized === "typescript" ||
|
||||
normalized === "ts" ||
|
||||
normalized === "tsx"
|
||||
? "//"
|
||||
: "#";
|
||||
|
||||
if (!firstLine.trimStart().startsWith(commentPrefix)) {
|
||||
return { header: null, code };
|
||||
}
|
||||
|
||||
const candidate = firstLine
|
||||
.trim()
|
||||
.slice(commentPrefix.length)
|
||||
.trim();
|
||||
const looksLikePath =
|
||||
candidate.includes("/") &&
|
||||
/\.[A-Za-z0-9]+(?:["'`)]*)?$/.test(candidate);
|
||||
|
||||
if (!looksLikePath) return { header: null, code };
|
||||
|
||||
return {
|
||||
header: candidate,
|
||||
code: stripOneLeadingBlankLine(rest),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLanguage(language: string | null) {
|
||||
return language?.toLowerCase() ?? "";
|
||||
}
|
||||
|
||||
function pushMatchedToken(
|
||||
parts: ReactNode[],
|
||||
token: string,
|
||||
className: string,
|
||||
key: string,
|
||||
) {
|
||||
parts.push(
|
||||
<span key={key} className={className}>
|
||||
{token}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
function highlightJson(code: string) {
|
||||
const parts: ReactNode[] = [];
|
||||
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)
|
||||
? "ktx-token-key"
|
||||
: "ktx-token-string"
|
||||
: /^-?\d/.test(token)
|
||||
? "ktx-token-number"
|
||||
: /^(true|false|null)$/.test(token)
|
||||
? "ktx-token-constant"
|
||||
: "ktx-token-punctuation";
|
||||
|
||||
pushMatchedToken(parts, token, className, `json-${tokenIndex}`);
|
||||
lastIndex = index + token.length;
|
||||
tokenIndex += 1;
|
||||
}
|
||||
|
||||
if (lastIndex < code.length) parts.push(code.slice(lastIndex));
|
||||
return parts;
|
||||
}
|
||||
|
||||
function highlightYaml(code: string) {
|
||||
const parts: ReactNode[] = [];
|
||||
const lines = code.split(/(\n)/);
|
||||
let tokenIndex = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line === "\n") {
|
||||
parts.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const commentIndex = line.search(/\s#/);
|
||||
const fullLineComment = line.trimStart().startsWith("#");
|
||||
const contentEnd =
|
||||
fullLineComment || commentIndex === -1 ? line.length : commentIndex + 1;
|
||||
const content = fullLineComment ? "" : line.slice(0, contentEnd);
|
||||
const comment = fullLineComment ? line : line.slice(contentEnd);
|
||||
const keyMatch = content.match(/^(\s*(?:-\s*)?)([A-Za-z_][\w.-]*)(\s*:)/);
|
||||
|
||||
if (keyMatch) {
|
||||
parts.push(keyMatch[1]);
|
||||
pushMatchedToken(parts, keyMatch[2], "ktx-token-key", `yaml-key-${tokenIndex}`);
|
||||
pushMatchedToken(
|
||||
parts,
|
||||
keyMatch[3],
|
||||
"ktx-token-punctuation",
|
||||
`yaml-colon-${tokenIndex}`,
|
||||
);
|
||||
const rest = content.slice(keyMatch[0].length);
|
||||
if (rest) parts.push(...highlightInlineValue(rest, `yaml-${tokenIndex}`));
|
||||
} else if (content) {
|
||||
parts.push(...highlightInlineValue(content, `yaml-${tokenIndex}`));
|
||||
}
|
||||
|
||||
if (comment) {
|
||||
pushMatchedToken(parts, comment, "ktx-token-comment", `yaml-comment-${tokenIndex}`);
|
||||
}
|
||||
tokenIndex += 1;
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
function highlightInlineValue(value: string, keyPrefix: string) {
|
||||
const parts: ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
let tokenIndex = 0;
|
||||
const pattern = /'(?:''|[^'])*'|"(?:\\.|[^"\\])*"|-?\b\d+(?:\.\d+)?\b|\b(?:true|false|null)\b|[()[\]{},:=!<>+-]/g;
|
||||
|
||||
for (const match of value.matchAll(pattern)) {
|
||||
const token = match[0];
|
||||
const index = match.index ?? 0;
|
||||
if (index > lastIndex) parts.push(value.slice(lastIndex, index));
|
||||
|
||||
const className =
|
||||
token.startsWith("'") || token.startsWith('"')
|
||||
? "ktx-token-string"
|
||||
: /^-?\d/.test(token)
|
||||
? "ktx-token-number"
|
||||
: /^(true|false|null)$/.test(token)
|
||||
? "ktx-token-constant"
|
||||
: "ktx-token-punctuation";
|
||||
|
||||
pushMatchedToken(parts, token, className, `${keyPrefix}-value-${tokenIndex}`);
|
||||
lastIndex = index + token.length;
|
||||
tokenIndex += 1;
|
||||
}
|
||||
|
||||
if (lastIndex < value.length) parts.push(value.slice(lastIndex));
|
||||
return parts;
|
||||
}
|
||||
|
||||
function highlightSql(code: string) {
|
||||
const parts: ReactNode[] = [];
|
||||
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("--")
|
||||
? "ktx-token-comment"
|
||||
: token.startsWith("'")
|
||||
? "ktx-token-string"
|
||||
: /^\d/.test(token)
|
||||
? "ktx-token-number"
|
||||
: SQL_FUNCTIONS.has(lowerToken)
|
||||
? "ktx-token-function"
|
||||
: /^[a-z_]+$/i.test(token)
|
||||
? "ktx-token-keyword"
|
||||
: "ktx-token-punctuation";
|
||||
|
||||
pushMatchedToken(parts, token, className, `sql-${tokenIndex}`);
|
||||
lastIndex = index + token.length;
|
||||
tokenIndex += 1;
|
||||
}
|
||||
|
||||
if (lastIndex < code.length) parts.push(code.slice(lastIndex));
|
||||
return parts;
|
||||
}
|
||||
|
||||
function highlightCodeLike(code: string) {
|
||||
const parts: ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
let tokenIndex = 0;
|
||||
|
||||
for (const match of code.matchAll(CODE_LIKE_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("//") || token.startsWith("/*") || token.startsWith("#")
|
||||
? "ktx-token-comment"
|
||||
: token.startsWith("'") || token.startsWith('"') || token.startsWith("`")
|
||||
? "ktx-token-string"
|
||||
: /^-?\d/.test(token)
|
||||
? "ktx-token-number"
|
||||
: CODE_CONSTANTS.has(lowerToken)
|
||||
? "ktx-token-constant"
|
||||
: CODE_KEYWORDS.has(lowerToken)
|
||||
? "ktx-token-keyword"
|
||||
: COMMAND_KEYWORDS.has(lowerToken)
|
||||
? "ktx-token-function"
|
||||
: token.startsWith("-")
|
||||
? "ktx-token-flag"
|
||||
: "ktx-token-punctuation";
|
||||
|
||||
pushMatchedToken(parts, token, className, `code-${tokenIndex}`);
|
||||
lastIndex = index + token.length;
|
||||
tokenIndex += 1;
|
||||
}
|
||||
|
||||
if (lastIndex < code.length) parts.push(code.slice(lastIndex));
|
||||
return parts;
|
||||
}
|
||||
|
||||
function highlightCode(language: string | null, code: string) {
|
||||
const normalized = normalizeLanguage(language);
|
||||
if (normalized === "json" || normalized === "jsonc") return highlightJson(code);
|
||||
if (normalized === "yaml" || normalized === "yml") return highlightYaml(code);
|
||||
if (normalized === "sql") return highlightSql(code);
|
||||
if (
|
||||
[
|
||||
"bash",
|
||||
"sh",
|
||||
"shell",
|
||||
"zsh",
|
||||
"javascript",
|
||||
"js",
|
||||
"jsx",
|
||||
"typescript",
|
||||
"ts",
|
||||
"tsx",
|
||||
"python",
|
||||
"py",
|
||||
].includes(normalized)
|
||||
) {
|
||||
return highlightCodeLike(code);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
export function CodeBlock(props: Props) {
|
||||
const { children, title, className: _ignored, ...rest } = props;
|
||||
const language = detectLanguage(props, children);
|
||||
const codeText = extractText(children);
|
||||
const rawCodeText = extractText(children);
|
||||
const extractedHeader = extractCodeHeader(language, rawCodeText);
|
||||
const codeText = extractedHeader.code;
|
||||
const headerTitle =
|
||||
typeof title === "string" && title.length > 0
|
||||
? title
|
||||
: extractedHeader.header;
|
||||
const highlightedCode = highlightCode(language, codeText);
|
||||
|
||||
const hasTitle = typeof title === "string" && title.length > 0;
|
||||
const hasHeader = typeof headerTitle === "string" && headerTitle.length > 0;
|
||||
const isOutput =
|
||||
!hasTitle &&
|
||||
(WIZARD_GLYPHS.test(codeText) ||
|
||||
!hasHeader &&
|
||||
(WIZARD_GLYPHS.test(rawCodeText) ||
|
||||
(language !== null && OUTPUT_LANGS.has(language)));
|
||||
|
||||
// Mode D - Output preview (wizard prompts, terminal output)
|
||||
|
|
@ -81,7 +390,7 @@ export function CodeBlock(props: Props) {
|
|||
return (
|
||||
<div className="not-prose ktx-code ktx-code-output group relative">
|
||||
<span className="ktx-code-output-label">output</span>
|
||||
<CopyButton text={codeText} className="ktx-code-output-copy" />
|
||||
<CopyButton text={rawCodeText} className="ktx-code-output-copy" />
|
||||
<pre {...rest} className="ktx-code-body ktx-code-body-output">
|
||||
{children}
|
||||
</pre>
|
||||
|
|
@ -89,18 +398,17 @@ export function CodeBlock(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
// Mode B - VS Code tab (filename present)
|
||||
if (hasTitle) {
|
||||
// Mode B - Header (filename present)
|
||||
if (hasHeader) {
|
||||
return (
|
||||
<div className="not-prose ktx-code ktx-code-tab group">
|
||||
<div className="ktx-code-tab-head">
|
||||
<span className="ktx-file-glyph" data-lang={language ?? ""} />
|
||||
<span className="ktx-code-tab-filename">{title}</span>
|
||||
{language && <span className="ktx-lang-pill">{language}</span>}
|
||||
<span className="ktx-code-tab-filename">{headerTitle}</span>
|
||||
<CopyButton text={codeText} className="ml-auto" />
|
||||
</div>
|
||||
<pre {...rest} className="ktx-code-body ktx-code-body-tab">
|
||||
{children}
|
||||
{highlightedCode}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -111,7 +419,7 @@ export function CodeBlock(props: Props) {
|
|||
<div className="not-prose ktx-code ktx-code-minimal group relative">
|
||||
<CopyButton text={codeText} className="ktx-code-minimal-copy" />
|
||||
<pre {...rest} className="ktx-code-body ktx-code-body-minimal">
|
||||
{children}
|
||||
{highlightedCode}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Controls,
|
||||
Handle,
|
||||
MarkerType,
|
||||
type Node,
|
||||
type NodeProps,
|
||||
type OnInit,
|
||||
Position,
|
||||
ReactFlow,
|
||||
} from "@xyflow/react";
|
||||
|
|
@ -108,6 +111,7 @@ 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",
|
||||
|
|
@ -363,7 +367,7 @@ const edges = [
|
|||
id: "agent-manual",
|
||||
source: "agent",
|
||||
target: "manual-sql",
|
||||
type: "smoothstep" as const,
|
||||
type: "default" as const,
|
||||
label: "writes raw SQL",
|
||||
labelBgPadding: [6, 3] as [number, number],
|
||||
labelBgBorderRadius: 4,
|
||||
|
|
@ -388,7 +392,8 @@ const edges = [
|
|||
id: "manual-warehouse",
|
||||
source: "manual-sql",
|
||||
target: "warehouse",
|
||||
type: "smoothstep" as const,
|
||||
targetHandle: "warehouse-manual",
|
||||
type: "default" as const,
|
||||
style: {
|
||||
stroke: MANUAL_STROKE,
|
||||
strokeWidth: 1.5,
|
||||
|
|
@ -400,7 +405,7 @@ const edges = [
|
|||
id: "agent-slquery",
|
||||
source: "agent",
|
||||
target: "sl-query",
|
||||
type: "smoothstep" as const,
|
||||
type: "default" as const,
|
||||
label: "sends Semantic Query",
|
||||
labelBgPadding: [6, 3] as [number, number],
|
||||
labelBgBorderRadius: 4,
|
||||
|
|
@ -437,12 +442,15 @@ const edges = [
|
|||
id: "compiled-warehouse",
|
||||
source: "compiled-sql",
|
||||
target: "warehouse",
|
||||
type: "smoothstep" as const,
|
||||
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
|
||||
|
|
@ -507,6 +515,88 @@ function LaneBadge({
|
|||
);
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
@ -522,6 +612,8 @@ function CodeBlock({
|
|||
: 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">
|
||||
|
|
@ -542,7 +634,7 @@ function CodeBlock({
|
|||
className="m-0 flex-1 overflow-auto px-3 py-2 font-mono text-fd-foreground"
|
||||
style={{ fontSize: "11.5px", lineHeight: "17.5px" }}
|
||||
>
|
||||
{code}
|
||||
{highlightedCode}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -576,10 +668,11 @@ function ManualSqlNodeView({ data }: NodeProps<ManualSqlNode>) {
|
|||
className="flex items-start gap-1.5 text-[11.5px] leading-4 text-fd-muted-foreground"
|
||||
>
|
||||
<span
|
||||
className="mt-1 h-1 w-1 flex-none rounded-full"
|
||||
style={{ background: MANUAL_STROKE }}
|
||||
className="mt-[3px] flex h-2.5 w-2.5 flex-none items-center justify-center text-[11px] font-semibold leading-none text-red-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
>
|
||||
×
|
||||
</span>
|
||||
<span>{note}</span>
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -707,7 +800,20 @@ function WarehouseNodeView({ data }: NodeProps<WarehouseNode>) {
|
|||
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 type="target" position={Position.Top} className="!opacity-0" />
|
||||
<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"
|
||||
|
|
@ -748,6 +854,15 @@ const nodeTypes = {
|
|||
};
|
||||
|
||||
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
|
||||
className="not-prose my-10 w-full max-w-full min-w-0 space-y-4"
|
||||
|
|
@ -777,33 +892,36 @@ export function SemanticLayerFlow() {
|
|||
</div>
|
||||
|
||||
<div
|
||||
className="sl-flow-canvas bg-fd-background"
|
||||
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}
|
||||
fitView
|
||||
fitViewOptions={{ padding: 0.05 }}
|
||||
onInit={handleFlowInit}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
nodesFocusable={false}
|
||||
edgesFocusable={false}
|
||||
elementsSelectable={false}
|
||||
panOnDrag={false}
|
||||
panOnDrag
|
||||
panOnScroll={false}
|
||||
zoomOnScroll={false}
|
||||
zoomOnPinch={false}
|
||||
zoomOnDoubleClick={false}
|
||||
preventScrolling={false}
|
||||
minZoom={0.2}
|
||||
zoomOnScroll
|
||||
zoomOnPinch
|
||||
zoomOnDoubleClick
|
||||
preventScrolling
|
||||
minZoom={minZoom}
|
||||
maxZoom={1.5}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Controls position="bottom-right" showInteractive={false} />
|
||||
<Background
|
||||
variant={BackgroundVariant.Dots}
|
||||
gap={18}
|
||||
|
|
@ -839,7 +957,10 @@ export function SemanticLayerFlow() {
|
|||
box-shadow: none;
|
||||
}
|
||||
.sl-flow-canvas .react-flow__pane {
|
||||
cursor: default;
|
||||
cursor: grab;
|
||||
}
|
||||
.sl-flow-canvas .react-flow__pane:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.sl-flow-canvas .react-flow__handle {
|
||||
width: 1px;
|
||||
|
|
@ -864,6 +985,53 @@ export function SemanticLayerFlow() {
|
|||
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;
|
||||
}
|
||||
.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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue