🚩(project) add more backend AI feature flags

The Blocknote AI feature is a bit flaky, we want
to be able to disable it if to much issues arise,
without having to do a new release.
We add a bunch of feature flags to be able to
disable the AI features if needed:
- add AI_FEATURE_BLOCKNOTE_ENABLED, to display
or not the feature powered by blocknote
- add AI_FEATURE_LEGACY_ENABLED, to display or not
the legacy AI features
This commit is contained in:
Anthony LC
2026-02-26 11:47:25 +01:00
parent 24ec1fa70e
commit 1070b91d2f
16 changed files with 397 additions and 391 deletions

View File

@@ -13,6 +13,8 @@ These are the environment variables you can set for the `impress-backend` contai
| AI_BASE_URL | OpenAI compatible AI base url | |
| AI_BOT | Information to give to the frontend about the AI bot | { "name": "Docs AI", "color": "#8bc6ff" }
| AI_FEATURE_ENABLED | Enable AI options | false |
| AI_FEATURE_BLOCKNOTE_ENABLED | Enable Blocknote AI options | false |
| AI_FEATURE_LEGACY_ENABLED | Enable legacyAI options | true |
| AI_MODEL | AI Model to use | |
| AI_VERCEL_SDK_VERSION | The vercel AI SDK version used | 6 |
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |

View File

