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

Add writefull "AI Assistance" section

GitOrigin-RevId: c6d4cb60601c0b808cde96f29f6b79b26f631906
This commit is contained in:
Malik Glossop
2026-05-04 12:56:24 +02:00
committed by Copybot
parent ed3f9517fd
commit 47473bc5f4
9 changed files with 173 additions and 38 deletions

View File

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

View File

@@ -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": "",

View File

@@ -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'

View File

@@ -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'

View File

@@ -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
}
const editorTabExtraSectionHooks: SettingsSectionHook[] = importOverleafModules(
'settingsModalEditorTabSections'
)
.map((m: any) => m?.import?.default)
.filter((h: unknown): h is SettingsSectionHook => typeof h === 'function')
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
}
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(

View File

@@ -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

View File

@@ -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>}

View File

@@ -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": "Were sorry—it looks like AI Assist isnt 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 didnt fix the error",
"ai_feedback_the_suggestion_wasnt_the_best_fix_available": "The suggestion wasnt 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": "Youre 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",

View File

@@ -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', {