@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,156 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useId, useState } from "react";
|
|
4
|
+
import { Send, TriangleAlert } from "lucide-react";
|
|
5
|
+
import { cn } from "@stigmer/theme";
|
|
6
|
+
import { getUserMessage } from "@stigmer/sdk";
|
|
7
|
+
import { ChannelSendOutcome } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
|
|
8
|
+
import type { SendChannelMessageOutput } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
|
|
9
|
+
import { Button } from "../button/Button.js";
|
|
10
|
+
import { SpinnerIcon } from "../composer/icons.js";
|
|
11
|
+
import { useComposer } from "../composer/useComposer.js";
|
|
12
|
+
|
|
13
|
+
/** Props for {@link ConversationComposer}. */
|
|
14
|
+
export interface ConversationComposerProps {
|
|
15
|
+
/** Send a staff reply (from `useConversationParticipation().reply`). */
|
|
16
|
+
readonly onSend: (text: string) => Promise<SendChannelMessageOutput>;
|
|
17
|
+
/** `true` while a reply is in flight. */
|
|
18
|
+
readonly isSending: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Why replying is unavailable, when it is — rendered in place of the
|
|
21
|
+
* input (a senderless provider, or a conversation the customer has
|
|
22
|
+
* not started yet). `null` enables the composer.
|
|
23
|
+
*/
|
|
24
|
+
readonly disabledReason: string | null;
|
|
25
|
+
/**
|
|
26
|
+
* A pre-send forecast that ANNOTATES the enabled input — the
|
|
27
|
+
* `disabledReason` pattern's sibling with the opposite contract
|
|
28
|
+
* (channel-conversations DD-014 D-e): a closed service window is a
|
|
29
|
+
* forecast, not a structural block, so the input stays usable and the
|
|
30
|
+
* send engines remain the authority. Rendered above the input and
|
|
31
|
+
* associated via `aria-describedby` (never a live region: the
|
|
32
|
+
* advisory is state a reader meets on open, not an event). Compute it
|
|
33
|
+
* with `serviceWindowOf`; `null` or omitted renders nothing.
|
|
34
|
+
*/
|
|
35
|
+
readonly advisory?: string | null;
|
|
36
|
+
/** Additional classes for the composer container. */
|
|
37
|
+
readonly className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The staff reply composer. Sending is an implicit takeover — the agent
|
|
42
|
+
* goes quiet the moment a staff reply lands, before the send itself —
|
|
43
|
+
* so this composer is only ever rendered alongside the control banner
|
|
44
|
+
* that shows that state change.
|
|
45
|
+
*
|
|
46
|
+
* Outcome honesty: `refused` keeps the draft (the words never left) and
|
|
47
|
+
* renders the server's refusal detail verbatim; `queued` clears it and
|
|
48
|
+
* says the platform is retrying; a thrown failure keeps the draft and
|
|
49
|
+
* renders the error. The timeline shows the delivered item via its own
|
|
50
|
+
* read — this component never fabricates one.
|
|
51
|
+
*/
|
|
52
|
+
export function ConversationComposer({
|
|
53
|
+
onSend,
|
|
54
|
+
isSending,
|
|
55
|
+
disabledReason,
|
|
56
|
+
advisory,
|
|
57
|
+
className,
|
|
58
|
+
}: ConversationComposerProps) {
|
|
59
|
+
const advisoryId = useId();
|
|
60
|
+
const hasAdvisory = advisory != null && advisory !== "";
|
|
61
|
+
const [notice, setNotice] = useState<{
|
|
62
|
+
readonly kind: "refused" | "queued" | "error";
|
|
63
|
+
readonly text: string;
|
|
64
|
+
} | null>(null);
|
|
65
|
+
|
|
66
|
+
const composer = useComposer({
|
|
67
|
+
onSubmit: (message) => {
|
|
68
|
+
setNotice(null);
|
|
69
|
+
onSend(message).then(
|
|
70
|
+
(output) => {
|
|
71
|
+
if (output.outcome === ChannelSendOutcome.refused) {
|
|
72
|
+
// The words never left; keep them so the user can adjust.
|
|
73
|
+
composer.setMessage(message);
|
|
74
|
+
setNotice({
|
|
75
|
+
kind: "refused",
|
|
76
|
+
text: output.detail || "The provider refused this message.",
|
|
77
|
+
});
|
|
78
|
+
} else if (output.outcome === ChannelSendOutcome.queued) {
|
|
79
|
+
setNotice({
|
|
80
|
+
kind: "queued",
|
|
81
|
+
text: "Delivery hit a transient issue — the platform is retrying in the background.",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
(err) => {
|
|
86
|
+
composer.setMessage(message);
|
|
87
|
+
setNotice({ kind: "error", text: getUserMessage(err) });
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
disabled: disabledReason !== null || isSending,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (disabledReason !== null) {
|
|
95
|
+
return (
|
|
96
|
+
<div className={cn("border-t border-border px-4 py-3", className)}>
|
|
97
|
+
<p className="text-center text-xs text-muted-foreground">{disabledReason}</p>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className={cn("border-t border-border px-4 py-3", className)}>
|
|
104
|
+
{/* The persistent forecast sits above the transient send-outcome
|
|
105
|
+
notice: the notice is the more recent event and belongs closest
|
|
106
|
+
to the input it reacts to. */}
|
|
107
|
+
{hasAdvisory && (
|
|
108
|
+
<p
|
|
109
|
+
id={advisoryId}
|
|
110
|
+
className="mb-2 flex items-start gap-1.5 rounded-md bg-status-degraded-subtle px-2.5 py-1.5 text-xs text-status-degraded"
|
|
111
|
+
>
|
|
112
|
+
<TriangleAlert aria-hidden="true" className="mt-0.5 size-3.5 shrink-0" />
|
|
113
|
+
<span className="min-w-0">{advisory}</span>
|
|
114
|
+
</p>
|
|
115
|
+
)}
|
|
116
|
+
{notice && (
|
|
117
|
+
<p
|
|
118
|
+
className={cn(
|
|
119
|
+
"mb-2 text-xs",
|
|
120
|
+
notice.kind === "queued" ? "text-muted-foreground" : "text-destructive",
|
|
121
|
+
)}
|
|
122
|
+
role="status"
|
|
123
|
+
>
|
|
124
|
+
{notice.text}
|
|
125
|
+
</p>
|
|
126
|
+
)}
|
|
127
|
+
<div className="flex items-end gap-2">
|
|
128
|
+
<textarea
|
|
129
|
+
{...composer.textareaProps}
|
|
130
|
+
rows={1}
|
|
131
|
+
placeholder="Reply as your business…"
|
|
132
|
+
aria-label="Reply to the customer"
|
|
133
|
+
aria-describedby={hasAdvisory ? advisoryId : undefined}
|
|
134
|
+
className={cn(
|
|
135
|
+
"min-h-9 flex-1 resize-none rounded-md border border-border bg-background",
|
|
136
|
+
"px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground-faint",
|
|
137
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
138
|
+
)}
|
|
139
|
+
/>
|
|
140
|
+
<Button
|
|
141
|
+
variant="primary"
|
|
142
|
+
size="sm"
|
|
143
|
+
onClick={composer.submit}
|
|
144
|
+
disabled={!composer.canSubmit || isSending}
|
|
145
|
+
aria-label="Send reply"
|
|
146
|
+
>
|
|
147
|
+
{/* The house in-flight glyph (the session composer's pattern):
|
|
148
|
+
the spinner replaces the Send icon so the button itself
|
|
149
|
+
reports progress, not just its label. */}
|
|
150
|
+
{isSending ? <SpinnerIcon /> : <Send aria-hidden="true" className="size-4" />}
|
|
151
|
+
{isSending ? "Sending…" : "Send"}
|
|
152
|
+
</Button>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Bot, User } from "lucide-react";
|
|
5
|
+
import { cn } from "@stigmer/theme";
|
|
6
|
+
import { getUserMessage } from "@stigmer/sdk";
|
|
7
|
+
import {
|
|
8
|
+
ConversationControl,
|
|
9
|
+
type ChannelConversation,
|
|
10
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/conversation_io_pb";
|
|
11
|
+
import { Button } from "../button/Button.js";
|
|
12
|
+
import type { UseConversationParticipationReturn } from "./useConversationParticipation.js";
|
|
13
|
+
|
|
14
|
+
/** Props for {@link ConversationControlBanner}. */
|
|
15
|
+
export interface ConversationControlBannerProps {
|
|
16
|
+
/** The conversation's fresh participation state. */
|
|
17
|
+
readonly conversation: ChannelConversation;
|
|
18
|
+
/** The participation commands (from `useConversationParticipation`). */
|
|
19
|
+
readonly participation: UseConversationParticipationReturn;
|
|
20
|
+
/**
|
|
21
|
+
* `true` when the customer's last message has not been answered —
|
|
22
|
+
* the conversation row's server-derived `awaiting_reply` fact, which
|
|
23
|
+
* is bounce-aware: a staff reply the provider later failed does not
|
|
24
|
+
* count as an answer (channel-conversations F-28/DD-015). Arms the
|
|
25
|
+
* handback confirm guard (DD-007 D-e): handing back runs NO turn, so
|
|
26
|
+
* the agent stays quiet until the customer next speaks, and the
|
|
27
|
+
* unanswered state must be unmissable. Never derive this from
|
|
28
|
+
* timeline authorship — the send attempt's presence on the timeline
|
|
29
|
+
* says nothing about whether the customer received it.
|
|
30
|
+
*/
|
|
31
|
+
readonly unansweredCustomer: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Whether the channel's provider has a staff send lane. Senderless
|
|
34
|
+
* providers disable takeover with the reason — a takeover there would
|
|
35
|
+
* silence the agent while no human reply could reach the customer,
|
|
36
|
+
* which is exactly what the server refuses.
|
|
37
|
+
*/
|
|
38
|
+
readonly supportsStaffReplies: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* The signed-in staff member's identity account id, when the host
|
|
41
|
+
* knows it — lets the banner attribute a human hold: "You have this
|
|
42
|
+
* conversation" when it matches, "A teammate has this conversation"
|
|
43
|
+
* when it differs. When omitted the banner only states that a human
|
|
44
|
+
* holds it — the holder may be the viewer themself, so "a teammate"
|
|
45
|
+
* would be a guess. Optional: the SDK never assumes a host's auth
|
|
46
|
+
* state.
|
|
47
|
+
*/
|
|
48
|
+
readonly currentIdentityAccountId?: string;
|
|
49
|
+
/** Additional classes for the banner container. */
|
|
50
|
+
readonly className?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Who may speak to the customer, and the controls that move it: take
|
|
55
|
+
* over (the agent goes quiet) and hand back (the agent resumes, with
|
|
56
|
+
* the context it missed injected on its next turn).
|
|
57
|
+
*
|
|
58
|
+
* Truth rules: state renders from the conversation row the server
|
|
59
|
+
* answered — a takeover that lost its race renders the WINNER's hold,
|
|
60
|
+
* because that is what happened. Failures render verbatim beside the
|
|
61
|
+
* control that caused them.
|
|
62
|
+
*/
|
|
63
|
+
export function ConversationControlBanner({
|
|
64
|
+
conversation,
|
|
65
|
+
participation,
|
|
66
|
+
unansweredCustomer,
|
|
67
|
+
supportsStaffReplies,
|
|
68
|
+
currentIdentityAccountId,
|
|
69
|
+
className,
|
|
70
|
+
}: ConversationControlBannerProps) {
|
|
71
|
+
const [confirmingHandBack, setConfirmingHandBack] = useState(false);
|
|
72
|
+
|
|
73
|
+
const humanHeld = conversation.control === ConversationControl.control_human;
|
|
74
|
+
const heldByMe =
|
|
75
|
+
humanHeld &&
|
|
76
|
+
currentIdentityAccountId !== undefined &&
|
|
77
|
+
conversation.controlledBy === currentIdentityAccountId;
|
|
78
|
+
|
|
79
|
+
const { takeOver, handBack, pendingCommands, commandErrors } = participation;
|
|
80
|
+
const error = commandErrors.get("takeOver") ?? commandErrors.get("handBack");
|
|
81
|
+
|
|
82
|
+
const requestHandBack = () => {
|
|
83
|
+
if (unansweredCustomer) {
|
|
84
|
+
setConfirmingHandBack(true);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
void handBack().catch(() => {
|
|
88
|
+
// Recorded in commandErrors and rendered below.
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
className={cn(
|
|
95
|
+
"border-b border-border px-4 py-2",
|
|
96
|
+
humanHeld ? "bg-primary-subtle" : "bg-muted-faint",
|
|
97
|
+
className,
|
|
98
|
+
)}
|
|
99
|
+
>
|
|
100
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
101
|
+
<p className="flex items-center gap-1.5 text-sm text-foreground">
|
|
102
|
+
{humanHeld ? (
|
|
103
|
+
<>
|
|
104
|
+
<User aria-hidden="true" className="size-4 text-muted-foreground" />
|
|
105
|
+
{heldByMe
|
|
106
|
+
? "You have this conversation — the agent is quiet until you hand it back."
|
|
107
|
+
: currentIdentityAccountId !== undefined
|
|
108
|
+
? "A teammate has this conversation — the agent is quiet until handback."
|
|
109
|
+
: // Without the host's identity the holder may be the
|
|
110
|
+
// viewer themself — claiming "a teammate" would be a
|
|
111
|
+
// guess. State only what the row proves: a human
|
|
112
|
+
// holds it (channel-conversations F-01).
|
|
113
|
+
"This conversation is with a human — the agent is quiet until handback."}
|
|
114
|
+
</>
|
|
115
|
+
) : (
|
|
116
|
+
<>
|
|
117
|
+
<Bot aria-hidden="true" className="size-4 text-muted-foreground" />
|
|
118
|
+
The agent is serving this conversation.
|
|
119
|
+
</>
|
|
120
|
+
)}
|
|
121
|
+
</p>
|
|
122
|
+
|
|
123
|
+
{confirmingHandBack ? (
|
|
124
|
+
<div className="flex items-center gap-2">
|
|
125
|
+
<span className="text-xs text-destructive">
|
|
126
|
+
The customer's last message has no reply — the agent stays
|
|
127
|
+
quiet until they write again.
|
|
128
|
+
</span>
|
|
129
|
+
<Button
|
|
130
|
+
variant="outline"
|
|
131
|
+
size="xs"
|
|
132
|
+
onClick={() => {
|
|
133
|
+
setConfirmingHandBack(false);
|
|
134
|
+
void handBack().catch(() => {
|
|
135
|
+
// Recorded in commandErrors and rendered below.
|
|
136
|
+
});
|
|
137
|
+
}}
|
|
138
|
+
disabled={pendingCommands.has("handBack")}
|
|
139
|
+
>
|
|
140
|
+
Hand back anyway
|
|
141
|
+
</Button>
|
|
142
|
+
<Button
|
|
143
|
+
variant="primary"
|
|
144
|
+
size="xs"
|
|
145
|
+
onClick={() => setConfirmingHandBack(false)}
|
|
146
|
+
>
|
|
147
|
+
Keep the conversation
|
|
148
|
+
</Button>
|
|
149
|
+
</div>
|
|
150
|
+
) : humanHeld ? (
|
|
151
|
+
<Button
|
|
152
|
+
variant="outline"
|
|
153
|
+
size="xs"
|
|
154
|
+
onClick={requestHandBack}
|
|
155
|
+
disabled={pendingCommands.has("handBack")}
|
|
156
|
+
>
|
|
157
|
+
{pendingCommands.has("handBack") ? "Handing back…" : "Hand back to agent"}
|
|
158
|
+
</Button>
|
|
159
|
+
) : (
|
|
160
|
+
<div className="flex items-center gap-2">
|
|
161
|
+
{!supportsStaffReplies && (
|
|
162
|
+
// Visible text, never a native title (F-18): a disabled
|
|
163
|
+
// Button swallows hover (disabled:pointer-events-none) and
|
|
164
|
+
// leaves the tab order, so a title on it is unreachable by
|
|
165
|
+
// every input method. Kept short — the composer's own
|
|
166
|
+
// notice already explains the missing staff reply lane.
|
|
167
|
+
<span className="text-xs text-muted-foreground">
|
|
168
|
+
This channel's provider has no staff send lane yet — a
|
|
169
|
+
takeover would silence the agent with no way to reply.
|
|
170
|
+
</span>
|
|
171
|
+
)}
|
|
172
|
+
<Button
|
|
173
|
+
variant="outline"
|
|
174
|
+
size="xs"
|
|
175
|
+
onClick={() =>
|
|
176
|
+
void takeOver().catch(() => {
|
|
177
|
+
// Recorded in commandErrors and rendered below.
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
disabled={!supportsStaffReplies || pendingCommands.has("takeOver")}
|
|
181
|
+
>
|
|
182
|
+
{pendingCommands.has("takeOver") ? "Taking over…" : "Take over"}
|
|
183
|
+
</Button>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{error && (
|
|
189
|
+
<p className="mt-1 text-xs text-destructive">{getUserMessage(error)}</p>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|