(frontend) integrate new Blocknote AI feature

We integrate the new Blocknote AI feature
into Docs, enhancing the document editing experience
with AI capabilities.
This commit is contained in:
Anthony LC
2026-01-28 14:39:48 +01:00
parent 09438a8941
commit 1a022450c6
22 changed files with 1028 additions and 21 deletions

View File

@@ -8,6 +8,7 @@ and this project adheres to
### Added
- ✨(frontend) integrate new Blocknote AI feature #1016
- 👷(docker) add arm64 platform support for image builds
- ✨(tracking) add UTM parameters to shared document links
- ✨(frontend) add floating bar with leftpanel collapse button #1876

View File

@@ -93,9 +93,7 @@ test.describe('Config', () => {
expect(
await page.locator('button[data-test="convertMarkdown"]').count(),
).toBe(1);
expect(await page.locator('button[data-test="ai-actions"]').count()).toBe(
0,
);
await expect(page.getByRole('button', { name: 'Ask AI' })).toBeHidden();
});
test('it checks that Crisp is trying to init from config endpoint', async ({

View File

@@ -11,7 +11,12 @@ import {
overrideConfig,
verifyDocName,
} from './utils-common';
import { getEditor, openSuggestionMenu, writeInEditor } from './utils-editor';
import {
getEditor,
mockAIResponse,
openSuggestionMenu,
writeInEditor,
} from './utils-editor';
import { connectOtherUserToDoc, updateShareLink } from './utils-share';
import {
createRootSubPage,
@@ -39,6 +44,7 @@ test.describe('Doc Editor', () => {
.selectText();
const toolbar = page.locator('.bn-formatting-toolbar');
await expect(toolbar.getByRole('button', { name: 'Ask AI' })).toBeVisible();
await expect(
toolbar.locator('button[data-test="comment-toolbar-button"]'),
).toBeVisible();
@@ -64,9 +70,6 @@ test.describe('Doc Editor', () => {
await expect(
toolbar.locator('button[data-test="createLink"]'),
).toBeVisible();
await expect(
toolbar.locator('button[data-test="ai-actions"]'),
).toBeVisible();
await expect(
toolbar.locator('button[data-test="convertMarkdown"]'),
).toBeVisible();
@@ -93,14 +96,12 @@ test.describe('Doc Editor', () => {
await expect(image).toHaveAttribute('role', 'presentation');
await image.dblclick();
await image.click();
await expect(toolbar.getByRole('button', { name: 'Ask AI' })).toBeHidden();
await expect(
toolbar.locator('button[data-test="comment-toolbar-button"]'),
).toBeHidden();
await expect(
toolbar.locator('button[data-test="ai-actions"]'),
).toBeHidden();
await expect(
toolbar.locator('button[data-test="convertMarkdown"]'),
).toBeHidden();
@@ -389,6 +390,156 @@ test.describe('Doc Editor', () => {
await expect(image).toHaveAttribute('aria-hidden', 'true');
});
test('it checks the AI feature and accepts changes', async ({
page,
browserName,
}) => {
await overrideConfig(page, {
AI_BOT: {
name: 'Albert AI',
color: '#8bc6ff',
},
});
await mockAIResponse(page);
await page.goto('/');
await createDoc(page, 'doc-ai', browserName, 1);
await openSuggestionMenu({ page });
await page.getByText('Ask AI').click();
await expect(
page.getByRole('option', { name: 'Continue Writing' }),
).toBeVisible();
await expect(page.getByRole('option', { name: 'Summarize' })).toBeVisible();
await page.keyboard.press('Escape');
const editor = await writeInEditor({ page, text: 'Hello World' });
await editor.getByText('Hello World').selectText();
// Check from toolbar
await page.getByRole('button', { name: 'Ask AI' }).click();
await expect(
page.getByRole('option', { name: 'Improve Writing' }),
).toBeVisible();
await expect(
page.getByRole('option', { name: 'Fix Spelling' }),
).toBeVisible();
await expect(page.getByRole('option', { name: 'Translate' })).toBeVisible();
await page.getByRole('option', { name: 'Translate' }).click();
await page
.getByRole('textbox', { name: 'Ask anything...' })
.fill('Translate into french');
await page.getByRole('textbox', { name: 'Ask anything...' }).press('Enter');
await expect(editor.getByText('Albert AI')).toBeVisible();
await page
.locator('p.bn-mt-suggestion-menu-item-title')
.getByText('Accept')
.click();
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
// Check Suggestion menu
await page.locator('.bn-block-outer').last().fill('/');
await expect(page.getByText('Write with AI')).toBeVisible();
// Reload the page to check that the AI change is still there
await page.goto(page.url());
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
});
test('it reverts with the AI feature', async ({ page, browserName }) => {
await overrideConfig(page, {
AI_BOT: {
name: 'Albert AI',
color: '#8bc6ff',
},
});
await mockAIResponse(page);
await page.goto('/');
await createDoc(page, 'doc-ai', browserName, 1);
const editor = await writeInEditor({ page, text: 'Hello World' });
await editor.getByText('Hello World').selectText();
// Check from toolbar
await page.getByRole('button', { name: 'Ask AI' }).click();
await page.getByRole('option', { name: 'Translate' }).click();
await page
.getByRole('textbox', { name: 'Ask anything...' })
.fill('Translate into french');
await page.getByRole('textbox', { name: 'Ask anything...' }).press('Enter');
await expect(editor.getByText('Albert AI')).toBeVisible();
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
await page
.locator('p.bn-mt-suggestion-menu-item-title')
.getByText('Revert')
.click();
await expect(editor.getByText('Hello World')).toBeVisible();
});
test('it checks the AI buttons feature', async ({ page, browserName }) => {
await page.route(/.*\/ai-translate\//, async (route) => {
const request = route.request();
if (request.method().includes('POST')) {
await route.fulfill({
json: {
answer: 'Bonjour le monde',
},
});
} else {
await route.continue();
}
});
await createDoc(page, 'doc-ai', browserName, 1);
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'AI' }).click();
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Rephrase' }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Summarize' }),
).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Correct' })).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
await page.getByRole('menuitem', { name: 'Language' }).hover();
await expect(
page.getByRole('menuitem', { name: 'English', exact: true }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'French', exact: true }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'German', exact: true }),
).toBeVisible();
await page.getByRole('menuitem', { name: 'English', exact: true }).click();
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
});
test('it checks the AI buttons', async ({ page, browserName }) => {
await page.route(/.*\/ai-translate\//, async (route) => {
const request = route.request();

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Page } from '@playwright/test';
export const getEditor = async ({ page }: { page: Page }) => {
@@ -30,3 +32,55 @@ export const writeInEditor = async ({
.fill(text);
return editor;
};
export const mockAIResponse = async (page: Page) => {
await page.route(/.*\/ai-proxy\//, async (route) => {
const req = route.request();
if (req.method() !== 'POST') {
return route.continue();
}
// Extract the block ID from the request's selectedBlocks
const requestData = req.postDataJSON();
const messages = requestData?.messages || [];
const userMessage = messages.find((msg: any) => msg.role === 'user');
const documentState = userMessage?.metadata?.documentState;
const selectedBlocks = documentState?.selectedBlocks || [];
const blockId = selectedBlocks[0]?.id || 'initialBlockId$';
const sse = [
`data: {"type":"start"}\n\n`,
`data: {"type":"start-step"}\n\n`,
`data: ${JSON.stringify({
type: 'tool-input-available',
toolCallId: 'chatcmpl-mock-0',
toolName: 'applyDocumentOperations',
input: {
operations: [
{
type: 'update',
id: blockId,
block: '<p>Bonjour le monde</p>',
},
],
},
})}\n\n`,
`data: {"type":"finish-step"}\n\n`,
`data: {"type":"finish","finishReason":"tool-calls"}\n\n`,
`data: [DONE]\n\n`,
].join('');
await route.fulfill({
status: 200,
headers: {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
'x-vercel-ai-data-stream': 'v1',
'x-accel-buffering': 'no',
Connection: 'keep-alive',
},
body: sse,
});
});
};

View File

@@ -19,10 +19,12 @@
},
"dependencies": {
"@ag-media/react-pdf-table": "2.0.3",
"@ai-sdk/openai": "3.0.19",
"@blocknote/code-block": "0.47.0",
"@blocknote/core": "0.47.0",
"@blocknote/mantine": "0.47.0",
"@blocknote/react": "0.47.0",
"@blocknote/xl-ai": "0.47.0",
"@blocknote/xl-docx-exporter": "0.47.0",
"@blocknote/xl-multi-column": "0.47.0",
"@blocknote/xl-odt-exporter": "0.47.0",
@@ -44,6 +46,7 @@
"@sentry/nextjs": "10.38.0",
"@tanstack/react-query": "5.90.21",
"@tiptap/extensions": "*",
"ai": "6.0.49",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.1.1",
@@ -71,6 +74,7 @@
"uuid": "13.0.0",
"y-protocols": "1.0.7",
"yjs": "*",
"zod": "3.25.28",
"zustand": "5.0.11"
},
"devDependencies": {

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.89904 7.9473C6.0847 7.9473 6.19816 7.84673 6.23941 7.6456C6.31677 7.17114 6.39671 6.76887 6.47922 6.43881C6.56174 6.10359 6.66746 5.82768 6.79639 5.61108C6.92532 5.38932 7.09551 5.21139 7.30696 5.0773C7.5184 4.93806 7.78916 4.82718 8.11922 4.74466C8.44928 4.66215 8.85928 4.58737 9.34922 4.52033C9.56066 4.49454 9.66639 4.3785 9.66639 4.17221C9.66639 3.98139 9.56066 3.87051 9.34922 3.83957C8.8696 3.77768 8.46475 3.70548 8.13469 3.62297C7.80979 3.53529 7.53903 3.42441 7.32243 3.29033C7.11098 3.15624 6.93822 2.98089 6.80413 2.76429C6.6752 2.54253 6.5669 2.26662 6.47922 1.93656C6.39671 1.60649 6.31677 1.20165 6.23941 0.722032C6.19816 0.515743 6.0847 0.412598 5.89904 0.412598C5.70306 0.412598 5.58702 0.515743 5.55092 0.722032C5.47872 1.1965 5.40136 1.60134 5.31885 1.93656C5.23633 2.26662 5.12803 2.53995 4.99394 2.75655C4.86501 2.97316 4.69483 3.15108 4.48338 3.29033C4.27193 3.42441 4.00118 3.53272 3.67112 3.61523C3.34106 3.69775 2.93364 3.77253 2.44886 3.83957C2.23741 3.87051 2.13169 3.98139 2.13169 4.17221C2.13169 4.36819 2.23741 4.48422 2.44886 4.52033C3.04709 4.60284 3.52929 4.70083 3.89546 4.81429C4.26162 4.92774 4.54784 5.0902 4.75413 5.30164C4.96042 5.51309 5.1203 5.80705 5.23376 6.18353C5.35237 6.55485 5.45809 7.04221 5.55092 7.6456C5.59218 7.84673 5.70822 7.9473 5.89904 7.9473ZM2.53395 9.27786C2.6732 9.27786 2.75829 9.20824 2.78923 9.06899C2.83565 8.77503 2.87691 8.54296 2.91301 8.37277C2.95426 8.20774 3.01357 8.08138 3.09093 7.99371C3.17345 7.90604 3.30238 7.839 3.47772 7.79258C3.65307 7.74617 3.90061 7.69459 4.22036 7.63786C4.35961 7.61208 4.42923 7.52956 4.42923 7.39032C4.42923 7.25623 4.35961 7.17629 4.22036 7.15051C3.90061 7.09894 3.65307 7.05252 3.47772 7.01126C3.30753 6.96485 3.1786 6.90038 3.09093 6.81787C3.00841 6.73019 2.94911 6.60126 2.91301 6.43107C2.87691 6.26089 2.83565 6.02623 2.78923 5.72711C2.75313 5.57756 2.66804 5.50278 2.53395 5.50278C2.40502 5.50278 2.3225 5.57756 2.2864 5.72711C2.23483 6.02108 2.19099 6.25057 2.15489 6.4156C2.11879 6.58063 2.05949 6.70699 1.97697 6.79466C1.89961 6.87717 1.77326 6.94164 1.59791 6.98805C1.42773 7.03447 1.18276 7.08862 0.863011 7.15051C0.713451 7.17629 0.638672 7.25623 0.638672 7.39032C0.638672 7.52956 0.721187 7.61208 0.886218 7.63786C1.19565 7.68944 1.43546 7.73843 1.60565 7.78485C1.77584 7.8261 1.89961 7.89057 1.97697 7.97824C2.05949 8.06592 2.11879 8.19227 2.15489 8.3573C2.19099 8.52749 2.23483 8.75956 2.2864 9.05352C2.32766 9.20308 2.41018 9.27786 2.53395 9.27786Z"
fill="currentColor"
/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,32 @@
<svg
width="21"
height="23"
viewBox="0 0 21 23"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.2354 3.79043C14.2354 3.27923 14.2354 3.02363 14.1824 2.79024C14.0843 2.35887 13.8545 1.96867 13.5249 1.67369C13.3465 1.5141 13.123 1.39012 12.6759 1.14218L11.5335 0.508578C11.1112 0.274342 10.9 0.157224 10.6821 0.0913217C10.2795 -0.0304406 9.84994 -0.0304406 9.44735 0.0913218C9.22946 0.157224 9.01829 0.274342 8.59596 0.508578L8.12483 0.769879C7.553 1.08703 7.26708 1.2456 7.1361 1.43019C6.89111 1.77543 6.89133 2.23786 7.13665 2.58287C7.26781 2.76733 7.55388 2.92563 8.12601 3.24223L8.5074 3.45329C8.95515 3.70106 9.17902 3.82494 9.35767 3.98455C9.68787 4.27955 9.91806 4.67001 10.0163 5.10175C10.0694 5.33534 10.0694 5.59121 10.0694 6.10294V6.53965C10.0694 7.15975 10.0694 7.4698 10.1626 7.66969C10.3367 8.04326 10.7231 8.27085 11.1343 8.24203C11.3543 8.22661 11.6254 8.07629 12.1678 7.77566L12.6753 7.49434C13.1225 7.24644 13.3461 7.12249 13.5246 6.9629C13.8544 6.66791 14.0842 6.27765 14.1824 5.84618C14.2354 5.61274 14.2354 5.35708 14.2354 4.84574V3.79043Z"
fill="currentColor"
/>
<path
d="M5.68991 3.90829C5.2472 3.65269 5.02584 3.52489 4.7972 3.45414C4.37459 3.32338 3.92177 3.32728 3.50147 3.46529C3.27408 3.53997 3.05495 3.67156 2.61671 3.93474L1.49679 4.60729C1.08277 4.85592 0.875755 4.98024 0.709735 5.13599C0.402994 5.42376 0.188193 5.7958 0.0923494 6.20533C0.0404756 6.42699 0.0363194 6.66842 0.028007 7.15129L0.0187342 7.68995C0.00747948 8.34375 0.00185211 8.67064 0.0962156 8.87638C0.272705 9.26116 0.673295 9.49219 1.09474 9.45224C1.32007 9.43088 1.60019 9.26229 2.16045 8.92511L2.53392 8.70034C2.97237 8.43646 3.19159 8.30452 3.41914 8.22961C3.83972 8.09115 4.29296 8.08703 4.71598 8.21783C4.94485 8.2886 5.16644 8.41653 5.60961 8.67239L5.98781 8.89075C6.52483 9.2008 6.79334 9.35582 7.01304 9.37508C7.42362 9.41108 7.81391 9.19026 7.99452 8.81979C8.09116 8.62155 8.09657 8.31155 8.1074 7.69154L8.11752 7.11137C8.12645 6.60011 8.13091 6.34449 8.08191 6.11016C7.99135 5.67704 7.76831 5.28283 7.4437 4.98213C7.26808 4.81944 7.04667 4.69161 6.60384 4.43595L5.68991 3.90829Z"
fill="currentColor"
/>
<path
d="M1.51922 11.3679C1.07651 11.6235 0.85515 11.7513 0.679557 11.9139C0.355009 12.2145 0.131977 12.6086 0.0413546 13.0416C-0.00767554 13.2759 -0.00327613 13.5314 0.00552269 14.0426L0.0280076 15.3487C0.0363201 15.8316 0.0404763 16.073 0.0923502 16.2947C0.188194 16.7042 0.402994 17.0762 0.709736 17.364C0.875756 17.5198 1.08277 17.6441 1.49679 17.8927L1.95865 18.1701C2.51922 18.5067 2.79951 18.675 3.02486 18.6962C3.44634 18.7357 3.84671 18.5043 4.02283 18.1194C4.117 17.9135 4.11106 17.5867 4.09918 16.9329L4.09126 16.4971C4.08196 15.9854 4.07731 15.7296 4.12621 15.4951C4.21659 15.0616 4.43964 14.667 4.76443 14.3661C4.94015 14.2033 5.16173 14.0753 5.6049 13.8195L5.9831 13.6011C6.52012 13.2911 6.78864 13.136 6.91517 12.9554C7.15163 12.6178 7.15554 12.1694 6.92501 11.8278C6.80165 11.6449 6.53588 11.4853 6.00435 11.1659L5.50697 10.867C5.06867 10.6037 4.84952 10.472 4.62209 10.3973C4.20172 10.2591 3.7488 10.2552 3.32609 10.386C3.09739 10.4567 2.87597 10.5845 2.43315 10.8402L1.51922 11.3679Z"
fill="currentColor"
/>
<path
d="M5.89405 18.7096C5.89405 19.2208 5.89405 19.4764 5.9471 19.7098C6.04517 20.1411 6.27495 20.5313 6.60462 20.8263C6.78299 20.9859 7.00651 21.1099 7.45356 21.3578L8.59596 21.9914C9.0183 22.2257 9.22946 22.3428 9.44736 22.4087C9.84994 22.5304 10.2795 22.5304 10.6821 22.4087C10.9 22.3428 11.1112 22.2257 11.5335 21.9914L12.0047 21.7301C12.5765 21.413 12.8624 21.2544 12.9934 21.0698C13.2384 20.7246 13.2382 20.2621 12.9928 19.9171C12.8617 19.7327 12.5756 19.5744 12.0035 19.2578L11.6221 19.0467C11.1743 18.7989 10.9505 18.6751 10.7718 18.5155C10.4416 18.2205 10.2114 17.83 10.1132 17.3982C10.06 17.1647 10.06 16.9088 10.06 16.3971V15.9604C10.06 15.3403 10.06 15.0302 9.96687 14.8303C9.79275 14.4567 9.40637 14.2291 8.99523 14.258C8.77522 14.2734 8.50405 14.4237 7.9617 14.7243L7.45419 15.0057C7.00697 15.2536 6.78336 15.3775 6.60492 15.5371C6.27512 15.8321 6.04523 16.2224 5.94713 16.6538C5.89405 16.8873 5.89405 17.1429 5.89405 17.6543V18.7096Z"
fill="currentColor"
/>
<path
d="M14.4396 18.5917C14.8823 18.8473 15.1036 18.9751 15.3323 19.0459C15.7549 19.1766 16.2077 19.1727 16.628 19.0347C16.8554 18.96 17.0745 18.8284 17.5128 18.5653L18.6327 17.8927C19.0467 17.6441 19.2537 17.5198 19.4197 17.364C19.7265 17.0762 19.9413 16.7042 20.0371 16.2947C20.089 16.073 20.0932 15.8316 20.1015 15.3487L20.1107 14.81C20.122 14.1563 20.1276 13.8294 20.0333 13.6236C19.8568 13.2388 19.4562 13.0078 19.0347 13.0478C18.8094 13.0691 18.5293 13.2377 17.969 13.5749L17.5956 13.7997C17.1571 14.0635 16.9379 14.1955 16.7103 14.2704C16.2898 14.4088 15.8365 14.413 15.4135 14.2822C15.1846 14.2114 14.963 14.0835 14.5199 13.8276L14.1417 13.6093C13.6047 13.2992 13.3361 13.1442 13.1164 13.1249C12.7059 13.0889 12.3156 13.3097 12.135 13.6802C12.0383 13.8785 12.0329 14.1885 12.0221 14.8085L12.012 15.3886C12.003 15.8999 11.9986 16.1555 12.0476 16.3898C12.1381 16.823 12.3612 17.2172 12.6858 17.5179C12.8614 17.6806 13.0828 17.8084 13.5256 18.0641L14.4396 18.5917Z"
fill="currentColor"
/>
<path
d="M18.6103 11.1321C19.053 10.8765 19.2743 10.7487 19.4499 10.5861C19.7745 10.2855 19.9975 9.89139 20.0881 9.4584C20.1372 9.22413 20.1328 8.96857 20.124 8.45744L20.1015 7.15129C20.0932 6.66842 20.089 6.42699 20.0371 6.20533C19.9413 5.7958 19.7265 5.42376 19.4197 5.13599C19.2537 4.98024 19.0467 4.85592 18.6327 4.60729L18.1708 4.32993C17.6103 3.99328 17.33 3.82496 17.1046 3.80382C16.6831 3.76427 16.2828 3.99568 16.1067 4.38063C16.0125 4.58645 16.0184 4.91334 16.0303 5.56713L16.0382 6.00295C16.0475 6.5146 16.0522 6.77042 16.0033 7.00493C15.9129 7.43839 15.6898 7.83297 15.3651 8.13392C15.1893 8.29675 14.9678 8.42468 14.5246 8.68054L14.1464 8.8989C13.6094 9.20895 13.3408 9.36397 13.2143 9.54461C12.9779 9.88219 12.9739 10.3306 13.2045 10.6722C13.3278 10.8551 13.5936 11.0147 14.1251 11.3341L14.6225 11.633C15.0608 11.8963 15.28 12.028 15.5074 12.1027C15.9278 12.2409 16.3807 12.2448 16.8034 12.114C17.0321 12.0433 17.2535 11.9155 17.6963 11.6598L18.6103 11.1321Z"
fill="currentColor"
/>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4.4 21L3 19.6L10.525 12.05L6 10.925L10.95 7.85L10.525 2L15 5.775L20.4 3.575L18.225 9L22 13.45L16.15 13.05L13.05 18L11.925 13.475L4.4 21ZM5 8L3 6L5 4L7 6L5 8ZM13.875 12.925L15.075 10.95L17.4 11.125L15.9 9.35L16.775 7.2L14.625 8.075L12.85 6.6L13.025 8.9L11.05 10.125L13.3 10.675L13.875 12.925ZM18 21L16 19L18 17L20 19L18 21Z"
fill="#303030"
/>
</svg>

After

Width:  |  Height:  |  Size: 444 B

View File

@@ -0,0 +1,275 @@
/**
* We have to override the default BlockNote AI Menu to customize the items shown to the user.
*
* See original implementation:
* https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-ai/src/components/AIMenu/AIMenu.tsx
*/
import {
useBlockNoteEditor,
useComponentsContext,
useExtension,
useExtensionState,
} from '@blocknote/react';
import {
AIExtension,
AIMenuSuggestionItem,
PromptSuggestionMenu,
getDefaultAIMenuItems,
} from '@blocknote/xl-ai';
import '@blocknote/xl-ai/style.css';
import { Button } from '@gouvfr-lasuite/cunningham-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { createGlobalStyle } from 'styled-components';
import { Box } from '@/components/Box';
import { Icon } from '@/components/Icon';
import IconWandStar from '../../assets/wand_stars.svg';
import {
DocsBlockNoteEditor,
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema,
} from '../../types';
import { IconAI } from './IconAI';
const AIMenuStyle = createGlobalStyle`
#ai-suggestion-menu .bn-suggestion-menu-item-small .bn-mt-suggestion-menu-item-section[data-position=left] svg {
height: 18px;
width: 18px;
}
.--docs--ai-menu input[name="ai-prompt"]{
padding-inline-start: 3rem;
}
.--docs--ai-menu .mantine-TextInput-section[data-position="left"] {
margin-inline: 0.75rem;
}
.--docs--ai-menu .mantine-TextInput-section[data-position="right"] {
inset-inline-end: 2rem;
}
`;
export type AIMenuProps = {
items?: (
editor: DocsBlockNoteEditor,
aiResponseStatus:
| 'user-input'
| 'thinking'
| 'ai-writing'
| 'error'
| 'user-reviewing'
| 'closed',
) => AIMenuSuggestionItem[];
onManualPromptSubmit?: (userPrompt: string) => void;
};
export const AIMenu = (props: AIMenuProps) => {
const editor = useBlockNoteEditor<
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema
>();
const [prompt, setPrompt] = useState('');
const { t } = useTranslation();
const Components = useComponentsContext();
const ai = useExtension(AIExtension);
const aiResponseStatus = useExtensionState(AIExtension, {
selector: (state) =>
state.aiMenuState !== 'closed' ? state.aiMenuState.status : 'closed',
});
const { items: externalItems } = props;
// note, technically there might be a bug with this useMemo when quickly changing the selection and opening the menu
// would not call getDefaultAIMenuItems with the correct selection, because the component is reused and the memo not retriggered
// practically this should not happen (you can test it by using a high transition duration in useUIElementPositioning)
const items = useMemo(() => {
let items: AIMenuSuggestionItem[];
if (externalItems) {
items = externalItems(editor, aiResponseStatus);
} else {
items = getDefaultAIMenuItems(editor, aiResponseStatus);
}
/**
* Customizations to the default AI Menu items
*/
if (aiResponseStatus === 'user-input') {
if (editor.getSelection()) {
items = items
.filter((item) => ['simplify'].indexOf(item.key) === -1)
.map((item) => {
if (item.key === 'improve_writing') {
return {
...item,
icon: <IconWandStar />,
};
} else if (item.key === 'translate') {
return {
...item,
icon: (
<Icon iconName="translate" $color="inherit" $size="18px" />
),
};
}
return item;
});
} else {
items = items.filter(
(item) => ['action_items', 'write_anything'].indexOf(item.key) === -1,
);
}
} else if (aiResponseStatus === 'user-reviewing') {
items = items.map((item) => {
if (item.key === 'accept') {
return {
...item,
icon: (
<Icon iconName="check_circle" $color="inherit" $size="18px" />
),
};
}
return item;
});
} else if (aiResponseStatus === 'error') {
items.unshift({
key: 'accept',
icon: <Icon iconName="check_circle" $color="inherit" $size="18px" />,
title: t('Accept anyway'),
onItemClick: () => {
ai.acceptChanges();
ai.closeAIMenu();
},
size: 'small',
});
}
// map from AI items to React Items required by PromptSuggestionMenu
return items.map((item) => {
return {
...item,
onItemClick: () => {
item.onItemClick(setPrompt);
},
};
});
}, [externalItems, aiResponseStatus, editor, t, ai]);
const onManualPromptSubmitDefault = useCallback(
async (userPrompt: string) => {
await ai.invokeAI({
userPrompt,
useSelection: editor.getSelection() !== undefined,
});
},
[ai, editor],
);
useEffect(() => {
// this is a bit hacky to run a useeffect to reset the prompt when the AI response is done
if (
aiResponseStatus === 'ai-writing' ||
aiResponseStatus === 'thinking' ||
aiResponseStatus === 'user-reviewing' ||
aiResponseStatus === 'error'
) {
setPrompt('');
}
}, [aiResponseStatus]);
const placeholder = useMemo(() => {
if (aiResponseStatus === 'thinking') {
return t('Thinking...');
} else if (aiResponseStatus === 'ai-writing') {
return t('Writing...');
} else if (aiResponseStatus === 'error') {
return t('An error occurred...');
}
return t('Ask anything...');
}, [aiResponseStatus, t]);
const IconInput = useMemo(() => {
if (aiResponseStatus === 'thinking') {
return <IconAI width="24px" isLoading />;
} else if (aiResponseStatus === 'ai-writing') {
return <IconAI width="24px" isHighlighted />;
} else if (aiResponseStatus === 'error') {
return <IconAI width="23px" isError />;
}
return <IconAI width="24px" />;
}, [aiResponseStatus]);
const rightSection = useMemo(() => {
if (aiResponseStatus === 'thinking' || aiResponseStatus === 'ai-writing') {
if (!Components) {
return null;
}
return (
<Button
onClick={async () => {
await ai.abort();
ai.rejectChanges();
ai.closeAIMenu();
}}
size="small"
variant="secondary"
icon={
<Icon
$size="lg"
$withThemeInherited
iconName="stop"
variant="filled"
/>
}
>
{t('Stop')}
</Button>
);
}
return undefined;
}, [Components, ai, aiResponseStatus, t]);
useEffect(() => {
const handleEscape = async (event: KeyboardEvent) => {
if (event.key === 'Escape') {
await ai.abort();
ai.rejectChanges();
ai.closeAIMenu();
}
};
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [ai]);
return (
<Box className="--docs--ai-menu" $width="100%" $maxWidth="500px">
<AIMenuStyle />
<PromptSuggestionMenu
onManualPromptSubmit={
props.onManualPromptSubmit || onManualPromptSubmitDefault
}
items={items}
promptText={prompt}
onPromptTextChange={setPrompt}
placeholder={placeholder}
disabled={
aiResponseStatus === 'thinking' || aiResponseStatus === 'ai-writing'
}
icon={IconInput}
rightSection={rightSection}
/>
</Box>
);
};

View File

@@ -0,0 +1,109 @@
/**
* We have to override the default BlockNote AI Toolbar Button to customize its appearance.
*
* See original implementation:
* https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-ai/src/components/FormattingToolbar/AIToolbarButton.tsx
*/
import { FormattingToolbarExtension } from '@blocknote/core/extensions';
import {
useBlockNoteEditor,
useComponentsContext,
useExtension,
useSelectedBlocks,
} from '@blocknote/react';
import { AIExtension } from '@blocknote/xl-ai';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import {
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema,
} from '../../types';
import { IconAI } from './IconAI';
export const AIToolbarButton = () => {
const { t } = useTranslation();
const Components = useComponentsContext();
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const editor = useBlockNoteEditor<
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema
>();
const ai = useExtension(AIExtension);
const formattingToolbar = useExtension(FormattingToolbarExtension);
const [isHighlighted, setIsHighlighted] = useState(false);
const selectedBlocks = useSelectedBlocks(editor);
const isContent = useMemo(() => {
return !!selectedBlocks.find((block) => block.content !== undefined);
}, [selectedBlocks]);
if (!editor.isEditable || !Components || !isContent) {
return null;
}
const onClick = () => {
const selection = editor.getSelection();
if (!selection) {
throw new Error('No selection');
}
const position = selection.blocks[selection.blocks.length - 1].id;
ai.openAIMenuAtBlock(position);
formattingToolbar.store.setState(false);
};
return (
<Box
$css={css`
& > button.mantine-Button-root {
padding-inline: 0;
transition: all 0.1s ease-in;
& .mantine-Button-label {
padding-inline: ${spacingsTokens['2xs']};
}
&:hover,
&:hover {
background-color: ${colorsTokens['gray-050']};
}
}
`}
onMouseEnter={() => setIsHighlighted(true)}
onMouseLeave={() => setIsHighlighted(false)}
$direction="row"
className="--docs--ai-toolbar-button"
>
<Components.Generic.Toolbar.Button
className="bn-button"
onClick={onClick}
>
<Box
as="span"
$direction="row"
$align="center"
$gap={spacingsTokens['xs']}
$padding={{ right: '2xs' }}
>
<IconAI isHighlighted={isHighlighted} width="18px" />
{t('Ask AI')}
</Box>
</Components.Generic.Toolbar.Button>
<Box
$background={colorsTokens['gray-100']}
$width="1px"
$height="70%"
$margin={{ left: '2px' }}
$css={css`
align-self: center;
`}
/>
</Box>
);
};

View File

@@ -0,0 +1,79 @@
import { RuleSet, css } from 'styled-components';
import { Icon } from '@/components';
import IconAIBase from '../../assets/IconAI.svg';
import IconAILoading from '../../assets/ai-loader.svg';
interface IconAIProps {
isError?: boolean;
isHighlighted?: boolean;
isLoading?: boolean;
width: string;
$css?: string | RuleSet<object>;
}
export const IconAI = ({
isError,
isHighlighted,
isLoading,
width,
$css,
}: IconAIProps) => {
if (isError) {
return (
<Icon
$theme="error"
$variation="secondary"
icon={<Icon iconName="error" $withThemeInherited $size={width} />}
/>
);
}
if (isLoading) {
return (
<Icon
$theme="brand"
$variation="tertiary"
$css={css`
animation: spin 5s linear infinite;
@keyframes spin {
0% {
transform: rotate(360deg);
}
100% {
transform: rotate(0deg);
}
}
${$css}
`}
$padding="0.15rem"
$width={width}
icon={<IconAILoading />}
/>
);
}
return (
<Icon
$css={css`
border: 1px solid var(--c--globals--colors--gray-100);
color: var(--c--globals--colors--gray-700);
transition: all 0.1s ease-in;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.05);
${isHighlighted &&
css`
background-color: var(--c--globals--colors--brand-450);
border: 1px solid var(--c--globals--colors--brand-350);
color: #ffffff;
box-shadow: 0 1px 4px 0 rgba(88, 88, 225, 0.25);
`}
${$css}
`}
$radius="100%"
$padding="0.15rem"
$width={width}
icon={<IconAIBase />}
/>
);
};

View File

@@ -0,0 +1,3 @@
export * from './AIMenu';
export * from './AIToolbarButton';
export * from './useAI';

View File

@@ -0,0 +1,31 @@
import { AIExtension } from '@blocknote/xl-ai';
import { DefaultChatTransport } from 'ai';
import { useMemo } from 'react';
import { fetchAPI } from '@/api';
import { useConfig } from '@/core';
import { Doc } from '@/docs/doc-management';
export const useAI = (docId: Doc['id']) => {
const conf = useConfig().data;
return useMemo(() => {
const extension = AIExtension({
transport: new DefaultChatTransport({
fetch: (input, init) => {
// Create a new headers object without the Authorization header
const headers = new Headers(init?.headers);
headers.delete('Authorization');
return fetchAPI(`documents/${docId}/ai-proxy/`, {
...init,
headers,
});
},
}),
agentCursor: conf?.AI_BOT,
});
return extension;
}, [conf?.AI_BOT, docId]);
};

View File

@@ -12,6 +12,9 @@ import * as locales from '@blocknote/core/locales';
import { BlockNoteView } from '@blocknote/mantine';
import '@blocknote/mantine/style.css';
import { useCreateBlockNote } from '@blocknote/react';
import { AIMenuController } from '@blocknote/xl-ai';
import { en as aiEn } from '@blocknote/xl-ai/locales';
import '@blocknote/xl-ai/style.css';
import { HocuspocusProvider } from '@hocuspocus/provider';
import { useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
@@ -36,6 +39,7 @@ import { cssEditor } from '../styles';
import { DocsBlockNoteEditor } from '../types';
import { randomColor } from '../utils';
import { AIMenu, useAI } from './AI';
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
import { cssComments, useComments } from './comments/';
@@ -99,6 +103,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
}
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
const aiExtension = useAI(doc.id);
const collabName = user?.full_name || user?.email;
const cursorName = collabName || t('Anonymous');
@@ -168,6 +173,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
...(multiColumnLocales && {
multi_column:
multiColumnLocales[lang as keyof typeof multiColumnLocales],
ai: aiEn,
}),
},
pasteHandler: ({ event, defaultPasteHandler }) => {
@@ -190,7 +196,15 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
return defaultPasteHandler();
},
extensions: [CommentsExtension({ threadStore, resolveUsers })],
extensions: [
CommentsExtension({ threadStore, resolveUsers }),
...(aiExtension ? [aiExtension] : []),
],
visualMedia: {
image: {
maxWidth: 760,
},
},
tables: {
splitCells: true,
cellBackgroundColor: true,
@@ -243,6 +257,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
comments={showComments}
aria-label={t('Document editor')}
>
{aiExtension && <AIMenuController aiMenu={AIMenu} />}
<BlockNoteSuggestionMenu />
<BlockNoteToolbar />
</BlockNoteView>

View File

@@ -8,9 +8,12 @@ import {
useBlockNoteEditor,
useDictionary,
} from '@blocknote/react';
import { getAISlashMenuItems } from '@blocknote/xl-ai';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '@/core';
import {
DocsBlockSchema,
DocsInlineContentSchema,
@@ -39,6 +42,7 @@ export const BlockNoteSuggestionMenu = () => {
const fileBlocksName = dictionaryDate.slash_menu.file.group;
const getInterlinkingMenuItems = useGetInterlinkingMenuItems();
const { data: conf } = useConfig();
const getSlashMenuItems = useMemo(() => {
// We insert it after the "Code Block" item to have the interlinking block displayed after the basic blocks
@@ -50,6 +54,7 @@ export const BlockNoteSuggestionMenu = () => {
getMultiColumnSlashMenuItems?.(editor) || [],
getPdfReactSlashMenuItems(editor, t, fileBlocksName),
getCalloutReactSlashMenuItems(editor, t, basicBlocksName),
conf?.AI_FEATURE_ENABLED ? getAISlashMenuItems(editor) : [],
);
const index = combinedMenu.findIndex(
@@ -66,7 +71,14 @@ export const BlockNoteSuggestionMenu = () => {
return async (query: string) =>
Promise.resolve(filterSuggestionItems(newSlashMenuItems, query));
}, [basicBlocksName, editor, getInterlinkingMenuItems, t, fileBlocksName]);
}, [
editor,
t,
fileBlocksName,
basicBlocksName,
conf?.AI_FEATURE_ENABLED,
getInterlinkingMenuItems,
]);
return (
<SuggestionMenuController

View File

@@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next';
import { useConfig } from '@/core/config/api';
import { AIToolbarButton } from '../AI/';
import { CommentToolbarButton } from '../comments/CommentToolbarButton';
import { getCalloutFormattingToolbarItems } from '../custom-blocks';
@@ -69,6 +70,8 @@ export const BlockNoteToolbar = () => {
const formattingToolbar = useCallback(() => {
return (
<FormattingToolbar>
{conf?.AI_FEATURE_ENABLED && <AIToolbarButton />}
<CommentToolbarButton />
{toolbarItems}

View File

@@ -52,7 +52,7 @@ export const DocEditorContainer = ({
<Box
$direction="row"
$width="100%"
$css="overflow-x: clip; flex: 1;"
$css="flex: 1;"
$position="relative"
className="--docs--doc-editor-content"
>

View File

@@ -35,7 +35,26 @@ export const useSaveDoc = (
_updatedDoc: Y.Doc,
transaction: Y.Transaction,
) => {
setIsLocalChange(transaction.local);
/**
* When the AI edit the doc transaction.local is false,
* so we check if the origin constructor to know where
* the transaction comes from.
* "PluginKey" constructor comes from the current user, but transaction.local is more reliable
* "HocuspocusProvider" constructor comes from other users from the collaboration server,
* it seems quite reliable too.
* The AI constructor name seems to not be reliable enough, but by deduction if it's not local
* and not from other users, it has to be from the AI.
*
* TODO: see if we can get the local changes from the AI
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const transactionOrigin = transaction?.origin?.constructor?.name;
const PROVIDER_ORIGIN_CONSTRUCTOR = 'HocuspocusProvider';
const isAIChange =
!transaction.local && transactionOrigin !== PROVIDER_ORIGIN_CONSTRUCTOR;
setIsLocalChange(transaction.local || isAIChange);
};
yDoc.on('update', onUpdate);

View File

@@ -1,6 +1,11 @@
import { css } from 'styled-components';
export const cssEditor = css`
.mantine-Menu-itemLabel,
.mantine-Button-label {
font-family: var(--c--components--button--font-family);
}
&,
& > .bn-container,
& .ProseMirror {
@@ -149,6 +154,16 @@ export const cssEditor = css`
font-style: italic;
}
/**
* AI
*/
ins,
[data-type='modification'] {
background: var(--c--globals--colors--brand-100);
border-bottom: 2px solid var(--c--globals--colors--brand-300);
color: var(--c--globals--colors--brand-700);
}
/**
* Divider
*/

View File

@@ -41,7 +41,7 @@ export function HomeContent() {
aria-label={t('Main content')}
$css={css`
&:focus {
outline: 3px solid var(--c--theme--colors--primary-600);
outline: 3px solid var(--c--globals--colors--primary-600);
outline-offset: -3px;
}
&:focus:not(:focus-visible) {

View File

@@ -36,7 +36,7 @@ export function PageLayout({
$css={css`
flex-grow: 1;
&:focus {
outline: 3px solid var(--c--theme--colors--primary-600);
outline: 3px solid var(--c--globals--colors--primary-600);
outline-offset: -3px;
}
&:focus:not(:focus-visible) {

View File

@@ -17,6 +17,74 @@
resolved "https://registry.yarnpkg.com/@ag-media/react-pdf-table/-/react-pdf-table-2.0.3.tgz#113554b583b46e41a098cf64fecb5decd59ba004"
integrity sha512-IscjfAOKwsyQok9YmzvuToe6GojN7J8hF0kb8C+K8qZX1DvhheGO+hRSAPxbv2nKMbSpvk7CIhSqJEkw++XVWg==
"@ai-sdk/gateway@3.0.22":
version "3.0.22"
resolved "https://registry.yarnpkg.com/@ai-sdk/gateway/-/gateway-3.0.22.tgz#96836072096ead43f046192c29be188109a5bec6"
integrity sha512-NgnlY73JNuooACHqUIz5uMOEWvqR1MMVbb2soGLMozLY1fgwEIF5iJFDAGa5/YArlzw2ATVU7zQu7HkR/FUjgA==
dependencies:
"@ai-sdk/provider" "3.0.5"
"@ai-sdk/provider-utils" "4.0.9"
"@vercel/oidc" "3.1.0"
"@ai-sdk/gateway@3.0.55":
version "3.0.55"
resolved "https://registry.yarnpkg.com/@ai-sdk/gateway/-/gateway-3.0.55.tgz#0a74425037ed39756ce59b153235bf3ba13cd43a"
integrity sha512-7xMeTJnCjwRwXKVCiv4Ly4qzWvDuW3+W1WIV0X1EFu6W83d4mEhV9bFArto10MeTw40ewuDjrbrZd21mXKohkw==
dependencies:
"@ai-sdk/provider" "3.0.8"
"@ai-sdk/provider-utils" "4.0.15"
"@vercel/oidc" "3.1.0"
"@ai-sdk/openai@3.0.19":
version "3.0.19"
resolved "https://registry.yarnpkg.com/@ai-sdk/openai/-/openai-3.0.19.tgz#c4471a0e667c3404339a645e575e867538bf407e"
integrity sha512-qpMGKV6eYfW8IzErk/OppchQwVui3GPc4BEfg/sQGRzR89vf2Sa8qvSavXeZi5w/oUF56d+VtobwSH0FRooFCQ==
dependencies:
"@ai-sdk/provider" "3.0.5"
"@ai-sdk/provider-utils" "4.0.9"
"@ai-sdk/provider-utils@4.0.15", "@ai-sdk/provider-utils@^4.0.2":
version "4.0.15"
resolved "https://registry.yarnpkg.com/@ai-sdk/provider-utils/-/provider-utils-4.0.15.tgz#d585c7c89cfdf13697a40be5768ecd907a251585"
integrity sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w==
dependencies:
"@ai-sdk/provider" "3.0.8"
"@standard-schema/spec" "^1.1.0"
eventsource-parser "^3.0.6"
"@ai-sdk/provider-utils@4.0.9":
version "4.0.9"
resolved "https://registry.yarnpkg.com/@ai-sdk/provider-utils/-/provider-utils-4.0.9.tgz#f15d6ed31fca8aeca402fa56278659a20581057e"
integrity sha512-bB4r6nfhBOpmoS9mePxjRoCy+LnzP3AfhyMGCkGL4Mn9clVNlqEeKj26zEKEtB6yoSVcT1IQ0Zh9fytwMCDnow==
dependencies:
"@ai-sdk/provider" "3.0.5"
"@standard-schema/spec" "^1.1.0"
eventsource-parser "^3.0.6"
"@ai-sdk/provider@3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@ai-sdk/provider/-/provider-3.0.5.tgz#730c5acdc4f074c877a547c1492fafc81bdc4f53"
integrity sha512-2Xmoq6DBJqmSl80U6V9z5jJSJP7ehaJJQMy2iFUqTay06wdCqTnPVBBQbtEL8RCChenL+q5DC5H5WzU3vV3v8w==
dependencies:
json-schema "^0.4.0"
"@ai-sdk/provider@3.0.8":
version "3.0.8"
resolved "https://registry.yarnpkg.com/@ai-sdk/provider/-/provider-3.0.8.tgz#fd7fac7533c03534ac1d3fb710a6b96e2aa00263"
integrity sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==
dependencies:
json-schema "^0.4.0"
"@ai-sdk/react@^3.0.5":
version "3.0.103"
resolved "https://registry.yarnpkg.com/@ai-sdk/react/-/react-3.0.103.tgz#3e0da588714fea4972e06d68f1f67b8dc47b8608"
integrity sha512-9kpQpOVv4Jf2I7lX/GCaO6u5F03e8QkVNIsneWJTH8P/txwPoaAp6T0rt9VVTE8bH65sEv/tZo9Ohst0GP/flQ==
dependencies:
"@ai-sdk/provider-utils" "4.0.15"
ai "6.0.101"
swr "^2.2.5"
throttleit "2.1.0"
"@apideck/better-ajv-errors@^0.3.1":
version "0.3.6"
resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz#957d4c28e886a64a8141f7522783be65733ff097"
@@ -1296,6 +1364,36 @@
y-protocols "^1.0.6"
yjs "^13.6.27"
"@blocknote/xl-ai@0.47.0":
version "0.47.0"
resolved "https://registry.yarnpkg.com/@blocknote/xl-ai/-/xl-ai-0.47.0.tgz#fff198e65b05758ef1776bb24be4acc93917143a"
integrity sha512-czx1DyO5bMy2nBCQxesbMQyEZhFpqA6pRUyhnhUENHKievlUZgymRAH3DyO3Yl3STdZkwAiWQDkhVIQiecRNwA==
dependencies:
"@ai-sdk/provider-utils" "^4.0.2"
"@ai-sdk/react" "^3.0.5"
"@blocknote/core" "0.47.0"
"@blocknote/mantine" "0.47.0"
"@blocknote/react" "0.47.0"
"@floating-ui/react" "^0.26.28"
"@handlewithcare/prosemirror-suggest-changes" "^0.1.8"
"@tiptap/core" "^3.13.0"
ai "^6.0.5"
lodash.isequal "^4.5.0"
lodash.merge "^4.6.2"
prosemirror-changeset "^2.3.1"
prosemirror-model "^1.25.4"
prosemirror-state "^1.4.4"
prosemirror-tables "^1.8.3"
prosemirror-transform "^1.10.5"
prosemirror-view "^1.41.4"
react "^19.2.3"
react-dom "^19.2.3"
react-icons "^5.5.0"
remark-parse "^11.0.0"
remark-stringify "^11.0.0"
unified "^11.0.5"
y-prosemirror "^1.3.7"
"@blocknote/xl-docx-exporter@0.47.0":
version "0.47.0"
resolved "https://registry.yarnpkg.com/@blocknote/xl-docx-exporter/-/xl-docx-exporter-0.47.0.tgz#b78ee9082538410efc5867cf276ec325a632ef23"
@@ -1831,6 +1929,13 @@
dependencies:
"@floating-ui/utils" "^0.2.10"
"@floating-ui/core@^1.7.4":
version "1.7.4"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.4.tgz#4a006a6e01565c0f87ba222c317b056a2cffd2f4"
integrity sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==
dependencies:
"@floating-ui/utils" "^0.2.10"
"@floating-ui/dom@^1.0.0", "@floating-ui/dom@^1.0.1", "@floating-ui/dom@^1.7.4":
version "1.7.4"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.4.tgz#ee667549998745c9c3e3e84683b909c31d6c9a77"
@@ -1839,6 +1944,21 @@
"@floating-ui/core" "^1.7.3"
"@floating-ui/utils" "^0.2.10"
"@floating-ui/dom@^1.7.5":
version "1.7.5"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.5.tgz#60bfc83a4d1275b2a90db76bf42ca2a5f2c231c2"
integrity sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==
dependencies:
"@floating-ui/core" "^1.7.4"
"@floating-ui/utils" "^0.2.10"
"@floating-ui/react-dom@^2.1.2":
version "2.1.7"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.7.tgz#529475cc16ee4976ba3387968117e773d9aa703e"
integrity sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==
dependencies:
"@floating-ui/dom" "^1.7.5"
"@floating-ui/react-dom@^2.1.6":
version "2.1.6"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.6.tgz#189f681043c1400561f62972f461b93f01bf2231"
@@ -1846,6 +1966,15 @@
dependencies:
"@floating-ui/dom" "^1.7.4"
"@floating-ui/react@^0.26.28":
version "0.26.28"
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.28.tgz#93f44ebaeb02409312e9df9507e83aab4a8c0dc7"
integrity sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==
dependencies:
"@floating-ui/react-dom" "^2.1.2"
"@floating-ui/utils" "^0.2.8"
tabbable "^6.0.0"
"@floating-ui/react@^0.27.16":
version "0.27.16"
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.27.16.tgz#6e485b5270b7a3296fdc4d0faf2ac9abf955a2f7"
@@ -1855,7 +1984,7 @@
"@floating-ui/utils" "^0.2.10"
tabbable "^6.0.0"
"@floating-ui/utils@0.2.10", "@floating-ui/utils@^0.2.10":
"@floating-ui/utils@0.2.10", "@floating-ui/utils@^0.2.10", "@floating-ui/utils@^0.2.8":
version "0.2.10"
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c"
integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==
@@ -2011,6 +2140,11 @@
prosemirror-history "^1.4.1"
prosemirror-transform "^1.0.0"
"@handlewithcare/prosemirror-suggest-changes@^0.1.8":
version "0.1.8"
resolved "https://registry.yarnpkg.com/@handlewithcare/prosemirror-suggest-changes/-/prosemirror-suggest-changes-0.1.8.tgz#707d432376718d4618065b22aafbc55b9ce4ea5b"
integrity sha512-ewrJl4a8dTpPJNhqYySE2ZCjTRpXulWlUmFy3sbyJgPnGtN/zx7+8tbQ1OhHfMzZWfdmA8VjP9ecy+KO4HdOpA==
"@hocuspocus/common@^3.4.4":
version "3.4.4"
resolved "https://registry.yarnpkg.com/@hocuspocus/common/-/common-3.4.4.tgz#a888fbd6dff2f0b8947c76b7841bddb89eb4d795"
@@ -2825,7 +2959,7 @@
dependencies:
"@opentelemetry/api" "^1.3.0"
"@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.9.0":
"@opentelemetry/api@1.9.0", "@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe"
integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==
@@ -6557,6 +6691,11 @@
resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c"
integrity sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==
"@standard-schema/spec@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8"
integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==
"@surma/rollup-plugin-off-main-thread@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
@@ -7607,6 +7746,11 @@
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz#538b1e103bf8d9864e7b85cc96fa8d6fb6c40777"
integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==
"@vercel/oidc@3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@vercel/oidc/-/oidc-3.1.0.tgz#066caee449b84079f33c7445fc862464fe10ec32"
integrity sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==
"@vitejs/plugin-react@5.1.4":
version "5.1.4"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz#5b477e060bf612a7394c4febacc5de33a219b0e4"
@@ -7878,6 +8022,26 @@ agent-base@^7.1.0, agent-base@^7.1.2:
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8"
integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
ai@6.0.101, ai@^6.0.5:
version "6.0.101"
resolved "https://registry.yarnpkg.com/ai/-/ai-6.0.101.tgz#ddfc38440085efbd89624b52911d4dbef2a3fd6f"
integrity sha512-Ur/NgbgOp1rdhyDiKDk6EOpSgd1g5ADlbcD1cjQJtQsnmhEngz3Rf8nK5JetDh0vnbLy2aEBpaQeL+zvLRWuaA==
dependencies:
"@ai-sdk/gateway" "3.0.55"
"@ai-sdk/provider" "3.0.8"
"@ai-sdk/provider-utils" "4.0.15"
"@opentelemetry/api" "1.9.0"
ai@6.0.49:
version "6.0.49"
resolved "https://registry.yarnpkg.com/ai/-/ai-6.0.49.tgz#7db4d174af9ab8b51062ff1a935fbc32b127b30e"
integrity sha512-LABniBX/0R6Tv+iUK5keUZhZLaZUe4YjP5M2rZ4wAdZ8iKV3EfTAoJxuL1aaWTSJKIilKa9QUEkCgnp89/32bw==
dependencies:
"@ai-sdk/gateway" "3.0.22"
"@ai-sdk/provider" "3.0.5"
"@ai-sdk/provider-utils" "4.0.9"
"@opentelemetry/api" "1.9.0"
ajv-formats@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
@@ -9999,6 +10163,11 @@ events@^3.2.0, events@^3.3.0:
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
eventsource-parser@^3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.6.tgz#292e165e34cacbc936c3c92719ef326d4aeb4e90"
integrity sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==
execa@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@@ -13717,6 +13886,13 @@ prosemirror-changeset@^2.3.0:
dependencies:
prosemirror-transform "^1.0.0"
prosemirror-changeset@^2.3.1:
version "2.4.0"
resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz#8d8ea0290cb9545c298ec427ac3a8f298c39170f"
integrity sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==
dependencies:
prosemirror-transform "^1.0.0"
prosemirror-collab@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz#0e8c91e76e009b53457eb3b3051fb68dad029a33"
@@ -14252,7 +14428,7 @@ react-dnd@^14.0.3:
fast-deep-equal "^3.1.3"
hoist-non-react-statics "^3.3.2"
react-dom@*, react-dom@19.2.4:
react-dom@*, react-dom@19.2.4, react-dom@^19.2.3:
version "19.2.4"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.4.tgz#6fac6bd96f7db477d966c7ec17c1a2b1ad8e6591"
integrity sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==
@@ -14507,7 +14683,7 @@ react-window@^1.8.11:
"@babel/runtime" "^7.0.0"
memoize-one ">=3.1.1 <6"
react@*, react@19.2.4:
react@*, react@19.2.4, react@^19.2.3:
version "19.2.4"
resolved "https://registry.yarnpkg.com/react/-/react-19.2.4.tgz#438e57baa19b77cb23aab516cf635cd0579ee09a"
integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==
@@ -15717,6 +15893,14 @@ svgo@^3.0.2:
csso "^5.0.5"
picocolors "^1.0.0"
swr@^2.2.5:
version "2.4.0"
resolved "https://registry.yarnpkg.com/swr/-/swr-2.4.0.tgz#cd11e368cb13597f61ee3334428aa20b5e81f36e"
integrity sha512-sUlC20T8EOt1pHmDiqueUWMmRRX03W7w5YxovWX7VR2KHEPCTMly85x05vpkP5i6Bu4h44ePSMD9Tc+G2MItFw==
dependencies:
dequal "^2.0.3"
use-sync-external-store "^1.6.0"
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@@ -15821,6 +16005,11 @@ text-decoder@^1.1.0:
dependencies:
b4a "^1.6.4"
throttleit@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-2.1.0.tgz#a7e4aa0bf4845a5bd10daa39ea0c783f631a07b4"
integrity sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==
through2@^2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
@@ -17185,6 +17374,11 @@ yoga-layout@^3.2.1:
resolved "https://registry.yarnpkg.com/yoga-layout/-/yoga-layout-3.2.1.tgz#d2d1ba06f0e81c2eb650c3e5ad8b0b4adde1e843"
integrity sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==
zod@3.25.28:
version "3.25.28"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.28.tgz#8ab13d04afa05933598fd9fca32490ca92c7ea3a"
integrity sha512-/nt/67WYKnr5by3YS7LroZJbtcCBurDKKPBPWWzaxvVCGuG/NOsiKkrjoOhI8mJ+SQUXEbUzeB3S+6XDUEEj7Q==
zustand@5.0.11:
version "5.0.11"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.11.tgz#99f912e590de1ca9ce6c6d1cab6cdb1f034ab494"