@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/CHANGELOG.md +7 -0
- package/dist/{chunk-CZE533PM.js → chunk-VFZI3SEL.js} +186 -9
- package/dist/{document-engine-IAY2T6B3.js → document-engine-RKD5SLXR.js} +1 -1
- package/dist/index.cjs +685 -280
- package/dist/index.d.cts +44 -1
- package/dist/index.d.ts +44 -1
- package/dist/index.js +532 -309
- package/dist/june.css +43 -19
- package/package.json +1 -1
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
|
|
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 =
|
|
358
|
+
const decorations = import_view2.DecorationSet.create(
|
|
181
359
|
doc,
|
|
182
360
|
matches.map(
|
|
183
|
-
(m, i) =>
|
|
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
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
searchKey = new
|
|
237
|
-
DocumentSearch =
|
|
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
|
|
419
|
+
new import_state2.Plugin({
|
|
242
420
|
key: searchKey,
|
|
243
421
|
state: {
|
|
244
|
-
init: () => ({ term: "", caseSensitive: false, current: -1, matches: [], decorations:
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
const
|
|
2402
|
-
const
|
|
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
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
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
|
-
|
|
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,
|
|
2426
|
-
const
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
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
|
|
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,
|