Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f05c1b3c2 | |||
| c1e07d5eb1 | |||
| d235b32a2c | |||
| b277ca70eb | |||
| a0c5a911d7 | |||
| 6181e147ac |
@@ -6,6 +6,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'feature/blocknote-ai'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
@@ -10,14 +10,8 @@ and this project adheres to
|
||||
## [Unreleased]
|
||||
|
||||
## Added
|
||||
|
||||
- 📝(doc) Add security.md and codeofconduct.md #604
|
||||
|
||||
## Fixed
|
||||
|
||||
- 💄improve export spacings PDF #613
|
||||
|
||||
|
||||
## [2.1.0] - 2025-01-29
|
||||
|
||||
## Added
|
||||
|
||||
@@ -15,9 +15,14 @@
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocknote/core": "0.21.0",
|
||||
"@blocknote/mantine": "0.21.0",
|
||||
"@blocknote/react": "0.21.0",
|
||||
"ai": "^4.1.18",
|
||||
"zod": "^3.24.1",
|
||||
"@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-pdf-exporter": "0.21.0",
|
||||
"@gouvfr-lasuite/integration": "1.0.2",
|
||||
|
||||
@@ -95,7 +95,7 @@ export function AIGroupButton() {
|
||||
return (
|
||||
<Components.Generic.Menu.Root>
|
||||
<Components.Generic.Menu.Trigger>
|
||||
<Components.FormattingToolbar.Button
|
||||
<Components.Toolbar.Button
|
||||
className="bn-button bn-menu-item"
|
||||
data-test="ai-actions"
|
||||
label="AI"
|
||||
|
||||
+91
-4
@@ -1,8 +1,31 @@
|
||||
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 { BlockNoteView } from '@blocknote/mantine';
|
||||
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 { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -21,6 +44,34 @@ import { randomColor } from '../utils';
|
||||
|
||||
import { BlockNoteToolbar } from './BlockNoteToolbar';
|
||||
|
||||
const blocknoteAIClient = createBlockNoteAIClient({
|
||||
apiKey: 'BLOCKNOTE-API-KEY-CURRENTLY-NOT-NEEDED',
|
||||
baseURL: 'https://blocknote-esy4.onrender.com/ai',
|
||||
});
|
||||
|
||||
const model = createOpenAI({
|
||||
baseURL: 'https://albert.api.staging.etalab.gouv.fr/v1',
|
||||
...blocknoteAIClient.getProviderSettings('albert-etalab'),
|
||||
compatibility: 'compatible',
|
||||
})('neuralmagic/Meta-Llama-3.1-70B-Instruct-FP8');
|
||||
|
||||
// We call the model via a proxy server (see above) that has the API key,
|
||||
// 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');
|
||||
*/
|
||||
|
||||
export type DocsBlockNoteEditor = BNEditor<
|
||||
Record<string, BlockConfig>,
|
||||
InlineContentSchema,
|
||||
StyleSchema
|
||||
>;
|
||||
|
||||
interface BlockNoteEditorProps {
|
||||
doc: Doc;
|
||||
provider: HocuspocusProvider;
|
||||
@@ -44,6 +95,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
|
||||
const editor = useCreateBlockNote(
|
||||
{
|
||||
_extensions: {
|
||||
aiSelection: new AIShowSelectionPlugin(),
|
||||
},
|
||||
|
||||
collaboration: {
|
||||
provider,
|
||||
fragment: provider.document.getXmlFragment('document-store'),
|
||||
@@ -77,7 +132,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
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,
|
||||
},
|
||||
[collabName, lang, provider, uploadFile],
|
||||
@@ -113,13 +171,42 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
formattingToolbar={false}
|
||||
editable={!readOnly}
|
||||
theme="light"
|
||||
slashMenu={false}
|
||||
>
|
||||
<BlockNoteToolbar />
|
||||
<BlockNoteAIContextProvider
|
||||
model={model}
|
||||
dataFormat="markdown"
|
||||
stream={false}
|
||||
>
|
||||
<BlockNoteAIUI />
|
||||
<BlockNoteToolbar />
|
||||
<SuggestionMenu editor={editor as unknown as DocsBlockNoteEditor} />
|
||||
</BlockNoteAIContextProvider>
|
||||
</BlockNoteView>
|
||||
</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 {
|
||||
initialContent: Y.XmlFragment;
|
||||
}
|
||||
|
||||
+4
-3
@@ -5,9 +5,9 @@ import {
|
||||
FormattingToolbarProps,
|
||||
getFormattingToolbarItems,
|
||||
} 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';
|
||||
|
||||
export const BlockNoteToolbar = () => {
|
||||
@@ -17,7 +17,8 @@ export const BlockNoteToolbar = () => {
|
||||
{getFormattingToolbarItems(blockTypeSelectItems)}
|
||||
|
||||
{/* 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 */}
|
||||
<MarkdownButton key="customButton" />
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@ import {
|
||||
useSelectedBlocks,
|
||||
} from '@blocknote/react';
|
||||
import { forEach, isArray } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Block = {
|
||||
@@ -80,11 +80,11 @@ export function MarkdownButton() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Components.FormattingToolbar.Button
|
||||
<Components.Toolbar.Button
|
||||
mainTooltip={t('Convert Markdown')}
|
||||
onClick={handleConvertMarkdown}
|
||||
>
|
||||
M
|
||||
</Components.FormattingToolbar.Button>
|
||||
</Components.Toolbar.Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { BlockNoteEditor, BlockNoteSchema } from '@blocknote/core';
|
||||
|
||||
export interface DocAttachment {
|
||||
file: string;
|
||||
}
|
||||
@@ -14,19 +12,3 @@ export type HeadingBlock = {
|
||||
level: number;
|
||||
};
|
||||
};
|
||||
|
||||
export const blockNoteInstance = BlockNoteSchema.create();
|
||||
export type DocsBlockSchema = typeof blockNoteInstance.blockSchema;
|
||||
export type DocsInlineContentSchema =
|
||||
typeof blockNoteInstance.inlineContentSchema;
|
||||
export type DocsStyleSchema = typeof blockNoteInstance.styleSchema;
|
||||
export type DocsBlockNoteEditor = BlockNoteEditor<
|
||||
DocsBlockSchema,
|
||||
DocsInlineContentSchema,
|
||||
DocsStyleSchema
|
||||
>;
|
||||
export type DocsBlockNoteSchema = BlockNoteSchema<
|
||||
DocsBlockSchema,
|
||||
DocsInlineContentSchema,
|
||||
DocsStyleSchema
|
||||
>;
|
||||
|
||||
@@ -2,6 +2,10 @@ import {
|
||||
DOCXExporter,
|
||||
docxDefaultSchemaMappings,
|
||||
} from '@blocknote/xl-docx-exporter';
|
||||
import {
|
||||
PDFExporter,
|
||||
pdfDefaultSchemaMappings,
|
||||
} from '@blocknote/xl-pdf-exporter';
|
||||
import {
|
||||
Button,
|
||||
Loader,
|
||||
@@ -21,7 +25,6 @@ import { useEditorStore } from '@/features/docs/doc-editor';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
|
||||
import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
|
||||
import { DocsPDFExporter } from '../libs/DocsPDFExporter';
|
||||
import { downloadFile, exportResolveFileUrl } from '../utils';
|
||||
|
||||
enum DocDownloadFormat {
|
||||
@@ -88,12 +91,19 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
|
||||
let blobExport: Blob;
|
||||
if (format === DocDownloadFormat.PDF) {
|
||||
const defaultExporter = new DocsPDFExporter(editor.schema);
|
||||
const exporter = new DocsPDFExporter(editor.schema, {
|
||||
resolveFileUrl: async (url) =>
|
||||
exportResolveFileUrl(url, defaultExporter.options.resolveFileUrl),
|
||||
});
|
||||
const defaultExporter = new PDFExporter(
|
||||
editor.schema,
|
||||
pdfDefaultSchemaMappings,
|
||||
);
|
||||
|
||||
const exporter = new PDFExporter(
|
||||
editor.schema,
|
||||
pdfDefaultSchemaMappings,
|
||||
{
|
||||
resolveFileUrl: async (url) =>
|
||||
exportResolveFileUrl(url, defaultExporter.options.resolveFileUrl),
|
||||
},
|
||||
);
|
||||
const pdfDocument = await exporter.toReactPDFDocument(exportDocument);
|
||||
blobExport = await pdf(pdfDocument).toBlob();
|
||||
} else {
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { Block, DefaultProps, ExporterOptions } from '@blocknote/core';
|
||||
import {
|
||||
PDFExporter,
|
||||
pdfDefaultSchemaMappings,
|
||||
} from '@blocknote/xl-pdf-exporter';
|
||||
import { Font } from '@react-pdf/renderer';
|
||||
|
||||
import {
|
||||
DocsBlockNoteSchema,
|
||||
DocsBlockSchema,
|
||||
DocsInlineContentSchema,
|
||||
DocsStyleSchema,
|
||||
} from '@/features/docs/doc-editor';
|
||||
|
||||
type Options = ExporterOptions & {
|
||||
emojiSource: false | ReturnType<typeof Font.getEmojiSource>;
|
||||
};
|
||||
|
||||
type DocsDefaultProps = DefaultProps & {
|
||||
level?: number;
|
||||
};
|
||||
|
||||
export class DocsPDFExporter extends PDFExporter<
|
||||
DocsBlockSchema,
|
||||
DocsStyleSchema,
|
||||
DocsInlineContentSchema
|
||||
> {
|
||||
constructor(
|
||||
protected readonly schemaMappings: DocsBlockNoteSchema,
|
||||
options?: Partial<Options>,
|
||||
) {
|
||||
super(schemaMappings, pdfDefaultSchemaMappings, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Breaklines are not displayed in PDFs, by adding a space we ensure that the line is not ignored
|
||||
* @param blocks
|
||||
* @param nestingLevel
|
||||
* @returns
|
||||
*/
|
||||
public transformBlocks(
|
||||
blocks: Block<DocsBlockSchema, DocsInlineContentSchema, DocsStyleSchema>[], // Or BlockFromConfig<B[keyof B], I, S>?
|
||||
nestingLevel?: number,
|
||||
) {
|
||||
blocks.forEach((block) => {
|
||||
if (Array.isArray(block.content)) {
|
||||
block.content.forEach((content) => {
|
||||
if (content.type === 'text' && !content.text) {
|
||||
content.text = ' ';
|
||||
}
|
||||
});
|
||||
|
||||
if (!block.content.length) {
|
||||
block.content.push({
|
||||
styles: {},
|
||||
text: ' ',
|
||||
type: 'text',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return super.transformBlocks(blocks, nestingLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the method to add our custom styles
|
||||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
public blocknoteDefaultPropsToReactPDFStyle(
|
||||
props: Partial<DocsDefaultProps>,
|
||||
) {
|
||||
let styles = super.blocknoteDefaultPropsToReactPDFStyle(props);
|
||||
|
||||
// Add margin to headings
|
||||
if (props.level) {
|
||||
styles = {
|
||||
marginTop: 15,
|
||||
marginBottom: 15,
|
||||
...styles,
|
||||
};
|
||||
}
|
||||
|
||||
return styles;
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,10 @@
|
||||
"eslint": "8.57.0",
|
||||
"react": "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"
|
||||
}
|
||||
}
|
||||
|
||||
+841
-30
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user