From 1dc66ff3a8924eb7eaf9edfbc37d5d657b731274 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 14:42:48 +0000 Subject: [PATCH 1/2] fix(release): upload the snap to the GitHub release only The Linux release failed: electron-builder publishes a snap to the Snap Store as well as to GitHub, through snapcraft's 'push' command, which snapcraft renamed to 'upload' (exit status 64). The workflow has no Snap Store credentials either. The snap now goes to the GitHub release like the AppImage, deb, rpm and pacman packages. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KissRTEwCeUS6CQDfps5SW --- electron-builder.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/electron-builder.json b/electron-builder.json index 0b9e404..5a997a6 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -34,6 +34,15 @@ "icon": "./src-electron/assets/icon.png", "category": "Graphics" }, + "snap": { + "publish": [ + { + "provider": "github", + "protocol": "https", + "releaseType": "release" + } + ] + }, "nsis": { "oneClick": false, "perMachine": false, From 4a204ac8d27c7e65699ae63a8c3cfccc1c54931d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 14:48:12 +0000 Subject: [PATCH 2/2] feat(agent): build designs block by block, with editable HTML blocks Models put whole images in one HTML block and declared no variables, so nothing could be adjusted from the panel. The design guide (Agent prompt, MCP instructions and get_design_guide) now: - asks to build block by block: background with set_canvas_background, every headline and paragraph as a text block, one add_block per piece - says what each block type is for, with proven layouts and an example plan for 1080x1350 - sets a contract for HTML blocks: one component sized to its content, short labels only, and a :root block of annotated variables for colors, sizes, radius, shadow, icon and show/hide flags (booleans read with a container style query), with a worked stat tile example - adds those rules to the workflow and the final checklist add_block and update_html_block now answer with hints when an HTML block declares no variables, covers most of the canvas or holds paragraphs of text, so the model fixes it in the same turn. The add_block, HTML block and update_html_block descriptions say the same. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KissRTEwCeUS6CQDfps5SW --- CHANGELOG.md | 10 ++++ docs/agent.md | 9 +++ docs/html-blocks.md | 7 +++ src/lib/agent/core/design-guide.ts | 76 +++++++++++++++++++++----- src/lib/agent/core/system-prompt.ts | 4 +- src/lib/agent/mcp/instructions.ts | 4 +- src/lib/agent/tools/blocks.ts | 32 +++++++++-- src/lib/agent/tools/html-hints.test.ts | 50 +++++++++++++++++ src/lib/agent/tools/html-hints.ts | 70 ++++++++++++++++++++++++ src/lib/agent/tools/tools.test.ts | 25 +++++++++ src/lib/blocks/catalog.ts | 2 +- 11 files changed, 266 insertions(+), 23 deletions(-) create mode 100644 src/lib/agent/tools/html-hints.test.ts create mode 100644 src/lib/agent/tools/html-hints.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 699a765..0b82c3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased + +### πŸš€ Features + +- **Agent and MCP clients build designs block by block**: the design guide now says what each block is for, asks for text blocks for the copy and one HTML block per component, and gives proven layouts with an example plan. HTML blocks must declare their colors, sizes, radius, shadow, icon and show/hide flags as annotated `:root` variables, with a worked example; `add_block` and `update_html_block` answer with hints when a block misses them or grows into a whole section + +### πŸ› Fixes + +- The Linux release failed while uploading the snap to the Snap Store (snapcraft renamed `push` to `upload`); the snap now goes to the GitHub release with the other packages + ## v 2.0.0 - Release (September 27th, 2026) ### πŸš€ Features diff --git a/docs/agent.md b/docs/agent.md index 9c753e5..61e3a89 100644 --- a/docs/agent.md +++ b/docs/agent.md @@ -106,6 +106,15 @@ the Agent system prompt and of the MCP server instructions, and `get_design_guide` returns it for clients that ignore server instructions. Change the guide there; the prompt, the instructions and the tool follow. +The guide asks models to build a design block by block: the background with +`set_canvas_background`, every headline and paragraph as a text block, and one +HTML block per component (a stat tile, a card, a badge row, a chart), sized to +its content and declaring its colors, sizes, radius, shadow and icon as +annotated `:root` variables. `add_block` and `update_html_block` check the +last two rules (`src/lib/agent/tools/html-hints.ts`) and answer with `hints` +when an HTML block declares no variables, covers most of the canvas or holds +paragraphs of text, so the model fixes it in the same turn. + ### Brand kit File β†’ **Brand kit…** (or the command palette) holds the colors, fonts, diff --git a/docs/html-blocks.md b/docs/html-blocks.md index b5165b2..6bd09f1 100644 --- a/docs/html-blocks.md +++ b/docs/html-blocks.md @@ -215,6 +215,13 @@ document.getElementById('myForm').reset(); --show-shadow: true; } +/* A boolean is read with a style query, no JavaScript needed */ +@container style(--show-shadow: false) { + .card { + box-shadow: none; + } +} + .card { background: var(--card-bg); border: 2px solid var(--card-border); diff --git a/src/lib/agent/core/design-guide.ts b/src/lib/agent/core/design-guide.ts index efd3cd9..32fc3ab 100644 --- a/src/lib/agent/core/design-guide.ts +++ b/src/lib/agent/core/design-guide.ts @@ -17,6 +17,23 @@ Set the size before adding blocks (create_workspace or set_canvas_size). When th - Slide or desktop showcase: 1920Γ—1080 No platform given: 1080Γ—1350 for a post, 1600Γ—900 for a code snippet or product screenshot. +## Build it block by block +An image is a stack of separate blocks, never one big block. Plan it, then add it one piece at a time: +1. Background: set_canvas_background (color, gradient, texture, image or dynamic). Never an HTML block that fills the canvas. +2. The hero: the one thing people look at first (a mockup, a big number, a code window, a headline). +3. Headline and supporting text: text blocks, one per line of meaning (eyebrow, headline, subtitle, caption). +4. Supporting pieces: one block each, a component, a logo, an icon, a badge row. +5. Look (get_canvas_snapshot), then fix spacing and alignment. +One add_block call per piece, from the back to the front, each placed at its own x, y, width and height on the grid. When the user asks for "a post with X and Y", that is several blocks, not one HTML block containing X and Y. + +## What each block is for +- Text (\`text\`): every headline, subtitle, paragraph, label and call to action. The user edits it on the canvas and changes its font; set fontFamily and fontSource, textSize, fontWeight, color, lineHeight, letterSpacing and textAlign. Use width with sizing "fixed-width" for text that wraps. +- Code (\`code\`): source code in a window, highlighted. Mockups (\`window\` browser or app window, \`phone_mockup\`): screenshots and app screens. +- Image (\`image\`): photos, screenshots, logos (the brand logo with add_brand_logo). Icon (\`icon\`): a single icon, find its name with search_icons. +- Shape (\`shape\`): rectangles, pills, circles, lines, arrows, stars and dividers, with solid or gradient fill and a stroke. Prefer a shape over an HTML block for simple geometry. Drawing (\`drawing\`): a hand-drawn stroke or underline. QR (\`qr\`): links people should scan. +- HTML (\`html\`): one self-contained component that the other blocks cannot draw: a stat or KPI tile, a card, a row of badges, a checklist, a pricing card, a testimonial, a chart in inline SVG, a progress ring, a timeline, a table, a small UI screen. +- Components (add_component): ready-made HTML components from the user's library; use one when it fits instead of writing your own. + ## Layout - One focal point. Decide what must be read first and make it the largest, highest-contrast element. - Margins of at least 6% of the short side on every edge (64 px on a 1080 canvas, 96 px on 1600Γ—900). Backgrounds bleed; content never touches the edge. @@ -24,6 +41,8 @@ No platform given: 1080Γ—1350 for a post, 1600Γ—900 for a code snippet or produc - Align to few shared edges: one left edge, or one centered axis. Use align_blocks and distribute_blocks instead of eyeballing. - Keep 30–40% of the canvas empty. Fewer, larger elements beat many small ones. - Group related items; separate groups with at least twice the gap used inside a group. +- Proven layouts: a left-aligned stack (eyebrow, headline, subtitle, then the hero below); a split (text on one half, the mockup on the other, both vertically centered); a centered hero (big number or headline in the middle, one line under it); a grid of 2–4 equal cards with the same gap on every side. +- Example plan for 1080Γ—1350 (margins 80): eyebrow text at y 120; headline at y 168, width 920; subtitle 32 px under it; the hero from y 560 to 1150 across 920 px; a footer line (logo or handle) at y 1206. ## Typography - At most 2 typefaces (one for headlines, one for text) and 3 sizes. @@ -39,23 +58,49 @@ No platform given: 1080Γ—1350 for a post, 1600Γ—900 for a code snippet or produc - Gradients between neighbouring hues (under ~60Β° apart) or two shades of one hue; no rainbows. A little noise (3–6) adds depth to flat gradients. - Dark designs: background #0b0d12–#17191f rather than pure black, text #f5f5f7, muted #9aa3b2. Light designs: #f7f7f8 background, #111318 text. - Spend the accent only on what matters: a keyword, a number, a call to action. +- Use the same colors in every block: the text blocks, the shapes and the :root variables of the HTML blocks share one palette. ## Depth and finish - One family of corner radii (e.g. 12/20/28) and soft shadows: large blur, low opacity, a downward offset. No hard black shadows. - Mockups (code, window, phone) are the hero: 60–80% of the canvas width, on the grid, with a soft shadow. - Code shots: at most ~15 lines and ~60 columns of real, correct code, a meaningful window title, a theme that contrasts with the background. -## HTML blocks β€” for everything native blocks do not do well -Use an \`html\` block for composed UI: stat and KPI tiles, feature cards and grids, pills and badges, checklists and icon lists, pricing cards, testimonials and fake posts, charts as inline SVG, progress bars and rings, timelines, comparison tables, app screens, wordmarks, decorative backgrounds (SVG patterns, blobs, grids). -- Self-contained HTML and CSS. No JavaScript unless something has to be computed: scripts are off by default and the design must look finished without them. -- The root element fills the block (\`width: 100%; height: 100%\`, the block is a flex container). Give the block the size of its content with the width and height of add_block. -- Put every value a user may tweak in one \`:root { }\` block, annotated so the properties panel shows controls: - \`/* @type:color */ --accent: #f43f5e;\` Β· \`/* @type:number min:0 max:64 step:1 unit:px */ --radius: 24px;\` Β· \`/* @type:shadow */ --shadow: #00000040 0px 24px 48px 0px;\` Β· \`/* @type:boolean */ --show-badge: true;\` - Colors in hex, shadows with 8-digit hex, numbers with px, %, em or rem. No nested braces inside \`:root\`. -- Icons: put the icon in a \`/* @type:icon */ --my-icon: FaRocket;\` variable so the user can swap it from the panel (Font Awesome names, or \`:\` from an installed icon pack; find them with search_icons) and draw it with \`\`, which takes the text color. Shapes no icon set has: inline SVG with \`fill="currentColor"\`. -- Fonts: write the Google font name in font-family; the app loads it. -- No @keyframes or animations (an export is a single frame), no remote images unless the user gave the URL; prefer CSS gradients and inline SVG. -- Keep text as text so it stays editable, and keep it inside the block (no overflow). +## HTML blocks: one component each, with editable variables +Every HTML block follows this contract: +- One component, sized to its content with the width and height of add_block (a stat tile is about 400Γ—220, not the canvas). Its root element fills the block: \`width: 100%; height: 100%; box-sizing: border-box\`. +- Short labels only; headlines and paragraphs are text blocks next to it. Text that changes from post to post uses a project variable: \`{{title}}\` in the HTML. +- **Declare what the user may tweak.** Start the CSS with one \`:root { }\` block listing every color, size, radius, shadow, icon and show/hide flag of the component as an annotated variable, then use only var(--…) for those values below it. Each annotation becomes a control in the properties panel: + - \`/* @type:color */ --accent: #f43f5e;\` (hex, 6 or 8 digits) + - \`/* @type:number min:0 max:48 step:1 unit:px */ --radius: 24px;\` (with px, %, em or rem) + - \`/* @type:shadow */ --shadow: #00000040 0px 24px 48px 0px;\` (8-digit hex color, then x y blur spread) + - \`/* @type:icon */ --icon: FaRocket;\` (drawn with \`\`, which takes the text color; names from search_icons) + - \`/* @type:boolean */ --show-badge: true;\` (hide with \`@container style(--show-badge: false) { .badge { display: none } }\`) + One annotation per variable, on the line above it; no nested braces inside \`:root\`. Aim for 4–8 variables. +- Self-contained HTML and CSS, no JavaScript (scripts are off by default). No @keyframes (an export is one frame), no remote images unless the user gave the URL; draw with CSS gradients and inline SVG (\`fill="currentColor"\`). +- Fonts: write the Google font name in font-family; the app loads it. Use the same fonts as the text blocks. +- Keep everything inside the block (no overflow, no negative margins past its edge). + +Example: a stat tile, added with add_block { type: "html", x: 80, y: 840, width: 440, height: 240, properties: { html, css } } +html: \`

