@stigmer/react 3.5.3 → 3.7.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/agent/AgentPicker.d.ts +10 -1
- package/agent/AgentPicker.d.ts.map +1 -1
- package/agent/AgentPicker.js +2 -2
- package/agent/AgentPicker.js.map +1 -1
- 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 +22 -5
- package/channel/AgentChannelsPanel.js.map +1 -1
- package/channel/ChannelTemplatesDialog.d.ts +53 -0
- package/channel/ChannelTemplatesDialog.d.ts.map +1 -0
- package/channel/ChannelTemplatesDialog.js +150 -0
- package/channel/ChannelTemplatesDialog.js.map +1 -0
- package/channel/index.d.ts +8 -0
- package/channel/index.d.ts.map +1 -1
- package/channel/index.js +4 -0
- package/channel/index.js.map +1 -1
- package/channel/providers.d.ts +12 -0
- package/channel/providers.d.ts.map +1 -1
- package/channel/providers.js +14 -2
- package/channel/providers.js.map +1 -1
- package/channel/templatePresentation.d.ts +37 -0
- package/channel/templatePresentation.d.ts.map +1 -0
- package/channel/templatePresentation.js +69 -0
- package/channel/templatePresentation.js.map +1 -0
- package/channel/useChannelTemplateList.d.ts +64 -0
- package/channel/useChannelTemplateList.d.ts.map +1 -0
- package/channel/useChannelTemplateList.js +69 -0
- package/channel/useChannelTemplateList.js.map +1 -0
- package/channel/useChannelTemplateReadiness.d.ts +53 -0
- package/channel/useChannelTemplateReadiness.d.ts.map +1 -0
- package/channel/useChannelTemplateReadiness.js +42 -0
- package/channel/useChannelTemplateReadiness.js.map +1 -0
- 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/dashboard/useDashboardFailedRuns.d.ts.map +1 -1
- package/dashboard/useDashboardFailedRuns.js +2 -0
- package/dashboard/useDashboardFailedRuns.js.map +1 -1
- 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 +4 -2
- package/index.d.ts.map +1 -1
- package/index.js +10 -1
- 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/library/useExportResource.d.ts +4 -3
- package/library/useExportResource.d.ts.map +1 -1
- package/library/useExportResource.js +1 -1
- package/library/useExportResource.js.map +1 -1
- package/models/ModelSelector.d.ts +26 -1
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +27 -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 +10 -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/organization/OrgProvider.d.ts +11 -4
- package/organization/OrgProvider.d.ts.map +1 -1
- package/organization/OrgProvider.js +28 -4
- package/organization/OrgProvider.js.map +1 -1
- package/package.json +4 -4
- package/resource-detail/useDeleteResource.d.ts +1 -1
- package/resource-detail/useDeleteResource.d.ts.map +1 -1
- package/resource-detail/useDeleteResource.js +5 -0
- package/resource-detail/useDeleteResource.js.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts +10 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.js +6 -2
- package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.d.ts +8 -0
- package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.js +2 -2
- package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
- package/schedule/CadenceField.d.ts +31 -0
- package/schedule/CadenceField.d.ts.map +1 -0
- package/schedule/CadenceField.js +116 -0
- package/schedule/CadenceField.js.map +1 -0
- package/schedule/ScheduleDetailView.d.ts +81 -0
- package/schedule/ScheduleDetailView.d.ts.map +1 -0
- package/schedule/ScheduleDetailView.js +648 -0
- package/schedule/ScheduleDetailView.js.map +1 -0
- package/schedule/ScheduleForm.d.ts +46 -0
- package/schedule/ScheduleForm.d.ts.map +1 -0
- package/schedule/ScheduleForm.js +214 -0
- package/schedule/ScheduleForm.js.map +1 -0
- package/schedule/ScheduleRowActions.d.ts +20 -0
- package/schedule/ScheduleRowActions.d.ts.map +1 -0
- package/schedule/ScheduleRowActions.js +54 -0
- package/schedule/ScheduleRowActions.js.map +1 -0
- 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/TimeZoneField.d.ts +32 -0
- package/schedule/TimeZoneField.d.ts.map +1 -0
- package/schedule/TimeZoneField.js +104 -0
- package/schedule/TimeZoneField.js.map +1 -0
- package/schedule/cadence.d.ts +95 -0
- package/schedule/cadence.d.ts.map +1 -0
- package/schedule/cadence.js +242 -0
- package/schedule/cadence.js.map +1 -0
- package/schedule/index.d.ts +37 -0
- package/schedule/index.d.ts.map +1 -0
- package/schedule/index.js +19 -0
- package/schedule/index.js.map +1 -0
- package/schedule/scheduleColumns.d.ts +26 -0
- package/schedule/scheduleColumns.d.ts.map +1 -0
- package/schedule/scheduleColumns.js +77 -0
- package/schedule/scheduleColumns.js.map +1 -0
- package/schedule/scheduleListFn.d.ts +50 -0
- package/schedule/scheduleListFn.d.ts.map +1 -0
- package/schedule/scheduleListFn.js +65 -0
- package/schedule/scheduleListFn.js.map +1 -0
- package/schedule/scheduleState.d.ts +43 -0
- package/schedule/scheduleState.d.ts.map +1 -0
- package/schedule/scheduleState.js +67 -0
- package/schedule/scheduleState.js.map +1 -0
- package/schedule/useCreateSchedule.d.ts +57 -0
- package/schedule/useCreateSchedule.d.ts.map +1 -0
- package/schedule/useCreateSchedule.js +61 -0
- package/schedule/useCreateSchedule.js.map +1 -0
- package/schedule/useResumeSchedule.d.ts +30 -0
- package/schedule/useResumeSchedule.d.ts.map +1 -0
- package/schedule/useResumeSchedule.js +45 -0
- package/schedule/useResumeSchedule.js.map +1 -0
- package/schedule/useSchedule.d.ts +39 -0
- package/schedule/useSchedule.d.ts.map +1 -0
- package/schedule/useSchedule.js +44 -0
- package/schedule/useSchedule.js.map +1 -0
- package/schedule/useScheduleCount.d.ts +38 -0
- package/schedule/useScheduleCount.d.ts.map +1 -0
- package/schedule/useScheduleCount.js +30 -0
- package/schedule/useScheduleCount.js.map +1 -0
- package/schedule/useScheduleList.d.ts +48 -0
- package/schedule/useScheduleList.d.ts.map +1 -0
- package/schedule/useScheduleList.js +43 -0
- package/schedule/useScheduleList.js.map +1 -0
- 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/useSetScheduleEnabled.d.ts +31 -0
- package/schedule/useSetScheduleEnabled.d.ts.map +1 -0
- package/schedule/useSetScheduleEnabled.js +53 -0
- package/schedule/useSetScheduleEnabled.js.map +1 -0
- package/schedule/useTriggerSchedule.d.ts +41 -0
- package/schedule/useTriggerSchedule.d.ts.map +1 -0
- package/schedule/useTriggerSchedule.js +62 -0
- package/schedule/useTriggerSchedule.js.map +1 -0
- 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/search/useResourceCount.d.ts +2 -1
- package/search/useResourceCount.d.ts.map +1 -1
- package/search/useResourceCount.js +6 -1
- package/search/useResourceCount.js.map +1 -1
- 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/src/agent/AgentPicker.tsx +13 -1
- package/src/billing/CreditLedgerTable.tsx +3 -42
- package/src/channel/AgentChannelsPanel.tsx +43 -1
- package/src/channel/ChannelTemplatesDialog.tsx +433 -0
- package/src/channel/__tests__/AgentChannelsPanel.test.tsx +73 -0
- package/src/channel/__tests__/ChannelTemplatesDialog.test.tsx +292 -0
- package/src/channel/__tests__/providers.test.ts +9 -0
- package/src/channel/__tests__/templatePresentation.test.ts +96 -0
- package/src/channel/__tests__/useChannelTemplateList.test.tsx +216 -0
- package/src/channel/__tests__/useChannelTemplateReadiness.test.tsx +124 -0
- package/src/channel/index.ts +15 -0
- package/src/channel/providers.ts +26 -2
- package/src/channel/templatePresentation.ts +82 -0
- package/src/channel/useChannelTemplateList.ts +123 -0
- package/src/channel/useChannelTemplateReadiness.ts +69 -0
- package/src/composer/ComposerToolbar.tsx +9 -0
- package/src/composer/SessionComposer.tsx +24 -1
- package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +156 -0
- package/src/dashboard/__tests__/useDashboardFailedRuns.test.tsx +106 -0
- package/src/dashboard/useDashboardFailedRuns.ts +2 -0
- package/src/execution/useCreateAgentExecution.ts +18 -0
- package/src/index.ts +70 -0
- package/src/internal/Pagination.tsx +72 -0
- package/src/library/useExportResource.ts +4 -3
- package/src/models/ModelSelector.tsx +100 -3
- package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +172 -0
- package/src/models/__tests__/useModelRegistry.test.tsx +15 -0
- package/src/models/index.ts +2 -0
- package/src/models/registry.ts +16 -0
- package/src/models/service-tier.ts +49 -0
- package/src/organization/OrgProvider.tsx +29 -4
- package/src/organization/__tests__/OrgProvider.orgSwitch.test.tsx +89 -0
- package/src/resource-detail/useDeleteResource.ts +11 -1
- package/src/resource-workbench/__tests__/ResourceWorkbench.test.tsx +112 -0
- package/src/resource-workbench/components/ResourceWorkbench.tsx +32 -15
- package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
- package/src/schedule/CadenceField.tsx +365 -0
- package/src/schedule/ScheduleDetailView.tsx +1651 -0
- package/src/schedule/ScheduleForm.tsx +615 -0
- package/src/schedule/ScheduleRowActions.tsx +124 -0
- package/src/schedule/ScheduleRunsTable.tsx +424 -0
- package/src/schedule/TimeZoneField.tsx +199 -0
- package/src/schedule/__tests__/ScheduleDetailView.test.tsx +683 -0
- package/src/schedule/__tests__/cadence.test.ts +278 -0
- package/src/schedule/__tests__/scheduleColumnsRowActions.test.tsx +176 -0
- package/src/schedule/__tests__/scheduleCreation.test.tsx +537 -0
- package/src/schedule/__tests__/scheduleHooks.test.tsx +399 -0
- package/src/schedule/__tests__/scheduleListFn.test.ts +128 -0
- package/src/schedule/__tests__/scheduleState.test.ts +133 -0
- package/src/schedule/cadence.ts +298 -0
- package/src/schedule/index.ts +69 -0
- package/src/schedule/scheduleColumns.tsx +118 -0
- package/src/schedule/scheduleListFn.ts +108 -0
- package/src/schedule/scheduleState.ts +96 -0
- package/src/schedule/useCreateSchedule.ts +88 -0
- package/src/schedule/useResumeSchedule.ts +67 -0
- package/src/schedule/useSchedule.ts +67 -0
- package/src/schedule/useScheduleCount.ts +57 -0
- package/src/schedule/useScheduleList.ts +83 -0
- package/src/schedule/useScheduleRuns.ts +93 -0
- package/src/schedule/useSetScheduleEnabled.ts +84 -0
- package/src/schedule/useTriggerSchedule.ts +90 -0
- package/src/schedule/useUpdateScheduleSpec.ts +108 -0
- package/src/search/__tests__/useResourceCount.test.tsx +89 -0
- package/src/search/useResourceCount.ts +8 -2
- package/src/session/useNewSessionFlow.ts +1 -0
- package/src/session/useSessionConversation.ts +12 -0
- package/src/session/useSessionPageFlow.ts +1 -0
- package/src/workflow/WorkflowDashboard.tsx +1 -0
- 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/src/workflow/useWorkflowExecutionList.ts +13 -2
- package/styles.css +1 -1
- package/workflow/CanvasTransitionEdge.d.ts +1 -1
- package/workflow/WorkflowDashboard.d.ts.map +1 -1
- package/workflow/WorkflowDashboard.js +1 -0
- package/workflow/WorkflowDashboard.js.map +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
- package/workflow/useWorkflowExecutionList.d.ts +10 -0
- package/workflow/useWorkflowExecutionList.d.ts.map +1 -1
- package/workflow/useWorkflowExecutionList.js +3 -2
- package/workflow/useWorkflowExecutionList.js.map +1 -1
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef, type ReactNode } from "react";
|
|
4
|
+
import { ExternalLink, LayoutTemplate, X } from "lucide-react";
|
|
5
|
+
import { cn } from "@stigmer/theme";
|
|
6
|
+
import { getUserMessage } from "@stigmer/sdk";
|
|
7
|
+
import type { AgentChannel } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
|
|
8
|
+
import type { ChannelTemplate } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
|
|
9
|
+
import { Button } from "../button/Button.js";
|
|
10
|
+
import { EmptyState } from "../empty-state/EmptyState.js";
|
|
11
|
+
import { CloudFeatureNotice } from "../internal/CloudFeatureNotice.js";
|
|
12
|
+
import { StatusBadge } from "../resource-workbench/components/StatusBadge.js";
|
|
13
|
+
import {
|
|
14
|
+
channelPresentationOf,
|
|
15
|
+
DEFAULT_CHANNEL_PRESENTATION,
|
|
16
|
+
} from "./providerPresentation.js";
|
|
17
|
+
import {
|
|
18
|
+
splitTemplateBody,
|
|
19
|
+
templateStatusPhase,
|
|
20
|
+
} from "./templatePresentation.js";
|
|
21
|
+
import { useChannelTemplateList } from "./useChannelTemplateList.js";
|
|
22
|
+
import { useChannelTemplateReadiness } from "./useChannelTemplateReadiness.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Where businesses author and manage WhatsApp message templates — the
|
|
26
|
+
* provider's own console. Stigmer keeps no copy of the list (DD-003:
|
|
27
|
+
* the provider is the registry), so this surface links out rather than
|
|
28
|
+
* offering any editing of its own.
|
|
29
|
+
*/
|
|
30
|
+
const WHATSAPP_MANAGER_TEMPLATES_URL =
|
|
31
|
+
"https://business.facebook.com/wa/manage/message-templates/";
|
|
32
|
+
|
|
33
|
+
/** Props for {@link ChannelTemplatesDialog}. */
|
|
34
|
+
export interface ChannelTemplatesDialogProps {
|
|
35
|
+
/** Whether the dialog is open. */
|
|
36
|
+
readonly open: boolean;
|
|
37
|
+
/** Called when the dialog requests an open-state change. */
|
|
38
|
+
readonly onOpenChange: (open: boolean) => void;
|
|
39
|
+
/** The channel whose provider template registry is listed. */
|
|
40
|
+
readonly channel: AgentChannel;
|
|
41
|
+
/**
|
|
42
|
+
* Opens the channel's YAML editor — the only place
|
|
43
|
+
* `proactive_messaging_enabled` can be granted today. Wired by
|
|
44
|
+
* {@link AgentChannelsPanel} to its Edit YAML dialog; when absent,
|
|
45
|
+
* the teaching state renders without the affordance.
|
|
46
|
+
*/
|
|
47
|
+
readonly onEditYaml?: () => void;
|
|
48
|
+
/**
|
|
49
|
+
* When `false`, renders as an in-flow open dialog instead of a
|
|
50
|
+
* top-layer modal — for embedding in constrained surfaces
|
|
51
|
+
* (documentation demos, visual tests). Interactive hosts keep the
|
|
52
|
+
* default.
|
|
53
|
+
* @default true
|
|
54
|
+
*/
|
|
55
|
+
readonly modal?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Lists a channel's message templates as the provider's registry
|
|
60
|
+
* reports them — for WhatsApp, the templates a business authored in
|
|
61
|
+
* WhatsApp Manager, each with its approval status.
|
|
62
|
+
*
|
|
63
|
+
* This is the diagnosis surface for business-initiated messaging: it is
|
|
64
|
+
* the one place that shows the provider's rejection copy AND Stigmer's
|
|
65
|
+
* own `unsupported_reason` (why an approved template still cannot be
|
|
66
|
+
* sent by the agent). The runner's prompt deliberately filters
|
|
67
|
+
* unsendable templates rather than annotating them, so a template
|
|
68
|
+
* "missing" from the agent's repertoire is explained here and nowhere
|
|
69
|
+
* else.
|
|
70
|
+
*
|
|
71
|
+
* The dialog is a window onto the provider, never an editor: templates
|
|
72
|
+
* are authored and approved in WhatsApp Manager, and the empty state
|
|
73
|
+
* says so. Before fetching, a readiness pre-check answers the
|
|
74
|
+
* preconditions the channel resource already knows (cloud runtime,
|
|
75
|
+
* installed, serving, proactive grant) as teaching states instead of
|
|
76
|
+
* relayed refusals; once a fetch does fail, the server's message
|
|
77
|
+
* renders verbatim.
|
|
78
|
+
*
|
|
79
|
+
* Built on the native `<dialog>` element, matching the SDK's modal
|
|
80
|
+
* convention. Most hosts mount it via {@link AgentChannelsPanel}'s
|
|
81
|
+
* channel card action menu, which shows the action only for providers
|
|
82
|
+
* whose descriptor declares `supportsMessageTemplates`.
|
|
83
|
+
*/
|
|
84
|
+
export function ChannelTemplatesDialog({
|
|
85
|
+
open,
|
|
86
|
+
onOpenChange,
|
|
87
|
+
channel,
|
|
88
|
+
onEditYaml,
|
|
89
|
+
modal = true,
|
|
90
|
+
}: ChannelTemplatesDialogProps) {
|
|
91
|
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
92
|
+
|
|
93
|
+
const handleClose = useCallback(() => {
|
|
94
|
+
dialogRef.current?.close();
|
|
95
|
+
onOpenChange(false);
|
|
96
|
+
}, [onOpenChange]);
|
|
97
|
+
|
|
98
|
+
// Sync native dialog open state (matches the SDK dialog convention).
|
|
99
|
+
const prevOpenRef = useRef(false);
|
|
100
|
+
if (modal && open !== prevOpenRef.current) {
|
|
101
|
+
prevOpenRef.current = open;
|
|
102
|
+
if (open) {
|
|
103
|
+
requestAnimationFrame(() => {
|
|
104
|
+
if (dialogRef.current && !dialogRef.current.open) {
|
|
105
|
+
dialogRef.current.showModal();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
} else if (dialogRef.current?.open) {
|
|
109
|
+
dialogRef.current.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<dialog
|
|
115
|
+
ref={dialogRef}
|
|
116
|
+
open={modal ? undefined : open}
|
|
117
|
+
onClose={handleClose}
|
|
118
|
+
className={cn(
|
|
119
|
+
// Wider than the conversations dialog on purpose: template
|
|
120
|
+
// bodies are the content, and a narrow column shreds them.
|
|
121
|
+
"w-full max-w-2xl rounded-xl border border-border bg-popover p-0 shadow-xl",
|
|
122
|
+
modal ? "fixed inset-0 m-auto backdrop:bg-black/50" : "relative",
|
|
123
|
+
)}
|
|
124
|
+
aria-labelledby="channel-templates-title"
|
|
125
|
+
>
|
|
126
|
+
{/* Body mounts only while open so each opening fetches fresh —
|
|
127
|
+
approval statuses change on the provider's side at any time. */}
|
|
128
|
+
{open && (
|
|
129
|
+
<ChannelTemplatesDialogBody
|
|
130
|
+
channel={channel}
|
|
131
|
+
onEditYaml={onEditYaml}
|
|
132
|
+
onClose={handleClose}
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
</dialog>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function ChannelTemplatesDialogBody({
|
|
140
|
+
channel,
|
|
141
|
+
onEditYaml,
|
|
142
|
+
onClose,
|
|
143
|
+
}: {
|
|
144
|
+
readonly channel: AgentChannel;
|
|
145
|
+
readonly onEditYaml?: () => void;
|
|
146
|
+
readonly onClose: () => void;
|
|
147
|
+
}) {
|
|
148
|
+
const channelName =
|
|
149
|
+
channel.metadata?.name || channel.metadata?.slug || "this channel";
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div className="flex max-h-[75vh] flex-col">
|
|
153
|
+
<div className="flex items-start justify-between gap-3 border-b border-border px-5 py-4">
|
|
154
|
+
<div className="min-w-0">
|
|
155
|
+
<h2
|
|
156
|
+
id="channel-templates-title"
|
|
157
|
+
className="text-sm font-semibold text-popover-foreground"
|
|
158
|
+
>
|
|
159
|
+
Templates
|
|
160
|
+
</h2>
|
|
161
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
162
|
+
The message templates WhatsApp holds for {channelName} — authored
|
|
163
|
+
and approved in WhatsApp Manager, listed here as WhatsApp reports
|
|
164
|
+
them.
|
|
165
|
+
</p>
|
|
166
|
+
</div>
|
|
167
|
+
<Button
|
|
168
|
+
variant="ghost"
|
|
169
|
+
size="xs"
|
|
170
|
+
onClick={onClose}
|
|
171
|
+
aria-label="Close templates"
|
|
172
|
+
>
|
|
173
|
+
<X className="size-4" />
|
|
174
|
+
</Button>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div className="min-h-0 flex-1 overflow-y-auto p-4">
|
|
178
|
+
<ChannelTemplatesContent channel={channel} onEditYaml={onEditYaml} />
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The readiness gate: each precondition the channel resource already
|
|
186
|
+
* answers renders as a teaching state without a network call — the
|
|
187
|
+
* courtesy-pre-check posture (project DD-007 D4). Only `ready` fetches;
|
|
188
|
+
* the server stays authoritative for everything past this point.
|
|
189
|
+
*/
|
|
190
|
+
function ChannelTemplatesContent({
|
|
191
|
+
channel,
|
|
192
|
+
onEditYaml,
|
|
193
|
+
}: {
|
|
194
|
+
readonly channel: AgentChannel;
|
|
195
|
+
readonly onEditYaml?: () => void;
|
|
196
|
+
}) {
|
|
197
|
+
const readiness = useChannelTemplateReadiness(channel);
|
|
198
|
+
const presentation =
|
|
199
|
+
channelPresentationOf(channel.spec?.providerConfig?.case) ??
|
|
200
|
+
DEFAULT_CHANNEL_PRESENTATION;
|
|
201
|
+
|
|
202
|
+
switch (readiness.status) {
|
|
203
|
+
case "cloud-only":
|
|
204
|
+
return (
|
|
205
|
+
<CloudFeatureNotice>
|
|
206
|
+
Message templates come from the provider through Stigmer
|
|
207
|
+
Cloud's channel runtime. Channel configuration still works
|
|
208
|
+
here, but the template registry can only be read on a cloud
|
|
209
|
+
deployment.
|
|
210
|
+
</CloudFeatureNotice>
|
|
211
|
+
);
|
|
212
|
+
case "not-installed":
|
|
213
|
+
// describeChannel already carries the right sentence per provider
|
|
214
|
+
// and per install state (pending vs revoked) — the card's copy,
|
|
215
|
+
// reused so the two surfaces cannot drift.
|
|
216
|
+
return (
|
|
217
|
+
<EmptyState
|
|
218
|
+
variant="first-use"
|
|
219
|
+
icon={<LayoutTemplate className="size-8" />}
|
|
220
|
+
title="Connect this channel first"
|
|
221
|
+
description={`${presentation.describeChannel(channel)} Templates become readable once the connection is complete.`}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
case "channel-off":
|
|
225
|
+
return (
|
|
226
|
+
<EmptyState
|
|
227
|
+
variant="first-use"
|
|
228
|
+
icon={<LayoutTemplate className="size-8" />}
|
|
229
|
+
title="This channel is turned off"
|
|
230
|
+
description="Turn the channel on with the switch on its card, then check back here."
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
233
|
+
case "not-proactive":
|
|
234
|
+
return (
|
|
235
|
+
<EmptyState
|
|
236
|
+
variant="first-use"
|
|
237
|
+
icon={<LayoutTemplate className="size-8" />}
|
|
238
|
+
title="Business-initiated messaging is not enabled"
|
|
239
|
+
description={
|
|
240
|
+
"Templates power messages your agent sends first — reminders, " +
|
|
241
|
+
"invoices, notifications. To allow them, set " +
|
|
242
|
+
"proactive_messaging_enabled on this channel's definition."
|
|
243
|
+
}
|
|
244
|
+
>
|
|
245
|
+
{onEditYaml && (
|
|
246
|
+
<Button variant="primary" onClick={onEditYaml}>
|
|
247
|
+
Edit channel YAML
|
|
248
|
+
</Button>
|
|
249
|
+
)}
|
|
250
|
+
</EmptyState>
|
|
251
|
+
);
|
|
252
|
+
case "ready":
|
|
253
|
+
return <ChannelTemplatesList channel={channel} />;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function ChannelTemplatesList({
|
|
258
|
+
channel,
|
|
259
|
+
}: {
|
|
260
|
+
readonly channel: AgentChannel;
|
|
261
|
+
}) {
|
|
262
|
+
const { templates, isLoading, error } = useChannelTemplateList(
|
|
263
|
+
channel.metadata?.slug ?? "",
|
|
264
|
+
channel.metadata?.org ?? "",
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (isLoading) {
|
|
268
|
+
return <TemplatesSkeleton />;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (error) {
|
|
272
|
+
// The server's copy is the teaching state past the pre-check —
|
|
273
|
+
// its refusals name the fix (scope, token, app binding) verbatim.
|
|
274
|
+
return (
|
|
275
|
+
<p className="px-2 py-6 text-center text-sm text-destructive">
|
|
276
|
+
{getUserMessage(error)}
|
|
277
|
+
</p>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (templates.length === 0) {
|
|
282
|
+
// "found", never "you have none": an empty success is
|
|
283
|
+
// indistinguishable from a filtered or truncated one on the wire.
|
|
284
|
+
return (
|
|
285
|
+
<EmptyState
|
|
286
|
+
variant="first-use"
|
|
287
|
+
icon={<LayoutTemplate className="size-8" />}
|
|
288
|
+
title="No templates found for this channel"
|
|
289
|
+
description={
|
|
290
|
+
"Templates are authored in WhatsApp Manager and approved by " +
|
|
291
|
+
"WhatsApp — Stigmer keeps no copy. Once a template is approved, " +
|
|
292
|
+
"it appears here and in your agent's repertoire automatically."
|
|
293
|
+
}
|
|
294
|
+
>
|
|
295
|
+
<WhatsAppManagerLink label="Open WhatsApp Manager" />
|
|
296
|
+
</EmptyState>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const sendable = templates.filter(
|
|
301
|
+
(t) => t.status === "APPROVED" && t.unsupportedReason === "",
|
|
302
|
+
).length;
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<div>
|
|
306
|
+
<div className="flex items-center justify-between gap-3 px-1 pb-3">
|
|
307
|
+
<p className="text-xs text-muted-foreground">
|
|
308
|
+
{templates.length} {templates.length === 1 ? "template" : "templates"}
|
|
309
|
+
{" \u00b7 "}
|
|
310
|
+
{sendable} ready to send
|
|
311
|
+
</p>
|
|
312
|
+
<WhatsAppManagerLink label="Manage in WhatsApp Manager" />
|
|
313
|
+
</div>
|
|
314
|
+
<ul className="space-y-2">
|
|
315
|
+
{templates.map((template) => (
|
|
316
|
+
<TemplateRow
|
|
317
|
+
key={`${template.name}\u0000${template.language}`}
|
|
318
|
+
template={template}
|
|
319
|
+
/>
|
|
320
|
+
))}
|
|
321
|
+
</ul>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// Row — one (name, language) template as the provider reports it
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
function TemplateRow({ template }: { readonly template: ChannelTemplate }) {
|
|
331
|
+
return (
|
|
332
|
+
<li className="rounded-lg border border-border p-3">
|
|
333
|
+
<div className="flex items-start justify-between gap-3">
|
|
334
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
335
|
+
<span
|
|
336
|
+
className="truncate text-sm font-medium text-foreground"
|
|
337
|
+
title={template.name}
|
|
338
|
+
>
|
|
339
|
+
{template.name}
|
|
340
|
+
</span>
|
|
341
|
+
{/* (name, language) is the template's identity — each
|
|
342
|
+
language is approved independently, so it always shows. */}
|
|
343
|
+
<span className="shrink-0 text-xs text-muted-foreground">
|
|
344
|
+
{template.language}
|
|
345
|
+
</span>
|
|
346
|
+
{template.category && <FactChip>{template.category}</FactChip>}
|
|
347
|
+
{template.headerFormat && (
|
|
348
|
+
<FactChip>{template.headerFormat} header</FactChip>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
<StatusBadge
|
|
352
|
+
phase={templateStatusPhase(template.status)}
|
|
353
|
+
label={template.status}
|
|
354
|
+
className="shrink-0"
|
|
355
|
+
/>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{template.bodyText && <TemplateBody body={template.bodyText} />}
|
|
359
|
+
|
|
360
|
+
{template.rejectionReason && (
|
|
361
|
+
<p className="mt-2 text-xs text-destructive">
|
|
362
|
+
Rejected: {template.rejectionReason}
|
|
363
|
+
</p>
|
|
364
|
+
)}
|
|
365
|
+
{template.unsupportedReason && (
|
|
366
|
+
<p className="mt-2 text-xs text-warning" role="status">
|
|
367
|
+
Not sendable: {template.unsupportedReason}
|
|
368
|
+
</p>
|
|
369
|
+
)}
|
|
370
|
+
</li>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The body verbatim, with `{{...}}` placeholders set off visually —
|
|
376
|
+
* they are the parts the agent fills at send time, and the reason the
|
|
377
|
+
* full text is on the wire at all.
|
|
378
|
+
*/
|
|
379
|
+
function TemplateBody({ body }: { readonly body: string }) {
|
|
380
|
+
return (
|
|
381
|
+
<p className="mt-2 whitespace-pre-wrap text-xs leading-relaxed text-muted-foreground">
|
|
382
|
+
{splitTemplateBody(body).map((segment, i) =>
|
|
383
|
+
segment.kind === "placeholder" ? (
|
|
384
|
+
<code
|
|
385
|
+
key={i}
|
|
386
|
+
className="rounded bg-muted-subtle px-1 font-mono text-foreground"
|
|
387
|
+
>
|
|
388
|
+
{segment.value}
|
|
389
|
+
</code>
|
|
390
|
+
) : (
|
|
391
|
+
<span key={i}>{segment.value}</span>
|
|
392
|
+
),
|
|
393
|
+
)}
|
|
394
|
+
</p>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Provider-verbatim fact rendered small and quiet (category, header). */
|
|
399
|
+
function FactChip({ children }: { readonly children: ReactNode }) {
|
|
400
|
+
return (
|
|
401
|
+
<span className="shrink-0 rounded bg-muted-subtle px-1.5 py-0.5 text-xs text-muted-foreground">
|
|
402
|
+
{children}
|
|
403
|
+
</span>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function WhatsAppManagerLink({ label }: { readonly label: string }) {
|
|
408
|
+
return (
|
|
409
|
+
<a
|
|
410
|
+
href={WHATSAPP_MANAGER_TEMPLATES_URL}
|
|
411
|
+
target="_blank"
|
|
412
|
+
rel="noreferrer noopener"
|
|
413
|
+
className={cn(
|
|
414
|
+
"inline-flex shrink-0 items-center gap-1 text-xs font-medium text-foreground",
|
|
415
|
+
"underline underline-offset-2 hover:text-muted-foreground",
|
|
416
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded",
|
|
417
|
+
)}
|
|
418
|
+
>
|
|
419
|
+
{label}
|
|
420
|
+
<ExternalLink aria-hidden="true" className="size-3" />
|
|
421
|
+
</a>
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function TemplatesSkeleton() {
|
|
426
|
+
return (
|
|
427
|
+
<div className="space-y-2 p-1">
|
|
428
|
+
{[1, 2, 3].map((i) => (
|
|
429
|
+
<div key={i} className="h-20 animate-pulse rounded-lg bg-muted-faint" />
|
|
430
|
+
))}
|
|
431
|
+
</div>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
@@ -613,6 +613,79 @@ describe("AgentChannelsPanel", () => {
|
|
|
613
613
|
expect(screen.queryByText(/including credentials/i)).toBeNull();
|
|
614
614
|
});
|
|
615
615
|
|
|
616
|
+
it("offers Templates on WhatsApp cards and opens the dialog from the menu", async () => {
|
|
617
|
+
const client = createMockStigmer({
|
|
618
|
+
channels: [makeWhatsAppChannel()],
|
|
619
|
+
});
|
|
620
|
+
render(
|
|
621
|
+
<Providers client={client}>
|
|
622
|
+
<AgentChannelsPanel agent={makeAgent()} />
|
|
623
|
+
</Providers>,
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
await waitFor(() =>
|
|
627
|
+
expect(screen.getByText("Support WhatsApp")).toBeTruthy(),
|
|
628
|
+
);
|
|
629
|
+
fireEvent.click(screen.getByRole("button", { name: /actions for/i }));
|
|
630
|
+
fireEvent.click(await screen.findByRole("menuitem", { name: "Templates" }));
|
|
631
|
+
|
|
632
|
+
expect(
|
|
633
|
+
await screen.findByRole("heading", { name: "Templates" }),
|
|
634
|
+
).toBeTruthy();
|
|
635
|
+
// This channel has no proactive grant — the dialog teaches instead
|
|
636
|
+
// of firing a doomed listTemplates call (project DD-007 D4).
|
|
637
|
+
expect(
|
|
638
|
+
screen.getByText("Business-initiated messaging is not enabled"),
|
|
639
|
+
).toBeTruthy();
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it("offers no Templates item on providers without a template registry", async () => {
|
|
643
|
+
const client = createMockStigmer({
|
|
644
|
+
channels: [makeChannel({ teamName: "Acme HQ" })],
|
|
645
|
+
});
|
|
646
|
+
render(
|
|
647
|
+
<Providers client={client}>
|
|
648
|
+
<AgentChannelsPanel agent={makeAgent()} />
|
|
649
|
+
</Providers>,
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
await waitFor(() => expect(screen.getByText("Support Slack")).toBeTruthy());
|
|
653
|
+
fireEvent.click(screen.getByRole("button", { name: /actions for/i }));
|
|
654
|
+
|
|
655
|
+
// Slack has no template concept: hidden, not disabled — a disabled
|
|
656
|
+
// item would imply the capability might one day exist there.
|
|
657
|
+
expect(
|
|
658
|
+
await screen.findByRole("menuitem", { name: /conversations/i }),
|
|
659
|
+
).toBeTruthy();
|
|
660
|
+
expect(screen.queryByRole("menuitem", { name: "Templates" })).toBeNull();
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it("hides Templates from viewers — the server's listTemplates bar is can_edit", async () => {
|
|
664
|
+
const checkMyPermission = vi
|
|
665
|
+
.fn()
|
|
666
|
+
.mockResolvedValue({ isAuthorized: false });
|
|
667
|
+
const client = createMockStigmer({
|
|
668
|
+
channels: [makeWhatsAppChannel()],
|
|
669
|
+
checkMyPermission,
|
|
670
|
+
});
|
|
671
|
+
render(
|
|
672
|
+
<Providers client={client}>
|
|
673
|
+
<AgentChannelsPanel agent={makeAgent()} />
|
|
674
|
+
</Providers>,
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
await waitFor(() =>
|
|
678
|
+
expect(screen.getByText("Support WhatsApp")).toBeTruthy(),
|
|
679
|
+
);
|
|
680
|
+
await waitFor(() => expect(checkMyPermission).toHaveBeenCalled());
|
|
681
|
+
fireEvent.click(screen.getByRole("button", { name: /actions for/i }));
|
|
682
|
+
|
|
683
|
+
expect(
|
|
684
|
+
await screen.findByRole("menuitem", { name: /conversations/i }),
|
|
685
|
+
).toBeTruthy();
|
|
686
|
+
expect(screen.queryByRole("menuitem", { name: "Templates" })).toBeNull();
|
|
687
|
+
});
|
|
688
|
+
|
|
616
689
|
it("renders an error state when the list fails to load", async () => {
|
|
617
690
|
const client = {
|
|
618
691
|
agentChannel: {
|