pi-studio 0.9.47 → 0.9.49

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.
@@ -85,6 +85,7 @@
85
85
  const showMeResponseBtn = document.getElementById("showMeResponseBtn");
86
86
  const quizBtn = document.getElementById("quizBtn");
87
87
  const lensSelect = document.getElementById("lensSelect");
88
+ const importFileBtn = document.getElementById("importFileBtn");
88
89
  const fileInput = document.getElementById("fileInput");
89
90
  const resourceDirBtn = document.getElementById("resourceDirBtn");
90
91
  const resourceDirLabel = document.getElementById("resourceDirLabel");
@@ -226,7 +227,10 @@
226
227
  if (
227
228
  !previewResourceHelpers
228
229
  || typeof previewResourceHelpers.areStudioPreviewResourceContextsEqual !== "function"
230
+ || typeof previewResourceHelpers.buildStudioPdfVersionSignature !== "function"
231
+ || typeof previewResourceHelpers.createStudioPdfVersionObservationState !== "function"
229
232
  || typeof previewResourceHelpers.hydrateStudioPreviewLocalImages !== "function"
233
+ || typeof previewResourceHelpers.observeStudioPdfVersion !== "function"
230
234
  ) {
231
235
  throw new Error("Studio preview resource helpers failed to load.");
232
236
  }
@@ -268,10 +272,19 @@
268
272
  let studioPdfFocusFrameEl = null;
269
273
  let studioPdfFocusTitleEl = null;
270
274
  let studioPdfFocusOpenLinkEl = null;
275
+ let studioPdfFocusSystemViewerBtn = null;
276
+ let studioPdfFocusRevealBtn = null;
277
+ let studioPdfFocusAutoRefreshBtn = null;
271
278
  let studioPdfFocusFullscreenBtn = null;
272
279
  let studioPdfFocusCloseBtn = null;
273
280
  let studioPdfFocusLastFocusedEl = null;
274
281
  let studioPdfFocusMovedFrameState = null;
282
+ let studioPdfFocusResourceQuery = null;
283
+ let studioPdfFocusSourceCard = null;
284
+ let studioPdfFocusStandaloneAutoRefreshState = null;
285
+ const studioPdfCardAutoRefreshStates = new WeakMap();
286
+ const STUDIO_PDF_AUTO_REFRESH_INTERVAL_MS = 1_000;
287
+ const STUDIO_PDF_AUTO_REFRESH_STABLE_OBSERVATIONS = 2;
275
288
  let studioHtmlFocusOverlayEl = null;
276
289
  let studioHtmlFocusShellEl = null;
277
290
  let studioHtmlFocusFullscreenBtn = null;
@@ -289,6 +302,16 @@
289
302
  let studioImageFocusLastFocusedEl = null;
290
303
  let studioImageFocusZoomMode = "fit";
291
304
  let studioImageFocusZoom = 1;
305
+ let studioDecisionOverlayEl = null;
306
+ let studioDecisionDialogEl = null;
307
+ let studioDecisionTitleEl = null;
308
+ let studioDecisionMessageEl = null;
309
+ let studioDecisionInputEl = null;
310
+ let studioDecisionCancelBtn = null;
311
+ let studioDecisionSecondaryBtn = null;
312
+ let studioDecisionConfirmBtn = null;
313
+ let studioDecisionState = null;
314
+ let studioImportDecisionOpen = false;
292
315
  let pendingRequestId = null;
293
316
  let pendingKind = null;
294
317
  let stickyStudioKind = null;
@@ -2595,7 +2618,7 @@
2595
2618
  event.preventDefault();
2596
2619
  event.stopPropagation();
2597
2620
  closeStudioUiRefreshMenus();
2598
- resetEditorOrigin();
2621
+ void resetEditorOrigin();
2599
2622
  });
2600
2623
  sourceOpenCurrentFileTabBtn = makeStudioUiRefreshElement("button", "source-open-file-tab-btn", "Open current file in new editor tab");
2601
2624
  sourceOpenCurrentFileTabBtn.type = "button";
@@ -3625,6 +3648,207 @@
3625
3648
  });
3626
3649
  }
3627
3650
 
