@sentry/junior-dashboard 0.265.0 → 0.267.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/app.js +49 -12
- package/dist/client.js +94 -63
- package/dist/index.js +49 -12
- package/dist/tailwind.css +1 -1
- package/package.json +11 -5
- package/src/app.ts +0 -2
- package/src/auth.ts +4 -7
- package/src/client/App.tsx +35 -53
- package/src/client/components/ActorAvatar.tsx +10 -22
- package/src/client/components/ConnectionBanner.tsx +4 -3
- package/src/client/components/CopyButton.tsx +3 -4
- package/src/client/components/DetailsPanel.tsx +12 -13
- package/src/client/components/Drawer.tsx +46 -23
- package/src/client/components/EmptyTelemetry.tsx +5 -3
- package/src/client/components/ImageAttachment.tsx +49 -63
- package/src/client/components/InlineError.tsx +8 -4
- package/src/client/components/Notice.tsx +19 -26
- package/src/client/components/Pagination.tsx +26 -26
- package/src/client/components/ParticipantAvatarStack.tsx +13 -10
- package/src/client/components/README.md +28 -3
- package/src/client/components/SearchInput.tsx +51 -41
- package/src/client/components/VersionDriftBanner.tsx +9 -7
- package/src/client/components/charts/CacheWrites.tsx +52 -43
- package/src/client/components/ui/alert.tsx +64 -0
- package/src/client/components/ui/avatar.tsx +53 -0
- package/src/client/components/ui/button-group.tsx +59 -0
- package/src/client/components/ui/button.tsx +2 -1
- package/src/client/components/ui/dialog.tsx +60 -0
- package/src/client/components/ui/drawer.tsx +140 -0
- package/src/client/components/ui/empty.tsx +61 -0
- package/src/client/components/ui/input-group.tsx +96 -0
- package/src/client/components/ui/pagination.tsx +60 -0
- package/src/client/components/ui/separator.tsx +22 -0
- package/src/client/components/ui/spinner.tsx +21 -0
- package/src/client/components/ui/table.tsx +22 -1
- package/src/client/components/ui/tooltip.tsx +1 -1
- package/src/client/components/ui/useSheet.ts +5 -1
- package/src/client/conversations/ComposerImages.tsx +66 -14
- package/src/client/conversations/ConversationChildAgents.tsx +5 -5
- package/src/client/conversations/ConversationComposer.tsx +84 -12
- package/src/client/conversations/ConversationDetails.tsx +3 -27
- package/src/client/conversations/ConversationEventDetails.tsx +26 -10
- package/src/client/conversations/ConversationHeader.tsx +5 -4
- package/src/client/conversations/ConversationHeaderActions.tsx +7 -11
- package/src/client/conversations/ConversationPage.tsx +156 -2
- package/src/client/conversations/ConversationSidebar.tsx +5 -4
- package/src/client/conversations/ConversationTranscript.tsx +48 -16
- package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
- package/src/client/conversations/ConversationWatches.tsx +67 -59
- package/src/client/conversations/DetailsList.tsx +77 -29
- package/src/client/conversations/EventDetails.tsx +46 -19
- package/src/client/conversations/EventUsage.tsx +99 -74
- package/src/client/conversations/ForkConversationButton.tsx +79 -73
- package/src/client/conversations/PendingMailboxStack.tsx +312 -119
- package/src/client/conversations/README.md +30 -10
- package/src/client/conversations/ShareConversationButton.tsx +3 -4
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
- package/src/client/conversations/TranscriptActivityGroup.tsx +22 -19
- package/src/client/conversations/TranscriptAttachment.tsx +39 -4
- package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
- package/src/client/conversations/TranscriptEventView.tsx +35 -16
- package/src/client/conversations/TranscriptFailureView.tsx +10 -13
- package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
- package/src/client/conversations/TranscriptMessageContextView.tsx +31 -20
- package/src/client/conversations/TranscriptMessageView.tsx +7 -1
- package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
- package/src/client/conversations/TranscriptSummary.tsx +13 -7
- package/src/client/conversations/TranscriptTurnContextView.tsx +78 -56
- package/src/client/conversations/TranscriptView.tsx +7 -3
- package/src/client/conversations/UserInputRequestCard.tsx +244 -0
- package/src/client/conversations/conversationOutbox.ts +46 -0
- package/src/client/conversations/eventLog.ts +1 -0
- package/src/client/conversations/eventTime.ts +13 -1
- package/src/client/conversations/eventTranscript.ts +12 -5
- package/src/client/conversations/queries.ts +127 -26
- package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
- package/src/client/conversations/transcriptMentions.tsx +18 -5
- package/src/client/conversations/userInputRequest.ts +56 -0
- package/src/client/format.ts +10 -3
- package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
- package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
- package/src/client/pages/automations/AutomationEditor.tsx +182 -156
- package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
- package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
- package/src/client/pages/automations/AutomationOverview.tsx +238 -0
- package/src/client/pages/automations/AutomationPage.tsx +343 -0
- package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
- package/src/client/pages/automations/AutomationRow.tsx +11 -13
- package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
- package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
- package/src/client/pages/automations/AutomationStatus.tsx +164 -108
- package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
- package/src/client/pages/automations/AutomationsPage.tsx +10 -98
- package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
- package/src/client/pages/automations/README.md +56 -46
- package/src/client/types.ts +3 -0
- package/src/mock-reporting/fixtures.ts +44 -2
- package/src/mock-reporting/watches.ts +1 -1
- package/src/client/conversations/transcriptStyles.ts +0 -10
- package/src/client/pages/automations/AutomationDetails.tsx +0 -193
- package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
- package/src/client/pages/automations/AutomationVersionsPage.tsx +0 -390
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
useAppendConversationMessage,
|
|
20
20
|
useArchiveConversation,
|
|
21
21
|
useCancelConversationPendingMessages,
|
|
22
|
+
useSteerConversationPendingMessages,
|
|
22
23
|
useConversationData,
|
|
23
24
|
useStopConversationTurn,
|
|
24
25
|
type PendingArchiveConversationUpdate,
|
|
@@ -32,7 +33,13 @@ import {
|
|
|
32
33
|
CONVERSATION_EVENT_PARAM,
|
|
33
34
|
parseConversationEventSeq,
|
|
34
35
|
} from "./conversationRoutes";
|
|
35
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
ConversationComposer,
|
|
38
|
+
restoreStoredDraftText,
|
|
39
|
+
type ConversationComposerHandle,
|
|
40
|
+
} from "./ConversationComposer";
|
|
41
|
+
import { readStoredComposerImages } from "./ComposerImages";
|
|
42
|
+
import { useDashboardCoreData } from "../api";
|
|
36
43
|
import { ConversationHeader } from "./ConversationHeader";
|
|
37
44
|
import { ConversationDetails } from "./ConversationDetails";
|
|
38
45
|
import { ConversationEventDetails } from "./ConversationEventDetails";
|
|
@@ -66,6 +73,11 @@ import {
|
|
|
66
73
|
hasConversationUsageBreakdown,
|
|
67
74
|
} from "./ConversationUsageBreakdown";
|
|
68
75
|
import { PendingMailboxStack } from "./PendingMailboxStack";
|
|
76
|
+
import { UserInputRequestCard } from "./UserInputRequestCard";
|
|
77
|
+
import {
|
|
78
|
+
openUserInputRequest,
|
|
79
|
+
type OpenUserInputRequest,
|
|
80
|
+
} from "./userInputRequest";
|
|
69
81
|
import {
|
|
70
82
|
buildConversations,
|
|
71
83
|
conversationDetailTitle,
|
|
@@ -184,6 +196,10 @@ export function ConversationPage(props: {
|
|
|
184
196
|
() => committedMessageIds(detail.data?.events),
|
|
185
197
|
[detail.data?.events],
|
|
186
198
|
);
|
|
199
|
+
const userInputRequest = useMemo(
|
|
200
|
+
() => openUserInputRequest(detail.data?.events),
|
|
201
|
+
[detail.data?.events],
|
|
202
|
+
);
|
|
187
203
|
// Cancel needs the latest mailbox watermark, but not as a render prop. Live
|
|
188
204
|
// polls refresh generatedAt every 2s even when the queue is unchanged.
|
|
189
205
|
const pendingGeneratedAtRef = useRef(detail.pendingGeneratedAt);
|
|
@@ -434,6 +450,7 @@ export function ConversationPage(props: {
|
|
|
434
450
|
loadingPreviousPage={detail.isLoadingPreviousPage}
|
|
435
451
|
onLoadPreviousPage={detail.loadPreviousPage}
|
|
436
452
|
onSelectedEventSeqChange={setSelectedEventSeq}
|
|
453
|
+
pendingMessages={detail.inlineMessages}
|
|
437
454
|
pinRequestVersion={pinRequestVersion}
|
|
438
455
|
responding={
|
|
439
456
|
!detail.error && conversationIsResponding(transcript)
|
|
@@ -466,7 +483,17 @@ export function ConversationPage(props: {
|
|
|
466
483
|
// every 2s; a prop would bust footer memo while the reader types.
|
|
467
484
|
pendingGeneratedAtRef={pendingGeneratedAtRef}
|
|
468
485
|
pendingMessages={detail.pendingMessages}
|
|
486
|
+
// Without live history, an inline send would never leave the
|
|
487
|
+
// transcript tail. A started Turn reads a new send only later.
|
|
488
|
+
// Keep those sends in the queue. An earlier inline send that
|
|
489
|
+
// history does not have yet can start that Turn first.
|
|
490
|
+
sendsInline={
|
|
491
|
+
detail.data.eventHistory.status === "available" &&
|
|
492
|
+
!conversationIsResponding(detail.data) &&
|
|
493
|
+
detail.inlineMessages.length === 0
|
|
494
|
+
}
|
|
469
495
|
stoppable={live}
|
|
496
|
+
userInputRequest={userInputRequest}
|
|
470
497
|
/>
|
|
471
498
|
) : undefined
|
|
472
499
|
}
|
|
@@ -548,8 +575,15 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
548
575
|
pendingAuthorization?: ConversationPendingMessagesReport["authorization"];
|
|
549
576
|
pendingGeneratedAtRef: { current: string | undefined };
|
|
550
577
|
pendingMessages: readonly ConversationMailboxMessage[];
|
|
578
|
+
/**
|
|
579
|
+
* True when history is live and no Turn has started, so the next Turn will
|
|
580
|
+
* read a new send at once.
|
|
581
|
+
*/
|
|
582
|
+
sendsInline: boolean;
|
|
551
583
|
/** True while the conversation has a Turn that a stop can end. */
|
|
552
584
|
stoppable: boolean;
|
|
585
|
+
/** Junior's open question, if no user message answers it yet. */
|
|
586
|
+
userInputRequest?: OpenUserInputRequest;
|
|
553
587
|
}) {
|
|
554
588
|
const appendMessage = useAppendConversationMessage(props.conversationId);
|
|
555
589
|
const stopTurn = useStopConversationTurn(props.conversationId);
|
|
@@ -558,6 +592,9 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
558
592
|
const cancelPendingMessages = useCancelConversationPendingMessages(
|
|
559
593
|
props.conversationId,
|
|
560
594
|
);
|
|
595
|
+
const [editTargetInboundMessageId, setEditTargetInboundMessageId] =
|
|
596
|
+
useState<string>();
|
|
597
|
+
const [queueActionError, setQueueActionError] = useState<string>();
|
|
561
598
|
// Keep submit identity stable across mutation status flips so the memoized
|
|
562
599
|
// composer does not re-render while the reader is still typing.
|
|
563
600
|
const appendMessageRef = useRef(appendMessage);
|
|
@@ -583,10 +620,21 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
583
620
|
if (!window.matchMedia("(max-width: 767px)").matches) return;
|
|
584
621
|
onPinRequestRef.current();
|
|
585
622
|
}, [pendingMessageVersion]);
|
|
623
|
+
// No Turn runs and nothing waits in the queue, so the next send would
|
|
624
|
+
// leave the queue almost at once. Show it in the transcript instead. A
|
|
625
|
+
// conversation can be active before its Turn starts; that still counts.
|
|
626
|
+
const committedIds = new Set(props.committedMessageIds);
|
|
627
|
+
const sendInlineRef = useRef(false);
|
|
628
|
+
sendInlineRef.current =
|
|
629
|
+
props.sendsInline &&
|
|
630
|
+
!props.pendingMessages.some(
|
|
631
|
+
(message) => !committedIds.has(message.messageId),
|
|
632
|
+
);
|
|
586
633
|
const onSubmit = useCallback(
|
|
587
634
|
async (message: string, idempotencyKey: string, images?: InputImage[]) => {
|
|
588
635
|
await appendMessageRef.current.mutateAsync({
|
|
589
636
|
idempotencyKey,
|
|
637
|
+
inline: sendInlineRef.current,
|
|
590
638
|
message,
|
|
591
639
|
images,
|
|
592
640
|
});
|
|
@@ -608,11 +656,19 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
608
656
|
cancelPendingMessagesRef.current.reset();
|
|
609
657
|
}
|
|
610
658
|
if (!stopTurnRef.current.isPending) stopTurnRef.current.reset();
|
|
659
|
+
setQueueActionError(undefined);
|
|
611
660
|
onPinRequestRef.current();
|
|
612
661
|
}, []);
|
|
613
662
|
const cancellableMessageIds = props.pendingMessages
|
|
614
663
|
.filter((message) => message.clientStatus === undefined)
|
|
615
664
|
.map((message) => message.inboundMessageId);
|
|
665
|
+
const onAnswerUserInput = useCallback(async (answer: string) => {
|
|
666
|
+
onPinRequestRef.current();
|
|
667
|
+
await appendMessageRef.current.mutateAsync({
|
|
668
|
+
idempotencyKey: crypto.randomUUID(),
|
|
669
|
+
message: answer,
|
|
670
|
+
});
|
|
671
|
+
}, []);
|
|
616
672
|
const onCancelMessage = useCallback(
|
|
617
673
|
(message: ConversationMailboxMessage) => {
|
|
618
674
|
const receivedBefore = props.pendingGeneratedAtRef.current;
|
|
@@ -633,6 +689,86 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
633
689
|
),
|
|
634
690
|
);
|
|
635
691
|
|
|
692
|
+
// Edit uses its own cancel mutation so remove errors stay on remove.
|
|
693
|
+
const editCancel = useCancelConversationPendingMessages(props.conversationId);
|
|
694
|
+
const editCancelRef = useRef(editCancel);
|
|
695
|
+
editCancelRef.current = editCancel;
|
|
696
|
+
const editingRef = useRef(false);
|
|
697
|
+
const composerRef = useRef<ConversationComposerHandle>(null);
|
|
698
|
+
const viewerEmail = useDashboardCoreData().data?.me.user.email;
|
|
699
|
+
const onEditMessage = useCallback(
|
|
700
|
+
async (message: ConversationMailboxMessage) => {
|
|
701
|
+
const receivedBefore = props.pendingGeneratedAtRef.current;
|
|
702
|
+
// The image read runs before the cancel starts, so isPending alone does
|
|
703
|
+
// not stop a second click.
|
|
704
|
+
if (!receivedBefore || editingRef.current) return;
|
|
705
|
+
editingRef.current = true;
|
|
706
|
+
const inboundMessageId = message.inboundMessageId;
|
|
707
|
+
setEditTargetInboundMessageId(inboundMessageId);
|
|
708
|
+
setQueueActionError(undefined);
|
|
709
|
+
try {
|
|
710
|
+
// Read images first. A failed read must leave the message queued.
|
|
711
|
+
const images = await readStoredComposerImages(
|
|
712
|
+
props.conversationId,
|
|
713
|
+
message.attachments,
|
|
714
|
+
);
|
|
715
|
+
// The reader left during the read. Keep the message in the queue.
|
|
716
|
+
if (!composerRef.current) return;
|
|
717
|
+
// Check before the cancel. After it, images that do not fit are lost.
|
|
718
|
+
if (!composerRef.current.canRestoreImages(images)) {
|
|
719
|
+
setQueueActionError("Remove some draft images to edit that message.");
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const result = await editCancelRef.current.mutateAsync({
|
|
723
|
+
inboundMessageIds: [inboundMessageId],
|
|
724
|
+
receivedBefore,
|
|
725
|
+
});
|
|
726
|
+
// Junior can read the message before the cancel lands. Then it is
|
|
727
|
+
// already in the Conversation and there is nothing to edit.
|
|
728
|
+
if (!result.cancelledInboundMessageIds.includes(inboundMessageId)) {
|
|
729
|
+
setQueueActionError("Junior already read that message.");
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const restored = { images, text: message.text ?? "" };
|
|
733
|
+
if (composerRef.current) {
|
|
734
|
+
composerRef.current.restoreMessage(restored);
|
|
735
|
+
} else {
|
|
736
|
+
// The reader left during the cancel. Keep the text in the saved
|
|
737
|
+
// draft so it is there when they come back.
|
|
738
|
+
restoreStoredDraftText(props.conversationId, restored.text);
|
|
739
|
+
}
|
|
740
|
+
} catch {
|
|
741
|
+
setQueueActionError("Could not edit the queued message. Try again.");
|
|
742
|
+
} finally {
|
|
743
|
+
editingRef.current = false;
|
|
744
|
+
setEditTargetInboundMessageId(undefined);
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
[props.conversationId, props.pendingGeneratedAtRef],
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
const steerPendingMessages = useSteerConversationPendingMessages(
|
|
751
|
+
props.conversationId,
|
|
752
|
+
);
|
|
753
|
+
const steerPendingMessagesRef = useRef(steerPendingMessages);
|
|
754
|
+
steerPendingMessagesRef.current = steerPendingMessages;
|
|
755
|
+
const onSteerMessage = useCallback((message: ConversationMailboxMessage) => {
|
|
756
|
+
if (steerPendingMessagesRef.current.isPending) return;
|
|
757
|
+
setQueueActionError(undefined);
|
|
758
|
+
steerPendingMessagesRef.current.mutate(
|
|
759
|
+
{ inboundMessageIds: [message.inboundMessageId] },
|
|
760
|
+
{
|
|
761
|
+
onError: () =>
|
|
762
|
+
setQueueActionError(
|
|
763
|
+
"Could not send the queued message now. Try again.",
|
|
764
|
+
),
|
|
765
|
+
},
|
|
766
|
+
);
|
|
767
|
+
}, []);
|
|
768
|
+
const steerTargetInboundMessageId = steerPendingMessages.isPending
|
|
769
|
+
? steerPendingMessages.variables?.inboundMessageIds[0]
|
|
770
|
+
: undefined;
|
|
771
|
+
|
|
636
772
|
const onStop = useCallback(async () => {
|
|
637
773
|
const result = await stopTurnRef.current.mutateAsync();
|
|
638
774
|
const restored = result.stoppedMessages.map((message) => message.text);
|
|
@@ -657,22 +793,40 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
657
793
|
{props.pendingAuthorization ? (
|
|
658
794
|
<PendingAuthorization authorization={props.pendingAuthorization} />
|
|
659
795
|
) : null}
|
|
796
|
+
{/* A queued message already answers the question. */}
|
|
797
|
+
{props.userInputRequest && props.pendingMessages.length === 0 ? (
|
|
798
|
+
<UserInputRequestCard
|
|
799
|
+
key={props.userInputRequest.toolCallId}
|
|
800
|
+
onAnswer={onAnswerUserInput}
|
|
801
|
+
request={props.userInputRequest}
|
|
802
|
+
/>
|
|
803
|
+
) : null}
|
|
660
804
|
<PendingMailboxStack
|
|
661
805
|
cancelError={cancelError}
|
|
662
806
|
cancelPending={cancelPendingMessages.isPending}
|
|
663
807
|
cancelTargetInboundMessageId={cancelTargetInboundMessageId}
|
|
664
808
|
committedMessageIds={props.committedMessageIds}
|
|
809
|
+
conversationId={props.conversationId}
|
|
810
|
+
editTargetInboundMessageId={editTargetInboundMessageId}
|
|
665
811
|
messages={props.pendingMessages}
|
|
666
812
|
onCancelMessage={onCancelMessage}
|
|
813
|
+
onEditMessage={onEditMessage}
|
|
667
814
|
onLayoutChange={onMailboxLayoutChange}
|
|
668
815
|
onRetry={onRetry}
|
|
816
|
+
onSteerMessage={props.stoppable ? onSteerMessage : undefined}
|
|
817
|
+
steerTargetInboundMessageId={steerTargetInboundMessageId}
|
|
818
|
+
viewerEmail={viewerEmail}
|
|
669
819
|
/>
|
|
670
820
|
</>
|
|
671
821
|
}
|
|
672
822
|
>
|
|
673
823
|
<ConversationComposer
|
|
674
824
|
draftId={props.conversationId}
|
|
675
|
-
error={
|
|
825
|
+
error={
|
|
826
|
+
queueActionError ??
|
|
827
|
+
(stopTurn.error ? "Could not stop Junior. Try again." : undefined)
|
|
828
|
+
}
|
|
829
|
+
ref={composerRef}
|
|
676
830
|
label="Continue this conversation"
|
|
677
831
|
submitLabel="Send"
|
|
678
832
|
onFocus={onComposerFocus}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./conversationSections";
|
|
20
20
|
import { EmptyTelemetry } from "../components/EmptyTelemetry";
|
|
21
21
|
import { SearchInput } from "../components/SearchInput";
|
|
22
|
+
import { Button } from "../components/ui/button";
|
|
22
23
|
import { Skeleton } from "../components/ui/skeleton";
|
|
23
24
|
import {
|
|
24
25
|
Tooltip,
|
|
@@ -112,14 +113,14 @@ export function ConversationSidebar(props: {
|
|
|
112
113
|
<Tooltip>
|
|
113
114
|
<TooltipTrigger
|
|
114
115
|
render={
|
|
115
|
-
<
|
|
116
|
+
<Button
|
|
116
117
|
aria-label="New conversation"
|
|
117
|
-
className="
|
|
118
|
+
className="size-7"
|
|
118
119
|
onClick={props.onNewConversation}
|
|
119
|
-
|
|
120
|
+
size="icon"
|
|
120
121
|
>
|
|
121
122
|
<SquarePen aria-hidden="true" size={15} />
|
|
122
|
-
</
|
|
123
|
+
</Button>
|
|
123
124
|
}
|
|
124
125
|
/>
|
|
125
126
|
<TooltipContent size="compact">New conversation</TooltipContent>
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { memo, useMemo, useRef, type ReactNode } from "react";
|
|
2
|
+
import { History } from "lucide-react";
|
|
3
|
+
import type { ConversationMailboxMessage } from "./conversationOutbox";
|
|
2
4
|
|
|
5
|
+
import { EmptyTelemetry } from "../components/EmptyTelemetry";
|
|
6
|
+
import {
|
|
7
|
+
Empty,
|
|
8
|
+
EmptyDescription,
|
|
9
|
+
EmptyMedia,
|
|
10
|
+
EmptyTitle,
|
|
11
|
+
} from "../components/ui/empty";
|
|
3
12
|
import { Card } from "../components/layout/Card";
|
|
4
|
-
import { unavailableTranscriptLabel } from "../format";
|
|
13
|
+
import { formatTime, unavailableTranscriptLabel } from "../format";
|
|
5
14
|
import { ConversationBrief } from "./ConversationBrief";
|
|
6
15
|
import { transcriptMessagesFromEvents } from "./eventTranscript";
|
|
7
16
|
import type {
|
|
@@ -39,7 +48,6 @@ import {
|
|
|
39
48
|
groupTranscriptMessages,
|
|
40
49
|
type RenderedTranscriptEntry,
|
|
41
50
|
} from "./transcriptRenderModel";
|
|
42
|
-
import { transcriptEmptyClass } from "./transcriptStyles";
|
|
43
51
|
import { entryMatchesSearch, useTranscriptSearch } from "./transcriptSearch";
|
|
44
52
|
import { TranscriptRow, TranscriptRows } from "./TranscriptRows";
|
|
45
53
|
import { TranscriptTimestampProvider } from "./TranscriptTimestamp";
|
|
@@ -74,13 +82,20 @@ export const ConversationTranscriptView = memo(
|
|
|
74
82
|
conversation: ConversationTranscript;
|
|
75
83
|
}) => void;
|
|
76
84
|
conversation: ConversationTranscript;
|
|
85
|
+
/** Sends that history does not have yet. History wins on message id. */
|
|
86
|
+
pendingMessages?: readonly ConversationMailboxMessage[];
|
|
77
87
|
responding?: boolean;
|
|
78
88
|
}) {
|
|
79
89
|
// Cards can change while their Message events stay unchanged.
|
|
80
90
|
const { events, annotations } = props.conversation;
|
|
81
91
|
const messages = useMemo(
|
|
82
|
-
() =>
|
|
83
|
-
|
|
92
|
+
() =>
|
|
93
|
+
transcriptMessagesFromEvents(
|
|
94
|
+
events,
|
|
95
|
+
annotations,
|
|
96
|
+
props.pendingMessages,
|
|
97
|
+
),
|
|
98
|
+
[events, annotations, props.pendingMessages],
|
|
84
99
|
);
|
|
85
100
|
|
|
86
101
|
return (
|
|
@@ -110,7 +125,10 @@ function SegmentEvents(props: {
|
|
|
110
125
|
return (
|
|
111
126
|
<TranscriptRows>
|
|
112
127
|
{props.conversation.eventHistory.status === "expired" ? (
|
|
113
|
-
<ExpiredTranscriptView
|
|
128
|
+
<ExpiredTranscriptView
|
|
129
|
+
brief={props.conversation.brief}
|
|
130
|
+
expiredAt={props.conversation.eventHistory.expiredAt}
|
|
131
|
+
/>
|
|
114
132
|
) : props.conversation.eventHistory.status === "available" ? (
|
|
115
133
|
<VisibleTranscriptEntries
|
|
116
134
|
onOpenSubagentTranscript={props.onOpenSubagentTranscript}
|
|
@@ -131,9 +149,9 @@ function SegmentEvents(props: {
|
|
|
131
149
|
conversation={props.conversation}
|
|
132
150
|
/>
|
|
133
151
|
) : (
|
|
134
|
-
<
|
|
152
|
+
<EmptyTelemetry>
|
|
135
153
|
{unavailableTranscriptLabel(props.conversation)}
|
|
136
|
-
</
|
|
154
|
+
</EmptyTelemetry>
|
|
137
155
|
)}
|
|
138
156
|
{props.responding ? (
|
|
139
157
|
<TranscriptRow indent>
|
|
@@ -145,21 +163,37 @@ function SegmentEvents(props: {
|
|
|
145
163
|
}
|
|
146
164
|
|
|
147
165
|
function ExpiredTranscriptView(props: {
|
|
148
|
-
|
|
166
|
+
brief: ConversationTranscript["brief"];
|
|
167
|
+
expiredAt: string;
|
|
149
168
|
}) {
|
|
150
169
|
return (
|
|
151
170
|
<>
|
|
152
|
-
{props.
|
|
171
|
+
{props.brief ? (
|
|
153
172
|
<Card as="section" padding="md" variant="section">
|
|
154
173
|
<h3 className="mt-0 mb-3 font-display text-lg font-medium text-dashboard-text">
|
|
155
174
|
Brief
|
|
156
175
|
</h3>
|
|
157
|
-
<ConversationBrief brief={props.
|
|
176
|
+
<ConversationBrief brief={props.brief} />
|
|
158
177
|
</Card>
|
|
159
178
|
) : null}
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
179
|
+
<Empty className="flex items-start gap-3 border-dashed border-dashboard-border-emphasis bg-dashboard-fill-soft">
|
|
180
|
+
<EmptyMedia>
|
|
181
|
+
<History />
|
|
182
|
+
</EmptyMedia>
|
|
183
|
+
<div className="min-w-0">
|
|
184
|
+
<EmptyTitle>Transcript expired</EmptyTitle>
|
|
185
|
+
<EmptyDescription className="mt-1 text-sm">
|
|
186
|
+
The retention policy removed these events on{" "}
|
|
187
|
+
<time dateTime={props.expiredAt}>
|
|
188
|
+
{formatTime(props.expiredAt, {
|
|
189
|
+
dateStyle: "medium",
|
|
190
|
+
timeStyle: "short",
|
|
191
|
+
})}
|
|
192
|
+
</time>
|
|
193
|
+
.{props.brief ? " The brief above is still available." : null}
|
|
194
|
+
</EmptyDescription>
|
|
195
|
+
</div>
|
|
196
|
+
</Empty>
|
|
163
197
|
</>
|
|
164
198
|
);
|
|
165
199
|
}
|
|
@@ -348,9 +382,7 @@ function TranscriptEntryList(props: {
|
|
|
348
382
|
}
|
|
349
383
|
|
|
350
384
|
if (search.active && rows.length === 0) {
|
|
351
|
-
return
|
|
352
|
-
<div className={transcriptEmptyClass()}>No events match your search.</div>
|
|
353
|
-
);
|
|
385
|
+
return <EmptyTelemetry>No events match your search.</EmptyTelemetry>;
|
|
354
386
|
}
|
|
355
387
|
|
|
356
388
|
// Let rows participate in the parent transcript stack so messages, tool
|
|
@@ -13,6 +13,16 @@ import {
|
|
|
13
13
|
totalConversationCost,
|
|
14
14
|
} from "../format";
|
|
15
15
|
import { ShimmerText } from "../components/ShimmerText";
|
|
16
|
+
import {
|
|
17
|
+
Table,
|
|
18
|
+
TableBody,
|
|
19
|
+
TableCell,
|
|
20
|
+
TableFooter,
|
|
21
|
+
TableHead,
|
|
22
|
+
TableHeader,
|
|
23
|
+
TableRow,
|
|
24
|
+
} from "../components/ui/table";
|
|
25
|
+
import { cn } from "../styles";
|
|
16
26
|
import type { Conversation } from "../types";
|
|
17
27
|
import { liveModelId } from "./ConversationMeta";
|
|
18
28
|
import {
|
|
@@ -70,7 +80,7 @@ function tokenTypeParts(
|
|
|
70
80
|
return parts.length ? parts : undefined;
|
|
71
81
|
}
|
|
72
82
|
|
|
73
|
-
const CELL = "text-right align-top font-sans tabular-nums";
|
|
83
|
+
const CELL = "px-0 text-right align-top font-sans tabular-nums";
|
|
74
84
|
|
|
75
85
|
/**
|
|
76
86
|
* One usage table. Every table in the breakdown shares column widths, so the
|
|
@@ -85,85 +95,118 @@ function BreakdownTable(props: {
|
|
|
85
95
|
total?: { cost: string; count: string; label: string };
|
|
86
96
|
}) {
|
|
87
97
|
return (
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
>
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
98
|
+
<Table
|
|
99
|
+
aria-label={props.label}
|
|
100
|
+
className="min-w-72 table-fixed text-xs leading-relaxed"
|
|
101
|
+
>
|
|
102
|
+
<colgroup>
|
|
103
|
+
<col />
|
|
104
|
+
<col className="w-20" />
|
|
105
|
+
<col className="w-20" />
|
|
106
|
+
</colgroup>
|
|
107
|
+
<TableHeader>
|
|
108
|
+
<TableRow className="border-dashboard-border">
|
|
109
|
+
<TableHead
|
|
110
|
+
scope="col"
|
|
111
|
+
className="px-0 py-2 leading-relaxed font-normal"
|
|
112
|
+
>
|
|
113
|
+
{props.label}
|
|
114
|
+
</TableHead>
|
|
115
|
+
<TableHead
|
|
116
|
+
scope="col"
|
|
117
|
+
className="px-0 py-2 text-right leading-relaxed font-normal"
|
|
118
|
+
>
|
|
119
|
+
{props.countLabel}
|
|
120
|
+
</TableHead>
|
|
121
|
+
<TableHead
|
|
122
|
+
scope="col"
|
|
123
|
+
className="px-0 py-2 text-right leading-relaxed font-normal"
|
|
124
|
+
>
|
|
125
|
+
Cost
|
|
126
|
+
</TableHead>
|
|
127
|
+
</TableRow>
|
|
128
|
+
</TableHeader>
|
|
129
|
+
{props.rows.map((row) => {
|
|
130
|
+
const padding = row.parts ? "pt-2 pb-0.5" : "py-2";
|
|
131
|
+
return (
|
|
132
|
+
<TableBody
|
|
113
133
|
className="border-b border-dashboard-border-subtle last-of-type:border-b-0"
|
|
114
134
|
key={row.key}
|
|
115
135
|
>
|
|
116
|
-
|
|
117
|
-
|
|
136
|
+
{/* The tbody draws the group border, so its rows have none. */}
|
|
137
|
+
<TableRow className="border-0">
|
|
138
|
+
<TableHead
|
|
118
139
|
scope="row"
|
|
119
|
-
className={
|
|
140
|
+
className={cn(
|
|
141
|
+
"pl-0 pr-3 align-top leading-relaxed font-normal text-dashboard-text",
|
|
142
|
+
padding,
|
|
143
|
+
)}
|
|
120
144
|
>
|
|
121
145
|
<span className="block truncate">{row.label}</span>
|
|
122
|
-
</
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<td className={`${CELL} ${row.parts ? "pt-2 pb-0.5" : "py-2"}`}>
|
|
127
|
-
{row.cost}
|
|
128
|
-
</td>
|
|
129
|
-
</tr>
|
|
146
|
+
</TableHead>
|
|
147
|
+
<TableCell className={cn(CELL, padding)}>{row.count}</TableCell>
|
|
148
|
+
<TableCell className={cn(CELL, padding)}>{row.cost}</TableCell>
|
|
149
|
+
</TableRow>
|
|
130
150
|
{row.parts?.map((part, index, parts) => {
|
|
131
|
-
const
|
|
151
|
+
const partPadding =
|
|
152
|
+
index === parts.length - 1 ? "pt-0 pb-2" : "py-0";
|
|
132
153
|
return (
|
|
133
|
-
<
|
|
134
|
-
className="text-2xs leading-5
|
|
154
|
+
<TableRow
|
|
155
|
+
className="border-0 text-2xs leading-5"
|
|
135
156
|
key={part.label}
|
|
136
157
|
>
|
|
137
|
-
<
|
|
158
|
+
<TableHead
|
|
138
159
|
scope="row"
|
|
139
|
-
className={
|
|
160
|
+
className={cn(
|
|
161
|
+
"truncate pl-3 pr-3 align-top text-2xs leading-5 font-normal text-dashboard-text-faint",
|
|
162
|
+
partPadding,
|
|
163
|
+
)}
|
|
140
164
|
>
|
|
141
165
|
{part.label}
|
|
142
|
-
</
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
166
|
+
</TableHead>
|
|
167
|
+
<TableCell
|
|
168
|
+
className={cn(
|
|
169
|
+
CELL,
|
|
170
|
+
"text-dashboard-text-faint",
|
|
171
|
+
partPadding,
|
|
172
|
+
)}
|
|
173
|
+
>
|
|
174
|
+
{part.count}
|
|
175
|
+
</TableCell>
|
|
176
|
+
<TableCell
|
|
177
|
+
className={cn(
|
|
178
|
+
CELL,
|
|
179
|
+
"text-dashboard-text-faint",
|
|
180
|
+
partPadding,
|
|
181
|
+
)}
|
|
182
|
+
>
|
|
183
|
+
{part.cost}
|
|
184
|
+
</TableCell>
|
|
185
|
+
</TableRow>
|
|
146
186
|
);
|
|
147
187
|
})}
|
|
148
|
-
</
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
</
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
188
|
+
</TableBody>
|
|
189
|
+
);
|
|
190
|
+
})}
|
|
191
|
+
{props.total ? (
|
|
192
|
+
<TableFooter className="font-semibold">
|
|
193
|
+
<TableRow>
|
|
194
|
+
<TableHead
|
|
195
|
+
scope="row"
|
|
196
|
+
className="px-0 py-2 leading-relaxed font-semibold text-dashboard-text"
|
|
197
|
+
>
|
|
198
|
+
{props.total.label}
|
|
199
|
+
</TableHead>
|
|
200
|
+
<TableCell className="px-0 py-2 text-right font-sans tabular-nums">
|
|
201
|
+
{props.total.count}
|
|
202
|
+
</TableCell>
|
|
203
|
+
<TableCell className="px-0 py-2 text-right font-sans tabular-nums">
|
|
204
|
+
{props.total.cost}
|
|
205
|
+
</TableCell>
|
|
206
|
+
</TableRow>
|
|
207
|
+
</TableFooter>
|
|
208
|
+
) : null}
|
|
209
|
+
</Table>
|
|
167
210
|
);
|
|
168
211
|
}
|
|
169
212
|
|