Compare commits

..

38 Commits

Author SHA1 Message Date
Waleed
19442f19e2 v0.6.7: kb improvements, edge z index fix, captcha, new trust center, block classifications 2026-03-21 12:43:33 -07:00
Waleed
951c8fd5e9 feat(integrations): add integrationType and tags classification to all blocks (#3702)
* feat(integrations): add integrationType and tags classification to all blocks

* improvement(integrations): replace generic api/oauth tags with use-case-oriented tags

* lint

* upgrade turbo
2026-03-21 11:45:49 -07:00
Waleed
4a34ac3015 feat(auth): add Turnstile captcha + harmony disposable email blocking (#3699)
* feat(turnstile): conditionally added CF turnstile to signup

* feat(auth): add execute-on-submit Turnstile, conditional harmony, and feature flag

- Switch Turnstile to execution: 'execute' mode so challenge runs on
  form submit (fresh token every time, no expiry issues)
- Make emailHarmony conditional via SIGNUP_EMAIL_VALIDATION_ENABLED
  feature flag so self-hosted users can opt out
- Add isSignupEmailValidationEnabled to feature-flags.ts following
  existing pattern
- Add better-auth-harmony to Next.js transpilePackages (required for
  validator.js ESM compatibility)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(validation): remove dead validateEmail and checkMXRecord

Server-side disposable email blocking is now handled by
better-auth-harmony. The async validateEmail (with MX check) had no
remaining callers. Only quickValidateEmail remains for client-side
form feedback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(auth): add 15s timeout to Turnstile captcha promise

Prevents form from hanging indefinitely if Turnstile never fires
onSuccess/onError (e.g. script fails to load, network drop).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(helm): add Turnstile and harmony env vars to values.yaml

Adds TURNSTILE_SECRET_KEY, NEXT_PUBLIC_TURNSTILE_SITE_KEY, and
SIGNUP_EMAIL_VALIDATION_ENABLED to the helm chart so self-hosted
deployments can configure captcha and disposable email blocking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(auth): reject captcha promise on token expiry

onExpire now rejects the pending promise so the form doesn't hang
if the Turnstile token expires mid-challenge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(login): replace useEffect keydown listener with form onSubmit

The forgot-password modal used a global window keydown listener in a
useEffect to handle Enter key — a "you might not need an effect"
anti-pattern with a stale closure risk. Replaced with a native
<form onSubmit> wrapper which handles Enter natively, eliminating
the useEffect, the global listener, and the stale closure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(auth): clear dangling timeout after captcha promise settles

Use .finally(() => clearTimeout(timeoutId)) to clean up the 15s
timeout timer when the captcha resolves before the deadline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(auth): use getResponsePromise() for Turnstile token retrieval

Replace the manual Promise + refs + timeout pattern with the
documented getResponsePromise(timeout) API from @marsidev/react-turnstile.
This eliminates captchaToken state, captchaResolveRef, captchaRejectRef,
and all callback wiring on the Turnstile component.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(auth): show captcha errors as form-level message, not password error

Captcha failures were misleadingly displayed under the password field.
Added a dedicated formError state that renders above the submit button,
making it clear the issue is with verification, not the password.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 11:23:45 -07:00
Waleed
224ff5dacc chore(trust): replace Delve trust center with Vanta (#3701)
* chore(trust): replace Delve trust center with Vanta

* lint
2026-03-21 11:05:17 -07:00
Waleed
cb3cc378b8 fix(canvas): correct z-index layering for selected blocks and connected edges (#3698)
* fix(canvas): correct z-index layering for selected blocks and connected edges

* fix(canvas): derive subflow edge z-index from connected node z-index

* fix(canvas): fix nodesForRender early-return guard for regular blocks

* lint

* fix(canvas): ensure elevated edges and last-interacted nodes sit above siblings at same base z-index
2026-03-21 10:21:13 -07:00
Waleed
a64afac075 feat(kb): harden sync engine and add connector audit logging (#3697)
* feat(kb): harden sync engine and add connector audit logging

- Fix stuck syncing status: added finally block in executeSync + stale lock recovery in cron scheduler (2hr TTL)
- Fix token expiry mid-sync: refresh OAuth token between pagination pages and before deferred content hydration
- GitHub deferred content loading: use Git blob SHA for change detection, only fetch content for new/changed docs
- Add network error keywords to isRetryableError (fetch failed, econnreset, etc.)
- Extract sanitizeStorageTitle helper to fix S3 key length limit issues
- Add audit logging for connector CRUD, sync triggers, document exclude/restore, and resource restoration paths

* lint

* fix(tests): update audit mock and route tests for new audit actions

* fix(kb): address PR review - finally block race, contentHash propagation, resourceName

- Replace DB-read finally block with local syncExitedCleanly flag to avoid race condition
- Propagate fullDoc.contentHash during deferred content hydration
- Add resourceName to file restore audit record

* fix(audit): include fileId in file restore audit description
2026-03-21 09:36:43 -07:00
Vikhyath Mondreti
e270756886 fix(kb): max depth exceeded chunks page error (#3695) 2026-03-20 15:23:44 -07:00
Adithya Krishna
6d7121110e feat(loading) show route specific skeleton UI (#3671)
* chore: fix conflicts

* chore: updated loading states
2026-03-20 12:46:24 -07:00
Waleed
1731a4d7f0 v0.6.6: landing improvements, styling consistency, mothership table renaming 2026-03-19 23:58:30 -07:00
Waleed
9fcd02fd3b v0.6.5: email validation, integrations page, mothership and custom tool fixes 2026-03-19 16:08:30 -07:00
Waleed
ff7b5b528c v0.6.4: subflows, docusign, ashby new tools, box, workday, billing bug fixes 2026-03-18 23:12:36 -07:00
Waleed
30f2d1a0fc v0.6.3: hubspot integration, kb block improvements 2026-03-18 11:19:55 -07:00
Waleed
4bd0731871 v0.6.2: mothership stability, chat iframe embedding, KB upserts, new blog post 2026-03-18 03:29:39 -07:00
Waleed
4f3bc37fe4 v0.6.1: added better auth admin plugin 2026-03-17 15:16:16 -07:00
Waleed
84d6fdc423 v0.6: mothership, tables, connectors 2026-03-17 12:21:15 -07:00
Vikhyath Mondreti
4c12914d35 v0.5.113: jira, ashby, google ads, grain updates 2026-03-12 22:54:25 -07:00
Waleed
e9bdc57616 v0.5.112: trace spans improvements, fathom integration, jira fixes, canvas navigation updates 2026-03-12 13:30:20 -07:00
Vikhyath Mondreti
36612ae42a v0.5.111: non-polling webhook execs off trigger.dev, gmail subject headers, webhook trigger configs (#3530) 2026-03-11 17:47:28 -07:00
Waleed
1c2c2c65d4 v0.5.110: webhook execution speedups, SSRF patches 2026-03-11 15:00:24 -07:00
Waleed
ecd3536a72 v0.5.109: obsidian and evernote integrations, slack fixes, remove memory instrumentation 2026-03-09 10:40:37 -07:00
Vikhyath Mondreti
8c0a2e04b1 v0.5.108: workflow input params in agent tools, bun upgrade, dropdown selectors for 14 blocks 2026-03-06 21:02:25 -08:00
Waleed
6586c5ce40 v0.5.107: new reddit, slack tools 2026-03-05 22:48:20 -08:00
Vikhyath Mondreti
3ce947566d v0.5.106: condition block and legacy kbs fixes, GPT 5.4 2026-03-05 17:30:05 -08:00
Waleed
70c36cb7aa v0.5.105: slack remove reaction, nested subflow locks fix, servicenow pagination, memory improvements 2026-03-04 22:38:26 -08:00
Waleed
f1ec5fe824 v0.5.104: memory improvements, nested subflows, careers page redirect, brandfetch, google meet 2026-03-03 23:45:29 -08:00
Waleed
e07e3c34cc v0.5.103: memory util instrumentation, API docs, amplitude, google pagespeed insights, pagerduty 2026-03-01 23:27:02 -08:00
Waleed
0d2e6ff31d v0.5.102: new integrations, new tools, ci speedups, memory leak instrumentation 2026-02-28 12:48:10 -08:00
Waleed
4fd0989264 v0.5.101: circular dependency mitigation, confluence enhancements, google tasks and bigquery integrations, workflow lock 2026-02-26 15:04:53 -08:00
Waleed
67f8a687f6 v0.5.100: multiple credentials, 40% speedup, gong, attio, audit log improvements 2026-02-25 00:28:25 -08:00
Waleed
af592349d3 v0.5.99: local dev improvements, live workflow logs in terminal 2026-02-23 00:24:49 -08:00
Waleed
0d86ea01f0 v0.5.98: change detection improvements, rate limit and code execution fixes, removed retired models, hex integration 2026-02-21 18:07:40 -08:00
Waleed
115f04e989 v0.5.97: oidc discovery for copilot mcp 2026-02-21 02:06:25 -08:00
Waleed
34d92fae89 v0.5.96: sim oauth provider, slack ephemeral message tool and blockkit support 2026-02-20 18:22:20 -08:00
Waleed
67aa4bb332 v0.5.95: gemini 3.1 pro, cloudflare, dataverse, revenuecat, redis, upstash, algolia tools; isolated-vm robustness improvements, tables backend (#3271)
* feat(tools): advanced fields for youtube, vercel; added cloudflare and dataverse tools (#3257)

* refactor(vercel): mark optional fields as advanced mode

Move optional/power-user fields behind the advanced toggle:
- List Deployments: project filter, target, state
- Create Deployment: project ID override, redeploy from, target
- List Projects: search
- Create/Update Project: framework, build/output/install commands
- Env Vars: variable type
- Webhooks: project IDs filter
- Checks: path, details URL
- Team Members: role filter
- All operations: team ID scope

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(youtube): mark optional params as advanced mode

Hide pagination, sort order, and filter fields behind the advanced
toggle for a cleaner default UX across all YouTube operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* added advanced fields for vercel and youtube, added cloudflare and dataverse block

* addded desc for dataverse

* add more tools

* ack comment

* more

* ops

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat(tables): added tables (#2867)

* updates

* required

* trashy table viewer

* updates

* updates

* filtering ui

* updates

* updates

* updates

* one input mode

* format

* fix lints

* improved errors

* updates

* updates

* chages

* doc strings

* breaking down file

* update comments with ai

* updates

* comments

* changes

* revert

* updates

* dedupe

* updates

* updates

* updates

* refactoring

* renames & refactors

* refactoring

* updates

* undo

* update db

* wand

* updates

* fix comments

* fixes

* simplify comments

* u[dates

* renames

* better comments

* validation

* updates

* updates

* updates

* fix sorting

* fix appearnce

* updating prompt to make it user sort

* rm

* updates

* rename

* comments

* clean comments

* simplicifcaiton

* updates

* updates

* refactor

* reduced type confusion

* undo

* rename

* undo changes

* undo

* simplify

* updates

* updates

* revert

* updates

* db updates

* type fix

* fix

* fix error handling

* updates

* docs

* docs

* updates

* rename

* dedupe

* revert

* uncook

* updates

* fix

* fix

* fix

* fix

* prepare merge

* readd migrations

* add back missed code

* migrate enrichment logic to general abstraction

* address bugbot concerns

* adhere to size limits for tables

* remove conflicting migration

* add back migrations

* fix tables auth

* fix permissive auth

* fix lint

* reran migrations

* migrate to use tanstack query for all server state

* update table-selector

* update names

* added tables to permission groups, updated subblock types

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: waleed <walif6@gmail.com>

* fix(snapshot): changed insert to upsert when concurrent identical child workflows are running (#3259)

* fix(snapshot): changed insert to upsert when concurrent identical child workflows are running

* fixed ci tests failing

* fix(workflows): disallow duplicate workflow names at the same folder level (#3260)

* feat(tools): added redis, upstash, algolia, and revenuecat (#3261)

* feat(tools): added redis, upstash, algolia, and revenuecat

* ack comment

* feat(models): add gemini-3.1-pro-preview and update gemini-3-pro thinking levels (#3263)

* fix(audit-log): lazily resolve actor name/email when missing (#3262)

* fix(blocks): move type coercions from tools.config.tool to tools.config.params (#3264)

* fix(blocks): move type coercions from tools.config.tool to tools.config.params

Number() coercions in tools.config.tool ran at serialization time before
variable resolution, destroying dynamic references like <block.result.count>
by converting them to NaN/null. Moved all coercions to tools.config.params
which runs at execution time after variables are resolved.

Fixed in 15 blocks: exa, arxiv, sentry, incidentio, wikipedia, ahrefs,
posthog, elasticsearch, dropbox, hunter, lemlist, spotify, youtube, grafana,
parallel. Also added mode: 'advanced' to optional exa fields.

Closes #3258

* fix(blocks): address PR review — move remaining param mutations from tool() to params()

- Moved field mappings from tool() to params() in grafana, posthog,
  lemlist, spotify, dropbox (same dynamic reference bug)
- Fixed parallel.ts excerpts/full_content boolean logic
- Fixed parallel.ts search_queries empty case (must set undefined)
- Fixed elasticsearch.ts timeout not included when already ends with 's'
- Restored dropbox.ts tool() switch for proper default fallback

* fix(blocks): restore field renames to tool() for serialization-time validation

Field renames (e.g. personalApiKey→apiKey) must be in tool() because
validateRequiredFieldsBeforeExecution calls selectToolId()→tool() then
checks renamed field names on params. Only type coercions (Number(),
boolean) stay in params() to avoid destroying dynamic variable references.

* improvement(resolver): resovled empty sentinel to not pass through unexecuted valid refs to text inputs (#3266)

* fix(blocks): add required constraint for serviceDeskId in JSM block (#3268)

* fix(blocks): add required constraint for serviceDeskId in JSM block

* fix(blocks): rename custom field values to request field values in JSM create request

* fix(trigger): add isolated-vm support to trigger.dev container builds (#3269)

Scheduled workflow executions running in trigger.dev containers were
failing to spawn isolated-vm workers because the native module wasn't
available in the container. This caused loop condition evaluation to
silently fail and exit after one iteration.

- Add isolated-vm to build.external and additionalPackages in trigger config
- Include isolated-vm-worker.cjs via additionalFiles for child process spawning
- Add fallback path resolution for worker file in trigger.dev environment

* fix(tables): hide tables from sidebar and block registry (#3270)

* fix(tables): hide tables from sidebar and block registry

* fix(trigger): add isolated-vm support to trigger.dev container builds (#3269)

Scheduled workflow executions running in trigger.dev containers were
failing to spawn isolated-vm workers because the native module wasn't
available in the container. This caused loop condition evaluation to
silently fail and exit after one iteration.

- Add isolated-vm to build.external and additionalPackages in trigger config
- Include isolated-vm-worker.cjs via additionalFiles for child process spawning
- Add fallback path resolution for worker file in trigger.dev environment

* lint

* fix(trigger): update node version to align with main app (#3272)

* fix(build): fix corrupted sticky disk cache on blacksmith (#3273)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
2026-02-20 13:43:07 -08:00
Waleed
15ace5e63f v0.5.94: vercel integration, folder insertion, migrated tracking redirects to rewrites 2026-02-18 16:53:34 -08:00
Waleed
fdca73679d v0.5.93: NextJS config changes, MCP and Blocks whitelisting, copilot keyboard shortcuts, audit logs 2026-02-18 12:10:05 -08:00
Waleed
da46a387c9 v0.5.92: shortlinks, copilot scrolling stickiness, pagination 2026-02-17 15:13:21 -08:00
Waleed
b7e377ec4b v0.5.91: docs i18n, turborepo upgrade 2026-02-16 00:36:05 -08:00
260 changed files with 16985 additions and 1168 deletions

View File

@@ -19,7 +19,7 @@ When the user asks you to create a block:
```typescript
import { {ServiceName}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'
export const {ServiceName}Block: BlockConfig = {
@@ -29,6 +29,8 @@ export const {ServiceName}Block: BlockConfig = {
longDescription: 'Detailed description for docs',
docsLink: 'https://docs.sim.ai/tools/{service}',
category: 'tools', // 'tools' | 'blocks' | 'triggers'
integrationType: IntegrationType.X, // Primary category (see IntegrationType enum)
tags: ['oauth', 'api'], // Cross-cutting tags (see IntegrationTag type)
bgColor: '#HEXCOLOR', // Brand color
icon: {ServiceName}Icon,
@@ -629,7 +631,7 @@ export const registry: Record<string, BlockConfig> = {
```typescript
import { ServiceIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'
export const ServiceBlock: BlockConfig = {
@@ -639,6 +641,8 @@ export const ServiceBlock: BlockConfig = {
longDescription: 'Full description for documentation...',
docsLink: 'https://docs.sim.ai/tools/service',
category: 'tools',
integrationType: IntegrationType.DeveloperTools,
tags: ['oauth', 'api'],
bgColor: '#FF6B6B',
icon: ServiceIcon,
authMode: AuthMode.OAuth,
@@ -796,6 +800,8 @@ All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MU
## Checklist Before Finishing
- [ ] `integrationType` is set to the correct `IntegrationType` enum value
- [ ] `tags` array includes all applicable `IntegrationTag` values
- [ ] All subBlocks have `id`, `title` (except switch), and `type`
- [ ] Conditions use correct syntax (field, value, not, and)
- [ ] DependsOn set for fields that need other values

View File

@@ -113,7 +113,7 @@ export const {service}{Action}Tool: ToolConfig<Params, Response> = {
```typescript
import { {Service}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'
export const {Service}Block: BlockConfig = {
@@ -123,6 +123,8 @@ export const {Service}Block: BlockConfig = {
longDescription: '...',
docsLink: 'https://docs.sim.ai/tools/{service}',
category: 'tools',
integrationType: IntegrationType.X, // Primary category (see IntegrationType enum)
tags: ['oauth', 'api'], // Cross-cutting tags (see IntegrationTag type)
bgColor: '#HEXCOLOR',
icon: {Service}Icon,
authMode: AuthMode.OAuth, // or AuthMode.ApiKey
@@ -410,6 +412,8 @@ If creating V2 versions (API-aligned outputs):
### Block
- [ ] Created `blocks/blocks/{service}.ts`
- [ ] Set `integrationType` to the correct `IntegrationType` enum value
- [ ] Set `tags` array with all applicable `IntegrationTag` values
- [ ] Defined operation dropdown with all operations
- [ ] Added credential field with `requiredScopes: getScopesForService('{service}')`
- [ ] Added conditional fields per operation

View File

@@ -4143,12 +4143,11 @@ export function InfisicalIcon(props: SVGProps<SVGSVGElement>) {
<svg {...props} viewBox='20 25 233 132' xmlns='http://www.w3.org/2000/svg'>
<path
d='m191.6 39.4c-20.3 0-37.15 13.21-52.9 30.61-12.99-16.4-29.8-30.61-51.06-30.61-27.74 0-50.44 23.86-50.44 51.33 0 26.68 21.43 51.8 48.98 51.8 20.55 0 37.07-13.86 51.32-31.81 12.69 16.97 29.1 31.41 53.2 31.41 27.13 0 49.85-22.96 49.85-51.4 0-27.12-20.44-51.33-48.95-51.33zm-104.3 77.94c-14.56 0-25.51-12.84-25.51-26.07 0-13.7 10.95-28.29 25.51-28.29 14.93 0 25.71 11.6 37.6 27.34-11.31 15.21-22.23 27.02-37.6 27.02zm104.4 0.25c-15 0-25.28-11.13-37.97-27.37 12.69-16.4 22.01-27.24 37.59-27.24 14.97 0 24.79 13.25 24.79 27.26 0 13-10.17 27.35-24.41 27.35z'
fill='black'
fill='currentColor'
/>
</svg>
)
}
export function IntercomIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

View File

@@ -0,0 +1,24 @@
import { Skeleton } from '@/components/emcn'
export default function LoginLoading() {
return (
<div className='flex flex-col items-center'>
<Skeleton className='h-[38px] w-[80px] rounded-[4px]' />
<div className='mt-[32px] w-full space-y-[8px]'>
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
<Skeleton className='h-[44px] w-full rounded-[10px]' />
</div>
<div className='mt-[16px] w-full space-y-[8px]'>
<Skeleton className='h-[14px] w-[64px] rounded-[4px]' />
<Skeleton className='h-[44px] w-full rounded-[10px]' />
</div>
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
<Skeleton className='mt-[24px] h-[1px] w-full rounded-[1px]' />
<div className='mt-[24px] flex w-full gap-[12px]'>
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
</div>
<Skeleton className='mt-[24px] h-[14px] w-[200px] rounded-[4px]' />
</div>
)
}

View File

@@ -1,6 +1,7 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { useMemo, useRef, useState } from 'react'
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
import { createLogger } from '@sim/logger'
import { Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
@@ -86,6 +87,9 @@ export default function LoginPage({
const [password, setPassword] = useState('')
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
const [showValidationError, setShowValidationError] = useState(false)
const [formError, setFormError] = useState<string | null>(null)
const turnstileRef = useRef<TurnstileInstance>(null)
const turnstileSiteKey = useMemo(() => getEnv('NEXT_PUBLIC_TURNSTILE_SITE_KEY'), [])
const buttonClass = useBrandedButtonClass()
const callbackUrlParam = searchParams?.get('callbackUrl')
@@ -115,19 +119,6 @@ export default function LoginPage({
: null
)
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Enter' && forgotPasswordOpen) {
handleForgotPassword()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => {
window.removeEventListener('keydown', handleKeyDown)
}
}, [forgotPasswordEmail, forgotPasswordOpen])
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newEmail = e.target.value
setEmail(newEmail)
@@ -178,6 +169,21 @@ export default function LoginPage({
const safeCallbackUrl = callbackUrl
let errorHandled = false
// Execute Turnstile challenge on submit and get a fresh token
let token: string | undefined
if (turnstileSiteKey && turnstileRef.current) {
try {
turnstileRef.current.reset()
turnstileRef.current.execute()
token = await turnstileRef.current.getResponsePromise(15_000)
} catch {
setFormError('Captcha verification failed. Please try again.')
setIsLoading(false)
return
}
}
setFormError(null)
const result = await client.signIn.email(
{
email,
@@ -185,6 +191,11 @@ export default function LoginPage({
callbackURL: safeCallbackUrl,
},
{
fetchOptions: {
headers: {
...(token ? { 'x-captcha-response': token } : {}),
},
},
onError: (ctx) => {
logger.error('Login error:', ctx.error)
@@ -460,6 +471,20 @@ export default function LoginPage({
</div>
</div>
{turnstileSiteKey && (
<Turnstile
ref={turnstileRef}
siteKey={turnstileSiteKey}
options={{ size: 'invisible', execution: 'execute' }}
/>
)}
{formError && (
<div className='text-red-400 text-xs'>
<p>{formError}</p>
</div>
)}
<BrandedButton
type='submit'
disabled={isLoading}
@@ -540,45 +565,51 @@ export default function LoginPage({
<ModalContent className='dark' size='sm'>
<ModalHeader>Reset Password</ModalHeader>
<ModalBody>
<ModalDescription className='mb-4 text-[var(--text-muted)] text-sm'>
Enter your email address and we'll send you a link to reset your password if your
account exists.
</ModalDescription>
<div className='space-y-4'>
<div className='space-y-2'>
<Label htmlFor='reset-email'>Email</Label>
<Input
id='reset-email'
value={forgotPasswordEmail}
onChange={(e) => setForgotPasswordEmail(e.target.value)}
placeholder='Enter your email'
required
type='email'
className={cn(
resetStatus.type === 'error' && 'border-red-500 focus:border-red-500'
<form
onSubmit={(e) => {
e.preventDefault()
handleForgotPassword()
}}
>
<ModalDescription className='mb-4 text-[var(--text-muted)] text-sm'>
Enter your email address and we'll send you a link to reset your password if your
account exists.
</ModalDescription>
<div className='space-y-4'>
<div className='space-y-2'>
<Label htmlFor='reset-email'>Email</Label>
<Input
id='reset-email'
value={forgotPasswordEmail}
onChange={(e) => setForgotPasswordEmail(e.target.value)}
placeholder='Enter your email'
required
type='email'
className={cn(
resetStatus.type === 'error' && 'border-red-500 focus:border-red-500'
)}
/>
{resetStatus.type === 'error' && (
<div className='mt-1 text-red-400 text-xs'>
<p>{resetStatus.message}</p>
</div>
)}
/>
{resetStatus.type === 'error' && (
<div className='mt-1 text-red-400 text-xs'>
</div>
{resetStatus.type === 'success' && (
<div className='mt-1 text-[#4CAF50] text-xs'>
<p>{resetStatus.message}</p>
</div>
)}
<BrandedButton
type='submit'
disabled={isSubmittingReset}
loading={isSubmittingReset}
loadingText='Sending'
>
Send Reset Link
</BrandedButton>
</div>
{resetStatus.type === 'success' && (
<div className='mt-1 text-[#4CAF50] text-xs'>
<p>{resetStatus.message}</p>
</div>
)}
<BrandedButton
type='button'
onClick={handleForgotPassword}
disabled={isSubmittingReset}
loading={isSubmittingReset}
loadingText='Sending'
>
Send Reset Link
</BrandedButton>
</div>
</form>
</ModalBody>
</ModalContent>
</Modal>

View File

@@ -0,0 +1,21 @@
import { Skeleton } from '@/components/emcn'
export default function OAuthConsentLoading() {
return (
<div className='flex flex-col items-center'>
<div className='flex items-center gap-[16px]'>
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
<Skeleton className='h-[20px] w-[20px] rounded-[4px]' />
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
</div>
<Skeleton className='mt-[24px] h-[38px] w-[220px] rounded-[4px]' />
<Skeleton className='mt-[8px] h-[14px] w-[280px] rounded-[4px]' />
<Skeleton className='mt-[24px] h-[56px] w-full rounded-[8px]' />
<Skeleton className='mt-[16px] h-[120px] w-full rounded-[8px]' />
<div className='mt-[24px] flex w-full max-w-[410px] gap-[12px]'>
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
</div>
</div>
)
}

View File

@@ -0,0 +1,16 @@
import { Skeleton } from '@/components/emcn'
export default function ResetPasswordLoading() {
return (
<div className='flex flex-col items-center'>
<Skeleton className='h-[38px] w-[160px] rounded-[4px]' />
<Skeleton className='mt-[12px] h-[14px] w-[280px] rounded-[4px]' />
<div className='mt-[32px] w-full space-y-[8px]'>
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
<Skeleton className='h-[44px] w-full rounded-[10px]' />
</div>
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
<Skeleton className='mt-[24px] h-[14px] w-[120px] rounded-[4px]' />
</div>
)
}

View File

@@ -0,0 +1,28 @@
import { Skeleton } from '@/components/emcn'
export default function SignupLoading() {
return (
<div className='flex flex-col items-center'>
<Skeleton className='h-[38px] w-[100px] rounded-[4px]' />
<div className='mt-[32px] w-full space-y-[8px]'>
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
<Skeleton className='h-[44px] w-full rounded-[10px]' />
</div>
<div className='mt-[16px] w-full space-y-[8px]'>
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
<Skeleton className='h-[44px] w-full rounded-[10px]' />
</div>
<div className='mt-[16px] w-full space-y-[8px]'>
<Skeleton className='h-[14px] w-[64px] rounded-[4px]' />
<Skeleton className='h-[44px] w-full rounded-[10px]' />
</div>
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
<Skeleton className='mt-[24px] h-[1px] w-full rounded-[1px]' />
<div className='mt-[24px] flex w-full gap-[12px]'>
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
</div>
<Skeleton className='mt-[24px] h-[14px] w-[220px] rounded-[4px]' />
</div>
)
}

View File

@@ -1,6 +1,7 @@
'use client'
import { Suspense, useMemo, useState } from 'react'
import { Suspense, useMemo, useRef, useState } from 'react'
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
import { createLogger } from '@sim/logger'
import { Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
@@ -90,6 +91,9 @@ function SignupFormContent({
const [emailError, setEmailError] = useState('')
const [emailErrors, setEmailErrors] = useState<string[]>([])
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
const [formError, setFormError] = useState<string | null>(null)
const turnstileRef = useRef<TurnstileInstance>(null)
const turnstileSiteKey = useMemo(() => getEnv('NEXT_PUBLIC_TURNSTILE_SITE_KEY'), [])
const buttonClass = useBrandedButtonClass()
const redirectUrl = useMemo(
@@ -245,6 +249,21 @@ function SignupFormContent({
const sanitizedName = trimmedName
// Execute Turnstile challenge on submit and get a fresh token
let token: string | undefined
if (turnstileSiteKey && turnstileRef.current) {
try {
turnstileRef.current.reset()
turnstileRef.current.execute()
token = await turnstileRef.current.getResponsePromise(15_000)
} catch {
setFormError('Captcha verification failed. Please try again.')
setIsLoading(false)
return
}
}
setFormError(null)
const response = await client.signUp.email(
{
email: emailValue,
@@ -252,6 +271,11 @@ function SignupFormContent({
name: sanitizedName,
},
{
fetchOptions: {
headers: {
...(token ? { 'x-captcha-response': token } : {}),
},
},
onError: (ctx) => {
logger.error('Signup error:', ctx.error)
const errorMessage: string[] = ['Failed to create account']
@@ -453,6 +477,20 @@ function SignupFormContent({
</div>
</div>
{turnstileSiteKey && (
<Turnstile
ref={turnstileRef}
siteKey={turnstileSiteKey}
options={{ size: 'invisible', execution: 'execute' }}
/>
)}
{formError && (
<div className='text-red-400 text-xs'>
<p>{formError}</p>
</div>
)}
<BrandedButton
type='submit'
disabled={isLoading}

View File

@@ -0,0 +1,16 @@
import { Skeleton } from '@/components/emcn'
export default function SSOLoading() {
return (
<div className='flex flex-col items-center'>
<Skeleton className='h-[38px] w-[120px] rounded-[4px]' />
<Skeleton className='mt-[12px] h-[14px] w-[260px] rounded-[4px]' />
<div className='mt-[32px] w-full space-y-[8px]'>
<Skeleton className='h-[14px] w-[80px] rounded-[4px]' />
<Skeleton className='h-[44px] w-full rounded-[10px]' />
</div>
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
<Skeleton className='mt-[24px] h-[14px] w-[120px] rounded-[4px]' />
</div>
)
}

View File

@@ -0,0 +1,12 @@
import { Skeleton } from '@/components/emcn'
export default function VerifyLoading() {
return (
<div className='flex flex-col items-center'>
<Skeleton className='h-[38px] w-[180px] rounded-[4px]' />
<Skeleton className='mt-[12px] h-[14px] w-[300px] rounded-[4px]' />
<Skeleton className='mt-[4px] h-[14px] w-[240px] rounded-[4px]' />
<Skeleton className='mt-[32px] h-[44px] w-full rounded-[10px]' />
</div>
)
}

View File

@@ -460,7 +460,7 @@ function TrustStrip() {
<div className='mx-6 mt-4 grid grid-cols-1 overflow-hidden rounded-[8px] border border-[#2A2A2A] sm:grid-cols-3 md:mx-8'>
{/* SOC 2 + HIPAA combined */}
<Link
href='https://trust.delve.co/sim-studio'
href='https://app.vanta.com/sim.ai/trust/v35ia0jil4l7dteqjgaktn'
target='_blank'
rel='noopener noreferrer'
className='group flex items-center gap-3 border-[#2A2A2A] border-b px-4 py-[14px] transition-colors hover:bg-[#212121] sm:border-r sm:border-b-0'

View File

@@ -0,0 +1,59 @@
import { Skeleton } from '@/components/emcn'
export default function BlogPostLoading() {
return (
<article className='w-full'>
{/* Header area */}
<div className='mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
{/* Back link */}
<div className='mb-6'>
<Skeleton className='h-[16px] w-[60px] rounded-[4px] bg-[#2A2A2A]' />
</div>
{/* Image + title row */}
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
{/* Image */}
<div className='w-full flex-shrink-0 md:w-[450px]'>
<Skeleton className='aspect-[450/360] w-full rounded-lg bg-[#2A2A2A]' />
</div>
{/* Title + author */}
<div className='flex flex-1 flex-col justify-between'>
<div>
<Skeleton className='h-[48px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='mt-[8px] h-[48px] w-[80%] rounded-[4px] bg-[#2A2A2A]' />
</div>
<div className='mt-4 flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Skeleton className='h-[24px] w-[24px] rounded-full bg-[#2A2A2A]' />
<Skeleton className='h-[16px] w-[100px] rounded-[4px] bg-[#2A2A2A]' />
</div>
<Skeleton className='h-[32px] w-[32px] rounded-[6px] bg-[#2A2A2A]' />
</div>
</div>
</div>
{/* Divider */}
<Skeleton className='mt-8 h-[1px] w-full bg-[#2A2A2A] sm:mt-12' />
{/* Date + description */}
<div className='flex flex-col gap-6 py-8 sm:flex-row sm:items-start sm:justify-between sm:gap-8 sm:py-10'>
<Skeleton className='h-[16px] w-[120px] flex-shrink-0 rounded-[4px] bg-[#2A2A2A]' />
<div className='flex-1 space-y-[8px]'>
<Skeleton className='h-[20px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[20px] w-[70%] rounded-[4px] bg-[#2A2A2A]' />
</div>
</div>
</div>
{/* Article body */}
<div className='mx-auto max-w-[900px] px-6 pb-20 sm:px-8 md:px-12'>
<div className='space-y-[16px]'>
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[16px] w-[95%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[16px] w-[88%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='mt-[24px] h-[24px] w-[200px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[16px] w-[92%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[16px] w-[85%] rounded-[4px] bg-[#2A2A2A]' />
</div>
</div>
</article>
)
}

View File

@@ -0,0 +1,25 @@
import { Skeleton } from '@/components/emcn'
const SKELETON_POST_COUNT = 4
export default function AuthorLoading() {
return (
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
<div className='mb-6 flex items-center gap-3'>
<Skeleton className='h-[40px] w-[40px] rounded-full bg-[#2A2A2A]' />
<Skeleton className='h-[32px] w-[160px] rounded-[4px] bg-[#2A2A2A]' />
</div>
<div className='grid grid-cols-1 gap-8 sm:grid-cols-2'>
{Array.from({ length: SKELETON_POST_COUNT }).map((_, i) => (
<div key={i} className='overflow-hidden rounded-lg border border-[#2A2A2A]'>
<Skeleton className='h-[160px] w-full rounded-none bg-[#2A2A2A]' />
<div className='p-3'>
<Skeleton className='mb-1 h-[12px] w-[80px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[14px] w-[200px] rounded-[4px] bg-[#2A2A2A]' />
</div>
</div>
))}
</div>
</main>
)
}

View File

@@ -0,0 +1,29 @@
import { Skeleton } from '@/components/emcn'
const SKELETON_CARD_COUNT = 6
export default function BlogLoading() {
return (
<main className='mx-auto max-w-[1200px] px-6 py-12 sm:px-8 md:px-12'>
<Skeleton className='h-[48px] w-[100px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='mt-3 h-[18px] w-[420px] rounded-[4px] bg-[#2A2A2A]' />
<div className='mt-10 grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3'>
{Array.from({ length: SKELETON_CARD_COUNT }).map((_, i) => (
<div key={i} className='flex flex-col overflow-hidden rounded-xl border border-[#2A2A2A]'>
<Skeleton className='aspect-video w-full rounded-none bg-[#2A2A2A]' />
<div className='flex flex-1 flex-col p-4'>
<Skeleton className='mb-2 h-[12px] w-[80px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='mb-1 h-[20px] w-[85%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='mb-3 h-[14px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[14px] w-[70%] rounded-[4px] bg-[#2A2A2A]' />
<div className='mt-3 flex items-center gap-2'>
<Skeleton className='h-[16px] w-[16px] rounded-full bg-[#2A2A2A]' />
<Skeleton className='h-[12px] w-[80px] rounded-[4px] bg-[#2A2A2A]' />
</div>
</div>
</div>
))}
</div>
</main>
)
}

View File

@@ -0,0 +1,20 @@
import { Skeleton } from '@/components/emcn'
const SKELETON_TAG_COUNT = 12
export default function TagsLoading() {
return (
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
<Skeleton className='mb-6 h-[32px] w-[200px] rounded-[4px] bg-[#2A2A2A]' />
<div className='flex flex-wrap gap-3'>
{Array.from({ length: SKELETON_TAG_COUNT }).map((_, i) => (
<Skeleton
key={i}
className='h-[30px] rounded-full bg-[#2A2A2A]'
style={{ width: `${60 + (i % 4) * 24}px` }}
/>
))}
</div>
</main>
)
}

View File

@@ -6,7 +6,11 @@ export default function ComplianceBadges() {
return (
<div className='mt-[6px] flex items-center gap-[12px]'>
{/* SOC2 badge */}
<Link href='https://trust.delve.co/sim-studio' target='_blank' rel='noopener noreferrer'>
<Link
href='https://app.vanta.com/sim.ai/trust/v35ia0jil4l7dteqjgaktn'
target='_blank'
rel='noopener noreferrer'
>
<Image
src='/footer/soc2.png'
alt='SOC2 Compliant'
@@ -18,7 +22,11 @@ export default function ComplianceBadges() {
/>
</Link>
{/* HIPAA badge */}
<Link href='https://trust.delve.co/sim-studio' target='_blank' rel='noopener noreferrer'>
<Link
href='https://app.vanta.com/sim.ai/trust/v35ia0jil4l7dteqjgaktn'
target='_blank'
rel='noopener noreferrer'
>
<HIPAABadgeIcon className='h-[54px] w-[54px]' />
</Link>
</div>

View File

@@ -6,54 +6,132 @@ import { blockTypeToIconMap } from '@/app/(landing)/integrations/data/icon-mappi
import type { Integration } from '@/app/(landing)/integrations/data/types'
import { IntegrationCard } from './integration-card'
const CATEGORY_LABELS: Record<string, string> = {
ai: 'AI',
analytics: 'Analytics',
automation: 'Automation',
communication: 'Communication',
crm: 'CRM',
'customer-support': 'Customer Support',
databases: 'Databases',
design: 'Design',
'developer-tools': 'Developer Tools',
documents: 'Documents',
ecommerce: 'E-commerce',
email: 'Email',
'file-storage': 'File Storage',
hr: 'HR',
media: 'Media',
productivity: 'Productivity',
'sales-intelligence': 'Sales Intelligence',
search: 'Search',
security: 'Security',
social: 'Social',
other: 'Other',
} as const
interface IntegrationGridProps {
integrations: Integration[]
}
export function IntegrationGrid({ integrations }: IntegrationGridProps) {
const [query, setQuery] = useState('')
const [activeCategory, setActiveCategory] = useState<string | null>(null)
const availableCategories = useMemo(() => {
const counts = new Map<string, number>()
for (const i of integrations) {
if (i.integrationType) {
counts.set(i.integrationType, (counts.get(i.integrationType) || 0) + 1)
}
}
return Array.from(counts.entries())
.sort((a, b) => b[1] - a[1])
.map(([key]) => key)
}, [integrations])
const filtered = useMemo(() => {
let results = integrations
if (activeCategory) {
results = results.filter((i) => i.integrationType === activeCategory)
}
const q = query.trim().toLowerCase()
if (!q) return integrations
return integrations.filter(
(i) =>
i.name.toLowerCase().includes(q) ||
i.description.toLowerCase().includes(q) ||
i.operations.some(
(op) => op.name.toLowerCase().includes(q) || op.description.toLowerCase().includes(q)
) ||
i.triggers.some((t) => t.name.toLowerCase().includes(q))
)
}, [integrations, query])
if (q) {
results = results.filter(
(i) =>
i.name.toLowerCase().includes(q) ||
i.description.toLowerCase().includes(q) ||
i.operations.some(
(op) => op.name.toLowerCase().includes(q) || op.description.toLowerCase().includes(q)
) ||
i.triggers.some((t) => t.name.toLowerCase().includes(q))
)
}
return results
}, [integrations, query, activeCategory])
return (
<div>
<div className='relative mb-8 max-w-[480px]'>
<svg
aria-hidden='true'
className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-4 w-4 text-[#555]'
fill='none'
stroke='currentColor'
strokeWidth={2}
viewBox='0 0 24 24'
<div className='mb-6 flex flex-col gap-4 sm:flex-row sm:items-center'>
<div className='relative max-w-[480px] flex-1'>
<svg
aria-hidden='true'
className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-4 w-4 text-[#555]'
fill='none'
stroke='currentColor'
strokeWidth={2}
viewBox='0 0 24 24'
>
<circle cx={11} cy={11} r={8} />
<path d='m21 21-4.35-4.35' />
</svg>
<Input
type='search'
placeholder='Search integrations, tools, or triggers…'
value={query}
onChange={(e) => setQuery(e.target.value)}
className='pl-9'
aria-label='Search integrations'
/>
</div>
</div>
<div className='mb-8 flex flex-wrap gap-2'>
<button
type='button'
onClick={() => setActiveCategory(null)}
className={`rounded-md border px-3 py-1 text-[12px] transition-colors ${
activeCategory === null
? 'border-[#555] bg-[#333] text-[#ECECEC]'
: 'border-[#2A2A2A] bg-transparent text-[#999] hover:border-[#3d3d3d] hover:text-[#ECECEC]'
}`}
>
<circle cx={11} cy={11} r={8} />
<path d='m21 21-4.35-4.35' />
</svg>
<Input
type='search'
placeholder='Search integrations, tools, or triggers…'
value={query}
onChange={(e) => setQuery(e.target.value)}
className='pl-9'
aria-label='Search integrations'
/>
All
</button>
{availableCategories.map((cat) => (
<button
key={cat}
type='button'
onClick={() => setActiveCategory(activeCategory === cat ? null : cat)}
className={`rounded-md border px-3 py-1 text-[12px] transition-colors ${
activeCategory === cat
? 'border-[#555] bg-[#333] text-[#ECECEC]'
: 'border-[#2A2A2A] bg-transparent text-[#999] hover:border-[#3d3d3d] hover:text-[#ECECEC]'
}`}
>
{CATEGORY_LABELS[cat] || cat}
</button>
))}
</div>
{filtered.length === 0 ? (
<p className='py-12 text-center text-[#555] text-[15px]'>
No integrations found for &ldquo;{query}&rdquo;
No integrations found
{query ? <> for &ldquo;{query}&rdquo;</> : null}
{activeCategory ? <> in {CATEGORY_LABELS[activeCategory] || activeCategory}</> : null}
</p>
) : (
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>

File diff suppressed because it is too large Load Diff

View File

@@ -34,4 +34,6 @@ export interface Integration {
triggerCount: number
authType: AuthType
category: string
integrationType?: string
tags?: string[]
}

View File

@@ -0,0 +1,59 @@
import { Skeleton } from '@/components/emcn'
export default function PrivacyLoading() {
return (
<main className='min-h-screen bg-[#1C1C1C] text-[#ECECEC]'>
<div className='flex h-[52px] items-center border-[#2A2A2A] border-b px-6'>
<Skeleton className='h-[22px] w-[60px] rounded-[4px] bg-[#2A2A2A]' />
<div className='ml-auto flex items-center gap-[12px]'>
<Skeleton className='h-[30px] w-[64px] rounded-[5px] bg-[#2A2A2A]' />
<Skeleton className='h-[30px] w-[80px] rounded-[5px] bg-[#2A2A2A]' />
</div>
</div>
<div className='mx-auto max-w-[800px] px-6 pt-[60px] pb-[80px] sm:px-12'>
<Skeleton className='mx-auto h-[48px] w-[280px] rounded-[4px] bg-[#2A2A2A]' />
<div className='mt-12 space-y-8'>
<div className='space-y-[10px]'>
<Skeleton className='h-[15px] w-[180px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[95%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[88%] rounded-[4px] bg-[#2A2A2A]' />
</div>
<div className='mt-12 space-y-[10px]'>
<Skeleton className='h-[28px] w-[320px] rounded-[4px] bg-[#2A2A2A]' />
<div className='mt-4 space-y-[10px]'>
<Skeleton className='h-[20px] w-[160px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[92%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[85%] rounded-[4px] bg-[#2A2A2A]' />
</div>
</div>
<div className='mt-12 space-y-[10px]'>
<Skeleton className='h-[28px] w-[260px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[90%] rounded-[4px] bg-[#2A2A2A]' />
<div className='mt-4 space-y-[8px] pl-6'>
<Skeleton className='h-[15px] w-[70%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[60%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[75%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[65%] rounded-[4px] bg-[#2A2A2A]' />
</div>
</div>
<div className='mt-12 space-y-[10px]'>
<Skeleton className='h-[28px] w-[300px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[95%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[88%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
</div>
<div className='mt-12 space-y-[10px]'>
<Skeleton className='h-[28px] w-[220px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[93%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[80%] rounded-[4px] bg-[#2A2A2A]' />
</div>
</div>
</div>
</main>
)
}

View File

@@ -0,0 +1,52 @@
import { Skeleton } from '@/components/emcn'
export default function TermsLoading() {
return (
<main className='min-h-screen bg-[#1C1C1C] text-[#ECECEC]'>
<div className='flex h-[52px] items-center border-[#2A2A2A] border-b px-6'>
<Skeleton className='h-[22px] w-[60px] rounded-[4px] bg-[#2A2A2A]' />
<div className='ml-auto flex items-center gap-[12px]'>
<Skeleton className='h-[30px] w-[64px] rounded-[5px] bg-[#2A2A2A]' />
<Skeleton className='h-[30px] w-[80px] rounded-[5px] bg-[#2A2A2A]' />
</div>
</div>
<div className='mx-auto max-w-[800px] px-6 pt-[60px] pb-[80px] sm:px-12'>
<Skeleton className='mx-auto h-[48px] w-[320px] rounded-[4px] bg-[#2A2A2A]' />
<div className='mt-12 space-y-8'>
<div className='space-y-[10px]'>
<Skeleton className='h-[15px] w-[180px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[95%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[88%] rounded-[4px] bg-[#2A2A2A]' />
</div>
<div className='mt-12 space-y-[10px]'>
<Skeleton className='h-[28px] w-[140px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[92%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[85%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
</div>
<div className='mt-12 space-y-[10px]'>
<Skeleton className='h-[28px] w-[260px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[90%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[75%] rounded-[4px] bg-[#2A2A2A]' />
</div>
<div className='mt-12 space-y-[10px]'>
<Skeleton className='h-[28px] w-[380px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[95%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[88%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
</div>
<div className='mt-12 space-y-[10px]'>
<Skeleton className='h-[28px] w-[300px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[93%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[15px] w-[80%] rounded-[4px] bg-[#2A2A2A]' />
</div>
</div>
</div>
</main>
)
}

View File

@@ -53,6 +53,11 @@ vi.mock('@/lib/auth/hybrid', () => ({
vi.mock('@/lib/core/utils/request', () => ({
generateRequestId: vi.fn().mockReturnValue('test-req-id'),
}))
vi.mock('@/lib/audit/log', () => ({
recordAudit: vi.fn(),
AuditAction: {},
AuditResourceType: {},
}))
import { GET, PATCH } from '@/app/api/knowledge/[id]/connectors/[connectorId]/documents/route'
@@ -168,8 +173,16 @@ describe('Connector Documents API Route', () => {
})
it('returns success for restore operation', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
mockCheckSession.mockResolvedValue({
success: true,
userId: 'user-1',
userName: 'Test',
userEmail: 'test@test.com',
})
mockCheckWriteAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: { workspaceId: 'ws-1', name: 'Test KB' },
})
mockDbChain.limit.mockResolvedValueOnce([{ id: 'conn-456' }])
mockDbChain.returning.mockResolvedValueOnce([{ id: 'doc-1' }])
@@ -182,8 +195,16 @@ describe('Connector Documents API Route', () => {
})
it('returns success for exclude operation', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
mockCheckSession.mockResolvedValue({
success: true,
userId: 'user-1',
userName: 'Test',
userEmail: 'test@test.com',
})
mockCheckWriteAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: { workspaceId: 'ws-1', name: 'Test KB' },
})
mockDbChain.limit.mockResolvedValueOnce([{ id: 'conn-456' }])
mockDbChain.returning.mockResolvedValueOnce([{ id: 'doc-2' }, { id: 'doc-3' }])

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq, inArray, isNull } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
@@ -184,6 +185,19 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
logger.info(`[${requestId}] Restored ${updated.length} excluded documents`, { connectorId })
recordAudit({
workspaceId: writeCheck.knowledgeBase.workspaceId,
actorId: auth.userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.CONNECTOR_DOCUMENT_RESTORED,
resourceType: AuditResourceType.CONNECTOR,
resourceId: connectorId,
description: `Restored ${updated.length} excluded document(s) for knowledge base "${writeCheck.knowledgeBase.name}"`,
metadata: { knowledgeBaseId, documentCount: updated.length },
request,
})
return NextResponse.json({
success: true,
data: { restoredCount: updated.length, documentIds: updated.map((d) => d.id) },
@@ -206,6 +220,19 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
logger.info(`[${requestId}] Excluded ${updated.length} documents`, { connectorId })
recordAudit({
workspaceId: writeCheck.knowledgeBase.workspaceId,
actorId: auth.userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.CONNECTOR_DOCUMENT_EXCLUDED,
resourceType: AuditResourceType.CONNECTOR,
resourceId: connectorId,
description: `Excluded ${updated.length} document(s) from knowledge base "${writeCheck.knowledgeBase.name}"`,
metadata: { knowledgeBaseId, documentCount: updated.length },
request,
})
return NextResponse.json({
success: true,
data: { excludedCount: updated.length, documentIds: updated.map((d) => d.id) },

View File

@@ -75,6 +75,11 @@ vi.mock('@/lib/knowledge/tags/service', () => ({
vi.mock('@/lib/knowledge/documents/service', () => ({
deleteDocumentStorageFiles: vi.fn().mockResolvedValue(undefined),
}))
vi.mock('@/lib/audit/log', () => ({
recordAudit: vi.fn(),
AuditAction: {},
AuditResourceType: {},
}))
import { DELETE, GET, PATCH } from '@/app/api/knowledge/[id]/connectors/[connectorId]/route'
@@ -183,8 +188,16 @@ describe('Knowledge Connector By ID API Route', () => {
})
it('returns 200 and updates status', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
mockCheckSession.mockResolvedValue({
success: true,
userId: 'user-1',
userName: 'Test',
userEmail: 'test@test.com',
})
mockCheckWriteAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: { workspaceId: 'ws-1', name: 'Test KB' },
})
const updatedConnector = { id: 'conn-456', status: 'paused', syncIntervalMinutes: 120 }
mockDbChain.limit.mockResolvedValueOnce([updatedConnector])
@@ -210,8 +223,16 @@ describe('Knowledge Connector By ID API Route', () => {
})
it('returns 200 on successful hard-delete', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
mockCheckSession.mockResolvedValue({
success: true,
userId: 'user-1',
userName: 'Test',
userEmail: 'test@test.com',
})
mockCheckWriteAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: { workspaceId: 'ws-1', name: 'Test KB' },
})
mockDbChain.where
.mockReturnValueOnce(mockDbChain)
.mockResolvedValueOnce([{ id: 'doc-1', fileUrl: '/api/uploads/test.txt' }])

View File

@@ -11,6 +11,7 @@ import { and, desc, eq, inArray, isNull, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { decryptApiKey } from '@/lib/api-key/crypto'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { deleteDocumentStorageFiles } from '@/lib/knowledge/documents/service'
@@ -233,6 +234,21 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
.limit(1)
const { encryptedApiKey: __, ...updatedData } = updated[0]
recordAudit({
workspaceId: writeCheck.knowledgeBase.workspaceId,
actorId: auth.userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.CONNECTOR_UPDATED,
resourceType: AuditResourceType.CONNECTOR,
resourceId: connectorId,
resourceName: updatedData.connectorType,
description: `Updated connector for knowledge base "${writeCheck.knowledgeBase.name}"`,
metadata: { knowledgeBaseId, updatedFields: Object.keys(parsed.data) },
request,
})
return NextResponse.json({ success: true, data: updatedData })
} catch (error) {
logger.error(`[${requestId}] Error updating connector`, error)
@@ -260,7 +276,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
}
const existingConnector = await db
.select({ id: knowledgeConnector.id })
.select({ id: knowledgeConnector.id, connectorType: knowledgeConnector.connectorType })
.from(knowledgeConnector)
.where(
and(
@@ -323,6 +339,20 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
logger.info(`[${requestId}] Hard-deleted connector ${connectorId} and its documents`)
recordAudit({
workspaceId: writeCheck.knowledgeBase.workspaceId,
actorId: auth.userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.CONNECTOR_DELETED,
resourceType: AuditResourceType.CONNECTOR,
resourceId: connectorId,
resourceName: existingConnector[0].connectorType,
description: `Deleted connector from knowledge base "${writeCheck.knowledgeBase.name}"`,
metadata: { knowledgeBaseId, documentsDeleted: connectorDocuments.length },
request,
})
return NextResponse.json({ success: true })
} catch (error) {
logger.error(`[${requestId}] Error deleting connector`, error)

View File

@@ -43,6 +43,11 @@ vi.mock('@/lib/core/utils/request', () => ({
vi.mock('@/lib/knowledge/connectors/sync-engine', () => ({
dispatchSync: mockDispatchSync,
}))
vi.mock('@/lib/audit/log', () => ({
recordAudit: vi.fn(),
AuditAction: {},
AuditResourceType: {},
}))
import { POST } from '@/app/api/knowledge/[id]/connectors/[connectorId]/sync/route'
@@ -92,8 +97,16 @@ describe('Connector Manual Sync API Route', () => {
})
it('dispatches sync on valid request', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
mockCheckSession.mockResolvedValue({
success: true,
userId: 'user-1',
userName: 'Test',
userEmail: 'test@test.com',
})
mockCheckWriteAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: { workspaceId: 'ws-1', name: 'Test KB' },
})
mockDbChain.limit.mockResolvedValueOnce([{ id: 'conn-456', status: 'active' }])
const req = createMockRequest('POST')

View File

@@ -3,6 +3,7 @@ import { knowledgeConnector } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine'
@@ -54,6 +55,20 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
logger.info(`[${requestId}] Manual sync triggered for connector ${connectorId}`)
recordAudit({
workspaceId: writeCheck.knowledgeBase.workspaceId,
actorId: auth.userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.CONNECTOR_SYNCED,
resourceType: AuditResourceType.CONNECTOR,
resourceId: connectorId,
resourceName: connectorRows[0].connectorType,
description: `Triggered manual sync for connector on knowledge base "${writeCheck.knowledgeBase.name}"`,
metadata: { knowledgeBaseId },
request,
})
dispatchSync(connectorId, { requestId }).catch((error) => {
logger.error(
`[${requestId}] Failed to dispatch manual sync for connector ${connectorId}`,

View File

@@ -5,6 +5,7 @@ import { and, desc, eq, isNull, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { encryptApiKey } from '@/lib/api-key/crypto'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine'
@@ -226,6 +227,20 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
logger.info(`[${requestId}] Created connector ${connectorId} for KB ${knowledgeBaseId}`)
recordAudit({
workspaceId: writeCheck.knowledgeBase.workspaceId,
actorId: auth.userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.CONNECTOR_CREATED,
resourceType: AuditResourceType.CONNECTOR,
resourceId: connectorId,
resourceName: connectorType,
description: `Created ${connectorType} connector for knowledge base "${writeCheck.knowledgeBase.name}"`,
metadata: { knowledgeBaseId, connectorType, syncIntervalMinutes },
request,
})
dispatchSync(connectorId, { requestId }).catch((error) => {
logger.error(
`[${requestId}] Failed to dispatch initial sync for connector ${connectorId}`,

View File

@@ -3,6 +3,7 @@ import { knowledgeBase } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { restoreKnowledgeBase } from '@/lib/knowledge/service'
@@ -23,6 +24,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const [kb] = await db
.select({
id: knowledgeBase.id,
name: knowledgeBase.name,
workspaceId: knowledgeBase.workspaceId,
userId: knowledgeBase.userId,
})
@@ -47,6 +49,19 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
logger.info(`[${requestId}] Restored knowledge base ${id}`)
recordAudit({
workspaceId: kb.workspaceId,
actorId: auth.userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.KNOWLEDGE_BASE_RESTORED,
resourceType: AuditResourceType.KNOWLEDGE_BASE,
resourceId: id,
resourceName: kb.name,
description: `Restored knowledge base "${kb.name}"`,
request,
})
return NextResponse.json({ success: true })
} catch (error) {
logger.error(`[${requestId}] Error restoring knowledge base ${id}`, error)

View File

@@ -27,6 +27,34 @@ export async function GET(request: NextRequest) {
try {
const now = new Date()
const STALE_SYNC_TTL_MS = 120 * 60 * 1000
const staleCutoff = new Date(now.getTime() - STALE_SYNC_TTL_MS)
const recoveredConnectors = await db
.update(knowledgeConnector)
.set({
status: 'error',
lastSyncError: 'Sync timed out (stale lock recovered)',
nextSyncAt: new Date(now.getTime() + 10 * 60 * 1000),
updatedAt: now,
})
.where(
and(
eq(knowledgeConnector.status, 'syncing'),
lte(knowledgeConnector.updatedAt, staleCutoff),
isNull(knowledgeConnector.archivedAt),
isNull(knowledgeConnector.deletedAt)
)
)
.returning({ id: knowledgeConnector.id })
if (recoveredConnectors.length > 0) {
logger.warn(
`[${requestId}] Recovered ${recoveredConnectors.length} stale syncing connectors`,
{ ids: recoveredConnectors.map((c) => c.id) }
)
}
const dueConnectors = await db
.select({
id: knowledgeConnector.id,

View File

@@ -1,5 +1,6 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { getTableById, restoreTable } from '@/lib/table'
@@ -34,6 +35,19 @@ export async function POST(
logger.info(`[${requestId}] Restored table ${tableId}`)
recordAudit({
workspaceId: table.workspaceId,
actorId: auth.userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.TABLE_RESTORED,
resourceType: AuditResourceType.TABLE,
resourceId: tableId,
resourceName: table.name,
description: `Restored table "${table.name}"`,
request,
})
return NextResponse.json({ success: true })
} catch (error) {
logger.error(`[${requestId}] Error restoring table ${tableId}`, error)

View File

@@ -1,5 +1,6 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { restoreWorkflow } from '@/lib/workflows/lifecycle'
@@ -44,6 +45,19 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
logger.info(`[${requestId}] Restored workflow ${workflowId}`)
recordAudit({
workspaceId: workflowData.workspaceId,
actorId: auth.userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.WORKFLOW_RESTORED,
resourceType: AuditResourceType.WORKFLOW,
resourceId: workflowId,
resourceName: workflowData.name,
description: `Restored workflow "${workflowData.name}"`,
request,
})
return NextResponse.json({ success: true })
} catch (error) {
logger.error(`[${requestId}] Error restoring workflow ${workflowId}`, error)

View File

@@ -1,5 +1,6 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { restoreWorkspaceFile } from '@/lib/uploads/contexts/workspace'
@@ -29,6 +30,19 @@ export async function POST(
logger.info(`[${requestId}] Restored workspace file ${fileId}`)
recordAudit({
workspaceId,
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.FILE_RESTORED,
resourceType: AuditResourceType.FILE,
resourceId: fileId,
resourceName: fileId,
description: `Restored workspace file ${fileId}`,
request,
})
return NextResponse.json({ success: true })
} catch (error) {
logger.error(`[${requestId}] Error restoring workspace file ${fileId}`, error)

View File

@@ -0,0 +1,38 @@
import { Skeleton } from '@/components/emcn'
export default function ChangelogLoading() {
return (
<div className='min-h-screen'>
<div className='relative grid md:grid-cols-2'>
<div className='relative top-0 overflow-hidden border-[#2A2A2A] border-b px-6 py-16 sm:px-10 md:sticky md:h-dvh md:border-r md:border-b-0 md:px-12 md:py-24'>
<div className='relative mx-auto h-full max-w-xl md:flex md:flex-col md:justify-center'>
<Skeleton className='mt-6 h-[48px] w-[200px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='mt-4 h-[14px] w-[300px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='mt-[4px] h-[14px] w-[260px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='mt-6 h-[1px] w-full bg-[#2A2A2A]' />
<div className='mt-6 flex flex-wrap items-center gap-3'>
<Skeleton className='h-[32px] w-[130px] rounded-[5px] bg-[#2A2A2A]' />
<Skeleton className='h-[32px] w-[120px] rounded-[5px] bg-[#2A2A2A]' />
<Skeleton className='h-[32px] w-[80px] rounded-[5px] bg-[#2A2A2A]' />
</div>
</div>
</div>
<div className='px-6 py-16 sm:px-10 md:px-12 md:py-24'>
<div className='max-w-2xl space-y-[32px]'>
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className='space-y-[12px]'>
<Skeleton className='h-[20px] w-[160px] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[14px] w-[100px] rounded-[4px] bg-[#2A2A2A]' />
<div className='space-y-[8px]'>
<Skeleton className='h-[14px] w-full rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[14px] w-[90%] rounded-[4px] bg-[#2A2A2A]' />
<Skeleton className='h-[14px] w-[75%] rounded-[4px] bg-[#2A2A2A]' />
</div>
</div>
))}
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,39 @@
import { Skeleton } from '@/components/emcn'
export default function ChatLoading() {
return (
<div className='fixed inset-0 z-[100] flex flex-col bg-white text-foreground'>
<div className='border-b px-4 py-3'>
<div className='mx-auto flex max-w-3xl items-center justify-between'>
<div className='flex items-center gap-[12px]'>
<Skeleton className='h-[28px] w-[28px] rounded-[6px]' />
<Skeleton className='h-[18px] w-[120px] rounded-[4px]' />
</div>
<Skeleton className='h-[28px] w-[80px] rounded-[6px]' />
</div>
</div>
<div className='flex min-h-0 flex-1 items-center justify-center px-4'>
<div className='w-full max-w-[410px]'>
<div className='flex flex-col items-center justify-center'>
<div className='space-y-2 text-center'>
<Skeleton className='mx-auto h-8 w-32' />
<Skeleton className='mx-auto h-4 w-48' />
</div>
<div className='mt-8 w-full space-y-8'>
<div className='space-y-2'>
<Skeleton className='h-4 w-16' />
<Skeleton className='h-10 w-full rounded-[10px]' />
</div>
<Skeleton className='h-10 w-full rounded-[10px]' />
</div>
</div>
</div>
</div>
<div className='relative p-3 pb-4 md:p-4 md:pb-6'>
<div className='relative mx-auto max-w-3xl md:max-w-[748px]'>
<Skeleton className='h-[48px] w-full rounded-[12px]' />
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,19 @@
import { Skeleton } from '@/components/emcn'
export default function CredentialAccountLoading() {
return (
<main className='relative flex min-h-screen flex-col text-foreground'>
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
<div className='w-full max-w-lg px-4'>
<div className='flex flex-col items-center justify-center'>
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
<Skeleton className='mt-[16px] h-[24px] w-[200px] rounded-[4px]' />
<Skeleton className='mt-[8px] h-[14px] w-[280px] rounded-[4px]' />
<Skeleton className='mt-[4px] h-[14px] w-[240px] rounded-[4px]' />
<Skeleton className='mt-[24px] h-[44px] w-[200px] rounded-[10px]' />
</div>
</div>
</div>
</main>
)
}

View File

@@ -0,0 +1,29 @@
import { Skeleton } from '@/components/emcn'
export default function FormLoading() {
return (
<main className='relative flex min-h-screen flex-col text-foreground'>
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
<div className='w-full max-w-[410px]'>
<div className='flex flex-col items-center justify-center'>
<div className='space-y-2 text-center'>
<Skeleton className='mx-auto h-8 w-32' />
<Skeleton className='mx-auto h-4 w-48' />
</div>
<div className='mt-8 w-full space-y-8'>
<div className='space-y-2'>
<Skeleton className='h-4 w-16' />
<Skeleton className='h-10 w-full rounded-[10px]' />
</div>
<div className='space-y-2'>
<Skeleton className='h-4 w-20' />
<Skeleton className='h-10 w-full rounded-[10px]' />
</div>
<Skeleton className='h-10 w-full rounded-[10px]' />
</div>
</div>
</div>
</div>
</main>
)
}

View File

@@ -0,0 +1,13 @@
import { Skeleton } from '@/components/emcn'
export default function InviteLoading() {
return (
<div className='flex flex-col items-center justify-center'>
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
<Skeleton className='mt-[16px] h-[24px] w-[200px] rounded-[4px]' />
<Skeleton className='mt-[8px] h-[14px] w-[280px] rounded-[4px]' />
<Skeleton className='mt-[4px] h-[14px] w-[240px] rounded-[4px]' />
<Skeleton className='mt-[24px] h-[44px] w-[200px] rounded-[10px]' />
</div>
)
}

View File

@@ -0,0 +1,42 @@
import { Skeleton } from '@/components/emcn'
export default function ResumeLoading() {
return (
<div className='min-h-screen bg-background'>
<div className='border-b px-4 py-3'>
<div className='mx-auto flex max-w-[1200px] items-center justify-between'>
<Skeleton className='h-[24px] w-[80px] rounded-[4px]' />
<Skeleton className='h-[28px] w-[100px] rounded-[6px]' />
</div>
</div>
<div className='mx-auto max-w-[1200px] px-6 py-8'>
<div className='grid grid-cols-[280px_1fr] gap-6'>
<div className='space-y-[8px]'>
<Skeleton className='h-[20px] w-[120px] rounded-[4px]' />
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className='h-[48px] w-full rounded-[8px]' />
))}
</div>
<div className='rounded-[8px] border p-6'>
<Skeleton className='h-[24px] w-[200px] rounded-[4px]' />
<Skeleton className='mt-[12px] h-[16px] w-[320px] rounded-[4px]' />
<div className='mt-[24px] space-y-[16px]'>
<div className='space-y-[8px]'>
<Skeleton className='h-[14px] w-[80px] rounded-[4px]' />
<Skeleton className='h-[40px] w-full rounded-[8px]' />
</div>
<div className='space-y-[8px]'>
<Skeleton className='h-[14px] w-[100px] rounded-[4px]' />
<Skeleton className='h-[80px] w-full rounded-[8px]' />
</div>
</div>
<div className='mt-[24px] flex gap-[12px]'>
<Skeleton className='h-[40px] w-[120px] rounded-[8px]' />
<Skeleton className='h-[40px] w-[120px] rounded-[8px]' />
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,55 @@
import { Skeleton } from '@/components/emcn'
export default function TemplateDetailLoading() {
return (
<div className='flex min-h-screen flex-col'>
<div className='border-b px-6 py-3'>
<div className='mx-auto flex max-w-[1200px] items-center justify-between'>
<Skeleton className='h-[24px] w-[80px] rounded-[4px]' />
<div className='flex items-center gap-[12px]'>
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
</div>
</div>
</div>
<div className='mx-auto w-full max-w-[1200px] px-6 pt-[24px] pb-[24px]'>
<div className='flex items-center gap-[8px]'>
<Skeleton className='h-[14px] w-[72px] rounded-[4px]' />
<Skeleton className='h-[14px] w-[8px] rounded-[2px]' />
<Skeleton className='h-[14px] w-[120px] rounded-[4px]' />
</div>
<div className='mt-[14px] flex items-center justify-between'>
<Skeleton className='h-[27px] w-[250px] rounded-[4px]' />
<div className='flex items-center gap-[8px]'>
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
</div>
</div>
<Skeleton className='mt-[4px] h-[16px] w-[360px] rounded-[4px]' />
<div className='mt-[16px] flex items-center gap-[8px]'>
<Skeleton className='h-[16px] w-[48px] rounded-[4px]' />
<Skeleton className='h-[16px] w-[48px] rounded-[4px]' />
<Skeleton className='h-[16px] w-[1px] rounded-[1px]' />
<Skeleton className='h-[20px] w-[20px] rounded-full' />
<Skeleton className='h-[16px] w-[80px] rounded-[4px]' />
</div>
<Skeleton className='mt-[24px] h-[450px] w-full rounded-[8px]' />
<Skeleton className='mt-[32px] h-[20px] w-[180px] rounded-[4px]' />
<div className='mt-[12px] space-y-[8px]'>
<Skeleton className='h-[14px] w-full rounded-[4px]' />
<Skeleton className='h-[14px] w-[85%] rounded-[4px]' />
<Skeleton className='h-[14px] w-[70%] rounded-[4px]' />
</div>
<Skeleton className='mt-[32px] h-[20px] w-[160px] rounded-[4px]' />
<div className='mt-[12px] flex items-center gap-[12px]'>
<Skeleton className='h-[48px] w-[48px] rounded-full' />
<div className='space-y-[6px]'>
<Skeleton className='h-[16px] w-[120px] rounded-[4px]' />
<Skeleton className='h-[14px] w-[200px] rounded-[4px]' />
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,36 @@
import { Skeleton } from '@/components/emcn'
const SKELETON_CARD_COUNT = 8
export default function TemplatesLoading() {
return (
<div className='min-h-screen bg-white'>
<div className='border-b px-6 py-3'>
<div className='mx-auto flex max-w-[1200px] items-center justify-between'>
<Skeleton className='h-[24px] w-[80px] rounded-[4px]' />
<div className='flex items-center gap-[12px]'>
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
</div>
</div>
</div>
<div className='mx-auto max-w-[1200px] px-6 py-8'>
<Skeleton className='h-[40px] w-[400px] rounded-[8px]' />
<div className='mt-[16px] flex gap-[8px]'>
<Skeleton className='h-[32px] w-[64px] rounded-[6px]' />
</div>
<div className='mt-[24px] grid grid-cols-1 gap-x-[20px] gap-y-[40px] md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
{Array.from({ length: SKELETON_CARD_COUNT }).map((_, i) => (
<div key={i} className='h-[268px] w-full rounded-[8px] bg-[var(--surface-3)] p-[8px]'>
<Skeleton className='h-[180px] w-full rounded-[6px]' />
<div className='mt-[10px] px-[4px]'>
<Skeleton className='h-[14px] w-[120px] rounded-[4px]' />
<Skeleton className='mt-[6px] h-[12px] w-[180px] rounded-[4px]' />
</div>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,13 @@
import { Skeleton } from '@/components/emcn'
export default function UnsubscribeLoading() {
return (
<div className='flex flex-col items-center justify-center'>
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
<Skeleton className='mt-[16px] h-[24px] w-[180px] rounded-[4px]' />
<Skeleton className='mt-[8px] h-[14px] w-[300px] rounded-[4px]' />
<Skeleton className='mt-[4px] h-[14px] w-[260px] rounded-[4px]' />
<Skeleton className='mt-[24px] h-[44px] w-[200px] rounded-[10px]' />
</div>
)
}

View File

@@ -0,0 +1,9 @@
import { Loader2 } from 'lucide-react'
export default function FileViewLoading() {
return (
<div className='fixed inset-0 z-50 flex items-center justify-center bg-[var(--bg)]'>
<Loader2 className='h-[20px] w-[20px] animate-spin text-[var(--text-tertiary)]' />
</div>
)
}

View File

@@ -0,0 +1,62 @@
import { Skeleton } from '@/components/emcn'
const SKELETON_ROW_COUNT = 5
const COLUMN_COUNT = 6
export default function FilesLoading() {
return (
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-[12px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='h-[14px] w-[32px] rounded-[4px]' />
</div>
<div className='flex items-center gap-[6px]'>
<Skeleton className='h-[28px] w-[72px] rounded-[6px]' />
<Skeleton className='h-[28px] w-[64px] rounded-[6px]' />
</div>
</div>
</div>
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
<div className='flex items-center'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='ml-[10px] h-[14px] w-[120px] rounded-[4px]' />
</div>
</div>
<div className='min-h-0 flex-1 overflow-auto'>
<table className='w-full'>
<thead>
<tr className='border-[var(--border)] border-b'>
<th className='w-[40px] px-[12px] py-[8px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
</th>
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
<th key={i} className='px-[12px] py-[8px] text-left'>
<Skeleton className='h-[12px] w-[56px] rounded-[4px]' />
</th>
))}
</tr>
</thead>
<tbody>
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
<tr key={rowIndex} className='border-[var(--border)] border-b'>
<td className='w-[40px] px-[12px] py-[10px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
</td>
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
<td key={colIndex} className='px-[12px] py-[10px]'>
<Skeleton
className='h-[14px] rounded-[4px]'
style={{ width: colIndex === 0 ? '128px' : '80px' }}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}

View File

@@ -0,0 +1,22 @@
import { Skeleton } from '@/components/emcn'
const SKELETON_LINE_COUNT = 4
export default function HomeLoading() {
return (
<div className='flex h-full flex-col bg-[var(--bg)]'>
<div className='min-h-0 flex-1 overflow-hidden px-6 py-4'>
<div className='mx-auto max-w-[42rem] space-y-[10px] pt-3'>
{Array.from({ length: SKELETON_LINE_COUNT }).map((_, i) => (
<Skeleton key={i} className='h-[16px]' style={{ width: `${120 + (i % 4) * 48}px` }} />
))}
</div>
</div>
<div className='flex-shrink-0 px-[24px] pb-[16px]'>
<div className='mx-auto max-w-[42rem]'>
<Skeleton className='h-[48px] w-full rounded-[12px]' />
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,67 @@
import { Skeleton } from '@/components/emcn'
const SKELETON_ROW_COUNT = 5
const COLUMN_COUNT = 4
export default function DocumentLoading() {
return (
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
<div className='border-[var(--border)] border-b px-[16px] py-[8.5px]'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-[8px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='h-[14px] w-[96px] rounded-[4px]' />
<Skeleton className='h-[14px] w-[8px] rounded-[2px]' />
<Skeleton className='h-[14px] w-[100px] rounded-[4px]' />
<Skeleton className='h-[14px] w-[8px] rounded-[2px]' />
<Skeleton className='h-[14px] w-[80px] rounded-[4px]' />
</div>
</div>
</div>
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
<div className='flex items-center justify-between'>
<div className='flex items-center'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='ml-[10px] h-[14px] w-[120px] rounded-[4px]' />
</div>
<div className='flex items-center gap-[6px]'>
<Skeleton className='h-[28px] w-[56px] rounded-[6px]' />
</div>
</div>
</div>
<div className='min-h-0 flex-1 overflow-auto'>
<table className='w-full'>
<thead>
<tr className='border-[var(--border)] border-b'>
<th className='w-[40px] px-[12px] py-[8px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
</th>
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
<th key={i} className='px-[12px] py-[8px] text-left'>
<Skeleton className='h-[12px] w-[56px] rounded-[4px]' />
</th>
))}
</tr>
</thead>
<tbody>
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
<tr key={rowIndex} className='border-[var(--border)] border-b'>
<td className='w-[40px] px-[12px] py-[10px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
</td>
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
<td key={colIndex} className='px-[12px] py-[10px]'>
<Skeleton
className='h-[14px] rounded-[4px]'
style={{ width: colIndex === 0 ? '200px' : '80px' }}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}

View File

@@ -0,0 +1,69 @@
import { Skeleton } from '@/components/emcn'
const SKELETON_ROW_COUNT = 5
const COLUMN_COUNT = 7
export default function KnowledgeBaseLoading() {
return (
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
<div className='border-[var(--border)] border-b px-[16px] py-[8.5px]'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-[8px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='h-[14px] w-[96px] rounded-[4px]' />
<Skeleton className='h-[14px] w-[8px] rounded-[2px]' />
<Skeleton className='h-[14px] w-[120px] rounded-[4px]' />
</div>
<div className='flex items-center gap-[6px]'>
<Skeleton className='h-[28px] w-[112px] rounded-[6px]' />
</div>
</div>
</div>
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
<div className='flex items-center justify-between'>
<div className='flex items-center'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='ml-[10px] h-[14px] w-[140px] rounded-[4px]' />
</div>
<div className='flex items-center gap-[6px]'>
<Skeleton className='h-[28px] w-[56px] rounded-[6px]' />
<Skeleton className='h-[28px] w-[56px] rounded-[6px]' />
</div>
</div>
</div>
<div className='min-h-0 flex-1 overflow-auto'>
<table className='w-full'>
<thead>
<tr className='border-[var(--border)] border-b'>
<th className='w-[40px] px-[12px] py-[8px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
</th>
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
<th key={i} className='px-[12px] py-[8px] text-left'>
<Skeleton className='h-[12px] w-[56px] rounded-[4px]' />
</th>
))}
</tr>
</thead>
<tbody>
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
<tr key={rowIndex} className='border-[var(--border)] border-b'>
<td className='w-[40px] px-[12px] py-[10px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
</td>
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
<td key={colIndex} className='px-[12px] py-[10px]'>
<Skeleton
className='h-[14px] rounded-[4px]'
style={{ width: colIndex === 0 ? '128px' : '80px' }}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}

View File

@@ -0,0 +1,63 @@
import { Skeleton } from '@/components/emcn'
const SKELETON_ROW_COUNT = 5
const COLUMN_COUNT = 6
export default function KnowledgeLoading() {
return (
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-[12px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='h-[14px] w-[96px] rounded-[4px]' />
</div>
<div className='flex items-center gap-[6px]'>
<Skeleton className='h-[28px] w-[80px] rounded-[6px]' />
</div>
</div>
</div>
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
<div className='flex items-center'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='ml-[10px] h-[14px] w-[160px] rounded-[4px]' />
</div>
</div>
<div className='min-h-0 flex-1 overflow-auto'>
<table className='w-full'>
<thead>
<tr className='border-[var(--border)] border-b'>
<th className='w-[40px] px-[12px] py-[8px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
</th>
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
<th key={i} className='px-[12px] py-[8px] text-left'>
<Skeleton className='h-[12px] w-[56px] rounded-[4px]' />
</th>
))}
</tr>
</thead>
<tbody>
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
<tr key={rowIndex} className='border-[var(--border)] border-b'>
<td className='w-[40px] px-[12px] py-[10px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
</td>
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
<td key={colIndex} className='px-[12px] py-[10px]'>
<Skeleton
className='h-[14px] rounded-[4px]'
style={{ width: colIndex === 0 ? '128px' : '80px' }}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}

View File

@@ -0,0 +1,71 @@
import { Skeleton } from '@/components/emcn'
const SKELETON_ROW_COUNT = 5
const COLUMN_COUNT = 6
export default function LogsLoading() {
return (
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-[12px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='h-[14px] w-[32px] rounded-[4px]' />
</div>
<div className='flex items-center gap-[6px]'>
<Skeleton className='h-[28px] w-[64px] rounded-[6px]' />
<Skeleton className='h-[28px] w-[96px] rounded-[6px]' />
<Skeleton className='h-[28px] w-[28px] rounded-[6px]' />
<Skeleton className='h-[28px] w-[56px] rounded-[6px]' />
<Skeleton className='h-[28px] w-[72px] rounded-[6px]' />
</div>
</div>
</div>
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
<div className='flex items-center justify-between'>
<div className='flex items-center'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='ml-[10px] h-[14px] w-[120px] rounded-[4px]' />
</div>
<div className='flex items-center gap-[6px]'>
<Skeleton className='h-[28px] w-[56px] rounded-[6px]' />
<Skeleton className='h-[28px] w-[56px] rounded-[6px]' />
</div>
</div>
</div>
<div className='min-h-0 flex-1 overflow-auto'>
<table className='w-full'>
<thead>
<tr className='border-[var(--border)] border-b'>
<th className='w-[40px] px-[12px] py-[8px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
</th>
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
<th key={i} className='px-[12px] py-[8px] text-left'>
<Skeleton className='h-[12px] w-[56px] rounded-[4px]' />
</th>
))}
</tr>
</thead>
<tbody>
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
<tr key={rowIndex} className='border-[var(--border)] border-b'>
<td className='w-[40px] px-[12px] py-[10px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
</td>
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
<td key={colIndex} className='px-[12px] py-[10px]'>
<Skeleton
className='h-[14px] rounded-[4px]'
style={{ width: colIndex === 0 ? '128px' : '80px' }}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}

View File

@@ -0,0 +1,63 @@
import { Skeleton } from '@/components/emcn'
const SKELETON_ROW_COUNT = 5
const COLUMN_COUNT = 4
export default function ScheduledTasksLoading() {
return (
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-[12px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='h-[14px] w-[104px] rounded-[4px]' />
</div>
<div className='flex items-center gap-[6px]'>
<Skeleton className='h-[28px] w-[136px] rounded-[6px]' />
</div>
</div>
</div>
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
<div className='flex items-center'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='ml-[10px] h-[14px] w-[160px] rounded-[4px]' />
</div>
</div>
<div className='min-h-0 flex-1 overflow-auto'>
<table className='w-full'>
<thead>
<tr className='border-[var(--border)] border-b'>
<th className='w-[40px] px-[12px] py-[8px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
</th>
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
<th key={i} className='px-[12px] py-[8px] text-left'>
<Skeleton className='h-[12px] w-[56px] rounded-[4px]' />
</th>
))}
</tr>
</thead>
<tbody>
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
<tr key={rowIndex} className='border-[var(--border)] border-b'>
<td className='w-[40px] px-[12px] py-[10px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
</td>
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
<td key={colIndex} className='px-[12px] py-[10px]'>
<Skeleton
className='h-[14px] rounded-[4px]'
style={{
width: colIndex === 0 ? '128px' : colIndex === 1 ? '160px' : '80px',
}}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}

View File

@@ -0,0 +1,15 @@
import { Skeleton } from '@/components/emcn'
export default function SettingsLoading() {
return (
<div>
<Skeleton className='mb-[28px] h-[28px] w-[140px] rounded-[4px]' />
<div className='flex flex-col gap-[16px]'>
<Skeleton className='h-[20px] w-[200px] rounded-[4px]' />
<Skeleton className='h-[40px] w-full rounded-[8px]' />
<Skeleton className='h-[40px] w-full rounded-[8px]' />
<Skeleton className='h-[40px] w-full rounded-[8px]' />
</div>
</div>
)
}

View File

@@ -0,0 +1,51 @@
import { Skeleton } from '@/components/emcn'
const SKELETON_ROW_COUNT = 8
const COLUMN_COUNT = 5
export default function TableDetailLoading() {
return (
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
<div className='border-[var(--border)] border-b px-[16px] py-[8.5px]'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-[8px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='h-[14px] w-[44px] rounded-[4px]' />
<Skeleton className='h-[14px] w-[8px] rounded-[2px]' />
<Skeleton className='h-[14px] w-[100px] rounded-[4px]' />
</div>
<div className='flex items-center gap-[6px]'>
<Skeleton className='h-[28px] w-[80px] rounded-[6px]' />
</div>
</div>
</div>
<div className='min-h-0 flex-1 overflow-auto'>
<table className='w-full'>
<thead>
<tr className='border-[var(--border)] border-b'>
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
<th key={i} className='px-[12px] py-[8px] text-left'>
<Skeleton className='h-[12px] w-[72px] rounded-[4px]' />
</th>
))}
</tr>
</thead>
<tbody>
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
<tr key={rowIndex} className='border-[var(--border)] border-b'>
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
<td key={colIndex} className='px-[12px] py-[10px]'>
<Skeleton
className='h-[14px] rounded-[4px]'
style={{ width: `${80 + (colIndex % 3) * 40}px` }}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}

View File

@@ -0,0 +1,61 @@
import { Skeleton } from '@/components/emcn'
const SKELETON_ROW_COUNT = 5
const COLUMN_COUNT = 6
export default function TablesLoading() {
return (
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-[12px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='h-[14px] w-[44px] rounded-[4px]' />
</div>
<div className='flex items-center gap-[6px]'>
<Skeleton className='h-[28px] w-[80px] rounded-[6px]' />
</div>
</div>
</div>
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
<div className='flex items-center'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='ml-[10px] h-[14px] w-[120px] rounded-[4px]' />
</div>
</div>
<div className='min-h-0 flex-1 overflow-auto'>
<table className='w-full'>
<thead>
<tr className='border-[var(--border)] border-b'>
<th className='w-[40px] px-[12px] py-[8px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
</th>
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
<th key={i} className='px-[12px] py-[8px] text-left'>
<Skeleton className='h-[12px] w-[56px] rounded-[4px]' />
</th>
))}
</tr>
</thead>
<tbody>
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
<tr key={rowIndex} className='border-[var(--border)] border-b'>
<td className='w-[40px] px-[12px] py-[10px]'>
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
</td>
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
<td key={colIndex} className='px-[12px] py-[10px]'>
<Skeleton
className='h-[14px] rounded-[4px]'
style={{ width: colIndex === 0 ? '128px' : '80px' }}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}

View File

@@ -0,0 +1,47 @@
import { Skeleton } from '@/components/emcn'
export default function TemplateDetailLoading() {
return (
<div className='flex h-full flex-1 flex-col overflow-hidden'>
<div className='flex flex-1 overflow-hidden'>
<div className='flex flex-1 flex-col overflow-auto bg-white px-[24px] pt-[24px] pb-[24px] dark:bg-[var(--bg)]'>
<div className='flex items-center gap-[8px]'>
<Skeleton className='h-[14px] w-[72px] rounded-[4px]' />
<Skeleton className='h-[14px] w-[8px] rounded-[2px]' />
<Skeleton className='h-[14px] w-[120px] rounded-[4px]' />
</div>
<div className='mt-[14px] flex items-center justify-between'>
<Skeleton className='h-[27px] w-[250px] rounded-[4px]' />
<div className='flex items-center gap-[8px]'>
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
</div>
</div>
<Skeleton className='mt-[4px] h-[16px] w-[360px] rounded-[4px]' />
<div className='mt-[16px] flex items-center gap-[8px]'>
<Skeleton className='h-[16px] w-[48px] rounded-[4px]' />
<Skeleton className='h-[16px] w-[48px] rounded-[4px]' />
<Skeleton className='h-[16px] w-[1px] rounded-[1px]' />
<Skeleton className='h-[20px] w-[20px] rounded-full' />
<Skeleton className='h-[16px] w-[80px] rounded-[4px]' />
</div>
<Skeleton className='mt-[24px] h-[450px] w-full rounded-[8px]' />
<Skeleton className='mt-[32px] h-[20px] w-[180px] rounded-[4px]' />
<div className='mt-[12px] space-y-[8px]'>
<Skeleton className='h-[14px] w-full rounded-[4px]' />
<Skeleton className='h-[14px] w-[85%] rounded-[4px]' />
<Skeleton className='h-[14px] w-[70%] rounded-[4px]' />
</div>
<Skeleton className='mt-[32px] h-[20px] w-[160px] rounded-[4px]' />
<div className='mt-[12px] flex items-center gap-[12px]'>
<Skeleton className='h-[48px] w-[48px] rounded-full' />
<div className='space-y-[6px]'>
<Skeleton className='h-[16px] w-[120px] rounded-[4px]' />
<Skeleton className='h-[14px] w-[200px] rounded-[4px]' />
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,38 @@
import { Skeleton } from '@/components/emcn'
const SKELETON_CARD_COUNT = 8
export default function TemplatesLoading() {
return (
<div className='flex h-full flex-1 flex-col'>
<div className='flex flex-1 overflow-hidden'>
<div className='flex flex-1 flex-col overflow-auto bg-[var(--bg)] px-[24px] pt-[28px] pb-[24px]'>
<div className='flex items-center gap-[12px]'>
<Skeleton className='h-[26px] w-[26px] rounded-[6px]' />
<Skeleton className='h-[22px] w-[80px] rounded-[4px]' />
</div>
<Skeleton className='mt-[6px] h-[14px] w-[280px] rounded-[4px]' />
<div className='mt-[14px] flex items-center justify-between'>
<Skeleton className='h-[32px] w-[400px] rounded-[8px]' />
<div className='flex items-center gap-[8px]'>
<Skeleton className='h-[32px] w-[64px] rounded-[6px]' />
<Skeleton className='h-[32px] w-[100px] rounded-[6px]' />
</div>
</div>
<div className='mt-[24px] grid grid-cols-1 gap-x-[20px] gap-y-[40px] md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
{Array.from({ length: SKELETON_CARD_COUNT }).map((_, i) => (
<div key={i} className='h-[268px] w-full rounded-[8px] bg-[var(--surface-3)] p-[8px]'>
<Skeleton className='h-[180px] w-full rounded-[6px]' />
<div className='mt-[10px] px-[4px]'>
<Skeleton className='h-[14px] w-[120px] rounded-[4px]' />
<Skeleton className='mt-[6px] h-[12px] w-[180px] rounded-[4px]' />
</div>
</div>
))}
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,11 @@
import { Loader2 } from 'lucide-react'
export default function WorkflowLoading() {
return (
<div className='flex h-full w-full flex-col overflow-hidden bg-[var(--bg)]'>
<div className='relative flex h-full w-full flex-1 items-center justify-center bg-[var(--bg)]'>
<Loader2 className='h-[20px] w-[20px] animate-spin text-[var(--text-tertiary)]' />
</div>
</div>
)
}

View File

@@ -201,8 +201,6 @@ const edgeTypes: EdgeTypes = {
const defaultEdgeOptions = { type: 'custom' }
const reactFlowStyles = [
'[&_.react-flow__edges]:!z-0',
'[&_.react-flow__node]:z-[21]',
'[&_.react-flow__handle]:!z-[30]',
'[&_.react-flow__edge-labels]:!z-[1001]',
'[&_.react-flow__pane]:select-none',
@@ -2478,6 +2476,7 @@ const WorkflowContent = React.memo(
// Local state for nodes - allows smooth drag without store updates on every frame
const [displayNodes, setDisplayNodes] = useState<Node[]>([])
const [lastInteractedNodeId, setLastInteractedNodeId] = useState<string | null>(null)
const selectedNodeIds = useMemo(
() => displayNodes.filter((node) => node.selected).map((node) => node.id),
@@ -2489,6 +2488,14 @@ const WorkflowContent = React.memo(
syncPanelWithSelection(selectedNodeIds)
}, [selectedNodeIdsKey])
// Keep the most recently selected block on top even after deselection, so a
// dragged block doesn't suddenly drop behind other overlapping blocks.
useEffect(() => {
if (selectedNodeIds.length > 0) {
setLastInteractedNodeId(selectedNodeIds[selectedNodeIds.length - 1])
}
}, [selectedNodeIdsKey])
useEffect(() => {
// Check for pending selection (from paste/duplicate), otherwise preserve existing selection
if (pendingSelection && pendingSelection.length > 0) {
@@ -3723,18 +3730,58 @@ const WorkflowContent = React.memo(
[removeEdge, edges, blocks, addNotification, activeWorkflowId]
)
// Elevate nodes using React Flow's native zIndex so selected/recent blocks
// always sit above edges and other blocks.
//
// Z-index layers (regular blocks):
// 21 — default
// 22 — last interacted (dragged/selected, now deselected) so it stays on
// top of siblings until another block is touched
// 31 — currently selected (above connected edges at z-22 and handles at z-30)
//
// Subflow container nodes are skipped — they use depth-based zIndex for
// correct parent/child layering and must not be bumped.
// Child blocks inside containers already carry zIndex 1000 and are bumped by
// +10 when selected so they stay above their sibling child blocks.
const nodesForRender = useMemo(() => {
return displayNodes.map((node) => {
if (node.type === 'subflowNode') return node
const base = node.zIndex ?? 21
const target = node.selected
? base + 10
: node.id === lastInteractedNodeId
? Math.max(base + 1, 22)
: base
if (target === (node.zIndex ?? 21)) return node
return { ...node, zIndex: target }
})
}, [displayNodes, lastInteractedNodeId])
/** Transforms edges to include selection state and delete handlers. Memoized to prevent re-renders. */
const edgesWithSelection = useMemo(() => {
const nodeMap = new Map(displayNodes.map((n) => [n.id, n]))
const elevatedNodeIdSet = new Set(
lastInteractedNodeId ? [...selectedNodeIds, lastInteractedNodeId] : selectedNodeIds
)
return edgesForDisplay.map((edge) => {
const sourceNode = nodeMap.get(edge.source)
const targetNode = nodeMap.get(edge.target)
const parentLoopId = sourceNode?.parentId || targetNode?.parentId
const edgeContextId = `${edge.id}${parentLoopId ? `-${parentLoopId}` : ''}`
const connectedToElevated =
elevatedNodeIdSet.has(edge.source) || elevatedNodeIdSet.has(edge.target)
// Derive elevated z-index from connected nodes so edges inside subflows
// (child nodes at z-1000) stay above their sibling child blocks.
const elevatedZIndex = Math.max(
22,
(sourceNode?.zIndex ?? 21) + 1,
(targetNode?.zIndex ?? 21) + 1
)
return {
...edge,
zIndex: connectedToElevated ? elevatedZIndex : 0,
data: {
...edge.data,
isSelected: selectedEdges.has(edgeContextId),
@@ -3745,7 +3792,14 @@ const WorkflowContent = React.memo(
},
}
})
}, [edgesForDisplay, displayNodes, selectedEdges, handleEdgeDelete])
}, [
edgesForDisplay,
displayNodes,
selectedNodeIds,
selectedEdges,
handleEdgeDelete,
lastInteractedNodeId,
])
/** Handles Delete/Backspace to remove selected edges or blocks. */
useEffect(() => {
@@ -3885,7 +3939,7 @@ const WorkflowContent = React.memo(
{showTrainingModal && <TrainingModal />}
<ReactFlow
nodes={displayNodes}
nodes={nodesForRender}
edges={edgesWithSelection}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
@@ -3952,7 +4006,7 @@ const WorkflowContent = React.memo(
onNodeDragStart={effectivePermissions.canEdit ? onNodeDragStart : undefined}
snapToGrid={snapToGrid}
snapGrid={snapGrid}
elevateEdgesOnSelect={true}
elevateEdgesOnSelect={false}
onlyRenderVisibleElements={false}
deleteKeyCode={null}
elevateNodesOnSelect={false}

View File

@@ -4,13 +4,11 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/emcn'
import { Plus } from '@/components/emcn/icons'
import { cn } from '@/lib/core/utils/cn'
import type { useHoverMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
import type { FolderTreeNode } from '@/stores/folders/types'
@@ -23,8 +21,6 @@ interface CollapsedSidebarMenuProps {
ariaLabel?: string
children: React.ReactNode
className?: string
createLabel?: string
onCreateClick?: () => void
}
export function CollapsedSidebarMenu({
@@ -34,8 +30,6 @@ export function CollapsedSidebarMenu({
ariaLabel,
children,
className,
createLabel,
onCreateClick,
}: CollapsedSidebarMenuProps) {
return (
<div className={cn('flex flex-col px-[8px]', className)}>
@@ -60,15 +54,6 @@ export function CollapsedSidebarMenu({
</DropdownMenuTrigger>
</div>
<DropdownMenuContent side='right' align='start' sideOffset={8} {...hover.contentProps}>
{createLabel && onCreateClick && (
<>
<DropdownMenuItem onSelect={onCreateClick}>
<Plus className='h-[14px] w-[14px]' />
<span>{createLabel}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
{children}
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -1109,52 +1109,6 @@ export const Sidebar = memo(function Sidebar() {
</div>
</div>
{/* Quick-create button (collapsed only) */}
{isCollapsed && showCollapsedContent && (
<div className='flex flex-shrink-0 flex-col px-[8px] pt-[8px]'>
<DropdownMenu>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<DropdownMenuTrigger asChild>
<button
type='button'
aria-label='Create new'
className='mx-[2px] flex h-[30px] items-center justify-center rounded-[8px] px-[8px] hover:bg-[var(--surface-active)]'
>
<Plus className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
</button>
</DropdownMenuTrigger>
</Tooltip.Trigger>
<Tooltip.Content side='right'>
<p>Create new</p>
</Tooltip.Content>
</Tooltip.Root>
<DropdownMenuContent side='right' align='start' sideOffset={8}>
<DropdownMenuItem
onSelect={() => navigateToPage(`/workspace/${workspaceId}/home`)}
>
<Blimp className='h-[14px] w-[14px]' />
<span>New task</span>
</DropdownMenuItem>
<DropdownMenuItem
onSelect={handleCreateWorkflow}
disabled={!canEdit || isCreatingWorkflow}
>
<div
className='h-[14px] w-[14px] flex-shrink-0 rounded-[3px] border-[2px]'
style={{
backgroundColor: 'var(--text-icon)',
borderColor: 'color-mix(in srgb, var(--text-icon) 60%, transparent)',
backgroundClip: 'padding-box',
}}
/>
<span>{isCreatingWorkflow ? 'Creating...' : 'New workflow'}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
{/* Scrollable Tasks + Workflows */}
<div
ref={isCollapsed ? undefined : scrollContainerRef}
@@ -1195,8 +1149,6 @@ export const Sidebar = memo(function Sidebar() {
onClick={() => navigateToPage(`/workspace/${workspaceId}/home`)}
ariaLabel='Tasks'
className='mt-[6px]'
createLabel='New task'
onCreateClick={() => navigateToPage(`/workspace/${workspaceId}/home`)}
>
{tasksLoading ? (
<DropdownMenuItem disabled>
@@ -1366,8 +1318,6 @@ export const Sidebar = memo(function Sidebar() {
onClick={handleCreateWorkflow}
ariaLabel='Workflows'
className='mt-[6px]'
createLabel='New workflow'
onCreateClick={canEdit ? handleCreateWorkflow : undefined}
>
{workflowsLoading && regularWorkflows.length === 0 ? (
<DropdownMenuItem disabled>

View File

@@ -0,0 +1,11 @@
import { Loader2 } from 'lucide-react'
export default function WorkflowsLoading() {
return (
<div className='flex h-full w-full flex-col overflow-hidden bg-[var(--bg)]'>
<div className='relative flex h-full w-full flex-1 items-center justify-center bg-[var(--bg)]'>
<Loader2 className='h-[20px] w-[20px] animate-spin text-[var(--text-tertiary)]' />
</div>
</div>
)
}

View File

@@ -1,5 +1,6 @@
import { A2AIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { IntegrationType } from '@/blocks/types'
import { normalizeFileInput } from '@/blocks/utils'
import type { ToolResponse } from '@/tools/types'
@@ -63,6 +64,8 @@ export const A2ABlock: BlockConfig<A2AResponse> = {
'Compatible with any A2A-compliant agent including LangGraph, Google ADK, and other Sim workflows.',
docsLink: 'https://docs.sim.ai/blocks/a2a',
category: 'tools',
integrationType: IntegrationType.DeveloperTools,
tags: ['agentic', 'automation'],
bgColor: '#4151B5',
icon: A2AIcon,
subBlocks: [

View File

@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
import { AgentIcon } from '@/components/icons'
import { getScopesForService } from '@/lib/oauth/utils'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { getApiKeyCondition, getModelOptions, RESPONSE_FORMAT_WAND_CONFIG } from '@/blocks/utils'
import {
getBaseModelProviders,
@@ -69,6 +69,8 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
`,
docsLink: 'https://docs.sim.ai/blocks/agent',
category: 'blocks',
integrationType: IntegrationType.AI,
tags: ['llm', 'agentic', 'automation'],
bgColor: 'var(--brand-primary-hex)',
icon: AgentIcon,
subBlocks: [

View File

@@ -1,6 +1,6 @@
import { AhrefsIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { AhrefsResponse } from '@/tools/ahrefs/types'
export const AhrefsBlock: BlockConfig<AhrefsResponse> = {
@@ -12,6 +12,8 @@ export const AhrefsBlock: BlockConfig<AhrefsResponse> = {
'Integrate Ahrefs SEO tools into your workflow. Analyze domain ratings, backlinks, organic keywords, top pages, and more. Requires an Ahrefs Enterprise plan with API access.',
docsLink: 'https://docs.ahrefs.com/docs/api/reference/introduction',
category: 'tools',
integrationType: IntegrationType.Analytics,
tags: ['seo', 'marketing', 'data-analytics'],
bgColor: '#E0E0E0',
icon: AhrefsIcon,
subBlocks: [

View File

@@ -1,7 +1,7 @@
import { AirtableIcon } from '@/components/icons'
import { getScopesForService } from '@/lib/oauth/utils'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { AirtableResponse } from '@/tools/airtable/types'
import { getTrigger } from '@/triggers'
@@ -14,6 +14,8 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
'Integrates Airtable into the workflow. Can list bases, list tables (with schema), and create, get, list, or update records. Can also be used in trigger mode to trigger a workflow when an update is made to an Airtable table.',
docsLink: 'https://docs.sim.ai/tools/airtable',
category: 'tools',
integrationType: IntegrationType.Databases,
tags: ['spreadsheet', 'automation'],
bgColor: '#E0E0E0',
icon: AirtableIcon,
subBlocks: [

View File

@@ -1,6 +1,6 @@
import { AirweaveIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { AirweaveSearchResponse } from '@/tools/airweave/types'
export const AirweaveBlock: BlockConfig<AirweaveSearchResponse> = {
@@ -12,6 +12,8 @@ export const AirweaveBlock: BlockConfig<AirweaveSearchResponse> = {
'Search across your synced data sources using Airweave. Supports semantic search with hybrid, neural, or keyword retrieval strategies. Optionally generate AI-powered answers from search results.',
docsLink: 'https://docs.airweave.ai',
category: 'tools',
integrationType: IntegrationType.Search,
tags: ['vector-search', 'knowledge-base'],
bgColor: '#6366F1',
icon: AirweaveIcon,
subBlocks: [

View File

@@ -1,6 +1,6 @@
import { AlgoliaIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
export const AlgoliaBlock: BlockConfig = {
type: 'algolia',
@@ -10,6 +10,8 @@ export const AlgoliaBlock: BlockConfig = {
'Integrate Algolia into your workflow. Search indices, manage records (add, update, delete, browse), configure index settings, and perform batch operations.',
docsLink: 'https://docs.sim.ai/tools/algolia',
category: 'tools',
integrationType: IntegrationType.Search,
tags: ['vector-search', 'knowledge-base'],
bgColor: '#003DFF',
icon: AlgoliaIcon,
authMode: AuthMode.ApiKey,

View File

@@ -1,5 +1,5 @@
import { AmplitudeIcon } from '@/components/icons'
import { AuthMode, type BlockConfig } from '@/blocks/types'
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
export const AmplitudeBlock: BlockConfig = {
type: 'amplitude',
@@ -9,6 +9,8 @@ export const AmplitudeBlock: BlockConfig = {
'Integrate Amplitude into your workflow to track events, identify users and groups, search for users, query analytics, and retrieve revenue data.',
docsLink: 'https://docs.sim.ai/tools/amplitude',
category: 'tools',
integrationType: IntegrationType.Analytics,
tags: ['data-analytics', 'marketing'],
bgColor: '#1B1F3B',
icon: AmplitudeIcon,
authMode: AuthMode.ApiKey,

View File

@@ -1,5 +1,6 @@
import { ApiIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { IntegrationType } from '@/blocks/types'
import type { RequestResponse } from '@/tools/http/types'
export const ApiBlock: BlockConfig<RequestResponse> = {
@@ -13,6 +14,8 @@ export const ApiBlock: BlockConfig<RequestResponse> = {
- Curl the endpoint yourself before filling out the API block to make sure it's working IF you have the necessary authentication headers. Clarify with the user if you need any additional headers.
`,
category: 'blocks',
integrationType: IntegrationType.DeveloperTools,
tags: ['automation', 'webhooks'],
bgColor: '#2F55FF',
icon: ApiIcon,
subBlocks: [

View File

@@ -1,5 +1,6 @@
import { ApifyIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { IntegrationType } from '@/blocks/types'
import type { RunActorResult } from '@/tools/apify/types'
export const ApifyBlock: BlockConfig<RunActorResult> = {
@@ -10,6 +11,8 @@ export const ApifyBlock: BlockConfig<RunActorResult> = {
'Integrate Apify into your workflow. Run any Apify actor with custom input and retrieve results. Supports both synchronous and asynchronous execution with automatic dataset fetching.',
docsLink: 'https://docs.sim.ai/tools/apify',
category: 'tools',
integrationType: IntegrationType.Automation,
tags: ['web-scraping', 'automation', 'data-analytics'],
bgColor: '#E0E0E0',
icon: ApifyIcon,

View File

@@ -1,6 +1,6 @@
import { ApolloIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { ApolloResponse } from '@/tools/apollo/types'
export const ApolloBlock: BlockConfig<ApolloResponse> = {
@@ -12,6 +12,8 @@ export const ApolloBlock: BlockConfig<ApolloResponse> = {
'Integrates Apollo.io into the workflow. Search for people and companies, enrich contact data, manage your CRM contacts and accounts, add contacts to sequences, and create tasks.',
docsLink: 'https://docs.sim.ai/tools/apollo',
category: 'tools',
integrationType: IntegrationType.SalesIntelligence,
tags: ['enrichment', 'sales-engagement'],
bgColor: '#EBF212',
icon: ApolloIcon,
subBlocks: [

View File

@@ -1,5 +1,6 @@
import { ArxivIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { IntegrationType } from '@/blocks/types'
import type { ArxivResponse } from '@/tools/arxiv/types'
export const ArxivBlock: BlockConfig<ArxivResponse> = {
@@ -10,6 +11,8 @@ export const ArxivBlock: BlockConfig<ArxivResponse> = {
'Integrates ArXiv into the workflow. Can search for papers, get paper details, and get author papers. Does not require OAuth or an API key.',
docsLink: 'https://docs.sim.ai/tools/arxiv',
category: 'tools',
integrationType: IntegrationType.Search,
tags: ['document-processing', 'knowledge-base'],
bgColor: '#E0E0E0',
icon: ArxivIcon,
subBlocks: [

View File

@@ -1,7 +1,7 @@
import { AsanaIcon } from '@/components/icons'
import { getScopesForService } from '@/lib/oauth/utils'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { AsanaResponse } from '@/tools/asana/types'
export const AsanaBlock: BlockConfig<AsanaResponse> = {
@@ -12,6 +12,8 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
longDescription: 'Integrate Asana into the workflow. Can read, write, and update tasks.',
docsLink: 'https://docs.sim.ai/tools/asana',
category: 'tools',
integrationType: IntegrationType.Productivity,
tags: ['project-management', 'ticketing', 'automation'],
bgColor: '#E0E0E0',
icon: AsanaIcon,
subBlocks: [

View File

@@ -1,5 +1,5 @@
import { AshbyIcon } from '@/components/icons'
import { AuthMode, type BlockConfig } from '@/blocks/types'
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
import { getTrigger } from '@/triggers'
export const AshbyBlock: BlockConfig = {
@@ -10,6 +10,8 @@ export const AshbyBlock: BlockConfig = {
'Integrate Ashby into the workflow. Manage candidates (list, get, create, update, search, tag), applications (list, get, create, change stage), jobs (list, get), job postings (list, get), offers (list, get), notes (list, create), interviews (list), and reference data (sources, tags, archive reasons, custom fields, departments, locations, openings, users).',
docsLink: 'https://docs.sim.ai/tools/ashby',
category: 'tools',
integrationType: IntegrationType.HR,
tags: ['hiring'],
bgColor: '#5D4ED6',
icon: AshbyIcon,
authMode: AuthMode.ApiKey,

View File

@@ -1,6 +1,6 @@
import { AttioIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { AttioResponse } from '@/tools/attio/types'
import { getTrigger } from '@/triggers'
@@ -12,6 +12,8 @@ export const AttioBlock: BlockConfig<AttioResponse> = {
'Connect to Attio to manage CRM records (people, companies, custom objects), notes, tasks, lists, list entries, comments, workspace members, and webhooks.',
docsLink: 'https://docs.sim.ai/tools/attio',
category: 'tools',
integrationType: IntegrationType.CRM,
tags: ['sales-engagement', 'enrichment'],
bgColor: '#1D1E20',
icon: AttioIcon,
authMode: AuthMode.OAuth,

View File

@@ -1,7 +1,7 @@
import { BoxCompanyIcon } from '@/components/icons'
import { getScopesForService } from '@/lib/oauth/utils'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { normalizeFileInput } from '@/blocks/utils'
export const BoxBlock: BlockConfig = {
@@ -12,6 +12,8 @@ export const BoxBlock: BlockConfig = {
'Integrate Box into your workflow to manage files, folders, and e-signatures. Upload and download files, search content, create folders, send documents for e-signature, track signing status, and more.',
docsLink: 'https://docs.sim.ai/tools/box',
category: 'tools',
integrationType: IntegrationType.FileStorage,
tags: ['cloud', 'content-management', 'e-signatures'],
bgColor: '#FFFFFF',
icon: BoxCompanyIcon,
authMode: AuthMode.OAuth,

View File

@@ -1,6 +1,6 @@
import { BrandfetchIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { BrandfetchGetBrandResponse, BrandfetchSearchResponse } from '@/tools/brandfetch/types'
export const BrandfetchBlock: BlockConfig<BrandfetchGetBrandResponse | BrandfetchSearchResponse> = {
@@ -11,6 +11,8 @@ export const BrandfetchBlock: BlockConfig<BrandfetchGetBrandResponse | Brandfetc
'Integrate Brandfetch into your workflow. Retrieve brand logos, colors, fonts, and company data by domain, ticker, or name search.',
docsLink: 'https://docs.sim.ai/tools/brandfetch',
category: 'tools',
integrationType: IntegrationType.SalesIntelligence,
tags: ['enrichment', 'marketing'],
bgColor: '#000000',
icon: BrandfetchIcon,
authMode: AuthMode.ApiKey,

View File

@@ -1,5 +1,5 @@
import { BrowserUseIcon } from '@/components/icons'
import { AuthMode, type BlockConfig } from '@/blocks/types'
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
import type { BrowserUseResponse } from '@/tools/browser_use/types'
export const BrowserUseBlock: BlockConfig<BrowserUseResponse> = {
@@ -11,6 +11,8 @@ export const BrowserUseBlock: BlockConfig<BrowserUseResponse> = {
'Integrate Browser Use into the workflow. Can navigate the web and perform actions as if a real user was interacting with the browser.',
docsLink: 'https://docs.sim.ai/tools/browser_use',
category: 'tools',
integrationType: IntegrationType.Automation,
tags: ['web-scraping', 'automation', 'agentic'],
bgColor: '#181C1E',
icon: BrowserUseIcon,
subBlocks: [

View File

@@ -1,6 +1,6 @@
import { CalComIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { ToolResponse } from '@/tools/types'
import { getTrigger } from '@/triggers'
@@ -14,6 +14,8 @@ export const CalComBlock: BlockConfig<ToolResponse> = {
'Integrate Cal.com into your workflow. Create and manage bookings, event types, schedules, and check availability slots. Supports creating, listing, rescheduling, and canceling bookings, as well as managing event types and schedules. Can also trigger workflows based on Cal.com webhook events (booking created, cancelled, rescheduled). Connect your Cal.com account via OAuth.',
docsLink: 'https://docs.sim.ai/tools/calcom',
category: 'tools',
integrationType: IntegrationType.Productivity,
tags: ['scheduling', 'calendar', 'meeting'],
bgColor: '#FFFFFE',
icon: CalComIcon,
subBlocks: [

View File

@@ -1,6 +1,6 @@
import { CalendlyIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { ToolResponse } from '@/tools/types'
import { getTrigger } from '@/triggers'
@@ -14,6 +14,8 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
'Integrate Calendly into your workflow. Manage event types, scheduled events, invitees, and webhooks. Can also trigger workflows based on Calendly webhook events (invitee scheduled, invitee canceled, routing form submitted). Requires Personal Access Token.',
docsLink: 'https://docs.sim.ai/tools/calendly',
category: 'tools',
integrationType: IntegrationType.Productivity,
tags: ['scheduling', 'calendar', 'meeting'],
bgColor: '#FFFFFF',
icon: CalendlyIcon,
subBlocks: [

View File

@@ -1,5 +1,6 @@
import { CirclebackIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { IntegrationType } from '@/blocks/types'
import { getTrigger } from '@/triggers'
export const CirclebackBlock: BlockConfig = {
@@ -9,6 +10,8 @@ export const CirclebackBlock: BlockConfig = {
longDescription:
'Receive meeting notes, action items, transcripts, and recordings when meetings are processed. Circleback uses webhooks to push data to your workflows.',
category: 'triggers',
integrationType: IntegrationType.AI,
tags: ['meeting', 'note-taking', 'automation'],
bgColor: 'linear-gradient(180deg, #E0F7FA 0%, #FFFFFF 100%)',
docsLink: 'https://docs.sim.ai/tools/circleback',
icon: CirclebackIcon,

View File

@@ -1,5 +1,5 @@
import { ClayIcon } from '@/components/icons'
import { AuthMode, type BlockConfig } from '@/blocks/types'
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
import type { ClayPopulateResponse } from '@/tools/clay/types'
export const ClayBlock: BlockConfig<ClayPopulateResponse> = {
@@ -10,6 +10,8 @@ export const ClayBlock: BlockConfig<ClayPopulateResponse> = {
longDescription: 'Integrate Clay into the workflow. Can populate a table with data.',
docsLink: 'https://docs.sim.ai/tools/clay',
category: 'tools',
integrationType: IntegrationType.SalesIntelligence,
tags: ['enrichment', 'sales-engagement', 'data-analytics'],
bgColor: '#E0E0E0',
icon: ClayIcon,
subBlocks: [

View File

@@ -1,5 +1,6 @@
import { ClerkIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { IntegrationType } from '@/blocks/types'
import type { ClerkResponse } from '@/tools/clerk/types'
export const ClerkBlock: BlockConfig<ClerkResponse> = {
@@ -10,6 +11,8 @@ export const ClerkBlock: BlockConfig<ClerkResponse> = {
'Integrate Clerk authentication and user management into your workflow. Create, update, delete, and list users. Manage organizations and their memberships. Monitor and control user sessions.',
docsLink: 'https://docs.sim.ai/tools/clerk',
category: 'tools',
integrationType: IntegrationType.Security,
tags: ['identity', 'automation'],
bgColor: '#131316',
icon: ClerkIcon,

View File

@@ -1,5 +1,5 @@
import { CloudflareIcon } from '@/components/icons'
import { AuthMode, type BlockConfig } from '@/blocks/types'
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
import type { CloudflareResponse } from '@/tools/cloudflare/types'
export const CloudflareBlock: BlockConfig<CloudflareResponse> = {
@@ -11,6 +11,8 @@ export const CloudflareBlock: BlockConfig<CloudflareResponse> = {
'Integrate Cloudflare into the workflow. Manage zones (domains), DNS records, SSL/TLS certificates, zone settings, DNS analytics, and cache purging via the Cloudflare API.',
docsLink: 'https://docs.sim.ai/tools/cloudflare',
category: 'tools',
integrationType: IntegrationType.DeveloperTools,
tags: ['cloud', 'monitoring'],
bgColor: '#F5F6FA',
icon: CloudflareIcon,
subBlocks: [

View File

@@ -1,7 +1,7 @@
import { ConfluenceIcon } from '@/components/icons'
import { getScopesForService } from '@/lib/oauth/utils'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { normalizeFileInput } from '@/blocks/utils'
import type { ConfluenceResponse } from '@/tools/confluence/types'
import { getTrigger } from '@/triggers'
@@ -16,6 +16,8 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
'Integrate Confluence into the workflow. Can read, create, update, delete pages, manage comments, attachments, labels, and search content.',
docsLink: 'https://docs.sim.ai/tools/confluence',
category: 'tools',
integrationType: IntegrationType.Documents,
tags: ['knowledge-base', 'content-management', 'note-taking'],
bgColor: '#E0E0E0',
icon: ConfluenceIcon,
subBlocks: [

View File

@@ -1,6 +1,6 @@
import { CursorIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { createVersionedToolSelector } from '@/blocks/utils'
import type { CursorResponse } from '@/tools/cursor/types'
@@ -12,6 +12,8 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
'Interact with Cursor Cloud Agents API to launch AI agents that can work on your GitHub repositories. Supports launching agents, adding follow-up instructions, checking status, viewing conversations, and managing agent lifecycle.',
docsLink: 'https://cursor.com/docs/cloud-agent/api/endpoints',
category: 'tools',
integrationType: IntegrationType.DeveloperTools,
tags: ['agentic', 'automation'],
bgColor: '#1E1E1E',
icon: CursorIcon,
authMode: AuthMode.ApiKey,

View File

@@ -1,6 +1,6 @@
import { DatabricksIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { DatabricksResponse } from '@/tools/databricks/types'
export const DatabricksBlock: BlockConfig<DatabricksResponse> = {
@@ -12,6 +12,8 @@ export const DatabricksBlock: BlockConfig<DatabricksResponse> = {
'Connect to Databricks to execute SQL queries against SQL warehouses, trigger and monitor job runs, manage clusters, and retrieve run outputs. Requires a Personal Access Token and workspace host URL.',
docsLink: 'https://docs.sim.ai/tools/databricks',
category: 'tools',
integrationType: IntegrationType.Databases,
tags: ['data-warehouse', 'data-analytics', 'cloud'],
bgColor: '#F9F7F4',
icon: DatabricksIcon,
subBlocks: [

View File

@@ -1,6 +1,6 @@
import { DatadogIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { DatadogResponse } from '@/tools/datadog/types'
export const DatadogBlock: BlockConfig<DatadogResponse> = {
@@ -12,6 +12,8 @@ export const DatadogBlock: BlockConfig<DatadogResponse> = {
'Integrate Datadog monitoring into workflows. Submit metrics, manage monitors, query logs, create events, handle downtimes, and more.',
docsLink: 'https://docs.sim.ai/tools/datadog',
category: 'tools',
integrationType: IntegrationType.Analytics,
tags: ['monitoring', 'incident-management', 'error-tracking'],
bgColor: '#632CA6',
icon: DatadogIcon,
subBlocks: [

View File

@@ -1,6 +1,6 @@
import { DevinIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
export const DevinBlock: BlockConfig = {
type: 'devin',
@@ -17,6 +17,8 @@ export const DevinBlock: BlockConfig = {
`,
docsLink: 'https://docs.sim.ai/tools/devin',
category: 'tools',
integrationType: IntegrationType.DeveloperTools,
tags: ['agentic', 'automation'],
bgColor: '#12141A',
icon: DevinIcon,
authMode: AuthMode.ApiKey,

View File

@@ -1,6 +1,6 @@
import { DiscordIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { normalizeFileInput } from '@/blocks/utils'
import type { DiscordResponse } from '@/tools/discord/types'
@@ -12,6 +12,8 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
longDescription:
'Comprehensive Discord integration: messages, threads, channels, roles, members, invites, and webhooks.',
category: 'tools',
integrationType: IntegrationType.Communication,
tags: ['messaging', 'webhooks', 'automation'],
bgColor: '#5865F2',
icon: DiscordIcon,
docsLink: 'https://docs.sim.ai/tools/discord',

View File

@@ -1,7 +1,7 @@
import { DocuSignIcon } from '@/components/icons'
import { getScopesForService } from '@/lib/oauth/utils'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { normalizeFileInput } from '@/blocks/utils'
import type { DocuSignResponse } from '@/tools/docusign/types'
@@ -13,6 +13,8 @@ export const DocuSignBlock: BlockConfig<DocuSignResponse> = {
'Create and send envelopes for e-signature, use templates, check signing status, download signed documents, and manage recipients with DocuSign.',
docsLink: 'https://docs.sim.ai/tools/docusign',
category: 'tools',
integrationType: IntegrationType.Documents,
tags: ['e-signatures', 'document-processing'],
bgColor: '#FFFFFF',
icon: DocuSignIcon,
authMode: AuthMode.OAuth,

View File

@@ -1,7 +1,7 @@
import { DropboxIcon } from '@/components/icons'
import { getScopesForService } from '@/lib/oauth/utils'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { normalizeFileInput } from '@/blocks/utils'
import type { DropboxResponse } from '@/tools/dropbox/types'
@@ -14,6 +14,8 @@ export const DropboxBlock: BlockConfig<DropboxResponse> = {
'Integrate Dropbox into your workflow for file management, sharing, and collaboration. Upload files, download content, create folders, manage shared links, and more.',
docsLink: 'https://docs.sim.ai/tools/dropbox',
category: 'tools',
integrationType: IntegrationType.FileStorage,
tags: ['cloud', 'document-processing'],
icon: DropboxIcon,
bgColor: '#0061FF',
subBlocks: [

View File

@@ -1,5 +1,6 @@
import { DsPyIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { IntegrationType } from '@/blocks/types'
export const DSPyBlock: BlockConfig = {
type: 'dspy',
@@ -8,6 +9,8 @@ export const DSPyBlock: BlockConfig = {
longDescription:
'Integrate with your self-hosted DSPy programs for LLM-powered predictions. Supports Predict, Chain of Thought, and ReAct agents. DSPy is the framework for programming—not prompting—language models.',
category: 'tools',
integrationType: IntegrationType.AI,
tags: ['llm', 'agentic', 'automation'],
bgColor: '#E0E0E0',
icon: DsPyIcon,

View File

@@ -1,6 +1,6 @@
import { DubIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { DubResponse } from '@/tools/dub/types'
export const DubBlock: BlockConfig<DubResponse> = {
@@ -12,6 +12,8 @@ export const DubBlock: BlockConfig<DubResponse> = {
'Create, manage, and track short links with Dub. Supports custom domains, UTM parameters, link analytics, and more.',
docsLink: 'https://docs.sim.ai/tools/dub',
category: 'tools',
integrationType: IntegrationType.DeveloperTools,
tags: ['link-management', 'marketing', 'data-analytics'],
bgColor: '#181C1E',
icon: DubIcon,
subBlocks: [

View File

@@ -1,5 +1,6 @@
import { DuckDuckGoIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { IntegrationType } from '@/blocks/types'
import type { DuckDuckGoResponse } from '@/tools/duckduckgo/types'
export const DuckDuckGoBlock: BlockConfig<DuckDuckGoResponse> = {
@@ -10,6 +11,8 @@ export const DuckDuckGoBlock: BlockConfig<DuckDuckGoResponse> = {
'Search the web using DuckDuckGo Instant Answers API. Returns instant answers, abstracts, related topics, and more. Free to use without an API key.',
docsLink: 'https://docs.sim.ai/tools/duckduckgo',
category: 'tools',
integrationType: IntegrationType.Search,
tags: ['web-scraping', 'seo'],
bgColor: '#FFFFFF',
icon: DuckDuckGoIcon,
subBlocks: [

View File

@@ -1,5 +1,6 @@
import { DynamoDBIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { IntegrationType } from '@/blocks/types'
import type { DynamoDBIntrospectResponse, DynamoDBResponse } from '@/tools/dynamodb/types'
export const DynamoDBBlock: BlockConfig<DynamoDBResponse | DynamoDBIntrospectResponse> = {
@@ -10,6 +11,8 @@ export const DynamoDBBlock: BlockConfig<DynamoDBResponse | DynamoDBIntrospectRes
'Integrate Amazon DynamoDB into workflows. Supports Get, Put, Query, Scan, Update, Delete, and Introspect operations on DynamoDB tables.',
docsLink: 'https://docs.sim.ai/tools/dynamodb',
category: 'tools',
integrationType: IntegrationType.Databases,
tags: ['cloud', 'data-warehouse'],
bgColor: 'linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)',
icon: DynamoDBIcon,
subBlocks: [

View File

@@ -1,6 +1,6 @@
import { ElasticsearchIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { ElasticsearchResponse } from '@/tools/elasticsearch/types'
export const ElasticsearchBlock: BlockConfig<ElasticsearchResponse> = {
@@ -12,6 +12,8 @@ export const ElasticsearchBlock: BlockConfig<ElasticsearchResponse> = {
'Integrate Elasticsearch into workflows for powerful search, indexing, and data management. Supports document CRUD operations, advanced search queries, bulk operations, index management, and cluster monitoring. Works with both self-hosted and Elastic Cloud deployments.',
docsLink: 'https://docs.sim.ai/tools/elasticsearch',
category: 'tools',
integrationType: IntegrationType.Databases,
tags: ['vector-search', 'data-analytics'],
bgColor: '#E0E0E0',
icon: ElasticsearchIcon,
subBlocks: [

View File

@@ -1,5 +1,5 @@
import { ElevenLabsIcon } from '@/components/icons'
import { AuthMode, type BlockConfig } from '@/blocks/types'
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
import type { ElevenLabsBlockResponse } from '@/tools/elevenlabs/types'
export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
@@ -10,6 +10,8 @@ export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
longDescription: 'Integrate ElevenLabs into the workflow. Can convert text to speech.',
docsLink: 'https://docs.sim.ai/tools/elevenlabs',
category: 'tools',
integrationType: IntegrationType.Media,
tags: ['text-to-speech'],
bgColor: '#181C1E',
icon: ElevenLabsIcon,

View File

@@ -1,6 +1,6 @@
import { EnrichSoIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
export const EnrichBlock: BlockConfig = {
type: 'enrich',
@@ -11,6 +11,8 @@ export const EnrichBlock: BlockConfig = {
'Access real-time B2B data intelligence with Enrich.so. Enrich profiles from email addresses, find work emails from LinkedIn, verify email deliverability, search for people and companies, and analyze LinkedIn post engagement.',
docsLink: 'https://docs.enrich.so/',
category: 'tools',
integrationType: IntegrationType.SalesIntelligence,
tags: ['enrichment', 'data-analytics'],
bgColor: '#E5E5E6',
icon: EnrichSoIcon,
subBlocks: [

View File

@@ -1,6 +1,6 @@
import { EvernoteIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
export const EvernoteBlock: BlockConfig = {
type: 'evernote',
@@ -10,6 +10,8 @@ export const EvernoteBlock: BlockConfig = {
'Integrate with Evernote to manage notes, notebooks, and tags. Create, read, update, copy, search, and delete notes. Create and list notebooks and tags.',
docsLink: 'https://docs.sim.ai/tools/evernote',
category: 'tools',
integrationType: IntegrationType.Documents,
tags: ['note-taking', 'knowledge-base'],
bgColor: '#E0E0E0',
icon: EvernoteIcon,
authMode: AuthMode.ApiKey,

Some files were not shown because too many files have changed in this diff Show More