Skip to content
Merged
28 changes: 9 additions & 19 deletions apps/sim/app/(auth)/login/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
Expand Down Expand Up @@ -560,24 +559,15 @@ export default function LoginPage({
{resetStatus.type === 'error' ? resetStatus.message : null}
</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='filled'
flush
onClick={() => setForgotPasswordOpen(false)}
disabled={isSubmittingReset}
>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleForgotPassword}
disabled={!forgotPasswordEmail || isSubmittingReset}
>
{isSubmittingReset ? 'Sending…' : 'Send Reset Link'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => setForgotPasswordOpen(false)}
cancelDisabled={isSubmittingReset}
primaryAction={{
label: isSubmittingReset ? 'Sending…' : 'Send Reset Link',
onClick: handleForgotPassword,
disabled: !forgotPasswordEmail || isSubmittingReset,
}}
/>
</ChipModal>
</>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { cn } from '@/lib/core/utils/cn'
import { trackLandingCta } from '@/app/(landing)/landing-analytics'

interface TemplateCardButtonProps {
/**
* Curated template prompt, already rewritten to `@`-mention form by the
* page's server-side `mentionifyPromptForNames` (registry-free, so the
* landing client bundle never pulls the full block registry). Stored verbatim
* for the home input to consume after signup.
*/
prompt: string
className?: string
children: React.ReactNode
Expand Down
61 changes: 59 additions & 2 deletions apps/sim/app/(landing)/integrations/(shell)/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,37 @@ function sentenceWithTerminalPunctuation(value: string): string {
return /[.!?]$/.test(trimmedValue) ? trimmedValue : `${trimmedValue}.`
}

function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

/**
* Server-side rewrite of bare integration names in a curated template prompt
* to `@`-mention form (`Slack` → `@Slack`) so the prompt chips with brand
* icons once it is populated into the Mothership home input after signup —
* the home auto-mention pipeline only chips token-starting `@` mentions, so
* curated prompts must opt in.
*
* Unlike the workspace surface, which calls `mentionifyIntegrations` from
* `@/blocks/integration-matcher`, this runs only over the handful of names a
* template actually references (its owner + `otherBlockTypes`) and lives in a
* Server Component, so it never pulls the full block/tool/icon registry into
* the landing client bundle. Whole-token, longest-first matching with
* lookarounds mirrors the canonical matcher; idempotent on already-prefixed
* names.
*/
function mentionifyPromptForNames(prompt: string, names: readonly string[]): string {
const unique = [...new Set(names.filter((n) => n.trim().length >= 2))].sort(
(a, b) => b.length - a.length
)
if (unique.length === 0) return prompt
const regex = new RegExp(
`(?<![A-Za-z0-9_@])(${unique.map(escapeRegex).join('|')})(?![A-Za-z0-9_])`,
'gi'
)
return prompt.replace(regex, (match) => `@${match}`)
}

/**
* Generates targeted FAQs from integration metadata.
* Questions mirror real search queries to drive FAQPage rich snippets.
Expand Down Expand Up @@ -652,6 +683,32 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
...template.otherBlockTypes,
]

const resolveDisplayName = (bt: string): string | null => {
const resolvedBt = byType.get(bt)
? bt
: byType.get(`${bt}_v2`)
? `${bt}_v2`
: byType.get(`${bt}_v3`)
? `${bt}_v3`
: bt
return byType.get(resolvedBt)?.name ?? null
}

/**
* The curated template prompt rewritten so the integrations it
* references chip in the home input after signup. Computed
* server-side from the template's own integration set — never the
* full registry — so the visible card text stays raw while the
* stored prompt opts into mention treatment.
*/
const storedPrompt = (template: (typeof matchingTemplates)[number]) =>
mentionifyPromptForNames(
template.prompt,
resolveTypes(template)
.map(resolveDisplayName)
.filter((n): n is string => n !== null)
)

const renderIcons = (allTypes: string[]) =>
allTypes.map((bt, idx) => {
const resolvedBt = byType.get(bt)
Expand Down Expand Up @@ -698,7 +755,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
{row.map((template) => (
<TemplateCardButton
key={template.title}
prompt={template.prompt}
prompt={storedPrompt(template)}
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] border-t p-6 transition-colors first:border-t-0 hover:bg-[var(--landing-bg-elevated)] sm:border-t-0 sm:border-l sm:first:border-l-0'
>
<div className='flex items-center gap-1.5'>
Expand All @@ -724,7 +781,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
{lastTemplate && (
<>
<TemplateCardButton
prompt={lastTemplate.prompt}
prompt={storedPrompt(lastTemplate)}
className='group/link flex items-center gap-4 px-6 py-4 transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
<div className='flex items-center gap-1.5'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { useCallback, useState } from 'react'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
Expand Down Expand Up @@ -138,32 +137,22 @@ export function RequestIntegrationModal() {
)}
</ChipModalBody>

<ChipModalFooter>
{status === 'success' ? (
<Chip variant='primary' flush onClick={() => handleOpenChange(false)}>
Done
</Chip>
) : (
<>
<Chip
variant='filled'
flush
onClick={() => setOpen(false)}
disabled={status === 'submitting'}
>
Cancel
</Chip>
<Chip
variant='primary'
flush
onClick={handleSubmit}
disabled={!canSubmit && status !== 'error'}
>
{status === 'submitting' ? 'Submitting...' : 'Submit request'}
</Chip>
</>
)}
</ChipModalFooter>
{status === 'success' ? (
<ChipModalFooter
onCancel={() => handleOpenChange(false)}
primaryAction={{ label: 'Done', onClick: () => handleOpenChange(false) }}
/>
) : (
<ChipModalFooter
onCancel={() => setOpen(false)}
cancelDisabled={status === 'submitting'}
primaryAction={{
label: status === 'submitting' ? 'Submitting...' : 'Submit request',
onClick: handleSubmit,
disabled: !canSubmit && status !== 'error',
}}
/>
)}
</ChipModal>
</>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import {
Badge,
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
Expand Down Expand Up @@ -442,14 +441,15 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {

<ChipModalError>{submitError}</ChipModalError>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='ghost' onClick={handleClose} disabled={isPending}>
Cancel
</Chip>
<Chip variant='primary' onClick={handleConnect} disabled={isDisabled}>
{isPending ? 'Connecting...' : 'Connect'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={handleClose}
cancelDisabled={isPending}
primaryAction={{
label: isPending ? 'Connecting...' : 'Connect',
onClick: handleConnect,
disabled: isDisabled,
}}
/>
</ChipModal>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useCallback, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalField,
Expand Down Expand Up @@ -154,15 +153,14 @@ export function AddPeopleModal({ credentialId, open, onOpenChange }: AddPeopleMo
disabled={isAdding}
/>
</ChipModalBody>
<ChipModalFooter>
<Chip
variant='primary'
onClick={handleAddPeople}
disabled={emailsToAdd.length === 0 || isAdding}
>
{isAdding ? 'Adding...' : 'Add'}
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={handleClose}
primaryAction={{
label: isAdding ? 'Adding...' : 'Add',
onClick: handleAddPeople,
disabled: emailsToAdd.length === 0 || isAdding,
}}
/>
</ChipModal>
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Chip, ChipModal, ChipModalBody, ChipModalFooter, ChipModalHeader } from '@/components/emcn'
import { ChipConfirmModal } from '@/components/emcn'

interface UnsavedChangesModalProps {
open: boolean
Expand All @@ -14,21 +14,14 @@ interface UnsavedChangesModalProps {
*/
export function UnsavedChangesModal({ open, onOpenChange, onDiscard }: UnsavedChangesModalProps) {
return (
<ChipModal open={open} onOpenChange={onOpenChange} srTitle='Unsaved Changes'>
<ChipModalHeader showDivider={false}>Unsaved Changes</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
You have unsaved changes. Are you sure you want to discard them?
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => onOpenChange(false)}>
Keep Editing
</Chip>
<Chip variant='destructive' flush onClick={onDiscard}>
Discard Changes
</Chip>
</ChipModalFooter>
</ChipModal>
<ChipConfirmModal
open={open}
onOpenChange={onOpenChange}
srTitle='Unsaved Changes'
title='Unsaved Changes'
description='You have unsaved changes. Are you sure you want to discard them?'
dismissLabel='Keep editing'
confirm={{ label: 'Discard Changes', onClick: onDiscard }}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { GitBranch } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import {
Check,
Chip,
ChipModal,
ChipModalBody,
ChipModalField,
Expand Down Expand Up @@ -279,14 +278,13 @@ export const MessageActions = memo(function MessageActions({
}
/>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => handleModalClose(false)}>
Cancel
</Chip>
<Chip variant='primary' flush onClick={handleSubmitFeedback}>
Submit
</Chip>
</ChipModalFooter>
<ChipModalFooter
onCancel={() => handleModalClose(false)}
primaryAction={{
label: 'Submit',
onClick: handleSubmitFeedback,
}}
/>
</ChipModal>
</>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { memo } from 'react'
import { Chip, ChipModal, ChipModalBody, ChipModalFooter, ChipModalHeader } from '@/components/emcn'
import { ChipConfirmModal } from '@/components/emcn'

interface DeleteConfirmModalProps {
open: boolean
Expand Down Expand Up @@ -34,29 +34,28 @@ export const DeleteConfirmModal = memo(function DeleteConfirmModal({
: 'You can restore it from Recently Deleted in Settings.'

return (
<ChipModal open={open} onOpenChange={onOpenChange} srTitle={title}>
<ChipModalHeader onClose={() => onOpenChange(false)} showDivider={false}>
{title}
</ChipModalHeader>
<ChipModalBody>
<p className='px-2 text-[var(--text-secondary)] text-sm'>
<ChipConfirmModal
open={open}
onOpenChange={onOpenChange}
srTitle={title}
title={title}
description={
<>
Are you sure you want to delete{' '}
{fileName ? (
<span className='font-medium text-[var(--text-primary)]'>{fileName}</span>
) : (
`${totalCount} item${totalCount === 1 ? '' : 's'}`
)}
? {consequence}
</p>
</ChipModalBody>
<ChipModalFooter>
<Chip variant='filled' flush onClick={() => onOpenChange(false)} disabled={isPending}>
Cancel
</Chip>
<Chip variant='destructive' flush onClick={onDelete} disabled={isPending}>
{isPending ? 'Deleting...' : 'Delete'}
</Chip>
</ChipModalFooter>
</ChipModal>
</>
}
confirm={{
label: 'Delete',
onClick: onDelete,
pending: isPending,
pendingLabel: 'Deleting...',
}}
/>
)
})
Loading
Loading