Răsfoiți Sursa

Move AI provider usage to the backend (#18562)

GitOrigin-RevId: 4f66c6576571c4fbb7381d8d0e34f2e468d6f34f
Alf Eaton 2 ani în urmă
părinte
comite
b9a8a7f7ec

+ 15 - 117
package-lock.json

@@ -14708,17 +14708,6 @@
         "node": ">= 6.0.0"
       }
     },
-    "node_modules/agentkeepalive": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
-      "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
-      "dependencies": {
-        "humanize-ms": "^1.2.1"
-      },
-      "engines": {
-        "node": ">= 8.0.0"
-      }
-    },
     "node_modules/aggregate-error": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
@@ -21858,6 +21847,14 @@
       "resolved": "https://registry.npmjs.org/events-listener/-/events-listener-1.1.0.tgz",
       "integrity": "sha512-Kd3EgYfODHueq6GzVfs/VUolh2EgJsS8hkO3KpnDrxVjU3eq63eXM2ujXkhPP+OkeUOhL8CxdfZbQXzryb5C4g=="
     },
+    "node_modules/eventsource-parser": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
+      "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==",
+      "engines": {
+        "node": ">=14.18"
+      }
+    },
     "node_modules/execa": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -25275,14 +25272,6 @@
         "node": ">=8.12.0"
       }
     },
-    "node_modules/humanize-ms": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
-      "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
-      "dependencies": {
-        "ms": "^2.0.0"
-      }
-    },
     "node_modules/hyphenate-style-name": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
@@ -28840,23 +28829,6 @@
         "micromark-util-types": "^2.0.0"
       }
     },
-    "node_modules/micromark-extension-gfm-table": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz",
-      "integrity": "sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw==",
-      "dev": true,
-      "dependencies": {
-        "devlop": "^1.0.0",
-        "micromark-factory-space": "^2.0.0",
-        "micromark-util-character": "^2.0.0",
-        "micromark-util-symbol": "^2.0.0",
-        "micromark-util-types": "^2.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/unified"
-      }
-    },
     "node_modules/micromark-factory-destination": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz",
@@ -30897,32 +30869,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/openai": {
-      "version": "4.36.0",
-      "resolved": "https://registry.npmjs.org/openai/-/openai-4.36.0.tgz",
-      "integrity": "sha512-AtYrhhWY64LhB9P6f3H0nV8nTSaQJ89mWPnfNU5CnYg81zlYaV8nkyO+aTNfprdqP/9xv10woNNUgefXINT4Dg==",
-      "dependencies": {
-        "@types/node": "^18.11.18",
-        "@types/node-fetch": "^2.6.4",
-        "abort-controller": "^3.0.0",
-        "agentkeepalive": "^4.2.1",
-        "form-data-encoder": "1.7.2",
-        "formdata-node": "^4.3.2",
-        "node-fetch": "^2.6.7",
-        "web-streams-polyfill": "^3.2.1"
-      },
-      "bin": {
-        "openai": "bin/cli"
-      }
-    },
-    "node_modules/openai/node_modules/web-streams-polyfill": {
-      "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
-      "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
-      "engines": {
-        "node": ">= 8"
-      }
-    },
     "node_modules/openapi3-ts": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-3.1.2.tgz",
@@ -44630,6 +44576,7 @@
         "east": "^2.0.2",
         "ejs": "^3.1.10",
         "email-addresses": "^5.0.0",
+        "eventsource-parser": "^1.1.2",
         "express": "^4.19.2",
         "express-bearer-token": "^2.4.0",
         "express-http-proxy": "^1.6.0",
@@ -44661,7 +44608,6 @@
         "nodemailer": "^6.7.0",
         "nodemailer-ses-transport": "^1.5.1",
         "on-headers": "^1.0.2",
-        "openai": "^4.36.0",
         "otplib": "^12.0.1",
         "p-limit": "^2.3.0",
         "p-props": "4.0.0",
