refactor(ai): drop Generative AI flag, gate title generation on AI chat

Remove the separate, un-toggleable `settings.ai.generative` workspace flag
(and its write-side alias `generativeAi`) along with the dead "Ask AI"
generative editor menu, and re-gate the AI page-title generation on the
general AI chat flag (`settings.ai.chat`) — the same toggle that enables
the chat agent and the chat stream endpoint.

Why: the `generative` flag had no UI toggle (its switch was already removed,
leaving orphaned i18n strings), so the title-generation button was
unreachable on self-hosted. The "Ask AI" menu was dead — its atom was never
rendered. Consolidating onto the AI chat flag makes the title button follow
the one AI switch users actually have.

Changes:
- server: title-gen endpoint gate generative -> chat (ai-chat.controller.ts);
  remove generativeAi from update DTO and workspace service (update block,
  delete line, cloud default now { ai: { chat: true } }); fix repo comment;
  migrate generate-page-title spec assertions generative -> chat.
- client: title-gen gate -> settings.ai.chat (full-editor.tsx); remove the
  dead Ask AI button + showAiMenu wiring from bubble-menu; remove AskAiGroup
  usage/import and commented block from fixed-toolbar; delete ask-ai-group.tsx;
  remove showAiMenuAtom; drop generative/generativeAi from workspace types.
- i18n: remove 3 orphaned generative-AI keys from all 12 locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude_code
2026-06-26 21:35:30 +03:00
parent 406921ac6a
commit 276ccc0783
24 changed files with 21 additions and 139 deletions

View File

@@ -665,9 +665,6 @@
"AI-powered search (AI Answers)": "KI-unterstützte Suche (KI-Antworten)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "Die KI-Suche verwendet Vektor-Einbettungen, um semantische Suchfunktionen in Ihrem Arbeitsbereich bereitzustellen.",
"Toggle AI search": "KI-Suche umschalten",
"Generative AI (Ask AI)": "Generative KI (KI fragen)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Aktivieren Sie die KI-unterstützte Inhaltserstellung im Editor. Ermöglicht Benutzern das Erzeugen, Verbessern, Übersetzen und Transformieren von Text.",
"Toggle generative AI": "Generative KI umschalten",
"Upgrade your plan": "Upgrade Ihres Plans",
"Available with a paid license": "Verfügbar mit einer kostenpflichtigen Lizenz",
"Upgrade your license tier.": "Stufen Sie Ihre Lizenz hoch.",

View File

@@ -687,9 +687,6 @@
"AI-powered search (AI Answers)": "AI-powered search (AI Answers)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.",
"Toggle AI search": "Toggle AI search",
"Generative AI (Ask AI)": "Generative AI (Ask AI)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.",
"Toggle generative AI": "Toggle generative AI",
"Upgrade your plan": "Upgrade your plan",
"Available with a paid license": "Available with a paid license",
"Upgrade your license tier.": "Upgrade your license tier.",

View File

@@ -665,9 +665,6 @@
"AI-powered search (AI Answers)": "Búsqueda impulsada por IA (Respuestas de IA)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "La búsqueda de IA utiliza incrustaciones vectoriales para proporcionar capacidades de búsqueda semántica en todo el contenido de su espacio de trabajo.",
"Toggle AI search": "Alternar búsqueda de IA",
"Generative AI (Ask AI)": "IA generativa (Preguntar a la IA)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Habilitar la generación de contenido impulsada por IA en el editor. Permite a los usuarios generar, mejorar, traducir y transformar texto.",
"Toggle generative AI": "Activar IA generativa",
"Upgrade your plan": "Mejora tu plan",
"Available with a paid license": "Disponible con una licencia de pago",
"Upgrade your license tier.": "Mejora el nivel de tu licencia.",

View File

