diff --git a/apps/sim/app/(landing)/integrations/(shell)/page.tsx b/apps/sim/app/(landing)/integrations/(shell)/page.tsx index c2a5dfd1356..75760b22f41 100644 --- a/apps/sim/app/(landing)/integrations/(shell)/page.tsx +++ b/apps/sim/app/(landing)/integrations/(shell)/page.tsx @@ -6,9 +6,9 @@ import { type FAQItem, INTEGRATIONS, type Integration, - POPULAR_WORKFLOWS, toIntegrationSummary, } from '@/lib/integrations' +import { POPULAR_WORKFLOWS } from '@/lib/integrations/popular-workflows' import { withFilteredNoindex } from '@/lib/landing/seo' import { JsonLd } from '@/app/(landing)/components/json-ld' import { LandingFAQ } from '@/app/(landing)/components/landing-faq' diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.tsx index 4f261851c4e..cecd097fd81 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.tsx @@ -3,7 +3,7 @@ import { cn } from '@sim/emcn' import type { ReactNodeViewProps } from '@tiptap/react' import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react' import { useParams, useRouter } from 'next/navigation' -import { getBareIconStyle, type StyleableIcon } from '@/blocks/icon-color' +import { getBareIconStyle, type StyleableIcon } from '@/blocks/brand-icon-style' import { mentionIcon } from './mention-icon' import { MarkdownMention, type MentionAttrs } from './mention-node' import { simLinkPath } from './sim-link' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx index 233b0e8d202..50cec53dfa9 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx @@ -13,7 +13,7 @@ import { import { AgentSkillsIcon, McpIcon } from '@/components/icons' import { getDocumentIcon } from '@/components/icons/document-icons' import type { ChatContextKind, ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types' -import { getBareIconStyle } from '@/blocks/icon-color' +import { getBareIconStyle } from '@/blocks/brand-icon-style' import { getBlockRegistry } from '@/blocks/registry' interface RenderIconArgs { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx index 38afd89461b..4e07b66f747 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx @@ -12,7 +12,7 @@ import { import { getReadTargetBlock } from '@/lib/copilot/tools/client/read-block' import { extractStreamingStringArgument } from '@/lib/copilot/tools/streaming-args' import { getToolStatusDisplayTitle, getWaitCountdownTitle } from '@/lib/copilot/tools/tool-display' -import { getBareIconStyle } from '@/blocks/icon-color' +import { getBareIconStyle } from '@/blocks/brand-icon-style' import { getBlockByToolName } from '@/blocks/registry' import type { ToolCallStatus } from '../../../../types' import { resolveToolDisplayState } from '../../utils' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx index 1681f3a07b8..dafefd33d5d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx @@ -21,7 +21,7 @@ import type { MothershipResource, MothershipResourceType, } from '@/app/workspace/[workspaceId]/home/types' -import { getBareIconStyle, type StyleableIcon } from '@/blocks/icon-color' +import { getBareIconStyle, type StyleableIcon } from '@/blocks/brand-icon-style' import { logKeys } from '@/hooks/queries/logs' import { mothershipChatKeys } from '@/hooks/queries/mothership-chats' import { scheduleKeys } from '@/hooks/queries/schedules' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx index 736bf30fefa..c90e0795ee8 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx @@ -9,7 +9,6 @@ import { useParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { GmailIcon, SlackIcon } from '@/components/icons' import { - getAllBlockMeta, INTEGRATIONS, type OAuthServiceMatch, resolveOAuthServiceForIntegration, @@ -17,7 +16,8 @@ import { } from '@/lib/integrations' import { captureEvent } from '@/lib/posthog/client' import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal' -import { getBareIconStyle } from '@/blocks/icon-color' +import { getBareIconStyle } from '@/blocks/brand-icon-style' +import { getAllBlockMeta } from '@/blocks/registry' import type { ModuleTag } from '@/blocks/types' import { useWorkspaceCredentials } from '@/hooks/queries/credentials' import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 269993d45bd..f161f672f78 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -33,6 +33,7 @@ import type { WorkflowLogSummary, } from '@/lib/api/contracts/logs' import { dollarsToCredits } from '@/lib/billing/credits/conversion' +import { formatDateShort } from '@/lib/core/utils/date-display' import { getEndDateFromTimeRange, getStartDateFromTimeRange, @@ -91,7 +92,6 @@ import { DELETED_WORKFLOW_LABEL, extractRetryInput, formatDate, - formatDateShort, getDisplayStatus, type LogStatus, parseDuration, diff --git a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts index 01d336f082a..cadb003f19a 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts @@ -188,31 +188,6 @@ export function formatLatency(ms: number): string { return formatDuration(ms, { precision: 2 }) ?? '—' } -export function formatDateShort(dateStr: string): string { - const hasTime = dateStr.includes('T') - const [datePart, timePart] = dateStr.split('T') - const [, month, day] = datePart.split('-').map(Number) - const months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ] - const dateLabel = `${months[month - 1]} ${day}` - if (hasTime && timePart) { - return `${dateLabel} ${timePart.slice(0, 5)}` - } - return dateLabel -} - export const formatDate = (dateString: string) => { const date = new Date(dateString) return { diff --git a/apps/sim/app/workspace/[workspaceId]/settings/billing/credit-usage/credit-usage-view.tsx b/apps/sim/app/workspace/[workspaceId]/settings/billing/credit-usage/credit-usage-view.tsx index 320e41f16ff..2c9f36ab1db 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/billing/credit-usage/credit-usage-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/billing/credit-usage/credit-usage-view.tsx @@ -18,8 +18,8 @@ import { useRouter } from 'next/navigation' import { useQueryStates } from 'nuqs' import type { UsageLogEntry, UsageLogPeriod } from '@/lib/api/contracts/user' import { formatApportionedCreditCost, formatCreditsLabel } from '@/lib/billing/credits/conversion' +import { formatDateShort } from '@/lib/core/utils/date-display' import { USAGE_LOG_SOURCE_LABELS } from '@/app/api/users/me/usage-logs/source-labels' -import { formatDateShort } from '@/app/workspace/[workspaceId]/logs/utils' import { creditUsageParsers, creditUsageUrlKeys, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx index 466224ba63f..46910e0a005 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx @@ -24,7 +24,7 @@ import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider' -import { getBareIconStyle, type StyleableIcon } from '@/blocks/icon-color' +import { getBareIconStyle, type StyleableIcon } from '@/blocks/brand-icon-style' import type { SubBlockConfig } from '@/blocks/types' import { useWorkspaceCredential, useWorkspaceCredentials } from '@/hooks/queries/credentials' import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials' diff --git a/apps/sim/blocks/brand-icon-style.ts b/apps/sim/blocks/brand-icon-style.ts new file mode 100644 index 00000000000..4e29977cb23 --- /dev/null +++ b/apps/sim/blocks/brand-icon-style.ts @@ -0,0 +1,53 @@ +/** + * Brand-icon styling that needs the block registry, kept apart from the pure + * contrast helpers in `@/blocks/icon-color`. + * + * The split is by dependency, not by topic: this module reads `getAllBlocks()`, + * so importing it pulls `blocks/registry-maps` → all 282 block configs → the + * tool registry. `icon-color.ts` needs none of that, and it is imported by the + * public landing `/integrations` page — which was inheriting the whole registry + * purely because the two lived in one file. + * + * Every consumer here is under `app/workspace/**`, where the registry is + * already legitimately present. + */ + +import type { ComponentType, CSSProperties } from 'react' +import { getAllBlocks } from '@/blocks/registry' + +/** A brand icon component that accepts standard styling props. */ +export type StyleableIcon = ComponentType<{ className?: string; style?: CSSProperties }> + +/** + * Lazily-built lookup from a block's icon component to its theme-safe brand + * {@link BlockConfig.iconColor}. Keyed by component reference so callers that + * already hold the icon (suggested actions, credential pickers, …) never need + * to thread a block type or hand-pick a color. Built once on first read since + * the block registry is static for the app's lifetime. + */ +let iconColorByComponent: Map | null = null + +function getIconColorMap(): Map { + if (iconColorByComponent) return iconColorByComponent + const map = new Map() + for (const block of getAllBlocks()) { + if (block.iconColor) map.set(block.icon, block.iconColor) + } + iconColorByComponent = map + return map +} + +/** + * Inline `style` for rendering a brand icon bare (without its colored tile + * background): the block's theme-safe {@link BlockConfig.iconColor} as `color`, + * or `undefined` when none is defined so the caller keeps its own default + * icon styling. + * + * Single-fill icons drawn with `fill='currentColor'` (e.g. HubSpot) adopt the + * color; multi-color brand icons that hardcode their own fills (Slack, Gmail, + * Jira, Salesforce, Google Calendar) ignore it and keep their own colors. + */ +export function getBareIconStyle(icon: StyleableIcon): CSSProperties | undefined { + const color = getIconColorMap().get(icon) + return color ? { color } : undefined +} diff --git a/apps/sim/blocks/icon-color.ts b/apps/sim/blocks/icon-color.ts index 9e3030127ec..5332a473573 100644 --- a/apps/sim/blocks/icon-color.ts +++ b/apps/sim/blocks/icon-color.ts @@ -1,43 +1,10 @@ -import type { ComponentType, CSSProperties } from 'react' -import { isLightColor } from '@/lib/colors' -import { getAllBlocks } from '@/blocks/registry' - -/** A brand icon component that accepts standard styling props. */ -export type StyleableIcon = ComponentType<{ className?: string; style?: CSSProperties }> - /** - * Lazily-built lookup from a block's icon component to its theme-safe brand - * {@link BlockConfig.iconColor}. Keyed by component reference so callers that - * already hold the icon (suggested actions, credential pickers, …) never need - * to thread a block type or hand-pick a color. Built once on first read since - * the block registry is static for the app's lifetime. + * Contrast helpers for brand tiles. Pure colour maths — deliberately free of any + * `@/blocks/registry` import so the public landing `/integrations` page can use + * these without pulling 282 block configs and the tool registry into its bundle. + * Registry-backed icon styling lives in `@/blocks/brand-icon-style`. */ -let iconColorByComponent: Map | null = null - -function getIconColorMap(): Map { - if (iconColorByComponent) return iconColorByComponent - const map = new Map() - for (const block of getAllBlocks()) { - if (block.iconColor) map.set(block.icon, block.iconColor) - } - iconColorByComponent = map - return map -} - -/** - * Inline `style` for rendering a brand icon bare (without its colored tile - * background): the block's theme-safe {@link BlockConfig.iconColor} as `color`, - * or `undefined` when none is defined so the caller keeps its own default - * icon styling. - * - * Single-fill icons drawn with `fill='currentColor'` (e.g. HubSpot) adopt the - * color; multi-color brand icons that hardcode their own fills (Slack, Gmail, - * Jira, Salesforce, Google Calendar) ignore it and keep their own colors. - */ -export function getBareIconStyle(icon: StyleableIcon): CSSProperties | undefined { - const color = getIconColorMap().get(icon) - return color ? { color } : undefined -} +import { isLightColor } from '@/lib/colors' /** * Brightness above which a brand tile is "clearly light" and a white foreground diff --git a/apps/sim/ee/audit-logs/components/audit-logs.tsx b/apps/sim/ee/audit-logs/components/audit-logs.tsx index bb6fa438ecf..a7942f2a4bf 100644 --- a/apps/sim/ee/audit-logs/components/audit-logs.tsx +++ b/apps/sim/ee/audit-logs/components/audit-logs.tsx @@ -21,10 +21,10 @@ import { createLogger } from '@sim/logger' import { formatDateTime } from '@sim/utils/formatting' import { isRecordLike } from '@sim/utils/object' import { useQueryStates } from 'nuqs' +import { formatDateShort } from '@/lib/core/utils/date-display' import { getEndDateFromTimeRange, getStartDateFromTimeRange } from '@/lib/logs/filters' import { SEARCH_DEBOUNCE_MS } from '@/lib/url-state' import type { EnterpriseAuditLogEntry } from '@/app/api/v1/audit-logs/format' -import { formatDateShort } from '@/app/workspace/[workspaceId]/logs/utils' import { ActivityLog, type ActivityLogEntry, diff --git a/apps/sim/lib/core/utils/date-display.ts b/apps/sim/lib/core/utils/date-display.ts new file mode 100644 index 00000000000..e698dfbe423 --- /dev/null +++ b/apps/sim/lib/core/utils/date-display.ts @@ -0,0 +1,41 @@ +/** + * Compact date labels for dense table rows (`Jul 31`, `Jul 31 14:22`). + * + * Lives in `lib/` rather than beside the logs table because three unrelated + * features render it — logs, credit usage, and audit logs — and the previous + * home (`app/workspace/[workspaceId]/logs/utils.ts`) also exports registry-backed + * badge components. Importing a date formatter from there pulled + * `@/blocks/registry` → all 282 block configs → the tool registry into every + * consumer's bundle. + */ + +const MONTHS = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', +] as const + +/** + * Formats an ISO-ish date string as `MMM D`, appending `HH:mm` when the input + * carries a time component. Parsed by string split rather than `Date` so the + * label reflects the timestamp exactly as stored, with no timezone shifting. + */ +export function formatDateShort(dateStr: string): string { + const hasTime = dateStr.includes('T') + const [datePart, timePart] = dateStr.split('T') + const [, month, day] = datePart.split('-').map(Number) + const dateLabel = `${MONTHS[month - 1]} ${day}` + if (hasTime && timePart) { + return `${dateLabel} ${timePart.slice(0, 5)}` + } + return dateLabel +} diff --git a/apps/sim/lib/integrations/index.ts b/apps/sim/lib/integrations/index.ts index 12e58e2ff35..97228c7cf69 100644 --- a/apps/sim/lib/integrations/index.ts +++ b/apps/sim/lib/integrations/index.ts @@ -6,16 +6,16 @@ * `INTEGRATIONS` is the serialized projection of `BlockConfig` written by * `scripts/generate-docs.ts` whenever a block changes. * - * `POPULAR_WORKFLOWS` is derived from each block's `*BlockMeta` export (see - * `apps/sim/blocks/registry.ts`, which now hosts both the execution - * `BlockConfig` lookups and the presentation `BlockMeta` lookups). Block - * files are the source of truth for both surfaces. + * Deliberately free of `@/blocks` value imports: the landing `/integrations` + * grid is a client component, so anything imported here that reaches + * `blocks/registry-maps` ships all 282 block configs and the tool registry to a + * public marketing page. `POPULAR_WORKFLOWS` lives in + * `@/lib/integrations/popular-workflows` for exactly that reason. Type-only + * imports are fine — they erase. */ -import { stripVersionSuffix } from '@sim/utils/string' import integrationsJson from '@/lib/integrations/integrations.json' import type { Integration, IntegrationSummary } from '@/lib/integrations/types' -import { getAllBlockMeta } from '@/blocks/registry' /** All integrations surfaced in the catalog, ordered by `scripts/generate-docs.ts`. */ export const INTEGRATIONS: readonly Integration[] = @@ -28,45 +28,6 @@ export const INTEGRATIONS: readonly Integration[] = */ export const INTEGRATIONS_UPDATED_AT: string = integrationsJson.updatedAt -/** A curated `from → to` block-pair workflow surfaced on the landing page. */ -export interface PopularWorkflow { - /** Integration display name (matches `Integration.name`). */ - from: string - /** Integration display name. */ - to: string - headline: string - description: string -} - -const TYPE_TO_NAME = new Map() -for (const integration of INTEGRATIONS) { - TYPE_TO_NAME.set(integration.type, integration.name) - TYPE_TO_NAME.set(stripVersionSuffix(integration.type), integration.name) -} - -/** - * Curated popular workflow pairs (templates flagged `featured: true` that - * reference at least one other integration). Derived from per-block meta — - * each entry's `from` is the owner block, `to` is the first - * `alsoIntegrations` entry, and `headline`/`description` come from the - * template title and prompt. - */ -export const POPULAR_WORKFLOWS: readonly PopularWorkflow[] = (() => { - const pairs: PopularWorkflow[] = [] - for (const [ownerType, meta] of Object.entries(getAllBlockMeta())) { - for (const template of meta.templates ?? []) { - if (!template.featured) continue - const toType = template.alsoIntegrations?.[0] - if (!toType) continue - const from = TYPE_TO_NAME.get(ownerType) ?? TYPE_TO_NAME.get(stripVersionSuffix(ownerType)) - const to = TYPE_TO_NAME.get(toType) ?? TYPE_TO_NAME.get(stripVersionSuffix(toType)) - if (!from || !to) continue - pairs.push({ from, to, headline: template.title, description: template.prompt }) - } - } - return pairs -})() - /** * Projects a full `Integration` down to the fields the `/integrations` * catalog grid renders and searches by, replacing the full `operations`/ @@ -109,6 +70,5 @@ export { resolveOAuthServiceForSlug, } from '@/lib/integrations/oauth-service' export type { AuthType, FAQItem, Integration, IntegrationSummary } from '@/lib/integrations/types' -export { getAllBlockMeta, getBlockMeta, getTemplatesForBlock } from '@/blocks/registry' export type { BlockMeta, BlockTemplate } from '@/blocks/types' export { formatIntegrationType } from '@/blocks/types' diff --git a/apps/sim/lib/integrations/popular-workflows.ts b/apps/sim/lib/integrations/popular-workflows.ts new file mode 100644 index 00000000000..31ccd3816f7 --- /dev/null +++ b/apps/sim/lib/integrations/popular-workflows.ts @@ -0,0 +1,58 @@ +/** + * Curated `from → to` block-pair workflows for the landing page, derived from + * each block's `*BlockMeta` export. + * + * Split out of the `@/lib/integrations` barrel rather than living beside the + * rest of the catalog data, because building this list calls `getAllBlockMeta()` + * at module scope. That single call pulls `@/blocks/registry` → + * `blocks/registry-maps` → all 282 block configs → the tool registry, and any + * client component importing *anything* from the barrel inherited the whole + * graph. The landing `/integrations` grid is a client component, so a public + * marketing page was shipping the full registry to render a catalog of names + * and icons. + * + * Keeping it in its own module means the one server component that needs it + * imports it directly, and the barrel stays free of `@/blocks` value imports. + */ + +import { stripVersionSuffix } from '@sim/utils/string' +import { INTEGRATIONS } from '@/lib/integrations' +import { getAllBlockMeta } from '@/blocks/registry' + +/** A curated `from → to` block-pair workflow surfaced on the landing page. */ +export interface PopularWorkflow { + /** Integration display name (matches `Integration.name`). */ + from: string + /** Integration display name. */ + to: string + headline: string + description: string +} + +const TYPE_TO_NAME = new Map() +for (const integration of INTEGRATIONS) { + TYPE_TO_NAME.set(integration.type, integration.name) + TYPE_TO_NAME.set(stripVersionSuffix(integration.type), integration.name) +} + +/** + * Templates flagged `featured: true` that reference at least one other + * integration. Each entry's `from` is the owner block, `to` is the first + * `alsoIntegrations` entry, and `headline`/`description` come from the template + * title and prompt. + */ +export const POPULAR_WORKFLOWS: readonly PopularWorkflow[] = (() => { + const pairs: PopularWorkflow[] = [] + for (const [ownerType, meta] of Object.entries(getAllBlockMeta())) { + for (const template of meta.templates ?? []) { + if (!template.featured) continue + const toType = template.alsoIntegrations?.[0] + if (!toType) continue + const from = TYPE_TO_NAME.get(ownerType) ?? TYPE_TO_NAME.get(stripVersionSuffix(ownerType)) + const to = TYPE_TO_NAME.get(toType) ?? TYPE_TO_NAME.get(stripVersionSuffix(toType)) + if (!from || !to) continue + pairs.push({ from, to, headline: template.title, description: template.prompt }) + } + } + return pairs +})()