[web] add clsi-cache prompts (#29281)
* [web] add clsi-cache prompts * [web] add new editor variant to segmentation * [web] add tests for useNewEditorVariant * [web] adjust start of using clsi-cache in split-test GitOrigin-RevId: c9c5b1eff2ceefb65ef82516d9074cb971cb4c48
This commit is contained in:
@@ -74,8 +74,9 @@ async function _getSplitTestOptions(req, res) {
|
||||
'populate-clsi-cache'
|
||||
)
|
||||
let populateClsiCache = populateClsiCacheVariant === 'enabled'
|
||||
const compileFromClsiCache = populateClsiCache // use same split-test
|
||||
let compileFromClsiCache = populateClsiCache // use same split-test
|
||||
|
||||
let clsiCachePromptVariant = 'default'
|
||||
if (!populateClsiCache) {
|
||||
// Pre-populate the cache for the users in the split-test for prompts.
|
||||
// Keep the compile from cache disabled for now.
|
||||
@@ -84,7 +85,17 @@ async function _getSplitTestOptions(req, res) {
|
||||
res,
|
||||
'populate-clsi-cache-for-prompt'
|
||||
)
|
||||
;({ variant: clsiCachePromptVariant } =
|
||||
await SplitTestHandler.promises.getAssignment(
|
||||
editorReq,
|
||||
res,
|
||||
'clsi-cache-prompt'
|
||||
))
|
||||
populateClsiCache = variant === 'enabled'
|
||||
if (res.locals.splitTestInfo?.['clsi-cache-prompt']?.active) {
|
||||
// Start using the cache when the split-test for the prompts is activated.
|
||||
compileFromClsiCache = populateClsiCache
|
||||
}
|
||||
}
|
||||
|
||||
const pdfDownloadDomain = Settings.pdfDownloadDomain
|
||||
@@ -94,6 +105,7 @@ async function _getSplitTestOptions(req, res) {
|
||||
return {
|
||||
compileFromClsiCache,
|
||||
populateClsiCache,
|
||||
clsiCachePromptVariant,
|
||||
pdfDownloadDomain,
|
||||
enablePdfCaching: false,
|
||||
}
|
||||
@@ -113,6 +125,7 @@ async function _getSplitTestOptions(req, res) {
|
||||
return {
|
||||
compileFromClsiCache,
|
||||
populateClsiCache,
|
||||
clsiCachePromptVariant,
|
||||
pdfDownloadDomain,
|
||||
enablePdfCaching: false,
|
||||
}
|
||||
@@ -121,6 +134,7 @@ async function _getSplitTestOptions(req, res) {
|
||||
return {
|
||||
compileFromClsiCache,
|
||||
populateClsiCache,
|
||||
clsiCachePromptVariant,
|
||||
pdfDownloadDomain,
|
||||
enablePdfCaching,
|
||||
pdfCachingMinChunkSize,
|
||||
@@ -212,6 +226,7 @@ const _CompileController = {
|
||||
let {
|
||||
compileFromClsiCache,
|
||||
populateClsiCache,
|
||||
clsiCachePromptVariant,
|
||||
enablePdfCaching,
|
||||
pdfCachingMinChunkSize,
|
||||
pdfDownloadDomain,
|
||||
@@ -294,6 +309,7 @@ const _CompileController = {
|
||||
compileGroup: limits?.compileGroup,
|
||||
clsiServerId,
|
||||
clsiCacheShard,
|
||||
clsiCachePromptVariant,
|
||||
validationProblems,
|
||||
stats,
|
||||
timings,
|
||||
|
||||
@@ -386,6 +386,7 @@ const _ProjectController = {
|
||||
'external-socket-heartbeat',
|
||||
'null-test-share-modal',
|
||||
'populate-clsi-cache',
|
||||
'populate-clsi-cache-for-prompt',
|
||||
'pdf-caching-cached-url-lookup',
|
||||
'pdf-caching-mode',
|
||||
'pdf-caching-prefetch-large',
|
||||
@@ -818,14 +819,26 @@ const _ProjectController = {
|
||||
|
||||
const planDetails = Settings.plans.find(p => p.planCode === planCode)
|
||||
|
||||
const projectOwnerHasPremiumOnPageLoad =
|
||||
ownerFeatures?.compileGroup === 'priority'
|
||||
if (
|
||||
projectOwnerHasPremiumOnPageLoad &&
|
||||
splitTestAssignments['populate-clsi-cache']?.variant !== 'enabled'
|
||||
) {
|
||||
await SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
'clsi-cache-prompt'
|
||||
)
|
||||
}
|
||||
|
||||
res.render(template, {
|
||||
title: project.name,
|
||||
priority_title: true,
|
||||
bodyClasses: ['editor'],
|
||||
project_id: project._id,
|
||||
projectName: project.name,
|
||||
projectOwnerHasPremiumOnPageLoad:
|
||||
ownerFeatures?.compileGroup === 'priority',
|
||||
projectOwnerHasPremiumOnPageLoad,
|
||||
user: {
|
||||
id: userId,
|
||||
email: user.email,
|
||||
|
||||
@@ -278,6 +278,19 @@
|
||||
"clicking_delete_will_remove_sso_config_and_clear_saml_data": "",
|
||||
"clone_with_git": "",
|
||||
"close": "",
|
||||
"clsi_cache_prompt_compile_faster": "",
|
||||
"clsi_cache_prompt_compile_question": "",
|
||||
"clsi_cache_prompt_compile_same": "",
|
||||
"clsi_cache_prompt_compile_slower": "",
|
||||
"clsi_cache_prompt_preview_less": "",
|
||||
"clsi_cache_prompt_preview_more": "",
|
||||
"clsi_cache_prompt_preview_question": "",
|
||||
"clsi_cache_prompt_preview_same": "",
|
||||
"clsi_cache_prompt_synctex_less": "",
|
||||
"clsi_cache_prompt_synctex_more": "",
|
||||
"clsi_cache_prompt_synctex_question": "",
|
||||
"clsi_cache_prompt_synctex_same": "",
|
||||
"clsi_cache_prompt_thanks": "",
|
||||
"clsi_maintenance": "",
|
||||
"clsi_unavailable": "",
|
||||
"code_check_failed": "",
|
||||
|
||||
@@ -18,6 +18,7 @@ import PdfClearCacheButton from '@/features/pdf-preview/components/pdf-clear-cac
|
||||
import PdfDownloadFilesButton from '@/features/pdf-preview/components/pdf-download-files-button'
|
||||
import { useIsNewErrorLogsPositionEnabled } from '../../utils/new-editor-utils'
|
||||
import RollingBuildSelectedReminder from './rolling-build-selected-reminder'
|
||||
import ClsiCachePrompt from '@/features/pdf-preview/components/clsi-cache-prompt'
|
||||
|
||||
const logsComponents: Array<{
|
||||
import: { default: ElementType }
|
||||
@@ -86,6 +87,7 @@ function ErrorLogs({
|
||||
))}
|
||||
<TabContent className="error-logs new-error-logs">
|
||||
<div className="logs-pane-content">
|
||||
<ClsiCachePrompt />
|
||||
<RollingBuildSelectedReminder />
|
||||
{stoppedOnFirstError && includeErrors && <StopOnFirstErrorPrompt />}
|
||||
|
||||
|
||||
+2
@@ -2,6 +2,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||
import ErrorState from './error-state'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
|
||||
import ClsiCachePrompt from '@/features/pdf-preview/components/clsi-cache-prompt'
|
||||
|
||||
export default function RenderingErrorExpectedState() {
|
||||
const { t } = useTranslation()
|
||||
@@ -30,6 +31,7 @@ export default function RenderingErrorExpectedState() {
|
||||
{t('recompile')}
|
||||
</OLButton>,
|
||||
]}
|
||||
extraContent={<ClsiCachePrompt />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,3 +86,13 @@ export const useAreNewErrorLogsEnabled = () => {
|
||||
const newEditorEnabled = useIsNewEditorEnabled()
|
||||
return newEditorEnabled && canUseNewLogs()
|
||||
}
|
||||
|
||||
export function useNewEditorVariant() {
|
||||
const newEditor = useIsNewEditorEnabled()
|
||||
const newErrorLogs = useAreNewErrorLogsEnabled()
|
||||
const newErrorLogsPosition = useIsNewErrorLogsPositionEnabled()
|
||||
if (!newEditor) return 'default'
|
||||
if (!newErrorLogs) return 'new-editor-old-logs'
|
||||
if (!newErrorLogsPosition) return 'new-editor-new-logs-old-position'
|
||||
return 'new-editor'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { memo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { sendMB } from '../../../infrastructure/event-tracking'
|
||||
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import '../../../../stylesheets/pages/editor/clsi-cache-prompt.scss'
|
||||
import { fallBackToClsiCache } from '@/features/pdf-preview/util/pdf-caching-flags'
|
||||
import { useNewEditorVariant } from '@/features/ide-redesign/utils/new-editor-utils'
|
||||
|
||||
const SAY_THANKS_TIMEOUT = 10 * 1000
|
||||
|
||||
function ClsiCachePromptContent() {
|
||||
const { clsiCachePromptVariant, clsiCachePromptSegmentation } =
|
||||
useCompileContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const newEditorVariant = useNewEditorVariant()
|
||||
|
||||
const [hasRatedProject, setHasRatedProject] = usePersistedState(
|
||||
`clsi-cache-prompt:${clsiCachePromptVariant}:${projectId}`,
|
||||
false,
|
||||
{ listen: true }
|
||||
)
|
||||
const [dismiss, setDismiss] = usePersistedState(
|
||||
`clsi-cache-prompt:dismiss`,
|
||||
false,
|
||||
{ listen: true }
|
||||
)
|
||||
const [sayThanks, setSayThanks] = useState(false)
|
||||
|
||||
function sendEvent(feedback: string) {
|
||||
sendMB('clsi-cache-prompt', {
|
||||
projectId,
|
||||
clsiCacheEnabled: fallBackToClsiCache,
|
||||
clsiCachePromptVariant,
|
||||
newEditorVariant,
|
||||
...clsiCachePromptSegmentation[clsiCachePromptVariant],
|
||||
feedback,
|
||||
})
|
||||
}
|
||||
|
||||
function submitFeedback(feedback: string) {
|
||||
sendEvent(feedback)
|
||||
setHasRatedProject(true)
|
||||
setSayThanks(true)
|
||||
window.setTimeout(() => {
|
||||
setSayThanks(false)
|
||||
}, SAY_THANKS_TIMEOUT)
|
||||
}
|
||||
|
||||
function dismissFeedback() {
|
||||
sendEvent('dismiss')
|
||||
setDismiss(true)
|
||||
}
|
||||
|
||||
const { t } = useTranslation()
|
||||
if (clsiCachePromptVariant === 'default') return null
|
||||
switch (true) {
|
||||
case sayThanks:
|
||||
return (
|
||||
<OLNotification
|
||||
type="info"
|
||||
className="clsi-cache-prompt"
|
||||
onDismiss={() => setSayThanks(false)}
|
||||
content={t('clsi_cache_prompt_thanks')}
|
||||
/>
|
||||
)
|
||||
case dismiss || hasRatedProject:
|
||||
return null
|
||||
case clsiCachePromptSegmentation?.[clsiCachePromptVariant] != null: {
|
||||
let question: string
|
||||
let answers: Record<string, string>
|
||||
switch (clsiCachePromptVariant) {
|
||||
case 'compile': {
|
||||
question = t('clsi_cache_prompt_compile_question')
|
||||
answers = {
|
||||
slower: t('clsi_cache_prompt_compile_slower'),
|
||||
same: t('clsi_cache_prompt_compile_same'),
|
||||
faster: t('clsi_cache_prompt_compile_faster'),
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'preview':
|
||||
case 'preview-error': {
|
||||
question = t('clsi_cache_prompt_preview_question')
|
||||
answers = {
|
||||
less: t('clsi_cache_prompt_preview_less'),
|
||||
same: t('clsi_cache_prompt_preview_same'),
|
||||
more: t('clsi_cache_prompt_preview_more'),
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'synctex': {
|
||||
question = t('clsi_cache_prompt_synctex_question')
|
||||
answers = {
|
||||
less: t('clsi_cache_prompt_synctex_less'),
|
||||
same: t('clsi_cache_prompt_synctex_same'),
|
||||
more: t('clsi_cache_prompt_synctex_more'),
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return (
|
||||
<OLNotification
|
||||
type="info"
|
||||
className="clsi-cache-prompt"
|
||||
customIcon={<></>}
|
||||
isDismissible
|
||||
onDismiss={dismissFeedback}
|
||||
content={
|
||||
<>
|
||||
{question}
|
||||
<br />
|
||||
{Object.entries(answers).map(([feedback, text]) => (
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => submitFeedback(feedback)}
|
||||
key={feedback}
|
||||
>
|
||||
{text}
|
||||
</OLButton>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function ClsiCachePrompt() {
|
||||
const { clsiCachePromptVariant, showLogs } = useCompileContext()
|
||||
|
||||
const onlyInLogsPane = clsiCachePromptVariant === 'preview-error'
|
||||
if (clsiCachePromptVariant === 'default' || showLogs !== onlyInLogsPane) {
|
||||
return null
|
||||
}
|
||||
return <ClsiCachePromptContent />
|
||||
}
|
||||
|
||||
export default memo(ClsiCachePrompt)
|
||||
@@ -25,8 +25,14 @@ type PdfJsViewerProps = {
|
||||
function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
const { setError, firstRenderDone, highlights, position, setPosition } =
|
||||
useCompileContext()
|
||||
const {
|
||||
setError,
|
||||
firstRenderDone,
|
||||
highlights,
|
||||
position,
|
||||
setPosition,
|
||||
setClsiCachePromptSegmentation,
|
||||
} = useCompileContext()
|
||||
|
||||
const { setLoadingError } = usePdfPreviewContext()
|
||||
|
||||
@@ -82,6 +88,10 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
|
||||
}, [])
|
||||
|
||||
const [startFetch, setStartFetch] = useState(0)
|
||||
const startFetchRef = useRef(startFetch)
|
||||
useEffect(() => {
|
||||
startFetchRef.current = startFetch
|
||||
}, [startFetch])
|
||||
|
||||
// listen for events and trigger rendering.
|
||||
// Do everything in one effect to mitigate de-sync between events.
|
||||
@@ -148,6 +158,27 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
|
||||
setPage(event.pageNumber)
|
||||
}
|
||||
|
||||
const handleRenderedClsiCachePrompt = () => {
|
||||
const delay = Math.ceil(
|
||||
(performance.now() - startFetchRef.current) / 1_000
|
||||
)
|
||||
if (delay > 30) {
|
||||
setClsiCachePromptSegmentation(prev => {
|
||||
// Always overwrite segmentation; emit the greatest delay.
|
||||
return {
|
||||
...prev,
|
||||
preview: {
|
||||
previewFailed: false,
|
||||
delay,
|
||||
usedClsiCache: pdfJsWrapper.usedClsiCache(),
|
||||
},
|
||||
}
|
||||
})
|
||||
pdfJsWrapper.eventBus.off('pagerendered', handleRenderedClsiCachePrompt)
|
||||
}
|
||||
}
|
||||
pdfJsWrapper.eventBus.on('pagerendered', handleRenderedClsiCachePrompt)
|
||||
|
||||
// `pagesinit` fires when the data for rendering the first page is ready.
|
||||
pdfJsWrapper.eventBus.on('pagesinit', handlePagesinit)
|
||||
// `pagerendered` fires when a page was actually rendered.
|
||||
@@ -164,15 +195,22 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
|
||||
pdfJsWrapper.eventBus.off('pagerendered', handleRenderedInitialPageNumber)
|
||||
pdfJsWrapper.eventBus.off('scalechanging', handleScaleChanged)
|
||||
pdfJsWrapper.eventBus.off('pagechanging', handlePageChanging)
|
||||
pdfJsWrapper.eventBus.off('pagerendered', handleRenderedClsiCachePrompt)
|
||||
}
|
||||
}, [pdfJsWrapper, firstRenderDone, startFetch])
|
||||
}, [
|
||||
pdfJsWrapper,
|
||||
firstRenderDone,
|
||||
startFetch,
|
||||
setClsiCachePromptSegmentation,
|
||||
])
|
||||
|
||||
// load the PDF document from the URL
|
||||
useEffect(() => {
|
||||
if (pdfJsWrapper && url) {
|
||||
setInitialised(false)
|
||||
setError(undefined)
|
||||
setStartFetch(performance.now())
|
||||
const t0 = performance.now()
|
||||
setStartFetch(t0)
|
||||
|
||||
const abortController = new AbortController()
|
||||
const handleFetchError = (err: any) => {
|
||||
@@ -180,6 +218,21 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
|
||||
// The error is already logged at the call-site with additional context.
|
||||
if (err instanceof PDFJS.ResponseException && err.missing) {
|
||||
setError('rendering-error-expected')
|
||||
setClsiCachePromptSegmentation(prev => {
|
||||
if (prev['preview-error'] !== null) {
|
||||
return prev // keep delay segmentation from first error.
|
||||
}
|
||||
const delay = Math.ceil((performance.now() - t0) / 1_000)
|
||||
return {
|
||||
...prev,
|
||||
preview: null,
|
||||
'preview-error': {
|
||||
previewFailed: true,
|
||||
delay,
|
||||
usedClsiCache: pdfJsWrapper.usedClsiCache(),
|
||||
},
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setError('rendering-error')
|
||||
}
|
||||
@@ -190,6 +243,9 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
|
||||
if (doc) {
|
||||
setTotalPages(doc.numPages)
|
||||
}
|
||||
setClsiCachePromptSegmentation(prev => {
|
||||
return { ...prev, preview: null, 'preview-error': null }
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
if (abortController.signal.aborted) return
|
||||
@@ -200,7 +256,14 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
|
||||
abortController.abort()
|
||||
}
|
||||
}
|
||||
}, [pdfJsWrapper, url, pdfFile, setError, setStartFetch])
|
||||
}, [
|
||||
pdfJsWrapper,
|
||||
url,
|
||||
pdfFile,
|
||||
setError,
|
||||
setStartFetch,
|
||||
setClsiCachePromptSegmentation,
|
||||
])
|
||||
|
||||
// listen for scroll events
|
||||
useEffect(() => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useDetachCompileContext as useCompileContext } from '../../../shared/co
|
||||
import PdfLogEntry from './pdf-log-entry'
|
||||
import { usePdfPreviewContext } from '@/features/pdf-preview/components/pdf-preview-provider'
|
||||
import getMeta from '@/utils/meta'
|
||||
import ClsiCachePrompt from '@/features/pdf-preview/components/clsi-cache-prompt'
|
||||
|
||||
function PdfLogsViewer({ alwaysVisible = false }: { alwaysVisible?: boolean }) {
|
||||
const {
|
||||
@@ -26,6 +27,7 @@ function PdfLogsViewer({ alwaysVisible = false }: { alwaysVisible?: boolean }) {
|
||||
validationIssues,
|
||||
showLogs,
|
||||
stoppedOnFirstError,
|
||||
clsiCachePromptVariant,
|
||||
} = useCompileContext()
|
||||
|
||||
const { loadingError } = usePdfPreviewContext()
|
||||
@@ -42,6 +44,8 @@ function PdfLogsViewer({ alwaysVisible = false }: { alwaysVisible?: boolean }) {
|
||||
data-testid="logs-pane"
|
||||
>
|
||||
<div className="logs-pane-content">
|
||||
{clsiCachePromptVariant === 'preview-error' && <ClsiCachePrompt />}
|
||||
|
||||
<RollingBuildSelectedReminder />
|
||||
|
||||
{codeCheckFailed && <PdfCodeCheckFailedNotice />}
|
||||
|
||||
@@ -19,6 +19,7 @@ import importOverleafModules from '../../../../macros/import-overleaf-module.mac
|
||||
import PdfCodeCheckFailedBanner from '@/features/ide-redesign/components/pdf-preview/pdf-code-check-failed-banner'
|
||||
import getMeta from '@/utils/meta'
|
||||
import NewPdfLogsViewer from '@/features/ide-redesign/components/pdf-preview/pdf-logs-viewer'
|
||||
import ClsiCachePrompt from './clsi-cache-prompt'
|
||||
|
||||
function PdfPreviewPane() {
|
||||
const { pdfUrl } = useCompileContext()
|
||||
@@ -50,6 +51,7 @@ function PdfPreviewPane() {
|
||||
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
|
||||
<div className="pdf-viewer" data-testid="pdf-viewer">
|
||||
<PdfViewer />
|
||||
<ClsiCachePrompt />
|
||||
</div>
|
||||
</Suspense>
|
||||
{newErrorLogsPosition ? (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useState, useRef } from 'react'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import { getJSON } from '../../../infrastructure/fetch-json'
|
||||
import { FetchError, getJSON } from '../../../infrastructure/fetch-json'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import useIsMounted from '../../../shared/hooks/use-is-mounted'
|
||||
import useAbortController from '../../../shared/hooks/use-abort-controller'
|
||||
@@ -33,8 +33,14 @@ export default function useSynctex(): {
|
||||
const { projectId, project } = useProjectContext()
|
||||
const rootDocId = project?.rootDocId
|
||||
|
||||
const { clsiServerId, pdfFile, position, setShowLogs, setHighlights } =
|
||||
useCompileContext()
|
||||
const {
|
||||
clsiServerId,
|
||||
pdfFile,
|
||||
position,
|
||||
setShowLogs,
|
||||
setHighlights,
|
||||
setClsiCachePromptSegmentation,
|
||||
} = useCompileContext()
|
||||
|
||||
const { selectedEntities } = useFileTreeData()
|
||||
const { findEntityByPath, dirname, pathInFolder } = useFileTreePathContext()
|
||||
@@ -121,6 +127,16 @@ export default function useSynctex(): {
|
||||
.then(data => {
|
||||
setShowLogs(false)
|
||||
setHighlights(data.pdf)
|
||||
setClsiCachePromptSegmentation(prev => {
|
||||
return {
|
||||
...prev,
|
||||
synctex: {
|
||||
direction: 'pdf',
|
||||
navigationFailed: false,
|
||||
restoredFromCache: data.downloadedFromCache,
|
||||
},
|
||||
}
|
||||
})
|
||||
if (data.downloadedFromCache) {
|
||||
sendMB('synctex-downloaded-from-cache', {
|
||||
projectId,
|
||||
@@ -129,6 +145,17 @@ export default function useSynctex(): {
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error instanceof FetchError && error.response?.status === 404) {
|
||||
setClsiCachePromptSegmentation(prev => {
|
||||
return {
|
||||
...prev,
|
||||
synctex: {
|
||||
direction: 'pdf',
|
||||
navigationFailed: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
showSynctexRequestErrorToast()
|
||||
debugConsole.error(error)
|
||||
})
|
||||
@@ -146,6 +173,7 @@ export default function useSynctex(): {
|
||||
setShowLogs,
|
||||
setHighlights,
|
||||
setSyncToPdfInFlight,
|
||||
setClsiCachePromptSegmentation,
|
||||
signal,
|
||||
]
|
||||
)
|
||||
@@ -235,6 +263,16 @@ export default function useSynctex(): {
|
||||
.then(data => {
|
||||
const [{ file, line }] = data.code
|
||||
goToCodeLine(file, line, selectText)
|
||||
setClsiCachePromptSegmentation(prev => {
|
||||
return {
|
||||
...prev,
|
||||
synctex: {
|
||||
direction: 'code',
|
||||
navigationFailed: false,
|
||||
restoredFromCache: data.downloadedFromCache,
|
||||
},
|
||||
}
|
||||
})
|
||||
if (data.downloadedFromCache) {
|
||||
sendMB('synctex-downloaded-from-cache', {
|
||||
projectId,
|
||||
@@ -243,6 +281,17 @@ export default function useSynctex(): {
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error instanceof FetchError && error.response?.status === 404) {
|
||||
setClsiCachePromptSegmentation(prev => {
|
||||
return {
|
||||
...prev,
|
||||
synctex: {
|
||||
direction: 'code',
|
||||
navigationFailed: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
debugConsole.error(error)
|
||||
showSynctexRequestErrorToast()
|
||||
})
|
||||
@@ -259,6 +308,7 @@ export default function useSynctex(): {
|
||||
signal,
|
||||
isMounted,
|
||||
setSyncToCodeInFlight,
|
||||
setClsiCachePromptSegmentation,
|
||||
goToCodeLine,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -153,6 +153,7 @@ export default class DocumentCompiler {
|
||||
)
|
||||
|
||||
const compileTimeClientE2E = Math.ceil(performance.now() - t0)
|
||||
if (data.timings) data.timings.compileTimeClientE2E = compileTimeClientE2E
|
||||
const { deliveryLatencies, firstRenderDone } = trackPdfDownload(
|
||||
data,
|
||||
compileTimeClientE2E,
|
||||
|
||||
@@ -33,4 +33,6 @@ export const projectOwnerHasPremiumOnPageLoad = getMeta(
|
||||
'ol-projectOwnerHasPremiumOnPageLoad'
|
||||
)
|
||||
export const fallBackToClsiCache =
|
||||
projectOwnerHasPremiumOnPageLoad && isFlagEnabled('populate-clsi-cache')
|
||||
projectOwnerHasPremiumOnPageLoad &&
|
||||
(isFlagEnabled('populate-clsi-cache') ||
|
||||
isFlagEnabled('populate-clsi-cache-for-prompt'))
|
||||
|
||||
@@ -48,6 +48,10 @@ export default class PDFJSWrapper {
|
||||
this.pdfCachingTransportFactory = generatePdfCachingTransportFactory()
|
||||
}
|
||||
|
||||
usedClsiCache() {
|
||||
return !!this.pdfCachingTransportFactory?.sentEventFallbackToClsiCache
|
||||
}
|
||||
|
||||
// load a document from a URL
|
||||
async loadDocument({
|
||||
url,
|
||||
|
||||
@@ -42,6 +42,17 @@ export type PdfFileDataList = {
|
||||
archive?: PdfFileArchiveData
|
||||
}
|
||||
|
||||
export type ClsiCachePromptVariant =
|
||||
| 'default'
|
||||
| 'compile'
|
||||
| 'preview'
|
||||
| 'preview-error'
|
||||
| 'synctex'
|
||||
export type ClsiCachePromptSegmentation = Record<
|
||||
ClsiCachePromptVariant,
|
||||
Record<string, any> | null
|
||||
>
|
||||
|
||||
export type HighlightData = {
|
||||
page: number
|
||||
h: number
|
||||
|
||||
@@ -56,6 +56,9 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
setStopOnValidationError: _setStopOnValidationError,
|
||||
showLogs: _showLogs,
|
||||
showCompileTimeWarning: _showCompileTimeWarning,
|
||||
clsiCachePromptVariant: _clsiCachePromptVariant,
|
||||
clsiCachePromptSegmentation: _clsiCachePromptSegmentation,
|
||||
setClsiCachePromptSegmentation: _setClsiCachePromptSegmentation,
|
||||
stopOnFirstError: _stopOnFirstError,
|
||||
stopOnValidationError: _stopOnValidationError,
|
||||
stoppedOnFirstError: _stoppedOnFirstError,
|
||||
@@ -201,6 +204,24 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [clsiCachePromptVariant] = useDetachStateWatcher(
|
||||
'clsiCachePromptVariant',
|
||||
_clsiCachePromptVariant,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [clsiCachePromptSegmentation] = useDetachStateWatcher(
|
||||
'clsiCachePromptSegmentation',
|
||||
_clsiCachePromptSegmentation,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const setClsiCachePromptSegmentation = useDetachAction(
|
||||
'setClsiCachePromptSegmentation',
|
||||
_setClsiCachePromptSegmentation,
|
||||
'detacher',
|
||||
'detached'
|
||||
)
|
||||
const [stopOnFirstError] = useDetachStateWatcher(
|
||||
'stopOnFirstError',
|
||||
_stopOnFirstError,
|
||||
@@ -419,6 +440,9 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
setStopOnValidationError,
|
||||
showLogs,
|
||||
showCompileTimeWarning,
|
||||
clsiCachePromptVariant,
|
||||
clsiCachePromptSegmentation,
|
||||
setClsiCachePromptSegmentation,
|
||||
startCompile,
|
||||
stopCompile,
|
||||
stopOnFirstError,
|
||||
@@ -472,6 +496,9 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
setStopOnValidationError,
|
||||
showCompileTimeWarning,
|
||||
showLogs,
|
||||
clsiCachePromptVariant,
|
||||
clsiCachePromptSegmentation,
|
||||
setClsiCachePromptSegmentation,
|
||||
startCompile,
|
||||
stopCompile,
|
||||
stopOnFirstError,
|
||||
|
||||
@@ -39,18 +39,20 @@ import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
||||
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
|
||||
import { getJSON } from '@/infrastructure/fetch-json'
|
||||
import { CompileResponseData } from '../../../../types/compile'
|
||||
import { CompileResponseData, PDFFile } from '../../../../types/compile'
|
||||
import {
|
||||
PdfScrollPosition,
|
||||
usePdfScrollPosition,
|
||||
} from '@/shared/hooks/use-pdf-scroll-position'
|
||||
import {
|
||||
ClsiCachePromptSegmentation,
|
||||
ClsiCachePromptVariant,
|
||||
DeliveryLatencies,
|
||||
HighlightData,
|
||||
LogEntry,
|
||||
PdfFileDataList,
|
||||
} from '@/features/pdf-preview/util/types'
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
import { getSplitTestVariant, isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
import { captureException } from '@/infrastructure/error-reporter'
|
||||
import OError from '@overleaf/o-error'
|
||||
import getMeta from '@/utils/meta'
|
||||
@@ -99,6 +101,11 @@ export type CompileContext = {
|
||||
setStopOnValidationError: (value: boolean) => void
|
||||
showCompileTimeWarning: boolean
|
||||
showLogs: boolean
|
||||
clsiCachePromptVariant: ClsiCachePromptVariant
|
||||
clsiCachePromptSegmentation: ClsiCachePromptSegmentation
|
||||
setClsiCachePromptSegmentation: Dispatch<
|
||||
SetStateAction<ClsiCachePromptSegmentation>
|
||||
>
|
||||
stopOnFirstError: boolean
|
||||
stopOnValidationError: boolean
|
||||
stoppedOnFirstError: boolean
|
||||
@@ -200,7 +207,8 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
// fetch initial compile response from cache
|
||||
const [initialCompileFromCache, setInitialCompileFromCache] = useState(
|
||||
getMeta('ol-projectOwnerHasPremiumOnPageLoad') &&
|
||||
isSplitTestEnabled('populate-clsi-cache') &&
|
||||
(isSplitTestEnabled('populate-clsi-cache') ||
|
||||
isSplitTestEnabled('populate-clsi-cache-for-prompt')) &&
|
||||
// Avoid fetching the initial compile from cache in PDF detach tab
|
||||
role !== 'detached'
|
||||
)
|
||||
@@ -209,6 +217,7 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
useState(false)
|
||||
// Raw data from clsi-cache, will need post-processing and check settings
|
||||
const [dataFromCache, setDataFromCache] = useState<CompileResponseData>()
|
||||
const [compileFromCacheStartedAt, setCompileFromCacheStartedAt] = useState(0)
|
||||
|
||||
// whether the cache is being cleared
|
||||
const [clearingCache, setClearingCache] = useState(false)
|
||||
@@ -216,6 +225,18 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
// whether the logs should be visible
|
||||
const [showLogs, setShowLogs] = useState(false)
|
||||
|
||||
// flags for clsi-cache prompt
|
||||
const [clsiCachePromptVariant, setClsiCachePromptVariant] =
|
||||
useState<ClsiCachePromptVariant>('default')
|
||||
const [clsiCachePromptSegmentation, setClsiCachePromptSegmentation] =
|
||||
useState<ClsiCachePromptSegmentation>({
|
||||
default: null,
|
||||
compile: null,
|
||||
preview: null,
|
||||
'preview-error': null,
|
||||
synctex: null,
|
||||
})
|
||||
|
||||
// whether the compile dropdown arrow should be animated
|
||||
const [animateCompileDropdownArrow, setAnimateCompileDropdownArrow] =
|
||||
useState(false)
|
||||
@@ -356,6 +377,7 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
useEffect(() => {
|
||||
if (initialCompileFromCache && !pendingInitialCompileFromCache) {
|
||||
setPendingInitialCompileFromCache(true)
|
||||
setCompileFromCacheStartedAt(performance.now())
|
||||
getJSON(`/project/${projectId}/output/cached/output.overleaf.json`)
|
||||
.then((data: any) => {
|
||||
// Hand data over to next effect, it will wait for project/doc loading.
|
||||
@@ -401,6 +423,14 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
|
||||
if (settingsUpToDate) {
|
||||
sendMB('compile-from-cache', { projectId })
|
||||
dataFromCache.clsiCachePromptVariant = getSplitTestVariant(
|
||||
'clsi-cache-prompt',
|
||||
'default'
|
||||
) as ClsiCachePromptVariant
|
||||
if (!dataFromCache.timings) dataFromCache.timings = {}
|
||||
dataFromCache.timings.compileTimeClientE2E = Math.ceil(
|
||||
performance.now() - compileFromCacheStartedAt
|
||||
)
|
||||
setData(dataFromCache)
|
||||
setCompiledOnce(true)
|
||||
}
|
||||
@@ -409,6 +439,7 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
setPendingInitialCompileFromCache(false)
|
||||
}, [
|
||||
projectId,
|
||||
compileFromCacheStartedAt,
|
||||
dataFromCache,
|
||||
joinedOnce,
|
||||
currentDocument,
|
||||
@@ -486,6 +517,32 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
if (data.clsiServerId) {
|
||||
setClsiServerId(data.clsiServerId) // set in scope, for PdfSynctexController
|
||||
}
|
||||
setClsiCachePromptVariant(data.clsiCachePromptVariant ?? 'default')
|
||||
setClsiCachePromptSegmentation(prev => {
|
||||
if (
|
||||
!(
|
||||
data.status === 'success' &&
|
||||
(data.fromCache || data.stats?.isInitialCompile === 1)
|
||||
)
|
||||
) {
|
||||
return { ...prev, compile: null }
|
||||
}
|
||||
const pdfSize = (
|
||||
data.outputFiles.find(f => f.path === 'output.pdf') as PDFFile
|
||||
)?.size
|
||||
return {
|
||||
...prev,
|
||||
compile: {
|
||||
pdfSize,
|
||||
isCompileFromCache: Boolean(data.fromCache),
|
||||
isInitialCompile: data.stats?.isInitialCompile === 1,
|
||||
restoredClsiCache: data.stats?.restoredClsiCache === 1,
|
||||
compileTimeServerE2E: data.timings?.compileE2E,
|
||||
compileTimeClientE2E: data.timings?.compileTimeClientE2E,
|
||||
clsiCacheEnabled: Boolean(data.clsiCacheShard),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
if (data.outputFiles) {
|
||||
const outputFiles = new Map()
|
||||
@@ -781,6 +838,9 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
setStopOnFirstError,
|
||||
setStopOnValidationError,
|
||||
showLogs,
|
||||
clsiCachePromptVariant,
|
||||
clsiCachePromptSegmentation,
|
||||
setClsiCachePromptSegmentation,
|
||||
startCompile,
|
||||
stopCompile,
|
||||
stopOnFirstError,
|
||||
@@ -831,6 +891,9 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
setStopOnValidationError,
|
||||
showCompileTimeWarning,
|
||||
showLogs,
|
||||
clsiCachePromptVariant,
|
||||
clsiCachePromptSegmentation,
|
||||
setClsiCachePromptSegmentation,
|
||||
startCompile,
|
||||
stopCompile,
|
||||
stopOnFirstError,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
@import '../../foundations/spacing';
|
||||
|
||||
.clsi-cache-prompt {
|
||||
width: auto; // Do not expand to full width.
|
||||
|
||||
.pdf-viewer &,
|
||||
.pdf-error-state & {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0.5rem; // Add space for scrollbar.
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
display: none; // Hide the (i) icon.
|
||||
}
|
||||
|
||||
.notification-close-btn {
|
||||
// Put even spacing between question, X and notification border.
|
||||
padding: 0 0 0 (16px - 5.5px); // The X button already has 5.5px padding.
|
||||
}
|
||||
|
||||
.btn {
|
||||
white-space: nowrap;
|
||||
margin: 10px 0 0 10px;
|
||||
}
|
||||
}
|
||||
@@ -355,6 +355,19 @@
|
||||
"clicking_delete_will_remove_sso_config_and_clear_saml_data": "Clicking <0>Delete</0> will remove your SSO configuration and unlink all users. You can only do this when SSO is disabled in your group settings.",
|
||||
"clone_with_git": "Clone with Git",
|
||||
"close": "Close",
|
||||
"clsi_cache_prompt_compile_faster": "Faster",
|
||||
"clsi_cache_prompt_compile_question": "Was this compile different than usual?",
|
||||
"clsi_cache_prompt_compile_same": "Same",
|
||||
"clsi_cache_prompt_compile_slower": "Slower",
|
||||
"clsi_cache_prompt_preview_less": "Less reliable",
|
||||
"clsi_cache_prompt_preview_more": "More reliable",
|
||||
"clsi_cache_prompt_preview_question": "How are you finding the PDF preview at the moment?",
|
||||
"clsi_cache_prompt_preview_same": "Same as usual",
|
||||
"clsi_cache_prompt_synctex_less": "Less reliable",
|
||||
"clsi_cache_prompt_synctex_more": "More reliable",
|
||||
"clsi_cache_prompt_synctex_question": "How are you finding the navigation between text and PDF?",
|
||||
"clsi_cache_prompt_synctex_same": "Same as usual",
|
||||
"clsi_cache_prompt_thanks": "Thanks for the feedback!",
|
||||
"clsi_maintenance": "The compile servers are down for maintenance, and will be back shortly.",
|
||||
"clsi_unavailable": "Sorry, the compile server for your project was temporarily unavailable. Please try again in a few moments.",
|
||||
"cn": "Chinese (Simplified)",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { expect } from 'chai'
|
||||
import { mockScope } from './scope'
|
||||
import { EditorProviders } from '../../helpers/editor-providers'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useNewEditorVariant } from '@/features/ide-redesign/utils/new-editor-utils'
|
||||
|
||||
describe('new-editor-utils', function () {
|
||||
describe('useNewEditorVariant', function () {
|
||||
const newEditorVariants = [
|
||||
'default',
|
||||
'new-editor',
|
||||
'new-editor-old-logs',
|
||||
'new-editor-new-logs-old-position',
|
||||
]
|
||||
for (const variant of newEditorVariants) {
|
||||
it(`forwards ?editor-redesign-new-users=${variant}`, function () {
|
||||
window.metaAttributesCache.set('ol-splitTestVariants', {
|
||||
'editor-redesign-new-users': variant,
|
||||
})
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
const { result } = renderHook(() => useNewEditorVariant(), {
|
||||
wrapper: ({ children }) => (
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
userSettings={{ enableNewEditor: true }}
|
||||
>
|
||||
{children}
|
||||
</EditorProviders>
|
||||
),
|
||||
})
|
||||
expect(result.current).to.equal(variant)
|
||||
})
|
||||
}
|
||||
for (const variant of newEditorVariants) {
|
||||
it(`ignores ?editor-redesign-new-users=${variant} when disabled by user`, function () {
|
||||
window.metaAttributesCache.set('ol-splitTestVariants', {
|
||||
'editor-redesign-new-users': variant,
|
||||
})
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
const { result } = renderHook(() => useNewEditorVariant(), {
|
||||
wrapper: ({ children }) => (
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
userSettings={{ enableNewEditor: false }}
|
||||
>
|
||||
{children}
|
||||
</EditorProviders>
|
||||
),
|
||||
})
|
||||
expect(result.current).to.equal('default')
|
||||
})
|
||||
}
|
||||
it(`handles ?editor-redesign=enabled`, function () {
|
||||
window.metaAttributesCache.set('ol-splitTestVariants', {
|
||||
'editor-redesign': 'enabled',
|
||||
})
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
const { result } = renderHook(() => useNewEditorVariant(), {
|
||||
wrapper: ({ children }) => (
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
userSettings={{ enableNewEditor: true }}
|
||||
>
|
||||
{children}
|
||||
</EditorProviders>
|
||||
),
|
||||
})
|
||||
expect(result.current).to.equal('new-editor')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -72,7 +72,7 @@ const defaultUserSettings = {
|
||||
}
|
||||
|
||||
export type EditorProvidersProps = {
|
||||
user?: { id: string; email: string }
|
||||
user?: { id: string; email: string; signUpDate?: string }
|
||||
projectId?: string
|
||||
projectName?: string
|
||||
projectOwner?: ProjectMetadata['owner']
|
||||
@@ -147,7 +147,11 @@ const layoutContextDefault = {
|
||||
} satisfies Partial<LayoutContextValue>
|
||||
|
||||
export function EditorProviders({
|
||||
user = { id: USER_ID, email: USER_EMAIL },
|
||||
user = {
|
||||
id: USER_ID,
|
||||
email: USER_EMAIL,
|
||||
signUpDate: '2025-10-10T10:10:10Z',
|
||||
},
|
||||
projectId = projectDefaults._id,
|
||||
projectName = projectDefaults.name,
|
||||
projectOwner = projectDefaults.owner,
|
||||
|
||||
@@ -223,6 +223,7 @@ describe('CompileController', function () {
|
||||
url: `/project/${ctx.projectId}/user/wat/build/${ctx.build_id}/output/output.zip`,
|
||||
type: 'zip',
|
||||
},
|
||||
clsiCachePromptVariant: 'default',
|
||||
pdfDownloadDomain: 'https://compiles.overleaf.test',
|
||||
})
|
||||
)
|
||||
@@ -247,6 +248,7 @@ describe('CompileController', function () {
|
||||
timings: undefined,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
buildId: ctx.build_id,
|
||||
clsiCachePromptVariant: 'default',
|
||||
})
|
||||
await ctx.CompileController.compile(ctx.req, ctx.res, ctx.next)
|
||||
})
|
||||
@@ -268,6 +270,7 @@ describe('CompileController', function () {
|
||||
url: `/project/${ctx.projectId}/user/wat/build/${ctx.build_id}/output/output.zip`,
|
||||
type: 'zip',
|
||||
},
|
||||
clsiCachePromptVariant: 'default',
|
||||
outputUrlPrefix: '/zone/b',
|
||||
pdfDownloadDomain: 'https://compiles.overleaf.test/zone/b',
|
||||
})
|
||||
@@ -318,6 +321,7 @@ describe('CompileController', function () {
|
||||
url: `/project/${ctx.projectId}/user/wat/build/${ctx.build_id}/output/output.zip`,
|
||||
type: 'zip',
|
||||
},
|
||||
clsiCachePromptVariant: 'default',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ClsiCachePromptVariant } from '@/features/pdf-preview/util/types'
|
||||
|
||||
export type Chunk = {
|
||||
start: number
|
||||
end: number
|
||||
@@ -69,9 +71,10 @@ export type CompileResponseData = {
|
||||
pdfDownloadDomain?: string
|
||||
pdfCachingMinChunkSize: number
|
||||
validationProblems: any
|
||||
stats: any
|
||||
timings: any
|
||||
stats?: Record<string, number>
|
||||
timings?: Record<string, number>
|
||||
outputFilesArchive?: CompileOutputFile
|
||||
clsiCachePromptVariant?: ClsiCachePromptVariant
|
||||
|
||||
// assigned on response body by DocumentCompiler in frontend
|
||||
rootDocId?: string | null
|
||||
|
||||
Reference in New Issue
Block a user