Sfoglia il codice sorgente

Merge pull request #33044 from overleaf/worktree-mg-writefull-setting

Add writefull "AI Assistance" section

GitOrigin-RevId: c6d4cb60601c0b808cde96f29f6b79b26f631906
Malik Glossop 3 mesi fa
parent
commit
47473bc5f4

+ 1 - 0
services/web/config/settings.defaults.js

@@ -1071,6 +1071,7 @@ module.exports = {
     ],
     integrationPanelComponents: [],
     referenceSearchSetting: [],
+    settingsModalEditorTabSections: [],
     errorLogsComponents: [],
     referenceIndices: [],
     railEntries: [],

+ 6 - 0
services/web/frontend/extracted-translations.json

@@ -119,6 +119,7 @@
   "ai_assist_in_overleaf_is_included_via_writefull_individual": "",
   "ai_assist_subscriber_can_now_write_smarter": "",
   "ai_assist_unavailable_due_to_subscription_type": "",
+  "ai_assistance": "",
   "ai_can_make_mistakes": "",
   "ai_features": "",
   "ai_feedback_please_provide_more_detail": "",
@@ -128,6 +129,8 @@
   "ai_feedback_the_suggestion_didnt_fix_the_error": "",
   "ai_feedback_the_suggestion_wasnt_the_best_fix_available": "",
   "ai_feedback_there_was_no_code_fix_suggested": "",
+  "ai_shortcut_on_empty_lines": "",
+  "ai_shortcut_on_text_selection": "",
   "alignment": "",
   "all_borders": "",
   "all_events": "",
@@ -1424,6 +1427,8 @@
   "premium_feature": "",
   "premium_plan_label": "",
   "presentation_mode": "",
+  "press_shift_space_for_suggestions": "",
+  "press_space_to_open_the_ai_assistant": "",
   "preview": "",
   "preview_editor_tabs": "",
   "previous_page": "",
@@ -1733,6 +1738,7 @@
   "send_message": "",
   "send_request": "",
   "sending": "",
+  "sentence_completion": "",
   "server_error": "",
   "server_pro_license_entitlement_line_1": "",
   "server_pro_license_entitlement_line_2": "",

+ 1 - 1
services/web/frontend/js/features/settings/components/settings-modal-body.tsx

@@ -1,7 +1,7 @@
 import MaterialIcon from '@/shared/components/material-icon'
 
 import { Nav, NavLink, TabContainer, TabContent } from 'react-bootstrap'
-import { SettingsEntry } from '../context/settings-modal-context'
+import { SettingsEntry } from '../context/types'
 import SettingsTabPane from './settings-tab-pane'
 import BetaBadgeIcon from '@/shared/components/beta-badge-icon'
 import OLTooltip from '@/shared/components/ol/ol-tooltip'

+ 1 - 1
services/web/frontend/js/features/settings/components/settings-tab-pane.tsx

@@ -1,5 +1,5 @@
 import { TabPane } from 'react-bootstrap'
-import { SettingsTab } from '../context/settings-modal-context'
+import { SettingsTab } from '../context/types'
 import SettingsSection from './settings-section'
 import { Fragment } from 'react'
 

+ 23 - 30
services/web/frontend/js/features/settings/context/settings-modal-context.tsx

@@ -24,7 +24,6 @@ import EditorThemeSetting from '@/features/settings/components/appearance-settin
 import FontSizeSetting from '@/features/settings/components/appearance-settings/font-size-setting'
 import LineHeightSetting from '@/features/settings/components/appearance-settings/line-height-setting'
 import FontFamilySetting from '@/features/settings/components/appearance-settings/font-family-setting'
-import { AvailableUnfilledIcon } from '@/shared/components/material-icon'
 import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
 import DarkModePdfSetting from '@/features/settings/components/appearance-settings/dark-mode-pdf-setting'
 
@@ -32,41 +31,25 @@ import { useProjectSettingsContext } from '@/features/editor-left-menu/context/p
 import { useFeatureFlag } from '@/shared/context/split-test-context'
 import ProjectNotificationsSetting from '@/features/settings/components/editor-settings/project-notifications-setting'
 import getMeta from '@/utils/meta'
+import type {
+  SettingsEntry,
+  SettingsSection,
+  SettingsSectionHook,
+} from '@/features/settings/context/types'
 
 const [referenceSearchSettingModule] = importOverleafModules(
   'referenceSearchSetting'
 )
 const ReferenceSearchSetting = referenceSearchSettingModule?.import.default
 