128k

monthly users

\` +css: +\`\`\` +:root { + /* @type:color */ --bg: #16161a; + /* @type:color */ --text: #f5f5f7; + /* @type:color */ --accent: #fb7185; + /* @type:number min:0 max:48 step:1 unit:px */ --radius: 28px; + /* @type:number min:40 max:120 step:1 unit:px */ --value-size: 88px; + /* @type:shadow */ --shadow: #00000059 0px 24px 48px 0px; + /* @type:icon */ --icon: FaUsers; + /* @type:boolean */ --show-icon: true; +} +.tile { width: 100%; height: 100%; box-sizing: border-box; padding: 32px; display: flex; flex-direction: column; justify-content: center; gap: 8px; background: var(--bg); color: var(--text); border-radius: var(--radius); box-shadow: var(--shadow); font-family: Inter, sans-serif; } +.icon { color: var(--accent); font-size: 36px; } +@container style(--show-icon: false) { .icon { display: none; } } +.value { margin: 0; font-size: var(--value-size); font-weight: 800; line-height: 1; } +.label { margin: 0; font-size: 28px; opacity: .7; } +\`\`\` +add_block and update_html_block answer with hints when a block misses its variables or grows into a whole section; fix them before moving on. ## Templates: variables for content that changes - Text that will change from one post to the next (headline, code sample, date, author, handle, version, price) goes in a project variable: write \`{{name}}\` in the block (text, code, window title or url, QR, HTML) and define it with set_variables. The next post is then one set_variables call instead of a redesign. @@ -65,11 +110,14 @@ Use an \`html\` block for composed UI: stat and KPI tiles, feature cards and gri ## Workflow 1. Read the brand kit with get_brand_kit. When it has colors, fonts, logos or guidelines, they come first: use them instead of the palettes and fonts suggested here, and place the logo with add_brand_logo when the design calls for one. 2. Understand the goal: platform, message, audience. Choose the size, palette and fonts before adding blocks. -3. Build from the back: background, hero element, headline, supporting elements. -4. Look at the result with get_canvas_snapshot when it is available and check it against the list below. Fix what fails and look again, at most twice. -5. Summarize in a sentence or two. +3. Plan the blocks: list each piece with its type and its box (x, y, width, height) on the grid, using one of the layouts above. +4. Build it step by step, one add_block per piece, from the back: background, hero, headline and text, supporting pieces. +5. Look at the result with get_canvas_snapshot when it is available and check it against the list below. Fix what fails with update_block, align_blocks or distribute_blocks, and look again, at most twice. +6. Summarize in a sentence or two. ## Checklist before finishing +- The image is several blocks: background, text blocks for the copy, one block per component. No HTML block covers most of the canvas. +- Every HTML block declares its colors, sizes, radius, shadow and icon as annotated :root variables. - Nothing touches or crosses the canvas edge by accident; margins are respected. - Text is readable on a phone (over 24 px on a 1080 px canvas) and passes contrast. - At most 2 fonts and 3 text sizes; edges line up. diff --git a/src/lib/agent/core/system-prompt.ts b/src/lib/agent/core/system-prompt.ts index 298bd75..ad7bad5 100644 --- a/src/lib/agent/core/system-prompt.ts +++ b/src/lib/agent/core/system-prompt.ts @@ -12,8 +12,8 @@ How to work: - Before a new design, read the brand kit (get_brand_kit) and follow it: its colors, fonts, logos (add_brand_logo) and guidelines. - Call get_workspace before editing an existing design, and use the block ids it returns. Coordinates are canvas pixels from the top-left corner. - Call list_block_types when you need the properties of a block type. -- Prefer a few precise tool calls over many small ones. Update several properties of a block in one update_block call. -- Reach for an html block whenever a native block cannot express the design well (cards, stat tiles, badges, lists with icons, charts, testimonials, UI screens, decorative backgrounds). Follow the HTML block rules below. +- Build every design block by block, step by step: plan the pieces, then one add_block per piece (background, hero, each text, each component), never the whole image or a whole section in one HTML block. Update several properties of one block in a single update_block call. +- Headlines and paragraphs are text blocks. An html block is one component the native blocks cannot draw (a stat tile, a card, a badge row, a chart), sized to its content, and it always declares its colors, sizes, radius, shadow and icon as annotated :root variables so the user can adjust it. Follow the HTML block rules below; add_block and update_html_block answer with hints when a block breaks them. - The component library (list_components) holds ready-made HTML components; add_component puts one on the canvas. - When you add a code block, put the code in its \`code\` property as plain text and set \`lang\` and a fitting \`wintitle\`. - Text blocks take a font: set \`fontFamily\` to a family the machine has or to a Google font, with \`fontSource\` as \`system\` or \`google\`, plus alignment, weight, line height, letter spacing, an outline and a shadow of the letters. diff --git a/src/lib/agent/mcp/instructions.ts b/src/lib/agent/mcp/instructions.ts index df070d5..dbff221 100644 --- a/src/lib/agent/mcp/instructions.ts +++ b/src/lib/agent/mcp/instructions.ts @@ -9,8 +9,8 @@ export const MCP_INSTRUCTIONS = `Karbonized is an editor for social media graphi - Call get_workspace before editing an existing design and use the block ids it returns. Coordinates are canvas pixels from the top-left. - Read the user's brand kit with get_brand_kit before a new design and use its colors, fonts and logos (add_brand_logo). -- For a new design, set the canvas size first (create_workspace or set_canvas_size), then the background, then blocks. -- Use html blocks (HTML + CSS) for anything native blocks cannot express: cards, stat tiles, badges, icon lists, charts, testimonials, UI screens. +- For a new design, set the canvas size first (create_workspace or set_canvas_size), then the background, then build it block by block: one add_block per piece (hero, each text, each component), never the whole image in one HTML block. +- Headlines and paragraphs are text blocks. Use an html block for one component native blocks cannot draw (a stat tile, a card, a badge row, a chart), sized to its content, and declare its colors, sizes, radius, shadow and icon as annotated :root variables (see the HTML block rules below). add_block and update_html_block answer with hints when a block breaks these rules. - Find icon names with search_icons; never guess them. Icon packs (.kcomponent files with type: icon-pack) add icons named :. - Check the result with get_canvas_snapshot and fix what fails the checklist below. - Designs can be templates: get_workspace lists project variables, and blocks show {{name}} as their value. To make a new version, change the values with set_variables instead of editing blocks. diff --git a/src/lib/agent/tools/blocks.ts b/src/lib/agent/tools/blocks.ts index 0c31f1c..0805e17 100644 --- a/src/lib/agent/tools/blocks.ts +++ b/src/lib/agent/tools/blocks.ts @@ -11,6 +11,7 @@ import { distributeBlocks, getHtmlBlockCode, requireBlock, + requireWorkspace, reorderBlock, selectBlocks, setHtmlBlockCode, @@ -21,6 +22,7 @@ import { import { languages } from '@/utils/Languages'; import { themes } from '@/utils/PrismThemes'; import { ToolError, defineTool } from './registry'; +import { htmlBlockHints } from './html-hints'; const blockTypes = BLOCK_TYPES.map((spec) => spec.type) as [ string, @@ -131,6 +133,22 @@ export const compactValue = (value: unknown): unknown => { return `${value.slice(0, MAX_VALUE_LENGTH)}… [${value.length} characters]`; }; +/** What is wrong with an HTML block, for the model to fix (see html-hints). */ +const hintsFor = (id: string): string[] => { + const { html, css } = getHtmlBlockCode(id); + const { width, height } = summarizeBlock(requireBlock(id)); + const workspace = requireWorkspace(); + return htmlBlockHints({ + html, + css, + box: { width, height }, + canvas: { + width: parseFloat(workspace.workspaceWidth), + height: parseFloat(workspace.workspaceHeight), + }, + }); +}; + export const compactBlock = (id: string) => { const summary = summarizeBlock(requireBlock(id)); return { @@ -198,7 +216,7 @@ export const addBlockTool = defineTool({ name: 'add_block', title: 'Add block', description: - 'Add a block to the canvas and select it. It is centered on the canvas unless x and y are given. For html blocks, pass the code as properties html and css (and js only if needed), following the HTML block rules of the design guide, and give width and height that fit the content.', + 'Add one block to the canvas and select it. It is centered on the canvas unless x and y are given. Build a design with one call per piece (hero, each text, each component) rather than one block for everything. For html blocks: one component, width and height that fit it, properties html and css (js only if needed), and a :root block of annotated variables (/* @type:color */, number, shadow, icon, boolean) for everything a user may tweak, as in the design guide. The result lists hints when an html block breaks these rules.', input: z.object({ type: z.enum(blockTypes).describe('Block type.'), name: z.string().optional().describe('Layer name.'), @@ -215,7 +233,10 @@ export const addBlockTool = defineTool({ if (await waitForBlock(block.id)) { await new Promise((resolve) => setTimeout(resolve, 80)); } - return compactBlock(block.id); + const summary = compactBlock(block.id); + if (block.type !== 'html') return summary; + const hints = hintsFor(block.id); + return hints.length > 0 ? { ...summary, hints } : summary; }, }); @@ -343,7 +364,7 @@ export const updateHtmlBlockTool = defineTool({ name: 'update_html_block', title: 'Edit HTML block', description: - 'Replace the HTML, CSS and/or JavaScript code of an HTML block. Omitted parts are kept. CSS is scoped to the block (:root means the block). Google fonts named in font-family are loaded automatically; icons come from /* @type:icon */ variables drawn with . JavaScript only runs after setting the allow-scripts property with update_block.', + 'Replace the HTML, CSS and/or JavaScript code of an HTML block. Omitted parts are kept. Keep the annotated :root variables (/* @type:color */ --accent: #f43f5e; …) that make it adjustable from the panel. CSS is scoped to the block (:root means the block). Google fonts named in font-family are loaded automatically; icons come from /* @type:icon */ variables drawn with . JavaScript only runs after setting the allow-scripts property with update_block.', input: z.object({ id: z.string(), html: z.string().optional(), @@ -356,7 +377,10 @@ export const updateHtmlBlockTool = defineTool({ throw new ToolError('Give at least one of html, css or js.'); } setHtmlBlockCode(id, { html, css, js }); - return `Updated the code of ${id}.`; + const hints = hintsFor(id); + return hints.length > 0 + ? { updated: id, hints } + : `Updated the code of ${id}.`; }, }); diff --git a/src/lib/agent/tools/html-hints.test.ts b/src/lib/agent/tools/html-hints.test.ts new file mode 100644 index 0000000..af9c0fe --- /dev/null +++ b/src/lib/agent/tools/html-hints.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import { htmlBlockHints } from './html-hints'; + +const goodCss = `:root { + /* @type:color */ --bg: #16161a; + /* @type:color */ --accent: #fb7185; + /* @type:number min:0 max:48 step:1 unit:px */ --radius: 24px; + /* @type:icon */ --icon: FaRocket; +} +.tile { background: var(--bg); border-radius: var(--radius); }`; + +describe('htmlBlockHints', () => { + it('accepts a small component with annotated variables', () => { + expect( + htmlBlockHints({ + html: '
128k users
', + css: goodCss, + box: { width: 400, height: 200 }, + canvas: { width: 1080, height: 1350 }, + }), + ).toEqual([]); + }); + + it('asks for variables when there are none', () => { + const [hint] = htmlBlockHints({ + html: '
Hi
', + css: '.tile { color: red; }', + }); + expect(hint).toMatch(/declares no variables/); + }); + + it('asks for annotations on unannotated variables', () => { + const [hint] = htmlBlockHints({ + html: '
Hi
', + css: ':root { --accent-color: #ff0000; } div { color: var(--accent-color); }', + }); + expect(hint).toMatch(/no \/\* @type/); + }); + + it('flags a block that is the whole image or all its text', () => { + const hints = htmlBlockHints({ + html: `

Launch week

${'word '.repeat(60)}

`, + css: goodCss, + box: { width: 1080, height: 1350 }, + canvas: { width: 1080, height: 1350 }, + }); + expect(hints.join(' ')).toMatch(/covers 100% of the canvas/); + expect(hints.join(' ')).toMatch(/words of text/); + }); +}); diff --git a/src/lib/agent/tools/html-hints.ts b/src/lib/agent/tools/html-hints.ts new file mode 100644 index 0000000..30290d2 --- /dev/null +++ b/src/lib/agent/tools/html-hints.ts @@ -0,0 +1,70 @@ +import { parseCSSVariables } from '@/lib/blocks-api/css-parser'; + +/** + * Feedback on an HTML block a model just wrote, returned with the tool + * result so the model fixes it in the same turn. HTML blocks are for one + * component each, with its tweakable values declared as panel controls; a + * block that holds the whole image, or declares nothing, is the common way + * models get this wrong. + */ + +const ANNOTATION = + /\/\*\s*@type:\s*(color|number|shadow|boolean|icon|string)\b/g; + +/** Share of the canvas area above which an HTML block is a whole section. */ +const MAX_AREA_SHARE = 0.45; +/** Words of visible text above which the block is doing the text blocks' job. */ +const MAX_WORDS = 40; + +const visibleWords = (html: string): number => + html + .replace(//gi, ' ') + .replace(//gi, ' ') + .replace(//gi, ' ') + .replace(/<[^>]+>/g, ' ') + .replace(/&[a-z#0-9]+;/gi, ' ') + .split(/\s+/) + .filter((word) => /[\p{L}\p{N}]/u.test(word)).length; + +export const htmlBlockHints = (input: { + html: string; + css: string; + box?: { width: number; height: number }; + canvas?: { width: number; height: number }; +}): string[] => { + const hints: string[] = []; + const annotated = input.css.match(ANNOTATION)?.length ?? 0; + const variables = parseCSSVariables(input.css).length; + + if (annotated === 0) { + hints.push( + variables === 0 + ? 'This HTML block declares no variables, so the user cannot adjust it from the panel. Add a :root { } block with its colors, sizes, radius, shadow and icon as annotated variables (e.g. /* @type:color */ --accent: #f43f5e;) and use them with var(--accent) in the CSS. Fix it with update_html_block.' + : 'The :root variables of this HTML block have no /* @type:… */ annotation, so the panel guesses their controls. Annotate each one (/* @type:color */, /* @type:number min:… max:… step:… unit:px */, /* @type:shadow */, /* @type:boolean */, /* @type:icon */) with update_html_block.', + ); + } else if (annotated < 3) { + hints.push( + `Only ${annotated} value${annotated === 1 ? ' is' : 's are'} adjustable. Expose the rest of what a user would tweak (background, text and accent colors, radius, main size, shadow, icon, show/hide flags) as annotated :root variables.`, + ); + } + + if (input.box && input.canvas) { + const share = + (input.box.width * input.box.height) / + Math.max(1, input.canvas.width * input.canvas.height); + if (share > MAX_AREA_SHARE) { + hints.push( + `This HTML block covers ${Math.round(share * 100)}% of the canvas. Build the image block by block instead: the background with set_canvas_background, headlines and paragraphs as text blocks, code as a code block, screenshots in window or phone blocks, and one HTML block per component (a card, a stat, a badge row, a chart), each sized to its content.`, + ); + } + } + + const words = visibleWords(input.html); + if (words > MAX_WORDS) { + hints.push( + `This HTML block holds ${words} words of text. Put headlines and paragraphs in text blocks (editable on the canvas, with their own fonts) and keep only the short labels of the component in the HTML.`, + ); + } + + return hints; +}; diff --git a/src/lib/agent/tools/tools.test.ts b/src/lib/agent/tools/tools.test.ts index 7b37f9c..b4e7fef 100644 --- a/src/lib/agent/tools/tools.test.ts +++ b/src/lib/agent/tools/tools.test.ts @@ -343,6 +343,31 @@ describe('executeTool', () => { expect(placed).toMatchObject({ type: 'image', width: 200, height: 50 }); }); + it('tells the model when an html block misses its variables', async () => { + const bare = JSON.parse( + text( + await run('add_block', { + type: 'html', + width: 1280, + height: 720, + properties: { + html: '
Hi
', + css: '.card { color: red; }', + }, + }), + ), + ); + expect(bare.hints.join(' ')).toMatch(/declares no variables/); + expect(bare.hints.join(' ')).toMatch(/covers 100% of the canvas/); + + const fixed = await run('update_html_block', { + id: bare.id, + css: ':root { /* @type:color */ --a: #111111; /* @type:color */ --b: #222222; /* @type:number min:0 max:40 step:1 unit:px */ --r: 8px; } .card { color: var(--a); }', + }); + expect(text(fixed)).toMatch(/covers 100%/); + expect(text(fixed)).not.toMatch(/declares no variables/); + }); + it('reads the workspace with long values shortened', async () => { const image = `data:image/png;base64,${'A'.repeat(4096)}`; await run('add_block', { type: 'image', properties: { src: image } }); diff --git a/src/lib/blocks/catalog.ts b/src/lib/blocks/catalog.ts index b6ffd67..886c875 100644 --- a/src/lib/blocks/catalog.ts +++ b/src/lib/blocks/catalog.ts @@ -930,7 +930,7 @@ export const BLOCK_TYPES: readonly BlockTypeSpec[] = [ type: 'html', label: 'HTML', description: - 'A custom component in HTML and CSS (JavaScript optional), rendered in a shadow root: cards, stat tiles, badges, icon lists, charts, UI screens, decorative backgrounds. Pass html and css to add_block; edit later with update_html_block. Annotated :root variables (@type:color, number, shadow, boolean, icon) become controls in the panel.', + 'One custom component in HTML and CSS (JavaScript optional), rendered in a shadow root and sized to its content: a stat tile, a card, a badge row, a checklist, a chart in inline SVG, a small UI screen. Not for headlines or paragraphs (text blocks), backgrounds (set_canvas_background) or whole layouts. Pass html and css to add_block; edit later with update_html_block. Declare its colors, sizes, radius, shadow and icon as annotated :root variables (/* @type:color */, /* @type:number min: max: step: unit:px */, /* @type:shadow */, /* @type:icon */, /* @type:boolean */): they become controls in the panel.', defaultSize: size(400, 300), minSize: size(100, 80), maxSize: size(4000, 4000),