@@ -44833,7 +44779,6 @@
         "mathjax": "^3.2.2",
         "mensch": "^0.3.4",
         "micromark": "^4.0.0",
-        "micromark-extension-gfm-table": "^2.0.0",
         "mini-css-extract-plugin": "^2.7.6",
         "mocha": "^10.2.0",
         "mocha-each": "^2.0.1",
@@ -53295,6 +53240,7 @@
         "eslint-plugin-react": "^7.32.2",
         "eslint-plugin-react-hooks": "^4.6.0",
         "events": "^3.3.0",
+        "eventsource-parser": "^1.1.2",
         "expose-loader": "^4.1.0",
         "express": "^4.19.2",
         "express-bearer-token": "^2.4.0",
@@ -53331,7 +53277,6 @@
         "mensch": "^0.3.4",
         "method-override": "^2.3.3",
         "micromark": "^4.0.0",
-        "micromark-extension-gfm-table": "^2.0.0",
         "mini-css-extract-plugin": "^2.7.6",
         "minimatch": "^7.4.2",
         "minimist": "^1.2.7",
@@ -53349,7 +53294,6 @@
         "nodemailer-ses-transport": "^1.5.1",
         "nvd3": "^1.8.6",
         "on-headers": "^1.0.2",
-        "openai": "^4.36.0",
         "otplib": "^12.0.1",
         "p-limit": "^2.3.0",
         "p-props": "4.0.0",
@@ -59198,14 +59142,6 @@
         "debug": "4"
       }
     },
-    "agentkeepalive": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
-      "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
-      "requires": {
-        "humanize-ms": "^1.2.1"
-      }
-    },
     "aggregate-error": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
@@ -64617,6 +64553,11 @@
       "resolved": "https://registry.npmjs.org/events-listener/-/events-listener-1.1.0.tgz",
       "integrity": "sha512-Kd3EgYfODHueq6GzVfs/VUolh2EgJsS8hkO3KpnDrxVjU3eq63eXM2ujXkhPP+OkeUOhL8CxdfZbQXzryb5C4g=="
     },
+    "eventsource-parser": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
+      "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA=="
+    },
     "execa": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -67268,14 +67209,6 @@
       "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
       "dev": true
     },
-    "humanize-ms": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
-      "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
-      "requires": {
-        "ms": "^2.0.0"
-      }
-    },
     "hyphenate-style-name": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
@@ -70942,19 +70875,6 @@
         "micromark-util-types": "^2.0.0"
       }
     },
-    "micromark-extension-gfm-table": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz",
-      "integrity": "sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw==",
-      "dev": true,
-      "requires": {
-        "devlop": "^1.0.0",
-        "micromark-factory-space": "^2.0.0",
-        "micromark-util-character": "^2.0.0",
-        "micromark-util-symbol": "^2.0.0",
-        "micromark-util-types": "^2.0.0"
-      }
-    },
     "micromark-factory-destination": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz",
@@ -72380,28 +72300,6 @@
         "is-wsl": "^2.2.0"
       }
     },
-    "openai": {
-      "version": "4.36.0",
-      "resolved": "https://registry.npmjs.org/openai/-/openai-4.36.0.tgz",
-      "integrity": "sha512-AtYrhhWY64LhB9P6f3H0nV8nTSaQJ89mWPnfNU5CnYg81zlYaV8nkyO+aTNfprdqP/9xv10woNNUgefXINT4Dg==",
-      "requires": {
-        "@types/node": "^18.11.18",
-        "@types/node-fetch": "^2.6.4",
-        "abort-controller": "^3.0.0",
-        "agentkeepalive": "^4.2.1",
-        "form-data-encoder": "1.7.2",
-        "formdata-node": "^4.3.2",
-        "node-fetch": "^2.6.7",
-        "web-streams-polyfill": "^3.2.1"
-      },
-      "dependencies": {
-        "web-streams-polyfill": {
-          "version": "3.3.3",
-          "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
-          "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="
-        }
-      }
-    },
     "openapi3-ts": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-3.1.2.tgz",