-type Setting = {
-  key: string
-  component: React.ReactNode
-  hidden?: boolean
-}
-
-type SettingsSection = {
-  title?: string
-  key: string
-  settings: Setting[]
-}
-
-export type SettingsTab = {
-  key: string
-  icon: AvailableUnfilledIcon
-  sections: SettingsSection[]
-  title: string
-  hidden?: boolean
-}
-
-type SettingsLink = {
-  key: string
-  icon: AvailableUnfilledIcon
-  href: string
-  title: string
-  hidden?: boolean
-}
+const editorTabExtraSectionHooks: SettingsSectionHook[] = importOverleafModules(
+  'settingsModalEditorTabSections'
+)
+  .map((m: any) => m?.import?.default)
+  .filter((h: unknown): h is SettingsSectionHook => typeof h === 'function')
 
-export type SettingsEntry = SettingsLink | SettingsTab
+const useSlotSections = (hooks: SettingsSectionHook[]): SettingsSection[] =>
+  hooks.map(hook => hook()).filter((s): s is SettingsSection => s != null)
 
 type SettingsModalState = {
   show: boolean
@@ -94,6 +77,8 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
   const hasEmailNotifications = useFeatureFlag('email-notifications')
   const hasEditorTabs = useFeatureFlag('editor-tabs')
 
+  const editorTabExtraSections = useSlotSections(editorTabExtraSectionHooks)
+
   const allSettingsTabs: SettingsEntry[] = useMemo(
     () => [
       {
@@ -168,6 +153,7 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
               },
             ],
           },
+          ...editorTabExtraSections,
         ],
       },
       {
@@ -276,7 +262,14 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
         hidden: !isOverleaf,
       },
     ],
