diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts index 064a9af6b5..e2274140f7 100644 --- a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts +++ b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts @@ -15,6 +15,20 @@ import { } from "../../../nodeConversions/blockToNode.js"; import { nodeToCustomInlineContent } from "../../../nodeConversions/nodeToBlock.js"; +/** + * Placeholder character inserted into empty inline-content blocks when + * exporting to external HTML. An empty block serializes to an element with no + * children (e.g. `
`), which is dropped when the HTML is parsed back into + * blocks. Filling it with this character keeps the block alive through the + * round trip, and the parser strips the character again so the block ends up + * empty (see `HTMLToBlocks`). + * + * The Unicode object replacement character (U+FFFC) is used because it's a + * reserved placeholder that users don't type, so it can be safely removed on + * import without discarding legitimate content (unlike a non-breaking space). + */ +export const EMPTY_BLOCK_PLACEHOLDER = ""; + function addAttributesAndRemoveClasses(element: HTMLElement) { // Removes all BlockNote specific class names. const className = @@ -265,15 +279,35 @@ function serializeBlock< } } - if (ret.contentDOM && block.content) { - const ic = serializeInlineContentExternalHTML( - editor, - block.content as any, // TODO - serializer, - { ...options, blockType: block.type }, - ); + if (ret.contentDOM) { + if (block.content) { + const ic = serializeInlineContentExternalHTML( + editor, + block.content as any, // TODO + serializer, + { ...options, blockType: block.type }, + ); + + ret.contentDOM.appendChild(ic); + } - ret.contentDOM.appendChild(ic); + // Blocks with empty inline content (e.g. an empty paragraph) serialize to + // an element with no children (e.g. ``), which is ignored when the + // HTML is parsed back into blocks. To make these blocks survive such a + // round trip, we fill their content with a placeholder character that the + // parser strips out again (see `EMPTY_BLOCK_PLACEHOLDER`). + // + // Only applies to blocks that hold inline content: containers (columns, + // tables) fill their `contentDOM` with child blocks later on, and code + // blocks would turn the placeholder into literal content. + const blockNodeType = editor.pmSchema.nodes[block.type as any]; + if ( + blockNodeType?.inlineContent && + !blockNodeType.spec.code && + ret.contentDOM.childNodes.length === 0 + ) { + ret.contentDOM.appendChild(doc.createTextNode(EMPTY_BLOCK_PLACEHOLDER)); + } } let listType = undefined; diff --git a/packages/core/src/api/exporters/markdown/markdownExporter.ts b/packages/core/src/api/exporters/markdown/markdownExporter.ts index 2f73616dc0..98be8b38c6 100644 --- a/packages/core/src/api/exporters/markdown/markdownExporter.ts +++ b/packages/core/src/api/exporters/markdown/markdownExporter.ts @@ -8,11 +8,20 @@ import { StyleSchema, } from "../../../schema/index.js"; import { createExternalHTMLExporter } from "../html/externalHTMLExporter.js"; +import { EMPTY_BLOCK_PLACEHOLDER } from "../html/util/serializeBlocksExternalHTML.js"; import { htmlToMarkdown } from "./htmlToMarkdown.js"; // Needs to be sync because it's used in drag handler event (SideMenuPlugin) export function cleanHTMLToMarkdown(cleanHTMLString: string) { - return htmlToMarkdown(cleanHTMLString); + // The external HTML exporter fills empty inline-content blocks with a + // placeholder character so they survive an HTML round trip (see + // `EMPTY_BLOCK_PLACEHOLDER`). Markdown has no need for that placeholder, so we + // remove it to avoid it showing up as a stray character in the output. + const withoutPlaceholder = cleanHTMLString + .split(EMPTY_BLOCK_PLACEHOLDER) + .join(""); + + return htmlToMarkdown(withoutPlaceholder); } export function blocksToMarkdown< diff --git a/packages/core/src/api/parsers/html/parseHTML.ts b/packages/core/src/api/parsers/html/parseHTML.ts index 808dd20137..86243d5be9 100644 --- a/packages/core/src/api/parsers/html/parseHTML.ts +++ b/packages/core/src/api/parsers/html/parseHTML.ts @@ -6,10 +6,51 @@ import { } from "../../../schema/index.js"; import { Block } from "../../../blocks/defaultBlocks.js"; +import { EMPTY_BLOCK_PLACEHOLDER } from "../../exporters/html/util/serializeBlocksExternalHTML.js"; import { nodeToBlock } from "../../nodeConversions/nodeToBlock.js"; import { nestedListsToBlockNoteStructure } from "./util/nestedLists.js"; import { preprocessHTMLWhitespace } from "./util/normalizeWhitespace.js"; +/** + * Removes the placeholder character that the external HTML exporter inserts + * into empty inline-content blocks (see `EMPTY_BLOCK_PLACEHOLDER`). The + * placeholder keeps such blocks from being dropped while the HTML is parsed; + * stripping it here lets the block round trip back to genuinely empty content. + */ +function stripEmptyBlockPlaceholder(content: any[]): any[] { + const stripped: any[] = []; + + for (const item of content) { + if (item.type === "text") { + const text = item.text.split(EMPTY_BLOCK_PLACEHOLDER).join(""); + if (text.length > 0) { + stripped.push({ ...item, text }); + } + } else if (Array.isArray(item.content)) { + // Links and custom inline content that hold nested styled text. + stripped.push({ + ...item, + content: stripEmptyBlockPlaceholder(item.content), + }); + } else { + stripped.push(item); + } + } + + return stripped; +} + +function stripEmptyBlockPlaceholderFromBlocks(blocks: Block block (wrapped with ) to specify the language.\\n\\nIf the user requests updates to the document, use the \\\"applyDocumentOperations\\\" tool to update the document.\\n---\\nIF there is no selection active in the latest state, first, determine what part of the document the user is talking about. You SHOULD probably take cursor info into account if needed.\\n EXAMPLE: if user says \\\"below\\\" (without pointing to a specific part of the document) he / she probably indicates the block(s) after the cursor. \\n EXAMPLE: If you want to insert content AT the cursor position (UNLESS indicated otherwise by the user), then you need `referenceId` to point to the block before the cursor with position `after` (or block below and `before`\\n---\\n \"}],\"messages\":[{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"There is no active selection. This is the latest state of the document (ignore previous documents, you MUST issue operations against this latest version of the document). \\nThe cursor is BETWEEN two blocks as indicated by cursor: true.\\nBecause the document is empty, YOU MUST first update the empty block before adding new blocks.\"},{\"type\":\"text\",\"text\":\"[{\\\"id\\\":\\\"ref1$\\\",\\\"block\\\":\\\"\\\"},{\\\"cursor\\\":true}]\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"write a new paragraph with the text 'You look great today!'\"}]}],\"tools\":[{\"name\":\"applyDocumentOperations\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"operations\":{\"type\":\"array\",\"items\":{\"anyOf\":[{\"type\":\"object\",\"description\":\"Update a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"update\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to update\"},\"block\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single HTML element)\"}},\"required\":[\"type\",\"id\",\"block\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Insert new blocks\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"add\"]},\"referenceId\":{\"type\":\"string\",\"description\":\"MUST be an id of a block in the document\"},\"position\":{\"type\":\"string\",\"enum\":[\"before\",\"after\"],\"description\":\"`after` to add blocks AFTER (below) the block with `referenceId`, `before` to add the block BEFORE (above)\"},\"blocks\":{\"items\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single, VALID HTML element)\"},\"type\":\"array\"}},\"required\":[\"type\",\"referenceId\",\"position\",\"blocks\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Delete a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"delete\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to delete\"}},\"required\":[\"type\",\"id\"],\"additionalProperties\":false}]}}},\"additionalProperties\":false,\"required\":[\"operations\"]}}],\"tool_choice\":{\"type\":\"any\"},\"stream\":true}",
- "headers": [],
+ "body": "{\"model\":\"claude-3-7-sonnet-latest\",\"max_tokens\":64000,\"system\":[{\"type\":\"text\",\"text\":\"You're manipulating a text document using HTML blocks. \\nMake sure to follow the json schema provided. When referencing ids they MUST be EXACTLY the same (including the trailing $). \\nList items are 1 block with 1 list item each, so block content `- item1
` is valid, but `- item1
- item2
` is invalid. We'll merge them automatically.\\nFor code blocks, you can use the `data-language` attribute on a block (wrapped with ) to specify the language.\\n\\nIf the user requests updates to the document, use the \\\"applyDocumentOperations\\\" tool to update the document.\\n---\\nIF there is no selection active in the latest state, first, determine what part of the document the user is talking about. You SHOULD probably take cursor info into account if needed.\\n EXAMPLE: if user says \\\"below\\\" (without pointing to a specific part of the document) he / she probably indicates the block(s) after the cursor. \\n EXAMPLE: If you want to insert content AT the cursor position (UNLESS indicated otherwise by the user), then you need `referenceId` to point to the block before the cursor with position `after` (or block below and `before`\\n---\\n \"}],\"messages\":[{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"There is no active selection. This is the latest state of the document (ignore previous documents, you MUST issue operations against this latest version of the document). \\nThe cursor is BETWEEN two blocks as indicated by cursor: true.\\nBecause the document is empty, YOU MUST first update the empty block before adding new blocks.\"},{\"type\":\"text\",\"text\":\"[{\\\"id\\\":\\\"ref1$\\\",\\\"block\\\":\\\"\ufffc
\\\"},{\\\"cursor\\\":true}]\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"write a new paragraph with the text 'You look great today!'\"}]}],\"tools\":[{\"name\":\"applyDocumentOperations\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"operations\":{\"type\":\"array\",\"items\":{\"anyOf\":[{\"type\":\"object\",\"description\":\"Update a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"update\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to update\"},\"block\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single HTML element)\"}},\"required\":[\"type\",\"id\",\"block\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Insert new blocks\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"add\"]},\"referenceId\":{\"type\":\"string\",\"description\":\"MUST be an id of a block in the document\"},\"position\":{\"type\":\"string\",\"enum\":[\"before\",\"after\"],\"description\":\"`after` to add blocks AFTER (below) the block with `referenceId`, `before` to add the block BEFORE (above)\"},\"blocks\":{\"items\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single, VALID HTML element)\"},\"type\":\"array\"}},\"required\":[\"type\",\"referenceId\",\"position\",\"blocks\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Delete a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"delete\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to delete\"}},\"required\":[\"type\",\"id\"],\"additionalProperties\":false}]}}},\"additionalProperties\":false,\"required\":[\"operations\"]}}],\"tool_choice\":{\"type\":\"any\"},\"stream\":true}",
+ "headers": [
+ [
+ "anthropic-beta",
+ "fine-grained-tool-streaming-2025-05-14"
+ ],
+ [
+ "anthropic-version",
+ "2023-06-01"
+ ],
+ [
+ "content-type",
+ "application/json"
+ ],
+ [
+ "user-agent",
+ "ai-sdk/anthropic/3.0.2 ai-sdk/provider-utils/4.0.2 runtime/browser"
+ ],
+ [
+ "x-api-key",
+ "not-available-in-ci"
+ ]
+ ],
"cookies": []
},
"response": {
@@ -12,4 +33,4 @@
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-3-7-sonnet-20250219\",\"id\":\"msg_016r1xY1xw9dBXbuUikPJbe6\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":1148,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":11,\"service_tier\":\"standard\"}}}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_019x69kHjsPjYsLZ1suXT9QU\",\"name\":\"applyDocumentOperations\",\"input\":{}} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"operations\\\": [\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\n {\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\n \\\"type\\\":\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" \\\"update\\\",\\n \"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"id\\\": \\\"ref\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"1$\\\",\\n \"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"block\\\": \\\"You look great today\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"!
\\\"\\n \"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"}\\n]\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null},\"usage\":{\"input_tokens\":1148,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":82} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
"headers": []
}
-}
\ No newline at end of file
+}
diff --git a/packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Add/__msw_snapshots__/groq.chat/llama-3.3-70b-versatile (streaming)/add a new paragraph (empty doc)_1_3f2fffcfa733575ca2a6c4f9d1a8f615.json b/packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Add/__msw_snapshots__/groq.chat/llama-3.3-70b-versatile (streaming)/add a new paragraph (empty doc)_1_965ae67e6f11e3d04bb0403636e829f7.json
similarity index 63%
rename from packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Add/__msw_snapshots__/groq.chat/llama-3.3-70b-versatile (streaming)/add a new paragraph (empty doc)_1_3f2fffcfa733575ca2a6c4f9d1a8f615.json
rename to packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Add/__msw_snapshots__/groq.chat/llama-3.3-70b-versatile (streaming)/add a new paragraph (empty doc)_1_965ae67e6f11e3d04bb0403636e829f7.json
index cb1534cb08..64dcbd017f 100644
--- a/packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Add/__msw_snapshots__/groq.chat/llama-3.3-70b-versatile (streaming)/add a new paragraph (empty doc)_1_3f2fffcfa733575ca2a6c4f9d1a8f615.json
+++ b/packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Add/__msw_snapshots__/groq.chat/llama-3.3-70b-versatile (streaming)/add a new paragraph (empty doc)_1_965ae67e6f11e3d04bb0403636e829f7.json
@@ -2,8 +2,21 @@
"request": {
"method": "POST",
"url": "https://api.groq.com/openai/v1/chat/completions",
- "body": "{\"model\":\"llama-3.3-70b-versatile\",\"messages\":[{\"role\":\"system\",\"content\":\"You're manipulating a text document using HTML blocks. \\nMake sure to follow the json schema provided. When referencing ids they MUST be EXACTLY the same (including the trailing $). \\nList items are 1 block with 1 list item each, so block content `- item1
` is valid, but `- item1
- item2
` is invalid. We'll merge them automatically.\\nFor code blocks, you can use the `data-language` attribute on a block (wrapped with ) to specify the language.\\n\\nIf the user requests updates to the document, use the \\\"applyDocumentOperations\\\" tool to update the document.\\n---\\nIF there is no selection active in the latest state, first, determine what part of the document the user is talking about. You SHOULD probably take cursor info into account if needed.\\n EXAMPLE: if user says \\\"below\\\" (without pointing to a specific part of the document) he / she probably indicates the block(s) after the cursor. \\n EXAMPLE: If you want to insert content AT the cursor position (UNLESS indicated otherwise by the user), then you need `referenceId` to point to the block before the cursor with position `after` (or block below and `before`\\n---\\n \"},{\"role\":\"assistant\",\"content\":\"There is no active selection. This is the latest state of the document (ignore previous documents, you MUST issue operations against this latest version of the document). \\nThe cursor is BETWEEN two blocks as indicated by cursor: true.\\nBecause the document is empty, YOU MUST first update the empty block before adding new blocks.[{\\\"id\\\":\\\"ref1$\\\",\\\"block\\\":\\\"\\\"},{\\\"cursor\\\":true}]\"},{\"role\":\"user\",\"content\":\"write a new paragraph with the text 'You look great today!'\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"applyDocumentOperations\",\"parameters\":{\"type\":\"object\",\"properties\":{\"operations\":{\"type\":\"array\",\"items\":{\"anyOf\":[{\"type\":\"object\",\"description\":\"Update a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"update\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to update\"},\"block\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single HTML element)\"}},\"required\":[\"type\",\"id\",\"block\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Insert new blocks\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"add\"]},\"referenceId\":{\"type\":\"string\",\"description\":\"MUST be an id of a block in the document\"},\"position\":{\"type\":\"string\",\"enum\":[\"before\",\"after\"],\"description\":\"`after` to add blocks AFTER (below) the block with `referenceId`, `before` to add the block BEFORE (above)\"},\"blocks\":{\"items\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single, VALID HTML element)\"},\"type\":\"array\"}},\"required\":[\"type\",\"referenceId\",\"position\",\"blocks\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Delete a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"delete\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to delete\"}},\"required\":[\"type\",\"id\"],\"additionalProperties\":false}]}}},\"additionalProperties\":false,\"required\":[\"operations\"]}}}],\"tool_choice\":\"required\",\"stream\":true}",
- "headers": [],
+ "body": "{\"model\":\"llama-3.3-70b-versatile\",\"messages\":[{\"role\":\"system\",\"content\":\"You're manipulating a text document using HTML blocks. \\nMake sure to follow the json schema provided. When referencing ids they MUST be EXACTLY the same (including the trailing $). \\nList items are 1 block with 1 list item each, so block content `- item1
` is valid, but `- item1
- item2
` is invalid. We'll merge them automatically.\\nFor code blocks, you can use the `data-language` attribute on a block (wrapped with ) to specify the language.\\n\\nIf the user requests updates to the document, use the \\\"applyDocumentOperations\\\" tool to update the document.\\n---\\nIF there is no selection active in the latest state, first, determine what part of the document the user is talking about. You SHOULD probably take cursor info into account if needed.\\n EXAMPLE: if user says \\\"below\\\" (without pointing to a specific part of the document) he / she probably indicates the block(s) after the cursor. \\n EXAMPLE: If you want to insert content AT the cursor position (UNLESS indicated otherwise by the user), then you need `referenceId` to point to the block before the cursor with position `after` (or block below and `before`\\n---\\n \"},{\"role\":\"assistant\",\"content\":\"There is no active selection. This is the latest state of the document (ignore previous documents, you MUST issue operations against this latest version of the document). \\nThe cursor is BETWEEN two blocks as indicated by cursor: true.\\nBecause the document is empty, YOU MUST first update the empty block before adding new blocks.[{\\\"id\\\":\\\"ref1$\\\",\\\"block\\\":\\\"\ufffc
\\\"},{\\\"cursor\\\":true}]\"},{\"role\":\"user\",\"content\":\"write a new paragraph with the text 'You look great today!'\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"applyDocumentOperations\",\"parameters\":{\"type\":\"object\",\"properties\":{\"operations\":{\"type\":\"array\",\"items\":{\"anyOf\":[{\"type\":\"object\",\"description\":\"Update a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"update\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to update\"},\"block\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single HTML element)\"}},\"required\":[\"type\",\"id\",\"block\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Insert new blocks\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"add\"]},\"referenceId\":{\"type\":\"string\",\"description\":\"MUST be an id of a block in the document\"},\"position\":{\"type\":\"string\",\"enum\":[\"before\",\"after\"],\"description\":\"`after` to add blocks AFTER (below) the block with `referenceId`, `before` to add the block BEFORE (above)\"},\"blocks\":{\"items\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single, VALID HTML element)\"},\"type\":\"array\"}},\"required\":[\"type\",\"referenceId\",\"position\",\"blocks\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Delete a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"delete\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to delete\"}},\"required\":[\"type\",\"id\"],\"additionalProperties\":false}]}}},\"additionalProperties\":false,\"required\":[\"operations\"]}}}],\"tool_choice\":\"required\",\"stream\":true}",
+ "headers": [
+ [
+ "authorization",
+ "Bearer not-available-in-ci"
+ ],
+ [
+ "content-type",
+ "application/json"
+ ],
+ [
+ "user-agent",
+ "ai-sdk/groq/3.0.2 ai-sdk/provider-utils/4.0.2 runtime/browser"
+ ]
+ ],
"cookies": []
},
"response": {
@@ -12,4 +25,4 @@
"body": "data: {\"id\":\"chatcmpl-82f7ef27-ca01-433d-a94c-90d790674b93\",\"object\":\"chat.completion.chunk\",\"created\":1767361375,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_f8b414701e\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null},\"logprobs\":null,\"finish_reason\":null}],\"x_groq\":{\"id\":\"req_01kdzf4v5rfnsav2kd19rcrfz6\",\"seed\":2140344246}}\n\ndata: {\"id\":\"chatcmpl-82f7ef27-ca01-433d-a94c-90d790674b93\",\"object\":\"chat.completion.chunk\",\"created\":1767361375,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_f8b414701e\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"id\":\"khb558s13\",\"type\":\"function\",\"function\":{\"name\":\"applyDocumentOperations\",\"arguments\":\"{\\\"operations\\\":[{\\\"block\\\":\\\"\\\\u003cp\\\\u003eYou look great today!\\\\u003c/p\\\\u003e\\\",\\\"id\\\":\\\"ref1$\\\",\\\"type\\\":\\\"update\\\"}]}\"},\"index\":0}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-82f7ef27-ca01-433d-a94c-90d790674b93\",\"object\":\"chat.completion.chunk\",\"created\":1767361375,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_f8b414701e\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"tool_calls\"}],\"x_groq\":{\"id\":\"req_01kdzf4v5rfnsav2kd19rcrfz6\",\"usage\":{\"queue_time\":0.047394491,\"prompt_tokens\":850,\"prompt_time\":0.046081609,\"completion_tokens\":32,\"completion_time\":0.09129475,\"total_tokens\":882,\"total_time\":0.137376359}},\"usage\":{\"queue_time\":0.047394491,\"prompt_tokens\":850,\"prompt_time\":0.046081609,\"completion_tokens\":32,\"completion_time\":0.09129475,\"total_tokens\":882,\"total_time\":0.137376359}}\n\ndata: [DONE]\n\n",
"headers": []
}
-}
\ No newline at end of file
+}
diff --git a/packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Add/__msw_snapshots__/openai.responses/gpt-4o-2024-08-06 (streaming)/add a new paragraph (empty doc)_1_100f09592ece8a32c33dff2dc612c649.json b/packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Add/__msw_snapshots__/openai.responses/gpt-4o-2024-08-06 (streaming)/add a new paragraph (empty doc)_1_f197d9625d431cd2edeab6e1461b332f.json
similarity index 89%
rename from packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Add/__msw_snapshots__/openai.responses/gpt-4o-2024-08-06 (streaming)/add a new paragraph (empty doc)_1_100f09592ece8a32c33dff2dc612c649.json
rename to packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Add/__msw_snapshots__/openai.responses/gpt-4o-2024-08-06 (streaming)/add a new paragraph (empty doc)_1_f197d9625d431cd2edeab6e1461b332f.json
index 58b410cc97..68bbf21dff 100644
--- a/packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Add/__msw_snapshots__/openai.responses/gpt-4o-2024-08-06 (streaming)/add a new paragraph (empty doc)_1_100f09592ece8a32c33dff2dc612c649.json
+++ b/packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Add/__msw_snapshots__/openai.responses/gpt-4o-2024-08-06 (streaming)/add a new paragraph (empty doc)_1_f197d9625d431cd2edeab6e1461b332f.json
@@ -2,8 +2,21 @@
"request": {
"method": "POST",
"url": "https://api.openai.com/v1/responses",
- "body": "{\"model\":\"gpt-4o-2024-08-06\",\"input\":[{\"role\":\"system\",\"content\":\"You're manipulating a text document using HTML blocks. \\nMake sure to follow the json schema provided. When referencing ids they MUST be EXACTLY the same (including the trailing $). \\nList items are 1 block with 1 list item each, so block content `- item1
` is valid, but `- item1
- item2
` is invalid. We'll merge them automatically.\\nFor code blocks, you can use the `data-language` attribute on a block (wrapped with ) to specify the language.\\n\\nIf the user requests updates to the document, use the \\\"applyDocumentOperations\\\" tool to update the document.\\n---\\nIF there is no selection active in the latest state, first, determine what part of the document the user is talking about. You SHOULD probably take cursor info into account if needed.\\n EXAMPLE: if user says \\\"below\\\" (without pointing to a specific part of the document) he / she probably indicates the block(s) after the cursor. \\n EXAMPLE: If you want to insert content AT the cursor position (UNLESS indicated otherwise by the user), then you need `referenceId` to point to the block before the cursor with position `after` (or block below and `before`\\n---\\n \"},{\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"There is no active selection. This is the latest state of the document (ignore previous documents, you MUST issue operations against this latest version of the document). \\nThe cursor is BETWEEN two blocks as indicated by cursor: true.\\nBecause the document is empty, YOU MUST first update the empty block before adding new blocks.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"[{\\\"id\\\":\\\"ref1$\\\",\\\"block\\\":\\\"\\\"},{\\\"cursor\\\":true}]\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"write a new paragraph with the text 'You look great today!'\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"applyDocumentOperations\",\"parameters\":{\"type\":\"object\",\"properties\":{\"operations\":{\"type\":\"array\",\"items\":{\"anyOf\":[{\"type\":\"object\",\"description\":\"Update a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"update\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to update\"},\"block\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single HTML element)\"}},\"required\":[\"type\",\"id\",\"block\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Insert new blocks\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"add\"]},\"referenceId\":{\"type\":\"string\",\"description\":\"MUST be an id of a block in the document\"},\"position\":{\"type\":\"string\",\"enum\":[\"before\",\"after\"],\"description\":\"`after` to add blocks AFTER (below) the block with `referenceId`, `before` to add the block BEFORE (above)\"},\"blocks\":{\"items\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single, VALID HTML element)\"},\"type\":\"array\"}},\"required\":[\"type\",\"referenceId\",\"position\",\"blocks\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Delete a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"delete\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to delete\"}},\"required\":[\"type\",\"id\"],\"additionalProperties\":false}]}}},\"additionalProperties\":false,\"required\":[\"operations\"]}}],\"tool_choice\":\"required\",\"stream\":true}",
- "headers": [],
+ "body": "{\"model\":\"gpt-4o-2024-08-06\",\"input\":[{\"role\":\"system\",\"content\":\"You're manipulating a text document using HTML blocks. \\nMake sure to follow the json schema provided. When referencing ids they MUST be EXACTLY the same (including the trailing $). \\nList items are 1 block with 1 list item each, so block content `- item1
` is valid, but `- item1
- item2
` is invalid. We'll merge them automatically.\\nFor code blocks, you can use the `data-language` attribute on a block (wrapped with ) to specify the language.\\n\\nIf the user requests updates to the document, use the \\\"applyDocumentOperations\\\" tool to update the document.\\n---\\nIF there is no selection active in the latest state, first, determine what part of the document the user is talking about. You SHOULD probably take cursor info into account if needed.\\n EXAMPLE: if user says \\\"below\\\" (without pointing to a specific part of the document) he / she probably indicates the block(s) after the cursor. \\n EXAMPLE: If you want to insert content AT the cursor position (UNLESS indicated otherwise by the user), then you need `referenceId` to point to the block before the cursor with position `after` (or block below and `before`\\n---\\n \"},{\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"There is no active selection. This is the latest state of the document (ignore previous documents, you MUST issue operations against this latest version of the document). \\nThe cursor is BETWEEN two blocks as indicated by cursor: true.\\nBecause the document is empty, YOU MUST first update the empty block before adding new blocks.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"[{\\\"id\\\":\\\"ref1$\\\",\\\"block\\\":\\\"\ufffc
\\\"},{\\\"cursor\\\":true}]\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"write a new paragraph with the text 'You look great today!'\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"applyDocumentOperations\",\"parameters\":{\"type\":\"object\",\"properties\":{\"operations\":{\"type\":\"array\",\"items\":{\"anyOf\":[{\"type\":\"object\",\"description\":\"Update a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"update\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to update\"},\"block\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single HTML element)\"}},\"required\":[\"type\",\"id\",\"block\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Insert new blocks\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"add\"]},\"referenceId\":{\"type\":\"string\",\"description\":\"MUST be an id of a block in the document\"},\"position\":{\"type\":\"string\",\"enum\":[\"before\",\"after\"],\"description\":\"`after` to add blocks AFTER (below) the block with `referenceId`, `before` to add the block BEFORE (above)\"},\"blocks\":{\"items\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single, VALID HTML element)\"},\"type\":\"array\"}},\"required\":[\"type\",\"referenceId\",\"position\",\"blocks\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Delete a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"delete\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to delete\"}},\"required\":[\"type\",\"id\"],\"additionalProperties\":false}]}}},\"additionalProperties\":false,\"required\":[\"operations\"]}}],\"tool_choice\":\"required\",\"stream\":true}",
+ "headers": [
+ [
+ "authorization",
+ "Bearer not-available-in-ci"
+ ],
+ [
+ "content-type",
+ "application/json"
+ ],
+ [
+ "user-agent",
+ "ai-sdk/openai/3.0.2 ai-sdk/provider-utils/4.0.2 runtime/browser"
+ ]
+ ],
"cookies": []
},
"response": {
@@ -12,4 +25,4 @@
"body": "event: response.created\ndata: {\"type\":\"response.created\",\"sequence_number\":0,\"response\":{\"id\":\"resp_09fb538de8ef257d006957cb43a23c8197a0af82b58ef83d2c\",\"object\":\"response\",\"created_at\":1767361347,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-2024-08-06\",\"output\":[],\"parallel_tool_calls\":true,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":null,\"reasoning\":{\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"required\",\"tools\":[{\"type\":\"function\",\"description\":null,\"name\":\"applyDocumentOperations\",\"parameters\":{\"type\":\"object\",\"properties\":{\"operations\":{\"type\":\"array\",\"items\":{\"anyOf\":[{\"type\":\"object\",\"description\":\"Update a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"update\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to update\"},\"block\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single HTML element)\"}},\"required\":[\"type\",\"id\",\"block\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Insert new blocks\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"add\"]},\"referenceId\":{\"type\":\"string\",\"description\":\"MUST be an id of a block in the document\"},\"position\":{\"type\":\"string\",\"enum\":[\"before\",\"after\"],\"description\":\"`after` to add blocks AFTER (below) the block with `referenceId`, `before` to add the block BEFORE (above)\"},\"blocks\":{\"items\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single, VALID HTML element)\"},\"type\":\"array\"}},\"required\":[\"type\",\"referenceId\",\"position\",\"blocks\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Delete a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"delete\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to delete\"}},\"required\":[\"type\",\"id\"],\"additionalProperties\":false}]}}},\"additionalProperties\":false,\"required\":[\"operations\"]},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}}}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"sequence_number\":1,\"response\":{\"id\":\"resp_09fb538de8ef257d006957cb43a23c8197a0af82b58ef83d2c\",\"object\":\"response\",\"created_at\":1767361347,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-2024-08-06\",\"output\":[],\"parallel_tool_calls\":true,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":null,\"reasoning\":{\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"required\",\"tools\":[{\"type\":\"function\",\"description\":null,\"name\":\"applyDocumentOperations\",\"parameters\":{\"type\":\"object\",\"properties\":{\"operations\":{\"type\":\"array\",\"items\":{\"anyOf\":[{\"type\":\"object\",\"description\":\"Update a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"update\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to update\"},\"block\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single HTML element)\"}},\"required\":[\"type\",\"id\",\"block\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Insert new blocks\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"add\"]},\"referenceId\":{\"type\":\"string\",\"description\":\"MUST be an id of a block in the document\"},\"position\":{\"type\":\"string\",\"enum\":[\"before\",\"after\"],\"description\":\"`after` to add blocks AFTER (below) the block with `referenceId`, `before` to add the block BEFORE (above)\"},\"blocks\":{\"items\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single, VALID HTML element)\"},\"type\":\"array\"}},\"required\":[\"type\",\"referenceId\",\"position\",\"blocks\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Delete a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"delete\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to delete\"}},\"required\":[\"type\",\"id\"],\"additionalProperties\":false}]}}},\"additionalProperties\":false,\"required\":[\"operations\"]},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}}}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"sequence_number\":2,\"output_index\":0,\"item\":{\"id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_QUjPke6rjKsntZkyRUmUcnA3\",\"name\":\"applyDocumentOperations\"}}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":3,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"{\\\"\",\"obfuscation\":\"aS3xdKHRBUOc9N\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":4,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"operations\",\"obfuscation\":\"WcqSbl\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":5,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"\\\":[\",\"obfuscation\":\"DuRzGywwHgber\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":6,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"{\\\"\",\"obfuscation\":\"DbwB6sDiazPOsP\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":7,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"type\",\"obfuscation\":\"1gC70wKL4sCr\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":8,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"\\\":\\\"\",\"obfuscation\":\"hcmK0SFqLkXe6\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":9,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"update\",\"obfuscation\":\"9J8qcUZ4rX\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":10,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"\\\",\\\"\",\"obfuscation\":\"wqO7moJuHT7a2\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":11,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"id\",\"obfuscation\":\"qiNOSv7SF54UIG\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":12,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"\\\":\\\"\",\"obfuscation\":\"Y7cglR5UZBuYk\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":13,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"ref\",\"obfuscation\":\"uxgZ5xLNEN5ON\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":14,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"1\",\"obfuscation\":\"5RGKDjte6kjyR8y\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":15,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"$\",\"obfuscation\":\"zh2iMhysH0xqrhF\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":16,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"\\\",\\\"\",\"obfuscation\":\"FGeTYH6lFCZEF\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":17,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"block\",\"obfuscation\":\"PWIgAVM8KvX\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":18,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"\\\":\\\"\",\"obfuscation\":\"xxR9beQFDFRKI\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":19,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"<\",\"obfuscation\":\"sLUveO94eZaeE5W\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":20,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"p\",\"obfuscation\":\"xkxOPo6jK7qjAlA\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":21,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\">You\",\"obfuscation\":\"oGyc4O6RpYq9\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":22,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\" look\",\"obfuscation\":\"oesTOJDwtde\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":23,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\" great\",\"obfuscation\":\"rqNFQSzcCy\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":24,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\" today\",\"obfuscation\":\"0h7iTFIA3M\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":25,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"!\",\"obfuscation\":\"jLkMJzX9tctFW\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":26,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"p\",\"obfuscation\":\"nRiJkefBYDtJcTn\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":27,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\">\",\"obfuscation\":\"Av4SiyCjLMhzttX\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":28,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"\\\"}\",\"obfuscation\":\"ejzrizOPxMsuIn\"}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"sequence_number\":29,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"delta\":\"]}\",\"obfuscation\":\"9SBifFIzi75p4E\"}\n\nevent: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"sequence_number\":30,\"item_id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"output_index\":0,\"arguments\":\"{\\\"operations\\\":[{\\\"type\\\":\\\"update\\\",\\\"id\\\":\\\"ref1$\\\",\\\"block\\\":\\\"You look great today!
\\\"}]}\"}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"sequence_number\":31,\"output_index\":0,\"item\":{\"id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"operations\\\":[{\\\"type\\\":\\\"update\\\",\\\"id\\\":\\\"ref1$\\\",\\\"block\\\":\\\"You look great today!
\\\"}]}\",\"call_id\":\"call_QUjPke6rjKsntZkyRUmUcnA3\",\"name\":\"applyDocumentOperations\"}}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"sequence_number\":32,\"response\":{\"id\":\"resp_09fb538de8ef257d006957cb43a23c8197a0af82b58ef83d2c\",\"object\":\"response\",\"created_at\":1767361347,\"status\":\"completed\",\"background\":false,\"completed_at\":1767361348,\"error\":null,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-2024-08-06\",\"output\":[{\"id\":\"fc_09fb538de8ef257d006957cb43efe48197a17343e61f430f15\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"operations\\\":[{\\\"type\\\":\\\"update\\\",\\\"id\\\":\\\"ref1$\\\",\\\"block\\\":\\\"You look great today!
\\\"}]}\",\"call_id\":\"call_QUjPke6rjKsntZkyRUmUcnA3\",\"name\":\"applyDocumentOperations\"}],\"parallel_tool_calls\":true,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":null,\"reasoning\":{\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":true,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"required\",\"tools\":[{\"type\":\"function\",\"description\":null,\"name\":\"applyDocumentOperations\",\"parameters\":{\"type\":\"object\",\"properties\":{\"operations\":{\"type\":\"array\",\"items\":{\"anyOf\":[{\"type\":\"object\",\"description\":\"Update a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"update\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to update\"},\"block\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single HTML element)\"}},\"required\":[\"type\",\"id\",\"block\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Insert new blocks\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"add\"]},\"referenceId\":{\"type\":\"string\",\"description\":\"MUST be an id of a block in the document\"},\"position\":{\"type\":\"string\",\"enum\":[\"before\",\"after\"],\"description\":\"`after` to add blocks AFTER (below) the block with `referenceId`, `before` to add the block BEFORE (above)\"},\"blocks\":{\"items\":{\"type\":\"string\",\"description\":\"html of block (MUST be a single, VALID HTML element)\"},\"type\":\"array\"}},\"required\":[\"type\",\"referenceId\",\"position\",\"blocks\"],\"additionalProperties\":false},{\"type\":\"object\",\"description\":\"Delete a block\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"delete\"]},\"id\":{\"type\":\"string\",\"description\":\"id of block to delete\"}},\"required\":[\"type\",\"id\"],\"additionalProperties\":false}]}}},\"additionalProperties\":false,\"required\":[\"operations\"]},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":572,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":28,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":600},\"user\":null,\"metadata\":{}}}\n\n",
"headers": []
}
-}
\ No newline at end of file
+}
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/emptyBetween.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/emptyBetween.html
new file mode 100644
index 0000000000..65c75517ae
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/emptyBetween.html
@@ -0,0 +1,25 @@
+
+
+
+
+ Before
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ After
+
+
+
+
\ No newline at end of file
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/complex/misc.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/complex/misc.html
index 5dd72c369b..921a895594 100644
--- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/complex/misc.html
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/complex/misc.html
@@ -13,6 +13,6 @@
-
+ 
\ No newline at end of file
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/empty.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/empty.html
index 540135ad6a..52705e4f81 100644
--- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/empty.html
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/empty.html
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/emptyBetween.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/emptyBetween.html
new file mode 100644
index 0000000000..d7c238bb58
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/emptyBetween.html
@@ -0,0 +1,3 @@
+Before
+
+After
\ No newline at end of file
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/paragraph/emptyBetween.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/paragraph/emptyBetween.md
new file mode 100644
index 0000000000..4de6aa9b79
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/paragraph/emptyBetween.md
@@ -0,0 +1,5 @@
+Before
+
+
+
+After
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/paragraph/emptyBetween.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/paragraph/emptyBetween.json
new file mode 100644
index 0000000000..5807d53144
--- /dev/null
+++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/paragraph/emptyBetween.json
@@ -0,0 +1,62 @@
+[
+ {
+ "attrs": {
+ "id": "1",
+ },
+ "content": [
+ {
+ "attrs": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "content": [
+ {
+ "text": "Before",
+ "type": "text",
+ },
+ ],
+ "type": "paragraph",
+ },
+ ],
+ "type": "blockContainer",
+ },
+ {
+ "attrs": {
+ "id": "2",
+ },
+ "content": [
+ {
+ "attrs": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ ],
+ "type": "blockContainer",
+ },
+ {
+ "attrs": {
+ "id": "3",
+ },
+ "content": [
+ {
+ "attrs": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "content": [
+ {
+ "text": "After",
+ "type": "text",
+ },
+ ],
+ "type": "paragraph",
+ },
+ ],
+ "type": "blockContainer",
+ },
+]
\ No newline at end of file
diff --git a/tests/src/unit/core/formatConversion/export/exportTestInstances.ts b/tests/src/unit/core/formatConversion/export/exportTestInstances.ts
index bd8cb77493..1d901c81a9 100644
--- a/tests/src/unit/core/formatConversion/export/exportTestInstances.ts
+++ b/tests/src/unit/core/formatConversion/export/exportTestInstances.ts
@@ -29,6 +29,25 @@ export const exportTestInstancesBlockNoteHTML: TestInstance<
},
executeTest: testExportBlockNoteHTML,
},
+ {
+ testCase: {
+ name: "paragraph/emptyBetween",
+ content: [
+ {
+ type: "paragraph",
+ content: "Before",
+ },
+ {
+ type: "paragraph",
+ },
+ {
+ type: "paragraph",
+ content: "After",
+ },
+ ],
+ },
+ executeTest: testExportBlockNoteHTML,
+ },
{
testCase: {
name: "paragraph/basic",