diff --git a/studio/frontend/src/components/markdown/markdown-preview.tsx b/studio/frontend/src/components/markdown/markdown-preview.tsx
index b3a00a01eb..c10ec783be 100644
--- a/studio/frontend/src/components/markdown/markdown-preview.tsx
+++ b/studio/frontend/src/components/markdown/markdown-preview.tsx
@@ -19,16 +19,24 @@ function MarkdownPreviewImpl({
className,
plain = false,
}: MarkdownPreviewProps): ReactElement {
+ const markdownClassName =
+ "w-full max-w-none min-w-0 space-y-2 [overflow-wrap:anywhere] [&_*]:max-w-none [&_p]:w-full [&_ul]:w-full [&_ol]:w-full [&_li]:w-full [&_h1]:w-full [&_h2]:w-full [&_h3]:w-full [&_h4]:w-full [&_h5]:w-full [&_h6]:w-full [&_pre]:w-full [&_table]:w-full [&_p]:break-words [&_li]:break-words [&_code]:break-words [&_pre]:whitespace-pre-wrap [&_pre]:break-words";
+
return (
-
+
{markdown.trim() ? markdown : "_Empty note_"}
diff --git a/studio/frontend/src/features/data-recipes/learning-recipes/conversation.json b/studio/frontend/src/features/data-recipes/learning-recipes/conversation.json
index 1c14bb160e..ac5c18b5ff 100644
--- a/studio/frontend/src/features/data-recipes/learning-recipes/conversation.json
+++ b/studio/frontend/src/features/data-recipes/learning-recipes/conversation.json
@@ -137,6 +137,18 @@
},
"ui": {
"nodes": [
+ {
+ "id": "provider_1",
+ "x": -1056.848383841495,
+ "y": 519.6373927070263,
+ "width": 400
+ },
+ {
+ "id": "model_1",
+ "x": -543.7221365246206,
+ "y": 488.2975724283656,
+ "width": 400
+ },
{
"id": "domain",
"x": 0,
@@ -168,16 +180,48 @@
"width": 400
},
{
- "id": "provider_1",
- "x": -1056.848383841495,
- "y": 519.6373927070263,
- "width": 400
+ "id": "note_1",
+ "x": 210.01377182764494,
+ "y": -262.9440547613487,
+ "width": 400,
+ "node_type": "markdown_note",
+ "name": "note_1",
+ "markdown": "### Start with controlled chat context\nThis recipe uses sampler columns to shape each conversation:\n\n- `domain`\n- `topic`\n- `conversation_length` (4 or 6 messages)\n\n**Why this helps**:\n\n- You get varied conversations without manual writing\n- Each row stays grounded in a clear scenario\n- You can scale quickly while keeping data quality consistent",
+ "note_color": "#FFE4E6",
+ "note_opacity": "35"
},
{
- "id": "model_1",
- "x": -543.7221365246206,
- "y": 488.2975724283656,
- "width": 400
+ "id": "note_2",
+ "x": 515.9369583007435,
+ "y": 454.3936030274385,
+ "width": 400,
+ "node_type": "markdown_note",
+ "name": "note_2",
+ "markdown": "The **LLM Text** block (`user_goal`) creates one realistic user intent from sampler context.\n\n**It should be**:\n\n- **specific**\n- **practical**\n- **short**\n\nThis goal becomes the anchor for the full multi-turn conversation.",
+ "note_color": "#FFE4E6",
+ "note_opacity": "35"
+ },
+ {
+ "id": "note_3",
+ "x": -12.952616065779239,
+ "y": 912.1316336111515,
+ "width": 400,
+ "node_type": "markdown_note",
+ "name": "note_3",
+ "markdown": "The **LLM Structured** block (`output_format`) generates the conversation as strict JSON.\n\nIn this recipe, schema enforces:\n\n- `conversation` array\n- message objects with `role` + `content`\n- role enum: `user` / `assistant`\n- no extra keys\n\nPrompt constraints also enforce:\n\n- exact length (`{{ conversation_length }}`)\n- alternating roles\n- first user message, last assistant message\n- natural ending\n\nThis is key for training data: same shape, less cleanup.",
+ "note_color": "#FFE4E6",
+ "note_opacity": "35"
+ },
+ {
+ "id": "note_4",
+ "x": -519.9585237323188,
+ "y": 81.84144119564277,
+ "width": 400,
+ "node_type": "markdown_note",
+ "name": "note_4",
+ "markdown": "Sampler columns are useful during generation but usually noisy in final export.\n\nSet helper columns to `drop=true`, keep only core outputs such as:\n\n- `user_goal`\n- `output_format`\n\nTip: Keep final schema close to your training format, not your generation scaffolding.\n",
+ "note_color": "#FFE4E6",
+ "note_opacity": "35"
}
],
"edges": [
@@ -233,4 +277,4 @@
],
"layout_direction": "LR"
}
-}
\ No newline at end of file
+}
diff --git a/studio/frontend/src/features/data-recipes/learning-recipes/instruction-from-answer.json b/studio/frontend/src/features/data-recipes/learning-recipes/instruction-from-answer.json
index 8d271e1ff8..845fdc4f17 100644
--- a/studio/frontend/src/features/data-recipes/learning-recipes/instruction-from-answer.json
+++ b/studio/frontend/src/features/data-recipes/learning-recipes/instruction-from-answer.json
@@ -64,7 +64,7 @@
"node_type": "markdown_note",
"name": "note_1",
"markdown": "#### Hugginface seed block\nThis recipe uses a ** HuggingFace dataset ** as seed data.\nYou provide dataset identity, load columns, then generate new fields from seed columns.\n\n##### Setup:\n\n1. Paste dataset id as `org/repo` (example: `unsloth/alpaca-cleaned`)\n2. Add token only if dataset is gated/private\n3. Load columns + preview rows so variables are available in prompts\n\n##### Why this matters:\n- Seed columns can drive generation quality\n- You can reference seed values directly in prompts (for example `{{ output }}`)",
- "note_color": "#d6fff8",
+ "note_color": "#DCFCE7",
"note_opacity": "35"
},
{
@@ -75,7 +75,7 @@
"node_type": "markdown_note",
"name": "note_2",
"markdown": "##### Drop columns behavior:\n\n- You can mark specific seed columns to **drop from final output**\n- Those columns are still used during generation\n- They are removed only from exported final dataset\n\n##### Example:\n- Keep `generated_instruction` from llm-text block\n- Drop original `instruction`, `input`, `output` from the hugginface dataset from final artifact\n- Result: clean training output while still using source columns as generation context\n",
- "note_color": "#d6fff8",
+ "note_color": "#DCFCE7",
"note_opacity": "35"
},
{
@@ -191,4 +191,4 @@
"unstructured_chunk_size": "1200",
"unstructured_chunk_overlap": "200"
}
-}
\ No newline at end of file
+}
diff --git a/studio/frontend/src/features/data-recipes/learning-recipes/pdf-grounded-qa.json b/studio/frontend/src/features/data-recipes/learning-recipes/pdf-grounded-qa.json
index 2329300b52..3f2fb95743 100644
--- a/studio/frontend/src/features/data-recipes/learning-recipes/pdf-grounded-qa.json
+++ b/studio/frontend/src/features/data-recipes/learning-recipes/pdf-grounded-qa.json
@@ -78,8 +78,8 @@
"node_type": "markdown_note",
"name": "note_1",
"markdown": "This recipe uses **seed data** from external documents.\nInstead of starting from empty generation, we load real source text first.\n\nIn this flow, the seed source is **Unstructured Documents**:\n\n- Upload: `.pdf`, `.docx`, `.txt`\n- Text is extracted and split on client into chunks\n- Each chunk becomes a row-like seed record (`chunk_text`) that you can reference in prompts with `{{ chunk_text }} `",
- "note_color": "#30f9fd",
- "note_opacity": "10"
+ "note_color": "#F3E8FF",
+ "note_opacity": "35"
},
{
"id": "note_2",
@@ -89,8 +89,8 @@
"node_type": "markdown_note",
"name": "note_2",
"markdown": "##### Chunking settings:\n\n- **Chunk size**: how much text per chunk\n- **Chunk overlap**: shared text between neighboring chunks to preserve context\n\n##### Sampling settings:\n\n- **Ordered**: keep original document order\n- **Shuffle**: randomize chunk order\n- **Selection index / selection settings**: choose which part/subset of seed data to use",
- "note_color": "#2bf9fd",
- "note_opacity": "10"
+ "note_color": "#F3E8FF",
+ "note_opacity": "35"
},
{
"id": "note_3",
@@ -100,8 +100,8 @@
"node_type": "markdown_note",
"name": "note_3",
"markdown": "- LLM prompt: `{{ chunk_text }}`\n- Expression block: combine/format values using `{{ chunk_text }}`\n- Processor templates: use `{{ chunk_text }}` during transforms\n\nTip:\n- Start with medium chunk size + small overlap.\n- Increase overlap only if answers lose context between chunks.",
- "note_color": "#8bf0fd",
- "note_opacity": "20"
+ "note_color": "#F3E8FF",
+ "note_opacity": "35"
},
{
"id": "seed",
@@ -194,4 +194,4 @@
"unstructured_chunk_size": "1200",
"unstructured_chunk_overlap": "200"
}
-}
\ No newline at end of file
+}
diff --git a/studio/frontend/src/features/data-recipes/learning-recipes/structured-outputs-jinja.json b/studio/frontend/src/features/data-recipes/learning-recipes/structured-outputs-jinja.json
index 58f9b7ef13..f6f11bbc71 100644
--- a/studio/frontend/src/features/data-recipes/learning-recipes/structured-outputs-jinja.json
+++ b/studio/frontend/src/features/data-recipes/learning-recipes/structured-outputs-jinja.json
@@ -174,7 +174,7 @@
"node_type": "markdown_note",
"name": "note_1",
"markdown": "## Expression columns \nAre like lightweight spreadsheet formulas.\nUse them when you want to transform existing columns quickly, without calling an LLM.\n\n### What you can do:\n\n- Use values from other columns: `{{ first_name }} {{ last_name }}`\n- Clean/format text: `{{ city | upper }}`, `{{ product_name | trim }}`\n- Conditional logic:\n - `{% if order_total >= 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_color": "#CFFAFE",
"note_opacity": "35"
},
{
@@ -185,7 +185,7 @@
"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_color": "#CFFAFE",
"note_opacity": "35"
},
{
@@ -196,7 +196,7 @@
"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_color": "#CFFAFE",
"note_opacity": "35"
},
{
@@ -207,7 +207,7 @@
"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_color": "#CFFAFE",
"note_opacity": "35"
},
{
@@ -359,4 +359,4 @@
],
"layout_direction": "LR"
}
-}
\ No newline at end of file
+}
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 83b36fa396..1208e4a959 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
@@ -159,7 +159,7 @@
"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_color": "#FEF3C7",
"note_opacity": "35"
},
{
@@ -170,7 +170,7 @@
"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_color": "#FEF3C7",
"note_opacity": "35"
}
],
@@ -234,4 +234,4 @@
],
"layout_direction": "LR"
}
-}
\ No newline at end of file
+}
diff --git a/studio/frontend/src/features/data-recipes/learning-recipes/text-to-sql.json b/studio/frontend/src/features/data-recipes/learning-recipes/text-to-sql.json
index 7aadf09a13..60957bcf4a 100644
--- a/studio/frontend/src/features/data-recipes/learning-recipes/text-to-sql.json
+++ b/studio/frontend/src/features/data-recipes/learning-recipes/text-to-sql.json
@@ -119,6 +119,18 @@
},
"ui": {
"nodes": [
+ {
+ "id": "provider_1",
+ "x": -1092.2003193114556,
+ "y": 715.157165665104,
+ "width": 400
+ },
+ {
+ "id": "model_1",
+ "x": -546.1001596557278,
+ "y": 681.8114012018752,
+ "width": 400
+ },
{
"id": "domain",
"x": -18.379173679952572,
@@ -156,16 +168,48 @@
"width": 400
},
{
- "id": "model_1",
- "x": -546.1001596557278,
- "y": 681.8114012018752,
- "width": 400
+ "id": "note_1",
+ "x": -103.00586025666547,
+ "y": -332.088439142397,
+ "width": 600,
+ "node_type": "markdown_note",
+ "name": "note_1",
+ "markdown": "##### This recipe starts with **sampler columns** to create controlled SQL task context:\n\n- `domain`\n- `topic` (subcategory from `domain`)\n- `sql_task_type`\n- `instruction_phrase`\n\n##### Why this is useful:\n\n- You get diverse tasks without writing every prompt by hand\n- You can steer business context + task pattern in a predictable way\n- LLM prompts become cleaner because context is already structured",
+ "note_color": "#DBEAFE",
+ "note_opacity": "35"
},
{
- "id": "provider_1",
- "x": -1092.2003193114556,
- "y": 715.157165665104,
- "width": 400
+ "id": "note_2",
+ "x": 517.0372102151987,
+ "y": 600.4949327304814,
+ "width": 400,
+ "node_type": "markdown_note",
+ "name": "note_2",
+ "markdown": "The **LLM Text** block (`sql_prompt`) turns sampler context into one clean natural-language SQL task.\n\n##### Prompt pattern in this recipe:\n\n- references prior columns with Jinja (`{{ domain }}`, `{{ topic }}`, etc.)\n- enforces start phrase with `{{ instruction_phrase }}`\n- returns instruction text only (no SQL yet)\n\n##### Tip:\n\n- Keep this instruction block concise and specific\n- Save implementation details for the next SQL generation block",
+ "note_color": "#DBEAFE",
+ "note_opacity": "35"
+ },
+ {
+ "id": "note_3",
+ "x": 12.635681904967385,
+ "y": 1224.7626182706356,
+ "width": 400,
+ "node_type": "markdown_note",
+ "name": "note_3",
+ "markdown": "The **LLM Code** block (`sql`) generates SQL script from `{{ sql_prompt }}`.\n\n##### In this recipe it returns:\n\n- schema section (`CREATE TABLE`)\n- sample seed rows (`INSERT`)\n- final query (`SELECT`)\n\n##### Current status:\n\n- SQL validation block is **not** included yet in this learning recipe\n- We will add SQL validation later",
+ "note_color": "#DBEAFE",
+ "note_opacity": "35"
+ },
+ {
+ "id": "note_4",
+ "x": -1044,
+ "y": 108.64730935525904,
+ "width": 400,
+ "node_type": "markdown_note",
+ "name": "note_4",
+ "markdown": "Sampler columns are useful during generation, but often noisy in final output.\n\nSet helper columns to **drop=true** (like in this recipe), keep only output columns you want to export.\n\n#### Final keep we have set here:\n\n- `sql_prompt`\n- `sql`\n\n",
+ "note_color": "#DBEAFE",
+ "note_opacity": "35"
}
],
"edges": [
@@ -228,4 +272,4 @@
],
"layout_direction": "LR"
}
-}
\ No newline at end of file
+}
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 e86c9adc03..54bef62fc6 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
@@ -27,7 +27,7 @@ import {
type NodeProps,
} from "@xyflow/react";
import { memo, type ReactElement, useEffect } from "react";
-import { MAX_NODE_WIDTH, MIN_NODE_WIDTH } from "../constants";
+import { MAX_NODE_WIDTH, MAX_NOTE_NODE_WIDTH, MIN_NODE_WIDTH } from "../constants";
import { useRecipeStudioStore } from "../stores/recipe-studio";
import type {
RecipeNode as RecipeGraphNodeType,
@@ -393,7 +393,7 @@ function RecipeGraphNodeBase({
isVisible={selected}
minWidth={MIN_NODE_WIDTH}
minHeight={80}
- maxWidth={MAX_NODE_WIDTH}
+ maxWidth={MAX_NOTE_NODE_WIDTH}
maxHeight={520}
color="var(--primary)"
lineClassName="!border-transparent !shadow-none"
diff --git a/studio/frontend/src/features/recipe-studio/constants.ts b/studio/frontend/src/features/recipe-studio/constants.ts
index 20e9f5c9ef..94fdb02868 100644
--- a/studio/frontend/src/features/recipe-studio/constants.ts
+++ b/studio/frontend/src/features/recipe-studio/constants.ts
@@ -2,3 +2,4 @@ export const DEFAULT_NODE_WIDTH = 400;
export const DEFAULT_NODE_HEIGHT = 120;
export const MIN_NODE_WIDTH = 260;
export const MAX_NODE_WIDTH = 900;
+export const MAX_NOTE_NODE_WIDTH = 600;
diff --git a/studio/frontend/src/index.css b/studio/frontend/src/index.css
index d96cd8ab0a..f459913d55 100644
--- a/studio/frontend/src/index.css
+++ b/studio/frontend/src/index.css
@@ -298,6 +298,24 @@
.dark .shadow-border {
--tw-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
+
+ [data-streamdown="unordered-list"] {
+ list-style-type: disc;
+ list-style-position: outside;
+ padding-left: 1.25rem;
+ margin-block: 0.5rem;
+ }
+
+ [data-streamdown="ordered-list"] {
+ list-style-type: decimal;
+ list-style-position: outside;
+ padding-left: 1.25rem;
+ margin-block: 0.5rem;
+ }
+
+ [data-streamdown="list-item"] {
+ display: list-item;
+ }
}
/* Minimal scrollbar — thumb only, no track */
@@ -340,4 +358,4 @@
body {
@apply bg-background text-foreground;
}
-}
\ No newline at end of file
+}