dsh-drawai 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1058,6 +1058,29 @@ window.__ModuleLoader__.load({
1058
1058
  return { STYLE_KEY_ORDER, PALETTE, NODE_SHAPES, NODE_SHAPE_STYLE, DASH_KINDS, ARROW_KINDS, SIDES, DEFAULT_FILL, DEFAULT_STROKE, DEFAULT_FONT, DEFAULT_NODE_STYLE, DEFAULT_EDGE_STYLE, DEFAULT_VIDEO_POSTER, parseStyle, formatStyle, styleGet, styleSet, stylePatch, styleIsEmpty, styleNumber, nodeShapeFromStyle, styleFromNodeShape, styleWithNodeShape, isVideoNodeData, reportNodeShape, isSvgMarkup, normalizeSvgMarkup, isSvgDataUri, isRasterDataUri, svgMarkupFromImageValue, imageValueFromSvgMarkup, imageValueFromStyle, svgMarkupFromStyle, styleWithSvgMarkup, styleWithoutSvgMarkup, styleWithImageDataUri, styleWithoutImageDataUri, SVG_IMAGE_VALUE_PREFIX, SVG_IMAGE_VALUE_PREFIX_B64, colorsFromStyle, colorNameFromStyle, styleWithColorName, styleWithColors, textColorNameFromStyle, styleWithTextColorName, dashFromStyle, styleWithDash, dashPatternFromStyle, arrowFromStyle, styleWithArrow, sideFromStyle, fractionFromStyle, styleWithSide, sideFromFraction, edgeStyleValueFromStyle, isOrthogonalEdgeStyle, jettyFromStyle, styleWithJetty, lineKindFromStyle, styleWithLineKind, curvedFromStyle, avoidFromStyle, styleWithAvoid, normalizePoint, normalizePoints, pointsEqual, edgeTerminalId, edgeFreePoint, copyData, normalizeDrawioDoc, snapDocGeometry }
1059
1059
  })()
1060
1060
 
1061
+ function applyTaskPatches(doc, patch, strict = false) {
1062
+ const stable = (value) => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
1063
+ ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]])) : item)
1064
+ for (const change of patch.changes || [patch]) {
1065
+ const removed = new Set(change.removed || [])
1066
+ if (strict) {
1067
+ for (const before of change.removedNodes || []) {
1068
+ const current = doc.nodes.find((node) => node.id === before.id)
1069
+ if (current && stable(current) !== stable(before)) throw new Error('后台撤销与本地节点编辑冲突,本地编辑已保留')
1070
+ }
1071
+ if ((doc.edges || []).some((edge) => removed.has(edge.from) || removed.has(edge.to))) throw new Error('后台撤销与本地连线冲突,本地编辑已保留')
1072
+ }
1073
+ doc.nodes = doc.nodes.filter((node) => !removed.has(node.id))
1074
+ if (Array.isArray(doc.drawOrder)) doc.drawOrder = doc.drawOrder.filter((id) => !removed.has(id))
1075
+ for (const node of change.added || []) if (!doc.nodes.some((item) => item.id === node.id)) {
1076
+ doc.nodes.push(JSON.parse(JSON.stringify(node)))
1077
+ if (Array.isArray(doc.drawOrder) && !doc.drawOrder.includes(node.id)) doc.drawOrder.push(node.id)
1078
+ }
1079
+ }
1080
+ return doc
1081
+ }
1082
+
1083
+
1061
1084
  // Bounded aggregates shared by browser, host and Workers. Durations may overlap.
