feat: improve markdown note styles and layout logic

This commit is contained in:
Shine1i 2026-02-24 04:04:02 +01:00
commit dad78ae0ce
10 changed files with 157 additions and 42 deletions

View file

@ -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 (
<div
className={cn(
plain
? "h-full w-full overflow-auto p-2 text-xs leading-relaxed pointer-events-none select-none"
: "nodrag max-h-56 overflow-auto rounded-md border border-border/60 bg-muted/20 p-2 text-xs leading-relaxed",
? "h-full w-full min-w-0 overflow-auto p-2 text-xs leading-relaxed pointer-events-none select-none"
: "nodrag max-h-56 w-full min-w-0 overflow-auto rounded-md border border-border/60 bg-muted/20 p-2 text-xs leading-relaxed",
className,
)}
>
<Streamdown mode="static" plugins={MARKDOWN_PLUGINS} controls={false}>
<Streamdown
mode="static"
plugins={MARKDOWN_PLUGINS}
controls={false}
className={markdownClassName}
>
{markdown.trim() ? markdown : "_Empty note_"}
</Streamdown>
</div>

View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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"

View file

@ -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;

View file

@ -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;
}
}
}