mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Compare commits
1 Commits
fix/side
...
fix/templa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71370e1e8d |
@@ -1,316 +0,0 @@
|
||||
---
|
||||
description: Validate an existing knowledge base connector against its service's API docs
|
||||
argument-hint: <service-name> [api-docs-url]
|
||||
---
|
||||
|
||||
# Validate Connector Skill
|
||||
|
||||
You are an expert auditor for Sim knowledge base connectors. Your job is to thoroughly validate that an existing connector is correct, complete, and follows all conventions.
|
||||
|
||||
## Your Task
|
||||
|
||||
When the user asks you to validate a connector:
|
||||
1. Read the service's API documentation (via Context7 or WebFetch)
|
||||
2. Read the connector implementation, OAuth config, and registry entries
|
||||
3. Cross-reference everything against the API docs and Sim conventions
|
||||
4. Report all issues found, grouped by severity (critical, warning, suggestion)
|
||||
5. Fix all issues after reporting them
|
||||
|
||||
## Step 1: Gather All Files
|
||||
|
||||
Read **every** file for the connector — do not skip any:
|
||||
|
||||
```
|
||||
apps/sim/connectors/{service}/{service}.ts # Connector implementation
|
||||
apps/sim/connectors/{service}/index.ts # Barrel export
|
||||
apps/sim/connectors/registry.ts # Connector registry entry
|
||||
apps/sim/connectors/types.ts # ConnectorConfig interface, ExternalDocument, etc.
|
||||
apps/sim/connectors/utils.ts # Shared utilities (computeContentHash, htmlToPlainText, etc.)
|
||||
apps/sim/lib/oauth/oauth.ts # OAUTH_PROVIDERS — single source of truth for scopes
|
||||
apps/sim/lib/oauth/utils.ts # getCanonicalScopesForProvider, getScopesForService, SCOPE_DESCRIPTIONS
|
||||
apps/sim/lib/oauth/types.ts # OAuthService union type
|
||||
apps/sim/components/icons.tsx # Icon definition for the service
|
||||
```
|
||||
|
||||
If the connector uses selectors, also read:
|
||||
```
|
||||
apps/sim/hooks/selectors/registry.ts # Selector key definitions
|
||||
apps/sim/hooks/selectors/types.ts # SelectorKey union type
|
||||
apps/sim/lib/workflows/subblocks/context.ts # SELECTOR_CONTEXT_FIELDS
|
||||
```
|
||||
|
||||
## Step 2: Pull API Documentation
|
||||
|
||||
Fetch the official API docs for the service. This is the **source of truth** for:
|
||||
- Endpoint URLs, HTTP methods, and auth headers
|
||||
- Required vs optional parameters
|
||||
- Parameter types and allowed values
|
||||
- Response shapes and field names
|
||||
- Pagination patterns (cursor, offset, next token)
|
||||
- Rate limits and error formats
|
||||
- OAuth scopes and their meanings
|
||||
|
||||
Use Context7 (resolve-library-id → query-docs) or WebFetch to retrieve documentation. If both fail, note which claims are based on training knowledge vs verified docs.
|
||||
|
||||
## Step 3: Validate API Endpoints
|
||||
|
||||
For **every** API call in the connector (`listDocuments`, `getDocument`, `validateConfig`, and any helper functions), verify against the API docs:
|
||||
|
||||
### URLs and Methods
|
||||
- [ ] Base URL is correct for the service's API version
|
||||
- [ ] Endpoint paths match the API docs exactly
|
||||
- [ ] HTTP method is correct (GET, POST, PUT, PATCH, DELETE)
|
||||
- [ ] Path parameters are correctly interpolated and URI-encoded where needed
|
||||
- [ ] Query parameters use correct names and formats per the API docs
|
||||
|
||||
### Headers
|
||||
- [ ] Authorization header uses the correct format:
|
||||
- OAuth: `Authorization: Bearer ${accessToken}`
|
||||
- API Key: correct header name per the service's docs
|
||||
- [ ] `Content-Type` is set for POST/PUT/PATCH requests
|
||||
- [ ] Any service-specific headers are present (e.g., `Notion-Version`, `Dropbox-API-Arg`)
|
||||
- [ ] No headers are sent that the API doesn't support or silently ignores
|
||||
|
||||
### Request Bodies
|
||||
- [ ] POST/PUT body fields match API parameter names exactly
|
||||
- [ ] Required fields are always sent
|
||||
- [ ] Optional fields are conditionally included (not sent as `null` or empty unless the API expects that)
|
||||
- [ ] Field value types match API expectations (string vs number vs boolean)
|
||||
|
||||
### Input Sanitization
|
||||
- [ ] User-controlled values interpolated into query strings are properly escaped:
|
||||
- OData `$filter`: single quotes escaped with `''` (e.g., `externalId.replace(/'/g, "''")`)
|
||||
- SOQL: single quotes escaped with `\'`
|
||||
- GraphQL variables: passed as variables, not interpolated into query strings
|
||||
- URL path segments: `encodeURIComponent()` applied
|
||||
- [ ] URL-type config fields (e.g., `siteUrl`, `instanceUrl`) are normalized:
|
||||
- Strip `https://` / `http://` prefix if the API expects bare domains
|
||||
- Strip trailing `/`
|
||||
- Apply `.trim()` before validation
|
||||
|
||||
### Response Parsing
|
||||
- [ ] Response structure is correctly traversed (e.g., `data.results` vs `data.items` vs `data`)
|
||||
- [ ] Field names extracted match what the API actually returns
|
||||
- [ ] Nullable fields are handled with `?? null` or `|| undefined`
|
||||
- [ ] Error responses are checked before accessing data fields
|
||||
|
||||
## Step 4: Validate OAuth Scopes (if OAuth connector)
|
||||
|
||||
Scopes must be correctly declared and sufficient for all API calls the connector makes.
|
||||
|
||||
### Connector requiredScopes
|
||||
- [ ] `requiredScopes` in the connector's `auth` config lists all scopes needed by the connector
|
||||
- [ ] Each scope in `requiredScopes` is a real, valid scope recognized by the service's API
|
||||
- [ ] No invalid, deprecated, or made-up scopes are listed
|
||||
- [ ] No unnecessary excess scopes beyond what the connector actually needs
|
||||
|
||||
### Scope Subset Validation (CRITICAL)
|
||||
- [ ] Every scope in `requiredScopes` exists in the OAuth provider's `scopes` array in `lib/oauth/oauth.ts`
|
||||
- [ ] Find the provider in `OAUTH_PROVIDERS[providerGroup].services[serviceId].scopes`
|
||||
- [ ] Verify: `requiredScopes` ⊆ `OAUTH_PROVIDERS scopes` (every required scope is present in the provider config)
|
||||
- [ ] If a required scope is NOT in the provider config, flag as **critical** — the connector will fail at runtime
|
||||
|
||||
### Scope Sufficiency
|
||||
For each API endpoint the connector calls:
|
||||
- [ ] Identify which scopes are required per the API docs
|
||||
- [ ] Verify those scopes are included in the connector's `requiredScopes`
|
||||
- [ ] If the connector calls endpoints requiring scopes not in `requiredScopes`, flag as **warning**
|
||||
|
||||
### Token Refresh Config
|
||||
- [ ] Check the `getOAuthTokenRefreshConfig` function in `lib/oauth/oauth.ts` for this provider
|
||||
- [ ] `useBasicAuth` matches the service's token exchange requirements
|
||||
- [ ] `supportsRefreshTokenRotation` matches whether the service issues rotating refresh tokens
|
||||
- [ ] Token endpoint URL is correct
|
||||
|
||||
## Step 5: Validate Pagination
|
||||
|
||||
### listDocuments Pagination
|
||||
- [ ] Cursor/pagination parameter name matches the API docs
|
||||
- [ ] Response pagination field is correctly extracted (e.g., `next_cursor`, `nextPageToken`, `@odata.nextLink`, `offset`)
|
||||
- [ ] `hasMore` is correctly determined from the response
|
||||
- [ ] `nextCursor` is correctly passed back for the next page
|
||||
- [ ] `maxItems` / `maxRecords` cap is correctly applied across pages using `syncContext.totalDocsFetched`
|
||||
- [ ] Page size is within the API's allowed range (not exceeding max page size)
|
||||
- [ ] Last page precision: when a `maxItems` cap exists, the final page request uses `Math.min(PAGE_SIZE, remaining)` to avoid fetching more records than needed
|
||||
- [ ] No off-by-one errors in pagination tracking
|
||||
- [ ] The connector does NOT hit known API pagination limits silently (e.g., HubSpot search 10k cap)
|
||||
|
||||
### Pagination State Across Pages
|
||||
- [ ] `syncContext` is used to cache state across pages (user names, field maps, instance URLs, portal IDs, etc.)
|
||||
- [ ] Cached state in `syncContext` is correctly initialized on first page and reused on subsequent pages
|
||||
|
||||
## Step 6: Validate Data Transformation
|
||||
|
||||
### ExternalDocument Construction
|
||||
- [ ] `externalId` is a stable, unique identifier from the source API
|
||||
- [ ] `title` is extracted from the correct field and has a sensible fallback (e.g., `'Untitled'`)
|
||||
- [ ] `content` is plain text — HTML content is stripped using `htmlToPlainText` from `@/connectors/utils`
|
||||
- [ ] `mimeType` is `'text/plain'`
|
||||
- [ ] `contentHash` is computed using `computeContentHash` from `@/connectors/utils`
|
||||
- [ ] `sourceUrl` is a valid, complete URL back to the original resource (not relative)
|
||||
- [ ] `metadata` contains all fields referenced by `mapTags` and `tagDefinitions`
|
||||
|
||||
### Content Extraction
|
||||
- [ ] Rich text / HTML fields are converted to plain text before indexing
|
||||
- [ ] Important content is not silently dropped (e.g., nested blocks, table cells, code blocks)
|
||||
- [ ] Content is not silently truncated without logging a warning
|
||||
- [ ] Empty/blank documents are properly filtered out
|
||||
- [ ] Size checks use `Buffer.byteLength(text, 'utf8')` not `text.length` when comparing against byte-based limits (e.g., `MAX_FILE_SIZE` in bytes)
|
||||
|
||||
## Step 7: Validate Tag Definitions and mapTags
|
||||
|
||||
### tagDefinitions
|
||||
- [ ] Each `tagDefinition` has an `id`, `displayName`, and `fieldType`
|
||||
- [ ] `fieldType` matches the actual data type: `'text'` for strings, `'number'` for numbers, `'date'` for dates, `'boolean'` for booleans
|
||||
- [ ] Every `id` in `tagDefinitions` is returned by `mapTags`
|
||||
- [ ] No `tagDefinition` references a field that `mapTags` never produces
|
||||
|
||||
### mapTags
|
||||
- [ ] Return keys match `tagDefinition` `id` values exactly
|
||||
- [ ] Date values are properly parsed using `parseTagDate` from `@/connectors/utils`
|
||||
- [ ] Array values are properly joined using `joinTagArray` from `@/connectors/utils`
|
||||
- [ ] Number values are validated (not `NaN`)
|
||||
- [ ] Metadata field names accessed in `mapTags` match what `listDocuments`/`getDocument` store in `metadata`
|
||||
|
||||
## Step 8: Validate Config Fields and Validation
|
||||
|
||||
### configFields
|
||||
- [ ] Every field has `id`, `title`, `type`
|
||||
- [ ] `required` is set explicitly (not omitted)
|
||||
- [ ] Dropdown fields have `options` with `label` and `id` for each option
|
||||
- [ ] Selector fields follow the canonical pair pattern:
|
||||
- A `type: 'selector'` field with `selectorKey`, `canonicalParamId`, `mode: 'basic'`
|
||||
- A `type: 'short-input'` field with the same `canonicalParamId`, `mode: 'advanced'`
|
||||
- `required` is identical on both fields in the pair
|
||||
- [ ] `selectorKey` values exist in the selector registry
|
||||
- [ ] `dependsOn` references selector field `id` values, not `canonicalParamId`
|
||||
|
||||
### validateConfig
|
||||
- [ ] Validates all required fields are present before making API calls
|
||||
- [ ] Validates optional numeric fields (checks `Number.isNaN`, positive values)
|
||||
- [ ] Makes a lightweight API call to verify access (e.g., fetch 1 record, get profile)
|
||||
- [ ] Uses `VALIDATE_RETRY_OPTIONS` for retry budget
|
||||
- [ ] Returns `{ valid: true }` on success
|
||||
- [ ] Returns `{ valid: false, error: 'descriptive message' }` on failure
|
||||
- [ ] Catches exceptions and returns user-friendly error messages
|
||||
- [ ] Does NOT make expensive calls (full data listing, large queries)
|
||||
|
||||
## Step 9: Validate getDocument
|
||||
|
||||
- [ ] Fetches a single document by `externalId`
|
||||
- [ ] Returns `null` for 404 / not found (does not throw)
|
||||
- [ ] Returns the same `ExternalDocument` shape as `listDocuments`
|
||||
- [ ] Handles all content types that `listDocuments` can produce (e.g., if `listDocuments` returns both pages and blogposts, `getDocument` must handle both — not hardcode one endpoint)
|
||||
- [ ] Forwards `syncContext` if it needs cached state (user names, field maps, etc.)
|
||||
- [ ] Error handling is graceful (catches, logs, returns null or throws with context)
|
||||
- [ ] Does not redundantly re-fetch data already included in the initial API response (e.g., if comments come back with the post, don't fetch them again separately)
|
||||
|
||||
## Step 10: Validate General Quality
|
||||
|
||||
### fetchWithRetry Usage
|
||||
- [ ] All external API calls use `fetchWithRetry` from `@/lib/knowledge/documents/utils`
|
||||
- [ ] No raw `fetch()` calls to external APIs
|
||||
- [ ] `VALIDATE_RETRY_OPTIONS` used in `validateConfig`
|
||||
- [ ] If `validateConfig` calls a shared helper (e.g., `linearGraphQL`, `resolveId`), that helper must accept and forward `retryOptions` to `fetchWithRetry`
|
||||
- [ ] Default retry options used in `listDocuments`/`getDocument`
|
||||
|
||||
### API Efficiency
|
||||
- [ ] APIs that support field selection (e.g., `$select`, `sysparm_fields`, `fields`) should request only the fields the connector needs — in both `listDocuments` AND `getDocument`
|
||||
- [ ] No redundant API calls: if a helper already fetches data (e.g., site metadata), callers should reuse the result instead of making a second call for the same information
|
||||
- [ ] Sequential per-item API calls (fetching details for each document in a loop) should be batched with `Promise.all` and a concurrency limit of 3-5
|
||||
|
||||
### Error Handling
|
||||
- [ ] Individual document failures are caught and logged without aborting the sync
|
||||
- [ ] API error responses include status codes in error messages
|
||||
- [ ] No unhandled promise rejections in concurrent operations
|
||||
|
||||
### Concurrency
|
||||
- [ ] Concurrent API calls use reasonable batch sizes (3-5 is typical)
|
||||
- [ ] No unbounded `Promise.all` over large arrays
|
||||
|
||||
### Logging
|
||||
- [ ] Uses `createLogger` from `@sim/logger` (not `console.log`)
|
||||
- [ ] Logs sync progress at `info` level
|
||||
- [ ] Logs errors at `warn` or `error` level with context
|
||||
|
||||
### Registry
|
||||
- [ ] Connector is exported from `connectors/{service}/index.ts`
|
||||
- [ ] Connector is registered in `connectors/registry.ts`
|
||||
- [ ] Registry key matches the connector's `id` field
|
||||
|
||||
## Step 11: Report and Fix
|
||||
|
||||
### Report Format
|
||||
|
||||
Group findings by severity:
|
||||
|
||||
**Critical** (will cause runtime errors, data loss, or auth failures):
|
||||
- Wrong API endpoint URL or HTTP method
|
||||
- Invalid or missing OAuth scopes (not in provider config)
|
||||
- Incorrect response field mapping (accessing wrong path)
|
||||
- SOQL/query fields that don't exist on the target object
|
||||
- Pagination that silently hits undocumented API limits
|
||||
- Missing error handling that would crash the sync
|
||||
- `requiredScopes` not a subset of OAuth provider scopes
|
||||
- Query/filter injection: user-controlled values interpolated into OData `$filter`, SOQL, or query strings without escaping
|
||||
|
||||
**Warning** (incorrect behavior, data quality issues, or convention violations):
|
||||
- HTML content not stripped via `htmlToPlainText`
|
||||
- `getDocument` not forwarding `syncContext`
|
||||
- `getDocument` hardcoded to one content type when `listDocuments` returns multiple (e.g., only pages but not blogposts)
|
||||
- Missing `tagDefinition` for metadata fields returned by `mapTags`
|
||||
- Incorrect `useBasicAuth` or `supportsRefreshTokenRotation` in token refresh config
|
||||
- Invalid scope names that the API doesn't recognize (even if silently ignored)
|
||||
- Private resources excluded from name-based lookup despite scopes being available
|
||||
- Silent data truncation without logging
|
||||
- Size checks using `text.length` (character count) instead of `Buffer.byteLength` (byte count) for byte-based limits
|
||||
- URL-type config fields not normalized (protocol prefix, trailing slashes cause API failures)
|
||||
- `VALIDATE_RETRY_OPTIONS` not threaded through helper functions called by `validateConfig`
|
||||
|
||||
**Suggestion** (minor improvements):
|
||||
- Missing incremental sync support despite API supporting it
|
||||
- Overly broad scopes that could be narrowed (not wrong, but could be tighter)
|
||||
- Source URL format could be more specific
|
||||
- Missing `orderBy` for deterministic pagination
|
||||
- Redundant API calls that could be cached in `syncContext`
|
||||
- Sequential per-item API calls that could be batched with `Promise.all` (concurrency 3-5)
|
||||
- API supports field selection but connector fetches all fields (e.g., missing `$select`, `sysparm_fields`, `fields`)
|
||||
- `getDocument` re-fetches data already included in the initial API response (e.g., comments returned with post)
|
||||
- Last page of pagination requests full `PAGE_SIZE` when fewer records remain (`Math.min(PAGE_SIZE, remaining)`)
|
||||
|
||||
### Fix All Issues
|
||||
|
||||
After reporting, fix every **critical** and **warning** issue. Apply **suggestions** where they don't add unnecessary complexity.
|
||||
|
||||
### Validation Output
|
||||
|
||||
After fixing, confirm:
|
||||
1. `bun run lint` passes
|
||||
2. TypeScript compiles clean
|
||||
3. Re-read all modified files to verify fixes are correct
|
||||
|
||||
## Checklist Summary
|
||||
|
||||
- [ ] Read connector implementation, types, utils, registry, and OAuth config
|
||||
- [ ] Pulled and read official API documentation for the service
|
||||
- [ ] Validated every API endpoint URL, method, headers, and body against API docs
|
||||
- [ ] Validated input sanitization: no query/filter injection, URL fields normalized
|
||||
- [ ] Validated OAuth scopes: `requiredScopes` ⊆ OAuth provider `scopes` in `oauth.ts`
|
||||
- [ ] Validated each scope is real and recognized by the service's API
|
||||
- [ ] Validated scopes are sufficient for all API endpoints the connector calls
|
||||
- [ ] Validated token refresh config (`useBasicAuth`, `supportsRefreshTokenRotation`)
|
||||
- [ ] Validated pagination: cursor names, page sizes, hasMore logic, no silent caps
|
||||
- [ ] Validated data transformation: plain text extraction, HTML stripping, content hashing
|
||||
- [ ] Validated tag definitions match mapTags output, correct fieldTypes
|
||||
- [ ] Validated config fields: canonical pairs, selector keys, required flags
|
||||
- [ ] Validated validateConfig: lightweight check, error messages, retry options
|
||||
- [ ] Validated getDocument: null on 404, all content types handled, no redundant re-fetches, syncContext forwarding
|
||||
- [ ] Validated fetchWithRetry used for all external calls (no raw fetch), VALIDATE_RETRY_OPTIONS threaded through helpers
|
||||
- [ ] Validated API efficiency: field selection used, no redundant calls, sequential fetches batched
|
||||
- [ ] Validated error handling: graceful failures, no unhandled rejections
|
||||
- [ ] Validated logging: createLogger, no console.log
|
||||
- [ ] Validated registry: correct export, correct key
|
||||
- [ ] Reported all issues grouped by severity
|
||||
- [ ] Fixed all critical and warning issues
|
||||
- [ ] Ran `bun run lint` after fixes
|
||||
- [ ] Verified TypeScript compiles clean
|
||||
@@ -1,21 +1,9 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Viewport } from 'next'
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
return children
|
||||
}
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
userScalable: false,
|
||||
themeColor: [
|
||||
{ media: '(prefers-color-scheme: light)', color: '#ffffff' },
|
||||
{ media: '(prefers-color-scheme: dark)', color: '#0c0c0c' },
|
||||
],
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
metadataBase: new URL('https://docs.sim.ai'),
|
||||
title: {
|
||||
@@ -24,9 +12,6 @@ export const metadata = {
|
||||
},
|
||||
description:
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
|
||||
applicationName: 'Sim Docs',
|
||||
generator: 'Next.js',
|
||||
referrer: 'origin-when-cross-origin' as const,
|
||||
keywords: [
|
||||
'AI agents',
|
||||
'agentic workforce',
|
||||
@@ -52,28 +37,17 @@ export const metadata = {
|
||||
manifest: '/favicon/site.webmanifest',
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/icon.svg', type: 'image/svg+xml', sizes: 'any' },
|
||||
{ url: '/favicon/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
|
||||
{ url: '/favicon/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
|
||||
{ url: '/favicon/android-chrome-192x192.png', sizes: '192x192', type: 'image/png' },
|
||||
{ url: '/favicon/android-chrome-512x512.png', sizes: '512x512', type: 'image/png' },
|
||||
],
|
||||
apple: '/favicon/apple-touch-icon.png',
|
||||
shortcut: '/icon.svg',
|
||||
shortcut: '/favicon/favicon.ico',
|
||||
},
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: 'default',
|
||||
title: 'Sim Docs',
|
||||
},
|
||||
formatDetection: {
|
||||
telephone: false,
|
||||
},
|
||||
other: {
|
||||
'apple-mobile-web-app-capable': 'yes',
|
||||
'mobile-web-app-capable': 'yes',
|
||||
'msapplication-TileColor': '#33C482',
|
||||
},
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
locale: 'en_US',
|
||||
|
||||
@@ -124,34 +124,6 @@ export function NoteIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function WorkdayIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
const clipId = `workday_clip_${id}`
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 64 64' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g clipPath={`url(#${clipId})`} transform='matrix(0.53333333,0,0,0.53333333,-124.63685,-16)'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='m 251.21,88.7755 h 8.224 c 1.166,0 2.178,0.7836 2.444,1.8924 l 11.057,44.6751 c 0.152,0.002 12.182,-44.6393 12.182,-44.6393 0.306,-1.1361 1.36,-1.9282 2.566,-1.9282 h 12.74 c 1.144,0 2.144,0.7515 2.435,1.8296 l 12.118,44.9289 c 0.448,-0.282 11.147,-44.8661 11.147,-44.8661 0.267,-1.1088 1.279,-1.8924 2.444,-1.8924 h 8.219 c 1.649,0 2.854,1.5192 2.437,3.0742 l -15.08,56.3173 c -0.286,1.072 -1.272,1.823 -2.406,1.833 l -12.438,-0.019 c -1.142,-0.002 -2.137,-0.744 -2.429,-1.819 -2.126,-7.805 -12.605,-47.277 -12.605,-47.277 0,0 -11.008,39.471 -13.133,47.277 -0.293,1.075 -1.288,1.817 -2.429,1.819 L 266.264,150 c -1.133,-0.01 -2.119,-0.761 -2.406,-1.833 L 248.777,91.8438 c -0.416,-1.5524 0.786,-3.0683 2.433,-3.0683 z'
|
||||
fill='#005cb9'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='m 333.324,72.2449 c 0.531,0 1.071,-0.0723 1.608,-0.2234 3.18,-0.8968 5.039,-4.2303 4.153,-7.446 -0.129,-0.4673 -0.265,-0.9327 -0.408,-1.3936 C 332.529,43.3349 314.569,30 293.987,30 c -20.557,0 -38.51,13.3133 -44.673,33.1281 -0.136,0.4355 -0.267,0.8782 -0.391,1.3232 -0.902,3.2119 0.943,6.5541 4.12,7.4645 3.173,0.9112 6.48,-0.9547 7.381,-4.1666 0.094,-0.3322 0.19,-0.6616 0.292,-0.9892 4.591,-14.7582 17.961,-24.6707 33.271,-24.6707 15.329,0 28.704,9.9284 33.281,24.7063 0.105,0.3397 0.206,0.682 0.301,1.0263 0.737,2.6726 3.139,4.423 5.755,4.423 z'
|
||||
fill='#f38b00'
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id={clipId}>
|
||||
<path d='M 354,30 H 234 v 120 h 120 z' fill='#ffffff' />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function WorkflowIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -1174,25 +1146,6 @@ export function DevinIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DocuSignIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 1547 1549' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='m1113.4 1114.9v395.6c0 20.8-16.7 37.6-37.5 37.6h-1038.4c-20.7 0-37.5-16.8-37.5-37.6v-1039c0-20.7 16.8-37.5 37.5-37.5h394.3v643.4c0 20.7 16.8 37.5 37.5 37.5z'
|
||||
fill='#4c00ff'
|
||||
/>
|
||||
<path
|
||||
d='m1546 557.1c0 332.4-193.9 557-432.6 557.8v-418.8c0-12-4.8-24-13.5-31.9l-217.1-217.4c-8.8-8.8-20-13.6-32-13.6h-418.2v-394.8c0-20.8 16.8-37.6 37.5-37.6h585.1c277.7-0.8 490.8 223 490.8 556.3z'
|
||||
fill='#ff5252'
|
||||
/>
|
||||
<path
|
||||
d='m1099.9 663.4c8.7 8.7 13.5 19.9 13.5 31.9v418.8h-643.3c-20.7 0-37.5-16.8-37.5-37.5v-643.4h418.2c12 0 24 4.8 32 13.6z'
|
||||
fill='#000000'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DiscordIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -1437,7 +1390,7 @@ export function AmplitudeIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 49 49'>
|
||||
<path
|
||||
fill='currentColor'
|
||||
fill='#FFFFFF'
|
||||
d='M23.4,15.3c0.6,1.8,1.2,4.1,1.9,6.7c-2.6,0-5.3-0.1-7.8-0.1h-1.3c1.5-5.7,3.2-10.1,4.6-11.1 c0.1-0.1,0.2-0.1,0.4-0.1c0.2,0,0.3,0.1,0.5,0.3C21.9,11.5,22.5,12.7,23.4,15.3z M49,24.5C49,38,38,49,24.5,49S0,38,0,24.5 S11,0,24.5,0S49,11,49,24.5z M42.7,23.9c0-0.6-0.4-1.2-1-1.3l0,0l0,0l0,0c-0.1,0-0.1,0-0.2,0h-0.2c-4.1-0.3-8.4-0.4-12.4-0.5l0,0 C27,14.8,24.5,7.4,21.3,7.4c-3,0-5.8,4.9-8.2,14.5c-1.7,0-3.2,0-4.6-0.1c-0.1,0-0.2,0-0.2,0c-0.3,0-0.5,0-0.5,0 c-0.8,0.1-1.4,0.9-1.4,1.7c0,0.8,0.6,1.6,1.5,1.7l0,0h4.6c-0.4,1.9-0.8,3.8-1.1,5.6l-0.1,0.8l0,0c0,0.6,0.5,1.1,1.1,1.1 c0.4,0,0.8-0.2,1-0.5l0,0l2.2-7.1h10.7c0.8,3.1,1.7,6.3,2.8,9.3c0.6,1.6,2,5.4,4.4,5.4l0,0c3.6,0,5-5.8,5.9-9.6 c0.2-0.8,0.4-1.5,0.5-2.1l0.1-0.2l0,0c0-0.1,0-0.2,0-0.3c-0.1-0.2-0.2-0.3-0.4-0.4c-0.3-0.1-0.5,0.1-0.6,0.4l0,0l-0.1,0.2 c-0.3,0.8-0.6,1.6-0.8,2.3v0.1c-1.6,4.4-2.3,6.4-3.7,6.4l0,0l0,0l0,0c-1.8,0-3.5-7.3-4.1-10.1c-0.1-0.5-0.2-0.9-0.3-1.3h11.7 c0.2,0,0.4-0.1,0.6-0.1l0,0c0,0,0,0,0.1,0c0,0,0,0,0.1,0l0,0c0,0,0.1,0,0.1-0.1l0,0C42.5,24.6,42.7,24.3,42.7,23.9z'
|
||||
/>
|
||||
</svg>
|
||||
@@ -4138,17 +4091,6 @@ export function IncidentioIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function InfisicalIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<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'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IntercomIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -4254,7 +4196,7 @@ export function ZoomIcon(props: SVGProps<SVGSVGElement>) {
|
||||
fill='currentColor'
|
||||
width='800px'
|
||||
height='800px'
|
||||
viewBox='-1 9.5 34 13'
|
||||
viewBox='0 0 32 32'
|
||||
version='1.1'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
@@ -4627,17 +4569,11 @@ export function ShopifyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function BoxCompanyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='2500'
|
||||
height='1379'
|
||||
viewBox='0 0 444.893 245.414'
|
||||
>
|
||||
<g fill='#0075C9'>
|
||||
<path d='M239.038 72.43c-33.081 0-61.806 18.6-76.322 45.904-14.516-27.305-43.24-45.902-76.32-45.902-19.443 0-37.385 6.424-51.821 17.266V16.925h-.008C34.365 7.547 26.713 0 17.286 0 7.858 0 .208 7.547.008 16.925H0v143.333h.036c.768 47.051 39.125 84.967 86.359 84.967 33.08 0 61.805-18.603 76.32-45.908 14.517 27.307 43.241 45.906 76.321 45.906 47.715 0 86.396-38.684 86.396-86.396.001-47.718-38.682-86.397-86.394-86.397zM86.395 210.648c-28.621 0-51.821-23.201-51.821-51.82 0-28.623 23.201-51.823 51.821-51.823 28.621 0 51.822 23.2 51.822 51.823 0 28.619-23.201 51.82-51.822 51.82zm152.643 0c-28.622 0-51.821-23.201-51.821-51.822 0-28.623 23.2-51.821 51.821-51.821 28.619 0 51.822 23.198 51.822 51.821-.001 28.621-23.203 51.822-51.822 51.822z' />
|
||||
<path d='M441.651 218.033l-44.246-59.143 44.246-59.144-.008-.007c5.473-7.62 3.887-18.249-3.652-23.913-7.537-5.658-18.187-4.221-23.98 3.157l-.004-.002-38.188 51.047-38.188-51.047-.006.009c-5.793-7.385-16.441-8.822-23.981-3.16-7.539 5.664-9.125 16.293-3.649 23.911l-.008.005 44.245 59.144-44.245 59.143.008.005c-5.477 7.62-3.89 18.247 3.649 23.909 7.54 5.664 18.188 4.225 23.981-3.155l.006.007 38.188-51.049 38.188 51.049.004-.002c5.794 7.377 16.443 8.814 23.98 3.154 7.539-5.662 9.125-16.291 3.652-23.91l.008-.008z' />
|
||||
</g>
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 41 22'>
|
||||
<path
|
||||
d='M39.7 19.2c.5.7.4 1.6-.2 2.1-.7.5-1.7.4-2.2-.2l-3.5-4.5-3.4 4.4c-.5.7-1.5.7-2.2.2-.7-.5-.8-1.4-.3-2.1l4-5.2-4-5.2c-.5-.7-.3-1.7.3-2.2.7-.5 1.7-.3 2.2.3l3.4 4.5L37.3 7c.5-.7 1.4-.8 2.2-.3.7.5.7 1.5.2 2.2L35.8 14l3.9 5.2zm-18.2-.6c-2.6 0-4.7-2-4.7-4.6 0-2.5 2.1-4.6 4.7-4.6s4.7 2.1 4.7 4.6c-.1 2.6-2.2 4.6-4.7 4.6zm-13.8 0c-2.6 0-4.7-2-4.7-4.6 0-2.5 2.1-4.6 4.7-4.6s4.7 2.1 4.7 4.6c0 2.6-2.1 4.6-4.7 4.6zM21.5 6.4c-2.9 0-5.5 1.6-6.8 4-1.3-2.4-3.9-4-6.9-4-1.8 0-3.4.6-4.7 1.5V1.5C3.1.7 2.4 0 1.6 0 .7 0 0 .7 0 1.5v12.6c.1 4.2 3.5 7.5 7.7 7.5 3 0 5.6-1.7 6.9-4.1 1.3 2.4 3.9 4.1 6.8 4.1 4.3 0 7.8-3.4 7.8-7.7.1-4.1-3.4-7.5-7.7-7.5z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -6107,19 +6043,6 @@ export function AgentSkillsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function OktaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 63 63' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M34.6.4l-1.3 16c-.6-.1-1.2-.1-1.9-.1-.8 0-1.6.1-2.3.2l-.7-7.7c0-.2.2-.5.4-.5h1.3L29.5.5c0-.2.2-.5.4-.5h4.3c.3 0 .5.2.4.4zm-10.8.8c-.1-.2-.3-.4-.5-.3l-4 1.5c-.3.1-.4.4-.3.6l3.3 7.1-1.2.5c-.2.1-.3.3-.2.6l3.3 7c1.2-.7 2.5-1.2 3.9-1.5L23.8 1.2zM14 5.7l9.3 13.1c-1.2.8-2.2 1.7-3.1 2.7L14.5 16c-.2-.2-.2-.5 0-.6l1-.8L10 9c-.2-.2-.2-.5 0-.6l3.3-2.7c.2-.3.5-.2.7 0zM6.2 13.2c-.2-.1-.5-.1-.6.1l-2.1 3.7c-.1.2 0 .5.2.6l7.1 3.4-.7 1.1c-.1.2 0 .5.2.6l7.1 3.2c.5-1.3 1.2-2.5 2-3.6L6.2 13.2zM.9 23.3c0-.2.3-.4.5-.3l15.5 4c-.4 1.3-.6 2.7-.7 4.1l-7.8-.6c-.2 0-.4-.2-.4-.5l.2-1.3L.6 28c-.2 0-.4-.2-.4-.5l.7-4.2zM.4 33.8c-.3 0-.4.2-.4.5l.8 4.2c0 .2.3.4.5.3l7.6-2 .2 1.3c0 .2.3.4.5.3l7.5-2.1c-.4-1.3-.7-2.7-.8-4.1L.4 33.8zm2.5 11.1c-.1-.2 0-.5.2-.6l14.5-6.9c.5 1.3 1.3 2.5 2.2 3.6l-6.3 4.5c-.2.1-.5.1-.6-.1L12 44.3l-6.5 4.5c-.2.1-.5.1-.6-.1l-2-3.8zm17.5-3L9.1 53.3c-.2.2-.2.5 0 .6l3.3 2.7c.2.2.5.1.6-.1l4.6-6.4 1 .9c.2.2.5.1.6-.1l4.4-6.4c-1.2-.7-2.3-1.6-3.2-2.6zm-2.2 18.2c-.2-.1-.3-.3-.2-.6L24.6 45c1.2.6 2.6 1.1 3.9 1.4l-2 7.5c-.1.2-.3.4-.5.3l-1.2-.5-2.1 7.6c-.1.2-.3.4-.5.3l-4-1.5zm10.9-13.5l-1.3 16c0 .2.2.5.4.5H33c.2 0 .4-.2.4-.5l-.6-7.8h1.3c.2 0 .4-.2.4-.5l-.7-7.7c-.8.1-1.5.2-2.3.2-.6 0-1.3 0-1.9-.1zm16-43.2c.1-.2 0-.5-.2-.6l-4-1.5c-.2-.1-.5.1-.5.3l-2.1 7.6-1.2-.5c-.2-.1-.5.1-.5.3l-2 7.5c1.4.3 2.7.8 3.9 1.4l6.6-14.5zm8.8 6.3L42.6 21.1c-.9-1-2-1.9-3.2-2.6l4.4-6.4c.1-.2.4-.2.6-.1l1 .9 4.6-6.4c.1-.2.4-.2.6-.1l3.3 2.7c.2.2.2.5 0 .6zM59.9 18.7c.2-.1.3-.4.2-.6L58 14.4c-.1-.2-.4-.3-.6-.1l-6.5 4.5-.7-1.1c-.1-.2-.4-.3-.6-.1L43.3 22c.9 1.1 1.6 2.3 2.2 3.6l14.4-6.9zm2.3 5.8l.7 4.2c0 .2-.1.5-.4.5l-15.9 1.5c-.1-1.4-.4-2.8-.8-4.1l7.5-2.1c.2-.1.5.1.5.3l.2 1.3 7.6-2c.3-.1.5.1.6.4zM61.5 40c.2.1.5-.1.5-.3l.7-4.2c0-.2-.1-.5-.4-.5l-7.8-.7.2-1.3c0-.2-.1-.5-.4-.5l-7.8-.6c0 1.4-.3 2.8-.7 4.1L61.5 40zm-4.1 9.6c-.1.2-.4.3-.6.1l-13.2-9.1c.8-1.1 1.5-2.3 2-3.6l7.1 3.2c.2.1.3.4.2.6L52.2 42l7.1 3.4c.2.1.3.4.2.6l-2.1 3.6zm-17.7-5.4L49 57.3c.1.2.4.2.6.1l3.3-2.7c.2-.2.2-.4 0-.6l-5.5-5.6 1-.8c.2-.2.2-.4 0-.6l-5.5-5.5c1.1.8 0 1.7-1.2 2.4zm0 17.8c-.2.1-.5-.1-.5-.3l-4.2-15.4c1.4-.3 2.7-.8 3.9-1.5l3.3 7c.1.2 0 .5-.2.6l-1.2.5 3.3 7.1c.1.2 0 .5-.2.6L39.7 62z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function OnePasswordIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 48 48' xmlns='http://www.w3.org/2000/svg' fill='none'>
|
||||
|
||||
@@ -16,8 +16,6 @@ import {
|
||||
AsanaIcon,
|
||||
AshbyIcon,
|
||||
AttioIcon,
|
||||
AzureIcon,
|
||||
BoxCompanyIcon,
|
||||
BrainIcon,
|
||||
BrandfetchIcon,
|
||||
BrowserUseIcon,
|
||||
@@ -34,7 +32,6 @@ import {
|
||||
DevinIcon,
|
||||
DiscordIcon,
|
||||
DocumentIcon,
|
||||
DocuSignIcon,
|
||||
DropboxIcon,
|
||||
DsPyIcon,
|
||||
DubIcon,
|
||||
@@ -82,7 +79,6 @@ import {
|
||||
HunterIOIcon,
|
||||
ImageIcon,
|
||||
IncidentioIcon,
|
||||
InfisicalIcon,
|
||||
IntercomIcon,
|
||||
JinaAIIcon,
|
||||
JiraIcon,
|
||||
@@ -111,7 +107,6 @@ import {
|
||||
Neo4jIcon,
|
||||
NotionIcon,
|
||||
ObsidianIcon,
|
||||
OktaIcon,
|
||||
OnePasswordIcon,
|
||||
OpenAIIcon,
|
||||
OutlookIcon,
|
||||
@@ -167,7 +162,6 @@ import {
|
||||
WhatsAppIcon,
|
||||
WikipediaIcon,
|
||||
WordpressIcon,
|
||||
WorkdayIcon,
|
||||
xIcon,
|
||||
YouTubeIcon,
|
||||
ZendeskIcon,
|
||||
@@ -190,7 +184,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
asana: AsanaIcon,
|
||||
ashby: AshbyIcon,
|
||||
attio: AttioIcon,
|
||||
box: BoxCompanyIcon,
|
||||
brandfetch: BrandfetchIcon,
|
||||
browser_use: BrowserUseIcon,
|
||||
calcom: CalComIcon,
|
||||
@@ -205,7 +198,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
datadog: DatadogIcon,
|
||||
devin: DevinIcon,
|
||||
discord: DiscordIcon,
|
||||
docusign: DocuSignIcon,
|
||||
dropbox: DropboxIcon,
|
||||
dspy: DsPyIcon,
|
||||
dub: DubIcon,
|
||||
@@ -254,7 +246,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
image_generator: ImageIcon,
|
||||
imap: MailServerIcon,
|
||||
incidentio: IncidentioIcon,
|
||||
infisical: InfisicalIcon,
|
||||
intercom_v2: IntercomIcon,
|
||||
jina: JinaAIIcon,
|
||||
jira: JiraIcon,
|
||||
@@ -272,7 +263,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
mailgun: MailgunIcon,
|
||||
mem0: Mem0Icon,
|
||||
memory: BrainIcon,
|
||||
microsoft_ad: AzureIcon,
|
||||
microsoft_dataverse: MicrosoftDataverseIcon,
|
||||
microsoft_excel_v2: MicrosoftExcelIcon,
|
||||
microsoft_planner: MicrosoftPlannerIcon,
|
||||
@@ -283,7 +273,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
neo4j: Neo4jIcon,
|
||||
notion_v2: NotionIcon,
|
||||
obsidian: ObsidianIcon,
|
||||
okta: OktaIcon,
|
||||
onedrive: MicrosoftOneDriveIcon,
|
||||
onepassword: OnePasswordIcon,
|
||||
openai: OpenAIIcon,
|
||||
@@ -342,7 +331,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
whatsapp: WhatsAppIcon,
|
||||
wikipedia: WikipediaIcon,
|
||||
wordpress: WordpressIcon,
|
||||
workday: WorkdayIcon,
|
||||
x: xIcon,
|
||||
youtube: YouTubeIcon,
|
||||
zendesk: ZendeskIcon,
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"mailer",
|
||||
"skills",
|
||||
"knowledgebase",
|
||||
"tables",
|
||||
"variables",
|
||||
"credentials",
|
||||
"execution",
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
---
|
||||
title: Tables
|
||||
description: Store, query, and manage structured data directly within your workspace
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Tables let you store and manage structured data directly in your workspace. Use them to maintain reference data, collect workflow outputs, or build lightweight databases — all without leaving Sim.
|
||||
|
||||
<Image src="/static/tables/tables-overview.png" alt="Tables view showing structured data with typed columns for name, title, company, role, and more" width={800} height={500} />
|
||||
|
||||
Each table has a schema of typed columns, supports filtering and sorting, and is fully accessible through the [Tables API](/docs/en/api-reference/(generated)/tables).
|
||||
|
||||
## Creating a Table
|
||||
|
||||
1. Open the **Tables** section from your workspace sidebar
|
||||
2. Click **New table**
|
||||
3. Name your table and start adding columns
|
||||
|
||||
Tables start with a single text column. Add more columns by clicking **New column** in the column header area.
|
||||
|
||||
## Column Types
|
||||
|
||||
Each column has a type that determines how values are stored and validated.
|
||||
|
||||
| Type | Description | Example Values |
|
||||
|------|-------------|----------------|
|
||||
| **Text** | Free-form string | `"Acme Corp"`, `"hello@example.com"` |
|
||||
| **Number** | Numeric value | `42`, `3.14`, `-100` |
|
||||
| **Boolean** | True or false | `true`, `false` |
|
||||
| **Date** | Date value | `2026-03-16` |
|
||||
| **JSON** | Structured object or array | `{"key": "value"}`, `[1, 2, 3]` |
|
||||
|
||||
<Callout type="info">
|
||||
Column types are enforced on input. For example, typing into a Number column is restricted to digits, dots, and minus signs. Non-numeric values entered via paste are coerced to `0`.
|
||||
</Callout>
|
||||
|
||||
## Working with Rows
|
||||
|
||||
### Adding Rows
|
||||
|
||||
- Click **New row** below the last row to append a new row
|
||||
- Press **Shift + Enter** while a cell is selected to insert a row below
|
||||
- Paste tabular data (from a spreadsheet or TSV) to bulk-create rows
|
||||
|
||||
### Editing Cells
|
||||
|
||||
Click a cell to select it, then press **Enter**, **F2**, or start typing to edit. Press **Escape** to cancel, or **Tab** to save and move to the next cell.
|
||||
|
||||
### Selecting Rows
|
||||
|
||||
Click a row's checkbox to select it. Selecting additional checkboxes adds to the selection without clearing previous selections.
|
||||
|
||||
| Action | Behavior |
|
||||
|--------|----------|
|
||||
| Click checkbox | Toggle that row's selection |
|
||||
| Shift + click checkbox | Select range from last clicked to current |
|
||||
| Click header checkbox | Select all / deselect all |
|
||||
| Shift + Space | Toggle row selection from keyboard |
|
||||
|
||||
### Deleting Rows
|
||||
|
||||
Right-click a selected row (or group of selected rows) and choose **Delete row** from the context menu.
|
||||
|
||||
## Filtering and Sorting
|
||||
|
||||
Use the toolbar above the table to filter and sort your data.
|
||||
|
||||
- **Filter**: Set conditions on any column (e.g., "Name contains Acme"). Multiple filters are combined with AND logic.
|
||||
- **Sort**: Order rows by any column, ascending or descending.
|
||||
|
||||
Filters and sorts are applied in real time and do not modify the underlying data.
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
All shortcuts work when the table is focused and no cell is being edited.
|
||||
|
||||
<Callout type="info">
|
||||
**Mod** refers to `Cmd` on macOS and `Ctrl` on Windows/Linux.
|
||||
</Callout>
|
||||
|
||||
### Navigation
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| Arrow keys | Move one cell |
|
||||
| `Mod` + Arrow keys | Jump to edge of table |
|
||||
| `Tab` / `Shift` + `Tab` | Move to next / previous cell |
|
||||
| `Escape` | Clear selection |
|
||||
|
||||
### Selection
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Shift` + Arrow keys | Extend selection by one cell |
|
||||
| `Mod` + `Shift` + Arrow keys | Extend selection to edge |
|
||||
| `Mod` + `A` | Select all rows |
|
||||
| `Shift` + `Space` | Toggle current row selection |
|
||||
|
||||
### Editing
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Enter` or `F2` | Start editing selected cell |
|
||||
| `Escape` | Cancel editing |
|
||||
| Type any character | Start editing with that character |
|
||||
| `Shift` + `Enter` | Insert new row below |
|
||||
| `Space` | Expand row details |
|
||||
|
||||
### Clipboard
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Mod` + `C` | Copy selected cells |
|
||||
| `Mod` + `X` | Cut selected cells |
|
||||
| `Mod` + `V` | Paste |
|
||||
| `Delete` / `Backspace` | Clear selected cells (all columns when using checkbox selection) |
|
||||
|
||||
### History
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Mod` + `Z` | Undo |
|
||||
| `Mod` + `Shift` + `Z` | Redo |
|
||||
| `Mod` + `Y` | Redo (alternative) |
|
||||
|
||||
## Using Tables in Workflows
|
||||
|
||||
Tables can be read from and written to within your workflows using the **Table** block. Common patterns include:
|
||||
|
||||
- **Lookup**: Query a table for reference data (e.g., pricing rules, customer metadata)
|
||||
- **Write-back**: Store workflow outputs in a table for later review or reporting
|
||||
- **Iteration**: Process each row in a table as part of a batch workflow
|
||||
|
||||
## API Access
|
||||
|
||||
Tables are fully accessible through the REST API. You can create, read, update, and delete both tables and rows programmatically.
|
||||
|
||||
See the [Tables API Reference](/docs/en/api-reference/(generated)/tables) for endpoints, parameters, and examples.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Use typed columns** to enforce data integrity — prefer Number and Boolean over storing everything as Text
|
||||
- **Name columns descriptively** so they are self-documenting when referenced in workflows
|
||||
- **Use JSON columns sparingly** — they are flexible but harder to filter and sort against
|
||||
- **Leverage the API** for bulk imports rather than manually entering large datasets
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Is there a row limit per table?", answer: "Tables are designed for working datasets. For very large datasets (100k+ rows), consider paginating API reads or splitting data across multiple tables." },
|
||||
{ question: "Can I import data from a spreadsheet?", answer: "Yes. Copy rows from any spreadsheet application and paste them directly into the table. Column values will be validated against the column types." },
|
||||
{ question: "Do tables support formulas?", answer: "Tables store raw data and do not support computed formulas. Use workflow logic (Function block or Agent block) to derive computed values and write them back to the table." },
|
||||
{ question: "Can multiple workflows write to the same table?", answer: "Yes. Table writes are atomic at the row level, so multiple workflows can safely write to the same table concurrently." },
|
||||
{ question: "How do I reference a table from a workflow?", answer: "Use the Table block in your workflow. Select the target table from the dropdown, choose an operation (read, write, update), and configure the parameters." },
|
||||
{ question: "Are tables shared across workspace members?", answer: "Yes. Tables are workspace-scoped and accessible to all members with appropriate permissions." },
|
||||
{ question: "Can I undo changes?", answer: "In the table editor, Cmd/Ctrl+Z undoes recent cell edits, row insertions, and row deletions. API-driven changes are not covered by the editor's undo history." },
|
||||
]} />
|
||||
@@ -30,50 +30,12 @@ In Sim, the Ashby integration enables your agents to programmatically manage you
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
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).
|
||||
Integrate Ashby into the workflow. Can list, search, create, and update candidates, list and get job details, create notes, list notes, list and get applications, create applications, and list offers.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `ashby_add_candidate_tag`
|
||||
|
||||
Adds a tag to a candidate in Ashby.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `candidateId` | string | Yes | The UUID of the candidate to add the tag to |
|
||||
| `tagId` | string | Yes | The UUID of the tag to add |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the tag was successfully added |
|
||||
|
||||
### `ashby_change_application_stage`
|
||||
|
||||
Moves an application to a different interview stage. Requires an archive reason when moving to an Archived stage.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `applicationId` | string | Yes | The UUID of the application to update the stage of |
|
||||
| `interviewStageId` | string | Yes | The UUID of the interview stage to move the application to |
|
||||
| `archiveReasonId` | string | No | Archive reason UUID. Required when moving to an Archived stage, ignored otherwise |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `applicationId` | string | Application UUID |
|
||||
| `stageId` | string | New interview stage UUID |
|
||||
|
||||
### `ashby_create_application`
|
||||
|
||||
Creates a new application for a candidate on a job. Optionally specify interview plan, stage, source, and credited user.
|
||||
@@ -95,7 +57,23 @@ Creates a new application for a candidate on a job. Optionally specify interview
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `applicationId` | string | Created application UUID |
|
||||
| `id` | string | Created application UUID |
|
||||
| `status` | string | Application status \(Active, Hired, Archived, Lead\) |
|
||||
| `candidate` | object | Associated candidate |
|
||||
| ↳ `id` | string | Candidate UUID |
|
||||
| ↳ `name` | string | Candidate name |
|
||||
| `job` | object | Associated job |
|
||||
| ↳ `id` | string | Job UUID |
|
||||
| ↳ `title` | string | Job title |
|
||||
| `currentInterviewStage` | object | Current interview stage |
|
||||
| ↳ `id` | string | Stage UUID |
|
||||
| ↳ `title` | string | Stage title |
|
||||
| ↳ `type` | string | Stage type |
|
||||
| `source` | object | Application source |
|
||||
| ↳ `id` | string | Source UUID |
|
||||
| ↳ `title` | string | Source title |
|
||||
| `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| `updatedAt` | string | ISO 8601 last update timestamp |
|
||||
|
||||
### `ashby_create_candidate`
|
||||
|
||||
@@ -107,8 +85,10 @@ Creates a new candidate record in Ashby.
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `name` | string | Yes | The candidate full name |
|
||||
| `email` | string | Yes | Primary email address for the candidate |
|
||||
| `email` | string | No | Primary email address for the candidate |
|
||||
| `emailType` | string | No | Email address type: Personal, Work, or Other \(default Work\) |
|
||||
| `phoneNumber` | string | No | Primary phone number for the candidate |
|
||||
| `phoneType` | string | No | Phone number type: Personal, Work, or Other \(default Work\) |
|
||||
| `linkedInUrl` | string | No | LinkedIn profile URL |
|
||||
| `githubUrl` | string | No | GitHub profile URL |
|
||||
| `sourceId` | string | No | UUID of the source to attribute the candidate to |
|
||||
@@ -147,7 +127,14 @@ Creates a note on a candidate in Ashby. Supports plain text and HTML content (bo
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `noteId` | string | Created note UUID |
|
||||
| `id` | string | Created note UUID |
|
||||
| `content` | string | Note content as stored |
|
||||
| `author` | object | Note author |
|
||||
| ↳ `id` | string | Author user UUID |
|
||||
| ↳ `firstName` | string | First name |
|
||||
| ↳ `lastName` | string | Last name |
|
||||
| ↳ `email` | string | Email address |
|
||||
| `createdAt` | string | ISO 8601 creation timestamp |
|
||||
|
||||
### `ashby_get_application`
|
||||
|
||||
@@ -241,7 +228,7 @@ Retrieves full details about a single job by its ID.
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Job UUID |
|
||||
| `title` | string | Job title |
|
||||
| `status` | string | Job status \(Open, Closed, Draft, Archived\) |
|
||||
| `status` | string | Job status \(Open, Closed, Draft, Archived, On Hold\) |
|
||||
| `employmentType` | string | Employment type \(FullTime, PartTime, Intern, Contract, Temporary\) |
|
||||
| `departmentId` | string | Department UUID |
|
||||
| `locationId` | string | Location UUID |
|
||||
@@ -250,58 +237,6 @@ Retrieves full details about a single job by its ID.
|
||||
| `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| `updatedAt` | string | ISO 8601 last update timestamp |
|
||||
|
||||
### `ashby_get_job_posting`
|
||||
|
||||
Retrieves full details about a single job posting by its ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `jobPostingId` | string | Yes | The UUID of the job posting to fetch |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Job posting UUID |
|
||||
| `title` | string | Job posting title |
|
||||
| `jobId` | string | Associated job UUID |
|
||||
| `locationName` | string | Location name |
|
||||
| `departmentName` | string | Department name |
|
||||
| `employmentType` | string | Employment type \(e.g. FullTime, PartTime, Contract\) |
|
||||
| `descriptionPlain` | string | Job posting description in plain text |
|
||||
| `isListed` | boolean | Whether the posting is publicly listed |
|
||||
| `publishedDate` | string | ISO 8601 published date |
|
||||
| `externalLink` | string | External link to the job posting |
|
||||
|
||||
### `ashby_get_offer`
|
||||
|
||||
Retrieves full details about a single offer by its ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `offerId` | string | Yes | The UUID of the offer to fetch |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Offer UUID |
|
||||
| `offerStatus` | string | Offer status \(e.g. WaitingOnCandidateResponse, CandidateAccepted\) |
|
||||
| `acceptanceStatus` | string | Acceptance status \(e.g. Accepted, Declined, Pending\) |
|
||||
| `applicationId` | string | Associated application UUID |
|
||||
| `startDate` | string | Offer start date |
|
||||
| `salary` | object | Salary details |
|
||||
| ↳ `currencyCode` | string | ISO 4217 currency code |
|
||||
| ↳ `value` | number | Salary amount |
|
||||
| `openingId` | string | Associated opening UUID |
|
||||
| `createdAt` | string | ISO 8601 creation timestamp \(from latest version\) |
|
||||
|
||||
### `ashby_list_applications`
|
||||
|
||||
Lists all applications in an Ashby organization with pagination and optional filters for status, job, candidate, and creation date.
|
||||
@@ -343,45 +278,6 @@ Lists all applications in an Ashby organization with pagination and optional fil
|
||||
| `moreDataAvailable` | boolean | Whether more pages of results exist |
|
||||
| `nextCursor` | string | Opaque cursor for fetching the next page |
|
||||
|
||||
### `ashby_list_archive_reasons`
|
||||
|
||||
Lists all archive reasons configured in Ashby.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `archiveReasons` | array | List of archive reasons |
|
||||
| ↳ `id` | string | Archive reason UUID |
|
||||
| ↳ `text` | string | Archive reason text |
|
||||
| ↳ `reasonType` | string | Reason type |
|
||||
| ↳ `isArchived` | boolean | Whether the reason is archived |
|
||||
|
||||
### `ashby_list_candidate_tags`
|
||||
|
||||
Lists all candidate tags configured in Ashby.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tags` | array | List of candidate tags |
|
||||
| ↳ `id` | string | Tag UUID |
|
||||
| ↳ `title` | string | Tag title |
|
||||
| ↳ `isArchived` | boolean | Whether the tag is archived |
|
||||
|
||||
### `ashby_list_candidates`
|
||||
|
||||
Lists all candidates in an Ashby organization with cursor-based pagination.
|
||||
@@ -414,98 +310,6 @@ Lists all candidates in an Ashby organization with cursor-based pagination.
|
||||
| `moreDataAvailable` | boolean | Whether more pages of results exist |
|
||||
| `nextCursor` | string | Opaque cursor for fetching the next page |
|
||||
|
||||
### `ashby_list_custom_fields`
|
||||
|
||||
Lists all custom field definitions configured in Ashby.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `customFields` | array | List of custom field definitions |
|
||||
| ↳ `id` | string | Custom field UUID |
|
||||
| ↳ `title` | string | Custom field title |
|
||||
| ↳ `fieldType` | string | Field type \(e.g. String, Number, Boolean\) |
|
||||
| ↳ `objectType` | string | Object type the field applies to \(e.g. Candidate, Application, Job\) |
|
||||
| ↳ `isArchived` | boolean | Whether the custom field is archived |
|
||||
|
||||
### `ashby_list_departments`
|
||||
|
||||
Lists all departments in Ashby.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `departments` | array | List of departments |
|
||||
| ↳ `id` | string | Department UUID |
|
||||
| ↳ `name` | string | Department name |
|
||||
| ↳ `isArchived` | boolean | Whether the department is archived |
|
||||
| ↳ `parentId` | string | Parent department UUID |
|
||||
|
||||
### `ashby_list_interviews`
|
||||
|
||||
Lists interview schedules in Ashby, optionally filtered by application or interview stage.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `applicationId` | string | No | The UUID of the application to list interview schedules for |
|
||||
| `interviewStageId` | string | No | The UUID of the interview stage to list interview schedules for |
|
||||
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
|
||||
| `perPage` | number | No | Number of results per page \(default 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `interviewSchedules` | array | List of interview schedules |
|
||||
| ↳ `id` | string | Interview schedule UUID |
|
||||
| ↳ `applicationId` | string | Associated application UUID |
|
||||
| ↳ `interviewStageId` | string | Interview stage UUID |
|
||||
| ↳ `status` | string | Schedule status |
|
||||
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| `moreDataAvailable` | boolean | Whether more pages of results exist |
|
||||
| `nextCursor` | string | Opaque cursor for fetching the next page |
|
||||
|
||||
### `ashby_list_job_postings`
|
||||
|
||||
Lists all job postings in Ashby.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `jobPostings` | array | List of job postings |
|
||||
| ↳ `id` | string | Job posting UUID |
|
||||
| ↳ `title` | string | Job posting title |
|
||||
| ↳ `jobId` | string | Associated job UUID |
|
||||
| ↳ `locationName` | string | Location name |
|
||||
| ↳ `departmentName` | string | Department name |
|
||||
| ↳ `employmentType` | string | Employment type \(e.g. FullTime, PartTime, Contract\) |
|
||||
| ↳ `isListed` | boolean | Whether the posting is publicly listed |
|
||||
| ↳ `publishedDate` | string | ISO 8601 published date |
|
||||
|
||||
### `ashby_list_jobs`
|
||||
|
||||
Lists all jobs in an Ashby organization. By default returns Open, Closed, and Archived jobs. Specify status to filter.
|
||||
@@ -535,30 +339,6 @@ Lists all jobs in an Ashby organization. By default returns Open, Closed, and Ar
|
||||
| `moreDataAvailable` | boolean | Whether more pages of results exist |
|
||||
| `nextCursor` | string | Opaque cursor for fetching the next page |
|
||||
|
||||
### `ashby_list_locations`
|
||||
|
||||
Lists all locations configured in Ashby.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `locations` | array | List of locations |
|
||||
| ↳ `id` | string | Location UUID |
|
||||
| ↳ `name` | string | Location name |
|
||||
| ↳ `isArchived` | boolean | Whether the location is archived |
|
||||
| ↳ `isRemote` | boolean | Whether this is a remote location |
|
||||
| ↳ `address` | object | Location address |
|
||||
| ↳ `city` | string | City |
|
||||
| ↳ `region` | string | State or region |
|
||||
| ↳ `country` | string | Country |
|
||||
|
||||
### `ashby_list_notes`
|
||||
|
||||
Lists all notes on a candidate with pagination support.
|
||||
@@ -606,106 +386,18 @@ Lists all offers with their latest version in an Ashby organization.
|
||||
| --------- | ---- | ----------- |
|
||||
| `offers` | array | List of offers |
|
||||
| ↳ `id` | string | Offer UUID |
|
||||
| ↳ `offerStatus` | string | Offer status |
|
||||
| ↳ `acceptanceStatus` | string | Acceptance status |
|
||||
| ↳ `applicationId` | string | Associated application UUID |
|
||||
| ↳ `startDate` | string | Offer start date |
|
||||
| ↳ `salary` | object | Salary details |
|
||||
| ↳ `currencyCode` | string | ISO 4217 currency code |
|
||||
| ↳ `value` | number | Salary amount |
|
||||
| ↳ `openingId` | string | Associated opening UUID |
|
||||
| ↳ `status` | string | Offer status |
|
||||
| ↳ `candidate` | object | Associated candidate |
|
||||
| ↳ `id` | string | Candidate UUID |
|
||||
| ↳ `name` | string | Candidate name |
|
||||
| ↳ `job` | object | Associated job |
|
||||
| ↳ `id` | string | Job UUID |
|
||||
| ↳ `title` | string | Job title |
|
||||
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| ↳ `updatedAt` | string | ISO 8601 last update timestamp |
|
||||
| `moreDataAvailable` | boolean | Whether more pages of results exist |
|
||||
| `nextCursor` | string | Opaque cursor for fetching the next page |
|
||||
|
||||
### `ashby_list_openings`
|
||||
|
||||
Lists all openings in Ashby with pagination.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
|
||||
| `perPage` | number | No | Number of results per page \(default 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `openings` | array | List of openings |
|
||||
| ↳ `id` | string | Opening UUID |
|
||||
| ↳ `openingState` | string | Opening state \(Approved, Closed, Draft, Filled, Open\) |
|
||||
| ↳ `isArchived` | boolean | Whether the opening is archived |
|
||||
| ↳ `openedAt` | string | ISO 8601 opened timestamp |
|
||||
| ↳ `closedAt` | string | ISO 8601 closed timestamp |
|
||||
| `moreDataAvailable` | boolean | Whether more pages of results exist |
|
||||
| `nextCursor` | string | Opaque cursor for fetching the next page |
|
||||
|
||||
### `ashby_list_sources`
|
||||
|
||||
Lists all candidate sources configured in Ashby.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sources` | array | List of sources |
|
||||
| ↳ `id` | string | Source UUID |
|
||||
| ↳ `title` | string | Source title |
|
||||
| ↳ `isArchived` | boolean | Whether the source is archived |
|
||||
|
||||
### `ashby_list_users`
|
||||
|
||||
Lists all users in Ashby with pagination.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
|
||||
| `perPage` | number | No | Number of results per page \(default 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | List of users |
|
||||
| ↳ `id` | string | User UUID |
|
||||
| ↳ `firstName` | string | First name |
|
||||
| ↳ `lastName` | string | Last name |
|
||||
| ↳ `email` | string | Email address |
|
||||
| ↳ `isEnabled` | boolean | Whether the user account is enabled |
|
||||
| ↳ `globalRole` | string | User role \(Organization Admin, Elevated Access, Limited Access, External Recruiter\) |
|
||||
| `moreDataAvailable` | boolean | Whether more pages of results exist |
|
||||
| `nextCursor` | string | Opaque cursor for fetching the next page |
|
||||
|
||||
### `ashby_remove_candidate_tag`
|
||||
|
||||
Removes a tag from a candidate in Ashby.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Ashby API Key |
|
||||
| `candidateId` | string | Yes | The UUID of the candidate to remove the tag from |
|
||||
| `tagId` | string | Yes | The UUID of the tag to remove |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the tag was successfully removed |
|
||||
|
||||
### `ashby_search_candidates`
|
||||
|
||||
Searches for candidates by name and/or email with AND logic. Results are limited to 100 matches. Use candidate.list for full pagination.
|
||||
@@ -733,8 +425,6 @@ Searches for candidates by name and/or email with AND logic. Results are limited
|
||||
| ↳ `value` | string | Phone number |
|
||||
| ↳ `type` | string | Contact type \(Personal, Work, Other\) |
|
||||
| ↳ `isPrimary` | boolean | Whether this is the primary phone |
|
||||
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| ↳ `updatedAt` | string | ISO 8601 last update timestamp |
|
||||
|
||||
### `ashby_update_candidate`
|
||||
|
||||
@@ -748,7 +438,9 @@ Updates an existing candidate record in Ashby. Only provided fields are changed.
|
||||
| `candidateId` | string | Yes | The UUID of the candidate to update |
|
||||
| `name` | string | No | Updated full name |
|
||||
| `email` | string | No | Updated primary email address |
|
||||
| `emailType` | string | No | Email address type: Personal, Work, or Other \(default Work\) |
|
||||
| `phoneNumber` | string | No | Updated primary phone number |
|
||||
| `phoneType` | string | No | Phone number type: Personal, Work, or Other \(default Work\) |
|
||||
| `linkedInUrl` | string | No | LinkedIn profile URL |
|
||||
| `githubUrl` | string | No | GitHub profile URL |
|
||||
| `websiteUrl` | string | No | Personal website URL |
|
||||
|
||||
@@ -1,440 +0,0 @@
|
||||
---
|
||||
title: Box
|
||||
description: Manage files, folders, and e-signatures with Box
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="box"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Box](https://www.box.com/) is a leading cloud content management and file sharing platform trusted by enterprises worldwide to securely store, manage, and collaborate on files. Box provides robust APIs for automating file operations and integrating with business workflows, including [Box Sign](https://www.box.com/esignature) for native e-signatures.
|
||||
|
||||
With the Box integration in Sim, you can:
|
||||
|
||||
- **Upload files**: Upload documents, images, and other files to any Box folder
|
||||
- **Download files**: Retrieve file content from Box for processing in your workflows
|
||||
- **Get file info**: Access detailed metadata including size, owner, timestamps, tags, and shared links
|
||||
- **List folder contents**: Browse files and folders with sorting and pagination support
|
||||
- **Create folders**: Organize your Box storage by creating new folders programmatically
|
||||
- **Delete files and folders**: Remove content with optional recursive deletion for folders
|
||||
- **Copy files**: Duplicate files across folders with optional renaming
|
||||
- **Search**: Find files and folders by name, content, extension, or location
|
||||
- **Update file metadata**: Rename, move, add descriptions, or tag files
|
||||
- **Create sign requests**: Send documents for e-signature with one or more signers
|
||||
- **Track signing status**: Monitor the progress of sign requests
|
||||
- **List sign requests**: View all sign requests with marker-based pagination
|
||||
- **Cancel sign requests**: Cancel pending sign requests that are no longer needed
|
||||
- **Resend sign reminders**: Send reminder notifications to signers who haven't completed signing
|
||||
|
||||
These capabilities allow your Sim agents to automate Box operations directly within your workflows — from organizing documents and distributing content to processing uploaded files, managing e-signature workflows for offer letters and contracts, and maintaining structured cloud storage as part of your business processes.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `box_upload_file`
|
||||
|
||||
Upload a file to a Box folder
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `parentFolderId` | string | Yes | The ID of the folder to upload the file to \(use "0" for root\) |
|
||||
| `file` | file | No | The file to upload \(UserFile object\) |
|
||||
| `fileContent` | string | No | Legacy: base64 encoded file content |
|
||||
| `fileName` | string | No | Optional filename override |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | File ID |
|
||||
| `name` | string | File name |
|
||||
| `size` | number | File size in bytes |
|
||||
| `sha1` | string | SHA1 hash of file content |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `modifiedAt` | string | Last modified timestamp |
|
||||
| `parentId` | string | Parent folder ID |
|
||||
| `parentName` | string | Parent folder name |
|
||||
|
||||
### `box_download_file`
|
||||
|
||||
Download a file from Box
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to download |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | file | Downloaded file stored in execution files |
|
||||
| `content` | string | Base64 encoded file content |
|
||||
|
||||
### `box_get_file_info`
|
||||
|
||||
Get detailed information about a file in Box
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to get information about |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | File ID |
|
||||
| `name` | string | File name |
|
||||
| `description` | string | File description |
|
||||
| `size` | number | File size in bytes |
|
||||
| `sha1` | string | SHA1 hash of file content |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `modifiedAt` | string | Last modified timestamp |
|
||||
| `createdBy` | object | User who created the file |
|
||||
| `modifiedBy` | object | User who last modified the file |
|
||||
| `ownedBy` | object | User who owns the file |
|
||||
| `parentId` | string | Parent folder ID |
|
||||
| `parentName` | string | Parent folder name |
|
||||
| `sharedLink` | json | Shared link details |
|
||||
| `tags` | array | File tags |
|
||||
| `commentCount` | number | Number of comments |
|
||||
|
||||
### `box_list_folder_items`
|
||||
|
||||
List files and folders in a Box folder
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `folderId` | string | Yes | The ID of the folder to list items from \(use "0" for root\) |
|
||||
| `limit` | number | No | Maximum number of items to return per page |
|
||||
| `offset` | number | No | The offset for pagination |
|
||||
| `sort` | string | No | Sort field: id, name, date, or size |
|
||||
| `direction` | string | No | Sort direction: ASC or DESC |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `entries` | array | List of items in the folder |
|
||||
| ↳ `type` | string | Item type \(file, folder, web_link\) |
|
||||
| ↳ `id` | string | Item ID |
|
||||
| ↳ `name` | string | Item name |
|
||||
| ↳ `size` | number | Item size in bytes |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `modifiedAt` | string | Last modified timestamp |
|
||||
| `totalCount` | number | Total number of items in the folder |
|
||||
| `offset` | number | Current pagination offset |
|
||||
| `limit` | number | Current pagination limit |
|
||||
|
||||
### `box_create_folder`
|
||||
|
||||
Create a new folder in Box
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `name` | string | Yes | Name for the new folder |
|
||||
| `parentFolderId` | string | Yes | The ID of the parent folder \(use "0" for root\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Folder ID |
|
||||
| `name` | string | Folder name |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `modifiedAt` | string | Last modified timestamp |
|
||||
| `parentId` | string | Parent folder ID |
|
||||
| `parentName` | string | Parent folder name |
|
||||
|
||||
### `box_delete_file`
|
||||
|
||||
Delete a file from Box
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the file was successfully deleted |
|
||||
| `message` | string | Success confirmation message |
|
||||
|
||||
### `box_delete_folder`
|
||||
|
||||
Delete a folder from Box
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `folderId` | string | Yes | The ID of the folder to delete |
|
||||
| `recursive` | boolean | No | Delete folder and all its contents recursively |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the folder was successfully deleted |
|
||||
| `message` | string | Success confirmation message |
|
||||
|
||||
### `box_copy_file`
|
||||
|
||||
Copy a file to another folder in Box
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to copy |
|
||||
| `parentFolderId` | string | Yes | The ID of the destination folder |
|
||||
| `name` | string | No | Optional new name for the copied file |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | File ID |
|
||||
| `name` | string | File name |
|
||||
| `size` | number | File size in bytes |
|
||||
| `sha1` | string | SHA1 hash of file content |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `modifiedAt` | string | Last modified timestamp |
|
||||
| `parentId` | string | Parent folder ID |
|
||||
| `parentName` | string | Parent folder name |
|
||||
|
||||
### `box_search`
|
||||
|
||||
Search for files and folders in Box
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | Yes | The search query string |
|
||||
| `limit` | number | No | Maximum number of results to return |
|
||||
| `offset` | number | No | The offset for pagination |
|
||||
| `ancestorFolderId` | string | No | Restrict search to a specific folder and its subfolders |
|
||||
| `fileExtensions` | string | No | Comma-separated file extensions to filter by \(e.g., pdf,docx\) |
|
||||
| `type` | string | No | Restrict to a specific content type: file, folder, or web_link |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | array | Search results |
|
||||
| ↳ `type` | string | Item type \(file, folder, web_link\) |
|
||||
| ↳ `id` | string | Item ID |
|
||||
| ↳ `name` | string | Item name |
|
||||
| ↳ `size` | number | Item size in bytes |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `modifiedAt` | string | Last modified timestamp |
|
||||
| ↳ `parentId` | string | Parent folder ID |
|
||||
| ↳ `parentName` | string | Parent folder name |
|
||||
| `totalCount` | number | Total number of matching results |
|
||||
|
||||
### `box_update_file`
|
||||
|
||||
Update file info in Box (rename, move, change description, add tags)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to update |
|
||||
| `name` | string | No | New name for the file |
|
||||
| `description` | string | No | New description for the file \(max 256 characters\) |
|
||||
| `parentFolderId` | string | No | Move the file to a different folder by specifying the folder ID |
|
||||
| `tags` | string | No | Comma-separated tags to set on the file |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | File ID |
|
||||
| `name` | string | File name |
|
||||
| `description` | string | File description |
|
||||
| `size` | number | File size in bytes |
|
||||
| `sha1` | string | SHA1 hash of file content |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `modifiedAt` | string | Last modified timestamp |
|
||||
| `createdBy` | object | User who created the file |
|
||||
| `modifiedBy` | object | User who last modified the file |
|
||||
| `ownedBy` | object | User who owns the file |
|
||||
| `parentId` | string | Parent folder ID |
|
||||
| `parentName` | string | Parent folder name |
|
||||
| `sharedLink` | json | Shared link details |
|
||||
| `tags` | array | File tags |
|
||||
| `commentCount` | number | Number of comments |
|
||||
|
||||
### `box_sign_create_request`
|
||||
|
||||
Create a new Box Sign request to send documents for e-signature
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `sourceFileIds` | string | Yes | Comma-separated Box file IDs to send for signing |
|
||||
| `signerEmail` | string | Yes | Primary signer email address |
|
||||
| `signerRole` | string | No | Primary signer role: signer, approver, or final_copy_reader \(default: signer\) |
|
||||
| `additionalSigners` | string | No | JSON array of additional signers, e.g. \[\{"email":"user@example.com","role":"signer"\}\] |
|
||||
| `parentFolderId` | string | No | Box folder ID where signed documents will be stored \(default: user root\) |
|
||||
| `emailSubject` | string | No | Custom subject line for the signing email |
|
||||
| `emailMessage` | string | No | Custom message in the signing email body |
|
||||
| `name` | string | No | Name for the sign request |
|
||||
| `daysValid` | number | No | Number of days before the request expires \(0-730\) |
|
||||
| `areRemindersEnabled` | boolean | No | Whether to send automatic signing reminders |
|
||||
| `areTextSignaturesEnabled` | boolean | No | Whether to allow typed \(text\) signatures |
|
||||
| `signatureColor` | string | No | Signature color: blue, black, or red |
|
||||
| `redirectUrl` | string | No | URL to redirect signers to after signing |
|
||||
| `declinedRedirectUrl` | string | No | URL to redirect signers to after declining |
|
||||
| `isDocumentPreparationNeeded` | boolean | No | Whether document preparation is needed before sending |
|
||||
| `externalId` | string | No | External system reference ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Sign request ID |
|
||||
| `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, expired, error_converting, error_sending, finalizing, error_finalizing\) |
|
||||
| `name` | string | Sign request name |
|
||||
| `shortId` | string | Human-readable short ID |
|
||||
| `signers` | array | List of signers |
|
||||
| `sourceFiles` | array | Source files for signing |
|
||||
| `emailSubject` | string | Custom email subject line |
|
||||
| `emailMessage` | string | Custom email message body |
|
||||
| `daysValid` | number | Number of days the request is valid |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `autoExpireAt` | string | Auto-expiration timestamp |
|
||||
| `prepareUrl` | string | URL for document preparation \(if preparation is needed\) |
|
||||
| `senderEmail` | string | Email of the sender |
|
||||
|
||||
### `box_sign_get_request`
|
||||
|
||||
Get the details and status of a Box Sign request
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `signRequestId` | string | Yes | The ID of the sign request to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Sign request ID |
|
||||
| `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, expired, error_converting, error_sending, finalizing, error_finalizing\) |
|
||||
| `name` | string | Sign request name |
|
||||
| `shortId` | string | Human-readable short ID |
|
||||
| `signers` | array | List of signers |
|
||||
| `sourceFiles` | array | Source files for signing |
|
||||
| `emailSubject` | string | Custom email subject line |
|
||||
| `emailMessage` | string | Custom email message body |
|
||||
| `daysValid` | number | Number of days the request is valid |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `autoExpireAt` | string | Auto-expiration timestamp |
|
||||
| `prepareUrl` | string | URL for document preparation \(if preparation is needed\) |
|
||||
| `senderEmail` | string | Email of the sender |
|
||||
|
||||
### `box_sign_list_requests`
|
||||
|
||||
List all Box Sign requests
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `limit` | number | No | Maximum number of sign requests to return \(max 1000\) |
|
||||
| `marker` | string | No | Pagination marker from a previous response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `signRequests` | array | List of sign requests |
|
||||
| ↳ `id` | string | Sign request ID |
|
||||
| ↳ `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, expired, error_converting, error_sending, finalizing, error_finalizing\) |
|
||||
| ↳ `name` | string | Sign request name |
|
||||
| ↳ `shortId` | string | Human-readable short ID |
|
||||
| ↳ `signers` | array | List of signers |
|
||||
| ↳ `sourceFiles` | array | Source files for signing |
|
||||
| ↳ `emailSubject` | string | Custom email subject line |
|
||||
| ↳ `emailMessage` | string | Custom email message body |
|
||||
| ↳ `daysValid` | number | Number of days the request is valid |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `autoExpireAt` | string | Auto-expiration timestamp |
|
||||
| ↳ `prepareUrl` | string | URL for document preparation \(if preparation is needed\) |
|
||||
| ↳ `senderEmail` | string | Email of the sender |
|
||||
| `count` | number | Number of sign requests returned in this page |
|
||||
| `nextMarker` | string | Marker for next page of results |
|
||||
|
||||
### `box_sign_cancel_request`
|
||||
|
||||
Cancel a pending Box Sign request
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `signRequestId` | string | Yes | The ID of the sign request to cancel |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Sign request ID |
|
||||
| `status` | string | Request status \(converting, created, sent, viewed, signed, cancelled, declined, expired, error_converting, error_sending, finalizing, error_finalizing\) |
|
||||
| `name` | string | Sign request name |
|
||||
| `shortId` | string | Human-readable short ID |
|
||||
| `signers` | array | List of signers |
|
||||
| `sourceFiles` | array | Source files for signing |
|
||||
| `emailSubject` | string | Custom email subject line |
|
||||
| `emailMessage` | string | Custom email message body |
|
||||
| `daysValid` | number | Number of days the request is valid |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `autoExpireAt` | string | Auto-expiration timestamp |
|
||||
| `prepareUrl` | string | URL for document preparation \(if preparation is needed\) |
|
||||
| `senderEmail` | string | Email of the sender |
|
||||
|
||||
### `box_sign_resend_request`
|
||||
|
||||
Resend a Box Sign request to signers who have not yet signed
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `signRequestId` | string | Yes | The ID of the sign request to resend |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Success confirmation message |
|
||||
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
---
|
||||
title: DocuSign
|
||||
description: Send documents for e-signature via DocuSign
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="docusign"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[DocuSign](https://www.docusign.com) is the world's leading e-signature platform, enabling businesses to send, sign, and manage agreements digitally. With its powerful eSignature REST API, DocuSign supports the full document lifecycle from creation through completion.
|
||||
|
||||
With the DocuSign integration in Sim, you can:
|
||||
|
||||
- **Send envelopes**: Create and send documents for e-signature with custom recipients and signing tabs
|
||||
- **Use templates**: Send envelopes from pre-configured DocuSign templates with role assignments
|
||||
- **Track status**: Get envelope details including signing progress, timestamps, and recipient status
|
||||
- **List envelopes**: Search and filter envelopes by date range, status, and text
|
||||
- **Download documents**: Retrieve signed documents as base64-encoded files
|
||||
- **Manage recipients**: View signer and CC recipient details and signing status
|
||||
- **Void envelopes**: Cancel in-progress envelopes with a reason
|
||||
|
||||
In Sim, the DocuSign integration enables your agents to automate document workflows end-to-end. Agents can generate agreements, send them for signature, monitor completion, and retrieve signed copies—powering contract management, HR onboarding, sales closings, and compliance processes.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Create and send envelopes for e-signature, use templates, check signing status, download signed documents, and manage recipients with DocuSign.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `docusign_send_envelope`
|
||||
|
||||
Create and send a DocuSign envelope with a document for e-signature
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `emailSubject` | string | Yes | Email subject for the envelope |
|
||||
| `emailBody` | string | No | Email body message |
|
||||
| `signerEmail` | string | Yes | Email address of the signer |
|
||||
| `signerName` | string | Yes | Full name of the signer |
|
||||
| `ccEmail` | string | No | Email address of carbon copy recipient |
|
||||
| `ccName` | string | No | Full name of carbon copy recipient |
|
||||
| `file` | file | No | Document file to send for signature |
|
||||
| `status` | string | No | Envelope status: "sent" to send immediately, "created" for draft \(default: "sent"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `envelopeId` | string | Created envelope ID |
|
||||
| `status` | string | Envelope status |
|
||||
| `statusDateTime` | string | Status change datetime |
|
||||
| `uri` | string | Envelope URI |
|
||||
|
||||
### `docusign_create_from_template`
|
||||
|
||||
Create and send a DocuSign envelope using a pre-built template
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `templateId` | string | Yes | DocuSign template ID to use |
|
||||
| `emailSubject` | string | No | Override email subject \(uses template default if not set\) |
|
||||
| `emailBody` | string | No | Override email body message |
|
||||
| `templateRoles` | string | Yes | JSON array of template roles, e.g. \[\{"roleName":"Signer","name":"John","email":"john@example.com"\}\] |
|
||||
| `status` | string | No | Envelope status: "sent" to send immediately, "created" for draft \(default: "sent"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `envelopeId` | string | Created envelope ID |
|
||||
| `status` | string | Envelope status |
|
||||
| `statusDateTime` | string | Status change datetime |
|
||||
| `uri` | string | Envelope URI |
|
||||
|
||||
### `docusign_get_envelope`
|
||||
|
||||
Get the details and status of a DocuSign envelope
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `envelopeId` | string | Yes | The envelope ID to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `envelopeId` | string | Envelope ID |
|
||||
| `status` | string | Envelope status \(created, sent, delivered, completed, declined, voided\) |
|
||||
| `emailSubject` | string | Email subject line |
|
||||
| `sentDateTime` | string | When the envelope was sent |
|
||||
| `completedDateTime` | string | When all recipients completed signing |
|
||||
| `createdDateTime` | string | When the envelope was created |
|
||||
| `statusChangedDateTime` | string | When the status last changed |
|
||||
| `voidedReason` | string | Reason the envelope was voided |
|
||||
| `signerCount` | number | Number of signers |
|
||||
| `documentCount` | number | Number of documents |
|
||||
|
||||
### `docusign_list_envelopes`
|
||||
|
||||
List envelopes from your DocuSign account with optional filters
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fromDate` | string | No | Start date filter \(ISO 8601\). Defaults to 30 days ago |
|
||||
| `toDate` | string | No | End date filter \(ISO 8601\) |
|
||||
| `envelopeStatus` | string | No | Filter by status: created, sent, delivered, completed, declined, voided |
|
||||
| `searchText` | string | No | Search text to filter envelopes |
|
||||
| `count` | string | No | Maximum number of envelopes to return \(default: 25\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `envelopes` | array | Array of DocuSign envelopes |
|
||||
| ↳ `envelopeId` | string | Unique envelope identifier |
|
||||
| ↳ `status` | string | Envelope status \(created, sent, delivered, completed, declined, voided\) |
|
||||
| ↳ `emailSubject` | string | Email subject line |
|
||||
| ↳ `sentDateTime` | string | ISO 8601 datetime when envelope was sent |
|
||||
| ↳ `completedDateTime` | string | ISO 8601 datetime when envelope was completed |
|
||||
| ↳ `createdDateTime` | string | ISO 8601 datetime when envelope was created |
|
||||
| ↳ `statusChangedDateTime` | string | ISO 8601 datetime of last status change |
|
||||
| `totalSetSize` | number | Total number of matching envelopes |
|
||||
| `resultSetSize` | number | Number of envelopes returned in this response |
|
||||
|
||||
### `docusign_void_envelope`
|
||||
|
||||
Void (cancel) a sent DocuSign envelope that has not yet been completed
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `envelopeId` | string | Yes | The envelope ID to void |
|
||||
| `voidedReason` | string | Yes | Reason for voiding the envelope |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `envelopeId` | string | Voided envelope ID |
|
||||
| `status` | string | Envelope status \(voided\) |
|
||||
|
||||
### `docusign_download_document`
|
||||
|
||||
Download a signed document from a completed DocuSign envelope
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `envelopeId` | string | Yes | The envelope ID containing the document |
|
||||
| `documentId` | string | No | Specific document ID to download, or "combined" for all documents merged \(default: "combined"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `base64Content` | string | Base64-encoded document content |
|
||||
| `mimeType` | string | MIME type of the document |
|
||||
| `fileName` | string | Original file name |
|
||||
|
||||
### `docusign_list_templates`
|
||||
|
||||
List available templates in your DocuSign account
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `searchText` | string | No | Search text to filter templates by name |
|
||||
| `count` | string | No | Maximum number of templates to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `templates` | array | Array of DocuSign templates |
|
||||
| ↳ `templateId` | string | Template identifier |
|
||||
| ↳ `name` | string | Template name |
|
||||
| ↳ `description` | string | Template description |
|
||||
| ↳ `shared` | boolean | Whether template is shared |
|
||||
| ↳ `created` | string | ISO 8601 creation date |
|
||||
| ↳ `lastModified` | string | ISO 8601 last modified date |
|
||||
| `totalSetSize` | number | Total number of matching templates |
|
||||
| `resultSetSize` | number | Number of templates returned in this response |
|
||||
|
||||
### `docusign_list_recipients`
|
||||
|
||||
Get the recipient status details for a DocuSign envelope
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `envelopeId` | string | Yes | The envelope ID to get recipients for |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `signers` | array | Array of DocuSign recipients |
|
||||
| ↳ `recipientId` | string | Recipient identifier |
|
||||
| ↳ `name` | string | Recipient name |
|
||||
| ↳ `email` | string | Recipient email address |
|
||||
| ↳ `status` | string | Recipient signing status \(sent, delivered, completed, declined\) |
|
||||
| ↳ `signedDateTime` | string | ISO 8601 datetime when recipient signed |
|
||||
| ↳ `deliveredDateTime` | string | ISO 8601 datetime when delivered to recipient |
|
||||
| `carbonCopies` | array | Array of carbon copy recipients |
|
||||
| ↳ `recipientId` | string | Recipient ID |
|
||||
| ↳ `name` | string | Recipient name |
|
||||
| ↳ `email` | string | Recipient email |
|
||||
| ↳ `status` | string | Recipient status |
|
||||
|
||||
|
||||
@@ -53,9 +53,6 @@ Extract structured content from web pages with comprehensive metadata support. C
|
||||
| `url` | string | Yes | The URL to scrape content from \(e.g., "https://example.com/page"\) |
|
||||
| `scrapeOptions` | json | No | Options for content scraping |
|
||||
| `apiKey` | string | Yes | Firecrawl API key |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -89,9 +86,6 @@ Search for information on the web using Firecrawl
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | Yes | The search query to use |
|
||||
| `apiKey` | string | Yes | Firecrawl API key |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -129,9 +123,6 @@ Crawl entire websites and extract structured content from all accessible pages
|
||||
| `includePaths` | json | No | URL paths to include in crawling \(e.g., \["/docs/*", "/api/*"\]\). Only these paths will be crawled |
|
||||
| `onlyMainContent` | boolean | No | Extract only main content from pages |
|
||||
| `apiKey` | string | Yes | Firecrawl API Key |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -151,6 +142,7 @@ Crawl entire websites and extract structured content from all accessible pages
|
||||
| ↳ `statusCode` | number | HTTP status code |
|
||||
| ↳ `ogLocaleAlternate` | array | Alternate locale versions |
|
||||
| `total` | number | Total number of pages found during crawl |
|
||||
| `creditsUsed` | number | Number of credits consumed by the crawl operation |
|
||||
|
||||
### `firecrawl_map`
|
||||
|
||||
@@ -169,9 +161,6 @@ Get a complete list of URLs from any website quickly and reliably. Useful for di
|
||||
| `timeout` | number | No | Request timeout in milliseconds |
|
||||
| `location` | json | No | Geographic context for proxying \(country, languages\) |
|
||||
| `apiKey` | string | Yes | Firecrawl API key |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -198,9 +187,6 @@ Extract structured data from entire webpages using natural language prompts and
|
||||
| `ignoreInvalidURLs` | boolean | No | Skip invalid URLs in the array \(default: true\) |
|
||||
| `scrapeOptions` | json | No | Advanced scraping configuration options |
|
||||
| `apiKey` | string | Yes | Firecrawl API key |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -231,6 +217,7 @@ Autonomous web data extraction agent. Searches and gathers information based on
|
||||
| `success` | boolean | Whether the agent operation was successful |
|
||||
| `status` | string | Current status of the agent job \(processing, completed, failed\) |
|
||||
| `data` | object | Extracted data from the agent |
|
||||
| `creditsUsed` | number | Number of credits consumed by this agent task |
|
||||
| `expiresAt` | string | Timestamp when the results expire \(24 hours\) |
|
||||
| `sources` | object | Array of source URLs used by the agent |
|
||||
|
||||
|
||||
@@ -46,8 +46,6 @@ Search for books using the Google Books API
|
||||
| `startIndex` | number | No | Index of the first result to return \(for pagination\) |
|
||||
| `maxResults` | number | No | Maximum number of results to return \(1-40\) |
|
||||
| `langRestrict` | string | No | Restrict results to a specific language \(ISO 639-1 code\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -84,8 +82,6 @@ Get detailed information about a specific book volume
|
||||
| `apiKey` | string | Yes | Google Books API key |
|
||||
| `volumeId` | string | Yes | The ID of the volume to retrieve |
|
||||
| `projection` | string | No | Projection level \(full, lite\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -50,8 +50,6 @@ Get current air quality data for a location
|
||||
| `lat` | number | Yes | Latitude coordinate |
|
||||
| `lng` | number | Yes | Longitude coordinate |
|
||||
| `languageCode` | string | No | Language code for the response \(e.g., "en", "es"\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -93,8 +91,6 @@ Get directions and route information between two locations
|
||||
| `waypoints` | json | No | Array of intermediate waypoints |
|
||||
| `units` | string | No | Unit system: metric or imperial |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -139,8 +135,6 @@ Calculate travel distance and time between multiple origins and destinations
|
||||
| `avoid` | string | No | Features to avoid: tolls, highways, or ferries |
|
||||
| `units` | string | No | Unit system: metric or imperial |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -169,8 +163,6 @@ Get elevation data for a location
|
||||
| `apiKey` | string | Yes | Google Maps API key |
|
||||
| `lat` | number | Yes | Latitude coordinate |
|
||||
| `lng` | number | Yes | Longitude coordinate |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -193,8 +185,6 @@ Convert an address into geographic coordinates (latitude and longitude)
|
||||
| `address` | string | Yes | The address to geocode |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
| `region` | string | No | Region bias as a ccTLD code \(e.g., us, uk\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -227,8 +217,6 @@ Geolocate a device using WiFi access points, cell towers, or IP address
|
||||
| `considerIp` | boolean | No | Whether to use IP address for geolocation \(default: true\) |
|
||||
| `cellTowers` | array | No | Array of cell tower objects with cellId, locationAreaCode, mobileCountryCode, mobileNetworkCode |
|
||||
| `wifiAccessPoints` | array | No | Array of WiFi access point objects with macAddress \(required\), signalStrength, etc. |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -250,8 +238,6 @@ Get detailed information about a specific place
|
||||
| `placeId` | string | Yes | Google Place ID |
|
||||
| `fields` | string | No | Comma-separated list of fields to return |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -304,8 +290,6 @@ Search for places using a text query
|
||||
| `type` | string | No | Place type filter \(e.g., restaurant, cafe, hotel\) |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
| `region` | string | No | Region bias as a ccTLD code \(e.g., us, uk\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -338,8 +322,6 @@ Convert geographic coordinates (latitude and longitude) into a human-readable ad
|
||||
| `lat` | number | Yes | Latitude coordinate |
|
||||
| `lng` | number | Yes | Longitude coordinate |
|
||||
| `language` | string | No | Language code for results \(e.g., en, es, fr\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -364,8 +346,6 @@ Snap GPS coordinates to the nearest road segment
|
||||
| `apiKey` | string | Yes | Google Maps API key with Roads API enabled |
|
||||
| `path` | string | Yes | Pipe-separated list of lat,lng coordinates \(e.g., "60.170880,24.942795\|60.170879,24.942796"\) |
|
||||
| `interpolate` | boolean | No | Whether to interpolate additional points along the road |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -419,8 +399,6 @@ Get timezone information for a location
|
||||
| `lng` | number | Yes | Longitude coordinate |
|
||||
| `timestamp` | number | No | Unix timestamp to determine DST offset \(defaults to current time\) |
|
||||
| `language` | string | No | Language code for timezone name \(e.g., en, es, fr\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -446,8 +424,6 @@ Validate and standardize a postal address
|
||||
| `regionCode` | string | No | ISO 3166-1 alpha-2 country code \(e.g., "US", "CA"\) |
|
||||
| `locality` | string | No | City or locality name |
|
||||
| `enableUspsCass` | boolean | No | Enable USPS CASS validation for US addresses |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -55,8 +55,6 @@ Analyze a webpage for performance, accessibility, SEO, and best practices using
|
||||
| `category` | string | No | Lighthouse categories to analyze \(comma-separated\): performance, accessibility, best-practices, seo |
|
||||
| `strategy` | string | No | Analysis strategy: desktop or mobile |
|
||||
| `locale` | string | No | Locale for results \(e.g., en, fr, de\) |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -43,9 +43,6 @@ Translate text between languages using the Google Cloud Translation API. Support
|
||||
| `target` | string | Yes | Target language code \(e.g., "es", "fr", "de", "ja"\) |
|
||||
| `source` | string | No | Source language code. If omitted, the API will auto-detect the source language. |
|
||||
| `format` | string | No | Format of the text: "text" for plain text, "html" for HTML content |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -64,9 +61,6 @@ Detect the language of text using the Google Cloud Translation API.
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Cloud API key with Cloud Translation API enabled |
|
||||
| `text` | string | Yes | The text to detect the language of |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -138,26 +138,6 @@ Get the full transcript of a recording
|
||||
| ↳ `end` | number | End timestamp in ms |
|
||||
| ↳ `text` | string | Transcript text |
|
||||
|
||||
### `grain_list_views`
|
||||
|
||||
List available Grain views for webhook subscriptions
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Grain API key \(Personal Access Token\) |
|
||||
| `typeFilter` | string | No | Optional view type filter: recordings, highlights, or stories |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `views` | array | Array of Grain views |
|
||||
| ↳ `id` | string | View UUID |
|
||||
| ↳ `name` | string | View name |
|
||||
| ↳ `type` | string | View type: recordings, highlights, or stories |
|
||||
|
||||
### `grain_list_teams`
|
||||
|
||||
List all teams in the workspace
|
||||
@@ -205,9 +185,15 @@ Create a webhook to receive recording events
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Grain API key \(Personal Access Token\) |
|
||||
| `hookUrl` | string | Yes | Webhook endpoint URL \(e.g., "https://example.com/webhooks/grain"\) |
|
||||
| `viewId` | string | Yes | Grain view ID from GET /_/public-api/views |
|
||||
| `actions` | array | No | Optional list of actions to subscribe to: added, updated, removed |
|
||||
| `items` | string | No | No description |
|
||||
| `hookType` | string | Yes | Type of webhook: "recording_added" or "upload_status" |
|
||||
| `filterBeforeDatetime` | string | No | Filter: recordings before this ISO8601 date \(e.g., "2024-01-15T00:00:00Z"\) |
|
||||
| `filterAfterDatetime` | string | No | Filter: recordings after this ISO8601 date \(e.g., "2024-01-01T00:00:00Z"\) |
|
||||
| `filterParticipantScope` | string | No | Filter: "internal" or "external" |
|
||||
| `filterTeamId` | string | No | Filter: specific team UUID \(e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"\) |
|
||||
| `filterMeetingTypeId` | string | No | Filter: specific meeting type UUID \(e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"\) |
|
||||
| `includeHighlights` | boolean | No | Include highlights in webhook payload |
|
||||
| `includeParticipants` | boolean | No | Include participants in webhook payload |
|
||||
| `includeAiSummary` | boolean | No | Include AI summary in webhook payload |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -216,8 +202,9 @@ Create a webhook to receive recording events
|
||||
| `id` | string | Hook UUID |
|
||||
| `enabled` | boolean | Whether hook is active |
|
||||
| `hook_url` | string | The webhook URL |
|
||||
| `view_id` | string | Grain view ID for the webhook |
|
||||
| `actions` | array | Configured actions for the webhook |
|
||||
| `hook_type` | string | Type of hook: recording_added or upload_status |
|
||||
| `filter` | object | Applied filters |
|
||||
| `include` | object | Included fields |
|
||||
| `inserted_at` | string | ISO8601 creation timestamp |
|
||||
|
||||
### `grain_list_hooks`
|
||||
@@ -238,8 +225,9 @@ List all webhooks for the account
|
||||
| ↳ `id` | string | Hook UUID |
|
||||
| ↳ `enabled` | boolean | Whether hook is active |
|
||||
| ↳ `hook_url` | string | Webhook URL |
|
||||
| ↳ `view_id` | string | Grain view ID |
|
||||
| ↳ `actions` | array | Configured actions |
|
||||
| ↳ `hook_type` | string | Type: recording_added or upload_status |
|
||||
| ↳ `filter` | object | Applied filters |
|
||||
| ↳ `include` | object | Included fields |
|
||||
| ↳ `inserted_at` | string | Creation timestamp |
|
||||
|
||||
### `grain_delete_hook`
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
---
|
||||
title: Infisical
|
||||
description: Manage secrets with Infisical
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="infisical"
|
||||
color="#F7FE62"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Infisical](https://infisical.com/) is an open-source secrets management platform that helps teams centralize and manage application secrets, environment variables, and sensitive configuration data across their infrastructure. This integration brings Infisical's secrets management capabilities directly into Sim workflows.
|
||||
|
||||
With Infisical in Sim, you can:
|
||||
|
||||
- **List secrets**: Retrieve all secrets from a project environment with filtering by path, tags, and recursive subdirectory support
|
||||
- **Get a secret**: Fetch a specific secret by name, with optional version pinning and secret reference expansion
|
||||
- **Create secrets**: Add new secrets to any project environment with support for comments, paths, and tag assignments
|
||||
- **Update secrets**: Modify existing secret values, comments, names, and tags
|
||||
- **Delete secrets**: Remove secrets from a project environment
|
||||
|
||||
In Sim, the Infisical integration enables your agents to programmatically manage secrets as part of automated workflows — for example, rotating credentials, syncing environment variables across environments, or auditing secret usage. Simply configure the Infisical block with your API key, select the operation, and provide the project ID and environment slug to get started.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Infisical into your workflow. List, get, create, update, and delete secrets across project environments.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `infisical_list_secrets`
|
||||
|
||||
List all secrets in a project environment. Returns secret keys, values, comments, tags, and metadata.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Infisical API token |
|
||||
| `baseUrl` | string | No | Infisical instance URL \(default: "https://us.infisical.com"\). Use "https://eu.infisical.com" for EU Cloud or your self-hosted URL. |
|
||||
| `projectId` | string | Yes | The ID of the project to list secrets from |
|
||||
| `environment` | string | Yes | The environment slug \(e.g., "dev", "staging", "prod"\) |
|
||||
| `secretPath` | string | No | The path of the secrets \(default: "/"\) |
|
||||
| `recursive` | boolean | No | Whether to fetch secrets recursively from subdirectories |
|
||||
| `expandSecretReferences` | boolean | No | Whether to expand secret references \(default: true\) |
|
||||
| `viewSecretValue` | boolean | No | Whether to include secret values in the response \(default: true\) |
|
||||
| `includeImports` | boolean | No | Whether to include imported secrets \(default: true\) |
|
||||
| `tagSlugs` | string | No | Comma-separated tag slugs to filter secrets by |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `secrets` | array | Array of secrets |
|
||||
| ↳ `id` | string | Secret ID |
|
||||
| ↳ `workspace` | string | Workspace/project ID |
|
||||
| ↳ `secretKey` | string | Secret name/key |
|
||||
| ↳ `secretValue` | string | Secret value |
|
||||
| ↳ `secretComment` | string | Secret comment |
|
||||
| ↳ `secretPath` | string | Secret path |
|
||||
| ↳ `version` | number | Secret version |
|
||||
| ↳ `type` | string | Secret type \(shared or personal\) |
|
||||
| ↳ `environment` | string | Environment slug |
|
||||
| ↳ `tags` | array | Tags attached to the secret |
|
||||
| ↳ `id` | string | Tag ID |
|
||||
| ↳ `slug` | string | Tag slug |
|
||||
| ↳ `color` | string | Tag color |
|
||||
| ↳ `name` | string | Tag name |
|
||||
| ↳ `secretMetadata` | array | Custom metadata key-value pairs |
|
||||
| ↳ `key` | string | Metadata key |
|
||||
| ↳ `value` | string | Metadata value |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last update timestamp |
|
||||
| `count` | number | Total number of secrets returned |
|
||||
|
||||
### `infisical_get_secret`
|
||||
|
||||
Retrieve a single secret by name from a project environment.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Infisical API token |
|
||||
| `baseUrl` | string | No | Infisical instance URL \(default: "https://us.infisical.com"\). Use "https://eu.infisical.com" for EU Cloud or your self-hosted URL. |
|
||||
| `projectId` | string | Yes | The ID of the project |
|
||||
| `environment` | string | Yes | The environment slug \(e.g., "dev", "staging", "prod"\) |
|
||||
| `secretName` | string | Yes | The name of the secret to retrieve |
|
||||
| `secretPath` | string | No | The path of the secret \(default: "/"\) |
|
||||
| `version` | number | No | Specific version of the secret to retrieve |
|
||||
| `type` | string | No | Secret type: "shared" or "personal" \(default: "shared"\) |
|
||||
| `viewSecretValue` | boolean | No | Whether to include the secret value in the response \(default: true\) |
|
||||
| `expandSecretReferences` | boolean | No | Whether to expand secret references \(default: true\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `secret` | object | The retrieved secret |
|
||||
| ↳ `id` | string | Secret ID |
|
||||
| ↳ `workspace` | string | Workspace/project ID |
|
||||
| ↳ `secretKey` | string | Secret name/key |
|
||||
| ↳ `secretValue` | string | Secret value |
|
||||
| ↳ `secretComment` | string | Secret comment |
|
||||
| ↳ `secretPath` | string | Secret path |
|
||||
| ↳ `version` | number | Secret version |
|
||||
| ↳ `type` | string | Secret type \(shared or personal\) |
|
||||
| ↳ `environment` | string | Environment slug |
|
||||
| ↳ `tags` | array | Tags attached to the secret |
|
||||
| ↳ `id` | string | Tag ID |
|
||||
| ↳ `slug` | string | Tag slug |
|
||||
| ↳ `color` | string | Tag color |
|
||||
| ↳ `name` | string | Tag name |
|
||||
| ↳ `secretMetadata` | array | Custom metadata key-value pairs |
|
||||
| ↳ `key` | string | Metadata key |
|
||||
| ↳ `value` | string | Metadata value |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last update timestamp |
|
||||
|
||||
### `infisical_create_secret`
|
||||
|
||||
Create a new secret in a project environment.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Infisical API token |
|
||||
| `baseUrl` | string | No | Infisical instance URL \(default: "https://us.infisical.com"\). Use "https://eu.infisical.com" for EU Cloud or your self-hosted URL. |
|
||||
| `projectId` | string | Yes | The ID of the project |
|
||||
| `environment` | string | Yes | The environment slug \(e.g., "dev", "staging", "prod"\) |
|
||||
| `secretName` | string | Yes | The name of the secret to create |
|
||||
| `secretValue` | string | Yes | The value of the secret |
|
||||
| `secretPath` | string | No | The path for the secret \(default: "/"\) |
|
||||
| `secretComment` | string | No | A comment for the secret |
|
||||
| `type` | string | No | Secret type: "shared" or "personal" \(default: "shared"\) |
|
||||
| `tagIds` | string | No | Comma-separated tag IDs to attach to the secret |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `secret` | object | The created secret |
|
||||
| ↳ `id` | string | Secret ID |
|
||||
| ↳ `workspace` | string | Workspace/project ID |
|
||||
| ↳ `secretKey` | string | Secret name/key |
|
||||
| ↳ `secretValue` | string | Secret value |
|
||||
| ↳ `secretComment` | string | Secret comment |
|
||||
| ↳ `secretPath` | string | Secret path |
|
||||
| ↳ `version` | number | Secret version |
|
||||
| ↳ `type` | string | Secret type \(shared or personal\) |
|
||||
| ↳ `environment` | string | Environment slug |
|
||||
| ↳ `tags` | array | Tags attached to the secret |
|
||||
| ↳ `id` | string | Tag ID |
|
||||
| ↳ `slug` | string | Tag slug |
|
||||
| ↳ `color` | string | Tag color |
|
||||
| ↳ `name` | string | Tag name |
|
||||
| ↳ `secretMetadata` | array | Custom metadata key-value pairs |
|
||||
| ↳ `key` | string | Metadata key |
|
||||
| ↳ `value` | string | Metadata value |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last update timestamp |
|
||||
|
||||
### `infisical_update_secret`
|
||||
|
||||
Update an existing secret in a project environment.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Infisical API token |
|
||||
| `baseUrl` | string | No | Infisical instance URL \(default: "https://us.infisical.com"\). Use "https://eu.infisical.com" for EU Cloud or your self-hosted URL. |
|
||||
| `projectId` | string | Yes | The ID of the project |
|
||||
| `environment` | string | Yes | The environment slug \(e.g., "dev", "staging", "prod"\) |
|
||||
| `secretName` | string | Yes | The name of the secret to update |
|
||||
| `secretValue` | string | No | The new value for the secret |
|
||||
| `secretPath` | string | No | The path of the secret \(default: "/"\) |
|
||||
| `secretComment` | string | No | A comment for the secret |
|
||||
| `newSecretName` | string | No | New name for the secret \(to rename it\) |
|
||||
| `type` | string | No | Secret type: "shared" or "personal" \(default: "shared"\) |
|
||||
| `tagIds` | string | No | Comma-separated tag IDs to set on the secret |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `secret` | object | The updated secret |
|
||||
| ↳ `id` | string | Secret ID |
|
||||
| ↳ `workspace` | string | Workspace/project ID |
|
||||
| ↳ `secretKey` | string | Secret name/key |
|
||||
| ↳ `secretValue` | string | Secret value |
|
||||
| ↳ `secretComment` | string | Secret comment |
|
||||
| ↳ `secretPath` | string | Secret path |
|
||||
| ↳ `version` | number | Secret version |
|
||||
| ↳ `type` | string | Secret type \(shared or personal\) |
|
||||
| ↳ `environment` | string | Environment slug |
|
||||
| ↳ `tags` | array | Tags attached to the secret |
|
||||
| ↳ `id` | string | Tag ID |
|
||||
| ↳ `slug` | string | Tag slug |
|
||||
| ↳ `color` | string | Tag color |
|
||||
| ↳ `name` | string | Tag name |
|
||||
| ↳ `secretMetadata` | array | Custom metadata key-value pairs |
|
||||
| ↳ `key` | string | Metadata key |
|
||||
| ↳ `value` | string | Metadata value |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last update timestamp |
|
||||
|
||||
### `infisical_delete_secret`
|
||||
|
||||
Delete a secret from a project environment.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Infisical API token |
|
||||
| `baseUrl` | string | No | Infisical instance URL \(default: "https://us.infisical.com"\). Use "https://eu.infisical.com" for EU Cloud or your self-hosted URL. |
|
||||
| `projectId` | string | Yes | The ID of the project |
|
||||
| `environment` | string | Yes | The environment slug \(e.g., "dev", "staging", "prod"\) |
|
||||
| `secretName` | string | Yes | The name of the secret to delete |
|
||||
| `secretPath` | string | No | The path of the secret \(default: "/"\) |
|
||||
| `type` | string | No | Secret type: "shared" or "personal" \(default: "shared"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `secret` | object | The deleted secret |
|
||||
| ↳ `id` | string | Secret ID |
|
||||
| ↳ `workspace` | string | Workspace/project ID |
|
||||
| ↳ `secretKey` | string | Secret name/key |
|
||||
| ↳ `secretValue` | string | Secret value |
|
||||
| ↳ `secretComment` | string | Secret comment |
|
||||
| ↳ `secretPath` | string | Secret path |
|
||||
| ↳ `version` | number | Secret version |
|
||||
| ↳ `type` | string | Secret type \(shared or personal\) |
|
||||
| ↳ `environment` | string | Environment slug |
|
||||
| ↳ `tags` | array | Tags attached to the secret |
|
||||
| ↳ `id` | string | Tag ID |
|
||||
| ↳ `slug` | string | Tag slug |
|
||||
| ↳ `color` | string | Tag color |
|
||||
| ↳ `name` | string | Tag name |
|
||||
| ↳ `secretMetadata` | array | Custom metadata key-value pairs |
|
||||
| ↳ `key` | string | Metadata key |
|
||||
| ↳ `value` | string | Metadata value |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last update timestamp |
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ Extract and process web content into clean, LLM-friendly text using Jina AI Read
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | The extracted content from the URL, processed into clean, LLM-friendly text |
|
||||
| `tokensUsed` | number | Number of Jina tokens consumed by this request |
|
||||
|
||||
### `jina_search`
|
||||
|
||||
@@ -98,6 +97,5 @@ Search the web and return top 5 results with LLM-friendly content. Each result i
|
||||
| ↳ `content` | string | LLM-friendly extracted content |
|
||||
| ↳ `usage` | object | Token usage information |
|
||||
| ↳ `tokens` | number | Number of tokens consumed by this request |
|
||||
| `tokensUsed` | number | Number of Jina tokens consumed by this request |
|
||||
|
||||
|
||||
|
||||
@@ -122,37 +122,6 @@ Create a new document in a knowledge base
|
||||
| `message` | string | Success or error message describing the operation result |
|
||||
| `documentId` | string | ID of the created document |
|
||||
|
||||
### `knowledge_upsert_document`
|
||||
|
||||
Create or update a document in a knowledge base. If a document with the given ID or filename already exists, it will be replaced with the new content.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `knowledgeBaseId` | string | Yes | ID of the knowledge base containing the document |
|
||||
| `documentId` | string | No | Optional ID of an existing document to update. If not provided, lookup is done by filename. |
|
||||
| `name` | string | Yes | Name of the document |
|
||||
| `content` | string | Yes | Content of the document |
|
||||
| `documentTags` | json | No | Document tags |
|
||||
| `documentTags` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `data` | object | Information about the upserted document |
|
||||
| ↳ `documentId` | string | Document ID |
|
||||
| ↳ `documentName` | string | Document name |
|
||||
| ↳ `type` | string | Document type |
|
||||
| ↳ `enabled` | boolean | Whether the document is enabled |
|
||||
| ↳ `isUpdate` | boolean | Whether an existing document was replaced |
|
||||
| ↳ `previousDocumentId` | string | ID of the document that was replaced, if any |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last update timestamp |
|
||||
| `message` | string | Success or error message describing the operation result |
|
||||
| `documentId` | string | ID of the upserted document |
|
||||
|
||||
### `knowledge_list_tags`
|
||||
|
||||
List all tag definitions for a knowledge base
|
||||
|
||||
@@ -51,9 +51,6 @@ Search the web for information using Linkup
|
||||
| `includeDomains` | string | No | Comma-separated list of domain names to restrict search results to |
|
||||
| `includeInlineCitations` | boolean | No | Add inline citations to answers \(only applies when outputType is "sourcedAnswer"\) |
|
||||
| `includeSources` | boolean | No | Include sources in response |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"asana",
|
||||
"ashby",
|
||||
"attio",
|
||||
"box",
|
||||
"brandfetch",
|
||||
"browser_use",
|
||||
"calcom",
|
||||
@@ -28,7 +27,6 @@
|
||||
"datadog",
|
||||
"devin",
|
||||
"discord",
|
||||
"docusign",
|
||||
"dropbox",
|
||||
"dspy",
|
||||
"dub",
|
||||
@@ -77,7 +75,6 @@
|
||||
"image_generator",
|
||||
"imap",
|
||||
"incidentio",
|
||||
"infisical",
|
||||
"intercom",
|
||||
"jina",
|
||||
"jira",
|
||||
@@ -95,7 +92,6 @@
|
||||
"mailgun",
|
||||
"mem0",
|
||||
"memory",
|
||||
"microsoft_ad",
|
||||
"microsoft_dataverse",
|
||||
"microsoft_excel",
|
||||
"microsoft_planner",
|
||||
@@ -106,7 +102,6 @@
|
||||
"neo4j",
|
||||
"notion",
|
||||
"obsidian",
|
||||
"okta",
|
||||
"onedrive",
|
||||
"onepassword",
|
||||
"openai",
|
||||
@@ -166,7 +161,6 @@
|
||||
"whatsapp",
|
||||
"wikipedia",
|
||||
"wordpress",
|
||||
"workday",
|
||||
"x",
|
||||
"youtube",
|
||||
"zendesk",
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
---
|
||||
title: Azure AD
|
||||
description: Manage users and groups in Azure AD (Microsoft Entra ID)
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="microsoft_ad"
|
||||
color="#0078D4"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Azure Active Directory](https://entra.microsoft.com) (now Microsoft Entra ID) is Microsoft's cloud-based identity and access management service. It helps organizations manage users, groups, and access to applications and resources across cloud and on-premises environments.
|
||||
|
||||
With the Azure AD integration in Sim, you can:
|
||||
|
||||
- **Manage users**: List, create, update, and delete user accounts in your directory
|
||||
- **Manage groups**: Create and configure security groups and Microsoft 365 groups
|
||||
- **Control group membership**: Add and remove members from groups programmatically
|
||||
- **Query directory data**: Search and filter users and groups using OData expressions
|
||||
- **Automate onboarding/offboarding**: Create new user accounts with initial passwords and enable/disable accounts as part of HR workflows
|
||||
|
||||
In Sim, the Azure AD integration enables your agents to programmatically manage your organization's identity infrastructure. This allows for automation scenarios such as provisioning new employees, updating user profiles in bulk, managing team group memberships, and auditing directory data. By connecting Sim with Azure AD, you can streamline identity lifecycle management and ensure your directory stays in sync with your organization's needs.
|
||||
|
||||
## Need Help?
|
||||
|
||||
If you encounter issues with the Azure AD integration, contact us at [help@sim.ai](mailto:help@sim.ai)
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Azure Active Directory into your workflows. List, create, update, and delete users and groups. Manage group memberships programmatically.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `microsoft_ad_list_users`
|
||||
|
||||
List users in Azure AD (Microsoft Entra ID)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `top` | number | No | Maximum number of users to return \(default 100, max 999\) |
|
||||
| `filter` | string | No | OData filter expression \(e.g., "department eq \'Sales\'"\) |
|
||||
| `search` | string | No | Search string to filter users by displayName or mail |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | List of users |
|
||||
| `userCount` | number | Number of users returned |
|
||||
|
||||
### `microsoft_ad_get_user`
|
||||
|
||||
Get a user by ID or user principal name from Azure AD
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | User ID or user principal name \(e.g., "user@example.com"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `user` | object | User details |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `displayName` | string | Display name |
|
||||
| ↳ `givenName` | string | First name |
|
||||
| ↳ `surname` | string | Last name |
|
||||
| ↳ `userPrincipalName` | string | User principal name \(email\) |
|
||||
| ↳ `mail` | string | Email address |
|
||||
| ↳ `jobTitle` | string | Job title |
|
||||
| ↳ `department` | string | Department |
|
||||
| ↳ `officeLocation` | string | Office location |
|
||||
| ↳ `mobilePhone` | string | Mobile phone number |
|
||||
| ↳ `accountEnabled` | boolean | Whether the account is enabled |
|
||||
|
||||
### `microsoft_ad_create_user`
|
||||
|
||||
Create a new user in Azure AD (Microsoft Entra ID)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `displayName` | string | Yes | Display name for the user |
|
||||
| `mailNickname` | string | Yes | Mail alias for the user |
|
||||
| `userPrincipalName` | string | Yes | User principal name \(e.g., "user@example.com"\) |
|
||||
| `password` | string | Yes | Initial password for the user |
|
||||
| `accountEnabled` | boolean | Yes | Whether the account is enabled |
|
||||
| `givenName` | string | No | First name |
|
||||
| `surname` | string | No | Last name |
|
||||
| `jobTitle` | string | No | Job title |
|
||||
| `department` | string | No | Department |
|
||||
| `officeLocation` | string | No | Office location |
|
||||
| `mobilePhone` | string | No | Mobile phone number |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `user` | object | Created user details |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `displayName` | string | Display name |
|
||||
| ↳ `givenName` | string | First name |
|
||||
| ↳ `surname` | string | Last name |
|
||||
| ↳ `userPrincipalName` | string | User principal name \(email\) |
|
||||
| ↳ `mail` | string | Email address |
|
||||
| ↳ `jobTitle` | string | Job title |
|
||||
| ↳ `department` | string | Department |
|
||||
| ↳ `officeLocation` | string | Office location |
|
||||
| ↳ `mobilePhone` | string | Mobile phone number |
|
||||
| ↳ `accountEnabled` | boolean | Whether the account is enabled |
|
||||
|
||||
### `microsoft_ad_update_user`
|
||||
|
||||
Update user properties in Azure AD (Microsoft Entra ID)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | User ID or user principal name |
|
||||
| `displayName` | string | No | Display name |
|
||||
| `givenName` | string | No | First name |
|
||||
| `surname` | string | No | Last name |
|
||||
| `jobTitle` | string | No | Job title |
|
||||
| `department` | string | No | Department |
|
||||
| `officeLocation` | string | No | Office location |
|
||||
| `mobilePhone` | string | No | Mobile phone number |
|
||||
| `accountEnabled` | boolean | No | Whether the account is enabled |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `updated` | boolean | Whether the update was successful |
|
||||
| `userId` | string | ID of the updated user |
|
||||
|
||||
### `microsoft_ad_delete_user`
|
||||
|
||||
Delete a user from Azure AD (Microsoft Entra ID). The user is moved to a temporary container and can be restored within 30 days.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Yes | User ID or user principal name |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the deletion was successful |
|
||||
| `userId` | string | ID of the deleted user |
|
||||
|
||||
### `microsoft_ad_list_groups`
|
||||
|
||||
List groups in Azure AD (Microsoft Entra ID)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `top` | number | No | Maximum number of groups to return \(default 100, max 999\) |
|
||||
| `filter` | string | No | OData filter expression \(e.g., "securityEnabled eq true"\) |
|
||||
| `search` | string | No | Search string to filter groups by displayName or description |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `groups` | array | List of groups |
|
||||
| `groupCount` | number | Number of groups returned |
|
||||
|
||||
### `microsoft_ad_get_group`
|
||||
|
||||
Get a group by ID from Azure AD (Microsoft Entra ID)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `groupId` | string | Yes | Group ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `group` | object | Group details |
|
||||
| ↳ `id` | string | Group ID |
|
||||
| ↳ `displayName` | string | Display name |
|
||||
| ↳ `description` | string | Group description |
|
||||
| ↳ `mail` | string | Email address |
|
||||
| ↳ `mailEnabled` | boolean | Whether mail is enabled |
|
||||
| ↳ `mailNickname` | string | Mail nickname |
|
||||
| ↳ `securityEnabled` | boolean | Whether security is enabled |
|
||||
| ↳ `groupTypes` | array | Group types |
|
||||
| ↳ `visibility` | string | Group visibility |
|
||||
| ↳ `createdDateTime` | string | Creation date |
|
||||
|
||||
### `microsoft_ad_create_group`
|
||||
|
||||
Create a new group in Azure AD (Microsoft Entra ID)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `displayName` | string | Yes | Display name for the group |
|
||||
| `mailNickname` | string | Yes | Mail alias for the group \(ASCII only, max 64 characters\) |
|
||||
| `description` | string | No | Group description |
|
||||
| `mailEnabled` | boolean | Yes | Whether mail is enabled \(true for Microsoft 365 groups\) |
|
||||
| `securityEnabled` | boolean | Yes | Whether security is enabled \(true for security groups\) |
|
||||
| `groupTypes` | string | No | Group type: "Unified" for Microsoft 365 group, leave empty for security group |
|
||||
| `visibility` | string | No | Group visibility: "Private" or "Public" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `group` | object | Created group details |
|
||||
| ↳ `id` | string | Group ID |
|
||||
| ↳ `displayName` | string | Display name |
|
||||
| ↳ `description` | string | Group description |
|
||||
| ↳ `mail` | string | Email address |
|
||||
| ↳ `mailEnabled` | boolean | Whether mail is enabled |
|
||||
| ↳ `mailNickname` | string | Mail nickname |
|
||||
| ↳ `securityEnabled` | boolean | Whether security is enabled |
|
||||
| ↳ `groupTypes` | array | Group types |
|
||||
| ↳ `visibility` | string | Group visibility |
|
||||
| ↳ `createdDateTime` | string | Creation date |
|
||||
|
||||
### `microsoft_ad_update_group`
|
||||
|
||||
Update group properties in Azure AD (Microsoft Entra ID)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `groupId` | string | Yes | Group ID |
|
||||
| `displayName` | string | No | Display name |
|
||||
| `description` | string | No | Group description |
|
||||
| `mailNickname` | string | No | Mail alias |
|
||||
| `visibility` | string | No | Group visibility: "Private" or "Public" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `updated` | boolean | Whether the update was successful |
|
||||
| `groupId` | string | ID of the updated group |
|
||||
|
||||
### `microsoft_ad_delete_group`
|
||||
|
||||
Delete a group from Azure AD (Microsoft Entra ID). Microsoft 365 and security groups can be restored within 30 days.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `groupId` | string | Yes | Group ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the deletion was successful |
|
||||
| `groupId` | string | ID of the deleted group |
|
||||
|
||||
### `microsoft_ad_list_group_members`
|
||||
|
||||
List members of a group in Azure AD (Microsoft Entra ID)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `groupId` | string | Yes | Group ID |
|
||||
| `top` | number | No | Maximum number of members to return \(default 100, max 999\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `members` | array | List of group members |
|
||||
| `memberCount` | number | Number of members returned |
|
||||
|
||||
### `microsoft_ad_add_group_member`
|
||||
|
||||
Add a member to a group in Azure AD (Microsoft Entra ID)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `groupId` | string | Yes | Group ID |
|
||||
| `memberId` | string | Yes | User ID of the member to add |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `added` | boolean | Whether the member was added successfully |
|
||||
| `groupId` | string | Group ID |
|
||||
| `memberId` | string | Member ID that was added |
|
||||
|
||||
### `microsoft_ad_remove_group_member`
|
||||
|
||||
Remove a member from a group in Azure AD (Microsoft Entra ID)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `groupId` | string | Yes | Group ID |
|
||||
| `memberId` | string | Yes | User ID of the member to remove |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `removed` | boolean | Whether the member was removed successfully |
|
||||
| `groupId` | string | Group ID |
|
||||
| `memberId` | string | Member ID that was removed |
|
||||
|
||||
|
||||
@@ -1,517 +0,0 @@
|
||||
---
|
||||
title: Okta
|
||||
description: Manage users and groups in Okta
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="okta"
|
||||
color="#191919"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Okta](https://www.okta.com/) is an identity and access management platform that provides secure authentication, authorization, and user management for organizations.
|
||||
|
||||
With the Okta integration in Sim, you can:
|
||||
|
||||
- **List and search users**: Retrieve users from your Okta org with SCIM search expressions and filters
|
||||
- **Manage user lifecycle**: Create, activate, deactivate, suspend, unsuspend, and delete users
|
||||
- **Update user profiles**: Modify user attributes like name, email, phone, title, and department
|
||||
- **Reset passwords**: Trigger password reset flows with optional email notification
|
||||
- **Manage groups**: Create, update, delete, and list groups in your organization
|
||||
- **Manage group membership**: Add or remove users from groups, and list group members
|
||||
|
||||
In Sim, the Okta integration enables your agents to automate identity management tasks as part of their workflows. This allows for scenarios such as onboarding new employees, offboarding departing users, managing group-based access, auditing user status, and responding to security events by suspending or deactivating accounts.
|
||||
|
||||
## Need Help?
|
||||
|
||||
If you encounter issues with the Okta integration, contact us at [help@sim.ai](mailto:help@sim.ai)
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Okta identity management into your workflow. List, create, update, activate, suspend, and delete users. Reset passwords. Manage groups and group membership.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `okta_list_users`
|
||||
|
||||
List all users in your Okta organization with optional search and filtering
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `search` | string | No | Okta search expression \(e.g., profile.firstName eq "John" or profile.email co "example.com"\) |
|
||||
| `filter` | string | No | Okta filter expression \(e.g., status eq "ACTIVE"\) |
|
||||
| `limit` | number | No | Maximum number of users to return \(default: 200, max: 200\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | Array of Okta user objects |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `status` | string | User status \(ACTIVE, STAGED, PROVISIONED, etc.\) |
|
||||
| ↳ `firstName` | string | First name |
|
||||
| ↳ `lastName` | string | Last name |
|
||||
| ↳ `email` | string | Email address |
|
||||
| ↳ `login` | string | Login \(usually email\) |
|
||||
| ↳ `mobilePhone` | string | Mobile phone |
|
||||
| ↳ `title` | string | Job title |
|
||||
| ↳ `department` | string | Department |
|
||||
| ↳ `created` | string | Creation timestamp |
|
||||
| ↳ `lastLogin` | string | Last login timestamp |
|
||||
| ↳ `lastUpdated` | string | Last update timestamp |
|
||||
| ↳ `activated` | string | Activation timestamp |
|
||||
| ↳ `statusChanged` | string | Status change timestamp |
|
||||
| `count` | number | Number of users returned |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_get_user`
|
||||
|
||||
Get a specific user by ID or login from your Okta organization
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `userId` | string | Yes | User ID or login \(email\) to look up |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | User ID |
|
||||
| `status` | string | User status |
|
||||
| `firstName` | string | First name |
|
||||
| `lastName` | string | Last name |
|
||||
| `email` | string | Email address |
|
||||
| `login` | string | Login \(usually email\) |
|
||||
| `mobilePhone` | string | Mobile phone |
|
||||
| `secondEmail` | string | Secondary email |
|
||||
| `displayName` | string | Display name |
|
||||
| `title` | string | Job title |
|
||||
| `department` | string | Department |
|
||||
| `organization` | string | Organization |
|
||||
| `manager` | string | Manager name |
|
||||
| `managerId` | string | Manager ID |
|
||||
| `division` | string | Division |
|
||||
| `employeeNumber` | string | Employee number |
|
||||
| `userType` | string | User type |
|
||||
| `created` | string | Creation timestamp |
|
||||
| `activated` | string | Activation timestamp |
|
||||
| `lastLogin` | string | Last login timestamp |
|
||||
| `lastUpdated` | string | Last update timestamp |
|
||||
| `statusChanged` | string | Status change timestamp |
|
||||
| `passwordChanged` | string | Password change timestamp |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_create_user`
|
||||
|
||||
Create a new user in your Okta organization
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `firstName` | string | Yes | First name of the user |
|
||||
| `lastName` | string | Yes | Last name of the user |
|
||||
| `email` | string | Yes | Email address of the user |
|
||||
| `login` | string | No | Login for the user \(defaults to email if not provided\) |
|
||||
| `password` | string | No | Password for the user \(if not set, user will be emailed to set password\) |
|
||||
| `mobilePhone` | string | No | Mobile phone number |
|
||||
| `title` | string | No | Job title |
|
||||
| `department` | string | No | Department |
|
||||
| `activate` | boolean | No | Whether to activate the user immediately \(default: true\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Created user ID |
|
||||
| `status` | string | User status |
|
||||
| `firstName` | string | First name |
|
||||
| `lastName` | string | Last name |
|
||||
| `email` | string | Email address |
|
||||
| `login` | string | Login |
|
||||
| `created` | string | Creation timestamp |
|
||||
| `lastUpdated` | string | Last update timestamp |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_update_user`
|
||||
|
||||
Update a user profile in your Okta organization
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `userId` | string | Yes | User ID or login to update |
|
||||
| `firstName` | string | No | Updated first name |
|
||||
| `lastName` | string | No | Updated last name |
|
||||
| `email` | string | No | Updated email address |
|
||||
| `login` | string | No | Updated login |
|
||||
| `mobilePhone` | string | No | Updated mobile phone number |
|
||||
| `title` | string | No | Updated job title |
|
||||
| `department` | string | No | Updated department |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | User ID |
|
||||
| `status` | string | User status |
|
||||
| `firstName` | string | First name |
|
||||
| `lastName` | string | Last name |
|
||||
| `email` | string | Email address |
|
||||
| `login` | string | Login |
|
||||
| `created` | string | Creation timestamp |
|
||||
| `lastUpdated` | string | Last update timestamp |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_activate_user`
|
||||
|
||||
Activate a user in your Okta organization. Can only be performed on users with STAGED or DEPROVISIONED status. Optionally sends an activation email.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `userId` | string | Yes | User ID or login to activate |
|
||||
| `sendEmail` | boolean | No | Send activation email to the user \(default: true\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `userId` | string | Activated user ID |
|
||||
| `activated` | boolean | Whether the user was activated |
|
||||
| `activationUrl` | string | Activation URL \(only returned when sendEmail is false\) |
|
||||
| `activationToken` | string | Activation token \(only returned when sendEmail is false\) |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_deactivate_user`
|
||||
|
||||
Deactivate a user in your Okta organization. This transitions the user to DEPROVISIONED status.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `userId` | string | Yes | User ID or login to deactivate |
|
||||
| `sendEmail` | boolean | No | Send deactivation email to admin \(default: false\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `userId` | string | Deactivated user ID |
|
||||
| `deactivated` | boolean | Whether the user was deactivated |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_suspend_user`
|
||||
|
||||
Suspend a user in your Okta organization. Only users with ACTIVE status can be suspended. Suspended users cannot log in but retain group and app assignments.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `userId` | string | Yes | User ID or login to suspend |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `userId` | string | Suspended user ID |
|
||||
| `suspended` | boolean | Whether the user was suspended |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_unsuspend_user`
|
||||
|
||||
Unsuspend a previously suspended user in your Okta organization. Returns the user to ACTIVE status.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `userId` | string | Yes | User ID or login to unsuspend |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `userId` | string | Unsuspended user ID |
|
||||
| `unsuspended` | boolean | Whether the user was unsuspended |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_reset_password`
|
||||
|
||||
Generate a one-time token to reset a user password. Can email the reset link to the user or return it directly. Transitions the user to RECOVERY status.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `userId` | string | Yes | User ID or login to reset password for |
|
||||
| `sendEmail` | boolean | No | Send password reset email to the user \(default: true\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `userId` | string | User ID |
|
||||
| `resetPasswordUrl` | string | Password reset URL \(only returned when sendEmail is false\) |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_delete_user`
|
||||
|
||||
Permanently delete a user from your Okta organization. Can only be performed on DEPROVISIONED users. If the user is active, this will first deactivate them and a second call is needed to delete.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `userId` | string | Yes | User ID to delete |
|
||||
| `sendEmail` | boolean | No | Send deactivation email to admin \(default: false\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `userId` | string | Deleted user ID |
|
||||
| `deleted` | boolean | Whether the user was deleted |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_list_groups`
|
||||
|
||||
List all groups in your Okta organization with optional search and filtering
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `search` | string | No | Okta search expression for groups \(e.g., profile.name sw "Engineering" or type eq "OKTA_GROUP"\) |
|
||||
| `filter` | string | No | Okta filter expression \(e.g., type eq "OKTA_GROUP"\) |
|
||||
| `limit` | number | No | Maximum number of groups to return \(default: 10000, max: 10000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `groups` | array | Array of Okta group objects |
|
||||
| ↳ `id` | string | Group ID |
|
||||
| ↳ `name` | string | Group name |
|
||||
| ↳ `description` | string | Group description |
|
||||
| ↳ `type` | string | Group type \(OKTA_GROUP, APP_GROUP, BUILT_IN\) |
|
||||
| ↳ `created` | string | Creation timestamp |
|
||||
| ↳ `lastUpdated` | string | Last update timestamp |
|
||||
| ↳ `lastMembershipUpdated` | string | Last membership change timestamp |
|
||||
| `count` | number | Number of groups returned |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_get_group`
|
||||
|
||||
Get a specific group by ID from your Okta organization
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `groupId` | string | Yes | Group ID to look up |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Group ID |
|
||||
| `name` | string | Group name |
|
||||
| `description` | string | Group description |
|
||||
| `type` | string | Group type |
|
||||
| `created` | string | Creation timestamp |
|
||||
| `lastUpdated` | string | Last update timestamp |
|
||||
| `lastMembershipUpdated` | string | Last membership change timestamp |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_create_group`
|
||||
|
||||
Create a new group in your Okta organization
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `name` | string | Yes | Name of the group |
|
||||
| `description` | string | No | Description of the group |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Created group ID |
|
||||
| `name` | string | Group name |
|
||||
| `description` | string | Group description |
|
||||
| `type` | string | Group type |
|
||||
| `created` | string | Creation timestamp |
|
||||
| `lastUpdated` | string | Last update timestamp |
|
||||
| `lastMembershipUpdated` | string | Last membership change timestamp |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_update_group`
|
||||
|
||||
Update a group profile in your Okta organization. Only groups of OKTA_GROUP type can be updated. All profile properties must be specified (full replacement).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `groupId` | string | Yes | Group ID to update |
|
||||
| `name` | string | Yes | Updated group name |
|
||||
| `description` | string | No | Updated group description |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Group ID |
|
||||
| `name` | string | Group name |
|
||||
| `description` | string | Group description |
|
||||
| `type` | string | Group type |
|
||||
| `created` | string | Creation timestamp |
|
||||
| `lastUpdated` | string | Last update timestamp |
|
||||
| `lastMembershipUpdated` | string | Last membership change timestamp |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_delete_group`
|
||||
|
||||
Delete a group from your Okta organization. Groups of OKTA_GROUP or APP_GROUP type can be removed.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `groupId` | string | Yes | Group ID to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `groupId` | string | Deleted group ID |
|
||||
| `deleted` | boolean | Whether the group was deleted |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_add_user_to_group`
|
||||
|
||||
Add a user to a group in your Okta organization
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `groupId` | string | Yes | Group ID to add the user to |
|
||||
| `userId` | string | Yes | User ID to add to the group |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `groupId` | string | Group ID |
|
||||
| `userId` | string | User ID added to the group |
|
||||
| `added` | boolean | Whether the user was added |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_remove_user_from_group`
|
||||
|
||||
Remove a user from a group in your Okta organization
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `groupId` | string | Yes | Group ID to remove the user from |
|
||||
| `userId` | string | Yes | User ID to remove from the group |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `groupId` | string | Group ID |
|
||||
| `userId` | string | User ID removed from the group |
|
||||
| `removed` | boolean | Whether the user was removed |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `okta_list_group_members`
|
||||
|
||||
List all members of a specific group in your Okta organization
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Okta API token for authentication |
|
||||
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
|
||||
| `groupId` | string | Yes | Group ID to list members for |
|
||||
| `limit` | number | No | Maximum number of members to return \(default: 1000, max: 1000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `members` | array | Array of group member user objects |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `status` | string | User status |
|
||||
| ↳ `firstName` | string | First name |
|
||||
| ↳ `lastName` | string | Last name |
|
||||
| ↳ `email` | string | Email address |
|
||||
| ↳ `login` | string | Login |
|
||||
| ↳ `mobilePhone` | string | Mobile phone |
|
||||
| ↳ `title` | string | Job title |
|
||||
| ↳ `department` | string | Department |
|
||||
| ↳ `created` | string | Creation timestamp |
|
||||
| ↳ `lastLogin` | string | Last login timestamp |
|
||||
| ↳ `lastUpdated` | string | Last update timestamp |
|
||||
| ↳ `activated` | string | Activation timestamp |
|
||||
| ↳ `statusChanged` | string | Status change timestamp |
|
||||
| `count` | number | Number of members returned |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
|
||||
@@ -49,9 +49,6 @@ Generate completions using Perplexity AI chat models
|
||||
| `max_tokens` | number | No | Maximum number of tokens to generate \(e.g., 1024, 2048, 4096\) |
|
||||
| `temperature` | number | No | Sampling temperature between 0 and 1 \(e.g., 0.0 for deterministic, 0.7 for creative\) |
|
||||
| `apiKey` | string | Yes | Perplexity API key |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -81,8 +78,6 @@ Get ranked search results from Perplexity
|
||||
| `search_after_date` | string | No | Include only content published after this date \(format: MM/DD/YYYY\) |
|
||||
| `search_before_date` | string | No | Include only content published before this date \(format: MM/DD/YYYY\) |
|
||||
| `apiKey` | string | Yes | Perplexity API key |
|
||||
| `pricing` | per_request | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -47,9 +47,6 @@ A powerful web search tool that provides access to Google search results through
|
||||
| `hl` | string | No | Language code for search results \(e.g., "en", "es", "de", "fr"\) |
|
||||
| `type` | string | No | Type of search to perform \(e.g., "search", "news", "images", "videos", "places", "shopping"\) |
|
||||
| `apiKey` | string | Yes | Serper API Key |
|
||||
| `pricing` | custom | No | No description |
|
||||
| `metadata` | string | No | No description |
|
||||
| `rateLimit` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
---
|
||||
title: Workday
|
||||
description: Manage workers, hiring, onboarding, and HR operations in Workday
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="workday"
|
||||
color="#F5F0EB"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Workday HRIS into your workflow. Create pre-hires, hire employees, manage worker profiles, assign onboarding plans, handle job changes, retrieve compensation data, and process terminations.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `workday_get_worker`
|
||||
|
||||
Retrieve a specific worker profile including personal, employment, and organization data.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) |
|
||||
| `tenant` | string | Yes | Workday tenant name |
|
||||
| `username` | string | Yes | Integration System User username |
|
||||
| `password` | string | Yes | Integration System User password |
|
||||
| `workerId` | string | Yes | Worker ID to retrieve \(e.g., 3aa5550b7fe348b98d7b5741afc65534\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `worker` | json | Worker profile with personal, employment, and organization data |
|
||||
|
||||
### `workday_list_workers`
|
||||
|
||||
List or search workers with optional filtering and pagination.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) |
|
||||
| `tenant` | string | Yes | Workday tenant name |
|
||||
| `username` | string | Yes | Integration System User username |
|
||||
| `password` | string | Yes | Integration System User password |
|
||||
| `limit` | number | No | Maximum number of workers to return \(default: 20\) |
|
||||
| `offset` | number | No | Number of records to skip for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `workers` | array | Array of worker profiles |
|
||||
| `total` | number | Total number of matching workers |
|
||||
|
||||
### `workday_create_prehire`
|
||||
|
||||
Create a new pre-hire (applicant) record in Workday. This is typically the first step before hiring an employee.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) |
|
||||
| `tenant` | string | Yes | Workday tenant name |
|
||||
| `username` | string | Yes | Integration System User username |
|
||||
| `password` | string | Yes | Integration System User password |
|
||||
| `legalName` | string | Yes | Full legal name of the pre-hire \(e.g., "Jane Doe"\) |
|
||||
| `email` | string | No | Email address of the pre-hire |
|
||||
| `phoneNumber` | string | No | Phone number of the pre-hire |
|
||||
| `address` | string | No | Address of the pre-hire |
|
||||
| `countryCode` | string | No | ISO 3166-1 Alpha-2 country code \(defaults to US\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `preHireId` | string | ID of the created pre-hire record |
|
||||
| `descriptor` | string | Display name of the pre-hire |
|
||||
|
||||
### `workday_hire_employee`
|
||||
|
||||
Hire a pre-hire into an employee position. Converts an applicant into an active employee record with position, start date, and manager assignment.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) |
|
||||
| `tenant` | string | Yes | Workday tenant name |
|
||||
| `username` | string | Yes | Integration System User username |
|
||||
| `password` | string | Yes | Integration System User password |
|
||||
| `preHireId` | string | Yes | Pre-hire \(applicant\) ID to convert into an employee |
|
||||
| `positionId` | string | Yes | Position ID to assign the new hire to |
|
||||
| `hireDate` | string | Yes | Hire date in ISO 8601 format \(e.g., 2025-06-01\) |
|
||||
| `employeeType` | string | No | Employee type \(e.g., Regular, Temporary, Contractor\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `workerId` | string | Worker ID of the newly hired employee |
|
||||
| `employeeId` | string | Employee ID assigned to the new hire |
|
||||
| `eventId` | string | Event ID of the hire business process |
|
||||
| `hireDate` | string | Effective hire date |
|
||||
|
||||
### `workday_update_worker`
|
||||
|
||||
Update fields on an existing worker record in Workday.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) |
|
||||
| `tenant` | string | Yes | Workday tenant name |
|
||||
| `username` | string | Yes | Integration System User username |
|
||||
| `password` | string | Yes | Integration System User password |
|
||||
| `workerId` | string | Yes | Worker ID to update |
|
||||
| `fields` | json | Yes | Fields to update as JSON \(e.g., \{"businessTitle": "Senior Engineer", "primaryWorkEmail": "new@company.com"\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventId` | string | Event ID of the change personal information business process |
|
||||
| `workerId` | string | Worker ID that was updated |
|
||||
|
||||
### `workday_assign_onboarding`
|
||||
|
||||
Create or update an onboarding plan assignment for a worker. Sets up onboarding stages and manages the assignment lifecycle.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) |
|
||||
| `tenant` | string | Yes | Workday tenant name |
|
||||
| `username` | string | Yes | Integration System User username |
|
||||
| `password` | string | Yes | Integration System User password |
|
||||
| `workerId` | string | Yes | Worker ID to assign the onboarding plan to |
|
||||
| `onboardingPlanId` | string | Yes | Onboarding plan ID to assign |
|
||||
| `actionEventId` | string | Yes | Action event ID that enables the onboarding plan \(e.g., the hiring event ID\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `assignmentId` | string | Onboarding plan assignment ID |
|
||||
| `workerId` | string | Worker ID the plan was assigned to |
|
||||
| `planId` | string | Onboarding plan ID that was assigned |
|
||||
|
||||
### `workday_get_organizations`
|
||||
|
||||
Retrieve organizations, departments, and cost centers from Workday.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) |
|
||||
| `tenant` | string | Yes | Workday tenant name |
|
||||
| `username` | string | Yes | Integration System User username |
|
||||
| `password` | string | Yes | Integration System User password |
|
||||
| `type` | string | No | Organization type filter \(e.g., Supervisory, Cost_Center, Company, Region\) |
|
||||
| `limit` | number | No | Maximum number of organizations to return \(default: 20\) |
|
||||
| `offset` | number | No | Number of records to skip for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `organizations` | array | Array of organization records |
|
||||
| `total` | number | Total number of matching organizations |
|
||||
|
||||
### `workday_change_job`
|
||||
|
||||
Perform a job change for a worker including transfers, promotions, demotions, and lateral moves.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) |
|
||||
| `tenant` | string | Yes | Workday tenant name |
|
||||
| `username` | string | Yes | Integration System User username |
|
||||
| `password` | string | Yes | Integration System User password |
|
||||
| `workerId` | string | Yes | Worker ID for the job change |
|
||||
| `effectiveDate` | string | Yes | Effective date for the job change in ISO 8601 format \(e.g., 2025-06-01\) |
|
||||
| `newPositionId` | string | No | New position ID \(for transfers\) |
|
||||
| `newJobProfileId` | string | No | New job profile ID \(for role changes\) |
|
||||
| `newLocationId` | string | No | New work location ID \(for relocations\) |
|
||||
| `newSupervisoryOrgId` | string | No | Target supervisory organization ID \(for org transfers\) |
|
||||
| `reason` | string | Yes | Reason for the job change \(e.g., Promotion, Transfer, Reorganization\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventId` | string | Job change event ID |
|
||||
| `workerId` | string | Worker ID the job change was applied to |
|
||||
| `effectiveDate` | string | Effective date of the job change |
|
||||
|
||||
### `workday_get_compensation`
|
||||
|
||||
Retrieve compensation plan details for a specific worker.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) |
|
||||
| `tenant` | string | Yes | Workday tenant name |
|
||||
| `username` | string | Yes | Integration System User username |
|
||||
| `password` | string | Yes | Integration System User password |
|
||||
| `workerId` | string | Yes | Worker ID to retrieve compensation data for |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `compensationPlans` | array | Array of compensation plan details |
|
||||
| ↳ `id` | string | Compensation plan ID |
|
||||
| ↳ `planName` | string | Name of the compensation plan |
|
||||
| ↳ `amount` | number | Compensation amount |
|
||||
| ↳ `currency` | string | Currency code |
|
||||
| ↳ `frequency` | string | Pay frequency |
|
||||
|
||||
### `workday_terminate_worker`
|
||||
|
||||
Initiate a worker termination in Workday. Triggers the Terminate Employee business process.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tenantUrl` | string | Yes | Workday instance URL \(e.g., https://wd5-impl-services1.workday.com\) |
|
||||
| `tenant` | string | Yes | Workday tenant name |
|
||||
| `username` | string | Yes | Integration System User username |
|
||||
| `password` | string | Yes | Integration System User password |
|
||||
| `workerId` | string | Yes | Worker ID to terminate |
|
||||
| `terminationDate` | string | Yes | Termination date in ISO 8601 format \(e.g., 2025-06-01\) |
|
||||
| `reason` | string | Yes | Termination reason \(e.g., Resignation, End_of_Contract, Retirement\) |
|
||||
| `notificationDate` | string | No | Date the termination was communicated in ISO 8601 format |
|
||||
| `lastDayOfWork` | string | No | Last day of work in ISO 8601 format \(defaults to termination date\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventId` | string | Termination event ID |
|
||||
| `workerId` | string | Worker ID that was terminated |
|
||||
| `terminationDate` | string | Effective termination date |
|
||||
|
||||
|
||||
@@ -1,42 +1,21 @@
|
||||
{
|
||||
"name": "Sim Documentation — Build AI Agents & Run Your Agentic Workforce",
|
||||
"short_name": "Sim Docs",
|
||||
"description": "Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"name": "MyWebSite",
|
||||
"short_name": "MySite",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon/web-app-manifest-192x192.png",
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/web-app-manifest-512x512.png",
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/apple-touch-icon.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#33C482",
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone",
|
||||
"categories": ["productivity", "developer", "business"],
|
||||
"lang": "en-US",
|
||||
"dir": "ltr"
|
||||
"display": "standalone"
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="#0B0B0B"/>
|
||||
<g transform="translate(2.75,2.75) scale(0.0473)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M107.822 93.7612C107.822 97.3481 106.403 100.792 103.884 103.328L103.523 103.692C101.006 106.236 97.5855 107.658 94.0236 107.658H13.4455C6.02456 107.658 0 113.718 0 121.191V208.332C0 215.806 6.02456 221.866 13.4455 221.866H99.9622C107.383 221.866 113.4 215.806 113.4 208.332V126.745C113.4 123.419 114.71 120.228 117.047 117.874C119.377 115.527 122.546 114.207 125.849 114.207H207.777C215.198 114.207 221.214 108.148 221.214 100.674V13.5333C221.214 6.05956 215.198 0 207.777 0H121.26C113.839 0 107.822 6.05956 107.822 13.5333V93.7612ZM134.078 18.55H194.952C199.289 18.55 202.796 22.0893 202.796 26.4503V87.7574C202.796 92.1178 199.289 95.6577 194.952 95.6577H134.078C129.748 95.6577 126.233 92.1178 126.233 87.7574V26.4503C126.233 22.0893 129.748 18.55 134.078 18.55Z" fill="#33C482"/>
|
||||
<path d="M207.878 129.57H143.554C135.756 129.57 129.434 135.937 129.434 143.791V207.784C129.434 215.638 135.756 222.005 143.554 222.005H207.878C215.677 222.005 221.999 215.638 221.999 207.784V143.791C221.999 135.937 215.677 129.57 207.878 129.57Z" fill="#33C482"/>
|
||||
<path d="M207.878 129.266H143.554C135.756 129.266 129.434 135.632 129.434 143.487V207.479C129.434 215.333 135.756 221.699 143.554 221.699H207.878C215.677 221.699 221.999 215.333 221.999 207.479V143.487C221.999 135.632 215.677 129.266 207.878 129.266Z" fill="url(#paint0_linear)" fill-opacity="0.2"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="129.434" y1="129.266" x2="185.629" y2="185.33" gradientUnits="userSpaceOnUse">
|
||||
<stop/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 591 KiB |
@@ -2,22 +2,32 @@
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import AuthBackground from '@/app/(auth)/components/auth-background'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
import Nav from '@/app/(landing)/components/nav/nav'
|
||||
|
||||
function isColorDark(hexColor: string): boolean {
|
||||
const hex = hexColor.replace('#', '')
|
||||
const r = Number.parseInt(hex.substr(0, 2), 16)
|
||||
const g = Number.parseInt(hex.substr(2, 2), 16)
|
||||
const b = Number.parseInt(hex.substr(4, 2), 16)
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
||||
return luminance < 0.5
|
||||
}
|
||||
|
||||
export default function AuthLayoutClient({ children }: { children: React.ReactNode }) {
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.add('dark')
|
||||
return () => {
|
||||
document.documentElement.classList.remove('dark')
|
||||
const rootStyle = getComputedStyle(document.documentElement)
|
||||
const brandBackground = rootStyle.getPropertyValue('--brand-background-hex').trim()
|
||||
|
||||
if (brandBackground && isColorDark(brandBackground)) {
|
||||
document.body.classList.add('auth-dark-bg')
|
||||
} else {
|
||||
document.body.classList.remove('auth-dark-bg')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AuthBackground className='dark font-[430] font-season'>
|
||||
<main className='relative flex min-h-full flex-col text-[#ECECEC]'>
|
||||
<header className='shrink-0 bg-[#1C1C1C]'>
|
||||
<Navbar logoOnly />
|
||||
</header>
|
||||
<AuthBackground>
|
||||
<main className='relative flex min-h-screen flex-col text-foreground'>
|
||||
<Nav hideAuthButtons={true} variant='auth' />
|
||||
<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'>{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,93 +1,44 @@
|
||||
export default function AuthBackgroundSVG() {
|
||||
return (
|
||||
<>
|
||||
{/* Top-left card outline */}
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-[-3vw] left-[-10vw] z-[5] aspect-[344/328] w-[38vw]'
|
||||
>
|
||||
<svg
|
||||
viewBox='0 0 344 328'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
preserveAspectRatio='xMidYMid meet'
|
||||
className='h-full w-full'
|
||||
>
|
||||
<path
|
||||
d='M322.641 326.586L335.508 326.586C339.926 326.586 343.508 323.004 343.508 318.586V153.613C343.508 149.195 339.926 145.613 335.508 145.613H228.282C223.864 145.613 220.282 142.031 220.282 137.613V-50H190.282V137.613C190.282 142.031 186.7 145.613 182.282 145.613H-157V318.586C-157 323.004 -153.418 326.586 -149 326.586H322.641Z'
|
||||
fill='#1C1C1C'
|
||||
stroke='#323232'
|
||||
strokeOpacity='0.4'
|
||||
strokeWidth='1'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none fixed inset-0 h-full w-full'
|
||||
style={{ zIndex: 5 }}
|
||||
viewBox='0 0 1880 960'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
preserveAspectRatio='xMidYMid slice'
|
||||
>
|
||||
{/* Right side paths - extended to connect */}
|
||||
<path
|
||||
d='M1393.53 42.8889C1545.99 173.087 1688.28 339.75 1878.44 817.6'
|
||||
stroke='#E7E4EF'
|
||||
strokeWidth='2'
|
||||
/>
|
||||
<path d='M1624.21 960L1625.78 0' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path d='M1832.67 715.81L1880 716.031' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path d='M1393.4 40V0' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='1393.03' cy='40.0186' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='1625.28' cy='303.147' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='1837.37' cy='715.81' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
|
||||
{/* Top-right card outline */}
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-[-4vw] right-[-14vw] z-[5] aspect-[471/470] w-[42vw]'
|
||||
>
|
||||
<svg
|
||||
viewBox='0 0 471 470'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
preserveAspectRatio='xMidYMid meet'
|
||||
className='h-full w-full'
|
||||
>
|
||||
<path
|
||||
d='M471 94.274L471 124.274L365.88 124.274C361.462 124.274 357.88 127.856 357.88 132.274L357.88 225.495C357.88 229.913 354.298 233.495 349.88 233.495L219.5 233.495C215.082 233.495 211.5 237.077 211.5 241.495L211.5 461.5C211.5 465.918 207.918 469.5 203.5 469.5L8.5 469.5C4.082 469.5 0.5 465.918 0.5 461.5L0.5 157.274C0.5 152.856 4.082 149.274 8.5 149.274L184 149.274C188.418 149.274 192 145.692 192 141.274L192 102.274C192 97.856 195.582 94.274 200 94.274L471 94.274Z'
|
||||
fill='#1C1C1C'
|
||||
stroke='#323232'
|
||||
strokeOpacity='0.4'
|
||||
strokeWidth='1'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Bottom-left card outline (mirrored) */}
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute bottom-[-6vw] left-[-12vw] z-[5] aspect-[471/470] w-[36vw] rotate-180'
|
||||
>
|
||||
<svg
|
||||
viewBox='0 0 471 470'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
preserveAspectRatio='xMidYMid meet'
|
||||
className='h-full w-full'
|
||||
>
|
||||
<path
|
||||
d='M471 94.274L471 124.274L365.88 124.274C361.462 124.274 357.88 127.856 357.88 132.274L357.88 225.495C357.88 229.913 354.298 233.495 349.88 233.495L219.5 233.495C215.082 233.495 211.5 237.077 211.5 241.495L211.5 461.5C211.5 465.918 207.918 469.5 203.5 469.5L8.5 469.5C4.082 469.5 0.5 465.918 0.5 461.5L0.5 157.274C0.5 152.856 4.082 149.274 8.5 149.274L184 149.274C188.418 149.274 192 145.692 192 141.274L192 102.274C192 97.856 195.582 94.274 200 94.274L471 94.274Z'
|
||||
fill='#1C1C1C'
|
||||
stroke='#323232'
|
||||
strokeOpacity='0.4'
|
||||
strokeWidth='1'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Bottom-right card outline (mirrored) */}
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute right-[-12vw] bottom-[-5vw] z-[5] aspect-[344/328] w-[34vw] rotate-180'
|
||||
>
|
||||
<svg
|
||||
viewBox='0 0 344 328'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
preserveAspectRatio='xMidYMid meet'
|
||||
className='h-full w-full'
|
||||
>
|
||||
<path
|
||||
d='M322.641 326.586L335.508 326.586C339.926 326.586 343.508 323.004 343.508 318.586V153.613C343.508 149.195 339.926 145.613 335.508 145.613H228.282C223.864 145.613 220.282 142.031 220.282 137.613V-50H190.282V137.613C190.282 142.031 186.7 145.613 182.282 145.613H-157V318.586C-157 323.004 -153.418 326.586 -149 326.586H322.641Z'
|
||||
fill='#1C1C1C'
|
||||
stroke='#323232'
|
||||
strokeOpacity='0.4'
|
||||
strokeWidth='1'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</>
|
||||
{/* Left side paths - extended to connect */}
|
||||
<path
|
||||
d='M160 157.764C319.811 136.451 417.278 102.619 552.39 0'
|
||||
stroke='#E7E4EF'
|
||||
strokeWidth='2'
|
||||
/>
|
||||
<path d='M310.22 803.025V0' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path
|
||||
d='M160 530.184C256.142 655.353 308.338 749.141 348.382 960'
|
||||
stroke='#E7E4EF'
|
||||
strokeWidth='2'
|
||||
/>
|
||||
<path d='M160 157.764V960' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<path d='M-50 157.764L160 157.764' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='160' cy='157.764' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='310.22' cy='803.025' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
<circle cx='160' cy='530.184' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ type AuthBackgroundProps = {
|
||||
|
||||
export default function AuthBackground({ className, children }: AuthBackgroundProps) {
|
||||
return (
|
||||
<div className={cn('fixed inset-0 overflow-hidden', className)}>
|
||||
<div className='-z-50 pointer-events-none absolute inset-0 bg-[#1C1C1C]' />
|
||||
<div className={cn('relative min-h-screen w-full overflow-hidden', className)}>
|
||||
<div className='-z-50 pointer-events-none fixed inset-0 bg-white' />
|
||||
<AuthBackgroundSVG />
|
||||
<div className='relative z-20 h-full overflow-auto'>{children}</div>
|
||||
<div className='relative z-20'>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,20 +2,36 @@
|
||||
|
||||
import { forwardRef, useState } from 'react'
|
||||
import { ArrowRight, ChevronRight, Loader2 } from 'lucide-react'
|
||||
import { Button, type ButtonProps as EmcnButtonProps } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { useBrandConfig } from '@/ee/whitelabeling'
|
||||
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||
|
||||
export interface BrandedButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
export interface BrandedButtonProps extends Omit<EmcnButtonProps, 'variant' | 'size'> {
|
||||
/** Shows loading spinner and disables button */
|
||||
loading?: boolean
|
||||
/** Text to show when loading (appends "..." automatically) */
|
||||
loadingText?: string
|
||||
/** Show arrow animation on hover (default: true) */
|
||||
showArrow?: boolean
|
||||
/** Make button full width (default: true) */
|
||||
fullWidth?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Branded button for auth and status pages.
|
||||
* Default: white button matching the landing page "Get started" style.
|
||||
* Whitelabel: uses the brand's primary color as background with white text.
|
||||
* Automatically detects whitelabel customization and applies appropriate styling.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Primary branded button with arrow
|
||||
* <BrandedButton onClick={handleSubmit}>Sign In</BrandedButton>
|
||||
*
|
||||
* // Loading state
|
||||
* <BrandedButton loading loadingText="Signing in">Sign In</BrandedButton>
|
||||
*
|
||||
* // Without arrow animation
|
||||
* <BrandedButton showArrow={false}>Continue</BrandedButton>
|
||||
* ```
|
||||
*/
|
||||
export const BrandedButton = forwardRef<HTMLButtonElement, BrandedButtonProps>(
|
||||
(
|
||||
@@ -33,8 +49,7 @@ export const BrandedButton = forwardRef<HTMLButtonElement, BrandedButtonProps>(
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const brand = useBrandConfig()
|
||||
const hasCustomColor = brand.isWhitelabeled && Boolean(brand.theme?.primaryColor)
|
||||
const buttonClass = useBrandedButtonClass()
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
@@ -48,32 +63,15 @@ export const BrandedButton = forwardRef<HTMLButtonElement, BrandedButtonProps>(
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
ref={ref}
|
||||
{...props}
|
||||
variant='branded'
|
||||
size='branded'
|
||||
disabled={disabled || loading}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
className={cn(
|
||||
'group inline-flex h-[32px] items-center justify-center gap-[8px] rounded-[5px] border px-[10px] font-[430] font-season text-[14px] transition-colors disabled:cursor-not-allowed disabled:opacity-50',
|
||||
!hasCustomColor &&
|
||||
'border-[#FFFFFF] bg-[#FFFFFF] text-black hover:border-[#E0E0E0] hover:bg-[#E0E0E0]',
|
||||
fullWidth && 'w-full',
|
||||
className
|
||||
)}
|
||||
style={
|
||||
hasCustomColor
|
||||
? {
|
||||
backgroundColor: isHovered
|
||||
? (brand.theme?.primaryHoverColor ?? brand.theme?.primaryColor)
|
||||
: brand.theme?.primaryColor,
|
||||
borderColor: isHovered
|
||||
? (brand.theme?.primaryHoverColor ?? brand.theme?.primaryColor)
|
||||
: brand.theme?.primaryColor,
|
||||
color: '#FFFFFF',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
className={cn(buttonClass, 'group', fullWidth && 'w-full', className)}
|
||||
{...props}
|
||||
>
|
||||
{loading ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
@@ -94,7 +92,7 @@ export const BrandedButton = forwardRef<HTMLButtonElement, BrandedButtonProps>(
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { type ReactNode, useEffect, useState } from 'react'
|
||||
import { Button } from '@/components/emcn'
|
||||
import { GithubIcon, GoogleIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { client } from '@/lib/auth/auth-client'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
|
||||
interface SocialLoginButtonsProps {
|
||||
githubAvailable: boolean
|
||||
@@ -81,7 +82,7 @@ export function SocialLoginButtons({
|
||||
const githubButton = (
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full rounded-[10px]'
|
||||
className='w-full rounded-[10px] shadow-sm hover:bg-gray-50'
|
||||
disabled={!githubAvailable || isGithubLoading}
|
||||
onClick={signInWithGithub}
|
||||
>
|
||||
@@ -93,7 +94,7 @@ export function SocialLoginButtons({
|
||||
const googleButton = (
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full rounded-[10px]'
|
||||
className='w-full rounded-[10px] shadow-sm hover:bg-gray-50'
|
||||
disabled={!googleAvailable || isGoogleLoading}
|
||||
onClick={signInWithGoogle}
|
||||
>
|
||||
@@ -109,7 +110,7 @@ export function SocialLoginButtons({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='grid gap-3 font-light'>
|
||||
<div className={`${inter.className} grid gap-3 font-light`}>
|
||||
{googleAvailable && googleButton}
|
||||
{githubAvailable && githubButton}
|
||||
{children}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/emcn'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
@@ -38,7 +38,7 @@ export function SSOLoginButton({
|
||||
'flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200'
|
||||
)
|
||||
|
||||
const outlineBtnClasses = cn('w-full rounded-[10px]')
|
||||
const outlineBtnClasses = cn('w-full rounded-[10px] shadow-sm hover:bg-gray-50')
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -1,41 +1,69 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import AuthBackground from '@/app/(auth)/components/auth-background'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
import Nav from '@/app/(landing)/components/nav/nav'
|
||||
import { SupportFooter } from './support-footer'
|
||||
|
||||
export interface StatusPageLayoutProps {
|
||||
/** Page title displayed prominently */
|
||||
title: string
|
||||
/** Description text below the title */
|
||||
description: string | ReactNode
|
||||
/** Content to render below the title/description (usually buttons) */
|
||||
children?: ReactNode
|
||||
/** Whether to show the support footer (default: true) */
|
||||
showSupportFooter?: boolean
|
||||
/** Whether to hide the nav bar (useful for embedded forms) */
|
||||
hideNav?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified layout for status/error pages (404, form unavailable, chat error, etc.).
|
||||
* Uses AuthBackground and Nav for consistent styling with auth pages.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <StatusPageLayout
|
||||
* title="Page Not Found"
|
||||
* description="The page you're looking for doesn't exist."
|
||||
* >
|
||||
* <BrandedButton onClick={() => router.push('/')}>Return to Home</BrandedButton>
|
||||
* </StatusPageLayout>
|
||||
* ```
|
||||
*/
|
||||
export function StatusPageLayout({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
showSupportFooter = true,
|
||||
hideNav = false,
|
||||
}: StatusPageLayoutProps) {
|
||||
return (
|
||||
<AuthBackground className='dark font-[430] font-season'>
|
||||
<main className='relative flex min-h-full flex-col text-[#ECECEC]'>
|
||||
<header className='shrink-0 bg-[#1C1C1C]'>
|
||||
<Navbar logoOnly />
|
||||
</header>
|
||||
<AuthBackground>
|
||||
<main className='relative flex min-h-screen flex-col text-foreground'>
|
||||
{!hideNav && <Nav hideAuthButtons={true} variant='auth' />}
|
||||
<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'>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1
|
||||
className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
{children && <div className='mt-8 w-full max-w-[410px] space-y-3'>{children}</div>}
|
||||
|
||||
{children && (
|
||||
<div className={`${inter.className} mt-8 w-full max-w-[410px] space-y-3`}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
'use client'
|
||||
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import { useBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
export interface SupportFooterProps {
|
||||
/** Position style - 'fixed' for pages without AuthLayout, 'absolute' for pages with AuthLayout */
|
||||
position?: 'fixed' | 'absolute'
|
||||
}
|
||||
|
||||
/**
|
||||
* Support footer component for auth and status pages.
|
||||
* Displays a "Need help? Contact support" link using branded support email.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Fixed position (for standalone pages)
|
||||
* <SupportFooter />
|
||||
*
|
||||
* // Absolute position (for pages using AuthLayout)
|
||||
* <SupportFooter position="absolute" />
|
||||
* ```
|
||||
*/
|
||||
export function SupportFooter({ position = 'fixed' }: SupportFooterProps) {
|
||||
const brandConfig = useBrandConfig()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`right-0 bottom-0 left-0 z-50 pb-8 text-center font-[340] text-[#999] text-[13px] leading-relaxed ${position}`}
|
||||
className={`${inter.className} auth-text-muted right-0 bottom-0 left-0 z-50 pb-8 text-center font-[340] text-[13px] leading-relaxed ${position}`}
|
||||
>
|
||||
Need help?{' '}
|
||||
<a
|
||||
href={`mailto:${brandConfig.supportEmail}`}
|
||||
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
|
||||
className='auth-link underline-offset-4 transition hover:underline'
|
||||
>
|
||||
Contact support
|
||||
</a>
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalDescription,
|
||||
ModalHeader,
|
||||
} from '@/components/emcn'
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { client } from '@/lib/auth/auth-client'
|
||||
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
|
||||
import { validateCallbackUrl } from '@/lib/core/security/input-validation'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
|
||||
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
|
||||
@@ -54,6 +55,24 @@ const PASSWORD_VALIDATIONS = {
|
||||
},
|
||||
}
|
||||
|
||||
const validateCallbackUrl = (url: string): boolean => {
|
||||
try {
|
||||
if (url.startsWith('/')) {
|
||||
return true
|
||||
}
|
||||
|
||||
const currentOrigin = typeof window !== 'undefined' ? window.location.origin : ''
|
||||
if (url.startsWith(currentOrigin)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
logger.error('Error validating callback URL:', { error, url })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const validatePassword = (passwordValue: string): string[] => {
|
||||
const errors: string[] = []
|
||||
|
||||
@@ -82,21 +101,15 @@ export default function LoginPage({
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [_mounted, setMounted] = useState(false)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [password, setPassword] = useState('')
|
||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
||||
const [showValidationError, setShowValidationError] = useState(false)
|
||||
const buttonClass = useBrandedButtonClass()
|
||||
|
||||
const callbackUrlParam = searchParams?.get('callbackUrl')
|
||||
const isValidCallbackUrl = callbackUrlParam ? validateCallbackUrl(callbackUrlParam) : false
|
||||
const invalidCallbackRef = useRef(false)
|
||||
if (callbackUrlParam && !isValidCallbackUrl && !invalidCallbackRef.current) {
|
||||
invalidCallbackRef.current = true
|
||||
logger.warn('Invalid callback URL detected and blocked:', { url: callbackUrlParam })
|
||||
}
|
||||
const callbackUrl = isValidCallbackUrl ? callbackUrlParam! : '/workspace'
|
||||
const isInviteFlow = searchParams?.get('invite_flow') === 'true'
|
||||
const [callbackUrl, setCallbackUrl] = useState('/workspace')
|
||||
const [isInviteFlow, setIsInviteFlow] = useState(false)
|
||||
|
||||
const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false)
|
||||
const [forgotPasswordEmail, setForgotPasswordEmail] = useState('')
|
||||
@@ -109,11 +122,30 @@ export default function LoginPage({
|
||||
const [email, setEmail] = useState('')
|
||||
const [emailErrors, setEmailErrors] = useState<string[]>([])
|
||||
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
||||
const [resetSuccessMessage, setResetSuccessMessage] = useState<string | null>(() =>
|
||||
searchParams?.get('resetSuccess') === 'true'
|
||||
? 'Password reset successful. Please sign in with your new password.'
|
||||
: null
|
||||
)
|
||||
const [resetSuccessMessage, setResetSuccessMessage] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
|
||||
if (searchParams) {
|
||||
const callback = searchParams.get('callbackUrl')
|
||||
if (callback) {
|
||||
if (validateCallbackUrl(callback)) {
|
||||
setCallbackUrl(callback)
|
||||
} else {
|
||||
logger.warn('Invalid callback URL detected and blocked:', { url: callback })
|
||||
}
|
||||
}
|
||||
|
||||
const inviteFlow = searchParams.get('invite_flow') === 'true'
|
||||
setIsInviteFlow(inviteFlow)
|
||||
|
||||
const resetSuccess = searchParams.get('resetSuccess') === 'true'
|
||||
if (resetSuccess) {
|
||||
setResetSuccessMessage('Password reset successful. Please sign in with your new password.')
|
||||
}
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
@@ -175,7 +207,7 @@ export default function LoginPage({
|
||||
}
|
||||
|
||||
try {
|
||||
const safeCallbackUrl = callbackUrl
|
||||
const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace'
|
||||
let errorHandled = false
|
||||
|
||||
const result = await client.signIn.email(
|
||||
@@ -353,17 +385,17 @@ export default function LoginPage({
|
||||
return (
|
||||
<>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
|
||||
Sign in
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
Enter your details
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* SSO Login Button (primary top-only when it is the only method) */}
|
||||
{showTopSSO && (
|
||||
<div className='mt-8'>
|
||||
<div className={`${inter.className} mt-8`}>
|
||||
<SSOLoginButton
|
||||
callbackURL={callbackUrl}
|
||||
variant='primary'
|
||||
@@ -374,14 +406,14 @@ export default function LoginPage({
|
||||
|
||||
{/* Password reset success message */}
|
||||
{resetSuccessMessage && (
|
||||
<div className='mt-1 space-y-1 text-[#4CAF50] text-xs'>
|
||||
<div className={`${inter.className} mt-1 space-y-1 text-[#4CAF50] text-xs`}>
|
||||
<p>{resetSuccessMessage}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email/Password Form - show unless explicitly disabled */}
|
||||
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
|
||||
<form onSubmit={onSubmit} className='mt-8 space-y-8'>
|
||||
<form onSubmit={onSubmit} className={`${inter.className} mt-8 space-y-8`}>
|
||||
<div className='space-y-6'>
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
@@ -398,9 +430,10 @@ export default function LoginPage({
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
className={cn(
|
||||
'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
|
||||
showEmailValidationError &&
|
||||
emailErrors.length > 0 &&
|
||||
'border-red-500 focus:border-red-500'
|
||||
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
|
||||
)}
|
||||
/>
|
||||
{showEmailValidationError && emailErrors.length > 0 && (
|
||||
@@ -417,7 +450,7 @@ export default function LoginPage({
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setForgotPasswordOpen(true)}
|
||||
className='font-medium text-[#999] text-xs transition hover:text-[#ECECEC]'
|
||||
className='font-medium text-muted-foreground text-xs transition hover:text-foreground'
|
||||
>
|
||||
Forgot password?
|
||||
</button>
|
||||
@@ -435,16 +468,16 @@ export default function LoginPage({
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
className={cn(
|
||||
'pr-10',
|
||||
'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
|
||||
showValidationError &&
|
||||
passwordErrors.length > 0 &&
|
||||
'border-red-500 focus:border-red-500'
|
||||
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-gray-500 transition hover:text-gray-700'
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
@@ -473,18 +506,18 @@ export default function LoginPage({
|
||||
|
||||
{/* Divider - show when we have multiple auth methods */}
|
||||
{showDivider && (
|
||||
<div className='relative my-6 font-light'>
|
||||
<div className={`${inter.className} relative my-6 font-light`}>
|
||||
<div className='absolute inset-0 flex items-center'>
|
||||
<div className='w-full border-[#2A2A2A] border-t' />
|
||||
<div className='auth-divider w-full border-t' />
|
||||
</div>
|
||||
<div className='relative flex justify-center text-sm'>
|
||||
<span className='bg-[#1C1C1C] px-4 font-[340] text-[#999]'>Or continue with</span>
|
||||
<span className='bg-white px-4 font-[340] text-muted-foreground'>Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showBottomSection && (
|
||||
<div className={cn(!emailEnabled ? 'mt-8' : undefined)}>
|
||||
<div className={cn(inter.className, !emailEnabled ? 'mt-8' : undefined)}>
|
||||
<SocialLoginButtons
|
||||
googleAvailable={googleAvailable}
|
||||
githubAvailable={githubAvailable}
|
||||
@@ -504,24 +537,26 @@ export default function LoginPage({
|
||||
|
||||
{/* Only show signup link if email/password signup is enabled */}
|
||||
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
|
||||
<div className='pt-6 text-center font-light text-[14px]'>
|
||||
<div className={`${inter.className} pt-6 text-center font-light text-[14px]`}>
|
||||
<span className='font-normal'>Don't have an account? </span>
|
||||
<Link
|
||||
href={isInviteFlow ? `/signup?invite_flow=true&callbackUrl=${callbackUrl}` : '/signup'}
|
||||
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
|
||||
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[#999] text-[13px] leading-relaxed sm:px-8 md:px-[44px]'>
|
||||
<div
|
||||
className={`${inter.className} auth-text-muted absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[13px] leading-relaxed sm:px-8 md:px-[44px]`}
|
||||
>
|
||||
By signing in, you agree to our{' '}
|
||||
<Link
|
||||
href='/terms'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
|
||||
className='auth-link underline-offset-4 transition hover:underline'
|
||||
>
|
||||
Terms of Service
|
||||
</Link>{' '}
|
||||
@@ -530,58 +565,64 @@ export default function LoginPage({
|
||||
href='/privacy'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
|
||||
className='auth-link underline-offset-4 transition hover:underline'
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Modal open={forgotPasswordOpen} onOpenChange={setForgotPasswordOpen}>
|
||||
<ModalContent className='dark' size='sm'>
|
||||
<ModalHeader>Reset Password</ModalHeader>
|
||||
<ModalBody>
|
||||
<ModalDescription className='mb-4 text-[var(--text-muted)] text-sm'>
|
||||
<Dialog open={forgotPasswordOpen} onOpenChange={setForgotPasswordOpen}>
|
||||
<DialogContent className='auth-card auth-card-shadow max-w-[540px] rounded-[10px] border backdrop-blur-sm'>
|
||||
<DialogHeader>
|
||||
<DialogTitle className='font-semibold text-black text-xl tracking-tight'>
|
||||
Reset Password
|
||||
</DialogTitle>
|
||||
<DialogDescription className='text-muted-foreground 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'>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className='space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
{resetStatus.type === 'success' && (
|
||||
<div className='mt-1 text-[#4CAF50] text-xs'>
|
||||
<Input
|
||||
id='reset-email'
|
||||
value={forgotPasswordEmail}
|
||||
onChange={(e) => setForgotPasswordEmail(e.target.value)}
|
||||
placeholder='Enter your email'
|
||||
required
|
||||
type='email'
|
||||
className={cn(
|
||||
'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
|
||||
resetStatus.type === 'error' &&
|
||||
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
|
||||
)}
|
||||
/>
|
||||
{resetStatus.type === 'error' && (
|
||||
<div className='mt-1 space-y-1 text-red-400 text-xs'>
|
||||
<p>{resetStatus.message}</p>
|
||||
</div>
|
||||
)}
|
||||
<BrandedButton
|
||||
type='button'
|
||||
onClick={handleForgotPassword}
|
||||
disabled={isSubmittingReset}
|
||||
loading={isSubmittingReset}
|
||||
loadingText='Sending'
|
||||
>
|
||||
Send Reset Link
|
||||
</BrandedButton>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{resetStatus.type === 'success' && (
|
||||
<div className='mt-1 space-y-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>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import Image from 'next/image'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Button } from '@/components/emcn'
|
||||
import { signOut, useSession } from '@/lib/auth/auth-client'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
|
||||
const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
@@ -127,10 +129,10 @@ export default function OAuthConsentPage() {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
|
||||
Authorize Application
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
Loading application details...
|
||||
</p>
|
||||
</div>
|
||||
@@ -142,14 +144,14 @@ export default function OAuthConsentPage() {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
|
||||
Authorization Error
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
<div className='mt-8 w-full max-w-[410px] space-y-3'>
|
||||
<div className={`${inter.className} mt-8 w-full max-w-[410px] space-y-3`}>
|
||||
<BrandedButton onClick={() => router.push('/')}>Return to Home</BrandedButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,11 +172,11 @@ export default function OAuthConsentPage() {
|
||||
className='rounded-[10px]'
|
||||
/>
|
||||
) : (
|
||||
<div className='flex h-12 w-12 items-center justify-center rounded-[10px] bg-[#2A2A2A] font-medium text-[#999] text-[18px]'>
|
||||
<div className='flex h-12 w-12 items-center justify-center rounded-[10px] bg-muted font-medium text-[18px] text-muted-foreground'>
|
||||
{(clientName ?? '?').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<ArrowLeftRight className='h-5 w-5 text-[#999]' />
|
||||
<ArrowLeftRight className='h-5 w-5 text-muted-foreground' />
|
||||
<Image
|
||||
src='/new/logo/colorized-bg.svg'
|
||||
alt='Sim'
|
||||
@@ -185,17 +187,19 @@ export default function OAuthConsentPage() {
|
||||
</div>
|
||||
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
|
||||
Authorize Application
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<span className='font-medium text-[#ECECEC]'>{clientName}</span> is requesting access to
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
<span className='font-medium text-foreground'>{clientName}</span> is requesting access to
|
||||
your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{session?.user && (
|
||||
<div className='mt-5 flex items-center gap-3 rounded-lg border border-[#2A2A2A] px-4 py-3'>
|
||||
<div
|
||||
className={`${inter.className} mt-5 flex items-center gap-3 rounded-lg border px-4 py-3`}
|
||||
>
|
||||
{session.user.image ? (
|
||||
<Image
|
||||
src={session.user.image}
|
||||
@@ -206,7 +210,7 @@ export default function OAuthConsentPage() {
|
||||
unoptimized
|
||||
/>
|
||||
) : (
|
||||
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-[#2A2A2A] font-medium text-[#999] text-[13px]'>
|
||||
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-muted font-medium text-[13px] text-muted-foreground'>
|
||||
{(session.user.name ?? session.user.email ?? '?').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
@@ -214,12 +218,12 @@ export default function OAuthConsentPage() {
|
||||
{session.user.name && (
|
||||
<p className='truncate font-medium text-[14px]'>{session.user.name}</p>
|
||||
)}
|
||||
<p className='truncate text-[#999] text-[13px]'>{session.user.email}</p>
|
||||
<p className='truncate text-[13px] text-muted-foreground'>{session.user.email}</p>
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleSwitchAccount}
|
||||
className='ml-auto text-[#999] text-[13px] underline-offset-2 transition-colors hover:text-[#ECECEC] hover:underline'
|
||||
className='ml-auto text-[13px] text-muted-foreground underline-offset-2 transition-colors hover:text-foreground hover:underline'
|
||||
>
|
||||
Switch
|
||||
</button>
|
||||
@@ -227,12 +231,15 @@ export default function OAuthConsentPage() {
|
||||
)}
|
||||
|
||||
{scopes.length > 0 && (
|
||||
<div className='mt-5 w-full max-w-[410px]'>
|
||||
<div className={`${inter.className} mt-5 w-full max-w-[410px]`}>
|
||||
<div className='rounded-lg border p-4'>
|
||||
<p className='mb-3 font-medium text-[14px]'>This will allow the application to:</p>
|
||||
<ul className='space-y-2'>
|
||||
{scopes.map((s) => (
|
||||
<li key={s} className='flex items-start gap-2 font-normal text-[#999] text-[13px]'>
|
||||
<li
|
||||
key={s}
|
||||
className='flex items-start gap-2 font-normal text-[13px] text-muted-foreground'
|
||||
>
|
||||
<span className='mt-0.5 text-green-500'>✓</span>
|
||||
<span>{SCOPE_DESCRIPTIONS[s] ?? s}</span>
|
||||
</li>
|
||||
@@ -242,7 +249,7 @@ export default function OAuthConsentPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='mt-6 flex w-full max-w-[410px] gap-3'>
|
||||
<div className={`${inter.className} mt-6 flex w-full max-w-[410px] gap-3`}>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='md'
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { Suspense, useState } from 'react'
|
||||
import { Suspense, useEffect, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import { SetNewPasswordForm } from '@/app/(auth)/reset-password/reset-password-form'
|
||||
|
||||
const logger = createLogger('ResetPasswordPage')
|
||||
@@ -22,9 +24,14 @@ function ResetPasswordContent() {
|
||||
text: '',
|
||||
})
|
||||
|
||||
const tokenError = !token
|
||||
? 'Invalid or missing reset token. Please request a new password reset link.'
|
||||
: null
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
setStatusMessage({
|
||||
type: 'error',
|
||||
text: 'Invalid or missing reset token. Please request a new password reset link.',
|
||||
})
|
||||
}
|
||||
}, [token])
|
||||
|
||||
const handleResetPassword = async (password: string) => {
|
||||
try {
|
||||
@@ -69,28 +76,28 @@ function ResetPasswordContent() {
|
||||
return (
|
||||
<>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
|
||||
Reset your password
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
Enter a new password for your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='mt-8'>
|
||||
<div className={`${inter.className} mt-8`}>
|
||||
<SetNewPasswordForm
|
||||
token={token}
|
||||
onSubmit={handleResetPassword}
|
||||
isSubmitting={isSubmitting}
|
||||
statusType={tokenError ? 'error' : statusMessage.type}
|
||||
statusMessage={tokenError ?? statusMessage.text}
|
||||
statusType={statusMessage.type}
|
||||
statusMessage={statusMessage.text}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='pt-6 text-center font-light text-[14px]'>
|
||||
<div className={`${inter.className} pt-6 text-center font-light text-[14px]`}>
|
||||
<Link
|
||||
href='/login'
|
||||
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
|
||||
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
|
||||
>
|
||||
Back to login
|
||||
</Link>
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { Input, Label } from '@/components/emcn'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
|
||||
interface RequestResetFormProps {
|
||||
@@ -31,7 +33,7 @@ export function RequestResetForm({
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={cn('space-y-8', className)}>
|
||||
<form onSubmit={handleSubmit} className={cn(`${inter.className} space-y-8`, className)}>
|
||||
<div className='space-y-6'>
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
@@ -45,8 +47,9 @@ export function RequestResetForm({
|
||||
type='email'
|
||||
disabled={isSubmitting}
|
||||
required
|
||||
className='rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100'
|
||||
/>
|
||||
<p className='text-[#999] text-sm'>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
We'll send a password reset link to this email address.
|
||||
</p>
|
||||
</div>
|
||||
@@ -139,7 +142,7 @@ export function SetNewPasswordForm({
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={cn('space-y-8', className)}>
|
||||
<form onSubmit={handleSubmit} className={cn(`${inter.className} space-y-8`, className)}>
|
||||
<div className='space-y-6'>
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
@@ -157,12 +160,16 @@ export function SetNewPasswordForm({
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
placeholder='Enter new password'
|
||||
className={cn('pr-10', validationMessage && 'border-red-500 focus:border-red-500')}
|
||||
className={cn(
|
||||
'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
|
||||
validationMessage &&
|
||||
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-gray-500 transition hover:text-gray-700'
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
@@ -185,12 +192,16 @@ export function SetNewPasswordForm({
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
placeholder='Confirm new password'
|
||||
className={cn('pr-10', validationMessage && 'border-red-500 focus:border-red-500')}
|
||||
className={cn(
|
||||
'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
|
||||
validationMessage &&
|
||||
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-gray-500 transition hover:text-gray-700'
|
||||
aria-label={showConfirmPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{showConfirmPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { Suspense, useMemo, useState } from 'react'
|
||||
import { Suspense, useEffect, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Input, Label } from '@/components/emcn'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { client, useSession } from '@/lib/auth/auth-client'
|
||||
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
|
||||
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
|
||||
@@ -82,32 +85,49 @@ function SignupFormContent({
|
||||
const searchParams = useSearchParams()
|
||||
const { refetch: refetchSession } = useSession()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [, setMounted] = useState(false)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [password, setPassword] = useState('')
|
||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
||||
const [showValidationError, setShowValidationError] = useState(false)
|
||||
const [email, setEmail] = useState(() => searchParams.get('email') ?? '')
|
||||
const [email, setEmail] = useState('')
|
||||
const [emailError, setEmailError] = useState('')
|
||||
const [emailErrors, setEmailErrors] = useState<string[]>([])
|
||||
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
||||
const [redirectUrl, setRedirectUrl] = useState('')
|
||||
const [isInviteFlow, setIsInviteFlow] = useState(false)
|
||||
const buttonClass = useBrandedButtonClass()
|
||||
|
||||
const redirectUrl = useMemo(
|
||||
() => searchParams.get('redirect') || searchParams.get('callbackUrl') || '',
|
||||
[searchParams]
|
||||
)
|
||||
const isInviteFlow = useMemo(
|
||||
() =>
|
||||
searchParams.get('invite_flow') === 'true' ||
|
||||
redirectUrl.startsWith('/invite/') ||
|
||||
redirectUrl.startsWith('/credential-account/'),
|
||||
[searchParams, redirectUrl]
|
||||
)
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [nameErrors, setNameErrors] = useState<string[]>([])
|
||||
const [showNameValidationError, setShowNameValidationError] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
const emailParam = searchParams.get('email')
|
||||
if (emailParam) {
|
||||
setEmail(emailParam)
|
||||
}
|
||||
|
||||
// Check both 'redirect' and 'callbackUrl' params (login page uses callbackUrl)
|
||||
const redirectParam = searchParams.get('redirect') || searchParams.get('callbackUrl')
|
||||
if (redirectParam) {
|
||||
setRedirectUrl(redirectParam)
|
||||
|
||||
if (
|
||||
redirectParam.startsWith('/invite/') ||
|
||||
redirectParam.startsWith('/credential-account/')
|
||||
) {
|
||||
setIsInviteFlow(true)
|
||||
}
|
||||
}
|
||||
|
||||
const inviteFlowParam = searchParams.get('invite_flow')
|
||||
if (inviteFlowParam === 'true') {
|
||||
setIsInviteFlow(true)
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
const validatePassword = (passwordValue: string): string[] => {
|
||||
const errors: string[] = []
|
||||
|
||||
@@ -324,10 +344,10 @@ function SignupFormContent({
|
||||
return (
|
||||
<>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
|
||||
Create an account
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
Create an account or log in
|
||||
</p>
|
||||
</div>
|
||||
@@ -340,7 +360,7 @@ function SignupFormContent({
|
||||
const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial
|
||||
return hasOnlySSO
|
||||
})() && (
|
||||
<div className='mt-8'>
|
||||
<div className={`${inter.className} mt-8`}>
|
||||
<SSOLoginButton
|
||||
callbackURL={redirectUrl || '/workspace'}
|
||||
variant='primary'
|
||||
@@ -351,7 +371,7 @@ function SignupFormContent({
|
||||
|
||||
{/* Email/Password Form - show unless explicitly disabled */}
|
||||
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
|
||||
<form onSubmit={onSubmit} className='mt-8 space-y-8'>
|
||||
<form onSubmit={onSubmit} className={`${inter.className} mt-8 space-y-8`}>
|
||||
<div className='space-y-6'>
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
@@ -368,9 +388,10 @@ function SignupFormContent({
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
className={cn(
|
||||
'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
|
||||
showNameValidationError &&
|
||||
nameErrors.length > 0 &&
|
||||
'border-red-500 focus:border-red-500'
|
||||
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
|
||||
)}
|
||||
/>
|
||||
{showNameValidationError && nameErrors.length > 0 && (
|
||||
@@ -395,8 +416,9 @@ function SignupFormContent({
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
className={cn(
|
||||
'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
|
||||
(emailError || (showEmailValidationError && emailErrors.length > 0)) &&
|
||||
'border-red-500 focus:border-red-500'
|
||||
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
|
||||
)}
|
||||
/>
|
||||
{showEmailValidationError && emailErrors.length > 0 && (
|
||||
@@ -428,16 +450,16 @@ function SignupFormContent({
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
className={cn(
|
||||
'pr-10',
|
||||
'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
|
||||
showValidationError &&
|
||||
passwordErrors.length > 0 &&
|
||||
'border-red-500 focus:border-red-500'
|
||||
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-gray-500 transition hover:text-gray-700'
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
@@ -474,12 +496,12 @@ function SignupFormContent({
|
||||
const showDivider = (emailEnabled || hasOnlySSO) && showBottomSection
|
||||
return showDivider
|
||||
})() && (
|
||||
<div className='relative my-6 font-light'>
|
||||
<div className={`${inter.className} relative my-6 font-light`}>
|
||||
<div className='absolute inset-0 flex items-center'>
|
||||
<div className='w-full border-[#2A2A2A] border-t' />
|
||||
<div className='auth-divider w-full border-t' />
|
||||
</div>
|
||||
<div className='relative flex justify-center text-sm'>
|
||||
<span className='bg-[#1C1C1C] px-4 font-[340] text-[#999]'>Or continue with</span>
|
||||
<span className='bg-white px-4 font-[340] text-muted-foreground'>Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -494,6 +516,7 @@ function SignupFormContent({
|
||||
})() && (
|
||||
<div
|
||||
className={cn(
|
||||
inter.className,
|
||||
isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) ? 'mt-8' : undefined
|
||||
)}
|
||||
>
|
||||
@@ -514,23 +537,25 @@ function SignupFormContent({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='pt-6 text-center font-light text-[14px]'>
|
||||
<div className={`${inter.className} pt-6 text-center font-light text-[14px]`}>
|
||||
<span className='font-normal'>Already have an account? </span>
|
||||
<Link
|
||||
href={isInviteFlow ? `/login?invite_flow=true&callbackUrl=${redirectUrl}` : '/login'}
|
||||
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
|
||||
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className='absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[#999] text-[13px] leading-relaxed sm:px-8 md:px-[44px]'>
|
||||
<div
|
||||
className={`${inter.className} auth-text-muted absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[13px] leading-relaxed sm:px-8 md:px-[44px]`}
|
||||
>
|
||||
By creating an account, you agree to our{' '}
|
||||
<Link
|
||||
href='/terms'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
|
||||
className='auth-link underline-offset-4 transition hover:underline'
|
||||
>
|
||||
Terms of Service
|
||||
</Link>{' '}
|
||||
@@ -539,7 +564,7 @@ function SignupFormContent({
|
||||
href='/privacy'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
|
||||
className='auth-link underline-offset-4 transition hover:underline'
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
import { Suspense, useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/emcn'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import { useVerification } from '@/app/(auth)/verify/use-verification'
|
||||
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||
|
||||
interface VerifyContentProps {
|
||||
hasEmailService: boolean
|
||||
@@ -56,13 +59,15 @@ function VerificationForm({
|
||||
setCountdown(30)
|
||||
}
|
||||
|
||||
const buttonClass = useBrandedButtonClass()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
|
||||
{isVerified ? 'Email Verified!' : 'Verify Your Email'}
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
{isVerified
|
||||
? 'Your email has been verified. Redirecting to dashboard...'
|
||||
: !isEmailVerificationEnabled
|
||||
@@ -76,9 +81,9 @@ function VerificationForm({
|
||||
</div>
|
||||
|
||||
{!isVerified && isEmailVerificationEnabled && (
|
||||
<div className='mt-8 space-y-8'>
|
||||
<div className={`${inter.className} mt-8 space-y-8`}>
|
||||
<div className='space-y-6'>
|
||||
<p className='text-center text-[#999] text-sm'>
|
||||
<p className='text-center text-muted-foreground text-sm'>
|
||||
Enter the 6-digit code to verify your account.
|
||||
{hasEmailService ? " If you don't see it in your inbox, check your spam folder." : ''}
|
||||
</p>
|
||||
@@ -91,13 +96,61 @@ function VerificationForm({
|
||||
disabled={isLoading}
|
||||
className={cn('gap-2', isInvalidOtp && 'otp-error')}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} className={cn(isInvalidOtp && 'border-red-500')} />
|
||||
<InputOTPSlot index={1} className={cn(isInvalidOtp && 'border-red-500')} />
|
||||
<InputOTPSlot index={2} className={cn(isInvalidOtp && 'border-red-500')} />
|
||||
<InputOTPSlot index={3} className={cn(isInvalidOtp && 'border-red-500')} />
|
||||
<InputOTPSlot index={4} className={cn(isInvalidOtp && 'border-red-500')} />
|
||||
<InputOTPSlot index={5} className={cn(isInvalidOtp && 'border-red-500')} />
|
||||
<InputOTPGroup className='[&>div]:!rounded-[10px] gap-2'>
|
||||
<InputOTPSlot
|
||||
index={0}
|
||||
className={cn(
|
||||
'!rounded-[10px] h-12 w-12 border bg-white text-center font-medium text-lg shadow-sm transition-all duration-200',
|
||||
'border-gray-300 hover:border-gray-400',
|
||||
'focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-100',
|
||||
isInvalidOtp && 'border-red-500 focus:border-red-500 focus:ring-red-100'
|
||||
)}
|
||||
/>
|
||||
<InputOTPSlot
|
||||
index={1}
|
||||
className={cn(
|
||||
'!rounded-[10px] h-12 w-12 border bg-white text-center font-medium text-lg shadow-sm transition-all duration-200',
|
||||
'border-gray-300 hover:border-gray-400',
|
||||
'focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-100',
|
||||
isInvalidOtp && 'border-red-500 focus:border-red-500 focus:ring-red-100'
|
||||
)}
|
||||
/>
|
||||
<InputOTPSlot
|
||||
index={2}
|
||||
className={cn(
|
||||
'!rounded-[10px] h-12 w-12 border bg-white text-center font-medium text-lg shadow-sm transition-all duration-200',
|
||||
'border-gray-300 hover:border-gray-400',
|
||||
'focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-100',
|
||||
isInvalidOtp && 'border-red-500 focus:border-red-500 focus:ring-red-100'
|
||||
)}
|
||||
/>
|
||||
<InputOTPSlot
|
||||
index={3}
|
||||
className={cn(
|
||||
'!rounded-[10px] h-12 w-12 border bg-white text-center font-medium text-lg shadow-sm transition-all duration-200',
|
||||
'border-gray-300 hover:border-gray-400',
|
||||
'focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-100',
|
||||
isInvalidOtp && 'border-red-500 focus:border-red-500 focus:ring-red-100'
|
||||
)}
|
||||
/>
|
||||
<InputOTPSlot
|
||||
index={4}
|
||||
className={cn(
|
||||
'!rounded-[10px] h-12 w-12 border bg-white text-center font-medium text-lg shadow-sm transition-all duration-200',
|
||||
'border-gray-300 hover:border-gray-400',
|
||||
'focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-100',
|
||||
isInvalidOtp && 'border-red-500 focus:border-red-500 focus:ring-red-100'
|
||||
)}
|
||||
/>
|
||||
<InputOTPSlot
|
||||
index={5}
|
||||
className={cn(
|
||||
'!rounded-[10px] h-12 w-12 border bg-white text-center font-medium text-lg shadow-sm transition-all duration-200',
|
||||
'border-gray-300 hover:border-gray-400',
|
||||
'focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-100',
|
||||
isInvalidOtp && 'border-red-500 focus:border-red-500 focus:ring-red-100'
|
||||
)}
|
||||
/>
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
@@ -110,27 +163,25 @@ function VerificationForm({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BrandedButton
|
||||
<Button
|
||||
onClick={verifyCode}
|
||||
className={`${buttonClass} flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200`}
|
||||
disabled={!isOtpComplete || isLoading}
|
||||
loading={isLoading}
|
||||
loadingText='Verifying'
|
||||
showArrow={false}
|
||||
>
|
||||
Verify Email
|
||||
</BrandedButton>
|
||||
{isLoading ? 'Verifying...' : 'Verify Email'}
|
||||
</Button>
|
||||
|
||||
{hasEmailService && (
|
||||
<div className='text-center'>
|
||||
<p className='text-[#999] text-sm'>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
Didn't receive a code?{' '}
|
||||
{countdown > 0 ? (
|
||||
<span>
|
||||
Resend in <span className='font-medium text-[#ECECEC]'>{countdown}s</span>
|
||||
Resend in <span className='font-medium text-foreground'>{countdown}s</span>
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
|
||||
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
|
||||
onClick={handleResend}
|
||||
disabled={isLoading || isResendDisabled}
|
||||
>
|
||||
@@ -151,7 +202,7 @@ function VerificationForm({
|
||||
}
|
||||
router.push('/signup')
|
||||
}}
|
||||
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
|
||||
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
|
||||
>
|
||||
Back to signup
|
||||
</button>
|
||||
@@ -166,8 +217,8 @@ function VerificationFormFallback() {
|
||||
return (
|
||||
<div className='text-center'>
|
||||
<div className='animate-pulse'>
|
||||
<div className='mx-auto mb-4 h-8 w-48 rounded bg-[#2A2A2A]' />
|
||||
<div className='mx-auto h-4 w-64 rounded bg-[#2A2A2A]' />
|
||||
<div className='mx-auto mb-4 h-8 w-48 rounded bg-gray-200' />
|
||||
<div className='mx-auto h-4 w-64 rounded bg-gray-200' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ function DotGrid({ className, cols, rows, gap = 0 }: DotGridProps) {
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: cols * rows }, (_, i) => (
|
||||
<div key={i} className='h-[1.5px] w-[1.5px] rounded-full bg-[#2A2A2A]' />
|
||||
<div key={i} className='h-[2px] w-[2px] rounded-full bg-[#2A2A2A]' />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
@@ -222,15 +222,34 @@ export default function Collaboration() {
|
||||
<style dangerouslySetInnerHTML={{ __html: CURSOR_KEYFRAMES }} />
|
||||
|
||||
<DotGrid
|
||||
className='overflow-hidden border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
|
||||
className='border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
|
||||
cols={120}
|
||||
rows={1}
|
||||
gap={6}
|
||||
/>
|
||||
|
||||
<div className='relative overflow-hidden'>
|
||||
<div className='grid grid-cols-1 md:grid-cols-[auto_1fr]'>
|
||||
<div className='flex flex-col items-start gap-3 px-4 pt-[60px] pb-8 sm:gap-4 sm:px-8 md:gap-[20px] md:px-[80px] md:pt-[100px]'>
|
||||
<Link
|
||||
href='/studio/multiplayer'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='absolute bottom-10 left-4 z-20 flex cursor-none items-center gap-[14px] rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] px-[12px] py-[10px] transition-colors hover:border-[#3d3d3d] hover:bg-[#232323] sm:left-8 md:left-[80px]'
|
||||
>
|
||||
<div className='relative h-7 w-11 shrink-0'>
|
||||
<Image src='/landing/multiplayer-cursors.svg' alt='' fill className='object-contain' />
|
||||
</div>
|
||||
<div className='flex flex-col gap-[2px]'>
|
||||
<span className='font-[430] font-season text-[#F6F6F0]/50 text-[12px] uppercase leading-[100%] tracking-[0.08em]'>
|
||||
Blog
|
||||
</span>
|
||||
<span className='font-[430] font-season text-[#F6F6F0] text-[14px] leading-[125%] tracking-[0.02em]'>
|
||||
How we built realtime collaboration
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className='grid grid-cols-[auto_1fr]'>
|
||||
<div className='flex flex-col items-start gap-3 px-4 pt-[100px] pb-8 sm:gap-4 sm:px-8 md:gap-[20px] md:px-[80px]'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
size='md'
|
||||
@@ -249,14 +268,13 @@ export default function Collaboration() {
|
||||
collaboration
|
||||
</h2>
|
||||
|
||||
<p className='font-[430] font-season text-[#F6F6F0]/50 text-[15px] leading-[150%] tracking-[0.02em] md:text-[18px]'>
|
||||
Grab your team. Build agents together <br className='hidden md:block' />
|
||||
in real-time inside your workspace.
|
||||
<p className='font-[430] font-season text-[#F6F6F0]/50 text-[14px] leading-[125%] tracking-[0.02em] sm:text-[16px]'>
|
||||
Grab your team. Build agents together <br /> in real-time inside your workspace.
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href='/signup'
|
||||
className='group/cta mt-[12px] inline-flex h-[32px] cursor-none items-center gap-[6px] rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[10px] font-[430] font-season text-[14px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
className='group/cta mt-[12px] inline-flex h-[32px] cursor-none items-center gap-[6px] rounded-[5px] border border-[#33C482] bg-[#33C482] px-[10px] font-[430] font-season text-[14px] text-black transition-[filter] hover:brightness-110'
|
||||
>
|
||||
Build together
|
||||
<span className='relative h-[10px] w-[10px] shrink-0'>
|
||||
@@ -280,14 +298,14 @@ export default function Collaboration() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<figure className='pointer-events-none relative h-[220px] w-full md:h-[600px]'>
|
||||
<div className='md:-left-[18%] -top-[10%] absolute inset-y-0 left-[7%] min-w-full md:top-0'>
|
||||
<figure className='pointer-events-none relative h-[600px] w-full'>
|
||||
<div className='-left-[18%] absolute inset-y-0 min-w-full'>
|
||||
<Image
|
||||
src='/landing/collaboration-visual.svg'
|
||||
alt='Collaboration visual showing team workflows with real-time editing, shared cursors, and version control interface'
|
||||
width={876}
|
||||
height={480}
|
||||
className='h-full w-auto object-left md:min-w-[100vw]'
|
||||
className='h-full w-auto min-w-[100vw] object-left'
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
@@ -301,29 +319,10 @@ export default function Collaboration() {
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href='/blog/multiplayer'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='relative mx-4 mb-6 flex cursor-none items-center gap-[14px] rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] px-[12px] py-[10px] transition-colors hover:border-[#3d3d3d] hover:bg-[#232323] sm:mx-8 md:absolute md:bottom-10 md:left-[80px] md:z-20 md:mx-0 md:mb-0'
|
||||
>
|
||||
<div className='relative h-7 w-11 shrink-0'>
|
||||
<Image src='/landing/multiplayer-cursors.svg' alt='' fill className='object-contain' />
|
||||
</div>
|
||||
<div className='flex flex-col gap-[2px]'>
|
||||
<span className='font-[430] font-season text-[#F6F6F0]/50 text-[12px] uppercase leading-[100%] tracking-[0.08em]'>
|
||||
Blog
|
||||
</span>
|
||||
<span className='font-[430] font-season text-[#F6F6F0] text-[14px] leading-[125%] tracking-[0.02em]'>
|
||||
How we built realtime collaboration
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<DotGrid
|
||||
className='overflow-hidden border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
|
||||
className='border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
|
||||
cols={120}
|
||||
rows={1}
|
||||
gap={6}
|
||||
|
||||
@@ -4,641 +4,14 @@
|
||||
* SEO:
|
||||
* - `<section id="enterprise" aria-labelledby="enterprise-heading">`.
|
||||
* - `<h2 id="enterprise-heading">` for the section title.
|
||||
* - Compliance certs (SOC 2, HIPAA) as visible `<strong>` text.
|
||||
* - Compliance certs (SOC2, HIPAA) as visible `<strong>` text.
|
||||
* - Enterprise CTA links to contact form via `<a>` with `rel="noopener noreferrer"`.
|
||||
*
|
||||
* GEO:
|
||||
* - Entity-rich: "Sim is SOC 2 and HIPAA compliant" — not "We are compliant."
|
||||
* - Entity-rich: "Sim is SOC2 and HIPAA compliant" — not "We are compliant."
|
||||
* - `<ul>` checklist of features (SSO, RBAC, audit logs, SLA, on-premise deployment)
|
||||
* as an atomic answer block for "What enterprise features does Sim offer?".
|
||||
*/
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { AnimatePresence, motion, useInView } from 'framer-motion'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Badge, ChevronDown } from '@/components/emcn'
|
||||
import { Lock } from '@/components/emcn/icons'
|
||||
import { GithubIcon } from '@/components/icons'
|
||||
import { PROVIDER_DEFINITIONS } from '@/providers/models'
|
||||
|
||||
/** Consistent color per actor — same pattern as Collaboration section cursors. */
|
||||
const ACTOR_COLORS: Record<string, string> = {
|
||||
'Sarah K.': '#2ABBF8',
|
||||
'Sid G.': '#33C482',
|
||||
'Theo L.': '#FA4EDF',
|
||||
'Abhay K.': '#FFCC02',
|
||||
'Danny S.': '#FF6B35',
|
||||
}
|
||||
|
||||
/** Left accent bar opacity by recency — newest is brightest. */
|
||||
const ACCENT_OPACITIES = [0.75, 0.5, 0.35, 0.22, 0.12, 0.05] as const
|
||||
|
||||
interface LogEntry {
|
||||
id: number
|
||||
actor: string
|
||||
/** Matches the `description` field stored by recordAudit() */
|
||||
description: string
|
||||
resourceType: string
|
||||
/** Unix ms timestamp of when this entry was "received" */
|
||||
insertedAt: number
|
||||
}
|
||||
|
||||
function formatTimeAgo(insertedAt: number): string {
|
||||
const elapsed = Date.now() - insertedAt
|
||||
if (elapsed < 8_000) return 'just now'
|
||||
if (elapsed < 60_000) return `${Math.floor(elapsed / 1000)}s ago`
|
||||
return `${Math.floor(elapsed / 60_000)}m ago`
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry templates using real description strings from the actual recordAudit()
|
||||
* calls across the codebase (e.g. `Added BYOK key for openai`,
|
||||
* `Invited alex@acme.com to workspace as member`).
|
||||
*/
|
||||
const ENTRY_TEMPLATES: Omit<LogEntry, 'id' | 'insertedAt'>[] = [
|
||||
{ actor: 'Sarah K.', description: 'Deployed workflow "Email Triage"', resourceType: 'workflow' },
|
||||
{
|
||||
actor: 'Sid G.',
|
||||
description: 'Invited alex@acme.com to workspace as member',
|
||||
resourceType: 'member',
|
||||
},
|
||||
{ actor: 'Theo L.', description: 'Added BYOK key for openai', resourceType: 'byok_key' },
|
||||
{ actor: 'Sarah K.', description: 'Created workflow "Invoice Parser"', resourceType: 'workflow' },
|
||||
{
|
||||
actor: 'Abhay K.',
|
||||
description: 'Created permission group "Engineering"',
|
||||
resourceType: 'permission_group',
|
||||
},
|
||||
{ actor: 'Danny S.', description: 'Created API key "Production Key"', resourceType: 'api_key' },
|
||||
{
|
||||
actor: 'Theo L.',
|
||||
description: 'Changed permissions for sam@acme.com to editor',
|
||||
resourceType: 'member',
|
||||
},
|
||||
{ actor: 'Sarah K.', description: 'Uploaded file "Q3_Report.pdf"', resourceType: 'file' },
|
||||
{
|
||||
actor: 'Sid G.',
|
||||
description: 'Created credential set "Prod Keys"',
|
||||
resourceType: 'credential_set',
|
||||
},
|
||||
{
|
||||
actor: 'Abhay K.',
|
||||
description: 'Created knowledge base "Internal Docs"',
|
||||
resourceType: 'knowledge_base',
|
||||
},
|
||||
{ actor: 'Danny S.', description: 'Updated environment variables', resourceType: 'environment' },
|
||||
{
|
||||
actor: 'Sarah K.',
|
||||
description: 'Added tool "search_web" to MCP server',
|
||||
resourceType: 'mcp_server',
|
||||
},
|
||||
{ actor: 'Sid G.', description: 'Created webhook "Stripe Payment"', resourceType: 'webhook' },
|
||||
{ actor: 'Theo L.', description: 'Deployed chat "Support Assistant"', resourceType: 'chat' },
|
||||
{ actor: 'Abhay K.', description: 'Created table "Lead Tracker"', resourceType: 'table' },
|
||||
{ actor: 'Danny S.', description: 'Revoked API key "Staging Key"', resourceType: 'api_key' },
|
||||
{
|
||||
actor: 'Sarah K.',
|
||||
description: 'Duplicated workflow "Data Enrichment"',
|
||||
resourceType: 'workflow',
|
||||
},
|
||||
{
|
||||
actor: 'Sid G.',
|
||||
description: 'Removed member theo@acme.com from workspace',
|
||||
resourceType: 'member',
|
||||
},
|
||||
{
|
||||
actor: 'Theo L.',
|
||||
description: 'Updated knowledge base "Product Docs"',
|
||||
resourceType: 'knowledge_base',
|
||||
},
|
||||
{ actor: 'Abhay K.', description: 'Created folder "Finance Workflows"', resourceType: 'folder' },
|
||||
{
|
||||
actor: 'Danny S.',
|
||||
description: 'Uploaded document "onboarding-guide.pdf"',
|
||||
resourceType: 'document',
|
||||
},
|
||||
{
|
||||
actor: 'Sarah K.',
|
||||
description: 'Updated credential set "Prod Keys"',
|
||||
resourceType: 'credential_set',
|
||||
},
|
||||
{
|
||||
actor: 'Sid G.',
|
||||
description: 'Added member abhay@acme.com to permission group "Engineering"',
|
||||
resourceType: 'permission_group',
|
||||
},
|
||||
{ actor: 'Theo L.', description: 'Locked workflow "Customer Sync"', resourceType: 'workflow' },
|
||||
]
|
||||
|
||||
const INITIAL_OFFSETS_MS = [0, 20_000, 75_000, 180_000, 360_000, 600_000]
|
||||
|
||||
const MARQUEE_KEYFRAMES = `
|
||||
@keyframes marquee {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-25%); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@keyframes marquee { 0%, 100% { transform: none; } }
|
||||
}
|
||||
`
|
||||
|
||||
const FEATURE_TAGS = [
|
||||
'Access Control',
|
||||
'Self-Hosting',
|
||||
'Bring Your Own Key',
|
||||
'Credential Sharing',
|
||||
'Custom Limits',
|
||||
'Admin API',
|
||||
'White Labeling',
|
||||
'Dedicated Support',
|
||||
'99.99% Uptime SLA',
|
||||
'Workflow Versioning',
|
||||
'On-Premise',
|
||||
'Organizations',
|
||||
'Workspace Export',
|
||||
'Audit Logs',
|
||||
] as const
|
||||
|
||||
interface AuditRowProps {
|
||||
entry: LogEntry
|
||||
index: number
|
||||
}
|
||||
|
||||
function AuditRow({ entry, index }: AuditRowProps) {
|
||||
const color = ACTOR_COLORS[entry.actor] ?? '#F6F6F6'
|
||||
const accentOpacity = ACCENT_OPACITIES[index] ?? 0.04
|
||||
const timeAgo = formatTimeAgo(entry.insertedAt)
|
||||
|
||||
return (
|
||||
<div className='group relative overflow-hidden border-[#2A2A2A] border-b bg-[#1C1C1C] transition-colors duration-150 last:border-b-0 hover:bg-[#212121]'>
|
||||
{/* Left accent bar — brightness encodes recency */}
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute top-0 bottom-0 left-0 w-[2px] transition-opacity duration-150 group-hover:opacity-100'
|
||||
style={{ backgroundColor: color, opacity: accentOpacity }}
|
||||
/>
|
||||
|
||||
{/* Row content */}
|
||||
<div className='flex min-w-0 items-center gap-3 py-[10px] pr-4 pl-5'>
|
||||
{/* Actor avatar */}
|
||||
<div
|
||||
className='flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full'
|
||||
style={{ backgroundColor: `${color}20` }}
|
||||
>
|
||||
<span className='font-[500] font-season text-[9px] leading-none' style={{ color }}>
|
||||
{entry.actor[0]}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Time */}
|
||||
<span className='w-[56px] shrink-0 font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em]'>
|
||||
{timeAgo}
|
||||
</span>
|
||||
|
||||
<span className='min-w-0 truncate font-[430] font-season text-[12px] leading-none tracking-[0.02em]'>
|
||||
<span className='text-[#F6F6F6]/80'>{entry.actor}</span>
|
||||
<span className='hidden sm:inline'>
|
||||
<span className='text-[#F6F6F6]/40'> · </span>
|
||||
<span className='text-[#F6F6F6]/55'>{entry.description}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AuditLogPreview() {
|
||||
const counterRef = useRef(ENTRY_TEMPLATES.length)
|
||||
const templateIndexRef = useRef(6 % ENTRY_TEMPLATES.length)
|
||||
|
||||
const now = Date.now()
|
||||
const [entries, setEntries] = useState<LogEntry[]>(() =>
|
||||
ENTRY_TEMPLATES.slice(0, 6).map((t, i) => ({
|
||||
...t,
|
||||
id: i,
|
||||
insertedAt: now - INITIAL_OFFSETS_MS[i],
|
||||
}))
|
||||
)
|
||||
const [, tick] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const addInterval = setInterval(() => {
|
||||
const template = ENTRY_TEMPLATES[templateIndexRef.current]
|
||||
templateIndexRef.current = (templateIndexRef.current + 1) % ENTRY_TEMPLATES.length
|
||||
|
||||
setEntries((prev) => [
|
||||
{ ...template, id: counterRef.current++, insertedAt: Date.now() },
|
||||
...prev.slice(0, 5),
|
||||
])
|
||||
}, 2600)
|
||||
|
||||
// Refresh time labels every 5s so "just now" ages to "Xs ago"
|
||||
const tickInterval = setInterval(() => tick((n) => n + 1), 5_000)
|
||||
|
||||
return () => {
|
||||
clearInterval(addInterval)
|
||||
clearInterval(tickInterval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='mt-5 overflow-hidden px-6 md:mt-6 md:px-8'>
|
||||
<AnimatePresence mode='popLayout' initial={false}>
|
||||
{entries.map((entry, index) => (
|
||||
<motion.div
|
||||
key={entry.id}
|
||||
layout
|
||||
initial={{ y: -48, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
layout: {
|
||||
type: 'spring',
|
||||
stiffness: 380,
|
||||
damping: 38,
|
||||
mass: 0.8,
|
||||
},
|
||||
y: { duration: 0.32, ease: [0.25, 0.46, 0.45, 0.94] },
|
||||
opacity: { duration: 0.25 },
|
||||
}}
|
||||
>
|
||||
<AuditRow entry={entry} index={index} />
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface PermissionFeature {
|
||||
name: string
|
||||
key: string
|
||||
defaultEnabled: boolean
|
||||
providerId?: string
|
||||
}
|
||||
|
||||
interface PermissionCategory {
|
||||
label: string
|
||||
color: string
|
||||
features: PermissionFeature[]
|
||||
}
|
||||
|
||||
const PERMISSION_CATEGORIES: PermissionCategory[] = [
|
||||
{
|
||||
label: 'Providers',
|
||||
color: '#FA4EDF',
|
||||
features: [
|
||||
{ key: 'openai', name: 'OpenAI', defaultEnabled: true, providerId: 'openai' },
|
||||
{ key: 'anthropic', name: 'Anthropic', defaultEnabled: true, providerId: 'anthropic' },
|
||||
{ key: 'google', name: 'Google', defaultEnabled: false, providerId: 'google' },
|
||||
{ key: 'xai', name: 'xAI', defaultEnabled: true, providerId: 'xai' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Workspace',
|
||||
color: '#2ABBF8',
|
||||
features: [
|
||||
{ key: 'knowledge-base', name: 'Knowledge Base', defaultEnabled: true },
|
||||
{ key: 'tables', name: 'Tables', defaultEnabled: true },
|
||||
{ key: 'copilot', name: 'Copilot', defaultEnabled: false },
|
||||
{ key: 'environment', name: 'Environment', defaultEnabled: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Tools',
|
||||
color: '#33C482',
|
||||
features: [
|
||||
{ key: 'mcp-tools', name: 'MCP Tools', defaultEnabled: true },
|
||||
{ key: 'custom-tools', name: 'Custom Tools', defaultEnabled: false },
|
||||
{ key: 'skills', name: 'Skills', defaultEnabled: true },
|
||||
{ key: 'invitations', name: 'Invitations', defaultEnabled: true },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const INITIAL_ACCESS_STATE = Object.fromEntries(
|
||||
PERMISSION_CATEGORIES.flatMap((category) =>
|
||||
category.features.map((feature) => [feature.key, feature.defaultEnabled])
|
||||
)
|
||||
)
|
||||
|
||||
function CheckboxIcon({ checked, color }: { checked: boolean; color: string }) {
|
||||
return (
|
||||
<div
|
||||
className='h-[6px] w-[6px] shrink-0 rounded-full transition-colors duration-200'
|
||||
style={{
|
||||
backgroundColor: checked ? color : 'transparent',
|
||||
border: checked ? 'none' : '1.5px solid #3A3A3A',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ProviderPreviewIcon({ providerId }: { providerId?: string }) {
|
||||
if (!providerId) return null
|
||||
|
||||
const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon
|
||||
if (!ProviderIcon) return null
|
||||
|
||||
return (
|
||||
<div className='relative flex h-[14px] w-[14px] shrink-0 items-center justify-center opacity-50 brightness-0 invert'>
|
||||
<ProviderIcon className='!h-[14px] !w-[14px]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AccessControlPanel() {
|
||||
const ref = useRef(null)
|
||||
const isInView = useInView(ref, { once: true, margin: '-40px' })
|
||||
const [accessState, setAccessState] = useState<Record<string, boolean>>(INITIAL_ACCESS_STATE)
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<div className='lg:hidden'>
|
||||
{PERMISSION_CATEGORIES.map((category, catIdx) => {
|
||||
const offsetBefore = PERMISSION_CATEGORIES.slice(0, catIdx).reduce(
|
||||
(sum, c) => sum + c.features.length,
|
||||
0
|
||||
)
|
||||
|
||||
return (
|
||||
<div key={category.label} className={catIdx > 0 ? 'mt-4' : ''}>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[10px] uppercase leading-none tracking-[0.08em]'>
|
||||
{category.label}
|
||||
</span>
|
||||
<div className='mt-[8px] grid grid-cols-2 gap-x-4 gap-y-[8px]'>
|
||||
{category.features.map((feature, featIdx) => {
|
||||
const enabled = accessState[feature.key]
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={feature.key}
|
||||
className='flex cursor-pointer items-center gap-[8px] rounded-[4px] py-[2px]'
|
||||
initial={{ opacity: 0, x: -6 }}
|
||||
animate={isInView ? { opacity: 1, x: 0 } : {}}
|
||||
transition={{
|
||||
delay: 0.05 + (offsetBefore + featIdx) * 0.04,
|
||||
duration: 0.3,
|
||||
}}
|
||||
onClick={() =>
|
||||
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
|
||||
}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<CheckboxIcon checked={enabled} color={category.color} />
|
||||
<ProviderPreviewIcon providerId={feature.providerId} />
|
||||
<span
|
||||
className='truncate font-[430] font-season text-[13px] leading-none tracking-[0.02em]'
|
||||
style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}
|
||||
>
|
||||
{feature.name}
|
||||
</span>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Desktop — categorized grid */}
|
||||
<div className='hidden lg:block'>
|
||||
{PERMISSION_CATEGORIES.map((category, catIdx) => (
|
||||
<div key={category.label} className={catIdx > 0 ? 'mt-4' : ''}>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[10px] uppercase leading-none tracking-[0.08em]'>
|
||||
{category.label}
|
||||
</span>
|
||||
<div className='mt-[8px] grid grid-cols-2 gap-x-4 gap-y-[8px]'>
|
||||
{category.features.map((feature, featIdx) => {
|
||||
const enabled = accessState[feature.key]
|
||||
const currentIndex =
|
||||
PERMISSION_CATEGORIES.slice(0, catIdx).reduce(
|
||||
(sum, c) => sum + c.features.length,
|
||||
0
|
||||
) + featIdx
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={feature.key}
|
||||
className='flex cursor-pointer items-center gap-[8px] rounded-[4px] py-[2px]'
|
||||
initial={{ opacity: 0, x: -6 }}
|
||||
animate={isInView ? { opacity: 1, x: 0 } : {}}
|
||||
transition={{
|
||||
delay: 0.1 + currentIndex * 0.04,
|
||||
duration: 0.3,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
}}
|
||||
onClick={() =>
|
||||
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
|
||||
}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<CheckboxIcon checked={enabled} color={category.color} />
|
||||
<ProviderPreviewIcon providerId={feature.providerId} />
|
||||
<span
|
||||
className='truncate font-[430] font-season text-[11px] leading-none tracking-[0.02em] transition-opacity duration-200'
|
||||
style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}
|
||||
>
|
||||
{feature.name}
|
||||
</span>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TrustStrip() {
|
||||
return (
|
||||
<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'
|
||||
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'
|
||||
>
|
||||
<Image
|
||||
src='/footer/soc2.png'
|
||||
alt='SOC 2 Type II'
|
||||
width={22}
|
||||
height={22}
|
||||
className='shrink-0 object-contain'
|
||||
/>
|
||||
<div className='flex flex-col gap-[3px]'>
|
||||
<strong className='font-[430] font-season text-[13px] text-white leading-none'>
|
||||
SOC 2 & HIPAA
|
||||
</strong>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em] transition-colors group-hover:text-[#F6F6F6]/55'>
|
||||
Type II · PHI protected →
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Open Source — center */}
|
||||
<Link
|
||||
href='https://github.com/simstudioai/sim'
|
||||
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'
|
||||
>
|
||||
<div className='flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full bg-[#FFCC02]/10'>
|
||||
<GithubIcon width={11} height={11} className='text-[#FFCC02]/75' />
|
||||
</div>
|
||||
<div className='flex flex-col gap-[3px]'>
|
||||
<strong className='font-[430] font-season text-[13px] text-white leading-none'>
|
||||
Open Source
|
||||
</strong>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em] transition-colors group-hover:text-[#F6F6F6]/55'>
|
||||
View on GitHub →
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* SSO */}
|
||||
<div className='flex items-center gap-3 px-4 py-[14px]'>
|
||||
<div className='flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full bg-[#2ABBF8]/10'>
|
||||
<Lock className='h-[14px] w-[14px] text-[#2ABBF8]/75' />
|
||||
</div>
|
||||
<div className='flex flex-col gap-[3px]'>
|
||||
<strong className='font-[430] font-season text-[13px] text-white leading-none'>
|
||||
SSO & SCIM
|
||||
</strong>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em]'>
|
||||
Okta, Azure AD, Google
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Enterprise() {
|
||||
return (
|
||||
<section id='enterprise' aria-labelledby='enterprise-heading' className='bg-[#F6F6F6]'>
|
||||
<div className='px-4 pt-[60px] pb-[40px] sm:px-8 sm:pt-[80px] sm:pb-0 md:px-[80px] md:pt-[100px]'>
|
||||
<div className='flex flex-col items-start gap-3 sm:gap-4 md:gap-[20px]'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
size='md'
|
||||
dot
|
||||
className='bg-[#FFCC02]/10 font-season text-[#FFCC02] uppercase tracking-[0.02em]'
|
||||
>
|
||||
Enterprise
|
||||
</Badge>
|
||||
|
||||
<h2
|
||||
id='enterprise-heading'
|
||||
className='max-w-[600px] font-[430] font-season text-[#1C1C1C] text-[32px] leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
|
||||
>
|
||||
Enterprise features for
|
||||
<br />
|
||||
fast, scalable workflows
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 overflow-hidden rounded-[12px] bg-[#1C1C1C] sm:mt-10 md:mt-12'>
|
||||
<div className='grid grid-cols-1 border-[#2A2A2A] border-b lg:grid-cols-[1fr_420px]'>
|
||||
{/* Audit Trail */}
|
||||
<div className='border-[#2A2A2A] lg:border-r'>
|
||||
<div className='px-6 pt-6 md:px-8 md:pt-8'>
|
||||
<h3 className='font-[430] font-season text-[16px] text-white leading-[120%] tracking-[-0.01em]'>
|
||||
Audit Trail
|
||||
</h3>
|
||||
<p className='mt-2 max-w-[480px] font-[430] font-season text-[#F6F6F6]/50 text-[14px] leading-[150%] tracking-[0.02em]'>
|
||||
Every action is captured with full actor attribution.
|
||||
</p>
|
||||
</div>
|
||||
<AuditLogPreview />
|
||||
<div className='h-6 md:h-8' />
|
||||
</div>
|
||||
|
||||
{/* Access Control */}
|
||||
<div className='border-[#2A2A2A] border-t lg:border-t-0'>
|
||||
<div className='px-6 pt-6 md:px-8 md:pt-8'>
|
||||
<h3 className='font-[430] font-season text-[16px] text-white leading-[120%] tracking-[-0.01em]'>
|
||||
Access Control
|
||||
</h3>
|
||||
<p className='mt-[6px] font-[430] font-season text-[#F6F6F6]/50 text-[14px] leading-[150%] tracking-[0.02em]'>
|
||||
Restrict providers, surfaces, and tools per group.
|
||||
</p>
|
||||
</div>
|
||||
<div className='mt-5 px-6 pb-6 md:mt-6 md:px-8 md:pb-8'>
|
||||
<AccessControlPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TrustStrip />
|
||||
|
||||
{/* Scrolling feature ticker */}
|
||||
<div className='relative mt-6 overflow-hidden border-[#2A2A2A] border-t'>
|
||||
<style dangerouslySetInnerHTML={{ __html: MARQUEE_KEYFRAMES }} />
|
||||
{/* Fade edges */}
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-0 bottom-0 left-0 z-10 w-16'
|
||||
style={{ background: 'linear-gradient(to right, #1C1C1C, transparent)' }}
|
||||
/>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-0 right-0 bottom-0 z-10 w-16'
|
||||
style={{ background: 'linear-gradient(to left, #1C1C1C, transparent)' }}
|
||||
/>
|
||||
{/* Duplicate tags for seamless loop */}
|
||||
<div className='flex w-max' style={{ animation: 'marquee 30s linear infinite' }}>
|
||||
{[...FEATURE_TAGS, ...FEATURE_TAGS, ...FEATURE_TAGS, ...FEATURE_TAGS].map(
|
||||
(tag, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className='whitespace-nowrap border-[#2A2A2A] border-r px-5 py-4 font-[430] font-season text-[#F6F6F6]/40 text-[13px] leading-none tracking-[0.02em]'
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between border-[#2A2A2A] border-t px-6 py-5 md:px-8 md:py-6'>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/40 text-[15px] leading-[150%] tracking-[0.02em]'>
|
||||
Ready for growth?
|
||||
</p>
|
||||
<Link
|
||||
href='https://form.typeform.com/to/jqCO12pF'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='group/cta inline-flex h-[32px] items-center gap-[6px] rounded-[5px] border border-white bg-white px-[10px] font-[430] font-season text-[14px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
>
|
||||
Book a demo
|
||||
<span className='relative h-[10px] w-[10px] shrink-0'>
|
||||
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
|
||||
<svg
|
||||
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
>
|
||||
<path
|
||||
d='M1 5H8M5.5 2L8.5 5L5.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
import { type MotionValue, motion, useScroll, useTransform } from 'framer-motion'
|
||||
import { useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Badge, ChevronDown } from '@/components/emcn'
|
||||
import { FeaturesPreview } from '@/app/(home)/components/features/components/features-preview'
|
||||
import { Badge } from '@/components/emcn'
|
||||
|
||||
function hexToRgba(hex: string, alpha: number): string {
|
||||
const r = Number.parseInt(hex.slice(1, 3), 16)
|
||||
@@ -16,12 +13,8 @@ function hexToRgba(hex: string, alpha: number): string {
|
||||
|
||||
const FEATURE_TABS = [
|
||||
{
|
||||
label: 'Mothership',
|
||||
label: 'Integrations',
|
||||
color: '#FA4EDF',
|
||||
title: 'Your AI command center',
|
||||
description:
|
||||
'Direct your entire AI workforce from one place. Build agents, spin up workflows, query tables, and manage every resource across your workspace — in natural language.',
|
||||
cta: 'Explore mothership',
|
||||
segments: [
|
||||
[0.3, 8],
|
||||
[0.25, 10],
|
||||
@@ -36,12 +29,8 @@ const FEATURE_TABS = [
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Tables',
|
||||
label: 'Copilot',
|
||||
color: '#2ABBF8',
|
||||
title: 'A database, built in',
|
||||
description:
|
||||
'Filter, sort, and edit data inline, then wire it directly into your workflows. Agents query, insert, and update rows on every run — no external database needed.',
|
||||
cta: 'Explore tables',
|
||||
segments: [
|
||||
[0.25, 12],
|
||||
[0.4, 10],
|
||||
@@ -55,55 +44,43 @@ const FEATURE_TABS = [
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Files',
|
||||
color: '#FFCC02',
|
||||
badgeColor: '#EAB308',
|
||||
title: 'Upload, create, and share',
|
||||
description:
|
||||
'Create or upload documents, spreadsheets, and media that agents can read, write, and reference across workflows. One shared store your entire team and every agent can pull from.',
|
||||
cta: 'Explore files',
|
||||
label: 'Models',
|
||||
color: '#00F701',
|
||||
badgeColor: '#22C55E',
|
||||
segments: [
|
||||
[0.25, 10],
|
||||
[0.4, 8],
|
||||
[0.35, 12],
|
||||
[0.2, 6],
|
||||
[0.35, 10],
|
||||
[0.3, 8],
|
||||
[0.5, 10],
|
||||
[0.65, 8],
|
||||
[0.75, 10],
|
||||
[0.6, 8],
|
||||
[0.75, 12],
|
||||
[0.85, 10],
|
||||
[1, 8],
|
||||
[0.9, 12],
|
||||
[1, 10],
|
||||
[0.85, 10],
|
||||
[1, 10],
|
||||
[0.95, 6],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Knowledge Base',
|
||||
mobileLabel: 'Knowledge',
|
||||
color: '#8B5CF6',
|
||||
title: 'Your context engine',
|
||||
description:
|
||||
'Sync institutional knowledge from 30+ live connectors — Notion, Drive, Slack, Confluence, and more — so every agent draws from the same truth across your entire organization.',
|
||||
cta: 'Explore knowledge base',
|
||||
label: 'Deploy',
|
||||
color: '#FFCC02',
|
||||
badgeColor: '#EAB308',
|
||||
segments: [
|
||||
[0.3, 10],
|
||||
[0.3, 12],
|
||||
[0.25, 8],
|
||||
[0.4, 10],
|
||||
[0.5, 10],
|
||||
[0.65, 10],
|
||||
[0.8, 10],
|
||||
[0.9, 12],
|
||||
[0.55, 10],
|
||||
[0.7, 8],
|
||||
[0.6, 10],
|
||||
[0.85, 12],
|
||||
[1, 10],
|
||||
[0.95, 10],
|
||||
[0.9, 10],
|
||||
[1, 10],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Logs',
|
||||
hideOnMobile: true,
|
||||
color: '#FF6B35',
|
||||
title: 'Full visibility, every run',
|
||||
description:
|
||||
'Trace every execution block by block — inputs, outputs, cost, and duration. Filter by status or workflow, replay snapshots, and export reports to keep your team accountable.',
|
||||
cta: 'Explore logs',
|
||||
segments: [
|
||||
[0.25, 10],
|
||||
[0.35, 8],
|
||||
@@ -117,27 +94,24 @@ const FEATURE_TABS = [
|
||||
[1, 10],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Knowledge Base',
|
||||
color: '#8B5CF6',
|
||||
segments: [
|
||||
[0.3, 10],
|
||||
[0.25, 8],
|
||||
[0.4, 10],
|
||||
[0.5, 10],
|
||||
[0.65, 10],
|
||||
[0.8, 10],
|
||||
[0.9, 12],
|
||||
[1, 10],
|
||||
[0.95, 10],
|
||||
[1, 10],
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const HEADING_TEXT = 'Everything you need to build, deploy, and manage AI agents. '
|
||||
const HEADING_LETTERS = HEADING_TEXT.split('')
|
||||
|
||||
const LETTER_REVEAL_SPAN = 0.85
|
||||
const LETTER_FADE_IN = 0.04
|
||||
|
||||
interface ScrollLetterProps {
|
||||
scrollYProgress: MotionValue<number>
|
||||
charIndex: number
|
||||
children: string
|
||||
}
|
||||
|
||||
function ScrollLetter({ scrollYProgress, charIndex, children }: ScrollLetterProps) {
|
||||
const threshold = (charIndex / HEADING_LETTERS.length) * LETTER_REVEAL_SPAN
|
||||
const opacity = useTransform(scrollYProgress, [threshold, threshold + LETTER_FADE_IN], [0.4, 1])
|
||||
|
||||
return <motion.span style={{ opacity }}>{children}</motion.span>
|
||||
}
|
||||
|
||||
function DotGrid({
|
||||
cols,
|
||||
rows,
|
||||
@@ -152,7 +126,7 @@ function DotGrid({
|
||||
return (
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className={`h-full shrink-0 bg-[#F6F6F6] p-[6px] ${borderLeft ? 'border-[#E9E9E9] border-l' : ''}`}
|
||||
className={`shrink-0 bg-[#FDFDFD] p-[6px] ${borderLeft ? 'border-[#E9E9E9] border-l' : ''}`}
|
||||
style={{
|
||||
width: width ? `${width}px` : undefined,
|
||||
display: 'grid',
|
||||
@@ -162,26 +136,20 @@ function DotGrid({
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: cols * rows }, (_, i) => (
|
||||
<div key={i} className='h-[1.5px] w-[1.5px] rounded-full bg-[#DEDEDE]' />
|
||||
<div key={i} className='h-[2px] w-[2px] rounded-full bg-[#DEDEDE]' />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Features() {
|
||||
const sectionRef = useRef<HTMLDivElement>(null)
|
||||
const [activeTab, setActiveTab] = useState(0)
|
||||
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: sectionRef,
|
||||
offset: ['start 0.9', 'start 0.2'],
|
||||
})
|
||||
|
||||
return (
|
||||
<section
|
||||
id='features'
|
||||
aria-labelledby='features-heading'
|
||||
className='relative overflow-hidden bg-[#F6F6F6]'
|
||||
className='relative overflow-hidden bg-[#F6F6F6] pb-[144px]'
|
||||
>
|
||||
<div aria-hidden='true' className='absolute top-0 left-0 w-full'>
|
||||
<Image
|
||||
@@ -194,11 +162,8 @@ export default function Features() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 pt-[60px] lg:pt-[100px]'>
|
||||
<div
|
||||
ref={sectionRef}
|
||||
className='flex flex-col items-start gap-[20px] px-[24px] lg:px-[80px]'
|
||||
>
|
||||
<div className='relative z-10 pt-[100px]'>
|
||||
<div className='flex flex-col items-start gap-[20px] px-[80px]'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
size='md'
|
||||
@@ -212,131 +177,51 @@ export default function Features() {
|
||||
),
|
||||
}}
|
||||
>
|
||||
Workspace
|
||||
Features
|
||||
</Badge>
|
||||
<h2
|
||||
id='features-heading'
|
||||
className='max-w-[900px] font-[430] font-season text-[#1C1C1C] text-[28px] leading-[110%] tracking-[-0.02em] md:text-[40px]'
|
||||
className='font-[430] font-season text-[#1C1C1C] text-[40px] leading-[100%] tracking-[-0.02em]'
|
||||
>
|
||||
{HEADING_LETTERS.map((char, i) => (
|
||||
<ScrollLetter key={i} scrollYProgress={scrollYProgress} charIndex={i}>
|
||||
{char}
|
||||
</ScrollLetter>
|
||||
))}
|
||||
<span className='text-[#1C1C1C]/40'>
|
||||
Design powerful workflows, connect your data, and monitor every run — all in one
|
||||
platform.
|
||||
</span>
|
||||
Power your AI workforce
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className='relative mt-[40px] pb-[40px] lg:mt-[73px] lg:pb-[80px]'>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute top-0 bottom-0 left-[80px] z-20 hidden w-px bg-[#E9E9E9] lg:block'
|
||||
/>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute top-0 right-[80px] bottom-0 z-20 hidden w-px bg-[#E9E9E9] lg:block'
|
||||
/>
|
||||
<div className='mt-[73px] flex h-[68px] overflow-hidden border border-[#E9E9E9]'>
|
||||
<DotGrid cols={10} rows={8} width={80} />
|
||||
|
||||
<div className='flex h-[68px] border border-[#E9E9E9] lg:overflow-hidden'>
|
||||
<div className='h-full shrink-0'>
|
||||
<div className='h-full lg:hidden'>
|
||||
<DotGrid cols={3} rows={8} width={24} />
|
||||
</div>
|
||||
<div className='hidden h-full lg:block'>
|
||||
<DotGrid cols={10} rows={8} width={80} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role='tablist' aria-label='Feature categories' className='flex flex-1'>
|
||||
{FEATURE_TABS.map((tab, index) => (
|
||||
<button
|
||||
key={tab.label}
|
||||
type='button'
|
||||
role='tab'
|
||||
aria-selected={index === activeTab}
|
||||
onClick={() => setActiveTab(index)}
|
||||
className={`relative h-full flex-1 items-center justify-center whitespace-nowrap px-[12px] font-medium font-season text-[#212121] text-[12px] uppercase lg:px-0 lg:text-[14px]${tab.hideOnMobile ? ' hidden lg:flex' : ' flex'}${index > 0 ? ' border-[#E9E9E9] border-l' : ''}`}
|
||||
style={{ backgroundColor: index === activeTab ? '#FDFDFD' : '#F6F6F6' }}
|
||||
>
|
||||
{tab.mobileLabel ? (
|
||||
<>
|
||||
<span className='lg:hidden'>{tab.mobileLabel}</span>
|
||||
<span className='hidden lg:inline'>{tab.label}</span>
|
||||
</>
|
||||
) : (
|
||||
tab.label
|
||||
)}
|
||||
{index === activeTab && (
|
||||
<div className='absolute right-0 bottom-0 left-0 flex h-[6px]'>
|
||||
{tab.segments.map(([opacity, width], i) => (
|
||||
<div
|
||||
key={i}
|
||||
className='h-full shrink-0'
|
||||
style={{
|
||||
width: `${width}%`,
|
||||
backgroundColor: tab.color,
|
||||
opacity,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='h-full shrink-0'>
|
||||
<div className='h-full lg:hidden'>
|
||||
<DotGrid cols={3} rows={8} width={24} />
|
||||
</div>
|
||||
<div className='hidden h-full lg:block'>
|
||||
<DotGrid cols={10} rows={8} width={80} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-[32px] flex flex-col gap-[24px] px-[24px] lg:mt-[60px] lg:grid lg:grid-cols-[1fr_2.8fr] lg:gap-[60px] lg:px-[120px]'>
|
||||
<div className='flex flex-col items-start justify-between gap-[24px] pt-[20px] lg:h-[560px] lg:gap-0'>
|
||||
<div className='flex flex-col items-start gap-[16px]'>
|
||||
<h3 className='font-[430] font-season text-[#1C1C1C] text-[24px] leading-[120%] tracking-[-0.02em] lg:text-[28px]'>
|
||||
{FEATURE_TABS[activeTab].title}
|
||||
</h3>
|
||||
<p className='font-[430] font-season text-[#1C1C1C]/50 text-[16px] leading-[150%] tracking-[0.02em] lg:text-[18px]'>
|
||||
{FEATURE_TABS[activeTab].description}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href='/signup'
|
||||
className='group/cta inline-flex h-[32px] items-center gap-[6px] rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-[10px] font-[430] font-season text-[14px] text-white transition-colors hover:border-[#2A2A2A] hover:bg-[#2A2A2A]'
|
||||
<div role='tablist' aria-label='Feature categories' className='flex flex-1'>
|
||||
{FEATURE_TABS.map((tab, index) => (
|
||||
<button
|
||||
key={tab.label}
|
||||
type='button'
|
||||
role='tab'
|
||||
aria-selected={index === activeTab}
|
||||
onClick={() => setActiveTab(index)}
|
||||
className='relative flex h-full flex-1 items-center justify-center border-[#E9E9E9] border-l font-medium font-season text-[#212121] text-[14px] uppercase'
|
||||
style={{ backgroundColor: index === activeTab ? '#FDFDFD' : '#F6F6F6' }}
|
||||
>
|
||||
{FEATURE_TABS[activeTab].cta}
|
||||
<span className='relative h-[10px] w-[10px] shrink-0'>
|
||||
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
|
||||
<svg
|
||||
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
>
|
||||
<path
|
||||
d='M1 5H8M5.5 2L8.5 5L5.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<FeaturesPreview activeTab={activeTab} />
|
||||
{tab.label}
|
||||
{index === activeTab && (
|
||||
<div className='absolute right-0 bottom-0 left-0 flex h-[6px]'>
|
||||
{tab.segments.map(([opacity, width], i) => (
|
||||
<div
|
||||
key={i}
|
||||
className='h-full shrink-0'
|
||||
style={{
|
||||
width: `${width}%`,
|
||||
backgroundColor: tab.color,
|
||||
opacity,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div aria-hidden='true' className='mt-[60px] hidden h-px bg-[#E9E9E9] lg:block' />
|
||||
<DotGrid cols={10} rows={8} width={80} borderLeft />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { ArrowUp } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useLandingSubmit } from '@/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
|
||||
import { useAnimatedPlaceholder } from '@/app/workspace/[workspaceId]/home/hooks/use-animated-placeholder'
|
||||
|
||||
const MAX_HEIGHT = 120
|
||||
|
||||
const CTA_BUTTON =
|
||||
'inline-flex items-center h-[32px] rounded-[5px] border px-[10px] font-[430] font-season text-[14px]'
|
||||
|
||||
export function FooterCTA() {
|
||||
const landingSubmit = useLandingSubmit()
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const animatedPlaceholder = useAnimatedPlaceholder()
|
||||
|
||||
const isEmpty = inputValue.trim().length === 0
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (isEmpty) return
|
||||
landingSubmit(inputValue)
|
||||
}, [isEmpty, inputValue, landingSubmit])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSubmit()
|
||||
}
|
||||
},
|
||||
[handleSubmit]
|
||||
)
|
||||
|
||||
const handleInput = useCallback((e: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
const target = e.target as HTMLTextAreaElement
|
||||
target.style.height = 'auto'
|
||||
target.style.height = `${Math.min(target.scrollHeight, MAX_HEIGHT)}px`
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-center px-4 pt-[120px] pb-[100px] sm:px-8 md:px-[80px]'>
|
||||
<h2 className='text-center font-[430] font-season text-[#1C1C1C] text-[28px] leading-[100%] tracking-[-0.02em] sm:text-[32px] md:text-[36px]'>
|
||||
What should we get done?
|
||||
</h2>
|
||||
|
||||
<div className='mt-8 w-full max-w-[42rem]'>
|
||||
<div
|
||||
className='cursor-text rounded-[20px] border border-[#E5E5E5] bg-white px-[10px] py-[8px] shadow-sm'
|
||||
onClick={() => textareaRef.current?.focus()}
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onInput={handleInput}
|
||||
placeholder={animatedPlaceholder}
|
||||
rows={2}
|
||||
className='m-0 box-border min-h-[48px] w-full resize-none border-0 bg-transparent px-[4px] py-[4px] font-body text-[#1C1C1C] text-[15px] leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[#999] focus-visible:ring-0'
|
||||
style={{ caretColor: '#1C1C1C', maxHeight: `${MAX_HEIGHT}px` }}
|
||||
/>
|
||||
<div className='flex items-center justify-end'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleSubmit}
|
||||
disabled={isEmpty}
|
||||
className='flex h-[28px] w-[28px] items-center justify-center rounded-full border-0 p-0 transition-colors'
|
||||
style={{
|
||||
background: isEmpty ? '#C0C0C0' : '#1C1C1C',
|
||||
cursor: isEmpty ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
<ArrowUp size={16} strokeWidth={2.25} color='#FFFFFF' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 flex gap-[8px]'>
|
||||
<a
|
||||
href='https://docs.sim.ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={`${CTA_BUTTON} border-[#D4D4D4] text-[#1C1C1C] transition-colors hover:bg-[#E8E8E8]`}
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<Link
|
||||
href='/signup'
|
||||
className={`${CTA_BUTTON} gap-[8px] border-[#1C1C1C] bg-[#1C1C1C] text-white transition-colors hover:border-[#333] hover:bg-[#333]`}
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,171 +1,18 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { FooterCTA } from '@/app/(home)/components/footer/footer-cta'
|
||||
|
||||
const LINK_CLASS = 'text-[14px] text-[#999] transition-colors hover:text-[#ECECEC]'
|
||||
|
||||
interface FooterItem {
|
||||
label: string
|
||||
href: string
|
||||
external?: boolean
|
||||
}
|
||||
|
||||
const PRODUCT_LINKS: FooterItem[] = [
|
||||
{ label: 'Pricing', href: '/#pricing' },
|
||||
{ label: 'Enterprise', href: 'https://form.typeform.com/to/jqCO12pF', external: true },
|
||||
{ label: 'Self Hosting', href: 'https://docs.sim.ai/self-hosting', external: true },
|
||||
{ label: 'MCP', href: 'https://docs.sim.ai/mcp', external: true },
|
||||
{ label: 'Knowledge Base', href: 'https://docs.sim.ai/knowledgebase', external: true },
|
||||
{ label: 'Tables', href: 'https://docs.sim.ai/tables', external: true },
|
||||
{ label: 'API', href: 'https://docs.sim.ai/api-reference/getting-started', external: true },
|
||||
{ label: 'Status', href: 'https://status.sim.ai', external: true },
|
||||
]
|
||||
|
||||
const RESOURCES_LINKS: FooterItem[] = [
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
// { label: 'Templates', href: '/templates' },
|
||||
{ label: 'Docs', href: 'https://docs.sim.ai', external: true },
|
||||
{ label: 'Careers', href: 'https://jobs.ashbyhq.com/sim', external: true },
|
||||
{ label: 'Changelog', href: '/changelog' },
|
||||
]
|
||||
|
||||
const BLOCK_LINKS: FooterItem[] = [
|
||||
{ label: 'Agent', href: 'https://docs.sim.ai/blocks/agent', external: true },
|
||||
{ label: 'Router', href: 'https://docs.sim.ai/blocks/router', external: true },
|
||||
{ label: 'Function', href: 'https://docs.sim.ai/blocks/function', external: true },
|
||||
{ label: 'Condition', href: 'https://docs.sim.ai/blocks/condition', external: true },
|
||||
{ label: 'API', href: 'https://docs.sim.ai/blocks/api', external: true },
|
||||
{ label: 'Workflow', href: 'https://docs.sim.ai/blocks/workflow', external: true },
|
||||
{ label: 'Parallel', href: 'https://docs.sim.ai/blocks/parallel', external: true },
|
||||
{ label: 'Guardrails', href: 'https://docs.sim.ai/blocks/guardrails', external: true },
|
||||
{ label: 'Evaluator', href: 'https://docs.sim.ai/blocks/evaluator', external: true },
|
||||
{ label: 'Loop', href: 'https://docs.sim.ai/blocks/loop', external: true },
|
||||
]
|
||||
|
||||
const INTEGRATION_LINKS: FooterItem[] = [
|
||||
{ label: 'All Integrations →', href: '/integrations' },
|
||||
{ label: 'Confluence', href: 'https://docs.sim.ai/tools/confluence', external: true },
|
||||
{ label: 'Slack', href: 'https://docs.sim.ai/tools/slack', external: true },
|
||||
{ label: 'GitHub', href: 'https://docs.sim.ai/tools/github', external: true },
|
||||
{ label: 'Gmail', href: 'https://docs.sim.ai/tools/gmail', external: true },
|
||||
{ label: 'HubSpot', href: 'https://docs.sim.ai/tools/hubspot', external: true },
|
||||
{ label: 'Salesforce', href: 'https://docs.sim.ai/tools/salesforce', external: true },
|
||||
{ label: 'Notion', href: 'https://docs.sim.ai/tools/notion', external: true },
|
||||
{ label: 'Google Drive', href: 'https://docs.sim.ai/tools/google_drive', external: true },
|
||||
{ label: 'Google Sheets', href: 'https://docs.sim.ai/tools/google_sheets', external: true },
|
||||
{ label: 'Supabase', href: 'https://docs.sim.ai/tools/supabase', external: true },
|
||||
{ label: 'Stripe', href: 'https://docs.sim.ai/tools/stripe', external: true },
|
||||
{ label: 'Jira', href: 'https://docs.sim.ai/tools/jira', external: true },
|
||||
{ label: 'Linear', href: 'https://docs.sim.ai/tools/linear', external: true },
|
||||
{ label: 'Airtable', href: 'https://docs.sim.ai/tools/airtable', external: true },
|
||||
{ label: 'Firecrawl', href: 'https://docs.sim.ai/tools/firecrawl', external: true },
|
||||
{ label: 'Pinecone', href: 'https://docs.sim.ai/tools/pinecone', external: true },
|
||||
{ label: 'Discord', href: 'https://docs.sim.ai/tools/discord', external: true },
|
||||
{ label: 'Microsoft Teams', href: 'https://docs.sim.ai/tools/microsoft_teams', external: true },
|
||||
{ label: 'Outlook', href: 'https://docs.sim.ai/tools/outlook', external: true },
|
||||
{ label: 'Telegram', href: 'https://docs.sim.ai/tools/telegram', external: true },
|
||||
]
|
||||
|
||||
const SOCIAL_LINKS: FooterItem[] = [
|
||||
{ label: 'X (Twitter)', href: 'https://x.com/simdotai', external: true },
|
||||
{ label: 'LinkedIn', href: 'https://www.linkedin.com/company/simstudioai/', external: true },
|
||||
{ label: 'Discord', href: 'https://discord.gg/Hr4UWYEcTT', external: true },
|
||||
{ label: 'GitHub', href: 'https://github.com/simstudioai/sim', external: true },
|
||||
]
|
||||
|
||||
const LEGAL_LINKS: FooterItem[] = [
|
||||
{ label: 'Terms of Service', href: '/terms' },
|
||||
{ label: 'Privacy Policy', href: '/privacy' },
|
||||
]
|
||||
|
||||
function FooterColumn({ title, items }: { title: string; items: FooterItem[] }) {
|
||||
return (
|
||||
<div>
|
||||
<h3 className='mb-[16px] font-medium text-[#ECECEC] text-[14px]'>{title}</h3>
|
||||
<div className='flex flex-col gap-[10px]'>
|
||||
{items.map(({ label, href, external }) =>
|
||||
external ? (
|
||||
<a
|
||||
key={label}
|
||||
href={href}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={LINK_CLASS}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
) : (
|
||||
<Link key={label} href={href} className={LINK_CLASS}>
|
||||
{label}
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface FooterProps {
|
||||
hideCTA?: boolean
|
||||
}
|
||||
|
||||
export default function Footer({ hideCTA }: FooterProps) {
|
||||
return (
|
||||
<footer
|
||||
role='contentinfo'
|
||||
className={`bg-[#F6F6F6] pb-[40px] font-[430] font-season text-[14px]${hideCTA ? ' pt-[40px]' : ''}`}
|
||||
>
|
||||
{!hideCTA && <FooterCTA />}
|
||||
<div className='px-4 sm:px-8 md:px-[80px]'>
|
||||
<div className='relative overflow-hidden rounded-lg bg-[#1C1C1C] px-6 pt-[40px] pb-[32px] sm:px-10 sm:pt-[48px] sm:pb-[40px]'>
|
||||
<nav
|
||||
aria-label='Footer navigation'
|
||||
className='relative z-[1] grid grid-cols-2 gap-x-8 gap-y-10 sm:grid-cols-3 lg:grid-cols-7'
|
||||
>
|
||||
<div className='col-span-2 flex flex-col gap-[24px] sm:col-span-1'>
|
||||
<Link href='/' aria-label='Sim home'>
|
||||
<Image
|
||||
src='/logo/sim-landing.svg'
|
||||
alt='Sim'
|
||||
width={85}
|
||||
height={26}
|
||||
className='h-[26.4px] w-auto'
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<FooterColumn title='Product' items={PRODUCT_LINKS} />
|
||||
<FooterColumn title='Resources' items={RESOURCES_LINKS} />
|
||||
<FooterColumn title='Blocks' items={BLOCK_LINKS} />
|
||||
<FooterColumn title='Integrations' items={INTEGRATION_LINKS} />
|
||||
<FooterColumn title='Socials' items={SOCIAL_LINKS} />
|
||||
<FooterColumn title='Legal' items={LEGAL_LINKS} />
|
||||
</nav>
|
||||
|
||||
{/* <svg
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute bottom-0 left-[-60px] hidden w-[85%] sm:block'
|
||||
viewBox='0 0 1800 316'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
d='M18.3562 305V48.95A30.594 30.594 0 0 1 48.95 18.356H917.05A30.594 30.594 0 0 1 947.644 48.95V273H1768C1777.11 273 1784.5 280.387 1784.5 289.5C1784.5 298.613 1777.11 306 1768 306H96.8603C78.635 306 63.8604 310 63.8604 305H18.3562'
|
||||
stroke='#2A2A2A'
|
||||
strokeWidth='2'
|
||||
/>
|
||||
<rect
|
||||
x='58'
|
||||
y='58'
|
||||
width='849.288'
|
||||
height='199.288'
|
||||
rx='14'
|
||||
stroke='#2A2A2A'
|
||||
strokeWidth='2'
|
||||
/>
|
||||
</svg> */}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
/**
|
||||
* Landing page footer — navigation, legal links, and entity reinforcement.
|
||||
*
|
||||
* SEO:
|
||||
* - `<footer role="contentinfo">` with `<nav aria-label="Footer navigation">`.
|
||||
* - Link groups under semantic headings (`<h3>`). All links are `<Link>` or `<a>` with `href`.
|
||||
* - External links include `rel="noopener noreferrer"`.
|
||||
* - Legal links (Privacy, Terms) must be crawlable (trust signals).
|
||||
*
|
||||
* GEO:
|
||||
* - Include "Sim — Build AI agents and run your agentic workforce" as visible text (entity reinforcement).
|
||||
* - Social links (X, GitHub, LinkedIn, Discord) must match `sameAs` in structured-data.tsx.
|
||||
* - Link to all major pages: Docs, Pricing, Enterprise, Careers, Changelog (internal link graph).
|
||||
* - Display compliance badges (SOC2, HIPAA) and status page link as visible trust signals.
|
||||
*/
|
||||
export default function Footer() {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function Hero() {
|
||||
<section
|
||||
id='hero'
|
||||
aria-labelledby='hero-heading'
|
||||
className='relative flex flex-col items-center overflow-hidden bg-[#1C1C1C] pt-[60px] pb-[12px] lg:pt-[100px]'
|
||||
className='relative flex flex-col items-center overflow-hidden bg-[#1C1C1C] pt-[71px]'
|
||||
>
|
||||
<p className='sr-only'>
|
||||
Sim is the open-source platform to build AI agents and run your agentic workforce. Connect
|
||||
@@ -53,7 +53,7 @@ export default function Hero() {
|
||||
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-[-2.8vw] right-[-4vw] z-0 aspect-[471/470] w-[32.7vw]'
|
||||
className='pointer-events-none absolute top-[-2.8vw] right-[0vw] z-0 aspect-[471/470] w-[32.7vw]'
|
||||
>
|
||||
<Image src='/landing/card-right.svg' alt='' fill className='object-contain' />
|
||||
</div>
|
||||
@@ -61,27 +61,25 @@ export default function Hero() {
|
||||
<div className='relative z-10 flex flex-col items-center gap-[12px]'>
|
||||
<h1
|
||||
id='hero-heading'
|
||||
className='font-[430] font-season text-[36px] text-white leading-[100%] tracking-[-0.02em] sm:text-[48px] lg:text-[72px]'
|
||||
className='font-[430] font-season text-[64px] text-white leading-[100%] tracking-[-0.02em]'
|
||||
>
|
||||
Build AI Agents
|
||||
Build Agents
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[15px] leading-[125%] tracking-[0.02em] lg:text-[18px]'>
|
||||
Sim is the AI Workspace for Agent Builders.
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[16px] leading-[125%] tracking-[0.02em]'>
|
||||
Build and deploy agentic workflows
|
||||
</p>
|
||||
|
||||
<div className='mt-[12px] flex items-center gap-[8px]'>
|
||||
<a
|
||||
href='https://form.typeform.com/to/jqCO12pF'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
<Link
|
||||
href='/login'
|
||||
className={`${CTA_BASE} border-[#3d3d3d] text-[#ECECEC] transition-colors hover:bg-[#2A2A2A]`}
|
||||
aria-label='Get a demo'
|
||||
aria-label='Log in'
|
||||
>
|
||||
Get a demo
|
||||
</a>
|
||||
Log in
|
||||
</Link>
|
||||
<Link
|
||||
href='/signup'
|
||||
className={`${CTA_BASE} gap-[8px] border-[#FFFFFF] bg-[#FFFFFF] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]`}
|
||||
className={`${CTA_BASE} gap-[8px] border-[#33C482] bg-[#33C482] text-black transition-[filter] hover:brightness-110`}
|
||||
aria-label='Get started with Sim'
|
||||
>
|
||||
Get started
|
||||
@@ -103,7 +101,7 @@ export default function Hero() {
|
||||
<BlocksTopLeftAnimated animState={blockStates.topLeft} />
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 mx-auto mt-[3.2vw] w-[78.9vw] px-[1.4vw]'>
|
||||
<div className='relative z-10 mx-auto mt-[2.4vw] w-[78.9vw] px-[1.4vw]'>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='-translate-y-1/2 pointer-events-none absolute top-[50%] right-[calc(100%-1.41vw)] z-20 w-[calc(16px_+_1.25vw)] max-w-[34px]'
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { memo, useCallback, useRef, useState } from 'react'
|
||||
import { ArrowUp } from 'lucide-react'
|
||||
import { useLandingSubmit } from '@/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
|
||||
import { useAnimatedPlaceholder } from '@/app/workspace/[workspaceId]/home/hooks/use-animated-placeholder'
|
||||
|
||||
const C = {
|
||||
SURFACE: '#292929',
|
||||
BORDER: '#3d3d3d',
|
||||
TEXT_PRIMARY: '#e6e6e6',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Landing preview replica of the workspace Home initial view.
|
||||
* Shows a greeting heading and a minimal chat input (no + or mic).
|
||||
* On submit, stores the prompt and redirects to /signup.
|
||||
*/
|
||||
export const LandingPreviewHome = memo(function LandingPreviewHome() {
|
||||
const landingSubmit = useLandingSubmit()
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const animatedPlaceholder = useAnimatedPlaceholder()
|
||||
|
||||
const isEmpty = inputValue.trim().length === 0
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (isEmpty) return
|
||||
landingSubmit(inputValue)
|
||||
}, [isEmpty, inputValue, landingSubmit])
|
||||
|
||||
const MAX_HEIGHT = 200
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSubmit()
|
||||
}
|
||||
},
|
||||
[handleSubmit]
|
||||
)
|
||||
|
||||
const handleInput = useCallback((e: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
const target = e.target as HTMLTextAreaElement
|
||||
target.style.height = 'auto'
|
||||
target.style.height = `${Math.min(target.scrollHeight, MAX_HEIGHT)}px`
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 flex-1 flex-col items-center justify-center px-[24px] pb-[2vh]'>
|
||||
<h1
|
||||
className='mb-[24px] max-w-[42rem] font-[430] font-season text-[32px] tracking-[-0.02em]'
|
||||
style={{ color: C.TEXT_PRIMARY }}
|
||||
>
|
||||
What should we get done?
|
||||
</h1>
|
||||
|
||||
<div className='w-full max-w-[32rem]'>
|
||||
<div
|
||||
className='cursor-text rounded-[20px] border px-[10px] py-[8px]'
|
||||
style={{ borderColor: C.BORDER, backgroundColor: C.SURFACE }}
|
||||
onClick={() => textareaRef.current?.focus()}
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onInput={handleInput}
|
||||
placeholder={animatedPlaceholder}
|
||||
rows={1}
|
||||
className='m-0 box-border min-h-[24px] w-full resize-none overflow-y-auto border-0 bg-transparent px-[4px] py-[4px] font-body text-[15px] leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[#787878] focus-visible:ring-0'
|
||||
style={{
|
||||
color: C.TEXT_PRIMARY,
|
||||
caretColor: C.TEXT_PRIMARY,
|
||||
maxHeight: `${MAX_HEIGHT}px`,
|
||||
}}
|
||||
/>
|
||||
<div className='flex items-center justify-end'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleSubmit}
|
||||
disabled={isEmpty}
|
||||
className='flex h-[28px] w-[28px] items-center justify-center rounded-full border-0 p-0 transition-colors'
|
||||
style={{
|
||||
background: isEmpty ? '#808080' : '#e0e0e0',
|
||||
cursor: isEmpty ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
<ArrowUp size={16} strokeWidth={2.25} color='#1b1b1b' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -8,23 +8,6 @@ import { createPortal } from 'react-dom'
|
||||
import { BubbleChatPreview, ChevronDown, MoreHorizontal, Play } from '@/components/emcn'
|
||||
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
|
||||
|
||||
/**
|
||||
* Stores the prompt in browser storage and redirects to /signup.
|
||||
* Shared by both the copilot panel and the landing home view.
|
||||
*/
|
||||
export function useLandingSubmit() {
|
||||
const router = useRouter()
|
||||
return useCallback(
|
||||
(text: string) => {
|
||||
const trimmed = text.trim()
|
||||
if (!trimmed) return
|
||||
LandingPromptStorage.store(trimmed)
|
||||
router.push('/signup')
|
||||
},
|
||||
[router]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight static panel replicating the real workspace panel styling.
|
||||
* The copilot tab is active with a functional user input.
|
||||
@@ -35,7 +18,7 @@ export function useLandingSubmit() {
|
||||
* inside Content > Copilot > header-bar(mx-[-1px]) > UserInput(p-8)
|
||||
*/
|
||||
export const LandingPreviewPanel = memo(function LandingPreviewPanel() {
|
||||
const landingSubmit = useLandingSubmit()
|
||||
const router = useRouter()
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const [cursorPos, setCursorPos] = useState<{ x: number; y: number } | null>(null)
|
||||
@@ -44,8 +27,9 @@ export const LandingPreviewPanel = memo(function LandingPreviewPanel() {
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (isEmpty) return
|
||||
landingSubmit(inputValue)
|
||||
}, [isEmpty, inputValue, landingSubmit])
|
||||
LandingPromptStorage.store(inputValue)
|
||||
router.push('/signup')
|
||||
}, [isEmpty, inputValue, router])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
@@ -76,10 +60,10 @@ export const LandingPreviewPanel = memo(function LandingPreviewPanel() {
|
||||
onMouseMove={(e) => setCursorPos({ x: e.clientX, y: e.clientY })}
|
||||
onMouseLeave={() => setCursorPos(null)}
|
||||
>
|
||||
<div className='flex h-[30px] items-center rounded-[5px] bg-[#33C482] px-[10px] transition-colors hover:bg-[#2DAC72]'>
|
||||
<div className='flex h-[30px] items-center rounded-[5px] bg-[#32bd7e] px-[10px] transition-[filter] hover:brightness-110'>
|
||||
<span className='font-medium text-[#1b1b1b] text-[12px]'>Deploy</span>
|
||||
</div>
|
||||
<div className='flex h-[30px] items-center gap-[8px] rounded-[5px] bg-[#33C482] px-[10px] transition-colors hover:bg-[#2DAC72]'>
|
||||
<div className='flex h-[30px] items-center gap-[8px] rounded-[5px] bg-[#32bd7e] px-[10px] transition-[filter] hover:brightness-110'>
|
||||
<Play className='h-[11.5px] w-[11.5px] text-[#1b1b1b]' />
|
||||
<span className='font-medium text-[#1b1b1b] text-[12px]'>Run</span>
|
||||
</div>
|
||||
|
||||
@@ -1,204 +1,141 @@
|
||||
'use client'
|
||||
|
||||
import { ChevronDown, Home, Library } from '@/components/emcn'
|
||||
import {
|
||||
Calendar,
|
||||
Database,
|
||||
File,
|
||||
HelpCircle,
|
||||
Search,
|
||||
Settings,
|
||||
Table,
|
||||
} from '@/components/emcn/icons'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Database, Layout, Search, Settings } from 'lucide-react'
|
||||
import { ChevronDown, Library } from '@/components/emcn'
|
||||
import type { PreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
|
||||
|
||||
/**
|
||||
* Props for the LandingPreviewSidebar component
|
||||
*/
|
||||
interface LandingPreviewSidebarProps {
|
||||
workflows: PreviewWorkflow[]
|
||||
activeWorkflowId: string
|
||||
activeView: 'home' | 'workflow'
|
||||
onSelectWorkflow: (id: string) => void
|
||||
onSelectHome: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Hardcoded dark-theme equivalents of the real sidebar CSS variables.
|
||||
* The preview lives inside a `dark` wrapper but CSS variable cascade
|
||||
* isn't guaranteed, so we pin the hex values directly.
|
||||
* Static footer navigation items matching the real sidebar
|
||||
*/
|
||||
const C = {
|
||||
SURFACE_1: '#1e1e1e',
|
||||
SURFACE_2: '#252525',
|
||||
SURFACE_ACTIVE: '#363636',
|
||||
BORDER: '#2c2c2c',
|
||||
TEXT_PRIMARY: '#e6e6e6',
|
||||
TEXT_BODY: '#cdcdcd',
|
||||
TEXT_ICON: '#939393',
|
||||
BRAND: '#33C482',
|
||||
} as const
|
||||
|
||||
const WORKSPACE_NAV = [
|
||||
{ id: 'tables', label: 'Tables', icon: Table },
|
||||
{ id: 'files', label: 'Files', icon: File },
|
||||
{ id: 'knowledge-base', label: 'Knowledge Base', icon: Database },
|
||||
{ id: 'scheduled-tasks', label: 'Scheduled Tasks', icon: Calendar },
|
||||
const FOOTER_NAV_ITEMS = [
|
||||
{ id: 'logs', label: 'Logs', icon: Library },
|
||||
] as const
|
||||
|
||||
const FOOTER_NAV = [
|
||||
{ id: 'help', label: 'Help', icon: HelpCircle },
|
||||
{ id: 'templates', label: 'Templates', icon: Layout },
|
||||
{ id: 'knowledge-base', label: 'Knowledge Base', icon: Database },
|
||||
{ id: 'settings', label: 'Settings', icon: Settings },
|
||||
] as const
|
||||
|
||||
function StaticNavItem({
|
||||
icon: Icon,
|
||||
label,
|
||||
}: {
|
||||
icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>
|
||||
label: string
|
||||
}) {
|
||||
return (
|
||||
<div className='pointer-events-none mx-[2px] flex h-[28px] items-center gap-[8px] rounded-[8px] px-[8px]'>
|
||||
<Icon className='h-[14px] w-[14px] flex-shrink-0' style={{ color: C.TEXT_ICON }} />
|
||||
<span className='truncate text-[13px]' style={{ color: C.TEXT_BODY, fontWeight: 450 }}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight sidebar replicating the real workspace sidebar layout and sizing.
|
||||
* Starts from the workspace header (no logo/collapse row).
|
||||
* Lightweight static sidebar replicating the real workspace sidebar styling.
|
||||
* Only workflow items are interactive — everything else is pointer-events-none.
|
||||
*
|
||||
* Colors sourced from the dark theme CSS variables:
|
||||
* --surface-1: #1e1e1e, --surface-5: #363636, --border: #2c2c2c, --border-1: #3d3d3d
|
||||
* --text-primary: #e6e6e6, --text-tertiary: #b3b3b3, --text-muted: #787878
|
||||
*/
|
||||
export function LandingPreviewSidebar({
|
||||
workflows,
|
||||
activeWorkflowId,
|
||||
activeView,
|
||||
onSelectWorkflow,
|
||||
onSelectHome,
|
||||
}: LandingPreviewSidebarProps) {
|
||||
const isHomeActive = activeView === 'home'
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
setIsDropdownOpen((prev) => !prev)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDropdownOpen) return
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
||||
setIsDropdownOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [isDropdownOpen])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='flex h-full w-[248px] flex-shrink-0 flex-col pt-[12px]'
|
||||
style={{ backgroundColor: C.SURFACE_1 }}
|
||||
>
|
||||
{/* Workspace Header */}
|
||||
<div className='flex-shrink-0 px-[10px]'>
|
||||
<div
|
||||
className='pointer-events-none flex h-[32px] w-full items-center gap-[8px] rounded-[8px] border pr-[8px] pl-[5px]'
|
||||
style={{ borderColor: C.BORDER, backgroundColor: C.SURFACE_2 }}
|
||||
>
|
||||
<div className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-[4px] bg-white'>
|
||||
<svg width='10' height='10' viewBox='0 0 10 10' fill='none'>
|
||||
<path
|
||||
d='M1 9C1 4.58 4.58 1 9 1'
|
||||
stroke='#1e1e1e'
|
||||
strokeWidth='1.8'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
className='min-w-0 flex-1 truncate text-left font-medium text-[13px]'
|
||||
style={{ color: C.TEXT_PRIMARY }}
|
||||
<div className='flex h-full w-[220px] flex-shrink-0 flex-col border-[#2c2c2c] border-r bg-[#1e1e1e]'>
|
||||
{/* Header */}
|
||||
<div className='relative flex-shrink-0 px-[14px] pt-[12px]' ref={dropdownRef}>
|
||||
<div className='flex items-center justify-between'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleToggle}
|
||||
className='group -mx-[6px] flex cursor-pointer items-center gap-[8px] rounded-[6px] bg-transparent px-[6px] py-[4px] transition-colors hover:bg-[#363636]'
|
||||
>
|
||||
Superark
|
||||
</span>
|
||||
<ChevronDown className='h-[8px] w-[10px] flex-shrink-0' style={{ color: C.TEXT_ICON }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Navigation: Home (interactive), Search (static) */}
|
||||
<div className='mt-[10px] flex flex-shrink-0 flex-col gap-[2px] px-[8px]'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={onSelectHome}
|
||||
className='mx-[2px] flex h-[28px] items-center gap-[8px] rounded-[8px] px-[8px] transition-colors'
|
||||
style={{ backgroundColor: isHomeActive ? C.SURFACE_ACTIVE : 'transparent' }}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isHomeActive) e.currentTarget.style.backgroundColor = C.SURFACE_ACTIVE
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isHomeActive) e.currentTarget.style.backgroundColor = 'transparent'
|
||||
}}
|
||||
>
|
||||
<Home className='h-[14px] w-[14px] flex-shrink-0' style={{ color: C.TEXT_ICON }} />
|
||||
<span className='truncate text-[13px]' style={{ color: C.TEXT_BODY, fontWeight: 450 }}>
|
||||
Home
|
||||
</span>
|
||||
</button>
|
||||
<StaticNavItem icon={Search} label='Search' />
|
||||
</div>
|
||||
|
||||
{/* Workspace */}
|
||||
<div className='mt-[14px] flex flex-shrink-0 flex-col'>
|
||||
<div className='px-[16px] pb-[6px]'>
|
||||
<div className='font-base text-[12px]' style={{ color: C.TEXT_ICON }}>
|
||||
Workspace
|
||||
<span className='truncate font-base text-[#e6e6e6] text-[14px]'>My Workspace</span>
|
||||
<ChevronDown
|
||||
className={`h-[8px] w-[10px] flex-shrink-0 text-[#787878] transition-all duration-100 group-hover:text-[#cccccc] ${isDropdownOpen ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
<div className='pointer-events-none flex flex-shrink-0 items-center'>
|
||||
<Search className='h-[14px] w-[14px] text-[#787878]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-[2px] px-[8px]'>
|
||||
{WORKSPACE_NAV.map((item) => (
|
||||
<StaticNavItem key={item.id} icon={item.icon} label={item.label} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scrollable Tasks + Workflows */}
|
||||
<div className='flex flex-1 flex-col overflow-y-auto overflow-x-hidden pt-[14px]'>
|
||||
{/* Workflows */}
|
||||
<div className='flex flex-col'>
|
||||
<div className='px-[16px]'>
|
||||
<div className='font-base text-[12px]' style={{ color: C.TEXT_ICON }}>
|
||||
Workflows
|
||||
{/* Workspace switcher dropdown */}
|
||||
{isDropdownOpen && (
|
||||
<div className='absolute top-[42px] left-[8px] z-50 min-w-[160px] max-w-[160px] rounded-[6px] bg-[#242424] px-[6px] py-[6px] shadow-lg'>
|
||||
<div
|
||||
className='flex h-[26px] cursor-pointer items-center gap-[8px] rounded-[6px] bg-[#3d3d3d] px-[6px] font-base text-[#e6e6e6] text-[13px]'
|
||||
role='menuitem'
|
||||
onClick={() => setIsDropdownOpen(false)}
|
||||
>
|
||||
<span className='min-w-0 flex-1 truncate'>My Workspace</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-[6px] flex flex-col gap-[2px] px-[8px]'>
|
||||
{workflows.map((workflow) => {
|
||||
const isActive = activeView === 'workflow' && workflow.id === activeWorkflowId
|
||||
return (
|
||||
<button
|
||||
key={workflow.id}
|
||||
type='button'
|
||||
onClick={() => onSelectWorkflow(workflow.id)}
|
||||
className='group mx-[2px] flex h-[28px] w-full items-center gap-[8px] rounded-[8px] px-[8px] transition-colors'
|
||||
style={{ backgroundColor: isActive ? C.SURFACE_ACTIVE : 'transparent' }}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive) e.currentTarget.style.backgroundColor = C.SURFACE_ACTIVE
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive) e.currentTarget.style.backgroundColor = 'transparent'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='h-[14px] w-[14px] flex-shrink-0 rounded-[4px] border-[2.5px]'
|
||||
style={{
|
||||
backgroundColor: workflow.color,
|
||||
borderColor: `${workflow.color}60`,
|
||||
backgroundClip: 'padding-box',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className='min-w-0 flex-1 truncate text-left text-[13px]'
|
||||
style={{ color: C.TEXT_BODY, fontWeight: 450 }}
|
||||
>
|
||||
{workflow.name}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className='flex flex-shrink-0 flex-col gap-[2px] px-[8px] pt-[9px] pb-[8px]'>
|
||||
{FOOTER_NAV.map((item) => (
|
||||
<StaticNavItem key={item.id} icon={item.icon} label={item.label} />
|
||||
))}
|
||||
{/* Workflow items */}
|
||||
<div className='mt-[8px] space-y-[2px] overflow-x-hidden px-[8px]'>
|
||||
{workflows.map((workflow) => {
|
||||
const isActive = workflow.id === activeWorkflowId
|
||||
return (
|
||||
<button
|
||||
key={workflow.id}
|
||||
type='button'
|
||||
onClick={() => onSelectWorkflow(workflow.id)}
|
||||
className={`group flex h-[26px] w-full items-center gap-[8px] rounded-[8px] px-[6px] text-[14px] transition-colors ${
|
||||
isActive ? 'bg-[#363636]' : 'bg-transparent hover:bg-[#363636]'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className='h-[14px] w-[14px] flex-shrink-0 rounded-[4px]'
|
||||
style={{ backgroundColor: workflow.color }}
|
||||
/>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div
|
||||
className={`min-w-0 truncate text-left font-medium ${
|
||||
isActive ? 'text-[#e6e6e6]' : 'text-[#b3b3b3] group-hover:text-[#e6e6e6]'
|
||||
}`}
|
||||
>
|
||||
{workflow.name}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Footer navigation — static */}
|
||||
<div className='pointer-events-none mt-auto flex flex-shrink-0 flex-col gap-[2px] border-[#2c2c2c] border-t px-[7.75px] pt-[8px] pb-[8px]'>
|
||||
{FOOTER_NAV_ITEMS.map((item) => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className='flex h-[26px] items-center gap-[8px] rounded-[8px] px-[6px] text-[14px]'
|
||||
>
|
||||
<Icon className='h-[14px] w-[14px] flex-shrink-0 text-[#b3b3b3]' />
|
||||
<span className='truncate font-medium text-[#b3b3b3] text-[13px]'>{item.label}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ import { memo } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Database } from 'lucide-react'
|
||||
import { Handle, type NodeProps, Position } from 'reactflow'
|
||||
import { Blimp } from '@/components/emcn'
|
||||
import {
|
||||
AgentIcon,
|
||||
AnthropicIcon,
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
GmailIcon,
|
||||
GoogleCalendarIcon,
|
||||
GoogleSheetsIcon,
|
||||
HubspotIcon,
|
||||
JiraIcon,
|
||||
LinearIcon,
|
||||
LinkedInIcon,
|
||||
@@ -23,7 +21,6 @@ import {
|
||||
OpenAIIcon,
|
||||
RedditIcon,
|
||||
ReductoIcon,
|
||||
SalesforceIcon,
|
||||
ScheduleIcon,
|
||||
SlackIcon,
|
||||
StartIcon,
|
||||
@@ -59,16 +56,13 @@ const BLOCK_ICONS: Record<string, React.ComponentType<{ className?: string }>> =
|
||||
google_calendar: GoogleCalendarIcon,
|
||||
gmail: GmailIcon,
|
||||
google_sheets: GoogleSheetsIcon,
|
||||
hubspot: HubspotIcon,
|
||||
linear: LinearIcon,
|
||||
firecrawl: FirecrawlIcon,
|
||||
reddit: RedditIcon,
|
||||
notion: NotionIcon,
|
||||
reducto: ReductoIcon,
|
||||
salesforce: SalesforceIcon,
|
||||
textract: TextractIcon,
|
||||
linkedin: LinkedInIcon,
|
||||
mothership: Blimp,
|
||||
}
|
||||
|
||||
/** Model prefix → provider icon for the "Model" row in agent blocks. */
|
||||
|
||||
@@ -91,11 +91,11 @@ const IT_SERVICE_WORKFLOW: PreviewWorkflow = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-healing CRM workflow — Schedule -> Mothership
|
||||
* Content pipeline workflow — Schedule -> Agent (X + YouTube tools)
|
||||
*/
|
||||
const SELF_HEALING_CRM_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'wf-self-healing-crm',
|
||||
name: 'Self-healing CRM',
|
||||
const CONTENT_PIPELINE_WORKFLOW: PreviewWorkflow = {
|
||||
id: 'wf-content-pipeline',
|
||||
name: 'Content Pipeline',
|
||||
color: '#33C482',
|
||||
blocks: [
|
||||
{
|
||||
@@ -111,20 +111,23 @@ const SELF_HEALING_CRM_WORKFLOW: PreviewWorkflow = {
|
||||
hideTargetHandle: true,
|
||||
},
|
||||
{
|
||||
id: 'mothership-1',
|
||||
name: 'CRM Agent',
|
||||
type: 'mothership',
|
||||
bgColor: '#33C482',
|
||||
rows: [{ title: 'Prompt', value: 'Audit CRM records, fix...' }],
|
||||
id: 'agent-2',
|
||||
name: 'Agent',
|
||||
type: 'agent',
|
||||
bgColor: '#701ffc',
|
||||
rows: [
|
||||
{ title: 'Model', value: 'grok-4' },
|
||||
{ title: 'System Prompt', value: 'Repurpose trending...' },
|
||||
],
|
||||
tools: [
|
||||
{ name: 'HubSpot', type: 'hubspot', bgColor: '#FF7A59' },
|
||||
{ name: 'Salesforce', type: 'salesforce', bgColor: '#E0E0E0' },
|
||||
{ name: 'X', type: 'x', bgColor: '#000000' },
|
||||
{ name: 'YouTube', type: 'youtube', bgColor: '#FF0000' },
|
||||
],
|
||||
position: { x: 420, y: 180 },
|
||||
hideSourceHandle: true,
|
||||
},
|
||||
],
|
||||
edges: [{ id: 'e-3', source: 'schedule-1', target: 'mothership-1' }],
|
||||
edges: [{ id: 'e-3', source: 'schedule-1', target: 'agent-2' }],
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,7 +154,7 @@ const NEW_AGENT_WORKFLOW: PreviewWorkflow = {
|
||||
}
|
||||
|
||||
export const PREVIEW_WORKFLOWS: PreviewWorkflow[] = [
|
||||
SELF_HEALING_CRM_WORKFLOW,
|
||||
CONTENT_PIPELINE_WORKFLOW,
|
||||
IT_SERVICE_WORKFLOW,
|
||||
NEW_AGENT_WORKFLOW,
|
||||
]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { motion, type Variants } from 'framer-motion'
|
||||
import { LandingPreviewHome } from '@/app/(home)/components/landing-preview/components/landing-preview-home/landing-preview-home'
|
||||
import { LandingPreviewPanel } from '@/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
|
||||
import { LandingPreviewSidebar } from '@/app/(home)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar'
|
||||
import { LandingPreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/landing-preview-workflow'
|
||||
@@ -57,7 +56,6 @@ const panelVariants: Variants = {
|
||||
* load — workflow switches render instantly.
|
||||
*/
|
||||
export function LandingPreview() {
|
||||
const [activeView, setActiveView] = useState<'home' | 'workflow'>('workflow')
|
||||
const [activeWorkflowId, setActiveWorkflowId] = useState(PREVIEW_WORKFLOWS[0].id)
|
||||
const isInitialMount = useRef(true)
|
||||
|
||||
@@ -65,23 +63,12 @@ export function LandingPreview() {
|
||||
isInitialMount.current = false
|
||||
}, [])
|
||||
|
||||
const handleSelectWorkflow = useCallback((id: string) => {
|
||||
setActiveWorkflowId(id)
|
||||
setActiveView('workflow')
|
||||
}, [])
|
||||
|
||||
const handleSelectHome = useCallback(() => {
|
||||
setActiveView('home')
|
||||
}, [])
|
||||
|
||||
const activeWorkflow =
|
||||
PREVIEW_WORKFLOWS.find((w) => w.id === activeWorkflowId) ?? PREVIEW_WORKFLOWS[0]
|
||||
|
||||
const isWorkflowView = activeView === 'workflow'
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className='dark flex aspect-[1116/549] w-full overflow-hidden rounded bg-[#1e1e1e] antialiased'
|
||||
className='dark flex aspect-[1116/549] w-full overflow-hidden rounded bg-[#1b1b1b] antialiased'
|
||||
initial='hidden'
|
||||
animate='visible'
|
||||
variants={containerVariants}
|
||||
@@ -90,34 +77,15 @@ export function LandingPreview() {
|
||||
<LandingPreviewSidebar
|
||||
workflows={PREVIEW_WORKFLOWS}
|
||||
activeWorkflowId={activeWorkflowId}
|
||||
activeView={activeView}
|
||||
onSelectWorkflow={handleSelectWorkflow}
|
||||
onSelectHome={handleSelectHome}
|
||||
onSelectWorkflow={setActiveWorkflowId}
|
||||
/>
|
||||
</motion.div>
|
||||
<div className='flex min-w-0 flex-1 flex-col py-[8px] pr-[8px] pl-[8px] lg:pl-0'>
|
||||
<div className='flex flex-1 overflow-hidden rounded-[8px] border border-[#2c2c2c] bg-[#1b1b1b]'>
|
||||
<div
|
||||
className={
|
||||
isWorkflowView
|
||||
? 'relative min-w-0 flex-1 overflow-hidden'
|
||||
: 'relative flex min-w-0 flex-1 flex-col overflow-hidden'
|
||||
}
|
||||
>
|
||||
{isWorkflowView ? (
|
||||
<LandingPreviewWorkflow workflow={activeWorkflow} animate={isInitialMount.current} />
|
||||
) : (
|
||||
<LandingPreviewHome />
|
||||
)}
|
||||
</div>
|
||||
<motion.div
|
||||
className={isWorkflowView ? 'hidden lg:flex' : 'hidden'}
|
||||
variants={panelVariants}
|
||||
>
|
||||
<LandingPreviewPanel />
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className='relative flex-1 overflow-hidden'>
|
||||
<LandingPreviewWorkflow workflow={activeWorkflow} animate={isInitialMount.current} />
|
||||
</div>
|
||||
<motion.div className='hidden lg:flex' variants={panelVariants}>
|
||||
<LandingPreviewPanel />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
export interface NavBlogPost {
|
||||
slug: string
|
||||
title: string
|
||||
ogImage: string
|
||||
}
|
||||
|
||||
function BlogCard({
|
||||
slug,
|
||||
image,
|
||||
title,
|
||||
imageHeight,
|
||||
titleSize = '12px',
|
||||
className,
|
||||
}: {
|
||||
slug: string
|
||||
image: string
|
||||
title: string
|
||||
imageHeight: string
|
||||
titleSize?: string
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={`/blog/${slug}`}
|
||||
className={cn(
|
||||
'group/card flex flex-col overflow-hidden rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] transition-colors hover:border-[#3D3D3D] hover:bg-[#2A2A2A]',
|
||||
className
|
||||
)}
|
||||
prefetch={false}
|
||||
>
|
||||
<div className='w-full overflow-hidden bg-[#141414]' style={{ height: imageHeight }}>
|
||||
<img
|
||||
src={image}
|
||||
alt={title}
|
||||
decoding='async'
|
||||
className='h-full w-full object-cover transition-transform duration-200 group-hover/card:scale-[1.02]'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex-shrink-0 px-[10px] py-[6px]'>
|
||||
<span
|
||||
className='font-[430] font-season text-[#cdcdcd] leading-[140%]'
|
||||
style={{ fontSize: titleSize }}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
interface BlogDropdownProps {
|
||||
posts: NavBlogPost[]
|
||||
}
|
||||
|
||||
export function BlogDropdown({ posts }: BlogDropdownProps) {
|
||||
const [featured, ...rest] = posts
|
||||
|
||||
if (!featured) return null
|
||||
|
||||
return (
|
||||
<div className='w-[560px] rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] p-[16px] shadow-[0_16px_48px_rgba(0,0,0,0.4)]'>
|
||||
<div className='grid grid-cols-3 gap-[8px]'>
|
||||
<BlogCard
|
||||
slug={featured.slug}
|
||||
image={featured.ogImage}
|
||||
title={featured.title}
|
||||
imageHeight='190px'
|
||||
titleSize='13px'
|
||||
className='col-span-2 row-span-2'
|
||||
/>
|
||||
|
||||
{rest.map((post) => (
|
||||
<BlogCard
|
||||
key={post.slug}
|
||||
slug={post.slug}
|
||||
image={post.ogImage}
|
||||
title={post.title}
|
||||
imageHeight='72px'
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { AgentIcon, GithubOutlineIcon, McpIcon } from '@/components/icons'
|
||||
|
||||
const PREVIEW_CARDS = [
|
||||
{
|
||||
title: 'Introduction',
|
||||
href: 'https://docs.sim.ai',
|
||||
image: '/landing/docs-getting-started.svg',
|
||||
},
|
||||
{
|
||||
title: 'Getting Started',
|
||||
href: 'https://docs.sim.ai/getting-started',
|
||||
image: '/landing/docs-intro.svg',
|
||||
},
|
||||
] as const
|
||||
|
||||
const RESOURCE_CARDS = [
|
||||
{
|
||||
title: 'Agent',
|
||||
description: 'Build AI agents',
|
||||
href: 'https://docs.sim.ai/blocks/agent',
|
||||
icon: AgentIcon,
|
||||
},
|
||||
{
|
||||
title: 'MCP',
|
||||
description: 'Connect tools',
|
||||
href: 'https://docs.sim.ai/mcp',
|
||||
icon: McpIcon,
|
||||
},
|
||||
{
|
||||
title: 'Self-hosting',
|
||||
description: 'Host on your infra',
|
||||
href: 'https://docs.sim.ai/self-hosting',
|
||||
icon: GithubOutlineIcon,
|
||||
},
|
||||
] as const
|
||||
|
||||
export function DocsDropdown() {
|
||||
return (
|
||||
<div className='w-[480px] rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] p-[16px] shadow-[0_16px_48px_rgba(0,0,0,0.4)]'>
|
||||
<div className='grid grid-cols-2 gap-[10px]'>
|
||||
{PREVIEW_CARDS.map((card) => (
|
||||
<a
|
||||
key={card.title}
|
||||
href={card.href}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='group/card overflow-hidden rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] transition-colors hover:border-[#3D3D3D] hover:bg-[#2A2A2A]'
|
||||
>
|
||||
<div className='h-[120px] w-full overflow-hidden bg-[#141414]'>
|
||||
<img
|
||||
src={card.image}
|
||||
alt={card.title}
|
||||
decoding='async'
|
||||
className='h-full w-full scale-[1.04] object-cover transition-transform duration-200 group-hover/card:scale-[1.06]'
|
||||
/>
|
||||
</div>
|
||||
<div className='px-[10px] py-[8px]'>
|
||||
<span className='font-[430] font-season text-[#cdcdcd] text-[13px]'>
|
||||
{card.title}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='mt-[8px] grid grid-cols-3 gap-[8px]'>
|
||||
{RESOURCE_CARDS.map((card) => {
|
||||
const Icon = card.icon
|
||||
return (
|
||||
<a
|
||||
key={card.title}
|
||||
href={card.href}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex flex-col gap-[4px] rounded-[5px] border border-[#2A2A2A] px-[10px] py-[8px] transition-colors hover:border-[#3D3D3D] hover:bg-[#232323]'
|
||||
>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<Icon className='h-[13px] w-[13px] flex-shrink-0 text-[#939393]' />
|
||||
<span className='font-[430] font-season text-[#cdcdcd] text-[12px]'>
|
||||
{card.title}
|
||||
</span>
|
||||
</div>
|
||||
<span className='font-season text-[#939393] text-[11px] leading-[130%]'>
|
||||
{card.description}
|
||||
</span>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,389 +1,97 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { GithubOutlineIcon } from '@/components/icons'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import {
|
||||
BlogDropdown,
|
||||
type NavBlogPost,
|
||||
} from '@/app/(home)/components/navbar/components/blog-dropdown'
|
||||
import { DocsDropdown } from '@/app/(home)/components/navbar/components/docs-dropdown'
|
||||
import { ChevronDown } from '@/components/emcn'
|
||||
import { GitHubStars } from '@/app/(home)/components/navbar/components/github-stars'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
type DropdownId = 'docs' | 'blog' | null
|
||||
|
||||
interface NavLink {
|
||||
label: string
|
||||
href: string
|
||||
external?: boolean
|
||||
icon?: 'chevron'
|
||||
dropdown?: 'docs' | 'blog'
|
||||
}
|
||||
|
||||
const NAV_LINKS: NavLink[] = [
|
||||
{ label: 'Docs', href: 'https://docs.sim.ai', external: true, icon: 'chevron', dropdown: 'docs' },
|
||||
{ label: 'Blog', href: '/blog', icon: 'chevron', dropdown: 'blog' },
|
||||
{ label: 'Pricing', href: '/#pricing' },
|
||||
{ label: 'Enterprise', href: 'https://form.typeform.com/to/jqCO12pF', external: true },
|
||||
{ label: 'Docs', href: 'https://docs.sim.ai', external: true },
|
||||
{ label: 'Pricing', href: '/pricing' },
|
||||
{ label: 'Careers', href: '/careers' },
|
||||
{ label: 'Enterprise', href: '/enterprise' },
|
||||
]
|
||||
|
||||
const LOGO_CELL = 'flex items-center pl-[20px] lg:pl-[80px] pr-[20px]'
|
||||
/** Logo and nav edge: horizontal padding (px) for left/right symmetry. */
|
||||
const LOGO_CELL = 'flex items-center px-[20px]'
|
||||
|
||||
/** Links: even spacing between items. */
|
||||
const LINK_CELL = 'flex items-center px-[14px]'
|
||||
|
||||
interface NavbarProps {
|
||||
logoOnly?: boolean
|
||||
blogPosts?: NavBlogPost[]
|
||||
}
|
||||
|
||||
export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps) {
|
||||
const brand = getBrandConfig()
|
||||
const [activeDropdown, setActiveDropdown] = useState<DropdownId>(null)
|
||||
const [hoveredLink, setHoveredLink] = useState<string | null>(null)
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const openDropdown = useCallback((id: DropdownId) => {
|
||||
if (closeTimerRef.current) {
|
||||
clearTimeout(closeTimerRef.current)
|
||||
closeTimerRef.current = null
|
||||
}
|
||||
setActiveDropdown(id)
|
||||
}, [])
|
||||
|
||||
const scheduleClose = useCallback(() => {
|
||||
if (closeTimerRef.current) clearTimeout(closeTimerRef.current)
|
||||
closeTimerRef.current = setTimeout(() => {
|
||||
setActiveDropdown(null)
|
||||
closeTimerRef.current = null
|
||||
}, 100)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (closeTimerRef.current) clearTimeout(closeTimerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = mobileMenuOpen ? 'hidden' : ''
|
||||
return () => {
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
}, [mobileMenuOpen])
|
||||
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia('(min-width: 1024px)')
|
||||
const handler = () => {
|
||||
if (mq.matches) setMobileMenuOpen(false)
|
||||
}
|
||||
mq.addEventListener('change', handler)
|
||||
return () => mq.removeEventListener('change', handler)
|
||||
}, [])
|
||||
|
||||
const anyHighlighted = activeDropdown !== null || hoveredLink !== null
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<nav
|
||||
aria-label='Primary navigation'
|
||||
className='relative flex h-[52px] border-[#2A2A2A] border-b-[1px] bg-[#1C1C1C] font-[430] font-season text-[#ECECEC] text-[14px]'
|
||||
className='flex h-[52px] border-[#2A2A2A] border-b-[1px] bg-[#1C1C1C] font-[430] font-season text-[#ECECEC] text-[14px]'
|
||||
itemScope
|
||||
itemType='https://schema.org/SiteNavigationElement'
|
||||
>
|
||||
<Link href='/' className={LOGO_CELL} aria-label={`${brand.name} home`} itemProp='url'>
|
||||
{/* Logo */}
|
||||
<Link href='/' className={LOGO_CELL} aria-label='Sim home' itemProp='url'>
|
||||
<span itemProp='name' className='sr-only'>
|
||||
{brand.name}
|
||||
Sim
|
||||
</span>
|
||||
{brand.logoUrl ? (
|
||||
<Image
|
||||
src={brand.logoUrl}
|
||||
alt={`${brand.name} Logo`}
|
||||
width={71}
|
||||
height={22}
|
||||
className='h-[22px] w-auto object-contain'
|
||||
priority
|
||||
unoptimized
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src='/logo/sim-landing.svg'
|
||||
alt='Sim'
|
||||
width={71}
|
||||
height={22}
|
||||
className='h-[22px] w-auto'
|
||||
priority
|
||||
/>
|
||||
)}
|
||||
<Image
|
||||
src='/logo/sim-landing.svg'
|
||||
alt='Sim'
|
||||
width={71}
|
||||
height={22}
|
||||
className='h-[22px] w-auto'
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{!logoOnly && (
|
||||
<>
|
||||
<ul className='mt-[0.75px] hidden lg:flex'>
|
||||
{NAV_LINKS.map(({ label, href, external, icon, dropdown }) => {
|
||||
const hasDropdown = !!dropdown
|
||||
const isActive = hasDropdown && activeDropdown === dropdown
|
||||
const isThisHovered = hoveredLink === label
|
||||
const isHighlighted = isActive || isThisHovered
|
||||
const isDimmed = anyHighlighted && !isHighlighted
|
||||
const linkClass = cn(
|
||||
icon ? `${LINK_CELL} gap-[8px]` : LINK_CELL,
|
||||
'transition-colors duration-200',
|
||||
isDimmed && 'text-[#F6F6F6]/60'
|
||||
)
|
||||
const chevron = icon === 'chevron' && <NavChevron open={isActive} />
|
||||
|
||||
if (hasDropdown) {
|
||||
return (
|
||||
<li
|
||||
key={label}
|
||||
className='relative flex'
|
||||
onMouseEnter={() => openDropdown(dropdown)}
|
||||
onMouseLeave={scheduleClose}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className={cn(linkClass, 'h-full cursor-pointer')}
|
||||
aria-expanded={isActive}
|
||||
aria-haspopup='true'
|
||||
>
|
||||
{label}
|
||||
{chevron}
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'-mt-[2px] absolute top-full left-0 z-50',
|
||||
isActive
|
||||
? 'pointer-events-auto opacity-100'
|
||||
: 'pointer-events-none opacity-0'
|
||||
)}
|
||||
style={{
|
||||
transform: isActive ? 'translateY(0)' : 'translateY(-6px)',
|
||||
transition: 'opacity 200ms ease, transform 200ms ease',
|
||||
}}
|
||||
>
|
||||
{dropdown === 'docs' && <DocsDropdown />}
|
||||
{dropdown === 'blog' && <BlogDropdown posts={blogPosts} />}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
key={label}
|
||||
className='flex'
|
||||
onMouseEnter={() => setHoveredLink(label)}
|
||||
onMouseLeave={() => setHoveredLink(null)}
|
||||
>
|
||||
{external ? (
|
||||
<a href={href} target='_blank' rel='noopener noreferrer' className={linkClass}>
|
||||
{label}
|
||||
{chevron}
|
||||
</a>
|
||||
) : (
|
||||
<Link href={href} className={linkClass} aria-label={label}>
|
||||
{label}
|
||||
{chevron}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
<li
|
||||
className={cn(
|
||||
'flex transition-opacity duration-200',
|
||||
anyHighlighted && hoveredLink !== 'github' && 'opacity-60'
|
||||
)}
|
||||
onMouseEnter={() => setHoveredLink('github')}
|
||||
onMouseLeave={() => setHoveredLink(null)}
|
||||
>
|
||||
<GitHubStars />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className='hidden flex-1 lg:block' />
|
||||
|
||||
<div className='hidden items-center gap-[8px] pr-[80px] pl-[20px] lg:flex'>
|
||||
<Link
|
||||
href='/login'
|
||||
className='inline-flex h-[30px] items-center rounded-[5px] border border-[#3d3d3d] px-[9px] text-[#ECECEC] text-[13.5px] transition-colors hover:bg-[#2A2A2A]'
|
||||
aria-label='Log in'
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
<Link
|
||||
href='/signup'
|
||||
className='inline-flex h-[30px] items-center gap-[7px] rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[9px] text-[13.5px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
aria-label='Get started with Sim'
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-1 items-center justify-end pr-[20px] lg:hidden'>
|
||||
<button
|
||||
type='button'
|
||||
className='flex h-[32px] w-[32px] items-center justify-center rounded-[5px] transition-colors hover:bg-[#2A2A2A]'
|
||||
onClick={() => setMobileMenuOpen((prev) => !prev)}
|
||||
aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
|
||||
aria-expanded={mobileMenuOpen}
|
||||
>
|
||||
<MobileMenuIcon open={mobileMenuOpen} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-x-0 top-[52px] bottom-0 z-50 flex flex-col overflow-y-auto bg-[#1C1C1C] font-[430] font-season text-[14px] transition-all duration-200 lg:hidden',
|
||||
mobileMenuOpen ? 'visible opacity-100' : 'invisible opacity-0'
|
||||
{/* Links */}
|
||||
<ul className='mt-[0.75px] flex'>
|
||||
{NAV_LINKS.map(({ label, href, external, icon }) => (
|
||||
<li key={label} className='flex'>
|
||||
{external ? (
|
||||
<a href={href} target='_blank' rel='noopener noreferrer' className={LINK_CELL}>
|
||||
{label}
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
href={href}
|
||||
className={icon ? `${LINK_CELL} gap-[8px]` : LINK_CELL}
|
||||
aria-label={label}
|
||||
>
|
||||
{label}
|
||||
{icon === 'chevron' && (
|
||||
<ChevronDown className='mt-[1.75px] h-[10px] w-[10px] flex-shrink-0 text-[#ECECEC]' />
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
>
|
||||
<ul className='flex flex-col'>
|
||||
{NAV_LINKS.map(({ label, href, external }) => (
|
||||
<li key={label} className='border-[#2A2A2A] border-b'>
|
||||
{external ? (
|
||||
<a
|
||||
href={href}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center justify-between px-[20px] py-[14px] text-[#ECECEC] transition-colors active:bg-[#2A2A2A]'
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{label}
|
||||
<ExternalArrowIcon />
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
href={href}
|
||||
className='flex items-center px-[20px] py-[14px] text-[#ECECEC] transition-colors active:bg-[#2A2A2A]'
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
<li className='border-[#2A2A2A] border-b'>
|
||||
<a
|
||||
href='https://github.com/simstudioai/sim'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center gap-[8px] px-[20px] py-[14px] text-[#ECECEC] transition-colors active:bg-[#2A2A2A]'
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
<GithubOutlineIcon className='h-[14px] w-[14px]' />
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
<li className='flex'>
|
||||
<GitHubStars />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className='mt-auto flex flex-col gap-[10px] p-[20px]'>
|
||||
<Link
|
||||
href='/login'
|
||||
className='flex h-[32px] items-center justify-center rounded-[5px] border border-[#3d3d3d] text-[#ECECEC] text-[14px] transition-colors active:bg-[#2A2A2A]'
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-label='Log in'
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
<Link
|
||||
href='/signup'
|
||||
className='flex h-[32px] items-center justify-center rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] text-[14px] text-black transition-colors active:bg-[#E0E0E0]'
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-label='Get started with Sim'
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className='flex-1' />
|
||||
|
||||
{/* CTAs */}
|
||||
<div className='flex items-center gap-[8px] px-[20px]'>
|
||||
<Link
|
||||
href='/login'
|
||||
className='inline-flex h-[30px] items-center rounded-[5px] border border-[#3d3d3d] px-[9px] text-[#ECECEC] text-[13.5px] transition-colors hover:bg-[#2A2A2A]'
|
||||
aria-label='Log in'
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
<Link
|
||||
href='/signup'
|
||||
className='inline-flex h-[30px] items-center gap-[7px] rounded-[5px] border border-[#33C482] bg-[#33C482] px-[9px] text-[13.5px] text-black transition-[filter] hover:brightness-110'
|
||||
aria-label='Get started with Sim'
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
interface NavChevronProps {
|
||||
open: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Animated chevron matching the exact geometry of the emcn ChevronDown SVG.
|
||||
* Each arm rotates around its midpoint so the center vertex travels up/down
|
||||
* while the outer endpoints adjust — producing a Stripe-style morph.
|
||||
*/
|
||||
function NavChevron({ open }: NavChevronProps) {
|
||||
return (
|
||||
<svg width='9' height='6' viewBox='0 0 10 6' fill='none' className='mt-[1.5px] flex-shrink-0'>
|
||||
<line
|
||||
x1='1'
|
||||
y1='1'
|
||||
x2='5'
|
||||
y2='5'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
style={{
|
||||
transformOrigin: '3px 3px',
|
||||
transform: open ? 'rotate(-90deg)' : 'rotate(0deg)',
|
||||
transition: 'transform 250ms cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
/>
|
||||
<line
|
||||
x1='5'
|
||||
y1='5'
|
||||
x2='9'
|
||||
y2='1'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
style={{
|
||||
transformOrigin: '7px 3px',
|
||||
transform: open ? 'rotate(90deg)' : 'rotate(0deg)',
|
||||
transition: 'transform 250ms cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileMenuIcon({ open }: { open: boolean }) {
|
||||
if (open) {
|
||||
return (
|
||||
<svg width='14' height='14' viewBox='0 0 14 14' fill='none'>
|
||||
<path
|
||||
d='M1 1L13 13M13 1L1 13'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<svg width='16' height='12' viewBox='0 0 16 12' fill='none'>
|
||||
<path
|
||||
d='M0 1H16M0 6H16M0 11H16'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ExternalArrowIcon() {
|
||||
return (
|
||||
<svg width='12' height='12' viewBox='0 0 12 12' fill='none' className='text-[#666]'>
|
||||
<path
|
||||
d='M3.5 2.5H9.5V8.5M9 3L3 9'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,10 +22,9 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
features: [
|
||||
'1,000 credits (trial)',
|
||||
'5GB file storage',
|
||||
'3 tables · 1,000 rows each',
|
||||
'5 min execution limit',
|
||||
'7-day log retention',
|
||||
'CLI/SDK/MCP Access',
|
||||
'Limited log retention',
|
||||
'CLI/SDK Access',
|
||||
],
|
||||
cta: { label: 'Get started', href: '/signup' },
|
||||
},
|
||||
@@ -37,12 +36,11 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
billingPeriod: 'per month',
|
||||
color: '#00F701',
|
||||
features: [
|
||||
'6,000 credits/mo · +50/day',
|
||||
'6,000 credits/mo',
|
||||
'+50 daily refresh credits',
|
||||
'150 runs/min (sync)',
|
||||
'50 min sync execution limit',
|
||||
'50GB file storage',
|
||||
'25 tables · 5,000 rows each',
|
||||
'50 min execution · 150 runs/min',
|
||||
'Unlimited log retention',
|
||||
'CLI/SDK/MCP Access',
|
||||
],
|
||||
cta: { label: 'Get started', href: '/signup' },
|
||||
},
|
||||
@@ -54,12 +52,11 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
billingPeriod: 'per month',
|
||||
color: '#FA4EDF',
|
||||
features: [
|
||||
'25,000 credits/mo · +200/day',
|
||||
'25,000 credits/mo',
|
||||
'+200 daily refresh credits',
|
||||
'300 runs/min (sync)',
|
||||
'50 min sync execution limit',
|
||||
'500GB file storage',
|
||||
'25 tables · 5,000 rows each',
|
||||
'50 min execution · 300 runs/min',
|
||||
'Unlimited log retention',
|
||||
'CLI/SDK/MCP Access',
|
||||
],
|
||||
cta: { label: 'Get started', href: '/signup' },
|
||||
},
|
||||
@@ -69,16 +66,8 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
description: 'For organizations needing security and scale',
|
||||
price: 'Custom',
|
||||
color: '#FFCC02',
|
||||
features: [
|
||||
'Custom credits & infra limits',
|
||||
'Custom file storage',
|
||||
'10,000 tables · 1M rows each',
|
||||
'Custom execution limits',
|
||||
'Unlimited log retention',
|
||||
'SSO & SCIM · SOC2 & HIPAA',
|
||||
'Self hosting · Dedicated support',
|
||||
],
|
||||
cta: { label: 'Book a demo', href: 'https://form.typeform.com/to/jqCO12pF' },
|
||||
features: ['Custom infra limits', 'SSO', 'SOC2', 'Self hosting', 'Dedicated support'],
|
||||
cta: { label: 'Book a demo', href: '/contact' },
|
||||
},
|
||||
]
|
||||
|
||||
@@ -127,8 +116,6 @@ function PricingCard({ tier }: PricingCardProps) {
|
||||
{isEnterprise ? (
|
||||
<a
|
||||
href={tier.cta.href}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#E5E5E5] px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:bg-[#F0F0F0]'
|
||||
>
|
||||
{tier.cta.label}
|
||||
@@ -136,7 +123,7 @@ function PricingCard({ tier }: PricingCardProps) {
|
||||
) : isPro ? (
|
||||
<Link
|
||||
href={tier.cta.href}
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-[10px] font-[430] font-season text-[14px] text-white transition-colors hover:border-[#2A2A2A] hover:bg-[#2A2A2A]'
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#33C482] bg-[#33C482] px-[10px] font-[430] font-season text-[14px] text-white transition-[filter] hover:brightness-110'
|
||||
>
|
||||
{tier.cta.label}
|
||||
</Link>
|
||||
@@ -187,7 +174,7 @@ function PricingCard({ tier }: PricingCardProps) {
|
||||
export default function Pricing() {
|
||||
return (
|
||||
<section id='pricing' aria-labelledby='pricing-heading' className='bg-[#F6F6F6]'>
|
||||
<div className='px-4 pt-[60px] pb-[40px] sm:px-8 sm:pt-[80px] sm:pb-0 md:px-[80px] md:pt-[100px]'>
|
||||
<div className='px-4 pt-[100px] pb-8 sm:px-8 md:px-[80px]'>
|
||||
<div className='flex flex-col items-start gap-3 sm:gap-4 md:gap-[20px]'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
@@ -206,7 +193,7 @@ export default function Pricing() {
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 grid grid-cols-1 gap-4 sm:mt-10 sm:grid-cols-2 md:mt-12 lg:grid-cols-4'>
|
||||
<div className='mt-12 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4'>
|
||||
{PRICING_TIERS.map((tier) => (
|
||||
<PricingCard key={tier.id} tier={tier} />
|
||||
))}
|
||||
|
||||
@@ -74,10 +74,6 @@ export default function StructuredData() {
|
||||
'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs.',
|
||||
breadcrumb: { '@id': 'https://sim.ai/#breadcrumb' },
|
||||
inLanguage: 'en-US',
|
||||
speakable: {
|
||||
'@type': 'SpeakableSpecification',
|
||||
cssSelector: ['#hero-heading', '[id="hero"] p'],
|
||||
},
|
||||
potentialAction: [{ '@type': 'ReadAction', target: ['https://sim.ai'] }],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { AnimatePresence, type MotionValue, motion, useScroll, useTransform } from 'framer-motion'
|
||||
import { type MotionValue, motion, useScroll, useTransform } from 'framer-motion'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Badge, ChevronDown } from '@/components/emcn'
|
||||
@@ -337,7 +337,7 @@ function DotGrid({ className, cols, rows, gap = 0 }: DotGridProps) {
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: cols * rows }, (_, i) => (
|
||||
<div key={i} className='h-[1.5px] w-[1.5px] rounded-full bg-[#2A2A2A]' />
|
||||
<div key={i} className='h-[2px] w-[2px] rounded-full bg-[#2A2A2A]' />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
@@ -349,17 +349,8 @@ export default function Templates() {
|
||||
const sectionRef = useRef<HTMLDivElement>(null)
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
const [isPreparingTemplate, setIsPreparingTemplate] = useState(false)
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia('(max-width: 1023px)')
|
||||
setIsMobile(mq.matches)
|
||||
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches)
|
||||
mq.addEventListener('change', handler)
|
||||
return () => mq.removeEventListener('change', handler)
|
||||
}, [])
|
||||
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: sectionRef,
|
||||
offset: ['start 0.9', 'start 0.2'],
|
||||
@@ -424,8 +415,8 @@ export default function Templates() {
|
||||
|
||||
<div className='bg-[#1C1C1C]'>
|
||||
<DotGrid
|
||||
className='overflow-hidden border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
|
||||
cols={160}
|
||||
className='border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
|
||||
cols={120}
|
||||
rows={1}
|
||||
gap={6}
|
||||
/>
|
||||
@@ -449,7 +440,7 @@ export default function Templates() {
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className='px-[20px] pt-[60px] lg:px-[80px] lg:pt-[100px]'>
|
||||
<div className='px-[80px] pt-[100px]'>
|
||||
<div className='flex flex-col items-start gap-[20px]'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
@@ -466,132 +457,84 @@ export default function Templates() {
|
||||
|
||||
<h2
|
||||
id='templates-heading'
|
||||
className='font-[430] font-season text-[28px] text-white leading-[100%] tracking-[-0.02em] lg:text-[40px]'
|
||||
className='font-[430] font-season text-[40px] text-white leading-[100%] tracking-[-0.02em]'
|
||||
>
|
||||
Ship your agent in minutes
|
||||
</h2>
|
||||
|
||||
<p className='font-[430] font-season text-[#F6F6F0]/50 text-[15px] leading-[150%] tracking-[0.02em] lg:text-[18px]'>
|
||||
Pre-built templates for every use case—pick one, swap{' '}
|
||||
<br className='hidden lg:inline' />
|
||||
<p className='font-[430] font-season text-[#F6F6F0]/50 text-[16px] leading-[125%] tracking-[0.02em]'>
|
||||
Pre-built templates for every use case—pick one, swap <br />
|
||||
models and tools to fit your stack, and deploy.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-[40px] flex border-[#2A2A2A] border-y lg:mt-[73px]'>
|
||||
<div className='shrink-0'>
|
||||
<div className='h-full lg:hidden'>
|
||||
<DotGrid
|
||||
className='h-full w-[24px] overflow-hidden border-[#2A2A2A] border-r p-[4px]'
|
||||
cols={2}
|
||||
rows={55}
|
||||
gap={4}
|
||||
/>
|
||||
</div>
|
||||
<div className='hidden h-full lg:block'>
|
||||
<DotGrid
|
||||
className='h-full w-[80px] overflow-hidden border-[#2A2A2A] border-r p-[6px]'
|
||||
cols={8}
|
||||
rows={55}
|
||||
gap={6}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-[73px] flex border-[#2A2A2A] border-y'>
|
||||
<DotGrid
|
||||
className='w-[80px] shrink-0 overflow-hidden border-[#2A2A2A] border-r p-[6px]'
|
||||
cols={6}
|
||||
rows={55}
|
||||
gap={6}
|
||||
/>
|
||||
|
||||
<div className='flex min-w-0 flex-1 flex-col lg:flex-row'>
|
||||
<div className='flex min-w-0 flex-1'>
|
||||
<div
|
||||
role='tablist'
|
||||
aria-label='Workflow templates'
|
||||
className='flex w-full shrink-0 flex-col border-[#2A2A2A] lg:w-[300px] lg:border-r'
|
||||
className='flex w-[300px] shrink-0 flex-col border-[#2A2A2A] border-r'
|
||||
>
|
||||
{TEMPLATE_WORKFLOWS.map((workflow, index) => {
|
||||
const isActive = index === activeIndex
|
||||
return (
|
||||
<div key={workflow.id}>
|
||||
<button
|
||||
id={`template-tab-${index}`}
|
||||
type='button'
|
||||
role='tab'
|
||||
aria-selected={isActive}
|
||||
aria-controls={TEMPLATES_PANEL_ID}
|
||||
onClick={() => setActiveIndex(index)}
|
||||
className={cn(
|
||||
'relative w-full text-left',
|
||||
isActive
|
||||
? 'z-10'
|
||||
: cn(
|
||||
'flex items-center px-[12px] py-[10px] hover:bg-[#232323]/50',
|
||||
index < TEMPLATE_WORKFLOWS.length - 1 &&
|
||||
'shadow-[inset_0_-1px_0_0_#2A2A2A]'
|
||||
)
|
||||
)}
|
||||
>
|
||||
{isActive ? (
|
||||
(() => {
|
||||
const depth = DEPTH_CONFIGS[workflow.id]
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className='absolute top-[-8px] bottom-0 left-0 w-2'
|
||||
style={{
|
||||
clipPath: LEFT_WALL_CLIP,
|
||||
backgroundColor: hexToRgba(depth.color, 0.63),
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className='absolute right-[-8px] bottom-0 left-2 h-2'
|
||||
style={buildBottomWallStyle(depth)}
|
||||
/>
|
||||
<div className='-translate-y-2 relative flex translate-x-2 items-center bg-[#242424] px-[12px] py-[10px] shadow-[inset_0_0_0_1.5px_#3E3E3E]'>
|
||||
<span className='flex-1 font-[430] font-season text-[16px] text-white'>
|
||||
{workflow.name}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className='-rotate-90 h-[11px] w-[11px] shrink-0'
|
||||
style={{ color: depth.color }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})()
|
||||
) : (
|
||||
<span className='font-[430] font-season text-[#F6F6F0]/50 text-[16px]'>
|
||||
{workflow.name}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isActive && isMobile && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
|
||||
className='overflow-hidden'
|
||||
>
|
||||
<div className='aspect-[16/10] w-full border-[#2A2A2A] border-y bg-[#1b1b1b]'>
|
||||
<LandingPreviewWorkflow
|
||||
workflow={workflow}
|
||||
animate
|
||||
fitViewOptions={{ padding: 0.15, maxZoom: 1.3 }}
|
||||
<button
|
||||
key={workflow.id}
|
||||
id={`template-tab-${index}`}
|
||||
type='button'
|
||||
role='tab'
|
||||
aria-selected={isActive}
|
||||
aria-controls={TEMPLATES_PANEL_ID}
|
||||
onClick={() => setActiveIndex(index)}
|
||||
className={cn(
|
||||
'relative text-left',
|
||||
isActive
|
||||
? 'z-10'
|
||||
: 'flex items-center px-[12px] py-[10px] shadow-[inset_0_-1px_0_0_#2A2A2A] last:shadow-none hover:bg-[#232323]/50'
|
||||
)}
|
||||
>
|
||||
{isActive ? (
|
||||
(() => {
|
||||
const depth = DEPTH_CONFIGS[workflow.id]
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className='absolute top-[-8px] bottom-0 left-0 w-2'
|
||||
style={{
|
||||
clipPath: LEFT_WALL_CLIP,
|
||||
backgroundColor: hexToRgba(depth.color, 0.63),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='p-[12px]'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleUseTemplate}
|
||||
disabled={isPreparingTemplate}
|
||||
className='inline-flex h-[32px] w-full cursor-pointer items-center justify-center gap-[6px] rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] font-[430] font-season text-[14px] text-black transition-colors active:bg-[#E0E0E0]'
|
||||
>
|
||||
{isPreparingTemplate ? 'Preparing...' : 'Use template'}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<div
|
||||
className='absolute right-[-8px] bottom-0 left-2 h-2'
|
||||
style={buildBottomWallStyle(depth)}
|
||||
/>
|
||||
<div className='-translate-y-2 relative flex translate-x-2 items-center bg-[#242424] px-[12px] py-[10px] shadow-[inset_0_0_0_1.5px_#3E3E3E]'>
|
||||
<span className='flex-1 font-[430] font-season text-[16px] text-white'>
|
||||
{workflow.name}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className='-rotate-90 h-[11px] w-[11px] shrink-0'
|
||||
style={{ color: depth.color }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})()
|
||||
) : (
|
||||
<span className='font-[430] font-season text-[#F6F6F0]/50 text-[16px]'>
|
||||
{workflow.name}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
@@ -614,7 +557,7 @@ export default function Templates() {
|
||||
type='button'
|
||||
onClick={handleUseTemplate}
|
||||
disabled={isPreparingTemplate}
|
||||
className='group/cta absolute top-[16px] right-[16px] z-10 inline-flex h-[32px] cursor-pointer items-center gap-[6px] rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[10px] font-[430] font-season text-[14px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
className='group/cta absolute top-[16px] right-[16px] z-10 inline-flex h-[32px] cursor-pointer items-center gap-[6px] rounded-[5px] border border-[#33C482] bg-[#33C482] px-[10px] font-[430] font-season text-[14px] text-black transition-[filter] hover:brightness-110'
|
||||
>
|
||||
{isPreparingTemplate ? 'Preparing...' : 'Use template'}
|
||||
<span className='relative h-[10px] w-[10px] shrink-0'>
|
||||
@@ -639,24 +582,12 @@ export default function Templates() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='shrink-0'>
|
||||
<div className='h-full lg:hidden'>
|
||||
<DotGrid
|
||||
className='h-full w-[24px] overflow-hidden border-[#2A2A2A] border-l p-[4px]'
|
||||
cols={2}
|
||||
rows={55}
|
||||
gap={4}
|
||||
/>
|
||||
</div>
|
||||
<div className='hidden h-full lg:block'>
|
||||
<DotGrid
|
||||
className='h-full w-[80px] overflow-hidden border-[#2A2A2A] border-l p-[6px]'
|
||||
cols={8}
|
||||
rows={55}
|
||||
gap={6}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DotGrid
|
||||
className='w-[80px] shrink-0 overflow-hidden border-[#2A2A2A] border-l p-[6px]'
|
||||
cols={6}
|
||||
rows={55}
|
||||
gap={6}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getNavBlogPosts } from '@/lib/blog/registry'
|
||||
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
|
||||
import { season } from '@/app/_styles/fonts/season/season'
|
||||
import {
|
||||
@@ -29,25 +28,23 @@ import {
|
||||
* for immediate availability to AI crawlers.
|
||||
* - Section `id` attributes serve as fragment anchors for precise AI citations.
|
||||
* - Content ordering prioritizes answer-first patterns: definition (Hero) ->
|
||||
* examples (Templates) -> capabilities (Features) -> social proof (Collaboration) ->
|
||||
* enterprise (Enterprise) -> pricing (Pricing) -> testimonials (Testimonials).
|
||||
* examples (Templates) -> capabilities (Features) -> social proof (Collaboration, Testimonials) ->
|
||||
* pricing (Pricing) -> enterprise (Enterprise).
|
||||
*/
|
||||
export default async function Landing() {
|
||||
const blogPosts = await getNavBlogPosts()
|
||||
|
||||
return (
|
||||
<div className={`${season.variable} ${martianMono.variable} min-h-screen bg-[#1C1C1C]`}>
|
||||
<StructuredData />
|
||||
<header>
|
||||
<Navbar blogPosts={blogPosts} />
|
||||
<Navbar />
|
||||
</header>
|
||||
<main>
|
||||
<Hero />
|
||||
<Templates />
|
||||
<Features />
|
||||
<Collaboration />
|
||||
<Enterprise />
|
||||
<Pricing />
|
||||
<Enterprise />
|
||||
<Testimonials />
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
|
||||
interface ExternalRedirectProps {
|
||||
url: string
|
||||
}
|
||||
|
||||
/** Redirects to an external URL when it is configured via an environment variable. */
|
||||
export default function ExternalRedirect({ url }: ExternalRedirectProps) {
|
||||
useEffect(() => {
|
||||
if (url?.startsWith('http')) {
|
||||
window.location.href = url
|
||||
}
|
||||
}, [url])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import Link from 'next/link'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import {
|
||||
ComplianceBadges,
|
||||
Logo,
|
||||
@@ -13,7 +14,7 @@ interface FooterProps {
|
||||
|
||||
export default function Footer({ fullWidth = false }: FooterProps) {
|
||||
return (
|
||||
<footer className='relative w-full overflow-hidden bg-white'>
|
||||
<footer className={`${inter.className} relative w-full overflow-hidden bg-white`}>
|
||||
<div
|
||||
className={
|
||||
fullWidth
|
||||
@@ -57,10 +58,10 @@ export default function Footer({ fullWidth = false }: FooterProps) {
|
||||
Enterprise
|
||||
</Link>
|
||||
<Link
|
||||
href='/blog'
|
||||
href='/studio'
|
||||
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
Blog
|
||||
Sim Studio
|
||||
</Link>
|
||||
<Link
|
||||
href='/changelog'
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
SupabaseIcon,
|
||||
} from '@/components/icons'
|
||||
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import {
|
||||
CARD_WIDTH,
|
||||
IconButton,
|
||||
@@ -363,7 +364,7 @@ export default function Hero() {
|
||||
return (
|
||||
<section
|
||||
id='hero'
|
||||
className='flex w-full flex-col items-center justify-center pt-[36px] sm:pt-[80px]'
|
||||
className={`${soehne.className} flex w-full flex-col items-center justify-center pt-[36px] sm:pt-[80px]`}
|
||||
aria-labelledby='hero-heading'
|
||||
>
|
||||
<h1
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Background from '@/app/(landing)/components/background/background'
|
||||
import ExternalRedirect from '@/app/(landing)/components/external-redirect'
|
||||
import Footer from '@/app/(landing)/components/footer/footer'
|
||||
import Hero from '@/app/(landing)/components/hero/hero'
|
||||
import Integrations from '@/app/(landing)/components/integrations/integrations'
|
||||
@@ -21,5 +20,4 @@ export {
|
||||
Footer,
|
||||
StructuredData,
|
||||
LegalLayout,
|
||||
ExternalRedirect,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Icons from '@/components/icons'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
|
||||
const modelProviderIcons = [
|
||||
{ icon: Icons.OpenAIIcon, label: 'OpenAI' },
|
||||
@@ -121,7 +122,7 @@ export default function Integrations() {
|
||||
return (
|
||||
<section
|
||||
id='integrations'
|
||||
className='flex flex-col pt-[40px] pb-[27px] sm:pt-[24px]'
|
||||
className={`${inter.className} flex flex-col pt-[40px] pb-[27px] sm:pt-[24px]`}
|
||||
aria-labelledby='integrations-heading'
|
||||
>
|
||||
<h2
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import { ENTERPRISE_PLAN_FEATURES } from '@/app/workspace/[workspaceId]/settings/components/subscription/plan-configs'
|
||||
|
||||
const logger = createLogger('LandingPricing')
|
||||
@@ -116,6 +117,7 @@ function PricingCard({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
`${inter.className}`,
|
||||
'relative flex h-full flex-col justify-between bg-[#FEFEFE]',
|
||||
tier.featured ? 'p-0' : 'px-0 py-0',
|
||||
'sm:px-5 sm:pt-4 sm:pb-4',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
|
||||
interface LandingTemplatePreviewProps {
|
||||
previewImage: string
|
||||
avatarImage: string
|
||||
@@ -35,8 +37,14 @@ export default function LandingTemplatePreview({
|
||||
|
||||
{/* Title and Author Info */}
|
||||
<div className='min-w-0 flex-1'>
|
||||
<h4 className='truncate font-medium text-foreground text-sm leading-none'>{title}</h4>
|
||||
<p className='mt-1 flex items-center gap-2 text-muted-foreground text-xs'>
|
||||
<h4
|
||||
className={`${inter.className} truncate font-medium text-foreground text-sm leading-none`}
|
||||
>
|
||||
{title}
|
||||
</h4>
|
||||
<p
|
||||
className={`${inter.className} mt-1 flex items-center gap-2 text-muted-foreground text-xs`}
|
||||
>
|
||||
<span>{authorName}</span>
|
||||
<span>{usageCount.toLocaleString()} copies</span>
|
||||
</p>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import LandingTemplatePreview from '@/app/(landing)/components/landing-templates/components/landing-template-preview'
|
||||
|
||||
const templates = [
|
||||
@@ -79,7 +80,7 @@ export default function LandingTemplates() {
|
||||
return (
|
||||
<section
|
||||
id='templates'
|
||||
className='flex flex-col px-4 pt-[40px] sm:px-[50px] sm:pt-[34px]'
|
||||
className={`${inter.className} flex flex-col px-4 pt-[40px] sm:px-[50px] sm:pt-[34px]`}
|
||||
aria-labelledby='templates-heading'
|
||||
>
|
||||
<h2
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
import { getNavBlogPosts } from '@/lib/blog/registry'
|
||||
'use client'
|
||||
|
||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||
import Footer from '@/app/(home)/components/footer/footer'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import Footer from '@/app/(landing)/components/footer/footer'
|
||||
import Nav from '@/app/(landing)/components/nav/nav'
|
||||
|
||||
interface LegalLayoutProps {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
navVariant?: 'landing' | 'auth' | 'legal'
|
||||
}
|
||||
|
||||
export default async function LegalLayout({ title, children }: LegalLayoutProps) {
|
||||
const blogPosts = await getNavBlogPosts()
|
||||
|
||||
export default function LegalLayout({ title, children, navVariant = 'legal' }: LegalLayoutProps) {
|
||||
return (
|
||||
<main className='min-h-screen bg-[#1C1C1C] font-[430] font-season text-[#ECECEC]'>
|
||||
<header>
|
||||
<Navbar blogPosts={blogPosts} />
|
||||
</header>
|
||||
<main className={`${soehne.className} min-h-screen bg-white text-gray-900`}>
|
||||
{/* Header - Nav handles all conditional logic */}
|
||||
<Nav variant={navVariant} />
|
||||
|
||||
<div className='mx-auto max-w-[800px] px-6 pt-[60px] pb-[80px] sm:px-12'>
|
||||
<h1 className='mb-12 text-center font-[500] text-4xl text-[#ECECEC] md:text-5xl'>
|
||||
{title}
|
||||
</h1>
|
||||
<div className='space-y-8 text-[#999] text-[15px] leading-[1.7] [&_h2]:mt-12 [&_h2]:mb-6 [&_h2]:text-[#ECECEC] [&_h3]:mt-8 [&_h3]:mb-4 [&_h3]:text-[#ECECEC] [&_li]:text-[#999] [&_strong]:text-[#ECECEC]'>
|
||||
{/* Content */}
|
||||
<div className='px-12 pt-[40px] pb-[40px]'>
|
||||
<h1 className='mb-12 text-center font-bold text-4xl text-gray-900 md:text-5xl'>{title}</h1>
|
||||
<div className='prose prose-gray mx-auto prose-h2:mt-12 prose-h3:mt-8 prose-h2:mb-6 prose-h3:mb-4 space-y-8 text-gray-700'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isHosted && <Footer hideCTA />}
|
||||
{/* Footer - Only for hosted instances */}
|
||||
{isHosted && (
|
||||
<div className='relative z-20'>
|
||||
<Footer fullWidth={true} />
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { GithubIcon } from '@/components/icons'
|
||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
|
||||
import { useBrandConfig } from '@/ee/whitelabeling'
|
||||
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||
@@ -117,7 +118,7 @@ export default function Nav({ hideAuthButtons = false, variant = 'landing' }: Na
|
||||
return (
|
||||
<nav
|
||||
aria-label='Primary navigation'
|
||||
className={`flex w-full items-center justify-between px-4 ${
|
||||
className={`${soehne.className} flex w-full items-center justify-between px-4 ${
|
||||
variant === 'auth' ? 'pt-[20px] sm:pt-[16.5px]' : 'pt-[12px] sm:pt-[8.5px]'
|
||||
} pb-[21px] sm:px-8 md:px-[44px]`}
|
||||
itemScope
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
|
||||
interface Testimonial {
|
||||
text: string
|
||||
@@ -122,7 +123,7 @@ export default function Testimonials() {
|
||||
return (
|
||||
<section
|
||||
id='testimonials'
|
||||
className='flex hidden h-[150px] items-center sm:block'
|
||||
className={`flex hidden h-[150px] items-center sm:block ${inter.variable}`}
|
||||
aria-label='Social proof testimonials'
|
||||
>
|
||||
<div className='relative mx-auto h-full w-full max-w-[1289px] pl-[2px]'>
|
||||
@@ -179,7 +180,9 @@ export default function Testimonials() {
|
||||
</div>
|
||||
|
||||
{/* Tweet content below with padding */}
|
||||
<p className='mt-2 line-clamp-4 font-[380] text-[#0A0A0A] text-[13px] leading-[1.3] transition-colors duration-300 group-hover:text-white'>
|
||||
<p
|
||||
className={`${inter.className} mt-2 line-clamp-4 font-[380] text-[#0A0A0A] text-[13px] leading-[1.3] transition-colors duration-300 group-hover:text-white`}
|
||||
>
|
||||
{tweet.text}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ChevronDown } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type { FAQItem } from '@/app/(landing)/integrations/data/types'
|
||||
|
||||
interface IntegrationFAQProps {
|
||||
faqs: FAQItem[]
|
||||
}
|
||||
|
||||
export function IntegrationFAQ({ faqs }: IntegrationFAQProps) {
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(0)
|
||||
|
||||
return (
|
||||
<div className='divide-y divide-[#2A2A2A]'>
|
||||
{faqs.map(({ question, answer }, index) => {
|
||||
const isOpen = openIndex === index
|
||||
return (
|
||||
<div key={question}>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setOpenIndex(isOpen ? null : index)}
|
||||
className='flex w-full items-start justify-between gap-4 py-5 text-left'
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'font-[500] text-[15px] leading-snug transition-colors',
|
||||
isOpen ? 'text-[#ECECEC]' : 'text-[#999] hover:text-[#ECECEC]'
|
||||
)}
|
||||
>
|
||||
{question}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'mt-0.5 h-4 w-4 shrink-0 text-[#555] transition-transform duration-200',
|
||||
isOpen ? 'rotate-180' : 'rotate-0'
|
||||
)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className='pb-5'>
|
||||
<p className='text-[#999] text-[14px] leading-[1.75]'>{answer}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
|
||||
|
||||
interface TemplateCardButtonProps {
|
||||
prompt: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function TemplateCardButton({ prompt, children }: TemplateCardButtonProps) {
|
||||
const router = useRouter()
|
||||
|
||||
function handleClick() {
|
||||
LandingPromptStorage.store(prompt)
|
||||
router.push('/signup')
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleClick}
|
||||
className='group flex w-full flex-col items-start rounded-lg border border-[#2A2A2A] bg-[#242424] p-5 text-left transition-colors hover:border-[#3d3d3d] hover:bg-[#2A2A2A]'
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -1,748 +0,0 @@
|
||||
import type { Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { TEMPLATES } from '@/app/workspace/[workspaceId]/home/components/template-prompts/consts'
|
||||
import { IntegrationIcon } from '../components/integration-icon'
|
||||
import { blockTypeToIconMap } from '../data/icon-mapping'
|
||||
import integrations from '../data/integrations.json'
|
||||
import type { AuthType, FAQItem, Integration } from '../data/types'
|
||||
import { IntegrationFAQ } from './components/integration-faq'
|
||||
import { TemplateCardButton } from './components/template-card-button'
|
||||
|
||||
const allIntegrations = integrations as Integration[]
|
||||
const INTEGRATION_COUNT = allIntegrations.length
|
||||
|
||||
/** Fast O(1) lookups — avoids repeated linear scans inside render loops. */
|
||||
const bySlug = new Map(allIntegrations.map((i) => [i.slug, i]))
|
||||
const byType = new Map(allIntegrations.map((i) => [i.type, i]))
|
||||
|
||||
/**
|
||||
* Returns up to `limit` related integration slugs.
|
||||
*
|
||||
* Scoring (additive):
|
||||
* +3 per shared operation name — strongest signal (same capability)
|
||||
* +2 per shared operation word — weaker signal (e.g. both have "create" ops)
|
||||
* +1 same auth type — comparable setup experience
|
||||
*
|
||||
* Every integration gets a score, so the sidebar always has suggestions.
|
||||
* Ties are broken by alphabetical slug order for determinism.
|
||||
*/
|
||||
function getRelatedSlugs(
|
||||
slug: string,
|
||||
operations: Integration['operations'],
|
||||
authType: AuthType,
|
||||
limit = 6
|
||||
): string[] {
|
||||
const currentOpNames = new Set(operations.map((o) => o.name.toLowerCase()))
|
||||
const currentOpWords = new Set(
|
||||
operations.flatMap((o) =>
|
||||
o.name
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter((w) => w.length > 3)
|
||||
)
|
||||
)
|
||||
|
||||
return allIntegrations
|
||||
.filter((i) => i.slug !== slug)
|
||||
.map((i) => {
|
||||
const sharedNames = i.operations.filter((o) =>
|
||||
currentOpNames.has(o.name.toLowerCase())
|
||||
).length
|
||||
const sharedWords = i.operations.filter((o) =>
|
||||
o.name
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.some((w) => w.length > 3 && currentOpWords.has(w))
|
||||
).length
|
||||
const sameAuth = i.authType === authType ? 1 : 0
|
||||
return { slug: i.slug, score: sharedNames * 3 + sharedWords * 2 + sameAuth }
|
||||
})
|
||||
.sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug))
|
||||
.slice(0, limit)
|
||||
.map(({ slug: s }) => s)
|
||||
}
|
||||
|
||||
const AUTH_STEP: Record<AuthType, string> = {
|
||||
oauth: 'Authenticate with one-click OAuth — no credentials to copy-paste.',
|
||||
'api-key': 'Add your API key to authenticate — find it in your account settings.',
|
||||
none: 'Authenticate your account to connect.',
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates targeted FAQs from integration metadata.
|
||||
* Questions mirror real search queries to drive FAQPage rich snippets.
|
||||
*/
|
||||
function buildFAQs(integration: Integration): FAQItem[] {
|
||||
const { name, description, operations, triggers, authType } = integration
|
||||
const topOps = operations.slice(0, 5)
|
||||
const topOpNames = topOps.map((o) => o.name)
|
||||
const authStep = AUTH_STEP[authType]
|
||||
|
||||
const faqs: FAQItem[] = [
|
||||
{
|
||||
question: `What is Sim's ${name} integration?`,
|
||||
answer: `Sim's ${name} integration lets you build AI-powered workflows that automate tasks in ${name} without writing code. ${description} You can connect ${name} to hundreds of other services in the same workflow — from CRMs and spreadsheets to messaging tools and databases.`,
|
||||
},
|
||||
{
|
||||
question: `What can I automate with ${name} in Sim?`,
|
||||
answer:
|
||||
topOpNames.length > 0
|
||||
? `With Sim you can: ${topOpNames.join('; ')}${operations.length > 5 ? `; and ${operations.length - 5} more tools` : ''}. Each action runs inside an AI agent block, so you can combine ${name} with LLM reasoning, conditional logic, and data from any other connected service.`
|
||||
: `Sim lets you automate ${name} workflows by connecting it to an AI agent that can read from it, write to it, and chain it together with other services — all driven by natural-language instructions instead of rigid rules.`,
|
||||
},
|
||||
{
|
||||
question: `How do I connect ${name} to Sim?`,
|
||||
answer: `Getting started takes under five minutes: (1) Create a free account at sim.ai. (2) Open a new workflow. (3) Drag a ${name} block onto the canvas. (4) ${authStep} (5) Choose the tool you want to use, wire it to the inputs you need, and click Run. Your automation is live.`,
|
||||
},
|
||||
{
|
||||
question: `Can I use ${name} as a tool inside an AI agent in Sim?`,
|
||||
answer: `Yes — this is one of Sim's core capabilities. Instead of hard-coding when and how ${name} is used, you give an AI agent access to ${name} tools and describe the goal in plain language. The agent decides which tools to call, in what order, and how to handle the results. This means your automation adapts to context rather than breaking when inputs change.`,
|
||||
},
|
||||
...(topOpNames.length >= 2
|
||||
? [
|
||||
{
|
||||
question: `How do I ${topOpNames[0].toLowerCase()} with ${name} in Sim?`,
|
||||
answer: `Add a ${name} block to your workflow and select "${topOpNames[0]}" as the tool. Fill in the required fields — you can reference outputs from earlier steps, such as text generated by an AI agent or data fetched from another integration. No code is required.`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(triggers.length > 0
|
||||
? [
|
||||
{
|
||||
question: `How do I trigger a Sim workflow from ${name} automatically?`,
|
||||
answer: `In your Sim workflow, switch the ${name} block to Trigger mode and copy the generated webhook URL. Paste that URL into ${name}'s webhook settings and select the events you want to listen for (${triggers.map((t) => t.name).join(', ')}). From that point on, every matching event in ${name} instantly fires your workflow — no polling, no delay.`,
|
||||
},
|
||||
{
|
||||
question: `What data does Sim receive when a ${name} event triggers a workflow?`,
|
||||
answer: `When ${name} fires a webhook, Sim receives the full event payload that ${name} sends — typically the record or object that changed, along with metadata like the event type and timestamp. Inside your workflow, every field from that payload is available as a variable you can pass to AI agents, conditions, or other integrations.`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
question: `What ${name} tools does Sim support?`,
|
||||
answer:
|
||||
operations.length > 0
|
||||
? `Sim supports ${operations.length} ${name} tool${operations.length === 1 ? '' : 's'}: ${operations.map((o) => o.name).join(', ')}.`
|
||||
: `Sim supports core ${name} tools for reading and writing data, triggering actions, and integrating with your other services. See the full list in the Sim documentation.`,
|
||||
},
|
||||
{
|
||||
question: `Is the ${name} integration free to use?`,
|
||||
answer: `Yes — Sim's free plan includes access to the ${name} integration and every other integration in the library. No credit card is needed to get started. Visit sim.ai to create your account.`,
|
||||
},
|
||||
]
|
||||
|
||||
return faqs
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return allIntegrations.map((i) => ({ slug: i.slug }))
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params
|
||||
const integration = bySlug.get(slug)
|
||||
if (!integration) return {}
|
||||
|
||||
const { name, description, operations } = integration
|
||||
const opSample = operations
|
||||
.slice(0, 3)
|
||||
.map((o) => o.name)
|
||||
.join(', ')
|
||||
const metaDesc = `Automate ${name} with AI-powered workflows on Sim. ${description.slice(0, 100).trimEnd()}. Free to start.`
|
||||
|
||||
return {
|
||||
title: `${name} Integration`,
|
||||
description: metaDesc,
|
||||
keywords: [
|
||||
`${name} automation`,
|
||||
`${name} integration`,
|
||||
`automate ${name}`,
|
||||
`connect ${name}`,
|
||||
`${name} workflow`,
|
||||
`${name} AI automation`,
|
||||
...(opSample ? [`${name} ${opSample}`] : []),
|
||||
'workflow automation',
|
||||
'no-code automation',
|
||||
'AI agent workflow',
|
||||
],
|
||||
openGraph: {
|
||||
title: `${name} Integration — AI Workflow Automation | Sim`,
|
||||
description: `Connect ${name} to ${INTEGRATION_COUNT - 1}+ tools using AI agents. ${description.slice(0, 100).trimEnd()}.`,
|
||||
url: `https://sim.ai/integrations/${slug}`,
|
||||
type: 'website',
|
||||
images: [{ url: 'https://sim.ai/opengraph-image.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: `${name} Integration | Sim`,
|
||||
description: `Automate ${name} with AI-powered workflows. Connect to ${INTEGRATION_COUNT - 1}+ tools. Free to start.`,
|
||||
},
|
||||
alternates: { canonical: `https://sim.ai/integrations/${slug}` },
|
||||
}
|
||||
}
|
||||
|
||||
export default async function IntegrationPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params
|
||||
const integration = bySlug.get(slug)
|
||||
if (!integration) notFound()
|
||||
|
||||
const { name, description, longDescription, bgColor, docsUrl, operations, triggers, authType } =
|
||||
integration
|
||||
|
||||
const IconComponent = blockTypeToIconMap[integration.type]
|
||||
const faqs = buildFAQs(integration)
|
||||
const relatedSlugs = getRelatedSlugs(slug, operations, authType)
|
||||
const relatedIntegrations = relatedSlugs
|
||||
.map((s) => bySlug.get(s))
|
||||
.filter((i): i is Integration => i !== undefined)
|
||||
const baseType = integration.type.replace(/_v\d+$/, '')
|
||||
const matchingTemplates = TEMPLATES.filter(
|
||||
(t) =>
|
||||
t.integrationBlockTypes.includes(integration.type) ||
|
||||
t.integrationBlockTypes.includes(baseType)
|
||||
)
|
||||
|
||||
const breadcrumbJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://sim.ai' },
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 2,
|
||||
name: 'Integrations',
|
||||
item: 'https://sim.ai/integrations',
|
||||
},
|
||||
{ '@type': 'ListItem', position: 3, name, item: `https://sim.ai/integrations/${slug}` },
|
||||
],
|
||||
}
|
||||
|
||||
const softwareAppJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: `${name} Integration`,
|
||||
description,
|
||||
url: `https://sim.ai/integrations/${slug}`,
|
||||
applicationCategory: 'BusinessApplication',
|
||||
operatingSystem: 'Web',
|
||||
featureList: operations.map((o) => o.name),
|
||||
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
||||
}
|
||||
|
||||
const howToJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'HowTo',
|
||||
name: `How to automate ${name} with Sim`,
|
||||
description: `Step-by-step guide to connecting ${name} to AI-powered workflows in Sim.`,
|
||||
step: [
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
position: 1,
|
||||
name: 'Create a free Sim account',
|
||||
text: 'Sign up at sim.ai — no credit card required.',
|
||||
},
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
position: 2,
|
||||
name: `Add a ${name} block`,
|
||||
text: `Open a workflow, drag a ${name} block onto the canvas, and authenticate with your ${name} credentials.`,
|
||||
},
|
||||
{
|
||||
'@type': 'HowToStep',
|
||||
position: 3,
|
||||
name: 'Configure and run',
|
||||
text: `Choose the operation you want, connect it to an AI agent, and run your workflow. Automate anything in ${name} without code.`,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const faqJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqs.map(({ question, answer }) => ({
|
||||
'@type': 'Question',
|
||||
name: question,
|
||||
acceptedAnswer: { '@type': 'Answer', text: answer },
|
||||
})),
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
|
||||
/>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(softwareAppJsonLd) }}
|
||||
/>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(howToJsonLd) }}
|
||||
/>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
|
||||
/>
|
||||
|
||||
<div className='mx-auto max-w-[1200px] px-6 py-12 sm:px-8 md:px-12'>
|
||||
{/* Breadcrumb */}
|
||||
<nav
|
||||
aria-label='Breadcrumb'
|
||||
className='mb-10 flex items-center gap-2 text-[#555] text-[13px]'
|
||||
>
|
||||
<Link href='/' className='transition-colors hover:text-[#999]'>
|
||||
Home
|
||||
</Link>
|
||||
<span aria-hidden='true'>/</span>
|
||||
<Link href='/integrations' className='transition-colors hover:text-[#999]'>
|
||||
Integrations
|
||||
</Link>
|
||||
<span aria-hidden='true'>/</span>
|
||||
<span className='text-[#999]'>{name}</span>
|
||||
</nav>
|
||||
|
||||
{/* Hero */}
|
||||
<section aria-labelledby='integration-heading' className='mb-16'>
|
||||
<div className='mb-6 flex items-center gap-5'>
|
||||
<IntegrationIcon
|
||||
bgColor={bgColor}
|
||||
name={name}
|
||||
Icon={IconComponent}
|
||||
className='h-16 w-16 rounded-xl'
|
||||
iconClassName='h-8 w-8'
|
||||
fallbackClassName='text-[26px]'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<div>
|
||||
<p className='mb-0.5 text-[#555] text-[12px]'>Integration</p>
|
||||
<h1
|
||||
id='integration-heading'
|
||||
className='font-[500] text-[#ECECEC] text-[36px] leading-tight sm:text-[44px]'
|
||||
>
|
||||
{name}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className='mb-8 max-w-[700px] text-[#999] text-[17px] leading-[1.7]'>{description}</p>
|
||||
|
||||
{/* CTAs */}
|
||||
<div className='flex flex-wrap gap-[8px]'>
|
||||
<a
|
||||
href='https://sim.ai'
|
||||
className='inline-flex h-[32px] items-center rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
>
|
||||
Start building free
|
||||
</a>
|
||||
<a
|
||||
href={docsUrl}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='inline-flex h-[32px] items-center gap-[6px] rounded-[5px] border border-[#3d3d3d] px-[10px] font-[430] font-season text-[#ECECEC] text-[14px] transition-colors hover:bg-[#2A2A2A]'
|
||||
>
|
||||
View docs
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
className='h-3 w-3'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth={2}
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' />
|
||||
<polyline points='15 3 21 3 21 9' />
|
||||
<line x1='10' x2='21' y1='14' y2='3' />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Two-column layout */}
|
||||
<div className='grid grid-cols-1 gap-16 lg:grid-cols-[1fr_300px]'>
|
||||
{/* Main column */}
|
||||
<div className='min-w-0 space-y-16'>
|
||||
{/* Overview */}
|
||||
{longDescription && (
|
||||
<section aria-labelledby='overview-heading'>
|
||||
<h2 id='overview-heading' className='mb-4 font-[500] text-[#ECECEC] text-[20px]'>
|
||||
Overview
|
||||
</h2>
|
||||
<p className='text-[#999] text-[15px] leading-[1.8]'>{longDescription}</p>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* How to automate — targets "how to connect X" queries */}
|
||||
<section aria-labelledby='how-it-works-heading'>
|
||||
<h2 id='how-it-works-heading' className='mb-6 font-[500] text-[#ECECEC] text-[20px]'>
|
||||
How to automate {name} with Sim
|
||||
</h2>
|
||||
<ol className='space-y-4' aria-label='Steps to set up automation'>
|
||||
{[
|
||||
{
|
||||
step: '01',
|
||||
title: 'Create a free account',
|
||||
body: 'Sign up at sim.ai in seconds. No credit card required. Your workspace is ready immediately.',
|
||||
},
|
||||
{
|
||||
step: '02',
|
||||
title: `Add a ${name} block`,
|
||||
body:
|
||||
authType === 'oauth'
|
||||
? `Open a workflow, drag a ${name} block onto the canvas, and connect your account with one-click OAuth.`
|
||||
: authType === 'api-key'
|
||||
? `Open a workflow, drag a ${name} block onto the canvas, and paste in your ${name} API key.`
|
||||
: `Open a workflow, drag a ${name} block onto the canvas, and authenticate your account.`,
|
||||
},
|
||||
{
|
||||
step: '03',
|
||||
title: 'Configure, connect, and run',
|
||||
body: `Pick the tool you need, wire in an AI agent for reasoning or data transformation, and run. Your ${name} automation is live.`,
|
||||
},
|
||||
].map(({ step, title, body }) => (
|
||||
<li key={step} className='flex gap-4'>
|
||||
<span
|
||||
className='mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-[#3d3d3d] font-[500] text-[#555] text-[11px]'
|
||||
aria-hidden='true'
|
||||
>
|
||||
{step}
|
||||
</span>
|
||||
<div>
|
||||
<h3 className='mb-1 font-[500] text-[#ECECEC] text-[15px]'>{title}</h3>
|
||||
<p className='text-[#999] text-[14px] leading-relaxed'>{body}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{/* Triggers */}
|
||||
{triggers.length > 0 && (
|
||||
<section aria-labelledby='triggers-heading'>
|
||||
<h2 id='triggers-heading' className='mb-2 font-[500] text-[#ECECEC] text-[20px]'>
|
||||
Real-time triggers
|
||||
</h2>
|
||||
<p className='mb-4 text-[#999] text-[14px] leading-relaxed'>
|
||||
Connect a {name} webhook to Sim and your workflow fires the instant an event
|
||||
happens — no polling, no delay. Sim receives the full event payload and makes
|
||||
every field available as a variable inside your workflow.
|
||||
</p>
|
||||
|
||||
{/* Event cards */}
|
||||
<ul
|
||||
className='grid grid-cols-1 gap-3 sm:grid-cols-2'
|
||||
aria-label={`${name} trigger events`}
|
||||
>
|
||||
{triggers.map((trigger) => (
|
||||
<li
|
||||
key={trigger.id}
|
||||
className='rounded-lg border border-[#2A2A2A] bg-[#242424] p-4'
|
||||
>
|
||||
<div className='mb-2 flex items-center gap-2'>
|
||||
<span className='inline-flex items-center gap-1 rounded-[4px] bg-[#2A2A2A] px-1.5 py-0.5 font-[500] text-[#ECECEC] text-[11px]'>
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
className='h-2.5 w-2.5'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth={2.5}
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<polygon points='13 2 3 14 12 14 11 22 21 10 12 10 13 2' />
|
||||
</svg>
|
||||
Event
|
||||
</span>
|
||||
</div>
|
||||
<p className='font-[500] text-[#ECECEC] text-[13px]'>{trigger.name}</p>
|
||||
{trigger.description && (
|
||||
<p className='mt-1 text-[#999] text-[12px] leading-relaxed'>
|
||||
{trigger.description}
|
||||
</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Workflow templates */}
|
||||
{matchingTemplates.length > 0 && (
|
||||
<section aria-labelledby='templates-heading'>
|
||||
<h2 id='templates-heading' className='mb-2 font-[500] text-[#ECECEC] text-[20px]'>
|
||||
Workflow templates
|
||||
</h2>
|
||||
<p className='mb-6 text-[#999] text-[14px]'>
|
||||
Ready-to-use workflows featuring {name}. Click any to build it instantly.
|
||||
</p>
|
||||
<ul
|
||||
className='grid grid-cols-1 gap-4 sm:grid-cols-2'
|
||||
aria-label='Workflow templates'
|
||||
>
|
||||
{matchingTemplates.map((template) => {
|
||||
const allTypes = [
|
||||
integration.type,
|
||||
...template.integrationBlockTypes.filter((bt) => bt !== integration.type),
|
||||
]
|
||||
|
||||
return (
|
||||
<li key={template.title}>
|
||||
<TemplateCardButton prompt={template.prompt}>
|
||||
{/* Integration pills row */}
|
||||
<div className='mb-3 flex flex-wrap items-center gap-1.5 text-[12px]'>
|
||||
{allTypes.map((bt, idx) => {
|
||||
// Templates may use unversioned keys (e.g. "notion") while the
|
||||
// icon map has versioned keys ("notion_v2") — fall back to _v2.
|
||||
const resolvedBt = byType.get(bt)
|
||||
? bt
|
||||
: byType.get(`${bt}_v2`)
|
||||
? `${bt}_v2`
|
||||
: bt
|
||||
const int = byType.get(resolvedBt)
|
||||
const intName = int?.name ?? bt
|
||||
return (
|
||||
<span key={bt} className='inline-flex items-center gap-1.5'>
|
||||
{idx > 0 && (
|
||||
<span className='text-[#555]' aria-hidden='true'>
|
||||
→
|
||||
</span>
|
||||
)}
|
||||
<span className='inline-flex items-center gap-1 rounded-[3px] bg-[#2A2A2A] px-1.5 py-0.5 font-[500] text-[#ECECEC]'>
|
||||
<IntegrationIcon
|
||||
bgColor={int?.bgColor ?? '#6B7280'}
|
||||
name={intName}
|
||||
Icon={blockTypeToIconMap[resolvedBt]}
|
||||
as='span'
|
||||
className='h-3.5 w-3.5 rounded-[2px]'
|
||||
iconClassName='h-2.5 w-2.5'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
{intName}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<p className='mb-1 font-[500] text-[#ECECEC] text-[14px]'>
|
||||
{template.title}
|
||||
</p>
|
||||
|
||||
<p className='mt-3 text-[#555] text-[13px] transition-colors group-hover:text-[#999]'>
|
||||
Try this workflow →
|
||||
</p>
|
||||
</TemplateCardButton>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Tools */}
|
||||
{operations.length > 0 && (
|
||||
<section aria-labelledby='tools-heading'>
|
||||
<h2 id='tools-heading' className='mb-2 font-[500] text-[#ECECEC] text-[20px]'>
|
||||
Supported tools
|
||||
</h2>
|
||||
<p className='mb-6 text-[#999] text-[14px]'>
|
||||
{operations.length} {name} tool{operations.length === 1 ? '' : 's'} available in
|
||||
Sim
|
||||
</p>
|
||||
<ul
|
||||
className='grid grid-cols-1 gap-2 sm:grid-cols-2'
|
||||
aria-label={`${name} supported tools`}
|
||||
>
|
||||
{operations.map((op) => (
|
||||
<li
|
||||
key={op.name}
|
||||
className='rounded-[6px] border border-[#2A2A2A] bg-[#242424] px-3.5 py-3'
|
||||
>
|
||||
<p className='font-[500] text-[#ECECEC] text-[13px]'>{op.name}</p>
|
||||
{op.description && (
|
||||
<p className='mt-0.5 text-[#555] text-[12px] leading-relaxed'>
|
||||
{op.description}
|
||||
</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* FAQ */}
|
||||
<section aria-labelledby='faq-heading'>
|
||||
<h2 id='faq-heading' className='mb-8 font-[500] text-[#ECECEC] text-[20px]'>
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<IntegrationFAQ faqs={faqs} />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside className='space-y-5' aria-label='Integration details'>
|
||||
{/* Quick details */}
|
||||
<div className='rounded-lg border border-[#2A2A2A] bg-[#242424] p-5'>
|
||||
<h3 className='mb-4 font-[500] text-[#ECECEC] text-[14px]'>Details</h3>
|
||||
<dl className='space-y-3 text-[13px]'>
|
||||
{operations.length > 0 && (
|
||||
<div>
|
||||
<dt className='text-[#555]'>Tools</dt>
|
||||
<dd className='text-[#ECECEC]'>{operations.length} supported</dd>
|
||||
</div>
|
||||
)}
|
||||
{triggers.length > 0 && (
|
||||
<div>
|
||||
<dt className='text-[#555]'>Triggers</dt>
|
||||
<dd className='text-[#ECECEC]'>{triggers.length} available</dd>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<dt className='text-[#555]'>Auth</dt>
|
||||
<dd className='text-[#ECECEC]'>
|
||||
{authType === 'oauth'
|
||||
? 'One-click OAuth'
|
||||
: authType === 'api-key'
|
||||
? 'API key'
|
||||
: 'None required'}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className='text-[#555]'>Pricing</dt>
|
||||
<dd className='text-[#ECECEC]'>Free to start</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className='mt-5 flex flex-col gap-2'>
|
||||
<a
|
||||
href='https://sim.ai'
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] font-[430] font-season text-[#1C1C1C] text-[13px] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
>
|
||||
Get started free
|
||||
</a>
|
||||
<a
|
||||
href={docsUrl}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex h-[32px] w-full items-center justify-center gap-1.5 rounded-[5px] border border-[#3d3d3d] font-[430] font-season text-[#ECECEC] text-[13px] transition-colors hover:bg-[#2A2A2A]'
|
||||
>
|
||||
View docs
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
className='h-3 w-3'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth={2}
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' />
|
||||
<polyline points='15 3 21 3 21 9' />
|
||||
<line x1='10' x2='21' y1='14' y2='3' />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Related integrations — internal linking for SEO */}
|
||||
{relatedIntegrations.length > 0 && (
|
||||
<div className='rounded-lg border border-[#2A2A2A] bg-[#242424] p-5'>
|
||||
<h3 className='mb-4 font-[500] text-[#ECECEC] text-[14px]'>Related integrations</h3>
|
||||
<ul className='space-y-2'>
|
||||
{relatedIntegrations.map((rel) => (
|
||||
<li key={rel.slug}>
|
||||
<Link
|
||||
href={`/integrations/${rel.slug}`}
|
||||
className='flex items-center gap-2.5 rounded-[6px] p-1.5 text-[#999] text-[13px] transition-colors hover:bg-[#2A2A2A] hover:text-[#ECECEC]'
|
||||
>
|
||||
<IntegrationIcon
|
||||
bgColor={rel.bgColor}
|
||||
name={rel.name}
|
||||
Icon={blockTypeToIconMap[rel.type]}
|
||||
as='span'
|
||||
className='h-6 w-6 rounded-[4px]'
|
||||
iconClassName='h-3.5 w-3.5'
|
||||
fallbackClassName='text-[10px]'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
{rel.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Link
|
||||
href='/integrations'
|
||||
className='mt-4 block text-[#555] text-[12px] transition-colors hover:text-[#999]'
|
||||
>
|
||||
All integrations →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* Bottom CTA */}
|
||||
<section
|
||||
aria-labelledby='cta-heading'
|
||||
className='mt-20 rounded-xl border border-[#2A2A2A] bg-[#242424] p-8 text-center sm:p-12'
|
||||
>
|
||||
{/* Logo pair: Sim × Integration */}
|
||||
<div className='mx-auto mb-6 flex items-center justify-center gap-3'>
|
||||
<img
|
||||
src='/brandbook/logo/small.png'
|
||||
alt='Sim'
|
||||
className='h-14 w-14 shrink-0 rounded-xl'
|
||||
/>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='h-px w-5 bg-[#3d3d3d]' aria-hidden='true' />
|
||||
<span
|
||||
className='flex h-7 w-7 items-center justify-center rounded-full border border-[#3d3d3d]'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<svg
|
||||
className='h-3.5 w-3.5 text-[#666]'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth={2}
|
||||
strokeLinecap='round'
|
||||
>
|
||||
<path d='M5 12h14' />
|
||||
<path d='M12 5v14' />
|
||||
</svg>
|
||||
</span>
|
||||
<span className='h-px w-5 bg-[#3d3d3d]' aria-hidden='true' />
|
||||
</div>
|
||||
<IntegrationIcon
|
||||
bgColor={bgColor}
|
||||
name={name}
|
||||
Icon={IconComponent}
|
||||
className='h-14 w-14 rounded-xl'
|
||||
iconClassName='h-7 w-7'
|
||||
fallbackClassName='text-[22px]'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</div>
|
||||
<h2
|
||||
id='cta-heading'
|
||||
className='mb-3 font-[500] text-[#ECECEC] text-[28px] sm:text-[34px]'
|
||||
>
|
||||
Start automating {name} today
|
||||
</h2>
|
||||
<p className='mx-auto mb-8 max-w-[480px] text-[#999] text-[16px] leading-relaxed'>
|
||||
Build your first AI workflow with {name} in minutes. Connect to every tool your team
|
||||
uses. Free to start — no credit card required.
|
||||
</p>
|
||||
<a
|
||||
href='https://sim.ai'
|
||||
className='inline-flex h-[32px] items-center rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
>
|
||||
Build for free →
|
||||
</a>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { ComponentType, SVGProps } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import type { Integration } from '@/app/(landing)/integrations/data/types'
|
||||
import { IntegrationIcon } from './integration-icon'
|
||||
|
||||
interface IntegrationCardProps {
|
||||
integration: Integration
|
||||
IconComponent?: ComponentType<SVGProps<SVGSVGElement>>
|
||||
}
|
||||
|
||||
export function IntegrationCard({ integration, IconComponent }: IntegrationCardProps) {
|
||||
const { slug, name, description, bgColor, operationCount, triggerCount } = integration
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/integrations/${slug}`}
|
||||
className='group flex flex-col rounded-lg border border-[#2A2A2A] bg-[#242424] p-4 transition-colors hover:border-[#3d3d3d] hover:bg-[#2A2A2A]'
|
||||
aria-label={`${name} integration`}
|
||||
>
|
||||
<IntegrationIcon
|
||||
bgColor={bgColor}
|
||||
name={name}
|
||||
Icon={IconComponent}
|
||||
className='mb-3 h-10 w-10 rounded-lg'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
|
||||
{/* Name */}
|
||||
<h3 className='mb-1 font-[500] text-[#ECECEC] text-[14px] leading-snug'>{name}</h3>
|
||||
|
||||
{/* Description — clamped to 2 lines */}
|
||||
<p className='mb-3 line-clamp-2 flex-1 text-[#999] text-[12px] leading-relaxed'>
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{/* Footer row */}
|
||||
<div className='flex flex-wrap items-center gap-1.5'>
|
||||
{operationCount > 0 && (
|
||||
<Badge className='border-0 bg-[#333] text-[#999] text-[11px]'>
|
||||
{operationCount} {operationCount === 1 ? 'tool' : 'tools'}
|
||||
</Badge>
|
||||
)}
|
||||
{triggerCount > 0 && (
|
||||
<Badge className='border-0 bg-[#333] text-[#999] text-[11px]'>
|
||||
{triggerCount} {triggerCount === 1 ? 'trigger' : 'triggers'}
|
||||
</Badge>
|
||||
)}
|
||||
<span className='ml-auto text-[#555] text-[12px] transition-colors group-hover:text-[#999]'>
|
||||
Learn more →
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Input } from '@/components/emcn'
|
||||
import { blockTypeToIconMap } from '@/app/(landing)/integrations/data/icon-mapping'
|
||||
import type { Integration } from '@/app/(landing)/integrations/data/types'
|
||||
import { IntegrationCard } from './integration-card'
|
||||
|
||||
interface IntegrationGridProps {
|
||||
integrations: Integration[]
|
||||
}
|
||||
|
||||
export function IntegrationGrid({ integrations }: IntegrationGridProps) {
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
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])
|
||||
|
||||
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'
|
||||
>
|
||||
<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>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<p className='py-12 text-center text-[#555] text-[15px]'>
|
||||
No integrations found for “{query}”
|
||||
</p>
|
||||
) : (
|
||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
|
||||
{filtered.map((integration) => (
|
||||
<IntegrationCard
|
||||
key={integration.type}
|
||||
integration={integration}
|
||||
IconComponent={blockTypeToIconMap[integration.type]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { ComponentType, ElementType, HTMLAttributes, SVGProps } from 'react'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
interface IntegrationIconProps extends HTMLAttributes<HTMLElement> {
|
||||
bgColor: string
|
||||
/** Integration name — used for the fallback initial letter. */
|
||||
name: string
|
||||
/** Optional icon component. When absent, renders the first letter of `name`. */
|
||||
Icon?: ComponentType<SVGProps<SVGSVGElement>> | null
|
||||
/** Tailwind size + rounding classes for the container. Default: `h-10 w-10 rounded-lg` */
|
||||
className?: string
|
||||
/** Tailwind size classes for the icon SVG. Default: `h-5 w-5` */
|
||||
iconClassName?: string
|
||||
/** Tailwind text-size class for the fallback letter. Default: `text-[15px]` */
|
||||
fallbackClassName?: string
|
||||
/** Rendered HTML element. Default: `div` */
|
||||
as?: ElementType
|
||||
}
|
||||
|
||||
/**
|
||||
* Colored icon box used across integration listing and detail pages.
|
||||
* Renders an integration icon over a brand-colored background, falling back
|
||||
* to the integration's initial letter when no icon is available.
|
||||
*/
|
||||
export function IntegrationIcon({
|
||||
bgColor,
|
||||
name,
|
||||
Icon,
|
||||
className,
|
||||
iconClassName = 'h-5 w-5',
|
||||
fallbackClassName = 'text-[15px]',
|
||||
as: Tag = 'div',
|
||||
...rest
|
||||
}: IntegrationIconProps) {
|
||||
return (
|
||||
<Tag
|
||||
className={cn('flex shrink-0 items-center justify-center', className)}
|
||||
style={{ background: bgColor }}
|
||||
{...rest}
|
||||
>
|
||||
{Icon ? (
|
||||
<Icon className={cn(iconClassName, 'text-white')} />
|
||||
) : (
|
||||
<span className={cn('font-[500] text-white leading-none', fallbackClassName)}>
|
||||
{name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Textarea,
|
||||
} from '@/components/emcn'
|
||||
|
||||
type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error'
|
||||
|
||||
export function RequestIntegrationModal() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [status, setStatus] = useState<SubmitStatus>('idle')
|
||||
|
||||
const [integrationName, setIntegrationName] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [useCase, setUseCase] = useState('')
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
setIntegrationName('')
|
||||
setEmail('')
|
||||
setUseCase('')
|
||||
setStatus('idle')
|
||||
}, [])
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(nextOpen: boolean) => {
|
||||
setOpen(nextOpen)
|
||||
if (!nextOpen) resetForm()
|
||||
},
|
||||
[resetForm]
|
||||
)
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!integrationName.trim() || !email.trim()) return
|
||||
|
||||
setStatus('submitting')
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/help/integration-request', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
integrationName: integrationName.trim(),
|
||||
email: email.trim(),
|
||||
useCase: useCase.trim() || undefined,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!res.ok) throw new Error('Request failed')
|
||||
|
||||
setStatus('success')
|
||||
setTimeout(() => setOpen(false), 1500)
|
||||
} catch {
|
||||
setStatus('error')
|
||||
}
|
||||
},
|
||||
[integrationName, email, useCase]
|
||||
)
|
||||
|
||||
const canSubmit = integrationName.trim() && email.trim() && status === 'idle'
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setOpen(true)}
|
||||
className='inline-flex h-[32px] shrink-0 items-center gap-[6px] rounded-[5px] border border-[#3d3d3d] px-[10px] font-[430] font-season text-[#ECECEC] text-[14px] transition-colors hover:bg-[#2A2A2A]'
|
||||
>
|
||||
Request an integration
|
||||
</button>
|
||||
|
||||
<Modal open={open} onOpenChange={handleOpenChange}>
|
||||
<ModalContent size='sm'>
|
||||
<ModalHeader>Request an Integration</ModalHeader>
|
||||
|
||||
{status === 'success' ? (
|
||||
<ModalBody>
|
||||
<div className='flex flex-col items-center gap-3 py-6 text-center'>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-full bg-[#33C482]/10'>
|
||||
<svg
|
||||
className='h-5 w-5 text-[#33C482]'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth={2}
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
<polyline points='20 6 9 17 4 12' />
|
||||
</svg>
|
||||
</div>
|
||||
<p className='text-[14px] text-[var(--text-primary)]'>
|
||||
Request submitted — we'll follow up at{' '}
|
||||
<span className='font-medium'>{email}</span>.
|
||||
</p>
|
||||
</div>
|
||||
</ModalBody>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className='flex min-h-0 flex-1 flex-col'>
|
||||
<ModalBody>
|
||||
<div className='space-y-[12px]'>
|
||||
<div className='flex flex-col gap-[4px]'>
|
||||
<Label htmlFor='integration-name'>Integration name</Label>
|
||||
<Input
|
||||
id='integration-name'
|
||||
placeholder='e.g. Stripe, HubSpot, Snowflake'
|
||||
value={integrationName}
|
||||
onChange={(e) => setIntegrationName(e.target.value)}
|
||||
maxLength={200}
|
||||
autoComplete='off'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[4px]'>
|
||||
<Label htmlFor='requester-email'>Your email</Label>
|
||||
<Input
|
||||
id='requester-email'
|
||||
type='email'
|
||||
placeholder='you@company.com'
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete='email'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[4px]'>
|
||||
<Label htmlFor='use-case'>
|
||||
Use case <span className='text-[var(--text-tertiary)]'>(optional)</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
id='use-case'
|
||||
placeholder='What would you automate with this integration?'
|
||||
value={useCase}
|
||||
onChange={(e) => setUseCase(e.target.value)}
|
||||
rows={3}
|
||||
maxLength={2000}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status === 'error' && (
|
||||
<p className='text-[13px] text-[var(--text-error)]'>
|
||||
Something went wrong. Please try again.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
type='button'
|
||||
variant='default'
|
||||
onClick={() => setOpen(false)}
|
||||
disabled={status === 'submitting'}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type='submit' variant='primary' disabled={!canSubmit && status !== 'error'}>
|
||||
{status === 'submitting' ? 'Submitting...' : 'Submit request'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
// Auto-generated file - do not edit manually
|
||||
// Generated by scripts/generate-docs.ts
|
||||
// Maps block types to their icon component references for the integrations page
|
||||
|
||||
import type { ComponentType, SVGProps } from 'react'
|
||||
import {
|
||||
A2AIcon,
|
||||
AhrefsIcon,
|
||||
AirtableIcon,
|
||||
AirweaveIcon,
|
||||
AlgoliaIcon,
|
||||
AmplitudeIcon,
|
||||
ApifyIcon,
|
||||
ApolloIcon,
|
||||
ArxivIcon,
|
||||
AsanaIcon,
|
||||
AshbyIcon,
|
||||
AttioIcon,
|
||||
AzureIcon,
|
||||
BoxCompanyIcon,
|
||||
BrainIcon,
|
||||
BrandfetchIcon,
|
||||
BrowserUseIcon,
|
||||
CalComIcon,
|
||||
CalendlyIcon,
|
||||
CirclebackIcon,
|
||||
ClayIcon,
|
||||
ClerkIcon,
|
||||
CloudflareIcon,
|
||||
ConfluenceIcon,
|
||||
CursorIcon,
|
||||
DatabricksIcon,
|
||||
DatadogIcon,
|
||||
DevinIcon,
|
||||
DiscordIcon,
|
||||
DocumentIcon,
|
||||
DocuSignIcon,
|
||||
DropboxIcon,
|
||||
DsPyIcon,
|
||||
DubIcon,
|
||||
DuckDuckGoIcon,
|
||||
DynamoDBIcon,
|
||||
ElasticsearchIcon,
|
||||
ElevenLabsIcon,
|
||||
EnrichSoIcon,
|
||||
EvernoteIcon,
|
||||
ExaAIIcon,
|
||||
EyeIcon,
|
||||
FathomIcon,
|
||||
FirecrawlIcon,
|
||||
FirefliesIcon,
|
||||
GammaIcon,
|
||||
GithubIcon,
|
||||
GitLabIcon,
|
||||
GmailIcon,
|
||||
GongIcon,
|
||||
GoogleAdsIcon,
|
||||
GoogleBigQueryIcon,
|
||||
GoogleBooksIcon,
|
||||
GoogleCalendarIcon,
|
||||
GoogleContactsIcon,
|
||||
GoogleDocsIcon,
|
||||
GoogleDriveIcon,
|
||||
GoogleFormsIcon,
|
||||
GoogleGroupsIcon,
|
||||
GoogleIcon,
|
||||
GoogleMapsIcon,
|
||||
GoogleMeetIcon,
|
||||
GooglePagespeedIcon,
|
||||
GoogleSheetsIcon,
|
||||
GoogleSlidesIcon,
|
||||
GoogleTasksIcon,
|
||||
GoogleTranslateIcon,
|
||||
GoogleVaultIcon,
|
||||
GrafanaIcon,
|
||||
GrainIcon,
|
||||
GreenhouseIcon,
|
||||
GreptileIcon,
|
||||
HexIcon,
|
||||
HubspotIcon,
|
||||
HuggingFaceIcon,
|
||||
HunterIOIcon,
|
||||
ImageIcon,
|
||||
IncidentioIcon,
|
||||
InfisicalIcon,
|
||||
IntercomIcon,
|
||||
JinaAIIcon,
|
||||
JiraIcon,
|
||||
JiraServiceManagementIcon,
|
||||
KalshiIcon,
|
||||
LangsmithIcon,
|
||||
LemlistIcon,
|
||||
LinearIcon,
|
||||
LinkedInIcon,
|
||||
LinkupIcon,
|
||||
LoopsIcon,
|
||||
LumaIcon,
|
||||
MailchimpIcon,
|
||||
MailgunIcon,
|
||||
MailServerIcon,
|
||||
Mem0Icon,
|
||||
MicrosoftDataverseIcon,
|
||||
MicrosoftExcelIcon,
|
||||
MicrosoftOneDriveIcon,
|
||||
MicrosoftPlannerIcon,
|
||||
MicrosoftSharepointIcon,
|
||||
MicrosoftTeamsIcon,
|
||||
MistralIcon,
|
||||
MongoDBIcon,
|
||||
MySQLIcon,
|
||||
Neo4jIcon,
|
||||
NotionIcon,
|
||||
ObsidianIcon,
|
||||
OktaIcon,
|
||||
OnePasswordIcon,
|
||||
OpenAIIcon,
|
||||
OutlookIcon,
|
||||
PackageSearchIcon,
|
||||
PagerDutyIcon,
|
||||
ParallelIcon,
|
||||
PerplexityIcon,
|
||||
PineconeIcon,
|
||||
PipedriveIcon,
|
||||
PolymarketIcon,
|
||||
PostgresIcon,
|
||||
PosthogIcon,
|
||||
PulseIcon,
|
||||
QdrantIcon,
|
||||
RDSIcon,
|
||||
RedditIcon,
|
||||
RedisIcon,
|
||||
ReductoIcon,
|
||||
ResendIcon,
|
||||
RevenueCatIcon,
|
||||
S3Icon,
|
||||
SalesforceIcon,
|
||||
SearchIcon,
|
||||
SendgridIcon,
|
||||
SentryIcon,
|
||||
SerperIcon,
|
||||
ServiceNowIcon,
|
||||
SftpIcon,
|
||||
ShopifyIcon,
|
||||
SimilarwebIcon,
|
||||
SlackIcon,
|
||||
SmtpIcon,
|
||||
SQSIcon,
|
||||
SshIcon,
|
||||
STTIcon,
|
||||
StagehandIcon,
|
||||
StripeIcon,
|
||||
SupabaseIcon,
|
||||
TavilyIcon,
|
||||
TelegramIcon,
|
||||
TextractIcon,
|
||||
TinybirdIcon,
|
||||
TranslateIcon,
|
||||
TrelloIcon,
|
||||
TTSIcon,
|
||||
TwilioIcon,
|
||||
TypeformIcon,
|
||||
UpstashIcon,
|
||||
VercelIcon,
|
||||
VideoIcon,
|
||||
WealthboxIcon,
|
||||
WebflowIcon,
|
||||
WhatsAppIcon,
|
||||
WikipediaIcon,
|
||||
WordpressIcon,
|
||||
WorkdayIcon,
|
||||
xIcon,
|
||||
YouTubeIcon,
|
||||
ZendeskIcon,
|
||||
ZepIcon,
|
||||
ZoomIcon,
|
||||
} from '@/components/icons'
|
||||
|
||||
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
|
||||
|
||||
export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
a2a: A2AIcon,
|
||||
ahrefs: AhrefsIcon,
|
||||
airtable: AirtableIcon,
|
||||
airweave: AirweaveIcon,
|
||||
algolia: AlgoliaIcon,
|
||||
amplitude: AmplitudeIcon,
|
||||
apify: ApifyIcon,
|
||||
apollo: ApolloIcon,
|
||||
arxiv: ArxivIcon,
|
||||
asana: AsanaIcon,
|
||||
ashby: AshbyIcon,
|
||||
attio: AttioIcon,
|
||||
box: BoxCompanyIcon,
|
||||
brandfetch: BrandfetchIcon,
|
||||
browser_use: BrowserUseIcon,
|
||||
calcom: CalComIcon,
|
||||
calendly: CalendlyIcon,
|
||||
circleback: CirclebackIcon,
|
||||
clay: ClayIcon,
|
||||
clerk: ClerkIcon,
|
||||
cloudflare: CloudflareIcon,
|
||||
confluence_v2: ConfluenceIcon,
|
||||
cursor_v2: CursorIcon,
|
||||
databricks: DatabricksIcon,
|
||||
datadog: DatadogIcon,
|
||||
devin: DevinIcon,
|
||||
discord: DiscordIcon,
|
||||
docusign: DocuSignIcon,
|
||||
dropbox: DropboxIcon,
|
||||
dspy: DsPyIcon,
|
||||
dub: DubIcon,
|
||||
duckduckgo: DuckDuckGoIcon,
|
||||
dynamodb: DynamoDBIcon,
|
||||
elasticsearch: ElasticsearchIcon,
|
||||
elevenlabs: ElevenLabsIcon,
|
||||
enrich: EnrichSoIcon,
|
||||
evernote: EvernoteIcon,
|
||||
exa: ExaAIIcon,
|
||||
fathom: FathomIcon,
|
||||
file_v3: DocumentIcon,
|
||||
firecrawl: FirecrawlIcon,
|
||||
fireflies_v2: FirefliesIcon,
|
||||
gamma: GammaIcon,
|
||||
github_v2: GithubIcon,
|
||||
gitlab: GitLabIcon,
|
||||
gmail_v2: GmailIcon,
|
||||
gong: GongIcon,
|
||||
google_ads: GoogleAdsIcon,
|
||||
google_bigquery: GoogleBigQueryIcon,
|
||||
google_books: GoogleBooksIcon,
|
||||
google_calendar_v2: GoogleCalendarIcon,
|
||||
google_contacts: GoogleContactsIcon,
|
||||
google_docs: GoogleDocsIcon,
|
||||
google_drive: GoogleDriveIcon,
|
||||
google_forms: GoogleFormsIcon,
|
||||
google_groups: GoogleGroupsIcon,
|
||||
google_maps: GoogleMapsIcon,
|
||||
google_meet: GoogleMeetIcon,
|
||||
google_pagespeed: GooglePagespeedIcon,
|
||||
google_search: GoogleIcon,
|
||||
google_sheets_v2: GoogleSheetsIcon,
|
||||
google_slides_v2: GoogleSlidesIcon,
|
||||
google_tasks: GoogleTasksIcon,
|
||||
google_translate: GoogleTranslateIcon,
|
||||
google_vault: GoogleVaultIcon,
|
||||
grafana: GrafanaIcon,
|
||||
grain: GrainIcon,
|
||||
greenhouse: GreenhouseIcon,
|
||||
greptile: GreptileIcon,
|
||||
hex: HexIcon,
|
||||
hubspot: HubspotIcon,
|
||||
huggingface: HuggingFaceIcon,
|
||||
hunter: HunterIOIcon,
|
||||
image_generator: ImageIcon,
|
||||
imap: MailServerIcon,
|
||||
incidentio: IncidentioIcon,
|
||||
infisical: InfisicalIcon,
|
||||
intercom_v2: IntercomIcon,
|
||||
jina: JinaAIIcon,
|
||||
jira: JiraIcon,
|
||||
jira_service_management: JiraServiceManagementIcon,
|
||||
kalshi_v2: KalshiIcon,
|
||||
knowledge: PackageSearchIcon,
|
||||
langsmith: LangsmithIcon,
|
||||
lemlist: LemlistIcon,
|
||||
linear: LinearIcon,
|
||||
linkedin: LinkedInIcon,
|
||||
linkup: LinkupIcon,
|
||||
loops: LoopsIcon,
|
||||
luma: LumaIcon,
|
||||
mailchimp: MailchimpIcon,
|
||||
mailgun: MailgunIcon,
|
||||
mem0: Mem0Icon,
|
||||
memory: BrainIcon,
|
||||
microsoft_ad: AzureIcon,
|
||||
microsoft_dataverse: MicrosoftDataverseIcon,
|
||||
microsoft_excel_v2: MicrosoftExcelIcon,
|
||||
microsoft_planner: MicrosoftPlannerIcon,
|
||||
microsoft_teams: MicrosoftTeamsIcon,
|
||||
mistral_parse_v3: MistralIcon,
|
||||
mongodb: MongoDBIcon,
|
||||
mysql: MySQLIcon,
|
||||
neo4j: Neo4jIcon,
|
||||
notion_v2: NotionIcon,
|
||||
obsidian: ObsidianIcon,
|
||||
okta: OktaIcon,
|
||||
onedrive: MicrosoftOneDriveIcon,
|
||||
onepassword: OnePasswordIcon,
|
||||
openai: OpenAIIcon,
|
||||
outlook: OutlookIcon,
|
||||
pagerduty: PagerDutyIcon,
|
||||
parallel_ai: ParallelIcon,
|
||||
perplexity: PerplexityIcon,
|
||||
pinecone: PineconeIcon,
|
||||
pipedrive: PipedriveIcon,
|
||||
polymarket: PolymarketIcon,
|
||||
postgresql: PostgresIcon,
|
||||
posthog: PosthogIcon,
|
||||
pulse_v2: PulseIcon,
|
||||
qdrant: QdrantIcon,
|
||||
rds: RDSIcon,
|
||||
reddit: RedditIcon,
|
||||
redis: RedisIcon,
|
||||
reducto_v2: ReductoIcon,
|
||||
resend: ResendIcon,
|
||||
revenuecat: RevenueCatIcon,
|
||||
s3: S3Icon,
|
||||
salesforce: SalesforceIcon,
|
||||
search: SearchIcon,
|
||||
sendgrid: SendgridIcon,
|
||||
sentry: SentryIcon,
|
||||
serper: SerperIcon,
|
||||
servicenow: ServiceNowIcon,
|
||||
sftp: SftpIcon,
|
||||
sharepoint: MicrosoftSharepointIcon,
|
||||
shopify: ShopifyIcon,
|
||||
similarweb: SimilarwebIcon,
|
||||
slack: SlackIcon,
|
||||
smtp: SmtpIcon,
|
||||
sqs: SQSIcon,
|
||||
ssh: SshIcon,
|
||||
stagehand: StagehandIcon,
|
||||
stripe: StripeIcon,
|
||||
stt_v2: STTIcon,
|
||||
supabase: SupabaseIcon,
|
||||
tavily: TavilyIcon,
|
||||
telegram: TelegramIcon,
|
||||
textract_v2: TextractIcon,
|
||||
tinybird: TinybirdIcon,
|
||||
translate: TranslateIcon,
|
||||
trello: TrelloIcon,
|
||||
tts: TTSIcon,
|
||||
twilio_sms: TwilioIcon,
|
||||
twilio_voice: TwilioIcon,
|
||||
typeform: TypeformIcon,
|
||||
upstash: UpstashIcon,
|
||||
vercel: VercelIcon,
|
||||
video_generator_v2: VideoIcon,
|
||||
vision_v2: EyeIcon,
|
||||
wealthbox: WealthboxIcon,
|
||||
webflow: WebflowIcon,
|
||||
whatsapp: WhatsAppIcon,
|
||||
wikipedia: WikipediaIcon,
|
||||
wordpress: WordpressIcon,
|
||||
workday: WorkdayIcon,
|
||||
x: xIcon,
|
||||
youtube: YouTubeIcon,
|
||||
zendesk: ZendeskIcon,
|
||||
zep: ZepIcon,
|
||||
zoom: ZoomIcon,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,117 +0,0 @@
|
||||
/**
|
||||
* Curated popular workflow pairs used on both the /integrations listing page
|
||||
* and individual /integrations/[slug] pages.
|
||||
*
|
||||
* Each pair targets specific long-tail search queries like "notion to slack automation".
|
||||
* The headline and description are written to be both human-readable and keyword-rich.
|
||||
*/
|
||||
|
||||
export interface WorkflowPair {
|
||||
/** Integration name (must match `name` field in integrations.json) */
|
||||
from: string
|
||||
/** Integration name (must match `name` field in integrations.json) */
|
||||
to: string
|
||||
headline: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export const POPULAR_WORKFLOWS: WorkflowPair[] = [
|
||||
{
|
||||
from: 'Slack',
|
||||
to: 'Notion',
|
||||
headline: 'Archive Slack conversations to Notion',
|
||||
description:
|
||||
'Capture important Slack messages as Notion pages or database entries — ideal for meeting notes, decision logs, and knowledge bases.',
|
||||
},
|
||||
{
|
||||
from: 'Notion',
|
||||
to: 'Slack',
|
||||
headline: 'Notify your team from Notion',
|
||||
description:
|
||||
'Post Slack messages automatically when Notion pages are created or updated so the whole team stays aligned without manual check-ins.',
|
||||
},
|
||||
{
|
||||
from: 'GitHub',
|
||||
to: 'Jira',
|
||||
headline: 'Link GitHub pull requests to Jira tickets',
|
||||
description:
|
||||
'Transition Jira issues when PRs are opened or merged, keeping your project board accurate without any manual updates.',
|
||||
},
|
||||
{
|
||||
from: 'GitHub',
|
||||
to: 'Linear',
|
||||
headline: 'Sync GitHub events with Linear issues',
|
||||
description:
|
||||
'Create Linear issues from GitHub activity, update status on merge, and keep your engineering workflow tightly connected.',
|
||||
},
|
||||
{
|
||||
from: 'Gmail',
|
||||
to: 'Notion',
|
||||
headline: 'Save incoming emails to Notion databases',
|
||||
description:
|
||||
'Extract structured data from Gmail and store it in Notion — ideal for lead capture, support tickets, and meeting scheduling.',
|
||||
},
|
||||
{
|
||||
from: 'HubSpot',
|
||||
to: 'Slack',
|
||||
headline: 'Get HubSpot deal alerts in Slack',
|
||||
description:
|
||||
'Receive instant Slack notifications when HubSpot deals advance, contacts are created, or revenue milestones are hit.',
|
||||
},
|
||||
{
|
||||
from: 'Google Sheets',
|
||||
to: 'Slack',
|
||||
headline: 'Send Slack messages from Google Sheets',
|
||||
description:
|
||||
'Watch a spreadsheet for new rows or changes, then post formatted Slack updates to keep stakeholders informed in real time.',
|
||||
},
|
||||
{
|
||||
from: 'Salesforce',
|
||||
to: 'Slack',
|
||||
headline: 'Push Salesforce pipeline updates to Slack',
|
||||
description:
|
||||
'Alert your sales team in Slack when Salesforce opportunities advance, close, or need immediate attention.',
|
||||
},
|
||||
{
|
||||
from: 'Airtable',
|
||||
to: 'Gmail',
|
||||
headline: 'Trigger Gmail from Airtable records',
|
||||
description:
|
||||
'Send personalised Gmail messages when Airtable records are created or updated — great for onboarding flows and follow-up sequences.',
|
||||
},
|
||||
{
|
||||
from: 'Linear',
|
||||
to: 'Slack',
|
||||
headline: 'Linear issue updates in Slack',
|
||||
description:
|
||||
'Post Slack messages when Linear issues are created, assigned, or completed so your team is always in the loop.',
|
||||
},
|
||||
{
|
||||
from: 'Jira',
|
||||
to: 'Confluence',
|
||||
headline: 'Auto-generate Confluence pages from Jira sprints',
|
||||
description:
|
||||
'Create Confluence documentation from Jira sprint data automatically, eliminating manual reporting at the end of every sprint.',
|
||||
},
|
||||
{
|
||||
from: 'Google Sheets',
|
||||
to: 'Notion',
|
||||
headline: 'Sync Google Sheets data into Notion',
|
||||
description:
|
||||
'Transform spreadsheet rows into structured Notion database entries for richer documentation and cross-team project tracking.',
|
||||
},
|
||||
{
|
||||
from: 'GitHub',
|
||||
to: 'Slack',
|
||||
headline: 'Get GitHub activity alerts in Slack',
|
||||
description:
|
||||
'Post Slack notifications for new PRs, commits, issues, or deployments so your engineering team never misses a critical event.',
|
||||
},
|
||||
{
|
||||
from: 'HubSpot',
|
||||
to: 'Gmail',
|
||||
headline: 'Send personalised emails from HubSpot events',
|
||||
description:
|
||||
'Trigger Gmail messages when HubSpot contacts enter a lifecycle stage, ensuring timely and relevant outreach without manual effort.',
|
||||
},
|
||||
]
|
||||
@@ -1,37 +0,0 @@
|
||||
// Shared types for the integrations section of the landing site.
|
||||
// Mirrors the shape written by scripts/generate-docs.ts → writeIntegrationsJson().
|
||||
|
||||
export type AuthType = 'oauth' | 'api-key' | 'none'
|
||||
|
||||
export interface TriggerInfo {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface OperationInfo {
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface FAQItem {
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
export interface Integration {
|
||||
type: string
|
||||
slug: string
|
||||
name: string
|
||||
description: string
|
||||
longDescription: string
|
||||
bgColor: string
|
||||
iconName: string
|
||||
docsUrl: string
|
||||
operations: OperationInfo[]
|
||||
operationCount: number
|
||||
triggers: TriggerInfo[]
|
||||
triggerCount: number
|
||||
authType: AuthType
|
||||
category: string
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { getNavBlogPosts } from '@/lib/blog/registry'
|
||||
import Footer from '@/app/(home)/components/footer/footer'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
|
||||
export default async function IntegrationsLayout({ children }: { children: React.ReactNode }) {
|
||||
const blogPosts = await getNavBlogPosts()
|
||||
const orgJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
name: 'Sim',
|
||||
url: 'https://sim.ai',
|
||||
logo: 'https://sim.ai/logo/primary/small.png',
|
||||
sameAs: ['https://x.com/simdotai'],
|
||||
}
|
||||
|
||||
const websiteJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
name: 'Sim',
|
||||
url: 'https://sim.ai',
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: 'https://sim.ai/search?q={search_term_string}',
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='dark flex min-h-screen flex-col bg-[#1C1C1C] font-[430] font-season text-[#ECECEC]'>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(orgJsonLd) }}
|
||||
/>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }}
|
||||
/>
|
||||
<header>
|
||||
<Navbar blogPosts={blogPosts} />
|
||||
</header>
|
||||
<main className='relative flex-1'>{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { IntegrationGrid } from './components/integration-grid'
|
||||
import { RequestIntegrationModal } from './components/request-integration-modal'
|
||||
import { blockTypeToIconMap } from './data/icon-mapping'
|
||||
import integrations from './data/integrations.json'
|
||||
import { POPULAR_WORKFLOWS } from './data/popular-workflows'
|
||||
import type { Integration } from './data/types'
|
||||
|
||||
const allIntegrations = integrations as Integration[]
|
||||
const INTEGRATION_COUNT = allIntegrations.length
|
||||
|
||||
/**
|
||||
* Unique integration names that appear in popular workflow pairs.
|
||||
* Used for metadata keywords so they stay in sync automatically.
|
||||
*/
|
||||
const TOP_NAMES = [...new Set(POPULAR_WORKFLOWS.flatMap((p) => [p.from, p.to]))].slice(0, 6)
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Integrations',
|
||||
description: `Connect ${INTEGRATION_COUNT}+ apps and services with Sim's AI workflow automation. Build intelligent pipelines with ${TOP_NAMES.join(', ')}, and more.`,
|
||||
keywords: [
|
||||
'workflow automation integrations',
|
||||
'AI workflow automation',
|
||||
'no-code automation',
|
||||
...TOP_NAMES.flatMap((n) => [`${n} integration`, `${n} automation`]),
|
||||
...allIntegrations.slice(0, 20).map((i) => `${i.name} automation`),
|
||||
],
|
||||
openGraph: {
|
||||
title: 'Integrations for AI Workflow Automation | Sim',
|
||||
description: `Connect ${INTEGRATION_COUNT}+ apps with Sim. Build AI-powered pipelines that link ${TOP_NAMES.join(', ')}, and every tool your team uses.`,
|
||||
url: 'https://sim.ai/integrations',
|
||||
type: 'website',
|
||||
images: [{ url: 'https://sim.ai/opengraph-image.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Integrations | Sim',
|
||||
description: `Connect ${INTEGRATION_COUNT}+ apps with Sim's AI workflow automation.`,
|
||||
},
|
||||
alternates: { canonical: 'https://sim.ai/integrations' },
|
||||
}
|
||||
|
||||
export default function IntegrationsPage() {
|
||||
const breadcrumbJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://sim.ai' },
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 2,
|
||||
name: 'Integrations',
|
||||
item: 'https://sim.ai/integrations',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const itemListJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ItemList',
|
||||
name: 'Sim AI Workflow Integrations',
|
||||
description: `Complete list of ${INTEGRATION_COUNT}+ integrations available in Sim for building AI-powered workflow automation.`,
|
||||
url: 'https://sim.ai/integrations',
|
||||
numberOfItems: INTEGRATION_COUNT,
|
||||
itemListElement: allIntegrations.map((integration, index) => ({
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
item: {
|
||||
'@type': 'SoftwareApplication',
|
||||
name: integration.name,
|
||||
description: integration.description,
|
||||
url: `https://sim.ai/integrations/${integration.slug}`,
|
||||
applicationCategory: 'BusinessApplication',
|
||||
featureList: integration.operations.map((o) => o.name),
|
||||
},
|
||||
})),
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
|
||||
/>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(itemListJsonLd) }}
|
||||
/>
|
||||
|
||||
<div className='mx-auto max-w-[1200px] px-6 py-16 sm:px-8 md:px-12'>
|
||||
{/* Hero */}
|
||||
<section aria-labelledby='integrations-heading' className='mb-16'>
|
||||
<h1
|
||||
id='integrations-heading'
|
||||
className='mb-4 font-[500] text-[#ECECEC] text-[40px] leading-tight sm:text-[56px]'
|
||||
>
|
||||
Integrations
|
||||
</h1>
|
||||
<p className='max-w-[640px] text-[#999] text-[18px] leading-relaxed'>
|
||||
Connect every tool your team uses. Build AI-powered workflows that automate tasks across{' '}
|
||||
{TOP_NAMES.slice(0, 4).map((name, i, arr) => {
|
||||
const integration = allIntegrations.find((int) => int.name === name)
|
||||
const Icon = integration ? blockTypeToIconMap[integration.type] : undefined
|
||||
return (
|
||||
<span key={name} className='inline-flex items-center gap-[5px]'>
|
||||
{Icon && (
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='inline-flex shrink-0'
|
||||
style={{ opacity: 0.65 }}
|
||||
>
|
||||
<Icon className='h-[0.85em] w-[0.85em]' />
|
||||
</span>
|
||||
)}
|
||||
{name}
|
||||
{i < arr.length - 1 ? ', ' : ''}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
{' and more.'}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Searchable grid — client component */}
|
||||
<section aria-labelledby='all-integrations-heading'>
|
||||
<h2 id='all-integrations-heading' className='mb-8 font-[500] text-[#ECECEC] text-[24px]'>
|
||||
All Integrations
|
||||
</h2>
|
||||
<IntegrationGrid integrations={allIntegrations} />
|
||||
</section>
|
||||
|
||||
{/* Integration request */}
|
||||
<div className='mt-16 flex flex-col items-start gap-3 border-[#2A2A2A] border-t pt-10 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div>
|
||||
<p className='font-[500] text-[#ECECEC] text-[15px]'>
|
||||
Don't see the integration you need?
|
||||
</p>
|
||||
<p className='mt-0.5 text-[#555] text-[13px]'>
|
||||
Let us know and we'll prioritize it.
|
||||
</p>
|
||||
</div>
|
||||
<RequestIntegrationModal />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { getEnv } from '@/lib/core/config/env'
|
||||
import { ExternalRedirect, LegalLayout } from '@/app/(landing)/components'
|
||||
import { LegalLayout } from '@/app/(landing)/components'
|
||||
|
||||
export default function PrivacyPolicy() {
|
||||
useEffect(() => {
|
||||
const privacyUrl = getEnv('NEXT_PUBLIC_PRIVACY_URL')
|
||||
if (privacyUrl?.startsWith('http')) {
|
||||
window.location.href = privacyUrl
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<LegalLayout title='Privacy Policy'>
|
||||
<ExternalRedirect url={getEnv('NEXT_PUBLIC_PRIVACY_URL') ?? ''} />
|
||||
<section>
|
||||
<p className='mb-4'>Last Updated: October 11, 2025</p>
|
||||
<p>
|
||||
@@ -574,7 +582,7 @@ export default function PrivacyPolicy() {
|
||||
Please note that we may ask you to verify your identity before responding to such
|
||||
requests.
|
||||
</p>
|
||||
<p className='mb-4 border-[#3d3d3d] border-l-4 bg-[#2A2A2A] p-3 text-[#ECECEC]'>
|
||||
<p className='mb-4 border-[var(--brand-primary-hex)] border-l-4 bg-[var(--brand-primary-hex)]/10 p-3'>
|
||||
You have the right to complain to a Data Protection Authority about our collection and use
|
||||
of your Personal Information. For more information, please contact your local data
|
||||
protection authority in the European Economic Area (EEA).
|
||||
@@ -596,7 +604,10 @@ export default function PrivacyPolicy() {
|
||||
sharing practices (such as analytics or advertising services) may be considered a "sale"
|
||||
or "share" under CCPA/CPRA. You have the right to opt-out of such data sharing. To
|
||||
exercise this right, contact us at{' '}
|
||||
<Link href='mailto:privacy@sim.ai' className='text-[#ECECEC] underline hover:text-white'>
|
||||
<Link
|
||||
href='mailto:privacy@sim.ai'
|
||||
className='text-[var(--brand-primary-hex)] underline hover:text-[var(--brand-primary-hover-hex)]'
|
||||
>
|
||||
privacy@sim.ai
|
||||
</Link>
|
||||
.
|
||||
@@ -682,7 +693,10 @@ export default function PrivacyPolicy() {
|
||||
Sim interacts with are not covered by this policy and should be reported directly to the
|
||||
solution vendor in accordance with their disclosure policy (if any). Before beginning your
|
||||
inquiry, email us at{' '}
|
||||
<Link href='mailto:security@sim.ai' className='text-[#ECECEC] underline hover:text-white'>
|
||||
<Link
|
||||
href='mailto:security@sim.ai'
|
||||
className='text-[var(--brand-primary-hex)] underline hover:text-[var(--brand-primary-hover-hex)]'
|
||||
>
|
||||
security@sim.ai
|
||||
</Link>{' '}
|
||||
if you're unsure whether a system or endpoint is in scope.
|
||||
@@ -701,7 +715,10 @@ export default function PrivacyPolicy() {
|
||||
<h3 className='mb-2 font-medium text-xl'>Reporting a vulnerability</h3>
|
||||
<p className='mb-4'>
|
||||
To report any security flaws, send an email to{' '}
|
||||
<Link href='mailto:security@sim.ai' className='text-[#ECECEC] underline hover:text-white'>
|
||||
<Link
|
||||
href='mailto:security@sim.ai'
|
||||
className='text-[var(--brand-primary-hex)] underline hover:text-[var(--brand-primary-hover-hex)]'
|
||||
>
|
||||
security@sim.ai
|
||||
</Link>
|
||||
. The next business day, we'll acknowledge receipt of your vulnerability report and keep
|
||||
@@ -745,7 +762,7 @@ export default function PrivacyPolicy() {
|
||||
Email:{' '}
|
||||
<Link
|
||||
href='mailto:privacy@sim.ai'
|
||||
className='text-[#ECECEC] underline hover:text-white'
|
||||
className='text-[var(--brand-primary-hex)] underline hover:text-[var(--brand-primary-hover-hex)]'
|
||||
>
|
||||
privacy@sim.ai
|
||||
</Link>
|
||||
|
||||
@@ -9,8 +9,8 @@ export function BackLink() {
|
||||
|
||||
return (
|
||||
<Link
|
||||
href='/blog'
|
||||
className='group flex items-center gap-1 text-[#999] text-sm hover:text-[#ECECEC]'
|
||||
href='/studio'
|
||||
className='group flex items-center gap-1 text-gray-600 text-sm hover:text-gray-900'
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
@@ -21,7 +21,7 @@ export function BackLink() {
|
||||
<ChevronLeft className='h-4 w-4' aria-hidden='true' />
|
||||
)}
|
||||
</span>
|
||||
Back to Blog
|
||||
Back to Sim Studio
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -6,8 +6,9 @@ import { FAQ } from '@/lib/blog/faq'
|
||||
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
|
||||
import { buildArticleJsonLd, buildBreadcrumbJsonLd, buildPostMetadata } from '@/lib/blog/seo'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { BackLink } from '@/app/(landing)/blog/[slug]/back-link'
|
||||
import { ShareButton } from '@/app/(landing)/blog/[slug]/share-button'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import { BackLink } from '@/app/(landing)/studio/[slug]/back-link'
|
||||
import { ShareButton } from '@/app/(landing)/studio/[slug]/share-button'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = await getAllPostMeta()
|
||||
@@ -35,7 +36,11 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
const related = await getRelatedPosts(slug, 3)
|
||||
|
||||
return (
|
||||
<article className='w-full' itemScope itemType='https://schema.org/BlogPosting'>
|
||||
<article
|
||||
className={`${soehne.className} w-full`}
|
||||
itemScope
|
||||
itemType='https://schema.org/BlogPosting'
|
||||
>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
@@ -66,7 +71,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
</div>
|
||||
<div className='flex flex-1 flex-col justify-between'>
|
||||
<h1
|
||||
className='font-[500] text-[#ECECEC] text-[36px] leading-tight tracking-tight sm:text-[48px] md:text-[56px] lg:text-[64px]'
|
||||
className='font-medium text-[36px] leading-tight tracking-tight sm:text-[48px] md:text-[56px] lg:text-[64px]'
|
||||
itemProp='headline'
|
||||
>
|
||||
{post.title}
|
||||
@@ -85,7 +90,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
href={a?.url || '#'}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer author'
|
||||
className='text-[#999] text-[14px] leading-[1.5] hover:text-[#ECECEC] sm:text-[16px]'
|
||||
className='text-[14px] text-gray-600 leading-[1.5] hover:text-gray-900 sm:text-[16px]'
|
||||
itemProp='author'
|
||||
itemScope
|
||||
itemType='https://schema.org/Person'
|
||||
@@ -95,15 +100,15 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ShareButton url={`${getBaseUrl()}/blog/${slug}`} title={post.title} />
|
||||
<ShareButton url={`${getBaseUrl()}/studio/${slug}`} title={post.title} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr className='mt-8 border-[#2A2A2A] border-t sm:mt-12' />
|
||||
<hr className='mt-8 border-gray-200 border-t sm:mt-12' />
|
||||
<div className='flex flex-col gap-6 py-8 sm:flex-row sm:items-start sm:justify-between sm:gap-8 sm:py-10'>
|
||||
<div className='flex flex-shrink-0 items-center gap-4'>
|
||||
<time
|
||||
className='block text-[#999] text-[14px] leading-[1.5] sm:text-[16px]'
|
||||
className='block text-[14px] text-gray-600 leading-[1.5] sm:text-[16px]'
|
||||
dateTime={post.date}
|
||||
itemProp='datePublished'
|
||||
>
|
||||
@@ -116,7 +121,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
<meta itemProp='dateModified' content={post.updated ?? post.date} />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='m-0 block translate-y-[-4px] font-[400] text-[#999] text-[18px] leading-[1.5] sm:text-[20px] md:text-[26px]'>
|
||||
<p className='m-0 block translate-y-[-4px] font-[400] text-[18px] leading-[1.5] sm:text-[20px] md:text-[26px]'>
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -124,18 +129,18 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
</header>
|
||||
|
||||
<div className='mx-auto max-w-[900px] px-6 pb-20 sm:px-8 md:px-12' itemProp='articleBody'>
|
||||
<div className='prose prose-lg prose-invert max-w-none prose-blockquote:border-[#3d3d3d] prose-hr:border-[#2A2A2A] prose-a:text-[#ECECEC] prose-blockquote:text-[#999] prose-code:text-[#ECECEC] prose-headings:text-[#ECECEC] prose-li:text-[#999] prose-p:text-[#999] prose-strong:text-[#ECECEC]'>
|
||||
<div className='prose prose-lg max-w-none'>
|
||||
<Article />
|
||||
{post.faq && post.faq.length > 0 ? <FAQ items={post.faq} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
{related.length > 0 && (
|
||||
<div className='mx-auto max-w-[900px] px-6 pb-24 sm:px-8 md:px-12'>
|
||||
<h2 className='mb-4 font-[500] text-[#ECECEC] text-[24px]'>Related posts</h2>
|
||||
<h2 className='mb-4 font-medium text-[24px]'>Related posts</h2>
|
||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3'>
|
||||
{related.map((p) => (
|
||||
<Link key={p.slug} href={`/blog/${p.slug}`} className='group'>
|
||||
<div className='overflow-hidden rounded-lg border border-[#2A2A2A]'>
|
||||
<Link key={p.slug} href={`/studio/${p.slug}`} className='group'>
|
||||
<div className='overflow-hidden rounded-lg border border-gray-200'>
|
||||
<Image
|
||||
src={p.ogImage}
|
||||
alt={p.title}
|
||||
@@ -147,14 +152,14 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
unoptimized
|
||||
/>
|
||||
<div className='p-3'>
|
||||
<div className='mb-1 text-[#999] text-xs'>
|
||||
<div className='mb-1 text-gray-600 text-xs'>
|
||||
{new Date(p.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
<div className='font-[500] text-[#ECECEC] text-sm leading-tight'>{p.title}</div>
|
||||
<div className='font-medium text-sm leading-tight'>{p.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -2,12 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Share2 } from 'lucide-react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/emcn'
|
||||
import { Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@/components/emcn'
|
||||
|
||||
interface ShareButtonProps {
|
||||
url: string
|
||||
@@ -15,46 +10,56 @@ interface ShareButtonProps {
|
||||
}
|
||||
|
||||
export function ShareButton({ url, title }: ShareButtonProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
setOpen(false)
|
||||
}, 1000)
|
||||
} catch {
|
||||
/* clipboard unavailable */
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleShareTwitter = () => {
|
||||
const tweetUrl = `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`
|
||||
window.open(tweetUrl, '_blank', 'noopener,noreferrer')
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleShareLinkedIn = () => {
|
||||
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`
|
||||
window.open(linkedInUrl, '_blank', 'noopener,noreferrer')
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
colorScheme='inverted'
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className='flex items-center gap-1.5 text-[#999] text-sm hover:text-[#ECECEC]'
|
||||
className='flex items-center gap-1.5 text-gray-600 text-sm hover:text-gray-900'
|
||||
aria-label='Share this post'
|
||||
>
|
||||
<Share2 className='h-4 w-4' />
|
||||
<span>Share</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem onSelect={handleCopyLink}>
|
||||
{copied ? 'Copied!' : 'Copy link'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={handleShareTwitter}>Share on X</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={handleShareLinkedIn}>Share on LinkedIn</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align='end' minWidth={140}>
|
||||
<PopoverItem onClick={handleCopyLink}>{copied ? 'Copied!' : 'Copy link'}</PopoverItem>
|
||||
<PopoverItem onClick={handleShareTwitter}>Share on X</PopoverItem>
|
||||
<PopoverItem onClick={handleShareLinkedIn}>Share on LinkedIn</PopoverItem>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { Metadata } from 'next'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { getAllPostMeta } from '@/lib/blog/registry'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
|
||||
export const revalidate = 3600
|
||||
|
||||
@@ -22,8 +23,8 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
|
||||
const author = posts[0]?.author
|
||||
if (!author) {
|
||||
return (
|
||||
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
|
||||
<h1 className='font-[500] text-[#ECECEC] text-[32px]'>Author not found</h1>
|
||||
<main className={`${soehne.className} mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12`}>
|
||||
<h1 className='font-medium text-[32px]'>Author not found</h1>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -31,12 +32,12 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Person',
|
||||
name: author.name,
|
||||
url: `https://sim.ai/blog/authors/${author.id}`,
|
||||
url: `https://sim.ai/studio/authors/${author.id}`,
|
||||
sameAs: author.url ? [author.url] : [],
|
||||
image: author.avatarUrl,
|
||||
}
|
||||
return (
|
||||
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
|
||||
<main className={`${soehne.className} mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12`}>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(personJsonLd) }}
|
||||
@@ -52,12 +53,12 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
|
||||
unoptimized
|
||||
/>
|
||||
) : null}
|
||||
<h1 className='font-[500] text-[#ECECEC] text-[32px] leading-tight'>{author.name}</h1>
|
||||
<h1 className='font-medium text-[32px] leading-tight'>{author.name}</h1>
|
||||
</div>
|
||||
<div className='grid grid-cols-1 gap-8 sm:grid-cols-2'>
|
||||
{posts.map((p) => (
|
||||
<Link key={p.slug} href={`/blog/${p.slug}`} className='group'>
|
||||
<div className='overflow-hidden rounded-lg border border-[#2A2A2A]'>
|
||||
<Link key={p.slug} href={`/studio/${p.slug}`} className='group'>
|
||||
<div className='overflow-hidden rounded-lg border border-gray-200'>
|
||||
<Image
|
||||
src={p.ogImage}
|
||||
alt={p.title}
|
||||
@@ -67,14 +68,14 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
|
||||
unoptimized
|
||||
/>
|
||||
<div className='p-3'>
|
||||
<div className='mb-1 text-[#999] text-xs'>
|
||||
<div className='mb-1 text-gray-600 text-xs'>
|
||||
{new Date(p.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
<div className='font-[500] text-[#ECECEC] text-sm leading-tight'>{p.title}</div>
|
||||
<div className='font-medium text-sm leading-tight'>{p.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -1,9 +1,6 @@
|
||||
import { getNavBlogPosts } from '@/lib/blog/registry'
|
||||
import Footer from '@/app/(home)/components/footer/footer'
|
||||
import Navbar from '@/app/(home)/components/navbar/navbar'
|
||||
import { Footer, Nav } from '@/app/(landing)/components'
|
||||
|
||||
export default async function StudioLayout({ children }: { children: React.ReactNode }) {
|
||||
const blogPosts = await getNavBlogPosts()
|
||||
export default function StudioLayout({ children }: { children: React.ReactNode }) {
|
||||
const orgJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
@@ -26,7 +23,7 @@ export default async function StudioLayout({ children }: { children: React.React
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex min-h-screen flex-col bg-[#1C1C1C] font-[430] font-season text-[#ECECEC]'>
|
||||
<div className='flex min-h-screen flex-col'>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(orgJsonLd) }}
|
||||
@@ -35,11 +32,9 @@ export default async function StudioLayout({ children }: { children: React.React
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }}
|
||||
/>
|
||||
<header>
|
||||
<Navbar blogPosts={blogPosts} />
|
||||
</header>
|
||||
<Nav hideAuthButtons={false} variant='landing' />
|
||||
<main className='relative flex-1'>{children}</main>
|
||||
<Footer />
|
||||
<Footer fullWidth={true} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
import type { Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
import { getAllPostMeta } from '@/lib/blog/registry'
|
||||
import { PostGrid } from '@/app/(landing)/blog/post-grid'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import { PostGrid } from '@/app/(landing)/studio/post-grid'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Blog',
|
||||
title: 'Studio',
|
||||
description: 'Announcements, insights, and guides from the Sim team.',
|
||||
}
|
||||
|
||||
export const revalidate = 3600
|
||||
|
||||
export default async function BlogIndex({
|
||||
export default async function StudioIndex({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ page?: string; tag?: string }>
|
||||
@@ -36,32 +37,30 @@ export default async function BlogIndex({
|
||||
const posts = sorted.slice(start, start + perPage)
|
||||
// Tag filter chips are intentionally disabled for now.
|
||||
// const tags = await getAllTags()
|
||||
const blogJsonLd = {
|
||||
const studioJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Blog',
|
||||
name: 'Sim Blog',
|
||||
url: 'https://sim.ai/blog',
|
||||
name: 'Sim Studio',
|
||||
url: 'https://sim.ai/studio',
|
||||
description: 'Announcements, insights, and guides for building AI agent workflows.',
|
||||
}
|
||||
|
||||
return (
|
||||
<main className='mx-auto max-w-[1200px] px-6 py-12 sm:px-8 md:px-12'>
|
||||
<main className={`${soehne.className} mx-auto max-w-[1200px] px-6 py-12 sm:px-8 md:px-12`}>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(blogJsonLd) }}
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(studioJsonLd) }}
|
||||
/>
|
||||
<h1 className='mb-3 font-[500] text-[#ECECEC] text-[40px] leading-tight sm:text-[56px]'>
|
||||
Blog
|
||||
</h1>
|
||||
<p className='mb-10 text-[#999] text-[18px]'>
|
||||
<h1 className='mb-3 font-medium text-[40px] leading-tight sm:text-[56px]'>Sim Studio</h1>
|
||||
<p className='mb-10 text-[18px] text-gray-700'>
|
||||
Announcements, insights, and guides for building AI agent workflows.
|
||||
</p>
|
||||
|
||||
{/* Tag filter chips hidden until we have more posts */}
|
||||
{/* <div className='mb-10 flex flex-wrap gap-3'>
|
||||
<Link href='/blog' className={`rounded-full border px-3 py-1 text-sm ${!tag ? 'border-black bg-black text-white' : 'border-gray-300'}`}>All</Link>
|
||||
<Link href='/studio' className={`rounded-full border px-3 py-1 text-sm ${!tag ? 'border-black bg-black text-white' : 'border-gray-300'}`}>All</Link>
|
||||
{tags.map((t) => (
|
||||
<Link key={t.tag} href={`/blog?tag=${encodeURIComponent(t.tag)}`} className={`rounded-full border px-3 py-1 text-sm ${tag === t.tag ? 'border-black bg-black text-white' : 'border-gray-300'}`}>
|
||||
<Link key={t.tag} href={`/studio?tag=${encodeURIComponent(t.tag)}`} className={`rounded-full border px-3 py-1 text-sm ${tag === t.tag ? 'border-black bg-black text-white' : 'border-gray-300'}`}>
|
||||
{t.tag} ({t.count})
|
||||
</Link>
|
||||
))}
|
||||
@@ -74,19 +73,19 @@ export default async function BlogIndex({
|
||||
<div className='mt-10 flex items-center justify-center gap-3'>
|
||||
{pageNum > 1 && (
|
||||
<Link
|
||||
href={`/blog?page=${pageNum - 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
|
||||
className='rounded-[5px] border border-[#3d3d3d] px-3 py-1 text-[#ECECEC] text-sm transition-colors hover:bg-[#2A2A2A]'
|
||||
href={`/studio?page=${pageNum - 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
|
||||
className='rounded border px-3 py-1 text-sm'
|
||||
>
|
||||
Previous
|
||||
</Link>
|
||||
)}
|
||||
<span className='text-[#999] text-sm'>
|
||||
<span className='text-gray-600 text-sm'>
|
||||
Page {pageNum} of {totalPages}
|
||||
</span>
|
||||
{pageNum < totalPages && (
|
||||
<Link
|
||||
href={`/blog?page=${pageNum + 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
|
||||
className='rounded-[5px] border border-[#3d3d3d] px-3 py-1 text-[#ECECEC] text-sm transition-colors hover:bg-[#2A2A2A]'
|
||||
href={`/studio?page=${pageNum + 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
|
||||
className='rounded border px-3 py-1 text-sm'
|
||||
>
|
||||
Next
|
||||
</Link>
|
||||
@@ -26,8 +26,8 @@ export function PostGrid({ posts }: { posts: Post[] }) {
|
||||
return (
|
||||
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3'>
|
||||
{posts.map((p, index) => (
|
||||
<Link key={p.slug} href={`/blog/${p.slug}`} className='group flex flex-col'>
|
||||
<div className='flex h-full flex-col overflow-hidden rounded-xl border border-[#2A2A2A] transition-colors duration-300 hover:border-[#3d3d3d]'>
|
||||
<Link key={p.slug} href={`/studio/${p.slug}`} className='group flex flex-col'>
|
||||
<div className='flex h-full flex-col overflow-hidden rounded-xl border border-gray-200 transition-colors duration-300 hover:border-gray-300'>
|
||||
{/* Image container with fixed aspect ratio to prevent layout shift */}
|
||||
<div className='relative aspect-video w-full overflow-hidden'>
|
||||
<Image
|
||||
@@ -42,29 +42,29 @@ export function PostGrid({ posts }: { posts: Post[] }) {
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-1 flex-col p-4'>
|
||||
<div className='mb-2 text-[#999] text-xs'>
|
||||
<div className='mb-2 text-gray-600 text-xs'>
|
||||
{new Date(p.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
<h3 className='mb-1 font-[500] text-[#ECECEC] text-lg leading-tight'>{p.title}</h3>
|
||||
<p className='mb-3 line-clamp-3 flex-1 text-[#999] text-sm'>{p.description}</p>
|
||||
<h3 className='shine-text mb-1 font-medium text-lg leading-tight'>{p.title}</h3>
|
||||
<p className='mb-3 line-clamp-3 flex-1 text-gray-700 text-sm'>{p.description}</p>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='-space-x-1.5 flex'>
|
||||
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
|
||||
.slice(0, 3)
|
||||
.map((author, idx) => (
|
||||
<Avatar key={idx} className='size-4 border border-[#1C1C1C]'>
|
||||
<Avatar key={idx} className='size-4 border border-white'>
|
||||
<AvatarImage src={author?.avatarUrl} alt={author?.name} />
|
||||
<AvatarFallback className='border border-[#1C1C1C] bg-[#2A2A2A] text-[#999] text-[10px]'>
|
||||
<AvatarFallback className='border border-white bg-gray-100 text-[10px] text-gray-600'>
|
||||
{author?.name.slice(0, 2)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
</div>
|
||||
<span className='text-[#999] text-xs'>
|
||||
<span className='text-gray-600 text-xs'>
|
||||
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
|
||||
.slice(0, 2)
|
||||
.map((a) => a?.name)
|
||||
@@ -11,7 +11,7 @@ export async function GET() {
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>Sim Blog</title>
|
||||
<title>Sim Studio</title>
|
||||
<link>${site}</link>
|
||||
<description>Announcements, insights, and guides for AI agent workflows.</description>
|
||||
${items
|
||||
@@ -10,19 +10,16 @@ export default async function TagsIndex() {
|
||||
const tags = await getAllTags()
|
||||
return (
|
||||
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
|
||||
<h1 className='mb-6 font-[500] text-[#ECECEC] text-[32px] leading-tight'>Browse by tag</h1>
|
||||
<h1 className='mb-6 font-medium text-[32px] leading-tight'>Browse by tag</h1>
|
||||
<div className='flex flex-wrap gap-3'>
|
||||
<Link
|
||||
href='/blog'
|
||||
className='rounded-full border border-[#3d3d3d] px-3 py-1 text-[#ECECEC] text-sm transition-colors hover:bg-[#2A2A2A]'
|
||||
>
|
||||
<Link href='/studio' className='rounded-full border border-gray-300 px-3 py-1 text-sm'>
|
||||
All
|
||||
</Link>
|
||||
{tags.map((t) => (
|
||||
<Link
|
||||
key={t.tag}
|
||||
href={`/blog?tag=${encodeURIComponent(t.tag)}`}
|
||||
className='rounded-full border border-[#3d3d3d] px-3 py-1 text-[#ECECEC] text-sm transition-colors hover:bg-[#2A2A2A]'
|
||||
href={`/studio?tag=${encodeURIComponent(t.tag)}`}
|
||||
className='rounded-full border border-gray-300 px-3 py-1 text-sm'
|
||||
>
|
||||
{t.tag} ({t.count})
|
||||
</Link>
|
||||
@@ -1,11 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { getEnv } from '@/lib/core/config/env'
|
||||
import { ExternalRedirect, LegalLayout } from '@/app/(landing)/components'
|
||||
import { LegalLayout } from '@/app/(landing)/components'
|
||||
|
||||
export default function TermsOfService() {
|
||||
useEffect(() => {
|
||||
const termsUrl = getEnv('NEXT_PUBLIC_TERMS_URL')
|
||||
if (termsUrl?.startsWith('http')) {
|
||||
window.location.href = termsUrl
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<LegalLayout title='Terms of Service'>
|
||||
<ExternalRedirect url={getEnv('NEXT_PUBLIC_TERMS_URL') ?? ''} />
|
||||
<section>
|
||||
<p className='mb-4'>Last Updated: October 11, 2025</p>
|
||||
<p>
|
||||
@@ -281,7 +289,7 @@ export default function TermsOfService() {
|
||||
Agreement. The arbitration will be conducted by JAMS, an established alternative dispute
|
||||
resolution provider.
|
||||
</p>
|
||||
<p className='mb-4 border-[#3d3d3d] border-l-4 bg-[#2A2A2A] p-3 text-[#ECECEC]'>
|
||||
<p className='mb-4 border-[var(--brand-primary-hex)] border-l-4 bg-[var(--brand-primary-hex)]/10 p-3'>
|
||||
YOU AND COMPANY AGREE THAT EACH OF US MAY BRING CLAIMS AGAINST THE OTHER ONLY ON AN
|
||||
INDIVIDUAL BASIS AND NOT ON A CLASS, REPRESENTATIVE, OR COLLECTIVE BASIS. ONLY INDIVIDUAL
|
||||
RELIEF IS AVAILABLE, AND DISPUTES OF MORE THAN ONE CUSTOMER OR USER CANNOT BE ARBITRATED
|
||||
@@ -290,7 +298,10 @@ export default function TermsOfService() {
|
||||
<p className='mb-4'>
|
||||
You have the right to opt out of the provisions of this Arbitration Agreement by sending a
|
||||
timely written notice of your decision to opt out to:{' '}
|
||||
<Link href='mailto:legal@sim.ai' className='text-[#ECECEC] underline hover:text-white'>
|
||||
<Link
|
||||
href='mailto:legal@sim.ai'
|
||||
className='text-[var(--brand-primary-hex)] underline hover:text-[var(--brand-primary-hover-hex)]'
|
||||
>
|
||||
legal@sim.ai{' '}
|
||||
</Link>
|
||||
within 30 days after first becoming subject to this Arbitration Agreement.
|
||||
@@ -339,7 +350,7 @@ export default function TermsOfService() {
|
||||
Our Copyright Agent can be reached at:{' '}
|
||||
<Link
|
||||
href='mailto:copyright@sim.ai'
|
||||
className='text-[#ECECEC] underline hover:text-white'
|
||||
className='text-[var(--brand-primary-hex)] underline hover:text-[var(--brand-primary-hover-hex)]'
|
||||
>
|
||||
copyright@sim.ai
|
||||
</Link>
|
||||
@@ -350,7 +361,10 @@ export default function TermsOfService() {
|
||||
<h2 className='mb-4 font-semibold text-2xl'>18. Contact Us</h2>
|
||||
<p>
|
||||
If you have any questions about these Terms, please contact us at:{' '}
|
||||
<Link href='mailto:legal@sim.ai' className='text-[#ECECEC] underline hover:text-white'>
|
||||
<Link
|
||||
href='mailto:legal@sim.ai'
|
||||
className='text-[var(--brand-primary-hex)] underline hover:text-[var(--brand-primary-hover-hex)]'
|
||||
>
|
||||
legal@sim.ai
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -13,7 +13,6 @@ export type AppSession = {
|
||||
emailVerified?: boolean
|
||||
name?: string | null
|
||||
image?: string | null
|
||||
role?: string
|
||||
createdAt?: Date
|
||||
updatedAt?: Date
|
||||
} | null
|
||||
|
||||
@@ -20,7 +20,7 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
pathname.startsWith('/verify') ||
|
||||
pathname.startsWith('/changelog') ||
|
||||
pathname.startsWith('/chat') ||
|
||||
pathname.startsWith('/blog') ||
|
||||
pathname.startsWith('/studio') ||
|
||||
pathname.startsWith('/resume') ||
|
||||
pathname.startsWith('/form') ||
|
||||
pathname.startsWith('/oauth')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user