@@ -665,9 +665,6 @@
"AI-powered search (AI Answers)": "Recherche propulsée par IA (Réponses IA)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "La recherche IA utilise des incorporations vectorielles pour fournir des capacités de recherche sémantique à travers le contenu de votre espace de travail.",
"Toggle AI search": "Basculer la recherche IA",
"Generative AI (Ask AI)": "IA générative (Demandez à l'IA)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Activer la génération de contenu assistée par IA dans l'éditeur. Permet aux utilisateurs de générer, améliorer, traduire et transformer du texte.",
"Toggle generative AI": "Activer/désactiver l'IA générative",
"Upgrade your plan": "Mettez à niveau votre forfait",
"Available with a paid license": "Disponible avec une licence payante",
"Upgrade your license tier.": "Mettez à niveau votre niveau de licence.",

View File

@@ -665,9 +665,6 @@
"AI-powered search (AI Answers)": "Ricerca con AI (Risposte AI)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "La ricerca AI utilizza embeddings vettoriali per fornire capacità di ricerca semantica nel contenuto della tua area di lavoro.",
"Toggle AI search": "Attiva/disattiva ricerca AI",
"Generative AI (Ask AI)": "AI generativa (Chiedi AI)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Abilita la generazione di contenuti con AI nell'editor. Consente agli utenti di generare, migliorare, tradurre e trasformare il testo.",
"Toggle generative AI": "Attiva/Disattiva AI generativa",
"Upgrade your plan": "Aggiorna il tuo piano",
"Available with a paid license": "Disponibile con una licenza a pagamento",
"Upgrade your license tier.": "Aggiorna il livello della tua licenza.",

View File

@@ -665,9 +665,6 @@
"AI-powered search (AI Answers)": "AI搭載検索 (AI回答)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI検索はベクター埋め込みを使用してワークスペース全体の意味検索を実現します",
"Toggle AI search": "AI検索を切り替え",
"Generative AI (Ask AI)": "生成AI (Ask AI)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "エディターでAIを活用したコンテンツ生成を有効にします。ユーザーがテキストの生成、改善、翻訳、および変換を行うことができます。",
"Toggle generative AI": "生成AIを切り替える",
"Upgrade your plan": "プランをアップグレードする",
"Available with a paid license": "有料ライセンスで利用可能",
"Upgrade your license tier.": "ライセンスタイアをアップグレードしてください。",

View File

@@ -665,9 +665,6 @@
"AI-powered search (AI Answers)": "AI 구동 검색 (AI 답변)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI 검색은 벡터 임베딩을 사용하여 작업공간 콘텐츠에 대한 의미 검색 기능을 제공합니다.",
"Toggle AI search": "AI 검색 전환",
"Generative AI (Ask AI)": "생성 AI (Ask AI)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "편집기에서 AI 구동 콘텐츠 생성을 활성화합니다. 사용자가 텍스트를 생성, 개선, 번역 및 변환할 수 있습니다.",
"Toggle generative AI": "생성 AI 토글",
"Upgrade your plan": "요금제를 업그레이드하세요",
"Available with a paid license": "유료 라이선스에서만 사용 가능합니다",
"Upgrade your license tier.": "라이선스 등급을 업그레이드하세요.",

View File

@@ -665,9 +665,6 @@
"AI-powered search (AI Answers)": "AI-gestuurde zoekopdracht (AI Antwoorden)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI-zoekopdracht maakt gebruik van vectorembeddings om semantische zoekmogelijkheden te bieden in uw werkruimte-inhoud.",
"Toggle AI search": "Schakel AI-zoekopdracht in/uit",
"Generative AI (Ask AI)": "Generatieve AI (Vraag het AI)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Schakel AI-gestuurde inhoudsgeneratie in de editor in. Hiermee kunnen gebruikers tekst genereren, verbeteren, vertalen en transformeren.",
"Toggle generative AI": "Generatieve AI schakelen",
"Upgrade your plan": "Upgrade je abonnement",
"Available with a paid license": "Beschikbaar met een betaalde licentie",
"Upgrade your license tier.": "Upgrade je licentieniveau.",

View File

