From dad78ae0ceeb18cd040affda2f06e9e510d65c69 Mon Sep 17 00:00:00 2001 From: Shine1i Date: Tue, 24 Feb 2026 04:04:02 +0100 Subject: [PATCH] feat: improve markdown note styles and layout logic --- .../components/markdown/markdown-preview.tsx | 14 ++++- .../learning-recipes/conversation.json | 62 ++++++++++++++++--- .../instruction-from-answer.json | 6 +- .../learning-recipes/pdf-grounded-qa.json | 14 ++--- .../structured-outputs-jinja.json | 10 +-- .../learning-recipes/text-to-python.json | 6 +- .../learning-recipes/text-to-sql.json | 62 ++++++++++++++++--- .../components/recipe-graph-node.tsx | 4 +- .../src/features/recipe-studio/constants.ts | 1 + studio/frontend/src/index.css | 20 +++++- 10 files changed, 157 insertions(+), 42 deletions(-) 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 +}