@stigmer/react 3.12.4 → 3.12.6
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/agent/AgentDetailView.js +2 -2
- package/agent/AgentDetailView.js.map +1 -1
- package/agent/index.d.ts +0 -1
- package/agent/index.d.ts.map +1 -1
- package/agent/index.js +0 -1
- package/agent/index.js.map +1 -1
- package/agent/useUpdateAgent.d.ts +3 -3
- package/agent/useUpdateAgent.js +3 -3
- package/agent-instance/AgentInstanceDetailPanel.d.ts.map +1 -1
- package/agent-instance/AgentInstanceDetailPanel.js +5 -7
- package/agent-instance/AgentInstanceDetailPanel.js.map +1 -1
- package/agent-instance/useUpdateAgentInstance.d.ts +1 -1
- package/agent-instance/useUpdateAgentInstance.js +1 -1
- package/attachment/AttachmentChipList.js +16 -6
- package/attachment/AttachmentChipList.js.map +1 -1
- package/attachment/index.d.ts +1 -1
- package/attachment/index.d.ts.map +1 -1
- package/attachment/index.js.map +1 -1
- package/attachment/useAttachments.d.ts +51 -8
- package/attachment/useAttachments.d.ts.map +1 -1
- package/attachment/useAttachments.js +74 -3
- package/attachment/useAttachments.js.map +1 -1
- package/channel/AgentChannelsPanel.d.ts.map +1 -1
- package/channel/AgentChannelsPanel.js +10 -5
- package/channel/AgentChannelsPanel.js.map +1 -1
- package/channel/ChannelRunConfigDialog.d.ts +51 -0
- package/channel/ChannelRunConfigDialog.d.ts.map +1 -0
- package/channel/ChannelRunConfigDialog.js +121 -0
- package/channel/ChannelRunConfigDialog.js.map +1 -0
- package/channel/index.d.ts +2 -0
- package/channel/index.d.ts.map +1 -1
- package/channel/index.js +1 -0
- package/channel/index.js.map +1 -1
- package/channel/useSaveAgentChannel.d.ts.map +1 -1
- package/channel/useSaveAgentChannel.js +6 -3
- package/channel/useSaveAgentChannel.js.map +1 -1
- package/channel-app/ChannelAppDetailPanel.js +9 -9
- package/channel-app/ChannelAppDetailPanel.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 +39 -13
- package/composer/SessionComposer.js.map +1 -1
- package/conversation/ConversationComposer.d.ts +19 -3
- package/conversation/ConversationComposer.d.ts.map +1 -1
- package/conversation/ConversationComposer.js +4 -4
- package/conversation/ConversationComposer.js.map +1 -1
- package/conversation/ConversationTemplatePickerDialog.d.ts +63 -0
- package/conversation/ConversationTemplatePickerDialog.d.ts.map +1 -0
- package/conversation/ConversationTemplatePickerDialog.js +151 -0
- package/conversation/ConversationTemplatePickerDialog.js.map +1 -0
- package/conversation/ConversationsWorkbench.d.ts.map +1 -1
- package/conversation/ConversationsWorkbench.js +16 -5
- package/conversation/ConversationsWorkbench.js.map +1 -1
- package/conversation/index.d.ts +3 -1
- package/conversation/index.d.ts.map +1 -1
- package/conversation/index.js +1 -0
- package/conversation/index.js.map +1 -1
- package/conversation/useConversationParticipation.d.ts +41 -4
- package/conversation/useConversationParticipation.d.ts.map +1 -1
- package/conversation/useConversationParticipation.js +13 -2
- package/conversation/useConversationParticipation.js.map +1 -1
- package/cursor-accounts/CursorAccountsConsole.d.ts.map +1 -1
- package/cursor-accounts/CursorAccountsConsole.js +12 -3
- package/cursor-accounts/CursorAccountsConsole.js.map +1 -1
- package/cursor-accounts/cursor-account-coverage.d.ts +30 -1
- package/cursor-accounts/cursor-account-coverage.d.ts.map +1 -1
- package/cursor-accounts/cursor-account-coverage.js +21 -0
- package/cursor-accounts/cursor-account-coverage.js.map +1 -1
- package/execution/useCreateAgentExecution.d.ts +15 -0
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +5 -0
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/identity-account/AccountPreferencesPanel.d.ts +36 -0
- package/identity-account/AccountPreferencesPanel.d.ts.map +1 -0
- package/identity-account/AccountPreferencesPanel.js +202 -0
- package/identity-account/AccountPreferencesPanel.js.map +1 -0
- package/identity-account/index.d.ts +8 -0
- package/identity-account/index.d.ts.map +1 -1
- package/identity-account/index.js +4 -0
- package/identity-account/index.js.map +1 -1
- package/identity-account/useAccountExecutionDefaults.d.ts +36 -0
- package/identity-account/useAccountExecutionDefaults.d.ts.map +1 -0
- package/identity-account/useAccountExecutionDefaults.js +46 -0
- package/identity-account/useAccountExecutionDefaults.js.map +1 -0
- package/identity-account/useMyIdentityAccount.d.ts +55 -0
- package/identity-account/useMyIdentityAccount.d.ts.map +1 -0
- package/identity-account/useMyIdentityAccount.js +37 -0
- package/identity-account/useMyIdentityAccount.js.map +1 -0
- package/identity-account/useUpdateIdentityAccount.d.ts +42 -0
- package/identity-account/useUpdateIdentityAccount.d.ts.map +1 -0
- package/identity-account/useUpdateIdentityAccount.js +53 -0
- package/identity-account/useUpdateIdentityAccount.js.map +1 -0
- package/identity-provider/IdentityProviderDetailPanel.d.ts.map +1 -1
- package/identity-provider/IdentityProviderDetailPanel.js +16 -15
- package/identity-provider/IdentityProviderDetailPanel.js.map +1 -1
- package/index.d.ts +15 -13
- package/index.d.ts.map +1 -1
- package/index.js +11 -8
- package/index.js.map +1 -1
- package/internal/StandingContextField.d.ts +37 -0
- package/internal/StandingContextField.d.ts.map +1 -0
- package/internal/StandingContextField.js +24 -0
- package/internal/StandingContextField.js.map +1 -0
- package/library/InstanceVisibilitySelector.d.ts.map +1 -1
- package/library/InstanceVisibilitySelector.js +9 -2
- package/library/InstanceVisibilitySelector.js.map +1 -1
- package/library/ResourceVisibilityControl.d.ts +13 -3
- package/library/ResourceVisibilityControl.d.ts.map +1 -1
- package/library/ResourceVisibilityControl.js +33 -5
- package/library/ResourceVisibilityControl.js.map +1 -1
- package/library/VisibilityOptionRow.d.ts +6 -0
- package/library/VisibilityOptionRow.d.ts.map +1 -1
- package/library/VisibilityOptionRow.js +11 -1
- package/library/VisibilityOptionRow.js.map +1 -1
- package/library/VisibilitySelector.d.ts.map +1 -1
- package/library/VisibilitySelector.js +14 -2
- package/library/VisibilitySelector.js.map +1 -1
- package/library/index.d.ts +4 -2
- package/library/index.d.ts.map +1 -1
- package/library/index.js +2 -1
- package/library/index.js.map +1 -1
- package/library/useCanSetPublicVisibility.d.ts +30 -0
- package/library/useCanSetPublicVisibility.d.ts.map +1 -0
- package/library/useCanSetPublicVisibility.js +41 -0
- package/library/useCanSetPublicVisibility.js.map +1 -0
- package/library/visibilityLevels.d.ts +32 -1
- package/library/visibilityLevels.d.ts.map +1 -1
- package/library/visibilityLevels.js +34 -14
- package/library/visibilityLevels.js.map +1 -1
- package/mcp-server/McpServerDetailView.js +2 -2
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/mcp-server/index.d.ts +0 -1
- package/mcp-server/index.d.ts.map +1 -1
- package/mcp-server/index.js +0 -1
- package/mcp-server/index.js.map +1 -1
- package/mcp-server/internal/definitionDrift.d.ts +14 -12
- package/mcp-server/internal/definitionDrift.d.ts.map +1 -1
- package/mcp-server/internal/definitionDrift.js +22 -19
- package/mcp-server/internal/definitionDrift.js.map +1 -1
- package/mcp-server/useMcpServerConnect.d.ts +8 -5
- package/mcp-server/useMcpServerConnect.d.ts.map +1 -1
- package/mcp-server/useMcpServerConnect.js +2 -1
- package/mcp-server/useMcpServerConnect.js.map +1 -1
- package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -1
- package/mcp-server/useMcpServerOAuthConnect.js +7 -2
- package/mcp-server/useMcpServerOAuthConnect.js.map +1 -1
- package/mcp-server/useUpdateMcpServer.d.ts +3 -3
- package/mcp-server/useUpdateMcpServer.js +3 -3
- package/models/ModelSelector.d.ts +33 -1
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +37 -4
- 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 +16 -0
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js +13 -0
- package/models/registry.js.map +1 -1
- package/models/thinking-mode.d.ts +27 -0
- package/models/thinking-mode.d.ts.map +1 -0
- package/models/thinking-mode.js +32 -0
- package/models/thinking-mode.js.map +1 -0
- package/oauth-app/CreateOAuthAppForm.d.ts +2 -2
- package/oauth-app/CreateOAuthAppForm.d.ts.map +1 -1
- package/oauth-app/CreateOAuthAppForm.js +13 -4
- package/oauth-app/CreateOAuthAppForm.js.map +1 -1
- package/oauth-app/OAuthAppDetailPanel.d.ts.map +1 -1
- package/oauth-app/OAuthAppDetailPanel.js +50 -22
- package/oauth-app/OAuthAppDetailPanel.js.map +1 -1
- package/organization/OrgPreferencesPanel.d.ts +32 -0
- package/organization/OrgPreferencesPanel.d.ts.map +1 -0
- package/organization/OrgPreferencesPanel.js +90 -0
- package/organization/OrgPreferencesPanel.js.map +1 -0
- package/organization/OrgProfilePanel.d.ts.map +1 -1
- package/organization/OrgProfilePanel.js +5 -4
- package/organization/OrgProfilePanel.js.map +1 -1
- package/organization/index.d.ts +2 -0
- package/organization/index.d.ts.map +1 -1
- package/organization/index.js +1 -0
- package/organization/index.js.map +1 -1
- package/package.json +4 -4
- package/platform-client/PlatformClientDetailPanel.d.ts.map +1 -1
- package/platform-client/PlatformClientDetailPanel.js +12 -13
- package/platform-client/PlatformClientDetailPanel.js.map +1 -1
- package/schedule/ScheduleDetailView.d.ts.map +1 -1
- package/schedule/ScheduleDetailView.js +21 -10
- package/schedule/ScheduleDetailView.js.map +1 -1
- package/schedule/ScheduleForm.d.ts.map +1 -1
- package/schedule/ScheduleForm.js +13 -3
- package/schedule/ScheduleForm.js.map +1 -1
- package/session/NewSessionViewer.d.ts +10 -1
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +2 -1
- package/session/NewSessionViewer.js.map +1 -1
- package/session/run-config.d.ts +9 -0
- package/session/run-config.d.ts.map +1 -1
- package/session/run-config.js +5 -0
- package/session/run-config.js.map +1 -1
- package/session/useNewSessionFlow.d.ts +27 -1
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +51 -8
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionConversation.d.ts +12 -1
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +15 -24
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +14 -10
- package/session/useSessionPageFlow.js.map +1 -1
- package/session/useUpdateSession.d.ts +18 -24
- package/session/useUpdateSession.d.ts.map +1 -1
- package/session/useUpdateSession.js +18 -24
- package/session/useUpdateSession.js.map +1 -1
- package/settings/AccountPreferencesSection.d.ts +3 -0
- package/settings/AccountPreferencesSection.d.ts.map +1 -0
- package/settings/AccountPreferencesSection.js +10 -0
- package/settings/AccountPreferencesSection.js.map +1 -0
- package/settings/OrgPreferencesSection.d.ts +3 -0
- package/settings/OrgPreferencesSection.d.ts.map +1 -0
- package/settings/OrgPreferencesSection.js +13 -0
- package/settings/OrgPreferencesSection.js.map +1 -0
- package/settings/index.d.ts +2 -0
- package/settings/index.d.ts.map +1 -1
- package/settings/index.js +2 -0
- package/settings/index.js.map +1 -1
- package/settings/settings-nav.d.ts.map +1 -1
- package/settings/settings-nav.js +17 -1
- package/settings/settings-nav.js.map +1 -1
- package/src/agent/AgentDetailView.tsx +2 -2
- package/src/agent/index.ts +0 -2
- package/src/agent/useUpdateAgent.ts +3 -3
- package/src/agent-instance/AgentInstanceDetailPanel.tsx +5 -7
- package/src/agent-instance/__tests__/AgentInstanceDetailPanel.test.tsx +75 -0
- package/src/agent-instance/useUpdateAgentInstance.ts +1 -1
- package/src/attachment/AttachmentChipList.tsx +19 -8
- package/src/attachment/__tests__/AttachmentChipList.test.tsx +12 -0
- package/src/attachment/__tests__/useAttachments.test.ts +293 -0
- package/src/attachment/index.ts +1 -0
- package/src/attachment/useAttachments.ts +143 -11
- package/src/channel/AgentChannelsPanel.tsx +26 -1
- package/src/channel/ChannelRunConfigDialog.tsx +351 -0
- package/src/channel/__tests__/ChannelRunConfigDialog.test.tsx +210 -0
- package/src/channel/index.ts +2 -0
- package/src/channel/useSaveAgentChannel.ts +6 -3
- package/src/channel-app/ChannelAppDetailPanel.tsx +9 -9
- package/src/channel-app/__tests__/channelAppWhatsApp.test.tsx +2 -0
- package/src/channel-app/__tests__/channelAppWipeSafety.test.tsx +87 -0
- package/src/composer/ComposerToolbar.tsx +9 -0
- package/src/composer/SessionComposer.tsx +59 -16
- package/src/composer/__tests__/SessionComposer-thinkingMode.test.tsx +176 -0
- package/src/conversation/ConversationComposer.tsx +38 -4
- package/src/conversation/ConversationTemplatePickerDialog.tsx +441 -0
- package/src/conversation/ConversationsWorkbench.tsx +40 -4
- package/src/conversation/__tests__/ConversationComposer.test.tsx +56 -1
- package/src/conversation/__tests__/ConversationTemplatePickerDialog.test.tsx +224 -0
- package/src/conversation/__tests__/useConversationParticipation.test.tsx +45 -4
- package/src/conversation/index.ts +4 -0
- package/src/conversation/useConversationParticipation.ts +58 -6
- package/src/cursor-accounts/CursorAccountsConsole.tsx +40 -5
- package/src/cursor-accounts/__tests__/CursorAccountsConsole.test.tsx +32 -0
- package/src/cursor-accounts/__tests__/cursor-account-coverage.test.ts +58 -1
- package/src/cursor-accounts/cursor-account-coverage.ts +55 -0
- package/src/execution/useCreateAgentExecution.ts +19 -0
- package/src/identity-account/AccountPreferencesPanel.tsx +532 -0
- package/src/identity-account/__tests__/AccountPreferencesPanel.test.tsx +267 -0
- package/src/identity-account/__tests__/useAccountExecutionDefaults.test.tsx +116 -0
- package/src/identity-account/__tests__/useMyIdentityAccount.test.tsx +121 -0
- package/src/identity-account/index.ts +11 -0
- package/src/identity-account/useAccountExecutionDefaults.ts +63 -0
- package/src/identity-account/useMyIdentityAccount.ts +75 -0
- package/src/identity-account/useUpdateIdentityAccount.ts +73 -0
- package/src/identity-provider/IdentityProviderDetailPanel.tsx +18 -15
- package/src/identity-provider/__tests__/IdentityProviderDetailPanel.test.tsx +80 -0
- package/src/index.ts +26 -4
- package/src/internal/StandingContextField.tsx +92 -0
- package/src/library/InstanceVisibilitySelector.tsx +9 -2
- package/src/library/ResourceVisibilityControl.tsx +46 -13
- package/src/library/VisibilityOptionRow.tsx +28 -6
- package/src/library/VisibilitySelector.tsx +14 -2
- package/src/library/__tests__/VisibilitySelector.test.tsx +48 -1
- package/src/library/__tests__/visibilityLevels.test.ts +67 -1
- package/src/library/index.ts +6 -1
- package/src/library/useCanSetPublicVisibility.ts +56 -0
- package/src/library/visibilityLevels.ts +65 -14
- package/src/mcp-server/McpServerDetailView.tsx +2 -2
- package/src/mcp-server/__tests__/useMcpServerConnect.asyncLane.test.tsx +117 -0
- package/src/mcp-server/index.ts +0 -2
- package/src/mcp-server/internal/definitionDrift.ts +22 -20
- package/src/mcp-server/useMcpServerConnect.ts +9 -6
- package/src/mcp-server/useMcpServerOAuthConnect.ts +7 -2
- package/src/mcp-server/useUpdateMcpServer.ts +3 -3
- package/src/models/ModelSelector.tsx +94 -1
- package/src/models/index.ts +2 -0
- package/src/models/registry.ts +29 -0
- package/src/models/thinking-mode.ts +48 -0
- package/src/oauth-app/CreateOAuthAppForm.tsx +58 -3
- package/src/oauth-app/OAuthAppDetailPanel.tsx +103 -20
- package/src/oauth-app/__tests__/OAuthAppDetailPanel.test.tsx +94 -0
- package/src/organization/OrgPreferencesPanel.tsx +221 -0
- package/src/organization/OrgProfilePanel.tsx +5 -4
- package/src/organization/__tests__/OrgPreferencesPanel.test.tsx +145 -0
- package/src/organization/__tests__/OrgProfilePanel.test.tsx +108 -0
- package/src/organization/index.ts +2 -0
- package/src/platform-client/PlatformClientDetailPanel.tsx +14 -13
- package/src/platform-client/__tests__/PlatformClientDetailPanel.test.tsx +82 -0
- package/src/schedule/ScheduleDetailView.tsx +29 -4
- package/src/schedule/ScheduleForm.tsx +14 -1
- package/src/session/NewSessionViewer.tsx +12 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +151 -0
- package/src/session/run-config.ts +16 -0
- package/src/session/useNewSessionFlow.ts +82 -9
- package/src/session/useSessionConversation.ts +32 -40
- package/src/session/useSessionPageFlow.ts +14 -11
- package/src/session/useUpdateSession.ts +18 -24
- package/src/settings/AccountPreferencesSection.tsx +25 -0
- package/src/settings/OrgPreferencesSection.tsx +35 -0
- package/src/settings/__tests__/settings-nav.test.ts +27 -0
- package/src/settings/index.ts +2 -0
- package/src/settings/settings-nav.ts +18 -0
- package/src/workflow/ApprovalFormBuilder.tsx +20 -27
- package/src/workflow/WorkflowDetailView.tsx +2 -2
- package/src/workflow/__tests__/WorkflowInstanceDetailPanel.test.tsx +83 -0
- package/src/workflow/index.ts +0 -2
- package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +6 -7
- package/src/workflow/useUpdateWorkflow.ts +3 -3
- package/styles.css +1 -1
- package/workflow/ApprovalFormBuilder.d.ts.map +1 -1
- package/workflow/ApprovalFormBuilder.js +10 -5
- package/workflow/ApprovalFormBuilder.js.map +1 -1
- package/workflow/WorkflowDetailView.js +2 -2
- package/workflow/WorkflowDetailView.js.map +1 -1
- package/workflow/index.d.ts +0 -1
- package/workflow/index.d.ts.map +1 -1
- package/workflow/index.js +0 -1
- package/workflow/index.js.map +1 -1
- package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceDetailPanel.js +6 -7
- package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
- package/workflow/useUpdateWorkflow.d.ts +3 -3
- package/workflow/useUpdateWorkflow.js +3 -3
- package/agent/internal/agentToInput.d.ts +0 -15
- package/agent/internal/agentToInput.d.ts.map +0 -1
- package/agent/internal/agentToInput.js +0 -84
- package/agent/internal/agentToInput.js.map +0 -1
- package/mcp-server/internal/mcpServerToInput.d.ts +0 -12
- package/mcp-server/internal/mcpServerToInput.d.ts.map +0 -1
- package/mcp-server/internal/mcpServerToInput.js +0 -84
- package/mcp-server/internal/mcpServerToInput.js.map +0 -1
- package/session/session-spec-converters.d.ts +0 -24
- package/session/session-spec-converters.d.ts.map +0 -1
- package/session/session-spec-converters.js +0 -72
- package/session/session-spec-converters.js.map +0 -1
- package/src/agent/internal/__tests__/agentToInput.test.ts +0 -145
- package/src/agent/internal/agentToInput.ts +0 -102
- package/src/mcp-server/__tests__/mcpServerToInput.test.ts +0 -201
- package/src/mcp-server/internal/mcpServerToInput.ts +0 -98
- package/src/session/__tests__/session-spec-converters.test.ts +0 -183
- package/src/session/session-spec-converters.ts +0 -86
- package/src/workflow/internal/workflowToInput.ts +0 -96
- package/workflow/internal/workflowToInput.d.ts +0 -12
- package/workflow/internal/workflowToInput.d.ts.map +0 -1
- package/workflow/internal/workflowToInput.js +0 -73
- package/workflow/internal/workflowToInput.js.map +0 -1
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useId, useMemo, useState } from "react";
|
|
4
|
+
import { ArrowLeft, LayoutTemplate, Send, X } 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 {
|
|
9
|
+
ChannelTemplate,
|
|
10
|
+
SendChannelMessageOutput,
|
|
11
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
|
|
12
|
+
import { Button } from "../button/Button.js";
|
|
13
|
+
import { EmptyState } from "../empty-state/EmptyState.js";
|
|
14
|
+
import { DialogShell } from "../internal/DialogShell.js";
|
|
15
|
+
import { SpinnerIcon } from "../internal/SpinnerIcon.js";
|
|
16
|
+
import { UNSTYLED_LIST } from "../internal/element-resets.js";
|
|
17
|
+
import { TruncatedText } from "../internal/truncated-text.js";
|
|
18
|
+
import { splitTemplateBody } from "../channel/templatePresentation.js";
|
|
19
|
+
import { useChannelTemplateList } from "../channel/useChannelTemplateList.js";
|
|
20
|
+
import type { ConversationReplyPayload } from "./useConversationParticipation.js";
|
|
21
|
+
|
|
22
|
+
/** Props for {@link ConversationTemplatePickerDialog}. */
|
|
23
|
+
export interface ConversationTemplatePickerDialogProps {
|
|
24
|
+
/** Whether the dialog is open. */
|
|
25
|
+
readonly open: boolean;
|
|
26
|
+
/** Called when the dialog requests an open-state change. */
|
|
27
|
+
readonly onOpenChange: (open: boolean) => void;
|
|
28
|
+
/**
|
|
29
|
+
* The conversation's channel, addressed as the template registry
|
|
30
|
+
* addresses it: `metadata.slug` + `metadata.org` (the registry is
|
|
31
|
+
* derived from the channel's phone number, not stored on a resource).
|
|
32
|
+
*/
|
|
33
|
+
readonly channelSlug: string;
|
|
34
|
+
readonly org: string;
|
|
35
|
+
/**
|
|
36
|
+
* Send the chosen template as a staff reply — the SAME seam the
|
|
37
|
+
* composer's textarea uses (`useConversationParticipation().reply`
|
|
38
|
+
* or the host's wrapper around it), so a template send takes over,
|
|
39
|
+
* settles, and reports outcomes exactly like a text send.
|
|
40
|
+
*/
|
|
41
|
+
readonly onSend: (payload: ConversationReplyPayload) => Promise<SendChannelMessageOutput>;
|
|
42
|
+
/** `true` while a reply is in flight. */
|
|
43
|
+
readonly isSending: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* When `false`, renders as an in-flow open dialog instead of a
|
|
46
|
+
* top-layer modal — for embedding in constrained surfaces
|
|
47
|
+
* (documentation demos, visual tests). Interactive hosts keep the
|
|
48
|
+
* default.
|
|
49
|
+
* @default true
|
|
50
|
+
*/
|
|
51
|
+
readonly modal?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Pick a provider-approved template, fill its variables, and send it as
|
|
56
|
+
* a staff reply — the one lane WhatsApp offers a business once the
|
|
57
|
+
* customer's 24-hour service window has closed (cloud#260).
|
|
58
|
+
*
|
|
59
|
+
* Where {@link ChannelTemplatesDialog} is the channel surface's
|
|
60
|
+
* read-only registry *viewer* (every status, diagnosis copy), this is
|
|
61
|
+
* the conversation surface's *picker*: it lists only approved
|
|
62
|
+
* templates, marks the ones this platform version cannot send (an
|
|
63
|
+
* `unsupported_reason` from the server, or an IMAGE header — the send
|
|
64
|
+
* payload needs a public HTTPS asset no console surface can supply
|
|
65
|
+
* yet), and walks the two steps of a send: choose, then fill the
|
|
66
|
+
* `{{...}}` variables with a live preview of the exact body the
|
|
67
|
+
* customer will receive.
|
|
68
|
+
*
|
|
69
|
+
* Outcome honesty mirrors the composer's contract: a `refused` outcome
|
|
70
|
+
* keeps the dialog open with every filled value intact and renders the
|
|
71
|
+
* server's corrective detail verbatim (the pre-check names missing
|
|
72
|
+
* parameters, wrong languages, paused templates); a thrown failure does
|
|
73
|
+
* the same with the transport error. `accepted` and `queued` close the
|
|
74
|
+
* dialog — the ledger row exists, so the timeline shows the real item
|
|
75
|
+
* with its true delivery status (the SDK never fabricates one).
|
|
76
|
+
*
|
|
77
|
+
* The body mounts only while open, so each opening fetches the registry
|
|
78
|
+
* fresh (approval statuses change on the provider's side at any time)
|
|
79
|
+
* and a closed dialog holds no draft — the same clean-reset rule that
|
|
80
|
+
* keys the workbench's detail column (F-22).
|
|
81
|
+
*/
|
|
82
|
+
export function ConversationTemplatePickerDialog({
|
|
83
|
+
open,
|
|
84
|
+
onOpenChange,
|
|
85
|
+
channelSlug,
|
|
86
|
+
org,
|
|
87
|
+
onSend,
|
|
88
|
+
isSending,
|
|
89
|
+
modal = true,
|
|
90
|
+
}: ConversationTemplatePickerDialogProps) {
|
|
91
|
+
const titleId = useId();
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<DialogShell
|
|
95
|
+
open={open}
|
|
96
|
+
onOpenChange={onOpenChange}
|
|
97
|
+
modal={modal}
|
|
98
|
+
// Template bodies are the content — the templates-dialog width.
|
|
99
|
+
width="2xl"
|
|
100
|
+
aria-labelledby={titleId}
|
|
101
|
+
>
|
|
102
|
+
{open && (
|
|
103
|
+
<PickerBody
|
|
104
|
+
titleId={titleId}
|
|
105
|
+
channelSlug={channelSlug}
|
|
106
|
+
org={org}
|
|
107
|
+
onSend={onSend}
|
|
108
|
+
isSending={isSending}
|
|
109
|
+
onClose={() => onOpenChange(false)}
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
</DialogShell>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Why a listed template cannot be sent from here, or null when it can. */
|
|
117
|
+
function unsendableReasonOf(template: ChannelTemplate): string | null {
|
|
118
|
+
if (template.unsupportedReason) {
|
|
119
|
+
return `Not sendable: ${template.unsupportedReason}`;
|
|
120
|
+
}
|
|
121
|
+
if (template.headerFormat === "IMAGE") {
|
|
122
|
+
// The wire's header_image_link needs a public HTTPS asset at send
|
|
123
|
+
// time; no console surface can supply one yet (v1 scope cut,
|
|
124
|
+
// recorded on cloud#260).
|
|
125
|
+
return "Not sendable from the console yet: this template's image header needs a hosted image at send time.";
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The verbatim `{{ key }}` placeholder's inner key. */
|
|
131
|
+
function placeholderKeyOf(placeholder: string): string {
|
|
132
|
+
return placeholder.replace(/^\{\{\s*|\s*\}\}$/g, "");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function PickerBody({
|
|
136
|
+
titleId,
|
|
137
|
+
channelSlug,
|
|
138
|
+
org,
|
|
139
|
+
onSend,
|
|
140
|
+
isSending,
|
|
141
|
+
onClose,
|
|
142
|
+
}: {
|
|
143
|
+
readonly titleId: string;
|
|
144
|
+
readonly channelSlug: string;
|
|
145
|
+
readonly org: string;
|
|
146
|
+
readonly onSend: (payload: ConversationReplyPayload) => Promise<SendChannelMessageOutput>;
|
|
147
|
+
readonly isSending: boolean;
|
|
148
|
+
readonly onClose: () => void;
|
|
149
|
+
}) {
|
|
150
|
+
// Approved-only is the wire filter; unsupported/image-header entries
|
|
151
|
+
// still list (marked unsendable) so "why is my template missing"
|
|
152
|
+
// never needs a second surface.
|
|
153
|
+
const { templates, isLoading, error } = useChannelTemplateList(channelSlug, org, {
|
|
154
|
+
approvedOnly: true,
|
|
155
|
+
});
|
|
156
|
+
const [selected, setSelected] = useState<ChannelTemplate | null>(null);
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div className="stg:flex stg:max-h-[75vh] stg:flex-col">
|
|
160
|
+
<div className="stg:flex stg:items-start stg:justify-between stg:gap-3 stg:border-b stg:border-border stg:px-5 stg:py-4">
|
|
161
|
+
<div className="stg:flex stg:min-w-0 stg:items-center stg:gap-2">
|
|
162
|
+
{selected !== null && (
|
|
163
|
+
<Button
|
|
164
|
+
variant="ghost"
|
|
165
|
+
size="xs"
|
|
166
|
+
onClick={() => setSelected(null)}
|
|
167
|
+
disabled={isSending}
|
|
168
|
+
aria-label="Back to the template list"
|
|
169
|
+
>
|
|
170
|
+
<ArrowLeft className="stg:size-4" />
|
|
171
|
+
</Button>
|
|
172
|
+
)}
|
|
173
|
+
<div className="stg:min-w-0">
|
|
174
|
+
<h2
|
|
175
|
+
id={titleId}
|
|
176
|
+
className="stg:text-sm stg:font-semibold stg:text-popover-foreground"
|
|
177
|
+
>
|
|
178
|
+
{selected === null ? "Send a template" : selected.name}
|
|
179
|
+
</h2>
|
|
180
|
+
<p className="stg:mt-0.5 stg:text-xs stg:text-muted-foreground">
|
|
181
|
+
{selected === null
|
|
182
|
+
? "Approved templates deliver even after the 24-hour reply window closes."
|
|
183
|
+
: "Fill the template's variables — the preview is the exact message the customer receives."}
|
|
184
|
+
</p>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
<Button variant="ghost" size="xs" onClick={onClose} aria-label="Close template picker">
|
|
188
|
+
<X className="stg:size-4" />
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div className="stg:min-h-0 stg:flex-1 stg:overflow-y-auto stg:p-4">
|
|
193
|
+
{selected === null ? (
|
|
194
|
+
<TemplateChoiceList
|
|
195
|
+
templates={templates}
|
|
196
|
+
isLoading={isLoading}
|
|
197
|
+
error={error}
|
|
198
|
+
onChoose={setSelected}
|
|
199
|
+
/>
|
|
200
|
+
) : (
|
|
201
|
+
<TemplateFillForm
|
|
202
|
+
template={selected}
|
|
203
|
+
onSend={onSend}
|
|
204
|
+
isSending={isSending}
|
|
205
|
+
onClose={onClose}
|
|
206
|
+
/>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function TemplateChoiceList({
|
|
214
|
+
templates,
|
|
215
|
+
isLoading,
|
|
216
|
+
error,
|
|
217
|
+
onChoose,
|
|
218
|
+
}: {
|
|
219
|
+
readonly templates: readonly ChannelTemplate[];
|
|
220
|
+
readonly isLoading: boolean;
|
|
221
|
+
readonly error: Error | null;
|
|
222
|
+
readonly onChoose: (template: ChannelTemplate) => void;
|
|
223
|
+
}) {
|
|
224
|
+
if (isLoading) {
|
|
225
|
+
return (
|
|
226
|
+
<div className="stg:space-y-2 stg:p-1">
|
|
227
|
+
{[1, 2, 3].map((i) => (
|
|
228
|
+
<div key={i} className="stg:h-16 stg:animate-pulse stg:rounded-lg stg:bg-muted-faint" />
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (error) {
|
|
234
|
+
// Past the fetch, the server's copy is the teaching state — its
|
|
235
|
+
// refusals name the fix verbatim (the templates-dialog posture).
|
|
236
|
+
return (
|
|
237
|
+
<p className="stg:px-2 stg:py-6 stg:text-center stg:text-sm stg:text-destructive">
|
|
238
|
+
{getUserMessage(error)}
|
|
239
|
+
</p>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (templates.length === 0) {
|
|
243
|
+
// "found", never "you have none": an empty success is
|
|
244
|
+
// indistinguishable from a filtered or truncated one on the wire.
|
|
245
|
+
return (
|
|
246
|
+
<EmptyState
|
|
247
|
+
variant="first-use"
|
|
248
|
+
icon={<LayoutTemplate className="stg:size-8" />}
|
|
249
|
+
title="No approved templates found"
|
|
250
|
+
description={
|
|
251
|
+
"Templates are authored and approved in WhatsApp Manager — " +
|
|
252
|
+
"Stigmer keeps no copy. Approved templates appear here " +
|
|
253
|
+
"automatically; the channel card's Templates dialog shows every " +
|
|
254
|
+
"status, including pending and rejected ones."
|
|
255
|
+
}
|
|
256
|
+
/>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<ul className={cn(UNSTYLED_LIST, "stg:space-y-2")}>
|
|
262
|
+
{templates.map((template) => {
|
|
263
|
+
const unsendable = unsendableReasonOf(template);
|
|
264
|
+
return (
|
|
265
|
+
<li key={`${template.name}\u0000${template.language}`}>
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
onClick={() => onChoose(template)}
|
|
269
|
+
disabled={unsendable !== null}
|
|
270
|
+
className={cn(
|
|
271
|
+
"stg:w-full stg:rounded-lg stg:border stg:border-border stg:p-3 stg:text-left",
|
|
272
|
+
"stg:focus-visible:outline-none stg:focus-visible:ring-2 stg:focus-visible:ring-ring",
|
|
273
|
+
unsendable === null
|
|
274
|
+
? "stg:hover:border-ring stg:hover:bg-muted-faint"
|
|
275
|
+
: "stg:opacity-60",
|
|
276
|
+
)}
|
|
277
|
+
>
|
|
278
|
+
<div className="stg:flex stg:min-w-0 stg:flex-wrap stg:items-center stg:gap-x-2 stg:gap-y-1">
|
|
279
|
+
<TruncatedText
|
|
280
|
+
text={template.name}
|
|
281
|
+
className="stg:text-sm stg:font-medium stg:text-foreground"
|
|
282
|
+
/>
|
|
283
|
+
{/* (name, language) is the template's identity — each
|
|
284
|
+
language is approved independently. */}
|
|
285
|
+
<span className="stg:shrink-0 stg:text-xs stg:text-muted-foreground">
|
|
286
|
+
{template.language}
|
|
287
|
+
</span>
|
|
288
|
+
{template.headerFormat && (
|
|
289
|
+
<span className="stg:shrink-0 stg:rounded stg:bg-muted-subtle stg:px-1.5 stg:py-0.5 stg:text-xs stg:text-muted-foreground">
|
|
290
|
+
{template.headerFormat} header
|
|
291
|
+
</span>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
{template.bodyText && (
|
|
295
|
+
<p className="stg:mt-1.5 stg:line-clamp-2 stg:whitespace-pre-wrap stg:text-xs stg:leading-relaxed stg:text-muted-foreground">
|
|
296
|
+
{template.bodyText}
|
|
297
|
+
</p>
|
|
298
|
+
)}
|
|
299
|
+
{unsendable !== null && (
|
|
300
|
+
<p className="stg:mt-1.5 stg:text-xs stg:text-warning">{unsendable}</p>
|
|
301
|
+
)}
|
|
302
|
+
</button>
|
|
303
|
+
</li>
|
|
304
|
+
);
|
|
305
|
+
})}
|
|
306
|
+
</ul>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function TemplateFillForm({
|
|
311
|
+
template,
|
|
312
|
+
onSend,
|
|
313
|
+
isSending,
|
|
314
|
+
onClose,
|
|
315
|
+
}: {
|
|
316
|
+
readonly template: ChannelTemplate;
|
|
317
|
+
readonly onSend: (payload: ConversationReplyPayload) => Promise<SendChannelMessageOutput>;
|
|
318
|
+
readonly isSending: boolean;
|
|
319
|
+
readonly onClose: () => void;
|
|
320
|
+
}) {
|
|
321
|
+
const fieldIdPrefix = useId();
|
|
322
|
+
const [values, setValues] = useState<Readonly<Record<string, string>>>({});
|
|
323
|
+
const [notice, setNotice] = useState<{
|
|
324
|
+
readonly kind: "refused" | "error";
|
|
325
|
+
readonly text: string;
|
|
326
|
+
} | null>(null);
|
|
327
|
+
|
|
328
|
+
const segments = useMemo(() => splitTemplateBody(template.bodyText), [template.bodyText]);
|
|
329
|
+
// The declared parameters are the contract: names for NAMED
|
|
330
|
+
// templates, positions ("1", "2", …) for POSITIONAL — the pre-check
|
|
331
|
+
// refuses anything missing or extra, so the form asks for exactly
|
|
332
|
+
// this list.
|
|
333
|
+
const parameterKeys = template.parameterNames;
|
|
334
|
+
const allFilled = parameterKeys.every((key) => (values[key] ?? "").trim() !== "");
|
|
335
|
+
|
|
336
|
+
const handleSend = () => {
|
|
337
|
+
setNotice(null);
|
|
338
|
+
const parameters = Object.fromEntries(
|
|
339
|
+
parameterKeys.map((key) => [key, (values[key] ?? "").trim()]),
|
|
340
|
+
);
|
|
341
|
+
onSend({
|
|
342
|
+
kind: "template",
|
|
343
|
+
name: template.name,
|
|
344
|
+
// Always the chosen entry's language: (name, language) is the
|
|
345
|
+
// identity we listed, and sending it structurally avoids the
|
|
346
|
+
// wire's ambiguous-language refusal.
|
|
347
|
+
language: template.language,
|
|
348
|
+
parameters,
|
|
349
|
+
}).then(
|
|
350
|
+
(output) => {
|
|
351
|
+
if (output.outcome === ChannelSendOutcome.refused) {
|
|
352
|
+
// The words never left; every filled value stays for the
|
|
353
|
+
// correction the detail asks for (the composer's contract).
|
|
354
|
+
setNotice({
|
|
355
|
+
kind: "refused",
|
|
356
|
+
text: output.detail || "The provider refused this template.",
|
|
357
|
+
});
|
|
358
|
+
} else {
|
|
359
|
+
// accepted or queued: the ledger row exists — the timeline
|
|
360
|
+
// shows the real item with its true delivery status.
|
|
361
|
+
onClose();
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
(err) => {
|
|
365
|
+
setNotice({ kind: "error", text: getUserMessage(err) });
|
|
366
|
+
},
|
|
367
|
+
);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<div className="stg:space-y-4">
|
|
372
|
+
{notice && (
|
|
373
|
+
<p className="stg:text-xs stg:text-destructive" role="status">
|
|
374
|
+
{notice.text}
|
|
375
|
+
</p>
|
|
376
|
+
)}
|
|
377
|
+
|
|
378
|
+
{parameterKeys.length > 0 && (
|
|
379
|
+
<div className="stg:space-y-3">
|
|
380
|
+
{parameterKeys.map((key) => (
|
|
381
|
+
<div key={key}>
|
|
382
|
+
<label
|
|
383
|
+
htmlFor={`${fieldIdPrefix}-${key}`}
|
|
384
|
+
className="stg:mb-1 stg:block stg:text-xs stg:font-medium stg:text-foreground"
|
|
385
|
+
>
|
|
386
|
+
{key}
|
|
387
|
+
</label>
|
|
388
|
+
<input
|
|
389
|
+
id={`${fieldIdPrefix}-${key}`}
|
|
390
|
+
type="text"
|
|
391
|
+
value={values[key] ?? ""}
|
|
392
|
+
onChange={(e) => setValues((v) => ({ ...v, [key]: e.target.value }))}
|
|
393
|
+
disabled={isSending}
|
|
394
|
+
className={cn(
|
|
395
|
+
"stg:w-full stg:rounded-md stg:border stg:border-border stg:bg-background",
|
|
396
|
+
"stg:px-3 stg:py-2 stg:text-sm stg:text-foreground stg:placeholder:text-muted-foreground-faint",
|
|
397
|
+
"stg:focus-visible:outline-none stg:focus-visible:ring-2 stg:focus-visible:ring-ring",
|
|
398
|
+
)}
|
|
399
|
+
/>
|
|
400
|
+
</div>
|
|
401
|
+
))}
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
|
|
405
|
+
<div>
|
|
406
|
+
<p className="stg:mb-1 stg:text-xs stg:font-medium stg:text-foreground">Preview</p>
|
|
407
|
+
<p className="stg:whitespace-pre-wrap stg:rounded-md stg:border stg:border-border stg:bg-muted-faint stg:p-3 stg:text-sm stg:leading-relaxed stg:text-foreground">
|
|
408
|
+
{segments.map((segment, i) => {
|
|
409
|
+
if (segment.kind === "text") {
|
|
410
|
+
return <span key={i}>{segment.value}</span>;
|
|
411
|
+
}
|
|
412
|
+
const value = (values[placeholderKeyOf(segment.value)] ?? "").trim();
|
|
413
|
+
return value !== "" ? (
|
|
414
|
+
<span key={i}>{value}</span>
|
|
415
|
+
) : (
|
|
416
|
+
<code
|
|
417
|
+
key={i}
|
|
418
|
+
className="stg:rounded stg:bg-muted-subtle stg:px-1 stg:font-mono stg:text-muted-foreground"
|
|
419
|
+
>
|
|
420
|
+
{segment.value}
|
|
421
|
+
</code>
|
|
422
|
+
);
|
|
423
|
+
})}
|
|
424
|
+
</p>
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
<div className="stg:flex stg:justify-end">
|
|
428
|
+
<Button
|
|
429
|
+
variant="primary"
|
|
430
|
+
size="sm"
|
|
431
|
+
onClick={handleSend}
|
|
432
|
+
disabled={!allFilled || isSending}
|
|
433
|
+
aria-label="Send template"
|
|
434
|
+
>
|
|
435
|
+
{isSending ? <SpinnerIcon /> : <Send aria-hidden="true" className="stg:size-4" />}
|
|
436
|
+
{isSending ? "Sending…" : "Send template"}
|
|
437
|
+
</Button>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
@@ -16,6 +16,7 @@ import { ConversationAttentionBanner } from "./ConversationAttentionBanner.js";
|
|
|
16
16
|
import { ConversationComposer } from "./ConversationComposer.js";
|
|
17
17
|
import { ConversationControlBanner } from "./ConversationControlBanner.js";
|
|
18
18
|
import { ConversationListPane, type ConversationIdentity } from "./ConversationListPane.js";
|
|
19
|
+
import { ConversationTemplatePickerDialog } from "./ConversationTemplatePickerDialog.js";
|
|
19
20
|
import { ConversationTimelineView } from "./ConversationTimelineView.js";
|
|
20
21
|
import {
|
|
21
22
|
conversationContactOf,
|
|
@@ -25,7 +26,10 @@ import {
|
|
|
25
26
|
} from "./conversationPresentation.js";
|
|
26
27
|
import { useConversation } from "./useConversation.js";
|
|
27
28
|
import { useConversationList } from "./useConversationList.js";
|
|
28
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
useConversationParticipation,
|
|
31
|
+
type ConversationReplyPayload,
|
|
32
|
+
} from "./useConversationParticipation.js";
|
|
29
33
|
import { useConversationTimeline } from "./useConversationTimeline.js";
|
|
30
34
|
|
|
31
35
|
/**
|
|
@@ -179,9 +183,20 @@ export function ConversationsWorkbench({
|
|
|
179
183
|
const serviceWindow = detail.conversation
|
|
180
184
|
? serviceWindowOf(detail.conversation, descriptor?.id ?? null, now ?? new Date())
|
|
181
185
|
: null;
|
|
186
|
+
// cloud#260: templates are the lane that SUCCEEDS on a closed window,
|
|
187
|
+
// offered from the persistent affordance beside Send — the advisory
|
|
188
|
+
// points at it in copy only (it is the input's description with zero
|
|
189
|
+
// tab stops, F-18; a control inside it would be unreachable context).
|
|
190
|
+
const [templatePickerOpen, setTemplatePickerOpen] = useState(false);
|
|
191
|
+
const templatesAvailable =
|
|
192
|
+
(descriptor?.supportsMessageTemplates ?? false) && selectedChannel !== null;
|
|
182
193
|
const composerAdvisory =
|
|
183
194
|
serviceWindow === "closed" && descriptor
|
|
184
|
-
? `${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
|
|
195
|
+
? `${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.${
|
|
196
|
+
templatesAvailable
|
|
197
|
+
? " Approved templates still deliver — use the template button beside Send."
|
|
198
|
+
: ""
|
|
199
|
+
}`
|
|
185
200
|
: null;
|
|
186
201
|
|
|
187
202
|
const handleSelect = useCallback(
|
|
@@ -219,8 +234,8 @@ export function ConversationsWorkbench({
|
|
|
219
234
|
}, [timeline.items, settlingItemId]);
|
|
220
235
|
|
|
221
236
|
const handleSend = useCallback(
|
|
222
|
-
async (
|
|
223
|
-
const output = await participation.reply(
|
|
237
|
+
async (payload: ConversationReplyPayload) => {
|
|
238
|
+
const output = await participation.reply(payload);
|
|
224
239
|
// The ledger row is committed before reply answers, so one head
|
|
225
240
|
// refresh renders the REAL item (with its true status) — the SDK
|
|
226
241
|
// never fabricates an optimistic item that might misstate what
|
|
@@ -371,9 +386,30 @@ export function ConversationsWorkbench({
|
|
|
371
386
|
isSending={
|
|
372
387
|
participation.pendingCommands.has("reply") || settlingItemId !== null
|
|
373
388
|
}
|
|
389
|
+
onOpenTemplatePicker={
|
|
390
|
+
templatesAvailable && composerDisabledReason === null
|
|
391
|
+
? () => setTemplatePickerOpen(true)
|
|
392
|
+
: undefined
|
|
393
|
+
}
|
|
374
394
|
disabledReason={composerDisabledReason}
|
|
375
395
|
advisory={composerAdvisory}
|
|
376
396
|
/>
|
|
397
|
+
|
|
398
|
+
{/* Inside the keyed column on purpose: switching conversations
|
|
399
|
+
remounts and closes it, so a half-filled template can never
|
|
400
|
+
travel into another customer's conversation (F-22). */}
|
|
401
|
+
{templatesAvailable && (
|
|
402
|
+
<ConversationTemplatePickerDialog
|
|
403
|
+
open={templatePickerOpen}
|
|
404
|
+
onOpenChange={setTemplatePickerOpen}
|
|
405
|
+
channelSlug={selectedChannel?.metadata?.slug ?? ""}
|
|
406
|
+
org={selectedChannel?.metadata?.org ?? ""}
|
|
407
|
+
onSend={handleSend}
|
|
408
|
+
isSending={
|
|
409
|
+
participation.pendingCommands.has("reply") || settlingItemId !== null
|
|
410
|
+
}
|
|
411
|
+
/>
|
|
412
|
+
)}
|
|
377
413
|
</div>
|
|
378
414
|
)}
|
|
379
415
|
</div>
|
|
@@ -40,10 +40,45 @@ describe("ConversationComposer", () => {
|
|
|
40
40
|
const input = screen.getByLabelText("Reply to the customer");
|
|
41
41
|
await user.type(input, "on my way{Enter}");
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
// One reply command, two lanes (cloud#260): the textarea always
|
|
44
|
+
// submits the text lane of the payload union.
|
|
45
|
+
expect(onSend).toHaveBeenCalledWith({ kind: "text", body: "on my way" });
|
|
44
46
|
await waitFor(() => expect((input as HTMLTextAreaElement).value).toBe(""));
|
|
45
47
|
});
|
|
46
48
|
|
|
49
|
+
it("renders the template affordance only when a picker is wired, disabled while sending", () => {
|
|
50
|
+
const onOpenTemplatePicker = vi.fn();
|
|
51
|
+
const { rerender } = render(
|
|
52
|
+
<ConversationComposer onSend={vi.fn()} isSending={false} disabledReason={null} />,
|
|
53
|
+
);
|
|
54
|
+
expect(screen.queryByRole("button", { name: "Send a template reply" })).toBeNull();
|
|
55
|
+
|
|
56
|
+
rerender(
|
|
57
|
+
<ConversationComposer
|
|
58
|
+
onSend={vi.fn()}
|
|
59
|
+
isSending={false}
|
|
60
|
+
disabledReason={null}
|
|
61
|
+
onOpenTemplatePicker={onOpenTemplatePicker}
|
|
62
|
+
/>,
|
|
63
|
+
);
|
|
64
|
+
const affordance = screen.getByRole("button", { name: "Send a template reply" });
|
|
65
|
+
affordance.click();
|
|
66
|
+
expect(onOpenTemplatePicker).toHaveBeenCalledTimes(1);
|
|
67
|
+
|
|
68
|
+
rerender(
|
|
69
|
+
<ConversationComposer
|
|
70
|
+
onSend={vi.fn()}
|
|
71
|
+
isSending={true}
|
|
72
|
+
disabledReason={null}
|
|
73
|
+
onOpenTemplatePicker={onOpenTemplatePicker}
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
expect(
|
|
77
|
+
(screen.getByRole("button", { name: "Send a template reply" }) as HTMLButtonElement)
|
|
78
|
+
.disabled,
|
|
79
|
+
).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
47
82
|
it("keeps the draft and shows the refusal detail verbatim when the send is refused", async () => {
|
|
48
83
|
const user = userEvent.setup();
|
|
49
84
|
const onSend = vi
|
|
@@ -118,6 +153,26 @@ describe("ConversationComposer", () => {
|
|
|
118
153
|
expect(advisoryEl?.querySelector("button, a, [tabindex]")).toBeNull();
|
|
119
154
|
});
|
|
120
155
|
|
|
156
|
+
it("keeps the advisory interaction-free even with a template picker wired (F-18)", () => {
|
|
157
|
+
render(
|
|
158
|
+
<ConversationComposer
|
|
159
|
+
onSend={vi.fn()}
|
|
160
|
+
isSending={false}
|
|
161
|
+
disabledReason={null}
|
|
162
|
+
advisory="The window has closed — send an approved template with the template button."
|
|
163
|
+
onOpenTemplatePicker={vi.fn()}
|
|
164
|
+
/>,
|
|
165
|
+
);
|
|
166
|
+
const input = screen.getByLabelText("Reply to the customer");
|
|
167
|
+
const advisoryEl = document.getElementById(
|
|
168
|
+
input.getAttribute("aria-describedby") as string,
|
|
169
|
+
);
|
|
170
|
+
// The way forward lives on the persistent affordance BESIDE Send —
|
|
171
|
+
// the description itself must never grow a control.
|
|
172
|
+
expect(advisoryEl?.querySelector("button, a, [tabindex]")).toBeNull();
|
|
173
|
+
expect(screen.getByRole("button", { name: "Send a template reply" })).toBeDefined();
|
|
174
|
+
});
|
|
175
|
+
|
|
121
176
|
it("renders no advisory and no dangling description when there is no claim", () => {
|
|
122
177
|
render(
|
|
123
178
|
<ConversationComposer onSend={vi.fn()} isSending={false} disabledReason={null} advisory={null} />,
|