From f2ed8e0ea183f323a8f50e74892d9c8e14061cf1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Mar 2025 23:21:26 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(frontend)=20conditionally=20render?= =?UTF-8?q?=20AI=20button=20in=20toolbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added a feature flag check to ensure the AIGroupButton is only rendered when AI_FEATURE_ENABLED is explicitly set to "true". This prevents the AI button from appearing when the feature is not configured or disabled. Fixes #782 Signed-off-by: Matthias --- CHANGELOG.md | 1 + env.d/development/common.dist | 1 + src/backend/core/api/viewsets.py | 1 + src/backend/core/tests/test_api_config.py | 1 + src/backend/impress/settings.py | 3 ++ .../e2e/__tests__/app-impress/config.spec.ts | 33 +++++++++++++++++++ .../__tests__/app-impress/doc-editor.spec.ts | 11 +++++-- .../impress/src/core/config/api/useConfig.tsx | 1 + .../BlockNoteToolBar/BlockNoteToolbar.tsx | 7 ++-- 9 files changed, 55 insertions(+), 4 deletions(-) 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 ( <>