Compare commits

...

6 Commits

Author SHA1 Message Date
Anthony LC
9f05c1b3c2 upgrade blocknote v3 2025-02-06 09:43:35 +01:00
Anthony LC
c1e07d5eb1 upgrade blocknote 2025-02-05 10:31:53 +01:00
Anthony LC
d235b32a2c deploy-docs-ai 2025-02-05 09:56:17 +01:00
Anthony LC
b277ca70eb fix model 2025-02-05 09:55:04 +01:00
Anthony LC
a0c5a911d7 fix lint 2025-02-05 09:44:19 +01:00
yousefed
6181e147ac blocknote ai 2025-02-04 21:47:44 +01:00
9 changed files with 1038 additions and 128 deletions

View File

@@ -6,6 +6,7 @@ on:
push: push:
branches: branches:
- 'main' - 'main'
- 'feature/blocknote-ai'
tags: tags:
- 'v*' - 'v*'
pull_request: pull_request:

View File

@@ -15,9 +15,14 @@
"test:watch": "jest --watch" "test:watch": "jest --watch"
}, },
"dependencies": { "dependencies": {
"@blocknote/core": "0.21.0", "ai": "^4.1.18",
"@blocknote/mantine": "0.21.0", "zod": "^3.24.1",
"@blocknote/react": "0.21.0", "@ai-sdk/openai": "^1.1.9",
"@blocknote/core": "*",
"@blocknote/mantine": "*",
"@blocknote/react": "*",
"@blocknote/xl-ai": "*",
"vitest": "^2.0.3",
"@blocknote/xl-docx-exporter": "0.21.0", "@blocknote/xl-docx-exporter": "0.21.0",
"@blocknote/xl-pdf-exporter": "0.21.0", "@blocknote/xl-pdf-exporter": "0.21.0",
"@gouvfr-lasuite/integration": "1.0.2", "@gouvfr-lasuite/integration": "1.0.2",

View File

@@ -95,7 +95,7 @@ export function AIGroupButton() {
return ( return (
<Components.Generic.Menu.Root> <Components.Generic.Menu.Root>
<Components.Generic.Menu.Trigger> <Components.Generic.Menu.Trigger>
<Components.FormattingToolbar.Button <Components.Toolbar.Button
className="bn-button bn-menu-item" className="bn-button bn-menu-item"
data-test="ai-actions" data-test="ai-actions"
label="AI" label="AI"

View File

@@ -1,12 +1,34 @@
import { Dictionary, locales } from '@blocknote/core'; import { createOpenAI } from '@ai-sdk/openai';
import {
BlockNoteEditor as BNEditor,
BlockConfig,
Dictionary,
InlineContentSchema,
StyleSchema,
filterSuggestionItems,
locales,
} from '@blocknote/core';
import '@blocknote/core/fonts/inter.css'; import '@blocknote/core/fonts/inter.css';
import { BlockNoteView } from '@blocknote/mantine'; import { BlockNoteView } from '@blocknote/mantine';
import '@blocknote/mantine/style.css'; import '@blocknote/mantine/style.css';
import { useCreateBlockNote } from '@blocknote/react'; import {
SuggestionMenuController,
getDefaultReactSlashMenuItems,
useCreateBlockNote,
} from '@blocknote/react';
import {
AIShowSelectionPlugin,
BlockNoteAIContextProvider,
BlockNoteAIUI,
locales as aiLocales,
createBlockNoteAIClient,
getAISlashMenuItems,
useBlockNoteAIContext,
} from '@blocknote/xl-ai';
import '@blocknote/xl-ai/style.css';
import { HocuspocusProvider } from '@hocuspocus/provider'; import { HocuspocusProvider } from '@hocuspocus/provider';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { Box, TextErrors } from '@/components'; import { Box, TextErrors } from '@/components';
@@ -17,95 +39,38 @@ import { useUploadFile } from '../hook';
import { useHeadings } from '../hook/useHeadings'; import { useHeadings } from '../hook/useHeadings';
import useSaveDoc from '../hook/useSaveDoc'; import useSaveDoc from '../hook/useSaveDoc';
import { useEditorStore } from '../stores'; import { useEditorStore } from '../stores';
import { cssEditor } from '../styles';
import { randomColor } from '../utils'; import { randomColor } from '../utils';
import { BlockNoteToolbar } from './BlockNoteToolbar'; import { BlockNoteToolbar } from './BlockNoteToolbar';
const cssEditor = (readonly: boolean) => css` const blocknoteAIClient = createBlockNoteAIClient({
&, apiKey: 'BLOCKNOTE-API-KEY-CURRENTLY-NOT-NEEDED',
& > .bn-container, baseURL: 'https://blocknote-esy4.onrender.com/ai',
& .ProseMirror { });
height: 100%;
.bn-side-menu[data-block-type='heading'][data-level='1'] { const model = createOpenAI({
height: 50px; baseURL: 'https://albert.api.staging.etalab.gouv.fr/v1',
} ...blocknoteAIClient.getProviderSettings('albert-etalab'),
.bn-side-menu[data-block-type='heading'][data-level='2'] { compatibility: 'compatible',
height: 43px; })('neuralmagic/Meta-Llama-3.1-70B-Instruct-FP8');
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 35px;
}
h1 {
font-size: 1.875rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.25rem;
}
a {
color: var(--c--theme--colors--greyscale-500);
cursor: pointer;
}
.bn-block-group
.bn-block-group
.bn-block-outer:not([data-prev-depth-changed]):before {
border-left: none;
}
}
.bn-editor { // We call the model via a proxy server (see above) that has the API key,
color: var(--c--theme--colors--greyscale-700); // but we could also call the model directly from the frontend.
} // i.e., this should work as well (but it would leak your albert key to the frontend):
/*
return createOpenAI({
baseURL: 'https://albert.api.staging.etalab.gouv.fr/v1',
apiKey: 'ALBERT-API-KEY',
compatibility: 'compatible',
})('albert-etalab/neuralmagic/Meta-Llama-3.1-70B-Instruct-FP8');
*/
.bn-block-outer:not(:first-child) { export type DocsBlockNoteEditor = BNEditor<
&:has(h1) { Record<string, BlockConfig>,
padding-top: 32px; InlineContentSchema,
} StyleSchema
&:has(h2) { >;
padding-top: 24px;
}
&:has(h3) {
padding-top: 16px;
}
}
& .bn-inline-content code {
background-color: gainsboro;
padding: 2px;
border-radius: 4px;
}
@media screen and (width <= 560px) {
& .bn-editor {
${readonly && `padding-left: 10px;`}
}
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 46px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 40px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 40px;
}
& .bn-editor h1 {
font-size: 1.6rem;
}
& .bn-editor h2 {
font-size: 1.35rem;
}
& .bn-editor h3 {
font-size: 1.2rem;
}
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
font-size: 14px;
}
}
`;
interface BlockNoteEditorProps { interface BlockNoteEditorProps {
doc: Doc; doc: Doc;
@@ -130,6 +95,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
const editor = useCreateBlockNote( const editor = useCreateBlockNote(
{ {
_extensions: {
aiSelection: new AIShowSelectionPlugin(),
},
collaboration: { collaboration: {
provider, provider,
fragment: provider.document.getXmlFragment('document-store'), fragment: provider.document.getXmlFragment('document-store'),
@@ -163,7 +132,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
return cursor; return cursor;
}, },
}, },
dictionary: locales[lang as keyof typeof locales] as Dictionary, dictionary: {
...(locales[lang as keyof typeof locales] as Dictionary),
ai: aiLocales['en'] as unknown as Dictionary,
},
uploadFile, uploadFile,
}, },
[collabName, lang, provider, uploadFile], [collabName, lang, provider, uploadFile],
@@ -199,13 +171,42 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
formattingToolbar={false} formattingToolbar={false}
editable={!readOnly} editable={!readOnly}
theme="light" theme="light"
slashMenu={false}
> >
<BlockNoteToolbar /> <BlockNoteAIContextProvider
model={model}
dataFormat="markdown"
stream={false}
>
<BlockNoteAIUI />
<BlockNoteToolbar />
<SuggestionMenu editor={editor as unknown as DocsBlockNoteEditor} />
</BlockNoteAIContextProvider>
</BlockNoteView> </BlockNoteView>
</Box> </Box>
); );
}; };
function SuggestionMenu(props: { editor: DocsBlockNoteEditor }) {
const ctx = useBlockNoteAIContext();
return (
<SuggestionMenuController
triggerCharacter="/"
getItems={async (query) =>
Promise.resolve(
filterSuggestionItems(
[
...getDefaultReactSlashMenuItems(props.editor),
...getAISlashMenuItems(props.editor, ctx),
],
query,
),
)
}
/>
);
}
interface BlockNoteEditorVersionProps { interface BlockNoteEditorVersionProps {
initialContent: Y.XmlFragment; initialContent: Y.XmlFragment;
} }

View File

@@ -5,9 +5,9 @@ import {
FormattingToolbarProps, FormattingToolbarProps,
getFormattingToolbarItems, getFormattingToolbarItems,
} from '@blocknote/react'; } from '@blocknote/react';
import React, { useCallback } from 'react'; import { AIToolbarButton } from '@blocknote/xl-ai';
import { useCallback } from 'react';
import { AIGroupButton } from './AIButton';
import { MarkdownButton } from './MarkdownButton'; import { MarkdownButton } from './MarkdownButton';
export const BlockNoteToolbar = () => { export const BlockNoteToolbar = () => {
@@ -17,7 +17,8 @@ export const BlockNoteToolbar = () => {
{getFormattingToolbarItems(blockTypeSelectItems)} {getFormattingToolbarItems(blockTypeSelectItems)}
{/* Extra button to do some AI powered actions */} {/* Extra button to do some AI powered actions */}
<AIGroupButton key="AIButton" /> {/* <AIGroupButton key="AIButton" /> */}
<AIToolbarButton key="AIButton" />
{/* Extra button to convert from markdown to json */} {/* Extra button to convert from markdown to json */}
<MarkdownButton key="customButton" /> <MarkdownButton key="customButton" />

View File

@@ -5,7 +5,7 @@ import {
useSelectedBlocks, useSelectedBlocks,
} from '@blocknote/react'; } from '@blocknote/react';
import { forEach, isArray } from 'lodash'; import { forEach, isArray } from 'lodash';
import React, { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
type Block = { type Block = {
@@ -80,11 +80,11 @@ export function MarkdownButton() {
} }
return ( return (
<Components.FormattingToolbar.Button <Components.Toolbar.Button
mainTooltip={t('Convert Markdown')} mainTooltip={t('Convert Markdown')}
onClick={handleConvertMarkdown} onClick={handleConvertMarkdown}
> >
M M
</Components.FormattingToolbar.Button> </Components.Toolbar.Button>
); );
} }

View File

@@ -0,0 +1,87 @@
import { css } from 'styled-components';
export const cssEditor = (readonly: boolean) => css`
&,
& > .bn-container,
& .ProseMirror {
height: 100%;
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 50px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 43px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 35px;
}
h1 {
font-size: 1.875rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.25rem;
}
a {
color: var(--c--theme--colors--greyscale-500);
cursor: pointer;
}
.bn-block-group
.bn-block-group
.bn-block-outer:not([data-prev-depth-changed]):before {
border-left: none;
}
}
.bn-editor {
color: var(--c--theme--colors--greyscale-700);
}
.bn-block-outer:not(:first-child) {
&:has(h1) {
padding-top: 32px;
}
&:has(h2) {
padding-top: 24px;
}
&:has(h3) {
padding-top: 16px;
}
}
& .bn-inline-content code {
background-color: gainsboro;
padding: 2px;
border-radius: 4px;
}
@media screen and (width <= 560px) {
& .bn-editor {
${readonly && `padding-left: 10px;`}
}
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 46px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 40px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 40px;
}
& .bn-editor h1 {
font-size: 1.6rem;
}
& .bn-editor h2 {
font-size: 1.35rem;
}
& .bn-editor h3 {
font-size: 1.2rem;
}
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
font-size: 14px;
}
}
`;

View File

@@ -35,6 +35,10 @@
"eslint": "8.57.0", "eslint": "8.57.0",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"typescript": "5.7.3" "typescript": "5.7.3",
"@blocknote/core": "https://gitpkg.vercel.app/typecellOS/blocknote/packages/core?ai-block-built&v=3",
"@blocknote/mantine": "https://gitpkg.vercel.app/typecellOS/blocknote/packages/mantine?ai-block-built&v=3",
"@blocknote/react": "https://gitpkg.vercel.app/typecellOS/blocknote/packages/react?ai-block-built&v=3",
"@blocknote/xl-ai": "https://gitpkg.vercel.app/typecellOS/blocknote/packages/xl-ai?ai-block-built&v=3"
} }
} }

File diff suppressed because it is too large Load Diff