import { LandingPromptStorage } from '@/lib/core/utils/browser-storage' import { getCanonicalBlocksByCategory } from '@/blocks/registry' import type { BlockIcon } from '@/blocks/types' import { registerBlockCacheInvalidator } from '@/blocks/visibility/context' /** * Public descriptor for a single integration block, exposed to UI surfaces * that need to render an integration tile (mention menu, chip glyph, etc.) * without depending on the full {@link BlockConfig} shape. */ export interface IntegrationDescriptor { /** Stable block type identifier (the block's registry key). */ blockType: string /** Display name with `(Legacy)` / `V2` suffixes stripped. */ name: string /** Brand SVG icon component. */ icon: BlockIcon /** Background color hex string used by integration tiles. */ bgColor: string } /** * Precomputed lookup tables used by the auto-mention engine and the mention * menu. Names are sorted longest-first so multi-word matches like * `Google Sheets` win over `Google`; lookarounds prevent substring hits like * `Slack` inside `Slackbot`. */ export interface IntegrationMatcher { /** Regex matching any known integration name as a standalone token, or `null` when no integrations are registered. */ regex: RegExp | null /** Lowercase display name -> descriptor for canonical lookup after a regex match. */ byName: Map } function escapeRegex(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } /** Strips ` (Legacy)` / ` V2` suffixes so the display uses the natural name. */ function normalizeDisplayName(name: string): string { return name .replace(/\s*\(legacy\)\s*$/i, '') .replace(/\s+v\d+(\.\d+)*\s*$/i, '') .trim() } let cachedMatcher: IntegrationMatcher | null = null let cachedList: readonly IntegrationDescriptor[] | null = null /** * Drops the memoized matcher/list. Registered with the block cache-invalidator * seam so a client block-visibility change (preview reveal / kill switch) * rebuilds the matcher against the new canonical block set. */ function clearIntegrationMatcherCache(): void { cachedMatcher = null cachedList = null } registerBlockCacheInvalidator(clearIntegrationMatcherCache) function buildMatcher(): IntegrationMatcher { const byName = new Map() const names: string[] = [] for (const block of getCanonicalBlocksByCategory('tools')) { if (!block.name || block.name.trim().length < 2) continue const displayName = normalizeDisplayName(block.name) const key = displayName.toLowerCase() if (byName.has(key)) continue byName.set(key, { blockType: block.type, name: displayName, icon: block.icon, bgColor: block.bgColor, }) names.push(displayName) } names.sort((a, b) => b.length - a.length) const regex = names.length ? new RegExp(`(? offset > 0 && text[offset - 1] === '@' ? match : `@${match}` ) } /** * Stores a CURATED prompt (a suggested action, template, or showcase CTA — never * free-form user prose) for the home chat input to consume after navigation, * running it through {@link mentionifyIntegrations} first so its integration * names chip with brand icons on arrival. * * This is the single seam every curated-prompt producer that hands off via * {@link LandingPromptStorage} must use — it pairs the rewrite with the store so * a new producer cannot forget the rewrite (the regression class this guards * against). User-typed prose (e.g. the landing preview panel) intentionally * bypasses this and calls {@link LandingPromptStorage.store} directly, since * bare integration names in prose must never be auto-chipped (the scunthorpe * problem). */ export function storeCuratedPrompt(prompt: string): boolean { return LandingPromptStorage.store(mentionifyIntegrations(prompt)) } /** * Lazily builds (once per session) and returns all known integrations sorted * alphabetically by display name for menu rendering. Shares the underlying * scan with {@link getIntegrationMatcher} so the registry is iterated at most * once per call site. */ export function listIntegrations(): readonly IntegrationDescriptor[] { if (cachedList) return cachedList const { byName } = getIntegrationMatcher() cachedList = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)) return cachedList }