mirror of
https://github.com/suitenumerique/docs.git
synced 2026-04-25 17:15:01 +02:00
Compare commits
6 Commits
fix/link-p
...
feature/bl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f05c1b3c2 | ||
|
|
c1e07d5eb1 | ||
|
|
d235b32a2c | ||
|
|
b277ca70eb | ||
|
|
a0c5a911d7 | ||
|
|
6181e147ac |
1
.github/workflows/docker-hub.yml
vendored
1
.github/workflows/docker-hub.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'feature/blocknote-ai'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 { 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';
|
||||
import { css } from 'styled-components';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Box, TextErrors } from '@/components';
|
||||
@@ -17,95 +39,38 @@ import { useUploadFile } from '../hook';
|
||||
import { useHeadings } from '../hook/useHeadings';
|
||||
import useSaveDoc from '../hook/useSaveDoc';
|
||||
import { useEditorStore } from '../stores';
|
||||
import { cssEditor } from '../styles';
|
||||
import { randomColor } from '../utils';
|
||||
|
||||
import { BlockNoteToolbar } from './BlockNoteToolbar';
|
||||
|
||||
const cssEditor = (readonly: boolean) => css`
|
||||
&,
|
||||
& > .bn-container,
|
||||
& .ProseMirror {
|
||||
height: 100%;
|
||||
const blocknoteAIClient = createBlockNoteAIClient({
|
||||
apiKey: 'BLOCKNOTE-API-KEY-CURRENTLY-NOT-NEEDED',
|
||||
baseURL: 'https://blocknote-esy4.onrender.com/ai',
|
||||
});
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
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');
|
||||
|
||||
.bn-editor {
|
||||
color: var(--c--theme--colors--greyscale-700);
|
||||
}
|
||||
// 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');
|
||||
*/
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type DocsBlockNoteEditor = BNEditor<
|
||||
Record<string, BlockConfig>,
|
||||
InlineContentSchema,
|
||||
StyleSchema
|
||||
>;
|
||||
|
||||
interface BlockNoteEditorProps {
|
||||
doc: Doc;
|
||||
@@ -130,6 +95,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
|
||||
const editor = useCreateBlockNote(
|
||||
{
|
||||
_extensions: {
|
||||
aiSelection: new AIShowSelectionPlugin(),
|
||||
},
|
||||
|
||||
collaboration: {
|
||||
provider,
|
||||
fragment: provider.document.getXmlFragment('document-store'),
|
||||
@@ -163,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],
|
||||
@@ -199,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;
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user