@elevasis/ui 2.72.0 → 2.74.0
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/api/index.js +3 -3
- package/dist/app/index.d.ts +172 -20
- package/dist/app/index.js +9 -10
- package/dist/auth/index.js +5 -6
- package/dist/charts/index.js +5 -6
- package/dist/chunk-2GIJBHZT.js +60 -0
- package/dist/{chunk-2454MK7P.js → chunk-2MRRDF32.js} +115 -8
- package/dist/chunk-3LOKYZJC.js +231 -0
- package/dist/chunk-3QHX4ZKV.js +224 -0
- package/dist/chunk-43LC6XAX.js +69 -0
- package/dist/chunk-4M2BSHVT.js +116 -0
- package/dist/{chunk-SV4M2BUI.js → chunk-4MXNHJTA.js} +111 -46
- package/dist/{chunk-LEQGGX2U.js → chunk-5A6KCFS5.js} +36 -43
- package/dist/{chunk-KIDZXZM5.js → chunk-6JN2TLV2.js} +1480 -4200
- package/dist/{chunk-UZ5JO5TK.js → chunk-7ARBBELS.js} +1 -1
- package/dist/{chunk-DJWIIPKW.js → chunk-7RJ7P6S6.js} +6 -3
- package/dist/chunk-ARAFQBNI.js +515 -0
- package/dist/{chunk-DKVGO6RJ.js → chunk-BA3AZJUR.js} +1 -1
- package/dist/chunk-BRTGCFBH.js +305 -0
- package/dist/{chunk-7ESZKTQG.js → chunk-CQPULKKL.js} +5 -3
- package/dist/chunk-CRA7WQZH.js +49 -0
- package/dist/{chunk-XZIOCB5D.js → chunk-DCQKTDIG.js} +2 -52
- package/dist/{chunk-A3JHFZDP.js → chunk-E6YGA5WH.js} +1 -1
- package/dist/chunk-FA7LMNQO.js +439 -0
- package/dist/chunk-FZSG6M6W.js +45 -0
- package/dist/chunk-GDOR57D7.js +165 -0
- package/dist/{chunk-RRS2QR3W.js → chunk-GO4XJ5D4.js} +1 -1
- package/dist/chunk-GPQQGQGT.js +645 -0
- package/dist/chunk-HYQOGRWG.js +54 -0
- package/dist/{chunk-LCHL5SM2.js → chunk-LN4UW6S4.js} +1 -1
- package/dist/chunk-N6K2Q5BM.js +393 -0
- package/dist/{chunk-Z2W5VNN3.js → chunk-O32UQ26A.js} +3 -2
- package/dist/{chunk-M7WWRZ5Z.js → chunk-QFGNRKIE.js} +1 -78
- package/dist/{chunk-Q3O3WY7Q.js → chunk-R5UGIFYU.js} +74 -18
- package/dist/{chunk-753MKWFV.js → chunk-RFNJ2PTO.js} +1 -1
- package/dist/chunk-VM2OWOXL.js +24 -0
- package/dist/{chunk-ZBXCEWKF.js → chunk-W2QRBQQA.js} +5 -3
- package/dist/chunk-WDBNW44Z.js +49 -0
- package/dist/{chunk-IXBRUIYI.js → chunk-WZU3MCKI.js} +1 -1
- package/dist/chunk-YQQ5GRUR.js +21 -0
- package/dist/{chunk-IMZTRDY2.js → chunk-ZFA56VSR.js} +2 -2
- package/dist/{chunk-AFBFSXP3.js → chunk-ZWI62BQ6.js} +7 -4
- package/dist/chunk-ZYNVCWDZ.js +286 -0
- package/dist/components/index.d.ts +490 -22
- package/dist/components/index.js +335 -35
- package/dist/components/navigation/index.js +6 -7
- package/dist/features/auth/index.d.ts +154 -2
- package/dist/features/auth/index.js +18 -9
- package/dist/features/clients/index.js +7 -7
- package/dist/features/content/index.d.ts +718 -34
- package/dist/features/content/index.js +1345 -51
- package/dist/features/crm/index.d.ts +172 -20
- package/dist/features/crm/index.js +10 -8
- package/dist/features/dashboard/index.d.ts +19 -10
- package/dist/features/dashboard/index.js +9 -8
- package/dist/features/delivery/index.d.ts +134 -0
- package/dist/features/delivery/index.js +23 -7
- package/dist/features/knowledge/index.js +5 -6
- package/dist/features/lead-gen/index.d.ts +47 -35
- package/dist/features/lead-gen/index.js +12 -8
- package/dist/features/monitoring/index.js +11 -9
- package/dist/features/monitoring/requests/index.js +8 -7
- package/dist/features/operations/index.d.ts +38 -20
- package/dist/features/operations/index.js +15 -11
- package/dist/features/public-agent-chat/index.js +3 -1
- package/dist/features/settings/index.d.ts +134 -0
- package/dist/features/settings/index.js +9 -8
- package/dist/hooks/access/index.js +5 -6
- package/dist/hooks/delivery/index.d.ts +134 -0
- package/dist/hooks/delivery/index.js +3 -2
- package/dist/hooks/operations/command-view/utils/transformCommandViewData.d.ts +20 -11
- package/dist/hooks/published.d.ts +1421 -29
- package/dist/hooks/published.js +22 -6
- package/dist/initialization/index.d.ts +134 -0
- package/dist/knowledge/index.d.ts +38 -20
- package/dist/knowledge/index.js +167 -11
- package/dist/{knowledge-search-index-RZLIVWPJ.js → knowledge-search-index-6GQK55IQ.js} +1 -1
- package/dist/layout/index.js +8 -9
- package/dist/profile/index.d.ts +134 -0
- package/dist/provider/index.d.ts +191 -21
- package/dist/provider/index.js +5 -6
- package/dist/provider/published.d.ts +219 -21
- package/dist/provider/published.js +5 -6
- package/dist/test-utils/index.d.ts +16 -5
- package/dist/test-utils/index.js +17 -18
- package/dist/types/index.d.ts +184 -14
- package/dist/utils/index.d.ts +102 -13
- package/dist/utils/index.js +1 -1
- package/package.json +7 -6
- package/dist/chunk-6M6OLGQY.js +0 -36
- package/dist/chunk-MKH2KOAO.js +0 -39
- package/dist/hooks/index.css +0 -659
- package/dist/hooks/index.d.ts +0 -12853
- package/dist/hooks/index.js +0 -27
- package/dist/supabase/index.d.ts +0 -6969
- package/dist/supabase/index.js +0 -3
- package/dist/typeform/index.d.ts +0 -462
- package/dist/typeform/index.js +0 -2051
- package/dist/typeform/schemas.d.ts +0 -67
- package/dist/typeform/schemas.js +0 -2
|
@@ -1,18 +1,18 @@
|
|
|
1
|
+
import { useCloudStorageTemporaryLinks, useStorageSignedUrls, storageObjectRefKey } from '../../chunk-3QHX4ZKV.js';
|
|
1
2
|
import { PageContainer } from '../../chunk-BZZCNLT6.js';
|
|
2
3
|
import '../../chunk-TUMSNGTX.js';
|
|
3
4
|
import { FilterBar } from '../../chunk-PDHTXPSF.js';
|
|
4
|
-
import { CustomModal, useContentItems, useContentDistributions, useContentConfig, SubshellContentContainer, PageTitleCaption, CenteredErrorState, EmptyState, StatCard, getContentItemIdentity, usePaginationState,
|
|
5
|
-
export { CONTENT_ITEMS, ContentSidebar, ContentSidebarMiddle, DEFAULT_CONTENT_VOCABULARY, MyReviewQueuePanel, contentManifest, deriveContentBoard, getContentItemIdentity, resolveContentStepResource, resolveContentVocabulary, useContentConfig } from '../../chunk-
|
|
5
|
+
import { CustomModal, useContentItemSourceAssetDetails, useContentSourceAssets, useAddContentItemSourceAsset, useRemoveContentItemSourceAsset, useReorderContentItemSourceAssets, useUpdateContentItemSourceAsset, useContentItemAttempts, useExecuteAsync, useUpdateContentItem, useExecution, contentKeys, useContentItems, useContentDistributions, useContentConfig, isAwaitingReview, SubshellContentContainer, PageTitleCaption, CenteredErrorState, EmptyState, StatCard, getContentItemIdentity, usePaginationState, resolveCurrentContentStep, useContentItem, useReviewContentItem, resolveReviewTarget, useContentDistribution, useContentDistributionMetrics, useContentPipelineSummary, deriveContentBoard, resolveContentWorkspaceRoute, useExecutions } from '../../chunk-6JN2TLV2.js';
|
|
6
|
+
export { CONTENT_ITEMS, ContentSidebar, ContentSidebarMiddle, DEFAULT_CONTENT_VOCABULARY, MyReviewQueuePanel, contentManifest, deriveContentBoard, getContentItemIdentity, resolveContentStepResource, resolveContentVocabulary, useContentConfig } from '../../chunk-6JN2TLV2.js';
|
|
6
7
|
import '../../chunk-R6NMRXNO.js';
|
|
7
8
|
import '../../chunk-RBGVNPA6.js';
|
|
8
|
-
import '../../chunk-
|
|
9
|
-
import '../../chunk-Z2W5VNN3.js';
|
|
9
|
+
import '../../chunk-O32UQ26A.js';
|
|
10
10
|
import '../../chunk-L7BZZ4SI.js';
|
|
11
|
-
import { showSuccessNotification } from '../../chunk-
|
|
11
|
+
import { showSuccessNotification } from '../../chunk-HYQOGRWG.js';
|
|
12
12
|
import '../../chunk-MISHPEJX.js';
|
|
13
13
|
import { CardHeader } from '../../chunk-62GVNH5U.js';
|
|
14
14
|
import '../../chunk-P45GQ3ZW.js';
|
|
15
|
-
import '../../chunk-
|
|
15
|
+
import '../../chunk-2GIJBHZT.js';
|
|
16
16
|
import '../../chunk-3KMDHCAR.js';
|
|
17
17
|
import '../../chunk-2IFYDILW.js';
|
|
18
18
|
import '../../chunk-BLXJEIQS.js';
|
|
@@ -20,7 +20,7 @@ import '../../chunk-UD657ICS.js';
|
|
|
20
20
|
import '../../chunk-DD3CCMCZ.js';
|
|
21
21
|
import '../../chunk-RNP5R5I3.js';
|
|
22
22
|
import '../../chunk-RT4KRGZT.js';
|
|
23
|
-
import { formatTimeAgo, formatRelativeTime } from '../../chunk-
|
|
23
|
+
import { formatTimeAgo, formatRelativeTime } from '../../chunk-R5UGIFYU.js';
|
|
24
24
|
import '../../chunk-Y6I3IC45.js';
|
|
25
25
|
import '../../chunk-6SSQGWK7.js';
|
|
26
26
|
import '../../chunk-BI5VDO2K.js';
|
|
@@ -29,12 +29,13 @@ import '../../chunk-TVTSASST.js';
|
|
|
29
29
|
import '../../chunk-BRJ3QZ4E.js';
|
|
30
30
|
import '../../chunk-Q7DJKLEN.js';
|
|
31
31
|
import '../../chunk-I2KLQ2HA.js';
|
|
32
|
-
import { Stack, Group, Text, Badge, Button, Textarea, Box, Loader, Accordion, Tooltip, Card, Alert, List, Paper,
|
|
33
|
-
import { IconAlertTriangle, IconShieldCheck, IconShieldX, IconArticle, IconArrowRight, IconClock, IconSend, IconSearch, IconFileText, IconExternalLink, IconListDetails, IconAlertCircle, IconLayoutGrid, IconShare, IconPlayerPlay, IconCircleCheck, IconCircleDashed, IconMaximize, IconCopy, IconDownload, IconArrowLeft, IconArrowsExchange,
|
|
32
|
+
import { Stack, Group, Text, Badge, Button, Textarea, Box, Loader, Accordion, Tooltip, Card, Alert, List, Paper, SegmentedControl, Slider, Title, Table, TextInput, Select, Modal, ActionIcon, Center, SimpleGrid, Pagination, Tabs, Anchor, Divider, Progress, Image as Image$1, Stepper, Grid, UnstyledButton, ScrollArea } from '@mantine/core';
|
|
33
|
+
import { IconAlertTriangle, IconShieldCheck, IconShieldX, IconPhoto, IconGripVertical, IconCrop, IconArrowUp, IconArrowDown, IconUnlink, IconLink, IconPhotoOff, IconChevronLeft, IconChevronRight, IconCheck, IconSparkles, IconEye, IconArticle, IconArrowRight, IconClock, IconSend, IconSearch, IconFileText, IconExternalLink, IconListDetails, IconAlertCircle, IconLayoutGrid, IconShare, IconPlayerPlay, IconCircleCheck, IconCircleDashed, IconMaximize, IconCopy, IconDownload, IconArrowLeft, IconArrowsExchange, IconX, IconChecklist, IconRoute, IconInfoCircle, IconClockHour4 } from '@tabler/icons-react';
|
|
34
34
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
35
|
-
import { useState, useMemo, Fragment as Fragment$1
|
|
35
|
+
import { useState, useRef, useCallback, useMemo, useEffect, Fragment as Fragment$1 } from 'react';
|
|
36
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
37
|
+
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
|
|
36
38
|
import { useNavigate, useSearch } from '@tanstack/react-router';
|
|
37
|
-
import { useDisclosure } from '@mantine/hooks';
|
|
38
39
|
|
|
39
40
|
function renderFieldValue(value, kind) {
|
|
40
41
|
if (value === void 0 || value === null || value === "") {
|
|
@@ -536,6 +537,1007 @@ function ContentReviewCard({
|
|
|
536
537
|
/* @__PURE__ */ jsx(ReviewActionBar, { actions, onAction, pending: actionsPending })
|
|
537
538
|
] }) });
|
|
538
539
|
}
|
|
540
|
+
function moveItem(items, fromIndex, toIndex) {
|
|
541
|
+
const next = items.slice();
|
|
542
|
+
const [moved] = next.splice(fromIndex, 1);
|
|
543
|
+
next.splice(toIndex, 0, moved);
|
|
544
|
+
return next;
|
|
545
|
+
}
|
|
546
|
+
function SelectedAssetStrip({ assets, onReorder }) {
|
|
547
|
+
const [dragOverIndex, setDragOverIndex] = useState(null);
|
|
548
|
+
const draggingIndexRef = useRef(null);
|
|
549
|
+
const handleDragStart = useCallback((e, index) => {
|
|
550
|
+
draggingIndexRef.current = index;
|
|
551
|
+
e.dataTransfer.effectAllowed = "move";
|
|
552
|
+
e.dataTransfer.setData("text/plain", String(index));
|
|
553
|
+
}, []);
|
|
554
|
+
const handleDragOver = useCallback((e, index) => {
|
|
555
|
+
e.preventDefault();
|
|
556
|
+
e.dataTransfer.dropEffect = "move";
|
|
557
|
+
setDragOverIndex(index);
|
|
558
|
+
}, []);
|
|
559
|
+
const handleDragLeave = useCallback(() => {
|
|
560
|
+
setDragOverIndex(null);
|
|
561
|
+
}, []);
|
|
562
|
+
const handleDrop = useCallback(
|
|
563
|
+
(e, targetIndex) => {
|
|
564
|
+
e.preventDefault();
|
|
565
|
+
setDragOverIndex(null);
|
|
566
|
+
const sourceIndex = draggingIndexRef.current;
|
|
567
|
+
draggingIndexRef.current = null;
|
|
568
|
+
if (sourceIndex === null || sourceIndex === targetIndex) return;
|
|
569
|
+
onReorder(moveItem(assets, sourceIndex, targetIndex));
|
|
570
|
+
},
|
|
571
|
+
[assets, onReorder]
|
|
572
|
+
);
|
|
573
|
+
const handleDragEnd = useCallback(() => {
|
|
574
|
+
draggingIndexRef.current = null;
|
|
575
|
+
setDragOverIndex(null);
|
|
576
|
+
}, []);
|
|
577
|
+
if (assets.length === 0) {
|
|
578
|
+
return /* @__PURE__ */ jsx(
|
|
579
|
+
Paper,
|
|
580
|
+
{
|
|
581
|
+
withBorder: true,
|
|
582
|
+
p: "md",
|
|
583
|
+
"data-testid": "selected-asset-strip-empty",
|
|
584
|
+
style: { borderStyle: "dashed", borderColor: "var(--color-border)" },
|
|
585
|
+
children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", ta: "center", children: "No assets selected yet." })
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return /* @__PURE__ */ jsx(Group, { gap: "sm", wrap: "wrap", "data-testid": "selected-asset-strip", role: "list", children: assets.map((asset, index) => {
|
|
590
|
+
const isCover = index === 0;
|
|
591
|
+
const isDragTarget = dragOverIndex === index;
|
|
592
|
+
return /* @__PURE__ */ jsx(
|
|
593
|
+
Paper,
|
|
594
|
+
{
|
|
595
|
+
withBorder: true,
|
|
596
|
+
role: "listitem",
|
|
597
|
+
"data-testid": `selected-asset-strip-item-${asset.id}`,
|
|
598
|
+
draggable: assets.length > 1,
|
|
599
|
+
onDragStart: (e) => handleDragStart(e, index),
|
|
600
|
+
onDragOver: (e) => handleDragOver(e, index),
|
|
601
|
+
onDragLeave: handleDragLeave,
|
|
602
|
+
onDrop: (e) => handleDrop(e, index),
|
|
603
|
+
onDragEnd: handleDragEnd,
|
|
604
|
+
style: {
|
|
605
|
+
width: 120,
|
|
606
|
+
overflow: "hidden",
|
|
607
|
+
cursor: assets.length > 1 ? "grab" : "default",
|
|
608
|
+
userSelect: "none",
|
|
609
|
+
borderColor: isDragTarget ? "var(--color-primary)" : "var(--color-border)",
|
|
610
|
+
transition: `border-color var(--duration-fast) var(--easing)`
|
|
611
|
+
},
|
|
612
|
+
children: /* @__PURE__ */ jsxs(Stack, { gap: 4, p: 6, children: [
|
|
613
|
+
/* @__PURE__ */ jsxs(
|
|
614
|
+
Box,
|
|
615
|
+
{
|
|
616
|
+
style: {
|
|
617
|
+
position: "relative",
|
|
618
|
+
width: "100%",
|
|
619
|
+
aspectRatio: "1 / 1",
|
|
620
|
+
borderRadius: 4,
|
|
621
|
+
overflow: "hidden",
|
|
622
|
+
backgroundColor: "var(--color-surface)"
|
|
623
|
+
},
|
|
624
|
+
children: [
|
|
625
|
+
asset.thumbnailUrl ? /* @__PURE__ */ jsx(
|
|
626
|
+
"img",
|
|
627
|
+
{
|
|
628
|
+
src: asset.thumbnailUrl,
|
|
629
|
+
alt: asset.name,
|
|
630
|
+
style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
|
|
631
|
+
}
|
|
632
|
+
) : /* @__PURE__ */ jsx(Group, { justify: "center", align: "center", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsx(IconPhoto, { size: 24, color: "var(--color-textDimmed)" }) }),
|
|
633
|
+
isCover && /* @__PURE__ */ jsx(
|
|
634
|
+
Badge,
|
|
635
|
+
{
|
|
636
|
+
size: "xs",
|
|
637
|
+
variant: "filled",
|
|
638
|
+
"data-testid": `selected-asset-strip-cover-${asset.id}`,
|
|
639
|
+
style: { position: "absolute", top: 4, left: 4 },
|
|
640
|
+
children: "Cover"
|
|
641
|
+
}
|
|
642
|
+
),
|
|
643
|
+
assets.length > 1 && /* @__PURE__ */ jsx(Box, { style: { position: "absolute", top: 4, right: 4, color: "var(--color-textDimmed)" }, children: /* @__PURE__ */ jsx(IconGripVertical, { size: 16 }) })
|
|
644
|
+
]
|
|
645
|
+
}
|
|
646
|
+
),
|
|
647
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", lineClamp: 1, title: asset.name, children: asset.name })
|
|
648
|
+
] })
|
|
649
|
+
},
|
|
650
|
+
asset.id
|
|
651
|
+
);
|
|
652
|
+
}) });
|
|
653
|
+
}
|
|
654
|
+
var CROP_ASPECT_OPTIONS = [
|
|
655
|
+
{ value: "original", label: "Original", ratio: null },
|
|
656
|
+
{ value: "portrait", label: "4:5", ratio: 4 / 5 },
|
|
657
|
+
{ value: "square", label: "1:1", ratio: 1 },
|
|
658
|
+
{ value: "landscape", label: "1.91:1", ratio: 1.91 }
|
|
659
|
+
];
|
|
660
|
+
var MIN_CROP_SCALE = 0.2;
|
|
661
|
+
function fitBox(pixelRatio, naturalWidth, naturalHeight) {
|
|
662
|
+
if (pixelRatio === null || naturalWidth === 0 || naturalHeight === 0) return { width: 1, height: 1 };
|
|
663
|
+
const normalizedRatio = pixelRatio * naturalHeight / naturalWidth;
|
|
664
|
+
return normalizedRatio >= 1 ? { width: 1, height: 1 / normalizedRatio } : { width: normalizedRatio, height: 1 };
|
|
665
|
+
}
|
|
666
|
+
function clamp(value, min, max) {
|
|
667
|
+
return Math.min(Math.max(value, min), max);
|
|
668
|
+
}
|
|
669
|
+
function round(value) {
|
|
670
|
+
return Math.round(value * 1e4) / 1e4;
|
|
671
|
+
}
|
|
672
|
+
function AssetCropEditor({
|
|
673
|
+
imageUrl,
|
|
674
|
+
value,
|
|
675
|
+
onChange,
|
|
676
|
+
defaultAspect = "original",
|
|
677
|
+
aspect,
|
|
678
|
+
disabled = false,
|
|
679
|
+
label
|
|
680
|
+
}) {
|
|
681
|
+
const [internalAspect, setInternalAspect] = useState(defaultAspect);
|
|
682
|
+
const activeAspect = aspect ?? internalAspect;
|
|
683
|
+
const [natural, setNatural] = useState(null);
|
|
684
|
+
const frameRef = useRef(null);
|
|
685
|
+
const dragOffsetRef = useRef(null);
|
|
686
|
+
const ratio = useMemo(
|
|
687
|
+
() => CROP_ASPECT_OPTIONS.find((option) => option.value === activeAspect)?.ratio ?? null,
|
|
688
|
+
[activeAspect]
|
|
689
|
+
);
|
|
690
|
+
const maxBox = useMemo(
|
|
691
|
+
() => fitBox(ratio, natural?.width ?? 0, natural?.height ?? 0),
|
|
692
|
+
[ratio, natural?.width, natural?.height]
|
|
693
|
+
);
|
|
694
|
+
const crop = value ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
695
|
+
const scale = maxBox.width === 0 ? 1 : clamp(crop.width / maxBox.width, MIN_CROP_SCALE, 1);
|
|
696
|
+
const emit = useCallback(
|
|
697
|
+
(next) => {
|
|
698
|
+
const width = clamp(next.width, 0.01, 1);
|
|
699
|
+
const height = clamp(next.height, 0.01, 1);
|
|
700
|
+
onChange({
|
|
701
|
+
x: round(clamp(next.x, 0, 1 - width)),
|
|
702
|
+
y: round(clamp(next.y, 0, 1 - height)),
|
|
703
|
+
width: round(width),
|
|
704
|
+
height: round(height)
|
|
705
|
+
});
|
|
706
|
+
},
|
|
707
|
+
[onChange]
|
|
708
|
+
);
|
|
709
|
+
const refit = useCallback(
|
|
710
|
+
(nextScale) => {
|
|
711
|
+
const width = maxBox.width * nextScale;
|
|
712
|
+
const height = maxBox.height * nextScale;
|
|
713
|
+
emit({ x: (1 - width) / 2, y: (1 - height) / 2, width, height });
|
|
714
|
+
},
|
|
715
|
+
[emit, maxBox.height, maxBox.width]
|
|
716
|
+
);
|
|
717
|
+
const fittedRatioRef = useRef(void 0);
|
|
718
|
+
useEffect(() => {
|
|
719
|
+
if (natural === null || disabled) return;
|
|
720
|
+
const isFirstMeasurement = fittedRatioRef.current === void 0;
|
|
721
|
+
fittedRatioRef.current = ratio;
|
|
722
|
+
if (isFirstMeasurement && value !== null) return;
|
|
723
|
+
if (ratio === null && value === null) return;
|
|
724
|
+
refit(scale);
|
|
725
|
+
}, [ratio, natural?.width, natural?.height]);
|
|
726
|
+
const handlePointerDown = (event) => {
|
|
727
|
+
if (disabled || frameRef.current === null) return;
|
|
728
|
+
const frame = frameRef.current.getBoundingClientRect();
|
|
729
|
+
dragOffsetRef.current = {
|
|
730
|
+
x: (event.clientX - frame.left) / frame.width - crop.x,
|
|
731
|
+
y: (event.clientY - frame.top) / frame.height - crop.y
|
|
732
|
+
};
|
|
733
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
734
|
+
};
|
|
735
|
+
const handlePointerMove = (event) => {
|
|
736
|
+
const offset = dragOffsetRef.current;
|
|
737
|
+
if (offset === null || frameRef.current === null) return;
|
|
738
|
+
const frame = frameRef.current.getBoundingClientRect();
|
|
739
|
+
emit({
|
|
740
|
+
x: (event.clientX - frame.left) / frame.width - offset.x,
|
|
741
|
+
y: (event.clientY - frame.top) / frame.height - offset.y,
|
|
742
|
+
width: crop.width,
|
|
743
|
+
height: crop.height
|
|
744
|
+
});
|
|
745
|
+
};
|
|
746
|
+
const endDrag = (event) => {
|
|
747
|
+
dragOffsetRef.current = null;
|
|
748
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
749
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
const handleKeyDown = (event) => {
|
|
753
|
+
if (disabled) return;
|
|
754
|
+
const step = 0.01;
|
|
755
|
+
const nudge = {
|
|
756
|
+
ArrowLeft: [-step, 0],
|
|
757
|
+
ArrowRight: [step, 0],
|
|
758
|
+
ArrowUp: [0, -step],
|
|
759
|
+
ArrowDown: [0, step]
|
|
760
|
+
};
|
|
761
|
+
const delta = nudge[event.key];
|
|
762
|
+
if (delta === void 0) return;
|
|
763
|
+
event.preventDefault();
|
|
764
|
+
emit({ x: crop.x + delta[0], y: crop.y + delta[1], width: crop.width, height: crop.height });
|
|
765
|
+
};
|
|
766
|
+
const croppedPixels = natural === null ? null : `${Math.round(crop.width * natural.width)}x${Math.round(crop.height * natural.height)}`;
|
|
767
|
+
return /* @__PURE__ */ jsxs(Stack, { gap: "xs", "data-testid": "asset-crop-editor", children: [
|
|
768
|
+
/* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", children: [
|
|
769
|
+
label !== void 0 && /* @__PURE__ */ jsx(Text, { fw: 500, children: label }),
|
|
770
|
+
croppedPixels !== null && /* @__PURE__ */ jsx(Badge, { variant: "light", size: "sm", children: croppedPixels })
|
|
771
|
+
] }),
|
|
772
|
+
/* @__PURE__ */ jsx(Paper, { withBorder: true, p: 0, style: { overflow: "hidden", position: "relative" }, children: /* @__PURE__ */ jsxs("div", { ref: frameRef, style: { position: "relative", lineHeight: 0 }, children: [
|
|
773
|
+
imageUrl === null ? /* @__PURE__ */ jsx(Group, { justify: "center", p: "xl", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : /* @__PURE__ */ jsx(
|
|
774
|
+
"img",
|
|
775
|
+
{
|
|
776
|
+
src: imageUrl,
|
|
777
|
+
alt: "",
|
|
778
|
+
style: { width: "100%", display: "block" },
|
|
779
|
+
onLoad: (event) => setNatural({
|
|
780
|
+
width: event.currentTarget.naturalWidth,
|
|
781
|
+
height: event.currentTarget.naturalHeight
|
|
782
|
+
})
|
|
783
|
+
}
|
|
784
|
+
),
|
|
785
|
+
imageUrl !== null && /* @__PURE__ */ jsx(
|
|
786
|
+
"div",
|
|
787
|
+
{
|
|
788
|
+
role: "slider",
|
|
789
|
+
tabIndex: disabled ? -1 : 0,
|
|
790
|
+
"aria-label": `Crop area${label === void 0 ? "" : ` for ${label}`}`,
|
|
791
|
+
"aria-valuetext": `x ${Math.round(crop.x * 100)}%, y ${Math.round(crop.y * 100)}%, ${Math.round(
|
|
792
|
+
crop.width * 100
|
|
793
|
+
)}% wide`,
|
|
794
|
+
"data-testid": "asset-crop-box",
|
|
795
|
+
onPointerDown: handlePointerDown,
|
|
796
|
+
onPointerMove: handlePointerMove,
|
|
797
|
+
onPointerUp: endDrag,
|
|
798
|
+
onPointerCancel: endDrag,
|
|
799
|
+
onKeyDown: handleKeyDown,
|
|
800
|
+
style: {
|
|
801
|
+
position: "absolute",
|
|
802
|
+
left: `${crop.x * 100}%`,
|
|
803
|
+
top: `${crop.y * 100}%`,
|
|
804
|
+
width: `${crop.width * 100}%`,
|
|
805
|
+
height: `${crop.height * 100}%`,
|
|
806
|
+
border: "2px solid var(--mantine-color-blue-5)",
|
|
807
|
+
boxShadow: "0 0 0 9999px rgba(0, 0, 0, 0.45)",
|
|
808
|
+
cursor: disabled ? "default" : "move",
|
|
809
|
+
touchAction: "none"
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
)
|
|
813
|
+
] }) }),
|
|
814
|
+
/* @__PURE__ */ jsxs(Group, { gap: "sm", align: "center", wrap: "wrap", children: [
|
|
815
|
+
aspect === void 0 && /* @__PURE__ */ jsx(
|
|
816
|
+
SegmentedControl,
|
|
817
|
+
{
|
|
818
|
+
size: "xs",
|
|
819
|
+
disabled,
|
|
820
|
+
value: activeAspect,
|
|
821
|
+
onChange: setInternalAspect,
|
|
822
|
+
data: CROP_ASPECT_OPTIONS.map((option) => ({ value: option.value, label: option.label }))
|
|
823
|
+
}
|
|
824
|
+
),
|
|
825
|
+
/* @__PURE__ */ jsx(
|
|
826
|
+
Slider,
|
|
827
|
+
{
|
|
828
|
+
"aria-label": "Crop size",
|
|
829
|
+
style: { flex: 1, minWidth: 140 },
|
|
830
|
+
disabled: disabled || imageUrl === null,
|
|
831
|
+
min: MIN_CROP_SCALE,
|
|
832
|
+
max: 1,
|
|
833
|
+
step: 0.01,
|
|
834
|
+
value: scale,
|
|
835
|
+
label: (current) => `${Math.round(current * 100)}%`,
|
|
836
|
+
onChange: refit
|
|
837
|
+
}
|
|
838
|
+
)
|
|
839
|
+
] })
|
|
840
|
+
] });
|
|
841
|
+
}
|
|
842
|
+
var ASSET_PICKER_FETCH_LIMIT = 100;
|
|
843
|
+
function describeAsset(membership, asset) {
|
|
844
|
+
return asset?.title ?? membership.sourceAssetId;
|
|
845
|
+
}
|
|
846
|
+
function readStorageReference(asset) {
|
|
847
|
+
const metadata = asset?.metadata;
|
|
848
|
+
if (typeof metadata !== "object" || metadata === null) return null;
|
|
849
|
+
const { credentialName, cloudStorageId } = metadata;
|
|
850
|
+
if (typeof credentialName !== "string" || typeof cloudStorageId !== "string") return null;
|
|
851
|
+
return { credentialName, cloudStorageId };
|
|
852
|
+
}
|
|
853
|
+
function SourceAssetsPanel({ itemId }) {
|
|
854
|
+
const { memberships, isMembershipLoaded } = useContentItemSourceAssetDetails(itemId);
|
|
855
|
+
const assetsQuery = useContentSourceAssets({ limit: ASSET_PICKER_FETCH_LIMIT });
|
|
856
|
+
const addMembership = useAddContentItemSourceAsset();
|
|
857
|
+
const removeMembership = useRemoveContentItemSourceAsset();
|
|
858
|
+
const reorderMemberships = useReorderContentItemSourceAssets();
|
|
859
|
+
const updateMembership = useUpdateContentItemSourceAsset();
|
|
860
|
+
const [assetToLink, setAssetToLink] = useState(null);
|
|
861
|
+
const [altDrafts, setAltDrafts] = useState({});
|
|
862
|
+
const [croppingAssetId, setCroppingAssetId] = useState(null);
|
|
863
|
+
const [cropDraft, setCropDraft] = useState(null);
|
|
864
|
+
const croppingDetail = memberships.find((detail) => detail.membership.sourceAssetId === croppingAssetId);
|
|
865
|
+
const croppingReference = readStorageReference(croppingDetail?.asset);
|
|
866
|
+
const { links } = useCloudStorageTemporaryLinks(
|
|
867
|
+
croppingReference?.credentialName ?? "",
|
|
868
|
+
croppingReference === null ? [] : [croppingReference.cloudStorageId]
|
|
869
|
+
);
|
|
870
|
+
const linkedIds = useMemo(() => new Set(memberships.map((detail) => detail.membership.sourceAssetId)), [memberships]);
|
|
871
|
+
const linkableOptions = useMemo(
|
|
872
|
+
() => (assetsQuery.data?.data ?? []).filter((asset) => !linkedIds.has(asset.id)).map((asset) => ({ value: asset.id, label: `${asset.title} (${asset.kind})` })),
|
|
873
|
+
[assetsQuery.data, linkedIds]
|
|
874
|
+
);
|
|
875
|
+
const orderedIds = memberships.map((detail) => detail.membership.sourceAssetId);
|
|
876
|
+
const handleLink = () => {
|
|
877
|
+
if (!assetToLink) return;
|
|
878
|
+
const highestPosition = memberships.reduce((highest, detail) => Math.max(highest, detail.membership.position), -1);
|
|
879
|
+
addMembership.mutate(
|
|
880
|
+
{ itemId, membership: { sourceAssetId: assetToLink, position: highestPosition + 1 } },
|
|
881
|
+
{ onSuccess: () => setAssetToLink(null) }
|
|
882
|
+
);
|
|
883
|
+
};
|
|
884
|
+
const stripAssets = useMemo(
|
|
885
|
+
() => memberships.map((detail) => ({
|
|
886
|
+
id: detail.membership.sourceAssetId,
|
|
887
|
+
name: describeAsset(detail.membership, detail.asset),
|
|
888
|
+
kind: "file"
|
|
889
|
+
})),
|
|
890
|
+
[memberships]
|
|
891
|
+
);
|
|
892
|
+
const handleMove = (fromIndex, toIndex) => {
|
|
893
|
+
if (toIndex < 0 || toIndex >= orderedIds.length) return;
|
|
894
|
+
const next = orderedIds.slice();
|
|
895
|
+
const [moved] = next.splice(fromIndex, 1);
|
|
896
|
+
next.splice(toIndex, 0, moved);
|
|
897
|
+
reorderMemberships.mutate({ itemId, orderedAssetIds: next });
|
|
898
|
+
};
|
|
899
|
+
const handleSaveAltText = (sourceAssetId) => {
|
|
900
|
+
const draft = altDrafts[sourceAssetId];
|
|
901
|
+
if (draft === void 0) return;
|
|
902
|
+
const trimmed = draft.trim();
|
|
903
|
+
updateMembership.mutate(
|
|
904
|
+
// `altText` is `.min(1)` when present, so clearing the field is `null` rather
|
|
905
|
+
// than an empty string -- an empty string is a validation error, not a clear.
|
|
906
|
+
{ itemId, sourceAssetId, updates: { altText: trimmed === "" ? null : trimmed } },
|
|
907
|
+
{
|
|
908
|
+
onSuccess: () => setAltDrafts((drafts) => {
|
|
909
|
+
const { [sourceAssetId]: _cleared, ...rest } = drafts;
|
|
910
|
+
return rest;
|
|
911
|
+
})
|
|
912
|
+
}
|
|
913
|
+
);
|
|
914
|
+
};
|
|
915
|
+
const isMutating = addMembership.isPending || removeMembership.isPending || reorderMemberships.isPending || updateMembership.isPending;
|
|
916
|
+
return /* @__PURE__ */ jsxs(Paper, { withBorder: true, p: "md", "data-testid": "source-assets-panel", children: [
|
|
917
|
+
/* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
918
|
+
/* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
|
|
919
|
+
/* @__PURE__ */ jsx(Title, { order: 4, children: "Source Assets" }),
|
|
920
|
+
isMembershipLoaded && /* @__PURE__ */ jsxs(Badge, { variant: "light", size: "sm", children: [
|
|
921
|
+
memberships.length,
|
|
922
|
+
" linked"
|
|
923
|
+
] })
|
|
924
|
+
] }),
|
|
925
|
+
!isMembershipLoaded ? /* @__PURE__ */ jsx(Group, { justify: "center", py: "md", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : memberships.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No source assets linked to this item." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
926
|
+
/* @__PURE__ */ jsx(
|
|
927
|
+
SelectedAssetStrip,
|
|
928
|
+
{
|
|
929
|
+
assets: stripAssets,
|
|
930
|
+
onReorder: (reordered) => reorderMemberships.mutate({ itemId, orderedAssetIds: reordered.map((asset) => asset.id) })
|
|
931
|
+
}
|
|
932
|
+
),
|
|
933
|
+
/* @__PURE__ */ jsxs(Table, { children: [
|
|
934
|
+
/* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
|
|
935
|
+
/* @__PURE__ */ jsx(Table.Th, { children: "Position" }),
|
|
936
|
+
/* @__PURE__ */ jsx(Table.Th, { children: "Asset" }),
|
|
937
|
+
/* @__PURE__ */ jsx(Table.Th, { children: "Kind" }),
|
|
938
|
+
/* @__PURE__ */ jsx(Table.Th, { children: "Alt text" }),
|
|
939
|
+
/* @__PURE__ */ jsx(Table.Th, { children: "Crop" }),
|
|
940
|
+
/* @__PURE__ */ jsx(Table.Th, {})
|
|
941
|
+
] }) }),
|
|
942
|
+
/* @__PURE__ */ jsx(Table.Tbody, { children: memberships.map((detail, index) => {
|
|
943
|
+
const { membership, asset } = detail;
|
|
944
|
+
const draft = altDrafts[membership.sourceAssetId];
|
|
945
|
+
const storedAltText = membership.altText ?? "";
|
|
946
|
+
return /* @__PURE__ */ jsxs(
|
|
947
|
+
Table.Tr,
|
|
948
|
+
{
|
|
949
|
+
"data-testid": `source-asset-row-${membership.sourceAssetId}`,
|
|
950
|
+
children: [
|
|
951
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
|
|
952
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", children: membership.position }),
|
|
953
|
+
index === 0 && /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "filled", children: "Cover" })
|
|
954
|
+
] }) }),
|
|
955
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", children: describeAsset(membership, asset) }) }),
|
|
956
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: asset?.kind ?? "\u2014" }) }),
|
|
957
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
|
|
958
|
+
/* @__PURE__ */ jsx(
|
|
959
|
+
TextInput,
|
|
960
|
+
{
|
|
961
|
+
size: "xs",
|
|
962
|
+
placeholder: "Describe this slide",
|
|
963
|
+
value: draft ?? storedAltText,
|
|
964
|
+
onChange: (event) => setAltDrafts((drafts) => ({
|
|
965
|
+
...drafts,
|
|
966
|
+
[membership.sourceAssetId]: event.currentTarget.value
|
|
967
|
+
}))
|
|
968
|
+
}
|
|
969
|
+
),
|
|
970
|
+
draft !== void 0 && draft !== storedAltText && /* @__PURE__ */ jsx(
|
|
971
|
+
Button,
|
|
972
|
+
{
|
|
973
|
+
size: "compact-xs",
|
|
974
|
+
variant: "light",
|
|
975
|
+
disabled: isMutating,
|
|
976
|
+
onClick: () => handleSaveAltText(membership.sourceAssetId),
|
|
977
|
+
children: "Save"
|
|
978
|
+
}
|
|
979
|
+
)
|
|
980
|
+
] }) }),
|
|
981
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
|
|
982
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: membership.crop ? "set" : "\u2014" }),
|
|
983
|
+
/* @__PURE__ */ jsx(
|
|
984
|
+
Button,
|
|
985
|
+
{
|
|
986
|
+
size: "compact-xs",
|
|
987
|
+
variant: "subtle",
|
|
988
|
+
leftSection: /* @__PURE__ */ jsx(IconCrop, { size: 14 }),
|
|
989
|
+
disabled: isMutating || readStorageReference(asset) === null,
|
|
990
|
+
onClick: () => {
|
|
991
|
+
setCropDraft(membership.crop);
|
|
992
|
+
setCroppingAssetId(membership.sourceAssetId);
|
|
993
|
+
},
|
|
994
|
+
children: "Crop"
|
|
995
|
+
}
|
|
996
|
+
)
|
|
997
|
+
] }) }),
|
|
998
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Group, { gap: 4, justify: "flex-end", wrap: "nowrap", children: [
|
|
999
|
+
/* @__PURE__ */ jsx(
|
|
1000
|
+
Button,
|
|
1001
|
+
{
|
|
1002
|
+
size: "compact-xs",
|
|
1003
|
+
variant: "subtle",
|
|
1004
|
+
"aria-label": "Move up",
|
|
1005
|
+
disabled: index === 0 || isMutating,
|
|
1006
|
+
onClick: () => handleMove(index, index - 1),
|
|
1007
|
+
children: /* @__PURE__ */ jsx(IconArrowUp, { size: 14 })
|
|
1008
|
+
}
|
|
1009
|
+
),
|
|
1010
|
+
/* @__PURE__ */ jsx(
|
|
1011
|
+
Button,
|
|
1012
|
+
{
|
|
1013
|
+
size: "compact-xs",
|
|
1014
|
+
variant: "subtle",
|
|
1015
|
+
"aria-label": "Move down",
|
|
1016
|
+
disabled: index === memberships.length - 1 || isMutating,
|
|
1017
|
+
onClick: () => handleMove(index, index + 1),
|
|
1018
|
+
children: /* @__PURE__ */ jsx(IconArrowDown, { size: 14 })
|
|
1019
|
+
}
|
|
1020
|
+
),
|
|
1021
|
+
/* @__PURE__ */ jsx(
|
|
1022
|
+
Button,
|
|
1023
|
+
{
|
|
1024
|
+
size: "compact-xs",
|
|
1025
|
+
variant: "subtle",
|
|
1026
|
+
color: "red",
|
|
1027
|
+
leftSection: /* @__PURE__ */ jsx(IconUnlink, { size: 14 }),
|
|
1028
|
+
disabled: isMutating,
|
|
1029
|
+
onClick: () => removeMembership.mutate({ itemId, sourceAssetId: membership.sourceAssetId }),
|
|
1030
|
+
children: "Unlink"
|
|
1031
|
+
}
|
|
1032
|
+
)
|
|
1033
|
+
] }) })
|
|
1034
|
+
]
|
|
1035
|
+
},
|
|
1036
|
+
membership.sourceAssetId
|
|
1037
|
+
);
|
|
1038
|
+
}) })
|
|
1039
|
+
] })
|
|
1040
|
+
] }),
|
|
1041
|
+
/* @__PURE__ */ jsxs(Group, { gap: "sm", align: "flex-end", children: [
|
|
1042
|
+
/* @__PURE__ */ jsx(
|
|
1043
|
+
Select,
|
|
1044
|
+
{
|
|
1045
|
+
size: "xs",
|
|
1046
|
+
searchable: true,
|
|
1047
|
+
clearable: true,
|
|
1048
|
+
label: "Link an existing source asset",
|
|
1049
|
+
placeholder: linkableOptions.length === 0 ? "Nothing left to link" : "Choose an asset",
|
|
1050
|
+
data: linkableOptions,
|
|
1051
|
+
value: assetToLink,
|
|
1052
|
+
onChange: setAssetToLink,
|
|
1053
|
+
disabled: linkableOptions.length === 0 || isMutating,
|
|
1054
|
+
style: { minWidth: 320 }
|
|
1055
|
+
}
|
|
1056
|
+
),
|
|
1057
|
+
/* @__PURE__ */ jsx(
|
|
1058
|
+
Button,
|
|
1059
|
+
{
|
|
1060
|
+
size: "compact-sm",
|
|
1061
|
+
variant: "light",
|
|
1062
|
+
leftSection: /* @__PURE__ */ jsx(IconLink, { size: 14 }),
|
|
1063
|
+
disabled: !assetToLink || isMutating,
|
|
1064
|
+
onClick: handleLink,
|
|
1065
|
+
children: "Link"
|
|
1066
|
+
}
|
|
1067
|
+
)
|
|
1068
|
+
] }),
|
|
1069
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Position 0 is the cover. Unlinking leaves the remaining positions untouched, so a gap is normal \u2014 use move up / move down to renumber the whole set. Unlinking never deletes the source asset itself." })
|
|
1070
|
+
] }),
|
|
1071
|
+
/* @__PURE__ */ jsx(
|
|
1072
|
+
Modal,
|
|
1073
|
+
{
|
|
1074
|
+
opened: croppingAssetId !== null,
|
|
1075
|
+
onClose: () => setCroppingAssetId(null),
|
|
1076
|
+
title: croppingDetail === void 0 ? "Crop" : `Crop ${describeAsset(croppingDetail.membership, croppingDetail.asset)}`,
|
|
1077
|
+
size: "lg",
|
|
1078
|
+
children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
1079
|
+
/* @__PURE__ */ jsx(
|
|
1080
|
+
AssetCropEditor,
|
|
1081
|
+
{
|
|
1082
|
+
imageUrl: croppingReference === null ? null : links[croppingReference.cloudStorageId] ?? null,
|
|
1083
|
+
value: cropDraft,
|
|
1084
|
+
onChange: setCropDraft,
|
|
1085
|
+
disabled: isMutating
|
|
1086
|
+
}
|
|
1087
|
+
),
|
|
1088
|
+
/* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
|
|
1089
|
+
/* @__PURE__ */ jsx(
|
|
1090
|
+
Button,
|
|
1091
|
+
{
|
|
1092
|
+
variant: "subtle",
|
|
1093
|
+
color: "red",
|
|
1094
|
+
disabled: isMutating || cropDraft === null,
|
|
1095
|
+
onClick: () => setCropDraft(null),
|
|
1096
|
+
children: "Clear crop"
|
|
1097
|
+
}
|
|
1098
|
+
),
|
|
1099
|
+
/* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
|
|
1100
|
+
/* @__PURE__ */ jsx(Button, { variant: "default", onClick: () => setCroppingAssetId(null), children: "Cancel" }),
|
|
1101
|
+
/* @__PURE__ */ jsx(
|
|
1102
|
+
Button,
|
|
1103
|
+
{
|
|
1104
|
+
disabled: isMutating || croppingAssetId === null,
|
|
1105
|
+
onClick: () => {
|
|
1106
|
+
if (croppingAssetId === null) return;
|
|
1107
|
+
updateMembership.mutate(
|
|
1108
|
+
{ itemId, sourceAssetId: croppingAssetId, updates: { crop: cropDraft } },
|
|
1109
|
+
{ onSuccess: () => setCroppingAssetId(null) }
|
|
1110
|
+
);
|
|
1111
|
+
},
|
|
1112
|
+
children: "Save crop"
|
|
1113
|
+
}
|
|
1114
|
+
)
|
|
1115
|
+
] })
|
|
1116
|
+
] })
|
|
1117
|
+
] })
|
|
1118
|
+
}
|
|
1119
|
+
)
|
|
1120
|
+
] });
|
|
1121
|
+
}
|
|
1122
|
+
var PREVIEW_MAX_WIDTH = 380;
|
|
1123
|
+
function PostPreview({ slides, caption, hashtags = [], aspectRatio = 4 / 5, title }) {
|
|
1124
|
+
const [index, setIndex] = useState(0);
|
|
1125
|
+
useEffect(() => {
|
|
1126
|
+
if (index > slides.length - 1) setIndex(Math.max(0, slides.length - 1));
|
|
1127
|
+
}, [slides.length, index]);
|
|
1128
|
+
const slide = slides[index];
|
|
1129
|
+
return /* @__PURE__ */ jsxs(Stack, { gap: "xs", style: { maxWidth: PREVIEW_MAX_WIDTH }, "data-testid": "post-preview", children: [
|
|
1130
|
+
title !== void 0 && /* @__PURE__ */ jsx(Text, { fw: 500, lineClamp: 1, children: title }),
|
|
1131
|
+
/* @__PURE__ */ jsx(Paper, { withBorder: true, p: 0, style: { overflow: "hidden" }, children: /* @__PURE__ */ jsxs(
|
|
1132
|
+
Box,
|
|
1133
|
+
{
|
|
1134
|
+
style: {
|
|
1135
|
+
position: "relative",
|
|
1136
|
+
width: "100%",
|
|
1137
|
+
aspectRatio: `${aspectRatio}`,
|
|
1138
|
+
overflow: "hidden",
|
|
1139
|
+
background: "var(--mantine-color-gray-1)"
|
|
1140
|
+
},
|
|
1141
|
+
"data-testid": "post-preview-frame",
|
|
1142
|
+
children: [
|
|
1143
|
+
slide === void 0 || slide.imageUrl === null ? /* @__PURE__ */ jsx(Group, { justify: "center", align: "center", h: "100%", children: /* @__PURE__ */ jsx(IconPhotoOff, { size: 20, color: "var(--mantine-color-dimmed)" }) }) : /* @__PURE__ */ jsx(
|
|
1144
|
+
"img",
|
|
1145
|
+
{
|
|
1146
|
+
src: slide.imageUrl,
|
|
1147
|
+
alt: slide.altText ?? "",
|
|
1148
|
+
"data-testid": "post-preview-image",
|
|
1149
|
+
style: cropStyle(slide.crop ?? null)
|
|
1150
|
+
}
|
|
1151
|
+
),
|
|
1152
|
+
index > 0 && /* @__PURE__ */ jsx(
|
|
1153
|
+
ActionIcon,
|
|
1154
|
+
{
|
|
1155
|
+
variant: "default",
|
|
1156
|
+
radius: "xl",
|
|
1157
|
+
size: "lg",
|
|
1158
|
+
"aria-label": "Previous slide",
|
|
1159
|
+
onClick: () => setIndex((current) => Math.max(0, current - 1)),
|
|
1160
|
+
style: { position: "absolute", left: 8, top: "50%", transform: "translateY(-50%)" },
|
|
1161
|
+
children: /* @__PURE__ */ jsx(IconChevronLeft, { size: 18 })
|
|
1162
|
+
}
|
|
1163
|
+
),
|
|
1164
|
+
index < slides.length - 1 && /* @__PURE__ */ jsx(
|
|
1165
|
+
ActionIcon,
|
|
1166
|
+
{
|
|
1167
|
+
variant: "default",
|
|
1168
|
+
radius: "xl",
|
|
1169
|
+
size: "lg",
|
|
1170
|
+
"aria-label": "Next slide",
|
|
1171
|
+
onClick: () => setIndex((current) => Math.min(slides.length - 1, current + 1)),
|
|
1172
|
+
style: { position: "absolute", right: 8, top: "50%", transform: "translateY(-50%)" },
|
|
1173
|
+
children: /* @__PURE__ */ jsx(IconChevronRight, { size: 18 })
|
|
1174
|
+
}
|
|
1175
|
+
)
|
|
1176
|
+
]
|
|
1177
|
+
}
|
|
1178
|
+
) }),
|
|
1179
|
+
slides.length > 1 && /* @__PURE__ */ jsx(Group, { justify: "center", gap: 6, "data-testid": "post-preview-dots", children: slides.map((each, dotIndex) => /* @__PURE__ */ jsx(
|
|
1180
|
+
Box,
|
|
1181
|
+
{
|
|
1182
|
+
style: {
|
|
1183
|
+
width: 6,
|
|
1184
|
+
height: 6,
|
|
1185
|
+
borderRadius: "50%",
|
|
1186
|
+
background: dotIndex === index ? "var(--mantine-color-blue-5)" : "var(--mantine-color-gray-4)"
|
|
1187
|
+
}
|
|
1188
|
+
},
|
|
1189
|
+
each.id
|
|
1190
|
+
)) }),
|
|
1191
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", style: { whiteSpace: "pre-wrap" }, "data-testid": "post-preview-caption", children: caption === null || caption === void 0 || caption.length === 0 ? /* @__PURE__ */ jsx(Text, { component: "span", size: "sm", c: "dimmed", children: "No caption yet" }) : caption }),
|
|
1192
|
+
hashtags.length > 0 && /* @__PURE__ */ jsx(Text, { size: "sm", c: "blue", "data-testid": "post-preview-hashtags", children: hashtags.map((tag) => `#${tag.replace(/^#/, "")}`).join(" ") })
|
|
1193
|
+
] });
|
|
1194
|
+
}
|
|
1195
|
+
function cropStyle(crop) {
|
|
1196
|
+
if (crop === null) {
|
|
1197
|
+
return { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" };
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
position: "absolute",
|
|
1201
|
+
left: `${-crop.x / crop.width * 100}%`,
|
|
1202
|
+
top: `${-crop.y / crop.height * 100}%`,
|
|
1203
|
+
width: `${1 / crop.width * 100}%`,
|
|
1204
|
+
height: `${1 / crop.height * 100}%`,
|
|
1205
|
+
objectFit: "cover"
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// src/features/content/renderCrop.ts
|
|
1210
|
+
var CONTENT_DERIVATIVES_BUCKET = "content-derivatives";
|
|
1211
|
+
var DERIVATIVE_MAX_WIDTH = 1080;
|
|
1212
|
+
var DERIVATIVE_JPEG_QUALITY = 0.9;
|
|
1213
|
+
function derivativeObjectKey(contentItemId, sourceAssetId) {
|
|
1214
|
+
return `${contentItemId}/${sourceAssetId}.jpg`;
|
|
1215
|
+
}
|
|
1216
|
+
function isDerivativeStale(crop, derivativeCrop, derivativePath) {
|
|
1217
|
+
if (crop === null || derivativePath === null) return false;
|
|
1218
|
+
if (derivativeCrop === null) return true;
|
|
1219
|
+
return crop.x !== derivativeCrop.x || crop.y !== derivativeCrop.y || crop.width !== derivativeCrop.width || crop.height !== derivativeCrop.height;
|
|
1220
|
+
}
|
|
1221
|
+
function loadImage(url) {
|
|
1222
|
+
return new Promise((resolve, reject) => {
|
|
1223
|
+
const image = new Image();
|
|
1224
|
+
image.crossOrigin = "anonymous";
|
|
1225
|
+
image.onload = () => resolve(image);
|
|
1226
|
+
image.onerror = () => reject(new Error(`Could not load the image to crop: ${url}`));
|
|
1227
|
+
image.src = url;
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
function canvasToJpegBlob(canvas, quality) {
|
|
1231
|
+
return new Promise((resolve, reject) => {
|
|
1232
|
+
canvas.toBlob(
|
|
1233
|
+
(blob) => {
|
|
1234
|
+
if (blob === null) {
|
|
1235
|
+
reject(new Error("The browser produced no image data for the crop"));
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
resolve(blob);
|
|
1239
|
+
},
|
|
1240
|
+
"image/jpeg",
|
|
1241
|
+
quality
|
|
1242
|
+
);
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
async function renderCropToJpeg(imageUrl, crop, options = {}) {
|
|
1246
|
+
const maxWidth = options.maxWidth ?? DERIVATIVE_MAX_WIDTH;
|
|
1247
|
+
const quality = options.quality ?? DERIVATIVE_JPEG_QUALITY;
|
|
1248
|
+
const image = await loadImage(imageUrl);
|
|
1249
|
+
const box = crop ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
1250
|
+
const sourceX = box.x * image.naturalWidth;
|
|
1251
|
+
const sourceY = box.y * image.naturalHeight;
|
|
1252
|
+
const sourceWidth = box.width * image.naturalWidth;
|
|
1253
|
+
const sourceHeight = box.height * image.naturalHeight;
|
|
1254
|
+
if (sourceWidth < 1 || sourceHeight < 1) {
|
|
1255
|
+
throw new Error("The crop resolves to less than one pixel of the source");
|
|
1256
|
+
}
|
|
1257
|
+
const width = Math.max(1, Math.round(Math.min(maxWidth, sourceWidth)));
|
|
1258
|
+
const height = Math.max(1, Math.round(width * sourceHeight / sourceWidth));
|
|
1259
|
+
const canvas = document.createElement("canvas");
|
|
1260
|
+
canvas.width = width;
|
|
1261
|
+
canvas.height = height;
|
|
1262
|
+
const context = canvas.getContext("2d");
|
|
1263
|
+
if (context === null) throw new Error("The browser would not provide a 2D canvas context");
|
|
1264
|
+
context.fillStyle = "#ffffff";
|
|
1265
|
+
context.fillRect(0, 0, width, height);
|
|
1266
|
+
context.drawImage(image, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, width, height);
|
|
1267
|
+
return canvasToJpegBlob(canvas, quality);
|
|
1268
|
+
}
|
|
1269
|
+
var CAPTION_STEP_KEY = "caption-generation";
|
|
1270
|
+
var CAPTION_WORKFLOW_ID = "cnt-caption-generation-workflow";
|
|
1271
|
+
var AUTOSAVE_DELAY_MS = 800;
|
|
1272
|
+
function readCaptionAttempt(attempt) {
|
|
1273
|
+
if (attempt.stepKey !== CAPTION_STEP_KEY) return null;
|
|
1274
|
+
const payload = attempt.payload;
|
|
1275
|
+
const caption = payload?.caption;
|
|
1276
|
+
if (typeof caption !== "string" || caption.length === 0) return null;
|
|
1277
|
+
return {
|
|
1278
|
+
attempt,
|
|
1279
|
+
caption,
|
|
1280
|
+
hashtags: Array.isArray(payload?.hashtags) ? payload.hashtags.filter((tag) => typeof tag === "string") : [],
|
|
1281
|
+
instructions: typeof payload?.instructions === "string" ? payload.instructions : ""
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
function CaptionPanel({ itemId, body, onDraftChange }) {
|
|
1285
|
+
const queryClient = useQueryClient();
|
|
1286
|
+
const savedBody = body ?? "";
|
|
1287
|
+
const [instructions, setInstructions] = useState("");
|
|
1288
|
+
const [executionId, setExecutionId] = useState(null);
|
|
1289
|
+
const [failure, setFailure] = useState(null);
|
|
1290
|
+
const [draft, setDraft] = useState(savedBody);
|
|
1291
|
+
const attemptsQuery = useContentItemAttempts(itemId);
|
|
1292
|
+
const executeAsync = useExecuteAsync();
|
|
1293
|
+
const updateItem = useUpdateContentItem();
|
|
1294
|
+
const execution = useExecution(CAPTION_WORKFLOW_ID, executionId ?? "");
|
|
1295
|
+
const captions = useMemo(() => {
|
|
1296
|
+
const rows = (attemptsQuery.data?.data ?? []).map(readCaptionAttempt).filter((row) => row !== null);
|
|
1297
|
+
rows.sort((left, right) => right.attempt.attemptNumber - left.attempt.attemptNumber);
|
|
1298
|
+
return rows.map((row, index) => ({ ...row, displayNumber: rows.length - index }));
|
|
1299
|
+
}, [attemptsQuery.data]);
|
|
1300
|
+
const status = execution.data?.status;
|
|
1301
|
+
const isRunning = executeAsync.isPending || executionId !== null && (status === void 0 || status === "running");
|
|
1302
|
+
const lastSyncedRef = useRef(savedBody);
|
|
1303
|
+
const [debouncedDraft] = useDebouncedValue(draft, AUTOSAVE_DELAY_MS);
|
|
1304
|
+
useEffect(() => {
|
|
1305
|
+
if (savedBody === lastSyncedRef.current) return;
|
|
1306
|
+
lastSyncedRef.current = savedBody;
|
|
1307
|
+
setDraft(savedBody);
|
|
1308
|
+
}, [savedBody]);
|
|
1309
|
+
useEffect(() => {
|
|
1310
|
+
if (debouncedDraft === lastSyncedRef.current) return;
|
|
1311
|
+
lastSyncedRef.current = debouncedDraft;
|
|
1312
|
+
updateItem.mutate({ itemId, updates: { body: debouncedDraft } });
|
|
1313
|
+
}, [debouncedDraft, itemId, updateItem]);
|
|
1314
|
+
useEffect(() => {
|
|
1315
|
+
onDraftChange?.(draft);
|
|
1316
|
+
}, [draft, onDraftChange]);
|
|
1317
|
+
const isDirty = draft !== lastSyncedRef.current;
|
|
1318
|
+
useEffect(() => {
|
|
1319
|
+
if (executionId === null || status === void 0 || status === "running") return;
|
|
1320
|
+
if (status === "failed") {
|
|
1321
|
+
setFailure(execution.data?.error ?? "The caption workflow failed. Check the execution log for details.");
|
|
1322
|
+
}
|
|
1323
|
+
void queryClient.invalidateQueries({ queryKey: contentKeys.items() });
|
|
1324
|
+
setExecutionId(null);
|
|
1325
|
+
}, [status, executionId, execution.data?.error, queryClient]);
|
|
1326
|
+
const handleGenerate = async () => {
|
|
1327
|
+
setFailure(null);
|
|
1328
|
+
try {
|
|
1329
|
+
const result = await executeAsync.mutateAsync({
|
|
1330
|
+
resourceId: CAPTION_WORKFLOW_ID,
|
|
1331
|
+
resourceType: "workflow",
|
|
1332
|
+
input: {
|
|
1333
|
+
contentItemId: itemId,
|
|
1334
|
+
...instructions.trim().length > 0 ? { instructions: instructions.trim() } : {}
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1337
|
+
setExecutionId(result.executionId);
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
setFailure(error instanceof Error ? error.message : "Could not start the caption workflow.");
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
const handleUse = (candidate) => {
|
|
1343
|
+
setDraft(
|
|
1344
|
+
candidate.hashtags.length === 0 ? candidate.caption : `${candidate.caption}
|
|
1345
|
+
|
|
1346
|
+
${candidate.hashtags.map((tag) => `#${tag.replace(/^#/, "")}`).join(" ")}`
|
|
1347
|
+
);
|
|
1348
|
+
};
|
|
1349
|
+
return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", "data-testid": "caption-panel", children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
1350
|
+
/* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
|
|
1351
|
+
/* @__PURE__ */ jsx(Title, { order: 5, children: "Caption" }),
|
|
1352
|
+
/* @__PURE__ */ jsxs(Badge, { variant: "light", size: "sm", children: [
|
|
1353
|
+
captions.length,
|
|
1354
|
+
" generated"
|
|
1355
|
+
] })
|
|
1356
|
+
] }),
|
|
1357
|
+
failure !== null && /* @__PURE__ */ jsx(Alert, { color: "red", icon: /* @__PURE__ */ jsx(IconAlertTriangle, { size: 16 }), "data-testid": "caption-panel-error", children: failure }),
|
|
1358
|
+
/* @__PURE__ */ jsx(
|
|
1359
|
+
Textarea,
|
|
1360
|
+
{
|
|
1361
|
+
label: "The caption",
|
|
1362
|
+
description: "Edit it freely. Changes save on their own.",
|
|
1363
|
+
placeholder: "Write it yourself, or generate one below and edit from there.",
|
|
1364
|
+
autosize: true,
|
|
1365
|
+
minRows: 4,
|
|
1366
|
+
maxRows: 16,
|
|
1367
|
+
value: draft,
|
|
1368
|
+
onChange: (event) => setDraft(event.currentTarget.value),
|
|
1369
|
+
"data-testid": "caption-panel-body"
|
|
1370
|
+
}
|
|
1371
|
+
),
|
|
1372
|
+
/* @__PURE__ */ jsx(Group, { justify: "flex-end", gap: "xs", mt: -6, children: updateItem.isPending ? /* @__PURE__ */ jsxs(Group, { gap: 6, children: [
|
|
1373
|
+
/* @__PURE__ */ jsx(Loader, { size: 12 }),
|
|
1374
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Saving" })
|
|
1375
|
+
] }) : isDirty ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", "data-testid": "caption-panel-save-state", children: "Unsaved" }) : /* @__PURE__ */ jsxs(Group, { gap: 4, children: [
|
|
1376
|
+
/* @__PURE__ */ jsx(IconCheck, { size: 12, color: "var(--mantine-color-dimmed)" }),
|
|
1377
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", "data-testid": "caption-panel-save-state", children: "Saved" })
|
|
1378
|
+
] }) }),
|
|
1379
|
+
/* @__PURE__ */ jsx(
|
|
1380
|
+
Textarea,
|
|
1381
|
+
{
|
|
1382
|
+
label: "Direction (optional)",
|
|
1383
|
+
description: "The angle, the occasion, who is in frame -- anything the photos cannot show on their own.",
|
|
1384
|
+
placeholder: "e.g. this is the second shop opening, keep it short and warm",
|
|
1385
|
+
autosize: true,
|
|
1386
|
+
minRows: 2,
|
|
1387
|
+
maxRows: 5,
|
|
1388
|
+
value: instructions,
|
|
1389
|
+
disabled: isRunning,
|
|
1390
|
+
onChange: (event) => setInstructions(event.currentTarget.value)
|
|
1391
|
+
}
|
|
1392
|
+
),
|
|
1393
|
+
/* @__PURE__ */ jsxs(Group, { children: [
|
|
1394
|
+
/* @__PURE__ */ jsx(
|
|
1395
|
+
Button,
|
|
1396
|
+
{
|
|
1397
|
+
leftSection: isRunning ? /* @__PURE__ */ jsx(Loader, { size: 14 }) : /* @__PURE__ */ jsx(IconSparkles, { size: 16 }),
|
|
1398
|
+
disabled: isRunning,
|
|
1399
|
+
onClick: () => void handleGenerate(),
|
|
1400
|
+
"data-testid": "caption-panel-generate",
|
|
1401
|
+
children: captions.length === 0 ? "Generate caption" : "Regenerate"
|
|
1402
|
+
}
|
|
1403
|
+
),
|
|
1404
|
+
isRunning && /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Reading the slides and writing a caption. This does not overwrite the previous one." })
|
|
1405
|
+
] }),
|
|
1406
|
+
attemptsQuery.isLoading ? /* @__PURE__ */ jsx(Group, { justify: "center", p: "md", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : captions.map((candidate) => {
|
|
1407
|
+
const isLive = draft.startsWith(candidate.caption);
|
|
1408
|
+
return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "sm", "data-testid": "caption-panel-attempt", children: /* @__PURE__ */ jsxs(Stack, { gap: 6, children: [
|
|
1409
|
+
/* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", children: [
|
|
1410
|
+
/* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
|
|
1411
|
+
/* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
|
|
1412
|
+
"Attempt ",
|
|
1413
|
+
candidate.displayNumber
|
|
1414
|
+
] }),
|
|
1415
|
+
isLive && /* @__PURE__ */ jsx(Badge, { size: "xs", color: "green", variant: "light", leftSection: /* @__PURE__ */ jsx(IconCheck, { size: 10 }), children: "In use" })
|
|
1416
|
+
] }),
|
|
1417
|
+
/* @__PURE__ */ jsx(
|
|
1418
|
+
Button,
|
|
1419
|
+
{
|
|
1420
|
+
size: "compact-xs",
|
|
1421
|
+
variant: "light",
|
|
1422
|
+
disabled: isLive || updateItem.isPending,
|
|
1423
|
+
onClick: () => handleUse(candidate),
|
|
1424
|
+
children: "Use this caption"
|
|
1425
|
+
}
|
|
1426
|
+
)
|
|
1427
|
+
] }),
|
|
1428
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", style: { whiteSpace: "pre-wrap" }, children: candidate.caption }),
|
|
1429
|
+
candidate.hashtags.length > 0 && /* @__PURE__ */ jsx(Text, { size: "sm", c: "blue", children: candidate.hashtags.map((tag) => `#${tag.replace(/^#/, "")}`).join(" ") }),
|
|
1430
|
+
candidate.instructions.length > 0 && /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
|
|
1431
|
+
"Asked for: ",
|
|
1432
|
+
candidate.instructions
|
|
1433
|
+
] })
|
|
1434
|
+
] }) }, candidate.attempt.id);
|
|
1435
|
+
}),
|
|
1436
|
+
!attemptsQuery.isLoading && captions.length === 0 && /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "Nothing generated yet. Analysed slides make for a caption that describes what is actually in them -- or just write the caption above yourself." })
|
|
1437
|
+
] }) });
|
|
1438
|
+
}
|
|
1439
|
+
var IMAGE_ANALYSIS_WORKFLOW_ID = "cnt-image-analysis-workflow";
|
|
1440
|
+
function readStoredAnalysis(asset) {
|
|
1441
|
+
const metadata = asset?.metadata;
|
|
1442
|
+
if (typeof metadata !== "object" || metadata === null) return null;
|
|
1443
|
+
const analysis = metadata.analysis;
|
|
1444
|
+
if (typeof analysis !== "object" || analysis === null) return null;
|
|
1445
|
+
const raw = analysis;
|
|
1446
|
+
const asString = (key) => typeof raw[key] === "string" ? raw[key] : void 0;
|
|
1447
|
+
return {
|
|
1448
|
+
description: asString("description"),
|
|
1449
|
+
subjects: Array.isArray(raw.subjects) ? raw.subjects.filter((one) => typeof one === "string") : void 0,
|
|
1450
|
+
setting: asString("setting"),
|
|
1451
|
+
mood: asString("mood"),
|
|
1452
|
+
textInImage: asString("textInImage"),
|
|
1453
|
+
altText: asString("altText"),
|
|
1454
|
+
model: asString("model"),
|
|
1455
|
+
generatedAt: asString("generatedAt")
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
function SlideAnalysisPanel({ itemId }) {
|
|
1459
|
+
const queryClient = useQueryClient();
|
|
1460
|
+
const [executionId, setExecutionId] = useState(null);
|
|
1461
|
+
const [failure, setFailure] = useState(null);
|
|
1462
|
+
const { memberships, isMembershipLoaded } = useContentItemSourceAssetDetails(itemId);
|
|
1463
|
+
const executeAsync = useExecuteAsync();
|
|
1464
|
+
const execution = useExecution(IMAGE_ANALYSIS_WORKFLOW_ID, executionId ?? "");
|
|
1465
|
+
const status = execution.data?.status;
|
|
1466
|
+
const isRunning = executeAsync.isPending || executionId !== null && (status === void 0 || status === "running");
|
|
1467
|
+
const analyzedCount = useMemo(
|
|
1468
|
+
() => memberships.filter((detail) => readStoredAnalysis(detail.asset) !== null).length,
|
|
1469
|
+
[memberships]
|
|
1470
|
+
);
|
|
1471
|
+
useEffect(() => {
|
|
1472
|
+
if (executionId === null || status === void 0 || status === "running") return;
|
|
1473
|
+
if (status === "failed") {
|
|
1474
|
+
setFailure(execution.data?.error ?? "The analysis workflow failed. Check the execution log for details.");
|
|
1475
|
+
}
|
|
1476
|
+
void queryClient.invalidateQueries({ queryKey: contentKeys.sourceAssets() });
|
|
1477
|
+
setExecutionId(null);
|
|
1478
|
+
}, [status, executionId, execution.data?.error, queryClient]);
|
|
1479
|
+
const handleAnalyze = async () => {
|
|
1480
|
+
setFailure(null);
|
|
1481
|
+
try {
|
|
1482
|
+
const result = await executeAsync.mutateAsync({
|
|
1483
|
+
resourceId: IMAGE_ANALYSIS_WORKFLOW_ID,
|
|
1484
|
+
resourceType: "workflow",
|
|
1485
|
+
input: { contentItemId: itemId }
|
|
1486
|
+
});
|
|
1487
|
+
setExecutionId(result.executionId);
|
|
1488
|
+
} catch (error) {
|
|
1489
|
+
setFailure(error instanceof Error ? error.message : "Could not start the analysis workflow.");
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", "data-testid": "slide-analysis-panel", children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
1493
|
+
/* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
|
|
1494
|
+
/* @__PURE__ */ jsx(Title, { order: 5, children: "Slide analysis" }),
|
|
1495
|
+
/* @__PURE__ */ jsxs(Badge, { variant: "light", size: "sm", children: [
|
|
1496
|
+
analyzedCount,
|
|
1497
|
+
" of ",
|
|
1498
|
+
memberships.length,
|
|
1499
|
+
" analysed"
|
|
1500
|
+
] })
|
|
1501
|
+
] }),
|
|
1502
|
+
failure !== null && /* @__PURE__ */ jsx(Alert, { color: "red", icon: /* @__PURE__ */ jsx(IconAlertTriangle, { size: 16 }), "data-testid": "slide-analysis-error", children: failure }),
|
|
1503
|
+
/* @__PURE__ */ jsxs(Group, { children: [
|
|
1504
|
+
/* @__PURE__ */ jsx(
|
|
1505
|
+
Button,
|
|
1506
|
+
{
|
|
1507
|
+
leftSection: isRunning ? /* @__PURE__ */ jsx(Loader, { size: 14 }) : /* @__PURE__ */ jsx(IconEye, { size: 16 }),
|
|
1508
|
+
disabled: isRunning || memberships.length === 0,
|
|
1509
|
+
onClick: () => void handleAnalyze(),
|
|
1510
|
+
"data-testid": "slide-analysis-run",
|
|
1511
|
+
children: analyzedCount === 0 ? "Analyse slides" : "Re-analyse slides"
|
|
1512
|
+
}
|
|
1513
|
+
),
|
|
1514
|
+
isRunning && /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Reading each slide with a vision model." })
|
|
1515
|
+
] }),
|
|
1516
|
+
!isMembershipLoaded ? /* @__PURE__ */ jsx(Group, { justify: "center", p: "md", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : memberships.map((detail) => {
|
|
1517
|
+
const analysis = readStoredAnalysis(detail.asset);
|
|
1518
|
+
const label = detail.asset?.title ?? detail.membership.sourceAssetId;
|
|
1519
|
+
return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "sm", "data-testid": "slide-analysis-row", children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
|
|
1520
|
+
/* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
|
|
1521
|
+
/* @__PURE__ */ jsxs(Badge, { size: "xs", variant: "light", children: [
|
|
1522
|
+
"Slide ",
|
|
1523
|
+
detail.membership.position + 1
|
|
1524
|
+
] }),
|
|
1525
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, lineClamp: 1, children: label })
|
|
1526
|
+
] }),
|
|
1527
|
+
analysis === null ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Not analysed yet." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1528
|
+
analysis.description !== void 0 && /* @__PURE__ */ jsx(Text, { size: "sm", children: analysis.description }),
|
|
1529
|
+
analysis.subjects !== void 0 && analysis.subjects.length > 0 && /* @__PURE__ */ jsx(Group, { gap: 4, children: analysis.subjects.map((subject) => /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "outline", children: subject }, subject)) }),
|
|
1530
|
+
(analysis.setting !== void 0 || analysis.mood !== void 0) && /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: [analysis.setting, analysis.mood].filter(Boolean).join(" - ") }),
|
|
1531
|
+
analysis.textInImage !== void 0 && analysis.textInImage.length > 0 && /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
|
|
1532
|
+
"Text in image: ",
|
|
1533
|
+
analysis.textInImage
|
|
1534
|
+
] })
|
|
1535
|
+
] })
|
|
1536
|
+
] }) }, detail.membership.id);
|
|
1537
|
+
}),
|
|
1538
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Analysis is stored on the photo itself, so every post that reuses it reads the same description and a re-crop does not invalidate it." })
|
|
1539
|
+
] }) });
|
|
1540
|
+
}
|
|
539
1541
|
|
|
540
1542
|
// src/lib/content/stage-colors.ts
|
|
541
1543
|
var FALLBACK_CONTENT_STAGE_COLORS = {
|
|
@@ -555,6 +1557,10 @@ function getContentStageColor(entry) {
|
|
|
555
1557
|
}
|
|
556
1558
|
var OVERVIEW_ITEMS_LIMIT = 100;
|
|
557
1559
|
var RECENT_ITEMS_LIMIT = 8;
|
|
1560
|
+
function resolveItemCurrentStep(item, pipelines) {
|
|
1561
|
+
const pipeline = pipelines.find((candidate) => candidate.id === item.pipelineId);
|
|
1562
|
+
return resolveCurrentContentStep(item, pipeline?.steps ?? []);
|
|
1563
|
+
}
|
|
558
1564
|
function ContentOverviewPage({ headerActions, footer } = {}) {
|
|
559
1565
|
const navigate = useNavigate();
|
|
560
1566
|
const itemsQuery = useContentItems({ limit: OVERVIEW_ITEMS_LIMIT });
|
|
@@ -569,7 +1575,9 @@ function ContentOverviewPage({ headerActions, footer } = {}) {
|
|
|
569
1575
|
}
|
|
570
1576
|
return counts;
|
|
571
1577
|
}, [items]);
|
|
572
|
-
const pendingReviewCount = items.filter(
|
|
1578
|
+
const pendingReviewCount = items.filter(
|
|
1579
|
+
(item) => isAwaitingReview(item, resolveItemCurrentStep(item, contentConfig.pipelines))
|
|
1580
|
+
).length;
|
|
573
1581
|
const totalItems = itemsQuery.data?.total ?? items.length;
|
|
574
1582
|
const totalDistributions = distributionsQuery.data?.data.length ?? 0;
|
|
575
1583
|
const recentItems = useMemo(
|
|
@@ -711,10 +1719,9 @@ function ContentItemsPage({ headerActions } = {}) {
|
|
|
711
1719
|
const pagination = usePaginationState(PAGE_SIZE, [searchQuery, statusFilter, pillarFilter, reviewFilterActive]);
|
|
712
1720
|
const openItem = (item) => {
|
|
713
1721
|
const pipeline = contentConfig.pipelines.find((candidate) => candidate.id === item.pipelineId);
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
void navigate({ to: workspaceRoute });
|
|
1722
|
+
const target = resolveReviewTarget(item, pipeline, pipeline?.steps ?? []);
|
|
1723
|
+
if (target.kind === "workspace") {
|
|
1724
|
+
void navigate({ to: target.route });
|
|
718
1725
|
return;
|
|
719
1726
|
}
|
|
720
1727
|
void navigate({ to: "/content/items/$itemId", params: { itemId: item.id } });
|
|
@@ -727,7 +1734,11 @@ function ContentItemsPage({ headerActions } = {}) {
|
|
|
727
1734
|
offset: pagination.offset
|
|
728
1735
|
});
|
|
729
1736
|
const fetchedItems = itemsQuery.data?.data ?? [];
|
|
730
|
-
const items = reviewFilterActive ? fetchedItems.filter((item) =>
|
|
1737
|
+
const items = reviewFilterActive ? fetchedItems.filter((item) => {
|
|
1738
|
+
const pipeline = contentConfig.pipelines.find((candidate) => candidate.id === item.pipelineId);
|
|
1739
|
+
const step = resolveCurrentContentStep(item, pipeline?.steps ?? []);
|
|
1740
|
+
return isAwaitingReview(item, step);
|
|
1741
|
+
}) : fetchedItems;
|
|
731
1742
|
const total = itemsQuery.data?.total ?? 0;
|
|
732
1743
|
const statusOptions = [
|
|
733
1744
|
{ value: "", label: "All statuses" },
|
|
@@ -945,18 +1956,18 @@ function ContentItemReviewPage({ itemId, headerActions }) {
|
|
|
945
1956
|
const pipeline = item ? contentConfig.pipelines.find((candidate) => candidate.id === item.pipelineId) : void 0;
|
|
946
1957
|
const pipelineSteps = useMemo(() => pipeline?.steps ?? [], [pipeline]);
|
|
947
1958
|
const isTerminalPipeline = pipelineSteps.length <= 1;
|
|
948
|
-
const
|
|
949
|
-
() =>
|
|
950
|
-
[item
|
|
1959
|
+
const reviewTarget = useMemo(
|
|
1960
|
+
() => item ? resolveReviewTarget(item, pipeline, pipelineSteps) : { kind: "none" },
|
|
1961
|
+
[item, pipeline, pipelineSteps]
|
|
951
1962
|
);
|
|
952
|
-
const earliestOpenGate =
|
|
1963
|
+
const earliestOpenGate = reviewTarget.kind === "queued" ? reviewTarget.gate : void 0;
|
|
1964
|
+
const workspaceRoute = reviewTarget.kind === "workspace" ? reviewTarget.route : void 0;
|
|
953
1965
|
const reviewDisabledReason = useMemo(() => {
|
|
954
|
-
if (
|
|
1966
|
+
if (reviewTarget.kind !== "none") return void 0;
|
|
955
1967
|
if (!item?.pipelineId) return "This item has no pipeline, so there is no review gate to clear.";
|
|
956
1968
|
if (!pipeline) return `Unknown pipeline "${item.pipelineId}" -- there is no review gate to clear.`;
|
|
957
1969
|
return "No open review gate on this item. Every queued step has already been reviewed, or the current step is still running.";
|
|
958
|
-
}, [
|
|
959
|
-
const workspaceRoute = resolveContentWorkspaceRoute(pipeline, earliestOpenGate?.step);
|
|
1970
|
+
}, [reviewTarget, item?.pipelineId, pipeline]);
|
|
960
1971
|
const reviewActions = useMemo(
|
|
961
1972
|
() => workspaceRoute ? [] : buildReviewActions(isTerminalPipeline, earliestOpenGate, reviewDisabledReason),
|
|
962
1973
|
[workspaceRoute, isTerminalPipeline, earliestOpenGate, reviewDisabledReason]
|
|
@@ -983,8 +1994,8 @@ function ContentItemReviewPage({ itemId, headerActions }) {
|
|
|
983
1994
|
});
|
|
984
1995
|
const reviewQueue = useMemo(() => {
|
|
985
1996
|
if (!item?.pipelineId) return [];
|
|
986
|
-
return (pipelineItemsQuery.data?.data ?? []).filter((candidate) => candidate
|
|
987
|
-
}, [pipelineItemsQuery.data, item?.pipelineId]);
|
|
1997
|
+
return (pipelineItemsQuery.data?.data ?? []).filter((candidate) => isAwaitingReview(candidate, resolveCurrentContentStep(candidate, pipelineSteps))).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
1998
|
+
}, [pipelineItemsQuery.data, item?.pipelineId, pipelineSteps]);
|
|
988
1999
|
const queueIndex = item ? reviewQueue.findIndex((candidate) => candidate.id === item.id) : -1;
|
|
989
2000
|
const queuePosition = queueIndex === -1 ? void 0 : queueIndex + 1;
|
|
990
2001
|
const nextQueueItemId = queueIndex === -1 ? void 0 : reviewQueue[queueIndex + 1]?.id;
|
|
@@ -1128,7 +2139,7 @@ function ContentItemReviewPage({ itemId, headerActions }) {
|
|
|
1128
2139
|
children: /* @__PURE__ */ jsx(Text, { size: "sm", children: reviewDisabledReason })
|
|
1129
2140
|
}
|
|
1130
2141
|
),
|
|
1131
|
-
workspaceRoute &&
|
|
2142
|
+
workspaceRoute && /* @__PURE__ */ jsx(
|
|
1132
2143
|
Alert,
|
|
1133
2144
|
{
|
|
1134
2145
|
icon: /* @__PURE__ */ jsx(IconExternalLink, { size: 16 }),
|
|
@@ -1174,6 +2185,9 @@ function ContentItemReviewPage({ itemId, headerActions }) {
|
|
|
1174
2185
|
/* @__PURE__ */ jsx(Text, { size: "xs", c: "var(--color-error)", fw: 600, children: "Reject reason" }),
|
|
1175
2186
|
/* @__PURE__ */ jsx(Text, { size: "sm", children: item.rejectReason })
|
|
1176
2187
|
] }) }),
|
|
2188
|
+
/* @__PURE__ */ jsx(SourceAssetsPanel, { itemId }),
|
|
2189
|
+
/* @__PURE__ */ jsx(SlideAnalysisPanel, { itemId }),
|
|
2190
|
+
/* @__PURE__ */ jsx(CaptionPanel, { itemId, body: item.body }),
|
|
1177
2191
|
/* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
1178
2192
|
/* @__PURE__ */ jsx(Title, { order: 4, children: "Attempt History" }),
|
|
1179
2193
|
attempts.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No recorded attempts." }) : /* @__PURE__ */ jsxs(Table, { children: [
|
|
@@ -1300,17 +2314,18 @@ function ContentPipelinesPage({ headerActions } = {}) {
|
|
|
1300
2314
|
] }) }) });
|
|
1301
2315
|
}
|
|
1302
2316
|
var ACTOR_LEAD = {
|
|
1303
|
-
vendor: "An outside service",
|
|
1304
|
-
workflow: "An automated workflow",
|
|
1305
|
-
agent: "An AI agent"
|
|
2317
|
+
vendor: "An outside service handles",
|
|
2318
|
+
workflow: "An automated workflow handles",
|
|
2319
|
+
agent: "An AI agent handles",
|
|
2320
|
+
human: "You do"
|
|
1306
2321
|
};
|
|
1307
|
-
var
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
none: "
|
|
2322
|
+
var REVIEW_VENUE_TAIL = {
|
|
2323
|
+
workspace: "and you review it on your own screen, in the moment",
|
|
2324
|
+
queue: "then it waits for your review",
|
|
2325
|
+
none: "with no review after it"
|
|
1311
2326
|
};
|
|
1312
2327
|
function describeStep(step) {
|
|
1313
|
-
return `${ACTOR_LEAD[step.actor]}
|
|
2328
|
+
return `${ACTOR_LEAD[step.actor]} "${step.label}," ${REVIEW_VENUE_TAIL[step.reviewVenue]}.`;
|
|
1314
2329
|
}
|
|
1315
2330
|
function StepRow({ step, resourceId }) {
|
|
1316
2331
|
return /* @__PURE__ */ jsxs(Group, { gap: "sm", align: "flex-start", wrap: "nowrap", children: [
|
|
@@ -1726,6 +2741,13 @@ var QUERY_LIMIT = 100;
|
|
|
1726
2741
|
function formatDate(value) {
|
|
1727
2742
|
return value ? new Date(value).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : "\u2014";
|
|
1728
2743
|
}
|
|
2744
|
+
function summarizeMetrics2(metrics) {
|
|
2745
|
+
if (!metrics || typeof metrics !== "object" || Array.isArray(metrics)) return "\u2014";
|
|
2746
|
+
const entries = Object.entries(metrics);
|
|
2747
|
+
if (entries.length === 0) return "\u2014";
|
|
2748
|
+
const shown = entries.slice(0, 2).map(([key, value]) => `${key} ${String(value)}`).join(" \xB7 ");
|
|
2749
|
+
return entries.length > 2 ? `${shown} +${entries.length - 2}` : shown;
|
|
2750
|
+
}
|
|
1729
2751
|
function ContentDistributionsPage({ headerActions } = {}) {
|
|
1730
2752
|
const navigate = useNavigate();
|
|
1731
2753
|
const contentConfig = useContentConfig();
|
|
@@ -1821,7 +2843,10 @@ function ContentDistributionsPage({ headerActions } = {}) {
|
|
|
1821
2843
|
/* @__PURE__ */ jsx(Table.Th, { children: "Platform" }),
|
|
1822
2844
|
/* @__PURE__ */ jsx(Table.Th, { children: "Format" }),
|
|
1823
2845
|
/* @__PURE__ */ jsx(Table.Th, { children: "Status" }),
|
|
1824
|
-
/* @__PURE__ */ jsx(Table.Th, { children: "Publish Method" }),
|
|
2846
|
+
/* @__PURE__ */ jsx(Table.Th, { style: { whiteSpace: "nowrap" }, children: "Publish Method" }),
|
|
2847
|
+
/* @__PURE__ */ jsx(Table.Th, { style: { whiteSpace: "nowrap" }, children: "Platform post ID" }),
|
|
2848
|
+
/* @__PURE__ */ jsx(Table.Th, { children: "Metrics" }),
|
|
2849
|
+
/* @__PURE__ */ jsx(Table.Th, { style: { whiteSpace: "nowrap" }, children: "Metrics updated" }),
|
|
1825
2850
|
/* @__PURE__ */ jsx(Table.Th, { children: "Published" }),
|
|
1826
2851
|
/* @__PURE__ */ jsx(Table.Th, {})
|
|
1827
2852
|
] }) }),
|
|
@@ -1852,6 +2877,9 @@ function ContentDistributionsPage({ headerActions } = {}) {
|
|
|
1852
2877
|
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", children: distribution.format }) }),
|
|
1853
2878
|
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", children: distribution.status }) }),
|
|
1854
2879
|
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: distribution.publishMethod ?? "\u2014" }) }),
|
|
2880
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "xs", ff: "monospace", c: "dimmed", children: distribution.platformPostId ?? "\u2014" }) }),
|
|
2881
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: summarizeMetrics2(distribution.metrics) }) }),
|
|
2882
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatDate(distribution.metricsUpdatedAt) }) }),
|
|
1855
2883
|
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatDate(distribution.publishedAt) }) }),
|
|
1856
2884
|
/* @__PURE__ */ jsx(Table.Td, { children: distribution.platformUrl && /* @__PURE__ */ jsx(
|
|
1857
2885
|
Anchor,
|
|
@@ -1948,6 +2976,22 @@ function MetricsDisplay({ metrics }) {
|
|
|
1948
2976
|
/* @__PURE__ */ jsx(Text, { size: "sm", children: String(value) })
|
|
1949
2977
|
] }, key)) });
|
|
1950
2978
|
}
|
|
2979
|
+
var METRIC_HISTORY_COLUMNS = [
|
|
2980
|
+
{ key: "views", label: "Views" },
|
|
2981
|
+
{ key: "reach", label: "Reach" },
|
|
2982
|
+
{ key: "likes", label: "Likes" },
|
|
2983
|
+
{ key: "comments", label: "Comments" },
|
|
2984
|
+
{ key: "shares", label: "Shares" },
|
|
2985
|
+
{ key: "saves", label: "Saves" },
|
|
2986
|
+
{ key: "engagements", label: "Engagements" },
|
|
2987
|
+
{ key: "profile_visits", label: "Profile Visits" },
|
|
2988
|
+
{ key: "profile_activity", label: "Profile Activity" },
|
|
2989
|
+
{ key: "follows", label: "Follows" },
|
|
2990
|
+
{ key: "clicks", label: "Clicks" }
|
|
2991
|
+
];
|
|
2992
|
+
function formatMetricValue(value) {
|
|
2993
|
+
return value === void 0 ? "\u2014" : value.toLocaleString();
|
|
2994
|
+
}
|
|
1951
2995
|
function ContentDistributionDetailPage({
|
|
1952
2996
|
distributionId,
|
|
1953
2997
|
headerActions
|
|
@@ -1957,8 +3001,16 @@ function ContentDistributionDetailPage({
|
|
|
1957
3001
|
const distributionQuery = useContentDistribution(distributionId);
|
|
1958
3002
|
const dist = distributionQuery.data;
|
|
1959
3003
|
const itemQuery = useContentItem(dist?.contentItemId);
|
|
3004
|
+
const metricsHistoryQuery = useContentDistributionMetrics(distributionId);
|
|
3005
|
+
const metricsHistory = metricsHistoryQuery.data?.data ?? [];
|
|
1960
3006
|
const [mediaOpened, { open: openMedia, close: closeMedia }] = useDisclosure(false);
|
|
1961
3007
|
const [previewUrl, setPreviewUrl] = useState(null);
|
|
3008
|
+
const mediaEntries = useMemo(() => dist?.mediaUrls ?? [], [dist]);
|
|
3009
|
+
const mediaRefs = useMemo(
|
|
3010
|
+
() => mediaEntries.filter((entry) => entry.kind === "storage").map(({ bucket, path }) => ({ bucket, path })),
|
|
3011
|
+
[mediaEntries]
|
|
3012
|
+
);
|
|
3013
|
+
const { urls: signedMediaUrls } = useStorageSignedUrls(mediaRefs);
|
|
1962
3014
|
const backButton = /* @__PURE__ */ jsx(
|
|
1963
3015
|
Button,
|
|
1964
3016
|
{
|
|
@@ -1985,7 +3037,9 @@ function ContentDistributionDetailPage({
|
|
|
1985
3037
|
const contentTitle = contentItem?.title ?? dist.contentItemId;
|
|
1986
3038
|
const checklist = toChecklist(dist.checklist);
|
|
1987
3039
|
const checklistProgress = getChecklistProgress(checklist);
|
|
1988
|
-
const
|
|
3040
|
+
const mediaItems = mediaEntries.map(
|
|
3041
|
+
(entry) => entry.kind === "storage" ? { key: storageObjectRefKey(entry), url: signedMediaUrls[storageObjectRefKey(entry)] ?? null } : { key: entry.url, url: entry.url }
|
|
3042
|
+
);
|
|
1989
3043
|
const platformEntry = contentConfig.stageCatalog[dist.platform];
|
|
1990
3044
|
const platformColors = getContentStageColor(platformEntry);
|
|
1991
3045
|
const pillarEntry = contentItem?.pillar ? contentConfig.stageCatalog[contentItem.pillar] : void 0;
|
|
@@ -2103,6 +3157,23 @@ function ContentDistributionDetailPage({
|
|
|
2103
3157
|
/* @__PURE__ */ jsx(Divider, {}),
|
|
2104
3158
|
/* @__PURE__ */ jsx(MetricsDisplay, { metrics: dist.metrics })
|
|
2105
3159
|
] }) }),
|
|
3160
|
+
/* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
3161
|
+
/* @__PURE__ */ jsx(Title, { order: 3, children: "Metrics History" }),
|
|
3162
|
+
/* @__PURE__ */ jsx(Divider, {}),
|
|
3163
|
+
metricsHistoryQuery.isLoading ? /* @__PURE__ */ jsx(Center, { p: "md", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : metricsHistoryQuery.isError ? /* @__PURE__ */ jsx(CenteredErrorState, { error: metricsHistoryQuery.error, title: "Failed to load metrics history" }) : metricsHistory.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No metrics readings captured yet." }) : /* @__PURE__ */ jsx(Table.ScrollContainer, { minWidth: 900, children: /* @__PURE__ */ jsxs(Table, { children: [
|
|
3164
|
+
/* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
|
|
3165
|
+
/* @__PURE__ */ jsx(Table.Th, { children: "Captured" }),
|
|
3166
|
+
/* @__PURE__ */ jsx(Table.Th, { children: "Source" }),
|
|
3167
|
+
METRIC_HISTORY_COLUMNS.map((column) => /* @__PURE__ */ jsx(Table.Th, { children: column.label }, column.key))
|
|
3168
|
+
] }) }),
|
|
3169
|
+
/* @__PURE__ */ jsx(Table.Tbody, { children: metricsHistory.map((reading) => /* @__PURE__ */ jsxs(Table.Tr, { children: [
|
|
3170
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatDateTime3(reading.capturedAt) }) }),
|
|
3171
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", children: reading.source }) }),
|
|
3172
|
+
METRIC_HISTORY_COLUMNS.map((column) => /* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", children: formatMetricValue(reading.metrics[column.key]) }) }, column.key))
|
|
3173
|
+
] }, reading.id)) })
|
|
3174
|
+
] }) }),
|
|
3175
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "An em dash means this platform does not report that metric -- it is never the same as zero." })
|
|
3176
|
+
] }) }),
|
|
2106
3177
|
checklist.length > 0 ? /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
2107
3178
|
/* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
|
|
2108
3179
|
/* @__PURE__ */ jsx(Title, { order: 3, children: "Checklist" }),
|
|
@@ -2161,24 +3232,24 @@ function ContentDistributionDetailPage({
|
|
|
2161
3232
|
/* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
2162
3233
|
/* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
|
|
2163
3234
|
/* @__PURE__ */ jsx(Title, { order: 3, children: "Media" }),
|
|
2164
|
-
|
|
2165
|
-
|
|
3235
|
+
mediaItems.length > 0 ? /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
|
|
3236
|
+
mediaItems.length,
|
|
2166
3237
|
" image",
|
|
2167
|
-
|
|
3238
|
+
mediaItems.length !== 1 ? "s" : ""
|
|
2168
3239
|
] }) : null
|
|
2169
3240
|
] }),
|
|
2170
3241
|
/* @__PURE__ */ jsx(Divider, {}),
|
|
2171
|
-
|
|
2172
|
-
/* @__PURE__ */ jsx(
|
|
2173
|
-
Image,
|
|
3242
|
+
mediaItems.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No media yet." }) : /* @__PURE__ */ jsx(SimpleGrid, { cols: { base: 1, xs: 2, sm: 3 }, spacing: "sm", children: mediaItems.map((item, index) => /* @__PURE__ */ jsxs(Card, { withBorder: true, radius: "md", style: { overflow: "hidden" }, children: [
|
|
3243
|
+
item.url === null ? /* @__PURE__ */ jsx(Center, { h: 120, children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : /* @__PURE__ */ jsx(
|
|
3244
|
+
Image$1,
|
|
2174
3245
|
{
|
|
2175
|
-
src: url,
|
|
3246
|
+
src: item.url,
|
|
2176
3247
|
alt: `Media variant ${index + 1}`,
|
|
2177
3248
|
w: "100%",
|
|
2178
3249
|
radius: "sm",
|
|
2179
3250
|
style: { cursor: "pointer" },
|
|
2180
3251
|
onClick: () => {
|
|
2181
|
-
setPreviewUrl(url);
|
|
3252
|
+
setPreviewUrl(item.url);
|
|
2182
3253
|
openMedia();
|
|
2183
3254
|
}
|
|
2184
3255
|
}
|
|
@@ -2189,21 +3260,244 @@ function ContentDistributionDetailPage({
|
|
|
2189
3260
|
{
|
|
2190
3261
|
variant: "subtle",
|
|
2191
3262
|
size: "sm",
|
|
3263
|
+
disabled: item.url === null,
|
|
2192
3264
|
onClick: () => {
|
|
2193
|
-
setPreviewUrl(url);
|
|
3265
|
+
setPreviewUrl(item.url);
|
|
2194
3266
|
openMedia();
|
|
2195
3267
|
},
|
|
2196
3268
|
children: /* @__PURE__ */ jsx(IconMaximize, { size: 16 })
|
|
2197
3269
|
}
|
|
2198
3270
|
) }),
|
|
2199
|
-
/* @__PURE__ */ jsx(Tooltip, { label: "Copy URL", children: /* @__PURE__ */ jsx(
|
|
2200
|
-
|
|
3271
|
+
/* @__PURE__ */ jsx(Tooltip, { label: "Copy URL", children: /* @__PURE__ */ jsx(
|
|
3272
|
+
ActionIcon,
|
|
3273
|
+
{
|
|
3274
|
+
variant: "subtle",
|
|
3275
|
+
size: "sm",
|
|
3276
|
+
disabled: item.url === null,
|
|
3277
|
+
onClick: () => {
|
|
3278
|
+
if (item.url !== null) void navigator.clipboard.writeText(item.url);
|
|
3279
|
+
},
|
|
3280
|
+
children: /* @__PURE__ */ jsx(IconCopy, { size: 16 })
|
|
3281
|
+
}
|
|
3282
|
+
) }),
|
|
3283
|
+
/* @__PURE__ */ jsx(Tooltip, { label: "Download", children: /* @__PURE__ */ jsx(
|
|
3284
|
+
ActionIcon,
|
|
3285
|
+
{
|
|
3286
|
+
variant: "subtle",
|
|
3287
|
+
size: "sm",
|
|
3288
|
+
disabled: item.url === null,
|
|
3289
|
+
component: "a",
|
|
3290
|
+
href: item.url ?? void 0,
|
|
3291
|
+
download: true,
|
|
3292
|
+
target: "_blank",
|
|
3293
|
+
children: /* @__PURE__ */ jsx(IconDownload, { size: 16 })
|
|
3294
|
+
}
|
|
3295
|
+
) })
|
|
2201
3296
|
] })
|
|
2202
|
-
] },
|
|
3297
|
+
] }, item.key)) })
|
|
2203
3298
|
] }) })
|
|
2204
3299
|
] }),
|
|
2205
|
-
/* @__PURE__ */ jsx(CustomModal, { opened: mediaOpened, onClose: closeMedia, size: "xl", children: previewUrl ? /* @__PURE__ */ jsx(Image, { src: previewUrl, alt: "Media preview", fit: "contain" }) : null })
|
|
3300
|
+
/* @__PURE__ */ jsx(CustomModal, { opened: mediaOpened, onClose: closeMedia, size: "xl", children: previewUrl ? /* @__PURE__ */ jsx(Image$1, { src: previewUrl, alt: "Media preview", fit: "contain" }) : null })
|
|
3301
|
+
] });
|
|
3302
|
+
}
|
|
3303
|
+
function ContentWorkspaceShell({
|
|
3304
|
+
pipelineId,
|
|
3305
|
+
panels,
|
|
3306
|
+
initialDraft,
|
|
3307
|
+
title,
|
|
3308
|
+
caption,
|
|
3309
|
+
headerRight,
|
|
3310
|
+
renderStart,
|
|
3311
|
+
renderPersistent,
|
|
3312
|
+
renderAside,
|
|
3313
|
+
canOpenStep,
|
|
3314
|
+
testId
|
|
3315
|
+
}) {
|
|
3316
|
+
const { pipelines, defaultPipelineId } = useContentConfig();
|
|
3317
|
+
const resolvedPipelineId = pipelineId ?? defaultPipelineId;
|
|
3318
|
+
const pipeline = useMemo(
|
|
3319
|
+
() => pipelines.find((entry) => entry.id === resolvedPipelineId),
|
|
3320
|
+
[pipelines, resolvedPipelineId]
|
|
3321
|
+
);
|
|
3322
|
+
const steps = useMemo(() => pipeline?.steps ?? [], [pipeline]);
|
|
3323
|
+
const [isStarted, setIsStarted] = useState(renderStart === void 0);
|
|
3324
|
+
const [activeStepKey, setActiveStepKey] = useState(null);
|
|
3325
|
+
const [furthestIndex, setFurthestIndex] = useState(0);
|
|
3326
|
+
const [adoptedItem, setAdoptedItem] = useState(null);
|
|
3327
|
+
const [bodyDraft, setBodyDraft] = useState(null);
|
|
3328
|
+
const [draft, setDraftState] = useState(initialDraft);
|
|
3329
|
+
const leaveGuardRef = useRef(null);
|
|
3330
|
+
const itemId = adoptedItem?.id;
|
|
3331
|
+
const liveItem = useContentItem(itemId);
|
|
3332
|
+
const distributions = useContentDistributions({ contentItemId: itemId });
|
|
3333
|
+
const item = useMemo(() => {
|
|
3334
|
+
const live = liveItem.data;
|
|
3335
|
+
return live !== void 0 && live.id === itemId ? live : adoptedItem;
|
|
3336
|
+
}, [liveItem.data, itemId, adoptedItem]);
|
|
3337
|
+
const distribution = useMemo(() => {
|
|
3338
|
+
if (itemId === void 0) return null;
|
|
3339
|
+
return distributions.data?.data.find((row) => row.contentItemId === itemId) ?? null;
|
|
3340
|
+
}, [distributions.data, itemId]);
|
|
3341
|
+
const stepIndex = useMemo(() => {
|
|
3342
|
+
const index = steps.findIndex((step2) => step2.key === activeStepKey);
|
|
3343
|
+
return index === -1 ? 0 : index;
|
|
3344
|
+
}, [steps, activeStepKey]);
|
|
3345
|
+
const step = steps[stepIndex];
|
|
3346
|
+
const setDraft = useCallback((update) => {
|
|
3347
|
+
setDraftState(
|
|
3348
|
+
(current) => typeof update === "function" ? update(current) : { ...current, ...update }
|
|
3349
|
+
);
|
|
3350
|
+
}, []);
|
|
3351
|
+
const setItem = useCallback((next) => {
|
|
3352
|
+
setAdoptedItem(next);
|
|
3353
|
+
setBodyDraft(null);
|
|
3354
|
+
}, []);
|
|
3355
|
+
const refetchDistributionsQuery = distributions.refetch;
|
|
3356
|
+
const refetchDistributions = useCallback(() => void refetchDistributionsQuery(), [refetchDistributionsQuery]);
|
|
3357
|
+
const registerLeaveGuard = useCallback((guard) => {
|
|
3358
|
+
leaveGuardRef.current = guard;
|
|
3359
|
+
}, []);
|
|
3360
|
+
const goToStep = useCallback(
|
|
3361
|
+
(stepKey) => {
|
|
3362
|
+
const nextIndex = steps.findIndex((entry) => entry.key === stepKey);
|
|
3363
|
+
const next = steps[nextIndex];
|
|
3364
|
+
if (next === void 0) return;
|
|
3365
|
+
const guard = leaveGuardRef.current;
|
|
3366
|
+
void (async () => {
|
|
3367
|
+
if (guard !== null && nextIndex > stepIndex) await guard(next);
|
|
3368
|
+
setActiveStepKey(next.key);
|
|
3369
|
+
setFurthestIndex((current) => Math.max(current, nextIndex));
|
|
3370
|
+
})();
|
|
3371
|
+
},
|
|
3372
|
+
[steps, stepIndex]
|
|
3373
|
+
);
|
|
3374
|
+
const goToNextStep = useCallback(() => {
|
|
3375
|
+
const next = steps[stepIndex + 1];
|
|
3376
|
+
if (next !== void 0) goToStep(next.key);
|
|
3377
|
+
}, [steps, stepIndex, goToStep]);
|
|
3378
|
+
const goToPreviousStep = useCallback(() => {
|
|
3379
|
+
const previous = steps[stepIndex - 1];
|
|
3380
|
+
if (previous !== void 0) goToStep(previous.key);
|
|
3381
|
+
}, [steps, stepIndex, goToStep]);
|
|
3382
|
+
const restart = useCallback(() => {
|
|
3383
|
+
setAdoptedItem(null);
|
|
3384
|
+
setBodyDraft(null);
|
|
3385
|
+
setDraftState(initialDraft);
|
|
3386
|
+
setActiveStepKey(null);
|
|
3387
|
+
setFurthestIndex(0);
|
|
3388
|
+
leaveGuardRef.current = null;
|
|
3389
|
+
setIsStarted(renderStart === void 0);
|
|
3390
|
+
}, [initialDraft, renderStart]);
|
|
3391
|
+
const state = useMemo(() => {
|
|
3392
|
+
if (pipeline === void 0 || step === void 0) return null;
|
|
3393
|
+
const base = {
|
|
3394
|
+
pipeline,
|
|
3395
|
+
steps,
|
|
3396
|
+
step,
|
|
3397
|
+
stepIndex,
|
|
3398
|
+
item,
|
|
3399
|
+
setItem,
|
|
3400
|
+
body: bodyDraft ?? item?.body ?? "",
|
|
3401
|
+
bodyDraft,
|
|
3402
|
+
setBodyDraft,
|
|
3403
|
+
distribution,
|
|
3404
|
+
refetchDistributions,
|
|
3405
|
+
draft,
|
|
3406
|
+
setDraft,
|
|
3407
|
+
goToStep,
|
|
3408
|
+
goToNextStep,
|
|
3409
|
+
goToPreviousStep,
|
|
3410
|
+
registerLeaveGuard,
|
|
3411
|
+
restart
|
|
3412
|
+
};
|
|
3413
|
+
const resolved = {
|
|
3414
|
+
...base,
|
|
3415
|
+
canOpenStep: (candidate) => canOpenStep === void 0 ? steps.indexOf(candidate) <= furthestIndex : canOpenStep(candidate, resolved)
|
|
3416
|
+
};
|
|
3417
|
+
return resolved;
|
|
3418
|
+
}, [
|
|
3419
|
+
pipeline,
|
|
3420
|
+
steps,
|
|
3421
|
+
step,
|
|
3422
|
+
stepIndex,
|
|
3423
|
+
item,
|
|
3424
|
+
setItem,
|
|
3425
|
+
bodyDraft,
|
|
3426
|
+
distribution,
|
|
3427
|
+
refetchDistributions,
|
|
3428
|
+
draft,
|
|
3429
|
+
setDraft,
|
|
3430
|
+
goToStep,
|
|
3431
|
+
goToNextStep,
|
|
3432
|
+
goToPreviousStep,
|
|
3433
|
+
registerLeaveGuard,
|
|
3434
|
+
restart,
|
|
3435
|
+
canOpenStep,
|
|
3436
|
+
furthestIndex
|
|
3437
|
+
]);
|
|
3438
|
+
const startControls = useMemo(
|
|
3439
|
+
() => ({
|
|
3440
|
+
onStartNew: () => setIsStarted(true),
|
|
3441
|
+
onResume: (resumed, resumeAtStepKey) => {
|
|
3442
|
+
setItem(resumed);
|
|
3443
|
+
setDraftState(initialDraft);
|
|
3444
|
+
setFurthestIndex(steps.length === 0 ? 0 : steps.length - 1);
|
|
3445
|
+
setActiveStepKey(resumeAtStepKey ?? steps[0]?.key ?? null);
|
|
3446
|
+
setIsStarted(true);
|
|
3447
|
+
}
|
|
3448
|
+
}),
|
|
3449
|
+
[setItem, initialDraft, steps]
|
|
3450
|
+
);
|
|
3451
|
+
const body = (() => {
|
|
3452
|
+
if (pipeline === void 0) {
|
|
3453
|
+
return /* @__PURE__ */ jsx(Alert, { color: "yellow", icon: /* @__PURE__ */ jsx(IconAlertTriangle, { size: 16 }), children: resolvedPipelineId === void 0 ? "The deployed Organization Model declares no content pipelines, so there is nothing to walk through here." : `The deployed Organization Model does not declare a "${resolvedPipelineId}" pipeline.` });
|
|
3454
|
+
}
|
|
3455
|
+
if (steps.length === 0 || state === null || step === void 0) {
|
|
3456
|
+
return /* @__PURE__ */ jsxs(Alert, { color: "yellow", icon: /* @__PURE__ */ jsx(IconAlertTriangle, { size: 16 }), children: [
|
|
3457
|
+
"The ",
|
|
3458
|
+
/* @__PURE__ */ jsx("strong", { children: pipeline.label }),
|
|
3459
|
+
" pipeline declares no steps, so there is nothing to render."
|
|
3460
|
+
] });
|
|
3461
|
+
}
|
|
3462
|
+
const Panel = panels[step.key];
|
|
3463
|
+
const panelNode = Panel === void 0 ? /* @__PURE__ */ jsxs(Alert, { color: "yellow", icon: /* @__PURE__ */ jsx(IconAlertTriangle, { size: 16 }), children: [
|
|
3464
|
+
"No panel is registered for the ",
|
|
3465
|
+
/* @__PURE__ */ jsx("strong", { children: step.label }),
|
|
3466
|
+
" step (",
|
|
3467
|
+
/* @__PURE__ */ jsx("code", { children: step.key }),
|
|
3468
|
+
"). Add one to the workspace's panel map."
|
|
3469
|
+
] }) : /* @__PURE__ */ jsx(Panel, { ...state });
|
|
3470
|
+
const aside = renderAside?.(state) ?? null;
|
|
3471
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3472
|
+
/* @__PURE__ */ jsx(Stepper, { active: stepIndex, onStepClick: (next) => goToStep(steps[next]?.key ?? step.key), size: "sm", children: steps.map((entry) => /* @__PURE__ */ jsx(Stepper.Step, { label: entry.label, allowStepSelect: state.canOpenStep(entry) }, entry.key)) }),
|
|
3473
|
+
step.description === void 0 || step.description === "" ? null : /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: step.description }),
|
|
3474
|
+
renderPersistent?.(state),
|
|
3475
|
+
aside === null ? panelNode : /* @__PURE__ */ jsxs(Grid, { gutter: "md", align: "flex-start", children: [
|
|
3476
|
+
/* @__PURE__ */ jsx(Grid.Col, { span: { base: 12, lg: 7 }, children: panelNode }),
|
|
3477
|
+
/* @__PURE__ */ jsx(Grid.Col, { span: { base: 12, lg: 5 }, children: /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: aside }) })
|
|
3478
|
+
] })
|
|
3479
|
+
] });
|
|
3480
|
+
})();
|
|
3481
|
+
return /* @__PURE__ */ jsx(SubshellContentContainer, { children: /* @__PURE__ */ jsx(PageContainer, { children: /* @__PURE__ */ jsxs(Stack, { "data-testid": testId, children: [
|
|
3482
|
+
/* @__PURE__ */ jsx(PageTitleCaption, { title, caption, rightSection: headerRight }),
|
|
3483
|
+
!isStarted && renderStart !== void 0 ? renderStart(startControls) : body
|
|
3484
|
+
] }) }) });
|
|
3485
|
+
}
|
|
3486
|
+
function ContentAnalysisStepPanel(state) {
|
|
3487
|
+
const { item, stepIndex, steps, goToNextStep, goToPreviousStep } = state;
|
|
3488
|
+
if (item === null) {
|
|
3489
|
+
return /* @__PURE__ */ jsx(Alert, { color: "yellow", icon: /* @__PURE__ */ jsx(IconAlertTriangle, { size: 16 }), children: "Nothing has been created yet, so there is nothing to analyse. Go back and finish the earlier steps first." });
|
|
3490
|
+
}
|
|
3491
|
+
return /* @__PURE__ */ jsxs(Stack, { children: [
|
|
3492
|
+
/* @__PURE__ */ jsx(SlideAnalysisPanel, { itemId: item.id }),
|
|
3493
|
+
/* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
|
|
3494
|
+
stepIndex > 0 ? /* @__PURE__ */ jsxs(Button, { variant: "default", leftSection: /* @__PURE__ */ jsx(IconArrowLeft, { size: 16 }), onClick: goToPreviousStep, children: [
|
|
3495
|
+
"Back to ",
|
|
3496
|
+
steps[stepIndex - 1]?.label
|
|
3497
|
+
] }) : /* @__PURE__ */ jsx("span", {}),
|
|
3498
|
+
stepIndex < steps.length - 1 && /* @__PURE__ */ jsx(Button, { rightSection: /* @__PURE__ */ jsx(IconArrowRight, { size: 16 }), onClick: goToNextStep, children: steps[stepIndex + 1]?.label })
|
|
3499
|
+
] })
|
|
2206
3500
|
] });
|
|
2207
3501
|
}
|
|
2208
3502
|
|
|
2209
|
-
export { AlternatesPanel, ContentDistributionDetailPage, ContentDistributionsPage, ContentItemReviewPage, ContentItemsPage, ContentOverviewPage, ContentPipelineBoardPage, ContentPipelineBoardPage as ContentPipelineWorkspacePage, ContentPipelinesPage, ContentReviewCard, PayloadBody, ProcessingStateStrip, ReviewActionBar, firstLine, formatElapsed, lastLine, swapFirstLine, swapLastLine };
|
|
3503
|
+
export { AlternatesPanel, AssetCropEditor, CAPTION_STEP_KEY, CAPTION_WORKFLOW_ID, CONTENT_DERIVATIVES_BUCKET, CROP_ASPECT_OPTIONS, CaptionPanel, ContentAnalysisStepPanel, ContentDistributionDetailPage, ContentDistributionsPage, ContentItemReviewPage, ContentItemsPage, ContentOverviewPage, ContentPipelineBoardPage, ContentPipelineBoardPage as ContentPipelineWorkspacePage, ContentPipelinesPage, ContentReviewCard, ContentWorkspaceShell, DERIVATIVE_JPEG_QUALITY, DERIVATIVE_MAX_WIDTH, IMAGE_ANALYSIS_WORKFLOW_ID, PayloadBody, PostPreview, ProcessingStateStrip, ReviewActionBar, SelectedAssetStrip, SlideAnalysisPanel, SourceAssetsPanel, cropStyle, derivativeObjectKey, firstLine, formatElapsed, isDerivativeStale, lastLine, readStoredAnalysis, renderCropToJpeg, swapFirstLine, swapLastLine };
|