+ 1 - 0
services/web/app/src/Features/Project/ProjectController.js

@@ -670,6 +670,7 @@ const _ProjectController = {
         debugPdfDetach,
         showSymbolPalette,
         symbolPaletteAvailable: Features.hasFeature('symbol-palette'),
+        showAiErrorAssistant: user.alphaProgram, // TODO: labs experiment
         detachRole,
         metadata: { viewport: false },
         showUpgradePrompt,

+ 1 - 0
services/web/app/src/Features/Tutorial/TutorialController.js

@@ -8,6 +8,7 @@ const VALID_KEYS = [
   'writefull-integration',
   'writefull-oauth-promotion',
   'bib-file-tpr-prompt',
+  'ai-error-assistant-consent',
 ]
 
 async function completeTutorial(req, res, next) {

+ 1 - 0
services/web/app/views/project/editor/meta.pug

@@ -21,6 +21,7 @@ meta(name="ol-pdfjsVariant" content=pdfjsVariant)
 meta(name="ol-debugPdfDetach" data-type="boolean" content=debugPdfDetach)
 meta(name="ol-showSymbolPalette" data-type="boolean" content=showSymbolPalette)
 meta(name="ol-symbolPaletteAvailable" data-type="boolean" content=symbolPaletteAvailable)
+meta(name="ol-showAiErrorAssistant" data-type="boolean" content=showAiErrorAssistant)
 meta(name="ol-detachRole" data-type="string" content=detachRole)
 meta(name="ol-allowedImageNames" data-type="json" content=allowedImageNames)
 meta(name="ol-languages" data-type="json" content=languages)

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

@@ -876,6 +876,7 @@ module.exports = {
     sourceEditorExtensions: [],
     sourceEditorComponents: [],
     pdfLogEntryComponents: [],
+    diagnosticActions: [],
     sourceEditorCompletionSources: [],
     sourceEditorSymbolPalette: [],
     sourceEditorToolbarComponents: [],

+ 24 - 1
services/web/frontend/js/features/pdf-preview/components/pdf-log-entry.jsx

@@ -5,6 +5,7 @@ import PreviewLogEntryHeader from '../../preview/components/preview-log-entry-he
 import PdfLogEntryContent from './pdf-log-entry-content'
 import HumanReadableLogsHints from '../../../ide/human-readable-logs/HumanReadableLogsHints'
 import { sendMB } from '@/infrastructure/event-tracking'
+import getMeta from '@/utils/meta'
 
 function PdfLogEntry({
   ruleId,
@@ -25,7 +26,10 @@ function PdfLogEntry({
   onClose,
   index,
   logEntry,
+  id,
 }) {
+  const showAiErrorAssistant = getMeta('ol-showAiErrorAssistant')
+
   if (ruleId && HumanReadableLogsHints[ruleId]) {
     const hint = HumanReadableLogsHints[ruleId]
     formattedContent = hint.formattedContent(contentDetails)
@@ -44,11 +48,29 @@ function PdfLogEntry({
     [level, onSourceLocationClick, ruleId, sourceLocation]
   )
 
+  const logEntryRef = useCallback(
+    element => {
+      if (element) {
+        window.addEventListener('editor:view-compile-log-entry', event => {
+          if (event.detail.id === id) {
+            element.scrollIntoView({ block: 'start', inline: 'nearest' })
+          }
+
+          if (event.detail.suggestFix) {
+            element.querySelector('button[data-action="suggest-fix"]')?.click()
+          }
+        })
+      }
+    },
+    [id]
+  )
+
   return (
     <div
       className={classNames('log-entry', customClass)}
       aria-label={entryAriaLabel}
       data-ruleid={ruleId}
+      ref={logEntryRef}
     >
       <PreviewLogEntryHeader
         level={level}
@@ -62,7 +84,7 @@ function PdfLogEntry({
         onClose={onClose}
       />
 
-      {(rawContent || formattedContent || window.user.alphaProgram) && (
+      {(rawContent || formattedContent || showAiErrorAssistant) && (
         <PdfLogEntryContent
           rawContent={rawContent}
           formattedContent={formattedContent}
@@ -101,6 +123,7 @@ PdfLogEntry.propTypes = {
   onClose: PropTypes.func,
   index: PropTypes.number,
   logEntry: PropTypes.any,
+  id: PropTypes.string,
 }
 
 export default memo(PdfLogEntry)

+ 1 - 0
services/web/frontend/js/features/pdf-preview/components/pdf-logs-entries.jsx

@@ -26,6 +26,7 @@ function PdfLogsEntries({ entries, hasErrors }) {
         <PdfLogEntry
           key={logEntry.key}
           index={index}
+          id={logEntry.key}
           logEntry={logEntry}
           ruleId={logEntry.ruleId}
           headerTitle={logEntry.messageComponent ?? logEntry.message}

+ 7 - 1
services/web/frontend/js/features/pdf-preview/util/output-files.js

@@ -41,7 +41,7 @@ export function handleOutputFiles(outputFiles, projectId, data) {
 let nextEntryId = 1
 
 function generateEntryKey() {
-  return '' + nextEntryId++
+  return 'compile-log-entry-' + nextEntryId++
 }
 
 export const handleLogFiles = async (outputFiles, data, signal) => {
@@ -140,6 +140,7 @@ export function buildLogEntryAnnotations(entries, fileTreeData, rootDocId) {
   const rootDocDirname = dirname(fileTreeData, rootDocId)
 
   const logEntryAnnotations = {}
+  const seenLine = {}
 
   for (const entry of entries) {
     if (entry.file) {
@@ -153,12 +154,17 @@ export function buildLogEntryAnnotations(entries, fileTreeData, rootDocId) {
         }
 
         logEntryAnnotations[entity._id].push({
+          id: entry.key,
+          entryIndex: logEntryAnnotations[entity._id].length, // used for maintaining the order of items on the same line
           row: entry.line - 1,
           type: entry.level === 'error' ? 'error' : 'warning',
           text: entry.message,
           source: 'compile', // NOTE: this is used in Ace for filtering the annotations
           ruleId: entry.ruleId,
+          firstOnLine: !seenLine[entry.line],
         })
+
+        seenLine[entry.line] = true
       }
     }
   }

+ 52 - 14
services/web/frontend/js/features/source-editor/extensions/annotations.ts

@@ -12,6 +12,34 @@ import {
 import { Annotation } from '../../../../../types/annotation'
 import { debugConsole } from '@/utils/debugging'
 import { sendMB } from '@/infrastructure/event-tracking'
+import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
+
+interface CompileLogDiagnostic extends Diagnostic {
+  compile?: true
+  ruleId?: string
+  id?: string
+  entryIndex: number
+  firstOnLine?: boolean
+}
+
+type RenderedDiagnostic = Pick<
+  CompileLogDiagnostic,
+  | 'message'
+  | 'severity'
+  | 'ruleId'
+  | 'compile'
+  | 'source'
+  | 'id'
+  | 'firstOnLine'
+>
+
+export type DiagnosticAction = (
+  diagnostic: RenderedDiagnostic
+) => HTMLButtonElement | null
+
+const diagnosticActions = importOverleafModules('diagnosticActions') as {
+  import: { default: DiagnosticAction }
+}[]
 
 const compileLintSourceConf = new Compartment()
 
@@ -56,7 +84,8 @@ export const lintSourceConfig = {
  */
 const compileLogLintSource = (): Extension =>
   linter(view => {
-    const items: Diagnostic[] = []
+    const items: CompileLogDiagnostic[] = []
+    // NOTE: iter() changes the order of diagnostics on the same line
     const cursor = view.state.field(compileDiagnosticsState).iter()
     while (cursor.value !== null) {
       const { diagnostic } = cursor.value
@@ -68,14 +97,11 @@ const compileLogLintSource = (): Extension =>
       })
       cursor.next()
     }
+    // restore the original order of items
+    items.sort((a, b) => a.from - b.from || a.entryIndex - b.entryIndex)
     return items
   }, lintSourceConfig)
 
-interface CompileLogDiagnostic extends Diagnostic {
-  compile?: true
-  ruleId?: string
-}
-
 class CompileLogDiagnosticRangeValue extends RangeValue {
   constructor(public diagnostic: CompileLogDiagnostic) {
     super()
@@ -117,7 +143,7 @@ export const compileDiagnosticsState = StateField.define<
 })
 
 export const setAnnotations = (doc: Text, annotations: Annotation[]) => {
-  const diagnostics: Diagnostic[] = []
+  const diagnostics: CompileLogDiagnostic[] = []
 
   for (const annotation of annotations) {
     // ignore "whole document" (row: -1) annotations
@@ -162,19 +188,31 @@ const convertAnnotationToDiagnostic = (
     message: annotation.text,
     ruleId: annotation.ruleId,
     compile: true,
+    id: annotation.id,
+    entryIndex: annotation.entryIndex,
+    source: annotation.source,
+    firstOnLine: annotation.firstOnLine,
   }
 }
 
-export const renderMessage = (
-  diagnostic: Pick<
-    CompileLogDiagnostic,
-    'message' | 'severity' | 'ruleId' | 'compile'
-  >
-) => {
+export const renderMessage = (diagnostic: RenderedDiagnostic) => {
   const { message, severity, ruleId, compile = false } = diagnostic
 
   const div = document.createElement('div')
-  div.textContent = message
+  div.classList.add('ol-cm-diagnostic-message')
+
+  div.append(message)
+
+  const activeDiagnosticActions = diagnosticActions
+    .map(m => m.import.default(diagnostic))
+    .filter(Boolean) as HTMLButtonElement[]
+
+  if (activeDiagnosticActions.length) {
+    const actions = document.createElement('div')
+    actions.classList.add('ol-cm-diagnostic-actions')
+    actions.append(...activeDiagnosticActions)
+    div.append(actions)
+  }
 
   window.setTimeout(() => {
     if (div.isConnected) {

+ 9 - 0
services/web/frontend/js/features/source-editor/extensions/theme.ts

@@ -155,6 +155,15 @@ const baseTheme = EditorView.baseTheme({
     boxShadow: '0 1px 1px rgba(255, 255, 255, 0.7)',
     backgroundColor: 'rgba(255, 255, 255, 0.2)',
   },
+  '.cm-diagnosticSource': {
+    display: 'none',
+  },
+  '.ol-cm-diagnostic-actions': {
+    marginTop: '4px',
+  },
+  '.cm-diagnostic:last-of-type .ol-cm-diagnostic-actions': {
+    marginBottom: '4px',
+  },
 })
 
 /**

+ 7 - 5
services/web/frontend/js/ide/log-parser/latex-log-parser.js

@@ -211,7 +211,7 @@ export default class LatexParser {
   // Check if we're entering or leaving a new file in this line
 
   parseParensForFilenames() {
-    const pos = this.currentLine.search(/\(|\)/)
+    const pos = this.currentLine.search(/[()]/)
     if (pos !== -1) {
       const token = this.currentLine[pos]
       this.currentLine = this.currentLine.slice(pos + 1)
@@ -251,12 +251,14 @@ export default class LatexParser {
 
   consumeFilePath() {
     // Our heuristic for detecting file names are rather crude
-    // A file may not contain a ')' in it
-    // To be a file path it must have at least one /
-    if (!this.currentLine.match(/^\/?([^ )]+\/)+/)) {
+
+    // To contain a file path this line must have at least one / before any '(', ')' or '\'
+    if (!this.currentLine.match(/^\/?([^ ()\\]+\/)+/)) {
       return false
     }
-    let endOfFilePath = this.currentLine.search(/ |\)/)
+
+    // A file may not contain a '(', ')' or '\'
+    let endOfFilePath = this.currentLine.search(/[ ()\\]/)
 
     // handle the case where there is a space in a filename
     while (endOfFilePath !== -1 && this.currentLine[endOfFilePath] === ' ') {

+ 12 - 0
services/web/frontend/js/shared/context/local-compile-context.tsx

@@ -596,6 +596,18 @@ export const LocalCompileProvider: FC = ({ children }) => {
   // used for that compile.
   const lastCompileOptions = useMemo(() => data?.options || {}, [data])
 
+  useEffect(() => {
+    const listener = (event: Event) => {
+      setShowLogs((event as CustomEvent<boolean>).detail as boolean)
+    }
+
+    window.addEventListener('editor:show-logs', listener)
+
+    return () => {
+      window.removeEventListener('editor:show-logs', listener)
+    }
+  }, [])
+
   const value = useMemo(
     () => ({
       animateCompileDropdownArrow,

+ 4 - 0
services/web/frontend/js/shared/context/project-context.tsx

@@ -10,6 +10,7 @@ const ProjectContext = createContext<
       _id: string
       name: string
       rootDocId?: string
+      compiler: string
       members: { _id: UserId; email: string; privileges: string }[]
       invites: { _id: UserId }[]
       features: {
@@ -66,6 +67,7 @@ export const ProjectProvider: FC = ({ children }) => {
 
   const {
     _id,
+    compiler,
     name,
     rootDoc_id: rootDocId,
     members,
@@ -87,6 +89,7 @@ export const ProjectProvider: FC = ({ children }) => {
   const value = useMemo(() => {
     return {
       _id,
+      compiler,
       name,
       rootDocId,
       members,
@@ -99,6 +102,7 @@ export const ProjectProvider: FC = ({ children }) => {
     }
   }, [
     _id,
+    compiler,
     name,
     rootDocId,
     members,

+ 4 - 0
services/web/frontend/stylesheets/bootstrap-5/components/button.scss

@@ -85,6 +85,10 @@
     );
 
     background: var(--premium-gradient);
+    transition: none;
+    &:hover {
+      background: var(--blue-70);
+    }
   }
 }
 

+ 3 - 0
services/web/frontend/stylesheets/components/buttons.less

@@ -127,6 +127,9 @@
 .btn-premium {
   .premium-background;
   color: @white;
+  &:hover {
+    background: @blue-70;
+  }
 }
 
 .reset-btns {

+ 1 - 2
services/web/package.json

@@ -109,6 +109,7 @@
     "east": "^2.0.2",
     "ejs": "^3.1.10",
     "email-addresses": "^5.0.0",
+    "eventsource-parser": "^1.1.2",
     "express": "^4.19.2",
     "express-bearer-token": "^2.4.0",
     "express-http-proxy": "^1.6.0",
@@ -140,7 +141,6 @@
     "nodemailer": "^6.7.0",
     "nodemailer-ses-transport": "^1.5.1",
     "on-headers": "^1.0.2",
-    "openai": "^4.36.0",
     "otplib": "^12.0.1",
     "p-limit": "^2.3.0",
     "p-props": "4.0.0",
@@ -312,7 +312,6 @@
     "mathjax": "^3.2.2",
     "mensch": "^0.3.4",
     "micromark": "^4.0.0",
-    "micromark-extension-gfm-table": "^2.0.0",
     "mini-css-extract-plugin": "^2.7.6",
     "mocha": "^10.2.0",
     "mocha-each": "^2.0.1",

+ 1 - 1
services/web/test/frontend/bootstrap.js

@@ -1,6 +1,6 @@
 // Run babel on tests to allow support for import/export statements in Node
 require('@babel/register')({
-  extensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs'],
+  extensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.svg'],
   plugins: [['module-resolver', { alias: { '^@/(.+)': './frontend/js/\\1' } }]],
 })
 

+ 3 - 0
services/web/types/annotation.ts

@@ -4,4 +4,7 @@ export type Annotation = {
   text: string
   source?: string
   ruleId?: string
+  id: string
+  entryIndex: number
+  firstOnLine: boolean
 }