@@ -138,6 +138,8 @@ AI is disabled by default. To enable it, the following environment variables mus
```env
AI_FEATURE_ENABLED=true # is false by default
AI_FEATURE_BLOCKNOTE_ENABLED=true # is false by default
AI_FEATURE_LEGACY_ENABLED=true # is true by default, AI_FEATURE_ENABLED must be set to true to enable it
AI_BASE_URL=https://openaiendpoint.com
AI_API_KEY=<API key>
AI_MODEL=<model used> e.g. llama

View File

@@ -64,6 +64,8 @@ USER_RECONCILIATION_FORM_URL=http://localhost:3000
# AI
AI_FEATURE_ENABLED=true
AI_FEATURE_BLOCKNOTE_ENABLED=true
AI_FEATURE_LEGACY_ENABLED=true
AI_BASE_URL=https://openaiendpoint.com
AI_API_KEY=password
AI_MODEL=llama

View File

@@ -58,6 +58,8 @@ OIDC_REDIRECT_ALLOWED_HOSTS=["https://${DOCS_HOST}"]
# AI
#AI_FEATURE_ENABLED=true # is false by default
#AI_FEATURE_BLOCKNOTE_ENABLED=true # is false by default
#AI_FEATURE_LEGACY_ENABLED=true # is true by default, AI_FEATURE_ENABLED must be set to true to enable it
#AI_BASE_URL=https://openaiendpoint.com
#AI_API_KEY=<API key>
#AI_MODEL=<model used> e.g. llama

View File

@@ -1852,7 +1852,7 @@ class DocumentViewSet(
# Check permissions first
self.get_object()
if not settings.AI_FEATURE_ENABLED:
if not settings.AI_FEATURE_ENABLED or not settings.AI_FEATURE_BLOCKNOTE_ENABLED:
raise ValidationError("AI feature is not enabled.")
ai_service = AIService()
@@ -2572,6 +2572,8 @@ class ConfigView(drf.views.APIView):
array_settings = [
"AI_BOT",
"AI_FEATURE_ENABLED",
"AI_FEATURE_BLOCKNOTE_ENABLED",
"AI_FEATURE_LEGACY_ENABLED",
"API_USERS_SEARCH_QUERY_MIN_LENGTH",
"COLLABORATION_WS_URL",
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY",

View File

@@ -23,6 +23,8 @@ def ai_settings(settings):
settings.AI_BASE_URL = "http://localhost-ai:12345/"
settings.AI_API_KEY = "test-key"
settings.AI_FEATURE_ENABLED = True
settings.AI_FEATURE_BLOCKNOTE_ENABLED = True
settings.AI_FEATURE_LEGACY_ENABLED = True
settings.LANGFUSE_PUBLIC_KEY = None
settings.AI_VERCEL_SDK_VERSION = 6
@@ -239,9 +241,12 @@ def test_api_documents_ai_proxy_success(mock_stream, via, role, mock_user_teams)
mock_stream.assert_called_once()
def test_api_documents_ai_proxy_ai_feature_disabled(settings):
@pytest.mark.parametrize(
"setting_to_disable", ["AI_FEATURE_ENABLED", "AI_FEATURE_BLOCKNOTE_ENABLED"]
)
def test_api_documents_ai_proxy_ai_feature_disabled(settings, setting_to_disable):
"""When AI_FEATURE_ENABLED is False, the endpoint returns 400."""
settings.AI_FEATURE_ENABLED = False
setattr(settings, setting_to_disable, False)
user = factories.UserFactory()

View File

@@ -21,6 +21,8 @@ pytestmark = pytest.mark.django_db
@override_settings(
AI_BOT={"name": "Test Bot", "color": "#000000"},
AI_FEATURE_ENABLED=False,
AI_FEATURE_BLOCKNOTE_ENABLED=False,
AI_FEATURE_LEGACY_ENABLED=False,
API_USERS_SEARCH_QUERY_MIN_LENGTH=6,
COLLABORATION_WS_URL="http://testcollab/",
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY=True,
@@ -47,6 +49,8 @@ def test_api_config(is_authenticated):
assert response.json() == {
"AI_BOT": {"name": "Test Bot", "color": "#000000"},
"AI_FEATURE_ENABLED": False,
"AI_FEATURE_BLOCKNOTE_ENABLED": False,
"AI_FEATURE_LEGACY_ENABLED": False,
"API_USERS_SEARCH_QUERY_MIN_LENGTH": 6,
"COLLABORATION_WS_URL": "http://testcollab/",
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY": True,

View File

@@ -29,6 +29,8 @@ def ai_settings(settings):
settings.AI_BASE_URL = "http://example.com"
settings.AI_API_KEY = "test-key"
settings.AI_FEATURE_ENABLED = True
settings.AI_FEATURE_BLOCKNOTE_ENABLED = True
settings.AI_FEATURE_LEGACY_ENABLED = True
settings.LANGFUSE_PUBLIC_KEY = None
settings.AI_VERCEL_SDK_VERSION = 6

View File

@@ -710,9 +710,22 @@ class Base(Configuration):
"hour": 100,
"day": 500,
}
# Master settings to enable AI features, if you set it to False,
# all AI features will be disabled even if the other settings are enabled.
AI_FEATURE_ENABLED = values.BooleanValue(
default=False, environ_name="AI_FEATURE_ENABLED", environ_prefix=None
)
# Far better UI but more flaky for the moment
# ⚠️ AGPL license, be sure to comply with the Blocknote license
# if you enable it (https://www.blocknotejs.org/)
AI_FEATURE_BLOCKNOTE_ENABLED = values.BooleanValue(
default=False, environ_name="AI_FEATURE_BLOCKNOTE_ENABLED", environ_prefix=None
)
# UI with less features but more stable
# MIT friendly license, you can enable it without worrying about the license
AI_FEATURE_LEGACY_ENABLED = values.BooleanValue(
default=True, environ_name="AI_FEATURE_LEGACY_ENABLED", environ_prefix=None
)
AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None)
AI_VERCEL_SDK_VERSION = values.IntegerValue(
6, environ_name="AI_VERCEL_SDK_VERSION", environ_prefix=None

View File

@@ -76,26 +76,6 @@ test.describe('Config', () => {
expect(webSocket.url()).toContain('ws://localhost:4444/collaboration/ws/');
});
test('it checks the AI feature flag from config endpoint', async ({
page,
browserName,
}) => {
await overrideConfig(page, {
AI_FEATURE_ENABLED: false,
});
await page.goto('/');
await createDoc(page, 'doc-ai-feature', browserName, 1);
await page.locator('.bn-block-outer').last().fill('Anything');
await page.getByText('Anything').selectText();
expect(
await page.locator('button[data-test="convertMarkdown"]').count(),
).toBe(1);
await expect(page.getByRole('button', { name: 'Ask AI' })).toBeHidden();
});
test('it checks that Crisp is trying to init from config endpoint', async ({
page,
}) => {

View File

@@ -0,0 +1,339 @@
/* eslint-disable playwright/no-conditional-expect */
import { expect, test } from '@playwright/test';
import {
createDoc,
mockedDocument,
overrideConfig,
verifyDocName,
} from './utils-common';
import {
mockAIResponse,
openSuggestionMenu,
writeInEditor,
} from './utils-editor';
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('Doc AI feature', () => {
[
{
AI_FEATURE_ENABLED: false,
selector: 'Ask AI',
},
{
AI_FEATURE_ENABLED: true,
AI_FEATURE_BLOCKNOTE_ENABLED: false,
selector: 'Ask AI',
},
{
AI_FEATURE_ENABLED: true,
AI_FEATURE_LEGACY_ENABLED: false,
selector: 'AI',
},
].forEach((config) => {
test(`it checks the AI feature flag from config endpoint: ${JSON.stringify(config)}`, async ({
page,
browserName,
}) => {
await overrideConfig(page, config);
await page.goto('/');
await createDoc(page, 'doc-ai-feature', browserName, 1);
await page.locator('.bn-block-outer').last().fill('Anything');
await page.getByText('Anything').selectText();
expect(
await page.locator('button[data-test="convertMarkdown"]').count(),
).toBe(1);
await expect(
page.getByRole('button', { name: config.selector, exact: true }),
).toBeHidden();
});
});
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 legacy', async ({
page,
browserName,
}) => {
await page.route(/.*\/ai-translate\//, async (route) => {
const request = route.request();
if (request.method().includes('POST')) {
await route.fulfill({
json: {
answer: 'Hallo Welt',
},
});
} 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', exact: true }).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: 'German', exact: true }).click();
await expect(editor.getByText('Hallo Welt')).toBeVisible();
});
[
{ ai_transform: false, ai_translate: false },
{ ai_transform: true, ai_translate: false },
{ ai_transform: false, ai_translate: true },
].forEach(({ ai_transform, ai_translate }) => {
test(`it checks AI buttons when can transform is at "${ai_transform}" and can translate is at "${ai_translate}"`, async ({
page,
browserName,
}) => {
await mockedDocument(page, {
accesses: [
{
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
role: 'owner',
user: {
email: 'super@owner.com',
full_name: 'Super Owner',
},
},
],
abilities: {
destroy: true, // Means owner
link_configuration: true,
ai_transform,
ai_translate,
accesses_manage: true,
accesses_view: true,
update: true,
partial_update: true,
retrieve: true,
},
link_reach: 'restricted',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
title: '',
});
const [randomDoc] = await createDoc(
page,
'doc-editor-ai',
browserName,
1,
);
await verifyDocName(page, randomDoc);
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
if (!ai_transform && !ai_translate) {
await expect(
page.getByRole('button', { name: 'AI', exact: true }),
).toBeHidden();
return;
}
await page.getByRole('button', { name: 'AI', exact: true }).click();
if (ai_transform) {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeHidden();
}
if (ai_translate) {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeHidden();
}
});
});
test(`it checks ai_proxy ability`, async ({ page, browserName }) => {
await mockedDocument(page, {
accesses: [
{
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
role: 'owner',
user: {
email: 'super@owner.com',
full_name: 'Super Owner',
},
},
],
abilities: {
destroy: true, // Means owner
link_configuration: true,
ai_proxy: false,
accesses_manage: true,
accesses_view: true,
update: true,
partial_update: true,
retrieve: true,
},
link_reach: 'restricted',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
title: '',
});
const [randomDoc] = await createDoc(
page,
'doc-editor-ai-proxy',
browserName,
1,
);
await verifyDocName(page, randomDoc);
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
await expect(page.getByRole('button', { name: 'Ask AI' })).toBeHidden();
await page.locator('.bn-block-outer').last().fill('/');
await expect(page.getByText('Write with AI')).toBeHidden();
});
});

View File

@@ -1,4 +1,3 @@
/* eslint-disable playwright/no-conditional-expect */
import path from 'path';
import { expect, test } from '@playwright/test';
@@ -7,16 +6,10 @@ import cs from 'convert-stream';
import {
createDoc,
goToGridDoc,
mockedDocument,
overrideConfig,
verifyDocName,
} from './utils-common';
import {
getEditor,
mockAIResponse,
openSuggestionMenu,
writeInEditor,
} from './utils-editor';
import { getEditor, openSuggestionMenu, writeInEditor } from './utils-editor';
import { connectOtherUserToDoc, updateShareLink } from './utils-share';
import {
createRootSubPage,
@@ -390,359 +383,6 @@ 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();
});
/**
* We have to skip this test for the CI for now, we cannot assert
* it because of `process.env.NEXT_PUBLIC_PUBLISH_AS_MIT` that is set
* at build time.
* It can be interesting to keep it for local tests.
*/
test.skip('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();
});
/**
* We have to skip this test for the CI for now, we cannot assert
* it because of `process.env.NEXT_PUBLIC_PUBLISH_AS_MIT` that is set
* at build time.
* It can be interesting to keep it for local tests.
*/
test.skip('it checks the AI buttons', 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', exact: true }).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();
});
/**
* We have to skip this test for the CI for now, we cannot assert
* it because of `process.env.NEXT_PUBLIC_PUBLISH_AS_MIT` that is set
* at build time.
* It can be interesting to keep it for local tests.
*/
[
{ ai_transform: false, ai_translate: false },
{ ai_transform: true, ai_translate: false },
{ ai_transform: false, ai_translate: true },
].forEach(({ ai_transform, ai_translate }) => {
test.skip(`it checks AI buttons when can transform is at "${ai_transform}" and can translate is at "${ai_translate}"`, async ({
page,
browserName,
}) => {
await mockedDocument(page, {
accesses: [
{
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
role: 'owner',
user: {
email: 'super@owner.com',
full_name: 'Super Owner',
},
},
],
abilities: {
destroy: true, // Means owner
link_configuration: true,
ai_transform,
ai_translate,
accesses_manage: true,
accesses_view: true,
update: true,
partial_update: true,
retrieve: true,
},
link_reach: 'restricted',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
title: '',
});
const [randomDoc] = await createDoc(
page,
'doc-editor-ai',
browserName,
1,
);
await verifyDocName(page, randomDoc);
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
if (!ai_transform && !ai_translate) {
await expect(
page.getByRole('button', { name: 'AI', exact: true }),
).toBeHidden();
return;
}
await page.getByRole('button', { name: 'AI', exact: true }).click();
if (ai_transform) {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeHidden();
}
if (ai_translate) {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeHidden();
}
});
});
test(`it checks ai_proxy ability`, async ({ page, browserName }) => {
await mockedDocument(page, {
accesses: [
{
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
role: 'owner',
user: {
email: 'super@owner.com',
full_name: 'Super Owner',
},
},
],
abilities: {
destroy: true, // Means owner
link_configuration: true,
ai_proxy: false,
accesses_manage: true,
accesses_view: true,
update: true,
partial_update: true,
retrieve: true,
},
link_reach: 'restricted',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
title: '',
});
const [randomDoc] = await createDoc(
page,
'doc-editor-ai-proxy',
browserName,
1,
);
await verifyDocName(page, randomDoc);
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
await expect(page.getByRole('button', { name: 'Ask AI' })).toBeHidden();
await page.locator('.bn-block-outer').last().fill('/');
await expect(page.getByText('Write with AI')).toBeHidden();
});
test('it downloads unsafe files', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);

View File

@@ -14,6 +14,8 @@ export const CONFIG = {
color: '#8bc6ff',
},
AI_FEATURE_ENABLED: true,
AI_FEATURE_BLOCKNOTE_ENABLED: true,
AI_FEATURE_LEGACY_ENABLED: true,
API_USERS_SEARCH_QUERY_MIN_LENGTH: 3,
CRISP_WEBSITE_ID: null,
COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/',

View File

@@ -29,6 +29,8 @@ interface ThemeCustomization {
export interface ConfigResponse {
AI_BOT: { name: string; color: string };
AI_FEATURE_ENABLED?: boolean;
AI_FEATURE_BLOCKNOTE_ENABLED?: boolean;
AI_FEATURE_LEGACY_ENABLED?: boolean;
API_USERS_SEARCH_QUERY_MIN_LENGTH?: number;
COLLABORATION_WS_URL?: string;
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY?: boolean;

View File

@@ -105,8 +105,12 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
const conf = useConfig().data;
const aiAllowed = !!(conf?.AI_FEATURE_ENABLED && doc.abilities?.ai_proxy);
const aiExtension = useAI?.(doc.id, aiAllowed);
const aiBlockNoteAllowed = !!(
conf?.AI_FEATURE_ENABLED &&
conf?.AI_FEATURE_BLOCKNOTE_ENABLED &&
doc.abilities?.ai_proxy
);
const aiExtension = useAI?.(doc.id, aiBlockNoteAllowed);
const collabName = user?.full_name || user?.email;
const cursorName = collabName || t('Anonymous');
@@ -268,11 +272,11 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
comments={showComments}
aria-label={t('Document editor')}
>
{aiAllowed && AIMenuController && AIMenu && (
{aiBlockNoteAllowed && AIMenuController && AIMenu && (
<AIMenuController aiMenu={AIMenu} />
)}
<BlockNoteSuggestionMenu aiAllowed={aiAllowed} />
<BlockNoteToolbar aiAllowed={aiAllowed} />
<BlockNoteSuggestionMenu aiAllowed={aiBlockNoteAllowed} />
<BlockNoteToolbar aiAllowed={aiBlockNoteAllowed} />
</BlockNoteView>
</Box>
);

View File

@@ -79,7 +79,7 @@ export const BlockNoteToolbar = ({ aiAllowed }: { aiAllowed: boolean }) => {
{toolbarItems}
{/* Extra button to do some AI powered actions - only if AIToolbarButton is not available because of MIT license */}
{conf?.AI_FEATURE_ENABLED && !AIToolbarButton && (
{conf?.AI_FEATURE_ENABLED && conf?.AI_FEATURE_LEGACY_ENABLED && (
<AIGroupButton key="AIButton" />
)}
@@ -87,7 +87,12 @@ export const BlockNoteToolbar = ({ aiAllowed }: { aiAllowed: boolean }) => {
<MarkdownButton key="customButton" />
</FormattingToolbar>
);
}, [toolbarItems, aiAllowed, conf?.AI_FEATURE_ENABLED]);
}, [
toolbarItems,
aiAllowed,
conf?.AI_FEATURE_ENABLED,
conf?.AI_FEATURE_LEGACY_ENABLED,
]);
return (
<>