@stigmer/react 3.7.0 → 3.8.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/channel/AgentChannelsPanel.d.ts.map +1 -1
- package/channel/AgentChannelsPanel.js +21 -2
- package/channel/AgentChannelsPanel.js.map +1 -1
- package/channel/ChannelConversationsDialog.d.ts +12 -4
- package/channel/ChannelConversationsDialog.d.ts.map +1 -1
- package/channel/ChannelConversationsDialog.js +18 -24
- package/channel/ChannelConversationsDialog.js.map +1 -1
- package/channel/providers.d.ts +23 -0
- package/channel/providers.d.ts.map +1 -1
- package/channel/providers.js +4 -0
- package/channel/providers.js.map +1 -1
- package/channel/useSaveAgentChannel.d.ts +5 -4
- package/channel/useSaveAgentChannel.d.ts.map +1 -1
- package/channel/useSaveAgentChannel.js +18 -4
- package/channel/useSaveAgentChannel.js.map +1 -1
- package/composer/ComposerToolbar.d.ts +1 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/SessionComposer.d.ts +1 -1
- package/composer/SessionComposer.js +1 -1
- package/composer/SessionComposer.js.map +1 -1
- package/conversation/ConversationAttentionBanner.d.ts +22 -0
- package/conversation/ConversationAttentionBanner.d.ts.map +1 -0
- package/conversation/ConversationAttentionBanner.js +25 -0
- package/conversation/ConversationAttentionBanner.js.map +1 -0
- package/conversation/ConversationComposer.d.ts +41 -0
- package/conversation/ConversationComposer.d.ts.map +1 -0
- package/conversation/ConversationComposer.js +57 -0
- package/conversation/ConversationComposer.js.map +1 -0
- package/conversation/ConversationControlBanner.d.ts +52 -0
- package/conversation/ConversationControlBanner.d.ts.map +1 -0
- package/conversation/ConversationControlBanner.js +59 -0
- package/conversation/ConversationControlBanner.js.map +1 -0
- package/conversation/ConversationListPane.d.ts +68 -0
- package/conversation/ConversationListPane.d.ts.map +1 -0
- package/conversation/ConversationListPane.js +120 -0
- package/conversation/ConversationListPane.js.map +1 -0
- package/conversation/ConversationTimelineView.d.ts +46 -0
- package/conversation/ConversationTimelineView.d.ts.map +1 -0
- package/conversation/ConversationTimelineView.js +166 -0
- package/conversation/ConversationTimelineView.js.map +1 -0
- package/conversation/ConversationsWorkbench.d.ts +63 -0
- package/conversation/ConversationsWorkbench.d.ts.map +1 -0
- package/conversation/ConversationsWorkbench.js +165 -0
- package/conversation/ConversationsWorkbench.js.map +1 -0
- package/conversation/conversationPresentation.d.ts +137 -0
- package/conversation/conversationPresentation.d.ts.map +1 -0
- package/conversation/conversationPresentation.js +230 -0
- package/conversation/conversationPresentation.js.map +1 -0
- package/conversation/index.d.ts +26 -0
- package/conversation/index.d.ts.map +1 -0
- package/conversation/index.js +14 -0
- package/conversation/index.js.map +1 -0
- package/conversation/polling.d.ts +39 -0
- package/conversation/polling.d.ts.map +1 -0
- package/conversation/polling.js +39 -0
- package/conversation/polling.js.map +1 -0
- package/conversation/useConversation.d.ts +61 -0
- package/conversation/useConversation.d.ts.map +1 -0
- package/conversation/useConversation.js +153 -0
- package/conversation/useConversation.js.map +1 -0
- package/conversation/useConversationList.d.ts +98 -0
- package/conversation/useConversationList.d.ts.map +1 -0
- package/conversation/useConversationList.js +200 -0
- package/conversation/useConversationList.js.map +1 -0
- package/conversation/useConversationParticipation.d.ts +63 -0
- package/conversation/useConversationParticipation.d.ts.map +1 -0
- package/conversation/useConversationParticipation.js +84 -0
- package/conversation/useConversationParticipation.js.map +1 -0
- package/conversation/useConversationTimeline.d.ts +71 -0
- package/conversation/useConversationTimeline.d.ts.map +1 -0
- package/conversation/useConversationTimeline.js +152 -0
- package/conversation/useConversationTimeline.js.map +1 -0
- package/conversation/useConversationsWantsHumanCount.d.ts +42 -0
- package/conversation/useConversationsWantsHumanCount.d.ts.map +1 -0
- package/conversation/useConversationsWantsHumanCount.js +40 -0
- package/conversation/useConversationsWantsHumanCount.js.map +1 -0
- package/index.d.ts +4 -0
- package/index.d.ts.map +1 -1
- package/index.js +8 -0
- package/index.js.map +1 -1
- package/internal/tooltip.d.ts.map +1 -1
- package/internal/tooltip.js +7 -1
- package/internal/tooltip.js.map +1 -1
- package/internal/useAutoScroll.d.ts +25 -4
- package/internal/useAutoScroll.d.ts.map +1 -1
- package/internal/useAutoScroll.js +55 -15
- package/internal/useAutoScroll.js.map +1 -1
- package/internal/useFetch.d.ts.map +1 -1
- package/internal/useFetch.js +14 -4
- package/internal/useFetch.js.map +1 -1
- package/models/ModelSelector.d.ts +24 -5
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +31 -9
- package/models/ModelSelector.js.map +1 -1
- package/models/registry.d.ts +3 -2
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js.map +1 -1
- package/models/service-tier.d.ts +1 -1
- package/models/service-tier.d.ts.map +1 -1
- package/models/service-tier.js +1 -1
- package/models/service-tier.js.map +1 -1
- package/package.json +4 -4
- package/schedule/ScheduleDetailView.js +1 -1
- package/schedule/ScheduleDetailView.js.map +1 -1
- package/schedule/ScheduleForm.d.ts.map +1 -1
- package/schedule/ScheduleForm.js +7 -6
- package/schedule/ScheduleForm.js.map +1 -1
- package/sharing/useSaveAgentShare.d.ts +10 -1
- package/sharing/useSaveAgentShare.d.ts.map +1 -1
- package/sharing/useSaveAgentShare.js +9 -0
- package/sharing/useSaveAgentShare.js.map +1 -1
- package/sidebar/WorkspaceSidebar.d.ts +12 -4
- package/sidebar/WorkspaceSidebar.d.ts.map +1 -1
- package/sidebar/WorkspaceSidebar.js +19 -7
- package/sidebar/WorkspaceSidebar.js.map +1 -1
- package/src/channel/AgentChannelsPanel.tsx +33 -2
- package/src/channel/ChannelConversationsDialog.tsx +22 -25
- package/src/channel/__tests__/AgentChannelsPanel.test.tsx +36 -5
- package/src/channel/__tests__/ChannelConversationsDialog.test.tsx +38 -1
- package/src/channel/__tests__/useSaveAgentChannel.test.tsx +26 -0
- package/src/channel/providers.ts +27 -0
- package/src/channel/useSaveAgentChannel.ts +18 -4
- package/src/composer/ComposerToolbar.tsx +1 -1
- package/src/composer/SessionComposer.tsx +2 -2
- package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +4 -3
- package/src/conversation/ConversationAttentionBanner.tsx +77 -0
- package/src/conversation/ConversationComposer.tsx +156 -0
- package/src/conversation/ConversationControlBanner.tsx +193 -0
- package/src/conversation/ConversationListPane.tsx +459 -0
- package/src/conversation/ConversationTimelineView.tsx +435 -0
- package/src/conversation/ConversationsWorkbench.tsx +378 -0
- package/src/conversation/__tests__/ConversationAttentionBanner.test.tsx +82 -0
- package/src/conversation/__tests__/ConversationComposer.test.tsx +160 -0
- package/src/conversation/__tests__/ConversationControlBanner.test.tsx +190 -0
- package/src/conversation/__tests__/ConversationListPane.test.tsx +372 -0
- package/src/conversation/__tests__/ConversationTimelineView.layout.test.tsx +160 -0
- package/src/conversation/__tests__/ConversationTimelineView.test.tsx +405 -0
- package/src/conversation/__tests__/ConversationsWorkbench.test.tsx +860 -0
- package/src/conversation/__tests__/a11y/conversation-surfaces.a11y.test.tsx +235 -0
- package/src/conversation/__tests__/a11y/harness.tsx +100 -0
- package/src/conversation/__tests__/conversation-tooltips.layout.test.tsx +167 -0
- package/src/conversation/__tests__/conversationPollingComposition.test.tsx +138 -0
- package/src/conversation/__tests__/conversationPresentation.test.ts +289 -0
- package/src/conversation/__tests__/useConversation.test.tsx +243 -0
- package/src/conversation/__tests__/useConversationList.test.tsx +492 -0
- package/src/conversation/__tests__/useConversationParticipation.test.tsx +190 -0
- package/src/conversation/__tests__/useConversationTimeline.test.tsx +258 -0
- package/src/conversation/__tests__/useConversationsWantsHumanCount.test.tsx +102 -0
- package/src/conversation/conversationPresentation.ts +305 -0
- package/src/conversation/index.ts +80 -0
- package/src/conversation/polling.ts +41 -0
- package/src/conversation/useConversation.ts +221 -0
- package/src/conversation/useConversationList.ts +328 -0
- package/src/conversation/useConversationParticipation.ts +178 -0
- package/src/conversation/useConversationTimeline.ts +244 -0
- package/src/conversation/useConversationsWantsHumanCount.ts +86 -0
- package/src/index.ts +67 -0
- package/src/internal/__tests__/fake-timer-slices.ts +29 -0
- package/src/internal/__tests__/useAutoScroll.layout.test.tsx +143 -0
- package/src/internal/__tests__/useAutoScroll.test.tsx +62 -5
- package/src/internal/__tests__/useFetch-poll-under-render-pressure.test.tsx +79 -0
- package/src/internal/tooltip.tsx +6 -1
- package/src/internal/useAutoScroll.ts +66 -20
- package/src/internal/useFetch.ts +15 -4
- package/src/models/ModelSelector.tsx +81 -49
- package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +105 -21
- package/src/models/registry.ts +3 -2
- package/src/models/service-tier.ts +1 -1
- package/src/schedule/ScheduleDetailView.tsx +1 -1
- package/src/schedule/ScheduleForm.tsx +7 -6
- package/src/schedule/__tests__/scheduleCreation.test.tsx +2 -2
- package/src/sharing/__tests__/useSaveAgentShare.test.tsx +55 -1
- package/src/sharing/__tests__/useShareToolReadiness.test.tsx +1 -0
- package/src/sharing/useSaveAgentShare.ts +19 -1
- package/src/sidebar/WorkspaceSidebar.tsx +62 -3
- package/src/sidebar/__tests__/WorkspaceSidebar.test.tsx +56 -1
- package/styles.css +1 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { memo, useMemo } from "react";
|
|
4
|
+
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
5
|
+
import { Ban, Check, CheckCheck, Clock, Info, MessageSquare, TriangleAlert } from "lucide-react";
|
|
6
|
+
import { cn } from "@stigmer/theme";
|
|
7
|
+
import { getUserMessage } from "@stigmer/sdk";
|
|
8
|
+
import { channelProviderOf } from "../channel/providers.js";
|
|
9
|
+
import { Button } from "../button/Button.js";
|
|
10
|
+
import { EmptyState } from "../empty-state/EmptyState.js";
|
|
11
|
+
import { JumpToLatestButton } from "../internal/JumpToLatestButton.js";
|
|
12
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "../internal/tooltip.js";
|
|
13
|
+
import { useAutoScroll } from "../internal/useAutoScroll.js";
|
|
14
|
+
import { authorKindOf, inboundPlaceholderOf, isInternalItem, receiptOf, sendAttemptOf, } from "./conversationPresentation.js";
|
|
15
|
+
/**
|
|
16
|
+
* One conversation's customer-visible thread, WhatsApp-style: customer
|
|
17
|
+
* on the left, the business (agent, teammate, platform copy) on the
|
|
18
|
+
* right, internal-lane events (escalations, attention clears) as inline
|
|
19
|
+
* system rows the customer never saw.
|
|
20
|
+
*
|
|
21
|
+
* Honesty rules, enforced here rather than hoped for: outbound items
|
|
22
|
+
* carry their send attempt AND the provider's receipt as two independent
|
|
23
|
+
* facts (DD-004 D-d — an item can be handed to the provider yet fail on
|
|
24
|
+
* the customer's side), failed and suppressed sends render as exactly
|
|
25
|
+
* that, and non-text inbound renders a typed placeholder instead of
|
|
26
|
+
* disappearing.
|
|
27
|
+
*
|
|
28
|
+
* Presentational — pair with `useConversationTimeline`, or compose via
|
|
29
|
+
* `ConversationsWorkbench`.
|
|
30
|
+
*/
|
|
31
|
+
export function ConversationTimelineView({ items, isLoading, error, hasOlder, loadOlder, isLoadingOlder, provider, now, className, }) {
|
|
32
|
+
const { scrollRef, sentinelRef, contentRef, isFollowing, jumpToLatest } = useAutoScroll();
|
|
33
|
+
const days = useMemo(() => groupByDay(items, now), [items, now]);
|
|
34
|
+
const descriptor = channelProviderOf(provider ?? undefined);
|
|
35
|
+
const showsCustomerMessages = descriptor?.timelineIncludesCustomerMessages ?? true;
|
|
36
|
+
return (
|
|
37
|
+
// The provider is context-only (no DOM node); one per view arms
|
|
38
|
+
// every status-glyph tooltip below (the WorkspaceSidebar precedent).
|
|
39
|
+
_jsx(TooltipProvider, { children: _jsxs("div", { "aria-label": "Conversation timeline", className: cn("relative flex min-h-0 flex-1 flex-col", className), children: [_jsxs("div", { ref: scrollRef, className: "min-h-0 flex-1 overflow-y-auto [overflow-anchor:none] px-4 py-3", children: [!showsCustomerMessages && descriptor && (_jsxs("div", { className: "mb-3 flex items-start gap-2 rounded-md border border-border bg-muted-faint px-3 py-2 text-xs text-muted-foreground", children: [_jsx(Info, { "aria-hidden": "true", className: "mt-0.5 size-3.5 shrink-0" }), _jsxs("p", { children: ["Customer messages on ", descriptor.label, " channels aren't shown here yet \u2014 this thread has replies, sends, and internal events only."] })] })), isLoading ? (_jsx(TimelineSkeleton, {})) : error ? (_jsx("p", { className: "px-2 py-6 text-center text-sm text-destructive", children: getUserMessage(error) })) : items.length === 0 ? (_jsx(EmptyState, { variant: "first-use", icon: _jsx(MessageSquare, { className: "size-8" }), title: "No messages yet", description: "The conversation's messages appear here as they happen." })) : (_jsxs("div", { ref: contentRef, children: [hasOlder && (_jsx("div", { className: "flex justify-center pb-2", children: _jsx(Button, { variant: "ghost", size: "xs", onClick: loadOlder, disabled: isLoadingOlder, children: isLoadingOlder ? "Loading…" : "Load earlier messages" }) })), days.map((day) => (_jsxs("div", { children: [_jsx("div", { className: "my-2 text-center text-xs text-muted-foreground-faint", role: "separator", children: day.label }), _jsx("ul", { className: "space-y-1.5", children: day.items.map((item) => (_jsx(TimelineItemRow, { item: item }, item.itemId))) })] }, day.label)))] })), _jsx("div", { ref: sentinelRef, "aria-hidden": "true" })] }), _jsx(JumpToLatestButton, { visible: !isFollowing, onClick: jumpToLatest })] }) }));
|
|
40
|
+
}
|
|
41
|
+
const BUSINESS_CAPTION = {
|
|
42
|
+
agent: "Agent",
|
|
43
|
+
teammate: "Teammate",
|
|
44
|
+
platform: "Automated",
|
|
45
|
+
unknown: "Unknown sender",
|
|
46
|
+
};
|
|
47
|
+
const TimelineItemRow = memo(function TimelineItemRow({ item, }) {
|
|
48
|
+
if (isInternalItem(item)) {
|
|
49
|
+
return _jsx(InternalEventRow, { item: item });
|
|
50
|
+
}
|
|
51
|
+
const author = authorKindOf(item);
|
|
52
|
+
const isCustomer = author === "customer";
|
|
53
|
+
const body = item.text || inboundPlaceholderOf(item);
|
|
54
|
+
// DD-014 D-c (amended at the Sitting 2 gate, R-1): the provider's
|
|
55
|
+
// failure explanation is decision-bearing — window closed vs bad
|
|
56
|
+
// number changes what the operator does next — so it renders as
|
|
57
|
+
// VISIBLE text, not hover-only (the footer glyphs are deliberately
|
|
58
|
+
// non-focusable per F-18, which makes a tooltip mouse-only). Verbatim
|
|
59
|
+
// relay, never pattern-matched; the numeric twin (receipt_error_code)
|
|
60
|
+
// stays off the surface as machine vocabulary. Gated exactly like
|
|
61
|
+
// ReceiptTicks' failed arm — attempt delivered AND receipt failed —
|
|
62
|
+
// so the attempt-axis explanation (last_error, F-25's slice) can
|
|
63
|
+
// structurally never leak in here.
|
|
64
|
+
const receiptExplanation = sendAttemptOf(item) === "delivered" &&
|
|
65
|
+
receiptOf(item) === "failed" &&
|
|
66
|
+
item.receiptDetail !== ""
|
|
67
|
+
? item.receiptDetail
|
|
68
|
+
: null;
|
|
69
|
+
return (_jsx("li", { className: cn("flex", isCustomer ? "justify-start" : "justify-end"), children: _jsxs("div", { className: cn("max-w-[80%] rounded-lg px-3 py-2", isCustomer ? "bg-muted-subtle" : "bg-primary-subtle"), children: [!isCustomer && (_jsx("p", { className: "text-xs font-medium text-muted-foreground", children: BUSINESS_CAPTION[author] })), _jsx("p", { className: cn("whitespace-pre-wrap break-words text-sm text-foreground", !item.text && "italic text-muted-foreground"), children: body || "Message content unavailable" }), receiptExplanation !== null && (_jsx("p", { className: "mt-1 break-words text-xs text-destructive", children: receiptExplanation })), _jsx(ItemFooter, { item: item })] }) }));
|
|
70
|
+
});
|
|
71
|
+
/**
|
|
72
|
+
* A footer status glyph with its explanation on the house tooltip
|
|
73
|
+
* (F-18, replacing native `title` — OS-delayed, imprecise, invisible to
|
|
74
|
+
* keyboard and touch). The trigger renders as a plain `<span>` and is
|
|
75
|
+
* deliberately NOT focusable: these sit inside every bubble's footer,
|
|
76
|
+
* so focusable triggers would add several tab stops per message. Their
|
|
77
|
+
* accessible name stays the visible or `sr-only` text; the tooltip is
|
|
78
|
+
* the visual description only.
|
|
79
|
+
*/
|
|
80
|
+
function StatusHint({ hint, className, children, }) {
|
|
81
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx("span", { className: className }), children: children }), _jsx(TooltipContent, { side: "top", children: hint })] }));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The bubble footer: the item's instant plus, for outbound items, the
|
|
85
|
+
* two status axes — our attempt and the provider's receipt, never
|
|
86
|
+
* collapsed into one glyph.
|
|
87
|
+
*/
|
|
88
|
+
function ItemFooter({ item }) {
|
|
89
|
+
const attempt = sendAttemptOf(item);
|
|
90
|
+
const time = item.at
|
|
91
|
+
? timestampDate(item.at).toLocaleTimeString(undefined, {
|
|
92
|
+
hour: "2-digit",
|
|
93
|
+
minute: "2-digit",
|
|
94
|
+
})
|
|
95
|
+
: "";
|
|
96
|
+
return (_jsxs("p", { className: "mt-1 flex items-center justify-end gap-1 text-xs text-muted-foreground-faint", children: [_jsx("span", { children: time }), attempt === "failed" && (_jsxs(StatusHint, { hint: "The platform could not deliver this message", className: "flex items-center gap-0.5 text-destructive", children: [_jsx(TriangleAlert, { "aria-hidden": "true", className: "size-3" }), "Not delivered"] })), attempt === "suppressed" && (_jsxs(StatusHint, { hint: "Withheld because a human had the conversation when it came due", className: "flex items-center gap-0.5", children: [_jsx(Ban, { "aria-hidden": "true", className: "size-3" }), "Held"] })), (attempt === "pending" || attempt === "delivering") && (_jsxs(StatusHint, { hint: "Sending", className: "flex items-center gap-0.5", children: [_jsx(Clock, { "aria-hidden": "true", className: "size-3" }), _jsx("span", { className: "sr-only", children: "Sending" })] })), attempt === "delivered" && _jsx(ReceiptTicks, { item: item })] }));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The provider's receipt report, WhatsApp-tick style. Renders only on
|
|
100
|
+
* items the platform successfully handed to the provider; the receipt
|
|
101
|
+
* can still be `failed` — the "we sent it, it never arrived" case a
|
|
102
|
+
* collapsed status hides.
|
|
103
|
+
*/
|
|
104
|
+
function ReceiptTicks({ item }) {
|
|
105
|
+
const receipt = receiptOf(item);
|
|
106
|
+
if (receipt === "failed") {
|
|
107
|
+
return (_jsxs(StatusHint, { hint: "The provider accepted this message but could not deliver it", className: "flex items-center gap-0.5 text-destructive", children: [_jsx(TriangleAlert, { "aria-hidden": "true", className: "size-3" }), "Delivery failed"] }));
|
|
108
|
+
}
|
|
109
|
+
if (receipt === "read") {
|
|
110
|
+
return (_jsxs(StatusHint, { hint: "Read by the customer", className: "text-primary", children: [_jsx(CheckCheck, { "aria-hidden": "true", className: "size-3.5" }), _jsx("span", { className: "sr-only", children: "Read" })] }));
|
|
111
|
+
}
|
|
112
|
+
if (receipt === "delivered") {
|
|
113
|
+
return (_jsxs(StatusHint, { hint: "Delivered to the customer's device", children: [_jsx(CheckCheck, { "aria-hidden": "true", className: "size-3.5" }), _jsx("span", { className: "sr-only", children: "Delivered" })] }));
|
|
114
|
+
}
|
|
115
|
+
// Handed to the provider; no (further) receipt yet.
|
|
116
|
+
return (_jsxs(StatusHint, { hint: "Sent", children: [_jsx(Check, { "aria-hidden": "true", className: "size-3.5" }), _jsx("span", { className: "sr-only", children: "Sent" })] }));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Internal-lane rows — org-internal events the customer never saw,
|
|
120
|
+
* rendered as centered system copy so they can never be misread as
|
|
121
|
+
* messages: the agent's escalation (its reason verbatim) and a
|
|
122
|
+
* teammate's attention clear.
|
|
123
|
+
*/
|
|
124
|
+
function InternalEventRow({ item }) {
|
|
125
|
+
const author = authorKindOf(item);
|
|
126
|
+
const isEscalation = author === "agent" && item.text !== "";
|
|
127
|
+
return (_jsx("li", { className: "flex justify-center", children: _jsx("div", { className: cn("flex max-w-[90%] items-start gap-1.5 rounded-md px-3 py-1.5 text-center text-xs", isEscalation
|
|
128
|
+
? "bg-destructive-subtle text-destructive"
|
|
129
|
+
: "bg-muted-faint text-muted-foreground"), children: isEscalation ? (_jsxs(_Fragment, { children: [_jsx(TriangleAlert, { "aria-hidden": "true", className: "mt-0.5 size-3.5 shrink-0" }), _jsxs("span", { className: "min-w-0 break-words", children: ["Agent escalated to a human \u2014 \u201C", item.text, "\u201D"] })] })) : item.text !== "" ? (
|
|
130
|
+
// A future internal note flows through with zero code change.
|
|
131
|
+
_jsx("span", { className: "min-w-0 break-words", children: item.text })) : (_jsx("span", { children: "A teammate resolved the attention flag" })) }) }));
|
|
132
|
+
}
|
|
133
|
+
function TimelineSkeleton() {
|
|
134
|
+
return (_jsxs("div", { className: "space-y-2", "aria-hidden": "true", children: [_jsx("div", { className: "h-10 w-3/5 animate-pulse rounded-lg bg-muted-faint" }), _jsx("div", { className: "ml-auto h-10 w-3/5 animate-pulse rounded-lg bg-muted-faint" }), _jsx("div", { className: "h-10 w-2/5 animate-pulse rounded-lg bg-muted-faint" })] }));
|
|
135
|
+
}
|
|
136
|
+
/** Group chronological items under day separators. */
|
|
137
|
+
function groupByDay(items, now) {
|
|
138
|
+
const ref = now ?? new Date();
|
|
139
|
+
const groups = [];
|
|
140
|
+
let currentKey = "";
|
|
141
|
+
for (const item of items) {
|
|
142
|
+
const date = item.at ? timestampDate(item.at) : null;
|
|
143
|
+
const key = date ? date.toDateString() : "undated";
|
|
144
|
+
if (key !== currentKey) {
|
|
145
|
+
currentKey = key;
|
|
146
|
+
groups.push({ label: date ? dayLabel(date, ref) : "Earlier", items: [] });
|
|
147
|
+
}
|
|
148
|
+
groups[groups.length - 1].items.push(item);
|
|
149
|
+
}
|
|
150
|
+
return groups;
|
|
151
|
+
}
|
|
152
|
+
function dayLabel(date, ref) {
|
|
153
|
+
if (date.toDateString() === ref.toDateString())
|
|
154
|
+
return "Today";
|
|
155
|
+
const yesterday = new Date(ref);
|
|
156
|
+
yesterday.setDate(ref.getDate() - 1);
|
|
157
|
+
if (date.toDateString() === yesterday.toDateString())
|
|
158
|
+
return "Yesterday";
|
|
159
|
+
return date.toLocaleDateString(undefined, {
|
|
160
|
+
weekday: "short",
|
|
161
|
+
month: "short",
|
|
162
|
+
day: "numeric",
|
|
163
|
+
...(date.getFullYear() === ref.getFullYear() ? {} : { year: "numeric" }),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=ConversationTimelineView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConversationTimelineView.js","sourceRoot":"","sources":["../../src/conversation/ConversationTimelineView.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AgentChannel } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
|
|
2
|
+
import { type ConversationIdentity } from "./ConversationListPane.js";
|
|
3
|
+
/**
|
|
4
|
+
* Context handed to a function-form
|
|
5
|
+
* {@link ConversationsWorkbenchProps.headerAccessory}.
|
|
6
|
+
*/
|
|
7
|
+
export interface ConversationHeaderContext {
|
|
8
|
+
/**
|
|
9
|
+
* The open conversation's channel, once the org's channel list
|
|
10
|
+
* answers — `null` while it loads. Carries the channel's name (for
|
|
11
|
+
* scope-explicit accessories like the "Channel access" trigger) and
|
|
12
|
+
* `spec.agent_ref` (for navigation), so hosts never re-fetch what the
|
|
13
|
+
* workbench already holds.
|
|
14
|
+
*/
|
|
15
|
+
readonly channel: AgentChannel | null;
|
|
16
|
+
}
|
|
17
|
+
/** Props for {@link ConversationsWorkbench}. */
|
|
18
|
+
export interface ConversationsWorkbenchProps {
|
|
19
|
+
/** Organization whose conversations to show. */
|
|
20
|
+
readonly org: string;
|
|
21
|
+
/**
|
|
22
|
+
* The open conversation. Controlled by the host so selection can live
|
|
23
|
+
* in the URL (deep links, reloads, back button).
|
|
24
|
+
*/
|
|
25
|
+
readonly selected: ConversationIdentity | null;
|
|
26
|
+
/** Called when the user opens or leaves a conversation. */
|
|
27
|
+
readonly onSelectionChange: (selection: ConversationIdentity | null) => void;
|
|
28
|
+
/**
|
|
29
|
+
* The signed-in staff member's identity account id, when the host
|
|
30
|
+
* knows it — used only for "You have this conversation" copy.
|
|
31
|
+
*/
|
|
32
|
+
readonly currentIdentityAccountId?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Rendered in the open conversation's header (the host's seam for
|
|
35
|
+
* page-level actions — e.g. the channel access panel trigger). The
|
|
36
|
+
* function form receives {@link ConversationHeaderContext} so the
|
|
37
|
+
* accessory can name the channel it acts on.
|
|
38
|
+
*/
|
|
39
|
+
readonly headerAccessory?: React.ReactNode | ((context: ConversationHeaderContext) => React.ReactNode);
|
|
40
|
+
/**
|
|
41
|
+
* Maps the open conversation's channel to the host's channel surface
|
|
42
|
+
* — the owning agent's Channels tab (channels have no standalone
|
|
43
|
+
* page). When provided, the header's channel name renders as a link;
|
|
44
|
+
* `null` for a specific channel keeps it plain text. The SDK never
|
|
45
|
+
* assumes a routing scheme (DD-004).
|
|
46
|
+
*/
|
|
47
|
+
readonly channelHref?: (channel: AgentChannel) => string | null;
|
|
48
|
+
/** Frozen instant for deterministic hosts (tests, documentation tours). */
|
|
49
|
+
readonly now?: Date;
|
|
50
|
+
/** Additional classes for the workbench container. */
|
|
51
|
+
readonly className?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The Conversations console surface (channel-conversations T04), fully
|
|
55
|
+
* composed: the org-wide inbox on the left; the open conversation's
|
|
56
|
+
* timeline, participation banners, and staff composer on the right.
|
|
57
|
+
*
|
|
58
|
+
* Everything under it is independently importable — hosts that need a
|
|
59
|
+
* different layout compose `ConversationListPane`,
|
|
60
|
+
* `ConversationTimelineView`, the banners, and the hooks directly.
|
|
61
|
+
*/
|
|
62
|
+
export declare function ConversationsWorkbench({ org, selected, onSelectionChange, currentIdentityAccountId, headerAccessory, channelHref, now, className, }: ConversationsWorkbenchProps): import("react/jsx-runtime").JSX.Element;
|
|
63
|
+
//# sourceMappingURL=ConversationsWorkbench.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConversationsWorkbench.d.ts","sourceRoot":"","sources":["../../src/conversation/ConversationsWorkbench.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2DAA2D,CAAC;AAY9F,OAAO,EAAwB,KAAK,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAa5F;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAAC;CACvC;AAED,gDAAgD;AAChD,MAAM,WAAW,2BAA2B;IAC1C,gDAAgD;IAChD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC/C,2DAA2D;IAC3D,QAAQ,CAAC,iBAAiB,EAAE,CAAC,SAAS,EAAE,oBAAoB,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7E;;;OAGG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,CAAC,EACrB,KAAK,CAAC,SAAS,GACf,CAAC,CAAC,OAAO,EAAE,yBAAyB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAC9D;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,MAAM,GAAG,IAAI,CAAC;IAChE,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC;IACpB,sDAAsD;IACtD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,GAAG,EACH,QAAQ,EACR,iBAAiB,EACjB,wBAAwB,EACxB,eAAe,EACf,WAAW,EACX,GAAG,EACH,SAAS,GACV,EAAE,2BAA2B,2CAmR7B"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { MessagesSquare } from "lucide-react";
|
|
5
|
+
import { cn } from "@stigmer/theme";
|
|
6
|
+
import { ChannelConversationListFilter, } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/conversation_io_pb";
|
|
7
|
+
import { ChannelSendOutcome } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
|
|
8
|
+
import { channelProviderOf } from "../channel/providers.js";
|
|
9
|
+
import { useOrgAgentChannelList } from "../channel/useOrgAgentChannelList.js";
|
|
10
|
+
import { EmptyState } from "../empty-state/EmptyState.js";
|
|
11
|
+
import { ConversationAttentionBanner } from "./ConversationAttentionBanner.js";
|
|
12
|
+
import { ConversationComposer } from "./ConversationComposer.js";
|
|
13
|
+
import { ConversationControlBanner } from "./ConversationControlBanner.js";
|
|
14
|
+
import { ConversationListPane } from "./ConversationListPane.js";
|
|
15
|
+
import { ConversationTimelineView } from "./ConversationTimelineView.js";
|
|
16
|
+
import { conversationContactOf, conversationLabelOf, outboundItemIdOf, serviceWindowOf, } from "./conversationPresentation.js";
|
|
17
|
+
import { useConversation } from "./useConversation.js";
|
|
18
|
+
import { useConversationList } from "./useConversationList.js";
|
|
19
|
+
import { useConversationParticipation } from "./useConversationParticipation.js";
|
|
20
|
+
import { useConversationTimeline } from "./useConversationTimeline.js";
|
|
21
|
+
/**
|
|
22
|
+
* The Conversations console surface (channel-conversations T04), fully
|
|
23
|
+
* composed: the org-wide inbox on the left; the open conversation's
|
|
24
|
+
* timeline, participation banners, and staff composer on the right.
|
|
25
|
+
*
|
|
26
|
+
* Everything under it is independently importable — hosts that need a
|
|
27
|
+
* different layout compose `ConversationListPane`,
|
|
28
|
+
* `ConversationTimelineView`, the banners, and the hooks directly.
|
|
29
|
+
*/
|
|
30
|
+
export function ConversationsWorkbench({ org, selected, onSelectionChange, currentIdentityAccountId, headerAccessory, channelHref, now, className, }) {
|
|
31
|
+
const [channelFilter, setChannelFilter] = useState("");
|
|
32
|
+
const [listFilter, setListFilter] = useState(ChannelConversationListFilter.channel_conversation_list_filter_unspecified);
|
|
33
|
+
const { channels } = useOrgAgentChannelList(org || null);
|
|
34
|
+
const list = useConversationList({
|
|
35
|
+
org: org || null,
|
|
36
|
+
agentChannelId: channelFilter,
|
|
37
|
+
filter: listFilter,
|
|
38
|
+
});
|
|
39
|
+
const detail = useConversation(selected?.agentChannelId ?? "", selected?.conversationKey ?? "");
|
|
40
|
+
const timeline = useConversationTimeline(selected?.agentChannelId ?? "", selected?.conversationKey ?? "");
|
|
41
|
+
// DD-012 D-a: a command answer is fresher than any in-flight poll on
|
|
42
|
+
// BOTH surfaces rendering this conversation — fan it out to the detail
|
|
43
|
+
// seam (banners, composer state) and the list seam (the inbox row), so
|
|
44
|
+
// your own takeover never lags the inbox (F-06).
|
|
45
|
+
const detailApply = detail.applyServerState;
|
|
46
|
+
const listApply = list.applyServerState;
|
|
47
|
+
const adoptConversation = useCallback((fresh) => {
|
|
48
|
+
detailApply(fresh);
|
|
49
|
+
listApply(fresh);
|
|
50
|
+
}, [detailApply, listApply]);
|
|
51
|
+
const participation = useConversationParticipation({
|
|
52
|
+
agentChannelId: selected?.agentChannelId ?? "",
|
|
53
|
+
conversationKey: selected?.conversationKey ?? "",
|
|
54
|
+
onConversation: adoptConversation,
|
|
55
|
+
});
|
|
56
|
+
const selectedChannel = useMemo(() => selected
|
|
57
|
+
? channels.find((c) => c.metadata?.id === selected.agentChannelId) ?? null
|
|
58
|
+
: null, [channels, selected]);
|
|
59
|
+
const descriptor = channelProviderOf(selectedChannel?.spec?.providerConfig?.case);
|
|
60
|
+
// DD-007 D-e: the handback guard arms on the row's server-derived
|
|
61
|
+
// awaiting_reply fact — bounce-aware since T08 (a staff reply the
|
|
62
|
+
// provider later failed does NOT count as an answer), and platform
|
|
63
|
+
// acknowledgments never stamp it. Never re-derive this from timeline
|
|
64
|
+
// authorship: that derivation read a bounced staff reply as "answered"
|
|
65
|
+
// and went silent in exactly the scenario the guard exists for (F-28)
|
|
66
|
+
// — the client-side twin of the re-derivation DD-011 A-1 removed
|
|
67
|
+
// server-side. No loading gap: the Hand back button renders from this
|
|
68
|
+
// same row, so the guard and the control it protects appear together.
|
|
69
|
+
const unansweredCustomer = detail.conversation?.awaitingReply ?? false;
|
|
70
|
+
const composerDisabledReason = descriptor && !descriptor.supportsStaffReplies
|
|
71
|
+
? `Staff replies aren't available on ${descriptor.label} channels yet — the agent keeps serving this conversation.`
|
|
72
|
+
: detail.awaitingCustomer
|
|
73
|
+
? "The customer hasn't written yet — staff replies unlock with their first message."
|
|
74
|
+
: null;
|
|
75
|
+
// DD-014 D-b/D-e: the closed-window forecast, computed fresh on EVERY
|
|
76
|
+
// render — deliberately NOT memoized on [detail.conversation]: the
|
|
77
|
+
// row's reference is intentionally stable across identical polls
|
|
78
|
+
// (DD-010) while the window is wall-clock-anchored, so a memo keyed on
|
|
79
|
+
// the row would freeze this warning forever on a quiet conversation.
|
|
80
|
+
// The 5s detail poll's re-render is the refresh cadence; the workbench
|
|
81
|
+
// test "the 5s detail poll alone carries the advisory over the
|
|
82
|
+
// boundary" is the guard. The advisory names the rule (DD-014 D-c's
|
|
83
|
+
// one home for it); the failed tick relays the provider's verdict.
|
|
84
|
+
const serviceWindow = detail.conversation
|
|
85
|
+
? serviceWindowOf(detail.conversation, descriptor?.id ?? null, now ?? new Date())
|
|
86
|
+
: null;
|
|
87
|
+
const composerAdvisory = serviceWindow === "closed" && descriptor
|
|
88
|
+
? `${descriptor.label} closes free-form replies 24 hours after the customer's last message — a reply sent now will probably fail. The window reopens when the customer writes again.`
|
|
89
|
+
: null;
|
|
90
|
+
const handleSelect = useCallback((conversation) => onSelectionChange({
|
|
91
|
+
agentChannelId: conversation.agentChannelId,
|
|
92
|
+
conversationKey: conversation.conversationKey,
|
|
93
|
+
}), [onSelectionChange]);
|
|
94
|
+
// F-05: an accepted reply's ledger item is only visible once the next
|
|
95
|
+
// timeline read answers, so the composer's busy state must span
|
|
96
|
+
// click-to-visible, not end at the RPC. The settling record names the
|
|
97
|
+
// item the reply output promised (`ob:<outbound_message_id>`) and the
|
|
98
|
+
// selection it belongs to — carrying the selection makes stale state
|
|
99
|
+
// from a previously open conversation structurally inert (the derived
|
|
100
|
+
// id is null there) instead of needing a reset effect.
|
|
101
|
+
const [replySettling, setReplySettling] = useState(null);
|
|
102
|
+
const selectionKey = selected
|
|
103
|
+
? `${selected.agentChannelId}:${selected.conversationKey}`
|
|
104
|
+
: "";
|
|
105
|
+
const settlingItemId = replySettling !== null && replySettling.selectionKey === selectionKey
|
|
106
|
+
? replySettling.itemId
|
|
107
|
+
: null;
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (settlingItemId === null)
|
|
110
|
+
return;
|
|
111
|
+
if (timeline.items.some((item) => item.itemId === settlingItemId)) {
|
|
112
|
+
setReplySettling(null);
|
|
113
|
+
}
|
|
114
|
+
}, [timeline.items, settlingItemId]);
|
|
115
|
+
const handleSend = useCallback(async (text) => {
|
|
116
|
+
const output = await participation.reply(text);
|
|
117
|
+
// The ledger row is committed before reply answers, so one head
|
|
118
|
+
// refresh renders the REAL item (with its true status) — the SDK
|
|
119
|
+
// never fabricates an optimistic item that might misstate what
|
|
120
|
+
// the provider did. Until that item lands, the composer stays
|
|
121
|
+
// busy; even if this refetch fails, the head poll delivers the
|
|
122
|
+
// item and clears the hold. A refusal never settles: it restores
|
|
123
|
+
// the draft for editing, and holding the composer busy would
|
|
124
|
+
// fight the correction its notice asks for.
|
|
125
|
+
if (output.outcome !== ChannelSendOutcome.refused &&
|
|
126
|
+
output.outboundMessageId !== "") {
|
|
127
|
+
setReplySettling({
|
|
128
|
+
selectionKey,
|
|
129
|
+
itemId: outboundItemIdOf(output.outboundMessageId),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
timeline.refetch();
|
|
133
|
+
return output;
|
|
134
|
+
}, [participation.reply, timeline.refetch, selectionKey]);
|
|
135
|
+
const detailLabel = detail.conversation
|
|
136
|
+
? conversationLabelOf(detail.conversation, descriptor?.id ?? null)
|
|
137
|
+
: selected?.conversationKey ?? "";
|
|
138
|
+
// The customer's reachable address (e.g. the WhatsApp number a display
|
|
139
|
+
// name hides) — the call-back path belongs beside the channel name.
|
|
140
|
+
const detailContact = detail.conversation
|
|
141
|
+
? conversationContactOf(detail.conversation, descriptor?.id ?? null)
|
|
142
|
+
: null;
|
|
143
|
+
const channelName = selectedChannel
|
|
144
|
+
? selectedChannel.metadata?.name || selectedChannel.metadata?.slug
|
|
145
|
+
: null;
|
|
146
|
+
const selectedChannelHref = selectedChannel && channelHref ? channelHref(selectedChannel) : null;
|
|
147
|
+
const headerAccessoryNode = typeof headerAccessory === "function"
|
|
148
|
+
? headerAccessory({ channel: selectedChannel })
|
|
149
|
+
: headerAccessory;
|
|
150
|
+
return (_jsxs("div", { "aria-label": "Conversations workbench", className: cn("flex min-h-0 flex-1", className), children: [_jsx(ConversationListPane, { className: "w-80 shrink-0 border-r border-border", conversations: list.conversations, isLoading: list.isLoading, error: list.error, hasMore: list.hasMore, loadMore: list.loadMore, isLoadingMore: list.isLoadingMore, channels: channels, channelFilter: channelFilter, onChannelFilterChange: setChannelFilter, filter: listFilter, onFilterChange: setListFilter, selected: selected, onSelect: handleSelect, now: now }), selected === null ? (_jsx("div", { className: "flex min-w-0 flex-1 items-center justify-center p-6", children: _jsx(EmptyState, { variant: "first-use", icon: _jsx(MessagesSquare, { className: "size-8" }), title: "Select a conversation", description: "Pick a conversation to read its thread, reply as your business, or take over from the agent." }) })) : (
|
|
151
|
+
// Keyed by conversation identity (DD-014: `key` remounts are the
|
|
152
|
+
// clean-reset pattern): every open starts a fresh detail column, so
|
|
153
|
+
// auto-scroll opens following at the newest message and the
|
|
154
|
+
// composer's draft and notice can never travel from one customer's
|
|
155
|
+
// conversation into another's (F-22). The data hooks live above
|
|
156
|
+
// this column, so the remount refetches nothing; the inbox pane
|
|
157
|
+
// stays outside it so its scroll position and filter survive.
|
|
158
|
+
// Channel ids never contain ":", so the joined key is unambiguous.
|
|
159
|
+
_jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-border px-4 py-2.5", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "truncate text-sm font-semibold text-foreground", children: detailLabel }), (detailContact !== null || channelName) && (_jsxs("p", { className: "truncate text-xs text-muted-foreground", children: [detailContact, detailContact !== null && channelName && " · ", channelName &&
|
|
160
|
+
// The path from a conversation to its channel (F-11):
|
|
161
|
+
// the channel name links to the host's channel surface
|
|
162
|
+
// when a route exists.
|
|
163
|
+
(selectedChannelHref ? (_jsx("a", { href: selectedChannelHref, className: "hover:text-foreground hover:underline", children: channelName })) : (channelName))] }))] }), headerAccessoryNode] }), detail.conversation && (_jsx(ConversationAttentionBanner, { conversation: detail.conversation, participation: participation })), detail.conversation && (_jsx(ConversationControlBanner, { conversation: detail.conversation, participation: participation, unansweredCustomer: unansweredCustomer, supportsStaffReplies: descriptor?.supportsStaffReplies ?? true, currentIdentityAccountId: currentIdentityAccountId })), _jsx(ConversationTimelineView, { items: timeline.items, isLoading: timeline.isLoading, error: timeline.error, hasOlder: timeline.hasOlder, loadOlder: timeline.loadOlder, isLoadingOlder: timeline.isLoadingOlder, provider: descriptor?.id ?? null, now: now }), _jsx(ConversationComposer, { onSend: handleSend, isSending: participation.pendingCommands.has("reply") || settlingItemId !== null, disabledReason: composerDisabledReason, advisory: composerAdvisory })] }, `${selected.agentChannelId}:${selected.conversationKey}`))] }));
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=ConversationsWorkbench.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConversationsWorkbench.js","sourceRoot":"","sources":["../../src/conversation/ConversationsWorkbench.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { type ChannelConversation, type ConversationTimelineItem } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/conversation_io_pb";
|
|
2
|
+
import type { ChannelProviderId } from "../channel/providers.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pure render vocabulary for the conversation surface — the single place
|
|
5
|
+
* the wire enums become words and layout facts (the providerPresentation
|
|
6
|
+
* convention). No React, no I/O; every component and every test reads the
|
|
7
|
+
* same mapping.
|
|
8
|
+
*/
|
|
9
|
+
/** Who authored a timeline item, in render vocabulary. */
|
|
10
|
+
export type ConversationAuthorKind = "customer" | "agent" | "teammate" | "platform" | "unknown";
|
|
11
|
+
/** Map the wire author onto the render vocabulary. */
|
|
12
|
+
export declare function authorKindOf(item: ConversationTimelineItem): ConversationAuthorKind;
|
|
13
|
+
/** True for org-internal items (escalations, attention clears, notes). */
|
|
14
|
+
export declare function isInternalItem(item: ConversationTimelineItem): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The platform's own send attempt, in render vocabulary. `null` on items
|
|
17
|
+
* that carry no attempt (inbound messages, internal events).
|
|
18
|
+
*
|
|
19
|
+
* `"suppressed"` and `"failed"` are items the customer NEVER saw and
|
|
20
|
+
* `"pending"`/`"delivering"` are items the customer has not seen YET —
|
|
21
|
+
* a renderer that shows any of them as a plain sent bubble lies to staff.
|
|
22
|
+
*/
|
|
23
|
+
export type SendAttemptKind = "pending" | "delivering" | "delivered" | "failed" | "suppressed";
|
|
24
|
+
/** Map the wire delivery status onto the render vocabulary. */
|
|
25
|
+
export declare function sendAttemptOf(item: ConversationTimelineItem): SendAttemptKind | null;
|
|
26
|
+
/**
|
|
27
|
+
* The provider's receipt report, in render vocabulary. `null` until the
|
|
28
|
+
* provider reports (and always on items with no receipt axis).
|
|
29
|
+
*
|
|
30
|
+
* A SECOND axis beside {@link sendAttemptOf}, never collapsed into it
|
|
31
|
+
* (channel-conversations DD-004 D-d): "we handed it to WhatsApp" and
|
|
32
|
+
* "it reached the phone" are different facts — an item can be
|
|
33
|
+
* `delivered` + `receipt_failed`.
|
|
34
|
+
*/
|
|
35
|
+
export type ReceiptKind = "sent" | "delivered" | "read" | "failed";
|
|
36
|
+
/** Map the wire receipt state onto the render vocabulary. */
|
|
37
|
+
export declare function receiptOf(item: ConversationTimelineItem): ReceiptKind | null;
|
|
38
|
+
/**
|
|
39
|
+
* Placeholder copy for inbound items whose body the platform cannot
|
|
40
|
+
* render (`text` empty, `provider_message_type` names the kind). Media
|
|
41
|
+
* retrieval is a named non-goal of this surface — the placeholder tells
|
|
42
|
+
* staff something arrived and what shape it was.
|
|
43
|
+
*
|
|
44
|
+
* Returns `null` when the item needs no placeholder (it has text, or it
|
|
45
|
+
* is not a provider-typed inbound item — e.g. an attention-clear event,
|
|
46
|
+
* whose copy is the renderer's own vocabulary).
|
|
47
|
+
*/
|
|
48
|
+
export declare function inboundPlaceholderOf(item: ConversationTimelineItem): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* The list-row label for a conversation: the provider-reported display
|
|
51
|
+
* name when one exists, else a provider-aware fallback.
|
|
52
|
+
*
|
|
53
|
+
* The fallback is provider-aware because conversation keys are not
|
|
54
|
+
* uniformly meaningful: WhatsApp's key is the customer's phone number
|
|
55
|
+
* (a real label), while Slack's key is a thread timestamp like
|
|
56
|
+
* `1723012345.678900` (noise a human cannot use).
|
|
57
|
+
*/
|
|
58
|
+
export declare function conversationLabelOf(conversation: ChannelConversation, provider: ChannelProviderId | null): string;
|
|
59
|
+
/**
|
|
60
|
+
* The customer's reachable address, when the label hides it: the fact a
|
|
61
|
+
* staffer needs to call or message the customer outside the platform.
|
|
62
|
+
*
|
|
63
|
+
* Provider-aware like {@link conversationLabelOf}, because conversation
|
|
64
|
+
* keys are not uniformly addresses: WhatsApp's key IS the customer's
|
|
65
|
+
* phone number (the wa_id), and when a display name wins the label slot
|
|
66
|
+
* that number would otherwise appear nowhere on the surface. Slack's
|
|
67
|
+
* key is a thread timestamp — not a way to reach anyone — so there is
|
|
68
|
+
* nothing to show. Unknown providers show nothing rather than guess.
|
|
69
|
+
*
|
|
70
|
+
* Returns `null` when the label already shows the key (no display
|
|
71
|
+
* name), so the same fact never renders twice. The key returns
|
|
72
|
+
* verbatim — no fabricated `+` prefix: the wire value is the truth this
|
|
73
|
+
* module reports, and formatting belongs to hosts that know their
|
|
74
|
+
* display rules.
|
|
75
|
+
*/
|
|
76
|
+
export declare function conversationContactOf(conversation: ChannelConversation, provider: ChannelProviderId | null): string | null;
|
|
77
|
+
/** The service-window estimate, in render vocabulary. */
|
|
78
|
+
export type ServiceWindowState = "open" | "closed";
|
|
79
|
+
/**
|
|
80
|
+
* Estimate whether the provider's free-form reply window is open
|
|
81
|
+
* (channel-conversations DD-014 D-a). Provider-aware like
|
|
82
|
+
* {@link conversationLabelOf}: WhatsApp is the only windowed provider, so
|
|
83
|
+
* every other provider answers `null` — no window, no claim — as does a
|
|
84
|
+
* conversation whose customer has never written
|
|
85
|
+
* (`last_customer_message_at` unset; that conversation takes the
|
|
86
|
+
* composer's disabled branch instead).
|
|
87
|
+
*
|
|
88
|
+
* Client-derived on purpose, and NOT a divergence from DD-011 A-1's
|
|
89
|
+
* "the predicate is expressed once, in SQL" rule: the window is
|
|
90
|
+
* wall-clock-anchored — it flips with NO row change — so a
|
|
91
|
+
* server-computed boolean would be stale by construction between polls.
|
|
92
|
+
* Time-decaying presentation derived from wire instants is this
|
|
93
|
+
* surface's established class (relative timestamps, day grouping).
|
|
94
|
+
*
|
|
95
|
+
* Honesty caveat (DD-014 D-a): the anchor is the platform's receipt
|
|
96
|
+
* instant, minutes AFTER Meta's own clock started the window, so the
|
|
97
|
+
* estimate can claim "open" briefly after Meta closed it — which is why
|
|
98
|
+
* consumers warn and never block (D-b). For the same reason the exact
|
|
99
|
+
* 24-hour tie reads "closed": erring a moment early offsets an anchor
|
|
100
|
+
* that always errs late.
|
|
101
|
+
*/
|
|
102
|
+
export declare function serviceWindowOf(conversation: ChannelConversation, provider: ChannelProviderId | null, now: Date): ServiceWindowState | null;
|
|
103
|
+
/**
|
|
104
|
+
* How strongly the awaiting-reply indicator renders. Two strengths, not
|
|
105
|
+
* one, because "waiting" means different things per holder (DD-011 D-a).
|
|
106
|
+
*/
|
|
107
|
+
export type AwaitingIndicator = "strong" | "muted";
|
|
108
|
+
/**
|
|
109
|
+
* The awaiting-reply indicator for a conversation (channel-conversations
|
|
110
|
+
* DD-011 D-a): `"strong"` when a human holds it — the agent will not
|
|
111
|
+
* answer, so a person must (F-13's exact case) — and `"muted"` when the
|
|
112
|
+
* agent holds it: the agent is about to answer, and if its turn dies the
|
|
113
|
+
* conversation stays visibly waiting, which is the built-in recovery
|
|
114
|
+
* path (DD-011 D-b). `null` when the customer's last message has been
|
|
115
|
+
* answered (or the customer has not written).
|
|
116
|
+
*
|
|
117
|
+
* The fact itself is server-derived (`awaiting_reply` rides the wire as
|
|
118
|
+
* a boolean precisely so no client re-implements the NULL-and-compare
|
|
119
|
+
* rule); this helper only maps fact × holder onto render strength.
|
|
120
|
+
*/
|
|
121
|
+
export declare function awaitingIndicatorOf(conversation: ChannelConversation): AwaitingIndicator | null;
|
|
122
|
+
/**
|
|
123
|
+
* The timeline item id an accepted staff reply appears under: the
|
|
124
|
+
* outbound ledger's `ob:` namespace over the reply output's
|
|
125
|
+
* `outbound_message_id` — the contract
|
|
126
|
+
* `useConversationParticipation.reply` documents ("the item will
|
|
127
|
+
* appear on the timeline as `ob:<that id>`"). The workbench's
|
|
128
|
+
* post-send busy state watches for exactly this id (F-05).
|
|
129
|
+
*/
|
|
130
|
+
export declare function outboundItemIdOf(outboundMessageId: string): string;
|
|
131
|
+
/**
|
|
132
|
+
* Newest-first timeline order: `(at, item_id)` descending — the exact
|
|
133
|
+
* comparator the server pages with, so client-side merges can never
|
|
134
|
+
* disagree with server pages about order.
|
|
135
|
+
*/
|
|
136
|
+
export declare function compareTimelineItemsNewestFirst(a: ConversationTimelineItem, b: ConversationTimelineItem): number;
|
|
137
|
+
//# sourceMappingURL=conversationPresentation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversationPresentation.d.ts","sourceRoot":"","sources":["../../src/conversation/conversationPresentation.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,mBAAmB,EAIxB,KAAK,wBAAwB,EAC9B,MAAM,uEAAuE,CAAC;AAG/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE;;;;;GAKG;AAEH,0DAA0D;AAC1D,MAAM,MAAM,sBAAsB,GAC9B,UAAU,GACV,OAAO,GACP,UAAU,GACV,UAAU,GACV,SAAS,CAAC;AAEd,sDAAsD;AACtD,wBAAgB,YAAY,CAAC,IAAI,EAAE,wBAAwB,GAAG,sBAAsB,CAanF;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,IAAI,EAAE,wBAAwB,GAAG,OAAO,CAEtE;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GACvB,SAAS,GACT,YAAY,GACZ,WAAW,GACX,QAAQ,GACR,YAAY,CAAC;AAEjB,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,IAAI,EAAE,wBAAwB,GAAG,eAAe,GAAG,IAAI,CAepF;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEnE,6DAA6D;AAC7D,wBAAgB,SAAS,CAAC,IAAI,EAAE,wBAAwB,GAAG,WAAW,GAAG,IAAI,CAa5E;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,wBAAwB,GAAG,MAAM,GAAG,IAAI,CAwBlF;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,mBAAmB,EACjC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,GACjC,MAAM,CAIR;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,mBAAmB,EACjC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,GACjC,MAAM,GAAG,IAAI,CAIf;AAYD,yDAAyD;AACzD,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,eAAe,CAC7B,YAAY,EAAE,mBAAmB,EACjC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,EAClC,GAAG,EAAE,IAAI,GACR,kBAAkB,GAAG,IAAI,CAM3B;AAED;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEnD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,mBAAmB,GAChC,iBAAiB,GAAG,IAAI,CAK1B;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,CAAC,EAAE,wBAAwB,EAC3B,CAAC,EAAE,wBAAwB,GAC1B,MAAM,CAIR"}
|