d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
145 lines
5.4 KiB
TypeScript
145 lines
5.4 KiB
TypeScript
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<string, IntegrationDescriptor>
|
|
}
|
|
|
|
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<string, IntegrationDescriptor>()
|
|
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(`(?<![A-Za-z0-9_])(${names.map(escapeRegex).join('|')})(?![A-Za-z0-9_])`, 'gi')
|
|
: null
|
|
|
|
return { regex, byName }
|
|
}
|
|
|
|
/**
|
|
* Lazily builds (once per session) and returns the precomputed integration
|
|
* matcher. Reused by both the auto-mention keystroke fast-path and the bulk
|
|
* text rewrite path.
|
|
*/
|
|
export function getIntegrationMatcher(): IntegrationMatcher {
|
|
if (cachedMatcher) return cachedMatcher
|
|
cachedMatcher = buildMatcher()
|
|
return cachedMatcher
|
|
}
|
|
|
|
/**
|
|
* Rewrites bare integration names in `text` to `@`-mention form (`Slack` →
|
|
* `@Slack`) so they chip when the prompt is populated into the chat input —
|
|
* the auto-mention pipeline deliberately ignores un-prefixed names (mention
|
|
* treatment is strictly opt-in via a token-starting `@`), so curated prompts
|
|
* that should chip must opt in here. Idempotent: names already prefixed with
|
|
* `@` are left untouched.
|
|
*/
|
|
export function mentionifyIntegrations(text: string): string {
|
|
const { regex } = getIntegrationMatcher()
|
|
if (!regex || !text) return text
|
|
return text.replace(regex, (match: string, _name: string, offset: number) =>
|
|
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
|
|
}
|