@agimon-ai/po-author 0.0.0-alpha.67 → 0.0.0-alpha.69
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { apiResponse, createApiClient, decodeBase64, defineChannelFile, defineFileLinks, defineFill, defineGlobalStore, defineNativePlugin, defineSessionStore, defineSlotFile, defineToolRenderer, defineWebLifecycle, encodeBase64, nativeApiTransport, sessionApiAddress } from "@agimon-ai/po-core/native";
|
|
2
2
|
import { Store } from "@tanstack/store";
|
|
3
3
|
import { Image } from "react-native-unistyles/components/native/Image";
|
|
4
|
-
import { AnnotationSurface, Badge, Button, EmptyState, Markdown, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, SectionLabel, SegmentedControl, Spinner, Textarea, TrashIcon, collapseLines, fontOf, resultTextLines, toolTone } from "@agimon-ai/po-native-components";
|
|
4
|
+
import { AnnotationSurface, Badge, Button, EmptyState, Markdown, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, SectionLabel, SegmentedControl, Sheet, SheetBody, SheetContent, SheetDescription, SheetHeader, SheetTitle, Spinner, Textarea, TrashIcon, collapseLines, fontOf, resultTextLines, selectionTick, toolTone } from "@agimon-ai/po-native-components";
|
|
5
5
|
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
6
6
|
import { AppState } from "react-native";
|
|
7
7
|
import { View } from "react-native-unistyles/components/native/View";
|
|
@@ -656,7 +656,7 @@ const authorPreviewActionSlot = {
|
|
|
656
656
|
};
|
|
657
657
|
/** Keep freehand evidence small and source-relative. Reject malformed geometry rather than degrading it into a crop. */
|
|
658
658
|
function validAuthorStroke(value) {
|
|
659
|
-
return Array.isArray(value) && value.length >= 2 && value.length <= 128 && value.
|
|
659
|
+
return Array.isArray(value) && value.length >= 2 && value.length <= 128 && value.every((point) => typeof point === "object" && point !== null && Number.isFinite(point.x) && Number.isFinite(point.y) && point.x >= 0 && point.x <= 1 && point.y >= 0 && point.y <= 1) && value.some((point) => point.x !== value[0]?.x || point.y !== value[0]?.y);
|
|
660
660
|
}
|
|
661
661
|
function authorStrokeBounds(points) {
|
|
662
662
|
if (!validAuthorStroke(points)) return null;
|
|
@@ -690,6 +690,64 @@ function normalizedAuthorRectangle(bounds, drag) {
|
|
|
690
690
|
height: (bottom - top) / height
|
|
691
691
|
};
|
|
692
692
|
}
|
|
693
|
+
/** Shapes share the existing bounded stroke contract, including reverse drags and image aspect ratios. */
|
|
694
|
+
function authorAnnotationStroke(tool, points, aspectRatio) {
|
|
695
|
+
if (!validAuthorStroke(points) || !Number.isFinite(aspectRatio) || aspectRatio <= 0) return void 0;
|
|
696
|
+
const first = points[0];
|
|
697
|
+
const last = points.at(-1);
|
|
698
|
+
if (tool === "rectangle") {
|
|
699
|
+
const rect = normalizedAuthorRectangle({
|
|
700
|
+
left: 0,
|
|
701
|
+
top: 0,
|
|
702
|
+
right: 1,
|
|
703
|
+
bottom: 1
|
|
704
|
+
}, {
|
|
705
|
+
left: first.x,
|
|
706
|
+
top: first.y,
|
|
707
|
+
right: last.x,
|
|
708
|
+
bottom: last.y
|
|
709
|
+
});
|
|
710
|
+
return rect ? [
|
|
711
|
+
{
|
|
712
|
+
x: rect.x,
|
|
713
|
+
y: rect.y
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
x: rect.x + rect.width,
|
|
717
|
+
y: rect.y
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
x: rect.x + rect.width,
|
|
721
|
+
y: rect.y + rect.height
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
x: rect.x,
|
|
725
|
+
y: rect.y + rect.height
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
x: rect.x,
|
|
729
|
+
y: rect.y
|
|
730
|
+
}
|
|
731
|
+
] : void 0;
|
|
732
|
+
}
|
|
733
|
+
if (tool !== "arrow") return points.map((point) => ({ ...point }));
|
|
734
|
+
const dx = (last.x - first.x) * aspectRatio;
|
|
735
|
+
const dy = last.y - first.y;
|
|
736
|
+
const length = Math.hypot(dx, dy);
|
|
737
|
+
if (length === 0) return void 0;
|
|
738
|
+
const head = Math.min(.04, length / 3);
|
|
739
|
+
const wing = (side) => ({
|
|
740
|
+
x: Math.max(0, Math.min(1, last.x - (dx + side * dy * .6) / length * head / aspectRatio)),
|
|
741
|
+
y: Math.max(0, Math.min(1, last.y - (dy - side * dx * .6) / length * head))
|
|
742
|
+
});
|
|
743
|
+
return [
|
|
744
|
+
{ ...first },
|
|
745
|
+
{ ...last },
|
|
746
|
+
wing(1),
|
|
747
|
+
{ ...last },
|
|
748
|
+
wing(-1)
|
|
749
|
+
];
|
|
750
|
+
}
|
|
693
751
|
const AUTHOR_QUOTE_MAX_BYTES = 4096;
|
|
694
752
|
function bytes$1(value) {
|
|
695
753
|
let length = 0;
|
|
@@ -901,10 +959,14 @@ const IMAGE_SAVE_MAX_BYTES = 33554432;
|
|
|
901
959
|
const PDF_MAX_BYTES = 33554432;
|
|
902
960
|
const VIDEO_MAX_BYTES = 134217728;
|
|
903
961
|
const extensionOf = (path) => path.toLowerCase().split("/").at(-1)?.split(".").slice(1).at(-1) ?? "";
|
|
962
|
+
/** Reuse the native decoder's supported image paths for canvas-specific host layout. */
|
|
963
|
+
function authorNativeIsImage(path) {
|
|
964
|
+
return IMAGE_EXTENSIONS.has(extensionOf(path));
|
|
965
|
+
}
|
|
904
966
|
/** Whether the phone's canvas can mark this path up; anything else stays with its own viewer. */
|
|
905
967
|
function authorNativeCanOpen(path) {
|
|
906
968
|
const extension = extensionOf(path);
|
|
907
|
-
return extension === "pdf" || VIDEO_MIMES[extension] !== void 0 || structuredFormat(path) !== void 0 ||
|
|
969
|
+
return extension === "pdf" || VIDEO_MIMES[extension] !== void 0 || structuredFormat(path) !== void 0 || authorNativeIsImage(path) || TEXT_EXTENSIONS.has(extension);
|
|
908
970
|
}
|
|
909
971
|
/**
|
|
910
972
|
* The raw reads Author's canvas already uses, restated for the native transport:
|
|
@@ -1438,6 +1500,10 @@ function note(value) {
|
|
|
1438
1500
|
};
|
|
1439
1501
|
});
|
|
1440
1502
|
}
|
|
1503
|
+
if (record.highlight !== void 0) {
|
|
1504
|
+
if (typeof record.highlight !== "boolean") throw new Error("Invalid stored Author highlight.");
|
|
1505
|
+
result.highlight = record.highlight;
|
|
1506
|
+
}
|
|
1441
1507
|
if (record.page !== void 0) {
|
|
1442
1508
|
if (typeof record.page !== "number" || !Number.isSafeInteger(record.page) || record.page < 1) throw new Error("Invalid stored Author page.");
|
|
1443
1509
|
result.page = record.page;
|
|
@@ -1624,6 +1690,671 @@ function startNativeAuthorRequests(runtime) {
|
|
|
1624
1690
|
};
|
|
1625
1691
|
}
|
|
1626
1692
|
//#endregion
|
|
1693
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorCanvasWorkspace.native.tsx
|
|
1694
|
+
const TOOLS = [
|
|
1695
|
+
{
|
|
1696
|
+
id: "move",
|
|
1697
|
+
label: "Move",
|
|
1698
|
+
hint: "Drag to move. Pinch to zoom."
|
|
1699
|
+
},
|
|
1700
|
+
{
|
|
1701
|
+
id: "pin",
|
|
1702
|
+
label: "Pin",
|
|
1703
|
+
hint: "Tap to mark. Keep going, explain later."
|
|
1704
|
+
},
|
|
1705
|
+
{
|
|
1706
|
+
id: "rectangle",
|
|
1707
|
+
label: "Box",
|
|
1708
|
+
hint: "Drag around a region. Keep going, explain later."
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
id: "arrow",
|
|
1712
|
+
label: "Arrow",
|
|
1713
|
+
hint: "Drag toward what should change."
|
|
1714
|
+
},
|
|
1715
|
+
{
|
|
1716
|
+
id: "draw",
|
|
1717
|
+
label: "Draw",
|
|
1718
|
+
hint: "Draw freely. Each stroke becomes a mark."
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
id: "highlight",
|
|
1722
|
+
label: "Highlight",
|
|
1723
|
+
hint: "Draw over a detail. The original stays unchanged."
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
id: "erase",
|
|
1727
|
+
label: "Erase",
|
|
1728
|
+
hint: "Tap a numbered mark to remove it, not image pixels."
|
|
1729
|
+
}
|
|
1730
|
+
];
|
|
1731
|
+
const FIT = {
|
|
1732
|
+
width: 0,
|
|
1733
|
+
height: 0,
|
|
1734
|
+
zoom: 1,
|
|
1735
|
+
originX: 0,
|
|
1736
|
+
originY: 0
|
|
1737
|
+
};
|
|
1738
|
+
const SEND_LIMIT = 16;
|
|
1739
|
+
/** Image feedback is a canvas first. Explanations and delivery live in the review sheet. */
|
|
1740
|
+
function AuthorCanvasWorkspace(props) {
|
|
1741
|
+
const { body, notes, draft, status } = props;
|
|
1742
|
+
const [tool, setTool] = useState("move");
|
|
1743
|
+
const [reviewOpen, setReviewOpen] = useState(!!draft || !!props.selectedId);
|
|
1744
|
+
const [reviewId, setReviewId] = useState(props.selectedId ?? notes[0]?.id);
|
|
1745
|
+
const [area, setArea] = useState({
|
|
1746
|
+
width: 0,
|
|
1747
|
+
height: 0
|
|
1748
|
+
});
|
|
1749
|
+
const [viewport, setViewport] = useState(FIT);
|
|
1750
|
+
const sending = status.state === "sending";
|
|
1751
|
+
const sendAfterFit = useRef(false);
|
|
1752
|
+
useEffect(() => {
|
|
1753
|
+
if (sendAfterFit.current && viewport.zoom === 1 && viewport.originX === 0 && viewport.originY === 0) {
|
|
1754
|
+
sendAfterFit.current = false;
|
|
1755
|
+
props.onSend?.();
|
|
1756
|
+
}
|
|
1757
|
+
}, [viewport, props.onSend]);
|
|
1758
|
+
const send = () => {
|
|
1759
|
+
if (viewport.zoom === 1 && viewport.originX === 0 && viewport.originY === 0) props.onSend?.();
|
|
1760
|
+
else {
|
|
1761
|
+
sendAfterFit.current = true;
|
|
1762
|
+
setViewport((current) => ({
|
|
1763
|
+
...current,
|
|
1764
|
+
zoom: 1,
|
|
1765
|
+
originX: 0,
|
|
1766
|
+
originY: 0
|
|
1767
|
+
}));
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
const onViewportChange = useCallback((next) => {
|
|
1771
|
+
setViewport((previous) => previous.width === next.width && previous.height === next.height && previous.zoom === next.zoom && previous.originX === next.originX && previous.originY === next.originY ? previous : next);
|
|
1772
|
+
props.onViewportChange?.(next);
|
|
1773
|
+
}, [props.onViewportChange]);
|
|
1774
|
+
const ratio = Number.isFinite(body.aspectRatio) && body.aspectRatio > 0 ? body.aspectRatio : 1;
|
|
1775
|
+
const width = Math.min(area.width, area.height * ratio);
|
|
1776
|
+
const height = width / ratio;
|
|
1777
|
+
const overviewWidth = 64 * Math.min(1, ratio);
|
|
1778
|
+
const overviewHeight = 64 / Math.max(1, ratio);
|
|
1779
|
+
const zoomTo = (zoom) => {
|
|
1780
|
+
const next = Math.min(4, Math.max(1, zoom));
|
|
1781
|
+
setViewport((current) => ({
|
|
1782
|
+
...current,
|
|
1783
|
+
zoom: next,
|
|
1784
|
+
originX: current.width / 2 - (current.width / 2 - current.originX) / current.zoom * next,
|
|
1785
|
+
originY: current.height / 2 - (current.height / 2 - current.originY) / current.zoom * next
|
|
1786
|
+
}));
|
|
1787
|
+
};
|
|
1788
|
+
const focus = (note) => {
|
|
1789
|
+
setReviewId(note.id);
|
|
1790
|
+
props.onSelect?.(note.id);
|
|
1791
|
+
setViewport((current) => ({
|
|
1792
|
+
...current,
|
|
1793
|
+
zoom: 2,
|
|
1794
|
+
originX: current.width / 2 - note.x * current.width * 2,
|
|
1795
|
+
originY: current.height / 2 - note.y * current.height * 2
|
|
1796
|
+
}));
|
|
1797
|
+
};
|
|
1798
|
+
const batch = notes.slice(0, SEND_LIMIT);
|
|
1799
|
+
const needsExplanation = batch.some((note) => !note.text.trim());
|
|
1800
|
+
const stale = batch.some((note) => note.sourceSha256 !== void 0 && note.sourceSha256 !== props.sourceSha256);
|
|
1801
|
+
const selected = notes.find((note) => note.id === reviewId);
|
|
1802
|
+
const marks = notes.flatMap((note, index) => note.sourceSha256 !== void 0 && note.sourceSha256 !== props.sourceSha256 ? [] : [{
|
|
1803
|
+
...note,
|
|
1804
|
+
label: String(index + 1),
|
|
1805
|
+
description: note.text || "Needs an explanation"
|
|
1806
|
+
}]);
|
|
1807
|
+
if (draft) marks.push({
|
|
1808
|
+
...draft,
|
|
1809
|
+
id: "draft",
|
|
1810
|
+
label: String(notes.length + 1),
|
|
1811
|
+
description: "Unfinished note"
|
|
1812
|
+
});
|
|
1813
|
+
const annotation = /* @__PURE__ */ jsx(AnnotationSurface, {
|
|
1814
|
+
accessibilityLabel: props.name,
|
|
1815
|
+
zoomable: !sending,
|
|
1816
|
+
navigation: tool === "move" && !sending,
|
|
1817
|
+
viewport,
|
|
1818
|
+
onViewportChange,
|
|
1819
|
+
marks,
|
|
1820
|
+
selectedId: selected?.id ?? props.selectedId,
|
|
1821
|
+
drawingHighlight: tool === "highlight",
|
|
1822
|
+
onPlace: !sending && tool === "pin" ? (point) => {
|
|
1823
|
+
if (props.onAddMark) props.onAddMark({
|
|
1824
|
+
...point,
|
|
1825
|
+
text: ""
|
|
1826
|
+
});
|
|
1827
|
+
else props.onPlace?.(point);
|
|
1828
|
+
} : void 0,
|
|
1829
|
+
onDraw: !sending && props.onAddMark && tool !== "move" && tool !== "pin" && tool !== "erase" ? (points) => {
|
|
1830
|
+
const stroke = authorAnnotationStroke(tool, points, ratio);
|
|
1831
|
+
const point = tool === "arrow" ? points.at(-1) : stroke?.at(-1);
|
|
1832
|
+
if (stroke && point) props.onAddMark?.({
|
|
1833
|
+
...point,
|
|
1834
|
+
points: stroke,
|
|
1835
|
+
text: "",
|
|
1836
|
+
...tool === "highlight" ? { highlight: true } : {}
|
|
1837
|
+
});
|
|
1838
|
+
} : void 0,
|
|
1839
|
+
onSelectMark: sending ? void 0 : (id) => {
|
|
1840
|
+
if (id === "draft") {
|
|
1841
|
+
setReviewOpen(true);
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
if (tool === "erase") {
|
|
1845
|
+
props.onDelete?.(id);
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
const note = notes.find((note) => note.id === id);
|
|
1849
|
+
if (note) focus(note);
|
|
1850
|
+
setReviewOpen(true);
|
|
1851
|
+
},
|
|
1852
|
+
children: /* @__PURE__ */ jsx(Image, {
|
|
1853
|
+
source: { uri: body.uri },
|
|
1854
|
+
style: {
|
|
1855
|
+
width,
|
|
1856
|
+
height
|
|
1857
|
+
},
|
|
1858
|
+
resizeMode: "contain"
|
|
1859
|
+
})
|
|
1860
|
+
});
|
|
1861
|
+
const sendMessage = status.state === "failed" ? `Couldn't send: ${status.message} Your notes are still here.` : status.state === "sent" ? "Sent to the agent. You can keep marking." : void 0;
|
|
1862
|
+
return /* @__PURE__ */ jsxs(View, {
|
|
1863
|
+
style: s$1.screen,
|
|
1864
|
+
children: [
|
|
1865
|
+
/* @__PURE__ */ jsxs(View, {
|
|
1866
|
+
style: s$1.header,
|
|
1867
|
+
children: [
|
|
1868
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1869
|
+
style: s$1.meta,
|
|
1870
|
+
children: "Image feedback"
|
|
1871
|
+
}),
|
|
1872
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1873
|
+
style: s$1.title,
|
|
1874
|
+
numberOfLines: 1,
|
|
1875
|
+
children: props.name
|
|
1876
|
+
}),
|
|
1877
|
+
props.ownedElsewhere ? /* @__PURE__ */ jsx(Badge, {
|
|
1878
|
+
tone: "magenta",
|
|
1879
|
+
children: "Also open on another device"
|
|
1880
|
+
}) : null,
|
|
1881
|
+
props.storageMessage ? /* @__PURE__ */ jsx(Text, {
|
|
1882
|
+
style: s$1.meta,
|
|
1883
|
+
accessibilityLiveRegion: "polite",
|
|
1884
|
+
children: props.storageMessage
|
|
1885
|
+
}) : null
|
|
1886
|
+
]
|
|
1887
|
+
}),
|
|
1888
|
+
/* @__PURE__ */ jsxs(View, {
|
|
1889
|
+
style: s$1.navigation,
|
|
1890
|
+
children: [
|
|
1891
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1892
|
+
size: "card",
|
|
1893
|
+
variant: "ghost",
|
|
1894
|
+
accessibilityLabel: "Zoom out",
|
|
1895
|
+
disabled: sending || viewport.zoom <= 1,
|
|
1896
|
+
onPress: () => zoomTo(viewport.zoom / 1.5),
|
|
1897
|
+
children: "-"
|
|
1898
|
+
}),
|
|
1899
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
1900
|
+
style: s$1.zoom,
|
|
1901
|
+
accessibilityLabel: `Zoom ${Math.round(viewport.zoom * 100)} percent`,
|
|
1902
|
+
children: [Math.round(viewport.zoom * 100), "%"]
|
|
1903
|
+
}),
|
|
1904
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1905
|
+
size: "card",
|
|
1906
|
+
variant: "ghost",
|
|
1907
|
+
accessibilityLabel: "Zoom in",
|
|
1908
|
+
disabled: sending || viewport.zoom >= 4,
|
|
1909
|
+
onPress: () => zoomTo(viewport.zoom * 1.5),
|
|
1910
|
+
children: "+"
|
|
1911
|
+
}),
|
|
1912
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1913
|
+
size: "card",
|
|
1914
|
+
variant: "outline",
|
|
1915
|
+
disabled: sending,
|
|
1916
|
+
onPress: () => setViewport((current) => ({
|
|
1917
|
+
...current,
|
|
1918
|
+
zoom: 1,
|
|
1919
|
+
originX: 0,
|
|
1920
|
+
originY: 0
|
|
1921
|
+
})),
|
|
1922
|
+
children: "Fit"
|
|
1923
|
+
}),
|
|
1924
|
+
/* @__PURE__ */ jsx(View, { style: s$1.grow }),
|
|
1925
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1926
|
+
size: "card",
|
|
1927
|
+
variant: "ghost",
|
|
1928
|
+
disabled: sending || !props.canUndo,
|
|
1929
|
+
onPress: props.onUndo,
|
|
1930
|
+
children: "Undo"
|
|
1931
|
+
}),
|
|
1932
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1933
|
+
size: "card",
|
|
1934
|
+
variant: "ghost",
|
|
1935
|
+
disabled: sending || !props.canRedo,
|
|
1936
|
+
onPress: props.onRedo,
|
|
1937
|
+
children: "Redo"
|
|
1938
|
+
})
|
|
1939
|
+
]
|
|
1940
|
+
}),
|
|
1941
|
+
/* @__PURE__ */ jsxs(View, {
|
|
1942
|
+
style: s$1.canvas,
|
|
1943
|
+
onLayout: (event) => {
|
|
1944
|
+
const { width, height } = event.nativeEvent.layout;
|
|
1945
|
+
if (Number.isFinite(width) && Number.isFinite(height)) setArea({
|
|
1946
|
+
width: Math.max(0, width),
|
|
1947
|
+
height: Math.max(0, height)
|
|
1948
|
+
});
|
|
1949
|
+
},
|
|
1950
|
+
children: [width > 0 && height > 0 ? /* @__PURE__ */ jsx(View, {
|
|
1951
|
+
style: {
|
|
1952
|
+
width,
|
|
1953
|
+
height
|
|
1954
|
+
},
|
|
1955
|
+
children: props.captureContent ? props.captureContent(annotation) : annotation
|
|
1956
|
+
}) : null, viewport.zoom > 1 && width > 0 ? /* @__PURE__ */ jsxs(View, {
|
|
1957
|
+
pointerEvents: "none",
|
|
1958
|
+
accessible: false,
|
|
1959
|
+
style: s$1.overview,
|
|
1960
|
+
children: [/* @__PURE__ */ jsx(Image, {
|
|
1961
|
+
source: { uri: body.uri },
|
|
1962
|
+
style: {
|
|
1963
|
+
width: overviewWidth,
|
|
1964
|
+
height: overviewHeight
|
|
1965
|
+
},
|
|
1966
|
+
resizeMode: "contain"
|
|
1967
|
+
}), /* @__PURE__ */ jsx(View, { style: [s$1.overviewWindow, {
|
|
1968
|
+
left: -viewport.originX / (viewport.zoom * width) * overviewWidth,
|
|
1969
|
+
top: -viewport.originY / (viewport.zoom * height) * overviewHeight,
|
|
1970
|
+
width: overviewWidth / viewport.zoom,
|
|
1971
|
+
height: overviewHeight / viewport.zoom
|
|
1972
|
+
}] })]
|
|
1973
|
+
}) : null]
|
|
1974
|
+
}),
|
|
1975
|
+
/* @__PURE__ */ jsxs(View, {
|
|
1976
|
+
style: s$1.dock,
|
|
1977
|
+
children: [
|
|
1978
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1979
|
+
style: s$1.hint,
|
|
1980
|
+
accessibilityLiveRegion: "polite",
|
|
1981
|
+
children: TOOLS.find((item) => item.id === tool)?.hint
|
|
1982
|
+
}),
|
|
1983
|
+
/* @__PURE__ */ jsx(View, {
|
|
1984
|
+
style: s$1.tools,
|
|
1985
|
+
children: TOOLS.map((item) => /* @__PURE__ */ jsx(Button, {
|
|
1986
|
+
size: "card",
|
|
1987
|
+
variant: "ghost",
|
|
1988
|
+
style: [s$1.tool, tool === item.id && s$1.toolSelected],
|
|
1989
|
+
accessibilityLabel: `${item.label} tool`,
|
|
1990
|
+
accessibilityState: { selected: tool === item.id },
|
|
1991
|
+
disabled: sending || item.id !== "move" && !props.onAddMark,
|
|
1992
|
+
onPress: () => {
|
|
1993
|
+
selectionTick();
|
|
1994
|
+
setTool(item.id);
|
|
1995
|
+
},
|
|
1996
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
1997
|
+
style: [s$1.toolLabel, tool === item.id && s$1.toolLabelSelected],
|
|
1998
|
+
children: item.label
|
|
1999
|
+
})
|
|
2000
|
+
}, item.id))
|
|
2001
|
+
}),
|
|
2002
|
+
sendMessage ? /* @__PURE__ */ jsx(Text, {
|
|
2003
|
+
style: status.state === "failed" ? s$1.failed : s$1.meta,
|
|
2004
|
+
accessibilityLiveRegion: "polite",
|
|
2005
|
+
children: sendMessage
|
|
2006
|
+
}) : null,
|
|
2007
|
+
/* @__PURE__ */ jsxs(View, {
|
|
2008
|
+
style: s$1.summary,
|
|
2009
|
+
children: [/* @__PURE__ */ jsxs(View, {
|
|
2010
|
+
style: s$1.grow,
|
|
2011
|
+
children: [/* @__PURE__ */ jsxs(Text, {
|
|
2012
|
+
style: s$1.title,
|
|
2013
|
+
children: [
|
|
2014
|
+
notes.length,
|
|
2015
|
+
" mark",
|
|
2016
|
+
notes.length === 1 ? "" : "s"
|
|
2017
|
+
]
|
|
2018
|
+
}), /* @__PURE__ */ jsx(Text, {
|
|
2019
|
+
style: s$1.meta,
|
|
2020
|
+
children: "Original stays unchanged"
|
|
2021
|
+
})]
|
|
2022
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
2023
|
+
size: "card",
|
|
2024
|
+
variant: reviewOpen ? "outline" : "primary",
|
|
2025
|
+
disabled: sending || !notes.length && !draft && !props.requests?.length,
|
|
2026
|
+
onPress: () => setReviewOpen(true),
|
|
2027
|
+
children: "Review & send"
|
|
2028
|
+
})]
|
|
2029
|
+
})
|
|
2030
|
+
]
|
|
2031
|
+
}),
|
|
2032
|
+
/* @__PURE__ */ jsx(Sheet, {
|
|
2033
|
+
open: reviewOpen,
|
|
2034
|
+
onOpenChange: (open) => {
|
|
2035
|
+
if (!sending) setReviewOpen(open);
|
|
2036
|
+
},
|
|
2037
|
+
children: /* @__PURE__ */ jsxs(SheetContent, {
|
|
2038
|
+
keep: true,
|
|
2039
|
+
footer: /* @__PURE__ */ jsxs(View, {
|
|
2040
|
+
style: s$1.sheetActions,
|
|
2041
|
+
children: [
|
|
2042
|
+
sendMessage ? /* @__PURE__ */ jsx(Text, {
|
|
2043
|
+
style: status.state === "failed" ? s$1.failed : s$1.meta,
|
|
2044
|
+
accessibilityLiveRegion: "polite",
|
|
2045
|
+
children: sendMessage
|
|
2046
|
+
}) : null,
|
|
2047
|
+
/* @__PURE__ */ jsx(Text, {
|
|
2048
|
+
style: s$1.meta,
|
|
2049
|
+
children: stale ? "Some notes refer to an earlier image. Keep them and reopen that version before sending." : needsExplanation ? "Add an explanation to each mark before sending." : notes.length > SEND_LIMIT ? `Sends the first ${SEND_LIMIT} notes. The rest stay here for the next batch.` : "Sends the annotated image and your explanations together."
|
|
2050
|
+
}),
|
|
2051
|
+
/* @__PURE__ */ jsxs(View, {
|
|
2052
|
+
style: s$1.row,
|
|
2053
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
2054
|
+
size: "card",
|
|
2055
|
+
variant: "ghost",
|
|
2056
|
+
style: s$1.grow,
|
|
2057
|
+
disabled: sending,
|
|
2058
|
+
onPress: () => setReviewOpen(false),
|
|
2059
|
+
children: "Keep marking"
|
|
2060
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
2061
|
+
size: "card",
|
|
2062
|
+
variant: "primary",
|
|
2063
|
+
style: s$1.grow,
|
|
2064
|
+
loading: sending,
|
|
2065
|
+
loadingLabel: "Sending feedback",
|
|
2066
|
+
disabled: !batch.length || needsExplanation || stale || !!draft || !props.onSend,
|
|
2067
|
+
onPress: send,
|
|
2068
|
+
children: status.state === "failed" ? "Try again" : `Send ${batch.length} note${batch.length === 1 ? "" : "s"}`
|
|
2069
|
+
})]
|
|
2070
|
+
})
|
|
2071
|
+
]
|
|
2072
|
+
}),
|
|
2073
|
+
children: [/* @__PURE__ */ jsx(SheetHeader, {
|
|
2074
|
+
closeLabel: "Close review",
|
|
2075
|
+
dismissible: !sending,
|
|
2076
|
+
children: /* @__PURE__ */ jsx(SheetTitle, { children: "Review your marks" })
|
|
2077
|
+
}), /* @__PURE__ */ jsxs(SheetBody, { children: [
|
|
2078
|
+
/* @__PURE__ */ jsx(SheetDescription, { children: "Mark first, explain here. Tap a note to find its place." }),
|
|
2079
|
+
draft ? /* @__PURE__ */ jsxs(View, {
|
|
2080
|
+
style: s$1.note,
|
|
2081
|
+
children: [
|
|
2082
|
+
/* @__PURE__ */ jsx(Text, {
|
|
2083
|
+
style: s$1.title,
|
|
2084
|
+
children: "Unfinished note"
|
|
2085
|
+
}),
|
|
2086
|
+
/* @__PURE__ */ jsx(Textarea, {
|
|
2087
|
+
rows: 3,
|
|
2088
|
+
maxLength: 2048,
|
|
2089
|
+
accessibilityLabel: "Unfinished note explanation",
|
|
2090
|
+
value: draft.text,
|
|
2091
|
+
disabled: sending,
|
|
2092
|
+
onChangeText: props.onDraftChange
|
|
2093
|
+
}),
|
|
2094
|
+
/* @__PURE__ */ jsxs(View, {
|
|
2095
|
+
style: s$1.row,
|
|
2096
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
2097
|
+
size: "card",
|
|
2098
|
+
variant: "danger-outline",
|
|
2099
|
+
disabled: sending,
|
|
2100
|
+
onPress: props.onDraftCancel,
|
|
2101
|
+
children: "Discard draft"
|
|
2102
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
2103
|
+
size: "card",
|
|
2104
|
+
disabled: sending || !draft.text.trim(),
|
|
2105
|
+
onPress: props.onDraftSave,
|
|
2106
|
+
children: "Keep note"
|
|
2107
|
+
})]
|
|
2108
|
+
})
|
|
2109
|
+
]
|
|
2110
|
+
}) : null,
|
|
2111
|
+
!notes.length && !draft ? /* @__PURE__ */ jsx(Text, {
|
|
2112
|
+
style: s$1.meta,
|
|
2113
|
+
children: "No marks yet. Keep marking whenever you are ready."
|
|
2114
|
+
}) : null,
|
|
2115
|
+
notes.map((note, index) => /* @__PURE__ */ jsxs(View, {
|
|
2116
|
+
style: [s$1.note, selected?.id === note.id && s$1.noteSelected],
|
|
2117
|
+
children: [
|
|
2118
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
2119
|
+
size: "card",
|
|
2120
|
+
variant: "ghost",
|
|
2121
|
+
accessibilityLabel: `Review mark ${index + 1}`,
|
|
2122
|
+
accessibilityState: { selected: selected?.id === note.id },
|
|
2123
|
+
disabled: sending,
|
|
2124
|
+
onPress: () => focus(note),
|
|
2125
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
2126
|
+
style: s$1.noteNumber,
|
|
2127
|
+
children: index + 1
|
|
2128
|
+
}), /* @__PURE__ */ jsx(Text, {
|
|
2129
|
+
style: s$1.noteText,
|
|
2130
|
+
children: note.text || "Add an explanation"
|
|
2131
|
+
})]
|
|
2132
|
+
}),
|
|
2133
|
+
note.sourceSha256 !== void 0 && note.sourceSha256 !== props.sourceSha256 ? /* @__PURE__ */ jsx(Text, {
|
|
2134
|
+
style: s$1.meta,
|
|
2135
|
+
children: "Earlier image version"
|
|
2136
|
+
}) : null,
|
|
2137
|
+
selected?.id === note.id ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Textarea, {
|
|
2138
|
+
rows: 3,
|
|
2139
|
+
maxLength: 2048,
|
|
2140
|
+
accessibilityLabel: `Mark ${index + 1} explanation`,
|
|
2141
|
+
placeholder: "What should change here?",
|
|
2142
|
+
value: note.text,
|
|
2143
|
+
disabled: sending,
|
|
2144
|
+
onChangeText: (text) => props.onNoteChange?.(note.id, text)
|
|
2145
|
+
}), /* @__PURE__ */ jsxs(View, {
|
|
2146
|
+
style: s$1.row,
|
|
2147
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
2148
|
+
size: "card",
|
|
2149
|
+
variant: "danger-outline",
|
|
2150
|
+
disabled: sending || !props.onDelete,
|
|
2151
|
+
onPress: () => props.onDelete?.(note.id),
|
|
2152
|
+
children: "Remove mark"
|
|
2153
|
+
}), note.points && props.onCrop ? /* @__PURE__ */ jsx(Button, {
|
|
2154
|
+
size: "card",
|
|
2155
|
+
loading: props.cropping,
|
|
2156
|
+
disabled: sending,
|
|
2157
|
+
onPress: () => {
|
|
2158
|
+
setReviewOpen(false);
|
|
2159
|
+
props.onCrop?.();
|
|
2160
|
+
},
|
|
2161
|
+
children: "Preview crop"
|
|
2162
|
+
}) : null]
|
|
2163
|
+
})] }) : null
|
|
2164
|
+
]
|
|
2165
|
+
}, note.id)),
|
|
2166
|
+
props.requests?.map((request, index) => /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsxs(Text, {
|
|
2167
|
+
style: request.status === "FAILED" ? s$1.failed : s$1.meta,
|
|
2168
|
+
children: [
|
|
2169
|
+
"Request ",
|
|
2170
|
+
index + 1,
|
|
2171
|
+
": ",
|
|
2172
|
+
request.status.toLowerCase()
|
|
2173
|
+
]
|
|
2174
|
+
}), request.error ? /* @__PURE__ */ jsx(Text, {
|
|
2175
|
+
style: s$1.failed,
|
|
2176
|
+
children: request.error
|
|
2177
|
+
}) : null] }, request.id))
|
|
2178
|
+
] })]
|
|
2179
|
+
})
|
|
2180
|
+
})
|
|
2181
|
+
]
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
const s$1 = StyleSheet.create((theme, rt) => ({
|
|
2185
|
+
screen: {
|
|
2186
|
+
flex: 1,
|
|
2187
|
+
backgroundColor: theme.colors.bg,
|
|
2188
|
+
uni__dependencies: [0]
|
|
2189
|
+
},
|
|
2190
|
+
header: {
|
|
2191
|
+
paddingHorizontal: theme.spacing(4),
|
|
2192
|
+
paddingTop: theme.spacing(3),
|
|
2193
|
+
gap: theme.spacing(1),
|
|
2194
|
+
alignItems: "flex-start",
|
|
2195
|
+
uni__dependencies: [0]
|
|
2196
|
+
},
|
|
2197
|
+
title: {
|
|
2198
|
+
color: theme.colors.hi,
|
|
2199
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
2200
|
+
fontSize: theme.fontSize.lg,
|
|
2201
|
+
lineHeight: theme.lineHeight.lg,
|
|
2202
|
+
fontWeight: theme.fontWeight.semibold,
|
|
2203
|
+
uni__dependencies: [0]
|
|
2204
|
+
},
|
|
2205
|
+
meta: {
|
|
2206
|
+
color: theme.colors.dim,
|
|
2207
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
2208
|
+
fontSize: theme.fontSize.sm,
|
|
2209
|
+
lineHeight: theme.lineHeight.sm,
|
|
2210
|
+
uni__dependencies: [0]
|
|
2211
|
+
},
|
|
2212
|
+
navigation: {
|
|
2213
|
+
flexDirection: "row",
|
|
2214
|
+
flexWrap: "wrap",
|
|
2215
|
+
alignItems: "center",
|
|
2216
|
+
paddingHorizontal: theme.spacing(2),
|
|
2217
|
+
gap: theme.spacing(1),
|
|
2218
|
+
uni__dependencies: [0]
|
|
2219
|
+
},
|
|
2220
|
+
zoom: {
|
|
2221
|
+
color: theme.colors.hi,
|
|
2222
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
2223
|
+
fontSize: theme.fontSize.sm,
|
|
2224
|
+
minWidth: 40,
|
|
2225
|
+
textAlign: "center",
|
|
2226
|
+
uni__dependencies: [0]
|
|
2227
|
+
},
|
|
2228
|
+
canvas: {
|
|
2229
|
+
flex: 1,
|
|
2230
|
+
minHeight: 44,
|
|
2231
|
+
alignItems: "center",
|
|
2232
|
+
justifyContent: "center",
|
|
2233
|
+
marginHorizontal: theme.spacing(4),
|
|
2234
|
+
marginBottom: theme.spacing(3),
|
|
2235
|
+
backgroundColor: theme.colors.deep,
|
|
2236
|
+
overflow: "hidden",
|
|
2237
|
+
borderRadius: theme.borderRadius.md,
|
|
2238
|
+
uni__dependencies: [0]
|
|
2239
|
+
},
|
|
2240
|
+
overview: {
|
|
2241
|
+
position: "absolute",
|
|
2242
|
+
right: theme.spacing(2),
|
|
2243
|
+
top: theme.spacing(2),
|
|
2244
|
+
borderWidth: 1,
|
|
2245
|
+
borderColor: theme.colors.border,
|
|
2246
|
+
backgroundColor: theme.colors.rail,
|
|
2247
|
+
overflow: "hidden",
|
|
2248
|
+
uni__dependencies: [0]
|
|
2249
|
+
},
|
|
2250
|
+
overviewWindow: {
|
|
2251
|
+
position: "absolute",
|
|
2252
|
+
borderWidth: 2,
|
|
2253
|
+
borderColor: theme.colors.green,
|
|
2254
|
+
uni__dependencies: [0]
|
|
2255
|
+
},
|
|
2256
|
+
dock: {
|
|
2257
|
+
padding: theme.spacing(3),
|
|
2258
|
+
paddingBottom: Math.max(theme.spacing(3), rt.insets.bottom),
|
|
2259
|
+
gap: theme.spacing(3),
|
|
2260
|
+
borderTopWidth: 1,
|
|
2261
|
+
borderTopColor: theme.colors.border,
|
|
2262
|
+
backgroundColor: theme.colors.rail,
|
|
2263
|
+
uni__dependencies: [0, 9]
|
|
2264
|
+
},
|
|
2265
|
+
hint: {
|
|
2266
|
+
color: theme.colors.dim,
|
|
2267
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
2268
|
+
fontSize: theme.fontSize.sm,
|
|
2269
|
+
lineHeight: theme.lineHeight.sm,
|
|
2270
|
+
minHeight: theme.lineHeight.sm,
|
|
2271
|
+
uni__dependencies: [0]
|
|
2272
|
+
},
|
|
2273
|
+
tools: {
|
|
2274
|
+
flexDirection: "row",
|
|
2275
|
+
flexWrap: "wrap",
|
|
2276
|
+
gap: theme.spacing(2),
|
|
2277
|
+
uni__dependencies: [0]
|
|
2278
|
+
},
|
|
2279
|
+
tool: {
|
|
2280
|
+
flexBasis: "22%",
|
|
2281
|
+
flexGrow: 1,
|
|
2282
|
+
minHeight: 48,
|
|
2283
|
+
borderWidth: 1,
|
|
2284
|
+
borderColor: theme.colors.border,
|
|
2285
|
+
uni__dependencies: [0]
|
|
2286
|
+
},
|
|
2287
|
+
toolSelected: {
|
|
2288
|
+
borderColor: theme.colors["edge-green"],
|
|
2289
|
+
backgroundColor: theme.colors["tint-green"],
|
|
2290
|
+
uni__dependencies: [0]
|
|
2291
|
+
},
|
|
2292
|
+
toolLabel: {
|
|
2293
|
+
color: theme.colors.dim,
|
|
2294
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
2295
|
+
fontSize: theme.fontSize.base,
|
|
2296
|
+
uni__dependencies: [0]
|
|
2297
|
+
},
|
|
2298
|
+
toolLabelSelected: {
|
|
2299
|
+
color: theme.colors.green,
|
|
2300
|
+
fontWeight: theme.fontWeight.semibold,
|
|
2301
|
+
uni__dependencies: [0]
|
|
2302
|
+
},
|
|
2303
|
+
summary: {
|
|
2304
|
+
flexDirection: "row",
|
|
2305
|
+
alignItems: "center",
|
|
2306
|
+
gap: theme.spacing(3),
|
|
2307
|
+
uni__dependencies: [0]
|
|
2308
|
+
},
|
|
2309
|
+
row: {
|
|
2310
|
+
flexDirection: "row",
|
|
2311
|
+
flexWrap: "wrap",
|
|
2312
|
+
gap: theme.spacing(2),
|
|
2313
|
+
uni__dependencies: [0]
|
|
2314
|
+
},
|
|
2315
|
+
grow: { flex: 1 },
|
|
2316
|
+
sheetActions: {
|
|
2317
|
+
gap: theme.spacing(3),
|
|
2318
|
+
paddingHorizontal: theme.spacing(4),
|
|
2319
|
+
paddingVertical: theme.spacing(3),
|
|
2320
|
+
uni__dependencies: [0]
|
|
2321
|
+
},
|
|
2322
|
+
note: {
|
|
2323
|
+
borderWidth: 1,
|
|
2324
|
+
borderColor: theme.colors.border,
|
|
2325
|
+
borderRadius: theme.borderRadius.md,
|
|
2326
|
+
padding: theme.spacing(2),
|
|
2327
|
+
gap: theme.spacing(2),
|
|
2328
|
+
uni__dependencies: [0]
|
|
2329
|
+
},
|
|
2330
|
+
noteSelected: {
|
|
2331
|
+
borderColor: theme.colors["edge-magenta"],
|
|
2332
|
+
uni__dependencies: [0]
|
|
2333
|
+
},
|
|
2334
|
+
noteNumber: {
|
|
2335
|
+
color: theme.colors.magenta,
|
|
2336
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
2337
|
+
fontSize: theme.fontSize.base,
|
|
2338
|
+
fontWeight: theme.fontWeight.semibold,
|
|
2339
|
+
uni__dependencies: [0]
|
|
2340
|
+
},
|
|
2341
|
+
noteText: {
|
|
2342
|
+
flex: 1,
|
|
2343
|
+
color: theme.colors.hi,
|
|
2344
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
2345
|
+
fontSize: theme.fontSize.base,
|
|
2346
|
+
lineHeight: theme.lineHeight.base,
|
|
2347
|
+
uni__dependencies: [0]
|
|
2348
|
+
},
|
|
2349
|
+
failed: {
|
|
2350
|
+
color: theme.colors.red,
|
|
2351
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
2352
|
+
fontSize: theme.fontSize.sm,
|
|
2353
|
+
lineHeight: theme.lineHeight.sm,
|
|
2354
|
+
uni__dependencies: [0]
|
|
2355
|
+
}
|
|
2356
|
+
}));
|
|
2357
|
+
//#endregion
|
|
1627
2358
|
//#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorDocumentView.native.tsx
|
|
1628
2359
|
const DRAFT_ID = "draft";
|
|
1629
2360
|
/** One open document on the phone: the content to mark up, its notes, and one way to hand them to the agent. */
|
|
@@ -1638,6 +2369,10 @@ function AuthorDocumentView(props) {
|
|
|
1638
2369
|
const [sourceSelection, setSourceSelection] = useState();
|
|
1639
2370
|
const sourceKey = `${props.name}\n${props.sourceSha256 ?? ""}`;
|
|
1640
2371
|
const selectedRange = body.kind === "text" && sourceSelection?.key === sourceKey ? nativeAuthorTextRange(body.text, sourceSelection) : void 0;
|
|
2372
|
+
if (body.kind === "image" && !props.edit) return /* @__PURE__ */ jsx(AuthorCanvasWorkspace, {
|
|
2373
|
+
...props,
|
|
2374
|
+
body
|
|
2375
|
+
}, sourceKey);
|
|
1641
2376
|
const canInspectSource = body.kind === "text" && props.nativeContent === void 0;
|
|
1642
2377
|
const markdown = body.kind === "text" && /\.(?:md|markdown)$/iu.test(props.name);
|
|
1643
2378
|
const rangeLabel = (note) => {
|
|
@@ -2386,7 +3121,21 @@ function AuthorDocumentPanel({ path, ...props }) {
|
|
|
2386
3121
|
const setDraft = (value) => setForm(key, value);
|
|
2387
3122
|
const [storageState, setStorageState] = useState();
|
|
2388
3123
|
const [selectedId, setSelectedId] = useState();
|
|
2389
|
-
const [
|
|
3124
|
+
const [sendState, setSendState] = useState({
|
|
3125
|
+
key,
|
|
3126
|
+
status: { state: "idle" }
|
|
3127
|
+
});
|
|
3128
|
+
const status = sendState.key === key ? sendState.status : { state: "idle" };
|
|
3129
|
+
const setStatus = (status) => setSendState({
|
|
3130
|
+
key,
|
|
3131
|
+
status
|
|
3132
|
+
});
|
|
3133
|
+
const history = useRef({
|
|
3134
|
+
key,
|
|
3135
|
+
past: [],
|
|
3136
|
+
future: []
|
|
3137
|
+
});
|
|
3138
|
+
const noteSequence = useRef(0);
|
|
2390
3139
|
const requests = useSyncExternalStore(subscribeRequests, () => nativeAuthorRequests.select(nativeAuthorRequests.store.state, sessionId));
|
|
2391
3140
|
const sending = useRef(false);
|
|
2392
3141
|
const [mediaPage, setMediaPage] = useState(1);
|
|
@@ -2411,6 +3160,41 @@ function AuthorDocumentPanel({ path, ...props }) {
|
|
|
2411
3160
|
};
|
|
2412
3161
|
else if (captureRef.current?.key === key) captureRef.current = void 0;
|
|
2413
3162
|
}, [key]);
|
|
3163
|
+
const changeNotes = (change, group) => {
|
|
3164
|
+
if (sending.current === "submitting") return;
|
|
3165
|
+
const current = notesStore.store.state[key] ?? EMPTY;
|
|
3166
|
+
const next = change(current);
|
|
3167
|
+
if (next === current) return;
|
|
3168
|
+
const previous = history.current;
|
|
3169
|
+
const valid = previous.key === key && previous.present === current;
|
|
3170
|
+
history.current = {
|
|
3171
|
+
key,
|
|
3172
|
+
present: next,
|
|
3173
|
+
group,
|
|
3174
|
+
future: [],
|
|
3175
|
+
past: valid && group !== void 0 && previous.group === group ? previous.past : [...valid ? previous.past : [], current].slice(-50)
|
|
3176
|
+
};
|
|
3177
|
+
setNotes(key, () => next);
|
|
3178
|
+
setStatus({ state: "idle" });
|
|
3179
|
+
};
|
|
3180
|
+
const travelHistory = (direction) => {
|
|
3181
|
+
const timeline = history.current;
|
|
3182
|
+
const current = notesStore.store.state[key] ?? EMPTY;
|
|
3183
|
+
if (sending.current || timeline.key !== key || timeline.present !== current) return;
|
|
3184
|
+
const target = timeline[direction].at(-1);
|
|
3185
|
+
if (!target) return;
|
|
3186
|
+
const opposite = direction === "past" ? "future" : "past";
|
|
3187
|
+
history.current = {
|
|
3188
|
+
...timeline,
|
|
3189
|
+
present: target,
|
|
3190
|
+
group: void 0,
|
|
3191
|
+
[direction]: timeline[direction].slice(0, -1),
|
|
3192
|
+
[opposite]: [...timeline[opposite], current].slice(-50)
|
|
3193
|
+
};
|
|
3194
|
+
setNotes(key, () => target);
|
|
3195
|
+
setSelectedId(void 0);
|
|
3196
|
+
setStatus({ state: "idle" });
|
|
3197
|
+
};
|
|
2414
3198
|
useEffect(() => {
|
|
2415
3199
|
if (sessionId === null) return;
|
|
2416
3200
|
const controller = new AbortController();
|
|
@@ -2591,17 +3375,21 @@ function AuthorDocumentPanel({ path, ...props }) {
|
|
|
2591
3375
|
const visibleNotes = nativeAuthorVisibleNotes(notes, current.file.kind, mediaPage, videoFrame?.timeSeconds);
|
|
2592
3376
|
const send = async () => {
|
|
2593
3377
|
if (sending.current) return;
|
|
2594
|
-
sending.current =
|
|
2595
|
-
const
|
|
3378
|
+
sending.current = "capturing";
|
|
3379
|
+
const sourceNotes = notesStore.store.state[key] ?? EMPTY;
|
|
3380
|
+
const sent = nativeAuthorVisibleNotes(sourceNotes, current.file.kind, mediaPage, videoFrame?.timeSeconds).slice(0, 16);
|
|
2596
3381
|
const capturedAt = Date.now();
|
|
2597
3382
|
const captureId = `native-author-${String(capturedAt)}-${sent[0]?.id ?? "notes"}`;
|
|
2598
3383
|
let recorded = false;
|
|
2599
3384
|
try {
|
|
2600
3385
|
if (!sent.length) throw new Error("Open a marked page or capture its video frame before sending notes.");
|
|
3386
|
+
if (sent.some((note) => !note.text.trim())) throw new Error("Add an explanation to each mark before sending.");
|
|
2601
3387
|
if (!props.submitCapture) throw new Error("This host cannot confirm feedback delivery. Your notes have not been sent.");
|
|
2602
3388
|
const provider = captureRef.current;
|
|
2603
3389
|
if (provider?.key !== key) throw new Error("The document is not ready to capture. Try again when it has finished opening.");
|
|
2604
3390
|
const { capture, viewport } = await captureNativeAuthorViewport(provider.capture, () => geometry.current?.key === key ? geometry.current.viewport : void 0);
|
|
3391
|
+
if ((notesStore.store.state[key] ?? EMPTY) !== sourceNotes) throw new Error("The notes changed during capture. Keep them still and try again.");
|
|
3392
|
+
sending.current = "submitting";
|
|
2605
3393
|
setStatus({ state: "sending" });
|
|
2606
3394
|
if (capture.bytes.byteLength > 2097152 || Math.max(capture.width, capture.height) > 1600) throw new Error("This capture exceeds the Author evidence budget.");
|
|
2607
3395
|
const { file, body } = current;
|
|
@@ -2624,13 +3412,18 @@ function AuthorDocumentPanel({ path, ...props }) {
|
|
|
2624
3412
|
mimeType: capture.mimeType,
|
|
2625
3413
|
context: authorCaptureContext(packet)
|
|
2626
3414
|
});
|
|
3415
|
+
if (history.current.key === key) history.current = {
|
|
3416
|
+
key,
|
|
3417
|
+
past: [],
|
|
3418
|
+
future: []
|
|
3419
|
+
};
|
|
2627
3420
|
setNotes(key, (notes) => notes.filter((note) => !sent.includes(note)));
|
|
2628
3421
|
setSelectedId(void 0);
|
|
2629
|
-
setStatus({ state: "sent" });
|
|
2630
3422
|
await persistNativeDraft(key, path, props.nativeAuthor).catch((error) => setStorageState({
|
|
2631
3423
|
key,
|
|
2632
3424
|
message: `Sent, but couldn't update the stored draft: ${String(error)} It may reappear after restart.`
|
|
2633
3425
|
}));
|
|
3426
|
+
setStatus({ state: "sent" });
|
|
2634
3427
|
} catch (error) {
|
|
2635
3428
|
if (recorded) failNativeAuthorRequest(sessionId, captureId, error);
|
|
2636
3429
|
setStatus({
|
|
@@ -2904,17 +3697,56 @@ function AuthorDocumentPanel({ path, ...props }) {
|
|
|
2904
3697
|
points
|
|
2905
3698
|
});
|
|
2906
3699
|
},
|
|
3700
|
+
onAddMark: current.file.kind === "image" ? (mark) => {
|
|
3701
|
+
if (sending.current === "submitting") return;
|
|
3702
|
+
if ((notesStore.store.state[key]?.length ?? 0) >= 100) {
|
|
3703
|
+
setStorageState({
|
|
3704
|
+
key,
|
|
3705
|
+
message: "This draft has 100 marks. Send or remove some before adding more."
|
|
3706
|
+
});
|
|
3707
|
+
return;
|
|
3708
|
+
}
|
|
3709
|
+
try {
|
|
3710
|
+
noteSequence.current += 1;
|
|
3711
|
+
const note = parseNativeAuthorDraft({
|
|
3712
|
+
version: 1,
|
|
3713
|
+
notes: [{
|
|
3714
|
+
...mark,
|
|
3715
|
+
id: `note-${Date.now()}-${noteSequence.current}`,
|
|
3716
|
+
sourceSha256: current.file.sourceSha256
|
|
3717
|
+
}]
|
|
3718
|
+
}).notes[0];
|
|
3719
|
+
changeNotes((notes) => [...notes, note]);
|
|
3720
|
+
} catch (error) {
|
|
3721
|
+
setStorageState({
|
|
3722
|
+
key,
|
|
3723
|
+
message: `Couldn't add this mark: ${error instanceof Error ? error.message : String(error)} Your other notes are still here.`
|
|
3724
|
+
});
|
|
3725
|
+
}
|
|
3726
|
+
} : void 0,
|
|
3727
|
+
onNoteChange: (id, text) => {
|
|
3728
|
+
if (text.length > 2048) return;
|
|
3729
|
+
changeNotes((notes) => notes.map((note) => note.id === id ? {
|
|
3730
|
+
...note,
|
|
3731
|
+
text
|
|
3732
|
+
} : note), id);
|
|
3733
|
+
},
|
|
3734
|
+
canUndo: history.current.key === key && history.current.present === notes && history.current.past.length > 0,
|
|
3735
|
+
canRedo: history.current.key === key && history.current.present === notes && history.current.future.length > 0,
|
|
3736
|
+
onUndo: () => travelHistory("past"),
|
|
3737
|
+
onRedo: () => travelHistory("future"),
|
|
2907
3738
|
onSelect: (id) => {
|
|
3739
|
+
history.current.group = void 0;
|
|
2908
3740
|
const note = notes.find((note) => note.id === id);
|
|
2909
3741
|
if (note?.page !== void 0) setMediaPage(note.page);
|
|
2910
3742
|
if (note?.timeSeconds !== void 0) {
|
|
2911
3743
|
setVideoFrame(void 0);
|
|
2912
3744
|
setSeekSeconds(note.timeSeconds);
|
|
2913
3745
|
}
|
|
2914
|
-
setSelectedId((value) => value === id ? void 0 : id);
|
|
3746
|
+
setSelectedId((value) => current.file.kind === "image" ? id : value === id ? void 0 : id);
|
|
2915
3747
|
},
|
|
2916
3748
|
onDelete: (id) => {
|
|
2917
|
-
|
|
3749
|
+
changeNotes((notes) => notes.filter((note) => note.id !== id));
|
|
2918
3750
|
if (selectedId === id) setSelectedId(void 0);
|
|
2919
3751
|
},
|
|
2920
3752
|
onDraftChange: (text) => {
|
|
@@ -2926,9 +3758,18 @@ function AuthorDocumentPanel({ path, ...props }) {
|
|
|
2926
3758
|
onDraftCancel: () => setDraft(void 0),
|
|
2927
3759
|
onDraftSave: () => {
|
|
2928
3760
|
if (draft === void 0 || draft.text.trim() === "") return;
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
3761
|
+
if (sending.current) return;
|
|
3762
|
+
if (notes.length >= 100) {
|
|
3763
|
+
setStorageState({
|
|
3764
|
+
key,
|
|
3765
|
+
message: "This draft has 100 marks. Send or remove some before adding more."
|
|
3766
|
+
});
|
|
3767
|
+
return;
|
|
3768
|
+
}
|
|
3769
|
+
noteSequence.current += 1;
|
|
3770
|
+
changeNotes((notes) => [...notes, {
|
|
3771
|
+
...draft,
|
|
3772
|
+
id: `note-${Date.now()}-${noteSequence.current}`
|
|
2932
3773
|
}]);
|
|
2933
3774
|
setDraft(void 0);
|
|
2934
3775
|
if (status.state !== "sending") setStatus({ state: "idle" });
|
|
@@ -2952,7 +3793,7 @@ const authorNativeOpenSourceAction = {
|
|
|
2952
3793
|
return {
|
|
2953
3794
|
id: tabId(sessionId, valid),
|
|
2954
3795
|
label: fileName(valid),
|
|
2955
|
-
retainComposer:
|
|
3796
|
+
retainComposer: !authorNativeIsImage(valid),
|
|
2956
3797
|
panel: (props) => {
|
|
2957
3798
|
if (/\.stories\.(?:jsx?|tsx?)$/iu.test(valid)) {
|
|
2958
3799
|
const source = {
|