@agimon-ai/po-author 0.0.0-alpha.34 → 0.0.0-alpha.35
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 +520 -299
- package/dist/extensions/web.mjs.map +1 -1
- package/package.json +8 -8
package/dist/extensions/web.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { apiResponse, createApiClient, defineApiRoutes, defineChannelFile, defineDockFace, defineFileLinks, defineFill, defineGlobalStore, defineLeaderBinding, defineMinorModeFile, defineSessionStore, defineSlotFile, defineToolRenderer, defineWebLifecycle, defineWebPlugin, sessionApiAddress, toolResultTextLines } from "@agimon-ai/po-core/web";
|
|
2
2
|
import { Store } from "@tanstack/store";
|
|
3
3
|
import { useStore } from "@tanstack/react-store";
|
|
4
|
-
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
import { Button, CodeEditor, Dot, FileIcon, Markdown, MediaPreview, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, MessageLines, SparklesIcon, StatusBadge, TrashIcon, collapseLines, toolTone } from "@agimon-ai/po-web-components";
|
|
6
|
+
import { Button, CodeEditor, Dot, FileIcon, Markdown, MediaPreview, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, MessageLines, SparklesIcon, StatusBadge, TrashIcon, collapseLines, drawableImageUrl, markdownMediaReview, markdownMediaReviewSlot, svgImageUrl, toolTone } from "@agimon-ai/po-web-components";
|
|
7
7
|
import { sealedTransport } from "@agimon-ai/po-web-security/browser";
|
|
8
8
|
//#region src/types/webAuthor.ts
|
|
9
9
|
const authorChannelType = "author_webmcp";
|
|
@@ -1219,6 +1219,8 @@ var author_webmcp_web_default = defineChannelFile({
|
|
|
1219
1219
|
//#endregion
|
|
1220
1220
|
//#region src/constants/author.ts
|
|
1221
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:";
|
|
1222
1224
|
//#endregion
|
|
1223
1225
|
//#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorGridOverlay.tsx
|
|
1224
1226
|
function autonomousVoiceGridVisible(statuses) {
|
|
@@ -1396,7 +1398,8 @@ function createAuthorCapturePacket(captureId, capturedAt, document, regions, ali
|
|
|
1396
1398
|
revision: document.version,
|
|
1397
1399
|
...document.sourceSha256 === void 0 ? {} : { sourceSha256: document.sourceSha256 },
|
|
1398
1400
|
...document.storyPreview === void 0 ? {} : { storyPreview: structuredClone(document.storyPreview) },
|
|
1399
|
-
...document.imageArtifact === void 0 ? {} : { imageArtifact: { ...document.imageArtifact } }
|
|
1401
|
+
...document.imageArtifact === void 0 ? {} : { imageArtifact: { ...document.imageArtifact } },
|
|
1402
|
+
...document.markdownMedia === void 0 ? {} : { markdownMedia: { ...document.markdownMedia } }
|
|
1400
1403
|
},
|
|
1401
1404
|
regions: regions.map((region, index) => {
|
|
1402
1405
|
if (region.documentPath !== document.path || region.revision !== document.version || region.sourceSha256 !== document.sourceSha256) throw new Error(`Region ${String(index + 1)} is stale for the focused document.`);
|
|
@@ -1462,12 +1465,25 @@ function storyPreviewProvenance(packet) {
|
|
|
1462
1465
|
"After editing the underlying story or component source, rebuild the preview before treating it as visually verified."
|
|
1463
1466
|
].join("\n");
|
|
1464
1467
|
}
|
|
1468
|
+
function filePath(path) {
|
|
1469
|
+
return path.startsWith("artifact:") ? `${path} (relative to the session artifact folder)` : path;
|
|
1470
|
+
}
|
|
1471
|
+
function markdownMediaProvenance(packet) {
|
|
1472
|
+
const media = packet.document.markdownMedia;
|
|
1473
|
+
if (media === void 0) return void 0;
|
|
1474
|
+
const lines = [media.kind === "mermaid" ? "Reviewed a mermaid diagram rendered from Markdown. Change the diagram by editing its source." : "Reviewed an image shown in rendered Markdown. Change it by editing or regenerating its source file."];
|
|
1475
|
+
if (media.sourcePath !== void 0) lines.push(`Source image: ${filePath(media.sourcePath)}`);
|
|
1476
|
+
if (media.documentPath !== void 0) lines.push(`Embedded in document: ${filePath(media.documentPath)}`);
|
|
1477
|
+
if (media.documentPath === void 0 && media.sourcePath === void 0) lines.push("Embedded in a conversation message, not a file.");
|
|
1478
|
+
if (media.mermaidSource !== void 0) lines.push("Mermaid source:", "```mermaid", media.mermaidSource, "```");
|
|
1479
|
+
return lines.join("\n");
|
|
1480
|
+
}
|
|
1465
1481
|
function authorCaptureContext(packet) {
|
|
1466
1482
|
const metadata = JSON.stringify(packet);
|
|
1467
1483
|
if (bytes(metadata) > 65536) throw new Error("Author capture packet exceeds 64 KiB.");
|
|
1468
1484
|
const annotations = packet.regions.map((region) => `(${String(region.ordinal)}) ${region.comment} [${anchorLocation(region.anchor)}]\nregionId: ${JSON.stringify(region.id)}${region.quote ? `\nQuoted text: ${region.quote}` : ""}`).join("\n\n");
|
|
1469
1485
|
const artifact = packet.document.imageArtifact;
|
|
1470
|
-
const provenance = artifact === void 0 ? storyPreviewProvenance(packet) : [
|
|
1486
|
+
const provenance = artifact === void 0 ? markdownMediaProvenance(packet) ?? storyPreviewProvenance(packet) : [
|
|
1471
1487
|
"Generated image feedback: preserve the original image; generate a new refinement only when requested.",
|
|
1472
1488
|
`Original artifact: ${artifact.id} (session ${artifact.sessionId})`,
|
|
1473
1489
|
`Original canonical path: ${artifact.originalPath}`,
|
|
@@ -1493,7 +1509,7 @@ function canvasBlob(canvas, type, quality) {
|
|
|
1493
1509
|
canvas.toBlob((blob) => blob === null ? reject(/* @__PURE__ */ new Error("Unable to capture viewport")) : resolve(blob), type, quality);
|
|
1494
1510
|
});
|
|
1495
1511
|
}
|
|
1496
|
-
async function base64(blob) {
|
|
1512
|
+
async function base64$1(blob) {
|
|
1497
1513
|
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
1498
1514
|
let binary = "";
|
|
1499
1515
|
for (let offset = 0; offset < bytes.length; offset += 32768) binary += String.fromCharCode(...bytes.subarray(offset, offset + 32768));
|
|
@@ -1518,7 +1534,7 @@ async function boundedCanvasCapture(canvas) {
|
|
|
1518
1534
|
}
|
|
1519
1535
|
if (blob.size > 2097152) throw new Error("Viewport capture exceeds 2 MiB");
|
|
1520
1536
|
return {
|
|
1521
|
-
data: await base64(blob),
|
|
1537
|
+
data: await base64$1(blob),
|
|
1522
1538
|
mimeType
|
|
1523
1539
|
};
|
|
1524
1540
|
}
|
|
@@ -2047,222 +2063,6 @@ function AuthorRequestLog({ requests }) {
|
|
|
2047
2063
|
});
|
|
2048
2064
|
}
|
|
2049
2065
|
//#endregion
|
|
2050
|
-
//#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorToolPalette.tsx
|
|
2051
|
-
/** One tool tile. An active annotation tool is the person's mark (rose); an active view tool is Po's jade. */
|
|
2052
|
-
function authorToolClassName(active, annotates) {
|
|
2053
|
-
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"}`;
|
|
2054
|
-
}
|
|
2055
|
-
function AuthorToolPalette({ sessionId, path, kind, activeTool }) {
|
|
2056
|
-
const [error, setError] = useState();
|
|
2057
|
-
const format = (style) => {
|
|
2058
|
-
const selected = authorDocumentAnnotations(sessionId, path).candidate;
|
|
2059
|
-
const document = selected && authorDocument(sessionId, selected.documentPath);
|
|
2060
|
-
if (!selected || !document || selected.anchor.kind !== "text-range" || selected.revision !== document.version || selected.sourceSha256 !== document.sourceSha256) {
|
|
2061
|
-
setError("Select text in the document first.");
|
|
2062
|
-
return;
|
|
2063
|
-
}
|
|
2064
|
-
const { startOffset, endOffset } = selected.anchor;
|
|
2065
|
-
const content = document.content ?? "";
|
|
2066
|
-
const text = content.slice(startOffset, endOffset);
|
|
2067
|
-
const replacement = style === "Bold" ? `**${text}**` : style === "Link" ? `[${text}](https://)` : text.split("\n").map((line) => `${style === "Heading" ? "## " : "- "}${line}`).join("\n");
|
|
2068
|
-
reviseAuthorDocument(sessionId, document.path, content.slice(0, startOffset) + replacement + content.slice(endOffset));
|
|
2069
|
-
setAuthorRegionCandidate(sessionId, path, void 0);
|
|
2070
|
-
setError(void 0);
|
|
2071
|
-
};
|
|
2072
|
-
const tools = [
|
|
2073
|
-
...[
|
|
2074
|
-
"image",
|
|
2075
|
-
"pdf",
|
|
2076
|
-
"video",
|
|
2077
|
-
"story-preview"
|
|
2078
|
-
].includes(kind) ? [{
|
|
2079
|
-
label: "Pan",
|
|
2080
|
-
glyph: "✋",
|
|
2081
|
-
action: () => setAuthorToolMode(sessionId, path, activeTool === "pan" ? "select" : "pan"),
|
|
2082
|
-
active: activeTool === "pan",
|
|
2083
|
-
annotates: false
|
|
2084
|
-
}, {
|
|
2085
|
-
label: "Draw",
|
|
2086
|
-
glyph: "✎",
|
|
2087
|
-
action: () => setAuthorToolMode(sessionId, path, activeTool === "draw" ? "select" : "draw"),
|
|
2088
|
-
active: activeTool === "draw",
|
|
2089
|
-
annotates: true
|
|
2090
|
-
}] : [],
|
|
2091
|
-
{
|
|
2092
|
-
label: "Region",
|
|
2093
|
-
glyph: "⌗",
|
|
2094
|
-
action: () => setAuthorToolMode(sessionId, path, activeTool === "mark" ? "select" : "mark"),
|
|
2095
|
-
active: activeTool === "mark",
|
|
2096
|
-
annotates: true
|
|
2097
|
-
},
|
|
2098
|
-
...kind === "markdown" ? [
|
|
2099
|
-
{
|
|
2100
|
-
label: "Bold",
|
|
2101
|
-
glyph: "B",
|
|
2102
|
-
action: () => format("Bold")
|
|
2103
|
-
},
|
|
2104
|
-
{
|
|
2105
|
-
label: "Heading",
|
|
2106
|
-
glyph: "H",
|
|
2107
|
-
action: () => format("Heading")
|
|
2108
|
-
},
|
|
2109
|
-
{
|
|
2110
|
-
label: "Link",
|
|
2111
|
-
glyph: "↗",
|
|
2112
|
-
action: () => format("Link")
|
|
2113
|
-
},
|
|
2114
|
-
{
|
|
2115
|
-
label: "List",
|
|
2116
|
-
glyph: "≡",
|
|
2117
|
-
action: () => format("List")
|
|
2118
|
-
}
|
|
2119
|
-
] : [],
|
|
2120
|
-
{
|
|
2121
|
-
label: "Comment",
|
|
2122
|
-
glyph: "●",
|
|
2123
|
-
action: () => setAuthorToolMode(sessionId, path, activeTool === "comment" ? "select" : "comment"),
|
|
2124
|
-
active: activeTool === "comment",
|
|
2125
|
-
annotates: true
|
|
2126
|
-
}
|
|
2127
|
-
];
|
|
2128
|
-
return /* @__PURE__ */ jsxs("section", {
|
|
2129
|
-
"data-testid": "author-tool-palette",
|
|
2130
|
-
className: "space-y-2",
|
|
2131
|
-
children: [
|
|
2132
|
-
/* @__PURE__ */ jsx("h3", {
|
|
2133
|
-
className: "sr-only",
|
|
2134
|
-
children: "Tools"
|
|
2135
|
-
}),
|
|
2136
|
-
/* @__PURE__ */ jsx("div", {
|
|
2137
|
-
className: "grid grid-cols-4 gap-1.5",
|
|
2138
|
-
children: tools.map((tool) => /* @__PURE__ */ jsxs(Button, {
|
|
2139
|
-
size: "md",
|
|
2140
|
-
variant: "outline",
|
|
2141
|
-
"aria-label": tool.label === "Region" ? "mark region" : tool.label,
|
|
2142
|
-
"aria-pressed": tool.active,
|
|
2143
|
-
onClick: tool.action,
|
|
2144
|
-
className: authorToolClassName(tool.active === true, tool.annotates === true),
|
|
2145
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
2146
|
-
"aria-hidden": "true",
|
|
2147
|
-
className: "text-lg leading-none",
|
|
2148
|
-
children: tool.glyph
|
|
2149
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
2150
|
-
className: "text-sm",
|
|
2151
|
-
children: tool.label
|
|
2152
|
-
})]
|
|
2153
|
-
}, tool.label))
|
|
2154
|
-
}),
|
|
2155
|
-
/* @__PURE__ */ jsx("p", {
|
|
2156
|
-
className: "text-base leading-normal text-po-dim sm:text-sm",
|
|
2157
|
-
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."
|
|
2158
|
-
}),
|
|
2159
|
-
error ? /* @__PURE__ */ jsx("output", {
|
|
2160
|
-
className: "block text-base text-po-red",
|
|
2161
|
-
children: error
|
|
2162
|
-
}) : null
|
|
2163
|
-
]
|
|
2164
|
-
});
|
|
2165
|
-
}
|
|
2166
|
-
//#endregion
|
|
2167
|
-
//#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorPanel.tsx
|
|
2168
|
-
function AuthorPanel({ sessionId, submitCapture, statuses, renderSlot, renderSessionActivity }) {
|
|
2169
|
-
const documents = useStore(authorWorkspace.store, (state) => {
|
|
2170
|
-
if (sessionId === null) return [];
|
|
2171
|
-
const prefix = `${sessionId}\n`;
|
|
2172
|
-
return Object.entries(state.documents).filter(([key]) => key.startsWith(prefix)).map(([, document]) => document);
|
|
2173
|
-
});
|
|
2174
|
-
const workspace = useStore(authorWorkspace.store, (state) => sessionId === null ? void 0 : state.sessions[sessionId]);
|
|
2175
|
-
const grid = useStore(authorGrid.store, (state) => sessionId === null ? void 0 : state.sessions[sessionId]);
|
|
2176
|
-
const focused = documents.find((document) => document.path === workspace?.focusedDocument?.path);
|
|
2177
|
-
const focusedPath = workspace?.focusedDocument?.path;
|
|
2178
|
-
const requests = (workspace?.requests ?? []).filter((request) => focusedPath === void 0 || request.documentPath === focusedPath);
|
|
2179
|
-
const drafts = useStore(authorWorkspace.store, (state) => authorDocumentAnnotations(sessionId, focusedPath ?? "", state));
|
|
2180
|
-
const activity = sessionId !== null && focusedPath !== void 0 && requests.some((request) => request.status === "REQUESTED" || request.status === "CHANGING") ? renderSessionActivity?.() ?? null : null;
|
|
2181
|
-
return /* @__PURE__ */ jsxs("section", {
|
|
2182
|
-
"data-testid": "author-panel",
|
|
2183
|
-
className: "flex min-h-0 flex-1 flex-col overflow-hidden",
|
|
2184
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
2185
|
-
className: "min-h-0 flex-1 overflow-y-auto p-3",
|
|
2186
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
2187
|
-
className: "flex flex-col gap-3",
|
|
2188
|
-
children: [
|
|
2189
|
-
/* @__PURE__ */ jsx("div", {
|
|
2190
|
-
"data-testid": "author-preview-providers",
|
|
2191
|
-
className: "empty:hidden",
|
|
2192
|
-
children: renderSlot("author.preview-provider")
|
|
2193
|
-
}),
|
|
2194
|
-
sessionId !== null && workspace !== void 0 && focused !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2195
|
-
/* @__PURE__ */ jsx(AuthorToolPalette, {
|
|
2196
|
-
sessionId,
|
|
2197
|
-
path: focused.path,
|
|
2198
|
-
kind: focused.kind,
|
|
2199
|
-
activeTool: workspace.toolsByDocument[normalizeAuthorPath(focused.path)] ?? "select"
|
|
2200
|
-
}),
|
|
2201
|
-
/* @__PURE__ */ jsx("h2", {
|
|
2202
|
-
className: "border-t border-po-border-soft pt-3 text-lg font-semibold text-po-hi",
|
|
2203
|
-
children: "Annotations"
|
|
2204
|
-
}),
|
|
2205
|
-
/* @__PURE__ */ jsx("p", {
|
|
2206
|
-
className: "text-base leading-normal text-po-dim sm:text-sm",
|
|
2207
|
-
children: "Drag a region or place a point, add feedback, then submit here. Requests queue while the agent is working."
|
|
2208
|
-
}),
|
|
2209
|
-
focused.kind === "video" ? /* @__PURE__ */ jsx("p", {
|
|
2210
|
-
className: "text-base text-po-dim sm:text-sm",
|
|
2211
|
-
children: "Annotations reference video frames. They do not save changes to the source video."
|
|
2212
|
-
}) : null,
|
|
2213
|
-
autonomousVoiceGridVisible(statuses) && grid !== void 0 ? /* @__PURE__ */ jsxs("div", {
|
|
2214
|
-
"data-testid": "author-grid-snapshot",
|
|
2215
|
-
className: "rounded border border-po-red/40 bg-po-red/5 p-2 text-base text-po-dim",
|
|
2216
|
-
children: [/* @__PURE__ */ jsx("strong", {
|
|
2217
|
-
className: "text-po-red",
|
|
2218
|
-
children: "VOICE GRID A1–H8"
|
|
2219
|
-
}), /* @__PURE__ */ jsxs("p", {
|
|
2220
|
-
className: "mt-1 truncate",
|
|
2221
|
-
children: ["token ", grid.geometryToken]
|
|
2222
|
-
})]
|
|
2223
|
-
}) : null,
|
|
2224
|
-
/* @__PURE__ */ jsx("div", {
|
|
2225
|
-
className: "max-sm:hidden",
|
|
2226
|
-
children: /* @__PURE__ */ jsx(AuthorFeedbackControls, {
|
|
2227
|
-
sessionId,
|
|
2228
|
-
document: focused,
|
|
2229
|
-
drafts,
|
|
2230
|
-
submitCapture
|
|
2231
|
-
}, focused.path)
|
|
2232
|
-
})
|
|
2233
|
-
] }) : null,
|
|
2234
|
-
/* @__PURE__ */ jsx("div", {
|
|
2235
|
-
className: focused === void 0 ? void 0 : "border-t border-po-border-soft pt-3",
|
|
2236
|
-
children: /* @__PURE__ */ jsx(AuthorRequestLog, { requests })
|
|
2237
|
-
})
|
|
2238
|
-
]
|
|
2239
|
-
})
|
|
2240
|
-
}), activity === null ? null : /* @__PURE__ */ jsx("footer", {
|
|
2241
|
-
"data-testid": "author-session-activity",
|
|
2242
|
-
className: "shrink-0 border-t border-po-border bg-po-rail p-3",
|
|
2243
|
-
children: activity
|
|
2244
|
-
})]
|
|
2245
|
-
});
|
|
2246
|
-
}
|
|
2247
|
-
//#endregion
|
|
2248
|
-
//#region src/extensions/workspaces/sessions/(frontend)/dock/authoring.web.tsx
|
|
2249
|
-
var authoring_web_default = defineDockFace({
|
|
2250
|
-
id: "authoring",
|
|
2251
|
-
label: "Author",
|
|
2252
|
-
order: 30,
|
|
2253
|
-
autoSelect: ({ tabId }) => tabId?.startsWith(AUTHOR_FILE_TAB_PREFIX) === true,
|
|
2254
|
-
panel: AuthorPanel,
|
|
2255
|
-
visibility: {
|
|
2256
|
-
subscribe(listener) {
|
|
2257
|
-
const subscription = authorWorkspace.store.subscribe(listener);
|
|
2258
|
-
return () => subscription.unsubscribe();
|
|
2259
|
-
},
|
|
2260
|
-
isVisible(sessionId) {
|
|
2261
|
-
return sessionId !== null && authorWorkspace.store.state.sessions[sessionId]?.focusedDocument !== void 0;
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
});
|
|
2265
|
-
//#endregion
|
|
2266
2066
|
//#region src/types/apiRoutes.ts
|
|
2267
2067
|
/**
|
|
2268
2068
|
* This package's routes, as data.
|
|
@@ -2373,27 +2173,6 @@ const api = createApiClient(apiRoutes_default, {
|
|
|
2373
2173
|
sessionAddress: sessionApiAddress
|
|
2374
2174
|
});
|
|
2375
2175
|
//#endregion
|
|
2376
|
-
//#region src/types/authorPreview.ts
|
|
2377
|
-
function parseAuthorOpenSourceAction(input) {
|
|
2378
|
-
if (typeof input !== "object" || input === null) return null;
|
|
2379
|
-
const candidate = input;
|
|
2380
|
-
return candidate.version === 1 && typeof candidate.createTab === "function" ? candidate : null;
|
|
2381
|
-
}
|
|
2382
|
-
function parseAuthorPreviewAction(input) {
|
|
2383
|
-
if (typeof input !== "object" || input === null) return null;
|
|
2384
|
-
const candidate = input;
|
|
2385
|
-
if (candidate.version !== 1 || typeof candidate.label !== "string" || candidate.label.trim() === "") return null;
|
|
2386
|
-
if (candidate.detail !== void 0 && typeof candidate.detail !== "string") return null;
|
|
2387
|
-
if (typeof candidate.createTab !== "function") return null;
|
|
2388
|
-
if (candidate.supportsSource !== void 0 && typeof candidate.supportsSource !== "function") return null;
|
|
2389
|
-
if (candidate.embeddedPanel !== void 0 && typeof candidate.embeddedPanel !== "function") return null;
|
|
2390
|
-
return candidate;
|
|
2391
|
-
}
|
|
2392
|
-
const authorPreviewActionSlot = {
|
|
2393
|
-
slot: "author.preview-action",
|
|
2394
|
-
parse: parseAuthorPreviewAction
|
|
2395
|
-
};
|
|
2396
|
-
//#endregion
|
|
2397
2176
|
//#region src/types/authorApi.ts
|
|
2398
2177
|
/** Query parameter naming the session a document request belongs to. */
|
|
2399
2178
|
const SESSION_QUERY_PARAM = "session";
|
|
@@ -2422,6 +2201,10 @@ function structuredFormat(path) {
|
|
|
2422
2201
|
if (lower.endsWith(".pptx")) return "pptx";
|
|
2423
2202
|
if (lower.endsWith(".xlsx")) return "xlsx";
|
|
2424
2203
|
}
|
|
2204
|
+
/** A Mermaid source file: edited as text, previewed as the diagram it draws. */
|
|
2205
|
+
function isMermaidPath(path) {
|
|
2206
|
+
return /\.(?:mmd|mermaid)$/iu.test(path);
|
|
2207
|
+
}
|
|
2425
2208
|
function authorKindForPath(path) {
|
|
2426
2209
|
const lower = path.toLowerCase();
|
|
2427
2210
|
if (/\.stories\.(?:js|jsx|ts|tsx)$/u.test(lower)) return "story-preview";
|
|
@@ -2429,7 +2212,7 @@ function authorKindForPath(path) {
|
|
|
2429
2212
|
if (format === "markdown-slides") return "slides";
|
|
2430
2213
|
if (format !== void 0) return format;
|
|
2431
2214
|
const extension = lower.split(".").at(-1) ?? "";
|
|
2432
|
-
if (extension === "md") return "markdown";
|
|
2215
|
+
if (extension === "md" || isMermaidPath(lower)) return "markdown";
|
|
2433
2216
|
if (IMAGE_EXTENSIONS.has(extension)) return "image";
|
|
2434
2217
|
if (VIDEO_EXTENSIONS.has(extension)) return "video";
|
|
2435
2218
|
if (extension === "pdf") return "pdf";
|
|
@@ -2460,6 +2243,27 @@ function authorSessionFileUrl(sessionId, path) {
|
|
|
2460
2243
|
if (path.startsWith("artifact:")) return artifactApi.session(sessionId).content.url({ query: { path: path.slice(9) } });
|
|
2461
2244
|
return api.session(sessionId).file.url({ query: { path } });
|
|
2462
2245
|
}
|
|
2246
|
+
/**
|
|
2247
|
+
* A Markdown image path read against the document's own folder, as GitHub reads
|
|
2248
|
+
* it, keeping the `artifact:` scheme. Artifact paths are normalised here because
|
|
2249
|
+
* the artifact route refuses `..`; it still owns containment.
|
|
2250
|
+
*/
|
|
2251
|
+
function authorMarkdownImagePath(documentPath, imagePath) {
|
|
2252
|
+
const artifact = documentPath.startsWith(AUTHOR_ARTIFACT_PREFIX);
|
|
2253
|
+
const local = artifact ? documentPath.slice(9) : documentPath;
|
|
2254
|
+
if (imagePath.startsWith("/")) {
|
|
2255
|
+
if (artifact) throw new Error("Artifact images must use a relative path.");
|
|
2256
|
+
return imagePath;
|
|
2257
|
+
}
|
|
2258
|
+
const segments = local.split("/").slice(0, -1);
|
|
2259
|
+
for (const segment of imagePath.split("/")) {
|
|
2260
|
+
if (segment === "" || segment === ".") continue;
|
|
2261
|
+
if (segment !== "..") segments.push(segment);
|
|
2262
|
+
else if (segments.length === 0 || segments.at(-1) === "") throw new Error("The image is outside this folder.");
|
|
2263
|
+
else segments.pop();
|
|
2264
|
+
}
|
|
2265
|
+
return `${artifact ? AUTHOR_ARTIFACT_PREFIX : ""}${segments.join("/")}`;
|
|
2266
|
+
}
|
|
2463
2267
|
/** The file name a person sees: the last path segment, without the `artifact:` scheme. */
|
|
2464
2268
|
function authorFileName(path) {
|
|
2465
2269
|
const local = path.startsWith("artifact:") ? path.slice(9) : path;
|
|
@@ -2575,55 +2379,292 @@ async function croppedImageBytes(documentInput, signal) {
|
|
|
2575
2379
|
bitmap.close();
|
|
2576
2380
|
}
|
|
2577
2381
|
}
|
|
2578
|
-
async function bytesToSave(sessionId, document, signal) {
|
|
2579
|
-
if (document.kind === "text" || document.kind === "markdown" || document.kind === "story-preview") return document.content ?? "";
|
|
2580
|
-
if (document.kind === "image" && document.crop !== void 0) return await croppedImageBytes(document, signal);
|
|
2581
|
-
if (document.structuredFormat === void 0) throw new Error("This Author view cannot be saved.");
|
|
2582
|
-
const operations = operationsFor(document);
|
|
2583
|
-
const request = {
|
|
2584
|
-
path: document.path,
|
|
2585
|
-
format: document.structuredFormat,
|
|
2586
|
-
operations,
|
|
2587
|
-
...document.csvDialect === void 0 ? {} : { csvDialect: document.csvDialect }
|
|
2588
|
-
};
|
|
2589
|
-
const preflight = await documentRequest(api.session(sessionId).documentsPreflight, sessionId, request, signal);
|
|
2590
|
-
if (preflight.sourceDigest !== void 0 && preflight.sourceDigest !== document.sourceSha256) throw new Error("Document source changed before saving. Reopen the document.");
|
|
2591
|
-
if (!preflight.accepted) throw new Error(preflight.issues.map((issue) => issue.message).join("\n") || "Author preflight rejected the edit.");
|
|
2592
|
-
const serialized = await documentRequest(api.session(sessionId).documentsSerialize, sessionId, {
|
|
2593
|
-
...request,
|
|
2594
|
-
preflightDigest: preflight.digest
|
|
2595
|
-
}, signal);
|
|
2596
|
-
if (serialized.encoding !== "base64") throw new Error("Author serialize returned an unsupported encoding.");
|
|
2597
|
-
const bytes = decodeBase64(serialized.bytes);
|
|
2598
|
-
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
2382
|
+
async function bytesToSave(sessionId, document, signal) {
|
|
2383
|
+
if (document.kind === "text" || document.kind === "markdown" || document.kind === "story-preview") return document.content ?? "";
|
|
2384
|
+
if (document.kind === "image" && document.crop !== void 0) return await croppedImageBytes(document, signal);
|
|
2385
|
+
if (document.structuredFormat === void 0) throw new Error("This Author view cannot be saved.");
|
|
2386
|
+
const operations = operationsFor(document);
|
|
2387
|
+
const request = {
|
|
2388
|
+
path: document.path,
|
|
2389
|
+
format: document.structuredFormat,
|
|
2390
|
+
operations,
|
|
2391
|
+
...document.csvDialect === void 0 ? {} : { csvDialect: document.csvDialect }
|
|
2392
|
+
};
|
|
2393
|
+
const preflight = await documentRequest(api.session(sessionId).documentsPreflight, sessionId, request, signal);
|
|
2394
|
+
if (preflight.sourceDigest !== void 0 && preflight.sourceDigest !== document.sourceSha256) throw new Error("Document source changed before saving. Reopen the document.");
|
|
2395
|
+
if (!preflight.accepted) throw new Error(preflight.issues.map((issue) => issue.message).join("\n") || "Author preflight rejected the edit.");
|
|
2396
|
+
const serialized = await documentRequest(api.session(sessionId).documentsSerialize, sessionId, {
|
|
2397
|
+
...request,
|
|
2398
|
+
preflightDigest: preflight.digest
|
|
2399
|
+
}, signal);
|
|
2400
|
+
if (serialized.encoding !== "base64") throw new Error("Author serialize returned an unsupported encoding.");
|
|
2401
|
+
const bytes = decodeBase64(serialized.bytes);
|
|
2402
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
2403
|
+
}
|
|
2404
|
+
async function saveAuthorDocument(sessionId, document, signal) {
|
|
2405
|
+
if (document.path.startsWith("artifact:")) throw new Error("Artifacts are read-only.");
|
|
2406
|
+
if (document.sourceSha256 === void 0) throw new Error("This Author view has no source digest.");
|
|
2407
|
+
const response = await sealedTransport.fetch(authorSessionFileUrl(sessionId, document.path), {
|
|
2408
|
+
method: "PUT",
|
|
2409
|
+
headers: {
|
|
2410
|
+
"Content-Type": "application/octet-stream",
|
|
2411
|
+
"X-Expected-SHA256": document.sourceSha256
|
|
2412
|
+
},
|
|
2413
|
+
body: await bytesToSave(sessionId, document, signal),
|
|
2414
|
+
signal
|
|
2415
|
+
});
|
|
2416
|
+
if (!response.ok) throw new Error(`Author save failed (${response.status})`);
|
|
2417
|
+
const sha256 = response.headers.get("X-File-SHA256");
|
|
2418
|
+
if (sha256 === null || !/^[a-f0-9]{64}$/.test(sha256)) throw new Error("Author save returned an invalid digest.");
|
|
2419
|
+
return sha256;
|
|
2420
|
+
}
|
|
2421
|
+
function csvDialectFromMetadata(metadata) {
|
|
2422
|
+
if (metadata === void 0) return void 0;
|
|
2423
|
+
const { delimiter, quote, recordDelimiter } = metadata;
|
|
2424
|
+
if (delimiter !== "," && delimiter !== ";" && delimiter !== " " && delimiter !== "|" || quote !== "\"" && quote !== "'" || recordDelimiter !== "\n" && recordDelimiter !== "\r\n") throw new Error("Author document API returned an invalid CSV dialect.");
|
|
2425
|
+
return {
|
|
2426
|
+
delimiter,
|
|
2427
|
+
quote,
|
|
2428
|
+
recordDelimiter
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
//#endregion
|
|
2432
|
+
//#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorToolPalette.tsx
|
|
2433
|
+
/** One tool tile. An active annotation tool is the person's mark (rose); an active view tool is Po's jade. */
|
|
2434
|
+
function authorToolClassName(active, annotates) {
|
|
2435
|
+
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"}`;
|
|
2436
|
+
}
|
|
2437
|
+
function AuthorToolPalette({ sessionId, path, kind, activeTool }) {
|
|
2438
|
+
const [error, setError] = useState();
|
|
2439
|
+
const format = (style) => {
|
|
2440
|
+
const selected = authorDocumentAnnotations(sessionId, path).candidate;
|
|
2441
|
+
const document = selected && authorDocument(sessionId, selected.documentPath);
|
|
2442
|
+
if (!selected || !document || selected.anchor.kind !== "text-range" || selected.revision !== document.version || selected.sourceSha256 !== document.sourceSha256) {
|
|
2443
|
+
setError("Select text in the document first.");
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
const { startOffset, endOffset } = selected.anchor;
|
|
2447
|
+
const content = document.content ?? "";
|
|
2448
|
+
const text = content.slice(startOffset, endOffset);
|
|
2449
|
+
const replacement = style === "Bold" ? `**${text}**` : style === "Link" ? `[${text}](https://)` : text.split("\n").map((line) => `${style === "Heading" ? "## " : "- "}${line}`).join("\n");
|
|
2450
|
+
reviseAuthorDocument(sessionId, document.path, content.slice(0, startOffset) + replacement + content.slice(endOffset));
|
|
2451
|
+
setAuthorRegionCandidate(sessionId, path, void 0);
|
|
2452
|
+
setError(void 0);
|
|
2453
|
+
};
|
|
2454
|
+
const tools = [
|
|
2455
|
+
...[
|
|
2456
|
+
"image",
|
|
2457
|
+
"pdf",
|
|
2458
|
+
"video",
|
|
2459
|
+
"story-preview"
|
|
2460
|
+
].includes(kind) ? [{
|
|
2461
|
+
label: "Pan",
|
|
2462
|
+
glyph: "✋",
|
|
2463
|
+
action: () => setAuthorToolMode(sessionId, path, activeTool === "pan" ? "select" : "pan"),
|
|
2464
|
+
active: activeTool === "pan",
|
|
2465
|
+
annotates: false
|
|
2466
|
+
}, {
|
|
2467
|
+
label: "Draw",
|
|
2468
|
+
glyph: "✎",
|
|
2469
|
+
action: () => setAuthorToolMode(sessionId, path, activeTool === "draw" ? "select" : "draw"),
|
|
2470
|
+
active: activeTool === "draw",
|
|
2471
|
+
annotates: true
|
|
2472
|
+
}] : [],
|
|
2473
|
+
{
|
|
2474
|
+
label: "Region",
|
|
2475
|
+
glyph: "⌗",
|
|
2476
|
+
action: () => setAuthorToolMode(sessionId, path, activeTool === "mark" ? "select" : "mark"),
|
|
2477
|
+
active: activeTool === "mark",
|
|
2478
|
+
annotates: true
|
|
2479
|
+
},
|
|
2480
|
+
...kind === "markdown" && !isMermaidPath(path) ? [
|
|
2481
|
+
{
|
|
2482
|
+
label: "Bold",
|
|
2483
|
+
glyph: "B",
|
|
2484
|
+
action: () => format("Bold")
|
|
2485
|
+
},
|
|
2486
|
+
{
|
|
2487
|
+
label: "Heading",
|
|
2488
|
+
glyph: "H",
|
|
2489
|
+
action: () => format("Heading")
|
|
2490
|
+
},
|
|
2491
|
+
{
|
|
2492
|
+
label: "Link",
|
|
2493
|
+
glyph: "↗",
|
|
2494
|
+
action: () => format("Link")
|
|
2495
|
+
},
|
|
2496
|
+
{
|
|
2497
|
+
label: "List",
|
|
2498
|
+
glyph: "≡",
|
|
2499
|
+
action: () => format("List")
|
|
2500
|
+
}
|
|
2501
|
+
] : [],
|
|
2502
|
+
{
|
|
2503
|
+
label: "Comment",
|
|
2504
|
+
glyph: "●",
|
|
2505
|
+
action: () => setAuthorToolMode(sessionId, path, activeTool === "comment" ? "select" : "comment"),
|
|
2506
|
+
active: activeTool === "comment",
|
|
2507
|
+
annotates: true
|
|
2508
|
+
}
|
|
2509
|
+
];
|
|
2510
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
2511
|
+
"data-testid": "author-tool-palette",
|
|
2512
|
+
className: "space-y-2",
|
|
2513
|
+
children: [
|
|
2514
|
+
/* @__PURE__ */ jsx("h3", {
|
|
2515
|
+
className: "sr-only",
|
|
2516
|
+
children: "Tools"
|
|
2517
|
+
}),
|
|
2518
|
+
/* @__PURE__ */ jsx("div", {
|
|
2519
|
+
className: "grid grid-cols-4 gap-1.5",
|
|
2520
|
+
children: tools.map((tool) => /* @__PURE__ */ jsxs(Button, {
|
|
2521
|
+
size: "md",
|
|
2522
|
+
variant: "outline",
|
|
2523
|
+
"aria-label": tool.label === "Region" ? "mark region" : tool.label,
|
|
2524
|
+
"aria-pressed": tool.active,
|
|
2525
|
+
onClick: tool.action,
|
|
2526
|
+
className: authorToolClassName(tool.active === true, tool.annotates === true),
|
|
2527
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2528
|
+
"aria-hidden": "true",
|
|
2529
|
+
className: "text-lg leading-none",
|
|
2530
|
+
children: tool.glyph
|
|
2531
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2532
|
+
className: "text-sm",
|
|
2533
|
+
children: tool.label
|
|
2534
|
+
})]
|
|
2535
|
+
}, tool.label))
|
|
2536
|
+
}),
|
|
2537
|
+
/* @__PURE__ */ jsx("p", {
|
|
2538
|
+
className: "text-base leading-normal text-po-dim sm:text-sm",
|
|
2539
|
+
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."
|
|
2540
|
+
}),
|
|
2541
|
+
error ? /* @__PURE__ */ jsx("output", {
|
|
2542
|
+
className: "block text-base text-po-red",
|
|
2543
|
+
children: error
|
|
2544
|
+
}) : null
|
|
2545
|
+
]
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
//#endregion
|
|
2549
|
+
//#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorPanel.tsx
|
|
2550
|
+
function AuthorPanel({ sessionId, submitCapture, statuses, renderSlot, renderSessionActivity }) {
|
|
2551
|
+
const documents = useStore(authorWorkspace.store, (state) => {
|
|
2552
|
+
if (sessionId === null) return [];
|
|
2553
|
+
const prefix = `${sessionId}\n`;
|
|
2554
|
+
return Object.entries(state.documents).filter(([key]) => key.startsWith(prefix)).map(([, document]) => document);
|
|
2555
|
+
});
|
|
2556
|
+
const workspace = useStore(authorWorkspace.store, (state) => sessionId === null ? void 0 : state.sessions[sessionId]);
|
|
2557
|
+
const grid = useStore(authorGrid.store, (state) => sessionId === null ? void 0 : state.sessions[sessionId]);
|
|
2558
|
+
const focused = documents.find((document) => document.path === workspace?.focusedDocument?.path);
|
|
2559
|
+
const focusedPath = workspace?.focusedDocument?.path;
|
|
2560
|
+
const requests = (workspace?.requests ?? []).filter((request) => focusedPath === void 0 || request.documentPath === focusedPath);
|
|
2561
|
+
const drafts = useStore(authorWorkspace.store, (state) => authorDocumentAnnotations(sessionId, focusedPath ?? "", state));
|
|
2562
|
+
const activity = sessionId !== null && focusedPath !== void 0 && requests.some((request) => request.status === "REQUESTED" || request.status === "CHANGING") ? renderSessionActivity?.() ?? null : null;
|
|
2563
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
2564
|
+
"data-testid": "author-panel",
|
|
2565
|
+
className: "flex min-h-0 flex-1 flex-col overflow-hidden",
|
|
2566
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2567
|
+
className: "min-h-0 flex-1 overflow-y-auto p-3",
|
|
2568
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2569
|
+
className: "flex flex-col gap-3",
|
|
2570
|
+
children: [
|
|
2571
|
+
/* @__PURE__ */ jsx("div", {
|
|
2572
|
+
"data-testid": "author-preview-providers",
|
|
2573
|
+
className: "empty:hidden",
|
|
2574
|
+
children: renderSlot("author.preview-provider")
|
|
2575
|
+
}),
|
|
2576
|
+
sessionId !== null && workspace !== void 0 && focused !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2577
|
+
/* @__PURE__ */ jsx(AuthorToolPalette, {
|
|
2578
|
+
sessionId,
|
|
2579
|
+
path: focused.path,
|
|
2580
|
+
kind: focused.kind,
|
|
2581
|
+
activeTool: workspace.toolsByDocument[normalizeAuthorPath(focused.path)] ?? "select"
|
|
2582
|
+
}),
|
|
2583
|
+
/* @__PURE__ */ jsx("h2", {
|
|
2584
|
+
className: "border-t border-po-border-soft pt-3 text-lg font-semibold text-po-hi",
|
|
2585
|
+
children: "Annotations"
|
|
2586
|
+
}),
|
|
2587
|
+
/* @__PURE__ */ jsx("p", {
|
|
2588
|
+
className: "text-base leading-normal text-po-dim sm:text-sm",
|
|
2589
|
+
children: "Drag a region or place a point, add feedback, then submit here. Requests queue while the agent is working."
|
|
2590
|
+
}),
|
|
2591
|
+
focused.kind === "video" ? /* @__PURE__ */ jsx("p", {
|
|
2592
|
+
className: "text-base text-po-dim sm:text-sm",
|
|
2593
|
+
children: "Annotations reference video frames. They do not save changes to the source video."
|
|
2594
|
+
}) : null,
|
|
2595
|
+
autonomousVoiceGridVisible(statuses) && grid !== void 0 ? /* @__PURE__ */ jsxs("div", {
|
|
2596
|
+
"data-testid": "author-grid-snapshot",
|
|
2597
|
+
className: "rounded border border-po-red/40 bg-po-red/5 p-2 text-base text-po-dim",
|
|
2598
|
+
children: [/* @__PURE__ */ jsx("strong", {
|
|
2599
|
+
className: "text-po-red",
|
|
2600
|
+
children: "VOICE GRID A1–H8"
|
|
2601
|
+
}), /* @__PURE__ */ jsxs("p", {
|
|
2602
|
+
className: "mt-1 truncate",
|
|
2603
|
+
children: ["token ", grid.geometryToken]
|
|
2604
|
+
})]
|
|
2605
|
+
}) : null,
|
|
2606
|
+
/* @__PURE__ */ jsx("div", {
|
|
2607
|
+
className: "max-sm:hidden",
|
|
2608
|
+
children: /* @__PURE__ */ jsx(AuthorFeedbackControls, {
|
|
2609
|
+
sessionId,
|
|
2610
|
+
document: focused,
|
|
2611
|
+
drafts,
|
|
2612
|
+
submitCapture
|
|
2613
|
+
}, focused.path)
|
|
2614
|
+
})
|
|
2615
|
+
] }) : null,
|
|
2616
|
+
/* @__PURE__ */ jsx("div", {
|
|
2617
|
+
className: focused === void 0 ? void 0 : "border-t border-po-border-soft pt-3",
|
|
2618
|
+
children: /* @__PURE__ */ jsx(AuthorRequestLog, { requests })
|
|
2619
|
+
})
|
|
2620
|
+
]
|
|
2621
|
+
})
|
|
2622
|
+
}), activity === null ? null : /* @__PURE__ */ jsx("footer", {
|
|
2623
|
+
"data-testid": "author-session-activity",
|
|
2624
|
+
className: "shrink-0 border-t border-po-border bg-po-rail p-3",
|
|
2625
|
+
children: activity
|
|
2626
|
+
})]
|
|
2627
|
+
});
|
|
2599
2628
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2629
|
+
//#endregion
|
|
2630
|
+
//#region src/extensions/workspaces/sessions/(frontend)/dock/authoring.web.tsx
|
|
2631
|
+
var authoring_web_default = defineDockFace({
|
|
2632
|
+
id: "authoring",
|
|
2633
|
+
label: "Author",
|
|
2634
|
+
order: 30,
|
|
2635
|
+
autoSelect: ({ tabId }) => tabId?.startsWith(AUTHOR_FILE_TAB_PREFIX) === true,
|
|
2636
|
+
panel: AuthorPanel,
|
|
2637
|
+
visibility: {
|
|
2638
|
+
subscribe(listener) {
|
|
2639
|
+
const subscription = authorWorkspace.store.subscribe(listener);
|
|
2640
|
+
return () => subscription.unsubscribe();
|
|
2608
2641
|
},
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2642
|
+
isVisible(sessionId) {
|
|
2643
|
+
return sessionId !== null && authorWorkspace.store.state.sessions[sessionId]?.focusedDocument !== void 0;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2647
|
+
//#endregion
|
|
2648
|
+
//#region src/types/authorPreview.ts
|
|
2649
|
+
function parseAuthorOpenSourceAction(input) {
|
|
2650
|
+
if (typeof input !== "object" || input === null) return null;
|
|
2651
|
+
const candidate = input;
|
|
2652
|
+
return candidate.version === 1 && typeof candidate.createTab === "function" ? candidate : null;
|
|
2616
2653
|
}
|
|
2617
|
-
function
|
|
2618
|
-
if (
|
|
2619
|
-
const
|
|
2620
|
-
if (
|
|
2621
|
-
return
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2654
|
+
function parseAuthorPreviewAction(input) {
|
|
2655
|
+
if (typeof input !== "object" || input === null) return null;
|
|
2656
|
+
const candidate = input;
|
|
2657
|
+
if (candidate.version !== 1 || typeof candidate.label !== "string" || candidate.label.trim() === "") return null;
|
|
2658
|
+
if (candidate.detail !== void 0 && typeof candidate.detail !== "string") return null;
|
|
2659
|
+
if (typeof candidate.createTab !== "function") return null;
|
|
2660
|
+
if (candidate.supportsSource !== void 0 && typeof candidate.supportsSource !== "function") return null;
|
|
2661
|
+
if (candidate.embeddedPanel !== void 0 && typeof candidate.embeddedPanel !== "function") return null;
|
|
2662
|
+
return candidate;
|
|
2626
2663
|
}
|
|
2664
|
+
const authorPreviewActionSlot = {
|
|
2665
|
+
slot: "author.preview-action",
|
|
2666
|
+
parse: parseAuthorPreviewAction
|
|
2667
|
+
};
|
|
2627
2668
|
//#endregion
|
|
2628
2669
|
//#region src/extensions/workspaces/sessions/(frontend)/_lib/authorGridTools.ts
|
|
2629
2670
|
const stringProperty = {
|
|
@@ -3036,6 +3077,9 @@ function authorProfilesForDocument(sessionId, path, kind) {
|
|
|
3036
3077
|
* declares as `file` with `host: true`; this package serves no bytes of its
|
|
3037
3078
|
* own. The transport stays raw because what comes back is a blob, and a call
|
|
3038
3079
|
* through the generated client reads the body as JSON to report a refusal.
|
|
3080
|
+
*
|
|
3081
|
+
* An SVG comes back as a sized data URL (see `svgImageUrl`), so it has an
|
|
3082
|
+
* intrinsic size and a region capture can still draw it to a canvas.
|
|
3039
3083
|
*/
|
|
3040
3084
|
async function loadAuthorMedia(source, signal, imageData) {
|
|
3041
3085
|
let blob;
|
|
@@ -3047,6 +3091,14 @@ async function loadAuthorMedia(source, signal, imageData) {
|
|
|
3047
3091
|
});
|
|
3048
3092
|
if (!response.ok) throw new Error(`Author media could not be loaded (${response.status}).`);
|
|
3049
3093
|
blob = await response.blob();
|
|
3094
|
+
if (blob.type.startsWith("image/svg+xml")) {
|
|
3095
|
+
const url = svgImageUrl(await blob.text());
|
|
3096
|
+
signal.throwIfAborted();
|
|
3097
|
+
return {
|
|
3098
|
+
url,
|
|
3099
|
+
dispose() {}
|
|
3100
|
+
};
|
|
3101
|
+
}
|
|
3050
3102
|
}
|
|
3051
3103
|
signal.throwIfAborted();
|
|
3052
3104
|
const url = URL.createObjectURL(blob);
|
|
@@ -3954,7 +4006,7 @@ function AuthorStructuredView({ sessionId, document, displayedRegions }) {
|
|
|
3954
4006
|
}
|
|
3955
4007
|
//#endregion
|
|
3956
4008
|
//#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorTextView.tsx
|
|
3957
|
-
function AuthorTextView({ sessionId, document, preview, displayedRegions }) {
|
|
4009
|
+
function AuthorTextView({ sessionId, document, preview, displayedRegions, renderMediaReview }) {
|
|
3958
4010
|
const host = useRef(null);
|
|
3959
4011
|
const editor = useRef(null);
|
|
3960
4012
|
useEffect(() => registerAuthorGridResolver(sessionId, (cell, geometry) => {
|
|
@@ -3986,6 +4038,13 @@ function AuthorTextView({ sessionId, document, preview, displayedRegions }) {
|
|
|
3986
4038
|
}] : []));
|
|
3987
4039
|
}, [displayedRegions]);
|
|
3988
4040
|
const lines = (document.content ?? "").split("\n").length;
|
|
4041
|
+
const loadImage = useCallback(async (imagePath) => {
|
|
4042
|
+
const path = authorMarkdownImagePath(document.path, imagePath);
|
|
4043
|
+
return {
|
|
4044
|
+
...await loadAuthorMedia(authorSessionFileUrl(sessionId, path), new AbortController().signal),
|
|
4045
|
+
path
|
|
4046
|
+
};
|
|
4047
|
+
}, [sessionId, document.path]);
|
|
3989
4048
|
return /* @__PURE__ */ jsxs("div", {
|
|
3990
4049
|
ref: host,
|
|
3991
4050
|
className: "flex min-h-0 flex-1 flex-col",
|
|
@@ -3994,7 +4053,12 @@ function AuthorTextView({ sessionId, document, preview, displayedRegions }) {
|
|
|
3994
4053
|
className: "min-h-0 flex-1 overflow-auto bg-po-deep/50 p-6",
|
|
3995
4054
|
children: /* @__PURE__ */ jsx("article", {
|
|
3996
4055
|
className: "mx-auto min-h-full max-w-3xl 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",
|
|
3997
|
-
children: /* @__PURE__ */ jsx(Markdown, {
|
|
4056
|
+
children: /* @__PURE__ */ jsx(Markdown, {
|
|
4057
|
+
text: isMermaidPath(document.path) ? `\`\`\`\`mermaid\n${document.content ?? ""}\n\`\`\`\`` : document.content ?? "",
|
|
4058
|
+
loadImage,
|
|
4059
|
+
documentPath: document.path,
|
|
4060
|
+
renderMediaReview
|
|
4061
|
+
})
|
|
3998
4062
|
})
|
|
3999
4063
|
}) : /* @__PURE__ */ jsx(CodeEditor, {
|
|
4000
4064
|
value: document.content ?? "",
|
|
@@ -4160,7 +4224,8 @@ function AuthorDocumentPanel(props) {
|
|
|
4160
4224
|
sessionId,
|
|
4161
4225
|
document,
|
|
4162
4226
|
preview: document.kind === "markdown" && markdownPreview && activeTool === "select" && !gridVisible,
|
|
4163
|
-
displayedRegions
|
|
4227
|
+
displayedRegions,
|
|
4228
|
+
renderMediaReview: markdownMediaReview(props)
|
|
4164
4229
|
});
|
|
4165
4230
|
const sourcePanel = document.kind === "text" || document.kind === "markdown" || document.kind === "story-preview" ? textPanel : document.structuredFormat !== void 0 ? /* @__PURE__ */ jsx(AuthorStructuredView, {
|
|
4166
4231
|
sessionId,
|
|
@@ -4617,6 +4682,147 @@ var ImageFeedback_author_image_feedback_web_default = defineFill({
|
|
|
4617
4682
|
}
|
|
4618
4683
|
});
|
|
4619
4684
|
//#endregion
|
|
4685
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/authorMarkdownMedia.ts
|
|
4686
|
+
const MAX_DIMENSION = 4096;
|
|
4687
|
+
const MERMAID_SOURCE_MAX_CHARACTERS = 8192;
|
|
4688
|
+
function base64(bytes) {
|
|
4689
|
+
let binary = "";
|
|
4690
|
+
for (let index = 0; index < bytes.length; index += 32768) binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
|
|
4691
|
+
return btoa(binary);
|
|
4692
|
+
}
|
|
4693
|
+
/**
|
|
4694
|
+
* A picture opened from rendered Markdown, as an in-memory Author image.
|
|
4695
|
+
*
|
|
4696
|
+
* Every picture becomes PNG bytes held in the document, so annotation and
|
|
4697
|
+
* capture work the same for a raster image, an SVG and a mermaid diagram, and
|
|
4698
|
+
* nothing is written back. The path is synthetic and keyed by content, so the
|
|
4699
|
+
* same picture reopens with its unsent drafts.
|
|
4700
|
+
*/
|
|
4701
|
+
async function authorMarkdownMediaDocument(media, signal) {
|
|
4702
|
+
const image = new Image();
|
|
4703
|
+
image.src = await drawableImageUrl(media.url, signal);
|
|
4704
|
+
await image.decode();
|
|
4705
|
+
signal.throwIfAborted();
|
|
4706
|
+
if (image.naturalWidth === 0 || image.naturalHeight === 0) throw new Error("This picture has no size to annotate.");
|
|
4707
|
+
const scale = Math.min(1, MAX_DIMENSION / Math.max(image.naturalWidth, image.naturalHeight));
|
|
4708
|
+
const canvas = document.createElement("canvas");
|
|
4709
|
+
canvas.width = Math.max(1, Math.round(image.naturalWidth * scale));
|
|
4710
|
+
canvas.height = Math.max(1, Math.round(image.naturalHeight * scale));
|
|
4711
|
+
const context = canvas.getContext("2d");
|
|
4712
|
+
if (context === null) throw new Error("Canvas is unavailable.");
|
|
4713
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
4714
|
+
let png;
|
|
4715
|
+
try {
|
|
4716
|
+
png = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
|
|
4717
|
+
} catch {
|
|
4718
|
+
throw new Error("This picture comes from another site and cannot be annotated.");
|
|
4719
|
+
}
|
|
4720
|
+
if (png === null) throw new Error("This picture could not be prepared for annotation.");
|
|
4721
|
+
const bytes = new Uint8Array(await png.arrayBuffer());
|
|
4722
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
4723
|
+
signal.throwIfAborted();
|
|
4724
|
+
const sha256 = Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4725
|
+
return {
|
|
4726
|
+
path: `markdown-media/${sha256}.png`,
|
|
4727
|
+
kind: "image",
|
|
4728
|
+
title: media.title,
|
|
4729
|
+
sourceSha256: sha256,
|
|
4730
|
+
imageData: base64(bytes),
|
|
4731
|
+
mediaUrl: `markdown-media:${sha256}`,
|
|
4732
|
+
markdownMedia: {
|
|
4733
|
+
kind: media.kind,
|
|
4734
|
+
...media.sourcePath === void 0 ? {} : { sourcePath: media.sourcePath },
|
|
4735
|
+
...media.documentPath === void 0 ? {} : { documentPath: media.documentPath },
|
|
4736
|
+
...media.mermaidSource === void 0 ? {} : { mermaidSource: media.mermaidSource.slice(0, MERMAID_SOURCE_MAX_CHARACTERS) }
|
|
4737
|
+
}
|
|
4738
|
+
};
|
|
4739
|
+
}
|
|
4740
|
+
//#endregion
|
|
4741
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorMediaReviewPanel.tsx
|
|
4742
|
+
/**
|
|
4743
|
+
* Pan, annotate and send feedback on a picture opened from rendered Markdown.
|
|
4744
|
+
*
|
|
4745
|
+
* It fills the full-screen viewer, which covers the Author rail, so the tools
|
|
4746
|
+
* and the submit button live beside the canvas here rather than in the rail.
|
|
4747
|
+
*/
|
|
4748
|
+
function AuthorMediaReviewPanel({ media, sessionId, submitCapture }) {
|
|
4749
|
+
const [document, setDocument] = useState();
|
|
4750
|
+
const [error, setError] = useState();
|
|
4751
|
+
useEffect(() => {
|
|
4752
|
+
setDocument(void 0);
|
|
4753
|
+
setError(void 0);
|
|
4754
|
+
if (sessionId === null) return;
|
|
4755
|
+
const controller = new AbortController();
|
|
4756
|
+
let generation;
|
|
4757
|
+
authorMarkdownMediaDocument(media, controller.signal).then((input) => {
|
|
4758
|
+
if (controller.signal.aborted) return;
|
|
4759
|
+
setDocument(putAuthorDocument(sessionId, input));
|
|
4760
|
+
generation = focusAuthorDocument(sessionId, input.path);
|
|
4761
|
+
}).catch((reason) => {
|
|
4762
|
+
if (!controller.signal.aborted) setError(reason instanceof Error ? reason.message : String(reason));
|
|
4763
|
+
});
|
|
4764
|
+
return () => {
|
|
4765
|
+
controller.abort();
|
|
4766
|
+
if (generation !== void 0) releaseAuthorDocumentFocus(sessionId, generation);
|
|
4767
|
+
};
|
|
4768
|
+
}, [media, sessionId]);
|
|
4769
|
+
const drafts = useStore(authorWorkspace.store, (state) => authorDocumentAnnotations(sessionId, document?.path ?? "", state));
|
|
4770
|
+
const tool = useStore(authorWorkspace.store, (state) => sessionId === null ? "select" : state.sessions[sessionId]?.toolsByDocument[document?.path ?? ""] ?? "select");
|
|
4771
|
+
if (sessionId === null) return /* @__PURE__ */ jsx("output", {
|
|
4772
|
+
className: "p-4 text-po-dim",
|
|
4773
|
+
children: "Open a session to review this picture."
|
|
4774
|
+
});
|
|
4775
|
+
if (error) return /* @__PURE__ */ jsx("output", {
|
|
4776
|
+
className: "p-4 text-po-red",
|
|
4777
|
+
children: error
|
|
4778
|
+
});
|
|
4779
|
+
if (!document) return /* @__PURE__ */ jsx("output", {
|
|
4780
|
+
className: "p-4 text-po-dim",
|
|
4781
|
+
children: "Preparing the picture for review…"
|
|
4782
|
+
});
|
|
4783
|
+
const displayedRegions = drafts.annotations.map((region, index) => ({
|
|
4784
|
+
ordinal: index + 1,
|
|
4785
|
+
region
|
|
4786
|
+
}));
|
|
4787
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
4788
|
+
"data-testid": "author-media-review",
|
|
4789
|
+
className: "flex min-h-0 flex-1 flex-col sm:flex-row",
|
|
4790
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
4791
|
+
className: "flex min-h-0 flex-1 flex-col",
|
|
4792
|
+
children: /* @__PURE__ */ jsx(AuthorMediaView, {
|
|
4793
|
+
sessionId,
|
|
4794
|
+
document,
|
|
4795
|
+
activeTool: tool,
|
|
4796
|
+
displayedRegions,
|
|
4797
|
+
pendingCandidate: drafts.candidate !== void 0
|
|
4798
|
+
})
|
|
4799
|
+
}), /* @__PURE__ */ jsxs("aside", {
|
|
4800
|
+
className: "flex max-h-[45dvh] shrink-0 flex-col gap-4 overflow-y-auto border-t border-po-border bg-po-bg p-4 sm:max-h-none sm:w-80 sm:border-t-0 sm:border-l",
|
|
4801
|
+
children: [/* @__PURE__ */ jsx(AuthorToolPalette, {
|
|
4802
|
+
sessionId,
|
|
4803
|
+
path: document.path,
|
|
4804
|
+
kind: "image",
|
|
4805
|
+
activeTool: tool
|
|
4806
|
+
}), /* @__PURE__ */ jsx(AuthorFeedbackControls, {
|
|
4807
|
+
sessionId,
|
|
4808
|
+
document,
|
|
4809
|
+
drafts,
|
|
4810
|
+
submitCapture
|
|
4811
|
+
})]
|
|
4812
|
+
})]
|
|
4813
|
+
});
|
|
4814
|
+
}
|
|
4815
|
+
//#endregion
|
|
4816
|
+
//#region src/extensions/workspaces/sessions/(frontend)/fill/MediaReview.author.media-review.web.ts
|
|
4817
|
+
var MediaReview_author_media_review_web_default = defineFill({
|
|
4818
|
+
slot: "author.media-review",
|
|
4819
|
+
id: "media-review",
|
|
4820
|
+
data: {
|
|
4821
|
+
version: 1,
|
|
4822
|
+
panel: AuthorMediaReviewPanel
|
|
4823
|
+
}
|
|
4824
|
+
});
|
|
4825
|
+
//#endregion
|
|
4620
4826
|
//#region src/extensions/workspaces/sessions/(frontend)/leader/author-toggle.web.ts
|
|
4621
4827
|
var author_toggle_web_default = defineLeaderBinding({
|
|
4622
4828
|
id: "author.toggle",
|
|
@@ -4890,6 +5096,13 @@ var image_feedback_web_default = defineSlotFile({
|
|
|
4890
5096
|
parse: parseAuthorImageFeedbackContribution
|
|
4891
5097
|
});
|
|
4892
5098
|
//#endregion
|
|
5099
|
+
//#region src/extensions/workspaces/sessions/(frontend)/slot/media-review.web.ts
|
|
5100
|
+
/** Any Markdown host reads this through `markdownMediaReview` to annotate an opened picture. */
|
|
5101
|
+
var media_review_web_default = defineSlotFile({
|
|
5102
|
+
slot: "author.media-review",
|
|
5103
|
+
parse: markdownMediaReviewSlot.parse
|
|
5104
|
+
});
|
|
5105
|
+
//#endregion
|
|
4893
5106
|
//#region src/extensions/workspaces/sessions/(frontend)/slot/open-source.web.ts
|
|
4894
5107
|
var open_source_web_default = defineSlotFile({
|
|
4895
5108
|
slot: "author.open-source",
|
|
@@ -5132,13 +5345,20 @@ const webPlugin = defineWebPlugin({
|
|
|
5132
5345
|
return author_document_web_default;
|
|
5133
5346
|
},
|
|
5134
5347
|
get fills() {
|
|
5135
|
-
return [
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5348
|
+
return [
|
|
5349
|
+
via({
|
|
5350
|
+
slot: "author.open-source",
|
|
5351
|
+
id: "author"
|
|
5352
|
+
}, Author_author_open_source_web_default),
|
|
5353
|
+
via({
|
|
5354
|
+
slot: "author.image-feedback",
|
|
5355
|
+
id: "image-feedback"
|
|
5356
|
+
}, ImageFeedback_author_image_feedback_web_default),
|
|
5357
|
+
via({
|
|
5358
|
+
slot: "author.media-review",
|
|
5359
|
+
id: "media-review"
|
|
5360
|
+
}, MediaReview_author_media_review_web_default)
|
|
5361
|
+
];
|
|
5142
5362
|
},
|
|
5143
5363
|
get leaderBindings() {
|
|
5144
5364
|
return [via({ id: "author.author-toggle" }, author_toggle_web_default)];
|
|
@@ -5149,6 +5369,7 @@ const webPlugin = defineWebPlugin({
|
|
|
5149
5369
|
get slots() {
|
|
5150
5370
|
return [
|
|
5151
5371
|
via({ slot: "author.image-feedback" }, image_feedback_web_default),
|
|
5372
|
+
via({ slot: "author.media-review" }, media_review_web_default),
|
|
5152
5373
|
via({ slot: "author.open-source" }, open_source_web_default),
|
|
5153
5374
|
via({ slot: "author.preview-action" }, preview_action_web_default),
|
|
5154
5375
|
via({ slot: "author.preview-provider" }, preview_provider_web_default)
|