@@ -665,9 +665,6 @@
"AI-powered search (AI Answers)": "Pesquisa com IA (Respostas de IA)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "A pesquisa IA usa vetores de incorporação para fornecer capacidades de pesquisa semântica em todo o conteúdo do seu espaço de trabalho.",
"Toggle AI search": "Alternar pesquisa de IA",
"Generative AI (Ask AI)": "IA generativa (Perguntar à IA)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Habilitar geração de conteúdo com IA no editor. Permite aos usuários gerar, melhorar, traduzir e transformar texto.",
"Toggle generative AI": "Alternar IA generativa",
"Upgrade your plan": "Faça upgrade do seu plano",
"Available with a paid license": "Disponível com uma licença paga",
"Upgrade your license tier.": "Faça upgrade do seu nível de licença.",

View File

@@ -749,9 +749,6 @@
"AI-powered search (AI Answers)": "Поиск на базе ИИ (Ответы ИИ)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "Поиск ИИ использует векторные встраивания для обеспечения семантического поиска по содержимому вашего рабочего пространства.",
"Toggle AI search": "Переключить поиск ИИ",
"Generative AI (Ask AI)": "Генеративный ИИ (Спросить ИИ)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Включите создание контента на базе ИИ в редакторе. Позволяет пользователям генерировать, улучшать, переводить и преобразовывать текст.",
"Toggle generative AI": "Переключить генеративный ИИ",
"Upgrade your plan": "Обновите свой тарифный план",
"Available with a paid license": "Доступно с платной лицензией",
"Upgrade your license tier.": "Обновите уровень вашей лицензии.",

View File

@@ -665,9 +665,6 @@
"AI-powered search (AI Answers)": "Пошук на базі ШІ (Відповіді ШІ)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "Пошук з ШІ використовує векторні вбудовування для надання можливостей семантичного пошуку у вашому робочому вмісті.",
"Toggle AI search": "Переключити пошук з ШІ",
"Generative AI (Ask AI)": "Генеративний ШІ (Запитати ШІ)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Увімкнути генерацію контенту за допомогою ШІ в редакторі. Дозволяє користувачам генерувати, покращувати, перекладати та трансформувати текст.",
"Toggle generative AI": "Переключити генеративний ШІ",
"Upgrade your plan": "Оновіть свій тарифний план",
"Available with a paid license": "Доступно за платною ліцензією",
"Upgrade your license tier.": "Оновіть рівень своєї ліцензії.",

View File

@@ -665,9 +665,6 @@
"AI-powered search (AI Answers)": "AI驱动的搜索 (AI答案)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI搜索使用向量嵌入提供跨工作空间内容的语义搜索功能。",
"Toggle AI search": "切换AI搜索",
"Generative AI (Ask AI)": "生成型AI (询问AI)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "在编辑器中启用AI驱动的内容生成。允许用户生成、改进、翻译和转换文本。",
"Toggle generative AI": "切换生成型AI",
"Upgrade your plan": "升级您的方案",
"Available with a paid license": "需付费许可才可用",
"Upgrade your license tier.": "升级您的许可等级。",

View File

@@ -10,8 +10,6 @@ export const readOnlyEditorAtom = atom<Editor | null>(null);
export const yjsConnectionStatusAtom = atom<string>("");
export const showAiMenuAtom = atom(false);
export const showLinkMenuAtom = atom(false);
// Current page's edit mode — initialized from the user's saved preference on

View File

