From 1dd7ae2b2d00a04f5a807306449a14ab9bfa43fe Mon Sep 17 00:00:00 2001 From: Shine1i Date: Tue, 24 Feb 2026 03:47:42 +0100 Subject: [PATCH] feat: enhance markdown note blocks with style options and double-click config access - Added support for configuring markdown note block styles, including color and opacity. - Enabled double-click on markdown notes to open their configuration dialog. - Adjusted layout styles in markdown previews for better interaction control. - Updated relevant payloads, types, and UI logic to support added styling features. - Integrated multiple example notes in learning recipes for better visualization. --- .../components/markdown/markdown-preview.tsx | 2 +- .../instruction-from-answer.json | 66 ++++++++++++------- .../learning-recipes/pdf-grounded-qa.json | 56 +++++++++++++--- .../structured-outputs-jinja.json | 44 +++++++++++++ .../learning-recipes/text-to-python.json | 22 +++++++ .../components/recipe-graph-node.tsx | 55 ++++++++++++++++ .../markdown-note/markdown-note-dialog.tsx | 37 +++++++++++ .../recipe-studio/recipe-studio-page.tsx | 16 +++++ .../src/features/recipe-studio/types/index.ts | 4 ++ .../recipe-studio/utils/config-factories.ts | 2 + .../recipe-studio/utils/import/importer.ts | 6 ++ .../utils/payload/build-payload.ts | 2 + .../recipe-studio/utils/payload/types.ts | 2 + 13 files changed, 280 insertions(+), 34 deletions(-) diff --git a/studio/frontend/src/components/markdown/markdown-preview.tsx b/studio/frontend/src/components/markdown/markdown-preview.tsx index cc6ddede65..b3a00a01eb 100644 --- a/studio/frontend/src/components/markdown/markdown-preview.tsx +++ b/studio/frontend/src/components/markdown/markdown-preview.tsx @@ -23,7 +23,7 @@ function MarkdownPreviewImpl({
= 100 %}VIP{% elif order_total >= 50 %}Standard{% else %}Starter{% endif %}`\n- Simple math:\n - `{{ quantity * unit_price }}`\n - `{{ (subtotal - discount) | round(2) }}`\n\n### Good rule:\n- If the value can be computed from existing data, use Expression first.\n- Use LLM only when you need true language generation.", + "note_color": "#fffbeb", + "note_opacity": "35" + }, + { + "id": "note_2", + "x": 1944, + "y": 760.9999999999999, + "width": 400, + "node_type": "markdown_note", + "name": "note_2", + "markdown": "### LLM Structured block\nGenerates JSON that matches your Output Format schema.\nThink of Output Format as a contract for what the model must return.\n\n#### Prompt tips:\n\n- Reference existing columns with Jinja: `{{ column_name }}`\n- You can reference nested values too: `{{ customer.first_name }}`\n- Be explicit about what each field should contain.\n\n#### Example prompt pattern:\n\n```text\nCreate a support ticket summary.\nCustomer: {{ customer_name }}\nIssue text: {{ issue_text }}\n\nReturn data for:\n- priority\n- short_title\n- resolution_steps\n```", + "note_color": "#f4efdc", + "note_opacity": "35" + }, + { + "id": "note_3", + "x": 2381.178207301403, + "y": 790.2196835690842, + "width": 638, + "node_type": "markdown_note", + "name": "note_3", + "markdown": "## Example output format shape (concept):\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"priority\": { \"type\": \"string\" },\n \"short_title\": { \"type\": \"string\" },\n \"resolution_steps\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n },\n \"required\": [\"priority\", \"short_title\", \"resolution_steps\"]\n}\n```", + "note_color": "#fbf6e5", + "note_opacity": "35" + }, + { + "id": "note_4", + "x": 2405.796928768747, + "y": -362.26583299682716, + "width": 399, + "node_type": "markdown_note", + "name": "note_4", + "markdown": "### Model provider & Config\nEvery LLM block needs a model alias.\nThat alias comes from a Model Config.\nModel Config points to a Model Provider.\n\n#### Minimum setup:\n\n1. Create **Model Provider**\n - Set endpoint/provider type\n - Prefer env var auth (`api_key_env`) over hardcoded keys\n\n2. Create **Model Config**\n - Set alias (example: `model_1`)\n - Set model id\n - Link to provider\n - Tune params (temperature, max_tokens, etc.)\n\n3. In each LLM block\n - Set `model_alias` to that alias\n\nIf alias/provider link is missing, validation/run will fail.", + "note_color": "#f6f4ef", + "note_opacity": "35" + }, { "id": "provider_column", "x": 1947.2039072039072, diff --git a/studio/frontend/src/features/data-recipes/learning-recipes/text-to-python.json b/studio/frontend/src/features/data-recipes/learning-recipes/text-to-python.json index 7b65b9b1bc..83b36fa396 100644 --- a/studio/frontend/src/features/data-recipes/learning-recipes/text-to-python.json +++ b/studio/frontend/src/features/data-recipes/learning-recipes/text-to-python.json @@ -150,6 +150,28 @@ "x": 2040.9251520522098, "y": -13.362336454344792, "width": 400 + }, + { + "id": "note_1", + "x": 1482.1328175027095, + "y": 242.4370179053253, + "width": 568, + "node_type": "markdown_note", + "name": "note_1", + "markdown": "The **LLM Code** block is where Python code is generated from your instruction/prompt.\n\n##### How it works in this recipe:\n\n- You provide a clear prompt (often using Jinja references from earlier columns)\n- The model returns a response\n- The block extracts code content directly for the output column\n\n##### Current status:\n\n- We are **not** running Python lint/syntax validation in this recipe yet (Soon)\n- Validation support is planned and will be added\n\n##### What this means:\n\n- You may get mostly correct code, but some rows can still have syntax/style issues\n- Keep prompts specific and constrained to reduce bad outputs\n\n##### Tip:\n\n- Ask for one self-contained function/script\n- Ask for required imports\n- Ask for no markdown fences if you want cleaner extraction\n", + "note_color": "#FDE68A", + "note_opacity": "35" + }, + { + "id": "note_2", + "x": 2513.2527820497985, + "y": -235.2544980991115, + "width": 471, + "node_type": "markdown_note", + "name": "note_2", + "markdown": "The **LLM Judge** block evaluates generated outputs with rubric-style scores.\n\n##### Important:\n\n- A judge can have **one or many scores**\n- Each score has:\n - a name (for example: `Correctness`)\n - a description\n - options (value + meaning)\n\n##### Example multi-score setup:\n\n- Correctness\n- Readability\n- Efficiency\n\n##### Why use multiple scores:\n\n- You get richer quality signals than a single pass/fail\n- Easier filtering and weighting later in training data prep\n\n##### Practical pattern:\n\n1. Generate code with LLM Code\n2. Judge with 2-4 focused scores\n3. Keep high-quality rows based on score thresholds\n", + "note_color": "#FDE68A", + "note_opacity": "35" } ], "edges": [ diff --git a/studio/frontend/src/features/recipe-studio/components/recipe-graph-node.tsx b/studio/frontend/src/features/recipe-studio/components/recipe-graph-node.tsx index e58510aa2d..e86c9adc03 100644 --- a/studio/frontend/src/features/recipe-studio/components/recipe-graph-node.tsx +++ b/studio/frontend/src/features/recipe-studio/components/recipe-graph-node.tsx @@ -54,6 +54,27 @@ import { LabeledHandle } from "./rf-ui/labeled-handle"; type IconType = typeof CodeIcon; +function hexToRgb(hex: string): { r: number; g: number; b: number } | null { + const normalized = hex.trim().replace("#", ""); + if (!/^[0-9a-fA-F]{6}$/.test(normalized)) { + return null; + } + const int = Number.parseInt(normalized, 16); + return { + r: (int >> 16) & 255, + g: (int >> 8) & 255, + b: int & 255, + }; +} + +function parseNoteOpacity(value: string | undefined): number { + const parsed = Number.parseInt(value ?? "", 10); + if (!Number.isFinite(parsed)) { + return 0.35; + } + return Math.max(0.05, Math.min(1, parsed / 100)); +} + const NODE_META = { sampler: { tone: "bg-emerald-50 text-emerald-600 border-emerald-100", @@ -353,6 +374,40 @@ function RecipeGraphNodeBase({ updateNodeInternals(id); }, [id, layoutDirection, config, updateNodeInternals]); + if (config?.kind === "markdown_note") { + const rgb = hexToRgb(config.note_color ?? "#FDE68A"); + const alpha = parseNoteOpacity(config.note_opacity); + const noteStyle = rgb + ? { + backgroundColor: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`, + borderColor: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${Math.min(1, Math.max(alpha + 0.15, 0.3))})`, + } + : undefined; + + return ( + + + + + + + ); + } + const showDataHandles = data.kind === "llm" || data.kind === "expression" || diff --git a/studio/frontend/src/features/recipe-studio/dialogs/markdown-note/markdown-note-dialog.tsx b/studio/frontend/src/features/recipe-studio/dialogs/markdown-note/markdown-note-dialog.tsx index 4724ff9efe..5cb04b5169 100644 --- a/studio/frontend/src/features/recipe-studio/dialogs/markdown-note/markdown-note-dialog.tsx +++ b/studio/frontend/src/features/recipe-studio/dialogs/markdown-note/markdown-note-dialog.tsx @@ -1,3 +1,4 @@ +import { Slider } from "@/components/ui/slider"; import { Textarea } from "@/components/ui/textarea"; import type { ReactElement } from "react"; import type { MarkdownNoteConfig } from "../../types"; @@ -14,10 +15,46 @@ export function MarkdownNoteDialog({ onUpdate, }: MarkdownNoteDialogProps): ReactElement { const markdownId = `${config.id}-markdown`; + const colorId = `${config.id}-note-color`; + const opacity = + Number.parseInt(config.note_opacity ?? "35", 10) > 0 + ? Math.max(0, Math.min(100, Number.parseInt(config.note_opacity ?? "35", 10))) + : 35; return (
onUpdate({ name: value })} /> +
+ +
+ onUpdate({ note_color: event.target.value })} + /> +
+
+ Opacity + {opacity}% +
+ + onUpdate({ note_opacity: String(Math.round(value)) }) + } + /> +
+
+
) => { + if (node.type !== "builder") { + return; + } + const nodeConfig = configs[node.id]; + if (nodeConfig?.kind === "markdown_note") { + openConfig(node.id); + } + }, + [configs, openConfig], + ); + const handleNodesChange = useCallback( (changes: NodeChange>[]) => { applyAuxNodeChanges(changes, { setAuxNodePosition, setAuxNodeSize }); @@ -408,6 +423,7 @@ export function RecipeStudioPage({ onEdgesChange={handleEdgesChange} onConnect={onConnect} onNodeClick={handleNodeClick} + onNodeDoubleClick={handleNodeDoubleClick} isValidConnection={isValidConnection} nodesDraggable={interactive} nodesConnectable={interactive} diff --git a/studio/frontend/src/features/recipe-studio/types/index.ts b/studio/frontend/src/features/recipe-studio/types/index.ts index f2febc4bc0..ec61d3aaa5 100644 --- a/studio/frontend/src/features/recipe-studio/types/index.ts +++ b/studio/frontend/src/features/recipe-studio/types/index.ts @@ -222,6 +222,10 @@ export type MarkdownNoteConfig = { kind: "markdown_note"; name: string; markdown: string; + // ui-only + note_color?: string; + // ui-only (0-100 as string for slider/input ergonomics) + note_opacity?: string; }; export type SeedConfig = { diff --git a/studio/frontend/src/features/recipe-studio/utils/config-factories.ts b/studio/frontend/src/features/recipe-studio/utils/config-factories.ts index dee84dacec..f92589b213 100644 --- a/studio/frontend/src/features/recipe-studio/utils/config-factories.ts +++ b/studio/frontend/src/features/recipe-studio/utils/config-factories.ts @@ -287,6 +287,8 @@ export function makeMarkdownNoteConfig( kind: "markdown_note", name: nextName(existing, "note"), markdown: "## Note\n\nAdd markdown here.", + note_color: "#FDE68A", + note_opacity: "35", }; } diff --git a/studio/frontend/src/features/recipe-studio/utils/import/importer.ts b/studio/frontend/src/features/recipe-studio/utils/import/importer.ts index 60f44c3825..e2ea6d754a 100644 --- a/studio/frontend/src/features/recipe-studio/utils/import/importer.ts +++ b/studio/frontend/src/features/recipe-studio/utils/import/importer.ts @@ -44,6 +44,8 @@ type UiInput = { type UiMarkdownNoteNode = { name: string; markdown: string; + note_color?: string; + note_opacity?: string; }; function readStringNumber(value: unknown): string | undefined { @@ -247,6 +249,8 @@ function parseUiMarkdownNoteNodes(input: unknown): UiMarkdownNoteNode[] { noteNodes.push({ name: name.trim(), markdown: readString(node.markdown) ?? "", + note_color: readString(node.note_color) ?? undefined, + note_opacity: readStringNumber(node.note_opacity) ?? undefined, }); } return noteNodes; @@ -342,6 +346,8 @@ export function importRecipePayload(input: string): ImportResult { kind: "markdown_note", name: note.name, markdown: note.markdown, + note_color: note.note_color ?? "#FDE68A", + note_opacity: note.note_opacity ?? "35", }; if (nameToId.has(config.name)) { errors.push(`Duplicate column name: ${config.name}.`); diff --git a/studio/frontend/src/features/recipe-studio/utils/payload/build-payload.ts b/studio/frontend/src/features/recipe-studio/utils/payload/build-payload.ts index ee49fec0aa..606f6a3cd4 100644 --- a/studio/frontend/src/features/recipe-studio/utils/payload/build-payload.ts +++ b/studio/frontend/src/features/recipe-studio/utils/payload/build-payload.ts @@ -202,6 +202,8 @@ export function buildRecipePayload( node_type: "markdown_note" as const, name: config.name, markdown: config.markdown, + note_color: config.note_color, + note_opacity: config.note_opacity, }, ]; } diff --git a/studio/frontend/src/features/recipe-studio/utils/payload/types.ts b/studio/frontend/src/features/recipe-studio/utils/payload/types.ts index 12e3fce9ff..e97f8db970 100644 --- a/studio/frontend/src/features/recipe-studio/utils/payload/types.ts +++ b/studio/frontend/src/features/recipe-studio/utils/payload/types.ts @@ -38,6 +38,8 @@ export type RecipePayload = { node_type?: "markdown_note"; name?: string; markdown?: string; + note_color?: string; + note_opacity?: string; }>; edges: { from: string;