diff --git a/Dockerfile b/Dockerfile index c8f6a6f46..8975beab5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM node:26-slim AS base LABEL org.opencontainers.image.source="https://github.com/docmost/docmost" -RUN npm install -g pnpm@11.15.1 +RUN npm install -g pnpm@11.23.0 FROM base AS builder @@ -18,6 +18,15 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends curl bash \ && rm -rf /var/lib/apt/lists/* +# drop npm and corepack +RUN rm -rf /usr/local/lib/node_modules/npm \ + && rm -rf /usr/local/lib/node_modules/corepack \ + && rm -rf /usr/local/bin/npm \ + && rm -rf /usr/local/bin/npx \ + && rm -rf /usr/local/bin/corepack \ + && rm -rf /root/.npm \ + && rm -rf /root/.node-gyp + WORKDIR /app # Copy apps diff --git a/apps/client/package.json b/apps/client/package.json index e5bb5ad5a..d836d5db6 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -52,7 +52,7 @@ "mantine-form-zod-resolver": "1.3.0", "mermaid": "11.16.1", "mitt": "3.0.1", - "nanoid": "3.3.17", + "nanoid": "3.3.18", "posthog-js": "1.391.2", "react": "19.2.7", "react-clear-modal": "^2.0.18", @@ -64,6 +64,7 @@ "react-router-dom": "7.18.2", "semver": "7.7.4", "socket.io-client": "4.8.3", + "yet-another-react-lightbox": "^3.32.2", "zod": "4.3.6" }, "devDependencies": { diff --git a/apps/client/public/locales/de-DE/translation.json b/apps/client/public/locales/de-DE/translation.json index f067ecc27..084c92ef5 100644 --- a/apps/client/public/locales/de-DE/translation.json +++ b/apps/client/public/locales/de-DE/translation.json @@ -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", diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index e93010c68..a5bf72589 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -294,6 +294,7 @@ "Export space": "Export space", "Export {{type}}": "Export {{type}}", "File exceeds the {{limit}} attachment limit": "File exceeds the {{limit}} attachment limit", + "Media": "Media", "Align left": "Align left", "Align right": "Align right", "Align center": "Align center", @@ -698,7 +699,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", @@ -1332,5 +1333,13 @@ "Applications and AI assistants you have authorized to access your account.": "Applications and AI assistants you have authorized to access your account.", "Your workspace has MCP enabled. Connect AI assistants with your Docmost account, or with an API key.": "Your workspace has MCP enabled. Connect AI assistants with your Docmost account, or with an API key.", "Authorized apps": "Authorized apps", - "No authorized apps yet.": "No authorized apps yet." + "No authorized apps yet.": "No authorized apps yet.", + "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", + "Title only": "Title only", + "you": "you" } diff --git a/apps/client/public/locales/es-ES/translation.json b/apps/client/public/locales/es-ES/translation.json index fd834a2c6..fb5364725 100644 --- a/apps/client/public/locales/es-ES/translation.json +++ b/apps/client/public/locales/es-ES/translation.json @@ -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", diff --git a/apps/client/public/locales/fr-FR/translation.json b/apps/client/public/locales/fr-FR/translation.json index 04ebbc6ad..e9fbeed06 100644 --- a/apps/client/public/locales/fr-FR/translation.json +++ b/apps/client/public/locales/fr-FR/translation.json @@ -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", diff --git a/apps/client/public/locales/it-IT/translation.json b/apps/client/public/locales/it-IT/translation.json index e3aaa82ce..81eb1727f 100644 --- a/apps/client/public/locales/it-IT/translation.json +++ b/apps/client/public/locales/it-IT/translation.json @@ -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", diff --git a/apps/client/public/locales/ja-JP/translation.json b/apps/client/public/locales/ja-JP/translation.json index e408bfda8..c1bfa1652 100644 --- a/apps/client/public/locales/ja-JP/translation.json +++ b/apps/client/public/locales/ja-JP/translation.json @@ -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", diff --git a/apps/client/public/locales/ko-KR/translation.json b/apps/client/public/locales/ko-KR/translation.json index fb8e9884a..a7d795454 100644 --- a/apps/client/public/locales/ko-KR/translation.json +++ b/apps/client/public/locales/ko-KR/translation.json @@ -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", diff --git a/apps/client/public/locales/nl-NL/translation.json b/apps/client/public/locales/nl-NL/translation.json index 59c1d7a73..1f1465ab1 100644 --- a/apps/client/public/locales/nl-NL/translation.json +++ b/apps/client/public/locales/nl-NL/translation.json @@ -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", diff --git a/apps/client/public/locales/pt-BR/translation.json b/apps/client/public/locales/pt-BR/translation.json index 2e4cd89ef..82f213703 100644 --- a/apps/client/public/locales/pt-BR/translation.json +++ b/apps/client/public/locales/pt-BR/translation.json @@ -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", diff --git a/apps/client/public/locales/ru-RU/translation.json b/apps/client/public/locales/ru-RU/translation.json index bae44d133..9d4ddc8a6 100644 --- a/apps/client/public/locales/ru-RU/translation.json +++ b/apps/client/public/locales/ru-RU/translation.json @@ -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", diff --git a/apps/client/public/locales/uk-UA/translation.json b/apps/client/public/locales/uk-UA/translation.json index d3dd687ea..be09a646f 100644 --- a/apps/client/public/locales/uk-UA/translation.json +++ b/apps/client/public/locales/uk-UA/translation.json @@ -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", diff --git a/apps/client/public/locales/zh-CN/translation.json b/apps/client/public/locales/zh-CN/translation.json index 97ff9ce85..1b273e71c 100644 --- a/apps/client/public/locales/zh-CN/translation.json +++ b/apps/client/public/locales/zh-CN/translation.json @@ -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", diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index 8d12779cc..4207c3006 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -1,61 +1,94 @@ +import { lazy, Suspense, useEffect } from "react"; import { Navigate, Route, Routes } from "react-router-dom"; -import SetupWorkspace from "@/pages/auth/setup-workspace.tsx"; -import LoginPage from "@/pages/auth/login"; -import Home from "@/pages/dashboard/home"; -import Page from "@/pages/page/page"; -import AccountSettings from "@/pages/settings/account/account-settings"; -import WorkspaceMembers from "@/pages/settings/workspace/workspace-members"; -import WorkspaceSettings from "@/pages/settings/workspace/workspace-settings"; -import Groups from "@/pages/settings/group/groups"; -import GroupInfo from "./pages/settings/group/group-info"; -import Spaces from "@/pages/settings/space/spaces.tsx"; -import { Error404 } from "@/components/ui/error-404.tsx"; -import AccountPreferences from "@/pages/settings/account/account-preferences.tsx"; -import SpaceHome from "@/pages/space/space-home.tsx"; -import PageRedirect from "@/pages/page/page-redirect.tsx"; import Layout from "@/components/layouts/global/layout.tsx"; -import InviteSignup from "@/pages/auth/invite-signup.tsx"; -import ForgotPassword from "@/pages/auth/forgot-password.tsx"; -import PasswordReset from "./pages/auth/password-reset"; -import Billing from "@/ee/billing/pages/billing.tsx"; -import CloudLogin from "@/ee/pages/cloud-login.tsx"; -import CreateWorkspace from "@/ee/pages/create-workspace.tsx"; +import { Error404 } from "@/components/ui/error-404.tsx"; import { isCloud } from "@/lib/config.ts"; import { useTranslation } from "react-i18next"; -import Security from "@/ee/security/pages/security.tsx"; -import License from "@/ee/licence/pages/license.tsx"; import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.tsx"; -import SharedPage from "@/pages/share/shared-page.tsx"; -import PdfRenderPage from "@/ee/pdf-export/pdf-render-page.tsx"; -import Shares from "@/pages/settings/shares/shares.tsx"; -import ShareLayout from "@/features/share/components/share-layout.tsx"; -import ShareRedirect from "@/pages/share/share-redirect.tsx"; import { useTrackOrigin } from "@/hooks/use-track-origin"; -import SpacesPage from "@/pages/spaces/spaces.tsx"; -import { MfaChallengePage } from "@/ee/mfa/pages/mfa-challenge-page"; -import { MfaSetupRequiredPage } from "@/ee/mfa/pages/mfa-setup-required-page"; -import SpaceTrash from "@/pages/space/space-trash.tsx"; -import UserApiKeys from "@/ee/api-key/pages/user-api-keys"; -import WorkspaceApiKeys from "@/ee/api-key/pages/workspace-api-keys"; -import AiSettings from "@/ee/ai/pages/ai-settings.tsx"; -import BasePage from "@/ee/base/pages/base-page.tsx"; -import AuditLogs from "@/ee/audit/pages/audit-logs.tsx"; -import VerifiedPages from "@/ee/page-verification/pages/verified-pages.tsx"; -import TemplateList from "@/ee/template/pages/template-list"; -import TemplateEditor from "@/ee/template/pages/template-editor"; -import FavoritesPage from "@/pages/favorites/favorites-page"; -import AiChat from "@/ee/ai-chat/pages/ai-chat.tsx"; -import VerifyEmail from "@/ee/pages/verify-email.tsx"; -import LabelPage from "@/pages/label/label-page"; -import OAuthConsent from "@/ee/oauth/pages/oauth-consent.tsx"; + + +const SetupWorkspace = lazy(() => import("@/pages/auth/setup-workspace.tsx")); +const LoginPage = lazy(() => import("@/pages/auth/login")); +const Home = lazy(() => import("@/pages/dashboard/home")); +const Page = lazy(() => import("@/pages/page/page")); +const AccountSettings = lazy( + () => import("@/pages/settings/account/account-settings"), +); +const WorkspaceMembers = lazy( + () => import("@/pages/settings/workspace/workspace-members"), +); +const WorkspaceSettings = lazy( + () => import("@/pages/settings/workspace/workspace-settings"), +); +const Groups = lazy(() => import("@/pages/settings/group/groups")); +const GroupInfo = lazy(() => import("./pages/settings/group/group-info")); +const Spaces = lazy(() => import("@/pages/settings/space/spaces.tsx")); +const AccountPreferences = lazy( + () => import("@/pages/settings/account/account-preferences.tsx"), +); +const SpaceHome = lazy(() => import("@/pages/space/space-home.tsx")); +const PageRedirect = lazy(() => import("@/pages/page/page-redirect.tsx")); +const InviteSignup = lazy(() => import("@/pages/auth/invite-signup.tsx")); +const ForgotPassword = lazy(() => import("@/pages/auth/forgot-password.tsx")); +const PasswordReset = lazy(() => import("./pages/auth/password-reset")); +const Billing = lazy(() => import("@/ee/billing/pages/billing.tsx")); +const CloudLogin = lazy(() => import("@/ee/pages/cloud-login.tsx")); +const CreateWorkspace = lazy(() => import("@/ee/pages/create-workspace.tsx")); +const Security = lazy(() => import("@/ee/security/pages/security.tsx")); +const License = lazy(() => import("@/ee/licence/pages/license.tsx")); +const SharedPage = lazy(() => import("@/pages/share/shared-page.tsx")); +const PdfRenderPage = lazy(() => import("@/ee/pdf-export/pdf-render-page.tsx")); +const Shares = lazy(() => import("@/pages/settings/shares/shares.tsx")); +const ShareLayout = lazy( + () => import("@/features/share/components/share-layout.tsx"), +); +const ShareRedirect = lazy(() => import("@/pages/share/share-redirect.tsx")); +const SpacesPage = lazy(() => import("@/pages/spaces/spaces.tsx")); +const MfaChallengePage = lazy(() => + import("@/ee/mfa/pages/mfa-challenge-page").then((m) => ({ + default: m.MfaChallengePage, + })), +); +const MfaSetupRequiredPage = lazy(() => + import("@/ee/mfa/pages/mfa-setup-required-page").then((m) => ({ + default: m.MfaSetupRequiredPage, + })), +); +const SpaceTrash = lazy(() => import("@/pages/space/space-trash.tsx")); +const UserApiKeys = lazy(() => import("@/ee/api-key/pages/user-api-keys")); +const WorkspaceApiKeys = lazy( + () => import("@/ee/api-key/pages/workspace-api-keys"), +); +const AiSettings = lazy(() => import("@/ee/ai/pages/ai-settings.tsx")); +const BasePage = lazy(() => import("@/ee/base/pages/base-page.tsx")); +const AuditLogs = lazy(() => import("@/ee/audit/pages/audit-logs.tsx")); +const VerifiedPages = lazy( + () => import("@/ee/page-verification/pages/verified-pages.tsx"), +); +const TemplateList = lazy(() => import("@/ee/template/pages/template-list")); +const TemplateEditor = lazy( + () => import("@/ee/template/pages/template-editor"), +); +const FavoritesPage = lazy(() => import("@/pages/favorites/favorites-page")); +const AiChat = lazy(() => import("@/ee/ai-chat/pages/ai-chat.tsx")); +const VerifyEmail = lazy(() => import("@/ee/pages/verify-email.tsx")); +const LabelPage = lazy(() => import("@/pages/label/label-page")); +const OAuthConsent = lazy(() => import("@/ee/oauth/pages/oauth-consent.tsx")); export default function App() { const { t } = useTranslation(); useRedirectToCloudSelect(); useTrackOrigin(); + useEffect(() => { + // warm the editor chunk so opening a page doesn't wait on the network + const timer = setTimeout(() => import("@/pages/page/page"), 3000); + return () => clearTimeout(timer); + }, []); + return ( - <> + } /> } /> @@ -141,6 +174,6 @@ export default function App() { } /> - + ); } diff --git a/apps/client/src/components/layouts/global/aside.tsx b/apps/client/src/components/layouts/global/aside.tsx index 556adbf17..856e02141 100644 --- a/apps/client/src/components/layouts/global/aside.tsx +++ b/apps/client/src/components/layouts/global/aside.tsx @@ -1,17 +1,30 @@ import { ActionIcon, Box, Group, ScrollArea, Title, Tooltip } from "@mantine/core"; import { IconX } from "@tabler/icons-react"; -import CommentListWithTabs from "@/features/comment/components/comment-list-with-tabs.tsx"; import { useAtom } from "jotai"; import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; -import React, { ReactNode, useEffect } from "react"; +import React, { lazy, ReactNode, Suspense, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { TableOfContents } from "@/features/editor/components/table-of-contents/table-of-contents.tsx"; import { useAtomValue } from "jotai"; import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms.ts"; -import AsideChatPanel from "@/ee/ai-chat/components/aside-chat-panel"; -import { PageDetailsAside } from "@/features/page-details/components/page-details-aside.tsx"; import { ASIDE_PANEL_ID } from "@/hooks/use-toggle-aside.tsx"; +const CommentListWithTabs = lazy( + () => import("@/features/comment/components/comment-list-with-tabs.tsx"), +); +const TableOfContents = lazy(() => + import( + "@/features/editor/components/table-of-contents/table-of-contents.tsx" + ).then((m) => ({ default: m.TableOfContents })), +); +const AsideChatPanel = lazy( + () => import("@/ee/ai-chat/components/aside-chat-panel"), +); +const PageDetailsAside = lazy(() => + import("@/features/page-details/components/page-details-aside.tsx").then( + (m) => ({ default: m.PageDetailsAside }), + ), +); + export default function Aside() { const [{ tab, isAsideOpen }, setAsideState] = useAtom(asideStateAtom); const { t } = useTranslation(); @@ -68,17 +81,19 @@ export default function Aside() { )} - {tab === "comments" || tab === "chat" ? ( - component - ) : ( - -
{component}
-
- )} + + {tab === "comments" || tab === "chat" ? ( + component + ) : ( + +
{component}
+
+ )} +
)} diff --git a/apps/client/src/components/layouts/global/global-app-shell.tsx b/apps/client/src/components/layouts/global/global-app-shell.tsx index f35029d40..272ceba62 100644 --- a/apps/client/src/components/layouts/global/global-app-shell.tsx +++ b/apps/client/src/components/layouts/global/global-app-shell.tsx @@ -11,7 +11,10 @@ import { sidebarWidthAtom, } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx"; -import AiChatSidebar from "@/ee/ai-chat/components/ai-chat-sidebar.tsx"; + +const AiChatSidebar = React.lazy( + () => import("@/ee/ai-chat/components/ai-chat-sidebar.tsx"), +); import { AppHeader } from "@/components/layouts/global/app-header.tsx"; import Aside from "@/components/layouts/global/aside.tsx"; import classes from "./app-shell.module.css"; @@ -126,7 +129,11 @@ export default function GlobalAppShell({ )} {isSpaceRoute && } {isSettingsRoute && } - {isAiRoute && } + {isAiRoute && ( + + + + )} {showGlobalSidebar && } diff --git a/apps/client/src/components/ui/checkbox-menu-item.tsx b/apps/client/src/components/ui/checkbox-menu-item.tsx new file mode 100644 index 000000000..12b1f990e --- /dev/null +++ b/apps/client/src/components/ui/checkbox-menu-item.tsx @@ -0,0 +1,12 @@ +import { UnstyledButton } from "@mantine/core"; +import { type ComponentPropsWithoutRef, forwardRef } from "react"; + +// Menu.Item hard-codes role="menuitem"; use as its `component` to restore role="menuitemcheckbox" so aria-checked works. +export const CheckboxMenuItem = forwardRef< + HTMLButtonElement, + ComponentPropsWithoutRef<"button"> +>((props, ref) => ( + +)); + +CheckboxMenuItem.displayName = "CheckboxMenuItem"; diff --git a/apps/client/src/ee/ai-chat/components/ai-chat-read-only.tsx b/apps/client/src/ee/ai-chat/components/ai-chat-read-only.tsx new file mode 100644 index 000000000..732800299 --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/ai-chat-read-only.tsx @@ -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 ( + +
+ + {t("Read-only mode")} + {!hasAccess && ( + + {t("Enterprise")} + + )} + + + {t( + "AI Chat can search and read workspace content, but cannot create or edit pages.", + )} + +
+ + +
+ ); +} + +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) => { + 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 ( + + + + ); +} diff --git a/apps/client/src/ee/ai-chat/components/ai-chat-workspace-knowledge-only.tsx b/apps/client/src/ee/ai-chat/components/ai-chat-workspace-knowledge-only.tsx new file mode 100644 index 000000000..497a337b4 --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/ai-chat-workspace-knowledge-only.tsx @@ -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 ( + +
+ + {t("Workspace knowledge only")} + {!hasAccess && ( + + {t("Enterprise")} + + )} + + + {t( + "Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.", + )} + +
+ + +
+ ); +} + +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) => { + 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 ( + + + + ); +} diff --git a/apps/client/src/ee/ai-chat/components/chat-empty-state.tsx b/apps/client/src/ee/ai-chat/components/chat-empty-state.tsx index 9583a2f03..423557c53 100644 --- a/apps/client/src/ee/ai-chat/components/chat-empty-state.tsx +++ b/apps/client/src/ee/ai-chat/components/chat-empty-state.tsx @@ -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: , text: "Create a new page", prompt: "Create a new page titled ", + write: true, }, { icon: , @@ -36,6 +41,7 @@ const SUGGESTIONS: Suggestion[] = [ icon: , 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(null); const handleSuggestionClick = (prompt: string) => { - onSend(prompt, [], []); + inputRef.current?.prefill(prompt); }; return ( @@ -62,6 +72,7 @@ export default function ChatEmptyState({ isStreaming, onSend, onStop }: Props) {

{t("Get started")}

- {SUGGESTIONS.map((s) => ( + {SUGGESTIONS.filter((s) => !writesDisabled || !s.write).map((s) => (