diff --git a/CHANGELOG.md b/CHANGELOG.md index 2144bbf8..4dc767f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to ## Fixed +- 🐛(frontend) conditionally render AI button only when feature is enabled #814 - 🐛(backend) compute ancestor_links in get_abilities if needed #725 - 🔒️(back) restrict access to document accesses #801 diff --git a/env.d/development/common.dist b/env.d/development/common.dist index 0ce3a53e..4b1389bf 100644 --- a/env.d/development/common.dist +++ b/env.d/development/common.dist @@ -50,6 +50,7 @@ OIDC_REDIRECT_ALLOWED_HOSTS=["http://localhost:8083", "http://localhost:3000"] OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"} # AI +AI_FEATURE_ENABLED=true AI_BASE_URL=https://openaiendpoint.com AI_API_KEY=password AI_MODEL=llama diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index ea8d6be2..cdd6db84 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -1684,6 +1684,7 @@ class ConfigView(drf.views.APIView): Return a dictionary of public settings. """ array_settings = [ + "AI_FEATURE_ENABLED", "COLLABORATION_WS_URL", "CRISP_WEBSITE_ID", "ENVIRONMENT", diff --git a/src/backend/core/tests/test_api_config.py b/src/backend/core/tests/test_api_config.py index d535b927..cf2ad341 100644 --- a/src/backend/core/tests/test_api_config.py +++ b/src/backend/core/tests/test_api_config.py @@ -49,4 +49,5 @@ def test_api_config(is_authenticated): "MEDIA_BASE_URL": "http://testserver/", "POSTHOG_KEY": {"id": "132456", "host": "https://eu.i.posthog-test.com"}, "SENTRY_DSN": "https://sentry.test/123", + "AI_FEATURE_ENABLED": False, } diff --git a/src/backend/impress/settings.py b/src/backend/impress/settings.py index 205a2993..8b73fcca 100755 --- a/src/backend/impress/settings.py +++ b/src/backend/impress/settings.py @@ -528,6 +528,9 @@ class Base(Configuration): ) # AI service + AI_FEATURE_ENABLED = values.BooleanValue( + default=False, environ_name="AI_FEATURE_ENABLED", environ_prefix=None + ) AI_API_KEY = values.Value(None, environ_name="AI_API_KEY", environ_prefix=None) AI_BASE_URL = values.Value(None, environ_name="AI_BASE_URL", environ_prefix=None) AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None) diff --git a/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts index 43896ec1..dde84fdd 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts @@ -5,6 +5,7 @@ import { expect, test } from '@playwright/test'; import { createDoc, verifyDocName } from './common'; const config = { + AI_FEATURE_ENABLED: true, CRISP_WEBSITE_ID: null, COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/', ENVIRONMENT: 'development', @@ -117,6 +118,38 @@ 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 page.route('**/api/v1.0/config/', async (route) => { + const request = route.request(); + if (request.method().includes('GET')) { + await route.fulfill({ + json: { + ...config, + AI_FEATURE_ENABLED: false, + }, + }); + } else { + await route.continue(); + } + }); + + await page.goto('/'); + + await createDoc(page, 'doc-ai-feature', browserName, 1); + + await page.locator('.bn-block-outer').last().fill('Anything'); + await page.getByText('Anything').dblclick(); + expect( + await page.locator('button[data-test="convertMarkdown"]').count(), + ).toBe(1); + expect(await page.locator('button[data-test="ai-actions"]').count()).toBe( + 0, + ); + }); + test('it checks that Crisp is trying to init from config endpoint', async ({ page, }) => { diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts index d451cd69..21c080aa 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts @@ -338,6 +338,7 @@ test.describe('Doc Editor', () => { ].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: [ @@ -364,11 +365,17 @@ test.describe('Doc Editor', () => { link_reach: 'public', link_role: 'editor', created_at: '2021-09-01T09:00:00Z', + title: '', }); - await goToGridDoc(page); + const [randomDoc] = await createDoc( + page, + 'doc-editor-ai', + browserName, + 1, + ); - await verifyDocName(page, 'Mocked document'); + await verifyDocName(page, randomDoc); await page.locator('.bn-block-outer').last().fill('Hello World'); diff --git a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx index 7f26e9a8..5c105c7a 100644 --- a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx +++ b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx @@ -14,6 +14,7 @@ interface ConfigResponse { MEDIA_BASE_URL?: string; POSTHOG_KEY?: PostHogConf; SENTRY_DSN?: string; + AI_FEATURE_ENABLED?: boolean; } export const getConfig = async (): Promise => { diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/BlockNoteToolbar.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/BlockNoteToolbar.tsx index 4d9e340b..37632f7c 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/BlockNoteToolbar.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/BlockNoteToolbar.tsx @@ -8,6 +8,8 @@ import { import React, { JSX, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useConfig } from '@/core/config/api'; + import { getQuoteFormattingToolbarItems } from '../custom-blocks'; import { AIGroupButton } from './AIButton'; @@ -20,6 +22,7 @@ export const BlockNoteToolbar = () => { const [confirmOpen, setIsConfirmOpen] = useState(false); const [onConfirm, setOnConfirm] = useState<() => void | Promise>(); const { t } = useTranslation(); + const { data: conf } = useConfig(); const toolbarItems = useMemo(() => { const toolbarItems = getFormattingToolbarItems([ @@ -56,13 +59,13 @@ export const BlockNoteToolbar = () => { {toolbarItems} {/* Extra button to do some AI powered actions */} - + {conf?.AI_FEATURE_ENABLED && } {/* Extra button to convert from markdown to json */} ); - }, [toolbarItems]); + }, [toolbarItems, conf?.AI_FEATURE_ENABLED]); return ( <>