@@ -9,11 +9,10 @@ import {
IconStrikethrough,
IconUnderline,
IconMessage,
IconSparkles,
} from "@tabler/icons-react";
import clsx from "clsx";
import classes from "./bubble-menu.module.css";
import { ActionIcon, Button, rem, Tooltip } from "@mantine/core";
import { ActionIcon, rem, Tooltip } from "@mantine/core";
import { ColorSelector } from "./color-selector";
import { NodeSelector } from "./node-selector";
import { TextAlignmentSelector } from "./text-alignment-selector";
@@ -26,8 +25,8 @@ import { v7 as uuid7 } from "uuid";
import { isCellSelection, isTextSelected } from "@docmost/editor-ext";
import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx";
import { useTranslation } from "react-i18next";
import { showAiMenuAtom, showLinkMenuAtom } from "@/features/editor/atoms/editor-atoms";
import { userAtom, workspaceAtom } from "@/features/user/atoms/current-user-atom";
import { showLinkMenuAtom } from "@/features/editor/atoms/editor-atoms";
import { userAtom } from "@/features/user/atoms/current-user-atom";
export interface BubbleMenuItem {
name: string;
@@ -44,16 +43,12 @@ type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children" | "editor"> & {
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
const { templateMode = false } = props;
const { t } = useTranslation();
const [showAiMenu, setShowAiMenu] = useAtom(showAiMenuAtom);
const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom);
const workspace = useAtomValue(workspaceAtom);
const isGenerativeAiEnabled = workspace?.settings?.ai?.generative === true;
const user = useAtomValue(userAtom);
const editorToolbarEnabled =
user?.settings?.preferences?.editorToolbar ?? false;
const [, setDraftCommentId] = useAtom(draftCommentIdAtom);
const showCommentPopupRef = useRef(showCommentPopup);
const showAiMenuRef = useRef(showAiMenu);
const [showLinkMenu] = useAtom(showLinkMenuAtom);
const showLinkMenuRef = useRef(showLinkMenu);
@@ -61,10 +56,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
showCommentPopupRef.current = showCommentPopup;
}, [showCommentPopup]);
useEffect(() => {
showAiMenuRef.current = showAiMenu;
}, [showAiMenu]);
useEffect(() => {
showLinkMenuRef.current = showLinkMenu;
}, [showLinkMenu]);
@@ -145,7 +136,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
empty ||
isNodeSelection(selection) ||
isCellSelection(selection) ||
showAiMenuRef.current ||
showLinkMenuRef.current ||
showCommentPopupRef?.current
) {
@@ -168,8 +158,8 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
const [isTextAlignmentSelectorOpen, setIsTextAlignmentOpen] = useState(false);
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
// Hide the bubble menu immediately when AI menu is shown
if (showAiMenu || showLinkMenu) return;
// Hide the bubble menu immediately when the link menu is shown
if (showLinkMenu) return;
return (
<BubbleMenu
@@ -177,22 +167,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
style={{ zIndex: 199, position: "relative" }}
>
<div className={classes.bubbleMenu}>
{isGenerativeAiEnabled && (
<>
<Button
variant="default"
className={clsx(classes.buttonRoot)}
radius="0"
leftSection={<IconSparkles size={16} />}
onClick={() => {
setShowAiMenu(true);
}}
>
{t("Ask AI")}
</Button>
<div className={classes.divider} />
</>
)}
{!editorToolbarEnabled && (
<>
<NodeSelector

View File

@@ -12,8 +12,6 @@ import { MediaGroup } from "./groups/media-group";
import { QuickInsertsGroup } from "./groups/quick-inserts-group";
import { MoreInsertsGroup } from "./groups/more-inserts-group";
import { HistoryGroup } from "./groups/history-group";
import { AskAiGroup } from "./groups/ask-ai-group";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom";
import classes from "./fixed-toolbar.module.css";
type FixedToolbarProps = {
@@ -28,8 +26,6 @@ export const FixedToolbar: FC<FixedToolbarProps> = ({
const editorFromAtom = useAtomValue(pageEditorAtom);
const editor = editorProp ?? editorFromAtom;
const state = useToolbarState(editor);
const workspace = useAtomValue(workspaceAtom);
const isGenerativeAiEnabled = workspace?.settings?.ai?.generative === true;
if (!editor || !state) return null;
@@ -43,12 +39,6 @@ export const FixedToolbar: FC<FixedToolbarProps> = ({
onMouseDown={(e) => e.preventDefault()}
>
<div className={classes.inner}>
{/* {isGenerativeAiEnabled && (
<>
<AskAiGroup />
<div className={classes.divider} />
</>
)} */}
<BlockTypeGroup editor={editor} />
<div className={classes.divider} />
<InlineMarksGroup editor={editor} state={state} />

View File

@@ -1,23 +0,0 @@
import { FC } from "react";
import { Button } from "@mantine/core";
import { IconSparkles } from "@tabler/icons-react";
import { useSetAtom } from "jotai";
import { useTranslation } from "react-i18next";
import { showAiMenuAtom } from "@/features/editor/atoms/editor-atoms";
export const AskAiGroup: FC = () => {
const { t } = useTranslation();
const setShowAiMenu = useSetAtom(showAiMenuAtom);
return (
<Button
variant="subtle"
color="dark"
size="xs"
leftSection={<IconSparkles size={14} />}
onClick={() => setShowAiMenu(true)}
>
{t("Ask AI")}
</Button>
);
};

View File

@@ -13,7 +13,7 @@ interface Props {
/**
* AI "generate title" button (#199). Reads the live editor content and applies a
* model-suggested title immediately. Rendered in the page byline, only in edit
* mode and when the workspace's generative AI flag is on.
* mode and when the workspace's AI chat flag is on.
*/
export const GenerateTitleGroup: FC<Props> = ({
pageId,

View File

@@ -77,9 +77,9 @@ export function FullEditor({
const [user] = useAtom(userAtom);
const workspace = useAtomValue(workspaceAtom);
const isDictationEnabled = workspace?.settings?.ai?.dictation === true;
// AI title generation reuses the generative AI flag (same gate as the on-page
// generative menu); the server enforces it too (#199).
const isTitleGenEnabled = workspace?.settings?.ai?.generative === true;
// AI title generation is gated by the general AI chat flag (the same toggle
// that enables the chat agent); the server enforces it too (#199).
const isTitleGenEnabled = workspace?.settings?.ai?.chat === true;
const fullPageWidth = user.settings?.preferences?.fullPageWidth;
const editorToolbarEnabled =
user.settings?.preferences?.editorToolbar ?? false;
@@ -254,7 +254,7 @@ function PageByline({
{showDictation && editor && (
<DictationGroup editor={editor} color="gray" iconSize={20} />
)}
{/* Shown only in edit mode when the workspace's generative AI flag is on,
{/* Shown only in edit mode when the workspace's AI chat flag is on,
so AI title generation stays reachable from the byline (#199). */}
{showTitleGen && (
<GenerateTitleGroup pageId={pageId} color="gray" iconSize={20} />

View File

@@ -20,7 +20,6 @@ export interface IWorkspace {
plan?: string;
enforceMfa?: boolean;
aiSearch?: boolean;
generativeAi?: boolean;
disablePublicSharing?: boolean;
mcpEnabled?: boolean;
aiChat?: boolean;
@@ -61,7 +60,6 @@ export interface IWorkspaceApiSettings {
export interface IWorkspaceAiSettings {
search?: boolean;
generative?: boolean;
mcp?: boolean;
chat?: boolean;
dictation?: boolean;

View File

@@ -319,8 +319,8 @@ export class AiChatController {
/**
* Generate a page title from supplied note content (#199). One-shot,
* non-streaming. Gated by the workspace AI flag (reusing settings.ai.generative,
* the same flag that gates the on-page generative AI menu); returns { title }.
* non-streaming. Gated by the AI chat flag (settings.ai.chat, the same toggle
* that enables the chat agent); returns { title }.
* The endpoint NEVER writes the page — the client applies the title via the
* existing /pages/update route (which enforces edit permission), so access
* checks are not duplicated here. Throttled per user via AI_CHAT_THROTTLER.
@@ -334,9 +334,9 @@ export class AiChatController {
@AuthWorkspace() workspace: Workspace,
): Promise<{ title: string }> {
const settings = (workspace.settings ?? {}) as {
ai?: { generative?: boolean };
ai?: { chat?: boolean };
};
if (settings.ai?.generative !== true) {
if (settings.ai?.chat !== true) {
throw new ForbiddenException('AI title generation is disabled');
}
try {

View File

@@ -42,7 +42,7 @@ describe('cleanGeneratedTitle', () => {
/**
* Wiring spec for the #199 `POST /ai-chat/generate-page-title` endpoint. It must:
* gate on settings.ai.generative (403 when off), delegate to the service when on,
* gate on settings.ai.chat (403 when off), delegate to the service when on,
* rethrow HttpExceptions verbatim (e.g. AiNotConfiguredException -> 503), and map
* any other provider/transport fault to a 503. Exercised by instantiating the
* controller with hand-rolled mocks — no Nest graph, no DB.
@@ -50,7 +50,7 @@ describe('cleanGeneratedTitle', () => {
describe('AiChatController.generatePageTitle', () => {
const enabledWorkspace = {
id: 'ws1',
settings: { ai: { generative: true } },
settings: { ai: { chat: true } },
} as unknown as Workspace;
function makeController(generate: jest.Mock) {
@@ -64,7 +64,7 @@ describe('AiChatController.generatePageTitle', () => {
return { controller, aiChatService };
}
it('forbids when the generative AI flag is off', async () => {
it('forbids when the AI chat flag is off', async () => {
const generate = jest.fn();
const { controller } = makeController(generate);
const disabled = { id: 'ws1', settings: {} } as unknown as Workspace;
@@ -74,12 +74,12 @@ describe('AiChatController.generatePageTitle', () => {
expect(generate).not.toHaveBeenCalled();
});
it('forbids when settings.ai.generative is anything but exactly true', async () => {
it('forbids when settings.ai.chat is anything but exactly true', async () => {
const generate = jest.fn();
const { controller } = makeController(generate);
const ws = {
id: 'ws1',
settings: { ai: { generative: 'yes' } },
settings: { ai: { chat: 'yes' } },
} as unknown as Workspace;
await expect(
controller.generatePageTitle({ content: 'body' }, ws),

View File

@@ -31,10 +31,6 @@ export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {
@IsBoolean()
aiSearch: boolean;
@IsOptional()
@IsBoolean()
generativeAi: boolean;
@IsOptional()
@IsBoolean()
disablePublicSharing: boolean;

View File

@@ -145,7 +145,7 @@ export class WorkspaceService {
status = WorkspaceStatus.Active;
plan = 'standard';
billingEmail = user.email;
settings = { ai: { generative: true, chat: true } };
settings = { ai: { chat: true } };
}
// create workspace
@@ -439,20 +439,6 @@ export class WorkspaceService {
);
}
if (typeof updateWorkspaceDto.generativeAi !== 'undefined') {
const prev = settingsBefore?.ai?.generative ?? false;
if (prev !== updateWorkspaceDto.generativeAi) {
before.generativeAi = prev;
after.generativeAi = updateWorkspaceDto.generativeAi;
}
await this.workspaceRepo.updateAiSettings(
workspaceId,
'generative',
updateWorkspaceDto.generativeAi,
trx,
);
}
if (typeof updateWorkspaceDto.disablePublicSharing !== 'undefined') {
const prev = settingsBefore?.sharing?.disabled ?? false;
if (prev !== updateWorkspaceDto.disablePublicSharing) {
@@ -587,7 +573,6 @@ export class WorkspaceService {
delete updateWorkspaceDto.restrictApiToAdmins;
delete updateWorkspaceDto.aiSearch;
delete updateWorkspaceDto.generativeAi;
delete updateWorkspaceDto.disablePublicSharing;
delete updateWorkspaceDto.mcpEnabled;
delete updateWorkspaceDto.allowMemberTemplates;

View File

@@ -246,7 +246,7 @@ export class WorkspaceRepo {
* otherwise re-serialize a `JSON.stringify`'d string, yielding a jsonb string
* that `||` turns into an array). A `jsonb_typeof = 'object'` CASE self-heals
* workspaces whose `settings.ai.provider` was previously corrupted into an
* array/string. Sibling `settings.ai.*` keys (search / generative / chat / mcp
* array/string. Sibling `settings.ai.*` keys (search / chat / mcp
* / systemPrompt) and provider fields absent from the partial are preserved via
* jsonb `||` merge.
*/