-    [t, overallTheme, hasEmailNotifications, isOverleaf, hasEditorTabs]
+    [
+      t,
+      hasEditorTabs,
+      overallTheme,
+      hasEmailNotifications,
+      isOverleaf,
+      editorTabExtraSections,
+    ]
   )
 
   const settingsTabs = useMemo(

+ 34 - 0
services/web/frontend/js/features/settings/context/types.ts

@@ -0,0 +1,34 @@
+import type { ReactNode } from 'react'
+import type { AvailableUnfilledIcon } from '@/shared/components/material-icon'
+
+export type Setting = {
+  key: string
+  component: ReactNode
+  hidden?: boolean
+}
+
+export type SettingsSection = {
+  title?: string
+  key: string
+  settings: Setting[]
+}
+
+export type SettingsSectionHook = () => SettingsSection | null
+
+export type SettingsTab = {
+  key: string
+  icon: AvailableUnfilledIcon
+  sections: SettingsSection[]
+  title: string
+  hidden?: boolean
+}
+
+export type SettingsLink = {
+  key: string
+  icon: AvailableUnfilledIcon
+  href: string
+  title: string
+  hidden?: boolean
+}
+
+export type SettingsEntry = SettingsLink | SettingsTab

+ 9 - 6
services/web/frontend/js/shared/components/notification.tsx

@@ -23,7 +23,7 @@ export type NotificationProps = {
   isDismissible?: boolean
   isActionBelowContent?: boolean
   onDismiss?: () => void
-  title?: string
+  title?: React.ReactNode
   type: NotificationType
   id?: string
 }
@@ -114,11 +114,14 @@ function Notification({
 
       <div className="notification-content-and-cta">
         <div className="notification-content">
-          {title && (
-            <p>
-              <b>{title}</b>
-            </p>
-          )}
+          {title &&
+            (typeof title === 'string' ? (
+              <p>
+                <b>{title}</b>
+              </p>
+            ) : (
+              title
+            ))}
           {content}
         </div>
         {action && <div className="notification-cta">{action}</div>}

+ 6 - 0
services/web/locales/en.json

@@ -152,6 +152,7 @@
   "ai_assist_in_overleaf_is_included_via_writefull_individual": "AI Assist in Overleaf is included as part of your Writefull subscription. You can cancel or manage your access to AI Assist in your Writefull subscription settings.",
   "ai_assist_subscriber_can_now_write_smarter": "AI Assist subscribers can now write smarter, find citations, and generate LaTeX from prompts and images.",
   "ai_assist_unavailable_due_to_subscription_type": "We’re sorry—it looks like AI Assist isn’t available to you just yet due to your current subscription type.",
+  "ai_assistance": "AI assistance",
   "ai_assistant": "AI Assistant",
   "ai_assistant_explanation": "A LaTeX-fluent AI Assistant built into your editor.",
   "ai_can_make_mistakes": "AI can make mistakes. Review fixes before you apply them.",
@@ -163,6 +164,8 @@
   "ai_feedback_the_suggestion_didnt_fix_the_error": "The suggestion didn’t fix the error",
   "ai_feedback_the_suggestion_wasnt_the_best_fix_available": "The suggestion wasn’t the best fix available",
   "ai_feedback_there_was_no_code_fix_suggested": "There was no code fix suggested",
+  "ai_shortcut_on_empty_lines": "AI shortcut on empty lines",
+  "ai_shortcut_on_text_selection": "AI shortcut on text selection",
   "ai_usage": "AI usage",
   "ai_usage_explanation": "Control AI usage globally for your organization",
   "alignment": "Alignment",
@@ -1902,6 +1905,8 @@
   "premium_plan_label": "You’re using <b>Overleaf Premium</b>",
   "presentation": "Presentation",
   "presentation_mode": "Presentation mode",
+  "press_shift_space_for_suggestions": "Press Shift+Space for suggestions",
+  "press_space_to_open_the_ai_assistant": "Press Space to open the AI assistant",
   "preview": "Preview",
   "preview_editor_tabs": "Preview editor tabs",
   "previous_24_hours_only": "previous 24 hours only",
@@ -2277,6 +2282,7 @@
   "send_test_email": "Send a test email",
   "sending": "Sending",
   "sent": "Sent",
+  "sentence_completion": "Sentence completion",
   "september": "September",
   "server_error": "Server Error",
   "server_pro_license_entitlement_line_1": "<0>__appName__</0> Server Pro license",

+ 92 - 0
services/web/test/frontend/features/settings-modal/settings-modal.test.tsx

@@ -130,6 +130,98 @@ describe('<SettingsModal />', function () {
     })
   })
 
+  describe('when a user has Writefull enabled', function () {
+    beforeEach(function () {
+      window.metaAttributesCache.set('ol-writefullEnabled', true)
+      window.metaAttributesCache.set('ol-showAiFeatures', true)
+    })
+
+    afterEach(function () {
+      window.metaAttributesCache.delete('ol-writefullEnabled')
+      window.metaAttributesCache.delete('ol-showAiFeatures')
+    })
+
+    it('shows the AI assistance section in the Editor tab', async function () {
+      render(
+        <EditorProviders
+          rootFolder={[rootFolder as any]}
+          layoutContext={{ leftMenuShown: true }}
+        >
+          <SettingsModal />
+        </EditorProviders>
+      )
+
+      selectTab('Editor')
+      await waitFor(() => expect(screen.getByText('AI assistance')).to.exist)
+    })
+  })
+
+  describe('when a user does not have Writefull enabled', function () {
+    afterEach(function () {
+      window.metaAttributesCache.delete('ol-writefullEnabled')
+      window.metaAttributesCache.delete('ol-showAiFeatures')
+      window.metaAttributesCache.delete('ol-cannot-use-ai')
+    })
+
+    it('does not show the AI assistance section when ol-writefullEnabled is false', async function () {
+      window.metaAttributesCache.set('ol-writefullEnabled', false)
+      window.metaAttributesCache.set('ol-showAiFeatures', true)
+      render(
+        <EditorProviders
+          rootFolder={[rootFolder as any]}
+          layoutContext={{ leftMenuShown: true }}
+        >
+          <SettingsModal />
+        </EditorProviders>
+      )
+
+      selectTab('Editor')
+      await waitFor(
+        () => expect(screen.getByLabelText('Auto-complete')).to.exist
+      )
+      expect(screen.queryByText('AI assistance')).to.be.null
+    })
+
+    it('does not show the AI assistance section when ol-showAiFeatures is false', async function () {
+      window.metaAttributesCache.set('ol-writefullEnabled', true)
+      window.metaAttributesCache.set('ol-showAiFeatures', false)
+      render(
+        <EditorProviders
+          rootFolder={[rootFolder as any]}
+          layoutContext={{ leftMenuShown: true }}
+        >
+          <SettingsModal />
+        </EditorProviders>
+      )
+
+      selectTab('Editor')
+      await waitFor(
+        () => expect(screen.getByLabelText('Auto-complete')).to.exist
+      )
+      expect(screen.queryByText('AI assistance')).to.be.null
+    })
+
+    it('does not show the AI assistance section when ol-cannot-use-ai is true', async function () {
+      window.metaAttributesCache.set('ol-writefullEnabled', true)
+      window.metaAttributesCache.set('ol-showAiFeatures', true)
+      window.metaAttributesCache.set('ol-cannot-use-ai', true)
+      render(
+        <EditorProviders
+          rootFolder={[rootFolder as any]}
+          layoutContext={{ leftMenuShown: true }}
+        >
+          <SettingsModal />
+        </EditorProviders>
+      )
+
+      selectTab('Editor')
+      await waitFor(
+        () => expect(screen.getByLabelText('Auto-complete')).to.exist
+      )
+      expect(screen.queryByText('AI assistance')).to.be.null
+    })
+  })
+
   describe('when open=project-notifications query param is present', function () {
     beforeEach(function () {
       window.metaAttributesCache.set('ol-splitTestVariants', {