@stigmer/react 3.6.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/billing/CreditLedgerTable.d.ts.map +1 -1
- package/billing/CreditLedgerTable.js +2 -7
- package/billing/CreditLedgerTable.js.map +1 -1
- 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 +6 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +2 -2
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +9 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +13 -2
- 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/execution/useCreateAgentExecution.d.ts +14 -0
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +5 -0
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/index.d.ts +6 -2
- package/index.d.ts.map +1 -1
- package/index.js +14 -5
- package/index.js.map +1 -1
- package/internal/Pagination.d.ts +15 -0
- package/internal/Pagination.d.ts.map +1 -0
- package/internal/Pagination.js +20 -0
- package/internal/Pagination.js.map +1 -0
- 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 +45 -1
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +49 -6
- package/models/ModelSelector.js.map +1 -1
- package/models/index.d.ts +2 -0
- package/models/index.d.ts.map +1 -1
- package/models/index.js +1 -0
- package/models/index.js.map +1 -1
- package/models/registry.d.ts +11 -0
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js +3 -0
- package/models/registry.js.map +1 -1
- package/models/service-tier.d.ts +27 -0
- package/models/service-tier.d.ts.map +1 -0
- package/models/service-tier.js +34 -0
- package/models/service-tier.js.map +1 -0
- package/package.json +4 -4
- package/schedule/ScheduleDetailView.d.ts +31 -8
- package/schedule/ScheduleDetailView.d.ts.map +1 -1
- package/schedule/ScheduleDetailView.js +441 -29
- package/schedule/ScheduleDetailView.js.map +1 -1
- package/schedule/ScheduleForm.d.ts +7 -4
- package/schedule/ScheduleForm.d.ts.map +1 -1
- package/schedule/ScheduleForm.js +94 -8
- package/schedule/ScheduleForm.js.map +1 -1
- package/schedule/ScheduleRunsTable.d.ts +72 -0
- package/schedule/ScheduleRunsTable.d.ts.map +1 -0
- package/schedule/ScheduleRunsTable.js +161 -0
- package/schedule/ScheduleRunsTable.js.map +1 -0
- package/schedule/index.d.ts +6 -0
- package/schedule/index.d.ts.map +1 -1
- package/schedule/index.js +3 -0
- package/schedule/index.js.map +1 -1
- package/schedule/useScheduleRuns.d.ts +44 -0
- package/schedule/useScheduleRuns.d.ts.map +1 -0
- package/schedule/useScheduleRuns.js +48 -0
- package/schedule/useScheduleRuns.js.map +1 -0
- package/schedule/useTriggerSchedule.d.ts +23 -10
- package/schedule/useTriggerSchedule.d.ts.map +1 -1
- package/schedule/useTriggerSchedule.js +28 -9
- package/schedule/useTriggerSchedule.js.map +1 -1
- package/schedule/useUpdateScheduleSpec.d.ts +49 -0
- package/schedule/useUpdateScheduleSpec.d.ts.map +1 -0
- package/schedule/useUpdateScheduleSpec.js +70 -0
- package/schedule/useUpdateScheduleSpec.js.map +1 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +1 -0
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionConversation.d.ts +11 -0
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +1 -0
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +1 -0
- package/session/useSessionPageFlow.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/billing/CreditLedgerTable.tsx +3 -42
- 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 +9 -0
- package/src/composer/SessionComposer.tsx +24 -1
- package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +157 -0
- 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/execution/useCreateAgentExecution.ts +18 -0
- package/src/index.ts +76 -4
- package/src/internal/Pagination.tsx +72 -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 +133 -4
- package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +256 -0
- package/src/models/__tests__/useModelRegistry.test.tsx +15 -0
- package/src/models/index.ts +2 -0
- package/src/models/registry.ts +17 -0
- package/src/models/service-tier.ts +49 -0
- package/src/schedule/ScheduleDetailView.tsx +1179 -111
- package/src/schedule/ScheduleForm.tsx +218 -7
- package/src/schedule/ScheduleRunsTable.tsx +424 -0
- package/src/schedule/__tests__/ScheduleDetailView.test.tsx +431 -8
- package/src/schedule/__tests__/scheduleCreation.test.tsx +197 -0
- package/src/schedule/__tests__/scheduleHooks.test.tsx +94 -7
- package/src/schedule/index.ts +12 -0
- package/src/schedule/useScheduleRuns.ts +93 -0
- package/src/schedule/useTriggerSchedule.ts +39 -14
- package/src/schedule/useUpdateScheduleSpec.ts +108 -0
- package/src/session/useNewSessionFlow.ts +1 -0
- package/src/session/useSessionConversation.ts +12 -0
- package/src/session/useSessionPageFlow.ts +1 -0
- 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/src/workflow/__tests__/inspector-forms.test.tsx +119 -2
- package/src/workflow/inspector/forms/AgentCallForm.tsx +260 -37
- package/src/workflow/inspector/tabs/RuntimeTab.tsx +72 -33
- package/src/workflow/starter-workflow-yaml.ts +2 -1
- package/styles.css +1 -1
- package/workflow/inspector/forms/AgentCallForm.d.ts +18 -2
- package/workflow/inspector/forms/AgentCallForm.d.ts.map +1 -1
- package/workflow/inspector/forms/AgentCallForm.js +87 -9
- package/workflow/inspector/forms/AgentCallForm.js.map +1 -1
- package/workflow/inspector/tabs/RuntimeTab.d.ts +2 -1
- package/workflow/inspector/tabs/RuntimeTab.d.ts.map +1 -1
- package/workflow/inspector/tabs/RuntimeTab.js +31 -5
- package/workflow/inspector/tabs/RuntimeTab.js.map +1 -1
- package/workflow/starter-workflow-yaml.d.ts +1 -1
- package/workflow/starter-workflow-yaml.d.ts.map +1 -1
- package/workflow/starter-workflow-yaml.js +2 -1
- package/workflow/starter-workflow-yaml.js.map +1 -1
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo, useCallback, useRef } from "react";
|
|
4
|
+
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
5
|
+
import { MessageSquare, TriangleAlert, User } from "lucide-react";
|
|
6
|
+
import { cn } from "@stigmer/theme";
|
|
7
|
+
import { getUserMessage } from "@stigmer/sdk";
|
|
8
|
+
import type { AgentChannel } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
|
|
9
|
+
import {
|
|
10
|
+
ChannelConversationListFilter,
|
|
11
|
+
ConversationControl,
|
|
12
|
+
type ChannelConversation,
|
|
13
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/conversation_io_pb";
|
|
14
|
+
import { formatRelativeTime } from "../activity/format-relative-time.js";
|
|
15
|
+
import { channelProviderOf } from "../channel/providers.js";
|
|
16
|
+
import { Button } from "../button/Button.js";
|
|
17
|
+
import { EmptyState } from "../empty-state/EmptyState.js";
|
|
18
|
+
import {
|
|
19
|
+
Tooltip,
|
|
20
|
+
TooltipContent,
|
|
21
|
+
TooltipProvider,
|
|
22
|
+
TooltipTrigger,
|
|
23
|
+
} from "../internal/tooltip.js";
|
|
24
|
+
import {
|
|
25
|
+
awaitingIndicatorOf,
|
|
26
|
+
conversationContactOf,
|
|
27
|
+
conversationLabelOf,
|
|
28
|
+
} from "./conversationPresentation.js";
|
|
29
|
+
|
|
30
|
+
/** The (channel, key) pair identifying one conversation. */
|
|
31
|
+
export interface ConversationIdentity {
|
|
32
|
+
readonly agentChannelId: string;
|
|
33
|
+
readonly conversationKey: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Props for {@link ConversationListPane}. */
|
|
37
|
+
export interface ConversationListPaneProps {
|
|
38
|
+
/** Conversations, newest activity first (from `useConversationList`). */
|
|
39
|
+
readonly conversations: readonly ChannelConversation[];
|
|
40
|
+
/** `true` only during the first load. */
|
|
41
|
+
readonly isLoading: boolean;
|
|
42
|
+
/** Error from the list read, or `null`. */
|
|
43
|
+
readonly error: Error | null;
|
|
44
|
+
/** `true` while more conversations exist beyond what is loaded. */
|
|
45
|
+
readonly hasMore: boolean;
|
|
46
|
+
/** Load the next page. */
|
|
47
|
+
readonly loadMore: () => void;
|
|
48
|
+
/** `true` while the next page is in flight. */
|
|
49
|
+
readonly isLoadingMore: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The org's channels the caller can view (from
|
|
52
|
+
* `useOrgAgentChannelList`) — the channel filter's options, each
|
|
53
|
+
* row's provider identity, and the honest empty-state distinction
|
|
54
|
+
* (no channel access vs no conversations yet).
|
|
55
|
+
*/
|
|
56
|
+
readonly channels: readonly AgentChannel[];
|
|
57
|
+
/** Current channel filter; empty string means all channels. */
|
|
58
|
+
readonly channelFilter: string;
|
|
59
|
+
/** Called when the user picks a channel filter. */
|
|
60
|
+
readonly onChannelFilterChange: (agentChannelId: string) => void;
|
|
61
|
+
/**
|
|
62
|
+
* The server-evaluated predicate filter currently applied to the list
|
|
63
|
+
* (DD-011 D-g — the same predicate the nav badge counts, so the
|
|
64
|
+
* filtered list always matches the number that opened it). Defaults
|
|
65
|
+
* to unspecified (all conversations).
|
|
66
|
+
*/
|
|
67
|
+
readonly filter?: ChannelConversationListFilter;
|
|
68
|
+
/**
|
|
69
|
+
* Called when the user picks a predicate filter. The control renders
|
|
70
|
+
* only when this is wired — a dead toggle over an unfiltered list
|
|
71
|
+
* would lie.
|
|
72
|
+
*/
|
|
73
|
+
readonly onFilterChange?: (filter: ChannelConversationListFilter) => void;
|
|
74
|
+
/** The open conversation, for row highlighting. `null` when none. */
|
|
75
|
+
readonly selected: ConversationIdentity | null;
|
|
76
|
+
/** Called when the user opens a conversation. */
|
|
77
|
+
readonly onSelect: (conversation: ChannelConversation) => void;
|
|
78
|
+
/** Frozen instant for deterministic hosts (tests, documentation tours). */
|
|
79
|
+
readonly now?: Date;
|
|
80
|
+
/** Additional classes for the pane container. */
|
|
81
|
+
readonly className?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The conversation inbox pane (channel-conversations T04): every
|
|
86
|
+
* conversation across the org's channels, newest activity first, with
|
|
87
|
+
* control, attention, and awaiting-reply indicators.
|
|
88
|
+
*
|
|
89
|
+
* Presentational — pair with `useConversationList` and
|
|
90
|
+
* `useOrgAgentChannelList`, or compose via `ConversationsWorkbench`.
|
|
91
|
+
* The wants-human filter is SERVER-evaluated (DD-011 D-g closed the API
|
|
92
|
+
* gap that T04 D1 cited): a client-side tab over one fetched page would
|
|
93
|
+
* silently lie across pages, so the pane only reports the choice and the
|
|
94
|
+
* list hook sends it to the server.
|
|
95
|
+
*/
|
|
96
|
+
export function ConversationListPane({
|
|
97
|
+
conversations,
|
|
98
|
+
isLoading,
|
|
99
|
+
error,
|
|
100
|
+
hasMore,
|
|
101
|
+
loadMore,
|
|
102
|
+
isLoadingMore,
|
|
103
|
+
channels,
|
|
104
|
+
channelFilter,
|
|
105
|
+
onChannelFilterChange,
|
|
106
|
+
filter = ChannelConversationListFilter.channel_conversation_list_filter_unspecified,
|
|
107
|
+
onFilterChange,
|
|
108
|
+
selected,
|
|
109
|
+
onSelect,
|
|
110
|
+
now,
|
|
111
|
+
className,
|
|
112
|
+
}: ConversationListPaneProps) {
|
|
113
|
+
const providerById = new Map(
|
|
114
|
+
channels.map((channel) => [
|
|
115
|
+
channel.metadata?.id ?? "",
|
|
116
|
+
channelProviderOf(channel.spec?.providerConfig?.case),
|
|
117
|
+
]),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
aria-label="Conversation list"
|
|
123
|
+
className={cn("flex min-h-0 flex-col", className)}
|
|
124
|
+
>
|
|
125
|
+
{(onFilterChange !== undefined || channels.length > 1) && (
|
|
126
|
+
<div className="flex flex-col gap-2 border-b border-border px-3 py-2">
|
|
127
|
+
{onFilterChange !== undefined && (
|
|
128
|
+
<ConversationFilterToggle value={filter} onChange={onFilterChange} />
|
|
129
|
+
)}
|
|
130
|
+
{channels.length > 1 && (
|
|
131
|
+
<>
|
|
132
|
+
<label className="sr-only" htmlFor="stgm-conversation-channel-filter">
|
|
133
|
+
Filter by channel
|
|
134
|
+
</label>
|
|
135
|
+
<select
|
|
136
|
+
id="stgm-conversation-channel-filter"
|
|
137
|
+
value={channelFilter}
|
|
138
|
+
onChange={(e) => onChannelFilterChange(e.target.value)}
|
|
139
|
+
className={cn(
|
|
140
|
+
"w-full rounded-md border border-border bg-background px-2 py-1.5",
|
|
141
|
+
"text-sm text-foreground",
|
|
142
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
143
|
+
)}
|
|
144
|
+
>
|
|
145
|
+
<option value="">All channels</option>
|
|
146
|
+
{channels.map((channel) => (
|
|
147
|
+
<option key={channel.metadata?.id} value={channel.metadata?.id}>
|
|
148
|
+
{channel.metadata?.name || channel.metadata?.slug}
|
|
149
|
+
</option>
|
|
150
|
+
))}
|
|
151
|
+
</select>
|
|
152
|
+
</>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
<div className="min-h-0 flex-1 overflow-y-auto p-2">
|
|
158
|
+
{isLoading ? (
|
|
159
|
+
<ListSkeleton />
|
|
160
|
+
) : error ? (
|
|
161
|
+
<p className="px-2 py-6 text-center text-sm text-destructive">
|
|
162
|
+
{getUserMessage(error)}
|
|
163
|
+
</p>
|
|
164
|
+
) : conversations.length === 0 ? (
|
|
165
|
+
channels.length === 0 ? (
|
|
166
|
+
<EmptyState
|
|
167
|
+
variant="first-use"
|
|
168
|
+
icon={<MessageSquare className="size-8" />}
|
|
169
|
+
title="No channels to watch"
|
|
170
|
+
description="Conversations arrive through an agent's channels. Connect a channel — or ask a channel owner for access — and customer conversations appear here."
|
|
171
|
+
/>
|
|
172
|
+
) : filter === ChannelConversationListFilter.filter_wants_human ? (
|
|
173
|
+
// Its own empty state on purpose: under the filter, "No
|
|
174
|
+
// conversations yet — when a customer messages…" would lie
|
|
175
|
+
// to an org with a full inbox and nothing needing a person.
|
|
176
|
+
<EmptyState
|
|
177
|
+
variant="first-use"
|
|
178
|
+
icon={<MessageSquare className="size-8" />}
|
|
179
|
+
title="Nothing needs a human right now"
|
|
180
|
+
description="Conversations appear here when the agent asks for attention, or when a human-held conversation has a customer message awaiting a reply."
|
|
181
|
+
/>
|
|
182
|
+
) : (
|
|
183
|
+
<EmptyState
|
|
184
|
+
variant="first-use"
|
|
185
|
+
icon={<MessageSquare className="size-8" />}
|
|
186
|
+
title="No conversations yet"
|
|
187
|
+
description="When a customer messages one of your channels, their conversation appears here."
|
|
188
|
+
/>
|
|
189
|
+
)
|
|
190
|
+
) : (
|
|
191
|
+
<>
|
|
192
|
+
{/* One context-only provider arms every row's attention
|
|
193
|
+
tooltip (the WorkspaceSidebar recents-list precedent). */}
|
|
194
|
+
<TooltipProvider>
|
|
195
|
+
<ul className="space-y-0.5">
|
|
196
|
+
{conversations.map((conversation) => (
|
|
197
|
+
<ConversationRow
|
|
198
|
+
key={`${conversation.agentChannelId}:${conversation.conversationKey}`}
|
|
199
|
+
conversation={conversation}
|
|
200
|
+
label={conversationLabelOf(
|
|
201
|
+
conversation,
|
|
202
|
+
providerById.get(conversation.agentChannelId)?.id ?? null,
|
|
203
|
+
)}
|
|
204
|
+
contact={conversationContactOf(
|
|
205
|
+
conversation,
|
|
206
|
+
providerById.get(conversation.agentChannelId)?.id ?? null,
|
|
207
|
+
)}
|
|
208
|
+
isSelected={
|
|
209
|
+
selected?.agentChannelId === conversation.agentChannelId &&
|
|
210
|
+
selected?.conversationKey === conversation.conversationKey
|
|
211
|
+
}
|
|
212
|
+
onSelect={onSelect}
|
|
213
|
+
now={now}
|
|
214
|
+
/>
|
|
215
|
+
))}
|
|
216
|
+
</ul>
|
|
217
|
+
</TooltipProvider>
|
|
218
|
+
{hasMore && (
|
|
219
|
+
<div className="flex justify-center py-2">
|
|
220
|
+
<Button
|
|
221
|
+
variant="ghost"
|
|
222
|
+
size="xs"
|
|
223
|
+
onClick={loadMore}
|
|
224
|
+
disabled={isLoadingMore}
|
|
225
|
+
>
|
|
226
|
+
{isLoadingMore ? "Loading…" : "Show more"}
|
|
227
|
+
</Button>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const FILTER_OPTIONS: readonly {
|
|
238
|
+
readonly value: ChannelConversationListFilter;
|
|
239
|
+
readonly label: string;
|
|
240
|
+
readonly ariaLabel: string;
|
|
241
|
+
}[] = [
|
|
242
|
+
{
|
|
243
|
+
value: ChannelConversationListFilter.channel_conversation_list_filter_unspecified,
|
|
244
|
+
label: "All",
|
|
245
|
+
ariaLabel: "All conversations",
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
value: ChannelConversationListFilter.filter_wants_human,
|
|
249
|
+
label: "Needs human",
|
|
250
|
+
ariaLabel: "Conversations where a human action is wanted",
|
|
251
|
+
},
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Segmented control for the server-evaluated list filter. Renders as a
|
|
256
|
+
* WAI-ARIA Radio Group with roving tabindex and arrow-key navigation —
|
|
257
|
+
* the `ScopeToggle` pattern (its second sighting; extract a shared
|
|
258
|
+
* primitive if a third segmented control appears).
|
|
259
|
+
*
|
|
260
|
+
* "Needs human" is exactly the nav badge's predicate (DD-011 D-f/D-g),
|
|
261
|
+
* so the number on the badge and the list this control opens can never
|
|
262
|
+
* disagree.
|
|
263
|
+
*/
|
|
264
|
+
function ConversationFilterToggle({
|
|
265
|
+
value,
|
|
266
|
+
onChange,
|
|
267
|
+
}: {
|
|
268
|
+
readonly value: ChannelConversationListFilter;
|
|
269
|
+
readonly onChange: (filter: ChannelConversationListFilter) => void;
|
|
270
|
+
}) {
|
|
271
|
+
const optionRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
272
|
+
|
|
273
|
+
const handleSelect = useCallback(
|
|
274
|
+
(next: ChannelConversationListFilter) => {
|
|
275
|
+
if (next !== value) onChange(next);
|
|
276
|
+
},
|
|
277
|
+
[value, onChange],
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const handleKeyDown = useCallback(
|
|
281
|
+
(e: React.KeyboardEvent<HTMLButtonElement>, index: number) => {
|
|
282
|
+
let nextIndex: number | null = null;
|
|
283
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
nextIndex = (index + 1) % FILTER_OPTIONS.length;
|
|
286
|
+
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
nextIndex = (index - 1 + FILTER_OPTIONS.length) % FILTER_OPTIONS.length;
|
|
289
|
+
}
|
|
290
|
+
if (nextIndex !== null) {
|
|
291
|
+
optionRefs.current[nextIndex]?.focus();
|
|
292
|
+
handleSelect(FILTER_OPTIONS[nextIndex].value);
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
[handleSelect],
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<div
|
|
300
|
+
role="radiogroup"
|
|
301
|
+
aria-label="Conversation filter"
|
|
302
|
+
className="inline-flex self-start rounded-md bg-muted p-0.5"
|
|
303
|
+
>
|
|
304
|
+
{FILTER_OPTIONS.map((option, index) => {
|
|
305
|
+
const isSelected = value === option.value;
|
|
306
|
+
return (
|
|
307
|
+
<button
|
|
308
|
+
key={option.value}
|
|
309
|
+
ref={(el) => {
|
|
310
|
+
optionRefs.current[index] = el;
|
|
311
|
+
}}
|
|
312
|
+
type="button"
|
|
313
|
+
role="radio"
|
|
314
|
+
aria-checked={isSelected}
|
|
315
|
+
aria-label={option.ariaLabel}
|
|
316
|
+
tabIndex={isSelected ? 0 : -1}
|
|
317
|
+
onClick={() => handleSelect(option.value)}
|
|
318
|
+
onKeyDown={(e) => handleKeyDown(e, index)}
|
|
319
|
+
className={cn(
|
|
320
|
+
"inline-flex cursor-pointer items-center rounded-sm px-2 py-1 text-xs font-medium transition-colors",
|
|
321
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
322
|
+
isSelected
|
|
323
|
+
? "bg-background text-foreground shadow-sm"
|
|
324
|
+
: "text-muted-foreground hover:text-foreground",
|
|
325
|
+
)}
|
|
326
|
+
>
|
|
327
|
+
{option.label}
|
|
328
|
+
</button>
|
|
329
|
+
);
|
|
330
|
+
})}
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const ConversationRow = memo(function ConversationRow({
|
|
336
|
+
conversation,
|
|
337
|
+
label,
|
|
338
|
+
contact,
|
|
339
|
+
isSelected,
|
|
340
|
+
onSelect,
|
|
341
|
+
now,
|
|
342
|
+
}: {
|
|
343
|
+
readonly conversation: ChannelConversation;
|
|
344
|
+
readonly label: string;
|
|
345
|
+
/**
|
|
346
|
+
* The customer's reachable address when the label hides it
|
|
347
|
+
* (`conversationContactOf`) — `null` when the label already shows it.
|
|
348
|
+
*/
|
|
349
|
+
readonly contact: string | null;
|
|
350
|
+
readonly isSelected: boolean;
|
|
351
|
+
readonly onSelect: (conversation: ChannelConversation) => void;
|
|
352
|
+
readonly now?: Date;
|
|
353
|
+
}) {
|
|
354
|
+
const humanHeld = conversation.control === ConversationControl.control_human;
|
|
355
|
+
const awaiting = awaitingIndicatorOf(conversation);
|
|
356
|
+
const lastActivity = conversation.lastActivityAt
|
|
357
|
+
? timestampDate(conversation.lastActivityAt)
|
|
358
|
+
: null;
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<li>
|
|
362
|
+
<button
|
|
363
|
+
type="button"
|
|
364
|
+
onClick={() => onSelect(conversation)}
|
|
365
|
+
aria-current={isSelected ? "true" : undefined}
|
|
366
|
+
className={cn(
|
|
367
|
+
"flex w-full items-center gap-2.5 rounded-md px-2.5 py-2 text-left",
|
|
368
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
369
|
+
isSelected ? "bg-primary-subtle" : "hover:bg-accent",
|
|
370
|
+
)}
|
|
371
|
+
>
|
|
372
|
+
<div className="min-w-0 flex-1">
|
|
373
|
+
<div className="flex items-center gap-1.5">
|
|
374
|
+
{/* No truncation title on the label (F-18): the full name
|
|
375
|
+
renders in the open conversation's header, so a native
|
|
376
|
+
tooltip here added noise without adding reach. */}
|
|
377
|
+
<p className="truncate text-sm font-medium text-foreground">{label}</p>
|
|
378
|
+
{conversation.needsAttention && (
|
|
379
|
+
// The reason rides the house tooltip (F-18). The trigger
|
|
380
|
+
// renders as a span — it sits INSIDE the row button, so a
|
|
381
|
+
// default (button) trigger would nest buttons — and stays
|
|
382
|
+
// out of the tab order; the sr-only text remains the
|
|
383
|
+
// accessible name. The full reason is also always visible
|
|
384
|
+
// in the open conversation's attention banner.
|
|
385
|
+
<Tooltip>
|
|
386
|
+
<TooltipTrigger render={<span className="shrink-0" />}>
|
|
387
|
+
<TriangleAlert
|
|
388
|
+
aria-hidden="true"
|
|
389
|
+
className="size-3.5 text-destructive"
|
|
390
|
+
/>
|
|
391
|
+
<span className="sr-only">
|
|
392
|
+
{conversation.attentionReason
|
|
393
|
+
? `Needs attention: ${conversation.attentionReason}`
|
|
394
|
+
: "Needs attention"}
|
|
395
|
+
</span>
|
|
396
|
+
</TooltipTrigger>
|
|
397
|
+
<TooltipContent side="top">
|
|
398
|
+
{conversation.attentionReason || "Needs attention"}
|
|
399
|
+
</TooltipContent>
|
|
400
|
+
</Tooltip>
|
|
401
|
+
)}
|
|
402
|
+
</div>
|
|
403
|
+
{contact !== null && (
|
|
404
|
+
<p className="mt-0.5 truncate text-xs text-muted-foreground-faint">
|
|
405
|
+
{contact}
|
|
406
|
+
</p>
|
|
407
|
+
)}
|
|
408
|
+
{humanHeld && (
|
|
409
|
+
<p className="mt-0.5 flex items-center gap-1 text-xs text-muted-foreground">
|
|
410
|
+
<User aria-hidden="true" className="size-3" />
|
|
411
|
+
Human has the conversation
|
|
412
|
+
</p>
|
|
413
|
+
)}
|
|
414
|
+
</div>
|
|
415
|
+
{(lastActivity !== null || awaiting !== null) && (
|
|
416
|
+
<span className="flex shrink-0 flex-col items-end gap-1">
|
|
417
|
+
{lastActivity && (
|
|
418
|
+
<span className="text-xs text-muted-foreground-faint">
|
|
419
|
+
{formatRelativeTime(lastActivity, now)}
|
|
420
|
+
</span>
|
|
421
|
+
)}
|
|
422
|
+
{awaiting && (
|
|
423
|
+
// The WhatsApp inbox convention: an awaiting conversation
|
|
424
|
+
// carries a dot under its timestamp. Strength maps the
|
|
425
|
+
// holder (DD-011 D-a): strong when a human holds it — the
|
|
426
|
+
// agent will not answer, a person must — muted when the
|
|
427
|
+
// agent does. No tooltip and no tab stop (the F-18
|
|
428
|
+
// discipline); the sr-only text is the accessible meaning.
|
|
429
|
+
<span className="flex">
|
|
430
|
+
<span
|
|
431
|
+
aria-hidden="true"
|
|
432
|
+
className={cn(
|
|
433
|
+
"size-2 rounded-full",
|
|
434
|
+
awaiting === "strong" ? "bg-primary" : "bg-muted-foreground",
|
|
435
|
+
)}
|
|
436
|
+
/>
|
|
437
|
+
<span className="sr-only">
|
|
438
|
+
{awaiting === "strong"
|
|
439
|
+
? "Customer awaiting reply — the conversation is human-held"
|
|
440
|
+
: "Customer awaiting reply"}
|
|
441
|
+
</span>
|
|
442
|
+
</span>
|
|
443
|
+
)}
|
|
444
|
+
</span>
|
|
445
|
+
)}
|
|
446
|
+
</button>
|
|
447
|
+
</li>
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
function ListSkeleton() {
|
|
452
|
+
return (
|
|
453
|
+
<div className="space-y-1 p-1" aria-hidden="true">
|
|
454
|
+
{[1, 2, 3, 4].map((i) => (
|
|
455
|
+
<div key={i} className="h-11 animate-pulse rounded-md bg-muted-faint" />
|
|
456
|
+
))}
|
|
457
|
+
</div>
|
|
458
|
+
);
|
|
459
|
+
}
|