3651
+ function finishStudioDecision(value, restoreFocus) {
3652
+ const state = studioDecisionState;
3653
+ if (!state) return;
3654
+ studioDecisionState = null;
3655
+ if (studioDecisionOverlayEl) studioDecisionOverlayEl.hidden = true;
3656
+ if (document.body) document.body.classList.remove("studio-decision-open");
3657
+ const returnFocusEl = state.returnFocusEl;
3658
+ state.resolve(value);
3659
+ if (restoreFocus !== false && returnFocusEl && typeof returnFocusEl.focus === "function") {
3660
+ window.setTimeout(() => {
3661
+ if (returnFocusEl.isConnected) returnFocusEl.focus({ preventScroll: true });
3662
+ }, 0);
3663
+ }
3664
+ }
3665
+
3666
+ function getStudioDecisionFocusableElements() {
3667
+ return [studioDecisionInputEl, studioDecisionCancelBtn, studioDecisionSecondaryBtn, studioDecisionConfirmBtn]
3668
+ .filter((element) => element && !element.hidden && !element.disabled);
3669
+ }
3670
+
3671
+ function ensureStudioDecisionDialog() {
3672
+ if (studioDecisionOverlayEl) return studioDecisionOverlayEl;
3673
+
3674
+ const overlay = document.createElement("div");
3675
+ overlay.className = "studio-decision-overlay";
3676
+ overlay.hidden = true;
3677
+
3678
+ const dialog = document.createElement("div");
3679
+ dialog.className = "studio-decision-dialog";
3680
+ dialog.setAttribute("role", "alertdialog");
3681
+ dialog.setAttribute("aria-modal", "true");
3682
+ dialog.setAttribute("aria-labelledby", "studioDecisionTitle");
3683
+ dialog.setAttribute("aria-describedby", "studioDecisionMessage");
3684
+
3685
+ const title = document.createElement("h2");
3686
+ title.id = "studioDecisionTitle";
3687
+ title.className = "studio-decision-title";
3688
+ dialog.appendChild(title);
3689
+
3690
+ const message = document.createElement("div");
3691
+ message.id = "studioDecisionMessage";
3692
+ message.className = "studio-decision-message";
3693
+ dialog.appendChild(message);
3694
+
3695
+ const input = document.createElement("input");
3696
+ input.type = "text";
3697
+ input.className = "studio-decision-input";
3698
+ input.hidden = true;
3699
+ input.autocomplete = "off";
3700
+ input.spellcheck = false;
3701
+ input.setAttribute("autocapitalize", "off");
3702
+ input.setAttribute("aria-label", "Value");
3703
+ dialog.appendChild(input);
3704
+
3705
+ const actions = document.createElement("div");
3706
+ actions.className = "studio-decision-actions";
3707
+
3708
+ const cancelBtn = document.createElement("button");
3709
+ cancelBtn.type = "button";
3710
+ cancelBtn.className = "studio-decision-cancel";
3711
+ cancelBtn.textContent = "Cancel";
3712
+ cancelBtn.addEventListener("click", () => finishStudioDecision(null));
3713
+ actions.appendChild(cancelBtn);
3714
+
3715
+ const secondaryBtn = document.createElement("button");
3716
+ secondaryBtn.type = "button";
3717
+ secondaryBtn.className = "studio-decision-secondary";
3718
+ secondaryBtn.hidden = true;
3719
+ secondaryBtn.addEventListener("click", () => {
3720
+ const handler = studioDecisionState && studioDecisionState.onSecondary;
3721
+ if (typeof handler !== "function") return;
3722
+ try {
3723
+ const result = handler();
3724
+ if (result && typeof result.catch === "function") {
3725
+ result.catch((error) => {
3726
+ setStatus("Action failed: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
3727
+ });
3728
+ }
3729
+ } catch (error) {
3730
+ setStatus("Action failed: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
3731
+ }
3732
+ });
3733
+ actions.appendChild(secondaryBtn);
3734
+
3735
+ const confirmBtn = document.createElement("button");
3736
+ confirmBtn.type = "button";
3737
+ confirmBtn.className = "studio-decision-confirm";
3738
+ confirmBtn.textContent = "Confirm";
3739
+ confirmBtn.addEventListener("click", () => {
3740
+ if (!studioDecisionState) return;
3741
+ finishStudioDecision(studioDecisionState.mode === "prompt" ? input.value : true);
3742
+ });
3743
+ actions.appendChild(confirmBtn);
3744
+
3745
+ dialog.appendChild(actions);
3746
+ overlay.appendChild(dialog);
3747
+ document.body.appendChild(overlay);
3748
+
3749
+ overlay.addEventListener("click", (event) => {
3750
+ if (event.target === overlay) finishStudioDecision(null);
3751
+ });
3752
+ dialog.addEventListener("keydown", (event) => {
3753
+ event.stopPropagation();
3754
+ if (event.key === "Escape") {
3755
+ event.preventDefault();
3756
+ finishStudioDecision(null);
3757
+ return;
3758
+ }
3759
+ if (event.key === "Enter" && event.target === input) {
3760
+ event.preventDefault();
3761
+ confirmBtn.click();
3762
+ return;
3763
+ }
3764
+ if (event.key !== "Tab") return;
3765
+ const focusable = getStudioDecisionFocusableElements();
3766
+ if (focusable.length < 2) return;
3767
+ const currentIndex = focusable.indexOf(document.activeElement);
3768
+ const nextIndex = event.shiftKey
3769
+ ? (currentIndex <= 0 ? focusable.length - 1 : currentIndex - 1)
3770
+ : (currentIndex < 0 || currentIndex === focusable.length - 1 ? 0 : currentIndex + 1);
3771
+ event.preventDefault();
3772
+ focusable[nextIndex].focus();
3773
+ });
3774
+
3775
+ studioDecisionOverlayEl = overlay;
3776
+ studioDecisionDialogEl = dialog;
3777
+ studioDecisionTitleEl = title;
3778
+ studioDecisionMessageEl = message;
3779
+ studioDecisionInputEl = input;
3780
+ studioDecisionCancelBtn = cancelBtn;
3781
+ studioDecisionSecondaryBtn = secondaryBtn;
3782
+ studioDecisionConfirmBtn = confirmBtn;
3783
+ return overlay;
3784
+ }
3785
+
3786
+ function openStudioDecision(options) {
3787
+ const settings = options && typeof options === "object" ? options : {};
3788
+ const mode = settings.mode === "prompt" ? "prompt" : "confirm";
3789
+ ensureStudioDecisionDialog();
3790
+ if (studioDecisionState) finishStudioDecision(null, false);
3791
+ const returnFocusEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
3792
+
3793
+ const secondaryLabel = String(settings.secondaryLabel || "").trim();
3794
+ studioDecisionTitleEl.textContent = String(settings.title || (mode === "prompt" ? "Enter a value" : "Confirm action"));
3795
+ studioDecisionMessageEl.textContent = String(settings.message || "");
3796
+ studioDecisionInputEl.hidden = mode !== "prompt";
3797
+ studioDecisionInputEl.value = mode === "prompt" ? String(settings.defaultValue || "") : "";
3798
+ studioDecisionInputEl.placeholder = mode === "prompt" ? String(settings.placeholder || "") : "";
3799
+ studioDecisionInputEl.setAttribute("aria-label", String(settings.inputLabel || "Value"));
3800
+ studioDecisionCancelBtn.textContent = String(settings.cancelLabel || "Cancel");
3801
+ studioDecisionSecondaryBtn.hidden = !secondaryLabel;
3802
+ studioDecisionSecondaryBtn.disabled = settings.secondaryDisabled === true;
3803
+ studioDecisionSecondaryBtn.textContent = secondaryLabel;
3804
+ studioDecisionConfirmBtn.textContent = String(settings.confirmLabel || (mode === "prompt" ? "Continue" : "Confirm"));
3805
+ studioDecisionConfirmBtn.classList.toggle("is-destructive", settings.destructive === true);
3806
+ studioDecisionDialogEl.classList.toggle("is-destructive", settings.destructive === true);
3807
+ studioDecisionOverlayEl.hidden = false;
3808
+ if (document.body) document.body.classList.add("studio-decision-open");
3809
+
3810
+ return new Promise((resolve) => {
3811
+ const decisionState = {
3812
+ mode,
3813
+ resolve,
3814
+ returnFocusEl,
3815
+ onSecondary: typeof settings.onSecondary === "function" ? settings.onSecondary : null,
3816
+ };
3817
+ studioDecisionState = decisionState;
3818
+ const schedule = typeof window.requestAnimationFrame === "function"
3819
+ ? window.requestAnimationFrame.bind(window)
3820
+ : (callback) => window.setTimeout(callback, 16);
3821
+ schedule(() => {
3822
+ if (studioDecisionState !== decisionState) return;
3823
+ if (mode === "prompt") {
3824
+ studioDecisionInputEl.focus();
3825
+ studioDecisionInputEl.select();
3826
+ } else {
3827
+ studioDecisionCancelBtn.focus();
3828
+ }
3829
+ });
3830
+ });
3831
+ }
3832
+
3833
+ async function requestStudioConfirmation(message, options) {
3834
+ const value = await openStudioDecision({
3835
+ ...(options && typeof options === "object" ? options : {}),
3836
+ mode: "confirm",
3837
+ message: String(message || ""),
3838
+ });
3839
+ return value === true;
3840
+ }
3841
+
3842
+ async function requestStudioTextInput(message, defaultValue, options) {
3843
+ const value = await openStudioDecision({
3844
+ ...(options && typeof options === "object" ? options : {}),
3845
+ mode: "prompt",
3846
+ message: String(message || ""),
3847
+ defaultValue: String(defaultValue || ""),
3848
+ });
3849
+ return typeof value === "string" ? value : null;
3850
+ }
3851
+
3628
3852
  renderStatus();
3629
3853
 
3630
3854
  window.addEventListener("focus", () => {
@@ -3714,14 +3938,17 @@
3714
3938
  }
3715
3939
  }
3716
3940
 
3717
- function resetEditorOrigin() {
3941
+ async function resetEditorOrigin() {
3718
3942
  const descriptor = getCurrentStudioDocumentDescriptor();
3719
3943
  const message = descriptor.fileBacked
3720
3944
  ? ("Reset editor origin and detach the current text from\n\n" + descriptor.label + "\n\ninto a new draft? The file on disk will not be changed, and the current scratchpad/review notes will carry into the new draft.")
3721
3945
  : ("Reset editor origin and start a new independent draft? The current editor text, scratchpad, and review notes will carry into the new draft.");
3722
- if (!window.confirm(message)) {
3723
- return;
3724
- }
3946
+ const confirmed = await requestStudioConfirmation(message, {
3947
+ title: descriptor.fileBacked ? "Detach editor from file?" : "Reset editor origin?",
3948
+ confirmLabel: descriptor.fileBacked ? "Detach" : "Reset origin",
3949
+ destructive: true,
3950
+ });
3951
+ if (!confirmed) return;
3725
3952
  const nextLabel = String(sourceTextEl.value || "").trim() ? "draft" : "blank";
3726
3953
  setSourceState({
3727
3954
  source: "blank",
@@ -4271,6 +4498,15 @@
4271
4498
 
4272
4499
  if (handleStudioImageFocusShortcut(event)) return;
4273
4500
 
4501
+ const otherModalOwnsEvent = scratchpadOwnsEvent
4502
+ || reviewNotesOwnsEvent
4503
+ || outlineOwnsEvent
4504
+ || shortcutsOwnsEvent
4505
+ || htmlFocusOwnsEvent
4506
+ || imageFocusOwnsEvent
4507
+ || quizOwnsEvent;
4508
+ if (!otherModalOwnsEvent && handleStudioPdfRefreshShortcut(event)) return;
4509
+
4274
4510
  if (isScratchpadOpen() && plainEscape) {
4275
4511
  event.preventDefault();
4276
4512
  closeScratchpad();
@@ -6633,7 +6869,7 @@
6633
6869
  }
6634
6870
 
6635
6871
  function parseStudioPdfBlockOptions(body) {
6636
- const options = { path: "", title: "", caption: "", page: "", height: "" };
6872
+ const options = { path: "", title: "", caption: "", page: "", height: "", watch: "" };
6637
6873
  String(body || "").split(/\r?\n/).forEach((line) => {
6638
6874
  const raw = String(line || "").trim();
6639
6875
  if (!raw || raw.startsWith("#")) return;
@@ -6646,6 +6882,7 @@
6646
6882
  else if (key === "caption") options.caption = value;
6647
6883
  else if (key === "page") options.page = value;
6648
6884
  else if (key === "height") options.height = value;
6885
+ else if (key === "watch" || key === "auto-refresh" || key === "autorefresh") options.watch = value;
6649
6886
  return;
6650
6887
  }
6651
6888
  if (!options.path) options.path = stripMatchingQuotes(raw);
@@ -6677,6 +6914,10 @@
6677
6914
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
6678
6915
  }
6679
6916
 
6917
+ function normalizeStudioPdfAutoRefresh(value) {
6918
+ return /^(?:1|true|yes|on|watch)$/i.test(String(value || "").trim());
6919
+ }
6920
+
6680
6921
  function isStudioPdfFocusOpen() {
6681
6922
  return Boolean(studioPdfFocusOverlayEl && studioPdfFocusOverlayEl.hidden === false);
6682
6923
  }
@@ -6723,18 +6964,49 @@
6723
6964
  openLink.className = "studio-pdf-focus-link";
6724
6965
  openLink.target = "_blank";
6725
6966
  openLink.rel = "noopener noreferrer";
6726
- openLink.textContent = "Open PDF";
6967
+ openLink.textContent = "Browser tab";
6968
+ openLink.title = "Open this PDF in a browser tab.";
6727
6969
  actions.appendChild(openLink);
6728
6970
 
6971
+ const systemViewerBtn = document.createElement("button");
6972
+ systemViewerBtn.type = "button";
6973
+ systemViewerBtn.className = "studio-pdf-focus-btn studio-pdf-focus-system-viewer";
6974
+ systemViewerBtn.textContent = "System viewer";
6975
+ systemViewerBtn.title = "Open the local PDF in the operating system's default PDF viewer.";
6976
+ systemViewerBtn.addEventListener("click", () => {
6977
+ void runStudioPdfLocalAction("system-viewer", studioPdfFocusResourceQuery);
6978
+ });
6979
+ actions.appendChild(systemViewerBtn);
6980
+
6981
+ const revealBtn = document.createElement("button");
6982
+ revealBtn.type = "button";
6983
+ revealBtn.className = "studio-pdf-focus-btn studio-pdf-focus-reveal";
6984
+ revealBtn.textContent = "Show in folder";
6985
+ revealBtn.title = "Reveal the local PDF in Finder or the system file manager.";
6986
+ revealBtn.addEventListener("click", () => {
6987
+ void runStudioPdfLocalAction("reveal", studioPdfFocusResourceQuery);
6988
+ });
6989
+ actions.appendChild(revealBtn);
6990
+
6729
6991
  const refreshBtn = document.createElement("button");
6730
6992
  refreshBtn.type = "button";
6731
6993
  refreshBtn.className = "studio-pdf-focus-btn studio-pdf-focus-refresh";
6732
6994
  refreshBtn.textContent = "Refresh";
6733
- refreshBtn.title = "Reload this PDF preview from disk.";
6995
+ refreshBtn.title = "Reload this PDF preview from disk. Shortcut: Cmd/Ctrl+Alt+R.";
6734
6996
  refreshBtn.setAttribute("aria-label", "Refresh PDF preview from disk");
6735
6997
  refreshBtn.addEventListener("click", () => refreshStudioPdfFocusViewer());
6736
6998
  actions.appendChild(refreshBtn);
6737
6999
 
7000
+ const autoRefreshBtn = document.createElement("button");
7001
+ autoRefreshBtn.type = "button";
7002
+ autoRefreshBtn.className = "studio-pdf-focus-btn studio-pdf-focus-auto-refresh";
7003
+ autoRefreshBtn.textContent = "Auto-refresh";
7004
+ autoRefreshBtn.title = "Watch this local PDF and reload it after a changed file is stable on disk.";
7005
+ autoRefreshBtn.setAttribute("aria-label", "Enable PDF auto-refresh");
7006
+ autoRefreshBtn.setAttribute("aria-pressed", "false");
7007
+ autoRefreshBtn.addEventListener("click", () => toggleStudioPdfFocusAutoRefresh());
7008
+ actions.appendChild(autoRefreshBtn);
7009
+
6738
7010
  const fullscreenBtn = document.createElement("button");
6739
7011
  fullscreenBtn.type = "button";
6740
7012
  fullscreenBtn.className = "studio-pdf-focus-btn studio-pdf-focus-fullscreen";
@@ -6789,16 +7061,23 @@
6789
7061
  studioPdfFocusFrameEl = frame;
6790
7062
  studioPdfFocusTitleEl = titleEl;
6791
7063
  studioPdfFocusOpenLinkEl = openLink;
7064
+ studioPdfFocusSystemViewerBtn = systemViewerBtn;
7065
+ studioPdfFocusRevealBtn = revealBtn;
7066
+ studioPdfFocusAutoRefreshBtn = autoRefreshBtn;
6792
7067
  studioPdfFocusFullscreenBtn = fullscreenBtn;
6793
7068
  studioPdfFocusCloseBtn = closeBtn;
6794
7069
  syncStudioPdfFocusFullscreenButton();
7070
+ syncStudioPdfFocusResourceActions();
6795
7071
  return overlay;
6796
7072
  }
6797
7073
 
6798
- function openStudioPdfFocusViewer(viewerUrl, title, sourceFrame) {
7074
+ function openStudioPdfFocusViewer(viewerUrl, title, sourceFrame, resourceQuery, sourceCard) {
6799
7075
  const src = String(viewerUrl || "").trim();
6800
7076
  if (!src) return;
6801
7077
  ensureStudioPdfFocusViewer();
7078
+ studioPdfFocusResourceQuery = normalizeStudioPdfResourceQuery(resourceQuery);
7079
+ setStudioPdfFocusAutoRefreshSource(sourceCard, studioPdfFocusResourceQuery);
7080
+ syncStudioPdfFocusResourceActions();
6802
7081
  studioPdfFocusLastFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
6803
7082
  if (studioPdfFocusTitleEl) studioPdfFocusTitleEl.textContent = String(title || "PDF preview").trim() || "PDF preview";
6804
7083
  if (studioPdfFocusOpenLinkEl) studioPdfFocusOpenLinkEl.href = src;
@@ -6827,6 +7106,9 @@
6827
7106
  restoreStudioPdfFocusMovedFrame();
6828
7107
  if (studioPdfFocusFrameEl) studioPdfFocusFrameEl.src = "about:blank";
6829
7108
  if (document.body) document.body.classList.remove("studio-pdf-focus-open");
7109
+ clearStudioPdfFocusAutoRefreshSource();
7110
+ studioPdfFocusResourceQuery = null;
7111
+ syncStudioPdfFocusResourceActions();
6830
7112
  syncStudioPdfFocusFullscreenButton();
6831
7113
  const focusTarget = studioPdfFocusLastFocusedEl;
6832
7114
  studioPdfFocusLastFocusedEl = null;
@@ -6836,24 +7118,274 @@
6836
7118
  return true;
6837
7119
  }
6838
7120
 
6839
- function buildStudioPdfResourceUrl(options, useEditorResourceContext) {
6840
- const token = getToken();
6841
- if (!token) return "";
7121
+ function buildStudioPdfResourceQuery(options, useEditorResourceContext) {
6842
7122
  const pdfPath = String(options && options.path ? options.path : "").trim();
6843
- if (!pdfPath) return "";
7123
+ if (!pdfPath) return null;
6844
7124
  const explicitSourcePath = options && typeof options.sourcePath === "string" ? options.sourcePath.trim() : "";
6845
7125
  const explicitResourceDir = options && typeof options.resourceDir === "string" ? normalizeStudioResourceDirValue(options.resourceDir) : "";
6846
7126
  const effectivePath = getEffectiveSavePath();
6847
7127
  const sourcePath = explicitSourcePath || (useEditorResourceContext ? (effectivePath || sourceState.path || "") : "");
6848
7128
  const resourceDir = explicitResourceDir || getCurrentResourceDirValue();
6849
- const params = new URLSearchParams({ token, path: pdfPath });
6850
- if (sourcePath) {
6851
- params.set("sourcePath", sourcePath);
7129
+ const query = { path: pdfPath };
7130
+ if (sourcePath) query.sourcePath = sourcePath;
7131
+ if (resourceDir) query.resourceDir = resourceDir;
7132
+ return query;
7133
+ }
7134
+
7135
+ function buildStudioPdfResourceUrl(options, useEditorResourceContext) {
7136
+ const token = getToken();
7137
+ const query = buildStudioPdfResourceQuery(options, useEditorResourceContext);
7138
+ if (!token || !query) return "";
7139
+ return "/pdf-resource?" + new URLSearchParams({ token, ...query }).toString();
7140
+ }
7141
+
7142
+ function normalizeStudioPdfResourceQuery(value) {
7143
+ if (!value || typeof value !== "object") return null;
7144
+ const path = typeof value.path === "string" ? value.path.trim() : "";
7145
+ if (!path) return null;
7146
+ const query = { path };
7147
+ if (typeof value.sourcePath === "string" && value.sourcePath.trim()) query.sourcePath = value.sourcePath.trim();
7148
+ if (typeof value.resourceDir === "string" && value.resourceDir.trim()) query.resourceDir = value.resourceDir.trim();
7149
+ return query;
7150
+ }
7151
+
7152
+ function syncStudioPdfAutoRefreshButton(button, enabled, available) {
7153
+ if (!button) return;
7154
+ const active = Boolean(enabled && available);
7155
+ button.disabled = !available;
7156
+ button.setAttribute("aria-pressed", active ? "true" : "false");
7157
+ button.setAttribute("aria-label", active ? "Disable PDF auto-refresh" : "Enable PDF auto-refresh");
7158
+ button.textContent = "Auto-refresh";
7159
+ button.title = active
7160
+ ? "Auto-refresh is on. Studio checks this PDF while the tab is visible and reloads it after a changed file is stable on disk."
7161
+ : "Watch this local PDF and reload it after a changed file is stable on disk.";
7162
+ }
7163
+
7164
+ function createStudioPdfAutoRefreshState(resourceQuery, hooks) {
7165
+ return {
7166
+ resourceQuery: normalizeStudioPdfResourceQuery(resourceQuery),
7167
+ enabled: false,
7168
+ timer: null,
7169
+ inFlight: false,
7170
+ generation: 0,
7171
+ observation: previewResourceHelpers.createStudioPdfVersionObservationState(),
7172
+ isAlive: hooks && typeof hooks.isAlive === "function" ? hooks.isAlive : () => false,
7173
+ refresh: hooks && typeof hooks.refresh === "function" ? hooks.refresh : () => false,
7174
+ sync: hooks && typeof hooks.sync === "function" ? hooks.sync : () => {},
7175
+ };
7176
+ }
7177
+
7178
+ function resetStudioPdfAutoRefreshObservation(state) {
7179
+ if (!state) return;
7180
+ state.observation = previewResourceHelpers.createStudioPdfVersionObservationState();
7181
+ }
7182
+
7183
+ function scheduleStudioPdfAutoRefreshPoll(state, delayMs) {
7184
+ if (!state || !state.enabled) return;
7185
+ if (state.timer) window.clearTimeout(state.timer);
7186
+ state.timer = window.setTimeout(() => {
7187
+ state.timer = null;
7188
+ void pollStudioPdfAutoRefreshState(state);
7189
+ }, Math.max(0, Number(delayMs) || 0));
7190
+ }
7191
+
7192
+ function stopStudioPdfAutoRefreshState(state) {
7193
+ if (!state) return;
7194
+ state.enabled = false;
7195
+ state.inFlight = false;
7196
+ state.generation += 1;
7197
+ if (state.timer) window.clearTimeout(state.timer);
7198
+ state.timer = null;
7199
+ resetStudioPdfAutoRefreshObservation(state);
7200
+ state.sync();
7201
+ }
7202
+
7203
+ function setStudioPdfAutoRefreshEnabled(state, enabled, options) {
7204
+ if (!state) return false;
7205
+ const nextEnabled = Boolean(enabled && state.resourceQuery);
7206
+ if (state.enabled === nextEnabled) {
7207
+ state.sync();
7208
+ return nextEnabled;
7209
+ }
7210
+ if (state.timer) window.clearTimeout(state.timer);
7211
+ state.timer = null;
7212
+ state.enabled = nextEnabled;
7213
+ state.inFlight = false;
7214
+ state.generation += 1;
7215
+ resetStudioPdfAutoRefreshObservation(state);
7216
+ state.sync();
7217
+ if (nextEnabled) {
7218
+ if (!options || !options.silent) state.refresh();
7219
+ scheduleStudioPdfAutoRefreshPoll(state, 0);
7220
+ }
7221
+ if (!options || !options.silent) {
7222
+ setStatus(nextEnabled ? "PDF auto-refresh on." : "PDF auto-refresh off.", "success");
7223
+ }
7224
+ return nextEnabled;
7225
+ }
7226
+
7227
+ async function fetchStudioPdfVersionSignature(resourceQuery) {
7228
+ const url = buildStudioPdfResourceUrl(resourceQuery, false);
7229
+ if (!url) throw new Error("Could not resolve this PDF for auto-refresh.");
7230
+ const response = await fetchWithTimeout(url, {
7231
+ method: "HEAD",
7232
+ cache: "no-store",
7233
+ }, 4_000, "PDF auto-refresh check");
7234
+ if (!response.ok) throw new Error("PDF auto-refresh check failed with HTTP " + response.status + ".");
7235
+ return previewResourceHelpers.buildStudioPdfVersionSignature(response.headers);
7236
+ }
7237
+
7238
+ async function pollStudioPdfAutoRefreshState(state) {
7239
+ if (!state || !state.enabled || state.inFlight) return;
7240
+ if (!state.isAlive()) {
7241
+ stopStudioPdfAutoRefreshState(state);
7242
+ return;
7243
+ }
7244
+ if (document.hidden || document.visibilityState === "hidden") {
7245
+ scheduleStudioPdfAutoRefreshPoll(state, STUDIO_PDF_AUTO_REFRESH_INTERVAL_MS);
7246
+ return;
6852
7247
  }
6853
- if (resourceDir) {
6854
- params.set("resourceDir", resourceDir);
7248
+ const generation = state.generation;
7249
+ state.inFlight = true;
7250
+ try {
7251
+ const signature = await fetchStudioPdfVersionSignature(state.resourceQuery);
7252
+ if (!state.enabled || state.generation !== generation) return;
7253
+ if (document.hidden || document.visibilityState === "hidden") return;
7254
+ if (!state.isAlive()) {
7255
+ stopStudioPdfAutoRefreshState(state);
7256
+ return;
7257
+ }
7258
+ const observed = previewResourceHelpers.observeStudioPdfVersion(
7259
+ state.observation,
7260
+ signature,
7261
+ STUDIO_PDF_AUTO_REFRESH_STABLE_OBSERVATIONS,
7262
+ );
7263
+ state.observation = observed.state;
7264
+ if (observed.changed) state.refresh();
7265
+ } catch {
7266
+ // LaTeX tools may briefly replace or lock the output PDF. Retry quietly.
7267
+ } finally {
7268
+ if (state.generation !== generation) return;
7269
+ state.inFlight = false;
7270
+ if (state.enabled) scheduleStudioPdfAutoRefreshPoll(state, STUDIO_PDF_AUTO_REFRESH_INTERVAL_MS);
6855
7271
  }
6856
- return "/pdf-resource?" + params.toString();
7272
+ }
7273
+
7274
+ function syncStudioPdfCardAutoRefreshButton(card) {
7275
+ if (!card) return;
7276
+ const state = studioPdfCardAutoRefreshStates.get(card) || null;
7277
+ const button = typeof card.querySelector === "function" ? card.querySelector(".studio-pdf-card-auto-refresh") : null;
7278
+ syncStudioPdfAutoRefreshButton(button, state && state.enabled, Boolean(state && state.resourceQuery));
7279
+ }
7280
+
7281
+ function ensureStudioPdfCardAutoRefreshState(card, resourceQuery) {
7282
+ if (!card) return null;
7283
+ let state = studioPdfCardAutoRefreshStates.get(card) || null;
7284
+ if (state) return state;
7285
+ state = createStudioPdfAutoRefreshState(resourceQuery, {
7286
+ isAlive: () => Boolean(card.isConnected || (isStudioPdfFocusOpen() && studioPdfFocusSourceCard === card)),
7287
+ refresh: () => {
7288
+ if (isStudioPdfFocusOpen() && studioPdfFocusSourceCard === card) {
7289
+ return refreshStudioPdfFocusViewer({ automatic: true });
7290
+ }
7291
+ return refreshStudioPdfCard(card, { automatic: true });
7292
+ },
7293
+ sync: () => {
7294
+ syncStudioPdfCardAutoRefreshButton(card);
7295
+ if (studioPdfFocusSourceCard === card) syncStudioPdfFocusAutoRefreshButton();
7296
+ },
7297
+ });
7298
+ studioPdfCardAutoRefreshStates.set(card, state);
7299
+ syncStudioPdfCardAutoRefreshButton(card);
7300
+ return state;
7301
+ }
7302
+
7303
+ function setStudioPdfCardAutoRefresh(card, enabled, options) {
7304
+ const state = studioPdfCardAutoRefreshStates.get(card) || null;
7305
+ return setStudioPdfAutoRefreshEnabled(state, enabled, options);
7306
+ }
7307
+
7308
+ function getStudioPdfFocusAutoRefreshState() {
7309
+ if (studioPdfFocusSourceCard) return studioPdfCardAutoRefreshStates.get(studioPdfFocusSourceCard) || null;
7310
+ return studioPdfFocusStandaloneAutoRefreshState;
7311
+ }
7312
+
7313
+ function syncStudioPdfFocusAutoRefreshButton() {
7314
+ const state = getStudioPdfFocusAutoRefreshState();
7315
+ syncStudioPdfAutoRefreshButton(
7316
+ studioPdfFocusAutoRefreshBtn,
7317
+ state && state.enabled,
7318
+ Boolean(state && state.resourceQuery),
7319
+ );
7320
+ }
7321
+
7322
+ function clearStudioPdfFocusAutoRefreshSource() {
7323
+ if (studioPdfFocusStandaloneAutoRefreshState) stopStudioPdfAutoRefreshState(studioPdfFocusStandaloneAutoRefreshState);
7324
+ studioPdfFocusStandaloneAutoRefreshState = null;
7325
+ studioPdfFocusSourceCard = null;
7326
+ syncStudioPdfFocusAutoRefreshButton();
7327
+ }
7328
+
7329
+ function setStudioPdfFocusAutoRefreshSource(sourceCard, resourceQuery) {
7330
+ clearStudioPdfFocusAutoRefreshSource();
7331
+ const card = sourceCard instanceof Element ? sourceCard : null;
7332
+ if (card) {
7333
+ studioPdfFocusSourceCard = card;
7334
+ ensureStudioPdfCardAutoRefreshState(card, resourceQuery);
7335
+ syncStudioPdfFocusAutoRefreshButton();
7336
+ return;
7337
+ }
7338
+ const query = normalizeStudioPdfResourceQuery(resourceQuery);
7339
+ if (!query) {
7340
+ syncStudioPdfFocusAutoRefreshButton();
7341
+ return;
7342
+ }
7343
+ studioPdfFocusStandaloneAutoRefreshState = createStudioPdfAutoRefreshState(query, {
7344
+ isAlive: () => Boolean(isStudioPdfFocusOpen() && !studioPdfFocusSourceCard),
7345
+ refresh: () => refreshStudioPdfFocusViewer({ automatic: true }),
7346
+ sync: () => syncStudioPdfFocusAutoRefreshButton(),
7347
+ });
7348
+ syncStudioPdfFocusAutoRefreshButton();
7349
+ }
7350
+
7351
+ function toggleStudioPdfFocusAutoRefresh() {
7352
+ const state = getStudioPdfFocusAutoRefreshState();
7353
+ if (!state) {
7354
+ setStatus("Could not resolve this PDF for auto-refresh.", "warning");
7355
+ return false;
7356
+ }
7357
+ return setStudioPdfAutoRefreshEnabled(state, !state.enabled);
7358
+ }
7359
+
7360
+ async function runStudioPdfLocalAction(action, resourceQuery) {
7361
+ const query = normalizeStudioPdfResourceQuery(resourceQuery);
7362
+ if (!query) {
7363
+ setStatus("Could not resolve this PDF's local path.", "warning");
7364
+ return false;
7365
+ }
7366
+ const openInSystemViewer = action === "system-viewer";
7367
+ const endpoint = openInSystemViewer ? "/open-local-resource" : "/reveal-local-resource";
7368
+ try {
7369
+ const payload = await fetchStudioJson(endpoint, {
7370
+ method: "POST",
7371
+ body: JSON.stringify(query),
7372
+ });
7373
+ setStatus(payload && payload.message
7374
+ ? payload.message
7375
+ : (openInSystemViewer ? "Opened PDF in the system viewer." : "Revealed PDF in the file manager."), "success");
7376
+ return true;
7377
+ } catch (error) {
7378
+ setStatus((error && error.message)
7379
+ ? error.message
7380
+ : (openInSystemViewer ? "Could not open PDF in the system viewer." : "Could not reveal PDF in the file manager."), "warning");
7381
+ return false;
7382
+ }
7383
+ }
7384
+
7385
+ function syncStudioPdfFocusResourceActions() {
7386
+ const available = Boolean(normalizeStudioPdfResourceQuery(studioPdfFocusResourceQuery));
7387
+ if (studioPdfFocusSystemViewerBtn) studioPdfFocusSystemViewerBtn.disabled = !available;
7388
+ if (studioPdfFocusRevealBtn) studioPdfFocusRevealBtn.disabled = !available;
6857
7389
  }
6858
7390
 
6859
7391
  function buildRefreshedStudioPdfViewerUrl(value) {
@@ -6886,7 +7418,7 @@
6886
7418
  if (focusBtn && focusBtn.dataset) focusBtn.dataset.studioPdfViewerUrl = nextUrl;
6887
7419
  }
6888
7420
 
6889
- function refreshStudioPdfCard(card) {
7421
+ function refreshStudioPdfCard(card, options) {
6890
7422
  if (!card) return false;
6891
7423
  const frame = typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
6892
7424
  const currentUrl = String(card.dataset && card.dataset.studioPdfViewerUrl ? card.dataset.studioPdfViewerUrl : "").trim()
@@ -6894,7 +7426,14 @@
6894
7426
  const nextUrl = buildRefreshedStudioPdfViewerUrl(currentUrl);
6895
7427
  if (!nextUrl) return false;
6896
7428
  syncStudioPdfCardViewerUrl(card, nextUrl);
6897
- setStatus("Refreshed PDF preview from disk.", "success");
7429
+ if (!options || !options.automatic) {
7430
+ resetStudioPdfAutoRefreshObservation(studioPdfCardAutoRefreshStates.get(card) || null);
7431
+ }
7432
+ if (!options || !options.silent) {
7433
+ setStatus(options && options.automatic
7434
+ ? "PDF changed on disk; refreshed preview."
7435
+ : "Refreshed PDF preview from disk.", "success");
7436
+ }
6898
7437
  return true;
6899
7438
  }
6900
7439
 
@@ -6905,7 +7444,7 @@
6905
7444
  return studioPdfFocusFrameEl;
6906
7445
  }
6907
7446
 
6908
- function refreshStudioPdfFocusViewer() {
7447
+ function refreshStudioPdfFocusViewer(options) {
6909
7448
  const frame = getStudioPdfFocusActiveFrame();
6910
7449
  const currentUrl = String(frame && frame.src ? frame.src : "").trim()
6911
7450
  || String(studioPdfFocusOpenLinkEl && studioPdfFocusOpenLinkEl.href ? studioPdfFocusOpenLinkEl.href : "").trim();
@@ -6916,7 +7455,58 @@
6916
7455
  }
6917
7456
  if (frame) frame.src = nextUrl;
6918
7457
  if (studioPdfFocusOpenLinkEl) studioPdfFocusOpenLinkEl.href = nextUrl;
6919
- setStatus("Refreshed PDF preview from disk.", "success");
7458
+ if (studioPdfFocusSourceCard) syncStudioPdfCardViewerUrl(studioPdfFocusSourceCard, nextUrl);
7459
+ if (!options || !options.automatic) {
7460
+ resetStudioPdfAutoRefreshObservation(getStudioPdfFocusAutoRefreshState());
7461
+ }
7462
+ if (!options || !options.silent) {
7463
+ setStatus(options && options.automatic
7464
+ ? "PDF changed on disk; refreshed preview."
7465
+ : "Refreshed PDF preview from disk.", "success");
7466
+ }
7467
+ return true;
7468
+ }
7469
+
7470
+ function getVisibleStudioPdfCards() {
7471
+ return Array.from(document.querySelectorAll(".studio-pdf-card")).filter((card) => {
7472
+ if (!card || !card.isConnected || card.hidden) return false;
7473
+ if (typeof card.getClientRects === "function" && card.getClientRects().length === 0) return false;
7474
+ try {
7475
+ const style = window.getComputedStyle(card);
7476
+ return style.display !== "none" && style.visibility !== "hidden";
7477
+ } catch {
7478
+ return true;
7479
+ }
7480
+ });
7481
+ }
7482
+
7483
+ function refreshVisibleStudioPdfPreviews() {
7484
+ if (isStudioPdfFocusOpen()) return refreshStudioPdfFocusViewer();
7485
+ const cards = getVisibleStudioPdfCards();
7486
+ let refreshed = 0;
7487
+ cards.forEach((card) => {
7488
+ if (refreshStudioPdfCard(card, { silent: true })) refreshed += 1;
7489
+ });
7490
+ if (refreshed > 0) {
7491
+ setStatus(refreshed === 1
7492
+ ? "Refreshed PDF preview from disk."
7493
+ : ("Refreshed " + refreshed + " PDF previews from disk."), "success");
7494
+ return true;
7495
+ }
7496
+ return false;
7497
+ }
7498
+
7499
+ function handleStudioPdfRefreshShortcut(event) {
7500
+ if (!event) return false;
7501
+ const key = typeof event.key === "string" ? event.key.toLowerCase() : "";
7502
+ const code = typeof event.code === "string" ? event.code : "";
7503
+ const matches = (key === "r" || code === "KeyR")
7504
+ && (event.metaKey || event.ctrlKey)
7505
+ && event.altKey
7506
+ && !event.shiftKey;
7507
+ if (!matches) return false;
7508
+ if (!refreshVisibleStudioPdfPreviews()) return false;
7509
+ event.preventDefault();
6920
7510
  return true;
6921
7511
  }
6922
7512
 
@@ -6989,8 +7579,15 @@
6989
7579
  || String(card && card.dataset ? (card.dataset.studioPdfTitle || "") : "").trim()
6990
7580
  || "PDF preview";
6991
7581
  const sourceFrame = card && typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
7582
+ const resourceQuery = card && card.dataset
7583
+ ? normalizeStudioPdfResourceQuery({
7584
+ path: card.dataset.studioPdfPath || "",
7585
+ sourcePath: card.dataset.studioPdfSourcePath || "",
7586
+ resourceDir: card.dataset.studioPdfResourceDir || "",
7587
+ })
7588
+ : null;
6992
7589
  if (!viewerUrl) return false;
6993
- openStudioPdfFocusViewer(viewerUrl, title, sourceFrame);
7590
+ openStudioPdfFocusViewer(viewerUrl, title, sourceFrame, resourceQuery, card);
6994
7591
  return true;
6995
7592
  }
6996
7593
 
@@ -7403,6 +8000,8 @@
7403
8000
  const caption = String(options.caption || "").trim();
7404
8001
  const height = normalizeStudioPdfHeight(options.height);
7405
8002
  const page = normalizeStudioPdfPage(options.page);
8003
+ const autoRefreshRequested = normalizeStudioPdfAutoRefresh(options.watch);
8004
+ const resourceQuery = buildStudioPdfResourceQuery(options, useEditorResourceContext);
7406
8005
  const resourceUrl = buildStudioPdfResourceUrl(options, useEditorResourceContext);
7407
8006
  const viewerUrl = resourceUrl && page ? resourceUrl + "#page=" + encodeURIComponent(String(page)) : resourceUrl;
7408
8007
 
@@ -7411,6 +8010,9 @@
7411
8010
  if (card.dataset) {
7412
8011
  card.dataset.studioPdfViewerUrl = viewerUrl || "";
7413
8012
  card.dataset.studioPdfTitle = title;
8013
+ card.dataset.studioPdfPath = resourceQuery && resourceQuery.path ? resourceQuery.path : "";
8014
+ card.dataset.studioPdfSourcePath = resourceQuery && resourceQuery.sourcePath ? resourceQuery.sourcePath : "";
8015
+ card.dataset.studioPdfResourceDir = resourceQuery && resourceQuery.resourceDir ? resourceQuery.resourceDir : "";
7414
8016
  }
7415
8017
 
7416
8018
  const header = document.createElement("figcaption");
@@ -7447,14 +8049,39 @@
7447
8049
  openLink.href = viewerUrl;
7448
8050
  openLink.target = "_blank";
7449
8051
  openLink.rel = "noopener noreferrer";
7450
- openLink.textContent = "Open PDF";
8052
+ openLink.textContent = "Browser tab";
8053
+ openLink.title = "Open this PDF in a browser tab.";
7451
8054
  actions.appendChild(openLink);
7452
8055
 
8056
+ const systemViewerBtn = document.createElement("button");
8057
+ systemViewerBtn.type = "button";
8058
+ systemViewerBtn.className = "studio-pdf-card-action studio-pdf-card-system-viewer";
8059
+ systemViewerBtn.textContent = "System viewer";
8060
+ systemViewerBtn.title = "Open the local PDF in the operating system's default PDF viewer.";
8061
+ systemViewerBtn.addEventListener("click", (event) => {
8062
+ event.preventDefault();
8063
+ event.stopPropagation();
8064
+ void runStudioPdfLocalAction("system-viewer", resourceQuery);
8065
+ });
8066
+ actions.appendChild(systemViewerBtn);
8067
+
8068
+ const revealBtn = document.createElement("button");
8069
+ revealBtn.type = "button";
8070
+ revealBtn.className = "studio-pdf-card-action studio-pdf-card-reveal";
8071
+ revealBtn.textContent = "Show in folder";
8072
+ revealBtn.title = "Reveal the local PDF in Finder or the system file manager.";
8073
+ revealBtn.addEventListener("click", (event) => {
8074
+ event.preventDefault();
8075
+ event.stopPropagation();
8076
+ void runStudioPdfLocalAction("reveal", resourceQuery);
8077
+ });
8078
+ actions.appendChild(revealBtn);
8079
+
7453
8080
  const refreshBtn = document.createElement("button");
7454
8081
  refreshBtn.type = "button";
7455
8082
  refreshBtn.className = "studio-pdf-card-action studio-pdf-card-refresh";
7456
8083
  refreshBtn.textContent = "Refresh";
7457
- refreshBtn.title = "Reload this PDF preview from disk.";
8084
+ refreshBtn.title = "Reload this PDF preview from disk. Shortcut: Cmd/Ctrl+Alt+R.";
7458
8085
  refreshBtn.addEventListener("click", (event) => {
7459
8086
  event.preventDefault();
7460
8087
  event.stopPropagation();
@@ -7462,6 +8089,25 @@
7462
8089
  });
7463
8090
  actions.appendChild(refreshBtn);
7464
8091
 
8092
+ const autoRefreshBtn = document.createElement("button");
8093
+ autoRefreshBtn.type = "button";
8094
+ autoRefreshBtn.className = "studio-pdf-card-action studio-pdf-card-auto-refresh";
8095
+ autoRefreshBtn.textContent = "Auto-refresh";
8096
+ autoRefreshBtn.title = "Watch this local PDF and reload it after a changed file is stable on disk.";
8097
+ autoRefreshBtn.setAttribute("aria-label", "Enable PDF auto-refresh");
8098
+ autoRefreshBtn.setAttribute("aria-pressed", "false");
8099
+ autoRefreshBtn.addEventListener("click", (event) => {
8100
+ event.preventDefault();
8101
+ event.stopPropagation();
8102
+ const state = ensureStudioPdfCardAutoRefreshState(card, resourceQuery);
8103
+ if (!state) {
8104
+ setStatus("Could not resolve this PDF for auto-refresh.", "warning");
8105
+ return;
8106
+ }
8107
+ setStudioPdfCardAutoRefresh(card, !state.enabled);
8108
+ });
8109
+ actions.appendChild(autoRefreshBtn);
8110
+
7465
8111
  header.appendChild(actions);
7466
8112
  }
7467
8113
  card.appendChild(header);
@@ -7488,6 +8134,12 @@
7488
8134
  iframe.loading = "lazy";
7489
8135
  iframe.style.height = height + "px";
7490
8136
  card.appendChild(iframe);
8137
+ const autoRefreshState = ensureStudioPdfCardAutoRefreshState(card, resourceQuery);
8138
+ if (autoRefreshRequested) {
8139
+ setStudioPdfAutoRefreshEnabled(autoRefreshState, true, { silent: true });
8140
+ } else {
8141
+ syncStudioPdfCardAutoRefreshButton(card);
8142
+ }
7491
8143
  return card;
7492
8144
  }
7493
8145
 
@@ -8777,7 +9429,13 @@
8777
9429
  } else {
8778
9430
  failPendingStudioTab(studioLaunch, "Studio did not return a preview-tab URL for this PDF export.");
8779
9431
  const viewerUrl = getStudioPdfViewerUrlForExportPayload(payload);
8780
- if (viewerUrl) openStudioPdfFocusViewer(viewerUrl, downloadName);
9432
+ const resourceQuery = exportPath
9433
+ ? buildStudioPdfResourceQuery({
9434
+ path: exportPath,
9435
+ resourceDir: exportPath.split(/[\\/]/).slice(0, -1).join("/"),
9436
+ }, false)
9437
+ : null;
9438
+ if (viewerUrl) openStudioPdfFocusViewer(viewerUrl, downloadName, null, resourceQuery);
8781
9439
  }
8782
9440
  if (writeError) {
8783
9441
  setStatus(openedStudio
@@ -11399,6 +12057,7 @@
11399
12057
  const canRefreshFromDisk = hasRefreshableFilePath();
11400
12058
 
11401
12059
  fileInput.disabled = uiBusy;
12060
+ if (importFileBtn) importFileBtn.disabled = uiBusy;
11402
12061
  if (sourceBadgeEl) sourceBadgeEl.disabled = uiBusy;
11403
12062
  if (sourceResetOriginBtn) sourceResetOriginBtn.disabled = uiBusy;
11404
12063
  if (sourceOpenCurrentFileTabBtn) {
@@ -11714,12 +12373,15 @@
11714
12373
  return true;
11715
12374
  }
11716
12375
 
11717
- function clearStudioWorkspace() {
12376
+ async function clearStudioWorkspace() {
11718
12377
  if (uiBusy) {
11719
12378
  setStatus("Studio is busy.", "warning");
11720
12379
  return;
11721
12380
  }
11722
- const confirmed = window.confirm("Reset the editor to a fresh blank draft in this browser tab? Saved files and responses are not changed.");
12381
+ const confirmed = await requestStudioConfirmation(
12382
+ "Reset the editor to a fresh blank draft in this browser tab? Saved files and responses are not changed.",
12383
+ { title: "Reset editor?", confirmLabel: "Reset editor", destructive: true },
12384
+ );
11723
12385
  if (!confirmed) return;
11724
12386
  const preservedResponseState = {
11725
12387
  responseHistory: Array.isArray(responseHistory) ? responseHistory.slice() : [],
@@ -12863,7 +13525,7 @@
12863
13525
  if (!raw || raw.charAt(0) === "#") return false;
12864
13526
  if (/^\/\//.test(raw)) return false;
12865
13527
  if (/^(?:https?|mailto|tel|data|blob|javascript|about):/i.test(raw)) return false;
12866
- if (/^\/(?:pdf-resource|html-preview-resource|export-pdf|export-html|render-preview|render-math|local-preview-link|reveal-local-resource)(?:[?#/]|$)/i.test(raw)) return false;
13528
+ if (/^\/(?:pdf-resource|html-preview-resource|export-pdf|export-html|render-preview|render-math|import-file-copy|local-preview-link|reveal-local-resource|open-local-resource)(?:[?#/]|$)/i.test(raw)) return false;
12867
13529
  return true;
12868
13530
  }
12869
13531
 
@@ -12949,6 +13611,7 @@
12949
13611
  if (kind === "pdf") {
12950
13612
  appendPreviewLinkMenuButton(menu, "Open PDF preview", "open-pdf");
12951
13613
  appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
13614
+ appendPreviewLinkMenuButton(menu, "Open in system viewer", "open-system");
12952
13615
  } else if (kind === "text") {
12953
13616
  appendPreviewLinkMenuButton(menu, "Open file tab", "open-new");
12954
13617
  appendPreviewLinkMenuButton(menu, "Open here", "open-here");
@@ -12989,7 +13652,14 @@
12989
13652
  setStatus("Could not resolve this PDF link. Open the source file or set a working directory first.", "warning");
12990
13653
  return false;
12991
13654
  }
12992
- openStudioPdfFocusViewer(viewerUrl, title || href);
13655
+ const cleanPath = stripPreviewLocalLinkUrlSuffix(href);
13656
+ const context = contextOverride && typeof contextOverride === "object" ? contextOverride : {};
13657
+ const resourceQuery = buildStudioPdfResourceQuery({
13658
+ path: cleanPath,
13659
+ sourcePath: context.sourcePath || "",
13660
+ resourceDir: context.resourceDir || "",
13661
+ }, true);
13662
+ openStudioPdfFocusViewer(viewerUrl, title || href, null, resourceQuery);
12993
13663
  return true;
12994
13664
  }
12995
13665
 
@@ -13022,16 +13692,17 @@
13022
13692
  }
13023
13693
  }
13024
13694
 
13025
- function confirmPreviewOfficeConversion(href, destination) {
13695
+ async function confirmPreviewOfficeConversion(href, destination) {
13026
13696
  if (getPreviewLocalLinkKind(href) !== "office") return true;
13027
13697
  const label = getPreviewOfficeConversionLabel(href);
13028
13698
  const target = destination === "here"
13029
13699
  ? "replace the current editor contents with an editable Markdown copy"
13030
13700
  : "open an editable Markdown copy in a new Studio tab";
13031
- const confirmed = window.confirm(
13701
+ const confirmed = await requestStudioConfirmation(
13032
13702
  "Convert " + label + " to Markdown?\n\n"
13033
13703
  + "Studio will use Pandoc to " + target + ". Some layout or formatting may change. "
13034
- + "The original DOCX/ODT file will not be overwritten, and edits will not round-trip back to it."
13704
+ + "The original DOCX/ODT file will not be overwritten, and edits will not round-trip back to it.",
13705
+ { title: "Convert document?", confirmLabel: "Convert" },
13035
13706
  );
13036
13707
  if (!confirmed) setStatus("Document conversion cancelled.", "warning");
13037
13708
  return confirmed;
@@ -13043,13 +13714,17 @@
13043
13714
  }
13044
13715
 
13045
13716
  async function openPreviewDocumentHere(href, contextOverride, options) {
13046
- if (!confirmPreviewOfficeConversion(href, "here")) return;
13717
+ if (!(await confirmPreviewOfficeConversion(href, "here"))) return;
13047
13718
  if (editorHasPotentialUnsavedContent()) {
13048
13719
  const kind = getPreviewLocalLinkKind(href);
13049
13720
  const prompt = kind === "office"
13050
13721
  ? "Replace the current editor contents with this converted Markdown copy? Unsaved editor changes may be lost."
13051
13722
  : "Open this file-backed document in the current editor?\n\nThis will replace the current editor contents and attach the editor to the file on disk, so Save editor and Refresh from disk use that file. Unsaved editor changes may be lost.";
13052
- const confirmed = window.confirm(prompt);
13723
+ const confirmed = await requestStudioConfirmation(prompt, {
13724
+ title: "Replace editor contents?",
13725
+ confirmLabel: "Replace",
13726
+ destructive: true,
13727
+ });
13053
13728
  if (!confirmed) return;
13054
13729
  }
13055
13730
  const payload = await fetchPreviewLocalLink("document", href, contextOverride);
@@ -13080,7 +13755,7 @@
13080
13755
  }
13081
13756
 
13082
13757
  async function openPreviewDocumentInNewEditor(href, contextOverride) {
13083
- if (!confirmPreviewOfficeConversion(href, "new")) return;
13758
+ if (!(await confirmPreviewOfficeConversion(href, "new"))) return;
13084
13759
  let launch = null;
13085
13760
  try {
13086
13761
  launch = openPendingStudioTab("document");
@@ -13136,6 +13811,10 @@
13136
13811
  openPreviewPdfLink(href, context.title || href, context);
13137
13812
  return;
13138
13813
  }
13814
+ if (action === "open-system") {
13815
+ await runStudioPdfLocalAction("system-viewer", getPreviewLinkResourceQuery(href, context));
13816
+ return;
13817
+ }
13139
13818
  if (action === "open-new") {
13140
13819
  await openPreviewDocumentInNewEditor(href, context);
13141
13820
  return;
@@ -14824,7 +15503,10 @@
14824
15503
  return;
14825
15504
  }
14826
15505
  if (String(scratchpadText || "").trim() && String(scratchpadText || "") !== String(text || "")) {
14827
- const confirmed = window.confirm("Replace the current scratchpad with this recent scratchpad? Current scratchpad text will remain saved under its current document/draft identity, but this panel will show the loaded text for the current document.");
15506
+ const confirmed = await requestStudioConfirmation(
15507
+ "Replace the current scratchpad with this recent scratchpad? Current scratchpad text will remain saved under its current document/draft identity, but this panel will show the loaded text for the current document.",
15508
+ { title: "Replace scratchpad?", confirmLabel: "Replace", destructive: true },
15509
+ );
14828
15510
  if (!confirmed) return;
14829
15511
  }
14830
15512
  setScratchpadText(text);
@@ -18646,7 +19328,7 @@
18646
19328
  deleteBtn.textContent = "Delete";
18647
19329
  deleteBtn.title = "Delete this local comment.";
18648
19330
  deleteBtn.addEventListener("click", () => {
18649
- deleteReviewNote(note.id);
19331
+ void deleteReviewNote(note.id);
18650
19332
  });
18651
19333
  actions.appendChild(deleteBtn);
18652
19334
 
@@ -19010,24 +19692,29 @@
19010
19692
  });
19011
19693
  }
19012
19694
 
19013
- function deleteReviewNote(noteId) {
19695
+ async function deleteReviewNote(noteId) {
19014
19696
  const note = reviewNotes.find((entry) => entry && entry.id === noteId);
19015
19697
  if (!note) return;
19016
- const confirmed = window.confirm("Delete this local comment?");
19698
+ const confirmed = await requestStudioConfirmation("Delete this local comment?", {
19699
+ title: "Delete comment?",
19700
+ confirmLabel: "Delete",
19701
+ destructive: true,
19702
+ });
19017
19703
  if (!confirmed) return;
19018
19704
  setReviewNotes(reviewNotes.filter((entry) => entry && entry.id !== noteId));
19019
19705
  setStatus("Deleted local comment.", "success");
19020
19706
  }
19021
19707
 
19022
- function deleteAllReviewNotes() {
19708
+ async function deleteAllReviewNotes() {
19023
19709
  if (!reviewNotes.length) {
19024
19710
  setStatus("No local comments to delete.", "warning");
19025
19711
  return;
19026
19712
  }
19027
19713
  const count = reviewNotes.length;
19028
- const confirmed = window.confirm(
19714
+ const confirmed = await requestStudioConfirmation(
19029
19715
  "Delete all " + count + " local comment" + (count === 1 ? "" : "s") + " for this document?\n\n"
19030
19716
  + "Existing inline [an: ...] annotations in the editor text will not be removed.",
19717
+ { title: "Delete all comments?", confirmLabel: "Delete all", destructive: true },
19031
19718
  );
19032
19719
  if (!confirmed) return;
19033
19720
  setReviewNotes([]);
@@ -21238,7 +21925,7 @@
21238
21925
  });
21239
21926
  }
21240
21927
 
21241
- function loadSelectedResponseIntoEditor(options) {
21928
+ async function loadSelectedResponseIntoEditor(options) {
21242
21929
  if (!latestResponseMarkdown.trim()) {
21243
21930
  setStatus("No response available yet.", "warning");
21244
21931
  return false;
@@ -21249,12 +21936,15 @@
21249
21936
  && sourceState.source === "last-response"
21250
21937
  && Boolean(currentEditorText.trim())
21251
21938
  && normalizeForCompare(currentEditorText) !== latestResponseNormalized;
21252
- if (
21253
- replacingEditedResponse
21254
- && !window.confirm("Replace your edited response with a fresh copy? Existing edits and annotations will be lost.")
21255
- ) {
21256
- setStatus("Kept the current editor text.");
21257
- return false;
21939
+ if (replacingEditedResponse) {
21940
+ const confirmed = await requestStudioConfirmation(
21941
+ "Replace your edited response with a fresh copy? Existing edits and annotations will be lost.",
21942
+ { title: "Replace edited response?", confirmLabel: "Replace", destructive: true },
21943
+ );
21944
+ if (!confirmed) {
21945
+ setStatus("Kept the current editor text.");
21946
+ return false;
21947
+ }
21258
21948
  }
21259
21949
  setEditorText(latestResponseMarkdown, { preserveScroll: false, preserveSelection: false });
21260
21950
  setSourceState({ source: "last-response", label: "last model response", path: null });
@@ -21272,11 +21962,11 @@
21272
21962
  }
21273
21963
 
21274
21964
  loadResponseBtn.addEventListener("click", () => {
21275
- loadSelectedResponseIntoEditor();
21965
+ void loadSelectedResponseIntoEditor();
21276
21966
  });
21277
21967
 
21278
21968
  annotateResponseBtn.addEventListener("click", () => {
21279
- loadSelectedResponseIntoEditor({ annotate: true });
21969
+ void loadSelectedResponseIntoEditor({ annotate: true });
21280
21970
  });
21281
21971
 
21282
21972
  loadCritiqueNotesBtn.addEventListener("click", () => {
@@ -21411,7 +22101,7 @@
21411
22101
  setFooterThemeMenuOpen(false);
21412
22102
  });
21413
22103
 
21414
- saveAsBtn.addEventListener("click", () => {
22104
+ saveAsBtn.addEventListener("click", async () => {
21415
22105
  const content = sourceTextEl.value;
21416
22106
  if (!content.trim()) {
21417
22107
  setStatus("Editor is empty. Nothing to save.", "warning");
@@ -21421,7 +22111,10 @@
21421
22111
  var suggestedName = sourceState.label ? stripImportedFileLabel(sourceState.label) : "draft.md";
21422
22112
  var suggestedDir = getCurrentResourceDirValue() ? getCurrentResourceDirValue().replace(/\/$/, "") + "/" : "./";
21423
22113
  const suggested = sourceState.path || (suggestedDir + suggestedName);
21424
- const path = window.prompt("Save editor content as:", suggested);
22114
+ const path = await requestStudioTextInput("Save editor content as:", suggested, {
22115
+ title: "Save editor as",
22116
+ confirmLabel: "Save",
22117
+ });
21425
22118
  if (!path) return;
21426
22119
 
21427
22120
  const requestId = beginUiAction("save_as");
@@ -21441,16 +22134,19 @@
21441
22134
  }
21442
22135
  });
21443
22136
 
21444
- saveOverBtn.addEventListener("click", () => {
22137
+ saveOverBtn.addEventListener("click", async () => {
21445
22138
  var effectivePath = getEffectiveSavePath();
21446
22139
  if (!effectivePath) {
21447
22140
  setStatus("Save editor requires a file path. Open via /studio <path>, set a working dir, or use Save editor as…", "warning");
21448
22141
  return;
21449
22142
  }
21450
22143
 
21451
- if (!window.confirm("Overwrite " + effectivePath + "?")) {
21452
- return;
21453
- }
22144
+ const confirmed = await requestStudioConfirmation("Overwrite " + effectivePath + "?", {
22145
+ title: "Overwrite file?",
22146
+ confirmLabel: "Overwrite",
22147
+ destructive: true,
22148
+ });
22149
+ if (!confirmed) return;
21454
22150
 
21455
22151
  const requestId = beginUiAction("save_over");
21456
22152
  if (!requestId) return;
@@ -21471,14 +22167,17 @@
21471
22167
  });
21472
22168
 
21473
22169
  if (refreshFromDiskBtn) {
21474
- refreshFromDiskBtn.addEventListener("click", () => {
22170
+ refreshFromDiskBtn.addEventListener("click", async () => {
21475
22171
  if (!hasRefreshableFilePath()) {
21476
22172
  setStatus("Refresh from disk needs a file path. Use Files → Open here, Files → Open file tab, or /studio-editor-only <path> for a refreshable editor tab.", "warning");
21477
22173
  return;
21478
22174
  }
21479
22175
 
21480
22176
  if (editorDiffersFromFileBackedBaseline()) {
21481
- const confirmed = window.confirm("Replace current editor contents with the latest version from disk?");
22177
+ const confirmed = await requestStudioConfirmation(
22178
+ "Replace current editor contents with the latest version from disk?",
22179
+ { title: "Refresh from disk?", confirmLabel: "Replace", destructive: true },
22180
+ );
21482
22181
  if (!confirmed) return;
21483
22182
  }
21484
22183
 
@@ -21501,7 +22200,7 @@
21501
22200
 
21502
22201
  if (clearWorkspaceBtn) {
21503
22202
  clearWorkspaceBtn.addEventListener("click", () => {
21504
- clearStudioWorkspace();
22203
+ void clearStudioWorkspace();
21505
22204
  });
21506
22205
  }
21507
22206
 
@@ -21764,7 +22463,7 @@
21764
22463
 
21765
22464
  if (reviewNotesDeleteAllBtn) {
21766
22465
  reviewNotesDeleteAllBtn.addEventListener("click", () => {
21767
- deleteAllReviewNotes();
22466
+ void deleteAllReviewNotes();
21768
22467
  });
21769
22468
  }
21770
22469
 
@@ -21966,9 +22665,13 @@
21966
22665
  }
21967
22666
 
21968
22667
  if (scratchpadClearBtn) {
21969
- scratchpadClearBtn.addEventListener("click", () => {
22668
+ scratchpadClearBtn.addEventListener("click", async () => {
21970
22669
  if (!String(scratchpadText || "").length) return;
21971
- const confirmed = window.confirm("Clear scratchpad text?");
22670
+ const confirmed = await requestStudioConfirmation("Clear scratchpad text?", {
22671
+ title: "Clear scratchpad?",
22672
+ confirmLabel: "Clear",
22673
+ destructive: true,
22674
+ });
21972
22675
  if (!confirmed) return;
21973
22676
  setScratchpadText("");
21974
22677
  if (scratchpadTextEl) scratchpadTextEl.focus();
@@ -21977,7 +22680,7 @@
21977
22680
  }
21978
22681
 
21979
22682
  if (saveAnnotatedBtn) {
21980
- saveAnnotatedBtn.addEventListener("click", () => {
22683
+ saveAnnotatedBtn.addEventListener("click", async () => {
21981
22684
  const content = sourceTextEl.value;
21982
22685
  if (!content.trim()) {
21983
22686
  setStatus("Editor is empty. Nothing to save.", "warning");
@@ -21985,7 +22688,10 @@
21985
22688
  }
21986
22689
 
21987
22690
  const suggested = buildAnnotatedSaveSuggestion();
21988
- const path = window.prompt("Save annotated editor content as:", suggested);
22691
+ const path = await requestStudioTextInput("Save annotated editor content as:", suggested, {
22692
+ title: "Save annotated editor as",
22693
+ confirmLabel: "Save",
22694
+ });
21989
22695
  if (!path) return;
21990
22696
 
21991
22697
  const requestId = beginUiAction("save_as");
@@ -22007,14 +22713,17 @@
22007
22713
  }
22008
22714
 
22009
22715
  if (stripAnnotationsBtn) {
22010
- stripAnnotationsBtn.addEventListener("click", () => {
22716
+ stripAnnotationsBtn.addEventListener("click", async () => {
22011
22717
  const content = sourceTextEl.value;
22012
22718
  if (!hasAnnotationMarkers(content)) {
22013
22719
  setStatus("No [an: ...] markers found in editor.", "warning");
22014
22720
  return;
22015
22721
  }
22016
22722
 
22017
- const confirmed = window.confirm("Remove all [an: ...] markers from editor text? This cannot be undone.");
22723
+ const confirmed = await requestStudioConfirmation(
22724
+ "Remove all [an: ...] markers from editor text? This cannot be undone.",
22725
+ { title: "Remove all annotations?", confirmLabel: "Remove", destructive: true },
22726
+ );
22018
22727
  if (!confirmed) return;
22019
22728
 
22020
22729
  const strippedContent = stripAnnotationMarkers(content);
@@ -22049,7 +22758,7 @@
22049
22758
  }
22050
22759
  if (sourceBadgeEl) {
22051
22760
  sourceBadgeEl.addEventListener("click", () => {
22052
- if (!studioUiRefreshEnabled) resetEditorOrigin();
22761
+ if (!studioUiRefreshEnabled) void resetEditorOrigin();
22053
22762
  });
22054
22763
  }
22055
22764
  if (resourceDirBtn) {
@@ -22092,6 +22801,72 @@
22092
22801
  });
22093
22802
  }
22094
22803
 
22804
+ function applyImportedFileCopy(text, filename) {
22805
+ const name = String(filename || "imported file").trim() || "imported file";
22806
+ setEditorText(String(text || ""), { preserveScroll: false, preserveSelection: false });
22807
+ setSourceState({
22808
+ source: "upload",
22809
+ label: "imported copy: " + name,
22810
+ path: null,
22811
+ });
22812
+ refreshResponseUi();
22813
+ const detectedLang = detectLanguageFromName(name);
22814
+ if (detectedLang) setEditorLanguage(detectedLang);
22815
+ setStatus("Imported file copy: " + name + ".", "success");
22816
+ }
22817
+
22818
+ function chooseStudioFileCopyWithBrowser() {
22819
+ fileInput.value = "";
22820
+ setStatus("If no file picker appeared, enter the file path and select Import from path.");
22821
+ try {
22822
+ fileInput.click();
22823
+ } catch {
22824
+ setStatus("This browser could not open its file picker. Enter the file path and select Import from path.", "warning");
22825
+ }
22826
+ }
22827
+
22828
+ async function openStudioFileCopyDialog() {
22829
+ const resourceDir = getCurrentResourceDirValue();
22830
+ const suggestedPath = resourceDir ? resourceDir.replace(/[\\/]$/, "") + "/" : "./";
22831
+ let path = null;
22832
+ studioImportDecisionOpen = true;
22833
+ try {
22834
+ path = await requestStudioTextInput(
22835
+ "Enter the path to a file you want to import, or use Browse to open your browser’s file picker.",
22836
+ suggestedPath,
22837
+ {
22838
+ title: "Import file copy",
22839
+ confirmLabel: "Import from path",
22840
+ secondaryLabel: "Browse…",
22841
+ onSecondary: chooseStudioFileCopyWithBrowser,
22842
+ inputLabel: "File path on computer running Pi",
22843
+ placeholder: "/path/to/file.md",
22844
+ },
22845
+ );
22846
+ } finally {
22847
+ studioImportDecisionOpen = false;
22848
+ }
22849
+ if (!path) return;
22850
+ try {
22851
+ const payload = await fetchStudioJson("/import-file-copy", {
22852
+ method: "POST",
22853
+ body: JSON.stringify({ path }),
22854
+ });
22855
+ if (typeof payload.text !== "string") throw new Error("Studio did not return file text.");
22856
+ applyImportedFileCopy(payload.text, typeof payload.filename === "string" ? payload.filename : path);
22857
+ } catch (error) {
22858
+ setStatus("Could not import file copy: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
22859
+ }
22860
+ }
22861
+
22862
+ if (importFileBtn) {
22863
+ importFileBtn.addEventListener("click", (event) => {
22864
+ event.preventDefault();
22865
+ event.stopPropagation();
22866
+ void openStudioFileCopyDialog();
22867
+ });
22868
+ }
22869
+
22095
22870
  fileInput.addEventListener("change", () => {
22096
22871
  const file = fileInput.files && fileInput.files[0];
22097
22872
  if (!file) return;
@@ -22103,21 +22878,11 @@
22103
22878
  const reader = new FileReader();
22104
22879
  reader.onload = () => {
22105
22880
  const text = typeof reader.result === "string" ? reader.result : "";
22106
- setEditorText(text, { preserveScroll: false, preserveSelection: false });
22107
- setSourceState({
22108
- source: "upload",
22109
- label: "imported copy: " + file.name,
22110
- path: null,
22111
- });
22112
- refreshResponseUi();
22113
- const detectedLang = detectLanguageFromName(file.name);
22114
- if (detectedLang) {
22115
- setEditorLanguage(detectedLang);
22116
- }
22117
- setStatus("Imported file copy: " + file.name + ".", "success");
22881
+ if (studioImportDecisionOpen) finishStudioDecision(null);
22882
+ applyImportedFileCopy(text, file.name);
22118
22883
  };
22119
22884
  reader.onerror = () => {
22120
- setStatus("Failed to read file.", "error");
22885
+ setStatus("Failed to read file. Enter its path in the import dialog or choose another file.", "error");
22121
22886
  };
22122
22887
  reader.readAsText(file);
22123
22888
  });