1062
1085
  function createPerformanceMetrics(clock = () => performance.now()) {
1063
1086
  const started = clock(), stages = Object.create(null), counters = Object.create(null), peaks = Object.create(null)
@@ -1872,6 +1895,84 @@ window.__ModuleLoader__.load({
1872
1895
  return { createVideoController, VideoSurface, VideoControls, VideoPreview }
1873
1896
  })()
1874
1897
 
1898
+ // Browser-only task presentation. Execution and persistence belong to the host.
1899
+ function BackgroundTaskPanel(props) {
1900
+ const [collapsed, setCollapsed] = React.useState(false)
1901
+ const [filter, setFilter] = React.useState('active')
1902
+ const [busy, setBusy] = React.useState('')
1903
+ const [error, setError] = React.useState('')
1904
+ const jobs = props.jobs.concat(props.exports.map((job) => ({ ...job, id: job.token, export: true, label: job.clip + ' · ' + job.format.toUpperCase() })))
1905
+ const attention = (job) => ['failed', 'error', 'interrupted'].includes(job.status) || job.delivery === 'conflict'
1906
+ const ended = (job) => ['succeeded', 'done', 'cancelled', 'failed', 'error'].includes(job.status)
1907
+ const live = jobs.filter((job) => !ended(job)).length, needs = jobs.filter(attention).length
1908
+ const visible = jobs.filter((job) => filter === 'all' || (filter === 'active' ? !ended(job) || attention(job) : filter === 'attention' ? attention(job) : ended(job) && !attention(job)))
1909
+ const labels = { queued: '等待中', running: '绘制中', paused: '已暂停', cancelling: '正在取消', interrupted: '等待恢复', failed: '绘制失败', cancelled: '已取消', succeeded: '已完成', rendering: '渲染中', encoding: '编码中', done: '已保存文件', error: '导出失败' }
1910
+ const phases = { preparing: '检查素材', probing: '探测视频', decoding: '解码抽帧', tracing: '绘制色块' }
1911
+ const invoke = async (job, action, value) => {
1912
+ setBusy(job.id || 'queue'); setError('')
1913
+ try { await props.onCommand(job, action, value) }
1914
+ catch (err) { setError(String(err.message || err)) }
1915
+ finally { setBusy('') }
1916
+ }
1917
+ const button = (text, action, disabled = false) => React.createElement('button', { key: text, type: 'button', className: 'drawai-trace-pin', disabled: !!busy || disabled, onClick: action }, text)
1918
+ if (props.hidden) return null
1919
+ return React.createElement('section', { className: 'drawai-video-task-card drawai-background-tasks', role: 'region', 'aria-label': '后台任务队列',
1920
+ style: { width: 'min(380px, calc(100% - 24px))', maxHeight: '60%', overflow: 'auto', left: 12, right: 'auto' },
1921
+ onPointerDown: (event) => event.stopPropagation(), onDoubleClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(),
1922
+ }, React.createElement('div', { className: 'drawai-trace-card-heading' },
1923
+ React.createElement('strong', null, '后台任务 · ' + live + ' 项进行中' + (needs ? ' · ' + needs + ' 项需处理' : '')),
1924
+ React.createElement('div', { className: 'drawai-trace-card-actions' },
1925
+ button(collapsed ? '展开' : '收起', () => setCollapsed(!collapsed)),
1926
+ React.createElement('button', { type: 'button', className: 'drawai-trace-pin', 'aria-label': '隐藏任务列表', onClick: props.onHide }, '隐藏'))),
1927
+ collapsed ? null : React.createElement('div', null,
1928
+ React.createElement('div', { className: 'drawai-trace-card-actions', style: { flexWrap: 'wrap' }, role: 'group', 'aria-label': '筛选任务' },
1929
+ [['active', '进行中'], ['attention', '需处理'], ['complete', '已结束'], ['all', '全部']].map(([key, label]) =>
1930
+ React.createElement('button', { key, type: 'button', className: 'drawai-trace-pin', 'aria-pressed': filter === key, style: { fontWeight: filter === key ? 700 : 400 }, onClick: () => setFilter(key) }, label)),
1931
+ props.jobs.length ? button(props.jobs.some((job) => job.queuePaused) ? '继续绘制队列' : '暂停后续绘制', () => invoke({}, 'pause-queue', !props.jobs.some((job) => job.queuePaused))) : null),
1932
+ React.createElement('small', null, '切换页面或关闭网页后继续;宿主重启后可恢复绘制。'),
1933
+ error || props.error ? React.createElement('div', { role: 'alert', className: 'drawai-err', style: { userSelect: 'text' } }, error || props.error) : null,
1934
+ !visible.length ? React.createElement('p', { className: 'drawai-note' }, jobs.length ? '此分类暂无任务,可切换到“全部”查看记录。' : '暂无后台任务') : null,
1935
+ visible.map((job) => {
1936
+ const running = ['running', 'rendering', 'encoding'].includes(job.status)
1937
+ const done = job.export ? job.status === 'rendering' ? job.received : job.encoded : job.done
1938
+ const total = job.export ? job.count : job.total
1939
+ const percent = total ? Math.min(100, Math.round((done || 0) / total * 100)) : 0
1940
+ const state = job.delivery === 'conflict' ? '产物已保存 · 等待加入画布' : job.delivery === 'undone' ? '生成节点已撤销' : job.delivery === 'applied' && job.status === 'succeeded' ? '已写入画布' : labels[job.status] || job.status
1941
+ const here = !job.export && job.scope.path === props.path && job.scope.page === props.page
1942
+ const queued = props.jobs.filter((item) => item.status === 'queued')
1943
+ return React.createElement('article', { key: job.id, className: 'drawai-video-task-detail', style: { borderTop: '1px solid #8883', paddingTop: 10, marginTop: 8, userSelect: 'text' } },
1944
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 8 } },
1945
+ React.createElement('strong', { style: { overflowWrap: 'anywhere' } }, job.label), React.createElement('small', { role: 'status' }, state)),
1946
+ !job.export ? React.createElement('small', null, ({ image: '图片绘制', video: '视频绘制', extract: '视频抽帧' }[job.kind]) + ' · ' + String(job.scope.path).split(/[\\/]/).pop() + ' · ' + (job.scope.pageName || '第 ' + (job.scope.page + 1) + ' 页') + (job.kind !== 'extract' ? ' · ' + job.options.colors + ' 色' : '')) : null,
1947
+ running ? React.createElement('div', null,
1948
+ React.createElement('progress', { value: percent, max: 100, style: { width: '100%' }, 'aria-label': job.label + '进度' }),
1949
+ React.createElement('small', null, (phases[job.phase] || labels[job.status]) + (total > 1 ? ' · ' + (done || 0) + '/' + total + ' 帧 · ' + percent + '%' : '') + (job.startedAt ? ' · ' + Math.floor((Date.now() - job.startedAt) / 1000) + ' 秒' : ''))) : null,
1950
+ job.pauseRequested ? React.createElement('small', null, '正在等待完整帧保存后暂停…') : null,
1951
+ job.reducedFrames ? React.createElement('small', null, job.reducedFrames + ' 帧因 SVG 字符预算进行了细节简化') : null,
1952
+ job.path ? React.createElement('small', { style: { overflowWrap: 'anywhere' } }, '文件:' + job.path) : null,
1953
+ job.error ? React.createElement('div', { className: 'drawai-err', style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, job.error) : null,
1954
+ React.createElement('div', { className: 'drawai-trace-card-actions', style: { flexWrap: 'wrap' } },
1955
+ job.export ? [job.status === 'done' ? button('下载文件', () => invoke(job, 'download')) : null,
1956
+ ['error', 'interrupted'].includes(job.status) ? button('重新导出', () => invoke(job, 'retry-export')) : null,
1957
+ button(ended(job) || job.status === 'interrupted' ? '移除记录' : '取消导出', () => invoke(job, 'discard'))] : [
1958
+ job.status === 'queued' ? button('上移', () => invoke(job, 'move', -1), queued[0]?.id === job.id) : null,
1959
+ job.status === 'queued' ? button('下移', () => invoke(job, 'move', 1), queued.at(-1)?.id === job.id) : null,
1960
+ job.status === 'queued' || job.status === 'running' && job.kind === 'video' ? button('暂停', () => invoke(job, 'pause'), job.pauseRequested) : null,
1961
+ ['paused', 'interrupted'].includes(job.status) ? button('继续', () => invoke(job, 'resume')) : null,
1962
+ ['failed', 'cancelled'].includes(job.status) ? button('重试', () => invoke(job, 'retry')) : null,
1963
+ ['failed', 'cancelled', 'interrupted', 'paused'].includes(job.status) ? button('重新开始', () => invoke(job, 'restart')) : null,
1964
+ ['queued', 'running', 'paused', 'interrupted'].includes(job.status) ? button('取消', () => invoke(job, 'cancel')) : null,
1965
+ job.delivery === 'conflict' || job.delivery === 'undone' ? button('加入原画布', () => invoke(job, 'apply')) : null,
1966
+ job.delivery === 'applied' && job.kind !== 'extract' ? button('撤销生成', () => invoke(job, 'undo')) : null,
1967
+ job.nodeId && here ? button(job.kind === 'image' ? '查看 / 回放绘制' : '定位结果', () => invoke(job, 'locate')) : null,
1968
+ !here ? button('打开原页面', () => props.onOpen(job)) : null,
1969
+ ended(job) ? button('移除记录', () => invoke(job, 'remove')) : null,
1970
+ ],
1971
+ job.error ? button('复制错误', () => writeClipboardText(job.error).catch(() => {})) : null))
1972
+ })))
1973
+ }
1974
+
1975
+
1875
1976
  const React = require('react')
