@june-ai/june-ui 0.2.5 → 0.2.6

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/dist/index.cjs CHANGED
@@ -145,6 +145,184 @@ var init_document_utils = __esm({
145
145
  }
146
146
  });
147
147
 
148
+ // src/components/document-editor/document-pagination.ts
149
+ function gapWidget(height) {
150
+ return () => {
151
+ const el = document.createElement("div");
152
+ el.className = "june-doc__page-gap";
153
+ el.contentEditable = "false";
154
+ el.setAttribute("aria-hidden", "true");
155
+ el.style.height = `${height}px`;
156
+ return el;
157
+ };
158
+ }
159
+ function build(doc, breaks) {
160
+ return import_view.DecorationSet.create(
161
+ doc,
162
+ breaks.map(
163
+ (b) => import_view.Decoration.widget(b.pos, gapWidget(b.height), {
164
+ side: -1,
165
+ ignoreSelection: true,
166
+ key: `jd-gap-${b.pos}-${Math.round(b.height)}`
167
+ })
168
+ )
169
+ );
170
+ }
171
+ function collectUnits(doc) {
172
+ const units = [];
173
+ const walk = (parent, start, inheritedBreak) => {
174
+ parent.forEach((child, offset, index) => {
175
+ const pos = start + offset;
176
+ const breakPos = index === 0 && inheritedBreak !== null ? inheritedBreak : pos;
177
+ if (SPLITTABLE.has(child.type.name) && child.childCount > 0) {
178
+ walk(child, pos + 1, breakPos);
179
+ } else {
180
+ units.push({ pos, breakPos, node: child });
181
+ }
182
+ });
183
+ };
184
+ walk(doc, 0, null);
185
+ return units;
186
+ }
187
+ function measure(view, cfg) {
188
+ const prose = view.dom;
189
+ const proseRect = prose.getBoundingClientRect();
190
+ const scale = prose.offsetWidth ? proseRect.width / prose.offsetWidth : 1;
191
+ if (!scale || !proseRect.width) return { breaks: [], pages: 1 };
192
+ const gaps = [...prose.querySelectorAll(".june-doc__page-gap")].map((g) => {
193
+ const r = g.getBoundingClientRect();
194
+ return { top: (r.top - proseRect.top) / scale, height: r.height / scale };
195
+ });
196
+ const gapsAbove = (y) => gaps.reduce((sum, g) => g.top <= y + 0.5 ? sum + g.height : sum, 0);
197
+ const contentH = Math.max(80, cfg.pageHeight - cfg.padTop - cfg.padBottom);
198
+ const stride = cfg.pageHeight + cfg.gap;
199
+ const breaks = /* @__PURE__ */ new Map();
200
+ let pageStart = 0;
201
+ let added = 0;
202
+ let force = false;
203
+ for (const unit of collectUnits(view.state.doc)) {
204
+ const el = view.nodeDOM(unit.pos);
205
+ if (!(el instanceof HTMLElement)) continue;
206
+ const r = el.getBoundingClientRect();
207
+ const rawTop = (r.top - proseRect.top) / scale;
208
+ const natural = rawTop - gapsAbove(rawTop);
209
+ const h = r.height / scale;
210
+ let top = natural + added;
211
+ let bottom = top + h;
212
+ const overflows = bottom > pageStart + contentH + 0.5 && top > pageStart + 1;
213
+ if (force || overflows) {
214
+ const target = pageStart + stride;
215
+ if (top < target) {
216
+ const spacer = target - top;
217
+ breaks.set(unit.breakPos, (breaks.get(unit.breakPos) ?? 0) + spacer);
218
+ added += spacer;
219
+ top = target;
220
+ bottom = top + h;
221
+ }
222
+ pageStart = target;
223
+ while (top >= pageStart + stride) pageStart += stride;
224
+ force = false;
225
+ }
226
+ while (bottom > pageStart + contentH + 0.5) pageStart += stride;
227
+ if (unit.node.type.name === "pageBreak") force = true;
228
+ }
229
+ const pages = Math.max(1, Math.round(pageStart / stride) + 1 + (force ? 1 : 0));
230
+ return {
231
+ breaks: [...breaks].map(([pos, height]) => ({ pos, height: Math.round(height * 10) / 10 })).sort((a, b) => a.pos - b.pos),
232
+ pages
233
+ };
234
+ }
235
+ function setPaginationConfig(editor, cfg) {
236
+ if (editor.isDestroyed) return;
237
+ const prev = configs.get(editor.view);
238
+ configs.set(editor.view, cfg);
239
+ if (!prev || prev.enabled !== cfg.enabled || prev.pageHeight !== cfg.pageHeight || prev.gap !== cfg.gap || prev.padTop !== cfg.padTop || prev.padBottom !== cfg.padBottom) {
240
+ remeasurePagination(editor);
241
+ }
242
+ }
243
+ function remeasurePagination(editor) {
244
+ if (editor.isDestroyed) return;
245
+ const s = paginationKey.getState(editor.state);
246
+ if (!s) return;
247
+ editor.view.dispatch(editor.state.tr.setMeta(paginationKey, { remeasure: true }).setMeta("addToHistory", false));
248
+ }
249
+ function getPageCount(editor) {
250
+ return editor && paginationKey.getState(editor.state)?.pages || 1;
251
+ }
252
+ var import_core, import_state, import_view, paginationKey, configs, SPLITTABLE, same2, DocumentPagination;
253
+ var init_document_pagination = __esm({
254
+ "src/components/document-editor/document-pagination.ts"() {
255
+ "use strict";
256
+ import_core = require("@tiptap/core");
257
+ import_state = require("@tiptap/pm/state");
258
+ import_view = require("@tiptap/pm/view");
259
+ paginationKey = new import_state.PluginKey("juneDocPagination");
260
+ configs = /* @__PURE__ */ new WeakMap();
261
+ SPLITTABLE = /* @__PURE__ */ new Set(["bulletList", "orderedList", "listItem", "blockquote"]);
262
+ same2 = (a, b) => a.length === b.length && a.every((x, i) => x.pos === b[i].pos && Math.abs(x.height - b[i].height) < 1);
263
+ DocumentPagination = import_core.Extension.create({
264
+ name: "juneDocPagination",
265
+ addProseMirrorPlugins() {
266
+ return [
267
+ new import_state.Plugin({
268
+ key: paginationKey,
269
+ state: {
270
+ init: () => ({ breaks: [], pages: 1, decorations: import_view.DecorationSet.empty, epoch: 0 }),
271
+ apply(tr, prev, _old, state) {
272
+ const meta = tr.getMeta(paginationKey);
273
+ if (meta?.remeasure) return { ...prev, epoch: prev.epoch + 1 };
274
+ if (meta?.breaks) {
275
+ return { ...prev, breaks: meta.breaks, pages: meta.pages ?? prev.pages, decorations: build(state.doc, meta.breaks) };
276
+ }
277
+ if (tr.docChanged) {
278
+ const breaks = prev.breaks.map((b) => ({ ...b, pos: tr.mapping.map(b.pos, -1) })).filter((b) => b.pos >= 0 && b.pos <= state.doc.content.size);
279
+ return { ...prev, breaks, decorations: prev.decorations.map(tr.mapping, tr.doc) };
280
+ }
281
+ return prev;
282
+ }
283
+ },
284
+ props: {
285
+ decorations: (state) => paginationKey.getState(state)?.decorations
286
+ },
287
+ view(view) {
288
+ let frame = null;
289
+ let rounds = 0;
290
+ const schedule = (reset) => {
291
+ if (reset) rounds = 0;
292
+ if (frame !== null || typeof window === "undefined") return;
293
+ const raf = window.requestAnimationFrame ?? ((cb) => window.setTimeout(() => cb(0), 16));
294
+ frame = raf(() => {
295
+ frame = null;
296
+ if (view.isDestroyed) return;
297
+ const cfg = configs.get(view);
298
+ const current = paginationKey.getState(view.state);
299
+ if (!current) return;
300
+ const next = cfg?.enabled ? measure(view, cfg) : { breaks: [], pages: 1 };
301
+ if (same2(next.breaks, current.breaks) && next.pages === current.pages) return;
302
+ if (++rounds > 4) return;
303
+ view.dispatch(view.state.tr.setMeta(paginationKey, next).setMeta("addToHistory", false));
304
+ });
305
+ };
306
+ schedule(true);
307
+ return {
308
+ update(v, prevState) {
309
+ const pg = paginationKey.getState(v.state);
310
+ const prevPg = paginationKey.getState(prevState);
311
+ if (v.state.doc !== prevState.doc || pg?.epoch !== prevPg?.epoch) schedule(true);
312
+ else if (pg !== prevPg) schedule(false);
313
+ },
314
+ destroy() {
315
+ frame = null;
316
+ }
317
+ };
318
+ }
319
+ })
320
+ ];
321
+ }
322
+ });
323
+ }
324
+ });
325
+
148
326
  // src/components/document-editor/document-search.ts
