@agimon-ai/po-author 0.0.0-alpha.40 → 0.0.0-alpha.41
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/extensions/web.mjs +964 -457
- package/dist/extensions/web.mjs.map +1 -1
- package/package.json +8 -8
package/dist/extensions/web.mjs
CHANGED
|
@@ -606,6 +606,118 @@ function resolveAuthorGridCell(sessionId, cell, expectedGeometryToken) {
|
|
|
606
606
|
}
|
|
607
607
|
};
|
|
608
608
|
}
|
|
609
|
+
//#endregion
|
|
610
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/authorMarkdownText.ts
|
|
611
|
+
const HEADING = /^#{1,6} /;
|
|
612
|
+
const LIST_ITEM = /^(?:[-*+]|\d+[.)]) /;
|
|
613
|
+
/**
|
|
614
|
+
* One formatting edit for the selected source range. Bold, Heading and List toggle off when already applied;
|
|
615
|
+
* Heading and List apply to every touched line and replace each other.
|
|
616
|
+
*/
|
|
617
|
+
function authorMarkdownFormat(content, from, to, format) {
|
|
618
|
+
if (format === "bold") {
|
|
619
|
+
const selected = content.slice(from, to);
|
|
620
|
+
if (selected.length >= 4 && selected.startsWith("**") && selected.endsWith("**")) return {
|
|
621
|
+
from,
|
|
622
|
+
to,
|
|
623
|
+
insert: selected.slice(2, -2),
|
|
624
|
+
select: {
|
|
625
|
+
from,
|
|
626
|
+
to: to - 4
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
if (from >= 2 && content.slice(from - 2, from) === "**" && content.slice(to, to + 2) === "**") return {
|
|
630
|
+
from: from - 2,
|
|
631
|
+
to: to + 2,
|
|
632
|
+
insert: selected,
|
|
633
|
+
select: {
|
|
634
|
+
from: from - 2,
|
|
635
|
+
to: to - 2
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
const text = selected || "bold text";
|
|
639
|
+
return {
|
|
640
|
+
from,
|
|
641
|
+
to,
|
|
642
|
+
insert: `**${text}**`,
|
|
643
|
+
select: {
|
|
644
|
+
from: from + 2,
|
|
645
|
+
to: from + 2 + text.length
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
if (format === "link") {
|
|
650
|
+
const text = content.slice(from, to) || "link text";
|
|
651
|
+
const url = from + text.length + 3;
|
|
652
|
+
return {
|
|
653
|
+
from,
|
|
654
|
+
to,
|
|
655
|
+
insert: `[${text}](https://)`,
|
|
656
|
+
select: {
|
|
657
|
+
from: url,
|
|
658
|
+
to: url + 8
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const start = content.lastIndexOf("\n", from - 1) + 1;
|
|
663
|
+
const last = to > from && content[to - 1] === "\n" ? to - 1 : to;
|
|
664
|
+
const lineEnd = content.indexOf("\n", Math.max(last, start));
|
|
665
|
+
const end = lineEnd === -1 ? content.length : lineEnd;
|
|
666
|
+
const own = format === "heading" ? HEADING : LIST_ITEM;
|
|
667
|
+
const prefix = format === "heading" ? "## " : "- ";
|
|
668
|
+
const lines = content.slice(start, end).split("\n");
|
|
669
|
+
const written = lines.filter((line) => line.trim() !== "");
|
|
670
|
+
const remove = written.length > 0 && written.every((line) => own.test(line));
|
|
671
|
+
const insert = lines.map((line) => line.trim() === "" ? line : remove ? line.replace(own, "") : prefix + line.replace(HEADING, "").replace(LIST_ITEM, "")).join("\n");
|
|
672
|
+
return {
|
|
673
|
+
from: start,
|
|
674
|
+
to: end,
|
|
675
|
+
insert,
|
|
676
|
+
select: {
|
|
677
|
+
from: start,
|
|
678
|
+
to: start + insert.length
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Moves a source range through one edit from `before` to `after`. Text typed just outside the range stays
|
|
684
|
+
* outside; text replaced inside it widens the range to the new text, so a note follows the passage it names.
|
|
685
|
+
*/
|
|
686
|
+
function rebaseTextRange(before, after, start, end) {
|
|
687
|
+
const limit = Math.min(before.length, after.length);
|
|
688
|
+
let prefix = 0;
|
|
689
|
+
while (prefix < limit && before[prefix] === after[prefix]) prefix += 1;
|
|
690
|
+
let suffix = 0;
|
|
691
|
+
while (suffix < limit - prefix && before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) suffix += 1;
|
|
692
|
+
const changedEnd = before.length - suffix;
|
|
693
|
+
const delta = after.length - before.length;
|
|
694
|
+
const startAt = start < prefix ? start : start >= changedEnd ? start + delta : prefix;
|
|
695
|
+
const endAt = end <= prefix ? end : end >= changedEnd ? end + delta : after.length - suffix;
|
|
696
|
+
return {
|
|
697
|
+
start: startAt,
|
|
698
|
+
end: Math.max(startAt, endAt)
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Where each rendered text run starts in the Markdown source, searched in reading order.
|
|
703
|
+
* Undefined when a run is blank or not verbatim source (entities, generated labels).
|
|
704
|
+
*/
|
|
705
|
+
function locateRenderedRuns(source, runs) {
|
|
706
|
+
let cursor = 0;
|
|
707
|
+
return runs.map((run) => {
|
|
708
|
+
if (run.trim() === "") return void 0;
|
|
709
|
+
const whole = source.indexOf(run, cursor);
|
|
710
|
+
const first = run.trimStart().split("\n")[0];
|
|
711
|
+
const at = whole === -1 ? source.indexOf(first, cursor) : whole;
|
|
712
|
+
if (at === -1) return void 0;
|
|
713
|
+
cursor = at + (whole === -1 ? first.length : run.length);
|
|
714
|
+
return at;
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
/** One-based source line of an offset. */
|
|
718
|
+
function authorSourceLine(source, offset) {
|
|
719
|
+
return source.slice(0, offset).split("\n").length;
|
|
720
|
+
}
|
|
609
721
|
const TERMINAL_REQUEST_STATUSES = /* @__PURE__ */ new Set([
|
|
610
722
|
"COMPLETE",
|
|
611
723
|
"FAILED",
|
|
@@ -749,28 +861,40 @@ function putAuthorDocument(sessionId, input) {
|
|
|
749
861
|
staleThumbnails.forEach(revokeThumbnail);
|
|
750
862
|
return result;
|
|
751
863
|
}
|
|
752
|
-
/**
|
|
864
|
+
/**
|
|
865
|
+
* Claims focus and returns a generation token that makes cleanup race-safe.
|
|
866
|
+
* Claims stack: a popup review over a document tab hands focus back to the tab
|
|
867
|
+
* when it closes, so the Author dock face stays for the document still shown.
|
|
868
|
+
*/
|
|
753
869
|
function focusAuthorDocument(sessionId, path) {
|
|
754
870
|
let generation = 0;
|
|
755
871
|
updateSession(sessionId, (session) => {
|
|
756
872
|
generation = session.generation + 1;
|
|
873
|
+
const focusedDocument = {
|
|
874
|
+
path: normalizeAuthorPath(path),
|
|
875
|
+
generation,
|
|
876
|
+
focusedAt: Date.now()
|
|
877
|
+
};
|
|
757
878
|
return {
|
|
758
879
|
...session,
|
|
759
880
|
generation,
|
|
760
|
-
focusedDocument
|
|
761
|
-
|
|
762
|
-
generation,
|
|
763
|
-
focusedAt: Date.now()
|
|
764
|
-
}
|
|
881
|
+
focusedDocument,
|
|
882
|
+
focusClaims: [...session.focusClaims ?? [], focusedDocument]
|
|
765
883
|
};
|
|
766
884
|
});
|
|
767
885
|
return generation;
|
|
768
886
|
}
|
|
769
887
|
function releaseAuthorDocumentFocus(sessionId, generation) {
|
|
770
|
-
updateSession(sessionId, (session) =>
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
888
|
+
updateSession(sessionId, (session) => {
|
|
889
|
+
const claims = session.focusClaims ?? [];
|
|
890
|
+
if (!claims.some((claim) => claim.generation === generation)) return session;
|
|
891
|
+
const focusClaims = claims.filter((claim) => claim.generation !== generation);
|
|
892
|
+
return {
|
|
893
|
+
...session,
|
|
894
|
+
focusedDocument: focusClaims.at(-1),
|
|
895
|
+
focusClaims
|
|
896
|
+
};
|
|
897
|
+
});
|
|
774
898
|
}
|
|
775
899
|
function seekAuthorVideo(sessionId, path, timeSeconds) {
|
|
776
900
|
const normalized = normalizeAuthorPath(path);
|
|
@@ -1017,12 +1141,14 @@ function updateDocument(sessionId, path, update) {
|
|
|
1017
1141
|
const next = update(document);
|
|
1018
1142
|
const session = state.sessions[sessionId];
|
|
1019
1143
|
const identityChanged = document.version !== next.version || document.sourceSha256 !== next.sourceSha256;
|
|
1020
|
-
const
|
|
1144
|
+
const current = identityChanged ? session?.annotationsByDocument[document.path] : void 0;
|
|
1145
|
+
const rebased = current && rebaseTextCollection(current, document, next);
|
|
1146
|
+
const collection = rebased === void 0 ? current : void 0;
|
|
1021
1147
|
if (collection !== void 0) {
|
|
1022
1148
|
if (collection.candidate?.thumbnailUrl !== void 0) staleThumbnails.push(collection.candidate.thumbnailUrl);
|
|
1023
1149
|
staleThumbnails.push(...collection.annotations.flatMap((annotation) => annotation.thumbnailUrl === void 0 ? [] : [annotation.thumbnailUrl]));
|
|
1024
1150
|
}
|
|
1025
|
-
const staleCollection = identityChanged ? staleAuthorCollection(collection, Date.now()) : void 0;
|
|
1151
|
+
const staleCollection = rebased ?? (identityChanged ? staleAuthorCollection(collection, Date.now()) : void 0);
|
|
1026
1152
|
const sessions = identityChanged && session !== void 0 ? {
|
|
1027
1153
|
...state.sessions,
|
|
1028
1154
|
[sessionId]: {
|
|
@@ -1091,6 +1217,40 @@ function staleAuthorCollection(collection, updatedAt) {
|
|
|
1091
1217
|
updatedAt
|
|
1092
1218
|
};
|
|
1093
1219
|
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Carries unsent text notes through a local edit or save instead of dropping them: each range follows its
|
|
1222
|
+
* passage and its quote is re-read. Undefined when the collection is already stale or holds a non-text anchor.
|
|
1223
|
+
*/
|
|
1224
|
+
function rebaseTextCollection(collection, before, after) {
|
|
1225
|
+
if (collection.stale || collection.revision !== before.version || collection.sourceSha256 !== before.sourceSha256 || before.content === void 0 || after.content === void 0) return void 0;
|
|
1226
|
+
if ([...collection.annotations, ...collection.candidate ? [collection.candidate] : []].some((item) => item.anchor.kind !== "text-range")) return void 0;
|
|
1227
|
+
const source = after.content;
|
|
1228
|
+
const rebase = (item) => {
|
|
1229
|
+
const anchor = item.anchor;
|
|
1230
|
+
const { start, end } = rebaseTextRange(before.content, source, anchor.startOffset, anchor.endOffset);
|
|
1231
|
+
return {
|
|
1232
|
+
...copyCandidate(item),
|
|
1233
|
+
revision: after.version,
|
|
1234
|
+
sourceSha256: after.sourceSha256,
|
|
1235
|
+
quote: source.slice(start, end),
|
|
1236
|
+
anchor: {
|
|
1237
|
+
...anchor,
|
|
1238
|
+
startOffset: start,
|
|
1239
|
+
endOffset: end,
|
|
1240
|
+
startLine: authorSourceLine(source, start),
|
|
1241
|
+
endLine: authorSourceLine(source, end)
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
};
|
|
1245
|
+
return {
|
|
1246
|
+
...collection,
|
|
1247
|
+
revision: after.version,
|
|
1248
|
+
sourceSha256: after.sourceSha256,
|
|
1249
|
+
candidate: collection.candidate && rebase(collection.candidate),
|
|
1250
|
+
annotations: collection.annotations.map(rebase),
|
|
1251
|
+
updatedAt: Date.now()
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1094
1254
|
function updateCollection(sessionId, path, update) {
|
|
1095
1255
|
const normalized = normalizeAuthorPath(path);
|
|
1096
1256
|
const document = authorDocument(sessionId, normalized);
|
|
@@ -1172,7 +1332,9 @@ function revokeThumbnail(url) {
|
|
|
1172
1332
|
if (!url.startsWith("blob:")) return;
|
|
1173
1333
|
try {
|
|
1174
1334
|
URL.revokeObjectURL(url);
|
|
1175
|
-
} catch {
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
console.warn(`Author could not release a thumbnail: ${error instanceof Error ? error.message : String(error)}`);
|
|
1337
|
+
}
|
|
1176
1338
|
}
|
|
1177
1339
|
//#endregion
|
|
1178
1340
|
//#region src/extensions/workspaces/sessions/(frontend)/channel/_lib/authorStore.ts
|
|
@@ -1217,11 +1379,6 @@ var author_webmcp_web_default = defineChannelFile({
|
|
|
1217
1379
|
}
|
|
1218
1380
|
});
|
|
1219
1381
|
//#endregion
|
|
1220
|
-
//#region src/constants/author.ts
|
|
1221
|
-
const AUTHOR_FILE_TAB_PREFIX = "author-file-";
|
|
1222
|
-
/** Names a file in the session's artifact folder rather than the working tree. Artifacts open read-only. */
|
|
1223
|
-
const AUTHOR_ARTIFACT_PREFIX = "artifact:";
|
|
1224
|
-
//#endregion
|
|
1225
1382
|
//#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorGridOverlay.tsx
|
|
1226
1383
|
function autonomousVoiceGridVisible(statuses) {
|
|
1227
1384
|
return statuses["po-voice"]?.trimStart().startsWith("voice auto:") === true;
|
|
@@ -1690,6 +1847,10 @@ function AuthorRegionDrafts({ sessionId, path, drafts }) {
|
|
|
1690
1847
|
" as an unsent annotation."
|
|
1691
1848
|
]
|
|
1692
1849
|
}),
|
|
1850
|
+
drafts.candidate.quote ? /* @__PURE__ */ jsx("blockquote", {
|
|
1851
|
+
className: "line-clamp-3 border-l-2 border-po-edge-magenta pl-2 text-base text-po-text sm:text-sm",
|
|
1852
|
+
children: drafts.candidate.quote
|
|
1853
|
+
}) : null,
|
|
1693
1854
|
drafts.candidate.anchor.kind === "video-time-rect" || drafts.candidate.anchor.kind === "video-time-point" ? /* @__PURE__ */ jsxs("p", {
|
|
1694
1855
|
className: "text-base text-po-dim sm:text-sm",
|
|
1695
1856
|
children: [
|
|
@@ -1761,8 +1922,8 @@ function AuthorRegionDrafts({ sessionId, path, drafts }) {
|
|
|
1761
1922
|
}),
|
|
1762
1923
|
region.comment,
|
|
1763
1924
|
/* @__PURE__ */ jsx("span", {
|
|
1764
|
-
className: "block text-sm text-po-dim",
|
|
1765
|
-
children: region.mode === "point" ? "Point" : "Region"
|
|
1925
|
+
className: "line-clamp-2 block text-sm text-po-dim",
|
|
1926
|
+
children: region.quote ? `“${region.quote}”` : region.mode === "point" ? "Point" : "Region"
|
|
1766
1927
|
})
|
|
1767
1928
|
]
|
|
1768
1929
|
}),
|
|
@@ -2067,6 +2228,191 @@ function AuthorRequestLog({ requests }) {
|
|
|
2067
2228
|
});
|
|
2068
2229
|
}
|
|
2069
2230
|
//#endregion
|
|
2231
|
+
//#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorToolPalette.tsx
|
|
2232
|
+
/** One tool tile. An active annotation tool is the person's mark (rose); an active view tool is Po's jade. */
|
|
2233
|
+
function authorToolClassName(active, annotates) {
|
|
2234
|
+
return `h-auto min-h-11 min-w-11 flex-col gap-1 rounded-md px-1 py-2 text-base [@media(pointer:fine)]:min-h-8 ${!active ? "border-po-border-soft bg-po-panel text-po-dim" : annotates ? "border-po-edge-magenta bg-po-tint-magenta font-semibold text-po-magenta hover:text-po-magenta" : "border-po-edge-green bg-po-tint-green font-semibold text-po-green hover:text-po-green"}`;
|
|
2235
|
+
}
|
|
2236
|
+
function AuthorToolPalette({ sessionId, path, kind, activeTool }) {
|
|
2237
|
+
const text = kind === "markdown" || kind === "text";
|
|
2238
|
+
const tools = [
|
|
2239
|
+
...[
|
|
2240
|
+
"image",
|
|
2241
|
+
"pdf",
|
|
2242
|
+
"video",
|
|
2243
|
+
"story-preview"
|
|
2244
|
+
].includes(kind) ? [{
|
|
2245
|
+
label: "Pan",
|
|
2246
|
+
glyph: "✋",
|
|
2247
|
+
action: () => setAuthorToolMode(sessionId, path, activeTool === "pan" ? "select" : "pan"),
|
|
2248
|
+
active: activeTool === "pan",
|
|
2249
|
+
annotates: false
|
|
2250
|
+
}] : [],
|
|
2251
|
+
...[
|
|
2252
|
+
"image",
|
|
2253
|
+
"pdf",
|
|
2254
|
+
"video",
|
|
2255
|
+
"story-preview",
|
|
2256
|
+
"html"
|
|
2257
|
+
].includes(kind) ? [{
|
|
2258
|
+
label: "Draw",
|
|
2259
|
+
glyph: "✎",
|
|
2260
|
+
action: () => setAuthorToolMode(sessionId, path, activeTool === "draw" ? "select" : "draw"),
|
|
2261
|
+
active: activeTool === "draw",
|
|
2262
|
+
annotates: true
|
|
2263
|
+
}] : [],
|
|
2264
|
+
{
|
|
2265
|
+
label: "Region",
|
|
2266
|
+
glyph: "⌗",
|
|
2267
|
+
action: () => setAuthorToolMode(sessionId, path, activeTool === "mark" ? "select" : "mark"),
|
|
2268
|
+
active: activeTool === "mark",
|
|
2269
|
+
annotates: true
|
|
2270
|
+
},
|
|
2271
|
+
{
|
|
2272
|
+
label: "Comment",
|
|
2273
|
+
glyph: "●",
|
|
2274
|
+
action: () => setAuthorToolMode(sessionId, path, activeTool === "comment" ? "select" : "comment"),
|
|
2275
|
+
active: activeTool === "comment",
|
|
2276
|
+
annotates: true
|
|
2277
|
+
}
|
|
2278
|
+
];
|
|
2279
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
2280
|
+
"data-testid": "author-tool-palette",
|
|
2281
|
+
className: "space-y-2",
|
|
2282
|
+
children: [
|
|
2283
|
+
/* @__PURE__ */ jsx("h3", {
|
|
2284
|
+
className: "sr-only",
|
|
2285
|
+
children: "Tools"
|
|
2286
|
+
}),
|
|
2287
|
+
/* @__PURE__ */ jsx("div", {
|
|
2288
|
+
className: "grid grid-cols-4 gap-1.5",
|
|
2289
|
+
children: tools.map((tool) => /* @__PURE__ */ jsxs(Button, {
|
|
2290
|
+
size: "md",
|
|
2291
|
+
variant: "outline",
|
|
2292
|
+
"aria-label": tool.label === "Region" ? "mark region" : tool.label,
|
|
2293
|
+
"aria-pressed": tool.active,
|
|
2294
|
+
onClick: tool.action,
|
|
2295
|
+
className: authorToolClassName(tool.active === true, tool.annotates === true),
|
|
2296
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2297
|
+
"aria-hidden": "true",
|
|
2298
|
+
className: "text-lg leading-none",
|
|
2299
|
+
children: tool.glyph
|
|
2300
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2301
|
+
className: "text-sm",
|
|
2302
|
+
children: tool.label
|
|
2303
|
+
})]
|
|
2304
|
+
}, tool.label))
|
|
2305
|
+
}),
|
|
2306
|
+
/* @__PURE__ */ jsx("p", {
|
|
2307
|
+
className: "text-base leading-normal text-po-dim sm:text-sm",
|
|
2308
|
+
children: activeTool === "draw" ? "Draw a location mark, then enter feedback text. Marks never edit the source." : activeTool === "pan" ? "Drag to pan; pinch to zoom. Use Fit to reset the view." : activeTool === "mark" ? text ? "Drag over the passage you want to mark. Choose Region again to stop." : "Drag over the document to mark a region." : activeTool === "comment" ? text ? "Click a paragraph or line to comment on it. Choose Comment again to stop." : "Click the document to place a comment pin." : text ? "Select text to comment on it, or choose Region or Comment." : "Choose Region to drag an area, or Comment to place a point."
|
|
2309
|
+
})
|
|
2310
|
+
]
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
//#endregion
|
|
2314
|
+
//#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorPanel.tsx
|
|
2315
|
+
function AuthorPanel({ sessionId, submitCapture, statuses, renderSlot, renderSessionActivity }) {
|
|
2316
|
+
const documents = useStore(authorWorkspace.store, (state) => {
|
|
2317
|
+
if (sessionId === null) return [];
|
|
2318
|
+
const prefix = `${sessionId}\n`;
|
|
2319
|
+
return Object.entries(state.documents).filter(([key]) => key.startsWith(prefix)).map(([, document]) => document);
|
|
2320
|
+
});
|
|
2321
|
+
const workspace = useStore(authorWorkspace.store, (state) => sessionId === null ? void 0 : state.sessions[sessionId]);
|
|
2322
|
+
const grid = useStore(authorGrid.store, (state) => sessionId === null ? void 0 : state.sessions[sessionId]);
|
|
2323
|
+
const focused = documents.find((document) => document.path === workspace?.focusedDocument?.path);
|
|
2324
|
+
const focusedPath = workspace?.focusedDocument?.path;
|
|
2325
|
+
const requests = (workspace?.requests ?? []).filter((request) => focusedPath === void 0 || request.documentPath === focusedPath);
|
|
2326
|
+
const drafts = useStore(authorWorkspace.store, (state) => authorDocumentAnnotations(sessionId, focusedPath ?? "", state));
|
|
2327
|
+
const activity = sessionId !== null && focusedPath !== void 0 && requests.some((request) => request.status === "REQUESTED" || request.status === "CHANGING") ? renderSessionActivity?.() ?? null : null;
|
|
2328
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
2329
|
+
"data-testid": "author-panel",
|
|
2330
|
+
className: "flex min-h-0 flex-1 flex-col overflow-hidden",
|
|
2331
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2332
|
+
className: "min-h-0 flex-1 overflow-y-auto p-3",
|
|
2333
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2334
|
+
className: "flex flex-col gap-3",
|
|
2335
|
+
children: [
|
|
2336
|
+
/* @__PURE__ */ jsx("div", {
|
|
2337
|
+
"data-testid": "author-preview-providers",
|
|
2338
|
+
className: "empty:hidden",
|
|
2339
|
+
children: renderSlot("author.preview-provider")
|
|
2340
|
+
}),
|
|
2341
|
+
sessionId !== null && workspace !== void 0 && focused !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2342
|
+
/* @__PURE__ */ jsx(AuthorToolPalette, {
|
|
2343
|
+
sessionId,
|
|
2344
|
+
path: focused.path,
|
|
2345
|
+
kind: focused.kind,
|
|
2346
|
+
activeTool: workspace.toolsByDocument[normalizeAuthorPath(focused.path)] ?? "select"
|
|
2347
|
+
}),
|
|
2348
|
+
/* @__PURE__ */ jsx("h2", {
|
|
2349
|
+
className: "border-t border-po-border-soft pt-3 text-lg font-semibold text-po-hi",
|
|
2350
|
+
children: "Annotations"
|
|
2351
|
+
}),
|
|
2352
|
+
/* @__PURE__ */ jsxs("p", {
|
|
2353
|
+
className: "text-base leading-normal text-po-dim sm:text-sm",
|
|
2354
|
+
children: [
|
|
2355
|
+
focused.kind === "markdown" || focused.kind === "text" ? "Select text or mark a passage, add feedback, then submit here." : "Drag a region or place a point, add feedback, then submit here.",
|
|
2356
|
+
" ",
|
|
2357
|
+
"Requests queue while the agent is working."
|
|
2358
|
+
]
|
|
2359
|
+
}),
|
|
2360
|
+
focused.kind === "video" ? /* @__PURE__ */ jsx("p", {
|
|
2361
|
+
className: "text-base text-po-dim sm:text-sm",
|
|
2362
|
+
children: "Annotations reference video frames. They do not save changes to the source video."
|
|
2363
|
+
}) : null,
|
|
2364
|
+
autonomousVoiceGridVisible(statuses) && grid !== void 0 ? /* @__PURE__ */ jsxs("div", {
|
|
2365
|
+
"data-testid": "author-grid-snapshot",
|
|
2366
|
+
className: "rounded border border-po-red/40 bg-po-red/5 p-2 text-base text-po-dim",
|
|
2367
|
+
children: [/* @__PURE__ */ jsx("strong", {
|
|
2368
|
+
className: "text-po-red",
|
|
2369
|
+
children: "VOICE GRID A1–H8"
|
|
2370
|
+
}), /* @__PURE__ */ jsxs("p", {
|
|
2371
|
+
className: "mt-1 truncate",
|
|
2372
|
+
children: ["token ", grid.geometryToken]
|
|
2373
|
+
})]
|
|
2374
|
+
}) : null,
|
|
2375
|
+
/* @__PURE__ */ jsx("div", {
|
|
2376
|
+
className: "max-sm:hidden",
|
|
2377
|
+
children: /* @__PURE__ */ jsx(AuthorFeedbackControls, {
|
|
2378
|
+
sessionId,
|
|
2379
|
+
document: focused,
|
|
2380
|
+
drafts,
|
|
2381
|
+
submitCapture
|
|
2382
|
+
}, focused.path)
|
|
2383
|
+
})
|
|
2384
|
+
] }) : null,
|
|
2385
|
+
/* @__PURE__ */ jsx("div", {
|
|
2386
|
+
className: focused === void 0 ? void 0 : "border-t border-po-border-soft pt-3",
|
|
2387
|
+
children: /* @__PURE__ */ jsx(AuthorRequestLog, { requests })
|
|
2388
|
+
})
|
|
2389
|
+
]
|
|
2390
|
+
})
|
|
2391
|
+
}), activity === null ? null : /* @__PURE__ */ jsx("footer", {
|
|
2392
|
+
"data-testid": "author-session-activity",
|
|
2393
|
+
className: "shrink-0 border-t border-po-border bg-po-rail p-3",
|
|
2394
|
+
children: activity
|
|
2395
|
+
})]
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
//#endregion
|
|
2399
|
+
//#region src/extensions/workspaces/sessions/(frontend)/dock/authoring.web.tsx
|
|
2400
|
+
var authoring_web_default = defineDockFace({
|
|
2401
|
+
id: "authoring",
|
|
2402
|
+
label: "Author",
|
|
2403
|
+
order: 30,
|
|
2404
|
+
panel: AuthorPanel,
|
|
2405
|
+
visibility: {
|
|
2406
|
+
subscribe(listener) {
|
|
2407
|
+
const subscription = authorWorkspace.store.subscribe(listener);
|
|
2408
|
+
return () => subscription.unsubscribe();
|
|
2409
|
+
},
|
|
2410
|
+
isVisible(sessionId) {
|
|
2411
|
+
return sessionId !== null && authorWorkspace.store.state.sessions[sessionId]?.focusedDocument !== void 0;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
});
|
|
2415
|
+
//#endregion
|
|
2070
2416
|
//#region src/types/apiRoutes.ts
|
|
2071
2417
|
/**
|
|
2072
2418
|
* This package's routes, as data.
|
|
@@ -2177,20 +2523,46 @@ const api = createApiClient(apiRoutes_default, {
|
|
|
2177
2523
|
sessionAddress: sessionApiAddress
|
|
2178
2524
|
});
|
|
2179
2525
|
//#endregion
|
|
2180
|
-
//#region src/
|
|
2181
|
-
|
|
2182
|
-
|
|
2526
|
+
//#region src/constants/author.ts
|
|
2527
|
+
const AUTHOR_FILE_TAB_PREFIX = "author-file-";
|
|
2528
|
+
/** Names a file in the session's artifact folder rather than the working tree. Artifacts open read-only. */
|
|
2529
|
+
const AUTHOR_ARTIFACT_PREFIX = "artifact:";
|
|
2183
2530
|
//#endregion
|
|
2184
|
-
//#region src/
|
|
2185
|
-
|
|
2186
|
-
"
|
|
2187
|
-
|
|
2188
|
-
"
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
"
|
|
2192
|
-
|
|
2193
|
-
"
|
|
2531
|
+
//#region src/types/authorPreview.ts
|
|
2532
|
+
function parseAuthorOpenSourceAction(input) {
|
|
2533
|
+
if (typeof input !== "object" || input === null) return null;
|
|
2534
|
+
const candidate = input;
|
|
2535
|
+
return candidate.version === 1 && typeof candidate.createTab === "function" ? candidate : null;
|
|
2536
|
+
}
|
|
2537
|
+
function parseAuthorPreviewAction(input) {
|
|
2538
|
+
if (typeof input !== "object" || input === null) return null;
|
|
2539
|
+
const candidate = input;
|
|
2540
|
+
if (candidate.version !== 1 || typeof candidate.label !== "string" || candidate.label.trim() === "") return null;
|
|
2541
|
+
if (candidate.detail !== void 0 && typeof candidate.detail !== "string") return null;
|
|
2542
|
+
if (typeof candidate.createTab !== "function") return null;
|
|
2543
|
+
if (candidate.supportsSource !== void 0 && typeof candidate.supportsSource !== "function") return null;
|
|
2544
|
+
if (candidate.embeddedPanel !== void 0 && typeof candidate.embeddedPanel !== "function") return null;
|
|
2545
|
+
return candidate;
|
|
2546
|
+
}
|
|
2547
|
+
const authorPreviewActionSlot = {
|
|
2548
|
+
slot: "author.preview-action",
|
|
2549
|
+
parse: parseAuthorPreviewAction
|
|
2550
|
+
};
|
|
2551
|
+
//#endregion
|
|
2552
|
+
//#region src/types/authorApi.ts
|
|
2553
|
+
/** Query parameter naming the session a document request belongs to. */
|
|
2554
|
+
const SESSION_QUERY_PARAM = "session";
|
|
2555
|
+
//#endregion
|
|
2556
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/authorFiles.ts
|
|
2557
|
+
const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2558
|
+
"avif",
|
|
2559
|
+
"bmp",
|
|
2560
|
+
"gif",
|
|
2561
|
+
"jpeg",
|
|
2562
|
+
"jpg",
|
|
2563
|
+
"png",
|
|
2564
|
+
"svg",
|
|
2565
|
+
"webp"
|
|
2194
2566
|
]);
|
|
2195
2567
|
const VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2196
2568
|
"m4v",
|
|
@@ -2318,364 +2690,120 @@ async function loadAuthorDocument(sessionId, path, signal) {
|
|
|
2318
2690
|
path,
|
|
2319
2691
|
kind,
|
|
2320
2692
|
title: authorFileName(path),
|
|
2321
|
-
structuredFormat: format,
|
|
2322
|
-
fragments: parsed.fragments,
|
|
2323
|
-
...csvDialect === void 0 ? {} : { csvDialect },
|
|
2324
|
-
originalFragments: parsed.fragments.map((fragment) => ({ ...fragment })),
|
|
2325
|
-
...sourceSha256 === void 0 ? {} : { sourceSha256 }
|
|
2326
|
-
};
|
|
2327
|
-
}
|
|
2328
|
-
const mediaUrl = kind === "image" || kind === "video" || kind === "pdf" || kind === "opaque" ? url : void 0;
|
|
2329
|
-
const content = kind === "text" || kind === "markdown" || kind === "story-preview" || kind === "html" ? await response.text() : void 0;
|
|
2330
|
-
return {
|
|
2331
|
-
path,
|
|
2332
|
-
kind,
|
|
2333
|
-
title: authorFileName(path),
|
|
2334
|
-
...content === void 0 ? {} : { content },
|
|
2335
|
-
...mediaUrl === void 0 ? {} : { mediaUrl },
|
|
2336
|
-
...sourceSha256 === void 0 ? {} : { sourceSha256 }
|
|
2337
|
-
};
|
|
2338
|
-
}
|
|
2339
|
-
function operationsFor(document) {
|
|
2340
|
-
const originals = new Map((document.originalFragments ?? []).map((fragment) => [fragment.id, fragment.text]));
|
|
2341
|
-
return (document.fragments ?? []).filter((fragment) => !fragment.readOnly && originals.get(fragment.id) !== fragment.text).map((fragment) => ({
|
|
2342
|
-
fragmentId: fragment.id,
|
|
2343
|
-
replacement: fragment.text
|
|
2344
|
-
}));
|
|
2345
|
-
}
|
|
2346
|
-
function decodeBase64(value) {
|
|
2347
|
-
if (!/^(?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{2}==|[A-Za-z\d+/]{3}=)?$/.test(value)) throw new Error("Invalid serialized document bytes.");
|
|
2348
|
-
const binary = atob(value);
|
|
2349
|
-
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
2350
|
-
}
|
|
2351
|
-
function imageMimeType(path) {
|
|
2352
|
-
const extension = path.split(".").at(-1)?.toLowerCase();
|
|
2353
|
-
if (extension === "png") return "image/png";
|
|
2354
|
-
if (extension === "jpg" || extension === "jpeg") return "image/jpeg";
|
|
2355
|
-
if (extension === "webp") return "image/webp";
|
|
2356
|
-
}
|
|
2357
|
-
async function croppedImageBytes(documentInput, signal) {
|
|
2358
|
-
const { crop, mediaUrl } = documentInput;
|
|
2359
|
-
if (crop === void 0 || mediaUrl === void 0) throw new Error("This Author image has no crop source.");
|
|
2360
|
-
const mimeType = imageMimeType(documentInput.path);
|
|
2361
|
-
if (mimeType === void 0) throw new Error("This image format cannot preserve its encoding after crop.");
|
|
2362
|
-
const response = await browserTransport.fetch(mediaUrl, {
|
|
2363
|
-
signal,
|
|
2364
|
-
cache: "no-store"
|
|
2365
|
-
});
|
|
2366
|
-
if (!response.ok) throw new Error(`Author could not read the image before saving (${response.status})`);
|
|
2367
|
-
const bitmap = await createImageBitmap(await response.blob());
|
|
2368
|
-
try {
|
|
2369
|
-
const sourceX = Math.round(crop.x * bitmap.width);
|
|
2370
|
-
const sourceY = Math.round(crop.y * bitmap.height);
|
|
2371
|
-
const sourceWidth = Math.max(1, Math.round(crop.width * bitmap.width));
|
|
2372
|
-
const sourceHeight = Math.max(1, Math.round(crop.height * bitmap.height));
|
|
2373
|
-
const canvas = window.document.createElement("canvas");
|
|
2374
|
-
canvas.width = sourceWidth;
|
|
2375
|
-
canvas.height = sourceHeight;
|
|
2376
|
-
const context = canvas.getContext("2d");
|
|
2377
|
-
if (context === null) throw new Error("Canvas is unavailable for image crop.");
|
|
2378
|
-
context.drawImage(bitmap, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, sourceWidth, sourceHeight);
|
|
2379
|
-
const blob = await new Promise((resolve) => canvas.toBlob(resolve, mimeType));
|
|
2380
|
-
if (blob === null || blob.type !== mimeType) throw new Error("The browser cannot encode this cropped image format.");
|
|
2381
|
-
return blob;
|
|
2382
|
-
} finally {
|
|
2383
|
-
bitmap.close();
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
async function bytesToSave(sessionId, document, signal) {
|
|
2387
|
-
if (document.kind === "text" || document.kind === "markdown" || document.kind === "story-preview" || document.kind === "html") return document.content ?? "";
|
|
2388
|
-
if (document.kind === "image" && document.crop !== void 0) return await croppedImageBytes(document, signal);
|
|
2389
|
-
if (document.structuredFormat === void 0) throw new Error("This Author view cannot be saved.");
|
|
2390
|
-
const operations = operationsFor(document);
|
|
2391
|
-
const request = {
|
|
2392
|
-
path: document.path,
|
|
2393
|
-
format: document.structuredFormat,
|
|
2394
|
-
operations,
|
|
2395
|
-
...document.csvDialect === void 0 ? {} : { csvDialect: document.csvDialect }
|
|
2396
|
-
};
|
|
2397
|
-
const preflight = await documentRequest(api.session(sessionId).documentsPreflight, sessionId, request, signal);
|
|
2398
|
-
if (preflight.sourceDigest !== void 0 && preflight.sourceDigest !== document.sourceSha256) throw new Error("Document source changed before saving. Reopen the document.");
|
|
2399
|
-
if (!preflight.accepted) throw new Error(preflight.issues.map((issue) => issue.message).join("\n") || "Author preflight rejected the edit.");
|
|
2400
|
-
const serialized = await documentRequest(api.session(sessionId).documentsSerialize, sessionId, {
|
|
2401
|
-
...request,
|
|
2402
|
-
preflightDigest: preflight.digest
|
|
2403
|
-
}, signal);
|
|
2404
|
-
if (serialized.encoding !== "base64") throw new Error("Author serialize returned an unsupported encoding.");
|
|
2405
|
-
const bytes = decodeBase64(serialized.bytes);
|
|
2406
|
-
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
2407
|
-
}
|
|
2408
|
-
async function saveAuthorDocument(sessionId, document, signal) {
|
|
2409
|
-
if (document.path.startsWith("artifact:")) throw new Error("Artifacts are read-only.");
|
|
2410
|
-
if (document.sourceSha256 === void 0) throw new Error("This Author view has no source digest.");
|
|
2411
|
-
const response = await browserTransport.fetch(authorSessionFileUrl(sessionId, document.path), {
|
|
2412
|
-
method: "PUT",
|
|
2413
|
-
headers: {
|
|
2414
|
-
"Content-Type": "application/octet-stream",
|
|
2415
|
-
"X-Expected-SHA256": document.sourceSha256
|
|
2416
|
-
},
|
|
2417
|
-
body: await bytesToSave(sessionId, document, signal),
|
|
2418
|
-
signal
|
|
2419
|
-
});
|
|
2420
|
-
if (!response.ok) throw new Error(`Author save failed (${response.status})`);
|
|
2421
|
-
const sha256 = response.headers.get("X-File-SHA256");
|
|
2422
|
-
if (sha256 === null || !/^[a-f0-9]{64}$/.test(sha256)) throw new Error("Author save returned an invalid digest.");
|
|
2423
|
-
return sha256;
|
|
2424
|
-
}
|
|
2425
|
-
function csvDialectFromMetadata(metadata) {
|
|
2426
|
-
if (metadata === void 0) return void 0;
|
|
2427
|
-
const { delimiter, quote, recordDelimiter } = metadata;
|
|
2428
|
-
if (delimiter !== "," && delimiter !== ";" && delimiter !== " " && delimiter !== "|" || quote !== "\"" && quote !== "'" || recordDelimiter !== "\n" && recordDelimiter !== "\r\n") throw new Error("Author document API returned an invalid CSV dialect.");
|
|
2429
|
-
return {
|
|
2430
|
-
delimiter,
|
|
2431
|
-
quote,
|
|
2432
|
-
recordDelimiter
|
|
2433
|
-
};
|
|
2434
|
-
}
|
|
2435
|
-
//#endregion
|
|
2436
|
-
//#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorToolPalette.tsx
|
|
2437
|
-
/** One tool tile. An active annotation tool is the person's mark (rose); an active view tool is Po's jade. */
|
|
2438
|
-
function authorToolClassName(active, annotates) {
|
|
2439
|
-
return `h-auto min-h-11 min-w-11 flex-col gap-1 rounded-md px-1 py-2 text-base [@media(pointer:fine)]:min-h-8 ${!active ? "border-po-border-soft bg-po-panel text-po-dim" : annotates ? "border-po-edge-magenta bg-po-tint-magenta font-semibold text-po-magenta hover:text-po-magenta" : "border-po-edge-green bg-po-tint-green font-semibold text-po-green hover:text-po-green"}`;
|
|
2440
|
-
}
|
|
2441
|
-
function AuthorToolPalette({ sessionId, path, kind, activeTool }) {
|
|
2442
|
-
const [error, setError] = useState();
|
|
2443
|
-
const format = (style) => {
|
|
2444
|
-
const selected = authorDocumentAnnotations(sessionId, path).candidate;
|
|
2445
|
-
const document = selected && authorDocument(sessionId, selected.documentPath);
|
|
2446
|
-
if (!selected || !document || selected.anchor.kind !== "text-range" || selected.revision !== document.version || selected.sourceSha256 !== document.sourceSha256) {
|
|
2447
|
-
setError("Select text in the document first.");
|
|
2448
|
-
return;
|
|
2449
|
-
}
|
|
2450
|
-
const { startOffset, endOffset } = selected.anchor;
|
|
2451
|
-
const content = document.content ?? "";
|
|
2452
|
-
const text = content.slice(startOffset, endOffset);
|
|
2453
|
-
const replacement = style === "Bold" ? `**${text}**` : style === "Link" ? `[${text}](https://)` : text.split("\n").map((line) => `${style === "Heading" ? "## " : "- "}${line}`).join("\n");
|
|
2454
|
-
reviseAuthorDocument(sessionId, document.path, content.slice(0, startOffset) + replacement + content.slice(endOffset));
|
|
2455
|
-
setAuthorRegionCandidate(sessionId, path, void 0);
|
|
2456
|
-
setError(void 0);
|
|
2457
|
-
};
|
|
2458
|
-
const tools = [
|
|
2459
|
-
...[
|
|
2460
|
-
"image",
|
|
2461
|
-
"pdf",
|
|
2462
|
-
"video",
|
|
2463
|
-
"story-preview"
|
|
2464
|
-
].includes(kind) ? [{
|
|
2465
|
-
label: "Pan",
|
|
2466
|
-
glyph: "✋",
|
|
2467
|
-
action: () => setAuthorToolMode(sessionId, path, activeTool === "pan" ? "select" : "pan"),
|
|
2468
|
-
active: activeTool === "pan",
|
|
2469
|
-
annotates: false
|
|
2470
|
-
}] : [],
|
|
2471
|
-
...[
|
|
2472
|
-
"image",
|
|
2473
|
-
"pdf",
|
|
2474
|
-
"video",
|
|
2475
|
-
"story-preview",
|
|
2476
|
-
"html"
|
|
2477
|
-
].includes(kind) ? [{
|
|
2478
|
-
label: "Draw",
|
|
2479
|
-
glyph: "✎",
|
|
2480
|
-
action: () => setAuthorToolMode(sessionId, path, activeTool === "draw" ? "select" : "draw"),
|
|
2481
|
-
active: activeTool === "draw",
|
|
2482
|
-
annotates: true
|
|
2483
|
-
}] : [],
|
|
2484
|
-
{
|
|
2485
|
-
label: "Region",
|
|
2486
|
-
glyph: "⌗",
|
|
2487
|
-
action: () => setAuthorToolMode(sessionId, path, activeTool === "mark" ? "select" : "mark"),
|
|
2488
|
-
active: activeTool === "mark",
|
|
2489
|
-
annotates: true
|
|
2490
|
-
},
|
|
2491
|
-
...kind === "markdown" && !isMermaidPath(path) ? [
|
|
2492
|
-
{
|
|
2493
|
-
label: "Bold",
|
|
2494
|
-
glyph: "B",
|
|
2495
|
-
action: () => format("Bold")
|
|
2496
|
-
},
|
|
2497
|
-
{
|
|
2498
|
-
label: "Heading",
|
|
2499
|
-
glyph: "H",
|
|
2500
|
-
action: () => format("Heading")
|
|
2501
|
-
},
|
|
2502
|
-
{
|
|
2503
|
-
label: "Link",
|
|
2504
|
-
glyph: "↗",
|
|
2505
|
-
action: () => format("Link")
|
|
2506
|
-
},
|
|
2507
|
-
{
|
|
2508
|
-
label: "List",
|
|
2509
|
-
glyph: "≡",
|
|
2510
|
-
action: () => format("List")
|
|
2511
|
-
}
|
|
2512
|
-
] : [],
|
|
2513
|
-
{
|
|
2514
|
-
label: "Comment",
|
|
2515
|
-
glyph: "●",
|
|
2516
|
-
action: () => setAuthorToolMode(sessionId, path, activeTool === "comment" ? "select" : "comment"),
|
|
2517
|
-
active: activeTool === "comment",
|
|
2518
|
-
annotates: true
|
|
2519
|
-
}
|
|
2520
|
-
];
|
|
2521
|
-
return /* @__PURE__ */ jsxs("section", {
|
|
2522
|
-
"data-testid": "author-tool-palette",
|
|
2523
|
-
className: "space-y-2",
|
|
2524
|
-
children: [
|
|
2525
|
-
/* @__PURE__ */ jsx("h3", {
|
|
2526
|
-
className: "sr-only",
|
|
2527
|
-
children: "Tools"
|
|
2528
|
-
}),
|
|
2529
|
-
/* @__PURE__ */ jsx("div", {
|
|
2530
|
-
className: "grid grid-cols-4 gap-1.5",
|
|
2531
|
-
children: tools.map((tool) => /* @__PURE__ */ jsxs(Button, {
|
|
2532
|
-
size: "md",
|
|
2533
|
-
variant: "outline",
|
|
2534
|
-
"aria-label": tool.label === "Region" ? "mark region" : tool.label,
|
|
2535
|
-
"aria-pressed": tool.active,
|
|
2536
|
-
onClick: tool.action,
|
|
2537
|
-
className: authorToolClassName(tool.active === true, tool.annotates === true),
|
|
2538
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
2539
|
-
"aria-hidden": "true",
|
|
2540
|
-
className: "text-lg leading-none",
|
|
2541
|
-
children: tool.glyph
|
|
2542
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
2543
|
-
className: "text-sm",
|
|
2544
|
-
children: tool.label
|
|
2545
|
-
})]
|
|
2546
|
-
}, tool.label))
|
|
2547
|
-
}),
|
|
2548
|
-
/* @__PURE__ */ jsx("p", {
|
|
2549
|
-
className: "text-base leading-normal text-po-dim sm:text-sm",
|
|
2550
|
-
children: activeTool === "draw" ? "Draw a location mark, then enter feedback text. Marks never edit the source." : activeTool === "pan" ? "Drag to pan; pinch to zoom. Use Fit to reset the view." : activeTool === "mark" ? "Drag over the document to mark a region." : activeTool === "comment" ? "Click the document to place a comment pin." : "Choose Region to drag an area, or Comment to place a point."
|
|
2551
|
-
}),
|
|
2552
|
-
error ? /* @__PURE__ */ jsx("output", {
|
|
2553
|
-
className: "block text-base text-po-red",
|
|
2554
|
-
children: error
|
|
2555
|
-
}) : null
|
|
2556
|
-
]
|
|
2557
|
-
});
|
|
2558
|
-
}
|
|
2559
|
-
//#endregion
|
|
2560
|
-
//#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorPanel.tsx
|
|
2561
|
-
function AuthorPanel({ sessionId, submitCapture, statuses, renderSlot, renderSessionActivity }) {
|
|
2562
|
-
const documents = useStore(authorWorkspace.store, (state) => {
|
|
2563
|
-
if (sessionId === null) return [];
|
|
2564
|
-
const prefix = `${sessionId}\n`;
|
|
2565
|
-
return Object.entries(state.documents).filter(([key]) => key.startsWith(prefix)).map(([, document]) => document);
|
|
2566
|
-
});
|
|
2567
|
-
const workspace = useStore(authorWorkspace.store, (state) => sessionId === null ? void 0 : state.sessions[sessionId]);
|
|
2568
|
-
const grid = useStore(authorGrid.store, (state) => sessionId === null ? void 0 : state.sessions[sessionId]);
|
|
2569
|
-
const focused = documents.find((document) => document.path === workspace?.focusedDocument?.path);
|
|
2570
|
-
const focusedPath = workspace?.focusedDocument?.path;
|
|
2571
|
-
const requests = (workspace?.requests ?? []).filter((request) => focusedPath === void 0 || request.documentPath === focusedPath);
|
|
2572
|
-
const drafts = useStore(authorWorkspace.store, (state) => authorDocumentAnnotations(sessionId, focusedPath ?? "", state));
|
|
2573
|
-
const activity = sessionId !== null && focusedPath !== void 0 && requests.some((request) => request.status === "REQUESTED" || request.status === "CHANGING") ? renderSessionActivity?.() ?? null : null;
|
|
2574
|
-
return /* @__PURE__ */ jsxs("section", {
|
|
2575
|
-
"data-testid": "author-panel",
|
|
2576
|
-
className: "flex min-h-0 flex-1 flex-col overflow-hidden",
|
|
2577
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
2578
|
-
className: "min-h-0 flex-1 overflow-y-auto p-3",
|
|
2579
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
2580
|
-
className: "flex flex-col gap-3",
|
|
2581
|
-
children: [
|
|
2582
|
-
/* @__PURE__ */ jsx("div", {
|
|
2583
|
-
"data-testid": "author-preview-providers",
|
|
2584
|
-
className: "empty:hidden",
|
|
2585
|
-
children: renderSlot("author.preview-provider")
|
|
2586
|
-
}),
|
|
2587
|
-
sessionId !== null && workspace !== void 0 && focused !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2588
|
-
/* @__PURE__ */ jsx(AuthorToolPalette, {
|
|
2589
|
-
sessionId,
|
|
2590
|
-
path: focused.path,
|
|
2591
|
-
kind: focused.kind,
|
|
2592
|
-
activeTool: workspace.toolsByDocument[normalizeAuthorPath(focused.path)] ?? "select"
|
|
2593
|
-
}),
|
|
2594
|
-
/* @__PURE__ */ jsx("h2", {
|
|
2595
|
-
className: "border-t border-po-border-soft pt-3 text-lg font-semibold text-po-hi",
|
|
2596
|
-
children: "Annotations"
|
|
2597
|
-
}),
|
|
2598
|
-
/* @__PURE__ */ jsx("p", {
|
|
2599
|
-
className: "text-base leading-normal text-po-dim sm:text-sm",
|
|
2600
|
-
children: "Drag a region or place a point, add feedback, then submit here. Requests queue while the agent is working."
|
|
2601
|
-
}),
|
|
2602
|
-
focused.kind === "video" ? /* @__PURE__ */ jsx("p", {
|
|
2603
|
-
className: "text-base text-po-dim sm:text-sm",
|
|
2604
|
-
children: "Annotations reference video frames. They do not save changes to the source video."
|
|
2605
|
-
}) : null,
|
|
2606
|
-
autonomousVoiceGridVisible(statuses) && grid !== void 0 ? /* @__PURE__ */ jsxs("div", {
|
|
2607
|
-
"data-testid": "author-grid-snapshot",
|
|
2608
|
-
className: "rounded border border-po-red/40 bg-po-red/5 p-2 text-base text-po-dim",
|
|
2609
|
-
children: [/* @__PURE__ */ jsx("strong", {
|
|
2610
|
-
className: "text-po-red",
|
|
2611
|
-
children: "VOICE GRID A1–H8"
|
|
2612
|
-
}), /* @__PURE__ */ jsxs("p", {
|
|
2613
|
-
className: "mt-1 truncate",
|
|
2614
|
-
children: ["token ", grid.geometryToken]
|
|
2615
|
-
})]
|
|
2616
|
-
}) : null,
|
|
2617
|
-
/* @__PURE__ */ jsx("div", {
|
|
2618
|
-
className: "max-sm:hidden",
|
|
2619
|
-
children: /* @__PURE__ */ jsx(AuthorFeedbackControls, {
|
|
2620
|
-
sessionId,
|
|
2621
|
-
document: focused,
|
|
2622
|
-
drafts,
|
|
2623
|
-
submitCapture
|
|
2624
|
-
}, focused.path)
|
|
2625
|
-
})
|
|
2626
|
-
] }) : null,
|
|
2627
|
-
/* @__PURE__ */ jsx("div", {
|
|
2628
|
-
className: focused === void 0 ? void 0 : "border-t border-po-border-soft pt-3",
|
|
2629
|
-
children: /* @__PURE__ */ jsx(AuthorRequestLog, { requests })
|
|
2630
|
-
})
|
|
2631
|
-
]
|
|
2632
|
-
})
|
|
2633
|
-
}), activity === null ? null : /* @__PURE__ */ jsx("footer", {
|
|
2634
|
-
"data-testid": "author-session-activity",
|
|
2635
|
-
className: "shrink-0 border-t border-po-border bg-po-rail p-3",
|
|
2636
|
-
children: activity
|
|
2637
|
-
})]
|
|
2693
|
+
structuredFormat: format,
|
|
2694
|
+
fragments: parsed.fragments,
|
|
2695
|
+
...csvDialect === void 0 ? {} : { csvDialect },
|
|
2696
|
+
originalFragments: parsed.fragments.map((fragment) => ({ ...fragment })),
|
|
2697
|
+
...sourceSha256 === void 0 ? {} : { sourceSha256 }
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
const mediaUrl = kind === "image" || kind === "video" || kind === "pdf" || kind === "opaque" ? url : void 0;
|
|
2701
|
+
const content = kind === "text" || kind === "markdown" || kind === "story-preview" || kind === "html" ? await response.text() : void 0;
|
|
2702
|
+
return {
|
|
2703
|
+
path,
|
|
2704
|
+
kind,
|
|
2705
|
+
title: authorFileName(path),
|
|
2706
|
+
...content === void 0 ? {} : { content },
|
|
2707
|
+
...mediaUrl === void 0 ? {} : { mediaUrl },
|
|
2708
|
+
...sourceSha256 === void 0 ? {} : { sourceSha256 }
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
function operationsFor(document) {
|
|
2712
|
+
const originals = new Map((document.originalFragments ?? []).map((fragment) => [fragment.id, fragment.text]));
|
|
2713
|
+
return (document.fragments ?? []).filter((fragment) => !fragment.readOnly && originals.get(fragment.id) !== fragment.text).map((fragment) => ({
|
|
2714
|
+
fragmentId: fragment.id,
|
|
2715
|
+
replacement: fragment.text
|
|
2716
|
+
}));
|
|
2717
|
+
}
|
|
2718
|
+
function decodeBase64(value) {
|
|
2719
|
+
if (!/^(?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{2}==|[A-Za-z\d+/]{3}=)?$/.test(value)) throw new Error("Invalid serialized document bytes.");
|
|
2720
|
+
const binary = atob(value);
|
|
2721
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
2722
|
+
}
|
|
2723
|
+
function imageMimeType(path) {
|
|
2724
|
+
const extension = path.split(".").at(-1)?.toLowerCase();
|
|
2725
|
+
if (extension === "png") return "image/png";
|
|
2726
|
+
if (extension === "jpg" || extension === "jpeg") return "image/jpeg";
|
|
2727
|
+
if (extension === "webp") return "image/webp";
|
|
2728
|
+
}
|
|
2729
|
+
async function croppedImageBytes(documentInput, signal) {
|
|
2730
|
+
const { crop, mediaUrl } = documentInput;
|
|
2731
|
+
if (crop === void 0 || mediaUrl === void 0) throw new Error("This Author image has no crop source.");
|
|
2732
|
+
const mimeType = imageMimeType(documentInput.path);
|
|
2733
|
+
if (mimeType === void 0) throw new Error("This image format cannot preserve its encoding after crop.");
|
|
2734
|
+
const response = await browserTransport.fetch(mediaUrl, {
|
|
2735
|
+
signal,
|
|
2736
|
+
cache: "no-store"
|
|
2638
2737
|
});
|
|
2738
|
+
if (!response.ok) throw new Error(`Author could not read the image before saving (${response.status})`);
|
|
2739
|
+
const bitmap = await createImageBitmap(await response.blob());
|
|
2740
|
+
try {
|
|
2741
|
+
const sourceX = Math.round(crop.x * bitmap.width);
|
|
2742
|
+
const sourceY = Math.round(crop.y * bitmap.height);
|
|
2743
|
+
const sourceWidth = Math.max(1, Math.round(crop.width * bitmap.width));
|
|
2744
|
+
const sourceHeight = Math.max(1, Math.round(crop.height * bitmap.height));
|
|
2745
|
+
const canvas = window.document.createElement("canvas");
|
|
2746
|
+
canvas.width = sourceWidth;
|
|
2747
|
+
canvas.height = sourceHeight;
|
|
2748
|
+
const context = canvas.getContext("2d");
|
|
2749
|
+
if (context === null) throw new Error("Canvas is unavailable for image crop.");
|
|
2750
|
+
context.drawImage(bitmap, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, sourceWidth, sourceHeight);
|
|
2751
|
+
const blob = await new Promise((resolve) => canvas.toBlob(resolve, mimeType));
|
|
2752
|
+
if (blob === null || blob.type !== mimeType) throw new Error("The browser cannot encode this cropped image format.");
|
|
2753
|
+
return blob;
|
|
2754
|
+
} finally {
|
|
2755
|
+
bitmap.close();
|
|
2756
|
+
}
|
|
2639
2757
|
}
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2758
|
+
async function bytesToSave(sessionId, document, signal) {
|
|
2759
|
+
if (document.kind === "text" || document.kind === "markdown" || document.kind === "story-preview" || document.kind === "html") return document.content ?? "";
|
|
2760
|
+
if (document.kind === "image" && document.crop !== void 0) return await croppedImageBytes(document, signal);
|
|
2761
|
+
if (document.structuredFormat === void 0) throw new Error("This Author view cannot be saved.");
|
|
2762
|
+
const operations = operationsFor(document);
|
|
2763
|
+
const request = {
|
|
2764
|
+
path: document.path,
|
|
2765
|
+
format: document.structuredFormat,
|
|
2766
|
+
operations,
|
|
2767
|
+
...document.csvDialect === void 0 ? {} : { csvDialect: document.csvDialect }
|
|
2768
|
+
};
|
|
2769
|
+
const preflight = await documentRequest(api.session(sessionId).documentsPreflight, sessionId, request, signal);
|
|
2770
|
+
if (preflight.sourceDigest !== void 0 && preflight.sourceDigest !== document.sourceSha256) throw new Error("Document source changed before saving. Reopen the document.");
|
|
2771
|
+
if (!preflight.accepted) throw new Error(preflight.issues.map((issue) => issue.message).join("\n") || "Author preflight rejected the edit.");
|
|
2772
|
+
const serialized = await documentRequest(api.session(sessionId).documentsSerialize, sessionId, {
|
|
2773
|
+
...request,
|
|
2774
|
+
preflightDigest: preflight.digest
|
|
2775
|
+
}, signal);
|
|
2776
|
+
if (serialized.encoding !== "base64") throw new Error("Author serialize returned an unsupported encoding.");
|
|
2777
|
+
const bytes = decodeBase64(serialized.bytes);
|
|
2778
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
2779
|
+
}
|
|
2780
|
+
async function saveAuthorDocument(sessionId, document, signal) {
|
|
2781
|
+
if (document.path.startsWith("artifact:")) throw new Error("Artifacts are read-only.");
|
|
2782
|
+
if (document.sourceSha256 === void 0) throw new Error("This Author view has no source digest.");
|
|
2783
|
+
const response = await browserTransport.fetch(authorSessionFileUrl(sessionId, document.path), {
|
|
2784
|
+
method: "PUT",
|
|
2785
|
+
headers: {
|
|
2786
|
+
"Content-Type": "application/octet-stream",
|
|
2787
|
+
"X-Expected-SHA256": document.sourceSha256
|
|
2652
2788
|
},
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
}
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
function parseAuthorOpenSourceAction(input) {
|
|
2661
|
-
if (typeof input !== "object" || input === null) return null;
|
|
2662
|
-
const candidate = input;
|
|
2663
|
-
return candidate.version === 1 && typeof candidate.createTab === "function" ? candidate : null;
|
|
2789
|
+
body: await bytesToSave(sessionId, document, signal),
|
|
2790
|
+
signal
|
|
2791
|
+
});
|
|
2792
|
+
if (!response.ok) throw new Error(`Author save failed (${response.status})`);
|
|
2793
|
+
const sha256 = response.headers.get("X-File-SHA256");
|
|
2794
|
+
if (sha256 === null || !/^[a-f0-9]{64}$/.test(sha256)) throw new Error("Author save returned an invalid digest.");
|
|
2795
|
+
return sha256;
|
|
2664
2796
|
}
|
|
2665
|
-
function
|
|
2666
|
-
if (
|
|
2667
|
-
const
|
|
2668
|
-
if (
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2797
|
+
function csvDialectFromMetadata(metadata) {
|
|
2798
|
+
if (metadata === void 0) return void 0;
|
|
2799
|
+
const { delimiter, quote, recordDelimiter } = metadata;
|
|
2800
|
+
if (delimiter !== "," && delimiter !== ";" && delimiter !== " " && delimiter !== "|" || quote !== "\"" && quote !== "'" || recordDelimiter !== "\n" && recordDelimiter !== "\r\n") throw new Error("Author document API returned an invalid CSV dialect.");
|
|
2801
|
+
return {
|
|
2802
|
+
delimiter,
|
|
2803
|
+
quote,
|
|
2804
|
+
recordDelimiter
|
|
2805
|
+
};
|
|
2674
2806
|
}
|
|
2675
|
-
const authorPreviewActionSlot = {
|
|
2676
|
-
slot: "author.preview-action",
|
|
2677
|
-
parse: parseAuthorPreviewAction
|
|
2678
|
-
};
|
|
2679
2807
|
//#endregion
|
|
2680
2808
|
//#region src/extensions/workspaces/sessions/(frontend)/_lib/authorGridTools.ts
|
|
2681
2809
|
const stringProperty = {
|
|
@@ -2908,6 +3036,7 @@ function mutationTool(sessionId, path, kind) {
|
|
|
2908
3036
|
revision: document.version + 1
|
|
2909
3037
|
};
|
|
2910
3038
|
} else throw new Error("UNSUPPORTED_REGION: PDF, video and rendered HTML regions are capture-only.");
|
|
3039
|
+
removeAuthorRegion(sessionId, path, region.id);
|
|
2911
3040
|
if (request !== void 0) {
|
|
2912
3041
|
const pendingRegions = rebasePendingRegions(pending, region, result, nextContent);
|
|
2913
3042
|
updateAuthorRequest(sessionId, request.id, (current) => ({
|
|
@@ -4589,9 +4718,118 @@ function AuthorStructuredView({ sessionId, document, displayedRegions }) {
|
|
|
4589
4718
|
}
|
|
4590
4719
|
//#endregion
|
|
4591
4720
|
//#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorTextView.tsx
|
|
4592
|
-
|
|
4721
|
+
const FORMATS = [
|
|
4722
|
+
{
|
|
4723
|
+
format: "bold",
|
|
4724
|
+
label: "Bold",
|
|
4725
|
+
glyph: "B"
|
|
4726
|
+
},
|
|
4727
|
+
{
|
|
4728
|
+
format: "heading",
|
|
4729
|
+
label: "Heading",
|
|
4730
|
+
glyph: "H"
|
|
4731
|
+
},
|
|
4732
|
+
{
|
|
4733
|
+
format: "link",
|
|
4734
|
+
label: "Link",
|
|
4735
|
+
glyph: "↗"
|
|
4736
|
+
},
|
|
4737
|
+
{
|
|
4738
|
+
format: "list",
|
|
4739
|
+
label: "List",
|
|
4740
|
+
glyph: "≡"
|
|
4741
|
+
}
|
|
4742
|
+
];
|
|
4743
|
+
/** The blocks Comment marks in the reading view: one paragraph, item, heading, quote or code block. */
|
|
4744
|
+
const COMMENT_BLOCKS = "p,li,h1,h2,h3,h4,h5,h6,pre,blockquote,td,th";
|
|
4745
|
+
/** The CSS highlight that paints unsent notes on the reading view. */
|
|
4746
|
+
const NOTE_HIGHLIGHT = "po-author-note";
|
|
4747
|
+
/** The label shown for the passage being marked, before it has feedback. */
|
|
4748
|
+
const CANDIDATE_LABEL = "•";
|
|
4749
|
+
/** Rendered text in reading order, minus diagrams and controls that are not document text. */
|
|
4750
|
+
function renderedRuns(article) {
|
|
4751
|
+
const walker = article.ownerDocument.createTreeWalker(article, NodeFilter.SHOW_TEXT, { acceptNode: (node) => node.parentElement?.closest("svg,button,[aria-hidden=\"true\"]") ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT });
|
|
4752
|
+
const runs = [];
|
|
4753
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) runs.push(node);
|
|
4754
|
+
return runs;
|
|
4755
|
+
}
|
|
4756
|
+
/** Maps a range over the rendered page back to the Markdown source it came from. */
|
|
4757
|
+
function sourceRange(article, source, range) {
|
|
4758
|
+
const runs = renderedRuns(article);
|
|
4759
|
+
const offsets = locateRenderedRuns(source, runs.map((run) => run.data));
|
|
4760
|
+
let from;
|
|
4761
|
+
let to;
|
|
4762
|
+
runs.forEach((run, index) => {
|
|
4763
|
+
const at = offsets[index];
|
|
4764
|
+
if (at === void 0 || !range.intersectsNode(run)) return;
|
|
4765
|
+
from ??= at + (run === range.startContainer ? range.startOffset : 0);
|
|
4766
|
+
to = Math.min(source.length, at + (run === range.endContainer ? range.endOffset : run.data.length));
|
|
4767
|
+
});
|
|
4768
|
+
return from === void 0 || to === void 0 || to <= from ? void 0 : {
|
|
4769
|
+
from,
|
|
4770
|
+
to
|
|
4771
|
+
};
|
|
4772
|
+
}
|
|
4773
|
+
/** The rendered text a source range covers: the way back from a note to the page. */
|
|
4774
|
+
function renderedRange(article, source, from, to) {
|
|
4775
|
+
const runs = renderedRuns(article);
|
|
4776
|
+
const offsets = locateRenderedRuns(source, runs.map((run) => run.data));
|
|
4777
|
+
let first;
|
|
4778
|
+
let last;
|
|
4779
|
+
runs.forEach((run, index) => {
|
|
4780
|
+
const at = offsets[index];
|
|
4781
|
+
if (at === void 0 || at + run.data.length <= from || at >= to) return;
|
|
4782
|
+
first ??= {
|
|
4783
|
+
run,
|
|
4784
|
+
offset: Math.max(0, from - at)
|
|
4785
|
+
};
|
|
4786
|
+
last = {
|
|
4787
|
+
run,
|
|
4788
|
+
offset: Math.min(run.data.length, to - at)
|
|
4789
|
+
};
|
|
4790
|
+
});
|
|
4791
|
+
if (first === void 0 || last === void 0) return void 0;
|
|
4792
|
+
const range = article.ownerDocument.createRange();
|
|
4793
|
+
range.setStart(first.run, first.offset);
|
|
4794
|
+
range.setEnd(last.run, last.offset);
|
|
4795
|
+
return range;
|
|
4796
|
+
}
|
|
4797
|
+
/** The rendered text a dragged box covers, run by run. */
|
|
4798
|
+
function rangeInBox(article, box) {
|
|
4799
|
+
const probe = article.ownerDocument.createRange();
|
|
4800
|
+
const covered = renderedRuns(article).filter((run) => {
|
|
4801
|
+
probe.selectNodeContents(run);
|
|
4802
|
+
return [...probe.getClientRects()].some((rect) => rect.right > box.left && rect.left < box.right && rect.bottom > box.top && rect.top < box.bottom);
|
|
4803
|
+
});
|
|
4804
|
+
const first = covered[0];
|
|
4805
|
+
const last = covered.at(-1);
|
|
4806
|
+
if (first === void 0 || last === void 0) return void 0;
|
|
4807
|
+
probe.setStart(first, 0);
|
|
4808
|
+
probe.setEnd(last, last.data.length);
|
|
4809
|
+
return probe;
|
|
4810
|
+
}
|
|
4811
|
+
function AuthorTextView({ sessionId, document, preview, activeTool = "select", displayedRegions, candidate, renderMediaReview }) {
|
|
4593
4812
|
const host = useRef(null);
|
|
4594
4813
|
const editor = useRef(null);
|
|
4814
|
+
const article = useRef(null);
|
|
4815
|
+
const selected = useRef({
|
|
4816
|
+
from: 0,
|
|
4817
|
+
to: 0
|
|
4818
|
+
});
|
|
4819
|
+
const [box, setBox] = useState();
|
|
4820
|
+
const [notice, setNotice] = useState();
|
|
4821
|
+
const [badges, setBadges] = useState([]);
|
|
4822
|
+
const formatting = !preview && document.kind === "markdown" && !isMermaidPath(document.path);
|
|
4823
|
+
const marks = [...displayedRegions.flatMap(({ ordinal, region }) => region.anchor.kind === "text-range" ? [{
|
|
4824
|
+
label: String(ordinal),
|
|
4825
|
+
from: region.anchor.startOffset,
|
|
4826
|
+
to: region.anchor.endOffset
|
|
4827
|
+
}] : []), ...candidate?.anchor.kind === "text-range" ? [{
|
|
4828
|
+
label: CANDIDATE_LABEL,
|
|
4829
|
+
from: candidate.anchor.startOffset,
|
|
4830
|
+
to: candidate.anchor.endOffset
|
|
4831
|
+
}] : []];
|
|
4832
|
+
const marksKey = JSON.stringify(marks);
|
|
4595
4833
|
useEffect(() => registerAuthorGridResolver(sessionId, (cell, geometry) => {
|
|
4596
4834
|
if (preview || editor.current === null) return void 0;
|
|
4597
4835
|
const { originX = 0, originY = 0, width, height } = geometry.viewport;
|
|
@@ -4614,12 +4852,44 @@ function AuthorTextView({ sessionId, document, preview, displayedRegions, render
|
|
|
4614
4852
|
};
|
|
4615
4853
|
}), [preview, sessionId]);
|
|
4616
4854
|
useEffect(() => {
|
|
4617
|
-
editor.current?.setMarkedRanges(
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4855
|
+
editor.current?.setMarkedRanges(JSON.parse(marksKey));
|
|
4856
|
+
}, [marksKey]);
|
|
4857
|
+
useEffect(() => {
|
|
4858
|
+
const root = article.current;
|
|
4859
|
+
const frame = root?.parentElement;
|
|
4860
|
+
if (!preview || !root || !frame) return void 0;
|
|
4861
|
+
const paint = () => {
|
|
4862
|
+
const source = document.content ?? "";
|
|
4863
|
+
const placed = JSON.parse(marksKey).flatMap((mark) => {
|
|
4864
|
+
const range = renderedRange(root, source, mark.from, mark.to);
|
|
4865
|
+
return range === void 0 ? [] : [{
|
|
4866
|
+
label: mark.label,
|
|
4867
|
+
range
|
|
4868
|
+
}];
|
|
4869
|
+
});
|
|
4870
|
+
if (typeof CSS !== "undefined" && "highlights" in CSS) CSS.highlights.set(NOTE_HIGHLIGHT, new Highlight(...placed.map(({ range }) => range)));
|
|
4871
|
+
const origin = frame.getBoundingClientRect();
|
|
4872
|
+
setBadges(placed.map(({ label, range }) => {
|
|
4873
|
+
const rect = range.getClientRects()[0] ?? range.getBoundingClientRect();
|
|
4874
|
+
return {
|
|
4875
|
+
label,
|
|
4876
|
+
left: rect.left - origin.left,
|
|
4877
|
+
top: rect.top - origin.top
|
|
4878
|
+
};
|
|
4879
|
+
}));
|
|
4880
|
+
};
|
|
4881
|
+
paint();
|
|
4882
|
+
const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(paint);
|
|
4883
|
+
observer?.observe(root);
|
|
4884
|
+
return () => {
|
|
4885
|
+
observer?.disconnect();
|
|
4886
|
+
if (typeof CSS !== "undefined" && "highlights" in CSS) CSS.highlights.delete(NOTE_HIGHLIGHT);
|
|
4887
|
+
};
|
|
4888
|
+
}, [
|
|
4889
|
+
preview,
|
|
4890
|
+
marksKey,
|
|
4891
|
+
document.content
|
|
4892
|
+
]);
|
|
4623
4893
|
const lines = (document.content ?? "").split("\n").length;
|
|
4624
4894
|
const loadImage = useCallback(async (imagePath) => {
|
|
4625
4895
|
const path = authorMarkdownImagePath(document.path, imagePath);
|
|
@@ -4628,69 +4898,296 @@ function AuthorTextView({ sessionId, document, preview, displayedRegions, render
|
|
|
4628
4898
|
path
|
|
4629
4899
|
};
|
|
4630
4900
|
}, [sessionId, document.path]);
|
|
4901
|
+
const format = (style) => {
|
|
4902
|
+
const current = authorDocument(sessionId, document.path);
|
|
4903
|
+
if (!current || editor.current === null) return;
|
|
4904
|
+
const { from, to } = selected.current;
|
|
4905
|
+
const edit = authorMarkdownFormat(current.content ?? "", from, to, style);
|
|
4906
|
+
editor.current.applyEdits([{
|
|
4907
|
+
from: edit.from,
|
|
4908
|
+
to: edit.to,
|
|
4909
|
+
insert: edit.insert
|
|
4910
|
+
}]);
|
|
4911
|
+
editor.current.revealAndSelect(edit.select);
|
|
4912
|
+
editor.current.focus();
|
|
4913
|
+
};
|
|
4914
|
+
/** Makes a source range the annotation candidate the rail asks feedback for. */
|
|
4915
|
+
const markSource = (found, mode, missing) => {
|
|
4916
|
+
const current = authorDocument(sessionId, document.path);
|
|
4917
|
+
const bounds = host.current?.getBoundingClientRect();
|
|
4918
|
+
if (!current || !bounds) return;
|
|
4919
|
+
if (found === void 0 || found.to <= found.from) {
|
|
4920
|
+
setNotice(missing);
|
|
4921
|
+
return;
|
|
4922
|
+
}
|
|
4923
|
+
const source = current.content ?? "";
|
|
4924
|
+
setNotice(void 0);
|
|
4925
|
+
setAuthorRegionCandidate(sessionId, document.path, {
|
|
4926
|
+
documentPath: current.path,
|
|
4927
|
+
revision: current.version,
|
|
4928
|
+
sourceSha256: current.sourceSha256,
|
|
4929
|
+
mode,
|
|
4930
|
+
quote: source.slice(found.from, found.to),
|
|
4931
|
+
anchor: {
|
|
4932
|
+
kind: "text-range",
|
|
4933
|
+
startOffset: found.from,
|
|
4934
|
+
endOffset: found.to,
|
|
4935
|
+
startLine: authorSourceLine(source, found.from),
|
|
4936
|
+
endLine: authorSourceLine(source, found.to)
|
|
4937
|
+
},
|
|
4938
|
+
viewport: {
|
|
4939
|
+
width: bounds.width,
|
|
4940
|
+
height: bounds.height
|
|
4941
|
+
},
|
|
4942
|
+
createdAt: Date.now()
|
|
4943
|
+
});
|
|
4944
|
+
};
|
|
4945
|
+
const markRendered = (range, mode) => {
|
|
4946
|
+
if (article.current === null) return;
|
|
4947
|
+
markSource(sourceRange(article.current, authorDocument(sessionId, document.path)?.content ?? "", range), mode, "That part of the page has no source text to mark. Press Edit to select it in the source.");
|
|
4948
|
+
};
|
|
4949
|
+
const markSelection = () => {
|
|
4950
|
+
if (activeTool !== "select" || article.current === null) return;
|
|
4951
|
+
const selection = window.getSelection();
|
|
4952
|
+
if (selection === null || selection.isCollapsed || selection.rangeCount === 0) return;
|
|
4953
|
+
const range = selection.getRangeAt(0);
|
|
4954
|
+
if (article.current.contains(range.commonAncestorContainer)) markRendered(range, "region");
|
|
4955
|
+
};
|
|
4956
|
+
const annotating = activeTool === "mark" || activeTool === "comment";
|
|
4957
|
+
const drawBox = (event, start) => {
|
|
4958
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
4959
|
+
const x = event.clientX - bounds.left;
|
|
4960
|
+
const y = event.clientY - bounds.top;
|
|
4961
|
+
return {
|
|
4962
|
+
...start,
|
|
4963
|
+
left: Math.min(start.x, x),
|
|
4964
|
+
top: Math.min(start.y, y),
|
|
4965
|
+
width: Math.abs(x - start.x),
|
|
4966
|
+
height: Math.abs(y - start.y)
|
|
4967
|
+
};
|
|
4968
|
+
};
|
|
4969
|
+
/** Region and Comment over the source editor: a dragged stretch of text, or the clicked line. */
|
|
4970
|
+
const finishSourceAnnotation = (event, started) => {
|
|
4971
|
+
const source = authorDocument(sessionId, document.path)?.content ?? "";
|
|
4972
|
+
if (editor.current === null) return;
|
|
4973
|
+
if (activeTool === "comment") {
|
|
4974
|
+
const hit = editor.current.resolveViewportRegion({
|
|
4975
|
+
left: event.clientX,
|
|
4976
|
+
top: event.clientY,
|
|
4977
|
+
right: event.clientX,
|
|
4978
|
+
bottom: event.clientY
|
|
4979
|
+
});
|
|
4980
|
+
const lineStart = hit === null ? 0 : source.lastIndexOf("\n", hit.from - 1) + 1;
|
|
4981
|
+
const lineEnd = hit === null ? -1 : source.indexOf("\n", hit.from);
|
|
4982
|
+
markSource(hit === null ? void 0 : {
|
|
4983
|
+
from: lineStart,
|
|
4984
|
+
to: lineEnd === -1 ? source.length : lineEnd
|
|
4985
|
+
}, "point", "Click a line with text to comment on it.");
|
|
4986
|
+
return;
|
|
4987
|
+
}
|
|
4988
|
+
const drawn = drawBox(event, started);
|
|
4989
|
+
if (drawn.width < 4 && drawn.height < 4) return;
|
|
4990
|
+
const overlay = event.currentTarget.getBoundingClientRect();
|
|
4991
|
+
const hit = editor.current.resolveViewportRegion({
|
|
4992
|
+
left: overlay.left + drawn.left,
|
|
4993
|
+
top: overlay.top + drawn.top,
|
|
4994
|
+
right: overlay.left + drawn.left + drawn.width,
|
|
4995
|
+
bottom: overlay.top + drawn.top + drawn.height
|
|
4996
|
+
});
|
|
4997
|
+
markSource(hit === null ? void 0 : {
|
|
4998
|
+
from: hit.from,
|
|
4999
|
+
to: hit.to
|
|
5000
|
+
}, "region", "Drag over some text to mark it.");
|
|
5001
|
+
};
|
|
5002
|
+
const finishAnnotation = (event) => {
|
|
5003
|
+
const started = box;
|
|
5004
|
+
setBox(void 0);
|
|
5005
|
+
if (started === void 0 || started.pointerId !== event.pointerId) return;
|
|
5006
|
+
if (!preview) {
|
|
5007
|
+
finishSourceAnnotation(event, started);
|
|
5008
|
+
return;
|
|
5009
|
+
}
|
|
5010
|
+
if (article.current === null) return;
|
|
5011
|
+
if (activeTool === "comment") {
|
|
5012
|
+
const block = window.document.elementsFromPoint(event.clientX, event.clientY).find((element) => article.current?.contains(element) && element.closest(COMMENT_BLOCKS) !== null)?.closest(COMMENT_BLOCKS);
|
|
5013
|
+
if (!block) return;
|
|
5014
|
+
const range = window.document.createRange();
|
|
5015
|
+
range.selectNodeContents(block);
|
|
5016
|
+
markRendered(range, "point");
|
|
5017
|
+
return;
|
|
5018
|
+
}
|
|
5019
|
+
const drawn = drawBox(event, started);
|
|
5020
|
+
if (drawn.width < 4 || drawn.height < 4) return;
|
|
5021
|
+
const overlay = event.currentTarget.getBoundingClientRect();
|
|
5022
|
+
const range = rangeInBox(article.current, new DOMRect(overlay.left + drawn.left, overlay.top + drawn.top, drawn.width, drawn.height));
|
|
5023
|
+
if (range === void 0) {
|
|
5024
|
+
setNotice("Drag over some text to mark it.");
|
|
5025
|
+
return;
|
|
5026
|
+
}
|
|
5027
|
+
markRendered(range, "region");
|
|
5028
|
+
};
|
|
5029
|
+
const overlay = annotating ? /* @__PURE__ */ jsx("div", {
|
|
5030
|
+
"data-testid": "author-text-overlay",
|
|
5031
|
+
className: `absolute inset-0 z-10 touch-none ${activeTool === "mark" ? "cursor-crosshair" : "cursor-pointer"}`,
|
|
5032
|
+
onPointerDown: (event) => {
|
|
5033
|
+
if (event.button !== 0) return;
|
|
5034
|
+
event.preventDefault();
|
|
5035
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
5036
|
+
const x = event.clientX - bounds.left;
|
|
5037
|
+
const y = event.clientY - bounds.top;
|
|
5038
|
+
setBox({
|
|
5039
|
+
pointerId: event.pointerId,
|
|
5040
|
+
x,
|
|
5041
|
+
y,
|
|
5042
|
+
left: x,
|
|
5043
|
+
top: y,
|
|
5044
|
+
width: 0,
|
|
5045
|
+
height: 0
|
|
5046
|
+
});
|
|
5047
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
5048
|
+
},
|
|
5049
|
+
onPointerMove: (event) => {
|
|
5050
|
+
if (activeTool === "mark" && box?.pointerId === event.pointerId) setBox(drawBox(event, box));
|
|
5051
|
+
},
|
|
5052
|
+
onPointerUp: finishAnnotation,
|
|
5053
|
+
onPointerCancel: () => setBox(void 0),
|
|
5054
|
+
onWheel: (event) => preview ? void 0 : host.current?.querySelector(".cm-scroller")?.scrollBy({
|
|
5055
|
+
left: event.deltaX,
|
|
5056
|
+
top: event.deltaY
|
|
5057
|
+
}),
|
|
5058
|
+
children: box === void 0 || activeTool !== "mark" ? null : /* @__PURE__ */ jsx("div", {
|
|
5059
|
+
"data-testid": "author-text-selection",
|
|
5060
|
+
className: "pointer-events-none absolute border-2 border-dashed border-po-magenta bg-po-magenta/10",
|
|
5061
|
+
style: {
|
|
5062
|
+
left: box.left,
|
|
5063
|
+
top: box.top,
|
|
5064
|
+
width: box.width,
|
|
5065
|
+
height: box.height
|
|
5066
|
+
}
|
|
5067
|
+
})
|
|
5068
|
+
}) : null;
|
|
4631
5069
|
return /* @__PURE__ */ jsxs("div", {
|
|
4632
5070
|
ref: host,
|
|
4633
5071
|
className: "flex min-h-0 flex-1 flex-col",
|
|
4634
|
-
children: [
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
5072
|
+
children: [
|
|
5073
|
+
formatting ? /* @__PURE__ */ jsxs("div", {
|
|
5074
|
+
role: "toolbar",
|
|
5075
|
+
"aria-label": "Format Markdown",
|
|
5076
|
+
"data-testid": "author-format-toolbar",
|
|
5077
|
+
className: "flex shrink-0 items-center gap-1 border-b border-po-border bg-po-bg px-3 py-1.5",
|
|
5078
|
+
children: [FORMATS.map(({ format: style, label, glyph }) => /* @__PURE__ */ jsxs(Button, {
|
|
5079
|
+
size: "sm",
|
|
5080
|
+
variant: "ghost",
|
|
5081
|
+
"aria-label": label,
|
|
5082
|
+
title: label,
|
|
5083
|
+
className: "min-h-11 gap-1.5 [@media(pointer:fine)]:min-h-8",
|
|
5084
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
5085
|
+
onClick: () => format(style),
|
|
5086
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
5087
|
+
"aria-hidden": "true",
|
|
5088
|
+
className: "font-semibold",
|
|
5089
|
+
children: glyph
|
|
5090
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
5091
|
+
className: "text-sm",
|
|
5092
|
+
children: label
|
|
5093
|
+
})]
|
|
5094
|
+
}, style)), /* @__PURE__ */ jsx("span", {
|
|
5095
|
+
className: "ml-auto truncate text-sm text-po-dim",
|
|
5096
|
+
children: "Select text, then format it."
|
|
5097
|
+
})]
|
|
5098
|
+
}) : null,
|
|
5099
|
+
preview ? /* @__PURE__ */ jsx("div", {
|
|
5100
|
+
"data-testid": "author-markdown",
|
|
5101
|
+
className: "min-h-0 flex-1 overflow-auto bg-po-deep/50 p-6",
|
|
5102
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
5103
|
+
className: "relative mx-auto min-h-full max-w-3xl",
|
|
5104
|
+
children: [
|
|
5105
|
+
/* @__PURE__ */ jsx("article", {
|
|
5106
|
+
ref: article,
|
|
5107
|
+
onPointerUp: markSelection,
|
|
5108
|
+
onKeyUp: markSelection,
|
|
5109
|
+
className: "min-h-full rounded-md bg-po-panel px-6 py-8 sm:px-10 text-lg leading-relaxed text-po-text shadow-sm ring-1 ring-po-border-soft [&::highlight(po-author-note)]:bg-po-tint-magenta [&_*::highlight(po-author-note)]:bg-po-tint-magenta",
|
|
5110
|
+
children: /* @__PURE__ */ jsx(Markdown, {
|
|
5111
|
+
text: isMermaidPath(document.path) ? `\`\`\`\`mermaid\n${document.content ?? ""}\n\`\`\`\`` : document.content ?? "",
|
|
5112
|
+
loadImage,
|
|
5113
|
+
documentPath: document.path,
|
|
5114
|
+
renderMediaReview
|
|
5115
|
+
})
|
|
5116
|
+
}),
|
|
5117
|
+
badges.map((badge, index) => /* @__PURE__ */ jsx("span", {
|
|
5118
|
+
"aria-hidden": "true",
|
|
5119
|
+
"data-author-region": badge.label,
|
|
5120
|
+
className: `pointer-events-none absolute z-10 flex h-6 min-w-6 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border-2 px-1 text-sm font-semibold ${badge.label === CANDIDATE_LABEL ? "border-po-magenta bg-po-panel text-po-magenta" : "border-po-panel bg-po-magenta text-po-rail"}`,
|
|
5121
|
+
style: {
|
|
5122
|
+
left: badge.left,
|
|
5123
|
+
top: badge.top
|
|
5124
|
+
},
|
|
5125
|
+
children: badge.label
|
|
5126
|
+
}, `${badge.label}-${String(index)}`)),
|
|
5127
|
+
overlay
|
|
5128
|
+
]
|
|
4644
5129
|
})
|
|
5130
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
5131
|
+
className: "relative flex min-h-0 flex-1 flex-col",
|
|
5132
|
+
children: [/* @__PURE__ */ jsx(CodeEditor, {
|
|
5133
|
+
value: document.content ?? "",
|
|
5134
|
+
path: document.path,
|
|
5135
|
+
"data-testid": "author-editor",
|
|
5136
|
+
controllerRef: editor,
|
|
5137
|
+
className: "min-h-0 flex-1",
|
|
5138
|
+
onChange: (content) => reviseAuthorDocument(sessionId, document.path, content),
|
|
5139
|
+
onSelect: (range) => {
|
|
5140
|
+
selected.current = {
|
|
5141
|
+
from: range.from,
|
|
5142
|
+
to: range.to
|
|
5143
|
+
};
|
|
5144
|
+
const current = authorDocument(sessionId, document.path);
|
|
5145
|
+
const bounds = host.current?.getBoundingClientRect();
|
|
5146
|
+
if (!current || !bounds || range.from === range.to) return;
|
|
5147
|
+
setAuthorRegionCandidate(sessionId, document.path, {
|
|
5148
|
+
documentPath: current.path,
|
|
5149
|
+
revision: current.version,
|
|
5150
|
+
sourceSha256: current.sourceSha256,
|
|
5151
|
+
quote: range.text,
|
|
5152
|
+
anchor: {
|
|
5153
|
+
kind: "text-range",
|
|
5154
|
+
startOffset: range.from,
|
|
5155
|
+
endOffset: range.to,
|
|
5156
|
+
startLine: range.startLine,
|
|
5157
|
+
endLine: range.endLine
|
|
5158
|
+
},
|
|
5159
|
+
viewport: {
|
|
5160
|
+
width: bounds.width,
|
|
5161
|
+
height: bounds.height
|
|
5162
|
+
},
|
|
5163
|
+
createdAt: Date.now()
|
|
5164
|
+
});
|
|
5165
|
+
}
|
|
5166
|
+
}), overlay]
|
|
5167
|
+
}),
|
|
5168
|
+
notice === void 0 ? null : /* @__PURE__ */ jsx("output", {
|
|
5169
|
+
className: "shrink-0 border-t border-po-border bg-po-bg px-4 py-1.5 text-sm text-po-dim",
|
|
5170
|
+
children: notice
|
|
5171
|
+
}),
|
|
5172
|
+
/* @__PURE__ */ jsxs("footer", {
|
|
5173
|
+
className: "flex shrink-0 items-center gap-2 border-t border-po-border bg-po-bg px-4 py-1.5 text-sm text-po-dim",
|
|
5174
|
+
children: [
|
|
5175
|
+
/* @__PURE__ */ jsx("span", {
|
|
5176
|
+
className: "first-letter:uppercase",
|
|
5177
|
+
children: document.kind
|
|
5178
|
+
}),
|
|
5179
|
+
/* @__PURE__ */ jsx("span", {
|
|
5180
|
+
"aria-hidden": "true",
|
|
5181
|
+
children: "·"
|
|
5182
|
+
}),
|
|
5183
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
5184
|
+
lines,
|
|
5185
|
+
" line",
|
|
5186
|
+
lines === 1 ? "" : "s"
|
|
5187
|
+
] })
|
|
5188
|
+
]
|
|
4645
5189
|
})
|
|
4646
|
-
|
|
4647
|
-
value: document.content ?? "",
|
|
4648
|
-
path: document.path,
|
|
4649
|
-
"data-testid": "author-editor",
|
|
4650
|
-
controllerRef: editor,
|
|
4651
|
-
className: "min-h-0 flex-1",
|
|
4652
|
-
onChange: (content) => reviseAuthorDocument(sessionId, document.path, content),
|
|
4653
|
-
onSelect: (range) => {
|
|
4654
|
-
const current = authorDocument(sessionId, document.path);
|
|
4655
|
-
const bounds = host.current?.getBoundingClientRect();
|
|
4656
|
-
if (!current || !bounds || range.from === range.to) return;
|
|
4657
|
-
setAuthorRegionCandidate(sessionId, document.path, {
|
|
4658
|
-
documentPath: current.path,
|
|
4659
|
-
revision: current.version,
|
|
4660
|
-
sourceSha256: current.sourceSha256,
|
|
4661
|
-
quote: range.text,
|
|
4662
|
-
anchor: {
|
|
4663
|
-
kind: "text-range",
|
|
4664
|
-
startOffset: range.from,
|
|
4665
|
-
endOffset: range.to,
|
|
4666
|
-
startLine: range.startLine,
|
|
4667
|
-
endLine: range.endLine
|
|
4668
|
-
},
|
|
4669
|
-
viewport: {
|
|
4670
|
-
width: bounds.width,
|
|
4671
|
-
height: bounds.height
|
|
4672
|
-
},
|
|
4673
|
-
createdAt: Date.now()
|
|
4674
|
-
});
|
|
4675
|
-
}
|
|
4676
|
-
}), /* @__PURE__ */ jsxs("footer", {
|
|
4677
|
-
className: "flex shrink-0 items-center gap-2 border-t border-po-border bg-po-bg px-4 py-1.5 text-sm text-po-dim",
|
|
4678
|
-
children: [
|
|
4679
|
-
/* @__PURE__ */ jsx("span", {
|
|
4680
|
-
className: "first-letter:uppercase",
|
|
4681
|
-
children: document.kind
|
|
4682
|
-
}),
|
|
4683
|
-
/* @__PURE__ */ jsx("span", {
|
|
4684
|
-
"aria-hidden": "true",
|
|
4685
|
-
children: "·"
|
|
4686
|
-
}),
|
|
4687
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
4688
|
-
lines,
|
|
4689
|
-
" line",
|
|
4690
|
-
lines === 1 ? "" : "s"
|
|
4691
|
-
] })
|
|
4692
|
-
]
|
|
4693
|
-
})]
|
|
5190
|
+
]
|
|
4694
5191
|
});
|
|
4695
5192
|
}
|
|
4696
5193
|
//#endregion
|
|
@@ -4704,6 +5201,13 @@ const MOBILE_TOOL_GLYPHS = {
|
|
|
4704
5201
|
};
|
|
4705
5202
|
const pendingCanvases = /* @__PURE__ */ new Map();
|
|
4706
5203
|
const canvasOwners = /* @__PURE__ */ new Map();
|
|
5204
|
+
const reason = (error) => error instanceof Error ? error.message : String(error);
|
|
5205
|
+
/** Closing a canvas nobody shows is best effort, but a failure is reported, never lost. */
|
|
5206
|
+
function closeCanvas(sessionId, alias) {
|
|
5207
|
+
api.session(sessionId).bridgeClose({ body: { alias } }).then((result) => {
|
|
5208
|
+
if (!result.ok) console.warn(`Author canvas '${alias}' did not close: ${result.error}`);
|
|
5209
|
+
}).catch((error) => console.warn(`Author canvas '${alias}' did not close: ${reason(error)}`));
|
|
5210
|
+
}
|
|
4707
5211
|
function tabId(path) {
|
|
4708
5212
|
let hash = 2166136261;
|
|
4709
5213
|
for (const char of normalizeAuthorPath(path)) hash = Math.imul(hash ^ char.charCodeAt(0), 16777619);
|
|
@@ -4810,8 +5314,10 @@ function AuthorDocumentPanel(props) {
|
|
|
4810
5314
|
const textPanel = /* @__PURE__ */ jsx(AuthorTextView, {
|
|
4811
5315
|
sessionId,
|
|
4812
5316
|
document,
|
|
4813
|
-
preview: document.kind === "markdown" && markdownPreview &&
|
|
5317
|
+
preview: document.kind === "markdown" && markdownPreview && !gridVisible,
|
|
5318
|
+
activeTool,
|
|
4814
5319
|
displayedRegions,
|
|
5320
|
+
candidate: drafts.candidate,
|
|
4815
5321
|
renderMediaReview: markdownMediaReview(props)
|
|
4816
5322
|
});
|
|
4817
5323
|
const sourcePanel = document.kind === "html" && !htmlSource ? /* @__PURE__ */ jsx(AuthorHtmlView, {
|
|
@@ -5049,7 +5555,7 @@ function authorFileTab(path, preferredAlias) {
|
|
|
5049
5555
|
const opened = result.data;
|
|
5050
5556
|
const canonicalAlias = opened.alias;
|
|
5051
5557
|
if (controller.signal.aborted || pendingCanvases.get(key) !== controller) {
|
|
5052
|
-
if ((canvasOwners.get(`${sessionId}\n${canonicalAlias}`)?.size ?? 0) === 0)
|
|
5558
|
+
if ((canvasOwners.get(`${sessionId}\n${canonicalAlias}`)?.size ?? 0) === 0) closeCanvas(sessionId, canonicalAlias);
|
|
5053
5559
|
return;
|
|
5054
5560
|
}
|
|
5055
5561
|
canvasAliases.setState((state) => ({
|
|
@@ -5096,10 +5602,11 @@ function authorFileTab(path, preferredAlias) {
|
|
|
5096
5602
|
body: { path: normalized },
|
|
5097
5603
|
query: { session: sessionId }
|
|
5098
5604
|
}).then((result) => {
|
|
5099
|
-
if (!result.ok
|
|
5605
|
+
if (!result.ok) throw new Error(result.error);
|
|
5606
|
+
if (!("alias" in result.data) || typeof result.data.alias !== "string") return;
|
|
5100
5607
|
const recoveredAlias = result.data.alias;
|
|
5101
|
-
if ((canvasOwners.get(`${sessionId}\n${recoveredAlias}`)?.size ?? 0) === 0)
|
|
5102
|
-
}).catch(() =>
|
|
5608
|
+
if ((canvasOwners.get(`${sessionId}\n${recoveredAlias}`)?.size ?? 0) === 0) closeCanvas(sessionId, recoveredAlias);
|
|
5609
|
+
}).catch((error) => console.warn(`Author could not check for a canvas left open for '${normalized}': ${reason(error)}`));
|
|
5103
5610
|
return;
|
|
5104
5611
|
}
|
|
5105
5612
|
const owners = canvasOwners.get(`${sessionId}\n${alias}`);
|
|
@@ -5122,7 +5629,7 @@ function authorFileTab(path, preferredAlias) {
|
|
|
5122
5629
|
delete next[key];
|
|
5123
5630
|
return next;
|
|
5124
5631
|
});
|
|
5125
|
-
if (lastOwner)
|
|
5632
|
+
if (lastOwner) closeCanvas(sessionId, alias);
|
|
5126
5633
|
},
|
|
5127
5634
|
panel: (props) => /* @__PURE__ */ jsx(AuthorDocumentPanel, {
|
|
5128
5635
|
...props,
|