mirror of
https://github.com/docmost/docmost.git
synced 2026-08-25 15:57:11 +08:00
feat(ee): add enterprise ai chat controls (#2421)
* feat(ee): add enterprise ai chat controls * feat: add comment listing tool to ai chat * fix: scope comment endpoints to workspace and skip trashed pages
This commit is contained in:
@@ -696,7 +696,7 @@
|
||||
"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.",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "KI ist nur in der Docmost Enterprise-Edition verfügbar. Kontaktieren Sie sales@docmost.com.",
|
||||
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "KI ist in den kostenpflichtigen Docmost-Editionen verfügbar. Kontaktieren Sie sales@docmost.com.",
|
||||
"AI & MCP": "KI & MCP",
|
||||
"AI": "KI",
|
||||
"MCP": "MCP",
|
||||
|
||||
@@ -698,7 +698,7 @@
|
||||
"Upgrade your plan": "Upgrade your plan",
|
||||
"Available with a paid license": "Available with a paid license",
|
||||
"Upgrade your license tier.": "Upgrade your license tier.",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
|
||||
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "AI is available in the Docmost paid editions. Contact sales@docmost.com.",
|
||||
"AI & MCP": "AI & MCP",
|
||||
"AI": "AI",
|
||||
"MCP": "MCP",
|
||||
@@ -1302,5 +1302,11 @@
|
||||
"Error loading attachments.": "Error loading attachments.",
|
||||
"No attachments on this page yet.": "No attachments on this page yet.",
|
||||
"Uploaded by {{name}}": "Uploaded by {{name}}",
|
||||
"Download {{name}}": "Download {{name}}"
|
||||
"Download {{name}}": "Download {{name}}",
|
||||
"Workspace knowledge only": "Workspace knowledge only",
|
||||
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.",
|
||||
"Toggle workspace knowledge only": "Toggle workspace knowledge only",
|
||||
"Read-only mode": "Read-only mode",
|
||||
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat can search and read workspace content, but cannot create or edit pages.",
|
||||
"Toggle AI Chat read-only mode": "Toggle AI Chat read-only mode"
|
||||
}
|
||||
|
||||
@@ -696,7 +696,7 @@
|
||||
"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.",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "La IA solo está disponible en la edición empresarial de Docmost. Contacte con sales@docmost.com.",
|
||||
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "La IA está disponible en las ediciones de pago de Docmost. Contacte con sales@docmost.com.",
|
||||
"AI & MCP": "IA y MCP",
|
||||
"AI": "IA",
|
||||
"MCP": "MCP",
|
||||
|
||||
@@ -696,7 +696,7 @@
|
||||
"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.",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "L'IA n'est disponible que dans l'édition Entreprise de Docmost. Contactez sales@docmost.com.",
|
||||
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "L'IA est disponible dans les éditions payantes de Docmost. Contactez sales@docmost.com.",
|
||||
"AI & MCP": "IA & MCP",
|
||||
"AI": "IA",
|
||||
"MCP": "MCP",
|
||||
|
||||
@@ -696,7 +696,7 @@
|
||||
"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.",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "L'IA è disponibile solo nell'edizione Enterprise di Docmost. Contatta sales@docmost.com.",
|
||||
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "L'IA è disponibile nelle edizioni a pagamento di Docmost. Contatta sales@docmost.com.",
|
||||
"AI & MCP": "IA e MCP",
|
||||
"AI": "IA",
|
||||
"MCP": "MCP",
|
||||
|
||||
@@ -696,7 +696,7 @@
|
||||
"Upgrade your plan": "プランをアップグレードする",
|
||||
"Available with a paid license": "有料ライセンスで利用可能",
|
||||
"Upgrade your license tier.": "ライセンスタイアをアップグレードしてください。",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI は Docmost のエンタープライズ版でのみ利用可能です。sales@docmost.com までお問い合わせください。",
|
||||
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "AI は Docmost の有料版で利用可能です。sales@docmost.com までお問い合わせください。",
|
||||
"AI & MCP": "AI と MCP",
|
||||
"AI": "AI",
|
||||
"MCP": "MCP",
|
||||
|
||||
@@ -696,7 +696,7 @@
|
||||
"Upgrade your plan": "요금제를 업그레이드하세요",
|
||||
"Available with a paid license": "유료 라이선스에서만 사용 가능합니다",
|
||||
"Upgrade your license tier.": "라이선스 등급을 업그레이드하세요.",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI는 Docmost 엔터프라이즈 에디션에서만 제공됩니다. sales@docmost.com으로 문의하세요.",
|
||||
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "AI는 Docmost 유료 에디션에서 제공됩니다. sales@docmost.com으로 문의하세요.",
|
||||
"AI & MCP": "AI 및 MCP",
|
||||
"AI": "AI",
|
||||
"MCP": "MCP",
|
||||
|
||||
@@ -696,7 +696,7 @@
|
||||
"Upgrade your plan": "Upgrade je abonnement",
|
||||
"Available with a paid license": "Beschikbaar met een betaalde licentie",
|
||||
"Upgrade your license tier.": "Upgrade je licentieniveau.",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI is alleen beschikbaar in de Docmost Enterprise-editie. Neem contact op met sales@docmost.com.",
|
||||
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "AI is beschikbaar in de betaalde edities van Docmost. Neem contact op met sales@docmost.com.",
|
||||
"AI & MCP": "AI & MCP",
|
||||
"AI": "AI",
|
||||
"MCP": "MCP",
|
||||
|
||||
@@ -696,7 +696,7 @@
|
||||
"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.",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "A IA está disponível apenas na edição empresarial do Docmost. Contate sales@docmost.com.",
|
||||
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "A IA está disponível nas edições pagas do Docmost. Contate sales@docmost.com.",
|
||||
"AI & MCP": "IA e MCP",
|
||||
"AI": "IA",
|
||||
"MCP": "MCP",
|
||||
|
||||
@@ -696,7 +696,7 @@
|
||||
"Upgrade your plan": "Обновите свой тарифный план",
|
||||
"Available with a paid license": "Доступно с платной лицензией",
|
||||
"Upgrade your license tier.": "Обновите уровень вашей лицензии.",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "ИИ доступен только в корпоративной версии Docmost. Свяжитесь по адресу sales@docmost.com.",
|
||||
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "ИИ доступен в платных версиях Docmost. Свяжитесь по адресу sales@docmost.com.",
|
||||
"AI & MCP": "ИИ и MCP",
|
||||
"AI": "ИИ",
|
||||
"MCP": "MCP",
|
||||
|
||||
@@ -696,7 +696,7 @@
|
||||
"Upgrade your plan": "Оновіть свій тарифний план",
|
||||
"Available with a paid license": "Доступно за платною ліцензією",
|
||||
"Upgrade your license tier.": "Оновіть рівень своєї ліцензії.",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "ШІ доступний лише в корпоративній редакції Docmost. Зверніться до sales@docmost.com.",
|
||||
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "ШІ доступний у платних редакціях Docmost. Зверніться до sales@docmost.com.",
|
||||
"AI & MCP": "ШІ та MCP",
|
||||
"AI": "ШІ",
|
||||
"MCP": "MCP",
|
||||
|
||||
@@ -696,7 +696,7 @@
|
||||
"Upgrade your plan": "升级您的方案",
|
||||
"Available with a paid license": "需付费许可才可用",
|
||||
"Upgrade your license tier.": "升级您的许可等级。",
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI 仅在 Docmost 企业版中提供。请联系 sales@docmost.com。",
|
||||
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "AI 在 Docmost 付费版中提供。请联系 sales@docmost.com。",
|
||||
"AI & MCP": "AI 与 MCP",
|
||||
"AI": "AI",
|
||||
"MCP": "MCP",
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Badge, Group, Text, Switch, Tooltip } from "@mantine/core";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
|
||||
|
||||
export default function AiChatReadOnly() {
|
||||
const { t } = useTranslation();
|
||||
const hasAccess = useHasFeature(Feature.AI_CONTROLS);
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Group gap="xs" align="center">
|
||||
<Text size="md">{t("Read-only mode")}</Text>
|
||||
{!hasAccess && (
|
||||
<Badge variant="light" size="sm" radius="sm">
|
||||
{t("Enterprise")}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t(
|
||||
"AI Chat can search and read workspace content, but cannot create or edit pages.",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<AiChatReadOnlyToggle />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
function AiChatReadOnlyToggle() {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const [checked, setChecked] = useState(
|
||||
workspace?.settings?.ai?.chatReadOnly,
|
||||
);
|
||||
const hasAccess = useHasFeature(Feature.AI_CONTROLS);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.checked;
|
||||
try {
|
||||
const updatedWorkspace = await updateWorkspace({
|
||||
aiChatReadOnly: value,
|
||||
});
|
||||
setChecked(value);
|
||||
setWorkspace(updatedWorkspace);
|
||||
} catch (err: any) {
|
||||
notifications.show({
|
||||
message: err?.response?.data?.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip label={upgradeLabel} disabled={hasAccess} refProp="rootRef">
|
||||
<Switch
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasAccess}
|
||||
aria-label={t("Toggle AI Chat read-only mode")}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Badge, Group, Text, Switch, Tooltip } from "@mantine/core";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
|
||||
|
||||
export default function AiChatWorkspaceKnowledgeOnly() {
|
||||
const { t } = useTranslation();
|
||||
const hasAccess = useHasFeature(Feature.AI_CONTROLS);
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Group gap="xs" align="center">
|
||||
<Text size="md">{t("Workspace knowledge only")}</Text>
|
||||
{!hasAccess && (
|
||||
<Badge variant="light" size="sm" radius="sm">
|
||||
{t("Enterprise")}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t(
|
||||
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<AiChatWorkspaceKnowledgeOnlyToggle />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
function AiChatWorkspaceKnowledgeOnlyToggle() {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const [checked, setChecked] = useState(
|
||||
workspace?.settings?.ai?.chatWorkspaceKnowledgeOnly,
|
||||
);
|
||||
const hasAccess = useHasFeature(Feature.AI_CONTROLS);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.checked;
|
||||
try {
|
||||
const updatedWorkspace = await updateWorkspace({
|
||||
aiChatWorkspaceKnowledgeOnly: value,
|
||||
});
|
||||
setChecked(value);
|
||||
setWorkspace(updatedWorkspace);
|
||||
} catch (err: any) {
|
||||
notifications.show({
|
||||
message: err?.response?.data?.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip label={upgradeLabel} disabled={hasAccess} refProp="rootRef">
|
||||
<Switch
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasAccess}
|
||||
aria-label={t("Toggle workspace knowledge only")}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
IconFileText,
|
||||
} from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ChatInput from "./chat-input";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { useRef } from "react";
|
||||
import ChatInput, { ChatInputHandle } from "./chat-input";
|
||||
import type { ChatAttachment, PageMention } from "../types/ai-chat.types";
|
||||
import classes from "../styles/ai-chat.module.css";
|
||||
|
||||
@@ -14,6 +17,7 @@ type Suggestion = {
|
||||
icon: React.ReactNode;
|
||||
text: string;
|
||||
prompt: string;
|
||||
write?: boolean;
|
||||
};
|
||||
|
||||
const SUGGESTIONS: Suggestion[] = [
|
||||
@@ -26,6 +30,7 @@ const SUGGESTIONS: Suggestion[] = [
|
||||
icon: <IconFilePlus size={16} />,
|
||||
text: "Create a new page",
|
||||
prompt: "Create a new page titled ",
|
||||
write: true,
|
||||
},
|
||||
{
|
||||
icon: <IconFileText size={16} />,
|
||||
@@ -36,6 +41,7 @@ const SUGGESTIONS: Suggestion[] = [
|
||||
icon: <IconEdit size={16} />,
|
||||
text: "Update page content",
|
||||
prompt: "Update the page @",
|
||||
write: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -47,9 +53,13 @@ type Props = {
|
||||
|
||||
export default function ChatEmptyState({ isStreaming, onSend, onStop }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const workspace = useAtomValue(workspaceAtom);
|
||||
const writesDisabled = workspace?.settings?.ai?.chatReadOnly === true;
|
||||
|
||||
const inputRef = useRef<ChatInputHandle>(null);
|
||||
|
||||
const handleSuggestionClick = (prompt: string) => {
|
||||
onSend(prompt, [], []);
|
||||
inputRef.current?.prefill(prompt);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -62,6 +72,7 @@ export default function ChatEmptyState({ isStreaming, onSend, onStop }: Props) {
|
||||
|
||||
<div className={classes.emptyStateInput}>
|
||||
<ChatInput
|
||||
ref={inputRef}
|
||||
isStreaming={isStreaming}
|
||||
onSend={onSend}
|
||||
onStop={onStop}
|
||||
@@ -73,7 +84,7 @@ export default function ChatEmptyState({ isStreaming, onSend, onStop }: Props) {
|
||||
<div className={classes.suggestionsSection}>
|
||||
<h2 className={classes.suggestionsLabel}>{t("Get started")}</h2>
|
||||
<div className={classes.suggestionsGrid}>
|
||||
{SUGGESTIONS.map((s) => (
|
||||
{SUGGESTIONS.filter((s) => !writesDisabled || !s.write).map((s) => (
|
||||
<button
|
||||
key={s.text}
|
||||
type="button"
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { useCallback, useId, useRef, useEffect, useState } from "react";
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useId,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconArrowUp, IconPaperclip, IconPlayerStopFilled, IconX, IconFile, IconPhoto, IconPlus, IconAt, IconFileText } from "@tabler/icons-react";
|
||||
import { Popover } from "@mantine/core";
|
||||
@@ -35,6 +43,10 @@ type Props = {
|
||||
chatId?: string;
|
||||
};
|
||||
|
||||
export type ChatInputHandle = {
|
||||
prefill: (text: string) => void;
|
||||
};
|
||||
|
||||
function extractMentions(json: any): PageMention[] {
|
||||
const mentions: PageMention[] = [];
|
||||
const seen = new Set<string>();
|
||||
@@ -89,7 +101,7 @@ function editorJsonToText(json: any): string {
|
||||
return text;
|
||||
}
|
||||
|
||||
export default function ChatInput({
|
||||
const ChatInput = forwardRef<ChatInputHandle, Props>(function ChatInput({
|
||||
isStreaming,
|
||||
onSend,
|
||||
onStop,
|
||||
@@ -100,7 +112,7 @@ export default function ChatInput({
|
||||
variant = "card",
|
||||
showDisclaimer = true,
|
||||
chatId,
|
||||
}: Props) {
|
||||
}: Props, ref) {
|
||||
const chatIdRef = useRef(chatId);
|
||||
chatIdRef.current = chatId;
|
||||
const { t } = useTranslation();
|
||||
@@ -270,6 +282,19 @@ export default function ChatInput({
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
prefill: (text: string) => {
|
||||
if (!editor || editor.isDestroyed) return;
|
||||
editor.commands.clearContent();
|
||||
editor.commands.insertContent(text);
|
||||
editor.commands.focus();
|
||||
},
|
||||
}),
|
||||
[editor],
|
||||
);
|
||||
|
||||
const hasContent = !isEmpty || pendingAttachments.some((a) => !a.uploading) || (contextPages?.length ?? 0) > 0;
|
||||
|
||||
const wrapperClass = variant === "flat" ? classes.inputWrapperFlat : classes.inputWrapper;
|
||||
@@ -429,4 +454,6 @@ export default function ChatInput({
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ChatInput;
|
||||
|
||||
@@ -5,8 +5,10 @@ import { useTranslation } from "react-i18next";
|
||||
import EnableAiSearch from "@/ee/ai/components/enable-ai-search.tsx";
|
||||
import EnableGenerativeAi from "@/ee/ai/components/enable-generative-ai.tsx";
|
||||
import EnableAiChat from "@/ee/ai-chat/components/enable-ai-chat.tsx";
|
||||
import AiChatReadOnly from "@/ee/ai-chat/components/ai-chat-read-only.tsx";
|
||||
import AiChatWorkspaceKnowledgeOnly from "@/ee/ai-chat/components/ai-chat-workspace-knowledge-only.tsx";
|
||||
import McpSettings from "@/ee/ai/components/mcp-settings.tsx";
|
||||
import { Alert, Stack, Tabs } from "@mantine/core";
|
||||
import { Alert, Collapse, Stack, Tabs } from "@mantine/core";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
@@ -14,12 +16,16 @@ import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { DocumentTitle } from "@/components/ui/document-title.tsx";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
|
||||
export default function AiSettings() {
|
||||
const { t } = useTranslation();
|
||||
const { isAdmin } = useUserRole();
|
||||
const hasAccess = useHasFeature(Feature.AI);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
const workspace = useAtomValue(workspaceAtom);
|
||||
const aiChatEnabled = workspace?.settings?.ai?.chat === true;
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -61,7 +67,7 @@ export default function AiSettings() {
|
||||
mb="lg"
|
||||
>
|
||||
{t(
|
||||
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
|
||||
"AI is available in the Docmost paid editions. Contact sales@docmost.com.",
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
@@ -70,6 +76,20 @@ export default function AiSettings() {
|
||||
{!isCloud() && <EnableAiSearch />}
|
||||
<EnableGenerativeAi />
|
||||
<EnableAiChat />
|
||||
<Collapse expanded={aiChatEnabled}>
|
||||
<Stack
|
||||
gap="md"
|
||||
pl="md"
|
||||
ml="xs"
|
||||
style={{
|
||||
borderLeft:
|
||||
"2px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))",
|
||||
}}
|
||||
>
|
||||
<AiChatReadOnly />
|
||||
<AiChatWorkspaceKnowledgeOnly />
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
|
||||
|
||||
@@ -22,4 +22,5 @@ export const Feature = {
|
||||
PERSONAL_SPACES: 'spaces:personal',
|
||||
DOCX_EXPORT: 'export:docx',
|
||||
BASES: 'bases',
|
||||
AI_CONTROLS: 'ai:controls',
|
||||
} as const;
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface IWorkspace {
|
||||
generativeAi?: boolean;
|
||||
disablePublicSharing?: boolean;
|
||||
mcpEnabled?: boolean;
|
||||
aiChatReadOnly?: boolean;
|
||||
aiChatWorkspaceKnowledgeOnly?: boolean;
|
||||
trashRetentionDays?: number;
|
||||
restrictApiToAdmins?: boolean;
|
||||
allowMemberTemplates?: boolean;
|
||||
@@ -51,6 +53,8 @@ export interface IWorkspaceAiSettings {
|
||||
generative?: boolean;
|
||||
mcp?: boolean;
|
||||
chat?: boolean;
|
||||
chatReadOnly?: boolean;
|
||||
chatWorkspaceKnowledgeOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface IWorkspaceSharingSettings {
|
||||
|
||||
@@ -23,6 +23,7 @@ export const Feature = {
|
||||
PERSONAL_SPACES: 'spaces:personal',
|
||||
DOCX_EXPORT: 'export:docx',
|
||||
BASES: 'bases',
|
||||
AI_CONTROLS: 'ai:controls',
|
||||
} as const;
|
||||
|
||||
export type FeatureKey = (typeof Feature)[keyof typeof Feature];
|
||||
|
||||
@@ -54,7 +54,7 @@ export class CommentController {
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const page = await this.pageRepo.findById(createCommentDto.pageId);
|
||||
if (!page || page.deletedAt) {
|
||||
if (!page || page.workspaceId !== workspace.id || page.deletedAt) {
|
||||
throw new NotFoundException('Page not found');
|
||||
}
|
||||
|
||||
@@ -89,9 +89,10 @@ export class CommentController {
|
||||
@Body()
|
||||
pagination: PaginationOptions,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const page = await this.pageRepo.findById(input.pageId);
|
||||
if (!page) {
|
||||
if (!page || page.workspaceId !== workspace.id || page.deletedAt) {
|
||||
throw new NotFoundException('Page not found');
|
||||
}
|
||||
|
||||
@@ -102,14 +103,18 @@ export class CommentController {
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('info')
|
||||
async findOne(@Body() input: CommentIdDto, @AuthUser() user: User) {
|
||||
async findOne(
|
||||
@Body() input: CommentIdDto,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const comment = await this.commentRepo.findById(input.commentId);
|
||||
if (!comment) {
|
||||
throw new NotFoundException('Comment not found');
|
||||
}
|
||||
|
||||
const page = await this.pageRepo.findById(comment.pageId);
|
||||
if (!page) {
|
||||
if (!page || page.workspaceId !== workspace.id || page.deletedAt) {
|
||||
throw new NotFoundException('Page not found');
|
||||
}
|
||||
|
||||
@@ -130,7 +135,7 @@ export class CommentController {
|
||||
}
|
||||
|
||||
const page = await this.pageRepo.findById(comment.pageId);
|
||||
if (!page) {
|
||||
if (!page || page.workspaceId !== workspace.id || page.deletedAt) {
|
||||
throw new NotFoundException('Page not found');
|
||||
}
|
||||
|
||||
@@ -148,7 +153,7 @@ export class CommentController {
|
||||
}
|
||||
|
||||
const page = await this.pageRepo.findById(comment.pageId);
|
||||
if (!page) {
|
||||
if (!page || page.workspaceId !== workspace.id || page.deletedAt) {
|
||||
throw new NotFoundException('Page not found');
|
||||
}
|
||||
|
||||
|
||||
@@ -68,4 +68,12 @@ export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {
|
||||
@IsString()
|
||||
@IsIn(['read', 'edit'])
|
||||
defaultPageEditMode: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
aiChatReadOnly: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
aiChatWorkspaceKnowledgeOnly: boolean;
|
||||
}
|
||||
|
||||
@@ -334,7 +334,9 @@ export class WorkspaceService {
|
||||
typeof updateWorkspaceDto.restrictApiToAdmins !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.allowMemberTemplates !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.isScimEnabled !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined'
|
||||
typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.aiChatReadOnly !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly !== 'undefined'
|
||||
) {
|
||||
const ws = await this.db
|
||||
.selectFrom('workspaces')
|
||||
@@ -374,6 +376,21 @@ export class WorkspaceService {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof updateWorkspaceDto.aiChatReadOnly !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly !== 'undefined'
|
||||
) {
|
||||
if (
|
||||
!this.licenseCheckService.hasFeature(
|
||||
ws.licenseKey,
|
||||
Feature.AI_CONTROLS,
|
||||
ws.plan,
|
||||
)
|
||||
) {
|
||||
throw new ForbiddenException('This feature requires a valid license');
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof updateWorkspaceDto.disablePublicSharing !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.trashRetentionDays !== 'undefined' ||
|
||||
@@ -516,6 +533,34 @@ export class WorkspaceService {
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof updateWorkspaceDto.aiChatReadOnly !== 'undefined') {
|
||||
const prev = settingsBefore?.ai?.chatReadOnly ?? false;
|
||||
if (prev !== updateWorkspaceDto.aiChatReadOnly) {
|
||||
before.aiChatReadOnly = prev;
|
||||
after.aiChatReadOnly = updateWorkspaceDto.aiChatReadOnly;
|
||||
}
|
||||
await this.workspaceRepo.updateAiSettings(
|
||||
workspaceId,
|
||||
'chatReadOnly',
|
||||
updateWorkspaceDto.aiChatReadOnly,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly !== 'undefined') {
|
||||
const prev = settingsBefore?.ai?.chatWorkspaceKnowledgeOnly ?? false;
|
||||
if (prev !== updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly) {
|
||||
before.aiChatWorkspaceKnowledgeOnly = prev;
|
||||
after.aiChatWorkspaceKnowledgeOnly = updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly;
|
||||
}
|
||||
await this.workspaceRepo.updateAiSettings(
|
||||
workspaceId,
|
||||
'chatWorkspaceKnowledgeOnly',
|
||||
updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined') {
|
||||
const prev = settingsBefore?.spaces?.allowPersonal ?? false;
|
||||
if (prev !== updateWorkspaceDto.allowPersonalSpaces) {
|
||||
@@ -553,6 +598,8 @@ export class WorkspaceService {
|
||||
delete updateWorkspaceDto.aiChat;
|
||||
delete updateWorkspaceDto.allowPersonalSpaces;
|
||||
delete updateWorkspaceDto.defaultPageEditMode;
|
||||
delete updateWorkspaceDto.aiChatReadOnly;
|
||||
delete updateWorkspaceDto.aiChatWorkspaceKnowledgeOnly;
|
||||
|
||||
await this.workspaceRepo.updateWorkspace(
|
||||
updateWorkspaceDto,
|
||||
|
||||
+1
-1
Submodule apps/server/src/ee updated: 6dfbcb9241...62f7456089
Reference in New Issue
Block a user