149
327
  function findMatches(doc, term, caseSensitive) {
150
328
  if (!term) return [];
@@ -174,13 +352,13 @@ function findMatches(doc, term, caseSensitive) {
174
352
  });
175
353
  return out;
176
354
  }
177
- function build(doc, term, caseSensitive, current) {
355
+ function build2(doc, term, caseSensitive, current) {
178
356
  const matches = findMatches(doc, term, caseSensitive);
179
357
  const active = matches.length ? Math.min(Math.max(current, 0), matches.length - 1) : -1;
180
- const decorations = import_view.DecorationSet.create(
358
+ const decorations = import_view2.DecorationSet.create(
181
359
  doc,
182
360
  matches.map(
183
- (m, i) => import_view.Decoration.inline(m.from, m.to, {
361
+ (m, i) => import_view2.Decoration.inline(m.from, m.to, {
184
362
  class: i === active ? "june-doc__match june-doc__match--active" : "june-doc__match"
185
363
  })
186
364
  )
@@ -226,27 +404,27 @@ function replaceAll(editor, replacement) {
226
404
  }).run();
227
405
  return matches.length;
228
406
  }
229
- var import_core, import_state, import_view, searchKey, DocumentSearch;
407
+ var import_core2, import_state2, import_view2, searchKey, DocumentSearch;
230
408
  var init_document_search = __esm({
231
409
  "src/components/document-editor/document-search.ts"() {
232
410
  "use strict";
233
- import_core = require("@tiptap/core");
234
- import_state = require("@tiptap/pm/state");
235
- import_view = require("@tiptap/pm/view");
236
- searchKey = new import_state.PluginKey("juneDocSearch");
237
- DocumentSearch = import_core.Extension.create({
411
+ import_core2 = require("@tiptap/core");
412
+ import_state2 = require("@tiptap/pm/state");
413
+ import_view2 = require("@tiptap/pm/view");
414
+ searchKey = new import_state2.PluginKey("juneDocSearch");
415
+ DocumentSearch = import_core2.Extension.create({
238
416
  name: "juneDocSearch",
239
417
  addProseMirrorPlugins() {
240
418
  return [
241
- new import_state.Plugin({
419
+ new import_state2.Plugin({
242
420
  key: searchKey,
243
421
  state: {
244
- init: () => ({ term: "", caseSensitive: false, current: -1, matches: [], decorations: import_view.DecorationSet.empty }),
422
+ init: () => ({ term: "", caseSensitive: false, current: -1, matches: [], decorations: import_view2.DecorationSet.empty }),
245
423
  apply(tr, prev, _old, state) {
246
424
  const meta = tr.getMeta(searchKey);
247
425
  if (!meta && !tr.docChanged) return prev;
248
426
  if (!meta && !prev.term) return prev;
249
- return build(
427
+ return build2(
250
428
  state.doc,
251
429
  meta?.term ?? prev.term,
252
430
  meta?.caseSensitive ?? prev.caseSensitive,
@@ -288,14 +466,16 @@ function createDocumentExtensions() {
288
466
  }),
289
467
  PageBreak,
290
468
  DocumentSearch,
469
+ DocumentPagination,
470
+ // inactive until the live editor sets a page geometry
291
471
  import_markdown.Markdown
292
472
  ];
293
473
  }
294
- var import_core2, import_extension_highlight, import_extension_image, import_extension_subscript, import_extension_superscript, import_extension_table, import_extension_text_align, import_extension_text_style, import_markdown, import_starter_kit, PageBreak;
474
+ var import_core3, import_extension_highlight, import_extension_image, import_extension_subscript, import_extension_superscript, import_extension_table, import_extension_text_align, import_extension_text_style, import_markdown, import_starter_kit, PageBreak;
295
475
  var init_document_extensions = __esm({
296
476
  "src/components/document-editor/document-extensions.ts"() {
297
477
  "use strict";
298
- import_core2 = require("@tiptap/core");
478
+ import_core3 = require("@tiptap/core");
299
479
  import_extension_highlight = __toESM(require("@tiptap/extension-highlight"), 1);
300
480
  import_extension_image = __toESM(require("@tiptap/extension-image"), 1);
301
481
  import_extension_subscript = __toESM(require("@tiptap/extension-subscript"), 1);
@@ -305,8 +485,9 @@ var init_document_extensions = __esm({
305
485
  import_extension_text_style = require("@tiptap/extension-text-style");
306
486
  import_markdown = require("@tiptap/markdown");
307
487
  import_starter_kit = __toESM(require("@tiptap/starter-kit"), 1);
488
+ init_document_pagination();
308
489
  init_document_search();
309
- PageBreak = import_core2.Node.create({
490
+ PageBreak = import_core3.Node.create({
310
491
  name: "pageBreak",
311
492
  group: "block",
312
493
  atom: true,
@@ -317,7 +498,7 @@ var init_document_extensions = __esm({
317
498
  renderHTML({ HTMLAttributes }) {
318
499
  return [
319
500
  "div",
320
- (0, import_core2.mergeAttributes)(HTMLAttributes, {
501
+ (0, import_core3.mergeAttributes)(HTMLAttributes, {
321
502
  "data-page-break": "true",
322
503
  class: "june-doc__page-break",
323
504
  style: "break-after: page; page-break-after: always;"
@@ -348,7 +529,7 @@ __export(document_engine_exports, {
348
529
  });
349
530
  function scratchEditor() {
350
531
  if (!scratch || scratch.isDestroyed) {
351
- scratch = new import_core3.Editor({ extensions: createDocumentExtensions(), content: "" });
532
+ scratch = new import_core4.Editor({ extensions: createDocumentExtensions(), content: "" });
352
533
  }
353
534
  return scratch;
354
535
  }
@@ -374,11 +555,11 @@ function load(doc) {
374
555
  editor.commands.setContent(doc, { emitUpdate: false });
375
556
  return editor;
376
557
  }
377
- var import_core3, scratch, docToHtml, docToMarkdown, docToText;
558
+ var import_core4, scratch, docToHtml, docToMarkdown, docToText;
378
559
  var init_document_engine = __esm({
379
560
  "src/components/document-editor/document-engine.ts"() {
380
561
  "use strict";
381
- import_core3 = require("@tiptap/core");
562
+ import_core4 = require("@tiptap/core");
382
563
  init_errors();
383
564
  init_document_extensions();
384
565
  init_document_utils();
@@ -418,17 +599,21 @@ __export(index_exports, {
418
599
  getDefaultDocumentStore: () => getDefaultDocumentStore,
419
600
  getDocumentEditorInstructions: () => getDocumentEditorInstructions,
420
601
  getDocumentEditorRules: () => getDocumentEditorRules,
602
+ isDocumentEcho: () => isDocumentEcho,
421
603
  isJuneError: () => isJuneError,
422
604
  jsonFieldExtractor: () => jsonFieldExtractor,
423
605
  juneLibrary: () => juneLibrary,
424
606
  lightTheme: () => lightTheme,
425
607
  listDocumentExporters: () => listDocumentExporters,
426
608
  mergeDocuments: () => mergeDocuments,
609
+ readDocumentFromProgram: () => readDocumentFromProgram,
427
610
  registerDocumentExporter: () => registerDocumentExporter,
611
+ rememberDocumentEcho: () => rememberDocumentEcho,
428
612
  runAction: () => runAction,
429
613
  serializeDocumentsForAgent: () => serializeDocumentsForAgent,
430
614
  sseToText: () => sseToText,
431
615
  toJuneError: () => toJuneError,
616
+ updateDocumentInProgram: () => updateDocumentInProgram,
432
617
  useDocumentOptions: () => useDocumentOptions,
433
618
  useJuneRuntime: () => useJuneRuntime,
434
619
  useJuneStream: () => useJuneStream,
@@ -511,9 +696,321 @@ var JuneDocumentContext = (0, import_react.createContext)({});
511
696
  function useDocumentOptions() {
512
697
  return (0, import_react.useContext)(JuneDocumentContext);
513
698
  }
514
- function useDocumentValue(store, id, selector) {
515
- const read = () => selector(store.get(id));
516
- return (0, import_react.useSyncExternalStore)(store.subscribe, read, read);
699
+ function useDocumentValue(store, id, selector) {
700
+ const read = () => selector(store.get(id));
701
+ return (0, import_react.useSyncExternalStore)(store.subscribe, read, read);
702
+ }
703
+
704
+ // src/components/document-editor/document-merge.ts
705
+ init_document_utils();
706
+ var MAX_CELLS = 4e6;
707
+ function matchIndices(a, b) {
708
+ const n = a.length;
709
+ const m = b.length;
710
+ if (n * m > MAX_CELLS) return null;
711
+ const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
712
+ for (let i2 = n - 1; i2 >= 0; i2--) {
713
+ for (let j2 = m - 1; j2 >= 0; j2--) {
714
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
715
+ }
716
+ }
717
+ const matches = /* @__PURE__ */ new Map();
718
+ let i = 0;
719
+ let j = 0;
720
+ while (i < n && j < m) {
721
+ if (a[i] === b[j]) {
722
+ matches.set(i, j);
723
+ i++;
724
+ j++;
725
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) i++;
726
+ else j++;
727
+ }
728
+ return matches;
729
+ }
730
+ var same = (x, y) => x.length === y.length && x.every((v, i) => v === y[i]);
731
+ function mergeBlocks(base, local, incoming) {
732
+ if (same(local, base)) return { blocks: incoming, conflicts: 0 };
733
+ if (same(incoming, base) || same(local, incoming)) return { blocks: local, conflicts: 0 };
734
+ const ml = matchIndices(base, local);
735
+ const mn = matchIndices(base, incoming);
736
+ if (!ml || !mn) return { blocks: null, conflicts: 1 };
737
+ const anchors = [...ml.keys()].filter((i) => mn.has(i)).sort((x, y) => x - y);
738
+ const out = [];
739
+ let conflicts = 0;
740
+ let pb = -1;
741
+ let pl = -1;
742
+ let pn = -1;
743
+ const region = (bEnd, lEnd, nEnd) => {
744
+ const B = base.slice(pb + 1, bEnd);
745
+ const L = local.slice(pl + 1, lEnd);
746
+ const N = incoming.slice(pn + 1, nEnd);
747
+ if (B.length === L.length && L.length === N.length) {
748
+ for (let k = 0; k < B.length; k++) {
749
+ if (L[k] === B[k]) out.push(N[k]);
750
+ else if (N[k] === B[k] || N[k] === L[k]) out.push(L[k]);
751
+ else {
752
+ conflicts++;
753
+ out.push(L[k]);
754
+ }
755
+ }
756
+ return;
757
+ }
758
+ if (same(L, B)) out.push(...N);
759
+ else if (same(N, B) || same(L, N)) out.push(...L);
760
+ else {
761
+ conflicts++;
762
+ out.push(...L);
763
+ }
764
+ };
765
+ for (const a of anchors) {
766
+ const la = ml.get(a);
767
+ const na = mn.get(a);
768
+ region(a, la, na);
769
+ out.push(local[la]);
770
+ pb = a;
771
+ pl = la;
772
+ pn = na;
773
+ }
774
+ region(base.length, local.length, incoming.length);
775
+ return { blocks: out, conflicts };
776
+ }
777
+ function mergeDocuments(base, local, incoming) {
778
+ const toBlocks = (d) => (d.content ?? []).map((node) => JSON.stringify(node));
779
+ const { blocks: blocks2, conflicts } = mergeBlocks(toBlocks(base), toBlocks(local), toBlocks(incoming));
780
+ if (!blocks2) return { doc: local, conflicts: Math.max(1, conflicts) };
781
+ if (blocks2.length === 0) return { doc: emptyDoc(), conflicts };
782
+ return { doc: { type: "doc", content: blocks2.map((b) => JSON.parse(b)) }, conflicts };
783
+ }
784
+
785
+ // src/components/document-editor/document-store.ts
786
+ init_document_utils();
787
+ var incomingKey = (doc) => JSON.stringify([doc.title, docKey(doc.json)]);
788
+ function createDocumentStore() {
789
+ const records = /* @__PURE__ */ new Map();
790
+ const listeners = /* @__PURE__ */ new Set();
791
+ const emit = () => listeners.forEach((listener) => listener());
792
+ const put = (record) => {
793
+ records.set(record.id, record);
794
+ emit();
795
+ };
796
+ const fresh2 = (doc) => ({
797
+ id: doc.id,
798
+ title: doc.title,
799
+ documentType: doc.documentType,
800
+ status: doc.status,
801
+ json: doc.json,
802
+ baseJson: doc.json,
803
+ baseTitle: doc.title,
804
+ lastIncomingKey: incomingKey(doc),
805
+ revision: 0,
806
+ origin: "external",
807
+ pending: null,
808
+ updatedAt: Date.now()
809
+ });
810
+ return {
811
+ get: (id) => records.get(id),
812
+ list: () => [...records.values()],
813
+ subscribe: (listener) => {
814
+ listeners.add(listener);
815
+ return () => {
816
+ listeners.delete(listener);
817
+ };
818
+ },
819
+ receive(incoming) {
820
+ const current = records.get(incoming.id);
821
+ if (!current) {
822
+ put(fresh2(incoming));
823
+ return "created";
824
+ }
825
+ const key = incomingKey(incoming);
826
+ if (current.lastIncomingKey === key) return "unchanged";
827
+ const userEditedBody = docKey(current.json) !== docKey(current.baseJson);
828
+ const userEditedTitle = current.title !== current.baseTitle;
829
+ const title = userEditedTitle ? current.title : incoming.title;
830
+ const meta = {
831
+ documentType: incoming.documentType ?? current.documentType,
832
+ status: incoming.status ?? current.status
833
+ };
834
+ if (!userEditedBody) {
835
+ put({
836
+ ...current,
837
+ ...meta,
838
+ title,
839
+ baseTitle: incoming.title,
840
+ json: incoming.json,
841
+ baseJson: incoming.json,
842
+ lastIncomingKey: key,
843
+ revision: current.revision + 1,
844
+ origin: "external",
845
+ pending: null,
846
+ updatedAt: Date.now()
847
+ });
848
+ return "applied";
849
+ }
850
+ const { doc, conflicts } = mergeDocuments(current.baseJson, current.json, incoming.json);
851
+ if (conflicts === 0) {
852
+ put({
853
+ ...current,
854
+ ...meta,
855
+ title,
856
+ baseTitle: incoming.title,
857
+ json: doc,
858
+ baseJson: incoming.json,
859
+ lastIncomingKey: key,
860
+ revision: current.revision + 1,
861
+ origin: "external",
862
+ pending: null,
863
+ updatedAt: Date.now()
864
+ });
865
+ return "merged";
866
+ }
867
+ put({
868
+ ...current,
869
+ ...meta,
870
+ lastIncomingKey: key,
871
+ pending: { json: incoming.json, title: incoming.title, merged: doc, conflicts }
872
+ });
873
+ return "conflict";
874
+ },
875
+ hydrate(doc) {
876
+ put(fresh2(doc));
877
+ },
878
+ edit(id, json, origin) {
879
+ const current = records.get(id);
880
+ if (!current || docKey(current.json) === docKey(json)) return;
881
+ put({ ...current, json, origin, revision: current.revision + 1, updatedAt: Date.now() });
882
+ },
883
+ setTitle(id, title) {
884
+ const current = records.get(id);
885
+ if (!current || current.title === title) return;
886
+ put({ ...current, title, updatedAt: Date.now() });
887
+ },
888
+ resolve(id, choice) {
889
+ const current = records.get(id);
890
+ const pending = current?.pending;
891
+ if (!current || !pending) return;
892
+ if (choice === "keep") {
893
+ put({ ...current, baseJson: pending.json, baseTitle: pending.title, pending: null });
894
+ return;
895
+ }
896
+ const json = choice === "merge" ? pending.merged : pending.json;
897
+ const title = choice === "merge" ? current.title : pending.title;
898
+ put({
899
+ ...current,
900
+ title,
901
+ json,
902
+ baseJson: pending.json,
903
+ baseTitle: pending.title,
904
+ revision: current.revision + 1,
905
+ origin: "external",
906
+ pending: null,
907
+ updatedAt: Date.now()
908
+ });
909
+ },
910
+ remove(id) {
911
+ if (records.delete(id)) emit();
912
+ }
913
+ };
914
+ }
915
+ var echoes = /* @__PURE__ */ new WeakMap();
916
+ var ECHO_MEMORY = 8;
917
+ function rememberDocumentEcho(store, id, content) {
918
+ let byId = echoes.get(store);
919
+ if (!byId) echoes.set(store, byId = /* @__PURE__ */ new Map());
920
+ const list2 = byId.get(id) ?? [];
921
+ list2.push(content);
922
+ if (list2.length > ECHO_MEMORY) list2.shift();
923
+ byId.set(id, list2);
924
+ }
925
+ function isDocumentEcho(store, id, content) {
926
+ return !!content && !!echoes.get(store)?.get(id)?.includes(content);
927
+ }
928
+ var defaultStore = null;
929
+ function getDefaultDocumentStore() {
930
+ if (!defaultStore) defaultStore = createDocumentStore();
931
+ return defaultStore;
932
+ }
933
+
934
+ // src/utils/update-document-program.ts
935
+ function readString(src, i) {
936
+ if (src[i] !== '"') return null;
937
+ let j = i + 1;
938
+ let value = "";
939
+ while (j < src.length) {
940
+ const ch = src[j];
941
+ if (ch === '"') return { start: i, end: j + 1, value };
942
+ if (ch === "\\") {
943
+ const next = src[j + 1];
944
+ if (next === void 0) return null;
945
+ if (next === "u" && /^[0-9a-fA-F]{4}$/.test(src.slice(j + 2, j + 6))) {
946
+ value += String.fromCharCode(parseInt(src.slice(j + 2, j + 6), 16));
947
+ j += 6;
948
+ continue;
949
+ }
950
+ value += next === "n" ? "\n" : next === "t" ? " " : next === "r" ? "\r" : next === "b" ? "\b" : next === "f" ? "\f" : next;
951
+ j += 2;
952
+ continue;
953
+ }
954
+ value += ch;
955
+ j++;
956
+ }
957
+ return null;
958
+ }
959
+ var skipSpace = (src, i) => {
960
+ while (i < src.length && /\s/.test(src[i])) i++;
961
+ return i;
962
+ };
963
+ var toLangString = (value) => JSON.stringify(value);
964
+ function updateDocumentInProgram(program, documentId, update, componentName = "DocumentEditor") {
965
+ const re = new RegExp(`\\b${componentName}\\s*\\(`, "g");
966
+ let match;
967
+ while (match = re.exec(program)) {
968
+ const args = [];
969
+ let i = match.index + match[0].length;
970
+ for (let n = 0; n < 3; n++) {
971
+ i = skipSpace(program, i);
972
+ const tok = readString(program, i);
973
+ if (!tok) break;
974
+ args.push(tok);
975
+ i = skipSpace(program, tok.end);
976
+ if (program[i] === ",") i++;
977
+ else break;
978
+ }
979
+ if (args[0]?.value !== documentId) continue;
980
+ const edits = [];
981
+ if (update.title !== void 0 && args[1] && args[1].value !== update.title) {
982
+ edits.push({ start: args[1].start, end: args[1].end, text: toLangString(update.title) });
983
+ }
984
+ if (update.content !== void 0 && args[2] && args[2].value !== update.content) {
985
+ edits.push({ start: args[2].start, end: args[2].end, text: toLangString(update.content) });
986
+ }
987
+ if (!args[2] && update.content !== void 0) return null;
988
+ let out = program;
989
+ for (const e of edits.sort((a, b) => b.start - a.start)) {
990
+ out = out.slice(0, e.start) + e.text + out.slice(e.end);
991
+ }
992
+ return out;
993
+ }
994
+ return null;
995
+ }
996
+ function readDocumentFromProgram(program, documentId, componentName = "DocumentEditor") {
997
+ const re = new RegExp(`\\b${componentName}\\s*\\(`, "g");
998
+ let match;
999
+ while (match = re.exec(program)) {
1000
+ const args = [];
1001
+ let i = match.index + match[0].length;
1002
+ for (let n = 0; n < 3; n++) {
1003
+ i = skipSpace(program, i);
1004
+ const tok = readString(program, i);
1005
+ if (!tok) break;
1006
+ args.push(tok);
1007
+ i = skipSpace(program, tok.end);
1008
+ if (program[i] === ",") i++;
1009
+ else break;
1010
+ }
1011
+ if (args[0]?.value === documentId && args[1] && args[2]) return { title: args[1].value, content: args[2].value };
1012
+ }
1013
+ return null;
517
1014
  }
518
1015
 
519
1016
  // src/core/context.ts
@@ -1074,14 +1571,43 @@ function JuneRenderer(props) {
1074
1571
  onError,
1075
1572
  onJuneError,
1076
1573
  documents,
1574
+ onResponseChange,
1575
+ responseChangeDelay = 300,
1077
1576
  loadingFallback = null,
1078
1577
  emptyFallback = null,
1079
1578
  errorFallback,
1080
1579
  resetKeys,
1081
1580
  ...rendererProps
1082
1581
  } = props;
1083
- const latest = (0, import_react5.useRef)({ actions, confirmAction, parseActionEvent, onAction, onError, onJuneError });
1084
- latest.current = { actions, confirmAction, parseActionEvent, onAction, onError, onJuneError };
1582
+ const latest = (0, import_react5.useRef)({ actions, confirmAction, parseActionEvent, onAction, onError, onJuneError, onResponseChange, responseChangeDelay, response });
1583
+ latest.current = { actions, confirmAction, parseActionEvent, onAction, onError, onJuneError, onResponseChange, responseChangeDelay, response };
1584
+ const syncTimers = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
1585
+ (0, import_react5.useEffect)(() => {
1586
+ const timers = syncTimers.current;
1587
+ return () => {
1588
+ timers.forEach(clearTimeout);
1589
+ timers.clear();
1590
+ };
1591
+ }, []);
1592
+ const syncProgram = (0, import_react5.useCallback)(async (change, store) => {
1593
+ const l = latest.current;
1594
+ if (!l.onResponseChange || typeof l.response !== "string") return;
1595
+ const record = store.get(change.documentId);
1596
+ const title = record?.title ?? change.title;
1597
+ const json = record?.json ?? change.json;
1598
+ try {
1599
+ const { docToMarkdown: docToMarkdown2 } = await Promise.resolve().then(() => (init_document_engine(), document_engine_exports));
1600
+ const markdown = docToMarkdown2(json);
1601
+ const program = latest.current.response;
1602
+ if (typeof program !== "string") return;
1603
+ const next = updateDocumentInProgram(program, change.documentId, { title, content: markdown });
1604
+ if (next === null || next === program) return;
1605
+ rememberDocumentEcho(store, change.documentId, markdown);
1606
+ latest.current.onResponseChange?.(next, { documentId: change.documentId, title, markdown, source: change.source });
1607
+ } catch (error) {
1608
+ latest.current.onJuneError?.(toJuneError(error, "DOCUMENT_ERROR", "Updating the program failed"));
1609
+ }
1610
+ }, []);
1085
1611
  const handleAction = (0, import_react5.useCallback)((event) => {
1086
1612
  const l = latest.current;
1087
1613
  l.onAction?.(event);
@@ -1120,12 +1646,27 @@ function JuneRenderer(props) {
1120
1646
  const documentOptions = (0, import_react5.useMemo)(
1121
1647
  () => ({
1122
1648
  ...documents,
1649
+ onDocumentChange: (change) => {
1650
+ documents?.onDocumentChange?.(change);
1651
+ if (!latest.current.onResponseChange || change.source === "ai") return;
1652
+ const store = documents?.store ?? getDefaultDocumentStore();
1653
+ const timers = syncTimers.current;
1654
+ const prev = timers.get(change.documentId);
1655
+ if (prev) clearTimeout(prev);
1656
+ timers.set(
1657
+ change.documentId,
1658
+ setTimeout(() => {
1659
+ timers.delete(change.documentId);
1660
+ void syncProgram(change, store);
1661
+ }, Math.max(0, latest.current.responseChangeDelay))
1662
+ );
1663
+ },
1123
1664
  onError: (error) => {
1124
1665
  documents?.onError?.(error);
1125
1666
  latest.current.onJuneError?.(error);
1126
1667
  }
1127
1668
  }),
1128
- [documents]
1669
+ [documents, syncProgram]
1129
1670
  );
1130
1671
  const parts = (0, import_react5.useMemo)(
1131
1672
  () => mixedContent && response ? splitResponse(response, { streaming: isStreaming }) : null,
@@ -2383,53 +2924,110 @@ function DocumentBubbleMenu({ editor }) {
2383
2924
  // src/components/document-editor/document-content.tsx
2384
2925
  var import_react11 = require("@tiptap/react");
2385
2926
  var import_react12 = require("react");
2927
+ init_document_pagination();
2386
2928
  var import_jsx_runtime9 = require("react/jsx-runtime");
2929
+ var PAGE_WIDTH = 794;
2387
2930
  var PAGE_HEIGHT = 1123;
2388
- var PAGE_PAD_Y = 96;
2931
+ var PAGE_GAP = 28;
2932
+ var CANVAS_PAD_X = 24;
2933
+ var useIsoLayoutEffect = typeof window === "undefined" ? import_react12.useEffect : import_react12.useLayoutEffect;
2934
+ function geometry(width) {
2935
+ const ratio = width / PAGE_WIDTH;
2936
+ const height = Math.round(PAGE_HEIGHT * ratio);
2937
+ const padX = Math.round(Math.min(96, Math.max(20, width * 0.12)));
2938
+ const padY = Math.round(Math.min(96, Math.max(36, 96 * ratio)));
2939
+ return { width, height, padX, padY };
2940
+ }
2389
2941
  function DocumentContent({ editor, layout = "page", zoom = 1, showMarks = false, onMetrics }) {
2390
2942
  const canvas = (0, import_react12.useRef)(null);
2391
2943
  const sheet = (0, import_react12.useRef)(null);
2944
+ const [avail, setAvail] = (0, import_react12.useState)(PAGE_WIDTH);
2945
+ const [sheetH, setSheetH] = (0, import_react12.useState)(0);
2392
2946
  const [pages, setPages] = (0, import_react12.useState)(1);
2393
2947
  const metricsRef = (0, import_react12.useRef)(onMetrics);
2394
2948
  metricsRef.current = onMetrics;
2395
2949
  const last = (0, import_react12.useRef)({ page: 0, pages: 0 });
2950
+ const paged = layout === "page";
2951
+ const width = paged ? Math.max(260, Math.min(PAGE_WIDTH, Math.floor(avail / zoom))) : Math.max(260, Math.floor(avail / zoom));
2952
+ const g = geometry(width);
2953
+ const stride = g.height + PAGE_GAP;
2954
+ useIsoLayoutEffect(() => {
2955
+ const c = canvas.current;
2956
+ if (!c) return;
2957
+ const read = () => {
2958
+ const cs = typeof getComputedStyle === "function" ? getComputedStyle(c) : null;
2959
+ const padX = cs ? (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0) : CANVAS_PAD_X * 2;
2960
+ const w = c.clientWidth - padX;
2961
+ if (w > 0) setAvail(w);
2962
+ };
2963
+ read();
2964
+ if (typeof ResizeObserver === "undefined") return;
2965
+ const ro = new ResizeObserver(read);
2966
+ ro.observe(c);
2967
+ return () => ro.disconnect();
2968
+ }, []);
2969
+ (0, import_react12.useEffect)(() => {
2970
+ if (!editor) return;
2971
+ setPaginationConfig(editor, {
2972
+ enabled: paged,
2973
+ pageHeight: g.height,
2974
+ gap: PAGE_GAP,
2975
+ padTop: g.padY,
2976
+ padBottom: g.padY
2977
+ });
2978
+ }, [editor, paged, g.height, g.padY, width]);
2396
2979
  (0, import_react12.useEffect)(() => {
2397
2980
  const c = canvas.current;
2398
- const p = sheet.current;
2399
- const prose = editor?.view.dom;
2400
- if (!c || !p || !prose) return;
2401
- const measure = () => {
2402
- const contentH = prose.getBoundingClientRect().height / zoom;
2403
- const total = layout === "page" ? Math.max(1, Math.ceil((contentH + PAGE_PAD_Y * 2) / PAGE_HEIGHT)) : 1;
2404
- const cRect = c.getBoundingClientRect();
2405
- const pRect = p.getBoundingClientRect();
2406
- const y = cRect.top + c.clientHeight * 0.35 - pRect.top;
2407
- const page = Math.min(total, Math.max(1, Math.floor(y / (PAGE_HEIGHT * zoom)) + 1));
2981
+ const s = sheet.current;
2982
+ if (!editor || !c || !s) return;
2983
+ const prose = editor.view.dom;
2984
+ const report = () => {
2985
+ const total = paged ? getPageCount(editor) : 1;
2408
2986
  setPages(total);
2987
+ setSheetH(s.offsetHeight);
2988
+ const cRect = c.getBoundingClientRect();
2989
+ const sRect = s.getBoundingClientRect();
2990
+ const y = (cRect.top + c.clientHeight * 0.35 - sRect.top) / zoom;
2991
+ const page = paged ? Math.min(total, Math.max(1, Math.floor(y / stride) + 1)) : 1;
2409
2992
  if (last.current.page !== page || last.current.pages !== total) {
2410
2993
  last.current = { page, pages: total };
2411
2994
  metricsRef.current?.({ page, pages: total });
2412
2995
  }
2413
2996
  };
2414
- measure();
2415
- c.addEventListener("scroll", measure, { passive: true });
2416
- const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(measure) : null;
2417
- ro?.observe(prose);
2418
- ro?.observe(c);
2419
- editor?.on("update", measure);
2997
+ report();
2998
+ editor.on("transaction", report);
2999
+ c.addEventListener("scroll", report, { passive: true });
3000
+ const onLoad = () => remeasurePagination(editor);
3001
+ prose.addEventListener("load", onLoad, true);
3002
+ const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(report) : null;
3003
+ ro?.observe(s);
2420
3004
  return () => {
2421
- c.removeEventListener("scroll", measure);
3005
+ editor.off("transaction", report);
3006
+ c.removeEventListener("scroll", report);
3007
+ prose.removeEventListener("load", onLoad, true);
2422
3008
  ro?.disconnect();
2423
- editor?.off("update", measure);
2424
3009
  };
2425
- }, [editor, layout, zoom]);
2426
- const sheetStyle = layout === "page" ? {
2427
- zoom,
2428
- minHeight: pages * PAGE_HEIGHT,
2429
- maxWidth: `calc(100% / ${zoom})`,
2430
- "--jd-page-h": `${PAGE_HEIGHT}px`
2431
- } : { zoom };
2432
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { ref: canvas, className: "june-doc__content june-doc__canvas", "data-layout": layout, "data-marks": showMarks || void 0, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { ref: sheet, className: "june-doc__sheet", style: sheetStyle, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react11.EditorContent, { editor }) }) });
3010
+ }, [editor, paged, zoom, stride]);
3011
+ const minHeight = paged ? pages * g.height + (pages - 1) * PAGE_GAP : void 0;
3012
+ const sheetStyle = {
3013
+ width,
3014
+ minHeight,
3015
+ transform: zoom === 1 ? void 0 : `scale(${zoom})`,
3016
+ "--jd-pad-x": `${g.padX}px`,
3017
+ "--jd-pad-y": `${g.padY}px`
3018
+ };
3019
+ const zoomerStyle = {
3020
+ width: width * zoom,
3021
+ height: zoom === 1 ? void 0 : Math.max(sheetH, minHeight ?? 0) * zoom
3022
+ };
3023
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { ref: canvas, className: "june-doc__content june-doc__canvas", "data-layout": layout, "data-marks": showMarks || void 0, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "june-doc__zoomer", style: zoomerStyle, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { ref: sheet, className: "june-doc__sheet", style: sheetStyle, children: [
3024
+ paged ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "june-doc__pages", "aria-hidden": "true", children: Array.from({ length: pages }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "june-doc__page", style: { top: i * stride, height: g.height }, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "june-doc__page-num", children: [
3025
+ i + 1,
3026
+ " / ",
3027
+ pages
3028
+ ] }) }, i)) }) : null,
3029
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react11.EditorContent, { editor })
3030
+ ] }) }) });
2433
3031
  }
2434
3032
 
2435
3033
  // src/components/document-editor/document-findbar.tsx
@@ -2578,7 +3176,7 @@ var FORMATS = {
2578
3176
  txt: { label: "Plain text", hint: ".txt", icon: "file" }
2579
3177
  };
2580
3178
  function DocumentHeader(props) {
2581
- const { title, editable, isDraft, fullscreen, onToggleFullscreen, onTitle, exportFormats, canSave, saveState, onExport, onCopy, onSave } = props;
3179
+ const { title, editable, isDraft, fullscreen, onToggleFullscreen, onTitle, exportFormats, canSave, showSave = canSave, saveState, onExport, onCopy, onSave } = props;
2582
3180
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "june-doc__header", children: [
2583
3181
  fullscreen ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: "june-doc__btn june-doc__btn--icon june-doc__btn--ghost", "aria-label": "Close full screen", title: "Close (Esc)", onClick: onToggleFullscreen, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon, { name: "close", size: 18 }) }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "june-doc__doc-icon", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon, { name: "file", size: 16 }) }),
2584
3182
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(DocumentTitle, { value: title, disabled: !editable, onChange: onTitle }),
@@ -2587,11 +3185,12 @@ function DocumentHeader(props) {
2587
3185
  "AI draft"
2588
3186
  ] }) : null,
2589
3187
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "june-doc__header-actions", children: [
2590
- canSave ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
3188
+ showSave ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2591
3189
  "button",
2592
3190
  {
2593
3191
  type: "button",
2594
- className: "june-doc__btn june-doc__btn--outline",
3192
+ className: "june-doc__btn june-doc__btn--outline june-doc__save",
3193
+ "data-state": saveState,
2595
3194
  "aria-label": "Save",
2596
3195
  title: "Save (Ctrl+S)",
2597
3196
  disabled: saveState === "saving" || saveState === "saved" || saveState === "idle",
@@ -3105,223 +3704,6 @@ ${docToText2(doc.json)}`;
3105
3704
  // src/components/document-editor/document-editor.tsx
3106
3705
  init_document_extensions();
3107
3706
 
3108
- // src/components/document-editor/document-merge.ts
3109
- init_document_utils();
3110
- var MAX_CELLS = 4e6;
3111
- function matchIndices(a, b) {
3112
- const n = a.length;
3113
- const m = b.length;
3114
- if (n * m > MAX_CELLS) return null;
3115
- const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
3116
- for (let i2 = n - 1; i2 >= 0; i2--) {
3117
- for (let j2 = m - 1; j2 >= 0; j2--) {
3118
- dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
3119
- }
3120
- }
3121
- const matches = /* @__PURE__ */ new Map();
3122
- let i = 0;
3123
- let j = 0;
3124
- while (i < n && j < m) {
3125
- if (a[i] === b[j]) {
3126
- matches.set(i, j);
3127
- i++;
3128
- j++;
3129
- } else if (dp[i + 1][j] >= dp[i][j + 1]) i++;
3130
- else j++;
3131
- }
3132
- return matches;
3133
- }
3134
- var same = (x, y) => x.length === y.length && x.every((v, i) => v === y[i]);
3135
- function mergeBlocks(base, local, incoming) {
3136
- if (same(local, base)) return { blocks: incoming, conflicts: 0 };
3137
- if (same(incoming, base) || same(local, incoming)) return { blocks: local, conflicts: 0 };
3138
- const ml = matchIndices(base, local);
3139
- const mn = matchIndices(base, incoming);
3140
- if (!ml || !mn) return { blocks: null, conflicts: 1 };
3141
- const anchors = [...ml.keys()].filter((i) => mn.has(i)).sort((x, y) => x - y);
3142
- const out = [];
3143
- let conflicts = 0;
3144
- let pb = -1;
3145
- let pl = -1;
3146
- let pn = -1;
3147
- const region = (bEnd, lEnd, nEnd) => {
3148
- const B = base.slice(pb + 1, bEnd);
3149
- const L = local.slice(pl + 1, lEnd);
3150
- const N = incoming.slice(pn + 1, nEnd);
3151
- if (B.length === L.length && L.length === N.length) {
3152
- for (let k = 0; k < B.length; k++) {
3153
- if (L[k] === B[k]) out.push(N[k]);
3154
- else if (N[k] === B[k] || N[k] === L[k]) out.push(L[k]);
3155
- else {
3156
- conflicts++;
3157
- out.push(L[k]);
3158
- }
3159
- }
3160
- return;
3161
- }
3162
- if (same(L, B)) out.push(...N);
3163
- else if (same(N, B) || same(L, N)) out.push(...L);
3164
- else {
3165
- conflicts++;
3166
- out.push(...L);
3167
- }
3168
- };
3169
- for (const a of anchors) {
3170
- const la = ml.get(a);
3171
- const na = mn.get(a);
3172
- region(a, la, na);
3173
- out.push(local[la]);
3174
- pb = a;
3175
- pl = la;
3176
- pn = na;
3177
- }
3178
- region(base.length, local.length, incoming.length);
3179
- return { blocks: out, conflicts };
3180
- }
3181
- function mergeDocuments(base, local, incoming) {
3182
- const toBlocks = (d) => (d.content ?? []).map((node) => JSON.stringify(node));
3183
- const { blocks: blocks2, conflicts } = mergeBlocks(toBlocks(base), toBlocks(local), toBlocks(incoming));
3184
- if (!blocks2) return { doc: local, conflicts: Math.max(1, conflicts) };
3185
- if (blocks2.length === 0) return { doc: emptyDoc(), conflicts };
3186
- return { doc: { type: "doc", content: blocks2.map((b) => JSON.parse(b)) }, conflicts };
3187
- }
3188
-
3189
- // src/components/document-editor/document-store.ts
3190
- init_document_utils();
3191
- var incomingKey = (doc) => JSON.stringify([doc.title, docKey(doc.json)]);
3192
- function createDocumentStore() {
3193
- const records = /* @__PURE__ */ new Map();
3194
- const listeners = /* @__PURE__ */ new Set();
3195
- const emit = () => listeners.forEach((listener) => listener());
3196
- const put = (record) => {
3197
- records.set(record.id, record);
3198
- emit();
3199
- };
3200
- const fresh2 = (doc) => ({
3201
- id: doc.id,
3202
- title: doc.title,
3203
- documentType: doc.documentType,
3204
- status: doc.status,
3205
- json: doc.json,
3206
- baseJson: doc.json,
3207
- baseTitle: doc.title,
3208
- lastIncomingKey: incomingKey(doc),
3209
- revision: 0,
3210
- origin: "external",
3211
- pending: null,
3212
- updatedAt: Date.now()
3213
- });
3214
- return {
3215
- get: (id) => records.get(id),
3216
- list: () => [...records.values()],
3217
- subscribe: (listener) => {
3218
- listeners.add(listener);
3219
- return () => {
3220
- listeners.delete(listener);
3221
- };
3222
- },
3223
- receive(incoming) {
3224
- const current = records.get(incoming.id);
3225
- if (!current) {
3226
- put(fresh2(incoming));
3227
- return "created";
3228
- }
3229
- const key = incomingKey(incoming);
3230
- if (current.lastIncomingKey === key) return "unchanged";
3231
- const userEditedBody = docKey(current.json) !== docKey(current.baseJson);
3232
- const userEditedTitle = current.title !== current.baseTitle;
3233
- const title = userEditedTitle ? current.title : incoming.title;
3234
- const meta = {
3235
- documentType: incoming.documentType ?? current.documentType,
3236
- status: incoming.status ?? current.status
3237
- };
3238
- if (!userEditedBody) {
3239
- put({
3240
- ...current,
3241
- ...meta,
3242
- title,
3243
- baseTitle: incoming.title,
3244
- json: incoming.json,
3245
- baseJson: incoming.json,
3246
- lastIncomingKey: key,
3247
- revision: current.revision + 1,
3248
- origin: "external",
3249
- pending: null,
3250
- updatedAt: Date.now()
3251
- });
3252
- return "applied";
3253
- }
3254
- const { doc, conflicts } = mergeDocuments(current.baseJson, current.json, incoming.json);
3255
- if (conflicts === 0) {
3256
- put({
3257
- ...current,
3258
- ...meta,
3259
- title,
3260
- baseTitle: incoming.title,
3261
- json: doc,
3262
- baseJson: incoming.json,
3263
- lastIncomingKey: key,
3264
- revision: current.revision + 1,
3265
- origin: "external",
3266
- pending: null,
3267
- updatedAt: Date.now()
3268
- });
3269
- return "merged";
3270
- }
3271
- put({
3272
- ...current,
3273
- ...meta,
3274
- lastIncomingKey: key,
3275
- pending: { json: incoming.json, title: incoming.title, merged: doc, conflicts }
3276
- });
3277
- return "conflict";
3278
- },
3279
- hydrate(doc) {
3280
- put(fresh2(doc));
3281
- },
3282
- edit(id, json, origin) {
3283
- const current = records.get(id);
3284
- if (!current || docKey(current.json) === docKey(json)) return;
3285
- put({ ...current, json, origin, revision: current.revision + 1, updatedAt: Date.now() });
3286
- },
3287
- setTitle(id, title) {
3288
- const current = records.get(id);
3289
- if (!current || current.title === title) return;
3290
- put({ ...current, title, updatedAt: Date.now() });
3291
- },
3292
- resolve(id, choice) {
3293
- const current = records.get(id);
3294
- const pending = current?.pending;
3295
- if (!current || !pending) return;
3296
- if (choice === "keep") {
3297
- put({ ...current, baseJson: pending.json, baseTitle: pending.title, pending: null });
3298
- return;
3299
- }
3300
- const json = choice === "merge" ? pending.merged : pending.json;
3301
- const title = choice === "merge" ? current.title : pending.title;
3302
- put({
3303
- ...current,
3304
- title,
3305
- json,
3306
- baseJson: pending.json,
3307
- baseTitle: pending.title,
3308
- revision: current.revision + 1,
3309
- origin: "external",
3310
- pending: null,
3311
- updatedAt: Date.now()
3312
- });
3313
- },
3314
- remove(id) {
3315
- if (records.delete(id)) emit();
3316
- }
3317
- };
3318
- }
3319
- var defaultStore = null;
3320
- function getDefaultDocumentStore() {
3321
- if (!defaultStore) defaultStore = createDocumentStore();
3322
- return defaultStore;
3323
- }
3324
-
3325
3707
  // src/components/document-editor/document-types.ts
3326
3708
  var ALL_EXPORT_FORMATS = [
3327
3709
  "html",
@@ -3431,8 +3813,13 @@ function DocumentEditorImpl(props) {
3431
3813
  saveRef.current = async () => {
3432
3814
  const record = store.get(documentId);
3433
3815
  const onSave = optionsRef.current.onSave;
3434
- if (!record || !onSave) return;
3816
+ if (!record) return;
3435
3817
  const key = JSON.stringify([record.title, docKey(record.json)]);
3818
+ if (!onSave) {
3819
+ lastSavedKey.current = key;
3820
+ setSaveState("saved");
3821
+ return;
3822
+ }
3436
3823
  if (key === lastSavedKey.current) {
3437
3824
  setSaveState("saved");
3438
3825
  return;
@@ -3453,11 +3840,22 @@ function DocumentEditorImpl(props) {
3453
3840
  optionsRef.current.onError?.(toJuneError(error, "DOCUMENT_ERROR", "Saving the document failed"));
3454
3841
  }
3455
3842
  };
3456
- const markDirty = (0, import_react16.useCallback)(() => {
3457
- if (!optionsRef.current.onSave) return;
3458
- setSaveState("dirty");
3459
- autosave?.();
3460
- }, [autosave]);
3843
+ const markDirty = (0, import_react16.useCallback)(
3844
+ (userEdit = true) => {
3845
+ if (!optionsRef.current.onSave) {
3846
+ if (userEdit) setSaveState("dirty");
3847
+ return;
3848
+ }
3849
+ setSaveState("dirty");
3850
+ autosave?.();
3851
+ },
3852
+ [autosave]
3853
+ );
3854
+ (0, import_react16.useEffect)(() => {
3855
+ if (saveState !== "saved" || options.onSave) return;
3856
+ const t = setTimeout(() => setSaveState((s) => s === "saved" ? "idle" : s), 2500);
3857
+ return () => clearTimeout(t);
3858
+ }, [saveState, options.onSave]);
3461
3859
  const [initialJson] = (0, import_react16.useState)(() => store.get(documentId)?.json ?? parsed.json ?? emptyDoc());
3462
3860
  const extensions = (0, import_react16.useMemo)(() => createDocumentExtensions(), []);
3463
3861
  const onUpdateRef = (0, import_react16.useRef)(() => {
@@ -3483,10 +3881,12 @@ function DocumentEditorImpl(props) {
3483
3881
  onUpdate: ({ editor: ed }) => onUpdateRef.current(ed.getJSON())
3484
3882
  });
3485
3883
  (0, import_react16.useEffect)(() => {
3486
- editor?.setEditable(editable);
3884
+ if (!editor || editor.isEditable === editable) return;
3885
+ editor.setEditable(editable);
3487
3886
  }, [editor, editable]);
3488
3887
  (0, import_react16.useEffect)(() => {
3489
3888
  if (!parsed.json) return;
3889
+ if (isDocumentEcho(store, documentId, contentKey)) return;
3490
3890
  const outcome = store.receive({
3491
3891
  id: documentId,
3492
3892
  title: titleProp,
@@ -3496,7 +3896,7 @@ function DocumentEditorImpl(props) {
3496
3896
  });
3497
3897
  if (outcome === "applied" || outcome === "merged") {
3498
3898
  emitChange(outcome === "applied" ? "ai" : "merge");
3499
- markDirty();
3899
+ markDirty(false);
3500
3900
  } else if (outcome === "conflict") {
3501
3901
  const record = store.get(documentId);
3502
3902
  optionsRef.current.onConflict?.({
@@ -3611,6 +4011,7 @@ function DocumentEditorImpl(props) {
3611
4011
  onTitle: handleTitle,
3612
4012
  exportFormats: formats,
3613
4013
  canSave: !!options.onSave,
4014
+ showSave: !!options.onSave || editable && saveState !== "idle",
3614
4015
  saveState,
3615
4016
  onExport: (f) => void handleExport(f),
3616
4017
  onCopy: () => void handleCopy(),
@@ -3821,17 +4222,21 @@ function JuneUI({ library = juneLibrary, ...props }) {
3821
4222
  getDefaultDocumentStore,
3822
4223
  getDocumentEditorInstructions,
3823
4224
  getDocumentEditorRules,
4225
+ isDocumentEcho,
3824
4226
  isJuneError,
3825
4227
  jsonFieldExtractor,
3826
4228
  juneLibrary,
3827
4229
  lightTheme,
3828
4230
  listDocumentExporters,
3829
4231
  mergeDocuments,
4232
+ readDocumentFromProgram,
3830
4233
  registerDocumentExporter,
4234
+ rememberDocumentEcho,
3831
4235
  runAction,
3832
4236
  serializeDocumentsForAgent,
3833
4237
  sseToText,
3834
4238
  toJuneError,
4239
+ updateDocumentInProgram,
3835
4240
  useDocumentOptions,
3836
4241
  useJuneRuntime,
3837
4242
  useJuneStream,