[web] Add AI workbench to alpha users (#29417)

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>
GitOrigin-RevId: 79bb329932b1e6fcc88f648bca9cc4bee215cd41
This commit is contained in:
Mathias Jakobsen
2025-11-11 09:06:08 +00:00
committed by Copybot
co-authored by Alf Eaton
parent c84cfc815a
commit 8024fe2c58
49 changed files with 8446 additions and 127 deletions
+1
View File
@@ -2,6 +2,7 @@
.eslint*
.prettier*
libraries/access-token-encryptor/**
libraries/ai/**
libraries/eslint-plugin/**
libraries/fetch-utils/**
libraries/logger/**
+3
View File
@@ -24,6 +24,7 @@ FROM base AS deps-prod
COPY package.json package-lock.json /overleaf/
COPY libraries/access-token-encryptor/package.json /overleaf/libraries/access-token-encryptor/package.json
COPY libraries/ai/package.json /overleaf/libraries/ai/package.json
COPY libraries/eslint-plugin/package.json /overleaf/libraries/eslint-plugin/package.json
COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
@@ -58,6 +59,7 @@ FROM deps AS dev
ARG SENTRY_RELEASE
ENV SENTRY_RELEASE=$SENTRY_RELEASE
COPY libraries/access-token-encryptor/ /overleaf/libraries/access-token-encryptor/
COPY libraries/ai/ /overleaf/libraries/ai/
COPY libraries/eslint-plugin/ /overleaf/libraries/eslint-plugin/
COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
COPY libraries/logger/ /overleaf/libraries/logger/
@@ -95,6 +97,7 @@ RUN nice find /overleaf/services/web/public -name '*.js.map' -delete
# copy source code and precompile pug images
FROM deps-prod AS pug
COPY libraries/access-token-encryptor/ /overleaf/libraries/access-token-encryptor/
COPY libraries/ai/ /overleaf/libraries/ai/
COPY libraries/eslint-plugin/ /overleaf/libraries/eslint-plugin/
COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
COPY libraries/logger/ /overleaf/libraries/logger/
+1
View File
@@ -570,6 +570,7 @@ IMAGE_SCRATCH ?= $(IMAGE_REPO):do-not-use-this-tag-for-deploys--it-is-used-for-e
IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
$(MONOREPO)/package.json \
$(MONOREPO)/package-lock.json \
$(MONOREPO)/libraries/ai/package.json \
$(MONOREPO)/libraries/access-token-encryptor/package.json \
$(MONOREPO)/libraries/eslint-plugin/package.json \
$(MONOREPO)/libraries/fetch-utils/package.json \
@@ -403,6 +403,7 @@ const _ProjectController = {
'writefull-frontend-migration',
'chat-edit-delete',
'compile-timeout-remove-info',
'ai-workbench',
'compile-timeout-target-plans',
].filter(Boolean)
@@ -9,6 +9,7 @@ const Usage = new Schema({
const UserFeatureUsageSchema = new Schema({
features: {
aiErrorAssistant: Usage,
aiWorkbench: Usage,
},
})
+1
View File
@@ -1040,6 +1040,7 @@ module.exports = {
referenceSearchSetting: [],
errorLogsComponents: [],
referenceIndices: [],
railEntries: [],
},
moduleImportSequence: [
@@ -124,6 +124,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_workbench": "",
"alignment": "",
"all_borders": "",
"all_events": "",
@@ -49,6 +49,7 @@ export default /** @type {const} */ ([
'shuffle',
'smart_toy',
'space_dashboard',
'star',
'strikethrough_s',
'table_chart',
'table_chart',
@@ -52,7 +52,9 @@ export default function RailPanel({
>
<Tab.Content className="ide-rail-tab-content">
{railTabs
.filter(({ hide }) => !hide)
.filter(({ hide }) => {
return typeof hide === 'function' ? !hide() : !hide
})
.map(({ key, component, mountOnFirstLoad }) => (
<Tab.Pane
eventKey={key}
@@ -26,6 +26,14 @@ import RailResizeHandle from './rail-resize-handle'
import RailModals from './rail-modals'
import RailOverflowDropdown from './rail-overflow-dropdown'
import useRailOverflow from '../../hooks/use-rail-overflow'
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
const moduleRailEntries = (
importOverleafModules('railEntries') as {
import: { default: RailElement }
path: string
}[]
).map(({ import: { default: element } }) => element)
export const RailLayout = () => {
const { sendEvent } = useEditorAnalytics()
@@ -79,6 +87,7 @@ export const RailLayout = () => {
title: t('chat'),
hide: !getMeta('ol-capabilities')?.includes('chat'),
},
...moduleRailEntries,
],
[t, features.trackChangesVisible, view]
)
@@ -125,7 +134,13 @@ export const RailLayout = () => {
} else {
// HACK: Apparently the onSelect event is triggered with href attributes
// from DropdownItems
if (!railTabs.some(tab => !tab.hide && tab.key === key)) {
if (
!railTabs.some(tab =>
typeof tab.hide === 'function'
? !tab.hide()
: !tab.hide && tab.key === key
)
) {
// Attempting to open a non-existent tab
return
}
@@ -150,7 +165,9 @@ export const RailLayout = () => {
)
useEffect(() => {
const validTabKeys = railTabs.filter(tab => !tab.hide).map(tab => tab.key)
const validTabKeys = railTabs
.filter(tab => (typeof tab.hide === 'function' ? !tab.hide() : !tab.hide))
.map(tab => tab.key)
if (!validTabKeys.includes(selectedTab) && isOpen) {
// If the selected tab is no longer valid (e.g. due to permissions changes),
// switch back to the file tree
@@ -199,7 +216,9 @@ export const RailLayout = () => {
<Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
<div className="ide-rail-tabs-wrapper" ref={tabWrapperRef}>
{tabsInRail
.filter(({ hide }) => !hide)
.filter(({ hide }) =>
typeof hide === 'function' ? !hide() : !hide
)
.map(({ icon, key, indicator, title, disabled }) => (
<RailTab
open={isOpen && selectedTab === key}
@@ -23,6 +23,7 @@ export type RailTabKey =
| 'review-panel'
| 'chat'
| 'full-project-search'
| 'workbench'
export type RailModalKey = 'keyboard-shortcuts' | 'contact-us' | 'dictionary'
@@ -8,7 +8,7 @@ export type RailElement = {
component: ReactElement | null
indicator?: ReactElement
title: string
hide?: boolean
hide?: boolean | (() => boolean)
disabled?: boolean
mountOnFirstLoad?: boolean
}
+1
View File
@@ -148,6 +148,7 @@
"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_workbench": "AI workbench",
"alignment": "Alignment",
"all": "All",
"all_borders": "All borders",
+4
View File
@@ -81,6 +81,8 @@
"safari > 14"
],
"dependencies": {
"@ai-sdk/mcp": "^1.0.0-beta.13",
"@ai-sdk/openai": "^3.0.0-beta.44",
"@aws-sdk/client-ses": "^3.864.0",
"@contentful/rich-text-html-renderer": "^16.0.2",
"@contentful/rich-text-types": "^16.0.2",
@@ -107,6 +109,7 @@
"@stripe/stripe-js": "^7.7.0",
"@xmldom/xmldom": "^0.7.13",
"accepts": "^1.3.7",
"ai": "^6.0.0-beta.84",
"ajv": "^8.12.0",
"archiver": "^5.3.0",
"async": "^3.2.5",
@@ -217,6 +220,7 @@
"@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.2",
"@lezer/markdown": "^1.4.3",
"@overleaf/ai": "^1.0.0",
"@overleaf/codemirror-tree-view": "^0.1.3",
"@overleaf/dictionaries": "https://github.com/overleaf/dictionaries/archive/refs/tags/v0.0.3.tar.gz",
"@overleaf/eslint-plugin": "*",
+1
View File
@@ -16,6 +16,7 @@
"emitDecoratorMetadata": true /* Allow decorators in writefull - inversify */,
"paths": {
"@/*": ["./frontend/js/*"],
"@modules/*": ["./modules/*"],
"@overleaf/o-error": ["../../libraries/o-error"],
"@overleaf/ranges-tracker": ["../../libraries/ranges-tracker"],
/* can't make this entry @types because that conflicts with the "types" entry below */
+22
View File
@@ -10,6 +10,7 @@ const {
const PackageVersions = require('./app/src/infrastructure/PackageVersions.js')
const invalidateBabelCacheIfNeeded = require('./frontend/macros/invalidate-babel-cache-if-needed')
const { dirname } = require('node:path')
// Make sure that babel-macros are re-evaluated after changing the modules config
invalidateBabelCacheIfNeeded()
@@ -260,6 +261,26 @@ module.exports = {
},
],
},
{
// CSS from AI module
include: dirname(require.resolve('@overleaf/ai')),
use: [
{
loader: 'css-loader',
options: {
exportType: 'css-style-sheet',
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
config: require.resolve('@overleaf/ai/postcss.config.js'),
},
},
},
],
},
{
// Standard CSS processing (extracted into separate file)
use: [MiniCssExtractPlugin.loader, 'css-loader'],
@@ -309,6 +330,7 @@ module.exports = {
alias: {
// custom prefixes for import paths
'@': path.resolve(__dirname, './frontend/js/'),
'@modules': path.resolve(__dirname, './modules/'),
'@ol-types': path.resolve(__dirname, './types/'),
'@wf': path.resolve(
__dirname,