1876
1977
  const { createVideoController, VideoSurface, VideoControls, VideoPreview } = videoPlayer
1877
1978
 
@@ -4455,6 +4556,12 @@ window.__ModuleLoader__.load({
4455
4556
  if (kind === 'text') {
4456
4557
  return svg(React.createElement('path', { d: 'M6 6.5h12M12 6.5V18' }))
4457
4558
  }
4559
+ if (kind === 'tasks') {
4560
+ return svg([
4561
+ React.createElement('path', { key: 'checks', d: 'M3 6l1 1 2-2M3 12l1 1 2-2M3 18l1 1 2-2' }),
4562
+ React.createElement('path', { key: 'rows', d: 'M10 6h11M10 12h11M10 18h11' }),
4563
+ ])
4564
+ }
4458
4565
  if (kind === 'section') {
4459
4566
  // 叠框:分区 / 容器(对应飞书侧栏的重叠矩形)。
4460
4567
  return svg([
@@ -5096,7 +5203,7 @@ window.__ModuleLoader__.load({
5096
5203
 
5097
5204
  /** 本机偏好(不进文件):网格、默认形状/配色。 */
5098
5205
  const DRAWAI_PREFS_KEY = 'dsh-drawai.prefs'
5099
- const DEFAULT_DRAWAI_PREFS = { showGrid: true, defaultShape: 'rounded', defaultColor: 'blue' }
5206
+ const DEFAULT_DRAWAI_PREFS = { showGrid: true, showTaskList: false, defaultShape: 'rounded', defaultColor: 'blue' }
5100
5207
 
5101
5208
  const STANDARD_COLORS = [
5102
5209
  ['#ffffff', '白'], ['#f5f5f5', '浅灰'], ['#e1d5e7', '浅紫'], ['#eff4ff', '冰蓝'],
@@ -5617,7 +5724,9 @@ window.__ModuleLoader__.load({
5617
5724
  const [options, setOptions] = React.useState(() => Object.assign({ range: 'clip', from: 0, end: 5, fps: 12, resolution: '480', width: 1024,
5618
5725
  level: 'medium', preset: 'default', colors: 12, removeBg: false }, props.initialOptions || props.job && props.job.options || {}))
5619
5726
  const job = props.job
5620
- const busy = job && job.busy
5727
+ const [submitting, setSubmitting] = React.useState(false)
5728
+ const [submitError, setSubmitError] = React.useState('')
5729
+ const busy = submitting || job && job.busy
5621
5730
  const changeOptions = (patch) => {
5622
5731
  const next = Object.assign({}, options, patch)
5623
5732
  setOptions(next)
@@ -5661,12 +5770,18 @@ window.__ModuleLoader__.load({
5661
5770
  React.createElement('div', { className: 'drawai-export-info', role: 'status' }, invalid || estimate),
5662
5771
  React.createElement('div', { className: 'drawai-export-info' }, '画质、帧率和范围由你决定。提高尺寸、帧率或时长会增加处理时间与文件大小;小视频不放大。原视频保留,结果不含音轨。'),
5663
5772
  job ? React.createElement('div', { role: job.error ? 'alert' : 'status', 'aria-live': 'polite' }, job.message) : null,
5773
+ submitError ? React.createElement('div', { role: 'alert', className: 'drawai-err' }, submitError) : null,
5664
5774
  busy ? React.createElement('progress', { value: job.done || 0, max: job.total || 1, 'aria-label': '视频复刻进度', style: { width: '100%' } }) : null,
5665
5775
  job && job.svg ? React.createElement('img', { src: 'data:image/svg+xml,' + encodeURIComponent(job.svg), alt: '最近完成的 SVG 帧', style: { width: '100%', maxHeight: '180px', objectFit: 'contain' } }) : null,
5666
5776
  React.createElement('div', { className: 'drawai-layer-footer' },
5667
5777
  props.inline ? null : React.createElement('button', { type: 'button', className: 'drawai-btn', onClick: busy ? props.onCancel : props.onClose }, busy ? '取消复刻' : '关闭'),
5668
5778
  React.createElement('button', { type: 'button', className: 'drawai-btn drawai-primary-btn', disabled: busy || !!invalid,
5669
- onClick: () => props.onStart(options) }, job && job.error ? '重新复刻' : props.queued ? '加入绘制队列' : '让大肥鱼开始画画!'))))
5779
+ onClick: async () => {
5780
+ setSubmitting(true); setSubmitError('')
5781
+ try { await props.onStart(options) }
5782
+ catch (error) { setSubmitError(String(error.message || error)) }
5783
+ finally { setSubmitting(false) }
5784
+ } }, job && job.error ? '重新复刻' : props.queued ? '加入绘制队列' : '让大肥鱼开始画画!'))))
5670
5785
  }
5671
5786
 
5672
5787
  // 安装监听后才触发 load/seek;超时、取消、解码错误均释放监听器。
@@ -5872,7 +5987,7 @@ window.__ModuleLoader__.load({
5872
5987
  }, props.renderPanel ? props.renderPanel(close) : React.createElement(ImageTracePanel, {
5873
5988
  key: props.source && props.source.id, source: props.source,
5874
5989
  onGenerate: (options) => props.onGenerate(props.source, options),
5875
- onEnqueue: props.onEnqueue ? (options) => { const job = props.onEnqueue(props.source, options); close(); return job } : undefined,
5990
+ onEnqueue: props.onEnqueue ? async (options) => { const job = await props.onEnqueue(props.source, options); close(); return job } : undefined,
5876
5991
  onApply: (result) => { props.onApply(props.source, result); close() },
5877
5992
  })),
5878
5993
  )
@@ -5900,6 +6015,29 @@ window.__ModuleLoader__.load({
5900
6015
  job.error && job.options ? React.createElement('button', { type: 'button', className: 'drawai-btn', onClick: props.onRetry }, '重试复刻') : null))
5901
6016
  }
5902
6017
 
6018
+ function VideoExportJobs(props) {
6019
+ const [collapsed, setCollapsed] = React.useState(false)
6020
+ if (!props.jobs.length) return null
6021
+ const button = (label, onClick) => React.createElement('button', { type: 'button', className: 'drawai-trace-pin', onClick }, label)
6022
+ return React.createElement('section', { className: 'drawai-video-task-card', role: 'region', 'aria-label': '后台视频导出',
6023
+ style: { top: 68, bottom: 'auto', left: 12, right: 'auto', width: 320, maxHeight: '45%', overflow: 'auto' },
6024
+ onPointerDown: (event) => event.stopPropagation(), onDoubleClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(),
6025
+ }, React.createElement('div', { className: 'drawai-trace-card-heading' },
6026
+ React.createElement('strong', null, '后台导出 · ' + props.jobs.length),
6027
+ button(collapsed ? '展开' : '收起', () => setCollapsed(!collapsed))),
6028
+ collapsed ? null : props.jobs.map((job) => React.createElement('div', { key: job.token, className: 'drawai-video-task-detail' },
6029
+ React.createElement('strong', null, job.clip + ' · ' + job.format.toUpperCase() + ' · ' +
6030
+ ({ rendering: '正在渲染', encoding: '正在编码', done: '已保存', error: '导出失败' }[job.status] || job.status)),
6031
+ ['rendering', 'encoding'].includes(job.status) ? React.createElement('small', null,
6032
+ (job.status === 'rendering' ? job.received : job.encoded) + (job.count ? '/' + job.count : '') + ' 帧;切换页面后仍会继续') : null,
6033
+ job.path ? React.createElement('small', { style: { overflowWrap: 'anywhere', userSelect: 'text' } }, '已保存:' + job.path) : null,
6034
+ job.error ? React.createElement('div', { className: 'drawai-err', role: 'alert' }, job.error) : null,
6035
+ React.createElement('div', { className: 'drawai-trace-card-actions' },
6036
+ job.error ? button('复制错误', () => writeClipboardText(job.error).catch(() => {})) : null,
6037
+ job.status === 'done' ? button('下载文件', () => props.onDownload(job)) : null,
6038
+ button(['done', 'error'].includes(job.status) ? '移除记录' : '取消导出', () => props.onDiscard(job))))))
6039
+ }
6040
+
5903
6041
  function DrawingQueuePanel(props) {
5904
6042
  const [, refresh] = React.useState(0)
5905
6043
  const [collapsed, setCollapsed] = React.useState(false)
@@ -8593,6 +8731,42 @@ window.__ModuleLoader__.load({
8593
8731
  mediaRequestRef.current = mediaAction
8594
8732
  const [videoController] = React.useState(() => createVideoController((action, extra) => mediaRequestRef.current(action, extra)))
8595
8733
  const videoExportRun = React.useRef(false)
8734
+ const [videoExportJobs, setVideoExportJobs] = React.useState([])
8735
+ const [backgroundDrawingJobs, setBackgroundDrawingJobs] = React.useState([])
8736
+ const [backgroundQueueError, setBackgroundQueueError] = React.useState('')
8737
+ React.useEffect(() => {
8738
+ if (!client || !hasPath || !active) return
8739
+ let alive = true, timer, fetching = false
8740
+ const poll = async () => {
8741
+ if (!alive || fetching) return
8742
+ fetching = true
8743
+ try {
8744
+ const result = await mediaAction('drawing-task-list', {}, { throwOnError: true })
8745
+ if (alive) { setBackgroundDrawingJobs(result.jobs || []); setBackgroundQueueError('') }
8746
+ } catch (error) { if (alive) setBackgroundQueueError('任务状态暂时无法更新:' + String(error.message || error)) }
8747
+ finally { fetching = false; if (alive) timer = setTimeout(poll, document.hidden ? 8000 : 1500) }
8748
+ }
8749
+ const resume = () => { if (!document.hidden) { clearTimeout(timer); poll() } }
8750
+ poll(); document.addEventListener('visibilitychange', resume)
8751
+ return () => { alive = false; clearTimeout(timer); document.removeEventListener('visibilitychange', resume) }
8752
+ }, [client, sessionId, target, hasPath, active])
8753
+ React.useEffect(() => {
8754
+ if (!client || !hasPath || !active) return
8755
+ let alive = true, timer, fetching = false
8756
+ const poll = async () => {
8757
+ if (!alive || fetching) return
8758
+ fetching = true
8759
+ try {
8760
+ const result = await mediaAction('media-video-export-list', {}, { throwOnError: true })
8761
+ if (alive && Array.isArray(result?.jobs)) setVideoExportJobs(result.jobs)
8762
+ } catch { /* Keep the last known progress while disconnected. */ }
8763
+ finally { fetching = false; if (alive) timer = setTimeout(poll, 1500) }
8764
+ }
8765
+ const resume = () => { if (!document.hidden) { clearTimeout(timer); poll() } }
8766
+ setVideoExportJobs([]); poll()
8767
+ document.addEventListener('visibilitychange', resume)
8768
+ return () => { alive = false; clearTimeout(timer); document.removeEventListener('visibilitychange', resume) }
8769
+ }, [client, sessionId, target, hasPath, active])
8596
8770
  React.useEffect(() => () => videoController.reset(), [videoController, target])
8597
8771
  React.useEffect(() => { if (!active) videoController.suspend() }, [videoController, active])
8598
8772
  React.useEffect(() => { videoController.prune(doc === null ? [] : doc.nodes) }, [videoController, doc])
@@ -8642,6 +8816,12 @@ window.__ModuleLoader__.load({
8642
8816
  const pageIndex = pageIndexState[0]
8643
8817
  const setPageIndex = pageIndexState[1]
8644
8818
  React.useEffect(() => { canvasSessions.workspace(sessionId).pages[target] = pageIndex }, [sessionId, target, pageIndex])
8819
+ const handledPageRequest = React.useRef(null)
8820
+ React.useEffect(() => {
8821
+ if (props.pageRequest && active && props.pageRequest !== handledPageRequest.current) {
8822
+ handledPageRequest.current = props.pageRequest; switchPage(props.pageRequest.page)
8823
+ }
8824
+ }, [props.pageRequest, active])
8645
8825
  React.useEffect(() => () => {
8646
8826
  pendingVideoRef.current = null
8647
8827
  for (const controller of videoImportsRef.current || []) controller.abort()
@@ -8655,6 +8835,7 @@ window.__ModuleLoader__.load({
8655
8835
  const prefs = prefsState[0]
8656
8836
  const setPrefs = prefsState[1]
8657
8837
  const showGrid = prefs.showGrid !== false
8838
+ const showTaskList = prefs.showTaskList === true
8658
8839
  function patchPrefs(patch) {
8659
8840
  const next = Object.assign({}, prefs, patch === undefined || patch === null ? {} : patch)
8660
8841
  setPrefs(next)
@@ -8674,6 +8855,18 @@ window.__ModuleLoader__.load({
8674
8855
  const edgePreview = previewState[0]
8675
8856
  const setEdgePreview = previewState[1]
8676
8857
  const [traceDrawing, setTraceDrawing] = React.useState(null)
8858
+ const autoReplayTasks = React.useRef(new Set())
8859
+ React.useEffect(() => {
8860
+ if (!active || !doc) return
8861
+ for (const taskId of autoReplayTasks.current) {
8862
+ const node = doc.nodes.find((item) => item.data?.drawaiTaskId === taskId)
8863
+ if (!node) continue
8864
+ autoReplayTasks.current.delete(taskId)
8865
+ mediaAction('drawing-task-result', { taskId }, { throwOnError: true }).then((result) => {
8866
+ if (result.output.drawing?.steps.length) setTraceDrawing({ id: node.id, svg: result.output.svg, drawing: result.output.drawing, metrics: result.output.metrics })
8867
+ }).catch((error) => setSaveNote('绘制结果已保存,回放加载失败:' + String(error.message || error)))
8868
+ }
8869
+ }, [active, doc])
8677
8870
  const videoTraceRun = canvasSessions.task(sessionId, target, pageIndex)
8678
8871
  const [, refreshVideoTrace] = React.useState(0)
8679
8872
  React.useEffect(() => canvasSessions.subscribe(videoTraceRun, () => refreshVideoTrace((value) => value + 1)), [videoTraceRun])
@@ -8715,23 +8908,50 @@ window.__ModuleLoader__.load({
8715
8908
  }
8716
8909
  }
8717
8910
  React.useEffect(applyPendingVideoTrace, [doc, videoTraceRun, videoTraceRun.result, status.kind])
8718
- function enqueueVideoTrace(options, source) {
8719
- const job = drawingQueue.enqueue({ scope: { sessionId: sessionId || '', path: target, page: pageIndex }, kind: 'video',
8720
- label: '视频 · ' + source.id, source, options }, (context, frozenSource, frozenOptions) => startVideoTrace(frozenOptions, frozenSource, context))
8721
- setSaveNote('已加入绘制队列,可继续添加图片或视频')
8722
- return job
8723
- }
8724
- function enqueueImageTrace(source, options) {
8725
- const job = drawingQueue.enqueue({ scope: { sessionId: sessionId || '', path: target, page: pageIndex }, kind: 'image',
8726
- label: '图片 · ' + source.id, source, options }, async (context, frozenSource, frozenOptions) => {
8727
- context.update({ message: '正在分析并绘制图片…' })
8728
- const output = await generateImageTrace(frozenSource, frozenOptions)
8729
- if (context.signal.aborted) throw new Error('已取消绘制')
8730
- context.ready()
8731
- canvasSessions.publish(videoTraceRun, { kind: 'image', taskId: context.id, source: frozenSource, output })
8732
- })
8733
- setSaveNote('已加入绘制队列,可继续添加图片或视频')
8734
- return job
8911
+ async function submitBackgroundDrawing(kind, source, options) {
8912
+ await saveNow()
8913
+ if (dirtyRef.current || saveInFlightRef.current) throw new Error('画布自动保存未完成,请稍后重试')
8914
+ const result = await mediaAction('drawing-task-start', { kind, sourceId: source.id, options,
8915
+ page: pageIndex, revision: revisionRef.current }, { throwOnError: true })
8916
+ setBackgroundDrawingJobs((jobs) => jobs.filter((job) => job.id !== result.job.id).concat(result.job))
8917
+ if (kind === 'image') autoReplayTasks.current.add(result.job.id)
8918
+ setSaveNote('已加入后台队列,切换或关闭网页后仍会继续')
8919
+ return result.job
8920
+ }
8921
+ async function enqueueVideoTrace(options, source) {
8922
+ return submitBackgroundDrawing('video', source, options)
8923
+ }
8924
+ async function enqueueImageTrace(source, options) {
8925
+ return submitBackgroundDrawing('image', source, options)
8926
+ }
8927
+ async function backgroundTaskCommand(job, command, value) {
8928
+ if (job.export) {
8929
+ if (command === 'download') return downloadVideoExport(job)
8930
+ if (command === 'retry-export') {
8931
+ const result = await mediaAction('media-video-export-start', { clip: job.clip, format: job.format }, { throwOnError: true })
8932
+ setVideoExportJobs((jobs) => jobs.concat(result)); return
8933
+ }
8934
+ await mediaAction('media-video-export-discard', { clip: job.clip, token: job.token }, { throwOnError: true })
8935
+ setVideoExportJobs((jobs) => jobs.filter((item) => item.token !== job.token)); return
8936
+ }
8937
+ if (command === 'locate') {
8938
+ const node = docRef.current?.nodes.find((item) => item.data?.drawaiTaskId === job.id)
8939
+ if (!node) throw new Error('结果尚未同步到当前页,请稍后重试或打开原页面')
8940
+ setSelectedIds([node.id]); setViewOverride(null)
8941
+ if (job.kind === 'image') {
8942
+ const result = await mediaAction('drawing-task-result', { taskId: job.id }, { throwOnError: true })
8943
+ if (result.output.drawing?.steps.length) setTraceDrawing({ id: node.id, svg: result.output.svg, drawing: result.output.drawing, metrics: result.output.metrics })
8944
+ }
8945
+ return
8946
+ }
8947
+ if (['apply', 'undo'].includes(command)) {
8948
+ await saveNow()
8949
+ if (dirtyRef.current) throw new Error('请等待当前编辑自动保存成功后重试')
8950
+ }
8951
+ await mediaAction('drawing-task-command', { taskId: job.id, command, value }, { throwOnError: true })
8952
+ const result = await mediaAction('drawing-task-list', {}, { throwOnError: true })
8953
+ setBackgroundDrawingJobs(result.jobs || [])
8954
+ if (['apply', 'undo'].includes(command)) setNonce((value) => value + 1)
8735
8955
  }
8736
8956
  async function startVideoTrace(options, sourceOverride, queueContext) {
8737
8957
  if (videoTraceRun.current || videoTraceRun.result || videoTraceRun.pendingSave || !(sourceOverride || videoTraceSource)) {
@@ -8983,6 +9203,15 @@ window.__ModuleLoader__.load({
8983
9203
  }
8984
9204
 
8985
9205
  /** 服务端推来新版本时清空历史 —— 否则一次撤销会把别人(或 AI)刚写的改动悄悄顶掉。 */
9206
+ function mergeBackgroundHistory(patch) {
9207
+ for (const document of historyRef.current.past.concat(historyRef.current.future)) applyTaskPatches(document, patch)
9208
+ if (docRef.current) {
9209
+ const next = applyTaskPatches(cloneDoc(docRef.current), patch)
9210
+ docRef.current = next; setDoc(next)
9211
+ }
9212
+ lastCoalesceRef.current = null
9213
+ }
9214
+
8986
9215
  function resetHistory() {
8987
9216
  historyRef.current.past.length = 0
8988
9217
  historyRef.current.future.length = 0
@@ -9586,10 +9815,9 @@ window.__ModuleLoader__.load({
9586
9815
  if (response === null || response.ok !== true) {
9587
9816
  const message = String(response !== null && response.error ? response.error : 'unknown')
9588
9817
  if (message === 'revision conflict') {
9589
- reportSaveError('自动保存遇到版本冲突,正在重新载入画布')
9590
- // 别人改了这份文件(drawio、AI、别的编辑器都算):不重试、不覆盖,
9591
- // 下一次轮询就会把新版本拉回来。
9592
- setSaveNote('保存被拒:文件已被他处修改,稍后会重新载入')
9818
+ dirtyRef.current = true
9819
+ reportSaveError('自动保存遇到版本冲突,本地编辑已保留')
9820
+ setSaveNote('保存冲突:本地编辑已保留,请先另存副本或处理外部修改')
9593
9821
  return
9594
9822
  }
9595
9823
  dirtyRef.current = true
@@ -9598,6 +9826,7 @@ window.__ModuleLoader__.load({
9598
9826
  return
9599
9827
  }
9600
9828
  revisionRef.current = response.revision
9829
+ if (response.backgroundPatch) mergeBackgroundHistory(response.backgroundPatch)
9601
9830
  canvasSessions.acknowledge(videoTraceRun, snapshot)
9602
9831
  // 有边被略过就如实说(两端都没有落点的边写不进文件),别让用户以为"全保存了"。
9603
9832
  const dropped = Array.isArray(response.dropped) ? response.dropped : []
@@ -12182,11 +12411,12 @@ window.__ModuleLoader__.load({
12182
12411
  */
12183
12412
  async function pull() {
12184
12413
  let payload = null
12414
+ const requestedRevision = revisionRef.current
12185
12415
  try {
12186
12416
  const raw = await fetch(SAVE_ENDPOINT, {
12187
12417
  method: 'POST',
12188
12418
  headers: { 'content-type': 'application/json', [SAVE_HEADER]: '1' },
12189
- body: JSON.stringify({ action: 'read', sessionId: sessionId, path: target, page: pageIndex }),
12419
+ body: JSON.stringify({ action: 'read', sessionId: sessionId, path: target, page: pageIndex, revision: requestedRevision }),
12190
12420
  signal: signal,
12191
12421
  })
12192
12422
  payload = await raw.json()
@@ -12279,6 +12509,7 @@ window.__ModuleLoader__.load({
12279
12509
  setStatus((prev) => (prev.kind === 'ready' ? prev : { kind: 'ready', path: absolute, error: '', absolute: absolute }))
12280
12510
  return
12281
12511
  }
12512
+ if (revisionRef.current !== requestedRevision) return
12282
12513
  if (loadedDocumentRef.current === videoTraceRun && parsed.doc.revision === revisionRef.current && docRef.current !== null) {
12283
12514
  setStatus((prev) => (prev.kind === 'ready' && prev.absolute === absolute ? prev : { kind: 'ready', path: absolute, error: '', absolute: absolute }))
12284
12515
  return
@@ -12287,7 +12518,8 @@ window.__ModuleLoader__.load({
12287
12518
  loadedDocumentRef.current = videoTraceRun
12288
12519
  canvasSessions.restore(videoTraceRun, parsed.doc)
12289
12520
  revisionRef.current = parsed.doc.revision
12290
- resetHistory()
12521
+ if (payload.backgroundPatch) mergeBackgroundHistory(payload.backgroundPatch)
12522
+ else resetHistory()
12291
12523
  setDoc(parsed.doc)
12292
12524
  if (Number.isFinite(Number(payload.pageCount))) setPageCount(Math.max(1, Number(payload.pageCount)))
12293
12525
  if (Number.isFinite(Number(payload.pageIndex))) setPageIndex(Number(payload.pageIndex))
@@ -12730,6 +12962,13 @@ window.__ModuleLoader__.load({
12730
12962
  * - `extractStillsFromVideo(node, every, max)` —— 工具栏「抽帧」按钮的老路径:t = i/12、步长 every。
12731
12963
  */
12732
12964
  async function extractStillsFromVideo(node, planOrEvery, legacyMax) {
12965
+ if (videoMetaOf(node)?.kind === 'raster') {
12966
+ const options = planOrEvery && typeof planOrEvery === 'object'
12967
+ ? { ...planOrEvery, range: 'segment' }
12968
+ : { range: 'all', fps: DEFAULT_MEDIA_FPS / Math.max(1, Number(planOrEvery) || 1), width: 480, max: legacyMax || 1200 }
12969
+ try { return await submitBackgroundDrawing('extract', node, options) }
12970
+ catch (error) { setSaveNote('后台抽帧失败:' + String(error.message || error)); return }
12971
+ }
12733
12972
  const meta = videoMetaOf(node)
12734
12973
  if (meta === null) return
12735
12974
  const plan =
@@ -12847,68 +13086,28 @@ window.__ModuleLoader__.load({
12847
13086
  setSaveNote('正在准备逐帧导出 ' + format.toUpperCase() + '…')
12848
13087
  const pack = await request('media-video-export-start', { format })
12849
13088
  perf.enable(pack.performanceEnabled)
12850
- const auth = { token: pack.token }
12851
- try {
12852
- if (meta.kind === 'svg') {
12853
- const canvas = document.createElement('canvas')
12854
- canvas.width = pack.w; canvas.height = pack.h
12855
- const context = canvas.getContext('2d')
12856
- if (!context) throw new Error('浏览器无法创建导出画布')
12857
- const signal = new AbortController().signal
12858
- // Every index is rendered and acknowledged, even if one frame is slow.
12859
- for (let i = 0; i < pack.count; i += 1) {
12860
- const frame = await request('media-frame', { index: i })
12861
- const image = new Image()
12862
- await perf.measure('svgImageDecode', () => new Promise((resolve, reject) => {
12863
- image.onload = resolve; image.onerror = () => reject(new Error('第 ' + (i + 1) + ' 帧解码失败'))
12864
- image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(frame.svg)
12865
- }))
12866
- const drawDone = perf.start('canvasRender')
12867
- context.fillStyle = '#fff'; context.fillRect(0, 0, pack.w, pack.h)
12868
- context.drawImage(image, 0, 0, pack.w, pack.h)
12869
- drawDone()
12870
- const png = await perf.measure('pngEncode', () => new Promise((resolve, reject) => canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error('帧图片导出失败')), 'image/png')))
12871
- perf.count('pngBytes', png.size)
12872
- for (let offset = 0; offset < png.size; offset += 1024 * 1024) {
12873
- const base64 = await perf.measure('base64Encode', () => readVideoChunk(png.slice(offset, offset + 1024 * 1024), signal))
12874
- await request('media-video-export-chunk', Object.assign({}, auth, { index: i, offset, base64 }))
12875
- }
12876
- await request('media-video-export-frame', Object.assign({}, auth, { index: i }))
12877
- setSaveNote('正在准备导出帧:' + (i + 1) + '/' + pack.count + '(' + pack.fps + 'fps)')
12878
- }
12879
- }
12880
- const encodeDone = perf.start('encodingAndPolling')
12881
- let status = await request('media-video-export-finish', auth)
12882
- while (status.status === 'encoding') {
12883
- setSaveNote('正在编码 ' + format.toUpperCase() + ':' + status.encoded + (pack.count ? '/' + pack.count : '') + ' 帧…')
12884
- await new Promise((resolve) => setTimeout(resolve, 250))
12885
- status = await request('media-video-export-status', auth)
12886
- }
12887
- encodeDone()
12888
- if (status.status !== 'done') throw new Error(status.error || '视频编码失败')
12889
- const downloadDone = perf.start('download')
12890
- const chunks = []
12891
- for (let offset = 0; offset < status.bytes;) {
12892
- const part = await request('media-video-export-read', Object.assign({}, auth, { offset }))
12893
- const bytes = Uint8Array.from(atob(part.base64), (c) => c.charCodeAt(0))
12894
- if (part.offset !== offset || !bytes.length || bytes.length !== part.bytes || part.total !== status.bytes) throw new Error('导出文件下载不完整')
12895
- chunks.push(bytes); offset += bytes.length
12896
- }
12897
- downloadDone()
12898
- const blob = new Blob(chunks, { type: 'video/' + format })
12899
- if (!blob.size) throw new Error('视频编码结果为空')
12900
- const url = URL.createObjectURL(blob)
12901
- try {
12902
- const anchor = document.createElement('a'); anchor.href = url
12903
- anchor.download = (typeof target === 'string' ? target.split(/[\/]/).pop().replace(/.drawio$/i, '') : 'clip') + '-' + meta.clip + '.' + format
12904
- anchor.click()
12905
- } finally { URL.revokeObjectURL(url) }
12906
- setSaveNote('已导出 ' + format.toUpperCase() + (pack.count ? ':' + pack.count + ' 帧,' + pack.fps + 'fps' : '') + '(白底,保留原片段音频)')
12907
- } finally {
12908
- const report = perf.report()
12909
- // Release encoded files and uploaded PNGs on both success and failure.
12910
- await mediaAction('media-video-export-discard', Object.assign({ clip: meta.clip, performance: pack.performanceEnabled ? report : undefined }, auth)).catch(() => {})
13089
+ if (!pack.token) throw new Error('后台导出任务未返回令牌')
13090
+ setVideoExportJobs((jobs) => [pack, ...jobs.filter((job) => job.token !== pack.token)])
13091
+ setSaveNote('已在后台开始导出 ' + format.toUpperCase() + ',完成后自动保存到画布文件旁,可切换标签页或对话')
13092
+ }
13093
+
13094
+ async function downloadVideoExport(job) {
13095
+ const chunks = []
13096
+ for (let offset = 0; offset < job.bytes;) {
13097
+ const part = await mediaAction('media-video-export-read', { clip: job.clip, token: job.token, offset }, { throwOnError: true })
13098
+ const bytes = Uint8Array.from(atob(part.base64), (c) => c.charCodeAt(0))
13099
+ if (part.offset !== offset || !bytes.length || bytes.length !== part.bytes || part.total !== job.bytes) throw new Error('导出文件下载不完整')
13100
+ chunks.push(bytes); offset += bytes.length
12911
13101
  }
13102
+ const blob = new Blob(chunks, { type: job.format === 'gif' ? 'image/gif' : 'video/' + job.format })
13103
+ if (!blob.size) throw new Error('导出文件为空')
13104
+ const url = URL.createObjectURL(blob)
13105
+ try {
13106
+ const anchor = document.createElement('a'); anchor.href = url
13107
+ anchor.download = job.path ? job.path.split(/[\\/]/).pop() : job.clip + '.' + job.format
13108
+ anchor.click()
13109
+ } finally { URL.revokeObjectURL(url) }
13110
+ setSaveNote('已下载 ' + job.format.toUpperCase() + ';后台保存的文件仍然保留')
12912
13111
  }
12913
13112
 
12914
13113
  async function grabVideoStill(node) {
@@ -12970,6 +13169,7 @@ window.__ModuleLoader__.load({
12970
13169
  if (result) setSaveNote('已导出 ' + result.path)
12971
13170
  } },
12972
13171
  { label: '导出 MP4', export: true, disabled: () => !hasPath || !meta || videoExportRun.current, run: () => exportVideoFile(node, 'mp4') },
13172
+ { label: '导出 GIF', export: true, disabled: () => !hasPath || !meta || videoExportRun.current, run: () => exportVideoFile(node, 'gif') },
12973
13173
  { label: '导出 WebM', export: true, mediaKind: 'svg', disabled: () => !hasPath || !meta || meta.kind !== 'svg' || videoExportRun.current, run: () => exportVideoFile(node, 'webm') },
12974
13174
  { label: '导出源视频', export: true, mediaKind: 'raster', disabled: () => !hasPath || !meta || meta.kind !== 'raster', run: async () => {
12975
13175
  if (!meta) return
@@ -12986,10 +13186,10 @@ window.__ModuleLoader__.load({
12986
13186
  renderPanel: (close) => React.createElement(VideoTracePanel, { inline: true, queued: true, source: {
12987
13187
  id: node.id, clip: meta.clip, w: Number(node.data.mediaW), h: Number(node.data.mediaH),
12988
13188
  duration: Number(node.data.mediaDuration) || videoController.get(node.id).duration,
12989
- }, onClose: close, onStart: (options) => {
13189
+ }, onClose: close, onStart: async (options) => {
12990
13190
  const source = { id: node.id, clip: meta.clip, w: Number(node.data.mediaW), h: Number(node.data.mediaH),
12991
13191
  duration: Number(node.data.mediaDuration) || videoController.get(node.id).duration }
12992
- try { enqueueVideoTrace(options, source); close() } catch (error) { reportError(error) }
13192
+ await enqueueVideoTrace(options, source); close()
12993
13193
  } }),
12994
13194
  }),
12995
13195
  renderExport: (items, state, run, icon) => React.createElement(ToolbarPopover, {
@@ -13217,6 +13417,18 @@ window.__ModuleLoader__.load({
13217
13417
  )
13218
13418
  }
13219
13419
 
13420
+ const activeTaskCount = backgroundDrawingJobs.concat(videoExportJobs).filter((job) =>
13421
+ !['succeeded', 'done', 'cancelled', 'failed', 'error'].includes(job.status)).length
13422
+ chips.push(React.createElement('div', { key: 'tasks-separator', className: 'drawai-rail-sep', style: { marginTop: 'auto' } }),
13423
+ React.createElement('button', {
13424
+ key: 'rail-tasks', type: 'button', className: 'drawai-rail-chip' + (showTaskList ? ' on' : ''),
13425
+ style: { position: 'relative' }, 'aria-label': showTaskList ? '隐藏任务列表' : '展开任务列表', 'aria-expanded': showTaskList,
13426
+ title: '后台任务' + (activeTaskCount ? ' · ' + activeTaskCount + ' 项进行中' : ''),
13427
+ onClick: () => patchPrefs({ showTaskList: !showTaskList }),
13428
+ }, railIcon('tasks'), activeTaskCount ? React.createElement('span', { 'aria-hidden': true,
13429
+ style: { position: 'absolute', top: 0, right: 0, borderRadius: 8, padding: '1px 4px', fontSize: 10, background: 'var(--dsw-alias-brand-primary,#3370ff)', color: '#fff', lineHeight: '14px' },
13430
+ }, activeTaskCount > 99 ? '99+' : activeTaskCount) : null))
13431
+
13220
13432
  const rail = React.createElement(
13221
13433
  'div',
13222
13434
  { className: 'drawai-rail', 'aria-label': '元素工具栏' },
@@ -14720,8 +14932,10 @@ window.__ModuleLoader__.load({
14720
14932
  // 4) 工作栏的下拉与「打开/新建/另存为」面板:挂在 root 上(不在画布里),
14721
14933
  // 否则插进标签条之后它们的参照系会算成画布上方的负坐标,被 overflow:hidden 裁掉。
14722
14934
  renderDocMenu(),
14723
- active ? React.createElement(DrawingQueuePanel, { sessionId, path: target, page: pageIndex,
14724
- onApply: applyPendingVideoTrace, onSave: saveNow }) : null,
14935
+ active ? React.createElement(BackgroundTaskPanel, { jobs: backgroundDrawingJobs, exports: videoExportJobs,
14936
+ hidden: !showTaskList, onHide: () => patchPrefs({ showTaskList: false }),
14937
+ path: target, page: pageIndex, error: backgroundQueueError, onCommand: backgroundTaskCommand,
14938
+ onOpen: (job) => job.scope.path === target ? switchPage(job.scope.page) : onOpenTab(job.scope.path, job.scope.page) }) : null,
14725
14939
  videoTraceSource && videoTraceJob && !videoTraceRun.queueId ? React.createElement(VideoTraceStatusCard, { job: videoTraceJob,
14726
14940
  onRetry: () => videoTraceRun.result ? applyPendingVideoTrace() : startVideoTrace(videoTraceJob.options, videoTraceSource), onCancel: () => {
14727
14941
  setVideoTraceJob((job) => Object.assign({}, job, { message: '正在取消并清理临时帧…' }))
@@ -14853,12 +15067,14 @@ window.__ModuleLoader__.load({
14853
15067
  }, [tabPath])
14854
15068
 
14855
15069
  /** 打开一个路径:已经在标签里就切过去,否则新开一个。规则在 openTabIn(可自测)。 */
14856
- function openPath(path) {
15070
+ function openPath(path, page) {
14857
15071
  const next = openTabIn(tabs, path)
14858
15072
  if (next.active === null) return
14859
15073
  bootstrappedRef.current = true
14860
15074
  workspace.bootstrapped = true
14861
- if (next.tabs !== tabs) setTabs(next.tabs)
15075
+ if (Number.isInteger(page) && page >= 0) {
15076
+ setTabs(next.tabs.map((tab) => tab.key === next.active ? { ...tab, pageRequest: { page } } : tab))
15077
+ } else if (next.tabs !== tabs) setTabs(next.tabs)
14862
15078
  setActiveKey(next.active)
14863
15079
  }
14864
15080
 
@@ -14936,6 +15152,7 @@ window.__ModuleLoader__.load({
14936
15152
  React.createElement(CanvasView, {
14937
15153
  sessionId: props.sessionId,
14938
15154
  path: t.path,
15155
+ pageRequest: t.pageRequest,
14939
15156
  active: isActive,
14940
15157
  infoError: '',
14941
15158
  onOpenTab: openPath,