Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

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
}