diff --git a/server-ce/test/editor.spec.ts b/server-ce/test/editor.spec.ts index 773121141f..1568b12f8d 100644 --- a/server-ce/test/editor.spec.ts +++ b/server-ce/test/editor.spec.ts @@ -44,6 +44,7 @@ describe('editor', function () { cy.log(`change project language to '${lng}'`) cy.findByRole('button', { name: 'Settings' }).click() cy.findByRole('dialog').within(() => { + cy.findByRole('tab', { name: 'Spelling and language' }).click() cy.findByLabelText('Spellcheck language').select(lng) }) cy.get('body').type('{esc}') @@ -76,6 +77,7 @@ describe('editor', function () { cy.log('remove word from dictionary') cy.findByRole('button', { name: 'Settings' }).click() cy.findByRole('dialog').within(() => { + cy.findByRole('tab', { name: 'Spelling and language' }).click() cy.findByLabelText('Dictionary').click() }) cy.findByTestId('dictionary-modal').within(() => { diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index 569d9babcd..83c5296497 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -1090,6 +1090,7 @@ module.exports = { integrationPanelComponents: [], referenceSearchSetting: [], settingsModalEditorTabSections: [], + settingsModalSpellcheckSections: [], errorLogsComponents: [], referenceIndices: [], railEntries: [], diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 7747d0724b..f0455fc789 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -133,6 +133,7 @@ "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_language_suggestions": "", "ai_shortcut_on_empty_lines": "", "ai_shortcut_on_text_selection": "", "alignment": "", @@ -216,6 +217,7 @@ "binary_history_error": "", "blank_project": "", "blocked_filename": "", + "blocked_language_suggestions": "", "blog": "", "bold": "", "booktabs": "", @@ -413,6 +415,8 @@ "currently_subscribed_to_plan": "", "custom": "", "custom_borders": "", + "custom_prompt": "", + "custom_prompt_example": "", "customer_stories": "", "customize_your_group_subscription": "", "customizing_figures": "", @@ -613,6 +617,9 @@ "enabling": "", "end_of_document": "", "end_time_utc": "", + "english__preference_for_ai_suggestion": "", + "english_american": "", + "english_british": "", "ensure_recover_account": "", "enter_any_size_including_units_or_valid_latex_command": "", "enter_emails_separated_by_commas": "", @@ -1042,6 +1049,13 @@ "labs_ongoing_experiments": "", "labs_settings": "", "language": "", + "language_model_gpt_any_language": "", + "language_model_gpt_any_language_description": "", + "language_model_gpt_custom_prompt": "", + "language_model_gpt_custom_prompt_description": "", + "language_model_writefull_english": "", + "language_model_writefull_english_description": "", + "language_suggestions": "", "last_active": "", "last_active_description": "", "last_edit": "", @@ -1205,6 +1219,7 @@ "missing_field_for_entry": "", "missing_fields_for_entry": "", "missing_payment_details": "", + "model": "", "money_back_guarantee": "", "month": "", "month_plural": "", @@ -1857,6 +1872,7 @@ "speech_input_not_available": "", "spellcheck": "", "spellcheck_language": "", + "spelling_and_language": "", "split_view": "", "sso": "", "sso_active": "", diff --git a/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 b/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 index 5ca6ba1da4..dab440a52b 100644 Binary files a/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 and b/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 differ diff --git a/services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs b/services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs index 2ea68a635b..f905407cbd 100644 --- a/services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs +++ b/services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs @@ -64,6 +64,7 @@ export default /** @type {const} */ ([ 'shuffle', 'smart_toy', 'space_dashboard', + 'spellcheck', 'star', 'strikethrough_s', 'table_chart', diff --git a/services/web/frontend/js/features/settings/context/settings-modal-context.tsx b/services/web/frontend/js/features/settings/context/settings-modal-context.tsx index 285ee16c41..bf3da53043 100644 --- a/services/web/frontend/js/features/settings/context/settings-modal-context.tsx +++ b/services/web/frontend/js/features/settings/context/settings-modal-context.tsx @@ -48,6 +48,11 @@ const editorTabExtraSectionHooks: SettingsSectionHook[] = importOverleafModules( .map((m: any) => m?.import?.default) .filter((h: unknown): h is SettingsSectionHook => typeof h === 'function') +const spellcheckExtraSectionHooks: SettingsSectionHook[] = + importOverleafModules('settingsModalSpellcheckSections') + .map((m: any) => m?.import?.default) + .filter((h: unknown): h is SettingsSectionHook => typeof h === 'function') + const useSlotSections = (hooks: SettingsSectionHook[]): SettingsSection[] => hooks.map(hook => hook()).filter((s): s is SettingsSection => s != null) @@ -78,6 +83,7 @@ export const SettingsModalProvider: FC = ({ const hasEditorTabs = useFeatureFlag('editor-tabs') const editorTabExtraSections = useSlotSections(editorTabExtraSectionHooks) + const spellcheckExtraSections = useSlotSections(spellcheckExtraSectionHooks) const allSettingsTabs: SettingsEntry[] = useMemo( () => [ @@ -125,20 +131,6 @@ export const SettingsModalProvider: FC = ({ }, ], }, - { - key: 'spellcheck', - title: t('spellcheck'), - settings: [ - { - key: 'spellCheckLanguage', - component: , - }, - { - key: 'dictionary-settings', - component: , - }, - ], - }, { key: 'tools', title: t('tools'), @@ -156,6 +148,28 @@ export const SettingsModalProvider: FC = ({ ...editorTabExtraSections, ], }, + { + key: 'spelling_and_language', + title: t('spelling_and_language'), + icon: 'spellcheck', + sections: [ + { + key: 'spellcheck', + title: t('spellcheck'), + settings: [ + { + key: 'spellCheckLanguage', + component: , + }, + { + key: 'dictionary-settings', + component: , + }, + ], + }, + ...spellcheckExtraSections, + ], + }, { key: 'compiler', title: t('compiler'), @@ -269,6 +283,7 @@ export const SettingsModalProvider: FC = ({ hasEmailNotifications, isOverleaf, editorTabExtraSections, + spellcheckExtraSections, ] ) diff --git a/services/web/frontend/js/shared/components/select.tsx b/services/web/frontend/js/shared/components/select.tsx index 3b5f36a77f..46e6f89ec3 100644 --- a/services/web/frontend/js/shared/components/select.tsx +++ b/services/web/frontend/js/shared/components/select.tsx @@ -5,6 +5,7 @@ import { useCallback, ReactNode, useState, + useId, } from 'react' import classNames from 'classnames' import { useSelect } from 'downshift' @@ -14,12 +15,47 @@ import FormControl from '@/shared/components/form/form-control' import MaterialIcon from '@/shared/components/material-icon' import { CaretUp, CaretDown, Check } from '@phosphor-icons/react' import { DropdownItem } from '@/shared/components/dropdown/dropdown-menu' +import OLOverlay from '@/shared/components/ol/ol-overlay' import OLSpinner from './ol/ol-spinner' import DSFormLabel from '@/shared/components/ds/ds-form-label' import DSFormGroup from '@/shared/components/ds/ds-form-group' import DSFormControl from '@/shared/components/ds/ds-form-control' import { DropdownItemProps } from '@/shared/components/types/dropdown-menu-props' +function SelectMenuPopover({ + show, + target, + onHide, + children, +}: { + show: boolean + target: HTMLElement | null + onHide: () => void + children: ReactNode +}) { + const id = useId() + return ( + + {({ ref, style }) => ( +
+ {children} +
+ )} +
+ ) +} + export type SelectProps = { // The items rendered as dropdown options. items: T[] @@ -63,6 +99,11 @@ export type SelectProps = { // CIAM-specific layout isCiam?: boolean size?: React.ComponentProps['size'] + // Renders the menu in a portal so it escapes overflow-clipping ancestors. + portal?: boolean + // Optional id for the toggle button element. When provided, enables association + // with an external